Skip to content

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.