cognitum_gate_kernel/
report.rs

1//! Tile report structures for coherence gate coordination
2//!
3//! Defines the 64-byte cache-line aligned report structure that tiles
4//! produce after each tick. These reports are aggregated by the coordinator
5//! to form witness fragments for the coherence gate.
6
7#![allow(missing_docs)]
8
9use crate::delta::TileVertexId;
10use crate::evidence::LogEValue;
11use core::mem::size_of;
12
13/// Tile status codes
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15#[repr(u8)]
16pub enum TileStatus {
17    /// Tile is idle (no work)
18    Idle = 0,
19    /// Tile is processing deltas
20    Processing = 1,
21    /// Tile completed tick successfully
22    Complete = 2,
23    /// Tile encountered an error
24    Error = 3,
25    /// Tile is waiting for synchronization
26    Waiting = 4,
27    /// Tile is checkpointing
28    Checkpointing = 5,
29    /// Tile is recovering from checkpoint
30    Recovering = 6,
31    /// Tile is shutting down
32    Shutdown = 7,
33}
34
35impl From<u8> for TileStatus {
36    fn from(v: u8) -> Self {
37        match v {
38            0 => TileStatus::Idle,
39            1 => TileStatus::Processing,
40            2 => TileStatus::Complete,
41            3 => TileStatus::Error,
42            4 => TileStatus::Waiting,
43            5 => TileStatus::Checkpointing,
44            6 => TileStatus::Recovering,
45            7 => TileStatus::Shutdown,
46            _ => TileStatus::Error,
47        }
48    }
49}
50
51/// Witness fragment for aggregation
52///
53/// Compact representation of local cut/partition information
54/// that can be merged across tiles.
55#[derive(Debug, Clone, Copy, Default)]
56#[repr(C, align(8))]
57pub struct WitnessFragment {
58    /// Seed vertex for this fragment
59    pub seed: TileVertexId,
60    /// Boundary size (cut edges crossing fragment)
61    pub boundary_size: u16,
62    /// Cardinality (vertices in fragment)
63    pub cardinality: u16,
64    /// Fragment hash for consistency checking
65    pub hash: u16,
66    /// Local minimum cut value (fixed-point)
67    pub local_min_cut: u16,
68    /// Component ID this fragment belongs to
69    pub component: u16,
70    /// Reserved padding
71    pub _reserved: u16,
72}
73
74impl WitnessFragment {
75    /// Create a new witness fragment
76    #[inline]
77    pub const fn new(
78        seed: TileVertexId,
79        boundary_size: u16,
80        cardinality: u16,
81        local_min_cut: u16,
82    ) -> Self {
83        Self {
84            seed,
85            boundary_size,
86            cardinality,
87            hash: 0,
88            local_min_cut,
89            component: 0,
90            _reserved: 0,
91        }
92    }
93
94    /// Compute fragment hash
95    pub fn compute_hash(&mut self) {
96        let mut h = self.seed as u32;
97        h = h.wrapping_mul(31).wrapping_add(self.boundary_size as u32);
98        h = h.wrapping_mul(31).wrapping_add(self.cardinality as u32);
99        h = h.wrapping_mul(31).wrapping_add(self.local_min_cut as u32);
100        self.hash = (h & 0xFFFF) as u16;
101    }
102
103    /// Check if fragment is empty
104    #[inline]
105    pub const fn is_empty(&self) -> bool {
106        self.cardinality == 0
107    }
108}
109
110/// Tile report produced after each tick (64 bytes, cache-line aligned)
111///
112/// This structure is designed to fit exactly in one cache line for
113/// efficient memory access patterns in the coordinator.
114#[derive(Debug, Clone, Copy)]
115#[repr(C, align(64))]
116pub struct TileReport {
117    // --- Header (8 bytes) ---
118    /// Tile ID (0-255)
119    pub tile_id: u8,
120    /// Tile status
121    pub status: TileStatus,
122    /// Generation/epoch number
123    pub generation: u16,
124    /// Current tick number
125    pub tick: u32,
126
127    // --- Graph state (8 bytes) ---
128    /// Number of active vertices
129    pub num_vertices: u16,
130    /// Number of active edges
131    pub num_edges: u16,
132    /// Number of connected components
133    pub num_components: u16,
134    /// Graph flags
135    pub graph_flags: u16,
136
137    // --- Evidence state (8 bytes) ---
138    /// Global log e-value (tile-local)
139    pub log_e_value: LogEValue,
140    /// Number of observations processed
141    pub obs_count: u16,
142    /// Number of rejected hypotheses
143    pub rejected_count: u16,
144
145    // --- Witness fragment (16 bytes) ---
146    /// Primary witness fragment
147    pub witness: WitnessFragment,
148
149    // --- Performance metrics (8 bytes) ---
150    /// Delta processing time (microseconds)
151    pub delta_time_us: u16,
152    /// Tick processing time (microseconds)
153    pub tick_time_us: u16,
154    /// Deltas processed this tick
155    pub deltas_processed: u16,
156    /// Memory usage (KB)
157    pub memory_kb: u16,
158
159    // --- Cross-tile coordination (8 bytes) ---
160    /// Number of ghost vertices
161    pub ghost_vertices: u16,
162    /// Number of ghost edges
163    pub ghost_edges: u16,
164    /// Boundary vertices (shared with other tiles)
165    pub boundary_vertices: u16,
166    /// Pending sync messages
167    pub pending_sync: u16,
168
169    // --- Reserved for future use (8 bytes) ---
170    /// Reserved fields
171    pub _reserved: [u8; 8],
172}
173
174impl Default for TileReport {
175    fn default() -> Self {
176        Self::new(0)
177    }
178}
179
180impl TileReport {
181    /// Graph flag: graph is connected
182    pub const GRAPH_CONNECTED: u16 = 0x0001;
183    /// Graph flag: graph is dirty (needs recomputation)
184    pub const GRAPH_DIRTY: u16 = 0x0002;
185    /// Graph flag: graph is at capacity
186    pub const GRAPH_FULL: u16 = 0x0004;
187    /// Graph flag: graph has ghost edges
188    pub const GRAPH_HAS_GHOSTS: u16 = 0x0008;
189
190    /// Create a new report for a tile
191    #[inline]
192    pub const fn new(tile_id: u8) -> Self {
193        Self {
194            tile_id,
195            status: TileStatus::Idle,
196            generation: 0,
197            tick: 0,
198            num_vertices: 0,
199            num_edges: 0,
200            num_components: 0,
201            graph_flags: 0,
202            log_e_value: 0,
203            obs_count: 0,
204            rejected_count: 0,
205            witness: WitnessFragment {
206                seed: 0,
207                boundary_size: 0,
208                cardinality: 0,
209                hash: 0,
210                local_min_cut: 0,
211                component: 0,
212                _reserved: 0,
213            },
214            delta_time_us: 0,
215            tick_time_us: 0,
216            deltas_processed: 0,
217            memory_kb: 0,
218            ghost_vertices: 0,
219            ghost_edges: 0,
220            boundary_vertices: 0,
221            pending_sync: 0,
222            _reserved: [0; 8],
223        }
224    }
225
226    /// Mark report as complete
227    #[inline]
228    pub fn set_complete(&mut self) {
229        self.status = TileStatus::Complete;
230    }
231
232    /// Mark report as error
233    #[inline]
234    pub fn set_error(&mut self) {
235        self.status = TileStatus::Error;
236    }
237
238    /// Set connected flag
239    #[inline]
240    pub fn set_connected(&mut self, connected: bool) {
241        if connected {
242            self.graph_flags |= Self::GRAPH_CONNECTED;
243        } else {
244            self.graph_flags &= !Self::GRAPH_CONNECTED;
245        }
246    }
247
248    /// Check if graph is connected
249    #[inline]
250    pub const fn is_connected(&self) -> bool {
251        self.graph_flags & Self::GRAPH_CONNECTED != 0
252    }
253
254    /// Check if graph is dirty
255    #[inline]
256    pub const fn is_dirty(&self) -> bool {
257        self.graph_flags & Self::GRAPH_DIRTY != 0
258    }
259
260    /// Get e-value as approximate f32
261    pub fn e_value_approx(&self) -> f32 {
262        let log2_val = (self.log_e_value as f32) / 65536.0;
263        libm::exp2f(log2_val)
264    }
265
266    /// Update witness fragment
267    pub fn set_witness(&mut self, witness: WitnessFragment) {
268        self.witness = witness;
269    }
270
271    /// Get the witness fragment
272    #[inline]
273    pub const fn get_witness(&self) -> &WitnessFragment {
274        &self.witness
275    }
276
277    /// Check if tile has any rejections
278    #[inline]
279    pub const fn has_rejections(&self) -> bool {
280        self.rejected_count > 0
281    }
282
283    /// Get processing rate (deltas per microsecond)
284    pub fn processing_rate(&self) -> f32 {
285        if self.tick_time_us == 0 {
286            0.0
287        } else {
288            (self.deltas_processed as f32) / (self.tick_time_us as f32)
289        }
290    }
291}
292
293/// Report aggregator for combining multiple tile reports
294#[derive(Debug, Clone, Copy, Default)]
295#[repr(C)]
296pub struct AggregatedReport {
297    /// Total vertices across all tiles
298    pub total_vertices: u32,
299    /// Total edges across all tiles
300    pub total_edges: u32,
301    /// Total components across all tiles
302    pub total_components: u16,
303    /// Number of tiles reporting
304    pub tiles_reporting: u16,
305    /// Tiles with errors
306    pub tiles_with_errors: u16,
307    /// Tiles with rejections
308    pub tiles_with_rejections: u16,
309    /// Global log e-value (sum of tile e-values)
310    pub global_log_e: i64,
311    /// Minimum local cut across tiles
312    pub global_min_cut: u16,
313    /// Tile with minimum cut
314    pub min_cut_tile: u8,
315    /// Reserved padding
316    pub _reserved: u8,
317    /// Total processing time (microseconds)
318    pub total_time_us: u32,
319    /// Tick number
320    pub tick: u32,
321}
322
323impl AggregatedReport {
324    /// Create a new aggregated report
325    pub const fn new(tick: u32) -> Self {
326        Self {
327            total_vertices: 0,
328            total_edges: 0,
329            total_components: 0,
330            tiles_reporting: 0,
331            tiles_with_errors: 0,
332            tiles_with_rejections: 0,
333            global_log_e: 0,
334            global_min_cut: u16::MAX,
335            min_cut_tile: 0,
336            _reserved: 0,
337            total_time_us: 0,
338            tick,
339        }
340    }
341
342    /// Merge a tile report into the aggregate
343    pub fn merge(&mut self, report: &TileReport) {
344        self.total_vertices += report.num_vertices as u32;
345        self.total_edges += report.num_edges as u32;
346        self.total_components += report.num_components;
347        self.tiles_reporting += 1;
348
349        if report.status == TileStatus::Error {
350            self.tiles_with_errors += 1;
351        }
352
353        if report.rejected_count > 0 {
354            self.tiles_with_rejections += 1;
355        }
356
357        self.global_log_e += report.log_e_value as i64;
358
359        if report.witness.local_min_cut < self.global_min_cut {
360            self.global_min_cut = report.witness.local_min_cut;
361            self.min_cut_tile = report.tile_id;
362        }
363
364        self.total_time_us = self.total_time_us.max(report.tick_time_us as u32);
365    }
366
367    /// Check if all tiles completed successfully
368    pub fn all_complete(&self, expected_tiles: u16) -> bool {
369        self.tiles_reporting == expected_tiles && self.tiles_with_errors == 0
370    }
371
372    /// Get global e-value as approximate f64
373    pub fn global_e_value(&self) -> f64 {
374        let log2_val = (self.global_log_e as f64) / 65536.0;
375        libm::exp2(log2_val)
376    }
377}
378
379// Compile-time size assertions
380const _: () = assert!(
381    size_of::<TileReport>() == 64,
382    "TileReport must be exactly 64 bytes"
383);
384const _: () = assert!(
385    size_of::<WitnessFragment>() == 16,
386    "WitnessFragment must be 16 bytes"
387);
388
389#[cfg(test)]
390mod tests {
391    use super::*;
392
393    #[test]
394    fn test_tile_report_size() {
395        assert_eq!(size_of::<TileReport>(), 64);
396    }
397
398    #[test]
399    fn test_tile_report_alignment() {
400        assert_eq!(core::mem::align_of::<TileReport>(), 64);
401    }
402
403    #[test]
404    fn test_witness_fragment_size() {
405        assert_eq!(size_of::<WitnessFragment>(), 16);
406    }
407
408    #[test]
409    fn test_new_report() {
410        let report = TileReport::new(5);
411        assert_eq!(report.tile_id, 5);
412        assert_eq!(report.status, TileStatus::Idle);
413        assert_eq!(report.tick, 0);
414    }
415
416    #[test]
417    fn test_set_status() {
418        let mut report = TileReport::new(0);
419        report.set_complete();
420        assert_eq!(report.status, TileStatus::Complete);
421
422        report.set_error();
423        assert_eq!(report.status, TileStatus::Error);
424    }
425
426    #[test]
427    fn test_connected_flag() {
428        let mut report = TileReport::new(0);
429        assert!(!report.is_connected());
430
431        report.set_connected(true);
432        assert!(report.is_connected());
433
434        report.set_connected(false);
435        assert!(!report.is_connected());
436    }
437
438    #[test]
439    fn test_witness_fragment() {
440        let mut frag = WitnessFragment::new(10, 5, 20, 100);
441        assert_eq!(frag.seed, 10);
442        assert_eq!(frag.boundary_size, 5);
443        assert_eq!(frag.cardinality, 20);
444        assert_eq!(frag.local_min_cut, 100);
445
446        frag.compute_hash();
447        assert_ne!(frag.hash, 0);
448    }
449
450    #[test]
451    fn test_aggregated_report() {
452        let mut agg = AggregatedReport::new(1);
453
454        let mut report1 = TileReport::new(0);
455        report1.num_vertices = 50;
456        report1.num_edges = 100;
457        report1.witness.local_min_cut = 200;
458
459        let mut report2 = TileReport::new(1);
460        report2.num_vertices = 75;
461        report2.num_edges = 150;
462        report2.witness.local_min_cut = 150;
463
464        agg.merge(&report1);
465        agg.merge(&report2);
466
467        assert_eq!(agg.tiles_reporting, 2);
468        assert_eq!(agg.total_vertices, 125);
469        assert_eq!(agg.total_edges, 250);
470        assert_eq!(agg.global_min_cut, 150);
471        assert_eq!(agg.min_cut_tile, 1);
472    }
473
474    #[test]
475    fn test_tile_status_roundtrip() {
476        for i in 0..=7 {
477            let status = TileStatus::from(i);
478            assert_eq!(status as u8, i);
479        }
480    }
481
482    #[test]
483    fn test_processing_rate() {
484        let mut report = TileReport::new(0);
485        report.deltas_processed = 100;
486        report.tick_time_us = 50;
487
488        assert!((report.processing_rate() - 2.0).abs() < 0.01);
489    }
490}