///|
/// Aggregate metrics for comparing cascade designs.
pub(all) struct CascadeMetrics {
stage_count : Int
total_feed_solute : Double
extracted_solute : Double
raffinate_solute : Double
recovery : Double
average_stage_recovery : Double
maximum_stage_transfer : Double
minimum_stage_transfer : Double
transfer_uniformity : Double
balance_residual : Double
} derive(Debug, Eq, ToJson)
///|
pub fn cascade_metrics(
result : CountercurrentResult,
) -> CascadeMetrics raise @domain.ExtractionError {
if result.stages.length() == 0 {
raise @domain.ExtractionError::InsufficientData(required=1, actual=0)
}
let first = result.stages[0]
let last = result.stages[result.stages.length() - 1]
let feed_solute = first.raffinate_solute_flow
let extracted = feed_solute - last.raffinate_solute_flow
let mut maximum = 0.0
let mut minimum = 1.0e300
let mut sum = 0.0
for stage in result.stages {
let transfer = stage.extract_solute_flow - stage.raffinate_solute_flow
let positive = transfer.abs()
maximum = maximum.max(positive)
minimum = minimum.min(positive)
sum += positive
}
let average = sum / result.stages.length().to_double()
{
stage_count: result.stages.length(),
total_feed_solute: feed_solute,
extracted_solute: extracted,
raffinate_solute: last.raffinate_solute_flow,
recovery: result.recovery,
average_stage_recovery: if feed_solute <= 0.0 {
0.0
} else {
average / feed_solute
},
maximum_stage_transfer: maximum,
minimum_stage_transfer: minimum,
transfer_uniformity: if maximum <= 1.0e-12 {
1.0
} else {
minimum / maximum
},
balance_residual: result.balance_residual,
}
}
///|
pub fn nonlinear_cascade_metrics(
result : NonlinearCountercurrentResult,
) -> CascadeMetrics raise @domain.ExtractionError {
if result.stages.length() == 0 {
raise @domain.ExtractionError::InsufficientData(required=1, actual=0)
}
let first = result.stages[0]
let last = result.stages[result.stages.length() - 1]
let feed_solute = first.raffinate_solute_flow
let extracted = feed_solute - last.raffinate_solute_flow
let transfers = [
for stage in result.stages => {
(stage.extract_solute_flow - stage.raffinate_solute_flow).abs()
}
]
let maximum = transfers.fold(init=0.0, (current, value) => current.max(value))
let minimum = transfers.fold(init=1.0e300, (current, value) => {
current.min(value)
})
{
stage_count: result.stages.length(),
total_feed_solute: feed_solute,
extracted_solute: extracted,
raffinate_solute: last.raffinate_solute_flow,
recovery: result.recovery,
average_stage_recovery: result.recovery / result.stages.length().to_double(),
maximum_stage_transfer: maximum,
minimum_stage_transfer: minimum,
transfer_uniformity: if maximum <= 1.0e-12 {
1.0
} else {
minimum / maximum
},
balance_residual: result.balance_residual,
}
}
///|
pub fn compare_metrics(
first : CascadeMetrics,
second : CascadeMetrics,
) -> Array[String] {
let changes = []
changes.push("recovery_delta=\{second.recovery - first.recovery}")
changes.push(
"solvent_transfer_delta=\{second.extracted_solute - first.extracted_solute}",
)
changes.push(
"uniformity_delta=\{second.transfer_uniformity - first.transfer_uniformity}",
)
changes.push(
"residual_delta=\{second.balance_residual - first.balance_residual}",
)
changes
}
///|
pub(all) struct StageRanking {
stage : Int
score : Double
transfer : Double
rank : Int
} derive(Debug, Eq, ToJson)
///|
pub fn rank_cascade_stages(
result : CountercurrentResult,
) -> Array[StageRanking] raise @domain.ExtractionError {
let metrics = cascade_metrics(result)
let raw = [
for index, stage in result.stages => {
let transfer = (stage.extract_solute_flow - stage.raffinate_solute_flow).abs()
{
stage: index + 1,
score: if metrics.maximum_stage_transfer <= 0.0 {
0.0
} else {
transfer / metrics.maximum_stage_transfer
},
transfer,
rank: 0,
}
}
]
let ranked : Array[StageRanking] = []
for item in raw {
let rank = 1 +
raw.fold(init=0, (count, other) => {
if other.score > item.score {
count + 1
} else {
count
}
})
ranked.push({
stage: item.stage,
score: item.score,
transfer: item.transfer,
rank,
})
}
ranked
}
///|
pub fn cascade_is_balanced(
result : CountercurrentResult,
tolerance~ : Double,
) -> Bool raise @domain.ExtractionError {
@domain.require_positive("cascade balance tolerance", tolerance)
result.balance_residual.abs() <= tolerance
}