From 57a583c6bfbca292d5561b69d8fa88b51a214ec2 Mon Sep 17 00:00:00 2001 From: Paullo9 Date: Wed, 28 Oct 2026 16:19:33 +0100 Subject: [PATCH] Comments and docs state facts, not internal process (#18) Sources, tests, Package.swift and docs no longer cite the archived design and registration documents, work-package and lane ids, ADR numbers, gate names or the maintainer-instructions file; each comment keeps the fact it relied on (the upstream function, the tolerance, the reason) or goes. Personal references (a named detector, 'the owner's file') are now neutral descriptions. Package.swift's comments say what a reader of the manifest needs. docs/status.md describes 0.12.0, docs/open-items.md has one item per number and cites nothing outside the repo, docs/design/eels-task-16.md is the design note docs/design/eels-chain.md without the hand-off sections, and auto-id-tem.md loses its decision preamble. No code changes except ProposalRules.source's wording ('fixed 2026-10-07, ...'). Co-Authored-By: Claude Opus 5.5 --- Package.swift | 55 ++++---- .../CSpectroLAPACK/include/CSpectroLAPACK.h | 2 +- .../Fit/BandedLinearAlgebra.swift | 2 +- .../Spectroscopy/Fit/LeastSquaresFit.swift | 8 +- .../Spectroscopy/Fit/PoissonMLFit.swift | 4 +- .../Spectroscopy/MapSmoothing.swift | 2 +- .../Spectroscopy/SpectroCancellation.swift | 2 +- .../Spectroscopy/SpectrumImage.swift | 6 +- .../Spectroscopy/SpectrumImageOpening.swift | 2 +- .../Spectroscopy/SpectrumImageSource.swift | 6 +- .../Statistics/CountingInterval.swift | 4 +- .../Statistics/LiveTimeNormalisation.swift | 4 +- .../Spectroscopy/Statistics/SigmaTerms.swift | 2 +- .../Quantifying-an-EDX-spectrum.md | 2 +- .../Spectroscopy/Fit/Continuum.swift | 14 +-- .../SpectroEDX/Spectroscopy/Fit/EDSFit.swift | 8 +- .../Spectroscopy/Fit/EDSModel.swift | 2 +- .../Fit/EnergyAxisRefinement.swift | 2 +- .../Spectroscopy/Fit/ReferenceShapes.swift | 4 +- .../Spectroscopy/Method/ProposerPolicy.swift | 2 +- .../Method/QuantificationMethod.swift | 15 ++- .../Proposer/ElementProposer.swift | 10 +- .../Spectroscopy/Proposer/LineConflicts.swift | 4 +- .../Spectroscopy/Proposer/ProposalRules.swift | 28 ++--- .../Spectroscopy/Quant/BrownPowellK.swift | 7 +- .../Quant/DetectorEfficiency.swift | 3 +- .../Spectroscopy/Quant/FitRange.swift | 4 +- .../Quant/FourDetectorAbsorption.swift | 3 +- .../Spectroscopy/Quant/KFactorSource.swift | 2 +- .../Quant/PooledQuantification.swift | 16 +-- .../Spectroscopy/Quant/QuantElementData.swift | 3 +- .../Quant/QuantificationPolicy.swift | 6 +- .../Spectroscopy/Quant/SDDEfficiency.swift | 2 +- .../Quant/UnlistedLineCheck.swift | 10 +- .../Spectroscopy/Quant/ZetaFactor.swift | 4 +- .../Spectroscopy/SpectrumQuantities.swift | 6 +- .../Spectroscopy/WindowIntensity.swift | 2 +- .../SpectroEDX/Spectroscopy/WindowRules.swift | 1 - Sources/SpectroEELS/EELSAlignment.swift | 2 +- Sources/SpectroEELS/EELSDeconvolution.swift | 2 +- Sources/SpectroEELS/EELSModelFit.swift | 4 +- Sources/SpectroEELS/EELSParallel.swift | 2 +- Sources/SpectroEELS/EELSRegionSum.swift | 2 +- Sources/SpectroEELS/EELSSpectrumImage.swift | 2 +- Sources/SpectroEELS/EELSSpectrumOps.swift | 4 +- Sources/SpectroEELS/EELSThickness.swift | 2 +- Sources/SpectroEELS/EdgeCrossSection.swift | 2 +- Sources/SpectroEELS/EdgeQuantification.swift | 14 +-- Sources/SpectroEELS/GOSTable.swift | 4 +- Sources/SpectroEELS/InterpolatingSpline.swift | 2 +- .../SpectroEELS/LowLossSpectrumImage.swift | 2 +- Sources/SpectroEELS/PowerLawBackground.swift | 6 +- Sources/SpectroEELS/ScipySimpson.swift | 2 +- Sources/SpectroEELS/ThicknessMap.swift | 10 +- .../AutoIDRulesWP4bTests.swift | 9 +- .../FinalPolishDTests+Spectro.swift | 4 +- .../MethodTests+Spectro.swift | 4 +- .../SpectroCancellationTests.swift | 2 +- .../SpectroscopyCoreTests.swift | 4 +- .../SpectroscopyDriveL10Tests.swift | 2 +- .../SpectroscopyDriveL12Tests+Spectro.swift | 12 +- .../SpectroscopyFitRangeTests+Spectro.swift | 4 +- .../SpectroscopyFitTests.swift | 8 +- .../SpectroscopyLaneUTests+Spectro.swift | 4 +- .../SpectroscopyProposerTests.swift | 6 +- .../SpectroscopyQuantTests.swift | 10 +- .../SpectroscopyQuantifyTests+Spectro.swift | 2 - ...pectroscopyUnlistedLineTests+Spectro.swift | 8 +- .../SpectrumImageOpeningTests+Spectro.swift | 2 - .../SumColumnDedupTests.swift | 4 +- .../SumEscapeIdenticalTests.swift | 12 +- Tests/SpectroEDXTests/AxisGateTests.swift | 12 +- Tests/SpectroEDXTests/CountsGateTests.swift | 2 +- .../IdentificationLoopTests.swift | 6 +- Tests/SpectroEDXTests/PolicyNamingTests.swift | 2 +- .../SpectroEDXTests/ProposerPolicyTests.swift | 2 +- .../QuantificationPolicyTests.swift | 2 +- .../SpectroEDXTests/RealFileAutoIDTests.swift | 6 +- Tests/SpectroEDXTests/WindowGateTests.swift | 4 +- Tests/SpectroEDXTests/WindowRulesTests.swift | 2 +- .../SpectroEELSTests/DefaultWindowTests.swift | 2 +- .../SpectroEELSTests/EELSAlignmentTests.swift | 2 +- .../EELSDeconvolutionTests.swift | 2 +- .../SpectroEELSTests/EELSModelFitTests.swift | 2 +- .../EELSSpectrumOpsTests.swift | 2 +- .../SpectroEELSTests/EdgeEdgeCaseTests.swift | 4 +- Tests/SpectroEELSTests/EdgeTruthTests.swift | 4 +- .../SpectroEELSTests/LineScanTruthTests.swift | 8 +- .../SpectroEELSTests/PlantedTruthTests.swift | 2 +- .../RefuterRegressionTests.swift | 6 +- .../AlignPinsTests.swift | 2 +- .../DeconvolutionPinsTests.swift | 4 +- .../EDSQuantPinsTests.swift | 2 +- .../EdgePackPinsTests.swift | 2 +- .../SpectroKitParityTests/EdgePinsTests.swift | 8 +- .../EdgeTablePinsTests.swift | 4 +- Tests/SpectroKitParityTests/GateDTests.swift | 23 ++-- .../ModelPinsTests.swift | 4 +- .../SpectroKitParityTests/PeakGateTests.swift | 14 +-- .../SpectroGoldenTests.swift | 2 +- .../SpectrumOpsPinsTests.swift | 2 +- .../ThicknessPinsTests.swift | 2 +- .../SmoothingKernelTests.swift | 2 +- Tests/SpectroTestSupport/EDXFixtures.swift | 6 +- Tests/SpectroTestSupport/EELSFixtures.swift | 2 +- Tests/SpectroTestSupport/EdgeFixtures.swift | 4 +- Tests/SpectroTestSupport/PlantedRuns.swift | 6 +- Tests/SpectroTestSupport/SeededPoisson.swift | 2 +- docs/benchmarks.md | 6 +- docs/design/auto-id-tem.md | 30 ++--- .../design/{eels-task-16.md => eels-chain.md} | 118 ++++-------------- docs/open-items.md | 15 ++- docs/parity.md | 10 +- docs/status.md | 24 ++-- 114 files changed, 335 insertions(+), 449 deletions(-) rename docs/design/{eels-task-16.md => eels-chain.md} (69%) diff --git a/Package.swift b/Package.swift index 66096eb..9646ec2 100644 --- a/Package.swift +++ b/Package.swift @@ -1,18 +1,14 @@ // swift-tools-version: 6.2 -// SpectroKit — a spectroscopy library (a Swift port of exSpy), extracted from an application's source at 5ba712a0. +// SpectroKit: EDS and EELS spectroscopy for the TEM, a Swift port of exSpy. // -// Library targets (SpectroCore the shared part, SpectroEDX first, SpectroEELS later): -// `SpectroCore` is what EDS and EELS share (the spectrum image, the energy axis, the least-squares and Poisson fits, the -// counting statistics; the Velox and DM4 file readers moved to FormatoKit at 0.4.0); `SpectroEDX` is the X-ray part (lines, the continuum and EDS fit -// models, the element proposer, the quantification method, Cliff-Lorimer, absorption, the k-factor sources) and depends on -// SpectroCore. SpectroCore never depends on SpectroEDX; a future SpectroEELS will depend on SpectroCore only. Swift 6 -// language mode, nonisolated by default (no `.defaultIsolation`), as in a consumer's core library. The six EPQ / FFAST / Krause tables -// are SpectroEDX's `Bundle.module` resources (`QuantificationTables.bundled()`, bundle `SpectroKit_SpectroEDX`); test -// fixtures are read by `#filePath` and are not resources. What one module needs from the other is `package`, never `public`. +// `SpectroCore` is what EDS and EELS share (the spectrum image, the energy axis, the least-squares and Poisson fits, the counting +// statistics); `SpectroEDX` (X-ray lines, the EDS fit, the element proposer, quantification, absorption) and `SpectroEELS` +// (thickness, alignment, deconvolution, core-loss edges) each depend on SpectroCore and never on each other. What one module needs +// from another is `package`, never `public`. Swift 6 language mode, nonisolated by default. The EDX tables and the EELS GOS tables +// are `Bundle.module` resources; test fixtures are read by `#filePath` and are not resources. // -// 0.4.0: the file readers (`DM4Experiment`, `VeloxEMDReader`, `HDF5Serial`) live in FormatoKit (a Swift port of RosettaSciIO, -// docs/consumers-0.4.0.md), a package dependency on a FormatoKit minor range (`0.4.0..<1.0.0` since 0.7.1, `0.4.0..<0.6.0` at 0.7.0 (0.3.x dropped: the Velox golden needs the 0.4.0 metadata fields), `0.3.0..<0.5.0` at 0.6.1; `.upToNextMinor` at 0.6.0, exact before) so a consumer's `swift build` resolves it alone. FormatoKit -// ships libhdf5 as binary xcframeworks (FormatoLibHDF5, FormatoLibSZ, FormatoLibAEC), so nothing here asks a consumer to supply one. +// The file readers are FormatoKit's (a Swift port of RosettaSciIO), on a minor range so a consumer's `swift build` resolves it alone; +// FormatoKit ships libhdf5 as binary xcframeworks, so nothing here asks a consumer to supply one. import PackageDescription let package = Package( @@ -25,14 +21,13 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/mac4DSTEM/FormatoKit", "0.4.0"..<"1.0.0"), - // SpectroEELS runs its Fourier deconvolutions on ImagoCore's exact-length FFT (task 16, decision D25); 0.6.1 is the first tag with it. + // SpectroEELS runs its Fourier deconvolutions on ImagoCore's exact-length FFT; 0.6.1 is the first tag with it. .package(url: "https://github.com/mac4DSTEM/ImagoKit", "0.6.1"..<"1.0.0"), ], targets: [ // Plain-C wrappers over the four Accelerate LAPACK routines `FitLinearAlgebra` calls. ACCELERATE_NEW_LAPACK is // defined for this C target only (a `cSettings` define, which SwiftPM accepts in a versioned dependency); its public - // header includes no Accelerate. This replaces S2's `-Xcc -DACCELERATE_NEW_LAPACK` unsafe flag: no unsafe - // flags anywhere in this manifest. + // header includes no Accelerate, so the manifest needs no unsafe flag. .target( name: "CSpectroLAPACK", cSettings: [.define("ACCELERATE_NEW_LAPACK")], @@ -46,20 +41,17 @@ let package = Package( .product(name: "FormatoDM", package: "FormatoKit"), .product(name: "FormatoEMD", package: "FormatoKit"), ], - // No Debug `-O` (S2 carried the source application's unsafe `-O` flag for the Debug configuration). Measured in S3: - // Debug at -Onone is 20-50x slower on every hot path (936 MB Velox 24.5 s vs 1.05 s, proposer 956 s vs 19.8 s), - // and `@_optimize(speed)` does not help at -Onone, but a versioned dependency cannot carry unsafe flags. So the - // optimisation is the consumer's: tools/test.sh runs this package's tests with `-c release`. + // No Debug `-O`: a versioned dependency cannot carry unsafe flags. Debug at -Onone is 20-50x slower on the hot paths + // (a 936 MB Velox file 24.5 s against 1.05 s, the proposer 956 s against 19.8 s), so optimising is the consumer's + // choice; tools/test.sh runs the tests with `-c release`. swiftSettings: [ .enableUpcomingFeature("NonisolatedNonsendingByDefault"), .enableUpcomingFeature("InferIsolatedConformances"), .enableUpcomingFeature("MemberImportVisibility"), ] ), - // EELS slice 1 (docs/eels-slice-1-registration.md): the t/λ thickness map, a port of exSpy 7185a4d1's log-ratio - // method. Depends on SpectroCore only (EnergyAxis's HyperSpy rounding, FitLinearAlgebra's least squares). Slice 2 - // (docs/eels-slice-2-registration.md): the C K edge by the ratio method; the dft GOS of the C, N and O K shells - // (CC-BY-4.0, NOTICE) are its `Bundle.module` resources (bundle `SpectroKit_SpectroEELS`). + // EELS: thickness, alignment, deconvolution and core-loss edges, ported from exSpy 7185a4d1. The dft GOS tables (CC BY 4.0, + // NOTICE) are its `Bundle.module` resources (bundle `SpectroKit_SpectroEELS`). .target( name: "SpectroEELS", dependencies: ["SpectroCore", .product(name: "ImagoCore", package: "ImagoKit")], @@ -82,9 +74,7 @@ let package = Package( .enableUpcomingFeature("MemberImportVisibility"), ] ), - // The 258 tests that moved from the application the library was extracted from, by name (its test partition of 2026-10-13). They - // stay in one target across the split (the names are the contract), so it depends on both modules. At 0.4.0 the 68 reader - // tests among them moved to FormatoKit with the readers; the 190 others stay here, by name. + // The tests that came with the extraction from mac4DSTEM, most of them on SpectroEDX (issue #32 splits them by module). .testTarget( name: "SpectroCoreTests", dependencies: [ @@ -98,8 +88,7 @@ let package = Package( .enableUpcomingFeature("MemberImportVisibility"), ] ), - // SpectroKit's own tests, written after the extraction (0.2.0 on). Since 0.4.0 only the rank-threshold test: the Velox - // metadata fields, the DM4 EELS reader and the quit barrier moved to FormatoKit with their readers. + // Small package-wide tests: the CI scripts, the rank threshold, the smoothing kernel. .testTarget( name: "SpectroKitTests", dependencies: ["SpectroCore", "SpectroTestSupport"], @@ -109,7 +98,7 @@ let package = Package( .enableUpcomingFeature("MemberImportVisibility"), ] ), - // EELS slice 1's tests: pins against exSpy, truth against planted spectra (Fixtures/, tools/eels-thickness/). + // SpectroEELS: pins against exSpy, truth against planted spectra (Fixtures/, written by tools/). .testTarget( name: "SpectroEELSTests", dependencies: ["SpectroEELS", "SpectroCore", "SpectroTestSupport"], @@ -120,9 +109,7 @@ let package = Package( .enableUpcomingFeature("MemberImportVisibility"), ] ), - // EDX stage gates (docs/edx-stage-gates-registration.md): counts, energy axis and peak identification on pins (exSpy, - // HyperSpy, RosettaSciIO) and planted truth (Fixtures/, tools/edx-stages/), and the Gate D diagnosis of the Mg auto-ID - // miss. Its own target (registration Q6), so SpectroCoreTests stays the moved names (190 since 0.4.0). + // SpectroEDX: counts, energy axis and peak identification on pins (exSpy, HyperSpy, RosettaSciIO) and planted truth (Fixtures/). .testTarget( name: "SpectroEDXTests", dependencies: ["SpectroEDX", "SpectroCore", "SpectroTestSupport", .product(name: "FormatoEMD", package: "FormatoKit")], @@ -145,8 +132,8 @@ let package = Package( .enableUpcomingFeature("MemberImportVisibility"), ] ), - // The pinned comparisons: the source application's byte-for-byte golden baseline (G-golden, recorded at its 71929a01), the - // exSpy/HyperSpy/pyEELSMODEL pins and the minute-long EDX gate runs (PeakGate, Gate D). `swift test -c release --parallel + // The pinned comparisons: the byte-for-byte golden baseline, the exSpy, HyperSpy and pyEELSMODEL pins and the minute-long + // planted EDX runs. `swift test -c release --parallel // --skip SpectroKitParityTests` is the per-session gate; the plain `swift test -c release --parallel` runs both. .testTarget( name: "SpectroKitParityTests", diff --git a/Sources/CSpectroLAPACK/include/CSpectroLAPACK.h b/Sources/CSpectroLAPACK/include/CSpectroLAPACK.h index 7293b01..adc240c 100644 --- a/Sources/CSpectroLAPACK/include/CSpectroLAPACK.h +++ b/Sources/CSpectroLAPACK/include/CSpectroLAPACK.h @@ -2,7 +2,7 @@ // CSpectroLAPACK.h // Plain-C wrappers over the Accelerate LAPACK routines SpectroCore calls: the four of `FitLinearAlgebra` (SpectroKit S3, // replacing a `-DACCELERATE_NEW_LAPACK` unsafe compiler flag), `dgelsd_` (SciPy's least squares, for the Savitzky-Golay port) and `dgbsv_` (SciPy's -// interpolating spline, EELS slice 2). This header includes no Accelerate: Swift sees only `int` and `double *`, so the +// interpolating spline). This header includes no Accelerate: Swift sees only `int` and `double *`, so the // library needs neither `import Accelerate` nor the `-DACCELERATE_NEW_LAPACK` unsafe flag. The define lives in // lapack_shim.c, the only file that sees Accelerate's LAPACK declarations. // diff --git a/Sources/SpectroCore/Spectroscopy/Fit/BandedLinearAlgebra.swift b/Sources/SpectroCore/Spectroscopy/Fit/BandedLinearAlgebra.swift index d903fea..5cae072 100644 --- a/Sources/SpectroCore/Spectroscopy/Fit/BandedLinearAlgebra.swift +++ b/Sources/SpectroCore/Spectroscopy/Fit/BandedLinearAlgebra.swift @@ -2,7 +2,7 @@ // BandedLinearAlgebra.swift // Role: A general banded linear solve on Accelerate's LAPACK `dgbsv_` (LU with partial pivoting), the routine SciPy 1.18.1's // `make_interp_spline` calls for its collocation system (`interpolate/_bsplines.py:1898-1909`). For SpectroEELS's -// interpolating spline (EELS slice 2); SpectroEDX does not use it. +// interpolating spline; SpectroEDX does not use it. // internal import CSpectroLAPACK diff --git a/Sources/SpectroCore/Spectroscopy/Fit/LeastSquaresFit.swift b/Sources/SpectroCore/Spectroscopy/Fit/LeastSquaresFit.swift index 6d7156d..54fe38c 100644 --- a/Sources/SpectroCore/Spectroscopy/Fit/LeastSquaresFit.swift +++ b/Sources/SpectroCore/Spectroscopy/Fit/LeastSquaresFit.swift @@ -1,7 +1,7 @@ // // LeastSquaresFit.swift // Role: Unweighted least squares with non-negative line areas: the NAMED DEFAULT of the -// spectrum fit (ADR 054, V5-Q1). Every result carries `LeastSquaresFit.label`. +// spectrum fit. Every result carries `LeastSquaresFit.label`. // // Lawson and Hanson, "Solving Least Squares Problems" (1974), Algorithm 23.10 (NNLS), with // the passive-set subproblem solved by Accelerate's QR-based `dgelsy_` (see @@ -14,14 +14,14 @@ // DEVIATION (eXSpy/hyperspy): `Model1D.fit` with its default "ls" loss is an UNBOUNDED // Levenberg-Marquardt on the line areas (`ext_force_positive` acts only with `bounded=True`), // and it stops short of the least-squares minimum: on EDS_TEM_FePt_nanoparticles it returns -// 2800.0213 / 14921.183 where the exact minimiser is 2800.2296 / 14920.805 (docs/archive/v5 fixture, -// 7e-5 and 3e-5 relative). Here the minimum is exact and areas cannot go negative. +// 2800.0213 / 14921.183 where the exact minimiser is 2800.2296 / 14920.805 (7e-5 and 3e-5 +// relative). Here the minimum is exact and areas cannot go negative. // import Foundation package nonisolated enum LeastSquaresFit { - /// Printed beside every least-squares result (pre-registration flag 4). + /// Printed beside every least-squares result. package static let label = "least squares, unweighted" package struct Solution: Sendable, Equatable { diff --git a/Sources/SpectroCore/Spectroscopy/Fit/PoissonMLFit.swift b/Sources/SpectroCore/Spectroscopy/Fit/PoissonMLFit.swift index 36ba6a7..578acde 100644 --- a/Sources/SpectroCore/Spectroscopy/Fit/PoissonMLFit.swift +++ b/Sources/SpectroCore/Spectroscopy/Fit/PoissonMLFit.swift @@ -2,7 +2,7 @@ // PoissonMLFit.swift // Role: Poisson maximum likelihood on the same linear design as the least-squares fit, by // iteratively reweighted least squares (Fisher scoring) with non-negative line areas. It sits -// under Expert (ADR 054 V5-Q1); the cap and tolerance are recorded in every result. +// under Expert; the cap and tolerance are recorded in every result. // // The model is mu = D theta + offset (identity link), the data y_i ~ Poisson(mu_i). The score is // s_j = sum_i D_ij (y_i - mu_i) / mu_i and the expected information F_jk = sum_i D_ij D_ik / mu_i, so @@ -18,7 +18,7 @@ // infinite weight or an infinite deviance. The number of clamped channels is reported. // // Convergence: relative decrease of D below `tolerance` or `maxIterations` reached. -// The cap and tolerance are chosen by the session (pre-registration: "decides alone, recorded in the method"). +// The cap and tolerance are chosen by the session. // import Foundation diff --git a/Sources/SpectroCore/Spectroscopy/MapSmoothing.swift b/Sources/SpectroCore/Spectroscopy/MapSmoothing.swift index dccfae7..acd8b20 100644 --- a/Sources/SpectroCore/Spectroscopy/MapSmoothing.swift +++ b/Sources/SpectroCore/Spectroscopy/MapSmoothing.swift @@ -5,7 +5,7 @@ // A count-conserving spatial filter for element maps (Velox 3.15 "pre-filter": Average or Gaussian, "counts are // redistributed and the total is conserved", UM pp. 64-68). Here it is display only: the controller applies it to the signed // net-count map before the display clamp, the raw map stays what the file gave, the export writes the raw map, and every -// tile names the kernel. Nothing a quantification reads passes through it (ADR 054: at% on pooled spectra only). +// tile names the kernel. Nothing a quantification reads passes through it: at% is computed on pooled spectra only. // // Conservation: a normalised kernel conserves the total wherever its whole window is inside the map; at the border the // weights are renormalised over the in-bounds taps, so a flat map stays flat to the edge (no dark rim, the mean per pixel is diff --git a/Sources/SpectroCore/Spectroscopy/SpectroCancellation.swift b/Sources/SpectroCore/Spectroscopy/SpectroCancellation.swift index 29e5d05..19f8a6a 100644 --- a/Sources/SpectroCore/Spectroscopy/SpectroCancellation.swift +++ b/Sources/SpectroCore/Spectroscopy/SpectroCancellation.swift @@ -1,6 +1,6 @@ // // SpectroCancellation.swift -// The one cancellation contract of the long spectroscopy calls (ADR 064, amendment (2) §2): a `cancelled` closure with the +// The one cancellation contract of the long spectroscopy calls: a `cancelled` closure with the // default `SpectroCancellation.task`, an optional `progress` closure (fraction 0…1), and `SpectroCancelled` thrown when the // closure answers true — nothing partial is kept. // diff --git a/Sources/SpectroCore/Spectroscopy/SpectrumImage.swift b/Sources/SpectroCore/Spectroscopy/SpectrumImage.swift index cdeb24e..c51aebe 100644 --- a/Sources/SpectroCore/Spectroscopy/SpectrumImage.swift +++ b/Sources/SpectroCore/Spectroscopy/SpectrumImage.swift @@ -7,8 +7,8 @@ // the Velox event store. // // Counts are integers and every sum here is an exact `UInt64` sum, so a window map -// from the sparse store and from the same data densified is identical bit for bit -// (pre-registration C2): the only floating-point step is the net-count formula in +// from the sparse store and from the same data densified is identical bit for bit: +// the only floating-point step is the net-count formula in // `WindowIntensity`, which both paths share. // // Joining Velox (lane A, `VeloxEventStore`, not in this tree): it already has @@ -40,7 +40,7 @@ public nonisolated protocol SpectrumImage: Sendable { /// (outer index = range, inner = pixel, row-major). Ranges are half-open and may overlap. func windowSums(_ ranges: [Range]) -> [[UInt64]] - /// The same sums, cancellable (ADR 064 amendment 2 §2): `cancelled` is asked once per block of pixels, never per value, and + /// The same sums, cancellable: `cancelled` is asked once per block of pixels, never per value, and /// `SpectroCancelled` is thrown with nothing kept; `progress` is monotone in 0...1 and ends at 1. A conformer that does not /// implement it gets the default below (one check before the plain call). The default `cancelled` of the concrete types /// and of `ElementWindows` is `SpectroCancellation.task`; a protocol requirement cannot carry it, so a caller through diff --git a/Sources/SpectroCore/Spectroscopy/SpectrumImageOpening.swift b/Sources/SpectroCore/Spectroscopy/SpectrumImageOpening.swift index 61152f1..e845aa9 100644 --- a/Sources/SpectroCore/Spectroscopy/SpectrumImageOpening.swift +++ b/Sources/SpectroCore/Spectroscopy/SpectrumImageOpening.swift @@ -61,7 +61,7 @@ public nonisolated enum SpectrumImageOpener { } /// Opens a file of a spectrum kind. For `.other` it throws `notASpectrumImage`. - /// Cancellable (ADR 064 amendment 2 §2): `SpectroCancelled` at a step boundary with nothing kept; `progress` 0...1, ends at 1. + /// Cancellable: `SpectroCancelled` at a step boundary with nothing kept; `progress` 0...1, ends at 1. public static func open(path: String, kind: SpectrumFileKind, cancelled: @Sendable () -> Bool = SpectroCancellation.task, progress: (@Sendable (Double) -> Void)? = nil) throws -> LoadedSpectrumImage { diff --git a/Sources/SpectroCore/Spectroscopy/SpectrumImageSource.swift b/Sources/SpectroCore/Spectroscopy/SpectrumImageSource.swift index 642601b..0da294f 100644 --- a/Sources/SpectroCore/Spectroscopy/SpectrumImageSource.swift +++ b/Sources/SpectroCore/Spectroscopy/SpectrumImageSource.swift @@ -6,8 +6,8 @@ // protocol carries dimensions, axis, metadata and the compute (decision of lane R, taken by R2). // // The metadata is the file's own, as read. Live and real times are stored as recorded and are NOT interpreted: -// their meaning differs by file (cumulative in one Velox file, per frame in another, 0 on the owner's: -// formats.md §2.2), so nothing computes with them. +// their meaning differs by file (cumulative in one Velox file, per frame in another, +// 0 in a third), so nothing computes with them. // import Foundation @@ -46,7 +46,7 @@ public nonisolated struct SpectrumImageMetadata: Equatable, Sendable { /// Energy channels per spectrum. public var channelCount: Int /// The energy axis as the file states it: E(i) = offset + i · dispersion. Refinement on the pooled spectrum - /// (ADR 054 item 4) is shown beside it, never written over it. + /// is shown beside it, never written over it. public var energyOffsetEV: Double public var energyDispersionEV: Double /// Real-space pixel size, when the file carries one. diff --git a/Sources/SpectroCore/Spectroscopy/Statistics/CountingInterval.swift b/Sources/SpectroCore/Spectroscopy/Statistics/CountingInterval.swift index 5a40289..7231970 100644 --- a/Sources/SpectroCore/Spectroscopy/Statistics/CountingInterval.swift +++ b/Sources/SpectroCore/Spectroscopy/Statistics/CountingInterval.swift @@ -1,8 +1,8 @@ // // CountingInterval.swift // Role: Poisson counting statistics for window counts: the Garwood exact -// interval at EVERY count (ADR 054 addendum: the "~20 counts" switch was a -// threshold and is dropped), and the net count N = G - s*B with +// interval at EVERY count (no switch to a normal approximation at some +// count), and the net count N = G - s*B with // Var = G + s^2*B. Inputs are plain numbers (gross G, background B, scale // s = signal-window / background-window channel ratio), so the layer does // not depend on the spectrum-image types. diff --git a/Sources/SpectroCore/Spectroscopy/Statistics/LiveTimeNormalisation.swift b/Sources/SpectroCore/Spectroscopy/Statistics/LiveTimeNormalisation.swift index 6eb3314..9f77e1b 100644 --- a/Sources/SpectroCore/Spectroscopy/Statistics/LiveTimeNormalisation.swift +++ b/Sources/SpectroCore/Spectroscopy/Statistics/LiveTimeNormalisation.swift @@ -1,7 +1,7 @@ // // LiveTimeNormalisation.swift -// Role: Put regions with different dead time on a common footing (ADR 054, -// V5-Q5). Route 1: per-pixel live time, when the file carries it, gives a +// Role: Put regions with different dead time on a common footing. +// Route 1: per-pixel live time, when the file carries it, gives a // rate = net counts / summed live time. Route 2: no live time, so the net // count of a line is expressed relative to the Al K-alpha net count of the // same region (the internal reference). The result NAMES the route. diff --git a/Sources/SpectroCore/Spectroscopy/Statistics/SigmaTerms.swift b/Sources/SpectroCore/Spectroscopy/Statistics/SigmaTerms.swift index e42eec0..0817878 100644 --- a/Sources/SpectroCore/Spectroscopy/Statistics/SigmaTerms.swift +++ b/Sources/SpectroCore/Spectroscopy/Statistics/SigmaTerms.swift @@ -6,7 +6,7 @@ // protocol that the fit lane conforms to, and the delta-method ratio // variance it feeds. // -// k enters a Cliff-Lorimer ratio as k_A/k_B (ADR 054 addendum, flag 5), i.e. as +// k enters a Cliff-Lorimer ratio as k_A/k_B, i.e. as // sqrt(sigma_kA^2 + sigma_kB^2) for independent k-factors (20 % each gives ~28 %). // In a RATIO OF RATIOS of two regions both ratios use the same k_A/k_B, so the // k term cancels exactly and `ratioOfRatios` carries it as 0. diff --git a/Sources/SpectroEDX/SpectroEDX.docc/Quantifying-an-EDX-spectrum.md b/Sources/SpectroEDX/SpectroEDX.docc/Quantifying-an-EDX-spectrum.md index 5fa503e..bcc719f 100644 --- a/Sources/SpectroEDX/SpectroEDX.docc/Quantifying-an-EDX-spectrum.md +++ b/Sources/SpectroEDX/SpectroEDX.docc/Quantifying-an-EDX-spectrum.md @@ -34,7 +34,7 @@ func proposeElements(counts: [UInt64], axis: EnergyAxis, beamEnergyKeV: Double) ## Record the method A ``QuantificationMethod`` holds every choice the number depends on: the estimator, the background, the k-factor source, the -absorption correction, the elements and their roles. Its defaults are the registered ones. +absorption correction, the elements and their roles. Its defaults are listed on each property. ```swift func method(for elements: [String], beamEnergyKeV: Double?) -> QuantificationMethod { diff --git a/Sources/SpectroEDX/Spectroscopy/Fit/Continuum.swift b/Sources/SpectroEDX/Spectroscopy/Fit/Continuum.swift index 5844d35..a334618 100644 --- a/Sources/SpectroEDX/Spectroscopy/Fit/Continuum.swift +++ b/Sources/SpectroEDX/Spectroscopy/Fit/Continuum.swift @@ -1,11 +1,11 @@ // // Continuum.swift // Role: The default background of the spectrum fit: an empirical whole-spectrum continuum -// with an absorption step at the Al K edge, fitted jointly with the lines (ADR 054 -// V5-Q2). It sits in the SAME linear design as the lines (free-sign coefficients), so +// with an absorption step at the Al K edge, fitted jointly with the lines. +// It sits in the SAME linear design as the lines (free-sign coefficients), so // lines and continuum are fitted together and no separate background pass exists. // -// FUNCTIONAL FORM (goes to Fable's review): +// FUNCTIONAL FORM: // // B(E) = K(E) * P_s(E), K(E) = (E0 - E) / E, // P_s(E) = sum_k c_{s,k} b_{k,n_s}(t), t = (E - lo_s) / (hi_s - lo_s), c_{s,k} >= 0, @@ -37,8 +37,8 @@ // generator, a different functional form from the fit), orders (5, 4) left a systematic misfit of up to // 10 sigma at 0.2-0.5 keV that leaked to ~2.5 sigma into 1.0-1.5 keV (F3 refuted: 2 of 30 seeds had all // |r| <= 3 in 1.1-1.9 keV, against 24 of 30 for the true model); (8, 5) gave 22, (9, 5) 25, (10, 5) 26. -// The roll-off of the owner's Super-X is not known here: the continuum's acceptance is Q2 on his pooled -// spectra, and the orders are exposed so that measurement can move them. +// The roll-off of a given Super-X detector is not known here: the orders are exposed so that a measurement +// on real pooled spectra can move them. // import Foundation @@ -72,7 +72,7 @@ public nonisolated struct ContinuumForm: Sendable, Equatable { package var nonNegative: Bool /// Round 2: an optional detector efficiency epsilon(E) multiplying the whole form (B = K x epsilon x P_s), so the detector /// roll-off is modelled and P_s only carries what epsilon does not. nil: the round-1 form. The model is generic - /// (`DetectorEfficiency`), not the owner's Super-X calibration. + /// (`DetectorEfficiency`), not a measured Super-X calibration. package var efficiency: (any DetectorEfficiency)? package static let alKEdge = FitPolicy.alKEdgeKeV @@ -140,7 +140,7 @@ public nonisolated struct ContinuumForm: Sendable, Equatable { return "continuum: Kramers\(eps)\u{00D7}\(nonNegative ? "Bernstein" : "poly")(\(ord)), \(split); orders chosen on synthetic data" } - /// Badge text the room shows beside any weak-line result (registered criterion 3: no form met |bias| <= 1 se at 0.3 % + /// Badge text the room shows beside any weak-line result (no form met |bias| <= 1 se at 0.3 % /// on both generators, so the bias is stated, never asserted away). Numbers: `testWeakLineRecoveryRegisteredSelection`, /// 30 seeds, Al K-alpha ~3e5 counts pooled, default form Kramers (9,5): the numbers below are that test's run. /// Wording after the tail experiment: supplying the true Al K-alpha tail as a reference shape on generator S takes the diff --git a/Sources/SpectroEDX/Spectroscopy/Fit/EDSFit.swift b/Sources/SpectroEDX/Spectroscopy/Fit/EDSFit.swift index 0e7c487..b9fc43d 100644 --- a/Sources/SpectroEDX/Spectroscopy/Fit/EDSFit.swift +++ b/Sources/SpectroEDX/Spectroscopy/Fit/EDSFit.swift @@ -38,7 +38,7 @@ public nonisolated struct FitSettings: Sendable, Equatable { self.lineTail = lineTail } - /// The upper end of the default fitted range, keV (WP3c, docs/archive/v5/wp3c-fit-range-preregistration-2026-10-06.md). + /// The upper end of the default fitted range, keV. /// 20 keV is the width the continuum's orders (9, 5) were measured on (`Continuum.swift`; every EDS fixture before WP3c ends at /// 19.997-20.03 keV; the 80 keV realistic-L fixture was added to test this rule): a property of those spectra, stated, not of the method. Fitting the same form to the end of an 80 keV /// axis biased a weak line by -8 % (Ti, realistic-L synthetic, z -2.28); every limit from 11 to 60 keV was unbiased (E1). @@ -54,9 +54,9 @@ public nonisolated struct FitSettings: Sendable, Equatable { /// The default fit: continuum with the Al K step, `policy.fitFromKeV` (0.2 keV) to `defaultFitTo` (20 keV, or the beam energy or the axis end /// when lower), escape peaks on, no reference shapes, unweighted least squares. The 0.2 keV start keeps the /// zero-energy noise peak out (a stated choice, not a measurement). - /// `efficiency`: the detector model of the continuum (`SDDEfficiency` for the registered default, see - /// `ContinuumForm.weakLineBiasNote`); nil gives the Kramers-only form, which IS the default: the registered selection (report2) had no tie - /// clause, 397 (with eps) vs 402 (without) counts at se ~33 is unresolved, so the no-filler rule decides: eps would add a generic EPQ SDD + /// `efficiency`: the detector model of the continuum (`SDDEfficiency` for the detector model, see + /// `ContinuumForm.weakLineBiasNote`); nil gives the Kramers-only form, which IS the default: the selection run had no tie + /// rule, 397 (with eps) vs 402 (without) counts at se ~33 is unresolved, so the no-filler rule decides: eps would add a generic EPQ SDD /// model to every footer for zero measured gain. eps stays an option. /// `policy` (default `FitPolicy.default`): the start and upper limit of the range, the continuum's orders, edges and basis, and the /// escape columns. Its `continuumEfficiency` is realised by the caller through `efficiency` (it needs the mass-absorption table). diff --git a/Sources/SpectroEDX/Spectroscopy/Fit/EDSModel.swift b/Sources/SpectroEDX/Spectroscopy/Fit/EDSModel.swift index 0d353e1..d907132 100644 --- a/Sources/SpectroEDX/Spectroscopy/Fit/EDSModel.swift +++ b/Sources/SpectroEDX/Spectroscopy/Fit/EDSModel.swift @@ -78,7 +78,7 @@ public nonisolated enum EDSLineWeights: Sendable, Equatable { /// eXSpy's `xray_lines.json` weights: the quantification fit (parity, D17). case exspy /// The identification fit (docs/design/auto-id-tem.md): the K family's Kβ from NIST EPQ `LineWeights.csv` (public domain), every K - /// transition other than K-L2 and K-L3 over those two (Al 0.0315 against eXSpy's 0.0132; the owner's 1339 and 1840 sums read 0.031 and + /// transition other than K-L2 and K-L3 over those two (Al 0.0315 against eXSpy's 0.0132; two measured Al-alloy map sums read 0.031 and /// 0.034 with Kβ free). L and M families keep eXSpy's weights (EPQ's are per subshell, with no clean map onto eXSpy's lines). case epqK } diff --git a/Sources/SpectroEDX/Spectroscopy/Fit/EnergyAxisRefinement.swift b/Sources/SpectroEDX/Spectroscopy/Fit/EnergyAxisRefinement.swift index 044914d..ece7801 100644 --- a/Sources/SpectroEDX/Spectroscopy/Fit/EnergyAxisRefinement.swift +++ b/Sources/SpectroEDX/Spectroscopy/Fit/EnergyAxisRefinement.swift @@ -1,6 +1,6 @@ // // EnergyAxisRefinement.swift -// Role: Refine the energy axis on the POOLED spectrum (ADR 054 V5-Q4): an offset correction, a +// Role: Refine the energy axis on the POOLED spectrum: an offset correction, a // relative gain correction and the detector resolution (FWHM at Mn K-alpha), keeping the // file's values beside the refined ones. // diff --git a/Sources/SpectroEDX/Spectroscopy/Fit/ReferenceShapes.swift b/Sources/SpectroEDX/Spectroscopy/Fit/ReferenceShapes.swift index dba1363..bfda656 100644 --- a/Sources/SpectroEDX/Spectroscopy/Fit/ReferenceShapes.swift +++ b/Sources/SpectroEDX/Spectroscopy/Fit/ReferenceShapes.swift @@ -1,7 +1,7 @@ // // ReferenceShapes.swift -// Role: The slot for components MEASURED once on a pure sample and tied to the fit (ADR 054 -// V5-Q7, corrected by flag 2). It ships EMPTY and every result says "reference shapes: none". +// Role: The slot for components MEASURED once on a pure sample and tied to the fit. +// It ships EMPTY and every result says "reference shapes: none". // // Two ties exist, because the physics differs: // * `.lineAmplitude`: the Al K-alpha incomplete-charge tail follows Al K-alpha, so its measured diff --git a/Sources/SpectroEDX/Spectroscopy/Method/ProposerPolicy.swift b/Sources/SpectroEDX/Spectroscopy/Method/ProposerPolicy.swift index 60814d3..8d1ac07 100644 --- a/Sources/SpectroEDX/Spectroscopy/Method/ProposerPolicy.swift +++ b/Sources/SpectroEDX/Spectroscopy/Method/ProposerPolicy.swift @@ -1,7 +1,7 @@ // // ProposerPolicy.swift // Role: The choices behind the element proposer and the unlisted-line check, as values with the library's choice as the -// default. Registered: docs/generality-0.5.0-registration.md (E1-E11, U1-U3). +// default (0.5.0). // import Foundation diff --git a/Sources/SpectroEDX/Spectroscopy/Method/QuantificationMethod.swift b/Sources/SpectroEDX/Spectroscopy/Method/QuantificationMethod.swift index 9e00e48..cb67bec 100644 --- a/Sources/SpectroEDX/Spectroscopy/Method/QuantificationMethod.swift +++ b/Sources/SpectroEDX/Spectroscopy/Method/QuantificationMethod.swift @@ -1,15 +1,14 @@ // // QuantificationMethod.swift -// Role: The quantification method as ONE value (v5.0 WP3 lane M, ADR 054 items 1-3, 6, 9 and the -// addendum): estimator, background, element roles, where k comes from, the absorption -// correction and the thickness. Nothing here computes a number; the fit (lane F), the k-factors -// and absorption (lane K) and the statistics (lane T) read it. +// Role: The quantification method as ONE value: estimator, background, element roles, where k comes from, the absorption +// correction and the thickness. Nothing here computes a number; the fit, the k-factors +// and absorption and the statistics read it. // // Codable with sorted keys, so encoding a decoded method gives the same bytes (`canonicalJSON`), and // `hash` is the SHA-256 of those bytes: two methods are the same method exactly when their hashes // are equal. The replay step "quantification" (`QuantificationStep`) records the JSON and the hash. // -// The defaults are ADR 054's and are those `SpectroscopySession.method` always had: least squares, +// The defaults: least squares, // unweighted; empirical continuum with the Al K-edge step; computed k; four-detector absorption on; // no thickness assumed. // @@ -55,7 +54,7 @@ public nonisolated struct QuantificationMethod: Codable, Equatable, Sendable { /// The element symbol, e.g. "Mg". public var symbol: String public var role: ElementRole - /// Picked by the user; the proposer never drops a manual pick (ADR 054). + /// Picked by the user; the proposer never drops a manual pick. package var isManual: Bool public var id: String { symbol } @@ -123,8 +122,8 @@ public nonisolated struct QuantificationMethod: Codable, Equatable, Sendable { public var beamEnergyKeV: Double? /// Expert: the order of the whole-range polynomial background (`.wholeRangePolynomial6`); nil is 6, eXSpy's. public var polynomialOrder: Int? - /// Expert: keep the file's energy axis instead of refining offset, gain and width on the pooled spectrum (ADR 054 - /// item 4); nil is "refine". + /// Expert: keep the file's energy axis instead of refining offset, gain and width on the pooled spectrum; + /// nil is "refine". public var lockEnergyAxis: Bool? /// Expert (WP3c): the upper end of the fitted range, keV; nil is the default, min(axis end, beam energy, 20 keV) /// (`FitSettings.defaultFitTo`). A typed value past the axis end or the beam energy is clamped to them, and the footer says so. diff --git a/Sources/SpectroEDX/Spectroscopy/Proposer/ElementProposer.swift b/Sources/SpectroEDX/Spectroscopy/Proposer/ElementProposer.swift index 80efb99..0cdb0d4 100644 --- a/Sources/SpectroEDX/Spectroscopy/Proposer/ElementProposer.swift +++ b/Sources/SpectroEDX/Spectroscopy/Proposer/ElementProposer.swift @@ -1,7 +1,7 @@ // // ElementProposer.swift -// Role: Propose elements from a pooled spectrum. It RETURNS candidates and never touches an element list: nothing is added silently -// (ADR 054, V5-Q6). Since 0.11.0 the search is the identification loop of docs/design/auto-id-tem.md, written from the published +// Role: Propose elements from a pooled spectrum. It RETURNS candidates and never touches an element list: nothing is added silently. +// Since 0.11.0 the search is the identification loop of docs/design/auto-id-tem.md, written from the published // method (Newbury and Ritchie's automatic qualitative analysis, as AutoEMX documents its defaults; no code copied): // // 1. fit the listed elements (the landed `EDSFit`: Kramers x Bernstein continuum with the Al K step, Si-escape columns, unweighted @@ -21,7 +21,7 @@ // candidate with `possibleReason` (or `familyNote` for the confirmation rule) and is never proposed. // // What went (0.11.0): the all-candidate joint fit with its L_C pruning and single-candidate forward step, and the flank-misfit factor that -// scaled a candidate's sigma_0 by its neighbour's misfit (x33 for Mg beside Al on the owner's file 1339, where Mg's own window fitted to +// scaled a candidate's sigma_0 by its neighbour's misfit (x33 for Mg beside Al on a measured Al-alloy map, where Mg's own window fitted to // ±3 σ). The 1 % model term does that job in the published way. // Not yet: the method's overlap check (elements whose every peak sits under a modelled one); such an element leaves no residual peak and // is not reported. An open item, not a proposal changer: the method reports its finds as possible only. @@ -310,7 +310,7 @@ public nonisolated struct ElementProposer: Sendable { /// - settings: the fit as the room runs it. `elements` is the CURRENT set (always fitted, never changed); /// range, background, escape setting and reference shapes are used as given, the method is forced to LS. /// - holeCounts: optional pooled spectrum of a hole region on the same axis, for the Cu grid check. - /// - cancelled: asked before every round of fits (ADR 064 amendment 2 §2); default Swift task cancellation. The proposer keeps + /// - cancelled: asked before every round of fits; default Swift task cancellation. The proposer keeps /// its own `ProposerError.cancelled`; nothing partial is returned. /// - progress: monotone in 0...1, ends at 1. The number of fits is not known in advance, so it is fits done over fits done plus the /// fits the current step still has planned, clamped never to fall. @@ -521,7 +521,7 @@ public nonisolated struct ElementProposer: Sendable { if !k.key.hasPrefix("sum:") && found[k.key] == nil { trials[k.key] = trial } } // The candidate that explains THIS peak best joins (its gain within one FWHM of the peak): a heavy element whose other families - // also soak up misfits elsewhere must not take a peak that a K line sits on exactly (W Mα took Si Kα on the owner's 1456 by total gain). + // also soak up misfits elsewhere must not take a peak that a K line sits on exactly (W Mα took Si Kα on a measured map by total gain). guard let best = eligible.max(by: { $0.peakGain < $1.peakGain }), let p = best.pass else { continue } let rivals = eligible.filter { $0.key != best.key && $0.peakGain >= best.peakGain - t.ambiguityGain }.map(\.key) for e in eligible where e.key != best.key && !e.key.hasPrefix("sum:") { diff --git a/Sources/SpectroEDX/Spectroscopy/Proposer/LineConflicts.swift b/Sources/SpectroEDX/Spectroscopy/Proposer/LineConflicts.swift index 634a7ef..0b1c810 100644 --- a/Sources/SpectroEDX/Spectroscopy/Proposer/LineConflicts.swift +++ b/Sources/SpectroEDX/Spectroscopy/Proposer/LineConflicts.swift @@ -9,7 +9,7 @@ // itself and its covariance, so an overlap is no longer a reason to doubt a proposal; what remains are the causes a // fit cannot tell apart from an element: a pile-up peak, a contaminant from the preparation, a grid. // Sum peaks are no longer a fixed list: every pair of detected parents is a candidate sum energy. -// Sources: docs/archive/v5/edx-dossier-2026-10-05.md section 6 (spurious peaks), ADR 054 (V5-Q6). Energies in keV. +// Energies in keV. // import Foundation @@ -26,7 +26,7 @@ public nonisolated struct LineConflict: Equatable, Sendable { package let energiesKeV: [Double] /// Elements that must be detected for the conflict to apply (a sum peak's parents). package let requires: [String] - /// The question shown to the owner. + /// The question shown to the user. public let question: String /// What can settle it. package let remedy: String diff --git a/Sources/SpectroEDX/Spectroscopy/Proposer/ProposalRules.swift b/Sources/SpectroEDX/Spectroscopy/Proposer/ProposalRules.swift index 8f23770..5bcef4d 100644 --- a/Sources/SpectroEDX/Spectroscopy/Proposer/ProposalRules.swift +++ b/Sources/SpectroEDX/Spectroscopy/Proposer/ProposalRules.swift @@ -1,6 +1,6 @@ // // ProposalRules.swift -// Role: The three cuts WP4 pre-registered on 2026-10-07 (docs/archive/v5/wp4-autoid-velox-preregistration-2026-10-07.md), as a +// Role: Three cuts on the proposer's picks (fixed on 2026-10-07 before they were measured), as a // separate, named post-selection step on a `ProposalResult`. The proposer's own output is never altered: `apply` returns // a `RuledProposal` that holds the raw result beside the picks, so the room can say which rule picked or withheld what. // @@ -10,11 +10,10 @@ // R3 release a candidate the proposer held back as a sum-peak question is released when net / L_D >= 10, and then // meets R1 and R2 like any other pick. // -// The cuts (3, 10 L_D, 1.5 FWHM) were read off 78 files of ONE operator, one detector, mostly Al-Mg-Si; CLAUDE.md: a threshold is -// a property of the dataset until measured on every dataset it will touch. `UNVALIDATED` stays on the Auto ID row; the hold-out -// and the demo-edx ladder are reported in docs/archive/v5/wp4-autoid-results-2026-10-07.md. A rule that did not hold there is -// switched off in `shipped`, not retuned (a changed cut is a new registration): R1's actinide half ships, R1's beside-K half -// is held by the independent refuter (2026-10-07): the sample never met a true L/M element beside a proposed K, and the line +// The cuts (3, 10 L_D, 1.5 FWHM) were read off 78 files of ONE operator, one detector, mostly Al-Mg-Si; a threshold is a +// property of the dataset until measured on every dataset it will touch, so `UNVALIDATED` stays on the Auto ID row. A rule +// that did not hold on the hold-out files and the synthetic dose ladder is switched off in `shipped`, not retuned: R1's +// actinide half ships; R1's beside-K half is off because an independent review found that the sample never met a true L/M element beside a proposed K, and the line // table has pairs where it would drop a real one (Hf Lα 1.0 FWHM from Cu Kα on a Cu grid, Pt Lα 1.2 from Ga Kα on a FIB // lamella, Pb Lα 0.04 from As Kα); the proposer keeps one group per element, so the element would vanish. R2 and R3 are off. // @@ -37,9 +36,9 @@ public nonisolated struct ProposalRules: Equatable, Sendable { public var hygiene: Bool /// R1's second half, separately switchable: the beside-K drop. Off in `shipped` (see the header). public var besideK: Bool - /// WP4b (docs/archive/v5/wp4b-autoid-rules-preregistration-2026-10-07.md), an OPTION that is off in `none`, `registered` and `shipped`: + /// An OPTION that is off in `none`, `registered` and `shipped`: /// with `besideK`, an L or M pick beside a proposed K alpha is dropped only when its net / L_D is at or below that K candidate's; - /// a stronger L/M stays. Measured, not shipped: turning it on anywhere is the registration's ship rule, not this flag's existence. + /// a stronger L/M stays. Measured, not shipped (see `shipped`). public var besideKGuard: Bool public var corroboration: Bool public var release: Bool @@ -58,18 +57,17 @@ public nonisolated struct ProposalRules: Equatable, Sendable { /// No rule: `apply` returns the proposer's own picks. public static let none = ProposalRules(hygiene: false, corroboration: false, release: false) - /// The three cuts exactly as registered, 2026-10-07. + /// The three cuts as fixed on 2026-10-07. public static let registered = ProposalRules(hygiene: true, corroboration: true, release: true) - /// What the room applies: R1 only. R3 was refuted on the demo-edx dose ladder (the Al+Al sum peak at 100 counts/px, 19.9 x L_D, - /// was released as Ar, an element absent from truth) and R2 missed its hold-out bar (precision 38.7 % under 45 % on the nine June - /// 2026 files); per the registration a cut that does not hold ships as the shown quantity (net / L_D beside each pick) and no - /// rule. docs/archive/v5/wp4-autoid-results-2026-10-07.md. Changing a cut or turning a rule back on is a new registration. - /// The beside-K half of R1 is off too (the refuter's finding above). WP4b (ADR 062) tested it with the evidence guard on true + /// What the room applies: R1 only. R3 failed on the synthetic dose ladder (the Al+Al sum peak at 100 counts/px, 19.9 x L_D, + /// was released as Ar, an element absent from truth) and R2 missed its hold-out bar (precision 38.7 % under 45 % on nine + /// hold-out files); a cut that does not hold ships as the shown quantity (net / L_D beside each pick) and no rule. + /// The beside-K half of R1 is off too (the review finding above). A later test with the evidence guard on true /// L/M + K pairs and refuted it: an L family's L_D is larger than a nearby K line's at equal area, so the guard still drops true Hf /// beside Cu and Pt beside Ga. R2 was refuted in-sample there too. A new beside-K test needs a different evidence term. public static let shipped = ProposalRules(hygiene: true, besideK: false, corroboration: false, release: false) /// Where the cuts come from (said in the notes, never hidden). - public static let source = "registered 2026-10-07 (WP4), cuts read off 78 Velox-session files: unvalidated" + public static let source = "fixed 2026-10-07, cuts read off 78 Velox-session files: unvalidated" public var isNone: Bool { !hygiene && !besideK && !corroboration && !release } // besideKGuard alone acts only with besideK public nonisolated struct Withheld: Equatable, Sendable { diff --git a/Sources/SpectroEDX/Spectroscopy/Quant/BrownPowellK.swift b/Sources/SpectroEDX/Spectroscopy/Quant/BrownPowellK.swift index a39660c..d3a1f41 100644 --- a/Sources/SpectroEDX/Spectroscopy/Quant/BrownPowellK.swift +++ b/Sources/SpectroEDX/Spectroscopy/Quant/BrownPowellK.swift @@ -7,9 +7,9 @@ // STATUS (round 2, 2026-10-05): the shipped cross-section is Bote-Salvat 2008 // (BoteSalvatCrossSection, from NIST EPQ), omega is Krause 1979 // (KrauseFluorescenceYield) and epsilon the generic SDD model (SDDEfficiency). There -// is NO Brown-Powell here -- no licence-clean source exists (ADR 054 addendum) -- so +// is NO Brown-Powell here -- no licence-clean source exists -- so // nothing in this file is called Brown-Powell and Velox parity is not claimed. The -// file name is kept from the WP3 plan. Any model that conforms to +// file name is historical. Any model that conforms to // `IonisationCrossSection` / `FluorescenceYield` plugs in. // // The k algebra below is derived, not ported. For a thin film the line intensity @@ -18,8 +18,7 @@ // k_i = A_i / (sigma_i omega_i a_i eps(E_i)) (per element, up to one // common factor; `kFactors(reference:)` normalises the reference element to 1). // The ratio k_A/k_B = (A_A sigma_B omega_B a_B eps_B)/(A_B sigma_A omega_A a_A eps_A). -// NOTE: the WP3 pre-registration prints this ratio with sigma_A and sigma_B -// exchanged relative to this derivation; the closed-loop test (forward intensities +// The closed-loop test (forward intensities // from a known composition, then Cliff-Lorimer) takes the version here. // diff --git a/Sources/SpectroEDX/Spectroscopy/Quant/DetectorEfficiency.swift b/Sources/SpectroEDX/Spectroscopy/Quant/DetectorEfficiency.swift index 0a2d482..e74254a 100644 --- a/Sources/SpectroEDX/Spectroscopy/Quant/DetectorEfficiency.swift +++ b/Sources/SpectroEDX/Spectroscopy/Quant/DetectorEfficiency.swift @@ -2,8 +2,7 @@ // DetectorEfficiency.swift // Role: The detector's intrinsic efficiency epsilon(E) as a NAMED input to a // computed k-factor. No curve ships: the Super-X G1 window/contact/dead- -// layer thicknesses are not in any file we hold (ADR 054, second opinion -// C2), so a computed k carries whatever source the user or the supervisor +// layer thicknesses are not in any file we hold, so a computed k carries whatever source the user or the supervisor // names, and the k badge shows `sourceName`. // diff --git a/Sources/SpectroEDX/Spectroscopy/Quant/FitRange.swift b/Sources/SpectroEDX/Spectroscopy/Quant/FitRange.swift index 51297fe..edf8b3f 100644 --- a/Sources/SpectroEDX/Spectroscopy/Quant/FitRange.swift +++ b/Sources/SpectroEDX/Spectroscopy/Quant/FitRange.swift @@ -1,6 +1,6 @@ // // FitRange.swift -// Role: WP3c, "the default fit range, not the continuum" (docs/archive/v5/wp3c-fit-range-preregistration-2026-10-06.md). +// Role: WP3c, "the default fit range, not the continuum". // The Quantify fit's upper energy limit as one value with its provenance (`FitRangeChoice`, the footer's "fit range" // line), and the range-sensitivity statement (`FitRangeSensitivity`): when the axis runs past the default range, one // extra fit to the axis end, reported per quantified element as a named statement, never as a sigma term. @@ -157,7 +157,7 @@ public nonisolated enum FitRangeSensitivityCheck { try! run(input: input, quantification: q, alternativeToKeV: alternativeToKeV, cancelled: { false }, progress: nil) } - /// The same extra fit, cancellable (ADR 064 amendment 2 §2): `SpectroCancelled` is thrown (never folded into `failure`), + /// The same extra fit, cancellable: `SpectroCancelled` is thrown (never folded into `failure`), /// nothing is returned. `progress` is monotone in 0...1 and ends at 1. public static func run(input: PooledQuantificationInput, quantification q: PooledQuantification, alternativeToKeV: Double? = nil, diff --git a/Sources/SpectroEDX/Spectroscopy/Quant/FourDetectorAbsorption.swift b/Sources/SpectroEDX/Spectroscopy/Quant/FourDetectorAbsorption.swift index 4f67d79..5603f7f 100644 --- a/Sources/SpectroEDX/Spectroscopy/Quant/FourDetectorAbsorption.swift +++ b/Sources/SpectroEDX/Spectroscopy/Quant/FourDetectorAbsorption.swift @@ -3,8 +3,7 @@ // Role: Self-absorption for a multi-segment EDS system (Super-X: four SDD // segments around the specimen) as ONE correction for the summed spectrum. // -// Not in eXSpy (it has one scalar take-off angle). Design: WP3 pre-registration -// flag 1 / ADR 054 addendum. The summed stream is sum_d I0 * Omega_d * T_d(E), +// Not in eXSpy (it has one scalar take-off angle). The summed stream is sum_d I0 * Omega_d * T_d(E), // so the exact correction for the sum is 1/Tbar with the SOLID-ANGLE-WEIGHTED // MEAN TRANSMISSION Tbar = sum_d Omega_d' T_d / sum_d Omega_d' (Omega_d' = // Omega_d (1 - holder shadow fraction)), T_d = (1 - e^-x_d)/x_d, diff --git a/Sources/SpectroEDX/Spectroscopy/Quant/KFactorSource.swift b/Sources/SpectroEDX/Spectroscopy/Quant/KFactorSource.swift index aaf8bee..dc1e98b 100644 --- a/Sources/SpectroEDX/Spectroscopy/Quant/KFactorSource.swift +++ b/Sources/SpectroEDX/Spectroscopy/Quant/KFactorSource.swift @@ -21,7 +21,7 @@ package nonisolated struct KFactorSet: Sendable, Equatable { /// ISO date the factors were typed/computed ("2026-10-05"). package var date: String /// Relative 1-sigma per factor (0.20 = 20 %), one per element. Velox's rule and - /// the pre-registered default; editable. + /// the default; editable. package var relativeSigma: [Double] /// The element whose k is fixed at 1 (Velox style, k_i relative to it). Its factor /// carries sigma_k = 0 by default, so a ratio against it has only the OTHER diff --git a/Sources/SpectroEDX/Spectroscopy/Quant/PooledQuantification.swift b/Sources/SpectroEDX/Spectroscopy/Quant/PooledQuantification.swift index 6640886..789f613 100644 --- a/Sources/SpectroEDX/Spectroscopy/Quant/PooledQuantification.swift +++ b/Sources/SpectroEDX/Spectroscopy/Quant/PooledQuantification.swift @@ -1,6 +1,6 @@ // // PooledQuantification.swift -// Role: The Quantify verb's computation (v5.0 R3, ADR 054): one pooled spectrum + a `QuantificationMethod` +// Role: The Quantify verb's computation: one pooled spectrum + a `QuantificationMethod` // -> the fit (EDSFit), the energy axis refinement, net areas with sigma, the k-free ratios, and, ONLY // when a k source exists, at%/wt% with sigma as named terms. Nothing here draws or reads a view; the // room's controller calls it off the main actor and puts the answer in the model. @@ -13,19 +13,19 @@ // a missing tilt or segment value, or no typed thickness -> `absorption == .refused(reason)`. The at% // is then computed WITHOUT the correction and the result says so; it never pretends. // -// DEVIATIONS (named, ADR 054 corrections): +// DEVIATIONS (named): // * Absorption is iterated to 1e-7 % (eXSpy stops at 0.5 %): the sigma terms are finite differences of // the composition, and a loop that stops at 0.5 % would put steps into them. // * The absorption "model spread" is |at%(geometric-mean transmission) - at%(weighted-mean transmission)|. The -// weighted mean is the method and the geometric mean only a comparison (ADR 054 corrections), so the spread is +// weighted mean is the method and the geometric mean only a comparison, so the spread is // a proxy for how much the averaging rule matters, NOT an uncertainty: it is printed beside the sigma as // "absorption model spread (AM vs GM)" and is kept OUT of `SigmaTerms.combined` and of the shown sigma. The real -// absorption uncertainty would be a mass-absorption-coefficient term with a sourced sigma_mu; that is not in this lane. +// absorption uncertainty would be a mass-absorption-coefficient term with a sourced sigma_mu; that is not here. // * Counting term: the fit covariance of the quantified areas propagated through Cliff-Lorimer (+ // absorption) by central differences. The k term: each non-reference k moved by +-sigma_k (20 % flat, // the reference carries 0), the effects added in quadrature. The thickness term: central difference // of the whole correction over thickness +- sigma (`ThicknessPropagation`). -// * Live-time normalisation (ADR 054 item 5) is a REGION-COMPARISON control and is not applied here: every pixel's +// * Live-time normalisation is a REGION-COMPARISON control and is not applied here: every pixel's // lines share that pixel's live time and a pool is the counts-weighted mean of its pixels, so it does not change // the at% of a single pool. // * The k term moves each non-reference k independently by sigma_k; it ignores the correlation a computed set has @@ -178,7 +178,7 @@ public nonisolated struct PooledQuantification: Sendable { public var hasAbundance: Bool { abundanceRefusal == nil } /// "\u{03C7}\u{00B2}\u{1D63} 54.8 (Pearson)" or "reduced deviance 1.02": shown beside the at% always (R7). public var qualityText: String { PooledQuantifier.qualityText(label: qualityLabel, value: quality) } - /// "unvalidated" whenever an at% was produced (ADR 054 item 3): the cross-section source is validation "none". + /// "unvalidated" whenever an at% was produced: the cross-section source is validation "none". public static let abundanceValidation = "none" } @@ -186,7 +186,7 @@ public nonisolated enum PooledQuantifier { // MARK: Entry - /// Cancellable (ADR 064 amendment 2 §2): `cancelled` is asked at the step boundaries (before the axis refinement, inside it + /// Cancellable: `cancelled` is asked at the step boundaries (before the axis refinement, inside it /// once per simplex iteration, before the fit and before the rows); `SpectroCancelled` propagates, never swallowed into a /// warning or a refusal, and nothing partial is returned. `progress` is monotone in 0...1 and ends at 1. public static func run(_ input: PooledQuantificationInput, tables: QuantificationTables?, @@ -334,7 +334,7 @@ public nonisolated enum PooledQuantifier { rows.append(row) } - // The k-free ratio: area / reference area (ADR 054 item 3: above at%, and independent of k). Reference: kReference, else Al when + // The k-free ratio: area / reference area (shown above at%, and independent of k). Reference: kReference, else Al when // it is quantified and measured, else the largest area. let valid = rows.indices.filter { rows[$0].failure == nil && rows[$0].net > 0 } var referenceIndex: Int? diff --git a/Sources/SpectroEDX/Spectroscopy/Quant/QuantElementData.swift b/Sources/SpectroEDX/Spectroscopy/Quant/QuantElementData.swift index bda17b9..cd1d7d7 100644 --- a/Sources/SpectroEDX/Spectroscopy/Quant/QuantElementData.swift +++ b/Sources/SpectroEDX/Spectroscopy/Quant/QuantElementData.swift @@ -5,8 +5,7 @@ // // Values are eXSpy 7185a4d1 `material/elements_general_properties.json` (GPL-3.0, // upstream provenance "unknown" in its own metadata), Z = 1...95, copied so the -// pins in docs/archive/v5/edx-research-2026-10-05/reports/validation.md §1b hold -// exactly. They are the older IUPAC weights (Al 26.9815386), not the 2013+ ones. +// exSpy pins hold exactly. They are the older IUPAC weights (Al 26.9815386), not the 2013+ ones. // Density is nil where eXSpy stores "NaN" (At, Fr). // diff --git a/Sources/SpectroEDX/Spectroscopy/Quant/QuantificationPolicy.swift b/Sources/SpectroEDX/Spectroscopy/Quant/QuantificationPolicy.swift index ccc634a..45678b6 100644 --- a/Sources/SpectroEDX/Spectroscopy/Quant/QuantificationPolicy.swift +++ b/Sources/SpectroEDX/Spectroscopy/Quant/QuantificationPolicy.swift @@ -2,7 +2,7 @@ // QuantificationPolicy.swift // Role: The choices behind a pooled quantification that the library made rather than took from the reference, as values with // the library's choice as the default. `FitPolicy` shapes the spectrum fit, `AbundancePolicy` the step from nets to -// at% and its uncertainty terms. Registered: docs/generality-0.5.0-registration.md (F1-F13, Q3-Q7). +// at% and its uncertainty terms. Since 0.5.0. // // A policy is NOT part of `QuantificationMethod` (its canonical JSON and hash stay what they were for every method recorded // before). A non-default policy is named in the result's footer, one line, so a number never travels without the choice that @@ -18,10 +18,10 @@ public nonisolated struct DetectorLayers: Sendable, Equatable, Codable { /// Gold contact layer, nm. Default 0 (EPQ `getDefaultSDDProperties`). More gold absorbs low-energy X-rays more. public var goldNm: Double /// Aluminium layer, nm. Default 10 (EPQ `getDefaultSDDProperties`). The Al K edge (1.560 keV) lies between Mg K alpha (1.254) and - /// Si K alpha (1.740 keV), so a thicker layer absorbs Si K alpha more and lowers the computed k_Mg / k_Si (measured, registered Q4). + /// Si K alpha (1.740 keV), so a thicker layer absorbs Si K alpha more and lowers the computed k_Mg / k_Si (measured). public var aluminiumNm: Double /// Silicon dead layer in front of the active volume, micrometres. Default 0 (EPQ `getDefaultSDDProperties`). The Si K edge - /// (1.839 keV) lies above both Mg and Si K alpha, so a thicker one raises the computed k_Mg / k_Si (measured, registered Q4). + /// (1.839 keV) lies above both Mg and Si K alpha, so a thicker one raises the computed k_Mg / k_Si (measured). public var deadLayerMicrons: Double /// Active silicon thickness, micrometres. Default 450 (EPQ `getDefaultSDDProperties`). A thinner one lets /// more high-energy X-rays pass undetected, lowering the efficiency there. diff --git a/Sources/SpectroEDX/Spectroscopy/Quant/SDDEfficiency.swift b/Sources/SpectroEDX/Spectroscopy/Quant/SDDEfficiency.swift index 62857af..f5d8988 100644 --- a/Sources/SpectroEDX/Spectroscopy/Quant/SDDEfficiency.swift +++ b/Sources/SpectroEDX/Spectroscopy/Quant/SDDEfficiency.swift @@ -1,7 +1,7 @@ // // SDDEfficiency.swift // Role: A GENERIC silicon-drift-detector efficiency model with every layer named. -// It is NOT the owner's Super-X calibration (Thermo publishes no layer data we hold). +// It is NOT a Super-X calibration (Thermo publishes no layer data we hold). // // eps(E) = exp(-mu_Au rho t_Au) exp(-mu_Al rho t_Al) exp(-mu_Si rho t_dead) // (1 - exp(-mu_Si rho t_Si)) diff --git a/Sources/SpectroEDX/Spectroscopy/Quant/UnlistedLineCheck.swift b/Sources/SpectroEDX/Spectroscopy/Quant/UnlistedLineCheck.swift index 80ea1b9..28c5a94 100644 --- a/Sources/SpectroEDX/Spectroscopy/Quant/UnlistedLineCheck.swift +++ b/Sources/SpectroEDX/Spectroscopy/Quant/UnlistedLineCheck.swift @@ -1,12 +1,12 @@ // // UnlistedLineCheck.swift -// Role: WP3b F1, "name what is missing before at%" (docs/archive/v5/wp3b-fit-robustness-preregistration-2026-10-06.md). +// Role: Name what is missing before at%. // After a Quantify fit the element proposer runs on the SAME pooled spectrum with the listed set; every candidate it -// finds at or above Currie's L_D is named. At% is NEVER blanked by the check (A2, Gate D 2026-10-06): when candidates +// finds at or above Currie's L_D is named. At% is NEVER blanked by the check: when candidates // exist the result carries `abundanceCaveat` (what is unlisted, the largest move) and the export carries it too. // Nothing here edits an element list. // -// A2 (docs/archive/v5, Gate D on the real Al-alloy fit): the "> sigma" bar that used to blank at% was measured on synthetic +// Why (measured 2026-10-06 on a real Al-alloy fit): the "> sigma" bar that used to blank at% was measured on synthetic // pools (0.3-0.9 sigma harmless, 27-33 sigma missing) and did not separate on the real ones (1.1 sigma to 25 sigma on pools with // nothing wrong a person could name), so by the threshold rule the quantity ships and the reader judges. The blanking was one // function (`withholding`); it now sets the caveat instead. @@ -204,7 +204,7 @@ public nonisolated enum UnlistedLineChecker { guard let fwhm = XRayLines.fwhm(resolutionMnKaEV: resolutionMnKaEV, atEnergy: e) else { return nil } // The STRONGEST (alpha, the group's first) line of each listed group is the reference, the nearest in FWHM units wins; the // net it carries (and the fraction against it) is that group's alpha area. Never a beta line: the shape misfit the label - // stands for lives at the strong line (Gate D 2026-10-06, beside-decision). + // stands for lives at the strong line. var best: (group: String, d: Double)? for g in listedGroups { guard let l = g.lines.first else { continue } @@ -231,7 +231,7 @@ public nonisolated enum UnlistedLineChecker { try! check(proposal: proposal, input: input, quantification: q, rules: rules, cancelled: { false }, progress: nil) } - /// The same decision, cancellable (ADR 064 amendment 2 §2): the refit is the long step; `SpectroCancelled` is thrown (never + /// The same decision, cancellable: the refit is the long step; `SpectroCancelled` is thrown (never /// folded into the check's `failure`) and nothing is returned. `progress` is monotone in 0...1 and ends at 1. public static func check(proposal: ProposalResult, input: PooledQuantificationInput, quantification q: PooledQuantification, rules: UnlistedLineRules = .default, diff --git a/Sources/SpectroEDX/Spectroscopy/Quant/ZetaFactor.swift b/Sources/SpectroEDX/Spectroscopy/Quant/ZetaFactor.swift index d8a08f8..53edc29 100644 --- a/Sources/SpectroEDX/Spectroscopy/Quant/ZetaFactor.swift +++ b/Sources/SpectroEDX/Spectroscopy/Quant/ZetaFactor.swift @@ -1,8 +1,8 @@ // // ZetaFactor.swift // Role: zeta-factor and ionisation-cross-section quantification, doses, and the -// zeta <-> cross-section conversion. Core only: ADR 054 keeps zeta out of -// the menu; it is here for eXSpy parity. +// zeta <-> cross-section conversion. Not offered in the +// pooled method; it is here for eXSpy parity. // // Port of eXSpy utils/eds/_quantification.py `quantification_zeta_factor` // (153-199), `quantification_cross_section` (238-290), `cross_section_to_zeta` diff --git a/Sources/SpectroEDX/Spectroscopy/SpectrumQuantities.swift b/Sources/SpectroEDX/Spectroscopy/SpectrumQuantities.swift index 42319ae..e8dc3f3 100644 --- a/Sources/SpectroEDX/Spectroscopy/SpectrumQuantities.swift +++ b/Sources/SpectroEDX/Spectroscopy/SpectrumQuantities.swift @@ -160,7 +160,7 @@ public nonisolated enum ElementWindows { /// /// DEVIATION from eXSpy (`_get_lines_from_elements`, `XRayLines.defaultLines`: "the first alpha line below the beam energy / 2"): /// at 200 kV on an 80 keV axis that picks Kα for Hf (55.8 keV) where the instrument vendor's software uses Lα. That software's - /// own table (one instrument's 200 kV file, `docs/archive/v5/velox-family-table-200kV-2026-10-07.json`) gives K for Z <= 44 + /// own table (read from one instrument's 200 kV file) gives K for Z <= 44 /// (Ru Kα 19.28 keV) and L for Z >= 45 (Rh Kα 20.22 keV), never M; the 20 keV limit is the boundary that table shows, measured /// on 200 kV files only. It changes which line a map or marker uses by default, never the fit, which takes whole families. public static func defaultFamily(of symbol: String) -> XRayFamily { defaultFamily(of: symbol, rules: .default) } @@ -312,7 +312,7 @@ public nonisolated enum ElementWindows { try! maps(image: image, windows: windows, cancelled: { false }, progress: nil) } - /// The same maps, cancellable (ADR 064 amendment 2 §2): `SpectroCancelled` with nothing returned; `progress` 0...1, ends at 1. + /// The same maps, cancellable: `SpectroCancelled` with nothing returned; `progress` 0...1, ends at 1. public static func maps(image: some SpectrumImage, windows: [LineWindow], cancelled: @Sendable () -> Bool = SpectroCancellation.task, progress: (@Sendable (Double) -> Void)? = nil) throws -> [[Double]?] { @@ -373,7 +373,7 @@ public nonisolated enum ElementWindows { /// "background overlaps Si Kα": one line for the conflicts of one result, at most two named. Lines whose INTEGRATION /// window the background reaches come first (a peak in the background), then those it only touches on a flank (a tail): - /// on the owner's file Mg's windows touch Cu Lα's flank and sit on Si Kα's peak, and the peak is the one to say. + /// on a measured Al-alloy file Mg's windows touch Cu Lα's flank and sit on Si Kα's peak, and the peak is the one to say. public static func conflictNote(_ conflicts: [WindowConflict]) -> String? { var names: [String] = [] for c in conflicts.filter(\.reachesIntegrationWindow) + conflicts.filter({ !$0.reachesIntegrationWindow }) { diff --git a/Sources/SpectroEDX/Spectroscopy/WindowIntensity.swift b/Sources/SpectroEDX/Spectroscopy/WindowIntensity.swift index d831e32..1c45a84 100644 --- a/Sources/SpectroEDX/Spectroscopy/WindowIntensity.swift +++ b/Sources/SpectroEDX/Spectroscopy/WindowIntensity.swift @@ -182,7 +182,7 @@ package nonisolated enum WindowIntensity { try! maps(image: image, windows: windows, cancelled: { false }, progress: nil) } - /// The same maps, cancellable (ADR 064 amendment 2 §2): the window sums are the long step and carry `cancelled`/`progress`. + /// The same maps, cancellable: the window sums are the long step and carry `cancelled`/`progress`. package static func maps(image: some SpectrumImage, windows: [ResolvedWindow], cancelled: @Sendable () -> Bool = SpectroCancellation.task, progress: (@Sendable (Double) -> Void)? = nil) throws -> [[Double]] { diff --git a/Sources/SpectroEDX/Spectroscopy/WindowRules.swift b/Sources/SpectroEDX/Spectroscopy/WindowRules.swift index 25d0cf0..3a56d39 100644 --- a/Sources/SpectroEDX/Spectroscopy/WindowRules.swift +++ b/Sources/SpectroEDX/Spectroscopy/WindowRules.swift @@ -2,7 +2,6 @@ // WindowRules.swift // Role: The rules behind the window-based line intensities and the default line of an element, as one value. Every field's // default is the value the windows have always used; a different value is a different, named choice, never a hidden one. -// Registered: docs/generality-0.5.0-registration.md (W1-W7). // import Foundation diff --git a/Sources/SpectroEELS/EELSAlignment.swift b/Sources/SpectroEELS/EELSAlignment.swift index c5d114a..bfbcf0d 100644 --- a/Sources/SpectroEELS/EELSAlignment.swift +++ b/Sources/SpectroEELS/EELSAlignment.swift @@ -1,6 +1,6 @@ // // EELSAlignment.swift -// Role: Task 16 step 1 (docs/design/eels-task-16.md section 2): zero-loss peak alignment and mask, a port of exSpy 0.3.2 +// Role: Zero-loss peak alignment and mask, a port of exSpy 0.3.2 // `align_zero_loss_peak` (`eels.py:318-483`), `estimate_zero_loss_peak_centre` (`:275-316`), `get_zero_loss_peak_mask` // (`:485-511`) and HyperSpy 2.4.0 `Signal1D.shift1D` (`signal1d.py:423-551`, `_shift1D :252-274`), `estimate_shift1D` // (`:626-736`, `_estimate_shift1D :236-249`, `interpolate1D :220-233`) and `align1D` (`:738-853`). diff --git a/Sources/SpectroEELS/EELSDeconvolution.swift b/Sources/SpectroEELS/EELSDeconvolution.swift index 733d566..8190218 100644 --- a/Sources/SpectroEELS/EELSDeconvolution.swift +++ b/Sources/SpectroEELS/EELSDeconvolution.swift @@ -1,6 +1,6 @@ // // EELSDeconvolution.swift -// Role: exSpy's Fourier-log and Fourier-ratio deconvolution (docs/design/eels-task-16.md section 4; `_eels.py:870-1097` at 7185a4d1) on +// Role: exSpy's Fourier-log and Fourier-ratio deconvolution (docs/design/eels-chain.md section 4; `_eels.py:870-1097` at 7185a4d1) on // ImagoCore's exact-length FFT, and HyperSpy's `estimate_peak_width` (`signal1d.py:1612-1725`) that the ratio method uses for its // Gaussian. CPU float64 only. // diff --git a/Sources/SpectroEELS/EELSModelFit.swift b/Sources/SpectroEELS/EELSModelFit.swift index aac9325..0d7feca 100644 --- a/Sources/SpectroEELS/EELSModelFit.swift +++ b/Sources/SpectroEELS/EELSModelFit.swift @@ -1,7 +1,7 @@ // // EELSModelFit.swift // Role: exSpy's EELS model (`exspy/models/eelsmodel.py` at 7185a4d1) on the dft GOS tables: a power-law background plus the cross sections of -// the requested edges, fitted per pixel (docs/design/eels-task-16.md section 6.2). `two_area_background_estimation`, +// the requested edges, fitted per pixel (docs/design/eels-chain.md section 6.2). `two_area_background_estimation`, // `fit_background`, `smart_fit` and the low-loss convolution of the edge components are ported; `quantify` is `EELSModelResult.quantifyText`. // // DEVIATION (exSpy): `Model1D.fit` is an unbounded Levenberg-Marquardt that stops short of the minimum (the EDX precedent, `LeastSquaresFit`: @@ -25,7 +25,7 @@ public struct EELSModelOptions: Sendable, Equatable { /// The power-law exponent r is searched in this range. exSpy's is unbounded; a fit that ends on a bound is flagged (`exponentAtBound`). public var exponentRange: ClosedRange = 0 ... 10 /// Per-edge windows only: each fitted edge (amplitude times its cross section on every channel) is subtracted from the spectrum before the - /// next edge's background and amplitude are fitted. Off by default: on the owner's real GMS maps the convolved C K model, fitted on its + /// next edge's background and amplitude are fitted. Off by default: on three measured GMS maps of a-C films the convolved C K model, fitted on its /// 30 eV signal window, overshoots the data 150 eV further up and leaves O K nothing to fit (2026-10-23); the planted tests hold the on case. public var stripPrecedingEdges: Bool = false public init(startEnergyEV: Double? = nil, preEdgeSafeWindowEV: Double = 2, exponentRange: ClosedRange = 0 ... 10, stripPrecedingEdges: Bool = false) { diff --git a/Sources/SpectroEELS/EELSParallel.swift b/Sources/SpectroEELS/EELSParallel.swift index 4df897e..7cf2edc 100644 --- a/Sources/SpectroEELS/EELSParallel.swift +++ b/Sources/SpectroEELS/EELSParallel.swift @@ -1,6 +1,6 @@ // // EELSParallel.swift -// Role: the pixel loop of task 16 (design section 11): pixels are independent, so the loop runs in chunks on all cores, a wave at a time, +// Role: the EELS pixel loop: pixels are independent, so the loop runs in chunks on all cores, a wave at a time, // asking `cancelled` and reporting progress between waves. Each chunk writes its own slots; results are assembled in pixel order, // so an answer never depends on the schedule. CPU float64 only (no Metal: the work per pixel is smaller than a copy). // diff --git a/Sources/SpectroEELS/EELSRegionSum.swift b/Sources/SpectroEELS/EELSRegionSum.swift index 5e65c28..7123361 100644 --- a/Sources/SpectroEELS/EELSRegionSum.swift +++ b/Sources/SpectroEELS/EELSRegionSum.swift @@ -1,6 +1,6 @@ // // EELSRegionSum.swift -// Role: the summed spectrum of a region (docs/design/eels-task-16.md section 12, the macTEM design 2.3 ask): the counts of every masked-in +// Role: the summed spectrum of a region: the counts of every masked-in // pixel added channel by channel, as a one-pixel image on the same energy axis. // diff --git a/Sources/SpectroEELS/EELSSpectrumImage.swift b/Sources/SpectroEELS/EELSSpectrumImage.swift index 39621db..f9ad726 100644 --- a/Sources/SpectroEELS/EELSSpectrumImage.swift +++ b/Sources/SpectroEELS/EELSSpectrumImage.swift @@ -1,6 +1,6 @@ // // EELSSpectrumImage.swift -// Role: What task 16's operations share (docs/design/eels-task-16.md sections 1 and 2): one protocol over the low-loss and the +// Role: What the EELS operations share (docs/design/eels-chain.md): one protocol over the low-loss and the // core-loss spectrum image, so alignment, taper and extrapolation return the caller's own type; the per-pixel "no answer" // reasons; the error cases of alignment, deconvolution and the edge model. // diff --git a/Sources/SpectroEELS/EELSSpectrumOps.swift b/Sources/SpectroEELS/EELSSpectrumOps.swift index 4492aea..df32689 100644 --- a/Sources/SpectroEELS/EELSSpectrumOps.swift +++ b/Sources/SpectroEELS/EELSSpectrumOps.swift @@ -1,13 +1,13 @@ // // EELSSpectrumOps.swift -// Role: Task 16 step 2 (docs/design/eels-task-16.md section 3): power-law extrapolation to the right and the Hanning taper, ports of exSpy 0.3.2 +// Role: Power-law extrapolation to the right and the Hanning taper, ports of exSpy 0.3.2 // `EELSSpectrum.power_law_extrapolation` (`eels.py:1236-1331`) and HyperSpy 2.4.0 `Signal1D.hanning_taper` (`signal1d.py:1453-1531`), // the engines of the Fourier deconvolutions. // // The extrapolation estimates A and r by HyperSpy's two-area method (`PowerLaw.estimate_parameters`, the existing `PowerLawFit`) over the last // `windowChannels` channels of each spectrum, adds `A * x^-r` times the channel width (a binned axis) at the new channels. Counts are per channel // (a BINNED axis); exSpy's UNBINNED branch (Simpson integral, no channel-width factor) is not offered. `add_noise` (a random draw) is not offered. -// Departure registered: where a window sum is not positive or A, r are not finite, HyperSpy's `nan_to_num` (or its whole call failing) is replaced +// Departure from HyperSpy: where a window sum is not positive or A, r are not finite, HyperSpy's `nan_to_num` (or its whole call failing) is replaced // by a nil fit and zeros at the new channels, as the rest of the kit does. // diff --git a/Sources/SpectroEELS/EELSThickness.swift b/Sources/SpectroEELS/EELSThickness.swift index 743d7b0..2d44bb8 100644 --- a/Sources/SpectroEELS/EELSThickness.swift +++ b/Sources/SpectroEELS/EELSThickness.swift @@ -1,6 +1,6 @@ // // EELSThickness.swift -// Role: EELS slice 1 (docs/eels-slice-1-registration.md): the t/λ thickness map of a low-loss spectrum image by the +// Role: The t/λ thickness map of a low-loss spectrum image by the // log-ratio method, a port of exSpy 7185a4d1's `estimate_thickness` (`exspy/signals/_eels.py:755-868`) with its // threshold (`:641-753`) and zero-loss sum (`:566-639`). // diff --git a/Sources/SpectroEELS/EdgeCrossSection.swift b/Sources/SpectroEELS/EdgeCrossSection.swift index 4ecd6d6..d304d83 100644 --- a/Sources/SpectroEELS/EdgeCrossSection.swift +++ b/Sources/SpectroEELS/EdgeCrossSection.swift @@ -4,7 +4,7 @@ // computes it: the effective collection angle (`exspy/utils/eels/effective_angle.py:23-80`, Egerton 2nd ed. p. 420), // the q integration (`exspy/_misc/eels/base_gos.py`, `get_qaxis_and_gos` and `integrateq`), the interpolating // spline between tabulated energies and the power law beyond them (`components/_eels_cl_edge.py:436-494`). -// EELS slice 2, registration section 3.3. +// // // Precision as exSpy runs on NumPy 2.5.4: the GOS arrays are float32 and, by NEP 50, a Python float meeting a float32 // scalar or array is cast to float32, so the q limits, the GOS interpolated to q_max, the prefactor and the diff --git a/Sources/SpectroEELS/EdgeQuantification.swift b/Sources/SpectroEELS/EdgeQuantification.swift index db94c10..c6289f2 100644 --- a/Sources/SpectroEELS/EdgeQuantification.swift +++ b/Sources/SpectroEELS/EdgeQuantification.swift @@ -1,6 +1,6 @@ // // EdgeQuantification.swift -// Role: EELS slice 2 (docs/eels-slice-2-registration.md): the areal density of one element from a core-loss spectrum +// Role: The areal density of one element from a core-loss spectrum // image by the ratio method (Egerton, EELS in the Electron Microscope, 3rd ed., eqs. 4.65 and 4.66): the power-law // background (`PowerLawBackground`), the edge counts over [E_k, E_k + Δ), the partial cross section from exSpy's dft // GOS (`EdgeCrossSection`), and the low-loss intensity, per pixel; with the counting-statistics σ. @@ -64,7 +64,7 @@ public nonisolated enum LowLossIntensity: Sendable { /// (registration amendment 1: −7.6, −17.6 and −30.7 % at t/λ 0.3, 0.7 and 1.3; −1.6 to −7.1 % at Δ = 150 eV): use a wide /// Δ where the data allow. case integratedToWidth(LowLossSpectrumImage) - /// I_0 of slice 1's thickness map (the zero-loss sum), Egerton eq. 4.65, for single scattering only. + /// I_0 of the thickness map (the zero-loss sum), Egerton eq. 4.65, for single scattering only. case zeroLoss(ThicknessMap) } @@ -81,7 +81,7 @@ public nonisolated enum EdgeUnavailable: String, Error, Sendable, Equatable, Cus public nonisolated struct ArealDensityPixel: Sendable, Equatable { /// Atoms per nm²; nil with `unavailable` set. public let atomsPerNM2: Double? - /// Counting-statistics standard deviation of `atomsPerNM2` (registration section 6; not the model, cross-section, + /// Counting-statistics standard deviation of `atomsPerNM2` (not the model, cross-section, /// angle, dose-ratio or onset errors). First order: trustworthy while it is small against n; where σ_n / n is large /// (above about 0.3) the estimate of n itself is no longer near-Gaussian (a review's Monte Carlo at σ_n / n > 5 found /// the mean off by 17 to 74 %). @@ -117,7 +117,7 @@ public nonisolated struct ArealDensityMap: Sendable { public let lowLossRoute: String public let coreLossToLowLossDose: Double public let method: String - /// Always "none": agreement with exSpy and planted truth are not validation (registration section 9). + /// Always "none": agreement with exSpy and planted truth are not validation. public let validation: String /// What the registration says a reader must know about this edge (N K and O K: their background sits on a preceding /// edge's extended tail, which a power law fitted below the edge misjudges); nil for C K. @@ -135,7 +135,7 @@ public nonisolated enum EELSEdgeError: Error, Equatable, Sendable { case backgroundWindowTooNarrow(fromEV: Double, toEV: Double) case backgroundWindowNotPositive(fromEV: Double) /// The requested pre-edge window is not inside the core-loss axis. HyperSpy would clamp it (to the whole spectrum when it - /// lies below the axis): a departure registered after review, since that fit crosses the edge and gives a wrong n silently. + /// lies below the axis): a departure from HyperSpy, since that fit crosses the edge and gives a wrong n silently. case backgroundWindowOutsideAxis(fromEV: Double, toEV: Double, axisFromEV: Double, axisToEV: Double) /// The partial cross section over the edge window is 0 (the window lies below the edge's onset): n would be infinite. case crossSectionZero(edgeWindowFromEV: Double, toEV: Double) @@ -151,7 +151,7 @@ public nonisolated enum EELSEdgeQuantification { "power law (two-area, HyperSpy 2.4.0) + ratio method (Egerton eq. 4.65/4.66), cross section exSpy 7185a4d1 dft GOS (Segger, Guzzinati and Kohl 1.5.0)" /// The default pre-edge background window: `[E_k − belowOnsetEV, E_k − gapEV]`, by default [E_k − 50, E_k − 5] eV. The library's - /// choice, registered with the method (exSpy has no default: its user picks the window); 50 eV is wide enough to estimate a power + /// choice (exSpy has no default: its user picks the window); 50 eV is wide enough to estimate a power /// law and 5 eV keeps the window clear of the edge's onset. `ratioMethod(backgroundWindowEV:)` takes any window. public static func defaultBackgroundWindow(onsetEV: Double, belowOnsetEV: Double = 50, gapEV: Double = 5) -> ClosedRange { (onsetEV - belowOnsetEV)...(onsetEV - gapEV) @@ -244,7 +244,7 @@ public nonisolated enum EELSEdgeQuantification { effectiveCollectionMrad: xs.effectiveAngleMrad, crossSectionBarn: sigma, backgroundWindowEV: window.x1...window.x2, edgeWindowEV: onset ..< edgeEnd, edgeChannels: channels, lowLossRoute: route, coreLossToLowLossDose: coreLossToLowLossDose, method: methodName, validation: "none", - caveat: edge == .carbonK ? nil : "background under a preceding edge's tail: the power law fitted below \(edge) is not trusted (registration section 1)") + caveat: edge == .carbonK ? nil : "background under a preceding edge's tail: the power law fitted below \(edge) is not trusted") } private static func crossSection(edge: CoreLossEdge, microscope m: EELSMicroscope) throws -> EdgeCrossSection { diff --git a/Sources/SpectroEELS/GOSTable.swift b/Sources/SpectroEELS/GOSTable.swift index 4029a68..012f7b8 100644 --- a/Sources/SpectroEELS/GOSTable.swift +++ b/Sources/SpectroEELS/GOSTable.swift @@ -2,7 +2,7 @@ // GOSTable.swift // Role: The generalised oscillator strength of one K shell, as exSpy 7185a4d1 reads it from the GOSH database // (`exspy/_misc/eels/gosh_gos.py`, `read_gos_data`; `base_gos.py`, `TabulatedGOS`), and the edges SpectroKit ships. -// EELS slice 2, registration section 3.3. +// // // The tables are the dft GOS of Segger, Guzzinati and Kohl (GOSH 1.5.0, DOI 10.5281/zenodo.7645765, CC-BY-4.0; md5 of the // file exSpy fetches 7fee8891c147a4f769668403b54c529b), the groups C/K1, N/K1 and O/K1, extracted by @@ -41,7 +41,7 @@ public nonisolated struct CoreLossEdge: Sendable, Equatable, CustomStringConvert /// Silicon K, onset 1839 eV. public static let siliconK = CoreLossEdge(element: "Si", subshell: "K1", onsetEV: 1839.0) - // The L edges (task 16, design section 6.1). L2 and L3 of one element are twins in exSpy's model: one intensity, the L3 name the master + // The L edges. L2 and L3 of one element are twins in exSpy's model: one intensity, the L3 name the master // (`eelsmodel.py:283-327`); Al has the single combined "L2,3". Onsets from exSpy's eels_binding_energies.json. /// Magnesium L1, onset 89 eV. public static let magnesiumL1 = CoreLossEdge(element: "Mg", subshell: "L1", onsetEV: 89.0) diff --git a/Sources/SpectroEELS/InterpolatingSpline.swift b/Sources/SpectroEELS/InterpolatingSpline.swift index d7f7cb1..3592bcd 100644 --- a/Sources/SpectroEELS/InterpolatingSpline.swift +++ b/Sources/SpectroEELS/InterpolatingSpline.swift @@ -2,7 +2,7 @@ // InterpolatingSpline.swift // Role: SciPy 1.18.1's `make_interp_spline(x, y, k=3)` with its default not-a-knot condition, and the resulting // `BSpline`'s evaluation with `extrapolate=True`, which exSpy uses for the energy-differential cross section between -// the tabulated GOS energies (`base_gos.py`, end of `integrateq`). EELS slice 2, registration section 3.3. +// the tabulated GOS energies (`base_gos.py`, end of `integrateq`). // // Knots (`interpolate/_bsplines.py:1278-1297`, `_not_a_knot`, odd k): x[0] four times, x[2 ..< n-2], x[n-1] four times. // The collocation matrix (`_dierckx._coloc`): row j holds the k+1 non-zero B-splines at x[j] in the knot interval l diff --git a/Sources/SpectroEELS/LowLossSpectrumImage.swift b/Sources/SpectroEELS/LowLossSpectrumImage.swift index 7f6afa1..6b2c31d 100644 --- a/Sources/SpectroEELS/LowLossSpectrumImage.swift +++ b/Sources/SpectroEELS/LowLossSpectrumImage.swift @@ -11,7 +11,7 @@ import Foundation /// Layout: `counts[(y * nx + x) * channels + c]`, row-major in y, then x, then channel (the order of exSpy's /// `(rows, cols, channels)` array). The energy of channel `c` is `energyOffsetEV + energyScaleEV * c`. The counts are the /// detector's counts per channel (a BINNED axis in HyperSpy's sense: the sum over channels is the intensity). exSpy -/// integrates an unbinned axis with Simpson's rule instead; that path is not ported (registration section 3.2), so +/// integrates an unbinned axis with Simpson's rule instead; that path is not ported, so /// counts must be per-channel sums, not densities. /// /// The zero-loss peak must sit at 0 eV (the registration's assumption, exSpy `_eels.py:647-648`): nothing is aligned or diff --git a/Sources/SpectroEELS/PowerLawBackground.swift b/Sources/SpectroEELS/PowerLawBackground.swift index 5bab436..ff7a2e2 100644 --- a/Sources/SpectroEELS/PowerLawBackground.swift +++ b/Sources/SpectroEELS/PowerLawBackground.swift @@ -3,9 +3,9 @@ // Role: The pre-edge background A E^-r by HyperSpy 2.4.0's two-area estimate (`_components/power_law.py:100-187`, // `PowerLaw.estimate_parameters`), evaluated as `remove_background(fast=True)` evaluates it on a binned axis // (`_signals/signal1d.py:1102-1152`; `models/model1d.py:375-390`: the component's value times the channel width). -// EELS slice 2, registration section 3.1. +// // -// Departure from HyperSpy, registered (section 1): where a window sum is not positive or A, r come out non-finite, +// Departure from HyperSpy: where a window sum is not positive or A, r come out non-finite, // HyperSpy's `nan_to_num` (or, on a zero sum, the whole image's estimate failing) is replaced by a nil pixel with its // reason. Everything else is HyperSpy's arithmetic in its order. // @@ -19,7 +19,7 @@ package nonisolated struct PowerLawWindow: Sendable, Equatable { /// `value_range_to_indices(x1, x2)` and the parity rule (`power_law.py:127-135`). package init(axis: EELSAxis, from lo: Double, to hi: Double) throws { - // Departure from HyperSpy (registered after review): a window not inside the axis is refused, not clamped. + // Departure from HyperSpy: a window not inside the axis is refused, not clamped. guard lo >= axis.lowValue, hi <= axis.highValue else { throw EELSEdgeError.backgroundWindowOutsideAxis(fromEV: lo, toEV: hi, axisFromEV: axis.lowValue, axisToEV: axis.highValue) } diff --git a/Sources/SpectroEELS/ScipySimpson.swift b/Sources/SpectroEELS/ScipySimpson.swift index 8570e0b..700f280 100644 --- a/Sources/SpectroEELS/ScipySimpson.swift +++ b/Sources/SpectroEELS/ScipySimpson.swift @@ -2,7 +2,7 @@ // ScipySimpson.swift // Role: SciPy 1.18.1's `scipy.integrate.simpson(y, x=x)` for one vector on a non-uniform grid (`integrate/_quadrature.py: // 358-388` `_basic_simpson`, `:392-527` `simpson`), which exSpy's GOS integration over ln((a0 q)^2) calls -// (`exspy/_misc/eels/base_gos.py`, `integrateq`). EELS slice 2 (docs/eels-slice-2-registration.md, section 3.3). +// (`exspy/_misc/eels/base_gos.py`, `integrateq`). // // An odd number of points: the composite rule over pairs of intervals. An even number: the composite rule over the first // N - 3 points and Cartwright's correction for the last interval (`:486-524`); two points: the trapezoid. The per-pair diff --git a/Sources/SpectroEELS/ThicknessMap.swift b/Sources/SpectroEELS/ThicknessMap.swift index a7ab557..0ef8fbc 100644 --- a/Sources/SpectroEELS/ThicknessMap.swift +++ b/Sources/SpectroEELS/ThicknessMap.swift @@ -52,8 +52,8 @@ public nonisolated struct ThicknessPixel: Sendable, Equatable { /// clipped: I_t <= I_0 is reported as t/λ <= 0. nil with `unavailable` where the method cannot answer. public let tOverLambda: Double? /// The counting-statistics standard deviation of t/λ, sqrt(1/I_0 - 1/I_t), when the stored counts are Poisson counts and - /// I_0 is a part of I_t (registration section 6). It is a FIRST-ORDER (delta-method) value, not exact: a Monte Carlo by the Gate B - /// refuter gave actual/formula standard deviation of 1.0009 at I_0 = 1000 (I_t/I_0 near 300), 1.009 at I_0 = 100 (20), 1.055 + /// I_0 is a part of I_t. It is a FIRST-ORDER (delta-method) value, not exact: an independent Monte Carlo + /// gave actual/formula standard deviation of 1.0009 at I_0 = 1000 (I_t/I_0 near 300), 1.009 at I_0 = 100 (20), 1.055 /// at I_0 = 20 (5) and 1.129 at I_0 = 10 (3): the formula is about 13 % low at I_0 near 10 counts and good above I_0 near 100. /// I_0 = I_t gives 0. It does NOT include the threshold's variability (with the estimated rule /// on noisy data the standard deviation of t/λ was 1.0 to 3.2 times larger), the λ model, or the angular correction's inputs. @@ -89,12 +89,12 @@ public nonisolated enum ZeroLossSource: Sendable, Equatable { /// (`estimate_thickness`, `_eels.py:755-868`). /// /// **Validation: none.** `validation` stays "none": agreement with exSpy and recovery of planted t/λ do not make a thickness -/// right on a real specimen (registration section 8). The method has its own bias (the plasmon below the threshold counts +/// right on a real specimen. The method has its own bias (the plasmon below the threshold counts /// as zero loss; the loss tail past the axis is missing from I_t), about 5e-4 at the planted t/λ = 2 with the estimated threshold /// and more with a fixed one. /// /// **`sigmaCounting` is the counting error at a given threshold, nothing more.** It leaves out the threshold's variability, -/// which, with the estimated rule on noisy data, made the standard deviation of t/λ 1.0 to 3.2 times larger on the registered +/// which, with the estimated rule on noisy data, made the standard deviation of t/λ 1.0 to 3.2 times larger on the /// planted images (the estimated threshold is set by one global `tol`, which the noisiest pixel decides). Noisy data want a fixed threshold. /// The stored counts are also not electron counts unless the detector's conversion has been applied; a consumer must carry the /// λ source and its own uncertainty separately. @@ -113,7 +113,7 @@ public nonisolated struct ThicknessMap: Sendable { public let angularCorrection: Double? /// The global `tol` (counts/eV) the estimated rule used; nil for other rules. public let estimatedTolerance: Double? - /// "none" until a sample of known thickness has been measured (registration section 8). + /// "none" until a sample of known thickness has been measured. public let validation: String public subscript(x: Int, y: Int) -> ThicknessPixel { pixels[y * nx + x] } diff --git a/Tests/SpectroCoreTests/AutoIDRulesWP4bTests.swift b/Tests/SpectroCoreTests/AutoIDRulesWP4bTests.swift index f7a0799..f832371 100644 --- a/Tests/SpectroCoreTests/AutoIDRulesWP4bTests.swift +++ b/Tests/SpectroCoreTests/AutoIDRulesWP4bTests.swift @@ -1,7 +1,6 @@ // // AutoIDRulesWP4bTests.swift -// Lane W (WP4b): the beside-K guard option of `ProposalRules` on hand-built `ProposalResult`s. Registration: -// docs/archive/v5/wp4b-autoid-rules-preregistration-2026-10-07.md (R1b: an L/M pick within 1.5 FWHM of another PROPOSED candidate's +// The beside-K guard option of `ProposalRules` on hand-built `ProposalResult`s (R1b: an L/M pick within 1.5 FWHM of another PROPOSED candidate's // K alpha is dropped only when its net / L_D is at or below that K candidate's). The option is off in none / registered / shipped. // Every test names the mutation it catches. // @@ -32,7 +31,7 @@ struct AutoIDRulesWP4bTests { let cu = XRayLines.line("Cu_Ka")!.energy let hfLa = XRayLines.line("Hf_La")!.energy let w = XRayLines.fwhm(resolutionMnKaEV: 130, atEnergy: hfLa)! - #expect(abs(hfLa - cu) <= (1.5 * w), "the test pair is inside the registered window") + #expect(abs(hfLa - cu) <= (1.5 * w), "the test pair is inside the beside-K window") let weakCu = candidate("Cu", "Cu_Ka", significance: 5), strongCu = candidate("Cu", "Cu_Ka", significance: 40) let hf = candidate("Hf", "Hf_La", significance: 12) #expect(picked(r1b, [weakCu, hf]) == ["Cu", "Hf"], "12 x L_D Hf beside 5 x L_D Cu: stays") @@ -79,10 +78,10 @@ struct AutoIDRulesWP4bTests { #expect(ProposalRules.registered.besideK) let cu = candidate("Cu", "Cu_Ka", significance: 5), hf = candidate("Hf", "Hf_La", significance: 12) #expect(picked(ProposalRules.shipped, [cu, hf]) == ["Cu", "Hf"]) - #expect(picked(ProposalRules.registered, [cu, hf]) == ["Cu"], "registered drops Hf however strong, as registered in WP4") + #expect(picked(ProposalRules.registered, [cu, hf]) == ["Cu"], "registered drops Hf however strong") let plain = r1.apply(proposal([cu, hf])) #expect(plain.picks.map(\.element) == ["Cu"], "R1 unguarded: dropped regardless of significance") - #expect(plain.withheld.first?.why == "within 1.5 FWHM of Cu K\u{03B1}", "the unguarded text is the registered one") + #expect(plain.withheld.first?.why == "within 1.5 FWHM of Cu K\u{03B1}", "the unguarded text is unchanged") #expect(picked(.none, [cu, hf]) == ["Cu", "Hf"]) } } diff --git a/Tests/SpectroCoreTests/FinalPolishDTests+Spectro.swift b/Tests/SpectroCoreTests/FinalPolishDTests+Spectro.swift index 69fa814..55352a8 100644 --- a/Tests/SpectroCoreTests/FinalPolishDTests+Spectro.swift +++ b/Tests/SpectroCoreTests/FinalPolishDTests+Spectro.swift @@ -1,11 +1,9 @@ -// SpectroKit S1 test partition (ADR 064 section 3.3): the methods of FinalPolishDTests that move with the EDX core; -// the rest stay in FinalPolishDTests.swift. Helpers are duplicated, not shared, across the split. import Foundation import Testing import SpectroCore import FormatoDM -/// Slot 4⅞ lane D (owner card Q1 = a, 2026-10-04): a DM4 whose disk has been +/// A DM4 whose disk has been /// disconnected refuses with `DM4Error.volumeGone` instead of dying with a /// SIGBUS, and the refusal LATCHES — once a read has found the disk gone, every /// later read of that reader throws, even if the disk answers again, because diff --git a/Tests/SpectroCoreTests/MethodTests+Spectro.swift b/Tests/SpectroCoreTests/MethodTests+Spectro.swift index b740404..233300b 100644 --- a/Tests/SpectroCoreTests/MethodTests+Spectro.swift +++ b/Tests/SpectroCoreTests/MethodTests+Spectro.swift @@ -1,8 +1,6 @@ -// SpectroKit S1 test partition (ADR 064 section 3.3): the methods of QuantificationMethodTests that move with the EDX core; -// the rest stay in MethodTests.swift. Helpers are duplicated, not shared, across the split. // // MethodTests.swift -// v5.0 WP3 lane M (prediction M1, first half): the quantification method as a byte-stable value with a hash, +// The quantification method as a byte-stable value with a hash, // and the replay step "quantification" with its input edges and its rewind. Each test names the mutation it // catches; each was broken once. // diff --git a/Tests/SpectroCoreTests/SpectroCancellationTests.swift b/Tests/SpectroCoreTests/SpectroCancellationTests.swift index 10568d2..d273fa9 100644 --- a/Tests/SpectroCoreTests/SpectroCancellationTests.swift +++ b/Tests/SpectroCoreTests/SpectroCancellationTests.swift @@ -3,7 +3,7 @@ import Testing import SpectroCore import SpectroEDX -/// SpectroKit S0 (ADR 064 amendment 2 §2): the long spectroscopy calls take `cancelled` (default `SpectroCancellation.task`) and an +/// SpectroKit S0: the long spectroscopy calls take `cancelled` (default `SpectroCancellation.task`) and an /// optional `progress`. Per family: cancel at the first step and cancel INSIDE the call both throw `SpectroCancelled` and return /// nothing; progress is monotone and ends at 1; the default arguments reproduce the plain call's numbers. Each test names its mutation. @MainActor diff --git a/Tests/SpectroCoreTests/SpectroscopyCoreTests.swift b/Tests/SpectroCoreTests/SpectroscopyCoreTests.swift index 57d7b47..e75c761 100644 --- a/Tests/SpectroCoreTests/SpectroscopyCoreTests.swift +++ b/Tests/SpectroCoreTests/SpectroscopyCoreTests.swift @@ -8,7 +8,7 @@ import SpectroEDX /// Every expected number in `Fixtures/eds-pins-exspy-7185a4d1.json` was produced by RUNNING eXSpy /// (generator: the lane's `gen/pins.py`), together with the two bundled spectra they were computed /// from (EDS_TEM_FePt_nanoparticles, EDS_SEM_TM002; both GPL-3.0 eXSpy test data, 992 / 1024 channels). -/// Integer pins are compared exactly; the FWHM to float32 as pre-registered (C1), in fact to 1e-15. +/// Integer pins are compared exactly; the FWHM to float32, in fact to 1e-15. struct SpectroscopyCoreTests { // MARK: Fixture @@ -312,7 +312,7 @@ struct SpectroscopyCoreTests { #expect(img.sum(mask: [true, false, false, true]) == s.counts.map { $0 * 2 }) } - // MARK: Review additions (Gate B round 1) + // MARK: Review additions /// S3. C Kα (0.2774 keV) at 110 eV: 2.5 (E - 5.8987) 1000 + 110² < 0, eXSpy's math.sqrt raises ValueError. /// Mutation: drop the `guard fwhmE >= 0` in `XRayLines.fwhm` -> NaN, red. diff --git a/Tests/SpectroCoreTests/SpectroscopyDriveL10Tests.swift b/Tests/SpectroCoreTests/SpectroscopyDriveL10Tests.swift index cb3d035..4f886d9 100644 --- a/Tests/SpectroCoreTests/SpectroscopyDriveL10Tests.swift +++ b/Tests/SpectroCoreTests/SpectroscopyDriveL10Tests.swift @@ -3,7 +3,7 @@ import Testing import SpectroCore import FormatoEMD -/// Lane L10 (the owner's drive findings, build 2): Velox's own element selection, read from the file (`VeloxEMDReader.storedElementSelection`). +/// Velox's own element selection, read from the file (`VeloxEMDReader.storedElementSelection`). /// The files are 600 to 700 byte synthetic Velox EMDs (a Version dataset and one quantification-settings group of variable-length JSON /// strings, the layout rsciio's `_parse_metadata_group` reads), written with h5py: `Operations/ImageQuantificationOperation` (version 9) /// and `SharedProperties/EDSSpectrumQuantificationSettings` (version 11), each entry the JSON `{"elementSelection": [atomic numbers], ...}`. diff --git a/Tests/SpectroCoreTests/SpectroscopyDriveL12Tests+Spectro.swift b/Tests/SpectroCoreTests/SpectroscopyDriveL12Tests+Spectro.swift index f5136f5..8ce5f48 100644 --- a/Tests/SpectroCoreTests/SpectroscopyDriveL12Tests+Spectro.swift +++ b/Tests/SpectroCoreTests/SpectroscopyDriveL12Tests+Spectro.swift @@ -1,10 +1,8 @@ -// SpectroKit S1 test partition (ADR 064 section 3.3): the methods of SpectroscopyDriveL12Tests that move with the EDX core; -// the rest stay in SpectroscopyDriveL12Tests.swift. Helpers are duplicated, not shared, across the split. // // SpectroscopyDriveL12Tests.swift -// Lane L12 (WP4): the three registered Auto ID rules (`ProposalRules`, Core) on hand-built `ProposalResult`s, and how +// The three Auto ID rules (`ProposalRules`, Core) on hand-built `ProposalResult`s, and how // `AutoIDPresentation.outcome(rules:)` shows them. No compute, no app driving. Every test names the mutation it catches. -// Registration: docs/archive/v5/wp4-autoid-velox-preregistration-2026-10-07.md (H1 never Z >= 89 and drop an L/M pick within +// The rules: (H1 never Z >= 89 and drop an L/M pick within // 1.5 FWHM of another proposed candidate's K alpha; H2 an L/M pick needs net / L_D >= 3; H3 a held sum-peak candidate at // net / L_D >= 10 is released). // @@ -138,7 +136,7 @@ struct SpectroscopyDriveL12Spectro { #expect(picked(s, [candidate("Eu", "Eu_La", significance: 1.5), held("Ar", "Ar_Ka", significance: 20)]) == ["Eu"], "shipped: no 3 x L_D bar, no release") } - /// A mixed result through the registered rules: K untouched, L/M cut at 3, actinide gone, strong hold released; the proposer's own result is unchanged. + /// A mixed result through the `registered` rules: K untouched, L/M cut at 3, actinide gone, strong hold released; the proposer's own result is unchanged. /// Mutation: `apply` rebuilds `raw` with the picks (the raw proposed list would shrink). @Test func testRegisteredOnAMixedResultAndTheRawProposalIsUntouched() { let cs = [candidate("Cu", "Cu_Ka", significance: 27), candidate("Ho", "Ho_La", significance: 1.9), candidate("In", "In_La", significance: 11.7), @@ -155,11 +153,11 @@ struct SpectroscopyDriveL12Spectro { #expect(ProposalRules.none.apply(r).picks.map(\.element) == r.proposed.map(\.element)) } - /// Supervisor, after the refuter (2026-10-07): the shipped set drops an actinide but keeps an L pick beside a proposed Kα (Hf Lα + /// After an independent review (2026-10-07): the shipped set drops an actinide but keeps an L pick beside a proposed Kα (Hf Lα /// 1.0 FWHM from Cu Kα: real on a Cu grid). Mutation: `shipped.besideK` true → red. @Test func testTheShippedSetKeepsAnLPickBesideAProposedK() throws { #expect(ProposalRules.shipped.hygiene); #expect(!ProposalRules.shipped.besideK) - #expect(ProposalRules.registered.besideK, "the registration had both halves") + #expect(ProposalRules.registered.besideK, "the fixed rule set had both halves") let hf = candidate("Hf", "Hf_La", significance: 4) let cu = candidate("Cu", "Cu_Ka", significance: 30) let th = candidate("Th", "Th_Ma", significance: 5) diff --git a/Tests/SpectroCoreTests/SpectroscopyFitRangeTests+Spectro.swift b/Tests/SpectroCoreTests/SpectroscopyFitRangeTests+Spectro.swift index ca11f62..c0d74bf 100644 --- a/Tests/SpectroCoreTests/SpectroscopyFitRangeTests+Spectro.swift +++ b/Tests/SpectroCoreTests/SpectroscopyFitRangeTests+Spectro.swift @@ -1,12 +1,10 @@ -// SpectroKit S1 test partition (ADR 064 section 3.3): the methods of SpectroscopyFitRangeTests that move with the EDX core; -// the rest stay in SpectroscopyFitRangeTests.swift. Helpers are duplicated, not shared, across the split. import Foundation import Testing import SpectroTestSupport import SpectroCore import SpectroEDX -/// WP3c, "the default fit range, not the continuum" (docs/archive/v5/wp3c-fit-range-preregistration-2026-10-06.md, P1-P6). +/// WP3c, "the default fit range, not the continuum". /// The default upper fit limit is min(axis end, beam, 20 keV), decided on the file's axis; a typed limit is Expert /// (`QuantificationMethod.fitToKeV`, nil = default, absent from the JSON); the at% row falls back from an unsupported K-alpha /// to the highest supported group, named; a range-sensitivity statement (one fit to the axis end, not a sigma term) runs diff --git a/Tests/SpectroCoreTests/SpectroscopyFitTests.swift b/Tests/SpectroCoreTests/SpectroscopyFitTests.swift index f13a74a..9b6d7ce 100644 --- a/Tests/SpectroCoreTests/SpectroscopyFitTests.swift +++ b/Tests/SpectroCoreTests/SpectroscopyFitTests.swift @@ -656,7 +656,7 @@ struct SpectroscopyFitTests { } } - // MARK: 11. Round 2: weak-line recovery on two generators, the registered selection, the tail finding + // MARK: 11. Weak-line recovery on two generators, the form selection, the tail finding /// One (form, generator) cell: Mg fitted over `seeds` Poisson draws of each level's expected spectrum. private struct Cell { var bias: Double; var se: Double; var sigma: Double } @@ -700,7 +700,7 @@ struct SpectroscopyFitTests { return (a, s) } - /// The registered criterion (docs: report2.md, written before the runs). Gate 1: no false positive at Mg = 0 + /// The criterion, fixed before the runs. Check 1: no false positive at Mg = 0 /// (mean <= 0.399 sigma_sandwich + 1 se: NNLS clamps, so an unbiased estimator has mean +0.399 sigma); /// selection: smallest max |bias| at 0.3 % and 1 % over both generators; if not <= 1 se at 0.3 % on both, badge. @Test func testWeakLineRecoveryRegisteredSelection() throws { @@ -730,14 +730,14 @@ struct SpectroscopyFitTests { // The measured band the badge quotes (counts): a deficit of 150-450 on both generators at 0.3 % and 1 %. for cell in [c.a[2], c.a[3], c.s[2], c.s[3]] { #expect((-450 ... -150).contains(cell.bias), "bias \(cell.bias)") } #expect(ContinuumForm.weakLineBiasNote.contains("eak-line bias measured on synthetic data")) - // The badge quotes the measured numbers (rounded): the registered table's Kramers (9,5) cells. + // The badge quotes the measured numbers (rounded): the selection table's Kramers (9,5) cells. for v in [c.a[2].bias, c.a[3].bias, c.s[2].bias, c.s[3].bias] { #expect(ContinuumForm.weakLineBiasNote.contains(String(format: "\u{2212}%.0f \u{00B1}", abs(v))), "badge lacks \(v)") } } /// The finding behind the bias on the simulator: with its Al K-alpha tail supplied as a reference shape the Mg bias - /// collapses (noise-free fit of the 1 % pool). The slot, not the continuum, is where the owner's pure-Al spectrum helps. + /// collapses (noise-free fit of the 1 % pool). The slot, not the continuum, is where a measured pure-Al spectrum helps. @Test func testTailReferenceShapeRemovesTheSimulatorWeakLineBias() throws { let sim = Self.simPools, axis = sim.axis let level = sim.levels[3] diff --git a/Tests/SpectroCoreTests/SpectroscopyLaneUTests+Spectro.swift b/Tests/SpectroCoreTests/SpectroscopyLaneUTests+Spectro.swift index 5d91fac..231c0b1 100644 --- a/Tests/SpectroCoreTests/SpectroscopyLaneUTests+Spectro.swift +++ b/Tests/SpectroCoreTests/SpectroscopyLaneUTests+Spectro.swift @@ -1,12 +1,10 @@ -// SpectroKit S1 test partition (ADR 064 section 3.3): the methods of SpectroscopyLaneUTests that move with the EDX core; -// the rest stay in SpectroscopyLaneUTests.swift. Helpers are duplicated, not shared, across the split. import Foundation import Testing import SpectroTestSupport import SpectroCore import SpectroEDX -/// v5.0 lane U: the first drive's wiring, text and presentation findings (docs/archive/v5/room-drive-2026-10-06.md, 3-13). +/// v5.0 lane U: the first drive's wiring, text and presentation findings. /// Each test names the mutation that must turn it red. /// The app labels a voltage supplied by the same run's 4D cube this way (Core/Data/GMSFourDRegistration.swift, which stays in /// the app); the library sees only `.supplied(label:)`, so these moving tests name the value, not the app's constant. diff --git a/Tests/SpectroCoreTests/SpectroscopyProposerTests.swift b/Tests/SpectroCoreTests/SpectroscopyProposerTests.swift index f75cbb1..32e443b 100644 --- a/Tests/SpectroCoreTests/SpectroscopyProposerTests.swift +++ b/Tests/SpectroCoreTests/SpectroscopyProposerTests.swift @@ -108,7 +108,7 @@ struct SpectroscopyProposerTests { // MARK: 1. Realistic continuum: no false Mg at Mg = 0, the detection rate at 0.3 % and 1 % /// A joint fit tests ~115 line groups, so about one spectrum in twenty shows a chance element at L_D (alpha = beta = 0.05 per - /// candidate, the registered rule; the proposal says so). Tests therefore assert on the NAMED elements (Mg, Si, Na, never + /// candidate, by rule; the proposal says so). Tests therefore assert on the NAMED elements (Mg, Si, Na, never /// proposed) and give chance elements a budget that a modelling fault (a continuum misfit) would exceed. private static let chanceBudgetPer50 = 5 @@ -249,7 +249,7 @@ struct SpectroscopyProposerTests { return (r, Self.sim.regions[region].components) } - /// The registered T2 core, on lane S's simulator at 10, 100 and 300 counts/px: where the truth sum peak is above 1.5 L_D it is + /// The T2 core, on the EDX forward model (tools/edx-sim) at 10, 100 and 300 counts/px: where the truth sum peak is above 1.5 L_D it is /// NAMED (a candidate on its energy carries the sum conflict, or the sum column itself is detected); where it is below /// 0.5 L_D nothing is named at its energy. L_D is the limit of whatever sits at that energy in the same proposal. @Test func testSumPeaksAreNamedExactlyWhereTruthExceedsTheDetectionLimit() throws { @@ -397,7 +397,7 @@ struct SpectroscopyProposerTests { #expect(look.contains("candidates were tested"), "\(look)") } - /// Lane Sigma (Gate D): the REPORTED covariance is a passive-set sandwich, so the fit's own sigma predicts the scatter of the + /// The REPORTED covariance is a passive-set sandwich, so the fit's own sigma predicts the scatter of the /// Al K-alpha area over 200 Poisson draws (the SD of an SD at 200 draws is 5 %). Before: the full-design pseudo-inverse counted the /// bound-active Mg column as free and gave ratio 0.81 (P9); predicted after: [0.85, 1.15], pinned [0.90, 1.15]; measured 1.03 (seeds 9000+s). /// NOT claimed: conservatism on weak lines. Fable's review measured empirical SD / sigma on a weak Mg line at 1.05 / 1.06 / 1.08 (LS, Mg 1/3/10 %), diff --git a/Tests/SpectroCoreTests/SpectroscopyQuantTests.swift b/Tests/SpectroCoreTests/SpectroscopyQuantTests.swift index 40405ab..8577534 100644 --- a/Tests/SpectroCoreTests/SpectroscopyQuantTests.swift +++ b/Tests/SpectroCoreTests/SpectroscopyQuantTests.swift @@ -4,10 +4,9 @@ import SpectroTestSupport import SpectroCore import SpectroEDX -/// WP3 lane K: k-factors, Cliff-Lorimer / zeta / cross-section quantification and +/// k-factors, Cliff-Lorimer / zeta / cross-section quantification and /// absorption. Every expected number below was produced by running eXSpy 7185a4d1 -/// + hyperspy 2.4.0 (venv under the research scratchpad; logs in -/// docs/archive/v5/edx-research-2026-10-05/) -- full precision where the upstream +/// + hyperspy 2.4.0 -- full precision where the upstream /// test's own tolerance is looser. The Al-Zn window intensities are the values the /// synthetic image in eXSpy's test_eds_tem.py yields from `get_lines_intensity()`. struct SpectroscopyQuantTests { @@ -102,9 +101,8 @@ struct SpectroscopyQuantTests { // Order invariance (eXSpy test_quant_lorimer_ac reversed lists). let r = try CliffLorimer.weightFractions(intensities: alZnIntensities.reversed(), kFactors: alZnK.reversed()) assertRel(r[1], w[0], 1e-12) - // FePt, k [1.450226, 5.075602]. NOTE the pre-registration pairs 15.409297 at% with - // the raw windows 3710/15872; eXSpy's own run (fept_quant-2026-10-05.log) gets it - // from the BACKGROUND-SUBTRACTED windows 2754/15090. Raw windows give 18.9171866. + // FePt, k [1.450226, 5.075602]. eXSpy gets 15.409297 at% from the + // BACKGROUND-SUBTRACTED windows 2754/15090. Raw windows give 18.9171866. func feAt(_ i: [Double]) throws -> (wt: Double, at: Double) { let f = try CliffLorimer.weightFractions(intensities: i, kFactors: [1.450226, 5.075602]) let a = try CompositionConversion.weightToAtomic(f.map { $0 * 100 }, elements: ["Fe", "Pt"]) diff --git a/Tests/SpectroCoreTests/SpectroscopyQuantifyTests+Spectro.swift b/Tests/SpectroCoreTests/SpectroscopyQuantifyTests+Spectro.swift index 7eb03df..5836aec 100644 --- a/Tests/SpectroCoreTests/SpectroscopyQuantifyTests+Spectro.swift +++ b/Tests/SpectroCoreTests/SpectroscopyQuantifyTests+Spectro.swift @@ -1,5 +1,3 @@ -// SpectroKit S1 test partition (ADR 064 section 3.3): the methods of SpectroscopyQuantifyTests that move with the EDX core; -// the rest stay in SpectroscopyQuantifyTests.swift. Helpers are duplicated, not shared, across the split. import Foundation import Testing import SpectroTestSupport diff --git a/Tests/SpectroCoreTests/SpectroscopyUnlistedLineTests+Spectro.swift b/Tests/SpectroCoreTests/SpectroscopyUnlistedLineTests+Spectro.swift index ceca48d..41aef8a 100644 --- a/Tests/SpectroCoreTests/SpectroscopyUnlistedLineTests+Spectro.swift +++ b/Tests/SpectroCoreTests/SpectroscopyUnlistedLineTests+Spectro.swift @@ -1,14 +1,12 @@ -// SpectroKit S1 test partition (ADR 064 section 3.3): the methods of SpectroscopyUnlistedLineTests that move with the EDX core; -// the rest stay in SpectroscopyUnlistedLineTests.swift. Helpers are duplicated, not shared, across the split. import Foundation import Testing import SpectroCore import SpectroEDX -/// WP3b F1 (docs/archive/v5/wp3b-fit-robustness-preregistration-2026-10-06.md, P4 and P5): after a Quantify fit the +/// WP3b F1: after a Quantify fit the /// proposer names every unlisted line at or above L_D (sum-peak questions included), and at% is withheld only when fitting /// them moves a quantified net by more than its reported sigma. Synthetic pool: `Fixtures/eds-unlisted-sim.json` -/// (generator `tools/edx-pins/unlisted_sim.py`, the refuter's S2c recipe), seeded Poisson noise here. The assertions are +/// (generator `tools/edx-sim/unlisted_sim.py`), seeded Poisson noise here. The assertions are /// the DECISIONS (named, withheld), not exact nets, so a continuum change (lane F23) does not re-band them. @MainActor struct SpectroscopyUnlistedLineSpectro { @@ -129,7 +127,7 @@ struct SpectroscopyUnlistedLineSpectro { #expect(namedSomewhere || check.names.contains("Ca")) } - // MARK: A2 (Gate D 2026-10-06: ship the quantity, label what is beside a listed line) + // MARK: ship the quantity, label what is beside a listed line private func groups(_ elements: [String]) -> [FitLineGroup] { EDSLineModel.build(elements: elements, axis: Self.pool.axis, beamEnergy: Self.pool.beam, resolutionMnKaEV: 130, escapePeaks: false).groups diff --git a/Tests/SpectroCoreTests/SpectrumImageOpeningTests+Spectro.swift b/Tests/SpectroCoreTests/SpectrumImageOpeningTests+Spectro.swift index 8401df9..e69eff1 100644 --- a/Tests/SpectroCoreTests/SpectrumImageOpeningTests+Spectro.swift +++ b/Tests/SpectroCoreTests/SpectrumImageOpeningTests+Spectro.swift @@ -1,5 +1,3 @@ -// SpectroKit S1 test partition (ADR 064 section 3.3): the methods of SpectrumImageOpeningTests that move with the EDX core; -// the rest stay in SpectrumImageOpeningTests.swift. Helpers are duplicated, not shared, across the split. import Foundation import Testing import SpectroTestSupport diff --git a/Tests/SpectroCoreTests/SumColumnDedupTests.swift b/Tests/SpectroCoreTests/SumColumnDedupTests.swift index f5e404b..c27e244 100644 --- a/Tests/SpectroCoreTests/SumColumnDedupTests.swift +++ b/Tests/SpectroCoreTests/SumColumnDedupTests.swift @@ -4,9 +4,9 @@ import SpectroTestSupport import SpectroCore import SpectroEDX -/// Auto ID failed on six of the owner's Velox entries with `ProposerError.rankDeficient`: `ElementProposer.sumColumns` added +/// Auto ID failed on six measured Velox entries with `ProposerError.rankDeficient`: `ElementProposer.sumColumns` added /// one pile-up column per parent pair, and two pairs whose energies coincide in the line table (Dy+Ti and Cs+Ho at 11.0061 keV) -/// gave two identical design columns. Registered fix: `docs/archive/v5/sum-column-dedup-preregistration-2026-10-08.md` +/// gave two identical design columns. The fix /// (a sum column within `sumPeakToleranceKeV` of one already added in the pass is not added again; the kept column's label names /// every pair). The spectra are generated from the fit's own line model with seeded Gaussian-approximated counting noise. struct SumColumnDedupTests { diff --git a/Tests/SpectroCoreTests/SumEscapeIdenticalTests.swift b/Tests/SpectroCoreTests/SumEscapeIdenticalTests.swift index 19be026..255d1ff 100644 --- a/Tests/SpectroCoreTests/SumEscapeIdenticalTests.swift +++ b/Tests/SpectroCoreTests/SumEscapeIdenticalTests.swift @@ -6,9 +6,9 @@ import SpectroEDX /// Auto ID failed with `ProposerError.rankDeficient` on a pile-up pair whose energy equals a ONE-COMPONENT column of a candidate: /// V K-alpha escape 3.2125 keV = Tb M-alpha + Ir M-alpha, on a spectral axis ending at 5.115 keV. There V's group is V K-alpha -/// alone, so its escape column is one Gaussian at 3.2125 keV, and the pile-up column is the same Gaussian. Registered fix: -/// `docs/archive/v5/sum-escape-only-preregistration-2026-10-12.md` (a pair is not added as a column when a group of the design the -/// fit serves has a one-component ESCAPE column at its energy; one-line LINE groups are not compared, owner card A1 option a). The spectra are generated from the fit's own line model with seeded +/// alone, so its escape column is one Gaussian at 3.2125 keV, and the pile-up column is the same Gaussian. The fix +/// (a pair is not added as a column when a group of the design the +/// fit serves has a one-component ESCAPE column at its energy; one-line LINE groups are not compared by choice). The spectra are generated from the fit's own line model with seeded /// Gaussian-approximated counting noise, as in `SumColumnDedupTests`. struct SumEscapeIdenticalTests { @@ -66,7 +66,7 @@ struct SumEscapeIdenticalTests { /// V K-alpha escape 3.2125 keV = Tb M-alpha 1.2326 + Ir M-alpha 1.9799. Without the fix every amplitude throws `rankDeficient` /// (the pile-up column and V's escape column are identical design columns); with it the run scores. At 40 000 V claims the /// energy and no pile-up column sits there. At 0 and 300 V is not in the final design, so the pair is compared with nothing and - /// keeps its own column (registration: docs/archive/v5/sum-escape-only-preregistration-2026-10-12.md, P1 and P2). + /// keeps its own column. @Test func testVKaEscapeEqualToTbMaPlusIrMaScoresOnAShortAxis() throws { let sum = XRayLines.line("Tb_Ma")!.energy + XRayLines.line("Ir_Ma")!.energy let escape = XRayLines.line("V_Ka")!.energy - XRayLines.line("Si_Ka")!.energy @@ -113,8 +113,8 @@ struct SumEscapeIdenticalTests { /// La K-alpha 33.4419 keV = Cd K-alpha 23.1737 + Tl L-alpha 10.2682, and La's K group is the single line K-alpha on an axis that /// tops out between K-alpha and K-beta (33.5 to 37.8 keV; here 35.98; the fit range must be the Expert one, to the axis top). /// The held patch's LINE branch would have dropped the pile-up pair at La's energy and let La's column take its counts (a false - /// La); the owner chose against it (card A1, option a), so the rule compares ESCAPE columns only. Measured on HEAD before the - /// change (registration, addendum): a planted pile-up of 120 to 200 counts, which La absorbs without reaching L_D (not claiming), + /// La); that was decided against, so the rule compares ESCAPE columns only. Measured on HEAD before the + /// change: a planted pile-up of 120 to 200 counts, which La absorbs without reaching L_D (not claiming), /// throws `rankDeficient`; with the line branch the same runs complete with La at net 106 to 185. Pinned: 160 counts throws the /// clear error; with none planted the Cd + Tl pair keeps its column (net 0) beside La (net 54, below L_D); at 2 000 La claims /// the energy and is a sum-peak question. La is never in `proposed`. diff --git a/Tests/SpectroEDXTests/AxisGateTests.swift b/Tests/SpectroEDXTests/AxisGateTests.swift index 87890e6..51fa3ee 100644 --- a/Tests/SpectroEDXTests/AxisGateTests.swift +++ b/Tests/SpectroEDXTests/AxisGateTests.swift @@ -1,6 +1,6 @@ // // AxisGateTests.swift -// Role: EDX stage gate G-E (docs/edx-stage-gates-registration.md section 3 and amendment 1): the energy axis. +// Role: The energy axis against exSpy, HyperSpy and planted truth. // // E1: `VeloxEnergyAxis` / the opener's `EnergyAxis` equal RosettaSciIO's `_get_dispersion_offset` exactly (a reader rule) on every public // Velox file and on planted headers (decoy detector, differing offsets, detector keys out of index order); @@ -100,9 +100,9 @@ struct AxisGateTests { /// E1 file order. Registration amendment 1, item 5: keys in file order Detector-0, Detector-10, Detector-2. /// RosettaSciIO takes Detector-10 (20 eV, -1900 eV) by FILE order. Until FormatoKit 0.0.3 `VeloxMetadata.parse` sorted the - /// detectors by index and took Detector-2 (10 eV, -500 eV): a registered finding, then a strict `withKnownIssue` here; + /// detectors by index and took Detector-2 (10 eV, -500 eV): a recorded finding, then a strict `withKnownIssue` here; /// FormatoKit 0.0.3 (its registration step 1, A7) takes them in the file's written order, so the rule holds and the - /// wrapper is gone (no public file and not the owner's header writes its keys out of index order). + /// wrapper is gone (no public file and no measured file's header writes its keys out of index order). @Test func testE1DetectorKeysOutOfIndexOrderFollowTheFileOrder() throws { let expected = pinned(rule.d("planted")["axis-order"]!) #expect(expected[0].cal == Cal(dispersionKeV: 0.02, offsetKeV: -1.9), "rsciio's rule takes Detector-10") @@ -235,11 +235,11 @@ struct AxisGateTests { let badGain = results.indices.filter { abs(results[$0].gainErr) > 1e-4 } gateNote("EDX E3 swift outside 1 eV: \(badOff.count) of \(results.count); outside 1e-4 gain: \(badGain.count)") // What holds today and is asserted: the lines land within 5 eV on the refined axis (measured max below) and the - // refinement improves the fit. Not registered bounds: they guard the measured behaviour against a regression. + // refinement improves the fit. Not upstream bounds: they guard the measured behaviour against a regression. let worstPos = results.flatMap { $0.posErr.map(abs) }.max()! gateNote("EDX E3 swift worst line position error \(worstPos) eV") #expect(worstPos < 1.2, "measured worst line position error on the refined axis") - withKnownIssue("registered finding (section 7, Predicted for E3): offset and gain are not recovered within 1 eV and 1e-4 on spectra whose lines span 1.25-1.84 keV; they trade off") { + withKnownIssue("known finding (predicted for E3): offset and gain are not recovered within 1 eV and 1e-4 on spectra whose lines span 1.25-1.84 keV; they trade off") { #expect(badOff.isEmpty, "offset outside 1 eV: \(badOff.count) of 384, e.g. \(badOff.prefix(3).map { "\(rows[$0].id)#\(rows[$0].real) \(String(format: "%.2f", results[$0].offErr)) eV" })") #expect(badGain.isEmpty, "gain outside 1e-4: \(badGain.count) of 384, e.g. \(badGain.prefix(3).map { "\(rows[$0].id)#\(rows[$0].real) \(String(format: "%.2e", results[$0].gainErr))" })") } @@ -280,7 +280,7 @@ struct AxisGateTests { #expect(v == [0.0198877, 0.0201678], "gain \(g)") } #expect(resolutionRaised == 2) - // the registered prediction "within +-0.2 eV at N_Al >= 1e6" is FALSE for exSpy (amendment 1 item 2): + // the prediction "within +-0.2 eV at N_Al >= 1e6" is FALSE for exSpy: #expect(e6.map { abs($0) }.max()! > 0.2) } } diff --git a/Tests/SpectroEDXTests/CountsGateTests.swift b/Tests/SpectroEDXTests/CountsGateTests.swift index 59a40e1..851c6f2 100644 --- a/Tests/SpectroEDXTests/CountsGateTests.swift +++ b/Tests/SpectroEDXTests/CountsGateTests.swift @@ -1,6 +1,6 @@ // // CountsGateTests.swift -// Role: EDX stage gate G-C (docs/edx-stage-gates-registration.md section 3): the Velox stream decode and the spectrum-image +// Role: The Velox stream decode and the spectrum-image // sums equal an independent decode, exactly (integer equality, no tolerance). // // C1: `SpectrumImageOpener` / `VeloxEMDReader` against pins.json's NumPy decode of RosettaSciIO's public files (the decode diff --git a/Tests/SpectroEDXTests/IdentificationLoopTests.swift b/Tests/SpectroEDXTests/IdentificationLoopTests.swift index e638b5c..3073e10 100644 --- a/Tests/SpectroEDXTests/IdentificationLoopTests.swift +++ b/Tests/SpectroEDXTests/IdentificationLoopTests.swift @@ -1,7 +1,7 @@ // // IdentificationLoopTests.swift // Role: the identification loop of docs/design/auto-id-tem.md (the residual-peak loop with the 1 % model term) on planted spectra, its -// filter and statistic, and its thresholds as named values. The planted pair is the case that hid Mg on the owner's file 1339: +// filter and statistic, and its thresholds as named values. The planted pair is the case that hid Mg on a measured Al-alloy map: // Mg Kα at 2 % of Al Kα beside an Al whose lines are 5 % narrower than the model fits them (a core misfit like the real detector's). // @@ -12,7 +12,7 @@ import SpectroCore struct IdentificationLoopTests { - private static let axis = EnergyAxis(offset: 0, scale: 0.005, size: 4000) // 0 to 20 keV, like the owner's files + private static let axis = EnergyAxis(offset: 0, scale: 0.005, size: 4000) // 0 to 20 keV, like the measured files private static let energies = (0.. 0) } } diff --git a/Tests/SpectroKitParityTests/AlignPinsTests.swift b/Tests/SpectroKitParityTests/AlignPinsTests.swift index 3fc2e25..b5e1350 100644 --- a/Tests/SpectroKitParityTests/AlignPinsTests.swift +++ b/Tests/SpectroKitParityTests/AlignPinsTests.swift @@ -1,6 +1,6 @@ // // AlignPinsTests.swift -// Role: Task 16 step 1 (docs/design/eels-task-16.md sections 2 and 7) against exSpy 0.3.2 / HyperSpy 2.4.0 (tools/eels-task16/pins.py -> +// Role: Zero-loss alignment against exSpy 0.3.2 / HyperSpy 2.4.0 (tools/eels-task16/pins.py -> // Fixtures/eels16-pins.json): the zero-loss peak centre and mask, `estimate_shift1D`, `shift1D` and the eleven `align_zero_loss_peak` // variants (offset, size, scale, data, the shifts applied, `also_align`). // diff --git a/Tests/SpectroKitParityTests/DeconvolutionPinsTests.swift b/Tests/SpectroKitParityTests/DeconvolutionPinsTests.swift index 6b38fb7..503eb9b 100644 --- a/Tests/SpectroKitParityTests/DeconvolutionPinsTests.swift +++ b/Tests/SpectroKitParityTests/DeconvolutionPinsTests.swift @@ -1,8 +1,8 @@ // // DeconvolutionPinsTests.swift -// Role: Task 16 step 4 against exSpy 0.3.2 / HyperSpy 2.4.0 (Fixtures/eels16-pins.json): `optimal_fft_size`, `estimate_peak_width`, Fourier-log +// Role: Fourier-log and Fourier-ratio deconvolution against exSpy 0.3.2 / HyperSpy 2.4.0 (Fixtures/eels16-pins.json): `optimal_fft_size`, `estimate_peak_width`, Fourier-log // and Fourier-ratio deconvolution. Upstream has no tolerance for the deconvolutions (`test_running` only runs), so the kit's decision -// stands (docs/design/eels-task-16.md section 4): per pixel rtol 1e-7 + atol 1e-7 x max|pinned result|; sizes and channel counts exact. +// stands (docs/design/eels-chain.md section 4): per pixel rtol 1e-7 + atol 1e-7 x max|pinned result|; sizes and channel counts exact. // import Foundation diff --git a/Tests/SpectroKitParityTests/EDSQuantPinsTests.swift b/Tests/SpectroKitParityTests/EDSQuantPinsTests.swift index 4e9fc11..cf9df5f 100644 --- a/Tests/SpectroKitParityTests/EDSQuantPinsTests.swift +++ b/Tests/SpectroKitParityTests/EDSQuantPinsTests.swift @@ -4,7 +4,7 @@ import SpectroTestSupport import SpectroCore import SpectroEDX -/// eXSpy quantification pins (oracle 1), pre-registered in docs/archive/v5/edx-quant-pins-preregistration-2026-10-07.md. +/// eXSpy quantification pins (tools/edx-exspy-pins/quant_pins.py). /// Every expected number in `Fixtures/eds-quant-pins-exspy-7185a4d1.json` is what eXSpy 7185a4d1 + hyperspy 2.4.0 returned /// from `EDSTEMSpectrum.quantification` (generator `tools/edx-pins/quant_pins.py`): Cliff-Lorimer and zeta, with and without the /// absorption correction, on eXSpy's FePt example and on synthetic 2-, 3- and 4-element and near-zero intensity sets. This file diff --git a/Tests/SpectroKitParityTests/EdgePackPinsTests.swift b/Tests/SpectroKitParityTests/EdgePackPinsTests.swift index 3a96f61..5a5ae73 100644 --- a/Tests/SpectroKitParityTests/EdgePackPinsTests.swift +++ b/Tests/SpectroKitParityTests/EdgePackPinsTests.swift @@ -3,7 +3,7 @@ // Role: every K, L and M edge exSpy builds with the dft GOS (0.12.0, Paul 2026-10-23: "we need to put everything in") against exSpy 7185a4d1's pins in // Fixtures/eelsall-edges-pins.json (tools/eels-task16/edges.py --all): all 231 tables of the pack bit-exact (sha256 of the float32 bytes), and // one edge per shell and table kind (B K; Ti L1, L2, L3; Zr M2, M3; Ba M4, M5) with qint, the effective angle and function(E) inside the -// registered edge bounds (EdgePinsTests: angle 1e-14, qint 1e-12, function 1e-10). +// edge bounds (EdgePinsTests: angle 1e-14, qint 1e-12, function 1e-10). // import CryptoKit diff --git a/Tests/SpectroKitParityTests/EdgePinsTests.swift b/Tests/SpectroKitParityTests/EdgePinsTests.swift index e5c0008..703d06f 100644 --- a/Tests/SpectroKitParityTests/EdgePinsTests.swift +++ b/Tests/SpectroKitParityTests/EdgePinsTests.swift @@ -1,10 +1,10 @@ // // EdgePinsTests.swift -// Role: EELS slice 2, gate 1 and gate 2 (docs/eels-slice-2-registration.md sections 4.1, 4.3, 7): the port against exSpy +// Role: The edge cross section and the ratio method against exSpy // 7185a4d1's pins in Fixtures/edge-pins.json (written by tools/eels-edge/pins.py before the Swift code), and against // pyEELSMODEL eed9f072's cross sections. // -// Bounds, registered: effective angle relative 1e-14; qint 1e-12; function(E) and sigma_k 1e-10; background indices equal; +// Bounds: effective angle relative 1e-14; qint 1e-12; function(E) and sigma_k 1e-10; background indices equal; // I1, I2, r, A, the background, I_k, I_b 1e-12; n 1e-10. pyEELSMODEL: within 3 %. // @@ -16,7 +16,7 @@ import SpectroEELS struct EdgePinsTests { - // Gate 1: the cross section on the whole registered grid (192 edge cases x 3 scales x 4 widths). + // The cross section on the whole pinned grid (192 edge cases x 3 scales x 4 widths). @Test func testCrossSectionEqualsExSpyOnTheGrid() throws { let grid = EdgeFixtures.pins["grid"] as! [[String: Any]] #expect(grid.count == 192) @@ -122,7 +122,7 @@ struct EdgePinsTests { dens.record(relativeDifference(px.atomsPerNM2.orFail(), dd.a("n")[p]), tag) compared += 1 } - // The I_0 route (slice 1's fixed thresholds). + // The I_0 route (the thickness map's fixed thresholds). for (theta, tv) in dd.d("i0Route") { let t = tv as! [String: Any] let tm = try EELSThickness.logRatio(low, threshold: .fixed(Double(theta)!)) diff --git a/Tests/SpectroKitParityTests/EdgeTablePinsTests.swift b/Tests/SpectroKitParityTests/EdgeTablePinsTests.swift index 47d7c67..901e3c1 100644 --- a/Tests/SpectroKitParityTests/EdgeTablePinsTests.swift +++ b/Tests/SpectroKitParityTests/EdgeTablePinsTests.swift @@ -1,8 +1,8 @@ // // EdgeTablePinsTests.swift -// Role: EELS task 16 step 3 (docs/design/eels-task-16.md section 6.1): the K and L edges added to the bundled GOS tables against +// Role: The K and L edges added to the bundled GOS tables against // exSpy 7185a4d1's pins in Fixtures/eels16-edges-pins.json (tools/eels-task16/edges.py). The tables are bit-exact -// (sha256 of the float32 bytes); qint, the effective angle and function(E) follow the registered edge bounds +// (sha256 of the float32 bytes); qint, the effective angle and function(E) follow the edge bounds // (EdgePinsTests: angle 1e-14, qint 1e-12, function 1e-10). // diff --git a/Tests/SpectroKitParityTests/GateDTests.swift b/Tests/SpectroKitParityTests/GateDTests.swift index 02119c1..a75dc8c 100644 --- a/Tests/SpectroKitParityTests/GateDTests.swift +++ b/Tests/SpectroKitParityTests/GateDTests.swift @@ -1,13 +1,12 @@ // // GateDTests.swift -// Role: The registered Gate D diagnosis of the Mg auto-ID miss (docs/edx-stage-gates-registration.md section 8), D0 to D5, -// run in that order against the SHIPPED `ElementProposer` on the planted spectra. D6 (the owner's real files) is not run: -// they are not on this Mac. +// Role: The diagnosis of the Mg auto-ID miss, experiments D0 to D5, run in that order against the SHIPPED `ElementProposer` +// on the planted spectra. D6 (measured files) is not run here. // -// These are experiments, not gates: each prints (EDX_GATE_LOG) the observation, the registered refuting observation and the -// registered prediction, and the verdict computed from them. The assertions are the structural facts an experiment rests on +// These are experiments, not gates: each prints (EDX_GATE_LOG) the observation, the refuting observation stated beforehand and the +// prediction, and the verdict computed from them. The assertions are the structural facts an experiment rests on // (the runs return; the matched control behaves as a control), so a broken proposer or loader still fails them. The method of -// every number is in the test; the outcomes are recorded in the registration's results section. +// every number is in the test; the outcomes are in the log it prints. // import Foundation @@ -50,7 +49,7 @@ struct GateDTests { gateNote("EDX D0: \(ids.count) cells, \(spectra) spectra (noise-free mean + first 10 realisations): single and full pool differ by a decision in \(decisionDiffers), by more than 10 % of the net in \(netDiffers); full pool loses a Mg the single test proposes in \(fullLoses). \(examples)") gateNote("EDX D0 verdict: \(fullLoses == 0 ? "REFUTED (no pruning loss: the full pool and the single-candidate design agree where it matters)" : "SUPPORTED on \(fullLoses) spectra") [registered prediction: same net in both, D0 refuted: \(netDiffers == 0 && decisionDiffers == 0 ? "met" : "NOT met in \(netDiffers + decisionDiffers) cases")]") #expect(spectra > 100) - // measured outcome guarded (not a registered bound): the full pool loses Mg on some spectra the single test proposes (9 here) + // measured outcome guarded (not an upstream bound): the full pool loses Mg on some spectra the single test proposes (9 here) #expect(fullLoses >= 1, "D0 measured: pruning by the pool costs Mg on some spectra") // the control: on matched, strongly detectable data both designs find Mg let ctl = RunStore.shared.full("M.s.N6.rho0p05", reals: 10) @@ -150,7 +149,7 @@ struct GateDTests { gateNote("EDX D2 refine-then-propose \(id), net/truth (P = proposed): " + parts.joined(separator: "; ")) } } - // measured outcome guarded (not a registered bound): refining axis and R_MnKa first brings Mg back on a +3 eV axis error + // measured outcome guarded (not an upstream bound): refining axis and R_MnKa first brings Mg back on a +3 eV axis error let c3 = cell("S+3.s.N7.rho0p01") let refined = try #require(refineThenPropose(EDXFixtures.meanSpectrum(c3), elements: ["Mg", "Al", "Si"], useRefinedResolution: true)) #expect(ratio(refined, c3.truth["Mg"]!) > 0.9, "refine-then-propose recovers Mg on S+3 (measured 0.99)") @@ -197,7 +196,7 @@ struct GateDTests { let withRef = PlantedRuns.run(EDXFixtures.meanSpectrum(c), reference: ref) let without = RunStore.shared.single(id).mean if t == "T2" && rho == "0p01" { - // measured outcome guarded (not a registered bound): the true tail as a reference shape brings Mg back (0.64 -> 0.84) + // measured outcome guarded (not an upstream bound): the true tail as a reference shape brings Mg back (0.64 -> 0.84) #expect(ratio(withRef, truth) > (ratio(without, truth) + 0.1), "the true tail supplied as a reference shape recovers Mg") } gateNote("EDX D3 reference shape \(id): net/truth without \(f(ratio(without, truth))) with the true tail supplied \(f(ratio(withRef, truth))) (flank factor \(f(without.misfit, 1)) -> \(f(withRef.misfit, 1)), L_D \(f(without.ld, 0)) -> \(f(withRef.ld, 0)))") @@ -212,7 +211,7 @@ struct GateDTests { /// A noise-free matched spectrum whose line shapes are sampled at the channel centre (the fit model's convention, hyperspy's) /// instead of integrated over the channel (the generator's, registration 5.2): the stored mean with its bin-integrated Mg, Al /// and Si lines taken out and the same lines, same areas and widths, put back centre-sampled. The continuum is the generator's. - /// Not a registered set: the control that separates the line-shape sampling convention from every other misfit. + /// Not a planted set of the diagnosis: the control that separates the line-shape sampling convention from every other misfit. private func centreSampledMatched(_ c: EDXFixtures.Cell) -> [Double] { var out = EDXFixtures.meanSpectrum(c) let table: [(String, [(Double, Double)], Double)] = [ @@ -240,8 +239,8 @@ struct GateDTests { } /// D4. Too little Mg in the pooled spectrum (the recorded explanation). Registration: refuted if the planted spectra with a - /// right model (set M) detect at the level a Mg/Al ratio of 0.002 to 0.01 would deliver (by 20 to 50 times), AND the owner's - /// alloy is nominally >= 0.3 wt % Mg, AND the real Al Ka misfit is small. The last two need the owner (Q2, D6) and are not run. + /// right model (set M) detect at the level a Mg/Al ratio of 0.002 to 0.01 would deliver (by 20 to 50 times), AND the measured + /// alloy is nominally >= 0.3 wt % Mg, AND the real Al Ka misfit is small. The last two need measured files (D6) and are not run. /// What is measured here: the margin (planted Mg / L_D(M)) of the shipped proposer on set M, and the same on the centre-sampled /// control, which separates the line-shape sampling convention of the model from the rest. @Test func testD4TooLittleMgMargin() throws { diff --git a/Tests/SpectroKitParityTests/ModelPinsTests.swift b/Tests/SpectroKitParityTests/ModelPinsTests.swift index c88d0f9..08db339 100644 --- a/Tests/SpectroKitParityTests/ModelPinsTests.swift +++ b/Tests/SpectroKitParityTests/ModelPinsTests.swift @@ -1,9 +1,9 @@ // // ModelPinsTests.swift -// Role: Task 16 step 5 against exSpy 0.3.2 (Fixtures/eels16-model-pins.json, tools/eels-task16/model.py): `two_area_background_estimation`, +// Role: The edge model against exSpy 0.3.2 (Fixtures/eels16-model-pins.json, tools/eels-task16/model.py): `two_area_background_estimation`, // `fit_background`, `smart_fit` (intensities, the model curve) and `quantify`, on noise-free and Poisson-noisy planted images, with // and without the low-loss convolution. Tolerances: two-area A and r at the allclose defaults; the fits within the kit's decision -// (docs/design/eels-task-16.md section 6.2): rtol 1e-3 of exSpy's converged intensities (its LM stops 1e-4 short of the minimum). +// (docs/design/eels-chain.md section 6.2): rtol 1e-3 of exSpy's converged intensities (its LM stops 1e-4 short of the minimum). // import Foundation diff --git a/Tests/SpectroKitParityTests/PeakGateTests.swift b/Tests/SpectroKitParityTests/PeakGateTests.swift index 3d6d14d..564a52a 100644 --- a/Tests/SpectroKitParityTests/PeakGateTests.swift +++ b/Tests/SpectroKitParityTests/PeakGateTests.swift @@ -1,6 +1,6 @@ // // PeakGateTests.swift -// Role: EDX stage gate G-P (docs/edx-stage-gates-registration.md section 3): peak identification. +// Role: Peak identification against exSpy pins and planted truth. // // exSpy offers no automatic identification (a table lookup and HyperSpy's generic find_peaks1D_ohaver), so the proposer's // reference is planted truth (section 5.2), not exSpy. P1: the lookup against exSpy's `get_xray_lines_near_energy`. @@ -75,7 +75,7 @@ extension PeakGateTests { /// P3, the independent eye: HyperSpy's `find_peaks1D_ohaver` on the first five realisations of U130 / family L (N_Al 1e5, /// 1e6, 1e7 x rho_Mg 0.002, 0.01, 0.05), two settings (pins.peaks), beside the shipped proposer on the SAME spectra. - /// Positions only, reported, no bound (registration). Asserted: the registered readings of the pins (defaults find only Al Ka; + /// Positions only, reported, no bound. Asserted: the recorded readings of the pins (defaults find only Al Ka; /// amp_thresh 0.2 % of max with medfilt 3 finds Mg within 20 eV in 5 of 5 at rho >= 0.01 and at rho 0.002 for N_Al >= 1e6, and /// 2 of 5 at 1e5 / 0.002, with 7 to 25 peaks) and that each pinned Al peak sits where the rebuilt spectrum peaks. @Test func testP3PeakFinderIsTheRegisteredIndependentEye() throws { @@ -124,10 +124,10 @@ extension PeakGateTests { /// Per-cell summary lines of the matched set (shared by the three criteria). private struct MSummary { let cell: EDXFixtures.Cell; let mean: MgRun; let reals: [MgRun] var truthMg: Double { cell.truth["Mg"]! } - /// 0.11.0, a stated change of the registered bar: the noise-free net >= 3 L_D(M) (was rho N >= 3 L_D(M)), with L_D(M) from the + /// 0.11.0, a stated change of the bar: the noise-free net >= 3 L_D(M) (was rho N >= 3 L_D(M)), with L_D(M) from the /// noise-free sandwich of the same design (the run on the mean). The generator integrates each line over its channel and the model /// samples at the channel centre (eXSpy's convention, D17), so Mg beside a 1e6 to 1e7 count Al reads 0.3 % of N_Al low on the noise-free - /// mean already (Gate D4 measures it against a centre-sampled control); before 0.11.0 the flank-misfit factor widened sigma until that + /// mean already (ProposerMgDiagnosis D4 measures it against a centre-sampled control); before 0.11.0 the flank-misfit factor widened sigma until that /// offset fitted inside 3 sigma. The criteria now test the statistics against the fit's own deterministic answer; the offset is D4's. var detectable: Bool { mean.present && mean.net >= 3 * mean.ld } } @@ -154,7 +154,7 @@ extension PeakGateTests { /// 0.11.0, stated: the identification loop finds a line only from a residual peak at 5 sigma of the top-hat filter (the published bar), /// which on M.s.N4.rho0p05 (461 counts of Mg beside Al at 1e4 counts, about 10 sigma of the fit's own area) is reached in 77 to 80 % of the - /// realisations. The detection bar for that cell is registered as a known issue, not lowered in the code. + /// realisations. The detection bar for that cell is recorded as a known issue, not lowered in the code. private static let peakBarCells: Set = ["M.s.N4.rho0p05"] private static let peakBarIssue: Comment = "the published 5 sigma residual-peak bar finds a 461-count Mg in 77 to 80 % (0.11.0)" @@ -211,10 +211,10 @@ extension PeakGateTests { gateNote(String(format: "EDX P2 M %@ (full pool, %d reals): truth %.0f noise-free net %.0f L_D %.0f; P %.2f; mean net %.0f", s.cell.id, n, s.truthMg, s.mean.net, s.mean.ld, s.reals.pProposed, s.reals.meanNet)) } - // Not a registered criterion: reported. The proposer's own notes state a look-elsewhere rate of chance proposals + // Not a criterion: reported. The proposer's own notes state a look-elsewhere rate of chance proposals // per spectrum; elements other than Al, Si and Mg on matched Al-Mg-Si data are such chance proposals. let spectra = all.reduce(0) { $0 + $1.reals.count + 1 } - gateNote("EDX P2 M full pool: other elements proposed on matched data in \(elsewhere.count) of \(spectra) spectra (not a registered criterion): \(elsewhere.prefix(40))") + gateNote("EDX P2 M full pool: other elements proposed on matched data in \(elsewhere.count) of \(spectra) spectra (not a criterion): \(elsewhere.prefix(40))") } /// P2 mismatched sets (U, S, T, C, HALF, B, W, X): no pass or fail, a table (registration P2): net/truth on the noise-free diff --git a/Tests/SpectroKitParityTests/SpectroGoldenTests.swift b/Tests/SpectroKitParityTests/SpectroGoldenTests.swift index 4465bb0..d056d54 100644 --- a/Tests/SpectroKitParityTests/SpectroGoldenTests.swift +++ b/Tests/SpectroKitParityTests/SpectroGoldenTests.swift @@ -1,6 +1,6 @@ // // SpectroGoldenTests.swift -// SpectroKit slot S0, gate G-golden (docs/archive/v5/spectrokit-slot-plan-2026-10-13.md §9): "no number moves". A baseline of +// SpectroKit slot S0, gate G-golden: "no number moves". A baseline of // the Spectroscopy core's outputs, recorded at 71929a01 BEFORE the Velox reader's per-block HDF5 lock and the cancel/progress // contract, and compared byte for byte afterwards (a quick check that nothing moved, D12; a mismatch is verified against the Python reference within tolerance). Every Double is its exact bit pattern (hex of `bitPattern`) with a readable // %.17g beside it; large arrays are listed by count, a bit-exact sum and an FNV-1a hash of every element's bits. Original baselines remain unchanged; portable comparisons below bound CPU-dependent Double roundoff. diff --git a/Tests/SpectroKitParityTests/SpectrumOpsPinsTests.swift b/Tests/SpectroKitParityTests/SpectrumOpsPinsTests.swift index f23c1b7..5f1cb0e 100644 --- a/Tests/SpectroKitParityTests/SpectrumOpsPinsTests.swift +++ b/Tests/SpectroKitParityTests/SpectrumOpsPinsTests.swift @@ -1,6 +1,6 @@ // // SpectrumOpsPinsTests.swift -// Role: Task 16 step 2 against exSpy 0.3.2 / HyperSpy 2.4.0 (Fixtures/eels16-pins.json): the power-law extrapolation (binned axis; A and r by +// Role: Power-law extrapolation and taper against exSpy 0.3.2 / HyperSpy 2.4.0 (Fixtures/eels16-pins.json): the power-law extrapolation (binned axis; A and r by // the two-area method, the continuation) and the Hanning taper (channels returned, data). Tolerances: A and r rtol 1e-7 (`test_power_law.py` // allclose defaults), the continuation rtol 1e-9, the taper exact to 1e-15 relative (a product with `np.hanning`'s own formula). // The UNBINNED power-law cases of the pins are not offered (the kit's spectra are per-channel counts); exSpy's two-area estimate on them diff --git a/Tests/SpectroKitParityTests/ThicknessPinsTests.swift b/Tests/SpectroKitParityTests/ThicknessPinsTests.swift index b3c3c38..2adc642 100644 --- a/Tests/SpectroKitParityTests/ThicknessPinsTests.swift +++ b/Tests/SpectroKitParityTests/ThicknessPinsTests.swift @@ -1,6 +1,6 @@ // // ThicknessPinsTests.swift -// Role: Gate 1 of the registration (docs/eels-slice-1-registration.md sections 4, 7 and 9): the port against what exSpy +// Role: The thickness port against what exSpy // 7185a4d1 answered (tools/eels-thickness/pins.py -> Fixtures/pins.json), on the planted noiseless and noisy images and // exSpy's own line scan: thresholds, I_0, I_t, t/lambda for every route, absolute thicknesses, lambda and the angular // correction. diff --git a/Tests/SpectroKitTests/SmoothingKernelTests.swift b/Tests/SpectroKitTests/SmoothingKernelTests.swift index e735067..b92458c 100644 --- a/Tests/SpectroKitTests/SmoothingKernelTests.swift +++ b/Tests/SpectroKitTests/SmoothingKernelTests.swift @@ -1,6 +1,6 @@ // // SmoothingKernelTests.swift -// Role: docs/generality-0.5.0-registration.md, family 4 (M1): `SmoothingKernel`, any box width or Gaussian sigma and truncation. +// Role: `SmoothingKernel`, any box width or Gaussian sigma and truncation. // The menu entries are the same kernels as before (asserted bit for bit today, D12's quick check); a non-default kernel spreads an impulse as predicted. // diff --git a/Tests/SpectroTestSupport/EDXFixtures.swift b/Tests/SpectroTestSupport/EDXFixtures.swift index 50e9354..6f55fa0 100644 --- a/Tests/SpectroTestSupport/EDXFixtures.swift +++ b/Tests/SpectroTestSupport/EDXFixtures.swift @@ -1,6 +1,6 @@ // // EDXFixtures.swift -// Role: Loaders for the EDX stage gates (docs/edx-stage-gates-registration.md): pins.json, the planted Velox streams and +// Role: Loaders for the EDX pins and planted fixtures: pins.json, the planted Velox streams and // their truth, the planted Al-Mg-Si spectra (means plus int16 residuals), the calibration set, and RosettaSciIO's // public Velox files. Formats: tools/edx-stages/README.md. Nothing here is the code under test. // @@ -149,7 +149,7 @@ package extension Dictionary where Key == String, Value == Any { func s(_ k: String) -> String { self[k] as! String } } -// MARK: parallel helper (at most 8 workers: suite CLAUDE.md, heavy CPU work) +// MARK: parallel helper (at most 8 workers, so heavy CPU work leaves the machine usable) package final class ResultBox: @unchecked Sendable { private let lock = NSLock() @@ -170,7 +170,7 @@ package func parallelMap(_ n: Int, _ body: @Sendable (Int) -> T) -> return box.values } -/// Lines of `EDX_GATE_LOG` (if set) and stdout: the numbers the gates and the Gate D experiments report. +/// Lines of `EDX_GATE_LOG` (if set) and stdout: the numbers the EDX checks and the Mg diagnosis report. package func gateNote(_ s: String) { print(s) guard let path = ProcessInfo.processInfo.environment["EDX_GATE_LOG"] else { return } diff --git a/Tests/SpectroTestSupport/EELSFixtures.swift b/Tests/SpectroTestSupport/EELSFixtures.swift index 62db392..51f873c 100644 --- a/Tests/SpectroTestSupport/EELSFixtures.swift +++ b/Tests/SpectroTestSupport/EELSFixtures.swift @@ -225,7 +225,7 @@ package final class Worst: @unchecked Sendable { } } -/// SciPy derivatives with the portable auto-SG tie policy; raw exSpy pins stay in pins.json/refuter/. +/// SciPy derivatives with the portable auto-SG tie policy; raw exSpy pins stay in pins.json and refuter/. package struct PortableThresholds: Decodable { package struct Route: Decodable { package let name: String diff --git a/Tests/SpectroTestSupport/EdgeFixtures.swift b/Tests/SpectroTestSupport/EdgeFixtures.swift index c6e8696..2ff012e 100644 --- a/Tests/SpectroTestSupport/EdgeFixtures.swift +++ b/Tests/SpectroTestSupport/EdgeFixtures.swift @@ -1,6 +1,6 @@ // // EdgeFixtures.swift -// Role: edge-pins.json and edge-planted.json (EELS slice 2) as plain dictionaries, and the planted core-loss and low-loss +// Role: edge-pins.json and edge-planted.json as plain dictionaries, and the planted core-loss and low-loss // images built from them. Shared by the edge edge-case tests and the edge pins. // @@ -30,7 +30,7 @@ package enum EdgeFixtures { energyOffsetEV: ax["offset"] as! Double, energyScaleEV: ax["scale"] as! Double) } - /// A planted low-loss image (slice 1's axis, -10 eV + 0.05 eV x 3400). + /// A planted low-loss image (the thickness fixtures' axis, -10 eV + 0.05 eV x 3400). package static func low(_ file: String) throws -> LowLossSpectrumImage { let ax = planted["lowLossAxis"] as! [String: Any] return try LowLossSpectrumImage(nx: 5, ny: 3, channels: ax["size"] as! Int, counts: EELSFixtures.doubles(file), diff --git a/Tests/SpectroTestSupport/PlantedRuns.swift b/Tests/SpectroTestSupport/PlantedRuns.swift index d611d33..dc9f7a0 100644 --- a/Tests/SpectroTestSupport/PlantedRuns.swift +++ b/Tests/SpectroTestSupport/PlantedRuns.swift @@ -1,7 +1,7 @@ // // PlantedRuns.swift // Role: Runs of the shipped `ElementProposer` on the planted Al-Mg-Si spectra (registration 5.2), shared by gate P2 and the -// Gate D experiments. Cached per process: every (cell, realisation, design) runs once. +// Mg diagnosis experiments. Cached per process: every (cell, realisation, design) runs once. // import Foundation @@ -27,7 +27,7 @@ package enum PlantedRuns { package static let axis = EDXFixtures.axis /// The design of the single-candidate test (registration P2): `pool: ["Mg"]`, `settings.elements = [Al, Si]`. - /// Sets the Mn Ka resolution of the model (130 eV unless a Gate D experiment scans it) and the fit range. + /// Sets the Mn Ka resolution of the model (130 eV unless a diagnosis experiment scans it) and the fit range. package static func run(_ counts: [Double], pool: [String]? = ["Mg"], current: [String] = ["Al", "Si"], resolution: Double = 130, axis: EnergyAxis = PlantedRuns.axis, reference: ReferenceShapes = .none) -> MgRun { var settings = FitSettings.standard(elements: current, axis: axis, resolutionMnKaEV: resolution, beamEnergy: 200) @@ -64,7 +64,7 @@ package extension Array where Element == MgRun { } } -/// Runs cached for the process (the gates and the Gate D experiments read the same spectra; every run happens once). +/// Runs cached for the process (the checks and the diagnosis experiments read the same spectra; every run happens once). package final class RunStore: @unchecked Sendable { package static let shared = RunStore() private let lock = NSLock() diff --git a/Tests/SpectroTestSupport/SeededPoisson.swift b/Tests/SpectroTestSupport/SeededPoisson.swift index c75e9fc..f5910ed 100644 --- a/Tests/SpectroTestSupport/SeededPoisson.swift +++ b/Tests/SpectroTestSupport/SeededPoisson.swift @@ -1,6 +1,6 @@ // // SeededPoisson.swift -// Role: A seeded Poisson sampler for the noise tests (registration section 7 gate 3: 200 realisations, seeds 1 to 200, +// Role: A seeded Poisson sampler for the noise tests (200 realisations, seeds 1 to 200, // generated in the test, not stored). Written here, not NumPy's stream: the realisations differ from the ones // tools/eels-thickness/pins.py drew, and the claim tested (standard deviation of t/lambda against sigma_counting) does // not depend on which stream. diff --git a/docs/benchmarks.md b/docs/benchmarks.md index 060f1c3..b6dbcd1 100644 --- a/docs/benchmarks.md +++ b/docs/benchmarks.md @@ -6,7 +6,7 @@ under `tools/memcap.sh` (8 GB; measured Swift peak 1.3 GB at the typical size). One warm-up, then five runs, median and (min-max). Machine: Apple M5 Pro, 64 GB, macOS 27.0.1 (26A434), Swift 6.4 (swiftlang-6.4.0.34.1), Python 3.12.14, NumPy 2.5.4, HyperSpy 2.4.0, exSpy 0.3.2 -(no Numba). **Other sessions were running on the machine**, so the Python spreads are wide; read ratios within about 30 %. +(no Numba). **Other jobs were running on the machine**, so the Python spreads are wide; read ratios within about 30 %. ## Results @@ -28,9 +28,9 @@ Machine: Apple M5 Pro, 64 GB, macOS 27.0.1 (26A434), Swift 6.4 (swiftlang-6.4.0. | t/lambda log ratio: estimated | typical | 4096 | 16.34 (16.29-17.02) | 87.30 (82.73-123.58) | 5.34 | Sizes: quantification small 8 x 8, typical 256 x 256 (3 elements), large 1024 x 1024; thickness small 8 x 8, typical 64 x 64 (2048 channels), large 256 x 256. -**Large: nightly** (not run in the session). +**Large: nightly** (not run here). -## Backend decision per function (D16) +## Backend decision per function (CPU float64 reference, Metal float32 where size pays) | function | decision | why | |---|---|---| diff --git a/docs/design/auto-id-tem.md b/docs/design/auto-id-tem.md index 663dded..673d443 100644 --- a/docs/design/auto-id-tem.md +++ b/docs/design/auto-id-tem.md @@ -1,10 +1,8 @@ -# Auto ID for TEM EDS: the identification loop, rebuilt for thin foils (design, 2026-10-23) +# Auto ID for TEM EDS: the identification loop for thin foils (design note, 0.11.0) -Paul's decision (2026-10-23 08:27 UTC): rebuild the automated element identification the way the one complete open -implementation does it (AutoEMX, Giunto et al. 2025, Lawrence Berkeley National Laboratory; the loop is the NIST -Newbury–Ritchie "automatic qualitative analysis" loop), for TEM, in Swift, Metal where it pays. Written by us from the -published method and its documented default thresholds; no AutoEMX code is copied (its licence is non-commercial). The -comparison that led here: https://claude.ai/artifact/AAb17m1fu7ps2dWEa6jFgi. +`ElementProposer` identifies elements the way the one complete open implementation does it (AutoEMX, Giunto et al. 2025, Lawrence Berkeley National Laboratory; the loop is the NIST +Newbury–Ritchie "automatic qualitative analysis" loop), adapted to TEM thin foils. Written from the +published method and its documented default thresholds; no AutoEMX code is copied (its licence is non-commercial). ## 1. What the loop does (the method, step by step) @@ -67,7 +65,7 @@ flank-misfit factor, which multiplied a neighbour's detection limit by the misfi - MLX has no place here: there is no learned part. A learned peak-shape or background model would need labelled spectra we do not have; it stays a later idea, not part of this. -## 4. Where it lands in the code (decided 2026-10-23) +## 4. Where it lives in the code - **One loop, the old public call.** `ElementProposer.propose(counts:axis:settings:holeCounts:cancelled:progress:)` keeps its signature and its `ProposalResult`, so both apps call it unchanged; its body becomes the loop above. The all-candidate joint @@ -83,7 +81,7 @@ flank-misfit factor, which multiplied a neighbour's detection limit by the misfi √(Σ k_j² max(y_j, 1)). The width follows σ(E), so the filter is recomputed per channel from the law; at 4000 channels this is microseconds, CPU. - **Line weights.** The identification fit uses the NIST EPQ weights (`LineWeights.csv`, already bundled for the k-factors) - through a `lineWeights` choice on `EDSLineModel.build`; the quantification fit keeps eXSpy's table (D17: the parity numbers + through a `lineWeights` choice on `EDSLineModel.build`; the quantification fit keeps eXSpy's table (the parity numbers do not move). Stated in the proposer's notes. - **Thresholds.** Named constants in `IdentificationThresholds` (public, on `ProposerPolicy`): peak 5 σ, ×1.5 below 1 keV, candidate window 2.5 σ + 25 eV, weight ≥ 0.1, confirmation 3 σ, Δχ²₁ ≥ 100, local share ≥ 0.2, contradiction 9 and 5 %, @@ -117,28 +115,26 @@ Each a stated departure from the method as documented, with the case that forced pile-up column whose parent was pruned goes with it. - **Not yet:** the overlap check (step 6). Its finds are possible only, so it changes no proposal; an open item. -Result on the three whole-map sums, refined axis: 1339 and 1840 propose O, Mg, Al, Si, Cu plus Zr, Mn (and Fe on 1840); 1456 +Result on three measured whole-map sums (two Al-Mg-Si alloys, files 1339 and 1840, and a FIB lamella, 1456), refined axis: 1339 and 1840 propose O, Mg, Al, Si, Cu plus Zr, Mn (and Fe on 1840); 1456 proposes O, Si, Ni, Ge, Pt plus Cu, Zr and Ga. Zr is on all three files (the instrument's); Mn reads 22 and 70 times its limit on -the Al alloys (a 6xxx minor element by the numbers, the owner's word pending). Planted: Mg at 2 % beside a 5 %-misfit Al reads +the Al alloys (a 6xxx minor element by the numbers; the alloy's specification is not yet known). Planted: Mg at 2 % beside a 5 %-misfit Al reads 121 509 of 120 000 and is proposed; without Mg nothing new is proposed. ## 6. The oracle and the tests (red-first) -- AutoEMX 1.8.0 run on the whole-map sums of 1339, 1840 and 1456 in a Python 3.12 environment under the session's - scratch folder (it refuses Python 3.14). Run A, its own fitter at 200 keV: the fit returns NaN on all three files +- AutoEMX 1.8.0 run on the whole-map sums of 1339, 1840 and 1456 in a Python 3.12 environment (it refuses Python + 3.14). Run A, its own fitter at 200 keV: the fit returns NaN on all three files with and without the truth elements (its bulk SEM model and the Phenom XL detector calibration do not reach a 200 kV thin foil); with SEM-like parameters (30 keV, range to 10 keV) it ran 11 minutes on 1339 without finishing. Run B, its loop with an exSpy EDS model as the plug-in fitter, finds every real peak (Mg at 100 to 120 σ, Si at 31 to 40 σ) and then prunes them, because the exSpy fit (polynomial background, unconstrained) fails its own background check (0.52 in 1 to 2.5 keV on 1339). With exSpy underneath the pruning means nothing; with our fitter it is the test. - The bar is therefore the known answers, not Run B's lists: 1339 and 1840 propose O, Mg, Al, Si, Cu (Fe and Zr allowed - where found), 1456 proposes O, Si, Ni, Ge, Pt; nothing else is proposed on any of the three (opt-in, the owner's files, - `SPECTRO_REAL_DATA=1`). -- Planted (in the gate): Mg at 2 % beside a misfit Al is proposed (today's `FlankMisfitCoreTests` known issue turns into + where found), 1456 proposes O, Si, Ni, Ge, Pt; nothing else is proposed on any of the three (`RealFileAutoIDTests`, opt-in with + `SPECTRO_REAL_DATA=1` and the files under `$MACEM_DATA`). +- Planted (in the gate): Mg at 2 % beside a misfit Al is proposed (the former `FlankMisfitCoreTests` known issue became a plain expectation); the same spectrum without Mg proposes no Mg; the thresholds as named constants with one test each; the top hat filters a straight line to zero; χ²₁ of a perfect model is Σ 0. -- Own numerics commit under "moved numbers, review before tag"; Sonnet review before it; SpectroKit 0.11.0's Auto ID - before the freeze and tag. ## Sources diff --git a/docs/design/eels-task-16.md b/docs/design/eels-chain.md similarity index 69% rename from docs/design/eels-task-16.md rename to docs/design/eels-chain.md index f0c1737..901bcec 100644 --- a/docs/design/eels-task-16.md +++ b/docs/design/eels-chain.md @@ -1,31 +1,9 @@ -# EELS task 16: zero-loss alignment, background, deconvolution, edge fit (design note) - -Suite ROADMAP task 16, SpectroKit 0.9.0. Written 2026-10-21 for a Mac session to implement without further design work. -Reference: exSpy 0.3.2 (`exspy/signals/eels.py`, `exspy/models/eelsmodel.py`, `exspy/components/eels_cl_edge.py`) and -HyperSpy 2.4.0 (`hyperspy/_signals/signal1d.py`, `hyperspy/misc/math_tools.py`, `hyperspy/_components/power_law.py`) -in `../References/python/hyperspy` (suite `tools/setup-python-refs.sh hyperspy`; NumPy 2.5.4, SciPy 1.18.1). Line -numbers below are from those files. Every result stays `validation: "none"`. No code beyond signatures; one screen per -section. - -## 0. Scope and order - -What the apps need first (macTEM task 8, then mac4DSTEM): the EELS room opens a spectrum image, shows the t/lambda map -and the zero-loss diagnostic. t/lambda exists; the room's first new need is **zero-loss alignment** (real data is never -at 0 eV) and the **zero-loss mask**. Everything else is composition from EELS, which the room offers only once the fit -lands. Implementation order, each step its own commit with a test that fails before and passes after: - -1. Zero-loss alignment and mask (section 2). Unblocks macTEM task 8. -2. Power-law extrapolation and Hanning taper (section 3). Small; the deconvolutions stand on them. -3. L-subshell GOS tables and the edge database rows the fit needs (section 6.1). -4. Fourier-log and Fourier-ratio deconvolution (section 4). -5. Edge model fit, background fit, quantify (section 6.2). -6. Richardson-Lucy (section 5), only if the session has time: the ROADMAP lists it out of scope, the coordinator asked - for its API, so it is designed and ordered last. - -Out of scope (ROADMAP): fine-structure fitting, Kramers-Kronig, `rebin`, EDS calibration, hydrogenic and Dirac GOS. -Dependencies: one new package dependency, ImagoKit `"0.6.0"..<"1.0.0"` for `ImagoCore.FFT2DDouble` used as a 1-D plan -(`nx: size, ny: 1`; vDSP for 2^n × {1,3,5,15}, Bluestein for the rest; DiffractoKit precedent; sections 4 and 11 say why). -`ImagoFFT.FFT1DDouble` is power-of-two only and does not fit `next_fast_len` sizes. Readers unchanged. +# The EELS chain: zero-loss alignment, background, deconvolution, edge fit (design note) + +How SpectroEELS ports exSpy 0.3.2 (`exspy/signals/eels.py`, `exspy/models/eelsmodel.py`, `exspy/components/eels_cl_edge.py`) and +HyperSpy 2.4.0 (`hyperspy/_signals/signal1d.py`, `hyperspy/misc/math_tools.py`, `hyperspy/_components/power_law.py`), as installed by +`tools/reference/setup.sh` (NumPy 2.5.4, SciPy 1.18.1). Line numbers are from those files. Every result stays `validation: "none"`. +Written for 0.9.0 (2026-10-21); the code is the reference where they differ, and `docs/parity.md` holds each function's state and tolerance. ## 1. Shared types (SpectroEELS) @@ -165,12 +143,12 @@ The FFT runs on `ImagoCore.FFT2DDouble(nx: size, ny: 1)` (vDSP for 2^n × {1,3,5 `next_fast_len` size is exact in length (if the `ny: 1` plan proves awkward, an any-length `FFT1DDouble` is a one-task ImagoKit change, not a SpectroKit copy); `irfft` of a half spectrum follows NumPy's Hermitian rule (imaginary parts of bins 0 and N/2 dropped). -Tolerances: upstream has none (`TestFourierRatioDeconvolution::test_running` only runs). D17 fallback: `assert_allclose` +Tolerances: upstream has none (`TestFourierRatioDeconvolution::test_running` only runs). Fallback: `assert_allclose` defaults are rtol 1e-7 with atol 0, which a deconvolved channel near zero can never meet across two FFT libraries. Kit decision, recorded in `parity.md`: rtol 1e-7 **and** atol 1e-7 × max|pinned result| per pixel; `fftSize`, `taperedChannels` and the FWHM root positions exact to 1e-9 eV (`UnivariateSpline` roots are a cubic solve; port `scipy`'s `sproot`-free route: cubic not-a-knot spline through the points, roots per interval by Cardano, as `InterpolatingSpline` already builds -the spline). The planted truth test (section 8) checks that deconvolving a planted plural-scattering spectrum returns +the spline). The planted truth test checks that deconvolving a planted plural-scattering spectrum returns the planted single-scattering distribution within its counting noise. ## 5. Richardson-Lucy deconvolution (ordered last) @@ -195,10 +173,9 @@ rule as section 4 (rtol 1e-7, atol 1e-7 × max|result|); `iterations < 1` refuse ### 6.1 Tables and edges -`CoreLossEdge` grows `subshell` beyond K1: `tools/eels-edge/extract_gos.py --subshells L1,L2,L3` writes -`Resources/GOS/dft--.json` the same way (float32 kept, sorted keys, md5 of the GOSH file checked). Bundle for -0.9.0 (the study's alloys and the usual contaminants; a pure engineering pick): K1 for C, N, O, Mg, Al, Si; L1, L2, L3 for -Mg, Al, Si, Fe, Cu. Onsets from exSpy's `eels_binding_energies.json` (exact, `tests/misc/test_gos.py::test_binding_energy_database`). +`CoreLossEdge` carries any subshell: `tools/eels-edge/extract_gos.py` writes every K, L and M table exSpy builds with the +dft GOS into one float32 pack (`Resources/GOS/dft-gos-tables.bin` with `dft-gos-index.json`; the GOSH file's md5 checked): +383 edges of 95 elements since 0.12.0 (0.9.0 bundled 20). Onsets from exSpy's `eels_binding_energies.json` (exact, `tests/misc/test_gos.py::test_binding_energy_database`). `EdgeCrossSection` already integrates any subshell's table (`_integrate_GOS`, `function(E)` at rtol 5e-7, `test_eels_cl_edge.py`); the only new rule is exSpy's twin: the subshells of one element share one intensity, the master is the lexicographically last name (`L3` over `L2` over `L1`, `eelsmodel.py:283-327`), the others' onsets ride on the master's with the fixed @@ -227,7 +204,7 @@ public struct EELSModelOptions: Sendable, Equatable { } public struct EdgeIntensityPixel: Sendable, Equatable { public let intensity: Double? // exSpy's intensity.value (counts × barn⁻¹ scale); nil with `unavailable` - public let arealDensityPerNM2: Double? // n from intensity by the ratioMethod unit rule (1 barn = 1e-10 nm²); see the pin, section 8 + public let arealDensityPerNM2: Double? // n from intensity by the ratioMethod unit rule (1 barn = 1e-10 nm²); see the pin public let background: PowerLawParameters?; public let unavailable: EELSUnavailable? } public struct EELSModelResult: Sendable { @@ -250,7 +227,7 @@ minimiser (Brent, `r ∈ [0, 10]`, start from the two-area estimate) with intens HyperSpy's LM stops short of the minimum by 1e-4 relative) and is the `DEVIATION` to write inline. Low-loss convolution: exSpy convolves each component with the raw low-loss counts over the model's convolution axis; port that convolution (linear, `vDSP_convD`, axis as `set_convolution_axis`) and record the factor between exSpy's `intensity` and `n` on the -planted set before fixing `arealDensityPerNM2` (the pin script writes both numbers, section 8). +planted set before fixing `arealDensityPerNM2` (the pin script writes both numbers). Tolerances: `test_eelsmodel.py::test_two_area_powerlaw_estimation_{BC,C,no_edge}` A and r at defaults (pinned numbers 2.1451237089380295 / 3.0118980767392736 and the two others); `test_fit_background_{B_C,C}` offset at defaults; @@ -274,78 +251,29 @@ LM shortfall is 1e-4), and within the counting-statistics sigma of the planted t | `EELSModelFit.fit` | `eelsmodel.py:444-480`, `:594-671`; `eels_cl_edge.py:441-497` | component rtol 5e-7; fit: none upstream, kit rtol 1e-3 + planted sigma | | `CoreLossEdge` L tables | `gosh_gos.py` `read_gos_data`; `eels_binding_energies.json` | `test_gos.py::test_binding_energy_database` exact; tables bit for bit | -Pin environment: exSpy 0.3.2 and HyperSpy 2.4.0 from `../References/python/hyperspy/bin/python -I -B` (Python 3.14, +Pin environment: exSpy 0.3.2 and HyperSpy 2.4.0 from `References/python/bin/python -I -B` (`tools/reference/setup.sh`; Python 3.14, NumPy 2.5.4, SciPy 1.18.1), the dft GOSH 1.5.0 file by md5 `7fee8891c147a4f769668403b54c529b`, as the existing scripts. -## 8. Pin scripts and fixtures (existing layout: `tools//pins.py` writes `Tests/SpectroEELSTests/Fixtures/`) - -| script | writes | holds | -|---|---|---| -| `tools/eels-align/make_planted.py` (NumPy only) | `align-planted.f64`, `align-planted.json` | 4×4×400 low loss, ZLP Gaussian FWHM 1.2 eV at planted shifts (−0.9…+1.3 eV, incl. the exact `TestAlignZLP` grid), background 2, a 20-count spike at channel 300, mask pixels [3, 5]; truth shifts | -| `tools/eels-align/pins.py` | `align-pins.json` | per option set (calibrate × subpixel × signal_range × crop × mask): zlpc before, shifts, new offset, kept channel range, aligned data (f64 sidecar), `get_zero_loss_peak_mask` for widths 2, 5, 10; `estimate_shift1D` raw | -| `tools/eels-deconv/make_planted.py` | `deconv-ssd.f64`, `deconv-zlp.f64`, `deconv-plural.f64`, `deconv-core.f64`, `.json` | planted single-scattering distribution, ZLP, plural spectrum = ZLP ⊛ exp(SSD) (Poisson series to order 6), core loss = SSD ⊛ edge; noiseless and `default_rng(20261021)` Poisson copies | -| `tools/eels-deconv/pins.py` | `deconv-pins.json` (+ `.f64` results) | `power_law_extrapolation` (20/100, 100/100, fix_neg_r), `hanning_taper` channels, `optimal_fft_size` for 1…5000 both rules, Fourier-log (add_zlp × crop), Fourier-ratio (fwhm given / estimated × extrapolate flags × threshold fixed / estimated) with `fftSize`, FWHM roots, I0; Richardson-Lucy 1, 5, 15 iterations | -| `tools/eels-edge/extract_gos.py --subshells` | `Resources/GOS/dft--.json` | the L tables, section 6.1 | -| `tools/eels-model/make_planted.py` | `model-core-*.f64`, `model-low-*.f64`, `model-planted.json`, `model-truth.json` | 3×5 pixels: power law × {3 backgrounds} with C K + O K (two K) and Fe L2,3 + O K (L twins) at planted n, low loss from `edge-low-ps07`; noiseless and noisy | -| `tools/eels-model/pins.py` | `model-pins.json` | `two_area_background_estimation` (A, r) incl. the three `test_eelsmodel` cases, `fit_background` range and parameters, `smart_fit` per pixel: every parameter, the fit range per edge, `quantify` text, and the intensity → n factor (exSpy `intensity`, I_low, σ per edge) | - -Every script records versions, its own sha256 and the GOSH md5 (as `tools/eels-edge/pins.py` does), writes byte-identical -output on a second run, and gets a `README.md` with the run lines. The exSpy-internal truth (`test_eelsmodel` numbers) -is copied into the pin, not retyped in Swift. - -## 9. `docs/parity.md` rows to move or add - -Zero-loss peak section: `estimate_zero_loss_peak_centre` partial → present TOL (mask and `signal_range` now held); -`align_zero_loss_peak`, `get_zero_loss_peak_mask` missing → present TOL / exact; new rows for HyperSpy `shift1D`, -`estimate_shift1D`, `align1D` under "Shared numerics" (TOL; note the `max_shift` no-op). Deconvolution section: -`fourier_log_deconvolution`, `fourier_ratio_deconvolution`, `richardson_lucy_deconvolution`, `power_law_extrapolation` -missing → present TOL, tolerance column "none upstream; kit rtol 1e-7 + atol 1e-7·max (decision, design note eels-task-16 -section 4)"; new rows `hanning_taper` (exact), `optimal_fft_size` (exact), `estimate_peak_width` (TOL). Model section: -`create_model` / `EELSModel` row splits into `two_area_background_estimation` (TOL defaults), `fit_background` (TOL -defaults), `smart_fit` / `fit` (DEV: variable projection + NNLS, rtol 1e-3, planted sigma), `quantify` (exact text -reproduced by a formatter test); `EELSCLEdge` L edges row → present for the bundled L elements, M and other K stay missing; -edges database row → partial with the new onsets. Summary table counts updated; "largest gaps" item 1 rewritten to what -remains (fine structure, Kramers-Kronig, hydrogenic/Dirac GOS, free onset energy). - -## 10. Tests: fast target against parity target - -`SpectroEELSTests` (fast, per session, must stay under a few seconds): refusals and shapes for every new error case; -hand-derived rules (`optimalFFTSize` against a list of 5-smooth numbers, Hanning window values, `next_fast_len` real vs -complex, twin master selection, `preEdgeSafeWindow`); planted truth tests (alignment recovers planted shifts within -0.5 channel coarse / atol 1e-3 eV subpixel; Fourier-log of the planted plural spectrum returns the SSD within noise; -model fit recovers planted n within the counting sigma); the `quantify` formatter against the exact text. - -`SpectroKitParityTests` (before a tag and nightly, CI on push): one `*PinsTests` class per fixture (`AlignPinsTests`, -`DeconvPinsTests`, `ModelPinsTests`) comparing against exSpy's numbers at the section 7 tolerances; the `.f64` results -loaded through `SpectroTestSupport` (`FixtureDirs.fixtures(of: "SpectroEELSTests")`, a `DeconvFixtures.swift` beside -`EELSFixtures.swift`). Benchmarks (`tools/benchmarks/bench.py`, nightly): alignment and Fourier-ratio at 64×64×2048 against -exSpy, with the Python side timed in the same venv. - -## 11. Performance: Accelerate, no Metal +## 8. Performance: Accelerate, no Metal Every path is per pixel on a 1-D axis of 1k to 4k channels: linear interpolation, one correlation, two FFTs and a -division, an NNLS of three to six columns. At the study's size (64×64×2048, 8 M values) that is tens of milliseconds +division, an NNLS of three to six columns. At a typical map size (64×64×2048, 8 M values) that is tens of milliseconds per step on the CPU in float64 with the pixel loop parallel over chunks (the pattern of `EDXQuantification.quantifyMaps`, -at most 8 threads). A Metal float32 path would spend its dispatch and copy on work smaller than the copy; D16's "where -size pays" does not hold for anything in this task, so no kernel, no `.metal` option, and the result records -`backend: "cpu"` only. Richardson-Lucy at 15 iterations is the heaviest (about 1 s at the study size); if a spectrum +at most 8 threads). A Metal float32 path would spend its dispatch and copy on work smaller than the copy; the "where +size pays" rule does not hold for anything in this task, so no kernel, no `.metal` option, and the result records +`backend: "cpu"` only. Richardson-Lucy at 15 iterations is the heaviest (about 1 s at that size); if a spectrum image ten times larger arrives, the FFT-based convolution on the same plan is the first step, Metal the second. The FFT comes from ImagoKit (`FFT2DDouble`, vDSP for 2^n × {1,3,5,15}, Bluestein for the other `next_fast_len` sizes); writing a second Bluestein in SpectroKit was the alternative and is not worth a copy. -## 12. Decisions a reviewer should see +## 9. Decisions a reviewer should see 1. ImagoKit becomes a dependency of SpectroKit (FFT only). Reverse by vendoring the Bluestein plan; nothing else changes. -2. Deconvolution tolerance: rtol 1e-7 plus atol 1e-7 × max|result| where exSpy tests nothing numerically (D17 fallback +2. Deconvolution tolerance: rtol 1e-7 plus atol 1e-7 × max|result| where exSpy tests nothing numerically (the `assert_allclose` default with an atol, because allclose's atol 0 is unattainable across FFT libraries for near-zero channels). 3. The model fit is an exact minimiser (variable projection + NNLS), not HyperSpy's LM; pinned at rtol 1e-3, truth at the planted sigma. Same deviation as the EDX fit, same inline `DEVIATION` note. 4. Non-finite per-pixel outcomes are nil with a reason (`EELSUnavailable`), never NaN, as every EELS result today. 5. `max_shift` and `add_noise` are not offered (a no-op and a random draw in the reference). -6. Richardson-Lucy is designed but ordered last; the ROADMAP lists it out of scope, so skipping it is not a gap. -7. The bundled L edges (Mg, Al, Si, Fe, Cu) are an engineering pick for the study's alloys; a different list is a - one-line change in `extract_gos.py` plus a resources commit. - -Closeout for the implementing session (CLAUDE.md): fast target green per commit, full suite before the tag, `CHANGELOG.md` -0.9.0 entry with a Migration paragraph (new dependency, `CoreLossEdge.subshell` values beyond `K1`), `docs/parity.md` -rows per section 9, `docs/status.md` one line, open item 4 rewritten, hand-off paragraph in the last commit. +6. Richardson-Lucy came last (0.10.0). +7. Since 0.12.0 every edge exSpy builds with the dft GOS is bundled, so there is no list to pick. diff --git a/docs/open-items.md b/docs/open-items.md index 4c72ff9..07687f0 100644 --- a/docs/open-items.md +++ b/docs/open-items.md @@ -1,6 +1,6 @@ # Open items -Slowest against Python first (diagnose before changing a number, `../CLAUDE.md`). +Slowest against Python first. Diagnose before changing a number: state the cause and what would refute it, run the cheap experiment first. 1. **t/lambda with a fixed threshold is 2x slower than HyperSpy at 64 x 64 x 2048** (9.1 ms against 4.5 ms, `docs/benchmarks.md`). Hypothesis to test, not yet tested: the per-pixel pairwise sums (bit-faithful to NumPy, required by the pins) and the zero-loss-centre argmax run single-threaded, @@ -8,9 +8,9 @@ Slowest against Python first (diagnose before changing a number, `../CLAUDE.md`) (the numbers cannot change: pixels are independent); refuted if the typical size does not drop below 4.5 ms. The estimated-threshold route is already 5x faster. 2. **Benchmarks missing**: EDX fit and auto ID on a Velox spectrum image, EELS power-law background fit, edge quantification, and every large size (nightly). 3. **EDX fit accuracy on real Al alloys** (Mg K alpha about 300 counts low, Si 8.7 % low, sigma 5 to 12 % small; Mg missed in 37 of 38 Al-Mg-Si files): - is diagnosed on the Al matrix of a real Al-Mg-Si map (the in-situ standard: pixels whose Mg and Si are below L_D), not on a pure-Al file. The old registered finding (offset and gain trade off on lines that span little of the axis, + is diagnosed on the Al matrix of a real Al-Mg-Si map (the in-situ standard: pixels whose Mg and Si are below L_D), not on a pure-Al file. The known finding (offset and gain trade off on lines that span little of the axis, `AxisGateTests.testE3RefinementRecoversPlantedOffsetAndGain`) stays a known issue. -4. **EELS, what 0.9.0 left** (task 16): fine structure and a free onset energy in the model fit; Kramers-Kronig; M edges and other elements' K and L tables (`tools/eels-edge/extract_gos.py --task16` shows how); counting sigma on the fitted intensities and on at% / wt%; the model fit on real data (no validation yet, badge `validation: "none"`); `power_law_extrapolation` on an unbinned axis (exSpy's Simpson branch is a factor two off the exact power law, not offered). Richardson-Lucy and the MLLS fit landed in 0.10.0; still open there: a Metal or FFT path for Richardson-Lucy at 64 x 64 x 2048 and more (the direct convolutions take 1.1 s for 32 x 32 x 2048 at 15 iterations on the Mac, about 4.4 s for 64 x 64), weighted MLLS (Poisson weights; the unweighted standard errors overstate the background's by 7x on the pinned spectrum), MLLS with a free onset or fine structure (shares the fine-structure item), and the MLLS and Richardson-Lucy on real data (badge `validation: "none"`). Line profile: weighting by partial pixel coverage (area) instead of hard membership is not offered; the hard cells overlap-free partition but a corner's inner side has a gap of at most a pixel. +4. **EELS, what 0.9.0 left**: fine structure and a free onset energy in the model fit; Kramers-Kronig; counting sigma on the fitted intensities of the joint fit (the per-edge windows have it since 0.12.0) and on at% / wt%; the model fit on real data (no validation yet, badge `validation: "none"`); `power_law_extrapolation` on an unbinned axis (exSpy's Simpson branch is a factor two off the exact power law, not offered). Richardson-Lucy and the MLLS fit landed in 0.10.0; still open there: a Metal or FFT path for Richardson-Lucy at 64 x 64 x 2048 and more (the direct convolutions take 1.1 s for 32 x 32 x 2048 at 15 iterations on the Mac, about 4.4 s for 64 x 64), weighted MLLS (Poisson weights; the unweighted standard errors overstate the background's by 7x on the pinned spectrum), MLLS with a free onset or fine structure (shares the fine-structure item), and the MLLS and Richardson-Lucy on real data (badge `validation: "none"`). Line profile: weighting by partial pixel coverage (area) instead of hard membership is not offered; the hard cells overlap-free partition but a corner's inner side has a gap of at most a pixel. 5. **EDS calibration and operations**: `rebin`, `get_calibration_from`, `calibrate_xray_lines`, public window parameters (`line_width`, `windows_width`, `integration_window_factor`), free/fix of line parameters, `vacuum_mask` threshold and closing (a mask is an input now). 6. **App-side `@testable` reach** (0.9.0 opened the window, segment, candidate and proposal-result initialisers and the `PooledQuantificationInput` fields; `UnlistedLineCheck.Candidate` stays `package`): the computed or typed k (`PooledQuantification.kFactors`), the line energy (`Row.lineEnergyKeV`) and the take-off angle (`EDXQuantification.takeOffDegrees`) are public since 0.8.2. `PooledQuantificationInput.axis`, `.method`, `.metadata`, `.regionName` and `.pixelCount` are still `package` (their init is public; reading them back is the open part). @@ -18,12 +18,11 @@ Slowest against Python first (diagnose before changing a number, `../CLAUDE.md`) 8. **EDS 0.10.0 asks, what is open**: the sum/escape model's pile-up coefficient is one number for the whole spectrum and the parents are the alpha lines of the strongest groups (family lines such as Kb are not parents); a real detector's escape fraction depends on energy, the common fraction is a first model. It is not tested on real data and has no standard: it needs one measured spectrum with a known sum peak (a thick pure Cu or Fe sample at a high rate) before the badge can change. `PooledQuantification.plotModel` includes the peaks, but the proposer and the unlisted-line check still use their own pile-up columns and do not know the model's. 9. **Sigma on wt% and at%**: the thickness sigma, the dose and the detector are not in the per-spectrum `quantifyWithSigma` (the pooled quantification has the thickness term); a per-pixel sigma map is not provided. 10. **k-factor import**: the table is taken in exSpy's convention (C_A/C_B = k_A/k_B I_A/I_B); a vendor table in the inverse convention is the caller's to invert. A real Velox export has not been read (none in the data folder); the layouts are the ones the task named. - -9. **0.11.0 leftovers, Auto ID** (2026-10-23): the identification loop finds every true element on the three files; open: the method's +11. **Auto ID, what 0.11.0 left**: the identification loop finds every true element on the three files; open: the method's overlap check (an element whose every peak sits under a modelled one leaves no residual peak and is not reported; the method reports - such finds as possible only), Mn on the Al alloys (proposed at 22 and 70 L_D, a 6xxx minor element by the numbers, the owner's word - pending), the line shape beside a dominant K line (the tail, opt-in in `LineTail`, to be measured per detector on the Al matrix and + such finds as possible only), Mn on the Al alloys (proposed at 22 and 70 L_D, a 6xxx minor element by the numbers, the alloy's + specification not yet known), the line shape beside a dominant K line (the tail, opt-in in `LineTail`, to be measured per detector on the Al matrix and switched on by the consumer); the proposer's own axis refinement with a two-line guard (the app refines on its K picks today); the downward family rule (a K pick of Z ≥ 30 shows its L line) and an "uncorroborated" tier in `ProposalResult` (the app tiers them today); the self-pair half - bound on sum peaks waits for a registration with planted sums that respect it; `AxisGateTests`' known issue (offset and gain trade off + bound on sum peaks waits for planted sums that respect it; `AxisGateTests`' known issue (offset and gain trade off on lines that span little of the axis) stands. diff --git a/docs/parity.md b/docs/parity.md index 3a17bf0..4e7a757 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -4,7 +4,7 @@ What SpectroKit has of exSpy's and HyperSpy's EDS and EELS API, row by row, what source on disk on 2026-10-21 for 0.8.0 (the walk of every public method of `exspy/signals/{eds,eds_tem,eds_sem,eels,dielectric_function}.py`, `exspy/models/*`, `exspy/components/*`, `exspy/utils/*`, `exspy/material.py`, and the tests in `exspy/tests/` for tolerances). -**Reference.** exSpy 0.3.2 and HyperSpy 2.4.0 in `References/python/hyperspy` (the suite folder's gitignored Python reference, `tools/setup-python-refs.sh`); +**Reference.** exSpy 0.3.2 and HyperSpy 2.4.0 in `References/python` (gitignored; `tools/reference/setup.sh` builds it from `tools/reference/requirements.txt`); fixtures stay pinned to exSpy `7185a4d1` (`References/exspy-7185a4d1`) with RosettaSciIO 0.14.0, NumPy 2.5.4, SciPy 1.18.1, except the 0.8.0 maps fixture, which is exSpy 0.3.2's own output (`tools/edx-quant-maps/pins.py`). The public methods were read from the installed modules; the tolerance column is what exSpy's own test files assert (`assert_allclose` defaults are rtol 1e-7). @@ -26,13 +26,13 @@ against planted truth, the evidence column says so. | code | meaning | |---|---| | BIT | bit-identical to the pin (a measured fact, not a requirement: numerics are held to the tolerance the Python package's own tests use; readers, I/O, indices and metadata exact, D17) | -| TOL | within a registered tolerance (the measured value is often far smaller) | -| DEV | ported with a registered deviation (named in the code or the evidence) | +| TOL | within a stated tolerance (the measured value is often far smaller) | +| DEV | ported with a stated deviation (named in the code or the evidence) | | NOPIN | ported, but no exSpy output is compared (truth, constants or source reading only) | | PART / NO / NP | partial / not ported / not planned | Priority: **study** = on Paul's path (the TEM study, the two apps' open kit asks: public quantification, the EDX fit accuracy, EDS -calibration and `rebin`, the EELS model fit and what it stands on); **useful** = worth porting while a session lasts; **not planned: reason**. +calibration and `rebin`, the EELS model fit and what it stands on); **useful** = worth porting soon; **not planned: reason**. "package" in the SpectroKit column means the code exists and is tested but is not public API: a consumer cannot call it. ## CI portability @@ -271,7 +271,7 @@ ratio method on exSpy's GOS), which exSpy does not have as a function. The proposer's confirmation-line rule (R4, `ProposerPolicy.familyConsistency`) and its sum-peak plausibility bound (`sumPeakFraction`): no Python counterpart; the bounds are physics (`familyOvervoltage` 2, `sumPeakFraction` 1, not measured cuts), `validation: "none"`; tests `FamilyConsistencyTests` and -`IdentificationLoopTests` on planted spectra, `RealFileAutoIDTests` (opt-in) on the owner's three files. The identification fit's Kβ weights are +`IdentificationLoopTests` on planted spectra, `RealFileAutoIDTests` (opt-in) on three measured files. The identification fit's Kβ weights are NIST EPQ's (`EDSLineWeights.epqK`), not eXSpy's; the quantification fit keeps eXSpy's, so no parity row moves. ## Where the source claims more than the tests show diff --git a/docs/status.md b/docs/status.md index 16aa279..104c4d1 100644 --- a/docs/status.md +++ b/docs/status.md @@ -1,15 +1,19 @@ # Status -SpectroKit 0.11.0 (2026-10-22, local main until tagged; 0.10.0 tagged), on FormatoKit `"0.4.0"..<"1.0.0"` and ImagoKit `"0.6.1"..<"1.0.0"`, against HyperSpy 2.4.0 and exSpy 0.3.2. Live: EDX (the line table, peak identification, net counts, Cliff-Lorimer, zeta and -cross-section quantification with the absorption iteration, per pixel over a spectrum image) and EELS (t/lambda thickness, zero-loss alignment, power-law extrapolation, Fourier-log and Fourier-ratio deconvolution, the multi-edge model fit on 20 K and L edges with at% and wt%, Richardson-Lucy deconvolution, the MLLS fit, region sums); in EDX also sum and escape peaks in the fit, k-factor import, sigma on wt% and at%, and the per-region result; in SpectroCore line-profile pooling, smoothing (display only) and sparse regions. Every quantification (including the sum/escape model, sigma and MLLS, none of which has a Python reference or a standard) says -`validation: "none"` until a standard of known composition is measured. Parity: `docs/parity.md`; timings against Python: `docs/benchmarks.md`; parity numbers (2026-10-22, recounted from the rows): 100 rows, 58 present, 11 partial, 23 missing, 8 not planned; study rows 52 of 67 present; what is open (EELS fine structure, Richardson-Lucy speed on large images, weighted MLLS, EDS -calibration, the t/lambda loop speed): `docs/open-items.md`. +SpectroKit 0.12.0 (tagged 2026-10-23), on FormatoKit `"0.4.0"..<"1.0.0"` and ImagoKit `"0.6.1"..<"1.0.0"`, against HyperSpy 2.4.0 and +exSpy 0.3.2 / 7185a4d1. macOS 15 and later. -Tests (2026-10-21): the pinned Python comparisons (golden baseline, exSpy/HyperSpy/pyEELSMODEL pins, benchmarks, the PeakGate and Gate D runs) sit in `SpectroKitParityTests`; helpers and fixture loaders shared with the fast targets are in `SpectroTestSupport`. 360 tests before and after the split (309 fast + 51 parity). Mac, release, `--parallel`: fast target (`--skip SpectroKitParityTests`) 30 s, full suite 1 min 54 s; before the split, 1 min 45 s for the one full suite. CI keeps running the full suite. +Live: +- **EDX**: the line table, element identification (the published identification loop, `ElementProposer`), net counts, the EDS fit with + sum and escape peaks, Cliff-Lorimer, zeta and cross-section quantification with the absorption iteration, per pixel over a spectrum + image, k-factor import, sigma on wt% and at%, the per-region result. +- **EELS**: t/lambda thickness, zero-loss alignment, power-law extrapolation, Fourier-log, Fourier-ratio and Richardson-Lucy + deconvolution, the edge model fit (joint, or per-edge windows with a 1σ per edge intensity) on 383 bundled K, L and M edges, the MLLS + fit, composition in at% and wt%, region sums. +- **SpectroCore**: line-profile pooling, display smoothing, sparse regions. -0.11.0 makes Auto ID the identification loop of the published method (docs/design/auto-id-tem.md: residual peaks, the statistic with the 1 % -model term, confirmation lines, prune; the flank-misfit factor is gone): on the owner's three files it proposes every true element -(`RealFileAutoIDTests`, opt-in). It also adds the sum-peak plausibility bound, the end of the pool at Bi and an opt-in low-energy tail of the -line shape (`LineTail`, `TailEstimate`, off by default). The parity target runs before the tag. +Every quantification says `validation: "none"` until a standard of known composition is measured. Parity row by row: `docs/parity.md`; +timings against Python: `docs/benchmarks.md`; what is open: `docs/open-items.md`; fixtures and their generators: `Tests/Fixtures/README.md`. -EELS per-edge windows (0.11.0, unreleased): `EELSModelFit.fit(edgeWindows:)` fits each edge's power law on its own background window and its amplitude on its own signal window (`EdgeWindows.defaults` holds the field's rules); `EELSBackground.preview` is the live power-law estimate; planted tests only, `validation: "none"`. +Tests: the fast targets run per change (`swift test -c release --parallel --skip SpectroKitParityTests`); the pinned Python comparisons +(golden baseline, exSpy/HyperSpy/pyEELSMODEL pins, the planted EDX runs) are `SpectroKitParityTests`, run in CI on every push and before a tag.