Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ let package = Package(
name: "MyTool",
platforms: [.macOS(.v15)],
dependencies: [
.package(url: "https://github.com/mac4DSTEM/SpectroKit", from: "0.12.0"),
.package(url: "https://github.com/mac4DSTEM/SpectroKit", from: "0.13.2"),
],
targets: [
.executableTarget(name: "MyTool", dependencies: [
Expand Down
279 changes: 0 additions & 279 deletions docs/design/eels-chain.md

This file was deleted.

4 changes: 2 additions & 2 deletions tools/edx-planted/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# tools/edx-planted: generators and pins for the the EDX checks

Written before the Swift tests, from the EDX design note. Reference venv `References/python` (tools/reference/setup.sh; HyperSpy 2.4.0, NumPy 2.5.4,
Fixtures from the EDX design note (archived in the suite's private folder). Reference venv `References/python` (tools/reference/setup.sh; HyperSpy 2.4.0, NumPy 2.5.4,
SciPy 1.18.1, h5py 3.16.0), run `-I -B`. Fixtures: `Tests/SpectroEDXTests/Fixtures/` (15 MB).

## Run order (all deterministic; two full runs wrote identical bytes, sha256 compared)
Expand Down Expand Up @@ -32,4 +32,4 @@ Docstrings of each script define every parameter; this file lists readings made
## Findings
**C (counts).** NumPy decode of `fei_SI_EDS-HAADF-4detectors_2frames.emd`: 916174 / 867647 / 913682 / 865236 events per stream in name order, 3562739 total, 512 markers per stream: equals the published constants (asserted, the script stops otherwise). Streams decoded for 8 public files (4detectors, three SuperX 10x50, fei_emd_si, EELS_EDS, SI_empty selection, Test SI 16x16; the ReducedData file has an empty stream group: skipped). `fei_emd_spectrum.emd` has a stream group whose grid is unresolved (skipped, reported). `example_velox_EELS_EDS.emd`: ScanSize x ScanArea 16 x 20 vs rsciio's last-image 128 x 128 (known deviation). Planted files: decode equals the draw for every frame range (self-check, passed). exSpy `sum()` of a uint16 image: dtype uint64, equals NumPy uint64 (max 131077 from two 65535), `live_time` 2.0 -> 40.0 over 20 pixels. Predicted outcome met.
**E (axis).** rsciio's `_get_dispersion_offset` (ast-cut, sha256 asserted) agrees with the hand-written expectations on all six planted files: 4 streams offsets -1.0/-1.0/-0.996/-1.0 keV (calibration must disagree), decoy picks (0.01, -0.5), axis-order picks Detector-10 (0.02, -1.9) by FILE order. Possible Swift deviation (read, not run): `VeloxEMDReader` sorts `detectors` by index before the "first whose name contains" rule, which would pick Detector-2 (10, -0.5) on `axis-order`. E3, exSpy `calibrate_energy_axis("offset")` (offset change vs -delta, eV): N 1e6 (288 fits) mean +0.03, max |err| 0.71; N 1e7 (96) mean +0.02, max 0.62. By gain: -3e-4 mean +0.46, 0 mean +0.03 (max 0.29 at 1e6, 0.15 at 1e7), +3e-4 mean -0.41. **Registered prediction "within +-0.2 eV at N_Al >= 1e6" is FALSE when the gain is off (max 0.71 eV) and marginally false at gain 0, N 1e6 (0.29 eV; measured 0.1 in the probe at one cell).** `"scale"` mode returns scale 0.0199577 for every planted gain (true 0.0200060 / 0.0200000 / 0.0199940): it does not recover gain here (and alters the cropped offset). `"resolution"` raised "FWHM_MnKa ... smaller than physically possible" in 2 of 384 fits. The Swift refinement bound (1 eV, 1e-4) is untested yet.
**P (peaks).** P1: **exSpy's `get_xray_lines_near_energy` order for exactly tied |dE| lines is not reproducible** (it iterates a hash-ordered element dict; the pins list differed between two processes). The registered "same order including ties" cannot be a contract; pins store canonical (|dE|, name) order (45 tied adjacent pairs in 360 cases). Window-method bias (exSpy windows, Mg net at rho_Mg = 0, noise-free mean of the generator): U130 family s -134 / -1335 / -13353 at N 1e5 / 1e6 / 1e7; family L -129 / -1292 / -12922; M/s -117 / -1174 / -11744. Registered 7.2 (U, 200 realisations): -127 / -1300 / -12893: reproduced. Realisations agree (e.g. U130/L 1e7 -12951 +- 534, n = 20). P3: `find_peaks1D_ohaver` with defaults finds one peak per spectrum and Mg in 0/5 at every cell; with amp 0.2 % of max, medfilt 3 it finds Mg (within 20 eV) 5/5 at rho >= 0.01 for N >= 1e5 and at rho 0.002 for N >= 1e6 (2/5 at 1e5), with 8.6 to 22 peaks per spectrum. Surrogate (probe3 design, SciPy NNLS) on the generator's noise-free means at N 1e7, Mg net/truth for rho 0.002 / 0.01: matched 0.99 / 0.99; shift +1 0.45 / 0.88, +2 0 / 0.72, +3 0 / 0.55, +5 0 / 0.16, +-10 0 / 0; Al width x1.02 0 / 0.77, x1.06 0 / 0.28; Fano (rho_c .0008) 0 / 0.10, (rho_c .008) 0 / 0.39; tail 2 % 0 / 0.77. This reproduces the 7.3 table (matched 1.00 / 0.98, +1 0.43 / 0.87, +2 0 / 0.71, +3 0 / 0.54, +5 0 / 0.15, x1.02 0 / 0.77, x1.06 / 0.28, Fano 0 / 0.39) to within 0.02, except the tail row (0.77 here against 0.81 registered) and with the matched L_D of 2009 against 2.2e3 registered. No Swift proposer was run.
**P (peaks).** P1: **exSpy's `get_xray_lines_near_energy` order for exactly tied |dE| lines is not reproducible** (it iterates a hash-ordered element dict; the pins list differed between two processes). The design note's "same order including ties" cannot be a contract; pins store canonical (|dE|, name) order (45 tied adjacent pairs in 360 cases). Window-method bias (exSpy windows, Mg net at rho_Mg = 0, noise-free mean of the generator): U130 family s -134 / -1335 / -13353 at N 1e5 / 1e6 / 1e7; family L -129 / -1292 / -12922; M/s -117 / -1174 / -11744. Design note 7.2 (U, 200 realisations): -127 / -1300 / -12893: reproduced. Realisations agree (e.g. U130/L 1e7 -12951 +- 534, n = 20). P3: `find_peaks1D_ohaver` with defaults finds one peak per spectrum and Mg in 0/5 at every cell; with amp 0.2 % of max, medfilt 3 it finds Mg (within 20 eV) 5/5 at rho >= 0.01 for N >= 1e5 and at rho 0.002 for N >= 1e6 (2/5 at 1e5), with 8.6 to 22 peaks per spectrum. Surrogate (probe3 design, SciPy NNLS) on the generator's noise-free means at N 1e7, Mg net/truth for rho 0.002 / 0.01: matched 0.99 / 0.99; shift +1 0.45 / 0.88, +2 0 / 0.72, +3 0 / 0.55, +5 0 / 0.16, +-10 0 / 0; Al width x1.02 0 / 0.77, x1.06 0 / 0.28; Fano (rho_c .0008) 0 / 0.10, (rho_c .008) 0 / 0.39; tail 2 % 0 / 0.77. This reproduces the 7.3 table (matched 1.00 / 0.98, +1 0.43 / 0.87, +2 0 / 0.71, +3 0 / 0.54, +5 0 / 0.15, x1.02 0 / 0.77, x1.06 / 0.28, Fano 0 / 0.39) to within 0.02, except the tail row (0.77 here against the design note's 0.81) and with the matched L_D of 2009 against the design note's 2.2e3. No Swift proposer was run.
2 changes: 1 addition & 1 deletion tools/eels-chain/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# tools/eels-chain: exSpy / HyperSpy pins for the EELS chain, sections 2, 3 and 4

Written before the Swift code, so the port of `docs/design/eels-chain.md` started with numbers.
Fixtures for the EELS chain (the 0.9.0 design note is archived in the suite's private folder, `process/archive/SpectroKit/`).
Covers zero-loss alignment and mask (design section 2), power-law extrapolation and Hanning taper (section 3), Fourier-log,
Fourier-ratio and `optimal_fft_size` (section 4). Not covered: GOS tables, the edge model fit (section 6), Richardson-Lucy.

Expand Down
11 changes: 6 additions & 5 deletions tools/eels-edge/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# tools/eels-edge: truth and pins for EELS edge quantification (power-law background, C K areal density)

Written before the Swift code, as the EELS edge quantification design note sections 3.3, 4 and 5 fix them. Four scripts:
Four scripts. The "design note" below is the EELS edge quantification design note (eels-slice-2), archived in the suite's
private folder; its section and gate numbers are kept so the findings can be read against it.

| script | needs | writes |
|---|---|---|
Expand Down Expand Up @@ -59,7 +60,7 @@ two-area estimate's I1, I2, r, A computed from the components.

## Findings

**1. Registered prediction false: the single-scattering bias is not below 1 %.** (Gate 3 predicted "the bias is below 1 %".)
**1. The design note's prediction is false: the single-scattering bias is not below 1 %.** (Gate 3 predicted "the bias is below 1 %".)
The bias is absolute (atoms/nm^2) and, for single scattering, independent of n_true: `(I_b,true - I_b,est) * 1e10 / (I_0 sigma_k)`.
Default window, Delta 50, sigma_k = 3850.67 barn, I_0 = 1e7: bias -0.428 (A=1e9, r=3.0), -3.548 (A=5e8, r=2.5), -0.494 (A=2e10, r=3.5)
atoms/nm^2. In units of n_true the 15 pixels read (row by row, n = 2, 5, 10, 20, 50):
Expand Down Expand Up @@ -99,7 +100,7 @@ and `energy_axis` as float32 (NumPy 2 keeps float32 + Python float float32), and
factor `4 pi a0^2 R^2 / E / T * subshell_factor * 1e28` is a float32 array; only `qint` itself (and everything after the
`searchsorted`/interpolation, which mix float32 data with float64 `qmin`/`qmax`) is float64. Replacing the arrays by their float64
widenings gives `qint` and `function(E)` that differ from exSpy's by up to 1.9e-7 and 1.0e-7 relative. A Double port will
miss the registered 1e-12 / 1e-10 bounds by five orders of magnitude unless it reproduces exSpy's float32 steps in the same places
miss the design note's 1e-12 / 1e-10 bounds by five orders of magnitude unless it reproduces exSpy's float32 steps in the same places
(or the bounds are amended). The pins hold `tabulatedEnergies` (the float32 axis, widened), `qint`, and for C K 200/10/20 the
`qmin`/`qmax` passed to `get_qaxis_and_gos` per energy (`splineC.integrateq`), to help find the first divergent step.

Expand All @@ -108,7 +109,7 @@ reused across E0 at alpha = 0 (effective angle = beta) keeps a stale `qint`. The
The effective angle is computed with `GOS.onset_energy` (the table's onset), not the editable onset.

**8. Counting statistics, first set.** The noisy images (seed 20261015) have the edge (7.7 to 192 counts over 50 eV) on 1717 to 15038 background counts: the
ss default-window Delta-50 n of the 15 noisy pixels runs from -141.7 to +138.3 atoms/nm^2 for n_true 2-50 (sigma_n ~ 36-130 atoms/nm^2). The first set stays as registered; the
ss default-window Delta-50 n of the 15 noisy pixels runs from -141.7 to +138.3 atoms/nm^2 for n_true 2-50 (sigma_n ~ 36-130 atoms/nm^2). The first set stays as the design note defines it; the
realistic set below is the one the bias and noise gates can meaningfully use.

## The second, realistic set (R): n_true 200, 500, 1000, 2000, 5000 atoms/nm^2 (a ~2-50 nm carbon film)
Expand All @@ -125,7 +126,7 @@ relative to n_true, default window, Delta 50, by row (n = 200, 500, 1000, 2000,
-0.214, -0.086, -0.043, -0.021, -0.009 %; -1.774, -0.71, -0.355, -0.177, -0.071 %; -0.247, -0.099, -0.049, -0.025, -0.010 %.
Min/max -1.774 % / -0.0086 %: **"<= 2 % for Delta 50 with the default window" holds at every pixel** (worst: A = 5e8, r = 2.5, n = 200). It holds for all
three windows and every Delta 30-150 too (worst -1.951 %, narrow window, Delta 30; default window: -1.827 % at Delta 30 ... -1.547 % at Delta 150).
The registered "below 1 %" fails at that one pixel only, at Delta 50 default (by 0.8 percentage points).
The design note's "below 1 %" fails at that one pixel only, at Delta 50 default (by 0.8 percentage points).

**R2. Plural scattering (gate 4), I_low(Delta) route, default window, Delta 50**, relative bias (min .. max over the 15 pixels):
tau 0.3: -9.40 % .. -7.64 %; tau 0.7: -19.35 % .. -17.57 %; tau 1.3: -32.57 % .. -30.70 %.
Expand Down
12 changes: 6 additions & 6 deletions tools/eels-thickness/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# tools/eels-thickness: truth and pins for EELS thickness (the t/λ thickness map)

Written before any Swift code, as the EELS thickness design note sections 4 and 5 fix them. Two generators, one
fixture directory, `Tests/SpectroEELSTests/Fixtures/` (written before the `SpectroEELS` target existed; `SpectroEELSTests` reads it).
Two generators, one fixture directory, `Tests/SpectroEELSTests/Fixtures/`. The "design note" below is the EELS thickness
design note (eels-slice-1), archived in the suite's private folder; its section numbers are kept so the findings can be read against it.

**Agreement with exSpy is not truth.** `pins.json` is what exSpy 7185a4d1 answers; a port that equals it is a port, not a
measurement. The truth is `planted-truth.json`, made by `make_planted.py` from NumPy alone, with no exSpy, HyperSpy or SciPy
in it. The two answer different gates (design note section 7: gate 1 against the pins, gates 2 and 3 against the truth).
in it. The two answer different tests (design note section 7: gate 1 against the pins, gates 2 and 3 against the truth).
Nothing here makes a thickness validated: `validation: "none"` stays until a sample of known thickness is measured.

## Run
Expand Down Expand Up @@ -126,7 +126,7 @@ is at or above the last channel's energy the crop ends at `size − 1`, so the l
stop = `value2index(θ)` (above), end exclusive, so I_0 is the sum of channels `[0, value2index(θ))`; the channel nearest θ is
not in it. If `value2index` raises because θ is above the last energy the slice runs to the end (I_0 = I_t, `axes.py:462-466`);
θ below the first energy raises `IndexError` (`:457-461`); θ exactly at the last energy gives stop = `size − 1`. For the
registered 2.5, 4.5, 6.0 eV the channels are 250, 290, 320 (planted) and 205, 209, 212 (line scan).
design note's 2.5, 4.5, 6.0 eV the channels are 250, 290, 320 (planted) and 205, 209, 212 (line scan).

**estimated threshold, per pixel** (`_eels.py:608-621`): NaN gives NaN; else `ind = axis.value2index(threshold)` and
`I_0 = data[:ind].sum()` (numpy pairwise sum of the float64 channels `[0, ind)`; `pins.py` asserts exSpy's I_0 equals exactly
Expand Down Expand Up @@ -180,9 +180,9 @@ noiseless image (≤ 5.6e-16: the generator and exSpy agree to rounding); and `g
per pixel and route, with the pixels above 4. For the estimated threshold it is **7.4 at the worst pixel (pixels 7 and 9 exceed 4)**,
for every fixed threshold at most 2.01: see the findings below.

## Findings while writing these (the design note is not amended here; these are for its amendment)
## Findings while writing these (read against the design note)

1. **Gate 3 as registered cannot hold for the estimated threshold.** With the estimated threshold the noisy image's thresholds
1. **Gate 3 as the design note states it cannot hold for the estimated threshold.** With the estimated threshold the noisy image's thresholds
differ from the noiseless ones (noiseless 1.2 to 1.35 eV; noisy 1.3 to 8.65 eV, because the global `tol` is set by the noisiest
pixel), so |t/λ_noisy − t/λ_noiseless| is a threshold effect, not counting noise: 6.2 σ_counting at the τ = 1.3 pixel and 7.4 σ_counting
at τ = 2.0 (gate: ≤ 4). Over 200 noise realisations (NumPy seeds 1 to 200, run once with exSpy, not stored) the standard
Expand Down
Loading