Surface realizer join: pulse output_versor → vault recall → ground_graph fills <pending> obj slots with recalled words → realize_semantic produces deterministic sentences. PulseResult replaces bare word list. Every intent type surfaces. Rust backend parity: unitize_f32 (exponential-map with boost/rotation blade distinction) and graph_diffusion_step now in core-rs. Python dispatches through algebra.backend, falls back transparently. 37x speedup on 200-step diffusion. Benchmark harness (core bench): determinism (100% trace stability), latency (~150ms median), backend speedup, versor closure audit (0 violations across all intermediate states), convergence proof (41/45 exact, 4 bounded oscillation), realizer coverage (8/8 intent types). Proof property tests (31 tests): Rust/Python parity, pulse determinism across prompts, V3 convergence for 10+ topologies, coupled V4 output validity, realizer coverage per intent, versor closure at every intermediate step. CLI: core pulse, core bench, core test --suite pulse, core test --suite proof. Fix test_correction_pulls_toward_target (diffuse first, then correct).
192 lines
5.5 KiB
Rust
192 lines
5.5 KiB
Rust
//! Graph diffusion operator and exponential-map unitizer.
|
|
//!
|
|
//! These are the hot-path operations for the pulse loop.
|
|
//! `unitize_f32` builds a proper rotor from bivector content via the
|
|
//! exponential map, distinguishing boost planes (cosh/sinh) from
|
|
//! rotation planes (cos/sin) in Cl(4,1).
|
|
//!
|
|
//! `graph_diffusion_step` runs one forward pass of damped blending
|
|
//! across all graph edges, re-unitizing each touched node.
|
|
|
|
use crate::cl41::geometric_product_f64;
|
|
use std::collections::HashMap;
|
|
|
|
/// Blade indices 9, 12, 14, 15 square to +1 (boost/hyperbolic planes involving e5).
|
|
/// Remaining bivector indices (6-8, 10-11, 13) square to -1 (rotation planes).
|
|
const BOOST_INDICES: [usize; 4] = [9, 12, 14, 15];
|
|
|
|
fn is_boost(blade_idx: usize) -> bool {
|
|
matches!(blade_idx, 9 | 12 | 14 | 15)
|
|
}
|
|
|
|
/// Unitize a multivector to versor condition via the exponential map.
|
|
///
|
|
/// Works in f64 throughout, returns f32. Matches the Python `_unitize_f32`
|
|
/// in `field/operators.py` exactly.
|
|
pub fn unitize_f32(v: &[f32; 32]) -> [f32; 32] {
|
|
let v64: [f64; 32] = {
|
|
let mut arr = [0f64; 32];
|
|
for i in 0..32 { arr[i] = v[i] as f64; }
|
|
arr
|
|
};
|
|
|
|
let norm: f64 = v64.iter().map(|x| x * x).sum::<f64>().sqrt();
|
|
if norm < 1e-12 {
|
|
let mut out = [0f32; 32];
|
|
out[0] = 1.0;
|
|
return out;
|
|
}
|
|
|
|
// Extract bivector content (indices 6..16)
|
|
let bv: [f64; 10] = {
|
|
let mut arr = [0f64; 10];
|
|
for i in 0..10 { arr[i] = v64[6 + i]; }
|
|
arr
|
|
};
|
|
let bv_norm: f64 = bv.iter().map(|x| x * x).sum::<f64>().sqrt();
|
|
if bv_norm < 1e-14 {
|
|
let mut out = [0f32; 32];
|
|
out[0] = if v64[0] >= 0.0 { 1.0 } else { -1.0 };
|
|
return out;
|
|
}
|
|
|
|
let angle = bv_norm.atan2(v64[0].abs());
|
|
|
|
let mut rotor = [0f64; 32];
|
|
rotor[0] = 1.0;
|
|
|
|
for i in 0..10usize {
|
|
let w = bv[i] / bv_norm;
|
|
if w.abs() < 1e-14 { continue; }
|
|
let theta = angle * w;
|
|
let mut factor = [0f64; 32];
|
|
let blade_idx = 6 + i;
|
|
if is_boost(blade_idx) {
|
|
factor[0] = theta.cosh();
|
|
factor[blade_idx] = theta.sinh();
|
|
} else {
|
|
factor[0] = theta.cos();
|
|
factor[blade_idx] = theta.sin();
|
|
}
|
|
rotor = geometric_product_f64(&rotor, &factor);
|
|
}
|
|
|
|
if v64[0] < 0.0 {
|
|
for x in rotor.iter_mut() { *x = -*x; }
|
|
}
|
|
|
|
let mut result = [0f32; 32];
|
|
for i in 0..32 { result[i] = rotor[i] as f32; }
|
|
result
|
|
}
|
|
|
|
/// One forward step of graph diffusion.
|
|
///
|
|
/// For each node that has incoming edges, blend it with the average
|
|
/// of its neighbors, then re-unitize via the exponential map.
|
|
///
|
|
/// Returns (new_fields, delta) where delta is L2 norm of change.
|
|
pub fn graph_diffusion_step(
|
|
fields: &[[f32; 32]],
|
|
edges: &[[i32; 2]],
|
|
damping: f64,
|
|
) -> (Vec<[f32; 32]>, f64) {
|
|
let n = fields.len();
|
|
let mut new_fields: Vec<[f32; 32]> = fields.to_vec();
|
|
|
|
// Build neighbor map: dst -> [src, ...]
|
|
let mut neighbors: HashMap<usize, Vec<usize>> = HashMap::new();
|
|
for edge in edges {
|
|
let dst = edge[1] as usize;
|
|
let src = edge[0] as usize;
|
|
neighbors.entry(dst).or_default().push(src);
|
|
}
|
|
|
|
for (&node, srcs) in &neighbors {
|
|
if node >= n || srcs.is_empty() { continue; }
|
|
|
|
// Current node in f64
|
|
let mut f = [0f64; 32];
|
|
for i in 0..32 { f[i] = fields[node][i] as f64; }
|
|
|
|
// Neighbor average in f64
|
|
let mut avg = [0f64; 32];
|
|
for &src in srcs {
|
|
for i in 0..32 { avg[i] += fields[src][i] as f64; }
|
|
}
|
|
let inv = 1.0 / srcs.len() as f64;
|
|
for x in avg.iter_mut() { *x *= inv; }
|
|
|
|
// Blend
|
|
let mut blended = [0f32; 32];
|
|
for i in 0..32 {
|
|
blended[i] = ((1.0 - damping) * f[i] + damping * avg[i]) as f32;
|
|
}
|
|
new_fields[node] = unitize_f32(&blended);
|
|
}
|
|
|
|
// Compute delta
|
|
let mut delta_sq = 0f64;
|
|
for i in 0..n {
|
|
for j in 0..32 {
|
|
let d = (new_fields[i][j] - fields[i][j]) as f64;
|
|
delta_sq += d * d;
|
|
}
|
|
}
|
|
|
|
(new_fields, delta_sq.sqrt())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn identity() -> [f32; 32] {
|
|
let mut v = [0f32; 32];
|
|
v[0] = 1.0;
|
|
v
|
|
}
|
|
|
|
#[test]
|
|
fn unitize_identity_is_identity() {
|
|
let id = identity();
|
|
let result = unitize_f32(&id);
|
|
assert!((result[0] - 1.0).abs() < 1e-5);
|
|
for i in 1..32 {
|
|
assert!(result[i].abs() < 1e-5, "component {} = {}", i, result[i]);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn unitize_zero_returns_identity() {
|
|
let zero = [0f32; 32];
|
|
let result = unitize_f32(&zero);
|
|
assert!((result[0] - 1.0).abs() < 1e-5);
|
|
}
|
|
|
|
#[test]
|
|
fn unitize_preserves_versor_condition() {
|
|
use crate::versor::versor_condition_raw;
|
|
let mut v = [0f32; 32];
|
|
v[0] = 0.8;
|
|
v[6] = 0.3;
|
|
v[9] = 0.2; // boost blade
|
|
let result = unitize_f32(&v);
|
|
let cond = versor_condition_raw(&result).unwrap();
|
|
assert!(cond < 1e-4, "versor condition {} too large", cond);
|
|
}
|
|
|
|
#[test]
|
|
fn diffusion_step_reduces_delta_over_iterations() {
|
|
let mut fields = vec![identity(); 3];
|
|
// Perturb node 1
|
|
fields[1][0] = 0.9;
|
|
fields[1][6] = 0.1;
|
|
fields[1] = unitize_f32(&fields[1]);
|
|
|
|
let edges = vec![[0i32, 2], [1, 2]];
|
|
let (f1, d1) = graph_diffusion_step(&fields, &edges, 0.5);
|
|
let (_, d2) = graph_diffusion_step(&f1, &edges, 0.5);
|
|
assert!(d2 < d1, "delta should decrease: d1={}, d2={}", d1, d2);
|
|
}
|
|
}
|