Thin-film thickness, roughness and SLD from one X-ray reflectivity curve: a CNN guess, then a differentiable Abeles fit.
A synthetic curve (grey; 240 Å film, roughness 6 Å, SLD 45×10⁻⁶ Å⁻², on a 12 Å oxide over Si, with footprint,
resolution, background and Poisson noise), the CNN's starting guess (blue, dashed) and the
gradient-refined fit through the same Abeles engine (red), from reflectolearn predict.
A 1D CNN trained on simulated curves guesses the film; a staged gradient fit through a differentiable Abeles simulator (PyTorch) refines the guess against the data. How it works, every command, the GenX fit and the configuration: docs/usage.md.
- Returned fit endpoints usually represent the minima of the basins they reach
- Counts in repeated public scans vary more than Poisson statistics predict
- One bilayer is locally determined except for the first film's thickness
| Date | Report | Result |
|---|---|---|
| 2026-10-25 | Are returned XRR fit endpoints basin minima? | Returned fit endpoints stand for their basins (supported); a few wrong basins need ≥ 10⁴ counts. |
| 2026-10-25 | Basin minimum validation: full run | Endpoint S₁ represents its basin (supported); 5 R2a and 1 X2e wrong basins need ≥ 10⁴ counts. |
| 2026-10-25 | Synthesis of B-1, B-2 and H-cal2 | Fisher-versus-global alone is not a paper; conditional yes inside an identifiability-versus-reach methods paper. |
| 2026-10-25 | H-cal2: count dispersion in repeated public scans | Repeated public scans are overdispersed against Poisson: median φ 1.34, above 1.2 at 64 % of points. |
| 2026-10-25 | Experiment 004 (X2e) results | Better starts fail: S12 beats C in 0 of 4 cells; wrong-basin reading supported, its source unresolved. |
Uses uv; Python 3.12 or newer.
git clone https://github.com/NaNIsaac-Research/ReflectoLearn.git
cd ReflectoLearn
uv sync
uv run reflectolearn --helpThe last line prints the command list and exits with an error if the install is broken. On Windows, torch is taken from the PyTorch CUDA 12.8 index; other platforms use the default PyPI wheel.
uv run reflectolearn config --out config.yaml
uv run reflectolearn train --config config.yaml
uv run reflectolearn predict measured.txt -k path/to/best.pt # 2 columns: 2θ or q, intensitypredict writes <name>_predict.png and <name>_predict.txt next to the input. Evaluation, tuning, stitching, the benchmark and the ONNX export are in docs/usage.md.
uv sync --extra genx --extra mcmc --extra onnx
uv run pytestThe two GenX tests are skipped without the genx extra, the emcee test without the mcmc extra, and the ONNX tests without the onnx extra.
The validation so far is synthetic only. No measured data ships with the repository.
examples/synthetic_film_fit/simulates counts from a known film with an independent Parratt engine. It then runs them through the fit path thatreflectolearn predictuses after its network, and compares the result with the known film.expected.jsonkeeps the truth (with tolerances) apart from a regression snapshot, andtests/test_example_synthetic_film_fit.pychecks both.- Known limit found by the example: on data with no angular offset, the default
refinemoves the 2θ offset towards its bound and the film SLD comes out about 30% low (#14). The SLD and roughness checks are strict expected failures until that is fixed. The thickness is right. - Measured XRR curves have not been validated yet.
The repository also holds a series of registered experiments on synthetic curves. Each has a
registration written before its run, run records, a report built by a script from those records,
and a verdict from the registered rules. The index with every file, verdict and command is
docs/research/README.md.
Experiment 001: is a wrong thickness from refine a second solution or a failed fit? Synthetic single films on Si, computed, not measured: Cu Kα1 (1.5406 Å) with Kα2 and the beam footprint in the model, q 0.014–0.43 Å⁻¹ (2θ 0.2–6°), s = 10⁶ counts at R = 1. (a) SLD depth profiles of the three true films (schematic), with the top 40 Å below. (b) One Poisson curve of mid_oxide with the bounded Poisson maximum-likelihood fit, the best refine endpoint (started at the truth) and the pre-registered pair point (profile point at Δχ² = 0.08, d = 239.4 Å), all recomputed from their saved parameters; below, the normalized residuals on a symmetric log scale. The refine endpoint misfits the low-q fringes (residuals up to 116; 99.8 % of its deviance excess over the ML fit is at q < 0.1 Å⁻¹), while the pair point fits as well as the ML fit. (c) Within refine's bounds, along the one branch the profile followed, at s = 10⁶, the deviance has a single minimum in d over ±29 %. Its widths at Δχ² = 1 (bars, from the table) are set where the Kα2 ratio or the oxide SLD reaches its bound for the 240 Å films: a prior, not information in the curve. The profile departs from the local Fisher (CRLB) parabola: it is steeper once a nuisance parameter reaches its bound and, for t2_low, flatter between 0 and +1 %; at Δχ² = 1 it is narrower in all three cases. Ticks along the bottom give the d where each fit stopped. (d) On one noisy curve per case, all 48 refine endpoints (open) stop at least 4.8×10⁴ in deviance above the best fit, with d from −31 % to +45 %, while 43 of 45 Poisson least-squares fits from the same starts (filled) reach it (the other two stop near d +89 %). All 36 refine endpoints off by more than 1 % in d are also worse than the ML fit on refine's own objective, so they are failed searches; the 5 within +0.18 to +0.50 % (×) are preferred by that objective, an error of the objective or of refine's model (amendment 3, not pre-registered). Data: nonunique_<case>.json and nonunique_tables.md; script: nonunique.py; redraw with uv run --group figures python experiments/001_identifiability/nonunique.py report (no fit is rerun).
- Identifiability (001). Whether wrong single-film fits come from the optimizer or from missing information. Within refine's bounds the three test films are identifiable; 43 of refine's 48 wrong fits are optimizer failures and 5 are errors of its objective or model (#25).
- Fit policy (002, 003, 004). Whether a scripted fitting protocol (layer count, which parameters to open, when to stop) beats a full fit of every layer count, and why its extra layers fail on held-out points. It does not beat the full fit (002); the extra layers compensate for a wrong start basin (003); better starts did not remove them (004).
- Fisher information against global fits (005). Whether local Fisher information describes what a global fit can tell apart. Partly: the low-q share of the discrimination and the overdispersion of repeated scans are supported, the Fisher-ratio hypothesis is unresolved.
| # | Experiment | Verdict |
|---|---|---|
| 001 | identifiability | optimizer failures within refine's bounds |
| 002 | fit policy X2a/X2b | no room for a learned policy |
| 003 | fit policy X2d | wrong-basin cause supported; protocol pass not attributable |
| 004 | fit policy X2e | fail; basin reading supported |
| 005 | Fisher against global fits | H-F unresolved; H-W2, H-cal2, H-BM1 supported |
The commits each registration cites are tagged frozen/exp…; result files too large for the
repository are in the release research-data-1.
See CITATION.cff.
MIT, see LICENSE.

