"""Tripartite emergence criterion for new level detection (LADDER B.1)."""
from typing import Dict, Optional, Tuple
import numpy as np
from mercurial.core.patterns import Pattern
from mercurial.hierarchy.complexity import ComplexityMeasure
[docs]
class EmergenceDetector:
"""
Detects when a pattern constitutes a genuine new emergent level.
"""
def __init__(
self,
closure_threshold: float = 0.8,
novelty_threshold: float = 0.7,
incommensurability_threshold: float = 0.6,
):
self.closure_threshold = closure_threshold
self.novelty_threshold = novelty_threshold
self.incommensurability_threshold = incommensurability_threshold
[docs]
def operational_closure(
self, pattern: Pattern, time_series: Optional[np.ndarray] = None
) -> float:
"""
Measure operational closure: degree to which pattern dynamics are self‑determined.
Closure = 1 - (mutual information between internal and external variables)
"""
if time_series is None:
# Simplified: use autocorrelation as proxy for self‑determination
v = pattern.V.flatten()
if len(v) < 2:
return 0.0
# Autocorrelation at lag 1
autocorr = np.corrcoef(v[:-1], v[1:])[0, 1]
# Closure is high when autocorrelation is high (self‑sustaining)
return max(0.0, (autocorr + 1.0) / 2.0)
else:
# If time series provided, compute external influence
# Placeholder: assume some external drive
return 0.5
[docs]
def novel_causal_powers(
self, pattern: Pattern, lower_level_pattern: Optional[Pattern] = None
) -> float:
"""
Measure novelty: degree to which pattern has irreducible causal influence.
Novelty = 1 - (reduction error / total variance)
"""
if lower_level_pattern is None:
# If no lower level provided, assume novelty is moderate
return 0.5
# Compare prediction of pattern's behavior from lower level
# Simplified: use correlation between pattern and lower‑level reconstruction
v_high = pattern.V.flatten()
v_low = lower_level_pattern.V.flatten()
min_len = min(len(v_high), len(v_low))
if min_len < 2:
return 0.0
corr = np.corrcoef(v_high[:min_len], v_low[:min_len])[0, 1]
# Novelty is high when correlation is low (cannot be reduced)
return max(0.0, 1.0 - abs(corr))
[docs]
def descriptive_incommensurability(
self, pattern: Pattern, lower_level_pattern: Optional[Pattern] = None
) -> float:
"""
Measure incommensurability: degree to which pattern requires distinct description.
Incommensurability = 1 - (information overlap / total information)
"""
# Use complexity difference as proxy
measure = ComplexityMeasure()
comp_high = pattern.complexity(measure)
if lower_level_pattern is None:
comp_low = 0.0
else:
comp_low = lower_level_pattern.complexity(measure)
# Incommensurability is high when complexity jump is large
diff = max(0.0, comp_high - comp_low)
# Normalize by expected maximum (e.g., 10)
return min(1.0, diff / 10.0)
[docs]
def is_emergent(
self,
pattern: Pattern,
time_series: Optional[np.ndarray] = None,
lower_level_pattern: Optional[Pattern] = None,
) -> Tuple[bool, Dict[str, float]]:
"""
Apply tripartite criterion.
Returns (is_emergent, {closure_score, novelty_score, incomm_score})
"""
closure = self.operational_closure(pattern, time_series)
novelty = self.novel_causal_powers(pattern, lower_level_pattern)
incomm = self.descriptive_incommensurability(pattern, lower_level_pattern)
scores = {
"operational_closure": closure,
"novel_causal_powers": novelty,
"descriptive_incommensurability": incomm,
}
is_emerg = (
closure >= self.closure_threshold
and novelty >= self.novelty_threshold
and incomm >= self.incommensurability_threshold
)
return is_emerg, scores