refactoring: constraints

This commit is contained in:
Jan Hoheisel 2025-12-23 22:44:09 +01:00
parent b65dd3ef95
commit d520dcafc5
2 changed files with 124 additions and 56 deletions

View File

@ -26,9 +26,19 @@
- `Dienst`: Datenmodell für Diensttypen - `Dienst`: Datenmodell für Diensttypen
- `Elterndienstplaner`: Hauptklasse mit Optimierungslogik - `Elterndienstplaner`: Hauptklasse mit Optimierungslogik
- Fairness-Berechnungen (global/lokal) - Fairness-Berechnungen (global/lokal)
- Optimierungsmodell-Erstellung - Optimierungsmodell-Erstellung (modular aufgeteilt)
- Statistiken - 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**: **Abhängigkeiten**:
- Importiert `csv_io` für Datei-Operationen - Importiert `csv_io` für Datei-Operationen
- Verwendet `pulp` für lineare Optimierung - Verwendet `pulp` für lineare Optimierung
@ -41,7 +51,7 @@ elterndienstplaner.py (700+ Zeilen)
├── Dienst Klasse ├── Dienst Klasse
├── CSV Parsing (150+ Zeilen) ├── CSV Parsing (150+ Zeilen)
├── Fairness-Berechnung ├── Fairness-Berechnung
├── Optimierung ├── Optimierung (200+ Zeilen inline)
└── CSV Schreiben └── CSV Schreiben
``` ```
@ -54,30 +64,29 @@ csv_io.py (220 Zeilen)
elterndienstplaner.py (500 Zeilen) elterndienstplaner.py (500 Zeilen)
├── Dienst Klasse ├── Dienst Klasse
├── Fairness-Berechnung ├── 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 └── Statistiken
``` ```
## Nächste Schritte (Optional) ## 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) ### Phase 3: Fairness-Modul (optional)
``` ```
fairness.py fairness.py
├── FairnessBerechner ├── FairnessBerechner
│ ├── berechne_global() │ ├── berechne_global()
│ └── berechne_lokal() │ └── berechne_lokal()
``` ```## Verwendung
## Verwendung
```python ```python
from csv_io import EingabeParser, AusgabeWriter from csv_io import EingabeParser, AusgabeWriter

View File

