#!/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)