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
191
bilder_04/erzeuge_vrp_touren.py
Normal file
191
bilder_04/erzeuge_vrp_touren.py
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# erzeuge_vrp_touren.py
|
||||
"""
|
||||
Erzeugt die Tourenkarte zum Graphen-Kapitel:
|
||||
|
||||
bilder_04/kap_graphen_vrp_touren.svg / .png
|
||||
|
||||
Das Vorgaengerbild war ein Schema: zwei Touren, vier Kunden, Zeitfenster in
|
||||
Uhrzeiten. Die Instanz des Kapitels hat 16 Kunden, vier Fahrzeuge und
|
||||
Zeitfenster in Minuten - das Bild zeigte also die Bauform, nicht den Fall.
|
||||
|
||||
Hier wird die Instanz aus VRP_Flotten_Routing.py geloest und die Loesung als
|
||||
Karte gezeichnet: Depot, Kunden (Punktgroesse = Bedarf), die vier Touren in
|
||||
vier Farben, und in der Legende je Fahrzeug Strecke und Auslastung.
|
||||
|
||||
ZUR REPRODUZIERBARKEIT: Das Buchprogramm gibt der Suche ein ZEITBUDGET von
|
||||
fuenf Sekunden. Ein Zeitbudget ist auf einer anderen Maschine ein anderes
|
||||
Rechenbudget, das Ergebnis also nicht garantiert identisch. Fuer das Bild wird
|
||||
deshalb zusaetzlich eine feste Loesungszahl als Abbruchkriterium gesetzt und
|
||||
die Gesamtstrecke ausgegeben - weicht sie vom abgedruckten Lauf ab, faellt es
|
||||
sofort auf, statt still ins Buch zu wandern.
|
||||
|
||||
Aufruf (aus dem Repository-Wurzelverzeichnis):
|
||||
python3 bilder_04/erzeuge_vrp_touren.py
|
||||
|
||||
Benoetigt: numpy, matplotlib, ortools
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
from ortools.constraint_solver import pywrapcp, routing_enums_pb2
|
||||
|
||||
# Reproduzierbare SVG-Ausgabe (siehe erzeuge_titelseite.py)
|
||||
plt.rcParams["svg.hashsalt"] = "or-mit-python-v04"
|
||||
|
||||
BASIS = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
BILDER = os.path.join(BASIS, "bilder_04")
|
||||
|
||||
TOURFARBEN = ["#4338ca", "#0891b2", "#b45309", "#059669"]
|
||||
|
||||
# --- Instanz: wortgleich zu VRP_Flotten_Routing.py --------------------------
|
||||
SERVICEZEIT = 10
|
||||
WARTEZEIT_MAX = 60
|
||||
SCHICHTLAENGE = 600
|
||||
SAAT = 42
|
||||
ZEITBUDGET_S = 5
|
||||
LOESUNGSGRENZE = 300 # zusaetzliches, maschinenunabhaengiges Kriterium
|
||||
|
||||
ZEITFENSTER = [
|
||||
(0, SCHICHTLAENGE),
|
||||
(30, 120), (60, 180), (100, 240), (150, 300),
|
||||
(60, 180), (120, 240), (200, 360), (300, 450),
|
||||
(180, 300), (240, 360), (300, 480), (360, 500),
|
||||
(60, 200), (120, 300), (240, 400), (300, 550),
|
||||
]
|
||||
BEDARFE = [0, 2, 3, 1, 4, 2, 2, 3, 1, 2, 4, 3, 2, 1, 2, 3, 2]
|
||||
KAPAZITAETEN = [10, 10, 10, 10]
|
||||
|
||||
|
||||
def erzeuge_daten():
|
||||
anzahl_orte = len(ZEITFENSTER)
|
||||
rng = np.random.default_rng(SAAT)
|
||||
koordinaten = rng.random((anzahl_orte, 2)) * 100
|
||||
distanz = np.zeros((anzahl_orte, anzahl_orte), dtype=int)
|
||||
for i in range(anzahl_orte):
|
||||
for j in range(anzahl_orte):
|
||||
distanz[i][j] = int(np.linalg.norm(koordinaten[i] - koordinaten[j]))
|
||||
return koordinaten, distanz
|
||||
|
||||
|
||||
def loese(distanz):
|
||||
"""CVRPTW mit denselben Einstellungen wie im Buchprogramm."""
|
||||
manager = pywrapcp.RoutingIndexManager(len(distanz), len(KAPAZITAETEN), 0)
|
||||
routing = pywrapcp.RoutingModel(manager)
|
||||
|
||||
def zeit_callback(von_index, nach_index):
|
||||
von = manager.IndexToNode(von_index)
|
||||
nach = manager.IndexToNode(nach_index)
|
||||
service = SERVICEZEIT if von != 0 else 0
|
||||
return int(distanz[von][nach]) + service
|
||||
|
||||
zeit_index = routing.RegisterTransitCallback(zeit_callback)
|
||||
routing.SetArcCostEvaluatorOfAllVehicles(zeit_index)
|
||||
|
||||
def bedarf_callback(von_index):
|
||||
return BEDARFE[manager.IndexToNode(von_index)]
|
||||
|
||||
bedarf_index = routing.RegisterUnaryTransitCallback(bedarf_callback)
|
||||
routing.AddDimensionWithVehicleCapacity(
|
||||
bedarf_index, 0, KAPAZITAETEN, True, "Kapazitaet")
|
||||
|
||||
routing.AddDimension(zeit_index, WARTEZEIT_MAX, SCHICHTLAENGE, False, "Zeit")
|
||||
zeit_dimension = routing.GetDimensionOrDie("Zeit")
|
||||
for ort, (fruehestens, spaetestens) in enumerate(ZEITFENSTER):
|
||||
zeit_dimension.CumulVar(manager.NodeToIndex(ort)).SetRange(
|
||||
fruehestens, spaetestens)
|
||||
|
||||
parameter = pywrapcp.DefaultRoutingSearchParameters()
|
||||
parameter.first_solution_strategy = (
|
||||
routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC)
|
||||
parameter.local_search_metaheuristic = (
|
||||
routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH)
|
||||
parameter.time_limit.seconds = ZEITBUDGET_S
|
||||
parameter.solution_limit = LOESUNGSGRENZE
|
||||
|
||||
loesung = routing.SolveWithParameters(parameter)
|
||||
if not loesung:
|
||||
raise SystemExit("Keine zulaessige Routenfuehrung gefunden.")
|
||||
|
||||
touren = []
|
||||
for fahrzeug in range(len(KAPAZITAETEN)):
|
||||
index = routing.Start(fahrzeug)
|
||||
if routing.IsEnd(loesung.Value(routing.NextVar(index))):
|
||||
continue
|
||||
stationen, fracht, strecke = [], 0, 0
|
||||
while not routing.IsEnd(index):
|
||||
knoten = manager.IndexToNode(index)
|
||||
stationen.append(knoten)
|
||||
fracht += BEDARFE[knoten]
|
||||
vorher = index
|
||||
index = loesung.Value(routing.NextVar(index))
|
||||
strecke += int(distanz[manager.IndexToNode(vorher)][
|
||||
manager.IndexToNode(index)])
|
||||
stationen.append(0)
|
||||
touren.append({"stationen": stationen, "fracht": fracht,
|
||||
"strecke": strecke,
|
||||
"kapazitaet": KAPAZITAETEN[fahrzeug]})
|
||||
return touren
|
||||
|
||||
|
||||
def zeichne(koordinaten, touren) -> None:
|
||||
figur, achse = plt.subplots(figsize=(7.6, 6.2))
|
||||
gesamt = sum(t["strecke"] for t in touren)
|
||||
|
||||
for nummer, (tour, farbe) in enumerate(zip(touren, TOURFARBEN), start=1):
|
||||
weg = koordinaten[tour["stationen"]]
|
||||
achse.plot(weg[:, 0], weg[:, 1], "-", color=farbe, linewidth=1.8,
|
||||
alpha=0.85, zorder=2,
|
||||
label=f"Fahrzeug {nummer}: {tour['strecke']} km, "
|
||||
f"{tour['fracht']}/{tour['kapazitaet']} Einheiten")
|
||||
|
||||
for ort in range(1, len(koordinaten)):
|
||||
achse.scatter(*koordinaten[ort], s=30 + 26 * BEDARFE[ort],
|
||||
color="#334155", zorder=3)
|
||||
achse.annotate(f"K{ort}", koordinaten[ort], textcoords="offset points",
|
||||
xytext=(7, 5), fontsize=7.5, color="#475569")
|
||||
|
||||
achse.scatter(*koordinaten[0], marker="s", s=190, color="#be123c", zorder=4)
|
||||
achse.annotate("Depot", koordinaten[0], textcoords="offset points",
|
||||
xytext=(9, 7), fontsize=9.5, color="#be123c",
|
||||
fontweight="bold")
|
||||
|
||||
achse.set_xlabel("km")
|
||||
achse.set_ylabel("km")
|
||||
achse.set_title(f"Vier Touren für 16 Kunden mit Zeitfenstern — "
|
||||
f"{gesamt} km insgesamt\n"
|
||||
"Punktgröße = Liefermenge", fontsize=10.5)
|
||||
achse.grid(linestyle=":", alpha=0.4)
|
||||
achse.legend(fontsize=8, loc="upper center", bbox_to_anchor=(0.5, -0.11),
|
||||
ncol=2, frameon=False)
|
||||
for seite in ("top", "right"):
|
||||
achse.spines[seite].set_visible(False)
|
||||
figur.tight_layout()
|
||||
|
||||
os.makedirs(BILDER, exist_ok=True)
|
||||
for endung in ("svg", "png"):
|
||||
pfad = os.path.join(BILDER, f"kap_graphen_vrp_touren.{endung}")
|
||||
figur.savefig(pfad, format=endung, dpi=160, bbox_inches="tight",
|
||||
metadata={"Date": None} if endung == "svg" else None)
|
||||
print(f"geschrieben: {pfad}")
|
||||
plt.close(figur)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
koordinaten, distanz = erzeuge_daten()
|
||||
touren = loese(distanz)
|
||||
gesamt = sum(t["strecke"] for t in touren)
|
||||
for nummer, tour in enumerate(touren, start=1):
|
||||
stationen = " -> ".join("Depot" if s == 0 else f"K{s}"
|
||||
for s in tour["stationen"])
|
||||
print(f"Fahrzeug {nummer}: {tour['strecke']:3} km, "
|
||||
f"{tour['fracht']}/{tour['kapazitaet']} | {stationen}")
|
||||
print(f"Gesamtstrecke: {gesamt} km")
|
||||
zeichne(koordinaten, touren)
|
||||
Loading…
Reference in a new issue