///|
pub(all) struct RecoveryEnvelope {
  nominal : @liquid.SingleStageResult
  minimum_recovery : Double
  maximum_recovery : Double
  minimum_case : SingleStageScenario
  maximum_case : SingleStageScenario
  evaluated_cases : Int
} derive(Debug, Eq, ToJson)

///|
/// Evaluate the corner cases of bounded solvent-flow and distribution ranges.
/// Monotonicity is not assumed by the implementation even though the ideal
/// constant-K model is monotone in both inputs.
pub fn single_stage_recovery_envelope(
  feed_flow~ : Double,
  feed_concentration~ : Double,
  nominal_solvent_flow~ : Double,
  solvent_flow_lower~ : Double,
  solvent_flow_upper~ : Double,
  nominal_distribution~ : Double,
  distribution_lower~ : Double,
  distribution_upper~ : Double,
) -> RecoveryEnvelope raise @domain.ExtractionError {
  @domain.require_positive("feed flow", feed_flow)
  @domain.require_non_negative("feed concentration", feed_concentration)
  @domain.require_positive("nominal solvent flow", nominal_solvent_flow)
  @domain.require_positive("solvent flow lower", solvent_flow_lower)
  @domain.require_positive("solvent flow upper", solvent_flow_upper)
  @domain.require_positive("nominal distribution", nominal_distribution)
  @domain.require_positive("distribution lower", distribution_lower)
  @domain.require_positive("distribution upper", distribution_upper)
  if solvent_flow_lower > nominal_solvent_flow ||
    nominal_solvent_flow > solvent_flow_upper {
    raise @domain.ExtractionError::InvalidArgument(
      message="nominal solvent flow must lie inside its bounds",
    )
  }
  if distribution_lower > nominal_distribution ||
    nominal_distribution > distribution_upper {
    raise @domain.ExtractionError::InvalidArgument(
      message="nominal distribution must lie inside its bounds",
    )
  }
  let cases = [
    SingleStageScenario::new(
      name="lower-flow/lower-K",
      feed_flow~,
      feed_concentration~,
      solvent_flow=solvent_flow_lower,
      distribution=distribution_lower,
    ),
    SingleStageScenario::new(
      name="lower-flow/upper-K",
      feed_flow~,
      feed_concentration~,
      solvent_flow=solvent_flow_lower,
      distribution=distribution_upper,
    ),
    SingleStageScenario::new(
      name="upper-flow/lower-K",
      feed_flow~,
      feed_concentration~,
      solvent_flow=solvent_flow_upper,
      distribution=distribution_lower,
    ),
    SingleStageScenario::new(
      name="upper-flow/upper-K",
      feed_flow~,
      feed_concentration~,
      solvent_flow=solvent_flow_upper,
      distribution=distribution_upper,
    ),
  ]
  let results = run_single_stage_scenarios(cases)
  let mut minimum = results[0]
  let mut maximum = results[0]
  for result in results {
    if result.result.recovery < minimum.result.recovery {
      minimum = result
    }
    if result.result.recovery > maximum.result.recovery {
      maximum = result
    }
  }
  {
    nominal: @liquid.single_stage_constant_k(
      feed_flow~,
      feed_concentration~,
      solvent_flow=nominal_solvent_flow,
      distribution=nominal_distribution,
    ),
    minimum_recovery: minimum.result.recovery,
    maximum_recovery: maximum.result.recovery,
    minimum_case: minimum.input,
    maximum_case: maximum.input,
    evaluated_cases: cases.length(),
  }
}