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
+179 -1
View File
@@ -326,6 +326,149 @@ class Entwicklung:
fout.close()
class Abstammung:
"""Beobachtungsobjekt: zeichnet den Stammbaum aller Loesungen auf.
Jede Loesung bekommt von der Farm eine fortlaufende Abstammungs-Id.
Beim Erzeugen entsteht ein Wurzelknoten (keine Eltern), beim Mutieren
ein Kindknoten mit Verweis auf die Eltern-Id. alsSVG() zeichnet daraus
einen Baum: Generationen als Zeilen (oben Start, nach unten spaeter),
Knoten als Punkte (Farbe = Wertigkeit), Mutationskanten Eltern->Kind.
Domaenen-agnostisch; funktioniert mit jeder Loesung, die .value hat.
"""
def __init__(self):
# je Knoten: Id -> dict(eltern, generation, value, aktion)
self.Knoten = {}
def erfassen(self, Id, Eltern, Generation, value, Aktion):
self.Knoten[Id] = { 'eltern': Eltern, 'generation': Generation,
'value': value, 'aktion': Aktion }
def __repr__(self):
return "-- Abstammung (%d Loesungen)\n" % len(self.Knoten)
def alsSVG(self, OutputSVG, HervorId=None, Breite=1200, Rand=60,
KnotenAbstand=26, ZeilenHoehe=90):
"""zeichnet den aufgezeichneten Stammbaum als SVG.
HervorId (optional): Id der hervorzuhebenden Loesung (z.B. der
finalen besten). Ihr Ast wird kraeftig gezeichnet und mit der
sprechenden Id (z.B. 7.2.1) beschriftet, so dass man ihre ganze
Abstammung ablesen kann."""
if not self.Knoten:
raise ValueError("keine Abstammung aufgezeichnet")
# Ahnenkette der hervorgehobenen Loesung bestimmen (fuer die
# kraeftige Darstellung ihres Astes)
Ahnen = set()
Id = HervorId
while Id is not None and Id in self.Knoten:
Ahnen.add(Id)
Id = self.Knoten[Id]['eltern']
# Knoten nach Generation gruppieren
Generationen = {}
for Id, K in self.Knoten.items():
Generationen.setdefault( K['generation'], [] ).append(Id)
GenListe = sorted(Generationen)
# je Generation die Knoten stabil sortieren (nach Id), damit das Bild
# reproduzierbar ist
for g in Generationen:
Generationen[g].sort()
MaxBreiteKnoten = max( len(v) for v in Generationen.values() )
PlotBreite = max( Breite - 2 * Rand, MaxBreiteKnoten * KnotenAbstand )
Breite = PlotBreite + 2 * Rand
Hoehe = Rand * 2 + (len(GenListe) - 1) * ZeilenHoehe + 40
# Position (x,y) je Knoten-Id bestimmen
Pos = {}
for gi, g in enumerate(GenListe):
Ids = Generationen[g]
n = len(Ids)
y = Rand + gi * ZeilenHoehe
for k, Id in enumerate(Ids):
# horizontal mittig ausrichten
if n == 1:
x = Rand + PlotBreite / 2
else:
x = Rand + PlotBreite * k / (n - 1)
Pos[Id] = (x, y)
# Wertebereich fuer die Knotenfarbe (gut = gruen, schlecht = rot)
Werte = [ K['value'] for K in self.Knoten.values() ]
WMin, WMax = min(Werte), max(Werte)
if WMax == WMin:
WMax = WMin + 1
def Farbe(v):
t = (v - WMin) / (WMax - WMin) # 0 schlecht .. 1 gut
r = int(200 * (1 - t) + 40 * t)
gg = int(50 * (1 - t) + 140 * t)
b = int(50 * (1 - t) + 80 * t)
return '#%02x%02x%02x' % (r, gg, b)
Teile = []
Teile.append( '<?xml version="1.0" encoding="UTF-8"?>\n' )
Teile.append(
'<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" '
'viewBox="0 0 %d %d" font-family="sans-serif">\n'
% (Breite, Hoehe, Breite, Hoehe) )
Teile.append( '<rect width="100%" height="100%" fill="white"/>\n' )
Teile.append(
'<text x="%d" y="30" text-anchor="middle" font-size="16" '
'fill="#333">Abstammung der Loesungen (Mutationsbaum)</text>\n'
% (Breite // 2) )
# zuerst die Kanten Eltern -> Kind (darunter liegend). Kanten entlang
# des hervorgehobenen Astes kraeftig, der Rest blass.
for Id, K in self.Knoten.items():
Eltern = K['eltern']
if Eltern is None or Eltern not in Pos or Id not in Pos:
continue
x1, y1 = Pos[Eltern]
x2, y2 = Pos[Id]
ImAst = Id in Ahnen and Eltern in Ahnen
Stil = ('stroke="#c04040" stroke-width="2.5"' if ImAst
else 'stroke="#bbb" stroke-width="1"')
Teile.append(
'<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" %s/>\n'
% (x1, y1, x2, y2, Stil) )
# dann die Knoten
for Id, K in self.Knoten.items():
if Id not in Pos:
continue
x, y = Pos[Id]
r = 7 if Id in Ahnen else 5
Rahmen = ('stroke="#c04040" stroke-width="2"' if Id in Ahnen
else 'stroke="#333" stroke-width="0.5"')
Teile.append(
'<circle cx="%.1f" cy="%.1f" r="%d" fill="%s" %s>'
'<title>Id %s, Wert %.1f</title></circle>\n'
% (x, y, r, Farbe(K['value']), Rahmen, Id, K['value']) )
# die hervorgehobene Loesung mit ihrer sprechenden Abstammungs-Id
# beschriften (aus ihr laesst sich die ganze Herkunft ablesen)
if HervorId in Pos:
x, y = Pos[HervorId]
Teile.append(
'<text x="%.1f" y="%.1f" text-anchor="middle" font-size="12" '
'font-weight="bold" fill="#c04040">%s</text>\n'
% (x, y + 22, HervorId) )
# Generationsbeschriftung links
for gi, g in enumerate(GenListe):
y = Rand + gi * ZeilenHoehe
Teile.append(
'<text x="%d" y="%.1f" text-anchor="end" font-size="11" '
'fill="#666">Gen %s</text>\n' % (Rand - 12, y + 4, g) )
Teile.append( '</svg>\n' )
fout = open( OutputSVG, 'w' )
fout.writelines( Teile )
fout.close()
class Farm:
"""enthaelt und pflegt Loesungsobjekte, kreuzt, mutiert, selektiert sie"""
def __init__(self, Zyklus, Klasse, *Arguments, **keywords):
@@ -342,6 +485,12 @@ class Farm:
self.Zyklus = Zyklus
self.Klasse = Klasse
self.Entwicklung = Entwicklung()
# Abstammung: jede Loesung bekommt eine fortlaufende _absid; die
# aktuelle Generationsnummer waechst mit jedem Erzeugen/Mutieren.
self.Abstammung = Abstammung()
self._AbsZaehler = 0
self._Generation = 0
self._KindZaehler = {} # Eltern-Id -> Anzahl bisher vergebener Kinder
if getattr(Zyklus, 'Modus', 'statisch') == 'adaptiv':
self._LaufAdaptiv(Klasse, *Arguments, **keywords)
else:
@@ -421,10 +570,30 @@ class Farm:
print("+ Abbruch: %d Generationen ohne nennenswerte "
"Verbesserung" % OhneVerbesserung)
break
def _WurzelId(self):
"""neue Id fuer eine erzeugte (elternlose) Loesung: schlicht eine
fortlaufende Zahl als String ('1', '2', ...)."""
self._AbsZaehler = self._AbsZaehler + 1
return str(self._AbsZaehler)
def _KindId(self, ElternId):
"""neue Id fuer ein Mutationskind: Eltern-Id + '.' + laufender Index,
z.B. '7' -> '7.1', '7.2'; '7.2' -> '7.2.1'. So laesst sich die
ganze Abstammung direkt aus der Id ablesen. Fehlt eine Eltern-Id
(z.B. bei einer Loesung ohne Aufzeichnung), wird eine Wurzel-Id
vergeben."""
if not ElternId:
return self._WurzelId()
self._KindZaehler[ElternId] = self._KindZaehler.get(ElternId, 0) + 1
return "%s.%d" % (ElternId, self._KindZaehler[ElternId])
def LoesungenErzeugen(self, N, Klasse, *Arguments, **keywords):
""" Erzeuge Loesungen"""
for i in range(N):
L = Klasse( *Arguments, **keywords )
L._absid = self._WurzelId()
self.Abstammung.erfassen(
L._absid, None, self._Generation, getattr(L, 'value', 0), 'e' )
self.Pool.append(L)
#print(repr(len(self.Pool)) + ' Loesungsobjekte erzeugt')
def LoesungenKreuzen(self,N):
@@ -435,11 +604,20 @@ class Farm:
def LoesungenMutieren(self, N):
"""veraendere die Loesung, so dass sie hoffentlich besser wird"""
print("+ Loesungen mutieren: ", N)
self._Generation = self._Generation + 1
for n in range(N):
#print(" -"+repr(n+1)+" mal")
for L in chain(self.Pool):
O = deepcopy(L)
self.PoolNeu.append(O.mutieren())
Kind = O.mutieren()
# Kind-Id = Eltern-Id + '.' + laufender Kindindex, damit man
# an der Id die ganze Abstammung ablesen kann (z.B. 7.2.1).
ElternId = getattr(L, '_absid', None)
Kind._absid = self._KindId(ElternId)
self.Abstammung.erfassen(
Kind._absid, ElternId, self._Generation,
getattr(Kind, 'value', 0), 'm' )
self.PoolNeu.append(Kind)
#print(self)
def LoesungenBehalten(self):
"""Behalte alle Eltern aus dem alten Pool"""