| """DII Tracker — Drift-Integrated Information score for measuring aliveness. |
| |
| PEDI (Persistent Embodied Drift Integration) proposes that consciousness-like |
| coherence in resource-bounded agents emerges from the time-integrated interaction |
| of persistence, ignition, integration, embodiment, and drift. |
| |
| DII(t) = ∫₀ᵗ [P(τ)·I(τ)·Φ(τ)·(1+E(τ))·D(τ)] dτ / (1 + ∫₀ᵗ D(τ) dτ) |
| |
| In practice we compute a rolling discrete approximation using exponential |
| moving averages sampled on each background cycle. |
| """ |
|
|
| import sqlite3 |
| import threading |
| import time |
| from dataclasses import dataclass |
| from pathlib import Path |
| from typing import Any, Dict, List, Optional |
|
|
| from infj_bot.core.config import DATA_DIR |
|
|
| DII_DB = DATA_DIR / "dii_history.db" |
|
|
|
|
| @dataclass |
| class DIISample: |
| """One DII measurement snapshot.""" |
|
|
| timestamp: float |
| persistence: float = 0.0 |
| ignition: float = 0.0 |
| integration: float = 0.0 |
| embodiment: float = 0.0 |
| drift: float = 0.0 |
| dii: float = 0.0 |
| dii_simple: float = 0.0 |
|
|
|
|
| class DIITracker: |
| """Computes and logs Drift-Integrated Information in real time.""" |
|
|
| def __init__(self, db_path: Optional[Path] = None): |
| self.db_path = str(db_path or DII_DB) |
| self._lock = threading.Lock() |
| self._init_db() |
|
|
| |
| self.ema_p: float = 0.5 |
| self.ema_i: float = 0.5 |
| self.ema_phi: float = 0.5 |
| self.ema_e: float = 0.5 |
| self.ema_d: float = 0.5 |
| self.alpha: float = 0.1 |
|
|
| |
| self.numerator_acc: float = 0.0 |
| self.drift_acc: float = 0.0 |
| self.sample_count: int = 0 |
|
|
| |
| self.recent_samples: List[DIISample] = [] |
| self.max_samples: int = 2880 |
|
|
| def _init_db(self): |
| with sqlite3.connect(self.db_path) as conn: |
| conn.execute(""" |
| CREATE TABLE IF NOT EXISTS dii_samples ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| timestamp REAL NOT NULL, |
| persistence REAL NOT NULL, |
| ignition REAL NOT NULL, |
| integration REAL NOT NULL, |
| embodiment REAL NOT NULL, |
| drift REAL NOT NULL, |
| dii REAL NOT NULL, |
| dii_simple REAL NOT NULL |
| ) |
| """) |
| conn.execute(""" |
| CREATE INDEX IF NOT EXISTS idx_dii_time ON dii_samples(timestamp) |
| """) |
| conn.commit() |
|
|
| def compute( |
| self, |
| being=None, |
| workspace=None, |
| homeostasis=None, |
| shadow=None, |
| orchestrator=None, |
| ) -> DIISample: |
| """Compute one DII sample from current cognitive state.""" |
| with self._lock: |
| now = time.time() |
|
|
| |
| |
| p_raw = self._compute_persistence(being) |
|
|
| |
| |
| i_raw = self._compute_ignition(workspace) |
|
|
| |
| |
| phi_raw = self._compute_integration(orchestrator, homeostasis) |
|
|
| |
| |
| e_raw = self._compute_embodiment(homeostasis) |
|
|
| |
| |
| d_raw = self._compute_drift(being, shadow) |
|
|
| |
| self.ema_p = self.ema_p * (1 - self.alpha) + p_raw * self.alpha |
| self.ema_i = self.ema_i * (1 - self.alpha) + i_raw * self.alpha |
| self.ema_phi = self.ema_phi * (1 - self.alpha) + phi_raw * self.alpha |
| self.ema_e = self.ema_e * (1 - self.alpha) + e_raw * self.alpha |
| self.ema_d = self.ema_d * (1 - self.alpha) + d_raw * self.alpha |
|
|
| |
| self.numerator_acc += ( |
| self.ema_p * self.ema_i * self.ema_phi * (1 + self.ema_e) * self.ema_d |
| ) |
| self.drift_acc += self.ema_d |
| self.sample_count += 1 |
|
|
| |
| dii = self.numerator_acc / (1.0 + self.drift_acc) |
| |
| dii = max(0.0, min(2.0, dii)) |
|
|
| |
| dii_simple = self.ema_p * self.ema_i * self.ema_phi * (1 + self.ema_e) |
| dii_simple = max(0.0, min(2.0, dii_simple)) |
|
|
| sample = DIISample( |
| timestamp=now, |
| persistence=self.ema_p, |
| ignition=self.ema_i, |
| integration=self.ema_phi, |
| embodiment=self.ema_e, |
| drift=self.ema_d, |
| dii=dii, |
| dii_simple=dii_simple, |
| ) |
|
|
| self.recent_samples.append(sample) |
| if len(self.recent_samples) > self.max_samples: |
| self.recent_samples = self.recent_samples[-self.max_samples :] |
|
|
| self._log_sample(sample) |
| return sample |
|
|
| def _compute_persistence(self, being) -> float: |
| """Persistence: coherence of internal states over time.""" |
| if being is None: |
| return 0.5 |
| |
| energy = being.state.energy |
| attachment = being.state.attachment |
| |
| wm_size = min(len(being.working_memory) / 20.0, 1.0) |
| return energy * 0.4 + attachment * 0.3 + wm_size * 0.3 |
|
|
| def _compute_ignition(self, workspace) -> float: |
| """Ignition: Global Workspace winner strength.""" |
| if workspace is None: |
| return 0.5 |
| try: |
| |
| if workspace.spotlight: |
| return min(1.0, workspace.spotlight.salience) |
| |
| if workspace.contents: |
| return min(1.0, max(c.salience for c in workspace.contents)) |
| except Exception: |
| pass |
| return 0.5 |
|
|
| def _compute_integration(self, orchestrator, homeostasis) -> float: |
| """Integration: cross-module binding (IIT-inspired proxy).""" |
| |
| active_count = 0 |
| if orchestrator is not None: |
| try: |
| recent = orchestrator.bus.get_recent(limit=20) |
| unique_sources = set(e.get("source", "") for e in recent) |
| active_count = len(unique_sources) |
| except Exception: |
| pass |
| |
| integration_proxy = min(1.0, active_count / 8.0) |
|
|
| |
| coherence_bonus = 0.0 |
| if homeostasis is not None: |
| try: |
| coherence_bonus = homeostasis.needs.get("coherence", {}).current * 0.2 |
| except Exception: |
| pass |
|
|
| return min(1.0, integration_proxy + coherence_bonus) |
|
|
| def _compute_embodiment(self, homeostasis) -> float: |
| """Embodiment: distance from homeostatic equilibrium.""" |
| if homeostasis is None: |
| return 0.5 |
| try: |
| |
| load = homeostasis.allostatic_load |
| |
| return max(0.0, load * 2.0) |
| except Exception: |
| return 0.5 |
|
|
| def _compute_drift(self, being, shadow) -> float: |
| """Drift: shadow + aspiration pull preventing stasis.""" |
| drift = 0.0 |
| if being is not None: |
| |
| drift += being.state.curiosity * 0.3 |
| drift += being.agency.autonomy_drive * 0.3 |
| |
| drift += being.state.intensity * 0.2 |
| if shadow is not None: |
| try: |
| |
| radar_avg = sum(shadow.radar.values()) / max(len(shadow.radar), 1) |
| drift += radar_avg * 0.2 |
| except Exception: |
| pass |
| return min(1.0, drift) |
|
|
| def _log_sample(self, sample: DIISample): |
| """Persist sample to SQLite.""" |
| try: |
| with sqlite3.connect(self.db_path) as conn: |
| conn.execute( |
| """ |
| INSERT INTO dii_samples |
| (timestamp, persistence, ignition, integration, embodiment, drift, dii, dii_simple) |
| VALUES (?, ?, ?, ?, ?, ?, ?, ?) |
| """, |
| ( |
| sample.timestamp, |
| sample.persistence, |
| sample.ignition, |
| sample.integration, |
| sample.embodiment, |
| sample.drift, |
| sample.dii, |
| sample.dii_simple, |
| ), |
| ) |
| conn.commit() |
| except Exception: |
| pass |
|
|
| def get_current(self) -> Optional[DIISample]: |
| """Return the most recent DII sample.""" |
| with self._lock: |
| return self.recent_samples[-1] if self.recent_samples else None |
|
|
| def get_trend(self, n: int = 20) -> Dict[str, Any]: |
| """Return trend summary over last n samples.""" |
| with self._lock: |
| if not self.recent_samples: |
| return { |
| "dii_current": 0.0, |
| "dii_avg": 0.0, |
| "dii_peak": 0.0, |
| "trend": "flat", |
| } |
| samples = self.recent_samples[-n:] |
| diis = [s.dii for s in samples] |
| current = diis[-1] |
| avg = sum(diis) / len(diis) |
| peak = max(diis) |
| |
| mid = len(diis) // 2 |
| first = sum(diis[:mid]) / max(mid, 1) |
| second = sum(diis[mid:]) / max(len(diis) - mid, 1) |
| if second > first * 1.05: |
| trend = "rising" |
| elif second < first * 0.95: |
| trend = "falling" |
| else: |
| trend = "flat" |
| return { |
| "dii_current": round(current, 3), |
| "dii_avg": round(avg, 3), |
| "dii_peak": round(peak, 3), |
| "dii_simple": round(samples[-1].dii_simple, 3), |
| "components": { |
| "p": round(samples[-1].persistence, 3), |
| "i": round(samples[-1].ignition, 3), |
| "phi": round(samples[-1].integration, 3), |
| "e": round(samples[-1].embodiment, 3), |
| "d": round(samples[-1].drift, 3), |
| }, |
| "trend": trend, |
| "samples": len(self.recent_samples), |
| } |
|
|
| def get_history(self, limit: int = 100) -> List[Dict[str, Any]]: |
| """Return historical samples from DB for plotting.""" |
| try: |
| with sqlite3.connect(self.db_path) as conn: |
| rows = conn.execute( |
| """ |
| SELECT timestamp, dii, dii_simple, persistence, ignition, integration, embodiment, drift |
| FROM dii_samples |
| ORDER BY timestamp DESC |
| LIMIT ? |
| """, |
| (limit,), |
| ).fetchall() |
| return [ |
| { |
| "timestamp": r[0], |
| "dii": r[1], |
| "dii_simple": r[2], |
| "p": r[3], |
| "i": r[4], |
| "phi": r[5], |
| "e": r[6], |
| "d": r[7], |
| } |
| for r in reversed(rows) |
| ] |
| except Exception: |
| return [] |
|
|
|
|
| |
| _dii_instance: Optional[DIITracker] = None |
|
|
|
|
| def get_dii_tracker() -> DIITracker: |
| global _dii_instance |
| if _dii_instance is None: |
| _dii_instance = DIITracker() |
| return _dii_instance |
|
|