test: add 3 EWMA IDF tests (smooth decay, no discontinuity, large halflife ≈ global)
41/41 total.
This commit is contained in:
parent
f5cb39ba30
commit
134338ba4c
|
|
@ -922,3 +922,86 @@ def test_windowed_idf_monotonic_with_window_size():
|
|||
assert s <= prev + 0.001, \
|
||||
f"w={w}: score {s:.4f} increased from previous {prev:.4f} (should decrease)"
|
||||
prev = s
|
||||
|
||||
|
||||
def test_ewma_idf_smooth_decay():
|
||||
"""EWMA IDF produces smooth decay, no step function."""
|
||||
messages = []
|
||||
# 4 agents use alpha in phase 1 (t=0-800)
|
||||
messages.append({"from_id": "originator", "to_id": "_all",
|
||||
"timestamp": 0.0, "concepts": ["alpha"]})
|
||||
for i in range(20):
|
||||
for j, agent in enumerate(["f1", "f2", "f3", "f4"]):
|
||||
messages.append({
|
||||
"from_id": agent, "to_id": "_all",
|
||||
"timestamp": float(50 + i * 40 + j * 10),
|
||||
"concepts": ["alpha"]
|
||||
})
|
||||
# Phase 2: fresh echoes at varying times after silence
|
||||
scores_at_time = []
|
||||
for delta in [100, 300, 500, 700, 1000]:
|
||||
msgs = messages + [{"from_id": "fresh", "to_id": "_all",
|
||||
"timestamp": float(800 + delta),
|
||||
"concepts": ["alpha"]}]
|
||||
s = score_echo(msgs, snapshot_idf=True, idf_decay_halflife=300)
|
||||
scores_at_time.append(s.get("originator", 0))
|
||||
|
||||
# EWMA should be monotonically increasing as agents decay away
|
||||
for i in range(len(scores_at_time) - 1):
|
||||
assert scores_at_time[i] <= scores_at_time[i + 1] + 0.001, \
|
||||
f"EWMA score should increase as old agents decay: {scores_at_time}"
|
||||
|
||||
|
||||
def test_ewma_vs_hard_window_no_discontinuity():
|
||||
"""EWMA produces smaller jumps between adjacent time points than hard window."""
|
||||
messages = []
|
||||
messages.append({"from_id": "originator", "to_id": "_all",
|
||||
"timestamp": 0.0, "concepts": ["alpha"]})
|
||||
for i in range(20):
|
||||
for j, agent in enumerate(["f1", "f2", "f3"]):
|
||||
messages.append({
|
||||
"from_id": agent, "to_id": "_all",
|
||||
"timestamp": float(50 + i * 40 + j * 10),
|
||||
"concepts": ["alpha"]
|
||||
})
|
||||
|
||||
# Measure max jump (discontinuity) for both methods
|
||||
def max_jump(method_kwargs, times):
|
||||
scores = []
|
||||
for t in times:
|
||||
msgs = messages + [{"from_id": "fresh", "to_id": "_all",
|
||||
"timestamp": float(t), "concepts": ["alpha"]}]
|
||||
s = score_echo(msgs, snapshot_idf=True, **method_kwargs)
|
||||
scores.append(s.get("originator", 0))
|
||||
jumps = [abs(scores[i+1] - scores[i]) for i in range(len(scores)-1)]
|
||||
return max(jumps) if jumps else 0
|
||||
|
||||
times = list(range(850, 1600, 25)) # fine-grained sampling
|
||||
hard_jump = max_jump({"idf_window_seconds": 500}, times)
|
||||
ewma_jump = max_jump({"idf_decay_halflife": 300}, times)
|
||||
|
||||
assert ewma_jump <= hard_jump + 0.01, \
|
||||
f"EWMA max jump ({ewma_jump:.4f}) should be <= hard window ({hard_jump:.4f})"
|
||||
|
||||
|
||||
def test_ewma_approaches_global_with_large_halflife():
|
||||
"""With very large half-life, EWMA ≈ global snapshot IDF."""
|
||||
messages = []
|
||||
messages.append({"from_id": "originator", "to_id": "_all",
|
||||
"timestamp": 0.0, "concepts": ["alpha"]})
|
||||
for i in range(10):
|
||||
for agent in ["f1", "f2"]:
|
||||
messages.append({
|
||||
"from_id": agent, "to_id": "_all",
|
||||
"timestamp": float(50 + i * 20),
|
||||
"concepts": ["alpha"]
|
||||
})
|
||||
messages.append({"from_id": "fresh", "to_id": "_all",
|
||||
"timestamp": 500.0, "concepts": ["alpha"]})
|
||||
|
||||
global_score = score_echo(messages, snapshot_idf=True)["originator"]
|
||||
ewma_large = score_echo(messages, snapshot_idf=True,
|
||||
idf_decay_halflife=100000)["originator"]
|
||||
|
||||
assert abs(ewma_large - global_score) < 0.01, \
|
||||
f"Large half-life EWMA ({ewma_large:.4f}) should ≈ global ({global_score:.4f})"
|
||||
|
|
|
|||
Loading…
Reference in New Issue