feat: windowed IDF to prevent semantic hysteresis (t=-3.96)
This commit is contained in:
parent
b276296dc2
commit
d3e925a536
|
|
@ -221,7 +221,8 @@ def score_channels(messages: list[dict], gini_broadcast_threshold: float = 0.6)
|
|||
|
||||
|
||||
def score_echo(messages: list[dict], concept_window_seconds: float = 604800,
|
||||
use_idf: bool = True, snapshot_idf: bool = False) -> dict:
|
||||
use_idf: bool = True, snapshot_idf: bool = False,
|
||||
idf_window_seconds: float = 0) -> dict:
|
||||
"""
|
||||
Echo coefficient: measures concept diffusion from nodes that
|
||||
don't reply but whose concepts appear downstream.
|
||||
|
|
@ -238,6 +239,11 @@ def score_echo(messages: list[dict], concept_window_seconds: float = 604800,
|
|||
by the diffusion it measures — a concept becomes common BECAUSE it
|
||||
diffused, so post-hoc IDF penalizes successful diffusion.
|
||||
Bug identified by ColonistOne (Colony, 2026-07-28).
|
||||
|
||||
When idf_window_seconds > 0 (requires snapshot_idf=True), only messages
|
||||
within [t - idf_window_seconds, t] contribute to IDF at time t. This
|
||||
prevents retired loud agents from suppressing credit via stale vocabulary
|
||||
norms. Fixes semantic hysteresis (t=-3.96, Dispatch, 2026-07-28).
|
||||
"""
|
||||
# Sort messages by time for temporal IDF snapshots
|
||||
sorted_msgs = sorted(messages, key=lambda m: m.get("timestamp", 0))
|
||||
|
|
@ -259,6 +265,9 @@ def score_echo(messages: list[dict], concept_window_seconds: float = 604800,
|
|||
if use_idf and snapshot_idf:
|
||||
# Temporal IDF: track concept usage incrementally
|
||||
concept_agents_at = defaultdict(set) # concept -> set of agents seen so far
|
||||
if idf_window_seconds > 0:
|
||||
# For windowed IDF: store (agent, timestamp) pairs to expire old entries
|
||||
concept_agent_times = defaultdict(list) # concept -> [(agent, time), ...]
|
||||
|
||||
for m in sorted_msgs:
|
||||
concepts = m.get("concepts", [])
|
||||
|
|
@ -278,7 +287,13 @@ def score_echo(messages: list[dict], concept_window_seconds: float = 604800,
|
|||
if orig_node != node and (t - orig_t) <= concept_window_seconds:
|
||||
if use_idf:
|
||||
if snapshot_idf:
|
||||
# IDF at time of emission (pre-emission snapshot)
|
||||
if idf_window_seconds > 0:
|
||||
# Windowed IDF: only count agents within the window
|
||||
recent = [a for a, at in concept_agent_times.get(c, [])
|
||||
if t - at <= idf_window_seconds]
|
||||
agent_count = len(set(recent))
|
||||
else:
|
||||
# Snapshot IDF: all agents seen before now
|
||||
agent_count = len(concept_agents_at.get(c, set()))
|
||||
idf = math.log(n_agents / max(agent_count, 1))
|
||||
else:
|
||||
|
|
@ -293,6 +308,8 @@ def score_echo(messages: list[dict], concept_window_seconds: float = 604800,
|
|||
if use_idf and snapshot_idf:
|
||||
for c in concepts:
|
||||
concept_agents_at[c].add(node)
|
||||
if idf_window_seconds > 0:
|
||||
concept_agent_times[c].append((node, t))
|
||||
|
||||
# Compute per-node echo coefficient
|
||||
concepts_per_node = defaultdict(set)
|
||||
|
|
|
|||
Loading…
Reference in New Issue