FAQ¶
Setup & running¶
Do I need an LLM API key to try it?¶
No. Install, tests, environment/population building, and rule-based studies
run with no key at all, and python scripts/demo_offline.py runs a full study
end to end offline. The from-scratch studies
dry-run with
llm_kind: "scripted". You need a key only for live LLM decision runs.
Can I just pip install socioverse2?¶
Yes, for the core library: pip install socioverse2 installs socioverse
(schemas, interfaces, engine, DuckDB store, validation) from
PyPI. The /sv-* workflow skills, the
dashboard, the reference studies and the demo script are not in the package;
they come with git clone https://github.com/sii-research/SocioVerse2.git. See
Installation.
Some tests skip. Is my install broken?¶
No — tests that need a companion repository (SocioVerse-ABM, ConsumerSim, HiSim), an optional extra or a live LLM auto-skip when it is absent. Skips + zero failures = healthy install.
Do I need Claude Code?¶
Only for the agentic workflow (/sv-* skills). The
programmatic path —
build artifacts, build_simulator, run(), SQL — is plain Python.
Claude Code needs its own Claude login (a Claude subscription) or an
ANTHROPIC_API_KEY. The SV_LLM_* key in .env does not power it; that key
only reaches the simulated agents. With only an OpenAI key you can try
Codex CLI, which reads the shipped
AGENTS.md and .codex/skills/; that path is experimental and has not had
a full end-to-end test yet. See
what powers the workflow skills.
Can I run several studies in parallel?¶
One study per process. Studies that wrap legacy engines may patch module globals, which is not thread-safe. For batch experiments, use separate processes (e.g. a process pool), one study each.
Can I background a /sv-run so the agent keeps working?¶
No. /sv-run must run in the foreground — issue the command and let it
block, with a ceiling of about 10 minutes. Never wrap it in
run_in_background, a trailing &, or nohup.
The reason is structural: in a headless or hosted agent, nothing re-invokes the agent when a background task finishes. The run completes into a void, the agent never learns it is done, and the whole flow stalls silently. A blocking run is what returns control with the result attached.
If a run genuinely needs longer than the ceiling, don't background it — cut the scale (fewer agents, fewer steps) or warn the user up front and let them decide.
Do I need the Event service or the user pool MCP?¶
No. Both are optional capabilities and both degrade gracefully: the events client falls back to a local cache and then to plain web search, and the persona pool falls back to grounded synthesis or a user-supplied persona file. You lose convenience and some grounding strength, not the ability to run a study. See External Capabilities.
What's the difference between running locally and using the hosted service?¶
Same framework, same artifacts. Locally you supply your own LLM key and any optional service endpoints, and you own the machine. On the hosted service the LLM provider, the Event service, the persona pool and a CJK-capable matplotlib are already wired up, runs and quota are metered for you, and the dashboard is served rather than self-hosted. See Hosted service.
Results & reproducibility¶
Are runs reproducible?¶
- Dry runs (
llm_kind: "scripted", rule-based models, orchicago_schelling's deterministic client): yes, exactly — seeded end to end. - Live LLM runs: no — sampling at nonzero temperature differs run to run. But every recorded run is permanent in its DuckDB store, and a branch replays its parent's stored actions exactly instead of re-asking the LLM.
Where are my results, and what's in them?¶
studies/<id>/trajectory/study.duckdb — tables panel (one row per agent
per step), metrics (one row per step), events (fired interventions), and
messages if the study used the message bus. Parquet exports sit alongside.
reports/report.md is the rendered version. Run stores are gitignored, so the
reference studies in a fresh clone carry their reports but no trajectory/
until you run them.
I changed an artifact and re-ran — my old results are gone. Why?¶
Opening a store for a new run replaces it, which is exactly why the rule
exists: changes to an existing study go through /sv-iterate, whose
version gate snapshots the live directory to versions/vN/ first. Anything
under a version snapshot is never deleted.
What is the difference between a branch and a version? When can I only create a version?¶
Both are entries in the study's versions.json, and the entry's kind
says which:
"branch"— written bycreate_branch(study_dir, note, *, fork_step=t*, source=None, resume_from_stage="sv-build-environment"). It recordswarm_start = {"source": "<parent>", "resume_from": t*-1}andfork_step: t*. At run timeSimulationLoop.run()(socioverse/engine/loop.py) sees that spec and calls_replay_inherited(), which rebuilds steps1..t*-1from the parent's stored panel actions —env.advance_to(t)thenenv.apply(actions), nodecide_batch, no LLM calls — and only then enters the real loop att*. Legal edits for a branch: addingscheduled_events/broadcastsatat_step >= t*, adding a new environment layer to carry one, and raisingn_steps."version"— written bycreate_version(...), cold run from step 0. Anything touchingpopulation/population.jsonorroster.jsonl, the study's own*.py/adapter/*.py(that isf),seed,decision_ref/decision_args,collector_*,store_*,engine_args,interaction_rounds, or an environment entry atat_step < t*can only be a version.n_stepsis deliberately exempt, so a branch may run a longer horizon than its parent.
check_branch_invariant() re-checks exactly that list against the parent's
frozen snapshot, and resolve_warm_start() calls it on every path into a run.
A violation downgrades rather than raising: kind becomes "version",
warm_start and fork_step are cleared, the run goes cold, and
downgraded_from_branch (source, fork step, one line per offending artifact)
stays on the entry as the audit trail.
Replay itself also requires interaction_rounds == 1 and an env.apply() that
is a pure function of the actions it is handed, so a study that wraps a legacy
simulator (chicago_schelling, whose move intent lives on hidden legacy agents)
and any multi-round study are version-only. Full treatment in
Iterate: versions and branches and
Versions & the controllable research loop.
Why is my Chinese text rendering as boxes in the figures?¶
matplotlib ships no CJK font by default, so every Chinese glyph renders as
a tofu box (□□□). Install one and point matplotlib at it:
import matplotlib
matplotlib.rcParams["font.sans-serif"] = ["Noto Sans CJK SC"]
matplotlib.rcParams["axes.unicode_minus"] = False # keep the minus sign readable
If matplotlib still can't see a freshly installed font, clear its cache
(rm -rf ~/.cache/matplotlib). The hosted service ships a CJK font
preconfigured, so figures there render Chinese out of the box.
Can I query the DuckDB store directly?¶
Yes — the store is the result, and the report is just one rendering of it.
study.duckdb is a plain DuckDB file; open it with the CLI, the Python API, or
anything that speaks DuckDB. The schema is:
| table | columns |
|---|---|
panel |
agent_id, step, state (JSON), action_kind, action_payload (JSON) |
metrics |
step plus one column per metric the study emitted |
events |
step, note |
messages (only if the message bus was used) |
author_id, step, round, channel, content, audience |
state and action_payload are JSON, so their inner keys are
study-specific — check your model.py for the field names before adapting
these:
-- who moved most between two steps, on a numeric state field
WITH s AS (
SELECT agent_id, step,
CAST(state ->> '$.opinion' AS DOUBLE) AS opinion
FROM panel WHERE step IN (0, 12)
)
SELECT a.agent_id,
a.opinion AS opinion_t0,
b.opinion AS opinion_t12,
b.opinion - a.opinion AS delta
FROM s a JOIN s b USING (agent_id)
WHERE a.step = 0 AND b.step = 12
ORDER BY abs(delta) DESC
LIMIT 20;
-- the per-agent rationale the LLM gave, for one step
SELECT agent_id, action_kind, action_payload ->> '$.reason' AS reason
FROM panel WHERE step = 5 AND reason IS NOT NULL;
-- how the action mix shifted over time
SELECT step, action_kind, count(*) AS n
FROM panel GROUP BY step, action_kind ORDER BY step, n DESC;
-- align a metric with the interventions that fired at the same step
SELECT m.*, e.note
FROM metrics m LEFT JOIN events e USING (step)
ORDER BY m.step;
Parquet exports (panel.parquet, metrics.parquet) sit alongside for
pandas/R.
Concepts¶
What makes this "longitudinal"? Other frameworks also run many steps.¶
The population is fixed with persistent ids, and one panel row is recorded per agent per step — so you can follow individual trajectories, not just aggregate curves. Cross-sectional frameworks typically resample crowds per experiment; here the same agents persist, which is what makes the output genuine panel data.
Where does the LLM actually sit?¶
In exactly one interface: DecisionModel.decide_batch. Environment,
population, storage, and reporting are LLM-free. See
LLM Clients & Dry Runs.
What stops the model from inventing numbers?¶
The grounding contract: load-bearing values cite a fact or a declared
assumption in grounding/grounding.json, and the report renders that
ledger next to the results. It's provenance for review, not a runtime
validator — see
Grounding & Provenance.
Extending¶
I have my own simulator. Can I plug it in?¶
Yes — that's Path C: wrap it behind the four interfaces with zero edits to its source, prove parity on a fixed seed, and it becomes a routable, forkable study.
How do I add my own data (personas, environment sources)?¶
Populations: point provider_ref at the generic file.personas provider
(CSV/Parquet with an agent_id column), or implement your own
PopulationProvider. Environment sources: declare them as layer sources
and materialize at build time. External services go through the
capabilities registry.