Spaces:
Runtime error
Wire ng_tract BTF into the organism: two tracts, no preclassification
Browse filesReplaces the torch-pickled state blob with the canonical ecosystem
pattern (Rust Substrate Layer #119). Embeddings are now numpy f32
end-to-end β no more Python-list inflation on every deposit.
Two tracts, each doing one biological job (Substrate As Cortex):
- experience.tract: raw text via ng_tract.deposit_experience.
Full bytes, source-annotated, append-forever. Law 7 sensory
stream. Source tag ('user' / 'nuwave_response') is origin
annotation, not classification.
- outcomes.tract: ng_tract.deposit_outcome per node, embedding
as numpy f32 (zero-copy), metadata via msgpack. This is the
neuromodulatory learning signal β success bool is dopamine
polarity, not a classification label.
Preclassification purged from deposit metadata:
- No more "type": "experience" / "response" on node metadata β
the substrate discovers that distinction via STDP co-firing
- No more text[:200] truncation at deposit time β full text
enters; Pith bucket decides what to truncate at extraction
- No more "query": user_message[:100] on responses β the causal
link is recoverable from timestamp adjacency in the tract
Embeddings moved off node.metadata into self._embeddings side-table
(numpy f32 dict). Graph checkpoints stay lean. Pith reads from the
side-table directly β no np.array() conversion on the hot path.
Sidecar (nuwave_sidecar.pt) carries only substrate-dynamics state
that BTF doesn't naturally persist: graph checkpoint, amplitudes,
phases, node_content cache, stats. Never embeddings.
Legacy state migration: _apply_state pops embedding lists out of
node.metadata on restore and lifts them into self._embeddings as
numpy. Existing organism_state.pt on the hub still loads cleanly.
Hub push now uploads sidecar + both tracts. Tract files survive
container rebuilds and are available for future analysis buckets
(e.g. interleaved-benchmark neighborhood-overlap extraction).
Verified end-to-end: deposit β step β pith β record_outcome β
warm restart β legacy migration. All tracts appendable, numpy
embeddings round-trip zero-copy via embedding_as_numpy().
Refs: ~/docs/concepts/Rust Substrate Layer.md
~/docs/concepts/Substrate As Cortex.md
~/docs/prd/Binary_Tract_Format_v0.1.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- nuwave/organism.py +275 -74
- requirements.txt +2 -0
|
@@ -16,6 +16,25 @@ communication protocol (Law 1). Raw experience in, classification
|
|
| 16 |
only at extraction (Law 7).
|
| 17 |
|
| 18 |
# ---- Changelog ----
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
# [2026-04-06] Claude Code (Opus 4.6) β Initial organism wiring
|
| 20 |
# What: Full substrate loop for HuggingFace NuWave demo
|
| 21 |
# Why: Baseline proved 47.2% with crude string KISS. Real substrate
|
|
@@ -36,6 +55,13 @@ from typing import Any, Dict, List, Optional, Tuple
|
|
| 36 |
|
| 37 |
import numpy as np
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
logger = logging.getLogger("nuwave.organism")
|
| 40 |
|
| 41 |
# Substrate path β added temporarily during import, not permanently
|
|
@@ -52,14 +78,25 @@ class NuWaveOrganism:
|
|
| 52 |
organism.record_outcome(user_message, response, success=True)
|
| 53 |
"""
|
| 54 |
|
| 55 |
-
# Hub persistence β survives container rebuilds, sleeps, everything
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
HUB_REPO = "Executor-Tyrant-Framework/nuwave-substrate-state"
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
|
| 59 |
def __init__(self, state_path: str = "/tmp/nuwave_substrate"):
|
| 60 |
self._state_path = state_path
|
| 61 |
os.makedirs(state_path, exist_ok=True)
|
| 62 |
-
self.
|
|
|
|
|
|
|
|
|
|
| 63 |
self._hf_token = os.environ.get("HF_TOKEN", None)
|
| 64 |
|
| 65 |
# Initialize substrate
|
|
@@ -67,7 +104,14 @@ class NuWaveOrganism:
|
|
| 67 |
self._embed_fn = None
|
| 68 |
self._step_result = None
|
| 69 |
self._step_count = 0
|
| 70 |
-
self._node_content: Dict[str, str] = {} # node_id β
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
# Amplitude field β per-node complex amplitude for interference dynamics
|
| 73 |
# amplitude: how strongly this node contributes (grown by use, decayed by time)
|
|
@@ -118,37 +162,108 @@ class NuWaveOrganism:
|
|
| 118 |
logger.info("No saved state found β starting fresh organism")
|
| 119 |
|
| 120 |
def _try_restore_local(self) -> bool:
|
| 121 |
-
"""Restore from local
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
|
| 132 |
def _try_restore_hub(self) -> bool:
|
| 133 |
-
"""Pull
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
if not self._hf_token:
|
| 135 |
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
try:
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
except Exception as exc:
|
| 147 |
-
logger.
|
| 148 |
-
return False
|
| 149 |
|
| 150 |
def _apply_state(self, state: dict, source: str) -> bool:
|
| 151 |
-
"""Apply a loaded state dict
|
| 152 |
try:
|
| 153 |
# Graph checkpoint β stored as JSON string
|
| 154 |
graph_json = state.get('graph_checkpoint_json')
|
|
@@ -159,6 +274,23 @@ class NuWaveOrganism:
|
|
| 159 |
self._graph.restore(tmp)
|
| 160 |
os.remove(tmp)
|
| 161 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
# Content map
|
| 163 |
self._node_content = state.get('node_content', {})
|
| 164 |
|
|
@@ -172,13 +304,14 @@ class NuWaveOrganism:
|
|
| 172 |
self._phases = state.get('phases', {})
|
| 173 |
|
| 174 |
logger.info(
|
| 175 |
-
"Organism restored from %s: %d nodes, %d syn, %d content, step %d, %d
|
| 176 |
source,
|
| 177 |
len(self._graph.nodes) if self._graph else 0,
|
| 178 |
len(self._graph.synapses) if self._graph else 0,
|
| 179 |
len(self._node_content),
|
| 180 |
self._step_count,
|
| 181 |
len(self._amplitudes),
|
|
|
|
| 182 |
)
|
| 183 |
return True
|
| 184 |
except Exception as exc:
|
|
@@ -265,24 +398,46 @@ class NuWaveOrganism:
|
|
| 265 |
return None
|
| 266 |
|
| 267 |
try:
|
| 268 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
node_id = f"exp_{self._step_count}_{hash(text) & 0xFFFF:04x}"
|
| 270 |
|
| 271 |
-
# Create node in the SNN
|
|
|
|
|
|
|
| 272 |
node = self._graph.create_node(
|
| 273 |
node_id=node_id,
|
| 274 |
-
metadata={
|
| 275 |
-
"content": text[:200],
|
| 276 |
-
"step": self._step_count,
|
| 277 |
-
"type": "experience",
|
| 278 |
-
},
|
| 279 |
)
|
| 280 |
|
| 281 |
-
#
|
| 282 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
|
| 284 |
-
#
|
| 285 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 286 |
|
| 287 |
# Initialize amplitude for new node β starts at 1.0 (just deposited)
|
| 288 |
self._amplitudes[node_id] = 1.0
|
|
@@ -300,11 +455,11 @@ class NuWaveOrganism:
|
|
| 300 |
for existing_id, existing_node in self._graph.nodes.items():
|
| 301 |
if existing_id == node_id:
|
| 302 |
continue
|
| 303 |
-
|
|
|
|
| 304 |
if existing_emb is None:
|
| 305 |
continue
|
| 306 |
|
| 307 |
-
existing_emb = np.array(existing_emb, dtype=np.float32)
|
| 308 |
sim = float(np.dot(embedding, existing_emb) /
|
| 309 |
(np.linalg.norm(embedding) * np.linalg.norm(existing_emb) + 1e-8))
|
| 310 |
|
|
@@ -482,7 +637,7 @@ class NuWaveOrganism:
|
|
| 482 |
|
| 483 |
try:
|
| 484 |
import math
|
| 485 |
-
query_embedding = self._embed_fn(query)
|
| 486 |
|
| 487 |
# Compute per-node Born rule score.
|
| 488 |
# The query interferes with each node's amplitude:
|
|
@@ -490,12 +645,14 @@ class NuWaveOrganism:
|
|
| 490 |
# existing amplitude is the base strength.
|
| 491 |
scored = []
|
| 492 |
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
|
|
|
|
|
|
|
|
|
| 496 |
continue
|
| 497 |
|
| 498 |
-
node_emb = np.array(emb, dtype=np.float32)
|
| 499 |
sim = float(np.dot(query_embedding, node_emb) /
|
| 500 |
(np.linalg.norm(query_embedding) * np.linalg.norm(node_emb) + 1e-8))
|
| 501 |
|
|
@@ -568,23 +725,45 @@ class NuWaveOrganism:
|
|
| 568 |
return
|
| 569 |
|
| 570 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 571 |
# Response as raw experience β create node, stimulate, reward
|
| 572 |
-
response_embedding = self._embed_fn(response)
|
| 573 |
node_id = f"resp_{self._step_count}_{hash(response) & 0xFFFF:04x}"
|
| 574 |
|
|
|
|
|
|
|
|
|
|
| 575 |
node = self._graph.create_node(
|
| 576 |
node_id=node_id,
|
| 577 |
-
metadata={
|
| 578 |
-
"content": response[:200],
|
| 579 |
-
"type": "response",
|
| 580 |
-
"step": self._step_count,
|
| 581 |
-
"query": user_message[:100],
|
| 582 |
-
},
|
| 583 |
)
|
| 584 |
-
|
| 585 |
|
| 586 |
self._graph.stimulate(node_id, current=1.0)
|
| 587 |
-
self._node_content[node_id] = response
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 588 |
|
| 589 |
# Reward signal β the outcome closes the loop
|
| 590 |
# Strong reward strengthens synapses between co-firing nodes via STDP.
|
|
@@ -605,14 +784,20 @@ class NuWaveOrganism:
|
|
| 605 |
return dict(self.stats)
|
| 606 |
|
| 607 |
def save(self):
|
| 608 |
-
"""Persist all state β
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 609 |
|
| 610 |
-
|
| 611 |
-
|
|
|
|
| 612 |
"""
|
| 613 |
import torch
|
| 614 |
|
| 615 |
-
# Build
|
| 616 |
state = {
|
| 617 |
'node_content': self._node_content,
|
| 618 |
'stats': dict(self.stats),
|
|
@@ -620,9 +805,14 @@ class NuWaveOrganism:
|
|
| 620 |
'phases': self._phases,
|
| 621 |
}
|
| 622 |
|
| 623 |
-
# Graph checkpoint β read as raw JSON string
|
|
|
|
|
|
|
| 624 |
if self._graph:
|
| 625 |
try:
|
|
|
|
|
|
|
|
|
|
| 626 |
tmp = os.path.join(self._state_path, "_tmp_save.ckpt")
|
| 627 |
self._graph.checkpoint(tmp)
|
| 628 |
with open(tmp, 'r') as f:
|
|
@@ -631,32 +821,43 @@ class NuWaveOrganism:
|
|
| 631 |
except Exception as exc:
|
| 632 |
logger.debug("Graph checkpoint failed: %s", exc)
|
| 633 |
|
| 634 |
-
# Save locally
|
| 635 |
try:
|
| 636 |
-
torch.save(state, self.
|
| 637 |
except Exception as exc:
|
| 638 |
-
logger.debug("Local save failed: %s", exc)
|
| 639 |
|
| 640 |
-
# Push
|
| 641 |
self._push_to_hub()
|
| 642 |
|
| 643 |
def _push_to_hub(self):
|
| 644 |
-
"""Push
|
| 645 |
-
if not self._hf_token
|
| 646 |
return
|
| 647 |
try:
|
| 648 |
from huggingface_hub import HfApi, create_repo
|
| 649 |
-
# Ensure repo exists
|
| 650 |
create_repo(self.HUB_REPO, repo_type="dataset", exist_ok=True,
|
| 651 |
private=True, token=self._hf_token)
|
| 652 |
api = HfApi()
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 660 |
logger.info("State pushed to hub (%s)", self.HUB_REPO)
|
| 661 |
except Exception as exc:
|
| 662 |
logger.debug("Hub push failed (non-fatal): %s", exc)
|
|
|
|
| 16 |
only at extraction (Law 7).
|
| 17 |
|
| 18 |
# ---- Changelog ----
|
| 19 |
+
# [2026-04-15] Claude Code (Opus 4.6) β BTF wire-in (Rust Substrate Layer)
|
| 20 |
+
# What: Wire ng_tract BTF into the deposit/outcome path. Two tracts:
|
| 21 |
+
# experience.tract (raw text, Law 7 sensory stream, append forever)
|
| 22 |
+
# outcomes.tract (embedding + reward, neuromodulatory log).
|
| 23 |
+
# Embeddings moved off node.metadata into self._embeddings
|
| 24 |
+
# side-table β kept as numpy f32 the whole way, no .tolist().
|
| 25 |
+
# Preclassification purged from deposit metadata: no more
|
| 26 |
+
# "type": "experience"/"response" labels, no text[:200] truncation
|
| 27 |
+
# at deposit time. Full bytes into the experience tract.
|
| 28 |
+
# Why: See ~/docs/concepts/Substrate As Cortex.md β the substrate is
|
| 29 |
+
# cortex, not a database. Raw experience in, classification at
|
| 30 |
+
# extraction. ~/docs/concepts/Rust Substrate Layer.md β BTF is
|
| 31 |
+
# the canonical ecosystem pattern; embedding.tolist() is the
|
| 32 |
+
# exact inflation point BTF was built to eliminate (PRD Β§2).
|
| 33 |
+
# How: deposit_experience(str, source, path, content_type) for raw
|
| 34 |
+
# sensory input. deposit_outcome(ts, module, target, success,
|
| 35 |
+
# numpy_emb, [path], msgpack_meta) for the learning signal.
|
| 36 |
+
# Sidecar holds only substrate-derived state (amplitudes, phases,
|
| 37 |
+
# graph checkpoint, node_content cache, stats) β never embeddings.
|
| 38 |
# [2026-04-06] Claude Code (Opus 4.6) β Initial organism wiring
|
| 39 |
# What: Full substrate loop for HuggingFace NuWave demo
|
| 40 |
# Why: Baseline proved 47.2% with crude string KISS. Real substrate
|
|
|
|
| 55 |
|
| 56 |
import numpy as np
|
| 57 |
|
| 58 |
+
# BTF path β msgpack for metadata bytes, ng_tract for tract I/O.
|
| 59 |
+
# Hard requirement: wheel is vendored in the repo root, declared in
|
| 60 |
+
# requirements.txt. If import fails the organism should fail loudly,
|
| 61 |
+
# not silently fall back to inflated JSON.
|
| 62 |
+
import msgpack
|
| 63 |
+
import ng_tract
|
| 64 |
+
|
| 65 |
logger = logging.getLogger("nuwave.organism")
|
| 66 |
|
| 67 |
# Substrate path β added temporarily during import, not permanently
|
|
|
|
| 78 |
organism.record_outcome(user_message, response, success=True)
|
| 79 |
"""
|
| 80 |
|
| 81 |
+
# Hub persistence β survives container rebuilds, sleeps, everything.
|
| 82 |
+
# Three artifacts now: sidecar (amplitudes, phases, graph, stats, content
|
| 83 |
+
# cache) + two tracts (experience.tract, outcomes.tract). Tracts are the
|
| 84 |
+
# canonical form for embeddings and Law 7 sensory log; sidecar is the
|
| 85 |
+
# substrate-dynamics state that BTF doesn't carry.
|
| 86 |
HUB_REPO = "Executor-Tyrant-Framework/nuwave-substrate-state"
|
| 87 |
+
HUB_SIDECAR_FILENAME = "nuwave_sidecar.pt"
|
| 88 |
+
HUB_EXP_TRACT_FILENAME = "experience.tract"
|
| 89 |
+
HUB_OUTCOMES_TRACT_FILENAME = "outcomes.tract"
|
| 90 |
+
# Legacy β read-only, for migration from the pre-BTF state format.
|
| 91 |
+
HUB_LEGACY_FILENAME = "organism_state.pt"
|
| 92 |
|
| 93 |
def __init__(self, state_path: str = "/tmp/nuwave_substrate"):
|
| 94 |
self._state_path = state_path
|
| 95 |
os.makedirs(state_path, exist_ok=True)
|
| 96 |
+
self._local_sidecar_path = os.path.join(state_path, self.HUB_SIDECAR_FILENAME)
|
| 97 |
+
self._local_legacy_path = os.path.join(state_path, self.HUB_LEGACY_FILENAME)
|
| 98 |
+
self._exp_tract_path = os.path.join(state_path, self.HUB_EXP_TRACT_FILENAME)
|
| 99 |
+
self._outcomes_tract_path = os.path.join(state_path, self.HUB_OUTCOMES_TRACT_FILENAME)
|
| 100 |
self._hf_token = os.environ.get("HF_TOKEN", None)
|
| 101 |
|
| 102 |
# Initialize substrate
|
|
|
|
| 104 |
self._embed_fn = None
|
| 105 |
self._step_result = None
|
| 106 |
self._step_count = 0
|
| 107 |
+
self._node_content: Dict[str, str] = {} # node_id β FULL text (no truncation)
|
| 108 |
+
|
| 109 |
+
# Embedding side-table β numpy f32, never a Python list. BTF outcomes
|
| 110 |
+
# tract is the persisted form; this dict is the hot-path runtime
|
| 111 |
+
# cache. Nodes in the Graph carry NO embeddings β keeps graph
|
| 112 |
+
# checkpoints small and eliminates the listβnumpy round-trip that
|
| 113 |
+
# BTF was built to kill (see Rust Substrate Layer doc Β§2).
|
| 114 |
+
self._embeddings: Dict[str, np.ndarray] = {}
|
| 115 |
|
| 116 |
# Amplitude field β per-node complex amplitude for interference dynamics
|
| 117 |
# amplitude: how strongly this node contributes (grown by use, decayed by time)
|
|
|
|
| 162 |
logger.info("No saved state found β starting fresh organism")
|
| 163 |
|
| 164 |
def _try_restore_local(self) -> bool:
|
| 165 |
+
"""Restore from local sidecar; fall back to legacy format for migration.
|
| 166 |
+
|
| 167 |
+
New format (post-BTF): nuwave_sidecar.pt holds substrate-derived
|
| 168 |
+
state, no embeddings. Embeddings are restored from outcomes.tract.
|
| 169 |
+
Legacy format (pre-BTF): organism_state.pt β embeddings as Python
|
| 170 |
+
lists inside graph checkpoint metadata. One-shot migration at
|
| 171 |
+
load time: lists are lifted into self._embeddings as numpy.
|
| 172 |
+
"""
|
| 173 |
+
for path, source in [
|
| 174 |
+
(self._local_sidecar_path, "local-sidecar"),
|
| 175 |
+
(self._local_legacy_path, "local-legacy"),
|
| 176 |
+
]:
|
| 177 |
+
if not os.path.exists(path):
|
| 178 |
+
continue
|
| 179 |
+
try:
|
| 180 |
+
import torch
|
| 181 |
+
state = torch.load(path, map_location="cpu", weights_only=False)
|
| 182 |
+
if self._apply_state(state, source=source):
|
| 183 |
+
# After sidecar/legacy applied, pull embeddings from
|
| 184 |
+
# outcomes tract (canonical form). Tract may or may
|
| 185 |
+
# not exist yet β this is best-effort.
|
| 186 |
+
self._restore_embeddings_from_tract()
|
| 187 |
+
return True
|
| 188 |
+
except Exception as exc:
|
| 189 |
+
logger.warning("Local restore (%s) failed: %s", source, exc)
|
| 190 |
+
return False
|
| 191 |
|
| 192 |
def _try_restore_hub(self) -> bool:
|
| 193 |
+
"""Pull sidecar + tracts from HuggingFace dataset repo.
|
| 194 |
+
|
| 195 |
+
Tries the new sidecar filename first. Falls back to legacy
|
| 196 |
+
organism_state.pt if present. Tracts are pulled alongside so
|
| 197 |
+
the local cache has them for analysis buckets and for embedding
|
| 198 |
+
restoration.
|
| 199 |
+
"""
|
| 200 |
if not self._hf_token:
|
| 201 |
return False
|
| 202 |
+
from huggingface_hub import hf_hub_download
|
| 203 |
+
|
| 204 |
+
def _pull(filename: str) -> Optional[str]:
|
| 205 |
+
try:
|
| 206 |
+
return hf_hub_download(
|
| 207 |
+
self.HUB_REPO, filename,
|
| 208 |
+
repo_type="dataset", local_dir=self._state_path,
|
| 209 |
+
token=self._hf_token,
|
| 210 |
+
)
|
| 211 |
+
except Exception as exc:
|
| 212 |
+
logger.info("Hub %s absent: %s", filename, exc)
|
| 213 |
+
return None
|
| 214 |
+
|
| 215 |
+
# Pull tracts first β they're the canonical embedding source.
|
| 216 |
+
_pull(self.HUB_EXP_TRACT_FILENAME)
|
| 217 |
+
_pull(self.HUB_OUTCOMES_TRACT_FILENAME)
|
| 218 |
+
|
| 219 |
+
# Try new-format sidecar, then legacy.
|
| 220 |
+
for filename, source in [
|
| 221 |
+
(self.HUB_SIDECAR_FILENAME, "hub-sidecar"),
|
| 222 |
+
(self.HUB_LEGACY_FILENAME, "hub-legacy"),
|
| 223 |
+
]:
|
| 224 |
+
local = _pull(filename)
|
| 225 |
+
if not local:
|
| 226 |
+
continue
|
| 227 |
+
try:
|
| 228 |
+
import torch
|
| 229 |
+
state = torch.load(local, map_location="cpu", weights_only=False)
|
| 230 |
+
if self._apply_state(state, source=source):
|
| 231 |
+
self._restore_embeddings_from_tract()
|
| 232 |
+
return True
|
| 233 |
+
except Exception as exc:
|
| 234 |
+
logger.warning("Hub apply (%s) failed: %s", source, exc)
|
| 235 |
+
return False
|
| 236 |
+
|
| 237 |
+
def _restore_embeddings_from_tract(self):
|
| 238 |
+
"""Populate self._embeddings from outcomes.tract (canonical form).
|
| 239 |
+
|
| 240 |
+
Walks the tract in order; later entries win for repeated node_ids
|
| 241 |
+
(outcome tract is append-only β a node's latest reward-modulated
|
| 242 |
+
embedding is the current one). Zero-copy via embedding_as_numpy.
|
| 243 |
+
"""
|
| 244 |
+
if not os.path.exists(self._outcomes_tract_path):
|
| 245 |
+
return
|
| 246 |
try:
|
| 247 |
+
with open(self._outcomes_tract_path, 'rb') as f:
|
| 248 |
+
raw = f.read()
|
| 249 |
+
count = 0
|
| 250 |
+
for entry in ng_tract.TractReader(raw):
|
| 251 |
+
# PyOutcomeEntry has .target_id, .embedding_as_numpy()
|
| 252 |
+
tid = getattr(entry, 'target_id', None)
|
| 253 |
+
if not tid:
|
| 254 |
+
continue
|
| 255 |
+
try:
|
| 256 |
+
self._embeddings[tid] = entry.embedding_as_numpy()
|
| 257 |
+
count += 1
|
| 258 |
+
except Exception:
|
| 259 |
+
continue
|
| 260 |
+
logger.info("Restored %d embeddings from outcomes.tract (unique=%d)",
|
| 261 |
+
count, len(self._embeddings))
|
| 262 |
except Exception as exc:
|
| 263 |
+
logger.warning("Tract embedding restore failed: %s", exc)
|
|
|
|
| 264 |
|
| 265 |
def _apply_state(self, state: dict, source: str) -> bool:
|
| 266 |
+
"""Apply a loaded state dict. Handles new + legacy formats."""
|
| 267 |
try:
|
| 268 |
# Graph checkpoint β stored as JSON string
|
| 269 |
graph_json = state.get('graph_checkpoint_json')
|
|
|
|
| 274 |
self._graph.restore(tmp)
|
| 275 |
os.remove(tmp)
|
| 276 |
|
| 277 |
+
# LEGACY MIGRATION: pre-BTF checkpoints stored embeddings
|
| 278 |
+
# as Python lists inside node.metadata['embedding']. Lift
|
| 279 |
+
# them into self._embeddings as numpy f32, strip from
|
| 280 |
+
# metadata so future checkpoints stay lean.
|
| 281 |
+
migrated = 0
|
| 282 |
+
for nid, node in self._graph.nodes.items():
|
| 283 |
+
if node.metadata and 'embedding' in node.metadata:
|
| 284 |
+
emb = node.metadata.pop('embedding')
|
| 285 |
+
if isinstance(emb, list):
|
| 286 |
+
self._embeddings[nid] = np.asarray(emb, dtype=np.float32)
|
| 287 |
+
migrated += 1
|
| 288 |
+
elif isinstance(emb, np.ndarray):
|
| 289 |
+
self._embeddings[nid] = emb.astype(np.float32, copy=False)
|
| 290 |
+
migrated += 1
|
| 291 |
+
if migrated:
|
| 292 |
+
logger.info("Migrated %d legacy embeddings from graph metadata", migrated)
|
| 293 |
+
|
| 294 |
# Content map
|
| 295 |
self._node_content = state.get('node_content', {})
|
| 296 |
|
|
|
|
| 304 |
self._phases = state.get('phases', {})
|
| 305 |
|
| 306 |
logger.info(
|
| 307 |
+
"Organism restored from %s: %d nodes, %d syn, %d content, step %d, %d amps, %d embs",
|
| 308 |
source,
|
| 309 |
len(self._graph.nodes) if self._graph else 0,
|
| 310 |
len(self._graph.synapses) if self._graph else 0,
|
| 311 |
len(self._node_content),
|
| 312 |
self._step_count,
|
| 313 |
len(self._amplitudes),
|
| 314 |
+
len(self._embeddings),
|
| 315 |
)
|
| 316 |
return True
|
| 317 |
except Exception as exc:
|
|
|
|
| 398 |
return None
|
| 399 |
|
| 400 |
try:
|
| 401 |
+
# Law 7: raw bytes go to the experience tract BEFORE any
|
| 402 |
+
# processing. Full text, no truncation, source-annotated.
|
| 403 |
+
# The substrate learns via STDP co-firing, not via curated fields.
|
| 404 |
+
try:
|
| 405 |
+
ng_tract.deposit_experience(
|
| 406 |
+
text, "user", self._exp_tract_path, "text"
|
| 407 |
+
)
|
| 408 |
+
except Exception as exc:
|
| 409 |
+
logger.debug("Experience tract write failed: %s", exc)
|
| 410 |
+
|
| 411 |
+
embedding = np.asarray(self._embed_fn(text), dtype=np.float32)
|
| 412 |
node_id = f"exp_{self._step_count}_{hash(text) & 0xFFFF:04x}"
|
| 413 |
|
| 414 |
+
# Create node in the SNN. Metadata carries biological timestamp
|
| 415 |
+
# only β no "type" label, no "content" curation. The retina
|
| 416 |
+
# does not classify photons; V1 discovers features via dynamics.
|
| 417 |
node = self._graph.create_node(
|
| 418 |
node_id=node_id,
|
| 419 |
+
metadata={"step": self._step_count},
|
|
|
|
|
|
|
|
|
|
|
|
|
| 420 |
)
|
| 421 |
|
| 422 |
+
# Embedding lives in the side-table as numpy f32 (zero inflation).
|
| 423 |
+
# Outcomes tract is the persisted form; this dict is runtime cache.
|
| 424 |
+
self._embeddings[node_id] = embedding
|
| 425 |
+
|
| 426 |
+
# Full-text content cache β Pith trims at extraction, not here.
|
| 427 |
+
self._node_content[node_id] = text
|
| 428 |
|
| 429 |
+
# Neuromodulatory signal to outcomes tract. success=True is
|
| 430 |
+
# the dopamine polarity for sensory deposits (reward the act
|
| 431 |
+
# of observing); the actual learning signal from a full
|
| 432 |
+
# interaction flows through record_outcome() after response.
|
| 433 |
+
try:
|
| 434 |
+
ng_tract.deposit_outcome(
|
| 435 |
+
time.time(), "nuwave", node_id, True, embedding,
|
| 436 |
+
[self._outcomes_tract_path],
|
| 437 |
+
msgpack.packb({"step": self._step_count, "source": "user"}),
|
| 438 |
+
)
|
| 439 |
+
except Exception as exc:
|
| 440 |
+
logger.debug("Outcomes tract write failed: %s", exc)
|
| 441 |
|
| 442 |
# Initialize amplitude for new node β starts at 1.0 (just deposited)
|
| 443 |
self._amplitudes[node_id] = 1.0
|
|
|
|
| 455 |
for existing_id, existing_node in self._graph.nodes.items():
|
| 456 |
if existing_id == node_id:
|
| 457 |
continue
|
| 458 |
+
# Embeddings live in the side-table now, not node.metadata.
|
| 459 |
+
existing_emb = self._embeddings.get(existing_id)
|
| 460 |
if existing_emb is None:
|
| 461 |
continue
|
| 462 |
|
|
|
|
| 463 |
sim = float(np.dot(embedding, existing_emb) /
|
| 464 |
(np.linalg.norm(embedding) * np.linalg.norm(existing_emb) + 1e-8))
|
| 465 |
|
|
|
|
| 637 |
|
| 638 |
try:
|
| 639 |
import math
|
| 640 |
+
query_embedding = np.asarray(self._embed_fn(query), dtype=np.float32)
|
| 641 |
|
| 642 |
# Compute per-node Born rule score.
|
| 643 |
# The query interferes with each node's amplitude:
|
|
|
|
| 645 |
# existing amplitude is the base strength.
|
| 646 |
scored = []
|
| 647 |
|
| 648 |
+
# Iterate the side-table, not graph.nodes β embeddings no
|
| 649 |
+
# longer live in node.metadata. Nodes without embeddings
|
| 650 |
+
# (e.g. legacy graph entries) are skipped naturally.
|
| 651 |
+
for nid in self._graph.nodes.keys():
|
| 652 |
+
node_emb = self._embeddings.get(nid)
|
| 653 |
+
if node_emb is None:
|
| 654 |
continue
|
| 655 |
|
|
|
|
| 656 |
sim = float(np.dot(query_embedding, node_emb) /
|
| 657 |
(np.linalg.norm(query_embedding) * np.linalg.norm(node_emb) + 1e-8))
|
| 658 |
|
|
|
|
| 725 |
return
|
| 726 |
|
| 727 |
try:
|
| 728 |
+
# Raw response bytes β experience tract (Law 7). Source tag
|
| 729 |
+
# distinguishes origin ("nuwave_response" vs "user") β that's
|
| 730 |
+
# origin annotation, not classification. The causal link to
|
| 731 |
+
# the query is derivable from timestamp adjacency in the
|
| 732 |
+
# tract; no need to curate a "query" field at deposit time.
|
| 733 |
+
try:
|
| 734 |
+
ng_tract.deposit_experience(
|
| 735 |
+
response, "nuwave_response", self._exp_tract_path, "text"
|
| 736 |
+
)
|
| 737 |
+
except Exception as exc:
|
| 738 |
+
logger.debug("Experience tract write failed: %s", exc)
|
| 739 |
+
|
| 740 |
# Response as raw experience β create node, stimulate, reward
|
| 741 |
+
response_embedding = np.asarray(self._embed_fn(response), dtype=np.float32)
|
| 742 |
node_id = f"resp_{self._step_count}_{hash(response) & 0xFFFF:04x}"
|
| 743 |
|
| 744 |
+
# Metadata carries biological timestamp only β no type label,
|
| 745 |
+
# no query field, no truncated content. The substrate discovers
|
| 746 |
+
# response-vs-experience distinction via STDP co-firing.
|
| 747 |
node = self._graph.create_node(
|
| 748 |
node_id=node_id,
|
| 749 |
+
metadata={"step": self._step_count},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 750 |
)
|
| 751 |
+
self._embeddings[node_id] = response_embedding
|
| 752 |
|
| 753 |
self._graph.stimulate(node_id, current=1.0)
|
| 754 |
+
self._node_content[node_id] = response
|
| 755 |
+
|
| 756 |
+
# Neuromodulatory deposit β the reward polarity is THE
|
| 757 |
+
# learning signal. This is biology (dopamine modulates STDP),
|
| 758 |
+
# not preclassification. Real outcome bool reaches the tract.
|
| 759 |
+
try:
|
| 760 |
+
ng_tract.deposit_outcome(
|
| 761 |
+
time.time(), "nuwave", node_id, success, response_embedding,
|
| 762 |
+
[self._outcomes_tract_path],
|
| 763 |
+
msgpack.packb({"step": self._step_count, "source": "nuwave_response"}),
|
| 764 |
+
)
|
| 765 |
+
except Exception as exc:
|
| 766 |
+
logger.debug("Outcomes tract write failed: %s", exc)
|
| 767 |
|
| 768 |
# Reward signal β the outcome closes the loop
|
| 769 |
# Strong reward strengthens synapses between co-firing nodes via STDP.
|
|
|
|
| 784 |
return dict(self.stats)
|
| 785 |
|
| 786 |
def save(self):
|
| 787 |
+
"""Persist all state β sidecar + tracts, local + hub.
|
| 788 |
+
|
| 789 |
+
Sidecar holds substrate-dynamics state that BTF doesn't carry:
|
| 790 |
+
graph checkpoint (synapses/hyperedges/predictions), amplitudes,
|
| 791 |
+
phases, node_content cache, stats. NO embeddings β those live
|
| 792 |
+
in outcomes.tract as numpy f32.
|
| 793 |
|
| 794 |
+
Tracts are append-only β written as deposits happen, not here.
|
| 795 |
+
This method only needs to save the sidecar and push everything
|
| 796 |
+
to the hub.
|
| 797 |
"""
|
| 798 |
import torch
|
| 799 |
|
| 800 |
+
# Build sidecar state β no embeddings.
|
| 801 |
state = {
|
| 802 |
'node_content': self._node_content,
|
| 803 |
'stats': dict(self.stats),
|
|
|
|
| 805 |
'phases': self._phases,
|
| 806 |
}
|
| 807 |
|
| 808 |
+
# Graph checkpoint β read as raw JSON string. If any legacy
|
| 809 |
+
# embeddings slipped into node.metadata, strip them here so
|
| 810 |
+
# future checkpoints stay lean.
|
| 811 |
if self._graph:
|
| 812 |
try:
|
| 813 |
+
for node in self._graph.nodes.values():
|
| 814 |
+
if node.metadata and 'embedding' in node.metadata:
|
| 815 |
+
del node.metadata['embedding']
|
| 816 |
tmp = os.path.join(self._state_path, "_tmp_save.ckpt")
|
| 817 |
self._graph.checkpoint(tmp)
|
| 818 |
with open(tmp, 'r') as f:
|
|
|
|
| 821 |
except Exception as exc:
|
| 822 |
logger.debug("Graph checkpoint failed: %s", exc)
|
| 823 |
|
| 824 |
+
# Save sidecar locally
|
| 825 |
try:
|
| 826 |
+
torch.save(state, self._local_sidecar_path)
|
| 827 |
except Exception as exc:
|
| 828 |
+
logger.debug("Local sidecar save failed: %s", exc)
|
| 829 |
|
| 830 |
+
# Push sidecar + tracts to hub
|
| 831 |
self._push_to_hub()
|
| 832 |
|
| 833 |
def _push_to_hub(self):
|
| 834 |
+
"""Push sidecar + both tracts to HF dataset repo."""
|
| 835 |
+
if not self._hf_token:
|
| 836 |
return
|
| 837 |
try:
|
| 838 |
from huggingface_hub import HfApi, create_repo
|
|
|
|
| 839 |
create_repo(self.HUB_REPO, repo_type="dataset", exist_ok=True,
|
| 840 |
private=True, token=self._hf_token)
|
| 841 |
api = HfApi()
|
| 842 |
+
|
| 843 |
+
for local_path, hub_name in [
|
| 844 |
+
(self._local_sidecar_path, self.HUB_SIDECAR_FILENAME),
|
| 845 |
+
(self._exp_tract_path, self.HUB_EXP_TRACT_FILENAME),
|
| 846 |
+
(self._outcomes_tract_path, self.HUB_OUTCOMES_TRACT_FILENAME),
|
| 847 |
+
]:
|
| 848 |
+
if not os.path.exists(local_path):
|
| 849 |
+
continue
|
| 850 |
+
try:
|
| 851 |
+
api.upload_file(
|
| 852 |
+
path_or_fileobj=local_path,
|
| 853 |
+
path_in_repo=hub_name,
|
| 854 |
+
repo_id=self.HUB_REPO,
|
| 855 |
+
repo_type="dataset",
|
| 856 |
+
token=self._hf_token,
|
| 857 |
+
)
|
| 858 |
+
except Exception as exc:
|
| 859 |
+
logger.debug("Hub push of %s failed (non-fatal): %s", hub_name, exc)
|
| 860 |
+
|
| 861 |
logger.info("State pushed to hub (%s)", self.HUB_REPO)
|
| 862 |
except Exception as exc:
|
| 863 |
logger.debug("Hub push failed (non-fatal): %s", exc)
|
|
@@ -3,3 +3,5 @@ torch>=2.0.0
|
|
| 3 |
accelerate>=0.27.0
|
| 4 |
onnxruntime>=1.16.0
|
| 5 |
tokenizers>=0.15.0
|
|
|
|
|
|
|
|
|
| 3 |
accelerate>=0.27.0
|
| 4 |
onnxruntime>=1.16.0
|
| 5 |
tokenizers>=0.15.0
|
| 6 |
+
msgpack>=1.0.0
|
| 7 |
+
./ng_tract-0.1.0-cp312-abi3-manylinux_2_34_x86_64.whl
|