FlyBrain-Lab / src /language /grounded.py
timfromhcs's picture
FlyBrain v4.1.0 Space build (REAL_SUBGRAPH, CPU-only, honest backend)
3d46076 verified
Raw
History Blame Contribute Delete
6.18 kB
"""Grounded language (STAGE G): symbols linked to actual sensory/internal
representations — never free-floating text. Production comes from internal
state; comprehension maps symbols back to grounded concepts; information
transfer is measured by behavior change in the receiver.
"""
import hashlib
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Tuple
import numpy as np
from src.common.determinism import derive_subseed
GROUNDING_MODALITIES = ("sensory", "internal", "social")
@dataclass
class Concept:
concept_id: str
name: str
modality: str # sensory | internal | social
feature_vector: Tuple[float, ...] # grounded features (e.g. sensory stats)
grounding_evidence: Dict[str, Any] = field(default_factory=dict)
use_count: int = 0
def to_dict(self) -> Dict[str, Any]:
d = dict(self.__dict__)
d["feature_vector"] = list(self.feature_vector)
return d
@classmethod
def from_dict(cls, d: Dict[str, Any]) -> "Concept":
return cls(d["concept_id"], d["name"], d["modality"],
tuple(d["feature_vector"]), d.get("grounding_evidence", {}),
int(d.get("use_count", 0)))
class GroundedLanguageSystem:
"""Symbol <-> concept <-> grounded representation pipeline."""
def __init__(self, seed: int = 42, feature_dim: int = 8):
self.seed = int(seed)
self.feature_dim = int(feature_dim)
self.concepts: Dict[str, Concept] = {} # concept_id -> Concept
self.lexicon: Dict[str, str] = {} # symbol -> concept_id
self.symbol_usage: Dict[str, int] = {} # symbol -> productions+comprehensions
self._seq = 0
# ------------------------------------------------------------ grounding
def ground_concept(self, name: str, feature_vector: List[float],
modality: str, evidence: Optional[Dict[str, Any]] = None) -> Concept:
if modality not in GROUNDING_MODALITIES:
raise ValueError(f"unknown modality {modality!r}")
v = tuple(round(float(x), 6) for x in feature_vector[:self.feature_dim])
if len(v) < self.feature_dim:
v = v + (0.0,) * (self.feature_dim - len(v))
if any(not np.isfinite(x) for x in v):
raise ValueError("concept features must be finite")
self._seq += 1
cid = hashlib.sha256(f"{self.seed}|{name}|{self._seq}".encode()).hexdigest()[:12]
c = Concept(cid, name, modality, v,
dict(evidence or {"grounded_at": self._seq}), use_count=0)
self.concepts[cid] = c
return c
def learn_symbol(self, symbol: str, concept_id: str, teacher: str = "self",
tick: int = 0) -> bool:
"""Bind a symbol to an ALREADY-GROUNDED concept (never the reverse)."""
if concept_id not in self.concepts:
raise ValueError(f"cannot bind symbol {symbol!r} to ungrounded concept")
if not symbol or any(ch in symbol for ch in " \n\t"):
raise ValueError("invalid symbol")
prev = self.lexicon.get(symbol)
self.lexicon[symbol] = concept_id
self.symbol_usage.setdefault(symbol, 0)
return prev is None or prev == concept_id
# ------------------------------------------------------- communication
def produce(self, internal_state: Dict[str, Any], max_symbols: int = 4) -> List[str]:
"""Symbol sequence FROM internal state (drive levels, sensed features,
active concept salience). Empty when nothing is communicable."""
out: List[str] = []
salient = internal_state.get("salient_concepts", [])
for cid in salient[:max_symbols]:
sym = self._symbol_for_concept(cid)
if sym and sym not in out:
self.concepts[cid].use_count += 1
self.symbol_usage[sym] = self.symbol_usage.get(sym, 0) + 1
out.append(sym)
return out
def _symbol_for_concept(self, concept_id: str) -> Optional[str]:
for sym, cid in self.lexicon.items():
if cid == concept_id:
return sym
return None
def comprehend(self, symbols: List[str]) -> List[str]:
"""Map received symbols to grounded concept ids."""
cids: List[str] = []
for s in symbols:
cid = self.lexicon.get(s)
if cid is not None:
self.concepts[cid].use_count += 1
self.symbol_usage[s] = self.symbol_usage.get(s, 0) + 1
cids.append(cid)
return cids
# -------------------------------------------------------- measurement
def grounding_strength(self, symbol: str) -> float:
"""0..1: how strongly a symbol is anchored to grounded experience."""
cid = self.lexicon.get(symbol)
if cid is None:
return 0.0
c = self.concepts[cid]
usage = min(1.0, self.symbol_usage.get(symbol, 0) / 10.0)
return round(0.5 * (1.0 if c.grounding_evidence else 0.0) + 0.5 * usage, 4)
def vocabulary_size(self) -> int:
return len(self.lexicon)
def ungrounded_symbols(self) -> List[str]:
return [s for s, cid in self.lexicon.items()
if not self.concepts[cid].grounding_evidence]
# -------------------------------------------------------- persistence
def snapshot(self) -> Dict[str, Any]:
return {"seed": self.seed, "feature_dim": self.feature_dim,
"concepts": [c.to_dict() for c in self.concepts.values()],
"lexicon": dict(self.lexicon),
"symbol_usage": dict(self.symbol_usage), "seq": self._seq}
@classmethod
def restore(cls, payload: Dict[str, Any]) -> "GroundedLanguageSystem":
sys_ = cls(payload["seed"], payload.get("feature_dim", 8))
for d in payload.get("concepts", []):
c = Concept.from_dict(d)
sys_.concepts[c.concept_id] = c
sys_.lexicon = dict(payload.get("lexicon", {}))
sys_.symbol_usage = dict(payload.get("symbol_usage", {}))
sys_._seq = int(payload.get("seq", 0))
return sys_