///|
/// Diagnostics for an isothermal flash result.
pub(all) struct FlashDiagnostic {
name : String
value : Double
threshold : Double
passed : Bool
message : String
} derive(Debug, Eq, ToJson)
///|
pub fn flash_diagnostics(
result : FlashResult,
composition_tolerance? : Double = 1.0e-8,
) -> Array[FlashDiagnostic] raise @domain.ExtractionError {
@domain.require_positive("flash composition tolerance", composition_tolerance)
let liquid_total = result.liquid_composition.fold(init=0.0, (sum, value) => {
sum + value
})
let vapor_total = result.vapor_composition.fold(init=0.0, (sum, value) => {
sum + value
})
let feed_total = result.feed_composition.fold(init=0.0, (sum, value) => {
sum + value
})
let diagnostics : Array[FlashDiagnostic] = []
diagnostics.push({
name: "liquid normalization",
value: (liquid_total - 1.0).abs(),
threshold: composition_tolerance,
passed: (liquid_total - 1.0).abs() <= composition_tolerance,
message: "liquid composition sums to one",
})
diagnostics.push({
name: "vapor normalization",
value: (vapor_total - 1.0).abs(),
threshold: composition_tolerance,
passed: (vapor_total - 1.0).abs() <= composition_tolerance,
message: "vapor composition sums to one",
})
diagnostics.push({
name: "feed normalization",
value: (feed_total - 1.0).abs(),
threshold: composition_tolerance,
passed: (feed_total - 1.0).abs() <= composition_tolerance,
message: "feed composition sums to one",
})
diagnostics.push({
name: "Rachford-Rice residual",
value: result.rachford_rice_residual.abs(),
threshold: composition_tolerance,
passed: result.rachford_rice_residual.abs() <= composition_tolerance,
message: "flash equation residual is small",
})
diagnostics
}
///|
pub fn flash_component_recovery(
result : FlashResult,
component : Int,
) -> Double raise @domain.ExtractionError {
if component < 0 || component >= result.feed_composition.length() {
raise @domain.ExtractionError::InvalidArgument(
message="flash component index out of range",
)
}
result.vapor_fraction *
result.vapor_composition[component] /
result.feed_composition[component].max(1.0e-12)
}
///|
pub fn flash_component_split(
result : FlashResult,
component : Int,
) -> Double raise @domain.ExtractionError {
let recovery = flash_component_recovery(result, component)
recovery.max(0.0).min(1.0)
}
///|
pub fn phase_composition_difference(
result : FlashResult,
) -> Double raise @domain.ExtractionError {
if result.liquid_composition.length() != result.vapor_composition.length() {
raise @domain.ExtractionError::InvalidArgument(
message="flash phase dimensions differ",
)
}
let mut sum = 0.0
for index in 0.. FlashSensitivity raise @domain.ExtractionError {
if component < 0 || component >= feed.length() {
raise @domain.ExtractionError::InvalidArgument(
message="flash sensitivity component out of range",
)
}
@domain.require_positive("flash sensitivity step", relative_step)
let base = isothermal_flash(feed_composition=feed, k_values~)
let perturbed_feed = feed.to_owned()
perturbed_feed[component] *= 1.0 + relative_step
let perturbed = isothermal_flash(feed_composition=perturbed_feed, k_values~)
let base_fraction = base.vapor_composition[component]
let perturbed_fraction = perturbed.vapor_composition[component]
let change = perturbed_fraction - base_fraction
{
component,
base_fraction,
perturbed_fraction,
change,
normalized_change: if base_fraction.abs() <= 1.0e-12 {
change
} else {
change / base_fraction
},
}
}
///|
pub(all) struct PhaseEnvelopePoint {
vapor_fraction : Double
liquid_composition : Array[Double]
vapor_composition : Array[Double]
separation : Double
} derive(Debug, Eq, ToJson)
///|
pub fn flash_envelope(
feed : ArrayView[Double],
k_values : ArrayView[Double],
vapor_fractions : ArrayView[Double],
) -> Array[PhaseEnvelopePoint] raise @domain.ExtractionError {
let base = isothermal_flash(feed_composition=feed, k_values~)
[
for fraction in vapor_fractions => {
@domain.require_fraction("envelope vapor fraction", fraction)
{
vapor_fraction: fraction,
liquid_composition: base.liquid_composition.copy(),
vapor_composition: base.vapor_composition.copy(),
separation: phase_composition_difference(base) * fraction,
}
}
]
}
///|
pub fn flash_recovery_vector(
result : FlashResult,
) -> Array[Double] raise @domain.ExtractionError {
[
for index in 0.. {
flash_component_recovery(result, index)
}
]
}
///|
pub fn all_components_recovered_above(
result : FlashResult,
threshold~ : Double,
) -> Bool raise @domain.ExtractionError {
@domain.require_fraction("recovery threshold", threshold)
for index in 0.. Int raise @domain.ExtractionError {
if result.feed_composition.length() == 0 {
raise @domain.ExtractionError::InsufficientData(required=1, actual=0)
}
let recoveries = flash_recovery_vector(result)
let mut index = 0
for cursor in 1..