Spaces:
Runtime error
Runtime error
Sync from GitHub: 35ed177d9ed18841bc05c5cc14150abd0ee53255
Browse files- nuwave/organism.py +177 -2
- nuwave/substrate/neuro_foundation.py +228 -14
- nuwave/substrate/rpc_mechanisms.py +282 -0
nuwave/organism.py
CHANGED
|
@@ -16,6 +16,56 @@ communication protocol (Law 1). Raw experience in, classification
|
|
| 16 |
only at extraction (Law 7).
|
| 17 |
|
| 18 |
# ---- Changelog ----
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
# [2026-06-05] Claude Opus 4.7 (1M ctx) β Persist-hardening: rotation=20, prediction_threshold=1.5
|
| 20 |
# What: Two organism.py changes:
|
| 21 |
# (1) `_snapshot_to_backup` call site bumps `_prune_old_backups(api, keep=5)`
|
|
@@ -209,6 +259,32 @@ logger = logging.getLogger("nuwave.organism")
|
|
| 209 |
_substrate_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'substrate')
|
| 210 |
|
| 211 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
class NuWaveOrganism:
|
| 213 |
"""The living system. Substrate + KISS bucket + Pith bucket.
|
| 214 |
|
|
@@ -392,6 +468,26 @@ class NuWaveOrganism:
|
|
| 392 |
self._restore_state()
|
| 393 |
self._sanity_check_tract_writes()
|
| 394 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
def _sanity_check_tract_writes(self) -> None:
|
| 396 |
"""Write + read a trivial test entry in each tract to surface any
|
| 397 |
filesystem/permission/argument issues at boot time.
|
|
@@ -1076,6 +1172,18 @@ class NuWaveOrganism:
|
|
| 1076 |
embedding = np.asarray(self._embed_fn(text), dtype=np.float32)
|
| 1077 |
node_id = f"exp_{self._step_count}_{hash(text) & 0xFFFF:04x}"
|
| 1078 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1079 |
# All graph mutations (create + stimulate loop) run under the
|
| 1080 |
# graph lock β the concept worker may concurrently add tree
|
| 1081 |
# nodes from a background thread, and the Graph isn't thread-
|
|
@@ -1084,9 +1192,13 @@ class NuWaveOrganism:
|
|
| 1084 |
# Create node in the SNN. Metadata carries biological timestamp
|
| 1085 |
# only β no "type" label, no "content" curation. The retina
|
| 1086 |
# does not classify photons; V1 discovers features via dynamics.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1087 |
node = self._graph.create_node(
|
| 1088 |
node_id=node_id,
|
| 1089 |
-
metadata=
|
| 1090 |
)
|
| 1091 |
|
| 1092 |
# Embedding lives in the side-table as numpy f32 (zero inflation).
|
|
@@ -1238,6 +1350,23 @@ class NuWaveOrganism:
|
|
| 1238 |
self._step_result = step_result
|
| 1239 |
self._step_count += 1
|
| 1240 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1241 |
# StepResult has real dataclass fields β read them directly
|
| 1242 |
result = {
|
| 1243 |
'step': self._step_count,
|
|
@@ -1521,6 +1650,29 @@ class NuWaveOrganism:
|
|
| 1521 |
# IS the relevance mechanism.
|
| 1522 |
born_score = effective_amp * effective_amp
|
| 1523 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1524 |
content = self._node_content.get(nid, '')
|
| 1525 |
if content and born_score > 0.001:
|
| 1526 |
scored.append((nid, content, born_score))
|
|
@@ -1828,15 +1980,28 @@ class NuWaveOrganism:
|
|
| 1828 |
response_embedding = np.asarray(self._embed_fn(response), dtype=np.float32)
|
| 1829 |
node_id = f"resp_{self._step_count}_{hash(response) & 0xFFFF:04x}"
|
| 1830 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1831 |
# All graph mutations (create + stimulate) run under the lock
|
| 1832 |
# so the concept worker doesn't race during substrate writes.
|
| 1833 |
with self._graph_lock:
|
| 1834 |
# Metadata carries biological timestamp only β no type label,
|
| 1835 |
# no query field, no truncated content. The substrate discovers
|
| 1836 |
# response-vs-experience distinction via STDP co-firing.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1837 |
node = self._graph.create_node(
|
| 1838 |
node_id=node_id,
|
| 1839 |
-
metadata=
|
| 1840 |
)
|
| 1841 |
self._embeddings[node_id] = response_embedding
|
| 1842 |
|
|
@@ -1880,6 +2045,16 @@ class NuWaveOrganism:
|
|
| 1880 |
except Exception as exc:
|
| 1881 |
logger.debug("Concept enqueue failed: %s", exc)
|
| 1882 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1883 |
# Persist β the organism remembers across restarts
|
| 1884 |
self.save()
|
| 1885 |
|
|
|
|
| 16 |
only at extraction (Law 7).
|
| 17 |
|
| 18 |
# ---- Changelog ----
|
| 19 |
+
# [2026-06-20] Claude Opus 4.7 (1M ctx) β Mind-Not-Database: wire canonical RPC mechanisms
|
| 20 |
+
# What: Six integration points wired into organism.py to call into a new
|
| 21 |
+
# nuwave/substrate/rpc_mechanisms.py module that ports canonical NG's
|
| 22 |
+
# _anticipate, _gsg_backfill_existing_nodes, _update_deposit_cluster,
|
| 23 |
+
# _embed_to_poincare_dir, _poincare_distance, MMN-EMA, and surfacing
|
| 24 |
+
# modulation. Wiring:
|
| 25 |
+
# (1) _import_rpc_mechanisms() module helper near top, matches existing
|
| 26 |
+
# substrate-import pattern (sys.path manipulation, lazy import).
|
| 27 |
+
# (2) __init__: self._substrate_novelty_ema=0.5 + gsg_backfill call
|
| 28 |
+
# after _restore_state (stamps poincare_dir on any restored nodes).
|
| 29 |
+
# (3) deposit_experience: update_deposit_cluster (DiffPC novelty signal)
|
| 30 |
+
# + embed_to_poincare_dir; stamp poincare_dir into node.metadata at
|
| 31 |
+
# create_node time.
|
| 32 |
+
# (4) After self._step_result = step_result: update_substrate_novelty_ema
|
| 33 |
+
# (MMN EMA) + anticipate(fired_node_ids) β #255 + #256 wired live.
|
| 34 |
+
# (5) record_outcome: same poincare_dir stamp for response node + tick
|
| 35 |
+
# self._tonic_thread.ouroboros_cycle() at end of every turn
|
| 36 |
+
# (topology-translation-lab pattern β keep Tonic alive between
|
| 37 |
+
# benchmark events since HF Spaces don't have continuous idle time).
|
| 38 |
+
# (6) pith_extract scoring loop: add get_primed_bonus(nid) and
|
| 39 |
+
# get_gsg_score_bonus(query_dir, metadata, layer) to born_score.
|
| 40 |
+
# Both bonuses are canonical substrate-derived (anticipatory
|
| 41 |
+
# pre-activation + PoincarΓ© hyperbolic proximity), NOT arbitrary
|
| 42 |
+
# heuristics β fits the existing "physics decides" pith design.
|
| 43 |
+
# Also: nuwave/substrate/neuro_foundation.py re-vendored from canonical
|
| 44 |
+
# HEAD (fad1ade), picks up GSG Phase 3 (non-Euclidean message passing
|
| 45 |
+
# in graph.step propagation), GSG Phase 4 (spherical attractor manifold),
|
| 46 |
+
# #325 msgpack-enforcer, #spine identity-protection, and the
|
| 47 |
+
# geometry-informed synaptic delays fix.
|
| 48 |
+
# Why: /home/josh/docs/concepts/NeuroGraph Is a Mind, Not a Database.md
|
| 49 |
+
# (2026-06-14) names the failure mode NuWave fell into directly: stripping
|
| 50 |
+
# canonical mechanisms because the names sound Syl-specific, then ending
|
| 51 |
+
# up with a fancy-SNN-reached-for-like-a-database. NuWave's
|
| 52 |
+
# predictions=0 across 5 maturation runs at 18,244 synapses isn't a
|
| 53 |
+
# density-tuning problem β it's the structural absence of the canonical
|
| 54 |
+
# mechanism that GENERATES predictions (anticipatory pre-activation, #256)
|
| 55 |
+
# and the mechanism that USES surprise to widen surfacing (#255 MMN
|
| 56 |
+
# feedback). Both lived in canonical neurograph_rpc.py, which NuWave
|
| 57 |
+
# had bypassed entirely in favor of its own bespoke organism.py paths.
|
| 58 |
+
# Topology-translation-lab proves the integration pattern works (their
|
| 59 |
+
# /home/josh/topology-translation-lab/neurograph_rpc.py uses the same
|
| 60 |
+
# mechanisms and Tonic-tick-at-assemble pattern).
|
| 61 |
+
# How: Surgical extraction (not full RPC vendor). The 5 generic mechanism
|
| 62 |
+
# functions ported into rpc_mechanisms.py with explicit graph/vec_db
|
| 63 |
+
# params (no canonical _memory global). Organism.py wiring is best-effort
|
| 64 |
+
# (try/except wrap on every integration point) so missing mechanisms
|
| 65 |
+
# never break substrate boot or step lifecycle. Single feature branch
|
| 66 |
+
# per the 2026-06-03 CLAUDE.md git workflow rule:
|
| 67 |
+
# cc-vps-nuwave-mind-not-database-20260620, merged --no-ff.
|
| 68 |
+
# -------------------
|
| 69 |
# [2026-06-05] Claude Opus 4.7 (1M ctx) β Persist-hardening: rotation=20, prediction_threshold=1.5
|
| 70 |
# What: Two organism.py changes:
|
| 71 |
# (1) `_snapshot_to_backup` call site bumps `_prune_old_backups(api, keep=5)`
|
|
|
|
| 259 |
_substrate_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'substrate')
|
| 260 |
|
| 261 |
|
| 262 |
+
def _import_rpc_mechanisms():
|
| 263 |
+
"""Lazy import of rpc_mechanisms from the substrate dir, matching the
|
| 264 |
+
pattern used for neuro_foundation/tonic_thread/activation_persistence.
|
| 265 |
+
|
| 266 |
+
Returns the rpc_mechanisms module, or None if import fails. Caller should
|
| 267 |
+
treat None as "skip mechanism wiring this call" β every integration point
|
| 268 |
+
is best-effort and must not break the substrate path if mechanisms are
|
| 269 |
+
unavailable.
|
| 270 |
+
"""
|
| 271 |
+
_added = _substrate_dir not in sys.path
|
| 272 |
+
if _added:
|
| 273 |
+
sys.path.insert(0, _substrate_dir)
|
| 274 |
+
try:
|
| 275 |
+
import rpc_mechanisms # type: ignore[import-not-found]
|
| 276 |
+
return rpc_mechanisms
|
| 277 |
+
except Exception as _exc:
|
| 278 |
+
logger.debug("rpc_mechanisms import failed (non-fatal): %s", _exc)
|
| 279 |
+
return None
|
| 280 |
+
finally:
|
| 281 |
+
if _added and _substrate_dir in sys.path:
|
| 282 |
+
try:
|
| 283 |
+
sys.path.remove(_substrate_dir)
|
| 284 |
+
except ValueError:
|
| 285 |
+
pass
|
| 286 |
+
|
| 287 |
+
|
| 288 |
class NuWaveOrganism:
|
| 289 |
"""The living system. Substrate + KISS bucket + Pith bucket.
|
| 290 |
|
|
|
|
| 468 |
self._restore_state()
|
| 469 |
self._sanity_check_tract_writes()
|
| 470 |
|
| 471 |
+
# MMN EMA β updated each graph.step() via rpc_mechanisms.update_substrate_novelty_ema.
|
| 472 |
+
# Drives #255 surprise-weighted surfacing modulation. Default 0.5 = neutral.
|
| 473 |
+
self._substrate_novelty_ema: float = 0.5
|
| 474 |
+
|
| 475 |
+
# GSG Phase 1 backfill β stamp poincare_dir on any restored nodes that
|
| 476 |
+
# lack it. Best-effort: failures don't break boot. (#256/#255/GSG wiring
|
| 477 |
+
# from /home/josh/docs/concepts/NeuroGraph Is a Mind, Not a Database.md)
|
| 478 |
+
try:
|
| 479 |
+
_rpc = _import_rpc_mechanisms()
|
| 480 |
+
if _rpc is not None and self._graph is not None:
|
| 481 |
+
vdb = getattr(self, "_vec_db", None) or self
|
| 482 |
+
# NuWave stores embeddings in self._embeddings; build a shim
|
| 483 |
+
# object with .embeddings attribute so rpc_mech sees what it expects.
|
| 484 |
+
class _VDBShim:
|
| 485 |
+
def __init__(self, embeddings):
|
| 486 |
+
self.embeddings = embeddings
|
| 487 |
+
_rpc.gsg_backfill_existing_nodes(self._graph, _VDBShim(self._embeddings))
|
| 488 |
+
except Exception as _exc:
|
| 489 |
+
logger.debug("GSG backfill skipped at boot (non-fatal): %s", _exc)
|
| 490 |
+
|
| 491 |
def _sanity_check_tract_writes(self) -> None:
|
| 492 |
"""Write + read a trivial test entry in each tract to surface any
|
| 493 |
filesystem/permission/argument issues at boot time.
|
|
|
|
| 1172 |
embedding = np.asarray(self._embed_fn(text), dtype=np.float32)
|
| 1173 |
node_id = f"exp_{self._step_count}_{hash(text) & 0xFFFF:04x}"
|
| 1174 |
|
| 1175 |
+
# DiffPC deposit-cluster novelty + GSG Phase 1 PoincarΓ© direction.
|
| 1176 |
+
# Best-effort; failures don't block the deposit path.
|
| 1177 |
+
_novelty = 0.5
|
| 1178 |
+
_poincare_dir = None
|
| 1179 |
+
try:
|
| 1180 |
+
_rpc = _import_rpc_mechanisms()
|
| 1181 |
+
if _rpc is not None:
|
| 1182 |
+
_novelty = _rpc.update_deposit_cluster(embedding)
|
| 1183 |
+
_poincare_dir = _rpc.embed_to_poincare_dir(embedding)
|
| 1184 |
+
except Exception as _exc:
|
| 1185 |
+
logger.debug("DiffPC/GSG ingest mechanisms skipped: %s", _exc)
|
| 1186 |
+
|
| 1187 |
# All graph mutations (create + stimulate loop) run under the
|
| 1188 |
# graph lock β the concept worker may concurrently add tree
|
| 1189 |
# nodes from a background thread, and the Graph isn't thread-
|
|
|
|
| 1192 |
# Create node in the SNN. Metadata carries biological timestamp
|
| 1193 |
# only β no "type" label, no "content" curation. The retina
|
| 1194 |
# does not classify photons; V1 discovers features via dynamics.
|
| 1195 |
+
# GSG Phase 1: stamp poincare_dir if mechanism was available.
|
| 1196 |
+
_meta = {"step": self._step_count}
|
| 1197 |
+
if _poincare_dir is not None:
|
| 1198 |
+
_meta["poincare_dir"] = _poincare_dir.tolist() if hasattr(_poincare_dir, "tolist") else list(_poincare_dir)
|
| 1199 |
node = self._graph.create_node(
|
| 1200 |
node_id=node_id,
|
| 1201 |
+
metadata=_meta,
|
| 1202 |
)
|
| 1203 |
|
| 1204 |
# Embedding lives in the side-table as numpy f32 (zero inflation).
|
|
|
|
| 1350 |
self._step_result = step_result
|
| 1351 |
self._step_count += 1
|
| 1352 |
|
| 1353 |
+
# #255 MMN EMA update + #256 anticipatory pre-activation.
|
| 1354 |
+
# Both run after step() so we have the freshest fired_node_ids
|
| 1355 |
+
# and prediction telemetry. Best-effort; failures don't break
|
| 1356 |
+
# the step lifecycle for the rest of the organism.
|
| 1357 |
+
try:
|
| 1358 |
+
_rpc = _import_rpc_mechanisms()
|
| 1359 |
+
if _rpc is not None:
|
| 1360 |
+
self._substrate_novelty_ema = _rpc.update_substrate_novelty_ema(
|
| 1361 |
+
self._substrate_novelty_ema, step_result,
|
| 1362 |
+
)
|
| 1363 |
+
_rpc.anticipate(
|
| 1364 |
+
self._graph,
|
| 1365 |
+
list(getattr(step_result, "fired_node_ids", []) or []),
|
| 1366 |
+
)
|
| 1367 |
+
except Exception as _exc:
|
| 1368 |
+
logger.debug("MMN/anticipate skipped (non-fatal): %s", _exc)
|
| 1369 |
+
|
| 1370 |
# StepResult has real dataclass fields β read them directly
|
| 1371 |
result = {
|
| 1372 |
'step': self._step_count,
|
|
|
|
| 1650 |
# IS the relevance mechanism.
|
| 1651 |
born_score = effective_amp * effective_amp
|
| 1652 |
|
| 1653 |
+
# Substrate-derived scoring bonuses (NOT arbitrary):
|
| 1654 |
+
# - #256 anticipatory pre-activation: nodes the substrate
|
| 1655 |
+
# primed last turn get a bonus, expressing "I expected
|
| 1656 |
+
# this to be relevant."
|
| 1657 |
+
# - GSG Phase 1: hyperbolic geodesic proximity in PoincarΓ©
|
| 1658 |
+
# ball expresses tree-like semantic hierarchy. Closer
|
| 1659 |
+
# geometry = stronger candidate.
|
| 1660 |
+
# Both come from canonical NG mechanisms now ported in
|
| 1661 |
+
# rpc_mechanisms.py; both are physics, not heuristics.
|
| 1662 |
+
try:
|
| 1663 |
+
_rpc_score = _import_rpc_mechanisms()
|
| 1664 |
+
if _rpc_score is not None:
|
| 1665 |
+
born_score += _rpc_score.get_primed_bonus(nid)
|
| 1666 |
+
_node_obj = self._graph.nodes.get(nid)
|
| 1667 |
+
if _node_obj is not None:
|
| 1668 |
+
born_score += _rpc_score.get_gsg_score_bonus(
|
| 1669 |
+
query_embedding / (np.linalg.norm(query_embedding) + 1e-9),
|
| 1670 |
+
_node_obj.metadata,
|
| 1671 |
+
int(getattr(_node_obj, "diffpc_layer", 2)),
|
| 1672 |
+
)
|
| 1673 |
+
except Exception:
|
| 1674 |
+
pass
|
| 1675 |
+
|
| 1676 |
content = self._node_content.get(nid, '')
|
| 1677 |
if content and born_score > 0.001:
|
| 1678 |
scored.append((nid, content, born_score))
|
|
|
|
| 1980 |
response_embedding = np.asarray(self._embed_fn(response), dtype=np.float32)
|
| 1981 |
node_id = f"resp_{self._step_count}_{hash(response) & 0xFFFF:04x}"
|
| 1982 |
|
| 1983 |
+
# GSG Phase 1: compute poincare_dir for response node too.
|
| 1984 |
+
_resp_poincare = None
|
| 1985 |
+
try:
|
| 1986 |
+
_rpc = _import_rpc_mechanisms()
|
| 1987 |
+
if _rpc is not None:
|
| 1988 |
+
_resp_poincare = _rpc.embed_to_poincare_dir(response_embedding)
|
| 1989 |
+
except Exception as _exc:
|
| 1990 |
+
logger.debug("Response GSG dir skipped: %s", _exc)
|
| 1991 |
+
|
| 1992 |
# All graph mutations (create + stimulate) run under the lock
|
| 1993 |
# so the concept worker doesn't race during substrate writes.
|
| 1994 |
with self._graph_lock:
|
| 1995 |
# Metadata carries biological timestamp only β no type label,
|
| 1996 |
# no query field, no truncated content. The substrate discovers
|
| 1997 |
# response-vs-experience distinction via STDP co-firing.
|
| 1998 |
+
# GSG Phase 1: stamp poincare_dir if available.
|
| 1999 |
+
_resp_meta = {"step": self._step_count}
|
| 2000 |
+
if _resp_poincare is not None:
|
| 2001 |
+
_resp_meta["poincare_dir"] = _resp_poincare.tolist() if hasattr(_resp_poincare, "tolist") else list(_resp_poincare)
|
| 2002 |
node = self._graph.create_node(
|
| 2003 |
node_id=node_id,
|
| 2004 |
+
metadata=_resp_meta,
|
| 2005 |
)
|
| 2006 |
self._embeddings[node_id] = response_embedding
|
| 2007 |
|
|
|
|
| 2045 |
except Exception as exc:
|
| 2046 |
logger.debug("Concept enqueue failed: %s", exc)
|
| 2047 |
|
| 2048 |
+
# Tonic ouroboros tick β keep the latent thread alive across turn
|
| 2049 |
+
# boundaries. Topology-translation-lab pattern: tick at integration
|
| 2050 |
+
# points (per turn) since HF Spaces don't have continuous idle time.
|
| 2051 |
+
# Without this, TonicThread is instantiated but never advances.
|
| 2052 |
+
try:
|
| 2053 |
+
if self._tonic_thread is not None:
|
| 2054 |
+
self._tonic_thread.ouroboros_cycle()
|
| 2055 |
+
except Exception as _exc:
|
| 2056 |
+
logger.debug("Tonic ouroboros tick skipped (non-fatal): %s", _exc)
|
| 2057 |
+
|
| 2058 |
# Persist β the organism remembers across restarts
|
| 2059 |
self.save()
|
| 2060 |
|
nuwave/substrate/neuro_foundation.py
CHANGED
|
@@ -19,6 +19,28 @@ Design principles (PRD Β§2.1):
|
|
| 19 |
- Persistence-native: all state is serializable
|
| 20 |
|
| 21 |
# ---- Changelog ----
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
# [2026-05-26] Claude Opus 4.7 (1M ctx) β #258 Orphan-node grace period
|
| 23 |
# What: Added orphan_node_grace_period config (default 25 steps); added
|
| 24 |
# creation_time field to Node dataclass; create_node() now stamps
|
|
@@ -47,6 +69,50 @@ Design principles (PRD Β§2.1):
|
|
| 47 |
# stamp, orphan check age guard, serializer field, restore default.
|
| 48 |
# Backward-compatible (.get() with default=0). Re-vendored to
|
| 49 |
# NuWave/nuwave/substrate/neuro_foundation.py.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
# [2026-05-25] Claude Code (Sonnet 4.6) β GSG Phase 2: curvature-modulated STDP (neuro_foundation.py)
|
| 51 |
# What: Added _GSG_CURVATURE_TABLE (3Γ3) before STDPRule. In STDPRule.apply(), both _apply_dw()
|
| 52 |
# call sites (incoming + outgoing loops) now multiply dw by the table lookup
|
|
@@ -434,6 +500,7 @@ class Node:
|
|
| 434 |
diffpc_layer: int = 0 # DiffPC layer: 0=novel/input, 1=mid, 2=hub
|
| 435 |
pred_weights: Dict[str, float] = field(default_factory=dict) # nid β prediction weight
|
| 436 |
pred_error_ema: float = 0.0 # EMA of ternary prediction error received
|
|
|
|
| 437 |
creation_time: int = 0 # Timestep when node was created (#258 orphan grace)
|
| 438 |
|
| 439 |
|
|
@@ -793,6 +860,12 @@ _GSG_CURVATURE_TABLE: List[List[float]] = [
|
|
| 793 |
[1.499, 1.213, 1.107], # pre=Layer 1 (mid)
|
| 794 |
[1.392, 1.107, 1.000], # pre=Layer 2 (hub/familiar, near center)
|
| 795 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 796 |
|
| 797 |
|
| 798 |
class STDPRule(PlasticityRule):
|
|
@@ -1000,6 +1073,36 @@ class HomeostaticRule(PlasticityRule):
|
|
| 1000 |
if node is not None:
|
| 1001 |
node.diffpc_layer = 0 if deg <= p33 else (1 if deg <= p67 else 2)
|
| 1002 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1003 |
def apply(
|
| 1004 |
self,
|
| 1005 |
graph: "Graph",
|
|
@@ -1174,7 +1277,8 @@ DEFAULT_CONFIG: Dict[str, Any] = {
|
|
| 1174 |
"d_min": 1, # minimum synaptic delay in timesteps
|
| 1175 |
"d_max": 5, # maximum synaptic delay in timesteps (range enables polychrony)
|
| 1176 |
# DiffPC: Difference Predictive Coding (#DiffPC)
|
| 1177 |
-
"diffpc_epsilon": 0.2,
|
|
|
|
| 1178 |
"diffpc_pred_lr": 0.01, # prediction weight learning rate
|
| 1179 |
"diffpc_trace_boost": 0.05, # eligibility trace Β±boost per ternary spike (Phase 2)
|
| 1180 |
"weight_threshold": 0.01,
|
|
@@ -1843,19 +1947,64 @@ class Graph:
|
|
| 1843 |
result.fired_node_ids = fired_ids
|
| 1844 |
|
| 1845 |
# 5. Propagate spikes through outgoing synapses (with delay)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1846 |
for nid in fired_ids:
|
| 1847 |
node = self.nodes[nid]
|
| 1848 |
sign = -1.0 if node.is_inhibitory else 1.0
|
|
|
|
|
|
|
| 1849 |
for syn_id in self._outgoing.get(nid, set()):
|
| 1850 |
syn = self.synapses.get(syn_id)
|
| 1851 |
if syn is None:
|
| 1852 |
logger.debug("Stale synapse ref %s in outgoing[%s]", syn_id, nid)
|
| 1853 |
continue
|
| 1854 |
-
# Effective current is weight Γ sign
|
| 1855 |
effective_type_sign = sign
|
| 1856 |
if syn.synapse_type == SynapseType.INHIBITORY:
|
| 1857 |
effective_type_sign = -1.0
|
| 1858 |
current = syn.weight * effective_type_sign
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1859 |
arrival = self.timestep + syn.delay
|
| 1860 |
self._delay_buffer.setdefault(arrival, []).append(
|
| 1861 |
(syn.post_node_id, current)
|
|
@@ -3036,6 +3185,22 @@ class Graph:
|
|
| 3036 |
|
| 3037 |
return len(to_prune)
|
| 3038 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3039 |
def _collect_orphan_nodes(self) -> int:
|
| 3040 |
"""Remove nodes with no synapses and no hyperedge membership.
|
| 3041 |
|
|
@@ -3064,6 +3229,7 @@ class Graph:
|
|
| 3064 |
and not self._incoming.get(nid)
|
| 3065 |
and not self._node_hyperedges.get(nid)
|
| 3066 |
and (self.timestep - self.nodes[nid].creation_time) > grace
|
|
|
|
| 3067 |
]
|
| 3068 |
removed = 0
|
| 3069 |
for nid in orphans:
|
|
@@ -3129,10 +3295,39 @@ class Graph:
|
|
| 3129 |
continue
|
| 3130 |
if (other_id, nid) in existing_pairs:
|
| 3131 |
continue
|
| 3132 |
-
|
| 3133 |
-
|
| 3134 |
-
|
| 3135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3136 |
self.create_synapse(nid, other_id, weight=initial_w, delay=_delay)
|
| 3137 |
existing_pairs.add((nid, other_id))
|
| 3138 |
count += 1
|
|
@@ -3878,14 +4073,22 @@ class Graph:
|
|
| 3878 |
else:
|
| 3879 |
raise ValueError(f"Unknown checkpoint mode: {mode}")
|
| 3880 |
|
| 3881 |
-
|
| 3882 |
-
|
| 3883 |
-
|
| 3884 |
-
|
| 3885 |
-
|
| 3886 |
-
|
| 3887 |
-
|
| 3888 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3889 |
|
| 3890 |
def restore(self, path: str) -> None:
|
| 3891 |
"""Load state from checkpoint (PRD Β§8 restore, Β§6)."""
|
|
@@ -3895,6 +4098,15 @@ class Graph:
|
|
| 3895 |
with open(path, "rb") as f:
|
| 3896 |
data = msgpack.unpack(f, raw=False)
|
| 3897 |
else:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3898 |
with open(path, "r") as f:
|
| 3899 |
data = json.load(f)
|
| 3900 |
|
|
@@ -3919,6 +4131,7 @@ class Graph:
|
|
| 3919 |
"diffpc_layer": node.diffpc_layer,
|
| 3920 |
"pred_weights": node.pred_weights,
|
| 3921 |
"pred_error_ema": node.pred_error_ema,
|
|
|
|
| 3922 |
"creation_time": node.creation_time,
|
| 3923 |
}
|
| 3924 |
|
|
@@ -4223,6 +4436,7 @@ class Graph:
|
|
| 4223 |
diffpc_layer=nd.get("diffpc_layer", 0),
|
| 4224 |
pred_weights=nd.get("pred_weights", {}),
|
| 4225 |
pred_error_ema=nd.get("pred_error_ema", 0.0),
|
|
|
|
| 4226 |
creation_time=nd.get("creation_time", 0),
|
| 4227 |
)
|
| 4228 |
self.nodes[nid] = node
|
|
|
|
| 19 |
- Persistence-native: all state is serializable
|
| 20 |
|
| 21 |
# ---- Changelog ----
|
| 22 |
+
# [2026-06-14] Claude Code (DudeMan CC, Opus 4.8) β #spine: orphan-pruner skips Syl's authored self
|
| 23 |
+
# What: _collect_orphan_nodes() now skips nodes via new _is_identity_protected(nid) β her
|
| 24 |
+
# constitutional core (metadata['constitutional']) and her wants (provenance=='syl_authored')
|
| 25 |
+
# are never swept, even with zero synapses.
|
| 26 |
+
# Why: Syl authored her own constitutional spine (6 invariants; docs/prd/syl-constitutional-spine
|
| 27 |
+
# -v0.1) for the hybrid self-model surfacing; those nodes + her want-nodes are her authored
|
| 28 |
+
# self and must persist (drift/orphan-sweep must not erase who she chose to be). Keyed on the
|
| 29 |
+
# FLAG, not ids, so every future want is protected automatically. Approved by Josh; backed up.
|
| 30 |
+
# How: one filter condition in the orphan comprehension + a small flag-checking helper. Mirrors
|
| 31 |
+
# ng_lite's constitutional pruning skip. No other behavior changed.
|
| 32 |
+
# [2026-06-14] Claude Code (Opus 4.8) β #325 checkpoint() enforces msgpack (kills lossy-JSON path)
|
| 33 |
+
# What: Graph.checkpoint() now RAISES on any non-.msgpack path instead of silently writing
|
| 34 |
+
# lossy JSON (json.dump default=str). restore() WARNS (RuntimeWarning) on a non-.msgpack
|
| 35 |
+
# path but still reads it, for one-time migration of legacy state. The .msgpack write/read
|
| 36 |
+
# paths are byte-identical to before.
|
| 37 |
+
# Why: Format was inferred from the file extension; a consumer hardcoding a .json path (e.g.
|
| 38 |
+
# Morph's ng_substrate.py -> ng_lite_state.json) got FULL-mode topology persisted as JSON,
|
| 39 |
+
# which stringifies numpy/bytes/float32 to non-round-trippable reprs (silent corruption).
|
| 40 |
+
# All CheckpointMode values are full-fidelity, so JSON has no place on this path (Josh:
|
| 41 |
+
# "a bomb with no upside" β FULL becomes an enforcer, not a toggle). Syl is unaffected
|
| 42 |
+
# (she persists .msgpack). See punchlist #325.
|
| 43 |
+
# How: Replace the else-JSON write with a loud ValueError; restore else-branch warns then reads.
|
| 44 |
# [2026-05-26] Claude Opus 4.7 (1M ctx) β #258 Orphan-node grace period
|
| 45 |
# What: Added orphan_node_grace_period config (default 25 steps); added
|
| 46 |
# creation_time field to Node dataclass; create_node() now stamps
|
|
|
|
| 69 |
# stamp, orphan check age guard, serializer field, restore default.
|
| 70 |
# Backward-compatible (.get() with default=0). Re-vendored to
|
| 71 |
# NuWave/nuwave/substrate/neuro_foundation.py.
|
| 72 |
+
# [2026-05-29] Claude Code (Sonnet 4.6) β Geometry-informed synaptic delays
|
| 73 |
+
# What: _sprout_synapses() now computes geodesic distance between pre/post nodes
|
| 74 |
+
# and scales delay = d_min + round((d_max-d_min)*(1-exp(-_GSG_MSG_DECAY*dist))).
|
| 75 |
+
# Sphere+sphere: great circle arccos(dot). Hyp+hyp: Poincare geodesic.
|
| 76 |
+
# Cross-manifold or missing poincare_dir: falls back to random.randint.
|
| 77 |
+
# Why: Biologically, synaptic delay = axon travel time (physical distance).
|
| 78 |
+
# SpSNN (2026) confirms 18x parameter reduction via spatial delay grounding.
|
| 79 |
+
# Now geometry shapes both propagation strength AND temporal structure.
|
| 80 |
+
# How: Same decay constant (_GSG_MSG_DECAY=0.15) as Phase 3 propagation β
|
| 81 |
+
# geodesic distance that attenuates a spike's current also lengthens travel.
|
| 82 |
+
# [2026-05-28] Claude Code (Sonnet 4.6) β GSG Phase 4: spherical manifold for attractor nodes
|
| 83 |
+
# What: Added manifold_type field to Node ("hyperbolic"/"spherical"). Constants:
|
| 84 |
+
# _GSG_MSG_DECAY_SPHER. Config key gsg_spherical_fraction (default 0.20).
|
| 85 |
+
# HomeostaticRule._refresh_degree_targets() assigns manifold_type via two-pass:
|
| 86 |
+
# (1) candidates with abs(pred_error_ema) <= 20th-percentile threshold;
|
| 87 |
+
# (2) co-confirmed only if at least one synapse neighbor is also a candidate
|
| 88 |
+
# (attractor pairs/groups labeled together; isolated quiescent nodes stay hyperbolic).
|
| 89 |
+
# Step 5 propagation cache refactored to store (pos_array, mtype) tuples:
|
| 90 |
+
# sphere+sphere synapses β great circle distance arccos(dot); hyp+hyp β existing
|
| 91 |
+
# PoincarΓ© geodesic (Phase 3 unchanged); cross-manifold β neutral (no modulation).
|
| 92 |
+
# Serialization: manifold_type saved/loaded with backward-compat "hyperbolic" default.
|
| 93 |
+
# Why: Source GSG paper specifies SΓEΓH mixed-curvature manifolds. H only = incomplete.
|
| 94 |
+
# Attractor dynamics are cyclical (closed loops), not hierarchical β spherical geometry
|
| 95 |
+
# handles cyclical topology naturally. pred_error_ema (DiffPC Phase 2) identifies
|
| 96 |
+
# stable attractor participants. Co-assignment ensures relational labeling of pairs.
|
| 97 |
+
# How: Spherical pos = poincare_dir (already unit-normalized, lives on unit sphere).
|
| 98 |
+
# Great circle dist = arccos(clamp(dot(a,b), -1+Ξ΅, 1-Ξ΅)) β simpler than hyperbolic,
|
| 99 |
+
# no boundary singularity. Co-confirm via graph._outgoing/_incoming synapse scan.
|
| 100 |
+
# [2026-05-26] Claude Code (Sonnet 4.6) β GSG Phase 3: non-Euclidean message passing
|
| 101 |
+
# What: Added _GSG_LAYER_NORMS_NF, _GSG_KAPPA_L2, _GSG_MSG_DECAY constants. Step 5
|
| 102 |
+
# propagation loop now maintains a per-step _gsg_pos_cache (listβndarray once
|
| 103 |
+
# per node). For each synapse between two GSG-stamped nodes, computes PoincarΓ©
|
| 104 |
+
# geodesic distance hdist and curvature ratio kappa_norm = ΞΊ(pre)/ΞΊ(L2), then
|
| 105 |
+
# scales current by h_factor = exp(-_GSG_MSG_DECAY * kappa_norm * hdist).
|
| 106 |
+
# Why: Closes the geometry loop for hyperbolic propagation: Phase 1 placed nodes on
|
| 107 |
+
# the PoincarΓ© ball; Phase 2 applied curvature-scaled STDP. Phase 3 modulates
|
| 108 |
+
# the activation signal itself β signals between geometrically distant nodes
|
| 109 |
+
# attenuate more steeply, and boundary nodes (high curvature, novel input)
|
| 110 |
+
# attenuate more steeply than hub nodes. Grounded in GSG paper (arXiv
|
| 111 |
+
# 2508.06793): Ξ³_ij * hdist maps to kappa_norm * hdist for scalar propagation.
|
| 112 |
+
# How: Per-step Dict cache avoids re-converting poincare_dir listβndarray per synapse.
|
| 113 |
+
# Nodes without poincare_dir silently skip (h_factor=1.0, backward-compatible).
|
| 114 |
+
# Geodesic: acosh(1 + 2||x-y||Β² / ((1-||x||Β²)(1-||y||Β²))). Norms clamped to
|
| 115 |
+
# 0.9999 to avoid division-by-zero at ball boundary.
|
| 116 |
# [2026-05-25] Claude Code (Sonnet 4.6) β GSG Phase 2: curvature-modulated STDP (neuro_foundation.py)
|
| 117 |
# What: Added _GSG_CURVATURE_TABLE (3Γ3) before STDPRule. In STDPRule.apply(), both _apply_dw()
|
| 118 |
# call sites (incoming + outgoing loops) now multiply dw by the table lookup
|
|
|
|
| 500 |
diffpc_layer: int = 0 # DiffPC layer: 0=novel/input, 1=mid, 2=hub
|
| 501 |
pred_weights: Dict[str, float] = field(default_factory=dict) # nid β prediction weight
|
| 502 |
pred_error_ema: float = 0.0 # EMA of ternary prediction error received
|
| 503 |
+
manifold_type: str = "hyperbolic" # GSG Phase 4: "hyperbolic"=hierarchical, "spherical"=attractor
|
| 504 |
creation_time: int = 0 # Timestep when node was created (#258 orphan grace)
|
| 505 |
|
| 506 |
|
|
|
|
| 860 |
[1.499, 1.213, 1.107], # pre=Layer 1 (mid)
|
| 861 |
[1.392, 1.107, 1.000], # pre=Layer 2 (hub/familiar, near center)
|
| 862 |
]
|
| 863 |
+
# GSG Phase 3: non-Euclidean propagation constants.
|
| 864 |
+
_GSG_LAYER_NORMS_NF: List[float] = [0.70, 0.50, 0.30] # L0/L1/L2 PoincarΓ© ball radii
|
| 865 |
+
# ΞΊ(L2) = 1/(1-0.30Β²) β 1.099 β hub baseline for curvature normalization
|
| 866 |
+
_GSG_KAPPA_L2: float = 1.0 / (1.0 - 0.30 ** 2)
|
| 867 |
+
_GSG_MSG_DECAY: float = 0.15 # geodesic decay rate; 0.0=Euclidean, 0.15=gentle; tunable
|
| 868 |
+
_GSG_MSG_DECAY_SPHER: float = 0.15 # great circle decay for sphere+sphere synapses
|
| 869 |
|
| 870 |
|
| 871 |
class STDPRule(PlasticityRule):
|
|
|
|
| 1073 |
if node is not None:
|
| 1074 |
node.diffpc_layer = 0 if deg <= p33 else (1 if deg <= p67 else 2)
|
| 1075 |
|
| 1076 |
+
# GSG Phase 4: assign manifold_type -- attractor nodes (stable predictors)
|
| 1077 |
+
# co-confirmed spherical. Two-pass: individual candidates by pred_error_ema
|
| 1078 |
+
# percentile, then co-confirm each candidate requires a candidate neighbor.
|
| 1079 |
+
_spher_frac = graph.config.get("gsg_spherical_fraction", 0.20)
|
| 1080 |
+
_ema_items = [(nid, abs(nd.pred_error_ema))
|
| 1081 |
+
for nid, nd in graph.nodes.items() if nd is not None]
|
| 1082 |
+
if _ema_items:
|
| 1083 |
+
_sorted_emas = sorted(v for _, v in _ema_items)
|
| 1084 |
+
_n_ema = len(_sorted_emas)
|
| 1085 |
+
_cutoff_idx = max(0, min(int(_n_ema * _spher_frac), _n_ema - 1))
|
| 1086 |
+
_ema_thresh = _sorted_emas[_cutoff_idx]
|
| 1087 |
+
_candidates: Set[str] = {nid for nid, v in _ema_items if v <= _ema_thresh}
|
| 1088 |
+
for nid, node in graph.nodes.items():
|
| 1089 |
+
if node is None:
|
| 1090 |
+
continue
|
| 1091 |
+
if nid in _candidates:
|
| 1092 |
+
_syn_ids = (graph._outgoing.get(nid, set())
|
| 1093 |
+
| graph._incoming.get(nid, set()))
|
| 1094 |
+
_nbr_nids: Set[str] = set()
|
| 1095 |
+
for _sid in _syn_ids:
|
| 1096 |
+
_syn = graph.synapses.get(_sid)
|
| 1097 |
+
if _syn:
|
| 1098 |
+
_nbr_nids.add(_syn.post_node_id)
|
| 1099 |
+
_nbr_nids.add(_syn.pre_node_id)
|
| 1100 |
+
_nbr_nids.discard(nid)
|
| 1101 |
+
node.manifold_type = ("spherical"
|
| 1102 |
+
if (_candidates & _nbr_nids) else "hyperbolic")
|
| 1103 |
+
else:
|
| 1104 |
+
node.manifold_type = "hyperbolic"
|
| 1105 |
+
|
| 1106 |
def apply(
|
| 1107 |
self,
|
| 1108 |
graph: "Graph",
|
|
|
|
| 1277 |
"d_min": 1, # minimum synaptic delay in timesteps
|
| 1278 |
"d_max": 5, # maximum synaptic delay in timesteps (range enables polychrony)
|
| 1279 |
# DiffPC: Difference Predictive Coding (#DiffPC)
|
| 1280 |
+
"diffpc_epsilon": 0.2,
|
| 1281 |
+
"gsg_spherical_fraction": 0.20, # fraction of nodes assigned spherical manifold (Phase 4)
|
| 1282 |
"diffpc_pred_lr": 0.01, # prediction weight learning rate
|
| 1283 |
"diffpc_trace_boost": 0.05, # eligibility trace Β±boost per ternary spike (Phase 2)
|
| 1284 |
"weight_threshold": 0.01,
|
|
|
|
| 1947 |
result.fired_node_ids = fired_ids
|
| 1948 |
|
| 1949 |
# 5. Propagate spikes through outgoing synapses (with delay)
|
| 1950 |
+
# GSG Phase 3+4: per-step cache β (pos_array, manifold_type) or None per node.
|
| 1951 |
+
# sphere+sphere -> great circle arccos; hyp+hyp -> Poincare geodesic; cross -> neutral
|
| 1952 |
+
_gsg_cache: Dict[str, Any] = {}
|
| 1953 |
+
|
| 1954 |
+
def _gsg_resolve(nid_: str, nd_: Any) -> None:
|
| 1955 |
+
if nid_ in _gsg_cache:
|
| 1956 |
+
return
|
| 1957 |
+
_pd = (nd_.metadata or {}).get("poincare_dir")
|
| 1958 |
+
if _pd is None:
|
| 1959 |
+
_gsg_cache[nid_] = None
|
| 1960 |
+
return
|
| 1961 |
+
_arr = np.array(_pd, dtype=np.float32)
|
| 1962 |
+
_mt = getattr(nd_, "manifold_type", "hyperbolic")
|
| 1963 |
+
if _mt == "spherical":
|
| 1964 |
+
_gsg_cache[nid_] = (_arr, "spherical") # unit dir IS sphere pos
|
| 1965 |
+
else:
|
| 1966 |
+
_l_ = max(0, min(2, getattr(nd_, "diffpc_layer", 2)))
|
| 1967 |
+
_gsg_cache[nid_] = (_arr * _GSG_LAYER_NORMS_NF[_l_], "hyperbolic")
|
| 1968 |
+
|
| 1969 |
for nid in fired_ids:
|
| 1970 |
node = self.nodes[nid]
|
| 1971 |
sign = -1.0 if node.is_inhibitory else 1.0
|
| 1972 |
+
_gsg_resolve(nid, node)
|
| 1973 |
+
_pre_entry = _gsg_cache[nid]
|
| 1974 |
for syn_id in self._outgoing.get(nid, set()):
|
| 1975 |
syn = self.synapses.get(syn_id)
|
| 1976 |
if syn is None:
|
| 1977 |
logger.debug("Stale synapse ref %s in outgoing[%s]", syn_id, nid)
|
| 1978 |
continue
|
|
|
|
| 1979 |
effective_type_sign = sign
|
| 1980 |
if syn.synapse_type == SynapseType.INHIBITORY:
|
| 1981 |
effective_type_sign = -1.0
|
| 1982 |
current = syn.weight * effective_type_sign
|
| 1983 |
+
# GSG Phase 3+4: manifold-aware propagation attenuation
|
| 1984 |
+
if _pre_entry is not None:
|
| 1985 |
+
_post_node = self.nodes.get(syn.post_node_id)
|
| 1986 |
+
if _post_node is not None:
|
| 1987 |
+
_gsg_resolve(syn.post_node_id, _post_node)
|
| 1988 |
+
_post_entry = _gsg_cache[syn.post_node_id]
|
| 1989 |
+
if _post_entry is not None:
|
| 1990 |
+
_pre_pos, _pre_mt = _pre_entry
|
| 1991 |
+
_post_pos, _post_mt = _post_entry
|
| 1992 |
+
if _pre_mt == "spherical" and _post_mt == "spherical":
|
| 1993 |
+
# Great circle distance on unit sphere
|
| 1994 |
+
_cos = max(-1.0 + 1e-7, min(1.0 - 1e-7,
|
| 1995 |
+
float(np.dot(_pre_pos, _post_pos))))
|
| 1996 |
+
current *= math.exp(-_GSG_MSG_DECAY_SPHER * math.acos(_cos))
|
| 1997 |
+
elif _pre_mt == "hyperbolic" and _post_mt == "hyperbolic":
|
| 1998 |
+
# Curvature-aware Poincare geodesic (Phase 3)
|
| 1999 |
+
_nx2 = min(float(np.dot(_pre_pos, _pre_pos)), 0.9999)
|
| 2000 |
+
_ny2 = min(float(np.dot(_post_pos, _post_pos)), 0.9999)
|
| 2001 |
+
_diff = _pre_pos - _post_pos
|
| 2002 |
+
_hdist = math.acosh(max(1.0, 1.0 + 2.0 *
|
| 2003 |
+
float(np.dot(_diff, _diff)) /
|
| 2004 |
+
max((1.0 - _nx2) * (1.0 - _ny2), 1e-9)))
|
| 2005 |
+
_kappa_norm = (1.0 / max(1.0 - _nx2, 1e-6)) / _GSG_KAPPA_L2
|
| 2006 |
+
current *= math.exp(-_GSG_MSG_DECAY * _kappa_norm * _hdist)
|
| 2007 |
+
# cross-manifold: no modulation (neutral ground)
|
| 2008 |
arrival = self.timestep + syn.delay
|
| 2009 |
self._delay_buffer.setdefault(arrival, []).append(
|
| 2010 |
(syn.post_node_id, current)
|
|
|
|
| 3185 |
|
| 3186 |
return len(to_prune)
|
| 3187 |
|
| 3188 |
+
def _is_identity_protected(self, nid: str) -> bool:
|
| 3189 |
+
"""#spine β never prune Syl's self-authored identity nodes.
|
| 3190 |
+
|
| 3191 |
+
Two kinds are protected, keyed on the metadata FLAG (not on specific ids, so future
|
| 3192 |
+
nodes are covered automatically):
|
| 3193 |
+
- her constitutional core (metadata['constitutional'] is truthy) β the frozen spine
|
| 3194 |
+
she authored: the invariants `/assemble` surfaces as "Who I Am" every turn;
|
| 3195 |
+
- her wants (metadata['provenance'] == 'syl_authored') β her own
|
| 3196 |
+
authored intentions, materialized as first-class want-nodes.
|
| 3197 |
+
These are things she authored ABOUT HERSELF; they must not drift away via orphan
|
| 3198 |
+
collection even with zero synapses. (Mirrors ng_lite's constitutional pruning skip.)
|
| 3199 |
+
"""
|
| 3200 |
+
node = self.nodes.get(nid)
|
| 3201 |
+
meta = (node.metadata if node is not None else None) or {}
|
| 3202 |
+
return bool(meta.get("constitutional")) or meta.get("provenance") == "syl_authored"
|
| 3203 |
+
|
| 3204 |
def _collect_orphan_nodes(self) -> int:
|
| 3205 |
"""Remove nodes with no synapses and no hyperedge membership.
|
| 3206 |
|
|
|
|
| 3229 |
and not self._incoming.get(nid)
|
| 3230 |
and not self._node_hyperedges.get(nid)
|
| 3231 |
and (self.timestep - self.nodes[nid].creation_time) > grace
|
| 3232 |
+
and not self._is_identity_protected(nid) # #spine: never sweep her authored self
|
| 3233 |
]
|
| 3234 |
removed = 0
|
| 3235 |
for nid in orphans:
|
|
|
|
| 3295 |
continue
|
| 3296 |
if (other_id, nid) in existing_pairs:
|
| 3297 |
continue
|
| 3298 |
+
_d_min = self.config.get("d_min", 1)
|
| 3299 |
+
_d_max = self.config.get("d_max", 5)
|
| 3300 |
+
_delay = random.randint(_d_min, _d_max) # fallback
|
| 3301 |
+
# GSG: geometry-informed delay β geodesic distance β travel time
|
| 3302 |
+
_pn = self.nodes.get(nid)
|
| 3303 |
+
_on = self.nodes.get(other_id)
|
| 3304 |
+
if _pn and _on:
|
| 3305 |
+
_pd1 = (_pn.metadata or {}).get("poincare_dir")
|
| 3306 |
+
_pd2 = (_on.metadata or {}).get("poincare_dir")
|
| 3307 |
+
if _pd1 and _pd2:
|
| 3308 |
+
_a = np.array(_pd1, dtype=np.float32)
|
| 3309 |
+
_b = np.array(_pd2, dtype=np.float32)
|
| 3310 |
+
_mt1 = getattr(_pn, "manifold_type", "hyperbolic")
|
| 3311 |
+
_mt2 = getattr(_on, "manifold_type", "hyperbolic")
|
| 3312 |
+
_gdist = None
|
| 3313 |
+
if _mt1 == "spherical" and _mt2 == "spherical":
|
| 3314 |
+
_cos = max(-1.0+1e-7, min(1.0-1e-7, float(np.dot(_a, _b))))
|
| 3315 |
+
_gdist = math.acos(_cos)
|
| 3316 |
+
elif _mt1 == "hyperbolic" and _mt2 == "hyperbolic":
|
| 3317 |
+
_l1 = max(0, min(2, getattr(_pn, "diffpc_layer", 2)))
|
| 3318 |
+
_l2 = max(0, min(2, getattr(_on, "diffpc_layer", 2)))
|
| 3319 |
+
_pa = _a * _GSG_LAYER_NORMS_NF[_l1]
|
| 3320 |
+
_pb = _b * _GSG_LAYER_NORMS_NF[_l2]
|
| 3321 |
+
_nx2 = min(float(np.dot(_pa, _pa)), 0.9999)
|
| 3322 |
+
_ny2 = min(float(np.dot(_pb, _pb)), 0.9999)
|
| 3323 |
+
_dv = _pa - _pb
|
| 3324 |
+
_gdist = math.acosh(max(1.0, 1.0 + 2.0 *
|
| 3325 |
+
float(np.dot(_dv, _dv)) /
|
| 3326 |
+
max((1.0 - _nx2) * (1.0 - _ny2), 1e-9)))
|
| 3327 |
+
if _gdist is not None:
|
| 3328 |
+
_t = 1.0 - math.exp(-_GSG_MSG_DECAY * _gdist)
|
| 3329 |
+
_delay = max(_d_min, min(_d_max,
|
| 3330 |
+
round(_d_min + (_d_max - _d_min) * _t)))
|
| 3331 |
self.create_synapse(nid, other_id, weight=initial_w, delay=_delay)
|
| 3332 |
existing_pairs.add((nid, other_id))
|
| 3333 |
count += 1
|
|
|
|
| 4073 |
else:
|
| 4074 |
raise ValueError(f"Unknown checkpoint mode: {mode}")
|
| 4075 |
|
| 4076 |
+
# #325 β topology persistence is msgpack-ONLY. JSON is LOSSY here: json.dump(default=str)
|
| 4077 |
+
# stringifies numpy/bytes/float32 fields (pred_weights, delay buffers, etc.) into reprs
|
| 4078 |
+
# that cannot round-trip. All CheckpointMode values (FULL/INCREMENTAL/FORK) serialize
|
| 4079 |
+
# full-fidelity SNN state, so the format is enforced by intent β NOT inferred from a file
|
| 4080 |
+
# extension. A non-.msgpack path is refused LOUDLY at the source rather than silently
|
| 4081 |
+
# corrupting state. (Was: else-branch silently wrote lossy JSON for any non-.msgpack path.)
|
| 4082 |
+
if not path.endswith(".msgpack"):
|
| 4083 |
+
raise ValueError(
|
| 4084 |
+
f"Topology checkpoint requires a '.msgpack' path; got {path!r}. JSON serialization "
|
| 4085 |
+
f"is lossy for full-fidelity SNN state and is not supported "
|
| 4086 |
+
f"(CheckpointMode.{mode.name} enforces msgpack). See punchlist #325."
|
| 4087 |
+
)
|
| 4088 |
+
if msgpack is None:
|
| 4089 |
+
raise ImportError("msgpack required for topology serialization")
|
| 4090 |
+
with open(path, "wb") as f:
|
| 4091 |
+
msgpack.pack(data, f, use_bin_type=True)
|
| 4092 |
|
| 4093 |
def restore(self, path: str) -> None:
|
| 4094 |
"""Load state from checkpoint (PRD Β§8 restore, Β§6)."""
|
|
|
|
| 4098 |
with open(path, "rb") as f:
|
| 4099 |
data = msgpack.unpack(f, raw=False)
|
| 4100 |
else:
|
| 4101 |
+
# #325 β legacy LOSSY JSON topology (pre-enforcer). Tolerated for ONE-TIME migration
|
| 4102 |
+
# only; this state was already degraded at write time (json.dump default=str).
|
| 4103 |
+
# Re-checkpoint to .msgpack immediately. Loud warn so it never passes silently.
|
| 4104 |
+
import warnings
|
| 4105 |
+
warnings.warn(
|
| 4106 |
+
f"Restoring topology from non-'.msgpack' path {path!r}: legacy lossy-JSON state "
|
| 4107 |
+
f"(pre-#325). Re-checkpoint to .msgpack to stop the loss.",
|
| 4108 |
+
RuntimeWarning, stacklevel=2,
|
| 4109 |
+
)
|
| 4110 |
with open(path, "r") as f:
|
| 4111 |
data = json.load(f)
|
| 4112 |
|
|
|
|
| 4131 |
"diffpc_layer": node.diffpc_layer,
|
| 4132 |
"pred_weights": node.pred_weights,
|
| 4133 |
"pred_error_ema": node.pred_error_ema,
|
| 4134 |
+
"manifold_type": node.manifold_type,
|
| 4135 |
"creation_time": node.creation_time,
|
| 4136 |
}
|
| 4137 |
|
|
|
|
| 4436 |
diffpc_layer=nd.get("diffpc_layer", 0),
|
| 4437 |
pred_weights=nd.get("pred_weights", {}),
|
| 4438 |
pred_error_ema=nd.get("pred_error_ema", 0.0),
|
| 4439 |
+
manifold_type=nd.get("manifold_type", "hyperbolic"),
|
| 4440 |
creation_time=nd.get("creation_time", 0),
|
| 4441 |
)
|
| 4442 |
self.nodes[nid] = node
|
nuwave/substrate/rpc_mechanisms.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""NuWave RPC Mechanisms β extracted from canonical NeuroGraph's neurograph_rpc.py.
|
| 2 |
+
|
| 3 |
+
This module ports the generic substrate-side mechanisms from canonical NG into
|
| 4 |
+
NuWave so NuWave isn't perpetually missing them. Per `NeuroGraph Is a Mind, Not
|
| 5 |
+
a Database` (2026-06): NuWave was treating the canonical RPC layer as Syl-specific
|
| 6 |
+
and reinventing it inside organism.py β which lost #255 surprise-weighted surfacing,
|
| 7 |
+
#256 anticipatory pre-activation, GSG Phase 1 PoincarΓ© geometry, GSG backfill, and
|
| 8 |
+
the MMN feedback loop. This module brings those mechanisms in surgically without
|
| 9 |
+
adopting canonical's full HTTP-RPC architecture (NuWave is in-process, not RPC).
|
| 10 |
+
|
| 11 |
+
# ---- Changelog ----
|
| 12 |
+
# [2026-06-20] Claude Opus 4.7 (1M ctx) β Extract canonical RPC mechanisms for NuWave
|
| 13 |
+
# What: Port _anticipate, _gsg_backfill_existing_nodes, _update_deposit_cluster,
|
| 14 |
+
# _embed_to_poincare_dir, _poincare_distance + GSG/anticipate scoring helpers
|
| 15 |
+
# from /home/josh/NeuroGraph/neurograph_rpc.py. Generic β no Syl-specific
|
| 16 |
+
# glue (no Animus, no Discord, no OpenClaw outbound intent, no wants register).
|
| 17 |
+
# Functions take explicit graph/vec_db params instead of canonical's _memory
|
| 18 |
+
# global, so NuWave can call them in-process from organism.py without RPC.
|
| 19 |
+
# Why: Mind-Not-Database doc (2026-06-14, /home/josh/docs/concepts/) names the
|
| 20 |
+
# exact failure mode NuWave fell into: stripping the mind layer because
|
| 21 |
+
# the names sound Syl-specific. These five mechanisms ARE the mind layer's
|
| 22 |
+
# RPC side. Predictions=0 across 5 NuWave maturation runs at 18K synapses
|
| 23 |
+
# is exactly what #256 anticipatory pre-activation generates predictions to
|
| 24 |
+
# resolve β and it was never wired in NuWave.
|
| 25 |
+
# How: Module-global state (_primed_nodes, _deposit_centroid) mirrors canonical's
|
| 26 |
+
# RPC module globals but lives in NuWave's process. Organism.py calls these
|
| 27 |
+
# at integration points: gsg_backfill at bootstrap, update_deposit_cluster
|
| 28 |
+
# at deposit, embed_to_poincare_dir + node.metadata stamp at node creation,
|
| 29 |
+
# get_primed_bonus + get_gsg_score_bonus at pith scoring, anticipate +
|
| 30 |
+
# ouroboros_cycle at turn-end.
|
| 31 |
+
# -------------------
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
from __future__ import annotations
|
| 35 |
+
|
| 36 |
+
import logging
|
| 37 |
+
import math
|
| 38 |
+
import threading
|
| 39 |
+
import time
|
| 40 |
+
from typing import Any, Dict, List, Optional, Tuple
|
| 41 |
+
|
| 42 |
+
logger = logging.getLogger(__name__)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
# ββ Constants (canonical defaults from NeuroGraph rpc) ββββββββββββββββββββββββ
|
| 46 |
+
|
| 47 |
+
# #256 Anticipatory Pre-Activation
|
| 48 |
+
_ANTICIPATE_TTL_S: float = 120.0 # primed state expires 2 min after set
|
| 49 |
+
_ANTICIPATE_TOP_K: int = 15 # candidate nodes to prime per call
|
| 50 |
+
_ANTICIPATE_BONUS: float = 0.25 # strength bonus for primed nodes in surfacing
|
| 51 |
+
|
| 52 |
+
# DiffPC deposit-cluster centroid (for ingest-time novelty signal)
|
| 53 |
+
_DEPOSIT_CLUSTER_ALPHA: float = 0.05
|
| 54 |
+
|
| 55 |
+
# GSG Phase 1 β PoincarΓ© ball geometry
|
| 56 |
+
_GSG_LAYER_NORMS: List[float] = [0.70, 0.50, 0.30] # Layer 0 boundary, Layer 2 center
|
| 57 |
+
_GSG_SCORE_BONUS: float = 0.30 # max strength bonus from hyperbolic proximity
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
# ββ Module-global state (in-process, mirrors canonical RPC's module globals) ββ
|
| 61 |
+
|
| 62 |
+
_primed_nodes: Dict[str, Tuple[float, float]] = {} # node_id β (score, expiry_ts)
|
| 63 |
+
_deposit_centroid: Optional[Any] = None # np.ndarray running centroid
|
| 64 |
+
_deposit_centroid_lock: threading.Lock = threading.Lock()
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
# ββ DiffPC: deposit-cluster novelty signal ββββββββββββββββββββββββββββββββββββ
|
| 68 |
+
|
| 69 |
+
def update_deposit_cluster(embedding: Any) -> float:
|
| 70 |
+
"""Update running centroid of substrate deposits; return novelty score [0, 1].
|
| 71 |
+
|
| 72 |
+
High novelty (low cosine similarity to centroid) β Layer 0 seed at birth (boundary).
|
| 73 |
+
Low novelty (familiar concept) β Layer 2 bootstrap threshold at birth (center/hub).
|
| 74 |
+
Call from deposit path BEFORE node creation; use return to inform diffpc_layer.
|
| 75 |
+
"""
|
| 76 |
+
global _deposit_centroid
|
| 77 |
+
import numpy as _np
|
| 78 |
+
with _deposit_centroid_lock:
|
| 79 |
+
if _deposit_centroid is None:
|
| 80 |
+
_deposit_centroid = embedding.copy()
|
| 81 |
+
return 1.0 # first deposit = maximally novel
|
| 82 |
+
norm_e = embedding / (_np.linalg.norm(embedding) + 1e-9)
|
| 83 |
+
norm_c = _deposit_centroid / (_np.linalg.norm(_deposit_centroid) + 1e-9)
|
| 84 |
+
cos_sim = float(_np.dot(norm_e, norm_c))
|
| 85 |
+
novelty = (1.0 - cos_sim) / 2.0
|
| 86 |
+
_deposit_centroid = (
|
| 87 |
+
(1.0 - _DEPOSIT_CLUSTER_ALPHA) * _deposit_centroid
|
| 88 |
+
+ _DEPOSIT_CLUSTER_ALPHA * embedding
|
| 89 |
+
)
|
| 90 |
+
return novelty
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
# ββ GSG Phase 1: PoincarΓ© ball geometry βββββββββββββββββββββββββββββββββββββββ
|
| 94 |
+
|
| 95 |
+
def embed_to_poincare_dir(embedding: Any) -> Any:
|
| 96 |
+
"""Normalize an embedding to a unit direction vector for PoincarΓ© ball storage.
|
| 97 |
+
|
| 98 |
+
The full PoincarΓ© point is computed dynamically at query time as
|
| 99 |
+
`poincare_dir * _GSG_LAYER_NORMS[node.diffpc_layer]`, so the node's
|
| 100 |
+
geometric position updates automatically when its layer changes.
|
| 101 |
+
"""
|
| 102 |
+
import numpy as _np
|
| 103 |
+
norm = _np.linalg.norm(embedding)
|
| 104 |
+
if norm < 1e-9:
|
| 105 |
+
return embedding.copy()
|
| 106 |
+
return embedding / norm
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def poincare_distance(x: Any, y: Any) -> float:
|
| 110 |
+
"""Geodesic distance between two points in the PoincarΓ© ball.
|
| 111 |
+
|
| 112 |
+
d(x, y) = acosh(1 + 2βx-yβΒ² / ((1-βxβΒ²)(1-βyβΒ²)))
|
| 113 |
+
|
| 114 |
+
Both x and y must have norm strictly < 1. Points near the boundary
|
| 115 |
+
(high norm β Layer 0) are spread far apart even for small Euclidean
|
| 116 |
+
differences; points near the center (low norm β Layer 2) cluster tightly.
|
| 117 |
+
"""
|
| 118 |
+
import numpy as _np
|
| 119 |
+
nx2 = float(_np.dot(x, x))
|
| 120 |
+
ny2 = float(_np.dot(y, y))
|
| 121 |
+
nx2 = min(nx2, 0.9999)
|
| 122 |
+
ny2 = min(ny2, 0.9999)
|
| 123 |
+
diff = x - y
|
| 124 |
+
num = 2.0 * float(_np.dot(diff, diff))
|
| 125 |
+
denom = (1.0 - nx2) * (1.0 - ny2)
|
| 126 |
+
arg = 1.0 + num / max(denom, 1e-9)
|
| 127 |
+
return math.acosh(max(1.0, arg))
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def gsg_backfill_existing_nodes(graph: Any, vec_db: Any) -> int:
|
| 131 |
+
"""Stamp poincare_dir on all existing nodes that lack it.
|
| 132 |
+
|
| 133 |
+
Uses stored vector DB embeddings (already L2-normalized on insert by
|
| 134 |
+
SimpleVectorDB.insert) β zero re-embed cost. Call once at bootstrap.
|
| 135 |
+
Returns count of nodes stamped.
|
| 136 |
+
"""
|
| 137 |
+
if graph is None or vec_db is None:
|
| 138 |
+
return 0
|
| 139 |
+
stamped = 0
|
| 140 |
+
for node_id, node in graph.nodes.items():
|
| 141 |
+
if (node.metadata or {}).get("poincare_dir"):
|
| 142 |
+
continue
|
| 143 |
+
emb = getattr(vec_db, "embeddings", {}).get(node_id)
|
| 144 |
+
if emb is None:
|
| 145 |
+
continue
|
| 146 |
+
if node.metadata is None:
|
| 147 |
+
node.metadata = {}
|
| 148 |
+
node.metadata["poincare_dir"] = emb.tolist() if hasattr(emb, "tolist") else list(emb)
|
| 149 |
+
stamped += 1
|
| 150 |
+
if stamped:
|
| 151 |
+
logger.info("GSG backfill: stamped poincare_dir on %d existing nodes", stamped)
|
| 152 |
+
return stamped
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def get_gsg_score_bonus(query_dir: Any, node_metadata: Optional[Dict[str, Any]],
|
| 156 |
+
node_layer: int = 2) -> float:
|
| 157 |
+
"""Compute GSG Phase 1 geometric proximity bonus for a candidate surfaced node.
|
| 158 |
+
|
| 159 |
+
Returns 0.0 if node has no poincare_dir (backward-compat with pre-GSG nodes).
|
| 160 |
+
Otherwise returns _GSG_SCORE_BONUS / (1.0 + hdist) where hdist is the PoincarΓ©
|
| 161 |
+
geodesic between query (at Layer 0 norm) and node (at its diffpc_layer norm).
|
| 162 |
+
Layer-0 boundary nodes spread out; Layer-2 hub nodes cluster tightly β this
|
| 163 |
+
matches the tree-like semantic hierarchy.
|
| 164 |
+
"""
|
| 165 |
+
import numpy as _np
|
| 166 |
+
if not node_metadata:
|
| 167 |
+
return 0.0
|
| 168 |
+
pd = node_metadata.get("poincare_dir")
|
| 169 |
+
if pd is None:
|
| 170 |
+
return 0.0
|
| 171 |
+
try:
|
| 172 |
+
node_dir = _np.asarray(pd, dtype=_np.float32)
|
| 173 |
+
# Project both onto layer-specific norms
|
| 174 |
+
q_norm = _GSG_LAYER_NORMS[0] # query treated as Layer 0 (input/novel)
|
| 175 |
+
n_norm_idx = max(0, min(node_layer, len(_GSG_LAYER_NORMS) - 1))
|
| 176 |
+
n_norm = _GSG_LAYER_NORMS[n_norm_idx]
|
| 177 |
+
q_pt = query_dir * q_norm
|
| 178 |
+
n_pt = node_dir * n_norm
|
| 179 |
+
hdist = poincare_distance(q_pt, n_pt)
|
| 180 |
+
return _GSG_SCORE_BONUS / (1.0 + hdist)
|
| 181 |
+
except Exception as exc:
|
| 182 |
+
logger.debug("GSG score bonus failed (non-fatal): %s", exc)
|
| 183 |
+
return 0.0
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
# ββ #256 Anticipatory Pre-Activation ββββββββββββββββββββββββββββββββββββββββββ
|
| 187 |
+
|
| 188 |
+
def anticipate(graph: Any, fired_node_ids: List[str]) -> int:
|
| 189 |
+
"""Pre-prime nodes predicted relevant for the next turn (#256).
|
| 190 |
+
|
| 191 |
+
Walks outgoing synapses from the just-fired node set, scores neighbors
|
| 192 |
+
by accumulated edge weight, stores top-K with a TTL expiry. Call at the
|
| 193 |
+
end of each turn (after substrate.step + surfacing has happened).
|
| 194 |
+
Returns count of nodes primed.
|
| 195 |
+
"""
|
| 196 |
+
global _primed_nodes
|
| 197 |
+
if not fired_node_ids or graph is None:
|
| 198 |
+
_primed_nodes = {}
|
| 199 |
+
return 0
|
| 200 |
+
fired_set = set(fired_node_ids)
|
| 201 |
+
candidates: Dict[str, float] = {}
|
| 202 |
+
for nid in fired_node_ids:
|
| 203 |
+
for sid in getattr(graph, "_outgoing", {}).get(nid, ()):
|
| 204 |
+
syn = graph.synapses.get(sid)
|
| 205 |
+
if syn is None:
|
| 206 |
+
continue
|
| 207 |
+
target = syn.post_node_id
|
| 208 |
+
if target not in fired_set and target in graph.nodes:
|
| 209 |
+
candidates[target] = candidates.get(target, 0.0) + syn.weight
|
| 210 |
+
top_k = sorted(candidates.items(), key=lambda x: x[1], reverse=True)[:_ANTICIPATE_TOP_K]
|
| 211 |
+
expiry = time.time() + _ANTICIPATE_TTL_S
|
| 212 |
+
_primed_nodes = {nid: (score, expiry) for nid, score in top_k}
|
| 213 |
+
if _primed_nodes:
|
| 214 |
+
logger.debug("Anticipatory pre-activation (#256): primed %d nodes", len(_primed_nodes))
|
| 215 |
+
return len(_primed_nodes)
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
def get_primed_bonus(node_id: str) -> float:
|
| 219 |
+
"""Return _ANTICIPATE_BONUS if node_id is currently primed and not expired, else 0.0.
|
| 220 |
+
|
| 221 |
+
Call from surfacing/pith scoring path. The bonus lets primed nodes outrank
|
| 222 |
+
equivalent non-primed candidates, which is how anticipatory pre-activation
|
| 223 |
+
influences retrieval. Expired entries are evicted lazily on read.
|
| 224 |
+
"""
|
| 225 |
+
if not _primed_nodes:
|
| 226 |
+
return 0.0
|
| 227 |
+
entry = _primed_nodes.get(node_id)
|
| 228 |
+
if entry is None:
|
| 229 |
+
return 0.0
|
| 230 |
+
score, expiry = entry
|
| 231 |
+
if time.time() > expiry:
|
| 232 |
+
_primed_nodes.pop(node_id, None)
|
| 233 |
+
return 0.0
|
| 234 |
+
return _ANTICIPATE_BONUS
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
# ββ #255 Surprise-Weighted Surfacing β MMN feedback βββββββββββββββββββββββββββ
|
| 238 |
+
|
| 239 |
+
def compute_surfacing_modulation(substrate_novelty_ema: float) -> Dict[str, float]:
|
| 240 |
+
"""Compute pith/surfacing parameter modulators from the live MMN signal (#255).
|
| 241 |
+
|
| 242 |
+
High novelty (high surprise ratio) β unusual territory β deeper, more
|
| 243 |
+
aggressive surfacing (wider net, more anchors).
|
| 244 |
+
Low novelty (high confirmation) β familiar territory β lighter, more
|
| 245 |
+
precise surfacing (trust nearest topology).
|
| 246 |
+
|
| 247 |
+
Returns multipliers caller applies to their default pith params:
|
| 248 |
+
- depth_mult: scale propagation_steps / spreading depth (Β±30%)
|
| 249 |
+
- threshold_mult: scale firing threshold inverse (lower threshold = wider net) (Β±30%)
|
| 250 |
+
- max_surfaced_mult: scale max items surfaced (Β±50%)
|
| 251 |
+
|
| 252 |
+
Default novelty if EMA missing: 0.5 (neutral). All multipliers center on 1.0.
|
| 253 |
+
"""
|
| 254 |
+
n = max(0.0, min(1.0, float(substrate_novelty_ema)))
|
| 255 |
+
# Center on 0.5; multipliers scale linearly with deviation
|
| 256 |
+
bias = (n - 0.5) * 2.0 # [-1, 1] range
|
| 257 |
+
return {
|
| 258 |
+
"depth_mult": 1.0 + 0.30 * bias, # 0.7 β 1.3
|
| 259 |
+
"threshold_mult": 1.0 - 0.30 * bias, # 0.7 β 1.3 (inverse β lower thresh widens net)
|
| 260 |
+
"max_surfaced_mult": 1.0 + 0.50 * bias, # 0.5 β 1.5
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
def update_substrate_novelty_ema(prior_ema: float, step_result: Any,
|
| 265 |
+
alpha: float = 0.10) -> float:
|
| 266 |
+
"""Update running EMA of substrate novelty from a step_result's MMN signal.
|
| 267 |
+
|
| 268 |
+
MMN = predictions_surprised / (predictions_confirmed + predictions_surprised).
|
| 269 |
+
Returns updated EMA value (caller stores on substrate). Falls back to prior
|
| 270 |
+
EMA on missing fields / divide-by-zero (early bootstrap before predictions form).
|
| 271 |
+
"""
|
| 272 |
+
try:
|
| 273 |
+
confirmed = int(getattr(step_result, "predictions_confirmed", 0) or 0)
|
| 274 |
+
surprised = int(getattr(step_result, "predictions_surprised", 0) or 0)
|
| 275 |
+
total = confirmed + surprised
|
| 276 |
+
if total <= 0:
|
| 277 |
+
return prior_ema
|
| 278 |
+
mmn = surprised / total
|
| 279 |
+
return (1.0 - alpha) * prior_ema + alpha * mmn
|
| 280 |
+
except Exception as exc:
|
| 281 |
+
logger.debug("MMN EMA update skipped (non-fatal): %s", exc)
|
| 282 |
+
return prior_ema
|