|
| 1 | +//! CausalEdge64 integration for Jina token triples. |
| 2 | +//! |
| 3 | +//! Every SPO triple from token palette indices packs into one u64 |
| 4 | +//! with NARS truth, Pearl hierarchy, plasticity, and temporal index. |
| 5 | +
|
| 6 | +use super::codec::JinaPalette; |
| 7 | + |
| 8 | +/// Pack an SPO triple + metadata into a CausalEdge64 (one u64). |
| 9 | +/// |
| 10 | +/// Bit layout: |
| 11 | +/// ```text |
| 12 | +/// [S:8][P:8][O:8][freq:8][conf:8][pearl:3][dir:3][inf:3][plast:3][temporal:12] |
| 13 | +/// ``` |
| 14 | +#[inline(always)] |
| 15 | +pub fn pack_edge( |
| 16 | + s_palette: u8, |
| 17 | + p_palette: u8, |
| 18 | + o_palette: u8, |
| 19 | + frequency: f32, |
| 20 | + confidence: f32, |
| 21 | + pearl_mask: u8, |
| 22 | + temporal: u16, |
| 23 | +) -> u64 { |
| 24 | + let f_u8 = (frequency.clamp(0.0, 1.0) * 255.0) as u8; |
| 25 | + let c_u8 = (confidence.clamp(0.0, 1.0) * 255.0) as u8; |
| 26 | + |
| 27 | + (s_palette as u64) |
| 28 | + | ((p_palette as u64) << 8) |
| 29 | + | ((o_palette as u64) << 16) |
| 30 | + | ((f_u8 as u64) << 24) |
| 31 | + | ((c_u8 as u64) << 32) |
| 32 | + | (((pearl_mask & 0x7) as u64) << 40) |
| 33 | + | (0b100u64 << 43) // default direction: S→O |
| 34 | + | (0b001u64 << 46) // default inference: observation |
| 35 | + | (0b100u64 << 49) // default plasticity: hot |
| 36 | + | (((temporal & 0xFFF) as u64) << 52) |
| 37 | +} |
| 38 | + |
| 39 | +/// Unpack S palette index from a CausalEdge64. |
| 40 | +#[inline(always)] |
| 41 | +pub fn edge_s(edge: u64) -> u8 { |
| 42 | + (edge & 0xFF) as u8 |
| 43 | +} |
| 44 | + |
| 45 | +/// Unpack P palette index. |
| 46 | +#[inline(always)] |
| 47 | +pub fn edge_p(edge: u64) -> u8 { |
| 48 | + ((edge >> 8) & 0xFF) as u8 |
| 49 | +} |
| 50 | + |
| 51 | +/// Unpack O palette index. |
| 52 | +#[inline(always)] |
| 53 | +pub fn edge_o(edge: u64) -> u8 { |
| 54 | + ((edge >> 16) & 0xFF) as u8 |
| 55 | +} |
| 56 | + |
| 57 | +/// Unpack NARS frequency [0.0, 1.0]. |
| 58 | +#[inline(always)] |
| 59 | +pub fn edge_freq(edge: u64) -> f32 { |
| 60 | + ((edge >> 24) & 0xFF) as f32 / 255.0 |
| 61 | +} |
| 62 | + |
| 63 | +/// Unpack NARS confidence [0.0, 1.0]. |
| 64 | +#[inline(always)] |
| 65 | +pub fn edge_conf(edge: u64) -> f32 { |
| 66 | + ((edge >> 32) & 0xFF) as f32 / 255.0 |
| 67 | +} |
| 68 | + |
| 69 | +/// Unpack Pearl mask (3 bits: S=4, P=2, O=1). |
| 70 | +#[inline(always)] |
| 71 | +pub fn edge_pearl(edge: u64) -> u8 { |
| 72 | + ((edge >> 40) & 0x7) as u8 |
| 73 | +} |
| 74 | + |
| 75 | +/// Unpack temporal index (12 bits: 0-4095). |
| 76 | +#[inline(always)] |
| 77 | +pub fn edge_temporal(edge: u64) -> u16 { |
| 78 | + ((edge >> 52) & 0xFFF) as u16 |
| 79 | +} |
| 80 | + |
| 81 | +/// Unpack plasticity (3 bits). |
| 82 | +#[inline(always)] |
| 83 | +pub fn edge_plasticity(edge: u64) -> u8 { |
| 84 | + ((edge >> 49) & 0x7) as u8 |
| 85 | +} |
| 86 | + |
| 87 | +/// Pearl-masked distance between two edges using palette distance table. |
| 88 | +/// |
| 89 | +/// Only the planes selected by `mask` contribute to the distance. |
| 90 | +/// mask=0b111 (SPO): full distance. mask=0b011 (_PO): interventional. |
| 91 | +#[inline] |
| 92 | +pub fn causal_distance(edge_a: u64, edge_b: u64, palette: &JinaPalette, mask: u8) -> u32 { |
| 93 | + let mut d = 0u32; |
| 94 | + if mask & 0b100 != 0 { |
| 95 | + d += palette.distance_table[edge_s(edge_a) as usize][edge_s(edge_b) as usize] as u32; |
| 96 | + } |
| 97 | + if mask & 0b010 != 0 { |
| 98 | + d += palette.distance_table[edge_p(edge_a) as usize][edge_p(edge_b) as usize] as u32; |
| 99 | + } |
| 100 | + if mask & 0b001 != 0 { |
| 101 | + d += palette.distance_table[edge_o(edge_a) as usize][edge_o(edge_b) as usize] as u32; |
| 102 | + } |
| 103 | + d |
| 104 | +} |
| 105 | + |
| 106 | +/// NARS revision: combine two truth values (old + new evidence). |
| 107 | +#[inline] |
| 108 | +pub fn nars_revision(old_freq: f32, old_conf: f32, new_freq: f32, new_conf: f32) -> (f32, f32) { |
| 109 | + let w1 = old_conf / (1.0 - old_conf + 1e-9); |
| 110 | + let w2 = new_conf / (1.0 - new_conf + 1e-9); |
| 111 | + let w = w1 + w2; |
| 112 | + let f = (w1 * old_freq + w2 * new_freq) / (w + 1e-9); |
| 113 | + let c = w / (w + 1.0); |
| 114 | + (f.clamp(0.0, 1.0), c.clamp(0.0, 0.999)) |
| 115 | +} |
| 116 | + |
| 117 | +/// Update an edge with new evidence via NARS revision. |
| 118 | +/// Returns the updated edge (new u64 with revised truth values). |
| 119 | +#[inline] |
| 120 | +pub fn revise_edge(edge: u64, evidence_freq: f32, evidence_conf: f32) -> u64 { |
| 121 | + let old_f = edge_freq(edge); |
| 122 | + let old_c = edge_conf(edge); |
| 123 | + let (new_f, new_c) = nars_revision(old_f, old_c, evidence_freq, evidence_conf); |
| 124 | + |
| 125 | + // Clear old truth bits, set new ones |
| 126 | + let mask = !(0xFF_u64 << 24 | 0xFF_u64 << 32); |
| 127 | + let cleared = edge & mask; |
| 128 | + let f_u8 = (new_f * 255.0) as u64; |
| 129 | + let c_u8 = (new_c * 255.0) as u64; |
| 130 | + cleared | (f_u8 << 24) | (c_u8 << 32) |
| 131 | +} |
| 132 | + |
| 133 | +/// NARS expectation: e = c × (f - 0.5) + 0.5. |
| 134 | +#[inline(always)] |
| 135 | +pub fn edge_expectation(edge: u64) -> f32 { |
| 136 | + let f = edge_freq(edge); |
| 137 | + let c = edge_conf(edge); |
| 138 | + c * (f - 0.5) + 0.5 |
| 139 | +} |
| 140 | + |
| 141 | +#[cfg(test)] |
| 142 | +mod tests { |
| 143 | + use super::*; |
| 144 | + |
| 145 | + #[test] |
| 146 | + fn test_pack_unpack_roundtrip() { |
| 147 | + let edge = pack_edge(42, 187, 91, 0.75, 0.60, 0b111, 1234); |
| 148 | + assert_eq!(edge_s(edge), 42); |
| 149 | + assert_eq!(edge_p(edge), 187); |
| 150 | + assert_eq!(edge_o(edge), 91); |
| 151 | + assert!((edge_freq(edge) - 0.75).abs() < 0.01); |
| 152 | + assert!((edge_conf(edge) - 0.60).abs() < 0.01); |
| 153 | + assert_eq!(edge_pearl(edge), 0b111); |
| 154 | + assert_eq!(edge_temporal(edge), 1234); |
| 155 | + } |
| 156 | + |
| 157 | + #[test] |
| 158 | + fn test_temporal_sort() { |
| 159 | + let e1 = pack_edge(0, 0, 0, 0.5, 0.3, 0b111, 100); |
| 160 | + let e2 = pack_edge(0, 0, 0, 0.5, 0.3, 0b111, 200); |
| 161 | + let e3 = pack_edge(0, 0, 0, 0.5, 0.3, 0b111, 50); |
| 162 | + let mut edges = vec![e2, e3, e1]; |
| 163 | + edges.sort(); // native u64 sort |
| 164 | + assert_eq!(edge_temporal(edges[0]), 50); |
| 165 | + assert_eq!(edge_temporal(edges[1]), 100); |
| 166 | + assert_eq!(edge_temporal(edges[2]), 200); |
| 167 | + } |
| 168 | + |
| 169 | + #[test] |
| 170 | + fn test_nars_revision_increases_confidence() { |
| 171 | + let edge = pack_edge(42, 187, 91, 0.5, 0.1, 0b111, 0); |
| 172 | + let revised = revise_edge(edge, 0.8, 0.3); |
| 173 | + assert!(edge_conf(revised) > edge_conf(edge)); |
| 174 | + } |
| 175 | + |
| 176 | + #[test] |
| 177 | + fn test_nars_revision_10_observations() { |
| 178 | + let mut edge = pack_edge(42, 187, 91, 0.5, 0.1, 0b111, 0); |
| 179 | + for _ in 0..10 { |
| 180 | + edge = revise_edge(edge, 0.8, 0.3); |
| 181 | + } |
| 182 | + assert!(edge_conf(edge) > 0.7, "10 observations should give conf > 0.7"); |
| 183 | + assert!(edge_freq(edge) > 0.6, "positive evidence should push freq up"); |
| 184 | + } |
| 185 | + |
| 186 | + #[test] |
| 187 | + fn test_expectation() { |
| 188 | + let edge = pack_edge(0, 0, 0, 0.9, 0.8, 0b111, 0); |
| 189 | + let exp = edge_expectation(edge); |
| 190 | + // e = 0.8 * (0.9 - 0.5) + 0.5 = 0.8 * 0.4 + 0.5 = 0.82 |
| 191 | + assert!((exp - 0.82).abs() < 0.02); |
| 192 | + } |
| 193 | + |
| 194 | + #[test] |
| 195 | + fn test_pearl_mask_distance() { |
| 196 | + use super::super::codec::{Base17Token, JinaPalette, BASE_DIM}; |
| 197 | + |
| 198 | + // Build minimal palette for testing |
| 199 | + let tokens: Vec<Base17Token> = (0..256) |
| 200 | + .map(|i| { |
| 201 | + let mut dims = [0i16; BASE_DIM]; |
| 202 | + dims[0] = (i as i16) * 100; |
| 203 | + dims[1] = (i as i16) * 50; |
| 204 | + Base17Token { dims } |
| 205 | + }) |
| 206 | + .collect(); |
| 207 | + let palette = JinaPalette::build(&tokens, 5); |
| 208 | + |
| 209 | + let e1 = pack_edge(10, 20, 30, 0.5, 0.3, 0b111, 0); |
| 210 | + let e2 = pack_edge(40, 50, 60, 0.5, 0.3, 0b111, 0); |
| 211 | + |
| 212 | + let d_spo = causal_distance(e1, e2, &palette, 0b111); // full |
| 213 | + let d_po = causal_distance(e1, e2, &palette, 0b011); // remove S |
| 214 | + let d_so = causal_distance(e1, e2, &palette, 0b101); // remove P |
| 215 | + let d_s = causal_distance(e1, e2, &palette, 0b100); // S only |
| 216 | + |
| 217 | + assert!(d_spo > d_po, "removing S should reduce distance"); |
| 218 | + assert!(d_spo > d_so, "removing P should reduce distance"); |
| 219 | + assert_eq!(d_spo, d_s + d_po - causal_distance(e1, e2, &palette, 0b000), |
| 220 | + "planes should be additive (within rounding)... actually just check ordering"); |
| 221 | + assert!(d_s > 0, "different S should have positive distance"); |
| 222 | + } |
| 223 | +} |
0 commit comments