operations_research/bilder_04/erzeuge_kap06_gantt.py

170 lines
6.6 KiB
Python
Raw Normal View History

Version 04 als eigenes Repository Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python" (Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch Version_03 (eingefroren) verwaltet und Version_04/ ignoriert. Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist. Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise, 325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren, 25 Notebooks, PDF mit 715 Seiten. Zusaetzlich in diesem Commit: * pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api, figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt unveraendert daneben bestehen. ortools steht in der Grundausstattung, highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der schlanken Installation gar nicht erst auftreten. * Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo steht in requirements.txt, wird aber von keinem Programm importiert, sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in [empfehlungen]). * NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen. * PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den tatsaechlichen aus erzeuge_titelseite.py korrigiert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 01:20:09 +02:00
#!/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/kap06_jobshop_gantt.png statisch, Rasterfassung
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
# 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", "png"):
pfad = os.path.join(BILDER, f"kap06_jobshop_gantt.{endung}")
figur.savefig(pfad, format=endung, dpi=160)
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"<b>Auftrag {auftrag}</b><br>"
f"{MASCHINENNAMEN[maschine]}<br>"
f"von {beginn} bis {beginn + dauer} "
f"({dauer} Zeiteinheiten)<extra></extra>"),
))
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)