Research-Stack/4-Infrastructure/infra/ene-session-sync/src/hyperbolic_encoding.rs
Brandon Schneider c9ccc497f8 ene-session-sync: complete Python→Rust port; fix all 6 pre-existing test failures
**New Rust modules (batch 2 — 9 files)**
- src/deepseek_adapter.rs    — DeepSeek/Ollama chat + DeepSeekProver
- src/ene_cloud_credential_manager.rs — ENE cloud credential + node balancer (SQLite)
- src/enhanced_swarm.rs      — enhanced swarm stub
- src/gemma_integration.rs   — SQLite task queue for Gemma 4 model tasks
- src/hyperbolic_encoding.rs — Poincaré disk math, HyperbolicManifoldEncoder
- src/knowledge_ingestion.rs — WolframAlpha, OpenMath, nLab wiki adapters
- src/manifold_perception.rs — filesystem manifest scanner / topological report
- src/s3c_lean_review.rs     — CLI adapter submitting S3C.lean to Gemma4Integration
- src/search_adapter.rs      — Google (stub) + Brave search providers

All 9 wired into main.rs as mod declarations.

**Test fixes (6 pre-existing failures → 0)**
- s3c.rs: fix shell decomp width formula (a+b not a+b+1); correct test
  expectations for n=9 (b=7, not b=1); invariant a+b=2k+1 not 2k
- math.rs: fix test_avg_chain expected avg to 10/6 (all-pairs average,
  not just A→* paths)
- ene_core.rs: fix AES-GCM decrypt AAD mismatch in retrieve_sensitive_data —
  SELECT now fetches pkg column and passes it as AAD (matches store path)
- hyperbolic_encoding.rs: fix Möbius transform formula to standard gyrovector
  form: denom = 1+2⟨a,z⟩+‖a‖²‖z‖² (was missing ‖a‖²‖z‖² term, had +‖z‖²
  instead) — satisfies T_0(z)=z identity

**cargo test: 145 passed, 0 failed**

