Web-M1-Ausbau: JSON-Gaesteliste, GA-Verlauf, Python-Serversteuerung, tische.ini-Upload

- bestellung.xml -> bestellung.json: neuer JSONConfig (loest XMLConfig ab),
  alle work-Beispiele konvertiert, Doku (CLAUDE/README/dateiformate) umgestellt
- ga.py: Beobachtungsobjekt Entwicklung zeichnet den Loesungsverlauf auf,
  neuer Schalter --show-development erzeugt Entwicklung.svg (beste/mittlere/
  schlechteste Wertigkeit ueber die Zyklus-Aktionen)
- web/main.py: neuer Endpunkt /tische-datei (tische.ini-Upload, ast.literal_eval),
  Frontend kann jetzt eine tische.ini laden
- libs/webserver.py: plattformunabhaengige start/stop/status-Steuerung der
  Web-Testinstanz (ersetzt run_web_helper.ps1), raeumt auch verwaiste
  --reload-Prozessbaeume zuverlaessig auf
- Sicherheit/Modernisierung: eval() -> ast.literal_eval in Tische/Strafliste,
  sicherer Mini-Auswerter fuer die String-Ausdruecke in zyklus.cfg,
  defaultdict(set) in Plaetze
- SVG: Tischnummer fett zentriert im Tischkreis statt "Tisch x" darueber
- Tests: JSONConfig, Entwicklung und der Zyklus-Auswerter abgedeckt (9 Tests)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Michael Stangl
2026-07-11 12:49:42 +02:00
parent 00dffc59eb
commit 9de80d2f2a
41 changed files with 5054 additions and 4033 deletions
+186 -3
View File
@@ -6,9 +6,37 @@ from random import *
from itertools import chain
from copy import deepcopy
from functools import total_ordering
import ast
import configparser, os
def _StringAusdruck(text):
"""wertet einen einfachen String-Ausdruck sicher aus.
zyklus.cfg nutzt kompakte Schreibweisen wie
'e,'+'s,z,m,j,z,'*3+'s'
also String-Literale, mit '+' verkettet und mit '*' vervielfacht.
ast.literal_eval kann das nicht (nur reine Literale), eval() waere
unsicher. Dieser Mini-Auswerter erlaubt ausschliesslich str-Literale,
ganze Zahlen sowie die Operatoren + und * - kein Funktionsaufruf,
keine Namen, kein beliebiger Code.
"""
knoten = ast.parse(text, mode='eval').body
def rechne(n):
if isinstance(n, ast.Constant) and isinstance(n.value, (str, int)):
return n.value
if isinstance(n, ast.BinOp) and isinstance(n.op, (ast.Add, ast.Mult)):
links, rechts = rechne(n.left), rechne(n.right)
return links + rechts if isinstance(n.op, ast.Add) else links * rechts
raise ValueError("unerlaubter Ausdruck in zyklus.cfg: %r" % text)
ergebnis = rechne(knoten)
if not isinstance(ergebnis, str):
raise ValueError("zyklus.cfg-Ausdruck ergibt keinen String: %r" % text)
return ergebnis
__author__ = "Michael Stangl"
__version__ = "$Revision: $"
__date__ = "$Date: $"
@@ -65,24 +93,174 @@ class Zyklus:
ZyklusPath= os.path.join( FileName )
config.read( ZyklusPath )
self.Name = ZyklusName
Abfolge = eval( config.get( ZyklusName, 'Abfolge' ))
Anzahl = eval( config.get( ZyklusName, 'Anzahl' ))
Abfolge = _StringAusdruck( config.get( ZyklusName, 'Abfolge' ))
Anzahl = _StringAusdruck( config.get( ZyklusName, 'Anzahl' ))
self.Abfolge = Abfolge.split(',')
self.Anzahl = Anzahl.split(',')
pass
class Entwicklung:
"""Beobachtungsobjekt: zeichnet den Verlauf der Loesungsfindung auf.
Nach jeder Zyklus-Aktion wird ein Messpunkt erfasst (bester,
durchschnittlicher und schlechtester Wert im aktuellen Pool).
alsSVG() zeichnet daraus eine Verlaufsgrafik: Zeitachse (Aktions-
index) nach rechts, Wertigkeit auf der Y-Achse. Domaenen-agnostisch;
funktioniert mit jeder Loesung, die ein Attribut .value hat.
"""
# Buchstaben der Zyklus-Abfolge -> lesbare Namen fuer die Beschriftung
AktionsNamen = {
'e': 'erzeugen', 'm': 'mutieren', 's': 'selektieren',
'S': 'zufaellig', 'j': 'behalten', 'z': 'Generation' }
def __init__(self):
# je Messpunkt: (Schrittindex, Aktion, bester, schnitt, schlechtester, N)
self.Punkte = []
def erfassen(self, Aktion, Pool):
"""einen Messpunkt aus dem aktuellen Pool aufnehmen"""
if not Pool:
return
Werte = [ L.value for L in Pool ]
self.Punkte.append( (
len(self.Punkte),
Aktion,
max(Werte),
sum(Werte) / len(Werte),
min(Werte),
len(Werte) ) )
def __repr__(self):
r = "-- Entwicklung (%d Messpunkte):\n" % len(self.Punkte)
for (i, Aktion, best, schnitt, schlecht, n) in self.Punkte:
r = r + (" %3d %-12s best=%s schnitt=%.1f schlecht=%s (N=%d)\n"
% (i, self.AktionsNamen.get(Aktion, Aktion),
best, schnitt, schlecht, n))
return r
def alsSVG(self, OutputSVG, Breite=900, Hoehe=500, Rand=60):
"""zeichnet den aufgezeichneten Verlauf als SVG-Liniendiagramm"""
if not self.Punkte:
raise ValueError("keine Messpunkte aufgezeichnet")
AlleWerte = []
for (_i, _a, best, schnitt, schlecht, _n) in self.Punkte:
AlleWerte.extend( (best, schnitt, schlecht) )
WMin, WMax = min(AlleWerte), max(AlleWerte)
if WMax == WMin:
WMax = WMin + 1 # flache Linie nicht auf den Rand kleben
N = len(self.Punkte)
PlotBreite = Breite - 2 * Rand
PlotHoehe = Hoehe - 2 * Rand
def X(i):
if N == 1:
return Rand + PlotBreite / 2
return Rand + PlotBreite * i / (N - 1)
def Y(wert):
# hoher (besserer) Wert oben, niedriger unten
return Rand + PlotHoehe * (WMax - wert) / (WMax - WMin)
def Polyline(Index, Farbe, Beschriftung):
punkte = ' '.join( '%.1f,%.1f' % (X(i), Y(p[Index]))
for i, p in enumerate(self.Punkte) )
return ('<polyline points="%s" fill="none" stroke="%s" '
'stroke-width="2"/>\n' % (punkte, Farbe), Beschriftung, Farbe)
Teile = []
Teile.append( '<?xml version="1.0" encoding="UTF-8"?>\n' )
Teile.append(
'<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" '
'viewBox="0 0 %d %d" font-family="sans-serif">\n'
% (Breite, Hoehe, Breite, Hoehe) )
Teile.append( '<rect width="100%" height="100%" fill="white"/>\n' )
Teile.append(
'<text x="%d" y="30" text-anchor="middle" font-size="16" '
'fill="#333">Entwicklung der Loesungsfindung</text>\n'
% (Breite // 2) )
# Achsen
Teile.append(
'<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#333" '
'stroke-width="1.5"/>\n' % (Rand, Rand, Rand, Hoehe - Rand) )
Teile.append(
'<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#333" '
'stroke-width="1.5"/>\n'
% (Rand, Hoehe - Rand, Breite - Rand, Hoehe - Rand) )
# Y-Gitter + Beschriftung (5 Linien)
for k in range(6):
wert = WMin + (WMax - WMin) * k / 5
y = Y(wert)
Teile.append(
'<line x1="%d" y1="%.1f" x2="%d" y2="%.1f" stroke="#eee" '
'stroke-width="1"/>\n' % (Rand, y, Breite - Rand, y) )
Teile.append(
'<text x="%d" y="%.1f" text-anchor="end" font-size="11" '
'fill="#666">%.0f</text>\n' % (Rand - 8, y + 4, wert) )
Teile.append(
'<text x="18" y="%d" text-anchor="middle" font-size="12" '
'fill="#333" transform="rotate(-90 18 %d)">Wertigkeit '
'(hoeher = besser)</text>\n' % (Hoehe // 2, Hoehe // 2) )
Teile.append(
'<text x="%d" y="%d" text-anchor="middle" font-size="12" '
'fill="#333">Zyklus-Aktion (Zeit)</text>\n'
% (Breite // 2, Hoehe - 18) )
# X-Beschriftung: Aktionsbuchstaben (bei wenigen Punkten alle,
# sonst ausgeduennt, damit die Achse lesbar bleibt)
Schritt = max(1, N // 30)
for i, p in enumerate(self.Punkte):
if i % Schritt == 0 or i == N - 1:
Teile.append(
'<text x="%.1f" y="%d" text-anchor="middle" font-size="10" '
'fill="#999">%s</text>\n' % (X(i), Hoehe - Rand + 16, p[1]) )
# die drei Kurven
Kurven = [
Polyline(2, '#2f6f4f', 'beste Loesung'),
Polyline(3, '#c08a2d', 'Durchschnitt'),
Polyline(4, '#a03030', 'schlechteste Loesung') ]
for (svg, _besch, _farbe) in Kurven:
Teile.append( svg )
# Legende oben rechts
LegX = Breite - Rand - 160
LegY = Rand + 10
for k, (_svg, besch, farbe) in enumerate(Kurven):
y = LegY + k * 18
Teile.append(
'<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="%s" '
'stroke-width="2"/>\n' % (LegX, y, LegX + 24, y, farbe) )
Teile.append(
'<text x="%d" y="%d" font-size="11" fill="#333">%s</text>\n'
% (LegX + 30, y + 4, besch) )
Teile.append( '</svg>\n' )
fout = open( OutputSVG, 'w' )
fout.writelines( Teile )
fout.close()
class Farm:
"""enthaelt und pflegt Loesungsobjekte, kreuzt, mutiert, selektiert sie"""
def __init__(self, Zyklus, Klasse, *Arguments, **keywords):
"""initialisiere die Farm
mit einem definerten Zyklus fuer die Zucht und dem gewunschten zu Zuchtobjekt
Beobachtung: self.Entwicklung zeichnet nach jeder Zyklus-Aktion
den Pool-Zustand auf und laesst sich per alsSVG() visualisieren.
"""
self.Position = 0
self.Pool = []
self.PoolNeu = []
self.Zyklus = Zyklus
self.Klasse = Klasse
self.Entwicklung = Entwicklung()
for Aktion, Zahl in zip(Zyklus.Abfolge,Zyklus.Anzahl):
#print 'Aktion:',Aktion
#print 'Zahl:',Zahl
@@ -101,6 +279,11 @@ class Farm:
self.NeueGeneration()
else:
pass
# nach jeder Aktion den aktuellen Pool-Zustand aufzeichnen.
# Der beobachtete Pool ist der, in dem gerade die Loesungen
# liegen: PoolNeu, sobald dort etwas gesammelt wurde, sonst Pool.
self.Entwicklung.erfassen(
Aktion, self.PoolNeu if self.PoolNeu else self.Pool )
def LoesungenErzeugen(self, N, Klasse, *Arguments, **keywords):
""" Erzeuge Loesungen"""
for i in range(N):