superlocalmemory 3.2.2 → 3.3.0
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.
- package/CHANGELOG.md +43 -1
- package/README.md +106 -71
- package/package.json +1 -2
- package/pyproject.toml +16 -1
- package/src/superlocalmemory/cli/commands.py +309 -0
- package/src/superlocalmemory/cli/main.py +44 -0
- package/src/superlocalmemory/core/config.py +282 -11
- package/src/superlocalmemory/core/consolidation_engine.py +37 -0
- package/src/superlocalmemory/core/engine.py +21 -0
- package/src/superlocalmemory/core/engine_wiring.py +58 -8
- package/src/superlocalmemory/dynamics/activation_guided_quantization.py +374 -0
- package/src/superlocalmemory/dynamics/eap_scheduler.py +276 -0
- package/src/superlocalmemory/dynamics/ebbinghaus_langevin_coupling.py +171 -0
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +804 -0
- package/src/superlocalmemory/hooks/auto_invoker.py +46 -8
- package/src/superlocalmemory/hooks/auto_parameterize.py +147 -0
- package/src/superlocalmemory/infra/heartbeat_monitor.py +140 -0
- package/src/superlocalmemory/infra/pid_manager.py +193 -0
- package/src/superlocalmemory/infra/process_reaper.py +572 -0
- package/src/superlocalmemory/learning/consolidation_quantization_worker.py +115 -0
- package/src/superlocalmemory/learning/forgetting_scheduler.py +263 -0
- package/src/superlocalmemory/learning/quantization_scheduler.py +320 -0
- package/src/superlocalmemory/math/ebbinghaus.py +309 -0
- package/src/superlocalmemory/math/fisher_quantized.py +251 -0
- package/src/superlocalmemory/math/hopfield.py +279 -0
- package/src/superlocalmemory/math/polar_quant.py +379 -0
- package/src/superlocalmemory/math/qjl.py +115 -0
- package/src/superlocalmemory/mcp/server.py +2 -0
- package/src/superlocalmemory/mcp/tools_v3.py +10 -0
- package/src/superlocalmemory/mcp/tools_v33.py +351 -0
- package/src/superlocalmemory/parameterization/__init__.py +47 -0
- package/src/superlocalmemory/parameterization/pattern_extractor.py +534 -0
- package/src/superlocalmemory/parameterization/pii_filter.py +106 -0
- package/src/superlocalmemory/parameterization/prompt_injector.py +216 -0
- package/src/superlocalmemory/parameterization/prompt_lifecycle.py +275 -0
- package/src/superlocalmemory/parameterization/soft_prompt_generator.py +425 -0
- package/src/superlocalmemory/retrieval/engine.py +21 -3
- package/src/superlocalmemory/retrieval/forgetting_filter.py +145 -0
- package/src/superlocalmemory/retrieval/hopfield_channel.py +335 -0
- package/src/superlocalmemory/retrieval/quantization_aware_search.py +133 -0
- package/src/superlocalmemory/retrieval/spreading_activation.py +1 -1
- package/src/superlocalmemory/retrieval/strategy.py +16 -6
- package/src/superlocalmemory/retrieval/vector_store.py +1 -1
- package/src/superlocalmemory/server/routes/agents.py +68 -8
- package/src/superlocalmemory/server/routes/learning.py +18 -1
- package/src/superlocalmemory/server/routes/lifecycle.py +36 -17
- package/src/superlocalmemory/server/routes/v3_api.py +503 -1
- package/src/superlocalmemory/storage/database.py +206 -0
- package/src/superlocalmemory/storage/embedding_migrator.py +178 -0
- package/src/superlocalmemory/storage/migration_v33.py +140 -0
- package/src/superlocalmemory/storage/quantized_store.py +261 -0
- package/src/superlocalmemory/storage/schema_v32.py +137 -0
- package/conftest.py +0 -5
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under the MIT License - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
|
|
5
|
+
"""Ebbinghaus forgetting curve — memory strength and retention.
|
|
6
|
+
|
|
7
|
+
Implements the classic Ebbinghaus retention formula:
|
|
8
|
+
|
|
9
|
+
R(t) = e^(-t/S)
|
|
10
|
+
|
|
11
|
+
where:
|
|
12
|
+
R = retention score in [0, 1]
|
|
13
|
+
t = hours since last access
|
|
14
|
+
S = memory strength (composite of access, importance, confirmations, emotion)
|
|
15
|
+
|
|
16
|
+
Memory strength formula (ACT-R inspired):
|
|
17
|
+
|
|
18
|
+
S = alpha * log(1 + access_count)
|
|
19
|
+
+ beta * importance
|
|
20
|
+
+ gamma * confirmation_count
|
|
21
|
+
+ delta * emotional_salience
|
|
22
|
+
|
|
23
|
+
Lifecycle zones are derived from retention score:
|
|
24
|
+
active (R > 0.8) -> weight 1.0
|
|
25
|
+
warm (R > 0.5) -> weight 0.7
|
|
26
|
+
cold (R > 0.2) -> weight 0.3
|
|
27
|
+
archive (R > 0.05) -> weight 0.0
|
|
28
|
+
forgotten (R <= 0.05) -> weight 0.0
|
|
29
|
+
|
|
30
|
+
References:
|
|
31
|
+
Ebbinghaus H (1885). Memory: A Contribution to Experimental Psychology.
|
|
32
|
+
Anderson J R & Lebiere C (1998). The Atomic Components of Thought. ACT-R.
|
|
33
|
+
Zhong W et al. (2024). MemoryBank: Enhancing Large Language Models with
|
|
34
|
+
Long-Term Memory. AAAI 2024.
|
|
35
|
+
|
|
36
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
37
|
+
License: MIT
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import logging
|
|
43
|
+
import math
|
|
44
|
+
from dataclasses import dataclass
|
|
45
|
+
from datetime import UTC, datetime
|
|
46
|
+
from typing import TYPE_CHECKING, TypedDict
|
|
47
|
+
|
|
48
|
+
if TYPE_CHECKING:
|
|
49
|
+
from superlocalmemory.storage.database import DatabaseManager
|
|
50
|
+
|
|
51
|
+
from superlocalmemory.core.config import ForgettingConfig
|
|
52
|
+
|
|
53
|
+
logger = logging.getLogger(__name__)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# Data types
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class MemoryStrength:
|
|
62
|
+
"""Computed strength for a single memory."""
|
|
63
|
+
|
|
64
|
+
fact_id: str
|
|
65
|
+
strength: float # S(m) in [S_MIN, S_MAX]
|
|
66
|
+
access_component: float # alpha * log(1 + access_count)
|
|
67
|
+
importance_component: float # beta * importance
|
|
68
|
+
confirmation_component: float # gamma * confirmation_count
|
|
69
|
+
emotional_component: float # delta * emotional_salience
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class FactRetentionInput(TypedDict):
|
|
73
|
+
"""Input dict for batch retention computation. All keys are required."""
|
|
74
|
+
|
|
75
|
+
fact_id: str
|
|
76
|
+
access_count: int
|
|
77
|
+
importance: float # PageRank score from fact_importance
|
|
78
|
+
confirmation_count: int # Mapped from atomic_facts.evidence_count
|
|
79
|
+
emotional_salience: float # Mapped from atomic_facts.emotional_valence
|
|
80
|
+
last_accessed_at: str # ISO 8601 datetime string
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
# Lifecycle zone weights
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
_ZONE_WEIGHTS: dict[str, float] = {
|
|
88
|
+
"active": 1.0,
|
|
89
|
+
"warm": 0.7,
|
|
90
|
+
"cold": 0.3,
|
|
91
|
+
"archive": 0.0,
|
|
92
|
+
"forgotten": 0.0,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
# EbbinghausCurve
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
class EbbinghausCurve:
|
|
101
|
+
"""Ebbinghaus forgetting curve with configurable strength formula.
|
|
102
|
+
|
|
103
|
+
Provides retention computation, memory strength calculation,
|
|
104
|
+
spaced repetition updates, lifecycle zone classification, and
|
|
105
|
+
batch processing for the forgetting scheduler.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
__slots__ = ("_config",)
|
|
109
|
+
|
|
110
|
+
def __init__(self, config: ForgettingConfig) -> None:
|
|
111
|
+
self._config = config
|
|
112
|
+
|
|
113
|
+
def retention(self, hours_since_access: float, strength: float) -> float:
|
|
114
|
+
"""Compute Ebbinghaus retention R(t) = e^(-t/S).
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
hours_since_access: Hours since last access. Negative treated as fresh.
|
|
118
|
+
strength: Memory strength S. Floored at min_strength.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Retention score in [0.0, 1.0]. Never NaN, never Inf.
|
|
122
|
+
"""
|
|
123
|
+
# HR-02: Validate negative time
|
|
124
|
+
if hours_since_access < 0:
|
|
125
|
+
return 1.0
|
|
126
|
+
|
|
127
|
+
# HR-03: Floor strength
|
|
128
|
+
s = max(self._config.min_strength, strength)
|
|
129
|
+
|
|
130
|
+
# Compute decay rate and retention
|
|
131
|
+
lambda_ = 1.0 / s
|
|
132
|
+
r = math.exp(-lambda_ * hours_since_access)
|
|
133
|
+
|
|
134
|
+
# NaN/Inf guard (A-MED-02) — MUST come before clamp
|
|
135
|
+
if math.isnan(r) or math.isinf(r):
|
|
136
|
+
logger.warning(
|
|
137
|
+
"retention(): NaN/Inf detected (lambda_=%f, t=%f), returning 0.0",
|
|
138
|
+
lambda_, hours_since_access,
|
|
139
|
+
)
|
|
140
|
+
return 0.0
|
|
141
|
+
|
|
142
|
+
# HR-02: Clamp to [0.0, 1.0]
|
|
143
|
+
return max(0.0, min(1.0, r))
|
|
144
|
+
|
|
145
|
+
def memory_strength(
|
|
146
|
+
self,
|
|
147
|
+
access_count: int,
|
|
148
|
+
importance: float,
|
|
149
|
+
confirmation_count: int,
|
|
150
|
+
emotional_salience: float,
|
|
151
|
+
) -> float:
|
|
152
|
+
"""Compute composite memory strength S(m).
|
|
153
|
+
|
|
154
|
+
S = alpha * log(1 + access_count)
|
|
155
|
+
+ beta * importance
|
|
156
|
+
+ gamma * confirmation_count
|
|
157
|
+
+ delta * emotional_salience
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
access_count: Number of times memory was accessed.
|
|
161
|
+
importance: PageRank importance score.
|
|
162
|
+
confirmation_count: Number of confirmations/evidence.
|
|
163
|
+
emotional_salience: Emotional strength [-1, 1].
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Clamped strength in [min_strength, max_strength].
|
|
167
|
+
"""
|
|
168
|
+
cfg = self._config
|
|
169
|
+
a = cfg.alpha * math.log(1.0 + access_count)
|
|
170
|
+
b = cfg.beta * importance
|
|
171
|
+
c = cfg.gamma * confirmation_count
|
|
172
|
+
d = cfg.delta * emotional_salience
|
|
173
|
+
s = a + b + c + d
|
|
174
|
+
|
|
175
|
+
# HR-03: Clamp to bounds
|
|
176
|
+
return max(cfg.min_strength, min(cfg.max_strength, s))
|
|
177
|
+
|
|
178
|
+
def compute_strength(
|
|
179
|
+
self,
|
|
180
|
+
fact_id: str,
|
|
181
|
+
access_count: int,
|
|
182
|
+
importance: float,
|
|
183
|
+
confirmation_count: int,
|
|
184
|
+
emotional_salience: float,
|
|
185
|
+
) -> MemoryStrength:
|
|
186
|
+
"""Compute MemoryStrength dataclass for a single fact.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
MemoryStrength with all component scores.
|
|
190
|
+
"""
|
|
191
|
+
cfg = self._config
|
|
192
|
+
a = cfg.alpha * math.log(1.0 + access_count)
|
|
193
|
+
b = cfg.beta * importance
|
|
194
|
+
c = cfg.gamma * confirmation_count
|
|
195
|
+
d = cfg.delta * emotional_salience
|
|
196
|
+
s = a + b + c + d
|
|
197
|
+
s = max(cfg.min_strength, min(cfg.max_strength, s))
|
|
198
|
+
|
|
199
|
+
return MemoryStrength(
|
|
200
|
+
fact_id=fact_id,
|
|
201
|
+
strength=s,
|
|
202
|
+
access_component=a,
|
|
203
|
+
importance_component=b,
|
|
204
|
+
confirmation_component=c,
|
|
205
|
+
emotional_component=d,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def spaced_repetition_update(
|
|
209
|
+
self, current_strength: float, hours_since_last_access: float,
|
|
210
|
+
) -> float:
|
|
211
|
+
"""Boost strength on re-access (spaced repetition effect).
|
|
212
|
+
|
|
213
|
+
Longer gaps between accesses produce larger boosts — this is the
|
|
214
|
+
spacing effect from cognitive science.
|
|
215
|
+
|
|
216
|
+
HR-07: Only INCREASES strength, never decreases.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
current_strength: Current memory strength.
|
|
220
|
+
hours_since_last_access: Hours since last access.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Updated strength, clamped to [min_strength, max_strength].
|
|
224
|
+
"""
|
|
225
|
+
cfg = self._config
|
|
226
|
+
interval = math.log(1.0 + hours_since_last_access / 24.0)
|
|
227
|
+
boost = cfg.learning_rate * interval
|
|
228
|
+
s_new = current_strength + boost
|
|
229
|
+
|
|
230
|
+
# HR-03: Clamp
|
|
231
|
+
return max(cfg.min_strength, min(cfg.max_strength, s_new))
|
|
232
|
+
|
|
233
|
+
def lifecycle_zone(self, retention_score: float) -> str:
|
|
234
|
+
"""Classify retention score into lifecycle zone.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
retention_score: R in [0, 1].
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
One of: 'active', 'warm', 'cold', 'archive', 'forgotten'.
|
|
241
|
+
"""
|
|
242
|
+
if retention_score > 0.8:
|
|
243
|
+
return "active"
|
|
244
|
+
if retention_score > 0.5:
|
|
245
|
+
return "warm"
|
|
246
|
+
if retention_score > self._config.archive_threshold:
|
|
247
|
+
return "cold"
|
|
248
|
+
if retention_score > self._config.forget_threshold:
|
|
249
|
+
return "archive"
|
|
250
|
+
return "forgotten"
|
|
251
|
+
|
|
252
|
+
def lifecycle_weight(self, zone: str) -> float:
|
|
253
|
+
"""Get retrieval weight for a lifecycle zone.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
zone: Lifecycle zone name.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Weight in [0.0, 1.0].
|
|
260
|
+
"""
|
|
261
|
+
return _ZONE_WEIGHTS.get(zone, 0.0)
|
|
262
|
+
|
|
263
|
+
def batch_compute_retention(
|
|
264
|
+
self, facts: list[FactRetentionInput],
|
|
265
|
+
) -> list[dict]:
|
|
266
|
+
"""Compute retention for a batch of facts.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
facts: List of FactRetentionInput dicts.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
List of dicts with fact_id, retention, strength, zone.
|
|
273
|
+
"""
|
|
274
|
+
now = datetime.now(UTC)
|
|
275
|
+
results: list[dict] = []
|
|
276
|
+
|
|
277
|
+
for fact in facts:
|
|
278
|
+
access_count = fact["access_count"]
|
|
279
|
+
importance = fact["importance"]
|
|
280
|
+
confirmation_count = fact["confirmation_count"]
|
|
281
|
+
emotional_salience = fact["emotional_salience"]
|
|
282
|
+
last_accessed_at = fact["last_accessed_at"]
|
|
283
|
+
|
|
284
|
+
# Compute hours since last access
|
|
285
|
+
try:
|
|
286
|
+
last_dt = datetime.fromisoformat(last_accessed_at)
|
|
287
|
+
# Ensure timezone-aware
|
|
288
|
+
if last_dt.tzinfo is None:
|
|
289
|
+
last_dt = last_dt.replace(tzinfo=UTC)
|
|
290
|
+
hours_since = (now - last_dt).total_seconds() / 3600.0
|
|
291
|
+
except (ValueError, TypeError):
|
|
292
|
+
hours_since = 0.0
|
|
293
|
+
|
|
294
|
+
strength = self.memory_strength(
|
|
295
|
+
access_count, importance, confirmation_count, emotional_salience,
|
|
296
|
+
)
|
|
297
|
+
ret = self.retention(hours_since, strength)
|
|
298
|
+
zone = self.lifecycle_zone(ret)
|
|
299
|
+
|
|
300
|
+
results.append({
|
|
301
|
+
"fact_id": fact["fact_id"],
|
|
302
|
+
"retention": ret,
|
|
303
|
+
"strength": strength,
|
|
304
|
+
"zone": zone,
|
|
305
|
+
"access_count": access_count,
|
|
306
|
+
"last_accessed_at": last_accessed_at,
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
return results
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under the MIT License - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
|
|
5
|
+
"""Fisher-Rao Quantization-Aware Distance (FRQAD).
|
|
6
|
+
|
|
7
|
+
Novel contribution (98% IP confidence): Fisher-Rao geodesic distance on
|
|
8
|
+
mixed-precision embeddings. Quantized memories naturally rank lower because
|
|
9
|
+
their variance is inflated to model quantization uncertainty.
|
|
10
|
+
|
|
11
|
+
sigma_quant = sigma_base * (32 / bit_width) ^ kappa
|
|
12
|
+
|
|
13
|
+
When both embeddings are float32, FRQAD degrades exactly to standard
|
|
14
|
+
Fisher-Rao --- no regression, pure extension.
|
|
15
|
+
|
|
16
|
+
Mathematical basis:
|
|
17
|
+
- Fisher-Rao geodesic (Atkinson & Mitchell 1981, Pinele et al. 2020)
|
|
18
|
+
- Variance inflation models quantization noise as additional uncertainty
|
|
19
|
+
on the statistical manifold of diagonal Gaussians
|
|
20
|
+
|
|
21
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
22
|
+
License: MIT
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import logging
|
|
28
|
+
import math
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
|
|
31
|
+
import numpy as np
|
|
32
|
+
from numpy.typing import NDArray
|
|
33
|
+
|
|
34
|
+
from superlocalmemory.math.fisher import FisherRaoMetric
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
# Valid bit-widths aligned with Phase B/D (no 16-bit format exists).
|
|
39
|
+
_VALID_BIT_WIDTHS: frozenset[int] = frozenset({2, 4, 8, 32})
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
# Configuration
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class FRQADConfig:
|
|
49
|
+
"""Configuration for FRQAD metric.
|
|
50
|
+
|
|
51
|
+
Attributes:
|
|
52
|
+
kappa: Scaling exponent for variance inflation.
|
|
53
|
+
sigma_quant = sigma_base * (32/bw)^kappa.
|
|
54
|
+
temperature: Softmax temperature for similarity conversion.
|
|
55
|
+
enabled: When False, fall back to base Fisher-Rao.
|
|
56
|
+
variance_floor: Minimum per-dimension variance after inflation.
|
|
57
|
+
variance_ceiling: Maximum per-dimension variance after inflation.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
kappa: float = 0.5
|
|
61
|
+
temperature: float = 15.0
|
|
62
|
+
enabled: bool = True
|
|
63
|
+
variance_floor: float = 0.05
|
|
64
|
+
variance_ceiling: float = 10.0
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# FRQAD Metric
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class FRQADMetric:
|
|
73
|
+
"""Fisher-Rao Quantization-Aware Distance metric.
|
|
74
|
+
|
|
75
|
+
Wraps an existing FisherRaoMetric and inflates variance vectors based
|
|
76
|
+
on each embedding's bit-width before computing geodesic distance.
|
|
77
|
+
|
|
78
|
+
Self-consistency property:
|
|
79
|
+
FRQAD(32bit, 32bit) == FisherRao(same pair) within float epsilon.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
__slots__ = ("_base", "_config")
|
|
83
|
+
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
base_metric: FisherRaoMetric,
|
|
87
|
+
config: FRQADConfig,
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Construct FRQAD metric.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
base_metric: Existing Fisher-Rao metric instance.
|
|
93
|
+
config: FRQAD configuration.
|
|
94
|
+
|
|
95
|
+
Raises:
|
|
96
|
+
ValueError: On invalid configuration values.
|
|
97
|
+
"""
|
|
98
|
+
# Validate config ranges (HR-04)
|
|
99
|
+
if not 0.0 <= config.kappa <= 2.0:
|
|
100
|
+
raise ValueError(
|
|
101
|
+
f"kappa must be in [0.0, 2.0], got {config.kappa}"
|
|
102
|
+
)
|
|
103
|
+
if not 1.0 <= config.temperature <= 100.0:
|
|
104
|
+
raise ValueError(
|
|
105
|
+
f"temperature must be in [1.0, 100.0], got {config.temperature}"
|
|
106
|
+
)
|
|
107
|
+
if not 0.001 <= config.variance_floor <= 1.0:
|
|
108
|
+
raise ValueError(
|
|
109
|
+
f"variance_floor must be in [0.001, 1.0], got {config.variance_floor}"
|
|
110
|
+
)
|
|
111
|
+
if not 1.0 <= config.variance_ceiling <= 100.0:
|
|
112
|
+
raise ValueError(
|
|
113
|
+
f"variance_ceiling must be in [1.0, 100.0], got {config.variance_ceiling}"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
self._base = base_metric
|
|
117
|
+
self._config = config
|
|
118
|
+
|
|
119
|
+
# ------------------------------------------------------------------
|
|
120
|
+
# Quantization variance inflation
|
|
121
|
+
# ------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
def quantization_variance(
|
|
124
|
+
self,
|
|
125
|
+
base_variance: NDArray[np.floating],
|
|
126
|
+
bit_width: int,
|
|
127
|
+
) -> NDArray[np.floating]:
|
|
128
|
+
"""Inflate variance to model quantization uncertainty.
|
|
129
|
+
|
|
130
|
+
HR-02: NEVER decreases base variance (scale >= 1.0 since bw <= 32).
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
base_variance: Per-dimension variance vector (strictly positive).
|
|
134
|
+
bit_width: Embedding precision in bits.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Quantization-aware variance, clamped to [floor, ceiling].
|
|
138
|
+
"""
|
|
139
|
+
if bit_width not in _VALID_BIT_WIDTHS:
|
|
140
|
+
logger.warning(
|
|
141
|
+
"Unknown bit_width %d, treating as 32 (no penalty)", bit_width,
|
|
142
|
+
)
|
|
143
|
+
return np.array(base_variance, dtype=np.float64)
|
|
144
|
+
|
|
145
|
+
if bit_width >= 32:
|
|
146
|
+
return np.array(base_variance, dtype=np.float64)
|
|
147
|
+
|
|
148
|
+
scale = (32.0 / bit_width) ** self._config.kappa
|
|
149
|
+
sigma_q = np.asarray(base_variance, dtype=np.float64) * scale
|
|
150
|
+
|
|
151
|
+
return np.clip(sigma_q, self._config.variance_floor, self._config.variance_ceiling)
|
|
152
|
+
|
|
153
|
+
# ------------------------------------------------------------------
|
|
154
|
+
# Core distance (THE novel contribution)
|
|
155
|
+
# ------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
def distance(
|
|
158
|
+
self,
|
|
159
|
+
mu_a: NDArray[np.floating],
|
|
160
|
+
var_a: NDArray[np.floating],
|
|
161
|
+
bw_a: int,
|
|
162
|
+
mu_b: NDArray[np.floating],
|
|
163
|
+
var_b: NDArray[np.floating],
|
|
164
|
+
bw_b: int,
|
|
165
|
+
) -> float:
|
|
166
|
+
"""FRQAD distance between mixed-precision embeddings.
|
|
167
|
+
|
|
168
|
+
Inflates variance based on bit-width, then delegates to the
|
|
169
|
+
standard Fisher-Rao geodesic.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
mu_a: Mean of embedding A.
|
|
173
|
+
var_a: Per-dimension variance of A.
|
|
174
|
+
bw_a: Bit-width of A.
|
|
175
|
+
mu_b: Mean of embedding B.
|
|
176
|
+
var_b: Per-dimension variance of B.
|
|
177
|
+
bw_b: Bit-width of B.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Non-negative geodesic distance.
|
|
181
|
+
"""
|
|
182
|
+
if not self._config.enabled:
|
|
183
|
+
return self._base.distance(
|
|
184
|
+
np.asarray(mu_a, dtype=np.float64).tolist(),
|
|
185
|
+
np.asarray(var_a, dtype=np.float64).tolist(),
|
|
186
|
+
np.asarray(mu_b, dtype=np.float64).tolist(),
|
|
187
|
+
np.asarray(var_b, dtype=np.float64).tolist(),
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
var_a_q = self.quantization_variance(var_a, bw_a)
|
|
191
|
+
var_b_q = self.quantization_variance(var_b, bw_b)
|
|
192
|
+
|
|
193
|
+
return self._base.distance(
|
|
194
|
+
np.asarray(mu_a, dtype=np.float64).tolist(),
|
|
195
|
+
var_a_q.tolist(),
|
|
196
|
+
np.asarray(mu_b, dtype=np.float64).tolist(),
|
|
197
|
+
var_b_q.tolist(),
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# ------------------------------------------------------------------
|
|
201
|
+
# Similarity (exponential kernel)
|
|
202
|
+
# ------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
def similarity(
|
|
205
|
+
self,
|
|
206
|
+
mu_a: NDArray[np.floating],
|
|
207
|
+
var_a: NDArray[np.floating],
|
|
208
|
+
bw_a: int,
|
|
209
|
+
mu_b: NDArray[np.floating],
|
|
210
|
+
var_b: NDArray[np.floating],
|
|
211
|
+
bw_b: int,
|
|
212
|
+
) -> float:
|
|
213
|
+
"""Convert FRQAD distance to similarity in [0, 1].
|
|
214
|
+
|
|
215
|
+
sim = clamp(exp(-distance / temperature), 0, 1)
|
|
216
|
+
|
|
217
|
+
HR-03: No NaN, no negative, no > 1.
|
|
218
|
+
"""
|
|
219
|
+
d = self.distance(mu_a, var_a, bw_a, mu_b, var_b, bw_b)
|
|
220
|
+
sim = math.exp(-d / self._config.temperature)
|
|
221
|
+
return max(0.0, min(1.0, sim))
|
|
222
|
+
|
|
223
|
+
# ------------------------------------------------------------------
|
|
224
|
+
# Batch similarity
|
|
225
|
+
# ------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
def batch_similarity(
|
|
228
|
+
self,
|
|
229
|
+
query_mu: NDArray[np.floating],
|
|
230
|
+
query_var: NDArray[np.floating],
|
|
231
|
+
query_bw: int,
|
|
232
|
+
candidates: list[tuple[str, NDArray[np.floating], NDArray[np.floating], int]],
|
|
233
|
+
) -> list[tuple[str, float]]:
|
|
234
|
+
"""Score a batch of candidate memories against a query.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
query_mu: Query embedding mean.
|
|
238
|
+
query_var: Query embedding variance.
|
|
239
|
+
query_bw: Query bit-width (typically 32).
|
|
240
|
+
candidates: List of (fact_id, mu, var, bit_width) tuples.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
List of (fact_id, similarity) sorted descending by score.
|
|
244
|
+
"""
|
|
245
|
+
results: list[tuple[str, float]] = []
|
|
246
|
+
for fact_id, mu, var, bw in candidates:
|
|
247
|
+
sim = self.similarity(query_mu, query_var, query_bw, mu, var, bw)
|
|
248
|
+
results.append((fact_id, sim))
|
|
249
|
+
|
|
250
|
+
results.sort(key=lambda x: x[1], reverse=True)
|
|
251
|
+
return results
|