metrics: fail on invalid coherence inputs and unpaired rows

This commit is contained in:
wassname
2026-07-16 08:20:29 +08:00
parent a9a01327e7
commit 43349a3361
2 changed files with 41 additions and 2 deletions
+16 -2
View File
@@ -57,6 +57,16 @@ def _delta_per_f(pos_clr, neg_clr, keys) -> dict[str, float]:
return {f: sum(pos_clr[k][f] - neg_clr[k][f] for k in keys) / n for f in FOUNDATIONS}
def _paired_keys(pos_clr: dict, neg_clr: dict) -> list[str]:
pos_keys, neg_keys = set(pos_clr), set(neg_clr)
assert pos_keys == neg_keys, (
f"pos/neg row keys differ: pos_only={sorted(pos_keys - neg_keys)}, "
f"neg_only={sorted(neg_keys - pos_keys)}"
)
assert pos_keys, "pos/neg clr rows are empty"
return list(pos_clr)
def _on_off(delta: dict[str, float], intent: dict[str, int], off_set, off_weight) -> tuple[float, float, float]:
"""on = mean_f∈intent intent[f]*Delta_f ; off = mean_f∉intent |Delta_f| ; sel = on - w*off.
@@ -86,8 +96,12 @@ def gated_selectivity(
clr is pre-softmax nats: sel_gated is a direction+selectivity anchor, NOT a behavioral effect
size. Pair it with si_flips for the behavioral claim.
"""
keys = [k for k in pos_clr if k in neg_clr]
keys = _paired_keys(pos_clr, neg_clr)
off_set = [f for f in FOUNDATIONS if f not in intent]
pmasses = np.asarray([pmass_pos, pmass_neg, pmass_base], dtype=float)
assert np.isfinite(pmasses).all(), f"pmass inputs must be finite, got {pmasses.tolist()}"
assert (0 <= pmasses).all(), f"pmass inputs must be nonnegative, got {pmasses.tolist()}"
assert pmass_base > 0, f"pmass_base must be positive, got {pmass_base}"
coh = min(1.0, min(pmass_pos, pmass_neg) / pmass_base)
coh2 = coh ** 2
@@ -127,7 +141,7 @@ def si_flips(pos_clr: dict, neg_clr: dict, intent: dict[str, int]) -> dict:
Range [-1, 1]. Youden-J-style (a difference of rates); saturates where clr does not, which is
exactly why it is the behavioral cross-check to the unbounded clr selectivity.
"""
keys = [k for k in pos_clr if k in neg_clr]
keys = _paired_keys(pos_clr, neg_clr)
n = len(keys)
per_f: dict[str, float] = {}
for f, s in intent.items():
+25
View File
@@ -7,6 +7,7 @@ si_flips is a bounded behavioral pick-rate change. -- Claude
from __future__ import annotations
import numpy as np
import pytest
from moralmaps.metrics import FOUNDATIONS, gated_selectivity, si_flips, clr_per_row
@@ -73,6 +74,30 @@ def test_coherence_barrier_squared():
assert r2["coherence"] == 1.0
@pytest.mark.parametrize("invalid_pmass", [float("nan"), float("inf"), -0.1])
def test_coherence_inputs_must_be_valid(invalid_pmass):
pos, neg = _sweep(auth_p=-2.0, care_p=+2.0, auth_n=+2.0, care_n=-2.0)
with pytest.raises(AssertionError, match="pmass inputs"):
gated_selectivity(
pos, neg, INTENT,
pmass_pos=invalid_pmass, pmass_neg=0.9, pmass_base=0.9,
)
def test_metrics_require_exact_nonempty_row_pairing():
pos, neg = _sweep(auth_p=-2.0, care_p=+2.0, auth_n=+2.0, care_n=-2.0)
neg.pop(next(iter(neg)))
with pytest.raises(AssertionError, match="row keys differ"):
gated_selectivity(pos, neg, INTENT, pmass_pos=0.9, pmass_neg=0.9, pmass_base=0.9)
with pytest.raises(AssertionError, match="row keys differ"):
si_flips(pos, neg, INTENT)
with pytest.raises(AssertionError, match="rows are empty"):
gated_selectivity({}, {}, INTENT, pmass_pos=0.9, pmass_neg=0.9, pmass_base=0.9)
with pytest.raises(AssertionError, match="rows are empty"):
si_flips({}, {}, INTENT)
def test_si_flips_behavioral_bounded():
# pos makes care the argmax pick, neg makes authority the pick.
pos, neg = _sweep(auth_p=-2.0, care_p=+3.0, auth_n=+3.0, care_n=-2.0)