From 584d84e5ad360bdd7790b51354ea739b0e5bd1c8 Mon Sep 17 00:00:00 2001 From: Paullo9 Date: Sat, 31 Oct 2026 14:42:04 +0100 Subject: [PATCH 1/2] feat: the proposer and the unlisted-line check take the sum/escape peaks as explained With the sum/escape model on, the unlisted-line check proposed on the bare spectrum and named the modelled pile-up and escape peaks as elements. SumEscapeResult.componentCounts (the fitted c S(A) + f E(A)) is now carried by the quantification and handed to ElementProposer.propose(explained:), which adds it to every fit as a fixed offset. nil (the default, and the model off) runs the proposer as before. Planted pools (tools/edx-sim proposer_sim, model on): 100 counts/px no longer names Ar, Ag, Pd; 300 counts/px no longer names Rh, Ar, In, Sn, Pd, Ag, Cd, Ru, Bi, K; a planted Fe escape with the free escape columns off no longer names La, Ce, Cr, Cs. A planted K K-alpha between two sum peaks is still named first. Model off: unchanged. Done: sum/escape peaks are explained counts for the proposer and the check. Next: docs rows for parity.md and open-items.md item 8. Co-Authored-By: Claude Opus 5.5 --- Sources/SpectroEDX/Fit/SumEscapeFit.swift | 14 +- .../SpectroEDX/Proposer/ElementProposer.swift | 22 ++- .../Quant/PooledQuantification.swift | 11 +- .../SpectroEDX/Quant/UnlistedLineCheck.swift | 5 +- .../SumEscapeExplainedTests.swift | 143 ++++++++++++++++++ 5 files changed, 185 insertions(+), 10 deletions(-) create mode 100644 Tests/SpectroEDXTests/SumEscapeExplainedTests.swift diff --git a/Sources/SpectroEDX/Fit/SumEscapeFit.swift b/Sources/SpectroEDX/Fit/SumEscapeFit.swift index c02f6e0..87adc58 100644 --- a/Sources/SpectroEDX/Fit/SumEscapeFit.swift +++ b/Sources/SpectroEDX/Fit/SumEscapeFit.swift @@ -111,6 +111,12 @@ public nonisolated struct SumEscapeResult: Sendable { public var escapeFraction: Double? public var escapeFractionSigma: Double? public var escapePeaks: [EscapeComponent] + /// The fitted sum and escape peaks together, counts per channel of `fit.channels` (c S(A) + f E(A) at the reported areas): what + /// the model explains beyond the line and continuum columns. The element proposer takes it as a fixed part of its model, so a + /// peak this model covers is not proposed as an element. All zeros when the model is off. It is the exact sum term at the + /// reported areas; `fit.model` holds its linearisation at the previous iterate, which differs by c times the squared area step + /// (nothing at convergence; the warnings say when the alternation did not converge). + public var componentCounts: [Double] public var iterations: Int public var converged: Bool /// The parameters of the model as run, one sentence each. @@ -136,7 +142,8 @@ public nonisolated enum EDSSumEscapeFit { if model.isOff { return SumEscapeResult(fit: baseline, baseline: baseline, sumCoefficient: nil, sumCoefficientSigma: nil, sumCoefficientAtBound: false, sumPeaks: [], escapeFraction: nil, escapeFractionSigma: nil, - escapePeaks: [], iterations: 0, converged: true, + escapePeaks: [], componentCounts: [Double](repeating: 0, count: baseline.channels.count), + iterations: 0, converged: true, parameters: ["sum and escape model: off (the default fit)"], validation: validation) } @@ -285,6 +292,9 @@ public nonisolated enum EDSSumEscapeFit { } var escComponents: [EscapeComponent] = [] for gi in 0.. Double? { @@ -331,7 +341,7 @@ public nonisolated enum EDSSumEscapeFit { return SumEscapeResult(fit: fit, baseline: baseline, sumCoefficient: model.sumPeaks ? c : nil, sumCoefficientSigma: cSigma, sumCoefficientAtBound: cAtBound, sumPeaks: sumComponents, escapeFraction: model.commonEscapeFraction ? f : nil, escapeFractionSigma: fSigma, - escapePeaks: escComponents, iterations: iterations, converged: converged, + escapePeaks: escComponents, componentCounts: componentCounts, iterations: iterations, converged: converged, parameters: params, validation: validation) } } diff --git a/Sources/SpectroEDX/Proposer/ElementProposer.swift b/Sources/SpectroEDX/Proposer/ElementProposer.swift index 38b29a4..7cd72b0 100644 --- a/Sources/SpectroEDX/Proposer/ElementProposer.swift +++ b/Sources/SpectroEDX/Proposer/ElementProposer.swift @@ -236,12 +236,15 @@ public nonisolated struct ElementProposer: Sendable { /// strong line's shape error dragged the continuum beside it (data over model 3.1 in 1 to 2.5 keV beside a planted Al 6 eV narrower than the /// model) and the hump it left became an element (Os M). The pass's `result` is returned in counts (its model and residual unscaled). private static func fitPass(groups: [FitLineGroup], data: [Double], settings s: FitSettings, axis: EnergyAxis, - channels: Range, dropped: [String], relative: Double, widths: [Int], weighted: Bool = true) throws -> Pass { + channels: Range, dropped: [String], relative: Double, widths: [Int], weighted: Bool = true, + explained: [Double]? = nil) throws -> Pass { let model = EDSLineModel(groups: groups, resolutionMnKaEV: s.resolutionMnKaEV, beamEnergy: s.beamEnergy, escapePeaks: s.escapePeaks, droppedLines: dropped, lineTail: s.lineTail) try EDSFit.validate(s.referenceShapes, axis: axis, model: model) var design = LinearDesign.build(model: model, axis: axis, channels: channels, background: s.background, shapes: s.referenceShapes, counts: data) + // Counts the caller's model already explains (the sum and escape peaks of a sum/escape fit): a fixed part of the model. + if let explained { for (k, ch) in channels.enumerated() { design.offset[k] += explained[ch] } } let y = channels.map { data[$0] } var prefix = [Double](repeating: 0, count: y.count + 1) for i in y.indices { prefix[i + 1] = prefix[i] + y[i] } @@ -273,13 +276,14 @@ public nonisolated struct ElementProposer: Sendable { /// `fitPass` for every job, concurrently; the results are in job order. private static func fitPasses(jobs: [[FitLineGroup]], data: [Double], settings s: FitSettings, axis: EnergyAxis, - channels: Range, dropped: [String], relative: Double, widths: [Int]) -> [Result] { + channels: Range, dropped: [String], relative: Double, widths: [Int], + explained: [Double]?) -> [Result] { var out = [Result?](repeating: nil, count: jobs.count) out.withUnsafeMutableBufferPointer { buf in let box = ResultBox(buffer: buf) DispatchQueue.concurrentPerform(iterations: jobs.count) { i in box.buffer[i] = Result { try fitPass(groups: jobs[i], data: data, settings: s, axis: axis, channels: channels, - dropped: dropped, relative: relative, widths: widths) } + dropped: dropped, relative: relative, widths: widths, explained: explained) } } } return out.map { $0! } @@ -319,11 +323,16 @@ public nonisolated struct ElementProposer: Sendable { /// 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. + /// - explained: counts per channel of `axis` that the caller's model already explains beyond line and continuum columns (the sum + /// and escape peaks of `EDSSumEscapeFit`, `SumEscapeResult.componentCounts`). They are a fixed part of every fit here, so a peak + /// they cover leaves no residual and is never proposed as an element. nil (the default) runs the proposer as before, bit for bit. public func propose(counts: [Double], axis: EnergyAxis, settings: FitSettings, holeCounts: [Double]? = nil, + explained: [Double]? = nil, cancelled: @Sendable () -> Bool = SpectroCancellation.task, progress: (@Sendable (Double) -> Void)? = nil) throws -> ProposalResult { - guard counts.count == axis.size, holeCounts.map({ $0.count == axis.size }) ?? true else { throw ProposerError.lengthMismatch } + guard counts.count == axis.size, holeCounts.map({ $0.count == axis.size }) ?? true, + explained.map({ $0.count == axis.size }) ?? true else { throw ProposerError.lengthMismatch } var s = settings s.method = .leastSquares let channels = axis.fitChannels(from: s.fitFrom, to: s.fitTo) @@ -379,7 +388,8 @@ public nonisolated struct ElementProposer: Sendable { func runAll(_ jobs: [[FitLineGroup]], on data: [Double]) throws -> [Result] { if cancelled() { throw ProposerError.cancelled } fitsPlanned = max(fitsPlanned, jobs.count) - let out = Self.fitPasses(jobs: jobs, data: data, settings: s, axis: axis, channels: channels, dropped: dropped, relative: t.relativeModelTerm, widths: widths) + let out = Self.fitPasses(jobs: jobs, data: data, settings: s, axis: axis, channels: channels, dropped: dropped, relative: t.relativeModelTerm, widths: widths, + explained: explained) fitsDone += jobs.count; tick() return out } @@ -600,7 +610,7 @@ public nonisolated struct ElementProposer: Sendable { // shape is off, and the statistic's weights spare that misfit). σ₀ and L_D are that fit's own null variance. Pruned, rejected and withheld // candidates keep the numbers of the fit that decided them (they are not in the final model). let report = try Self.fitPass(groups: model, data: counts, settings: s, axis: axis, channels: channels, dropped: dropped, - relative: t.relativeModelTerm, widths: widths, weighted: false) + relative: t.relativeModelTerm, widths: widths, weighted: false, explained: explained) fitsDone += 1 // Candidates: the found elements from the final fit, then the pruned, then the rest that were tested. let finalParents = parents(report) diff --git a/Sources/SpectroEDX/Quant/PooledQuantification.swift b/Sources/SpectroEDX/Quant/PooledQuantification.swift index 789f613..5ce10b8 100644 --- a/Sources/SpectroEDX/Quant/PooledQuantification.swift +++ b/Sources/SpectroEDX/Quant/PooledQuantification.swift @@ -162,6 +162,9 @@ public nonisolated struct PooledQuantification: Sendable { public var footerLines: [String] /// The settings the reported fit ran with (axis, width, range, continuum, estimator): the unlisted-line check proposes on them. package var fitSettings: FitSettings + /// The sum and escape peaks the reported fit modelled (`SumEscapeResult.componentCounts` on the full axis, 0 outside the fitted + /// channels); nil when the sum/escape model is off. The unlisted-line check hands it to the proposer as explained counts. + package var sumEscapeCounts: [Double]? = nil /// The fitted range as run and where its upper end came from (WP3c; the footer's "fit range" line). package var fitRange: FitRangeChoice /// WP3c: what one extra fit to the axis end does to each quantified net, when the axis runs past the default range; @@ -268,11 +271,15 @@ public nonisolated enum PooledQuantifier { try SpectroCancellation.check(cancelled) progress?(0.85) var sumEscapeLines: [String] = [] + var sumEscapeCounts: [Double]? do { if let model = input.sumEscape, !model.isOff { let r = try EDSSumEscapeFit.run(counts: counts, axis: axis, settings: fitSettings, model: model, escapeRatioWarning: policy.fit.escapeRatioWarning) fit = r.fit; sumEscapeLines = r.parameters + var full = [Double](repeating: 0, count: axis.size) + for (k, ch) in r.fit.channels.enumerated() where k < r.componentCounts.count { full[ch] = r.componentCounts[k] } + sumEscapeCounts = full } else { fit = try EDSFit.run(counts: counts, axis: axis, settings: fitSettings, escapeRatioWarning: policy.fit.escapeRatioWarning) } @@ -405,12 +412,14 @@ public nonisolated enum PooledQuantifier { try SpectroCancellation.check(cancelled) progress?(1) - return PooledQuantification( + var result = PooledQuantification( method: method, rows: rows, referenceElement: referenceIndex.map { rows[$0].element }, fit: fit, fileAxis: input.axis, usedAxis: axis, axisLocked: locked, refinement: refinement, plotModel: plotModel, plotBackground: plotBackground, kSet: kSet, abundanceRefusal: abundanceRefusal, absorption: absorption, warnings: warnings, qualityLabel: qualityLabel, quality: quality, footerLines: footer, fitSettings: fitSettings, fitRange: fitRange) + result.sumEscapeCounts = sumEscapeCounts + return result } /// What every shown sigma covers (WP3b F3 wording): the fit's counting statistics, not the choice of model. diff --git a/Sources/SpectroEDX/Quant/UnlistedLineCheck.swift b/Sources/SpectroEDX/Quant/UnlistedLineCheck.swift index 31bf367..39e03ca 100644 --- a/Sources/SpectroEDX/Quant/UnlistedLineCheck.swift +++ b/Sources/SpectroEDX/Quant/UnlistedLineCheck.swift @@ -171,7 +171,10 @@ public nonisolated enum UnlistedLineChecker { proposer: ElementProposer = ElementProposer(), cancelled: @Sendable () -> Bool = SpectroCancellation.task, progress: (@Sendable (Double) -> Void)? = nil) throws -> ProposalResult { - try proposer.propose(counts: counts, axis: q.usedAxis, settings: proposerSettings(q), cancelled: cancelled, progress: progress) + // The sum and escape peaks the reported fit modelled are explained counts for the proposer: a peak they cover is not an + // unlisted element (nil when the sum/escape model is off: the proposer runs as it always ran). + try proposer.propose(counts: counts, axis: q.usedAxis, settings: proposerSettings(q), explained: q.sumEscapeCounts, + cancelled: cancelled, progress: progress) } /// "Al_Ka" -> "Al K\u{03B1}", "Lu_Ma" -> "Lu M\u{03B1}", "Cu_Lb1" -> "Cu L\u{03B2}1". diff --git a/Tests/SpectroEDXTests/SumEscapeExplainedTests.swift b/Tests/SpectroEDXTests/SumEscapeExplainedTests.swift new file mode 100644 index 0000000..fd7609c --- /dev/null +++ b/Tests/SpectroEDXTests/SumEscapeExplainedTests.swift @@ -0,0 +1,143 @@ +// +// SumEscapeExplainedTests.swift +// Role: the sum/escape model's peaks are explained counts for the element proposer and the unlisted-line check. Before, a Quantify fit +// with `SumEscapeModel` on modelled the pile-up and escape peaks, but the check proposed on the bare spectrum and named +// them as elements (measured on the tools/edx-sim pool below: Ar, Ag, Pd at 100 counts/px; Rh, Ar, In, Sn, Pd, Ag, Cd, Ru, +// Bi, K at 300 counts/px; La, Ce, Cr, Cs at the planted Fe escape with the free escape columns off). Planted truth only; the +// model is `validation: "none"` and eXSpy has no sum or escape model to compare with. +// + +import Foundation +import Testing +import SpectroTestSupport +import SpectroCore +import SpectroEDX + +struct SumEscapeExplainedTests { + + private struct SimPools: Decodable { + struct Region: Decodable { let label: String; let expected: [Double]; let components: [String: Double] } + let offset: Double, scale: Double, size: Int + let regions: [Region] + var axis: EnergyAxis { EnergyAxis(offset: offset, scale: scale, size: size) } + } + + /// tools/edx-sim/proposer_sim.py: region 1 is the Al matrix at 100 counts/px (Al+Al sum 1845 counts), region 2 the beta'' + /// pixel at 300 counts/px (six sum peaks, 454 to 8553 counts). + private static let sim: SimPools = { + let url = EDXFixtures.dir.appendingPathComponent("eds-proposer-sim.json") + return try! JSONDecoder().decode(SimPools.self, from: Data(contentsOf: url)) + }() + + private static func input(counts: [Double], axis: EnergyAxis, elements: [String], sumEscape: SumEscapeModel?, + policy: QuantificationPolicy = .default) -> PooledQuantificationInput { + var m = QuantificationMethod() + m.elements = elements.map { .init(symbol: $0, role: .quantify, isManual: false) } + m.beamEnergyKeV = 200 + m.absorptionCorrection = false + m.lockEnergyAxis = true + let meta = SpectrumImageMetadata(fileName: "planted", filePath: "", scanWidth: 1, scanHeight: 1, channelCount: axis.size, + energyOffsetEV: axis.offset * 1000, energyDispersionEV: axis.scale * 1000) + return PooledQuantificationInput(counts: counts.map { UInt64(max($0, 0).rounded()) }, axis: axis, method: m, metadata: meta, + regionName: "planted", pixelCount: 1, policy: policy, sumEscape: sumEscape) + } + + private static func check(_ input: PooledQuantificationInput) throws -> (UnlistedLineCheck, PooledQuantification) { + let q = try PooledQuantifier.run(input, tables: QuantificationTables.bundled()) + let proposal = try UnlistedLineChecker.propose(counts: input.counts.map { Double($0) }, quantification: q) + return (UnlistedLineChecker.check(proposal: proposal, input: input, quantification: q), q) + } + + private static func simCounts(_ region: Int, plant: [String: Double] = [:]) -> [Double] { + var expected = sim.regions[region].expected + if !plant.isEmpty { + let axis = sim.axis + let symbols = Array(Set(plant.keys.map { String($0.split(separator: "_")[0]) })) + let model = EDSLineModel.build(elements: symbols, axis: axis, beamEnergy: 200, resolutionMnKaEV: 130, escapePeaks: false) + let energies = (0.. 0.5 * Self.sim.regions[region].components.filter { $0.key.hasPrefix("sum_") }.values.reduce(0, +)) + #expect(on.candidates.isEmpty, "the modelled sum peaks are explained: \(on.summary)") + } + + /// A real line between two sum peaks is still found with the model on: K K-alpha (3.314 keV, between Al+Si 3.226 and Si+Si 3.479), + /// the strongest candidate. (The elements its peak was tested against, In and Cd among them, are named beside it as the check + /// always names every tested candidate at or above L_D; that is the check's rule, not pile-up.) + @Test func aRealLineBesideTheSumPeaksIsStillNamed() throws { + let counts = Self.simCounts(2, plant: ["K_Ka": 3000]) + let (on, _) = try Self.check(Self.input(counts: counts, axis: Self.sim.axis, elements: Self.listed, sumEscape: .both)) + #expect(on.elementNames.first == "K", "\(on.summary)") + } + + /// Red before: a common escape fraction with the free escape columns off left the Fe K-alpha escape (4.66 keV) to the proposer, + /// which named it La (L-alpha 4.651 keV), with Ce, Cr and Cs beside it. + @Test func theCommonEscapeIsExplained() throws { + let axis = EnergyAxis(offset: 0, scale: 0.01, size: 2000) + let energies = (0.. Double in + let x = max(e, 0.05) + return e >= 0.2 ? 2.0 * max(200 - e, 0) / x * exp(-pow(0.3 / x, 3)) / 40 : 0 + } + let model = EDSLineModel.build(elements: ["Al", "Fe"], axis: axis, beamEnergy: 200, resolutionMnKaEV: 130, escapePeaks: true) + for g in model.groups where g.id == "Al_Ka" || g.id == "Fe_Ka" { + let col = EDSLineModel.column(of: g.lines, energies: energies, scale: axis.scale) + for i in mu.indices { mu[i] += 300_000 * col[i] } + if !g.escapes.isEmpty { + let ec = EDSLineModel.column(of: g.escapes, energies: energies, scale: axis.scale) + for i in mu.indices { mu[i] += 0.01 * 300_000 * ec[i] } + } + } + var rng = Xoshiro256StarStar(seed: 3) + let counts = mu.map { rng.poisson($0) } + var policy = QuantificationPolicy.default + policy.fit.escapePeaks = false + let (on, q) = try Self.check(Self.input(counts: counts, axis: axis, elements: ["Al", "Fe"], + sumEscape: SumEscapeModel(commonEscapeFraction: true), policy: policy)) + #expect(q.sumEscapeCounts != nil) + #expect(on.candidates.isEmpty, "the modelled Fe escape is explained: \(on.summary)") + } + + /// The proposer itself: the sum/escape fit's components as `explained` remove the sum peaks from what it can propose; nil runs it + /// as before (the same result as no argument). + @Test func theProposerTakesTheComponentsAsExplained() throws { + let axis = Self.sim.axis + let counts = Self.simCounts(2) + let st = FitSettings.standard(elements: Self.listed, axis: axis, resolutionMnKaEV: 130, beamEnergy: 200) + let r = try EDSSumEscapeFit.run(counts: counts, axis: axis, settings: st, model: .both) + #expect(r.componentCounts.count == r.fit.channels.count) + var explained = [Double](repeating: 0, count: axis.size) + for (k, ch) in r.fit.channels.enumerated() { explained[ch] = r.componentCounts[k] } + let bare = try ElementProposer().propose(counts: counts, axis: axis, settings: st) + let none = try ElementProposer().propose(counts: counts, axis: axis, settings: st, explained: nil) + #expect(bare.candidates == none.candidates) + #expect(bare.candidates.contains { $0.aboveDetectionLimit }) + let shared = try ElementProposer().propose(counts: counts, axis: axis, settings: st, explained: explained) + #expect(!shared.candidates.contains { $0.aboveDetectionLimit }, "\(shared.candidates.map(\.group))") + #expect(throws: ProposerError.lengthMismatch) { + _ = try ElementProposer().propose(counts: counts, axis: axis, settings: st, explained: [1, 2]) + } + let off = try EDSSumEscapeFit.run(counts: counts, axis: axis, settings: st, model: SumEscapeModel()) + #expect(off.componentCounts.allSatisfy { $0 == 0 } && off.componentCounts.count == off.fit.channels.count) + } +} From c45eeab00242296878afecd738663653acd6976c Mon Sep 17 00:00:00 2001 From: Paullo9 Date: Sat, 31 Oct 2026 23:22:33 +0100 Subject: [PATCH 2/2] test: re-record four golden baselines for the new sumEscapeCounts field The pooled quantification result gained a package field, sumEscapeCounts, in this branch. The golden dump is a Mirror walk of the result, so each quantification in demo-edx-tiny-dm4, eds-realistic-l-sim, eds-unlisted-sim and eds-weakline-sim printed one more line and SpectroGoldenTests failed on line count (4 issues, the red on test, test-floors and test-macos15). Re-recorded with SPECTRO_GOLDEN_DIR. diff old new: 9 lines added, all "quantification.sumEscapeCounts = nil", 0 lines removed or changed. No number moves. Done: golden baselines match the branch; parity target green locally (83 tests). Next: merge origin/main and push. Co-Authored-By: Claude Opus 5.5 --- .../Fixtures/spectro-golden/demo-edx-tiny-dm4.txt | 2 ++ .../Fixtures/spectro-golden/eds-realistic-l-sim.txt | 3 +++ .../Fixtures/spectro-golden/eds-unlisted-sim.txt | 2 ++ .../Fixtures/spectro-golden/eds-weakline-sim.txt | 2 ++ Tests/SpectroKitParityTests/SpectroGoldenTests.swift | 4 ++++ 5 files changed, 13 insertions(+) diff --git a/Tests/SpectroCoreTests/Fixtures/spectro-golden/demo-edx-tiny-dm4.txt b/Tests/SpectroCoreTests/Fixtures/spectro-golden/demo-edx-tiny-dm4.txt index ecd19ef..b343f7b 100644 --- a/Tests/SpectroCoreTests/Fixtures/spectro-golden/demo-edx-tiny-dm4.txt +++ b/Tests/SpectroCoreTests/Fixtures/spectro-golden/demo-edx-tiny-dm4.txt @@ -663,6 +663,7 @@ whole.quantification.fitSettings.escapePeaks = true whole.quantification.fitSettings.referenceShapes.shapes: n 0 sum 0000000000000000 0 fnv cbf29ce484222325 whole.quantification.fitSettings.method = .leastSquares whole.quantification.fitSettings.lineTail = nil +whole.quantification.sumEscapeCounts = nil whole.quantification.fitRange.fromKeV = 3fc999999999999a 0.20000000000000001 whole.quantification.fitRange.toKeV = 40239d932f3bac54 9.8077635536521157 whole.quantification.fitRange.source = .defaultReach @@ -901,6 +902,7 @@ region-locked-axis.quantification.fitSettings.escapePeaks = true region-locked-axis.quantification.fitSettings.referenceShapes.shapes: n 0 sum 0000000000000000 0 fnv cbf29ce484222325 region-locked-axis.quantification.fitSettings.method = .leastSquares region-locked-axis.quantification.fitSettings.lineTail = nil +region-locked-axis.quantification.sumEscapeCounts = nil region-locked-axis.quantification.fitRange.fromKeV = 3fc999999999999a 0.20000000000000001 region-locked-axis.quantification.fitRange.toKeV = 402380c28065dbf0 9.7514839290942916 region-locked-axis.quantification.fitRange.source = .defaultReach diff --git a/Tests/SpectroCoreTests/Fixtures/spectro-golden/eds-realistic-l-sim.txt b/Tests/SpectroCoreTests/Fixtures/spectro-golden/eds-realistic-l-sim.txt index 27852dc..f901ea0 100644 --- a/Tests/SpectroCoreTests/Fixtures/spectro-golden/eds-realistic-l-sim.txt +++ b/Tests/SpectroCoreTests/Fixtures/spectro-golden/eds-realistic-l-sim.txt @@ -318,6 +318,7 @@ seed0.seven.quantification.fitSettings.escapePeaks = true seed0.seven.quantification.fitSettings.referenceShapes.shapes: n 0 sum 0000000000000000 0 fnv cbf29ce484222325 seed0.seven.quantification.fitSettings.method = .leastSquares seed0.seven.quantification.fitSettings.lineTail = nil +seed0.seven.quantification.sumEscapeCounts = nil seed0.seven.quantification.fitRange.fromKeV = 3fc999999999999a 0.20000000000000001 seed0.seven.quantification.fitRange.toKeV = 4034000000000000 20 seed0.seven.quantification.fitRange.source = .defaultLimit @@ -686,6 +687,7 @@ seed0.seven-60keV.quantification.fitSettings.escapePeaks = true seed0.seven-60keV.quantification.fitSettings.referenceShapes.shapes: n 0 sum 0000000000000000 0 fnv cbf29ce484222325 seed0.seven-60keV.quantification.fitSettings.method = .leastSquares seed0.seven-60keV.quantification.fitSettings.lineTail = nil +seed0.seven-60keV.quantification.sumEscapeCounts = nil seed0.seven-60keV.quantification.fitRange.fromKeV = 3fc999999999999a 0.20000000000000001 seed0.seven-60keV.quantification.fitRange.toKeV = 404e000000000000 60 seed0.seven-60keV.quantification.fitRange.source = .typed @@ -981,6 +983,7 @@ seed0.four.quantification.fitSettings.escapePeaks = true seed0.four.quantification.fitSettings.referenceShapes.shapes: n 0 sum 0000000000000000 0 fnv cbf29ce484222325 seed0.four.quantification.fitSettings.method = .leastSquares seed0.four.quantification.fitSettings.lineTail = nil +seed0.four.quantification.sumEscapeCounts = nil seed0.four.quantification.fitRange.fromKeV = 3fc999999999999a 0.20000000000000001 seed0.four.quantification.fitRange.toKeV = 4034000000000000 20 seed0.four.quantification.fitRange.source = .defaultLimit diff --git a/Tests/SpectroCoreTests/Fixtures/spectro-golden/eds-unlisted-sim.txt b/Tests/SpectroCoreTests/Fixtures/spectro-golden/eds-unlisted-sim.txt index ccece0f..690e48e 100644 --- a/Tests/SpectroCoreTests/Fixtures/spectro-golden/eds-unlisted-sim.txt +++ b/Tests/SpectroCoreTests/Fixtures/spectro-golden/eds-unlisted-sim.txt @@ -292,6 +292,7 @@ seed0.four.quantification.fitSettings.escapePeaks = true seed0.four.quantification.fitSettings.referenceShapes.shapes: n 0 sum 0000000000000000 0 fnv cbf29ce484222325 seed0.four.quantification.fitSettings.method = .leastSquares seed0.four.quantification.fitSettings.lineTail = nil +seed0.four.quantification.sumEscapeCounts = nil seed0.four.quantification.fitRange.fromKeV = 3fc999999999999a 0.20000000000000001 seed0.four.quantification.fitRange.toKeV = 4034000000000000 20 seed0.four.quantification.fitRange.source = .defaultLimit @@ -1524,6 +1525,7 @@ seed0.seven.quantification.fitSettings.escapePeaks = true seed0.seven.quantification.fitSettings.referenceShapes.shapes: n 0 sum 0000000000000000 0 fnv cbf29ce484222325 seed0.seven.quantification.fitSettings.method = .leastSquares seed0.seven.quantification.fitSettings.lineTail = nil +seed0.seven.quantification.sumEscapeCounts = nil seed0.seven.quantification.fitRange.fromKeV = 3fc999999999999a 0.20000000000000001 seed0.seven.quantification.fitRange.toKeV = 4034000000000000 20 seed0.seven.quantification.fitRange.source = .defaultLimit diff --git a/Tests/SpectroCoreTests/Fixtures/spectro-golden/eds-weakline-sim.txt b/Tests/SpectroCoreTests/Fixtures/spectro-golden/eds-weakline-sim.txt index fe62c64..5cb0029 100644 --- a/Tests/SpectroCoreTests/Fixtures/spectro-golden/eds-weakline-sim.txt +++ b/Tests/SpectroCoreTests/Fixtures/spectro-golden/eds-weakline-sim.txt @@ -212,6 +212,7 @@ level0.quantification.fitSettings.escapePeaks = true level0.quantification.fitSettings.referenceShapes.shapes: n 0 sum 0000000000000000 0 fnv cbf29ce484222325 level0.quantification.fitSettings.method = .leastSquares level0.quantification.fitSettings.lineTail = nil +level0.quantification.sumEscapeCounts = nil level0.quantification.fitRange.fromKeV = 3fc999999999999a 0.20000000000000001 level0.quantification.fitRange.toKeV = 40340160bc0314bd 20.005382300140571 level0.quantification.fitRange.source = .defaultReach @@ -438,6 +439,7 @@ level4.quantification.fitSettings.escapePeaks = true level4.quantification.fitSettings.referenceShapes.shapes: n 0 sum 0000000000000000 0 fnv cbf29ce484222325 level4.quantification.fitSettings.method = .leastSquares level4.quantification.fitSettings.lineTail = nil +level4.quantification.sumEscapeCounts = nil level4.quantification.fitRange.fromKeV = 3fc999999999999a 0.20000000000000001 level4.quantification.fitRange.toKeV = 403404cb90bd12c4 20.018731161270594 level4.quantification.fitRange.source = .defaultReach diff --git a/Tests/SpectroKitParityTests/SpectroGoldenTests.swift b/Tests/SpectroKitParityTests/SpectroGoldenTests.swift index 3f0115c..96769f4 100644 --- a/Tests/SpectroKitParityTests/SpectroGoldenTests.swift +++ b/Tests/SpectroKitParityTests/SpectroGoldenTests.swift @@ -25,6 +25,10 @@ // this fixture) and nothing removed or changed (`diff` old new: 0 lines `<`). Every other file is still the first host app's // 71929a01 baseline, unchanged. // +// The sum/escape-explained proposer: `demo-edx-tiny-dm4`, `eds-realistic-l-sim`, `eds-unlisted-sim` and `eds-weakline-sim` +// re-recorded on purpose for the new `quantification.sumEscapeCounts` field of the pooled result. Each file is the old one +// plus one line per quantification (9 lines in all, every one `= nil` on these fixtures) and nothing removed or changed. +// import Testing import Foundation