mirror of
https://github.com/ruvnet/RuVector.git
synced 2026-05-24 13:54:31 +00:00
Implements the full delta-behavior framework - systems where change is permitted but collapse is not. ## Core Implementation - Coherence type with [0,1] bounds and safe constructors - Three-layer enforcement: energy cost, scheduling, memory gating - DeltaSystem trait for coherence-preserving systems - DeltaConfig with strict/relaxed/default presets ## 11 Exotic Applications 1. Self-Limiting Reasoning - AI that does less when uncertain 2. Computational Event Horizon - bounded computation without hard limits 3. Artificial Homeostasis - synthetic life with coherence-based survival 4. Self-Stabilizing World Model - models that refuse to hallucinate 5. Coherence-Bounded Creativity - novelty without chaos 6. Anti-Cascade Financial System - markets that cannot collapse 7. Graceful Aging - systems that simplify over time 8. Swarm Intelligence - collective behavior without pathology 9. Graceful Shutdown - systems that seek safe termination 10. Pre-AGI Containment - bounded intelligence growth 11. Extropic Substrate - goal mutation, agent lifecycles, spike semantics ## Performance Optimizations - O(n²) → O(n·k) swarm neighbor detection via SpatialGrid - O(n) → O(1) coherence calculation with incremental cache - VecDeque for O(1) history removal - SIMD utilities with 8x loop unrolling - Bounded history to prevent memory leaks ## Security Fixes - Replaced unsafe static mut with AtomicU64 for thread-safe RNG - NaN validation on all coherence inputs - Overflow protection in calculations ## WASM + TypeScript SDK - Full wasm-bindgen exports for all 11 applications - High-level TypeScript SDK with ergonomic APIs - Browser and Node.js examples ## Test Coverage - 32 lib tests, 14 WASM tests, 13 doc tests (59 total) Resolves #140 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1026 lines
29 KiB
Rust
1026 lines
29 KiB
Rust
//! SIMD-Optimized Utilities for Delta-Behavior
|
|
//!
|
|
//! Provides portable SIMD-style optimizations for vector operations:
|
|
//! - Batch distance calculations
|
|
//! - Range checks
|
|
//! - Vector coherence
|
|
//! - Normalization
|
|
//!
|
|
//! Uses manual loop unrolling and cache-friendly access patterns
|
|
//! that work across all platforms without external SIMD crates.
|
|
//!
|
|
//! # Design Philosophy
|
|
//!
|
|
//! Following ruvector patterns, this module provides:
|
|
//! - Portable scalar implementations that benefit from auto-vectorization
|
|
//! - Manual loop unrolling for better instruction-level parallelism
|
|
//! - Cache-line aware chunk sizes (typically 4 or 8 elements)
|
|
//! - Remainder handling for arbitrary-sized inputs
|
|
//!
|
|
//! # Example
|
|
//!
|
|
//! ```rust
|
|
//! use delta_behavior::simd_utils::{batch_squared_distances, batch_in_range, vector_coherence};
|
|
//!
|
|
//! // Calculate distances from center
|
|
//! let points = [(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (7.0, 8.0)];
|
|
//! let center = (0.0, 0.0);
|
|
//! let distances = batch_squared_distances(&points, center);
|
|
//!
|
|
//! // Check which points are within range
|
|
//! let range_sq = 25.0;
|
|
//! let in_range = batch_in_range(&points, center, range_sq);
|
|
//!
|
|
//! // Compute coherence between high-dimensional vectors
|
|
//! let v1 = vec![1.0, 0.0, 0.0];
|
|
//! let v2 = vec![0.707, 0.707, 0.0];
|
|
//! let coherence = vector_coherence(&v1, &v2);
|
|
//! ```
|
|
|
|
/// Unroll factor for batch operations (optimized for typical cache line sizes)
|
|
const UNROLL_FACTOR: usize = 4;
|
|
|
|
/// Larger unroll factor for high-dimensional vector operations
|
|
const VECTOR_UNROLL_FACTOR: usize = 8;
|
|
|
|
// ============================================================================
|
|
// Batch Squared Distance Calculation
|
|
// ============================================================================
|
|
|
|
/// Calculate squared Euclidean distances from multiple 2D points to a center point.
|
|
///
|
|
/// This is optimized for batch processing with loop unrolling to maximize
|
|
/// instruction-level parallelism and cache efficiency.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `points` - Slice of 2D points as (x, y) tuples
|
|
/// * `center` - The center point to measure distances from
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// A vector of squared distances, one per input point.
|
|
///
|
|
/// # Performance
|
|
///
|
|
/// Uses 4x loop unrolling for better ILP. For N points:
|
|
/// - Main loop: N/4 iterations processing 4 points each
|
|
/// - Remainder loop: 0-3 iterations for leftover points
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```rust
|
|
/// use delta_behavior::simd_utils::batch_squared_distances;
|
|
///
|
|
/// let points = [(3.0, 0.0), (0.0, 4.0), (3.0, 4.0)];
|
|
/// let center = (0.0, 0.0);
|
|
/// let dists = batch_squared_distances(&points, center);
|
|
///
|
|
/// assert!((dists[0] - 9.0).abs() < 1e-10); // 3^2 = 9
|
|
/// assert!((dists[1] - 16.0).abs() < 1e-10); // 4^2 = 16
|
|
/// assert!((dists[2] - 25.0).abs() < 1e-10); // 3^2 + 4^2 = 25
|
|
/// ```
|
|
#[inline]
|
|
pub fn batch_squared_distances(points: &[(f64, f64)], center: (f64, f64)) -> Vec<f64> {
|
|
let n = points.len();
|
|
let mut result = vec![0.0; n];
|
|
|
|
if n == 0 {
|
|
return result;
|
|
}
|
|
|
|
let cx = center.0;
|
|
let cy = center.1;
|
|
|
|
// Process in unrolled chunks of 4
|
|
let chunks = n / UNROLL_FACTOR;
|
|
|
|
for i in 0..chunks {
|
|
let base = i * UNROLL_FACTOR;
|
|
|
|
// Load 4 points
|
|
let (x0, y0) = points[base];
|
|
let (x1, y1) = points[base + 1];
|
|
let (x2, y2) = points[base + 2];
|
|
let (x3, y3) = points[base + 3];
|
|
|
|
// Compute differences
|
|
let dx0 = x0 - cx;
|
|
let dy0 = y0 - cy;
|
|
let dx1 = x1 - cx;
|
|
let dy1 = y1 - cy;
|
|
let dx2 = x2 - cx;
|
|
let dy2 = y2 - cy;
|
|
let dx3 = x3 - cx;
|
|
let dy3 = y3 - cy;
|
|
|
|
// Compute squared distances
|
|
result[base] = dx0 * dx0 + dy0 * dy0;
|
|
result[base + 1] = dx1 * dx1 + dy1 * dy1;
|
|
result[base + 2] = dx2 * dx2 + dy2 * dy2;
|
|
result[base + 3] = dx3 * dx3 + dy3 * dy3;
|
|
}
|
|
|
|
// Handle remainder
|
|
for i in (chunks * UNROLL_FACTOR)..n {
|
|
let (x, y) = points[i];
|
|
let dx = x - cx;
|
|
let dy = y - cy;
|
|
result[i] = dx * dx + dy * dy;
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
// ============================================================================
|
|
// Batch In-Range Check
|
|
// ============================================================================
|
|
|
|
/// Check which points are within a squared distance threshold from center.
|
|
///
|
|
/// This combines distance calculation with comparison in a single pass
|
|
/// for better cache utilization.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `points` - Slice of 2D points as (x, y) tuples
|
|
/// * `center` - The center point to measure distances from
|
|
/// * `range_sq` - The squared range threshold (use squared to avoid sqrt)
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// A vector of booleans indicating whether each point is within range.
|
|
///
|
|
/// # Performance
|
|
///
|
|
/// Uses 4x loop unrolling. The comparison is fused with distance calculation
|
|
/// to avoid a separate pass over the data.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```rust
|
|
/// use delta_behavior::simd_utils::batch_in_range;
|
|
///
|
|
/// let points = [(1.0, 0.0), (3.0, 0.0), (5.0, 0.0)];
|
|
/// let center = (0.0, 0.0);
|
|
/// let range_sq = 10.0; // sqrt(10) ~ 3.16
|
|
///
|
|
/// let in_range = batch_in_range(&points, center, range_sq);
|
|
///
|
|
/// assert!(in_range[0]); // 1^2 = 1 < 10
|
|
/// assert!(in_range[1]); // 3^2 = 9 < 10
|
|
/// assert!(!in_range[2]); // 5^2 = 25 > 10
|
|
/// ```
|
|
#[inline]
|
|
pub fn batch_in_range(points: &[(f64, f64)], center: (f64, f64), range_sq: f64) -> Vec<bool> {
|
|
let n = points.len();
|
|
let mut result = vec![false; n];
|
|
|
|
if n == 0 {
|
|
return result;
|
|
}
|
|
|
|
let cx = center.0;
|
|
let cy = center.1;
|
|
|
|
// Process in unrolled chunks of 4
|
|
let chunks = n / UNROLL_FACTOR;
|
|
|
|
for i in 0..chunks {
|
|
let base = i * UNROLL_FACTOR;
|
|
|
|
// Load 4 points
|
|
let (x0, y0) = points[base];
|
|
let (x1, y1) = points[base + 1];
|
|
let (x2, y2) = points[base + 2];
|
|
let (x3, y3) = points[base + 3];
|
|
|
|
// Compute differences
|
|
let dx0 = x0 - cx;
|
|
let dy0 = y0 - cy;
|
|
let dx1 = x1 - cx;
|
|
let dy1 = y1 - cy;
|
|
let dx2 = x2 - cx;
|
|
let dy2 = y2 - cy;
|
|
let dx3 = x3 - cx;
|
|
let dy3 = y3 - cy;
|
|
|
|
// Compute squared distances and compare
|
|
result[base] = dx0 * dx0 + dy0 * dy0 <= range_sq;
|
|
result[base + 1] = dx1 * dx1 + dy1 * dy1 <= range_sq;
|
|
result[base + 2] = dx2 * dx2 + dy2 * dy2 <= range_sq;
|
|
result[base + 3] = dx3 * dx3 + dy3 * dy3 <= range_sq;
|
|
}
|
|
|
|
// Handle remainder
|
|
for i in (chunks * UNROLL_FACTOR)..n {
|
|
let (x, y) = points[i];
|
|
let dx = x - cx;
|
|
let dy = y - cy;
|
|
result[i] = dx * dx + dy * dy <= range_sq;
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
// ============================================================================
|
|
// Vector Coherence Calculation
|
|
// ============================================================================
|
|
|
|
/// Calculate coherence between two high-dimensional vectors.
|
|
///
|
|
/// Coherence is defined as the cosine similarity (dot product of normalized vectors),
|
|
/// measuring how aligned two vectors are. This is a key metric in delta-behavior
|
|
/// for tracking system stability.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `v1` - First vector
|
|
/// * `v2` - Second vector (must have same length as v1)
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// Coherence value in range [-1.0, 1.0]:
|
|
/// - 1.0 = perfectly aligned (same direction)
|
|
/// - 0.0 = orthogonal (no correlation)
|
|
/// - -1.0 = opposite directions
|
|
///
|
|
/// Returns 0.0 if either vector has zero magnitude.
|
|
///
|
|
/// # Performance
|
|
///
|
|
/// Uses 8x loop unrolling for high-dimensional vectors. Computes dot product
|
|
/// and magnitudes in a single pass to maximize cache efficiency.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```rust
|
|
/// use delta_behavior::simd_utils::vector_coherence;
|
|
///
|
|
/// let v1 = vec![1.0, 0.0, 0.0];
|
|
/// let v2 = vec![1.0, 0.0, 0.0];
|
|
/// assert!((vector_coherence(&v1, &v2) - 1.0).abs() < 1e-10); // Same direction
|
|
///
|
|
/// let v3 = vec![0.0, 1.0, 0.0];
|
|
/// assert!(vector_coherence(&v1, &v3).abs() < 1e-10); // Orthogonal
|
|
///
|
|
/// let v4 = vec![-1.0, 0.0, 0.0];
|
|
/// assert!((vector_coherence(&v1, &v4) + 1.0).abs() < 1e-10); // Opposite
|
|
/// ```
|
|
#[inline]
|
|
pub fn vector_coherence(v1: &[f64], v2: &[f64]) -> f64 {
|
|
assert_eq!(v1.len(), v2.len(), "Vectors must have same length");
|
|
|
|
let n = v1.len();
|
|
if n == 0 {
|
|
return 0.0;
|
|
}
|
|
|
|
// Accumulate dot product and magnitudes in single pass
|
|
let mut dot = 0.0;
|
|
let mut mag1_sq = 0.0;
|
|
let mut mag2_sq = 0.0;
|
|
|
|
// Use 8x unrolling for high-dimensional vectors
|
|
let chunks = n / VECTOR_UNROLL_FACTOR;
|
|
|
|
for i in 0..chunks {
|
|
let base = i * VECTOR_UNROLL_FACTOR;
|
|
|
|
// Load 8 elements from each vector
|
|
let a0 = v1[base];
|
|
let a1 = v1[base + 1];
|
|
let a2 = v1[base + 2];
|
|
let a3 = v1[base + 3];
|
|
let a4 = v1[base + 4];
|
|
let a5 = v1[base + 5];
|
|
let a6 = v1[base + 6];
|
|
let a7 = v1[base + 7];
|
|
|
|
let b0 = v2[base];
|
|
let b1 = v2[base + 1];
|
|
let b2 = v2[base + 2];
|
|
let b3 = v2[base + 3];
|
|
let b4 = v2[base + 4];
|
|
let b5 = v2[base + 5];
|
|
let b6 = v2[base + 6];
|
|
let b7 = v2[base + 7];
|
|
|
|
// Accumulate dot product
|
|
dot += a0 * b0 + a1 * b1 + a2 * b2 + a3 * b3;
|
|
dot += a4 * b4 + a5 * b5 + a6 * b6 + a7 * b7;
|
|
|
|
// Accumulate squared magnitudes
|
|
mag1_sq += a0 * a0 + a1 * a1 + a2 * a2 + a3 * a3;
|
|
mag1_sq += a4 * a4 + a5 * a5 + a6 * a6 + a7 * a7;
|
|
|
|
mag2_sq += b0 * b0 + b1 * b1 + b2 * b2 + b3 * b3;
|
|
mag2_sq += b4 * b4 + b5 * b5 + b6 * b6 + b7 * b7;
|
|
}
|
|
|
|
// Handle remainder with 4x unrolling
|
|
let remaining_start = chunks * VECTOR_UNROLL_FACTOR;
|
|
let remaining = n - remaining_start;
|
|
let small_chunks = remaining / UNROLL_FACTOR;
|
|
|
|
for i in 0..small_chunks {
|
|
let base = remaining_start + i * UNROLL_FACTOR;
|
|
|
|
let a0 = v1[base];
|
|
let a1 = v1[base + 1];
|
|
let a2 = v1[base + 2];
|
|
let a3 = v1[base + 3];
|
|
|
|
let b0 = v2[base];
|
|
let b1 = v2[base + 1];
|
|
let b2 = v2[base + 2];
|
|
let b3 = v2[base + 3];
|
|
|
|
dot += a0 * b0 + a1 * b1 + a2 * b2 + a3 * b3;
|
|
mag1_sq += a0 * a0 + a1 * a1 + a2 * a2 + a3 * a3;
|
|
mag2_sq += b0 * b0 + b1 * b1 + b2 * b2 + b3 * b3;
|
|
}
|
|
|
|
// Handle final remainder
|
|
for i in (remaining_start + small_chunks * UNROLL_FACTOR)..n {
|
|
let a = v1[i];
|
|
let b = v2[i];
|
|
dot += a * b;
|
|
mag1_sq += a * a;
|
|
mag2_sq += b * b;
|
|
}
|
|
|
|
// Compute coherence (cosine similarity)
|
|
let denominator = (mag1_sq * mag2_sq).sqrt();
|
|
if denominator < f64::EPSILON {
|
|
return 0.0;
|
|
}
|
|
|
|
dot / denominator
|
|
}
|
|
|
|
// ============================================================================
|
|
// Batch Normalization
|
|
// ============================================================================
|
|
|
|
/// Normalize multiple vectors in-place to unit length.
|
|
///
|
|
/// Each vector is divided by its L2 norm. Zero-magnitude vectors are left unchanged.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `vectors` - Mutable slice of vectors to normalize
|
|
///
|
|
/// # Performance
|
|
///
|
|
/// Uses two-pass algorithm per vector:
|
|
/// 1. Compute squared magnitude with 8x unrolling
|
|
/// 2. Scale by inverse magnitude with 8x unrolling
|
|
///
|
|
/// This is more numerically stable than fusing the passes.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```rust
|
|
/// use delta_behavior::simd_utils::normalize_vectors;
|
|
///
|
|
/// let mut vectors = vec![
|
|
/// vec![3.0, 4.0], // magnitude = 5
|
|
/// vec![0.0, 0.0], // zero vector (unchanged)
|
|
/// vec![1.0, 1.0, 1.0], // magnitude = sqrt(3)
|
|
/// ];
|
|
///
|
|
/// normalize_vectors(&mut vectors);
|
|
///
|
|
/// // First vector: [3/5, 4/5] = [0.6, 0.8]
|
|
/// assert!((vectors[0][0] - 0.6).abs() < 1e-10);
|
|
/// assert!((vectors[0][1] - 0.8).abs() < 1e-10);
|
|
///
|
|
/// // Zero vector unchanged
|
|
/// assert_eq!(vectors[1], vec![0.0, 0.0]);
|
|
///
|
|
/// // Third vector: unit length
|
|
/// let mag: f64 = vectors[2].iter().map(|x| x * x).sum::<f64>().sqrt();
|
|
/// assert!((mag - 1.0).abs() < 1e-10);
|
|
/// ```
|
|
#[inline]
|
|
pub fn normalize_vectors(vectors: &mut [Vec<f64>]) {
|
|
for vec in vectors.iter_mut() {
|
|
normalize_vector_inplace(vec);
|
|
}
|
|
}
|
|
|
|
/// Normalize a single vector in-place to unit length.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `vec` - Mutable reference to vector to normalize
|
|
///
|
|
/// # Performance
|
|
///
|
|
/// Uses 8x loop unrolling for both magnitude computation and scaling.
|
|
#[inline]
|
|
pub fn normalize_vector_inplace(vec: &mut [f64]) {
|
|
let n = vec.len();
|
|
if n == 0 {
|
|
return;
|
|
}
|
|
|
|
// Pass 1: Compute squared magnitude
|
|
let mut mag_sq = 0.0;
|
|
let chunks = n / VECTOR_UNROLL_FACTOR;
|
|
|
|
for i in 0..chunks {
|
|
let base = i * VECTOR_UNROLL_FACTOR;
|
|
let a0 = vec[base];
|
|
let a1 = vec[base + 1];
|
|
let a2 = vec[base + 2];
|
|
let a3 = vec[base + 3];
|
|
let a4 = vec[base + 4];
|
|
let a5 = vec[base + 5];
|
|
let a6 = vec[base + 6];
|
|
let a7 = vec[base + 7];
|
|
|
|
mag_sq += a0 * a0 + a1 * a1 + a2 * a2 + a3 * a3;
|
|
mag_sq += a4 * a4 + a5 * a5 + a6 * a6 + a7 * a7;
|
|
}
|
|
|
|
// Handle remainder with 4x unrolling
|
|
let remaining_start = chunks * VECTOR_UNROLL_FACTOR;
|
|
let remaining = n - remaining_start;
|
|
let small_chunks = remaining / UNROLL_FACTOR;
|
|
|
|
for i in 0..small_chunks {
|
|
let base = remaining_start + i * UNROLL_FACTOR;
|
|
let a0 = vec[base];
|
|
let a1 = vec[base + 1];
|
|
let a2 = vec[base + 2];
|
|
let a3 = vec[base + 3];
|
|
mag_sq += a0 * a0 + a1 * a1 + a2 * a2 + a3 * a3;
|
|
}
|
|
|
|
for i in (remaining_start + small_chunks * UNROLL_FACTOR)..n {
|
|
mag_sq += vec[i] * vec[i];
|
|
}
|
|
|
|
// Check for zero vector
|
|
if mag_sq < f64::EPSILON {
|
|
return;
|
|
}
|
|
|
|
// Pass 2: Scale by inverse magnitude
|
|
let inv_mag = 1.0 / mag_sq.sqrt();
|
|
|
|
for i in 0..chunks {
|
|
let base = i * VECTOR_UNROLL_FACTOR;
|
|
vec[base] *= inv_mag;
|
|
vec[base + 1] *= inv_mag;
|
|
vec[base + 2] *= inv_mag;
|
|
vec[base + 3] *= inv_mag;
|
|
vec[base + 4] *= inv_mag;
|
|
vec[base + 5] *= inv_mag;
|
|
vec[base + 6] *= inv_mag;
|
|
vec[base + 7] *= inv_mag;
|
|
}
|
|
|
|
for i in 0..small_chunks {
|
|
let base = remaining_start + i * UNROLL_FACTOR;
|
|
vec[base] *= inv_mag;
|
|
vec[base + 1] *= inv_mag;
|
|
vec[base + 2] *= inv_mag;
|
|
vec[base + 3] *= inv_mag;
|
|
}
|
|
|
|
for i in (remaining_start + small_chunks * UNROLL_FACTOR)..n {
|
|
vec[i] *= inv_mag;
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Additional Utility Functions
|
|
// ============================================================================
|
|
|
|
/// Compute the L2 norm (magnitude) of a vector.
|
|
///
|
|
/// Uses 8x loop unrolling for optimal performance on high-dimensional vectors.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```rust
|
|
/// use delta_behavior::simd_utils::vector_magnitude;
|
|
///
|
|
/// let v = vec![3.0, 4.0];
|
|
/// assert!((vector_magnitude(&v) - 5.0).abs() < 1e-10);
|
|
/// ```
|
|
#[inline]
|
|
pub fn vector_magnitude(v: &[f64]) -> f64 {
|
|
vector_magnitude_squared(v).sqrt()
|
|
}
|
|
|
|
/// Compute the squared L2 norm of a vector.
|
|
///
|
|
/// This avoids the sqrt operation when only comparisons are needed.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```rust
|
|
/// use delta_behavior::simd_utils::vector_magnitude_squared;
|
|
///
|
|
/// let v = vec![3.0, 4.0];
|
|
/// assert!((vector_magnitude_squared(&v) - 25.0).abs() < 1e-10);
|
|
/// ```
|
|
#[inline]
|
|
pub fn vector_magnitude_squared(v: &[f64]) -> f64 {
|
|
let n = v.len();
|
|
if n == 0 {
|
|
return 0.0;
|
|
}
|
|
|
|
let mut sum = 0.0;
|
|
let chunks = n / VECTOR_UNROLL_FACTOR;
|
|
|
|
for i in 0..chunks {
|
|
let base = i * VECTOR_UNROLL_FACTOR;
|
|
let a0 = v[base];
|
|
let a1 = v[base + 1];
|
|
let a2 = v[base + 2];
|
|
let a3 = v[base + 3];
|
|
let a4 = v[base + 4];
|
|
let a5 = v[base + 5];
|
|
let a6 = v[base + 6];
|
|
let a7 = v[base + 7];
|
|
|
|
sum += a0 * a0 + a1 * a1 + a2 * a2 + a3 * a3;
|
|
sum += a4 * a4 + a5 * a5 + a6 * a6 + a7 * a7;
|
|
}
|
|
|
|
// Handle remainder
|
|
for i in (chunks * VECTOR_UNROLL_FACTOR)..n {
|
|
sum += v[i] * v[i];
|
|
}
|
|
|
|
sum
|
|
}
|
|
|
|
/// Compute dot product of two vectors.
|
|
///
|
|
/// Uses 8x loop unrolling for optimal performance.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```rust
|
|
/// use delta_behavior::simd_utils::vector_dot;
|
|
///
|
|
/// let v1 = vec![1.0, 2.0, 3.0];
|
|
/// let v2 = vec![4.0, 5.0, 6.0];
|
|
/// assert!((vector_dot(&v1, &v2) - 32.0).abs() < 1e-10); // 1*4 + 2*5 + 3*6 = 32
|
|
/// ```
|
|
#[inline]
|
|
pub fn vector_dot(v1: &[f64], v2: &[f64]) -> f64 {
|
|
assert_eq!(v1.len(), v2.len(), "Vectors must have same length");
|
|
|
|
let n = v1.len();
|
|
if n == 0 {
|
|
return 0.0;
|
|
}
|
|
|
|
let mut sum = 0.0;
|
|
let chunks = n / VECTOR_UNROLL_FACTOR;
|
|
|
|
for i in 0..chunks {
|
|
let base = i * VECTOR_UNROLL_FACTOR;
|
|
|
|
sum += v1[base] * v2[base];
|
|
sum += v1[base + 1] * v2[base + 1];
|
|
sum += v1[base + 2] * v2[base + 2];
|
|
sum += v1[base + 3] * v2[base + 3];
|
|
sum += v1[base + 4] * v2[base + 4];
|
|
sum += v1[base + 5] * v2[base + 5];
|
|
sum += v1[base + 6] * v2[base + 6];
|
|
sum += v1[base + 7] * v2[base + 7];
|
|
}
|
|
|
|
// Handle remainder
|
|
for i in (chunks * VECTOR_UNROLL_FACTOR)..n {
|
|
sum += v1[i] * v2[i];
|
|
}
|
|
|
|
sum
|
|
}
|
|
|
|
/// Batch squared distance calculation for 3D points.
|
|
///
|
|
/// Similar to `batch_squared_distances` but for 3D space.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```rust
|
|
/// use delta_behavior::simd_utils::batch_squared_distances_3d;
|
|
///
|
|
/// let points = [(1.0, 0.0, 0.0), (0.0, 2.0, 0.0), (0.0, 0.0, 3.0)];
|
|
/// let center = (0.0, 0.0, 0.0);
|
|
/// let dists = batch_squared_distances_3d(&points, center);
|
|
///
|
|
/// assert!((dists[0] - 1.0).abs() < 1e-10);
|
|
/// assert!((dists[1] - 4.0).abs() < 1e-10);
|
|
/// assert!((dists[2] - 9.0).abs() < 1e-10);
|
|
/// ```
|
|
#[inline]
|
|
pub fn batch_squared_distances_3d(
|
|
points: &[(f64, f64, f64)],
|
|
center: (f64, f64, f64),
|
|
) -> Vec<f64> {
|
|
let n = points.len();
|
|
let mut result = vec![0.0; n];
|
|
|
|
if n == 0 {
|
|
return result;
|
|
}
|
|
|
|
let (cx, cy, cz) = center;
|
|
|
|
// Process in unrolled chunks of 4
|
|
let chunks = n / UNROLL_FACTOR;
|
|
|
|
for i in 0..chunks {
|
|
let base = i * UNROLL_FACTOR;
|
|
|
|
let (x0, y0, z0) = points[base];
|
|
let (x1, y1, z1) = points[base + 1];
|
|
let (x2, y2, z2) = points[base + 2];
|
|
let (x3, y3, z3) = points[base + 3];
|
|
|
|
let dx0 = x0 - cx;
|
|
let dy0 = y0 - cy;
|
|
let dz0 = z0 - cz;
|
|
|
|
let dx1 = x1 - cx;
|
|
let dy1 = y1 - cy;
|
|
let dz1 = z1 - cz;
|
|
|
|
let dx2 = x2 - cx;
|
|
let dy2 = y2 - cy;
|
|
let dz2 = z2 - cz;
|
|
|
|
let dx3 = x3 - cx;
|
|
let dy3 = y3 - cy;
|
|
let dz3 = z3 - cz;
|
|
|
|
result[base] = dx0 * dx0 + dy0 * dy0 + dz0 * dz0;
|
|
result[base + 1] = dx1 * dx1 + dy1 * dy1 + dz1 * dz1;
|
|
result[base + 2] = dx2 * dx2 + dy2 * dy2 + dz2 * dz2;
|
|
result[base + 3] = dx3 * dx3 + dy3 * dy3 + dz3 * dz3;
|
|
}
|
|
|
|
// Handle remainder
|
|
for i in (chunks * UNROLL_FACTOR)..n {
|
|
let (x, y, z) = points[i];
|
|
let dx = x - cx;
|
|
let dy = y - cy;
|
|
let dz = z - cz;
|
|
result[i] = dx * dx + dy * dy + dz * dz;
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
/// Count how many points are within range of a center point.
|
|
///
|
|
/// More efficient than `batch_in_range` when only the count is needed.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```rust
|
|
/// use delta_behavior::simd_utils::count_in_range;
|
|
///
|
|
/// let points = [(1.0, 0.0), (3.0, 0.0), (5.0, 0.0), (7.0, 0.0)];
|
|
/// let center = (0.0, 0.0);
|
|
/// let count = count_in_range(&points, center, 10.0); // sqrt(10) ~ 3.16
|
|
/// assert_eq!(count, 2); // Points at distance 1 and 3 are within range
|
|
/// ```
|
|
#[inline]
|
|
pub fn count_in_range(points: &[(f64, f64)], center: (f64, f64), range_sq: f64) -> usize {
|
|
let n = points.len();
|
|
if n == 0 {
|
|
return 0;
|
|
}
|
|
|
|
let cx = center.0;
|
|
let cy = center.1;
|
|
let mut count = 0;
|
|
|
|
// Process in unrolled chunks
|
|
let chunks = n / UNROLL_FACTOR;
|
|
|
|
for i in 0..chunks {
|
|
let base = i * UNROLL_FACTOR;
|
|
|
|
let (x0, y0) = points[base];
|
|
let (x1, y1) = points[base + 1];
|
|
let (x2, y2) = points[base + 2];
|
|
let (x3, y3) = points[base + 3];
|
|
|
|
let dx0 = x0 - cx;
|
|
let dy0 = y0 - cy;
|
|
let dx1 = x1 - cx;
|
|
let dy1 = y1 - cy;
|
|
let dx2 = x2 - cx;
|
|
let dy2 = y2 - cy;
|
|
let dx3 = x3 - cx;
|
|
let dy3 = y3 - cy;
|
|
|
|
if dx0 * dx0 + dy0 * dy0 <= range_sq {
|
|
count += 1;
|
|
}
|
|
if dx1 * dx1 + dy1 * dy1 <= range_sq {
|
|
count += 1;
|
|
}
|
|
if dx2 * dx2 + dy2 * dy2 <= range_sq {
|
|
count += 1;
|
|
}
|
|
if dx3 * dx3 + dy3 * dy3 <= range_sq {
|
|
count += 1;
|
|
}
|
|
}
|
|
|
|
// Handle remainder
|
|
for i in (chunks * UNROLL_FACTOR)..n {
|
|
let (x, y) = points[i];
|
|
let dx = x - cx;
|
|
let dy = y - cy;
|
|
if dx * dx + dy * dy <= range_sq {
|
|
count += 1;
|
|
}
|
|
}
|
|
|
|
count
|
|
}
|
|
|
|
// ============================================================================
|
|
// Tests
|
|
// ============================================================================
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
const EPSILON: f64 = 1e-10;
|
|
|
|
#[test]
|
|
fn test_batch_squared_distances_empty() {
|
|
let result = batch_squared_distances(&[], (0.0, 0.0));
|
|
assert!(result.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_batch_squared_distances_single() {
|
|
let points = [(3.0, 4.0)];
|
|
let result = batch_squared_distances(&points, (0.0, 0.0));
|
|
assert_eq!(result.len(), 1);
|
|
assert!((result[0] - 25.0).abs() < EPSILON);
|
|
}
|
|
|
|
#[test]
|
|
fn test_batch_squared_distances_multiple() {
|
|
let points = [
|
|
(3.0, 0.0),
|
|
(0.0, 4.0),
|
|
(3.0, 4.0),
|
|
(1.0, 1.0),
|
|
(2.0, 2.0),
|
|
];
|
|
let center = (0.0, 0.0);
|
|
let result = batch_squared_distances(&points, center);
|
|
|
|
assert!((result[0] - 9.0).abs() < EPSILON);
|
|
assert!((result[1] - 16.0).abs() < EPSILON);
|
|
assert!((result[2] - 25.0).abs() < EPSILON);
|
|
assert!((result[3] - 2.0).abs() < EPSILON);
|
|
assert!((result[4] - 8.0).abs() < EPSILON);
|
|
}
|
|
|
|
#[test]
|
|
fn test_batch_squared_distances_with_offset_center() {
|
|
let points = [(4.0, 5.0)];
|
|
let center = (1.0, 1.0);
|
|
let result = batch_squared_distances(&points, center);
|
|
// (4-1)^2 + (5-1)^2 = 9 + 16 = 25
|
|
assert!((result[0] - 25.0).abs() < EPSILON);
|
|
}
|
|
|
|
#[test]
|
|
fn test_batch_in_range_empty() {
|
|
let result = batch_in_range(&[], (0.0, 0.0), 10.0);
|
|
assert!(result.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_batch_in_range_all_inside() {
|
|
let points = [(1.0, 0.0), (0.0, 1.0), (0.5, 0.5)];
|
|
let result = batch_in_range(&points, (0.0, 0.0), 10.0);
|
|
assert!(result.iter().all(|&x| x));
|
|
}
|
|
|
|
#[test]
|
|
fn test_batch_in_range_all_outside() {
|
|
let points = [(10.0, 0.0), (0.0, 10.0), (7.0, 7.0)];
|
|
let result = batch_in_range(&points, (0.0, 0.0), 10.0);
|
|
assert!(result.iter().all(|&x| !x));
|
|
}
|
|
|
|
#[test]
|
|
fn test_batch_in_range_mixed() {
|
|
let points = [(1.0, 0.0), (5.0, 0.0)];
|
|
let result = batch_in_range(&points, (0.0, 0.0), 10.0); // sqrt(10) ~ 3.16
|
|
assert!(result[0]); // 1 < sqrt(10)
|
|
assert!(!result[1]); // 5 > sqrt(10)
|
|
}
|
|
|
|
#[test]
|
|
fn test_batch_in_range_boundary() {
|
|
let points = [(3.0, 0.0)];
|
|
// Distance squared = 9, threshold = 9 (exact boundary)
|
|
let result = batch_in_range(&points, (0.0, 0.0), 9.0);
|
|
assert!(result[0]); // <= is used
|
|
}
|
|
|
|
#[test]
|
|
fn test_vector_coherence_identical() {
|
|
let v = vec![1.0, 2.0, 3.0];
|
|
let coherence = vector_coherence(&v, &v);
|
|
assert!((coherence - 1.0).abs() < EPSILON);
|
|
}
|
|
|
|
#[test]
|
|
fn test_vector_coherence_opposite() {
|
|
let v1 = vec![1.0, 0.0, 0.0];
|
|
let v2 = vec![-1.0, 0.0, 0.0];
|
|
let coherence = vector_coherence(&v1, &v2);
|
|
assert!((coherence + 1.0).abs() < EPSILON);
|
|
}
|
|
|
|
#[test]
|
|
fn test_vector_coherence_orthogonal() {
|
|
let v1 = vec![1.0, 0.0, 0.0];
|
|
let v2 = vec![0.0, 1.0, 0.0];
|
|
let coherence = vector_coherence(&v1, &v2);
|
|
assert!(coherence.abs() < EPSILON);
|
|
}
|
|
|
|
#[test]
|
|
fn test_vector_coherence_45_degrees() {
|
|
let v1 = vec![1.0, 0.0];
|
|
let v2 = vec![1.0, 1.0];
|
|
let coherence = vector_coherence(&v1, &v2);
|
|
// cos(45) = 1/sqrt(2) ~ 0.7071
|
|
assert!((coherence - 1.0 / 2.0_f64.sqrt()).abs() < EPSILON);
|
|
}
|
|
|
|
#[test]
|
|
fn test_vector_coherence_zero_vector() {
|
|
let v1 = vec![1.0, 2.0, 3.0];
|
|
let v2 = vec![0.0, 0.0, 0.0];
|
|
let coherence = vector_coherence(&v1, &v2);
|
|
assert!(coherence.abs() < EPSILON);
|
|
}
|
|
|
|
#[test]
|
|
fn test_vector_coherence_high_dimensional() {
|
|
// Test with 100 dimensions to exercise all unrolling paths
|
|
let mut v1 = vec![0.0; 100];
|
|
let mut v2 = vec![0.0; 100];
|
|
v1[0] = 1.0;
|
|
v2[0] = 1.0;
|
|
let coherence = vector_coherence(&v1, &v2);
|
|
assert!((coherence - 1.0).abs() < EPSILON);
|
|
}
|
|
|
|
#[test]
|
|
fn test_normalize_vectors_empty() {
|
|
let mut vectors: Vec<Vec<f64>> = vec![];
|
|
normalize_vectors(&mut vectors);
|
|
assert!(vectors.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_normalize_vectors_single() {
|
|
let mut vectors = vec![vec![3.0, 4.0]];
|
|
normalize_vectors(&mut vectors);
|
|
|
|
assert!((vectors[0][0] - 0.6).abs() < EPSILON);
|
|
assert!((vectors[0][1] - 0.8).abs() < EPSILON);
|
|
}
|
|
|
|
#[test]
|
|
fn test_normalize_vectors_multiple() {
|
|
let mut vectors = vec![
|
|
vec![3.0, 4.0],
|
|
vec![5.0, 12.0],
|
|
vec![1.0, 0.0],
|
|
];
|
|
normalize_vectors(&mut vectors);
|
|
|
|
// Check all are unit vectors
|
|
for v in &vectors {
|
|
let mag: f64 = v.iter().map(|x| x * x).sum::<f64>().sqrt();
|
|
assert!((mag - 1.0).abs() < EPSILON);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_normalize_vectors_zero_vector() {
|
|
let mut vectors = vec![vec![0.0, 0.0, 0.0]];
|
|
normalize_vectors(&mut vectors);
|
|
// Should remain unchanged
|
|
assert_eq!(vectors[0], vec![0.0, 0.0, 0.0]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_normalize_vectors_high_dimensional() {
|
|
// Test with 100 dimensions
|
|
let mut vectors = vec![vec![1.0; 100]];
|
|
normalize_vectors(&mut vectors);
|
|
|
|
let mag: f64 = vectors[0].iter().map(|x| x * x).sum::<f64>().sqrt();
|
|
assert!((mag - 1.0).abs() < EPSILON);
|
|
}
|
|
|
|
#[test]
|
|
fn test_vector_magnitude() {
|
|
let v = vec![3.0, 4.0];
|
|
assert!((vector_magnitude(&v) - 5.0).abs() < EPSILON);
|
|
}
|
|
|
|
#[test]
|
|
fn test_vector_magnitude_squared() {
|
|
let v = vec![3.0, 4.0];
|
|
assert!((vector_magnitude_squared(&v) - 25.0).abs() < EPSILON);
|
|
}
|
|
|
|
#[test]
|
|
fn test_vector_dot() {
|
|
let v1 = vec![1.0, 2.0, 3.0];
|
|
let v2 = vec![4.0, 5.0, 6.0];
|
|
assert!((vector_dot(&v1, &v2) - 32.0).abs() < EPSILON);
|
|
}
|
|
|
|
#[test]
|
|
fn test_batch_squared_distances_3d() {
|
|
let points = [(1.0, 0.0, 0.0), (0.0, 2.0, 0.0), (0.0, 0.0, 3.0)];
|
|
let center = (0.0, 0.0, 0.0);
|
|
let dists = batch_squared_distances_3d(&points, center);
|
|
|
|
assert!((dists[0] - 1.0).abs() < EPSILON);
|
|
assert!((dists[1] - 4.0).abs() < EPSILON);
|
|
assert!((dists[2] - 9.0).abs() < EPSILON);
|
|
}
|
|
|
|
#[test]
|
|
fn test_count_in_range() {
|
|
let points = [(1.0, 0.0), (3.0, 0.0), (5.0, 0.0), (7.0, 0.0)];
|
|
let center = (0.0, 0.0);
|
|
|
|
assert_eq!(count_in_range(&points, center, 1.0), 1); // Only (1,0)
|
|
assert_eq!(count_in_range(&points, center, 10.0), 2); // (1,0) and (3,0)
|
|
assert_eq!(count_in_range(&points, center, 26.0), 3); // (1,0), (3,0), (5,0)
|
|
assert_eq!(count_in_range(&points, center, 50.0), 4); // All
|
|
}
|
|
|
|
#[test]
|
|
fn test_unroll_remainder_handling() {
|
|
// Test with sizes that exercise all remainder paths
|
|
for n in 0..20 {
|
|
let points: Vec<(f64, f64)> = (0..n).map(|i| (i as f64, 0.0)).collect();
|
|
let result = batch_squared_distances(&points, (0.0, 0.0));
|
|
assert_eq!(result.len(), n);
|
|
|
|
for (i, &d) in result.iter().enumerate() {
|
|
let expected = (i as f64) * (i as f64);
|
|
assert!(
|
|
(d - expected).abs() < EPSILON,
|
|
"Mismatch at index {} for n={}: got {}, expected {}",
|
|
i,
|
|
n,
|
|
d,
|
|
expected
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_vector_coherence_remainder_handling() {
|
|
// Test with various vector sizes
|
|
for n in 0..20 {
|
|
let v1: Vec<f64> = (0..n).map(|i| (i + 1) as f64).collect();
|
|
let v2: Vec<f64> = (0..n).map(|i| (i + 1) as f64).collect();
|
|
|
|
if n == 0 {
|
|
assert!((vector_coherence(&v1, &v2)).abs() < EPSILON);
|
|
} else {
|
|
// Identical non-zero vectors have coherence 1.0
|
|
assert!((vector_coherence(&v1, &v2) - 1.0).abs() < EPSILON);
|
|
}
|
|
}
|
|
}
|
|
}
|