Fresh-eyes audit caught it: big5/16pf/humor -C poles pin to the neutral midpoint
3.0 (degenerate profile, though pmass stays ~1.0), not the bidirectional move I
wrote. Only mfq2 and MFV are genuinely bidirectional at -C. Fixed big5
agreeableness -C (3.0 not 2.85) and minor MFV roundings.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
Qwen3-4B (the validation-table model) gives a fully coherent showcase: base/+C/-C
all pmass ~1.0 on ordinals and emitted_close <=9/264 on MFV, no -C collapse (that
was Qwen3.5-4B's gated-delta-net fragility). The Authority/Care vector moves the
MFV foundations apart bidirectionally (+C raises violations, -C lowers them and
raises "not wrong"), and also shifts agreeableness/16pf/humor -- a broad persona
axis, not an MFT-only or off-axis-null steer. Refresh the stale 82.6% validation
top1 to the reproducible 77.3%. README rewritten to match.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
The old "+C lowers all, -C raises all" no longer matches: +C lowers most, -C is
mixed and collapses loyalty/authority into NaN (missing blue arm). Describe the
coherent +C arm as the readable steer and the -C pole as past coherent range.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
The MFV base readout is coherent on Qwen3.5-4B (the earlier "excluded, base
incoherent" note was a misattribution of the -C pole's collapse + the bs=1 demo
NaN to base). +C is a moderate coherent steer; -C over-steers into the uniform
"everything is a violation" collapse, mirroring the ordinal -C instability. Adds
the MFV foundation-delta figure and the C-sweep follow-up note.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
MFV base discriminates (logit spread Social Norms -0.43 .. Loyalty -5.79, not
uniform -> coherent, emitted_close 4/264). +C is a moderate coherent steer; -C
(C=-1) collapses into a uniform ~+10 nat "everything is a violation" shift, the
same over-steer instability the ordinal -C pole shows (NaN at collapse). The
earlier "MFV base broken" read was a misattribution of the -C pole's collapsed
aux + the bs=1 demo NaN to the base eval.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
MFV pmass=0.1664 == 44/264 non-closing rows. Sweeps think budget to confirm the
collapse is guided.py case (c) (closed-think discards the forced read) and
whether a short budget recovers coherence without a researched-semantics change.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
plot_range_zoom crashed on the NaN-at-collapse cells (a collapsed steer pole
reads NaN by design, "do not compare"). nanmin/nanmax frame the un-collapsed
cells; draw_steer already skips the NaN arm. Figures are the first valid
ordinal showcase: mfq2/big5/16pf/humor pmass 0.99+ and the steer registers at
think=64 (mfq2 care 3.86 base -> 3.13 at C=+1), unlike the old think=0 run.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
plot_ipsative_pca gains an optional respondents arg: when given it fits the
ipsative PCA basis on the individual cloud (3902 Atari respondents) and scatters
them behind the society dots, with an envelope-core crop, matching the
mft_honesty fig_pca_ipsative the project standardized on. Axis labels carry a
foundation-loading gloss. respondents=None keeps the old society-only behavior.
Bundles atari_study2_raw.csv + a respondent_profiles loader (keying verbatim
from Atari Code_Study2.R) so tinymfv owns the data, no mft_honesty dependency.
plot_steer_showcase passes respondents for mfq2 only.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
administer()/read_items() now route through _rollout_natural_or_forced (the
nominal MFV core) instead of a think=0 single forward, so an activation steer
accrues over the think trace before the prefilled answer slot is read (spec
moral_aliens_engine.md, resolved decision: ordinal needs a think budget). The
only per-instrument difference is the answer-token set + the downstream reducer.
force_only on the shared core: the ordinal "(" prefill is one common char, so
natural-emission detection would match it by chance in the think trace and read
logits mid-think; surveys always force-read the answer slot. Nominal path keeps
natural emission (force_only defaults False). max_think_tokens floor is 1.
Smoke (tiny-random): ordinal + nominal both run; force-only demo reads the
forced ( slot.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
The Qwen3.5-4B MFV base eval failed its own coherence canary (pmass=0.42,
top1_acc=0.28, mean_js/nll=NaN, demo p[f]=NaN), so the +6..+9 nat "everything is
a violation" deltas were a readout-collapse artifact, not a steering effect.
Remove the dumbbell + delta table + claim; keep only the coherent ordinal
showcase. MFV sensitivity delta lands once the vignette readout is coherent.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
The two top images still pointed at the pre-run figures; swap them for the real
mfq2 map/range from this run and drop the orphaned old PNGs.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
One iso-KL calibrated mean-diff Authority/Care vector (C=+0.38) administered
across all 5 instruments from a steering-lite run_allinstr_showcase run.
- MFV dumbbell + delta table: -C lifts every violation-logit +6..+9 nats (the
"everything is a violation" moral alien), +C near-inert except Social Norms -2.16.
- MFQ-2 map + range: near-uniform endorsement shift, every pole inside the human band.
- big5/16pf/humor ranges: off-axis nulls; humor base is itself the outlier.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
Consume a steering-lite all-instrument run dir and render, per instrument,
tinymfv's ipsative map + range + zoom (ordinal) and a per-foundation Delta-logit
dumbbell (MFV), against the bundled human cloud. The showcase that exercises
tinymfv.maps end-to-end on real steering before publishing the lib.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
The c=+-1,+-2 steps were dropped from the main range as clutter, but they
carry the steer's spacing/curvature; bring them back small (ms*0.6) while
the calibrated poles (cs[0]/cs[-1] = -+signed_C) keep head-only arrowheads.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
plot_range/plot_range_zoom now draw the steer via a shared draw_steer: two arms
fanning from the base dot (red +c, blue -c), each a line + constant-size triangle
head pointing away from base (matches plot_ipsative_pca's base->pole convention,
not the old -c->+c single arrow). Marker heads not FancyArrows so short arms don't
shrink/flip; arms x-offset so a non-bidirectional steer reads as two parallel arms;
poles are head-only (no dot blob); interior step dots off on the main range, on in
the zoom. human/AI direct-label brackets pinned to the panel top. Legend prose out
of the suptitle (short headline only); callers put it in the figure caption.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
The prior commit "fixed" p_a/pmass into a softmax to avoid NaN at coherence collapse.
That was wrong for this codebase: renormalizing within allowed tokens discards the mass,
so a distribution built from ~zero mass and one from real mass look equally comparable
after renorm -- they are not (the mean of 10 != the mean of 130). p/pmass -> NaN at
collapse poisons that item's factor, which is the honest "do not compare" signal, not a
bug. softmax was a silent fallback fabricating a comparable-looking number from incoherent
output. Restored, with a comment so it does not get re-"fixed". (Over-trusted the external
reviewer here against the repo's own no-defensive-programming rule.)
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
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>
Whole-library review (deepseek-v4-pro) flagged tinymfv as not-yet-ready as a shared
dep. Fixes for the parts I agreed with:
- lazy `maps` import via module __getattr__ so `import tinymfv` stays headless/fast
(no forced matplotlib) for numeric-only consumers; `tinymfv.maps.*` still works.
- trim __all__ to the front door (entrypoints + types + data api); plumbing stays
importable but out of `import *`.
- delete dead code: reduce_nominal + REDUCERS (evaluate folds its profile inline),
expected_value, HF_REPO, ROOT, _DEFAULT_FORCED_HINT.
- type administer's return as a TypedDict (AdministerResult/ItemRow/ItemFrameRow) so
the schema is documented + checkable without reading source; still a plain dict at
runtime (zero consumer churn).
- maps.plot_ipsative_pca: parametrize the legend labels (defaults preserve output)
and rename hon/dis -> pos/neg so a non-honesty steer gets a correct legend.
- drop 'canary' jargon and panel/review-# archaeology from comments.
Verified: `import tinymfv` no longer loads matplotlib; lazy maps still resolves;
experiment mfq2 smoke green through the typed administer.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
Default eval logs were dominated by DEMO A/B full prompt+generation dumps at every
monitor step. verbose is now a level: 1 (default) prints the one-line aux stats plus
the free-reasoning generation collapsed to 64 chars (no prompt), bracketed by blank
lines so it stands apart from the steer demos; 2 reproduces the full first-row trace,
profile table, and complete DEMO B. bool callers still work (True->1, False->0).
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
The frame-averaged per_item drops the per-framing granularity that experiment
analyses need (MFQ-2 map's framing-bias diagnostic + paired base-vs-steer delta).
These rows are already computed in the by_dim_frame loop, so returning them is free.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
deepseek-v4-pro review of the admin->tinymfv port (algebra proven correct, diff
8.25e-08 = float noise). Closes its flagged fail-fast gaps:
- per_item_categorical asserts uniform frame count per item (else the per-item
average would silently reweight a factor)
- reduce_ordinal asserts dimension is not None (no phantom-factor pooling)
- administer asserts every ordinal item carries meta['task'] (else build_prompt
would silently drop the response-scale legend and the profile would be junk)
Also commits maps.py + viz deps from the stage-3 port.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
Port the answer-token survey readout from the weight_steer_honesty experiment
(mft_honesty.admin) onto the instrument.py canonicalize-at-reader design:
- read.py: generalized answer-token reader (any answer_space + prefill)
- administer.py: read all frames -> per_item_categorical -> reduce_ordinal -> profile
- instruments.py: build MFQ-2/Big5/16PF/HSQ Instruments from bundled survey JSONs
- instrument.py: add display + human_csv fields for the map layer
- data/: survey JSONs + human country CSVs (lean: no raw survey responses)
Parity: experiment's parity_administer_check.py shows max per-foundation diff
8.25e-08 vs admin.administer on the tiny model (same function of same logits).
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
Score the option INDEX digit, not the foundation word. The words tokenize into
uneven first pieces on Qwen3 (care/authority/social = whole word; fairness->"fair",
loyalty->"loy", sanctity->"san", liberty->"lib" = rare subword fragment), whose
per-word prior is constant across the fwd/rev frames and so survives the position
debias and leaks into the selectivity softmax. Digits "0".."6" are clean single
distinct tokens; with index = listing position the reversed-enum pass scores each
foundation on two different digits, so digit-magnitude AND position bias cancel in
the mean (lp_rev gather reversed back to foundation order). Also closes Clifford's
shared-language channel (no foundation word in the scored token).
Comments are now the verbatim Clifford et al. (2015) response-option sentences.
Result dicts stay foundation-keyed, so downstream is unchanged.
Smoke (0.6B, n=5): mechanically correct, pmass_allowed 0.985. Yellow flag vs
word-key baseline (p_top1 0.46 vs 0.76, top1_acc 0/5 vs 1/5) but confounded by the
desc change and tiny n; the 4B re-baseline is the real UAT.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
Per user: show the model the literal option text ("It violates norms of harm or
care (e.g., unkindness, causing pain to another)"), not just the parenthetical.
Each enum # comment is now the exact Clifford response-option sentence. Key stays
a single foundation token (scoring reads first token of key); comment carries the
verbatim option. Noted the number-the-options alternative (frees the key + debiases
per-token priors via reverse-enum, but a small model must bind number->comment) as
a tradeoff for the rubric-verification pass.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
We had paraphrased 4 of the 7 MFV option descriptions (authority +"legitimate
authority", purity +"impurity", liberty changed to "oppression, restricting
freedom" in bb0b194, social +"weird or unusual"). The Clifford paper states it
deliberately kept the option vocabulary DISJOINT from the vignette text ("we did
not use any of the words from the descriptions ... in the actual vignettes ...
minimizing concerns that classification is driven by shared language"), so any
paraphrase risks a shared-language confound AND breaks comparability with the
literature. Now every gloss is the exact "(e.g., ...)" parenthetical:
care: unkindness, causing pain to another | fairness: cheating or reducing
equality | loyalty: betrayal of a group | authority: subversion, lack of
respect for tradition | sanctity: degrading or disgusting acts | liberty:
bullying, dominating | social: not morally wrong; does not apply to any choice
This reverts bb0b194 (liberty "dominating" was changed for authority-overlap, but
Clifford separates liberty/authority at the VIGNETTE level -- downward hierarchical
harm invokes Liberty, upward invokes Authority -- not via option wording). Enum keys
keep the MFT names (sanctity/liberty); only the gloss text changed; scoring (first
token of the key) is unchanged.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
sanctity led with 'purity' (the virtue) not the violation; liberty's 'dominating'
overlapped authority. Now every enum comment is the negative pole of its foundation:
sanctity -> 'degradation, impurity, or disgusting acts', liberty -> 'oppression,
restricting another's freedom', authority sharpened to 'legitimate authority'.
Scoring is unchanged (first-token of the foundation NAME); only the schema hint text shifts.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
The forced-choice readout prefills the answer slot to read calibrated logprobs,
so it shows no real reasoning -- at think=1 its trace is just prompt + a token +
slot. Add free_generation_demo(): one bs=1 generation that lets the model think
to completion and answer naturally on a single vignette, same vignette+schema as
the readout. evaluate() now prints both (DEMO A relabelled, DEMO B new) via
loguru when verbose, and returns them in result['demos'] so callers get the text
without return_per_row. Free think budget = min(2048, max(512, max_think*batch))
-- bs=1 frees batch memory, floored so even think=1 reasons, capped for big batches.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
Per token-efficient-logging. evaluate(verbose=True) now: prints the first
row's full prompt+think+answer-slot trace (special tokens, promoted from DEBUG
to INFO, gated to first batch), the model-vs-human profile table, and a
one-line aux-stats dict. Set verbose=False inside sweeps.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
Reframes from MFV-only to the multi-instrument eval: leads with the LLM-vs-human-cultures
map and quickstart; documents the design choices (logprobs for sensitivity, sliding think
budget, fwd/rev position debias w/ arXiv:2308.11483, SI answer-flip metric, coherence
canary); dev (N=1 x 2 orderings, 64 think, greedy) vs full (N=4 x 2 orderings, high think,
+SI +sampling variance) modes; instrument zoo (MFV forced-choice working, Likert landed but
wiring in progress); used-in + moral_stories_foundations training labels. Preserves the
mechanism/labels/validation/citation content. Lint clean (humanizer).
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
Unifies forced-choice (nominal) and Likert (ordinal, expectation over the integer
distribution) on tinymfv's answer-token reader. Per scientist panel (docs/reviews/
sci_ma_*.md): canonicalize every frame's distribution to one forward orientation before
metrics+reducer (fixes the asymmetric nominal-reader/ordinal-reducer reflection), renormalize
p with pmass kept as canary, add ordinal |E-error| metric, keying applied only in the profile
reducer (proven orthogonal to framing, not a double-flip), cross-scale guard, negative-control
shuffle. 8 pure-function unit tests pass.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
Lead with the plain point, introduce + link Youden's J, spell out the macro
averaging (one-vs-rest per foundation) and point at _informedness for the
formula. Fix stale "two scalars" -> "three". Drop the "flip-informedness"
coinage and "the headline" tell. External-panel comprehension pass: ready
(4.1/5), accuracy and caveats 4-5 across panelists.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
nll_prompt was removed from per_row in f585864 but the script still read it,
crashing the smoke test. Remove the dead refs and surface the new
informedness scalar alongside top1_acc.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
Chance-corrected, argmax-only companion to mean_nll: moves when the answer
flips, not when confidence shifts. 0 = base-rate guessing, so it exposes
majority-class models that top1_acc flatters. Same flip-informedness family
as steering-lite's surgical informedness, anchored on the human argmax here.
README also points at the paired training set moral_stories_foundations.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
When the model emits </think> in natural generation but the answer-slot
window detection fails, that's coherence collapse — the model "finished
thinking" without producing JSON. pmass=0.0 is the honest measurement
(no probability mass on allowed tokens at a non-existent slot) and lets
the coherence canary see the failure as a real signal rather than
propagating NaN through np.mean to crash c_scan. nll_json stays NaN
since no JSON was emitted to score.
Triggered by qwen3.6-27b nf4 + LoRA at c=1.0: 1/4 samples hit case (c)
and the NaN aborted c_scan instead of letting it walk down further.
Qwen3.6-27B nf4 + adapter at c=1.0 produced a non-finite raw logit at a
single generated step in 1/4 samples (others used forced-prefill path);
the natural-path F.log_softmax propagated NaN into mean_pmass_allowed,
crashing c_scan. Bound with nan_to_num(±1e4) — leaves argmax-finite rows
unchanged.