use crate::pipeline::engine_eval::EngineEvalResult;
use crate::core::channels::ChannelId;
use crate::core::grammar::GrammarState;
use std::fmt::Write;
#[derive(Debug)]
pub struct DiscriminationResult {
pub total_engines: usize,
pub hpc_primary_count: usize,
pub fan_primary_count: usize,
pub ambiguous_count: usize,
pub hpc_violation_count: usize,
pub fan_violation_count: usize,
pub ambiguous_violation_count: usize,
pub hpc_mean_lead: f64,
pub fan_mean_lead: f64,
pub classifications: Vec<(u16, FaultCluster)>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FaultCluster {
HpcPrimary,
FanPrimary,
Ambiguous,
}
const HPC_CHANNELS: &[ChannelId] = &[
ChannelId::TempHpcOutlet, ChannelId::PressureHpcOutlet, ChannelId::StaticPressureHpc, ChannelId::FuelFlowRatio, ];
const FAN_CHANNELS: &[ChannelId] = &[
ChannelId::FanSpeed, ChannelId::CorrectedFanSpeed, ChannelId::BypassRatio, ];
fn classify_engine(result: &EngineEvalResult) -> FaultCluster {
let mut earliest_hpc: Option<u32> = None;
let mut earliest_fan: Option<u32> = None;
for (ch, fb) in &result.channel_boundary_cycles {
if let Some(cycle) = fb {
let is_hpc = HPC_CHANNELS.contains(ch);
let is_fan = FAN_CHANNELS.contains(ch);
if is_hpc {
earliest_hpc = Some(earliest_hpc.map_or(*cycle, |c: u32| c.min(*cycle)));
}
if is_fan {
earliest_fan = Some(earliest_fan.map_or(*cycle, |c: u32| c.min(*cycle)));
}
}
}
match (earliest_hpc, earliest_fan) {
(Some(h), Some(f)) => {
if h < f { FaultCluster::HpcPrimary }
else if f < h { FaultCluster::FanPrimary }
else { FaultCluster::Ambiguous }
}
(Some(_), None) => FaultCluster::HpcPrimary,
(None, Some(_)) => FaultCluster::FanPrimary,
(None, None) => FaultCluster::Ambiguous,
}
}
fn peak_grammar_state(result: &EngineEvalResult) -> GrammarState {
let mut peak = GrammarState::Admissible;
for state in &result.grammar_trajectory {
if state.severity() > peak.severity() {
peak = *state;
}
}
peak
}
pub fn analyze_discrimination(results: &[EngineEvalResult]) -> DiscriminationResult {
let mut classifications = Vec::with_capacity(results.len());
let mut hpc_count = 0usize;
let mut fan_count = 0usize;
let mut ambiguous_count = 0usize;
let mut hpc_violation_count = 0usize;
let mut fan_violation_count = 0usize;
let mut ambiguous_violation_count = 0usize;
let mut hpc_lead_sum = 0.0f64;
let mut hpc_lead_n = 0usize;
let mut fan_lead_sum = 0.0f64;
let mut fan_lead_n = 0usize;
for result in results {
let cluster = classify_engine(result);
let peak_state = peak_grammar_state(result);
classifications.push((result.unit, cluster));
match cluster {
FaultCluster::HpcPrimary => {
hpc_count += 1;
if peak_state == GrammarState::Violation {
hpc_violation_count += 1;
}
if let Some(lt) = result.structural_lead_time {
hpc_lead_sum += lt as f64;
hpc_lead_n += 1;
}
}
FaultCluster::FanPrimary => {
fan_count += 1;
if peak_state == GrammarState::Violation {
fan_violation_count += 1;
}
if let Some(lt) = result.structural_lead_time {
fan_lead_sum += lt as f64;
fan_lead_n += 1;
}
}
FaultCluster::Ambiguous => {
ambiguous_count += 1;
if peak_state == GrammarState::Violation {
ambiguous_violation_count += 1;
}
}
}
}
DiscriminationResult {
total_engines: results.len(),
hpc_primary_count: hpc_count,
fan_primary_count: fan_count,
ambiguous_count,
hpc_violation_count,
fan_violation_count,
ambiguous_violation_count,
hpc_mean_lead: if hpc_lead_n > 0 { hpc_lead_sum / hpc_lead_n as f64 } else { 0.0 },
fan_mean_lead: if fan_lead_n > 0 { fan_lead_sum / fan_lead_n as f64 } else { 0.0 },
classifications,
}
}
pub fn discrimination_report(dr: &DiscriminationResult) -> String {
let mut out = String::with_capacity(4096);
let _ = writeln!(out, "── P1: FD003 Multi-Fault Discrimination Analysis ─────────────");
let _ = writeln!(out, " FD003 contains TWO fault modes: HPC degradation + Fan degradation.");
let _ = writeln!(out, " DSFB classifies engines by which channel group triggers Boundary first:");
let _ = writeln!(out, " HPC indicators: T30, P30, Ps30, phi");
let _ = writeln!(out, " Fan indicators: Nf, NRf, BPR");
let _ = writeln!(out);
let _ = writeln!(out, " Total engines: {}", dr.total_engines);
let _ = writeln!(out, " HPC-primary: {} ({:.1}%)", dr.hpc_primary_count,
100.0 * dr.hpc_primary_count as f64 / dr.total_engines.max(1) as f64);
let _ = writeln!(out, " Fan-primary: {} ({:.1}%)", dr.fan_primary_count,
100.0 * dr.fan_primary_count as f64 / dr.total_engines.max(1) as f64);
let _ = writeln!(out, " Ambiguous: {} ({:.1}%)", dr.ambiguous_count,
100.0 * dr.ambiguous_count as f64 / dr.total_engines.max(1) as f64);
let _ = writeln!(out);
let _ = writeln!(out, " Mean lead time (HPC-primary): {:.1} cycles", dr.hpc_mean_lead);
let _ = writeln!(out, " Mean lead time (Fan-primary): {:.1} cycles", dr.fan_mean_lead);
let _ = writeln!(out);
let _ = writeln!(out, " Aggregate trajectory severity summary:");
let _ = writeln!(out, " HPC-primary reaching Violation: {}/{}",
dr.hpc_violation_count, dr.hpc_primary_count);
let _ = writeln!(out, " Fan-primary reaching Violation: {}/{}",
dr.fan_violation_count, dr.fan_primary_count);
let _ = writeln!(out, " Ambiguous reaching Violation: {}/{}",
dr.ambiguous_violation_count, dr.ambiguous_count);
let _ = writeln!(out);
let _ = writeln!(out, " Interpretation:");
let _ = writeln!(out, " DSFB does NOT diagnose fault mode. It observes which residual");
let _ = writeln!(out, " channels show structural deviation first. The fact that different");
let _ = writeln!(out, " engines trigger different channel groups first is consistent with");
let _ = writeln!(out, " the presence of two distinct degradation mechanisms in FD003.");
let _ = writeln!(out, " This structural discrimination is information that scalar");
let _ = writeln!(out, " RUL-threshold alarms do not provide.");
let _ = writeln!(out);
let _ = writeln!(out, " Non-claim: This analysis does not prove fault-mode identification.");
let _ = writeln!(out, " It shows that DSFB grammar trajectories carry structural information");
let _ = writeln!(out, " that is consistent with the known fault-mode diversity of FD003.");
let _ = writeln!(out);
let _ = writeln!(out, " Per-engine classifications (first 20):");
for (unit, cluster) in dr.classifications.iter().take(20) {
let label = match cluster {
FaultCluster::HpcPrimary => "HPC-primary",
FaultCluster::FanPrimary => "Fan-primary",
FaultCluster::Ambiguous => "Ambiguous",
};
let _ = writeln!(out, " Unit {:3}: {}", unit, label);
}
out
}