///|
pub(all) struct LeachingStage {
stage : Int
wash_solution : Double
inlet_concentration : Double
outlet_concentration : Double
overflow_solution : Double
overflow_solute : Double
solute_remaining : Double
} derive(Debug, Eq, ToJson)
///|
pub(all) struct CrosscurrentLeachingResult {
stages : Array[LeachingStage]
final_underflow_concentration : Double
final_solute_remaining : Double
recovered_solute : Double
recovery : Double
balance_residual : Double
} derive(Debug, Eq, ToJson)
///|
/// Repeatedly wash a retained solution with fresh or specified wash streams.
pub fn crosscurrent_leach(
dry_solid_mass~ : Double,
initial_solution_mass~ : Double,
initial_solute_fraction~ : Double,
wash_masses : ArrayView[Double],
wash_solute_fractions? : Array[Double] = [],
retention_ratio~ : Double,
) -> CrosscurrentLeachingResult raise @domain.ExtractionError {
@domain.require_positive("dry solid mass", dry_solid_mass)
@domain.require_positive("initial solution mass", initial_solution_mass)
@domain.require_fraction("initial solute fraction", initial_solute_fraction)
@domain.require_non_negative("retention ratio", retention_ratio)
if wash_masses.length() == 0 {
raise @domain.ExtractionError::InsufficientData(required=1, actual=0)
}
if wash_solute_fractions.length() != 0 &&
wash_solute_fractions.length() != wash_masses.length() {
raise @domain.ExtractionError::InvalidArgument(
message="wash fractions must be empty or match wash masses",
)
}
let initial_solute = initial_solution_mass * initial_solute_fraction
let stages : Array[LeachingStage] = []
let mut retained_solution = initial_solution_mass
let mut concentration = initial_solute_fraction
let mut recovered = 0.0
let mut wash_inlet_solute = 0.0
for index, wash_mass in wash_masses {
let wash_fraction = if wash_solute_fractions.length() == 0 {
0.0
} else {
wash_solute_fractions[index]
}
let result = single_stage_leach(
dry_solid_mass~,
initial_solution_mass=retained_solution,
initial_solute_fraction=concentration,
wash_solution_mass=wash_mass,
wash_solute_fraction=wash_fraction,
retention_ratio~,
)
wash_inlet_solute += wash_mass * wash_fraction
recovered += result.solute_in_overflow
stages.push({
stage: index + 1,
wash_solution: wash_mass,
inlet_concentration: concentration,
outlet_concentration: result.equilibrium_concentration,
overflow_solution: result.overflow_solution,
overflow_solute: result.solute_in_overflow,
solute_remaining: result.solute_in_underflow,
})
retained_solution = result.retained_solution
concentration = result.equilibrium_concentration
}
let remaining = retained_solution * concentration
let feed_basis_recovered = initial_solute - remaining
{
stages,
final_underflow_concentration: concentration,
final_solute_remaining: remaining,
recovered_solute: feed_basis_recovered,
recovery: if initial_solute > 0.0 {
feed_basis_recovered / initial_solute
} else {
0.0
},
balance_residual: initial_solute + wash_inlet_solute - recovered - remaining,
}
}
///|
pub fn crosscurrent_equal_wash(
dry_solid_mass~ : Double,
initial_solution_mass~ : Double,
initial_solute_fraction~ : Double,
total_wash_mass~ : Double,
stage_count~ : Int,
retention_ratio~ : Double,
) -> CrosscurrentLeachingResult raise @domain.ExtractionError {
@domain.require_positive("total wash mass", total_wash_mass)
if stage_count <= 0 {
raise @domain.ExtractionError::InvalidArgument(
message="stage_count must be positive",
)
}
crosscurrent_leach(
dry_solid_mass~,
initial_solution_mass~,
initial_solute_fraction~,
Array::make(stage_count, total_wash_mass / stage_count.to_double()),
retention_ratio~,
)
}
///|
/// Evaluate a grid of stage counts and select the least wash meeting a target.
pub fn wash_for_target(
dry_solid_mass~ : Double,
initial_solution_mass~ : Double,
initial_solute_fraction~ : Double,
stage_count~ : Int,
retention_ratio~ : Double,
target_remaining_fraction~ : Double,
tolerance? : Double = 1.0e-8,
max_wash_mass? : Double = 1.0e9,
) -> CrosscurrentLeachingResult raise @domain.ExtractionError {
@domain.require_fraction(
"target remaining fraction", target_remaining_fraction,
)
@domain.require_positive("tolerance", tolerance)
@domain.require_positive("max wash mass", max_wash_mass)
if target_remaining_fraction >= 1.0 {
raise @domain.ExtractionError::InvalidArgument(
message="target remaining fraction must be below one",
)
}
let target_solute = initial_solution_mass *
initial_solute_fraction *
target_remaining_fraction
let mut lower = 0.0
let mut upper = 1.0
let mut upper_result = crosscurrent_equal_wash(
dry_solid_mass~,
initial_solution_mass~,
initial_solute_fraction~,
total_wash_mass=upper,
stage_count~,
retention_ratio~,
)
for ;; {
if upper_result.final_solute_remaining <= target_solute {
break
}
upper *= 2.0
if upper > max_wash_mass {
raise @domain.ExtractionError::ConvergenceFailure(
iterations=0,
residual=upper_result.final_solute_remaining - target_solute,
)
}
upper_result = crosscurrent_equal_wash(
dry_solid_mass~,
initial_solution_mass~,
initial_solute_fraction~,
total_wash_mass=upper,
stage_count~,
retention_ratio~,
)
}
for _ in 0..<100 {
let middle = lower + (upper - lower) / 2.0
let result = crosscurrent_equal_wash(
dry_solid_mass~,
initial_solution_mass~,
initial_solute_fraction~,
total_wash_mass=middle,
stage_count~,
retention_ratio~,
)
if result.final_solute_remaining <= target_solute {
upper = middle
upper_result = result
} else {
lower = middle
}
if upper - lower <= tolerance {
break
}
}
upper_result
}