gratinglab 0.2.1__tar.gz → 0.2.3__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.1/src/gratinglab.egg-info → gratinglab-0.2.3}/PKG-INFO +3 -3
  2. {gratinglab-0.2.1 → gratinglab-0.2.3}/pyproject.toml +2 -2
  3. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/__init__.py +1 -1
  4. gratinglab-0.2.3/src/gratinglab/gui/__init__.py +30 -0
  5. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/app.py +4 -12
  6. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/launcher.py +5 -13
  7. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/qt/__init__.py +4 -5
  8. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/qt/main_window.py +10 -9
  9. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/qt/worker.py +15 -17
  10. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/state.py +6 -7
  11. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/theme.py +22 -0
  12. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/config.py +13 -17
  13. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/gui/__init__.py +11 -13
  14. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/gui/qt/__init__.py +8 -6
  15. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/gui/qt/boundary_view.py +11 -20
  16. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/gui/qt/canvas.py +18 -13
  17. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/gui/qt/efficiency_dialog.py +3 -2
  18. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/gui/qt/import_view.py +11 -21
  19. gratinglab-0.2.3/src/gratinglab/metrology/gui/qt/layout.py +46 -0
  20. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/gui/qt/main_window.py +11 -23
  21. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/gui/qt/swath_dialog.py +3 -2
  22. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/wiki/__init__.py +10 -20
  23. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/base.py +8 -1
  24. {gratinglab-0.2.1 → gratinglab-0.2.3/src/gratinglab.egg-info}/PKG-INFO +3 -3
  25. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab.egg-info/SOURCES.txt +1 -1
  26. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab.egg-info/requires.txt +2 -2
  27. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_gui_qt.py +35 -30
  28. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_solver_progress.py +27 -0
  29. gratinglab-0.2.1/src/gratinglab/gui/qt/gc_guard.py +0 -102
  30. gratinglab-0.2.1/src/gratinglab/io/__init__.py +0 -0
  31. {gratinglab-0.2.1 → gratinglab-0.2.3}/LICENSE +0 -0
  32. {gratinglab-0.2.1 → gratinglab-0.2.3}/README.md +0 -0
  33. {gratinglab-0.2.1 → gratinglab-0.2.3}/setup.cfg +0 -0
  34. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/checks.py +0 -0
  35. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/compare.py +0 -0
  36. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/convergence.py +0 -0
  37. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/geometry.py +0 -0
  38. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/cmethod_options.py +0 -0
  39. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/diagram.py +0 -0
  40. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/diagram3d.py +0 -0
  41. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/docs.py +0 -0
  42. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/efficiency_plot.py +0 -0
  43. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/icon.py +0 -0
  44. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/integral_options.py +0 -0
  45. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/mathtext.py +0 -0
  46. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/orders.py +0 -0
  47. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/provenance.py +0 -0
  48. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/qt/cmethod_tab.py +0 -0
  49. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/qt/geometry_panel.py +0 -0
  50. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/qt/geometry_tab.py +0 -0
  51. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/qt/integral_tab.py +0 -0
  52. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/qt/launcher.py +0 -0
  53. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/qt/profile_plot_panel.py +0 -0
  54. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/qt/rcwa_tab.py +0 -0
  55. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/qt/scalar_tab.py +0 -0
  56. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/qt/scene3d_panel.py +0 -0
  57. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/qt/setup_tab.py +0 -0
  58. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/qt/sizing.py +0 -0
  59. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/qt/theming.py +0 -0
  60. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/qt/theory_viewer.py +0 -0
  61. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/rcwa_options.py +0 -0
  62. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/richtext.py +0 -0
  63. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/scalar_options.py +0 -0
  64. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/illumination.py +0 -0
  65. {gratinglab-0.2.1/src/gratinglab/gui → gratinglab-0.2.3/src/gratinglab/io}/__init__.py +0 -0
  66. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/io/boundary.py +0 -0
  67. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/io/efficiency_table.py +0 -0
  68. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/io/ggp.py +0 -0
  69. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/materials/__init__.py +0 -0
  70. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/materials/data/SOURCES.md +0 -0
  71. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/materials/fresnel.py +0 -0
  72. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/materials/optical.py +0 -0
  73. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/materials/refractiveindex.py +0 -0
  74. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/measured.py +0 -0
  75. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/__init__.py +0 -0
  76. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/analyzer.py +0 -0
  77. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/boundary/__init__.py +0 -0
  78. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/boundary/average.py +0 -0
  79. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/boundary/pipeline.py +0 -0
  80. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/boundary/swaths.py +0 -0
  81. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/cli.py +0 -0
  82. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/core/__init__.py +0 -0
  83. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/core/analysis.py +0 -0
  84. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/core/image_flatten.py +0 -0
  85. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/core/orientation.py +0 -0
  86. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/core/processing.py +0 -0
  87. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/core/region_mask.py +0 -0
  88. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/core/scars.py +0 -0
  89. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/core/tip.py +0 -0
  90. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/data/__init__.py +0 -0
  91. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/data/aggregation.py +0 -0
  92. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/gui/icon.py +0 -0
  93. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/gui/qt/app.py +0 -0
  94. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/gui/qt/wiki_view.py +0 -0
  95. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/gui/qt/worker.py +0 -0
  96. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/gui/state.py +0 -0
  97. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/io/__init__.py +0 -0
  98. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/io/file_io.py +0 -0
  99. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/io/ibw.py +0 -0
  100. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/io/metrics.py +0 -0
  101. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/io/spm.py +0 -0
  102. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/settings.py +0 -0
  103. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/stats/__init__.py +0 -0
  104. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/stats/analysis.py +0 -0
  105. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/stats/icc.py +0 -0
  106. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/visualization/__init__.py +0 -0
  107. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/visualization/diagnostics.py +0 -0
  108. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/visualization/profiles.py +0 -0
  109. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/visualization/statistics.py +0 -0
  110. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/wiki/facet-fitting.md +0 -0
  111. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/wiki/file-formats.md +0 -0
  112. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/wiki/flattening.md +0 -0
  113. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/wiki/groove-orientation.md +0 -0
  114. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/wiki/icc-correction.md +0 -0
  115. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/wiki/overview.md +0 -0
  116. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/wiki/reading-outputs.md +0 -0
  117. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/wiki/row-groups.md +0 -0
  118. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/wiki/scars-and-masking.md +0 -0
  119. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/wiki/swath-statistics.md +0 -0
  120. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/wiki/tip-correction.md +0 -0
  121. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/workflows.py +0 -0
  122. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/problem.py +0 -0
  123. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/profiles.py +0 -0
  124. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/resolution.py +0 -0
  125. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/result.py +0 -0
  126. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/__init__.py +0 -0
  127. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/_conditioning.py +0 -0
  128. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/_execution.py +0 -0
  129. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/cmethod/__init__.py +0 -0
  130. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/cmethod/_core.py +0 -0
  131. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/cmethod/_series.py +0 -0
  132. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/integral/__init__.py +0 -0
  133. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/integral/_boundary.py +0 -0
  134. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/integral/_core.py +0 -0
  135. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/integral/_finite.py +0 -0
  136. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/integral/_greens.py +0 -0
  137. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/integral/_kernels.py +0 -0
  138. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/integral/_nystrom.py +0 -0
  139. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/rcwa/__init__.py +0 -0
  140. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/rcwa/_core.py +0 -0
  141. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/rcwa/_stack.py +0 -0
  142. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/scalar.py +0 -0
  143. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab.egg-info/dependency_links.txt +0 -0
  144. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab.egg-info/entry_points.txt +0 -0
  145. {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab.egg-info/top_level.txt +0 -0
  146. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_changelog.py +0 -0
  147. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_checks.py +0 -0
  148. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_cmethod.py +0 -0
  149. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_compare.py +0 -0
  150. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_conditioning.py +0 -0
  151. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_convergence.py +0 -0
  152. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_cross_method.py +0 -0
  153. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_efficiency_table.py +0 -0
  154. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_fresnel.py +0 -0
  155. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_geometry.py +0 -0
  156. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_ggp.py +0 -0
  157. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_gui_cmethod_options.py +0 -0
  158. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_gui_cmethod_tab.py +0 -0
  159. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_gui_diagram.py +0 -0
  160. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_gui_diagram3d.py +0 -0
  161. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_gui_docs.py +0 -0
  162. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_gui_integral_options.py +0 -0
  163. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_gui_launcher.py +0 -0
  164. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_gui_mathtext.py +0 -0
  165. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_gui_orders.py +0 -0
  166. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_gui_provenance.py +0 -0
  167. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_gui_rcwa_options.py +0 -0
  168. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_gui_richtext.py +0 -0
  169. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_gui_scalar_options.py +0 -0
  170. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_gui_state.py +0 -0
  171. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_gui_theme.py +0 -0
  172. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_illumination.py +0 -0
  173. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_integral_core.py +0 -0
  174. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_integral_corpus.py +0 -0
  175. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_integral_finite.py +0 -0
  176. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_integral_graded.py +0 -0
  177. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_integral_greens.py +0 -0
  178. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_integral_kernels.py +0 -0
  179. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_integral_operators.py +0 -0
  180. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_integral_solver.py +0 -0
  181. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_io_boundary.py +0 -0
  182. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_materials.py +0 -0
  183. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_measured.py +0 -0
  184. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_perturbation.py +0 -0
  185. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_profiles.py +0 -0
  186. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_rayleigh_anomaly.py +0 -0
  187. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_rcwa.py +0 -0
  188. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_rcwa_benchmarks.py +0 -0
  189. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_refractiveindex.py +0 -0
  190. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_resolution.py +0 -0
  191. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_result.py +0 -0
  192. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_scalar.py +0 -0
  193. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_solver_conformance.py +0 -0
  194. {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_solver_execution.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gratinglab
3
- Version: 0.2.1
3
+ Version: 0.2.3
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 @@ License-File: LICENSE
22
22
  Requires-Dist: numpy>=1.26
23
23
  Requires-Dist: scipy>=1.11
24
24
  Requires-Dist: pydantic>=2.6
25
- Requires-Dist: endstation>=0.2.2
25
+ Requires-Dist: endstation>=0.2.6
26
26
  Requires-Dist: threadpoolctl>=3.1
27
27
  Provides-Extra: dev
28
28
  Requires-Dist: pytest>=7; extra == "dev"
@@ -34,7 +34,7 @@ Provides-Extra: gui
34
34
  Requires-Dist: matplotlib>=3.8; extra == "gui"
35
35
  Requires-Dist: PySide6-Essentials!=6.12.0,>=6.7; python_version < "3.12" and extra == "gui"
36
36
  Requires-Dist: PySide6-Essentials>=6.7; python_version >= "3.12" and extra == "gui"
37
- Requires-Dist: endstation[qt]>=0.2.2; extra == "gui"
37
+ Requires-Dist: endstation[qt]>=0.2.6; extra == "gui"
38
38
  Provides-Extra: metrology
39
39
  Requires-Dist: matplotlib>=3.9; extra == "metrology"
40
40
  Provides-Extra: refractiveindex
@@ -51,7 +51,7 @@ dependencies = [
51
51
  # `dependencies = []` and has a test that fails if that stops being true,
52
52
  # so this line adds no transitive weight -- the Qt and matplotlib parts sit
53
53
  # behind its own extras, which our `gui` extra asks for by name.
54
- "endstation>=0.2.2",
54
+ "endstation>=0.2.6",
55
55
  # The rigorous solvers cap BLAS threads for the length of a solve
56
56
  # (solvers/_execution.py): OpenBLAS's thread-per-core default made RCWA
57
57
  # up to 135x slower on a busy machine, measured in benchmarks/
@@ -85,7 +85,7 @@ gui = [
85
85
  "matplotlib>=3.8",
86
86
  "PySide6-Essentials>=6.7,!=6.12.0; python_version < '3.12'",
87
87
  "PySide6-Essentials>=6.7; python_version >= '3.12'",
88
- "endstation[qt]>=0.2.2",
88
+ "endstation[qt]>=0.2.6",
89
89
  ]
90
90
  # Groove metrology: reading AFM scans and averaging them into a boundary
91
91
  # profile. An extra for the same reason the GUI is one -- `pip install
@@ -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.1"
25
+ __version__ = "0.2.3"
26
26
 
27
27
  __all__ = [
28
28
  "Illumination",
@@ -0,0 +1,30 @@
1
+ """The desktop layer: two windows and the chooser that opens them.
2
+
3
+ Only :mod:`gratinglab.gui.qt` may import a toolkit. This module stays
4
+ toolkit-free so the console-script entry points, which live beside it, can
5
+ explain a missing PySide6 before anything tries to import it.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from endstation.optional import MissingExtra, require_extra
11
+
12
+ __all__ = ["STILL_WORKS", "exit_without_qt"]
13
+
14
+ #: What a missing toolkit leaves working. Without it, a user who wanted only
15
+ #: the solvers reads an absent GUI extra as a broken install.
16
+ STILL_WORKS = "Everything except the GUI works without it."
17
+
18
+
19
+ def exit_without_qt() -> None:
20
+ """Exit with an install instruction if PySide6 is not installed.
21
+
22
+ The check is :func:`endstation.optional.require_extra`, a ``find_spec``
23
+ call rather than an import. Its :class:`~endstation.optional.MissingExtra`
24
+ becomes ``SystemExit`` here, so a console script prints the message and
25
+ not a traceback naming a package the user never asked for.
26
+ """
27
+ try:
28
+ require_extra("PySide6", "gui", still_works=STILL_WORKS)
29
+ except MissingExtra as exc:
30
+ raise SystemExit(str(exc)) from exc
@@ -15,22 +15,14 @@ from __future__ import annotations
15
15
 
16
16
  import sys
17
17
 
18
- _QT_HELP = """\
19
- gratinglab-gui needs PySide6, which this install does not have.
20
-
21
- pip install -e ".[gui]" (from a checkout)
22
- pip install gratinglab[gui] (once published)
23
-
24
- Everything except the GUI works without it.
25
- """
18
+ from . import exit_without_qt
26
19
 
27
20
 
28
21
  def _require_qt():
29
22
  """Import PySide6 with an explanation instead of a traceback."""
30
- try:
31
- from PySide6.QtWidgets import QApplication
32
- except ImportError as exc: # pragma: no cover - depends on the install
33
- raise SystemExit(_QT_HELP) from exc
23
+ exit_without_qt()
24
+ from PySide6.QtWidgets import QApplication
25
+
34
26
  return QApplication
35
27
 
36
28
 
@@ -28,16 +28,9 @@ from __future__ import annotations
28
28
  import sys
29
29
  from dataclasses import dataclass
30
30
 
31
- __all__ = ["Mode", "MODES", "mode", "main"]
32
-
33
- _QT_HELP = """\
34
- gratinglab-app needs PySide6, which this install does not have.
35
-
36
- pip install -e ".[gui]" (from a checkout)
37
- pip install gratinglab[gui]
31
+ from . import exit_without_qt
38
32
 
39
- Everything except the GUI works without it.
40
- """
33
+ __all__ = ["Mode", "MODES", "mode", "main"]
41
34
 
42
35
 
43
36
  @dataclass(frozen=True)
@@ -91,10 +84,9 @@ def mode(key: str) -> Mode:
91
84
 
92
85
  def _require_qt():
93
86
  """Import PySide6 with an explanation instead of a traceback."""
94
- try:
95
- from PySide6.QtWidgets import QApplication
96
- except ImportError as exc: # pragma: no cover - depends on the install
97
- raise SystemExit(_QT_HELP) from exc
87
+ exit_without_qt()
88
+ from PySide6.QtWidgets import QApplication
89
+
98
90
  return QApplication
99
91
 
100
92
 
@@ -14,9 +14,8 @@ headlessly.
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
- import os
18
-
19
17
  # matplotlib's Qt shim picks a binding from sys.modules first and QT_API
20
- # second. Without this, a stray PyQt5 in the environment would win and its
21
- # canvas would refuse to parent into a PySide6 window.
22
- os.environ.setdefault("QT_API", "PySide6")
18
+ # second. Without QT_API, a stray PyQt5 in the environment would win and its
19
+ # canvas would refuse to parent into a PySide6 window. Importing endstation.qt
20
+ # sets it, with setdefault, before anything here imports matplotlib.
21
+ import endstation.qt # noqa: F401
@@ -25,6 +25,7 @@ from __future__ import annotations
25
25
 
26
26
  import threading
27
27
 
28
+ from endstation.qt.tokens import StaleGuard
28
29
  from PySide6.QtCore import Qt, QThread, QTimer, Signal
29
30
  from PySide6.QtGui import QKeySequence
30
31
  from PySide6.QtWidgets import (
@@ -131,12 +132,12 @@ class MainWindow(QMainWindow):
131
132
 
132
133
  self._themes = theming.install(QApplication.instance())
133
134
  # Solves run on a worker thread; keep the cycle collector off it.
134
- from .gc_guard import guard
135
+ from endstation.qt.gc_guard import guard
135
136
 
136
137
  guard(self)
137
138
 
138
139
  self._parsed = None
139
- self._token = 0
140
+ self._tokens = StaleGuard()
140
141
  self._running = False
141
142
  self._active_name: str | None = None
142
143
  # Replaced per solve rather than reused, so a Cancel clicked between
@@ -441,7 +442,7 @@ class MainWindow(QMainWindow):
441
442
 
442
443
  self._active_name = name
443
444
  self._parsed = geometry
444
- self._token += 1
445
+ token = self._tokens.issue()
445
446
  self._cancel = threading.Event()
446
447
  self._set_running(True)
447
448
  tab.show_solving(len(geometry.wavelengths))
@@ -455,7 +456,7 @@ class MainWindow(QMainWindow):
455
456
  _PROGRESS_DELAY_MS, tab, lambda: self._show_progress_if_still_running(tab)
456
457
  )
457
458
  signal = self._convergence_requested if sweep else self._requested
458
- signal.emit(self._token, name, geometry, options, self._cancel)
459
+ signal.emit(token, name, geometry, options, self._cancel)
459
460
 
460
461
  def cancel(self) -> None:
461
462
  """Stop the running solve.
@@ -475,13 +476,13 @@ class MainWindow(QMainWindow):
475
476
  if not self._running:
476
477
  return
477
478
  self._cancel.set()
478
- self._token += 1
479
+ self._tokens.invalidate()
479
480
  self._set_running(False)
480
481
  if self._active_name is not None:
481
482
  self.tabs[self._active_name].show_cancelled()
482
483
 
483
484
  def _on_solved(self, token: int, method: str, result) -> None:
484
- if token != self._token:
485
+ if self._tokens.is_stale(token):
485
486
  return # cancelled or superseded; the window has moved on
486
487
  self._set_running(False)
487
488
  # No `profile_panel.draw` here any more: the groove's shape owes
@@ -501,7 +502,7 @@ class MainWindow(QMainWindow):
501
502
  be enough to move a bar backwards over a result the window has already
502
503
  replaced.
503
504
  """
504
- if token != self._token or self._active_name is None:
505
+ if self._tokens.is_stale(token) or self._active_name is None:
505
506
  return
506
507
  self.tabs[self._active_name].show_progress_value(done, total)
507
508
 
@@ -515,7 +516,7 @@ class MainWindow(QMainWindow):
515
516
  """
516
517
 
517
518
  def _on_converged(self, token: int, method: str, result, report) -> None:
518
- if token != self._token:
519
+ if self._tokens.is_stale(token):
519
520
  return
520
521
  self._set_running(False)
521
522
  self.geometry_tab.show_geometry(self._parsed)
@@ -525,7 +526,7 @@ class MainWindow(QMainWindow):
525
526
  self.solved.emit()
526
527
 
527
528
  def _on_failed(self, token: int, method: str, message: str) -> None:
528
- if token != self._token:
529
+ if self._tokens.is_stale(token):
529
530
  return
530
531
  self._set_running(False)
531
532
  self.tabs[method].show_error(message)
@@ -10,7 +10,8 @@ only the latter, because ``sweep`` reaches a NumPy-bound call with no check
10
10
  point and a Python thread cannot be killed, so there was nothing to interrupt
11
11
  from here. `Capabilities.reports_progress` changed that: the solver calls back
12
12
  once per wavelength, and a callback that raises
13
- :class:`~gratinglab.solvers.base.SolveCancelled` unwinds it.
13
+ :class:`~endstation.progress.Cancelled` (or its subclass
14
+ :class:`~gratinglab.solvers.base.SolveCancelled`) unwinds it.
14
15
 
15
16
  The token is still needed, because real cancellation does not remove the race
16
17
  it was covering. The worker may already have finished when Cancel is clicked,
@@ -28,10 +29,9 @@ from __future__ import annotations
28
29
  import threading
29
30
  from typing import TYPE_CHECKING, NamedTuple
30
31
 
32
+ from endstation.progress import Cancelled, cancelling, throttled
31
33
  from PySide6.QtCore import QObject, Signal, Slot
32
34
 
33
- from ...solvers.base import SolveCancelled
34
-
35
35
  if TYPE_CHECKING: # pragma: no cover - imports for type checkers only
36
36
  from ...checks import EnergyReport
37
37
  from ...result import EfficiencyScan
@@ -123,7 +123,7 @@ class SolveWorker(QObject):
123
123
  progress=self._reporter(token, cancel),
124
124
  )[0]
125
125
  self.finished.emit(token, method, SolveResult(scan, _energy(scan)))
126
- except SolveCancelled:
126
+ except Cancelled:
127
127
  # Before the broad handler: the user asked for this, so
128
128
  # it is not a failure and rendering it as one would be the same
129
129
  # category mistake M8 fixed for the no-coating default.
@@ -176,7 +176,7 @@ class SolveWorker(QObject):
176
176
  SolveResult(report.scan, _energy(report.scan)),
177
177
  report,
178
178
  )
179
- except SolveCancelled:
179
+ except Cancelled:
180
180
  self.cancelled.emit(token, method)
181
181
  except Exception as exc: # noqa: BLE001 - see `run`
182
182
  self.failed.emit(token, method, f"{type(exc).__name__}: {exc}")
@@ -190,16 +190,14 @@ class SolveWorker(QObject):
190
190
  that could not have been seen. The cancellation check is not
191
191
  throttled: it runs on every call, so a Cancel click takes effect at
192
192
  the next wavelength instead of the next percent.
193
+
194
+ Both halves are endstation's: :func:`~endstation.progress.throttled`
195
+ for the bar and :func:`~endstation.progress.cancelling` for the stop,
196
+ which raises :class:`~endstation.progress.Cancelled`, the base class
197
+ of :class:`~gratinglab.solvers.base.SolveCancelled`.
193
198
  """
194
- last = -1
195
-
196
- def report(done: int, total: int) -> None:
197
- nonlocal last
198
- percent = done * 100 // total if total else 100
199
- if percent != last:
200
- last = percent
201
- self.progress.emit(token, done, total)
202
- if cancel.is_set():
203
- raise SolveCancelled(f"cancelled at {done} of {total}")
204
-
205
- return report
199
+
200
+ def emit(done: int, total: int) -> None:
201
+ self.progress.emit(token, done, total)
202
+
203
+ return cancelling(throttled(emit), cancel.is_set)
@@ -20,6 +20,7 @@ from __future__ import annotations
20
20
  from dataclasses import dataclass, replace
21
21
 
22
22
  from endstation.forms import FieldError as _FieldError
23
+ from endstation.forms import FormErrors as _FormErrors
23
24
  from pathlib import Path
24
25
  from typing import Any
25
26
 
@@ -79,13 +80,11 @@ ANGLE_LABELS: dict[str, tuple[str, str | None]] = {
79
80
  #: from the module it is used in.
80
81
  FieldError = _FieldError
81
82
 
82
-
83
- class FormErrors(ValueError):
84
- """Raised by :func:`build` when a form cannot be turned into a problem."""
85
-
86
- def __init__(self, errors: tuple[FieldError, ...]) -> None:
87
- self.errors = errors
88
- super().__init__("; ".join(str(e) for e in errors))
83
+ #: Raised by :func:`build`, and by each tab's ``build_options``, when a form
84
+ #: cannot be turned into a problem. endstation's, re-exported for the same
85
+ #: reason as `FieldError`. It derives from ``Exception``, not ``ValueError``
86
+ #: as this module's own copy did; nothing here caught it as a ValueError.
87
+ FormErrors = _FormErrors
89
88
 
90
89
 
91
90
  @dataclass(frozen=True, slots=True)
@@ -37,6 +37,7 @@ __all__ = [
37
37
  "order_colors",
38
38
  "provenance_colors",
39
39
  "rc_params",
40
+ "set_titles",
40
41
  "style_axes",
41
42
  "style_figure",
42
43
  ]
@@ -308,6 +309,27 @@ def style_axes(axes, theme: Theme) -> None:
308
309
  axis.line.set_color(theme.ink)
309
310
 
310
311
 
312
+ #: Plot titles: what the panel is, at this size and weight, left-aligned.
313
+ TITLE_SIZE = 11
314
+ TITLE_WEIGHT = "semibold"
315
+ #: The figures a panel reports beside its title, smaller and in ``muted``.
316
+ DETAIL_SIZE = 9
317
+
318
+
319
+ def set_titles(axes, title: str, detail: str | None = None, theme: Theme | None = None) -> None:
320
+ """Title a panel: its name on the left, the numbers it reports on the right.
321
+
322
+ One style for every plot, so a title says what the panel is and never has
323
+ to carry a measurement, a setting or an explanation on a second line. The
324
+ detail is where those go, quieter than the title. Pass ``theme`` from a
325
+ caller that has one; otherwise the plain ``LIGHT`` colours are used.
326
+ """
327
+ theme = theme or LIGHT
328
+ axes.set_title(title, loc="left", fontsize=TITLE_SIZE, fontweight=TITLE_WEIGHT,
329
+ color=theme.ink)
330
+ axes.set_title(detail or "", loc="right", fontsize=DETAIL_SIZE, color=theme.muted)
331
+
332
+
311
333
  def style_figure(figure, theme: Theme) -> None:
312
334
  """Restyle an existing figure and every axes on it."""
313
335
  figure.set_facecolor(theme.figure)
@@ -3,6 +3,8 @@ Configuration file for AFM blaze angle analysis
3
3
  """
4
4
  import os
5
5
 
6
+ from endstation.paths import data_root, find_project_root
7
+
6
8
  # ============ PROJECT PATHS ============
7
9
  # All paths below are resolved relative to the project root, not the current
8
10
  # working directory, so the analysis runs the same from anywhere.
@@ -16,22 +18,16 @@ import os
16
18
  # An installed (non-editable) copy has no project root above it; fall back to the
17
19
  # working directory so `pip install gratinglab[metrology]` still runs against a user's
18
20
  # own data and results folders.
19
- def _find_project_root(start):
20
- # pyproject.toml only. Searching for a "data" directory looked reasonable and
21
- # was wrong: this package contains its own data subpackage
22
- # (gratinglab/metrology/data/aggregation.py), so the walk stopped immediately and
23
- # DATA_DIR pointed inside the source tree.
24
- path = os.path.dirname(os.path.abspath(start))
25
- while True:
26
- if os.path.isfile(os.path.join(path, 'pyproject.toml')):
27
- return path
28
- parent = os.path.dirname(path)
29
- if parent == path:
30
- return os.getcwd()
31
- path = parent
32
-
33
-
34
- PROJECT_ROOT = _find_project_root(__file__)
21
+ #
22
+ # pyproject.toml only, not endstation's default markers. Searching for a "data"
23
+ # directory looked reasonable and was wrong: this package contains its own data
24
+ # subpackage (gratinglab/metrology/data/aggregation.py), so the walk stopped
25
+ # immediately and DATA_DIR pointed inside the source tree. A `.git` marker is
26
+ # no better: an installed copy inside some other checkout would claim that
27
+ # checkout's root.
28
+ #
29
+ # Kept as a str, as it always was: every consumer builds paths with os.path.
30
+ PROJECT_ROOT = str(find_project_root(__file__, markers=("pyproject.toml",)))
35
31
  RESULTS_DIR = os.path.join(PROJECT_ROOT, 'results')
36
32
 
37
33
  # Scans live *outside* the checkout, and are not committed.
@@ -45,7 +41,7 @@ RESULTS_DIR = os.path.join(PROJECT_ROOT, 'results')
45
41
  #
46
42
  # Point GRATINGLAB_AFM_DIR somewhere else to override.
47
43
  DEFAULT_AFM_DIR = os.path.join(os.path.expanduser('~'), 'Documents', 'afm_scans')
48
- DATA_DIR = os.environ.get('GRATINGLAB_AFM_DIR') or DEFAULT_AFM_DIR
44
+ DATA_DIR = str(data_root('GRATINGLAB_AFM_DIR', DEFAULT_AFM_DIR))
49
45
 
50
46
 
51
47
  def resolve_path(name):
@@ -23,15 +23,16 @@ Importing `gui.qt` requires PySide6, which is an optional extra::
23
23
  """
24
24
  from __future__ import annotations
25
25
 
26
+ from endstation.optional import missing_extra_message, require_extra
27
+
26
28
  __all__ = ["main", "require_qt", "QT_MISSING_MESSAGE"]
27
29
 
28
- QT_MISSING_MESSAGE = (
29
- "the Qt interface needs PySide6, which is an optional extra.\n"
30
- "Install it with:\n\n"
31
- " pip install -e '.[gui]'\n\n"
30
+ _STILL_WORKS = (
32
31
  "The analysis, the .ggp export and the ICC diagnostic all work without it."
33
32
  )
34
33
 
34
+ QT_MISSING_MESSAGE = missing_extra_message("PySide6", "gui", still_works=_STILL_WORKS)
35
+
35
36
 
36
37
  def require_qt() -> None:
37
38
  """Raise something useful rather than a bare ImportError.
@@ -39,16 +40,13 @@ def require_qt() -> None:
39
40
  ``ModuleNotFoundError: No module named 'PySide6'`` tells a user nothing about
40
41
  what to do, and this is the most likely first-run failure.
41
42
 
42
- The check uses find_spec instead of an import because this module sits
43
- outside `gui/qt`, and the rule that only `gui/qt` touches a toolkit is
44
- enforced by `tests/test_gui_boundary.py`. Checking availability should
45
- also not cost a full toolkit import, since the aim is to fail quickly and
46
- clearly.
43
+ The check is :func:`endstation.optional.require_extra`, a ``find_spec``
44
+ call rather than an import, because this module sits outside `gui/qt` and
45
+ the rule that only `gui/qt` touches a toolkit is enforced by
46
+ `tests/test_gui_boundary.py`. It raises
47
+ :class:`~endstation.optional.MissingExtra`, a ``ModuleNotFoundError``.
47
48
  """
48
- from importlib.util import find_spec
49
-
50
- if find_spec("PySide6") is None: # pragma: no cover - depends on environment
51
- raise ModuleNotFoundError(QT_MISSING_MESSAGE)
49
+ require_extra("PySide6", "gui", still_works=_STILL_WORKS)
52
50
 
53
51
 
54
52
  def main(argv=None):
@@ -10,10 +10,12 @@ it belongs in `gui/state.py`, which is pure and tested headlessly.
10
10
  """
11
11
  from __future__ import annotations
12
12
 
13
- import os
14
-
15
13
  # matplotlib's Qt shim picks a binding from sys.modules first and QT_API second.
16
- # Without this, a stray PyQt5 in the environment would win and its canvas would
17
- # refuse to parent into a PySide6 window. This project shipped PyQt5 until
18
- # recently, so that stray binding is a realistic thing to find installed.
19
- os.environ.setdefault("QT_API", "PySide6")
14
+ # Without QT_API, a stray PyQt5 in the environment would win and its canvas
15
+ # would refuse to parent into a PySide6 window. This project shipped PyQt5
16
+ # until recently, so that stray binding is a realistic thing to find installed.
17
+ # Importing endstation.qt sets it, with setdefault; the modules here run
18
+ # `from . import *` first thing to get this import, so it exports nothing.
19
+ import endstation.qt # noqa: F401
20
+
21
+ __all__: list[str] = []
@@ -21,14 +21,16 @@ import os
21
21
  from . import * # noqa: F401,F403 - sets QT_API before matplotlib's shim loads
22
22
 
23
23
  from PySide6.QtWidgets import (
24
- QCheckBox, QFileDialog, QFrame, QGroupBox, QHBoxLayout, QLabel,
25
- QMessageBox, QPushButton, QScrollArea, QSpinBox, QVBoxLayout, QWidget,
24
+ QCheckBox, QFileDialog, QGroupBox, QHBoxLayout, QLabel,
25
+ QMessageBox, QPushButton, QSpinBox, QVBoxLayout, QWidget,
26
26
  )
27
27
  from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
28
28
  from matplotlib.figure import Figure
29
29
 
30
30
  from gratinglab.gui.qt import theming
31
- from gratinglab.gui.theme import style_figure
31
+ from gratinglab.gui.theme import set_titles, style_figure
32
+
33
+ from .layout import controls_beside
32
34
 
33
35
  from ...boundary import build_boundary_profile
34
36
  from ...core.tip import format_angles
@@ -65,7 +67,6 @@ class BoundaryView(QWidget):
65
67
  root.setSpacing(8)
66
68
 
67
69
  left = QWidget()
68
- left.setFixedWidth(272)
69
70
  column = QVBoxLayout(left)
70
71
  column.setContentsMargins(0, 0, 0, 0)
71
72
  column.setSpacing(8)
@@ -73,23 +74,12 @@ class BoundaryView(QWidget):
73
74
  column.addWidget(self._build_metrics_group())
74
75
  column.addStretch()
75
76
 
76
- scroll = QScrollArea()
77
- scroll.setWidget(left)
78
- scroll.setWidgetResizable(True)
79
- scroll.setFixedWidth(292)
80
- scroll.setFrameShape(QFrame.NoFrame)
81
-
82
77
  self.figure = Figure(figsize=(8, 6), tight_layout=True)
83
78
  self.canvas = FigureCanvas(self.figure)
84
79
  self._show_placeholder()
85
80
  theming.subscribe(self._on_theme)
86
81
 
87
- root.addWidget(scroll)
88
- divider = QFrame()
89
- divider.setFrameShape(QFrame.VLine)
90
- divider.setFrameShadow(QFrame.Sunken)
91
- root.addWidget(divider)
92
- root.addWidget(self.canvas, stretch=1)
82
+ root.addWidget(controls_beside(left, self.canvas), stretch=1)
93
83
 
94
84
  def _build_export_group(self):
95
85
  group = QGroupBox("Boundary profile")
@@ -327,7 +317,8 @@ class BoundaryView(QWidget):
327
317
  alpha=0.8 if dark else 0.6)
328
318
  avg_ax.set_xlabel("Position (nm)", fontsize=10)
329
319
  avg_ax.set_ylabel("Height (nm)", fontsize=10)
330
- avg_ax.set_title("Averaged groove", fontsize=10)
320
+ set_titles(avg_ax, "Averaged groove", f"{p.n_used} grooves",
321
+ theming.current())
331
322
  avg_ax.legend(fontsize=8)
332
323
  avg_ax.grid(True, alpha=0.3)
333
324
 
@@ -336,9 +327,9 @@ class BoundaryView(QWidget):
336
327
  norm_ax.axhline(0, color=theming.current().ink, linestyle='--', linewidth=0.5)
337
328
  norm_ax.set_xlabel("Normalised position (0 to 1 = one period)", fontsize=10)
338
329
  norm_ax.set_ylabel("Height (fraction of period)", fontsize=10)
339
- norm_ax.set_title(
340
- f"Exported profile: {len(p.x_norm)} points, "
341
- f"depth {p.metrics['groove_depth']:.4f} of period", fontsize=10)
330
+ set_titles(norm_ax, "Exported profile",
331
+ f"{len(p.x_norm)} points, depth {p.metrics['groove_depth']:.4f} "
332
+ "of period", theming.current())
342
333
  norm_ax.grid(True, alpha=0.3)
343
334
 
344
335
  self.canvas.draw()
@@ -17,7 +17,7 @@ from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
17
17
  from matplotlib.figure import Figure
18
18
 
19
19
  from gratinglab.gui.qt import theming
20
- from gratinglab.gui.theme import style_figure
20
+ from gratinglab.gui.theme import set_titles, style_figure
21
21
 
22
22
  __all__ = ["PlotCanvas"]
23
23
 
@@ -71,7 +71,8 @@ class PlotCanvas(FigureCanvas):
71
71
  ax.plot(disp_um, profile_nm, color='steelblue', linewidth=1.0)
72
72
  ax.set_xlabel("Position (µm)", fontsize=11)
73
73
  ax.set_ylabel("Height (nm)", fontsize=11)
74
- ax.set_title(f"Raw profile, scan width {scan_size_um:.3f} µm", fontsize=11)
74
+ set_titles(ax, "Raw profile", f"scan width {scan_size_um:.3f} µm",
75
+ theming.current())
75
76
  ax.grid(True, alpha=0.3)
76
77
  self.draw()
77
78
 
@@ -84,7 +85,8 @@ class PlotCanvas(FigureCanvas):
84
85
  self.fig.colorbar(im, ax=ax, label='Height (nm)')
85
86
  ax.set_xlabel("X (µm)", fontsize=11)
86
87
  ax.set_ylabel("Scan line", fontsize=11)
87
- ax.set_title("Topography", fontsize=11)
88
+ set_titles(ax, "Topography", f"{data.shape[0]} scan lines",
89
+ theming.current())
88
90
  self.draw()
89
91
 
90
92
  @_remembered
@@ -102,9 +104,9 @@ class PlotCanvas(FigureCanvas):
102
104
  linewidth=1.8, label=f'Mean (n={n_groups})', zorder=5)
