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