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:
+39
-12
@@ -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
|
||||
|
||||
+179
-1
@@ -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"""
|
||||
|
||||
@@ -41,6 +41,12 @@ if __name__ == '__main__':
|
||||
help="zusaetzlich eine Entwicklung.svg im --indir Verzeichnis "
|
||||
"erzeugen: Verlauf der Loesungsfindung (Zeitachse nach rechts, "
|
||||
"beste/durchschnittliche/schlechteste Wertigkeit auf der Y-Achse)" )
|
||||
parser.add_option( "--show-lineage", dest="showlineage", action="store_true",
|
||||
default=False,
|
||||
help="zusaetzlich eine Abstammung.svg im --indir Verzeichnis "
|
||||
"erzeugen: Stammbaum aller Loesungen (Mutationsbaum); der Ast der "
|
||||
"finalen besten Loesung ist hervorgehoben und mit ihrer "
|
||||
"Abstammungs-Id beschriftet" )
|
||||
parser.add_option( "--zyklus-art", dest="zyklusart", default="Adaptiv",
|
||||
help="Name des GA-Profils in zyklus.cfg (Vorgabe: Adaptiv). "
|
||||
"Weitere mitgelieferte Profile: Easy, Simple" )
|
||||
@@ -72,6 +78,7 @@ if __name__ == '__main__':
|
||||
AusgabePT = os.path.join( InDir, 'PersonenTische.csv' )
|
||||
AusgabeSVG = os.path.join( InDir, 'Sitzplatzverteilung.svg' )
|
||||
AusgabeEntwicklung = os.path.join( InDir, 'Entwicklung.svg' )
|
||||
AusgabeAbstammung = os.path.join( InDir, 'Abstammung.svg' )
|
||||
|
||||
Z = Zyklus()
|
||||
Z.laden( Zyklusconfig, Zyklusart )
|
||||
@@ -101,6 +108,10 @@ if __name__ == '__main__':
|
||||
if options.showdev:
|
||||
F1.Entwicklung.alsSVG( AusgabeEntwicklung )
|
||||
print( "Entwicklungsverlauf geschrieben:", AusgabeEntwicklung )
|
||||
if options.showlineage:
|
||||
F1.Abstammung.alsSVG( AusgabeAbstammung,
|
||||
HervorId=getattr(S, '_absid', None) )
|
||||
print( "Abstammungsbaum geschrieben:", AusgabeAbstammung )
|
||||
|
||||
else:
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user