use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub use crate::graph::FleetGraph;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ConsensusResult {
pub is_consistent: bool,
pub deviation: f64,
pub conflicted_tiles: Vec<u64>,
pub information_bits: f64,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum ConsensusVote {
Unanimous,
Aligned,
Conflict,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ZhcTile {
pub id: u64,
pub state: [f64; 3],
pub neighbors: Vec<u64>,
pub cycle_id: Option<u64>,
}
impl ZhcTile {
fn gradient(&self) -> [f64; 3] {
let [sx, sy, sz] = self.state;
let mag = (sx * sx + sy * sy + sz * sz).sqrt();
if mag < 1e-10 {
[0.0, 0.0, 0.0]
} else {
[sx / mag, sy / mag, sz / mag]
}
}
fn check_consistency(&self, neighbor_states: &HashMap<u64, [f64; 3]>, tolerance: f64) -> ConsensusVote {
let grad = self.gradient();
if grad == [0.0, 0.0, 0.0] {
return ConsensusVote::Unanimous;
}
let mut aligned_count = 0;
for (&nid, &nstate) in neighbor_states {
if self.neighbors.contains(&nid) {
let ngrad = Self { id: 0, state: nstate, neighbors: vec![], cycle_id: None }.gradient();
let dot = grad[0] * ngrad[0] + grad[1] * ngrad[1] + grad[2] * ngrad[2];
if dot > tolerance {
aligned_count += 1;
}
}
}
if aligned_count == self.neighbors.len() {
ConsensusVote::Aligned
} else {
ConsensusVote::Conflict
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ZhcConsensus {
tolerance: f64,
tiles: HashMap<u64, ZhcTile>,
tile_index: HashMap<u64, usize>,
}
impl ZhcConsensus {
pub fn new(tolerance: f64) -> Self {
Self {
tolerance,
tiles: HashMap::new(),
tile_index: HashMap::new(),
}
}
pub fn add_tile(&mut self, id: u64, state: [f64; 3], neighbors: Vec<u64>) {
let idx = self.tiles.len();
self.tile_index.insert(id, idx);
self.tiles.insert(id, ZhcTile { id, state, neighbors, cycle_id: None });
}
pub fn run_consensus(&self) -> ConsensusResult {
let mut conflicted = Vec::new();
let mut total_deviation = 0.0;
let mut aligned_count = 0;
for (id, tile) in &self.tiles {
let neighbor_states: HashMap<u64, [f64; 3]> = tile
.neighbors
.iter()
.filter_map(|&nid| self.tiles.get(&nid).map(|t| (nid, t.state)))
.collect();
let vote = tile.check_consistency(&neighbor_states, self.tolerance);
match vote {
ConsensusVote::Unanimous => {
aligned_count += 1;
}
ConsensusVote::Aligned => {
aligned_count += 1;
}
ConsensusVote::Conflict => {
conflicted.push(*id);
total_deviation += 1.0;
}
}
}
let total = self.tiles.len();
let is_consistent = conflicted.is_empty();
let information_bits = if total > 0 {
(aligned_count as f64).log2().max(0.0)
} else {
0.0
};
ConsensusResult {
is_consistent,
deviation: total_deviation / total as f64,
conflicted_tiles: conflicted,
information_bits,
}
}
pub fn tile_count(&self) -> usize {
self.tiles.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_zhc_perfect_alignment() {
let mut zhc = ZhcConsensus::new(0.5);
zhc.add_tile(1, [0.6, 0.8, 0.0], vec![2, 3]);
zhc.add_tile(2, [0.6, 0.8, 0.0], vec![1, 3]);
zhc.add_tile(3, [0.6, 0.8, 0.0], vec![1, 2]);
let result = zhc.run_consensus();
assert!(result.is_consistent);
assert!(result.conflicted_tiles.is_empty());
assert!(result.deviation < 0.1);
}
#[test]
fn test_zhc_detects_conflict() {
let mut zhc = ZhcConsensus::new(0.5);
zhc.add_tile(1, [0.6, 0.8, 0.0], vec![2, 3]);
zhc.add_tile(2, [0.6, 0.8, 0.0], vec![1, 3]);
zhc.add_tile(3, [-0.8, 0.6, 0.0], vec![1, 2]);
let result = zhc.run_consensus();
assert!(!result.is_consistent);
assert!(!result.conflicted_tiles.is_empty());
}
#[test]
fn test_zeros_state_unanimous() {
let mut zhc = ZhcConsensus::new(0.5);
zhc.add_tile(1, [0.0, 0.0, 0.0], vec![]);
let result = zhc.run_consensus();
assert!(result.is_consistent);
}
}