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>
2026-09-08 01:20:09 +02:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
|
|
# erzeuge_branch_and_bound.py
|
|
|
|
|
"""
|
|
|
|
|
Erzeugt den Branch-and-Bound-Suchbaum zum MILP-Kapitel:
|
|
|
|
|
|
Aufgeraeumt: 33 PNG-Zweitfassungen, Bau-Ueberbleibsel, Ausgabepfade
Drei Aufraeumarbeiten - und zwei Funde, die dabei auffielen.
1. DIE PNG-ZWEITFASSUNGEN SIND WEG. Jedes Diagramm lag doppelt vor, als SVG
und als PNG, und kein einziges src=/href= in der Website zeigte je auf ein
PNG. Der Build kopierte sie trotzdem mit: 3,3 MB im Repository plus 3,3 MB,
die bei jeder Veroeffentlichung auf den Webserver gingen. Die 15 Generatoren
schreiben jetzt nur noch SVG, die Docstrings sind mitgezogen. Vor dem
Loeschen geprueft: Jedes PNG hatte sein gleichnamiges SVG, alle 33 waren
versioniert.
FUND 1: erzeuge_kap06_gantt.py folgte als einziger Generator nicht der
Konvention - weder svg.hashsalt noch metadata={"Date": None}. Sein SVG trug
einen echten Zeitstempel und bei jedem Lauf andere clip-path-IDs, war also
nie byteidentisch reproduzierbar, obwohl CLAUDE.md genau das fuer alle
Generatoren festhaelt. Aufgefallen nur, weil nach der PNG-Umstellung 32 von
33 SVGs bitgleich blieben und eines nicht. Jetzt byteidentisch ueber zwei
Laeufe.
FUND 2: spiegle_bilder() legte leere Verzeichnisse auf dem Webserver an. Der
Dateifilter arbeitete korrekt, aber os.walk durchlief auch __pycache__/, und
os.makedirs() erzeugte es am Ziel. Die Verzeichnisliste wird jetzt vorher
gefiltert. Gegengetestet.
2. BAU-UEBERBLEIBSEL entfernt (alle ignoriert und neu erzeugbar):
svg-inkscape/, build_v04.log, .pytest_cache/, Programme/output/, vier
__pycache__/ und die Excel-Mappen. Arbeitsbaum 36 -> 33 MB, danach null
ignorierte Ueberbleibsel.
3. Excel_Bruecke.py SCHREIBT NEBEN DAS SKRIPT statt ins
Arbeitsverzeichnis. Es benutzte blanke relative Namen; wer es aus der
Repository-Wurzel startete, verstreute dort produktionsmix.xlsx und
produktionsmix_ergebnis.xlsx. Jetzt wie die vier anderen schreibenden
Programme ueber os.path.dirname(os.path.abspath(__file__)). Nachgemessen:
Lauf aus der Wurzel legt dort null Dateien ab.
Ausserdem git gc: 653 lose Objekte gepackt, .git von 71 MB auf 28 MB -
reines Repacken, kein Inhalt beruehrt.
Geprueft: alle 33 im Buch referenzierten SVGs vorhanden, in Quelle und
Website; 16 Generatoren fehlerfrei; keine fehlenden Bilder im LaTeX-Lauf;
33 pytest-Tests; PDF unveraendert 760 Seiten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 14:22:26 +02:00
|
|
|
bilder_04/kap_milp_suchbaum.svg
|
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>
2026-09-08 01:20:09 +02:00
|
|
|
|
|
|
|
|
Gezeichnet wird der Baum der Handrechnung des Kapitels:
|
|
|
|
|
|
|
|
|
|
max 5 x1 + 4 x2 u.d.N. 6 x1 + 4 x2 <= 24, x1 + 2 x2 <= 6, x ganzzahlig
|
|
|
|
|
|
|
|
|
|
Der Baum wird NICHT abgeschrieben, sondern gerechnet: Eine kleine
|
|
|
|
|
Branch-and-Bound-Schleife loest die LP-Relaxationen mit scipy.linprog,
|
|
|
|
|
verzweigt ueber die erste gebrochene Variable und kappt nach denselben drei
|
|
|
|
|
Regeln, die der Kapiteltext nennt (unzulaessig, Schranke schlechter als der
|
|
|
|
|
Incumbent, Loesung bereits ganzzahlig). Am Ende prueft das Skript, dass Optimum
|
|
|
|
|
und Zahl der geloesten LPs mit dem Buchtext uebereinstimmen - stimmt etwas
|
|
|
|
|
nicht, bricht es ab, statt ein plausibles Bild zu malen.
|
|
|
|
|
|
|
|
|
|
Warum ueberhaupt neu: Das Vorgaengerbild zeigte einen Baum mit voellig anderen
|
|
|
|
|
Zahlen (Wurzel x1 = 2,7, Schranke 150,4, Incumbent 138) zu einem Modell, das
|
|
|
|
|
nirgends im Buch steht.
|
|
|
|
|
|
|
|
|
|
Aufruf (aus dem Repository-Wurzelverzeichnis):
|
|
|
|
|
python3 bilder_04/erzeuge_branch_and_bound.py
|
|
|
|
|
|
|
|
|
|
Benoetigt: numpy, scipy, graphviz (Python-Paket) und das Werkzeug 'dot'
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
|
|
|
|
|
import graphviz
|
|
|
|
|
import numpy as np
|
|
|
|
|
from scipy.optimize import linprog
|
|
|
|
|
|
|
|
|
|
BASIS = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
|
BILDER = os.path.join(BASIS, "bilder_04")
|
|
|
|
|
|
|
|
|
|
INDIGO, CYAN, VIOLETT, GRUEN, AMBER = (
|
|
|
|
|
"#4338ca", "#0891b2", "#7c3aed", "#059669", "#b45309")
|
|
|
|
|
TINT = {INDIGO: "#eef2ff", CYAN: "#ecfeff", VIOLETT: "#f5f3ff",
|
|
|
|
|
GRUEN: "#ecfdf5", AMBER: "#fffbeb"}
|
|
|
|
|
|
|
|
|
|
# Modell der Handrechnung
|
|
|
|
|
C = np.array([5.0, 4.0])
|
|
|
|
|
A = np.array([[6.0, 4.0], [1.0, 2.0]])
|
|
|
|
|
B = np.array([24.0, 6.0])
|
|
|
|
|
TOL = 1e-7
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def loese(untergrenzen, obergrenzen):
|
|
|
|
|
"""LP-Relaxation mit zusaetzlichen Schranken. Liefert (x, Z) oder (None, None)."""
|
|
|
|
|
ergebnis = linprog(c=-C, A_ub=A, b_ub=B,
|
|
|
|
|
bounds=list(zip(untergrenzen, obergrenzen)),
|
|
|
|
|
method="highs")
|
|
|
|
|
if not ergebnis.success:
|
|
|
|
|
return None, None
|
|
|
|
|
return np.asarray(ergebnis.x), float(-ergebnis.fun)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def erste_gebrochene(x):
|
|
|
|
|
for j, wert in enumerate(x):
|
|
|
|
|
if abs(wert - round(wert)) > 1e-6:
|
|
|
|
|
return j
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def zahl(wert: float) -> str:
|
|
|
|
|
"""Deutsche Schreibweise, hoechstens zwei Nachkommastellen.
|
|
|
|
|
|
|
|
|
|
Das '+ 0.0' faengt die negative Null ab: linprog liefert fuer x2 = 0
|
|
|
|
|
gelegentlich -0.0, und '(4; -0)' im Buch waere ein Schoenheitsfehler mit
|
|
|
|
|
Ansage.
|
|
|
|
|
"""
|
|
|
|
|
text = f"{wert + 0.0:.2f}".rstrip("0").rstrip(".")
|
|
|
|
|
if text in ("-0", "-"):
|
|
|
|
|
text = "0"
|
|
|
|
|
return text.replace(".", ",")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def branch_and_bound():
|
|
|
|
|
"""Baut den Suchbaum und liefert (Knotenliste, Kanten, Optimum, Zielwert).
|
|
|
|
|
|
|
|
|
|
Die Reihenfolge - erst der Ast mit der kleineren Obergrenze, dann der
|
|
|
|
|
andere - entspricht der Handrechnung: Ast A (x2 <= 1) wird vor Ast B
|
|
|
|
|
(x2 >= 2) bearbeitet, weshalb der Incumbent aus A stammt und B daran
|
|
|
|
|
scheitert.
|
|
|
|
|
"""
|
|
|
|
|
knoten, kanten = [], []
|
|
|
|
|
incumbent, incumbent_x = -np.inf, None
|
|
|
|
|
# TIEFENSUCHE (Stapel, nicht Warteschlange). Das ist keine Kosmetik: Nur so
|
|
|
|
|
# ist Ast A samt seinen Blaettern fertig, bevor Ast B an die Reihe kommt -
|
|
|
|
|
# und nur deshalb scheitert B am Incumbent 20, wie es der Kapiteltext
|
|
|
|
|
# beschreibt. Breitensuche wuerde B zum Incumbent machen und die Aussage
|
|
|
|
|
# des Bildes umdrehen.
|
|
|
|
|
stapel = [("P₀", None, [0.0, 0.0], [np.inf, np.inf], None)]
|
|
|
|
|
zaehler = 0
|
|
|
|
|
|
|
|
|
|
while stapel:
|
|
|
|
|
name, elter, unten, oben, kantentext = stapel.pop()
|
|
|
|
|
x, z = loese(unten, oben)
|
|
|
|
|
zaehler += 1
|
|
|
|
|
if elter is not None:
|
|
|
|
|
kanten.append((elter, name, kantentext))
|
|
|
|
|
|
|
|
|
|
if x is None:
|
|
|
|
|
knoten.append((name, "unzulässig — gekappt", AMBER, None))
|
|
|
|
|
continue
|
|
|
|
|
if z < incumbent - TOL:
|
|
|
|
|
knoten.append((name, f"Z = {zahl(z)} ≤ Incumbent {zahl(incumbent)}\\n"
|
|
|
|
|
"→ gekappt", AMBER, None))
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
j = erste_gebrochene(x)
|
|
|
|
|
if j is None:
|
|
|
|
|
besser = z > incumbent + TOL
|
|
|
|
|
if besser:
|
|
|
|
|
incumbent, incumbent_x = z, x
|
|
|
|
|
knoten.append((name,
|
|
|
|
|
f"x = ({zahl(x[0])}; {zahl(x[1])}), Z = {zahl(z)}\\n"
|
|
|
|
|
+ ("ganzzahlig — neuer Incumbent" if besser
|
|
|
|
|
else f"ganzzahlig, aber ≤ Incumbent {zahl(incumbent)}"),
|
|
|
|
|
GRUEN if besser else AMBER, z))
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
knoten.append((name,
|
|
|
|
|
f"x = ({zahl(x[0])}; {zahl(x[1])}), Schranke Z ≤ {zahl(z)}\\n"
|
|
|
|
|
f"x{j + 1} gebrochen — verzweigen", INDIGO if elter is None else CYAN,
|
|
|
|
|
z))
|
|
|
|
|
grenze = float(np.floor(x[j]))
|
|
|
|
|
links_oben = list(oben); links_oben[j] = grenze
|
|
|
|
|
rechts_unten = list(unten); rechts_unten[j] = grenze + 1
|
|
|
|
|
# Zuerst den ">="-Ast auflegen, damit der "<="-Ast oben liegt und
|
|
|
|
|
# als Erster bearbeitet wird - die Reihenfolge der Handrechnung.
|
|
|
|
|
# Namen wie im Kapiteltext: die Wurzel verzweigt in Ast A und Ast B,
|
|
|
|
|
# deren Kinder heissen A1/A2 bzw. B1/B2.
|
|
|
|
|
links, rechts = ("A", "B") if elter is None else (f"{name}1", f"{name}2")
|
|
|
|
|
stapel.append((rechts, name, rechts_unten, list(oben),
|
|
|
|
|
f"x{j + 1} ≥ {int(grenze) + 1}"))
|
|
|
|
|
stapel.append((links, name, list(unten), links_oben,
|
|
|
|
|
f"x{j + 1} ≤ {int(grenze)}"))
|
|
|
|
|
|
|
|
|
|
return knoten, kanten, incumbent_x, incumbent, zaehler
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def zeichne(knoten, kanten, bestes, zielwert, geloest) -> None:
|
|
|
|
|
punkt = graphviz.Digraph(format="svg")
|
|
|
|
|
punkt.attr(rankdir="TB", splines="polyline", nodesep="0.45", ranksep="0.55",
|
|
|
|
|
fontname="DejaVu Sans", bgcolor="transparent", fontsize="12",
|
|
|
|
|
labelloc="t",
|
|
|
|
|
label=(f"Branch-and-Bound: {geloest} gelöste LPs statt aller "
|
|
|
|
|
f"ganzzahligen Punkte\\n"
|
|
|
|
|
f"max 5x₁ + 4x₂ u.d.N. 6x₁+4x₂ ≤ 24, x₁+2x₂ ≤ 6"))
|
|
|
|
|
punkt.attr("node", fontname="DejaVu Sans", fontsize="10")
|
|
|
|
|
punkt.attr("edge", fontname="DejaVu Sans", fontsize="9", color="#5c6380")
|
|
|
|
|
|
|
|
|
|
for name, text, farbe, _ in knoten:
|
|
|
|
|
beschriftung = f"{name}\\n{text}"
|
|
|
|
|
punkt.node(name, beschriftung, shape="box", style="filled,rounded",
|
|
|
|
|
fillcolor=TINT[farbe], color=farbe, penwidth="2.0",
|
|
|
|
|
margin="0.16,0.09")
|
|
|
|
|
for von, nach, text in kanten:
|
|
|
|
|
punkt.edge(von, nach, label=text or "")
|
|
|
|
|
|
|
|
|
|
punkt.node("ergebnis",
|
|
|
|
|
f"Optimum: x* = ({zahl(bestes[0])}; {zahl(bestes[1])}), "
|
|
|
|
|
f"Z* = {zahl(zielwert)}",
|
|
|
|
|
shape="note", style="filled", fillcolor="#f8fafc",
|
|
|
|
|
color="#94a3b8", fontsize="10", margin="0.18,0.12")
|
|
|
|
|
|
|
|
|
|
os.makedirs(BILDER, exist_ok=True)
|
Aufgeraeumt: 33 PNG-Zweitfassungen, Bau-Ueberbleibsel, Ausgabepfade
Drei Aufraeumarbeiten - und zwei Funde, die dabei auffielen.
1. DIE PNG-ZWEITFASSUNGEN SIND WEG. Jedes Diagramm lag doppelt vor, als SVG
und als PNG, und kein einziges src=/href= in der Website zeigte je auf ein
PNG. Der Build kopierte sie trotzdem mit: 3,3 MB im Repository plus 3,3 MB,
die bei jeder Veroeffentlichung auf den Webserver gingen. Die 15 Generatoren
schreiben jetzt nur noch SVG, die Docstrings sind mitgezogen. Vor dem
Loeschen geprueft: Jedes PNG hatte sein gleichnamiges SVG, alle 33 waren
versioniert.
FUND 1: erzeuge_kap06_gantt.py folgte als einziger Generator nicht der
Konvention - weder svg.hashsalt noch metadata={"Date": None}. Sein SVG trug
einen echten Zeitstempel und bei jedem Lauf andere clip-path-IDs, war also
nie byteidentisch reproduzierbar, obwohl CLAUDE.md genau das fuer alle
Generatoren festhaelt. Aufgefallen nur, weil nach der PNG-Umstellung 32 von
33 SVGs bitgleich blieben und eines nicht. Jetzt byteidentisch ueber zwei
Laeufe.
FUND 2: spiegle_bilder() legte leere Verzeichnisse auf dem Webserver an. Der
Dateifilter arbeitete korrekt, aber os.walk durchlief auch __pycache__/, und
os.makedirs() erzeugte es am Ziel. Die Verzeichnisliste wird jetzt vorher
gefiltert. Gegengetestet.
2. BAU-UEBERBLEIBSEL entfernt (alle ignoriert und neu erzeugbar):
svg-inkscape/, build_v04.log, .pytest_cache/, Programme/output/, vier
__pycache__/ und die Excel-Mappen. Arbeitsbaum 36 -> 33 MB, danach null
ignorierte Ueberbleibsel.
3. Excel_Bruecke.py SCHREIBT NEBEN DAS SKRIPT statt ins
Arbeitsverzeichnis. Es benutzte blanke relative Namen; wer es aus der
Repository-Wurzel startete, verstreute dort produktionsmix.xlsx und
produktionsmix_ergebnis.xlsx. Jetzt wie die vier anderen schreibenden
Programme ueber os.path.dirname(os.path.abspath(__file__)). Nachgemessen:
Lauf aus der Wurzel legt dort null Dateien ab.
Ausserdem git gc: 653 lose Objekte gepackt, .git von 71 MB auf 28 MB -
reines Repacken, kein Inhalt beruehrt.
Geprueft: alle 33 im Buch referenzierten SVGs vorhanden, in Quelle und
Website; 16 Generatoren fehlerfrei; keine fehlenden Bilder im LaTeX-Lauf;
33 pytest-Tests; PDF unveraendert 760 Seiten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 14:22:26 +02:00
|
|
|
for endung in ("svg",):
|
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>
2026-09-08 01:20:09 +02:00
|
|
|
punkt.format = endung
|
|
|
|
|
pfad = punkt.render(os.path.join(BILDER, "kap_milp_suchbaum"), cleanup=True)
|
|
|
|
|
print(f"geschrieben: {pfad}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
knoten, kanten, bestes, zielwert, geloest = branch_and_bound()
|
|
|
|
|
print(f"{len(knoten)} Knoten, {geloest} geloeste LPs")
|
|
|
|
|
for name, text, _, _ in knoten:
|
|
|
|
|
print(f" {name:5} {text.replace(chr(92) + 'n', ' | ')}")
|
|
|
|
|
print(f"Optimum: ({bestes[0]:g}; {bestes[1]:g}), Z = {zielwert:g}")
|
|
|
|
|
|
|
|
|
|
# Gegenprobe gegen die Handrechnung im Kapitel
|
|
|
|
|
if not (np.allclose(bestes, [4.0, 0.0]) and abs(zielwert - 20.0) < TOL):
|
|
|
|
|
raise SystemExit("FEHLER: Ergebnis weicht von der Handrechnung ab "
|
|
|
|
|
"(erwartet x* = (4; 0), Z* = 20).")
|
|
|
|
|
zeichne(knoten, kanten, bestes, zielwert, geloest)
|