gratinglab 0.2.7__tar.gz → 0.3.0__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 (200) hide show
  1. {gratinglab-0.2.7/src/gratinglab.egg-info → gratinglab-0.3.0}/PKG-INFO +44 -20
  2. {gratinglab-0.2.7 → gratinglab-0.3.0}/README.md +43 -19
  3. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/__init__.py +1 -1
  4. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/cmethod_options.py +16 -4
  5. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/comparison.py +6 -6
  6. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/integral_options.py +90 -41
  7. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/io/ggp.py +34 -28
  8. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/boundary/__init__.py +6 -1
  9. gratinglab-0.3.0/src/gratinglab/metrology/boundary/cross_section.py +873 -0
  10. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/config.py +1 -1
  11. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/gui/qt/boundary_view.py +2 -1
  12. gratinglab-0.3.0/src/gratinglab/metrology/io/sem.py +603 -0
  13. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/settings.py +2 -2
  14. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/wiki/reading-outputs.md +6 -4
  15. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/problem.py +8 -2
  16. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/profiles.py +573 -13
  17. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/solvers/cmethod/__init__.py +120 -4
  18. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/solvers/cmethod/_series.py +138 -17
  19. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/solvers/integral/__init__.py +194 -29
  20. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/solvers/integral/_boundary.py +68 -7
  21. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/solvers/integral/_nystrom.py +60 -11
  22. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/solvers/scalar.py +12 -8
  23. {gratinglab-0.2.7 → gratinglab-0.3.0/src/gratinglab.egg-info}/PKG-INFO +44 -20
  24. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab.egg-info/SOURCES.txt +5 -1
  25. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_cmethod.py +255 -3
  26. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_ggp.py +24 -8
  27. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_gui_cmethod_options.py +35 -3
  28. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_gui_cmethod_tab.py +18 -4
  29. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_gui_compare_tab.py +12 -0
  30. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_gui_integral_options.py +42 -18
  31. gratinglab-0.3.0/tests/test_integral_measured_polygon.py +410 -0
  32. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_measured.py +304 -1
  33. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_profiles.py +145 -1
  34. gratinglab-0.3.0/tests/test_trapezoidal_solvers.py +232 -0
  35. {gratinglab-0.2.7 → gratinglab-0.3.0}/LICENSE +0 -0
  36. {gratinglab-0.2.7 → gratinglab-0.3.0}/pyproject.toml +0 -0
  37. {gratinglab-0.2.7 → gratinglab-0.3.0}/setup.cfg +0 -0
  38. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/checks.py +0 -0
  39. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/compare.py +0 -0
  40. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/convergence.py +0 -0
  41. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/geometry.py +0 -0
  42. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/__init__.py +0 -0
  43. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/app.py +0 -0
  44. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/diagram.py +0 -0
  45. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/diagram3d.py +0 -0
  46. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/docs.py +0 -0
  47. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/efficiency_plot.py +0 -0
  48. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/icon.py +0 -0
  49. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/launcher.py +0 -0
  50. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/mathtext.py +0 -0
  51. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/orders.py +0 -0
  52. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/provenance.py +0 -0
  53. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/qt/__init__.py +0 -0
  54. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/qt/cmethod_tab.py +0 -0
  55. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/qt/compare_tab.py +0 -0
  56. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/qt/geometry_panel.py +0 -0
  57. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/qt/geometry_tab.py +0 -0
  58. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/qt/integral_tab.py +0 -0
  59. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/qt/launcher.py +0 -0
  60. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/qt/main_window.py +0 -0
  61. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/qt/profile_plot_panel.py +0 -0
  62. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/qt/rcwa_tab.py +0 -0
  63. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/qt/scalar_tab.py +0 -0
  64. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/qt/scene3d_panel.py +0 -0
  65. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/qt/setup_tab.py +0 -0
  66. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/qt/sizing.py +0 -0
  67. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/qt/theming.py +0 -0
  68. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/qt/theory_viewer.py +0 -0
  69. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/qt/worker.py +0 -0
  70. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/rcwa_options.py +0 -0
  71. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/richtext.py +0 -0
  72. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/scalar_options.py +0 -0
  73. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/state.py +0 -0
  74. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/gui/theme.py +0 -0
  75. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/illumination.py +0 -0
  76. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/io/__init__.py +0 -0
  77. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/io/boundary.py +0 -0
  78. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/io/efficiency_table.py +0 -0
  79. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/materials/__init__.py +0 -0
  80. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/materials/data/SOURCES.md +0 -0
  81. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/materials/fresnel.py +0 -0
  82. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/materials/optical.py +0 -0
  83. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/materials/refractiveindex.py +0 -0
  84. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/measured.py +0 -0
  85. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/__init__.py +0 -0
  86. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/analyzer.py +0 -0
  87. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/boundary/average.py +0 -0
  88. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/boundary/pipeline.py +0 -0
  89. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/boundary/swaths.py +0 -0
  90. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/cli.py +0 -0
  91. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/core/__init__.py +0 -0
  92. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/core/analysis.py +0 -0
  93. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/core/image_flatten.py +0 -0
  94. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/core/orientation.py +0 -0
  95. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/core/processing.py +0 -0
  96. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/core/region_mask.py +0 -0
  97. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/core/scars.py +0 -0
  98. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/core/tip.py +0 -0
  99. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/data/__init__.py +0 -0
  100. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/data/aggregation.py +0 -0
  101. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/gui/__init__.py +0 -0
  102. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/gui/icon.py +0 -0
  103. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/gui/qt/__init__.py +0 -0
  104. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/gui/qt/app.py +0 -0
  105. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/gui/qt/canvas.py +0 -0
  106. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/gui/qt/efficiency_dialog.py +0 -0
  107. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/gui/qt/import_view.py +0 -0
  108. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/gui/qt/layout.py +0 -0
  109. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/gui/qt/main_window.py +0 -0
  110. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/gui/qt/swath_dialog.py +0 -0
  111. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/gui/qt/wiki_view.py +0 -0
  112. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/gui/qt/worker.py +0 -0
  113. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/gui/state.py +0 -0
  114. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/io/__init__.py +0 -0
  115. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/io/file_io.py +0 -0
  116. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/io/ibw.py +0 -0
  117. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/io/metrics.py +0 -0
  118. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/io/spm.py +0 -0
  119. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/stats/__init__.py +0 -0
  120. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/stats/analysis.py +0 -0
  121. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/stats/icc.py +0 -0
  122. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/visualization/__init__.py +0 -0
  123. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/visualization/diagnostics.py +0 -0
  124. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/visualization/profiles.py +0 -0
  125. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/visualization/statistics.py +0 -0
  126. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/wiki/__init__.py +0 -0
  127. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/wiki/facet-fitting.md +0 -0
  128. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/wiki/file-formats.md +0 -0
  129. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/wiki/flattening.md +0 -0
  130. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/wiki/groove-orientation.md +0 -0
  131. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/wiki/icc-correction.md +0 -0
  132. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/wiki/overview.md +0 -0
  133. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/wiki/row-groups.md +0 -0
  134. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/wiki/scars-and-masking.md +0 -0
  135. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/wiki/swath-statistics.md +0 -0
  136. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/wiki/tip-correction.md +0 -0
  137. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/metrology/workflows.py +0 -0
  138. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/resolution.py +0 -0
  139. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/result.py +0 -0
  140. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/solvers/__init__.py +0 -0
  141. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/solvers/_conditioning.py +0 -0
  142. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/solvers/_execution.py +0 -0
  143. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/solvers/base.py +0 -0
  144. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/solvers/cmethod/_core.py +0 -0
  145. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/solvers/integral/_core.py +0 -0
  146. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/solvers/integral/_finite.py +0 -0
  147. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/solvers/integral/_greens.py +0 -0
  148. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/solvers/integral/_kernels.py +0 -0
  149. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/solvers/rcwa/__init__.py +0 -0
  150. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/solvers/rcwa/_core.py +0 -0
  151. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab/solvers/rcwa/_stack.py +0 -0
  152. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab.egg-info/dependency_links.txt +0 -0
  153. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab.egg-info/entry_points.txt +0 -0
  154. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab.egg-info/requires.txt +0 -0
  155. {gratinglab-0.2.7 → gratinglab-0.3.0}/src/gratinglab.egg-info/top_level.txt +0 -0
  156. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_changelog.py +0 -0
  157. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_checks.py +0 -0
  158. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_compare.py +0 -0
  159. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_conditioning.py +0 -0
  160. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_convergence.py +0 -0
  161. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_cross_method.py +0 -0
  162. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_efficiency_table.py +0 -0
  163. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_four_way.py +0 -0
  164. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_fresnel.py +0 -0
  165. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_geometry.py +0 -0
  166. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_gui_diagram.py +0 -0
  167. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_gui_diagram3d.py +0 -0
  168. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_gui_docs.py +0 -0
  169. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_gui_launcher.py +0 -0
  170. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_gui_mathtext.py +0 -0
  171. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_gui_orders.py +0 -0
  172. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_gui_provenance.py +0 -0
  173. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_gui_qt.py +0 -0
  174. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_gui_rcwa_options.py +0 -0
  175. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_gui_richtext.py +0 -0
  176. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_gui_scalar_options.py +0 -0
  177. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_gui_state.py +0 -0
  178. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_gui_theme.py +0 -0
  179. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_illumination.py +0 -0
  180. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_integral_core.py +0 -0
  181. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_integral_corpus.py +0 -0
  182. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_integral_finite.py +0 -0
  183. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_integral_graded.py +0 -0
  184. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_integral_greens.py +0 -0
  185. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_integral_kernels.py +0 -0
  186. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_integral_operators.py +0 -0
  187. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_integral_solver.py +0 -0
  188. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_io_boundary.py +0 -0
  189. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_materials.py +0 -0
  190. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_perturbation.py +0 -0
  191. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_rayleigh_anomaly.py +0 -0
  192. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_rcwa.py +0 -0
  193. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_rcwa_benchmarks.py +0 -0
  194. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_refractiveindex.py +0 -0
  195. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_resolution.py +0 -0
  196. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_result.py +0 -0
  197. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_scalar.py +0 -0
  198. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_solver_conformance.py +0 -0
  199. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_solver_execution.py +0 -0
  200. {gratinglab-0.2.7 → gratinglab-0.3.0}/tests/test_solver_progress.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gratinglab
