Skip to main content

horon_engine/
utils.rs

1//! utils.rs - Optimization and Utility Functions for horon-engine
2//! # Utility functions for horon-engine
3//!
4//! This module provides high-performance utility functions for the crate:
5//!
6//! - SIMD-optimized vector operations (delegates to gMath's internal SIMD when available)
7//! - Hyperbolic geometry calculations for the Poincaré disk model
8//! - Performance monitoring tools for benchmarking and optimization
9//! - CPU feature detection for runtime selection of optimal algorithms
10
11use std::fmt::{self, Debug, Formatter};
12use g_math::fixed_point::{FixedPoint, FixedVector, FixedMatrix};
13use log::debug;
14use crate::constants;
15
16/// CPU feature detection result.
17#[derive(Clone, Copy, Debug)]
18pub struct CpuFeatures {
19    /// Is AVX supported
20    pub avx: bool,
21    /// Is AVX2 supported
22    pub avx2: bool,
23    /// Is SSE4.1 supported
24    pub sse41: bool,
25    /// Is SSE4.2 supported
26    pub sse42: bool,
27}
28
29impl CpuFeatures {
30    /// Detect CPU features.
31    #[cfg(target_arch = "x86_64")]
32    pub fn detect() -> Self {
33        Self {
34            avx: is_x86_feature_detected!("avx"),
35            avx2: is_x86_feature_detected!("avx2"),
36            sse41: is_x86_feature_detected!("sse4.1"),
37            sse42: is_x86_feature_detected!("sse4.2"),
38        }
39    }
40
41    /// Detect CPU features.
42    #[cfg(not(target_arch = "x86_64"))]
43    pub fn detect() -> Self {
44        Self {
45            avx: false,
46            avx2: false,
47            sse41: false,
48            sse42: false,
49        }
50    }
51}
52
53/// SIMD optimization module for vector operations.
54///
55/// Delegates to gMath's FixedVector methods which will gain SIMD acceleration
56/// as gMath promotes its internal SIMD infrastructure to production.
57pub struct SimdOptimization {
58    /// CPU features available
59    cpu_features: CpuFeatures,
60}
61
62impl SimdOptimization {
63    /// Create a new SIMD optimization instance.
64    pub fn new() -> Self {
65        let cpu_features = CpuFeatures::detect();
66        debug!("Detected CPU features: {:?}", cpu_features);
67
68        Self {
69            cpu_features,
70        }
71    }
72
73    /// Get the CPU features.
74    pub fn cpu_features(&self) -> CpuFeatures {
75        self.cpu_features
76    }
77
78    /// Perform optimized vector multiplication.
79    /// Delegates to gMath's FixedPoint operators (gains SIMD when gMath enables it).
80    pub fn vector_multiply(&self, a: &FixedVector, b: &FixedVector) -> FixedVector {
81        let len = a.len().min(b.len());
82        let mut result = FixedVector::new(len);
83
84        for i in 0..len {
85            result[i] = a[i] * b[i];
86        }
87
88        result
89    }
90
91    /// Perform optimized matrix-vector multiplication.
92    pub fn matrix_vector_multiply(&self, m: &FixedMatrix, v: &FixedVector) -> FixedVector {
93        assert_eq!(m.cols(), v.len(), "Matrix columns must match vector length");
94
95        let mut result = FixedVector::new(m.rows());
96
97        for i in 0..m.rows() {
98            let mut sum = FixedPoint::from_int(0);
99            for j in 0..m.cols() {
100                sum = sum + (m.get(i, j) * v[j]);
101            }
102            result[i] = sum;
103        }
104
105        result
106    }
107
108    /// Calculate hyperbolic distance using gMath's FixedVector methods.
109    pub fn hyperbolic_distance(&self,
110                               _disk_radius: FixedPoint,
111                               p1: &FixedVector,
112                               p2: &FixedVector) -> FixedPoint {
113        // Use gMath's fused Euclidean distance (single materialization)
114        let euclidean_distance = p1.distance_to(p2);
115
116        // Calculate the denominator: 1 - |p1|²|p2|²
117        let p1_norm_sq = p1.dot(p1);
118        let p2_norm_sq = p2.dot(p2);
119
120        let one = FixedPoint::from_int(1);
121        let two = FixedPoint::from_int(2);
122        let denominator = one - p1_norm_sq * p2_norm_sq;
123
124        // Degenerate denominator 1 − |p1|²|p2|² ≈ 0: reachable only as both
125        // points approach the boundary (|p1|,|p2| → 1). A divide-by-zero guard
126        // for a case the strictly-interior points here do not hit. Saturate to
127        // the largest distance the model represents — 2·atanh(near_boundary)
128        // ≈ 5.29, the same value the near-boundary clamp below yields —
129        // instead of an out-of-band 10000 sentinel.
130        if denominator.abs() < constants::epsilon() {
131            return two * constants::safe_atanh(constants::near_boundary());
132        }
133
134        // Calculate 2 * atanh(|p1-p2| / sqrt(|1-p1²p2²|))
135        let ratio = euclidean_distance / denominator.sqrt();
136
137        let safe_ratio = if ratio > constants::near_boundary() {
138            constants::near_boundary()
139        } else {
140            ratio
141        };
142
143        two * constants::safe_atanh(safe_ratio)
144    }
145
146    /// Calculate Euclidean distance using gMath's FixedVector.
147    pub fn euclidean_distance(&self, v1: &FixedVector, v2: &FixedVector) -> FixedPoint {
148        v1.distance_to(v2)
149    }
150
151    /// Calculate vector norm squared.
152    pub fn vector_norm_squared(&self, v: &FixedVector) -> FixedPoint {
153        v.dot(v)
154    }
155}
156
157impl Debug for SimdOptimization {
158    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
159        f.debug_struct("SimdOptimization")
160            .field("cpu_features", &self.cpu_features)
161            .finish()
162    }
163}
164
165/// Performance monitoring for HTT operations.
166pub struct PerformanceMonitor {
167    /// Operation timings
168    timings: std::collections::HashMap<String, Vec<std::time::Duration>>,
169    /// Start times for in-progress operations
170    start_times: std::collections::HashMap<String, std::time::Instant>,
171}
172
173impl PerformanceMonitor {
174    /// Create a new performance monitor.
175    pub fn new() -> Self {
176        Self {
177            timings: std::collections::HashMap::new(),
178            start_times: std::collections::HashMap::new(),
179        }
180    }
181
182    /// Start timing an operation.
183    pub fn start(&mut self, operation: &str) {
184        self.start_times.insert(
185            operation.to_string(),
186            std::time::Instant::now()
187        );
188    }
189
190    /// Stop timing an operation.
191    pub fn stop(&mut self, operation: &str) {
192        if let Some(start_time) = self.start_times.remove(operation) {
193            let duration = start_time.elapsed();
194
195            self.timings
196                .entry(operation.to_string())
197                .or_insert_with(Vec::new)
198                .push(duration);
199        }
200    }
201
202    /// Get the average timing for an operation.
203    pub fn average(&self, operation: &str) -> Option<std::time::Duration> {
204        if let Some(timings) = self.timings.get(operation) {
205            if timings.is_empty() {
206                return None;
207            }
208
209            let total = timings.iter().sum::<std::time::Duration>();
210            let count = timings.len() as u32;
211
212            Some(total / count)
213        } else {
214            None
215        }
216    }
217
218    /// Get the min timing for an operation.
219    pub fn min(&self, operation: &str) -> Option<std::time::Duration> {
220        if let Some(timings) = self.timings.get(operation) {
221            timings.iter().min().copied()
222        } else {
223            None
224        }
225    }
226
227    /// Get the max timing for an operation.
228    pub fn max(&self, operation: &str) -> Option<std::time::Duration> {
229        if let Some(timings) = self.timings.get(operation) {
230            timings.iter().max().copied()
231        } else {
232            None
233        }
234    }
235
236    /// Get statistics for an operation.
237    pub fn stats(&self, operation: &str) -> Option<(std::time::Duration, std::time::Duration, std::time::Duration)> {
238        if let (Some(avg), Some(min), Some(max)) = (
239            self.average(operation),
240            self.min(operation),
241            self.max(operation),
242        ) {
243            Some((avg, min, max))
244        } else {
245            None
246        }
247    }
248
249    /// Reset all timings.
250    pub fn reset(&mut self) {
251        self.timings.clear();
252        self.start_times.clear();
253    }
254
255    /// Get all operation stats.
256    pub fn all_stats(&self) -> std::collections::HashMap<String, (std::time::Duration, std::time::Duration, std::time::Duration)> {
257        let mut result = std::collections::HashMap::new();
258
259        for operation in self.timings.keys() {
260            if let Some(stats) = self.stats(operation) {
261                result.insert(operation.clone(), stats);
262            }
263        }
264
265        result
266    }
267}
268
269/// Formatted duration in microseconds.
270pub fn format_duration_us(duration: std::time::Duration) -> String {
271    format!("{} µs", duration.as_micros())
272}
273
274/// Formatted duration in milliseconds (display-boundary f64 usage).
275pub fn format_duration_ms(duration: std::time::Duration) -> String {
276    format!("{:.2} ms", duration.as_micros() as f64 / 1000.0)
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282
283    #[test]
284    fn test_cpu_features_detection() {
285        let features = CpuFeatures::detect();
286        println!("Detected CPU features: {:?}", features);
287    }
288
289    #[test]
290    fn test_degenerate_distance_saturates_not_sentinel() {
291        // Two coincident points hard against the boundary drive the
292        // denominator 1 − |p1|²|p2|² below epsilon, hitting the divide-by-zero
293        // guard. It must return the model's saturated maximum (~5.29), not the
294        // old out-of-band 10000 sentinel.
295        let simd = SimdOptimization::new();
296        let boundary = FixedVector::from_f32_slice(&[0.99999, 0.0]);
297        let d = simd.hyperbolic_distance(FixedPoint::from_int(1), &boundary, &boundary);
298
299        let saturated = FixedPoint::from_int(2) * constants::safe_atanh(constants::near_boundary());
300        assert!(
301            d <= saturated + constants::epsilon(),
302            "degenerate distance {} exceeded the saturated model max {}",
303            d, saturated
304        );
305    }
306
307    #[test]
308    fn test_simd_vector_multiply() {
309        let simd = SimdOptimization::new();
310
311        let a = FixedVector::from_f32_slice(&[1.0, 2.0, 3.0, 4.0]);
312        let b = FixedVector::from_f32_slice(&[5.0, 6.0, 7.0, 8.0]);
313
314        let product = simd.vector_multiply(&a, &b);
315
316        let mut expected = FixedVector::new(4);
317        for i in 0..4 {
318            expected[i] = a[i] * b[i];
319        }
320
321        for i in 0..4 {
322            assert!((product[i] - expected[i]).abs() < constants::epsilon(),
323                   "Mismatch at index {}", i);
324        }
325    }
326
327    #[test]
328    fn test_simd_matrix_vector_multiply() {
329        let simd = SimdOptimization::new();
330
331        let mut m = FixedMatrix::new(2, 3);
332        m.set(0, 0, FixedPoint::from_int(1));
333        m.set(0, 1, FixedPoint::from_int(2));
334        m.set(0, 2, FixedPoint::from_int(3));
335        m.set(1, 0, FixedPoint::from_int(4));
336        m.set(1, 1, FixedPoint::from_int(5));
337        m.set(1, 2, FixedPoint::from_int(6));
338
339        let v = FixedVector::from_f32_slice(&[7.0, 8.0, 9.0]);
340
341        let product = simd.matrix_vector_multiply(&m, &v);
342
343        let tolerance = FixedPoint::from_int(1) / FixedPoint::from_int(100);
344        assert!(product.len() == 2);
345        assert!((product[0] - FixedPoint::from_int(50)).abs() < tolerance);
346        assert!((product[1] - FixedPoint::from_int(122)).abs() < tolerance);
347    }
348
349    #[test]
350    fn test_hyperbolic_distance() {
351        let simd = SimdOptimization::new();
352
353        let origin = FixedVector::from_f32_slice(&[0.0, 0.0]);
354        let point = FixedVector::from_f32_slice(&[0.5, 0.0]);
355
356        let disk_radius = FixedPoint::from_int(1);
357        let distance = simd.hyperbolic_distance(disk_radius, &origin, &point);
358
359        // Expected: 2 * atanh(0.5)
360        let expected = FixedPoint::from_int(2) * constants::safe_atanh(constants::half());
361        let tolerance = FixedPoint::from_int(1) / FixedPoint::from_int(10);
362        assert!((distance - expected).abs() < tolerance);
363    }
364
365    #[test]
366    fn test_performance_monitor() {
367        let mut monitor = PerformanceMonitor::new();
368
369        monitor.start("test_op");
370        std::thread::sleep(std::time::Duration::from_millis(10));
371        monitor.stop("test_op");
372
373        let (avg, min, max) = monitor.stats("test_op").unwrap();
374
375        assert!(avg.as_millis() >= 9 && avg.as_millis() <= 20);
376        assert!(min.as_millis() >= 9 && min.as_millis() <= 20);
377        assert!(max.as_millis() >= 9 && max.as_millis() <= 20);
378
379        monitor.reset();
380        assert!(monitor.stats("test_op").is_none());
381    }
382
383    #[test]
384    fn test_format_duration() {
385        let duration = std::time::Duration::from_micros(1234);
386
387        assert_eq!(format_duration_us(duration), "1234 µs");
388        assert_eq!(format_duration_ms(duration), "1.23 ms");
389    }
390}