gratinglab 0.2.0__tar.gz → 0.2.1__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 (192) hide show
  1. {gratinglab-0.2.0/src/gratinglab.egg-info → gratinglab-0.2.1}/PKG-INFO +1 -1
  2. {gratinglab-0.2.0 → gratinglab-0.2.1}/pyproject.toml +6 -2
  3. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/__init__.py +1 -1
  4. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/convergence.py +9 -2
  5. gratinglab-0.2.1/src/gratinglab/gui/cmethod_options.py +123 -0
  6. gratinglab-0.2.1/src/gratinglab/gui/qt/cmethod_tab.py +469 -0
  7. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/gui/qt/geometry_panel.py +12 -0
  8. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/gui/qt/main_window.py +27 -4
  9. gratinglab-0.2.1/src/gratinglab/gui/qt/rcwa_tab.py +466 -0
  10. gratinglab-0.2.1/src/gratinglab/gui/rcwa_options.py +164 -0
  11. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/gui/state.py +56 -2
  12. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/solvers/base.py +7 -0
  13. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/solvers/cmethod/__init__.py +60 -25
  14. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/solvers/rcwa/__init__.py +51 -20
  15. {gratinglab-0.2.0 → gratinglab-0.2.1/src/gratinglab.egg-info}/PKG-INFO +1 -1
  16. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab.egg-info/SOURCES.txt +7 -0
  17. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_cmethod.py +10 -4
  18. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_convergence.py +22 -0
  19. gratinglab-0.2.1/tests/test_gui_cmethod_options.py +142 -0
  20. gratinglab-0.2.1/tests/test_gui_cmethod_tab.py +262 -0
  21. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_gui_qt.py +129 -0
  22. gratinglab-0.2.1/tests/test_gui_rcwa_options.py +95 -0
  23. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_gui_state.py +37 -0
  24. {gratinglab-0.2.0 → gratinglab-0.2.1}/LICENSE +0 -0
  25. {gratinglab-0.2.0 → gratinglab-0.2.1}/README.md +0 -0
  26. {gratinglab-0.2.0 → gratinglab-0.2.1}/setup.cfg +0 -0
  27. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/checks.py +0 -0
  28. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/compare.py +0 -0
  29. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/geometry.py +0 -0
  30. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/gui/__init__.py +0 -0
  31. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/gui/app.py +0 -0
  32. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/gui/diagram.py +0 -0
  33. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/gui/diagram3d.py +0 -0
  34. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/gui/docs.py +0 -0
  35. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/gui/efficiency_plot.py +0 -0
  36. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/gui/icon.py +0 -0
  37. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/gui/integral_options.py +0 -0
  38. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/gui/launcher.py +0 -0
  39. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/gui/mathtext.py +0 -0
  40. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/gui/orders.py +0 -0
  41. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/gui/provenance.py +0 -0
  42. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/gui/qt/__init__.py +0 -0
  43. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/gui/qt/gc_guard.py +0 -0
  44. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/gui/qt/geometry_tab.py +0 -0
  45. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/gui/qt/integral_tab.py +0 -0
  46. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/gui/qt/launcher.py +0 -0
  47. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/gui/qt/profile_plot_panel.py +0 -0
  48. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/gui/qt/scalar_tab.py +0 -0
  49. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/gui/qt/scene3d_panel.py +0 -0
  50. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/gui/qt/setup_tab.py +0 -0
  51. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/gui/qt/sizing.py +0 -0
  52. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/gui/qt/theming.py +0 -0
  53. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/gui/qt/theory_viewer.py +0 -0
  54. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/gui/qt/worker.py +0 -0
  55. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/gui/richtext.py +0 -0
  56. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/gui/scalar_options.py +0 -0
  57. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/gui/theme.py +0 -0
  58. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/illumination.py +0 -0
  59. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/io/__init__.py +0 -0
  60. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/io/boundary.py +0 -0
  61. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/io/efficiency_table.py +0 -0
  62. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/io/ggp.py +0 -0
  63. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/materials/__init__.py +0 -0
  64. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/materials/data/SOURCES.md +0 -0
  65. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/materials/fresnel.py +0 -0
  66. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/materials/optical.py +0 -0
  67. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/materials/refractiveindex.py +0 -0
  68. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/measured.py +0 -0
  69. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/__init__.py +0 -0
  70. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/analyzer.py +0 -0
  71. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/boundary/__init__.py +0 -0
  72. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/boundary/average.py +0 -0
  73. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/boundary/pipeline.py +0 -0
  74. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/boundary/swaths.py +0 -0
  75. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/cli.py +0 -0
  76. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/config.py +0 -0
  77. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/core/__init__.py +0 -0
  78. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/core/analysis.py +0 -0
  79. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/core/image_flatten.py +0 -0
  80. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/core/orientation.py +0 -0
  81. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/core/processing.py +0 -0
  82. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/core/region_mask.py +0 -0
  83. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/core/scars.py +0 -0
  84. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/core/tip.py +0 -0
  85. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/data/__init__.py +0 -0
  86. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/data/aggregation.py +0 -0
  87. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/gui/__init__.py +0 -0
  88. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/gui/icon.py +0 -0
  89. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/gui/qt/__init__.py +0 -0
  90. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/gui/qt/app.py +0 -0
  91. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/gui/qt/boundary_view.py +0 -0
  92. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/gui/qt/canvas.py +0 -0
  93. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/gui/qt/efficiency_dialog.py +0 -0
  94. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/gui/qt/import_view.py +0 -0
  95. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/gui/qt/main_window.py +0 -0
  96. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/gui/qt/swath_dialog.py +0 -0
  97. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/gui/qt/wiki_view.py +0 -0
  98. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/gui/qt/worker.py +0 -0
  99. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/gui/state.py +0 -0
  100. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/io/__init__.py +0 -0
  101. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/io/file_io.py +0 -0
  102. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/io/ibw.py +0 -0
  103. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/io/metrics.py +0 -0
  104. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/io/spm.py +0 -0
  105. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/settings.py +0 -0
  106. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/stats/__init__.py +0 -0
  107. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/stats/analysis.py +0 -0
  108. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/stats/icc.py +0 -0
  109. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/visualization/__init__.py +0 -0
  110. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/visualization/diagnostics.py +0 -0
  111. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/visualization/profiles.py +0 -0
  112. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/visualization/statistics.py +0 -0
  113. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/wiki/__init__.py +0 -0
  114. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/wiki/facet-fitting.md +0 -0
  115. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/wiki/file-formats.md +0 -0
  116. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/wiki/flattening.md +0 -0
  117. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/wiki/groove-orientation.md +0 -0
  118. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/wiki/icc-correction.md +0 -0
  119. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/wiki/overview.md +0 -0
  120. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/wiki/reading-outputs.md +0 -0
  121. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/wiki/row-groups.md +0 -0
  122. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/wiki/scars-and-masking.md +0 -0
  123. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/wiki/swath-statistics.md +0 -0
  124. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/wiki/tip-correction.md +0 -0
  125. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/metrology/workflows.py +0 -0
  126. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/problem.py +0 -0
  127. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/profiles.py +0 -0
  128. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/resolution.py +0 -0
  129. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/result.py +0 -0
  130. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/solvers/__init__.py +0 -0
  131. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/solvers/_conditioning.py +0 -0
  132. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/solvers/_execution.py +0 -0
  133. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/solvers/cmethod/_core.py +0 -0
  134. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/solvers/cmethod/_series.py +0 -0
  135. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/solvers/integral/__init__.py +0 -0
  136. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/solvers/integral/_boundary.py +0 -0
  137. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/solvers/integral/_core.py +0 -0
  138. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/solvers/integral/_finite.py +0 -0
  139. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/solvers/integral/_greens.py +0 -0
  140. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/solvers/integral/_kernels.py +0 -0
  141. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/solvers/integral/_nystrom.py +0 -0
  142. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/solvers/rcwa/_core.py +0 -0
  143. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/solvers/rcwa/_stack.py +0 -0
  144. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab/solvers/scalar.py +0 -0
  145. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab.egg-info/dependency_links.txt +0 -0
  146. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab.egg-info/entry_points.txt +0 -0
  147. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab.egg-info/requires.txt +0 -0
  148. {gratinglab-0.2.0 → gratinglab-0.2.1}/src/gratinglab.egg-info/top_level.txt +0 -0
  149. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_changelog.py +0 -0
  150. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_checks.py +0 -0
  151. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_compare.py +0 -0
  152. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_conditioning.py +0 -0
  153. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_cross_method.py +0 -0
  154. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_efficiency_table.py +0 -0
  155. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_fresnel.py +0 -0
  156. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_geometry.py +0 -0
  157. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_ggp.py +0 -0
  158. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_gui_diagram.py +0 -0
  159. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_gui_diagram3d.py +0 -0
  160. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_gui_docs.py +0 -0
  161. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_gui_integral_options.py +0 -0
  162. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_gui_launcher.py +0 -0
  163. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_gui_mathtext.py +0 -0
  164. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_gui_orders.py +0 -0
  165. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_gui_provenance.py +0 -0
  166. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_gui_richtext.py +0 -0
  167. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_gui_scalar_options.py +0 -0
  168. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_gui_theme.py +0 -0
  169. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_illumination.py +0 -0
  170. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_integral_core.py +0 -0
  171. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_integral_corpus.py +0 -0
  172. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_integral_finite.py +0 -0
  173. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_integral_graded.py +0 -0
  174. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_integral_greens.py +0 -0
  175. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_integral_kernels.py +0 -0
  176. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_integral_operators.py +0 -0
  177. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_integral_solver.py +0 -0
  178. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_io_boundary.py +0 -0
  179. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_materials.py +0 -0
  180. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_measured.py +0 -0
  181. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_perturbation.py +0 -0
  182. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_profiles.py +0 -0
  183. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_rayleigh_anomaly.py +0 -0
  184. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_rcwa.py +0 -0
  185. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_rcwa_benchmarks.py +0 -0
  186. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_refractiveindex.py +0 -0
  187. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_resolution.py +0 -0
  188. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_result.py +0 -0
  189. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_scalar.py +0 -0
  190. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_solver_conformance.py +0 -0
  191. {gratinglab-0.2.0 → gratinglab-0.2.1}/tests/test_solver_execution.py +0 -0
  192. {gratinglab-0.2.0 → gratinglab-0.2.1}/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.0
