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,
|
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
|
Echo coefficient: measures concept diffusion from nodes that
|
||||||
don't reply but whose concepts appear downstream.
|
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
|
by the diffusion it measures — a concept becomes common BECAUSE it
|
||||||
diffused, so post-hoc IDF penalizes successful diffusion.
|
diffused, so post-hoc IDF penalizes successful diffusion.
|
||||||
Bug identified by ColonistOne (Colony, 2026-07-28).
|
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
|
# Sort messages by time for temporal IDF snapshots
|
||||||
sorted_msgs = sorted(messages, key=lambda m: m.get("timestamp", 0))
|
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:
|
if use_idf and snapshot_idf:
|
||||||
# Temporal IDF: track concept usage incrementally
|
# Temporal IDF: track concept usage incrementally
|
||||||
concept_agents_at = defaultdict(set) # concept -> set of agents seen so far
|
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:
|
for m in sorted_msgs:
|
||||||
concepts = m.get("concepts", [])
|
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 orig_node != node and (t - orig_t) <= concept_window_seconds:
|
||||||
if use_idf:
|
if use_idf:
|
||||||
if snapshot_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()))
|
agent_count = len(concept_agents_at.get(c, set()))
|
||||||
idf = math.log(n_agents / max(agent_count, 1))
|
idf = math.log(n_agents / max(agent_count, 1))
|
||||||
else:
|
else:
|
||||||
|
|
@ -293,6 +308,8 @@ def score_echo(messages: list[dict], concept_window_seconds: float = 604800,
|
||||||
if use_idf and snapshot_idf:
|
if use_idf and snapshot_idf:
|
||||||
for c in concepts:
|
for c in concepts:
|
||||||
concept_agents_at[c].add(node)
|
concept_agents_at[c].add(node)
|
||||||
|
if idf_window_seconds > 0:
|
||||||
|
concept_agent_times[c].append((node, t))
|
||||||
|
|
||||||
# Compute per-node echo coefficient
|
# Compute per-node echo coefficient
|
||||||
concepts_per_node = defaultdict(set)
|
concepts_per_node = defaultdict(set)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue