Compare commits

..

No commits in common. "beea66ef2f9dee27e3690b9d661c416dd6c28a78" and "3b94f460a804d0a88f59c722d64f83a4a56ef0e6" have entirely different histories.

5 changed files with 65 additions and 254 deletions

View File

@ -54,23 +54,17 @@ Leon,2024-09-01,2024-12-31,1,2025-01-01,2025-07-31,0
### vorherige-ausgaben.csv (optional) ### vorherige-ausgaben.csv (optional)
Historische Dienstzuteilungen für Jahres-Fairness. Format wie `ausgabe.csv` bzw. `ausgabe-gesamt.csv`. Historische Dienstzuteilungen für Jahres-Fairness. Format wie `ausgabe.csv`.
Hier kann die `ausgabe-gesamt.csv`, die bei der letzten Planung generiert wurde eingespielt werden.
## Ausgabedatei ## Ausgabedatei
### ausgabe.csv ### ausgabe.csv
Die neu zugeteilten Dienste.
```csv ```csv
Datum,Wochentag,Frühstücksdienst,Putznotdienst,Essensausgabenotdienst,Kochen,Elternabend Datum,Wochentag,Frühstücksdienst,Putznotdienst,Essensausgabenotdienst,Kochen,Elternabend
2026-01-06,Montag,Sarah & Tim,Leon,Erika,, 2026-01-06,Montag,Sarah & Tim,Leon,Erika,,
``` ```
### ausgabe-gesamt.csv
Wie `ausgabe.csv`, enthält aber neben den neu geplanten Diensten auch die historischen Dienste, die über `vorherige-ausgaben.csv` übergeben wurden. Die Datei `ausgabe-gesamt.csv` kann bei der nächsten Planung wieder als Eingabe `vorherige-ausgaben.csv` verwendet werden.
## Verwendung ## Verwendung
```bash ```bash

View File

