gratinglab 0.1.4__tar.gz → 0.1.5__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.4/src/gratinglab.egg-info → gratinglab-0.1.5}/PKG-INFO +3 -2
- {gratinglab-0.1.4 → gratinglab-0.1.5}/README.md +2 -1
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/__init__.py +1 -1
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/boundary/__init__.py +3 -0
- gratinglab-0.1.5/src/gratinglab/metrology/boundary/swaths.py +259 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/gui/qt/boundary_view.py +24 -0
- gratinglab-0.1.5/src/gratinglab/metrology/gui/qt/swath_dialog.py +161 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/wiki/__init__.py +1 -0
- gratinglab-0.1.5/src/gratinglab/metrology/wiki/swath-statistics.md +61 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5/src/gratinglab.egg-info}/PKG-INFO +3 -2
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab.egg-info/SOURCES.txt +3 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/LICENSE +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/pyproject.toml +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/setup.cfg +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/checks.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/compare.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/convergence.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/geometry.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/__init__.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/app.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/diagram.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/diagram3d.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/docs.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/icon.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/integral_options.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/mathtext.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/orders.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/provenance.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/qt/__init__.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/qt/gc_guard.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/qt/geometry_panel.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/qt/geometry_tab.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/qt/integral_tab.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/qt/main_window.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/qt/profile_plot_panel.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/qt/scalar_tab.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/qt/scene3d_panel.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/qt/setup_tab.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/qt/sizing.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/qt/theory_viewer.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/qt/worker.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/richtext.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/scalar_options.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/state.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/illumination.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/io/__init__.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/io/boundary.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/io/efficiency_table.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/io/ggp.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/materials/__init__.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/materials/data/SOURCES.md +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/materials/fresnel.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/materials/optical.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/materials/refractiveindex.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/__init__.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/analyzer.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/boundary/average.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/boundary/pipeline.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/cli.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/config.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/core/__init__.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/core/analysis.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/core/image_flatten.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/core/processing.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/core/tip.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/data/__init__.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/data/aggregation.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/gui/__init__.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/gui/icon.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/gui/qt/__init__.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/gui/qt/app.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/gui/qt/canvas.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/gui/qt/efficiency_dialog.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/gui/qt/import_view.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/gui/qt/main_window.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/gui/qt/wiki_view.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/gui/qt/worker.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/gui/state.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/io/__init__.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/io/file_io.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/io/metrics.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/io/spm.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/settings.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/stats/__init__.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/stats/analysis.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/stats/icc.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/visualization/__init__.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/visualization/diagnostics.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/visualization/profiles.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/visualization/statistics.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/wiki/facet-fitting.md +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/wiki/file-formats.md +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/wiki/flattening.md +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/wiki/icc-correction.md +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/wiki/overview.md +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/wiki/reading-outputs.md +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/wiki/row-groups.md +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/wiki/tip-correction.md +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/workflows.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/problem.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/profiles.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/resolution.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/result.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/solvers/__init__.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/solvers/base.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/solvers/integral/__init__.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/solvers/integral/_boundary.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/solvers/integral/_core.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/solvers/integral/_finite.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/solvers/integral/_greens.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/solvers/integral/_kernels.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/solvers/integral/_nystrom.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/solvers/scalar.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab.egg-info/dependency_links.txt +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab.egg-info/entry_points.txt +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab.egg-info/requires.txt +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab.egg-info/top_level.txt +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_checks.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_compare.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_convergence.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_cross_method.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_efficiency_table.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_fresnel.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_geometry.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_ggp.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_gui_diagram.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_gui_diagram3d.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_gui_docs.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_gui_integral_options.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_gui_mathtext.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_gui_orders.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_gui_provenance.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_gui_qt.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_gui_richtext.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_gui_scalar_options.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_gui_state.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_illumination.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_integral_core.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_integral_corpus.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_integral_finite.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_integral_greens.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_integral_kernels.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_integral_operators.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_integral_solver.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_io_boundary.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_materials.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_perturbation.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_profiles.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_refractiveindex.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_resolution.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_result.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_scalar.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_solver_conformance.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_solver_progress.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gratinglab
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: Open platform for diffraction grating groove metrology and efficiency analysis
|
|
5
5
|
Author: Jake McCoy
|
|
6
6
|
License: BSD-3-Clause
|
|
@@ -98,7 +98,8 @@ contributed and not yet integrated.
|
|
|
98
98
|
|
|
99
99
|
Two halves. **Metrology** (`gratinglab.metrology`) reads AFM scans (Nanoscope `.spm`
|
|
100
100
|
or Gwyddion text exports) and produces blaze angles per groove with row-group
|
|
101
|
-
statistics and an ICC correction, plus one averaged groove normalised to a period
|
|
101
|
+
statistics and an ICC correction, plus one averaged groove normalised to a period,
|
|
102
|
+
and swath statistics for how much that groove varies down the scan.
|
|
102
103
|
Tip convolution can be undone where the tip reached the surface, and the region it
|
|
103
104
|
never reached is marked rather than smoothed over. **Efficiency** is everything else:
|
|
104
105
|
the problem spec, the solvers, the comparison and convergence harnesses, the materials
|
|
@@ -59,7 +59,8 @@ contributed and not yet integrated.
|
|
|
59
59
|
|
|
60
60
|
Two halves. **Metrology** (`gratinglab.metrology`) reads AFM scans (Nanoscope `.spm`
|
|
61
61
|
or Gwyddion text exports) and produces blaze angles per groove with row-group
|
|
62
|
-
statistics and an ICC correction, plus one averaged groove normalised to a period
|
|
62
|
+
statistics and an ICC correction, plus one averaged groove normalised to a period,
|
|
63
|
+
and swath statistics for how much that groove varies down the scan.
|
|
63
64
|
Tip convolution can be undone where the tip reached the surface, and the region it
|
|
64
65
|
never reached is marked rather than smoothed over. **Efficiency** is everything else:
|
|
65
66
|
the problem spec, the solvers, the comparison and convergence harnesses, the materials
|
|
@@ -21,7 +21,7 @@ from .illumination import Illumination, Polarization
|
|
|
21
21
|
from .resolution import ResolvingPower, resolving_power
|
|
22
22
|
from .result import EfficiencyScan, OrderEfficiency, Provenance
|
|
23
23
|
|
|
24
|
-
__version__ = "0.1.
|
|
24
|
+
__version__ = "0.1.5"
|
|
25
25
|
|
|
26
26
|
__all__ = [
|
|
27
27
|
"Illumination",
|
|
@@ -10,6 +10,7 @@ with the blaze-angle analysis in gratinglab.metrology.core.
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
from .pipeline import BoundaryProfile, build_boundary_profile
|
|
13
|
+
from .swaths import SwathStatistics, swath_statistics
|
|
13
14
|
from .average import (
|
|
14
15
|
flatten_endpoints,
|
|
15
16
|
average_grooves,
|
|
@@ -20,6 +21,8 @@ from .average import (
|
|
|
20
21
|
__all__ = [
|
|
21
22
|
'BoundaryProfile',
|
|
22
23
|
'build_boundary_profile',
|
|
24
|
+
'SwathStatistics',
|
|
25
|
+
'swath_statistics',
|
|
23
26
|
'flatten_endpoints',
|
|
24
27
|
'average_grooves',
|
|
25
28
|
'normalize_profile',
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""Groove shape statistics across swaths: how much the shape varies down a scan.
|
|
2
|
+
|
|
3
|
+
The boundary profile averages every row of a scan into one trace and every
|
|
4
|
+
groove along it into one shape. That answers "what does the groove look like"
|
|
5
|
+
and hides "how much does it vary". This module answers the second by building
|
|
6
|
+
the same averaged groove separately for each swath (the horizontal row bands
|
|
7
|
+
the blaze-angle analysis calls row groups, with exactly its banding) and
|
|
8
|
+
comparing them.
|
|
9
|
+
|
|
10
|
+
Two spreads come out, and the comparison between them is the point:
|
|
11
|
+
|
|
12
|
+
``between_rms_nm``
|
|
13
|
+
How much the averaged groove differs from swath to swath: variation *down*
|
|
14
|
+
the image, along the length of the grooves.
|
|
15
|
+
``within_rms_nm``
|
|
16
|
+
How much individual grooves differ from their swath's average: variation
|
|
17
|
+
*along* the scan line, groove to groove.
|
|
18
|
+
|
|
19
|
+
A between-swath spread well above the within-swath one says the shape changes
|
|
20
|
+
along the grooves (a ruling or etch gradient, or a drifting scan); the reverse
|
|
21
|
+
says the grooves differ from each other more than any one groove changes along
|
|
22
|
+
its length.
|
|
23
|
+
|
|
24
|
+
Each swath's groove is aligned to the others by a circular shift before it is
|
|
25
|
+
compared, so a groove that is the same shape but displaced counts
|
|
26
|
+
as the same shape. Normalisation already rolls every groove so its minimum
|
|
27
|
+
sits at the boundary, so that alignment only removes what is left over. The
|
|
28
|
+
lateral position is measured separately, from where each swath finds its
|
|
29
|
+
grooves, and reported as ``shift_nm``: a steady drift down the image is a fact
|
|
30
|
+
about the scan (the grooves rotated relative to the fast axis, or thermal
|
|
31
|
+
drift) and should not be mistaken for a shape change.
|
|
32
|
+
|
|
33
|
+
Swaths are not independent samples: they re-measure the same grooves, so a
|
|
34
|
+
spread here describes variation along the grooves, not the uncertainty of a
|
|
35
|
+
mean. The blaze-angle row groups carry the same caveat and correct for it;
|
|
36
|
+
see the ``icc-correction`` wiki page. The ``swath-statistics`` page is the
|
|
37
|
+
reader's guide to everything here.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import contextlib
|
|
43
|
+
import io
|
|
44
|
+
from dataclasses import dataclass, field
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
|
|
47
|
+
import numpy as np
|
|
48
|
+
|
|
49
|
+
from ..core.processing import raw_data_multi_group
|
|
50
|
+
from .pipeline import BoundaryProfile, build_boundary_profile
|
|
51
|
+
|
|
52
|
+
__all__ = ["SwathStatistics", "swath_statistics"]
|
|
53
|
+
|
|
54
|
+
#: Per-swath scalars reported, from each swath's profile metrics.
|
|
55
|
+
METRICS = ("groove_depth", "peak_to_valley", "max_angle_deg", "rms_slope", "max_curvature")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class SwathStatistics:
|
|
60
|
+
"""The averaged groove of each swath, aligned, and how much they differ."""
|
|
61
|
+
|
|
62
|
+
#: The common axis, fractions of one period, and the whole-scan profile the
|
|
63
|
+
#: swaths were aligned to.
|
|
64
|
+
t: np.ndarray
|
|
65
|
+
whole: BoundaryProfile
|
|
66
|
+
#: One row per swath that produced a profile, aligned to one another.
|
|
67
|
+
profiles: np.ndarray
|
|
68
|
+
#: (first row, end row) of each of those swaths, in scan rows.
|
|
69
|
+
row_ranges: list
|
|
70
|
+
#: Lateral offset of each swath's groove from the whole-scan one, nm.
|
|
71
|
+
shift_nm: np.ndarray
|
|
72
|
+
#: Per-swath scalars and counts, one dict per swath.
|
|
73
|
+
per_swath: list
|
|
74
|
+
#: Swaths that produced no profile, as ((first, end), reason).
|
|
75
|
+
skipped: list = field(default_factory=list)
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def n_swaths(self) -> int:
|
|
79
|
+
return len(self.profiles)
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def mean(self) -> np.ndarray:
|
|
83
|
+
return self.profiles.mean(axis=0)
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def std(self) -> np.ndarray:
|
|
87
|
+
"""Pointwise spread across swaths, fractions of the period."""
|
|
88
|
+
return self.profiles.std(axis=0, ddof=1) if self.n_swaths > 1 else np.zeros_like(self.t)
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def between_rms_nm(self) -> float:
|
|
92
|
+
"""Swath-to-swath shape spread, RMS over the period, nm."""
|
|
93
|
+
return float(np.sqrt(np.mean(self.std ** 2)) * self.whole.period_nm)
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def within_rms_nm(self) -> float:
|
|
97
|
+
"""Groove-to-groove spread within a swath, RMS over the period, averaged over swaths, nm."""
|
|
98
|
+
values = [s["within_rms_nm"] for s in self.per_swath]
|
|
99
|
+
return float(np.mean(values)) if values else float("nan")
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def whole_vs_swaths_rms_nm(self) -> float:
|
|
103
|
+
"""How far the whole-scan groove sits from the swaths' mean, RMS, nm.
|
|
104
|
+
|
|
105
|
+
Large when the grooves drift sideways down the image: averaging every
|
|
106
|
+
row then blurs the shape, so the whole-scan groove, the one exported,
|
|
107
|
+
is rounder than any swath. Compare with ``shift_nm``.
|
|
108
|
+
"""
|
|
109
|
+
mean = self.mean[:-1]
|
|
110
|
+
whole, _ = _align(self.whole.y_norm[:-1], mean)
|
|
111
|
+
return float(np.sqrt(np.mean((whole - mean) ** 2)) * self.whole.period_nm)
|
|
112
|
+
|
|
113
|
+
def metric_summary(self) -> dict:
|
|
114
|
+
"""mean, std, min and max of each per-swath scalar."""
|
|
115
|
+
out = {}
|
|
116
|
+
for name in METRICS + ("period_nm",):
|
|
117
|
+
values = np.array([s[name] for s in self.per_swath], dtype=float)
|
|
118
|
+
out[name] = {
|
|
119
|
+
"mean": float(values.mean()),
|
|
120
|
+
"std": float(values.std(ddof=1)) if len(values) > 1 else 0.0,
|
|
121
|
+
"min": float(values.min()),
|
|
122
|
+
"max": float(values.max()),
|
|
123
|
+
}
|
|
124
|
+
return out
|
|
125
|
+
|
|
126
|
+
def summary(self) -> str:
|
|
127
|
+
"""A few lines for a panel or a log."""
|
|
128
|
+
m = self.metric_summary()
|
|
129
|
+
lines = [
|
|
130
|
+
f"{self.n_swaths} swaths" + (f" ({len(self.skipped)} skipped)" if self.skipped else ""),
|
|
131
|
+
f"Shape spread between swaths : {self.between_rms_nm:.2f} nm RMS",
|
|
132
|
+
f"Groove-to-groove within one : {self.within_rms_nm:.2f} nm RMS",
|
|
133
|
+
f"Whole scan vs swath mean : {self.whole_vs_swaths_rms_nm:.2f} nm RMS",
|
|
134
|
+
f"Depth : {m['groove_depth']['mean']:.4f} +/- {m['groove_depth']['std']:.4f} of period",
|
|
135
|
+
f"Max sidewall : {m['max_angle_deg']['mean']:.2f} +/- {m['max_angle_deg']['std']:.2f} deg",
|
|
136
|
+
f"Period : {m['period_nm']['mean']:.2f} +/- {m['period_nm']['std']:.2f} nm",
|
|
137
|
+
f"Lateral drift: {np.ptp(self.shift_nm):.1f} nm across the swaths",
|
|
138
|
+
]
|
|
139
|
+
return "\n".join(lines)
|
|
140
|
+
|
|
141
|
+
def write_csv(self, path) -> Path:
|
|
142
|
+
"""One row per swath: rows, shift, counts and scalars, under # comment lines."""
|
|
143
|
+
path = Path(path)
|
|
144
|
+
columns = ["first_row", "end_row", "shift_nm", "n_grooves_used", "period_nm",
|
|
145
|
+
"within_rms_nm", *METRICS]
|
|
146
|
+
lines = [",".join(columns),
|
|
147
|
+
f"# between-swath shape spread: {self.between_rms_nm!r} nm RMS",
|
|
148
|
+
f"# within-swath groove-to-groove spread: {self.within_rms_nm!r} nm RMS",
|
|
149
|
+
f"# whole-scan groove vs swath mean: {self.whole_vs_swaths_rms_nm!r} nm RMS",
|
|
150
|
+
"# swaths re-measure the same grooves, so these describe variation, not independent samples"]
|
|
151
|
+
for (first, end), shift, s in zip(self.row_ranges, self.shift_nm, self.per_swath):
|
|
152
|
+
values = [first, end, float(shift), s["n_used"], s["period_nm"], s["within_rms_nm"],
|
|
153
|
+
*(s[name] for name in METRICS)]
|
|
154
|
+
lines.append(",".join(repr(v) if isinstance(v, float) else str(v) for v in values))
|
|
155
|
+
path.write_bytes(("\n".join(lines) + "\n").encode("utf-8"))
|
|
156
|
+
return path
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def swath_statistics(data, scan_x_size, settings, n_swaths=None) -> SwathStatistics:
|
|
160
|
+
"""Build the averaged groove for each swath of ``data`` and compare them.
|
|
161
|
+
|
|
162
|
+
Parameters:
|
|
163
|
+
data: 2-D height array in metres, image-flattened by the caller, the
|
|
164
|
+
same input ``build_boundary_profile`` takes.
|
|
165
|
+
scan_x_size: scan width in microns.
|
|
166
|
+
settings: AnalysisSettings, applied to each swath exactly as to the
|
|
167
|
+
whole scan (tip correction, edge exclusion, the ggp_* fields).
|
|
168
|
+
n_swaths: how many bands; defaults to ``settings.n_row_groups``. Bands
|
|
169
|
+
are at least 3 rows, so a short scan gets fewer, as with the
|
|
170
|
+
blaze-angle row groups whose banding this reuses.
|
|
171
|
+
|
|
172
|
+
Raises:
|
|
173
|
+
ValueError if the whole scan gives no profile, or fewer than two swaths
|
|
174
|
+
do, since there is then nothing to compare.
|
|
175
|
+
"""
|
|
176
|
+
n = settings.n_row_groups if n_swaths is None else int(n_swaths)
|
|
177
|
+
whole = build_boundary_profile(data, scan_x_size, settings)
|
|
178
|
+
reference = whole.y_norm[:-1] # closed profile: the last point repeats the first
|
|
179
|
+
|
|
180
|
+
with contextlib.redirect_stdout(io.StringIO()): # it narrates its banding
|
|
181
|
+
_, _, info = raw_data_multi_group(data, scan_x_size, n_groups=n)
|
|
182
|
+
|
|
183
|
+
whole_phase = _groove_phase(whole)
|
|
184
|
+
profiles, ranges, phases, per_swath, skipped = [], [], [], [], []
|
|
185
|
+
for first, end in info["group_ranges"]:
|
|
186
|
+
try:
|
|
187
|
+
swath = build_boundary_profile(data[first:end, :], scan_x_size, settings)
|
|
188
|
+
except ValueError as exc:
|
|
189
|
+
skipped.append(((first, end), str(exc)))
|
|
190
|
+
continue
|
|
191
|
+
if len(swath.y_norm) != len(whole.y_norm):
|
|
192
|
+
skipped.append(((first, end), "profile length differs from the whole scan's"))
|
|
193
|
+
continue
|
|
194
|
+
aligned, _ = _align(swath.y_norm[:-1], reference)
|
|
195
|
+
profiles.append(np.append(aligned, aligned[0]))
|
|
196
|
+
ranges.append((first, end))
|
|
197
|
+
phases.append(_groove_phase(swath) - whole_phase)
|
|
198
|
+
per_swath.append({
|
|
199
|
+
**{name: float(swath.metrics[name]) for name in METRICS},
|
|
200
|
+
"period_nm": float(swath.period_nm),
|
|
201
|
+
"n_used": int(swath.n_used),
|
|
202
|
+
"within_rms_nm": float(np.sqrt(np.mean(np.asarray(swath.y_std_nm) ** 2))),
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
if len(profiles) < 2:
|
|
206
|
+
raise ValueError(
|
|
207
|
+
f"only {len(profiles)} of {len(info['group_ranges'])} swaths produced a "
|
|
208
|
+
"groove profile, so there is nothing to compare; use fewer swaths")
|
|
209
|
+
|
|
210
|
+
# The whole-scan groove is only a first reference: if the grooves drift
|
|
211
|
+
# down the image, averaging every row blurs it, so it is rounder than any
|
|
212
|
+
# swath. Re-align to the swaths' own mean, then roll the set so that mean
|
|
213
|
+
# has its minimum at the boundary, the convention normalisation uses.
|
|
214
|
+
closed = np.array(profiles)
|
|
215
|
+
open_ = closed[:, :-1]
|
|
216
|
+
for _ in range(2):
|
|
217
|
+
target = open_.mean(axis=0)
|
|
218
|
+
open_ = np.array([_align(y, target)[0] for y in open_])
|
|
219
|
+
open_ = np.roll(open_, -int(np.argmin(open_.mean(axis=0))), axis=1)
|
|
220
|
+
profiles = np.hstack([open_, open_[:, :1]])
|
|
221
|
+
|
|
222
|
+
return SwathStatistics(
|
|
223
|
+
t=whole.x_norm,
|
|
224
|
+
whole=whole,
|
|
225
|
+
profiles=profiles,
|
|
226
|
+
row_ranges=ranges,
|
|
227
|
+
# Unwrapped down the scan, so a drift past half a period reads as a
|
|
228
|
+
# trend rather than jumping back.
|
|
229
|
+
shift_nm=np.unwrap(np.array(phases)) / (2 * np.pi) * whole.period_nm,
|
|
230
|
+
per_swath=per_swath,
|
|
231
|
+
skipped=skipped,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _align(y: np.ndarray, reference: np.ndarray) -> tuple[np.ndarray, int]:
|
|
236
|
+
"""Circularly shift ``y`` onto ``reference``; return it and the signed shift.
|
|
237
|
+
|
|
238
|
+
The shift minimising the squared difference is the one maximising the
|
|
239
|
+
circular cross-correlation, found by FFT. Signed, so a drift down the
|
|
240
|
+
image reads as a trend rather than wrapping.
|
|
241
|
+
"""
|
|
242
|
+
a = y - y.mean()
|
|
243
|
+
b = reference - reference.mean()
|
|
244
|
+
correlation = np.fft.irfft(np.fft.rfft(b) * np.conj(np.fft.rfft(a)), n=len(a))
|
|
245
|
+
k = int(np.argmax(correlation))
|
|
246
|
+
if k > len(a) // 2:
|
|
247
|
+
k -= len(a)
|
|
248
|
+
return np.roll(y, k), k
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _groove_phase(profile: BoundaryProfile) -> float:
|
|
252
|
+
"""Where the grooves sit along the scan line, as a phase of the period.
|
|
253
|
+
|
|
254
|
+
The circular mean of the groove centres, so it needs no matching of one
|
|
255
|
+
swath's grooves to another's and does not care which grooves the edge
|
|
256
|
+
exclusion dropped.
|
|
257
|
+
"""
|
|
258
|
+
x_nm = np.asarray(profile.profile_x_um)[np.asarray(profile.groove_centers)] * 1000.0
|
|
259
|
+
return float(np.angle(np.mean(np.exp(2j * np.pi * x_nm / profile.period_nm))))
|
|
@@ -164,6 +164,14 @@ class BoundaryView(QWidget):
|
|
|
164
164
|
"this scan measured already filled in.")
|
|
165
165
|
self.send_btn.clicked.connect(self.send_to_efficiency)
|
|
166
166
|
layout.addWidget(self.send_btn)
|
|
167
|
+
|
|
168
|
+
self.swaths_btn = QPushButton("Swath statistics…")
|
|
169
|
+
self.swaths_btn.setEnabled(False)
|
|
170
|
+
self.swaths_btn.setToolTip(
|
|
171
|
+
"Build the groove separately for each horizontal band of rows and "
|
|
172
|
+
"show how much the shape varies down the scan.")
|
|
173
|
+
self.swaths_btn.clicked.connect(self.show_swaths)
|
|
174
|
+
layout.addWidget(self.swaths_btn)
|
|
167
175
|
self._efficiency_window = None
|
|
168
176
|
return group
|
|
169
177
|
|
|
@@ -300,6 +308,7 @@ class BoundaryView(QWidget):
|
|
|
300
308
|
self.export_btn.setEnabled(enabled)
|
|
301
309
|
self.efficiency_btn.setEnabled(enabled)
|
|
302
310
|
self.send_btn.setEnabled(enabled)
|
|
311
|
+
self.swaths_btn.setEnabled(enabled)
|
|
303
312
|
|
|
304
313
|
# ── Out to a solver ──────────────────────────────────────────────────────
|
|
305
314
|
|
|
@@ -319,6 +328,21 @@ class BoundaryView(QWidget):
|
|
|
319
328
|
|
|
320
329
|
EfficiencyDialog(self._profile, self).exec()
|
|
321
330
|
|
|
331
|
+
def swath_dialog(self):
|
|
332
|
+
"""A SwathDialog for the current scan and settings, or None."""
|
|
333
|
+
if self._profile is None:
|
|
334
|
+
return None
|
|
335
|
+
from .swath_dialog import SwathDialog
|
|
336
|
+
|
|
337
|
+
settings = self._settings.with_(**self.settings_overrides())
|
|
338
|
+
return SwathDialog(self._data, self._scan_size, settings, self._filename, self)
|
|
339
|
+
|
|
340
|
+
def show_swaths(self):
|
|
341
|
+
"""How much the groove varies from swath to swath down the scan."""
|
|
342
|
+
dialog = self.swath_dialog()
|
|
343
|
+
if dialog is not None:
|
|
344
|
+
dialog.exec()
|
|
345
|
+
|
|
322
346
|
# ── Export ───────────────────────────────────────────────────────────────
|
|
323
347
|
|
|
324
348
|
#: File-dialog filters, and the suffix each one writes.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""
|
|
2
|
+
How much the averaged groove varies from swath to swath down the scan.
|
|
3
|
+
|
|
4
|
+
Opened from the Boundary tab. The tab averages every row into one groove; this
|
|
5
|
+
splits the rows into swaths, the same bands the blaze-angle analysis calls row
|
|
6
|
+
groups, builds the groove for each, and shows how far they disagree.
|
|
7
|
+
|
|
8
|
+
Computation lives in ``gratinglab.metrology.boundary.swath_statistics``.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
from . import * # noqa: F401,F403 - sets QT_API before matplotlib's shim loads
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
from PySide6.QtWidgets import (
|
|
18
|
+
QDialog, QFileDialog, QHBoxLayout, QLabel, QMessageBox, QPushButton,
|
|
19
|
+
QSpinBox, QVBoxLayout, QWidget,
|
|
20
|
+
)
|
|
21
|
+
from matplotlib import colormaps
|
|
22
|
+
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
|
|
23
|
+
from matplotlib.figure import Figure
|
|
24
|
+
|
|
25
|
+
from ...boundary import swath_statistics
|
|
26
|
+
from ...config import RESULTS_DIR
|
|
27
|
+
|
|
28
|
+
__all__ = ["SwathDialog"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SwathDialog(QDialog):
|
|
32
|
+
"""Per-swath groove shapes, their spread, and the scalars down the scan."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, data, scan_size, settings, filename=None,
|
|
35
|
+
parent: QWidget | None = None) -> None:
|
|
36
|
+
super().__init__(parent)
|
|
37
|
+
self._data = data
|
|
38
|
+
self._scan_size = scan_size
|
|
39
|
+
self._settings = settings
|
|
40
|
+
self._filename = filename
|
|
41
|
+
self._stats = None
|
|
42
|
+
|
|
43
|
+
self.setWindowTitle("Groove shape across swaths")
|
|
44
|
+
self.resize(980, 640)
|
|
45
|
+
self._build_ui()
|
|
46
|
+
self.recompute()
|
|
47
|
+
|
|
48
|
+
def _build_ui(self):
|
|
49
|
+
root = QHBoxLayout(self)
|
|
50
|
+
root.setContentsMargins(8, 8, 8, 8)
|
|
51
|
+
|
|
52
|
+
left = QWidget()
|
|
53
|
+
left.setFixedWidth(300)
|
|
54
|
+
column = QVBoxLayout(left)
|
|
55
|
+
column.setContentsMargins(0, 0, 0, 0)
|
|
56
|
+
|
|
57
|
+
row = QHBoxLayout()
|
|
58
|
+
row.addWidget(QLabel("Swaths:"))
|
|
59
|
+
self.n_spin = QSpinBox()
|
|
60
|
+
self.n_spin.setRange(2, 100)
|
|
61
|
+
self.n_spin.setValue(max(2, int(self._settings.n_row_groups)))
|
|
62
|
+
self.n_spin.setToolTip(
|
|
63
|
+
"Horizontal bands of rows, each averaged into its own groove. "
|
|
64
|
+
"At least 3 rows each, so a short scan gets fewer.")
|
|
65
|
+
self.n_spin.valueChanged.connect(self.recompute)
|
|
66
|
+
row.addWidget(self.n_spin)
|
|
67
|
+
column.addLayout(row)
|
|
68
|
+
|
|
69
|
+
self.summary_label = QLabel("")
|
|
70
|
+
self.summary_label.setWordWrap(True)
|
|
71
|
+
self.summary_label.setStyleSheet("font-family: monospace; font-size: 11px;")
|
|
72
|
+
column.addWidget(self.summary_label)
|
|
73
|
+
|
|
74
|
+
note = QLabel(
|
|
75
|
+
"Between swaths: how much the groove changes along its length, down "
|
|
76
|
+
"the image. Within one: how much grooves differ side by side. "
|
|
77
|
+
"Swaths re-measure the same grooves, so these describe variation, "
|
|
78
|
+
"not independent samples.")
|
|
79
|
+
note.setWordWrap(True)
|
|
80
|
+
note.setStyleSheet("font-size: 11px; color: gray;")
|
|
81
|
+
column.addWidget(note)
|
|
82
|
+
|
|
83
|
+
self.export_btn = QPushButton("Export per-swath CSV…")
|
|
84
|
+
self.export_btn.clicked.connect(self.export)
|
|
85
|
+
column.addWidget(self.export_btn)
|
|
86
|
+
column.addStretch(1)
|
|
87
|
+
root.addWidget(left)
|
|
88
|
+
|
|
89
|
+
self.figure = Figure(figsize=(7, 6), layout="constrained")
|
|
90
|
+
self.canvas = FigureCanvas(self.figure)
|
|
91
|
+
root.addWidget(self.canvas, stretch=1)
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def stats(self):
|
|
95
|
+
"""The current SwathStatistics, or None if the scan gave too few swaths."""
|
|
96
|
+
return self._stats
|
|
97
|
+
|
|
98
|
+
def recompute(self):
|
|
99
|
+
try:
|
|
100
|
+
self._stats = swath_statistics(
|
|
101
|
+
self._data, self._scan_size, self._settings, n_swaths=self.n_spin.value())
|
|
102
|
+
except ValueError as exc:
|
|
103
|
+
self._stats = None
|
|
104
|
+
self.export_btn.setEnabled(False)
|
|
105
|
+
self.summary_label.setText(f"No swath statistics:\n{exc}")
|
|
106
|
+
self.figure.clear()
|
|
107
|
+
self.canvas.draw()
|
|
108
|
+
return
|
|
109
|
+
self.export_btn.setEnabled(True)
|
|
110
|
+
text = self._stats.summary()
|
|
111
|
+
if self._stats.skipped:
|
|
112
|
+
text += "\n\nSkipped rows " + ", ".join(
|
|
113
|
+
f"{a}-{b - 1}" for (a, b), _ in self._stats.skipped)
|
|
114
|
+
self.summary_label.setText(text)
|
|
115
|
+
self._draw()
|
|
116
|
+
|
|
117
|
+
def _draw(self):
|
|
118
|
+
s = self._stats
|
|
119
|
+
self.figure.clear()
|
|
120
|
+
grid = self.figure.add_gridspec(2, 3, height_ratios=[1.6, 1])
|
|
121
|
+
shapes = self.figure.add_subplot(grid[0, :])
|
|
122
|
+
colours = colormaps["viridis"](np.linspace(0.0, 0.9, s.n_swaths))
|
|
123
|
+
for profile, colour, (first, end) in zip(s.profiles, colours, s.row_ranges):
|
|
124
|
+
shapes.plot(s.t, profile, color=colour, lw=0.8, alpha=0.8)
|
|
125
|
+
shapes.fill_between(s.t, s.mean - 2 * s.std, s.mean + 2 * s.std,
|
|
126
|
+
color="0.6", alpha=0.35, lw=0, label="±2σ between swaths")
|
|
127
|
+
shapes.plot(s.t, s.whole.y_norm, color="k", lw=1.4, ls="--", label="whole scan")
|
|
128
|
+
shapes.set_xlabel("position (fraction of period)")
|
|
129
|
+
shapes.set_ylabel("height (fraction of period)")
|
|
130
|
+
shapes.set_title(f"{s.n_swaths} swaths, coloured top to bottom of the scan")
|
|
131
|
+
shapes.legend(loc="best", fontsize=8)
|
|
132
|
+
|
|
133
|
+
centre = [(a + b - 1) / 2 for a, b in s.row_ranges]
|
|
134
|
+
panels = (
|
|
135
|
+
("groove_depth", "depth (frac. of period)"),
|
|
136
|
+
("max_angle_deg", "max sidewall (deg)"),
|
|
137
|
+
)
|
|
138
|
+
for i, (key, label) in enumerate(panels):
|
|
139
|
+
ax = self.figure.add_subplot(grid[1, i])
|
|
140
|
+
ax.plot(centre, [p[key] for p in s.per_swath], "o-", ms=3)
|
|
141
|
+
ax.set_xlabel("scan row")
|
|
142
|
+
ax.set_ylabel(label)
|
|
143
|
+
ax = self.figure.add_subplot(grid[1, 2])
|
|
144
|
+
ax.plot(centre, s.shift_nm, "o-", ms=3)
|
|
145
|
+
ax.set_xlabel("scan row")
|
|
146
|
+
ax.set_ylabel("lateral shift (nm)")
|
|
147
|
+
self.canvas.draw()
|
|
148
|
+
|
|
149
|
+
def export(self):
|
|
150
|
+
if self._stats is None:
|
|
151
|
+
return
|
|
152
|
+
stem = os.path.splitext(os.path.basename(self._filename or "scan"))[0]
|
|
153
|
+
start = os.path.join(str(RESULTS_DIR), f"{stem}_swaths.csv")
|
|
154
|
+
path, _ = QFileDialog.getSaveFileName(
|
|
155
|
+
self, "Export per-swath statistics", start, "CSV table (*.csv)")
|
|
156
|
+
if not path:
|
|
157
|
+
return
|
|
158
|
+
try:
|
|
159
|
+
self._stats.write_csv(path)
|
|
160
|
+
except OSError as exc:
|
|
161
|
+
QMessageBox.critical(self, "Export failed", str(exc))
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Swath statistics
|
|
2
|
+
|
|
3
|
+
The boundary profile averages every row of the scan into one trace, and every
|
|
4
|
+
groove along that trace into one shape. That is the right thing to export, and
|
|
5
|
+
it hides how much the groove varies. **Swath statistics** builds the same
|
|
6
|
+
averaged groove separately for each horizontal band of rows (the same bands the
|
|
7
|
+
blaze-angle analysis calls row groups) and compares them.
|
|
8
|
+
|
|
9
|
+
In the metrology window: load a scan, open the **Boundary** tab and press
|
|
10
|
+
**Swath statistics…**. The dialog uses the Boundary tab's settings and the tip
|
|
11
|
+
correction, so each swath is built exactly as the exported profile is. The
|
|
12
|
+
swath count starts at the row-group count and can be changed there.
|
|
13
|
+
|
|
14
|
+
## The two spreads
|
|
15
|
+
|
|
16
|
+
| Number | Means |
|
|
17
|
+
|---|---|
|
|
18
|
+
| Shape spread between swaths | How much the averaged groove changes **down the image**, along the length of the grooves. RMS over the period, nm. |
|
|
19
|
+
| Groove-to-groove within one | How much individual grooves differ **side by side**, from their swath's average. |
|
|
20
|
+
| Whole scan vs swath mean | How far the exported whole-scan groove sits from the swaths' mean. |
|
|
21
|
+
|
|
22
|
+
A between-swath spread well above the within-swath one says the shape changes
|
|
23
|
+
along the grooves: a ruling or etch gradient, or a scan that changed as it went.
|
|
24
|
+
The reverse says the grooves differ from one another more than any one groove
|
|
25
|
+
changes along its length.
|
|
26
|
+
|
|
27
|
+
## Drift, and why the exported groove can be rounder than every swath
|
|
28
|
+
|
|
29
|
+
Each swath's groove is aligned to the others before they are compared, so a
|
|
30
|
+
groove that has the same shape but sits a little to one side counts as the same
|
|
31
|
+
shape. The sideways position is reported separately as **lateral shift**,
|
|
32
|
+
measured from where each swath finds its grooves.
|
|
33
|
+
|
|
34
|
+
A steady shift down the image means the grooves are not parallel to the slow
|
|
35
|
+
scan axis (the sample is rotated slightly) or the scan drifted thermally. It
|
|
36
|
+
matters for the export: the whole-scan profile averages rows whose grooves sit
|
|
37
|
+
in different places, so it **blurs** the shape. On the synthetic grating, 20 nm
|
|
38
|
+
of drift across the scan makes the whole-scan groove depth read about 5% low,
|
|
39
|
+
while every swath reads correctly. That is what **Whole scan vs swath mean**
|
|
40
|
+
catches. If it is large and the shift plot is a slope, rotate the image or scan
|
|
41
|
+
before trusting the exported depth and angles.
|
|
42
|
+
|
|
43
|
+
## Not independent samples
|
|
44
|
+
|
|
45
|
+
Swaths re-measure the same grooves, so their spread describes how the groove
|
|
46
|
+
varies, not the uncertainty of a mean. Do not divide it by the square root of
|
|
47
|
+
the swath count. The blaze-angle row groups have the same problem and correct
|
|
48
|
+
for it; see the ICC correction page.
|
|
49
|
+
|
|
50
|
+
## Export
|
|
51
|
+
|
|
52
|
+
**Export per-swath CSV…** writes one row per swath: its rows, lateral shift,
|
|
53
|
+
grooves used, period, groove-to-groove spread and the profile metrics, with the
|
|
54
|
+
three spreads above as `#` comment lines under the header. From Python:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from gratinglab.metrology.boundary import swath_statistics
|
|
58
|
+
stats = swath_statistics(data, scan_x_um, settings, n_swaths=8)
|
|
59
|
+
print(stats.summary())
|
|
60
|
+
stats.write_csv("swaths.csv")
|
|
61
|
+
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gratinglab
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: Open platform for diffraction grating groove metrology and efficiency analysis
|
|
5
5
|
Author: Jake McCoy
|
|
6
6
|
License: BSD-3-Clause
|
|
@@ -98,7 +98,8 @@ contributed and not yet integrated.
|
|
|
98
98
|
|
|
99
99
|
Two halves. **Metrology** (`gratinglab.metrology`) reads AFM scans (Nanoscope `.spm`
|
|
100
100
|
or Gwyddion text exports) and produces blaze angles per groove with row-group
|
|
101
|
-
statistics and an ICC correction, plus one averaged groove normalised to a period
|
|
101
|
+
statistics and an ICC correction, plus one averaged groove normalised to a period,
|
|
102
|
+
and swath statistics for how much that groove varies down the scan.
|
|
102
103
|
Tip convolution can be undone where the tip reached the surface, and the region it
|
|
103
104
|
never reached is marked rather than smoothed over. **Efficiency** is everything else:
|
|
104
105
|
the problem spec, the solvers, the comparison and convergence harnesses, the materials
|
|
@@ -61,6 +61,7 @@ src/gratinglab/metrology/workflows.py
|
|
|
61
61
|
src/gratinglab/metrology/boundary/__init__.py
|
|
62
62
|
src/gratinglab/metrology/boundary/average.py
|
|
63
63
|
src/gratinglab/metrology/boundary/pipeline.py
|
|
64
|
+
src/gratinglab/metrology/boundary/swaths.py
|
|
64
65
|
src/gratinglab/metrology/core/__init__.py
|
|
65
66
|
src/gratinglab/metrology/core/analysis.py
|
|
66
67
|
src/gratinglab/metrology/core/image_flatten.py
|
|
@@ -78,6 +79,7 @@ src/gratinglab/metrology/gui/qt/canvas.py
|
|
|
78
79
|
src/gratinglab/metrology/gui/qt/efficiency_dialog.py
|
|
79
80
|
src/gratinglab/metrology/gui/qt/import_view.py
|
|
80
81
|
src/gratinglab/metrology/gui/qt/main_window.py
|
|
82
|
+
src/gratinglab/metrology/gui/qt/swath_dialog.py
|
|
81
83
|
src/gratinglab/metrology/gui/qt/wiki_view.py
|
|
82
84
|
src/gratinglab/metrology/gui/qt/worker.py
|
|
83
85
|
src/gratinglab/metrology/io/__init__.py
|
|
@@ -99,6 +101,7 @@ src/gratinglab/metrology/wiki/icc-correction.md
|
|
|
99
101
|
src/gratinglab/metrology/wiki/overview.md
|
|
100
102
|
src/gratinglab/metrology/wiki/reading-outputs.md
|
|
101
103
|
src/gratinglab/metrology/wiki/row-groups.md
|
|
104
|
+
src/gratinglab/metrology/wiki/swath-statistics.md
|
|
102
105
|
src/gratinglab/metrology/wiki/tip-correction.md
|
|
103
106
|
src/gratinglab/solvers/__init__.py
|
|
104
107
|
src/gratinglab/solvers/base.py
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|