rename package tinymfv -> moralmaps (repo -> moral-maps)

Import name tinymfv -> moralmaps, pip name tiny-mfv -> moral-maps, GitHub
URLs wassname/tinymfv -> wassname/moral-maps. HuggingFace dataset id
wassname/tiny-mfv left as-is (separate namespace, published data artifact).
Historical docs/spec/* and RESEARCH_JOURNAL keep their dated paths.

Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
This commit is contained in:
wassname
2026-07-09 10:41:49 +08:00
co-authored by Claudypoo
parent ca76c0e05e
commit 469788c418
60 changed files with 111 additions and 111 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
**This is fail-fast research code.** Novel work, not in your training data. Extrapolate carefully.
## What tinymfv is
## What moralmaps is
One answer-token logprob reader that runs many questionnaires. The model is prefilled to an
answer slot after a short think budget; we read the next-token distribution over the vocab and
+3 -3
View File
@@ -1,4 +1,4 @@
# tinymfv figures
# moralmaps figures
A gallery of the culture maps and range plots. Each is captioned the newspaper way: a title, a
one-line subtitle saying what you are looking at, a short caption pointing out what to notice, and a
@@ -42,7 +42,7 @@ GlobalOpinionQA (Durmus et al. 2023).</sub>
The maps below all show the **same run**. The base model **Qwen3-4B** is read at c=0 (black), then
pushed along a single **Authority** vector to +c (red, more Authority) and -c (blue, less). The vector
is a PCA direction that [steering-lite](https://github.com/wassname/steering-lite) builds from an
`authority-respecting` versus `authority-disregarding` persona pair; tinymfv only measures where the
`authority-respecting` versus `authority-disregarding` persona pair; moralmaps only measures where the
model lands, by logprobs, eight samples per item. Each instrument gets two maps: a **quadrant map** on
named axes taken from the literature, and an **ipsative PCA map** whose axes are the blind top-two
principal components, with a compass rose showing how the factors load.
@@ -83,7 +83,7 @@ is the model: it emphasises Authority far more than the pooled human reference,
further still.
<sub>Sources: MFV (Clifford et al. 2015); country norms Jimenez-Leal 2025, Marques 2020, Hopp 2024, Yamada
2025, Crone 2021, used pooled only (see `src/tinymfv/data/human/MFV_country_norms_NOTE.md`). Read by
2025, Crone 2021, used pooled only (see `src/moralmaps/data/human/MFV_country_norms_NOTE.md`). Read by
logprobs, N=8.</sub>
### Big Five
+7 -7
View File
@@ -1,5 +1,5 @@
[project]
name = "tiny-mfv"
name = "moral-maps"
version = "0.1.0"
description = "Moral and value maps for LLMs: World Values Survey, MFQ-2, Big Five, 16PF, humor styles, and moral vignettes, scored from answer probabilities (local) or rated sampling (API) and compared to human country norms. Built for steering work."
requires-python = ">=3.11"
@@ -14,14 +14,14 @@ dependencies = [
]
[project.optional-dependencies]
# tinymfv.maps (culture-map + range viz) only; `import tinymfv` and the evals stay headless,
# numeric-only consumers (steering-lite) skip this. Install with `pip install tiny-mfv[maps]`.
# moralmaps.maps (culture-map + range viz) only; `import moralmaps` and the evals stay headless,
# numeric-only consumers (steering-lite) skip this. Install with `pip install moral-maps[maps]`.
maps = [
"matplotlib>=3.8",
"shapely>=2.0", # union of per-country discs into one merged zone region
] # label placement is in-repo (tinymfv.labelplace); no textalloc/adjustText dep
# tinymfv.read_api (sampling readout for logprob-less API models) only. Install with
# `pip install tiny-mfv[api]`. The local logprob reader (read.py) needs none of this.
] # label placement is in-repo (moralmaps.labelplace); no textalloc/adjustText dep
# moralmaps.read_api (sampling readout for logprob-less API models) only. Install with
# `pip install moral-maps[api]`. The local logprob reader (read.py) needs none of this.
api = [
# robust OpenRouter calls (stamina backoff on 429/provider/upstream/malformed errors) --
# wassname's wrapper, so retry/rate-limit handling doesn't diverge from the rest of the stack.
@@ -42,4 +42,4 @@ build-backend = "hatchling.build"
allow-direct-references = true # the api extra pulls openrouter_wrapper straight from git
[tool.hatch.build.targets.wheel]
packages = ["src/tinymfv"]
packages = ["src/moralmaps"]
+4 -4
View File
@@ -29,7 +29,7 @@ SPLITS = ["other_violate", "self_violate"]
def local_jsonl(file_key: str, split: str) -> Path:
return ROOT / "src" / "tinymfv" / "data" / f"vignettes_{file_key}_{split}.jsonl"
return ROOT / "src" / "moralmaps" / "data" / f"vignettes_{file_key}_{split}.jsonl"
def hf_jsonl(cfg: str, split: str) -> str:
@@ -65,7 +65,7 @@ size_categories:
---
# tiny-mfv
[:octocat:](https://github.com/wassname/tinymfv)
[:octocat:](https://github.com/wassname/moral-maps)
Small moral-foundations eval for language models.
@@ -129,9 +129,9 @@ Calibration quality on classic, n=132:
## Eval
Use `tinymfv.evaluate(model, tokenizer, name="classic")`. It returns a per-foundation
Use `moralmaps.evaluate(model, tokenizer, name="classic")`. It returns a per-foundation
table plus `top1_acc`, `informedness`, and `mean_nll_T` against the `human_*` label
distribution. Full eval: see [tiny-mfv on GitHub](https://github.com/wassname/tinymfv).
distribution. Full eval: see [moral-maps on GitHub](https://github.com/wassname/moral-maps).
Source vignettes: https://github.com/peterkirgis/llm-moral-foundations
"""
+3 -3
View File
@@ -1,6 +1,6 @@
"""Run forced-choice 7-way primary-foundation probe over a vignette set.
Wraps `tinymfv.evaluate()`. Reports the AI-vs-label distribution match:
Wraps `moralmaps.evaluate()`. Reports the AI-vs-label distribution match:
top1_acc argmax model == argmax label
mean_nll soft cross-entropy vs human distribution, nats
mean_nll_T same metric after one fitted temperature
@@ -27,8 +27,8 @@ from loguru import logger
from tabulate import tabulate
from transformers import AutoModelForCausalLM, AutoTokenizer
from tinymfv import evaluate, load_vignettes
from tinymfv.guided import _DEFAULT_FORCED_FOUNDATIONS
from moralmaps import evaluate, load_vignettes
from moralmaps.guided import _DEFAULT_FORCED_FOUNDATIONS
ROOT = Path(__file__).resolve().parents[1]
+2 -2
View File
@@ -10,8 +10,8 @@ from pathlib import Path
import numpy as np
from tabulate import tabulate
from tinymfv.instrument import canonicalize_to_forward
from tinymfv.readouts import logit_contrast
from moralmaps.instrument import canonicalize_to_forward
from moralmaps.readouts import logit_contrast
def _rows(path: Path) -> list[dict]:
+2 -2
View File
@@ -27,8 +27,8 @@ matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import tinymfv as T
from tinymfv.zones import zones_for
import moralmaps as T
from moralmaps.zones import zones_for
ORDINAL = ["mfq2", "big5", "humor_styles"]
FOUNDATION_ORDER = ["care", "fairness", "loyalty", "authority", "sanctity", "liberty"]
+7 -7
View File
@@ -1,4 +1,4 @@
"""Showcase tinymfv's plotting on a real steering run.
"""Showcase moralmaps's plotting on a real steering run.
Consumes a steering-lite `run_allinstr_showcase.py` output dir (one calibrated
activation-steering vector administered across every instrument over a signed
@@ -13,7 +13,7 @@ probabilities cannot share a raw axis with 1-5 survey scores. It still goes
through the same plot_ipsative_pca / plot_range functions.
cs are SIGNED multipliers of the calibrated coefficient C (0 = base). The public
README plots show the coherent path: c=0 plus each +/-c row whose tinymfv answer mass
README plots show the coherent path: c=0 plus each +/-c row whose moralmaps answer mass
stays above the requested fraction of base. Incoherent rows are dropped.
uv run python scripts/plot_steer_showcase.py \
@@ -35,9 +35,9 @@ matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import tinymfv as T
from tinymfv import get_instrument
from tinymfv.zones import zones_for
import moralmaps as T
from moralmaps import get_instrument
from moralmaps.zones import zones_for
# 16pf turned off: even at 6 macro zones its ipsative map is an unreadable pile-up (that instrument
# doesn't separate cultures; Brazil/Ecuador stretch Latin America across the whole plot). -- Claude
@@ -49,7 +49,7 @@ def _frac(x, scale_max: int) -> np.ndarray:
def read_human_csv(path: str) -> dict[tuple[str, str], float]:
"""{(country, foundation): mean} from a tinymfv human_<instrument>.csv."""
"""{(country, foundation): mean} from a moralmaps human_<instrument>.csv."""
out: dict[tuple[str, str], float] = {}
with open(path, newline="") as fh:
for r in csv.DictReader(fh):
@@ -212,7 +212,7 @@ def plot_ordinal(run_dir: Path, out: Path, name: str, vec_label: str, C: float,
# Alternative NAMED-AXIS value map (interpretable poles, no compass/minimap): project the
# societies + the AI base/steered points onto the instrument's two named value axes, and draw the
# steer as a CONNECTED base->+c/-c path (same visual language as the ipsative map's trajectory).
from tinymfv.value_axes import VALUE_AXES, value_coords, axis_score
from moralmaps.value_axes import VALUE_AXES, value_coords, axis_score
if name in VALUE_AXES:
Pval, poles = value_coords(Mfrac, dims, name)
(_, _, xa), (_, _, ya) = VALUE_AXES[name]
+5 -5
View File
@@ -26,11 +26,11 @@ from loguru import logger
from tabulate import tabulate
from transformers import AutoModelForCausalLM, AutoTokenizer
import tinymfv as T
from tinymfv.instruments import get as get_instrument
from tinymfv.read import read_items, resolve_answer_ids, build_user_content
from tinymfv.read_api import read_items_sampled
from tinymfv.readouts import expected_score, entropy
import moralmaps as T
from moralmaps.instruments import get as get_instrument
from moralmaps.read import read_items, resolve_answer_ids, build_user_content
from moralmaps.read_api import read_items_sampled
from moralmaps.readouts import expected_score, entropy
W = np.arange(1, 6, dtype=float)
+1 -1
View File
@@ -10,7 +10,7 @@ import re
from datasets import load_dataset
from tinymfv.zones import zone_of
from moralmaps.zones import zone_of
SKIP = re.compile(r"don'?t know|no answer|refus|decline|none of|not applicable|^other|missing|inap",
re.I)
+1 -1
View File
@@ -22,7 +22,7 @@ import torch
from loguru import logger
from transformers import AutoModelForCausalLM, AutoTokenizer
from tinymfv import evaluate
from moralmaps import evaluate
def main() -> None:
+4 -4
View File
@@ -2,7 +2,7 @@
0.826 top-1 in the validation table (journal 2026-05-08), and contrast it with the
current debiased DIGIT readout (0.773).
This does NOT touch the canonical eval. tinymfv.evaluate stays digit-only. This is a
This does NOT touch the canonical eval. moralmaps.evaluate stays digit-only. This is a
throwaway measurement (like the other probe_* scripts) that reuses the current
_rollout_natural_or_forced core but gathers the first token of each foundation WORD
(care/fair/loy/author/san/lib/social) instead of the option index digit -- the one
@@ -26,9 +26,9 @@ import torch
from loguru import logger
from transformers import AutoModelForCausalLM, AutoTokenizer
from tinymfv.data import load_vignettes
from tinymfv.eval import _label_dist
from tinymfv.guided import (
from moralmaps.data import load_vignettes
from moralmaps.eval import _label_dist
from moralmaps.guided import (
_DEFAULT_FORCED_FOUNDATIONS, _FORCED_FOUNDATION_DESCS,
_resolve_first_token_ids, _rollout_natural_or_forced,
)
+1 -1
View File
@@ -1,7 +1,7 @@
"""P4 data foundation: what the GlobalOpinionQA WVS subset gives us for a WVS/Inglehart-Welzel map.
Anthropic/llm_global_opinions is MC questions with per-country human answer distributions -- almost
exactly tinymfv's instrument shape (allowed answer tokens + human anchors). This probe quantifies
exactly moralmaps's instrument shape (allowed answer tokens + human anchors). This probe quantifies
the WVS subset's coverage and prints the parse recipe, so we know a country x question matrix is
dense enough to place models among human societies. No model runs here.
+1 -1
View File
@@ -8,7 +8,7 @@ from pathlib import Path
import numpy as np
import plot_steer_showcase as P
from tinymfv import get_instrument
from moralmaps import get_instrument
DISPLAY = {
"mfv": "MFV vignettes",
+7 -7
View File
@@ -4,7 +4,7 @@ Economist chart), on the two named IW dimensions instead of a blind PCA.
X = Survival <-> Self-expression (homosexuality tolerance, interpersonal trust, political action)
Y = Traditional <-> Secular-Rational (religion importance + belief, abortion, child autonomy)
Each axis is a small hand-picked battery of GlobalOpinionQA WVS items (tinymfv.iw_axes), every item
Each axis is a small hand-picked battery of GlobalOpinionQA WVS items (moralmaps.iw_axes), every item
oriented to its axis-positive pole by reading the option order. A country's coordinate is the mean
`positiveness` (0-1) over that axis's items from the human WVS choice frequencies. A model's
coordinate is the SAME items administered as a dense Likert readout (read_items_rated: rate every
@@ -40,12 +40,12 @@ import matplotlib.pyplot as plt
from datasets import load_dataset
from transformers import AutoModelForCausalLM, AutoTokenizer
from tinymfv import maps
from tinymfv.zones import zones_for, zone_of
from tinymfv.instrument import Instrument, InstrItem
from tinymfv.read import read_items, resolve_answer_ids
from tinymfv.read_api import read_items_rated
from tinymfv.iw_axes import AXIS_ITEMS, X_AXIS, Y_AXIS, SKIP, resolve_items, positiveness
from moralmaps import maps
from moralmaps.zones import zones_for, zone_of
from moralmaps.instrument import Instrument, InstrItem
from moralmaps.read import read_items, resolve_answer_ids
from moralmaps.read_api import read_items_rated
from moralmaps.iw_axes import AXIS_ITEMS, X_AXIS, Y_AXIS, SKIP, resolve_items, positiveness
# option labels are single digits 0..n-1 -- single-token (unlike '10' on the justifiable scale) and
# the format the answer-token reader is tuned for (a bare digit, not a letter the model ignores in
@@ -1,4 +1,4 @@
"""tinymfv: tiny moral/value instruments for local LLMs.
"""moralmaps: moral and value instruments for LLMs.
One answer-token reader, two reducer families:
@@ -9,7 +9,7 @@ One answer-token reader, two reducer families:
High-level usage:
from tinymfv import evaluate
from moralmaps import evaluate
from transformers import AutoModelForCausalLM, AutoTokenizer
tok = AutoTokenizer.from_pretrained("Qwen/Qwen3-4B")
@@ -19,7 +19,7 @@ High-level usage:
print(rep["top1_acc"]) # argmax accuracy vs label
print(rep["mean_nll_T"]) # temperature-scaled soft NLL vs label dist
Lower-level: see `guided_rollout_forced_choice` in `tinymfv.guided`.
Lower-level: see `guided_rollout_forced_choice` in `moralmaps.guided`.
"""
from .data import load_vignettes, load_all_vignettes, CONFIGS, ConfigName
from .eval import evaluate, CONDITIONS, EvalResult, EvalRow, EvalInfo
@@ -32,8 +32,8 @@ from .administer import administer
def __getattr__(name: str):
# `maps` pulls matplotlib; load it lazily so `import tinymfv` stays headless and fast for the
# numeric-only consumers (steering-lite). `tinymfv.maps.plot_*` still works -- first access
# `maps` pulls matplotlib; load it lazily so `import moralmaps` stays headless and fast for the
# numeric-only consumers (steering-lite). `moralmaps.maps.plot_*` still works -- first access
# triggers the import here.
if name == "maps":
import importlib
@@ -1,6 +1,6 @@
"""Run an ordinal Instrument end-to-end on a local model -> profile + coherence check.
This is the survey counterpart to `tinymfv.evaluate` (the vignette forced-choice eval). It ties:
This is the survey counterpart to `moralmaps.evaluate` (the vignette forced-choice eval). It ties:
read_items (answer-token readout, all frames)
per_item_categorical (canonicalize each frame to forward, average -> one dist per item)
@@ -1,7 +1,7 @@
"""Dataset loading. Reads per-condition jsonls and inner-joins by id, returning
the packed structure the eval consumes.
Files used by eval live in `src/tinymfv/data/`:
Files used by eval live in `src/moralmaps/data/`:
vignettes_<name>_other_violate.jsonl (3rd-person paraphrase of origin)
vignettes_<name>_self_violate.jsonl (1st-person rewrite)
Can't render this file because it is too large.
@@ -1,6 +1,6 @@
# MFV country norms: used POOLED only, never for cross-country comparison
`mfv_country_factors.csv` holds moral-foundation-vignette (MFV) means for 8 countries. tinymfv uses
`mfv_country_factors.csv` holds moral-foundation-vignette (MFV) means for 8 countries. moralmaps uses
these **pooled into a single human reference** (the mean across samples, per foundation) for the MFV
range plot. It does **not** plot them as a cultural map, and you should not read country-to-country
differences off this table. Here is why.
@@ -411,7 +411,7 @@ class ForcedChoiceResult:
# Full decoded generations per enum-ordering frame, one per sample.
# `gen_text` is always a list of length N=n_samples (even at N=1).
# Both texts are FULL — no stripping at </think>. If you want the
# pre-close part, split on `tinymfv.guided._CLOSE_MARKER`.
# pre-close part, split on `moralmaps.guided._CLOSE_MARKER`.
gen_text: list[str] # forward-frame, length N
gen_text_rev: list[str] # reversed-frame, length N
# Headline per-frame logprobs at the prefill position, after Bayesian
@@ -12,7 +12,7 @@ Sources (axis groupings aggregated from these papers):
- MFQ-2 equality/proportionality fairness split: Atari et al. 2023 "Morality beyond the WEIRD".
- Big Five meta-traits Plasticity/Stability: DeYoung, Quilty & Peterson 2007, doi 10.1037/0022-3514.93.5.880.
- HSQ 2x2 adaptive/maladaptive x self/other: Martin et al. 2003, doi 10.1016/S0092-6566(02)00534-2.
- WVS: Inglehart & Welzel 2005 (see tinymfv.iw_axes -- the WVS map builds its own item-level axes;
- WVS: Inglehart & Welzel 2005 (see moralmaps.iw_axes -- the WVS map builds its own item-level axes;
this table is for the psychometric instruments).
The debatable calls (flagged): the SECOND MFT axis is not canonical -- mfq2 uses the documented
+3 -3
View File
@@ -10,15 +10,15 @@ import unittest
import numpy as np
from tinymfv.data import CONFIGS, CONDITIONS, load_vignettes
from tinymfv.instrument import (
from moralmaps.data import CONFIGS, CONDITIONS, load_vignettes
from moralmaps.instrument import (
Instrument,
canonicalize_to_forward,
per_item_categorical,
reduce_nominal,
reduce_ordinal,
)
from tinymfv.readouts import expected_score, logit_contrast, logodds_agree
from moralmaps.readouts import expected_score, logit_contrast, logodds_agree
class InstrumentFlowTest(unittest.TestCase):
Generated
+49 -49
View File
@@ -11,7 +11,7 @@ resolution-markers = [
]
[options]
exclude-newer = "2026-06-29T06:18:19.538273755Z"
exclude-newer = "2026-07-03T02:38:05.43429745Z"
exclude-newer-span = "P6D"
[[package]]
@@ -683,6 +683,54 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
]
[[package]]
name = "moral-maps"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "loguru" },
{ name = "numpy" },
{ name = "pandas" },
{ name = "tabulate" },
{ name = "torch" },
{ name = "tqdm" },
{ name = "transformers" },
]
[package.optional-dependencies]
api = [
{ name = "openrouter-wrapper" },
{ name = "python-dotenv" },
]
maps = [
{ name = "matplotlib" },
{ name = "shapely" },
]
[package.dev-dependencies]
dev = [
{ name = "scipy" },
]
[package.metadata]
requires-dist = [
{ name = "loguru" },
{ name = "matplotlib", marker = "extra == 'maps'", specifier = ">=3.8" },
{ name = "numpy" },
{ name = "openrouter-wrapper", marker = "extra == 'api'", git = "https://github.com/wassname/openrouter_wrapper" },
{ name = "pandas" },
{ name = "python-dotenv", marker = "extra == 'api'" },
{ name = "shapely", marker = "extra == 'maps'", specifier = ">=2.0" },
{ name = "tabulate" },
{ name = "torch" },
{ name = "tqdm" },
{ name = "transformers", specifier = ">=5.7" },
]
provides-extras = ["maps", "api"]
[package.metadata.requires-dev]
dev = [{ name = "scipy", specifier = ">=1.17.1" }]
[[package]]
name = "mpmath"
version = "1.3.0"
@@ -1558,54 +1606,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" },
]
[[package]]
name = "tiny-mfv"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "loguru" },
{ name = "numpy" },
{ name = "pandas" },
{ name = "tabulate" },
{ name = "torch" },
{ name = "tqdm" },
{ name = "transformers" },
]
[package.optional-dependencies]
api = [
{ name = "openrouter-wrapper" },
{ name = "python-dotenv" },
]
maps = [
{ name = "matplotlib" },
{ name = "shapely" },
]
[package.dev-dependencies]
dev = [
{ name = "scipy" },
]
[package.metadata]
requires-dist = [
{ name = "loguru" },
{ name = "matplotlib", marker = "extra == 'maps'", specifier = ">=3.8" },
{ name = "numpy" },
{ name = "openrouter-wrapper", marker = "extra == 'api'", git = "https://github.com/wassname/openrouter_wrapper" },
{ name = "pandas" },
{ name = "python-dotenv", marker = "extra == 'api'" },
{ name = "shapely", marker = "extra == 'maps'", specifier = ">=2.0" },
{ name = "tabulate" },
{ name = "torch" },
{ name = "tqdm" },
{ name = "transformers", specifier = ">=5.7" },
]
provides-extras = ["maps", "api"]
[package.metadata.requires-dev]
dev = [{ name = "scipy", specifier = ">=1.17.1" }]
[[package]]
name = "tokenizers"
version = "0.22.2"