1use serde::{Deserialize, Serialize};
19use std::collections::HashMap;
20
21pub use crate::graph::FleetGraph;
23
24#[derive(Clone, Debug, Serialize, Deserialize)]
26pub struct ConsensusResult {
27 pub is_consistent: bool,
29 pub deviation: f64,
31 pub conflicted_tiles: Vec<u64>,
33 pub information_bits: f64,
35}
36
37#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
39pub enum ConsensusVote {
40 Unanimous,
42 Aligned,
44 Conflict,
46}
47
48#[derive(Clone, Debug, Serialize, Deserialize)]
50pub struct ZhcTile {
51 pub id: u64,
52 pub state: [f64; 3],
54 pub neighbors: Vec<u64>,
55 pub cycle_id: Option<u64>,
56}
57
58impl ZhcTile {
59 fn gradient(&self) -> [f64; 3] {
61 let [sx, sy, sz] = self.state;
62 let mag = (sx * sx + sy * sy + sz * sz).sqrt();
63 if mag < 1e-10 {
64 [0.0, 0.0, 0.0]
65 } else {
66 [sx / mag, sy / mag, sz / mag]
67 }
68 }
69
70 fn check_consistency(&self, neighbor_states: &HashMap<u64, [f64; 3]>, tolerance: f64) -> ConsensusVote {
72 let grad = self.gradient();
73 if grad == [0.0, 0.0, 0.0] {
74 return ConsensusVote::Unanimous;
75 }
76
77 let mut aligned_count = 0;
79 for (&nid, &nstate) in neighbor_states {
80 if self.neighbors.contains(&nid) {
81 let ngrad = Self { id: 0, state: nstate, neighbors: vec![], cycle_id: None }.gradient();
82 let dot = grad[0] * ngrad[0] + grad[1] * ngrad[1] + grad[2] * ngrad[2];
83 if dot > tolerance {
84 aligned_count += 1;
85 }
86 }
87 }
88
89 if aligned_count == self.neighbors.len() {
90 ConsensusVote::Aligned
91 } else {
92 ConsensusVote::Conflict
93 }
94 }
95}
96
97#[derive(Clone, Debug, Serialize, Deserialize)]
113pub struct ZhcConsensus {
114 tolerance: f64,
115 tiles: HashMap<u64, ZhcTile>,
116 tile_index: HashMap<u64, usize>,
117}
118
119impl ZhcConsensus {
120 pub fn new(tolerance: f64) -> Self {
121 Self {
122 tolerance,
123 tiles: HashMap::new(),
124 tile_index: HashMap::new(),
125 }
126 }
127
128 pub fn add_tile(&mut self, id: u64, state: [f64; 3], neighbors: Vec<u64>) {
130 let idx = self.tiles.len();
131 self.tile_index.insert(id, idx);
132 self.tiles.insert(id, ZhcTile { id, state, neighbors, cycle_id: None });
133 }
134
135 pub fn run_consensus(&self) -> ConsensusResult {
142 let mut conflicted = Vec::new();
143 let mut total_deviation = 0.0;
144 let mut aligned_count = 0;
145
146 for (id, tile) in &self.tiles {
147 let neighbor_states: HashMap<u64, [f64; 3]> = tile
148 .neighbors
149 .iter()
150 .filter_map(|&nid| self.tiles.get(&nid).map(|t| (nid, t.state)))
151 .collect();
152
153 let vote = tile.check_consistency(&neighbor_states, self.tolerance);
154
155 match vote {
156 ConsensusVote::Unanimous => {
157 aligned_count += 1;
158 }
159 ConsensusVote::Aligned => {
160 aligned_count += 1;
161 }
162 ConsensusVote::Conflict => {
163 conflicted.push(*id);
164 total_deviation += 1.0;
165 }
166 }
167 }
168
169 let total = self.tiles.len();
170 let is_consistent = conflicted.is_empty();
171
172 let information_bits = if total > 0 {
174 (aligned_count as f64).log2().max(0.0)
175 } else {
176 0.0
177 };
178
179 ConsensusResult {
180 is_consistent,
181 deviation: total_deviation / total as f64,
182 conflicted_tiles: conflicted,
183 information_bits,
184 }
185 }
186
187 pub fn tile_count(&self) -> usize {
189 self.tiles.len()
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn test_zhc_perfect_alignment() {
199 let mut zhc = ZhcConsensus::new(0.5);
200 zhc.add_tile(1, [0.6, 0.8, 0.0], vec![2, 3]);
202 zhc.add_tile(2, [0.6, 0.8, 0.0], vec![1, 3]);
203 zhc.add_tile(3, [0.6, 0.8, 0.0], vec![1, 2]);
204
205 let result = zhc.run_consensus();
206 assert!(result.is_consistent);
207 assert!(result.conflicted_tiles.is_empty());
208 assert!(result.deviation < 0.1);
209 }
210
211 #[test]
212 fn test_zhc_detects_conflict() {
213 let mut zhc = ZhcConsensus::new(0.5);
214 zhc.add_tile(1, [0.6, 0.8, 0.0], vec![2, 3]);
216 zhc.add_tile(2, [0.6, 0.8, 0.0], vec![1, 3]);
217 zhc.add_tile(3, [-0.8, 0.6, 0.0], vec![1, 2]); let result = zhc.run_consensus();
220 assert!(!result.is_consistent);
221 assert!(!result.conflicted_tiles.is_empty());
222 }
223
224 #[test]
225 fn test_zeros_state_unanimous() {
226 let mut zhc = ZhcConsensus::new(0.5);
227 zhc.add_tile(1, [0.0, 0.0, 0.0], vec![]);
229
230 let result = zhc.run_consensus();
231 assert!(result.is_consistent);
232 }
233}