gratinglab 0.2.4__tar.gz → 0.2.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 (194) hide show
  1. {gratinglab-0.2.4/src/gratinglab.egg-info → gratinglab-0.2.6}/PKG-INFO +1 -1
  2. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/__init__.py +1 -1
  3. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/gui/qt/theming.py +10 -1
  4. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/io/boundary.py +4 -2
  5. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/boundary/__init__.py +4 -0
  6. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/boundary/average.py +116 -16
  7. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/boundary/pipeline.py +60 -12
  8. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/gui/qt/boundary_view.py +7 -1
  9. gratinglab-0.2.6/src/gratinglab/metrology/gui/qt/layout.py +95 -0
  10. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/io/metrics.py +10 -0
  11. {gratinglab-0.2.4 → gratinglab-0.2.6/src/gratinglab.egg-info}/PKG-INFO +1 -1
  12. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab.egg-info/SOURCES.txt +1 -0
  13. gratinglab-0.2.6/tests/test_four_way.py +217 -0
  14. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_gui_docs.py +7 -2
  15. gratinglab-0.2.4/src/gratinglab/metrology/gui/qt/layout.py +0 -46
  16. {gratinglab-0.2.4 → gratinglab-0.2.6}/LICENSE +0 -0
  17. {gratinglab-0.2.4 → gratinglab-0.2.6}/README.md +0 -0
  18. {gratinglab-0.2.4 → gratinglab-0.2.6}/pyproject.toml +0 -0
  19. {gratinglab-0.2.4 → gratinglab-0.2.6}/setup.cfg +0 -0
  20. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/checks.py +0 -0
  21. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/compare.py +0 -0
  22. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/convergence.py +0 -0
  23. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/geometry.py +0 -0
  24. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/gui/__init__.py +0 -0
  25. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/gui/app.py +0 -0
  26. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/gui/cmethod_options.py +0 -0
  27. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/gui/diagram.py +0 -0
  28. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/gui/diagram3d.py +0 -0
  29. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/gui/docs.py +0 -0
  30. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/gui/efficiency_plot.py +0 -0
  31. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/gui/icon.py +0 -0
  32. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/gui/integral_options.py +0 -0
  33. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/gui/launcher.py +0 -0
  34. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/gui/mathtext.py +0 -0
  35. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/gui/orders.py +0 -0
  36. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/gui/provenance.py +0 -0
  37. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/gui/qt/__init__.py +0 -0
  38. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/gui/qt/cmethod_tab.py +0 -0
  39. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/gui/qt/geometry_panel.py +0 -0
  40. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/gui/qt/geometry_tab.py +0 -0
  41. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/gui/qt/integral_tab.py +0 -0
  42. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/gui/qt/launcher.py +0 -0
  43. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/gui/qt/main_window.py +0 -0
  44. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/gui/qt/profile_plot_panel.py +0 -0
  45. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/gui/qt/rcwa_tab.py +0 -0
  46. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/gui/qt/scalar_tab.py +0 -0
  47. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/gui/qt/scene3d_panel.py +0 -0
  48. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/gui/qt/setup_tab.py +0 -0
  49. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/gui/qt/sizing.py +0 -0
  50. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/gui/qt/theory_viewer.py +0 -0
  51. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/gui/qt/worker.py +0 -0
  52. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/gui/rcwa_options.py +0 -0
  53. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/gui/richtext.py +0 -0
  54. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/gui/scalar_options.py +0 -0
  55. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/gui/state.py +0 -0
  56. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/gui/theme.py +0 -0
  57. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/illumination.py +0 -0
  58. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/io/__init__.py +0 -0
  59. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/io/efficiency_table.py +0 -0
  60. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/io/ggp.py +0 -0
  61. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/materials/__init__.py +0 -0
  62. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/materials/data/SOURCES.md +0 -0
  63. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/materials/fresnel.py +0 -0
  64. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/materials/optical.py +0 -0
  65. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/materials/refractiveindex.py +0 -0
  66. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/measured.py +0 -0
  67. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/__init__.py +0 -0
  68. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/analyzer.py +0 -0
  69. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/boundary/swaths.py +0 -0
  70. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/cli.py +0 -0
  71. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/config.py +0 -0
  72. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/core/__init__.py +0 -0
  73. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/core/analysis.py +0 -0
  74. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/core/image_flatten.py +0 -0
  75. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/core/orientation.py +0 -0
  76. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/core/processing.py +0 -0
  77. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/core/region_mask.py +0 -0
  78. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/core/scars.py +0 -0
  79. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/core/tip.py +0 -0
  80. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/data/__init__.py +0 -0
  81. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/data/aggregation.py +0 -0
  82. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/gui/__init__.py +0 -0
  83. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/gui/icon.py +0 -0
  84. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/gui/qt/__init__.py +0 -0
  85. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/gui/qt/app.py +0 -0
  86. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/gui/qt/canvas.py +0 -0
  87. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/gui/qt/efficiency_dialog.py +0 -0
  88. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/gui/qt/import_view.py +0 -0
  89. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/gui/qt/main_window.py +0 -0
  90. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/gui/qt/swath_dialog.py +0 -0
  91. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/gui/qt/wiki_view.py +0 -0
  92. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/gui/qt/worker.py +0 -0
  93. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/gui/state.py +0 -0
  94. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/io/__init__.py +0 -0
  95. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/io/file_io.py +0 -0
  96. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/io/ibw.py +0 -0
  97. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/io/spm.py +0 -0
  98. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/settings.py +0 -0
  99. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/stats/__init__.py +0 -0
  100. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/stats/analysis.py +0 -0
  101. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/stats/icc.py +0 -0
  102. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/visualization/__init__.py +0 -0
  103. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/visualization/diagnostics.py +0 -0
  104. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/visualization/profiles.py +0 -0
  105. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/visualization/statistics.py +0 -0
  106. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/wiki/__init__.py +0 -0
  107. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/wiki/facet-fitting.md +0 -0
  108. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/wiki/file-formats.md +0 -0
  109. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/wiki/flattening.md +0 -0
  110. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/wiki/groove-orientation.md +0 -0
  111. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/wiki/icc-correction.md +0 -0
  112. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/wiki/overview.md +0 -0
  113. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/wiki/reading-outputs.md +0 -0
  114. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/wiki/row-groups.md +0 -0
  115. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/wiki/scars-and-masking.md +0 -0
  116. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/wiki/swath-statistics.md +0 -0
  117. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/wiki/tip-correction.md +0 -0
  118. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/metrology/workflows.py +0 -0
  119. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/problem.py +0 -0
  120. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/profiles.py +0 -0
  121. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/resolution.py +0 -0
  122. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/result.py +0 -0
  123. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/solvers/__init__.py +0 -0
  124. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/solvers/_conditioning.py +0 -0
  125. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/solvers/_execution.py +0 -0
  126. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/solvers/base.py +0 -0
  127. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/solvers/cmethod/__init__.py +0 -0
  128. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/solvers/cmethod/_core.py +0 -0
  129. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/solvers/cmethod/_series.py +0 -0
  130. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/solvers/integral/__init__.py +0 -0
  131. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/solvers/integral/_boundary.py +0 -0
  132. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/solvers/integral/_core.py +0 -0
  133. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/solvers/integral/_finite.py +0 -0
  134. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/solvers/integral/_greens.py +0 -0
  135. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/solvers/integral/_kernels.py +0 -0
  136. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/solvers/integral/_nystrom.py +0 -0
  137. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/solvers/rcwa/__init__.py +0 -0
  138. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/solvers/rcwa/_core.py +0 -0
  139. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/solvers/rcwa/_stack.py +0 -0
  140. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab/solvers/scalar.py +0 -0
  141. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab.egg-info/dependency_links.txt +0 -0
  142. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab.egg-info/entry_points.txt +0 -0
  143. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab.egg-info/requires.txt +0 -0
  144. {gratinglab-0.2.4 → gratinglab-0.2.6}/src/gratinglab.egg-info/top_level.txt +0 -0
  145. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_changelog.py +0 -0
  146. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_checks.py +0 -0
  147. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_cmethod.py +0 -0
  148. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_compare.py +0 -0
  149. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_conditioning.py +0 -0
  150. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_convergence.py +0 -0
  151. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_cross_method.py +0 -0
  152. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_efficiency_table.py +0 -0
  153. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_fresnel.py +0 -0
  154. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_geometry.py +0 -0
  155. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_ggp.py +0 -0
  156. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_gui_cmethod_options.py +0 -0
  157. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_gui_cmethod_tab.py +0 -0
  158. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_gui_diagram.py +0 -0
  159. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_gui_diagram3d.py +0 -0
  160. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_gui_integral_options.py +0 -0
  161. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_gui_launcher.py +0 -0
  162. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_gui_mathtext.py +0 -0
  163. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_gui_orders.py +0 -0
  164. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_gui_provenance.py +0 -0
  165. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_gui_qt.py +0 -0
  166. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_gui_rcwa_options.py +0 -0
  167. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_gui_richtext.py +0 -0
  168. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_gui_scalar_options.py +0 -0
  169. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_gui_state.py +0 -0
  170. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_gui_theme.py +0 -0
  171. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_illumination.py +0 -0
  172. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_integral_core.py +0 -0
  173. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_integral_corpus.py +0 -0
  174. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_integral_finite.py +0 -0
  175. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_integral_graded.py +0 -0
  176. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_integral_greens.py +0 -0
  177. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_integral_kernels.py +0 -0
  178. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_integral_operators.py +0 -0
  179. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_integral_solver.py +0 -0
  180. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_io_boundary.py +0 -0
  181. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_materials.py +0 -0
  182. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_measured.py +0 -0
  183. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_perturbation.py +0 -0
  184. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_profiles.py +0 -0
  185. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_rayleigh_anomaly.py +0 -0
  186. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_rcwa.py +0 -0
  187. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_rcwa_benchmarks.py +0 -0
  188. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_refractiveindex.py +0 -0
  189. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_resolution.py +0 -0
  190. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_result.py +0 -0
  191. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_scalar.py +0 -0
  192. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_solver_conformance.py +0 -0
  193. {gratinglab-0.2.4 → gratinglab-0.2.6}/tests/test_solver_execution.py +0 -0
  194. {gratinglab-0.2.4 → gratinglab-0.2.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.2.4
3
+ Version: 0.2.6
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
@@ -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.4"
25
+ __version__ = "0.2.6"
26
26
 
27
27
  __all__ = [
28
28
  "Illumination",
@@ -36,6 +36,7 @@ __all__ = [
36
36
  "current",
37
37
  "install",
38
38
  "manager",
39
+ "settings",
39
40
  "subscribe",
40
41
  "swatch",
41
42
  ]
@@ -47,13 +48,21 @@ _KEY = "appearance/theme"
47
48
  _PROPERTY = "_gratinglab_theme_manager"
48
49
 
49
50
 
50
- def _settings() -> QSettings:
51
+ def settings() -> QSettings:
52
+ """The application's settings store, or the INI file ``GRATINGLAB_SETTINGS`` names.
53
+
54
+ Shared by everything the windows remember (the theme, the AFM column
55
+ width), so the test suite's redirect covers all of it at once.
56
+ """
51
57
  path = os.environ.get(SETTINGS_ENV)
52
58
  if path:
53
59
  return QSettings(path, QSettings.Format.IniFormat)
54
60
  return QSettings("gratinglab", "gratinglab")
55
61
 
56
62
 
63
+ _settings = settings
64
+
65
+
57
66
  def palette(theme: Theme) -> QPalette:
58
67
  """A ``QPalette`` for ``theme``, with disabled text dimmed to its muted colour."""
59
68
  role = QPalette.ColorRole
@@ -88,8 +88,10 @@ class BoundaryFile:
88
88
  This is the same problem ``BoundaryProfile.to_problem`` builds in
89
89
  process, which is the purpose of the format; see
90
90
  ``tests/test_io_boundary.py``.
91
- Roughness is not in the file for the reason that method's docstring
92
- gives: nothing here measures surface microroughness.
91
+ Roughness is not applied, for the reason that method's docstring
92
+ gives. The measured groove residual travels in
93
+ ``metrics['groove_residual_rms_nm']`` and is passed only on request,
94
+ as ``roughness=``.
93
95
  """
94
96
  from ..problem import Problem
95
97
 
@@ -14,8 +14,10 @@ from .swaths import SwathStatistics, swath_statistics
14
14
  from .average import (
15
15
  flatten_endpoints,
16
16
  average_grooves,
17
+ groove_residuals,
17
18
  normalize_profile,
18
19
  profile_metrics,
20
+ select_grooves,
19
21
  )
20
22
 
21
23
  __all__ = [
@@ -25,6 +27,8 @@ __all__ = [
25
27
  'swath_statistics',
26
28
  'flatten_endpoints',
27
29
  'average_grooves',
30
+ 'groove_residuals',
28
31
  'normalize_profile',
29
32
  'profile_metrics',
33
+ 'select_grooves',
30
34
  ]
@@ -7,7 +7,7 @@ profile front-end now comes from gratinglab.metrology.core instead of a private
7
7
  copy.
8
8
  """
9
9
  import numpy as np
10
- from scipy.interpolate import interp1d
10
+ from scipy.interpolate import interp1d, make_interp_spline
11
11
  from scipy.ndimage import uniform_filter1d
12
12
 
13
13
  from ..core.processing import extract_single_groove
@@ -30,6 +30,36 @@ def flatten_endpoints(x, y):
30
30
  return y - line
31
31
 
32
32
 
33
+ def select_grooves(x, y, groove_centers, period_nm, margin=0.0,
34
+ min_half_width=10, require_full_period=True):
35
+ """
36
+ The grooves `average_grooves` averages, each centred on its own axis in µm.
37
+
38
+ Separate so that `groove_residuals` measures exactly the set that went
39
+ into the average, chosen by the same rules rather than a second copy of
40
+ them. The parameters mean what they mean in `average_grooves`.
41
+
42
+ Returns:
43
+ a list of (x_g, y_g), possibly empty
44
+ """
45
+ grooves = []
46
+ dx_nm = (x[1] - x[0]) * 1000
47
+ half_width = int(round((period_nm / 2) * (1 + margin) / dx_nm))
48
+
49
+ for center in groove_centers:
50
+ L = min(half_width, center, len(x) - 1 - center)
51
+ if require_full_period and L < half_width:
52
+ continue # the scan edge cuts into this groove's period
53
+ if L > min_half_width:
54
+ x_g, y_g = extract_single_groove(x, y, center, period_nm, margin,
55
+ allow_asymmetric=False)
56
+ # Re-create the exactly symmetric axis the original used, so that
57
+ # floating point matches to the last bit.
58
+ x_g = np.linspace(-L * dx_nm, L * dx_nm, len(y_g)) / 1000
59
+ grooves.append((x_g, y_g))
60
+ return grooves
61
+
62
+
33
63
  def average_grooves(x, y, groove_centers, period_nm, margin=0.0, n_points=2000,
34
64
  min_half_width=10, require_full_period=True):
35
65
  """
@@ -53,21 +83,9 @@ def average_grooves(x, y, groove_centers, period_nm, margin=0.0, n_points=2000,
53
83
  Returns:
54
84
  x_common, y_avg, y_std, n_used
55
85
  """
56
- grooves = []
57
- dx_nm = (x[1] - x[0]) * 1000
58
- half_width = int(round((period_nm / 2) * (1 + margin) / dx_nm))
59
-
60
- for center in groove_centers:
61
- L = min(half_width, center, len(x) - 1 - center)
62
- if require_full_period and L < half_width:
63
- continue # the scan edge cuts into this groove's period
64
- if L > min_half_width:
65
- x_g, y_g = extract_single_groove(x, y, center, period_nm, margin,
66
- allow_asymmetric=False)
67
- # Re-create the exactly symmetric axis the original used, so that
68
- # floating point matches to the last bit.
69
- x_g = np.linspace(-L * dx_nm, L * dx_nm, len(y_g)) / 1000
70
- grooves.append((x_g, y_g))
86
+ grooves = select_grooves(x, y, groove_centers, period_nm, margin=margin,
87
+ min_half_width=min_half_width,
88
+ require_full_period=require_full_period)
71
89
 
72
90
  if len(grooves) == 0:
73
91
  raise ValueError(
@@ -90,6 +108,88 @@ def average_grooves(x, y, groove_centers, period_nm, margin=0.0, n_points=2000,
90
108
  return x_common, np.mean(y_aligned, axis=0), np.std(y_aligned, axis=0), len(grooves)
91
109
 
92
110
 
111
+ def groove_residuals(grooves, x_common, y_avg, n_passes=3, n_steps=6):
112
+ """
113
+ How far each groove departs from the average, once registered to it.
114
+
115
+ This is the RMS residual of each averaged groove about the average: the
116
+ part of the measured profile that does not repeat from one groove to the
117
+ next. It is reported, not applied. `BoundaryProfile.to_problem` and
118
+ docs/theory/metrology.md section 4 say why it is not a Nevot-Croce sigma.
119
+
120
+ A plain difference against `y_avg` does not measure that. Each groove is
121
+ centred on the sample nearest its minimum, so the set is misregistered by
122
+ up to half a pixel, and on a facet steeper than 45 degrees half a pixel of
123
+ shift is several nanometres of height. On the synthetic sawtooth with no
124
+ noise at all, the plain difference reads 2.1 nm. So each groove is first
125
+ registered to the average by a sub-pixel shift and a height offset, fitted
126
+ by Gauss-Newton on a cubic spline of its own samples, and the average is
127
+ recomputed from the registered grooves, `n_passes` times over. The shift is
128
+ bounded to one pixel either way, and the window is the aligned window the
129
+ average uses less one pixel at each end, so a shifted groove is never
130
+ extrapolated.
131
+
132
+ The residual is then taken at each groove's own samples rather than on the
133
+ dense common axis, because interpolating onto that axis smooths pixel-scale
134
+ structure: it read white noise 10 to 15% low. It is scaled by
135
+ sqrt(N / (N - 1)), since the average contains the groove it is compared
136
+ with, and is undefined for a single groove.
137
+
138
+ `y_avg` itself is not changed. It is still the unregistered mean, and is
139
+ what is exported.
140
+
141
+ Returns:
142
+ dict with
143
+ rms_nm: over every groove, or None for fewer than two grooves
144
+ per_groove_nm: one RMS per groove, in the order of `grooves`
145
+ shift_px: each groove's registration shift, in pixels
146
+ """
147
+ n = len(grooves)
148
+ if n < 2:
149
+ return {'rms_nm': None, 'per_groove_nm': np.zeros(0),
150
+ 'shift_px': np.zeros(0)}
151
+
152
+ dx = grooves[0][0][1] - grooves[0][0][0]
153
+ inner = (x_common > x_common[0] + dx) & (x_common < x_common[-1] - dx)
154
+ xi = x_common[inner]
155
+ reference = np.asarray(y_avg, dtype=float)[inner]
156
+ splines = [make_interp_spline(g[0], g[1], k=3) for g in grooves]
157
+ slopes = [spline.derivative() for spline in splines]
158
+ shifts = np.zeros(n)
159
+
160
+ for _ in range(n_passes):
161
+ registered = np.empty((n, len(xi)))
162
+ for k, (spline, slope) in enumerate(zip(splines, slopes)):
163
+ d = shifts[k]
164
+ for _ in range(n_steps):
165
+ r = spline(xi + d) - reference
166
+ # Least squares in (shift, offset). The offset's normal
167
+ # equation removes the means, which leaves one unknown.
168
+ g = slope(xi + d)
169
+ g = g - g.mean()
170
+ denom = np.dot(g, g)
171
+ if denom == 0:
172
+ break
173
+ d = float(np.clip(d - np.dot(g, r - r.mean()) / denom, -dx, dx))
174
+ shifts[k] = d
175
+ curve = spline(xi + d)
176
+ registered[k] = curve - np.mean(curve - reference)
177
+ reference = registered.mean(axis=0)
178
+
179
+ at_reference = make_interp_spline(xi, reference, k=3)
180
+ mean_square = np.empty(n)
181
+ for k, (x_g, y_g) in enumerate(grooves):
182
+ xs = x_g - shifts[k]
183
+ keep = (xs >= xi[0]) & (xs <= xi[-1])
184
+ r = y_g[keep] - at_reference(xs[keep])
185
+ mean_square[k] = np.mean((r - r.mean()) ** 2)
186
+ mean_square *= n / (n - 1)
187
+
188
+ return {'rms_nm': float(np.sqrt(np.mean(mean_square))),
189
+ 'per_groove_nm': np.sqrt(mean_square),
190
+ 'shift_px': shifts / dx}
191
+
192
+
93
193
  def normalize_profile(x_avg, y_avg, period_nm, apply_smoothing=True,
94
194
  smoothing_window=5):
95
195
  """
@@ -25,8 +25,8 @@ from ..core.region_mask import (GROOVE_REACH_PERIODS, bridge_nan,
25
25
  describe_mask, groove_spacings_px, has_mask,
26
26
  reject_grooves_near_nan,
27
27
  tip_correct_around_nan)
28
- from .average import (average_grooves, flatten_endpoints, normalize_profile,
29
- profile_metrics)
28
+ from .average import (average_grooves, flatten_endpoints, groove_residuals,
29
+ normalize_profile, profile_metrics, select_grooves)
30
30
 
31
31
  __all__ = ["BoundaryProfile", "build_boundary_profile"]
32
32
 
@@ -66,6 +66,16 @@ class BoundaryProfile:
66
66
  f"period {self.period_nm:.2f} nm, "
67
67
  f"depth {self.metrics['groove_depth']:.4f} of period")
68
68
 
69
+ @property
70
+ def groove_residual_rms_nm(self):
71
+ """RMS residual of the grooves about their average, in nm.
72
+
73
+ None when fewer than two grooves were averaged. What it measures, and
74
+ why `to_problem` does not use it unless asked to, is in that method's
75
+ docstring and in docs/theory/metrology.md section 4.
76
+ """
77
+ return self.metrics.get('groove_residual_rms_nm')
78
+
69
79
  def to_problem(self, *, coating=None, substrate=None, roughness=0.0,
70
80
  blaze_angle=None):
71
81
  """This measured groove as an efficiency problem.
@@ -87,9 +97,11 @@ class BoundaryProfile:
87
97
  Parameters:
88
98
  coating, substrate: material names, resolved by
89
99
  `gratinglab.materials` at solve time.
90
- roughness: RMS surface roughness in nm. As the note below
91
- explains, this class does not measure it, and 0.0 means "not
92
- supplied", not "smooth".
100
+ roughness: RMS surface roughness in nm, the Nevot-Croce sigma.
101
+ 0.0 means "not supplied", not "smooth". It stays 0.0 unless
102
+ the caller passes a number, including the groove residual
103
+ this class measures; the note below says why that is not
104
+ the default.
93
105
  blaze_angle: fitted facet angle in degrees, if the same scan was
94
106
  also run through `core.analysis.extract_blaze_angle`. Without
95
107
  it the scalar solver evaluates reflection on the mean surface,
@@ -98,13 +110,39 @@ class BoundaryProfile:
98
110
 
99
111
  A note on roughness: `Problem.roughness` means high-spatial-frequency
100
112
  surface roughness, the quantity with which Nevot-Croce damps a Fresnel
101
- coefficient. This class computes no such number. `y_std_nm` is the
102
- groove-to-groove form spread and `metrics['rms_slope']` is a slope
103
- statistic; neither is surface microroughness, and passing either one
104
- in here would produce an efficiency that appears valid but is wrong,
105
- where an omitted value would at least be visibly missing. The
106
- appropriate quantity, the RMS residual of each groove about this
107
- average, is not computed yet.
113
+ coefficient. `y_std_nm` is the groove-to-groove form spread and
114
+ `metrics['rms_slope']` is a slope statistic, and neither is that.
115
+
116
+ `groove_residual_rms_nm` is the nearest thing this class measures: the
117
+ RMS of each groove about the average once registered to it, which is
118
+ the part of the profile that does not repeat with the period. It is
119
+ still not passed by default, for four reasons, each of which makes it
120
+ a different number from the sigma Nevot-Croce wants:
121
+
122
+ - It is measured on the row-averaged profile. Structure that varies
123
+ along a groove averages away over the scan lines, and for isotropic
124
+ roughness with a correlation length well under the scan height that
125
+ is nearly all of it. The number keeps only what is coherent along
126
+ the groove for the height of the scan, so it is a lower bound on the
127
+ surface's roughness in its band.
128
+ - Its band is not the sigma's band. It sees nothing finer than the
129
+ pixel pitch or the tip, a few nanometres, while the structure that
130
+ damps a soft X-ray reflection extends down to wavelengths near the
131
+ X-ray wavelength itself. At the other end it includes groove-scale
132
+ form error, whose correlation length is about a period, where
133
+ Nevot-Croce assumes a short one.
134
+ - Groove-to-groove form error removes light from the orders by
135
+ scattering it between them, a Debye-Waller factor on each order's
136
+ own momentum transfer, not a damping of the Fresnel coefficient at
137
+ the facet. Folding it into the facet reflectivity applies the wrong
138
+ factor to the right effect.
139
+ - It carries instrument terms: AFM noise divided by the square root of
140
+ the number of scan lines, and a floor where the surface has corners
141
+ sharper than a pixel (0.21 nm on the synthetic sawtooth).
142
+
143
+ A caller who accepts those can pass it, and the name then appears in
144
+ their own code, which is the point:
145
+ `profile.to_problem(roughness=profile.groove_residual_rms_nm)`.
108
146
  """
109
147
  # Imported here rather than at module scope: `boundary` is reachable
110
148
  # from the GUI preview, which has no reason to pull the solver stack in
@@ -217,6 +255,16 @@ def build_boundary_profile(data, scan_x_size, settings, orientation=None) -> Bou
217
255
  smoothing_window=settings.ggp_smoothing_window)
218
256
 
219
257
  metrics = profile_metrics(x_norm, y_norm, period_nm, n_used)
258
+ # The same grooves average_grooves used, chosen by the same rules.
259
+ residual = groove_residuals(
260
+ select_grooves(raw_x, flat_y, groove_centers, period_nm, margin=0.0,
261
+ min_half_width=settings.ggp_min_half_width),
262
+ x_avg, y_avg)
263
+ metrics['groove_residual_rms_nm'] = residual['rms_nm']
264
+ metrics['groove_residual_per_groove_nm'] = residual['per_groove_nm'].tolist()
265
+ metrics['groove_residual_max_shift_px'] = (
266
+ float(np.max(np.abs(residual['shift_px'])))
267
+ if residual['rms_nm'] is not None else None)
220
268
  if orientation is not None:
221
269
  metrics.update(orientation.as_metrics())
222
270
  metrics['tip_correction'] = settings.tip_correction
@@ -275,6 +275,11 @@ class BoundaryView(QWidget):
275
275
  f"+/- {m['groove_rotation_stderr_deg']:.3f} deg"
276
276
  + (", corrected" if m.get('groove_rotation_corrected')
277
277
  else ", not corrected"))
278
+ rms = m.get('groove_residual_rms_nm')
279
+ # Labelled as a residual, not a roughness: it is not what to_problem
280
+ # applies, and the panel should not suggest otherwise.
281
+ residual = ("not measured, one groove" if rms is None
282
+ else f"{rms:.3f} nm, not applied")
278
283
  self.metrics_label.setText(
279
284
  f"Period : {p.period_nm:.2f} nm\n"
280
285
  f"Grooves averaged: {p.n_used} of {p.n_grooves}{edge}\n"
@@ -282,7 +287,8 @@ class BoundaryView(QWidget):
282
287
  f"Peak-to-valley : {m['peak_to_valley']:.4f} of period\n"
283
288
  f"RMS slope : {m['rms_slope']:.4f}\n"
284
289
  f"Max sidewall : {m['max_angle_deg']:.2f} deg\n"
285
- f"Max curvature : {m['max_curvature']:.4f}{rotation}{tip}")
290
+ f"Max curvature : {m['max_curvature']:.4f}\n"
291
+ f"Groove residual : {residual}{rotation}{tip}")
286
292
 
287
293
  def _show_placeholder(self, message="Load an AFM file in the Import tab"):
288
294
  self._placeholder = message
@@ -0,0 +1,95 @@
1
+ """
2
+ The controls-and-plot layout every AFM tab shares.
3
+
4
+ Each tab was a fixed 272 px column of controls in a 292 px scroll area beside
5
+ its plot, so long labels were crammed and nothing could be widened. A splitter
6
+ replaces that: the column starts wide enough for its contents, cannot be
7
+ dragged narrower than its widest control needs, and gives the plot every pixel
8
+ the window gains.
9
+
10
+ The width is one setting, not one per tab. Dragging the column in any tab
11
+ moves it in the others, so switching tabs never makes the plot jump, and the
12
+ width is saved as it is dragged, so the window opens at it next time. It is
13
+ kept in the same store as the theme (``theming.settings``), which the test
14
+ suite redirects to a scratch file.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import weakref
19
+
20
+ from . import * # noqa: F401,F403 - sets QT_API before matplotlib's shim loads
21
+
22
+ from PySide6.QtCore import Qt
23
+ from PySide6.QtWidgets import QFrame, QScrollArea, QSplitter, QWidget
24
+
25
+ from gratinglab.gui.qt.theming import settings
26
+
27
+ __all__ = ["controls_beside", "MIN_CONTROLS_WIDTH", "START_CONTROLS_WIDTH",
28
+ "WIDTH_KEY"]
29
+
30
+ #: Narrower than this and the group boxes' labels start to wrap mid-word.
31
+ MIN_CONTROLS_WIDTH = 260
32
+ #: Where the column starts, unless its contents ask for more.
33
+ START_CONTROLS_WIDTH = 320
34
+ #: Where the chosen width is remembered.
35
+ WIDTH_KEY = "afm/controls_width"
36
+
37
+ # Every live splitter, so a drag in one tab can move the others. Weak, so a
38
+ # closed window's splitters drop out on their own.
39
+ _SPLITTERS: "weakref.WeakSet[QSplitter]" = weakref.WeakSet()
40
+
41
+
42
+ def _saved_width() -> int | None:
43
+ value = settings().value(WIDTH_KEY)
44
+ try:
45
+ return int(value) if value is not None else None
46
+ except (TypeError, ValueError):
47
+ return None # a hand-edited or corrupt value: fall back to the default
48
+
49
+
50
+ def _follow(source: QSplitter) -> None:
51
+ """Remember ``source``'s column width and give it to every other tab."""
52
+ width = source.sizes()[0]
53
+ store = settings()
54
+ store.setValue(WIDTH_KEY, width)
55
+ store.sync()
56
+ for other in list(_SPLITTERS):
57
+ if other is source:
58
+ continue
59
+ try:
60
+ total = sum(other.sizes())
61
+ other.blockSignals(True)
62
+ other.setSizes([width, max(1, total - width)])
63
+ other.blockSignals(False)
64
+ except RuntimeError:
65
+ # Its window was closed and Qt has deleted it, while the Python
66
+ # wrapper waits for collection.
67
+ _SPLITTERS.discard(other)
68
+
69
+
70
+ def controls_beside(controls: QWidget, plot: QWidget) -> QSplitter:
71
+ """A splitter holding the scrolling ``controls`` column and the ``plot``."""
72
+ scroll = QScrollArea()
73
+ scroll.setWidget(controls)
74
+ scroll.setWidgetResizable(True)
75
+ scroll.setFrameShape(QFrame.NoFrame)
76
+ # Width is the splitter's to decide; only the height should ever scroll.
77
+ scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
78
+
79
+ gutter = scroll.verticalScrollBar().sizeHint().width()
80
+ floor = max(MIN_CONTROLS_WIDTH, controls.minimumSizeHint().width() + gutter)
81
+ scroll.setMinimumWidth(floor)
82
+
83
+ splitter = QSplitter(Qt.Horizontal)
84
+ splitter.setChildrenCollapsible(False)
85
+ splitter.addWidget(scroll)
86
+ splitter.addWidget(plot)
87
+ splitter.setStretchFactor(0, 0)
88
+ splitter.setStretchFactor(1, 1)
89
+ # A remembered width wins, but never below what this tab's controls need.
90
+ start = max(_saved_width() or START_CONTROLS_WIDTH, floor)
91
+ splitter.setSizes([start, max(1, 4 * start)])
92
+
93
+ _SPLITTERS.add(splitter)
94
+ splitter.splitterMoved.connect(lambda *_: _follow(splitter))
95
+ return splitter
@@ -25,6 +25,16 @@ def write_profile_metrics(path, metrics):
25
25
  f.write(f"RMS slope: {metrics['rms_slope']:.4f} (normalized units)\n")
26
26
  f.write(f"Max slope magnitude: {metrics['max_slope']:.4f}\n")
27
27
  f.write(f"Max curvature: {metrics['max_curvature']:.6f} (normalized units)\n")
28
+ # Written only by a pipeline that measures it, so a metrics dict from
29
+ # before it existed still writes. Not applied as roughness: see
30
+ # BoundaryProfile.to_problem.
31
+ if 'groove_residual_rms_nm' in metrics:
32
+ rms = metrics['groove_residual_rms_nm']
33
+ if rms is None:
34
+ f.write("Groove residual RMS: not measured (one groove)\n")
35
+ else:
36
+ f.write(f"Groove residual RMS: {rms:.3f} nm (about the average, "
37
+ f"not applied as roughness)\n")
28
38
  # Present only when the pipeline ran a tip correction. Stated even so:
29
39
  # a corrected depth and an uncorrected one are different measurements,
30
40
  # and this sidecar is where a reader learns which this file holds.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gratinglab
3
- Version: 0.2.4
3
+ Version: 0.2.6
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
@@ -145,6 +145,7 @@ tests/test_conditioning.py
145
145
  tests/test_convergence.py
146
146
  tests/test_cross_method.py
147
147
  tests/test_efficiency_table.py
148
+ tests/test_four_way.py
148
149
  tests/test_fresnel.py
149
150
  tests/test_geometry.py
150
151
  tests/test_ggp.py