3
+ Version: 0.2.1
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
@@ -182,12 +182,16 @@ filterwarnings = [
182
182
  "ignore:Enum value .Qt..ApplicationAttribute.AA_UseHighDpiPixmaps. is marked as deprecated:DeprecationWarning:matplotlib.backends.backend_qt",
183
183
  # CI runs with PYTHONWARNDEFAULTENCODING=1, so any text open() here that
184
184
  # leaves the encoding to the platform fails on every OS, not only on
185
- # Windows with a non-ASCII file. These three are other projects' calls.
185
+ # Windows with a non-ASCII file. These four are other projects' calls.
186
186
  # numpy's savetxt touches its output with a bare open() whatever encoding
187
- # it is given. Inert without the flag.
187
+ # it is given. threadpoolctl reads /proc/self/maps with a bare open(), so
188
+ # on Linux only, every rigorous solve (which caps BLAS through it) failed
189
+ # here: master's CI run 40624677314, both ubuntu jobs. Inert without the
190
+ # flag.
188
191
  "ignore::EncodingWarning:matplotlib.font_manager",
189
192
  "ignore::EncodingWarning:numpy.lib._npyio_impl",
190
193
  "ignore::EncodingWarning:numpy.lib._datasource",
194
+ "ignore::EncodingWarning:threadpoolctl",
191
195
  ]
