Compare commits

..

No commits in common. "59534a98f07a27e017370beb66fcd674e03304d8" and "beea66ef2f9dee27e3690b9d661c416dd6c28a78" have entirely different histories.

View File

@ -15,7 +15,6 @@ from typing import Dict, List, Tuple, DefaultDict, Optional
from datenmodell import ElterndienstplanerDaten, Dienst, Eltern, Zielverteilung, Entscheidungsvariablen
from ausgabe import ElterndienstAusgabe
from csv_io import EingabeParser
class Elterndienstplaner:
@ -383,14 +382,13 @@ class Elterndienstplaner:
fairness_abweichung_lokal: Dict,
fairness_abweichung_global: Dict,
fairness_abweichung_gesamt_global: Dict,
fairness_abweichung_gesamt_lokal: Dict,
spreizungs_penalty: pulp.LpAffineExpression = None
fairness_abweichung_gesamt_lokal: Dict
) -> None:
"""Erstellt die Zielfunktion mit Fairness, Präferenzen und Spreizung"""
"""Erstellt die Zielfunktion mit Fairness und Praeferenzen"""
objective_terms = []
gewicht_global = 60
gewicht_lokal = 40
gewicht_global = 50
gewicht_lokal = 50
gewicht_f1 = gewicht_global
gewicht_f2 = gewicht_lokal
gewicht_f3_global = 0.25 * gewicht_global
@ -416,10 +414,6 @@ class Elterndienstplaner:
if (eltern, tag, dienst) in x and praef == -1:
objective_terms.append(20 * dienst.aufwand * x[eltern, tag, dienst])
# Spreizungs-Penalty (neu)
if spreizungs_penalty is not None and spreizungs_penalty != 0:
objective_terms.append(spreizungs_penalty)
if objective_terms:
prob += pulp.lpSum(objective_terms)
else:
@ -432,7 +426,11 @@ class Elterndienstplaner:
pulp.LpProblem,
Entscheidungsvariablen
]:
"""Erstellt das PuLP Optimierungsmodell mit Aufwandsspreizung"""
"""Erstellt das PuLP Optimierungsmodell
Returns:
Tuple mit (prob, x, ziel_dienste_lokal, ziel_dienste_global)
"""
print("Erstelle Optimierungsmodell...")
print("\nDebug: Verfuegbarkeit analysieren...")
@ -474,17 +472,8 @@ class Elterndienstplaner:
prob, x, ziel_dienste_lokal, "lokal"
)
# Spreizungs-Penalty (neu)
#print("\nErstelle Aufwandsspreizungs-Penalty...")
#spreizungs_penalty = self._erstelle_spreizungs_penalty_mit_dienstfaktor(
# x, fenster_groesse=15, sigma=0.8, spreizungs_gewicht=1.0
#)
#self.visualisiere_fenster_gewichte_mit_dienstfaktoren(fenster_groesse=15, sigma=0.8)
spreizungs_penalty = None
self._erstelle_zielfunktion(prob, x, fairness_abweichung_lokal, fairness_abweichung_global,
fairness_abweichung_gesamt_global, fairness_abweichung_gesamt_lokal,
spreizungs_penalty)
fairness_abweichung_gesamt_global, fairness_abweichung_gesamt_lokal)
print(f"Modell erstellt mit {len(x)} Variablen und {len(prob.constraints)} Constraints")
return prob, x
@ -528,229 +517,8 @@ class Elterndienstplaner:
return loesung
def _berechne_fenster_gewichte_glocke(self, num_fenster: int, sigma: float = 0.8) -> List[float]:
"""Berechnet Fenster-Gewichte basierend auf Gauß-Glockenkurve
Mittelfenster: Gewicht 1.0 (höchste Bestrafung von Häufungen)
Randfenster: Gewicht 0.3 (niedrigere Bestrafung)
Args:
num_fenster: Anzahl der Fenster
sigma: Steile der Kurve (0.3=spitz, 1.5=flach)
Returns:
Liste mit Gewichten [0.0...1.0] pro Fenster
"""
import math
if num_fenster <= 1:
return [1.0]
fenster_gewichte = []
mitte = (num_fenster - 1) / 2.0
for i in range(num_fenster):
# Normalisierte Position relativ zur Mitte (-1...+1)
position = (i - mitte) / (mitte + 1)
# Gauß-Funktion: exp(-(position² / sigma²))
gewicht = math.exp(-(position ** 2) / (sigma ** 2))
fenster_gewichte.append(gewicht)
return fenster_gewichte
def _erstelle_spreizungs_penalty_mit_dienstfaktor(
self,
x: Entscheidungsvariablen,
fenster_groesse: int = 15,
sigma: float = 0.8,
spreizungs_gewicht: float = 0.5
) -> pulp.LpAffineExpression:
"""Erstellt Penalty-Term für Aufwandsspreizung mit Dienstfaktor-Normalisierung
Verwendet 15-Tage-Sliding-Window (1-Tage-Schritte) zur Erkennung von Häufungen.
Höhere Aufwände in der Fenster-Mitte werden stärker bestraft.
Args:
x: Entscheidungsvariablen
fenster_groesse: Größe des Sliding-Window in Tagen (Standard: 15)
sigma: Parameter für Glockenkurven-Steile
spreizungs_gewicht: Gewichtung relative zur Fairness (Standard: 0.5)
Returns:
Penalty-Term als LpAffineExpression für Zielfunktion
"""
import math
from datetime import timedelta
penalty_terms = []
planungstage = self.daten.planungszeitraum
if len(planungstage) < fenster_groesse:
print(f" Warnung: Planungszeitraum ({len(planungstage)} Tage) < Fenster ({fenster_groesse} Tage)")
return pulp.lpSum([])
# Berechne Anzahl der Fenster für Glockenkurve
num_fenster = len(planungstage) - fenster_groesse + 1
fenster_gewichte = self._berechne_fenster_gewichte_glocke(num_fenster, sigma)
print(f" Spreizungs-Constraint: {len(planungstage)} Planungstage, "
f"{fenster_groesse}-Tage-Fenster → {num_fenster} Fenster")
print(f" Fenster-Gewichte (Glockenkurve, σ={sigma}):")
for i, gew in enumerate(fenster_gewichte):
print(f" Fenster {i+1}: {gew:.3f}")
# Erstelle Sliding-Windows (1-Tage-Schritte)
for fenster_idx in range(num_fenster):
fenster_start_idx = fenster_idx
fenster_ende_idx = fenster_idx + fenster_groesse
fenster_start_tag = planungstage[fenster_start_idx]
fenster_ende_tag = planungstage[fenster_ende_idx - 1]
fenster_gewicht = fenster_gewichte[fenster_idx]
# Für jeden Elterns: Aufwand im Fenster berechnen (normalisiert durch Dienstfaktor)
for eltern in self.daten.eltern:
dienstfaktor_summe = sum(
self.daten.dienstfaktoren[eltern][tag]
for tag in planungstage[fenster_start_idx:fenster_ende_idx]
)
# Schutz vor Division-by-Zero
if dienstfaktor_summe <= 0:
continue
# Summiere Aufwände aller Dienste im Fenster für diesen Elterns
aufwand_im_fenster = pulp.lpSum(
dienst.aufwand * x[eltern, tag, dienst]
for tag in planungstage[fenster_start_idx:fenster_ende_idx]
for dienst in self.daten.dienste
if (eltern, tag, dienst) in x
) if any(
(eltern, tag, dienst) in x
for tag in planungstage[fenster_start_idx:fenster_ende_idx]
for dienst in self.daten.dienste
) else 0
if aufwand_im_fenster == 0:
continue
# Normalisiere durch Dienstfaktor und multipliziere mit Fenster-Gewicht
# penalty = (aufwand / dienstfaktor) × fenster_gewicht
penalty_terms.append(
spreizungs_gewicht * fenster_gewicht * (aufwand_im_fenster / dienstfaktor_summe)
)
if penalty_terms:
return pulp.lpSum(penalty_terms)
else:
return pulp.lpSum([])
def visualisiere_fenster_gewichte_mit_dienstfaktoren(
self,
fenster_groesse: int = 15,
sigma: float = 0.8
) -> None:
"""Debug-Hilfsfunktion: Zeigt Fenster-Gewichte und Dienstfaktoren tabellarisch
Args:
fenster_groesse: Größe des Sliding-Window in Tagen
sigma: Parameter für Glockenkurven-Steile
"""
planungstage = self.daten.planungszeitraum
if len(planungstage) < fenster_groesse:
print(f"Planungszeitraum ({len(planungstage)} Tage) < Fenster ({fenster_groesse} Tage)")
return
num_fenster = len(planungstage) - fenster_groesse + 1
fenster_gewichte = self._berechne_fenster_gewichte_glocke(num_fenster, sigma)
print("\n" + "="*100)
print(f"DEBUG: Fenster-Gewichte ({fenster_groesse}-Tage-Sliding-Window, σ={sigma})")
print("="*100)
print(f"\nPlanungszeitraum: {planungstage[0]} bis {planungstage[-1]} ({len(planungstage)} Tage)")
print(f"Fenster: {num_fenster} × {fenster_groesse} Tage\n")
# Kopfzeile
print(f"{'Fenster':<12} {'Start':12} {'Ende':12} {'Gewicht':>10} {'→ Bedeutung':<30}")
print("-" * 100)
for fenster_idx in range(num_fenster):
fenster_start_idx = fenster_idx
fenster_ende_idx = fenster_idx + fenster_groesse
fenster_start_tag = planungstage[fenster_start_idx]
fenster_ende_tag = planungstage[fenster_ende_idx - 1]
fenster_gewicht = fenster_gewichte[fenster_idx]
# Interpretation
if fenster_gewicht > 0.9:
bedeutung = "Hoch (Häufungen stark bestraft)"
farbe = "\033[91m" # Rot
elif fenster_gewicht > 0.7:
bedeutung = "Mittel (Häufungen moderat bestraft)"
farbe = "\033[93m" # Gelb
else:
bedeutung = "Niedrig (Häufungen toleriert)"
farbe = "\033[92m" # Grün
reset = "\033[0m"
print(f"{fenster_idx+1:<12} {str(fenster_start_tag):12} {str(fenster_ende_tag):12} "
f"{farbe}{fenster_gewicht:>10.3f}{reset} {bedeutung}")
print("\n" + "-" * 100)
print("Legende:")
print(f" {'\033[91m'}Gewicht > 0.9{'\033[0m'}: Häufungen in der Mitte stark bestraft")
print(f" {'\033[93m'}Gewicht 0.7-0.9{'\033[0m'}: Häufungen moderat bestraft")
print(f" {'\033[92m'}Gewicht < 0.7{'\033[0m'}: Häufungen am Rand toleriert")
def main() -> None:
# New: stats-only mode: ./elterndienstplaner.py --stats-only <eltern.csv> [<vorherige-ausgaben.csv>]
if len(sys.argv) >= 2 and sys.argv[1] == '--stats-only':
if len(sys.argv) < 3:
print("Usage: ./elterndienstplaner.py --stats-only <eltern.csv> [<vorherige-ausgaben.csv>]")
sys.exit(1)
eltern_datei = sys.argv[2]
vorherige_datei = sys.argv[3] if len(sys.argv) > 3 else None
print("Elterndienstplaner: Statistik-Modus (keine Optimierung)")
try:
daten = ElterndienstplanerDaten()
# Nur Elterndaten und historische Dienste einlesen
daten.dienstfaktoren = EingabeParser.parse_eltern_csv(eltern_datei)
daten.eltern = list(daten.dienstfaktoren.keys())
if vorherige_datei:
daten.historische_dienste = EingabeParser.parse_vorherige_ausgaben_csv(
vorherige_datei, daten.eltern, daten.dienste
)
ausgabe = ElterndienstAusgabe(daten)
# Berechne globale Zielverteilung (benötigt für Übersicht)
planer = Elterndienstplaner(daten, ausgabe)
ziel_global = planer.berechne_faire_zielverteilung_global()
from collections import defaultdict
ziel_lokal = defaultdict(lambda: defaultdict(float))
ausgabe.setze_zielverteilungen(ziel_lokal, ziel_global)
ausgabe.setze_historische_dienste(daten.historische_dienste)
# Keine Optimierung: nur Übersicht ausgeben
ausgabe.visualisiere_dienste_uebersicht({})
sys.exit(0)
except Exception as e:
print(f"\n✗ Fehler im Statistik-Modus: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
# Standardmodus: Optimierung
if len(sys.argv) < 4:
print("Usage: ./elterndienstplaner.py <eingabe.csv> <eltern.csv> <ausgabe.csv> [<vorherige-ausgaben.csv>]")
sys.exit(1)