Adaptiver GA-Zyklus, Pool-Groessen-Panel, Konfig direkt aus cfg-Dateien

Zyklus/Farm:
- neuer adaptiver Modus: grosse Startpopulation, die je Generation
  schrumpft (Start/Schrumpfung/MinPopulation) und bei Konvergenz stoppt
  (Geduld Generationen ohne Verbesserung >= Schwelle, max MaxGenerationen)
- Zyklus.laden erkennt adaptives Profil am Feld 'Start' (Modus adaptiv),
  sonst klassisch (Abfolge/Anzahl); Farm._LaufStatisch/_LaufAdaptiv
- neue Trunkierungs-Selektion Farm.PopulationBegrenzen
- [Adaptiv]-Profil in zyklus.cfg (dokumentiert)

Entwicklungsgrafik:
- zweites Panel unter dem Verlauf zeigt die Anzahl erzeugter
  Verteilungen im Pool je Schritt (gemeinsame Zeitachse)

Konfiguration:
- platz.cfg entfernt; zyklus.cfg/strafen.cfg werden direkt aus
  $PLATZ_CFG geladen
- neuer Schalter --zyklus-art (Default Adaptiv) waehlt das GA-Profil
- web/main.py analog: direktes Laden, Profil via PLATZ_ZYKLUSART

Doku (README.md, CLAUDE.md) und Tests entsprechend aktualisiert;
Beispiele mit dem Adaptiv-Default neu berechnet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Michael Stangl
2026-07-11 14:31:59 +02:00
parent 4f87895303
commit 644f61ef6b
23 changed files with 1909 additions and 1607 deletions
+55 -2
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, _StringAusdruck
from ga import Entwicklung, Farm, Zyklus, _StringAusdruck
IMAGES_DIR = os.path.join(os.path.dirname(__file__), '..', 'doc', 'images')
@@ -267,12 +267,65 @@ class TestEntwicklung(unittest.TestCase):
with open(Pfad) as f:
svg = f.read()
self.assertIn( '<svg', svg )
self.assertEqual( svg.count('<polyline'), 3 ) # best/schnitt/schlecht
# 3 Wertigkeitskurven (best/schnitt/schlecht) + 1 Pool-Groessenkurve
self.assertEqual( svg.count('<polyline'), 4 )
# das zweite Panel zeigt die Anzahl erzeugter Verteilungen im Pool
self.assertIn( 'Anzahl erzeugter Verteilungen im Pool', svg )
self.assertIn( 'stroke="#4060a0"', svg ) # Pool-Kurve
def test_leere_entwicklung_wirft_beim_svg(self):
with self.assertRaises(ValueError):
Entwicklung().alsSVG( os.path.join(IMAGES_DIR, 'leer.svg') )
class TestAdaptiverZyklus(unittest.TestCase):
"""Der adaptive Zyklus startet mit vielen Loesungen, laesst die
Population schrumpfen und bricht bei Konvergenz ab."""
class _L:
"""minimale GA-Loesung: fester Wert, Mutation aendert nichts (so
konvergiert der beste Wert sofort -> Abbruch nach 'Geduld')."""
def __init__(self, *a, **k):
self.value = -100
def mutieren(self):
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_population_schrumpft_und_bricht_bei_konvergenz_ab(self):
Z = self._zyklus( Start=40, Schrumpfung=0.85, MinPopulation=5,
MaxGenerationen=50, Geduld=3, Schwelle=1.0 )
F = Farm( Z, self._L )
# Pool-Groessen je 's'-Aufzeichnung (Ende jeder Generation)
pop_pro_gen = [ p[5] for p in F.Entwicklung.Punkte if p[1] == 's' ]
# monoton fallend (Population schrumpft) ...
self.assertTrue( all( a >= b for a, b in zip(pop_pro_gen, pop_pro_gen[1:]) ),
"Population muss von Generation zu Generation schrumpfen: %s"
% pop_pro_gen )
self.assertLess( pop_pro_gen[-1], pop_pro_gen[0] )
# ... und da nichts besser wird, nach Geduld(=3) Generationen Abbruch,
# also klar vor MaxGenerationen(=50).
self.assertLessEqual( len(pop_pro_gen), 4 )
def test_population_faellt_nicht_unter_min(self):
Z = self._zyklus( Start=8, Schrumpfung=0.5, MinPopulation=5,
MaxGenerationen=10, Geduld=99, Schwelle=1.0 )
F = Farm( Z, self._L )
pop_pro_gen = [ p[5] for p in F.Entwicklung.Punkte if p[1] == 's' ]
self.assertTrue( all( n >= 5 for n in pop_pro_gen ),
"MinPopulation=5 darf nicht unterschritten werden: %s"
% pop_pro_gen )
if __name__ == '__main__':
unittest.main()