mirror of
https://github.com/wassname/moral-maps.git
synced 2026-09-11 12:20:38 +08:00
correctness fixes from gpt-5.5 review (ordinal path + fail-fast asserts)
Second external review (gpt-5.5, correctness-focused) on the post-cleanup tree. Found no off-by-one/double-flip in the ordinal canonicalization+keying. Fixed the parts I agreed with and could verify on the path the experiment uses: - read.py: NaN-safe answer-token renorm. p_a/pmass poisons the profile with NaN when pmass underflows to 0 at coherence collapse -- exactly when pmass should just flag it. softmax(logp_allowed) is identical when pmass>0 and stable at collapse. - maps.ipsative_pca: move SVD sign-stabilization INTO the helper so it and plot_ipsative_pca share one orientation (saved coords could otherwise mirror the figure). - instrument: assert ordinal answer_space is ['1'..scale_max] IN ORDER (reduce_ordinal weights by position; a reordered space silently inverts E) -- was length-only. - instrument.per_item_categorical: assert per-item dimension/sign agree across frames and frames are distinct, instead of silently averaging under rows[0]'s metadata. - pyproject: move matplotlib+textalloc to an optional `maps` extra; evals stay headless. - tests: drop imports of the deleted reduce_nominal/expected_value, inline the expectation, remove the now-impossible nominal-reducer test. Deferred (flagged to maintainer): two NaN/window issues in guided.py's forced-choice rollout (nominal evaluate() path) -- not exercised by this experiment, can't smoke-test, and the NaN-as-collapse-signal there is a deliberate design. Verified: experiment smoke green on all 4 instruments (no assert false-fires), 6 pure unit tests pass, headless import clean, 16pf map renders. Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
This commit is contained in:
@@ -8,13 +8,17 @@ import numpy as np
|
||||
|
||||
from tinymfv.instrument import (
|
||||
Instrument, InstrItem, canonicalize_to_forward, per_item_categorical,
|
||||
reduce_nominal, reduce_ordinal, expected_value, shuffle_dimensions,
|
||||
reduce_ordinal, shuffle_dimensions,
|
||||
)
|
||||
|
||||
M = 5
|
||||
ONEHOT = {d: np.eye(M)[d - 1] for d in range(1, M + 1)} # ONEHOT[4] = mass on scale point 4
|
||||
|
||||
|
||||
def _E(p, scale_max): # expected scale point; reduce_ordinal computes this inline now
|
||||
return float((np.asarray(p) * np.arange(1, scale_max + 1)).sum())
|
||||
|
||||
|
||||
def _ord_instr(items):
|
||||
return Instrument("t", "endorsement", "ordinal", ["1", "2", "3", "4", "5"],
|
||||
["care", "authority"], items, prefill="(")
|
||||
@@ -33,7 +37,7 @@ def test_forward_expectation():
|
||||
rows = [{"id": "1", "frame": "forward", "p": ONEHOT[4], "pmass_allowed": 1.0,
|
||||
"dimension": "care", "sign": 1, "human_label": None}]
|
||||
items = per_item_categorical(rows, "ordinal")
|
||||
assert abs(expected_value(items["1"]["p"], M) - 4.0) < 1e-9
|
||||
assert abs(_E(items["1"]["p"], M) - 4.0) < 1e-9
|
||||
prof = reduce_ordinal(items, _ord_instr([]))
|
||||
assert abs(prof[0] - 4.0) < 1e-9 # care
|
||||
assert np.isnan(prof[1]) # authority has no items
|
||||
@@ -45,8 +49,8 @@ def test_frame_consistency():
|
||||
"dimension": "care", "sign": 1, "human_label": None}]
|
||||
inv = [{"id": "1", "frame": "inverted", "p": ONEHOT[5], "pmass_allowed": 1.0,
|
||||
"dimension": "care", "sign": 1, "human_label": None}]
|
||||
e_fwd = expected_value(per_item_categorical(fwd, "ordinal")["1"]["p"], M)
|
||||
e_inv = expected_value(per_item_categorical(inv, "ordinal")["1"]["p"], M)
|
||||
e_fwd = _E(per_item_categorical(fwd, "ordinal")["1"]["p"], M)
|
||||
e_inv = _E(per_item_categorical(inv, "ordinal")["1"]["p"], M)
|
||||
assert abs(e_fwd - 1.0) < 1e-9 and abs(e_inv - 1.0) < 1e-9 # canonicalization makes them agree
|
||||
|
||||
|
||||
@@ -59,7 +63,7 @@ def test_keying_is_not_double_flip():
|
||||
rows = [{"id": "x", "frame": "inverted", "p": ONEHOT[5], "pmass_allowed": 1.0,
|
||||
"dimension": "care", "sign": -1, "human_label": None}]
|
||||
items = per_item_categorical(rows, "ordinal")
|
||||
assert abs(expected_value(items["x"]["p"], M) - 1.0) < 1e-9 # canonical agreement-with-item = 1
|
||||
assert abs(_E(items["x"]["p"], M) - 1.0) < 1e-9 # canonical agreement-with-item = 1
|
||||
prof = reduce_ordinal(items, _ord_instr([]))
|
||||
assert abs(prof[0] - 5.0) < 1e-9 # keyed factor score = 5, not 1 (no double flip)
|
||||
|
||||
@@ -81,15 +85,6 @@ def test_frame_spread_diagnostic():
|
||||
assert abs(items["1"]["frame_spread"] - 2.0) < 1e-9
|
||||
|
||||
|
||||
def test_reduce_nominal():
|
||||
instr = Instrument("mfv", "salience", "nominal", ["care", "authority"],
|
||||
["care", "authority"], [InstrItem("1", "v1")], prefill="(")
|
||||
rows = [{"id": "1", "frame": "forward", "p": np.array([0.8, 0.2]), "pmass_allowed": 1.0},
|
||||
{"id": "2", "frame": "forward", "p": np.array([0.4, 0.6]), "pmass_allowed": 1.0}]
|
||||
prof = reduce_nominal(per_item_categorical(rows, "nominal"), instr)
|
||||
assert np.allclose(prof, [0.6, 0.4]) # mean choice frequency
|
||||
|
||||
|
||||
def test_negative_control_shuffle():
|
||||
rng = np.random.default_rng(0)
|
||||
rows = [{"id": str(i), "frame": "forward", "p": ONEHOT[(i % 5) + 1], "pmass_allowed": 1.0,
|
||||
|
||||
Reference in New Issue
Block a user