diff --git a/CLAUDE.md b/CLAUDE.md index 1e60dbd..465d639 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -188,6 +188,31 @@ eine einzige lange Zeile mit eingebetteten Leerzeichenketten, aus der Pandoc son Codeblock macht. Genau daran waren alle vier Figuren kaputt, bis es die Schlussabnahme fand. Im PDF steht stattdessen ein Hinweis auf die Website. +## Karten + +`{karte:name}` bindet `bilder_04/karten/.html` als ` +

Was Sie auf der Karte sehen — und was nicht. Die Instanz ist synthetisch: 17 zufällige Punkte auf einem Quadrat von 100 × 100 km. Für die Karte wurden sie lagegetreu auf eine reale Region gelegt — mit einer azimutal-äquidistanten Projektion, die alle 136 Abstände zwischen den Punkten bis auf 0,001 % erhält. Die Karte zeigt also dieselbe Instanz, mit der das Modell rechnet, nur an geographischen Koordinaten statt auf einem Zahlenraster; kein Wert in diesem Kapitel ändert sich dadurch.

+

Die Touren sind aus demselben Grund Luftlinien und keine Straßenrouten: Das Modell kennt keine Straßen, es rechnet mit den Abständen der Distanzmatrix. Eine Linie, die einer echten Straße folgte, wäre hübscher und würde eine Genauigkeit vortäuschen, die das Modell nicht hat. Wer Straßenentfernungen braucht, ersetzt die Matrix — Abschnitt 8.7 sagt, was dabei zu beachten ist.

Kurzzyklen verhindern

Ein naives Modell erlaubt Subtouren: isolierte Kreise, die das Depot nie anfahren. Die klassische Gegenmaßnahme ist die MTZ-Formulierung nach Miller, Tucker und Zemlin. Man führt Rangvariablen u_i ein (die Position des Knotens in der Tour):

