API reference
Every public method on GroceryStoreSimulation — signature, parameters, and real output
This page walks through the whole public surface of grocery_sim: one class, GroceryStoreSimulation, plus the small SimulationData helper its .data() method returns. Every example on this page actually ran — output is real, trimmed for length in a few places (the validation report, describe(), erd()) but never invented.
from grocery_sim import GroceryStoreSimulationGroceryStoreSimulation()
Signature: GroceryStoreSimulation()
Description: The constructor. Takes no arguments and doesn’t run anything yet.
Returns: A new instance, with .settings and .validation both None until .setup() and .simulate() are called.
Example
sim = GroceryStoreSimulation()
print(sim.settings) # -> None
print(sim.validation) # -> None.setup(settings)
Signature: sim.setup(settings: dict | None = None) -> GroceryStoreSimulation
Description: Validates settings against the schema (settings.py) and merges it onto the package’s own defaults. A partial dict is always enough — missing keys and sections just inherit their default. Does not run anything yet.
Parameters:
settings— a (possibly partial) settings dict, with up to three top-level sections:basic,events,potential_investment.Noneresolves to the full defaults, unmodified.
Raises: SettingsError on anything invalid — an unknown key, an event date outside the horizon, a three-year-only field on a one-year run, and so on.
Returns: self, so it chains into .simulate().
Example
sim.setup(dict(basic = dict(year = 1)))
print(list(sim.settings.keys()))
print(sim.settings["basic"])Output
['basic', 'events', 'potential_investment']
{'name': 'My Grocery Store', 'random_seed': 20260712, 'year': 1,
'budget': 60000, 'year_start': '2025', 'retain_earning': False,
'retain_earning_from': None}
Every key not mentioned in the call above — random_seed, budget, year_start, and everything under events/potential_investment — is filled in from DEFAULTS, not left unset.
.simulate(out_dir=None)
Signature: sim.simulate(out_dir: str | Path | None = None) -> GroceryStoreSimulation
Description: Runs the full phase1 → phase2 → phase3 → recording → export pipeline, then self-validates the result.
Parameters:
out_dir— where to write the exported CSVs.None(the default) writes to a fresh temp directory, cleaned up later by.cleanup(). Pass a real path to keep the export.
Raises: ValidationError if a structural invariant fails — that always means a generator bug, never an ordinary consequence of the settings (see .validation below for the structural/reported-only distinction).
Returns: self.
Example
sim.simulate()Output
=== simulating: My Grocery Store ===
phase 1: location 5 (Q=0.33), 128 SKUs, 259 customers, believed profit 3,195 [1.7s]
phase 2: 5 events (energy_crisis, commodity_spike, import_disruption, import_disruption, commodity_spike)
calibrated outside option u0 = 3.88
Validation — structural invariants (must always hold):
PASS conservation (init+purchased-sold-spoiled == ending) max err 0.00e+00
PASS zero sales on closure days
PASS card-ID long tail (many one-off tokens) 1112 ids, 77% appear <=2x
...
PASS dirty layer: every defect family present in the hidden ledger 637 ledger rows across 10 families
.validation
Type: dict | None, set by .simulate().
Structure: {"structural_ok": bool, "checks": list[dict]}. Each check dict is {"name": str, "pass": bool, "detail": str, "tier": str} — tier is "structural" (raises on failure, always a generator bug) or "reported, not fatal" (a band calibrated against the baseline, expected to drift once events are active, printed for visibility only).
Example
print(list(sim.validation.keys()))
print(sim.validation["structural_ok"])
print(len(sim.validation["checks"]))
print(sim.validation["checks"][0])Output
['structural_ok', 'checks']
True
32
{'name': 'conservation (init+purchased-sold-spoiled == ending)',
'pass': True, 'detail': 'max err 0.00e+00', 'tier': 'structural'}
.describe()
Signature: sim.describe() -> str
Description: Builds a fictional owner (persona.py — name, prior career, age; cosmetic only) who narrates this run’s own real settings and results as a letter, an intake-interview Q&A, a data table, a list of questions, and the stakes — grounded entirely in this run’s actual numbers, nothing templated. Reproducible per random_seed. Also prints the brief to stdout as a side effect, in addition to returning it.
Returns: The brief as a string.
Example
text = sim.describe()Output (4,402 characters in full: the letter, a full intake interview, the data dictionary, the client’s own questions, and the stakes — shown here trimmed)
# Vinter's Market — Engagement Brief
*Client: Mikael Vinter, owner, Vinter's Market, Granstigen, Tjornby.*
## The letter
> Dear analyst,
>
> I'm 46. I spent 13 years working for Kornmagasinet before I opened
> Vinter's Market myself, on Granstigen in Tjornby, with about 60,000
> euros of my own money behind it. That was 2025.
>
> Over one year we have taken in about 743,000 euros across the till
> altogether, and the bottom line came out to a real profit. I want
> someone who isn't me to look at my numbers properly before I decide
> whether to expand further or simply stay the course.
>
...
*(This brief is generated from this run's own settings and its own real
results — no hidden simulation parameter appears above. Edit freely;
this is a starting outline, not a finished case.)*
Note: the owner’s name, story, and phrasing change with random_seed — this is the same method the Exemplar analyses pages and the generated briefs page are built from.
.create_analysis(path, mode="student")
Signature: sim.create_analysis(path: str | Path, mode: str = "student") -> Path
Description: Writes a real marimo notebook (.py, marimo’s native format) to path, pre-wired to load this run’s own exported tables into a DuckDB connection named con, with one cell per analysis-catalog layer already stubbed with that layer’s own questions as markdown prompts.
Parameters:
path— where to write the notebook.mode—"student"leaves every cell as a prompt with a# your queries hereplaceholder."instructor"additionally fills in a worked first pass.
Returns: The path it wrote to.
Example
path = sim.create_analysis(
path = "analysis.py",
mode = "student",
)Output (1,659 characters in student mode for this run; instructor mode is 2,184 — the extra length is the worked first pass added to each cell — shown here trimmed)
import marimo
__generated_with = "0.23.14"
app = marimo.App(width="full", app_title='My Grocery Store')
@app.cell
def _():
from pathlib import Path
import duckdb
import marimo as mo
import pandas as pd
VISIBLE = Path('/tmp/grocery_sim_t_13ly56/visible')
con = duckdb.connect(database = ":memory:")
for csv in sorted(VISIBLE.glob("*.csv")):
con.execute(
f"CREATE OR REPLACE TABLE {csv.stem} AS SELECT * FROM read_csv_auto('{csv.as_posix()}')"
)
mo.md("# Analysis — tables loaded into `con` (duckdb)")
return con, mo, pd
@app.cell
def _layer0cleantherecords(con, mo):
mo.md("## Layer 0 — Clean the records\n- Do the receipts reconcile to the ledger's revenue line?\n- Does book stock reconcile against deliveries, sales, and write-offs?")
# your queries here
return
@app.cell
def _layer1describethebusiness(con, mo):
mo.md('## Layer 1 — Describe the business\n- Where does the money come from and go?\n- When do people shop?')
# your queries here
return
.export_settings(path)
Signature: sim.export_settings(path: str | Path) -> None
Description: Writes sim.settings (already resolved and validated, not the partial dict passed to .setup()) to path as indented JSON — the natural way to save a specific run’s configuration for later reuse: GroceryStoreSimulation().setup(json.load(open(path))).
Parameters:
path— where to write the JSON file.
Returns: None.
Example
sim.export_settings("settings.json")
print(open("settings.json").read())Output
{
"basic": {
"name": "My Grocery Store",
"random_seed": 20260712,
"year": 1,
"budget": 60000,
"year_start": "2025",
"retain_earning": false,
"retain_earning_from": null
},
"events": {
"tax_cut": null,
"tax_raise": null,
"food_vat_cut": null,
"typhoon": null,
"competitor": null,
"operational_hazard": null,
"war": null
},
"potential_investment": {
"more_staff": true,
"bigger_store": false,
"upgrade_infrastructure": false
}
}.cleanup()
Signature: sim.cleanup() -> None
Description: Removes the temp export directory, if .simulate() was called with out_dir=None (its default). Safe to call unconditionally — a no-op if nothing was ever simulated, or if a real out_dir was given (your own directory is never deleted for you).
Returns: None.
Example
print(sim._out_dir.exists())
sim.cleanup()
print(sim._out_dir.exists())Output
True
False
SimulationData
Description: The object returned by .data(). A thin wrapper, not a dict subclass, but it supports attribute access, item access, and iteration over table names.
Interface:
tables.receipts— attribute accesstables["receipts"]—__getitem__list(tables)—__iter__over table namestables.keys()— same aslist(tables), spelled the familiar wayrepr(tables)— the compact schema summary shown under.data()above
Example
tables = sim.data()
tables.receipts
tables["receipts"]
list(tables)
tables.keys()Further reading
This page covers the object model; Theory covers why it’s built this way (the layer split that makes counterfactuals and grading possible), the Analysis catalog is 62 worked questions against these same methods, and Exemplar analyses are two full worked cases end to end.