3
- Version: 0.2.7
3
+ Version: 0.3.0
4
4
  Summary: Open platform for diffraction grating groove metrology and efficiency analysis
5
5
  Author: Jake McCoy, Aditi Gangadharan
6
6
  License: BSD-3-Clause
@@ -59,9 +59,12 @@ efficiency of a grating from such a profile.
59
59
  The AFM metrology is the part of the package in regular use. It measures blaze angles
60
60
  from a scan, averages the grooves into a single profile, reports how much that profile
61
61
  varies down the scan, corrects for the shape of the AFM tip where the data allow it,
62
- and exports the averaged groove as a boundary profile. The efficiency solvers are
63
- **experimental**: they pass their test suite but have not yet been used for real
64
- analysis, and their results should be treated as provisional.
62
+ and exports the averaged groove as a boundary profile. The three rigorous
63
+ efficiency solvers left experimental status in 0.3, after agreeing with one
64
+ another and with a measured campaign whose groove was measured too (the TASTE
65
+ comparison in `docs/findings.md`). Their known limits are stated in the table
66
+ under Status, and a number from any of them is still evidence to check rather
67
+ than a result to rely on.
65
68
 
66
69
  Rigorous methods for computing grating efficiency are well established, and open codes
67
70
  exist for several of them, although each carries its own conventions and
@@ -108,12 +111,24 @@ The project is at a pre-alpha stage. The AFM metrology is the more mature of its
108
111
  parts. On the efficiency side, the scalar and integral solvers, the comparison and
