Skip to main content

gratinglab

PyPI Python tests License: BSD-3-Clause

PyPI · Documentation · Changelog · Releases · Issues

gratinglab is a Python package for measuring the groove profiles of diffraction gratings from atomic force microscope (AFM) scans and for computing the diffraction efficiency of a grating from such a profile.

The AFM metrology is the part of the package in regular use. It measures blaze angles from a scan, averages the grooves into a single profile, reports how much that profile varies down the scan, corrects for the shape of the AFM tip where the data allow it, and exports the averaged groove as a boundary profile. The efficiency solvers are experimental: they pass their test suite but have not yet been used for real analysis, and their results should be treated as provisional.

Rigorous methods for computing grating efficiency are well established, and open codes exist for several of them, although each carries its own conventions and normalizations. gratinglab is intended to fill two gaps that remain: running the same physical problem through several methods so that their results can be compared directly, and starting that problem from a measured surface rather than an idealised one. To this end, the physical problem and the numerical method are kept separate. A Problem describes the grating and can be saved to a file, and the same Problem is passed to every solver, so any difference between their results is attributable to the methods themselves.

AFM scan  ->  BoundaryProfile   (gratinglab.metrology)
                      |          measured period, averaged groove, facet fit
                      v
Problem       (period + profile + materials; no solver fields)
Illumination  (wavelength + direction + polarization)
                      |
                      v
Solver (plugin)  ->  Result (+ provenance)

The metrology and the solvers are distributed as one package so that no information is lost between them. A plain boundary file records the groove shape but not the grating period, which must then be entered again by hand. BoundaryProfile.to_problem() carries the period measured from the scan, together with the fitted facet angle. A profile can be saved with that information intact in gratinglab's own .json format (gratinglab.io.boundary.write_boundary_json); reading it back gives the same problem, and "Load boundary profile" in the efficiency window takes the period from it. The Boundary tab of the metrology window exports .json, .csv, or a .ggp boundary file for PCGrate, and its "Send to efficiency window" button opens the groove in the efficiency window directly.

from gratinglab.metrology.boundary import build_boundary_profile

profile = build_boundary_profile(data, scan_x_size, settings)
problem = profile.to_problem(coating="Au", blaze_angle=29.5)

Status

The project is at a pre-alpha stage. The AFM metrology is the more mature of its two parts. On the efficiency side, the scalar and integral solvers, the comparison and convergence harnesses and the materials layer work end to end. They are checked against closed-form results, published benchmark tables, energy balance and reciprocity, but remain experimental until they have been used on real problems. Efficiencies are absolute when a coating is named, using an optical constant table supplied by the user (see Install). A rigorous coupled-wave analysis (RCWA) backend has been contributed but is not yet integrated.

Scope

The metrology (gratinglab.metrology) reads AFM scans, either Nanoscope .spm files or Gwyddion text exports. It produces blaze angles for each groove with statistics over groups of scan rows and a correction for the correlation between those groups, an averaged groove normalised to one period, and statistics describing how that groove varies between horizontal swaths of the scan. It can also undo the broadening introduced by the AFM tip where the tip reached the surface, and it marks the regions the tip could not reach instead of estimating them.

The efficiency side comprises the problem specification, the solvers, the comparison and convergence harnesses, the materials layer, and a calculation of the resolving power implied by the number of grooves measured.

Two mounting geometries are supported:

  • In-plane (classical): ultraviolet to near-infrared optical engineering.
  • Extreme off-plane (conical) at grazing incidence: soft x-ray spectroscopy, where the integral method is the established approach and general-purpose RCWA codes are least reliable.

The method backends, in order of development:

Method Status
Scalar (Kirchhoff, x-ray blaze form) experimental; checked against closed-form results
Integral method (boundary integral, conical, undercut-capable) experimental; perfect and finite conductivity (conductivity="tabulated", Goray & Schmidt 2010), absolute efficiencies with the absorbed fraction recorded; checked against published benchmark tables (Maystre & Popov; Goray & Schmidt), energy balance and reciprocity
RCWA (in-plane, conical, off-plane; Li factorization, S-matrix) contributed, not yet integrated
C-method (Chandezon) planned

The comparison harness also accepts an efficiency table already computed by an external program as one of its methods, so that comparison plots can be made without a licence for the program that produced the reference data:

reference = read_scan("run.txt", method="integral")
align(sweep(problem, illumination, wavelengths, ["scalar", reference]))

Because the file does not record the physics behind it, the caller states which method produced an imported table. Comparison legends then name the method, for example scalar vs integral, while Provenance.version records the program and version that generated the file.

Resolving power from a measured surface

A scan records the number of grooves it covers, so resolving power can be computed without entering anything by hand:

problem = profile.to_problem(coating="Au")     # n_grooves = the grooves averaged
resolving_power(problem, illumination, wavelength=2.0, order=3).r

