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:
dschlueter 2026-09-08 12:39:34 +02:00
commit 862e92bc7b
29 changed files with 2401 additions and 1912 deletions

View file

@ -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

View file

@ -8,85 +8,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)")
@ -95,17 +114,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)
@ -115,6 +140,6 @@ 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)

View file

@ -36,9 +36,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
@ -241,25 +241,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)
@ -271,7 +275,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}")