From 7ebe50723e4b9ae894ec961c9d4e8a132840f425 Mon Sep 17 00:00:00 2001 From: Jan Hoheisel Date: Tue, 23 Dec 2025 22:44:09 +0100 Subject: [PATCH] refactoring: constraints --- STRUKTUR.md | 39 +++++++----- elterndienstplaner.py | 141 ++++++++++++++++++++++++++++++------------ 2 files changed, 124 insertions(+), 56 deletions(-) diff --git a/STRUKTUR.md b/STRUKTUR.md index ad7db20..566eb27 100644 --- a/STRUKTUR.md +++ b/STRUKTUR.md @@ -26,9 +26,19 @@ - `Dienst`: Datenmodell für Diensttypen - `Elterndienstplaner`: Hauptklasse mit Optimierungslogik - Fairness-Berechnungen (global/lokal) - - Optimierungsmodell-Erstellung + - Optimierungsmodell-Erstellung (modular aufgeteilt) - Statistiken +**Constraint-Funktionen** (modular aufgeteilt): +- `_erstelle_entscheidungsvariablen()`: Erstellt binäre Variablen +- `_add_constraint_ein_dienst_pro_woche()`: C1 - Max 1 Dienst pro Woche +- `_add_constraint_ein_dienst_pro_tag()`: C2 - Max 1 Dienst pro Tag +- `_add_constraint_verfuegbarkeit()`: C3 - Nur verfügbare Eltern +- `_add_constraint_dienst_bedarf()`: C4 - Alle Dienste müssen besetzt werden +- `_add_fairness_constraints()`: F1 & F2 - Erstellt Variablen und Constraints für Fairness +- `_berechne_fairness_gewichte()`: Zeitabhängige Gewichtung (Sep-Jul) +- `_erstelle_zielfunktion()`: Zielfunktion mit Fairness & Präferenzen + **Abhängigkeiten**: - Importiert `csv_io` für Datei-Operationen - Verwendet `pulp` für lineare Optimierung @@ -41,7 +51,7 @@ elterndienstplaner.py (700+ Zeilen) ├── Dienst Klasse ├── CSV Parsing (150+ Zeilen) ├── Fairness-Berechnung -├── Optimierung +├── Optimierung (200+ Zeilen inline) └── CSV Schreiben ``` @@ -54,30 +64,29 @@ csv_io.py (220 Zeilen) elterndienstplaner.py (500 Zeilen) ├── Dienst Klasse ├── Fairness-Berechnung -├── Optimierung +│ ├── berechne_faire_zielverteilung_global() +│ └── berechne_faire_zielverteilung_lokal() +├── Optimierung (modular) +│ ├── erstelle_optimierungsmodell() (30 Zeilen - übersichtlich!) +│ ├── _erstelle_entscheidungsvariablen() +│ ├── _add_constraint_ein_dienst_pro_woche() +│ ├── _add_constraint_ein_dienst_pro_tag() +│ ├── _add_constraint_verfuegbarkeit() +│ ├── _add_constraint_dienst_bedarf() +│ ├── _add_fairness_constraints() +│ └── _erstelle_zielfunktion() └── Statistiken ``` ## Nächste Schritte (Optional) -### Phase 2: Constraint-Funktionen auslagern -```python -def _add_constraint_ein_dienst_pro_woche(prob, x, ...): - """C1: Je Eltern und Dienst nur einmal die Woche""" - -def _add_constraint_ein_dienst_pro_tag(prob, x, ...): - """C2: Je Eltern nur einen Dienst am Tag""" -``` - ### Phase 3: Fairness-Modul (optional) ``` fairness.py ├── FairnessBerechner │ ├── berechne_global() │ └── berechne_lokal() -``` - -## Verwendung +```## Verwendung ```python from csv_io import EingabeParser, AusgabeWriter diff --git a/elterndienstplaner.py b/elterndienstplaner.py index 3e0ffda..c7bd4c7 100755 --- a/elterndienstplaner.py +++ b/elterndienstplaner.py @@ -254,21 +254,8 @@ class Elterndienstplaner: return ziel_dienste_lokal - def erstelle_optimierungsmodell(self) -> Tuple[pulp.LpProblem, Dict[Tuple[str, date, Dienst], pulp.LpVariable]]: - """Erstellt das PuLP Optimierungsmodell""" - print("Erstelle Optimierungsmodell...") - - # Debugging: Verfügbarkeit prüfen - print("\nDebug: Verfügbarkeit analysieren...") - for tag in self.tage[:5]: # Erste 5 Tage - verfügbare = [e for e in self.eltern if self.verfügbarkeit.get((e, tag), True)] - benötigte = self.benoetigte_dienste.get(tag, []) - print(f" {tag}: Benötigt {len(benötigte)} Dienste {benötigte}, verfügbar: {verfügbare}") - - # LP Problem erstellen - prob = pulp.LpProblem("Elterndienstplaner", pulp.LpMinimize) - - # Entscheidungsvariablen: x[eltern, tag, dienst] ∈ {0,1} + def _erstelle_entscheidungsvariablen(self) -> Dict[Tuple[str, date, Dienst], pulp.LpVariable]: + """Erstellt die binären Entscheidungsvariablen x[eltern, tag, dienst]""" x: Dict[Tuple[str, date, Dienst], pulp.LpVariable] = {} for eltern in self.eltern: for tag in self.tage: @@ -278,10 +265,14 @@ class Elterndienstplaner: f"x_{eltern.replace(' ', '_')}_{tag}_{dienst.kuerzel}", cat='Binary' ) + return x - # Vereinfachtes Modell: Grundlegende Constraints - - # C1: Je Eltern und Dienst nur einmal die Woche + def _add_constraint_ein_dienst_pro_woche( + self, + prob: pulp.LpProblem, + x: Dict[Tuple[str, date, Dienst], pulp.LpVariable] + ) -> None: + """C1: Je Eltern und Dienst nur einmal die Woche""" woche_start = self.tage[0] woche_nr = 0 while woche_start <= self.tage[-1]: @@ -296,12 +287,18 @@ class Elterndienstplaner: woche_vars.append(x[eltern, tag, dienst]) if woche_vars: - prob += pulp.lpSum(woche_vars) <= 1, f"C1_{eltern.replace(' ', '_')}_{dienst.kuerzel}_w{woche_nr}" + prob += pulp.lpSum(woche_vars) <= 1, \ + f"C1_{eltern.replace(' ', '_')}_{dienst.kuerzel}_w{woche_nr}" woche_start += timedelta(days=7) woche_nr += 1 - # C2: Je Eltern nur einen Dienst am Tag + def _add_constraint_ein_dienst_pro_tag( + self, + prob: pulp.LpProblem, + x: Dict[Tuple[str, date, Dienst], pulp.LpVariable] + ) -> None: + """C2: Je Eltern nur einen Dienst am Tag""" for eltern in self.eltern: for tag in self.tage: tag_vars = [] @@ -312,39 +309,50 @@ class Elterndienstplaner: if tag_vars: prob += pulp.lpSum(tag_vars) <= 1, f"C2_{eltern.replace(' ', '_')}_{tag}" - # C3: Dienste nur verfügbaren Eltern zuteilen + def _add_constraint_verfuegbarkeit( + self, + prob: pulp.LpProblem, + x: Dict[Tuple[str, date, Dienst], pulp.LpVariable] + ) -> None: + """C3: Dienste nur verfügbaren Eltern zuteilen""" for eltern in self.eltern: for tag in self.tage: if not self.verfügbarkeit.get((eltern, tag), True): for dienst in self.dienste: if (eltern, tag, dienst) in x: - prob += x[eltern, tag, dienst] == 0, f"C3_{eltern.replace(' ', '_')}_{tag}_{dienst.kuerzel}" + prob += x[eltern, tag, dienst] == 0, \ + f"C3_{eltern.replace(' ', '_')}_{tag}_{dienst.kuerzel}" - # Alle benötigten Dienste müssen zugeteilt werden (flexibel) + def _add_constraint_dienst_bedarf( + self, + prob: pulp.LpProblem, + x: Dict[Tuple[str, date, Dienst], pulp.LpVariable] + ) -> None: + """C4: Alle benötigten Dienste müssen zugeteilt werden""" for tag in self.tage: for dienst in self.benoetigte_dienste.get(tag, []): dienst_vars = [] - verfuegbare_eltern = 0 for eltern in self.eltern: if (eltern, tag, dienst) in x: # Prüfe ob Eltern verfügbar if self.verfügbarkeit.get((eltern, tag), True): dienst_vars.append(x[eltern, tag, dienst]) - verfuegbare_eltern += 1 if dienst_vars: # Anzahl benötigter Personen pro Dienst (aus Dienst-Objekt) benoetigte_personen = dienst.personen_anzahl - prob += pulp.lpSum(dienst_vars) == benoetigte_personen, f"Bedarf_{tag}_{dienst.kuerzel}" + prob += pulp.lpSum(dienst_vars) == benoetigte_personen, \ + f"Bedarf_{tag}_{dienst.kuerzel}" - # FAIRNESS-CONSTRAINTS UND ZIELFUNKTION - objective_terms = [] - - # Berechne faire Zielverteilungen (global und lokal) - ziel_dienste_global = self.berechne_faire_zielverteilung_global() - ziel_dienste_lokal = self.berechne_faire_zielverteilung_lokal() - - # Hilfsvariablen für Fairness-Abweichungen + def _add_fairness_constraints( + self, + prob: pulp.LpProblem, + x: Dict[Tuple[str, date, Dienst], pulp.LpVariable], + ziel_dienste_global: DefaultDict[str, DefaultDict[Dienst, float]], + ziel_dienste_lokal: DefaultDict[str, DefaultDict[Dienst, float]] + ) -> Tuple[Dict, Dict]: + """F1 & F2: Erstellt Fairness-Variablen und fügt Fairness-Constraints hinzu (global und lokal)""" + # Hilfsvariablen für Fairness-Abweichungen erstellen fairness_abweichung_lokal = {} # F2 fairness_abweichung_global = {} # F1 @@ -355,7 +363,7 @@ class Elterndienstplaner: fairness_abweichung_global[eltern, dienst] = pulp.LpVariable( f"fair_global_{eltern.replace(' ', '_')}_{dienst.kuerzel}", lowBound=0) - # F1: Globale Fairness & F2: Lokale Fairness + # Fairness-Constraints hinzufügen for eltern in self.eltern: for dienst in self.dienste: # Tatsächliche Dienste im aktuellen Monat @@ -368,7 +376,6 @@ class Elterndienstplaner: # F2: Lokale Fairness - nur aktueller Monat ziel_lokal = ziel_dienste_lokal[eltern][dienst] if ziel_lokal > 0: - # Lokale Fairness-Constraints prob += (tatsaechliche_dienste_monat - ziel_lokal <= fairness_abweichung_lokal[eltern, dienst]) prob += (ziel_lokal - tatsaechliche_dienste_monat <= @@ -382,15 +389,17 @@ class Elterndienstplaner: # Tatsächliche Dienste global (Vergangenheit + geplant) total_dienste_inkl_vergangenheit = tatsaechliche_dienste_monat + vorherige_dienste - # Globale Fairness-Constraints prob += (total_dienste_inkl_vergangenheit - ziel_global <= fairness_abweichung_global[eltern, dienst]) prob += (ziel_global - total_dienste_inkl_vergangenheit <= fairness_abweichung_global[eltern, dienst]) - # Gewichtung: Jahresanfang F1 stärker, Jahresende F2 stärker - # Annahme: September = Jahresanfang, Juli = Jahresende + return fairness_abweichung_lokal, fairness_abweichung_global + + def _berechne_fairness_gewichte(self) -> Tuple[int, int]: + """Berechnet Gewichtung basierend auf Jahreszeit (Sep-Jul Schuljahr)""" aktueller_monat = self.tage[0].month if self.tage else 1 + if 9 <= aktueller_monat <= 12: # Sep-Dez: Jahresanfang gewicht_f1 = 100 # Global wichtiger gewicht_f2 = 50 # Lokal weniger wichtig @@ -401,6 +410,21 @@ class Elterndienstplaner: gewicht_f1 = 50 # Global weniger wichtig gewicht_f2 = 100 # Lokal wichtiger + return gewicht_f1, gewicht_f2 + + def _erstelle_zielfunktion( + self, + prob: pulp.LpProblem, + x: Dict[Tuple[str, date, Dienst], pulp.LpVariable], + fairness_abweichung_lokal: Dict, + fairness_abweichung_global: Dict + ) -> None: + """Erstellt die Zielfunktion mit Fairness und Präferenzen""" + objective_terms = [] + + # Fairness-Gewichtung + gewicht_f1, gewicht_f2 = self._berechne_fairness_gewichte() + # Fairness-Terme zur Zielfunktion hinzufügen for eltern in self.eltern: for dienst in self.dienste: @@ -410,12 +434,12 @@ class Elterndienstplaner: # P1: Bevorzugte Dienste (positiv belohnen) for (eltern, tag, dienst), präf in self.präferenzen.items(): if (eltern, tag, dienst) in x and präf == 1: # bevorzugt - objective_terms.append(-5 * x[eltern, tag, dienst]) # Schwächer als Fairness + objective_terms.append(-5 * x[eltern, tag, dienst]) # P2: Abgelehnte Dienste (bestrafen) for (eltern, tag, dienst), präf in self.präferenzen.items(): if (eltern, tag, dienst) in x and präf == -1: # abgelehnt - objective_terms.append(25 * x[eltern, tag, dienst]) # Schwächer als Fairness + objective_terms.append(25 * x[eltern, tag, dienst]) # Zielfunktion setzen if objective_terms: @@ -425,6 +449,41 @@ class Elterndienstplaner: prob += pulp.lpSum([var for var in x.values()]) print(f"Verwende Gewichtung: F1 (global) = {gewicht_f1}, F2 (lokal) = {gewicht_f2}") + + def erstelle_optimierungsmodell(self) -> Tuple[pulp.LpProblem, Dict[Tuple[str, date, Dienst], pulp.LpVariable]]: + """Erstellt das PuLP Optimierungsmodell""" + print("Erstelle Optimierungsmodell...") + + # Debugging: Verfügbarkeit prüfen + print("\nDebug: Verfügbarkeit analysieren...") + for tag in self.tage[:5]: # Erste 5 Tage + verfügbare = [e for e in self.eltern if self.verfügbarkeit.get((e, tag), True)] + benötigte = self.benoetigte_dienste.get(tag, []) + print(f" {tag}: Benötigt {len(benötigte)} Dienste {benötigte}, verfügbar: {verfügbare}") + + # LP Problem erstellen + prob = pulp.LpProblem("Elterndienstplaner", pulp.LpMinimize) + + # Entscheidungsvariablen erstellen + x = self._erstelle_entscheidungsvariablen() + + # Grundlegende Constraints hinzufügen + self._add_constraint_ein_dienst_pro_woche(prob, x) + self._add_constraint_ein_dienst_pro_tag(prob, x) + self._add_constraint_verfuegbarkeit(prob, x) + self._add_constraint_dienst_bedarf(prob, x) + + # Fairness-Constraints + ziel_dienste_global = self.berechne_faire_zielverteilung_global() + ziel_dienste_lokal = self.berechne_faire_zielverteilung_lokal() + + fairness_abweichung_lokal, fairness_abweichung_global = self._add_fairness_constraints( + prob, x, ziel_dienste_global, ziel_dienste_lokal + ) + + # Zielfunktion erstellen + self._erstelle_zielfunktion(prob, x, fairness_abweichung_lokal, fairness_abweichung_global) + print(f"Modell erstellt mit {len(x)} Variablen und {len(prob.constraints)} Constraints") return prob, x