109
112
  convergence harnesses and the materials layer work end to end. They are checked
110
113
  against closed-form results, published benchmark tables, energy balance and
111
- reciprocity, but remain experimental until they have been used on real problems.
112
- Efficiencies are absolute when a coating is named, using an optical constant table
113
- supplied by the user (see Install). Since 0.2.0 two more rigorous backends sit
114
- beside them, both written in-house and both experimental: a rigorous coupled-wave
115
- analysis (RCWA), which also models coated gratings through conformal layers, and
116
- the C-method (Chandezon). Each has a tab in the efficiency window since 0.2.1.
114
+ reciprocity. Efficiencies are absolute when a coating is named, using an optical
115
+ constant table supplied by the user (see Install). Since 0.2.0 two more rigorous
116
+ backends sit beside them, both written in-house: a rigorous coupled-wave analysis
117
+ (RCWA), which also models coated gratings through conformal layers, and the
118
+ C-method (Chandezon). Each has a tab in the efficiency window since 0.2.1.
119
+
120
+ **Since 0.3 the three rigorous solvers are no longer experimental.** The
121
+ criterion was use on a real problem: on the December 2018 TASTE campaign, 296
122
+ measured efficiencies against the grating's own AFM groove, the integral method,
123
+ the C-method and RCWA agree with one another to 2e-4 RMS and with the
124
+ measurement to 0.038, so the residual is measured not to be the solver. The two
125
+ defects that comparison found are dealt with. The C-method refuses a wall too
126
+ steep for its truncation. The integral solver's absorption on a measured polygon
127
+ under `conductivity="tabulated"` is fixed by `profile_smoothing="auto"`, which is
128
+ opt-in because it moves finely sampled profiles at long wavelengths; with the
129
+ default the polygon is solved as given, and the energy-balance warning names the
130
+ option when it fires. A tightly rounded valley remains a recorded limitation.
131
+ The scalar solver is an approximation and is described as one.
117
132
 
118
133
  **Every modelling method here still needs verification**, the rigorous ones
119
134
  included. The checks in the table below show that each method reproduces the
@@ -135,9 +150,17 @@ groove varies between horizontal swaths of the scan. It can also undo the broade
135
150
  introduced by the AFM tip where the tip reached the surface, and it marks the regions
136
151
  the tip could not reach instead of estimating them.
137
152
 
138
- The efficiency side comprises the problem specification, the solvers, the comparison
139
- and convergence harnesses, the materials layer, and a calculation of the resolving
140
- power implied by the number of grooves measured.
153
+ It also reads Zeiss SmartSEM electron micrographs (`metrology.io.sem`), with the
154
+ instrument's own pixel size, stage tilt and foreshortening. A cross-section is the
155
+ one groove measurement with no tip to correct for, and the only one that can see an
156
+ undercut or measure the width of a crest. Tracing a groove off a micrograph into a
157
+ profile is not written yet.
158
+
159
+ The efficiency side comprises the problem specification, the groove profiles
160
+ (blazed, lamellar, sinusoidal, trapezoidal with a sidewall angle and corner radius,
161
+ and any measured profile), the solvers, the comparison and convergence harnesses,
162
+ the materials layer, and a calculation of the resolving power implied by the number
163
+ of grooves measured.
141
164
 
142
165
  The aim is gratings in general: reflection and transmission, metallic and
143
166
  dielectric, bare and coated, from X-rays to the infrared. Every mount between the
@@ -155,10 +178,10 @@ The method backends, in order of development:
155
178
 
156
179
  | Method | Status |
157
180
  |---|---|
