mirror of
https://github.com/wassname/moral-maps.git
synced 2026-09-11 12:20:38 +08:00
metrics: fail on invalid coherence inputs and unpaired rows
This commit is contained in:
@@ -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():
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user