From b524edc2ba2760d9af8d5adecd2424172627bfd9 Mon Sep 17 00:00:00 2001 From: Jan Hoheisel Date: Sun, 25 Jan 2026 22:07:39 +0100 Subject: [PATCH 1/4] Tabellarische Debugausgabe Gibt die Anzahl zugeteilter Dienste (historisch + neu) nach Dienstart und Eltern aus, sowie abweichung zum Globalen Ziel --- ausgabe.py | 96 +++++++++++++++++++++++++++++++++++++++++++ elterndienstplaner.py | 9 ++-- 2 files changed, 101 insertions(+), 4 deletions(-) diff --git a/ausgabe.py b/ausgabe.py index 2195695..6bfae53 100644 --- a/ausgabe.py +++ b/ausgabe.py @@ -249,3 +249,99 @@ class ElterndienstAusgabe: 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("\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") diff --git a/elterndienstplaner.py b/elterndienstplaner.py index 107c518..ba09626 100755 --- a/elterndienstplaner.py +++ b/elterndienstplaner.py @@ -68,9 +68,9 @@ class Elterndienstplaner: faire_zuteilung = anteil * anzahl_dienste ziel_dienste[eltern][dienst] += faire_zuteilung - if faire_zuteilung > 0.01: - print(f" {tag}: {eltern} Faktor={self.daten.dienstfaktoren[eltern][tag]} " - f"-> {faire_zuteilung:.2f} von {anzahl_dienste} Diensten") + #if faire_zuteilung > 0.01: + # print(f" {tag}: {eltern} Faktor={self.daten.dienstfaktoren[eltern][tag]} " + # f"-> {faire_zuteilung:.2f} von {anzahl_dienste} Diensten") # 2. AKTUELLER PLANUNGSZEITRAUM: Faire Verteilung benoetigte_dienste_planungszeitraum = 0 @@ -467,7 +467,7 @@ class Elterndienstplaner: solver = None try: print("Versuche CBC Solver...") - solver = pulp.PULP_CBC_CMD(msg=0, timeLimit=10) + solver = pulp.PULP_CBC_CMD(msg=0, timeLimit=60) except: try: print("Versuche GLPK Solver...") @@ -523,6 +523,7 @@ def main() -> None: if loesung is not None: ausgabe.schreibe_ausgabe_csv(ausgabe_datei, loesung) ausgabe.drucke_statistiken(loesung) + ausgabe.visualisiere_dienste_uebersicht(loesung) ausgabe.visualisiere_verteilungen(loesung) ausgabe.visualisiere_praeferenz_verletzungen(loesung) From 08e5cf11bddbe14ec94cff3ffe10cd6ef145deda Mon Sep 17 00:00:00 2001 From: Jan Hoheisel Date: Wed, 28 Jan 2026 20:30:14 +0100 Subject: [PATCH 2/4] Mehr Fuktionen fuer historische Dienste Planungszeitraum und historischer Zeitraum koennen sich jetzt ueberlappen. So lassen sich einzelne Dienste (z.B. von einer Familie) nachtraeglich neu planen. historische Dienste werden bei den Constraints 1 Dienst pro Tag und 1 Dienst pro Woche korrekt beruecksichtigt elterndienstplaner.py erzeugt jetzt ausgaben-gesamt.csv, die fuer spaetere Aufrufe als Eingabe vorherige-dienste.csv verwendet werden kann. --- README.md | 8 +++++- ausgabe.py | 14 ++++++++-- csv_io.py | 59 ++++++++++++++++++++++++++++++++++++------- datenmodell.py | 13 +++++++--- elterndienstplaner.py | 32 ++++++++++++++++------- 5 files changed, 101 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 60da198..3af155d 100644 --- a/README.md +++ b/README.md @@ -54,17 +54,23 @@ Leon,2024-09-01,2024-12-31,1,2025-01-01,2025-07-31,0 ### vorherige-ausgaben.csv (optional) -Historische Dienstzuteilungen für Jahres-Fairness. Format wie `ausgabe.csv`. +Historische Dienstzuteilungen für Jahres-Fairness. Format wie `ausgabe.csv` bzw. `ausgabe-gesamt.csv`. +Hier kann die `ausgabe-gesamt.csv`, die bei der letzten Planung generiert wurde eingespielt werden. ## Ausgabedatei ### ausgabe.csv +Die neu zugeteilten Dienste. ```csv Datum,Wochentag,Frühstücksdienst,Putznotdienst,Essensausgabenotdienst,Kochen,Elternabend 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 ```bash diff --git a/ausgabe.py b/ausgabe.py index 6bfae53..35833db 100644 --- a/ausgabe.py +++ b/ausgabe.py @@ -6,7 +6,7 @@ Visualisierung und Export der Ergebnisse from datetime import date from collections import defaultdict -from typing import Dict, List, DefaultDict +from typing import Dict, List, DefaultDict, Tuple from datenmodell import ElterndienstplanerDaten, Dienst, Eltern, Zielverteilung from csv_io import AusgabeWriter @@ -20,6 +20,8 @@ class ElterndienstAusgabe: # Zwischenergebnisse aus der Optimierung (über Observer-Pattern gesetzt) self.ziel_lokal: 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( self, @@ -30,9 +32,17 @@ class ElterndienstAusgabe: self.ziel_lokal = ziel_lokal 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: """Schreibt die Lösung in die ausgabe.csv""" - AusgabeWriter.schreibe_ausgabe_csv(datei, lösung, self.daten.planungszeitraum, self.daten.dienste) + AusgabeWriter.schreibe_ausgabe_csv(datei, lösung, self.daten.planungszeitraum, self.daten.dienste, False) + # 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: """Druckt Statistiken zur Lösung""" diff --git a/csv_io.py b/csv_io.py index 8052e6b..7656c1b 100644 --- a/csv_io.py +++ b/csv_io.py @@ -6,7 +6,7 @@ Trennt CSV-Parsing und -Schreiben von der Business-Logik import csv from datetime import datetime, date, timedelta -from typing import Dict, List, Tuple, DefaultDict +from typing import Dict, List, Tuple, DefaultDict, Optional from collections import defaultdict @@ -213,7 +213,9 @@ class AusgabeWriter: datei: str, lösung: Dict[date, Dict[any, List[str]]], # Dienst-Objekt als Key tage: List[date], - dienste: List # List[Dienst] + dienste: List, # List[Dienst] + gesamt: bool = False, + historische_dienste: List[Tuple[date, str, any]] = None ) -> None: """ Schreibt die Lösung in die ausgabe.csv @@ -221,10 +223,50 @@ class AusgabeWriter: Args: datei: Pfad zur ausgabe.csv lösung: Dictionary mit Zuteilungen {datum: {dienst: [eltern]}} - tage: Liste aller Planungstage + tage: Liste aller Planungstage (aktueller Planungszeitraum) 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}...") + print(f"Schreibe Ergebnisse nach {datei}... (gesamt={gesamt})") + + # 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: writer = csv.writer(f) @@ -234,17 +276,16 @@ class AusgabeWriter: writer.writerow(header) # Daten schreiben - for tag in sorted(tage): + for tag in output_dates: wochentag = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'][tag.weekday()] row = [tag.strftime('%Y-%m-%d'), wochentag] 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) writer.writerow(row) diff --git a/datenmodell.py b/datenmodell.py index 75b5f6e..a01003e 100644 --- a/datenmodell.py +++ b/datenmodell.py @@ -102,11 +102,16 @@ class ElterndienstplanerDaten: vorherige_datei: Optionaler Pfad zur vorherige-ausgaben.csv für Fairness-Constraints """ # Eingabe CSV: Termine, Präferenzen, Verfügbarkeit - self.eltern, self.planungszeitraum, self.benoetigte_dienste, self.verfuegbarkeit, self.praeferenzen = \ - EingabeParser.parse_eingabe_csv(eingabe_datei, self.get_dienst) - - # Eltern CSV: Dienstfaktoren + # Eltern CSV: Dienstfaktoren (erst einlesen, damit self.eltern daraus abgeleitet wird) 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) # Vorherige Ausgaben CSV (optional): Historische Dienste für Fairness if vorherige_datei: diff --git a/elterndienstplaner.py b/elterndienstplaner.py index ba09626..5539d59 100755 --- a/elterndienstplaner.py +++ b/elterndienstplaner.py @@ -179,6 +179,10 @@ class Elterndienstplaner: woche_nr = 0 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: woche_ende = woche_start + timedelta(days=6) @@ -188,12 +192,13 @@ class Elterndienstplaner: # Zaehle historische Dienste in dieser Woche (VOR Planungszeitraum) historische_dienste_in_woche = 0 - if woche_start < erster_tag: - for hist_datum, hist_eltern, hist_dienst in self.daten.historische_dienste: - if (hist_eltern == eltern and - hist_dienst == dienst and - woche_start <= hist_datum < erster_tag): - historische_dienste_in_woche += 1 + #if woche_start < erster_tag: + for hist_datum, hist_eltern, hist_dienst in self.daten.historische_dienste: + if (hist_eltern == eltern and + hist_dienst == dienst and + woche_start <= hist_datum and + hist_datum <= woche_ende): + historische_dienste_in_woche += 1 for tag in self.daten.planungszeitraum: if woche_start <= tag <= woche_ende: @@ -201,8 +206,11 @@ class Elterndienstplaner: woche_vars.append(x[eltern, tag, dienst]) if woche_vars: - prob += pulp.lpSum(woche_vars) <= 1 - historische_dienste_in_woche, \ - f"C1_{eltern.replace(' ', '_')}_{dienst.kuerzel}_w{woche_nr}" + if (1 - historische_dienste_in_woche) >= 0: + prob += pulp.lpSum(woche_vars) <= 1 - historische_dienste_in_woche, \ + 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_nr += 1 @@ -216,12 +224,18 @@ class Elterndienstplaner: for eltern in self.daten.eltern: for tag in self.daten.planungszeitraum: tag_vars = [] + maximum = 1 for dienst in self.daten.dienste: if (eltern, tag, dienst) in x: 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: - prob += pulp.lpSum(tag_vars) <= 1, f"C2_{eltern.replace(' ', '_')}_{tag}" + prob += pulp.lpSum(tag_vars) <= maximum, f"C2_{eltern.replace(' ', '_')}_{tag}" def _add_constraint_verfuegbarkeit( self, From 9f7d3c6d4a6b168f585f8e317ed940ca234f46db Mon Sep 17 00:00:00 2001 From: Jan Hoheisel Date: Sun, 1 Feb 2026 20:33:11 +0100 Subject: [PATCH 3/4] Formatierung Ausgabe --- ausgabe.py | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/ausgabe.py b/ausgabe.py index 35833db..4cbfc60 100644 --- a/ausgabe.py +++ b/ausgabe.py @@ -118,28 +118,30 @@ class ElterndienstAusgabe: 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 - # Prüfe ob ALLE zugeteilten Dienste an nicht-präferierten Tagen sind for tag in zugeteilte_tage: if tag not in positive_praef_tage: - # Dienst wurde an nicht-präferiertem Tag zugeteilt verletzungen[eltern][dienst]['positiv_nicht_erfuellt'] += 1 - # Tabelle ausgeben - print(f"\n{'Eltern':<20} ", end='') + # Tabelle ausgeben (verbesserte Spaltenformatierung) + col_width = 14 # Breite pro Dienst-Spalte (sichtbar) + name_col = 20 + + # Header + print(f"\n{'Eltern':<{name_col}}", end='') for dienst in self.daten.dienste: - print(f"{dienst.kuerzel:>12}", end='') + print(f"{dienst.kuerzel:^{col_width}}", end='') print() - print(f"{'':20} ", end='') - for dienst in self.daten.dienste: - print(f"{'neg, pos':>12}", end='') + print(f"{'':{name_col}}", end='') + for _ in self.daten.dienste: + print(f"{'neg, pos':^{col_width}}", end='') print() - print("-" * (20 + 12 * len(self.daten.dienste))) + print("-" * (name_col + col_width * len(self.daten.dienste))) gesamt_negativ = defaultdict(int) gesamt_positiv = defaultdict(int) for eltern in sorted(self.daten.eltern): - print(f"{eltern:<20} ", end='') + print(f"{eltern:<{name_col}}", end='') for dienst in self.daten.dienste: neg = verletzungen[eltern][dienst]['negativ'] pos = verletzungen[eltern][dienst]['positiv_nicht_erfuellt'] @@ -147,22 +149,28 @@ class ElterndienstAusgabe: gesamt_negativ[dienst] += neg gesamt_positiv[dienst] += pos - # Farbcodierung + # Inhalt vor Padding erstellen + cell = f"{neg:>3}, {pos:>3}" + cell_padded = cell.center(col_width) + + # Farbcodierung (erst nach Padding anwenden) farbe = "" reset = "" if neg > 0 or pos > 0: farbe = "\033[91m" if neg > 0 else "\033[93m" # Rot für negativ, Gelb für positiv reset = "\033[0m" - print(f"{farbe}{neg:>3}, {pos:>3}{reset:>6}", end='') + print(f"{farbe}{cell_padded}{reset}", end='') print() # Summenzeile - print("-" * (20 + 12 * len(self.daten.dienste))) - print(f"{'SUMME':<20} ", end='') + print("-" * (name_col + col_width * len(self.daten.dienste))) + print(f"{'SUMME':<{name_col}}", end='') for dienst in self.daten.dienste: neg = gesamt_negativ[dienst] pos = gesamt_positiv[dienst] + cell = f"{neg:>3}, {pos:>3}" + cell_padded = cell.center(col_width) farbe = "" reset = "" @@ -170,7 +178,7 @@ class ElterndienstAusgabe: farbe = "\033[91m" if neg > 0 else "\033[93m" reset = "\033[0m" - print(f"{farbe}{neg:>3}, {pos:>3}{reset:>6}", end='') + print(f"{farbe}{cell_padded}{reset}", end='') print() print("\nLegende:") From f7b1267c989661ec797b527a0d7a1cca6f5d223e Mon Sep 17 00:00:00 2001 From: Jan Hoheisel Date: Sun, 1 Feb 2026 21:48:03 +0100 Subject: [PATCH 4/4] Dienstaufwand und paralleles Rechnen --- datenmodell.py | 17 +++++++++-------- elterndienstplaner.py | 31 +++++++++++++++++++------------ 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/datenmodell.py b/datenmodell.py index a01003e..30a7d3c 100644 --- a/datenmodell.py +++ b/datenmodell.py @@ -15,16 +15,17 @@ from csv_io import EingabeParser class Dienst: """Repräsentiert einen Diensttyp mit allen seinen Eigenschaften""" - def __init__(self, kuerzel: str, name: str, personen_anzahl: int = 1) -> None: + def __init__(self, kuerzel: str, name: str, personen_anzahl: int = 1, aufwand: int = 1) -> None: self.kuerzel: str = kuerzel self.name: str = name self.personen_anzahl: int = personen_anzahl + self.aufwand: int = aufwand def __str__(self) -> str: - return f"{self.kuerzel} ({self.name}): {self.personen_anzahl} Person(en)" + return f"{self.kuerzel} ({self.name}): {self.personen_anzahl} Person(en), Aufwand={self.aufwand}" def __repr__(self) -> str: - return f"Dienst('{self.kuerzel}', '{self.name}', {self.personen_anzahl})" + return f"Dienst('{self.kuerzel}', '{self.name}', {self.personen_anzahl}, {self.aufwand})" def braucht_mehrere_personen(self) -> bool: """Gibt True zurück, wenn mehr als eine Person benötigt wird""" @@ -49,11 +50,11 @@ class ElterndienstplanerDaten: def __init__(self) -> None: # Dienste als Liste definieren self.dienste: List[Dienst] = [ - Dienst('F', 'Frühstücksdienst', 1), - Dienst('P', 'Putznotdienst', 1), - Dienst('E', 'Essensausgabenotdienst', 1), - Dienst('K', 'Kochen', 1), - Dienst('A', 'Elternabend', 2) + Dienst('F', 'Frühstücksdienst', 1, aufwand=3), + Dienst('P', 'Putznotdienst', 1, aufwand=1), + Dienst('E', 'Essensausgabenotdienst', 1, aufwand=1), + Dienst('K', 'Kochen', 1, aufwand=3), + Dienst('A', 'Elternabend', 2, aufwand=2) ] # Datenstrukturen diff --git a/elterndienstplaner.py b/elterndienstplaner.py index 5539d59..0d5ae38 100755 --- a/elterndienstplaner.py +++ b/elterndienstplaner.py @@ -8,6 +8,7 @@ Datum: Dezember 2025 import sys import pulp +import multiprocessing from datetime import timedelta, date from collections import defaultdict from typing import Dict, List, Tuple, DefaultDict, Optional @@ -355,14 +356,16 @@ class Elterndienstplaner: lowBound=0) + # Zähle tatsächliche Dienste gewichtet mit dem Aufwand des Dienstes tatsaechliche_dienste_gesamt = pulp.lpSum( - x[eltern, tag, dienst] + dienst.aufwand * x[eltern, tag, dienst] for tag in self.daten.planungszeitraum for dienst in self.daten.dienste if (eltern, tag, dienst) in x ) - ziel_gesamt = sum(ziel_dienste[eltern][dienst] for dienst in self.daten.dienste) + # Zielgesamt ebenfalls mit Dienst-Aufwand gewichtet + ziel_gesamt = sum(ziel_dienste[eltern][dienst] * dienst.aufwand for dienst in self.daten.dienste) prob += (tatsaechliche_dienste_gesamt - ziel_gesamt <= fairness_abweichung_gesamt[eltern]) @@ -393,21 +396,23 @@ class Elterndienstplaner: for eltern in self.daten.eltern: for dienst in self.daten.dienste: - objective_terms.append(gewicht_f1 * fairness_abweichung_global[eltern, dienst]) - objective_terms.append(gewicht_f2 * fairness_abweichung_lokal[eltern, dienst]) + # Skaliere diensttyp-spezifische Fairness mit dem Aufwand des Dienstes + objective_terms.append(gewicht_f1 * fairness_abweichung_global[eltern, dienst] * dienst.aufwand) + 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_f4_lokal * fairness_abweichung_gesamt_lokal[eltern]) - # P1: Bevorzugte Dienste + # P1: Bevorzugte Dienste (stärker für aufwändigere Dienste) for (eltern, tag, dienst), praef in self.daten.praeferenzen.items(): if (eltern, tag, dienst) in x and praef == 1: - objective_terms.append(-5 * x[eltern, tag, dienst]) + objective_terms.append(-10 * dienst.aufwand * x[eltern, tag, dienst]) - # P2: Abgelehnte Dienste + # P2: Abgelehnte Dienste (stärker für aufwändigere Dienste) for (eltern, tag, dienst), praef in self.daten.praeferenzen.items(): if (eltern, tag, dienst) in x and praef == -1: - objective_terms.append(25 * x[eltern, tag, dienst]) + objective_terms.append(20 * dienst.aufwand * x[eltern, tag, dienst]) if objective_terms: prob += pulp.lpSum(objective_terms) @@ -480,13 +485,15 @@ class Elterndienstplaner: solver = None try: - print("Versuche CBC Solver...") - solver = pulp.PULP_CBC_CMD(msg=0, timeLimit=60) - except: + cpu_count = multiprocessing.cpu_count() + threads = max(1, cpu_count - 1) + print(f"Versuche CBC Solver mit {threads} Threads...") + solver = pulp.PULP_CBC_CMD(msg=0, timeLimit=20, threads=threads) + except Exception: try: print("Versuche GLPK Solver...") solver = pulp.GLPK_CMD(msg=0) - except: + except Exception: print("Kein spezifizierter Solver verfügbar, verwende Standard.") solver = None