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
|
|
@ -308,6 +308,34 @@ $$
|
|||
> prüft `highspy` und `cvxpy` in der Paketübersicht nur auf Anwesenheit
|
||||
> (`importlib.util.find_spec`) und importiert CVXPY erst im Funktionstest.
|
||||
|
||||
### Wie die Isolation aussieht, wenn sie tragen soll
|
||||
|
||||
„Eigener Prozess" ist schnell gesagt. Die naheliegende Umsetzung — ein Codeschnipsel als
|
||||
Zeichenkette an `python -c` übergeben — funktioniert und ist trotzdem die schlechteste:
|
||||
Der Schnipsel ist für Editor, Linter und Testwerkzeug unsichtbar, ein Tippfehler darin
|
||||
fällt erst zur Laufzeit auf, und übergeben lassen sich nur Zeichenketten.
|
||||
|
||||
Tragfähig ist stattdessen: **jeder Solver eine gewöhnliche Funktion mit lokalem Import**,
|
||||
ausgeführt von einem `ProcessPoolExecutor` mit zwei Einstellungen, die zusammen die
|
||||
Garantie ergeben:
|
||||
|
||||
| Einstellung | Wozu |
|
||||
| --- | --- |
|
||||
| `mp_context=multiprocessing.get_context("spawn")` | Der Kindprozess startet mit einem **frischen** Interpreter, statt den Speicher des Elternprozesses zu erben. Unter Linux ist `fork` der Standard — und damit wäre alles, was hier schon importiert ist, auch dort importiert. |
|
||||
| `max_tasks_per_child=1` | Jede Aufgabe bekommt einen **neuen** Prozess. Ohne das verwendet der Pool seinen Arbeiter wieder, und beim zweiten Solver ist der Konflikt zurück. Genau dieser Fehler ist leicht zu machen und schwer zu finden. |
|
||||
|
||||
> **⚠️ `max_tasks_per_child=1` ist nicht optional**
|
||||
> Ein Pool ohne diese Angabe ist der **Normalfall** — er soll seine Arbeiter ja
|
||||
> wiederverwenden. Wer die Isolation über einen Pool herstellt und das vergisst, hat einen
|
||||
> Prozesswechsel programmiert, aber keine Isolation gewonnen: Die zweite Aufgabe landet im
|
||||
> selben Interpreter wie die erste. Der Absturz kommt dann nicht beim ersten Solver,
|
||||
> sondern beim zweiten — und sieht aus wie ein Problem des zweiten.
|
||||
|
||||
Denselben Aufbau verwenden `Solverwechsel_CPSAT_HiGHS.py` ({ref:kap:praxisfallen}) und
|
||||
`Benchmark_Skalierung.py` ({ref:kap:testing}). Dort wandern zusätzlich **Datenobjekte** über
|
||||
die Prozessgrenze statt Zeichenketten — möglich, weil Domänenmodell und Lösungs-DTO keinen
|
||||
Solver kennen ({ref:sec:praxisfallen-or-kern}).
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
|
@ -319,85 +347,104 @@ Kapitel Oekosystem: Dasselbe LP in vier Bibliotheken.
|
|||
2*x1 + 3*x2 + x3 <= 50
|
||||
x >= 0
|
||||
|
||||
Deckt scipy.optimize, highspy, CVXPY und OR-Tools/GLOP ab, mit Kreuzvergleich am Ende.
|
||||
Deckt scipy.optimize, highspy, CVXPY und OR-Tools/GLOP ab, mit Kreuzvergleich
|
||||
am Ende.
|
||||
|
||||
WICHTIG: Jeder Solver läuft in einem EIGENEN Prozess, weil sich ortools und
|
||||
WICHTIG: Jeder Solver laeuft in einem EIGENEN Prozess, weil sich ortools und
|
||||
highspy auf vielen Systemen nicht gemeinsam importieren lassen (beide bringen
|
||||
eine eigene HiGHS-Kopie mit -> Symbolkonflikt).
|
||||
|
||||
Die Isolation besorgt ein ProcessPoolExecutor. Drei Einstellungen ergeben
|
||||
zusammen die Garantie:
|
||||
|
||||
mp_context "spawn" Der Kindprozess startet mit einem FRISCHEN
|
||||
Interpreter, statt den Speicher des Elternprozesses
|
||||
zu erben. Was hier schon importiert ist, ist dort
|
||||
nicht importiert. Mit dem Standard "fork" auf Linux
|
||||
waere das nicht so.
|
||||
max_tasks_per_child=1 Jede Aufgabe bekommt einen NEUEN Prozess. Ohne das
|
||||
wuerde der Pool seinen Arbeiter wiederverwenden - und
|
||||
beim zweiten Solver waere der Konflikt zurueck.
|
||||
max_workers=1 Haelt die vier Laeufe nacheinander. Nicht aus
|
||||
Vorsicht, sondern damit die gemessenen Zeiten
|
||||
vergleichbar bleiben.
|
||||
|
||||
Jeder Solver steht in einer eigenen Funktion mit LOKALEM Import. Das ist der
|
||||
Unterschied zu einem Codestring, den man an 'python -c' uebergibt: Die
|
||||
Funktion laesst sich einzeln aufrufen, testen und vom Editor pruefen - ein
|
||||
String nicht.
|
||||
|
||||
Benoetigt: scipy, highspy, cvxpy, ortools
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
import multiprocessing
|
||||
import time
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
|
||||
ERWARTET = 530.0 # Ergebnis der Handrechnung zum Produktionsprogramm
|
||||
|
||||
# Jeder Eintrag ist ein eigenständiges Miniprogramm, das sein Ergebnis als
|
||||
# JSON auf stdout ausgibt. So bleibt jeder Import in seinem eigenen Prozess.
|
||||
ANSAETZE: dict[str, str] = {
|
||||
# Die Instanz - einmal notiert, von allen vier Funktionen benutzt.
|
||||
ZIEL = [10.0, 15.0, 25.0]
|
||||
MATRIX = [[1, 1, 2], [2, 3, 1]]
|
||||
KAPAZITAET = [40.0, 50.0]
|
||||
|
||||
"scipy.optimize.linprog": """
|
||||
from scipy.optimize import linprog
|
||||
res = linprog(c=[-10.0, -15.0, -25.0], # linprog MINIMIERT -> negieren
|
||||
A_ub=[[1, 1, 2], [2, 3, 1]], b_ub=[40, 50],
|
||||
bounds=[(0, None)] * 3, method="highs")
|
||||
ausgabe = (-res.fun, list(res.x))
|
||||
""",
|
||||
|
||||
"highspy (natives HiGHS)": """
|
||||
import numpy as np, highspy
|
||||
h = highspy.Highs()
|
||||
h.setOptionValue("output_flag", False)
|
||||
h.addVars(3, np.zeros(3), np.full(3, highspy.kHighsInf))
|
||||
h.changeObjectiveSense(highspy.ObjSense.kMaximize)
|
||||
for j, wert in enumerate([10.0, 15.0, 25.0]):
|
||||
h.changeColCost(j, wert)
|
||||
# CSR-Format: starts[i] = Beginn von Zeile i in indices/values
|
||||
h.addRows(2, np.full(2, -highspy.kHighsInf), np.array([40.0, 50.0]), 6,
|
||||
np.array([0, 3], dtype=np.int32),
|
||||
np.array([0, 1, 2, 0, 1, 2], dtype=np.int32),
|
||||
np.array([1.0, 1.0, 2.0, 2.0, 3.0, 1.0]))
|
||||
h.run()
|
||||
ausgabe = (h.getInfo().objective_function_value,
|
||||
list(h.getSolution().col_value[:3]))
|
||||
""",
|
||||
def loese_mit_scipy() -> tuple[float, list[float]]:
|
||||
from scipy.optimize import linprog
|
||||
ergebnis = linprog(c=[-w for w in ZIEL], # linprog MINIMIERT -> negieren
|
||||
A_ub=MATRIX, b_ub=KAPAZITAET,
|
||||
bounds=[(0, None)] * 3, method="highs")
|
||||
return -ergebnis.fun, list(ergebnis.x)
|
||||
|
||||
"cvxpy": """
|
||||
import numpy as np, cvxpy as cp
|
||||
x = cp.Variable(3, nonneg=True)
|
||||
problem = cp.Problem(cp.Maximize(np.array([10.0, 15.0, 25.0]) @ x),
|
||||
[np.array([[1, 1, 2], [2, 3, 1]]) @ x <= np.array([40, 50])])
|
||||
problem.solve()
|
||||
ausgabe = (float(problem.value), [float(v) for v in x.value])
|
||||
""",
|
||||
|
||||
"ortools / GLOP": """
|
||||
from ortools.linear_solver import pywraplp
|
||||
s = pywraplp.Solver.CreateSolver("GLOP")
|
||||
x = [s.NumVar(0, s.infinity(), f"x{j+1}") for j in range(3)]
|
||||
A = [[1, 1, 2], [2, 3, 1]]
|
||||
for i, kap in enumerate([40, 50]):
|
||||
s.Add(sum(A[i][j] * x[j] for j in range(3)) <= kap)
|
||||
s.Maximize(10 * x[0] + 15 * x[1] + 25 * x[2])
|
||||
s.Solve()
|
||||
ausgabe = (s.Objective().Value(), [v.solution_value() for v in x])
|
||||
""",
|
||||
def loese_mit_highspy() -> tuple[float, list[float]]:
|
||||
import highspy
|
||||
import numpy as np
|
||||
h = highspy.Highs()
|
||||
h.setOptionValue("output_flag", False)
|
||||
h.addVars(3, np.zeros(3), np.full(3, highspy.kHighsInf))
|
||||
h.changeObjectiveSense(highspy.ObjSense.kMaximize)
|
||||
for j, wert in enumerate(ZIEL):
|
||||
h.changeColCost(j, wert)
|
||||
# CSR-Format: starts[i] = Beginn von Zeile i in indices/values
|
||||
h.addRows(2, np.full(2, -highspy.kHighsInf), np.array(KAPAZITAET), 6,
|
||||
np.array([0, 3], dtype=np.int32),
|
||||
np.array([0, 1, 2, 0, 1, 2], dtype=np.int32),
|
||||
np.array([float(w) for zeile in MATRIX for w in zeile]))
|
||||
h.run()
|
||||
return (h.getInfo().objective_function_value,
|
||||
list(h.getSolution().col_value[:3]))
|
||||
|
||||
|
||||
def loese_mit_cvxpy() -> tuple[float, list[float]]:
|
||||
import cvxpy as cp
|
||||
import numpy as np
|
||||
x = cp.Variable(3, nonneg=True)
|
||||
problem = cp.Problem(cp.Maximize(np.array(ZIEL) @ x),
|
||||
[np.array(MATRIX) @ x <= np.array(KAPAZITAET)])
|
||||
problem.solve()
|
||||
return float(problem.value), [float(v) for v in x.value]
|
||||
|
||||
|
||||
def loese_mit_ortools() -> tuple[float, list[float]]:
|
||||
from ortools.linear_solver import pywraplp
|
||||
s = pywraplp.Solver.CreateSolver("GLOP")
|
||||
x = [s.NumVar(0, s.infinity(), f"x{j+1}") for j in range(3)]
|
||||
for i, kapazitaet in enumerate(KAPAZITAET):
|
||||
s.Add(sum(MATRIX[i][j] * x[j] for j in range(3)) <= kapazitaet)
|
||||
s.Maximize(sum(ZIEL[j] * x[j] for j in range(3)))
|
||||
s.Solve()
|
||||
return s.Objective().Value(), [v.solution_value() for v in x]
|
||||
|
||||
|
||||
ANSAETZE = {
|
||||
"scipy.optimize.linprog": loese_mit_scipy,
|
||||
"highspy (natives HiGHS)": loese_mit_highspy,
|
||||
"cvxpy": loese_mit_cvxpy,
|
||||
"ortools / GLOP": loese_mit_ortools,
|
||||
}
|
||||
|
||||
|
||||
def fuehre_in_eigenem_prozess_aus(quelltext: str) -> tuple[float, list[float]]:
|
||||
"""Startet den Codeschnipsel als separaten Python-Prozess und liest das Ergebnis."""
|
||||
programm = textwrap.dedent(quelltext) + "\nimport json; print(json.dumps(ausgabe))\n"
|
||||
ergebnis = subprocess.run([sys.executable, "-c", programm],
|
||||
capture_output=True, text=True, timeout=120)
|
||||
if ergebnis.returncode != 0:
|
||||
raise RuntimeError(ergebnis.stderr.strip().splitlines()[-1])
|
||||
wert, loesung = json.loads(ergebnis.stdout.strip().splitlines()[-1])
|
||||
return wert, loesung
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 78)
|
||||
print(" EIN SYSTEM - VIER ANSAETZE (je eigener Prozess)")
|
||||
|
|
@ -406,17 +453,23 @@ if __name__ == "__main__":
|
|||
print("-" * 78)
|
||||
|
||||
werte = []
|
||||
for name, quelltext in ANSAETZE.items():
|
||||
t0 = time.perf_counter()
|
||||
try:
|
||||
wert, x = fuehre_in_eigenem_prozess_aus(quelltext)
|
||||
except RuntimeError as fehler:
|
||||
print(f"{name:<26} nicht verfuegbar: {fehler[:40]}")
|
||||
continue
|
||||
dauer = time.perf_counter() - t0
|
||||
werte.append(wert)
|
||||
print(f"{name:<26} {wert:>10.2f} {x[0]:>7.2f} {x[1]:>7.2f} {x[2]:>7.2f} "
|
||||
f"{dauer:>8.2f} s")
|
||||
# Ein Pool, vier Aufgaben, vier frische Prozesse. Der Kontext muss
|
||||
# "spawn" sein - siehe Modulkommentar.
|
||||
with ProcessPoolExecutor(
|
||||
max_workers=1,
|
||||
mp_context=multiprocessing.get_context("spawn"),
|
||||
max_tasks_per_child=1) as pool:
|
||||
for name, funktion in ANSAETZE.items():
|
||||
beginn = time.perf_counter()
|
||||
try:
|
||||
wert, x = pool.submit(funktion).result(timeout=120)
|
||||
except Exception as fehler: # Bibliothek fehlt o. Ae.
|
||||
print(f"{name:<26} nicht verfuegbar: {str(fehler)[:40]}")
|
||||
continue
|
||||
dauer = time.perf_counter() - beginn
|
||||
werte.append(wert)
|
||||
print(f"{name:<26} {wert:>10.2f} {x[0]:>7.2f} {x[1]:>7.2f} {x[2]:>7.2f} "
|
||||
f"{dauer:>8.2f} s")
|
||||
|
||||
print("-" * 78)
|
||||
spanne = max(werte) - min(werte)
|
||||
|
|
@ -426,8 +479,8 @@ if __name__ == "__main__":
|
|||
assert spanne < 1e-6, "Die Bibliotheken widersprechen sich!"
|
||||
assert abs(werte[0] - ERWARTET) < 1e-6, "Ergebnis weicht von der Handrechnung ab!"
|
||||
print("Alle Wege fuehren zum selben, von Hand bestaetigten Optimum.")
|
||||
print("(Die Zeiten enthalten den Prozessstart und den Import - sie messen")
|
||||
print(" NICHT die reine Solverleistung, siehe Uebung 3.5.)")
|
||||
print("(Die Zeiten enthalten Prozessstart und Import - sie messen NICHT die")
|
||||
print(" reine Solverleistung. Die Uebungsaufgabe 'Laufzeitvergleich' trennt beides.)")
|
||||
print("=" * 78)
|
||||
```
|
||||
|
||||
|
|
@ -439,16 +492,16 @@ if __name__ == "__main__":
|
|||
==============================================================================
|
||||
Bibliothek Z* x1 x2 x3 Zeit
|
||||
------------------------------------------------------------------------------
|
||||
scipy.optimize.linprog 530.00 0.00 12.00 14.00 0.55 s
|
||||
highspy (natives HiGHS) 530.00 0.00 12.00 14.00 0.17 s
|
||||
cvxpy 530.00 0.00 12.00 14.00 1.52 s
|
||||
ortools / GLOP 530.00 0.00 12.00 14.00 0.09 s
|
||||
scipy.optimize.linprog 530.00 0.00 12.00 14.00 0.59 s
|
||||
highspy (natives HiGHS) 530.00 0.00 12.00 14.00 0.12 s
|
||||
cvxpy 530.00 0.00 12.00 14.00 1.24 s
|
||||
ortools / GLOP 530.00 0.00 12.00 14.00 0.33 s
|
||||
------------------------------------------------------------------------------
|
||||
Spannweite zwischen den Bibliotheken: 2.41e-08
|
||||
Abweichung zur Handrechnung (530): 0.00e+00
|
||||
Alle Wege fuehren zum selben, von Hand bestaetigten Optimum.
|
||||
(Die Zeiten enthalten den Prozessstart und den Import - sie messen
|
||||
NICHT die reine Solverleistung, siehe Uebung 3.5.)
|
||||
(Die Zeiten enthalten Prozessstart und Import - sie messen NICHT die
|
||||
reine Solverleistung. Die Uebungsaufgabe 'Laufzeitvergleich' trennt beides.)
|
||||
==============================================================================
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -1666,9 +1666,9 @@ Benoetigt: numpy, pydantic, ortools, highspy (jeweils im eigenen Prozess)
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import multiprocessing
|
||||
import time
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
|
||||
import numpy as np
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
|
|
@ -1871,25 +1871,29 @@ def geoeffnete_lager(problem: Standortproblem, loesung: Loesung) -> list[str]:
|
|||
if any(loesung.werte[problem.schluessel(i, j)] > 0.5 for j in range(m))]
|
||||
|
||||
|
||||
def loese_in_eigenem_prozess(name: str) -> Loesung:
|
||||
"""Startet dieses Programm noch einmal - mit genau einem Solverimport."""
|
||||
ergebnis = subprocess.run([sys.executable, __file__, name],
|
||||
capture_output=True, text=True, timeout=300)
|
||||
if ergebnis.returncode != 0:
|
||||
raise RuntimeError(ergebnis.stderr.strip().splitlines()[-1])
|
||||
# Das DTO als JSON - genau dafuer ist ein Datenobjekt ohne Solverbezug gut.
|
||||
return Loesung.model_validate_json(ergebnis.stdout.strip().splitlines()[-1])
|
||||
def loese_in_eigenem_prozess(name: str, problem: Standortproblem) -> Loesung:
|
||||
"""Laesst genau einen Modellbauer in einem frischen Prozess rechnen.
|
||||
|
||||
'spawn' statt des Linux-Standards 'fork': Der Kindprozess startet mit
|
||||
einem leeren Interpreter und importiert nur den Solver, den SEIN
|
||||
Modellbauer braucht. max_tasks_per_child=1 sorgt dafuer, dass der Pool
|
||||
seinen Arbeiter nicht wiederverwendet - sonst saessen beim zweiten Aufruf
|
||||
wieder beide Bibliotheken im selben Prozess.
|
||||
|
||||
Hin und zurueck wandert das Domaenenmodell bzw. das Loesungs-DTO. Beide
|
||||
kennen keinen Solver, sind also serialisierbar - genau dafuer sind sie da.
|
||||
"""
|
||||
with ProcessPoolExecutor(
|
||||
max_workers=1,
|
||||
mp_context=multiprocessing.get_context("spawn"),
|
||||
max_tasks_per_child=1) as pool:
|
||||
return pool.submit(MODELLBAUER[name], problem).result(timeout=300)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
problem = beispielproblem()
|
||||
|
||||
# --- Kindprozess: rechnen und das DTO als JSON ausgeben ---------------
|
||||
if len(sys.argv) > 1:
|
||||
print(MODELLBAUER[sys.argv[1]](problem).model_dump_json())
|
||||
sys.exit(0)
|
||||
|
||||
# --- Hauptprozess: beide Solver anstossen und vergleichen -------------
|
||||
# --- Beide Solver anstossen und vergleichen ---------------------------
|
||||
print("=" * 82)
|
||||
print(" DERSELBE FALL, ZWEI SOLVER - UND EIN AUSWERTUNGSCODE")
|
||||
print("=" * 82)
|
||||
|
|
@ -1901,7 +1905,7 @@ if __name__ == "__main__":
|
|||
loesungen: dict[str, Loesung] = {}
|
||||
for name, beschriftung in [("cpsat", "OR-Tools CP-SAT"),
|
||||
("highs", "HiGHS (highspy)")]:
|
||||
loesung = loesungen[name] = loese_in_eigenem_prozess(name)
|
||||
loesung = loesungen[name] = loese_in_eigenem_prozess(name, problem)
|
||||
beanstandungen = pruefe_zuordnung(problem, loesung)
|
||||
|
||||
print(f"{beschriftung}")
|
||||
|
|
|
|||
|
|
@ -787,113 +787,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__":
|
||||
|
|
@ -911,8 +922,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
|
||||
|
|
@ -968,28 +979,28 @@ die Zielwerte und ihr Verhaeltnis zueinander nicht.
|
|||
--- 10 Lager x 10 Kunden = 100 Variablen ---------------------------------------------------
|
||||
Bibliothek Zielwert Aufbau Loesen Anteil Speicher
|
||||
------------------------------------------------------------------------
|
||||
scipy.linprog 13,509.48 0.000s 0.004s 1% 78 MB
|
||||
highspy 13,509.48 0.001s 0.002s 34% 41 MB
|
||||
ortools/GLOP 13,509.48 0.002s 0.001s 80% 54 MB
|
||||
scipy.linprog 13,509.48 0.000s 0.004s 1% 79 MB
|
||||
highspy 13,509.48 0.001s 0.002s 36% 44 MB
|
||||
ortools/GLOP 13,509.48 0.003s 0.001s 80% 56 MB
|
||||
cvxpy 13,509.48 0.001s 0.009s 9% 229 MB
|
||||
Spannweite der Zielwerte: 1.33e-06 (relativ 9.8e-11)
|
||||
|
||||
--- 32 Lager x 32 Kunden = 1,024 Variablen -------------------------------------------------
|
||||
Bibliothek Zielwert Aufbau Loesen Anteil Speicher
|
||||
------------------------------------------------------------------------
|
||||
scipy.linprog 35,744.25 0.000s 0.009s 4% 80 MB
|
||||
highspy 35,744.25 0.004s 0.005s 47% 42 MB
|
||||
ortools/GLOP 35,744.25 0.015s 0.002s 85% 55 MB
|
||||
cvxpy 35,744.25 0.001s 0.016s 5% 230 MB
|
||||
scipy.linprog 35,744.25 0.000s 0.009s 3% 81 MB
|
||||
highspy 35,744.25 0.004s 0.005s 44% 45 MB
|
||||
ortools/GLOP 35,744.25 0.014s 0.002s 85% 58 MB
|
||||
cvxpy 35,744.25 0.001s 0.017s 5% 231 MB
|
||||
Spannweite der Zielwerte: 9.54e-05 (relativ 2.7e-09)
|
||||
|
||||
--- 100 Lager x 100 Kunden = 10,000 Variablen ----------------------------------------------
|
||||
Bibliothek Zielwert Aufbau Loesen Anteil Speicher
|
||||
------------------------------------------------------------------------
|
||||
scipy.linprog 72,220.34 0.002s 0.070s 3% 120 MB
|
||||
highspy 72,220.34 0.029s 0.034s 45% 47 MB
|
||||
ortools/GLOP 72,220.34 0.123s 0.036s 77% 66 MB
|
||||
cvxpy 72,220.34 0.001s 0.103s 1% 242 MB
|
||||
scipy.linprog 72,220.34 0.004s 0.072s 5% 122 MB
|
||||
highspy 72,220.34 0.026s 0.032s 45% 50 MB
|
||||
ortools/GLOP 72,220.34 0.141s 0.043s 77% 68 MB
|
||||
cvxpy 72,220.34 0.001s 0.108s 1% 242 MB
|
||||
Spannweite der Zielwerte: 3.65e-04 (relativ 5.0e-09)
|
||||
|
||||
============================================================================================
|
||||
|
|
|
|||
|
|
@ -723,7 +723,8 @@ importiert, crasht daher mit derselben Meldung.
|
|||
|
||||
**Abhilfen (in dieser Reihenfolge):**
|
||||
1. Nur eines von beiden im selben Skript verwenden.
|
||||
2. Getrennte Prozesse (`subprocess`) — siehe `Ein_System_Vier_Ansaetze.py`.
|
||||
2. Getrennte Prozesse — ein `ProcessPoolExecutor` mit `mp_context="spawn"` und
|
||||
`max_tasks_per_child=1`, siehe `Ein_System_Vier_Ansaetze.py`.
|
||||
3. Auf `highspy` verzichten: HiGHS ist ohnehin Backend von `scipy.optimize.linprog` und
|
||||
CVXPY.
|
||||
4. Getrennte virtuelle Umgebungen.
|
||||
|
|
|
|||
|
|
@ -313,6 +313,20 @@ def pruefe_dateien() -> list[str]:
|
|||
f"{marken} Loesungen, das Kapitel aber {erwartet} Aufgaben.")
|
||||
fehlend.append(anhang)
|
||||
|
||||
# Ein {#sec:...}-Label an einer ###-Ueberschrift. ABSCHNITT_RE erkennt nur
|
||||
# '## ' - ein solches Label wird also NIE registriert, und jeder
|
||||
# {ref:...} darauf laeuft ins Leere. Der Fehler sieht dabei voellig
|
||||
# harmlos aus, weil die Ueberschrift richtig gesetzt wird.
|
||||
tiefes_label_re = re.compile(r"^#{3,} .*\{#sec:[\w-]+\}", re.MULTILINE)
|
||||
for name in DATEIEN:
|
||||
with open(os.path.join(HIER, name), encoding="utf-8") as f:
|
||||
inhalt = f.read()
|
||||
for treffer in tiefes_label_re.finditer(inhalt):
|
||||
zeile = inhalt[:treffer.start()].count("\n") + 1
|
||||
print(f"FEHLER: {name}:{zeile} haengt ein {{#sec:...}}-Label an eine "
|
||||
f"###-Ueberschrift - registriert werden nur '## '-Abschnitte.")
|
||||
fehlend.append(name)
|
||||
|
||||
# Die Lesekette. Jede Datei ausser der letzten schliesst mit
|
||||
# '*Weiter mit:* [...](naechste_datei.md)' - und zwar auf die Datei, die in
|
||||
# DATEIEN als naechste steht.
|
||||
|
|
|
|||
Loading…
Reference in a new issue