CIE-Unified


commit
fabbb69
parent
d00a2ea
author
im_wower
date
2026-04-01 10:59:35 +0800 CST
integration phase2.2: Kernel Sanity Gate——修3个内核bug+6项验证 (53/53)
4 files changed,  +207, -2
Raw patch view.
  1diff --git a/cie/dynamics.py b/cie/dynamics.py
  2index ea706aefd841cb5daf645b9cba3b9d431e76c988..c3dfcbb2a99034ffcd7a5b7dff01bd7b80aaf449 100644
  3--- a/cie/dynamics.py
  4+++ b/cie/dynamics.py
  5@@ -106,6 +106,8 @@ class Dynamics:
  6                 self.state.J[(node_id, nb)] = (
  7                     self.state.J.get((node_id, nb), 0.0) * 0.9 + flow
  8                 )
  9+                # attention accounting: 传播到邻居的激活也要记账
 10+                self.state.attention.allocate(nb, flow)
 11                 # 记录经验命中
 12                 self.state.experience_hits[nb] = (
 13                     self.state.experience_hits.get(nb, 0) + 1
 14@@ -113,6 +115,8 @@ class Dynamics:
 15                 self.state.active_region.add(nb)
 16 
 17             new_mu[node_id] = mu_v - spread_amount
 18+            # attention accounting: 释放从源节点流出的激活
 19+            self.state.attention.release(node_id, spread_amount)
 20 
 21         self.state.mu.update(new_mu)
 22         # 清理极低激活
 23diff --git a/cie/graph.py b/cie/graph.py
 24index 35e41402d73508d0d3bd48dce5f101b9791d1d20..d24bd4dec8cccd9a05086df1c90ab401c9bdd648 100644
 25--- a/cie/graph.py
 26+++ b/cie/graph.py
 27@@ -183,7 +183,7 @@ class Graph:
 28         result = 0.0
 29         # 对于每条边 (node_id, dst)
 30         for dst, fwd_edge in self.fwd_edges.get(node_id, {}).items():
 31-            bwd_w = self.get_bwd_weight(node_id, dst)
 32+            bwd_w = self.get_bwd_weight(dst, node_id)  # dst←node_id 的反向权重
 33             result += (fwd_edge.weight - bwd_w) * phi.get(dst, 0.0)
 34 
 35         return result
 36@@ -222,7 +222,7 @@ class Graph:
 37                       for nid, n in self.nodes.items()},
 38             'edges': [
 39                 {'src': e.src, 'dst': e.dst, 'weight': e.weight,
 40-                 'bwd_weight': self.get_bwd_weight(e.src, e.dst),
 41+                 'bwd_weight': self.get_bwd_weight(e.dst, e.src),  # dst←src 的反向权重
 42                  'type': e.edge_type}
 43                 for src_edges in self.fwd_edges.values()
 44                 for e in src_edges.values()
 45diff --git a/cie/runtime.py b/cie/runtime.py
 46index cb4b3b5b09a25ea799d685f0490df9b265ca8f8d..a5ddd668d78e51e42247e0501180cb1ccee4df41 100644
 47--- a/cie/runtime.py
 48+++ b/cie/runtime.py
 49@@ -55,9 +55,17 @@ class CIERuntime:
 50             elif isinstance(anchors, (list, tuple)):
 51                 anchor_tokens = list(anchors)
 52 
 53+        context_tokens = []
 54+        if context:
 55+            if isinstance(context, str):
 56+                context_tokens = list(context)[:10]
 57+            elif isinstance(context, (list, tuple)):
 58+                context_tokens = list(context)[:10]
 59+
 60         signal = PendingSignal(
 61             source="external",
 62             tokens=tokens,
 63+            context_tokens=context_tokens,
 64             anchor_tokens=anchor_tokens,
 65             strength=1.0,
 66             polarity=1,
 67@@ -118,6 +126,24 @@ class CIERuntime:
 68             self.state.update_confidence(src, 0, amount=0.5 * signal.strength)
 69             self.state.update_confidence(dst, 1, amount=0.5 * signal.strength)
 70 
 71+        # ── context 消费:context tokens 与主 tokens 建立弱关联 ──
 72+        for ctx in signal.context_tokens:
 73+            if not ctx:
 74+                continue
 75+            if not self.graph.has_node(ctx):
 76+                self.graph.add_node(ctx, label=ctx)
 77+                self.state.init_node(ctx, phi_val=self.rng.gauss(0.0, 0.05))
 78+            # context 与每个主 token 建立弱边
 79+            for token in tokens[:5]:  # 只取前 5 个避免爆炸
 80+                if token and token != ctx:
 81+                    existing = self.graph.get_edge_weight(ctx, token)
 82+                    self.graph.add_edge(
 83+                        ctx, token,
 84+                        weight=existing + 0.3 * signal.strength,
 85+                        bwd_weight=existing + 0.2 * signal.strength,
 86+                        edge_type='context'
 87+                    )
 88+
 89         # ── 锚点注入 ──
 90         for anchor in signal.anchor_tokens:
 91             if not self.graph.has_node(anchor):
 92diff --git a/tests/test_kernel_sanity.py b/tests/test_kernel_sanity.py
 93new file mode 100644
 94index 0000000000000000000000000000000000000000..aa7f237efb1d9cddcc1f81612679a2e44b2b4911
 95--- /dev/null
 96+++ b/tests/test_kernel_sanity.py
 97@@ -0,0 +1,175 @@
 98+"""
 99+Phase 2.2: Kernel Sanity Gate Tests
100+=====================================
101+验证 Branch B 内核的 4 个关键接线:
102+1. asymmetry / backward-weight 读取正确性
103+2. Dirichlet per-node wiring(cat0/cat1/cat2 真正被更新)
104+3. context 参数真实消费
105+4. attention ledger 与 mu 对齐
106+"""
107+
108+import sys
109+import os
110+import math
111+
112+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
113+from cie import CIERuntime
114+from cie.graph import Graph
115+
116+
117+def test_asymmetry_correct():
118+    """asymmetry_at 使用正确的反向权重"""
119+    g = Graph()
120+    g.add_edge('a', 'b', weight=2.0, bwd_weight=0.5)
121+
122+    phi = {'a': 1.0, 'b': 1.0}
123+
124+    # asymmetry_at('a') 应该用 fwd('a','b')=2.0 和 bwd('a','b')
125+    # bwd_weight 是 b→a 方向的权重 = 0.5
126+    # asymmetry = (fwd - bwd) * phi(b) = (2.0 - 0.5) * 1.0 = 1.5
127+    asym = g.asymmetry_at('a', phi)
128+    expected = (2.0 - 0.5) * 1.0  # 1.5
129+
130+    assert abs(asym - expected) < 0.01, \
131+        f"asymmetry_at wrong: got {asym}, expected {expected}"
132+    print(f"  PASS: asymmetry_at('a')={asym:.2f}, expected={expected:.2f}")
133+
134+
135+def test_bwd_weight_in_to_dict():
136+    """to_dict 导出正确的反向权重"""
137+    g = Graph()
138+    g.add_edge('x', 'y', weight=3.0, bwd_weight=1.0)
139+
140+    d = g.to_dict()
141+    edges = d['edges']
142+    xy_edge = next(e for e in edges if e['src'] == 'x' and e['dst'] == 'y')
143+
144+    assert abs(xy_edge['weight'] - 3.0) < 0.01, \
145+        f"fwd weight wrong: {xy_edge['weight']}"
146+    assert abs(xy_edge['bwd_weight'] - 1.0) < 0.01, \
147+        f"bwd weight wrong: {xy_edge['bwd_weight']}, expected 1.0"
148+    print(f"  PASS: to_dict bwd_weight={xy_edge['bwd_weight']:.2f}")
149+
150+
151+def test_dirichlet_per_node_wiring():
152+    """普通 ingest 真正更新 per-node Dirichlet cat0/cat1"""
153+    rt = CIERuntime(seed=42)
154+    rt.ingest("你好世界")
155+    rt.step(n=1)  # 消费信号
156+
157+    # bigram 你→好:你应该有 cat0 更新,好应该有 cat1 更新
158+    conf_ni = rt.state.confidence.get('你', None)
159+    conf_hao = rt.state.confidence.get('好', None)
160+
161+    assert conf_ni is not None, "Node '你' has no confidence"
162+    assert conf_hao is not None, "Node '好' has no confidence"
163+
164+    # cat0(左上下文)应该 > 1.0(先验)对于 '你'
165+    assert conf_ni[0] > 1.0, \
166+        f"'你' cat0 not updated: {conf_ni}"
167+
168+    # cat1(右上下文)应该 > 1.0 对于 '好'
169+    assert conf_hao[1] > 1.0, \
170+        f"'好' cat1 not updated: {conf_hao}"
171+
172+    # 不是全都一样(打破了均匀先验)
173+    assert not (abs(conf_ni[0] - conf_ni[1]) < 0.01 and abs(conf_ni[1] - conf_ni[2]) < 0.01), \
174+        f"'你' Dirichlet still uniform: {conf_ni}"
175+
176+    print(f"  PASS: Dirichlet per-node — '你'={[round(x,2) for x in conf_ni]}, "
177+          f"'好'={[round(x,2) for x in conf_hao]}")
178+
179+
180+def test_context_consumed():
181+    """context 参数被真实消费——建立了 context→token 的边"""
182+    rt = CIERuntime(seed=42)
183+    rt.ingest("你好", context="背景")
184+    rt.step(n=1)
185+
186+    # context 字符应该存在于图中
187+    assert rt.graph.has_node('背'), f"Context char '背' not in graph"
188+    assert rt.graph.has_node('景'), f"Context char '景' not in graph"
189+
190+    # context 字符与主 token 之间应有边
191+    has_edge = False
192+    for ctx_char in ['背', '景']:
193+        for tok_char in ['你', '好']:
194+            if rt.graph.get_edge_weight(ctx_char, tok_char) > 0:
195+                has_edge = True
196+                break
197+        if has_edge:
198+            break
199+
200+    assert has_edge, "No edges between context and tokens"
201+    print(f"  PASS: context consumed — '背','景' in graph with edges to tokens")
202+
203+
204+def test_attention_mu_alignment():
205+    """attention.used 与 sum(mu) 在受控场景下对齐"""
206+    rt = CIERuntime(seed=42)
207+    rt.ingest("测试")
208+    rt.step(n=5)
209+
210+    mu_sum = sum(v for v in rt.state.mu.values() if v > 0)
211+    attn_used = rt.state.attention.used
212+
213+    # 允许 20% 偏差(传播中有衰减和清理)
214+    if mu_sum > 0.1:
215+        ratio = attn_used / mu_sum if mu_sum > 0 else float('inf')
216+        # attention 应该 >= mu(因为还包含已衰减但未释放的部分)
217+        # 但不应该差太多
218+        assert 0.5 < ratio < 3.0, \
219+            f"attention/mu drift: attn={attn_used:.2f}, mu={mu_sum:.2f}, ratio={ratio:.2f}"
220+
221+    print(f"  PASS: attention/mu aligned — attn={attn_used:.2f}, mu={mu_sum:.2f}")
222+
223+
224+def test_attention_no_unbounded_drift():
225+    """多轮运行后 attention 不会无界漂移"""
226+    rt = CIERuntime(seed=42)
227+
228+    for i in range(20):
229+        rt.ingest(f"轮次{i}")
230+        rt.step(n=3)
231+
232+    attn = rt.state.attention
233+    assert attn.used <= attn.total + 0.01, \
234+        f"Attention overflow: used={attn.used:.2f} > total={attn.total:.2f}"
235+    assert attn.free >= -0.01, \
236+        f"Attention went negative: free={attn.free:.2f}"
237+
238+    mu_sum = sum(v for v in rt.state.mu.values() if v > 0)
239+    print(f"  PASS: no unbounded drift after 20 rounds — "
240+          f"attn_used={attn.used:.2f}, mu_sum={mu_sum:.2f}, free={attn.free:.2f}")
241+
242+
243+def run_all():
244+    tests = [
245+        ("asymmetry_correct", test_asymmetry_correct),
246+        ("bwd_weight_to_dict", test_bwd_weight_in_to_dict),
247+        ("dirichlet_per_node", test_dirichlet_per_node_wiring),
248+        ("context_consumed", test_context_consumed),
249+        ("attention_mu_align", test_attention_mu_alignment),
250+        ("attention_no_drift", test_attention_no_unbounded_drift),
251+    ]
252+
253+    passed = 0
254+    failed = 0
255+    for name, fn in tests:
256+        try:
257+            print(f"[KERNEL] {name}...")
258+            fn()
259+            passed += 1
260+        except Exception as e:
261+            print(f"  FAIL: {e}")
262+            failed += 1
263+
264+    print(f"\n{'='*50}")
265+    print(f"Kernel Sanity Gate: {passed} passed, {failed} failed, {len(tests)} total")
266+    print(f"{'='*50}")
267+    return failed == 0
268+
269+
270+if __name__ == '__main__':
271+    success = run_all()
272+    sys.exit(0 if success else 1)