#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use crate::monitor::{classify_alarm, AlarmReport, HybridMonitor, Leg};
pub const RECAL_WINDOW: usize = 400;
pub const GUARD_WINDOW: usize = 300;
pub const RECAL_COOLDOWN: u64 = 4000;
#[derive(Debug, Clone)]
pub enum Event {
Alarm { tick: u64, report: AlarmReport, class: &'static str },
Quarantined { tick: u64, channel: usize },
AdaptationStarted { tick: u64 },
Recalibrated { tick: u64 },
RolledBack { tick: u64, guard_report: AlarmReport },
}
enum Mode {
Monitoring,
Collecting { buffer: Vec<Vec<f64>>, target: usize },
Guarding { candidate: HybridMonitor, fed: usize },
}
pub struct AutoPilot {
monitor: HybridMonitor,
mode: Mode,
channels: usize,
tick: u64,
quarantined: Vec<bool>,
last_recal: Option<(u64, usize)>,
adapting_channel: usize,
drift_latch: Vec<Option<u64>>,
}
pub const DRIFT_LATCH_DECAY: u64 = 2000;
impl AutoPilot {
#[must_use]
pub fn new(monitor: HybridMonitor) -> AutoPilot {
let channels = monitor.channels();
let mut q = Vec::with_capacity(channels);
q.resize(channels, false);
AutoPilot {
monitor,
mode: Mode::Monitoring,
channels,
tick: 0,
quarantined: q,
last_recal: None,
adapting_channel: 0,
drift_latch: {
let mut d = Vec::with_capacity(channels);
d.resize(channels, None);
d
},
}
}
#[must_use]
pub fn monitor(&self) -> &HybridMonitor {
&self.monitor
}
pub fn push(&mut self, sample: &[f64], valid: &[bool]) -> Vec<Event> {
let mut events = Vec::new();
let tick = self.tick;
self.tick += 1;
match &mut self.mode {
Mode::Monitoring => {
if let Some(_leg) = self.monitor.push_with_validity(sample, valid) {
if let Some(report) = self.monitor.last_alarm() {
let class = classify_alarm(&report);
match report.leg {
Leg::RepeatedValue | Leg::Missingness => {
self.monitor.reset();
self.monitor.quarantine(report.channel);
self.quarantined[report.channel] = true;
events.push(Event::Alarm { tick, report, class });
events.push(Event::Quarantined {
tick,
channel: report.channel,
});
}
Leg::Parity if class == "cross_channel_inconsistency" => {
self.monitor.reset();
self.monitor.quarantine(report.channel);
self.quarantined[report.channel] = true;
events.push(Event::Alarm { tick, report, class });
events.push(Event::Quarantined {
tick,
channel: report.channel,
});
}
Leg::LevelShift => {
self.monitor.reset();
let ch = report.channel;
if matches!(
self.drift_latch[ch],
Some(t0) if tick - t0 < DRIFT_LATCH_DECAY
) {
self.drift_latch[ch] = Some(tick);
return events;
}
self.drift_latch[ch] = None;
let repeat_trend = matches!(
self.last_recal,
Some((rt, rch)) if rch == ch
&& tick - rt < RECAL_COOLDOWN
);
if repeat_trend {
self.drift_latch[ch] = Some(tick);
events.push(Event::Alarm {
tick,
report,
class: "drift_confirmed",
});
} else {
events.push(Event::Alarm { tick, report, class });
events.push(Event::AdaptationStarted { tick });
self.adapting_channel = report.channel;
let mut buffer = Vec::with_capacity(self.channels);
for _ in 0..self.channels {
buffer.push(Vec::with_capacity(RECAL_WINDOW));
}
self.mode =
Mode::Collecting { buffer, target: RECAL_WINDOW };
}
}
_ => {
self.monitor.reset();
events.push(Event::Alarm { tick, report, class });
}
}
} else {
self.monitor.reset();
}
}
}
Mode::Collecting { buffer, target } => {
for ch in 0..self.channels {
let v = if self.quarantined[ch] || !valid.get(ch).copied().unwrap_or(true)
{
self.monitor.virtual_value(ch).map(|(x, _)| x).unwrap_or(sample[ch])
} else {
sample[ch]
};
buffer[ch].push(v);
let _ = self.monitor.push_channel(ch, Some(v));
}
self.monitor.reset();
if buffer[0].len() >= *target {
match HybridMonitor::calibrate(buffer) {
Some(mut candidate) => {
for ch in 0..self.channels {
if self.quarantined[ch] {
candidate.quarantine(ch);
}
}
self.mode = Mode::Guarding { candidate, fed: 0 };
}
None => {
self.mode = Mode::Monitoring;
}
}
}
}
Mode::Guarding { candidate, fed } => {
*fed += 1;
if candidate.push_with_validity(sample, valid).is_some() {
let guard_report = candidate.last_alarm().unwrap_or(AlarmReport {
leg: Leg::LevelShift,
channel: 0,
tick,
observed: 0.0,
threshold: 0.0,
hit_gap: 0,
});
events.push(Event::RolledBack { tick, guard_report });
self.mode = Mode::Monitoring;
} else if *fed >= GUARD_WINDOW {
let mut accepted = match core::mem::replace(&mut self.mode, Mode::Monitoring)
{
Mode::Guarding { candidate, .. } => candidate,
_ => unreachable!(),
};
core::mem::swap(&mut self.monitor, &mut accepted);
self.last_recal = Some((tick, self.adapting_channel));
events.push(Event::Recalibrated { tick });
}
}
}
events
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::telemetry_bench::synth_spacecraft;
#[test]
fn autonomous_gauntlet() {
let n = 24_000usize;
let calib = synth_spacecraft(2048, 31_337 + 100);
let stream = synth_spacecraft(n, 31_337 + 200);
let mon = HybridMonitor::calibrate(&calib).expect("calibration");
let mut ap = AutoPilot::new(mon);
let mut quarantine_at = None;
let mut recal_at = None;
let mut drift_alarm_at = None;
let mut recal_count = 0usize;
let valid = [true; 6];
let mut sample = [0.0f64; 6];
let ch_sd: Vec<f64> = (0..6)
.map(|ch| {
let c = &calib[ch];
let m = c.iter().sum::<f64>() / c.len() as f64;
(c.iter().map(|x| (x - m) * (x - m)).sum::<f64>() / c.len() as f64).sqrt()
})
.collect();
for t in 0..n {
for ch in 0..6 {
let mut v = stream[ch][t];
if ch == 2 && t >= 4000 {
v = stream[2][4000];
}
if t >= 10_000 {
v += 0.8 * ch_sd[ch];
}
if ch == 0 && t >= 18_000 {
v += (t - 18_000) as f64 * 0.0005 * ch_sd[0];
}
sample[ch] = v;
}
for ev in ap.push(&sample, &valid) {
match ev {
Event::Quarantined { tick, channel }
if channel == 2 && quarantine_at.is_none() => {
quarantine_at = Some(tick);
}
Event::Recalibrated { tick } => {
recal_count += 1;
if recal_at.is_none() {
recal_at = Some(tick);
}
}
Event::Alarm { tick, report, class }
if tick > 17_000
&& report.channel == 0
&& class == "drift_confirmed"
&& drift_alarm_at.is_none()
=> {
drift_alarm_at = Some(tick);
}
_ => {}
}
}
}
let q = quarantine_at.expect("dead temp sensor must be auto-quarantined");
assert!((4000..6000).contains(&(q as usize)), "quarantine at {}", q);
let r = recal_at.expect("regime change must trigger accepted recalibration");
assert!((10_000..13_000).contains(&(r as usize)), "recal at {}", r);
let d = drift_alarm_at.expect("drift in the NEW regime must be CONFIRMED, not adapted into");
assert!((18_000..24_000).contains(&(d as usize)), "drift alarm at {}", d);
assert!(
recal_count <= 2,
"autopilot chased the drift: {} accepted recalibrations",
recal_count
);
}
#[test]
fn unstable_regime_rolls_back() {
let n = 16_000usize;
let calib = synth_spacecraft(2048, 777 + 100);
let stream = synth_spacecraft(n, 777 + 200);
let mon = HybridMonitor::calibrate(&calib).expect("calibration");
let mut ap = AutoPilot::new(mon);
let ch_sd: f64 = {
let c = &calib[5];
let m = c.iter().sum::<f64>() / c.len() as f64;
(c.iter().map(|x| (x - m) * (x - m)).sum::<f64>() / c.len() as f64).sqrt()
};
let valid = [true; 6];
let mut sample = [0.0f64; 6];
let mut rolled_back = false;
for t in 0..n {
for ch in 0..6 {
let mut v = stream[ch][t];
if ch == 5 && t >= 6000 {
let dt = (t - 6000) as f64;
v += ch_sd * (0.8 + dt * dt * 2e-7);
}
sample[ch] = v;
}
for ev in ap.push(&sample, &valid) {
match ev {
Event::RolledBack { .. } => rolled_back = true,
Event::Alarm { report, class, .. }
if report.channel == 5 && class == "drift_confirmed" =>
{
rolled_back = true;
}
_ => {}
}
}
}
assert!(rolled_back, "runaway disguised as regime change must be refused");
}
}