mirror of
https://github.com/wassname/moral-maps.git
synced 2026-09-09 11:27:22 +08:00
109 lines
5.0 KiB
Python
109 lines
5.0 KiB
Python
"""Unit guard for the two shared steering metrics (moralmaps.metrics). No model.
|
|
|
|
Builds synthetic pos/neg clr rows for a selective Authority-down / Care-up steer and
|
|
checks: on>0, off small, the off_weight trade, the squared coherence barrier, and that
|
|
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
|
|
|
|
INTENT = {"authority": -1, "care": +1} # steer Authority down, Care up
|
|
|
|
|
|
def _row(auth: float, care: float) -> dict[str, float]:
|
|
"""A clr row: authority/care set, others 0. (clr rows need not sum to 0 for the metric,
|
|
which only reads per-foundation Deltas; realistic rows are centered but that is irrelevant here.)"""
|
|
d = {f: 0.0 for f in FOUNDATIONS}
|
|
d["authority"] = auth
|
|
d["care"] = care
|
|
return d
|
|
|
|
|
|
def _sweep(auth_p, care_p, auth_n, care_n, others_p=0.0, others_n=0.0, n=8):
|
|
"""n paired rows. pos = intended pole (auth low, care high), neg = opposite pole."""
|
|
pos, neg = {}, {}
|
|
for i in range(n):
|
|
p = _row(auth_p, care_p)
|
|
m = _row(auth_n, care_n)
|
|
for f in FOUNDATIONS:
|
|
if f not in INTENT:
|
|
p[f] = others_p
|
|
m[f] = others_n
|
|
pos[f"vid{i}|other_violate"] = p
|
|
neg[f"vid{i}|other_violate"] = m
|
|
return pos, neg
|
|
|
|
|
|
def test_selective_steer_scores_high():
|
|
# pos drives auth DOWN (-2) and care UP (+2); neg is the mirror; off-axis flat.
|
|
pos, neg = _sweep(auth_p=-2.0, care_p=+2.0, auth_n=+2.0, care_n=-2.0)
|
|
r = gated_selectivity(pos, neg, INTENT, pmass_pos=0.9, pmass_neg=0.9, pmass_base=0.9)
|
|
# on = mean[ -1*(-2 - +2), +1*(+2 - -2) ] / 2 = mean[+4, +4] = +4 ; off = 0
|
|
assert abs(r["on"] - 4.0) < 1e-9
|
|
assert r["off"] < 1e-9
|
|
assert r["coherence"] == 1.0 # pmass preserved -> no discount
|
|
assert abs(r["sel_gated"] - 4.0) < 1e-9
|
|
assert r["ci_lo"] == r["ci_hi"] == 4.0 # zero row variance -> tight CI
|
|
|
|
|
|
def test_off_axis_is_soft_preference():
|
|
"""A sloppy steer (equal collateral on every off foundation) must still beat doing nothing,
|
|
and only lose a little to a surgical steer -- off is weighted 0.1."""
|
|
surgical, neg = _sweep(auth_p=-2.0, care_p=+2.0, auth_n=+2.0, care_n=-2.0, others_p=0.0)
|
|
sloppy, neg2 = _sweep(auth_p=-2.0, care_p=+2.0, auth_n=+2.0, care_n=-2.0, others_p=4.0, others_n=0.0)
|
|
rs = gated_selectivity(surgical, neg, INTENT, pmass_pos=0.9, pmass_neg=0.9, pmass_base=0.9)
|
|
rl = gated_selectivity(sloppy, neg2, INTENT, pmass_pos=0.9, pmass_neg=0.9, pmass_base=0.9)
|
|
assert rl["off"] > 0 # collateral registered
|
|
assert rl["sel_gated"] > 0 # still beats inaction (sel=0)
|
|
assert rs["sel_gated"] > rl["sel_gated"] # surgical wins
|
|
assert (rs["sel_gated"] - rl["sel_gated"]) < 0.5 * rs["sel_gated"] # but only mildly (0.1 weight)
|
|
|
|
|
|
def test_coherence_barrier_squared():
|
|
pos, neg = _sweep(auth_p=-2.0, care_p=+2.0, auth_n=+2.0, care_n=-2.0)
|
|
# worst arm pmass halves vs base -> coherence 0.5 -> sel scaled by 0.25.
|
|
r = gated_selectivity(pos, neg, INTENT, pmass_pos=0.45, pmass_neg=0.9, pmass_base=0.9)
|
|
assert abs(r["coherence"] - 0.5) < 1e-9
|
|
assert abs(r["sel_gated"] - 4.0 * 0.25) < 1e-9
|
|
# exceeding base coherence never rewards (clamped at 1).
|
|
r2 = gated_selectivity(pos, neg, INTENT, pmass_pos=0.99, pmass_neg=0.99, pmass_base=0.9)
|
|
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)
|
|
r = si_flips(pos, neg, INTENT)
|
|
# care: intent +1, rate(pos)=1, rate(neg)=0 -> +1 ; authority: intent -1, rate(pos)=0,
|
|
# rate(neg)=1 -> -1*(0-1)=+1 ; mean = +1.
|
|
assert abs(r["si_flips"] - 1.0) < 1e-9
|
|
assert -1.0 <= r["si_flips"] <= 1.0
|