Document the running-dinner concept, setup (uv + .env API key), CSV input schema, how to run it (CLI and library), output format, a "what to tweak" table mapping each knob to its file:line, the algorithm (group building, rotation topology, annealing), testing (offline + opt-in e2e), limitations, and development commands.
Tatami
Tool for Arranging Tasty Appointments, Meetings & Invitations.
Tatami generates a running dinner masterplan. Given a list of participants with home addresses, it forms small hosting groups, decides which groups cook which course and who visits whom, and orders the whole evening to minimize travel time (by bike, using the Google Maps Routes API), finishing at a shared after‑party.
- What is a running dinner?
- How Tatami builds the plan
- Requirements
- Setup
- Input: the participant CSV
- Running it
- Output format
- What you can tweak
- How the algorithm works
- Testing
- Limitations & scaling
- Project layout
- Development
What is a running dinner?
A running dinner is a social dinner event spread across many homes. Participants are split into hosting groups. The evening has three courses — starter, main, dessert — and each group cooks exactly one course in their own home. For the other two courses they travel to other groups' homes as guests. The tables are mixed for every course, so people meet many others over the night. Everyone converges on a common after‑party location at the end.
Tatami arranges all of this and tries to keep the total cycling time low.
How Tatami builds the plan
The pipeline (see the __main__ block of src/tatami/tatami_masterplan.py):
- Load participants from a tab‑separated CSV into
Participantobjects. - Fetch travel times — a full pairwise duration matrix between every participant address plus the after‑party address, via the Google Routes API.
- Build the masterplan:
- Rank participants by how convenient they are as hosts (kitchen‑size penalty plus distance to the after‑party), then split into hosts (one per group) and semi‑hosts (distributed into the host groups).
- Reduce the full matrix to a host‑to‑host matrix and bake in each host's kitchen‑size penalty.
- Run simulated annealing to find a low‑travel‑time assignment of groups to the dinner rotation.
- Assign each group a course and compute who hosts whom for each course.
- Return two lists of plain dicts (groups and participants) ready for serialization.
Requirements
- Python ≥ 3.13
- uv for dependency management
- A Google Maps API key with the Routes API enabled
(the new
routes.googleapis.comcomputeRouteMatrixendpoint — not the legacy Distance Matrix API).
Setup
# 1. Install dependencies (creates the virtualenv from uv.lock)
uv sync
# 2. Provide your API key
cp .env.example .env
# then edit .env and set GOOGLE_MAPS_API_KEY=...
The key is read from .env automatically (via python-dotenv). .env is
gitignored, so your secret never gets committed. An already‑exported
GOOGLE_MAPS_API_KEY environment variable takes precedence over the file.
The package raises at import time if no key is found, so
GOOGLE_MAPS_API_KEYmust be set (even to a dummy value) just to importtatami.traveltimes.
Input: the participant CSV
A tab‑separated file (the default working file is test-config.csv in the
directory you run from) with these columns:
| Column | Type | Meaning |
|---|---|---|
name |
string | Participant / household name. |
address |
string | Full postal address — this is what the Routes API geocodes. |
phone |
string | Contact number (carried through to the output, not used in routing). |
kitchen_size |
number | 0–10; a suitability‑to‑host proxy. Bigger = better kitchen. |
allergies |
string | Free text (carried through, not used in routing). |
Example (columns separated by tabs):
name address phone kitchen_size allergies
Alice Römerstr. 12, 76189 Karlsruhe 555-1234 8 none
Bob Gottesauerstr. 30, 76131 Karlsruhe 555-5678 7 peanuts
Charlie Hermann-Hesse-Str 50, 76189 Karlsruhe 555-8765 9 none
Running it
uv run python -m tatami.tatami_masterplan
This reads test-config.csv from the current directory, calls the Routes API,
and prints the generated masterplan. The after‑party address and travel mode are
currently set in the __main__ block of tatami_masterplan.py (see
What you can tweak).
To use Tatami from your own code:
from tatami.tatami_masterplan import (
get_after_party_group, get_masterplan, load_csv_to_participants,
)
from tatami.traveltimes import get_participant_distance_matrix
participants = load_csv_to_participants("my-participants.csv")
after_party = get_after_party_group("Some Street 1, 12345 City")
distance_matrix = get_participant_distance_matrix(
[*participants, after_party.main_member], mode="BICYCLE"
)
groups, participants_out = get_masterplan(participants, distance_matrix, after_party)
Output format
get_masterplan returns a tuple (group_dicts, participant_dicts).
Each group dict:
{
"uuid": "Group_…", # group id
"members": ["…", "…"], # participant uuids in this group
"main_member": "…", # the participant whose home is used for routing
"course": "starter", # "starter" | "main" | "dessert"
"hosts": ["…", "…", "…"], # the three groups this group eats with,
# ordered starter → main → dessert
# (includes this group itself, for its own course)
}
Each participant dict mirrors the CSV columns plus a uuid. Everything is
keyed by UUID, so resolve names/addresses by looking participants up by uuid.
Note: only a group's
main_memberaddress is used for all travel calculations; other members are assumed to join at the main member's home.
What you can tweak
All knobs currently live in the source. The most useful ones:
| What | Where | Default | Effect |
|---|---|---|---|
| After‑party address | tatami_masterplan.py:230 (__main__) |
a Karlsruhe address | Where everyone ends the night; also influences host ranking. |
| Travel mode | tatami_masterplan.py:233 (mode="BICYCLE") |
BICYCLE |
Any Routes API travelMode: BICYCLE, DRIVE, WALK, TWO_WHEELER, TRANSIT. |
| Group sizing | tatami_masterplan.py:24 (len(participants) / 6) |
1 group per ~6 people | The divisor sets how many participants form one "course‑triple". Larger → fewer, bigger groups. |
| Kitchen‑size penalty | classes.py:19 (minutes=3 * (10 - kitchen_size)) |
3 min per point | Travel‑time‑equivalent penalty for small kitchens. Raise the 3 to push hosting toward big kitchens. |
| Annealing schedule | tatami_masterplan.py:48–50 |
T=1000, cooling=0.99, iters=10000 |
Optimization quality vs. runtime. More iterations / slower cooling → better routes, slower. |
| Course names | tatami_masterplan.py:31 |
["starter", "main", "dessert"] |
The three courses. The 3‑course rotation is baked into the topology — changing the count needs more work (see below). |
| Rotation topology | tatami_masterplan.py:172–173 (offsets +1, -4) |
— | Defines who hosts whom. Changing these changes who meets whom; keep the invariant that each group's three hosts cover all three courses. |
| Distance vs. duration | traveltimes.py get_distance_matrix(value=…) |
"duration" |
Optimize on travel time ("duration") or distance ("distanceMeters"). |
After changing routing‑relevant knobs, run the test suite (uv run pytest) — the
topology and cost invariants are covered there.
How the algorithm works
Group building
groups_per_course = floor(n / 6) groups are created per course, for
3 × groups_per_course groups total. Participants are sorted by
get_after_party_time (kitchen penalty + distance to after‑party); the best
become the one host of each group, and the rest are shuffled in round‑robin
as semi‑hosts. A group's main_member (the host) is the only address used
for that group in all distance lookups.
Rotation topology (get_courses)
Each group occupies a slot 0 … n‑1. The course a slot cooks is slot % 3
(0 → starter, 1 → main, 2 → dessert). For slot i, the three groups it
dines with are slots i (itself, for its own course), i + 1, and i − 4
(mod n). These offsets are chosen so that:
- a group's three hosts always cover all three courses,
- every host serves exactly three groups (itself + two guests) for its course,
- guests are mixed differently at each course.
These invariants are verified in tests/test_routing.py.
Route optimization (simulated annealing)
The decision variable is which physical group sits in which slot. For a given
assignment, fast_total_time sums every group's route
(starter‑host → main‑host → dessert‑host → after‑party) using the reduced,
penalty‑baked matrix. simulated_annealing starts from a random assignment and
repeatedly proposes swapping two slots, accepting worse solutions with Boltzmann
probability exp(-Δ / T) while the temperature T cools, and returns the best
assignment it finds. It is a heuristic — good, not provably optimal — though
on small instances it reliably reaches the true optimum.
Testing
uv run pytest # full offline suite (no API calls, HTTP is mocked)
uv run pytest -m e2e # opt-in live test that calls the real Routes API
- The default suite (
tests/) covers the domain model, group building, the rotation topology, the route cost/optimization, and the Routes API wrapper (with the HTTP layer mocked) — no network, no API quota used. tests/test_e2e_api.pyis markede2eand excluded by default. Run it explicitly with-m e2e. It makes a single minimal request (two addresses → a 2×2, 4‑element matrix) and skips itself if only a placeholder/dummy key is available, so it never spuriously fails.
Limitations & scaling
- Participant count. The Routes API
computeRouteMatrixcaps at 625 elements (a 25×25 matrix), so Tatami currently handles up to ~24 participants plus the after‑party in one shot. Bigger events need batching. - Group counts are multiples of 3, and you need ≥ 6 participants before any groups are formed at all.
- Heuristic routing. Simulated annealing does not guarantee the global optimum on large instances; tune the schedule if results look poor.
- One address per group. Only the host's (
main_member's) address is used for routing; guests are assumed to gather there.
Project layout
src/tatami/
classes.py # Participant and Group domain model
traveltimes.py # Google Routes API wrapper + matrix helpers
tatami_masterplan.py # pipeline: load → fetch → group → optimize → assign
tests/ # pytest suite (offline + opt-in live e2e)
running_dinner/ # legacy standalone prototype — NOT used by the package
running_dinner/running_dinner.pyis a pre‑package prototype kept for reference only. It uses German field names, a brute‑force search, and the legacy Distance Matrix API. Don't assume its conventions apply tosrc/tatami/.
Development
This project uses uv and enforces quality with ruff and mypy.
uv sync # install (incl. dev tools)
uv run ruff check # lint
uv run ruff format # format
uv run mypy --allow-redefinition src/ # type-check
uv run pre-commit install # enable the pre-commit hooks
Pre‑commit runs ruff check, ruff format, and mypy automatically (see
.pre-commit-config.yaml).