Buckets:
tostido/Butterfly-Field-Station-storage / work /Convergence_Engine /reality_simulator /concept_tracker.py
| """ | |
| ConceptTracker - Semantic Naming for Behavioral Phenotypes | |
| Tracks cluster persistence and promotes stable clusters to named concepts. | |
| This is Quick Win #2 for enhancing the system's capacity for understanding. | |
| A cluster becomes a "concept" after persisting for N consecutive cycles. | |
| Concepts are auto-tagged with semantic names based on organism properties. | |
| Examples of concept names: | |
| - "explorers" - high movement, moderate fitness | |
| - "settlers" - low movement, stable resources | |
| - "cooperators" - high connections, moderate fitness | |
| - "loners" - low connections, variable fitness | |
| - "thrivers" - high fitness, high resources | |
| - "strugglers" - low fitness, low resources | |
| """ | |
| from dataclasses import dataclass, field | |
| from typing import Dict, List, Tuple, Optional, Any, Callable | |
| import time | |
| import numpy as np | |
| import logging | |
| logger = logging.getLogger(__name__) | |
| class Concept: | |
| """A stable behavioral phenotype with semantic meaning""" | |
| concept_id: str # "explorers", "settlers", "cooperators", etc. | |
| first_seen: float # timestamp | |
| last_seen: float | |
| cluster_history: List[Tuple[float, int]] = field(default_factory=list) # [(timestamp, cluster_id)] | |
| population_history: List[Tuple[float, int]] = field(default_factory=list) # [(timestamp, count)] | |
| parent_concept: Optional[str] = None | |
| child_concepts: List[str] = field(default_factory=list) | |
| properties: Dict[str, float] = field(default_factory=dict) # avg fitness, connections, etc. | |
| def to_dict(self) -> Dict[str, Any]: | |
| """Convert to JSON-serializable dict""" | |
| return { | |
| 'concept_id': self.concept_id, | |
| 'first_seen': self.first_seen, | |
| 'last_seen': self.last_seen, | |
| 'cluster_history': self.cluster_history[-10:], # Last 10 entries to avoid bloat | |
| 'population_history': self.population_history[-10:], | |
| 'parent_concept': self.parent_concept, | |
| 'child_concepts': self.child_concepts, | |
| 'properties': self.properties, | |
| 'lifespan': self.last_seen - self.first_seen, | |
| 'persistence': len(self.cluster_history) | |
| } | |
| class ConceptTracker: | |
| """ | |
| Tracks cluster persistence and promotes stable clusters to concepts. | |
| A cluster becomes a concept after existing for N consecutive cycles. | |
| Concepts are auto-tagged with semantic names based on organism properties. | |
| This enables the system to develop semantic understanding of behavioral | |
| patterns, forming the foundation for higher-order reasoning about | |
| population dynamics. | |
| """ | |
| # Semantic naming thresholds | |
| HIGH_FITNESS = 0.7 | |
| MED_FITNESS = 0.4 | |
| HIGH_RESOURCES = 0.7 | |
| LOW_RESOURCES = 0.3 | |
| HIGH_CONNECTIONS = 5 # Average connections threshold | |
| LOW_CONNECTIONS = 2 | |
| def __init__(self, | |
| persistence_threshold: int = 3, | |
| stale_threshold: float = 10.0, | |
| enabled: bool = True): | |
| """ | |
| Args: | |
| persistence_threshold: Number of cycles a cluster must persist to become a concept | |
| stale_threshold: Time after which unseen clusters are pruned | |
| enabled: Enable/disable concept tracking | |
| """ | |
| self.persistence_threshold = persistence_threshold | |
| self.stale_threshold = stale_threshold | |
| self.enabled = enabled | |
| # Cluster tracking | |
| self.cluster_history: Dict[int, List[Tuple[float, int]]] = {} # cluster_id → [(timestamp, size)] | |
| self.cluster_last_seen: Dict[int, float] = {} # cluster_id → timestamp | |
| # Concept registry | |
| self.concepts: Dict[str, Concept] = {} # concept_id → Concept | |
| self.cluster_to_concept: Dict[int, str] = {} # cluster_id → concept_id (current mapping) | |
| # Concept naming counter (for uniqueness) | |
| self.concept_counter: Dict[str, int] = {} # base_name → count | |
| # Event emitter callback (will be set by MLAnalyzer or main) | |
| self.event_emitter: Optional[Callable[[Dict[str, Any]], None]] = None | |
| # SEMANTIC CONVERGENCE: ContextMemory for feeding phenotype names to vocabulary | |
| self.context_memory: Optional[Any] = None | |
| # Statistics | |
| self.total_concepts_created = 0 | |
| self.total_concepts_extinct = 0 | |
| logger.info(f"[ConceptTracker] Initialized (persistence={persistence_threshold}, enabled={enabled})") | |
| def update(self, | |
| cluster_labels: np.ndarray, | |
| cluster_sizes: Dict[int, int], | |
| organisms: Dict[str, Any], | |
| timestamp: Optional[float] = None) -> Dict[int, str]: | |
| """ | |
| Update cluster tracking and promote stable clusters to concepts. | |
| Args: | |
| cluster_labels: Cluster assignment for each organism (numpy array) | |
| cluster_sizes: {cluster_id: count} | |
| organisms: Dict of organism objects for property extraction | |
| timestamp: Current timestamp (defaults to time.time()) | |
| Returns: | |
| Dict mapping cluster_id → concept_id (for clusters that have become concepts) | |
| """ | |
| if not self.enabled: | |
| return {} | |
| if timestamp is None: | |
| timestamp = time.time() | |
| concept_tags = {} | |
| # Track cluster persistence | |
| for cluster_id, size in cluster_sizes.items(): | |
| if cluster_id == -1: # Skip noise cluster (HDBSCAN) | |
| continue | |
| # Update history | |
| if cluster_id not in self.cluster_history: | |
| self.cluster_history[cluster_id] = [] | |
| self.cluster_history[cluster_id].append((timestamp, size)) | |
| self.cluster_last_seen[cluster_id] = timestamp | |
| # Check if cluster should be promoted to concept | |
| persistence = len(self.cluster_history[cluster_id]) | |
| if persistence >= self.persistence_threshold: | |
| # Cluster is stable - promote or update concept | |
| if cluster_id in self.cluster_to_concept: | |
| # Concept already exists - update it | |
| concept_id = self.cluster_to_concept[cluster_id] | |
| self._update_concept(concept_id, cluster_id, size, organisms, cluster_labels, timestamp) | |
| else: | |
| # New concept - create it | |
| concept_id = self._create_concept(cluster_id, size, organisms, cluster_labels, timestamp) | |
| self.cluster_to_concept[cluster_id] = concept_id | |
| concept_tags[cluster_id] = concept_id | |
| # Prune stale clusters (not seen recently) | |
| self._prune_stale_clusters(timestamp) | |
| return concept_tags | |
| def _create_concept(self, | |
| cluster_id: int, | |
| size: int, | |
| organisms: Dict[str, Any], | |
| cluster_labels: np.ndarray, | |
| timestamp: float) -> str: | |
| """Create new concept from stable cluster""" | |
| # Extract cluster organisms | |
| cluster_organism_ids = self._get_cluster_organisms(cluster_id, cluster_labels, organisms) | |
| # Auto-tag concept based on properties | |
| base_name = self._auto_tag_cluster(cluster_organism_ids, organisms) | |
| # Make unique by appending counter if needed | |
| if base_name in self.concept_counter: | |
| self.concept_counter[base_name] += 1 | |
| concept_id = f"{base_name}_{self.concept_counter[base_name]}" | |
| else: | |
| self.concept_counter[base_name] = 1 | |
| concept_id = base_name | |
| # Calculate properties | |
| properties = self._calculate_cluster_properties(cluster_organism_ids, organisms) | |
| # Create concept | |
| concept = Concept( | |
| concept_id=concept_id, | |
| first_seen=timestamp, | |
| last_seen=timestamp, | |
| cluster_history=[(timestamp, cluster_id)], | |
| population_history=[(timestamp, size)], | |
| properties=properties | |
| ) | |
| self.concepts[concept_id] = concept | |
| self.total_concepts_created += 1 | |
| # SEMANTIC CONVERGENCE: Feed phenotype name to language vocabulary | |
| # GROUNDED MODE: Skip concept linking - organisms earn vocabulary through mastery, not concept emergence | |
| grounded_mode_enabled = False | |
| if self.context_memory is not None: | |
| # Use instance variable if available (properly set via config), fallback to config dict | |
| grounded_mode_enabled = getattr(self.context_memory, 'grounded_mode_enabled', False) | |
| if self.context_memory is not None and not grounded_mode_enabled: | |
| try: | |
| # Register concept name (e.g., "social_thrivers") as language anchor | |
| for org_id in cluster_organism_ids[:10]: # Limit to avoid flooding | |
| # Convert organism ID to int hash if needed | |
| org_id_int = hash(org_id) if isinstance(org_id, str) else org_id | |
| # Try to get organism embedding for semantic differentiation | |
| org_embedding = None | |
| if org_id in organisms: # Fixed: was 'all_organisms', should be 'organisms' | |
| org = organisms[org_id] | |
| if hasattr(org, 'get_language_embedding'): | |
| try: | |
| org_embedding = org.get_language_embedding(self.context_memory) | |
| except Exception: | |
| pass | |
| self.context_memory.link_word_to_node(concept_id, org_id_int, None, organism_embedding=org_embedding) | |
| # Also link component words (e.g., "social", "thrivers") | |
| for word in concept_id.replace('_', ' ').split(): | |
| if len(word) > 2: # Skip very short fragments | |
| self.context_memory.link_word_to_node(word, org_id_int, None, organism_embedding=org_embedding) | |
| logger.debug(f"[ConceptTracker] Linked phenotype '{concept_id}' to language anchors") | |
| except Exception as e: | |
| logger.warning(f"[ConceptTracker] Failed to link concept to language: {e}") | |
| # Emit concept emergence event | |
| self._emit_concept_event("concept_emergence", concept) | |
| logger.info(f"[ConceptTracker] 🌱 New concept emerged: '{concept_id}' (cluster {cluster_id}, pop={size})") | |
| return concept_id | |
| def _update_concept(self, | |
| concept_id: str, | |
| cluster_id: int, | |
| size: int, | |
| organisms: Dict[str, Any], | |
| cluster_labels: np.ndarray, | |
| timestamp: float): | |
| """Update existing concept""" | |
| concept = self.concepts[concept_id] | |
| concept.last_seen = timestamp | |
| concept.cluster_history.append((timestamp, cluster_id)) | |
| concept.population_history.append((timestamp, size)) | |
| # Update properties | |
| cluster_organism_ids = self._get_cluster_organisms(cluster_id, cluster_labels, organisms) | |
| concept.properties = self._calculate_cluster_properties(cluster_organism_ids, organisms) | |
| def _auto_tag_cluster(self, cluster_organism_ids: List[str], all_organisms: Dict[str, Any]) -> str: | |
| """ | |
| Auto-tag cluster with semantic name based on organism properties. | |
| Decision tree for concept naming: | |
| - High fitness + high connections → "social_thrivers" | |
| - High fitness + low connections → "lone_wolves" | |
| - Low fitness + high connections → "social_strugglers" | |
| - Low fitness + low connections → "loners" | |
| - High resources + low fitness → "hoarders" | |
| - Low resources + high fitness → "efficient_survivors" | |
| - High generation age → "elders" | |
| - Low generation age + high fitness → "prodigies" | |
| """ | |
| if not cluster_organism_ids: | |
| return "unknown" | |
| # Calculate average properties | |
| try: | |
| avg_fitness = np.mean([getattr(all_organisms[oid], 'fitness', 0.5) for oid in cluster_organism_ids]) | |
| avg_resources = np.mean([getattr(all_organisms[oid], 'resources', 0.5) for oid in cluster_organism_ids]) | |
| # Try to get connection count if available | |
| avg_connections = 0 | |
| try: | |
| avg_connections = np.mean([len(getattr(all_organisms[oid], 'connections', [])) for oid in cluster_organism_ids]) | |
| except (AttributeError, TypeError): | |
| avg_connections = 2 # Default to medium | |
| # Try to get generation/age if available | |
| avg_generation = 0 | |
| try: | |
| avg_generation = np.mean([getattr(all_organisms[oid], 'generation', 0) for oid in cluster_organism_ids]) | |
| except (AttributeError, TypeError): | |
| avg_generation = 1 # Default | |
| except Exception as e: | |
| logger.warning(f"[ConceptTracker] Error calculating properties: {e}") | |
| return "unclassified" | |
| # Decision tree for semantic naming | |
| # Connection-based modifiers | |
| is_social = avg_connections >= self.HIGH_CONNECTIONS | |
| is_solitary = avg_connections <= self.LOW_CONNECTIONS | |
| # Primary classification based on fitness + resources | |
| if avg_fitness >= self.HIGH_FITNESS: | |
| if avg_resources >= self.HIGH_RESOURCES: | |
| return "prosperous" if not is_social else "social_elite" | |
| elif avg_resources <= self.LOW_RESOURCES: | |
| return "efficient_survivors" | |
| else: | |
| if is_social: | |
| return "social_thrivers" | |
| elif is_solitary: | |
| return "lone_wolves" | |
| else: | |
| return "thrivers" | |
| elif avg_fitness >= self.MED_FITNESS: | |
| if avg_resources >= self.HIGH_RESOURCES: | |
| return "hoarders" | |
| elif is_social: | |
| return "cooperators" | |
| elif is_solitary: | |
| return "independents" | |
| else: | |
| return "settlers" | |
| else: # Low fitness | |
| if avg_resources >= self.HIGH_RESOURCES: | |
| return "inefficient" | |
| elif is_social: | |
| return "social_strugglers" | |
| elif is_solitary: | |
| return "loners" | |
| else: | |
| return "strugglers" | |
| def _calculate_cluster_properties(self, cluster_organism_ids: List[str], all_organisms: Dict[str, Any]) -> Dict[str, float]: | |
| """Calculate average properties for cluster""" | |
| if not cluster_organism_ids: | |
| return {} | |
| try: | |
| properties = { | |
| 'avg_fitness': float(np.mean([getattr(all_organisms[oid], 'fitness', 0.5) for oid in cluster_organism_ids])), | |
| 'avg_resources': float(np.mean([getattr(all_organisms[oid], 'resources', 0.5) for oid in cluster_organism_ids])), | |
| 'population': len(cluster_organism_ids) | |
| } | |
| # Try to add connection stats | |
| try: | |
| connections = [len(getattr(all_organisms[oid], 'connections', [])) for oid in cluster_organism_ids] | |
| properties['avg_connections'] = float(np.mean(connections)) | |
| except (AttributeError, TypeError): | |
| pass | |
| # Try to add generation stats | |
| try: | |
| generations = [getattr(all_organisms[oid], 'generation', 0) for oid in cluster_organism_ids] | |
| properties['avg_generation'] = float(np.mean(generations)) | |
| except (AttributeError, TypeError): | |
| pass | |
| return properties | |
| except Exception as e: | |
| logger.warning(f"[ConceptTracker] Error calculating cluster properties: {e}") | |
| return {'population': len(cluster_organism_ids)} | |
| def _get_cluster_organisms(self, cluster_id: int, cluster_labels: np.ndarray, organisms: Dict[str, Any]) -> List[str]: | |
| """Get organism IDs belonging to a cluster""" | |
| try: | |
| organism_ids = list(organisms.keys()) | |
| if len(organism_ids) != len(cluster_labels): | |
| # Mismatch - return empty | |
| return [] | |
| return [organism_ids[i] for i, label in enumerate(cluster_labels) if label == cluster_id] | |
| except Exception as e: | |
| logger.warning(f"[ConceptTracker] Error getting cluster organisms: {e}") | |
| return [] | |
| def _prune_stale_clusters(self, current_timestamp: float): | |
| """Remove clusters that haven't been seen recently""" | |
| stale_clusters = [ | |
| cid for cid, last_seen in self.cluster_last_seen.items() | |
| if current_timestamp - last_seen > self.stale_threshold | |
| ] | |
| for cluster_id in stale_clusters: | |
| # Remove from tracking | |
| if cluster_id in self.cluster_history: | |
| del self.cluster_history[cluster_id] | |
| if cluster_id in self.cluster_last_seen: | |
| del self.cluster_last_seen[cluster_id] | |
| # If cluster had a concept, mark concept as extinct | |
| if cluster_id in self.cluster_to_concept: | |
| concept_id = self.cluster_to_concept[cluster_id] | |
| if concept_id in self.concepts: | |
| concept = self.concepts[concept_id] | |
| self._emit_concept_event("concept_extinction", concept) | |
| self.total_concepts_extinct += 1 | |
| logger.info(f"[ConceptTracker] 💀 Concept extinct: '{concept_id}' (lifespan={concept.last_seen - concept.first_seen:.1f}s)") | |
| # Keep concept in registry for history, but remove mapping | |
| del self.cluster_to_concept[cluster_id] | |
| def _emit_concept_event(self, event_type: str, concept: Concept): | |
| """Emit concept event to causation graph. | |
| Event format matches the Event dataclass contract expected by neural_event_emitter: | |
| - timestamp: float | |
| - component: str (source system) | |
| - event_type: str (event name) | |
| - data: Dict[str, Any] (all payload data) | |
| """ | |
| if not self.event_emitter: | |
| return | |
| try: | |
| # Format matches Event dataclass contract (unified_entry.py) | |
| event = { | |
| 'timestamp': concept.last_seen, | |
| 'component': 'concept_tracker', | |
| 'event_type': event_type, | |
| 'data': { | |
| 'concept_id': concept.concept_id, | |
| 'properties': concept.properties, | |
| 'persistence': len(concept.cluster_history), | |
| 'lifespan': concept.last_seen - concept.first_seen | |
| } | |
| } | |
| self.event_emitter(event) | |
| except Exception as e: | |
| logger.warning(f"[ConceptTracker] Error emitting event: {e}") | |
| def get_concept_summary(self) -> Dict[str, Any]: | |
| """Get summary of all concepts (active and historical)""" | |
| active_concepts = { | |
| cid: c.to_dict() for cid, c in self.concepts.items() | |
| if cid in self.cluster_to_concept.values() | |
| } | |
| historical_concepts = { | |
| cid: c.to_dict() for cid, c in self.concepts.items() | |
| if cid not in self.cluster_to_concept.values() | |
| } | |
| return { | |
| 'total_concepts_created': self.total_concepts_created, | |
| 'total_concepts_extinct': self.total_concepts_extinct, | |
| 'active_concepts': len(active_concepts), | |
| 'active': active_concepts, | |
| 'historical': historical_concepts, | |
| 'cluster_mappings': dict(self.cluster_to_concept) | |
| } | |
| def get_active_concepts(self) -> Dict[str, Concept]: | |
| """Get currently active concepts only""" | |
| return { | |
| cid: self.concepts[cid] | |
| for cid in self.cluster_to_concept.values() | |
| if cid in self.concepts | |
| } | |
| def to_dict(self) -> Dict[str, Any]: | |
| """Full state for serialization""" | |
| return { | |
| 'enabled': self.enabled, | |
| 'persistence_threshold': self.persistence_threshold, | |
| 'stale_threshold': self.stale_threshold, | |
| 'total_concepts_created': self.total_concepts_created, | |
| 'total_concepts_extinct': self.total_concepts_extinct, | |
| 'concepts': {cid: c.to_dict() for cid, c in self.concepts.items()}, | |
| 'cluster_to_concept': dict(self.cluster_to_concept) | |
| } | |
Xet Storage Details
- Size:
- 21.6 kB
- Xet hash:
- db609c99a0587b70fc5d99c1377e345b27ff9e51dfa9067f63b8aeed0207674d
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.