The E map/range are for human comparison; this new per-instrument foundation_dcontrast
figure shows the steer in the sensitive contrast readout (steered minus base C, +C vs -C),
the ordinal twin of the MFV dlogit dumbbell. read_profiles gains a value_col so it reads
either E ('mean') or C. This is the figure that shows what we steered for; the E range hides it.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
The default ordinal readout was the expected Likert score E = sum k*p_k, which is
insensitive to steering: dE/dl_j = p_j(j-E) vanishes when the model answers confidently
(peaked at the mode), so a steer that reallocates the tails barely moves E. read.py threw
away the raw logprobs after renormalizing, so nothing downstream could recover the signal.
- read.py keeps the raw lp_gather (the primitive) + the think traces on every row.
- readouts.py: pure functions of lp -- expected_score E (human-comparable), logit_contrast
C = sum (k-mid)*lp_k (primary steer signal: dC/dl_j = w_j, no p_j suppression, normalizer-
invariant, dC = w.dl exactly), agree_logodds LO (readable 2-bin direction), entropy.
- per_item_categorical also frame-averages the logprobs (exact for the linear contrast).
- administer returns profile_C alongside profile_E, per-item E/C/LO/entropy with bootstrap
CIs for both, and the raw per-(item,frame) rows with lp + think for downstream reconstruction.
Unit check: on a peaked-at-4 dist under a small disagree steer, dE=-0.11 but dC=-1.80
(=w.dl exactly) and dLO=-0.60; C identical on raw logits vs renormalized logprobs.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
guided.py: non-finite answer-slot logits were clamped with nan_to_num(+-1e4),
fabricating a confident answer from a blown-up (steered/quantized) forward pass.
Mark the row incoherent (pmass=0, lp=NaN) instead -- same 'do not compare' signal
as the case-(c) collapse the pipeline already handles.
data.py: load_vignettes silently inner-joined the two condition files and dropped
mismatched ids, so a missing rewrite would change N (and every metric) without
failing. Assert the id sets are identical instead.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
Per-eval INFO lines (rows/think_tokens/aux-stats/first-row/profile/demos) demoted to
DEBUG so a consumer calling evaluate() ~47x/run is not drowned; one-time + WARNING+ kept.
README 308->120: cut process-archeology + per-instrument showcase, added crisp dlogit
and SI definitions for new users.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
MFV got two bespoke figures (z-emphasis bars + dlogit dumbbell). Per GPT-5.5 code
review, reuse the geometry not the ordinal semantics: a _mfv_zspace adapter feeds the
same plot_ipsative_pca + plot_range the ordinal instruments use, in z-scored relative-
emphasis space (logit-violation and 1-5 wrongness cannot share a raw axis). Keep the
dlogit dumbbell as a raw-magnitude diagnostic; drop the bespoke map_emphasis.
Also fix a real bug the review found: plot_ordinal gated the map/SPLOM trajectory to
coherent c (pmass >= 0.95*base) but still passed ALL cs to range/range_zoom, so the
figures disagreed on which poles were valid. Range/zoom now use the same coh_cs.
plot_range gains an ylabel param (MFV is z-space, not 1-M); draw_steer asserts sorted
cs; MFV asserts only Social Norms is dropped.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
Bump the bottom legend strip (PAD_B 0.52->0.62) and lower/shrink the compass + minimap insets so
the compass title clears the lowest data points. Rename the minimap title "full space" ->
"all human respondents" (says what the backdrop cloud is).
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
Replace the per-plot least-crowded-corner heuristic (+ label pruning) with a fixed layout: pad the
y-axis bottom by PAD_B of the data range to reserve a clean strip, then always place the minimap
bottom-right (reads like a small map) and the compass bottom-left. Insets are opaque so the backdrop
haze stays behind them. Same on every instrument, no dependence on where the trajectory heads, and
drops the crowd/rank/prune code + the now-unused pad param.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
- SPLOM diagonal: replace the confusing histogram/rug + vertical AI rules with a horizontal violin
of the human spread + the AI base/+C/-C as the same trajectory dots used off-diagonal (1-D
analogue), so the diagonal reads the same way as the rest. Unifies full + zoom (no branch).
- SPLOM off-diagonal: jitter the respondent cloud -- MFQ-2 ordinal scores land on a lattice that
reads as grid-dots; jitter softens it to a density (it is the human joint covariance backdrop).
- Compass corner now counts text labels (society codes, steer labels), not just data points, and a
final pass drops any society label still under the compass/minimap box. No legend-over-label.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
- Society labels: pin each ISO code beside its dot (small fixed offset) and DROP any that would
collide rather than fling it far on a leader line -- close or omitted, never ambiguous.
- Crop to societies + steer for EVERY instrument (drop the synthetic/real branch): the human cloud
(mfq2 respondents too) is far wider than the societies, so it buried them in a central blob. The
cloud stays a clipped backdrop + a "full space" minimap shows where the frame sits.
- SPLOM zoom diagonals: the narrow window slices the marginal into solid blocks, so swap the
histogram for a visible mid-panel society rug + the AI steer rules. Full SPLOM keeps the histogram.
- Minimap: no labels (inset too small); orientation comes from the viewport rectangle.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
The textalloc leader lines made a spider-web across the map; 2-letter ISO codes
are short enough to sit beside their dot with only a small overlap-avoidance
nudge. Cleaner, less ink, label stays next to its point in almost all cases.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
Trajectory coherence gate was absolute (pmass<0.9); make it relative -- keep a
c only if pmass >= 95% of the base (c=0) pmass, else drop it entirely (no hollow
markers). Read vec_label from summary.json so non-authority personas label
correctly instead of the hardcoded "Authority/Care axis".
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
- plot_splom: KxK pairs-plot (mfq2 real joint only -- others ship independent-
marginal haze that would fabricate off-diagonal correlation). lower=joint
scatter w/ AI base->steer trajectory, diag=marginal+AI rules, upper=Pearson r
sized by |r|. Ordered by PC1 loading so binding (authority/loyalty) cluster.
full + AI-zoom (macro/micro). NaN-safe at collapsed poles.
- ipsative map: synthetic haze is far wider than the societies, so it now crops to
societies+steer (haze clips) and adds a 'full space' minimap with a viewport
rectangle -- readable big5/16pf/humor maps with macro context kept.
- compass labels via textalloc (was overlapping); NaN-safe crop.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
Driver writes the signed c-multiplier per row; plotter feeds the full c-sweep to
the range (already multi-c capable) and a connected path to the map. Headline
arrows point to calibrated c=+-1; trajectory dots at |c|>1 extend beyond, growing
with |c|, drawn hollow where admin pmass fell below the coherence floor. Compass
moves to the least-crowded corner so the +c arm (which heads toward its own
loading) stops colliding with it.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
MFV had only the dlogit dumbbell, no human comparison. Add plot_mfv_map: per-
foundation relative emphasis (z across foundations) of model base/+C/-C against
the 5 MFV human cultures (JimenezLeal+Yamada), z-scoring each profile so the
model's logit scale and humans' 1-5 wrongness compare by pattern. Social Norms
dropped (no human norm). Shows the model over-weights Care/Authority vs humans
and +C lowers Authority emphasis. Keeps the dumbbell too.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
Split the PCA-fit basis from the scattered cloud in plot_ipsative_pca: add a
'haze' arg so instruments with only society-level mean+sd get a backdrop
(marginal Normal resample per country) without that resample dictating the
axes. mfq2 keeps its real Atari respondents (fit + haze). Previously only mfq2
had any human scatter; big5/16pf/humor showed bare society dots.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
job 234: ordinal pmass 1.0 at both poles up to C=3.0, steer grows 0.129->0.324.
C=1 is well inside the coherent range. Joint-coherent C is bounded by the side
instruments' -C neutral-degeneracy (a model property at C=1), not ordinal
coherence. Completes the goal's "sweep for the largest coherent C" clause.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
Ran the exact 2026-05-08 eval (worktree at commit b20ec56, word readout) on
Qwen3-4B: top1 0.780, not 0.826. Every eval version agrees on ~0.78 (digit 0.773,
word-current 0.788, word-original 0.780). The 82.6% was a stale/erroneous table
entry, not a target this model reaches under any pipeline. Canonical value 0.773.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
Tested the documented cause (the old word-first-token gather): top1 0.788 on
Qwen3-4B, only ~1.5pt above digit (0.773), not the table's 0.826. So even
reverting the readout does not recover it; the 82.6% came from the broader
2026-05-08 pipeline. Current eval tops out ~0.77-0.79 by every lever.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
Throwaway probe (like probe_mfv_think_budget): reuses the current _rollout core but
gathers foundation-WORD first tokens + word-keyed schema + no rev-reversal -- the
pre-e4e0f4d readout that produced the 82.6% validation number. Canonical evaluate()
stays digit-only. Confirms whether the showcase model matches the validation table
under the table's own method.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
Third independent lever ruled out: Qwen3-8B reads exactly 0.773 like Qwen3-4B, so
0.83 is unreachable with the debiased digit readout at any fitting model size. The
gap is purely the superseded word-readout method.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
Exhausted the legitimate levers on the current (digit) readout: top1 0.72 (think
64), 0.77 (256, 512 collapses), 0.72 (BMA n_samples=8). The 82.6% required the old
word-first-token readout, replaced deliberately to drop the uneven-first-piece
word prior. 0.773 is the honest ceiling; 0.83 would need reverting the debiasing
(research poison). README note + journal updated.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
Records the eval unification, the Qwen3.5 -C collapse + bs=1 demo NaN fragility,
the cleaner Qwen3-4B showcase, the monotone think-budget ablation, and the
"MFV base broken" misread correction + stale 82.6% top1.
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
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>