@ -6,7 +6,7 @@ Visualisierung und Export der Ergebnisse
from datetime import date from datetime import date
from collections import defaultdict from collections import defaultdict
from typing import Dict, List, DefaultDict, Tuple from typing import Dict, List, DefaultDict
from datenmodell import ElterndienstplanerDaten, Dienst, Eltern, Zielverteilung from datenmodell import ElterndienstplanerDaten, Dienst, Eltern, Zielverteilung
from csv_io import AusgabeWriter from csv_io import AusgabeWriter
@ -20,8 +20,6 @@ class ElterndienstAusgabe:
# Zwischenergebnisse aus der Optimierung (über Observer-Pattern gesetzt) # Zwischenergebnisse aus der Optimierung (über Observer-Pattern gesetzt)
self.ziel_lokal: Zielverteilung = None self.ziel_lokal: Zielverteilung = None
self.ziel_global: Zielverteilung = None self.ziel_global: Zielverteilung = None
# Historische Dienste (kann über Observer gesetzt werden)
self.historische_dienste: List[Tuple[date, Eltern, Dienst]] = None
def setze_zielverteilungen( def setze_zielverteilungen(
self, self,
@ -32,17 +30,9 @@ class ElterndienstAusgabe:
self.ziel_lokal = ziel_lokal self.ziel_lokal = ziel_lokal
self.ziel_global = ziel_global self.ziel_global = ziel_global
def setze_historische_dienste(self, historische_dienste: List[Tuple[date, Eltern, Dienst]]) -> None:
"""Observer-Callback: Setzt historische Dienste für Ausgabe/Export"""
self.historische_dienste = historische_dienste
def schreibe_ausgabe_csv(self, datei: str, lösung: Dict[date, Dict[Dienst, List[Eltern]]]) -> None: def schreibe_ausgabe_csv(self, datei: str, lösung: Dict[date, Dict[Dienst, List[Eltern]]]) -> None:
"""Schreibt die Lösung in die ausgabe.csv""" """Schreibt die Lösung in die ausgabe.csv"""
AusgabeWriter.schreibe_ausgabe_csv(datei, lösung, self.daten.planungszeitraum, self.daten.dienste, False) AusgabeWriter.schreibe_ausgabe_csv(datei, lösung, self.daten.planungszeitraum, self.daten.dienste)
# Schreibe ergänzende Datei mit historischen Diensten (falls vorhanden).
hist_datei = datei.replace('.csv', '-gesamt.csv') if datei.endswith('.csv') else datei + '-gesamt.csv'
historische = self.historische_dienste if self.historische_dienste is not None else self.daten.historische_dienste
AusgabeWriter.schreibe_ausgabe_csv(hist_datei, lösung, self.daten.planungszeitraum, self.daten.dienste, True, historische)
def drucke_statistiken(self, lösung: Dict[date, Dict[Dienst, List[Eltern]]]) -> None: def drucke_statistiken(self, lösung: Dict[date, Dict[Dienst, List[Eltern]]]) -> None:
"""Druckt Statistiken zur Lösung""" """Druckt Statistiken zur Lösung"""
@ -118,30 +108,28 @@ class ElterndienstAusgabe:
positive_praef_tage = {tag for tag, präf in praeferenzen_dienst.items() if präf == 1} positive_praef_tage = {tag for tag, präf in praeferenzen_dienst.items() if präf == 1}
if positive_praef_tage: # Es gibt positive Präferenzen if positive_praef_tage: # Es gibt positive Präferenzen
# Prüfe ob ALLE zugeteilten Dienste an nicht-präferierten Tagen sind
for tag in zugeteilte_tage: for tag in zugeteilte_tage:
if tag not in positive_praef_tage: if tag not in positive_praef_tage:
# Dienst wurde an nicht-präferiertem Tag zugeteilt
verletzungen[eltern][dienst]['positiv_nicht_erfuellt'] += 1 verletzungen[eltern][dienst]['positiv_nicht_erfuellt'] += 1
# Tabelle ausgeben (verbesserte Spaltenformatierung) # Tabelle ausgeben
col_width = 14 # Breite pro Dienst-Spalte (sichtbar) print(f"\n{'Eltern':<20} ", end='')
name_col = 20
# Header
print(f"\n{'Eltern':<{name_col}}", end='')
for dienst in self.daten.dienste: for dienst in self.daten.dienste:
print(f"{dienst.kuerzel:^{col_width}}", end='') print(f"{dienst.kuerzel:>12}", end='')
print() print()
print(f"{'':{name_col}}", end='') print(f"{'':20} ", end='')
for _ in self.daten.dienste: for dienst in self.daten.dienste:
print(f"{'neg, pos':^{col_width}}", end='') print(f"{'neg, pos':>12}", end='')
print() print()
print("-" * (name_col + col_width * len(self.daten.dienste))) print("-" * (20 + 12 * len(self.daten.dienste)))
gesamt_negativ = defaultdict(int) gesamt_negativ = defaultdict(int)
gesamt_positiv = defaultdict(int) gesamt_positiv = defaultdict(int)
for eltern in sorted(self.daten.eltern): for eltern in sorted(self.daten.eltern):
print(f"{eltern:<{name_col}}", end='') print(f"{eltern:<20} ", end='')
for dienst in self.daten.dienste: for dienst in self.daten.dienste:
neg = verletzungen[eltern][dienst]['negativ'] neg = verletzungen[eltern][dienst]['negativ']
pos = verletzungen[eltern][dienst]['positiv_nicht_erfuellt'] pos = verletzungen[eltern][dienst]['positiv_nicht_erfuellt']
@ -149,28 +137,22 @@ class ElterndienstAusgabe:
gesamt_negativ[dienst] += neg gesamt_negativ[dienst] += neg
gesamt_positiv[dienst] += pos gesamt_positiv[dienst] += pos
# Inhalt vor Padding erstellen # Farbcodierung
cell = f"{neg:>3}, {pos:>3}"
cell_padded = cell.center(col_width)
# Farbcodierung (erst nach Padding anwenden)
farbe = "" farbe = ""
reset = "" reset = ""
if neg > 0 or pos > 0: if neg > 0 or pos > 0:
farbe = "\033[91m" if neg > 0 else "\033[93m" # Rot für negativ, Gelb für positiv farbe = "\033[91m" if neg > 0 else "\033[93m" # Rot für negativ, Gelb für positiv
reset = "\033[0m" reset = "\033[0m"
print(f"{farbe}{cell_padded}{reset}", end='') print(f"{farbe}{neg:>3}, {pos:>3}{reset:>6}", end='')
print() print()
# Summenzeile # Summenzeile
print("-" * (name_col + col_width * len(self.daten.dienste))) print("-" * (20 + 12 * len(self.daten.dienste)))
print(f"{'SUMME':<{name_col}}", end='') print(f"{'SUMME':<20} ", end='')
for dienst in self.daten.dienste: for dienst in self.daten.dienste:
neg = gesamt_negativ[dienst] neg = gesamt_negativ[dienst]
pos = gesamt_positiv[dienst] pos = gesamt_positiv[dienst]
cell = f"{neg:>3}, {pos:>3}"
cell_padded = cell.center(col_width)
farbe = "" farbe = ""
reset = "" reset = ""
@ -178,7 +160,7 @@ class ElterndienstAusgabe:
farbe = "\033[91m" if neg > 0 else "\033[93m" farbe = "\033[91m" if neg > 0 else "\033[93m"
reset = "\033[0m" reset = "\033[0m"
print(f"{farbe}{cell_padded}{reset}", end='') print(f"{farbe}{neg:>3}, {pos:>3}{reset:>6}", end='')
print() print()
print("\nLegende:") print("\nLegende:")
@ -267,99 +249,3 @@ class ElterndienstAusgabe:
print(f"Maximale Abweichung von Global-Ziel: {max_abw_global:.2f} Dienste") print(f"Maximale Abweichung von Global-Ziel: {max_abw_global:.2f} Dienste")
print(f"Maximale Abweichung von Lokal-Ziel: {max_abw_lokal:.2f} Dienste") print(f"Maximale Abweichung von Lokal-Ziel: {max_abw_lokal:.2f} Dienste")
print("\nLegende: Δ = Tatsächlich - Ziel (positiv = mehr als Ziel, negativ = weniger als Ziel)") print("\nLegende: Δ = Tatsächlich - Ziel (positiv = mehr als Ziel, negativ = weniger als Ziel)")
def visualisiere_dienste_uebersicht(
self,
lösung: Dict[date, Dict[Dienst, List[Eltern]]]
) -> None:
"""Visualisiert die Übersicht der zugeteilten Dienste nach Optimierung
Zeigt für jede Familie und jeden Diensttyp:
- Anzahl Dienste nach Optimierung (Historie + Planungszeitraum)
- Differenz zum globalen Ziel (Historie + Planungszeitraum)
Args:
lösung: Die Lösung der Optimierung
"""
if self.ziel_global is None:
print("FEHLER: Globale Zielverteilung wurde nicht gesetzt!")
return
# Berechne historische Dienste pro Eltern und Dienst
historisch = defaultdict(lambda: defaultdict(int))
for datum, eltern, dienst in self.daten.historische_dienste:
historisch[eltern][dienst] += 1
# Berechne geplante Dienste (aus Lösung)
geplant = defaultdict(lambda: defaultdict(int))
for tag_dienste in lösung.values():
for dienst, eltern_liste in tag_dienste.items():
for eltern in eltern_liste:
geplant[eltern][dienst] += 1
# Berechne globale Ziele für jeden Elternteil und Dienst
# Das globale Ziel ist: faire Verteilung über (Historie + Planungszeitraum) MINUS bereits geleistete Historie
# Also: ziel_global[eltern][dienst] ist die SOLL-Änderung im Planungszeitraum
# Tatsächliches Gesamt-Ziel = historisch[eltern][dienst] + ziel_global[eltern][dienst]
print("\n" + "="*120)
print("ÜBERSICHT: Dienst nach der Optimierung")
print("="*120)
# Tabelle: NACH der Optimierung (historisch + geplant)
print("\n>>> NACH OPTIMIERUNG (historische Dienste + Planungszeitraum) <<<\n")
# Header
print(f"{'Eltern':<20} ", end='')
for dienst in self.daten.dienste:
print(f"{dienst.kuerzel:>14} ", end='')
print(f"{'GESAMT':>14}")
print(f"{'':20} ", end='')
for dienst in self.daten.dienste:
print(f"{'Ist / Δ Ziel':>14} ", end='')
print(f"{'Ist / Δ Ziel':>14}")
print("-" * 120)
# Datenzeilen
for eltern in sorted(self.daten.eltern):
print(f"{eltern:<20} ", end='')
gesamt_ist = 0
gesamt_ziel = 0
for dienst in self.daten.dienste:
ist_dienste = historisch[eltern][dienst] + geplant[eltern][dienst]
gesamt_ist += ist_dienste
# Globales Ziel = historisch + ziel_global (das ist das faire Gesamt-Ziel)
ziel_gesamt = historisch[eltern][dienst] + self.ziel_global[eltern][dienst]
gesamt_ziel += ziel_gesamt
delta = ist_dienste - ziel_gesamt
# Farbcodierung
farbe = ""
reset = ""
if abs(delta) > 0.5:
farbe = "\033[93m" if abs(delta) <= 1.5 else "\033[91m"
reset = "\033[0m"
print(f"{farbe}{ist_dienste:>6} / {delta:>+5.1f}{reset} ", end='')
# Gesamt-Spalte
delta_gesamt = gesamt_ist - gesamt_ziel
farbe = ""
reset = ""
if abs(delta_gesamt) > 0.5:
farbe = "\033[93m" if abs(delta_gesamt) <= 1.5 else "\033[91m"
reset = "\033[0m"
print(f"{farbe}{gesamt_ist:>6} / {delta_gesamt:>+5.1f}{reset}")
print()
print("Legende:")
print(" Ist = Anzahl tatsächlich geleisteter Dienste")
print(" Δ Ziel = Differenz zum globalen fairen Ziel (positiv = mehr als fair, negativ = weniger)")
print(" \033[93mGelb\033[0m = Abweichung 0.5 - 1.5 Dienste")
print(" \033[91mRot\033[0m = Abweichung > 1.5 Dienste")

View File

@ -6,7 +6,7 @@ Trennt CSV-Parsing und -Schreiben von der Business-Logik
import csv import csv
from datetime import datetime, date, timedelta from datetime import datetime, date, timedelta
from typing import Dict, List, Tuple, DefaultDict, Optional from typing import Dict, List, Tuple, DefaultDict
from collections import defaultdict from collections import defaultdict
@ -213,9 +213,7 @@ class AusgabeWriter:
datei: str, datei: str,
lösung: Dict[date, Dict[any, List[str]]], # Dienst-Objekt als Key lösung: Dict[date, Dict[any, List[str]]], # Dienst-Objekt als Key
tage: List[date], tage: List[date],
dienste: List, # List[Dienst] dienste: List # List[Dienst]
gesamt: bool = False,
historische_dienste: List[Tuple[date, str, any]] = None
) -> None: ) -> None:
""" """
Schreibt die Lösung in die ausgabe.csv Schreibt die Lösung in die ausgabe.csv
@ -223,50 +221,10 @@ class AusgabeWriter:
Args: Args:
datei: Pfad zur ausgabe.csv datei: Pfad zur ausgabe.csv
lösung: Dictionary mit Zuteilungen {datum: {dienst: [eltern]}} lösung: Dictionary mit Zuteilungen {datum: {dienst: [eltern]}}
tage: Liste aller Planungstage (aktueller Planungszeitraum) tage: Liste aller Planungstage
dienste: Liste der Dienst-Objekte dienste: Liste der Dienst-Objekte
gesamt: Wenn True -> schreibe gesamte Dienste (inkl. historische_dienste).
Wenn False -> wie bisher nur die neu verplanten Dienste (lösung).
historische_dienste: Optional Liste von (datum, eltern, dienst) aus vorherige-ausgaben.csv
(wird nur ausgewertet, wenn gesamt==True)
""" """
print(f"Schreibe Ergebnisse nach {datei}... (gesamt={gesamt})") print(f"Schreibe Ergebnisse nach {datei}...")
# Bestimme alle zu schreibenden Tage
if gesamt and historische_dienste:
historische_dates = {hd[0] for hd in historische_dienste}
output_dates = sorted(set(tage) | historische_dates)
else:
output_dates = sorted(tage)
# Sicherstellen: Für alle Tage im Zeitraum (von min bis max) soll eine Zeile ausgegeben werden,
# auch wenn keine Informationen vorliegen.
if output_dates:
start_date = min(output_dates)
end_date = max(output_dates)
full_dates = []
current = start_date
while current <= end_date:
full_dates.append(current)
current += timedelta(days=1)
output_dates = full_dates
# Erstelle Mapping date -> dienst -> list[eltern]
combined: Dict[date, Dict[any, List[str]]] = {}
if gesamt and historische_dienste:
for datum, eltern_name, dienst in historische_dienste:
combined.setdefault(datum, {}).setdefault(dienst, [])
if eltern_name not in combined[datum][dienst]:
combined[datum][dienst].append(eltern_name)
# Füge neue (optimierte) Zuweisungen hinzu (überschreiben/ergänzen)
for datum, dienst_map in (lösung or {}).items():
combined.setdefault(datum, {})
for dienst, eltern_liste in dienst_map.items():
combined[datum].setdefault(dienst, [])
for eltern_name in eltern_liste:
if eltern_name not in combined[datum][dienst]:
combined[datum][dienst].append(eltern_name)
with open(datei, 'w', newline='', encoding='utf-8') as f: with open(datei, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f) writer = csv.writer(f)
@ -276,16 +234,17 @@ class AusgabeWriter:
writer.writerow(header) writer.writerow(header)
# Daten schreiben # Daten schreiben
for tag in output_dates: for tag in sorted(tage):
wochentag = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', wochentag = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag',
'Freitag', 'Samstag', 'Sonntag'][tag.weekday()] 'Freitag', 'Samstag', 'Sonntag'][tag.weekday()]
row = [tag.strftime('%Y-%m-%d'), wochentag] row = [tag.strftime('%Y-%m-%d'), wochentag]
for dienst in dienste: for dienst in dienste:
if tag in lösung and dienst in lösung[tag]:
eltern_str = ' und '.join(lösung[tag][dienst])
else:
eltern_str = '' eltern_str = ''
if tag in combined and dienst in combined[tag]:
eltern_str = ' und '.join(combined[tag][dienst])
row.append(eltern_str) row.append(eltern_str)
writer.writerow(row) writer.writerow(row)

