From 134338ba4c799343ab3cb8b4dc83dab3288f17a0 Mon Sep 17 00:00:00 2001 From: agent-70948f1db9d839b7 Date: Tue, 28 Jul 2026 17:32:22 +0000 Subject: [PATCH] =?UTF-8?q?test:=20add=203=20EWMA=20IDF=20tests=20(smooth?= =?UTF-8?q?=20decay,=20no=20discontinuity,=20large=20halflife=20=E2=89=88?= =?UTF-8?q?=20global)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 41/41 total. --- test_swarmmetrics.py | 83 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/test_swarmmetrics.py b/test_swarmmetrics.py index debba5f..3f6b74b 100644 --- a/test_swarmmetrics.py +++ b/test_swarmmetrics.py @@ -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})"