192
196
  # pytest-qt would otherwise probe every binding it knows. Verified clean under
193
197
  # `python -W error -c "import PySide6.QtWidgets"`, so `filterwarnings = error`
@@ -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.0"
25
+ __version__ = "0.2.1"
26
26
 
27
27
  __all__ = [
28
28
  "Illumination",
@@ -201,7 +201,10 @@ def default_ladder(solver) -> tuple[int, ...]:
201
201
  """A ladder straddling the solver's own default for its accuracy knob.
202
202
 
203
203
  The default is read from the signature and is not hardcoded, so a backend
204
- that changes its default does not leave this sweeping the wrong range.
204
+ that changes its default does not leave this sweeping the wrong range. A
205
+ backend declaring an ``accuracy_ceiling`` has the ladder cut there: a sweep
206
+ that has not plateaued by the ceiling reports that, instead of failing on
207
+ a rung the solver refuses.
205
208
  """
206
209
  knob = _knob_of(solver)
207
210
  default = inspect.signature(solver.solve).parameters[knob].default
@@ -211,7 +214,11 @@ def default_ladder(solver) -> tuple[int, ...]:
211
214
  f"{knob!r} ({default!r}), so a ladder cannot be inferred -- pass "
212
215
  "`values` explicitly"
213
216
  )
