Skip to main content

fleet_math_c/
lib.rs

1//! Fleet math library — C-compatible math primitives for fleet operations.
2//!
3//! Provides Eisenstein integer operations, Laman rigidity checks, holonomy
4//! verification, Manhattan distance for vector search, and Pythagorean-48
5//! directional encoding. All public functions are `#[no_mangle] extern "C"`.
6
7const SQRT_3_F32: f32 = 1.7320508_f32;
8
9// ---------------------------------------------------------------------------
10// Existing: Eisenstein lattice snapping (f32)
11// ---------------------------------------------------------------------------
12
13/// Result of snapping a point to the nearest Eisenstein integer.
14#[derive(Debug, Clone)]
15#[repr(C)]
16pub struct SnapResult {
17    pub snap_a: i32,
18    pub snap_b: i32,
19    pub error: f32,
20    pub chamber: i32,
21}
22
23/// Snap a single (x, y) point to the nearest Eisenstein integer.
24///
25/// Searches the 9 Eisenstein integer candidates around the continuous
26/// coordinates and picks the one with the smallest Euclidean distance.
27pub fn snap(x: f32, y: f32) -> SnapResult {
28    let b_approx = (2.0 * y) / SQRT_3_F32;
29    let a_approx = x + y / SQRT_3_F32;
30
31    let b0 = b_approx.floor() as i32;
32    let a0 = a_approx.floor() as i32;
33
34    let mut best_a = a0;
35    let mut best_b = b0;
36    let mut best_err = f32::MAX;
37
38    for da in -1i32..=1 {
39        for db in -1i32..=1 {
40            let a = a0 + da;
41            let b = b0 + db;
42            let px = a as f32 - b as f32 * 0.5;
43            let py = b as f32 * SQRT_3_F32 * 0.5;
44            let dx = x - px;
45            let dy = y - py;
46            let err = dx * dx + dy * dy;
47            if err < best_err {
48                best_err = err;
49                best_a = a;
50                best_b = b;
51            }
52        }
53    }
54
55    let best_err = best_err.sqrt();
56
57    let px = best_a as f32 - best_b as f32 * 0.5;
58    let py = best_b as f32 * SQRT_3_F32 * 0.5;
59    let dx = x - px;
60    let dy = y - py;
61
62    let chamber = if best_err < 1e-6 {
63        0
64    } else {
65        let angle = dy.atan2(dx);
66        let sector = ((angle + std::f32::consts::PI) / (std::f32::consts::PI / 3.0)).floor() as i32;
67        (sector % 6).min(5).max(0)
68    };
69
70    SnapResult {
71        snap_a: best_a,
72        snap_b: best_b,
73        error: best_err,
74        chamber,
75    }
76}
77
78/// Batch snap for interleaved (x, y) pairs.
79///
80/// `flat` must have even length: [x0, y0, x1, y1, ...].
81pub fn batch_snap(flat: &[f32]) -> Vec<SnapResult> {
82    assert!(flat.len() % 2 == 0, "flat must have even length");
83    flat.chunks_exact(2)
84        .map(|chunk| snap(chunk[0], chunk[1]))
85        .collect()
86}
87
88// ---------------------------------------------------------------------------
89// C-compatible API — Eisenstein norm
90// ---------------------------------------------------------------------------
91
92/// Eisenstein integer norm: N(a + bω) = a² − ab + b²
93///
94/// This is always non-negative and equals zero only for (0, 0).
95#[no_mangle]
96pub extern "C" fn fleet_eisenstein_norm(a: i32, b: i32) -> i64 {
97    let a = a as i64;
98    let b = b as i64;
99    a * a - a * b + b * b
100}
101
102// ---------------------------------------------------------------------------
103// C-compatible API — Laman rigidity
104// ---------------------------------------------------------------------------
105
106/// Minimum edges for a Laman-rigid graph on `vertices` vertices: 2V − 3.
107///
108/// Returns 0 for degenerate inputs (V < 2).
109#[no_mangle]
110pub extern "C" fn fleet_laman_edges(vertices: i32) -> i32 {
111    if vertices < 2 {
112        return 0;
113    }
114    2 * vertices - 3
115}
116
117/// Check whether a graph with `vertices` and `edges` satisfies the Laman
118/// rigidity condition E ≥ 2V − 3.
119///
120/// Returns 1 (true) if rigid, 0 (false) otherwise.
121#[no_mangle]
122pub extern "C" fn fleet_is_rigid(vertices: i32, edges: i32) -> bool {
123    if vertices < 2 {
124        return edges >= 0;
125    }
126    edges >= 2 * vertices - 3
127}
128
129// ---------------------------------------------------------------------------
130// C-compatible API — Holonomy check
131// ---------------------------------------------------------------------------
132
133/// Check if the product of integer transforms equals the identity (1).
134///
135/// Each element of `transforms` is treated as a multiplicative factor.
136/// The product must equal exactly 1 for the check to pass.
137///
138/// # Safety
139/// `transforms` must point to `len` valid `i64` values.
140#[no_mangle]
141pub extern "C" fn fleet_holonomy_check(transforms: *const i64, len: usize) -> bool {
142    if transforms.is_null() || len == 0 {
143        // Empty product is identity by convention
144        return true;
145    }
146    let slice = unsafe { std::slice::from_raw_parts(transforms, len) };
147    let product: i64 = slice.iter().product();
148    product == 1
149}
150
151// ---------------------------------------------------------------------------
152// C-compatible API — Manhattan distance
153// ---------------------------------------------------------------------------
154
155/// Compute the Manhattan (L1) distance between two integer vectors.
156///
157/// Sum of absolute differences: Σ|a[i] − b[i]|.
158///
159/// # Safety
160/// `a` and `b` must each point to `len` valid `i32` values.
161#[no_mangle]
162pub extern "C" fn fleet_manhattan_distance(a: *const i32, b: *const i32, len: usize) -> i64 {
163    if a.is_null() || b.is_null() || len == 0 {
164        return 0;
165    }
166    let sa = unsafe { std::slice::from_raw_parts(a, len) };
167    let sb = unsafe { std::slice::from_raw_parts(b, len) };
168    sa.iter()
169        .zip(sb.iter())
170        .map(|(&x, &y)| (x as i64 - y as i64).abs())
171        .sum()
172}
173
174// ---------------------------------------------------------------------------
175// C-compatible API — Pythagorean-48 directional encoding
176// ---------------------------------------------------------------------------
177
178/// Number of discrete directions in Pythagorean-48 encoding.
179pub const PYTHAGOREAN_48_DIRECTIONS: usize = 48;
180
181/// Quantize a 2D angle (from x, y components) into one of 48 discrete
182/// directions.
183///
184/// Each direction spans 7.5° (360°/48). Returns an integer in [0, 47]
185/// where 0 corresponds to the positive x-axis.
186///
187/// Returns 0 if (x, y) is the zero vector.
188#[no_mangle]
189pub extern "C" fn fleet_pythagorean48_encode(x: f64, y: f64) -> i32 {
190    if x == 0.0 && y == 0.0 {
191        return 0;
192    }
193    let angle = y.atan2(x); // [-π, π]
194    // Map to [0, 2π)
195    let angle = if angle < 0.0 { angle + 2.0 * std::f64::consts::PI } else { angle };
196    let sector = (angle / (2.0 * std::f64::consts::PI / 48.0)).floor() as i32;
197    sector.min(47).max(0)
198}
199
200// ---------------------------------------------------------------------------
201// Tests
202// ---------------------------------------------------------------------------
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    // -- Existing snap tests --
209
210    #[test]
211    fn test_snap_origin() {
212        let r = snap(0.0, 0.0);
213        assert_eq!(r.snap_a, 0);
214        assert_eq!(r.snap_b, 0);
215        assert!(r.error < 0.001);
216    }
217
218    #[test]
219    fn test_batch_snap() {
220        let flat = [0.0, 0.0, 1.0, 0.0, 0.0, 1.0];
221        let results = batch_snap(&flat);
222        assert_eq!(results.len(), 3);
223        assert_eq!(results[0].snap_a, 0);
224        assert_eq!(results[0].snap_b, 0);
225    }
226
227    // -- Eisenstein norm tests --
228
229    #[test]
230    fn test_eisenstein_norm_zero() {
231        assert_eq!(fleet_eisenstein_norm(0, 0), 0);
232    }
233
234    #[test]
235    fn test_eisenstein_norm_unit() {
236        // N(1, 0) = 1
237        assert_eq!(fleet_eisenstein_norm(1, 0), 1);
238        // N(0, 1) = 1
239        assert_eq!(fleet_eisenstein_norm(0, 1), 1);
240        // N(1, 1) = 1 - 1 + 1 = 1
241        assert_eq!(fleet_eisenstein_norm(1, 1), 1);
242    }
243
244    #[test]
245    fn test_eisenstein_norm_larger() {
246        // N(2, 3) = 4 - 6 + 9 = 7
247        assert_eq!(fleet_eisenstein_norm(2, 3), 7);
248        // N(-2, 3) = 4 + 6 + 9 = 19
249        assert_eq!(fleet_eisenstein_norm(-2, 3), 19);
250    }
251
252    #[test]
253    fn test_eisenstein_norm_symmetry() {
254        // N(a,b) = N(b,a) = N(-a,-b)
255        assert_eq!(fleet_eisenstein_norm(3, 5), fleet_eisenstein_norm(5, 3));
256        assert_eq!(fleet_eisenstein_norm(3, 5), fleet_eisenstein_norm(-3, -5));
257    }
258
259    // -- Laman edges tests --
260
261    #[test]
262    fn test_laman_edges_basic() {
263        assert_eq!(fleet_laman_edges(2), 1);  // 2*2-3 = 1
264        assert_eq!(fleet_laman_edges(3), 3);  // 2*3-3 = 3
265        assert_eq!(fleet_laman_edges(4), 5);  // 2*4-3 = 5
266        assert_eq!(fleet_laman_edges(10), 17); // 2*10-3 = 17
267    }
268
269    #[test]
270    fn test_laman_edges_degenerate() {
271        assert_eq!(fleet_laman_edges(0), 0);
272        assert_eq!(fleet_laman_edges(1), 0);
273        assert_eq!(fleet_laman_edges(-5), 0);
274    }
275
276    // -- Rigidity tests --
277
278    #[test]
279    fn test_is_rigid_true() {
280        assert!(fleet_is_rigid(3, 3));  // exactly Laman
281        assert!(fleet_is_rigid(4, 6));  // over-constrained
282        assert!(fleet_is_rigid(2, 1));  // minimal
283    }
284
285    #[test]
286    fn test_is_rigid_false() {
287        assert!(!fleet_is_rigid(3, 2));  // one short
288        assert!(!fleet_is_rigid(4, 4));  // need 5
289        assert!(!fleet_is_rigid(10, 10)); // need 17
290    }
291
292    #[test]
293    fn test_is_rigid_degenerate() {
294        // V < 2: any non-negative edge count is "rigid" (vacuous)
295        assert!(fleet_is_rigid(0, 0));
296        assert!(fleet_is_rigid(1, 0));
297        assert!(!fleet_is_rigid(0, -1)); // negative edges, not rigid
298    }
299
300    // -- Holonomy check tests --
301
302    #[test]
303    fn test_holonomy_identity() {
304        let t: [i64; 1] = [1];
305        assert!(fleet_holonomy_check(t.as_ptr(), 1));
306    }
307
308    #[test]
309    fn test_holonomy_pair() {
310        let t: [i64; 2] = [2, -2];
311        // 2 * (-2) = -4, not identity
312        assert!(!fleet_holonomy_check(t.as_ptr(), 2));
313    }
314
315    #[test]
316    fn test_holonomy_product_one() {
317        let _t: [i64; 3] = [2, 3, -6]; // not identity (product = -36)
318        // For multiplicative holonomy with integers, only ±1 factors give product 1.
319        let t2: [i64; 4] = [1, 1, 1, 1];
320        assert!(fleet_holonomy_check(t2.as_ptr(), 4));
321    }
322
323    #[test]
324    fn test_holonomy_null() {
325        assert!(fleet_holonomy_check(std::ptr::null(), 0));
326        assert!(fleet_holonomy_check(std::ptr::null(), 5)); // null pointer
327    }
328
329    #[test]
330    fn test_holonomy_non_identity() {
331        let t: [i64; 2] = [1, 2];
332        assert!(!fleet_holonomy_check(t.as_ptr(), 2));
333    }
334
335    // -- Manhattan distance tests --
336
337    #[test]
338    fn test_manhattan_same() {
339        let a: [i32; 3] = [1, 2, 3];
340        let b: [i32; 3] = [1, 2, 3];
341        assert_eq!(fleet_manhattan_distance(a.as_ptr(), b.as_ptr(), 3), 0);
342    }
343
344    #[test]
345    fn test_manhattan_simple() {
346        let a: [i32; 3] = [0, 0, 0];
347        let b: [i32; 3] = [1, 2, 3];
348        assert_eq!(fleet_manhattan_distance(a.as_ptr(), b.as_ptr(), 3), 6);
349    }
350
351    #[test]
352    fn test_manhattan_negative() {
353        let a: [i32; 2] = [-1, -2];
354        let b: [i32; 2] = [1, 2];
355        assert_eq!(fleet_manhattan_distance(a.as_ptr(), b.as_ptr(), 2), 6);
356    }
357
358    // -- Pythagorean-48 tests --
359
360    #[test]
361    fn test_pythag48_zero() {
362        assert_eq!(fleet_pythagorean48_encode(0.0, 0.0), 0);
363    }
364
365    #[test]
366    fn test_pythag48_positive_x() {
367        // 0° → sector 0
368        assert_eq!(fleet_pythagorean48_encode(1.0, 0.0), 0);
369    }
370
371    #[test]
372    fn test_pythag48_positive_y() {
373        // 90° → sector 12 (90 / 7.5 = 12)
374        assert_eq!(fleet_pythagorean48_encode(0.0, 1.0), 12);
375    }
376
377    #[test]
378    fn test_pythag48_negative_x() {
379        // 180° → sector 24 (180 / 7.5 = 24)
380        assert_eq!(fleet_pythagorean48_encode(-1.0, 0.0), 24);
381    }
382
383    #[test]
384    fn test_pythag48_negative_y() {
385        // 270° → sector 36 (270 / 7.5 = 36)
386        assert_eq!(fleet_pythagorean48_encode(0.0, -1.0), 36);
387    }
388
389    #[test]
390    fn test_pythag48_45deg() {
391        // 45° → sector 6 (45 / 7.5 = 6)
392        assert_eq!(fleet_pythagorean48_encode(1.0, 1.0), 6);
393    }
394}