Files
platz/CLAUDE.md
T
Michael Stangl 74e08f389c Add --tosvg switch to render seating as SVG, plus test images and docs
Sitzplatzverteilung.alsSVG() (libs/Strukturdaten.py) draws tables as
white circles and people as colored circles inside their table's
circle, colored by group membership. Table radius is derived from
the number of seats so all person-circles fit without touching; the
table-to-table grid spacing is derived from the largest table radius
so tables never overlap regardless of their Koordinaten values.

libs/platz.py gains a --tosvg switch that writes
<indir>/Sitzplatzverteilung.svg alongside the existing text/CSV
output.

The three unittest scenarios now each write their rendering to
doc/images/, and doc/docu_tests.md explains the three configurations
and what each test actually verifies (including why the "split
groups land on true neighbor tables" property only holds for a
specifically chosen seed in the circle scenario, not in general).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 12:55:32 +02:00

9.6 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project overview

platz (German: "Sitzplatzverteilung" / seating arrangement) is a genetic-algorithm-based solver that assigns people to tables ("Tische") for an event, given group memberships, table sizes/neighbor relationships, and a penalty scheme. It reads an XML order/guest list and INI table layout, runs a small custom GA framework to search for a good seating, and writes the result to text/CSV output files.

This is Python 3 code, migrated from an original Python 2 implementation (the git history / old comments may still say "Python 2" — that's now stale). Notable migration artifacts to be aware of when touching this code:

  • Gruppe, Loesung, and Sitzplatzverteilung use @functools.total_ordering with __eq__/__lt__ instead of the old Python 2 __cmp__ (Python 3 dropped __cmp__ and cmp() entirely; max()/.sort() need rich comparisons).
  • sets.Set → builtin set; ConfigParserconfigparser; string.splitstr.split; xrangerange; dict.has_key(k)k in dict; dict.iteritems()dict.items(); old-style raise Exception, "msg"raise Exception("msg").
  • Strukturdaten.LeuteAllein() was fixed to skip people with no entry in self.Gruppen.GvonPid (e.g. VIP-group members who were never added to the regular Gruppen container) instead of letting GruppeVonPid raise KeyError — this was a latent bug that only surfaced once VIP data (work/test2) was actually exercised end-to-end.
  • HandleBestellung() had a G.Anzahl comparison (G.Anzahl > 0) that used to silently compare a bound method to an int under Python 2's permissive cross-type ordering; Python 3 raises TypeError for that, which is what surfaced it. Fixed to call G.Anzahl().

There is no build system or linter configured in this repo. A tests/ unittest suite exists (see below); requirements.txt is still empty/commented — everything used is standard library — it's there only so bin/install_py.* has something to install into the .venv.

Running the program

Entry point is libs/platz.py, invoked via the scripts in bin/ (each provided as a .bat/.sh pair, following the "Standard Programm Template" convention — see ~/.claude/CLAUDE.md for the template rules):

  • bin/setenv.bat / bin/setenv.sh — derives PLATZ from the script's own location (no path editing needed), sets PLATZ_BIN, PLATZ_CFG, PLATZ_LIBS, PLATZ_WORK, PLATZ_IN, PLATZ_OUT, adds PLATZ_LIBS to PYTHONPATH (idempotently), and creates missing folders. Sourced by every other script; not normally invoked directly. The .sh variant must be sourced (source bin/setenv.sh), not executed.
  • bin/install_py.bat / bin/install_py.sh — calls setenv, then creates .venv (via py -m venv / python3 -m venv) and pip install -r requirements.txt if .venv doesn't already exist.
  • bin/activate_venv.bat / bin/activate_venv.sh — calls setenv, then activates the existing .venv (errors out if install_py hasn't been run yet).
  • bin/get_cmd.bat / bin/get_cmd.sh — calls setenv, then opens a new shell with the PLATZ_* environment already set.
  • bin/platz.bat / bin/platz.sh — calls setenv, then runs python "$PLATZ_LIBS/platz.py". This is the actual program entry point (replaces the old bin/run / bin/run.bat, which hardcoded absolute paths and have been removed).

libs/platz.py requires a command-line switch --indir <dir> (parsed with optparse) pointing at a directory that holds the per-run input files and receives the output files — e.g. work/test1/, work/test2/, or any new folder following the same layout:

  • tische.ini — table definitions: id, Nummer, Hof (venue/court), Plaetze (seats), Nachbarliste (neighbor table ids), Koordinaten.
  • bestellung.xml — the guest order: <Gruppe> blocks containing <Person> (with Vorname/Nachname/optional Titel) and an <Anzahl> (reservation count per person template — this expands into that many identical bookings), plus optional <Tisch> to pin a VIP group to a specific table.
  • Output (written back into the same --indir directory): TischePersonen.txt (table → seated people) and PersonenTische.csv (person → table, sorted).

cfg/platz.cfg (still located via $PLATZ_CFG, independent of --indir) only selects the GA cycle/penalty config:

  • cfg/zyklus.cfg — defines the GA run schedule ("Zyklus"): a sequence of actions (e=erzeugen/create, s=selektieren/select-best, S=random-select, m=mutieren/mutate, j=behalten/keep parents, z=neue Generation/advance generation) each with a count.
  • cfg/strafen.cfg — the penalty ("Strafpunkte") table: cost of splitting a group across tables (by group size and number of splits), cost of a lone person, and per-table under-utilization penalties.

tische.ini/bestellung.xml/TischePersonen.txt/PersonenTische.csv are fixed filenames resolved as os.path.join(indir, ...) in platz.py — they are no longer configured in platz.cfg.

Pass --tosvg to additionally write <indir>/Sitzplatzverteilung.svg via Sitzplatzverteilung.alsSVG() (see below).

Run the test suite with bin/run_tests.bat / bin/run_tests.sh (or directly: python -m unittest discover -s tests -p "test_*.py" -v). A stray global PYTHONPATH entry on this machine points at an unrelated project that also defines a tests package — this shadows import tests but does not affect unittest discover, which is what the run_tests scripts use.

Architecture

Two library modules under libs/, imported via PYTHONPATH=$PLATZ_LIBS:

ga.py — generic genetic-algorithm scaffolding

Domain-agnostic and reusable in principle:

  • Loesung ("Solution") — base class for anything bred/mutated/selected by the GA. Meant to be subclassed; Sitzplatzverteilung in Strukturdaten.py is the concrete solution type used here.
  • Zyklus ("Cycle") — parses Abfolge/Anzahl strings (from zyklus.cfg) into the ordered list of GA actions and their counts for one run.
  • Farm — owns the population (Pool/PoolNeu), and executes a Zyklus action-by-action against instances of a given solution class (passed as Klasse plus its constructor args/kwargs). Bester() returns the fittest (max()) solution found; solutions are ordered via __eq__/__lt__ (@total_ordering) on self.value.

Strukturdaten.py — domain model for this specific seating problem

  • Person, Gruppe (group — a Set of people, splittable via teilen()), Tisch (table — a Gruppe subclass with a seat capacity and neighbor-table ids).
  • Container/index classes: Personen, Gruppen, Tische, and Plaetze ("Seats") which indexes tables by how many free seats they currently have, to answer "give me a table with at least N free seats" or "give me two neighboring tables that together fit two groups of given sizes" efficiently during placement/mutation.
  • Strafliste ("Penalty list") — loads strafen.cfg and computes penalty points for: splitting a group (gibPunkte('Trennung', ...), recursing to smaller group sizes if no exact rule is configured), a person sitting with no group-mate at their table ('allein'), a person having no group-mate at a neighboring table ('nichtNachbar'), and poor seat utilization at a table ('Tischbesetzung').
  • Sitzplatzverteilung ("Seating arrangement") — the actual Loesung subclass bred by the GA. Constructor seats VIPs first at pinned tables, then randomly seats groups either largest-first or smallest-first (coin flip), splitting a group across two neighboring tables when no single table has enough free seats, recursively splitting further if needed, then calls bewerten() to score the arrangement per Strafliste. mutieren() picks the worst-scoring groups (SchlechteGruppenTopX), evicts them, and reseats them, hoping for a better score. speichern() writes the two output files. alsSVG() renders the layout to SVG: tables as white circles sized to fit all their seats' person-circles without touching, people as colored circles (color = group, via SVGGruppenFarben) placed evenly around the inside of their table's circle. The table-to-table grid spacing is derived from the largest table radius so tables never overlap regardless of their Koordinaten.
  • XMLConfig — parses bestellung.xml into Personen/Gruppen/VIP-group/VIP-seat-map, expanding each <Person> template by its sibling <Anzahl> into that many individual bookings, and treating any <Gruppe> containing a <Tisch> tag as a VIP group pinned to that table rather than going through normal GA placement.

Data flow through platz.py (__main__)

  1. Parse --indir (required) from the command line via optparse.
  2. Read platz.cfg to locate zyklus.cfg/strafen.cfg (env-var expansion via os.path.expandvars).
  3. Load Zyklus (GA schedule), Strafliste (penalties), Tische (table layout, from <indir>/tische.ini).
  4. Load <indir>/bestellung.xml via XMLConfig → people, groups, VIP group, VIP seat map.
  5. Build a Farm(Zyklus, Sitzplatzverteilung, Tische=..., Gruppen=..., Strafen=..., VIPListe=..., VIPs=...), which runs the whole GA cycle in its constructor.
  6. Take Farm.Bester() and call .speichern(...) to write <indir>/TischePersonen.txt and <indir>/PersonenTische.csv.

Both libs/platz.py and libs/Strukturdaten.py also have self-test code under if __name__ == '__main__': that exercises the classes directly with hardcoded sample data — useful as a reference for how the classes are meant to be constructed and used.