gratinglab 0.1.4__tar.gz → 0.1.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.1.4/src/gratinglab.egg-info → gratinglab-0.1.6}/PKG-INFO +5 -3
- {gratinglab-0.1.4 → gratinglab-0.1.6}/README.md +4 -2
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/__init__.py +1 -1
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/boundary/__init__.py +3 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/boundary/pipeline.py +2 -0
- gratinglab-0.1.6/src/gratinglab/metrology/boundary/swaths.py +259 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/config.py +8 -2
- gratinglab-0.1.6/src/gratinglab/metrology/core/tip.py +340 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/gui/qt/boundary_view.py +26 -1
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/gui/qt/main_window.py +130 -11
- gratinglab-0.1.6/src/gratinglab/metrology/gui/qt/swath_dialog.py +161 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/gui/state.py +4 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/io/metrics.py +3 -1
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/settings.py +21 -9
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/wiki/__init__.py +1 -0
- gratinglab-0.1.6/src/gratinglab/metrology/wiki/swath-statistics.md +61 -0
- gratinglab-0.1.6/src/gratinglab/metrology/wiki/tip-correction.md +173 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/workflows.py +2 -1
- {gratinglab-0.1.4 → gratinglab-0.1.6/src/gratinglab.egg-info}/PKG-INFO +5 -3
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab.egg-info/SOURCES.txt +3 -0
- gratinglab-0.1.4/src/gratinglab/metrology/core/tip.py +0 -233
- gratinglab-0.1.4/src/gratinglab/metrology/wiki/tip-correction.md +0 -113
- {gratinglab-0.1.4 → gratinglab-0.1.6}/LICENSE +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/pyproject.toml +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/setup.cfg +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/checks.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/compare.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/convergence.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/geometry.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/gui/__init__.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/gui/app.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/gui/diagram.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/gui/diagram3d.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/gui/docs.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/gui/icon.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/gui/integral_options.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/gui/mathtext.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/gui/orders.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/gui/provenance.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/gui/qt/__init__.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/gui/qt/gc_guard.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/gui/qt/geometry_panel.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/gui/qt/geometry_tab.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/gui/qt/integral_tab.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/gui/qt/main_window.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/gui/qt/profile_plot_panel.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/gui/qt/scalar_tab.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/gui/qt/scene3d_panel.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/gui/qt/setup_tab.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/gui/qt/sizing.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/gui/qt/theory_viewer.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/gui/qt/worker.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/gui/richtext.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/gui/scalar_options.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/gui/state.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/illumination.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/io/__init__.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/io/boundary.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/io/efficiency_table.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/io/ggp.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/materials/__init__.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/materials/data/SOURCES.md +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/materials/fresnel.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/materials/optical.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/materials/refractiveindex.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/__init__.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/analyzer.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/boundary/average.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/cli.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/core/__init__.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/core/analysis.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/core/image_flatten.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/core/processing.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/data/__init__.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/data/aggregation.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/gui/__init__.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/gui/icon.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/gui/qt/__init__.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/gui/qt/app.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/gui/qt/canvas.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/gui/qt/efficiency_dialog.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/gui/qt/import_view.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/gui/qt/wiki_view.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/gui/qt/worker.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/io/__init__.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/io/file_io.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/io/spm.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/stats/__init__.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/stats/analysis.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/stats/icc.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/visualization/__init__.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/visualization/diagnostics.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/visualization/profiles.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/visualization/statistics.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/wiki/facet-fitting.md +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/wiki/file-formats.md +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/wiki/flattening.md +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/wiki/icc-correction.md +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/wiki/overview.md +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/wiki/reading-outputs.md +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/metrology/wiki/row-groups.md +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/problem.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/profiles.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/resolution.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/result.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/solvers/__init__.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/solvers/base.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/solvers/integral/__init__.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/solvers/integral/_boundary.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/solvers/integral/_core.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/solvers/integral/_finite.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/solvers/integral/_greens.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/solvers/integral/_kernels.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/solvers/integral/_nystrom.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab/solvers/scalar.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab.egg-info/dependency_links.txt +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab.egg-info/entry_points.txt +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab.egg-info/requires.txt +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/src/gratinglab.egg-info/top_level.txt +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_checks.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_compare.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_convergence.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_cross_method.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_efficiency_table.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_fresnel.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_geometry.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_ggp.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_gui_diagram.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_gui_diagram3d.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_gui_docs.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_gui_integral_options.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_gui_mathtext.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_gui_orders.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_gui_provenance.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_gui_qt.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_gui_richtext.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_gui_scalar_options.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_gui_state.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_illumination.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_integral_core.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_integral_corpus.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_integral_finite.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_integral_greens.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_integral_kernels.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_integral_operators.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_integral_solver.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_io_boundary.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_materials.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_perturbation.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_profiles.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_refractiveindex.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_resolution.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_result.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_scalar.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/tests/test_solver_conformance.py +0 -0
- {gratinglab-0.1.4 → gratinglab-0.1.6}/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.6
|
|
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
|
|
@@ -150,7 +151,8 @@ formula is not used to approximate it.
|
|
|
150
151
|
### The tip is not the surface
|
|
151
152
|
|
|
152
153
|
An AFM image is the surface dilated by the tip. `tip_correction='erosion'` inverts that
|
|
153
|
-
where the inversion exists (a
|
|
154
|
+
where the inversion exists (a spherical apex on two flanks that may differ, so a
|
|
155
|
+
pyramid such as Bruker's ScanAsyst-Air can be modelled; Villarrubia 1997) and
|
|
154
156
|
reports a **certainty map** for where it does not. No algorithm can recover a trough
|
|
155
157
|
the tip never entered, and a correction that filled it in silently would be worse than
|
|
156
158
|
no correction.
|
|
@@ -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
|
|
@@ -111,7 +112,8 @@ formula is not used to approximate it.
|
|
|
111
112
|
### The tip is not the surface
|
|
112
113
|
|
|
113
114
|
An AFM image is the surface dilated by the tip. `tip_correction='erosion'` inverts that
|
|
114
|
-
where the inversion exists (a
|
|
115
|
+
where the inversion exists (a spherical apex on two flanks that may differ, so a
|
|
116
|
+
pyramid such as Bruker's ScanAsyst-Air can be modelled; Villarrubia 1997) and
|
|
115
117
|
reports a **certainty map** for where it does not. No algorithm can recover a trough
|
|
116
118
|
the tip never entered, and a correction that filled it in silently would be worse than
|
|
117
119
|
no correction.
|
|
@@ -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.6"
|
|
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',
|
|
@@ -192,6 +192,8 @@ def build_boundary_profile(data, scan_x_size, settings) -> BoundaryProfile:
|
|
|
192
192
|
if tip is not None:
|
|
193
193
|
metrics['tip_radius_nm'] = tip.radius_nm
|
|
194
194
|
metrics['tip_half_angle_deg'] = tip.half_angle_deg
|
|
195
|
+
if not tip.symmetric:
|
|
196
|
+
metrics['tip_half_angle_right_deg'] = tip.half_angle_right_deg
|
|
195
197
|
metrics['tip_certain_fraction'] = tip.certain_fraction
|
|
196
198
|
|
|
197
199
|
return BoundaryProfile(
|
|
@@ -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))))
|
|
@@ -102,8 +102,14 @@ IMAGE_FLATTEN_METHOD = 'align_rows'
|
|
|
102
102
|
# land, and a facet steeper than the tip flank (90 - half angle from the
|
|
103
103
|
# surface) is unrecoverable no matter the algorithm.
|
|
104
104
|
TIP_CORRECTION = 'none' # 'none' or 'erosion'
|
|
105
|
-
TIP_RADIUS_NM = 1.0 # Apex
|
|
106
|
-
TIP_HALF_ANGLE_DEG = 18.0 #
|
|
105
|
+
TIP_RADIUS_NM = 1.0 # Apex RADIUS; check a datasheet says radius, not width
|
|
106
|
+
TIP_HALF_ANGLE_DEG = 18.0 # Half angle from the tip axis, the -x flank
|
|
107
|
+
TIP_HALF_ANGLE_RIGHT_DEG = None # The +x flank; None for a symmetric cone.
|
|
108
|
+
# A pyramid tip meets the profile with different faces depending on how the
|
|
109
|
+
# cantilever lies against the fast-scan axis. Bruker ScanAsyst-Air: R = 2 nm
|
|
110
|
+
# nominal (12 max), front 15, back 25, side 17.5 deg, each +/- 2.5. Across the
|
|
111
|
+
# cantilever that is 17.5 / 17.5; along it 15 / 25 or 25 / 15 by orientation.
|
|
112
|
+
# core/tip.py TIP_PRESETS names all three.
|
|
107
113
|
|
|
108
114
|
# ============ PROFILE FLATTENING (1-D, after row averaging) ============
|
|
109
115
|
# This is the one that moves the answer: about 0.49 deg between methods.
|