File size: 8,298 Bytes
d997548
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
#!/usr/bin/env python3
"""
R5-compliant saturation module.

Cell = group of neurons in an MLP layer.
6D knowledge vector K = [L, W, H, D, T, E]

Dimensions (per R5 paper, Section 3.4):
  K1 (L) - sequential coverage (long-range reach)
  K2 (W) - domain breadth (number of domains activated)  [needs domain labels]
  K3 (H) - abstraction level (low entropy = focused)
  K4 (D) - reasoning depth (effective path length)       [needs plexus graph]
  K5 (T) - temporal freshness (exponential decay by age)
  K6 (E) - encompassment (full facet coverage)           [needs facets.json]

S = ||K||_omega (weighted L2, omega in simplex)
S* = 0.85 (split threshold), T_cons = 3 (consecutive steps)
"""
import json
import math
from dataclasses import dataclass, field, asdict
from pathlib import Path
from typing import Optional
import numpy as np

N_CELLS_PER_LAYER = 4
# R8 anti-explosion cap
MAX_CELLS_TOTAL = 5000
S_STAR = 0.70   # R5 paper value; K6 now contributes 20% to S
T_CONSECUTIVE = 3
DECAY_LAMBDA = 0.001  # for K5 (temporal freshness)


@dataclass
class Cell:
    cell_id: str
    layer_idx: int
    neuron_start: int
    neuron_end: int
    K: list = field(default_factory=lambda: [0.0] * 6)
    omega: list = field(default_factory=lambda: [1/6] * 6)
    S: float = 0.0
    S_history: list = field(default_factory=list)
    K_history: list = field(default_factory=list)   # R8: K per measurement
    state: str = "active"
    splits: int = 0
    parent_id: Optional[str] = None
    created_at: str = ""
    last_updated_step: int = 0
    fp_history: list = field(default_factory=list)   # recent pattern fingerprints
    pattern_sig: str = ""                              # dominant pattern signature
    last_split_step: int = 0                           # cooldown tracking

    def num_neurons(self) -> int:
        return self.neuron_end - self.neuron_start

def compute_K1_sequential_coverage(activations: np.ndarray) -> float:
    """
    K1 (L): sequential coverage / long-range reach.
    activations: shape [seq_len, num_neurons] for ONE sample/cell.
    Returns: average normalized range of positions where neurons are active.
    High K1 = cell responds to tokens spread across long range.
    Low K1 = cell responds only to nearby tokens.
    """
    if activations.size == 0:
        return 0.0
    a = np.abs(np.asarray(activations, dtype=np.float32))
    seq_len = a.shape[0]
    if seq_len < 2:
        return 0.0
    # per-neuron: measure span between first and last activation
    active = a > a.mean(axis=0, keepdims=True)
    spans = []
    for j in range(a.shape[1]):
        idx = np.where(active[:, j])[0]
        if len(idx) >= 2:
            spans.append((idx[-1] - idx[0]) / (seq_len - 1))
    if not spans:
        return 0.0
    return float(np.clip(np.mean(spans), 0.0, 1.0))

def compute_K3_abstraction(activations: np.ndarray) -> float:
    """
    K3 (H): abstraction level — activation sparsity.

    FIX (2026-10-02): Entropy-based formula gives ~0 with dense swish activations.
    New formula: Gini-style sparsity = fraction of "concentration" in top neurons.
    A cell that fires on few neurons strongly = high K3 (abstraction).
    A cell that fires uniformly = low K3 (diffuse).
    """
    if activations.size == 0:
        return 0.0
    a = np.abs(np.asarray(activations, dtype=np.float32))
    # mean activation per neuron
    p = a.mean(axis=0)
    n = len(p)
    if n < 2:
        return 1.0
    total = p.sum()
    if total < 1e-9:
        return 0.0
    # Gini coefficient: 0 = uniform, 1 = all concentrated in one
    p_sorted = np.sort(p)
    idx = np.arange(1, n + 1)
    gini = (2 * np.sum(idx * p_sorted)) / (n * total) - (n + 1) / n
    gini = float(np.clip(gini, 0.0, 1.0))
    return gini

def compute_K5_temporal_freshness(
    last_updated_step: int,
    current_step: int,
    decay_lambda: float = DECAY_LAMBDA,
) -> float:
    """
    K5 (T): temporal freshness.
    Exponential decay by age.
    Recently-updated cells: K5 near 1.
    Old cells: K5 near 0.
    """
    # FIX: if never measured (last_updated_step=0), treat as fresh (age=0)
    if last_updated_step == 0:
        return 1.0
    age = max(0, current_step - last_updated_step)
    # FIX: cap age to avoid catastrophic decay (e.g. max 500 steps)
    age = min(age, 500)
    return float(math.exp(-decay_lambda * age))