103
105
  ax.set_xlabel("Position (µm)", fontsize=11)
104
106
  ax.set_ylabel("Height (nm)", fontsize=11)
105
- ax.set_title("Row-group profiles\n"
106
- f"{n_groups} groups × {group_info['rows_per_group']} rows each | "
107
- f"Scan width: {scan_size_um:.3f} µm", fontsize=10)
107
+ set_titles(ax, "Row-group profiles",
108
+ f"{n_groups} groups of {group_info['rows_per_group']} rows, "
109
+ f"scan width {scan_size_um:.3f} µm", theming.current())
108
110
  ax.legend(fontsize=10)
109
111
  ax.grid(True, alpha=0.3)
110
112
 
@@ -140,7 +142,8 @@ class PlotCanvas(FigureCanvas):
140
142
 
141
143
  ax.set_xlabel("Position (µm)", fontsize=11)
142
144
  ax.set_ylabel("Height (nm)", fontsize=11)
143
- ax.set_title("Groove detection", fontsize=11)
145
+ set_titles(ax, "Groove detection", f"{len(centers)} grooves",
146
+ theming.current())
144
147
  ax.legend(fontsize=9)
145
148
  ax.grid(True, alpha=0.3)
146
149
  self.draw()
@@ -160,8 +163,8 @@ class PlotCanvas(FigureCanvas):
160
163
  label=f'Mean {mean:.2f}°')
