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>
This commit is contained in:
commit
b7af2f1d9a
468 changed files with 262141 additions and 0 deletions
170
bilder_04/erzeuge_kap06_gantt.py
Normal file
170
bilder_04/erzeuge_kap06_gantt.py
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
#!/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)
|
||||
Loading…
Reference in a new issue