gratinglab 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. gratinglab-0.1.0/LICENSE +28 -0
  2. gratinglab-0.1.0/PKG-INFO +319 -0
  3. gratinglab-0.1.0/README.md +280 -0
  4. gratinglab-0.1.0/pyproject.toml +179 -0
  5. gratinglab-0.1.0/setup.cfg +4 -0
  6. gratinglab-0.1.0/src/gratinglab/__init__.py +35 -0
  7. gratinglab-0.1.0/src/gratinglab/checks.py +263 -0
  8. gratinglab-0.1.0/src/gratinglab/compare.py +233 -0
  9. gratinglab-0.1.0/src/gratinglab/convergence.py +455 -0
  10. gratinglab-0.1.0/src/gratinglab/geometry.py +483 -0
  11. gratinglab-0.1.0/src/gratinglab/gui/__init__.py +0 -0
  12. gratinglab-0.1.0/src/gratinglab/gui/app.py +80 -0
  13. gratinglab-0.1.0/src/gratinglab/gui/diagram.py +699 -0
  14. gratinglab-0.1.0/src/gratinglab/gui/diagram3d.py +609 -0
  15. gratinglab-0.1.0/src/gratinglab/gui/docs.py +189 -0
  16. gratinglab-0.1.0/src/gratinglab/gui/icon.py +101 -0
  17. gratinglab-0.1.0/src/gratinglab/gui/integral_options.py +219 -0
  18. gratinglab-0.1.0/src/gratinglab/gui/mathtext.py +209 -0
  19. gratinglab-0.1.0/src/gratinglab/gui/orders.py +152 -0
  20. gratinglab-0.1.0/src/gratinglab/gui/provenance.py +273 -0
  21. gratinglab-0.1.0/src/gratinglab/gui/qt/__init__.py +20 -0
  22. gratinglab-0.1.0/src/gratinglab/gui/qt/geometry_panel.py +239 -0
  23. gratinglab-0.1.0/src/gratinglab/gui/qt/geometry_tab.py +387 -0
  24. gratinglab-0.1.0/src/gratinglab/gui/qt/integral_tab.py +447 -0
  25. gratinglab-0.1.0/src/gratinglab/gui/qt/main_window.py +514 -0
  26. gratinglab-0.1.0/src/gratinglab/gui/qt/profile_plot_panel.py +67 -0
  27. gratinglab-0.1.0/src/gratinglab/gui/qt/scalar_tab.py +498 -0
  28. gratinglab-0.1.0/src/gratinglab/gui/qt/scene3d_panel.py +347 -0
  29. gratinglab-0.1.0/src/gratinglab/gui/qt/setup_tab.py +215 -0
  30. gratinglab-0.1.0/src/gratinglab/gui/qt/sizing.py +25 -0
  31. gratinglab-0.1.0/src/gratinglab/gui/qt/theory_viewer.py +89 -0
  32. gratinglab-0.1.0/src/gratinglab/gui/qt/worker.py +203 -0
  33. gratinglab-0.1.0/src/gratinglab/gui/richtext.py +463 -0
  34. gratinglab-0.1.0/src/gratinglab/gui/scalar_options.py +139 -0
  35. gratinglab-0.1.0/src/gratinglab/gui/state.py +379 -0
  36. gratinglab-0.1.0/src/gratinglab/illumination.py +212 -0
  37. gratinglab-0.1.0/src/gratinglab/io/__init__.py +0 -0
  38. gratinglab-0.1.0/src/gratinglab/io/efficiency_table.py +382 -0
  39. gratinglab-0.1.0/src/gratinglab/io/ggp.py +248 -0
  40. gratinglab-0.1.0/src/gratinglab/materials/__init__.py +218 -0
  41. gratinglab-0.1.0/src/gratinglab/materials/data/SOURCES.md +53 -0
  42. gratinglab-0.1.0/src/gratinglab/materials/fresnel.py +282 -0
  43. gratinglab-0.1.0/src/gratinglab/materials/optical.py +319 -0
  44. gratinglab-0.1.0/src/gratinglab/materials/refractiveindex.py +306 -0
  45. gratinglab-0.1.0/src/gratinglab/metrology/__init__.py +121 -0
  46. gratinglab-0.1.0/src/gratinglab/metrology/analyzer.py +875 -0
  47. gratinglab-0.1.0/src/gratinglab/metrology/boundary/__init__.py +27 -0
  48. gratinglab-0.1.0/src/gratinglab/metrology/boundary/average.py +143 -0
  49. gratinglab-0.1.0/src/gratinglab/metrology/boundary/pipeline.py +211 -0
  50. gratinglab-0.1.0/src/gratinglab/metrology/cli.py +52 -0
  51. gratinglab-0.1.0/src/gratinglab/metrology/config.py +170 -0
  52. gratinglab-0.1.0/src/gratinglab/metrology/core/__init__.py +23 -0
  53. gratinglab-0.1.0/src/gratinglab/metrology/core/analysis.py +315 -0
  54. gratinglab-0.1.0/src/gratinglab/metrology/core/image_flatten.py +131 -0
  55. gratinglab-0.1.0/src/gratinglab/metrology/core/processing.py +344 -0
  56. gratinglab-0.1.0/src/gratinglab/metrology/core/tip.py +233 -0
  57. gratinglab-0.1.0/src/gratinglab/metrology/data/__init__.py +16 -0
  58. gratinglab-0.1.0/src/gratinglab/metrology/data/aggregation.py +193 -0
  59. gratinglab-0.1.0/src/gratinglab/metrology/gui/__init__.py +56 -0
  60. gratinglab-0.1.0/src/gratinglab/metrology/gui/icon.py +136 -0
  61. gratinglab-0.1.0/src/gratinglab/metrology/gui/qt/__init__.py +19 -0
  62. gratinglab-0.1.0/src/gratinglab/metrology/gui/qt/app.py +28 -0
  63. gratinglab-0.1.0/src/gratinglab/metrology/gui/qt/boundary_view.py +324 -0
  64. gratinglab-0.1.0/src/gratinglab/metrology/gui/qt/canvas.py +151 -0
  65. gratinglab-0.1.0/src/gratinglab/metrology/gui/qt/efficiency_dialog.py +273 -0
  66. gratinglab-0.1.0/src/gratinglab/metrology/gui/qt/import_view.py +380 -0
  67. gratinglab-0.1.0/src/gratinglab/metrology/gui/qt/main_window.py +398 -0
  68. gratinglab-0.1.0/src/gratinglab/metrology/gui/qt/wiki_view.py +99 -0
  69. gratinglab-0.1.0/src/gratinglab/metrology/gui/qt/worker.py +59 -0
  70. gratinglab-0.1.0/src/gratinglab/metrology/gui/state.py +168 -0
  71. gratinglab-0.1.0/src/gratinglab/metrology/io/__init__.py +8 -0
  72. gratinglab-0.1.0/src/gratinglab/metrology/io/file_io.py +253 -0
  73. gratinglab-0.1.0/src/gratinglab/metrology/io/metrics.py +38 -0
  74. gratinglab-0.1.0/src/gratinglab/metrology/io/spm.py +210 -0
  75. gratinglab-0.1.0/src/gratinglab/metrology/settings.py +216 -0
  76. gratinglab-0.1.0/src/gratinglab/metrology/stats/__init__.py +16 -0
  77. gratinglab-0.1.0/src/gratinglab/metrology/stats/analysis.py +329 -0
  78. gratinglab-0.1.0/src/gratinglab/metrology/stats/icc.py +128 -0
  79. gratinglab-0.1.0/src/gratinglab/metrology/visualization/__init__.py +16 -0
  80. gratinglab-0.1.0/src/gratinglab/metrology/visualization/diagnostics.py +111 -0
  81. gratinglab-0.1.0/src/gratinglab/metrology/visualization/profiles.py +234 -0
  82. gratinglab-0.1.0/src/gratinglab/metrology/visualization/statistics.py +412 -0
  83. gratinglab-0.1.0/src/gratinglab/metrology/wiki/__init__.py +91 -0
  84. gratinglab-0.1.0/src/gratinglab/metrology/wiki/facet-fitting.md +95 -0
  85. gratinglab-0.1.0/src/gratinglab/metrology/wiki/file-formats.md +116 -0
  86. gratinglab-0.1.0/src/gratinglab/metrology/wiki/flattening.md +105 -0
  87. gratinglab-0.1.0/src/gratinglab/metrology/wiki/icc-correction.md +111 -0
  88. gratinglab-0.1.0/src/gratinglab/metrology/wiki/overview.md +51 -0
  89. gratinglab-0.1.0/src/gratinglab/metrology/wiki/reading-outputs.md +154 -0
  90. gratinglab-0.1.0/src/gratinglab/metrology/wiki/row-groups.md +75 -0
  91. gratinglab-0.1.0/src/gratinglab/metrology/wiki/tip-correction.md +101 -0
  92. gratinglab-0.1.0/src/gratinglab/metrology/workflows.py +383 -0
  93. gratinglab-0.1.0/src/gratinglab/problem.py +79 -0
  94. gratinglab-0.1.0/src/gratinglab/profiles.py +531 -0
  95. gratinglab-0.1.0/src/gratinglab/resolution.py +198 -0
  96. gratinglab-0.1.0/src/gratinglab/result.py +268 -0
  97. gratinglab-0.1.0/src/gratinglab/solvers/__init__.py +34 -0
  98. gratinglab-0.1.0/src/gratinglab/solvers/base.py +207 -0
  99. gratinglab-0.1.0/src/gratinglab/solvers/integral/__init__.py +406 -0
  100. gratinglab-0.1.0/src/gratinglab/solvers/integral/_boundary.py +50 -0
  101. gratinglab-0.1.0/src/gratinglab/solvers/integral/_core.py +126 -0
  102. gratinglab-0.1.0/src/gratinglab/solvers/integral/_finite.py +396 -0
  103. gratinglab-0.1.0/src/gratinglab/solvers/integral/_greens.py +234 -0
  104. gratinglab-0.1.0/src/gratinglab/solvers/integral/_kernels.py +308 -0
  105. gratinglab-0.1.0/src/gratinglab/solvers/integral/_nystrom.py +474 -0
  106. gratinglab-0.1.0/src/gratinglab/solvers/scalar.py +1144 -0
  107. gratinglab-0.1.0/src/gratinglab.egg-info/PKG-INFO +319 -0
  108. gratinglab-0.1.0/src/gratinglab.egg-info/SOURCES.txt +146 -0
  109. gratinglab-0.1.0/src/gratinglab.egg-info/dependency_links.txt +1 -0
  110. gratinglab-0.1.0/src/gratinglab.egg-info/entry_points.txt +4 -0
  111. gratinglab-0.1.0/src/gratinglab.egg-info/requires.txt +25 -0
  112. gratinglab-0.1.0/src/gratinglab.egg-info/top_level.txt +1 -0
  113. gratinglab-0.1.0/tests/test_checks.py +563 -0
  114. gratinglab-0.1.0/tests/test_compare.py +353 -0
  115. gratinglab-0.1.0/tests/test_convergence.py +456 -0
  116. gratinglab-0.1.0/tests/test_cross_method.py +201 -0
  117. gratinglab-0.1.0/tests/test_efficiency_table.py +275 -0
  118. gratinglab-0.1.0/tests/test_fresnel.py +381 -0
  119. gratinglab-0.1.0/tests/test_geometry.py +563 -0
  120. gratinglab-0.1.0/tests/test_ggp.py +212 -0
  121. gratinglab-0.1.0/tests/test_gui_diagram.py +430 -0
  122. gratinglab-0.1.0/tests/test_gui_diagram3d.py +411 -0
  123. gratinglab-0.1.0/tests/test_gui_docs.py +144 -0
  124. gratinglab-0.1.0/tests/test_gui_integral_options.py +242 -0
  125. gratinglab-0.1.0/tests/test_gui_mathtext.py +160 -0
  126. gratinglab-0.1.0/tests/test_gui_orders.py +214 -0
  127. gratinglab-0.1.0/tests/test_gui_provenance.py +423 -0
  128. gratinglab-0.1.0/tests/test_gui_qt.py +1534 -0
  129. gratinglab-0.1.0/tests/test_gui_richtext.py +449 -0
  130. gratinglab-0.1.0/tests/test_gui_scalar_options.py +168 -0
  131. gratinglab-0.1.0/tests/test_gui_state.py +295 -0
  132. gratinglab-0.1.0/tests/test_illumination.py +202 -0
  133. gratinglab-0.1.0/tests/test_integral_core.py +190 -0
  134. gratinglab-0.1.0/tests/test_integral_corpus.py +112 -0
  135. gratinglab-0.1.0/tests/test_integral_finite.py +447 -0
  136. gratinglab-0.1.0/tests/test_integral_greens.py +243 -0
  137. gratinglab-0.1.0/tests/test_integral_kernels.py +176 -0
  138. gratinglab-0.1.0/tests/test_integral_operators.py +239 -0
  139. gratinglab-0.1.0/tests/test_integral_solver.py +398 -0
  140. gratinglab-0.1.0/tests/test_materials.py +404 -0
  141. gratinglab-0.1.0/tests/test_perturbation.py +305 -0
  142. gratinglab-0.1.0/tests/test_profiles.py +362 -0
  143. gratinglab-0.1.0/tests/test_refractiveindex.py +269 -0
  144. gratinglab-0.1.0/tests/test_resolution.py +229 -0
  145. gratinglab-0.1.0/tests/test_result.py +205 -0
  146. gratinglab-0.1.0/tests/test_scalar.py +1451 -0
  147. gratinglab-0.1.0/tests/test_solver_conformance.py +276 -0
  148. gratinglab-0.1.0/tests/test_solver_progress.py +223 -0
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Jake McCoy
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,319 @@
1
+ Metadata-Version: 2.4
2
+ Name: gratinglab
3
+ Version: 0.1.0
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.1
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.1; 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
+ An open platform for diffraction grating groove metrology and efficiency analysis —
43
+ from an AFM scan of a real grating to its diffraction efficiency, without leaving the
44
+ package.
45
+
46
+ Rigorous grating efficiency analysis today means either paying for PCGrate (integral
47
+ method) or assembling a patchwork of open RCWA codes that each carry their own
48
+ conventions, normalizations, and blind spots. Nothing open lets you run the *same*
49
+ physical problem through several methods and compare honestly — and nothing open
50
+ connects a *measured* surface to any of them.
51
+
52
+ `gratinglab` is built around one idea: **the physical problem and the numerical method
53
+ are separate, and the problem spec is serializable.** One `Problem` goes to every solver,
54
+ and the disagreement between methods is made visible rather than hidden.
55
+
56
+ ```
57
+ AFM scan -> BoundaryProfile (gratinglab.metrology)
58
+ | measured period, averaged groove, facet fit
59
+ v
60
+ Problem (period + profile + materials — no solver fields, ever)
61
+ Illumination (wavelength + direction + polarization)
62
+ |
63
+ v
64
+ Solver (plugin) -> Result (+ provenance)
65
+ ```
66
+
67
+ The two halves are one package because the seam between them loses information when
68
+ it is a file. A PCGrate `.ggp` boundary carries a *shape* and not a grating: it cannot
69
+ hold the period, so the period gets retyped, guessed, or recovered from a sidecar
70
+ later. `BoundaryProfile.to_problem()` carries the period the scan measured, and the
71
+ fitted facet angle with it.
72
+
73
+ ```python
74
+ from gratinglab.metrology.boundary import build_boundary_profile
75
+
76
+ profile = build_boundary_profile(data, scan_x_size, settings)
77
+ problem = profile.to_problem(coating="Au", blaze_angle=29.5)
78
+ ```
79
+
80
+ ## Status
81
+
82
+ Pre-alpha. The scalar and integral solvers, the comparison harness, the convergence
83
+ harness and the materials layer work end to end — efficiencies are absolute when a
84
+ coating is named, from an optical constant table of your own (see Install). RCWA is
85
+ contributed and not yet integrated.
86
+
87
+ ## Scope
88
+
89
+ Two halves. **Metrology** (`gratinglab.metrology`) reads AFM scans — Nanoscope `.spm`
90
+ or Gwyddion text exports — and produces blaze angles per groove with row-group
91
+ statistics and an ICC correction, plus one averaged groove normalised to a period.
92
+ Tip convolution can be undone where the tip actually reached, with the unrecoverable
93
+ region marked rather than smoothed over. **Efficiency** is everything else: the problem
94
+ spec, the solvers, the comparison and convergence harnesses, the materials layer, and
95
+ the resolving power the measured groove count implies.
96
+
97
+ Both mounts are first-class:
98
+
99
+ - **In-plane / classical** — UV–Vis–NIR optical engineering.
100
+ - **Extreme off-plane (conical), grazing incidence** — soft X-ray spectroscopy, where
101
+ PCGrate is the community standard and general-purpose open RCWA codes are weakest.
102
+
103
+ Planned method backends, in order:
104
+
105
+ | Method | Status |
106
+ |---|---|
107
+ | Scalar (Kirchhoff, X-ray blaze form) | **done**, validated |
108
+ | 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 |
109
+ | RCWA (in-plane, conical, off-plane; Li factorization, S-matrix) | contributed, not yet integrated |
110
+ | C-method (Chandezon) | planned |
111
+
112
+ An already-computed efficiency table from an external code is a first-class "method" in
113
+ the comparison harness, so scalar-vs-RCWA-vs-integral plots are possible without a
114
+ licence for the code that produced the reference data:
115
+
116
+ ```python
117
+ reference = read_scan("run.txt", method="integral")
118
+ align(sweep(problem, illumination, wavelengths, ["scalar", reference]))
119
+ ```
120
+
121
+ The caller declares what physics produced an imported file, because the file itself does
122
+ not say. Comparison legends then read `scalar vs integral` rather than naming a product,
123
+ while `Provenance.version` still records exactly which program and version generated it.
124
+
125
+ ### From a measured surface to a spectrograph number
126
+
127
+ The scan carries its own groove count, so resolving power needs nothing retyped:
128
+
129
+ ```python
130
+ problem = profile.to_problem(coating="Au") # n_grooves = the grooves averaged
131
+ resolving_power(problem, illumination, wavelength=2.0, order=3).r
132
+ ```
133
+
134
+ `R = |m|·N` by the Rayleigh criterion, with `line_profile` giving the finite-*N* line
135
+ shape it is derived from. A problem whose `n_grooves` is unset is **refused** rather
136
+ than treated as infinite: "R is undefined" and "R is very large" are different answers.
137
+ Resolving power degraded by real groove-placement error is named as the successor and
138
+ deliberately not approximated by the ideal formula.
139
+
140
+ ### The tip is not the surface
141
+
142
+ An AFM image is the surface dilated by the tip. `tip_correction='erosion'` inverts that
143
+ where the inversion exists — a cone-plus-spherical-apex tip, Villarrubia (1997) — and
144
+ reports a **certainty map** for where it does not, because the trough a tip never
145
+ entered is not recoverable by any algorithm and a correction that quietly filled it in
146
+ would be worse than no correction.
147
+
148
+ It has already earned its keep: on the one real scan whose depth and facet angle
149
+ disagree by 7.6°, erosion with the nominal probe recovers *nothing*, and reproducing
150
+ the deficit forward takes an apex worn to ~80 nm — so the nominal tip is excluded as
151
+ the explanation
152
+ ([findings](docs/findings.md)).
153
+
154
+ ## Install
155
+
156
+ ```bash
157
+ pip install gratinglab # solvers only: numpy, scipy, pydantic
158
+ pip install "gratinglab[metrology]" # + AFM readers and the boundary pipeline
159
+ pip install "gratinglab[gui]" # + the Qt windows
160
+ ```
161
+
162
+ From a checkout, `pip install -e ".[dev,gui,metrology,refractiveindex]"` instead.
163
+
164
+ No optical constant tables ship with the package, so absolute efficiencies need
165
+ your own. A library is a directory of them, one material per file, named for
166
+ the material:
167
+
168
+ | File | From | Range |
169
+ |---|---|---|
170
+ | `Pt.txt` | a [CXRO](https://henke.lbl.gov/optical_constants/) export | X-ray |
171
+ | `Pt.yml` | a [refractiveindex.info](https://refractiveindex.info) file, renamed from its author's name | ultraviolet to infrared |
172
+ | `Pt.ari` | a PCGrate table | whatever it covers |
173
+
174
+ Save one as `~/.gratinglab/materials/Pt.yml` and `coating="Pt"` works.
175
+ `GRATINGLAB_MATERIALS` points elsewhere, and `materials.from_cxro_file(path)`
176
+ or `from_refractiveindex_file(path)` reads a file without a library. The YAML
177
+ files need `pip install "gratinglab[refractiveindex]"`. See
178
+ [`SOURCES.md`](src/gratinglab/materials/data/SOURCES.md) for why none ship.
179
+
180
+ The core stays deliberately light — a solver-only user never installs a plotting
181
+ stack — and each subpackage says which extra it needs rather than failing with a bare
182
+ `ModuleNotFoundError`.
183
+
184
+ ### Where the measurement data lives
185
+
186
+ Scans are **not** in the repository. They are research data, and the same rule already
187
+ applies to the PCGrate reference corpus:
188
+
189
+ | Data | Environment variable | Default |
190
+ |---|---|---|
191
+ | AFM scans | `GRATINGLAB_AFM_DIR` | `~/Documents/afm_scans` |
192
+ | PCGrate reference corpus | `GRATINGLAB_REF_DIR` | `~/Documents/diffraction_efficiency` |
193
+
194
+ Tests that need either **skip cleanly** when it is absent, so the suite is green on a
195
+ fresh clone. Most of the metrology suite does not need it at all: it runs against a
196
+ committed synthetic scan of a *known* grating
197
+ (`tools/metrology/make_synthetic_scan.py`), which is also the control that lets a test
198
+ assert the pipeline recovers the period and blaze angle it was given.
199
+
200
+ ## GUI
201
+
202
+ ```bash
203
+ .venv/bin/python -m pip install -e ".[gui,metrology]"
204
+ gratinglab-gui # geometry, solvers, comparison
205
+ gratinglab-metrology-gui # AFM import, blaze angles, boundary profiles
206
+ ```
207
+
208
+ Qt (PySide6), so `pip install "gratinglab[gui]"` is self-sufficient — no system toolkit
209
+ to install first. PySide6 is LGPLv3 and an optional, dynamically-linked extra, which
210
+ leaves this package BSD-3.
211
+
212
+ Grating geometry lives in a dock on the left (Ctrl+G closes it and hands the width back),
213
+ and the tabs are one per concern:
214
+
215
+ - **Grating Geometry** — a rotatable 3D view of the diffraction cone, with every wave
216
+ vector drawn as an exact unit vector. Because the orders separate in *azimuth* rather
217
+ than polar angle — 93.8° of fan at 0.0° of polar spread for the reference off-plane
218
+ geometry — the "down the cone axis" preset plus a uniform 27× zoom makes them readable
219
+ at true scale. γ is never exaggerated: uniform zoom is a similarity transform, so every
220
+ angle in the view survives it exactly, and the magnification is stated on the canvas.
221
+ Beside it, the dispersion-plane cross-section, a `sin β` ladder carrying evanescent
222
+ orders too, and the groove profile. The picture follows the form as you type.
223
+ - **Scalar (Kirchhoff)** — the solve, efficiency per order with per-order visibility, and
224
+ the **provenance panel**: every validity warning verbatim, the summed-efficiency check,
225
+ and whether convergence was actually demonstrated. Nothing is hidden there; a result
226
+ that violates energy conservation says so. **Check convergence…** runs the sweep and
227
+ reports the coarsest defensible setting, with a progress bar and a Cancel that really
228
+ stops it — solvers report per wavelength, so cancellation is not a euphemism for
229
+ looking away.
230
+ - **Integral method** — the rigorous backend, with the same panel and the same
231
+ provenance discipline, plus its own boundary condition control (perfect conductivity
232
+ or tabulated optical constants). It is minutes where scalar is milliseconds, so the
233
+ window opens on the fast solver and this tab solves when you ask it to.
234
+ - **Setup** — the material library: which optical constants are installed, what range
235
+ each covers, where they came from, and how to add your own. The coating *control*
236
+ lives in the geometry dock, because it is a `Problem` field like the period and one
237
+ `Problem` feeds every tab.
238
+
239
+ A tab per registered solver is generated from the registry, so a contributed backend
240
+ gets its panel by registering rather than by editing the window.
241
+
242
+ Help carries the theory pages, rendered with typeset math and real tables.
243
+
244
+ A macOS `.app` can be built with `python tools/make_app.py` (macOS 13+, which is the
245
+ PySide6 wheel's own floor); the icon is regenerated by `python tools/make_icon.py`.
246
+
247
+ ## Documentation
248
+
249
+ | Document | What it holds |
250
+ |---|---|
251
+ | [`docs/roadmap.md`](docs/roadmap.md) | **Where the project is, what is next, and the open questions.** Start here |
252
+ | [`docs/conventions.md`](docs/conventions.md) | **Normative.** Signs, units, normalizations, and the reference errata |
253
+ | [`docs/theory/scalar.md`](docs/theory/scalar.md) | Full derivation for the scalar solver, its closed forms and its limits |
254
+ | [`docs/theory/integral.md`](docs/theory/integral.md) | The boundary-integral solver: conical formulation, perfect and finite conductivity |
255
+ | [`docs/theory/metrology.md`](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 |
256
+ | [`docs/findings.md`](docs/findings.md) | Empirical results, each with the evidence that established it |
257
+ | [`docs/references.md`](docs/references.md) | The literature each part is built from, mapped to the part it serves |
258
+ | [`docs/mutation-testing.md`](docs/mutation-testing.md) | What is mutated, what is not, and how to read a survivor |
259
+ | [`benchmarks/corpus.toml`](benchmarks/corpus.toml) | Geometry for the reference corpus, most of it recovered from the data |
260
+
261
+ `conventions.md` is worth singling out: read it before writing a solver or filing a bug
262
+ about a sign. The short version is time convention `exp(-iωt)`, lossy *n* = n′ + ik,
263
+ grating equation `sin α + sin β_m = mλ/(p sin γ)`, efficiencies **absolute**, lengths in
264
+ nm, angles in degrees at the API boundary.
265
+
266
+ A theory page accompanies each solver as it lands, stating its assumptions, its
267
+ derivation in the code's own notation, and the conditions under which it stops being
268
+ trustworthy.
269
+
270
+ The [wiki](https://github.com/jamccoy/gratinglab/wiki) is a published copy of `docs/`,
271
+ not a second source — edit `docs/` and republish, or the copy people read becomes the
272
+ one nobody edits:
273
+
274
+ ```bash
275
+ git clone https://github.com/jamccoy/gratinglab.wiki.git ../gratinglab.wiki
276
+ .venv/bin/python tools/publish_wiki.py --wiki ../gratinglab.wiki # then review and push
277
+ ```
278
+
279
+ ## Development
280
+
281
+ ```bash
282
+ .venv/bin/python -m pip install -e ".[dev,gui,metrology,refractiveindex]"
283
+ .venv/bin/python -m pytest
284
+ .venv/bin/ruff check .
285
+ ```
286
+
287
+ Three practices this project relies on:
288
+
289
+ - **Physics self-checks over reference data.** `checks.check_reciprocity` and
290
+ `checks.check_energy_balance` constrain the *model* rather than comparing against a
291
+ formula derived the same way the solver computes it. They need no reference data and
292
+ apply to every backend.
293
+ - **Convergence demonstrated, not assumed.** `convergence.check_convergence` sweeps
294
+ whatever a solver declares as its accuracy knob and reports the coarsest setting it
295
+ can defend — or reports that it found none, which is a result rather than a missing
296
+ one. It requires *three* consecutive values to agree, because quadrature error is
297
+ measurably non-monotone for a blazed profile and one agreement can be an accident
298
+ the next refinement contradicts
299
+ ([findings](docs/findings.md#quadrature-error-is-not-monotone-in-the-number-of-points)).
300
+
301
+ ```python
302
+ report = check_convergence(scalar, problem, illumination, wavelengths)
303
+ report.converged_at # 4096 -- what a production run should use
304
+ report.scan.provenance.converged # True, at last
305
+ ```
306
+ - **Mutation testing.** Deliberately corrupting the physics and confirming the suite
307
+ notices — coverage says a line ran, this asks whether anything would have noticed if
308
+ it were wrong. It has found real gaps both times it was run; see
309
+ [findings](docs/findings.md#mutation-testing-found-a-real-gap) and
310
+ [docs/mutation-testing.md](docs/mutation-testing.md) for how to run it.
311
+
312
+ ```bash
313
+ .venv/bin/python -m pip install -e ".[mut]"
314
+ .venv/bin/mutmut run
315
+ ```
316
+
317
+ ## License
318
+
319
+ BSD-3-Clause.
@@ -0,0 +1,280 @@
1
+ # gratinglab
2
+
3
+ An open platform for diffraction grating groove metrology and efficiency analysis —
4
+ from an AFM scan of a real grating to its diffraction efficiency, without leaving the
5
+ package.
6
+
7
+ Rigorous grating efficiency analysis today means either paying for PCGrate (integral
8
+ method) or assembling a patchwork of open RCWA codes that each carry their own
9
+ conventions, normalizations, and blind spots. Nothing open lets you run the *same*
10
+ physical problem through several methods and compare honestly — and nothing open
11
+ connects a *measured* surface to any of them.
12
+
13
+ `gratinglab` is built around one idea: **the physical problem and the numerical method
14
+ are separate, and the problem spec is serializable.** One `Problem` goes to every solver,
15
+ and the disagreement between methods is made visible rather than hidden.
16
+
17
+ ```
18
+ AFM scan -> BoundaryProfile (gratinglab.metrology)
19
+ | measured period, averaged groove, facet fit
20
+ v
21
+ Problem (period + profile + materials — no solver fields, ever)
22
+ Illumination (wavelength + direction + polarization)
23
+ |
24
+ v
25
+ Solver (plugin) -> Result (+ provenance)
26
+ ```
27
+
28
+ The two halves are one package because the seam between them loses information when
29
+ it is a file. A PCGrate `.ggp` boundary carries a *shape* and not a grating: it cannot
30
+ hold the period, so the period gets retyped, guessed, or recovered from a sidecar
31
+ later. `BoundaryProfile.to_problem()` carries the period the scan measured, and the
32
+ fitted facet angle with it.
33
+
34
+ ```python
35
+ from gratinglab.metrology.boundary import build_boundary_profile
36
+
37
+ profile = build_boundary_profile(data, scan_x_size, settings)
38
+ problem = profile.to_problem(coating="Au", blaze_angle=29.5)
39
+ ```
40
+
41
+ ## Status
42
+
43
+ Pre-alpha. The scalar and integral solvers, the comparison harness, the convergence
44
+ harness and the materials layer work end to end — efficiencies are absolute when a
45
+ coating is named, from an optical constant table of your own (see Install). RCWA is
46
+ contributed and not yet integrated.
47
+
48
+ ## Scope
49
+
50
+ Two halves. **Metrology** (`gratinglab.metrology`) reads AFM scans — Nanoscope `.spm`
51
+ or Gwyddion text exports — and produces blaze angles per groove with row-group
52
+ statistics and an ICC correction, plus one averaged groove normalised to a period.
53
+ Tip convolution can be undone where the tip actually reached, with the unrecoverable
54
+ region marked rather than smoothed over. **Efficiency** is everything else: the problem
55
+ spec, the solvers, the comparison and convergence harnesses, the materials layer, and
56
+ the resolving power the measured groove count implies.
57
+
58
+ Both mounts are first-class:
59
+
60
+ - **In-plane / classical** — UV–Vis–NIR optical engineering.
61
+ - **Extreme off-plane (conical), grazing incidence** — soft X-ray spectroscopy, where
62
+ PCGrate is the community standard and general-purpose open RCWA codes are weakest.
63
+
64
+ Planned method backends, in order:
65
+
66
+ | Method | Status |
67
+ |---|---|
68
+ | Scalar (Kirchhoff, X-ray blaze form) | **done**, validated |
69
+ | 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 |
70
+ | RCWA (in-plane, conical, off-plane; Li factorization, S-matrix) | contributed, not yet integrated |
71
+ | C-method (Chandezon) | planned |
72
+
73
+ An already-computed efficiency table from an external code is a first-class "method" in
74
+ the comparison harness, so scalar-vs-RCWA-vs-integral plots are possible without a
75
+ licence for the code that produced the reference data:
76
+
77
+ ```python
78
+ reference = read_scan("run.txt", method="integral")
79
+ align(sweep(problem, illumination, wavelengths, ["scalar", reference]))
80
+ ```
81
+
82
+ The caller declares what physics produced an imported file, because the file itself does
83
+ not say. Comparison legends then read `scalar vs integral` rather than naming a product,
84
+ while `Provenance.version` still records exactly which program and version generated it.
85
+
86
+ ### From a measured surface to a spectrograph number
87
+
88
+ The scan carries its own groove count, so resolving power needs nothing retyped:
89
+
90
+ ```python
91
+ problem = profile.to_problem(coating="Au") # n_grooves = the grooves averaged
92
+ resolving_power(problem, illumination, wavelength=2.0, order=3).r
93
+ ```
94
+
95
+ `R = |m|·N` by the Rayleigh criterion, with `line_profile` giving the finite-*N* line
96
+ shape it is derived from. A problem whose `n_grooves` is unset is **refused** rather
97
+ than treated as infinite: "R is undefined" and "R is very large" are different answers.
98
+ Resolving power degraded by real groove-placement error is named as the successor and
99
+ deliberately not approximated by the ideal formula.
100
+
101
+ ### The tip is not the surface
102
+
103
+ An AFM image is the surface dilated by the tip. `tip_correction='erosion'` inverts that
104
+ where the inversion exists — a cone-plus-spherical-apex tip, Villarrubia (1997) — and
105
+ reports a **certainty map** for where it does not, because the trough a tip never
106
+ entered is not recoverable by any algorithm and a correction that quietly filled it in
107
+ would be worse than no correction.
108
+
109
+ It has already earned its keep: on the one real scan whose depth and facet angle
110
+ disagree by 7.6°, erosion with the nominal probe recovers *nothing*, and reproducing
111
+ the deficit forward takes an apex worn to ~80 nm — so the nominal tip is excluded as
112
+ the explanation
113
+ ([findings](docs/findings.md)).
114
+
115
+ ## Install
116
+
117
+ ```bash
118
+ pip install gratinglab # solvers only: numpy, scipy, pydantic
119
+ pip install "gratinglab[metrology]" # + AFM readers and the boundary pipeline
120
+ pip install "gratinglab[gui]" # + the Qt windows
121
+ ```
122
+
123
+ From a checkout, `pip install -e ".[dev,gui,metrology,refractiveindex]"` instead.
124
+
125
+ No optical constant tables ship with the package, so absolute efficiencies need
126
+ your own. A library is a directory of them, one material per file, named for
127
+ the material:
128
+
129
+ | File | From | Range |
130
+ |---|---|---|
131
+ | `Pt.txt` | a [CXRO](https://henke.lbl.gov/optical_constants/) export | X-ray |
132
+ | `Pt.yml` | a [refractiveindex.info](https://refractiveindex.info) file, renamed from its author's name | ultraviolet to infrared |
133
+ | `Pt.ari` | a PCGrate table | whatever it covers |
134
+
135
+ Save one as `~/.gratinglab/materials/Pt.yml` and `coating="Pt"` works.
136
+ `GRATINGLAB_MATERIALS` points elsewhere, and `materials.from_cxro_file(path)`
137
+ or `from_refractiveindex_file(path)` reads a file without a library. The YAML
138
+ files need `pip install "gratinglab[refractiveindex]"`. See
139
+ [`SOURCES.md`](src/gratinglab/materials/data/SOURCES.md) for why none ship.
140
+
141
+ The core stays deliberately light — a solver-only user never installs a plotting
142
+ stack — and each subpackage says which extra it needs rather than failing with a bare
143
+ `ModuleNotFoundError`.
144
+
145
+ ### Where the measurement data lives
146
+
147
+ Scans are **not** in the repository. They are research data, and the same rule already
148
+ applies to the PCGrate reference corpus:
149
+
150
+ | Data | Environment variable | Default |
151
+ |---|---|---|
152
+ | AFM scans | `GRATINGLAB_AFM_DIR` | `~/Documents/afm_scans` |
153
+ | PCGrate reference corpus | `GRATINGLAB_REF_DIR` | `~/Documents/diffraction_efficiency` |
154
+
155
+ Tests that need either **skip cleanly** when it is absent, so the suite is green on a
156
+ fresh clone. Most of the metrology suite does not need it at all: it runs against a
157
+ committed synthetic scan of a *known* grating
158
+ (`tools/metrology/make_synthetic_scan.py`), which is also the control that lets a test
159
+ assert the pipeline recovers the period and blaze angle it was given.
160
+
161
+ ## GUI
162
+
163
+ ```bash
164
+ .venv/bin/python -m pip install -e ".[gui,metrology]"
165
+ gratinglab-gui # geometry, solvers, comparison
166
+ gratinglab-metrology-gui # AFM import, blaze angles, boundary profiles
167
+ ```
168
+
169
+ Qt (PySide6), so `pip install "gratinglab[gui]"` is self-sufficient — no system toolkit
170
+ to install first. PySide6 is LGPLv3 and an optional, dynamically-linked extra, which
171
+ leaves this package BSD-3.
172
+
173
+ Grating geometry lives in a dock on the left (Ctrl+G closes it and hands the width back),
174
+ and the tabs are one per concern:
175
+
176
+ - **Grating Geometry** — a rotatable 3D view of the diffraction cone, with every wave
177
+ vector drawn as an exact unit vector. Because the orders separate in *azimuth* rather
178
+ than polar angle — 93.8° of fan at 0.0° of polar spread for the reference off-plane
179
+ geometry — the "down the cone axis" preset plus a uniform 27× zoom makes them readable
180
+ at true scale. γ is never exaggerated: uniform zoom is a similarity transform, so every
181
+ angle in the view survives it exactly, and the magnification is stated on the canvas.
182
+ Beside it, the dispersion-plane cross-section, a `sin β` ladder carrying evanescent
183
+ orders too, and the groove profile. The picture follows the form as you type.
184
+ - **Scalar (Kirchhoff)** — the solve, efficiency per order with per-order visibility, and
185
+ the **provenance panel**: every validity warning verbatim, the summed-efficiency check,
186
+ and whether convergence was actually demonstrated. Nothing is hidden there; a result
187
+ that violates energy conservation says so. **Check convergence…** runs the sweep and
188
+ reports the coarsest defensible setting, with a progress bar and a Cancel that really
189
+ stops it — solvers report per wavelength, so cancellation is not a euphemism for
190
+ looking away.
191
+ - **Integral method** — the rigorous backend, with the same panel and the same
192
+ provenance discipline, plus its own boundary condition control (perfect conductivity
193
+ or tabulated optical constants). It is minutes where scalar is milliseconds, so the
194
+ window opens on the fast solver and this tab solves when you ask it to.
195
+ - **Setup** — the material library: which optical constants are installed, what range
196
+ each covers, where they came from, and how to add your own. The coating *control*
197
+ lives in the geometry dock, because it is a `Problem` field like the period and one
198
+ `Problem` feeds every tab.
199
+
200
+ A tab per registered solver is generated from the registry, so a contributed backend
201
+ gets its panel by registering rather than by editing the window.
202
+
203
+ Help carries the theory pages, rendered with typeset math and real tables.
204
+
205
+ A macOS `.app` can be built with `python tools/make_app.py` (macOS 13+, which is the
206
+ PySide6 wheel's own floor); the icon is regenerated by `python tools/make_icon.py`.
207
+
208
+ ## Documentation
209
+
210
+ | Document | What it holds |
211
+ |---|---|
212
+ | [`docs/roadmap.md`](docs/roadmap.md) | **Where the project is, what is next, and the open questions.** Start here |
213
+ | [`docs/conventions.md`](docs/conventions.md) | **Normative.** Signs, units, normalizations, and the reference errata |
214
+ | [`docs/theory/scalar.md`](docs/theory/scalar.md) | Full derivation for the scalar solver, its closed forms and its limits |
215
+ | [`docs/theory/integral.md`](docs/theory/integral.md) | The boundary-integral solver: conical formulation, perfect and finite conductivity |
216
+ | [`docs/theory/metrology.md`](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 |
217
+ | [`docs/findings.md`](docs/findings.md) | Empirical results, each with the evidence that established it |
218
+ | [`docs/references.md`](docs/references.md) | The literature each part is built from, mapped to the part it serves |
219
+ | [`docs/mutation-testing.md`](docs/mutation-testing.md) | What is mutated, what is not, and how to read a survivor |
220
+ | [`benchmarks/corpus.toml`](benchmarks/corpus.toml) | Geometry for the reference corpus, most of it recovered from the data |
221
+
222
+ `conventions.md` is worth singling out: read it before writing a solver or filing a bug
223
+ about a sign. The short version is time convention `exp(-iωt)`, lossy *n* = n′ + ik,
224
+ grating equation `sin α + sin β_m = mλ/(p sin γ)`, efficiencies **absolute**, lengths in
225
+ nm, angles in degrees at the API boundary.
226
+
227
+ A theory page accompanies each solver as it lands, stating its assumptions, its
228
+ derivation in the code's own notation, and the conditions under which it stops being
229
+ trustworthy.
230
+
231
+ The [wiki](https://github.com/jamccoy/gratinglab/wiki) is a published copy of `docs/`,
232
+ not a second source — edit `docs/` and republish, or the copy people read becomes the
233
+ one nobody edits:
234
+
235
+ ```bash
236
+ git clone https://github.com/jamccoy/gratinglab.wiki.git ../gratinglab.wiki
237
+ .venv/bin/python tools/publish_wiki.py --wiki ../gratinglab.wiki # then review and push
238
+ ```
239
+
240
+ ## Development
241
+
242
+ ```bash
243
+ .venv/bin/python -m pip install -e ".[dev,gui,metrology,refractiveindex]"
244
+ .venv/bin/python -m pytest
245
+ .venv/bin/ruff check .
246
+ ```
247
+
248
+ Three practices this project relies on:
249
+
250
+ - **Physics self-checks over reference data.** `checks.check_reciprocity` and
251
+ `checks.check_energy_balance` constrain the *model* rather than comparing against a
252
+ formula derived the same way the solver computes it. They need no reference data and
253
+ apply to every backend.
254
+ - **Convergence demonstrated, not assumed.** `convergence.check_convergence` sweeps
255
+ whatever a solver declares as its accuracy knob and reports the coarsest setting it
256
+ can defend — or reports that it found none, which is a result rather than a missing
257
+ one. It requires *three* consecutive values to agree, because quadrature error is
258
+ measurably non-monotone for a blazed profile and one agreement can be an accident
259
+ the next refinement contradicts
260
+ ([findings](docs/findings.md#quadrature-error-is-not-monotone-in-the-number-of-points)).
261
+
262
+ ```python
263
+ report = check_convergence(scalar, problem, illumination, wavelengths)
264
+ report.converged_at # 4096 -- what a production run should use
265
+ report.scan.provenance.converged # True, at last
266
+ ```
267
+ - **Mutation testing.** Deliberately corrupting the physics and confirming the suite
268
+ notices — coverage says a line ran, this asks whether anything would have noticed if
269
+ it were wrong. It has found real gaps both times it was run; see
270
+ [findings](docs/findings.md#mutation-testing-found-a-real-gap) and
271
+ [docs/mutation-testing.md](docs/mutation-testing.md) for how to run it.
272
+
273
+ ```bash
274
+ .venv/bin/python -m pip install -e ".[mut]"
275
+ .venv/bin/mutmut run
276
+ ```
277
+
278
+ ## License
279
+
280
+ BSD-3-Clause.