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
|
|
@ -33,113 +33,124 @@ Benoetigt: numpy; in den Kindprozessen scipy, highspy, ortools, cvxpy
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
import multiprocessing
|
||||
import resource
|
||||
import time
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
|
||||
import numpy as np
|
||||
|
||||
GROESSEN = [(10, 10), (32, 32), (100, 100)] # (Lager, Kunden) -> 100 / 1.024 / 10.000 Variablen
|
||||
|
||||
|
||||
# Jeder Eintrag ist ein eigenstaendiges Programm: Instanz aufbauen, loesen,
|
||||
# Ergebnis als JSON ausgeben. Die Instanz wird in jedem Kindprozess aus
|
||||
# derselben Saat neu erzeugt - so reist nichts ueber die Prozessgrenze,
|
||||
# was das Ergebnis verfaelschen koennte.
|
||||
VORSPANN = """
|
||||
import json, time, resource
|
||||
import numpy as np
|
||||
# Instanz und Speichermessung stehen als gewoehnliche Funktionen hier - nicht
|
||||
# in einem String, den ein Kindprozess ausfuehrt. Jede Messfunktion baut die
|
||||
# Instanz aus derselben Saat neu auf, damit ueber die Prozessgrenze nichts
|
||||
# reist, was das Ergebnis verfaelschen koennte.
|
||||
|
||||
def instanz(m, n):
|
||||
def instanz(m: int, n: int):
|
||||
rng = np.random.default_rng(20)
|
||||
kosten = rng.integers(5, 95, (m, n)).astype(float)
|
||||
angebot = rng.integers(50, 150, m).astype(float)
|
||||
bedarf = angebot.sum() * rng.dirichlet(np.ones(n))
|
||||
return kosten, angebot, bedarf
|
||||
|
||||
def speicher_mb():
|
||||
# ru_maxrss ist unter Linux in Kilobyte
|
||||
|
||||
def speicher_mb() -> float:
|
||||
# ru_maxrss ist unter Linux in Kilobyte. Gemessen wird der Kindprozess -
|
||||
# deshalb muss jede Messung einen eigenen bekommen.
|
||||
return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024
|
||||
|
||||
M, N = {m}, {n}
|
||||
kosten, angebot, bedarf = instanz(M, N)
|
||||
"""
|
||||
|
||||
ANSAETZE = {
|
||||
"scipy.linprog": """
|
||||
from scipy.optimize import linprog
|
||||
t0 = time.perf_counter()
|
||||
c = kosten.reshape(-1)
|
||||
A_ub = np.zeros((M, M * N)); A_eq = np.zeros((N, M * N))
|
||||
for i in range(M):
|
||||
A_ub[i, i * N:(i + 1) * N] = 1.0
|
||||
for j in range(N):
|
||||
A_eq[j, j::N] = 1.0
|
||||
aufbau = time.perf_counter() - t0
|
||||
t0 = time.perf_counter()
|
||||
r = linprog(c=c, A_ub=A_ub, b_ub=angebot, A_eq=A_eq, b_eq=bedarf,
|
||||
bounds=(0, None), method="highs")
|
||||
loesen = time.perf_counter() - t0
|
||||
ausgabe = (float(r.fun), aufbau, loesen, speicher_mb())
|
||||
""",
|
||||
|
||||
"highspy": """
|
||||
import highspy
|
||||
t0 = time.perf_counter()
|
||||
h = highspy.Highs(); h.setOptionValue("output_flag", False)
|
||||
h.addVars(M * N, np.zeros(M * N), np.full(M * N, highspy.kHighsInf))
|
||||
for k in range(M * N):
|
||||
h.changeColCost(k, float(kosten.reshape(-1)[k]))
|
||||
for i in range(M):
|
||||
idx = np.arange(i * N, (i + 1) * N, dtype=np.int32)
|
||||
h.addRow(-highspy.kHighsInf, float(angebot[i]), N, idx, np.ones(N))
|
||||
for j in range(N):
|
||||
idx = np.arange(j, M * N, N, dtype=np.int32)
|
||||
h.addRow(float(bedarf[j]), float(bedarf[j]), M, idx, np.ones(M))
|
||||
aufbau = time.perf_counter() - t0
|
||||
t0 = time.perf_counter(); h.run(); loesen = time.perf_counter() - t0
|
||||
ausgabe = (h.getInfo().objective_function_value, aufbau, loesen, speicher_mb())
|
||||
""",
|
||||
|
||||
"ortools/GLOP": """
|
||||
from ortools.linear_solver import pywraplp
|
||||
t0 = time.perf_counter()
|
||||
s = pywraplp.Solver.CreateSolver("GLOP")
|
||||
x = [[s.NumVar(0, s.infinity(), f"x{i}_{j}") for j in range(N)]
|
||||
for i in range(M)]
|
||||
for i in range(M):
|
||||
s.Add(sum(x[i]) <= float(angebot[i]))
|
||||
for j in range(N):
|
||||
s.Add(sum(x[i][j] for i in range(M)) == float(bedarf[j]))
|
||||
s.Minimize(sum(float(kosten[i, j]) * x[i][j]
|
||||
for i in range(M) for j in range(N)))
|
||||
aufbau = time.perf_counter() - t0
|
||||
t0 = time.perf_counter(); s.Solve(); loesen = time.perf_counter() - t0
|
||||
ausgabe = (s.Objective().Value(), aufbau, loesen, speicher_mb())
|
||||
""",
|
||||
|
||||
"cvxpy": """
|
||||
import cvxpy as cp
|
||||
t0 = time.perf_counter()
|
||||
x = cp.Variable((M, N), nonneg=True)
|
||||
problem = cp.Problem(cp.Minimize(cp.sum(cp.multiply(kosten, x))),
|
||||
[cp.sum(x, axis=1) <= angebot,
|
||||
cp.sum(x, axis=0) == bedarf])
|
||||
aufbau = time.perf_counter() - t0
|
||||
t0 = time.perf_counter(); problem.solve(); loesen = time.perf_counter() - t0
|
||||
ausgabe = (float(problem.value), aufbau, loesen, speicher_mb())
|
||||
""",
|
||||
}
|
||||
def messe_scipy(m: int, n: int):
|
||||
from scipy.optimize import linprog
|
||||
kosten, angebot, bedarf = instanz(m, n)
|
||||
t0 = time.perf_counter()
|
||||
c = kosten.reshape(-1)
|
||||
A_ub = np.zeros((m, m * n)); A_eq = np.zeros((n, m * n))
|
||||
for i in range(m):
|
||||
A_ub[i, i * n:(i + 1) * n] = 1.0
|
||||
for j in range(n):
|
||||
A_eq[j, j::n] = 1.0
|
||||
aufbau = time.perf_counter() - t0
|
||||
t0 = time.perf_counter()
|
||||
r = linprog(c=c, A_ub=A_ub, b_ub=angebot, A_eq=A_eq, b_eq=bedarf,
|
||||
bounds=(0, None), method="highs")
|
||||
loesen = time.perf_counter() - t0
|
||||
return float(r.fun), aufbau, loesen, speicher_mb()
|
||||
|
||||
|
||||
def messe(name: str, quelltext: str, m: int, n: int):
|
||||
"""Fuehrt einen Ansatz in einem eigenen Prozess aus."""
|
||||
programm = (VORSPANN.format(m=m, n=n) + textwrap.dedent(quelltext)
|
||||
+ "\nprint(json.dumps(ausgabe))\n")
|
||||
ergebnis = subprocess.run([sys.executable, "-c", programm],
|
||||
capture_output=True, text=True, timeout=600)
|
||||
if ergebnis.returncode != 0:
|
||||
return None, ergebnis.stderr.strip().splitlines()[-1][:60]
|
||||
return json.loads(ergebnis.stdout.strip().splitlines()[-1]), None
|
||||
def messe_highspy(m: int, n: int):
|
||||
import highspy
|
||||
kosten, angebot, bedarf = instanz(m, n)
|
||||
t0 = time.perf_counter()
|
||||
h = highspy.Highs(); h.setOptionValue("output_flag", False)
|
||||
h.addVars(m * n, np.zeros(m * n), np.full(m * n, highspy.kHighsInf))
|
||||
for k in range(m * n):
|
||||
h.changeColCost(k, float(kosten.reshape(-1)[k]))
|
||||
for i in range(m):
|
||||
idx = np.arange(i * n, (i + 1) * n, dtype=np.int32)
|
||||
h.addRow(-highspy.kHighsInf, float(angebot[i]), n, idx, np.ones(n))
|
||||
for j in range(n):
|
||||
idx = np.arange(j, m * n, n, dtype=np.int32)
|
||||
h.addRow(float(bedarf[j]), float(bedarf[j]), m, idx, np.ones(m))
|
||||
aufbau = time.perf_counter() - t0
|
||||
t0 = time.perf_counter(); h.run(); loesen = time.perf_counter() - t0
|
||||
return h.getInfo().objective_function_value, aufbau, loesen, speicher_mb()
|
||||
|
||||
|
||||
def messe_ortools(m: int, n: int):
|
||||
from ortools.linear_solver import pywraplp
|
||||
kosten, angebot, bedarf = instanz(m, n)
|
||||
t0 = time.perf_counter()
|
||||
s = pywraplp.Solver.CreateSolver("GLOP")
|
||||
x = [[s.NumVar(0, s.infinity(), f"x{i}_{j}") for j in range(n)]
|
||||
for i in range(m)]
|
||||
for i in range(m):
|
||||
s.Add(sum(x[i]) <= float(angebot[i]))
|
||||
for j in range(n):
|
||||
s.Add(sum(x[i][j] for i in range(m)) == float(bedarf[j]))
|
||||
s.Minimize(sum(float(kosten[i, j]) * x[i][j]
|
||||
for i in range(m) for j in range(n)))
|
||||
aufbau = time.perf_counter() - t0
|
||||
t0 = time.perf_counter(); s.Solve(); loesen = time.perf_counter() - t0
|
||||
return s.Objective().Value(), aufbau, loesen, speicher_mb()
|
||||
|
||||
|
||||
def messe_cvxpy(m: int, n: int):
|
||||
import cvxpy as cp
|
||||
kosten, angebot, bedarf = instanz(m, n)
|
||||
t0 = time.perf_counter()
|
||||
x = cp.Variable((m, n), nonneg=True)
|
||||
problem = cp.Problem(cp.Minimize(cp.sum(cp.multiply(kosten, x))),
|
||||
[cp.sum(x, axis=1) <= angebot,
|
||||
cp.sum(x, axis=0) == bedarf])
|
||||
aufbau = time.perf_counter() - t0
|
||||
t0 = time.perf_counter(); problem.solve(); loesen = time.perf_counter() - t0
|
||||
return float(problem.value), aufbau, loesen, speicher_mb()
|
||||
|
||||
|
||||
ANSAETZE = {"scipy.linprog": messe_scipy, "highspy": messe_highspy,
|
||||
"ortools/GLOP": messe_ortools, "cvxpy": messe_cvxpy}
|
||||
|
||||
|
||||
def messe(funktion, m: int, n: int):
|
||||
"""Fuehrt eine Messfunktion in einem FRISCHEN Prozess aus.
|
||||
|
||||
'spawn' und max_tasks_per_child=1 zusammen garantieren, was Regel 1
|
||||
verlangt: Jede Messung sieht einen leeren Interpreter. Ohne das
|
||||
zweite wuerde der Pool seinen Arbeiter wiederverwenden - dann waere
|
||||
der Speicherwert der zweiten Bibliothek um die erste zu hoch, und
|
||||
ortools und highspy saessen im selben Prozess.
|
||||
"""
|
||||
with ProcessPoolExecutor(
|
||||
max_workers=1,
|
||||
mp_context=multiprocessing.get_context("spawn"),
|
||||
max_tasks_per_child=1) as pool:
|
||||
try:
|
||||
return pool.submit(funktion, m, n).result(timeout=600), None
|
||||
except Exception as fehler:
|
||||
return None, str(fehler).strip().splitlines()[-1][:60]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
@ -157,8 +168,8 @@ if __name__ == "__main__":
|
|||
f"{'Loesen':>9} {'Anteil':>8} {'Speicher':>10}")
|
||||
print(" " + "-" * 72)
|
||||
zielwerte = {}
|
||||
for name, quelltext in ANSAETZE.items():
|
||||
werte, fehler = messe(name, quelltext, m, n)
|
||||
for name, funktion in ANSAETZE.items():
|
||||
werte, fehler = messe(funktion, m, n)
|
||||
if werte is None:
|
||||
print(f" {name:<16} nicht verfuegbar: {fehler}")
|
||||
continue
|
||||
|
|
|
|||
Loading…
Reference in a new issue