aether-context 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
aether_context/mpo.py ADDED
@@ -0,0 +1,151 @@
1
+ # aether-context (Unlimited Context)
2
+ # Copyright (c) 2026 Aether AI
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ """MPO context chain — chains the session's context space and improves selection accuracy.
5
+
6
+ Cosine/semantic search remains the retrieval mechanism; this does not replace it. The MPO
7
+ (Matrix Product Operator) chain links the session's slices into one connected space: when
8
+ cosine pulls an entry slice, the chain pulls in the slices most coupled to it, widening the
9
+ working set with the connected thread instead of isolated nearest-neighbors.
10
+
11
+ Deterministic, numpy-only, and purely additive — it only ever *adds* connected slices to a
12
+ result, never blocks or replaces a hit, so any failure degrades cleanly to plain cosine.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import logging
17
+ from dataclasses import dataclass
18
+ from typing import Sequence
19
+
20
+ import numpy as np
21
+
22
+ log = logging.getLogger("aether_context.mpo")
23
+
24
+ DEFAULT_CHAIN_WIDTH: int = 8
25
+ DEFAULT_CHAIN_HOPS: int = 1
26
+ _FEAT: int = 8
27
+ _BOND: int = 4
28
+ _OUT: int = 4
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class ChainItem:
33
+ """A retrieval candidate the chain operates on: id, unit vector, and ``c_t``."""
34
+
35
+ id: str
36
+ vector: np.ndarray
37
+ c_t: tuple[float, float]
38
+
39
+
40
+ def _lift(x: float, p: int) -> np.ndarray:
41
+ half = max(1, p // 2)
42
+ ks = np.arange(1, half + 1, dtype=np.float64)
43
+ f = np.concatenate([np.sin(np.pi * ks * x), np.cos(np.pi * ks * x)])
44
+ if f.size < p:
45
+ f = np.concatenate([f, np.zeros(p - f.size)])
46
+ return f[:p]
47
+
48
+
49
+ def _unit(v: np.ndarray) -> np.ndarray:
50
+ n = float(np.linalg.norm(v))
51
+ return v if n < 1e-12 else v / n
52
+
53
+
54
+ def _cos(a: np.ndarray, b: np.ndarray) -> float:
55
+ na, nb = float(np.linalg.norm(a)), float(np.linalg.norm(b))
56
+ if na < 1e-12 or nb < 1e-12:
57
+ return 0.0
58
+ return float(np.dot(a, b) / (na * nb))
59
+
60
+
61
+ def _minmax(x: np.ndarray) -> np.ndarray:
62
+ lo, hi = float(x.min()), float(x.max())
63
+ if hi - lo < 1e-12:
64
+ return np.full_like(x, 0.5)
65
+ return (x - lo) / (hi - lo)
66
+
67
+
68
+ class MpoChain:
69
+ """Chains slices and expands a cosine hit into the slices most coupled to it."""
70
+
71
+ def __init__(
72
+ self,
73
+ *,
74
+ seed: int = 0,
75
+ width: int = DEFAULT_CHAIN_WIDTH,
76
+ hops: int = DEFAULT_CHAIN_HOPS,
77
+ ) -> None:
78
+ rng = np.random.default_rng(seed)
79
+ p = _FEAT
80
+ self.width = max(1, int(width))
81
+ self.hops = max(1, int(hops))
82
+ self._w0 = (rng.standard_normal((1, p, _BOND)) * 0.5).astype(np.float64)
83
+ self._w1 = (rng.standard_normal((_BOND, p, _OUT)) * 0.5).astype(np.float64)
84
+
85
+ def _embed(self, c_t: tuple[float, float]) -> np.ndarray:
86
+ u = _lift(float(c_t[0]), _FEAT)
87
+ v = _lift(float(c_t[1]), _FEAT)
88
+ a = np.einsum("aib,i->ab", self._w0, u)[0]
89
+ e = np.einsum("b,bjm,j->m", a, self._w1, v)
90
+ return _unit(e)
91
+
92
+ def coupling(self, vec_i, e_i, vec_h, e_h) -> float:
93
+ """Similarity of two slices for chaining (higher = more coupled)."""
94
+ return _cos(np.asarray(vec_i), np.asarray(vec_h)) * (0.5 + 0.5 * _cos(e_i, e_h))
95
+
96
+ def expand(
97
+ self,
98
+ hit_ids: Sequence[str],
99
+ candidates: Sequence[ChainItem],
100
+ *,
101
+ width: int | None = None,
102
+ hops: int | None = None,
103
+ ) -> list[str]:
104
+ """Widen ``hit_ids`` with the candidates most coupled to them.
105
+
106
+ Returns ordered ids: the hits first (in their given order), then the coupled slices in
107
+ pull order, de-duplicated.
108
+ """
109
+ items = list(candidates)
110
+ if not items:
111
+ return list(hit_ids)
112
+ width = self.width if width is None else max(1, int(width))
113
+ hops = self.hops if hops is None else max(1, int(hops))
114
+
115
+ a0 = _minmax(np.array([it.c_t[0] for it in items], dtype=np.float64))
116
+ a1 = _minmax(np.array([it.c_t[1] for it in items], dtype=np.float64))
117
+ embeds = {it.id: self._embed((a0[i], a1[i])) for i, it in enumerate(items)}
118
+ by_id = {it.id: it for it in items}
119
+
120
+ present = [h for h in hit_ids if h in by_id] or [items[0].id]
121
+ selected: list[str] = []
122
+ seen: set[str] = set()
123
+ for h in present:
124
+ if h not in seen:
125
+ selected.append(h)
126
+ seen.add(h)
127
+ frontier = list(selected)
128
+
129
+ for _ in range(hops):
130
+ scored: list[tuple[float, str]] = []
131
+ for it in items:
132
+ if it.id in seen:
133
+ continue
134
+ best = max(
135
+ self.coupling(it.vector, embeds[it.id], by_id[h].vector, embeds[h])
136
+ for h in frontier
137
+ )
138
+ scored.append((best, it.id))
139
+ if not scored:
140
+ break
141
+ scored.sort(key=lambda p: p[0], reverse=True)
142
+ picked = [sid for _, sid in scored[:width]]
143
+ for sid in picked:
144
+ if sid not in seen:
145
+ selected.append(sid)
146
+ seen.add(sid)
147
+ frontier = picked
148
+ return selected
149
+
150
+
151
+ __all__ = ["MpoChain", "ChainItem", "DEFAULT_CHAIN_WIDTH", "DEFAULT_CHAIN_HOPS"]
File without changes
@@ -0,0 +1,86 @@
1
+ # aether-context (Unlimited Context)
2
+ # Copyright (c) 2026 Aether AI
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ """TurboVec — scalar quantization of unit retrieval vectors (the context pool's compression codec).
5
+
6
+ The context pool stores L2-normalized 256-dim float32 vectors (1024 B/row). TurboVec quantizes each
7
+ row to ``bits``-bit codes + a per-row float32 scale, so the footprint drops to ``dim*bits/8 + 4`` bytes
8
+ — ~8x at 4-bit, ~4x at 8-bit. The same byte ceiling then holds that many more slices in the hot set,
9
+ which is the coherence-at-scale win (more of the run stays resident, fewer cold reads).
10
+
11
+ Per-row SYMMETRIC quantization: ``code = round(v/scale) + half`` over ``scale = max|v| / half``, so the
12
+ row's dynamic range maps onto the integer grid. Dequantization recovers ``(code-half)*scale`` and
13
+ RE-NORMALIZES to unit length (cosine only cares about direction). Recall@k parity vs float32 is the gate
14
+ the tests enforce (>=0.98). ``bits=0`` is the identity (the float32 path) — TurboVec is fully reversible.
15
+
16
+ Pure numpy, no dependencies. Only 4- and 8-bit are supported (the codes pack to whole bytes cleanly).
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import numpy as np
21
+
22
+ SUPPORTED_BITS = (0, 4, 8)
23
+
24
+
25
+ def packed_bytes_per_row(dim: int, bits: int) -> int:
26
+ """On-disk/in-RAM code bytes for one row at ``bits`` (excludes the 4-byte per-row scale)."""
27
+ if bits == 0:
28
+ return dim * 4
29
+ if bits == 8:
30
+ return dim
31
+ if bits == 4:
32
+ return (dim + 1) // 2
33
+ raise ValueError(f"unsupported bits={bits}; use one of {SUPPORTED_BITS}")
34
+
35
+
36
+ def quantize(matrix: np.ndarray, bits: int = 4) -> tuple[np.ndarray, np.ndarray]:
37
+ """``(n, dim)`` float32 rows -> ``(codes uint8 (n, packed), scales float32 (n,))``.
38
+
39
+ Rows are assumed ~unit (the pool stores unit vectors). ``bits=0`` returns the raw float32 bytes as
40
+ uint8 with unit scales (identity passthrough), so callers can treat both paths uniformly.
41
+ """
42
+ m = np.ascontiguousarray(matrix, dtype=np.float32)
43
+ n, dim = m.shape
44
+ if bits == 0:
45
+ return m.view(np.uint8).reshape(n, dim * 4), np.ones(n, dtype=np.float32)
46
+ levels = (1 << bits) - 1
47
+ half = levels >> 1
48
+ maxabs = np.max(np.abs(m), axis=1)
49
+ scales = np.where(maxabs > 1e-12, maxabs / half, 1.0).astype(np.float32)
50
+ q = np.rint(m / scales[:, None]) + half
51
+ q = np.clip(q, 0, levels).astype(np.uint8) # (n, dim) integer codes
52
+ if bits == 8:
53
+ return np.ascontiguousarray(q), scales
54
+ # 4-bit: pack two nibbles per byte (pad odd dim with a zero nibble).
55
+ if dim & 1:
56
+ q = np.pad(q, ((0, 0), (0, 1)))
57
+ codes = ((q[:, 0::2] << 4) | q[:, 1::2]).astype(np.uint8)
58
+ return np.ascontiguousarray(codes), scales
59
+
60
+
61
+ def dequantize(codes: np.ndarray, scales: np.ndarray, dim: int, bits: int = 4) -> np.ndarray:
62
+ """Inverse of :func:`quantize` -> ``(n, dim)`` float32 UNIT rows (re-normalized for cosine)."""
63
+ if bits == 0:
64
+ return codes.reshape(codes.shape[0], dim * 4).view(np.float32).reshape(-1, dim).copy()
65
+ levels = (1 << bits) - 1
66
+ half = levels >> 1
67
+ if bits == 8:
68
+ q = codes.astype(np.float32)
69
+ else: # 4-bit: unpack the nibbles
70
+ hi = (codes >> 4) & 0xF
71
+ lo = codes & 0xF
72
+ q = np.empty((codes.shape[0], hi.shape[1] * 2), dtype=np.float32)
73
+ q[:, 0::2] = hi
74
+ q[:, 1::2] = lo
75
+ q = q[:, :dim]
76
+ v = (q - half) * scales[:, None]
77
+ norms = np.linalg.norm(v, axis=1, keepdims=True)
78
+ norms[norms < 1e-12] = 1.0
79
+ return (v / norms).astype(np.float32)
80
+
81
+
82
+ def compression_ratio(dim: int, bits: int) -> float:
83
+ """float32 row bytes / quantized row bytes (vector only) — the headline TurboVec multiple."""
84
+ if bits == 0:
85
+ return 1.0
86
+ return (dim * 4) / (packed_bytes_per_row(dim, bits) + 4)