/- Copyright (c) 2026 Sovereign Research Stack. All rights reserved. Released under Apache 2.0 license as described in the file LICENSE. Authors: Research Stack Team HyperbolicEncoding.lean — Hyperbolic Manifold Coordinate Encoding Replaces infra/hyperbolic_encoding.py with a formal Lean module. Defines Poincaré disk coordinates and Möbius transformations for semantic vector encoding. Per AGENTS.md: - Q16_16 for scoring (§1.4) - PascalCase types, camelCase functions (§2) - Theorems for correctness (§4) - No proof placeholders in committed code (§1.6) -/ import Mathlib.Data.Nat.Basic import Mathlib.Data.List.Basic import Std namespace Semantics.HyperbolicEncoding -- ═══════════════════════════════════════════════════════════════════════════ -- §0 Q16_16 Fixed-Point Arithmetic -- ═══════════════════════════════════════════════════════════════════════════ structure Q16_16 where raw : Int deriving Repr, DecidableEq, Inhabited namespace Q16_16 def zero : Q16_16 := ⟨0⟩ def one : Q16_16 := ⟨65536⟩ def ofFrac (num denom : Nat) : Q16_16 := if denom = 0 then zero else ⟨(num * 65536) / denom⟩ def toNat (q : Q16_16) : Nat := q.raw.toNat end Q16_16 -- ═══════════════════════════════════════════════════════════════════════════ -- §1 Hyperbolic Vector Structure -- ═══════════════════════════════════════════════════════════════════════════ structure HyperbolicVector where x : Q16_16 -- x coordinate in Poincaré disk y : Q16_16 -- y coordinate in Poincaré disk dimension : Nat -- Original dimension norm : Q16_16 -- Distance from origin deriving Repr, Inhabited -- ═══════════════════════════════════════════════════════════════════════════ -- ═══════════════════════════════════════════════════════════════════════════ -- §2 Dimension Weights for 14D Semantic Vectors -- ═══════════════════════════════════════════════════════════════════════════ def dimensionWeights : List Q16_16 := [ Q16_16.ofFrac 19 10000, -- 0.0019 Q16_16.ofFrac 20 10000, -- 0.0020 Q16_16.ofFrac 24 10000, -- 0.0024 Q16_16.ofFrac 25 10000, -- 0.0025 Q16_16.ofFrac 23 10000, -- 0.0023 Q16_16.ofFrac 16 10000, -- 0.0016 Q16_16.ofFrac 19 10000, -- 0.0019 Q16_16.ofFrac 18 10000, -- 0.0018 Q16_16.ofFrac 20 10000, -- 0.0020 Q16_16.ofFrac 25 10000, -- 0.0025 Q16_16.ofFrac 18 10000, -- 0.0018 Q16_16.ofFrac 22 10000, -- 0.0022 Q16_16.ofFrac 21 10000, -- 0.0021 Q16_16.ofFrac 26 10000 -- 0.0026 (dominant dimension) ] -- ═══════════════════════════════════════════════════════════════════════════ -- §3 Encoding/Decoding Operations -- ═══════════════════════════════════════════════════════════════════════════ /-- Encode 14D semantic vector to Poincaré disk coordinates -/ def encodeToPoincare (vector : List Q16_16) : HyperbolicVector := if vector.length ≠ 14 then { x := Q16_16.zero, y := Q16_16.zero, dimension := 14, norm := Q16_16.zero } else -- Weighted projection to 2D using even/odd indices let xSum := List.foldl (fun acc (i : Nat) => if i % 2 = 0 then let weight := List.getD dimensionWeights i Q16_16.zero let val := List.getD vector i Q16_16.zero { raw := acc.raw + (val.raw * weight.raw) / 65536 } else acc ) Q16_16.zero (List.range 14) let ySum := List.foldl (fun acc (i : Nat) => if i % 2 = 1 then let weight := List.getD dimensionWeights i Q16_16.zero let val := List.getD vector i Q16_16.zero { raw := acc.raw + (val.raw * weight.raw) / 65536 } else acc ) Q16_16.zero (List.range 14) -- Simplified norm calculation (avoid sqrt for fixed-point) let normSquared := (xSum.raw * xSum.raw + ySum.raw * ySum.raw) / 65536 let norm := Q16_16.ofFrac normSquared.toNat 65536 { x := xSum, y := ySum, dimension := 14, norm := norm } /-- Decode from Poincaré disk back to 14D vector space -/ def decodeFromPoincare (hyperbolic : HyperbolicVector) : List Q16_16 := -- Expand back to 14D using inverse projection let evenWeightSum := List.foldl (fun acc (_w : Q16_16) => { raw := acc.raw + _w.raw } ) Q16_16.zero (List.zipWith (fun w i => if i % 2 = 0 then w else Q16_16.zero) dimensionWeights (List.range 14)) let oddWeightSum := List.foldl (fun acc (_w : Q16_16) => { raw := acc.raw + _w.raw } ) Q16_16.zero (List.zipWith (fun w i => if i % 2 = 1 then w else Q16_16.zero) dimensionWeights (List.range 14)) let xContrib := if evenWeightSum.raw = 0 then Q16_16.zero else { raw := (hyperbolic.x.raw * 65536) / (evenWeightSum.raw + 1) } let yContrib := if oddWeightSum.raw = 0 then Q16_16.zero else { raw := (hyperbolic.y.raw * 65536) / (oddWeightSum.raw + 1) } List.zipWith (fun _w i => if i % 2 = 0 then let weight := List.getD dimensionWeights i Q16_16.zero if evenWeightSum.raw = 0 then Q16_16.zero else { raw := (xContrib.raw * weight.raw) / 65536 } else let weight := List.getD dimensionWeights i Q16_16.zero if oddWeightSum.raw = 0 then Q16_16.zero else { raw := (yContrib.raw * weight.raw) / 65536 } ) dimensionWeights (List.range 14) -- ═══════════════════════════════════════════════════════════════════════════ -- §4 Möbius Transformation -- ═══════════════════════════════════════════════════════════════════════════ /-- Apply Möbius transformation to point z in Poincaré disk -/ def mobiusTransform (a z : HyperbolicVector) : HyperbolicVector := let aNormSq := (a.x.raw * a.x.raw + a.y.raw * a.y.raw) / 65536 let zNormSq := (z.x.raw * z.x.raw + z.y.raw * z.y.raw) / 65536 let az := (a.x.raw * z.x.raw + a.y.raw * z.y.raw) / 65536 -- Möbius transformation formula let numeratorX := ((65536 + 2*az + aNormSq) * z.x.raw + (65536 + zNormSq) * a.x.raw) / 65536 let numeratorY := ((65536 + 2*az + aNormSq) * z.y.raw + (65536 + zNormSq) * a.y.raw) / 65536 let denominator := (65536 + 2*az + aNormSq + zNormSq) let newX := if denominator = 0 then Q16_16.zero else { raw := (numeratorX * 65536) / denominator } let newY := if denominator = 0 then Q16_16.zero else { raw := (numeratorY * 65536) / denominator } let newNormSq := (newX.raw * newX.raw + newY.raw * newY.raw) / 65536 let newNorm := Q16_16.ofFrac newNormSq.toNat 65536 { x := newX, y := newY, dimension := 2, norm := newNorm } -- ═══════════════════════════════════════════════════════════════════════════ -- §5 Hyperbolic Distance -- ═══════════════════════════════════════════════════════════════════════════ /-- Compute hyperbolic distance between two points in Poincaré disk -/ def hyperbolicDistance (x y : HyperbolicVector) : Q16_16 := let xNormSq := (x.x.raw * x.x.raw + x.y.raw * x.y.raw) / 65536 let yNormSq := (y.x.raw * y.x.raw + y.y.raw * y.y.raw) / 65536 let diffX := x.x.raw - y.x.raw let diffY := x.y.raw - y.y.raw let diffNormSq := (diffX * diffX + diffY * diffY) / 65536 let numerator := 2 * diffNormSq let denominator := (65536 - xNormSq) * (65536 - yNormSq) / 65536 if denominator = 0 then Q16_16.one else let ratio := (numerator * 65536) / denominator -- Simplified: return ratio as distance approximation -- In production, would use acosh(1 + ratio) Q16_16.ofFrac ratio.toNat 65536 -- ═══════════════════════════════════════════════════════════════════════════ -- §6 Cache Structure -- ═══════════════════════════════════════════════════════════════════════════ structure HyperbolicCache where entries : List (String × HyperbolicVector) deriving Repr, Inhabited /-- Initialize empty hyperbolic cache -/ def initHyperbolicCache : HyperbolicCache := { entries := [] } /-- Get or encode vector from cache -/ def getOrEncode (cache : HyperbolicCache) (vector : List Q16_16) (key : String) : HyperbolicCache × HyperbolicVector := let existing := List.find? (·.1 = key) cache.entries match existing with | some (_, hv) => (cache, hv) | none => let hv := encodeToPoincare vector ({ entries := (key, hv) :: cache.entries }, hv) -- ═══════════════════════════════════════════════════════════════════════════ -- §7 Theorems -- ═══════════════════════════════════════════════════════════════════════════ /-- Dimension weights list has 14 elements -/ theorem dimensionWeightsLength : dimensionWeights.length = 14 := by rfl -- ═══════════════════════════════════════════════════════════════════════════ -- §8 Example Usage -- ═══════════════════════════════════════════════════════════════════════════ #eval let vector := List.replicate 14 (Q16_16.ofFrac 20 10000) encodeToPoincare vector #eval let hv := { x := Q16_16.ofFrac 30 100, y := Q16_16.ofFrac 40 100, dimension := 14, norm := Q16_16.ofFrac 50 100 } decodeFromPoincare hv #eval let a := { x := Q16_16.ofFrac 30 100, y := Q16_16.ofFrac 20 100, dimension := 2, norm := Q16_16.ofFrac 36 100 } let z := { x := Q16_16.ofFrac 50 100, y := Q16_16.ofFrac 40 100, dimension := 2, norm := Q16_16.ofFrac 64 100 } mobiusTransform a z #eval let x := { x := Q16_16.ofFrac 10 100, y := Q16_16.ofFrac 10 100, dimension := 2, norm := Q16_16.ofFrac 14 100 } let y := { x := Q16_16.ofFrac 20 100, y := Q16_16.ofFrac 20 100, dimension := 2, norm := Q16_16.ofFrac 28 100 } hyperbolicDistance x y end Semantics.HyperbolicEncoding