///|
pub struct InventoryItem {
component : String
flow : Float
unit : MeasureUnit
} derive(Debug, Eq)
///|
pub fn inventory_item(
component : String,
flow : Float,
unit : MeasureUnit,
) -> InventoryItem {
{ component, flow, unit }
}
///|
pub fn InventoryItem::is_valid(self : InventoryItem) -> Bool {
self.component.trim().length() > 0 && self.flow >= 0.0
}
///|
pub fn InventoryItem::as_kg_per_hour(self : InventoryItem) -> Float {
match quantity(self.flow, self.unit).convert_to(KgPerHour) {
Ok(value) => value
Err(_) => 0.0
}
}
///|
pub struct StreamInventory {
name : String
items : Array[InventoryItem]
} derive(Debug, Eq)
///|
pub fn stream_inventory(
name : String,
items : Array[InventoryItem],
) -> StreamInventory {
{ name, items }
}
///|
pub fn StreamInventory::total_flow(self : StreamInventory) -> Float {
let mut total : Float = 0.0
for item in self.items {
total = total + item.flow
}
total
}
///|
pub fn StreamInventory::component_flow(
self : StreamInventory,
component : String,
) -> Float {
let mut total : Float = 0.0
for item in self.items {
if item.component == component {
total = total + item.flow
}
}
total
}
///|
pub fn StreamInventory::component_names(
self : StreamInventory,
) -> Array[String] {
let names : Array[String] = []
for item in self.items {
if !names.contains(item.component) {
names.push(item.component)
}
}
names
}
///|
pub fn StreamInventory::fraction(
self : StreamInventory,
component : String,
) -> Float {
let total = self.total_flow()
if total <= 0.0 {
0.0
} else {
self.component_flow(component) / total
}
}
///|
pub fn StreamInventory::is_empty(self : StreamInventory) -> Bool {
self.items.length() == 0 || self.total_flow() == 0.0
}
///|
pub fn StreamInventory::with_item(
self : StreamInventory,
item : InventoryItem,
) -> StreamInventory {
let items = self.items.copy()
items.push(item)
{ ..self, items, }
}
///|
pub fn StreamInventory::without_component(
self : StreamInventory,
component : String,
) -> StreamInventory {
let items : Array[InventoryItem] = []
for item in self.items {
if item.component != component {
items.push(item)
}
}
{ ..self, items, }
}
///|
fn inventory_component_index(
items : Array[InventoryItem],
component : String,
unit : MeasureUnit,
) -> Int? {
for index, item in items {
if item.component == component && item.unit.dimension() == unit.dimension() {
return Some(index)
}
}
None
}
///|
fn add_inventory_item(
items : Array[InventoryItem],
item : InventoryItem,
) -> Unit {
match inventory_component_index(items, item.component, item.unit) {
Some(index) =>
items[index] = { ..items[index], flow: items[index].flow + item.flow }
None => items.push(item)
}
}
///|
pub fn mix_streams(streams : Array[StreamInventory]) -> StreamInventory {
let items : Array[InventoryItem] = []
for source in streams {
for item in source.items {
add_inventory_item(items, item)
}
}
stream_inventory("mixed", items)
}
///|
pub fn split_stream(
source : StreamInventory,
fractions : Array[Float],
) -> Array[StreamInventory] {
let result : Array[StreamInventory] = []
for _, fraction in fractions {
let items : Array[InventoryItem] = []
let safe_fraction = clamp(fraction, 0.0, 1.0)
for item in source.items {
items.push({ ..item, flow: item.flow * safe_fraction })
}
result.push(stream_inventory("{source.name}-{index + 1}", items))
}
result
}
///|
pub fn split_fractions_are_closed(
fractions : Array[Float],
tolerance? : Float = 0.000001,
) -> Bool {
let mut total : Float = 0.0
for fraction in fractions {
total = total + fraction
}
(total - 1.0).abs() <= tolerance
}
///|
pub fn StreamInventory::normalize(
self : StreamInventory,
target : MeasureUnit,
) -> Result[StreamInventory, QuantityError] {
let items : Array[InventoryItem] = []
for item in self.items {
match quantity(item.flow, item.unit).convert_to(target) {
Ok(value) => items.push({ ..item, flow: value, unit: target })
Err(error) => return Err(error)
}
}
Ok({ ..self, items, })
}
///|
pub struct StreamDifference {
component : String
left : Float
right : Float
difference : Float
relative_difference : Float
} derive(Debug, Eq)
///|
pub fn compare_streams(
left : StreamInventory,
right : StreamInventory,
) -> Array[StreamDifference] {
let result : Array[StreamDifference] = []
let names = left.component_names()
for name in right.component_names() {
if !names.contains(name) {
names.push(name)
}
}
for component in names {
let left_value = left.component_flow(component)
let right_value = right.component_flow(component)
let denominator = if left_value.abs() > right_value.abs() {
left_value.abs()
} else {
right_value.abs()
}
let relative : Float = if denominator == 0.0 {
0.0
} else {
(left_value - right_value).abs() / denominator
}
result.push({
component,
left: left_value,
right: right_value,
difference: left_value - right_value,
relative_difference: relative,
})
}
result
}
///|
pub fn stream_difference_table(
differences : Array[StreamDifference],
) -> ReportTable {
let rows : Array[Array[String]] = []
for item in differences {
rows.push([
item.component,
"{item.left}",
"{item.right}",
"{item.difference}",
"{item.relative_difference}",
])
}
table(["component", "left", "right", "difference", "relative"], rows)
}
///|
pub fn stream_balance_error(
left : StreamInventory,
right : StreamInventory,
) -> Float {
let differences = compare_streams(left, right)
let mut total : Float = 0.0
for item in differences {
total = total + item.difference.abs()
}
total
}
///|
pub fn StreamInventory::mass_fraction(
self : StreamInventory,
component : String,
) -> Float {
self.fraction(component)
}
///|
pub fn StreamInventory::component_count(self : StreamInventory) -> Int {
self.component_names().length()
}
///|
pub fn StreamInventory::largest_component(self : StreamInventory) -> String? {
if self.items.length() == 0 {
None
} else {
let mut best = self.items[0]
for item in self.items[1:] {
if item.flow > best.flow {
best = item
}
}
Some(best.component)
}
}
///|
pub fn StreamInventory::smallest_component(self : StreamInventory) -> String? {
if self.items.length() == 0 {
None
} else {
let mut best = self.items[0]
for item in self.items[1:] {
if item.flow < best.flow {
best = item
}
}
Some(best.component)
}
}
///|
pub fn StreamInventory::to_table(self : StreamInventory) -> ReportTable {
let rows : Array[Array[String]] = []
for item in self.items {
rows.push([
item.component,
"{item.flow}",
item.unit.symbol(),
"{self.fraction(item.component)}",
])
}
table(["component", "flow", "unit", "fraction"], rows)
}
///|
pub fn StreamInventory::to_markdown(self : StreamInventory) -> String {
self.to_table().to_markdown()
}
///|
pub fn StreamInventory::to_json(self : StreamInventory) -> String {
let values : Array[String] = []
for item in self.items {
values.push(
"{\"component\":\{json_string(item.component)},\"flow\":\{item.flow},\"unit\":\{json_string(item.unit.symbol())}}",
)
}
"{\"name\":{json_string(self.name)},\"items\":[" + values.join(",") + "]}"
}
///|
pub fn combine_by_component(
streams : Array[StreamInventory],
component : String,
) -> Float {
let mut total : Float = 0.0
for item in streams {
total = total + item.component_flow(component)
}
total
}
///|
pub fn component_recovery(
feed : StreamInventory,
product : StreamInventory,
component : String,
) -> Float {
recovery(feed.component_flow(component), product.component_flow(component))
}
///|
pub fn component_loss(
feed : StreamInventory,
product : StreamInventory,
component : String,
) -> Float {
loss_fraction(
feed.component_flow(component),
product.component_flow(component),
)
}
///|
pub fn stream_yield(feed : StreamInventory, product : StreamInventory) -> Float {
recovery(feed.total_flow(), product.total_flow())
}
///|
pub fn stream_selectivity(
product : StreamInventory,
target : String,
byproduct : String,
) -> Float {
let denominator = product.component_flow(byproduct)
if denominator == 0.0 {
product.component_flow(target)
} else {
product.component_flow(target) / denominator
}
}