///|
pub(all) struct StreamStatistics {
stream : String
total_flow : Double
solute_flow : Double
solute_fraction : Double
concentration_ratio : Double
thermal_content : Double
pressure_energy : Double
} derive(Debug, Eq, ToJson)
///|
pub(all) struct StreamComparison {
carrier_flow_change : Double
solvent_flow_change : Double
solute_change : Double
temperature_change : Double
pressure_change : Double
concentration_change : Double
} derive(Debug, Eq, ToJson)
///|
pub fn stream_statistics(
stream : ProcessStream,
heat_capacity : Double,
) -> StreamStatistics {
let total = stream.total_flow()
let solute = stream.total_solute()
{
stream: stream.name,
total_flow: total,
solute_flow: solute,
solute_fraction: if total == 0.0 {
0.0
} else {
solute / total
},
concentration_ratio: stream_concentration_ratio(stream),
thermal_content: total * heat_capacity * stream.temperature.kelvin(),
pressure_energy: total * stream.pressure.pascal(),
}
}
///|
pub fn compare_streams(
first : ProcessStream,
second : ProcessStream,
) -> StreamComparison {
{
carrier_flow_change: second.carrier_flow - first.carrier_flow,
solvent_flow_change: second.solvent_flow - first.solvent_flow,
solute_change: second.total_solute() - first.total_solute(),
temperature_change: second.temperature.kelvin() - first.temperature.kelvin(),
pressure_change: second.pressure.pascal() - first.pressure.pascal(),
concentration_change: second.carrier_concentration -
first.carrier_concentration,
}
}
///|
pub fn stream_change_magnitude(change : StreamComparison) -> Double {
(change.carrier_flow_change * change.carrier_flow_change +
change.solvent_flow_change * change.solvent_flow_change +
change.solute_change * change.solute_change +
change.temperature_change * change.temperature_change +
change.pressure_change * change.pressure_change +
change.concentration_change * change.concentration_change).sqrt()
}
///|
pub fn stream_is_dilute(stream : ProcessStream, threshold : Double) -> Bool {
stream_concentration_ratio(stream) <= threshold
}
///|
pub fn stream_is_rich(stream : ProcessStream, threshold : Double) -> Bool {
stream_concentration_ratio(stream) >= threshold
}
///|
pub fn stream_solvent_fraction(stream : ProcessStream) -> Double {
if stream.total_flow() == 0.0 {
0.0
} else {
stream.solvent_flow / stream.total_flow()
}
}
///|
pub fn stream_carrier_fraction(stream : ProcessStream) -> Double {
if stream.total_flow() == 0.0 {
0.0
} else {
stream.carrier_flow / stream.total_flow()
}
}
///|
pub fn stream_flow_ratio(
first : ProcessStream,
second : ProcessStream,
) -> Double {
if second.total_flow() == 0.0 {
0.0
} else {
first.total_flow() / second.total_flow()
}
}
///|
pub fn stream_solute_ratio(
first : ProcessStream,
second : ProcessStream,
) -> Double {
if second.total_solute() == 0.0 {
0.0
} else {
first.total_solute() / second.total_solute()
}
}
///|
pub fn stream_temperature_ratio(
first : ProcessStream,
second : ProcessStream,
) -> Double {
if second.temperature.kelvin() == 0.0 {
0.0
} else {
first.temperature.kelvin() / second.temperature.kelvin()
}
}
///|
pub fn stream_pressure_ratio(
first : ProcessStream,
second : ProcessStream,
) -> Double {
if second.pressure.pascal() == 0.0 {
0.0
} else {
first.pressure.pascal() / second.pressure.pascal()
}
}
///|
pub fn stream_energy_change(
first : ProcessStream,
second : ProcessStream,
heat_capacity : Double,
) -> Double {
second.total_flow() * heat_capacity * second.temperature.kelvin() -
first.total_flow() * heat_capacity * first.temperature.kelvin()
}
///|
pub fn stream_pressure_work(
first : ProcessStream,
second : ProcessStream,
) -> Double {
second.total_flow() * second.pressure.pascal() -
first.total_flow() * first.pressure.pascal()
}
///|
pub fn stream_quality_index(stream : ProcessStream) -> Double {
let solute_fraction = stream_solute_fraction(stream)
let solvent_fraction = stream_solvent_fraction(stream)
(1.0 - solute_fraction) * 0.5 + solvent_fraction * 0.5
}
///|
pub fn stream_recovery_factor(
feed : ProcessStream,
product : ProcessStream,
) -> Double {
if feed.total_solute() == 0.0 {
0.0
} else {
product.total_solute() / feed.total_solute()
}
}
///|
pub fn stream_loss_factor(
feed : ProcessStream,
waste : ProcessStream,
) -> Double {
1.0 - stream_recovery_factor(feed, waste)
}
///|
pub fn stream_concentration_change(
first : ProcessStream,
second : ProcessStream,
) -> Double {
second.carrier_concentration - first.carrier_concentration
}
///|
pub fn stream_solvent_concentration_change(
first : ProcessStream,
second : ProcessStream,
) -> Double {
second.solvent_concentration - first.solvent_concentration
}
///|
pub fn stream_temperature_change(
first : ProcessStream,
second : ProcessStream,
) -> Double {
second.temperature.kelvin() - first.temperature.kelvin()
}
///|
pub fn stream_pressure_change(
first : ProcessStream,
second : ProcessStream,
) -> Double {
second.pressure.pascal() - first.pressure.pascal()
}
///|
pub fn streams_balanced(
inlet : ProcessStream,
outlet : ProcessStream,
tolerance : Double,
) -> Bool {
compare_streams(inlet, outlet).solute_change.abs() <= tolerance &&
compare_streams(inlet, outlet).carrier_flow_change.abs() <= tolerance &&
compare_streams(inlet, outlet).solvent_flow_change.abs() <= tolerance
}
///|
pub fn stream_summary_line(statistics : StreamStatistics) -> String {
statistics.stream +
": flow=" +
statistics.total_flow.to_string() +
",solute=" +
statistics.solute_flow.to_string() +
",fraction=" +
statistics.solute_fraction.to_string() +
",ratio=" +
statistics.concentration_ratio.to_string()
}
///|
pub fn stream_statistics_csv(
statistics : ArrayView[StreamStatistics],
) -> String {
let lines = [
"stream,total_flow,solute_flow,solute_fraction,concentration_ratio,thermal_content,pressure_energy",
]
for statistic in statistics {
lines.push(
statistic.stream +
"," +
statistic.total_flow.to_string() +
"," +
statistic.solute_flow.to_string() +
"," +
statistic.solute_fraction.to_string() +
"," +
statistic.concentration_ratio.to_string() +
"," +
statistic.thermal_content.to_string() +
"," +
statistic.pressure_energy.to_string(),
)
}
lines.join("\n")
}
///|
pub fn stream_statistics_markdown(
statistics : ArrayView[StreamStatistics],
) -> String {
let lines = [
"| Stream | Total flow | Solute flow | Solute fraction | Concentration ratio | Thermal content | Pressure energy |",
"|---|---:|---:|---:|---:|---:|---:|",
]
for statistic in statistics {
lines.push(
"| " +
statistic.stream +
" | " +
statistic.total_flow.to_string() +
" | " +
statistic.solute_flow.to_string() +
" | " +
statistic.solute_fraction.to_string() +
" | " +
statistic.concentration_ratio.to_string() +
" | " +
statistic.thermal_content.to_string() +
" | " +
statistic.pressure_energy.to_string() +
" |",
)
}
lines.join("\n")
}
///|
pub fn stream_comparison_report(comparison : StreamComparison) -> String {
"carrier_change=" +
comparison.carrier_flow_change.to_string() +
",solvent_change=" +
comparison.solvent_flow_change.to_string() +
",solute_change=" +
comparison.solute_change.to_string() +
",temperature_change=" +
comparison.temperature_change.to_string() +
",pressure_change=" +
comparison.pressure_change.to_string()
}
///|
pub fn statistics_mean(
statistics : ArrayView[StreamStatistics],
) -> StreamStatistics {
if statistics.length() == 0 {
return {
stream: "",
total_flow: 0.0,
solute_flow: 0.0,
solute_fraction: 0.0,
concentration_ratio: 0.0,
thermal_content: 0.0,
pressure_energy: 0.0,
}
}
let mut flow = 0.0
let mut solute = 0.0
let mut fraction = 0.0
let mut ratio = 0.0
let mut thermal = 0.0
let mut pressure = 0.0
for statistic in statistics {
flow += statistic.total_flow
solute += statistic.solute_flow
fraction += statistic.solute_fraction
ratio += statistic.concentration_ratio
thermal += statistic.thermal_content
pressure += statistic.pressure_energy
}
let count = statistics.length().to_double()
{
stream: "mean",
total_flow: flow / count,
solute_flow: solute / count,
solute_fraction: fraction / count,
concentration_ratio: ratio / count,
thermal_content: thermal / count,
pressure_energy: pressure / count,
}
}
///|
pub fn statistics_total_flow(
statistics : ArrayView[StreamStatistics],
) -> Double {
statistics.fold(init=0.0, (sum, statistic) => sum + statistic.total_flow)
}
///|
pub fn statistics_total_solute(
statistics : ArrayView[StreamStatistics],
) -> Double {
statistics.fold(init=0.0, (sum, statistic) => sum + statistic.solute_flow)
}
///|
pub fn statistics_maximum_ratio(
statistics : ArrayView[StreamStatistics],
) -> Double {
let mut result = 0.0
for statistic in statistics {
result = result.max(statistic.concentration_ratio)
}
result
}
///|
pub fn statistics_minimum_ratio(
statistics : ArrayView[StreamStatistics],
) -> Double {
if statistics.length() == 0 {
return 0.0
}
let mut result = statistics[0].concentration_ratio
for statistic in statistics {
result = result.min(statistic.concentration_ratio)
}
result
}
///|
pub fn statistics_ratio_range(
statistics : ArrayView[StreamStatistics],
) -> Double {
statistics_maximum_ratio(statistics) - statistics_minimum_ratio(statistics)
}
///|
pub fn statistics_filter_rich(
statistics : ArrayView[StreamStatistics],
threshold : Double,
) -> Array[StreamStatistics] {
let result = []
for statistic in statistics {
if statistic.concentration_ratio >= threshold {
result.push(statistic)
}
}
result
}
///|
pub fn statistics_filter_dilute(
statistics : ArrayView[StreamStatistics],
threshold : Double,
) -> Array[StreamStatistics] {
let result = []
for statistic in statistics {
if statistic.concentration_ratio <= threshold {
result.push(statistic)
}
}
result
}
///|
pub fn statistics_quality_range(
statistics : ArrayView[StreamStatistics],
) -> Double {
if statistics.length() == 0 {
return 0.0
}
let mut lower = statistics[0].solute_fraction
let mut upper = lower
for statistic in statistics {
lower = lower.min(statistic.solute_fraction)
upper = upper.max(statistic.solute_fraction)
}
upper - lower
}