Loesungs-Dedup: getGruppenId/getPersonenId, keine doppelten Sitzordnungen

- getGruppenId (Gruppen-Ebene, personen-unabhaengig: welche Gruppen mit
  welcher Anzahl je Tisch) und getPersonenId (Personen-Ebene: alphabetische
  Namen je Tisch) an Sitzplatzverteilung; Basis-Methoden an Loesung. Gleiche
  Gruppen-Verteilung -> gleiche getGruppenId und damit gleiche Wertigkeit,
  egal welche Personen die geteilten Plaetze einnehmen.
- Farm verwirft nach jeder Erzeugung/Mutation Loesungen mit schon gesehener
  getGruppenId, so dass dieselbe Sitzordnung nicht mehrfach gehalten und
  bewertet wird (defensiv: Klassen ohne getGruppenId bleiben unberuehrt,
  Tests unveraendert).
- Web: Varianten-Vergleich ueber getGruppenId statt der person-basierten
  _Signatur; gruppenId und personenId zusaetzlich im Ergebnis-JSON.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Michael Stangl
2026-07-15 18:10:23 +02:00
parent b29b3b22d4
commit d4844ded67
3 changed files with 80 additions and 16 deletions
+39
View File
@@ -14,6 +14,7 @@ from random import *
import ast
import configparser, os
import copy
import hashlib
import html
import math
from collections import defaultdict
@@ -1114,6 +1115,44 @@ class Sitzplatzverteilung:
return len( self.Gid_Tid[Gruppe.Id] ) - 1
except KeyError:
return 0
def getGruppenId(self):
"""eindeutige Id der Loesung auf GRUPPEN-Ebene (personen-unabhaengig).
Signatur: je Tisch, welche Gruppen mit wie vielen Mitgliedern dort
sitzen. Zwei Loesungen mit identischer Gruppen-Verteilung bekommen
dieselbe Id - egal welche konkreten Personen die geteilten Plaetze
einnehmen (die Teilung einer 5er-Gruppe auf die Tische 3 und 4 ist
dieselbe Sitzordnung, gleich welche Personen wohin kommen). Da die
Bewertung (Strafliste) nur von den Gruppen-Anzahlen je Tisch, den
Nachbarschaften und der Auslastung abhaengt, haben Loesungen mit
gleicher getGruppenId auch dieselbe Wertigkeit. Diese Id dient dem
Vergleich der Loesungen (Dedup), damit dieselbe Sitzordnung nicht
mehrfach als eigene Loesung gehalten und bewertet wird."""
Teile = []
for T in sorted( self.Tische, key=lambda t: t.Id ):
Zaehler = {}
for P in T:
try:
Gid = self.Gruppen.GruppeVonPid( P.Id ).Id
except KeyError:
Gid = -1 # VIP / feste Platzierung (ohne GA-Gruppe)
Zaehler[Gid] = Zaehler.get( Gid, 0 ) + 1
Teile.append( (T.Id, tuple(sorted( Zaehler.items() ))) )
return hashlib.sha1( repr(Teile).encode('utf-8') ).hexdigest()[:16]
def getPersonenId(self):
"""eindeutige Id der Loesung auf PERSONEN-Ebene.
Signatur: je Tisch die alphabetisch sortierten Personennamen.
Identifiziert WIRKLICH identische Sitzbelegungen (dieselben Personen
an denselben Tischen) - feiner als getGruppenId, das nur die Gruppen-
Verteilung vergleicht."""
Teile = []
for T in sorted( self.Tische, key=lambda t: t.Id ):
Namen = sorted(
' '.join( s for s in (P._Titel, P._Vorname, P._Name) if s )
for P in T )
Teile.append( (T.Id, tuple(Namen)) )
return hashlib.sha1( repr(Teile).encode('utf-8') ).hexdigest()[:16]
def VIPsSetzen(self, VIPListe, VIPs, Tische ):
"""setze die VIPs auf Ihre Plaetze"""
#print " VIPsSetzen:"
+32 -1
View File
@@ -61,6 +61,15 @@ class Loesung:
print(" lade Loesung " + repr(self.Id))
def speichern(self):
print(" speichere Loesung " + repr(self.Id))
def getGruppenId(self):
"""Id fuer den Dedup-Vergleich (Basis: jede Loesung eigenstaendig).
Unterklassen ueberladen dies mit einer inhaltlichen Signatur, so
dass gleiche Loesungen dieselbe Id bekommen und nicht mehrfach
gehalten werden."""
return id(self)
def getPersonenId(self):
"""feinere Id fuer wirklich identische Loesungen (Basis: eigenstaendig)"""
return id(self)
def __repr__(self):
return ' L '+ repr(self.Id)
def __eq__(self,other):
@@ -493,6 +502,9 @@ class Farm:
self.Abstammung = Abstammung()
self._AbsZaehler = 0 # vergibt fortlaufende Loesungs-Ids 1..N
self._Generation = 0
# schon gesehene Loesungs-Signaturen (getGruppenId), damit dieselbe
# Sitzordnung nicht mehrfach gehalten und bewertet wird.
self._GeseheneGruppenIds = set()
if getattr(Zyklus, 'Modus', 'statisch') == 'adaptiv':
self._LaufAdaptiv(Klasse, *Arguments, **keywords)
else:
@@ -580,10 +592,27 @@ class Farm:
self._AbsZaehler = self._AbsZaehler + 1
return self._AbsZaehler
def _LoesungIstNeu(self, L):
"""Kontrolle nach jeder Erzeugung: True, wenn die Loesung eine bisher
ungesehene Sitzordnung hat (Vergleich ueber getGruppenId, also auf
Gruppen-Ebene) - und merkt sie dann vor. Duplikate (gleiche Sitz-
ordnung) werden so nicht noch einmal gehalten und bewertet.
Loesungen ohne getGruppenId gelten immer als neu (kein Dedup)."""
Hole = getattr(L, 'getGruppenId', None)
if Hole is None:
return True
Gid = Hole()
if Gid in self._GeseheneGruppenIds:
return False
self._GeseheneGruppenIds.add(Gid)
return True
def LoesungenErzeugen(self, N, Klasse, *Arguments, **keywords):
""" Erzeuge Loesungen"""
""" Erzeuge Loesungen (bereits gesehene Sitzordnungen werden verworfen)"""
for i in range(N):
L = Klasse( *Arguments, **keywords )
if not self._LoesungIstNeu(L):
continue # gleiche Sitzordnung schon vorhanden -> nicht behalten
L._absid = self._NeueId() # Wurzel: kein Vorgaenger
self.Abstammung.erfassen(
L._absid, None, self._Generation, getattr(L, 'value', 0), 'e' )
@@ -603,6 +632,8 @@ class Farm:
for L in chain(self.Pool):
O = deepcopy(L)
Kind = O.mutieren()
if not self._LoesungIstNeu(Kind):
continue # gleiche Sitzordnung schon gesehen -> verwerfen
# Kind bekommt eine neue fortlaufende Nummer; der Vorgaenger
# (Eltern-Id) wird separat in der Abstammung gemerkt.
ElternId = getattr(L, '_absid', None)