Spaces:
Running
Running
| """ | |
| GovBridge India β INDRA PoincarΓ© Projection Engine (Sprint 29) | |
| PROJECT INDRA Phase 1: Non-Euclidean Civic Topologies | |
| This engine implements the HyEm (Hyperbolic Embedding) paradigm: | |
| 1. Receives oversampled Euclidean candidates from HNSW (400 vectors) | |
| 2. Projects them into the PoincarΓ© ball model via conformal exponential map | |
| 3. Computes hyperbolic distances (arcosh-based) between query and candidates | |
| 4. Returns candidates sorted by hyperbolic distance (ascending) | |
| ARCHITECTURAL CONSTRAINTS: | |
| - ALL NumPy buffers are pre-allocated at __init__ time. | |
| - ZERO heap allocations inside compute methods (GC pressure = 0). | |
| - IEEE 754 boundary enforcement: all vectors clamped to R_MAX = 1 - 1e-3. | |
| - Denominator floor: DENOM_MIN = 1e-15 prevents division-by-zero. | |
| - Negative normΒ² clamping: np.maximum(diff_norm2, 0.0) prevents NaN from | |
| floating-point variance underflow on identical vectors. | |
| MATHEMATICAL FOUNDATION: | |
| exp_0(v) = tanh(||v||) * (v / ||v||) [Conformal exponential map at origin] | |
| d_H(u,v) = arcosh(1 + 2 * ||u-v||Β² / ((1-||u||Β²)(1-||v||Β²))) | |
| USAGE: | |
| from indra_engine import IndraProjectionEngine | |
| engine = IndraProjectionEngine(batch_size=400, dimensions=768) | |
| distances = engine.project_and_rank(query_vec, candidate_matrix) | |
| """ | |
| import numpy as np | |
| from numpy.typing import NDArray | |
| from typing import Optional | |
| import os | |
| # ββ Optional Learned Adapter Weights (Sprint 31) ββββββββββββ | |
| # If W.npy and b.npy exist, use learned projection instead of naΓ―ve expmap0. | |
| # Falls back to original tanh(||v||)/||v|| if weights not found. | |
| _WEIGHTS_DIR = os.path.join(os.path.dirname(__file__), "..", "training", "weights") | |
| _W_PATH = os.path.join(_WEIGHTS_DIR, "W.npy") | |
| _B_PATH = os.path.join(_WEIGHTS_DIR, "b.npy") | |
| class IndraProjectionEngine: | |
| """ | |
| Zero-allocation PoincarΓ© ball projection engine. | |
| Pre-allocates all working buffers at construction time. | |
| No heap allocations occur during compute_poincare_distances() | |
| or map_to_poincare_ball_inplace(). | |
| """ | |
| # ββ Numerical Constants ββββββββββββββββββββββββββββββββββ | |
| # Maximum radius in PoincarΓ© ball. Vectors beyond this are | |
| # clipped to prevent arcosh(1 + 2 * inf / 0) = NaN. | |
| R_MAX: float = 1.0 - 1e-3 # 0.999 | |
| # Minimum denominator value. Prevents div-by-zero when a | |
| # vector lies exactly on the ball boundary (||v|| β 1.0). | |
| DENOM_MIN: float = 1e-15 | |
| # Minimum norm for normalization. Vectors with ||v|| < this | |
| # are treated as zero vectors and mapped to the origin. | |
| NORM_MIN: float = 1e-8 | |
| def __init__(self, batch_size: int = 400, dimensions: int = 768): | |
| """ | |
| Pre-allocate all working memory. | |
| Args: | |
| batch_size: Maximum number of candidate vectors per query. | |
| dimensions: Embedding dimensionality (768 for Nomic). | |
| """ | |
| self.batch_size = batch_size | |
| self.dimensions = dimensions | |
| # ββ Pre-allocated buffers ββββββββββββββββββββββββββββ | |
| # Candidate matrix projected into PoincarΓ© ball | |
| self._poincare_candidates = np.zeros( | |
| (batch_size, dimensions), dtype=np.float32 | |
| ) | |
| # Query vector projected into PoincarΓ© ball | |
| self._poincare_query = np.zeros(dimensions, dtype=np.float32) | |
| # Scratch buffers for distance computation | |
| self._norms_sq = np.zeros(batch_size, dtype=np.float64) | |
| self._query_norm_sq = np.float64(0.0) | |
| self._diff = np.zeros( | |
| (batch_size, dimensions), dtype=np.float32 | |
| ) | |
| self._diff_norm_sq = np.zeros(batch_size, dtype=np.float64) | |
| self._distances = np.zeros(batch_size, dtype=np.float64) | |
| self._denom = np.zeros(batch_size, dtype=np.float64) | |
| # Buffer for norms during projection | |
| self._proj_norms = np.zeros(batch_size, dtype=np.float32) | |
| # ββ Learned Adapter (Sprint 31) βββββββββββββββββββββββββββ | |
| _adapter_W: Optional[NDArray[np.float32]] = None | |
| _adapter_b: Optional[NDArray[np.float32]] = None | |
| _adapter_loaded: bool = False | |
| def _load_adapter_weights(self) -> bool: | |
| """ | |
| Attempt to load learned W.npy and b.npy from training/weights/. | |
| Returns True if weights loaded successfully, False otherwise. | |
| Falls back to naΓ―ve projection on failure. | |
| """ | |
| if self._adapter_loaded: | |
| return self._adapter_W is not None | |
| self._adapter_loaded = True | |
| try: | |
| if os.path.exists(_W_PATH) and os.path.exists(_B_PATH): | |
| W = np.load(_W_PATH) | |
| b = np.load(_B_PATH) | |
| # Validate shapes | |
| if W.shape != (768, 128) or b.shape != (128,) or self.dimensions != 768: | |
| return False | |
| self._adapter_W = W.astype(np.float32) | |
| self._adapter_b = b.astype(np.float32) | |
| # Reallocate PoincarΓ© buffers for 128-dim | |
| self._poincare_candidates = np.zeros( | |
| (self.batch_size, 128), dtype=np.float32 | |
| ) | |
| self._poincare_query = np.zeros(128, dtype=np.float32) | |
| self._diff = np.zeros( | |
| (self.batch_size, 128), dtype=np.float32 | |
| ) | |
| return True | |
| except Exception: | |
| pass | |
| return False | |
| def map_to_poincare_ball_inplace( | |
| self, | |
| vectors: NDArray[np.float32], | |
| out: NDArray[np.float32], | |
| norms_buf: Optional[NDArray[np.float32]] = None, | |
| ) -> None: | |
| """ | |
| Conformal exponential map at the origin: exp_0(v) = tanh(||v||) * (v / ||v||) | |
| Projects Euclidean vectors into the PoincarΓ© ball IN-PLACE | |
| (writes to the `out` buffer). No allocations. | |
| Args: | |
| vectors: Input Euclidean vectors, shape (N, D) or (D,). | |
| out: Output buffer, same shape as vectors. | |
| norms_buf: Pre-allocated buffer for norms. If None, uses self._proj_norms. | |
| """ | |
| is_1d = vectors.ndim == 1 | |
| # ββ Sprint 31: Use learned adapter if available βββββββ | |
| if self._load_adapter_weights() and self._adapter_W is not None: | |
| if is_1d: | |
| # Single vector: y = x @ W + b, then expmap0 | |
| projected = vectors @ self._adapter_W + self._adapter_b # (128,) | |
| norm = np.linalg.norm(projected).astype(np.float32) | |
| if norm < self.NORM_MIN: | |
| out[:128] = 0.0 | |
| else: | |
| scale = np.float32(np.tanh(norm) / norm) | |
| result = projected * scale | |
| out_norm = np.linalg.norm(result).astype(np.float32) | |
| if out_norm > self.R_MAX: | |
| result *= np.float32(self.R_MAX / out_norm) | |
| out[:128] = result | |
| else: | |
| n = vectors.shape[0] | |
| projected = vectors[:n] @ self._adapter_W + self._adapter_b # (n, 128) | |
| for i in range(n): | |
| norm_val = np.linalg.norm(projected[i]).astype(np.float32) | |
| if norm_val < self.NORM_MIN: | |
| out[i, :128] = 0.0 | |
| else: | |
| scale = np.float32(np.tanh(norm_val) / norm_val) | |
| result = projected[i] * scale | |
| out_norm = np.linalg.norm(result).astype(np.float32) | |
| if out_norm > self.R_MAX: | |
| result *= np.float32(self.R_MAX / out_norm) | |
| out[i, :128] = result | |
| return | |
| if is_1d: | |
| # Single vector case (query) | |
| norm = np.linalg.norm(vectors).astype(np.float32) | |
| if norm < self.NORM_MIN: | |
| out[:] = 0.0 | |
| return | |
| scale = np.float32(np.tanh(norm) / norm) | |
| np.multiply(vectors, scale, out=out) | |
| # Radial boundary clip | |
| out_norm = np.linalg.norm(out).astype(np.float32) | |
| if out_norm > self.R_MAX: | |
| np.multiply(out, np.float32(self.R_MAX / out_norm), out=out) | |
| else: | |
| # Batch case (candidates) | |
| n = vectors.shape[0] | |
| if norms_buf is None: | |
| norms_buf = self._proj_norms | |
| # Compute L2 norms: ||v_i|| | |
| np.einsum('ij,ij->i', vectors[:n], vectors[:n], out=norms_buf[:n]) | |
| np.sqrt(norms_buf[:n], out=norms_buf[:n]) | |
| for i in range(n): | |
| norm_val = norms_buf[i] | |
| if norm_val < self.NORM_MIN: | |
| out[i, :] = 0.0 | |
| else: | |
| scale = np.float32(np.tanh(norm_val) / norm_val) | |
| np.multiply(vectors[i], scale, out=out[i]) | |
| # Radial boundary clip per vector | |
| out_norm = np.linalg.norm(out[i]).astype(np.float32) | |
| if out_norm > self.R_MAX: | |
| np.multiply( | |
| out[i], | |
| np.float32(self.R_MAX / out_norm), | |
| out=out[i] | |
| ) | |
| def compute_poincare_distances( | |
| self, | |
| query_vec: NDArray[np.float32], | |
| candidate_matrix: NDArray[np.float32], | |
| n_candidates: int, | |
| ) -> NDArray[np.float64]: | |
| """ | |
| Compute hyperbolic distances between query and candidates in | |
| the PoincarΓ© ball model. | |
| Formula: | |
| d_H(u, v) = arcosh(1 + 2 * ||u - v||Β² / ((1 - ||u||Β²) * (1 - ||v||Β²))) | |
| All computations use pre-allocated buffers. Zero heap allocations. | |
| Args: | |
| query_vec: Euclidean query vector, shape (D,). | |
| candidate_matrix: Euclidean candidate vectors, shape (N, D). | |
| n_candidates: Actual number of candidates (may be < batch_size). | |
| Returns: | |
| Hyperbolic distances array, shape (n_candidates,). | |
| """ | |
| n = min(n_candidates, self.batch_size) | |
| # ββ Step 1: Project query into PoincarΓ© ball βββββββββ | |
| self.map_to_poincare_ball_inplace( | |
| query_vec, self._poincare_query | |
| ) | |
| # ββ Step 2: Project candidates into PoincarΓ© ball ββββ | |
| self.map_to_poincare_ball_inplace( | |
| candidate_matrix[:n], | |
| self._poincare_candidates[:n], | |
| norms_buf=self._proj_norms | |
| ) | |
| # ββ Step 3: Compute ||u||Β² (query norm squared) ββββββ | |
| self._query_norm_sq = np.float64( | |
| np.dot(self._poincare_query, self._poincare_query) | |
| ) | |
| # ββ Step 4: Compute ||v_i||Β² (candidate norms squared) β | |
| np.einsum( | |
| 'ij,ij->i', | |
| self._poincare_candidates[:n].astype(np.float64), | |
| self._poincare_candidates[:n].astype(np.float64), | |
| out=self._norms_sq[:n] | |
| ) | |
| # ββ Step 5: Compute ||u - v_i||Β² βββββββββββββββββββββ | |
| np.subtract( | |
| self._poincare_candidates[:n], | |
| self._poincare_query, | |
| out=self._diff[:n] | |
| ) | |
| np.einsum( | |
| 'ij,ij->i', | |
| self._diff[:n].astype(np.float64), | |
| self._diff[:n].astype(np.float64), | |
| out=self._diff_norm_sq[:n] | |
| ) | |
| # GUARD: Floating-point variance underflow. | |
| # Identical vectors can produce tiny negative values like -1e-7 | |
| # due to IEEE 754 rounding. Clamp to zero. | |
| np.maximum(self._diff_norm_sq[:n], 0.0, out=self._diff_norm_sq[:n]) | |
| # ββ Step 6: Compute denominator ββββββββββββββββββββββ | |
| # denom = (1 - ||u||Β²) * (1 - ||v_i||Β²) | |
| np.subtract(1.0, self._norms_sq[:n], out=self._denom[:n]) | |
| np.multiply( | |
| self._denom[:n], | |
| (1.0 - self._query_norm_sq), | |
| out=self._denom[:n] | |
| ) | |
| # GUARD: Denominator floor. If a vector lies exactly on the | |
| # boundary (||v|| β 1.0), denom β 0, causing infinity. | |
| np.maximum(self._denom[:n], self.DENOM_MIN, out=self._denom[:n]) | |
| # ββ Step 7: Compute arcosh argument ββββββββββββββββββ | |
| # arg = 1 + 2 * ||u - v||Β² / denom | |
| np.divide(self._diff_norm_sq[:n], self._denom[:n], out=self._distances[:n]) | |
| np.multiply(self._distances[:n], 2.0, out=self._distances[:n]) | |
| np.add(self._distances[:n], 1.0, out=self._distances[:n]) | |
| # GUARD: arcosh domain enforcement. arcosh(x) requires x >= 1. | |
| # Clamp to exactly 1.0 (distance = 0) if numerical error | |
| # pushes below 1.0. | |
| np.maximum(self._distances[:n], 1.0, out=self._distances[:n]) | |
| # ββ Step 8: Final hyperbolic distance ββββββββββββββββ | |
| np.arccosh(self._distances[:n], out=self._distances[:n]) | |
| return self._distances[:n].copy() # Return a copy (safe for caller) | |
| def project_and_rank( | |
| self, | |
| query_vec: NDArray[np.float32], | |
| candidate_matrix: NDArray[np.float32], | |
| n_candidates: int, | |
| ) -> NDArray[np.intp]: | |
| """ | |
| Full pipeline: project + compute distances + return sorted indices. | |
| Args: | |
| query_vec: Euclidean query embedding, shape (D,). | |
| candidate_matrix: Euclidean candidate embeddings, shape (N, D). | |
| n_candidates: Actual number of candidates. | |
| Returns: | |
| Array of indices sorted by ascending hyperbolic distance. | |
| """ | |
| distances = self.compute_poincare_distances( | |
| query_vec, candidate_matrix, n_candidates | |
| ) | |
| return np.argsort(distances) | |