Wrap a Legacy Simulator (Path C)¶
You already have a working simulator — a Mesa model, a bespoke ABM, a domain-specific engine — and you want it to run as a SocioVerse study: longitudinal loop, panel store, versioning, reports, the whole surface. Path C wraps it behind the four core interfaces with zero edits to the original source.
The golden rule: never modify the legacy code
The original simulator's source stays byte-for-byte untouched. All glue lives inside your study directory. This keeps the wrapped project upgradeable and the adaptation honest — parity against the original remains provable.
The shape of an adaptation¶
studies/<your_study>/
├── adapter/
│ ├── engine_seam.py # the SEAM — locates & constructs the legacy engine
│ └── providers.py # the four interface implementations, calling through the seam
├── study.yaml # discovery fields: legacy_simulator: "<name>", …
└── environment/ population/ simulation/ # the standard artifacts
1. Stand up the engine-seam¶
The seam is a study-internal context manager that injects configuration (paths, workspace overrides) and constructs the legacy engine inside it — by import, or by monkey-patching its globals if the legacy code reads module-level state. Core never calls the seam; only your providers do.
Don't confuse the two "engines":
- the Core Engine =
LongitudinalSimulator, reused as-is; - the engine-seam = your glue around the legacy engine.
2. Map the four interfaces onto the legacy engine¶
| interface | typical mapping |
|---|---|
EnvironmentProvider |
project the legacy world state; tag observation pieces into the 4 quadrants (macro/local × physical/information); route scheduled events & broadcasts into advance_to(t); endogenous updates into apply |
PopulationProvider |
project legacy agents → Persona with deterministic persistent ids; set group_key as the batching cohort; encode the neighbour/network relation as an InteractionStructure |
DecisionModel |
call the legacy engine's (batched) decision step; project results to typed Actions — keep batching, no per-agent LLM loops |
MetricCollector |
wrap the legacy metrics; the DuckDB store is reused from Core |
3. Author the artifacts & register¶
Same as any study: the four artifacts plus @register on your provider
classes. Fill study.yaml's discovery fields — in particular
legacy_simulator: "<name>" — so the study joins the catalog and future
questions can be routed to a fork of it (Path A) instead of a rebuild.
4. Prove parity¶
Before trusting the wrap, assert it reproduces the legacy trajectory:
- run both the original and the wrapped simulator on the same seed,
- with a deterministic LLM client standing in for any LLM calls,
- and compare trajectories step by step.
The repository's test suite contains a worked parity test to mirror.
Gotchas from real migrations¶
Step-counter coupling
Legacy engines often keep implicit step state — e.g. reading
len(metrics_history) internally, with __init__ pre-seeding a step-0
entry. Your collector must match that contract exactly (append for
t ≥ 1, return the pre-seeded row at t = 0) or the legacy logic
silently desynchronizes.
One study per process
Seams that monkey-patch module globals are not parallel-safe. Run one study per process; batch experiments with a process pool, not threads.
Pin the legacy dependencies
Wrapped studies inherit the legacy project's dependency constraints (e.g. a pinned Mesa version). Declare them as a study-specific extra rather than widening Core's requirements — Core itself stays lean.
If the legacy project lives outside the repository, point the study at it
with an environment variable (the shipped reference migration uses
SV_ABM_ROOT) rather than a hard-coded path.