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.
Files changed (154) hide show
  1. {gratinglab-0.1.4/src/gratinglab.egg-info → gratinglab-0.1.5}/PKG-INFO +3 -2
  2. {gratinglab-0.1.4 → gratinglab-0.1.5}/README.md +2 -1
  3. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/__init__.py +1 -1
  4. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/boundary/__init__.py +3 -0
  5. gratinglab-0.1.5/src/gratinglab/metrology/boundary/swaths.py +259 -0
  6. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/gui/qt/boundary_view.py +24 -0
  7. gratinglab-0.1.5/src/gratinglab/metrology/gui/qt/swath_dialog.py +161 -0
  8. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/wiki/__init__.py +1 -0
  9. gratinglab-0.1.5/src/gratinglab/metrology/wiki/swath-statistics.md +61 -0
  10. {gratinglab-0.1.4 → gratinglab-0.1.5/src/gratinglab.egg-info}/PKG-INFO +3 -2
  11. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab.egg-info/SOURCES.txt +3 -0
  12. {gratinglab-0.1.4 → gratinglab-0.1.5}/LICENSE +0 -0
  13. {gratinglab-0.1.4 → gratinglab-0.1.5}/pyproject.toml +0 -0
  14. {gratinglab-0.1.4 → gratinglab-0.1.5}/setup.cfg +0 -0
  15. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/checks.py +0 -0
  16. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/compare.py +0 -0
  17. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/convergence.py +0 -0
  18. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/geometry.py +0 -0
  19. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/__init__.py +0 -0
  20. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/app.py +0 -0
  21. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/diagram.py +0 -0
  22. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/diagram3d.py +0 -0
  23. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/docs.py +0 -0
  24. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/icon.py +0 -0
  25. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/integral_options.py +0 -0
  26. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/mathtext.py +0 -0
  27. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/orders.py +0 -0
  28. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/provenance.py +0 -0
  29. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/qt/__init__.py +0 -0
  30. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/qt/gc_guard.py +0 -0
  31. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/qt/geometry_panel.py +0 -0
  32. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/qt/geometry_tab.py +0 -0
  33. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/qt/integral_tab.py +0 -0
  34. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/qt/main_window.py +0 -0
  35. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/qt/profile_plot_panel.py +0 -0
  36. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/qt/scalar_tab.py +0 -0
  37. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/qt/scene3d_panel.py +0 -0
  38. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/qt/setup_tab.py +0 -0
  39. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/qt/sizing.py +0 -0
  40. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/qt/theory_viewer.py +0 -0
  41. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/qt/worker.py +0 -0
  42. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/richtext.py +0 -0
  43. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/scalar_options.py +0 -0
  44. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/gui/state.py +0 -0
  45. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/illumination.py +0 -0
  46. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/io/__init__.py +0 -0
  47. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/io/boundary.py +0 -0
  48. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/io/efficiency_table.py +0 -0
  49. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/io/ggp.py +0 -0
  50. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/materials/__init__.py +0 -0
  51. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/materials/data/SOURCES.md +0 -0
  52. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/materials/fresnel.py +0 -0
  53. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/materials/optical.py +0 -0
  54. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/materials/refractiveindex.py +0 -0
  55. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/__init__.py +0 -0
  56. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/analyzer.py +0 -0
  57. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/boundary/average.py +0 -0
  58. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/boundary/pipeline.py +0 -0
  59. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/cli.py +0 -0
  60. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/config.py +0 -0
  61. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/core/__init__.py +0 -0
  62. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/core/analysis.py +0 -0
  63. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/core/image_flatten.py +0 -0
  64. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/core/processing.py +0 -0
  65. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/core/tip.py +0 -0
  66. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/data/__init__.py +0 -0
  67. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/data/aggregation.py +0 -0
  68. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/gui/__init__.py +0 -0
  69. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/gui/icon.py +0 -0
  70. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/gui/qt/__init__.py +0 -0
  71. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/gui/qt/app.py +0 -0
  72. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/gui/qt/canvas.py +0 -0
  73. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/gui/qt/efficiency_dialog.py +0 -0
  74. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/gui/qt/import_view.py +0 -0
  75. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/gui/qt/main_window.py +0 -0
  76. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/gui/qt/wiki_view.py +0 -0
  77. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/gui/qt/worker.py +0 -0
  78. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/gui/state.py +0 -0
  79. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/io/__init__.py +0 -0
  80. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/io/file_io.py +0 -0
  81. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/io/metrics.py +0 -0
  82. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/io/spm.py +0 -0
  83. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/settings.py +0 -0
  84. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/stats/__init__.py +0 -0
  85. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/stats/analysis.py +0 -0
  86. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/stats/icc.py +0 -0
  87. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/visualization/__init__.py +0 -0
  88. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/visualization/diagnostics.py +0 -0
  89. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/visualization/profiles.py +0 -0
  90. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/visualization/statistics.py +0 -0
  91. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/wiki/facet-fitting.md +0 -0
  92. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/wiki/file-formats.md +0 -0
  93. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/wiki/flattening.md +0 -0
  94. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/wiki/icc-correction.md +0 -0
  95. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/wiki/overview.md +0 -0
  96. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/wiki/reading-outputs.md +0 -0
  97. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/wiki/row-groups.md +0 -0
  98. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/wiki/tip-correction.md +0 -0
  99. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/metrology/workflows.py +0 -0
  100. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/problem.py +0 -0
  101. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/profiles.py +0 -0
  102. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/resolution.py +0 -0
  103. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/result.py +0 -0
  104. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/solvers/__init__.py +0 -0
  105. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/solvers/base.py +0 -0
  106. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/solvers/integral/__init__.py +0 -0
  107. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/solvers/integral/_boundary.py +0 -0
  108. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/solvers/integral/_core.py +0 -0
  109. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/solvers/integral/_finite.py +0 -0
  110. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/solvers/integral/_greens.py +0 -0
  111. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/solvers/integral/_kernels.py +0 -0
  112. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/solvers/integral/_nystrom.py +0 -0
  113. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab/solvers/scalar.py +0 -0
  114. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab.egg-info/dependency_links.txt +0 -0
  115. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab.egg-info/entry_points.txt +0 -0
  116. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab.egg-info/requires.txt +0 -0
  117. {gratinglab-0.1.4 → gratinglab-0.1.5}/src/gratinglab.egg-info/top_level.txt +0 -0
  118. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_checks.py +0 -0
  119. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_compare.py +0 -0
  120. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_convergence.py +0 -0
  121. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_cross_method.py +0 -0
  122. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_efficiency_table.py +0 -0
  123. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_fresnel.py +0 -0
  124. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_geometry.py +0 -0
  125. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_ggp.py +0 -0
  126. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_gui_diagram.py +0 -0
  127. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_gui_diagram3d.py +0 -0
  128. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_gui_docs.py +0 -0
  129. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_gui_integral_options.py +0 -0
  130. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_gui_mathtext.py +0 -0
  131. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_gui_orders.py +0 -0
  132. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_gui_provenance.py +0 -0
  133. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_gui_qt.py +0 -0
  134. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_gui_richtext.py +0 -0
  135. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_gui_scalar_options.py +0 -0
  136. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_gui_state.py +0 -0
  137. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_illumination.py +0 -0
  138. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_integral_core.py +0 -0
  139. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_integral_corpus.py +0 -0
  140. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_integral_finite.py +0 -0
  141. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_integral_greens.py +0 -0
  142. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_integral_kernels.py +0 -0
  143. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_integral_operators.py +0 -0
  144. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_integral_solver.py +0 -0
  145. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_io_boundary.py +0 -0
  146. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_materials.py +0 -0
  147. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_perturbation.py +0 -0
  148. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_profiles.py +0 -0
  149. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_refractiveindex.py +0 -0
  150. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_resolution.py +0 -0
  151. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_result.py +0 -0
  152. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_scalar.py +0 -0
  153. {gratinglab-0.1.4 → gratinglab-0.1.5}/tests/test_solver_conformance.py +0 -0
  154. {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.4
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.4"
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))
@@ -31,6 +31,7 @@ PAGE_ORDER = (
31
31
  "tip-correction",
32
32
  "row-groups",
33
33
  "icc-correction",
34
+ "swath-statistics",
34
35
  "facet-fitting",
35
36
  "reading-outputs",
36
37
  )
@@ -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.4
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