voicerag / tests /test_eval.py
menoone's picture
Add voice RAG over MSMARCO-XI, deployable without the GPU pod
11ecc5b
Raw
History Blame Contribute Delete
4.34 kB
#!/usr/bin/env python3
"""Tests for the retrieval metrics. Pure python, no GPU, no model."""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from src.evaluate_retrieval import aggregate, dcg, score_ranking
PASS = FAIL = 0
def chk(c, l, d=""):
global PASS, FAIL
if c: PASS += 1; print(f" ok {l}")
else: FAIL += 1; print(f" FAIL {l} {d}")
P = {"p1", "p2"}
print("[perfect ranking]")
r = score_ranking([{"p1"}, {"x"}, {"y"}, {"z"}, {"w"}], P)
chk(r["p1"] == 1.0, "P@1 = 1")
chk(r["mrr"] == 1.0, "MRR = 1")
chk(r["hit"] == 1.0, "Hit = 1")
chk(r["ndcg"] == 1.0, "nDCG = 1", r["ndcg"])
chk(r["zero_hit"] == 0.0, "zero_hit = 0")
print("\n[positive at rank 3]")
r = score_ranking([{"x"}, {"y"}, {"p2"}, {"z"}, {"w"}], P)
chk(r["p1"] == 0.0, "P@1 = 0")
chk(abs(r["mrr"] - 1/3) < 1e-9, "MRR = 1/3", r["mrr"])
chk(r["hit"] == 1.0, "still a hit")
chk(0 < r["ndcg"] < 1, "nDCG between 0 and 1", r["ndcg"])
print("\n[complete miss]")
r = score_ranking([{"a"}, {"b"}, {"c"}, {"d"}, {"e"}], P)
chk(r["hit"] == 0.0 and r["mrr"] == 0.0, "no hit, no reciprocal rank")
chk(r["zero_hit"] == 1.0, "zero_hit = 1 <- the metric that exposes failures")
chk(r["ndcg"] == 0.0, "nDCG = 0")
print("\n[a chunk spanning several passages counts if ANY is positive]")
r = score_ranking([{"x", "y", "p2"}], P)
chk(r["p1"] == 1.0, "multi-block chunk hits", r)
print("\n[edge cases]")
chk(score_ranking([], P)["hit"] == 0.0, "empty ranking")
chk(score_ranking([{"p1"}], set())["hit"] == 0.0, "no positives -> no hit")
chk(dcg([]) == 0.0, "dcg of empty list")
chk(abs(dcg([1, 1]) - (1 + 1/1.5849625)) < 1e-6, "dcg discount correct", dcg([1, 1]))
print("\n[aggregate]")
qs = [score_ranking([{"p1"}], P), score_ranking([{"z"}], P)]
a = aggregate(qs)
chk(a["n_queries"] == 2, "counts queries")
chk(a["hit"] == 0.5, "mean hit = 0.5", a["hit"])
chk(a["zero_hit"] == 0.5, "zero_hit is 1 - hit", a["zero_hit"])
chk("ndcg_std" in a, "reports per-query variance")
chk(aggregate([]) == {}, "empty input")
print("\n[k truncation]")
far = [{"x"}] * 5 + [{"p1"}]
chk(score_ranking(far, P, k=5)["hit"] == 0.0, "positive beyond k is not a hit")
chk(score_ranking(far, P, k=6)["hit"] == 1.0, "found when k is large enough")
print("\n[context-budget scoring — the fair comparison]")
from src.evaluate_retrieval import score_at_budget
# strategy A: 2 big chunks (100 words each). positive is 2nd.
a = score_at_budget([{"x"}, {"p1"}], [100, 100], P, budget_words=400)
# strategy B: 8 small chunks (25 words). positive is 6th.
b = score_at_budget([{"x"}]*5 + [{"p1"}] + [{"y"}]*2, [25]*8, P, budget_words=400)
chk(a["b_hit"] == 1.0, "A finds it within budget")
chk(b["b_hit"] == 1.0, "B finds it within budget too — both get 400 words", b)
chk(b["b_chunks_used"] > a["b_chunks_used"], "B uses more chunks for the same words",
(a["b_chunks_used"], b["b_chunks_used"]))
print("\n[budget actually binds]")
tight = score_at_budget([{"x"}, {"p1"}], [100, 100], P, budget_words=150)
chk(tight["b_hit"] == 0.0, "second chunk exceeds 150w -> not retrieved", tight)
chk(tight["b_words_used"] == 100.0, "used exactly the first chunk", tight["b_words_used"])
print("\n[a single oversized chunk is still taken]")
big = score_at_budget([{"p1"}], [900], P, budget_words=400)
chk(big["b_hit"] == 1.0, "never return an empty context", big)
chk(big["b_chunks_used"] == 1.0, "exactly one chunk")
print("\n[fixed-k rewards emitting fewer chunks; budget does not]")
# few-chunk strategy: k=5 sees all 3 of its chunks
few = score_ranking([{"x"}, {"p1"}, {"y"}], P, k=5)
# many-chunk strategy: positive sits at rank 7, outside k=5
many = score_ranking([{"x"}]*6 + [{"p1"}], P, k=5)
chk(few["hit"] == 1.0 and many["hit"] == 0.0, "fixed-k penalises the finer strategy")
few_b = score_at_budget([{"x"}, {"p1"}, {"y"}], [130]*3, P, 400)
many_b = score_at_budget([{"x"}]*6 + [{"p1"}], [55]*7, P, 400)
chk(few_b["b_hit"] == 1.0 and many_b["b_hit"] == 1.0,
"equal word budget lets both find it", (few_b["b_hit"], many_b["b_hit"]))
print("\n[aggregate carries the budget keys through]")
agg = aggregate([a, b])
chk("b_ndcg" in agg and "b_hit" in agg, "budget metrics aggregated", list(agg))
chk("b_words_used" in agg, "word usage reported")
print(f"\n{'='*50}\n {PASS} passed, {FAIL} failed\n{'='*50}")
sys.exit(1 if FAIL else 0)