///|
/// Preliminary utility and capacity sizing helpers for early process design.
///
/// These calculations intentionally expose assumptions and margins instead of
/// hiding them in a single black-box equipment factor.
pub(all) struct UtilityLoad {
name : String
design_load : Double
operating_load : Double
margin : Double
unit : String
} derive(Debug, Eq, ToJson)
///|
pub(all) struct CapacityCheck {
name : String
required : Double
available : Double
utilization : Double
margin : Double
feasible : Bool
} derive(Debug, Eq, ToJson)
///|
pub(all) struct StorageSizing {
working_volume : Double
surge_volume : Double
total_volume : Double
diameter : Double
height : Double
aspect_ratio : Double
} derive(Debug, Eq, ToJson)
///|
pub(all) enum UtilityKind {
CoolingWater
ChilledWater
Steam
Electricity
CompressedAir
Nitrogen
} derive(Debug, Eq)
///|
pub fn utility_name(kind : UtilityKind) -> String {
match kind {
CoolingWater => "cooling-water"
ChilledWater => "chilled-water"
Steam => "steam"
Electricity => "electricity"
CompressedAir => "compressed-air"
Nitrogen => "nitrogen"
}
}
///|
pub fn sensible_heat_duty(
mass_flow : Double,
heat_capacity : Double,
inlet_temperature : Double,
outlet_temperature : Double,
) -> Double raise @domain.ExtractionError {
if mass_flow < 0.0 {
raise @domain.ExtractionError::InvalidFlow(
name="mass flow",
value=mass_flow,
)
}
if heat_capacity < 0.0 {
raise @domain.ExtractionError::InvalidCoefficient(
name="heat capacity",
value=heat_capacity,
)
}
mass_flow * heat_capacity * (outlet_temperature - inlet_temperature)
}
///|
pub fn latent_heat_duty(
mass_flow : Double,
latent_heat : Double,
) -> Double raise @domain.ExtractionError {
if mass_flow < 0.0 {
raise @domain.ExtractionError::InvalidFlow(
name="mass flow",
value=mass_flow,
)
}
if latent_heat < 0.0 {
raise @domain.ExtractionError::InvalidCoefficient(
name="latent heat",
value=latent_heat,
)
}
mass_flow * latent_heat
}
///|
pub fn cooling_water_flow(
duty : Double,
inlet_temperature : Double,
outlet_temperature : Double,
heat_capacity? : Double = 4.18,
) -> Double raise @domain.ExtractionError {
let rise = outlet_temperature - inlet_temperature
if duty < 0.0 || heat_capacity <= 0.0 || rise <= 0.0 {
raise @domain.ExtractionError::InvalidArgument(
message="cooling-water duty, heat capacity, and temperature rise must be positive",
)
}
duty / (heat_capacity * rise)
}
///|
pub fn chilled_water_flow(
duty : Double,
inlet_temperature : Double,
outlet_temperature : Double,
heat_capacity? : Double = 4.18,
) -> Double raise @domain.ExtractionError {
cooling_water_flow(
duty,
inlet_temperature,
outlet_temperature,
heat_capacity~,
)
}
///|
pub fn steam_flow(
duty : Double,
latent_heat : Double,
condensate_subcooling? : Double = 0.0,
heat_capacity? : Double = 4.18,
) -> Double raise @domain.ExtractionError {
if duty < 0.0 || latent_heat <= 0.0 || condensate_subcooling < 0.0 {
raise @domain.ExtractionError::InvalidArgument(
message="steam duty and latent heat are invalid",
)
}
let usable = latent_heat + heat_capacity * condensate_subcooling
duty / usable
}
///|
pub fn pump_power(
volumetric_flow : Double,
pressure_rise : Double,
efficiency : Double,
) -> Double raise @domain.ExtractionError {
if volumetric_flow < 0.0 {
raise @domain.ExtractionError::InvalidFlow(
name="volumetric flow",
value=volumetric_flow,
)
}
if pressure_rise < 0.0 {
raise @domain.ExtractionError::InvalidCoefficient(
name="pressure rise",
value=pressure_rise,
)
}
if efficiency <= 0.0 || efficiency > 1.0 {
raise @domain.ExtractionError::InvalidFraction(
name="pump efficiency",
value=efficiency,
)
}
volumetric_flow * pressure_rise / efficiency
}
///|
pub fn fan_power(
volumetric_flow : Double,
pressure_rise : Double,
efficiency : Double,
) -> Double raise @domain.ExtractionError {
pump_power(volumetric_flow, pressure_rise, efficiency)
}
///|
pub fn compressor_power(
molar_flow : Double,
inlet_temperature : Double,
pressure_ratio : Double,
heat_capacity_ratio : Double,
efficiency : Double,
) -> Double raise @domain.ExtractionError {
if molar_flow < 0.0 || inlet_temperature <= 0.0 || pressure_ratio < 1.0 {
raise @domain.ExtractionError::InvalidArgument(
message="compressor input is invalid",
)
}
if heat_capacity_ratio <= 1.0 {
raise @domain.ExtractionError::InvalidCoefficient(
name="heat capacity ratio",
value=heat_capacity_ratio,
)
}
if efficiency <= 0.0 || efficiency > 1.0 {
raise @domain.ExtractionError::InvalidFraction(
name="compressor efficiency",
value=efficiency,
)
}
let exponent = (heat_capacity_ratio - 1.0) / heat_capacity_ratio
molar_flow *
inlet_temperature *
exponent *
(@math.pow(pressure_ratio, exponent) - 1.0) /
efficiency
}
///|
pub fn design_load(
operating_load : Double,
design_margin? : Double = 0.15,
) -> Double raise @domain.ExtractionError {
if operating_load < 0.0 {
raise @domain.ExtractionError::InvalidArgument(
message="operating load cannot be negative",
)
}
if design_margin < 0.0 || design_margin > 2.0 {
raise @domain.ExtractionError::OutOfRange(
name="design margin",
value=design_margin,
lower=0.0,
upper=2.0,
)
}
operating_load * (1.0 + design_margin)
}
///|
pub fn make_utility_load(
name : String,
operating_load : Double,
margin? : Double = 0.15,
unit? : String = "kW",
) -> UtilityLoad raise @domain.ExtractionError {
let load = design_load(operating_load, design_margin=margin)
{ name, design_load: load, operating_load, margin, unit }
}
///|
pub fn check_capacity(
name : String,
required : Double,
available : Double,
design_margin? : Double = 0.0,
) -> CapacityCheck raise @domain.ExtractionError {
if required < 0.0 || available < 0.0 {
raise @domain.ExtractionError::InvalidArgument(
message="capacity cannot be negative",
)
}
let adjusted = design_load(required, design_margin~)
let utilization = if available == 0.0 { 0.0 } else { adjusted / available }
{
name,
required: adjusted,
available,
utilization,
margin: available - adjusted,
feasible: adjusted <= available,
}
}
///|
pub fn storage_volume(
flow_rate : Double,
residence_time : Double,
surge_fraction? : Double = 0.20,
) -> Double raise @domain.ExtractionError {
if flow_rate < 0.0 || residence_time < 0.0 {
raise @domain.ExtractionError::InvalidArgument(
message="flow and residence time are invalid",
)
}
if surge_fraction < 0.0 || surge_fraction > 5.0 {
raise @domain.ExtractionError::InvalidFraction(
name="surge fraction",
value=surge_fraction,
)
}
flow_rate * residence_time * (1.0 + surge_fraction)
}
///|
pub fn size_vertical_tank(
working_volume : Double,
surge_fraction? : Double = 0.20,
aspect_ratio? : Double = 2.5,
) -> StorageSizing raise @domain.ExtractionError {
if working_volume <= 0.0 || aspect_ratio <= 0.0 {
raise @domain.ExtractionError::InvalidArgument(
message="tank dimensions are invalid",
)
}
let total = working_volume * (1.0 + surge_fraction)
let radius = @math.pow(total / (aspect_ratio * 3.141592653589793), 1.0 / 3.0)
let diameter = 2.0 * radius
let height = aspect_ratio * diameter
{
working_volume,
surge_volume: total - working_volume,
total_volume: total,
diameter,
height,
aspect_ratio,
}
}
///|
pub fn size_horizontal_tank(
working_volume : Double,
surge_fraction? : Double = 0.20,
length_to_diameter? : Double = 3.0,
) -> StorageSizing raise @domain.ExtractionError {
if working_volume <= 0.0 || length_to_diameter <= 0.0 {
raise @domain.ExtractionError::InvalidArgument(
message="tank dimensions are invalid",
)
}
let total = working_volume * (1.0 + surge_fraction)
let diameter = @math.pow(
4.0 * total / (3.141592653589793 * length_to_diameter),
1.0 / 3.0,
)
let length = length_to_diameter * diameter
{
working_volume,
surge_volume: total - working_volume,
total_volume: total,
diameter,
height: length,
aspect_ratio: length_to_diameter,
}
}
///|
pub fn pipe_area(diameter : Double) -> Double raise @domain.ExtractionError {
if diameter <= 0.0 {
raise @domain.ExtractionError::InvalidArgument(
message="pipe diameter must be positive",
)
}
3.141592653589793 * diameter * diameter / 4.0
}
///|
pub fn pipe_velocity(
volumetric_flow : Double,
diameter : Double,
) -> Double raise @domain.ExtractionError {
if volumetric_flow < 0.0 {
raise @domain.ExtractionError::InvalidFlow(
name="volumetric flow",
value=volumetric_flow,
)
}
volumetric_flow / pipe_area(diameter)
}
///|
pub fn pipe_diameter_for_velocity(
volumetric_flow : Double,
target_velocity : Double,
) -> Double raise @domain.ExtractionError {
if volumetric_flow < 0.0 || target_velocity <= 0.0 {
raise @domain.ExtractionError::InvalidArgument(
message="flow and velocity are invalid",
)
}
@math.pow(4.0 * volumetric_flow / (3.141592653589793 * target_velocity), 0.5)
}
///|
pub fn pipe_pressure_drop(
length : Double,
diameter : Double,
density : Double,
velocity : Double,
friction_factor : Double,
) -> Double raise @domain.ExtractionError {
if length < 0.0 || density < 0.0 || velocity < 0.0 {
raise @domain.ExtractionError::InvalidArgument(
message="pipe pressure inputs are invalid",
)
}
if friction_factor < 0.0 {
raise @domain.ExtractionError::InvalidCoefficient(
name="friction factor",
value=friction_factor,
)
}
friction_factor * length / diameter * density * velocity * velocity / 2.0
}
///|
pub fn parallel_capacity(
capacities : ArrayView[Double],
) -> Double raise @domain.ExtractionError {
if capacities.length() == 0 {
raise @domain.ExtractionError::InsufficientData(required=1, actual=0)
}
let mut total = 0.0
for capacity in capacities {
if capacity < 0.0 {
raise @domain.ExtractionError::InvalidArgument(
message="capacity cannot be negative",
)
}
total += capacity
}
total
}
///|
pub fn series_resistance(
resistances : ArrayView[Double],
) -> Double raise @domain.ExtractionError {
if resistances.length() == 0 {
raise @domain.ExtractionError::InsufficientData(required=1, actual=0)
}
let mut total = 0.0
for resistance in resistances {
if resistance < 0.0 {
raise @domain.ExtractionError::InvalidArgument(
message="resistance cannot be negative",
)
}
total += resistance
}
total
}
///|
pub fn utility_loads_total(loads : ArrayView[UtilityLoad]) -> Double {
loads.fold(init=0.0, (sum, load) => sum + load.design_load)
}
///|
pub fn capacity_checks_feasible(checks : ArrayView[CapacityCheck]) -> Bool {
for check in checks {
if !check.feasible {
return false
}
}
true
}
///|
pub fn capacity_checks_worst_utilization(
checks : ArrayView[CapacityCheck],
) -> Double {
let mut worst = 0.0
for check in checks {
if check.utilization > worst {
worst = check.utilization
}
}
worst
}
///|
pub fn utility_loads_markdown(loads : ArrayView[UtilityLoad]) -> String {
let lines = [
"| Utility | Operating | Design | Margin | Unit |", "|---|---:|---:|---:|---|",
]
for load in loads {
lines.push(
"| " +
load.name +
" | " +
load.operating_load.to_string() +
" | " +
load.design_load.to_string() +
" | " +
load.margin.to_string() +
" | " +
load.unit +
" |",
)
}
lines.join("\n")
}
///|
pub fn capacity_checks_markdown(checks : ArrayView[CapacityCheck]) -> String {
let lines = [
"| Capacity | Required | Available | Utilization | Feasible |", "|---|---:|---:|---:|---|",
]
for check in checks {
lines.push(
"| " +
check.name +
" | " +
check.required.to_string() +
" | " +
check.available.to_string() +
" | " +
check.utilization.to_string() +
" | " +
check.feasible.to_string() +
" |",
)
}
lines.join("\n")
}
///|
pub fn thermal_load_split(
total_duty : Double,
hot_fraction : Double,
) -> (Double, Double) raise @domain.ExtractionError {
if total_duty < 0.0 || hot_fraction < 0.0 || hot_fraction > 1.0 {
raise @domain.ExtractionError::InvalidArgument(
message="thermal split is invalid",
)
}
(total_duty * hot_fraction, total_duty * (1.0 - hot_fraction))
}
///|
pub fn annualized_energy(
power : Double,
operating_hours : Double,
load_factor : Double,
) -> Double raise @domain.ExtractionError {
if power < 0.0 ||
operating_hours < 0.0 ||
load_factor < 0.0 ||
load_factor > 1.0 {
raise @domain.ExtractionError::InvalidArgument(
message="annual energy inputs are invalid",
)
}
power * operating_hours * load_factor
}
///|
pub fn carbon_emissions(
energy : Double,
emission_factor : Double,
) -> Double raise @domain.ExtractionError {
if energy < 0.0 || emission_factor < 0.0 {
raise @domain.ExtractionError::InvalidArgument(
message="emission inputs are invalid",
)
}
energy * emission_factor
}
///|
pub fn utility_cost(
consumption : Double,
unit_price : Double,
) -> Double raise @domain.ExtractionError {
if consumption < 0.0 || unit_price < 0.0 {
raise @domain.ExtractionError::InvalidArgument(
message="utility cost inputs are invalid",
)
}
consumption * unit_price
}