///|
/// One independent uncertainty source and its local sensitivity.
pub struct UncertaintySource {
  name : String
  standard_uncertainty : Double
  sensitivity : Double
} derive(Debug, Eq)

///|
/// Construct an uncertainty source.
pub fn UncertaintySource::new(
  name : String,
  standard_uncertainty : Double,
  sensitivity : Double,
) -> UncertaintySource {
  if standard_uncertainty < 0.0 {
    abort("standard uncertainty must be non-negative")
  }
  { name, standard_uncertainty, sensitivity }
}

///|
/// One propagated contribution in an uncertainty budget.
pub struct UncertaintyContribution {
  name : String
  effective_uncertainty : Double
  variance : Double
  fraction : Double
} derive(Debug, Eq)

///|
/// Combined RSS uncertainty and its named contributors.
pub struct UncertaintyBudget {
  contributors : Array[UncertaintyContribution]
  combined : Double
  total_variance : Double
} derive(Debug, Eq)

///|
/// Combine independent uncertainty sources by root-sum-square propagation.
pub fn uncertainty_budget(
  sources : Array[UncertaintySource],
) -> UncertaintyBudget {
  if sources.length() == 0 {
    abort("uncertainty budget must contain at least one source")
  }
  let effective = sources.map(source => {
    source.standard_uncertainty * source.sensitivity
  })
  let mut total_variance = 0.0
  for value in effective {
    total_variance += value * value
  }
  let combined = total_variance.sqrt()
  let contributors = sources.map(source => {
    let value = source.standard_uncertainty * source.sensitivity
    let variance = value * value
    let fraction = if total_variance == 0.0 {
      0.0
    } else {
      variance / total_variance
    }
    { name: source.name, effective_uncertainty: value, variance, fraction }
  })
  { contributors, combined, total_variance }
}

///|
/// Return a symmetric interval at a requested coverage multiplier.
pub fn UncertaintyBudget::interval(
  self : UncertaintyBudget,
  nominal : Double,
  coverage_factor : Double,
) -> Interval {
  if coverage_factor < 0.0 {
    abort("uncertainty coverage factor must be non-negative")
  }
  let margin = self.combined * coverage_factor
  Interval::new(nominal - margin, nominal + margin)
}

///|
/// Return the largest effective uncertainty contributor.
pub fn UncertaintyBudget::largest(
  self : UncertaintyBudget,
) -> UncertaintyContribution {
  let mut largest = self.contributors[0]
  for contributor in self.contributors {
    if contributor.variance > largest.variance {
      largest = contributor
    }
  }
  largest
}

///|
/// Return contributors ordered by variance fraction.
pub fn UncertaintyBudget::ranked(
  self : UncertaintyBudget,
) -> Array[UncertaintyContribution] {
  let result = self.contributors
  for index in 1.. 0 && result[position - 1].fraction < current.fraction {
      result[position] = result[position - 1]
      position -= 1
    }
    result[position] = current
  }
  result
}

///|
/// Calculate the reduction in combined uncertainty after removing a source.
pub fn uncertainty_without(
  budget : UncertaintyBudget,
  source_name : String,
) -> Double {
  let mut remaining_variance = budget.total_variance
  for contributor in budget.contributors {
    if contributor.name == source_name {
      remaining_variance -= contributor.variance
    }
  }
  if remaining_variance <= 0.0 {
    0.0
  } else {
    remaining_variance.sqrt()
  }
}