Phase 8.2: Solver-Isolation ohne subprocess-Codestrings
Setzt den Isolationsteil von Paket 1 aus Verbesserungen_02.md um. Der Plan
nannte zwei Programme; beim Suchen kam ein drittes dazu, das dasselbe Muster
verwendete.
Ein_System_Vier_Ansaetze.py und Benchmark_Skalierung.py hielten ihre vier
Solvervarianten als Zeichenketten in einem Dictionary und gaben sie an
"python -c" weiter - bei Benchmark_Skalierung.py sogar mit
.format()-Platzhaltern fuer die Instanzgroesse. Aus jeder Variante ist jetzt
eine gewoehnliche Funktion mit lokalem Import geworden.
Solverwechsel_CPSAT_HiGHS.py rief sich selbst ueber sys.argv erneut auf;
auch das entfaellt.
Ausgefuehrt wird ueber einen ProcessPoolExecutor mit zwei Einstellungen, die
beide noetig sind: mp_context "spawn" (frischer Interpreter statt geerbtem
Speicher - unter Linux ist fork der Standard) und max_tasks_per_child=1 (ein
neuer Prozess je Aufgabe; ohne das verwendet der Pool seinen Arbeiter
wieder, und beim zweiten Solver ist der Konflikt zurueck). Nachgemessen:
vier Aufgaben, vier verschiedene PIDs.
Der zweite Punkt hat einen eigenen Warnkasten bekommen, weil der Fehler
leicht zu machen und schwer zu finden ist: Der Absturz kaeme nicht beim
ersten Solver, sondern beim zweiten - und saehe aus wie ein Problem des
zweiten.
Regel 4, dreifach geprueft. Ein_System_Vier_Ansaetze.py: identisch bis auf
die Zeitspalte, einschliesslich der Spannweite 2,41e-08, auf die sich der
Merksatz des Kapitels beruft. Benchmark_Skalierung.py: alle zwoelf
Zielwerte und alle drei Spannweiten bitgleich; Zeiten und Speicher haben
sich verschoben, beide sind im Abdruck seit jeher als hardwareabhaengig
gekennzeichnet. Solverwechsel_CPSAT_HiGHS.py: Ausgabe ohne Zeiten
unveraendert.
Bewusst subprocess bleibt Mutationstest.py: Dort wird pytest auf einer
mutierten Kopie in einem temporaeren Verzeichnis gestartet - ein externes
Werkzeug auf veraenderten Dateien, nicht die Isolation eines Imports.
Neu im Kapitel Oekosystem: ein Abschnitt "Wie die Isolation aussieht, wenn
sie tragen soll" - warum ein Codestring die schlechteste Umsetzung von
"eigener Prozess" ist. Anhang C nennt jetzt ebenfalls ProcessPoolExecutor.
Ein eigener Fehler, gefunden und abgesichert: Ich hatte dem neuen ### ein
{#sec:...}-Label gegeben. ABSCHNITT_RE erkennt nur "## " - das Label waere
nie registriert worden und jeder Verweis darauf ins Leere gelaufen, ohne
Warnung. Label entfernt, --check meldet den Fall jetzt. Gegengetestet.
Stand: 818 Querverweise, 76 Programme, 33 pytest-Tests, PDF 760 Seiten, 69
netzfreie Programme fehlerfrei.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
9c83716095
commit
862e92bc7b
29 changed files with 2401 additions and 1912 deletions
|
|
@ -115,7 +115,7 @@
|
|||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Ein System — vier Programmieransätze\n",
|
||||
"## Wie die Isolation aussieht, wenn sie tragen soll\n",
|
||||
"\n",
|
||||
"`Ein_System_Vier_Ansaetze.py`\n"
|
||||
]
|
||||
|
|
@ -136,85 +136,104 @@
|
|||
" 2*x1 + 3*x2 + x3 <= 50\n",
|
||||
" x >= 0\n",
|
||||
"\n",
|
||||
"Deckt scipy.optimize, highspy, CVXPY und OR-Tools/GLOP ab, mit Kreuzvergleich am Ende.\n",
|
||||
"Deckt scipy.optimize, highspy, CVXPY und OR-Tools/GLOP ab, mit Kreuzvergleich\n",
|
||||
"am Ende.\n",
|
||||
"\n",
|
||||
"WICHTIG: Jeder Solver läuft in einem EIGENEN Prozess, weil sich ortools und\n",
|
||||
"WICHTIG: Jeder Solver laeuft in einem EIGENEN Prozess, weil sich ortools und\n",
|
||||
"highspy auf vielen Systemen nicht gemeinsam importieren lassen (beide bringen\n",
|
||||
"eine eigene HiGHS-Kopie mit -> Symbolkonflikt).\n",
|
||||
"\n",
|
||||
"Die Isolation besorgt ein ProcessPoolExecutor. Drei Einstellungen ergeben\n",
|
||||
"zusammen die Garantie:\n",
|
||||
"\n",
|
||||
" mp_context \"spawn\" Der Kindprozess startet mit einem FRISCHEN\n",
|
||||
" Interpreter, statt den Speicher des Elternprozesses\n",
|
||||
" zu erben. Was hier schon importiert ist, ist dort\n",
|
||||
" nicht importiert. Mit dem Standard \"fork\" auf Linux\n",
|
||||
" waere das nicht so.\n",
|
||||
" max_tasks_per_child=1 Jede Aufgabe bekommt einen NEUEN Prozess. Ohne das\n",
|
||||
" wuerde der Pool seinen Arbeiter wiederverwenden - und\n",
|
||||
" beim zweiten Solver waere der Konflikt zurueck.\n",
|
||||
" max_workers=1 Haelt die vier Laeufe nacheinander. Nicht aus\n",
|
||||
" Vorsicht, sondern damit die gemessenen Zeiten\n",
|
||||
" vergleichbar bleiben.\n",
|
||||
"\n",
|
||||
"Jeder Solver steht in einer eigenen Funktion mit LOKALEM Import. Das ist der\n",
|
||||
"Unterschied zu einem Codestring, den man an 'python -c' uebergibt: Die\n",
|
||||
"Funktion laesst sich einzeln aufrufen, testen und vom Editor pruefen - ein\n",
|
||||
"String nicht.\n",
|
||||
"\n",
|
||||
"Benoetigt: scipy, highspy, cvxpy, ortools\n",
|
||||
"\"\"\"\n",
|
||||
"\n",
|
||||
"import json\n",
|
||||
"import subprocess\n",
|
||||
"import sys\n",
|
||||
"import textwrap\n",
|
||||
"import multiprocessing\n",
|
||||
"import time\n",
|
||||
"from concurrent.futures import ProcessPoolExecutor\n",
|
||||
"\n",
|
||||
"ERWARTET = 530.0 # Ergebnis der Handrechnung zum Produktionsprogramm\n",
|
||||
"\n",
|
||||
"# Jeder Eintrag ist ein eigenständiges Miniprogramm, das sein Ergebnis als\n",
|
||||
"# JSON auf stdout ausgibt. So bleibt jeder Import in seinem eigenen Prozess.\n",
|
||||
"ANSAETZE: dict[str, str] = {\n",
|
||||
"# Die Instanz - einmal notiert, von allen vier Funktionen benutzt.\n",
|
||||
"ZIEL = [10.0, 15.0, 25.0]\n",
|
||||
"MATRIX = [[1, 1, 2], [2, 3, 1]]\n",
|
||||
"KAPAZITAET = [40.0, 50.0]\n",
|
||||
"\n",
|
||||
" \"scipy.optimize.linprog\": \"\"\"\n",
|
||||
" from scipy.optimize import linprog\n",
|
||||
" res = linprog(c=[-10.0, -15.0, -25.0], # linprog MINIMIERT -> negieren\n",
|
||||
" A_ub=[[1, 1, 2], [2, 3, 1]], b_ub=[40, 50],\n",
|
||||
" bounds=[(0, None)] * 3, method=\"highs\")\n",
|
||||
" ausgabe = (-res.fun, list(res.x))\n",
|
||||
" \"\"\",\n",
|
||||
"\n",
|
||||
" \"highspy (natives HiGHS)\": \"\"\"\n",
|
||||
" import numpy as np, highspy\n",
|
||||
" h = highspy.Highs()\n",
|
||||
" h.setOptionValue(\"output_flag\", False)\n",
|
||||
" h.addVars(3, np.zeros(3), np.full(3, highspy.kHighsInf))\n",
|
||||
" h.changeObjectiveSense(highspy.ObjSense.kMaximize)\n",
|
||||
" for j, wert in enumerate([10.0, 15.0, 25.0]):\n",
|
||||
" h.changeColCost(j, wert)\n",
|
||||
" # CSR-Format: starts[i] = Beginn von Zeile i in indices/values\n",
|
||||
" h.addRows(2, np.full(2, -highspy.kHighsInf), np.array([40.0, 50.0]), 6,\n",
|
||||
" np.array([0, 3], dtype=np.int32),\n",
|
||||
" np.array([0, 1, 2, 0, 1, 2], dtype=np.int32),\n",
|
||||
" np.array([1.0, 1.0, 2.0, 2.0, 3.0, 1.0]))\n",
|
||||
" h.run()\n",
|
||||
" ausgabe = (h.getInfo().objective_function_value,\n",
|
||||
" list(h.getSolution().col_value[:3]))\n",
|
||||
" \"\"\",\n",
|
||||
"def loese_mit_scipy() -> tuple[float, list[float]]:\n",
|
||||
" from scipy.optimize import linprog\n",
|
||||
" ergebnis = linprog(c=[-w for w in ZIEL], # linprog MINIMIERT -> negieren\n",
|
||||
" A_ub=MATRIX, b_ub=KAPAZITAET,\n",
|
||||
" bounds=[(0, None)] * 3, method=\"highs\")\n",
|
||||
" return -ergebnis.fun, list(ergebnis.x)\n",
|
||||
"\n",
|
||||
" \"cvxpy\": \"\"\"\n",
|
||||
" import numpy as np, cvxpy as cp\n",
|
||||
" x = cp.Variable(3, nonneg=True)\n",
|
||||
" problem = cp.Problem(cp.Maximize(np.array([10.0, 15.0, 25.0]) @ x),\n",
|
||||
" [np.array([[1, 1, 2], [2, 3, 1]]) @ x <= np.array([40, 50])])\n",
|
||||
" problem.solve()\n",
|
||||
" ausgabe = (float(problem.value), [float(v) for v in x.value])\n",
|
||||
" \"\"\",\n",
|
||||
"\n",
|
||||
" \"ortools / GLOP\": \"\"\"\n",
|
||||
" from ortools.linear_solver import pywraplp\n",
|
||||
" s = pywraplp.Solver.CreateSolver(\"GLOP\")\n",
|
||||
" x = [s.NumVar(0, s.infinity(), f\"x{j+1}\") for j in range(3)]\n",
|
||||
" A = [[1, 1, 2], [2, 3, 1]]\n",
|
||||
" for i, kap in enumerate([40, 50]):\n",
|
||||
" s.Add(sum(A[i][j] * x[j] for j in range(3)) <= kap)\n",
|
||||
" s.Maximize(10 * x[0] + 15 * x[1] + 25 * x[2])\n",
|
||||
" s.Solve()\n",
|
||||
" ausgabe = (s.Objective().Value(), [v.solution_value() for v in x])\n",
|
||||
" \"\"\",\n",
|
||||
"def loese_mit_highspy() -> tuple[float, list[float]]:\n",
|
||||
" import highspy\n",
|
||||
" import numpy as np\n",
|
||||
" h = highspy.Highs()\n",
|
||||
" h.setOptionValue(\"output_flag\", False)\n",
|
||||
" h.addVars(3, np.zeros(3), np.full(3, highspy.kHighsInf))\n",
|
||||
" h.changeObjectiveSense(highspy.ObjSense.kMaximize)\n",
|
||||
" for j, wert in enumerate(ZIEL):\n",
|
||||
" h.changeColCost(j, wert)\n",
|
||||
" # CSR-Format: starts[i] = Beginn von Zeile i in indices/values\n",
|
||||
" h.addRows(2, np.full(2, -highspy.kHighsInf), np.array(KAPAZITAET), 6,\n",
|
||||
" np.array([0, 3], dtype=np.int32),\n",
|
||||
" np.array([0, 1, 2, 0, 1, 2], dtype=np.int32),\n",
|
||||
" np.array([float(w) for zeile in MATRIX for w in zeile]))\n",
|
||||
" h.run()\n",
|
||||
" return (h.getInfo().objective_function_value,\n",
|
||||
" list(h.getSolution().col_value[:3]))\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def loese_mit_cvxpy() -> tuple[float, list[float]]:\n",
|
||||
" import cvxpy as cp\n",
|
||||
" import numpy as np\n",
|
||||
" x = cp.Variable(3, nonneg=True)\n",
|
||||
" problem = cp.Problem(cp.Maximize(np.array(ZIEL) @ x),\n",
|
||||
" [np.array(MATRIX) @ x <= np.array(KAPAZITAET)])\n",
|
||||
" problem.solve()\n",
|
||||
" return float(problem.value), [float(v) for v in x.value]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def loese_mit_ortools() -> tuple[float, list[float]]:\n",
|
||||
" from ortools.linear_solver import pywraplp\n",
|
||||
" s = pywraplp.Solver.CreateSolver(\"GLOP\")\n",
|
||||
" x = [s.NumVar(0, s.infinity(), f\"x{j+1}\") for j in range(3)]\n",
|
||||
" for i, kapazitaet in enumerate(KAPAZITAET):\n",
|
||||
" s.Add(sum(MATRIX[i][j] * x[j] for j in range(3)) <= kapazitaet)\n",
|
||||
" s.Maximize(sum(ZIEL[j] * x[j] for j in range(3)))\n",
|
||||
" s.Solve()\n",
|
||||
" return s.Objective().Value(), [v.solution_value() for v in x]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"ANSAETZE = {\n",
|
||||
" \"scipy.optimize.linprog\": loese_mit_scipy,\n",
|
||||
" \"highspy (natives HiGHS)\": loese_mit_highspy,\n",
|
||||
" \"cvxpy\": loese_mit_cvxpy,\n",
|
||||
" \"ortools / GLOP\": loese_mit_ortools,\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def fuehre_in_eigenem_prozess_aus(quelltext: str) -> tuple[float, list[float]]:\n",
|
||||
" \"\"\"Startet den Codeschnipsel als separaten Python-Prozess und liest das Ergebnis.\"\"\"\n",
|
||||
" programm = textwrap.dedent(quelltext) + \"\\nimport json; print(json.dumps(ausgabe))\\n\"\n",
|
||||
" ergebnis = subprocess.run([sys.executable, \"-c\", programm],\n",
|
||||
" capture_output=True, text=True, timeout=120)\n",
|
||||
" if ergebnis.returncode != 0:\n",
|
||||
" raise RuntimeError(ergebnis.stderr.strip().splitlines()[-1])\n",
|
||||
" wert, loesung = json.loads(ergebnis.stdout.strip().splitlines()[-1])\n",
|
||||
" return wert, loesung\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"if __name__ == \"__main__\":\n",
|
||||
" print(\"=\" * 78)\n",
|
||||
" print(\" EIN SYSTEM - VIER ANSAETZE (je eigener Prozess)\")\n",
|
||||
|
|
@ -223,17 +242,23 @@
|
|||
" print(\"-\" * 78)\n",
|
||||
"\n",
|
||||
" werte = []\n",
|
||||
" for name, quelltext in ANSAETZE.items():\n",
|
||||
" t0 = time.perf_counter()\n",
|
||||
" try:\n",
|
||||
" wert, x = fuehre_in_eigenem_prozess_aus(quelltext)\n",
|
||||
" except RuntimeError as fehler:\n",
|
||||
" print(f\"{name:<26} nicht verfuegbar: {fehler[:40]}\")\n",
|
||||
" continue\n",
|
||||
" dauer = time.perf_counter() - t0\n",
|
||||
" werte.append(wert)\n",
|
||||
" print(f\"{name:<26} {wert:>10.2f} {x[0]:>7.2f} {x[1]:>7.2f} {x[2]:>7.2f} \"\n",
|
||||
" f\"{dauer:>8.2f} s\")\n",
|
||||
" # Ein Pool, vier Aufgaben, vier frische Prozesse. Der Kontext muss\n",
|
||||
" # \"spawn\" sein - siehe Modulkommentar.\n",
|
||||
" with ProcessPoolExecutor(\n",
|
||||
" max_workers=1,\n",
|
||||
" mp_context=multiprocessing.get_context(\"spawn\"),\n",
|
||||
" max_tasks_per_child=1) as pool:\n",
|
||||
" for name, funktion in ANSAETZE.items():\n",
|
||||
" beginn = time.perf_counter()\n",
|
||||
" try:\n",
|
||||
" wert, x = pool.submit(funktion).result(timeout=120)\n",
|
||||
" except Exception as fehler: # Bibliothek fehlt o. Ae.\n",
|
||||
" print(f\"{name:<26} nicht verfuegbar: {str(fehler)[:40]}\")\n",
|
||||
" continue\n",
|
||||
" dauer = time.perf_counter() - beginn\n",
|
||||
" werte.append(wert)\n",
|
||||
" print(f\"{name:<26} {wert:>10.2f} {x[0]:>7.2f} {x[1]:>7.2f} {x[2]:>7.2f} \"\n",
|
||||
" f\"{dauer:>8.2f} s\")\n",
|
||||
"\n",
|
||||
" print(\"-\" * 78)\n",
|
||||
" spanne = max(werte) - min(werte)\n",
|
||||
|
|
@ -243,8 +268,8 @@
|
|||
" assert spanne < 1e-6, \"Die Bibliotheken widersprechen sich!\"\n",
|
||||
" assert abs(werte[0] - ERWARTET) < 1e-6, \"Ergebnis weicht von der Handrechnung ab!\"\n",
|
||||
" print(\"Alle Wege fuehren zum selben, von Hand bestaetigten Optimum.\")\n",
|
||||
" print(\"(Die Zeiten enthalten den Prozessstart und den Import - sie messen\")\n",
|
||||
" print(\" NICHT die reine Solverleistung, siehe Uebung 3.5.)\")\n",
|
||||
" print(\"(Die Zeiten enthalten Prozessstart und Import - sie messen NICHT die\")\n",
|
||||
" print(\" reine Solverleistung. Die Uebungsaufgabe 'Laufzeitvergleich' trennt beides.)\")\n",
|
||||
" print(\"=\" * 78)"
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1148,9 +1148,9 @@
|
|||
"\n",
|
||||
"from __future__ import annotations\n",
|
||||
"\n",
|
||||
"import subprocess\n",
|
||||
"import sys\n",
|
||||
"import multiprocessing\n",
|
||||
"import time\n",
|
||||
"from concurrent.futures import ProcessPoolExecutor\n",
|
||||
"\n",
|
||||
"import numpy as np\n",
|
||||
"from pydantic import BaseModel, Field, model_validator\n",
|
||||
|
|
@ -1353,25 +1353,29 @@
|
|||
" if any(loesung.werte[problem.schluessel(i, j)] > 0.5 for j in range(m))]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def loese_in_eigenem_prozess(name: str) -> Loesung:\n",
|
||||
" \"\"\"Startet dieses Programm noch einmal - mit genau einem Solverimport.\"\"\"\n",
|
||||
" ergebnis = subprocess.run([sys.executable, __file__, name],\n",
|
||||
" capture_output=True, text=True, timeout=300)\n",
|
||||
" if ergebnis.returncode != 0:\n",
|
||||
" raise RuntimeError(ergebnis.stderr.strip().splitlines()[-1])\n",
|
||||
" # Das DTO als JSON - genau dafuer ist ein Datenobjekt ohne Solverbezug gut.\n",
|
||||
" return Loesung.model_validate_json(ergebnis.stdout.strip().splitlines()[-1])\n",
|
||||
"def loese_in_eigenem_prozess(name: str, problem: Standortproblem) -> Loesung:\n",
|
||||
" \"\"\"Laesst genau einen Modellbauer in einem frischen Prozess rechnen.\n",
|
||||
"\n",
|
||||
" 'spawn' statt des Linux-Standards 'fork': Der Kindprozess startet mit\n",
|
||||
" einem leeren Interpreter und importiert nur den Solver, den SEIN\n",
|
||||
" Modellbauer braucht. max_tasks_per_child=1 sorgt dafuer, dass der Pool\n",
|
||||
" seinen Arbeiter nicht wiederverwendet - sonst saessen beim zweiten Aufruf\n",
|
||||
" wieder beide Bibliotheken im selben Prozess.\n",
|
||||
"\n",
|
||||
" Hin und zurueck wandert das Domaenenmodell bzw. das Loesungs-DTO. Beide\n",
|
||||
" kennen keinen Solver, sind also serialisierbar - genau dafuer sind sie da.\n",
|
||||
" \"\"\"\n",
|
||||
" with ProcessPoolExecutor(\n",
|
||||
" max_workers=1,\n",
|
||||
" mp_context=multiprocessing.get_context(\"spawn\"),\n",
|
||||
" max_tasks_per_child=1) as pool:\n",
|
||||
" return pool.submit(MODELLBAUER[name], problem).result(timeout=300)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"if __name__ == \"__main__\":\n",
|
||||
" problem = beispielproblem()\n",
|
||||
"\n",
|
||||
" # --- Kindprozess: rechnen und das DTO als JSON ausgeben ---------------\n",
|
||||
" if len(sys.argv) > 1:\n",
|
||||
" print(MODELLBAUER[sys.argv[1]](problem).model_dump_json())\n",
|
||||
" sys.exit(0)\n",
|
||||
"\n",
|
||||
" # --- Hauptprozess: beide Solver anstossen und vergleichen -------------\n",
|
||||
" # --- Beide Solver anstossen und vergleichen ---------------------------\n",
|
||||
" print(\"=\" * 82)\n",
|
||||
" print(\" DERSELBE FALL, ZWEI SOLVER - UND EIN AUSWERTUNGSCODE\")\n",
|
||||
" print(\"=\" * 82)\n",
|
||||
|
|
@ -1383,7 +1387,7 @@
|
|||
" loesungen: dict[str, Loesung] = {}\n",
|
||||
" for name, beschriftung in [(\"cpsat\", \"OR-Tools CP-SAT\"),\n",
|
||||
" (\"highs\", \"HiGHS (highspy)\")]:\n",
|
||||
" loesung = loesungen[name] = loese_in_eigenem_prozess(name)\n",
|
||||
" loesung = loesungen[name] = loese_in_eigenem_prozess(name, problem)\n",
|
||||
" beanstandungen = pruefe_zuordnung(problem, loesung)\n",
|
||||
"\n",
|
||||
" print(f\"{beschriftung}\")\n",
|
||||
|
|
|
|||
|
|
@ -593,113 +593,124 @@
|
|||
"\n",
|
||||
"from __future__ import annotations\n",
|
||||
"\n",
|
||||
"import json\n",
|
||||
"import subprocess\n",
|
||||
"import sys\n",
|
||||
"import textwrap\n",
|
||||
"import multiprocessing\n",
|
||||
"import resource\n",
|
||||
"import time\n",
|
||||
"from concurrent.futures import ProcessPoolExecutor\n",
|
||||
"\n",
|
||||
"import numpy as np\n",
|
||||
"\n",
|
||||
"GROESSEN = [(10, 10), (32, 32), (100, 100)] # (Lager, Kunden) -> 100 / 1.024 / 10.000 Variablen\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Jeder Eintrag ist ein eigenstaendiges Programm: Instanz aufbauen, loesen,\n",
|
||||
"# Ergebnis als JSON ausgeben. Die Instanz wird in jedem Kindprozess aus\n",
|
||||
"# derselben Saat neu erzeugt - so reist nichts ueber die Prozessgrenze,\n",
|
||||
"# was das Ergebnis verfaelschen koennte.\n",
|
||||
"VORSPANN = \"\"\"\n",
|
||||
"import json, time, resource\n",
|
||||
"import numpy as np\n",
|
||||
"# Instanz und Speichermessung stehen als gewoehnliche Funktionen hier - nicht\n",
|
||||
"# in einem String, den ein Kindprozess ausfuehrt. Jede Messfunktion baut die\n",
|
||||
"# Instanz aus derselben Saat neu auf, damit ueber die Prozessgrenze nichts\n",
|
||||
"# reist, was das Ergebnis verfaelschen koennte.\n",
|
||||
"\n",
|
||||
"def instanz(m, n):\n",
|
||||
"def instanz(m: int, n: int):\n",
|
||||
" rng = np.random.default_rng(20)\n",
|
||||
" kosten = rng.integers(5, 95, (m, n)).astype(float)\n",
|
||||
" angebot = rng.integers(50, 150, m).astype(float)\n",
|
||||
" bedarf = angebot.sum() * rng.dirichlet(np.ones(n))\n",
|
||||
" return kosten, angebot, bedarf\n",
|
||||
"\n",
|
||||
"def speicher_mb():\n",
|
||||
" # ru_maxrss ist unter Linux in Kilobyte\n",
|
||||
"\n",
|
||||
"def speicher_mb() -> float:\n",
|
||||
" # ru_maxrss ist unter Linux in Kilobyte. Gemessen wird der Kindprozess -\n",
|
||||
" # deshalb muss jede Messung einen eigenen bekommen.\n",
|
||||
" return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024\n",
|
||||
"\n",
|
||||
"M, N = {m}, {n}\n",
|
||||
"kosten, angebot, bedarf = instanz(M, N)\n",
|
||||
"\"\"\"\n",
|
||||
"\n",
|
||||
"ANSAETZE = {\n",
|
||||
" \"scipy.linprog\": \"\"\"\n",
|
||||
" from scipy.optimize import linprog\n",
|
||||
" t0 = time.perf_counter()\n",
|
||||
" c = kosten.reshape(-1)\n",
|
||||
" A_ub = np.zeros((M, M * N)); A_eq = np.zeros((N, M * N))\n",
|
||||
" for i in range(M):\n",
|
||||
" A_ub[i, i * N:(i + 1) * N] = 1.0\n",
|
||||
" for j in range(N):\n",
|
||||
" A_eq[j, j::N] = 1.0\n",
|
||||
" aufbau = time.perf_counter() - t0\n",
|
||||
" t0 = time.perf_counter()\n",
|
||||
" r = linprog(c=c, A_ub=A_ub, b_ub=angebot, A_eq=A_eq, b_eq=bedarf,\n",
|
||||
" bounds=(0, None), method=\"highs\")\n",
|
||||
" loesen = time.perf_counter() - t0\n",
|
||||
" ausgabe = (float(r.fun), aufbau, loesen, speicher_mb())\n",
|
||||
" \"\"\",\n",
|
||||
"\n",
|
||||
" \"highspy\": \"\"\"\n",
|
||||
" import highspy\n",
|
||||
" t0 = time.perf_counter()\n",
|
||||
" h = highspy.Highs(); h.setOptionValue(\"output_flag\", False)\n",
|
||||
" h.addVars(M * N, np.zeros(M * N), np.full(M * N, highspy.kHighsInf))\n",
|
||||
" for k in range(M * N):\n",
|
||||
" h.changeColCost(k, float(kosten.reshape(-1)[k]))\n",
|
||||
" for i in range(M):\n",
|
||||
" idx = np.arange(i * N, (i + 1) * N, dtype=np.int32)\n",
|
||||
" h.addRow(-highspy.kHighsInf, float(angebot[i]), N, idx, np.ones(N))\n",
|
||||
" for j in range(N):\n",
|
||||
" idx = np.arange(j, M * N, N, dtype=np.int32)\n",
|
||||
" h.addRow(float(bedarf[j]), float(bedarf[j]), M, idx, np.ones(M))\n",
|
||||
" aufbau = time.perf_counter() - t0\n",
|
||||
" t0 = time.perf_counter(); h.run(); loesen = time.perf_counter() - t0\n",
|
||||
" ausgabe = (h.getInfo().objective_function_value, aufbau, loesen, speicher_mb())\n",
|
||||
" \"\"\",\n",
|
||||
"\n",
|
||||
" \"ortools/GLOP\": \"\"\"\n",
|
||||
" from ortools.linear_solver import pywraplp\n",
|
||||
" t0 = time.perf_counter()\n",
|
||||
" s = pywraplp.Solver.CreateSolver(\"GLOP\")\n",
|
||||
" x = [[s.NumVar(0, s.infinity(), f\"x{i}_{j}\") for j in range(N)]\n",
|
||||
" for i in range(M)]\n",
|
||||
" for i in range(M):\n",
|
||||
" s.Add(sum(x[i]) <= float(angebot[i]))\n",
|
||||
" for j in range(N):\n",
|
||||
" s.Add(sum(x[i][j] for i in range(M)) == float(bedarf[j]))\n",
|
||||
" s.Minimize(sum(float(kosten[i, j]) * x[i][j]\n",
|
||||
" for i in range(M) for j in range(N)))\n",
|
||||
" aufbau = time.perf_counter() - t0\n",
|
||||
" t0 = time.perf_counter(); s.Solve(); loesen = time.perf_counter() - t0\n",
|
||||
" ausgabe = (s.Objective().Value(), aufbau, loesen, speicher_mb())\n",
|
||||
" \"\"\",\n",
|
||||
"\n",
|
||||
" \"cvxpy\": \"\"\"\n",
|
||||
" import cvxpy as cp\n",
|
||||
" t0 = time.perf_counter()\n",
|
||||
" x = cp.Variable((M, N), nonneg=True)\n",
|
||||
" problem = cp.Problem(cp.Minimize(cp.sum(cp.multiply(kosten, x))),\n",
|
||||
" [cp.sum(x, axis=1) <= angebot,\n",
|
||||
" cp.sum(x, axis=0) == bedarf])\n",
|
||||
" aufbau = time.perf_counter() - t0\n",
|
||||
" t0 = time.perf_counter(); problem.solve(); loesen = time.perf_counter() - t0\n",
|
||||
" ausgabe = (float(problem.value), aufbau, loesen, speicher_mb())\n",
|
||||
" \"\"\",\n",
|
||||
"}\n",
|
||||
"def messe_scipy(m: int, n: int):\n",
|
||||
" from scipy.optimize import linprog\n",
|
||||
" kosten, angebot, bedarf = instanz(m, n)\n",
|
||||
" t0 = time.perf_counter()\n",
|
||||
" c = kosten.reshape(-1)\n",
|
||||
" A_ub = np.zeros((m, m * n)); A_eq = np.zeros((n, m * n))\n",
|
||||
" for i in range(m):\n",
|
||||
" A_ub[i, i * n:(i + 1) * n] = 1.0\n",
|
||||
" for j in range(n):\n",
|
||||
" A_eq[j, j::n] = 1.0\n",
|
||||
" aufbau = time.perf_counter() - t0\n",
|
||||
" t0 = time.perf_counter()\n",
|
||||
" r = linprog(c=c, A_ub=A_ub, b_ub=angebot, A_eq=A_eq, b_eq=bedarf,\n",
|
||||
" bounds=(0, None), method=\"highs\")\n",
|
||||
" loesen = time.perf_counter() - t0\n",
|
||||
" return float(r.fun), aufbau, loesen, speicher_mb()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def messe(name: str, quelltext: str, m: int, n: int):\n",
|
||||
" \"\"\"Fuehrt einen Ansatz in einem eigenen Prozess aus.\"\"\"\n",
|
||||
" programm = (VORSPANN.format(m=m, n=n) + textwrap.dedent(quelltext)\n",
|
||||
" + \"\\nprint(json.dumps(ausgabe))\\n\")\n",
|
||||
" ergebnis = subprocess.run([sys.executable, \"-c\", programm],\n",
|
||||
" capture_output=True, text=True, timeout=600)\n",
|
||||
" if ergebnis.returncode != 0:\n",
|
||||
" return None, ergebnis.stderr.strip().splitlines()[-1][:60]\n",
|
||||
" return json.loads(ergebnis.stdout.strip().splitlines()[-1]), None\n",
|
||||
"def messe_highspy(m: int, n: int):\n",
|
||||
" import highspy\n",
|
||||
" kosten, angebot, bedarf = instanz(m, n)\n",
|
||||
" t0 = time.perf_counter()\n",
|
||||
" h = highspy.Highs(); h.setOptionValue(\"output_flag\", False)\n",
|
||||
" h.addVars(m * n, np.zeros(m * n), np.full(m * n, highspy.kHighsInf))\n",
|
||||
" for k in range(m * n):\n",
|
||||
" h.changeColCost(k, float(kosten.reshape(-1)[k]))\n",
|
||||
" for i in range(m):\n",
|
||||
" idx = np.arange(i * n, (i + 1) * n, dtype=np.int32)\n",
|
||||
" h.addRow(-highspy.kHighsInf, float(angebot[i]), n, idx, np.ones(n))\n",
|
||||
" for j in range(n):\n",
|
||||
" idx = np.arange(j, m * n, n, dtype=np.int32)\n",
|
||||
" h.addRow(float(bedarf[j]), float(bedarf[j]), m, idx, np.ones(m))\n",
|
||||
" aufbau = time.perf_counter() - t0\n",
|
||||
" t0 = time.perf_counter(); h.run(); loesen = time.perf_counter() - t0\n",
|
||||
" return h.getInfo().objective_function_value, aufbau, loesen, speicher_mb()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def messe_ortools(m: int, n: int):\n",
|
||||
" from ortools.linear_solver import pywraplp\n",
|
||||
" kosten, angebot, bedarf = instanz(m, n)\n",
|
||||
" t0 = time.perf_counter()\n",
|
||||
" s = pywraplp.Solver.CreateSolver(\"GLOP\")\n",
|
||||
" x = [[s.NumVar(0, s.infinity(), f\"x{i}_{j}\") for j in range(n)]\n",
|
||||
" for i in range(m)]\n",
|
||||
" for i in range(m):\n",
|
||||
" s.Add(sum(x[i]) <= float(angebot[i]))\n",
|
||||
" for j in range(n):\n",
|
||||
" s.Add(sum(x[i][j] for i in range(m)) == float(bedarf[j]))\n",
|
||||
" s.Minimize(sum(float(kosten[i, j]) * x[i][j]\n",
|
||||
" for i in range(m) for j in range(n)))\n",
|
||||
" aufbau = time.perf_counter() - t0\n",
|
||||
" t0 = time.perf_counter(); s.Solve(); loesen = time.perf_counter() - t0\n",
|
||||
" return s.Objective().Value(), aufbau, loesen, speicher_mb()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def messe_cvxpy(m: int, n: int):\n",
|
||||
" import cvxpy as cp\n",
|
||||
" kosten, angebot, bedarf = instanz(m, n)\n",
|
||||
" t0 = time.perf_counter()\n",
|
||||
" x = cp.Variable((m, n), nonneg=True)\n",
|
||||
" problem = cp.Problem(cp.Minimize(cp.sum(cp.multiply(kosten, x))),\n",
|
||||
" [cp.sum(x, axis=1) <= angebot,\n",
|
||||
" cp.sum(x, axis=0) == bedarf])\n",
|
||||
" aufbau = time.perf_counter() - t0\n",
|
||||
" t0 = time.perf_counter(); problem.solve(); loesen = time.perf_counter() - t0\n",
|
||||
" return float(problem.value), aufbau, loesen, speicher_mb()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"ANSAETZE = {\"scipy.linprog\": messe_scipy, \"highspy\": messe_highspy,\n",
|
||||
" \"ortools/GLOP\": messe_ortools, \"cvxpy\": messe_cvxpy}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def messe(funktion, m: int, n: int):\n",
|
||||
" \"\"\"Fuehrt eine Messfunktion in einem FRISCHEN Prozess aus.\n",
|
||||
"\n",
|
||||
" 'spawn' und max_tasks_per_child=1 zusammen garantieren, was Regel 1\n",
|
||||
" verlangt: Jede Messung sieht einen leeren Interpreter. Ohne das\n",
|
||||
" zweite wuerde der Pool seinen Arbeiter wiederverwenden - dann waere\n",
|
||||
" der Speicherwert der zweiten Bibliothek um die erste zu hoch, und\n",
|
||||
" ortools und highspy saessen im selben Prozess.\n",
|
||||
" \"\"\"\n",
|
||||
" with ProcessPoolExecutor(\n",
|
||||
" max_workers=1,\n",
|
||||
" mp_context=multiprocessing.get_context(\"spawn\"),\n",
|
||||
" max_tasks_per_child=1) as pool:\n",
|
||||
" try:\n",
|
||||
" return pool.submit(funktion, m, n).result(timeout=600), None\n",
|
||||
" except Exception as fehler:\n",
|
||||
" return None, str(fehler).strip().splitlines()[-1][:60]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"if __name__ == \"__main__\":\n",
|
||||
|
|
@ -717,8 +728,8 @@
|
|||
" f\"{'Loesen':>9} {'Anteil':>8} {'Speicher':>10}\")\n",
|
||||
" print(\" \" + \"-\" * 72)\n",
|
||||
" zielwerte = {}\n",
|
||||
" for name, quelltext in ANSAETZE.items():\n",
|
||||
" werte, fehler = messe(name, quelltext, m, n)\n",
|
||||
" for name, funktion in ANSAETZE.items():\n",
|
||||
" werte, fehler = messe(funktion, m, n)\n",
|
||||
" if werte is None:\n",
|
||||
" print(f\" {name:<16} nicht verfuegbar: {fehler}\")\n",
|
||||
" continue\n",
|
||||
|
|
|
|||
Loading…
Reference in a new issue