#!/usr/bin/env python3 # erzeuge_kap06_gantt.py """ Erzeugt das Gantt-Diagramm zum Job-Shop-Beispiel aus Kapitel 6 in zwei Fassungen: bilder_04/kap06_jobshop_gantt.svg statisch, fuer das PDF bilder_04/plotly/kap06_jobshop_gantt.html interaktiv, fuer die Website Die Daten sind identisch mit JobShop_Intervalle.py; geloest wird hier erneut, damit Diagramm und Buchtext nicht auseinanderlaufen koennen. Aufruf (aus dem Repository-Wurzelverzeichnis): python3 bilder_04/erzeuge_kap06_gantt.py Benoetigt: ortools, matplotlib, plotly """ from __future__ import annotations import os import matplotlib matplotlib.use("Agg") # kein Fenster, auch auf Servern import matplotlib.pyplot as plt import plotly.graph_objects as go from ortools.sat.python import cp_model # Reproduzierbare SVG-Ausgabe (siehe erzeuge_titelseite.py). Dieser Generator # war als einziger ohne beides - sein SVG trug deshalb einen echten # Zeitstempel und bei jedem Lauf andere clip-path-IDs. plt.rcParams["svg.hashsalt"] = "or-mit-python-v04" # Identisch zu JobShop_Intervalle.py AUFTRAEGE = [ [(0, 3), (1, 2), (2, 2)], # Auftrag 0 [(0, 2), (2, 1), (1, 4)], # Auftrag 1 [(1, 4), (2, 3)], # Auftrag 2 ] MASCHINENNAMEN = ["Fraese", "Dreherei", "Lackiererei"] # Buchpalette (Indigo, Cyan, Amber) - dieselben Farben wie im uebrigen Werk FARBEN = ["#4338ca", "#0891b2", "#b45309"] BASIS = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BILDER = os.path.join(BASIS, "bilder_04") PLOTLY_VERZ = os.path.join(BILDER, "plotly") def loese() -> tuple[list[tuple[int, int, int, int]], int]: """Liefert (Auftrag, Maschine, Beginn, Dauer) je Arbeitsgang und den Makespan.""" anzahl_maschinen = 1 + max(m for auftrag in AUFTRAEGE for m, _ in auftrag) horizont = sum(dauer for auftrag in AUFTRAEGE for _, dauer in auftrag) modell = cp_model.CpModel() intervalle: dict[int, list] = {m: [] for m in range(anzahl_maschinen)} beginn_var: dict[tuple[int, int], object] = {} ende_var: dict[tuple[int, int], object] = {} for a, auftrag in enumerate(AUFTRAEGE): for g, (maschine, dauer) in enumerate(auftrag): beginn = modell.NewIntVar(0, horizont, f"b_{a}_{g}") ende = modell.NewIntVar(0, horizont, f"e_{a}_{g}") intervall = modell.NewIntervalVar(beginn, dauer, ende, f"i_{a}_{g}") intervalle[maschine].append(intervall) beginn_var[a, g] = beginn ende_var[a, g] = ende if g > 0: # Arbeitsgaenge in ihrer Reihenfolge modell.Add(beginn >= ende_var[a, g - 1]) for maschine in range(anzahl_maschinen): modell.AddNoOverlap(intervalle[maschine]) makespan = modell.NewIntVar(0, horizont, "makespan") modell.AddMaxEquality(makespan, [ende_var[a, len(auftrag) - 1] for a, auftrag in enumerate(AUFTRAEGE)]) modell.Minimize(makespan) loeser = cp_model.CpSolver() # Dieselben Parameter wie in JobShop_Intervalle.py - sonst zeigt das # Diagramm einen anderen (gleich guten) Plan als der Buchtext. loeser.parameters.num_workers = 1 loeser.parameters.random_seed = 1 status = loeser.Solve(modell) if status not in (cp_model.OPTIMAL, cp_model.FEASIBLE): raise RuntimeError(f"Kein Plan gefunden: {loeser.StatusName(status)}") plan = [(a, maschine, loeser.Value(beginn_var[a, g]), dauer) for a, auftrag in enumerate(AUFTRAEGE) for g, (maschine, dauer) in enumerate(auftrag)] return plan, loeser.Value(makespan) def zeichne_statisch(plan, makespan: int) -> None: """Matplotlib-Fassung fuer das PDF.""" figur, achse = plt.subplots(figsize=(9.0, 3.2)) for auftrag, maschine, beginn, dauer in plan: achse.barh(y=maschine, width=dauer, left=beginn, height=0.55, color=FARBEN[auftrag % len(FARBEN)], edgecolor="white", linewidth=1.2) achse.text(beginn + dauer / 2, maschine, f"A{auftrag}", ha="center", va="center", color="white", fontsize=10, fontweight="bold") achse.set_yticks(range(len(MASCHINENNAMEN))) achse.set_yticklabels(MASCHINENNAMEN) achse.invert_yaxis() achse.set_xlabel("Zeiteinheiten") achse.set_xlim(0, makespan) achse.set_xticks(range(makespan + 1)) achse.set_title(f"Job-Shop-Plan, Makespan = {makespan}", fontsize=11) achse.grid(axis="x", linestyle=":", alpha=0.5) for rand in ("top", "right", "left"): achse.spines[rand].set_visible(False) figur.tight_layout() os.makedirs(BILDER, exist_ok=True) for endung in ("svg",): pfad = os.path.join(BILDER, f"kap06_jobshop_gantt.{endung}") figur.savefig(pfad, format=endung, dpi=160, metadata={"Date": None} if endung == "svg" else None) print(f"geschrieben: {pfad}") plt.close(figur) def zeichne_interaktiv(plan, makespan: int) -> None: """Plotly-Fassung fuer die Website. include_plotlyjs=False: Die Bibliothek wird vom Build einmal je Seite eingebunden (assets/plotly.min.js), nicht in jede Figur kopiert. """ figur = go.Figure() schon_beschriftet = set() for auftrag, maschine, beginn, dauer in plan: figur.add_trace(go.Bar( x=[dauer], y=[MASCHINENNAMEN[maschine]], base=[beginn], orientation="h", name=f"Auftrag {auftrag}", legendgroup=f"A{auftrag}", showlegend=auftrag not in schon_beschriftet, marker_color=FARBEN[auftrag % len(FARBEN)], marker_line=dict(color="white", width=1.5), hovertemplate=(f"Auftrag {auftrag}
" f"{MASCHINENNAMEN[maschine]}
" f"von {beginn} bis {beginn + dauer} " f"({dauer} Zeiteinheiten)"), )) schon_beschriftet.add(auftrag) figur.update_layout( barmode="overlay", title=f"Job-Shop-Plan, Makespan = {makespan}", xaxis=dict(title="Zeiteinheiten", dtick=1, range=[0, makespan]), yaxis=dict(autorange="reversed"), height=320, margin=dict(l=90, r=20, t=50, b=45), # Transparenter Hintergrund, damit die Figur sich in helles wie # dunkles Seitendesign einfuegt. paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)", legend=dict(orientation="h", y=-0.25), ) os.makedirs(PLOTLY_VERZ, exist_ok=True) pfad = os.path.join(PLOTLY_VERZ, "kap06_jobshop_gantt.html") with open(pfad, "w", encoding="utf-8") as datei: datei.write(figur.to_html(full_html=False, include_plotlyjs=False, div_id="kap06-jobshop-gantt")) print(f"geschrieben: {pfad}") if __name__ == "__main__": plan, makespan = loese() print(f"Makespan: {makespan}") zeichne_statisch(plan, makespan) zeichne_interaktiv(plan, makespan)