mirror of
https://github.com/ruvnet/RuView
synced 2026-08-08 20:11:43 +00:00
Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
@@ -0,0 +1,440 @@
|
||||
//! Angular and hyperspherical embeddings with π phase encoding
|
||||
//!
|
||||
//! Many embedding tricks quietly reduce to angles. Cosine similarity is
|
||||
//! literally angle-based.
|
||||
//!
|
||||
//! Using π explicitly:
|
||||
//! - Map vectors to phase space
|
||||
//! - Encode direction as multiples of π
|
||||
//! - Track angular velocity instead of Euclidean distance
|
||||
//!
|
||||
//! This is extremely friendly to 5-bit and 7-bit systems because:
|
||||
//! - Angles saturate naturally
|
||||
//! - Wraparound is meaningful
|
||||
//! - Overflow becomes topology, not error
|
||||
//!
|
||||
//! That is exactly how biological systems avoid numeric explosion.
|
||||
|
||||
use crate::precision::PrecisionLane;
|
||||
use std::f32::consts::PI;
|
||||
|
||||
/// Angular embedding projector
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AngularEmbedding {
|
||||
/// Precision lane
|
||||
lane: PrecisionLane,
|
||||
/// Dimension of embeddings
|
||||
dimension: usize,
|
||||
/// Phase scale (π / max_value for lane)
|
||||
phase_scale: f32,
|
||||
/// Angular velocity accumulator
|
||||
velocity: Vec<f32>,
|
||||
}
|
||||
|
||||
impl AngularEmbedding {
|
||||
/// Create a new angular embedding projector
|
||||
pub fn new(lane: PrecisionLane) -> Self {
|
||||
let phase_scale = match lane {
|
||||
PrecisionLane::Bit3 => PI / 4.0,
|
||||
PrecisionLane::Bit5 => PI / 16.0,
|
||||
PrecisionLane::Bit7 => PI / 64.0,
|
||||
PrecisionLane::Float32 => 1.0,
|
||||
};
|
||||
|
||||
Self {
|
||||
lane,
|
||||
dimension: 0,
|
||||
phase_scale,
|
||||
velocity: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Project Euclidean vector to angular space
|
||||
pub fn project(&self, values: &[f32]) -> Vec<f32> {
|
||||
// Compute magnitude for normalization
|
||||
let magnitude = values.iter().map(|x| x * x).sum::<f32>().sqrt().max(1e-10);
|
||||
|
||||
// Project to unit hypersphere, then to angles
|
||||
values
|
||||
.iter()
|
||||
.map(|&x| {
|
||||
let normalized = x / magnitude;
|
||||
// Map [-1, 1] to [-π, π] with phase scale
|
||||
normalized * PI * self.phase_scale
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Unproject from angular space to Euclidean
|
||||
pub fn unproject(&self, angles: &[f32], target_magnitude: f32) -> Vec<f32> {
|
||||
angles
|
||||
.iter()
|
||||
.map(|&angle| {
|
||||
let normalized = angle / (PI * self.phase_scale);
|
||||
normalized * target_magnitude
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Compute angular distance between two vectors
|
||||
pub fn angular_distance(&self, a: &[f32], b: &[f32]) -> f32 {
|
||||
if a.len() != b.len() || a.is_empty() {
|
||||
return f32::MAX;
|
||||
}
|
||||
|
||||
let angles_a = self.project(a);
|
||||
let angles_b = self.project(b);
|
||||
|
||||
// Sum of angular differences (with wraparound handling)
|
||||
let mut total_distance = 0.0f32;
|
||||
for (&a, &b) in angles_a.iter().zip(angles_b.iter()) {
|
||||
let diff = (a - b).abs();
|
||||
// Handle wraparound: use shorter arc
|
||||
let wrapped_diff = if diff > PI { 2.0 * PI - diff } else { diff };
|
||||
total_distance += wrapped_diff * wrapped_diff;
|
||||
}
|
||||
|
||||
total_distance.sqrt()
|
||||
}
|
||||
|
||||
/// Update angular velocity (for streaming embeddings)
|
||||
pub fn update_velocity(&mut self, previous: &[f32], current: &[f32]) {
|
||||
if previous.len() != current.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
let prev_angles = self.project(previous);
|
||||
let curr_angles = self.project(current);
|
||||
|
||||
if self.velocity.is_empty() {
|
||||
self.velocity = vec![0.0; current.len()];
|
||||
self.dimension = current.len();
|
||||
}
|
||||
|
||||
// Compute angular velocity (with momentum)
|
||||
let momentum = 0.9f32;
|
||||
for i in 0..self.dimension.min(self.velocity.len()) {
|
||||
let delta = curr_angles[i] - prev_angles[i];
|
||||
// Handle wraparound
|
||||
let wrapped_delta = if delta > PI {
|
||||
delta - 2.0 * PI
|
||||
} else if delta < -PI {
|
||||
delta + 2.0 * PI
|
||||
} else {
|
||||
delta
|
||||
};
|
||||
self.velocity[i] = momentum * self.velocity[i] + (1.0 - momentum) * wrapped_delta;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get current angular velocity
|
||||
pub fn get_velocity(&self) -> &[f32] {
|
||||
&self.velocity
|
||||
}
|
||||
|
||||
/// Predict next position based on angular velocity
|
||||
pub fn predict_next(&self, current: &[f32]) -> Vec<f32> {
|
||||
let angles = self.project(current);
|
||||
if self.velocity.is_empty() {
|
||||
return current.to_vec();
|
||||
}
|
||||
|
||||
let predicted_angles: Vec<f32> = angles
|
||||
.iter()
|
||||
.zip(self.velocity.iter())
|
||||
.map(|(&a, &v)| {
|
||||
let mut next = a + v;
|
||||
// Wrap to [-π, π]
|
||||
while next > PI {
|
||||
next -= 2.0 * PI;
|
||||
}
|
||||
while next < -PI {
|
||||
next += 2.0 * PI;
|
||||
}
|
||||
next
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Unproject with original magnitude
|
||||
let magnitude = current.iter().map(|x| x * x).sum::<f32>().sqrt();
|
||||
self.unproject(&predicted_angles, magnitude)
|
||||
}
|
||||
}
|
||||
|
||||
/// Phase encoder for quantized values
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PhaseEncoder {
|
||||
/// Base frequency (multiples of π)
|
||||
base_frequency: f32,
|
||||
/// Number of harmonics
|
||||
harmonics: usize,
|
||||
/// Lookup table for fast encoding
|
||||
lut: Option<Vec<f32>>,
|
||||
}
|
||||
|
||||
impl PhaseEncoder {
|
||||
/// Create a new phase encoder
|
||||
pub fn new(base_frequency: f32, harmonics: usize) -> Self {
|
||||
Self {
|
||||
base_frequency,
|
||||
harmonics,
|
||||
lut: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize lookup table for given quantization levels
|
||||
pub fn with_lut(mut self, levels: usize) -> Self {
|
||||
let mut lut = Vec::with_capacity(levels);
|
||||
for i in 0..levels {
|
||||
let normalized = (i as f32) / (levels - 1) as f32;
|
||||
let phase = normalized * 2.0 * PI * self.base_frequency;
|
||||
lut.push(phase.sin());
|
||||
}
|
||||
self.lut = Some(lut);
|
||||
self
|
||||
}
|
||||
|
||||
/// Encode value to phase
|
||||
pub fn encode(&self, value: f32) -> f32 {
|
||||
let mut encoded = 0.0f32;
|
||||
for h in 0..self.harmonics {
|
||||
let freq = self.base_frequency * (h + 1) as f32;
|
||||
let weight = 1.0 / (h + 1) as f32; // Harmonic weights
|
||||
encoded += weight * (value * freq * PI).sin();
|
||||
}
|
||||
encoded
|
||||
}
|
||||
|
||||
/// Encode quantized value using LUT
|
||||
pub fn encode_quantized(&self, level: usize) -> f32 {
|
||||
if let Some(ref lut) = self.lut {
|
||||
lut.get(level).copied().unwrap_or(0.0)
|
||||
} else {
|
||||
let normalized = level as f32 / 255.0; // Assume 8-bit max
|
||||
self.encode(normalized)
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode phase to approximate value
|
||||
pub fn decode(&self, phase: f32) -> f32 {
|
||||
// Inverse is approximate (lossy)
|
||||
phase.asin() / (self.base_frequency * PI)
|
||||
}
|
||||
}
|
||||
|
||||
/// Hyperspherical projection for high-dimensional embeddings
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HypersphericalProjection {
|
||||
/// Input dimension
|
||||
input_dim: usize,
|
||||
/// Output spherical coordinates (n-1 angles for n dimensions)
|
||||
output_dim: usize,
|
||||
/// Precision lane
|
||||
lane: PrecisionLane,
|
||||
}
|
||||
|
||||
impl HypersphericalProjection {
|
||||
/// Create a new hyperspherical projection
|
||||
pub fn new(dimension: usize, lane: PrecisionLane) -> Self {
|
||||
Self {
|
||||
input_dim: dimension,
|
||||
output_dim: dimension.saturating_sub(1),
|
||||
lane,
|
||||
}
|
||||
}
|
||||
|
||||
/// Project Cartesian coordinates to hyperspherical (angles)
|
||||
pub fn to_spherical(&self, cartesian: &[f32]) -> Vec<f32> {
|
||||
if cartesian.len() < 2 {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
let n = cartesian.len();
|
||||
let mut angles = Vec::with_capacity(n - 1);
|
||||
|
||||
// Radius (for reference, not returned)
|
||||
let r = cartesian.iter().map(|x| x * x).sum::<f32>().sqrt();
|
||||
if r < 1e-10 {
|
||||
return vec![0.0; n - 1];
|
||||
}
|
||||
|
||||
// Compute angles from the last coordinate backward
|
||||
// φ₁ = arctan2(x₂, x₁)
|
||||
// φₖ = arccos(xₖ₊₁ / √(xₖ₊₁² + ... + xₙ²)) for k > 1
|
||||
|
||||
// First angle (azimuthal)
|
||||
let phi_1 = cartesian[1].atan2(cartesian[0]);
|
||||
angles.push(phi_1);
|
||||
|
||||
// Remaining angles (polar)
|
||||
for k in 1..(n - 1) {
|
||||
let tail_sum: f32 = cartesian[k..].iter().map(|x| x * x).sum();
|
||||
let tail_r = tail_sum.sqrt();
|
||||
if tail_r < 1e-10 {
|
||||
angles.push(0.0);
|
||||
} else {
|
||||
let phi_k = (cartesian[k] / tail_r).clamp(-1.0, 1.0).acos();
|
||||
angles.push(phi_k);
|
||||
}
|
||||
}
|
||||
|
||||
angles
|
||||
}
|
||||
|
||||
/// Project hyperspherical coordinates back to Cartesian
|
||||
pub fn to_cartesian(&self, angles: &[f32], radius: f32) -> Vec<f32> {
|
||||
if angles.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
let n = angles.len() + 1;
|
||||
let mut cartesian = Vec::with_capacity(n);
|
||||
|
||||
// x₁ = r * sin(φₙ₋₁) * ... * sin(φ₂) * cos(φ₁)
|
||||
// x₂ = r * sin(φₙ₋₁) * ... * sin(φ₂) * sin(φ₁)
|
||||
// xₖ = r * sin(φₙ₋₁) * ... * sin(φₖ) * cos(φₖ₋₁) for k > 2
|
||||
// xₙ = r * cos(φₙ₋₁)
|
||||
|
||||
let mut sin_product = radius;
|
||||
for &angle in angles.iter().rev().skip(1) {
|
||||
sin_product *= angle.sin();
|
||||
}
|
||||
|
||||
// First two coordinates
|
||||
cartesian.push(sin_product * angles[0].cos());
|
||||
cartesian.push(sin_product * angles[0].sin());
|
||||
|
||||
// Remaining coordinates
|
||||
sin_product = radius;
|
||||
for i in (1..angles.len()).rev() {
|
||||
sin_product *= angles[i].sin();
|
||||
cartesian.push(sin_product * angles[i - 1].cos());
|
||||
}
|
||||
|
||||
// Last coordinate
|
||||
cartesian.push(radius * angles.last().unwrap_or(&0.0).cos());
|
||||
|
||||
// Note: reconstruction may not be perfect for all inputs
|
||||
cartesian.truncate(n);
|
||||
cartesian
|
||||
}
|
||||
|
||||
/// Compute geodesic distance on hypersphere
|
||||
pub fn geodesic_distance(&self, a: &[f32], b: &[f32]) -> f32 {
|
||||
if a.len() != b.len() || a.is_empty() {
|
||||
return f32::MAX;
|
||||
}
|
||||
|
||||
// Normalize to unit sphere
|
||||
let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt().max(1e-10);
|
||||
let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt().max(1e-10);
|
||||
|
||||
// Compute dot product of normalized vectors
|
||||
let dot: f32 = a
|
||||
.iter()
|
||||
.zip(b.iter())
|
||||
.map(|(&x, &y)| (x / norm_a) * (y / norm_b))
|
||||
.sum();
|
||||
|
||||
// Geodesic distance = arccos(dot product)
|
||||
dot.clamp(-1.0, 1.0).acos()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_angular_embedding_project() {
|
||||
let embedding = AngularEmbedding::new(PrecisionLane::Bit5);
|
||||
let values = vec![1.0, 2.0, 3.0, 4.0];
|
||||
let angles = embedding.project(&values);
|
||||
|
||||
assert_eq!(angles.len(), values.len());
|
||||
// All angles should be within bounds
|
||||
for &angle in &angles {
|
||||
assert!(angle.abs() <= PI);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_angular_embedding_roundtrip() {
|
||||
let embedding = AngularEmbedding::new(PrecisionLane::Bit7);
|
||||
let values = vec![1.0, 2.0, 3.0, 4.0];
|
||||
let magnitude = values.iter().map(|x| x * x).sum::<f32>().sqrt();
|
||||
|
||||
let angles = embedding.project(&values);
|
||||
let recovered = embedding.unproject(&angles, magnitude);
|
||||
|
||||
// Should approximately recover original
|
||||
for (&orig, &rec) in values.iter().zip(recovered.iter()) {
|
||||
assert!((orig - rec).abs() < 0.1, "orig={}, rec={}", orig, rec);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_angular_distance() {
|
||||
let embedding = AngularEmbedding::new(PrecisionLane::Bit5);
|
||||
|
||||
let a = vec![1.0, 0.0, 0.0];
|
||||
let b = vec![0.0, 1.0, 0.0];
|
||||
let c = vec![1.0, 0.0, 0.0];
|
||||
|
||||
let dist_ab = embedding.angular_distance(&a, &b);
|
||||
let dist_ac = embedding.angular_distance(&a, &c);
|
||||
|
||||
assert!(dist_ac < 0.001); // Same vectors
|
||||
assert!(dist_ab > 0.0); // Different vectors
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_phase_encoder() {
|
||||
let encoder = PhaseEncoder::new(1.0, 3);
|
||||
|
||||
let e1 = encoder.encode(0.0);
|
||||
let e2 = encoder.encode(0.5);
|
||||
let e3 = encoder.encode(1.0);
|
||||
|
||||
// Different inputs should produce different outputs
|
||||
assert!(e1 != e2);
|
||||
assert!(e2 != e3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_phase_encoder_lut() {
|
||||
let encoder = PhaseEncoder::new(1.0, 1).with_lut(16);
|
||||
|
||||
let e1 = encoder.encode_quantized(0);
|
||||
let e2 = encoder.encode_quantized(8);
|
||||
let e3 = encoder.encode_quantized(15);
|
||||
|
||||
assert!(e1 != e2);
|
||||
assert!(e2 != e3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hyperspherical_projection() {
|
||||
let proj = HypersphericalProjection::new(3, PrecisionLane::Bit5);
|
||||
|
||||
let cartesian = vec![1.0, 0.0, 0.0];
|
||||
let spherical = proj.to_spherical(&cartesian);
|
||||
|
||||
assert_eq!(spherical.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_geodesic_distance() {
|
||||
let proj = HypersphericalProjection::new(3, PrecisionLane::Bit5);
|
||||
|
||||
let a = vec![1.0, 0.0, 0.0];
|
||||
let b = vec![0.0, 1.0, 0.0];
|
||||
let c = vec![1.0, 0.0, 0.0];
|
||||
|
||||
let dist_ab = proj.geodesic_distance(&a, &b);
|
||||
let dist_ac = proj.geodesic_distance(&a, &c);
|
||||
|
||||
assert!(dist_ac < 0.001); // Same direction
|
||||
assert!((dist_ab - PI / 2.0).abs() < 0.001); // Orthogonal = π/2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
//! Deterministic chaos seeding using π digits
|
||||
//!
|
||||
//! π digits are deterministic but appear random. This makes π perfect for:
|
||||
//! - Deterministic jitter
|
||||
//! - Tie-breaking
|
||||
//! - Sampling order
|
||||
//! - Agent scheduling
|
||||
//! - Micro-LoRA update ordering
|
||||
//!
|
||||
//! You get pseudo-randomness without RNG state, clocks, or entropy sources.
|
||||
//! Same input, same behavior, always.
|
||||
//!
|
||||
//! That is gold for witness-logged systems.
|
||||
|
||||
use super::constants::PI_DIGITS;
|
||||
use std::f32::consts::PI;
|
||||
|
||||
/// π-based deterministic chaos generator
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PiChaos {
|
||||
/// Current position in π digit stream
|
||||
position: usize,
|
||||
/// Scale factor for jitter
|
||||
jitter_scale: f32,
|
||||
/// Extended digit buffer (for longer sequences)
|
||||
extended_buffer: Vec<u8>,
|
||||
}
|
||||
|
||||
impl PiChaos {
|
||||
/// Create a new π chaos generator
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
position: 0,
|
||||
jitter_scale: 0.001, // Default: small jitter
|
||||
extended_buffer: PI_DIGITS.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create with custom jitter scale
|
||||
pub fn with_jitter_scale(mut self, scale: f32) -> Self {
|
||||
self.jitter_scale = scale;
|
||||
self
|
||||
}
|
||||
|
||||
/// Get deterministic jitter for an index
|
||||
pub fn jitter(&self, index: usize) -> f32 {
|
||||
let digit_idx = index % PI_DIGITS.len();
|
||||
let digit = PI_DIGITS[digit_idx] as f32;
|
||||
|
||||
// Map digit (0-9) to jitter range
|
||||
(digit - 4.5) / 9.0 * self.jitter_scale
|
||||
}
|
||||
|
||||
/// Get jitter vector for a range of indices
|
||||
pub fn jitter_vector(&self, start: usize, count: usize) -> Vec<f32> {
|
||||
(start..(start + count)).map(|i| self.jitter(i)).collect()
|
||||
}
|
||||
|
||||
/// Get next π digit in sequence
|
||||
pub fn next_digit(&mut self) -> u8 {
|
||||
let digit = self.extended_buffer[self.position];
|
||||
self.position = (self.position + 1) % self.extended_buffer.len();
|
||||
digit
|
||||
}
|
||||
|
||||
/// Get next float in [0, 1) from π digits
|
||||
pub fn next_float(&mut self) -> f32 {
|
||||
// Use 3 digits for ~10 bits of precision
|
||||
let d1 = self.next_digit() as f32;
|
||||
let d2 = self.next_digit() as f32;
|
||||
let d3 = self.next_digit() as f32;
|
||||
|
||||
(d1 * 100.0 + d2 * 10.0 + d3) / 1000.0
|
||||
}
|
||||
|
||||
/// Get next integer in [0, max)
|
||||
pub fn next_int(&mut self, max: usize) -> usize {
|
||||
if max == 0 {
|
||||
return 0;
|
||||
}
|
||||
let f = self.next_float();
|
||||
(f * max as f32) as usize % max
|
||||
}
|
||||
|
||||
/// Reset to beginning of π sequence
|
||||
pub fn reset(&mut self) {
|
||||
self.position = 0;
|
||||
}
|
||||
|
||||
/// Seed at specific position
|
||||
pub fn seed(&mut self, position: usize) {
|
||||
self.position = position % self.extended_buffer.len();
|
||||
}
|
||||
|
||||
/// Generate deterministic permutation of indices
|
||||
pub fn permutation(&mut self, n: usize) -> Vec<usize> {
|
||||
let mut indices: Vec<usize> = (0..n).collect();
|
||||
|
||||
// Fisher-Yates shuffle with π randomness
|
||||
for i in (1..n).rev() {
|
||||
let j = self.next_int(i + 1);
|
||||
indices.swap(i, j);
|
||||
}
|
||||
|
||||
indices
|
||||
}
|
||||
|
||||
/// Get scheduling order for n agents
|
||||
pub fn schedule_order(&self, n: usize, round: usize) -> Vec<usize> {
|
||||
let mut chaos = self.clone();
|
||||
chaos.seed(round * n);
|
||||
chaos.permutation(n)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PiChaos {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Deterministic jitter generator for tie-breaking
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DeterministicJitter {
|
||||
/// Base jitter magnitude
|
||||
magnitude: f32,
|
||||
/// π chaos source
|
||||
chaos: PiChaos,
|
||||
}
|
||||
|
||||
impl DeterministicJitter {
|
||||
/// Create a new jitter generator
|
||||
pub fn new(magnitude: f32) -> Self {
|
||||
Self {
|
||||
magnitude,
|
||||
chaos: PiChaos::new().with_jitter_scale(magnitude),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add jitter to a value
|
||||
pub fn apply(&self, value: f32, index: usize) -> f32 {
|
||||
value + self.chaos.jitter(index)
|
||||
}
|
||||
|
||||
/// Add jitter to a vector
|
||||
pub fn apply_vector(&self, values: &[f32]) -> Vec<f32> {
|
||||
values
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &v)| self.apply(v, i))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Break tie between equal values using index-based jitter
|
||||
pub fn break_tie(&self, value: f32, indices: &[usize]) -> usize {
|
||||
indices
|
||||
.iter()
|
||||
.copied()
|
||||
.max_by(|&a, &b| {
|
||||
let ja = self.chaos.jitter(a);
|
||||
let jb = self.chaos.jitter(b);
|
||||
ja.partial_cmp(&jb).unwrap_or(std::cmp::Ordering::Equal)
|
||||
})
|
||||
.unwrap_or(0)
|
||||
}
|
||||
}
|
||||
|
||||
/// π-based scheduler for deterministic agent/task ordering
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PiScheduler {
|
||||
/// Number of agents/tasks
|
||||
num_items: usize,
|
||||
/// Current round
|
||||
round: usize,
|
||||
/// π chaos source
|
||||
chaos: PiChaos,
|
||||
/// Priority weights (optional)
|
||||
weights: Option<Vec<f32>>,
|
||||
}
|
||||
|
||||
impl PiScheduler {
|
||||
/// Create a new scheduler
|
||||
pub fn new(num_items: usize) -> Self {
|
||||
Self {
|
||||
num_items,
|
||||
round: 0,
|
||||
chaos: PiChaos::new(),
|
||||
weights: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set priority weights
|
||||
pub fn with_weights(mut self, weights: Vec<f32>) -> Self {
|
||||
assert_eq!(weights.len(), self.num_items);
|
||||
self.weights = Some(weights);
|
||||
self
|
||||
}
|
||||
|
||||
/// Get execution order for current round
|
||||
pub fn get_order(&self) -> Vec<usize> {
|
||||
self.chaos.schedule_order(self.num_items, self.round)
|
||||
}
|
||||
|
||||
/// Get weighted execution order
|
||||
pub fn get_weighted_order(&self) -> Vec<usize> {
|
||||
let mut order = self.get_order();
|
||||
|
||||
if let Some(ref weights) = self.weights {
|
||||
// Sort by weight, using π jitter for tie-breaking
|
||||
order.sort_by(|&a, &b| {
|
||||
let wa = weights[a] + self.chaos.jitter(a) * 0.001;
|
||||
let wb = weights[b] + self.chaos.jitter(b) * 0.001;
|
||||
wb.partial_cmp(&wa).unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
}
|
||||
|
||||
order
|
||||
}
|
||||
|
||||
/// Advance to next round
|
||||
pub fn next_round(&mut self) {
|
||||
self.round += 1;
|
||||
}
|
||||
|
||||
/// Reset to round 0
|
||||
pub fn reset(&mut self) {
|
||||
self.round = 0;
|
||||
}
|
||||
|
||||
/// Get item for micro-LoRA update based on π sequence
|
||||
pub fn get_lora_update_order(&self, round: usize) -> Vec<usize> {
|
||||
// For LoRA, we want a different permutation that prioritizes
|
||||
// items with higher impact (measured by weights)
|
||||
let base_order = self.chaos.schedule_order(self.num_items, round);
|
||||
|
||||
if let Some(ref weights) = self.weights {
|
||||
// Interleave high-weight and low-weight items
|
||||
let mut sorted_by_weight: Vec<(usize, f32)> =
|
||||
base_order.iter().map(|&i| (i, weights[i])).collect();
|
||||
sorted_by_weight
|
||||
.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||
|
||||
let mut result = Vec::with_capacity(self.num_items);
|
||||
let high_priority = &sorted_by_weight[..self.num_items / 2];
|
||||
let low_priority = &sorted_by_weight[self.num_items / 2..];
|
||||
|
||||
let mut h = 0;
|
||||
let mut l = 0;
|
||||
for i in 0..self.num_items {
|
||||
if i % 3 < 2 && h < high_priority.len() {
|
||||
result.push(high_priority[h].0);
|
||||
h += 1;
|
||||
} else if l < low_priority.len() {
|
||||
result.push(low_priority[l].0);
|
||||
l += 1;
|
||||
} else if h < high_priority.len() {
|
||||
result.push(high_priority[h].0);
|
||||
h += 1;
|
||||
}
|
||||
}
|
||||
result
|
||||
} else {
|
||||
base_order
|
||||
}
|
||||
}
|
||||
|
||||
/// Get sampling indices for mini-batch
|
||||
pub fn sample_indices(&mut self, batch_size: usize, total: usize) -> Vec<usize> {
|
||||
let mut chaos = self.chaos.clone();
|
||||
chaos.seed(self.round * total);
|
||||
let perm = chaos.permutation(total);
|
||||
perm.into_iter().take(batch_size.min(total)).collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_pi_chaos_deterministic() {
|
||||
let chaos1 = PiChaos::new();
|
||||
let chaos2 = PiChaos::new();
|
||||
|
||||
// Same index = same jitter
|
||||
assert_eq!(chaos1.jitter(0), chaos2.jitter(0));
|
||||
assert_eq!(chaos1.jitter(42), chaos2.jitter(42));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pi_chaos_different_indices() {
|
||||
let chaos = PiChaos::new();
|
||||
|
||||
let j0 = chaos.jitter(0);
|
||||
let j1 = chaos.jitter(1);
|
||||
let j2 = chaos.jitter(2);
|
||||
|
||||
// Different indices should have different jitter
|
||||
// (except by chance if same π digit)
|
||||
assert!(j0 != j1 || j1 != j2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pi_chaos_next_float() {
|
||||
let mut chaos = PiChaos::new();
|
||||
|
||||
let f1 = chaos.next_float();
|
||||
let f2 = chaos.next_float();
|
||||
|
||||
// Should be in [0, 1)
|
||||
assert!(f1 >= 0.0 && f1 < 1.0);
|
||||
assert!(f2 >= 0.0 && f2 < 1.0);
|
||||
|
||||
// Reset should give same sequence
|
||||
chaos.reset();
|
||||
assert_eq!(chaos.next_float(), f1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pi_chaos_permutation() {
|
||||
let mut chaos = PiChaos::new();
|
||||
let perm = chaos.permutation(10);
|
||||
|
||||
// Should contain all elements
|
||||
assert_eq!(perm.len(), 10);
|
||||
let mut sorted = perm.clone();
|
||||
sorted.sort();
|
||||
assert_eq!(sorted, (0..10).collect::<Vec<_>>());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pi_chaos_permutation_deterministic() {
|
||||
let mut chaos1 = PiChaos::new();
|
||||
let mut chaos2 = PiChaos::new();
|
||||
|
||||
let perm1 = chaos1.permutation(20);
|
||||
let perm2 = chaos2.permutation(20);
|
||||
|
||||
assert_eq!(perm1, perm2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deterministic_jitter() {
|
||||
let jitter = DeterministicJitter::new(0.01);
|
||||
|
||||
let values = vec![1.0, 1.0, 1.0, 1.0];
|
||||
let jittered = jitter.apply_vector(&values);
|
||||
|
||||
// All original values were same, but jittered should differ
|
||||
let unique: std::collections::HashSet<_> =
|
||||
jittered.iter().map(|x| (x * 10000.0) as i32).collect();
|
||||
assert!(unique.len() > 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pi_scheduler() {
|
||||
let scheduler = PiScheduler::new(5);
|
||||
let order1 = scheduler.get_order();
|
||||
|
||||
assert_eq!(order1.len(), 5);
|
||||
let mut sorted = order1.clone();
|
||||
sorted.sort();
|
||||
assert_eq!(sorted, vec![0, 1, 2, 3, 4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pi_scheduler_rounds() {
|
||||
let mut scheduler = PiScheduler::new(5);
|
||||
let order_r0 = scheduler.get_order();
|
||||
|
||||
scheduler.next_round();
|
||||
let order_r1 = scheduler.get_order();
|
||||
|
||||
// Different rounds may have different orders
|
||||
// (not guaranteed but likely with π digits)
|
||||
// Just check both are valid permutations
|
||||
assert_eq!(order_r0.len(), 5);
|
||||
assert_eq!(order_r1.len(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pi_scheduler_weighted() {
|
||||
let weights = vec![1.0, 0.5, 2.0, 0.1, 1.5];
|
||||
let scheduler = PiScheduler::new(5).with_weights(weights);
|
||||
let order = scheduler.get_weighted_order();
|
||||
|
||||
// Highest weight (index 2) should be early
|
||||
let pos_2 = order.iter().position(|&x| x == 2).unwrap();
|
||||
assert!(pos_2 < 3, "High weight item should be scheduled early");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_schedule_order_deterministic() {
|
||||
let chaos = PiChaos::new();
|
||||
let order1 = chaos.schedule_order(10, 5);
|
||||
let order2 = chaos.schedule_order(10, 5);
|
||||
assert_eq!(order1, order2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
//! π-derived calibration constants for low-precision systems
|
||||
//!
|
||||
//! Using π (or π-derived constants) for normalization, angular embeddings,
|
||||
//! periodic projections, and phase encoding gives a stable, universal reference
|
||||
//! that doesn't align with powers of two or quantization boundaries.
|
||||
//!
|
||||
//! This avoids resonance artifacts where values collapse into repeating buckets.
|
||||
//! In short: **π breaks symmetry**.
|
||||
|
||||
use crate::precision::PrecisionLane;
|
||||
use std::f32::consts::PI;
|
||||
|
||||
/// π-based scale factor for 3-bit quantization
|
||||
/// Chosen to avoid power-of-2 boundaries
|
||||
pub const PI_SCALE_3BIT: f32 = PI / 4.0; // ~0.785
|
||||
|
||||
/// π-based scale factor for 5-bit quantization
|
||||
pub const PI_SCALE_5BIT: f32 = PI / 16.0; // ~0.196
|
||||
|
||||
/// π-based scale factor for 7-bit quantization
|
||||
pub const PI_SCALE_7BIT: f32 = PI / 64.0; // ~0.049
|
||||
|
||||
/// Golden ratio derived from π for optimal distribution
|
||||
pub const PHI_APPROX: f32 = 2.0 / (PI - 1.0); // ~0.934
|
||||
|
||||
/// First 100 digits of π for deterministic seeding
|
||||
pub const PI_DIGITS: [u8; 100] = [
|
||||
3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9, 3, 2, 3, 8, 4, 6, 2, 6, 4, 3, 3, 8, 3, 2, 7, 9, 5,
|
||||
0, 2, 8, 8, 4, 1, 9, 7, 1, 6, 9, 3, 9, 9, 3, 7, 5, 1, 0, 5, 8, 2, 0, 9, 7, 4, 9, 4, 4, 5, 9, 2,
|
||||
3, 0, 7, 8, 1, 6, 4, 0, 6, 2, 8, 6, 2, 0, 8, 9, 9, 8, 6, 2, 8, 0, 3, 4, 8, 2, 5, 3, 4, 2, 1, 1,
|
||||
7, 0, 6, 7,
|
||||
];
|
||||
|
||||
/// π-derived calibration constants for a precision lane
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct PiCalibration {
|
||||
/// Base scale factor (π / 2^bits)
|
||||
pub scale: f32,
|
||||
/// Phase offset for angular encoding
|
||||
pub phase_offset: f32,
|
||||
/// Normalization factor
|
||||
pub norm_factor: f32,
|
||||
/// Precision lane
|
||||
pub lane: PrecisionLane,
|
||||
/// Anti-resonance offset (prevents bucket collapse)
|
||||
pub anti_resonance: f32,
|
||||
}
|
||||
|
||||
impl PiCalibration {
|
||||
/// Create calibration constants for a precision lane
|
||||
pub fn for_lane(lane: PrecisionLane) -> Self {
|
||||
match lane {
|
||||
PrecisionLane::Bit3 => Self {
|
||||
scale: PI_SCALE_3BIT,
|
||||
phase_offset: PI / 8.0,
|
||||
norm_factor: 3.0 / PI,
|
||||
lane,
|
||||
anti_resonance: Self::compute_anti_resonance(3),
|
||||
},
|
||||
PrecisionLane::Bit5 => Self {
|
||||
scale: PI_SCALE_5BIT,
|
||||
phase_offset: PI / 32.0,
|
||||
norm_factor: 15.0 / PI,
|
||||
lane,
|
||||
anti_resonance: Self::compute_anti_resonance(5),
|
||||
},
|
||||
PrecisionLane::Bit7 => Self {
|
||||
scale: PI_SCALE_7BIT,
|
||||
phase_offset: PI / 128.0,
|
||||
norm_factor: 63.0 / PI,
|
||||
lane,
|
||||
anti_resonance: Self::compute_anti_resonance(7),
|
||||
},
|
||||
PrecisionLane::Float32 => Self {
|
||||
scale: 1.0,
|
||||
phase_offset: 0.0,
|
||||
norm_factor: 1.0,
|
||||
lane,
|
||||
anti_resonance: 0.0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute anti-resonance offset for given bit depth
|
||||
/// Uses π fractional part to avoid power-of-2 alignment
|
||||
fn compute_anti_resonance(bits: u8) -> f32 {
|
||||
let pi_frac = PI - 3.0; // 0.14159...
|
||||
pi_frac / (1 << bits) as f32
|
||||
}
|
||||
|
||||
/// Normalize a value using π-based constants
|
||||
pub fn normalize(&self, value: f32) -> f32 {
|
||||
(value * self.norm_factor + self.anti_resonance) * self.scale
|
||||
}
|
||||
|
||||
/// Denormalize a value
|
||||
pub fn denormalize(&self, value: f32) -> f32 {
|
||||
(value / self.scale - self.anti_resonance) / self.norm_factor
|
||||
}
|
||||
|
||||
/// Apply phase encoding (maps to -π to π range)
|
||||
pub fn phase_encode(&self, value: f32) -> f32 {
|
||||
let normalized = self.normalize(value);
|
||||
(normalized + self.phase_offset).sin() * PI
|
||||
}
|
||||
|
||||
/// Decode phase-encoded value
|
||||
pub fn phase_decode(&self, phase: f32) -> f32 {
|
||||
let normalized = (phase / PI).asin() - self.phase_offset;
|
||||
self.denormalize(normalized)
|
||||
}
|
||||
|
||||
/// Get π-based angular velocity (for streaming updates)
|
||||
pub fn angular_velocity(&self, delta: f32) -> f32 {
|
||||
delta * self.scale * 2.0 * PI
|
||||
}
|
||||
|
||||
/// Quantize with π-based rounding (breaks symmetry)
|
||||
pub fn pi_quantize(&self, value: f32, max_val: i8) -> i8 {
|
||||
let scaled = value * self.norm_factor + self.anti_resonance;
|
||||
let rounded = (scaled + 0.5 * self.anti_resonance).round();
|
||||
(rounded as i8).clamp(-max_val, max_val - 1)
|
||||
}
|
||||
|
||||
/// Dequantize with π-based scaling
|
||||
pub fn pi_dequantize(&self, quantized: i8) -> f32 {
|
||||
((quantized as f32) - self.anti_resonance) / self.norm_factor
|
||||
}
|
||||
}
|
||||
|
||||
/// Angular frequency table for SIMD-friendly operations
|
||||
pub struct AngularFrequencyTable {
|
||||
/// Precomputed sin values at π intervals
|
||||
pub sin_table: [f32; 256],
|
||||
/// Precomputed cos values at π intervals
|
||||
pub cos_table: [f32; 256],
|
||||
/// Table resolution
|
||||
pub resolution: usize,
|
||||
}
|
||||
|
||||
impl AngularFrequencyTable {
|
||||
/// Create a new angular frequency table
|
||||
pub fn new() -> Self {
|
||||
let mut sin_table = [0.0f32; 256];
|
||||
let mut cos_table = [0.0f32; 256];
|
||||
|
||||
for i in 0..256 {
|
||||
let angle = (i as f32) * 2.0 * PI / 256.0;
|
||||
sin_table[i] = angle.sin();
|
||||
cos_table[i] = angle.cos();
|
||||
}
|
||||
|
||||
Self {
|
||||
sin_table,
|
||||
cos_table,
|
||||
resolution: 256,
|
||||
}
|
||||
}
|
||||
|
||||
/// Fast sin approximation using table lookup
|
||||
pub fn fast_sin(&self, angle: f32) -> f32 {
|
||||
let normalized = angle.rem_euclid(2.0 * PI);
|
||||
let index = ((normalized * 256.0 / (2.0 * PI)) as usize) % 256;
|
||||
self.sin_table[index]
|
||||
}
|
||||
|
||||
/// Fast cos approximation using table lookup
|
||||
pub fn fast_cos(&self, angle: f32) -> f32 {
|
||||
let normalized = angle.rem_euclid(2.0 * PI);
|
||||
let index = ((normalized * 256.0 / (2.0 * PI)) as usize) % 256;
|
||||
self.cos_table[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AngularFrequencyTable {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_pi_scales() {
|
||||
assert!((PI_SCALE_3BIT - 0.785).abs() < 0.01);
|
||||
assert!((PI_SCALE_5BIT - 0.196).abs() < 0.01);
|
||||
assert!((PI_SCALE_7BIT - 0.049).abs() < 0.01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calibration_roundtrip() {
|
||||
let cal = PiCalibration::for_lane(PrecisionLane::Bit5);
|
||||
let original = 0.5f32;
|
||||
let normalized = cal.normalize(original);
|
||||
let denormalized = cal.denormalize(normalized);
|
||||
assert!((original - denormalized).abs() < 0.001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_phase_encoding_roundtrip() {
|
||||
let cal = PiCalibration::for_lane(PrecisionLane::Bit7);
|
||||
let original = 0.3f32;
|
||||
let encoded = cal.phase_encode(original);
|
||||
// Phase encoding is lossy for values outside valid range
|
||||
assert!(encoded.is_finite());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pi_quantize() {
|
||||
let cal = PiCalibration::for_lane(PrecisionLane::Bit3);
|
||||
let q = cal.pi_quantize(1.0, 4);
|
||||
assert!(q >= -4 && q <= 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_angular_frequency_table() {
|
||||
let table = AngularFrequencyTable::new();
|
||||
|
||||
// Test at known angles
|
||||
assert!((table.fast_sin(0.0) - 0.0).abs() < 0.03);
|
||||
assert!((table.fast_sin(PI / 2.0) - 1.0).abs() < 0.03);
|
||||
assert!((table.fast_cos(0.0) - 1.0).abs() < 0.03);
|
||||
assert!((table.fast_cos(PI) - (-1.0)).abs() < 0.03);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_anti_resonance_nonzero() {
|
||||
let cal = PiCalibration::for_lane(PrecisionLane::Bit5);
|
||||
assert!(cal.anti_resonance > 0.0);
|
||||
assert!(cal.anti_resonance < 0.01);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
//! π-based drift detection for quantization honesty
|
||||
//!
|
||||
//! Because π cannot be represented exactly at any finite precision, it is
|
||||
//! perfect for detecting distortion. If you:
|
||||
//!
|
||||
//! 1. Project a signal through a π-based transform
|
||||
//! 2. Quantize
|
||||
//! 3. Dequantize
|
||||
//! 4. Project back
|
||||
//!
|
||||
//! Then measure error growth over time, you get a **quantization honesty signal**.
|
||||
//!
|
||||
//! If error grows faster than expected:
|
||||
//! - Precision is too low
|
||||
//! - Accumulation is biased
|
||||
//! - Or hardware is misbehaving
|
||||
//!
|
||||
//! This pairs beautifully with min-cut stability metrics.
|
||||
|
||||
use crate::precision::PrecisionLane;
|
||||
use std::f32::consts::PI;
|
||||
|
||||
/// Expected drift rate per lane (empirically calibrated)
|
||||
const DRIFT_RATE_3BIT: f32 = 0.15; // High drift expected
|
||||
const DRIFT_RATE_5BIT: f32 = 0.05; // Moderate drift
|
||||
const DRIFT_RATE_7BIT: f32 = 0.01; // Low drift
|
||||
const DRIFT_RATE_FLOAT: f32 = 0.0001; // Minimal drift
|
||||
|
||||
/// Drift detector using π transforms
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DriftDetector {
|
||||
/// Precision lane being monitored
|
||||
lane: PrecisionLane,
|
||||
/// Accumulated error
|
||||
accumulated_error: f32,
|
||||
/// Number of samples processed
|
||||
sample_count: usize,
|
||||
/// Error history (ring buffer)
|
||||
error_history: Vec<f32>,
|
||||
/// History index
|
||||
history_idx: usize,
|
||||
/// Expected drift rate for this lane
|
||||
expected_drift_rate: f32,
|
||||
/// π reference signal
|
||||
pi_reference: f32,
|
||||
/// Escalation threshold
|
||||
escalation_threshold: f32,
|
||||
}
|
||||
|
||||
impl DriftDetector {
|
||||
/// Create a new drift detector for a precision lane
|
||||
pub fn new(lane: PrecisionLane) -> Self {
|
||||
let expected_drift_rate = match lane {
|
||||
PrecisionLane::Bit3 => DRIFT_RATE_3BIT,
|
||||
PrecisionLane::Bit5 => DRIFT_RATE_5BIT,
|
||||
PrecisionLane::Bit7 => DRIFT_RATE_7BIT,
|
||||
PrecisionLane::Float32 => DRIFT_RATE_FLOAT,
|
||||
};
|
||||
|
||||
Self {
|
||||
lane,
|
||||
accumulated_error: 0.0,
|
||||
sample_count: 0,
|
||||
error_history: vec![0.0; 64], // Rolling window
|
||||
history_idx: 0,
|
||||
expected_drift_rate,
|
||||
pi_reference: PI,
|
||||
escalation_threshold: expected_drift_rate * 3.0, // 3x expected = escalate
|
||||
}
|
||||
}
|
||||
|
||||
/// Check quantization honesty between original and quantized values
|
||||
pub fn check(&mut self, original: &[f32], quantized: &[f32]) -> QuantizationHonesty {
|
||||
assert_eq!(original.len(), quantized.len());
|
||||
|
||||
// Apply π transform to both
|
||||
let pi_original: Vec<f32> = original.iter().map(|&x| self.pi_transform(x)).collect();
|
||||
let pi_quantized: Vec<f32> = quantized.iter().map(|&x| self.pi_transform(x)).collect();
|
||||
|
||||
// Compute error after π projection
|
||||
let error = self.compute_error(&pi_original, &pi_quantized);
|
||||
self.update(error);
|
||||
|
||||
// Check if error is within expected bounds
|
||||
let ratio = error / self.expected_drift_rate.max(0.0001);
|
||||
let is_honest = ratio < 2.0;
|
||||
let should_escalate = ratio > 3.0;
|
||||
|
||||
QuantizationHonesty {
|
||||
error,
|
||||
expected_error: self.expected_drift_rate,
|
||||
ratio,
|
||||
is_honest,
|
||||
should_escalate,
|
||||
sample_count: self.sample_count,
|
||||
}
|
||||
}
|
||||
|
||||
/// π transform: project value through π-based trigonometric function
|
||||
fn pi_transform(&self, value: f32) -> f32 {
|
||||
// Use both sin and cos to capture full information
|
||||
let angle = value * self.pi_reference;
|
||||
angle.sin() + angle.cos() * 0.5
|
||||
}
|
||||
|
||||
/// Inverse π transform (approximate)
|
||||
fn inverse_pi_transform(&self, transformed: f32) -> f32 {
|
||||
// This is lossy by design - the difference measures drift
|
||||
let angle = transformed.atan2(1.0);
|
||||
angle / self.pi_reference
|
||||
}
|
||||
|
||||
/// Compute mean squared error between transformed vectors
|
||||
fn compute_error(&self, a: &[f32], b: &[f32]) -> f32 {
|
||||
if a.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let mse: f32 = a
|
||||
.iter()
|
||||
.zip(b.iter())
|
||||
.map(|(&x, &y)| (x - y).powi(2))
|
||||
.sum::<f32>()
|
||||
/ a.len() as f32;
|
||||
|
||||
mse.sqrt()
|
||||
}
|
||||
|
||||
/// Update drift tracking with new error sample
|
||||
pub fn update(&mut self, error: f32) {
|
||||
self.accumulated_error += error;
|
||||
self.sample_count += 1;
|
||||
|
||||
// Update rolling history
|
||||
self.error_history[self.history_idx] = error;
|
||||
self.history_idx = (self.history_idx + 1) % self.error_history.len();
|
||||
}
|
||||
|
||||
/// Get drift report
|
||||
pub fn report(&self) -> DriftReport {
|
||||
let mean_error = if self.sample_count > 0 {
|
||||
self.accumulated_error / self.sample_count as f32
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
// Compute trend from history
|
||||
let trend = self.compute_trend();
|
||||
|
||||
// Check if drift is accelerating
|
||||
let is_accelerating = trend > self.expected_drift_rate * 0.1;
|
||||
|
||||
DriftReport {
|
||||
mean_error,
|
||||
accumulated_error: self.accumulated_error,
|
||||
sample_count: self.sample_count,
|
||||
trend,
|
||||
is_accelerating,
|
||||
should_escalate: mean_error > self.escalation_threshold,
|
||||
lane: self.lane,
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute error trend (slope of recent errors)
|
||||
fn compute_trend(&self) -> f32 {
|
||||
if self.sample_count < 2 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let n = self.error_history.len().min(self.sample_count);
|
||||
if n < 2 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Simple linear regression on recent errors
|
||||
let mut sum_x = 0.0f32;
|
||||
let mut sum_y = 0.0f32;
|
||||
let mut sum_xy = 0.0f32;
|
||||
let mut sum_xx = 0.0f32;
|
||||
|
||||
for i in 0..n {
|
||||
let x = i as f32;
|
||||
let y = self.error_history[i];
|
||||
sum_x += x;
|
||||
sum_y += y;
|
||||
sum_xy += x * y;
|
||||
sum_xx += x * x;
|
||||
}
|
||||
|
||||
let n_f = n as f32;
|
||||
let denominator = n_f * sum_xx - sum_x * sum_x;
|
||||
if denominator.abs() < 1e-10 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
(n_f * sum_xy - sum_x * sum_y) / denominator
|
||||
}
|
||||
|
||||
/// Reset drift tracking
|
||||
pub fn reset(&mut self) {
|
||||
self.accumulated_error = 0.0;
|
||||
self.sample_count = 0;
|
||||
self.error_history.fill(0.0);
|
||||
self.history_idx = 0;
|
||||
}
|
||||
|
||||
/// Run π checksum on a signal (deterministic honesty test)
|
||||
pub fn pi_checksum(&self, signal: &[f32]) -> f32 {
|
||||
if signal.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Accumulate through π transform
|
||||
let mut checksum = 0.0f32;
|
||||
for (i, &val) in signal.iter().enumerate() {
|
||||
let pi_phase = (i as f32 + 1.0) * PI / signal.len() as f32;
|
||||
checksum += val * pi_phase.sin();
|
||||
}
|
||||
|
||||
checksum / signal.len() as f32
|
||||
}
|
||||
|
||||
/// Verify π checksum after quantization
|
||||
pub fn verify_checksum(&self, original: &[f32], quantized: &[f32]) -> bool {
|
||||
let orig_checksum = self.pi_checksum(original);
|
||||
let quant_checksum = self.pi_checksum(quantized);
|
||||
|
||||
let error = (orig_checksum - quant_checksum).abs();
|
||||
error < self.expected_drift_rate
|
||||
}
|
||||
}
|
||||
|
||||
/// Quantization honesty result
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct QuantizationHonesty {
|
||||
/// Actual error measured
|
||||
pub error: f32,
|
||||
/// Expected error for this precision lane
|
||||
pub expected_error: f32,
|
||||
/// Ratio of actual to expected (>1 = worse than expected)
|
||||
pub ratio: f32,
|
||||
/// Is the quantization honest (within 2x expected)?
|
||||
pub is_honest: bool,
|
||||
/// Should we escalate to higher precision?
|
||||
pub should_escalate: bool,
|
||||
/// Number of samples in this measurement
|
||||
pub sample_count: usize,
|
||||
}
|
||||
|
||||
/// Drift report summary
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DriftReport {
|
||||
/// Mean error over all samples
|
||||
pub mean_error: f32,
|
||||
/// Total accumulated error
|
||||
pub accumulated_error: f32,
|
||||
/// Number of samples processed
|
||||
pub sample_count: usize,
|
||||
/// Error trend (positive = getting worse)
|
||||
pub trend: f32,
|
||||
/// Is drift accelerating?
|
||||
pub is_accelerating: bool,
|
||||
/// Should escalate precision lane?
|
||||
pub should_escalate: bool,
|
||||
/// Current precision lane
|
||||
pub lane: PrecisionLane,
|
||||
}
|
||||
|
||||
impl DriftReport {
|
||||
/// Get severity level (0-3)
|
||||
pub fn severity(&self) -> u8 {
|
||||
if self.should_escalate {
|
||||
3
|
||||
} else if self.is_accelerating {
|
||||
2
|
||||
} else if self.mean_error > 0.05 {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// Suggested next lane
|
||||
pub fn suggested_lane(&self) -> Option<PrecisionLane> {
|
||||
if self.should_escalate {
|
||||
match self.lane {
|
||||
PrecisionLane::Bit3 => Some(PrecisionLane::Bit5),
|
||||
PrecisionLane::Bit5 => Some(PrecisionLane::Bit7),
|
||||
PrecisionLane::Bit7 => Some(PrecisionLane::Float32),
|
||||
PrecisionLane::Float32 => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_drift_detector_creation() {
|
||||
let detector = DriftDetector::new(PrecisionLane::Bit5);
|
||||
assert_eq!(detector.sample_count, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pi_transform_deterministic() {
|
||||
let detector = DriftDetector::new(PrecisionLane::Bit5);
|
||||
let v1 = detector.pi_transform(0.5);
|
||||
let v2 = detector.pi_transform(0.5);
|
||||
assert_eq!(v1, v2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_honesty_check_identical() {
|
||||
let mut detector = DriftDetector::new(PrecisionLane::Bit7);
|
||||
let values = vec![0.1, 0.2, 0.3, 0.4, 0.5];
|
||||
let honesty = detector.check(&values, &values);
|
||||
assert!(honesty.error < 0.001);
|
||||
assert!(honesty.is_honest);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_honesty_check_with_error() {
|
||||
let mut detector = DriftDetector::new(PrecisionLane::Bit3);
|
||||
let original = vec![0.1, 0.2, 0.3, 0.4, 0.5];
|
||||
let quantized = vec![0.15, 0.25, 0.35, 0.45, 0.55]; // 0.05 error each
|
||||
let honesty = detector.check(&original, &quantized);
|
||||
assert!(honesty.error > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_drift_report() {
|
||||
let mut detector = DriftDetector::new(PrecisionLane::Bit5);
|
||||
detector.update(0.01);
|
||||
detector.update(0.02);
|
||||
detector.update(0.03);
|
||||
|
||||
let report = detector.report();
|
||||
assert_eq!(report.sample_count, 3);
|
||||
assert!(report.mean_error > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pi_checksum() {
|
||||
let detector = DriftDetector::new(PrecisionLane::Bit5);
|
||||
let signal = vec![1.0, 2.0, 3.0, 4.0, 5.0];
|
||||
let checksum = detector.pi_checksum(&signal);
|
||||
assert!(checksum.is_finite());
|
||||
|
||||
// Deterministic
|
||||
assert_eq!(detector.pi_checksum(&signal), checksum);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_checksum() {
|
||||
let detector = DriftDetector::new(PrecisionLane::Bit7);
|
||||
let original = vec![1.0, 2.0, 3.0, 4.0, 5.0];
|
||||
let nearly_same = vec![1.001, 2.001, 3.001, 4.001, 5.001];
|
||||
assert!(detector.verify_checksum(&original, &nearly_same));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_severity_levels() {
|
||||
let report = DriftReport {
|
||||
mean_error: 0.5,
|
||||
accumulated_error: 1.0,
|
||||
sample_count: 2,
|
||||
trend: 0.1,
|
||||
is_accelerating: true,
|
||||
should_escalate: true,
|
||||
lane: PrecisionLane::Bit3,
|
||||
};
|
||||
assert_eq!(report.severity(), 3);
|
||||
assert_eq!(report.suggested_lane(), Some(PrecisionLane::Bit5));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
//! π (Pi) Integration Module - Structural Constants for Low-Precision Systems
|
||||
//!
|
||||
//! π is irrational, non-repeating, and structure-rich. This makes it an ideal
|
||||
//! reference signal in systems where precision is constrained.
|
||||
//!
|
||||
//! # Why π Matters
|
||||
//!
|
||||
//! In 3/5/7-bit math, you deliberately throw away bits. π lets you check whether
|
||||
//! the system is still behaving honestly.
|
||||
//!
|
||||
//! # Module Components
|
||||
//!
|
||||
//! - **Calibration**: π-derived constants for normalization and phase encoding
|
||||
//! - **Drift Detection**: Quantization honesty signals using π transforms
|
||||
//! - **Angular Embeddings**: Hyperspherical embeddings with π phase encoding
|
||||
//! - **Chaos Seeding**: Deterministic pseudo-randomness from π digits
|
||||
//!
|
||||
//! # Key Insight
|
||||
//!
|
||||
//! π is not about geometry here. It is about injecting infinite structure into
|
||||
//! finite machines without breaking determinism.
|
||||
//!
|
||||
//! This pairs with:
|
||||
//! - Min-cut as coherence
|
||||
//! - Vectors as motion
|
||||
//! - Agents as reflexes
|
||||
//! - Precision as policy
|
||||
|
||||
pub mod angular;
|
||||
pub mod chaos;
|
||||
pub mod constants;
|
||||
pub mod drift;
|
||||
|
||||
pub use angular::{AngularEmbedding, HypersphericalProjection, PhaseEncoder};
|
||||
pub use chaos::{DeterministicJitter, PiChaos, PiScheduler};
|
||||
pub use constants::{PiCalibration, PI_SCALE_3BIT, PI_SCALE_5BIT, PI_SCALE_7BIT};
|
||||
pub use drift::{DriftDetector, DriftReport, QuantizationHonesty};
|
||||
|
||||
use crate::precision::PrecisionLane;
|
||||
|
||||
/// π-aware quantization context that tracks honesty metrics
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PiContext {
|
||||
/// Calibration constants
|
||||
pub calibration: PiCalibration,
|
||||
/// Drift detector for quantization honesty
|
||||
pub drift: DriftDetector,
|
||||
/// Angular embedding projector
|
||||
pub angular: AngularEmbedding,
|
||||
/// Chaos seeder for deterministic jitter
|
||||
pub chaos: PiChaos,
|
||||
/// Current precision lane
|
||||
pub lane: PrecisionLane,
|
||||
}
|
||||
|
||||
impl PiContext {
|
||||
/// Create a new π context for a precision lane
|
||||
pub fn new(lane: PrecisionLane) -> Self {
|
||||
Self {
|
||||
calibration: PiCalibration::for_lane(lane),
|
||||
drift: DriftDetector::new(lane),
|
||||
angular: AngularEmbedding::new(lane),
|
||||
chaos: PiChaos::new(),
|
||||
lane,
|
||||
}
|
||||
}
|
||||
|
||||
/// Calibrate a value using π-derived constants
|
||||
pub fn calibrate(&self, value: f32) -> f32 {
|
||||
self.calibration.normalize(value)
|
||||
}
|
||||
|
||||
/// Check quantization honesty
|
||||
pub fn check_honesty(&mut self, original: &[f32], quantized: &[f32]) -> QuantizationHonesty {
|
||||
self.drift.check(original, quantized)
|
||||
}
|
||||
|
||||
/// Project to angular space
|
||||
pub fn to_angular(&self, values: &[f32]) -> Vec<f32> {
|
||||
self.angular.project(values)
|
||||
}
|
||||
|
||||
/// Get deterministic jitter for tie-breaking
|
||||
pub fn jitter(&self, index: usize) -> f32 {
|
||||
self.chaos.jitter(index)
|
||||
}
|
||||
|
||||
/// Update drift tracking
|
||||
pub fn update_drift(&mut self, error: f32) {
|
||||
self.drift.update(error);
|
||||
}
|
||||
|
||||
/// Get drift report
|
||||
pub fn drift_report(&self) -> DriftReport {
|
||||
self.drift.report()
|
||||
}
|
||||
|
||||
/// Should escalate precision lane?
|
||||
pub fn should_escalate(&self) -> bool {
|
||||
self.drift.report().should_escalate
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PiContext {
|
||||
fn default() -> Self {
|
||||
Self::new(PrecisionLane::Bit5)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_pi_context_creation() {
|
||||
let ctx = PiContext::new(PrecisionLane::Bit3);
|
||||
assert_eq!(ctx.lane, PrecisionLane::Bit3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pi_context_calibration() {
|
||||
let ctx = PiContext::new(PrecisionLane::Bit5);
|
||||
let calibrated = ctx.calibrate(1.0);
|
||||
assert!(calibrated.is_finite());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pi_context_angular_projection() {
|
||||
let ctx = PiContext::new(PrecisionLane::Bit7);
|
||||
let values = vec![1.0, 2.0, 3.0, 4.0];
|
||||
let angular = ctx.to_angular(&values);
|
||||
assert_eq!(angular.len(), values.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pi_context_jitter() {
|
||||
let ctx = PiContext::new(PrecisionLane::Bit5);
|
||||
let j1 = ctx.jitter(0);
|
||||
let j2 = ctx.jitter(1);
|
||||
// Deterministic: same index = same jitter
|
||||
assert_eq!(ctx.jitter(0), j1);
|
||||
// Different indices = different jitter
|
||||
assert_ne!(j1, j2);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user