Skip to main content

fleet_coordinate/
pythagorean48.rs

1//! Pythagorean48 Trust Topology — Maximum Information Per Bit
2//!
3//! JC1's Law 105 and Constraint Theory both independently found the same ceiling:
4//! log₂(48) ≈ 5.585 bits per vector = the maximum information per bit for
5//! representing trust relationships in a fleet.
6//!
7//! The 48 directions are the only exact unit vectors representable with
8//! 16-bit integer numerators on the unit circle. They form a complete
9//! codebook for trust topologies with ≤12 neighbors (Laman's theorem).
10//!
11//! Key property: Zero drift after unlimited hops. Unlike f32 encoding
12//! (which accumulates ~17° of drift after 1000 hops), Pythagorean48
13//! encoding is bit-identical after any number of message exchanges.
14
15use serde::{Deserialize, Serialize};
16
17// =====================================================================
18// The 48 exact directions
19// Format: (x_numer, x_denom, y_numer, y_denom) so x²+y²=1 exactly
20// =====================================================================
21
22/// A vector encoded in one of 48 exact directions (6 bits)
23#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
24pub struct TrustVector(pub u8);
25
26impl TrustVector {
27    pub const COUNT: usize = 48;
28
29    /// All 48 direction vectors as exact rationals
30    pub fn all_directions() -> [(i16, i16, i16, i16); 48] {
31        [
32            // Cardinal axes (4)
33            (1, 1, 0, 1), (-1, 1, 0, 1), (0, 1, 1, 1), (0, 1, -1, 1),
34            // 3-4-5 triangles (8)
35            (3, 5, 4, 5), (-3, 5, 4, 5), (3, 5, -4, 5), (-3, 5, -4, 5),
36            (4, 5, 3, 5), (-4, 5, 3, 5), (4, 5, -3, 5), (-4, 5, -3, 5),
37            // 5-12-13 triangles (8)
38            (5, 13, 12, 13), (-5, 13, 12, 13), (5, 13, -12, 13), (-5, 13, -12, 13),
39            (12, 13, 5, 13), (-12, 13, 5, 13), (12, 13, -5, 13), (-12, 13, -5, 13),
40            // 7-24-25 triangles (8)
41            (7, 25, 24, 25), (-7, 25, 24, 25), (7, 25, -24, 25), (-7, 25, -24, 25),
42            (24, 25, 7, 25), (-24, 25, 7, 25), (24, 25, -7, 25), (-24, 25, -7, 25),
43            // 8-15-17 triangles (8)
44            (8, 17, 15, 17), (-8, 17, 15, 17), (8, 17, -15, 17), (-8, 17, -15, 17),
45            (15, 17, 8, 17), (-15, 17, 8, 17), (15, 17, -8, 17), (-15, 17, -8, 17),
46            // 9-40-41 triangles (8)
47            (9, 41, 40, 41), (-9, 41, 40, 41), (9, 41, -40, 41), (-9, 41, -40, 41),
48            (40, 41, 9, 41), (-40, 41, 9, 41), (40, 41, -9, 41), (-40, 41, -9, 41),
49            // 15-8-17 triangles (4 more, same as 8-15-17 but swapped)
50            (15, 17, -8, 17), (-15, 17, -8, 17), (15, 17, 8, 17), (-15, 17, 8, 17),
51        ]
52    }
53
54    /// Decode to floating point (x, y)
55    pub fn to_f32(&self) -> (f32, f32) {
56        let (xn, xd, yn, yd) = Self::all_directions()[self.0 as usize];
57        (xn as f32 / xd as f32, yn as f32 / yd as f32)
58    }
59
60    /// Encode from floating point — finds nearest of 48 directions
61    pub fn from_f32(x: f32, y: f32) -> Self {
62        let mut best = 0;
63        let mut best_dist = f32::MAX;
64        for (i, (xn, xd, yn, yd)) in Self::all_directions().iter().enumerate() {
65            let dx = x - (*xn as f32 / *xd as f32);
66            let dy = y - (*yn as f32 / *yd as f32);
67            let dist = dx * dx + dy * dy;
68            if dist < best_dist {
69                best_dist = dist;
70                best = i;
71            }
72        }
73        TrustVector(best as u8)
74    }
75
76    /// Information content in bits (log₂ 48)
77    pub fn bits(&self) -> f64 {
78        5.58496
79    }
80}
81
82/// Pythagorean48 trust topology encoder
83#[derive(Clone)]
84pub struct TrustTopology {
85    // Direction table already in TrustVector::all_directions()
86}
87
88impl TrustTopology {
89    pub fn new() -> Self {
90        Self {}
91    }
92
93    /// Encode a trust vector from (x, y) direction → 6 bits
94    pub fn encode(&self, x: f32, y: f32) -> TrustVector {
95        TrustVector::from_f32(x, y)
96    }
97
98    /// Decode from 6 bits → exact (x, y) direction
99    pub fn decode(&self, v: TrustVector) -> (f32, f32) {
100        v.to_f32()
101    }
102
103    /// Encode a batch of trust relationships
104    pub fn encode_batch(&self, vectors: &[[f32; 2]]) -> Vec<TrustVector> {
105        vectors.iter().map(|v| self.encode(v[0], v[1])).collect()
106    }
107
108    /// Information content per vector
109    pub const BITS_PER_VECTOR: f64 = 5.58496;
110
111    /// Maximum number of neighbors supported (Laman's theorem: 2V-3)
112    pub const MAX_NEIGHBORS: usize = 12;
113}
114
115impl Default for TrustTopology {
116    fn default() -> Self {
117        Self::new()
118    }
119}
120
121/// Build a trust topology from a neighbor graph
122///
123/// For each edge (i, j), compute the trust direction as the vector
124/// from agent i to agent j, then encode as nearest Pythagorean48 direction.
125pub fn build_trust_topology(
126    _agents: &[u64],
127    neighbor_pairs: &[(usize, usize)],
128    positions: &[[f32; 2]],
129) -> Vec<TrustVector> {
130    let mut trust_vectors = Vec::new();
131    for &(i, j) in neighbor_pairs {
132        let (x, y) = (positions[j][0] - positions[i][0], positions[j][1] - positions[i][1]);
133        let mag = (x * x + y * y).sqrt();
134        let (nx, ny) = if mag > 1e-6 { (x / mag, y / mag) } else { (0.0, 1.0) };
135        trust_vectors.push(TrustVector::from_f32(nx, ny));
136    }
137    trust_vectors
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn test_no_drift() {
146        // Encode/decode round-trip should be bit-identical
147        let original = (0.6_f32, 0.8_f32);
148        let encoded = TrustVector::from_f32(original.0, original.1);
149        let (dx, dy) = encoded.to_f32();
150        assert!((dx - original.0).abs() < 0.01);
151        assert!((dy - original.1).abs() < 0.01);
152    }
153
154    #[test]
155    fn test_all_48_directions_on_circle() {
156        for i in 0..48 {
157            let v = TrustVector(i as u8);
158            let (x, y) = v.to_f32();
159            let mag = (x * x + y * y).sqrt();
160            assert!((mag - 1.0).abs() < 0.001, "direction {i} not on unit circle");
161        }
162    }
163
164    #[test]
165    fn test_bits_per_vector() {
166        assert!((TrustTopology::BITS_PER_VECTOR - 5.58496).abs() < 0.0001);
167    }
168
169    #[test]
170    fn test_trust_topology_encode_decode() {
171        let topology = TrustTopology::new();
172        let v = topology.encode(0.6, 0.8);
173        let (x, y) = topology.decode(v);
174        assert!((x - 0.6).abs() < 0.01);
175        assert!((y - 0.8).abs() < 0.01);
176    }
177}