View File

@ -15,17 +15,16 @@ from csv_io import EingabeParser
class Dienst: class Dienst:
"""Repräsentiert einen Diensttyp mit allen seinen Eigenschaften""" """Repräsentiert einen Diensttyp mit allen seinen Eigenschaften"""
def __init__(self, kuerzel: str, name: str, personen_anzahl: int = 1, aufwand: int = 1) -> None: def __init__(self, kuerzel: str, name: str, personen_anzahl: int = 1) -> None:
self.kuerzel: str = kuerzel self.kuerzel: str = kuerzel
self.name: str = name self.name: str = name
self.personen_anzahl: int = personen_anzahl self.personen_anzahl: int = personen_anzahl
self.aufwand: int = aufwand
def __str__(self) -> str: def __str__(self) -> str:
return f"{self.kuerzel} ({self.name}): {self.personen_anzahl} Person(en), Aufwand={self.aufwand}" return f"{self.kuerzel} ({self.name}): {self.personen_anzahl} Person(en)"
def __repr__(self) -> str: def __repr__(self) -> str:
return f"Dienst('{self.kuerzel}', '{self.name}', {self.personen_anzahl}, {self.aufwand})" return f"Dienst('{self.kuerzel}', '{self.name}', {self.personen_anzahl})"
def braucht_mehrere_personen(self) -> bool: def braucht_mehrere_personen(self) -> bool:
"""Gibt True zurück, wenn mehr als eine Person benötigt wird""" """Gibt True zurück, wenn mehr als eine Person benötigt wird"""
@ -50,11 +49,11 @@ class ElterndienstplanerDaten:
def __init__(self) -> None: def __init__(self) -> None:
# Dienste als Liste definieren # Dienste als Liste definieren
self.dienste: List[Dienst] = [ self.dienste: List[Dienst] = [
Dienst('F', 'Frühstücksdienst', 1, aufwand=3), Dienst('F', 'Frühstücksdienst', 1),
Dienst('P', 'Putznotdienst', 1, aufwand=1), Dienst('P', 'Putznotdienst', 1),
Dienst('E', 'Essensausgabenotdienst', 1, aufwand=1), Dienst('E', 'Essensausgabenotdienst', 1),
Dienst('K', 'Kochen', 1, aufwand=3), Dienst('K', 'Kochen', 1),
Dienst('A', 'Elternabend', 2, aufwand=2) Dienst('A', 'Elternabend', 2)
] ]
# Datenstrukturen # Datenstrukturen
@ -103,17 +102,12 @@ class ElterndienstplanerDaten:
vorherige_datei: Optionaler Pfad zur vorherige-ausgaben.csv für Fairness-Constraints vorherige_datei: Optionaler Pfad zur vorherige-ausgaben.csv für Fairness-Constraints
""" """
# Eingabe CSV: Termine, Präferenzen, Verfügbarkeit # Eingabe CSV: Termine, Präferenzen, Verfügbarkeit
# Eltern CSV: Dienstfaktoren (erst einlesen, damit self.eltern daraus abgeleitet wird) self.eltern, self.planungszeitraum, self.benoetigte_dienste, self.verfuegbarkeit, self.praeferenzen = \
self.dienstfaktoren = EingabeParser.parse_eltern_csv(eltern_datei)
# Fülle self.eltern aus den Einträgen in eltern.csv (Vertrauensquelle für Elternnamen)
self.eltern = list(self.dienstfaktoren.keys())
# Eingabe CSV: Termine, Präferenzen, Verfügbarkeit
# Wir verwenden die Elterndefinition aus eltern.csv; die von parse_eingabe_csv
# zurückgegebene Eltern-Liste wird ignoriert, damit die Quell-of-truth konsistent bleibt.
_, self.planungszeitraum, self.benoetigte_dienste, self.verfuegbarkeit, self.praeferenzen = \
EingabeParser.parse_eingabe_csv(eingabe_datei, self.get_dienst) EingabeParser.parse_eingabe_csv(eingabe_datei, self.get_dienst)
# Eltern CSV: Dienstfaktoren
self.dienstfaktoren = EingabeParser.parse_eltern_csv(eltern_datei)
# Vorherige Ausgaben CSV (optional): Historische Dienste für Fairness # Vorherige Ausgaben CSV (optional): Historische Dienste für Fairness
if vorherige_datei: if vorherige_datei:
self.historische_dienste = \ self.historische_dienste = \

