///|
/// Calculate the distribution coefficient `K_D = y / x`.
pub fn distribution_coefficient(
  raffinate_concentration : Double,
  extract_concentration : Double,
) -> Double raise @domain.ExtractionError {
  @domain.require_positive("raffinate concentration", raffinate_concentration)
  @domain.require_non_negative("extract concentration", extract_concentration)
  extract_concentration / raffinate_concentration
}

///|
/// Calculate separation selectivity `(y_a/x_a) / (y_b/x_b)`.
pub fn selectivity(
  solute_a_raffinate~ : Double,
  solute_a_extract~ : Double,
  solute_b_raffinate~ : Double,
  solute_b_extract~ : Double,
) -> Double raise @domain.ExtractionError {
  let k_a = distribution_coefficient(solute_a_raffinate, solute_a_extract)
  let k_b = distribution_coefficient(solute_b_raffinate, solute_b_extract)
  if k_b <= 0.0 {
    raise @domain.ExtractionError::SingularBalance(
      context="reference solute has zero distribution coefficient",
    )
  }
  k_a / k_b
}

///|
pub fn extraction_factor(
  distribution~ : Double,
  solvent_flow~ : Double,
  carrier_flow~ : Double,
) -> Double raise @domain.ExtractionError {
  @domain.require_non_negative("distribution coefficient", distribution)
  @domain.require_positive("solvent flow", solvent_flow)
  @domain.require_positive("carrier flow", carrier_flow)
  distribution * solvent_flow / carrier_flow
}

///|
pub fn recovery_fraction(
  inlet_solute : Double,
  raffinate_solute : Double,
) -> Double raise @domain.ExtractionError {
  @domain.require_positive("inlet solute", inlet_solute)
  @domain.require_non_negative("raffinate solute", raffinate_solute)
  if raffinate_solute > inlet_solute {
    raise @domain.ExtractionError::InvalidArgument(
      message="raffinate solute cannot exceed inlet solute",
    )
  }
  (inlet_solute - raffinate_solute) / inlet_solute
}