///|
/// Equipment categories used for preliminary process sizing.
pub(all) enum EquipmentKind {
Mixer
Settler
ExtractorColumn
HeatExchanger
Pump
StorageTank
} derive(Debug, Eq, ToJson)
///|
pub(all) struct EquipmentSpec {
name : String
kind : EquipmentKind
design_flow : Double
design_residence_time : Double
design_pressure : Double
design_temperature : Double
safety_factor : Double
} derive(Debug, Eq, ToJson)
///|
pub fn EquipmentSpec::new(
name~ : String,
kind~ : EquipmentKind,
design_flow~ : Double,
design_residence_time~ : Double,
design_pressure~ : Double,
design_temperature~ : Double,
safety_factor? : Double = 1.15,
) -> EquipmentSpec raise @domain.ExtractionError {
if name.trim().length() == 0 {
raise @domain.ExtractionError::InvalidArgument(
message="equipment name cannot be blank",
)
}
@domain.require_positive("equipment design flow", design_flow)
@domain.require_positive("equipment residence time", design_residence_time)
@domain.require_positive("equipment design pressure", design_pressure)
@domain.require_positive("equipment design temperature", design_temperature)
@domain.require_positive("equipment safety factor", safety_factor)
{
name,
kind,
design_flow,
design_residence_time,
design_pressure,
design_temperature,
safety_factor,
}
}
///|
pub(all) struct EquipmentSizing {
name : String
kind : EquipmentKind
working_volume : Double
design_volume : Double
cross_section_area : Double
diameter : Double
heat_transfer_area : Double
pump_head : Double
warnings : Array[String]
} derive(Debug, Eq, ToJson)
///|
pub fn size_equipment(
spec : EquipmentSpec,
density~ : Double,
velocity~ : Double,
heat_duty~ : Double,
heat_transfer_coefficient~ : Double,
) -> EquipmentSizing raise @domain.ExtractionError {
@domain.require_positive("equipment density", density)
@domain.require_positive("equipment velocity", velocity)
@domain.require_non_negative("equipment heat duty", heat_duty)
@domain.require_positive(
"equipment heat-transfer coefficient", heat_transfer_coefficient,
)
let working_volume = spec.design_flow * spec.design_residence_time
let design_volume = working_volume * spec.safety_factor
let area = spec.design_flow / velocity
let diameter = (4.0 * area / 3.141592653589793).sqrt()
let heat_area = if heat_duty <= 0.0 {
0.0
} else {
heat_duty / heat_transfer_coefficient
}
let head = spec.design_pressure / (density * 9.80665)
let warnings : Array[String] = []
if diameter > 10.0 {
warnings.push("diameter exceeds preliminary equipment range")
}
if spec.design_residence_time < 1.0 {
warnings.push("residence time is below one second")
}
if heat_area > design_volume * 100.0 {
warnings.push("heat-transfer area is large relative to vessel volume")
}
{
name: spec.name,
kind: spec.kind,
working_volume,
design_volume,
cross_section_area: area,
diameter,
heat_transfer_area: heat_area,
pump_head: head,
warnings,
}
}
///|
pub(all) struct SettlerSizing {
volume : Double
interface_area : Double
length : Double
width : Double
height : Double
overflow_rate : Double
} derive(Debug, Eq, ToJson)
///|
pub fn size_settler(
total_flow~ : Double,
residence_time~ : Double,
length_to_width? : Double = 3.0,
width_to_height? : Double = 2.0,
) -> SettlerSizing raise @domain.ExtractionError {
@domain.require_positive("settler flow", total_flow)
@domain.require_positive("settler residence time", residence_time)
@domain.require_positive("settler length-to-width", length_to_width)
@domain.require_positive("settler width-to-height", width_to_height)
let volume = total_flow * residence_time
let height = @math.pow(
volume / (length_to_width * width_to_height),
1.0 / 3.0,
)
let width = height * width_to_height
let length = width * length_to_width
{
volume,
interface_area: length * width,
length,
width,
height,
overflow_rate: total_flow / (length * width),
}
}
///|
pub(all) struct MixerSizing {
volume : Double
power : Double
power_per_volume : Double
tip_speed : Double
torque : Double
} derive(Debug, Eq, ToJson)
///|
pub fn size_mixer(
flow~ : Double,
residence_time~ : Double,
density~ : Double,
power_number~ : Double,
rotation_per_second~ : Double,
impeller_diameter~ : Double,
) -> MixerSizing raise @domain.ExtractionError {
@domain.require_positive("mixer flow", flow)
@domain.require_positive("mixer residence time", residence_time)
@domain.require_positive("mixer density", density)
@domain.require_non_negative("mixer power number", power_number)
@domain.require_positive("mixer rotation", rotation_per_second)
@domain.require_positive("impeller diameter", impeller_diameter)
let volume = flow * residence_time
let power = power_number *
density *
rotation_per_second *
rotation_per_second *
rotation_per_second *
impeller_diameter *
impeller_diameter *
impeller_diameter *
impeller_diameter *
impeller_diameter
let tip = 3.141592653589793 * rotation_per_second * impeller_diameter
{
volume,
power,
power_per_volume: power / volume,
tip_speed: tip,
torque: power / (2.0 * 3.141592653589793 * rotation_per_second),
}
}
///|
pub(all) struct HeatExchangerSizing {
area : Double
log_mean_temperature_difference : Double
duty : Double
hot_outlet_temperature : Double
cold_outlet_temperature : Double
pinch_temperature_difference : Double
} derive(Debug, Eq, ToJson)
///|
pub fn size_heat_exchanger(
duty~ : Double,
overall_coefficient~ : Double,
hot_inlet_temperature~ : Double,
hot_outlet_temperature~ : Double,
cold_inlet_temperature~ : Double,
cold_outlet_temperature~ : Double,
) -> HeatExchangerSizing raise @domain.ExtractionError {
@domain.require_positive("heat exchanger duty", duty)
@domain.require_positive("overall coefficient", overall_coefficient)
let delta_one = hot_inlet_temperature - cold_outlet_temperature
let delta_two = hot_outlet_temperature - cold_inlet_temperature
if delta_one <= 0.0 || delta_two <= 0.0 {
raise @domain.ExtractionError::InvalidArgument(
message="heat exchanger temperature cross",
)
}
let lmtd = if (delta_one - delta_two).abs() <= 1.0e-12 {
delta_one
} else {
(delta_one - delta_two) / approximate_log(delta_one / delta_two)
}
{
area: duty / (overall_coefficient * lmtd),
log_mean_temperature_difference: lmtd,
duty,
hot_outlet_temperature,
cold_outlet_temperature,
pinch_temperature_difference: delta_one.min(delta_two),
}
}
///|
pub(all) struct PumpSizing {
head : Double
hydraulic_power : Double
shaft_power : Double
efficiency : Double
pressure_rise : Double
} derive(Debug, Eq, ToJson)
///|
pub fn size_pump(
flow~ : Double,
pressure_rise~ : Double,
density~ : Double,
efficiency~ : Double,
) -> PumpSizing raise @domain.ExtractionError {
@domain.require_positive("pump flow", flow)
@domain.require_positive("pressure rise", pressure_rise)
@domain.require_positive("pump density", density)
@domain.require_fraction("pump efficiency", efficiency)
if efficiency <= 0.0 {
raise @domain.ExtractionError::InvalidArgument(
message="pump efficiency must be positive",
)
}
let hydraulic = flow * pressure_rise
let shaft = hydraulic / efficiency
{
head: pressure_rise / (density * 9.80665),
hydraulic_power: hydraulic,
shaft_power: shaft,
efficiency,
pressure_rise,
}
}
///|
pub fn equipment_utilization(
actual_flow~ : Double,
design_flow~ : Double,
) -> Double raise @domain.ExtractionError {
@domain.require_non_negative("actual equipment flow", actual_flow)
@domain.require_positive("design equipment flow", design_flow)
actual_flow / design_flow
}
///|
pub fn equipment_margin(
actual_flow~ : Double,
design_flow~ : Double,
) -> Double raise @domain.ExtractionError {
1.0 - equipment_utilization(actual_flow~, design_flow~)
}
///|
pub fn vessel_volume_for_batch(
batch_mass~ : Double,
density~ : @units.Density,
fill_fraction~ : Double,
safety_factor? : Double = 1.15,
) -> @units.Volume raise @domain.ExtractionError {
@domain.require_positive("batch mass", batch_mass)
@domain.require_fraction("fill fraction", fill_fraction)
@domain.require_positive("vessel safety factor", safety_factor)
if fill_fraction <= 0.0 {
raise @domain.ExtractionError::InvalidArgument(
message="fill fraction must be positive",
)
}
@units.Volume::new(
value=batch_mass /
density.mass_kg_per_cubic_meter /
fill_fraction *
safety_factor,
unit=@units.VolumeUnit::CubicMeter,
)
}
///|
fn approximate_log(value : Double) -> Double {
let mut scaled = value
let mut exponent = 0.0
while scaled > 2.0 {
scaled /= 2.0
exponent += 1.0
}
while scaled < 0.5 {
scaled *= 2.0
exponent -= 1.0
}
let z = (scaled - 1.0) / (scaled + 1.0)
let z_squared = z * z
let mut power = z
let mut result = 0.0
for index in 0..<18 {
result += power / (2.0 * index.to_double() + 1.0)
power *= z_squared
}
2.0 * result + exponent * 0.6931471805599453
}