diff --git a/Operations_Research_mit_Python_Version_04.md b/Operations_Research_mit_Python_Version_04.md index 8905583..53229ae 100644 --- a/Operations_Research_mit_Python_Version_04.md +++ b/Operations_Research_mit_Python_Version_04.md @@ -9587,6 +9587,12 @@ Das **Traveling Salesperson Problem (TSP)**`\index{TSP (Traveling Salesperson Pr ![Abb. 8.2: Die Lösung der Instanz aus `VRP_Flotten_Routing.py`: 16 Kunden, vier Fahrzeuge, 619 km. **Beachten Sie, dass sich die Touren kreuzen.** Bei einem reinen Tourenproblem wäre das ein sicheres Zeichen für eine verbesserbare Lösung — hier ist es keines: Die Zeitfenster erzwingen die Reihenfolge, und wer die Kreuzungen auflöst, kommt zu spät. Erzeugt von `bilder_04/erzeuge_vrp_touren.py`.](bilder_04/kap_graphen_vrp_touren.svg) +*Eine interaktive Karte dieser Touren steht auf den Kapitelseiten der Website bereit.* + +**Was Sie auf der Karte sehen — und was nicht.** Die Instanz ist synthetisch: 17 zufällige Punkte auf einem Quadrat von 100 × 100 km. Für die Karte wurden sie *lagegetreu* auf eine reale Region gelegt — mit einer azimutal-äquidistanten Projektion, die alle 136 Abstände zwischen den Punkten bis auf **0,001 %** erhält. Die Karte zeigt also dieselbe Instanz, mit der das Modell rechnet, nur an geographischen Koordinaten statt auf einem Zahlenraster; kein Wert in diesem Kapitel ändert sich dadurch. + +Die Touren sind aus demselben Grund **Luftlinien und keine Straßenrouten**: Das Modell kennt keine Straßen, es rechnet mit den Abständen der Distanzmatrix. Eine Linie, die einer echten Straße folgte, wäre hübscher und würde eine Genauigkeit vortäuschen, die das Modell nicht hat. Wer Straßenentfernungen braucht, ersetzt die Matrix — [Abschnitt 8.7](#sec:graphen-denkfehler) sagt, was dabei zu beachten ist. + ### Kurzzyklen verhindern Ein naives Modell erlaubt **Subtouren**`\index{Subtour}`{=latex}: isolierte Kreise, die das Depot nie anfahren. Die klassische Gegenmaßnahme ist die **MTZ-Formulierung**`\index{MTZ-Formulierung}`{=latex} nach Miller, Tucker und Zemlin. Man führt Rangvariablen $u_i$ ein (die Position des Knotens in der Tour): diff --git a/Operations_Research_mit_Python_Version_04.pdf b/Operations_Research_mit_Python_Version_04.pdf index 9ccab79..a605227 100644 Binary files a/Operations_Research_mit_Python_Version_04.pdf and b/Operations_Research_mit_Python_Version_04.pdf differ diff --git a/Operations_Research_mit_Python_Version_04/23_Graphen_Fluesse_Touren.md b/Operations_Research_mit_Python_Version_04/23_Graphen_Fluesse_Touren.md index f850d23..597dfca 100644 --- a/Operations_Research_mit_Python_Version_04/23_Graphen_Fluesse_Touren.md +++ b/Operations_Research_mit_Python_Version_04/23_Graphen_Fluesse_Touren.md @@ -493,6 +493,21 @@ sicheres Zeichen für eine verbesserbare Lösung — hier ist es keines: Die Zei erzwingen die Reihenfolge, und wer die Kreuzungen auflöst, kommt zu spät. Erzeugt von `bilder_04/erzeuge_vrp_touren.py`.](bilder_04/kap_graphen_vrp_touren.svg) +{karte:kap_graphen_vrp_touren} + +**Was Sie auf der Karte sehen — und was nicht.** Die Instanz ist synthetisch: 17 zufällige +Punkte auf einem Quadrat von 100 × 100 km. Für die Karte wurden sie *lagegetreu* auf eine +reale Region gelegt — mit einer azimutal-äquidistanten Projektion, die alle 136 Abstände +zwischen den Punkten bis auf **0,001 %** erhält. Die Karte zeigt also dieselbe Instanz, mit +der das Modell rechnet, nur an geographischen Koordinaten statt auf einem Zahlenraster; kein +Wert in diesem Kapitel ändert sich dadurch. + +Die Touren sind aus demselben Grund **Luftlinien und keine Straßenrouten**: Das Modell kennt +keine Straßen, es rechnet mit den Abständen der Distanzmatrix. Eine Linie, die einer echten +Straße folgte, wäre hübscher und würde eine Genauigkeit vortäuschen, die das Modell nicht +hat. Wer Straßenentfernungen braucht, ersetzt die Matrix — {ref:sec:graphen-denkfehler} sagt, +was dabei zu beachten ist. + ### Kurzzyklen verhindern Ein naives Modell erlaubt **Subtouren**{idx:Subtour}: isolierte Kreise, die das Depot nie anfahren. Die diff --git a/Operations_Research_mit_Python_Version_04/build_version_04.py b/Operations_Research_mit_Python_Version_04/build_version_04.py index 8d622b0..61a541e 100644 --- a/Operations_Research_mit_Python_Version_04/build_version_04.py +++ b/Operations_Research_mit_Python_Version_04/build_version_04.py @@ -390,6 +390,23 @@ def pruefe_dateien() -> list[str]: f"folgt aber '{soll}' - die Lesekette ueberspringt eine Datei.") fehlend.append(name) + # Marken fuer interaktive Figuren: Zeigt eine ins Leere, verschwindet sie + # auf der Website stillschweigend - im PDF steht dann ein Verweis auf eine + # Figur, die es nicht gibt. + for name in DATEIEN: + with open(os.path.join(HIER, name), encoding="utf-8") as f: + inhalt = f.read() + for marke, verzeichnis in ((PLOTLY_RE, PLOTLY_VERZ), + (KARTE_RE, KARTEN_VERZ)): + for treffer in marke.finditer(inhalt): + pfad = os.path.join(verzeichnis, f"{treffer.group(1)}.html") + if not os.path.exists(pfad): + zeile = inhalt[:treffer.start()].count("\n") + 1 + print(f"FEHLER: {name}:{zeile}: '{treffer.group(0).strip()}' " + f"verweist auf eine nicht vorhandene Datei " + f"({os.path.relpath(pfad, BASIS)}).") + fehlend.append(f"{name}:{zeile}") + fehlend.extend(pruefe_assets()) fehlend.extend(pruefe_titeltexte()) return fehlend @@ -1088,6 +1105,7 @@ def baue_markdown() -> str: gesamt = "\n\n---\n\n".join(teile) + "\n" + _STICHWORTREGISTER_BLOCK gesamt, _ = resolve_plotly(gesamt, fuer_html=False) + gesamt = resolve_karte(gesamt, fuer_html=False) return resolve_index(resolve_numbering(reflow_markdown(gesamt))) @@ -1865,6 +1883,45 @@ PLOTLY_RE = re.compile(r"^\{plotly:([\w-]+)\}[ \t]*$", re.MULTILINE) PLOTLY_VERZ = os.path.join(BILDER_VERZ, "plotly") +# Dasselbe fuer Leaflet-Karten: +# +# {karte:kap_graphen_vrp_touren} +# +# Anders als die Plotly-Fragmente wird eine Karte NICHT eingebettet, sondern in +# einem \n' + '```') + + return KARTE_RE.sub(ersetze, text) + def resolve_plotly(text: str, fuer_html: bool) -> tuple[str, bool]: """Loest {plotly:name}-Marken auf. Liefert (Text, ob eine Figur eingebettet @@ -2138,6 +2195,7 @@ def baue_kapitel_seiten(html_verz: str, datei_seite: dict, labels: dict, text = resolve_numbering_seite(text, eintrag["seite"], labels, label_seite) text = resolve_index(text) text, mit_plotly = resolve_plotly(text, fuer_html=True) + text = resolve_karte(text, fuer_html=True) text = markiere_karten(text) fragment = _pandoc_fragment(text, html_verz) diff --git a/PROGRESS.md b/PROGRESS.md index acc19b7..e95cfc6 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -1376,11 +1376,11 @@ Teilhistorie, die vollständig aussieht und es nicht ist. Volle Historie ginge n 7.1 leisten dasselbe gezielter, ohne Eingriff in jeden Abschnitt. * **`joblib`/`ray`** (V02/2) — `num_workers` und `ProcessPoolExecutor` decken den Bedarf; ein Cluster-Framework wäre eine Abhängigkeit ohne Beispiel, das sie braucht. -* **Folium-Karten** (V03/3) — **blockiert durch eine offene Entscheidung**: Die VRP-Instanz - ist ein synthetisches 100 × 100-km-Raster ohne Geokoordinaten. Eine echte Karte verlangt - entweder eine neue Instanz (und damit neue abgedruckte Zahlen) oder eine willkürliche - Verankerung an einem realen Ort. -* **Executive-PDF-Report** (V03/6) — ohne Streamlit-Apps fehlt der Ort, an dem er entstünde. +* **Executive-PDF-Report** (V03/6) — ohne Streamlit-Apps fehlt der Ort, an dem er + entstünde. + +**Folium-Karten** (V03/3) standen hier lange als **blockiert** — sie sind es nicht mehr, +siehe den Eintrag im nächsten Abschnitt. ### Offen, rein redaktionell @@ -2258,6 +2258,51 @@ hat ihren Zweck erfüllt — aber einen Rückschritt könnte sie gar nicht mehr Datei ist gelöscht, die Probe bliebe auch dann grün, wenn jemand `--delete` aus dem `rsync`-Aufruf nähme. Von den ursprünglich vierzehn Adressen bleiben dreizehn. +### ✅ Folium-Karten — die Blockade war eine falsche Alternative + +Der einzige Punkt, der in diesem Projekt je als *blockiert* geführt wurde, ist erledigt. Die +Begründung lautete: Die VRP-Instanz sei ein synthetisches 100 × 100-km-Raster ohne +Geokoordinaten, eine echte Karte verlange also **entweder** eine neue Instanz (und damit +zwei Dutzend neue abgedruckte Zahlen) **oder** eine willkürliche Verankerung. + +**Es gibt einen dritten Weg, und er ist exakt.** Eine azimutal-äquidistante Projektion um den +Schwerpunkt des Rasters bildet die 17 Punkte so auf Länge und Breite ab, dass die +Großkreisdistanzen mit den Rasterdistanzen übereinstimmen: + +| Verfahren | Median | Maximum | +| --- | --- | --- | +| festes `cos(Breite)` | 0,360 % | 2,003 % | +| `cos` der eigenen Breite | 0,351 % | 0,908 % | +| **azimutal-äquidistant** | **0,000 %** | **0,001 %** | + +Ein Meter auf hundert Kilometer. Damit ist die Karte keine Näherung, für die man sich +entschuldigt, sondern eine zweite Darstellung derselben Instanz — **kein abgedruckter Wert +ändert sich**, die Gesamtstrecke bleibt bei 619 km, das SVG byte-identisch. + +Zwei Befunde stützten die Entscheidung: „100 × 100 km Raster" steht **nur in zwei +Codekommentaren**, nirgends im Fließtext; und der didaktische Punkt der Bildunterschrift — +die sich kreuzenden Touren, die *kein* Fehler sind — wird auf einer Karte deutlicher. + +**Umgesetzt** in `erzeuge_vrp_touren.py`, das jetzt SVG **und** Karte aus einem Lauf schreibt. +Ein zweites Skript hätte erneut gelöst, und die Routing-Bibliothek arbeitet heuristisch. +Neu im Build: die Marke `{karte:name}`, aufgelöst zu einem `