#!/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)