socioverse.engine — loop & assembly¶
The loop¶
socioverse.engine.loop ¶
LongitudinalSimulator — the E_t -> B_t -> E_{t+1} loop (the heart of beta).
Persistent population P built once; environment E evolves via two channels (advance_to = exogenous/scheduled, apply = endogenous feedback); each step records a panel row per agent + an aggregate metrics row. interaction_rounds>1 hosts intra-step agent-to-agent message exchange (scenario 2); =1 for Schelling.
LongitudinalSimulator ¶
LongitudinalSimulator(
*,
env: EnvironmentProvider,
population: PopulationProvider,
decision: DecisionModel,
store: TrajectoryStore,
collector: MetricCollector,
n_steps: int,
seed: int = 42,
interaction_rounds: int = 1,
memory_window: int = 8,
metric_columns: list[str] | None = None,
study_id: str = "study",
on_step: Any = None,
warm_start: Any = None,
)
Bases: Simulator
Source code in socioverse/engine/loop.py
materialize_initial ¶
Instantiate the fixed population P (persistent ids) and the initial environment E_0, returning the personas and the t=0 panel rows — WITHOUT opening the store or running any step. This is the "instantiate the agents" moment: shared by run() (its t=0 setup) and sv-build-population (which materializes the roster before any run). Idempotent for deterministic providers (a pure function of the seed).
Source code in socioverse/engine/loop.py
build_panel_rows ¶
build_panel_rows(
env: EnvironmentProvider,
personas: list[Persona],
actions,
t: int,
) -> list[TrajectoryRecord]
One panel row per persona = its current env state + (optional) action at step t.
Free function so both the loop and the pre-run materializer (engine.materialize_initial) build identically shaped rows the store + dashboard already understand.
Source code in socioverse/engine/loop.py
Assembly¶
socioverse.engine.builder ¶
build_simulator — generic, registry-driven assembly of a from-scratch study.
This is the Path B (build-new from Core) wiring: given the three validated bundles it
resolves the *_ref strings against the registry and wires a LongitudinalSimulator with
no per-study boilerplate. A sv-build-model-authored study only has to register its four abc
implementations; sv-run then calls this to run them.
Construction convention for the resolved classes (what a from-scratch study must honour):
- EnvironmentProvider / PopulationProvider -> cls(bundle) (sole arg is its bundle)
- DecisionModel -> cls(**sim.decision_args)
- MetricCollector -> cls(**sim.collector_args)
- TrajectoryStore -> built-in duckdb is special-cased;
any other store_ref is resolve("store", ref)(store_path, study_id=...)
Studies that wrap a legacy engine and need shared mutable state between providers
(e.g. chicago's ChicagoEngine) keep their own build_*_simulator factory instead — Core
never calls those. See CLAUDE-dev.md for that (Path C) path.
build_providers ¶
Resolve just the environment + population providers (the P and E of a study) from the
registry — no decision model, collector, or store. Same cls(bundle) convention as
build_simulator. Path-B only: studies whose providers share a mutable engine (chicago's
ChicagoEngine) must use their own materializer so both providers see one engine.
Source code in socioverse/engine/builder.py
materialize_initial ¶
materialize_initial(
env_bundle: EnvironmentBundle,
pop_bundle: PopulationBundle,
seed: int = 42,
) -> tuple[list[Persona], list[TrajectoryRecord]]
Instantiate P + E_0 for a from-scratch (Path-B) study and return
(personas, t0_panel_rows) — the "instantiate the agents" step, with no run and no
SimulationConfig (decision/collector/store are irrelevant to t=0). sv-build-population calls
this to write the initialized roster BEFORE sv-run exists. Deterministic in seed.
Source code in socioverse/engine/builder.py
build_simulator ¶
build_simulator(
*,
env_bundle: EnvironmentBundle,
pop_bundle: PopulationBundle,
sim_config: SimulationConfig,
store_path: str | Path,
on_step: Any = None,
) -> LongitudinalSimulator
Resolve refs from the registry and wire a runnable LongitudinalSimulator.
The study's model module must already be imported so its @register(...) decorators have
run (importing studies.<id> is enough if its __init__ imports model).
Source code in socioverse/engine/builder.py
Registry¶
socioverse.engine.registry ¶
Component registry — resolves bundle *_ref strings to concrete classes.
A study's model.py registers its providers/decision model/collector under string keys; the bundles reference those keys, so the engine can wire a study with no hard imports.
register ¶
Class decorator: register(kind, key)(cls). kind in _KINDS.
Source code in socioverse/engine/registry.py
Agent memory¶
socioverse.engine.memory ¶
AgentMemory — a per-agent rolling history that makes behavior genuinely longitudinal.
Each persistent agent keeps its own bounded trace of (step, action, observation summary) so a DecisionModel can condition on the agent's own past ("I already moved twice; I'll stay"). Optional for Schelling; essential for opinion-dynamics models (HiSim).
Message bus¶
socioverse.engine.messaging ¶
InMemoryMessageBus — reference inter-agent communication medium (scenario 2).
A study's EnvironmentProvider (e.g. HiSim) owns one of these: post lands an agent's
message; visible_to returns the messages a recipient sees from its network neighbours
in the current (step, round). The core simulation loop never touches the bus directly —
agent communication is just how a study's env implements its local-information layer.
InMemoryMessageBus ¶
Bases: MessageBus
Source code in socioverse/engine/messaging.py
visible_to ¶
visible_to(
recipient_id: str,
neighbors: list[str],
step: int,
round_idx: int = 0,
) -> list[dict[str, Any]]
Messages from neighbors posted earlier this step (round < round_idx) and,
optionally, the previous step's messages.