///|
pub(all) enum Step {
Asls(Double, IterationOptions)
Arpls(IterationOptions)
Snip(Int32, Bool, Bool)
PolynomialBaseline(Array[Bool], Int32)
LocalPolynomial(Int32, Int32, Int32)
Median(Int32)
Snv
UnitVector
RangeNormalize
Scatter(ScatterModel)
Affine(Double, Double)
Crop(Double, Double)
Resample(Array[Double])
Absorbance(Bool)
Transmittance(Bool)
}
///|
pub struct Pipeline {
steps : Array[Step]
maximum_work : Double
require_convergence : Bool
}
///|
pub(all) struct StepReport {
name : String
input_samples : Int32
output_samples : Int32
rms_before : Double
rms_after : Double
iterations : Int32?
converged : Bool?
relative_change : Double?
}
///|
pub(all) struct PipelineResult {
spectrum : Spectrum
trace : Array[StepReport]
estimated_work : Double
}
///|
/// Copy configuration arrays at construction. The budget is a conservative
/// operation estimate, not wall-clock time or a cancellation guarantee.
pub fn pipeline(
steps : Array[Step],
maximum_work~ : Double = 100000000.0,
require_convergence~ : Bool = true,
) -> Pipeline raise SpectralError {
if steps.length() < 1 || steps.length() > 32 {
raise Budget("pipeline needs 1..32 steps")
}
finite(maximum_work, "pipeline work budget")
if maximum_work < 1.0 || maximum_work > 1000000000.0 {
raise Budget("pipeline work budget outside [1,1e9]")
}
let owned = steps.map(fn(step) {
match step {
Resample(axis) => Resample(axis.copy())
PolynomialBaseline(mask, degree) =>
PolynomialBaseline(mask.copy(), degree)
other => other
}
})
{ steps: owned, maximum_work, require_convergence, }
}
///|
fn step_name(step : Step) -> String {
match step {
Asls(_, _) => "asls"
Arpls(_) => "arpls"
Snip(_, _, _) => "snip"
PolynomialBaseline(_, _) => "polynomial_baseline"
LocalPolynomial(_, _, _) => "local_polynomial"
Median(_) => "median"
Snv => "snv"
UnitVector => "unit_vector"
RangeNormalize => "range_normalize"
Scatter(_) => "msc"
Affine(_, _) => "affine"
Crop(_, _) => "crop"
Resample(_) => "resample"
Absorbance(_) => "absorbance"
Transmittance(_) => "transmittance"
}
}
///|
fn estimate_work(step : Step, count : Int32) -> Double raise SpectralError {
let n = count.to_double()
match step {
Asls(_, options) | Arpls(options) => {
validate_iteration_options(options)
100.0 * n * options.max_iterations.to_double()
}
Snip(window, _, _) => {
if window < 1 || window > 2048 {
raise Budget("SNIP window outside budget")
}
20.0 * n * window.to_double()
}
LocalPolynomial(window, degree, _) => {
if window < 3 || window > 101 || degree < 0 || degree > 8 {
raise Budget("local fit outside budget")
}
let p = (degree + 1).to_double()
10.0 * n * window.to_double() * p * p
}
PolynomialBaseline(_, degree) => {
if degree < 0 || degree > 8 {
raise Budget("polynomial degree outside budget")
}
let p = (degree + 1).to_double()
10.0 * n * p * p
}
Median(window) => {
if window < 3 || window > 101 {
raise Budget("median window outside budget")
}
10.0 * n * window.to_double() * window.to_double()
}
Resample(axis) => {
if axis.length() > 65536 {
raise Budget("resampling axis outside budget")
}
20.0 * (n + axis.length().to_double())
}
_ => 100.0 * n
}
}
///|
fn apply_step(
step : Step,
input : Spectrum,
) -> (Spectrum, BaselineResult?) raise SpectralError {
match step {
Asls(p, options) => {
let fit = asls(input, asymmetry=p, options~)
(input.with_values(fit.corrected), Some(fit))
}
Arpls(options) => {
let fit = arpls(input, options~)
(input.with_values(fit.corrected), Some(fit))
}
Snip(window, decreasing, transformed) => {
let fit = snip(
input,
half_window=window,
decreasing~,
count_transform=transformed,
)
(input.with_values(fit.corrected), Some(fit))
}
PolynomialBaseline(mask, degree) => {
let fit = masked_polynomial_baseline(input, mask, degree~)
(input.with_values(fit.corrected), Some(fit))
}
LocalPolynomial(window, degree, order) =>
(local_polynomial(input, window~, degree~, derivative=order), None)
Median(window) => (median_filter(input, window~), None)
Snv => (snv(input), None)
UnitVector => (unit_vector(input), None)
RangeNormalize => (range_normalize(input), None)
Scatter(model) => (model.transform(input).spectrum, None)
Affine(scale, offset) => (affine_intensity(input, scale, offset), None)
Crop(left, right) => (crop(input, left, right), None)
Resample(axis) => (resample(input, axis), None)
Absorbance(percent) => (absorbance(input, percent~), None)
Transmittance(percent) => (transmittance(input, percent~), None)
}
}
///|
/// Run deterministically and return only a complete result. Original input
/// and configuration remain owned; each successful step adds compact metrics.
pub fn Pipeline::run(
self : Pipeline,
input : Spectrum,
) -> PipelineResult raise SpectralError {
let mut current = input
let trace : Array[StepReport] = []
let mut work = 0.0
for step in self.steps {
work = work + estimate_work(step, current.length())
if work > self.maximum_work {
raise Budget("pipeline work estimate exceeds configured budget")
}
let before = root_mean_square(current.values)
let input_samples = current.length()
let (next, baseline) = apply_step(step, current)
let (iterations, converged, change) = match baseline {
Some(fit) => {
if self.require_convergence && !fit.converged {
raise Numerical("baseline step did not converge: " + step_name(step))
}
(Some(fit.iterations), Some(fit.converged), Some(fit.relative_change))
}
None => (None, None, None)
}
trace.push({
name: step_name(step),
input_samples,
output_samples: next.length(),
rms_before: before,
rms_after: root_mean_square(next.values),
iterations,
converged,
relative_change: change,
})
current = next
}
{ spectrum: current, trace, estimated_work: work, }
}