Bessere Verteilung: Strafen/Mutation, plus Abstammungsbaum mit Id-Herkunft

Weniger vermeidbare Gruppentrennungen:
- strafen.cfg: Tischbesetzung bestraft jetzt gestaffelt nach freien
  Plaetzen (teilbelegte Tische am teuersten, mit Fallback auf den
  naechstkleineren Eintrag), Trennungsstrafen deutlich erhoeht - so
  werden Gruppen nicht mehr getrennt, obwohl leere Tische danebenstehen
- gibPunkte('Tischbesetzung') mit Naechst-kleiner-Fallback
- mutieren() setzt geraeumte Gruppen groesste-zuerst neu
  (GruppenSetzen(GrosseZuerst=True)), damit grosse Gruppen die frei
  gewordenen ganzen Tische bekommen und wieder zusammenkommen
  (uebrig bleiben nur physisch unvermeidbare Trennungen, Gruppe > Tisch)

Abstammungsbaum (neuer Schalter --show-lineage -> Abstammung.svg):
- neue Klasse ga.Abstammung zeichnet den vollen Mutationsbaum aller
  Loesungen; der Ast der finalen besten Loesung ist hervorgehoben und
  mit ihrer Id beschriftet
- hierarchische Loesungs-Ids (Farm._WurzelId/_KindId): Wurzel '7',
  Mutationskind '7.2', dessen Kind '7.2.1' - die ganze Abstammung ist
  direkt aus der Id ablesbar

Tests (15 gruen), README und CLAUDE.md aktualisiert; Beispiele mit den
neuen Strafen und beiden Grafiken neu berechnet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Michael Stangl
2026-07-11 22:38:43 +02:00
parent 644f61ef6b
commit 6710d4f06a
28 changed files with 3908 additions and 1468 deletions
+39 -12
View File
@@ -398,10 +398,20 @@ class Strafliste:
# je groesser der Abstand, desto mehr Strafe.
return self.AbstandFaktor * Abstand
elif Typ == 'Tischbesetzung':
try:
# Strafe fuer freie Plaetze an einem (teil-)belegten Tisch. Gibt
# es keinen exakten Eintrag fuer 'freiePlaetze', wird auf den
# naechstkleineren konfigurierten Wert zurueckgegriffen, damit man
# nicht jede moegliche Platzzahl auflisten muss (z.B. deckt ein
# Eintrag fuer 4 auch 5..7 ab, bis der naechste Eintrag greift).
# Auch ein komplett leerer Tisch (freiePlaetze == Plaetze) wird
# bewertet - er trifft in der Regel den hoechsten konfigurierten
# Eintrag (z.B. 8 frei bei 8er-Tisch) oder dessen Fallback.
if freiePlaetze in self.TischWert:
return self.TischWert[freiePlaetze]
except KeyError:
return 0
kleinere = [ k for k in self.TischWert if k < freiePlaetze ]
if kleinere:
return self.TischWert[ max(kleinere) ]
return 0
elif Typ == 'Trennung':
if (N_Teilungen == 0):
return 0
@@ -746,7 +756,10 @@ class Sitzplatzverteilung:
G = self.Gruppen.vonId(Gid)
self.GruppeEntfernen(G)
SchlechteGruppen.append(G)
self.GruppenSetzen( SchlechteGruppen )
# beim Reseat die groessten zuerst, damit sie die durch das Entfernen
# frei gewordenen (ganzen) Tische bekommen und wieder zusammenkommen,
# statt erneut getrennt zu werden
self.GruppenSetzen( SchlechteGruppen, GrosseZuerst=True )
self.bewerten()
print("mutiert: ", self.value)
@@ -828,20 +841,34 @@ class Sitzplatzverteilung:
self.PersonSetzen( neuePerson, Tisch )
# Gruppenspezifische Funktionen
def GruppenSetzen(self, Gruppen ):
"""setzt mal die grossen mal die kleinen Gruppen auf beliebige Plaetze"""
# Mache Liste der Gruppen, der Groesse nach sortiert
def GruppenSetzen(self, Gruppen, GrosseZuerst=False ):
"""setzt Gruppen auf freie Plaetze.
GrosseZuerst=False (Erstbelegung): mal die grossen, mal die kleinen
zuerst (Zufall) - erzeugt Vielfalt in der Startpopulation.
GrosseZuerst=True (Mutation): konsequent die groesste Gruppe zuerst,
damit sie einen noch komplett freien Tisch bekommt und nicht getrennt
werden muss, weil kleine Gruppen die freien Tische schon anbrechen.
"""
# Mache Liste der Gruppen, der Groesse nach sortiert
Gruppenliste = []
for G in Gruppen:
Gruppenliste.append(G)
# uns setzte sie auf freie Plaetze
while len(Gruppenliste) > 0:
wahl = choice(['grosse', 'kleine'])
if wahl == 'grosse':
G = Gruppenliste.pop(0)
else:
G = Gruppenliste.pop(-1)
if GrosseZuerst:
# groesste zuerst: sie beanspruchen die noch komplett freien
# Tische, bevor kleine Gruppen diese teilbelegen (sonst muessen
# grosse Gruppen mangels ganzem Tisch getrennt werden)
G = max( Gruppenliste, key=lambda g: g.Anzahl() )
Gruppenliste.remove(G)
else:
wahl = choice(['grosse', 'kleine'])
if wahl == 'grosse':
G = Gruppenliste.pop(0)
else:
G = Gruppenliste.pop(-1)
self.GruppeSetzen(G)
def GruppeSetzen(self, Gruppe, Tisch=None ):
"""setzt eine Gruppe irgendwohin wo Platz ist