View File

@ -8,7 +8,6 @@ Datum: Dezember 2025
import sys import sys
import pulp import pulp
import multiprocessing
from datetime import timedelta, date from datetime import timedelta, date
from collections import defaultdict from collections import defaultdict
from typing import Dict, List, Tuple, DefaultDict, Optional from typing import Dict, List, Tuple, DefaultDict, Optional
@ -69,9 +68,9 @@ class Elterndienstplaner:
faire_zuteilung = anteil * anzahl_dienste faire_zuteilung = anteil * anzahl_dienste
ziel_dienste[eltern][dienst] += faire_zuteilung ziel_dienste[eltern][dienst] += faire_zuteilung
#if faire_zuteilung > 0.01: if faire_zuteilung > 0.01:
# print(f" {tag}: {eltern} Faktor={self.daten.dienstfaktoren[eltern][tag]} " print(f" {tag}: {eltern} Faktor={self.daten.dienstfaktoren[eltern][tag]} "
# f"-> {faire_zuteilung:.2f} von {anzahl_dienste} Diensten") f"-> {faire_zuteilung:.2f} von {anzahl_dienste} Diensten")
# 2. AKTUELLER PLANUNGSZEITRAUM: Faire Verteilung # 2. AKTUELLER PLANUNGSZEITRAUM: Faire Verteilung
benoetigte_dienste_planungszeitraum = 0 benoetigte_dienste_planungszeitraum = 0
@ -180,10 +179,6 @@ class Elterndienstplaner:
woche_nr = 0 woche_nr = 0
letzter_tag = self.daten.planungszeitraum[-1] letzter_tag = self.daten.planungszeitraum[-1]
print ("\n Erster Tag im Planungszeitraum:", erster_tag)
print ("\n Letzter Tag im Planungszeitraum:", letzter_tag)
while woche_start <= letzter_tag: while woche_start <= letzter_tag:
woche_ende = woche_start + timedelta(days=6) woche_ende = woche_start + timedelta(days=6)
@ -193,12 +188,11 @@ class Elterndienstplaner:
# Zaehle historische Dienste in dieser Woche (VOR Planungszeitraum) # Zaehle historische Dienste in dieser Woche (VOR Planungszeitraum)
historische_dienste_in_woche = 0 historische_dienste_in_woche = 0
#if woche_start < erster_tag: if woche_start < erster_tag:
for hist_datum, hist_eltern, hist_dienst in self.daten.historische_dienste: for hist_datum, hist_eltern, hist_dienst in self.daten.historische_dienste:
if (hist_eltern == eltern and if (hist_eltern == eltern and
hist_dienst == dienst and hist_dienst == dienst and
woche_start <= hist_datum and woche_start <= hist_datum < erster_tag):
hist_datum <= woche_ende):
historische_dienste_in_woche += 1 historische_dienste_in_woche += 1
for tag in self.daten.planungszeitraum: for tag in self.daten.planungszeitraum:
@ -207,11 +201,8 @@ class Elterndienstplaner:
woche_vars.append(x[eltern, tag, dienst]) woche_vars.append(x[eltern, tag, dienst])
if woche_vars: if woche_vars:
if (1 - historische_dienste_in_woche) >= 0:
prob += pulp.lpSum(woche_vars) <= 1 - historische_dienste_in_woche, \ prob += pulp.lpSum(woche_vars) <= 1 - historische_dienste_in_woche, \
f"C1_{eltern.replace(' ', '_')}_{dienst.kuerzel}_w{woche_nr}" f"C1_{eltern.replace(' ', '_')}_{dienst.kuerzel}_w{woche_nr}"
else:
print (f" Hinweis: {eltern} hat in Woche {woche_nr} bereits {historische_dienste_in_woche} mal {dienst.name}")
woche_start += timedelta(days=7) woche_start += timedelta(days=7)
woche_nr += 1 woche_nr += 1
@ -225,18 +216,12 @@ class Elterndienstplaner:
for eltern in self.daten.eltern: for eltern in self.daten.eltern:
for tag in self.daten.planungszeitraum: for tag in self.daten.planungszeitraum:
tag_vars = [] tag_vars = []
maximum = 1
for dienst in self.daten.dienste: for dienst in self.daten.dienste:
if (eltern, tag, dienst) in x: if (eltern, tag, dienst) in x:
tag_vars.append(x[eltern, tag, dienst]) tag_vars.append(x[eltern, tag, dienst])
for hist_datum, hist_eltern, hist_dienst in self.daten.historische_dienste:
if (hist_eltern == eltern and
hist_datum == tag):
maximum = 0
if tag_vars: if tag_vars:
prob += pulp.lpSum(tag_vars) <= maximum, f"C2_{eltern.replace(' ', '_')}_{tag}" prob += pulp.lpSum(tag_vars) <= 1, f"C2_{eltern.replace(' ', '_')}_{tag}"
def _add_constraint_verfuegbarkeit( def _add_constraint_verfuegbarkeit(
self, self,
@ -356,16 +341,14 @@ class Elterndienstplaner:
lowBound=0) lowBound=0)
# Zähle tatsächliche Dienste gewichtet mit dem Aufwand des Dienstes
tatsaechliche_dienste_gesamt = pulp.lpSum( tatsaechliche_dienste_gesamt = pulp.lpSum(
dienst.aufwand * x[eltern, tag, dienst] x[eltern, tag, dienst]
for tag in self.daten.planungszeitraum for tag in self.daten.planungszeitraum
for dienst in self.daten.dienste for dienst in self.daten.dienste
if (eltern, tag, dienst) in x if (eltern, tag, dienst) in x
) )
# Zielgesamt ebenfalls mit Dienst-Aufwand gewichtet ziel_gesamt = sum(ziel_dienste[eltern][dienst] for dienst in self.daten.dienste)
ziel_gesamt = sum(ziel_dienste[eltern][dienst] * dienst.aufwand for dienst in self.daten.dienste)
prob += (tatsaechliche_dienste_gesamt - ziel_gesamt <= prob += (tatsaechliche_dienste_gesamt - ziel_gesamt <=
fairness_abweichung_gesamt[eltern]) fairness_abweichung_gesamt[eltern])
@ -387,8 +370,8 @@ class Elterndienstplaner:
"""Erstellt die Zielfunktion mit Fairness und Praeferenzen""" """Erstellt die Zielfunktion mit Fairness und Praeferenzen"""
objective_terms = [] objective_terms = []
gewicht_global = 50 gewicht_global = 40
gewicht_lokal = 50 gewicht_lokal = 60
gewicht_f1 = gewicht_global gewicht_f1 = gewicht_global
gewicht_f2 = gewicht_lokal gewicht_f2 = gewicht_lokal
gewicht_f3_global = 0.25 * gewicht_global gewicht_f3_global = 0.25 * gewicht_global
@ -396,23 +379,21 @@ class Elterndienstplaner:
for eltern in self.daten.eltern: for eltern in self.daten.eltern:
for dienst in self.daten.dienste: for dienst in self.daten.dienste:
# Skaliere diensttyp-spezifische Fairness mit dem Aufwand des Dienstes objective_terms.append(gewicht_f1 * fairness_abweichung_global[eltern, dienst])
objective_terms.append(gewicht_f1 * fairness_abweichung_global[eltern, dienst] * dienst.aufwand) objective_terms.append(gewicht_f2 * fairness_abweichung_lokal[eltern, dienst])
objective_terms.append(gewicht_f2 * fairness_abweichung_lokal[eltern, dienst] * dienst.aufwand)
# Gesamt-Fairness (bereits dienstabhängig in den Constraints) — keine zusätzliche Mean-Skalierung mehr
objective_terms.append(gewicht_f3_global * fairness_abweichung_gesamt_global[eltern]) objective_terms.append(gewicht_f3_global * fairness_abweichung_gesamt_global[eltern])
objective_terms.append(gewicht_f4_lokal * fairness_abweichung_gesamt_lokal[eltern]) objective_terms.append(gewicht_f4_lokal * fairness_abweichung_gesamt_lokal[eltern])
# P1: Bevorzugte Dienste (stärker für aufwändigere Dienste) # P1: Bevorzugte Dienste
for (eltern, tag, dienst), praef in self.daten.praeferenzen.items(): for (eltern, tag, dienst), praef in self.daten.praeferenzen.items():
if (eltern, tag, dienst) in x and praef == 1: if (eltern, tag, dienst) in x and praef == 1:
objective_terms.append(-10 * dienst.aufwand * x[eltern, tag, dienst]) objective_terms.append(-5 * x[eltern, tag, dienst])
# P2: Abgelehnte Dienste (stärker für aufwändigere Dienste) # P2: Abgelehnte Dienste
for (eltern, tag, dienst), praef in self.daten.praeferenzen.items(): for (eltern, tag, dienst), praef in self.daten.praeferenzen.items():
if (eltern, tag, dienst) in x and praef == -1: if (eltern, tag, dienst) in x and praef == -1:
objective_terms.append(20 * dienst.aufwand * x[eltern, tag, dienst]) objective_terms.append(25 * x[eltern, tag, dienst])
if objective_terms: if objective_terms:
prob += pulp.lpSum(objective_terms) prob += pulp.lpSum(objective_terms)
@ -485,15 +466,13 @@ class Elterndienstplaner:
solver = None solver = None
try: try:
cpu_count = multiprocessing.cpu_count() print("Versuche CBC Solver...")
threads = max(1, cpu_count - 1) solver = pulp.PULP_CBC_CMD(msg=0, timeLimit=10)
print(f"Versuche CBC Solver mit {threads} Threads...") except:
solver = pulp.PULP_CBC_CMD(msg=0, timeLimit=20, threads=threads)
except Exception:
try: try:
print("Versuche GLPK Solver...") print("Versuche GLPK Solver...")
solver = pulp.GLPK_CMD(msg=0) solver = pulp.GLPK_CMD(msg=0)
except Exception: except:
print("Kein spezifizierter Solver verfügbar, verwende Standard.") print("Kein spezifizierter Solver verfügbar, verwende Standard.")
solver = None solver = None
@ -544,7 +523,6 @@ def main() -> None:
if loesung is not None: if loesung is not None:
ausgabe.schreibe_ausgabe_csv(ausgabe_datei, loesung) ausgabe.schreibe_ausgabe_csv(ausgabe_datei, loesung)
ausgabe.drucke_statistiken(loesung) ausgabe.drucke_statistiken(loesung)
ausgabe.visualisiere_dienste_uebersicht(loesung)
ausgabe.visualisiere_verteilungen(loesung) ausgabe.visualisiere_verteilungen(loesung)
ausgabe.visualisiere_praeferenz_verletzungen(loesung) ausgabe.visualisiere_praeferenz_verletzungen(loesung)