refactoring: constraints
This commit is contained in:
parent
c2f12dcce9
commit
7ebe50723e
39
STRUKTUR.md
39
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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user