in der Roademap weitergemacht

This commit is contained in:
Michael Stangl
2026-07-12 17:02:14 +02:00
parent dd84293c36
commit f880ae1c97
8 changed files with 752 additions and 74 deletions
+100 -30
View File
@@ -10,15 +10,17 @@ Endpunkte (siehe auch /docs — interaktive Swagger-UI von FastAPI):
GET /sitzung/{id} — Status der Session
POST /sitzung/{id}/tische — Tisch-Layout als JSON (Ersatz tische.ini)
POST /sitzung/{id}/personen — Gaesteliste als CSV-Upload (Ersatz bestellung.json)
POST /sitzung/{id}/berechnen — startet die GA-Optimierung
GET /sitzung/{id}/ergebnis — Ergebnis als JSON (Tische -> Personen)
GET /sitzung/{id}/ergebnis.svg — Ergebnis als SVG (alsSVG aus dem Kern)
POST /sitzung/{id}/berechnen — startet die GA-Optimierung; liefert bis zu
?varianten=N unterschiedliche Loesungen (M2)
GET /sitzung/{id}/ergebnis — alle Varianten des letzten Laufs als JSON
GET /sitzung/{id}/ergebnis.svg — eine Variante als SVG (?variante=k, 1 = beste)
Start (lokale Testinstanz, siehe bin/run_web.bat / bin/run_web.sh):
python -m uvicorn main:app --reload --app-dir web
"""
import ast
import configparser
import math
import os
import sys
@@ -35,7 +37,7 @@ CFGDIR = os.environ.get( 'PLATZ_CFG', os.path.join( BASISDIR, 'cfg' ) )
if LIBSDIR not in sys.path:
sys.path.insert( 0, LIBSDIR )
from fastapi import FastAPI, File, HTTPException, UploadFile
from fastapi import FastAPI, File, HTTPException, Query, UploadFile
from fastapi.responses import Response
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel, Field
@@ -149,11 +151,18 @@ def _TischeBauen(S):
return TE
def _ErgebnisJSON(S):
"""baut die JSON-Darstellung des besten Ergebnisses einer Session"""
SV = S['ergebnis']
GruppenNamen = S.get( 'gruppenNamen', {} )
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):
try:
G = SV.Gruppen.GruppeVonPid( P.Id )
@@ -193,14 +202,35 @@ def _ErgebnisJSON(S):
} )
GruppenListe.sort( key=lambda g: g['id'] )
return {
# Vergleichs-Kennzahlen (Roadmap M2: Wertigkeit, Gruppentrennungen,
# allein sitzende Personen — LeuteAllein zaehlt nur Personen aus
# mehrkoepfigen Gruppen ohne Gruppenmitglied am Tisch)
Kennzahlen = {
'wertigkeit': SV.value,
'variante': S['varianten'],
'getrennteGruppen': sum( 1 for G in GruppenListe if G['teilungen'] > 0 ),
'teilungenGesamt': sum( G['teilungen'] for G in GruppenListe ),
'alleinePersonen': sum( len( SV.LeuteAllein(T) ) for T in SV.Tische ),
}
return {
'nr': Nr,
'wertigkeit': SV.value,
'kennzahlen': Kennzahlen,
'tische': TischListe,
'gruppen': GruppenListe,
}
def _ErgebnisJSON(S):
"""baut die JSON-Darstellung aller Varianten des letzten Laufs"""
GruppenNamen = S.get( 'gruppenNamen', {} )
return {
'lauf': S['laeufe'],
'varianten': [ _VarianteJSON( SV, GruppenNamen, Nr + 1 )
for Nr, SV in enumerate( S['ergebnisse'] ) ],
}
# --------------------------------------------------------------- Endpunkte
@app.post( '/sitzung', status_code=201 )
def SitzungAnlegen():
@@ -212,8 +242,8 @@ def SitzungAnlegen():
'erzeugt': time.time(),
'tische': None,
'personen': None,
'ergebnis': None,
'varianten': 0,
'ergebnisse': [], # Varianten des letzten Laufs (beste zuerst)
'laeufe': 0, # wie oft .../berechnen aufgerufen wurde
}
return { 'id': SitzungsId, 'ttlStunden': SESSION_TTL_SEKUNDEN // 3600 }
@@ -227,8 +257,9 @@ def SitzungStatus(SitzungsId: str):
'tische': len( S['tische'] ) if S['tische'] else 0,
'personen': S['personen']['nLeute'] if S['personen'] else 0,
'vips': S['personen']['nVips'] if S['personen'] else 0,
'berechnet': S['ergebnis'] is not None,
'varianten': S['varianten'],
'berechnet': len( S['ergebnisse'] ) > 0,
'laeufe': S['laeufe'],
'varianten': len( S['ergebnisse'] ),
}
@@ -262,8 +293,10 @@ def TischeSetzen(SitzungsId: str, Layout: TischLayout):
'y': T.y,
'nachbarn': Nachbarn[T.id],
} for T in Layout.tische ]
S['ergebnis'] = None
S['varianten'] = 0
# Nachbar-Radius fuer den Rueckweg merken (GET .../tische), damit der
# grafische Editor dasselbe Layout inkl. Radius wieder laden kann.
S['nachbarRadius'] = Layout.nachbarRadius
S['ergebnisse'] = []
return {
'tische': S['tische'],
@@ -271,6 +304,19 @@ def TischeSetzen(SitzungsId: str, Layout: TischLayout):
}
@app.get( '/sitzung/{SitzungsId}/tische' )
def TischeHolen(SitzungsId: str):
"""das gespeicherte Tisch-Layout zurueckgeben (fuer den grafischen
Editor: Layout aus einer geteilten Session wieder aufnehmen).
Die abgeleitete Nachbarliste ist pro Tisch enthalten; der zuletzt
verwendete nachbarRadius kommt separat mit (Vorgabe 1.5)."""
S = _GibSession( SitzungsId )
return {
'tische': S['tische'] or [],
'nachbarRadius': S.get( 'nachbarRadius', 1.5 ),
}
@app.post( '/sitzung/{SitzungsId}/tische-datei' )
def TischDateiHochladen(SitzungsId: str, datei: UploadFile = File(...)):
"""bestehende tische.ini hochladen (gleiches Format wie fuer die CLI,
@@ -330,8 +376,7 @@ def TischDateiHochladen(SitzungsId: str, datei: UploadFile = File(...)):
detail='tische.ini enthaelt doppelte Tisch-Ids' )
S['tische'] = TischListe
S['ergebnis'] = None
S['varianten'] = 0
S['ergebnisse'] = []
return {
'tische': S['tische'],
'plaetzeGesamt': sum( T['plaetze'] for T in S['tische'] ),
@@ -364,8 +409,7 @@ def PersonenHochladen(SitzungsId: str, datei: UploadFile = File(...)):
'nVips': VIPGruppe.Anzahl(),
}
S['gruppenNamen'] = GruppenNamen
S['ergebnis'] = None
S['varianten'] = 0
S['ergebnisse'] = []
return {
'personen': GN.NLeute,
@@ -379,8 +423,12 @@ def PersonenHochladen(SitzungsId: str, datei: UploadFile = File(...)):
@app.post( '/sitzung/{SitzungsId}/berechnen' )
def Berechnen(SitzungsId: str):
"""startet die GA-Optimierung; jeder Aufruf erzeugt eine neue Variante"""
def Berechnen(SitzungsId: str,
varianten: int = Query( default=3, ge=1, le=5,
description='wieviele unterschiedliche Varianten der Lauf '
'zurueckgeben soll (aus Farm.TopN, dedupliziert)' )):
"""startet die GA-Optimierung; jeder Aufruf ist ein neuer Lauf und
liefert bis zu 'varianten' unterschiedliche gute Sitzordnungen"""
S = _GibSession( SitzungsId )
if S['tische'] is None:
raise HTTPException( status_code=400,
@@ -419,34 +467,56 @@ def Berechnen(SitzungsId: str):
except IndexError as Fehler:
raise HTTPException( status_code=400, detail=str(Fehler) )
S['ergebnis'] = GAFarm.Bester()
S['varianten'] = S['varianten'] + 1
# 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).
Ergebnisse, Gesehen = [], set()
for SV in GAFarm.TopN( len( GAFarm.Pool ) ):
Sig = _Signatur( SV )
if Sig in Gesehen:
continue
Gesehen.add( Sig )
Ergebnisse.append( SV )
if len( Ergebnisse ) == varianten:
break
S['ergebnisse'] = Ergebnisse
S['laeufe'] = S['laeufe'] + 1
return _ErgebnisJSON( S )
@app.get( '/sitzung/{SitzungsId}/ergebnis' )
def Ergebnis(SitzungsId: str):
"""das beste bisher berechnete Ergebnis als JSON"""
"""alle Varianten des letzten Laufs als JSON (beste zuerst)"""
S = _GibSession( SitzungsId )
if S['ergebnis'] is None:
if not S['ergebnisse']:
raise HTTPException( status_code=404,
detail='Noch nichts berechnet (POST .../berechnen)' )
return _ErgebnisJSON( S )
@app.get( '/sitzung/{SitzungsId}/ergebnis.svg' )
def ErgebnisSVG(SitzungsId: str):
"""das beste bisher berechnete Ergebnis als SVG (alsSVG aus dem Kern)"""
def ErgebnisSVG(SitzungsId: str,
variante: int = Query( default=1, ge=1,
description='Nummer der Variante (1 = beste)' )):
"""eine berechnete Variante als SVG (alsSVG aus dem Kern)"""
S = _GibSession( SitzungsId )
if S['ergebnis'] is None:
if not S['ergebnisse']:
raise HTTPException( status_code=404,
detail='Noch nichts berechnet (POST .../berechnen)' )
if variante > len( S['ergebnisse'] ):
raise HTTPException( status_code=404,
detail='Variante %d gibt es nicht (letzter Lauf hat %d)'
% (variante, len( S['ergebnisse'] )) )
# alsSVG schreibt in eine Datei -> Tempdatei schreiben, lesen, loeschen
Handle, TempPfad = tempfile.mkstemp( suffix='.svg' )
os.close( Handle )
try:
S['ergebnis'].alsSVG( TempPfad )
S['ergebnisse'][variante - 1].alsSVG( TempPfad )
fsock = open( TempPfad, 'r' )
SVG = fsock.read()
fsock.close()