205 lines
14 KiB
Markdown
205 lines
14 KiB
Markdown
# 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`; `ConfigParser` → `configparser`; `string.split` →
|
|
`str.split`; `xrange` → `range`; `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`), and the penalty
|
|
profile by the `--strafen <name>` switch (default `default`).
|
|
|
|
- `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.
|
|
|
|
`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.
|