cognitum_gate_kernel/
delta.rs

1//! Delta types for incremental graph updates
2//!
3//! Defines the message types that tiles receive from the coordinator.
4//! All types are `#[repr(C)]` for FFI compatibility and fixed-size
5//! for deterministic memory allocation.
6
7#![allow(missing_docs)]
8
9use core::mem::size_of;
10
11/// Compact vertex identifier (16-bit for tile-local addressing)
12pub type TileVertexId = u16;
13
14/// Compact edge identifier (16-bit for tile-local addressing)
15pub type TileEdgeId = u16;
16
17/// Fixed-point weight (16-bit, 0.01 precision)
18/// Actual weight = raw_weight / 100.0
19pub type FixedWeight = u16;
20
21/// Convert fixed-point weight to f32
22#[inline(always)]
23pub const fn weight_to_f32(w: FixedWeight) -> f32 {
24    (w as f32) / 100.0
25}
26
27/// Convert f32 weight to fixed-point (saturating)
28#[inline(always)]
29pub const fn f32_to_weight(w: f32) -> FixedWeight {
30    let scaled = (w * 100.0) as i32;
31    if scaled < 0 {
32        0
33    } else if scaled > 65535 {
34        65535
35    } else {
36        scaled as u16
37    }
38}
39
40/// Delta operation tag
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42#[repr(u8)]
43pub enum DeltaTag {
44    /// No operation (padding/sentinel)
45    Nop = 0,
46    /// Add an edge to the graph
47    EdgeAdd = 1,
48    /// Remove an edge from the graph
49    EdgeRemove = 2,
50    /// Update the weight of an existing edge
51    WeightUpdate = 3,
52    /// Observation for evidence accumulation
53    Observation = 4,
54    /// Batch boundary marker
55    BatchEnd = 5,
56    /// Checkpoint request
57    Checkpoint = 6,
58    /// Reset tile state
59    Reset = 7,
60}
61
62impl From<u8> for DeltaTag {
63    fn from(v: u8) -> Self {
64        match v {
65            1 => DeltaTag::EdgeAdd,
66            2 => DeltaTag::EdgeRemove,
67            3 => DeltaTag::WeightUpdate,
68            4 => DeltaTag::Observation,
69            5 => DeltaTag::BatchEnd,
70            6 => DeltaTag::Checkpoint,
71            7 => DeltaTag::Reset,
72            _ => DeltaTag::Nop,
73        }
74    }
75}
76
77/// Edge addition delta
78#[derive(Debug, Clone, Copy, Default)]
79#[repr(C)]
80pub struct EdgeAdd {
81    /// Source vertex (tile-local ID)
82    pub source: TileVertexId,
83    /// Target vertex (tile-local ID)
84    pub target: TileVertexId,
85    /// Edge weight (fixed-point)
86    pub weight: FixedWeight,
87    /// Edge flags (reserved for future use)
88    pub flags: u16,
89}
90
91impl EdgeAdd {
92    /// Create a new edge addition
93    #[inline]
94    pub const fn new(source: TileVertexId, target: TileVertexId, weight: FixedWeight) -> Self {
95        Self {
96            source,
97            target,
98            weight,
99            flags: 0,
100        }
101    }
102
103    /// Create from f32 weight
104    #[inline]
105    pub const fn with_f32_weight(source: TileVertexId, target: TileVertexId, weight: f32) -> Self {
106        Self::new(source, target, f32_to_weight(weight))
107    }
108}
109
110/// Edge removal delta
111#[derive(Debug, Clone, Copy, Default)]
112#[repr(C)]
113pub struct EdgeRemove {
114    /// Source vertex (tile-local ID)
115    pub source: TileVertexId,
116    /// Target vertex (tile-local ID)
117    pub target: TileVertexId,
118    /// Reserved padding for alignment
119    pub _reserved: u32,
120}
121
122impl EdgeRemove {
123    /// Create a new edge removal
124    #[inline]
125    pub const fn new(source: TileVertexId, target: TileVertexId) -> Self {
126        Self {
127            source,
128            target,
129            _reserved: 0,
130        }
131    }
132}
133
134/// Weight update delta
135#[derive(Debug, Clone, Copy, Default)]
136#[repr(C)]
137pub struct WeightUpdate {
138    /// Source vertex (tile-local ID)
139    pub source: TileVertexId,
140    /// Target vertex (tile-local ID)
141    pub target: TileVertexId,
142    /// New weight (fixed-point)
143    pub new_weight: FixedWeight,
144    /// Delta mode: 0 = absolute, 1 = relative add, 2 = relative multiply
145    pub mode: u8,
146    /// Reserved padding
147    pub _reserved: u8,
148}
149
150impl WeightUpdate {
151    /// Absolute weight update mode
152    pub const MODE_ABSOLUTE: u8 = 0;
153    /// Relative addition mode
154    pub const MODE_ADD: u8 = 1;
155    /// Relative multiply mode (fixed-point: value/100)
156    pub const MODE_MULTIPLY: u8 = 2;
157
158    /// Create an absolute weight update
159    #[inline]
160    pub const fn absolute(source: TileVertexId, target: TileVertexId, weight: FixedWeight) -> Self {
161        Self {
162            source,
163            target,
164            new_weight: weight,
165            mode: Self::MODE_ABSOLUTE,
166            _reserved: 0,
167        }
168    }
169
170    /// Create a relative weight addition
171    #[inline]
172    pub const fn add(source: TileVertexId, target: TileVertexId, delta: FixedWeight) -> Self {
173        Self {
174            source,
175            target,
176            new_weight: delta,
177            mode: Self::MODE_ADD,
178            _reserved: 0,
179        }
180    }
181}
182
183/// Observation for evidence accumulation
184///
185/// Represents a measurement or event that affects the e-value calculation.
186#[derive(Debug, Clone, Copy, Default)]
187#[repr(C)]
188pub struct Observation {
189    /// Vertex or region this observation applies to
190    pub vertex: TileVertexId,
191    /// Observation type/category
192    pub obs_type: u8,
193    /// Observation flags
194    pub flags: u8,
195    /// Observation value (interpretation depends on obs_type)
196    pub value: u32,
197}
198
199impl Observation {
200    /// Observation type: connectivity evidence
201    pub const TYPE_CONNECTIVITY: u8 = 0;
202    /// Observation type: cut membership evidence
203    pub const TYPE_CUT_MEMBERSHIP: u8 = 1;
204    /// Observation type: flow evidence
205    pub const TYPE_FLOW: u8 = 2;
206    /// Observation type: witness evidence
207    pub const TYPE_WITNESS: u8 = 3;
208
209    /// Create a connectivity observation
210    #[inline]
211    pub const fn connectivity(vertex: TileVertexId, connected: bool) -> Self {
212        Self {
213            vertex,
214            obs_type: Self::TYPE_CONNECTIVITY,
215            flags: if connected { 1 } else { 0 },
216            value: 0,
217        }
218    }
219
220    /// Create a cut membership observation
221    #[inline]
222    pub const fn cut_membership(vertex: TileVertexId, side: u8, confidence: u16) -> Self {
223        Self {
224            vertex,
225            obs_type: Self::TYPE_CUT_MEMBERSHIP,
226            flags: side,
227            value: confidence as u32,
228        }
229    }
230}
231
232/// Unified delta message (8 bytes, cache-aligned for batching)
233///
234/// Tagged union for all delta types. The layout is optimized for
235/// WASM memory access patterns.
236#[derive(Clone, Copy)]
237#[repr(C)]
238pub union DeltaPayload {
239    /// Edge addition payload
240    pub edge_add: EdgeAdd,
241    /// Edge removal payload
242    pub edge_remove: EdgeRemove,
243    /// Weight update payload
244    pub weight_update: WeightUpdate,
245    /// Observation payload
246    pub observation: Observation,
247    /// Raw bytes for custom payloads
248    pub raw: [u8; 8],
249}
250
251impl Default for DeltaPayload {
252    fn default() -> Self {
253        Self { raw: [0u8; 8] }
254    }
255}
256
257/// Complete delta message with tag
258#[derive(Clone, Copy)]
259#[repr(C, align(16))]
260pub struct Delta {
261    /// Delta operation tag
262    pub tag: DeltaTag,
263    /// Sequence number for ordering
264    pub sequence: u8,
265    /// Source tile ID (for cross-tile deltas)
266    pub source_tile: u8,
267    /// Reserved for future use
268    pub _reserved: u8,
269    /// Timestamp (lower 32 bits of tick counter)
270    pub timestamp: u32,
271    /// Delta payload
272    pub payload: DeltaPayload,
273}
274
275impl Default for Delta {
276    fn default() -> Self {
277        Self {
278            tag: DeltaTag::Nop,
279            sequence: 0,
280            source_tile: 0,
281            _reserved: 0,
282            timestamp: 0,
283            payload: DeltaPayload::default(),
284        }
285    }
286}
287
288impl Delta {
289    /// Create a NOP delta
290    #[inline]
291    pub const fn nop() -> Self {
292        Self {
293            tag: DeltaTag::Nop,
294            sequence: 0,
295            source_tile: 0,
296            _reserved: 0,
297            timestamp: 0,
298            payload: DeltaPayload { raw: [0u8; 8] },
299        }
300    }
301
302    /// Create an edge add delta
303    #[inline]
304    pub fn edge_add(source: TileVertexId, target: TileVertexId, weight: FixedWeight) -> Self {
305        Self {
306            tag: DeltaTag::EdgeAdd,
307            sequence: 0,
308            source_tile: 0,
309            _reserved: 0,
310            timestamp: 0,
311            payload: DeltaPayload {
312                edge_add: EdgeAdd::new(source, target, weight),
313            },
314        }
315    }
316
317    /// Create an edge remove delta
318    #[inline]
319    pub fn edge_remove(source: TileVertexId, target: TileVertexId) -> Self {
320        Self {
321            tag: DeltaTag::EdgeRemove,
322            sequence: 0,
323            source_tile: 0,
324            _reserved: 0,
325            timestamp: 0,
326            payload: DeltaPayload {
327                edge_remove: EdgeRemove::new(source, target),
328            },
329        }
330    }
331
332    /// Create a weight update delta
333    #[inline]
334    pub fn weight_update(source: TileVertexId, target: TileVertexId, weight: FixedWeight) -> Self {
335        Self {
336            tag: DeltaTag::WeightUpdate,
337            sequence: 0,
338            source_tile: 0,
339            _reserved: 0,
340            timestamp: 0,
341            payload: DeltaPayload {
342                weight_update: WeightUpdate::absolute(source, target, weight),
343            },
344        }
345    }
346
347    /// Create an observation delta
348    #[inline]
349    pub fn observation(obs: Observation) -> Self {
350        Self {
351            tag: DeltaTag::Observation,
352            sequence: 0,
353            source_tile: 0,
354            _reserved: 0,
355            timestamp: 0,
356            payload: DeltaPayload { observation: obs },
357        }
358    }
359
360    /// Create a batch end marker
361    #[inline]
362    pub const fn batch_end() -> Self {
363        Self {
364            tag: DeltaTag::BatchEnd,
365            sequence: 0,
366            source_tile: 0,
367            _reserved: 0,
368            timestamp: 0,
369            payload: DeltaPayload { raw: [0u8; 8] },
370        }
371    }
372
373    /// Check if this is a NOP
374    #[inline]
375    pub const fn is_nop(&self) -> bool {
376        matches!(self.tag, DeltaTag::Nop)
377    }
378
379    /// Get the edge add payload (unsafe: caller must verify tag)
380    #[inline]
381    pub unsafe fn get_edge_add(&self) -> &EdgeAdd {
382        unsafe { &self.payload.edge_add }
383    }
384
385    /// Get the edge remove payload (unsafe: caller must verify tag)
386    #[inline]
387    pub unsafe fn get_edge_remove(&self) -> &EdgeRemove {
388        unsafe { &self.payload.edge_remove }
389    }
390
391    /// Get the weight update payload (unsafe: caller must verify tag)
392    #[inline]
393    pub unsafe fn get_weight_update(&self) -> &WeightUpdate {
394        unsafe { &self.payload.weight_update }
395    }
396
397    /// Get the observation payload (unsafe: caller must verify tag)
398    #[inline]
399    pub unsafe fn get_observation(&self) -> &Observation {
400        unsafe { &self.payload.observation }
401    }
402}
403
404// Compile-time size assertions
405const _: () = assert!(size_of::<EdgeAdd>() == 8, "EdgeAdd must be 8 bytes");
406const _: () = assert!(size_of::<EdgeRemove>() == 8, "EdgeRemove must be 8 bytes");
407const _: () = assert!(size_of::<WeightUpdate>() == 8, "WeightUpdate must be 8 bytes");
408const _: () = assert!(size_of::<Observation>() == 8, "Observation must be 8 bytes");
409const _: () = assert!(size_of::<Delta>() == 16, "Delta must be 16 bytes");
410
411#[cfg(test)]
412mod tests {
413    use super::*;
414
415    #[test]
416    fn test_weight_conversion() {
417        assert_eq!(weight_to_f32(100), 1.0);
418        assert_eq!(weight_to_f32(50), 0.5);
419        assert_eq!(weight_to_f32(0), 0.0);
420
421        assert_eq!(f32_to_weight(1.0), 100);
422        assert_eq!(f32_to_weight(0.5), 50);
423        assert_eq!(f32_to_weight(0.0), 0);
424    }
425
426    #[test]
427    fn test_delta_tag_roundtrip() {
428        for i in 0..=7 {
429            let tag = DeltaTag::from(i);
430            assert_eq!(tag as u8, i);
431        }
432    }
433
434    #[test]
435    fn test_edge_add_creation() {
436        let ea = EdgeAdd::new(1, 2, 150);
437        assert_eq!(ea.source, 1);
438        assert_eq!(ea.target, 2);
439        assert_eq!(ea.weight, 150);
440    }
441
442    #[test]
443    fn test_delta_edge_add() {
444        let delta = Delta::edge_add(5, 10, 200);
445        assert_eq!(delta.tag, DeltaTag::EdgeAdd);
446        unsafe {
447            let ea = delta.get_edge_add();
448            assert_eq!(ea.source, 5);
449            assert_eq!(ea.target, 10);
450            assert_eq!(ea.weight, 200);
451        }
452    }
453
454    #[test]
455    fn test_observation_creation() {
456        let obs = Observation::connectivity(42, true);
457        assert_eq!(obs.vertex, 42);
458        assert_eq!(obs.obs_type, Observation::TYPE_CONNECTIVITY);
459        assert_eq!(obs.flags, 1);
460    }
461}