use serde::{Deserialize, Serialize};
pub const STEAM_EXPANSION_FACTOR: u32 = 1700;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ConformancePhase {
Frozen,
Liquid,
Vapor,
Unsettled,
Decomposed,
}
impl ConformancePhase {
pub fn as_status(self) -> &'static str {
match self {
Self::Frozen => "BLOCKED",
Self::Liquid => "PARTIAL",
Self::Vapor => "ADMITTED",
Self::Unsettled => "UNKNOWN",
Self::Decomposed => "REFUSED",
}
}
pub fn expansion_factor(self) -> u32 {
match self {
Self::Vapor => STEAM_EXPANSION_FACTOR,
Self::Liquid => 1,
Self::Frozen | Self::Unsettled | Self::Decomposed => 0,
}
}
}
impl std::fmt::Display for ConformancePhase {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_status())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PhaseInput {
pub andon_active: bool,
pub refused: bool,
pub unknown: bool,
pub conformance: f64,
pub boiling_point: f64,
}
pub fn phase_for(input: &PhaseInput) -> ConformancePhase {
if input.andon_active {
return ConformancePhase::Frozen;
}
if input.refused {
return ConformancePhase::Decomposed;
}
if input.unknown {
return ConformancePhase::Unsettled;
}
if input.conformance >= input.boiling_point {
ConformancePhase::Vapor
} else {
ConformancePhase::Liquid
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PhaseShiftReport {
pub phase: String,
pub expansion_factor: u32,
pub conformance: f64,
pub boiling_point: f64,
pub crossed_boiling_point: bool,
pub summary: String,
}
pub fn phase_shift_report(input: &PhaseInput) -> PhaseShiftReport {
let phase = phase_for(input);
let crossed = phase == ConformancePhase::Vapor;
let summary = match phase {
ConformancePhase::Frozen => {
"ANDON active; phase Frozen — status BLOCKED, no observation propagates".to_string()
}
ConformancePhase::Decomposed => {
"explicit refusal; phase Decomposed — status REFUSED, no observation propagates"
.to_string()
}
ConformancePhase::Unsettled => {
"measurement undetermined; phase Unsettled — status UNKNOWN, never coerced to PARTIAL \
or ADMITTED"
.to_string()
}
ConformancePhase::Liquid => format!(
"conformance {:.3} below boiling point {:.3}; phase Liquid — status PARTIAL",
input.conformance, input.boiling_point
),
ConformancePhase::Vapor => format!(
"conformance {:.3} at or above boiling point {:.3}; phase Vapor — status ADMITTED, mesh \
expansion {}x",
input.conformance, input.boiling_point, STEAM_EXPANSION_FACTOR
),
};
PhaseShiftReport {
phase: phase.as_status().to_string(),
expansion_factor: phase.expansion_factor(),
conformance: input.conformance,
boiling_point: input.boiling_point,
crossed_boiling_point: crossed,
summary,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn input(andon: bool, refused: bool, unknown: bool, c: f64, bp: f64) -> PhaseInput {
PhaseInput {
andon_active: andon,
refused,
unknown,
conformance: c,
boiling_point: bp,
}
}
#[test]
fn precedence_blocked_over_refused_over_unknown() {
assert_eq!(
phase_for(&input(true, true, true, 0.9, 0.5)),
ConformancePhase::Frozen
);
assert_eq!(
phase_for(&input(false, true, true, 0.9, 0.5)),
ConformancePhase::Decomposed
);
assert_eq!(
phase_for(&input(false, false, true, 0.9, 0.5)),
ConformancePhase::Unsettled
);
}
#[test]
fn unknown_never_collapses_into_liquid_or_vapor() {
let p = phase_for(&input(false, false, true, 1.0, 0.0));
assert_eq!(p, ConformancePhase::Unsettled);
assert_ne!(p, ConformancePhase::Liquid);
assert_ne!(p, ConformancePhase::Vapor);
assert_eq!(p.as_status(), "UNKNOWN");
}
#[test]
fn boiling_point_boundary_is_admission() {
assert_eq!(
phase_for(&input(false, false, false, 0.7, 0.7)),
ConformancePhase::Vapor
);
assert_eq!(
phase_for(&input(false, false, false, 0.699, 0.7)),
ConformancePhase::Liquid
);
}
#[test]
fn vapor_expands_seventeen_hundred_x() {
assert_eq!(ConformancePhase::Vapor.expansion_factor(), 1700);
assert_eq!(ConformancePhase::Liquid.expansion_factor(), 1);
assert_eq!(ConformancePhase::Frozen.expansion_factor(), 0);
assert_eq!(ConformancePhase::Unsettled.expansion_factor(), 0);
assert_eq!(ConformancePhase::Decomposed.expansion_factor(), 0);
}
#[test]
fn report_marks_crossing_only_for_vapor() {
let admitted = phase_shift_report(&input(false, false, false, 0.8, 0.7));
assert!(admitted.crossed_boiling_point);
assert_eq!(admitted.phase, "ADMITTED");
assert_eq!(admitted.expansion_factor, 1700);
let partial = phase_shift_report(&input(false, false, false, 0.5, 0.7));
assert!(!partial.crossed_boiling_point);
assert_eq!(partial.phase, "PARTIAL");
}
#[test]
fn all_phases_map_to_bounded_statuses() {
for p in [
ConformancePhase::Frozen,
ConformancePhase::Liquid,
ConformancePhase::Vapor,
ConformancePhase::Unsettled,
ConformancePhase::Decomposed,
] {
assert!(
["BLOCKED", "PARTIAL", "ADMITTED", "UNKNOWN", "REFUSED"].contains(&p.as_status()),
"{p:?} mapped to a non-bounded status"
);
}
}
}