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
+80 -1
View File
@@ -19,7 +19,7 @@ import unittest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'libs'))
from Strukturdaten import Gruppe, Gruppen, JSONConfig, Person, Sitzplatzverteilung, Strafliste, Tisch, Tische
from ga import Entwicklung, Farm, Zyklus, _StringAusdruck
from ga import Abstammung, Entwicklung, Farm, Zyklus, _StringAusdruck
IMAGES_DIR = os.path.join(os.path.dirname(__file__), '..', 'doc', 'images')
@@ -327,5 +327,84 @@ class TestAdaptiverZyklus(unittest.TestCase):
% pop_pro_gen )
class TestAbstammung(unittest.TestCase):
"""Der Stammbaum vergibt hierarchische Ids, aus denen sich die Abstammung
ablesen laesst, und zeichnet einen Baum."""
class _L:
"""GA-Loesung, deren Mutation den Wert leicht verbessert (damit ueber
Generationen ein mehrstufiger Ast entsteht)."""
def __init__(self, *a, **k):
self.value = -100
def mutieren(self):
self.value = self.value + 1
return self
def __eq__(self, other):
return self.value == other.value
def __lt__(self, other):
return self.value < other.value
def _zyklus(self, **params):
Z = Zyklus()
Z.Modus = 'adaptiv'
Z.Adaptiv = dict(Zyklus.AdaptivDefaults)
Z.Adaptiv.update(params)
return Z
def test_hierarchische_ids_kodieren_abstammung(self):
Z = self._zyklus( Start=5, Schrumpfung=1.0, MinPopulation=5,
MaxGenerationen=3, Geduld=99, Schwelle=0.0 )
F = Farm( Z, self._L )
Knoten = F.Abstammung.Knoten
# Wurzeln sind schlichte Zahlen ohne Punkt und haben keine Eltern
wurzeln = [ Id for Id, K in Knoten.items() if K['eltern'] is None ]
self.assertTrue( all( '.' not in Id for Id in wurzeln ) )
self.assertEqual( len(wurzeln), 5 )
# jede Kind-Id ist Eltern-Id + '.' + index; die Eltern-Id ist damit
# das Praefix bis zum letzten Punkt und existiert als Knoten
for Id, K in Knoten.items():
if K['eltern'] is None:
continue
self.assertIn( '.', Id )
self.assertEqual( Id.rsplit('.', 1)[0], K['eltern'] )
self.assertIn( K['eltern'], Knoten )
def test_ahnenkette_ist_aus_id_ablesbar(self):
# Bei einem Kind '3.2.1' muessen '3.2' und '3' die Ahnen sein
A = Abstammung()
A.erfassen( '3', None, 0, -100, 'e' )
A.erfassen( '3.2', '3', 1, -99, 'm' )
A.erfassen( '3.2.1', '3.2', 2, -98, 'm' )
# rueckwaerts ueber die Eltern-Verweise
kette = []
Id = '3.2.1'
while Id is not None:
kette.append(Id)
Id = A.Knoten[Id]['eltern']
self.assertEqual( kette, ['3.2.1', '3.2', '3'] )
# und das laesst sich auch rein syntaktisch aus der Id ablesen
self.assertEqual( '3.2.1'.split('.'), ['3', '2', '1'] )
def test_svg_hebt_beste_hervor(self):
Z = self._zyklus( Start=4, Schrumpfung=1.0, MinPopulation=4,
MaxGenerationen=2, Geduld=99, Schwelle=0.0 )
F = Farm( Z, self._L )
Bester = F.Bester()
Pfad = os.path.join( IMAGES_DIR, 'test_abstammung.svg' )
F.Abstammung.alsSVG( Pfad, HervorId=Bester._absid )
with open(Pfad) as f:
svg = f.read()
self.assertIn( '<svg', svg )
self.assertIn( 'Abstammung der Loesungen', svg )
# die Id der besten Loesung ist als Beschriftung enthalten
self.assertIn( '>%s</text>' % Bester._absid, svg )
def test_leere_abstammung_wirft_beim_svg(self):
with self.assertRaises(ValueError):
Abstammung().alsSVG( os.path.join(IMAGES_DIR, 'leer_abst.svg') )
if __name__ == '__main__':
unittest.main()