Lattice_Spike / evaluation /protocols.py
Md Saiful Islam Sajol
Upload Lattice_Spike project with full LFS weights
e2c9d42 verified
Raw History Blame Contribute Delete
4.93 kB
"""Versioned evaluation protocols and canonical comparison support.
The reportable token-level comparison is deliberately narrower than the raw
prediction artifact: it uses answer-token indices ``t >= 1`` with binary G/H
labels and valid finite scores. Token zero, uncertain labels, and invalid score
slots remain in artifacts so exclusions are auditable.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass
from enum import Enum
import hashlib
import json
from math import isfinite
from typing import Iterable, Sequence
PREDICTION_SCHEMA_VERSION = 2
class ProtocolSuite(str, Enum):
"""Top-level evaluation suites; each method also records a protocol id."""
CAUSAL_MATCHED = "causal_matched"
METHOD_FAITHFUL = "method_faithful"
OFFLINE = "offline"
LEGACY_FAIR_V0 = "legacy_fair_v0"
@dataclass(frozen=True)
class EvaluationProtocol:
"""Machine-readable method protocol bound into predictions and reports."""
suite: str
protocol_id: str
unit: str
information_horizon: str
label_semantics: str
token_zero_policy: str
uncertain_policy: str
prediction_schema_version: int = PREDICTION_SCHEMA_VERSION
def to_json_dict(self) -> dict:
return asdict(self)
@dataclass(frozen=True, order=True)
class SupportEntry:
"""One canonical token in the common comparison population."""
row_index: int
row_id: str | None
token_index: int
token_id: int
label: int
def to_json_dict(self) -> dict:
return {
"row_index": int(self.row_index),
"row_id": self.row_id,
"token_index": int(self.token_index),
"token_id": int(self.token_id),
"label": int(self.label),
}
def build_comparison_mask(
labels: Sequence[int] | None,
*,
token_indices: Sequence[int] | None = None,
score_valid_mask: Sequence[bool] | None = None,
exclude_token_zero: bool = True,
) -> list[bool]:
"""Build the reportable G/H support mask without dropping artifact slots."""
if labels is None:
return []
n = len(labels)
indices = list(range(n)) if token_indices is None else [int(v) for v in token_indices]
valid = [True] * n if score_valid_mask is None else [bool(v) for v in score_valid_mask]
if len(indices) != n or len(valid) != n:
raise ValueError("labels, token_indices, and score_valid_mask must align")
mask = []
for label, token_index, score_valid in zip(labels, indices, valid):
label = int(label)
if label not in {0, 1, 2}:
raise ValueError("labels must contain only 0/1/2 values")
mask.append(
bool(score_valid)
and label in {0, 1}
and (not exclude_token_zero or token_index >= 1)
)
return mask
def support_entries(
*,
row_index: int,
row_id: str | None,
token_ids: Sequence[int],
labels: Sequence[int],
token_indices: Sequence[int] | None = None,
comparison_mask: Sequence[bool] | None = None,
) -> list[SupportEntry]:
"""Construct canonical entries for one prediction row."""
n = len(labels)
indices = list(range(n)) if token_indices is None else [int(v) for v in token_indices]
mask = (
build_comparison_mask(labels, token_indices=indices)
if comparison_mask is None
else [bool(v) for v in comparison_mask]
)
if len(token_ids) != n or len(indices) != n or len(mask) != n:
raise ValueError("token ids, labels, indices, and comparison mask must align")
return [
SupportEntry(row_index, row_id, token_index, int(token_id), int(label))
for token_id, label, token_index, keep in zip(token_ids, labels, indices, mask)
if keep
]
def support_hash(entries: Iterable[SupportEntry]) -> str:
"""SHA256 of deterministically serialized, canonically sorted support."""
payload = [entry.to_json_dict() for entry in sorted(entries)]
encoded = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(encoded.encode("utf-8")).hexdigest()
def support_summary(entries: Iterable[SupportEntry]) -> dict:
values = list(entries)
labels = [entry.label for entry in values]
return {
"n_tokens": len(values),
"n_grounded": labels.count(0),
"n_hallucinated": labels.count(1),
"positive_prevalence": labels.count(1) / len(labels) if labels else None,
"support_sha256": support_hash(values),
}
def validate_finite_scores(scores: Sequence[float], mask: Sequence[bool]) -> None:
if len(scores) != len(mask):
raise ValueError("scores and mask must align")
bad = [index for index, (score, keep) in enumerate(zip(scores, mask)) if keep and not isfinite(float(score))]
if bad:
raise ValueError(f"comparison support contains non-finite scores at indices {bad[:10]}")