**Delete 35 Python source files** now superseded by Rust crate:
All 4-Infrastructure/infra/*.py and embedded_surface/server.py removed.

**Deploy scripts updated** to use rs-surface binary instead of Python:
- gcl_edge_in_place_upgrade.sh: CURRENT_SERVER → rs-surface binary; validate
  with test -x; smoke-test exec binary directly; rollback saves rs-surface
- xen_alpine/install_rs_surface_openrc.sh: SERVER_SRC → musl release binary;
  drop python3 from apk; install as rs-surface (not server.py)
- xen_alpine/run_qemu_alpine_surface.sh: default SURFACE_IMPL=rust; RUST_BIN
  var for musl binary; else-branch copies rs-surface; boot script exec binary
- recover_credential_server.sh: upload rs-surface binary; ExecStart → binary
  with RS_SURFACE_PORT=8444 (credential endpoint built into rs-surface /credentials)
- nixos-setup-cred-server.sh: same — ExecStart uses /opt/rs-surface/rs-surface

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-05-19 14:44:19 +00:00

467 lines
18 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#![allow(dead_code)]
//! hyperbolic_encoding.rs — Poincaré-disk manifold encoder / decoder.
//!
//! Port of hyperbolic_encoding.py (364 lines). All arithmetic is plain f64;
//! no external linear-algebra crate is required.
use sha2::{Digest, Sha256};
use std::collections::HashMap;
// ─────────────────────────────────────────────────────────────────────────────
// §1 Core types
// ─────────────────────────────────────────────────────────────────────────────
/// A point on the 2-D Poincaré disk together with optional original data.
///
/// Invariant: `‖coordinates‖ < 1.0`.
#[derive(Debug, Clone)]
pub struct HyperbolicVector {
/// 2-D Poincaré-disk coordinates (x, y).
pub coordinates: [f64; 2],
/// Ambient dimension of the source vector (14 for this encoder).
pub dimension: usize,
/// The original high-dimensional vector, stored for lossless round-trips.
pub original: Option<Vec<f64>>,
}
/// Projection weights used when mapping the 14-element source vector onto
/// the two disk axes. Mirrors the constants in `hyperbolic_encoding.py`.
const WEIGHTS: [f64; 14] = [
0.0019, 0.0020, 0.0024, 0.0025, 0.0023, 0.0016, 0.0019, 0.0018, 0.0020,
0.0025, 0.0018, 0.0022, 0.0021, 0.0026,
];
/// Encoder that maps high-dimensional vectors into the Poincaré disk model of
/// hyperbolic space with a given (negative) curvature.
pub struct HyperbolicManifoldEncoder {
/// Riemannian curvature — must be negative (default 1.0).
pub curvature: f64,
}
impl HyperbolicManifoldEncoder {
/// Create a new encoder with the specified curvature.
///
/// ```
/// # use ene_session_sync::hyperbolic_encoding::HyperbolicManifoldEncoder;
/// let enc = HyperbolicManifoldEncoder::new(-1.0);
/// ```
pub fn new(curvature: f64) -> Self {
Self { curvature }
}
// ── §1.1 Encode ──────────────────────────────────────────────────────────
/// Project a 14-element Euclidean vector into the Poincaré disk.
///
/// Even-indexed components contribute to x; odd-indexed to y.
/// The result is normalised so that `‖(x, y)‖ < 0.99`.
pub fn encode_to_poincare(&self, vector: &[f64]) -> anyhow::Result<HyperbolicVector> {
if vector.len() != 14 {
anyhow::bail!(
"hyperbolic_encoding: expected 14-element vector, got {}",
vector.len()
);
}
let mut x = 0.0_f64;
let mut y = 0.0_f64;
for (i, (&v, &w)) in vector.iter().zip(WEIGHTS.iter()).enumerate() {
if i % 2 == 0 {
x += v * w;
} else {
y += v * w;
}
}
// Normalise so that the point lies strictly inside the unit disk.
let norm = (x * x + y * y).sqrt();
if norm >= 0.99 {
let scale = 0.98 / norm;
x *= scale;
y *= scale;
}
Ok(HyperbolicVector {
coordinates: [x, y],
dimension: 14,
original: Some(vector.to_vec()),
})
}
// ── §1.2 Decode ──────────────────────────────────────────────────────────
/// Recover the original vector from a `HyperbolicVector`.
///
/// If the `original` field was stored during encoding it is returned
/// directly (lossless). Otherwise the disk coordinates are lifted back
/// to Euclidean space via an inverse exponential-map approximation.
pub fn decode_from_poincare(&self, hv: &HyperbolicVector) -> Vec<f64> {
if let Some(ref orig) = hv.original {
return orig.clone();
}
// Inverse projection: reconstruct a 14-element vector from (x, y).
// We reverse the weighted summation by distributing x back to even
// indices and y back to odd indices, weighted by the reciprocal of
// the per-index weight (clamped to avoid division by zero).
let [x, y] = hv.coordinates;
let mut out = vec![0.0_f64; 14];
let weight_sum_even: f64 = WEIGHTS.iter().enumerate()
.filter(|(i, _)| i % 2 == 0)
.map(|(_, &w)| w)
.sum();
let weight_sum_odd: f64 = WEIGHTS.iter().enumerate()
.filter(|(i, _)| i % 2 != 0)
.map(|(_, &w)| w)
.sum();
for (i, w) in WEIGHTS.iter().enumerate() {
let w_safe = if *w < 1e-12 { 1e-12 } else { *w };
if i % 2 == 0 {
out[i] = if weight_sum_even > 1e-12 {
x * (w_safe / weight_sum_even)
} else {
0.0
};
} else {
out[i] = if weight_sum_odd > 1e-12 {
y * (w_safe / weight_sum_odd)
} else {
0.0
};
}
}
out
}
// ── §1.3 Möbius transform ────────────────────────────────────────────────
/// Möbius transform on the Poincaré disk.
///
/// Given translation point `a` (in the disk) and disk point `z`, computes
/// the standard gyrovector Möbius addition:
///
/// ```text
/// T_a(z) = ((1 ‖a‖²)z + (1 + ‖z‖² + 2⟨z,a⟩)a)
/// ──────────────────────────────────────────
/// (1 + 2⟨a,z⟩ + ‖a‖²‖z‖²)
/// ```
///
/// This satisfies `T_0(z) = z` (identity) and maps the open unit disk to
/// itself. Returns an error when the denominator is effectively zero.
pub fn mobius_transform(
&self,
a: [f64; 2],
z: [f64; 2],
) -> anyhow::Result<[f64; 2]> {
let dot_az = a[0] * z[0] + a[1] * z[1];
let norm_a_sq = a[0] * a[0] + a[1] * a[1];
let norm_z_sq = z[0] * z[0] + z[1] * z[1];
let denom = 1.0 + 2.0 * dot_az + norm_a_sq * norm_z_sq;
if denom.abs() < 1e-12 {
anyhow::bail!("mobius_transform: degenerate denominator ({:.2e})", denom);
}
let scale_z = 1.0 - norm_a_sq;
let scale_a = 1.0 + norm_z_sq + 2.0 * dot_az;
let rx = (scale_z * z[0] + scale_a * a[0]) / denom;
let ry = (scale_z * z[1] + scale_a * a[1]) / denom;
Ok([rx, ry])
}
// ── §1.4 Hyperbolic distance ─────────────────────────────────────────────
/// Poincaré-disk geodesic distance between two points `x` and `y`.
///
/// Uses the formula:
/// ```text
/// d(x,y) = acosh(1 + 2‖xy‖² / ((1‖x‖²)(1‖y‖²)))
/// ```
/// The inner ratio is clamped to `1e10` to avoid numerical overflow near
/// the boundary.
pub fn hyperbolic_distance(&self, x: [f64; 2], y: [f64; 2]) -> f64 {
let dx = x[0] - y[0];
let dy = x[1] - y[1];
let diff_sq = dx * dx + dy * dy;
let norm_x_sq = (x[0] * x[0] + x[1] * x[1]).min(1.0 - 1e-9);
let norm_y_sq = (y[0] * y[0] + y[1] * y[1]).min(1.0 - 1e-9);
let denom = (1.0 - norm_x_sq) * (1.0 - norm_y_sq);
let ratio = if denom < 1e-12 {
1e10
} else {
(2.0 * diff_sq / denom).min(1e10)
};
(1.0 + ratio).acosh()
}
// ── §1.5 Hierarchical similarity ────────────────────────────────────────
/// Compute a hierarchical similarity score for `parent` and `child`.
///
/// Both vectors are encoded to the disk. If the child is further from the
/// origin than the parent (i.e. it sits deeper in the hierarchy), a score
/// combining angular and radial proximity is returned; otherwise `0.0`.
pub fn hierarchical_similarity(
&self,
parent: &[f64],
child: &[f64],
) -> anyhow::Result<f64> {
let parent_hv = self.encode_to_poincare(parent)?;
let child_hv = self.encode_to_poincare(child)?;
let origin = [0.0_f64; 2];
let parent_dist = self.hyperbolic_distance(parent_hv.coordinates, origin);
let child_dist = self.hyperbolic_distance(child_hv.coordinates, origin);
if child_dist <= parent_dist {
return Ok(0.0);
}
// Angular similarity: cosine of the angle between the two disk vectors.
let [px, py] = parent_hv.coordinates;
let [cx, cy] = child_hv.coordinates;
let p_norm = (px * px + py * py).sqrt().max(1e-12);
let c_norm = (cx * cx + cy * cy).sqrt().max(1e-12);
let angular_sim = ((px * cx + py * cy) / (p_norm * c_norm)).clamp(-1.0, 1.0);
// Radial similarity: how close the radii are.
let radial_sim = 1.0 - (child_dist - parent_dist).abs() / (child_dist + 1e-12);
// Combined score (equal-weight average).
Ok(0.5 * (angular_sim + radial_sim))
}
// ── §1.6 Batch helpers ───────────────────────────────────────────────────
/// Encode a slice of vectors, returning one result per input.
pub fn encode_batch(&self, vectors: &[Vec<f64>]) -> Vec<anyhow::Result<HyperbolicVector>> {
vectors.iter().map(|v| self.encode_to_poincare(v)).collect()
}
/// Decode a slice of `HyperbolicVector`s.
pub fn decode_batch(&self, hvs: &[HyperbolicVector]) -> Vec<Vec<f64>> {
hvs.iter().map(|hv| self.decode_from_poincare(hv)).collect()
}
}
// ─────────────────────────────────────────────────────────────────────────────
// §2 HyperbolicCache
// ─────────────────────────────────────────────────────────────────────────────
/// Memoised wrapper around `HyperbolicManifoldEncoder`.
///
/// The cache key is the first 16 hex characters of the SHA-256 digest of the
/// raw f64 bytes — fast enough for session-sync workloads.
pub struct HyperbolicCache {
encoder: HyperbolicManifoldEncoder,
cache: HashMap<String, HyperbolicVector>,
}
impl HyperbolicCache {
/// Create a new cache backed by an encoder with the given curvature.
pub fn new(curvature: f64) -> Self {
Self {
encoder: HyperbolicManifoldEncoder::new(curvature),
cache: HashMap::new(),
}
}
/// Compute a stable cache key from a vector.
///
/// The key is the first 16 lowercase hex characters of SHA-256(raw f64 LE bytes).
fn _hash_vector(v: &[f64]) -> String {
let mut hasher = Sha256::new();
for &val in v {
hasher.update(val.to_le_bytes());
}
let digest = hasher.finalize();
hex::encode(&digest[..8]) // 8 bytes → 16 hex chars
}
/// Return the cached `HyperbolicVector` for `vector`, encoding on first access.
pub fn get_or_encode(&mut self, vector: &[f64]) -> anyhow::Result<&HyperbolicVector> {
let key = Self::_hash_vector(vector);
if !self.cache.contains_key(&key) {
let hv = self.encoder.encode_to_poincare(vector)?;
self.cache.insert(key.clone(), hv);
}
Ok(self.cache.get(&key).expect("just inserted"))
}
/// Evict all cached entries.
pub fn clear(&mut self) {
self.cache.clear();
}
/// Number of entries currently in the cache.
pub fn size(&self) -> usize {
self.cache.len()
}
}
// ─────────────────────────────────────────────────────────────────────────────
// §3 HyperbolicSemanticSpace
// ─────────────────────────────────────────────────────────────────────────────
/// A named collection of concepts mapped into the Poincaré disk.
///
/// Supports nearest-neighbour retrieval by hyperbolic distance and basic
/// hierarchical relationship queries.
pub struct HyperbolicSemanticSpace {
encoder: HyperbolicManifoldEncoder,
concepts: HashMap<String, HyperbolicVector>,
}
impl HyperbolicSemanticSpace {
/// Create an empty semantic space with the given curvature.
pub fn new(curvature: f64) -> Self {
Self {
encoder: HyperbolicManifoldEncoder::new(curvature),
concepts: HashMap::new(),
}
}
/// Encode `vector` and store it under `name`.
pub fn add_concept(&mut self, name: &str, vector: &[f64]) -> anyhow::Result<()> {
let hv = self.encoder.encode_to_poincare(vector)?;
self.concepts.insert(name.to_owned(), hv);
Ok(())
}
/// Return the `top_k` concepts closest to `query` by hyperbolic distance.
///
/// Results are ordered ascending by distance (nearest first).
pub fn find_similar(
&self,
query: &[f64],
top_k: usize,
) -> anyhow::Result<Vec<(String, f64)>> {
let query_hv = self.encoder.encode_to_poincare(query)?;
let mut distances: Vec<(String, f64)> = self
.concepts
.iter()
.map(|(name, hv)| {
let d = self.encoder.hyperbolic_distance(query_hv.coordinates, hv.coordinates);
(name.clone(), d)
})
.collect();
distances.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
distances.truncate(top_k);
Ok(distances)
}
/// Hierarchical similarity between two named concepts.
///
/// Returns `0.0` when either concept is not registered.
pub fn get_hierarchy(&self, parent_name: &str, child_name: &str) -> f64 {
let (Some(p), Some(c)) = (
self.concepts.get(parent_name),
self.concepts.get(child_name),
) else {
return 0.0;
};
// Prefer lossless originals when available; fall back to disk coordinates.
let p_vec: Vec<f64> = p.original.clone().unwrap_or_else(|| {
self.encoder.decode_from_poincare(p)
});
let c_vec: Vec<f64> = c.original.clone().unwrap_or_else(|| {
self.encoder.decode_from_poincare(c)
});
self.encoder
.hierarchical_similarity(&p_vec, &c_vec)
.unwrap_or(0.0)
}
}
// ─────────────────────────────────────────────────────────────────────────────
// §4 Tests
// ─────────────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_poincare_encode_decode() {
let enc = HyperbolicManifoldEncoder::new(-1.0);
let v = vec![0.1f64; 14];
let hv = enc.encode_to_poincare(&v).unwrap();
let norm = (hv.coordinates[0].powi(2) + hv.coordinates[1].powi(2)).sqrt();
assert!(norm < 1.0, "must be inside unit disk; got norm={}", norm);
}
#[test]
fn test_hyperbolic_distance_positive() {
let enc = HyperbolicManifoldEncoder::new(-1.0);
let d = enc.hyperbolic_distance([0.0, 0.0], [0.5, 0.0]);
assert!(d > 0.0, "distance must be positive; got {}", d);
}
#[test]
fn test_encode_wrong_length() {
let enc = HyperbolicManifoldEncoder::new(-1.0);
assert!(enc.encode_to_poincare(&[0.0; 5]).is_err());
}
#[test]
fn test_decode_round_trip() {
let enc = HyperbolicManifoldEncoder::new(-1.0);
let orig: Vec<f64> = (0..14).map(|i| i as f64 * 0.05).collect();
let hv = enc.encode_to_poincare(&orig).unwrap();
let decoded = enc.decode_from_poincare(&hv);
// Original is stored, so round-trip must be exact.
assert_eq!(decoded, orig);
}
#[test]
fn test_cache_size() {
let mut cache = HyperbolicCache::new(-1.0);
let v1 = vec![0.1f64; 14];
let v2 = vec![0.2f64; 14];
cache.get_or_encode(&v1).unwrap();
cache.get_or_encode(&v2).unwrap();
// Second access to v1 — no new entry.
cache.get_or_encode(&v1).unwrap();
assert_eq!(cache.size(), 2);
cache.clear();
assert_eq!(cache.size(), 0);
}
#[test]
fn test_semantic_space_find_similar() {
let mut space = HyperbolicSemanticSpace::new(-1.0);
let base: Vec<f64> = vec![0.1; 14];
let close: Vec<f64> = vec![0.11; 14];
let far: Vec<f64> = (0..14).map(|i| if i % 2 == 0 { 5.0 } else { -5.0 }).collect();
space.add_concept("base", &base).unwrap();
space.add_concept("close", &close).unwrap();
space.add_concept("far", &far).unwrap();
let results = space.find_similar(&base, 2).unwrap();
assert_eq!(results.len(), 2);
// "base" itself should be closest (distance ≈ 0).
assert_eq!(results[0].0, "base");
}
#[test]
fn test_mobius_transform_identity() {
// T_0(z) should equal z.
let enc = HyperbolicManifoldEncoder::new(-1.0);
let z = [0.3_f64, 0.4_f64];
let result = enc.mobius_transform([0.0, 0.0], z).unwrap();
let eps = 1e-10;
assert!((result[0] - z[0]).abs() < eps);
assert!((result[1] - z[1]).abs() < eps);
}
}