@ -254,21 +254,8 @@ class Elterndienstplaner:
return ziel_dienste_lokal return ziel_dienste_lokal
def erstelle_optimierungsmodell(self) -> Tuple[pulp.LpProblem, Dict[Tuple[str, date, Dienst], pulp.LpVariable]]: def _erstelle_entscheidungsvariablen(self) -> Dict[Tuple[str, date, Dienst], pulp.LpVariable]:
"""Erstellt das PuLP Optimierungsmodell""" """Erstellt die binären Entscheidungsvariablen x[eltern, tag, dienst]"""
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}
x: Dict[Tuple[str, date, Dienst], pulp.LpVariable] = {} x: Dict[Tuple[str, date, Dienst], pulp.LpVariable] = {}
for eltern in self.eltern: for eltern in self.eltern:
for tag in self.tage: for tag in self.tage:
@ -278,10 +265,14 @@ class Elterndienstplaner:
f"x_{eltern.replace(' ', '_')}_{tag}_{dienst.kuerzel}", f"x_{eltern.replace(' ', '_')}_{tag}_{dienst.kuerzel}",
cat='Binary' cat='Binary'
) )
return x
# Vereinfachtes Modell: Grundlegende Constraints def _add_constraint_ein_dienst_pro_woche(
self,
# C1: Je Eltern und Dienst nur einmal die Woche 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_start = self.tage[0]
woche_nr = 0 woche_nr = 0
while woche_start <= self.tage[-1]: while woche_start <= self.tage[-1]:
@ -296,12 +287,18 @@ class Elterndienstplaner:
woche_vars.append(x[eltern, tag, dienst]) woche_vars.append(x[eltern, tag, dienst])
if woche_vars: 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_start += timedelta(days=7)
woche_nr += 1 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 eltern in self.eltern:
for tag in self.tage: for tag in self.tage:
tag_vars = [] tag_vars = []
@ -312,39 +309,50 @@ class Elterndienstplaner:
if tag_vars: if tag_vars:
prob += pulp.lpSum(tag_vars) <= 1, f"C2_{eltern.replace(' ', '_')}_{tag}" 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 eltern in self.eltern:
for tag in self.tage: for tag in self.tage:
if not self.verfügbarkeit.get((eltern, tag), True): if not self.verfügbarkeit.get((eltern, tag), True):
for dienst in self.dienste: for dienst in self.dienste:
if (eltern, tag, dienst) in x: 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 tag in self.tage:
for dienst in self.benoetigte_dienste.get(tag, []): for dienst in self.benoetigte_dienste.get(tag, []):
dienst_vars = [] dienst_vars = []
verfuegbare_eltern = 0
for eltern in self.eltern: for eltern in self.eltern:
if (eltern, tag, dienst) in x: if (eltern, tag, dienst) in x:
# Prüfe ob Eltern verfügbar # Prüfe ob Eltern verfügbar
if self.verfügbarkeit.get((eltern, tag), True): if self.verfügbarkeit.get((eltern, tag), True):
dienst_vars.append(x[eltern, tag, dienst]) dienst_vars.append(x[eltern, tag, dienst])
verfuegbare_eltern += 1
if dienst_vars: if dienst_vars:
# Anzahl benötigter Personen pro Dienst (aus Dienst-Objekt) # Anzahl benötigter Personen pro Dienst (aus Dienst-Objekt)
benoetigte_personen = dienst.personen_anzahl 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 def _add_fairness_constraints(
objective_terms = [] self,
prob: pulp.LpProblem,
# Berechne faire Zielverteilungen (global und lokal) x: Dict[Tuple[str, date, Dienst], pulp.LpVariable],
ziel_dienste_global = self.berechne_faire_zielverteilung_global() ziel_dienste_global: DefaultDict[str, DefaultDict[Dienst, float]],
ziel_dienste_lokal = self.berechne_faire_zielverteilung_lokal() ziel_dienste_lokal: DefaultDict[str, DefaultDict[Dienst, float]]
) -> Tuple[Dict, Dict]:
# Hilfsvariablen für Fairness-Abweichungen """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_lokal = {} # F2
fairness_abweichung_global = {} # F1 fairness_abweichung_global = {} # F1
@ -355,7 +363,7 @@ class Elterndienstplaner:
fairness_abweichung_global[eltern, dienst] = pulp.LpVariable( fairness_abweichung_global[eltern, dienst] = pulp.LpVariable(
f"fair_global_{eltern.replace(' ', '_')}_{dienst.kuerzel}", lowBound=0) 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 eltern in self.eltern:
for dienst in self.dienste: for dienst in self.dienste:
# Tatsächliche Dienste im aktuellen Monat # Tatsächliche Dienste im aktuellen Monat
@ -368,7 +376,6 @@ class Elterndienstplaner:
# F2: Lokale Fairness - nur aktueller Monat # F2: Lokale Fairness - nur aktueller Monat
ziel_lokal = ziel_dienste_lokal[eltern][dienst] ziel_lokal = ziel_dienste_lokal[eltern][dienst]
if ziel_lokal > 0: if ziel_lokal > 0:
# Lokale Fairness-Constraints
prob += (tatsaechliche_dienste_monat - ziel_lokal <= prob += (tatsaechliche_dienste_monat - ziel_lokal <=
fairness_abweichung_lokal[eltern, dienst]) fairness_abweichung_lokal[eltern, dienst])
prob += (ziel_lokal - tatsaechliche_dienste_monat <= prob += (ziel_lokal - tatsaechliche_dienste_monat <=
@ -382,15 +389,17 @@ class Elterndienstplaner:
# Tatsächliche Dienste global (Vergangenheit + geplant) # Tatsächliche Dienste global (Vergangenheit + geplant)
total_dienste_inkl_vergangenheit = tatsaechliche_dienste_monat + vorherige_dienste total_dienste_inkl_vergangenheit = tatsaechliche_dienste_monat + vorherige_dienste
# Globale Fairness-Constraints
prob += (total_dienste_inkl_vergangenheit - ziel_global <= prob += (total_dienste_inkl_vergangenheit - ziel_global <=
fairness_abweichung_global[eltern, dienst]) fairness_abweichung_global[eltern, dienst])
prob += (ziel_global - total_dienste_inkl_vergangenheit <= prob += (ziel_global - total_dienste_inkl_vergangenheit <=
fairness_abweichung_global[eltern, dienst]) fairness_abweichung_global[eltern, dienst])
# Gewichtung: Jahresanfang F1 stärker, Jahresende F2 stärker return fairness_abweichung_lokal, fairness_abweichung_global
# Annahme: September = Jahresanfang, Juli = Jahresende
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 aktueller_monat = self.tage[0].month if self.tage else 1
if 9 <= aktueller_monat <= 12: # Sep-Dez: Jahresanfang if 9 <= aktueller_monat <= 12: # Sep-Dez: Jahresanfang
gewicht_f1 = 100 # Global wichtiger gewicht_f1 = 100 # Global wichtiger
gewicht_f2 = 50 # Lokal weniger wichtig gewicht_f2 = 50 # Lokal weniger wichtig
@ -401,6 +410,21 @@ class Elterndienstplaner:
gewicht_f1 = 50 # Global weniger wichtig gewicht_f1 = 50 # Global weniger wichtig
gewicht_f2 = 100 # Lokal wichtiger 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 # Fairness-Terme zur Zielfunktion hinzufügen
for eltern in self.eltern: for eltern in self.eltern:
for dienst in self.dienste: for dienst in self.dienste:
@ -410,12 +434,12 @@ class Elterndienstplaner:
# P1: Bevorzugte Dienste (positiv belohnen) # P1: Bevorzugte Dienste (positiv belohnen)
for (eltern, tag, dienst), präf in self.präferenzen.items(): for (eltern, tag, dienst), präf in self.präferenzen.items():
if (eltern, tag, dienst) in x and präf == 1: # bevorzugt 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) # P2: Abgelehnte Dienste (bestrafen)
for (eltern, tag, dienst), präf in self.präferenzen.items(): for (eltern, tag, dienst), präf in self.präferenzen.items():
if (eltern, tag, dienst) in x and präf == -1: # abgelehnt 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 # Zielfunktion setzen
if objective_terms: if objective_terms:
@ -425,6 +449,41 @@ class Elterndienstplaner:
prob += pulp.lpSum([var for var in x.values()]) prob += pulp.lpSum([var for var in x.values()])
print(f"Verwende Gewichtung: F1 (global) = {gewicht_f1}, F2 (lokal) = {gewicht_f2}") 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") print(f"Modell erstellt mit {len(x)} Variablen und {len(prob.constraints)} Constraints")
return prob, x return prob, x