///|
/// Apply a finite affine intensity calibration; scale may be negative.
pub fn affine_intensity(
  input : Spectrum,
  scale : Double,
  offset : Double,
) -> Spectrum raise SpectralError {
  finite(scale, "intensity scale")
  finite(offset, "intensity offset")
  let y = Array::make(input.length(), 0.0)
  for i in 0.. Spectrum raise SpectralError {
  let center = mean(input.values)
  let deviation = sample_deviation(input.values)
  if deviation <= 1.0e-14 * root_mean_square(input.values) || deviation == 0.0 {
    raise Numerical("SNV requires nonconstant intensities")
  }
  let y = Array::makei(input.length(), fn(i) {
    (input.values[i] - center) / deviation
  })
  spectrum(input.axis, y)
}

///|
/// Normalize the full vector to unit Euclidean norm, retaining sign.
pub fn unit_vector(input : Spectrum) -> Spectrum raise SpectralError {
  let rms = root_mean_square(input.values)
  if rms == 0.0 {
    raise Numerical("cannot normalize a zero spectrum")
  }
  let norm = rms * input.length().to_double().sqrt()
  let y = input.values.map(fn(v) { v / norm })
  spectrum(input.axis, y)
}

///|
/// Map min/max onto [0,1]. A constant spectrum is rejected explicitly.
pub fn range_normalize(input : Spectrum) -> Spectrum raise SpectralError {
  let mut low = input.values[0]
  let mut high = low
  for v in input.values {
    if v < low {
      low = v
    }
    if v > high {
      high = v
    }
  }
  let span = high - low
  if span == 0.0 {
    raise Numerical("constant spectrum has no range")
  }
  spectrum(input.axis, input.values.map(fn(v) { (v - low) / span }))
}

///|
/// Convert transmittance to absorbance. Values must lie in (0,1] (fraction)
/// or (0,100] (percent). Zero is rejected instead of being silently clipped.
pub fn absorbance(
  input : Spectrum,
  percent~ : Bool = false,
) -> Spectrum raise SpectralError {
  let denominator = if percent { 100.0 } else { 1.0 }
  let y = Array::make(input.length(), 0.0)
  for i in 0.. 1.0 {
      raise Invalid("transmittance outside physical interval")
    }
    y[i] = -@math.log10(t)
  }
  spectrum(input.axis, y)
}

///|
/// Convert absorbance to fractional or percent transmittance.
pub fn transmittance(
  input : Spectrum,
  percent~ : Bool = false,
) -> Spectrum raise SpectralError {
  let scale = if percent { 100.0 } else { 1.0 }
  let y = Array::make(input.length(), 0.0)
  for i in 0..