use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use thiserror::Error;
#[cfg(feature = "structural")]
use ruvector_mincut::{SubpolyConfig, SubpolynomialMinCut};
#[derive(Error, Debug)]
pub enum FilterError {
#[error("Invalid threshold: {0}")]
InvalidThreshold(String),
#[error("Invalid system state: {0}")]
InvalidState(String),
#[error("Structural filter error: {0}")]
StructuralError(String),
#[error("Shift filter error: {0}")]
ShiftError(String),
#[error("Evidence filter error: {0}")]
EvidenceError(String),
}
pub type Result<T> = std::result::Result<T, FilterError>;
pub type EdgeId = u64;
pub type VertexId = u64;
pub type Weight = f64;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct RegionMask(pub u64);
impl RegionMask {
pub fn empty() -> Self {
Self(0)
}
pub fn all() -> Self {
Self(u64::MAX)
}
pub fn set(&mut self, region: u8) {
self.0 |= 1u64 << region;
}
pub fn clear(&mut self, region: u8) {
self.0 &= !(1u64 << region);
}
pub fn is_set(&self, region: u8) -> bool {
(self.0 & (1u64 << region)) != 0
}
pub fn count(&self) -> u32 {
self.0.count_ones()
}
pub fn any(&self) -> bool {
self.0 != 0
}
pub fn union(&self, other: &Self) -> Self {
Self(self.0 | other.0)
}
pub fn intersection(&self, other: &Self) -> Self {
Self(self.0 & other.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Verdict {
Permit,
Deny,
Defer,
}
#[derive(Debug, Clone)]
pub struct SystemState {
pub num_vertices: usize,
pub adjacency: HashMap<VertexId, Vec<(VertexId, Weight)>>,
pub syndromes: Vec<f64>,
pub syndrome_history: Vec<Vec<f64>>,
pub nonconformity_scores: Vec<f64>,
pub vertex_regions: HashMap<VertexId, u8>,
pub cycle: u64,
}
impl SystemState {
pub fn new(num_vertices: usize) -> Self {
Self {
num_vertices,
adjacency: HashMap::new(),
syndromes: Vec::new(),
syndrome_history: Vec::new(),
nonconformity_scores: Vec::new(),
vertex_regions: HashMap::new(),
cycle: 0,
}
}
pub fn add_edge(&mut self, u: VertexId, v: VertexId, weight: Weight) {
self.adjacency.entry(u).or_default().push((v, weight));
self.adjacency.entry(v).or_default().push((u, weight));
}
pub fn add_syndrome(&mut self, syndrome: f64) {
self.syndromes.push(syndrome);
}
pub fn advance_cycle(&mut self) {
if !self.syndromes.is_empty() {
self.syndrome_history.push(self.syndromes.clone());
self.syndromes.clear();
}
self.cycle += 1;
}
pub fn set_nonconformity(&mut self, region: usize, score: f64) {
if self.nonconformity_scores.len() <= region {
self.nonconformity_scores.resize(region + 1, 0.0);
}
self.nonconformity_scores[region] = score;
}
pub fn assign_region(&mut self, vertex: VertexId, region: u8) {
self.vertex_regions.insert(vertex, region);
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StructuralConfig {
pub threshold: f64,
pub max_cut_size: u64,
pub use_subpolynomial: bool,
pub phi: f64,
}
impl Default for StructuralConfig {
fn default() -> Self {
Self {
threshold: 2.0,
max_cut_size: 1000,
use_subpolynomial: true,
phi: 0.01,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StructuralResult {
pub cut_value: f64,
pub boundary_edges: Vec<EdgeId>,
pub is_coherent: bool,
pub healthy_vertices: Option<Vec<VertexId>>,
pub unhealthy_vertices: Option<Vec<VertexId>>,
pub compute_time_us: u64,
}
#[derive(Debug)]
pub struct StructuralFilter {
config: StructuralConfig,
#[cfg(feature = "structural")]
mincut: Option<SubpolynomialMinCut>,
adjacency: HashMap<VertexId, HashMap<VertexId, Weight>>,
next_edge_id: u64,
edge_ids: HashMap<(VertexId, VertexId), EdgeId>,
}
impl StructuralFilter {
pub fn new(threshold: f64) -> Self {
Self::with_config(StructuralConfig {
threshold,
..Default::default()
})
}
pub fn with_config(config: StructuralConfig) -> Self {
#[cfg(feature = "structural")]
let mincut = if config.use_subpolynomial {
let subpoly_config = SubpolyConfig {
phi: config.phi,
lambda_max: config.max_cut_size,
..Default::default()
};
Some(SubpolynomialMinCut::new(subpoly_config))
} else {
None
};
Self {
config,
#[cfg(feature = "structural")]
mincut,
adjacency: HashMap::new(),
next_edge_id: 1,
edge_ids: HashMap::new(),
}
}
pub fn insert_edge(&mut self, u: VertexId, v: VertexId, weight: Weight) -> Result<EdgeId> {
let key = Self::edge_key(u, v);
if self.edge_ids.contains_key(&key) {
return Err(FilterError::StructuralError(format!(
"Edge ({}, {}) already exists",
u, v
)));
}
let edge_id = self.next_edge_id;
self.next_edge_id += 1;
self.edge_ids.insert(key, edge_id);
self.adjacency.entry(u).or_default().insert(v, weight);
self.adjacency.entry(v).or_default().insert(u, weight);
#[cfg(feature = "structural")]
if let Some(ref mut mc) = self.mincut {
let _ = mc.insert_edge(u, v, weight);
}
Ok(edge_id)
}
pub fn delete_edge(&mut self, u: VertexId, v: VertexId) -> Result<()> {
let key = Self::edge_key(u, v);
if self.edge_ids.remove(&key).is_none() {
return Err(FilterError::StructuralError(format!(
"Edge ({}, {}) not found",
u, v
)));
}
if let Some(neighbors) = self.adjacency.get_mut(&u) {
neighbors.remove(&v);
}
if let Some(neighbors) = self.adjacency.get_mut(&v) {
neighbors.remove(&u);
}
#[cfg(feature = "structural")]
if let Some(ref mut mc) = self.mincut {
let _ = mc.delete_edge(u, v);
}
Ok(())
}
pub fn build(&mut self) {
#[cfg(feature = "structural")]
if let Some(ref mut mc) = self.mincut {
mc.build();
}
}
pub fn evaluate(&self, _state: &SystemState) -> StructuralResult {
let start = std::time::Instant::now();
#[cfg(feature = "structural")]
let cut_value = if let Some(ref mc) = self.mincut {
mc.min_cut_value()
} else {
self.compute_simple_cut()
};
#[cfg(not(feature = "structural"))]
let cut_value = self.compute_simple_cut();
let is_coherent = cut_value >= self.config.threshold;
let boundary_edges = if !is_coherent {
self.find_boundary_edges(cut_value)
} else {
Vec::new()
};
StructuralResult {
cut_value,
boundary_edges,
is_coherent,
healthy_vertices: None, unhealthy_vertices: None,
compute_time_us: start.elapsed().as_micros() as u64,
}
}
fn compute_simple_cut(&self) -> f64 {
if self.adjacency.is_empty() {
return f64::INFINITY;
}
let mut min_cut = f64::INFINITY;
for (_, neighbors) in &self.adjacency {
let vertex_cut: f64 = neighbors.values().sum();
min_cut = min_cut.min(vertex_cut);
}
min_cut
}
fn find_boundary_edges(&self, _cut_value: f64) -> Vec<EdgeId> {
let mut edges: Vec<_> = self.edge_ids.iter().collect();
edges.sort_by(|a, b| {
let weight_a = self
.adjacency
.get(&a.0 .0)
.and_then(|n| n.get(&a.0 .1))
.unwrap_or(&1.0);
let weight_b = self
.adjacency
.get(&b.0 .0)
.and_then(|n| n.get(&b.0 .1))
.unwrap_or(&1.0);
weight_a.partial_cmp(weight_b).unwrap()
});
edges
.into_iter()
.take(10)
.map(|(_, &id)| id)
.collect()
}
fn edge_key(u: VertexId, v: VertexId) -> (VertexId, VertexId) {
if u < v {
(u, v)
} else {
(v, u)
}
}
pub fn threshold(&self) -> f64 {
self.config.threshold
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShiftConfig {
pub threshold: f64,
pub window_size: usize,
pub decay_factor: f64,
pub num_regions: usize,
}
impl Default for ShiftConfig {
fn default() -> Self {
Self {
threshold: 0.5,
window_size: 100,
decay_factor: 0.95,
num_regions: 64,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShiftResult {
pub pressure: f64,
pub affected_regions: RegionMask,
pub region_shifts: Vec<f64>,
pub is_stable: bool,
pub lead_time: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct ShiftFilter {
config: ShiftConfig,
region_stats: Vec<RegionStats>,
global_mean: f64,
global_variance: f64,
num_observations: u64,
}
#[derive(Debug, Clone, Default)]
struct RegionStats {
mean: f64,
variance: f64,
shift_accumulator: f64,
count: u64,
recent_scores: Vec<f64>,
}
impl ShiftFilter {
pub fn new(threshold: f64, window_size: usize) -> Self {
Self::with_config(ShiftConfig {
threshold,
window_size,
..Default::default()
})
}
pub fn with_config(config: ShiftConfig) -> Self {
Self {
region_stats: vec![RegionStats::default(); config.num_regions],
global_mean: 0.0,
global_variance: 1.0,
num_observations: 0,
config,
}
}
pub fn update(&mut self, region: usize, score: f64) {
if region >= self.region_stats.len() {
return;
}
let stats = &mut self.region_stats[region];
stats.count += 1;
let delta = score - stats.mean;
stats.mean += delta / stats.count as f64;
let delta2 = score - stats.mean;
stats.variance += delta * delta2;
stats.recent_scores.push(score);
if stats.recent_scores.len() > self.config.window_size {
stats.recent_scores.remove(0);
}
let deviation = (score - self.global_mean).abs();
stats.shift_accumulator =
self.config.decay_factor * stats.shift_accumulator + deviation;
self.num_observations += 1;
let g_delta = score - self.global_mean;
self.global_mean += g_delta / self.num_observations as f64;
let g_delta2 = score - self.global_mean;
self.global_variance += g_delta * g_delta2;
}
pub fn evaluate(&self, state: &SystemState) -> ShiftResult {
let mut region_shifts = vec![0.0; self.config.num_regions];
let mut affected_regions = RegionMask::empty();
let mut total_pressure = 0.0;
for (region, stats) in self.region_stats.iter().enumerate() {
let shift = if region < state.nonconformity_scores.len() {
self.compute_shift(state.nonconformity_scores[region], stats)
} else {
self.compute_shift_from_stats(stats)
};
region_shifts[region] = shift;
total_pressure += shift;
if shift > self.config.threshold {
affected_regions.set(region as u8);
}
}
let num_active = self
.region_stats
.iter()
.filter(|s| s.count > 0)
.count()
.max(1);
let pressure = total_pressure / num_active as f64;
let is_stable = pressure < self.config.threshold;
let lead_time = if !is_stable && pressure > 0.0 {
let cycles_until_critical = ((1.0 - pressure) / pressure * 100.0) as u64;
Some(cycles_until_critical.max(1))
} else {
None
};
ShiftResult {
pressure,
affected_regions,
region_shifts,
is_stable,
lead_time,
}
}
fn compute_shift(&self, score: f64, stats: &RegionStats) -> f64 {
if stats.count < 2 {
return 0.0;
}
let region_std = (stats.variance / stats.count as f64).sqrt().max(1e-10);
let z_score = (score - stats.mean).abs() / region_std;
(z_score / 3.0).min(1.0) }
fn compute_shift_from_stats(&self, stats: &RegionStats) -> f64 {
if stats.count < self.config.window_size as u64 / 2 {
return 0.0;
}
let normalized = stats.shift_accumulator / stats.count as f64;
let global_std = (self.global_variance / self.num_observations.max(1) as f64)
.sqrt()
.max(1e-10);
(normalized / global_std / 2.0).min(1.0)
}
pub fn threshold(&self) -> f64 {
self.config.threshold
}
pub fn window_size(&self) -> usize {
self.config.window_size
}
pub fn reset(&mut self) {
self.region_stats = vec![RegionStats::default(); self.config.num_regions];
self.global_mean = 0.0;
self.global_variance = 1.0;
self.num_observations = 0;
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvidenceConfig {
pub tau_permit: f64,
pub tau_deny: f64,
pub prior: f64,
}
impl Default for EvidenceConfig {
fn default() -> Self {
Self {
tau_permit: 20.0, tau_deny: 1.0 / 20.0, prior: 0.95, }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvidenceAccumulator {
log_e_value: f64,
samples_seen: u64,
log_evidence_coherent: f64,
log_evidence_incoherent: f64,
}
impl Default for EvidenceAccumulator {
fn default() -> Self {
Self::new()
}
}
impl EvidenceAccumulator {
pub fn new() -> Self {
Self {
log_e_value: 0.0, samples_seen: 0,
log_evidence_coherent: 0.0,
log_evidence_incoherent: 0.0,
}
}
pub fn update(&mut self, likelihood_ratio: f64) {
self.samples_seen += 1;
let lr = likelihood_ratio.clamp(1e-10, 1e10);
self.log_e_value += lr.ln();
if lr > 1.0 {
self.log_evidence_coherent += lr.ln();
} else {
self.log_evidence_incoherent += (-lr.ln()).abs();
}
}
pub fn e_value(&self) -> f64 {
self.log_e_value.exp().min(1e100) }
pub fn log_e_value(&self) -> f64 {
self.log_e_value
}
pub fn samples_seen(&self) -> u64 {
self.samples_seen
}
pub fn reset(&mut self) {
self.log_e_value = 0.0;
self.samples_seen = 0;
self.log_evidence_coherent = 0.0;
self.log_evidence_incoherent = 0.0;
}
pub fn posterior_odds(&self, prior_odds: f64) -> f64 {
prior_odds * self.e_value()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvidenceResult {
pub e_value: f64,
pub log_e_value: f64,
pub samples_seen: u64,
pub verdict: Option<Verdict>,
pub confidence: f64,
}
#[derive(Debug, Clone)]
pub struct EvidenceFilter {
config: EvidenceConfig,
accumulator: EvidenceAccumulator,
region_accumulators: Vec<EvidenceAccumulator>,
}
impl EvidenceFilter {
pub fn new(tau_permit: f64, tau_deny: f64) -> Self {
Self::with_config(EvidenceConfig {
tau_permit,
tau_deny,
..Default::default()
})
}
pub fn with_config(config: EvidenceConfig) -> Self {
Self {
config,
accumulator: EvidenceAccumulator::new(),
region_accumulators: Vec::new(),
}
}
pub fn update(&mut self, likelihood_ratio: f64) {
self.accumulator.update(likelihood_ratio);
}
pub fn update_region(&mut self, region: usize, likelihood_ratio: f64) {
while self.region_accumulators.len() <= region {
self.region_accumulators.push(EvidenceAccumulator::new());
}
self.region_accumulators[region].update(likelihood_ratio);
}
pub fn evaluate(&self, _state: &SystemState) -> EvidenceResult {
let e_value = self.accumulator.e_value();
let log_e_value = self.accumulator.log_e_value();
let verdict = if e_value >= self.config.tau_permit {
Some(Verdict::Permit)
} else if e_value <= self.config.tau_deny {
Some(Verdict::Deny)
} else {
None
};
let confidence = if e_value >= self.config.tau_permit {
((e_value.ln() - self.config.tau_permit.ln())
/ (self.config.tau_permit.ln().abs() + 1.0))
.min(1.0)
} else if e_value <= self.config.tau_deny {
((self.config.tau_deny.ln() - e_value.ln())
/ (self.config.tau_deny.ln().abs() + 1.0))
.min(1.0)
} else {
0.0
};
EvidenceResult {
e_value,
log_e_value,
samples_seen: self.accumulator.samples_seen(),
verdict,
confidence,
}
}
pub fn tau_permit(&self) -> f64 {
self.config.tau_permit
}
pub fn tau_deny(&self) -> f64 {
self.config.tau_deny
}
pub fn accumulator(&self) -> &EvidenceAccumulator {
&self.accumulator
}
pub fn reset(&mut self) {
self.accumulator.reset();
for acc in &mut self.region_accumulators {
acc.reset();
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FilterConfig {
pub structural: StructuralConfig,
pub shift: ShiftConfig,
pub evidence: EvidenceConfig,
}
impl Default for FilterConfig {
fn default() -> Self {
Self {
structural: StructuralConfig::default(),
shift: ShiftConfig::default(),
evidence: EvidenceConfig::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FilterResults {
pub structural: StructuralResult,
pub shift: ShiftResult,
pub evidence: EvidenceResult,
pub verdict: Option<Verdict>,
pub affected_regions: RegionMask,
pub recommendations: Vec<String>,
pub total_time_us: u64,
}
#[derive(Debug)]
pub struct FilterPipeline {
structural: StructuralFilter,
shift: ShiftFilter,
evidence: EvidenceFilter,
}
impl FilterPipeline {
pub fn new(config: FilterConfig) -> Self {
Self {
structural: StructuralFilter::with_config(config.structural),
shift: ShiftFilter::with_config(config.shift),
evidence: EvidenceFilter::with_config(config.evidence),
}
}
pub fn default_config() -> Self {
Self::new(FilterConfig::default())
}
pub fn evaluate(&self, state: &SystemState) -> FilterResults {
let start = std::time::Instant::now();
let structural_result = self.structural.evaluate(state);
let shift_result = self.shift.evaluate(state);
let evidence_result = self.evidence.evaluate(state);
let verdict = self.combine_verdicts(
&structural_result,
&shift_result,
&evidence_result,
);
let mut affected_regions = shift_result.affected_regions;
let mut recommendations = Vec::new();
if !structural_result.is_coherent {
recommendations.push(format!(
"Structural: Cut value {:.2} below threshold {:.2} - partition forming",
structural_result.cut_value,
self.structural.threshold()
));
}
if !shift_result.is_stable {
recommendations.push(format!(
"Shift: Pressure {:.2} above threshold {:.2} - distribution drift detected",
shift_result.pressure,
self.shift.threshold()
));
if let Some(lead_time) = shift_result.lead_time {
recommendations.push(format!(
"Estimated {} cycles until critical drift",
lead_time
));
}
}
if evidence_result.verdict == Some(Verdict::Deny) {
recommendations.push(format!(
"Evidence: E-value {:.2e} below deny threshold - insufficient evidence for coherence",
evidence_result.e_value
));
} else if evidence_result.verdict.is_none() {
recommendations.push(format!(
"Evidence: E-value {:.2e} - gathering more evidence ({} samples)",
evidence_result.e_value,
evidence_result.samples_seen
));
}
FilterResults {
structural: structural_result,
shift: shift_result,
evidence: evidence_result,
verdict,
affected_regions,
recommendations,
total_time_us: start.elapsed().as_micros() as u64,
}
}
fn combine_verdicts(
&self,
structural: &StructuralResult,
shift: &ShiftResult,
evidence: &EvidenceResult,
) -> Option<Verdict> {
if !structural.is_coherent {
return Some(Verdict::Deny);
}
if !shift.is_stable {
if evidence.verdict == Some(Verdict::Deny) {
return Some(Verdict::Deny);
}
return Some(Verdict::Defer);
}
if evidence.verdict == Some(Verdict::Deny) {
return Some(Verdict::Deny);
}
if structural.is_coherent && shift.is_stable {
if evidence.verdict == Some(Verdict::Permit) {
return Some(Verdict::Permit);
}
if evidence.verdict.is_none() {
return Some(Verdict::Defer);
}
}
Some(Verdict::Defer)
}
pub fn structural_mut(&mut self) -> &mut StructuralFilter {
&mut self.structural
}
pub fn shift_mut(&mut self) -> &mut ShiftFilter {
&mut self.shift
}
pub fn evidence_mut(&mut self) -> &mut EvidenceFilter {
&mut self.evidence
}
pub fn structural(&self) -> &StructuralFilter {
&self.structural
}
pub fn shift(&self) -> &ShiftFilter {
&self.shift
}
pub fn evidence(&self) -> &EvidenceFilter {
&self.evidence
}
pub fn reset(&mut self) {
self.shift.reset();
self.evidence.reset();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_region_mask() {
let mut mask = RegionMask::empty();
assert!(!mask.any());
mask.set(5);
assert!(mask.is_set(5));
assert!(!mask.is_set(4));
assert_eq!(mask.count(), 1);
mask.set(10);
assert_eq!(mask.count(), 2);
mask.clear(5);
assert!(!mask.is_set(5));
assert!(mask.is_set(10));
}
#[test]
fn test_structural_filter_basic() {
let mut filter = StructuralFilter::new(2.0);
filter.insert_edge(1, 2, 1.0).unwrap();
filter.insert_edge(2, 3, 1.0).unwrap();
filter.insert_edge(3, 1, 1.0).unwrap();
let state = SystemState::new(3);
let result = filter.evaluate(&state);
assert!(result.cut_value >= 2.0);
assert!(result.is_coherent);
}
#[test]
fn test_structural_filter_low_cut() {
let config = StructuralConfig {
threshold: 3.0, use_subpolynomial: false, ..Default::default()
};
let mut filter = StructuralFilter::with_config(config);
filter.insert_edge(1, 2, 1.0).unwrap();
let state = SystemState::new(2);
let result = filter.evaluate(&state);
assert!(!result.is_coherent);
}
#[test]
fn test_shift_filter_stable() {
let mut filter = ShiftFilter::new(0.5, 100);
for i in 0..50 {
filter.update(0, 0.5 + (i as f64 * 0.01) % 0.1);
filter.update(1, 0.5 + (i as f64 * 0.01) % 0.1);
}
let state = SystemState::new(10);
let result = filter.evaluate(&state);
assert!(result.is_stable);
}
#[test]
fn test_shift_filter_drift() {
let mut filter = ShiftFilter::new(0.3, 100);
for _ in 0..30 {
filter.update(0, 0.5);
}
for i in 0..30 {
filter.update(0, 0.5 + i as f64 * 0.1);
}
let state = SystemState::new(10);
let result = filter.evaluate(&state);
assert!(result.pressure > 0.0);
}
#[test]
fn test_evidence_accumulator() {
let mut acc = EvidenceAccumulator::new();
assert_eq!(acc.e_value(), 1.0);
assert_eq!(acc.samples_seen(), 0);
acc.update(2.0); assert!(acc.e_value() > 1.0);
assert_eq!(acc.samples_seen(), 1);
acc.update(2.0);
acc.update(2.0);
assert_eq!(acc.samples_seen(), 3);
assert!(acc.e_value() > 4.0);
}
#[test]
fn test_evidence_filter_permit() {
let mut filter = EvidenceFilter::new(10.0, 0.1);
for _ in 0..10 {
filter.update(2.0);
}
let state = SystemState::new(10);
let result = filter.evaluate(&state);
assert!(result.e_value > 10.0);
assert_eq!(result.verdict, Some(Verdict::Permit));
}
#[test]
fn test_evidence_filter_deny() {
let mut filter = EvidenceFilter::new(10.0, 0.1);
for _ in 0..10 {
filter.update(0.5);
}
let state = SystemState::new(10);
let result = filter.evaluate(&state);
assert!(result.e_value < 0.1);
assert_eq!(result.verdict, Some(Verdict::Deny));
}
#[test]
fn test_filter_pipeline_permit() {
let config = FilterConfig {
structural: StructuralConfig {
threshold: 1.0,
..Default::default()
},
shift: ShiftConfig {
threshold: 0.5,
..Default::default()
},
evidence: EvidenceConfig {
tau_permit: 5.0,
tau_deny: 0.2,
..Default::default()
},
};
let mut pipeline = FilterPipeline::new(config);
pipeline.structural_mut().insert_edge(1, 2, 2.0).unwrap();
pipeline.structural_mut().insert_edge(2, 3, 2.0).unwrap();
pipeline.structural_mut().insert_edge(3, 1, 2.0).unwrap();
for i in 0..20 {
pipeline.shift_mut().update(0, 0.5);
}
for _ in 0..5 {
pipeline.evidence_mut().update(2.0);
}
let state = SystemState::new(3);
let result = pipeline.evaluate(&state);
assert_eq!(result.verdict, Some(Verdict::Permit));
}
#[test]
fn test_filter_pipeline_deny_structural() {
let config = FilterConfig {
structural: StructuralConfig {
threshold: 5.0, use_subpolynomial: false, ..Default::default()
},
..Default::default()
};
let mut pipeline = FilterPipeline::new(config);
pipeline.structural_mut().insert_edge(1, 2, 1.0).unwrap();
let state = SystemState::new(2);
let result = pipeline.evaluate(&state);
assert_eq!(result.verdict, Some(Verdict::Deny));
assert!(!result.recommendations.is_empty());
}
#[test]
fn test_system_state() {
let mut state = SystemState::new(10);
state.add_edge(1, 2, 1.0);
assert!(state.adjacency.contains_key(&1));
assert!(state.adjacency.contains_key(&2));
state.add_syndrome(0.5);
state.add_syndrome(0.6);
assert_eq!(state.syndromes.len(), 2);
state.advance_cycle();
assert!(state.syndromes.is_empty());
assert_eq!(state.syndrome_history.len(), 1);
assert_eq!(state.cycle, 1);
}
#[test]
fn test_filter_config_serialization() {
let config = FilterConfig::default();
let json = serde_json::to_string(&config).unwrap();
let restored: FilterConfig = serde_json::from_str(&json).unwrap();
assert_eq!(config.structural.threshold, restored.structural.threshold);
assert_eq!(config.shift.threshold, restored.shift.threshold);
assert_eq!(config.evidence.tau_permit, restored.evidence.tau_permit);
}
}