///|
pub(all) struct LeverResult {
first_fraction : Double
second_fraction : Double
first_amount : Double
second_amount : Double
projection_residual : Double
} derive(Debug, Eq, ToJson)
///|
/// Apply the lever rule by projecting an overall composition onto the line
/// between two phase endpoints. A small residual confirms collinearity.
pub fn lever_rule(
overall : @domain.TernaryComposition,
first : @domain.TernaryComposition,
second : @domain.TernaryComposition,
total_amount? : Double = 1.0,
tolerance? : Double = 1.0e-8,
) -> LeverResult raise @domain.ExtractionError {
@domain.require_positive("total amount", total_amount)
@domain.require_positive("tolerance", tolerance)
let dc = first.carrier - second.carrier
let du = first.solute - second.solute
let ds = first.solvent - second.solvent
let denominator = dc * dc + du * du + ds * ds
if denominator <= tolerance * tolerance {
raise @domain.ExtractionError::SingularBalance(
context="lever-rule endpoints coincide",
)
}
let oc = overall.carrier - second.carrier
let ou = overall.solute - second.solute
let os = overall.solvent - second.solvent
let first_fraction = (oc * dc + ou * du + os * ds) / denominator
if first_fraction < -tolerance || first_fraction > 1.0 + tolerance {
raise @domain.ExtractionError::OutOfRange(
name="lever fraction",
value=first_fraction,
lower=0.0,
upper=1.0,
)
}
let bounded_first = first_fraction.clamp(min=0.0, max=1.0)
let predicted = @domain.TernaryComposition::from_amounts(
carrier=bounded_first * first.carrier +
(1.0 - bounded_first) * second.carrier,
solute=bounded_first * first.solute + (1.0 - bounded_first) * second.solute,
solvent=bounded_first * first.solvent +
(1.0 - bounded_first) * second.solvent,
)
let residual = predicted.distance_squared(overall).sqrt()
if residual > tolerance {
raise @domain.ExtractionError::InvalidArgument(
message="overall composition is not on the phase-endpoint line; residual=\{residual}",
)
}
{
first_fraction: bounded_first,
second_fraction: 1.0 - bounded_first,
first_amount: total_amount * bounded_first,
second_amount: total_amount * (1.0 - bounded_first),
projection_residual: residual,
}
}
///|
pub fn split_on_tie_line(
overall : @domain.TernaryComposition,
tie_line : @domain.TieLine,
total_amount? : Double = 1.0,
tolerance? : Double = 1.0e-8,
) -> @domain.PhaseSplit raise @domain.ExtractionError {
let lever = lever_rule(
overall,
tie_line.raffinate,
tie_line.extract,
total_amount~,
tolerance~,
)
{
raffinate_amount: lever.first_amount,
extract_amount: lever.second_amount,
raffinate: tie_line.raffinate,
extract: tie_line.extract,
residual: lever.projection_residual,
}
}