189 lines
7.7 KiB
Python
189 lines
7.7 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
|
||
|
|
# erzeuge_bnb_geometrie.py
|
||
|
|
"""
|
||
|
|
Erzeugt die geometrische Tafel zum Branch-and-Bound im MILP-Kapitel:
|
||
|
|
|
||
|
|
bilder_04/kap_milp_bnb_geometrie.svg
|
||
|
|
|
||
|
|
ERGAENZT den Suchbaum kap_milp_suchbaum.svg, ersetzt ihn nicht: Der Baum zeigt,
|
||
|
|
WELCHE Teilprobleme entstehen. Diese Tafel zeigt, WAS Verzweigen mit dem
|
||
|
|
Loesungsraum macht - und warum es erlaubt ist.
|
||
|
|
|
||
|
|
Links das Polyeder der LP-Relaxation mit seinem Optimum (3; 1,5). Rechts
|
||
|
|
dasselbe Polyeder, aus dem der Streifen 1 < x2 < 2 herausgeschnitten ist; uebrig
|
||
|
|
bleiben die Aeste A (x2 <= 1) und B (x2 >= 2) mit ihren eigenen Optima.
|
||
|
|
|
||
|
|
Der Punkt, auf den es ankommt: In dem herausgeschnittenen Streifen liegt KEIN
|
||
|
|
einziger ganzzahliger Punkt - es geht also nichts verloren. Der Streifen
|
||
|
|
enthaelt aber das Optimum der Relaxation, und deshalb sinkt die Schranke von 21
|
||
|
|
auf 20,67 bzw. 18. Verzweigen heisst, wertlosen Raum wegzuschneiden, bis der
|
||
|
|
beste verbleibende Punkt ganzzahlig ist.
|
||
|
|
|
||
|
|
Modell und LP-Loeser stammen aus erzeuge_branch_and_bound.py, damit Baum und
|
||
|
|
Geometrie nicht auseinanderlaufen koennen. pruefe_befund() rechnet nach, dass im
|
||
|
|
Streifen wirklich kein ganzzahliger Punkt liegt und dass die drei Zielwerte die
|
||
|
|
des Baums sind.
|
||
|
|
|
||
|
|
Aufruf (aus dem Repository-Wurzelverzeichnis):
|
||
|
|
python3 bilder_04/erzeuge_bnb_geometrie.py
|
||
|
|
|
||
|
|
Benoetigt: numpy, scipy, matplotlib
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import itertools
|
||
|
|
import os
|
||
|
|
import sys
|
||
|
|
|
||
|
|
import numpy as np
|
||
|
|
|
||
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||
|
|
from stil_04 import FARBEN, speichere # noqa: E402
|
||
|
|
import matplotlib.pyplot as plt # noqa: E402
|
||
|
|
from erzeuge_branch_and_bound import A, B, C, loese # noqa: E402
|
||
|
|
|
||
|
|
# Die Verzweigung, die der Baum als Erstes vornimmt: x2 <= 1 gegen x2 >= 2.
|
||
|
|
GRENZE = 1.0
|
||
|
|
|
||
|
|
# Was der Suchbaum ausweist; nur zum Gegenpruefen.
|
||
|
|
SCHRANKEN_IM_BAUM = {"P0": 21.0, "A": 62.0 / 3.0, "B": 18.0}
|
||
|
|
|
||
|
|
|
||
|
|
def gitter(masche: int = 420):
|
||
|
|
x1 = np.linspace(0, 4.6, masche)
|
||
|
|
x2 = np.linspace(0, 3.4, masche)
|
||
|
|
return np.meshgrid(x1, x2)
|
||
|
|
|
||
|
|
|
||
|
|
def zulaessig(xx1, xx2, unten=None, oben=None):
|
||
|
|
"""Maske des zulaessigen Bereichs, wahlweise mit zusaetzlichen Schranken."""
|
||
|
|
maske = np.ones_like(xx1, dtype=bool)
|
||
|
|
for zeile, grenze in zip(A, B):
|
||
|
|
maske &= (zeile[0] * xx1 + zeile[1] * xx2 <= grenze + 1e-9)
|
||
|
|
if unten is not None:
|
||
|
|
maske &= (xx2 >= unten - 1e-9)
|
||
|
|
if oben is not None:
|
||
|
|
maske &= (xx2 <= oben + 1e-9)
|
||
|
|
return maske
|
||
|
|
|
||
|
|
|
||
|
|
def ganzzahlige_punkte():
|
||
|
|
"""Alle ganzzahligen zulaessigen Punkte - fuer die Tafel und den Test."""
|
||
|
|
punkte = []
|
||
|
|
for p1, p2 in itertools.product(range(0, 6), range(0, 5)):
|
||
|
|
if np.all(A @ np.array([p1, p2], dtype=float) <= B + 1e-9):
|
||
|
|
punkte.append((p1, p2))
|
||
|
|
return punkte
|
||
|
|
|
||
|
|
|
||
|
|
def pruefe_befund(loesungen, punkte) -> None:
|
||
|
|
"""Die beiden Aussagen der Tafel muessen stimmen."""
|
||
|
|
im_streifen = [p for p in punkte if GRENZE < p[1] < GRENZE + 1]
|
||
|
|
if im_streifen:
|
||
|
|
raise SystemExit(
|
||
|
|
f"B&B-Geometrie: Im herausgeschnittenen Streifen liegen die "
|
||
|
|
f"ganzzahligen Punkte {im_streifen}. Dann waere das Verzweigen "
|
||
|
|
f"nicht verlustfrei, und die Tafel behauptete etwas Falsches.")
|
||
|
|
for name, soll in SCHRANKEN_IM_BAUM.items():
|
||
|
|
ist = loesungen[name][1]
|
||
|
|
if abs(ist - soll) > 1e-6:
|
||
|
|
raise SystemExit(
|
||
|
|
f"B&B-Geometrie: {name} hat den Zielwert {ist:.4f}, der "
|
||
|
|
f"Suchbaum weist {soll:.4f} aus.")
|
||
|
|
|
||
|
|
|
||
|
|
def zahl(wert: float) -> str:
|
||
|
|
return f"{wert:.2f}".rstrip("0").rstrip(".").replace(".", ",")
|
||
|
|
|
||
|
|
|
||
|
|
def zeichne(loesungen, punkte) -> None:
|
||
|
|
figur, achsen = plt.subplots(1, 2, figsize=(10.0, 4.5))
|
||
|
|
xx1, xx2 = gitter()
|
||
|
|
|
||
|
|
for achse in achsen:
|
||
|
|
for p1, p2 in punkte:
|
||
|
|
achse.plot(p1, p2, "o", color=FARBEN["gedaempft"], markersize=4.5,
|
||
|
|
zorder=5)
|
||
|
|
for zeile, grenze in zip(A, B):
|
||
|
|
x = np.linspace(0, 4.6, 100)
|
||
|
|
achse.plot(x, (grenze - zeile[0] * x) / zeile[1], "--",
|
||
|
|
color=FARBEN["linie"], linewidth=1.1, zorder=3)
|
||
|
|
achse.set_xlim(0, 4.6)
|
||
|
|
achse.set_ylim(0, 3.4)
|
||
|
|
achse.set_xlabel("$x_1$")
|
||
|
|
achse.grid(linestyle=":", alpha=0.4)
|
||
|
|
|
||
|
|
achsen[0].set_ylabel("$x_2$")
|
||
|
|
|
||
|
|
# --- links: die Relaxation, ungeschnitten -----------------------------
|
||
|
|
achsen[0].contourf(xx1, xx2, zulaessig(xx1, xx2).astype(float),
|
||
|
|
levels=[0.5, 1.5], colors=[FARBEN["haupt"]],
|
||
|
|
alpha=0.20, zorder=1)
|
||
|
|
x_p0, z_p0 = loesungen["P0"]
|
||
|
|
achsen[0].plot(*x_p0, "*", color=FARBEN["fehler"], markersize=17, zorder=6)
|
||
|
|
achsen[0].annotate(f"LP-Optimum ({zahl(x_p0[0])}; {zahl(x_p0[1])})\n"
|
||
|
|
f"Z = {zahl(z_p0)} — nicht ganzzahlig",
|
||
|
|
xy=x_p0, xytext=(12, 12), textcoords="offset points",
|
||
|
|
fontsize=8.5, color=FARBEN["fehler"],
|
||
|
|
fontweight="bold", zorder=7)
|
||
|
|
achsen[0].set_title("$P_0$ — die Relaxation", fontsize=10.5,
|
||
|
|
color=FARBEN["text"])
|
||
|
|
|
||
|
|
# --- rechts: derselbe Bereich, der Streifen heraus ---------------------
|
||
|
|
for name, unten, oben, farbe, beschriftung in (
|
||
|
|
("A", None, GRENZE, FARBEN["zweit"], f"Ast A: $x_2 \\leq 1$"),
|
||
|
|
("B", GRENZE + 1, None, FARBEN["warnung"],
|
||
|
|
f"Ast B: $x_2 \\geq 2$")):
|
||
|
|
achsen[1].contourf(xx1, xx2,
|
||
|
|
zulaessig(xx1, xx2, unten, oben).astype(float),
|
||
|
|
levels=[0.5, 1.5], colors=[farbe], alpha=0.22,
|
||
|
|
zorder=1)
|
||
|
|
x_ast, z_ast = loesungen[name]
|
||
|
|
achsen[1].plot(*x_ast, "*", color=farbe, markersize=15, zorder=6)
|
||
|
|
# Ast A liegt an der Schnittkante; sein Text gehoert nach unten in die
|
||
|
|
# eigene Flaeche, sonst laeuft er in den Streifen hinein.
|
||
|
|
achsen[1].annotate(f"{beschriftung}\nZ = {zahl(z_ast)}",
|
||
|
|
xy=x_ast,
|
||
|
|
xytext=(-12, -30) if name == "A" else (12, 14),
|
||
|
|
textcoords="offset points", fontsize=8.5,
|
||
|
|
ha="right" if name == "A" else "left",
|
||
|
|
color=farbe, fontweight="bold", zorder=7)
|
||
|
|
|
||
|
|
achsen[1].axhspan(GRENZE, GRENZE + 1, color=FARBEN["fehler"], alpha=0.11,
|
||
|
|
zorder=2)
|
||
|
|
achsen[1].axhline(GRENZE, color=FARBEN["fehler"], linewidth=1.3, zorder=4)
|
||
|
|
achsen[1].axhline(GRENZE + 1, color=FARBEN["fehler"], linewidth=1.3,
|
||
|
|
zorder=4)
|
||
|
|
achsen[1].text(4.5, GRENZE + 0.5,
|
||
|
|
"herausgeschnitten:\n$1 < x_2 < 2$\nkein ganzzahliger "
|
||
|
|
"Punkt darin", fontsize=8, color=FARBEN["fehler"],
|
||
|
|
ha="right", va="center", zorder=7)
|
||
|
|
achsen[1].set_title("nach dem Verzweigen über $x_2$", fontsize=10.5,
|
||
|
|
color=FARBEN["text"])
|
||
|
|
|
||
|
|
figur.suptitle(f"Verzweigen schneidet weg, wo kein ganzzahliger Punkt "
|
||
|
|
f"liegt — die Schranke sinkt von {zahl(z_p0)} auf "
|
||
|
|
f"{zahl(max(loesungen['A'][1], loesungen['B'][1]))}",
|
||
|
|
fontsize=10.5, color=FARBEN["text"], y=1.02)
|
||
|
|
speichere(figur, "kap_milp_bnb_geometrie")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
unendlich = float("inf")
|
||
|
|
loesungen = {
|
||
|
|
"P0": loese([0.0, 0.0], [unendlich, unendlich]),
|
||
|
|
"A": loese([0.0, 0.0], [unendlich, GRENZE]),
|
||
|
|
"B": loese([0.0, GRENZE + 1], [unendlich, unendlich]),
|
||
|
|
}
|
||
|
|
punkte = ganzzahlige_punkte()
|
||
|
|
for name, (x, z) in loesungen.items():
|
||
|
|
print(f" {name:<3} x = ({x[0]:.4f}; {x[1]:.4f}) Z = {z:.4f}")
|
||
|
|
print(f" {len(punkte)} ganzzahlige zulässige Punkte")
|
||
|
|
|
||
|
|
pruefe_befund(loesungen, punkte)
|
||
|
|
print(" Streifen 1 < x₂ < 2 enthält keinen ganzzahligen Punkt")
|
||
|
|
print(" Zielwerte stimmen mit dem Suchbaum überein")
|
||
|
|
|
||
|
|
zeichne(loesungen, punkte)
|