gratinglab 0.1.0__tar.gz → 0.1.2__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 (151) hide show
  1. gratinglab-0.1.2/PKG-INFO +342 -0
  2. gratinglab-0.1.2/README.md +303 -0
  3. {gratinglab-0.1.0 → gratinglab-0.1.2}/pyproject.toml +11 -5
  4. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/__init__.py +1 -1
  5. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/compare.py +1 -1
  6. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/convergence.py +5 -5
  7. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/geometry.py +7 -7
  8. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/app.py +1 -1
  9. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/diagram.py +6 -6
  10. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/diagram3d.py +5 -5
  11. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/icon.py +1 -1
  12. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/integral_options.py +1 -1
  13. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/mathtext.py +2 -2
  14. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/orders.py +1 -1
  15. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/provenance.py +8 -8
  16. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/qt/geometry_panel.py +3 -3
  17. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/qt/geometry_tab.py +4 -4
  18. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/qt/integral_tab.py +1 -1
  19. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/qt/main_window.py +3 -3
  20. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/qt/profile_plot_panel.py +2 -2
  21. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/qt/scalar_tab.py +1 -1
  22. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/qt/scene3d_panel.py +2 -2
  23. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/qt/setup_tab.py +6 -6
  24. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/qt/worker.py +5 -5
  25. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/richtext.py +8 -8
  26. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/state.py +2 -2
  27. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/illumination.py +2 -2
  28. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/io/efficiency_table.py +2 -2
  29. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/io/ggp.py +1 -1
  30. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/materials/__init__.py +2 -2
  31. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/materials/data/SOURCES.md +1 -1
  32. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/materials/fresnel.py +19 -9
  33. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/materials/optical.py +68 -11
  34. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/analyzer.py +1 -1
  35. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/boundary/average.py +1 -1
  36. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/boundary/pipeline.py +4 -4
  37. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/core/image_flatten.py +1 -1
  38. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/core/tip.py +2 -2
  39. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/gui/__init__.py +1 -1
  40. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/gui/icon.py +2 -2
  41. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/gui/qt/boundary_view.py +4 -4
  42. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/gui/qt/canvas.py +6 -6
  43. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/gui/qt/efficiency_dialog.py +2 -2
  44. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/gui/qt/import_view.py +2 -2
  45. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/gui/qt/worker.py +1 -1
  46. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/gui/state.py +1 -1
  47. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/io/metrics.py +1 -1
  48. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/io/spm.py +3 -3
  49. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/stats/analysis.py +1 -1
  50. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/stats/icc.py +1 -1
  51. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/visualization/diagnostics.py +1 -1
  52. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/wiki/facet-fitting.md +9 -9
  53. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/wiki/file-formats.md +6 -6
  54. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/wiki/flattening.md +7 -7
  55. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/wiki/icc-correction.md +14 -14
  56. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/wiki/overview.md +7 -7
  57. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/wiki/reading-outputs.md +16 -16
  58. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/wiki/row-groups.md +7 -7
  59. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/wiki/tip-correction.md +10 -10
  60. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/workflows.py +2 -2
  61. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/profiles.py +3 -3
  62. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/resolution.py +2 -2
  63. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/result.py +2 -2
  64. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/solvers/base.py +2 -2
  65. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/solvers/scalar.py +46 -28
  66. gratinglab-0.1.2/src/gratinglab.egg-info/PKG-INFO +342 -0
  67. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab.egg-info/requires.txt +2 -2
  68. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_fresnel.py +25 -4
  69. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_materials.py +90 -5
  70. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_result.py +25 -7
  71. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_scalar.py +52 -0
  72. gratinglab-0.1.0/PKG-INFO +0 -319
  73. gratinglab-0.1.0/README.md +0 -280
  74. gratinglab-0.1.0/src/gratinglab.egg-info/PKG-INFO +0 -319
  75. {gratinglab-0.1.0 → gratinglab-0.1.2}/LICENSE +0 -0
  76. {gratinglab-0.1.0 → gratinglab-0.1.2}/setup.cfg +0 -0
  77. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/checks.py +0 -0
  78. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/__init__.py +0 -0
  79. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/docs.py +0 -0
  80. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/qt/__init__.py +0 -0
  81. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/qt/sizing.py +0 -0
  82. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/qt/theory_viewer.py +0 -0
  83. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/scalar_options.py +0 -0
  84. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/io/__init__.py +0 -0
  85. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/materials/refractiveindex.py +0 -0
  86. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/__init__.py +0 -0
  87. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/boundary/__init__.py +0 -0
  88. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/cli.py +0 -0
  89. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/config.py +0 -0
  90. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/core/__init__.py +0 -0
  91. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/core/analysis.py +0 -0
  92. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/core/processing.py +0 -0
  93. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/data/__init__.py +0 -0
  94. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/data/aggregation.py +0 -0
  95. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/gui/qt/__init__.py +0 -0
  96. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/gui/qt/app.py +0 -0
  97. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/gui/qt/main_window.py +0 -0
  98. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/gui/qt/wiki_view.py +0 -0
  99. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/io/__init__.py +0 -0
  100. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/io/file_io.py +0 -0
  101. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/settings.py +0 -0
  102. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/stats/__init__.py +0 -0
  103. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/visualization/__init__.py +0 -0
  104. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/visualization/profiles.py +0 -0
  105. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/visualization/statistics.py +0 -0
  106. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/wiki/__init__.py +0 -0
  107. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/problem.py +0 -0
  108. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/solvers/__init__.py +0 -0
  109. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/solvers/integral/__init__.py +0 -0
  110. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/solvers/integral/_boundary.py +0 -0
  111. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/solvers/integral/_core.py +0 -0
  112. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/solvers/integral/_finite.py +0 -0
  113. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/solvers/integral/_greens.py +0 -0
  114. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/solvers/integral/_kernels.py +0 -0
  115. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/solvers/integral/_nystrom.py +0 -0
  116. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab.egg-info/SOURCES.txt +0 -0
  117. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab.egg-info/dependency_links.txt +0 -0
  118. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab.egg-info/entry_points.txt +0 -0
  119. {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab.egg-info/top_level.txt +0 -0
  120. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_checks.py +0 -0
  121. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_compare.py +0 -0
  122. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_convergence.py +0 -0
  123. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_cross_method.py +0 -0
  124. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_efficiency_table.py +0 -0
  125. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_geometry.py +0 -0
  126. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_ggp.py +0 -0
  127. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_gui_diagram.py +0 -0
  128. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_gui_diagram3d.py +0 -0
  129. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_gui_docs.py +0 -0
  130. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_gui_integral_options.py +0 -0
  131. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_gui_mathtext.py +0 -0
  132. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_gui_orders.py +0 -0
  133. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_gui_provenance.py +0 -0
  134. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_gui_qt.py +0 -0
  135. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_gui_richtext.py +0 -0
  136. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_gui_scalar_options.py +0 -0
  137. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_gui_state.py +0 -0
  138. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_illumination.py +0 -0
  139. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_integral_core.py +0 -0
  140. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_integral_corpus.py +0 -0
  141. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_integral_finite.py +0 -0
  142. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_integral_greens.py +0 -0
  143. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_integral_kernels.py +0 -0
  144. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_integral_operators.py +0 -0
  145. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_integral_solver.py +0 -0
  146. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_perturbation.py +0 -0
  147. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_profiles.py +0 -0
  148. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_refractiveindex.py +0 -0
  149. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_resolution.py +0 -0
  150. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_solver_conformance.py +0 -0
  151. {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_solver_progress.py +0 -0
@@ -0,0 +1,342 @@
1
+ Metadata-Version: 2.4
2
+ Name: gratinglab
3
+ Version: 0.1.2
4
+ Summary: Open platform for diffraction grating groove metrology and efficiency analysis
5
+ Author: Jake McCoy
6
+ License: BSD-3-Clause
7
+ Project-URL: Homepage, https://github.com/jamccoy/gratinglab
8
+ Keywords: diffraction,grating,efficiency,rcwa,x-ray,spectroscopy,afm,metrology,blaze,pcgrate
9
+ Classifier: Development Status :: 2 - Pre-Alpha
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: License :: OSI Approved :: BSD License
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Topic :: Scientific/Engineering :: Physics
15
+ Requires-Python: >=3.11
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: numpy>=1.26
19
+ Requires-Dist: scipy>=1.11
20
+ Requires-Dist: pydantic>=2.6
21
+ Requires-Dist: endstation>=0.2.2
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=7; extra == "dev"
24
+ Requires-Dist: pytest-qt>=4.5; extra == "dev"
25
+ Requires-Dist: matplotlib>=3.8; extra == "dev"
26
+ Requires-Dist: pandas>=2.1; extra == "dev"
27
+ Requires-Dist: ruff>=0.16; extra == "dev"
28
+ Provides-Extra: gui
29
+ Requires-Dist: matplotlib>=3.8; extra == "gui"
30
+ Requires-Dist: PySide6-Essentials>=6.7; extra == "gui"
31
+ Requires-Dist: endstation[qt]>=0.2.2; extra == "gui"
32
+ Provides-Extra: metrology
33
+ Requires-Dist: matplotlib>=3.9; extra == "metrology"
34
+ Provides-Extra: refractiveindex
35
+ Requires-Dist: pyyaml>=6; extra == "refractiveindex"
36
+ Provides-Extra: mut
37
+ Requires-Dist: mutmut<3.3,>=3.2; extra == "mut"
38
+ Dynamic: license-file
39
+
40
+ # gratinglab
41
+
42
+ [![PyPI](https://img.shields.io/pypi/v/gratinglab?cacheSeconds=3600)](https://pypi.org/project/gratinglab/)
43
+ [![Python](https://img.shields.io/pypi/pyversions/gratinglab?cacheSeconds=3600)](https://pypi.org/project/gratinglab/)
44
+ [![tests](https://github.com/jamccoy/gratinglab/actions/workflows/test.yml/badge.svg)](https://github.com/jamccoy/gratinglab/actions/workflows/test.yml)
45
+ [![License: BSD-3-Clause](https://img.shields.io/badge/license-BSD--3--Clause-blue)](https://github.com/jamccoy/gratinglab/blob/master/LICENSE)
46
+
47
+ **[PyPI](https://pypi.org/project/gratinglab/)** · **[Documentation](https://github.com/jamccoy/gratinglab/wiki)** · **[Releases](https://github.com/jamccoy/gratinglab/releases)** · **[Issues](https://github.com/jamccoy/gratinglab/issues)**
48
+
49
+ An open platform for diffraction grating groove metrology and efficiency analysis,
50
+ from an AFM scan of a real grating to its diffraction efficiency, in one package.
51
+
52
+ Rigorous grating efficiency analysis today means either paying for PCGrate (integral
53
+ method) or assembling a patchwork of open RCWA codes that each carry their own
54
+ conventions, normalizations, and blind spots. No open code runs the *same* physical
55
+ problem through several methods for a fair comparison, and none connects a *measured*
56
+ surface to any of them.
57
+
58
+ `gratinglab` is built around one idea: **the physical problem and the numerical method
59
+ are separate, and the problem spec is serializable.** One `Problem` goes to every solver,
60
+ so disagreement between methods shows up in the comparison.
61
+
62
+ ```
63
+ AFM scan -> BoundaryProfile (gratinglab.metrology)
64
+ | measured period, averaged groove, facet fit
65
+ v
66
+ Problem (period + profile + materials; no solver fields)
67
+ Illumination (wavelength + direction + polarization)
68
+ |
69
+ v
70
+ Solver (plugin) -> Result (+ provenance)
71
+ ```
72
+
73
+ The two halves are one package because a file between them loses information. A
74
+ PCGrate `.ggp` boundary carries a *shape* and not a grating: it cannot hold the period,
75
+ so the period gets retyped, guessed, or recovered from a sidecar later.
76
+ `BoundaryProfile.to_problem()` carries the period the scan measured, and the fitted
77
+ facet angle with it.
78
+
79
+ ```python
80
+ from gratinglab.metrology.boundary import build_boundary_profile
81
+
82
+ profile = build_boundary_profile(data, scan_x_size, settings)
83
+ problem = profile.to_problem(coating="Au", blaze_angle=29.5)
84
+ ```
85
+
86
+ ## Status
87
+
88
+ Pre-alpha. The scalar and integral solvers, the comparison harness, the convergence
89
+ harness and the materials layer work end to end. Efficiencies are absolute when a
90
+ coating is named, using an optical constant table of your own (see Install). RCWA is
91
+ contributed and not yet integrated.
92
+
93
+ ## Scope
94
+
95
+ Two halves. **Metrology** (`gratinglab.metrology`) reads AFM scans (Nanoscope `.spm`
96
+ or Gwyddion text exports) and produces blaze angles per groove with row-group
97
+ statistics and an ICC correction, plus one averaged groove normalised to a period.
98
+ Tip convolution can be undone where the tip reached the surface, and the region it
99
+ never reached is marked rather than smoothed over. **Efficiency** is everything else:
100
+ the problem spec, the solvers, the comparison and convergence harnesses, the materials
101
+ layer, and the resolving power the measured groove count implies.
102
+
103
+ Both mounts are supported:
104
+
105
+ - **In-plane / classical:** UV–Vis–NIR optical engineering.
106
+ - **Extreme off-plane (conical), grazing incidence:** soft X-ray spectroscopy, where
107
+ PCGrate is the community standard and general-purpose open RCWA codes are weakest.
108
+
109
+ Planned method backends, in order:
110
+
111
+ | Method | Status |
112
+ |---|---|
113
+ | Scalar (Kirchhoff, X-ray blaze form) | **done**, validated |
114
+ | Integral method (boundary integral, conical, undercut-capable) | **done**: perfect conductivity validated orderwise against PCGrate, and finite conductivity (`conductivity="tabulated"`, Goray & Schmidt 2010) with absolute efficiencies and recorded absorption |
115
+ | RCWA (in-plane, conical, off-plane; Li factorization, S-matrix) | contributed, not yet integrated |
116
+ | C-method (Chandezon) | planned |
117
+
118
+ The comparison harness accepts an efficiency table already computed by an external code
119
+ as one of its methods, so scalar-vs-RCWA-vs-integral plots need no licence for the code
120
+ that produced the reference data:
121
+
122
+ ```python
123
+ reference = read_scan("run.txt", method="integral")
124
+ align(sweep(problem, illumination, wavelengths, ["scalar", reference]))
125
+ ```
126
+
127
+ The caller declares what physics produced an imported file, because the file itself does
128
+ not say. Comparison legends then read `scalar vs integral` rather than naming a product,
129
+ while `Provenance.version` still records which program and version generated it.
130
+
131
+ ### From a measured surface to a spectrograph number
132
+
133
+ The scan carries its own groove count, so resolving power needs nothing retyped:
134
+
135
+ ```python
136
+ problem = profile.to_problem(coating="Au") # n_grooves = the grooves averaged
137
+ resolving_power(problem, illumination, wavelength=2.0, order=3).r
138
+ ```
139
+
140
+ `R = |m|·N` by the Rayleigh criterion, with `line_profile` giving the finite-*N* line
141
+ shape it is derived from. A problem whose `n_grooves` is unset is **refused** rather
142
+ than treated as infinite: "R is undefined" and "R is very large" are different answers.
143
+ Resolving power degraded by real groove-placement error is planned next, and the ideal
144
+ formula is not used to approximate it.
145
+
146
+ ### The tip is not the surface
147
+
148
+ An AFM image is the surface dilated by the tip. `tip_correction='erosion'` inverts that
149
+ where the inversion exists (a cone-plus-spherical-apex tip, Villarrubia 1997) and
150
+ reports a **certainty map** for where it does not. No algorithm can recover a trough
151
+ the tip never entered, and a correction that filled it in silently would be worse than
152
+ no correction.
153
+
154
+ It has already been useful. On the one real scan whose depth and facet angle disagree
155
+ by 7.6°, erosion with the nominal probe recovers *nothing*, and reproducing the deficit
156
+ forward takes an apex worn to about 80 nm, which rules out the nominal tip as the
157
+ explanation ([findings](https://github.com/jamccoy/gratinglab/blob/master/docs/findings.md)).
158
+
159
+ ## Install
160
+
161
+ ```bash
162
+ pip install gratinglab # solvers only: numpy, scipy, pydantic
163
+ pip install "gratinglab[metrology]" # + AFM readers and the boundary pipeline
164
+ pip install "gratinglab[gui]" # + the Qt windows
165
+ pip install "gratinglab[refractiveindex]" # + refractiveindex.info tables
166
+ ```
167
+
168
+ With [uv](https://docs.astral.sh/uv/), the same extras work:
169
+
170
+ ```bash
171
+ uv pip install "gratinglab[refractiveindex]" # into the active environment
172
+ uv add gratinglab # into a uv project
173
+ uv tool install "gratinglab[gui]" # gratinglab-gui as a standalone command
174
+ ```
175
+
176
+ From a checkout, `pip install -e ".[dev,gui,metrology,refractiveindex]"` instead.
177
+
178
+ No optical constant tables ship with the package, so absolute efficiencies need
179
+ your own. A library is a directory of them, one material per file, named for
180
+ the material:
181
+
182
+ | File | From | Range |
183
+ |---|---|---|
184
+ | `Pt.txt` | a [CXRO](https://henke.lbl.gov/optical_constants/) export | X-ray |
185
+ | `Pt.yml` | a [refractiveindex.info](https://refractiveindex.info) file, renamed from its author's name | ultraviolet to infrared |
186
+ | `Pt.ari` | a PCGrate table | whatever it covers |
187
+
188
+ Save one as `~/.gratinglab/materials/Pt.yml` and `coating="Pt"` works.
189
+ `GRATINGLAB_MATERIALS` points elsewhere, and `materials.from_cxro_file(path)`
190
+ or `from_refractiveindex_file(path)` reads a file without a library. The YAML
191
+ files need the `refractiveindex` extra. See
192
+ [`SOURCES.md`](https://github.com/jamccoy/gratinglab/blob/master/src/gratinglab/materials/data/SOURCES.md)
193
+ for why none ship.
194
+
195
+ The core install has no plotting stack, so a user who only runs solvers never installs
196
+ one. Each subpackage that needs an extra names it when it is missing, rather than
197
+ failing with a bare `ModuleNotFoundError`.
198
+
199
+ Besides numpy, scipy and pydantic, the one dependency is
200
+ [endstation](https://github.com/jamccoy/endstation), shared scaffolding for desktop
201
+ science tools by the same author. gratinglab uses its optional-extra guards (the
202
+ messages above), form validation, Qt layout sizing, test helpers, and the tools that
203
+ build the icon and the macOS app and publish the wiki. endstation's own core has no
204
+ dependencies, so it adds nothing to a solver-only install; its Qt parts arrive only
205
+ with the `gui` extra.
206
+
207
+ ### Where the measurement data lives
208
+
209
+ Scans are **not** in the repository. They are research data, and the same rule already
210
+ applies to the PCGrate reference corpus:
211
+
212
+ | Data | Environment variable | Default |
213
+ |---|---|---|
214
+ | AFM scans | `GRATINGLAB_AFM_DIR` | `~/Documents/afm_scans` |
215
+ | PCGrate reference corpus | `GRATINGLAB_REF_DIR` | `~/Documents/diffraction_efficiency` |
216
+
217
+ Tests that need either **skip cleanly** when it is absent, so the suite is green on a
218
+ fresh clone. Most of the metrology suite does not need it at all: it runs against a
219
+ committed synthetic scan of a *known* grating
220
+ (`tools/metrology/make_synthetic_scan.py`), which also lets a test check that the
221
+ pipeline recovers the period and blaze angle it was given.
222
+
223
+ ## GUI
224
+
225
+ ```bash
226
+ .venv/bin/python -m pip install -e ".[gui,metrology]"
227
+ gratinglab-gui # geometry, solvers, comparison
228
+ gratinglab-metrology-gui # AFM import, blaze angles, boundary profiles
229
+ ```
230
+
231
+ Qt (PySide6), so `pip install "gratinglab[gui]"` is self-sufficient, with no system
232
+ toolkit to install first. PySide6 is LGPLv3 and an optional, dynamically-linked extra,
233
+ which leaves this package BSD-3.
234
+
235
+ Grating geometry lives in a dock on the left (Ctrl+G closes it and hands the width back),
236
+ and there is one tab per concern:
237
+
238
+ - **Grating Geometry:** a rotatable 3D view of the diffraction cone, with every wave
239
+ vector drawn as an exact unit vector. The orders separate in *azimuth* rather than
240
+ polar angle (93.8° of fan at 0.0° of polar spread for the reference off-plane
241
+ geometry), so the "down the cone axis" preset plus a uniform 27× zoom makes them
242
+ readable at true scale. γ is never exaggerated: uniform zoom is a similarity
243
+ transform, so every angle in the view is preserved exactly, and the magnification is
244
+ stated on the canvas. Beside it are the dispersion-plane cross-section, a `sin β`
245
+ ladder that includes evanescent orders, and the groove profile. The picture updates
246
+ as you type.
247
+ - **Scalar (Kirchhoff):** the solve, efficiency per order with per-order visibility, and
248
+ the **provenance panel**: every validity warning verbatim, the summed-efficiency check,
249
+ and whether convergence was demonstrated. A result that violates energy conservation
250
+ says so. **Check convergence…** runs the sweep and reports the coarsest defensible
251
+ setting, with a progress bar and a Cancel button. Solvers report per wavelength, so
252
+ Cancel stops the computation, not only the display.
253
+ - **Integral method:** the rigorous backend, with the same panel and the same
254
+ provenance, plus its own boundary condition control (perfect conductivity or
255
+ tabulated optical constants). It takes minutes where scalar takes milliseconds, so the
256
+ window opens on the fast solver and this tab solves when you ask it to.
257
+ - **Setup:** the material library: which optical constants are installed, what range
258
+ each covers, where they came from, and how to add your own. The coating *control*
259
+ lives in the geometry dock, because it is a `Problem` field like the period and one
260
+ `Problem` feeds every tab.
261
+
262
+ Each registered solver gets a tab generated from the registry, so a contributed backend
263
+ gets its panel by registering rather than by editing the window.
264
+
265
+ Help carries the theory pages, rendered with typeset math and real tables.
266
+
267
+ A macOS `.app` can be built with `python tools/make_app.py` (macOS 13+, which is the
268
+ PySide6 wheel's own floor); the icon is regenerated by `python tools/make_icon.py`.
269
+
270
+ ## Documentation
271
+
272
+ | Document | What it holds |
273
+ |---|---|
274
+ | [`docs/roadmap.md`](https://github.com/jamccoy/gratinglab/blob/master/docs/roadmap.md) | **Where the project is, what is next, and the open questions.** Start here |
275
+ | [`docs/conventions.md`](https://github.com/jamccoy/gratinglab/blob/master/docs/conventions.md) | **Normative.** Signs, units, normalizations, and the reference errata |
276
+ | [`docs/theory/scalar.md`](https://github.com/jamccoy/gratinglab/blob/master/docs/theory/scalar.md) | Full derivation for the scalar solver, its closed forms and its limits |
277
+ | [`docs/theory/integral.md`](https://github.com/jamccoy/gratinglab/blob/master/docs/theory/integral.md) | The boundary-integral solver: conical formulation, perfect and finite conductivity |
278
+ | [`docs/theory/metrology.md`](https://github.com/jamccoy/gratinglab/blob/master/docs/theory/metrology.md) | What the AFM pipeline assumes, what it measures, and what it does not, including what tip erosion can and cannot give back |
279
+ | [`docs/findings.md`](https://github.com/jamccoy/gratinglab/blob/master/docs/findings.md) | Empirical results, each with the evidence that established it |
280
+ | [`docs/references.md`](https://github.com/jamccoy/gratinglab/blob/master/docs/references.md) | The literature each part is built from, mapped to the part it serves |
281
+ | [`docs/mutation-testing.md`](https://github.com/jamccoy/gratinglab/blob/master/docs/mutation-testing.md) | What is mutated, what is not, and how to read a survivor |
282
+ | [`benchmarks/corpus.toml`](https://github.com/jamccoy/gratinglab/blob/master/benchmarks/corpus.toml) | Geometry for the reference corpus, most of it recovered from the data |
283
+
284
+ Read `conventions.md` before writing a solver or filing a bug about a sign. The short
285
+ version is time convention `exp(-iωt)`, lossy *n* = n′ + ik, grating equation
286
+ `sin α + sin β_m = mλ/(p sin γ)`, efficiencies **absolute**, lengths in nm, angles in
287
+ degrees at the API boundary.
288
+
289
+ Each solver has a theory page stating its assumptions, its derivation in the code's own
290
+ notation, and the conditions under which it stops being trustworthy.
291
+
292
+ The [wiki](https://github.com/jamccoy/gratinglab/wiki) is a published copy of `docs/`,
293
+ not a second source. Edit `docs/` and republish, or the copy people read becomes the
294
+ one nobody edits:
295
+
296
+ ```bash
297
+ git clone https://github.com/jamccoy/gratinglab.wiki.git ../gratinglab.wiki
298
+ .venv/bin/python tools/publish_wiki.py --wiki ../gratinglab.wiki # then review and push
299
+ ```
300
+
301
+ ## Development
302
+
303
+ ```bash
304
+ .venv/bin/python -m pip install -e ".[dev,gui,metrology,refractiveindex]"
305
+ .venv/bin/python -m pytest
306
+ .venv/bin/ruff check .
307
+ ```
308
+
309
+ Three practices this project relies on:
310
+
311
+ - **Physics self-checks over reference data.** `checks.check_reciprocity` and
312
+ `checks.check_energy_balance` constrain the *model* rather than comparing against a
313
+ formula derived the same way the solver computes it. They need no reference data and
314
+ apply to every backend.
315
+ - **Convergence demonstrated, not assumed.** `convergence.check_convergence` sweeps
316
+ whatever a solver declares as its accuracy knob and reports the coarsest setting it
317
+ can defend, or reports that it found none, which is itself a result. It requires
318
+ *three* consecutive values to agree, because quadrature error is measurably
319
+ non-monotone for a blazed profile and one agreement can be an accident the next
320
+ refinement contradicts
321
+ ([findings](https://github.com/jamccoy/gratinglab/blob/master/docs/findings.md#quadrature-error-is-not-monotone-in-the-number-of-points)).
322
+
323
+ ```python
324
+ report = check_convergence(scalar, problem, illumination, wavelengths)
325
+ report.converged_at # 4096 -- what a production run should use
326
+ report.scan.provenance.converged # True
327
+ ```
328
+ - **Mutation testing.** The suite is run against deliberately broken copies of the
329
+ physics to check that it notices. Coverage says a line ran; this asks whether any test
330
+ would fail if the line were wrong. It has found real gaps both times it was run; see
331
+ [findings](https://github.com/jamccoy/gratinglab/blob/master/docs/findings.md#mutation-testing-found-a-real-gap)
332
+ and [docs/mutation-testing.md](https://github.com/jamccoy/gratinglab/blob/master/docs/mutation-testing.md)
333
+ for how to run it.
334
+
335
+ ```bash
336
+ .venv/bin/python -m pip install -e ".[mut]"
337
+ .venv/bin/mutmut run
338
+ ```
339
+
340
+ ## License
341
+
342
+ BSD-3-Clause.
@@ -0,0 +1,303 @@
1
+ # gratinglab
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/gratinglab?cacheSeconds=3600)](https://pypi.org/project/gratinglab/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/gratinglab?cacheSeconds=3600)](https://pypi.org/project/gratinglab/)
5
+ [![tests](https://github.com/jamccoy/gratinglab/actions/workflows/test.yml/badge.svg)](https://github.com/jamccoy/gratinglab/actions/workflows/test.yml)
6
+ [![License: BSD-3-Clause](https://img.shields.io/badge/license-BSD--3--Clause-blue)](https://github.com/jamccoy/gratinglab/blob/master/LICENSE)
7
+
8
+ **[PyPI](https://pypi.org/project/gratinglab/)** · **[Documentation](https://github.com/jamccoy/gratinglab/wiki)** · **[Releases](https://github.com/jamccoy/gratinglab/releases)** · **[Issues](https://github.com/jamccoy/gratinglab/issues)**
9
+
10
+ An open platform for diffraction grating groove metrology and efficiency analysis,
11
+ from an AFM scan of a real grating to its diffraction efficiency, in one package.
12
+
13
+ Rigorous grating efficiency analysis today means either paying for PCGrate (integral
14
+ method) or assembling a patchwork of open RCWA codes that each carry their own
15
+ conventions, normalizations, and blind spots. No open code runs the *same* physical
16
+ problem through several methods for a fair comparison, and none connects a *measured*
17
+ surface to any of them.
18
+
19
+ `gratinglab` is built around one idea: **the physical problem and the numerical method
20
+ are separate, and the problem spec is serializable.** One `Problem` goes to every solver,
21
+ so disagreement between methods shows up in the comparison.
22
+
23
+ ```
24
+ AFM scan -> BoundaryProfile (gratinglab.metrology)
25
+ | measured period, averaged groove, facet fit
26
+ v
27
+ Problem (period + profile + materials; no solver fields)
28
+ Illumination (wavelength + direction + polarization)
29
+ |
30
+ v
31
+ Solver (plugin) -> Result (+ provenance)
32
+ ```
33
+
34
+ The two halves are one package because a file between them loses information. A
35
+ PCGrate `.ggp` boundary carries a *shape* and not a grating: it cannot hold the period,
36
+ so the period gets retyped, guessed, or recovered from a sidecar later.
37
+ `BoundaryProfile.to_problem()` carries the period the scan measured, and the fitted
38
+ facet angle with it.
39
+
40
+ ```python
41
+ from gratinglab.metrology.boundary import build_boundary_profile
42
+
43
+ profile = build_boundary_profile(data, scan_x_size, settings)
44
+ problem = profile.to_problem(coating="Au", blaze_angle=29.5)
45
+ ```
46
+
47
+ ## Status
48
+
49
+ Pre-alpha. The scalar and integral solvers, the comparison harness, the convergence
50
+ harness and the materials layer work end to end. Efficiencies are absolute when a
51
+ coating is named, using an optical constant table of your own (see Install). RCWA is
52
+ contributed and not yet integrated.
53
+
54
+ ## Scope
55
+
56
+ Two halves. **Metrology** (`gratinglab.metrology`) reads AFM scans (Nanoscope `.spm`
57
+ or Gwyddion text exports) and produces blaze angles per groove with row-group
58
+ statistics and an ICC correction, plus one averaged groove normalised to a period.
59
+ Tip convolution can be undone where the tip reached the surface, and the region it
60
+ never reached is marked rather than smoothed over. **Efficiency** is everything else:
61
+ the problem spec, the solvers, the comparison and convergence harnesses, the materials
62
+ layer, and the resolving power the measured groove count implies.
63
+
64
+ Both mounts are supported:
65
+
66
+ - **In-plane / classical:** UV–Vis–NIR optical engineering.
67
+ - **Extreme off-plane (conical), grazing incidence:** soft X-ray spectroscopy, where
68
+ PCGrate is the community standard and general-purpose open RCWA codes are weakest.
69
+
70
+ Planned method backends, in order:
71
+
72
+ | Method | Status |
73
+ |---|---|
74
+ | Scalar (Kirchhoff, X-ray blaze form) | **done**, validated |
75
+ | Integral method (boundary integral, conical, undercut-capable) | **done**: perfect conductivity validated orderwise against PCGrate, and finite conductivity (`conductivity="tabulated"`, Goray & Schmidt 2010) with absolute efficiencies and recorded absorption |
76
+ | RCWA (in-plane, conical, off-plane; Li factorization, S-matrix) | contributed, not yet integrated |
77
+ | C-method (Chandezon) | planned |
78
+
79
+ The comparison harness accepts an efficiency table already computed by an external code
80
+ as one of its methods, so scalar-vs-RCWA-vs-integral plots need no licence for the code
81
+ that produced the reference data:
82
+
83
+ ```python
84
+ reference = read_scan("run.txt", method="integral")
85
+ align(sweep(problem, illumination, wavelengths, ["scalar", reference]))
86
+ ```
87
+
88
+ The caller declares what physics produced an imported file, because the file itself does
89
+ not say. Comparison legends then read `scalar vs integral` rather than naming a product,
90
+ while `Provenance.version` still records which program and version generated it.
91
+
92
+ ### From a measured surface to a spectrograph number
93
+
94
+ The scan carries its own groove count, so resolving power needs nothing retyped:
95
+
96
+ ```python
97
+ problem = profile.to_problem(coating="Au") # n_grooves = the grooves averaged
98
+ resolving_power(problem, illumination, wavelength=2.0, order=3).r
99
+ ```
100
+
101
+ `R = |m|·N` by the Rayleigh criterion, with `line_profile` giving the finite-*N* line
102
+ shape it is derived from. A problem whose `n_grooves` is unset is **refused** rather
103
+ than treated as infinite: "R is undefined" and "R is very large" are different answers.
104
+ Resolving power degraded by real groove-placement error is planned next, and the ideal
105
+ formula is not used to approximate it.
106
+
107
+ ### The tip is not the surface
108
+
109
+ An AFM image is the surface dilated by the tip. `tip_correction='erosion'` inverts that
110
+ where the inversion exists (a cone-plus-spherical-apex tip, Villarrubia 1997) and
111
+ reports a **certainty map** for where it does not. No algorithm can recover a trough
112
+ the tip never entered, and a correction that filled it in silently would be worse than
113
+ no correction.
114
+
115
+ It has already been useful. On the one real scan whose depth and facet angle disagree
116
+ by 7.6°, erosion with the nominal probe recovers *nothing*, and reproducing the deficit
117
+ forward takes an apex worn to about 80 nm, which rules out the nominal tip as the
118
+ explanation ([findings](https://github.com/jamccoy/gratinglab/blob/master/docs/findings.md)).
119
+
120
+ ## Install
121
+
122
+ ```bash
123
+ pip install gratinglab # solvers only: numpy, scipy, pydantic
124
+ pip install "gratinglab[metrology]" # + AFM readers and the boundary pipeline
125
+ pip install "gratinglab[gui]" # + the Qt windows
126
+ pip install "gratinglab[refractiveindex]" # + refractiveindex.info tables
127
+ ```
128
+
129
+ With [uv](https://docs.astral.sh/uv/), the same extras work:
130
+
131
+ ```bash
132
+ uv pip install "gratinglab[refractiveindex]" # into the active environment
133
+ uv add gratinglab # into a uv project
134
+ uv tool install "gratinglab[gui]" # gratinglab-gui as a standalone command
135
+ ```
136
+
137
+ From a checkout, `pip install -e ".[dev,gui,metrology,refractiveindex]"` instead.
138
+
139
+ No optical constant tables ship with the package, so absolute efficiencies need
140
+ your own. A library is a directory of them, one material per file, named for
141
+ the material:
142
+
143
+ | File | From | Range |
144
+ |---|---|---|
145
+ | `Pt.txt` | a [CXRO](https://henke.lbl.gov/optical_constants/) export | X-ray |
146
+ | `Pt.yml` | a [refractiveindex.info](https://refractiveindex.info) file, renamed from its author's name | ultraviolet to infrared |
147
+ | `Pt.ari` | a PCGrate table | whatever it covers |
148
+
149
+ Save one as `~/.gratinglab/materials/Pt.yml` and `coating="Pt"` works.
150
+ `GRATINGLAB_MATERIALS` points elsewhere, and `materials.from_cxro_file(path)`
151
+ or `from_refractiveindex_file(path)` reads a file without a library. The YAML
152
+ files need the `refractiveindex` extra. See
153
+ [`SOURCES.md`](https://github.com/jamccoy/gratinglab/blob/master/src/gratinglab/materials/data/SOURCES.md)
154
+ for why none ship.
155
+
156
+ The core install has no plotting stack, so a user who only runs solvers never installs
157
+ one. Each subpackage that needs an extra names it when it is missing, rather than
158
+ failing with a bare `ModuleNotFoundError`.
159
+
160
+ Besides numpy, scipy and pydantic, the one dependency is
161
+ [endstation](https://github.com/jamccoy/endstation), shared scaffolding for desktop
162
+ science tools by the same author. gratinglab uses its optional-extra guards (the
163
+ messages above), form validation, Qt layout sizing, test helpers, and the tools that
164
+ build the icon and the macOS app and publish the wiki. endstation's own core has no
165
+ dependencies, so it adds nothing to a solver-only install; its Qt parts arrive only
166
+ with the `gui` extra.
167
+
168
+ ### Where the measurement data lives
169
+
170
+ Scans are **not** in the repository. They are research data, and the same rule already
171
+ applies to the PCGrate reference corpus:
172
+
173
+ | Data | Environment variable | Default |
174
+ |---|---|---|
175
+ | AFM scans | `GRATINGLAB_AFM_DIR` | `~/Documents/afm_scans` |
176
+ | PCGrate reference corpus | `GRATINGLAB_REF_DIR` | `~/Documents/diffraction_efficiency` |
177
+
178
+ Tests that need either **skip cleanly** when it is absent, so the suite is green on a
179
+ fresh clone. Most of the metrology suite does not need it at all: it runs against a
180
+ committed synthetic scan of a *known* grating
181
+ (`tools/metrology/make_synthetic_scan.py`), which also lets a test check that the
182
+ pipeline recovers the period and blaze angle it was given.
183
+
184
+ ## GUI
185
+
186
+ ```bash
187
+ .venv/bin/python -m pip install -e ".[gui,metrology]"
188
+ gratinglab-gui # geometry, solvers, comparison
189
+ gratinglab-metrology-gui # AFM import, blaze angles, boundary profiles
190
+ ```
191
+
192
+ Qt (PySide6), so `pip install "gratinglab[gui]"` is self-sufficient, with no system
193
+ toolkit to install first. PySide6 is LGPLv3 and an optional, dynamically-linked extra,
194
+ which leaves this package BSD-3.
195
+
196
+ Grating geometry lives in a dock on the left (Ctrl+G closes it and hands the width back),
197
+ and there is one tab per concern:
198
+
199
+ - **Grating Geometry:** a rotatable 3D view of the diffraction cone, with every wave
200
+ vector drawn as an exact unit vector. The orders separate in *azimuth* rather than
201
+ polar angle (93.8° of fan at 0.0° of polar spread for the reference off-plane
202
+ geometry), so the "down the cone axis" preset plus a uniform 27× zoom makes them
203
+ readable at true scale. γ is never exaggerated: uniform zoom is a similarity
204
+ transform, so every angle in the view is preserved exactly, and the magnification is
205
+ stated on the canvas. Beside it are the dispersion-plane cross-section, a `sin β`
206
+ ladder that includes evanescent orders, and the groove profile. The picture updates
207
+ as you type.
208
+ - **Scalar (Kirchhoff):** the solve, efficiency per order with per-order visibility, and
209
+ the **provenance panel**: every validity warning verbatim, the summed-efficiency check,
210
+ and whether convergence was demonstrated. A result that violates energy conservation
211
+ says so. **Check convergence…** runs the sweep and reports the coarsest defensible
212
+ setting, with a progress bar and a Cancel button. Solvers report per wavelength, so
213
+ Cancel stops the computation, not only the display.
214
+ - **Integral method:** the rigorous backend, with the same panel and the same
215
+ provenance, plus its own boundary condition control (perfect conductivity or
216
+ tabulated optical constants). It takes minutes where scalar takes milliseconds, so the
217
+ window opens on the fast solver and this tab solves when you ask it to.
218
+ - **Setup:** the material library: which optical constants are installed, what range
219
+ each covers, where they came from, and how to add your own. The coating *control*
220
+ lives in the geometry dock, because it is a `Problem` field like the period and one
221
+ `Problem` feeds every tab.
222
+
223
+ Each registered solver gets a tab generated from the registry, so a contributed backend
224
+ gets its panel by registering rather than by editing the window.
225
+
226
+ Help carries the theory pages, rendered with typeset math and real tables.
227
+
228
+ A macOS `.app` can be built with `python tools/make_app.py` (macOS 13+, which is the
229
+ PySide6 wheel's own floor); the icon is regenerated by `python tools/make_icon.py`.
230
+
231
+ ## Documentation
232
+
233
+ | Document | What it holds |
234
+ |---|---|
235
+ | [`docs/roadmap.md`](https://github.com/jamccoy/gratinglab/blob/master/docs/roadmap.md) | **Where the project is, what is next, and the open questions.** Start here |
236
+ | [`docs/conventions.md`](https://github.com/jamccoy/gratinglab/blob/master/docs/conventions.md) | **Normative.** Signs, units, normalizations, and the reference errata |
237
+ | [`docs/theory/scalar.md`](https://github.com/jamccoy/gratinglab/blob/master/docs/theory/scalar.md) | Full derivation for the scalar solver, its closed forms and its limits |
238
+ | [`docs/theory/integral.md`](https://github.com/jamccoy/gratinglab/blob/master/docs/theory/integral.md) | The boundary-integral solver: conical formulation, perfect and finite conductivity |
239
+ | [`docs/theory/metrology.md`](https://github.com/jamccoy/gratinglab/blob/master/docs/theory/metrology.md) | What the AFM pipeline assumes, what it measures, and what it does not, including what tip erosion can and cannot give back |
240
+ | [`docs/findings.md`](https://github.com/jamccoy/gratinglab/blob/master/docs/findings.md) | Empirical results, each with the evidence that established it |
241
+ | [`docs/references.md`](https://github.com/jamccoy/gratinglab/blob/master/docs/references.md) | The literature each part is built from, mapped to the part it serves |
242
+ | [`docs/mutation-testing.md`](https://github.com/jamccoy/gratinglab/blob/master/docs/mutation-testing.md) | What is mutated, what is not, and how to read a survivor |
243
+ | [`benchmarks/corpus.toml`](https://github.com/jamccoy/gratinglab/blob/master/benchmarks/corpus.toml) | Geometry for the reference corpus, most of it recovered from the data |
244
+
245
+ Read `conventions.md` before writing a solver or filing a bug about a sign. The short
246
+ version is time convention `exp(-iωt)`, lossy *n* = n′ + ik, grating equation
247
+ `sin α + sin β_m = mλ/(p sin γ)`, efficiencies **absolute**, lengths in nm, angles in
248
+ degrees at the API boundary.
249
+
250
+ Each solver has a theory page stating its assumptions, its derivation in the code's own
251
+ notation, and the conditions under which it stops being trustworthy.
252
+
253
+ The [wiki](https://github.com/jamccoy/gratinglab/wiki) is a published copy of `docs/`,
254
+ not a second source. Edit `docs/` and republish, or the copy people read becomes the
255
+ one nobody edits:
256
+
257
+ ```bash
258
+ git clone https://github.com/jamccoy/gratinglab.wiki.git ../gratinglab.wiki
259
+ .venv/bin/python tools/publish_wiki.py --wiki ../gratinglab.wiki # then review and push
260
+ ```
261
+
262
+ ## Development
263
+
264
+ ```bash
265
+ .venv/bin/python -m pip install -e ".[dev,gui,metrology,refractiveindex]"
266
+ .venv/bin/python -m pytest
267
+ .venv/bin/ruff check .
268
+ ```
269
+
270
+ Three practices this project relies on:
271
+
272
+ - **Physics self-checks over reference data.** `checks.check_reciprocity` and
273
+ `checks.check_energy_balance` constrain the *model* rather than comparing against a
274
+ formula derived the same way the solver computes it. They need no reference data and
275
+ apply to every backend.
276
+ - **Convergence demonstrated, not assumed.** `convergence.check_convergence` sweeps
277
+ whatever a solver declares as its accuracy knob and reports the coarsest setting it
278
+ can defend, or reports that it found none, which is itself a result. It requires
279
+ *three* consecutive values to agree, because quadrature error is measurably
280
+ non-monotone for a blazed profile and one agreement can be an accident the next
281
+ refinement contradicts
282
+ ([findings](https://github.com/jamccoy/gratinglab/blob/master/docs/findings.md#quadrature-error-is-not-monotone-in-the-number-of-points)).
283
+
284
+ ```python
285
+ report = check_convergence(scalar, problem, illumination, wavelengths)
286
+ report.converged_at # 4096 -- what a production run should use
287
+ report.scan.provenance.converged # True
288
+ ```
289
+ - **Mutation testing.** The suite is run against deliberately broken copies of the
290
+ physics to check that it notices. Coverage says a line ran; this asks whether any test
291
+ would fail if the line were wrong. It has found real gaps both times it was run; see
292
+ [findings](https://github.com/jamccoy/gratinglab/blob/master/docs/findings.md#mutation-testing-found-a-real-gap)
293
+ and [docs/mutation-testing.md](https://github.com/jamccoy/gratinglab/blob/master/docs/mutation-testing.md)
294
+ for how to run it.
295
+
296
+ ```bash
297
+ .venv/bin/python -m pip install -e ".[mut]"
298
+ .venv/bin/mutmut run
299
+ ```
300
+
301
+ ## License
302
+
303
+ BSD-3-Clause.