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
171
OR_HTML_04/programme/VRP_Kapazitaetsfalle.py
Normal file
171
OR_HTML_04/programme/VRP_Kapazitaetsfalle.py
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# VRP_Kapazitaetsfalle.py
|
||||
"""
|
||||
Kapitel Graphen: Die vergessene Dimension.
|
||||
|
||||
Die Routing-Bibliothek von OR-Tools kennt keine "Kapazitaet" von sich aus.
|
||||
Sie kennt nur DIMENSIONEN - benannte Groessen, die sich entlang einer Tour
|
||||
aufsummieren und begrenzt werden koennen. Distanz ist eine, Zeit ist eine,
|
||||
Ladung ist eine. Wer eine davon nicht anlegt, bekommt trotzdem eine Loesung:
|
||||
eine schoene, kurze, guenstige - und unfahrbare.
|
||||
|
||||
Dieses Programm loest dieselbe Instanz zweimal und prueft beide Ergebnisse
|
||||
gegen die tatsaechlichen Lademengen.
|
||||
|
||||
Instanz: 1 Depot, 16 Kunden, 4 Fahrzeuge zu je 10 Paletten.
|
||||
Gesamtbedarf 37 Paletten bei 40 Paletten Flottenkapazitaet - es ist also
|
||||
knapp, aber machbar.
|
||||
|
||||
Benoetigt: numpy, ortools
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
from ortools.constraint_solver import pywrapcp, routing_enums_pb2
|
||||
|
||||
# Dieselbe Instanz wie VRP_Flotten_Routing.py
|
||||
BEDARFE = [0, 2, 3, 1, 4, 2, 2, 3, 1, 2, 4, 3, 2, 1, 2, 3, 2]
|
||||
KAPAZITAETEN = [10, 10, 10, 10]
|
||||
ANZAHL_FAHRZEUGE = 4
|
||||
DEPOT = 0
|
||||
|
||||
|
||||
def distanzmatrix(seed: int = 42) -> list[list[int]]:
|
||||
rng = np.random.default_rng(seed)
|
||||
koordinaten = rng.random((len(BEDARFE), 2)) * 100 # 100 x 100 km
|
||||
n = len(BEDARFE)
|
||||
return [[int(np.linalg.norm(koordinaten[i] - koordinaten[j]))
|
||||
for j in range(n)] for i in range(n)]
|
||||
|
||||
|
||||
def plane(mit_kapazitaet: bool, zeitlimit: int = 5) -> dict:
|
||||
"""Loest die Tourenplanung - wahlweise mit oder ohne Ladungsdimension."""
|
||||
distanz = distanzmatrix()
|
||||
manager = pywrapcp.RoutingIndexManager(len(distanz), ANZAHL_FAHRZEUGE, DEPOT)
|
||||
routing = pywrapcp.RoutingModel(manager)
|
||||
|
||||
def entfernung(von_index, nach_index):
|
||||
return distanz[manager.IndexToNode(von_index)][manager.IndexToNode(nach_index)]
|
||||
|
||||
kosten_id = routing.RegisterTransitCallback(entfernung)
|
||||
routing.SetArcCostEvaluatorOfAllVehicles(kosten_id)
|
||||
|
||||
# DIE entscheidende Stelle. Ohne diesen Block existiert im Modell keine
|
||||
# Ladung - die Fahrzeuge sind dann unendlich gross.
|
||||
if mit_kapazitaet:
|
||||
def bedarf(index):
|
||||
return BEDARFE[manager.IndexToNode(index)]
|
||||
|
||||
bedarf_id = routing.RegisterUnaryTransitCallback(bedarf)
|
||||
routing.AddDimensionWithVehicleCapacity(
|
||||
bedarf_id,
|
||||
0, # kein Zwischenpuffer
|
||||
KAPAZITAETEN, # Obergrenze je Fahrzeug
|
||||
True, # Ladung startet bei 0
|
||||
"Ladung")
|
||||
|
||||
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.FromSeconds(zeitlimit)
|
||||
|
||||
loesung = routing.SolveWithParameters(parameter)
|
||||
if loesung is None:
|
||||
raise RuntimeError("Keine Loesung gefunden")
|
||||
|
||||
touren, strecken, ladungen = [], [], []
|
||||
for fahrzeug in range(ANZAHL_FAHRZEUGE):
|
||||
index = routing.Start(fahrzeug)
|
||||
tour, strecke, ladung = [], 0, 0
|
||||
while not routing.IsEnd(index):
|
||||
knoten = manager.IndexToNode(index)
|
||||
tour.append(knoten)
|
||||
ladung += BEDARFE[knoten]
|
||||
vorher = index
|
||||
index = loesung.Value(routing.NextVar(index))
|
||||
strecke += routing.GetArcCostForVehicle(vorher, index, fahrzeug)
|
||||
tour.append(manager.IndexToNode(index))
|
||||
touren.append(tour)
|
||||
strecken.append(strecke)
|
||||
ladungen.append(ladung)
|
||||
|
||||
return {"touren": touren, "strecken": strecken, "ladungen": ladungen,
|
||||
"gesamtstrecke": sum(strecken)}
|
||||
|
||||
|
||||
def pruefe(ergebnis: dict) -> list[str]:
|
||||
"""Prueft den Plan gegen die Wirklichkeit - unabhaengig vom Modell.
|
||||
|
||||
Genau diese Trennung ist der Punkt: Die Pruefung darf nicht dieselben
|
||||
Annahmen benutzen wie das Modell, sonst prueft sie nichts.
|
||||
"""
|
||||
beanstandungen = []
|
||||
for fahrzeug, (ladung, kapazitaet) in enumerate(
|
||||
zip(ergebnis["ladungen"], KAPAZITAETEN)):
|
||||
if ladung > kapazitaet:
|
||||
beanstandungen.append(
|
||||
f"Fahrzeug {fahrzeug + 1}: {ladung} Paletten geladen, "
|
||||
f"Kapazitaet {kapazitaet} ({ladung - kapazitaet} zu viel)")
|
||||
|
||||
beliefert = sorted(k for tour in ergebnis["touren"] for k in tour[1:-1])
|
||||
erwartet = list(range(1, len(BEDARFE)))
|
||||
if beliefert != erwartet:
|
||||
fehlend = set(erwartet) - set(beliefert)
|
||||
if fehlend:
|
||||
beanstandungen.append(f"nicht beliefert: {sorted(fehlend)}")
|
||||
return beanstandungen
|
||||
|
||||
|
||||
def zeige(titel: str, ergebnis: dict) -> None:
|
||||
print(f"\n{titel}")
|
||||
print(f" Gesamtstrecke {ergebnis['gesamtstrecke']} km")
|
||||
print(f" {'Fahrzeug':<10} {'Stopps':>7} {'Strecke':>9} {'Ladung':>8} "
|
||||
f"{'Kapazitaet':>11}")
|
||||
for i, (tour, strecke, ladung) in enumerate(
|
||||
zip(ergebnis["touren"], ergebnis["strecken"], ergebnis["ladungen"])):
|
||||
markierung = " <-- ueberladen" if ladung > KAPAZITAETEN[i] else ""
|
||||
print(f" {i + 1:<10} {len(tour) - 2:>7} {strecke:>8} km {ladung:>8} "
|
||||
f"{KAPAZITAETEN[i]:>11}{markierung}")
|
||||
|
||||
beanstandungen = pruefe(ergebnis)
|
||||
if beanstandungen:
|
||||
print(" PRUEFUNG: DURCHGEFALLEN")
|
||||
for text in beanstandungen:
|
||||
print(f" - {text}")
|
||||
else:
|
||||
print(" PRUEFUNG: bestanden")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 78)
|
||||
print(" DIE VERGESSENE DIMENSION")
|
||||
print("=" * 78)
|
||||
print(f"16 Kunden, Gesamtbedarf {sum(BEDARFE)} Paletten, "
|
||||
f"{ANZAHL_FAHRZEUGE} Fahrzeuge zu je {KAPAZITAETEN[0]} "
|
||||
f"= {sum(KAPAZITAETEN)} Paletten Flottenkapazitaet.")
|
||||
|
||||
ohne = plane(mit_kapazitaet=False)
|
||||
zeige("[1] Ohne Ladungsdimension", ohne)
|
||||
|
||||
mit = plane(mit_kapazitaet=True)
|
||||
zeige("[2] Mit AddDimensionWithVehicleCapacity", mit)
|
||||
|
||||
print("\n" + "=" * 78)
|
||||
mehr = mit["gesamtstrecke"] - ohne["gesamtstrecke"]
|
||||
print(f"Der korrekte Plan ist {mehr} km laenger "
|
||||
f"({mehr / ohne['gesamtstrecke'] * 100:.1f} %).")
|
||||
print()
|
||||
print("Und genau darin liegt die Gefahr: Lauf [1] sieht BESSER aus. Wer")
|
||||
print("beide Zahlen nebeneinander legt, ohne die Ladung zu pruefen, haelt")
|
||||
print("die unfahrbare Loesung fuer die bessere Optimierung - und den")
|
||||
print("korrekten Plan fuer schlechte Arbeit.")
|
||||
print()
|
||||
print("Die Routing-Bibliothek kennt keine 'Kapazitaet'. Sie kennt nur")
|
||||
print("Dimensionen, die man ihr anlegt. Was nicht als Dimension existiert,")
|
||||
print("wird nicht begrenzt - und faellt niemandem auf, weil das Ergebnis")
|
||||
print("plausibel aussieht.")
|
||||
print("=" * 78)
|
||||
Loading…
Reference in a new issue