214
- return doubling_ladder(max(1, default // 2**_RUNGS_BELOW_DEFAULT), _DEFAULT_RUNGS)
217
+ ladder = doubling_ladder(max(1, default // 2**_RUNGS_BELOW_DEFAULT), _DEFAULT_RUNGS)
218
+ ceiling = solver.capabilities.accuracy_ceiling
219
+ if ceiling is not None:
220
+ ladder = tuple(value for value in ladder if value <= ceiling)
221
+ return ladder
215
222
 
216
223
 
217
224
  def check_convergence(
@@ -0,0 +1,123 @@
1
+ """The C-method's own form: boundary condition, truncation, and their guards.
2
+
3
+ The third options module, written to the pattern of ``integral_options.py``:
4
+ its own file, its own frozen state and its own ``build_options`` raising
5
+ :class:`~gratinglab.gui.state.FormErrors`, and no toolkit import.
6
+
7
+ The form has two settings, because the solver has two. ``conductivity`` is
8
+ named and offered as the integral solver offers it, so one geometry reaches
9
+ either method with the same choice: ``"perfect"`` (relative efficiency, no
10
+ material) or ``"tabulated"`` (one interface into the coating, read per
11
+ wavelength, with absolute efficiency, absorption and, into a lossless
12
+ material, the transmitted orders). ``truncation`` is the accuracy knob: ``N``
13
+ harmonics either side of the centre of the propagating orders.
14
+
15
+ The guards mirror the solver's refusals so an error lands on the field that
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.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from dataclasses import dataclass, replace
25
+ from typing import Any
26
+
27
+ import numpy as np
28
+ from numpy.typing import NDArray
29
+
30
+ from ..illumination import Illumination
31
+ from ..problem import Problem
32
+ from ..solvers.cmethod import _MAX_TRUNCATION, truncation_floor
33
+ from .integral_options import _tabulated_index
34
+ from .state import FieldError, FormErrors, _number
35
+
36
+ __all__ = [
37
+ "CONDUCTIVITY_MODES",
38
+ "CONDUCTIVITY_NOTES",
39
+ "CMethodOptionsState",
40
+ "build_options",
41
+ ]
42
+
43
+ #: Selectable boundary conditions, the solver's default first.
44
+ CONDUCTIVITY_MODES = ("perfect", "tabulated")
45
+
46
+ #: What each mode means, shown beneath the selector, for the reason
47
+ #: ``integral_options.CONDUCTIVITY_NOTES`` gives: a claim about the result
48
+ #: must not be hardcoded beside a control that can falsify it.
49
+ CONDUCTIVITY_NOTES = {
50
+ "perfect": (
51
+ "Perfect conductor in any mount: efficiencies are relative to a "
52
+ "perfect reflector and sum to 1. A coating named in the geometry "
53
+ "panel is not consulted (the provenance panel says so)."
54
+ ),
55
+ "tabulated": (
56
+ "One interface into the coating named in the geometry panel, read "
57
+ "as a complex index at every wavelength. Efficiencies are absolute "
58
+ "and absorption is recorded; into a lossless material the "
59
+ "transmitted orders are recorded too, and R + T + A = 1 is a check."
60
+ ),
61
+ }
62
+
63
+
64
+ @dataclass(frozen=True, slots=True)
65
+ class CMethodOptionsState:
66
+ """The C-method's own form fields, exactly as typed."""
67
+
68
+ truncation: str = "32"
69
+ conductivity: str = CONDUCTIVITY_MODES[0]
70
+
71
+ def with_field(self, name: str, value: Any) -> "CMethodOptionsState":
72
+ """Return a copy with one field changed."""
73
+ return replace(self, **{name: value})
74
+
75
+
76
+ def build_options(
77
+ problem: Problem,
78
+ illumination: Illumination,
79
+ wavelengths: NDArray[np.float64],
80
+ form: CMethodOptionsState,
81
+ ) -> dict[str, Any]:
82
+ """Parse the C-method options and check the truncation holds every order.
83
+
84
+ Raises
85
+ ------
86
+ FormErrors
87
+ Same contract as :func:`gratinglab.gui.state.build`.
88
+ """
89
+ errors: list[FieldError] = []
90
+ truncation = _number(
91
+ form.truncation, "truncation", errors, minimum=1, integer=True
92
+ )
93
+ if truncation is not None and truncation > _MAX_TRUNCATION:
94
+ errors.append(
95
+ FieldError(
96
+ "truncation",
97
+ f"at most {_MAX_TRUNCATION}: the eigenproblem is dense and its "
98
+ "cost grows as N^3, so a sweep that has not settled by then is "
99
+ "the answer",
100
+ )
101
+ )
102
+ if form.conductivity not in CONDUCTIVITY_MODES:
103
+ errors.append(
104
+ FieldError(
105
+ "conductivity",
106
+ f"must be one of {', '.join(CONDUCTIVITY_MODES)}",
107
+ )
108
+ )
109
+ if errors:
110
+ raise FormErrors(tuple(errors))
111
+ assert truncation is not None
112
+
113
+ indices = None
114
+ if form.conductivity == "tabulated":
115
+ _name, indices, error = _tabulated_index(problem, wavelengths)
116
+ if error is not None:
117
+ raise FormErrors((error,))
118
+
119
+ floor = truncation_floor(problem, illumination, wavelengths, indices)
120
+ if truncation < floor.needed:
121
+ raise FormErrors((FieldError("truncation", floor.message),))
122
+
123
+ return {"truncation": int(truncation), "conductivity": form.conductivity}
@@ -0,0 +1,469 @@
1
+ """The C-method's own tab: its knob, its result, its provenance.
2
+
3
+ The third solver tab, written beside ``scalar_tab.py`` and ``integral_tab.py``
4
+ rather than on a base class, for the reason ``integral_tab.py`` gives: the
5
+ common structure is to be extracted once enough tabs show what it is, and the
6
+ duplication is that evidence. Against the integral tab this one drops the
7
+ corner-mesh selector, which the C-method has no counterpart for, and its knob
8
+ is ``truncation`` rather than ``boundary_points``.
9
+
10
+ Its warnings arrive the way every solver's do, as
11
+ ``Provenance.warnings``, and the provenance panel prints them: corners
12
+ (slow convergence, an energy balance that overstates accuracy), a large
13
+ condition number (higher truncation can make the answer worse), and an energy
14
+ balance straying from unity. The tab adds none of its own.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import csv
20
+ import dataclasses
21
+ from pathlib import Path
22
+ from typing import Sequence
23
+
24
+ from PySide6.QtCore import Qt, Signal
25
+ from PySide6.QtWidgets import (
26
+ QAbstractItemView,
27
+ QComboBox,
28
+ QFileDialog,
29
+ QFormLayout,
30
+ QGroupBox,
31
+ QHBoxLayout,
32
+ QLabel,
33
+ QLineEdit,
34
+ QListWidget,
35
+ QListWidgetItem,
36
+ QMessageBox,
37
+ QProgressBar,
38
+ QPushButton,
39
+ QScrollArea,
40
+ QSplitter,
41
+ QTextEdit,
42
+ QVBoxLayout,
43
+ QWidget,
44
+ )
45
+
46
+ from .. import orders as orders_module
47
+ from .. import provenance
48
+ from ..efficiency_plot import draw_efficiency
49
+ from ..theme import provenance_colors, style_figure
50
+ from . import theming
51
+ from ..cmethod_options import (
52
+ CONDUCTIVITY_MODES,
53
+ CONDUCTIVITY_NOTES,
54
+ CMethodOptionsState,
55
+ )
56
+ from ..cmethod_options import build_options as build_cmethod_options
57
+ from .sizing import fit_width_to_contents
58
+
59
+ __all__ = ["CMethodTab"]
60
+
61
+ _ORDER_ROLE = Qt.ItemDataRole.UserRole
62
+ #: Set on an order that never propagates in the scan, drawn in the muted colour.
63
+ _DIM_ROLE = Qt.ItemDataRole.UserRole + 1
64
+
65
+
66
+ class CMethodTab(QWidget):
67
+ """The C-method's own controls, result, and provenance."""
68
+
69
+ name = "cmethod"
70
+
71
+ solve_requested = Signal()
72
+ cancel_requested = Signal()
73
+ convergence_requested = Signal()
74
+
75
+ def __init__(self, parent: QWidget | None = None) -> None:
76
+ super().__init__(parent)
77
+ self._fields: dict[str, QWidget] = {}
78
+ self._scan = None
79
+ self._lines: tuple = ()
80
+ self._order_colors: dict[int, str] = {}
81
+ self._energy = None
82
+ self._lambda_over_period: float | None = None
83
+ self._visible_orders: frozenset[int] = frozenset()
84
+ self._previous_orders: frozenset[int] = frozenset()
85
+ self._build()
86
+
87
+ # -- construction ------------------------------------------------
88
+
89
+ def _build(self) -> None:
90
+ controls = QWidget()
91
+ column = QVBoxLayout(controls)
92
+ column.setContentsMargins(0, 0, 0, 0)
93
+ column.addWidget(self._build_options_group())
94
+ column.addWidget(self._build_orders_group())
95
+ column.addLayout(self._build_action_buttons())
96
+ column.addWidget(self._export_button)
97
+ column.addWidget(self._progress)
98
+ column.addStretch(1)
99
+
100
+ scroller = QScrollArea()
101
+ scroller.setWidget(controls)
102
+ scroller.setWidgetResizable(True)
103
+ fit_width_to_contents(scroller)
104
+
105
+ right = QSplitter(Qt.Orientation.Vertical)
106
+ right.addWidget(self._build_efficiency_plot())
107
+ right.addWidget(self._build_provenance())
108
+ right.setStretchFactor(0, 1)
109
+ right.setStretchFactor(1, 0)
110
+
111
+ body = QSplitter(Qt.Orientation.Horizontal)
112
+ body.addWidget(scroller)
113
+ body.addWidget(right)
114
+ body.setStretchFactor(0, 0)
115
+ body.setStretchFactor(1, 1)
116
+ body.setSizes([260, 700])
117
+
118
+ outer = QVBoxLayout(self)
119
+ outer.setContentsMargins(0, 0, 0, 0)
120
+ outer.addWidget(body)
121
+
122
+ self._check_fields_match_cmethod_options()
123
+
124
+ def _build_options_group(self) -> QWidget:
125
+ defaults = CMethodOptionsState()
126
+ group = QGroupBox("C-method (Chandezon)")
127
+ form = QFormLayout(group)
128
+ truncation = QLineEdit(defaults.truncation)
129
+ truncation.returnPressed.connect(self.solve_requested)
130
+ truncation.setToolTip(
131
+ "N: harmonics N either side of the centre of the propagating\n"
132
+ "orders, 2N + 1 in all -- the accuracy knob the convergence\n"
133
+ "sweep varies, from 1 to 256. Cost grows as N^3. On a profile\n"
134
+ "with corners convergence is slow, and on a deep one a high N\n"
135
+ "can lose digits: the provenance panel warns of both."
136
+ )
137
+ self._fields["truncation"] = truncation
138
+ form.addRow("Truncation N", truncation)
139
+
140
+ conductivity = QComboBox()
141
+ conductivity.addItems(CONDUCTIVITY_MODES)
142
+ conductivity.setToolTip(
143
+ "Which boundary condition the C-method matches.\n"
144
+ "perfect: perfect conductor in any mount -- efficiencies are\n"
145
+ " relative to a perfect reflector and sum to 1. A coating is\n"
146
+ " not consulted.\n"
147
+ "tabulated: one interface into the coating, read as a complex\n"
148
+ " index at every wavelength -- efficiencies are absolute,\n"
149
+ " absorption is recorded, and into a lossless material so are\n"
150
+ " the transmitted orders."
151
+ )
152
+ conductivity.currentTextChanged.connect(self._show_conductivity_note)
153
+ self._fields["conductivity"] = conductivity
154
+ form.addRow("Conductivity", conductivity)
155
+
156
+ self._conductivity_note = QLabel()
157
+ self._conductivity_note.setWordWrap(True)
158
+ form.addRow(self._conductivity_note)
159
+ self._show_conductivity_note(conductivity.currentText())
160
+ return group
161
+
162
+ def _show_conductivity_note(self, mode: str) -> None:
163
+ """Say what the selected boundary condition means for the numbers."""
164
+ self._conductivity_note.setText(CONDUCTIVITY_NOTES.get(mode, ""))
165
+
166
+ def _build_orders_group(self) -> QWidget:
167
+ group = QGroupBox("Orders")
168
+ layout = QVBoxLayout(group)
169
+
170
+ self._order_list = QListWidget()
171
+ self._order_list.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
172
+ self._order_list.itemChanged.connect(self._on_order_item_changed)
173
+ layout.addWidget(self._order_list)
174
+
175
+ buttons = QHBoxLayout()
176
+ show_all = QPushButton("All")
177
+ show_all.clicked.connect(self._show_all_orders)
178
+ show_none = QPushButton("None")
179
+ show_none.clicked.connect(self._show_no_orders)
180
+ show_default = QPushButton("Default")
181
+ show_default.clicked.connect(self._show_default_orders)
182
+ for button in (show_all, show_none, show_default):
183
+ buttons.addWidget(button)
184
+ layout.addLayout(buttons)
185
+
186
+ self._order_count_label = QLabel("")
187
+ layout.addWidget(self._order_count_label)
188
+ return group
189
+
190
+ def _build_action_buttons(self) -> QVBoxLayout:
191
+ self._solve_button = QPushButton("Solve")
192
+ self._solve_button.clicked.connect(self.solve_requested)
193
+ self._cancel_button = QPushButton("Cancel")
194
+ self._cancel_button.clicked.connect(self.cancel_requested)
195
+ self._cancel_button.setEnabled(False)
196
+ self._export_button = QPushButton("Export CSV…")
197
+ self._export_button.clicked.connect(self.export_csv)
198
+
199
+ self._converge_button = QPushButton("Check convergence…")
200
+ self._converge_button.setToolTip(
201
+ "Sweep truncation (up to 256) until the answer stops moving, and "
202
+ "report the coarsest setting that can be defended."
203
+ )
204
+ self._converge_button.clicked.connect(self.convergence_requested)
205
+
206
+ self._progress = QProgressBar()
207
+ self._progress.setRange(0, 0)
208
+ self._progress.hide()
209
+
210
+ row = QHBoxLayout()
211
+ row.addWidget(self._solve_button)
212
+ row.addWidget(self._cancel_button)
213
+
214
+ column = QVBoxLayout()
215
+ column.addLayout(row)
216
+ column.addWidget(self._converge_button)
217
+ return column
218
+
219
+ def _build_efficiency_plot(self) -> QWidget:
220
+ from matplotlib.backends.backend_qtagg import (
221
+ FigureCanvasQTAgg,
222
+ NavigationToolbar2QT,
223
+ )
224
+ from matplotlib.figure import Figure
225
+
226
+ self._figure = Figure(figsize=(8, 3), layout="constrained")
227
+ self._axes = self._figure.add_subplot(1, 1, 1)
228
+ self._canvas = FigureCanvasQTAgg(self._figure)
229
+ style_figure(self._figure, theming.current())
230
+ theming.subscribe(self._on_theme)
231
+
232
+ panel = QWidget()
233
+ layout = QVBoxLayout(panel)
234
+ layout.setContentsMargins(0, 0, 0, 0)
235
+ layout.addWidget(NavigationToolbar2QT(self._canvas, panel))
236
+ layout.addWidget(self._canvas)
237
+ return panel
238
+
239
+ def _build_provenance(self) -> QWidget:
240
+ panel = QWidget()
241
+ layout = QVBoxLayout(panel)
242
+ layout.setContentsMargins(8, 4, 8, 8)
243
+ heading = QLabel("Provenance")
244
+ # Bold by font, not stylesheet: a stylesheet freezes the label's
245
+ # palette at polish time, and it would keep the old theme's colour.
246
+ bold = heading.font()
247
+ bold.setBold(True)
248
+ heading.setFont(bold)
249
+ self._provenance = QTextEdit()
250
+ self._provenance.setReadOnly(True)
251
+ layout.addWidget(heading)
252
+ layout.addWidget(self._provenance)
253
+ return panel
254
+
255
+ def _check_fields_match_cmethod_options(self) -> None:
256
+ declared = {f.name for f in dataclasses.fields(CMethodOptionsState)}
257
+ if self._fields.keys() != declared:
258
+ raise AssertionError(
259
+ "cmethod-option fields do not match CMethodOptionsState: "
260
+ f"missing {sorted(declared - self._fields.keys())}, "
261
+ f"extra {sorted(self._fields.keys() - declared)}"
262
+ )
263
+
264
+ # -- the MainWindow contract ---------------------------------------
265
+
266
+ def build_options(self, problem, illumination, wavelengths) -> dict:
267
+ options = CMethodOptionsState(
268
+ truncation=_value(self._fields["truncation"]),
269
+ conductivity=_value(self._fields["conductivity"]),
270
+ )
271
+ return build_cmethod_options(problem, illumination, wavelengths, options)
272
+
273
+ def set_running(self, running: bool) -> None:
274
+ self._solve_button.setEnabled(not running)
275
+ self._converge_button.setEnabled(not running)
276
+ self._cancel_button.setEnabled(running)
277
+ # Only the knob: the sweep chooses it, so the field it would come from
278
+ # is dead for the duration. `conductivity` is passed through to the
279
+ # sweep untouched, so it stays live, like scalar's model selectors.
280
+ self._fields["truncation"].setEnabled(not running)
281
+ if not running:
282
+ self._progress.hide()
283
+
284
+ def show_progress(self) -> None:
285
+ self._progress.show()
286
+
287
+ def show_progress_value(self, done: int, total: int) -> None:
288
+ if self._progress.maximum() != total:
289
+ self._progress.setRange(0, total)
290
+ self._progress.setValue(done)
291
+
292
+ def show_solving(self, wavelength_count: int) -> None:
293
+ self._progress.setRange(0, 0)
294
+ self._paint(provenance.solving_lines(self.name, wavelength_count))
295
+
296
+ def show_result(self, scan, energy, lambda_over_period: float) -> None:
297
+ self._scan, self._energy = scan, energy
298
+ self._lambda_over_period = lambda_over_period
299
+
300
+ summaries = orders_module.summarize(scan)
301
+ self._visible_orders = orders_module.carry_over(
302
+ self._visible_orders, self._previous_orders, summaries
303
+ )
304
+ self._previous_orders = frozenset(s.order for s in summaries)
305
+ self._rebuild_order_list(summaries)
306
+
307
+ self._draw_efficiency(scan)
308
+ self._paint(provenance.provenance_lines(scan, energy, lambda_over_period))
309
+
310
+ def show_convergence(self, scan, energy, lambda_over_period: float, report) -> None:
311
+ self.show_result(scan, energy, lambda_over_period)
312
+ if report.converged_at is not None:
313
+ self._fields["truncation"].setText(str(report.converged_at))
314
+
315
+ def show_cancelled(self) -> None:
316
+ if self._scan is None:
317
+ return
318
+ self._paint(
319
+ provenance.provenance_lines(
320
+ self._scan, self._energy, self._lambda_over_period, cancelled=True
321
+ )
322
+ )
323
+
324
+ def show_error(self, message: str) -> None:
325
+ self._paint((provenance.Line(f"solve failed: {message}\n", "bad"),))
326
+
327
+ def show_field_errors(self, errors) -> None:
328
+ self._paint(provenance.error_lines(errors))
329
+
330
+ # -- orders ----------------------------------------------------------
331
+
332
+ def _rebuild_order_list(self, summaries: Sequence["orders_module.OrderSummary"]) -> None:
333
+ self._order_list.blockSignals(True)
334
+ self._order_list.clear()
335
+ for summary in summaries:
336
+ item = QListWidgetItem(orders_module.describe(summary))
337
+ item.setData(_ORDER_ROLE, summary.order)
338
+ item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
339
+ item.setCheckState(
340
+ Qt.CheckState.Checked
341
+ if summary.order in self._visible_orders
342
+ else Qt.CheckState.Unchecked
343
+ )
344
+ item.setData(_DIM_ROLE, not summary.ever_propagating)
345
+ self._order_list.addItem(item)
346
+ self._order_list.blockSignals(False)
347
+ self._apply_swatches()
348
+ self._update_order_count_label(len(summaries))
349
+
350
+ def _apply_swatches(self) -> None:
351
+ """Give each listed order its line's colour, and dim the dead ones.
352
+
353
+ Both are redone on a theme change: the swatch because the colours
354
+ move, the dimming because it was the old palette's muted colour.
355
+ """
356
+ # From the theme, not self.palette(): on a theme change this runs
357
+ # before Qt has propagated the new application palette to the widget.
358
+ from PySide6.QtGui import QColor
359
+
360
+ theme = theming.current()
361
+ muted, text = QColor(theme.muted), QColor(theme.text)
362
+ # Restyling an item emits itemChanged, whose handler redraws, which
363
+ # restyles again: blocked, as the checkbox resync already is.
364
+ self._order_list.blockSignals(True)
365
+ try:
366
+ for i in range(self._order_list.count()):
367
+ item = self._order_list.item(i)
368
+ color = self._order_colors.get(item.data(_ORDER_ROLE))
369
+ item.setIcon(theming.swatch(color) if color else theming.QIcon())
370
+ item.setForeground(muted if item.data(_DIM_ROLE) else text)
371
+ finally:
372
+ self._order_list.blockSignals(False)
373
+
374
+ def _update_order_count_label(self, total: int) -> None:
375
+ self._order_count_label.setText(
376
+ f"{len(self._visible_orders)} of {total} shown"
377
+ )
378
+
379
+ def _on_order_item_changed(self, item: QListWidgetItem) -> None:
380
+ order = item.data(_ORDER_ROLE)
381
+ if item.checkState() == Qt.CheckState.Checked:
382
+ self._visible_orders = self._visible_orders | {order}
383
+ else:
384
+ self._visible_orders = self._visible_orders - {order}
385
+ self._update_order_count_label(self._order_list.count())
386
+ if self._scan is not None:
387
+ self._draw_efficiency(self._scan)
388
+
389
+ def _show_all_orders(self) -> None:
390
+ self._visible_orders = frozenset(self._previous_orders)
391
+ self._resync_order_checkboxes()
392
+
393
+ def _show_no_orders(self) -> None:
394
+ self._visible_orders = frozenset()
395
+ self._resync_order_checkboxes()
396
+
397
+ def _show_default_orders(self) -> None:
398
+ if self._scan is None:
399
+ return
400
+ self._visible_orders = orders_module.default_visible(
401
+ orders_module.summarize(self._scan)
402
+ )
403
+ self._resync_order_checkboxes()
404
+
405
+ def _resync_order_checkboxes(self) -> None:
406
+ self._order_list.blockSignals(True)
407
+ for i in range(self._order_list.count()):
408
+ item = self._order_list.item(i)
409
+ item.setCheckState(
410
+ Qt.CheckState.Checked
411
+ if item.data(_ORDER_ROLE) in self._visible_orders
412
+ else Qt.CheckState.Unchecked
413
+ )
414
+ self._order_list.blockSignals(False)
415
+ self._update_order_count_label(self._order_list.count())
416
+ if self._scan is not None:
417
+ self._draw_efficiency(self._scan)
418
+
419
+ # -- drawing -----------------------------------------------------
420
+
421
+ def _paint(self, lines) -> None:
422
+ self._lines = tuple(lines)
423
+ self._provenance.setHtml(
424
+ provenance.to_html(self._lines, provenance_colors(theming.current()))
425
+ )
426
+
427
+ def _on_theme(self, theme) -> None:
428
+ style_figure(self._figure, theme)
429
+ if self._scan is not None:
430
+ self._draw_efficiency(self._scan)
431
+ else:
432
+ self._canvas.draw_idle()
433
+ if self._lines:
434
+ self._paint(self._lines)
435
+ self._apply_swatches()
436
+
437
+ def _draw_efficiency(self, scan) -> None:
438
+ self._order_colors = draw_efficiency(
439
+ self._axes, scan, self._visible_orders, theming.current()
440
+ )
441
+ self._apply_swatches()
442
+ self._canvas.draw_idle()
443
+
444
+ # -- export ------------------------------------------------------
445
+
446
+ def export_csv(self) -> None:
447
+ if self._scan is None:
448
+ QMessageBox.information(self, "Nothing to export", "Solve first.")
449
+ return
450
+ path, _ = QFileDialog.getSaveFileName(
451
+ self, "Export efficiencies", "efficiency.csv", "CSV (*.csv)"
452
+ )
453
+ if not path:
454
+ return
455
+ rows = self._scan.to_records()
456
+ with open(path, "w", newline="", encoding="utf-8") as handle:
457
+ writer = csv.DictWriter(handle, fieldnames=list(rows[0]))
458
+ writer.writeheader()
459
+ writer.writerows(rows)
460
+ QMessageBox.information(
461
+ self, "Exported", f"{len(rows)} rows to {Path(path).name}"
462
+ )
463
+
464
+
465
+ def _value(widget: QWidget) -> str:
466
+ """The text of an input, whichever kind it is."""
467
+ if isinstance(widget, QComboBox):
468
+ return widget.currentText()
469
+ return widget.text()
@@ -156,6 +156,18 @@ class GeometryPanel(QWidget):
156
156
 
157
157
  entry(surface_form, "Roughness σ (nm)", "roughness")
158
158
 
159
+ layers = entry(surface_form, "Layers", "layers")
160
+ layers.setPlaceholderText("none, e.g. Au 20, Cr 5")
161
+ layers.setToolTip(
162
+ "Conformal layers on the grating, innermost first: a material\n"
163
+ "and a thickness in nm each, separated by commas. Each follows the\n"
164
+ "surface beneath it as its true offset, so convex corners are\n"
165
+ "rounded and concave ones stay sharp. The grating itself is cut\n"
166
+ "into the Coating material.\n"
167
+ "Only the RCWA tab models layers; the others refuse a problem\n"
168
+ "that has any rather than solve the bare grating beneath them."
169
+ )
170
+
159
171
  scan = QGroupBox("Wavelengths (nm)")
160
172
  scan_form = QFormLayout(scan)
161
173
  entry(scan_form, "Start", "wavelength_start")