158
- | Scalar (Kirchhoff, x-ray blaze form) | experimental; checked against closed-form results |
159
- | Integral method (boundary integral, conical, undercut-capable) | experimental; perfect and finite conductivity (`conductivity="tabulated"`, Goray & Schmidt 2010), absolute efficiencies with the absorbed fraction recorded; a corner-graded mesh for profiles with corners (`corner_grading=4`, the default since 0.2.7; `0` is the equal-arc mesh), under either boundary condition; checked against published benchmark tables (Maystre & Popov; Goray & Schmidt), energy balance and reciprocity |
160
- | RCWA (in-plane, conical, off-plane; Li factorization, S-matrix, layer stacks) | experimental, since 0.2.0; lamellar gratings exact and other profiles staircased, transmitted orders, and coatings through conformal layers (`Problem.layers`); checked against Fresnel, the Airy film formula, reciprocity, the integral solver, and published lamellar values (Granet; Lalanne & Hugonin; Gralak; Goray & Schmidt's conical tables). Not yet checked: staircase accuracy in metallic TM, and layers of a second material against an independent code |
161
- | C-method (Chandezon) | experimental, since 0.2.0; perfect conductor in any mount, and one interface into an absorbing or lossless material, conical and coupled; checked against Goray & Schmidt's Table 3, Fresnel and the integral solver. Refuses vertical facets and undercuts; converges slowly on sawtooth corners, and loses precision at high truncation on profiles deeper than about half the period, both reported in provenance. No coatings |
181
+ | Scalar (Kirchhoff, x-ray blaze form) | an approximation; checked against closed-form results |
182
+ | Integral method (boundary integral, conical, undercut-capable) | since 0.3 not experimental; perfect and finite conductivity (`conductivity="tabulated"`, Goray & Schmidt 2010), absolute efficiencies with the absorbed fraction recorded; a corner-graded mesh for profiles with corners (`corner_grading=4`, the default since 0.2.7; `0` is the equal-arc mesh), under either boundary condition; checked against published benchmark tables (Maystre & Popov; Goray & Schmidt), energy balance and reciprocity |
183
+ | RCWA (in-plane, conical, off-plane; Li factorization, S-matrix, layer stacks) | since 0.2.0, not experimental since 0.3; lamellar gratings exact and other profiles staircased, transmitted orders, and coatings through conformal layers (`Problem.layers`); checked against Fresnel, the Airy film formula, reciprocity, the integral solver, and published lamellar values (Granet; Lalanne & Hugonin; Gralak; Goray & Schmidt's conical tables). Not yet checked: staircase accuracy in metallic TM, and layers of a second material against an independent code |
184
+ | C-method (Chandezon) | since 0.2.0, not experimental since 0.3; perfect conductor in any mount, and one interface into an absorbing or lossless material, conical and coupled; checked against Goray & Schmidt's Table 3, Fresnel and the integral solver. Refuses vertical facets, undercuts, and walls too steep for the scan (the harmonics a wall needs grow with its slope times P sin γ / λ; measured against the integral solver, see the method note); converges slowly on sawtooth corners, and loses precision at high truncation on profiles deeper than about half the period, both reported in provenance. No coatings |
162
185
 
163
186
  The comparison harness also accepts an efficiency table already computed by an
164
187
  external program as one of its methods, so that comparison plots can be made without
@@ -450,9 +473,10 @@ derived product without prior written permission.
450
473
 
451
474
  **What you do not get.** The software is provided as is, without warranty of any kind,
452
475
  and the author is not liable for any use of it. That disclaimer carries more weight
453
- here than it does for most packages, because the efficiency solvers are experimental
454
- (see Status): a number this software produces is evidence to be checked against
455
- another method, not a result to be relied on because a computer printed it.
476
+ here than it does for most packages, because the efficiency solvers are young and
477
+ validated on few gratings (see Status): a number this software produces is evidence
478
+ to be checked against another method, not a result to be relied on because a
479
+ computer printed it.
456
480
 
457
481
  Contributions are accepted under the same licence. A pull request is taken as agreement
458
482
  that the contribution may be distributed under BSD-3-Clause.
@@ -14,9 +14,12 @@ efficiency of a grating from such a profile.
14
14
  The AFM metrology is the part of the package in regular use. It measures blaze angles
15
15
  from a scan, averages the grooves into a single profile, reports how much that profile
16
16
  varies down the scan, corrects for the shape of the AFM tip where the data allow it,
17
- and exports the averaged groove as a boundary profile. The efficiency solvers are
18
- **experimental**: they pass their test suite but have not yet been used for real
19
- analysis, and their results should be treated as provisional.
17
+ and exports the averaged groove as a boundary profile. The three rigorous
18
+ efficiency solvers left experimental status in 0.3, after agreeing with one
19
+ another and with a measured campaign whose groove was measured too (the TASTE
20
+ comparison in `docs/findings.md`). Their known limits are stated in the table
21
+ under Status, and a number from any of them is still evidence to check rather
22
+ than a result to rely on.
20
23
 
21
24
  Rigorous methods for computing grating efficiency are well established, and open codes
22
25
  exist for several of them, although each carries its own conventions and
@@ -63,12 +66,24 @@ The project is at a pre-alpha stage. The AFM metrology is the more mature of its
63
66
  parts. On the efficiency side, the scalar and integral solvers, the comparison and
64
67
  convergence harnesses and the materials layer work end to end. They are checked
65
68
  against closed-form results, published benchmark tables, energy balance and
66
- reciprocity, but remain experimental until they have been used on real problems.
67
- Efficiencies are absolute when a coating is named, using an optical constant table
68
- supplied by the user (see Install). Since 0.2.0 two more rigorous backends sit
69
- beside them, both written in-house and both experimental: a rigorous coupled-wave
70
- analysis (RCWA), which also models coated gratings through conformal layers, and
71
- the C-method (Chandezon). Each has a tab in the efficiency window since 0.2.1.
69
+ reciprocity. Efficiencies are absolute when a coating is named, using an optical
70
+ constant table supplied by the user (see Install). Since 0.2.0 two more rigorous
71
+ backends sit beside them, both written in-house: a rigorous coupled-wave analysis
72
+ (RCWA), which also models coated gratings through conformal layers, and the
73
+ C-method (Chandezon). Each has a tab in the efficiency window since 0.2.1.
74
+
75
+ **Since 0.3 the three rigorous solvers are no longer experimental.** The
76
+ criterion was use on a real problem: on the December 2018 TASTE campaign, 296
77
+ measured efficiencies against the grating's own AFM groove, the integral method,
78
+ the C-method and RCWA agree with one another to 2e-4 RMS and with the
79
+ measurement to 0.038, so the residual is measured not to be the solver. The two
80
+ defects that comparison found are dealt with. The C-method refuses a wall too
81
+ steep for its truncation. The integral solver's absorption on a measured polygon
82
+ under `conductivity="tabulated"` is fixed by `profile_smoothing="auto"`, which is
83
+ opt-in because it moves finely sampled profiles at long wavelengths; with the
84
+ default the polygon is solved as given, and the energy-balance warning names the
85
+ option when it fires. A tightly rounded valley remains a recorded limitation.
86
+ The scalar solver is an approximation and is described as one.
72
87
 
73
88
  **Every modelling method here still needs verification**, the rigorous ones
74
89
  included. The checks in the table below show that each method reproduces the
@@ -90,9 +105,17 @@ groove varies between horizontal swaths of the scan. It can also undo the broade
90
105
  introduced by the AFM tip where the tip reached the surface, and it marks the regions
91
106
  the tip could not reach instead of estimating them.
92
107
 
93
- The efficiency side comprises the problem specification, the solvers, the comparison
94
- and convergence harnesses, the materials layer, and a calculation of the resolving
95
- power implied by the number of grooves measured.
108
+ It also reads Zeiss SmartSEM electron micrographs (`metrology.io.sem`), with the
109
+ instrument's own pixel size, stage tilt and foreshortening. A cross-section is the
110
+ one groove measurement with no tip to correct for, and the only one that can see an
111
+ undercut or measure the width of a crest. Tracing a groove off a micrograph into a
112
+ profile is not written yet.
113
+
114
+ The efficiency side comprises the problem specification, the groove profiles
115
+ (blazed, lamellar, sinusoidal, trapezoidal with a sidewall angle and corner radius,
116
+ and any measured profile), the solvers, the comparison and convergence harnesses,
117
+ the materials layer, and a calculation of the resolving power implied by the number
118
+ of grooves measured.
96
119
 
97
120
  The aim is gratings in general: reflection and transmission, metallic and
98
121
  dielectric, bare and coated, from X-rays to the infrared. Every mount between the
@@ -110,10 +133,10 @@ The method backends, in order of development:
110
133
 
111
134
  | Method | Status |
112
135
  |---|---|
113
- | Scalar (Kirchhoff, x-ray blaze form) | experimental; checked against closed-form results |
114
- | Integral method (boundary integral, conical, undercut-capable) | experimental; perfect and finite conductivity (`conductivity="tabulated"`, Goray & Schmidt 2010), absolute efficiencies with the absorbed fraction recorded; a corner-graded mesh for profiles with corners (`corner_grading=4`, the default since 0.2.7; `0` is the equal-arc mesh), under either boundary condition; checked against published benchmark tables (Maystre & Popov; Goray & Schmidt), energy balance and reciprocity |
115
- | RCWA (in-plane, conical, off-plane; Li factorization, S-matrix, layer stacks) | experimental, since 0.2.0; lamellar gratings exact and other profiles staircased, transmitted orders, and coatings through conformal layers (`Problem.layers`); checked against Fresnel, the Airy film formula, reciprocity, the integral solver, and published lamellar values (Granet; Lalanne & Hugonin; Gralak; Goray & Schmidt's conical tables). Not yet checked: staircase accuracy in metallic TM, and layers of a second material against an independent code |
116
- | C-method (Chandezon) | experimental, since 0.2.0; perfect conductor in any mount, and one interface into an absorbing or lossless material, conical and coupled; checked against Goray & Schmidt's Table 3, Fresnel and the integral solver. Refuses vertical facets and undercuts; converges slowly on sawtooth corners, and loses precision at high truncation on profiles deeper than about half the period, both reported in provenance. No coatings |
136
+ | Scalar (Kirchhoff, x-ray blaze form) | an approximation; checked against closed-form results |
137
+ | Integral method (boundary integral, conical, undercut-capable) | since 0.3 not experimental; perfect and finite conductivity (`conductivity="tabulated"`, Goray & Schmidt 2010), absolute efficiencies with the absorbed fraction recorded; a corner-graded mesh for profiles with corners (`corner_grading=4`, the default since 0.2.7; `0` is the equal-arc mesh), under either boundary condition; checked against published benchmark tables (Maystre & Popov; Goray & Schmidt), energy balance and reciprocity |
138
+ | RCWA (in-plane, conical, off-plane; Li factorization, S-matrix, layer stacks) | since 0.2.0, not experimental since 0.3; lamellar gratings exact and other profiles staircased, transmitted orders, and coatings through conformal layers (`Problem.layers`); checked against Fresnel, the Airy film formula, reciprocity, the integral solver, and published lamellar values (Granet; Lalanne & Hugonin; Gralak; Goray & Schmidt's conical tables). Not yet checked: staircase accuracy in metallic TM, and layers of a second material against an independent code |
139
+ | C-method (Chandezon) | since 0.2.0, not experimental since 0.3; perfect conductor in any mount, and one interface into an absorbing or lossless material, conical and coupled; checked against Goray & Schmidt's Table 3, Fresnel and the integral solver. Refuses vertical facets, undercuts, and walls too steep for the scan (the harmonics a wall needs grow with its slope times P sin γ / λ; measured against the integral solver, see the method note); converges slowly on sawtooth corners, and loses precision at high truncation on profiles deeper than about half the period, both reported in provenance. No coatings |
117
140
 
118
141
  The comparison harness also accepts an efficiency table already computed by an
119
142
  external program as one of its methods, so that comparison plots can be made without
@@ -405,9 +428,10 @@ derived product without prior written permission.
405
428
 
406
429
  **What you do not get.** The software is provided as is, without warranty of any kind,
407
430
  and the author is not liable for any use of it. That disclaimer carries more weight
408
- here than it does for most packages, because the efficiency solvers are experimental
409
- (see Status): a number this software produces is evidence to be checked against
410
- another method, not a result to be relied on because a computer printed it.
431
+ here than it does for most packages, because the efficiency solvers are young and
432
+ validated on few gratings (see Status): a number this software produces is evidence
433
+ to be checked against another method, not a result to be relied on because a
434
+ computer printed it.
411
435
 
412
436
  Contributions are accepted under the same licence. A pull request is taken as agreement
413
437
  that the contribution may be distributed under BSD-3-Clause.
@@ -22,7 +22,7 @@ from .measured import MeasuredEfficiency, MeasurementProvenance
22
22
  from .resolution import ResolvingPower, resolving_power
23
23
  from .result import EfficiencyScan, OrderEfficiency, Provenance
24
24
 
25
- __version__ = "0.2.7"
25
+ __version__ = "0.3.0"
26
26
 
27
27
  __all__ = [
28
28
  "Illumination",
@@ -14,9 +14,10 @@ harmonics either side of the centre of the propagating orders.
14
14
 
15
15
  The guards mirror the solver's refusals so an error lands on the field that
16
16
  caused it instead of arriving as a ValueError from the worker thread. The
17
- truncation floor is not re-derived here: it is
18
- :func:`gratinglab.solvers.cmethod.truncation_floor`, the function the solver
19
- itself applies, so the form and the solver cannot disagree about it.
17
+ truncation floors are not re-derived here: they are
18
+ :func:`gratinglab.solvers.cmethod.truncation_floor` (the orders) and
19
+ :func:`~gratinglab.solvers.cmethod.wall_floor` (a steep wall), the functions
20
+ the solver itself applies, so the form and the solver cannot disagree.
20
21
  """
21
22
 
22
23
  from __future__ import annotations
@@ -29,7 +30,8 @@ from numpy.typing import NDArray
29
30
 
30
31
  from ..illumination import Illumination
31
32
  from ..problem import Problem
32
- from ..solvers.cmethod import _MAX_TRUNCATION, truncation_floor
33
+ from ..solvers.base import UnsupportedConfiguration
34
+ from ..solvers.cmethod import _MAX_TRUNCATION, truncation_floor, wall_floor
33
35
  from .integral_options import _tabulated_index
34
36
  from .state import FieldError, FormErrors, _number
35
37
 
@@ -119,5 +121,15 @@ def build_options(
119
121
  floor = truncation_floor(problem, illumination, wavelengths, indices)
120
122
  if truncation < floor.needed:
121
123
  raise FormErrors((FieldError("truncation", floor.message),))
124
+ # A steep wall's floor goes on the field too, but only while a truncation
125
+ # the field accepts can meet it. Past the ceiling, or on a profile the
126
+ # method cannot describe at all, the solver refuses the geometry with
127
+ # UnsupportedConfiguration, and the tab names that refusal instead.
128
+ try:
129
+ wall = wall_floor(problem, illumination, wavelengths, indices)
130
+ except UnsupportedConfiguration:
131
+ wall = None
132
+ if wall is not None and truncation < wall.needed <= _MAX_TRUNCATION:
133
+ raise FormErrors((FieldError("truncation", wall.message),))
122
134
 
123
135
  return {"truncation": int(truncation), "conductivity": form.conductivity}
@@ -50,13 +50,13 @@ _STYLES = ("-", "--", "-.", ":")
50
50
 
51
51
  #: Closes every comparison the panel reports. Two solvers agreeing is evidence
52
52
  #: that neither has a bug the other lacks, not that either is right: they can
53
- #: share an assumption, a material table or a geometry convention. Every
54
- #: method here is experimental, so the panel says so where agreement is shown,
55
- #: dimmed because it is a fact about the methods, not a fault in this run.
53
+ #: share an assumption, a material table or a geometry convention. So the
54
+ #: panel says so where agreement is shown, dimmed because it is a fact about
55
+ #: the methods, not a fault in this run.
56
56
  VERIFICATION_NOTE = (
57
- "\nagreement between solvers is not verification: every method here is "
58
- "experimental and needs checking against measurement or a published "
59
- "benchmark\n"
57
+ "\nagreement between solvers is not verification: they share the problem, "
58
+ "the optical constants and the conventions, so check against measurement "
59
+ "or a published benchmark\n"
60
60
  )
61
61
 
62
62
  #: More legend entries than this and the order legend takes a second column,
@@ -28,6 +28,7 @@ solver's own check for nodes too close to separate is applied here too.
28
28
  from __future__ import annotations
29
29
 
30
30
  from dataclasses import dataclass, replace
31
+ from collections.abc import Callable
31
32
  from typing import Any
32
33
 
33
34
  import numpy as np
@@ -158,10 +159,55 @@ def build_options(
158
159
  arc_length = (
159
160
  problem.profile.boundary(_ARC_SAMPLES).arc_length * problem.period
160
161
  )
161
- # The floor is set by the coarsest spacing. On the equal-arc mesh that is
162
- # the mean, arc_length / P; a graded mesh is built here exactly as the
163
- # solver will build it, since its coarsest spacing depends on P.
164
- resolution_length = arc_length
162
+ material = None
163
+ if form.conductivity == "tabulated":
164
+ # Sequential prerequisites, not parallel checks: there is no index to
165
+ # range-check without a material, and no metal-side floor without an
166
+ # index. So these short-circuit rather than accumulating.
167
+ name, indices, error = _tabulated_index(problem, wavelengths)
168
+ if error is not None:
169
+ raise FormErrors((error,))
170
+ assert name is not None and indices is not None
171
+ material = (name, indices)
172
+
173
+ def floor_at(count: int) -> tuple[int, Callable[[int], str]]:
174
+ """The floor a mesh of ``count`` nodes imposes, and its message.
175
+
176
+ The floor is set by the coarsest spacing. On the equal-arc mesh that
177
+ is the mean, arc_length / P, so the floor does not depend on ``count``;
178
+ a graded mesh is built here exactly as the solver will build it, since
179
+ its coarsest spacing depends on P. The message takes the count to
180
+ quote, which `_first_passing` may raise above this floor.
181
+ """
182
+ resolution_length = arc_length
183
+ if grading:
184
+ mesh = physical_boundary(problem, count, grading=grading)
185
+ if mesh.graded:
186
+ resolution_length = mesh.resolution_length
187
+ reduced = float(wavelengths.min()) / illumination.sin_gamma
188
+ needed = int(np.ceil(_POINTS_PER_WAVELENGTH * resolution_length / reduced))
189
+
190
+ def message(quote: int) -> str:
191
+ return (
192
+ f"needs at least {quote} to give "
193
+ f"{_POINTS_PER_WAVELENGTH:g} nodes per transverse "
194
+ f"wavelength ({reduced:.4g} nm) along the "
195
+ f"{arc_length:.4g} nm boundary"
196
+ + (", where the graded mesh's coarsest spacing sets the floor"
197
+ if resolution_length != arc_length else "")
198
+ )
199
+
200
+ if material is not None:
201
+ metal_needed, metal_message = _metal_floor(
202
+ *material, illumination, wavelengths, resolution_length
203
+ )
204
+ # One error on one field: whichever side of the boundary binds
205
+ # harder. Reporting both would put two messages on
206
+ # `boundary_points` that a single number answers.
207
+ if metal_needed > needed:
208
+ return metal_needed, metal_message
209
+ return needed, message
210
+
165
211
  if grading:
166
212
  try:
167
213
  mesh = physical_boundary(problem, int(points), grading=grading)
@@ -176,38 +222,11 @@ def build_options(
176
222
  "can separate on this groove; choose a lower order (4 is "
177
223
  "recommended) or fewer points.",
178
224
  ),))
179
- if mesh.graded:
180
- resolution_length = mesh.resolution_length
181
- reduced = float(wavelengths.min()) / illumination.sin_gamma
182
- needed = int(np.ceil(_POINTS_PER_WAVELENGTH * resolution_length / reduced))
183
- message = (
184
- f"needs at least {needed} to give "
185
- f"{_POINTS_PER_WAVELENGTH:g} nodes per transverse "
186
- f"wavelength ({reduced:.4g} nm) along the "
187
- f"{arc_length:.4g} nm boundary"
188
- + (", where the graded mesh's coarsest spacing sets the floor"
189
- if resolution_length != arc_length else "")
190
- )
191
-
192
- if form.conductivity == "tabulated":
193
- # Sequential prerequisites, not parallel checks: there is no index to
194
- # range-check without a material, and no metal-side floor without an
195
- # index. So these short-circuit rather than accumulating.
196
- name, indices, error = _tabulated_index(problem, wavelengths)
197
- if error is not None:
198
- raise FormErrors((error,))
199
- assert name is not None and indices is not None
200
- metal_needed, metal_message = _metal_floor(
201
- name, indices, illumination, wavelengths, resolution_length
202
- )
203
- # One error on one field: whichever side of the boundary binds harder.
204
- # Reporting both would put two messages on `boundary_points` that a
205
- # single number answers.
206
- if metal_needed > needed:
207
- needed, message = metal_needed, metal_message
208
225
 
226
+ needed, message = floor_at(int(points))
209
227
  if points < needed:
210
- raise FormErrors((FieldError("boundary_points", message),))
228
+ quote = _first_passing(floor_at, needed) if grading else needed
229
+ raise FormErrors((FieldError("boundary_points", message(quote)),))
211
230
 
212
231
  return {
213
232
  "boundary_points": int(points),
@@ -270,7 +289,7 @@ def _metal_floor(
270
289
  illumination: Illumination,
271
290
  wavelengths: NDArray[np.float64],
272
291
  arc_length: float,
273
- ) -> tuple[int, str]:
292
+ ) -> tuple[int, Callable[[int], str]]:
274
293
  """The mesh floor the material side imposes, and what to say about it.
275
294
 
276
295
  It uses the same formula as the solver's own guard, so the two numbers
@@ -283,9 +302,39 @@ def _metal_floor(
283
302
  factors = np.abs(np.sqrt(indices**2 - cos_gamma**2))
284
303
  reduced = float((wavelengths / factors).min())
285
304
  needed = int(np.ceil(_POINTS_PER_WAVELENGTH * arc_length / reduced))
286
- return needed, (
287
- f"needs at least {needed} with coating {name}: the field inside a "
288
- f"conductive material oscillates and decays on the scale lambda/|n| "
289
- f"({reduced:.4g} nm here), and the mesh must resolve it along the "
290
- f'{arc_length:.4g} nm boundary. Raise it, or use conductivity="perfect".'
291
- )
305
+
306
+ def message(quote: int) -> str:
307
+ return (
308
+ f"needs at least {quote} with coating {name}: the field inside a "
309
+ f"conductive material oscillates and decays on the scale lambda/|n| "
310
+ f"({reduced:.4g} nm here), and the mesh must resolve it along the "
311
+ f'{arc_length:.4g} nm boundary. Raise it, or use conductivity="perfect".'
312
+ )
313
+
314
+ return needed, message
315
+
316
+
317
+ #: How far above the first estimate `_first_passing` looks. The graded floor
318
+ #: moves by a node or two between neighbouring counts, so this is generous.
319
+ _FLOOR_SEARCH = 64
320
+
321
+
322
+ def _first_passing(floor_at: Callable, estimate: int) -> int:
323
+ """The smallest count from ``estimate`` up that clears its own floor.
324
+
325
+ On a graded mesh the floor depends on the count, because the coarsest
326
+ spacing moves with it, so the floor computed at the count typed is not the
327
+ floor at the count it names. Quoting it as it stood was one short at the
328
+ floor: at 32 points the default blaze said "at least 208", and 208 then
329
+ said 209. Searching upward makes the quoted number one the form accepts.
330
+ A count whose mesh cannot be built is skipped. If none clears within
331
+ ``_FLOOR_SEARCH``, the estimate is returned, which is no worse than before.
332
+ """
333
+ for count in range(estimate, estimate + _FLOOR_SEARCH):
334
+ try:
335
+ needed, _ = floor_at(count)
336
+ except ValueError:
337
+ continue
338
+ if needed <= count:
339
+ return count
340
+ return estimate
@@ -10,10 +10,8 @@ points through :meth:`~gratinglab.profiles.FromProfileData.along_dispersion`
10
10
  and its inverse. The PCGrate corpus is what fixes the file's direction: the
11
11
  TASTE run matches only with ``x`` along ``+d-hat``.
12
12
 
13
- Canonical form::
13
+ Default form, points only::
14
14
 
15
- 3 0 - Polygonal type
16
- Period: 1 PSC: 1
17
15
  0.000000 0.000000
18
16
  0.012346 0.000808
19
17
  ...
@@ -22,19 +20,26 @@ The shared convention allows a scalar calculation and a PCGrate run to be
22
20
  driven from identical geometry, which isolates the method difference from any
23
21
  difference in assumed groove shape.
24
22
 
25
- Three header variants exist on disk, and only the first is accepted by PCGrate:
23
+ Four header variants exist on disk:
26
24
 
27
- =================================== ================ ==============
28
- Header PCGrate accepts Seen in
29
- =================================== ================ ==============
30
- ``3 0 - Polygonal type`` + ``Period`` yes most files
31
- ``# 3 0 - Polygonal type`` (hashed) **no** ``*_fix6/7.ggp``
32
- ``# X(normalized...)`` only **no** ``*_fix{,2,3,4}.ggp``
33
- =================================== ================ ==============
25
+ =================================== ==============================
26
+ Header Seen in
27
+ =================================== ==============================
28
+ none, points only Aditi's files, and the default
29
+ ``3 0 - Polygonal type`` + ``Period`` most legacy files
30
+ ``# 3 0 - Polygonal type`` (hashed) ``*_fix6/7.ggp``
31
+ ``# X(normalized...)`` only ``*_fix{,2,3,4}.ggp``
32
+ =================================== ==============================
34
33
 
35
- This module reads all three, since they are real files that need to be loaded,
36
- and writes only the canonical form. ``np.savetxt(header=...)`` is what produces
37
- the hashed variants, so it must not be used here.
34
+ Which of these PCGrate takes was never tested here. Until 2026-10-29 this
35
+ docstring said only the ``3 0 - Polygonal type`` form loads, but that was
36
+ inferred from which legacy files sat beside PCGrate runs. Aditi, who loads
37
+ these into PCGrate through its border-profile import, reports that it does not
38
+ want a header and that she used to delete it by hand, so the writer leaves it
39
+ out unless asked. ``write_ggp(header=True)`` still writes the two-line form.
40
+
41
+ This module reads all four. ``np.savetxt(header=...)`` is what produces the
42
+ hashed variants, so it must not be used here.
38
43
 
39
44
  This is now the only ``.ggp`` writer in the project. The metrology package
40
45
  previously shipped a second copy, and the two were kept in agreement by a
@@ -70,7 +75,8 @@ __all__ = [
70
75
  "to_profile",
71
76
  ]
72
77
 
73
- #: The only header PCGrate accepts. Never prefix these lines with ``#``.
78
+ #: The legacy two-line header, written only on request. Never prefix these
79
+ #: lines with ``#``.
74
80
  GGP_HEADER = "3 0 - Polygonal type\nPeriod: 1 PSC: 1"
75
81
 
76
82
  _CANONICAL_FIRST = "3 0 - Polygonal type"
@@ -80,9 +86,10 @@ _CANONICAL_FIRST = "3 0 - Polygonal type"
80
86
  class GgpFile:
81
87
  """A parsed ``.ggp``, with the header provenance kept.
82
88
 
83
- ``format_valid`` records whether the file as written would
84
- load in PCGrate. This is surfaced because a hashed header fails there
85
- while parsing without error here.
89
+ ``format_valid`` is False for the two hashed variants, which
90
+ ``np.savetxt(header=...)`` produces by accident, and True for a file with
91
+ no header or the uncommented two-line one. It records which form a file
92
+ is in, not a test against PCGrate; see the module docstring.
86
93
  """
87
94
 
88
95
  t: np.ndarray
@@ -187,9 +194,9 @@ def _numeric_pair(line: str) -> tuple[float, float] | None:
187
194
 
188
195
 
189
196
  def _classify(header_lines: list[str]) -> tuple[str, bool]:
190
- """Name the header variant and say whether PCGrate would accept it."""
197
+ """Name the header variant and say whether it is one we write."""
191
198
  if not header_lines:
192
- return "none", False
199
+ return "none", True
193
200
  if header_lines[0] == _CANONICAL_FIRST:
194
201
  return "canonical", True
195
202
  if header_lines[0].lstrip("# ").startswith("3 0"):
@@ -219,9 +226,9 @@ def write_ggp(
219
226
  t: ArrayLike | None = None,
220
227
  y: ArrayLike | None = None,
221
228
  fmt: str = "%f",
222
- header: bool = True,
229
+ header: bool = False,
223
230
  ) -> Path:
224
- """Write a profile in the canonical, PCGrate-loadable form.
231
+ """Write a profile as ``x y`` points for PCGrate.
225
232
 
226
233
  Pass either a ``profile`` or explicit ``t``/``y`` arrays. A profile is
227
234
  reflected onto the file's ``+d-hat`` axis, the inverse of
@@ -233,12 +240,11 @@ def write_ggp(
233
240
  that ends in one, as ``np.savetxt`` output does. The format is pinned by
234
241
  ``tests/test_ggp.py``; PCGrate rejects variations on it.
235
242
 
236
- ``header=False`` leaves out the two :data:`GGP_HEADER` lines and writes
237
- the points alone, as ``np.savetxt`` with no ``header`` does. It exists to
238
- test the claim in the module docstring that PCGrate needs the header:
239
- that claim comes from the files found on disk, not from PCGrate's
240
- documentation, and Aditi's script writes headerless files (2026-10-27).
241
- Reading such a file back reports ``header_variant == "none"``.
243
+ The points are written alone by default, with no header: Aditi reports
244
+ that PCGrate's border-profile import does not want one (2026-10-29).
245
+ ``header=True`` starts the file with the two legacy :data:`GGP_HEADER`
246
+ lines instead. Reading a headerless file back reports
247
+ ``header_variant == "none"``.
242
248
  """
243
249
  if (profile is None) == (t is None):
244
250
  raise ValueError("pass either a profile or both t and y, not both or neither")
@@ -3,7 +3,8 @@ Boundary profile generation for PCGrate
3
3
 
4
4
  This package averages the detected grooves of an AFM scan into a single
5
5
  representative groove, normalises it to one period, and exports it as a
6
- PCGrate .ggp boundary profile.
6
+ PCGrate .ggp boundary profile. `trace_cross_section` does the same for an SEM
7
+ micrograph of a cleaved cross-section, and fits a trapezoid to it.
7
8
 
8
9
  It shares the profile front-end (loading, groove detection, groove windowing)
9
10
  with the blaze-angle analysis in gratinglab.metrology.core.
@@ -11,6 +12,7 @@ with the blaze-angle analysis in gratinglab.metrology.core.
11
12
 
12
13
  from .pipeline import BoundaryProfile, build_boundary_profile
13
14
  from .swaths import SwathStatistics, swath_statistics
15
+ from .cross_section import CrossSection, TrapezoidFit, trace_cross_section
14
16
  from .average import (
15
17
  flatten_endpoints,
16
18
  average_grooves,
@@ -25,6 +27,9 @@ __all__ = [
25
27
  'build_boundary_profile',
26
28
  'SwathStatistics',
27
29
  'swath_statistics',
30
+ 'CrossSection',
31
+ 'TrapezoidFit',
32
+ 'trace_cross_section',
28
33
  'flatten_endpoints',
29
34
  'average_grooves',
30
35
  'groove_residuals',