2 Commits

Author SHA1 Message Date
lars 6b0fca0100 Move Participant/Group to pydantic and add a saveable/editable Plan model
Participant and Group are now pydantic BaseModels. Group.hosts can form
cycles between groups, so it's kept as a private, non-persisted live list
(set via set_hosts()/add_host()) backed by a serializable host_uuids field,
re-linked via resolve_hosts() after a reload.

A new Plan model (src/tatami/plan.py) bundles groups, the after-party group,
and the event config (course_times, organizer_contacts, info_text,
spreadsheet_id) and supports save()/load() to/from JSON. tatami_masterplan's
__main__ now saves to masterplan.json (PLAN_FILE env var to override) on
first run and loads it on later runs instead of recomputing, so the plan can
be hand-edited (move a member between groups, change a course, fill in
spreadsheet_id) and picked up on rerun without hitting the Routes API again.
spreadsheet_id moves out of .env (GOOGLE_SHEETS_SPREADSHEET_ID) onto the plan
itself, since it's part of the plan rather than a secret.
2026-06-19 16:07:47 +02:00
lars 4da624bfcf Fix route optimizer and group assignment; add test suite
Route building / optimization (tatami_masterplan.py):
- fast_total_time was permutation-invariant: it applied get_courses to
  group *values* instead of slots and ignored the permutation, so every
  ordering scored identically and the annealing optimized nothing. It now
  maps each rotation slot to its assigned group via the permutation.
- Replaced the broken next_permutation/simulated_annealing (enumerated n!
  orderings per iteration, fed unnormalized Boltzmann weights to
  np.random.choice -> ValueError, and returned the last random sample) with
  a standard neighbor-swap annealer that tracks and returns the best
  solution and handles <2 slots.
- Convert the reduced Timedelta matrix to float seconds before annealing
  (np.exp can't operate on Timedelta).

Group building (tatami_masterplan.py):
- assign_courses set each group's hosts (sorted by course) before all
  courses were assigned, so hosts whose course was still None got
  mis-ordered. Assign all courses first, then wire up hosts.
- get_masterplan no longer mutates the caller's participant list.

classes.py:
- Narrow casts on distance-matrix lookups to satisfy the mypy gate
  (pre-existing failures).

Tests:
- Add pytest suite (74 tests) covering the rotation topology, route cost
  and optimization, the domain model, the masterplan pipeline, and the
  Routes API wrapper (HTTP mocked). The cost cross-check caught the
  assign_courses ordering bug above.
2026-06-19 14:19:00 +02:00