The resolving power follows from the Rayleigh criterion as R = |m|·N, and line_profile gives the line shape for a finite number of grooves from which that result is derived. A problem whose n_grooves is not set is refused rather than treated as having infinitely many grooves. Resolving power degraded by errors in groove placement is planned; until it is implemented, the ideal formula is not used as an approximation for it.

Correcting for the AFM tip

An AFM image is the true surface dilated by the shape of the tip. tip_correction='erosion' reverses this dilation where it can be reversed (Villarrubia 1997) and reports a certainty map showing where it cannot. The tip is modelled as a spherical apex on two flanks whose angles may differ, so a pyramidal probe such as Bruker's ScanAsyst-Air can be represented. No method can recover the bottom of a trough the tip did not enter, and the correction leaves such regions as upper bounds instead of filling them in.

On one real scan, the groove depth and the fitted facet angle disagree by 7.6°. Erosion with the probe at its nominal radius recovers none of that difference, and reproducing it by simulation requires an apex worn to about 80 nm, which excludes the nominal tip as the cause (findings).

Install

pip install gratinglab                       # solvers only: numpy, scipy, pydantic
pip install "gratinglab[metrology]"          # + AFM readers and the boundary pipeline
pip install "gratinglab[gui]"                # + the Qt windows
pip install "gratinglab[refractiveindex]"    # + refractiveindex.info tables

With uv, the same extras work:

uv pip install "gratinglab[refractiveindex]"    # into the active environment
uv add gratinglab                               # into a uv project
uv tool install "gratinglab[gui]"               # gratinglab-gui as a standalone command

From a checkout, use pip install -e ".[dev,gui,metrology,refractiveindex]" instead.

No optical constant tables are distributed with the package, so absolute efficiencies require tables supplied by the user. A library is a directory of such tables, one material per file, each named after its material:

File From Range
Pt.txt a CXRO export x-ray
Pt.yml a refractiveindex.info file, renamed from its author's name ultraviolet to infrared
Pt.ari a PCGrate table whatever it covers

Saving one as ~/.gratinglab/materials/Pt.yml makes coating="Pt" available. GRATINGLAB_MATERIALS points to a different directory, and materials.from_cxro_file(path) or from_refractiveindex_file(path) reads a single file without a library. The YAML files require the refractiveindex extra. SOURCES.md explains why no tables are distributed.

The core install does not include a plotting stack, so a user who only runs the solvers does not install one. A subpackage that needs an optional extra names it when it is missing, instead of failing with a bare ModuleNotFoundError.

Apart from numpy, scipy and pydantic, the only dependency is endstation, a library of shared components for desktop scientific applications by the same author. gratinglab uses its guards for optional extras (the messages described above), form validation, Qt layout sizing, test helpers, and the tools that build the icon and the macOS application and publish the wiki. The core of endstation has no dependencies of its own, so it adds nothing to a solver-only install; its Qt components are installed only with the gui extra.

Where the measurement data lives

AFM scans are not included in the repository, because they are research data. The same applies to the private reference corpus of efficiency tables:

Data Environment variable Default
AFM scans GRATINGLAB_AFM_DIR ~/Documents/afm_scans
Reference efficiency tables GRATINGLAB_REF_DIR ~/Documents/diffraction_efficiency

Tests that need either are skipped when it is absent, so the suite passes on a fresh clone. Most of the metrology tests do not need it: they run against a committed synthetic scan of a grating with known geometry (tools/metrology/make_synthetic_scan.py), which also allows a test to check that the pipeline recovers the period and blaze angle used to generate it.

GUI

.venv/bin/python -m pip install -e ".[gui,metrology]"
gratinglab-gui                 # geometry, solvers, comparison
gratinglab-metrology-gui       # AFM import, blaze angles, boundary profiles

The windows are built with Qt (PySide6), so pip install "gratinglab[gui]" needs no system toolkit to be installed first. PySide6 is licensed under the LGPLv3 and is an optional, dynamically linked extra, so this package remains under the BSD-3 licence.

The grating geometry is set in a dock on the left of the efficiency window (Ctrl+G hides it), and the window has one tab for each task:

  • Grating Geometry: a rotatable 3D view of the diffraction cone, in which every wave vector is drawn as an exact unit vector. In the reference off-plane geometry the orders separate in azimuth rather than in polar angle (93.8° of azimuthal spread at 0.0° of polar spread), so the view looking down the cone axis, combined with a uniform 27× zoom, makes them distinguishable at true scale. The grazing angle γ is not exaggerated: a uniform zoom preserves every angle in the view, and the magnification is stated on the canvas. Alongside it are the cross-section in the dispersion plane, a sin β diagram that includes the evanescent orders, and the groove profile. The view updates as values are typed.
  • Scalar (Kirchhoff): the scalar solution, the efficiency of each order with per-order visibility, and a provenance panel that lists every validity warning, the summed-efficiency check, and whether convergence was demonstrated. A result that violates energy conservation is flagged. "Check convergence…" runs a convergence sweep and reports the coarsest setting that meets the criterion, with a progress bar and a Cancel button. Because solvers report progress for each wavelength, Cancel stops the computation itself.
  • Integral method: the rigorous backend, with the same panel and provenance and its own boundary-condition control (perfect conductivity or tabulated optical constants). It takes minutes where the scalar solver takes milliseconds, so the window opens on the scalar solver and this tab computes only when asked to.
  • Setup: the material library, showing which optical constants are installed, the range each covers, where each came from, and how to add others. The coating itself is chosen in the geometry dock, because it is a Problem field, like the period, and the same Problem is used by every tab.

