Files
Michael Stangl cded317a40 Doku: Farbprofile (--farben, cfg/farben.cfg, libs/Farben.py)
CLAUDE.md dokumentiert das neue SVG-Farbprofil-Feature: den Schalter
--farben, die Profile in cfg/farben.cfg (palette / kategorien) und das
Modul libs/Farben.py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 10:11:25 +02:00

15 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.json — the guest order: { "gruppen": [ ... ] }, each group holding a "personen" list (each with vorname/nachname/optional titel), an optional "anzahl" (reservation count — expands every person in the group into that many identical bookings), an optional "name" (display label), and an optional "tisch" to pin a VIP group to a specific table. Replaces the old bestellung.xml/XMLConfig.
  • Output (written back into the same --indir directory): TischePersonen.txt (table → seated people) and PersonenTische.csv (person → table, sorted).

The GA cycle and penalty scheme are read directly from $PLATZ_CFG/zyklus.cfg and $PLATZ_CFG/strafen.cfg (independent of --indir). There is no platz.cfg indirection any more — it was removed; zyklus.cfg/strafen.cfg are loaded straight from $PLATZ_CFG, the GA profile is chosen by the --zyklus-art <name> switch (default Adaptiv), the penalty profile by the --strafen <name> switch (default default), and the SVG colour profile by the --farben <name> switch (default grosse_gruppen).

  • cfg/zyklus.cfg — defines the GA profiles. Two flavours: static profiles (Easy, Simple) give an explicit 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 (Abfolge/Anzahl). The adaptive profile (Adaptiv, the default) is recognised by a Start field and has no fixed action list: Zyklus.Modus becomes 'adaptiv' and Farm._LaufAdaptiv() runs a generation loop with a shrinking population (Start, Schrumpfung, MinPopulation) and a convergence stop (Geduld generations without an improvement of at least Schwelle, capped by MaxGenerationen). Static profiles run via Farm._LaufStatisch().
  • cfg/strafen.cfg — the penalty ("Strafpunkte") profiles. Each section is one complete profile ("Vorgehen") in a single namespace, chosen by --strafen <section> (default default); this lets several scoring strategies live in one file (the shipped file also has a locker example with cheaper splits). Keys per profile: Anzahl_Trennungen_N ({groupsize:points} for a group split across N+1 tables, i.e. N splits — recursing to smaller sizes when no exact entry), NichtNachbar (no group-mate at a neighbouring table), Allein (a lone person), optional TischWertigkeiten ({freeseats:points} under-utilization, with a nearest-lower-key fallback; absent = free seats not penalised, as in the locker profile), and optional AbstandFaktor (per spatial-distance unit between split-off parts; absent = distance not penalised). Strafliste.laden(FileName, Sektion='default') parses one section; the old [Trennung]/[Globals]/[Abstand] three-section layout is gone.
  • cfg/farben.cfg — SVG colour profiles, chosen by --farben <section> (default grosse_gruppen) / env PLATZ_FARBENART, parsed by Farben.Farbschema.laden() (in libs/Farben.py) and passed into Sitzplatzverteilung.alsSVG(..., Farbschema=...). Two modes: Modus = palette (a Palette colour list cycled per group by sorted group id — the grosse_gruppen profile) and Modus = kategorien (the hochzeit profile — splits groups into Familie = members mostly share a surname, coloured along FamilienVerlaeufe gradients blue→green / yellow→orange, one gradient bucket per surname; Verein = any other named group, along VereinVerlauf violet→grey; VIP = fixed-table people with no GA group, the start of VIPVerlauf, red). Gradient syntax "start -> end". Anonymous bulk bookings (all members identical, e.g. an anzahl-expanded "Gast Musikverein") are not treated as a family. Absent/invalid config falls back to the built-in palette (Farbschema=None), so the SVG always renders.

tische.ini/bestellung.json/TischePersonen.txt/PersonenTische.csv are fixed filenames resolved as os.path.join(indir, ...) in platz.py.

Pass --tosvg to additionally write <indir>/Sitzplatzverteilung.svg via Sitzplatzverteilung.alsSVG() (see below). --show-development writes <indir>/Entwicklung.svg (value curves + pool-size panel), and --show-lineage writes <indir>/Abstammung.svg — the full mutation tree of every solution, with the final best solution's ancestral branch highlighted and labelled with its id. Every solution gets a plain running number (Farm._NeueId, 1..N); the ancestry is not encoded in the id but recorded via each node's stored predecessor (parent id) and followed back along the highlighted branch.

Pass --seed <int> to make a run fully reproducible: platz.py calls random.seed() with it before any GA action, so the same seed always yields the same seating (for support questions like "why is Aunt Erna seated there?" — rerun with the same seed to get the exact same plan). Reproducibility needs more than seeding the RNG: Person defines an Id-based __hash__/__eq__ (each booking has a unique running integer Id) so that the set() of people in a Gruppe iterates/pop()s in a process-stable order — otherwise the order would hinge on object memory addresses (and, for string-keyed containers, PYTHONHASHSEED), leaving the run non-reproducible even with --seed. Integer Ids aren't affected by PYTHONHASHSEED, so no environment tweaking is required.

Farm.TopN(n) returns the n best solutions (descending), clamped to [0, len(Pool)] — the list generalisation of Farm.Bester() (single best), for surfacing several good alternatives.

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. The Farm also tags every solution with a hierarchical lineage id (_absid; _WurzelId/_KindId) and records the mutation tree into self.Abstammung.
  • Entwicklung ("Development") — observer that records one measurement per cycle action (best/avg/worst value + pool size); alsSVG() draws the value curves plus a pool-size panel.
  • Abstammung ("Lineage") — observer that records the mutation tree (each solution's id, parent id, generation, value); alsSVG(HervorId=...) draws the tree and highlights/labels the ancestral branch of the given (usually the best) solution.

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' — the penalty rises with the number of free seats, with a nearest- lower-key fallback, so partly-filled tables are penalised most and groups get pulled onto a single table instead of being split next to empty ones).
  • 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 largest-first (GruppenSetzen(..., GrosseZuerst=True)) so big groups grab the now-freed whole tables and come back together instead of being re-split; the initial constructor placement still uses the random largest/smallest coin flip (GrosseZuerst=False). 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.
  • JSONConfig — parses bestellung.json into Personen/Gruppen/VIP-group/VIP-seat-map/group-names, expanding each person by the group's "anzahl" into that many individual bookings, and treating any group with a "tisch" key as a VIP group pinned to that table rather than going through normal GA placement. Returns a 5th element (GruppenNamen, {Gid: display name}) that the older XMLConfig did not; CSVConfig (the web CSV adapter) returns the same 5-tuple.

Data flow through platz.py (__main__)

  1. Parse --indir (required) and --zyklus-art (default Adaptiv) from the command line via optparse.
  2. Resolve zyklus.cfg/strafen.cfg directly as os.path.join($PLATZ_CFG, ...).
  3. Load Zyklus (GA profile named by --zyklus-art), Strafliste (penalties), Tische (table layout, from <indir>/tische.ini).
  4. Load <indir>/bestellung.json via JSONConfig → people, groups, VIP group, VIP seat map, group names.
  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.