Skip to main content

flux_ffi/
lib.rs

1use std::os::raw::c_int;
2
3/// Eisenstein integer norm: |a + bω|² = a² - ab + b²
4#[no_mangle]
5pub extern "C" fn flux_eisenstein_norm(a: c_int, b: c_int) -> i64 {
6    let a = a as i64;
7    let b = b as i64;
8    a * a - a * b + b * b
9}
10
11/// Laman graph edges: 2n - 3 for n >= 2
12#[no_mangle]
13pub extern "C" fn flux_laman_edges(vertices: c_int) -> c_int {
14    if vertices < 2 { return 0; }
15    2 * vertices - 3
16}
17
18/// Check if (vertices, edges) satisfies Laman rigidity condition
19#[no_mangle]
20pub extern "C" fn flux_is_rigid(vertices: c_int, edges: c_int) -> bool {
21    if vertices < 2 {
22        return edges == 0;
23    }
24    edges == 2 * vertices - 3
25}
26
27/// Holonomy check: sum of transforms should be identity (zero for our encoding)
28#[no_mangle]
29pub extern "C" fn flux_holonomy_check(transforms: *const i64, len: usize) -> bool {
30    if transforms.is_null() || len == 0 {
31        return true;
32    }
33    let slice = unsafe { std::slice::from_raw_parts(transforms, len) };
34    // Holonomy trivial if sum is zero
35    slice.iter().sum::<i64>() == 0
36}
37
38/// Manhattan (L1) distance between two integer vectors
39#[no_mangle]
40pub extern "C" fn flux_manhattan_distance(a: *const c_int, b: *const c_int, len: usize) -> i64 {
41    if a.is_null() || b.is_null() || len == 0 {
42        return 0;
43    }
44    let sa = unsafe { std::slice::from_raw_parts(a, len) };
45    let sb = unsafe { std::slice::from_raw_parts(b, len) };
46    sa.iter().zip(sb.iter())
47        .map(|(&x, &y)| (x as i64 - y as i64).abs())
48        .sum()
49}
50
51/// Pythagorean 48-cell encoding: maps (x,y) to a lattice index
52#[no_mangle]
53pub extern "C" fn flux_pythagorean48_encode(x: f64, y: f64) -> c_int {
54    // Quantize to integer lattice, compute sector and radius
55    let ix = x.round() as i32;
56    let iy = y.round() as i32;
57    let radius = (ix * ix + iy * iy) as i32;
58    if radius == 0 {
59        return 0;
60    }
61    // 48-fold symmetry: atan2 maps to 0..47 sectors
62    let angle = (iy as f64).atan2(ix as f64);
63    let sector = ((angle + std::f64::consts::PI) / (2.0 * std::f64::consts::PI / 48.0)).round() as i32;
64    radius * 48 + sector
65}
66
67/// Count how many constraints are violated: values[i] > bounds[i]
68#[no_mangle]
69pub extern "C" fn flux_constraint_check(values: *const f64, bounds: *const f64, len: usize) -> c_int {
70    if values.is_null() || bounds.is_null() || len == 0 {
71        return 0;
72    }
73    let sv = unsafe { std::slice::from_raw_parts(values, len) };
74    let sb = unsafe { std::slice::from_raw_parts(bounds, len) };
75    sv.iter().zip(sb.iter())
76        .filter(|(&v, &b)| v > b)
77        .count() as c_int
78}
79
80/// Simple linear interpolation along control points at parameter t ∈ [0,1]
81#[no_mangle]
82pub extern "C" fn flux_spline_interpolate(control_points: *const f64, t: f64, n_points: usize) -> f64 {
83    if control_points.is_null() || n_points == 0 {
84        return 0.0;
85    }
86    if n_points == 1 {
87        return unsafe { *control_points };
88    }
89    let slice = unsafe { std::slice::from_raw_parts(control_points, n_points) };
90    let t_clamped = t.clamp(0.0, 1.0);
91    let idx = t_clamped * (n_points - 1) as f64;
92    let lo = idx.floor() as usize;
93    let hi = (lo + 1).min(n_points - 1);
94    let frac = idx - lo as f64;
95    slice[lo] * (1.0 - frac) + slice[hi] * frac
96}
97
98/// Deadband filter: return value if |value - baseline| > threshold, else baseline
99#[no_mangle]
100pub extern "C" fn flux_deadband_filter(value: f64, baseline: f64, threshold: f64) -> f64 {
101    if (value - baseline).abs() > threshold {
102        value
103    } else {
104        baseline
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    // --- Eisenstein norm ---
113    #[test]
114    fn test_eisenstein_norm_zero() {
115        assert_eq!(flux_eisenstein_norm(0, 0), 0);
116    }
117
118    #[test]
119    fn test_eisenstein_norm_unit() {
120        assert_eq!(flux_eisenstein_norm(1, 0), 1);
121        assert_eq!(flux_eisenstein_norm(0, 1), 1);
122        assert_eq!(flux_eisenstein_norm(1, 1), 1);
123    }
124
125    #[test]
126    fn test_eisenstein_norm_general() {
127        // a=2, b=3: 4 - 6 + 9 = 7
128        assert_eq!(flux_eisenstein_norm(2, 3), 7);
129        // a=-1, b=2: 1 + 2 + 4 = 7
130        assert_eq!(flux_eisenstein_norm(-1, 2), 7);
131    }
132
133    // --- Laman edges ---
134    #[test]
135    fn test_laman_edges_small() {
136        assert_eq!(flux_laman_edges(0), 0);
137        assert_eq!(flux_laman_edges(1), 0);
138        assert_eq!(flux_laman_edges(2), 1);
139        assert_eq!(flux_laman_edges(3), 3);
140        assert_eq!(flux_laman_edges(4), 5);
141    }
142
143    // --- Is rigid ---
144    #[test]
145    fn test_is_rigid_laman() {
146        assert!(flux_is_rigid(4, 5));   // 2*4-3 = 5
147        assert!(flux_is_rigid(2, 1));    // 2*2-3 = 1
148        assert!(!flux_is_rigid(4, 4));   // under-constrained
149        assert!(!flux_is_rigid(4, 6));   // over-constrained
150    }
151
152    #[test]
153    fn test_is_rigid_trivial() {
154        assert!(flux_is_rigid(0, 0));
155        assert!(flux_is_rigid(1, 0));
156        assert!(!flux_is_rigid(1, 1));
157    }
158
159    // --- Holonomy check ---
160    #[test]
161    fn test_holonomy_trivial() {
162        assert!(flux_holonomy_check(std::ptr::null(), 0));
163    }
164
165    #[test]
166    fn test_holonomy_zero_sum() {
167        let transforms: [i64; 4] = [1, 2, -3, 0];
168        assert!(flux_holonomy_check(transforms.as_ptr(), 4));
169    }
170
171    #[test]
172    fn test_holonomy_nonzero() {
173        let transforms: [i64; 3] = [1, 2, 3];
174        assert!(!flux_holonomy_check(transforms.as_ptr(), 3));
175    }
176
177    // --- Manhattan distance ---
178    #[test]
179    fn test_manhattan_identity() {
180        let a: [i32; 3] = [1, 2, 3];
181        let b: [i32; 3] = [1, 2, 3];
182        assert_eq!(flux_manhattan_distance(a.as_ptr(), b.as_ptr(), 3), 0);
183    }
184
185    #[test]
186    fn test_manhattan_basic() {
187        let a: [i32; 3] = [0, 0, 0];
188        let b: [i32; 3] = [1, 2, 3];
189        assert_eq!(flux_manhattan_distance(a.as_ptr(), b.as_ptr(), 3), 6);
190    }
191
192    // --- Pythagorean 48 ---
193    #[test]
194    fn test_pythagorean48_origin() {
195        assert_eq!(flux_pythagorean48_encode(0.0, 0.0), 0);
196    }
197
198    #[test]
199    fn test_pythagorean48_unit_x() {
200        let idx = flux_pythagorean48_encode(1.0, 0.0);
201        // radius=1, sector at angle 0 => PI offset => sector ~24
202        assert!(idx > 0);
203    }
204
205    // --- Constraint check ---
206    #[test]
207    fn test_constraint_check_none_violated() {
208        let values: [f64; 3] = [1.0, 2.0, 3.0];
209        let bounds: [f64; 3] = [5.0, 5.0, 5.0];
210        assert_eq!(flux_constraint_check(values.as_ptr(), bounds.as_ptr(), 3), 0);
211    }
212
213    #[test]
214    fn test_constraint_check_some_violated() {
215        let values: [f64; 4] = [1.0, 6.0, 3.0, 10.0];
216        let bounds: [f64; 4] = [5.0, 5.0, 5.0, 5.0];
217        assert_eq!(flux_constraint_check(values.as_ptr(), bounds.as_ptr(), 4), 2);
218    }
219
220    // --- Spline interpolate ---
221    #[test]
222    fn test_spline_single_point() {
223        let pts: [f64; 1] = [42.0];
224        assert!((flux_spline_interpolate(pts.as_ptr(), 0.5, 1) - 42.0).abs() < 1e-10);
225    }
226
227    #[test]
228    fn test_spline_two_points() {
229        let pts: [f64; 2] = [0.0, 10.0];
230        assert!((flux_spline_interpolate(pts.as_ptr(), 0.0, 2) - 0.0).abs() < 1e-10);
231        assert!((flux_spline_interpolate(pts.as_ptr(), 1.0, 2) - 10.0).abs() < 1e-10);
232        assert!((flux_spline_interpolate(pts.as_ptr(), 0.5, 2) - 5.0).abs() < 1e-10);
233    }
234
235    #[test]
236    fn test_spline_three_points() {
237        let pts: [f64; 3] = [0.0, 10.0, 20.0];
238        assert!((flux_spline_interpolate(pts.as_ptr(), 0.25, 3) - 5.0).abs() < 1e-10);
239    }
240
241    // --- Deadband filter ---
242    #[test]
243    fn test_deadband_within_threshold() {
244        assert!((flux_deadband_filter(10.0, 10.0, 0.5) - 10.0).abs() < 1e-10);
245        assert!((flux_deadband_filter(10.3, 10.0, 0.5) - 10.0).abs() < 1e-10);
246    }
247
248    #[test]
249    fn test_deadband_outside_threshold() {
250        assert!((flux_deadband_filter(11.0, 10.0, 0.5) - 11.0).abs() < 1e-10);
251        assert!((flux_deadband_filter(8.0, 10.0, 1.0) - 8.0).abs() < 1e-10);
252    }
253
254    #[test]
255    fn test_deadband_negative() {
256        assert!((flux_deadband_filter(-1.0, 0.0, 0.5) - (-1.0)).abs() < 1e-10);
257    }
258}