Strafen können durch benannte Profile festgelegt werden

This commit is contained in:
Michael Stangl
2026-07-15 17:05:13 +02:00
parent db69f3498d
commit 4a059f32c5
5 changed files with 114 additions and 53 deletions
+15 -6
View File
@@ -72,8 +72,9 @@ the same layout:
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`, and
the GA profile is chosen by the `--zyklus-art <name>` switch (default `Adaptiv`).
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/
@@ -84,10 +85,18 @@ the GA profile is chosen by the `--zyklus-art <name>` switch (default `Adaptiv`)
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") table: cost of splitting a group across
tables (by group size and number of splits), cost of a lone person, per-table
under-utilization penalties, and (optional `[Abstand]` section) a penalty for the spatial
distance between split-off parts of a group.
- `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`.
+51 -33
View File
@@ -1,38 +1,56 @@
# Strafpunkte fuer die Trennung einer Gruppe:
# Aufbau: Anzahl der Trennungen = { Gruppengroesse:Strafpunkte, .. }
# Deutlich hoeher als frueher, damit eine Trennung teurer ist als der Gewinn,
# einen Tisch dafuer teilbelegt zu lassen (sonst werden Gruppen getrennt,
# obwohl daneben leere Tische stehen).
[Trennung]
1={ 2:-25, 3:-22, 4:-18, 6:-14, 8:-12, 10:-10 }
2={ 3:-40, 4:-30, 6:-22, 8:-18, 10:-14 }
3={ 4:-60, 6:-40, 8:-30, 10:-20 }
# Strafpunkte-Profile ("Vorgehen") fuer die Bewertung einer Sitzplatzverteilung.
#
# Jede Sektion ist EIN vollstaendiges Bewertungsprofil mit allen Angaben. So
# lassen sich mehrere Vorgehensweisen nebeneinander pflegen und speichern. Per
# Schalter --strafen <Sektionsname> waehlt man ein Profil; ohne Schalter wird
# [default] verwendet.
#
# Schluessel je Profil:
# Anzahl_Trennungen_N = { Gruppengroesse:Strafpunkte, .. }
# Strafpunkte fuer das Aufteilen einer Gruppe (N = Anzahl der Trennungen,
# also auf N+1 Tische verteilt), gestaffelt nach Gruppengroesse. Fehlt ein
# exakter Gruppengroessen-Eintrag, wird auf die naechstkleinere Groesse
# zurueckgegriffen. Mindestens Anzahl_Trennungen_1 sollte gesetzt sein.
# NichtNachbar = Strafe, wenn zwei Gruppenteile nicht am Nachbartisch sitzen
# Allein = Strafe, wenn eine Person ohne Gruppenmitglied am Tisch sitzt
# TischWertigkeiten = { freiePlaetze:Strafpunkte, .. } fuer freie Sitze an einem
# (teil-)belegten Tisch. Fehlt ein exakter Eintrag, gilt der
# naechstkleinere. Teilbelegte Tische sind am schlechtesten
# (dort passt keine ganze Gruppe mehr hin), daher steigt die
# Strafe mit den freien Plaetzen; ein komplett leerer Tisch
# (hoechster Schluessel) ist etwas besser, aber nicht gratis,
# damit sich das Zusammenziehen von Gruppen lohnt.
# Optional; fehlt der ganze Schluessel, werden freie Plaetze
# gar nicht bestraft -> mehr Teilbelegungen moeglich.
# AbstandFaktor = Strafpunkte je normierter Abstandseinheit (0..100) zwischen
# getrennten Teilen einer Gruppe. Der Abstand ist auf die
# maximale Ausdehnung der Tischanordnung normiert (100 =
# ein Teil ganz links/oben, der andere ganz rechts/unten).
# AbstandFaktor=-0.2 kostet den Maximalabstand also -20.
# Optional; fehlt der Schluessel, wird der Abstand nicht
# bestraft (Faktor 0).
# Wieviele Strafpunkte bekommt man
# wenn zwei Gruppen nicht am Nachbartisch sitzen
# wenn einer aus einer Gruppe allein sitzen muss
[Globals]
# Standardprofil: Trennungen sind bewusst teuer, damit eine Gruppe lieber
# zusammen an einem teilbelegten Tisch sitzt, als neben einem leeren getrennt zu
# werden.
[default]
Anzahl_Trennungen_1={ 2:-25, 3:-22, 4:-18, 6:-14, 8:-12, 10:-10 }
Anzahl_Trennungen_2={ 3:-40, 4:-30, 6:-22, 8:-18, 10:-14 }
Anzahl_Trennungen_3={ 4:-60, 6:-40, 8:-30, 10:-20 }
NichtNachbar=-3
Allein=-30
TischWertigkeiten={ 1:-3, 2:-3, 4:-5, 6:-6, 8:-4 }
AbstandFaktor=-0.2
# TischWertigkeiten sind Strafpunkte fuer freie Sitze an einem Tisch, je nach
# Anzahl freier Plaetze (fehlt ein exakter Eintrag, gilt der naechstkleinere).
# Ziel: teilbelegte Tische sind am schlechtesten (dort passt keine ganze
# Gruppe mehr hin und die Sitzenden haben leere Nachbarplaetze), daher steigt
# die Strafe mit den freien Plaetzen an (1..7 frei: -3 -> -6). Ein *komplett*
# leerer Tisch ist etwas besser als ein halbleerer (-4 bei 8 frei), weil ein
# leerer Tisch als Ganzes fuer eine Gruppe frei bleibt - er soll aber nicht
# gratis sein, damit sich das Zusammenziehen von Gruppen lohnt.
# Strafpunkte fuer den Abstand zwischen getrennten Teilen einer Gruppe.
# Wird eine Gruppe auf mehrere Tische aufgeteilt, ist ein grosser raeumlicher
# Abstand zwischen den Teilen viel schlechter als ein kleiner.
# Der Abstand wird als normierter Wert 0..100 berechnet: Bezugsgroesse ist die
# maximale Ausdehnung der Tischanordnung (Breite oder Laenge). Ein Abstand von
# 100 heisst also, dass ein Gruppenteil ganz links/oben und der andere ganz
# rechts/unten sitzt.
# Faktor = Strafpunkte je normierter Abstandseinheit. Bei Faktor=-0.2 kostet
# der maximal moegliche Abstand (100) demnach -20 Punkte.
[Abstand]
Faktor=-0.2
# Beispiel fuer ein alternatives Vorgehen: Trennungen sind guenstiger, der
# Abstand wird nicht bestraft, und freie Plaetze werden gar nicht bestraft
# (TischWertigkeiten fehlt bewusst) — so sind mehr Teilbelegungen moeglich.
# Nuetzlich fuer grosse Saele, in denen sich Gruppen kaum ungeteilt platzieren
# lassen. Aufruf: --strafen locker
[locker]
Anzahl_Trennungen_1={ 2:-12, 3:-10, 4:-8, 6:-6, 8:-5, 10:-4 }
Anzahl_Trennungen_2={ 3:-20, 4:-16, 6:-12, 8:-9, 10:-7 }
Anzahl_Trennungen_3={ 4:-30, 6:-22, 8:-16, 10:-12 }
NichtNachbar=-2
Allein=-20
AbstandFaktor=0.0
+35 -11
View File
@@ -437,21 +437,45 @@ class Strafliste:
return self.gibPunkte('Trennung',Gruppengroesse-1, N_Teilungen)
else:
raise IndexError("Bezeichner gibts nicht")
def laden( self, FileName ):
print("-- Strafliste laden")
def laden( self, FileName, Sektion='default' ):
"""laedt ein Strafpunkte-Profil aus einer Sektion der strafen.cfg.
Jede Sektion ist ein vollstaendiges Bewertungs-'Vorgehen' mit allen
Angaben in einem gemeinsamen Namensraum. Der Aufrufer waehlt das
Profil ueber 'Sektion' (Vorgabe 'default'); so lassen sich mehrere
Bewertungsstrategien in derselben Datei pflegen. Die Trennungs-
Straffen stehen unter den Schluesseln 'Anzahl_Trennungen_N'."""
print("-- Strafliste laden (Profil '%s')" % Sektion)
config = configparser.ConfigParser()
config.read( os.path.expanduser(FileName) )
if not config.has_section( Sektion ):
raise ValueError(
"Strafprofil '%s' nicht in %s gefunden (vorhanden: %s)"
% ( Sektion, FileName, ', '.join(config.sections()) or '-' ) )
# ast.literal_eval statt eval: die Werte sind reine Literale
# (Zahlen und Dicts wie {2:-10}), also kein Code noetig.
self.NichtNachbar = ast.literal_eval(config.get( 'Globals', 'NichtNachbar' ))
self.Allein = ast.literal_eval(config.get( 'Globals', 'Allein' ))
self.TischWert = ast.literal_eval( config.get( 'Globals', 'TischWertigkeiten' ) )
# Abschnitt [Abstand] ist optional; fehlt er, wird der Abstand
# getrennter Gruppen nicht bestraft (Faktor 0).
if config.has_section( 'Abstand' ):
self.AbstandFaktor = ast.literal_eval(config.get( 'Abstand', 'Faktor' ))
for (k,v) in config.items( 'Trennung' ):
self.GruppenTrennen[int(k)] = ast.literal_eval(v)
self.NichtNachbar = ast.literal_eval(config.get( Sektion, 'NichtNachbar' ))
self.Allein = ast.literal_eval(config.get( Sektion, 'Allein' ))
# TischWertigkeiten ist optional; fehlt der Schluessel, werden freie
# Plaetze gar nicht bestraft (leeres Dict -> gibPunkte('Tischbesetzung')
# gibt 0), wodurch mehr Teilbelegungen moeglich sind.
if config.has_option( Sektion, 'TischWertigkeiten' ):
self.TischWert = ast.literal_eval( config.get( Sektion, 'TischWertigkeiten' ) )
else:
self.TischWert = {}
# AbstandFaktor ist optional; fehlt er, wird der Abstand getrennter
# Gruppen nicht bestraft (Faktor 0).
if config.has_option( Sektion, 'AbstandFaktor' ):
self.AbstandFaktor = ast.literal_eval(config.get( Sektion, 'AbstandFaktor' ))
# Trennungs-Straffen: Schluessel 'Anzahl_Trennungen_N' -> N Trennungen.
# frisches Dict, damit nicht der (mutable) Konstruktor-Default geteilt
# wird und Reste eines frueheren laden() bleiben.
self.GruppenTrennen = {}
for (k, v) in config.items( Sektion ):
# configparser liefert Schluessel klein geschrieben
if k.startswith( 'anzahl_trennungen_' ):
N = int( k.rsplit( '_', 1 )[-1] )
self.GruppenTrennen[N] = ast.literal_eval(v)
# Sitzplatzverteilung, bestehend aus den Einzelelementen
+9 -1
View File
@@ -51,6 +51,10 @@ if __name__ == '__main__':
parser.add_option( "--zyklus-art", dest="zyklusart", default="Adaptiv",
help="Name des GA-Profils in zyklus.cfg (Vorgabe: Adaptiv). "
"Weitere mitgelieferte Profile: Easy, Simple" )
parser.add_option( "--strafen", dest="strafen", default="default",
help="Name des Strafpunkte-Profils (Sektion in strafen.cfg; "
"Vorgabe: default). Erlaubt mehrere Bewertungs-Vorgehen in einer "
"Datei, z.B. --strafen locker" )
parser.add_option( "--seed", dest="seed", type="int", default=None,
help="Zufalls-Startwert (int) fuer random.seed(), macht den Lauf "
"reproduzierbar. Ohne Angabe laeuft der GA nichtdeterministisch. "
@@ -83,6 +87,7 @@ if __name__ == '__main__':
Zyklusconfig = os.path.join( CfgDir, 'zyklus.cfg' )
Zyklusart = options.zyklusart
Strafenconfig = os.path.join( CfgDir, 'strafen.cfg' )
Strafenart = options.strafen
# lade Eingabedateien
Tischconfig = os.path.join( InDir, 'tische.ini' )
@@ -99,7 +104,10 @@ if __name__ == '__main__':
Z.laden( Zyklusconfig, Zyklusart )
print(Z)
SL = Strafliste()
SL.laden( Strafenconfig )
try:
SL.laden( Strafenconfig, Strafenart )
except ValueError as Fehler:
parser.error( str(Fehler) )
print(SL)
TE = Tische()
TE.laden( Tischconfig )
+4 -2
View File
@@ -56,15 +56,17 @@ app = FastAPI(
# Zyklus und Strafliste einmal beim Start laden (wie in libs/platz.py):
# zyklus.cfg und strafen.cfg direkt aus dem cfg-Verzeichnis (PLATZ_CFG bzw.
# Projekt-cfg). GA-Profil per Umgebungsvariable PLATZ_ZYKLUSART (Vorgabe:
# Adaptiv).
# Adaptiv), Strafpunkte-Profil per PLATZ_STRAFENART (Vorgabe: default) —
# analog zum Schalter --strafen in libs/platz.py.
ZYKLUSDATEI = os.path.join( CFGDIR, 'zyklus.cfg' )
STRAFENDATEI = os.path.join( CFGDIR, 'strafen.cfg' )
ZYKLUSART = os.environ.get( 'PLATZ_ZYKLUSART', 'Adaptiv' )
STRAFENART = os.environ.get( 'PLATZ_STRAFENART', 'default' )
GAZyklus = Zyklus()
GAZyklus.laden( ZYKLUSDATEI, ZYKLUSART )
Strafen = Strafliste()
Strafen.laden( STRAFENDATEI )
Strafen.laden( STRAFENDATEI, STRAFENART )
# ---------------------------------------------------------------- Sessions
SESSION_TTL_SEKUNDEN = 24 * 3600