kleine Änderungen

This commit is contained in:
dschlueter 2026-09-09 07:37:07 +02:00
commit dc088fa214
47 changed files with 183868 additions and 573 deletions

View file

@ -457,6 +457,18 @@ und deren Zahl wächst wieder kombinatorisch.
> Konvex heißt: Wer bergab geht, kommt am tiefsten Punkt an. Nicht-konvex heißt: Wer bergab
> geht, kommt *irgendwo* an — und weiß nicht, ob es der tiefste Punkt war.
![Die Funktion $f(x) = x^2 + 3\sin(3x)$, links eindimensional mit den fünf Startpunkten aus
`Konvexitaet_Demo.py`, rechts als separable Fortsetzung
$f(x_1,x_2) = f(x_1) + f(x_2)$ — auf jeder Achse liegt genau die Kurve von links. Erzeugt von
`bilder_04/erzeuge_3d_konvexitaet.py`.](bilder_04/kap02_konvexitaet_landschaft.svg)
**Was Sie in der Abbildung sehen.** Links sind es fünf Läufe desselben Verfahrens, die in
vier verschiedenen Tälern enden — dieselben vier, die Teil 2 von `Konvexitaet_Demo.py`
weiter unten auflistet. In
zwei Dimensionen werden aus den Tälern Mulden in einem Gitter, und der Unterschied zwischen
Start A und Start B beträgt hier bereits mehr als 24 Einheiten. Keiner der beiden Läufe hat
etwas falsch gemacht: Beide enden dort, wo es nicht mehr bergab geht.
### Was ist konvex, was nicht?
| Problemtyp | Konvex? | Konsequenz |

View file

@ -181,9 +181,16 @@ Ecke zu Ecke — und zwar immer in Richtung Verbesserung.
![Derselbe zulässige Bereich, aber der Weg der Handrechnung weiter unten: vom Ursprung über
(0; 8) zum Optimum (7,5; 5). Drei der fünf Ecken werden besucht, keine zweimal — das ist der
ganze Gewinn gegenüber dem Aufzählen aller Ecken. Erzeugt von
ganze Gewinn gegenüber dem Aufzählen aller Ecken. Punktiert die Niveaulinien der
Zielfunktion, der Pfeil ihr Gradient $\mathbf{c}$. Erzeugt von
`bilder_04/erzeuge_polyeder.py`.](bilder_04/kap_lp_simplex_ecken.svg)
**Was Sie in der Abbildung sehen.** Durch jede besuchte Ecke läuft genau eine Niveaulinie —
die Linien sind nicht gewählt, sondern die Zielwerte 0 €, 2.000 € und 2.375 € der drei Ecken.
Der Simplex schiebt diese Linie in Richtung des Pfeils so weit hinaus, wie das Polyeder es
zulässt. Dass er dabei an einer Ecke anhält und nicht mittendrin, ist keine Eigenheit des
Verfahrens, sondern der Fundamentalsatz: Weiter geht es nur noch hinaus.
### Der Ablauf
1. **Initialisierung.** Starte an einer bekannten Ecke — meist dem Ursprung
@ -540,6 +547,16 @@ Prüfen wir das an unserem Beispiel: $\mathbf{b}^\top\mathbf{y}^*
= 40 \cdot 12{,}5 + 60 \cdot 31{,}25 + 8 \cdot 0 = 500 + 1875 + 0 = 2375$ ✓ — identisch mit
$Z^* = 2375$.
![Beide Probleme geometrisch, gerechnet mit `Dualitaet_Nachweis.py`. Rechts ist die Ebene
$y_3 = 0$ gezeichnet — zulässig, weil das Marktlimit $x_1 \le 8$ bei $x_1^* = 7{,}5$ nicht
bindet und deshalb $y_3^* = 0$ gilt. Erzeugt von
`bilder_04/erzeuge_dualitaets_bruecke.py`.](bilder_04/kap_lp_dualitaets_bruecke.svg)
**Was Sie in der Abbildung sehen.** Die beiden Tafeln haben nichts gemeinsam: verschiedene
Achsen, verschiedene Einheiten, links ein geschlossenes Polyeder, rechts ein nach oben
offener Bereich. Links wird maximiert, rechts minimiert. Und doch steht an beiden Optima
dieselbe Zahl — 2.375 €. Genau das behauptet der Satz, und mehr ist an ihm nicht zu sehen.
Das lässt sich auch unabhängig von der Handrechnung nachweisen: Primal- und Dualproblem
werden als **zwei getrennte, voneinander unabhängige LPs** aufgestellt und gelöst — wenn
der starke Dualitätssatz{idx:Starker Dualitätssatz} stimmt, müssen beide denselben Zielwert liefern, ohne dass eines

View file

