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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions Sources/SpectroEDX/Fit/SumEscapeFit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
}

Expand Down Expand Up @@ -285,6 +292,9 @@ public nonisolated enum EDSSumEscapeFit {
}
var escComponents: [EscapeComponent] = []
for gi in 0..<g where escColumns[gi] != nil { escComponents.append(EscapeComponent(group: groups[gi].id, area: f * area[gi])) }
var componentCounts = [Double](repeating: 0, count: n)
if c != 0 { let q = sumShape(area); for k in 0..<n { componentCounts[k] += c * q[k] } }
if f != 0 { let q = escShape(area); for k in 0..<n { componentCounts[k] += f * q[k] } }

// Sigma of c and f: the term's shape with the fit's other columns projected out.
func sigmaOf(_ q: [Double]) -> Double? {
Expand Down Expand Up @@ -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)
}
}
22 changes: 16 additions & 6 deletions Sources/SpectroEDX/Proposer/ElementProposer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Int>, dropped: [String], relative: Double, widths: [Int], weighted: Bool = true) throws -> Pass {
channels: Range<Int>, 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] }
Expand Down Expand Up @@ -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<Int>, dropped: [String], relative: Double, widths: [Int]) -> [Result<Pass, Error>] {
channels: Range<Int>, dropped: [String], relative: Double, widths: [Int],
explained: [Double]?) -> [Result<Pass, Error>] {
var out = [Result<Pass, Error>?](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! }
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -379,7 +388,8 @@ public nonisolated struct ElementProposer: Sendable {
func runAll(_ jobs: [[FitLineGroup]], on data: [Double]) throws -> [Result<Pass, Error>] {
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
}
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions Sources/SpectroEDX/Quant/FitRange.swift
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ public nonisolated enum FitRangeSensitivityCheck {
var refit = input
refit.method.fitToKeV = to
refit.refinement = q.refinement
refit.sumEscape = PooledQuantifier.sameSumEscape(as: q) // the reported fit's sum-peak decision, not a new one
let after: PooledQuantification
do { after = try PooledQuantifier.run(refit, tables: nil, cancelled: cancelled, progress: progress.map { p in { @Sendable f in p(f * 0.95) } }) }
catch let c as SpectroCancelled { throw c }
Expand Down
Loading
Loading