Compare commits
No commits in common. "59534a98f07a27e017370beb66fcd674e03304d8" and "beea66ef2f9dee27e3690b9d661c416dd6c28a78" have entirely different histories.
59534a98f0
...
beea66ef2f
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user