@ -328,6 +328,16 @@ wird verworfen, weil sein Wert 18 den Incumbent 20 aus Ast A nicht schlägt. Erz
(der **Incumbent**{idx:Branch-and-Bound!Incumbent}),
* oder die LP-Lösung bereits ganzzahlig ist (dann ist dieser Ast fertig).
![Was der erste Verzweigungsschritt des Suchbaums geometrisch bedeutet. Graue Punkte sind die
13 ganzzahligen zulässigen Punkte. Erzeugt von
`bilder_04/erzeuge_bnb_geometrie.py`.](bilder_04/kap_milp_bnb_geometrie.svg)
**Was Sie in der Abbildung sehen.** Der rote Streifen ist genau das, was Phase 2 wegnimmt —
und in ihm liegt kein einziger grauer Punkt. Deshalb ist Verzweigen verlustfrei. Weggenommen
wird trotzdem etwas Wertvolles: das LP-Optimum (3; 1,5) selbst. Genau darum sinkt die
Schranke von 21 auf 20,67, und genau dieses Sinken ist der Fortschritt, den der Suchbaum
Knoten für Knoten protokolliert.
Zusätzlich fügen Solver **Schnittebenen**{idx:Schnittebenen} (*cutting planes*, z. B. Gomory-Cuts{idx:Schnittebenen!Gomory-Cuts}) hinzu:
gültige Ungleichungen, die gebrochene Bereiche wegschneiden, ohne einen einzigen zulässigen
ganzzahligen Punkt zu entfernen.

View file

@ -157,6 +157,15 @@ Projektterminierung und Job-Shop-Scheduling.
> einen Schritt zurück, sondern direkt zur Ursache des Konflikts
> (*non-chronological backtracking*).
![Dieselbe Handrechnung als Bild: grün die Werte, die in der Domain bleiben, rot der Wert,
den die jeweilige Runde streicht, grau die schon vorher gestrichenen. Erzeugt von
`bilder_04/erzeuge_propagation_ablauf.py`.](bilder_04/kap_cpsat_propagation_ablauf.svg)
**Was Sie in der Abbildung sehen.** Jede Runde streicht genau **einen** Wert — und trotzdem
sinkt die Zahl der Kombinationen von 16 auf 9, also um gut vierzig Prozent. Das ist der
Hebel: Gestrichen wird nicht eine Kombination, sondern ein Wert, und mit ihm alle
Kombinationen, in denen er vorkam.
```python
#!/usr/bin/env python3
@ -776,15 +785,26 @@ Erreichter Makespan: 11
==========================================================================
```
![Job-Shop-Plan als Gantt-Diagramm](bilder_04/kap06_jobshop_gantt.svg)
![Derselbe Plan als Gantt-Diagramm. Schraffiert die beiden Stillstände zwischen zwei
Arbeitsgängen, gestrichelt die untere Schranke. Erzeugt von
`bilder_04/erzeuge_kap06_gantt.py`.](bilder_04/kap06_jobshop_gantt.svg)
{plotly:kap06_jobshop_gantt}
**Lesen Sie das Gantt-Diagramm.** Auftrag 1 kommt zuerst an die Fräse (02), Auftrag 0 muss
warten und ist dort erst von 2 bis 5 an der Reihe. Auffällig ist die Lücke in der Dreherei
bei Zeitpunkt 4: Die Maschine steht still, obwohl Auftrag 0 schon Arbeit hätte — er ist aber
bis 5 noch an der Fräse. Dieses **Warten ist unvermeidlich**; es entsteht aus dem
Zusammenspiel von Reihenfolge- und Maschinenbedingungen, nicht aus einer schlechten Planung.
warten und ist dort erst von 2 bis 5 an der Reihe. Auffällig sind die beiden schraffierten
Lücken. Die in der Dreherei bei Zeitpunkt 4 ist die sprechendere: Die Maschine steht still,
obwohl Auftrag 0 schon Arbeit hätte — er ist aber bis 5 noch an der Fräse. Dieses **Warten
ist unvermeidlich**; es entsteht aus dem Zusammenspiel von Reihenfolge- und
Maschinenbedingungen, nicht aus einer schlechten Planung.
Die gestrichelte Linie bei 10 sagt dasselbe von der anderen Seite. Sie ist die untere Schranke
aus dem Programm, und sie stammt von der Dreherei: Deren drei Arbeitsgänge dauern zusammen
2 + 4 + 4 = 10 Zeiteinheiten, die sich nicht überlappen dürfen. Die Dreherei ist im Plan von 0
bis 11 belegt — also genau ihre 10 Einheiten Arbeit plus die eine schraffierte Einheit
Stillstand. Der Plan ist somit nicht deshalb elf lang, weil schlecht geplant wurde, sondern
weil die Engpassmaschine einmal warten muss. (Die zweite Lücke, in der Lackiererei, kostet
nichts: Diese Maschine ist ohnehin nicht der Engpass.)
> **⚠️ Mehrere gleich gute Pläne — und was das für Ihre Tests bedeutet**
>

View file

