///|
/// Diagnostics for an isothermal flash result.
pub(all) struct FlashDiagnostic {
  name : String
  value : Double
  threshold : Double
  passed : Bool
  message : String
} derive(Debug, Eq, ToJson)

///|
pub fn flash_diagnostics(
  result : FlashResult,
  composition_tolerance? : Double = 1.0e-8,
) -> Array[FlashDiagnostic] raise @domain.ExtractionError {
  @domain.require_positive("flash composition tolerance", composition_tolerance)
  let liquid_total = result.liquid_composition.fold(init=0.0, (sum, value) => {
    sum + value
  })
  let vapor_total = result.vapor_composition.fold(init=0.0, (sum, value) => {
    sum + value
  })
  let feed_total = result.feed_composition.fold(init=0.0, (sum, value) => {
    sum + value
  })
  let diagnostics : Array[FlashDiagnostic] = []
  diagnostics.push({
    name: "liquid normalization",
    value: (liquid_total - 1.0).abs(),
    threshold: composition_tolerance,
    passed: (liquid_total - 1.0).abs() <= composition_tolerance,
    message: "liquid composition sums to one",
  })
  diagnostics.push({
    name: "vapor normalization",
    value: (vapor_total - 1.0).abs(),
    threshold: composition_tolerance,
    passed: (vapor_total - 1.0).abs() <= composition_tolerance,
    message: "vapor composition sums to one",
  })
  diagnostics.push({
    name: "feed normalization",
    value: (feed_total - 1.0).abs(),
    threshold: composition_tolerance,
    passed: (feed_total - 1.0).abs() <= composition_tolerance,
    message: "feed composition sums to one",
  })
  diagnostics.push({
    name: "Rachford-Rice residual",
    value: result.rachford_rice_residual.abs(),
    threshold: composition_tolerance,
    passed: result.rachford_rice_residual.abs() <= composition_tolerance,
    message: "flash equation residual is small",
  })
  diagnostics
}

///|
pub fn flash_component_recovery(
  result : FlashResult,
  component : Int,
) -> Double raise @domain.ExtractionError {
  if component < 0 || component >= result.feed_composition.length() {
    raise @domain.ExtractionError::InvalidArgument(
      message="flash component index out of range",
    )
  }
  result.vapor_fraction *
  result.vapor_composition[component] /
  result.feed_composition[component].max(1.0e-12)
}

///|
pub fn flash_component_split(
  result : FlashResult,
  component : Int,
) -> Double raise @domain.ExtractionError {
  let recovery = flash_component_recovery(result, component)
  recovery.max(0.0).min(1.0)
}

///|
pub fn phase_composition_difference(
  result : FlashResult,
) -> Double raise @domain.ExtractionError {
  if result.liquid_composition.length() != result.vapor_composition.length() {
    raise @domain.ExtractionError::InvalidArgument(
      message="flash phase dimensions differ",
    )
  }
  let mut sum = 0.0
  for index in 0.. FlashSensitivity raise @domain.ExtractionError {
  if component < 0 || component >= feed.length() {
    raise @domain.ExtractionError::InvalidArgument(
      message="flash sensitivity component out of range",
    )
  }
  @domain.require_positive("flash sensitivity step", relative_step)
  let base = isothermal_flash(feed_composition=feed, k_values~)
  let perturbed_feed = feed.to_owned()
  perturbed_feed[component] *= 1.0 + relative_step
  let perturbed = isothermal_flash(feed_composition=perturbed_feed, k_values~)
  let base_fraction = base.vapor_composition[component]
  let perturbed_fraction = perturbed.vapor_composition[component]
  let change = perturbed_fraction - base_fraction
  {
    component,
    base_fraction,
    perturbed_fraction,
    change,
    normalized_change: if base_fraction.abs() <= 1.0e-12 {
      change
    } else {
      change / base_fraction
    },
  }
}

///|
pub(all) struct PhaseEnvelopePoint {
  vapor_fraction : Double
  liquid_composition : Array[Double]
  vapor_composition : Array[Double]
  separation : Double
} derive(Debug, Eq, ToJson)

///|
pub fn flash_envelope(
  feed : ArrayView[Double],
  k_values : ArrayView[Double],
  vapor_fractions : ArrayView[Double],
) -> Array[PhaseEnvelopePoint] raise @domain.ExtractionError {
  let base = isothermal_flash(feed_composition=feed, k_values~)
  [
    for fraction in vapor_fractions => {
      @domain.require_fraction("envelope vapor fraction", fraction)
      {
        vapor_fraction: fraction,
        liquid_composition: base.liquid_composition.copy(),
        vapor_composition: base.vapor_composition.copy(),
        separation: phase_composition_difference(base) * fraction,
      }
    }
  ]
}

///|
pub fn flash_recovery_vector(
  result : FlashResult,
) -> Array[Double] raise @domain.ExtractionError {
  [
    for index in 0.. {
      flash_component_recovery(result, index)
    }
  ]
}

///|
pub fn all_components_recovered_above(
  result : FlashResult,
  threshold~ : Double,
) -> Bool raise @domain.ExtractionError {
  @domain.require_fraction("recovery threshold", threshold)
  for index in 0.. Int raise @domain.ExtractionError {
  if result.feed_composition.length() == 0 {
    raise @domain.ExtractionError::InsufficientData(required=1, actual=0)
  }
  let recoveries = flash_recovery_vector(result)
  let mut index = 0
  for cursor in 1..