def compute_K2_domain_breadth(activations: np.ndarray, domains=None) -> float:
    """K2 (W): domain breadth. TODO: needs domain labels per sample."""
    return 0.0


def compute_K4_reasoning_depth(activations: np.ndarray, plexus=None) -> float:
    """K4 (D): reasoning depth. TODO: needs plexus graph."""
    return 0.0


def compute_K6_encompassment(activations: np.ndarray, facets=None) -> float:
    """K6 (E): encompassment. TODO: needs facets.json taxonomy."""
    return 0.0


def compute_S(K: list, omega: list) -> float:
    """
    Weighted L2 norm of K over ACTIVE dimensions only.
    
    FIX (2026-10-02): With K2=K4=K6=0 (stubs), max possible S was 0.707 < S*=0.85.
    Renormalizing omega over non-zero K dims makes S in [0,1] again.
    When all 6 dims are implemented, this reduces to the original formula.
    """
    k = np.asarray(K, dtype=np.float32)
    w_final = np.array([0.375, 0.0, 0.10, 0.0, 0.375, 0.15], dtype=np.float32)
    w_final = w_final / (w_final.sum() + 1e-9)
    return float(np.sqrt(np.sum(w_final * k * k)))


def save_cells(cells: list, path: Path):
    path = Path(path)
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps([asdict(c) for c in cells], indent=2))
    return path


def load_cells(path: Path) -> list:
    data = json.loads(Path(path).read_text())
    cells = []
    for d in data:
        # tolerate unknown fields
        try:
            cells.append(Cell(**d))
        except TypeError:
            # strip unknown keys
            from dataclasses import fields
            valid = {f.name for f in fields(Cell)}
            filtered = {k: v for k, v in d.items() if k in valid}
            cells.append(Cell(**filtered))
    return cells


if __name__ == "__main__":
    a = np.random.randn(128, 1536).astype(np.float32)
    K1 = compute_K1_sequential_coverage(a)
    K3 = compute_K3_abstraction(a)
    K5 = compute_K5_temporal_freshness(0, 1000)
    print(f"K1 = {K1:.3f}")
    print(f"K3 = {K3:.3f}")
    print(f"K5 = {K5:.3f}")
    K = [K1, 0.0, K3, 0.0, K5, 0.0]
    print(f"S  = {compute_S(K, [1/6]*6):.3f}")


def load_facets(path=None) -> dict:
    """Load facets taxonomy from facets.json."""
    if path is None:
        path = Path(__file__).parent / "facets.json"
    data = json.loads(Path(path).read_text())
    return data.get("domains", {})


def compute_K6_encompassment_real(
    activated_facets=None,
    domain=None,
    facets_map=None,
    fp_history=None,
    similarity_threshold=0.7,
) -> float:
    """
    K6 (E): encompassment — coverage of distinct knowledge patterns.

    Bottom-up design (2026-10-02): K6 grows when a cell responds to
    multiple distinct activation patterns over time.

    Logic:
      - Each pattern is a binary fingerprint of top-K neurons.
      - Count distinct patterns (by Hamming similarity < threshold).
      - K6 = min(1.0, distinct_patterns / TARGET_PATTERNS)
      - TARGET_PATTERNS = 5 (empirical; adjust if needed)
    """
    if fp_history is None or len(fp_history) < 3:
        return 0.0

    try:
        from cognitive_log import hamming_similarity
    except ImportError:
        import sys
        sys.path.insert(0, "/data/berna-r7/core/code")
        from cognitive_log import hamming_similarity

    TARGET = 5
    unique = []
    for fp in fp_history:
        is_new = True
        for u in unique:
            if hamming_similarity(fp, u) > similarity_threshold:
                is_new = False
                break
        if is_new:
            unique.append(fp)
            if len(unique) >= TARGET:
                return 1.0
    return float(len(unique) / TARGET)


def compute_K4_from_plexus(cell_id: str, plexus) -> float:
    """K4 (D) via plexus graph. Wrapper for plexus.compute_K4."""
    if plexus is None:
        return 0.0
    try:
        from .plexus import compute_K4 as _k4
        return _k4(plexus, cell_id)
    except Exception:
        return 0.0