FerrellSyntheticIntelligence commited on
Commit ·
d32bfdb
1
Parent(s): 7d9e142
Add ValenceEngine, SyntheticThalamus, DecisionGate
Browse files- src/basal_ganglia/__init__.py +1 -0
- src/basal_ganglia/decision_gate.py +105 -0
- src/thalamus/__init__.py +1 -0
- src/thalamus/thalamus.py +68 -0
- src/valence/__init__.py +1 -0
- src/valence/valence_engine.py +79 -0
src/basal_ganglia/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
src/basal_ganglia/decision_gate.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Basal Ganglia Decision Gate — Vitalis FSI
|
| 3 |
+
|
| 4 |
+
Probabilistic action selector.
|
| 5 |
+
High valence + high novelty + high confidence = action selected.
|
| 6 |
+
Low valence or high cost = action suppressed.
|
| 7 |
+
|
| 8 |
+
This is how Vitalis decides what to do next.
|
| 9 |
+
Not deterministic. Not random. Weighted by what it has learned.
|
| 10 |
+
"""
|
| 11 |
+
import numpy as np
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from src.valence.valence_engine import ValenceEngine
|
| 14 |
+
from src.dream_engine.helix_memory import HelixMemory
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class DecisionGate:
|
| 18 |
+
def __init__(
|
| 19 |
+
self,
|
| 20 |
+
valence_engine: ValenceEngine,
|
| 21 |
+
helix_path: Path = None,
|
| 22 |
+
alpha: float = 1.0, # valence weight
|
| 23 |
+
beta: float = 0.8, # novelty weight
|
| 24 |
+
gamma: float = 0.6, # confidence weight
|
| 25 |
+
delta: float = 0.2, # cost penalty
|
| 26 |
+
):
|
| 27 |
+
self.valence = valence_engine
|
| 28 |
+
self.helix = HelixMemory(
|
| 29 |
+
helix_path or Path.home() / ".vitalis_workspace" / "helix_memory.pkl"
|
| 30 |
+
)
|
| 31 |
+
self.alpha = alpha
|
| 32 |
+
self.beta = beta
|
| 33 |
+
self.gamma = gamma
|
| 34 |
+
self.delta = delta
|
| 35 |
+
self._history = []
|
| 36 |
+
|
| 37 |
+
def _novelty(self, hv: np.ndarray) -> float:
|
| 38 |
+
"""How different is this from anything in long-term memory."""
|
| 39 |
+
if not self.helix.entries:
|
| 40 |
+
return 1.0
|
| 41 |
+
sims = [float(np.mean(hv == proto))
|
| 42 |
+
for _, proto, _, _ in self.helix.entries]
|
| 43 |
+
return float(1.0 - max(sims))
|
| 44 |
+
|
| 45 |
+
def score(self, candidate: dict) -> float:
|
| 46 |
+
"""
|
| 47 |
+
Score one candidate action.
|
| 48 |
+
candidate must have:
|
| 49 |
+
"hv" : hypervector (np.ndarray)
|
| 50 |
+
"confidence" : float 0-1 (optional, default 0.5)
|
| 51 |
+
"cost" : float (optional, default 1.0)
|
| 52 |
+
"""
|
| 53 |
+
hv = candidate["hv"]
|
| 54 |
+
confidence = candidate.get("confidence", 0.5)
|
| 55 |
+
cost = candidate.get("cost", 1.0)
|
| 56 |
+
|
| 57 |
+
val, _ = self.valence.evaluate(hv)
|
| 58 |
+
novelty = self._novelty(hv)
|
| 59 |
+
|
| 60 |
+
return (
|
| 61 |
+
self.alpha * val +
|
| 62 |
+
self.beta * novelty +
|
| 63 |
+
self.gamma * confidence -
|
| 64 |
+
self.delta * cost
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
def select(self, candidates: list) -> dict:
|
| 68 |
+
"""
|
| 69 |
+
Select one action from a list of candidates via softmax sampling.
|
| 70 |
+
Each candidate is a dict with at minimum "hv" and "intent" keys.
|
| 71 |
+
Returns the selected candidate.
|
| 72 |
+
"""
|
| 73 |
+
if not candidates:
|
| 74 |
+
raise ValueError("No candidates provided")
|
| 75 |
+
|
| 76 |
+
if len(candidates) == 1:
|
| 77 |
+
return candidates[0]
|
| 78 |
+
|
| 79 |
+
scores = np.array([self.score(c) for c in candidates])
|
| 80 |
+
|
| 81 |
+
# Softmax for probabilistic selection
|
| 82 |
+
exp_s = np.exp(scores - scores.max())
|
| 83 |
+
probs = exp_s / exp_s.sum()
|
| 84 |
+
idx = int(np.random.choice(len(candidates), p=probs))
|
| 85 |
+
|
| 86 |
+
chosen = candidates[idx]
|
| 87 |
+
self._history.append({
|
| 88 |
+
"intent": chosen.get("intent", "unknown"),
|
| 89 |
+
"score": round(float(scores[idx]), 4),
|
| 90 |
+
"confidence": chosen.get("confidence", 0.5),
|
| 91 |
+
})
|
| 92 |
+
if len(self._history) > 100:
|
| 93 |
+
self._history.pop(0)
|
| 94 |
+
|
| 95 |
+
return chosen
|
| 96 |
+
|
| 97 |
+
def report(self) -> dict:
|
| 98 |
+
if not self._history:
|
| 99 |
+
return {"status": "No decisions made yet"}
|
| 100 |
+
avg_score = float(np.mean([h["score"] for h in self._history]))
|
| 101 |
+
return {
|
| 102 |
+
"total_decisions": len(self._history),
|
| 103 |
+
"avg_score": round(avg_score, 4),
|
| 104 |
+
"recent": self._history[-3:],
|
| 105 |
+
}
|
src/thalamus/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
src/thalamus/thalamus.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Synthetic Thalamus — Vitalis FSI
|
| 3 |
+
|
| 4 |
+
Multimodal input router. Fuses text, audio, and internal
|
| 5 |
+
signals into a single unified hypervector using temporal
|
| 6 |
+
binding and cyclic permutation.
|
| 7 |
+
|
| 8 |
+
All downstream systems receive the same representation space.
|
| 9 |
+
"""
|
| 10 |
+
import numpy as np
|
| 11 |
+
import time
|
| 12 |
+
from vitalis_ide.math_core.kernel import VitalisKernel
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def _permute(vec: np.ndarray, shift: int) -> np.ndarray:
|
| 16 |
+
return np.roll(vec, shift % len(vec))
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class SyntheticThalamus:
|
| 20 |
+
def __init__(self, dim: int = 10_000):
|
| 21 |
+
self.dim = dim
|
| 22 |
+
self.kernel = VitalisKernel()
|
| 23 |
+
self._tick = 0
|
| 24 |
+
|
| 25 |
+
def ingest(self, payload: dict) -> np.ndarray:
|
| 26 |
+
"""
|
| 27 |
+
payload keys:
|
| 28 |
+
"text" : str
|
| 29 |
+
"audio" : Path to wav file
|
| 30 |
+
"internal" : pre-vectorized int8 hypervector
|
| 31 |
+
Returns unified binary hypervector.
|
| 32 |
+
"""
|
| 33 |
+
self._tick += 1
|
| 34 |
+
vectors = []
|
| 35 |
+
|
| 36 |
+
if "text" in payload:
|
| 37 |
+
tokens = payload["text"].lower().split()
|
| 38 |
+
if tokens:
|
| 39 |
+
hv = self.kernel.vectorize_tokens(tokens, positional=False)
|
| 40 |
+
vectors.append(_permute(hv, self._tick))
|
| 41 |
+
|
| 42 |
+
if "audio" in payload:
|
| 43 |
+
try:
|
| 44 |
+
from src.audio_ear.feature_extractor import extract_features
|
| 45 |
+
from src.hdc_encoder.encoder import encode
|
| 46 |
+
mfcc, prosody = extract_features(payload["audio"])
|
| 47 |
+
hv = encode(mfcc, prosody)
|
| 48 |
+
vectors.append(_permute(hv, self._tick * 2))
|
| 49 |
+
except Exception:
|
| 50 |
+
pass
|
| 51 |
+
|
| 52 |
+
if "internal" in payload:
|
| 53 |
+
hv = np.array(payload["internal"], dtype=np.int8)
|
| 54 |
+
vectors.append(_permute(hv, self._tick * 3))
|
| 55 |
+
|
| 56 |
+
if not vectors:
|
| 57 |
+
return np.ones(self.dim, dtype=np.int8)
|
| 58 |
+
|
| 59 |
+
# Bundle all modalities
|
| 60 |
+
bundle = np.zeros(self.dim, dtype=np.int32)
|
| 61 |
+
for v in vectors:
|
| 62 |
+
bundle += v.astype(np.int32)
|
| 63 |
+
result = np.sign(bundle).astype(np.int8)
|
| 64 |
+
result[result == 0] = 1
|
| 65 |
+
return result
|
| 66 |
+
|
| 67 |
+
def ingest_text(self, text: str) -> np.ndarray:
|
| 68 |
+
return self.ingest({"text": text})
|
src/valence/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
from .valence_engine import ValenceEngine
|
src/valence/valence_engine.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Valence Engine — Vitalis FSI
|
| 3 |
+
|
| 4 |
+
Computes an affective score from a hypervector.
|
| 5 |
+
Score in [-1, 1]:
|
| 6 |
+
-1 = strong negative (failure, frustration)
|
| 7 |
+
0 = neutral / unknown
|
| 8 |
+
+1 = strong positive (success, curiosity)
|
| 9 |
+
|
| 10 |
+
Learned online from outcomes. No pretrained weights.
|
| 11 |
+
"""
|
| 12 |
+
import numpy as np
|
| 13 |
+
import os
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class ValenceEngine:
|
| 18 |
+
LEARNING_RATE = 0.01
|
| 19 |
+
BUFFER_MAX = 50
|
| 20 |
+
EPISODIC_BIAS = 0.3
|
| 21 |
+
|
| 22 |
+
def __init__(self, dim: int = 10_000):
|
| 23 |
+
self.dim = dim
|
| 24 |
+
self.path = Path.home() / ".vitalis_workspace" / "valence_weights.npy"
|
| 25 |
+
self.w = self._load_weights()
|
| 26 |
+
self.buffer = [] # (hv, reward) tuples
|
| 27 |
+
|
| 28 |
+
def _load_weights(self) -> np.ndarray:
|
| 29 |
+
if self.path.exists():
|
| 30 |
+
return np.load(self.path)
|
| 31 |
+
return np.random.randn(self.dim) * 0.001
|
| 32 |
+
|
| 33 |
+
def _save_weights(self):
|
| 34 |
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
| 35 |
+
np.save(self.path, self.w)
|
| 36 |
+
|
| 37 |
+
def reinforce(self, hv: np.ndarray, reward: float):
|
| 38 |
+
"""
|
| 39 |
+
Called after every outcome.
|
| 40 |
+
reward: +1.0 for success, -1.0 for failure, 0.0 for neutral.
|
| 41 |
+
"""
|
| 42 |
+
hv = hv.astype(np.float32)
|
| 43 |
+
pred = np.dot(self.w, hv) / self.dim
|
| 44 |
+
err = reward - pred
|
| 45 |
+
self.w += self.LEARNING_RATE * err * hv
|
| 46 |
+
self.buffer.append((hv.copy(), reward))
|
| 47 |
+
if len(self.buffer) > self.BUFFER_MAX:
|
| 48 |
+
self.buffer.pop(0)
|
| 49 |
+
self._save_weights()
|
| 50 |
+
|
| 51 |
+
def evaluate(self, hv: np.ndarray) -> tuple:
|
| 52 |
+
"""
|
| 53 |
+
Returns (valence, confidence).
|
| 54 |
+
confidence = |valence| — how certain the system is.
|
| 55 |
+
"""
|
| 56 |
+
hv_f = hv.astype(np.float32)
|
| 57 |
+
|
| 58 |
+
# Resonance term
|
| 59 |
+
raw = float(np.tanh(np.dot(self.w, hv_f) / self.dim))
|
| 60 |
+
|
| 61 |
+
# Episodic bias from recent high-valence experiences
|
| 62 |
+
bias = 0.0
|
| 63 |
+
if self.buffer:
|
| 64 |
+
sims = [float(np.mean(hv_f == bh)) for bh, _ in self.buffer]
|
| 65 |
+
weights = [bv for _, bv in self.buffer]
|
| 66 |
+
denom = sum(sims) + 1e-9
|
| 67 |
+
bias = float(np.dot(sims, weights) / denom)
|
| 68 |
+
|
| 69 |
+
valence = float(np.clip(0.7 * raw + self.EPISODIC_BIAS * bias, -1.0, 1.0))
|
| 70 |
+
confidence = abs(valence)
|
| 71 |
+
return valence, confidence
|
| 72 |
+
|
| 73 |
+
def report(self) -> dict:
|
| 74 |
+
return {
|
| 75 |
+
"buffer_size": len(self.buffer),
|
| 76 |
+
"avg_recent_reward": round(float(np.mean([r for _, r in self.buffer])), 4)
|
| 77 |
+
if self.buffer else 0.0,
|
| 78 |
+
"weight_norm": round(float(np.linalg.norm(self.w)), 4),
|
| 79 |
+
}
|