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.
- gratinglab-0.1.2/PKG-INFO +342 -0
- gratinglab-0.1.2/README.md +303 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/pyproject.toml +11 -5
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/__init__.py +1 -1
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/compare.py +1 -1
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/convergence.py +5 -5
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/geometry.py +7 -7
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/app.py +1 -1
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/diagram.py +6 -6
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/diagram3d.py +5 -5
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/icon.py +1 -1
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/integral_options.py +1 -1
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/mathtext.py +2 -2
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/orders.py +1 -1
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/provenance.py +8 -8
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/qt/geometry_panel.py +3 -3
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/qt/geometry_tab.py +4 -4
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/qt/integral_tab.py +1 -1
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/qt/main_window.py +3 -3
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/qt/profile_plot_panel.py +2 -2
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/qt/scalar_tab.py +1 -1
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/qt/scene3d_panel.py +2 -2
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/qt/setup_tab.py +6 -6
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/qt/worker.py +5 -5
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/richtext.py +8 -8
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/state.py +2 -2
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/illumination.py +2 -2
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/io/efficiency_table.py +2 -2
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/io/ggp.py +1 -1
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/materials/__init__.py +2 -2
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/materials/data/SOURCES.md +1 -1
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/materials/fresnel.py +19 -9
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/materials/optical.py +68 -11
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/analyzer.py +1 -1
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/boundary/average.py +1 -1
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/boundary/pipeline.py +4 -4
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/core/image_flatten.py +1 -1
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/core/tip.py +2 -2
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/gui/__init__.py +1 -1
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/gui/icon.py +2 -2
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/gui/qt/boundary_view.py +4 -4
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/gui/qt/canvas.py +6 -6
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/gui/qt/efficiency_dialog.py +2 -2
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/gui/qt/import_view.py +2 -2
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/gui/qt/worker.py +1 -1
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/gui/state.py +1 -1
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/io/metrics.py +1 -1
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/io/spm.py +3 -3
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/stats/analysis.py +1 -1
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/stats/icc.py +1 -1
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/visualization/diagnostics.py +1 -1
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/wiki/facet-fitting.md +9 -9
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/wiki/file-formats.md +6 -6
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/wiki/flattening.md +7 -7
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/wiki/icc-correction.md +14 -14
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/wiki/overview.md +7 -7
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/wiki/reading-outputs.md +16 -16
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/wiki/row-groups.md +7 -7
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/wiki/tip-correction.md +10 -10
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/workflows.py +2 -2
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/profiles.py +3 -3
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/resolution.py +2 -2
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/result.py +2 -2
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/solvers/base.py +2 -2
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/solvers/scalar.py +46 -28
- gratinglab-0.1.2/src/gratinglab.egg-info/PKG-INFO +342 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab.egg-info/requires.txt +2 -2
- {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_fresnel.py +25 -4
- {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_materials.py +90 -5
- {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_result.py +25 -7
- {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_scalar.py +52 -0
- gratinglab-0.1.0/PKG-INFO +0 -319
- gratinglab-0.1.0/README.md +0 -280
- gratinglab-0.1.0/src/gratinglab.egg-info/PKG-INFO +0 -319
- {gratinglab-0.1.0 → gratinglab-0.1.2}/LICENSE +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/setup.cfg +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/checks.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/__init__.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/docs.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/qt/__init__.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/qt/sizing.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/qt/theory_viewer.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/gui/scalar_options.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/io/__init__.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/materials/refractiveindex.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/__init__.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/boundary/__init__.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/cli.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/config.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/core/__init__.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/core/analysis.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/core/processing.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/data/__init__.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/data/aggregation.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/gui/qt/__init__.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/gui/qt/app.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/gui/qt/main_window.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/gui/qt/wiki_view.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/io/__init__.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/io/file_io.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/settings.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/stats/__init__.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/visualization/__init__.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/visualization/profiles.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/visualization/statistics.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/metrology/wiki/__init__.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/problem.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/solvers/__init__.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/solvers/integral/__init__.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/solvers/integral/_boundary.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/solvers/integral/_core.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/solvers/integral/_finite.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/solvers/integral/_greens.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/solvers/integral/_kernels.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab/solvers/integral/_nystrom.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab.egg-info/SOURCES.txt +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab.egg-info/dependency_links.txt +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab.egg-info/entry_points.txt +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/src/gratinglab.egg-info/top_level.txt +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_checks.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_compare.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_convergence.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_cross_method.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_efficiency_table.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_geometry.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_ggp.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_gui_diagram.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_gui_diagram3d.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_gui_docs.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_gui_integral_options.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_gui_mathtext.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_gui_orders.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_gui_provenance.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_gui_qt.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_gui_richtext.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_gui_scalar_options.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_gui_state.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_illumination.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_integral_core.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_integral_corpus.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_integral_finite.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_integral_greens.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_integral_kernels.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_integral_operators.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_integral_solver.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_perturbation.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_profiles.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_refractiveindex.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_resolution.py +0 -0
- {gratinglab-0.1.0 → gratinglab-0.1.2}/tests/test_solver_conformance.py +0 -0
- {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
|
+
[](https://pypi.org/project/gratinglab/)
|
|
43
|
+
[](https://pypi.org/project/gratinglab/)
|
|
44
|
+
[](https://github.com/jamccoy/gratinglab/actions/workflows/test.yml)
|
|
45
|
+
[](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
|
+
[](https://pypi.org/project/gratinglab/)
|
|
4
|
+
[](https://pypi.org/project/gratinglab/)
|
|
5
|
+
[](https://github.com/jamccoy/gratinglab/actions/workflows/test.yml)
|
|
6
|
+
[](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.
|