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 2195695..4cbfc60 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""" @@ -108,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'] @@ -137,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 = "" @@ -160,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:") @@ -249,3 +267,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/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..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 @@ -102,11 +103,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 107c518..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 @@ -68,9 +69,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 @@ -179,6 +180,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 +193,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 +207,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 +225,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, @@ -341,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]) @@ -379,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) @@ -466,13 +485,15 @@ class Elterndienstplaner: solver = None try: - print("Versuche CBC Solver...") - solver = pulp.PULP_CBC_CMD(msg=0, timeLimit=10) - 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 @@ -523,6 +544,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)