@ -543,7 +543,9 @@ Kompromisse sind für kein Gewicht erreichbar — und das ist keine Frage der Ra
![Pareto-Front: Kosten gegen CO₂. Die gefüllten Punkte liegen auf der unteren konvexen
Hülle und sind über Gewichte erreichbar; die vier offenen Kreise sind pareto-optimal,
werden aber von keiner Geraden gestützt.](bilder_04/kap_mehrziel_pareto.svg)
werden aber von keiner Geraden gestützt. Die dünne Gerade ist eine solche Stützgerade, die
punktierte die parallele Gerade durch den am weitesten abliegenden der vier Punkte. Erzeugt
von `bilder_04/erzeuge_pareto_front.py`.](bilder_04/kap_mehrziel_pareto.svg)
{plotly:kap_mehrziel_pareto}
@ -557,6 +559,15 @@ Das Programm rechnet diese Hülle in `untere_huelle()` unabhängig aus und vergl
die vier Punkte, die das Gewichtsraster verfehlt, liegen strikt oberhalb. Die Gegenprobe
steht in der Ausgabe.
**Was Sie in der Abbildung sehen.** Die beiden Geraden haben dieselbe Steigung, also dasselbe
Gewicht $w$. Die untere berührt die Hülle in zwei Ecken — eine von beiden meldet der Solver
als Optimum. Die obere geht durch einen pareto-optimalen Punkt, der damit für dieses Gewicht
chancenlos ist, und zwar nicht knapp: Er liegt eine ganze Stufe darüber. Verschieben Sie die
Steigung, verschiebt sich die Berührung von einer Ecke zur nächsten — über den Punkt
dazwischen springt sie hinweg. Die waagerechte Linie zeigt den Ausweg, um den es in
{ref:sec:mehrziel-epsilon} geht: Eine Schranke für den CO₂-Ausstoß statt eines Gewichts
findet genau diesen Punkt.
> **⚠️ Der Grund liegt in der Ganzzahligkeit**
>
> Bei einem reinen LP ist der zulässige Bereich konvex, und die Pareto-Front liegt vollständig

View file

@ -254,6 +254,16 @@ können: Einzeln meldet er für jede Anleihe einen *Gewinn* von 2 €, im Portfo
dagegen verhält sich korrekt: 101,33 € im Portfolio gegenüber 159,24 € bei getrennter
Betrachtung — die Streuung **senkt** das Risiko, wie es sein muss.
![Dieselbe Tabelle als Bild, gerechnet mit denselben Ziehungen wie `VaR_CVaR_Demo.py`. Erzeugt
von `bilder_04/erzeuge_var_vs_cvar.py`.](bilder_04/kap_cvar_subadditivitaet.svg)
**Was Sie in der Abbildung sehen.** Links wechselt der Balken über null das Vorzeichen: Aus
zwei gemeldeten Gewinnen wird im Portfolio ein Verlust. Rechts passiert nichts dergleichen —
der Portfoliobalken ist kürzer als der Summenbalken, und genau das heißt Subadditivität. Der
Grund für den Sprung links ist keine Rechenungenauigkeit, sondern die Bauart des Maßes: Eine
Schwelle bei 95 % sieht ein Ereignis mit 4 % Wahrscheinlichkeit nicht — bis zwei davon
zusammenkommen.
> **⚠️ Eine Falle bei der CVaR-Berechnung**
> Der naheliegende Weg — „Mittelwert aller Verluste $\ge$ VaR“ — ist **falsch**, sobald die
> Verteilung **Atome** hat (also einzelne Werte mit positiver Wahrscheinlichkeit, wie hier

View file

@ -918,7 +918,9 @@ und das ist weniger trivial, als es aussieht:
## Architektur einer produktionsreifen OR-Plattform {#sec:praxisfallen-architektur-einer-produktionsreifen-or-plattform}
![Architektur einer produktionsreifen OR-Plattform](bilder_04/kap15_or_plattform.svg)
![Die Schichten dieses Abschnitts als Bild — erzeugt **aus** der Tabelle daneben, damit beide
nicht auseinanderlaufen können. Erzeugt von
`bilder_04/erzeuge_or_architektur.py`.](bilder_04/kap15_or_plattform.svg)
In einer professionellen Umgebung ist der Optimierer kein Skript, sondern ein
**versionierter, zustandsloser Dienst**:
@ -932,6 +934,12 @@ In einer professionellen Umgebung ist der Optimierer kein Skript, sondern ein
| **Ausführung** | Kalender-Sync, Benachrichtigung, Order-Routing | Kalender-APIs, Messaging, Broker |
| **Betrieb** | Monitoring, Audit-Trail, Alarme | Logging, Metriken, Versionierung |
**Was Sie in der Abbildung sehen.** Fünf der sechs Schichten bilden einen Weg: Daten gehen
oben hinein und unten als Handlung wieder hinaus. Die sechste steht daneben, und das ist
keine zeichnerische Verlegenheit — Monitoring und Audit-Trail sind keine Station, die man
durchläuft, sondern eine Anforderung an jede. Wer sie als sechsten Kasten unten anhängt, baut
sie am Ende auch so: als etwas, das man hinterher noch macht.
**Fünf Prinzipien, die sich in der Praxis bewährt haben:**
1. **Snapshot-Prinzip.**{idx:Snapshot-Prinzip} Jeder Optimierungslauf arbeitet auf einem unveränderlichen