//! Tile report structures for coherence gate coordination //! //! Defines the 64-byte cache-line aligned report structure that tiles //! produce after each tick. These reports are aggregated by the coordinator //! to form witness fragments for the coherence gate. #![allow(missing_docs)] use crate::delta::TileVertexId; use crate::evidence::LogEValue; use core::mem::size_of; /// Tile status codes #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum TileStatus { /// Tile is idle (no work) Idle = 0, /// Tile is processing deltas Processing = 1, /// Tile completed tick successfully Complete = 2, /// Tile encountered an error Error = 3, /// Tile is waiting for synchronization Waiting = 4, /// Tile is checkpointing Checkpointing = 5, /// Tile is recovering from checkpoint Recovering = 6, /// Tile is shutting down Shutdown = 7, } impl From for TileStatus { fn from(v: u8) -> Self { match v { 0 => TileStatus::Idle, 1 => TileStatus::Processing, 2 => TileStatus::Complete, 3 => TileStatus::Error, 4 => TileStatus::Waiting, 5 => TileStatus::Checkpointing, 6 => TileStatus::Recovering, 7 => TileStatus::Shutdown, _ => TileStatus::Error, } } } /// Witness fragment for aggregation /// /// Compact representation of local cut/partition information /// that can be merged across tiles. #[derive(Debug, Clone, Copy, Default)] #[repr(C, align(8))] pub struct WitnessFragment { /// Seed vertex for this fragment pub seed: TileVertexId, /// Boundary size (cut edges crossing fragment) pub boundary_size: u16, /// Cardinality (vertices in fragment) pub cardinality: u16, /// Fragment hash for consistency checking pub hash: u16, /// Local minimum cut value (fixed-point) pub local_min_cut: u16, /// Component ID this fragment belongs to pub component: u16, /// Reserved padding pub _reserved: u16, } impl WitnessFragment { /// Create a new witness fragment #[inline] pub const fn new( seed: TileVertexId, boundary_size: u16, cardinality: u16, local_min_cut: u16, ) -> Self { Self { seed, boundary_size, cardinality, hash: 0, local_min_cut, component: 0, _reserved: 0, } } /// Compute fragment hash pub fn compute_hash(&mut self) { let mut h = self.seed as u32; h = h.wrapping_mul(31).wrapping_add(self.boundary_size as u32); h = h.wrapping_mul(31).wrapping_add(self.cardinality as u32); h = h.wrapping_mul(31).wrapping_add(self.local_min_cut as u32); self.hash = (h & 0xFFFF) as u16; } /// Check if fragment is empty #[inline] pub const fn is_empty(&self) -> bool { self.cardinality == 0 } } /// Tile report produced after each tick (64 bytes, cache-line aligned) /// /// This structure is designed to fit exactly in one cache line for /// efficient memory access patterns in the coordinator. #[derive(Debug, Clone, Copy)] #[repr(C, align(64))] pub struct TileReport { // --- Header (8 bytes) --- /// Tile ID (0-255) pub tile_id: u8, /// Tile status pub status: TileStatus, /// Generation/epoch number pub generation: u16, /// Current tick number pub tick: u32, // --- Graph state (8 bytes) --- /// Number of active vertices pub num_vertices: u16, /// Number of active edges pub num_edges: u16, /// Number of connected components pub num_components: u16, /// Graph flags pub graph_flags: u16, // --- Evidence state (8 bytes) --- /// Global log e-value (tile-local) pub log_e_value: LogEValue, /// Number of observations processed pub obs_count: u16, /// Number of rejected hypotheses pub rejected_count: u16, // --- Witness fragment (16 bytes) --- /// Primary witness fragment pub witness: WitnessFragment, // --- Performance metrics (8 bytes) --- /// Delta processing time (microseconds) pub delta_time_us: u16, /// Tick processing time (microseconds) pub tick_time_us: u16, /// Deltas processed this tick pub deltas_processed: u16, /// Memory usage (KB) pub memory_kb: u16, // --- Cross-tile coordination (8 bytes) --- /// Number of ghost vertices pub ghost_vertices: u16, /// Number of ghost edges pub ghost_edges: u16, /// Boundary vertices (shared with other tiles) pub boundary_vertices: u16, /// Pending sync messages pub pending_sync: u16, // --- Reserved for future use (8 bytes) --- /// Reserved fields pub _reserved: [u8; 8], } impl Default for TileReport { fn default() -> Self { Self::new(0) } } impl TileReport { /// Graph flag: graph is connected pub const GRAPH_CONNECTED: u16 = 0x0001; /// Graph flag: graph is dirty (needs recomputation) pub const GRAPH_DIRTY: u16 = 0x0002; /// Graph flag: graph is at capacity pub const GRAPH_FULL: u16 = 0x0004; /// Graph flag: graph has ghost edges pub const GRAPH_HAS_GHOSTS: u16 = 0x0008; /// Create a new report for a tile #[inline] pub const fn new(tile_id: u8) -> Self { Self { tile_id, status: TileStatus::Idle, generation: 0, tick: 0, num_vertices: 0, num_edges: 0, num_components: 0, graph_flags: 0, log_e_value: 0, obs_count: 0, rejected_count: 0, witness: WitnessFragment { seed: 0, boundary_size: 0, cardinality: 0, hash: 0, local_min_cut: 0, component: 0, _reserved: 0, }, delta_time_us: 0, tick_time_us: 0, deltas_processed: 0, memory_kb: 0, ghost_vertices: 0, ghost_edges: 0, boundary_vertices: 0, pending_sync: 0, _reserved: [0; 8], } } /// Mark report as complete #[inline] pub fn set_complete(&mut self) { self.status = TileStatus::Complete; } /// Mark report as error #[inline] pub fn set_error(&mut self) { self.status = TileStatus::Error; } /// Set connected flag #[inline] pub fn set_connected(&mut self, connected: bool) { if connected { self.graph_flags |= Self::GRAPH_CONNECTED; } else { self.graph_flags &= !Self::GRAPH_CONNECTED; } } /// Check if graph is connected #[inline] pub const fn is_connected(&self) -> bool { self.graph_flags & Self::GRAPH_CONNECTED != 0 } /// Check if graph is dirty #[inline] pub const fn is_dirty(&self) -> bool { self.graph_flags & Self::GRAPH_DIRTY != 0 } /// Get e-value as approximate f32 pub fn e_value_approx(&self) -> f32 { let log2_val = (self.log_e_value as f32) / 65536.0; libm::exp2f(log2_val) } /// Update witness fragment pub fn set_witness(&mut self, witness: WitnessFragment) { self.witness = witness; } /// Get the witness fragment #[inline] pub const fn get_witness(&self) -> &WitnessFragment { &self.witness } /// Check if tile has any rejections #[inline] pub const fn has_rejections(&self) -> bool { self.rejected_count > 0 } /// Get processing rate (deltas per microsecond) pub fn processing_rate(&self) -> f32 { if self.tick_time_us == 0 { 0.0 } else { (self.deltas_processed as f32) / (self.tick_time_us as f32) } } } /// Report aggregator for combining multiple tile reports #[derive(Debug, Clone, Copy, Default)] #[repr(C)] pub struct AggregatedReport { /// Total vertices across all tiles pub total_vertices: u32, /// Total edges across all tiles pub total_edges: u32, /// Total components across all tiles pub total_components: u16, /// Number of tiles reporting pub tiles_reporting: u16, /// Tiles with errors pub tiles_with_errors: u16, /// Tiles with rejections pub tiles_with_rejections: u16, /// Global log e-value (sum of tile e-values) pub global_log_e: i64, /// Minimum local cut across tiles pub global_min_cut: u16, /// Tile with minimum cut pub min_cut_tile: u8, /// Reserved padding pub _reserved: u8, /// Total processing time (microseconds) pub total_time_us: u32, /// Tick number pub tick: u32, } impl AggregatedReport { /// Create a new aggregated report pub const fn new(tick: u32) -> Self { Self { total_vertices: 0, total_edges: 0, total_components: 0, tiles_reporting: 0, tiles_with_errors: 0, tiles_with_rejections: 0, global_log_e: 0, global_min_cut: u16::MAX, min_cut_tile: 0, _reserved: 0, total_time_us: 0, tick, } } /// Merge a tile report into the aggregate pub fn merge(&mut self, report: &TileReport) { self.total_vertices += report.num_vertices as u32; self.total_edges += report.num_edges as u32; self.total_components += report.num_components; self.tiles_reporting += 1; if report.status == TileStatus::Error { self.tiles_with_errors += 1; } if report.rejected_count > 0 { self.tiles_with_rejections += 1; } self.global_log_e += report.log_e_value as i64; if report.witness.local_min_cut < self.global_min_cut { self.global_min_cut = report.witness.local_min_cut; self.min_cut_tile = report.tile_id; } self.total_time_us = self.total_time_us.max(report.tick_time_us as u32); } /// Check if all tiles completed successfully pub fn all_complete(&self, expected_tiles: u16) -> bool { self.tiles_reporting == expected_tiles && self.tiles_with_errors == 0 } /// Get global e-value as approximate f64 pub fn global_e_value(&self) -> f64 { let log2_val = (self.global_log_e as f64) / 65536.0; libm::exp2(log2_val) } } // Compile-time size assertions const _: () = assert!( size_of::() == 64, "TileReport must be exactly 64 bytes" ); const _: () = assert!( size_of::() == 16, "WitnessFragment must be 16 bytes" ); #[cfg(test)] mod tests { use super::*; #[test] fn test_tile_report_size() { assert_eq!(size_of::(), 64); } #[test] fn test_tile_report_alignment() { assert_eq!(core::mem::align_of::(), 64); } #[test] fn test_witness_fragment_size() { assert_eq!(size_of::(), 16); } #[test] fn test_new_report() { let report = TileReport::new(5); assert_eq!(report.tile_id, 5); assert_eq!(report.status, TileStatus::Idle); assert_eq!(report.tick, 0); } #[test] fn test_set_status() { let mut report = TileReport::new(0); report.set_complete(); assert_eq!(report.status, TileStatus::Complete); report.set_error(); assert_eq!(report.status, TileStatus::Error); } #[test] fn test_connected_flag() { let mut report = TileReport::new(0); assert!(!report.is_connected()); report.set_connected(true); assert!(report.is_connected()); report.set_connected(false); assert!(!report.is_connected()); } #[test] fn test_witness_fragment() { let mut frag = WitnessFragment::new(10, 5, 20, 100); assert_eq!(frag.seed, 10); assert_eq!(frag.boundary_size, 5); assert_eq!(frag.cardinality, 20); assert_eq!(frag.local_min_cut, 100); frag.compute_hash(); assert_ne!(frag.hash, 0); } #[test] fn test_aggregated_report() { let mut agg = AggregatedReport::new(1); let mut report1 = TileReport::new(0); report1.num_vertices = 50; report1.num_edges = 100; report1.witness.local_min_cut = 200; let mut report2 = TileReport::new(1); report2.num_vertices = 75; report2.num_edges = 150; report2.witness.local_min_cut = 150; agg.merge(&report1); agg.merge(&report2); assert_eq!(agg.tiles_reporting, 2); assert_eq!(agg.total_vertices, 125); assert_eq!(agg.total_edges, 250); assert_eq!(agg.global_min_cut, 150); assert_eq!(agg.min_cut_tile, 1); } #[test] fn test_tile_status_roundtrip() { for i in 0..=7 { let status = TileStatus::from(i); assert_eq!(status as u8, i); } } #[test] fn test_processing_rate() { let mut report = TileReport::new(0); report.deltas_processed = 100; report.tick_time_us = 50; assert!((report.processing_rate() - 2.0).abs() < 0.01); } }