Skip to content

Overview — B = f(P, E)

SocioVerse is built around one abstraction, borrowed from Kurt Lewin's field theory: behaviour is a function of the person and their environment, B = f(P, E). Everything in the runtime is a concrete answer to "what is P, what is E, and how does f close the loop over time".

Longitudinal, not cross-sectional

Frameworks like AgentSociety or OneSim excel at cross-sectional studies: many single-round experiments, often over freshly sampled crowds. SocioVerse targets the complementary question — how do the same people change over time?

That inverts two design choices:

cross-sectional SocioVerse (longitudinal)
population resampled per experiment fixed pool, persistent ids
environment mostly static per round dynamic — the thing that changes
output outcome distributions panel data: one row per agent per step

The persistent agent id is the longitudinal key: it is what lets you ask "show me household x-042 across all twelve steps" instead of only "what did the aggregate do".

The two-axis environment

E is dynamic along two independent axes, giving each agent a four-quadrant view of the world at every step:

macro local
physical the world's global state the agent's neighbourhood state
information broadcasts everyone receives feeds & messages scoped to this agent

Two kinds of change drive E forward:

  • Exogenous — scheduled interventions and information broadcasts you declare in the environment artifact ("a policy announcement at step 3").
  • Endogenous — the feedback of the agents' own actions (apply(actions) closes E_t → E_{t+1}).

The loop

sequenceDiagram
    participant Eng as Engine (loop)
    participant E as Environment (E)
    participant P as Population (P)
    participant D as Decision model (f)
    participant S as Panel store

    Note over Eng: t = 0 — build P once, reset E
    loop each step t = 1..N
        Eng->>E: advance_to(t) — exogenous events & broadcasts
        Eng->>E: observe_batch() → Observation[] (4-quadrant view)
        Eng->>D: decide_batch(observations) → Action[]  (= B)
        Eng->>E: apply(actions) — endogenous feedback
        Eng->>S: collect metrics + record panel rows
    end

The same loop, drawn with what actually flows through it — including the exogenous channel and the panel it leaves behind:

The main loop: E_t → B_t → E_{t+1}, with the exogenous channel and the per-step panel

Three properties of the loop are deliberate:

  1. P is built once. Personas get deterministic, persistent ids at build time and the pool never changes mid-run — that is what makes the output panel data.
  2. Decisions are batched. decide_batch receives all observations at once; there is no per-agent LLM call in a hot loop.
  3. Every step is recorded. One panel row per agent per step, plus aggregate metrics and fired events, land in a durable DuckDB store.

Typed hand-offs

The data flowing between stages is typed end to end:

Persona[]  →  Observation[]  →  Action[]  →  metrics

Each of these is a Pydantic model in socioverse/schemas/, and every stage boundary is guarded by strict validation — a malformed artifact fails at the gate, not three stages later.

Grounded by construction

A study is only as good as the numbers it is built from. Every study carries a grounding sidecar (grounding/grounding.json): facts with sources, modeling references, and declared assumptions. The build stages consult and extend it — load-bearing environment and population values cite a fact or an assumption id, and what cannot be sourced is declared rather than silently invented. See Grounding & Provenance.

Where each concept lives

concept in the code
the interfaces a study implements socioverse/abc/
the loop that drives them socioverse/engine/
the typed contracts socioverse/schemas/
reusable information-axis layers socioverse/env_layers/
the durable panel store socioverse/io/duckdb_store.py

Next: Anatomy of a Study — what's inside studies/<id>/.