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>
170 lines
6.6 KiB
Python
170 lines
6.6 KiB
Python
#!/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)
|