161
164
  ax1.set_xlabel("Blaze angle (°)", fontsize=10)
162
165
  ax1.set_ylabel("Count", fontsize=10)
163
- ax1.set_title(f"Distribution (N={len(angles)})\nσ={result['std_angle']:.2f}°",
164
- fontsize=10)
166
+ set_titles(ax1, "Distribution",
167
+ f"N = {len(angles)}, σ = {result['std_angle']:.2f}°", theming.current())
165
168
  ax1.legend(fontsize=9)
166
169
  ax1.grid(True, alpha=0.3)
167
170
 
@@ -169,16 +172,18 @@ class PlotCanvas(FigureCanvas):
169
172
  ax2.hist(r2, bins=20, edgecolor=theming.current().ink, alpha=0.75, color='seagreen')
170
173
  ax2.set_xlabel("Fit R²", fontsize=10)
171
174
  ax2.set_ylabel("Count", fontsize=10)
172
- ax2.set_title(f"Fit quality\nworst R² = {np.nanmin(r2):.4f}", fontsize=10)
175
+ set_titles(ax2, "Fit quality", f"worst R² {np.nanmin(r2):.4f}",
176
+ theming.current())
173
177
  ax2.grid(True, alpha=0.3)
174
178
 
175
179
  widths = np.array([q.get('blaze_width_nm', np.nan) for q in quality])
176
180
  ax3.plot(widths, angles, 'o', markersize=4, alpha=0.45, color='darkorange')
177
181
  ax3.set_xlabel("Blaze facet width (nm)", fontsize=10)
178
182
  ax3.set_ylabel("Blaze angle (°)", fontsize=10)
179
- ax3.set_title("Angle vs facet width\n(narrow facets = unreliable fits)",
180
- fontsize=10)
183
+ set_titles(ax3, "Angle against facet width", "narrow facets fit poorly",
184
+ theming.current())
181
185
  ax3.grid(True, alpha=0.3)
182
186
 
183
- self.fig.suptitle(f"Blaze angle {mean:.2f}° ± {sem:.3f}° (SEM)", fontsize=11)
187
+ self.fig.suptitle(f"Blaze angle {mean:.2f}° ± {sem:.3f}° (SEM)",
188
+ x=0.01, ha="left", fontsize=12, fontweight="semibold")
184
189
  self.draw()