///|
/// Batch conversion at a sequence of times.
pub fn batch_conversion_curve(
reaction : Reaction,
feed : Feed,
times : ArrayView[Double],
) -> Array[Double] {
times.map(fn(time) { batch_conversion_isothermal(reaction, feed, time) })
}
///|
/// Batch concentration at a sequence of times.
pub fn batch_concentration_curve(
reaction : Reaction,
feed : Feed,
times : ArrayView[Double],
) -> Array[Double] {
batch_conversion_curve(reaction, feed, times).map(fn(conversion) {
concentration_from_conversion(feed, conversion)
})
}
///|
/// Batch rate at a sequence of times.
pub fn batch_rate_curve(
reaction : Reaction,
feed : Feed,
times : ArrayView[Double],
) -> Array[Double] {
batch_concentration_curve(reaction, feed, times).map(fn(concentration) {
reaction.rate(concentration, feed.temperature)
})
}
///|
/// Find the first time at which a batch conversion target is reached.
pub fn batch_time_for_target(
reaction : Reaction,
feed : Feed,
target : Double,
maximum_time : Double,
points : Int,
) -> Double? {
let times = linspace(0.0, maximum_time.max(0.0), points.max(1))
for time in times {
if batch_conversion_isothermal(reaction, feed, time) >=
clamp_conversion(target) {
return Some(time)
}
}
None
}
///|
/// Average batch conversion over a time window.
pub fn average_batch_conversion(
reaction : Reaction,
feed : Feed,
duration : Double,
points : Int,
) -> Double {
let values = batch_conversion_curve(
reaction,
feed,
linspace(0.0, duration.max(0.0), points.max(2)),
)
if values.length() == 0 {
0.0
} else {
values.fold(init=0.0, fn(acc, value) { acc + value }) /
Double::from_int(values.length())
}
}
///|
/// Batch productivity at a selected cycle time.
pub fn batch_productivity(
feed : Feed,
conversion : Double,
cycle_time : Double,
cleaning_time : Double,
) -> Double {
feed.concentration.max(0.0) *
clamp_conversion(conversion) /
(cycle_time.max(0.0) + cleaning_time.max(0.0)).max(1.0e-12)
}
///|
/// Optimize batch cycle time on a regular grid.
pub fn optimize_batch_cycle(
reaction : Reaction,
feed : Feed,
maximum_time : Double,
cleaning_time : Double,
points : Int,
) -> ObjectiveScore? {
let mut best : ObjectiveScore? = None
for time in linspace(0.0, maximum_time.max(0.0), points.max(1)) {
let point = design_batch(reaction, feed, time)
let score = batch_productivity(feed, point.conversion, time, cleaning_time)
let candidate : ObjectiveScore = {
volume: time,
conversion: point.conversion,
temperature: point.outlet_temperature,
score,
feasible: true,
}
match best {
None => best = Some(candidate)
Some(current) => if score > current.score { best = Some(candidate) }
}
}
best
}
///|
/// Batch conversion gain between two times.
pub fn batch_conversion_gain(
reaction : Reaction,
feed : Feed,
first_time : Double,
second_time : Double,
) -> Double {
batch_conversion_isothermal(reaction, feed, second_time) -
batch_conversion_isothermal(reaction, feed, first_time)
}
///|
/// Check monotonicity of a batch curve.
pub fn batch_curve_is_monotone(values : ArrayView[Double]) -> Bool {
for i = 1; i < values.length(); i = i + 1 {
if values[i] + 1.0e-10 < values[i - 1] {
return false
}
}
true
}
///|
/// CSTR conversion over a sequence of volumes.
pub fn cstr_conversion_curve(
reaction : Reaction,
feed : Feed,
volumes : ArrayView[Double],
) -> Array[Double] {
volumes.map(fn(volume) { cstr_conversion_isothermal(reaction, feed, volume) })
}
///|
/// PFR conversion over a sequence of volumes.
pub fn pfr_conversion_curve(
reaction : Reaction,
feed : Feed,
volumes : ArrayView[Double],
) -> Array[Double] {
volumes.map(fn(volume) { pfr_conversion_isothermal(reaction, feed, volume) })
}
///|
/// Compare CSTR and PFR curves at equal volumes.
pub fn compare_reactor_curves(
reaction : Reaction,
feed : Feed,
volumes : ArrayView[Double],
) -> Array[(Double, Double, Double)] {
let cstr = cstr_conversion_curve(reaction, feed, volumes)
let pfr = pfr_conversion_curve(reaction, feed, volumes)
let result : Array[(Double, Double, Double)] = []
for i in 0.. Double {
if curve.length() == 0 {
0.0
} else {
curve.fold(init=0.0, fn(acc, point) { acc + point.2 - point.1 }) /
Double::from_int(curve.length())
}
}
///|
/// Select the smallest volume meeting a target on a curve.
pub fn curve_volume_for_target(
curve : ArrayView[(Double, Double, Double)],
target : Double,
use_pfr : Bool,
) -> Double? {
for point in curve {
if (if use_pfr { point.2 } else { point.1 }) >= clamp_conversion(target) {
return Some(point.0)
}
}
None
}
///|
/// Integrate a conversion curve with respect to volume.
pub fn conversion_volume_area(
curve : ArrayView[(Double, Double, Double)],
use_pfr : Bool,
) -> Double {
if curve.length() < 2 {
0.0
} else {
for i = 1, area = 0.0; i < curve.length(); i = i + 1 {
let left = if use_pfr { curve[i - 1].2 } else { curve[i - 1].1 }
let right = if use_pfr { curve[i].2 } else { curve[i].1 }
continue i + 1,
area + (curve[i].0 - curve[i - 1].0) * (left + right) / 2.0
} nobreak {
area
}
}
}
///|
/// A conservative batch design window.
pub fn batch_design_window(
reaction : Reaction,
feed : Feed,
minimum_conversion : Double,
maximum_temperature : Double,
maximum_time : Double,
points : Int,
) -> Array[EnvelopePoint] {
let result : Array[EnvelopePoint] = []
for time in linspace(0.0, maximum_time.max(0.0), points.max(1)) {
let point = design_batch(reaction, feed, time)
let conversion_ok = point.conversion >= clamp_conversion(minimum_conversion)
let temperature_ok = point.outlet_temperature <= maximum_temperature
result.push({
volume: time * feed.volumetric_flow,
conversion: point.conversion,
temperature: point.outlet_temperature,
feasible: conversion_ok && temperature_ok,
reason: if conversion_ok && temperature_ok {
"feasible"
} else {
"outside limits"
},
})
}
result
}