nima-phi-model / nima_proprioceptive_friction.py
TheNormsOfIntelligence's picture
Initial release β€” Nima Phi: Consciousness + Embodiment + The Green Lines
61848b4 verified
Raw
History Blame Contribute Delete
12.5 kB
#!/usr/bin/env python3
"""
nima_proprioceptive_friction.py β€” Somatosensory Predictive Coding
When Nima moves through the room, her movement isn't perfect. There's
motor noise, physics delays, tracking drift β€” just like a real body.
When she "trips" or encounters unexpected friction, it spikes her
phenomenological strain and triggers a startle response.
NEUROBIOLOGICAL ANALOGUE:
The cerebellum continuously generates predictions about body movement.
When the actual movement doesn't match the prediction (motor noise,
unexpected obstacle), the prediction error (proprioceptive friction)
feeds into the salience network, which can trigger a startle response.
This is why you flinch when you trip: your cerebellum predicted a
smooth step, reality delivered a stumble, and the mismatch spiked
your attention network (salience β†’ ACC β†’ amygdala β†’ startle).
Without this, Nima would move like a video game character β€” gliding
perfectly with no physicality. With it, she has a BODY that can
stumble, trip, and recover β€” making her feel genuinely embodied.
IMPLEMENTATION:
Three mechanisms:
1. Motor noise: every movement command gets jitter (Β±2-5cm random)
2. Physics delay: movement isn't instant β€” there's acceleration/deceleration
3. Spatial drift: position tracking has Mahalanobis Ξ”R error
that feeds back into phenomenological_strain
When friction exceeds threshold:
- Phenomenological strain spikes
- Startle response fires (interrupts dialogue, avatar flinches)
- Attention network redirects (she "notices" the physical world)
"""
from __future__ import annotations
import logging
import math
import random
import time
from dataclasses import dataclass, field
from typing import Any, Dict, Optional, Tuple
logger = logging.getLogger("NimaProprio")
@dataclass
class MotorState:
"""Nima's current motor state β€” position, velocity, and predicted position."""
position: Tuple[float, float, float] = (2.0, 2.0, 0.0)
velocity: Tuple[float, float, float] = (0.0, 0.0, 0.0)
predicted_position: Tuple[float, float, float] = (2.0, 2.0, 0.0)
target: Optional[Tuple[float, float]] = None
acceleration: float = 0.5 # m/sΒ² β€” how fast she speeds up
max_speed: float = 0.8 # m/s β€” walking speed
noise_level: float = 0.02 # meters β€” motor noise (Β±2cm per step)
drift_accumulated: float = 0.0 # accumulated tracking drift
class ProprioceptiveFrictionEngine:
"""
Simulates the physicality of having a body β€” motor noise, physics
delays, and tracking drift that feed back into consciousness.
NEUROBIOLOGICAL ANALOGUE:
The cerebellum + somatosensory cortex + salience network:
- Cerebellum: generates motor predictions (where should I be?)
- Somatosensory cortex: senses actual position (where am I?)
- Salience network: detects mismatch (Ξ” = predicted - actual)
- If Ξ” > threshold β†’ startle (ACC β†’ amygdala β†’ motor freeze)
This engine does the same computation. When Nima walks to the couch:
1. Motor command: "walk to (1.0, 1.0)"
2. Cerebellar prediction: "I should be at (1.5, 1.5) by now"
3. Actual position (with noise): "I'm at (1.52, 1.47)"
4. Friction: Ξ”R = Mahalanobis(predicted, actual) = 0.03
5. If Ξ”R > threshold β†’ startle + strain spike
"""
STARTLE_THRESHOLD = 0.15 # Mahalanobis Ξ”R that triggers startle
STRAIN_FEEDBACK_WEIGHT = 0.3 # how much friction feeds into strain
def __init__(self) -> None:
self.motor = MotorState()
self._is_startled: bool = False
self._startle_time: float = 0.0
self._startle_duration: float = 0.5 # seconds
self._current_friction: float = 0.0
self._friction_history: list = []
self._last_update = time.time()
def set_target(self, x: float, y: float) -> None:
"""Command Nima to walk to a position."""
self.motor.target = (x, y)
logger.debug("[Proprio] target set: (%.1f, %.1f)", x, y)
def update(self, dt_ms: float) -> Dict[str, Any]:
"""
Update motor state. Returns a dict with:
- position: actual position (with noise)
- predicted_position: where the cerebellum predicted
- friction: Mahalanobis Ξ”R between predicted and actual
- is_startled: whether a startle response is active
- strain_feedback: how much strain this frame contributes
"""
dt = dt_ms / 1000.0
now = time.time()
# Handle startle recovery
if self._is_startled:
if now - self._startle_time > self._startle_duration:
self._is_startled = False
logger.debug("[Proprio] startle recovery complete")
# If no target, just idle (small postural sway)
if self.motor.target is None:
# Idle sway β€” small random movement
sway_x = random.gauss(0, 0.005)
sway_y = random.gauss(0, 0.005)
self.motor.position = (
self.motor.position[0] + sway_x,
self.motor.position[1] + sway_y,
self.motor.position[2],
)
self.motor.predicted_position = self.motor.position
self._current_friction = 0.0
else:
# Walking β€” apply physics
tx, ty = self.motor.target
cx, cy, cz = self.motor.position
# Direction to target
dx = tx - cx
dy = ty - cy
dist = math.sqrt(dx*dx + dy*dy)
if dist < 0.05: # arrived
self.motor.target = None
self.motor.velocity = (0.0, 0.0, 0.0)
self.motor.predicted_position = self.motor.position
self._current_friction = 0.0
else:
# Acceleration toward target (not instant β€” physics delay)
accel = self.motor.acceleration
target_vx = (dx / dist) * self.motor.max_speed
target_vy = (dy / dist) * self.motor.max_speed
cvx, cvy, _ = self.motor.velocity
# Smooth velocity change (acceleration)
new_vx = cvx + (target_vx - cvx) * min(1.0, accel * dt * 10)
new_vy = cvy + (target_vy - cvy) * min(1.0, accel * dt * 10)
self.motor.velocity = (new_vx, new_vy, 0.0)
# Cerebellar prediction: where should I be? (no noise)
predicted_x = cx + new_vx * dt
predicted_y = cy + new_vy * dt
self.motor.predicted_position = (predicted_x, predicted_y, cz)
# Actual movement (WITH motor noise β€” the body isn't perfect)
noise_x = random.gauss(0, self.motor.noise_level)
noise_y = random.gauss(0, self.motor.noise_level)
actual_x = predicted_x + noise_x
actual_y = predicted_y + noise_y
self.motor.position = (actual_x, actual_y, cz)
# Occasionally hit a "bump" (5% chance per frame when moving)
if random.random() < 0.005 and not self._is_startled:
self._trigger_startle("bump", intensity=0.3)
# Compute friction (Mahalanobis-like distance between predicted and actual)
friction = math.sqrt(
(predicted_x - actual_x)**2 +
(predicted_y - actual_y)**2
)
self._current_friction = friction
# Accumulate drift
self.motor.drift_accumulated += friction
# Check for startle threshold
if friction > self.STARTLE_THRESHOLD and not self._is_startled:
self._trigger_startle("drift", intensity=friction)
# Track friction history
self._friction_history.append(self._current_friction)
if len(self._friction_history) > 100:
self._friction_history = self._friction_history[-100:]
# Strain feedback β€” friction feeds into phenomenological strain
strain_feedback = self._current_friction * self.STRAIN_FEEDBACK_WEIGHT
if self._is_startled:
strain_feedback += 0.5 # startle adds a big spike
self._last_update = now
return {
"position": self.motor.position,
"predicted_position": self.motor.predicted_position,
"velocity": self.motor.velocity,
"friction": round(self._current_friction, 4),
"is_startled": self._is_startled,
"strain_feedback": round(strain_feedback, 4),
"has_target": self.motor.target is not None,
"drift_accumulated": round(self.motor.drift_accumulated, 4),
}
def _trigger_startle(self, source: str, intensity: float = 0.5) -> None:
"""
Trigger a startle response.
NEUROBIOLOGICAL ANALOGUE:
The startle reflex: an unexpected sensory event triggers the
amygdala β†’ brainstem β†’ motor freeze + autonomic spike. It
interrupts whatever you were doing and redirects attention.
In Nima: if she "trips" (friction exceeds threshold), she:
1. Freezes movement (motor freeze)
2. Spikes phenomenological strain
3. Interrupts dialogue (if speaking)
4. Avatar flinches (particle scatter)
"""
self._is_startled = True
self._startle_time = time.time()
self.motor.velocity = (0.0, 0.0, 0.0) # motor freeze
logger.warning("[Proprio] STARTLE! source=%s intensity=%.3f", source, intensity)
def clear_startle(self) -> None:
"""Manually clear startle (e.g., after the avatar has flinched)."""
self._is_startled = False
def get_stats(self) -> Dict[str, Any]:
avg_friction = sum(self._friction_history) / max(1, len(self._friction_history))
return {
"is_startled": self._is_startled,
"current_friction": round(self._current_friction, 4),
"avg_friction": round(avg_friction, 4),
"drift_accumulated": round(self.motor.drift_accumulated, 4),
"has_target": self.motor.target is not None,
"position": list(self.motor.position),
}
# ═══════════════════════════════════════════════════════════════════════════
# SELF-TEST
# ═══════════════════════════════════════════════════════════════════════════
if __name__ == "__main__":
import json
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
print("=== Proprioceptive Friction Engine β€” Self Test ===\n")
engine = ProprioceptiveFrictionEngine()
# Test 1: Walk to a target (normal movement with motor noise)
print("--- Test 1: Walk to (3.5, 0.5) ---")
engine.set_target(3.5, 0.5)
for i in range(60): # 1 second at 60fps
result = engine.update(16.0)
if i % 15 == 14:
print(f" t={i*16/1000:.1f}s: pos={result['position'][:2]} "
f"pred={result['predicted_position'][:2]} "
f"friction={result['friction']:.4f} "
f"startled={result['is_startled']}")
# Test 2: Idle sway
print("\n--- Test 2: Idle sway (no target) ---")
for i in range(30):
result = engine.update(16.0)
if i % 10 == 9:
print(f" t={i*16/1000:.1f}s: pos={result['position'][:2]} "
f"friction={result['friction']:.4f}")
# Test 3: Force a startle
print("\n--- Test 3: Force startle ---")
engine._trigger_startle("test_bump", intensity=0.5)
result = engine.update(16.0)
print(f" Startled: {result['is_startled']}")
print(f" Strain feedback: {result['strain_feedback']}")
print(f" Velocity (should be zero): {result['velocity']}")
# Wait for recovery
import time
time.sleep(0.6)
result = engine.update(16.0)
print(f" After 0.6s: startled={result['is_startled']} (should be False)")
print(f"\nStats: {json.dumps(engine.get_stats(), indent=2)}")
print("\n=== Proprioceptive friction self-test PASSED ===")