Each registered solver receives a tab generated from the solver registry, so a contributed backend appears in the window once it is registered.

The Help menu contains the theory pages, rendered with typeset equations and tables.

A macOS .app can be built with python tools/make_app.py (macOS 13 or later, the minimum supported by the PySide6 wheels), and the icon is regenerated with python tools/make_icon.py.

Documentation

Document Contents
docs/roadmap.md The current state of the project, planned work and open questions; the place to start
docs/conventions.md The normative definitions: signs, units, normalizations, and errata in the references
docs/theory/scalar.md The derivation of the scalar solver, its closed-form results and its limits
docs/theory/integral.md The boundary-integral solver: conical formulation, perfect and finite conductivity
docs/theory/metrology.md The assumptions of the AFM pipeline, what it measures and what it does not, including what tip erosion can recover
docs/findings.md Empirical results, each with the evidence for it
docs/references.md The literature each part of the package is based on
docs/mutation-testing.md What is mutated, what is not, and how to interpret a surviving mutant
benchmarks/corpus.toml Geometry for the reference corpus, most of it recovered from the data

conventions.md should be read before writing a solver or reporting a bug involving a sign. In brief: the time convention is exp(-iωt), the complex refractive index is n = n′ + ik, the grating equation is sin α + sin β_m = mλ/(p sin γ), efficiencies are absolute, lengths are in nm, and angles are in degrees at the API boundary.

Each solver has a theory page stating its assumptions, its derivation in the notation of the code, and the conditions under which its results cease to be reliable.

The wiki is a published copy of docs/ and of the metrology window's own help pages, which appear there with an "AFM" prefix. Changes are made to the sources in the repository and then republished:

git clone https://github.com/jamccoy/gratinglab.wiki.git ../gratinglab.wiki
.venv/bin/python tools/publish_wiki.py --wiki ../gratinglab.wiki   # then review and push

Development

.venv/bin/python -m pip install -e ".[dev,gui,metrology,refractiveindex]"
.venv/bin/python -m pytest
.venv/bin/ruff check .

The test suite relies on three practices in particular:

  • Physical self-checks. checks.check_reciprocity and checks.check_energy_balance test properties that any correct model must satisfy, independently of how the solver computes its result. They require no reference data and apply to every backend.

  • Demonstrated convergence. convergence.check_convergence varies whatever a solver declares as its accuracy parameter and reports the coarsest setting that meets the criterion, or reports that none did. It requires three consecutive values to agree, because the quadrature error of a blazed profile does not decrease monotonically, so a single agreement can be coincidental (findings).

    report = check_convergence(scalar, problem, illumination, wavelengths)
    report.converged_at           # 4096 -- what a production run should use
    report.scan.provenance.converged   # True
    
  • Mutation testing. The suite is run against deliberately altered copies of the physics code to check that the changes are detected. Coverage shows that a line was executed; mutation testing shows whether any test would fail if that line were wrong. Both runs so far have found real gaps in the tests; see findings and docs/mutation-testing.md for how to run it.

    .venv/bin/python -m pip install -e ".[mut]"
    .venv/bin/mutmut run
    

License

BSD-3-Clause.

Metadata

Release files for gratinglab 0.1.8

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for gratinglab 0.1.8
File Size Uploaded Provenance
gratinglab-0.1.8.tar.gz 482.0 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for gratinglab 0.1.8
File Interpreter ABI Platform
gratinglab-0.1.8-py3-none-any.whl Python 3 none any Details

Total release size: 857.6 kB

Release files / gratinglab-0.1.8.tar.gz

Download URL gratinglab-0.1.8.tar.gz
Size 482.0 kB
Tags Source
SHA-256 checksum
How to use checksums
6a3e1a6af969faedc4b2d15988a105c6e8cddadaaa0290598b4a04510084c022
BLAKE2b-256 checksum
How to use checksums
6bd1f3bdbcb3119c348594373044ece44f73f2d9ea3620bf2ac23a0e9bdfef9c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

Release files / gratinglab-0.1.8-py3-none-any.whl

Download URL gratinglab-0.1.8-py3-none-any.whl
Size 375.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f3629b70cb097b76c88c71744adae6888b7d568ee180d0ddf852f9cdbdbc5693
BLAKE2b-256 checksum
How to use checksums
e8396f7cdfce8cd71f73b80bb7a1ec9ea6104be36609c515dfedfc249b54f749
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

Security for gratinglab 0.1.8

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for gratinglab-0.1.8.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page