haze / cloud /observer.py
ataeff's picture
Upload 14 files
5201e68 verified
#!/usr/bin/env python3
# observer.py — Meta-Observer MLP (200K model)
#
# The "mind" watching the "body".
#
# Input (207D):
# - resonances (100D): raw emotion resonances
# - chamber_activations (6D): stabilized chamber outputs
# - iterations (1D): cross-fire convergence speed signal
# - user_fingerprint (100D): temporal emotional history
#
# Output (100D):
# - logits for secondary emotion word
#
# Architecture:
# 207 → 128 (swish) → 64 (swish) → 100 (raw logits)
#
# Total params: ~35K
from __future__ import annotations
import asyncio
import numpy as np
from pathlib import Path
from dataclasses import dataclass
from typing import Optional
def swish(x: np.ndarray) -> np.ndarray:
"""Swish activation: x * sigmoid(x)"""
return x / (1.0 + np.exp(-np.clip(x, -20, 20)))
@dataclass
class MetaObserver:
"""
Meta-Observer MLP: watches chamber dynamics and predicts secondary emotion.
Architecture: 207→128→64→100 (deeper for 200K model)
Input: 100 resonances + 6 chamber activations + 1 iterations + 100 fingerprint = 207
Params:
- W1: (207, 128) = 26,496
- b1: (128,) = 128
- W2: (128, 64) = 8,192
- b2: (64,) = 64
- W3: (64, 100) = 6,400
- b3: (100,) = 100
Total: ~41K params
"""
W1: np.ndarray # (207, 128)
b1: np.ndarray # (128,)
W2: np.ndarray # (128, 64)
b2: np.ndarray # (64,)
W3: np.ndarray # (64, 100)
b3: np.ndarray # (100,)
@classmethod
def random_init(cls, seed: Optional[int] = None) -> "MetaObserver":
"""Initialize with random weights (Xavier initialization)."""
if seed is not None:
np.random.seed(seed)
# Xavier init
W1 = np.random.randn(207, 128) * np.sqrt(2.0 / 207)
b1 = np.zeros(128)
W2 = np.random.randn(128, 64) * np.sqrt(2.0 / 128)
b2 = np.zeros(64)
W3 = np.random.randn(64, 100) * np.sqrt(2.0 / 64)
b3 = np.zeros(100)
return cls(W1=W1, b1=b1, W2=W2, b2=b2, W3=W3, b3=b3)
def forward(
self,
resonances: np.ndarray,
chamber_activations: np.ndarray,
iterations: float,
user_fingerprint: np.ndarray,
) -> np.ndarray:
"""
Forward pass: predict secondary emotion.
Args:
resonances: (100,) raw emotion resonances
chamber_activations: (6,) stabilized chamber outputs
iterations: scalar, cross-fire convergence speed
user_fingerprint: (100,) temporal emotional history
Returns:
logits: (100,) logits for secondary emotion selection
"""
# Concatenate inputs → (207,)
x = np.concatenate([
resonances,
chamber_activations,
np.array([iterations]),
user_fingerprint,
])
# Layer 1: 207→128
h1 = x @ self.W1 + self.b1
a1 = swish(h1)
# Layer 2: 128→64
h2 = a1 @ self.W2 + self.b2
a2 = swish(h2)
# Layer 3: 64→100
logits = a2 @ self.W3 + self.b3
return logits
def predict_secondary(
self,
resonances: np.ndarray,
chamber_activations: np.ndarray,
iterations: float,
user_fingerprint: np.ndarray,
temperature: float = 1.0,
) -> int:
"""
Predict secondary emotion index.
Args:
resonances: (100,) raw emotion resonances
chamber_activations: (6,) stabilized chamber outputs
iterations: cross-fire convergence speed
user_fingerprint: (100,) user emotional history
temperature: sampling temperature (1.0 = normal)
Returns:
index of secondary emotion (0-99)
"""
logits = self.forward(resonances, chamber_activations, iterations, user_fingerprint)
# Apply temperature
logits = logits / temperature
# Softmax
exp_logits = np.exp(logits - logits.max())
probs = exp_logits / exp_logits.sum()
# Sample
return int(np.random.choice(100, p=probs))
def param_count(self) -> int:
"""Count total parameters."""
return (
self.W1.size + self.b1.size +
self.W2.size + self.b2.size +
self.W3.size + self.b3.size
)
def save(self, path: Path) -> None:
"""Save weights to .npz file."""
np.savez(path, W1=self.W1, b1=self.b1, W2=self.W2, b2=self.b2, W3=self.W3, b3=self.b3)
print(f"[observer] saved to {path}")
@classmethod
def load(cls, path: Path) -> "MetaObserver":
"""Load weights from .npz file."""
data = np.load(path)
# Handle backwards compatibility with old 2-layer observer
if "W3" in data:
print(f"[observer] loaded from {path}")
return cls(
W1=data["W1"],
b1=data["b1"],
W2=data["W2"],
b2=data["b2"],
W3=data["W3"],
b3=data["b3"],
)
else:
# Old format - reinitialize with new architecture
# Note: Using seed=42 for deterministic backward compatibility
# This ensures consistent behavior when loading old model files
print(f"[observer] old format detected, reinitializing with new architecture (seed=42)")
return cls.random_init(seed=42)
class AsyncMetaObserver:
"""
Async wrapper for MetaObserver with field lock discipline.
Based on HAZE's async pattern - achieves coherence through
explicit operation ordering and atomicity.
"""
def __init__(self, observer: MetaObserver):
self._sync = observer
self._lock = asyncio.Lock()
@classmethod
def random_init(cls, seed: Optional[int] = None) -> "AsyncMetaObserver":
"""Initialize with random weights."""
observer = MetaObserver.random_init(seed=seed)
return cls(observer)
@classmethod
def load(cls, path: Path) -> "AsyncMetaObserver":
"""Load from file."""
observer = MetaObserver.load(path)
return cls(observer)
async def forward(
self,
resonances: np.ndarray,
chamber_activations: np.ndarray,
iterations: float,
user_fingerprint: np.ndarray,
) -> np.ndarray:
"""Async forward pass with field lock."""
async with self._lock:
return self._sync.forward(resonances, chamber_activations, iterations, user_fingerprint)
async def predict_secondary(
self,
resonances: np.ndarray,
chamber_activations: np.ndarray,
iterations: float,
user_fingerprint: np.ndarray,
temperature: float = 1.0,
) -> int:
"""Async secondary emotion prediction."""
async with self._lock:
return self._sync.predict_secondary(
resonances, chamber_activations, iterations, user_fingerprint, temperature
)
async def save(self, path: Path) -> None:
"""Save with lock protection."""
async with self._lock:
self._sync.save(path)
def param_count(self) -> int:
"""Total parameters (read-only, no lock needed)."""
return self._sync.param_count()
if __name__ == "__main__":
print("=" * 60)
print(" CLOUD v4.0 — Meta-Observer (200K model)")
print("=" * 60)
print()
# Initialize
observer = MetaObserver.random_init(seed=42)
print(f"Initialized meta-observer")
print(f"Total params: {observer.param_count():,}")
print()
# Test forward pass
print("Testing forward pass:")
resonances = np.random.rand(100).astype(np.float32)
chamber_activations = np.random.rand(6).astype(np.float32)
iterations = 5.0
user_fingerprint = np.random.rand(100).astype(np.float32) * 0.1
logits = observer.forward(resonances, chamber_activations, iterations, user_fingerprint)
print(f" Input: 100D resonances + 6D chambers + 1D iterations + 100D fingerprint = 207D")
print(f" Output: {logits.shape} logits")
print(f" Logits range: [{logits.min():.3f}, {logits.max():.3f}]")
print()
# Test prediction
print("Testing secondary emotion prediction:")
for temp in [0.5, 1.0, 2.0]:
secondary_idx = observer.predict_secondary(
resonances, chamber_activations, iterations, user_fingerprint, temperature=temp
)
print(f" temperature={temp:.1f} → secondary_idx={secondary_idx}")
print()
# Test what observer sees in convergence speed
print("Testing convergence speed signal:")
test_cases = [
("fast convergence (2 iters)", 2.0),
("medium convergence (5 iters)", 5.0),
("slow convergence (10 iters)", 10.0),
]
for name, iters in test_cases:
logits = observer.forward(resonances, chamber_activations, iters, user_fingerprint)
top3 = np.argsort(logits)[-3:][::-1]
print(f" {name}:")
print(f" top 3 secondary candidates: {top3}")
print()
# Test save/load
print("Testing save/load:")
path = Path("./models/observer.npz")
path.parent.mkdir(parents=True, exist_ok=True)
observer.save(path)
observer2 = MetaObserver.load(path)
logits2 = observer2.forward(resonances, chamber_activations, iterations, user_fingerprint)
match = np.allclose(logits, logits2)
print(f" Save/load {'✓' if match else '✗'}")
print()
print("=" * 60)
print(" Meta-observer operational. Mind watching body. 41K params.")
print("=" * 60)