use crate::core::config::DsfbConfig;
use crate::core::residual::{compute_baseline, compute_residuals, compute_drift, compute_slew, sign_at};
use crate::core::envelope::AdmissibilityEnvelope;
use crate::core::grammar::{GrammarEngine, GrammarState, aggregate_grammar, MAX_CHANNELS};
use crate::core::heuristics::{HeuristicsBank, EngineReasonCode};
use crate::core::regime::OperatingRegime;
use crate::core::episode::Episode;
use crate::core::audit::{AuditEntry, AuditTrail};
use crate::core::theorem::TheoremOneBound;
use crate::core::channels::ChannelId;
const MAX_CYCLES: usize = 512;
#[derive(Debug)]
pub struct EngineEvalResult {
pub unit: u16,
pub total_cycles: u32,
pub grammar_trajectory: Vec<GrammarState>,
pub reason_trajectory: Vec<EngineReasonCode>,
pub first_boundary_cycle: Option<u32>,
pub first_violation_cycle: Option<u32>,
pub structural_lead_time: Option<u32>,
pub episodes: Vec<Episode>,
pub theorem_bound: Option<TheoremOneBound>,
pub channel_boundary_cycles: Vec<(ChannelId, Option<u32>)>,
pub primary_audit: AuditTrail,
}
pub fn evaluate_engine(
unit: u16,
channel_data: &[(ChannelId, Vec<f64>)],
config: &DsfbConfig,
) -> EngineEvalResult {
let total_cycles = channel_data.iter()
.map(|(_, vals)| vals.len())
.max()
.unwrap_or(0) as u32;
let bank = HeuristicsBank::default_gas_turbine();
let regime = OperatingRegime::SeaLevelStatic;
let num_channels = channel_data.len().min(MAX_CHANNELS);
let mut channel_engines: Vec<GrammarEngine> = vec![GrammarEngine::new(); num_channels];
let mut channel_envelopes: Vec<AdmissibilityEnvelope> = Vec::with_capacity(num_channels);
let mut channel_residuals: Vec<Vec<f64>> = Vec::with_capacity(num_channels);
let mut channel_drifts: Vec<Vec<f64>> = Vec::with_capacity(num_channels);
let mut channel_slews: Vec<Vec<f64>> = Vec::with_capacity(num_channels);
let mut channel_boundary_cycles: Vec<(ChannelId, Option<u32>)> = Vec::new();
for (ch_id, values) in channel_data.iter().take(num_channels) {
let n = values.len();
let (mean, std) = compute_baseline(values, config);
let envelope = AdmissibilityEnvelope::from_baseline(mean, std, regime, config);
channel_envelopes.push(envelope);
let mut resid = vec![0.0; n];
compute_residuals(values, mean, &mut resid);
let mut drift = vec![0.0; n];
compute_drift(&resid, config.drift_window, &mut drift);
let mut slew = vec![0.0; n];
compute_slew(&drift, config.slew_window, &mut slew);
channel_residuals.push(resid);
channel_drifts.push(drift);
channel_slews.push(slew);
channel_boundary_cycles.push((*ch_id, None));
}
let n_cycles = total_cycles as usize;
let capacity_hint = n_cycles.min(MAX_CYCLES);
let mut grammar_trajectory = Vec::with_capacity(capacity_hint);
let mut reason_trajectory = Vec::with_capacity(capacity_hint);
let mut primary_audit = AuditTrail::new();
let mut aggregate_first_boundary: Option<u32> = None;
let mut aggregate_first_violation: Option<u32> = None;
for k in 0..n_cycles {
let cycle = (k + 1) as u32;
let mut states_buf = [GrammarState::Admissible; MAX_CHANNELS];
for ch in 0..num_channels {
if k < channel_residuals[ch].len() {
let sign = sign_at(
&channel_residuals[ch],
&channel_drifts[ch],
&channel_slews[ch],
k, 1,
);
channel_engines[ch].advance(&sign, &channel_envelopes[ch], config);
if channel_engines[ch].first_boundary_cycle().is_some()
&& channel_boundary_cycles[ch].1.is_none()
{
channel_boundary_cycles[ch].1 = channel_engines[ch].first_boundary_cycle();
}
}
states_buf[ch] = channel_engines[ch].state();
}
let agg_state = aggregate_grammar(
&states_buf[..num_channels],
config.channel_vote_fraction,
);
let reason = if num_channels > 0 && k < channel_residuals[0].len() {
let env_stressed = channel_envelopes[0].classify_position(
channel_residuals[0][k]
) != crate::core::envelope::EnvelopeStatus::Interior;
bank.match_motif(
channel_drifts[0][k],
channel_slews[0][k],
agg_state,
env_stressed,
)
} else {
EngineReasonCode::NoAnomaly
};
if agg_state.severity() >= GrammarState::Boundary.severity()
&& aggregate_first_boundary.is_none()
{
aggregate_first_boundary = Some(cycle);
}
if agg_state == GrammarState::Violation && aggregate_first_violation.is_none() {
aggregate_first_violation = Some(cycle);
}
if num_channels > 0 && k < channel_residuals[0].len() {
let entry = AuditEntry {
cycle,
residual: channel_residuals[0][k],
drift: channel_drifts[0][k],
slew: channel_slews[0][k],
envelope_position: channel_envelopes[0].normalized_position(
channel_residuals[0][k]
),
envelope_status: channel_envelopes[0].classify_position(
channel_residuals[0][k]
),
grammar_state: agg_state,
reason_code: reason,
drift_persistence: 0, slew_persistence: 0,
};
primary_audit.push(entry);
}
grammar_trajectory.push(agg_state);
reason_trajectory.push(reason);
}
let episodes = form_episodes(
unit,
&grammar_trajectory,
&reason_trajectory,
if !channel_drifts.is_empty() { &channel_drifts[0] } else { &[] },
if !channel_slews.is_empty() { &channel_slews[0] } else { &[] },
);
let structural_lead_time = aggregate_first_boundary.map(|fb| {
if total_cycles > fb { total_cycles - fb } else { 0 }
});
let theorem_bound = if !channel_drifts.is_empty() && !channel_residuals.is_empty() {
compute_theorem_bound(
&channel_residuals[0],
&channel_drifts[0],
&channel_envelopes[0],
config,
aggregate_first_boundary,
)
} else {
None
};
EngineEvalResult {
unit,
total_cycles,
grammar_trajectory,
reason_trajectory,
first_boundary_cycle: aggregate_first_boundary,
first_violation_cycle: aggregate_first_violation,
structural_lead_time,
episodes,
theorem_bound,
channel_boundary_cycles,
primary_audit,
}
}
fn form_episodes(
unit: u16,
grammar: &[GrammarState],
reasons: &[EngineReasonCode],
drifts: &[f64],
slews: &[f64],
) -> Vec<Episode> {
let mut episodes = Vec::new();
let n = grammar.len();
let mut i = 0;
while i < n {
if grammar[i].severity() >= GrammarState::Boundary.severity() {
let start = i;
let mut peak_state = grammar[i];
let mut peak_reason = if i < reasons.len() { reasons[i] } else { EngineReasonCode::NoAnomaly };
let mut max_drift = 0.0f64;
let mut max_slew = 0.0f64;
while i < n && grammar[i].severity() >= GrammarState::Boundary.severity() {
if grammar[i].severity() > peak_state.severity() {
peak_state = grammar[i];
}
if i < reasons.len() && reasons[i].is_anomalous() {
peak_reason = reasons[i];
}
if i < drifts.len() && drifts[i].abs() > max_drift {
max_drift = drifts[i].abs();
}
if i < slews.len() && slews[i].abs() > max_slew {
max_slew = slews[i].abs();
}
i += 1;
}
episodes.push(Episode {
unit,
start_cycle: (start + 1) as u32,
end_cycle: i as u32,
peak_state,
reason_code: peak_reason,
max_drift,
max_slew,
duration_cycles: (i - start) as u32,
trigger_channel: 0,
});
} else {
i += 1;
}
}
episodes
}
fn compute_theorem_bound(
residuals: &[f64],
drifts: &[f64],
envelope: &AdmissibilityEnvelope,
config: &DsfbConfig,
first_boundary: Option<u32>,
) -> Option<TheoremOneBound> {
if residuals.len() < config.healthy_window + config.drift_window {
return None;
}
let start = config.healthy_window;
let mut drift_onset: Option<usize> = None;
let mut consecutive = 0u32;
for k in start..drifts.len() {
if drifts[k].abs() > config.drift_floor {
consecutive += 1;
if consecutive >= config.persistence_threshold as u32 && drift_onset.is_none() {
drift_onset = Some(k - config.persistence_threshold + 1);
}
} else {
consecutive = 0;
}
}
let onset_idx = drift_onset?;
let initial_gap = envelope.gap(residuals[onset_idx]);
let mut drift_sum = 0.0;
let mut drift_count = 0u32;
for k in onset_idx..drifts.len().min(onset_idx + config.persistence_threshold * 2) {
if drifts[k].abs() > config.drift_floor {
drift_sum += drifts[k].abs();
drift_count += 1;
}
}
let avg_drift = if drift_count > 0 { drift_sum / drift_count as f64 } else { 0.0 };
Some(TheoremOneBound::compute(
initial_gap.max(0.0),
avg_drift,
0.0, first_boundary,
Some((onset_idx + 1) as u32),
))
}