diff --git a/libs/Strukturdaten.py b/libs/Strukturdaten.py index ea89fcf..6335d7f 100755 --- a/libs/Strukturdaten.py +++ b/libs/Strukturdaten.py @@ -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:" diff --git a/libs/ga.py b/libs/ga.py index 3391198..c2360df 100755 --- a/libs/ga.py +++ b/libs/ga.py @@ -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) diff --git a/web/main.py b/web/main.py index 866d617..d01516d 100644 --- a/web/main.py +++ b/web/main.py @@ -153,16 +153,6 @@ def _TischeBauen(S): return TE -def _Signatur(SV): - """Sitzordnungs-Signatur einer Loesung: welche Personen an welchem Tisch. - Zwei Loesungen mit gleicher Signatur sind fuer den Nutzer dieselbe - Sitzordnung (der GA-Pool enthaelt viele Mutations-Verwandte) — die - Varianten-Auswahl dedupliziert damit (Roadmap M2: 'unterschiedliche - gute Loesungen').""" - return frozenset( - ( T.Id, frozenset( P.Id for P in T ) ) for T in SV.Tische ) - - def _VarianteJSON(SV, GruppenNamen, Nr): """baut die JSON-Darstellung EINER Loesung (eine Variante)""" def GruppeVonPerson(P): @@ -217,6 +207,8 @@ def _VarianteJSON(SV, GruppenNamen, Nr): return { 'nr': Nr, 'wertigkeit': SV.value, + 'gruppenId': SV.getGruppenId(), + 'personenId': SV.getPersonenId(), 'kennzahlen': Kennzahlen, 'tische': TischListe, 'gruppen': GruppenListe, @@ -471,13 +463,15 @@ def Berechnen(SitzungsId: str, # Roadmap M2: nicht nur den Besten, sondern bis zu 'varianten' # UNTERSCHIEDLICHE gute Loesungen aus dem Endpool — Farm.TopN liefert - # absteigend sortiert, die Signatur-Deduplizierung wirft Loesungen mit - # identischer Sitzordnung raus (Mutations-Verwandte). Liefert der Pool - # weniger unterschiedliche Sitzordnungen, kommen eben weniger zurueck - # (ein weiterer Klick auf Berechnen erzeugt einen frischen Lauf). + # absteigend sortiert. Der Vergleich der Loesungen laeuft ueber die + # getGruppenId (Gruppen-Ebene): dieselbe Sitzordnung zaehlt nicht mehrfach + # als eigene Variante. Der GA-Pool ist ohnehin schon per getGruppenId + # dedupliziert; das hier ist die zusaetzliche Absicherung fuer die Ausgabe. + # Liefert der Pool weniger unterschiedliche Sitzordnungen, kommen eben + # weniger zurueck (ein weiterer Klick auf Berechnen erzeugt einen frischen Lauf). Ergebnisse, Gesehen = [], set() for SV in GAFarm.TopN( len( GAFarm.Pool ) ): - Sig = _Signatur( SV ) + Sig = SV.getGruppenId() if Sig in Gesehen: continue Gesehen.add( Sig )