operations_research/OR_HTML_04/cpsat.html
dschlueter 293370b7a1 Phase 6.3: Parallelitaet und Determinismus - gemessen statt behauptet
Neuer Abschnitt im CP-SAT-Kapitel plus Parallele_Suche.py (76. Programm).
Setzt Paket 2 aus Verbesserungen_02.md um.

Was fehlte, war nicht die Regel, sondern die Messung: Das Buch setzt an acht
Stellen num_workers = 1 mit dem Kommentar "fuer eine reproduzierbare
Ausgabe", nachgeprueft hatte es nie jemand. Gerechnet wird auf demselben
Job-Shop wie das Intervallvariablen-Kapitel, nur gross genug, dass die Suche
arbeitet: 12 Auftraege, 10 Maschinen, 120 Arbeitsgaenge.

Zwei Befunde, beide staerker als die Behauptung:

* Die Beschleunigung ist ueberlinear. Acht Arbeiter waren im abgedruckten
  Lauf nicht achtmal, sondern 12,3-mal schneller als einer. Kein Messfehler:
  CP-SAT vervielfacht nicht dieselbe Suche, sondern laesst verschiedene
  Strategien nebeneinander laufen, die einander ihre Schranken mitteilen.
* Der Seed genuegt nicht - und zwar schon ab ZWEI Arbeitern. Ein Arbeiter:
  1 Plan aus 4 Laeufen. Zwei Arbeiter: 3 verschiedene Plaene aus 4 Laeufen,
  bei identischem random_seed und identischem Zielwert 183.

Die vollstaendige Antwort kam erst ueber die Uebungsaufgabe: Mit
num_workers = 1, aber fuenf verschiedenen Seeds ergeben sich ebenfalls fuenf
verschiedene Plaene. Keiner der beiden Parameter sichert die
Reproduzierbarkeit allein - erst die Kombination traegt.

Weiter fuer die Aufgabe gemessen: Der Gewinn kehrt sich um (auf 24 Kernen
Faktor 10,2 bei 8 Arbeitern, 12,3 bei 16, 9,0 bei 24) - "so viele Arbeiter
wie Kerne" ist damit widerlegt. Und bei 15 Auftraegen laeuft ein Arbeiter
ins 60-s-Limit (FEASIBLE, Makespan 200), waehrend acht OPTIMAL mit
demselben Makespan 200 nach 26,4 s melden: Der Unterschied liegt nicht in
der Loesung, sondern im Beweis, dass es keine bessere gibt.

Die abgedruckte Ausgabe traegt die Kennzeichnung "Laufzeiten und die Zahl
der verschiedenen Plaene sind hardwareabhaengig" - nach dem Muster, das das
Testing-Kapitel fuer Benchmark_Skalierung.py schon verwendet. Der Vergleich
des extrahierten Programms mit dem Abdruck weicht denn auch in genau einer
Zelle ab (4 statt 3 verschiedene Plaene bei 4 Arbeitern); Zielwert und
Struktur sind identisch. Hier ist die Nichtreproduzierbarkeit der
abgedruckten Zahl die Aussage selbst.

Mitgezogen: Kapitelkopf, Lernziele, Selbsttest, Zusammenfassung,
Vorwort-Programmverzeichnis, Uebungsaufgabe und Loesung in Anhang A, ein
Verweis aus dem bestehenden Callout zu mehrdeutigen Optima und einer aus
Warmstart_Effekt.py im MILP-Kapitel (dort nach Regel 12 der Kapitelname).

Stand: 295 Abschnitte, 730 Querverweise, 328 Indexmarken, 76 Programme,
140 Aufgaben mit 140 Loesungen, 33 pytest-Tests, PDF 744 Seiten, 69
netzfreie Programme fehlerfrei.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 11:19:53 +02:00

1833 lines
265 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Kapitel 7: Constraint Programming mit CP-SAT — Logik, Scheduling und Zuweisung · Optimierte Entscheidungsfindung mit Python</title>
<script>
(function () {
try {
var t = localStorage.getItem("or-theme");
if (t) document.documentElement.setAttribute("data-theme", t);
} catch (e) {}
})();
</script>
<link rel="stylesheet" href="assets/highlight.css" />
<link rel="stylesheet" href="katex/katex.min.css" />
<script defer="" src="katex/katex.min.js"></script>
<script>document.addEventListener("DOMContentLoaded", function () {
var mathElements = document.getElementsByClassName("math");
var macros = [];
for (var i = 0; i < mathElements.length; i++) {
var texText = mathElements[i].firstChild;
if (mathElements[i].tagName == "SPAN") {
katex.render(texText.data, mathElements[i], {
displayMode: mathElements[i].classList.contains('display'),
throwOnError: false,
macros: macros,
fleqn: false
});
}}
// Der Browser springt zu einem #anker in der URL schon beim ersten Rendern
// an, BEVOR die KaTeX-Formeln oben im Text ihre finale Hoehe bekommen -
// durch den Reflow landet der Anker danach zu weit unten. Nach dem
// Formel-Rendering hier erneut zum Anker springen, das behebt es.
if (location.hash) {
var ziel = document.getElementById(decodeURIComponent(location.hash.slice(1)));
if (ziel) ziel.scrollIntoView({behavior: "instant", block: "start"});
}
});
</script>
<link rel="stylesheet" href="assets/site.css" />
</head>
<body>
<svg style="display:none" aria-hidden="true"><symbol id="icon-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/>
</symbol>
<symbol id="icon-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.2" y2="16.2"/>
</symbol>
<symbol id="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="4.5"/>
<line x1="12" y1="1.5" x2="12" y2="4"/><line x1="12" y1="20" x2="12" y2="22.5"/>
<line x1="1.5" y1="12" x2="4" y2="12"/><line x1="20" y1="12" x2="22.5" y2="12"/>
<line x1="4.5" y1="4.5" x2="6.2" y2="6.2"/><line x1="17.8" y1="17.8" x2="19.5" y2="19.5"/>
<line x1="19.5" y1="4.5" x2="17.8" y2="6.2"/><line x1="6.2" y1="17.8" x2="4.5" y2="19.5"/>
</symbol>
<symbol id="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 14.5A8.5 8.5 0 1 1 9.5 4a6.8 6.8 0 0 0 10.5 10.5z"/>
</symbol>
<symbol id="icon-chevron-left" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="15 4 7 12 15 20"/>
</symbol>
<symbol id="icon-chevron-right" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="9 4 17 12 9 20"/>
</symbol>
<symbol id="icon-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="4 13 9.5 18.5 20 6"/>
</symbol>
<symbol id="icon-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 13.5V19a1.5 1.5 0 0 1-1.5 1.5H5A1.5 1.5 0 0 1 3.5 19V7A1.5 1.5 0 0 1 5 5.5h5.5"/>
<polyline points="14.5 3.5 20.5 3.5 20.5 9.5"/><line x1="11" y1="13" x2="20" y2="4"/>
</symbol>
<symbol id="icon-book" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 5.5A2 2 0 0 1 6 4h6v16H6a2 2 0 0 0-2 1.5z"/>
<path d="M20 5.5A2 2 0 0 0 18 4h-6v16h6a2 2 0 0 1 2 1.5z"/>
</symbol>
<symbol id="icon-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</symbol>
<symbol id="icon-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 3v12"/><polyline points="7 10 12 15 17 10"/><path d="M4 19.5h16"/>
</symbol></svg>
<header class="site-header">
<button type="button" class="icon-btn" id="sidebar-toggle" aria-label="Menü öffnen"><svg class="icon" aria-hidden="true"><use href="#icon-menu"></use></svg></button>
<a class="brand" href="index.html"><svg class="icon" aria-hidden="true"><use href="#icon-book"></use></svg> <span>Optimierte Entscheidungsfindung mit Python</span></a>
<div class="site-search">
<input id="suche-eingabe" type="search" placeholder="Suchen …" aria-label="Suche" autocomplete="off" />
<svg class="icon such-icon" aria-hidden="true"><use href="#icon-search"></use></svg>
<div id="suche-ergebnisse" class="suche-ergebnisse" hidden></div>
</div>
<button type="button" class="icon-btn" id="theme-toggle" aria-label="Darstellung umschalten">
<svg class="icon icon-sun" aria-hidden="true"><use href="#icon-sun"></use></svg><svg class="icon icon-moon" aria-hidden="true"><use href="#icon-moon"></use></svg>
</button>
</header>
<div class="site-body">
<div class="sidebar-overlay" id="sidebar-overlay" hidden></div>
<nav class="sidebar" id="sidebar" aria-label="Kapitelnavigation"><div class="sidebar-inhalt"><details class="sidebar-gruppe"><summary>Einstieg</summary><ul><li data-kapitel="vorwort.html"><a href="vorwort.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Vorwort & Lesehilfe</span></a></li><li data-kapitel="notation.html"><a href="notation.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Notation & Abkürzungen</span></a></li></ul></details><details class="sidebar-gruppe"><summary>Teil I: Grundlagen des Operations Research</summary><ul><li data-kapitel="einfuehrung.html"><a href="einfuehrung.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 1: Einführung in Operations Research — Vom Ursprung zur mathematischen Entscheidungsfindung</span></a></li><li data-kapitel="fundament.html"><a href="fundament.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 2: Das mathematische Fundament — Vektoren, Matrizen, Konvexität</span></a></li><li data-kapitel="oekosystem.html"><a href="oekosystem.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 3: Das Python-Ökosystem für OR — Solver, Bindings und Modellierungsschichten</span></a></li><li data-kapitel="modellierung.html"><a href="modellierung.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 4: Vom Management-Wunsch zum Modell</span></a></li></ul></details><details class="sidebar-gruppe" open><summary>Teil II: Die Kernverfahren der deterministischen Optimierung</summary><ul><li data-kapitel="lp.html"><a href="lp.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 5: Lineare Programmierung — Simplex, Dualität und Schattenpreise</span></a></li><li data-kapitel="milp.html"><a href="milp.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 6: Gemischt-ganzzahlige Optimierung — Diskrete Entscheidungen und Branch-and-Bound</span></a></li><li data-kapitel="cpsat.html" class="aktiv"><a href="cpsat.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 7: Constraint Programming mit CP-SAT — Logik, Scheduling und Zuweisung</span></a></li><li data-kapitel="graphen.html"><a href="graphen.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 8: Graphen, Flüsse und Touren — Min-Cost-Flow, Matching und VRP</span></a></li><li data-kapitel="metaheuristiken.html"><a href="metaheuristiken.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 9: Metaheuristiken — wenn der exakte Solver aussteigt</span></a></li><li data-kapitel="dekomposition.html"><a href="dekomposition.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 10: Spaltengenerierung — das Modell umbauen statt die Lösung raten</span></a></li></ul></details><details class="sidebar-gruppe"><summary>Teil III: Nichtlinearität, Unsicherheit und mehrperiodige Dynamik</summary><ul><li data-kapitel="qp-nlp.html"><a href="qp-nlp.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 11: Quadratische und nichtlineare Optimierung — KKT, Lagrange, Konvexität</span></a></li><li data-kapitel="unsicherheit.html"><a href="unsicherheit.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 12: Optimierung unter Unsicherheit — Monte-Carlo, Stochastik, Robustheit</span></a></li><li data-kapitel="dynamische-programmierung.html"><a href="dynamische-programmierung.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 13: Dynamische Programmierung — Die Bellman-Gleichung und Order-Execution</span></a></li><li data-kapitel="mehrziel.html"><a href="mehrziel.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 14: Mehrere Ziele — Pareto-Fronten statt Gewichte</span></a></li><li data-kapitel="prognose.html"><a href="prognose.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 15: Predict-then-Optimize — die bessere Prognose, die schlechtere Entscheidung</span></a></li></ul></details><details class="sidebar-gruppe"><summary>Teil IV: Anwendungen — Energiewirtschaft und Finanzmärkte</summary><ul><li data-kapitel="bruecke.html"><a href="bruecke.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 16: Die Strukturbrücke — dieselbe Mathematik, zwei Welten</span></a></li><li data-kapitel="supplychain.html"><a href="supplychain.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 17: Supply-Chain und Energieeinsatz unter Unsicherheit</span></a></li><li data-kapitel="finanzdaten.html"><a href="finanzdaten.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 18: Finanzdaten-Modellierung — Renditen, Kovarianz und Shrinkage</span></a></li><li data-kapitel="markowitz.html"><a href="markowitz.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 19: Die moderne Portfoliotheorie nach Markowitz</span></a></li><li data-kapitel="cvar.html"><a href="cvar.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 20: Tail-Risiko, CVaR und Transaktionskosten</span></a></li><li data-kapitel="handelsmaschine.html"><a href="handelsmaschine.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 21: Die vollständige quantitative Handelsmaschine</span></a></li></ul></details><details class="sidebar-gruppe"><summary>Teil V: Praxis</summary><ul><li data-kapitel="praxisfallen.html"><a href="praxisfallen.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 22: Praxisfallen und der Weg zum produktiven Einsatz</span></a></li><li data-kapitel="testing.html"><a href="testing.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 23: Testen, Messen, Ausliefern</span></a></li><li data-kapitel="projektwerkstatt.html"><a href="projektwerkstatt.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Projektwerkstatt</span></a></li></ul></details><details class="sidebar-gruppe"><summary>Anhänge</summary><ul><li data-kapitel="anhang-loesungen.html"><a href="anhang-loesungen.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Anhang A: Lösungen zu allen Übungsaufgaben</span></a></li><li data-kapitel="anhang-modellierungsmuster.html"><a href="anhang-modellierungsmuster.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Anhang B: Katalog der Modellierungsmuster</span></a></li><li data-kapitel="anhang-fehlerdiagnose.html"><a href="anhang-fehlerdiagnose.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Anhang C: Fehlerdiagnose-Handbuch</span></a></li><li data-kapitel="anhang-spickzettel.html"><a href="anhang-spickzettel.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Anhang D: Spickzettel der Solver</span></a></li><li data-kapitel="anhang-glossar-literatur.html"><a href="anhang-glossar-literatur.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Anhang E: Glossar und Literatur</span></a></li></ul></details><ul class="sidebar-extra"><li><a href="programme.html"><svg class="icon" aria-hidden="true"><use href="#icon-book"></use></svg> Beispielprogramme</a></li><li><a href="stichwortverzeichnis.html"><svg class="icon" aria-hidden="true"><use href="#icon-search"></use></svg> Stichwortverzeichnis</a></li><li><a href="gesamtdokument.html"><svg class="icon" aria-hidden="true"><use href="#icon-external-link"></use></svg> Gesamtdokument (eine Seite)</a></li><li><a href="Operations_Research_mit_Python_Version_04.pdf" download><svg class="icon" aria-hidden="true"><use href="#icon-download"></use></svg> Download als PDF</a></li></ul></div></nav>
<main class="content">
<nav class="breadcrumb" aria-label="Breadcrumb"><a href="index.html">Start</a> <span>Teil II</span> <span>Kapitel 7: Constraint Programming mit CP-SAT — Logik, Scheduling und Zuweisung</span></nav>
<nav class="prev-next"><a class="prev-next-knopf prev-next-prev" href="milp.html"><svg class="icon" aria-hidden="true"><use href="#icon-chevron-left"></use></svg><span><small>Zurück</small>Kapitel 6: Gemischt-ganzzahlige Optimierung — Diskrete Entscheidungen und Branch-and-Bound</span></a><a class="prev-next-knopf prev-next-next" href="graphen.html"><span><small>Weiter</small>Kapitel 8: Graphen, Flüsse und Touren — Min-Cost-Flow, Matching und VRP</span><svg class="icon" aria-hidden="true"><use href="#icon-chevron-right"></use></svg></a></nav>
<article>
<h1 id="kap-cpsat">Kapitel 7: Constraint Programming mit CP-SAT — Logik, Scheduling und Zuweisung</h1>
<div class="card card-blick">
<blockquote>
<p><strong>📌 Kapitel auf einen Blick</strong></p>
<p><strong>Worum geht es?</strong> Um ein völlig anderes Denkmodell: Statt algebraischer Ungleichungen arbeitet Constraint Programming mit <strong>Wertebereichen</strong> und <strong>logischen Regeln</strong>. Für Dienstpläne, Zuordnungen und Reihenfolgen ist das die mit Abstand produktivste Methode.</p>
<p><strong>Voraussetzungen:</strong> <a href="milp.html#kap-milp">Kapitel 6</a> (Binärvariablen, logische Bedingungen).</p>
<p><strong>Danach können Sie:</strong> Zuweisungs-, Scheduling- und Reihenfolgeprobleme mit CP-SAT modellieren, globale Constraints einsetzen, weiche Ziele über Strafkosten steuern — und alle fünf Solver-Antworten auseinanderhalten, statt nur auf <code>OPTIMAL</code> zu prüfen.</p>
<p><strong>Zeitbedarf:</strong> ca. 6,5 Stunden.</p>
<p><strong>Programme:</strong><br />
<code>Propagation_Demo.py</code><br />
<code>CP_SAT_Vertretungssystem.py</code><br />
<code>JobShop_Intervalle.py</code><br />
<code>Parallele_Suche.py</code><br />
<code>CP_SAT_Statusfaelle.py</code><br />
<code>Strafgewichte.py</code></p>
<p><strong>Notebook:</strong> <a href="Notebooks_04/cpsat.ipynb">cpsat.ipynb</a><br />
<a href="https://colab.research.google.com/github/dschlueter/or-mit-python/blob/main/Notebooks_04/cpsat.ipynb">In Google Colab öffnen</a></p>
</blockquote>
</div>
<hr />
<h2 id="sec:cpsat-schnellstart">7.1 In 5 Minuten gelöst</h2>
<div class="card card-schnellstart">
<blockquote>
<p><strong>🚀 In 5 Minuten gelöst: Der Wochendienstplan</strong></p>
<p>Vier Personen, fünf Tage Bereitschaftsdienst. Jeder Tag muss von <strong>genau einer</strong> Person besetzt sein, niemand soll mehr als <strong>zwei</strong> Dienste bekommen, und drei Abwesenheiten sind bekannt.</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> ortools.sat.python <span class="im">import</span> cp_model</span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a>personen <span class="op">=</span> [<span class="st">&quot;Abel&quot;</span>, <span class="st">&quot;Bux&quot;</span>, <span class="st">&quot;Cor&quot;</span>, <span class="st">&quot;Dane&quot;</span>]</span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a>dienste <span class="op">=</span> [<span class="st">&quot;Mo&quot;</span>, <span class="st">&quot;Di&quot;</span>, <span class="st">&quot;Mi&quot;</span>, <span class="st">&quot;Do&quot;</span>, <span class="st">&quot;Fr&quot;</span>]</span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a>nicht_verfuegbar <span class="op">=</span> {(<span class="st">&quot;Bux&quot;</span>, <span class="st">&quot;Mi&quot;</span>), (<span class="st">&quot;Cor&quot;</span>, <span class="st">&quot;Mo&quot;</span>), (<span class="st">&quot;Cor&quot;</span>, <span class="st">&quot;Di&quot;</span>)}</span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a>m <span class="op">=</span> cp_model.CpModel()</span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a>x <span class="op">=</span> {(p, d): m.NewBoolVar(<span class="ss">f&quot;</span><span class="sc">{</span>p<span class="sc">}</span><span class="ss">_</span><span class="sc">{</span>d<span class="sc">}</span><span class="ss">&quot;</span>) <span class="cf">for</span> p <span class="kw">in</span> personen <span class="cf">for</span> d <span class="kw">in</span> dienste}</span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a><span class="cf">for</span> d <span class="kw">in</span> dienste:</span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a> m.AddExactlyOne(x[p, d] <span class="cf">for</span> p <span class="kw">in</span> personen) <span class="co"># jeder Tag genau einmal besetzt</span></span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a><span class="cf">for</span> p <span class="kw">in</span> personen:</span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a> m.Add(<span class="bu">sum</span>(x[p, d] <span class="cf">for</span> d <span class="kw">in</span> dienste) <span class="op">&lt;=</span> <span class="dv">2</span>) <span class="co"># niemand mehr als zwei Dienste</span></span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a><span class="cf">for</span> p, d <span class="kw">in</span> nicht_verfuegbar:</span>
<span id="cb1-14"><a href="#cb1-14" aria-hidden="true" tabindex="-1"></a> m.Add(x[p, d] <span class="op">==</span> <span class="dv">0</span>) <span class="co"># Abwesenheiten</span></span>
<span id="cb1-15"><a href="#cb1-15" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-16"><a href="#cb1-16" aria-hidden="true" tabindex="-1"></a>s <span class="op">=</span> cp_model.CpSolver()</span>
<span id="cb1-17"><a href="#cb1-17" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(s.StatusName(s.Solve(m)))</span>
<span id="cb1-18"><a href="#cb1-18" aria-hidden="true" tabindex="-1"></a><span class="cf">for</span> d <span class="kw">in</span> dienste:</span>
<span id="cb1-19"><a href="#cb1-19" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(d, <span class="st">&quot;-&gt;&quot;</span>, <span class="bu">next</span>(p <span class="cf">for</span> p <span class="kw">in</span> personen <span class="cf">if</span> s.Value(x[p, d])))</span></code></pre></div>
<p><strong>Ausgabe:</strong></p>
<pre><code>OPTIMAL
Mo -&gt; Dane
Di -&gt; Dane
Mi -&gt; Abel
Do -&gt; Bux
Fr -&gt; Abel</code></pre>
</blockquote>
</div>
<p><strong>Elf Zeilen Modell, und der Plan steht.</strong> Beachten Sie besonders die Zeile <code>m.AddExactlyOne(...)</code>. Sie sagt wörtlich, was gemeint ist: <em>genau eine</em> Person je Tag. In MILP hätte man dafür eine Gleichung <span class="math inline">\sum_p x_{p,d} = 1</span> aufgestellt — machbar, aber schon eine Übersetzungsleistung. Bei Regeln wie „diese beiden Dienste nicht direkt hintereinander“ oder „höchstens eine Nachtschicht pro Woche“ wächst dieser Übersetzungsaufwand rasch; in CP-SAT bleibt er eine Zeile.</p>
<p><strong>Und jetzt sehen Sie sich das Ergebnis genau an.</strong> <em>Cor bekommt keinen einzigen Dienst.</em> Dane arbeitet Montag <strong>und</strong> Dienstag hintereinander. Ist das ein Fehler?</p>
<p>Nein — und das ist die wichtigste Lektion dieses Kapitels. Der Plan erfüllt <strong>jede</strong> Regel, die wir aufgeschrieben haben. Wir haben nur nie nach Fairness gefragt, und wir haben nie gesagt, dass Dienste nicht aufeinanderfolgen sollen. Ein Solver liefert exakt das Bestellte — nicht das Gemeinte.</p>
<blockquote>
<p><strong>🎯 Merksatz</strong> Ein Modell, das durchläuft, ist noch kein richtiges Modell. Die eigentliche Arbeit im Constraint Programming ist nicht das Lösen, sondern das vollständige Aufschreiben dessen, was „ein guter Plan“ überhaupt heißt. Wie man Wünsche wie Fairness als <strong>weiche</strong> Bedingungen mit Strafkosten formuliert, zeigt <a href="#sec:cpsat-praxisfall-dynamisches-vertretungssystem">Abschnitt 7.5</a> — und <a href="#sec:cpsat-denkfehler">Abschnitt 7.10</a> zeigt, was passiert, wenn man ihr Gewicht falsch wählt.</p>
</blockquote>
<hr />
<h2 id="sec:cpsat-lernziele">7.2 Lernziele</h2>
<p>Nach diesem Kapitel können Sie …</p>
<ol type="1">
<li>… erklären, wodurch sich Constraint Programming von MILP unterscheidet.</li>
<li>… die Wirkung von <strong>Constraint Propagation</strong> an einem Beispiel nachvollziehen.</li>
<li>… die wichtigsten globalen Constraints einsetzen: <code>AddExactlyOne</code>, <code>AddAllDifferent</code>, <code>AddNoOverlap</code>, <code>AddCumulative</code>.</li>
<li>… mit <strong>Intervallvariablen</strong> ein Maschinenbelegungsproblem modellieren.</li>
<li>… harte Regeln und weiche Ziele in einem Modell kombinieren und gewichten.</li>
<li>… die fünf CP-SAT-Statusfälle unterscheiden — insbesondere <code>INFEASIBLE</code> (es gibt keine Lösung) von <code>UNKNOWN</code> (es wurde keine gefunden) und <code>MODEL_INVALID</code> (Ihr Code ist fehlerhaft).</li>
<li>… begründen, warum <code>num_workers = 1</code> für einen reproduzierbaren Lauf nötig ist und ein fester <code>random_seed</code> allein nicht genügt — und was ein Test deshalb prüfen darf.</li>
<li>… begründen, warum ein Strafgewicht ein <strong>Wechselkurs</strong> ist, und welche Regeln deshalb niemals in die Zielfunktion gehören.</li>
</ol>
<hr />
<h2 id="sec:cpsat-ein-anderes-denkmodell">7.3 Ein anderes Denkmodell</h2>
<p>Während LP und MILP auf algebraischen Gleichungen und Simplex-Matrizen beruhen, kommt <strong>Constraint Programming (CP)</strong> aus der künstlichen Intelligenz:</p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr class="header">
<th></th>
<th>MILP</th>
<th>Constraint Programming</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Variablen</td>
<td>reell oder ganzzahlig, mit Schranken</td>
<td>diskrete <strong>Wertebereiche</strong> (<em>domains</em>)</td>
</tr>
<tr class="even">
<td>Bedingungen</td>
<td>lineare Ungleichungen</td>
<td>beliebige logische Relationen</td>
</tr>
<tr class="odd">
<td>Suchprinzip</td>
<td>Relaxation + Verzweigen + Schranken</td>
<td><strong>Propagation</strong> + Verzweigen + Konfliktlernen</td>
</tr>
<tr class="even">
<td>Stärke</td>
<td>ökonomische Modelle, Kosten, Kapazitäten</td>
<td>Zuordnung, Reihenfolge, Zeitfenster, Regeln</td>
</tr>
<tr class="odd">
<td>Schwäche</td>
<td>viele logische Regeln → viele Big-M</td>
<td>reine Kostenoptimierung über Kontinuum</td>
</tr>
</tbody>
</table>
<p>Der <strong>CP-SAT-Solver</strong> von Google OR-Tools verbindet Constraint Programming mit modernen <strong>SAT-Techniken</strong> (<em>Boolean Satisfiability</em>, Erfüllbarkeitsproblem der Aussagenlogik), insbesondere <strong>CDCL</strong> (<em>Conflict-Driven Clause Learning</em>, konfliktgetriebenes Klausellernen). Er gehört zu den weltweit leistungsfähigsten Engines für Schichtpläne, Vertretungsplanung, Projektterminierung und Job-Shop-Scheduling.</p>
<h3 id="was-propagation-leistet">Was Propagation leistet</h3>
<figure>
<img src="bilder_04/kap06_cp_sat_propagation.svg" alt="Abb. 7.1: CP-SAT Propagation und Conflict Learning" />
<figcaption aria-hidden="true">Abb. 7.1: CP-SAT Propagation und Conflict Learning</figcaption>
</figure>
<blockquote>
<p><strong>✏️ Handrechnung 7.1: Propagation von Hand</strong></p>
<p>Gegeben: <span class="math inline">x_1 \in \{1,2,3,4\}</span>, <span class="math inline">x_2 \in \{2,3,4,5\}</span>, Bedingung <span class="math inline">x_1 + x_2 = 7</span>.</p>
<p><strong>Runde 1 — von <span class="math inline">x_1</span> aus:</strong> * <span class="math inline">x_1 = 1</span><span class="math inline">x_2 = 6</span>. Aber <span class="math inline">6 \notin \{2,3,4,5\}</span><strong><span class="math inline">1</span> aus der Domain von <span class="math inline">x_1</span> streichen.</strong> * <span class="math inline">x_1 = 2</span><span class="math inline">x_2 = 5</span> ✓ bleibt. * <span class="math inline">x_1 = 3,4</span><span class="math inline">x_2 = 4,3</span> ✓ bleiben. Neue Domain: <span class="math inline">x_1 \in \{2,3,4\}</span>.</p>
<p><strong>Runde 2 — von <span class="math inline">x_2</span> aus:</strong> * <span class="math inline">x_2 = 2</span><span class="math inline">x_1 = 5 \notin \{2,3,4\}</span><strong>streichen.</strong> * <span class="math inline">x_2 = 3,4,5</span><span class="math inline">x_1 = 4,3,2</span> ✓ Neue Domain: <span class="math inline">x_2 \in \{3,4,5\}</span>.</p>
<p>Aus <span class="math inline">4 \times 4 = 16</span> Kombinationen sind ohne jedes Ausprobieren <strong>9</strong> geworden. Bei tausenden verketteten Bedingungen wirkt dieser Effekt lawinenartig — das ist der Kern der Leistungsfähigkeit von CP.</p>
<p><strong>Und wenn ein Widerspruch auftritt?</strong> Dann lernt der SAT-Kern eine <strong>Sperrklausel</strong> (<em>nogood</em>): „Diese Kombination nie wieder probieren.“ Der Solver springt dann nicht nur einen Schritt zurück, sondern direkt zur Ursache des Konflikts (<em>non-chronological backtracking</em>).</p>
</blockquote>
<div class="sourceCode" id="cb3"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="co">#!/usr/bin/env python3</span></span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a><span class="co"># Propagation_Demo.py</span></span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a><span class="co">Kapitel CP-SAT: Propagation sichtbar machen.</span></span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a><span class="co">Vergleicht die Zahl der geprueften Kombinationen bei roher Aufzaehlung</span></span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a><span class="co">mit der Zahl der Verzweigungen, die CP-SAT tatsaechlich braucht.</span></span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> itertools</span>
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> ortools.sat.python <span class="im">import</span> cp_model</span>
<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-14"><a href="#cb3-14" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-15"><a href="#cb3-15" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> rohe_aufzaehlung(n: <span class="bu">int</span>, obergrenze: <span class="bu">int</span>) <span class="op">-&gt;</span> <span class="bu">tuple</span>[<span class="bu">int</span>, <span class="bu">int</span>]:</span>
<span id="cb3-16"><a href="#cb3-16" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Zaehlt ALLE Kombinationen und prueft jede einzeln (Brute Force).&quot;&quot;&quot;</span></span>
<span id="cb3-17"><a href="#cb3-17" aria-hidden="true" tabindex="-1"></a> geprueft <span class="op">=</span> <span class="dv">0</span></span>
<span id="cb3-18"><a href="#cb3-18" aria-hidden="true" tabindex="-1"></a> loesungen <span class="op">=</span> <span class="dv">0</span></span>
<span id="cb3-19"><a href="#cb3-19" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> kombination <span class="kw">in</span> itertools.product(<span class="bu">range</span>(<span class="dv">1</span>, obergrenze <span class="op">+</span> <span class="dv">1</span>), repeat<span class="op">=</span>n):</span>
<span id="cb3-20"><a href="#cb3-20" aria-hidden="true" tabindex="-1"></a> geprueft <span class="op">+=</span> <span class="dv">1</span></span>
<span id="cb3-21"><a href="#cb3-21" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="bu">len</span>(<span class="bu">set</span>(kombination)) <span class="op">==</span> n <span class="kw">and</span> <span class="bu">sum</span>(kombination) <span class="op">==</span> <span class="dv">3</span> <span class="op">*</span> n:</span>
<span id="cb3-22"><a href="#cb3-22" aria-hidden="true" tabindex="-1"></a> loesungen <span class="op">+=</span> <span class="dv">1</span></span>
<span id="cb3-23"><a href="#cb3-23" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> geprueft, loesungen</span>
<span id="cb3-24"><a href="#cb3-24" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-25"><a href="#cb3-25" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-26"><a href="#cb3-26" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> mit_cp_sat(n: <span class="bu">int</span>, obergrenze: <span class="bu">int</span>) <span class="op">-&gt;</span> <span class="bu">tuple</span>[<span class="bu">int</span>, <span class="bu">int</span>, <span class="bu">float</span>]:</span>
<span id="cb3-27"><a href="#cb3-27" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Dasselbe Problem deklarativ: alle verschieden, Summe = 3n.&quot;&quot;&quot;</span></span>
<span id="cb3-28"><a href="#cb3-28" aria-hidden="true" tabindex="-1"></a> modell <span class="op">=</span> cp_model.CpModel()</span>
<span id="cb3-29"><a href="#cb3-29" aria-hidden="true" tabindex="-1"></a> x <span class="op">=</span> [modell.NewIntVar(<span class="dv">1</span>, obergrenze, <span class="ss">f&quot;x</span><span class="sc">{</span>i<span class="sc">}</span><span class="ss">&quot;</span>) <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(n)]</span>
<span id="cb3-30"><a href="#cb3-30" aria-hidden="true" tabindex="-1"></a> modell.AddAllDifferent(x) <span class="co"># globales Constraint</span></span>
<span id="cb3-31"><a href="#cb3-31" aria-hidden="true" tabindex="-1"></a> modell.Add(<span class="bu">sum</span>(x) <span class="op">==</span> <span class="dv">3</span> <span class="op">*</span> n)</span>
<span id="cb3-32"><a href="#cb3-32" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-33"><a href="#cb3-33" aria-hidden="true" tabindex="-1"></a> loeser <span class="op">=</span> cp_model.CpSolver()</span>
<span id="cb3-34"><a href="#cb3-34" aria-hidden="true" tabindex="-1"></a> loeser.parameters.enumerate_all_solutions <span class="op">=</span> <span class="va">True</span></span>
<span id="cb3-35"><a href="#cb3-35" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-36"><a href="#cb3-36" aria-hidden="true" tabindex="-1"></a> <span class="kw">class</span> Zaehler(cp_model.CpSolverSolutionCallback):</span>
<span id="cb3-37"><a href="#cb3-37" aria-hidden="true" tabindex="-1"></a> <span class="kw">def</span> <span class="fu">__init__</span>(<span class="va">self</span>):</span>
<span id="cb3-38"><a href="#cb3-38" aria-hidden="true" tabindex="-1"></a> <span class="bu">super</span>().<span class="fu">__init__</span>()</span>
<span id="cb3-39"><a href="#cb3-39" aria-hidden="true" tabindex="-1"></a> <span class="va">self</span>.anzahl <span class="op">=</span> <span class="dv">0</span></span>
<span id="cb3-40"><a href="#cb3-40" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-41"><a href="#cb3-41" aria-hidden="true" tabindex="-1"></a> <span class="kw">def</span> on_solution_callback(<span class="va">self</span>):</span>
<span id="cb3-42"><a href="#cb3-42" aria-hidden="true" tabindex="-1"></a> <span class="va">self</span>.anzahl <span class="op">+=</span> <span class="dv">1</span></span>
<span id="cb3-43"><a href="#cb3-43" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-44"><a href="#cb3-44" aria-hidden="true" tabindex="-1"></a> zaehler <span class="op">=</span> Zaehler()</span>
<span id="cb3-45"><a href="#cb3-45" aria-hidden="true" tabindex="-1"></a> loeser.Solve(modell, zaehler)</span>
<span id="cb3-46"><a href="#cb3-46" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> zaehler.anzahl, loeser.NumBranches(), loeser.WallTime()</span>
<span id="cb3-47"><a href="#cb3-47" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-48"><a href="#cb3-48" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-49"><a href="#cb3-49" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> <span class="va">__name__</span> <span class="op">==</span> <span class="st">&quot;__main__&quot;</span>:</span>
<span id="cb3-50"><a href="#cb3-50" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">84</span>)</span>
<span id="cb3-51"><a href="#cb3-51" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; PROPAGATION: WIE VIEL ARBEIT SPART SICH DER SOLVER?&quot;</span>)</span>
<span id="cb3-52"><a href="#cb3-52" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; Aufgabe: n Zahlen aus 1..G, alle verschieden, Summe = 3n&quot;</span>)</span>
<span id="cb3-53"><a href="#cb3-53" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">84</span>)</span>
<span id="cb3-54"><a href="#cb3-54" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;n&#39;</span><span class="sc">:&gt;3}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;G&#39;</span><span class="sc">:&gt;4}</span><span class="ss"> | </span><span class="sc">{</span><span class="st">&#39;Brute Force&#39;</span><span class="sc">:&gt;14}</span><span class="ss"> | </span><span class="sc">{</span><span class="st">&#39;CP-SAT&#39;</span><span class="sc">:&gt;10}</span><span class="ss"> | &quot;</span></span>
<span id="cb3-55"><a href="#cb3-55" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;Ersparnis&#39;</span><span class="sc">:&gt;11}</span><span class="ss"> | </span><span class="sc">{</span><span class="st">&#39;Loesungen&#39;</span><span class="sc">:&gt;10}</span><span class="ss">&quot;</span>)</span>
<span id="cb3-56"><a href="#cb3-56" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">84</span>)</span>
<span id="cb3-57"><a href="#cb3-57" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-58"><a href="#cb3-58" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> n, grenze <span class="kw">in</span> [(<span class="dv">4</span>, <span class="dv">8</span>), (<span class="dv">5</span>, <span class="dv">10</span>), (<span class="dv">6</span>, <span class="dv">12</span>), (<span class="dv">7</span>, <span class="dv">14</span>)]:</span>
<span id="cb3-59"><a href="#cb3-59" aria-hidden="true" tabindex="-1"></a> kombis, treffer_bf <span class="op">=</span> rohe_aufzaehlung(n, grenze)</span>
<span id="cb3-60"><a href="#cb3-60" aria-hidden="true" tabindex="-1"></a> treffer_cp, verzweigungen, dauer <span class="op">=</span> mit_cp_sat(n, grenze)</span>
<span id="cb3-61"><a href="#cb3-61" aria-hidden="true" tabindex="-1"></a> <span class="cf">assert</span> treffer_bf <span class="op">==</span> treffer_cp, <span class="st">&quot;Beide Verfahren muessen gleich viele finden!&quot;</span></span>
<span id="cb3-62"><a href="#cb3-62" aria-hidden="true" tabindex="-1"></a> ersparnis <span class="op">=</span> kombis <span class="op">/</span> <span class="bu">max</span>(verzweigungen, <span class="dv">1</span>)</span>
<span id="cb3-63"><a href="#cb3-63" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>n<span class="sc">:&gt;3}</span><span class="ss"> </span><span class="sc">{</span>grenze<span class="sc">:&gt;4}</span><span class="ss"> | </span><span class="sc">{</span>kombis<span class="sc">:&gt;14,}</span><span class="ss"> | </span><span class="sc">{</span>verzweigungen<span class="sc">:&gt;10,}</span><span class="ss"> | &quot;</span></span>
<span id="cb3-64"><a href="#cb3-64" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>ersparnis<span class="sc">:&gt;10.0f}</span><span class="ss">x | </span><span class="sc">{</span>treffer_cp<span class="sc">:&gt;10,}</span><span class="ss">&quot;</span>)</span>
<span id="cb3-65"><a href="#cb3-65" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-66"><a href="#cb3-66" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">84</span>)</span>
<span id="cb3-67"><a href="#cb3-67" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;&#39;Brute Force&#39; = alle Kombinationen. &#39;CP-SAT&#39; = tatsaechliche Verzweigungen.&quot;</span>)</span>
<span id="cb3-68"><a href="#cb3-68" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Der Rest wurde durch Propagation ausgeschlossen, ohne ihn anzusehen.&quot;</span>)</span>
<span id="cb3-69"><a href="#cb3-69" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">84</span>)</span></code></pre></div>
<blockquote>
<p><strong>🎯 Merksatz</strong> MILP fragt: „Wie weit kann ich den Zielwert noch verbessern?“ (Schranken). CP fragt: „Welche Werte sind überhaupt noch möglich?“ (Propagation). Bei Problemen, deren Schwierigkeit in den <strong>Regeln</strong> liegt und nicht in den <strong>Kosten</strong>, ist die zweite Frage die produktivere.</p>
</blockquote>
<hr />
<h2 id="sec:cpsat-globale-constraints-die-bausteine-von-cp">7.4 Globale Constraints — die Bausteine von CP-SAT</h2>
<p>CP-SAT stellt mächtige Makros bereit, die in MILP hunderte Big-M-Ungleichungen erfordern würden. Jedes davon hat einen spezialisierten, hocheffizienten Propagator.</p>
<table style="width:100%;">
<colgroup>
<col style="width: 60%" />
<col style="width: 18%" />
<col style="width: 21%" />
</colgroup>
<thead>
<tr class="header">
<th>Constraint</th>
<th>Bedeutung</th>
<th>Typische Anwendung</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>AddAllDifferent(vars)</code></td>
<td>Alle Variablen haben <strong>paarweise verschiedene</strong> Werte</td>
<td>Sudoku, Zuordnung, Stundenplan-Räume</td>
</tr>
<tr class="even">
<td><code>AddExactlyOne(bools)</code></td>
<td><strong>Genau eine</strong> der Booleschen ist wahr</td>
<td>„Jede Schicht wird von genau einer Person besetzt“</td>
</tr>
<tr class="odd">
<td><code>AddAtMostOne(bools)</code></td>
<td><strong>Höchstens eine</strong> ist wahr</td>
<td>Exklusive Ressourcennutzung</td>
</tr>
<tr class="even">
<td><code>AddBoolOr(bools)</code></td>
<td><strong>Mindestens eine</strong> ist wahr</td>
<td>„Mindestens eine Fachkraft im Dienst“</td>
</tr>
<tr class="odd">
<td><code>AddImplication(a, b)</code></td>
<td><span class="math inline">a \Rightarrow b</span></td>
<td>„Wenn Frühschicht, dann keine Nachtschicht davor“</td>
</tr>
<tr class="even">
<td><code>NewIntervalVar(start, size, end, name)</code></td>
<td>Zeitintervall mit Start, Dauer, Ende</td>
<td>Aufträge, Operationen, Belegungen</td>
</tr>
<tr class="odd">
<td><code>AddNoOverlap(intervals)</code></td>
<td>Intervalle überschneiden sich <strong>nicht</strong></td>
<td>Eine Maschine, ein Raum, eine Person</td>
</tr>
<tr class="even">
<td><code>AddCumulative(intervals, bedarfe, kapazitaet)</code></td>
<td>Zu <strong>keinem Zeitpunkt</strong> übersteigt der Parallelverbrauch die Kapazität</td>
<td>Personalstärke, Stromlast, Kranstunden</td>
</tr>
<tr class="odd">
<td><code>AddCircuit(arcs)</code></td>
<td>Die gewählten Kanten bilden <strong>einen</strong> Rundweg</td>
<td>TSP-artige Reihenfolgeprobleme</td>
</tr>
<tr class="even">
<td><code>.OnlyEnforceIf(bool)</code></td>
<td>Bedingung gilt <strong>nur wenn</strong></td>
<td>Reifizierung: Regel an-/abschaltbar machen</td>
</tr>
</tbody>
</table>
<div class="card card-formel">
<blockquote>
<p><strong>🔤 Formel-Übersetzer: was ein globales Constraint zusichert</strong></p>
<p>Globale Constraints tragen englische Methodennamen, meinen aber alltägliche Regeln. Links die mathematische Zusicherung, rechts der Satz, den Sie im Planungsgespräch sagen würden:</p>
<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<thead>
<tr class="header">
<th>Mathematik</th>
<th>Alltagssprache</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><span class="math inline">\sum_{p \in P} x_{p,s} = 1 \quad \forall s</span> (<code>AddExactlyOne</code>)</td>
<td>„Jede Schicht wird von <strong>genau einer</strong> Person übernommen — keine bleibt offen, keine ist doppelt besetzt.“</td>
</tr>
<tr class="even">
<td><span class="math inline">\sum_{p \in P} x_{p,s} \le 1</span> (<code>AddAtMostOne</code>)</td>
<td>„Höchstens einer — es darf aber auch niemand.“</td>
</tr>
<tr class="odd">
<td><span class="math inline">x_i \ne x_j \quad \forall i \ne j</span> (<code>AddAllDifferent</code>)</td>
<td>„Keine zwei bekommen dasselbe“ — dieselbe Maschine, denselben Raum, dieselbe Startzeit.</td>
</tr>
<tr class="even">
<td><span class="math inline">a \Rightarrow b</span> (<code>AddImplication</code>)</td>
<td>„Wenn das eine gilt, muss auch das andere gelten.“ Die Umkehrung ist damit <strong>nicht</strong> gefordert.</td>
</tr>
<tr class="odd">
<td><span class="math inline">[s_i, e_i) \cap [s_j, e_j) = \emptyset</span> (<code>AddNoOverlap</code>)</td>
<td>„Diese Ressource kann immer nur eines auf einmal — was sich zeitlich überschneidet, ist verboten.“</td>
</tr>
<tr class="even">
<td><span class="math inline">\sum_{i:\, t \in [s_i, e_i)} r_i \le C \quad \forall t</span> (<code>AddCumulative</code>)</td>
<td>„Zu <strong>keinem Zeitpunkt</strong> dürfen mehr als <span class="math inline">C</span> Einheiten gleichzeitig gebraucht werden“ — Personalstärke, Stromlast, Kranstunden.</td>
</tr>
<tr class="odd">
<td>Bedingung gilt nur falls <span class="math inline">b = 1</span> (<code>.OnlyEnforceIf(b)</code>)</td>
<td>„Diese Regel gilt nur, wenn … “ — so wird aus einer festen Regel eine ein- und ausschaltbare.</td>
</tr>
</tbody>
</table>
<p><strong>Der Unterschied zu MILP in einem Satz:</strong> Dort formuliert man <em>Ungleichungen, aus denen die Regel folgt</em>; hier schreibt man <strong>die Regel selbst</strong> hin.</p>
</blockquote>
</div>
<blockquote>
<p><strong>💡 Warum globale Constraints so viel bringen</strong> <code>AddAllDifferent</code> über 9 Variablen ersetzt <span class="math inline">\binom{9}{2} = 36</span> paarweise Ungleichheitsbedingungen — und propagiert <strong>stärker</strong> als die 36 Einzelbedingungen zusammen, weil der Propagator die Struktur als Ganzes betrachtet (er nutzt Matching-Theorie auf bipartiten Graphen). Ein MILP-Modell derselben Regel bräuchte Big-M-Konstruktionen und würde deutlich langsamer laufen.</p>
</blockquote>
<hr />
<h2 id="sec:cpsat-praxisfall-dynamisches-vertretungssystem">7.5 Praxisfall: Dynamisches Vertretungssystem</h2>
<p><strong>Problemstellung.</strong> Eine Schule muss nach spontanen Ausfällen vier Unterrichtsstunden nachbesetzen.</p>
<ul>
<li>4 offene Stunden (Zeitslots 14), 5 verfügbare Lehrkräfte.</li>
<li><strong>Harte Regeln:</strong>
<ol type="1">
<li>Jede Stunde wird von <strong>genau einer</strong> Person übernommen.</li>
<li>Niemand kann an zwei Orten gleichzeitig sein.</li>
<li><strong>Qualifikation:</strong> Nur fachlich qualifizierte Personen dürfen übernehmen.</li>
<li><strong>Maximallast:</strong> Höchstens 2 Vertretungsstunden pro Person und Tag.</li>
</ol></li>
<li><strong>Weiche Ziele</strong> (über Strafkosten):
<ul>
<li>Personen mit hoher Vorbelastung möglichst schonen.</li>
<li><strong>Freistundenlöcher</strong> vermeiden (Stunde 1 und 3 arbeiten, Stunde 2 frei).</li>
<li>Last fair verteilen.</li>
</ul></li>
</ul>
<div class="sourceCode" id="cb4"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="co">#!/usr/bin/env python3</span></span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a><span class="co"># CP_SAT_Vertretungssystem.py</span></span>
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a><span class="co">Kapitel CP-SAT: Dynamisches Vertretungs- und Einsatzplanungssystem mit CP-SAT.</span></span>
<span id="cb4-6"><a href="#cb4-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-7"><a href="#cb4-7" aria-hidden="true" tabindex="-1"></a><span class="co">Eigenschaften:</span></span>
<span id="cb4-8"><a href="#cb4-8" aria-hidden="true" tabindex="-1"></a><span class="co"> * Solver-Status wird ueber SolverStatus aus or_kern.py ausgewertet - dieselbe</span></span>
<span id="cb4-9"><a href="#cb4-9" aria-hidden="true" tabindex="-1"></a><span class="co"> Fallunterscheidung wie bei jedem anderen Solver im Buch</span></span>
<span id="cb4-10"><a href="#cb4-10" aria-hidden="true" tabindex="-1"></a><span class="co"> * Strafkosten-Zerlegung wird ausgewiesen (Erklaerbarkeit, Kapitel Praxisfallen)</span></span>
<span id="cb4-11"><a href="#cb4-11" aria-hidden="true" tabindex="-1"></a><span class="co"> * Fairness-Kriterium ergaenzt (Spannweite der Belastung minimieren)</span></span>
<span id="cb4-12"><a href="#cb4-12" aria-hidden="true" tabindex="-1"></a><span class="co"> * Vorabdiagnose auf offensichtliche Unloesbarkeit</span></span>
<span id="cb4-13"><a href="#cb4-13" aria-hidden="true" tabindex="-1"></a><span class="co"> * Abnahmepruefung des fertigen Plans OHNE den Solver zu fragen</span></span>
<span id="cb4-14"><a href="#cb4-14" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-15"><a href="#cb4-15" aria-hidden="true" tabindex="-1"></a><span class="co">Benoetigt: ortools, pandas, pydantic (ueber or_kern)</span></span>
<span id="cb4-16"><a href="#cb4-16" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb4-17"><a href="#cb4-17" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-18"><a href="#cb4-18" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> __future__ <span class="im">import</span> annotations</span>
<span id="cb4-19"><a href="#cb4-19" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-20"><a href="#cb4-20" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> ortools.sat.python <span class="im">import</span> cp_model</span>
<span id="cb4-21"><a href="#cb4-21" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> pandas <span class="im">as</span> pd</span>
<span id="cb4-22"><a href="#cb4-22" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-23"><a href="#cb4-23" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> or_kern <span class="im">import</span> Loesung, SolverStatus, status_von_cpsat</span>
<span id="cb4-24"><a href="#cb4-24" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-25"><a href="#cb4-25" aria-hidden="true" tabindex="-1"></a><span class="co"># --- 1. Datenbasis --------------------------------------------------------</span></span>
<span id="cb4-26"><a href="#cb4-26" aria-hidden="true" tabindex="-1"></a>SLOTS <span class="op">=</span> [<span class="dv">1</span>, <span class="dv">2</span>, <span class="dv">3</span>, <span class="dv">4</span>]</span>
<span id="cb4-27"><a href="#cb4-27" aria-hidden="true" tabindex="-1"></a>FAECHER <span class="op">=</span> [<span class="st">&quot;Mathematik&quot;</span>, <span class="st">&quot;Physik&quot;</span>, <span class="st">&quot;Mathematik&quot;</span>, <span class="st">&quot;Informatik&quot;</span>]</span>
<span id="cb4-28"><a href="#cb4-28" aria-hidden="true" tabindex="-1"></a>KLASSEN <span class="op">=</span> [<span class="st">&quot;Klasse 8a&quot;</span>, <span class="st">&quot;Klasse 10b&quot;</span>, <span class="st">&quot;Klasse 7a&quot;</span>, <span class="st">&quot;Klasse 9c&quot;</span>]</span>
<span id="cb4-29"><a href="#cb4-29" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-30"><a href="#cb4-30" aria-hidden="true" tabindex="-1"></a>PERSONAL <span class="op">=</span> [<span class="st">&quot;Frau_Mueller&quot;</span>, <span class="st">&quot;Herr_Schmidt&quot;</span>, <span class="st">&quot;Frau_Albrecht&quot;</span>, <span class="st">&quot;Herr_Bauer&quot;</span>, <span class="st">&quot;Frau_Koch&quot;</span>]</span>
<span id="cb4-31"><a href="#cb4-31" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-32"><a href="#cb4-32" aria-hidden="true" tabindex="-1"></a>QUALIFIKATION <span class="op">=</span> {</span>
<span id="cb4-33"><a href="#cb4-33" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Frau_Mueller&quot;</span>: {<span class="st">&quot;Mathematik&quot;</span>, <span class="st">&quot;Physik&quot;</span>},</span>
<span id="cb4-34"><a href="#cb4-34" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Herr_Schmidt&quot;</span>: {<span class="st">&quot;Informatik&quot;</span>, <span class="st">&quot;Mathematik&quot;</span>},</span>
<span id="cb4-35"><a href="#cb4-35" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Frau_Albrecht&quot;</span>: {<span class="st">&quot;Physik&quot;</span>, <span class="st">&quot;Informatik&quot;</span>},</span>
<span id="cb4-36"><a href="#cb4-36" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Herr_Bauer&quot;</span>: {<span class="st">&quot;Mathematik&quot;</span>},</span>
<span id="cb4-37"><a href="#cb4-37" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Frau_Koch&quot;</span>: {<span class="st">&quot;Mathematik&quot;</span>, <span class="st">&quot;Physik&quot;</span>, <span class="st">&quot;Informatik&quot;</span>},</span>
<span id="cb4-38"><a href="#cb4-38" aria-hidden="true" tabindex="-1"></a>}</span>
<span id="cb4-39"><a href="#cb4-39" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-40"><a href="#cb4-40" aria-hidden="true" tabindex="-1"></a>VORBELASTUNG <span class="op">=</span> { <span class="co"># bereits geleistete Stunden heute</span></span>
<span id="cb4-41"><a href="#cb4-41" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Frau_Mueller&quot;</span>: <span class="dv">1</span>, <span class="st">&quot;Herr_Schmidt&quot;</span>: <span class="dv">0</span>, <span class="st">&quot;Frau_Albrecht&quot;</span>: <span class="dv">2</span>,</span>
<span id="cb4-42"><a href="#cb4-42" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Herr_Bauer&quot;</span>: <span class="dv">0</span>, <span class="st">&quot;Frau_Koch&quot;</span>: <span class="dv">1</span>,</span>
<span id="cb4-43"><a href="#cb4-43" aria-hidden="true" tabindex="-1"></a>}</span>
<span id="cb4-44"><a href="#cb4-44" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-45"><a href="#cb4-45" aria-hidden="true" tabindex="-1"></a>MAX_VERTRETUNGEN <span class="op">=</span> <span class="dv">2</span></span>
<span id="cb4-46"><a href="#cb4-46" aria-hidden="true" tabindex="-1"></a>STRAFE_VORBELASTUNG <span class="op">=</span> <span class="dv">50</span> <span class="co"># je Stunde Vorbelastung und Zuweisung</span></span>
<span id="cb4-47"><a href="#cb4-47" aria-hidden="true" tabindex="-1"></a>STRAFE_LOCH <span class="op">=</span> <span class="dv">80</span> <span class="co"># je Freistundenloch</span></span>
<span id="cb4-48"><a href="#cb4-48" aria-hidden="true" tabindex="-1"></a>STRAFE_UNFAIRNESS <span class="op">=</span> <span class="dv">30</span> <span class="co"># je Einheit Spannweite der Gesamtbelastung</span></span>
<span id="cb4-49"><a href="#cb4-49" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-50"><a href="#cb4-50" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-51"><a href="#cb4-51" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> pruefe_grundsaetzliche_loesbarkeit() <span class="op">-&gt;</span> <span class="bu">bool</span>:</span>
<span id="cb4-52"><a href="#cb4-52" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Vorabdiagnose: Gibt es fuer jeden Slot ueberhaupt qualifiziertes Personal?&quot;&quot;&quot;</span></span>
<span id="cb4-53"><a href="#cb4-53" aria-hidden="true" tabindex="-1"></a> ok <span class="op">=</span> <span class="va">True</span></span>
<span id="cb4-54"><a href="#cb4-54" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> s, fach <span class="kw">in</span> <span class="bu">enumerate</span>(FAECHER):</span>
<span id="cb4-55"><a href="#cb4-55" aria-hidden="true" tabindex="-1"></a> kandidaten <span class="op">=</span> [p <span class="cf">for</span> p <span class="kw">in</span> PERSONAL <span class="cf">if</span> fach <span class="kw">in</span> QUALIFIKATION[p]]</span>
<span id="cb4-56"><a href="#cb4-56" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> kandidaten:</span>
<span id="cb4-57"><a href="#cb4-57" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; UNLOESBAR: Fuer Slot </span><span class="sc">{</span>SLOTS[s]<span class="sc">}</span><span class="ss"> (</span><span class="sc">{</span>fach<span class="sc">}</span><span class="ss">) gibt es niemanden.&quot;</span>)</span>
<span id="cb4-58"><a href="#cb4-58" aria-hidden="true" tabindex="-1"></a> ok <span class="op">=</span> <span class="va">False</span></span>
<span id="cb4-59"><a href="#cb4-59" aria-hidden="true" tabindex="-1"></a> kapazitaet <span class="op">=</span> <span class="bu">len</span>(PERSONAL) <span class="op">*</span> MAX_VERTRETUNGEN</span>
<span id="cb4-60"><a href="#cb4-60" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> kapazitaet <span class="op">&lt;</span> <span class="bu">len</span>(SLOTS):</span>
<span id="cb4-61"><a href="#cb4-61" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; UNLOESBAR: Kapazitaet </span><span class="sc">{</span>kapazitaet<span class="sc">}</span><span class="ss"> &lt; </span><span class="sc">{</span><span class="bu">len</span>(SLOTS)<span class="sc">}</span><span class="ss"> offene Stunden.&quot;</span>)</span>
<span id="cb4-62"><a href="#cb4-62" aria-hidden="true" tabindex="-1"></a> ok <span class="op">=</span> <span class="va">False</span></span>
<span id="cb4-63"><a href="#cb4-63" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> ok</span>
<span id="cb4-64"><a href="#cb4-64" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-65"><a href="#cb4-65" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-66"><a href="#cb4-66" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> pruefe_plan(plan: <span class="bu">dict</span>[<span class="bu">int</span>, <span class="bu">str</span>]) <span class="op">-&gt;</span> <span class="bu">list</span>[<span class="bu">str</span>]:</span>
<span id="cb4-67"><a href="#cb4-67" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Prueft den fertigen Plan gegen die harten Regeln - ohne den Solver.</span></span>
<span id="cb4-68"><a href="#cb4-68" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-69"><a href="#cb4-69" aria-hidden="true" tabindex="-1"></a><span class="co"> Der Solver kann nur pruefen, was ihm gesagt wurde. Diese Funktion prueft</span></span>
<span id="cb4-70"><a href="#cb4-70" aria-hidden="true" tabindex="-1"></a><span class="co"> gegen die ANFORDERUNG und benutzt dafuer bewusst keine Modellvariable</span></span>
<span id="cb4-71"><a href="#cb4-71" aria-hidden="true" tabindex="-1"></a><span class="co"> (siehe Kapitel Praxisfallen). Leere Liste heisst bestanden.</span></span>
<span id="cb4-72"><a href="#cb4-72" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb4-73"><a href="#cb4-73" aria-hidden="true" tabindex="-1"></a> beanstandungen: <span class="bu">list</span>[<span class="bu">str</span>] <span class="op">=</span> []</span>
<span id="cb4-74"><a href="#cb4-74" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="bu">sorted</span>(plan) <span class="op">!=</span> <span class="bu">list</span>(<span class="bu">range</span>(<span class="bu">len</span>(SLOTS))):</span>
<span id="cb4-75"><a href="#cb4-75" aria-hidden="true" tabindex="-1"></a> beanstandungen.append(<span class="ss">f&quot;nicht jede Stunde genau einmal besetzt: </span><span class="sc">{</span><span class="bu">sorted</span>(plan)<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb4-76"><a href="#cb4-76" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> beanstandungen</span>
<span id="cb4-77"><a href="#cb4-77" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> s, person <span class="kw">in</span> plan.items():</span>
<span id="cb4-78"><a href="#cb4-78" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> FAECHER[s] <span class="kw">not</span> <span class="kw">in</span> QUALIFIKATION[person]:</span>
<span id="cb4-79"><a href="#cb4-79" aria-hidden="true" tabindex="-1"></a> beanstandungen.append(<span class="ss">f&quot;</span><span class="sc">{</span>person<span class="sc">}</span><span class="ss"> ist nicht fuer </span><span class="sc">{</span>FAECHER[s]<span class="sc">}</span><span class="ss"> qualifiziert&quot;</span>)</span>
<span id="cb4-80"><a href="#cb4-80" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> person <span class="kw">in</span> PERSONAL:</span>
<span id="cb4-81"><a href="#cb4-81" aria-hidden="true" tabindex="-1"></a> anzahl <span class="op">=</span> <span class="bu">sum</span>(<span class="dv">1</span> <span class="cf">for</span> p <span class="kw">in</span> plan.values() <span class="cf">if</span> p <span class="op">==</span> person)</span>
<span id="cb4-82"><a href="#cb4-82" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> anzahl <span class="op">&gt;</span> MAX_VERTRETUNGEN:</span>
<span id="cb4-83"><a href="#cb4-83" aria-hidden="true" tabindex="-1"></a> beanstandungen.append(<span class="ss">f&quot;</span><span class="sc">{</span>person<span class="sc">}</span><span class="ss"> hat </span><span class="sc">{</span>anzahl<span class="sc">}</span><span class="ss"> Vertretungen &quot;</span></span>
<span id="cb4-84"><a href="#cb4-84" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;(hoechstens </span><span class="sc">{</span>MAX_VERTRETUNGEN<span class="sc">}</span><span class="ss">)&quot;</span>)</span>
<span id="cb4-85"><a href="#cb4-85" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> beanstandungen</span>
<span id="cb4-86"><a href="#cb4-86" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-87"><a href="#cb4-87" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-88"><a href="#cb4-88" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> plane_vertretung() <span class="op">-&gt;</span> <span class="bu">list</span>[<span class="bu">dict</span>[<span class="bu">str</span>, <span class="bu">str</span>]] <span class="op">|</span> <span class="va">None</span>:</span>
<span id="cb4-89"><a href="#cb4-89" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> pruefe_grundsaetzliche_loesbarkeit():</span>
<span id="cb4-90"><a href="#cb4-90" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="va">None</span></span>
<span id="cb4-91"><a href="#cb4-91" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-92"><a href="#cb4-92" aria-hidden="true" tabindex="-1"></a> modell <span class="op">=</span> cp_model.CpModel()</span>
<span id="cb4-93"><a href="#cb4-93" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-94"><a href="#cb4-94" aria-hidden="true" tabindex="-1"></a> <span class="co"># Entscheidungsvariablen: x[person, slot] = 1 &lt;=&gt; Person uebernimmt Slot</span></span>
<span id="cb4-95"><a href="#cb4-95" aria-hidden="true" tabindex="-1"></a> x <span class="op">=</span> {(p, s): modell.NewBoolVar(<span class="ss">f&quot;zuweisung_</span><span class="sc">{</span>p<span class="sc">}</span><span class="ss">_slot</span><span class="sc">{</span>s<span class="op">+</span><span class="dv">1</span><span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb4-96"><a href="#cb4-96" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> p <span class="kw">in</span> PERSONAL <span class="cf">for</span> s <span class="kw">in</span> <span class="bu">range</span>(<span class="bu">len</span>(SLOTS))}</span>
<span id="cb4-97"><a href="#cb4-97" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-98"><a href="#cb4-98" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- HARTE NEBENBEDINGUNGEN ------------------------------------------</span></span>
<span id="cb4-99"><a href="#cb4-99" aria-hidden="true" tabindex="-1"></a> <span class="co"># H1: Jede Stunde genau einmal besetzen</span></span>
<span id="cb4-100"><a href="#cb4-100" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> s <span class="kw">in</span> <span class="bu">range</span>(<span class="bu">len</span>(SLOTS)):</span>
<span id="cb4-101"><a href="#cb4-101" aria-hidden="true" tabindex="-1"></a> modell.AddExactlyOne(x[p, s] <span class="cf">for</span> p <span class="kw">in</span> PERSONAL)</span>
<span id="cb4-102"><a href="#cb4-102" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-103"><a href="#cb4-103" aria-hidden="true" tabindex="-1"></a> <span class="co"># H2: Qualifikation - unqualifizierte Zuweisung ausschliessen</span></span>
<span id="cb4-104"><a href="#cb4-104" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> s, fach <span class="kw">in</span> <span class="bu">enumerate</span>(FAECHER):</span>
<span id="cb4-105"><a href="#cb4-105" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> p <span class="kw">in</span> PERSONAL:</span>
<span id="cb4-106"><a href="#cb4-106" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> fach <span class="kw">not</span> <span class="kw">in</span> QUALIFIKATION[p]:</span>
<span id="cb4-107"><a href="#cb4-107" aria-hidden="true" tabindex="-1"></a> modell.Add(x[p, s] <span class="op">==</span> <span class="dv">0</span>)</span>
<span id="cb4-108"><a href="#cb4-108" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-109"><a href="#cb4-109" aria-hidden="true" tabindex="-1"></a> <span class="co"># H3: Hoechstens MAX_VERTRETUNGEN Stunden je Person</span></span>
<span id="cb4-110"><a href="#cb4-110" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> p <span class="kw">in</span> PERSONAL:</span>
<span id="cb4-111"><a href="#cb4-111" aria-hidden="true" tabindex="-1"></a> modell.Add(<span class="bu">sum</span>(x[p, s] <span class="cf">for</span> s <span class="kw">in</span> <span class="bu">range</span>(<span class="bu">len</span>(SLOTS))) <span class="op">&lt;=</span> MAX_VERTRETUNGEN)</span>
<span id="cb4-112"><a href="#cb4-112" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-113"><a href="#cb4-113" aria-hidden="true" tabindex="-1"></a> <span class="co"># (H4 &quot;nicht an zwei Orten gleichzeitig&quot; ist durch H1 bereits erfuellt:</span></span>
<span id="cb4-114"><a href="#cb4-114" aria-hidden="true" tabindex="-1"></a> <span class="co"># jeder Slot hat genau eine Person, und eine Person kann pro Slot nur</span></span>
<span id="cb4-115"><a href="#cb4-115" aria-hidden="true" tabindex="-1"></a> <span class="co"># eine Variable auf 1 setzen.)</span></span>
<span id="cb4-116"><a href="#cb4-116" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-117"><a href="#cb4-117" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- WEICHE ZIELE ----------------------------------------------------</span></span>
<span id="cb4-118"><a href="#cb4-118" aria-hidden="true" tabindex="-1"></a> strafterme <span class="op">=</span> []</span>
<span id="cb4-119"><a href="#cb4-119" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-120"><a href="#cb4-120" aria-hidden="true" tabindex="-1"></a> <span class="co"># W1: Vorbelastete Personen schonen</span></span>
<span id="cb4-121"><a href="#cb4-121" aria-hidden="true" tabindex="-1"></a> strafe_last <span class="op">=</span> []</span>
<span id="cb4-122"><a href="#cb4-122" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> p <span class="kw">in</span> PERSONAL:</span>
<span id="cb4-123"><a href="#cb4-123" aria-hidden="true" tabindex="-1"></a> zuweisungen <span class="op">=</span> <span class="bu">sum</span>(x[p, s] <span class="cf">for</span> s <span class="kw">in</span> <span class="bu">range</span>(<span class="bu">len</span>(SLOTS)))</span>
<span id="cb4-124"><a href="#cb4-124" aria-hidden="true" tabindex="-1"></a> strafe_last.append(VORBELASTUNG[p] <span class="op">*</span> STRAFE_VORBELASTUNG <span class="op">*</span> zuweisungen)</span>
<span id="cb4-125"><a href="#cb4-125" aria-hidden="true" tabindex="-1"></a> strafterme.extend(strafe_last)</span>
<span id="cb4-126"><a href="#cb4-126" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-127"><a href="#cb4-127" aria-hidden="true" tabindex="-1"></a> <span class="co"># W2: Freistundenloecher vermeiden (Muster: arbeitet, frei, arbeitet)</span></span>
<span id="cb4-128"><a href="#cb4-128" aria-hidden="true" tabindex="-1"></a> loch_variablen <span class="op">=</span> []</span>
<span id="cb4-129"><a href="#cb4-129" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> p <span class="kw">in</span> PERSONAL:</span>
<span id="cb4-130"><a href="#cb4-130" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> s <span class="kw">in</span> <span class="bu">range</span>(<span class="bu">len</span>(SLOTS) <span class="op">-</span> <span class="dv">2</span>):</span>
<span id="cb4-131"><a href="#cb4-131" aria-hidden="true" tabindex="-1"></a> loch <span class="op">=</span> modell.NewBoolVar(<span class="ss">f&quot;loch_</span><span class="sc">{</span>p<span class="sc">}</span><span class="ss">_</span><span class="sc">{</span>s<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb4-132"><a href="#cb4-132" aria-hidden="true" tabindex="-1"></a> <span class="co"># Reifizierung: loch == 1 &lt;=&gt; (x_s AND NOT x_{s+1} AND x_{s+2})</span></span>
<span id="cb4-133"><a href="#cb4-133" aria-hidden="true" tabindex="-1"></a> modell.AddBoolAnd([x[p, s], x[p, s <span class="op">+</span> <span class="dv">1</span>].Not(), x[p, s <span class="op">+</span> <span class="dv">2</span>]]).OnlyEnforceIf(loch)</span>
<span id="cb4-134"><a href="#cb4-134" aria-hidden="true" tabindex="-1"></a> modell.AddBoolOr([x[p, s].Not(), x[p, s <span class="op">+</span> <span class="dv">1</span>], x[p, s <span class="op">+</span> <span class="dv">2</span>].Not()]) <span class="op">\</span></span>
<span id="cb4-135"><a href="#cb4-135" aria-hidden="true" tabindex="-1"></a> .OnlyEnforceIf(loch.Not())</span>
<span id="cb4-136"><a href="#cb4-136" aria-hidden="true" tabindex="-1"></a> loch_variablen.append(loch)</span>
<span id="cb4-137"><a href="#cb4-137" aria-hidden="true" tabindex="-1"></a> strafterme.append(loch <span class="op">*</span> STRAFE_LOCH)</span>
<span id="cb4-138"><a href="#cb4-138" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-139"><a href="#cb4-139" aria-hidden="true" tabindex="-1"></a> <span class="co"># W3: Fairness - Spannweite der Gesamtbelastung minimieren</span></span>
<span id="cb4-140"><a href="#cb4-140" aria-hidden="true" tabindex="-1"></a> gesamtlast <span class="op">=</span> {}</span>
<span id="cb4-141"><a href="#cb4-141" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> p <span class="kw">in</span> PERSONAL:</span>
<span id="cb4-142"><a href="#cb4-142" aria-hidden="true" tabindex="-1"></a> last <span class="op">=</span> modell.NewIntVar(<span class="dv">0</span>, <span class="bu">len</span>(SLOTS) <span class="op">+</span> <span class="bu">max</span>(VORBELASTUNG.values()), <span class="ss">f&quot;last_</span><span class="sc">{</span>p<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb4-143"><a href="#cb4-143" aria-hidden="true" tabindex="-1"></a> modell.Add(last <span class="op">==</span> VORBELASTUNG[p] <span class="op">+</span> <span class="bu">sum</span>(x[p, s] <span class="cf">for</span> s <span class="kw">in</span> <span class="bu">range</span>(<span class="bu">len</span>(SLOTS))))</span>
<span id="cb4-144"><a href="#cb4-144" aria-hidden="true" tabindex="-1"></a> gesamtlast[p] <span class="op">=</span> last</span>
<span id="cb4-145"><a href="#cb4-145" aria-hidden="true" tabindex="-1"></a> max_last <span class="op">=</span> modell.NewIntVar(<span class="dv">0</span>, <span class="dv">10</span>, <span class="st">&quot;max_last&quot;</span>)</span>
<span id="cb4-146"><a href="#cb4-146" aria-hidden="true" tabindex="-1"></a> min_last <span class="op">=</span> modell.NewIntVar(<span class="dv">0</span>, <span class="dv">10</span>, <span class="st">&quot;min_last&quot;</span>)</span>
<span id="cb4-147"><a href="#cb4-147" aria-hidden="true" tabindex="-1"></a> modell.AddMaxEquality(max_last, <span class="bu">list</span>(gesamtlast.values()))</span>
<span id="cb4-148"><a href="#cb4-148" aria-hidden="true" tabindex="-1"></a> modell.AddMinEquality(min_last, <span class="bu">list</span>(gesamtlast.values()))</span>
<span id="cb4-149"><a href="#cb4-149" aria-hidden="true" tabindex="-1"></a> spannweite <span class="op">=</span> modell.NewIntVar(<span class="dv">0</span>, <span class="dv">10</span>, <span class="st">&quot;spannweite&quot;</span>)</span>
<span id="cb4-150"><a href="#cb4-150" aria-hidden="true" tabindex="-1"></a> modell.Add(spannweite <span class="op">==</span> max_last <span class="op">-</span> min_last)</span>
<span id="cb4-151"><a href="#cb4-151" aria-hidden="true" tabindex="-1"></a> strafterme.append(spannweite <span class="op">*</span> STRAFE_UNFAIRNESS)</span>
<span id="cb4-152"><a href="#cb4-152" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-153"><a href="#cb4-153" aria-hidden="true" tabindex="-1"></a> modell.Minimize(<span class="bu">sum</span>(strafterme))</span>
<span id="cb4-154"><a href="#cb4-154" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-155"><a href="#cb4-155" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- LOESEN -----------------------------------------------------------</span></span>
<span id="cb4-156"><a href="#cb4-156" aria-hidden="true" tabindex="-1"></a> loeser <span class="op">=</span> cp_model.CpSolver()</span>
<span id="cb4-157"><a href="#cb4-157" aria-hidden="true" tabindex="-1"></a> loeser.parameters.max_time_in_seconds <span class="op">=</span> <span class="fl">5.0</span></span>
<span id="cb4-158"><a href="#cb4-158" aria-hidden="true" tabindex="-1"></a> <span class="co"># Reproduzierbarkeit: ein Arbeiter, fester Startwert. Ohne das kann CP-SAT</span></span>
<span id="cb4-159"><a href="#cb4-159" aria-hidden="true" tabindex="-1"></a> <span class="co"># bei mehreren gleich guten Plaenen von Lauf zu Lauf einen anderen liefern</span></span>
<span id="cb4-160"><a href="#cb4-160" aria-hidden="true" tabindex="-1"></a> <span class="co"># (siehe JobShop_Intervalle.py im naechsten Abschnitt).</span></span>
<span id="cb4-161"><a href="#cb4-161" aria-hidden="true" tabindex="-1"></a> loeser.parameters.num_workers <span class="op">=</span> <span class="dv">1</span></span>
<span id="cb4-162"><a href="#cb4-162" aria-hidden="true" tabindex="-1"></a> loeser.parameters.random_seed <span class="op">=</span> <span class="dv">1</span></span>
<span id="cb4-163"><a href="#cb4-163" aria-hidden="true" tabindex="-1"></a> rohstatus <span class="op">=</span> loeser.Solve(modell)</span>
<span id="cb4-164"><a href="#cb4-164" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-165"><a href="#cb4-165" aria-hidden="true" tabindex="-1"></a> <span class="co"># Der Rueckgabewert wird in die gemeinsame Sprache uebersetzt. Ab hier</span></span>
<span id="cb4-166"><a href="#cb4-166" aria-hidden="true" tabindex="-1"></a> <span class="co"># sieht die Auswertung genauso aus wie bei HiGHS, GLOP oder CVXPY.</span></span>
<span id="cb4-167"><a href="#cb4-167" aria-hidden="true" tabindex="-1"></a> status <span class="op">=</span> status_von_cpsat(rohstatus)</span>
<span id="cb4-168"><a href="#cb4-168" aria-hidden="true" tabindex="-1"></a> loesung <span class="op">=</span> Loesung(</span>
<span id="cb4-169"><a href="#cb4-169" aria-hidden="true" tabindex="-1"></a> status<span class="op">=</span>status,</span>
<span id="cb4-170"><a href="#cb4-170" aria-hidden="true" tabindex="-1"></a> zielwert<span class="op">=</span>loeser.ObjectiveValue() <span class="cf">if</span> status.brauchbar <span class="cf">else</span> <span class="va">None</span>,</span>
<span id="cb4-171"><a href="#cb4-171" aria-hidden="true" tabindex="-1"></a> schranke<span class="op">=</span>loeser.BestObjectiveBound() <span class="cf">if</span> status.brauchbar <span class="cf">else</span> <span class="va">None</span>,</span>
<span id="cb4-172"><a href="#cb4-172" aria-hidden="true" tabindex="-1"></a> laufzeit<span class="op">=</span>loeser.WallTime())</span>
<span id="cb4-173"><a href="#cb4-173" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-174"><a href="#cb4-174" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb4-175"><a href="#cb4-175" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; DYNAMISCHER VERTRETUNGSPLAN (CP-SAT OPTIMIERT)&quot;</span>)</span>
<span id="cb4-176"><a href="#cb4-176" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb4-177"><a href="#cb4-177" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-178"><a href="#cb4-178" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> status.modellfehler:</span>
<span id="cb4-179"><a href="#cb4-179" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Kein zulaessiger Plan moeglich (Status: </span><span class="sc">{</span>status<span class="sc">.</span>value<span class="sc">}</span><span class="ss">).&quot;</span>)</span>
<span id="cb4-180"><a href="#cb4-180" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Die harten Regeln widersprechen sich. Naechster Schritt: eine Regel&quot;</span>)</span>
<span id="cb4-181"><a href="#cb4-181" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;weich machen (Kapitel Praxisfallen) oder den Konflikt eingrenzen&quot;</span>)</span>
<span id="cb4-182"><a href="#cb4-182" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;(Anhang Fehlerdiagnose).&quot;</span>)</span>
<span id="cb4-183"><a href="#cb4-183" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="va">None</span></span>
<span id="cb4-184"><a href="#cb4-184" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> status.brauchbar:</span>
<span id="cb4-185"><a href="#cb4-185" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Solver lieferte kein Ergebnis (Status: </span><span class="sc">{</span>status<span class="sc">.</span>value<span class="sc">}</span><span class="ss">).&quot;</span>)</span>
<span id="cb4-186"><a href="#cb4-186" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Zeitlimit erhoehen oder das Modell vereinfachen.&quot;</span>)</span>
<span id="cb4-187"><a href="#cb4-187" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="va">None</span></span>
<span id="cb4-188"><a href="#cb4-188" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-189"><a href="#cb4-189" aria-hidden="true" tabindex="-1"></a> guete <span class="op">=</span> (<span class="st">&quot;beweisbar optimal&quot;</span> <span class="cf">if</span> status <span class="kw">is</span> SolverStatus.OPTIMAL</span>
<span id="cb4-190"><a href="#cb4-190" aria-hidden="true" tabindex="-1"></a> <span class="cf">else</span> <span class="st">&quot;zulaessig, aber nicht bewiesen - Gap beachten&quot;</span>)</span>
<span id="cb4-191"><a href="#cb4-191" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Solver-Status: </span><span class="sc">{</span>loeser<span class="sc">.</span>StatusName(rohstatus)<span class="sc">}</span><span class="ss"> -&gt; </span><span class="sc">{</span>status<span class="sc">.</span>value<span class="sc">}</span><span class="ss"> (</span><span class="sc">{</span>guete<span class="sc">}</span><span class="ss">)&quot;</span>)</span>
<span id="cb4-192"><a href="#cb4-192" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>loesung<span class="sc">.</span>als_bericht()<span class="sc">}</span><span class="ch">\n</span><span class="ss">&quot;</span>)</span>
<span id="cb4-193"><a href="#cb4-193" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-194"><a href="#cb4-194" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Plan ausgeben ----------------------------------------------------</span></span>
<span id="cb4-195"><a href="#cb4-195" aria-hidden="true" tabindex="-1"></a> zuweisung <span class="op">=</span> {s: <span class="bu">next</span>(p <span class="cf">for</span> p <span class="kw">in</span> PERSONAL <span class="cf">if</span> loeser.Value(x[p, s]) <span class="op">==</span> <span class="dv">1</span>)</span>
<span id="cb4-196"><a href="#cb4-196" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> s <span class="kw">in</span> <span class="bu">range</span>(<span class="bu">len</span>(SLOTS))}</span>
<span id="cb4-197"><a href="#cb4-197" aria-hidden="true" tabindex="-1"></a> plan <span class="op">=</span> [{</span>
<span id="cb4-198"><a href="#cb4-198" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Stunde&quot;</span>: <span class="ss">f&quot;Std. </span><span class="sc">{</span>SLOTS[s]<span class="sc">}</span><span class="ss">&quot;</span>,</span>
<span id="cb4-199"><a href="#cb4-199" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Klasse&quot;</span>: KLASSEN[s],</span>
<span id="cb4-200"><a href="#cb4-200" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Fach&quot;</span>: FAECHER[s],</span>
<span id="cb4-201"><a href="#cb4-201" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Vertretung&quot;</span>: zuweisung[s],</span>
<span id="cb4-202"><a href="#cb4-202" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Vorbelastung&quot;</span>: <span class="ss">f&quot;</span><span class="sc">{</span>VORBELASTUNG[zuweisung[s]]<span class="sc">}</span><span class="ss"> Std.&quot;</span>,</span>
<span id="cb4-203"><a href="#cb4-203" aria-hidden="true" tabindex="-1"></a> } <span class="cf">for</span> s <span class="kw">in</span> <span class="bu">range</span>(<span class="bu">len</span>(SLOTS))]</span>
<span id="cb4-204"><a href="#cb4-204" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(pd.DataFrame(plan).to_string(index<span class="op">=</span><span class="va">False</span>))</span>
<span id="cb4-205"><a href="#cb4-205" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-206"><a href="#cb4-206" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Abnahmepruefung gegen die Anforderung ----------------------------</span></span>
<span id="cb4-207"><a href="#cb4-207" aria-hidden="true" tabindex="-1"></a> beanstandungen <span class="op">=</span> pruefe_plan(zuweisung)</span>
<span id="cb4-208"><a href="#cb4-208" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> beanstandungen:</span>
<span id="cb4-209"><a href="#cb4-209" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">ABNAHMEPRUEFUNG FEHLGESCHLAGEN:&quot;</span>)</span>
<span id="cb4-210"><a href="#cb4-210" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> beanstandung <span class="kw">in</span> beanstandungen:</span>
<span id="cb4-211"><a href="#cb4-211" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; - </span><span class="sc">{</span>beanstandung<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb4-212"><a href="#cb4-212" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="va">None</span></span>
<span id="cb4-213"><a href="#cb4-213" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">Abnahmepruefung: bestanden (Besetzung, Qualifikation, Hoechstzahl).&quot;</span>)</span>
<span id="cb4-214"><a href="#cb4-214" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-215"><a href="#cb4-215" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Erklaerbarkeit: Strafkosten aufschluesseln -----------------------</span></span>
<span id="cb4-216"><a href="#cb4-216" aria-hidden="true" tabindex="-1"></a> kosten_last <span class="op">=</span> <span class="bu">sum</span>(VORBELASTUNG[p] <span class="op">*</span> STRAFE_VORBELASTUNG</span>
<span id="cb4-217"><a href="#cb4-217" aria-hidden="true" tabindex="-1"></a> <span class="op">*</span> <span class="bu">sum</span>(loeser.Value(x[p, s]) <span class="cf">for</span> s <span class="kw">in</span> <span class="bu">range</span>(<span class="bu">len</span>(SLOTS)))</span>
<span id="cb4-218"><a href="#cb4-218" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> p <span class="kw">in</span> PERSONAL)</span>
<span id="cb4-219"><a href="#cb4-219" aria-hidden="true" tabindex="-1"></a> kosten_loecher <span class="op">=</span> <span class="bu">sum</span>(loeser.Value(v) <span class="cf">for</span> v <span class="kw">in</span> loch_variablen) <span class="op">*</span> STRAFE_LOCH</span>
<span id="cb4-220"><a href="#cb4-220" aria-hidden="true" tabindex="-1"></a> kosten_fairness <span class="op">=</span> loeser.Value(spannweite) <span class="op">*</span> STRAFE_UNFAIRNESS</span>
<span id="cb4-221"><a href="#cb4-221" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-222"><a href="#cb4-222" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">--- Woraus bestehen die Strafkosten? ---&quot;</span>)</span>
<span id="cb4-223"><a href="#cb4-223" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Vorbelastung geschont: </span><span class="sc">{</span>kosten_last<span class="sc">:5.0f}</span><span class="ss"> Punkte&quot;</span>)</span>
<span id="cb4-224"><a href="#cb4-224" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Freistundenloecher: </span><span class="sc">{</span>kosten_loecher<span class="sc">:5.0f}</span><span class="ss"> Punkte &quot;</span></span>
<span id="cb4-225"><a href="#cb4-225" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;(</span><span class="sc">{</span><span class="bu">sum</span>(loeser.Value(v) <span class="cf">for</span> v <span class="kw">in</span> loch_variablen)<span class="sc">}</span><span class="ss"> Loch/Loecher)&quot;</span>)</span>
<span id="cb4-226"><a href="#cb4-226" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Fairness (Spannweite </span><span class="sc">{</span>loeser<span class="sc">.</span>Value(spannweite)<span class="sc">}</span><span class="ss">): </span><span class="sc">{</span>kosten_fairness<span class="sc">:5.0f}</span><span class="ss"> Punkte&quot;</span>)</span>
<span id="cb4-227"><a href="#cb4-227" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; </span><span class="sc">{</span><span class="st">&#39;Summe&#39;</span><span class="sc">:&lt;23}</span><span class="ss"> </span><span class="sc">{</span>loeser<span class="sc">.</span>ObjectiveValue()<span class="sc">:5.0f}</span><span class="ss"> Punkte&quot;</span>)</span>
<span id="cb4-228"><a href="#cb4-228" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-229"><a href="#cb4-229" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">--- Auslastung nach Optimierung ---&quot;</span>)</span>
<span id="cb4-230"><a href="#cb4-230" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> p <span class="kw">in</span> PERSONAL:</span>
<span id="cb4-231"><a href="#cb4-231" aria-hidden="true" tabindex="-1"></a> heute <span class="op">=</span> <span class="bu">sum</span>(loeser.Value(x[p, s]) <span class="cf">for</span> s <span class="kw">in</span> <span class="bu">range</span>(<span class="bu">len</span>(SLOTS)))</span>
<span id="cb4-232"><a href="#cb4-232" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; * </span><span class="sc">{</span>p<span class="sc">:&lt;15}</span><span class="ss">: </span><span class="sc">{</span>VORBELASTUNG[p]<span class="sc">}</span><span class="ss"> vorher + </span><span class="sc">{</span>heute<span class="sc">}</span><span class="ss"> Vertretung &quot;</span></span>
<span id="cb4-233"><a href="#cb4-233" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;= </span><span class="sc">{</span>VORBELASTUNG[p] <span class="op">+</span> heute<span class="sc">}</span><span class="ss"> Stunden&quot;</span>)</span>
<span id="cb4-234"><a href="#cb4-234" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-235"><a href="#cb4-235" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="ch">\n</span><span class="ss">Suchstatistik: </span><span class="sc">{</span>loeser<span class="sc">.</span>NumBranches()<span class="sc">}</span><span class="ss"> Verzweigungen, &quot;</span></span>
<span id="cb4-236"><a href="#cb4-236" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>loeser<span class="sc">.</span>NumConflicts()<span class="sc">}</span><span class="ss"> Konflikte, </span><span class="sc">{</span>loeser<span class="sc">.</span>WallTime()<span class="sc">:.3f}</span><span class="ss"> s&quot;</span>)</span>
<span id="cb4-237"><a href="#cb4-237" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb4-238"><a href="#cb4-238" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> plan</span>
<span id="cb4-239"><a href="#cb4-239" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-240"><a href="#cb4-240" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-241"><a href="#cb4-241" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> <span class="va">__name__</span> <span class="op">==</span> <span class="st">&quot;__main__&quot;</span>:</span>
<span id="cb4-242"><a href="#cb4-242" aria-hidden="true" tabindex="-1"></a> plane_vertretung()</span></code></pre></div>
<p><strong>Erwartete Ausgabe</strong> (Laufzeit und Suchstatistik sind hardwareabhängig):</p>
<pre><code>==============================================================================
DYNAMISCHER VERTRETUNGSPLAN (CP-SAT OPTIMIERT)
==============================================================================
Solver-Status: OPTIMAL -&gt; optimal (beweisbar optimal)
Status: optimal | Zielwert: 80.00 | Gap: 0.00% | Zeit: 0.01s
Stunde Klasse Fach Vertretung Vorbelastung
Std. 1 Klasse 8a Mathematik Herr_Schmidt 0 Std.
Std. 2 Klasse 10b Physik Frau_Mueller 1 Std.
Std. 3 Klasse 7a Mathematik Herr_Bauer 0 Std.
Std. 4 Klasse 9c Informatik Herr_Schmidt 0 Std.
Abnahmepruefung: bestanden (Besetzung, Qualifikation, Hoechstzahl).
--- Woraus bestehen die Strafkosten? ---
Vorbelastung geschont: 50 Punkte
Freistundenloecher: 0 Punkte (0 Loch/Loecher)
Fairness (Spannweite 1): 30 Punkte
Summe 80 Punkte
--- Auslastung nach Optimierung ---
* Frau_Mueller : 1 vorher + 1 Vertretung = 2 Stunden
* Herr_Schmidt : 0 vorher + 2 Vertretung = 2 Stunden
* Frau_Albrecht : 2 vorher + 0 Vertretung = 2 Stunden
* Herr_Bauer : 0 vorher + 1 Vertretung = 1 Stunden
* Frau_Koch : 1 vorher + 0 Vertretung = 1 Stunden
Suchstatistik: 113 Verzweigungen, 3 Konflikte, 0.006 s
==============================================================================</code></pre>
<blockquote>
<p><strong>💻 Code-Durchgang: die drei wichtigsten Stellen</strong></p>
<p><strong>1. Reifizierung (W2).</strong> Die zwei Zeilen</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a>modell.AddBoolAnd([x[p,s], x[p,s<span class="op">+</span><span class="dv">1</span>].Not(), x[p,s<span class="op">+</span><span class="dv">2</span>]]).OnlyEnforceIf(loch)</span>
<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a>modell.AddBoolOr([x[p,s].Not(), x[p,s<span class="op">+</span><span class="dv">1</span>], x[p,s<span class="op">+</span><span class="dv">2</span>].Not()]).OnlyEnforceIf(loch.Not())</span></code></pre></div>
<p>koppeln die Hilfsvariable <code>loch</code> <strong>in beide Richtungen</strong> an das Muster „arbeitetfreiarbeitet“. Die erste Zeile erzwingt: <em>wenn</em> <code>loch = 1</code>, <em>dann</em> liegt das Muster vor. Die zweite: <em>wenn</em> <code>loch = 0</code>, <em>dann</em> liegt es nicht vor. In diesem Modell genügte streng genommen die erste Zeile, weil <code>loch</code> nur mit positiven Strafkosten vorkommt und der Minimierer es daher von selbst auf 0 drückt — die zweite Zeile macht das Modell aber robust gegen spätere Änderungen und ist selbstdokumentierend.</p>
<p><strong>2. Fairness über Spannweite (W3).</strong> <code>AddMaxEquality</code> und <code>AddMinEquality</code> sind CP-SAT-Konstrukte, die in MILP mehrere Big-M-Ungleichungen bräuchten. Die Spannweite <span class="math inline">\max - \min</span> zu minimieren ist ein Standardrezept für „gleichmäßig verteilen“.</p>
<p><strong>3. Kostenzerlegung.</strong> Der Block „Woraus bestehen die Strafkosten?“ ist kein Beiwerk. In der Praxis ist die erste Frage jedes Anwenders: <em>„Warum bekomme <strong>ich</strong> die Stunde?“</em> Wer darauf keine Antwort hat, dessen System wird nicht benutzt (<a href="praxisfallen.html#kap-praxisfallen">Kapitel 22</a>, <em>Explainable OR</em>).</p>
<p><strong>4. Statusauswertung ohne CP-SAT-Vokabular.</strong> Nach <code>Solve()</code> steht nur eine einzige Zeile mit einer CP-SAT-Konstante darin:</p>
<div class="sourceCode" id="cb7"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a>status <span class="op">=</span> status_von_cpsat(rohstatus)</span></code></pre></div>
<p>Danach heißen die Fälle <code>status.modellfehler</code> (die Regeln widersprechen sich — das Modell muss geändert werden) und <code>status.brauchbar</code> (es liegt etwas Ausführbares vor). Das ist <strong>dieselbe</strong> Fallunterscheidung wie bei HiGHS in <a href="milp.html#kap-milp">Kapitel 6</a> und bei GLOP in <a href="lp.html#kap-lp">Kapitel 5</a>; nur die eine Übersetzerzeile weiß, welcher Solver gerechnet hat. Wer den Auswertungscode direkt gegen <code>cp_model.INFEASIBLE</code> schreibt, kann ihn bei einem Solverwechsel wegwerfen — <a href="praxisfallen.html#sec:praxisfallen-solverwechsel">Abschnitt 22.7</a> führt genau diesen Wechsel vor.</p>
</blockquote>
<blockquote>
<p><strong>⚠️ Typische Fehler</strong></p>
<ul>
<li><strong>Zu viele harte Regeln.</strong> Jede zusätzliche harte Bedingung erhöht das Risiko von <code>INFEASIBLE</code>. Faustregel: Hart ist nur, was rechtlich oder physikalisch unmöglich ist. Alles andere wird weich mit hoher Strafe — dann liefert der Solver den „am wenigsten schlechten“ Plan statt einer Fehlermeldung.</li>
<li><strong>Strafgewichte willkürlich wählen.</strong> Die Gewichte (50, 80, 30) definieren implizit eine Werteordnung: „Ein Freistundenloch ist so schlimm wie 1,6 Stunden Vorbelastung.“ Besprechen Sie diese Zahlen mit den Betroffenen — sie sind eine fachliche, keine technische Entscheidung.</li>
<li><strong>Gleitkommazahlen in CP-SAT.</strong> CP-SAT rechnet <strong>ausschließlich ganzzahlig</strong>. Wer Euro und Cent braucht, rechnet in Cent.</li>
</ul>
</blockquote>
<hr />
<h2 id="sec:cpsat-intervallvariablen-job-shop-scheduling">7.6 Intervallvariablen: Job-Shop-Scheduling</h2>
<p>Die Königsdisziplin von CP-SAT sind Zeitplanungsprobleme. Intervallvariablen sind dabei das zentrale Werkzeug.</p>
<p><strong>Problem.</strong> Drei Aufträge, je drei Arbeitsgänge auf drei Maschinen. Jeder Auftrag muss seine Gänge in fester Reihenfolge durchlaufen; jede Maschine kann nur einen Gang gleichzeitig bearbeiten. Gesucht: der Plan mit der kürzesten Gesamtdauer (<em>Makespan</em>).</p>
<div class="sourceCode" id="cb8"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="co">#!/usr/bin/env python3</span></span>
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-3"><a href="#cb8-3" aria-hidden="true" tabindex="-1"></a><span class="co"># JobShop_Intervalle.py</span></span>
<span id="cb8-4"><a href="#cb8-4" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb8-5"><a href="#cb8-5" aria-hidden="true" tabindex="-1"></a><span class="co">Kapitel CP-SAT: Job-Shop-Scheduling mit Intervallvariablen.</span></span>
<span id="cb8-6"><a href="#cb8-6" aria-hidden="true" tabindex="-1"></a><span class="co">Zeigt NewIntervalVar, AddNoOverlap und die Minimierung des Makespan.</span></span>
<span id="cb8-7"><a href="#cb8-7" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb8-8"><a href="#cb8-8" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-9"><a href="#cb8-9" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> collections</span>
<span id="cb8-10"><a href="#cb8-10" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-11"><a href="#cb8-11" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> ortools.sat.python <span class="im">import</span> cp_model</span>
<span id="cb8-12"><a href="#cb8-12" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-13"><a href="#cb8-13" aria-hidden="true" tabindex="-1"></a><span class="co"># (Maschine, Dauer) je Arbeitsgang, in der Reihenfolge des Auftrags</span></span>
<span id="cb8-14"><a href="#cb8-14" aria-hidden="true" tabindex="-1"></a>AUFTRAEGE <span class="op">=</span> [</span>
<span id="cb8-15"><a href="#cb8-15" aria-hidden="true" tabindex="-1"></a> [(<span class="dv">0</span>, <span class="dv">3</span>), (<span class="dv">1</span>, <span class="dv">2</span>), (<span class="dv">2</span>, <span class="dv">2</span>)], <span class="co"># Auftrag 0</span></span>
<span id="cb8-16"><a href="#cb8-16" aria-hidden="true" tabindex="-1"></a> [(<span class="dv">0</span>, <span class="dv">2</span>), (<span class="dv">2</span>, <span class="dv">1</span>), (<span class="dv">1</span>, <span class="dv">4</span>)], <span class="co"># Auftrag 1</span></span>
<span id="cb8-17"><a href="#cb8-17" aria-hidden="true" tabindex="-1"></a> [(<span class="dv">1</span>, <span class="dv">4</span>), (<span class="dv">2</span>, <span class="dv">3</span>)], <span class="co"># Auftrag 2</span></span>
<span id="cb8-18"><a href="#cb8-18" aria-hidden="true" tabindex="-1"></a>]</span>
<span id="cb8-19"><a href="#cb8-19" aria-hidden="true" tabindex="-1"></a>MASCHINENNAMEN <span class="op">=</span> [<span class="st">&quot;Fraese&quot;</span>, <span class="st">&quot;Dreherei&quot;</span>, <span class="st">&quot;Lackiererei&quot;</span>]</span>
<span id="cb8-20"><a href="#cb8-20" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-21"><a href="#cb8-21" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-22"><a href="#cb8-22" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> loese_jobshop():</span>
<span id="cb8-23"><a href="#cb8-23" aria-hidden="true" tabindex="-1"></a> anzahl_maschinen <span class="op">=</span> <span class="dv">1</span> <span class="op">+</span> <span class="bu">max</span>(m <span class="cf">for</span> auftrag <span class="kw">in</span> AUFTRAEGE <span class="cf">for</span> m, _ <span class="kw">in</span> auftrag)</span>
<span id="cb8-24"><a href="#cb8-24" aria-hidden="true" tabindex="-1"></a> horizont <span class="op">=</span> <span class="bu">sum</span>(dauer <span class="cf">for</span> auftrag <span class="kw">in</span> AUFTRAEGE <span class="cf">for</span> _, dauer <span class="kw">in</span> auftrag)</span>
<span id="cb8-25"><a href="#cb8-25" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-26"><a href="#cb8-26" aria-hidden="true" tabindex="-1"></a> modell <span class="op">=</span> cp_model.CpModel()</span>
<span id="cb8-27"><a href="#cb8-27" aria-hidden="true" tabindex="-1"></a> Gang <span class="op">=</span> collections.namedtuple(<span class="st">&quot;Gang&quot;</span>, <span class="st">&quot;start ende intervall&quot;</span>)</span>
<span id="cb8-28"><a href="#cb8-28" aria-hidden="true" tabindex="-1"></a> plaene <span class="op">=</span> {}</span>
<span id="cb8-29"><a href="#cb8-29" aria-hidden="true" tabindex="-1"></a> maschinen_intervalle <span class="op">=</span> collections.defaultdict(<span class="bu">list</span>)</span>
<span id="cb8-30"><a href="#cb8-30" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-31"><a href="#cb8-31" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Intervallvariablen anlegen --------------------------------------</span></span>
<span id="cb8-32"><a href="#cb8-32" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> a, auftrag <span class="kw">in</span> <span class="bu">enumerate</span>(AUFTRAEGE):</span>
<span id="cb8-33"><a href="#cb8-33" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> g, (maschine, dauer) <span class="kw">in</span> <span class="bu">enumerate</span>(auftrag):</span>
<span id="cb8-34"><a href="#cb8-34" aria-hidden="true" tabindex="-1"></a> start <span class="op">=</span> modell.NewIntVar(<span class="dv">0</span>, horizont, <span class="ss">f&quot;start_</span><span class="sc">{</span>a<span class="sc">}</span><span class="ss">_</span><span class="sc">{</span>g<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb8-35"><a href="#cb8-35" aria-hidden="true" tabindex="-1"></a> ende <span class="op">=</span> modell.NewIntVar(<span class="dv">0</span>, horizont, <span class="ss">f&quot;ende_</span><span class="sc">{</span>a<span class="sc">}</span><span class="ss">_</span><span class="sc">{</span>g<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb8-36"><a href="#cb8-36" aria-hidden="true" tabindex="-1"></a> <span class="co"># Ein Intervall koppelt start + dauer == ende automatisch</span></span>
<span id="cb8-37"><a href="#cb8-37" aria-hidden="true" tabindex="-1"></a> intervall <span class="op">=</span> modell.NewIntervalVar(start, dauer, ende, <span class="ss">f&quot;intervall_</span><span class="sc">{</span>a<span class="sc">}</span><span class="ss">_</span><span class="sc">{</span>g<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb8-38"><a href="#cb8-38" aria-hidden="true" tabindex="-1"></a> plaene[a, g] <span class="op">=</span> Gang(start, ende, intervall)</span>
<span id="cb8-39"><a href="#cb8-39" aria-hidden="true" tabindex="-1"></a> maschinen_intervalle[maschine].append(intervall)</span>
<span id="cb8-40"><a href="#cb8-40" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-41"><a href="#cb8-41" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- H1: eine Maschine bearbeitet nur einen Gang gleichzeitig --------</span></span>
<span id="cb8-42"><a href="#cb8-42" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> maschine <span class="kw">in</span> <span class="bu">range</span>(anzahl_maschinen):</span>
<span id="cb8-43"><a href="#cb8-43" aria-hidden="true" tabindex="-1"></a> modell.AddNoOverlap(maschinen_intervalle[maschine])</span>
<span id="cb8-44"><a href="#cb8-44" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-45"><a href="#cb8-45" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- H2: Reihenfolge innerhalb eines Auftrags einhalten -------------</span></span>
<span id="cb8-46"><a href="#cb8-46" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> a, auftrag <span class="kw">in</span> <span class="bu">enumerate</span>(AUFTRAEGE):</span>
<span id="cb8-47"><a href="#cb8-47" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> g <span class="kw">in</span> <span class="bu">range</span>(<span class="bu">len</span>(auftrag) <span class="op">-</span> <span class="dv">1</span>):</span>
<span id="cb8-48"><a href="#cb8-48" aria-hidden="true" tabindex="-1"></a> modell.Add(plaene[a, g <span class="op">+</span> <span class="dv">1</span>].start <span class="op">&gt;=</span> plaene[a, g].ende)</span>
<span id="cb8-49"><a href="#cb8-49" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-50"><a href="#cb8-50" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Ziel: Makespan minimieren ---------------------------------------</span></span>
<span id="cb8-51"><a href="#cb8-51" aria-hidden="true" tabindex="-1"></a> makespan <span class="op">=</span> modell.NewIntVar(<span class="dv">0</span>, horizont, <span class="st">&quot;makespan&quot;</span>)</span>
<span id="cb8-52"><a href="#cb8-52" aria-hidden="true" tabindex="-1"></a> modell.AddMaxEquality(makespan, [plaene[a, <span class="bu">len</span>(auftrag) <span class="op">-</span> <span class="dv">1</span>].ende</span>
<span id="cb8-53"><a href="#cb8-53" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> a, auftrag <span class="kw">in</span> <span class="bu">enumerate</span>(AUFTRAEGE)])</span>
<span id="cb8-54"><a href="#cb8-54" aria-hidden="true" tabindex="-1"></a> modell.Minimize(makespan)</span>
<span id="cb8-55"><a href="#cb8-55" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-56"><a href="#cb8-56" aria-hidden="true" tabindex="-1"></a> loeser <span class="op">=</span> cp_model.CpSolver()</span>
<span id="cb8-57"><a href="#cb8-57" aria-hidden="true" tabindex="-1"></a> loeser.parameters.max_time_in_seconds <span class="op">=</span> <span class="fl">10.0</span></span>
<span id="cb8-58"><a href="#cb8-58" aria-hidden="true" tabindex="-1"></a> <span class="co"># Es gibt mehrere Plaene mit demselben kuerzesten Makespan. Welchen CP-SAT</span></span>
<span id="cb8-59"><a href="#cb8-59" aria-hidden="true" tabindex="-1"></a> <span class="co"># findet, haengt sonst davon ab, welcher seiner parallelen Suchstraenge</span></span>
<span id="cb8-60"><a href="#cb8-60" aria-hidden="true" tabindex="-1"></a> <span class="co"># zuerst fertig wird - dasselbe Programm liefert dann von Lauf zu Lauf</span></span>
<span id="cb8-61"><a href="#cb8-61" aria-hidden="true" tabindex="-1"></a> <span class="co"># verschiedene (gleich gute) Plaene. Fuer ein reproduzierbares Buchbeispiel</span></span>
<span id="cb8-62"><a href="#cb8-62" aria-hidden="true" tabindex="-1"></a> <span class="co"># fixieren wir beides. Im Produktivbetrieb laesst man die Standardwerte</span></span>
<span id="cb8-63"><a href="#cb8-63" aria-hidden="true" tabindex="-1"></a> <span class="co"># stehen: Mehrere Arbeiter sind dort deutlich schneller.</span></span>
<span id="cb8-64"><a href="#cb8-64" aria-hidden="true" tabindex="-1"></a> loeser.parameters.num_workers <span class="op">=</span> <span class="dv">1</span></span>
<span id="cb8-65"><a href="#cb8-65" aria-hidden="true" tabindex="-1"></a> loeser.parameters.random_seed <span class="op">=</span> <span class="dv">1</span></span>
<span id="cb8-66"><a href="#cb8-66" aria-hidden="true" tabindex="-1"></a> status <span class="op">=</span> loeser.Solve(modell)</span>
<span id="cb8-67"><a href="#cb8-67" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-68"><a href="#cb8-68" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> status <span class="kw">not</span> <span class="kw">in</span> (cp_model.OPTIMAL, cp_model.FEASIBLE):</span>
<span id="cb8-69"><a href="#cb8-69" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Kein Plan gefunden: </span><span class="sc">{</span>loeser<span class="sc">.</span>StatusName(status)<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb8-70"><a href="#cb8-70" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span></span>
<span id="cb8-71"><a href="#cb8-71" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-72"><a href="#cb8-72" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">74</span>)</span>
<span id="cb8-73"><a href="#cb8-73" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; JOB-SHOP-SCHEDULING MIT INTERVALLVARIABLEN&quot;</span>)</span>
<span id="cb8-74"><a href="#cb8-74" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">74</span>)</span>
<span id="cb8-75"><a href="#cb8-75" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Status: </span><span class="sc">{</span>loeser<span class="sc">.</span>StatusName(status)<span class="sc">}</span><span class="ss"> | &quot;</span></span>
<span id="cb8-76"><a href="#cb8-76" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Kuerzeste Gesamtdauer (Makespan): </span><span class="sc">{</span>loeser<span class="sc">.</span>Value(makespan)<span class="sc">}</span><span class="ss"> Zeiteinheiten</span><span class="ch">\n</span><span class="ss">&quot;</span>)</span>
<span id="cb8-77"><a href="#cb8-77" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-78"><a href="#cb8-78" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Gantt-Diagramm als Text ------------------------------------------</span></span>
<span id="cb8-79"><a href="#cb8-79" aria-hidden="true" tabindex="-1"></a> dauer_gesamt <span class="op">=</span> loeser.Value(makespan)</span>
<span id="cb8-80"><a href="#cb8-80" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;Maschine&#39;</span><span class="sc">:&lt;14}</span><span class="ss">|&quot;</span> <span class="op">+</span> <span class="st">&quot;&quot;</span>.join(<span class="ss">f&quot;</span><span class="sc">{</span>t<span class="sc">:&lt;3}</span><span class="ss">&quot;</span> <span class="cf">for</span> t <span class="kw">in</span> <span class="bu">range</span>(dauer_gesamt)))</span>
<span id="cb8-81"><a href="#cb8-81" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> (<span class="dv">15</span> <span class="op">+</span> <span class="dv">3</span> <span class="op">*</span> dauer_gesamt))</span>
<span id="cb8-82"><a href="#cb8-82" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> maschine <span class="kw">in</span> <span class="bu">range</span>(anzahl_maschinen):</span>
<span id="cb8-83"><a href="#cb8-83" aria-hidden="true" tabindex="-1"></a> zeile <span class="op">=</span> [<span class="st">&quot; . &quot;</span>] <span class="op">*</span> dauer_gesamt</span>
<span id="cb8-84"><a href="#cb8-84" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> a, auftrag <span class="kw">in</span> <span class="bu">enumerate</span>(AUFTRAEGE):</span>
<span id="cb8-85"><a href="#cb8-85" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> g, (m, dauer) <span class="kw">in</span> <span class="bu">enumerate</span>(auftrag):</span>
<span id="cb8-86"><a href="#cb8-86" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> m <span class="op">==</span> maschine:</span>
<span id="cb8-87"><a href="#cb8-87" aria-hidden="true" tabindex="-1"></a> beginn <span class="op">=</span> loeser.Value(plaene[a, g].start)</span>
<span id="cb8-88"><a href="#cb8-88" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> t <span class="kw">in</span> <span class="bu">range</span>(beginn, beginn <span class="op">+</span> dauer):</span>
<span id="cb8-89"><a href="#cb8-89" aria-hidden="true" tabindex="-1"></a> zeile[t] <span class="op">=</span> <span class="ss">f&quot; A</span><span class="sc">{</span>a<span class="sc">}</span><span class="ss">&quot;</span></span>
<span id="cb8-90"><a href="#cb8-90" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>MASCHINENNAMEN[maschine]<span class="sc">:&lt;14}</span><span class="ss">|&quot;</span> <span class="op">+</span> <span class="st">&quot;&quot;</span>.join(zeile))</span>
<span id="cb8-91"><a href="#cb8-91" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-92"><a href="#cb8-92" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">--- Detailplan ---&quot;</span>)</span>
<span id="cb8-93"><a href="#cb8-93" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> a, auftrag <span class="kw">in</span> <span class="bu">enumerate</span>(AUFTRAEGE):</span>
<span id="cb8-94"><a href="#cb8-94" aria-hidden="true" tabindex="-1"></a> teile <span class="op">=</span> []</span>
<span id="cb8-95"><a href="#cb8-95" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> g, (maschine, dauer) <span class="kw">in</span> <span class="bu">enumerate</span>(auftrag):</span>
<span id="cb8-96"><a href="#cb8-96" aria-hidden="true" tabindex="-1"></a> beginn <span class="op">=</span> loeser.Value(plaene[a, g].start)</span>
<span id="cb8-97"><a href="#cb8-97" aria-hidden="true" tabindex="-1"></a> teile.append(<span class="ss">f&quot;</span><span class="sc">{</span>MASCHINENNAMEN[maschine]<span class="sc">}</span><span class="ss"> </span><span class="sc">{</span>beginn<span class="sc">}</span><span class="ss">-</span><span class="sc">{</span>beginn <span class="op">+</span> dauer<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb8-98"><a href="#cb8-98" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Auftrag </span><span class="sc">{</span>a<span class="sc">}</span><span class="ss">: &quot;</span> <span class="op">+</span> <span class="st">&quot; -&gt; &quot;</span>.join(teile))</span>
<span id="cb8-99"><a href="#cb8-99" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-100"><a href="#cb8-100" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Untere Schranke zum Vergleich ------------------------------------</span></span>
<span id="cb8-101"><a href="#cb8-101" aria-hidden="true" tabindex="-1"></a> laengster_auftrag <span class="op">=</span> <span class="bu">max</span>(<span class="bu">sum</span>(d <span class="cf">for</span> _, d <span class="kw">in</span> a) <span class="cf">for</span> a <span class="kw">in</span> AUFTRAEGE)</span>
<span id="cb8-102"><a href="#cb8-102" aria-hidden="true" tabindex="-1"></a> hoechste_maschinenlast <span class="op">=</span> <span class="bu">max</span>(</span>
<span id="cb8-103"><a href="#cb8-103" aria-hidden="true" tabindex="-1"></a> <span class="bu">sum</span>(d <span class="cf">for</span> auftrag <span class="kw">in</span> AUFTRAEGE <span class="cf">for</span> m, d <span class="kw">in</span> auftrag <span class="cf">if</span> m <span class="op">==</span> maschine)</span>
<span id="cb8-104"><a href="#cb8-104" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> maschine <span class="kw">in</span> <span class="bu">range</span>(anzahl_maschinen))</span>
<span id="cb8-105"><a href="#cb8-105" aria-hidden="true" tabindex="-1"></a> schranke <span class="op">=</span> <span class="bu">max</span>(laengster_auftrag, hoechste_maschinenlast)</span>
<span id="cb8-106"><a href="#cb8-106" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="ch">\n</span><span class="ss">Untere Schranke (laengster Auftrag / hoechste Maschinenlast): </span><span class="sc">{</span>schranke<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb8-107"><a href="#cb8-107" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Erreichter Makespan: </span><span class="sc">{</span>loeser<span class="sc">.</span>Value(makespan)<span class="sc">}</span><span class="ss">&quot;</span></span>
<span id="cb8-108"><a href="#cb8-108" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39; -&gt; beweisbar bestmoeglich&#39;</span> <span class="cf">if</span> loeser<span class="sc">.</span>Value(makespan) <span class="op">==</span> schranke <span class="cf">else</span> <span class="st">&#39;&#39;</span><span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb8-109"><a href="#cb8-109" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">74</span>)</span>
<span id="cb8-110"><a href="#cb8-110" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-111"><a href="#cb8-111" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-112"><a href="#cb8-112" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> <span class="va">__name__</span> <span class="op">==</span> <span class="st">&quot;__main__&quot;</span>:</span>
<span id="cb8-113"><a href="#cb8-113" aria-hidden="true" tabindex="-1"></a> loese_jobshop()</span></code></pre></div>
<p><strong>Erwartete Ausgabe:</strong></p>
<pre><code>==========================================================================
JOB-SHOP-SCHEDULING MIT INTERVALLVARIABLEN
==========================================================================
Status: OPTIMAL | Kuerzeste Gesamtdauer (Makespan): 11 Zeiteinheiten
Maschine |0 1 2 3 4 5 6 7 8 9 10
------------------------------------------------
Fraese | A1 A1 A0 A0 A0 . . . . . .
Dreherei | A2 A2 A2 A2 . A0 A0 A1 A1 A1 A1
Lackiererei | . . A1 . A2 A2 A2 A0 A0 . .
--- Detailplan ---
Auftrag 0: Fraese 2-5 -&gt; Dreherei 5-7 -&gt; Lackiererei 7-9
Auftrag 1: Fraese 0-2 -&gt; Lackiererei 2-3 -&gt; Dreherei 7-11
Auftrag 2: Dreherei 0-4 -&gt; Lackiererei 4-7
Untere Schranke (laengster Auftrag / hoechste Maschinenlast): 10
Erreichter Makespan: 11
==========================================================================</code></pre>
<figure>
<img src="bilder_04/kap06_jobshop_gantt.svg" alt="Abb. 7.2: Job-Shop-Plan als Gantt-Diagramm" />
<figcaption aria-hidden="true">Abb. 7.2: Job-Shop-Plan als Gantt-Diagramm</figcaption>
</figure>
<div class="plotly-figur">
<div> <div id="kap06-jobshop-gantt" class="plotly-graph-div" style="height:320px; width:100%;"></div> <script type="text/javascript"> window.PLOTLYENV=window.PLOTLYENV || {}; if (document.getElementById("kap06-jobshop-gantt")) { Plotly.newPlot( "kap06-jobshop-gantt", [{"base":[2],"hovertemplate":"\u003cb\u003eAuftrag 0\u003c\u002fb\u003e\u003cbr\u003eFraese\u003cbr\u003evon 2 bis 5 (3 Zeiteinheiten)\u003cextra\u003e\u003c\u002fextra\u003e","legendgroup":"A0","marker":{"color":"#4338ca","line":{"color":"white","width":1.5}},"name":"Auftrag 0","orientation":"h","showlegend":true,"x":[3],"y":["Fraese"],"type":"bar"},{"base":[5],"hovertemplate":"\u003cb\u003eAuftrag 0\u003c\u002fb\u003e\u003cbr\u003eDreherei\u003cbr\u003evon 5 bis 7 (2 Zeiteinheiten)\u003cextra\u003e\u003c\u002fextra\u003e","legendgroup":"A0","marker":{"color":"#4338ca","line":{"color":"white","width":1.5}},"name":"Auftrag 0","orientation":"h","showlegend":false,"x":[2],"y":["Dreherei"],"type":"bar"},{"base":[7],"hovertemplate":"\u003cb\u003eAuftrag 0\u003c\u002fb\u003e\u003cbr\u003eLackiererei\u003cbr\u003evon 7 bis 9 (2 Zeiteinheiten)\u003cextra\u003e\u003c\u002fextra\u003e","legendgroup":"A0","marker":{"color":"#4338ca","line":{"color":"white","width":1.5}},"name":"Auftrag 0","orientation":"h","showlegend":false,"x":[2],"y":["Lackiererei"],"type":"bar"},{"base":[0],"hovertemplate":"\u003cb\u003eAuftrag 1\u003c\u002fb\u003e\u003cbr\u003eFraese\u003cbr\u003evon 0 bis 2 (2 Zeiteinheiten)\u003cextra\u003e\u003c\u002fextra\u003e","legendgroup":"A1","marker":{"color":"#0891b2","line":{"color":"white","width":1.5}},"name":"Auftrag 1","orientation":"h","showlegend":true,"x":[2],"y":["Fraese"],"type":"bar"},{"base":[2],"hovertemplate":"\u003cb\u003eAuftrag 1\u003c\u002fb\u003e\u003cbr\u003eLackiererei\u003cbr\u003evon 2 bis 3 (1 Zeiteinheiten)\u003cextra\u003e\u003c\u002fextra\u003e","legendgroup":"A1","marker":{"color":"#0891b2","line":{"color":"white","width":1.5}},"name":"Auftrag 1","orientation":"h","showlegend":false,"x":[1],"y":["Lackiererei"],"type":"bar"},{"base":[7],"hovertemplate":"\u003cb\u003eAuftrag 1\u003c\u002fb\u003e\u003cbr\u003eDreherei\u003cbr\u003evon 7 bis 11 (4 Zeiteinheiten)\u003cextra\u003e\u003c\u002fextra\u003e","legendgroup":"A1","marker":{"color":"#0891b2","line":{"color":"white","width":1.5}},"name":"Auftrag 1","orientation":"h","showlegend":false,"x":[4],"y":["Dreherei"],"type":"bar"},{"base":[0],"hovertemplate":"\u003cb\u003eAuftrag 2\u003c\u002fb\u003e\u003cbr\u003eDreherei\u003cbr\u003evon 0 bis 4 (4 Zeiteinheiten)\u003cextra\u003e\u003c\u002fextra\u003e","legendgroup":"A2","marker":{"color":"#b45309","line":{"color":"white","width":1.5}},"name":"Auftrag 2","orientation":"h","showlegend":true,"x":[4],"y":["Dreherei"],"type":"bar"},{"base":[4],"hovertemplate":"\u003cb\u003eAuftrag 2\u003c\u002fb\u003e\u003cbr\u003eLackiererei\u003cbr\u003evon 4 bis 7 (3 Zeiteinheiten)\u003cextra\u003e\u003c\u002fextra\u003e","legendgroup":"A2","marker":{"color":"#b45309","line":{"color":"white","width":1.5}},"name":"Auftrag 2","orientation":"h","showlegend":false,"x":[3],"y":["Lackiererei"],"type":"bar"}], {"template":{"data":{"histogram2dcontour":[{"type":"histogram2dcontour","colorbar":{"outlinewidth":0,"ticks":""},"colorscale":[[0.0,"#0d0887"],[0.1111111111111111,"#46039f"],[0.2222222222222222,"#7201a8"],[0.3333333333333333,"#9c179e"],[0.4444444444444444,"#bd3786"],[0.5555555555555556,"#d8576b"],[0.6666666666666666,"#ed7953"],[0.7777777777777778,"#fb9f3a"],[0.8888888888888888,"#fdca26"],[1.0,"#f0f921"]]}],"choropleth":[{"type":"choropleth","colorbar":{"outlinewidth":0,"ticks":""}}],"histogram2d":[{"type":"histogram2d","colorbar":{"outlinewidth":0,"ticks":""},"colorscale":[[0.0,"#0d0887"],[0.1111111111111111,"#46039f"],[0.2222222222222222,"#7201a8"],[0.3333333333333333,"#9c179e"],[0.4444444444444444,"#bd3786"],[0.5555555555555556,"#d8576b"],[0.6666666666666666,"#ed7953"],[0.7777777777777778,"#fb9f3a"],[0.8888888888888888,"#fdca26"],[1.0,"#f0f921"]]}],"heatmap":[{"type":"heatmap","colorbar":{"outlinewidth":0,"ticks":""},"colorscale":[[0.0,"#0d0887"],[0.1111111111111111,"#46039f"],[0.2222222222222222,"#7201a8"],[0.3333333333333333,"#9c179e"],[0.4444444444444444,"#bd3786"],[0.5555555555555556,"#d8576b"],[0.6666666666666666,"#ed7953"],[0.7777777777777778,"#fb9f3a"],[0.8888888888888888,"#fdca26"],[1.0,"#f0f921"]]}],"contourcarpet":[{"type":"contourcarpet","colorbar":{"outlinewidth":0,"ticks":""}}],"contour":[{"type":"contour","colorbar":{"outlinewidth":0,"ticks":""},"colorscale":[[0.0,"#0d0887"],[0.1111111111111111,"#46039f"],[0.2222222222222222,"#7201a8"],[0.3333333333333333,"#9c179e"],[0.4444444444444444,"#bd3786"],[0.5555555555555556,"#d8576b"],[0.6666666666666666,"#ed7953"],[0.7777777777777778,"#fb9f3a"],[0.8888888888888888,"#fdca26"],[1.0,"#f0f921"]]}],"surface":[{"type":"surface","colorbar":{"outlinewidth":0,"ticks":""},"colorscale":[[0.0,"#0d0887"],[0.1111111111111111,"#46039f"],[0.2222222222222222,"#7201a8"],[0.3333333333333333,"#9c179e"],[0.4444444444444444,"#bd3786"],[0.5555555555555556,"#d8576b"],[0.6666666666666666,"#ed7953"],[0.7777777777777778,"#fb9f3a"],[0.8888888888888888,"#fdca26"],[1.0,"#f0f921"]]}],"mesh3d":[{"type":"mesh3d","colorbar":{"outlinewidth":0,"ticks":""}}],"scatter":[{"fillpattern":{"fillmode":"overlay","size":10,"solidity":0.2},"type":"scatter"}],"parcoords":[{"type":"parcoords","line":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"scatterpolargl":[{"type":"scatterpolargl","marker":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"bar":[{"error_x":{"color":"#2a3f5f"},"error_y":{"color":"#2a3f5f"},"marker":{"line":{"color":"#E5ECF6","width":0.5},"pattern":{"fillmode":"overlay","size":10,"solidity":0.2}},"type":"bar"}],"scattergeo":[{"type":"scattergeo","marker":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"scatterpolar":[{"type":"scatterpolar","marker":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"histogram":[{"marker":{"pattern":{"fillmode":"overlay","size":10,"solidity":0.2}},"type":"histogram"}],"scattergl":[{"type":"scattergl","marker":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"scatter3d":[{"type":"scatter3d","line":{"colorbar":{"outlinewidth":0,"ticks":""}},"marker":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"scattermap":[{"type":"scattermap","marker":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"scattermapbox":[{"type":"scattermapbox","marker":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"scatterternary":[{"type":"scatterternary","marker":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"scattercarpet":[{"type":"scattercarpet","marker":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"carpet":[{"aaxis":{"endlinecolor":"#2a3f5f","gridcolor":"white","linecolor":"white","minorgridcolor":"white","startlinecolor":"#2a3f5f"},"baxis":{"endlinecolor":"#2a3f5f","gridcolor":"white","linecolor":"white","minorgridcolor":"white","startlinecolor":"#2a3f5f"},"type":"carpet"}],"table":[{"cells":{"fill":{"color":"#EBF0F8"},"line":{"color":"white"}},"header":{"fill":{"color":"#C8D4E3"},"line":{"color":"white"}},"type":"table"}],"barpolar":[{"marker":{"line":{"color":"#E5ECF6","width":0.5},"pattern":{"fillmode":"overlay","size":10,"solidity":0.2}},"type":"barpolar"}],"pie":[{"automargin":true,"type":"pie"}]},"layout":{"autotypenumbers":"strict","colorway":["#636efa","#EF553B","#00cc96","#ab63fa","#FFA15A","#19d3f3","#FF6692","#B6E880","#FF97FF","#FECB52"],"font":{"color":"#2a3f5f"},"hovermode":"closest","hoverlabel":{"align":"left"},"paper_bgcolor":"white","plot_bgcolor":"#E5ECF6","polar":{"bgcolor":"#E5ECF6","angularaxis":{"gridcolor":"white","linecolor":"white","ticks":""},"radialaxis":{"gridcolor":"white","linecolor":"white","ticks":""}},"ternary":{"bgcolor":"#E5ECF6","aaxis":{"gridcolor":"white","linecolor":"white","ticks":""},"baxis":{"gridcolor":"white","linecolor":"white","ticks":""},"caxis":{"gridcolor":"white","linecolor":"white","ticks":""}},"coloraxis":{"colorbar":{"outlinewidth":0,"ticks":""}},"colorscale":{"sequential":[[0.0,"#0d0887"],[0.1111111111111111,"#46039f"],[0.2222222222222222,"#7201a8"],[0.3333333333333333,"#9c179e"],[0.4444444444444444,"#bd3786"],[0.5555555555555556,"#d8576b"],[0.6666666666666666,"#ed7953"],[0.7777777777777778,"#fb9f3a"],[0.8888888888888888,"#fdca26"],[1.0,"#f0f921"]],"sequentialminus":[[0.0,"#0d0887"],[0.1111111111111111,"#46039f"],[0.2222222222222222,"#7201a8"],[0.3333333333333333,"#9c179e"],[0.4444444444444444,"#bd3786"],[0.5555555555555556,"#d8576b"],[0.6666666666666666,"#ed7953"],[0.7777777777777778,"#fb9f3a"],[0.8888888888888888,"#fdca26"],[1.0,"#f0f921"]],"diverging":[[0,"#8e0152"],[0.1,"#c51b7d"],[0.2,"#de77ae"],[0.3,"#f1b6da"],[0.4,"#fde0ef"],[0.5,"#f7f7f7"],[0.6,"#e6f5d0"],[0.7,"#b8e186"],[0.8,"#7fbc41"],[0.9,"#4d9221"],[1,"#276419"]]},"xaxis":{"gridcolor":"white","linecolor":"white","ticks":"","title":{"standoff":15},"zerolinecolor":"white","automargin":true,"zerolinewidth":2},"yaxis":{"gridcolor":"white","linecolor":"white","ticks":"","title":{"standoff":15},"zerolinecolor":"white","automargin":true,"zerolinewidth":2},"scene":{"xaxis":{"backgroundcolor":"#E5ECF6","gridcolor":"white","linecolor":"white","showbackground":true,"ticks":"","zerolinecolor":"white","gridwidth":2},"yaxis":{"backgroundcolor":"#E5ECF6","gridcolor":"white","linecolor":"white","showbackground":true,"ticks":"","zerolinecolor":"white","gridwidth":2},"zaxis":{"backgroundcolor":"#E5ECF6","gridcolor":"white","linecolor":"white","showbackground":true,"ticks":"","zerolinecolor":"white","gridwidth":2}},"shapedefaults":{"line":{"color":"#2a3f5f"}},"annotationdefaults":{"arrowcolor":"#2a3f5f","arrowhead":0,"arrowwidth":1},"geo":{"bgcolor":"white","landcolor":"#E5ECF6","subunitcolor":"white","showland":true,"showlakes":true,"lakecolor":"white"},"title":{"x":0.05},"mapbox":{"style":"light"}}},"xaxis":{"title":{"text":"Zeiteinheiten"},"dtick":1,"range":[0,11]},"yaxis":{"autorange":"reversed"},"margin":{"l":90,"r":20,"t":50,"b":45},"legend":{"orientation":"h","y":-0.25},"barmode":"overlay","title":{"text":"Job-Shop-Plan, Makespan = 11"},"height":320,"paper_bgcolor":"rgba(0,0,0,0)","plot_bgcolor":"rgba(0,0,0,0)"}, {"responsive": true} ) }; </script> </div>
</div>
<p><strong>Lesen Sie das Gantt-Diagramm.</strong> Auftrag 1 kommt zuerst an die Fräse (02), Auftrag 0 muss warten und ist dort erst von 2 bis 5 an der Reihe. Auffällig ist die Lücke in der Dreherei bei Zeitpunkt 4: Die Maschine steht still, obwohl Auftrag 0 schon Arbeit hätte — er ist aber bis 5 noch an der Fräse. Dieses <strong>Warten ist unvermeidlich</strong>; es entsteht aus dem Zusammenspiel von Reihenfolge- und Maschinenbedingungen, nicht aus einer schlechten Planung.</p>
<blockquote>
<p><strong>⚠️ Mehrere gleich gute Pläne — und was das für Ihre Tests bedeutet</strong></p>
<p>Der kürzeste Makespan beträgt 11, aber es gibt <strong>mehrere verschiedene Pläne</strong>, die ihn erreichen. Welchen CP-SAT findet, hängt normalerweise davon ab, welcher seiner parallelen Suchstränge zuerst fertig wird — dasselbe Programm liefert dann von Lauf zu Lauf einen anderen (gleich guten) Plan.</p>
<p>Deshalb setzt das Programm oben <code>num_workers = 1</code> und <code>random_seed = 1</code>: nur so ist die abgedruckte Ausgabe reproduzierbar. <strong>Im Produktivbetrieb lässt man beides weg</strong> — mehrere Arbeiter sind deutlich schneller.</p>
<p>Wie viel schneller, und warum der Seed allein <strong>nicht</strong> genügt, misst <a href="#sec:cpsat-parallele-suche">Abschnitt 7.7</a> an genau diesem Modell.</p>
<p>Die Lehre für Ihre eigenen Tests: Prüfen Sie bei mehrdeutigen Optima nie auf einen <strong>bestimmten Plan</strong>, sondern immer auf den <strong>Zielwert</strong> und auf die Einhaltung aller Regeln. Ein Test wie <code>assert plan == erwarteter_plan</code> besteht sonst mal und scheitert mal, ohne dass sich am Code etwas geändert hätte.</p>
</blockquote>
<p>Bemerkenswert ist die letzte Zeile: Die untere Schranke liegt bei 10, erreicht wurden 11. Die Schranke ist also <strong>nicht erreichbar</strong> — kein Plan kann 10 schaffen, obwohl weder ein einzelner Auftrag noch eine einzelne Maschine mehr als 10 Zeiteinheiten braucht. Der Grund sind die Wechselwirkungen. Das ist typisch für Scheduling: <strong>Einfache Schranken sind selten scharf.</strong> Trotzdem sind sie nützlich — sie sagten uns hier, dass wir höchstens 10 % vom theoretischen Ideal entfernt sein können, noch bevor der Solver fertig war.</p>
<blockquote>
<p><strong>💡 Warum Intervallvariablen so wertvoll sind</strong></p>
<p>Eine Intervallvariable bündelt drei Größen (Start, Dauer, Ende) und garantiert intern <code>start + dauer == ende</code>. Der eigentliche Gewinn ist aber <code>AddNoOverlap</code>: Diese eine Zeile ersetzt für <span class="math inline">k</span> Gänge auf einer Maschine <span class="math inline">\binom{k}{2}</span> Entweder-Oder-Konstruktionen mit je zwei Big-M-Ungleichungen und einer Binärvariablen. Bei 10 Gängen sind das 45 Disjunktionen — in MILP ein zäher Block, in CP-SAT eine Zeile mit einem spezialisierten Propagator.</p>
</blockquote>
<hr />
<h2 id="sec:cpsat-parallele-suche">7.7 Parallele Suche: was <code>num_workers</code> wirklich bewirkt</h2>
<p>An mehreren Stellen dieses Buchs steht <code>loeser.parameters.num_workers = 1</code>, mit dem Kommentar „für eine reproduzierbare Ausgabe“. Darin stecken zwei Behauptungen: dass parallele Suche <strong>schneller</strong> ist, und dass sie <strong>von Lauf zu Lauf andere, gleich gute</strong> Lösungen liefert. Beides lässt sich messen — und beim Messen kommt mehr heraus als erwartet.</p>
<p>Gerechnet wird auf demselben Modell wie <a href="#sec:cpsat-intervallvariablen-job-shop-scheduling">Abschnitt 7.6</a>, nur groß genug, dass die Suche wirklich arbeitet: 12 Aufträge auf 10 Maschinen, jeder Auftrag besucht jede Maschine. Entscheidend ist, dass <code>random_seed</code> in <strong>jedem</strong> Lauf denselben Wert hat.</p>
<div class="sourceCode" id="cb10"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a><span class="co">#!/usr/bin/env python3</span></span>
<span id="cb10-2"><a href="#cb10-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-3"><a href="#cb10-3" aria-hidden="true" tabindex="-1"></a><span class="co"># Parallele_Suche.py</span></span>
<span id="cb10-4"><a href="#cb10-4" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb10-5"><a href="#cb10-5" aria-hidden="true" tabindex="-1"></a><span class="co">Kapitel CP-SAT: Was mehrere Arbeiter wirklich bringen - und was sie kosten.</span></span>
<span id="cb10-6"><a href="#cb10-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-7"><a href="#cb10-7" aria-hidden="true" tabindex="-1"></a><span class="co">Das Buch setzt an mehreren Stellen &#39;num_workers = 1&#39;, damit die abgedruckte</span></span>
<span id="cb10-8"><a href="#cb10-8" aria-hidden="true" tabindex="-1"></a><span class="co">Ausgabe reproduzierbar ist. Behauptet wird dabei zweierlei: dass parallele</span></span>
<span id="cb10-9"><a href="#cb10-9" aria-hidden="true" tabindex="-1"></a><span class="co">Suche schneller ist, und dass sie von Lauf zu Lauf verschiedene, gleich gute</span></span>
<span id="cb10-10"><a href="#cb10-10" aria-hidden="true" tabindex="-1"></a><span class="co">Loesungen findet. Beides wird hier gemessen statt geglaubt.</span></span>
<span id="cb10-11"><a href="#cb10-11" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-12"><a href="#cb10-12" aria-hidden="true" tabindex="-1"></a><span class="co">Gerechnet wird auf einem Job-Shop wie in JobShop_Intervalle.py, nur gross</span></span>
<span id="cb10-13"><a href="#cb10-13" aria-hidden="true" tabindex="-1"></a><span class="co">genug, dass die Suche wirklich arbeitet: 12 Auftraege auf 10 Maschinen,</span></span>
<span id="cb10-14"><a href="#cb10-14" aria-hidden="true" tabindex="-1"></a><span class="co">jeder Auftrag besucht jede Maschine.</span></span>
<span id="cb10-15"><a href="#cb10-15" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-16"><a href="#cb10-16" aria-hidden="true" tabindex="-1"></a><span class="co">Zwei Messungen:</span></span>
<span id="cb10-17"><a href="#cb10-17" aria-hidden="true" tabindex="-1"></a><span class="co"> 1. Dieselbe Aufgabe mit 1, 2, 4 und 8 Arbeitern - Laufzeit und Ergebnis.</span></span>
<span id="cb10-18"><a href="#cb10-18" aria-hidden="true" tabindex="-1"></a><span class="co"> 2. Dieselbe Konfiguration mehrfach, bei FESTEM random_seed - wie oft kommt</span></span>
<span id="cb10-19"><a href="#cb10-19" aria-hidden="true" tabindex="-1"></a><span class="co"> derselbe Plan heraus?</span></span>
<span id="cb10-20"><a href="#cb10-20" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-21"><a href="#cb10-21" aria-hidden="true" tabindex="-1"></a><span class="co">Die zweite ist die wichtigere: Sie entscheidet, wie man Tests schreibt.</span></span>
<span id="cb10-22"><a href="#cb10-22" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-23"><a href="#cb10-23" aria-hidden="true" tabindex="-1"></a><span class="co">Achtung, Laufzeiten sind hardwareabhaengig. Die ZAHL der verschiedenen</span></span>
<span id="cb10-24"><a href="#cb10-24" aria-hidden="true" tabindex="-1"></a><span class="co">Plaene ist es auch - und genau das ist die Aussage.</span></span>
<span id="cb10-25"><a href="#cb10-25" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-26"><a href="#cb10-26" aria-hidden="true" tabindex="-1"></a><span class="co">Benoetigt: numpy, ortools</span></span>
<span id="cb10-27"><a href="#cb10-27" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb10-28"><a href="#cb10-28" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-29"><a href="#cb10-29" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> __future__ <span class="im">import</span> annotations</span>
<span id="cb10-30"><a href="#cb10-30" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-31"><a href="#cb10-31" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> collections</span>
<span id="cb10-32"><a href="#cb10-32" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> time</span>
<span id="cb10-33"><a href="#cb10-33" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-34"><a href="#cb10-34" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb10-35"><a href="#cb10-35" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> ortools.sat.python <span class="im">import</span> cp_model</span>
<span id="cb10-36"><a href="#cb10-36" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-37"><a href="#cb10-37" aria-hidden="true" tabindex="-1"></a>AUFTRAEGE <span class="op">=</span> <span class="dv">12</span></span>
<span id="cb10-38"><a href="#cb10-38" aria-hidden="true" tabindex="-1"></a>MASCHINEN <span class="op">=</span> <span class="dv">10</span></span>
<span id="cb10-39"><a href="#cb10-39" aria-hidden="true" tabindex="-1"></a>INSTANZ_SEED <span class="op">=</span> <span class="dv">20260908</span></span>
<span id="cb10-40"><a href="#cb10-40" aria-hidden="true" tabindex="-1"></a>SOLVER_SEED <span class="op">=</span> <span class="dv">1</span> <span class="co"># bleibt ueber ALLE Laeufe gleich - das ist der Punkt</span></span>
<span id="cb10-41"><a href="#cb10-41" aria-hidden="true" tabindex="-1"></a>ARBEITERZAHLEN <span class="op">=</span> (<span class="dv">1</span>, <span class="dv">2</span>, <span class="dv">4</span>, <span class="dv">8</span>)</span>
<span id="cb10-42"><a href="#cb10-42" aria-hidden="true" tabindex="-1"></a>WIEDERHOLUNGEN <span class="op">=</span> <span class="dv">4</span></span>
<span id="cb10-43"><a href="#cb10-43" aria-hidden="true" tabindex="-1"></a>ZEITLIMIT <span class="op">=</span> <span class="fl">120.0</span></span>
<span id="cb10-44"><a href="#cb10-44" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-45"><a href="#cb10-45" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-46"><a href="#cb10-46" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> baue_instanz() <span class="op">-&gt;</span> <span class="bu">list</span>[<span class="bu">list</span>[<span class="bu">tuple</span>[<span class="bu">int</span>, <span class="bu">int</span>]]]:</span>
<span id="cb10-47"><a href="#cb10-47" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Klassischer Job-Shop: Jeder Auftrag besucht jede Maschine genau einmal,</span></span>
<span id="cb10-48"><a href="#cb10-48" aria-hidden="true" tabindex="-1"></a><span class="co"> in einer eigenen zufaelligen Reihenfolge.&quot;&quot;&quot;</span></span>
<span id="cb10-49"><a href="#cb10-49" aria-hidden="true" tabindex="-1"></a> rng <span class="op">=</span> np.random.default_rng(INSTANZ_SEED)</span>
<span id="cb10-50"><a href="#cb10-50" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> [[(<span class="bu">int</span>(m), <span class="bu">int</span>(rng.integers(<span class="dv">2</span>, <span class="dv">20</span>))) <span class="cf">for</span> m <span class="kw">in</span> rng.permutation(MASCHINEN)]</span>
<span id="cb10-51"><a href="#cb10-51" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> _ <span class="kw">in</span> <span class="bu">range</span>(AUFTRAEGE)]</span>
<span id="cb10-52"><a href="#cb10-52" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-53"><a href="#cb10-53" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-54"><a href="#cb10-54" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> loese(auftraege, arbeiter: <span class="bu">int</span>):</span>
<span id="cb10-55"><a href="#cb10-55" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Minimiert den Makespan. Gibt Status, Zielwert, Laufzeit und den Plan</span></span>
<span id="cb10-56"><a href="#cb10-56" aria-hidden="true" tabindex="-1"></a><span class="co"> zurueck - den Plan als Tupel aller Startzeiten, damit sich zwei Laeufe</span></span>
<span id="cb10-57"><a href="#cb10-57" aria-hidden="true" tabindex="-1"></a><span class="co"> vergleichen lassen.&quot;&quot;&quot;</span></span>
<span id="cb10-58"><a href="#cb10-58" aria-hidden="true" tabindex="-1"></a> horizont <span class="op">=</span> <span class="bu">sum</span>(dauer <span class="cf">for</span> auftrag <span class="kw">in</span> auftraege <span class="cf">for</span> _, dauer <span class="kw">in</span> auftrag)</span>
<span id="cb10-59"><a href="#cb10-59" aria-hidden="true" tabindex="-1"></a> modell <span class="op">=</span> cp_model.CpModel()</span>
<span id="cb10-60"><a href="#cb10-60" aria-hidden="true" tabindex="-1"></a> Gang <span class="op">=</span> collections.namedtuple(<span class="st">&quot;Gang&quot;</span>, <span class="st">&quot;start ende intervall&quot;</span>)</span>
<span id="cb10-61"><a href="#cb10-61" aria-hidden="true" tabindex="-1"></a> plaene: <span class="bu">dict</span>[<span class="bu">tuple</span>[<span class="bu">int</span>, <span class="bu">int</span>], Gang] <span class="op">=</span> {}</span>
<span id="cb10-62"><a href="#cb10-62" aria-hidden="true" tabindex="-1"></a> je_maschine <span class="op">=</span> collections.defaultdict(<span class="bu">list</span>)</span>
<span id="cb10-63"><a href="#cb10-63" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-64"><a href="#cb10-64" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> a, auftrag <span class="kw">in</span> <span class="bu">enumerate</span>(auftraege):</span>
<span id="cb10-65"><a href="#cb10-65" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> g, (maschine, dauer) <span class="kw">in</span> <span class="bu">enumerate</span>(auftrag):</span>
<span id="cb10-66"><a href="#cb10-66" aria-hidden="true" tabindex="-1"></a> start <span class="op">=</span> modell.NewIntVar(<span class="dv">0</span>, horizont, <span class="ss">f&quot;start_</span><span class="sc">{</span>a<span class="sc">}</span><span class="ss">_</span><span class="sc">{</span>g<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb10-67"><a href="#cb10-67" aria-hidden="true" tabindex="-1"></a> ende <span class="op">=</span> modell.NewIntVar(<span class="dv">0</span>, horizont, <span class="ss">f&quot;ende_</span><span class="sc">{</span>a<span class="sc">}</span><span class="ss">_</span><span class="sc">{</span>g<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb10-68"><a href="#cb10-68" aria-hidden="true" tabindex="-1"></a> intervall <span class="op">=</span> modell.NewIntervalVar(start, dauer, ende, <span class="ss">f&quot;iv_</span><span class="sc">{</span>a<span class="sc">}</span><span class="ss">_</span><span class="sc">{</span>g<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb10-69"><a href="#cb10-69" aria-hidden="true" tabindex="-1"></a> plaene[a, g] <span class="op">=</span> Gang(start, ende, intervall)</span>
<span id="cb10-70"><a href="#cb10-70" aria-hidden="true" tabindex="-1"></a> je_maschine[maschine].append(intervall)</span>
<span id="cb10-71"><a href="#cb10-71" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-72"><a href="#cb10-72" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> maschine <span class="kw">in</span> <span class="bu">range</span>(MASCHINEN):</span>
<span id="cb10-73"><a href="#cb10-73" aria-hidden="true" tabindex="-1"></a> modell.AddNoOverlap(je_maschine[maschine])</span>
<span id="cb10-74"><a href="#cb10-74" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> a, auftrag <span class="kw">in</span> <span class="bu">enumerate</span>(auftraege):</span>
<span id="cb10-75"><a href="#cb10-75" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> g <span class="kw">in</span> <span class="bu">range</span>(<span class="bu">len</span>(auftrag) <span class="op">-</span> <span class="dv">1</span>):</span>
<span id="cb10-76"><a href="#cb10-76" aria-hidden="true" tabindex="-1"></a> modell.Add(plaene[a, g <span class="op">+</span> <span class="dv">1</span>].start <span class="op">&gt;=</span> plaene[a, g].ende)</span>
<span id="cb10-77"><a href="#cb10-77" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-78"><a href="#cb10-78" aria-hidden="true" tabindex="-1"></a> makespan <span class="op">=</span> modell.NewIntVar(<span class="dv">0</span>, horizont, <span class="st">&quot;makespan&quot;</span>)</span>
<span id="cb10-79"><a href="#cb10-79" aria-hidden="true" tabindex="-1"></a> modell.AddMaxEquality(</span>
<span id="cb10-80"><a href="#cb10-80" aria-hidden="true" tabindex="-1"></a> makespan, [plaene[a, <span class="bu">len</span>(auftrag) <span class="op">-</span> <span class="dv">1</span>].ende</span>
<span id="cb10-81"><a href="#cb10-81" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> a, auftrag <span class="kw">in</span> <span class="bu">enumerate</span>(auftraege)])</span>
<span id="cb10-82"><a href="#cb10-82" aria-hidden="true" tabindex="-1"></a> modell.Minimize(makespan)</span>
<span id="cb10-83"><a href="#cb10-83" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-84"><a href="#cb10-84" aria-hidden="true" tabindex="-1"></a> loeser <span class="op">=</span> cp_model.CpSolver()</span>
<span id="cb10-85"><a href="#cb10-85" aria-hidden="true" tabindex="-1"></a> loeser.parameters.num_workers <span class="op">=</span> arbeiter</span>
<span id="cb10-86"><a href="#cb10-86" aria-hidden="true" tabindex="-1"></a> loeser.parameters.random_seed <span class="op">=</span> SOLVER_SEED</span>
<span id="cb10-87"><a href="#cb10-87" aria-hidden="true" tabindex="-1"></a> loeser.parameters.max_time_in_seconds <span class="op">=</span> ZEITLIMIT</span>
<span id="cb10-88"><a href="#cb10-88" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-89"><a href="#cb10-89" aria-hidden="true" tabindex="-1"></a> beginn <span class="op">=</span> time.perf_counter()</span>
<span id="cb10-90"><a href="#cb10-90" aria-hidden="true" tabindex="-1"></a> status <span class="op">=</span> loeser.Solve(modell)</span>
<span id="cb10-91"><a href="#cb10-91" aria-hidden="true" tabindex="-1"></a> dauer <span class="op">=</span> time.perf_counter() <span class="op">-</span> beginn</span>
<span id="cb10-92"><a href="#cb10-92" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-93"><a href="#cb10-93" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> status <span class="kw">not</span> <span class="kw">in</span> (cp_model.OPTIMAL, cp_model.FEASIBLE):</span>
<span id="cb10-94"><a href="#cb10-94" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> loeser.StatusName(status), <span class="va">None</span>, dauer, <span class="va">None</span></span>
<span id="cb10-95"><a href="#cb10-95" aria-hidden="true" tabindex="-1"></a> plan <span class="op">=</span> <span class="bu">tuple</span>(loeser.Value(plaene[a, g].start)</span>
<span id="cb10-96"><a href="#cb10-96" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> a, auftrag <span class="kw">in</span> <span class="bu">enumerate</span>(auftraege)</span>
<span id="cb10-97"><a href="#cb10-97" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> g <span class="kw">in</span> <span class="bu">range</span>(<span class="bu">len</span>(auftrag)))</span>
<span id="cb10-98"><a href="#cb10-98" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> loeser.StatusName(status), <span class="bu">int</span>(loeser.ObjectiveValue()), dauer, plan</span>
<span id="cb10-99"><a href="#cb10-99" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-100"><a href="#cb10-100" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-101"><a href="#cb10-101" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> <span class="va">__name__</span> <span class="op">==</span> <span class="st">&quot;__main__&quot;</span>:</span>
<span id="cb10-102"><a href="#cb10-102" aria-hidden="true" tabindex="-1"></a> auftraege <span class="op">=</span> baue_instanz()</span>
<span id="cb10-103"><a href="#cb10-103" aria-hidden="true" tabindex="-1"></a> gaenge <span class="op">=</span> <span class="bu">sum</span>(<span class="bu">len</span>(a) <span class="cf">for</span> a <span class="kw">in</span> auftraege)</span>
<span id="cb10-104"><a href="#cb10-104" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-105"><a href="#cb10-105" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb10-106"><a href="#cb10-106" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; WAS MEHRERE ARBEITER WIRKLICH BRINGEN&quot;</span>)</span>
<span id="cb10-107"><a href="#cb10-107" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb10-108"><a href="#cb10-108" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Job-Shop: </span><span class="sc">{</span>AUFTRAEGE<span class="sc">}</span><span class="ss"> Auftraege, </span><span class="sc">{</span>MASCHINEN<span class="sc">}</span><span class="ss"> Maschinen, &quot;</span></span>
<span id="cb10-109"><a href="#cb10-109" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>gaenge<span class="sc">}</span><span class="ss"> Arbeitsgaenge.&quot;</span>)</span>
<span id="cb10-110"><a href="#cb10-110" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;random_seed = </span><span class="sc">{</span>SOLVER_SEED<span class="sc">}</span><span class="ss"> in JEDEM Lauf, &quot;</span></span>
<span id="cb10-111"><a href="#cb10-111" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>WIEDERHOLUNGEN<span class="sc">}</span><span class="ss"> Wiederholungen je Arbeiterzahl.</span><span class="ch">\n</span><span class="ss">&quot;</span>)</span>
<span id="cb10-112"><a href="#cb10-112" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-113"><a href="#cb10-113" aria-hidden="true" tabindex="-1"></a> <span class="co"># Jede Konfiguration mehrfach - ein einzelner Zeitwert sagt nichts.</span></span>
<span id="cb10-114"><a href="#cb10-114" aria-hidden="true" tabindex="-1"></a> ergebnisse: <span class="bu">dict</span>[<span class="bu">int</span>, <span class="bu">list</span>[<span class="bu">tuple</span>]] <span class="op">=</span> {}</span>
<span id="cb10-115"><a href="#cb10-115" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> arbeiter <span class="kw">in</span> ARBEITERZAHLEN:</span>
<span id="cb10-116"><a href="#cb10-116" aria-hidden="true" tabindex="-1"></a> ergebnisse[arbeiter] <span class="op">=</span> [loese(auftraege, arbeiter)</span>
<span id="cb10-117"><a href="#cb10-117" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> _ <span class="kw">in</span> <span class="bu">range</span>(WIEDERHOLUNGEN)]</span>
<span id="cb10-118"><a href="#cb10-118" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-119"><a href="#cb10-119" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb10-120"><a href="#cb10-120" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; (1) Laufzeit: was die Arbeiter bringen&quot;</span>)</span>
<span id="cb10-121"><a href="#cb10-121" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb10-122"><a href="#cb10-122" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;Arbeiter&#39;</span><span class="sc">:&gt;9}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Status&#39;</span><span class="sc">:&gt;9}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Makespan&#39;</span><span class="sc">:&gt;9}</span><span class="ss"> &quot;</span></span>
<span id="cb10-123"><a href="#cb10-123" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;schnellste&#39;</span><span class="sc">:&gt;11}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;langsamste&#39;</span><span class="sc">:&gt;11}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Beschleunigung&#39;</span><span class="sc">:&gt;15}</span><span class="ss">&quot;</span>)</span>
<span id="cb10-124"><a href="#cb10-124" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb10-125"><a href="#cb10-125" aria-hidden="true" tabindex="-1"></a> basis_zeit <span class="op">=</span> <span class="bu">min</span>(d <span class="cf">for</span> _, _, d, _ <span class="kw">in</span> ergebnisse[<span class="dv">1</span>])</span>
<span id="cb10-126"><a href="#cb10-126" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> arbeiter <span class="kw">in</span> ARBEITERZAHLEN:</span>
<span id="cb10-127"><a href="#cb10-127" aria-hidden="true" tabindex="-1"></a> laeufe <span class="op">=</span> ergebnisse[arbeiter]</span>
<span id="cb10-128"><a href="#cb10-128" aria-hidden="true" tabindex="-1"></a> zeiten <span class="op">=</span> [d <span class="cf">for</span> _, _, d, _ <span class="kw">in</span> laeufe]</span>
<span id="cb10-129"><a href="#cb10-129" aria-hidden="true" tabindex="-1"></a> ziele <span class="op">=</span> {o <span class="cf">for</span> _, o, _, _ <span class="kw">in</span> laeufe}</span>
<span id="cb10-130"><a href="#cb10-130" aria-hidden="true" tabindex="-1"></a> status <span class="op">=</span> {s <span class="cf">for</span> s, _, _, _ <span class="kw">in</span> laeufe}</span>
<span id="cb10-131"><a href="#cb10-131" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>arbeiter<span class="sc">:&gt;9}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;/&#39;</span><span class="sc">.</span>join(<span class="bu">sorted</span>(status))<span class="sc">:&gt;9}</span><span class="ss"> &quot;</span></span>
<span id="cb10-132"><a href="#cb10-132" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;/&#39;</span><span class="sc">.</span>join(<span class="bu">str</span>(z) <span class="cf">for</span> z <span class="kw">in</span> <span class="bu">sorted</span>(ziele))<span class="sc">:&gt;9}</span><span class="ss"> &quot;</span></span>
<span id="cb10-133"><a href="#cb10-133" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="bu">min</span>(zeiten)<span class="sc">:&gt;10.2f}</span><span class="ss">s </span><span class="sc">{</span><span class="bu">max</span>(zeiten)<span class="sc">:&gt;10.2f}</span><span class="ss">s &quot;</span></span>
<span id="cb10-134"><a href="#cb10-134" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>basis_zeit <span class="op">/</span> <span class="bu">min</span>(zeiten)<span class="sc">:&gt;14.1f}</span><span class="ss">x&quot;</span>)</span>
<span id="cb10-135"><a href="#cb10-135" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-136"><a href="#cb10-136" aria-hidden="true" tabindex="-1"></a> beste <span class="op">=</span> <span class="bu">min</span>(ARBEITERZAHLEN, key<span class="op">=</span><span class="kw">lambda</span> w: <span class="bu">min</span>(d <span class="cf">for</span> _, _, d, _ <span class="kw">in</span> ergebnisse[w]))</span>
<span id="cb10-137"><a href="#cb10-137" aria-hidden="true" tabindex="-1"></a> faktor <span class="op">=</span> basis_zeit <span class="op">/</span> <span class="bu">min</span>(d <span class="cf">for</span> _, _, d, _ <span class="kw">in</span> ergebnisse[beste])</span>
<span id="cb10-138"><a href="#cb10-138" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="ch">\n</span><span class="ss"> Am schnellsten sind </span><span class="sc">{</span>beste<span class="sc">}</span><span class="ss"> Arbeiter: Faktor </span><span class="sc">{</span>faktor<span class="sc">:.1f}</span><span class="ss"> gegenueber&quot;</span>)</span>
<span id="cb10-139"><a href="#cb10-139" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; einem einzigen - also MEHR, als </span><span class="sc">{</span>beste<span class="sc">}</span><span class="ss"> Kerne hergeben sollten.&quot;</span>)</span>
<span id="cb10-140"><a href="#cb10-140" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Das ist kein Messfehler. CP-SAT laesst nicht </span><span class="sc">{</span>beste<span class="sc">}</span><span class="ss">-mal dieselbe Suche&quot;</span>)</span>
<span id="cb10-141"><a href="#cb10-141" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; laufen, sondern verschiedene Strategien nebeneinander, die einander&quot;</span>)</span>
<span id="cb10-142"><a href="#cb10-142" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; ihre Schranken zurufen. Der zweite Arbeiter ist deshalb nicht der&quot;</span>)</span>
<span id="cb10-143"><a href="#cb10-143" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; &#39;zweite Kern&#39;, sondern ein anderes Verfahren.&quot;</span>)</span>
<span id="cb10-144"><a href="#cb10-144" aria-hidden="true" tabindex="-1"></a> letzte <span class="op">=</span> ARBEITERZAHLEN[<span class="op">-</span><span class="dv">1</span>]</span>
<span id="cb10-145"><a href="#cb10-145" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> beste <span class="op">!=</span> letzte:</span>
<span id="cb10-146"><a href="#cb10-146" aria-hidden="true" tabindex="-1"></a> zeit_letzte <span class="op">=</span> <span class="bu">min</span>(d <span class="cf">for</span> _, _, d, _ <span class="kw">in</span> ergebnisse[letzte])</span>
<span id="cb10-147"><a href="#cb10-147" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="ch">\n</span><span class="ss"> Und mehr ist nicht immer besser: </span><span class="sc">{</span>letzte<span class="sc">}</span><span class="ss"> Arbeiter brauchen &quot;</span></span>
<span id="cb10-148"><a href="#cb10-148" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>zeit_letzte<span class="sc">:.2f}</span><span class="ss">s&quot;</span>)</span>
<span id="cb10-149"><a href="#cb10-149" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; gegenueber </span><span class="sc">{</span><span class="bu">min</span>(d <span class="cf">for</span> _, _, d, _ <span class="kw">in</span> ergebnisse[beste])<span class="sc">:.2f}</span><span class="ss">s bei &quot;</span></span>
<span id="cb10-150"><a href="#cb10-150" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>beste<span class="sc">}</span><span class="ss">. Ab einer gewissen Zahl kosten Abstimmung&quot;</span>)</span>
<span id="cb10-151"><a href="#cb10-151" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; und Speicherbandbreite mehr, als ein weiterer Suchstrang einbringt.&quot;</span>)</span>
<span id="cb10-152"><a href="#cb10-152" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-153"><a href="#cb10-153" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">&quot;</span> <span class="op">+</span> <span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb10-154"><a href="#cb10-154" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; (2) Reproduzierbarkeit: derselbe Seed, derselbe Plan?&quot;</span>)</span>
<span id="cb10-155"><a href="#cb10-155" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb10-156"><a href="#cb10-156" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;Arbeiter&#39;</span><span class="sc">:&gt;9}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Laeufe&#39;</span><span class="sc">:&gt;7}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;verschiedene Plaene&#39;</span><span class="sc">:&gt;21}</span><span class="ss"> &quot;</span></span>
<span id="cb10-157"><a href="#cb10-157" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;Makespan&#39;</span><span class="sc">:&gt;10}</span><span class="ss">&quot;</span>)</span>
<span id="cb10-158"><a href="#cb10-158" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb10-159"><a href="#cb10-159" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> arbeiter <span class="kw">in</span> ARBEITERZAHLEN:</span>
<span id="cb10-160"><a href="#cb10-160" aria-hidden="true" tabindex="-1"></a> plaene <span class="op">=</span> {p <span class="cf">for</span> _, _, _, p <span class="kw">in</span> ergebnisse[arbeiter] <span class="cf">if</span> p <span class="kw">is</span> <span class="kw">not</span> <span class="va">None</span>}</span>
<span id="cb10-161"><a href="#cb10-161" aria-hidden="true" tabindex="-1"></a> ziele <span class="op">=</span> {o <span class="cf">for</span> _, o, _, _ <span class="kw">in</span> ergebnisse[arbeiter]}</span>
<span id="cb10-162"><a href="#cb10-162" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>arbeiter<span class="sc">:&gt;9}</span><span class="ss"> </span><span class="sc">{</span>WIEDERHOLUNGEN<span class="sc">:&gt;7}</span><span class="ss"> </span><span class="sc">{</span><span class="bu">len</span>(plaene)<span class="sc">:&gt;21}</span><span class="ss"> &quot;</span></span>
<span id="cb10-163"><a href="#cb10-163" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;/&#39;</span><span class="sc">.</span>join(<span class="bu">str</span>(z) <span class="cf">for</span> z <span class="kw">in</span> <span class="bu">sorted</span>(ziele))<span class="sc">:&gt;10}</span><span class="ss">&quot;</span>)</span>
<span id="cb10-164"><a href="#cb10-164" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-165"><a href="#cb10-165" aria-hidden="true" tabindex="-1"></a> einer <span class="op">=</span> <span class="bu">len</span>({p <span class="cf">for</span> _, _, _, p <span class="kw">in</span> ergebnisse[<span class="dv">1</span>]})</span>
<span id="cb10-166"><a href="#cb10-166" aria-hidden="true" tabindex="-1"></a> <span class="co"># Ab welcher Arbeiterzahl bricht die Reproduzierbarkeit? Das ist die</span></span>
<span id="cb10-167"><a href="#cb10-167" aria-hidden="true" tabindex="-1"></a> <span class="co"># eigentliche Zahl - nicht, was acht Arbeiter anrichten.</span></span>
<span id="cb10-168"><a href="#cb10-168" aria-hidden="true" tabindex="-1"></a> ab <span class="op">=</span> <span class="bu">next</span>((w <span class="cf">for</span> w <span class="kw">in</span> ARBEITERZAHLEN</span>
<span id="cb10-169"><a href="#cb10-169" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="bu">len</span>({p <span class="cf">for</span> _, _, _, p <span class="kw">in</span> ergebnisse[w]}) <span class="op">&gt;</span> <span class="dv">1</span>), <span class="va">None</span>)</span>
<span id="cb10-170"><a href="#cb10-170" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="ch">\n</span><span class="ss"> Mit einem Arbeiter: </span><span class="sc">{</span>einer<span class="sc">}</span><span class="ss"> Plan aus </span><span class="sc">{</span>WIEDERHOLUNGEN<span class="sc">}</span><span class="ss"> Laeufen.&quot;</span>)</span>
<span id="cb10-171"><a href="#cb10-171" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> ab <span class="kw">is</span> <span class="kw">not</span> <span class="va">None</span>:</span>
<span id="cb10-172"><a href="#cb10-172" aria-hidden="true" tabindex="-1"></a> viele <span class="op">=</span> <span class="bu">len</span>({p <span class="cf">for</span> _, _, _, p <span class="kw">in</span> ergebnisse[ab]})</span>
<span id="cb10-173"><a href="#cb10-173" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Schon mit </span><span class="sc">{</span>ab<span class="sc">}</span><span class="ss"> Arbeitern: </span><span class="sc">{</span>viele<span class="sc">}</span><span class="ss"> verschiedene Plaene aus &quot;</span></span>
<span id="cb10-174"><a href="#cb10-174" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>WIEDERHOLUNGEN<span class="sc">}</span><span class="ss"> Laeufen -&quot;</span>)</span>
<span id="cb10-175"><a href="#cb10-175" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; bei identischem random_seed und identischem Zielwert.&quot;</span>)</span>
<span id="cb10-176"><a href="#cb10-176" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Es braucht also keine acht Arbeiter, um die Reproduzierbarkeit&quot;</span>)</span>
<span id="cb10-177"><a href="#cb10-177" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; zu verlieren. </span><span class="sc">{</span>ab<span class="sc">}</span><span class="ss"> genuegen.&quot;</span>)</span>
<span id="cb10-178"><a href="#cb10-178" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st"> Der Seed allein macht einen Lauf also NICHT reproduzierbar. Er legt&quot;</span>)</span>
<span id="cb10-179"><a href="#cb10-179" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; fest, wie ein einzelner Suchstrang wuerfelt - nicht, welcher von&quot;</span>)</span>
<span id="cb10-180"><a href="#cb10-180" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; mehreren zuerst fertig wird. Das entscheidet die Uhr.&quot;</span>)</span>
<span id="cb10-181"><a href="#cb10-181" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-182"><a href="#cb10-182" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">&quot;</span> <span class="op">+</span> <span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb10-183"><a href="#cb10-183" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; (3) Was daraus fuer Tests folgt&quot;</span>)</span>
<span id="cb10-184"><a href="#cb10-184" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb10-185"><a href="#cb10-185" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; Ein Test der Form assert plan == erwarteter_plan besteht mal und&quot;</span>)</span>
<span id="cb10-186"><a href="#cb10-186" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; scheitert mal, ohne dass sich am Code etwas geaendert haette.&quot;</span>)</span>
<span id="cb10-187"><a href="#cb10-187" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; Zu pruefen sind stattdessen:&quot;</span>)</span>
<span id="cb10-188"><a href="#cb10-188" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; * der ZIELWERT (hier in allen Laeufen gleich),&quot;</span>)</span>
<span id="cb10-189"><a href="#cb10-189" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; * die Einhaltung aller Regeln (Abnahmepruefung, Kapitel Praxisfallen),&quot;</span>)</span>
<span id="cb10-190"><a href="#cb10-190" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; * und der Status - nicht die Gestalt der Loesung.&quot;</span>)</span>
<span id="cb10-191"><a href="#cb10-191" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st"> Wer doch einen bestimmten Plan braucht - fuer eine abgedruckte&quot;</span>)</span>
<span id="cb10-192"><a href="#cb10-192" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; Ausgabe, fuer einen Regressionstest -, setzt num_workers = 1.&quot;</span>)</span>
<span id="cb10-193"><a href="#cb10-193" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Das kostet hier den Faktor </span><span class="sc">{</span>faktor<span class="sc">:.1f}</span><span class="ss"> an Laufzeit und ist genau&quot;</span>)</span>
<span id="cb10-194"><a href="#cb10-194" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; deshalb eine Entscheidung fuer den Test, nicht fuer den Betrieb.&quot;</span>)</span>
<span id="cb10-195"><a href="#cb10-195" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span></code></pre></div>
<p><strong>Erwartete Ausgabe</strong> (Laufzeiten <em>und</em> die Zahl der verschiedenen Pläne sind hardwareabhängig — genau das ist die Aussage; der Zielwert ist es nicht):</p>
<pre><code>==============================================================================
WAS MEHRERE ARBEITER WIRKLICH BRINGEN
==============================================================================
Job-Shop: 12 Auftraege, 10 Maschinen, 120 Arbeitsgaenge.
random_seed = 1 in JEDEM Lauf, 4 Wiederholungen je Arbeiterzahl.
==============================================================================
(1) Laufzeit: was die Arbeiter bringen
==============================================================================
Arbeiter Status Makespan schnellste langsamste Beschleunigung
------------------------------------------------------------------------------
1 OPTIMAL 183 5.62s 5.82s 1.0x
2 OPTIMAL 183 1.43s 2.04s 3.9x
4 OPTIMAL 183 0.60s 0.66s 9.4x
8 OPTIMAL 183 0.46s 0.65s 12.3x
Am schnellsten sind 8 Arbeiter: Faktor 12.3 gegenueber
einem einzigen - also MEHR, als 8 Kerne hergeben sollten.
Das ist kein Messfehler. CP-SAT laesst nicht 8-mal dieselbe Suche
laufen, sondern verschiedene Strategien nebeneinander, die einander
ihre Schranken zurufen. Der zweite Arbeiter ist deshalb nicht der
&#39;zweite Kern&#39;, sondern ein anderes Verfahren.
==============================================================================
(2) Reproduzierbarkeit: derselbe Seed, derselbe Plan?
==============================================================================
Arbeiter Laeufe verschiedene Plaene Makespan
------------------------------------------------------------------------------
1 4 1 183
2 4 3 183
4 4 3 183
8 4 4 183
Mit einem Arbeiter: 1 Plan aus 4 Laeufen.
Schon mit 2 Arbeitern: 3 verschiedene Plaene aus 4 Laeufen -
bei identischem random_seed und identischem Zielwert.
Es braucht also keine acht Arbeiter, um die Reproduzierbarkeit
zu verlieren. 2 genuegen.
Der Seed allein macht einen Lauf also NICHT reproduzierbar. Er legt
fest, wie ein einzelner Suchstrang wuerfelt - nicht, welcher von
mehreren zuerst fertig wird. Das entscheidet die Uhr.
==============================================================================
(3) Was daraus fuer Tests folgt
==============================================================================
Ein Test der Form assert plan == erwarteter_plan besteht mal und
scheitert mal, ohne dass sich am Code etwas geaendert haette.
Zu pruefen sind stattdessen:
* der ZIELWERT (hier in allen Laeufen gleich),
* die Einhaltung aller Regeln (Abnahmepruefung, Kapitel Praxisfallen),
* und der Status - nicht die Gestalt der Loesung.
Wer doch einen bestimmten Plan braucht - fuer eine abgedruckte
Ausgabe, fuer einen Regressionstest -, setzt num_workers = 1.
Das kostet hier den Faktor 12.3 an Laufzeit und ist genau
deshalb eine Entscheidung fuer den Test, nicht fuer den Betrieb.
==============================================================================</code></pre>
<h3 id="was-die-messung-zeigt">Was die Messung zeigt</h3>
<p><strong>Erstens: Die Beschleunigung ist überlinear.</strong> Acht Arbeiter sind im abgedruckten Lauf nicht achtmal, sondern <strong>gut zwölfmal</strong> schneller als einer. Das sieht nach Messfehler aus, ist aber die korrekte Beschreibung dessen, was CP-SAT tut: Es lässt <strong>nicht</strong> achtmal dieselbe Suche auf verschiedenen Kernen laufen, sondern <strong>verschiedene Strategien nebeneinander</strong> — Suche mit LP-Relaxation, Suche mit anderen Verzweigungsregeln, lokale Suche —, und die teilen einander ihre Schranken mit. Der zweite Arbeiter ist deshalb nicht „der zweite Kern“, sondern ein anderes Verfahren, das zufällig auch rechnet.</p>
<p>Daraus folgt zweierlei. Ein Speedup über der Kernzahl ist bei CP-SAT normal und kein Grund zum Misstrauen. Und der Zugewinn flacht ab: Zwischen vier und acht Arbeitern lag in unseren Wiederholungen mal das eine, mal das andere vorn — ab einer gewissen Zahl kosten Abstimmung und Speicherbandbreite mehr, als ein weiterer Suchstrang einbringt. Auch hier gilt: messen, nicht schätzen.</p>
<p><strong>Zweitens — und das ist der Befund, der die Testpraxis bestimmt: Der Seed genügt nicht.</strong> Bei einem Arbeiter liefern vier Läufe <strong>einen</strong> Plan. Schon bei <strong>zwei</strong> Arbeitern liefern vier Läufe <strong>drei verschiedene</strong> — bei identischem <code>random_seed</code> und identischem Zielwert 183.</p>
<p>Es braucht also keine acht Arbeiter, um die Reproduzierbarkeit zu verlieren. Zwei genügen. Der Grund ist einfach, sobald man ihn ausspricht: Der Seed legt fest, wie ein <strong>einzelner</strong> Suchstrang würfelt. Er legt nicht fest, <strong>welcher von mehreren zuerst fertig wird</strong> — das entscheidet die Uhr, und die ist bei jedem Lauf anders.</p>
<blockquote>
<p><strong>⚠️ <code>random_seed</code> allein macht nichts reproduzierbar</strong> Ein häufiger Irrtum: „Ich habe doch einen Seed gesetzt.“ Für einen Zufallsgenerator stimmt das. Für einen <strong>parallelen</strong> Solver nicht — dort ist die zweite Zufallsquelle die Ausführungsreihenfolge, und die lässt sich nicht mit einem Parameter festnageln. Reproduzierbar wird der Lauf erst mit <code>num_workers = 1</code> <strong>und</strong> Seed. Wer nur eines von beidem setzt, hat einen Test, der gelegentlich grundlos rot wird.</p>
</blockquote>
<h3 id="was-daraus-für-tests-folgt">Was daraus für Tests folgt</h3>
<p>Der Zielwert war in <strong>allen</strong> sechzehn Läufen derselbe: 183. Die Gestalt der Lösung war es nicht. Genau daran entscheidet sich, was ein Test prüfen darf:</p>
<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<thead>
<tr class="header">
<th>Prüfen</th>
<th>Nicht prüfen</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>den <strong>Zielwert</strong> — er ist eindeutig, wenn das Modell optimal gelöst wurde</td>
<td>die <strong>Gestalt</strong> des Plans (<code>assert plan == erwarteter_plan</code>)</td>
</tr>
<tr class="even">
<td>die <strong>Einhaltung aller Regeln</strong> über eine Abnahmeprüfung (<a href="praxisfallen.html#sec:praxisfallen-or-kern">Abschnitt 22.6</a>)</td>
<td>die Reihenfolge, in der gleichwertige Aufträge zugewiesen wurden</td>
</tr>
<tr class="odd">
<td>den <strong>Status</strong> (<code>OPTIMAL</code>, <code>FEASIBLE</code>, <code>INFEASIBLE</code>)</td>
<td>die Laufzeit als feste Zahl</td>
</tr>
</tbody>
</table>
<p>Wer doch einen bestimmten Plan braucht — für eine abgedruckte Ausgabe, für einen Regressionstest —, setzt <code>num_workers = 1</code>. Das kostet hier den Faktor 12 an Laufzeit, und genau deshalb ist es eine Entscheidung <strong>für den Test</strong>, nicht für den Betrieb. In Produktion lässt man beide Parameter auf ihren Standardwerten.</p>
<blockquote>
<p><strong>🎯 Merksatz</strong> Ein paralleler Solver hat zwei Zufallsquellen: den Seed und die Uhr. Nur die erste lässt sich setzen. Wer Reproduzierbarkeit braucht, muss die zweite abschalten — und das heißt <code>num_workers = 1</code>.</p>
</blockquote>
<hr />
<h2 id="sec:cpsat-statusfaelle">7.8 Die fünf Antworten von CP-SAT</h2>
<p><a href="milp.html#sec:milp-gap">Abschnitt 6.8</a> hat gezeigt, warum <code>OPTIMAL</code> bei ganzzahligen Problemen nicht der Normalfall ist. Bei CP-SAT kommt eine Besonderheit hinzu: Es gibt <strong>fünf</strong> verschiedene Antworten, und drei davon werden regelmäßig verwechselt.</p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr class="header">
<th>Status</th>
<th>Was er über <em>was</em> aussagt</th>
<th>Richtige Reaktion</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>OPTIMAL</code></td>
<td>über die <strong>Lösung</strong> — sie ist beweisbar bestmöglich</td>
<td>ausführen</td>
</tr>
<tr class="even">
<td><code>FEASIBLE</code></td>
<td>über die <strong>Zeit</strong> — ein gültiger Plan liegt vor, der Beweis fehlt</td>
<td>Gap prüfen, dann entscheiden</td>
</tr>
<tr class="odd">
<td><code>INFEASIBLE</code></td>
<td>über Ihr <strong>Modell</strong> — es gibt keinen zulässigen Plan</td>
<td>harte Regeln lockern oder weich machen</td>
</tr>
<tr class="even">
<td><code>MODEL_INVALID</code></td>
<td>über Ihren <strong>Code</strong> — das Modell ist gar kein gültiges Modell</td>
<td>Programmierfehler beheben</td>
</tr>
<tr class="odd">
<td><code>UNKNOWN</code></td>
<td>über die <strong>Zeit</strong> — nichts gefunden, aber auch nichts widerlegt</td>
<td>mehr Zeit, einfacheres Modell oder <code>AddHint</code></td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong>🎯 Merksatz</strong> <code>INFEASIBLE</code> und <code>UNKNOWN</code> sehen im Log ähnlich aus und bedeuten das Gegenteil. <code>INFEASIBLE</code> heißt: <em>Es gibt keine Lösung</em> — auch unendlich viel Rechenzeit ändert daran nichts, die Ursache liegt in Ihren Regeln. <code>UNKNOWN</code> heißt: <em>Ich habe keine gefunden</em> — ob es eine gibt, ist völlig offen. Wer beides in ein <code>else: return None</code> zusammenfasst, wirft genau die Information weg, die zur Fehlersuche nötig wäre.</p>
</blockquote>
<p>Und <code>MODEL_INVALID</code> ist von beiden nochmals verschieden: Es ist immer ein <strong>Programmierfehler</strong>, kein fachliches Problem. Der klassische Fall ist eine Variable mit leerem Wertebereich — <code>NewIntVar(mindestbesetzung, kapazitaet, ...)</code>, wobei die Daten vertauscht ankamen und die untere Schranke über der oberen liegt.</p>
<p>Das folgende Programm erzeugt jeden Fall absichtlich und zeigt die passende Auswertung.</p>
<div class="sourceCode" id="cb12"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb12-1"><a href="#cb12-1" aria-hidden="true" tabindex="-1"></a><span class="co">#!/usr/bin/env python3</span></span>
<span id="cb12-2"><a href="#cb12-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-3"><a href="#cb12-3" aria-hidden="true" tabindex="-1"></a><span class="co"># CP_SAT_Statusfaelle.py</span></span>
<span id="cb12-4"><a href="#cb12-4" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb12-5"><a href="#cb12-5" aria-hidden="true" tabindex="-1"></a><span class="co">Kapitel CP-SAT: Alle fuenf Antworten von CP-SAT - und was jede bedeutet.</span></span>
<span id="cb12-6"><a href="#cb12-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-7"><a href="#cb12-7" aria-hidden="true" tabindex="-1"></a><span class="co">Der haeufigste Fehler beim Einsatz von CP-SAT im Betrieb ist eine Zeile wie</span></span>
<span id="cb12-8"><a href="#cb12-8" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-9"><a href="#cb12-9" aria-hidden="true" tabindex="-1"></a><span class="co"> if status == cp_model.OPTIMAL:</span></span>
<span id="cb12-10"><a href="#cb12-10" aria-hidden="true" tabindex="-1"></a><span class="co"> ...verwerte die Loesung...</span></span>
<span id="cb12-11"><a href="#cb12-11" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-12"><a href="#cb12-12" aria-hidden="true" tabindex="-1"></a><span class="co">Sie wirft in drei von fuenf Faellen etwas weg, das man haette gebrauchen</span></span>
<span id="cb12-13"><a href="#cb12-13" aria-hidden="true" tabindex="-1"></a><span class="co">koennen, und verschweigt in einem weiteren Fall einen Modellierungsfehler.</span></span>
<span id="cb12-14"><a href="#cb12-14" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-15"><a href="#cb12-15" aria-hidden="true" tabindex="-1"></a><span class="co">Dieses Programm erzeugt jeden Statusfall absichtlich und zeigt die passende</span></span>
<span id="cb12-16"><a href="#cb12-16" aria-hidden="true" tabindex="-1"></a><span class="co">Reaktion:</span></span>
<span id="cb12-17"><a href="#cb12-17" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-18"><a href="#cb12-18" aria-hidden="true" tabindex="-1"></a><span class="co"> OPTIMAL beweisbar bestmoeglich -&gt; ausfuehren</span></span>
<span id="cb12-19"><a href="#cb12-19" aria-hidden="true" tabindex="-1"></a><span class="co"> FEASIBLE zulaessig, Zeit war um -&gt; Gap pruefen, dann entscheiden</span></span>
<span id="cb12-20"><a href="#cb12-20" aria-hidden="true" tabindex="-1"></a><span class="co"> INFEASIBLE kein Plan existiert -&gt; harte Regeln lockern</span></span>
<span id="cb12-21"><a href="#cb12-21" aria-hidden="true" tabindex="-1"></a><span class="co"> MODEL_INVALID das Modell ist fehlerhaft -&gt; Programmierfehler beheben</span></span>
<span id="cb12-22"><a href="#cb12-22" aria-hidden="true" tabindex="-1"></a><span class="co"> UNKNOWN nichts gefunden, Zeit war um -&gt; mehr Zeit oder Heuristik</span></span>
<span id="cb12-23"><a href="#cb12-23" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-24"><a href="#cb12-24" aria-hidden="true" tabindex="-1"></a><span class="co">Benoetigt: numpy, ortools</span></span>
<span id="cb12-25"><a href="#cb12-25" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb12-26"><a href="#cb12-26" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-27"><a href="#cb12-27" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> __future__ <span class="im">import</span> annotations</span>
<span id="cb12-28"><a href="#cb12-28" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-29"><a href="#cb12-29" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> dataclasses <span class="im">import</span> dataclass</span>
<span id="cb12-30"><a href="#cb12-30" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-31"><a href="#cb12-31" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb12-32"><a href="#cb12-32" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> ortools.sat.python <span class="im">import</span> cp_model</span>
<span id="cb12-33"><a href="#cb12-33" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-34"><a href="#cb12-34" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-35"><a href="#cb12-35" aria-hidden="true" tabindex="-1"></a><span class="at">@dataclass</span></span>
<span id="cb12-36"><a href="#cb12-36" aria-hidden="true" tabindex="-1"></a><span class="kw">class</span> Auswertung:</span>
<span id="cb12-37"><a href="#cb12-37" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Was nach einem CP-SAT-Lauf tatsaechlich feststeht.&quot;&quot;&quot;</span></span>
<span id="cb12-38"><a href="#cb12-38" aria-hidden="true" tabindex="-1"></a> status: <span class="bu">str</span></span>
<span id="cb12-39"><a href="#cb12-39" aria-hidden="true" tabindex="-1"></a> hat_loesung: <span class="bu">bool</span></span>
<span id="cb12-40"><a href="#cb12-40" aria-hidden="true" tabindex="-1"></a> beweisbar_optimal: <span class="bu">bool</span></span>
<span id="cb12-41"><a href="#cb12-41" aria-hidden="true" tabindex="-1"></a> zielwert: <span class="bu">float</span> <span class="op">|</span> <span class="va">None</span></span>
<span id="cb12-42"><a href="#cb12-42" aria-hidden="true" tabindex="-1"></a> schranke: <span class="bu">float</span> <span class="op">|</span> <span class="va">None</span></span>
<span id="cb12-43"><a href="#cb12-43" aria-hidden="true" tabindex="-1"></a> gap: <span class="bu">float</span> <span class="op">|</span> <span class="va">None</span></span>
<span id="cb12-44"><a href="#cb12-44" aria-hidden="true" tabindex="-1"></a> empfehlung: <span class="bu">str</span></span>
<span id="cb12-45"><a href="#cb12-45" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-46"><a href="#cb12-46" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-47"><a href="#cb12-47" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> werte_aus(loeser: cp_model.CpSolver, status: <span class="bu">int</span>,</span>
<span id="cb12-48"><a href="#cb12-48" aria-hidden="true" tabindex="-1"></a> mit_zielfunktion: <span class="bu">bool</span>) <span class="op">-&gt;</span> Auswertung:</span>
<span id="cb12-49"><a href="#cb12-49" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Uebersetzt einen CP-SAT-Status in eine Handlungsempfehlung.</span></span>
<span id="cb12-50"><a href="#cb12-50" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-51"><a href="#cb12-51" aria-hidden="true" tabindex="-1"></a><span class="co"> Genau diese Funktion fehlt in den meisten Projekten. Sie ist kurz, aber</span></span>
<span id="cb12-52"><a href="#cb12-52" aria-hidden="true" tabindex="-1"></a><span class="co"> sie ist der Unterschied zwischen einem Prototyp und einem System, das</span></span>
<span id="cb12-53"><a href="#cb12-53" aria-hidden="true" tabindex="-1"></a><span class="co"> nachts ohne Aufsicht laeuft.</span></span>
<span id="cb12-54"><a href="#cb12-54" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb12-55"><a href="#cb12-55" aria-hidden="true" tabindex="-1"></a> name <span class="op">=</span> loeser.StatusName(status)</span>
<span id="cb12-56"><a href="#cb12-56" aria-hidden="true" tabindex="-1"></a> hat_loesung <span class="op">=</span> status <span class="kw">in</span> (cp_model.OPTIMAL, cp_model.FEASIBLE)</span>
<span id="cb12-57"><a href="#cb12-57" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-58"><a href="#cb12-58" aria-hidden="true" tabindex="-1"></a> zielwert <span class="op">=</span> schranke <span class="op">=</span> gap <span class="op">=</span> <span class="va">None</span></span>
<span id="cb12-59"><a href="#cb12-59" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> hat_loesung <span class="kw">and</span> mit_zielfunktion:</span>
<span id="cb12-60"><a href="#cb12-60" aria-hidden="true" tabindex="-1"></a> zielwert <span class="op">=</span> loeser.ObjectiveValue()</span>
<span id="cb12-61"><a href="#cb12-61" aria-hidden="true" tabindex="-1"></a> schranke <span class="op">=</span> loeser.BestObjectiveBound()</span>
<span id="cb12-62"><a href="#cb12-62" aria-hidden="true" tabindex="-1"></a> nenner <span class="op">=</span> <span class="bu">max</span>(<span class="bu">abs</span>(zielwert), <span class="fl">1e-9</span>)</span>
<span id="cb12-63"><a href="#cb12-63" aria-hidden="true" tabindex="-1"></a> gap <span class="op">=</span> <span class="bu">abs</span>(zielwert <span class="op">-</span> schranke) <span class="op">/</span> nenner</span>
<span id="cb12-64"><a href="#cb12-64" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-65"><a href="#cb12-65" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> status <span class="op">==</span> cp_model.OPTIMAL:</span>
<span id="cb12-66"><a href="#cb12-66" aria-hidden="true" tabindex="-1"></a> empfehlung <span class="op">=</span> <span class="st">&quot;Ausfuehren. Besser geht es nachweislich nicht.&quot;</span></span>
<span id="cb12-67"><a href="#cb12-67" aria-hidden="true" tabindex="-1"></a> <span class="cf">elif</span> status <span class="op">==</span> cp_model.FEASIBLE:</span>
<span id="cb12-68"><a href="#cb12-68" aria-hidden="true" tabindex="-1"></a> empfehlung <span class="op">=</span> (<span class="ss">f&quot;Brauchbar. Der Plan ist hoechstens </span><span class="sc">{</span>gap <span class="op">*</span> <span class="dv">100</span><span class="sc">:.2f}</span><span class="ss"> % &quot;</span></span>
<span id="cb12-69"><a href="#cb12-69" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;schlechter als das theoretisch Bestmoegliche.&quot;</span></span>
<span id="cb12-70"><a href="#cb12-70" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> gap <span class="kw">is</span> <span class="kw">not</span> <span class="va">None</span> <span class="cf">else</span></span>
<span id="cb12-71"><a href="#cb12-71" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Brauchbar, aber nicht als optimal bewiesen.&quot;</span>)</span>
<span id="cb12-72"><a href="#cb12-72" aria-hidden="true" tabindex="-1"></a> <span class="cf">elif</span> status <span class="op">==</span> cp_model.INFEASIBLE:</span>
<span id="cb12-73"><a href="#cb12-73" aria-hidden="true" tabindex="-1"></a> empfehlung <span class="op">=</span> (<span class="st">&quot;KEIN Plan existiert. Das ist eine Aussage ueber Ihre &quot;</span></span>
<span id="cb12-74"><a href="#cb12-74" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;harten Regeln, nicht ueber die Rechenzeit: Auch &quot;</span></span>
<span id="cb12-75"><a href="#cb12-75" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;unendlich viel Zeit wuerde nichts aendern. Regeln &quot;</span></span>
<span id="cb12-76"><a href="#cb12-76" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;lockern oder weich machen (Anhang C).&quot;</span>)</span>
<span id="cb12-77"><a href="#cb12-77" aria-hidden="true" tabindex="-1"></a> <span class="cf">elif</span> status <span class="op">==</span> cp_model.MODEL_INVALID:</span>
<span id="cb12-78"><a href="#cb12-78" aria-hidden="true" tabindex="-1"></a> empfehlung <span class="op">=</span> (<span class="st">&quot;Das MODELL ist fehlerhaft, nicht das Problem. &quot;</span></span>
<span id="cb12-79"><a href="#cb12-79" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Typisch: eine Variable mit leerem Wertebereich. &quot;</span></span>
<span id="cb12-80"><a href="#cb12-80" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;model.Validate() nennt die Ursache.&quot;</span>)</span>
<span id="cb12-81"><a href="#cb12-81" aria-hidden="true" tabindex="-1"></a> <span class="cf">else</span>: <span class="co"># UNKNOWN</span></span>
<span id="cb12-82"><a href="#cb12-82" aria-hidden="true" tabindex="-1"></a> empfehlung <span class="op">=</span> (<span class="st">&quot;Nichts gefunden - aber auch nicht widerlegt. Mehr Zeit &quot;</span></span>
<span id="cb12-83"><a href="#cb12-83" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;geben, das Modell vereinfachen oder mit einer &quot;</span></span>
<span id="cb12-84"><a href="#cb12-84" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Heuristik vorbelegen (AddHint).&quot;</span>)</span>
<span id="cb12-85"><a href="#cb12-85" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-86"><a href="#cb12-86" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> Auswertung(</span>
<span id="cb12-87"><a href="#cb12-87" aria-hidden="true" tabindex="-1"></a> status<span class="op">=</span>name,</span>
<span id="cb12-88"><a href="#cb12-88" aria-hidden="true" tabindex="-1"></a> hat_loesung<span class="op">=</span>hat_loesung,</span>
<span id="cb12-89"><a href="#cb12-89" aria-hidden="true" tabindex="-1"></a> beweisbar_optimal<span class="op">=</span>status <span class="op">==</span> cp_model.OPTIMAL,</span>
<span id="cb12-90"><a href="#cb12-90" aria-hidden="true" tabindex="-1"></a> zielwert<span class="op">=</span>zielwert, schranke<span class="op">=</span>schranke, gap<span class="op">=</span>gap,</span>
<span id="cb12-91"><a href="#cb12-91" aria-hidden="true" tabindex="-1"></a> empfehlung<span class="op">=</span>empfehlung,</span>
<span id="cb12-92"><a href="#cb12-92" aria-hidden="true" tabindex="-1"></a> )</span>
<span id="cb12-93"><a href="#cb12-93" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-94"><a href="#cb12-94" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-95"><a href="#cb12-95" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> loese(baue, zeitlimit: <span class="bu">float</span>, mit_zielfunktion: <span class="bu">bool</span>) <span class="op">-&gt;</span> Auswertung:</span>
<span id="cb12-96"><a href="#cb12-96" aria-hidden="true" tabindex="-1"></a> modell <span class="op">=</span> cp_model.CpModel()</span>
<span id="cb12-97"><a href="#cb12-97" aria-hidden="true" tabindex="-1"></a> baue(modell)</span>
<span id="cb12-98"><a href="#cb12-98" aria-hidden="true" tabindex="-1"></a> loeser <span class="op">=</span> cp_model.CpSolver()</span>
<span id="cb12-99"><a href="#cb12-99" aria-hidden="true" tabindex="-1"></a> loeser.parameters.max_time_in_seconds <span class="op">=</span> zeitlimit</span>
<span id="cb12-100"><a href="#cb12-100" aria-hidden="true" tabindex="-1"></a> <span class="co"># Fuer reproduzierbare Buchausgaben; im Betrieb weglassen.</span></span>
<span id="cb12-101"><a href="#cb12-101" aria-hidden="true" tabindex="-1"></a> loeser.parameters.num_workers <span class="op">=</span> <span class="dv">1</span></span>
<span id="cb12-102"><a href="#cb12-102" aria-hidden="true" tabindex="-1"></a> loeser.parameters.random_seed <span class="op">=</span> <span class="dv">1</span></span>
<span id="cb12-103"><a href="#cb12-103" aria-hidden="true" tabindex="-1"></a> status <span class="op">=</span> loeser.Solve(modell)</span>
<span id="cb12-104"><a href="#cb12-104" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> werte_aus(loeser, status, mit_zielfunktion)</span>
<span id="cb12-105"><a href="#cb12-105" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-106"><a href="#cb12-106" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-107"><a href="#cb12-107" aria-hidden="true" tabindex="-1"></a><span class="co"># --- Die fuenf Faelle --------------------------------------------------------</span></span>
<span id="cb12-108"><a href="#cb12-108" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-109"><a href="#cb12-109" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> dienstplan_loesbar(modell: cp_model.CpModel) <span class="op">-&gt;</span> <span class="va">None</span>:</span>
<span id="cb12-110"><a href="#cb12-110" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Vier Personen, fuenf Dienste, hoechstens zwei je Person - loesbar.&quot;&quot;&quot;</span></span>
<span id="cb12-111"><a href="#cb12-111" aria-hidden="true" tabindex="-1"></a> personen, dienste <span class="op">=</span> <span class="dv">4</span>, <span class="dv">5</span></span>
<span id="cb12-112"><a href="#cb12-112" aria-hidden="true" tabindex="-1"></a> x <span class="op">=</span> [[modell.NewBoolVar(<span class="ss">f&quot;x_</span><span class="sc">{</span>i<span class="sc">}</span><span class="ss">_</span><span class="sc">{</span>j<span class="sc">}</span><span class="ss">&quot;</span>) <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(dienste)]</span>
<span id="cb12-113"><a href="#cb12-113" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(personen)]</span>
<span id="cb12-114"><a href="#cb12-114" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(dienste):</span>
<span id="cb12-115"><a href="#cb12-115" aria-hidden="true" tabindex="-1"></a> modell.AddExactlyOne(x[i][j] <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(personen))</span>
<span id="cb12-116"><a href="#cb12-116" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(personen):</span>
<span id="cb12-117"><a href="#cb12-117" aria-hidden="true" tabindex="-1"></a> modell.Add(<span class="bu">sum</span>(x[i]) <span class="op">&lt;=</span> <span class="dv">2</span>)</span>
<span id="cb12-118"><a href="#cb12-118" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-119"><a href="#cb12-119" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-120"><a href="#cb12-120" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> dienstplan_unloesbar(modell: cp_model.CpModel) <span class="op">-&gt;</span> <span class="va">None</span>:</span>
<span id="cb12-121"><a href="#cb12-121" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Dieselben fuenf Dienste, aber hoechstens EIN Dienst je Person.</span></span>
<span id="cb12-122"><a href="#cb12-122" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-123"><a href="#cb12-123" aria-hidden="true" tabindex="-1"></a><span class="co"> Vier Personen koennen zusammen hoechstens vier Dienste uebernehmen - fuer</span></span>
<span id="cb12-124"><a href="#cb12-124" aria-hidden="true" tabindex="-1"></a><span class="co"> fuenf Dienste reicht das nicht. Kein Solver der Welt findet hier etwas.</span></span>
<span id="cb12-125"><a href="#cb12-125" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb12-126"><a href="#cb12-126" aria-hidden="true" tabindex="-1"></a> personen, dienste <span class="op">=</span> <span class="dv">4</span>, <span class="dv">5</span></span>
<span id="cb12-127"><a href="#cb12-127" aria-hidden="true" tabindex="-1"></a> x <span class="op">=</span> [[modell.NewBoolVar(<span class="ss">f&quot;x_</span><span class="sc">{</span>i<span class="sc">}</span><span class="ss">_</span><span class="sc">{</span>j<span class="sc">}</span><span class="ss">&quot;</span>) <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(dienste)]</span>
<span id="cb12-128"><a href="#cb12-128" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(personen)]</span>
<span id="cb12-129"><a href="#cb12-129" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(dienste):</span>
<span id="cb12-130"><a href="#cb12-130" aria-hidden="true" tabindex="-1"></a> modell.AddExactlyOne(x[i][j] <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(personen))</span>
<span id="cb12-131"><a href="#cb12-131" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(personen):</span>
<span id="cb12-132"><a href="#cb12-132" aria-hidden="true" tabindex="-1"></a> modell.Add(<span class="bu">sum</span>(x[i]) <span class="op">&lt;=</span> <span class="dv">1</span>)</span>
<span id="cb12-133"><a href="#cb12-133" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-134"><a href="#cb12-134" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-135"><a href="#cb12-135" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> modell_fehlerhaft(modell: cp_model.CpModel) <span class="op">-&gt;</span> <span class="va">None</span>:</span>
<span id="cb12-136"><a href="#cb12-136" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Eine Variable mit leerem Wertebereich: untere Schranke &gt; obere.</span></span>
<span id="cb12-137"><a href="#cb12-137" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-138"><a href="#cb12-138" aria-hidden="true" tabindex="-1"></a><span class="co"> Im Alltag entsteht das durch einen Rechenfehler in den Grenzen, etwa</span></span>
<span id="cb12-139"><a href="#cb12-139" aria-hidden="true" tabindex="-1"></a><span class="co"> NewIntVar(mindestbesetzung, kapazitaet, ...) bei falsch sortierten Daten.</span></span>
<span id="cb12-140"><a href="#cb12-140" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb12-141"><a href="#cb12-141" aria-hidden="true" tabindex="-1"></a> kaputt <span class="op">=</span> modell.NewIntVar(<span class="dv">5</span>, <span class="dv">2</span>, <span class="st">&quot;leerer_bereich&quot;</span>)</span>
<span id="cb12-142"><a href="#cb12-142" aria-hidden="true" tabindex="-1"></a> modell.Add(kaputt <span class="op">&gt;=</span> <span class="dv">0</span>)</span>
<span id="cb12-143"><a href="#cb12-143" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-144"><a href="#cb12-144" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-145"><a href="#cb12-145" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> lastverteilung(modell: cp_model.CpModel) <span class="op">-&gt;</span> <span class="va">None</span>:</span>
<span id="cb12-146"><a href="#cb12-146" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;120 Auftraege auf 11 Maschinen - gross genug, dass der Optimalitaets-</span></span>
<span id="cb12-147"><a href="#cb12-147" aria-hidden="true" tabindex="-1"></a><span class="co"> beweis laenger dauert als das Finden einer sehr guten Loesung.&quot;&quot;&quot;</span></span>
<span id="cb12-148"><a href="#cb12-148" aria-hidden="true" tabindex="-1"></a> rng <span class="op">=</span> np.random.default_rng(<span class="dv">3</span>)</span>
<span id="cb12-149"><a href="#cb12-149" aria-hidden="true" tabindex="-1"></a> dauer <span class="op">=</span> rng.integers(<span class="dv">100</span>, <span class="dv">9000</span>, <span class="dv">120</span>)</span>
<span id="cb12-150"><a href="#cb12-150" aria-hidden="true" tabindex="-1"></a> n, maschinen <span class="op">=</span> <span class="bu">len</span>(dauer), <span class="dv">11</span></span>
<span id="cb12-151"><a href="#cb12-151" aria-hidden="true" tabindex="-1"></a> obergrenze <span class="op">=</span> <span class="bu">int</span>(dauer.<span class="bu">sum</span>())</span>
<span id="cb12-152"><a href="#cb12-152" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-153"><a href="#cb12-153" aria-hidden="true" tabindex="-1"></a> x <span class="op">=</span> [[modell.NewBoolVar(<span class="ss">f&quot;x_</span><span class="sc">{</span>i<span class="sc">}</span><span class="ss">_</span><span class="sc">{</span>k<span class="sc">}</span><span class="ss">&quot;</span>) <span class="cf">for</span> k <span class="kw">in</span> <span class="bu">range</span>(maschinen)]</span>
<span id="cb12-154"><a href="#cb12-154" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(n)]</span>
<span id="cb12-155"><a href="#cb12-155" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(n):</span>
<span id="cb12-156"><a href="#cb12-156" aria-hidden="true" tabindex="-1"></a> modell.AddExactlyOne(x[i])</span>
<span id="cb12-157"><a href="#cb12-157" aria-hidden="true" tabindex="-1"></a> belegung <span class="op">=</span> [modell.NewIntVar(<span class="dv">0</span>, obergrenze, <span class="ss">f&quot;l_</span><span class="sc">{</span>k<span class="sc">}</span><span class="ss">&quot;</span>) <span class="cf">for</span> k <span class="kw">in</span> <span class="bu">range</span>(maschinen)]</span>
<span id="cb12-158"><a href="#cb12-158" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> k <span class="kw">in</span> <span class="bu">range</span>(maschinen):</span>
<span id="cb12-159"><a href="#cb12-159" aria-hidden="true" tabindex="-1"></a> modell.Add(belegung[k] <span class="op">==</span> <span class="bu">sum</span>(<span class="bu">int</span>(dauer[i]) <span class="op">*</span> x[i][k] <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(n)))</span>
<span id="cb12-160"><a href="#cb12-160" aria-hidden="true" tabindex="-1"></a> makespan <span class="op">=</span> modell.NewIntVar(<span class="dv">0</span>, obergrenze, <span class="st">&quot;makespan&quot;</span>)</span>
<span id="cb12-161"><a href="#cb12-161" aria-hidden="true" tabindex="-1"></a> modell.AddMaxEquality(makespan, belegung)</span>
<span id="cb12-162"><a href="#cb12-162" aria-hidden="true" tabindex="-1"></a> modell.Minimize(makespan)</span>
<span id="cb12-163"><a href="#cb12-163" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-164"><a href="#cb12-164" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-165"><a href="#cb12-165" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> zahlpartition(modell: cp_model.CpModel) <span class="op">-&gt;</span> <span class="va">None</span>:</span>
<span id="cb12-166"><a href="#cb12-166" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;46 grosse Zahlen exakt in zwei gleich schwere Haelften teilen.</span></span>
<span id="cb12-167"><a href="#cb12-167" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-168"><a href="#cb12-168" aria-hidden="true" tabindex="-1"></a><span class="co"> Ein beruehmt schweres Problem: Es gibt keine Zielfunktion, entweder es</span></span>
<span id="cb12-169"><a href="#cb12-169" aria-hidden="true" tabindex="-1"></a><span class="co"> geht auf oder nicht - und beides zu entscheiden dauert lange.</span></span>
<span id="cb12-170"><a href="#cb12-170" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb12-171"><a href="#cb12-171" aria-hidden="true" tabindex="-1"></a> rng <span class="op">=</span> np.random.default_rng(<span class="dv">1</span>)</span>
<span id="cb12-172"><a href="#cb12-172" aria-hidden="true" tabindex="-1"></a> gewichte <span class="op">=</span> rng.integers(<span class="dv">10</span><span class="op">**</span><span class="dv">6</span>, <span class="dv">2</span> <span class="op">*</span> <span class="dv">10</span><span class="op">**</span><span class="dv">6</span>, <span class="dv">46</span>)</span>
<span id="cb12-173"><a href="#cb12-173" aria-hidden="true" tabindex="-1"></a> x <span class="op">=</span> [modell.NewBoolVar(<span class="ss">f&quot;x_</span><span class="sc">{</span>i<span class="sc">}</span><span class="ss">&quot;</span>) <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(<span class="bu">len</span>(gewichte))]</span>
<span id="cb12-174"><a href="#cb12-174" aria-hidden="true" tabindex="-1"></a> modell.Add(<span class="bu">sum</span>(<span class="bu">int</span>(gewichte[i]) <span class="op">*</span> x[i] <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(<span class="bu">len</span>(gewichte)))</span>
<span id="cb12-175"><a href="#cb12-175" aria-hidden="true" tabindex="-1"></a> <span class="op">==</span> <span class="bu">int</span>(gewichte.<span class="bu">sum</span>() <span class="op">//</span> <span class="dv">2</span>))</span>
<span id="cb12-176"><a href="#cb12-176" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-177"><a href="#cb12-177" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-178"><a href="#cb12-178" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> zeige(titel: <span class="bu">str</span>, a: Auswertung) <span class="op">-&gt;</span> <span class="va">None</span>:</span>
<span id="cb12-179"><a href="#cb12-179" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="ch">\n</span><span class="sc">{</span>titel<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb12-180"><a href="#cb12-180" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Status </span><span class="sc">{</span>a<span class="sc">.</span>status<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb12-181"><a href="#cb12-181" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Loesung vorhanden </span><span class="sc">{</span><span class="st">&#39;ja&#39;</span> <span class="cf">if</span> a<span class="sc">.</span>hat_loesung <span class="cf">else</span> <span class="st">&#39;nein&#39;</span><span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb12-182"><a href="#cb12-182" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> a.zielwert <span class="kw">is</span> <span class="kw">not</span> <span class="va">None</span>:</span>
<span id="cb12-183"><a href="#cb12-183" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Zielwert / Schranke </span><span class="sc">{</span>a<span class="sc">.</span>zielwert<span class="sc">:,.0f}</span><span class="ss"> / </span><span class="sc">{</span>a<span class="sc">.</span>schranke<span class="sc">:,.0f}</span><span class="ss">&quot;</span></span>
<span id="cb12-184"><a href="#cb12-184" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot; (Gap </span><span class="sc">{</span>a<span class="sc">.</span>gap <span class="op">*</span> <span class="dv">100</span><span class="sc">:.3f}</span><span class="ss"> %)&quot;</span>)</span>
<span id="cb12-185"><a href="#cb12-185" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; -&gt; </span><span class="sc">{</span>a<span class="sc">.</span>empfehlung<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb12-186"><a href="#cb12-186" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-187"><a href="#cb12-187" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-188"><a href="#cb12-188" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> <span class="va">__name__</span> <span class="op">==</span> <span class="st">&quot;__main__&quot;</span>:</span>
<span id="cb12-189"><a href="#cb12-189" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb12-190"><a href="#cb12-190" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; DIE FUENF ANTWORTEN VON CP-SAT&quot;</span>)</span>
<span id="cb12-191"><a href="#cb12-191" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb12-192"><a href="#cb12-192" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-193"><a href="#cb12-193" aria-hidden="true" tabindex="-1"></a> zeige(<span class="st">&quot;[1] Dienstplan, hoechstens 2 Dienste je Person&quot;</span>,</span>
<span id="cb12-194"><a href="#cb12-194" aria-hidden="true" tabindex="-1"></a> loese(dienstplan_loesbar, <span class="fl">10.0</span>, mit_zielfunktion<span class="op">=</span><span class="va">False</span>))</span>
<span id="cb12-195"><a href="#cb12-195" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-196"><a href="#cb12-196" aria-hidden="true" tabindex="-1"></a> zeige(<span class="st">&quot;[2] Lastverteilung 120 Auftraege / 11 Maschinen, 0,5 s Limit&quot;</span>,</span>
<span id="cb12-197"><a href="#cb12-197" aria-hidden="true" tabindex="-1"></a> loese(lastverteilung, <span class="fl">0.5</span>, mit_zielfunktion<span class="op">=</span><span class="va">True</span>))</span>
<span id="cb12-198"><a href="#cb12-198" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-199"><a href="#cb12-199" aria-hidden="true" tabindex="-1"></a> zeige(<span class="st">&quot;[3] Derselbe Fall mit 10 s Limit&quot;</span>,</span>
<span id="cb12-200"><a href="#cb12-200" aria-hidden="true" tabindex="-1"></a> loese(lastverteilung, <span class="fl">10.0</span>, mit_zielfunktion<span class="op">=</span><span class="va">True</span>))</span>
<span id="cb12-201"><a href="#cb12-201" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-202"><a href="#cb12-202" aria-hidden="true" tabindex="-1"></a> zeige(<span class="st">&quot;[4] Dienstplan, hoechstens 1 Dienst je Person&quot;</span>,</span>
<span id="cb12-203"><a href="#cb12-203" aria-hidden="true" tabindex="-1"></a> loese(dienstplan_unloesbar, <span class="fl">10.0</span>, mit_zielfunktion<span class="op">=</span><span class="va">False</span>))</span>
<span id="cb12-204"><a href="#cb12-204" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-205"><a href="#cb12-205" aria-hidden="true" tabindex="-1"></a> zeige(<span class="st">&quot;[5] Variable mit leerem Wertebereich&quot;</span>,</span>
<span id="cb12-206"><a href="#cb12-206" aria-hidden="true" tabindex="-1"></a> loese(modell_fehlerhaft, <span class="fl">10.0</span>, mit_zielfunktion<span class="op">=</span><span class="va">False</span>))</span>
<span id="cb12-207"><a href="#cb12-207" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-208"><a href="#cb12-208" aria-hidden="true" tabindex="-1"></a> zeige(<span class="st">&quot;[6] Zahlpartition mit 46 grossen Zahlen, 2 s Limit&quot;</span>,</span>
<span id="cb12-209"><a href="#cb12-209" aria-hidden="true" tabindex="-1"></a> loese(zahlpartition, <span class="fl">2.0</span>, mit_zielfunktion<span class="op">=</span><span class="va">False</span>))</span>
<span id="cb12-210"><a href="#cb12-210" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-211"><a href="#cb12-211" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">&quot;</span> <span class="op">+</span> <span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb12-212"><a href="#cb12-212" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; DIE DREI, DIE MAN NICHT VERWECHSELN DARF&quot;</span>)</span>
<span id="cb12-213"><a href="#cb12-213" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb12-214"><a href="#cb12-214" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;INFEASIBLE Es gibt keine Loesung. Eine Aussage ueber Ihr MODELL.&quot;</span>)</span>
<span id="cb12-215"><a href="#cb12-215" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; Mehr Rechenzeit aendert daran nichts.&quot;</span>)</span>
<span id="cb12-216"><a href="#cb12-216" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;UNKNOWN Es wurde keine gefunden. Eine Aussage ueber die ZEIT.&quot;</span>)</span>
<span id="cb12-217"><a href="#cb12-217" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; Ob es eine gibt, ist offen.&quot;</span>)</span>
<span id="cb12-218"><a href="#cb12-218" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;MODEL_INVALID Das Modell ist gar kein gueltiges Modell. Eine&quot;</span>)</span>
<span id="cb12-219"><a href="#cb12-219" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; Aussage ueber Ihren CODE - immer ein Programmierfehler.&quot;</span>)</span>
<span id="cb12-220"><a href="#cb12-220" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>()</span>
<span id="cb12-221"><a href="#cb12-221" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Wer diese drei in einem &#39;else: return None&#39; zusammenfasst, verliert&quot;</span>)</span>
<span id="cb12-222"><a href="#cb12-222" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;genau die Information, die zur Fehlersuche noetig waere.&quot;</span>)</span>
<span id="cb12-223"><a href="#cb12-223" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span></code></pre></div>
<p><strong>Erwartete Ausgabe (Zielwerte in Lauf [2] hardwareabhängig):</strong></p>
<pre><code>==============================================================================
DIE FUENF ANTWORTEN VON CP-SAT
==============================================================================
[1] Dienstplan, hoechstens 2 Dienste je Person
Status OPTIMAL
Loesung vorhanden ja
-&gt; Ausfuehren. Besser geht es nachweislich nicht.
[2] Lastverteilung 120 Auftraege / 11 Maschinen, 0,5 s Limit
Status FEASIBLE
Loesung vorhanden ja
Zielwert / Schranke 49,744 / 49,277 (Gap 0.939 %)
-&gt; Brauchbar. Der Plan ist hoechstens 0.94 % schlechter als das theoretisch Bestmoegliche.
[3] Derselbe Fall mit 10 s Limit
Status FEASIBLE
Loesung vorhanden ja
Zielwert / Schranke 49,345 / 49,277 (Gap 0.138 %)
-&gt; Brauchbar. Der Plan ist hoechstens 0.14 % schlechter als das theoretisch Bestmoegliche.
[4] Dienstplan, hoechstens 1 Dienst je Person
Status INFEASIBLE
Loesung vorhanden nein
-&gt; KEIN Plan existiert. Das ist eine Aussage ueber Ihre harten Regeln, nicht ueber die Rechenzeit: Auch unendlich viel Zeit wuerde nichts aendern. Regeln lockern oder weich machen (Anhang C).
[5] Variable mit leerem Wertebereich
Status MODEL_INVALID
Loesung vorhanden nein
-&gt; Das MODELL ist fehlerhaft, nicht das Problem. Typisch: eine Variable mit leerem Wertebereich. model.Validate() nennt die Ursache.
[6] Zahlpartition mit 46 grossen Zahlen, 2 s Limit
Status UNKNOWN
Loesung vorhanden nein
-&gt; Nichts gefunden - aber auch nicht widerlegt. Mehr Zeit geben, das Modell vereinfachen oder mit einer Heuristik vorbelegen (AddHint).
==============================================================================
DIE DREI, DIE MAN NICHT VERWECHSELN DARF
==============================================================================
INFEASIBLE Es gibt keine Loesung. Eine Aussage ueber Ihr MODELL.
Mehr Rechenzeit aendert daran nichts.
UNKNOWN Es wurde keine gefunden. Eine Aussage ueber die ZEIT.
Ob es eine gibt, ist offen.
MODEL_INVALID Das Modell ist gar kein gueltiges Modell. Eine
Aussage ueber Ihren CODE - immer ein Programmierfehler.
Wer diese drei in einem &#39;else: return None&#39; zusammenfasst, verliert
genau die Information, die zur Fehlersuche noetig waere.
==============================================================================</code></pre>
<blockquote>
<p><strong>💻 Code-Durchgang</strong></p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr class="header">
<th>Stelle</th>
<th>Was passiert</th>
<th>Warum es zählt</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>werte_aus()</code></td>
<td>übersetzt Status in <strong>Handlungsempfehlung</strong></td>
<td>Genau diese Funktion fehlt in den meisten Projekten. Sie ist kurz — und der Unterschied zwischen einem Prototyp und einem System, das nachts ohne Aufsicht läuft.</td>
</tr>
<tr class="even">
<td><code>hat_loesung = status in (OPTIMAL, FEASIBLE)</code></td>
<td>die eigentliche Frage</td>
<td>Nicht „war der Solver erfolgreich?“, sondern „habe ich etwas in der Hand, das ich ausführen kann?“</td>
</tr>
<tr class="odd">
<td><code>BestObjectiveBound()</code></td>
<td>die bewiesene Schranke</td>
<td>Ohne sie ist <code>FEASIBLE</code> eine nutzlose Angabe. Mit ihr wird daraus „höchstens 1,27 % vom Optimum“.</td>
</tr>
<tr class="even">
<td>Läufe [2] und [3]</td>
<td>0,5 s gegen 10 s</td>
<td>Der Gap fällt von 1,27 % auf 0,14 %. Zwanzigfache Rechenzeit für gut ein Prozent — die Entscheidung, ob sich das lohnt, trifft die Anwendung, nicht der Solver.</td>
</tr>
<tr class="odd">
<td>Lauf [4] gegen Lauf [6]</td>
<td><code>INFEASIBLE</code> gegen <code>UNKNOWN</code></td>
<td>Beide liefern keine Lösung. Bei [4] ist bewiesen, dass es keine gibt (vier Personen können keine fünf Dienste mit je höchstens einem übernehmen); bei [6] weiß niemand, ob eine existiert.</td>
</tr>
</tbody>
</table>
</blockquote>
<blockquote>
<p><strong>⚠️ Typische Fehler</strong></p>
<ul>
<li><strong><code>if status == cp_model.OPTIMAL:</code> als einzige Prüfung.</strong> Verwirft <code>FEASIBLE</code>-Lösungen, die im Betrieb völlig brauchbar wären — und macht aus einem <code>INFEASIBLE</code> stillschweigend ein „hat nicht geklappt“.</li>
<li><strong>Bei <code>INFEASIBLE</code> das Zeitlimit erhöhen.</strong> Ändert nichts. Die Ursache liegt in den harten Regeln; Anhang C zeigt, wie man die widersprüchliche Teilmenge findet.</li>
<li><strong><code>ObjectiveValue()</code> ohne Statusprüfung abfragen.</strong> Bei <code>INFEASIBLE</code> oder <code>UNKNOWN</code> liefert der Aufruf einen bedeutungslosen Wert — und der wandert dann in einen Bericht.</li>
<li><strong><code>MODEL_INVALID</code> übersehen.</strong> Der Status verrät einen Fehler im eigenen Code. <code>model.Validate()</code> nennt die genaue Stelle; nutzen Sie es beim Entwickeln immer.</li>
</ul>
</blockquote>
<hr />
<h2 id="sec:cpsat-uebungsaufgaben">7.9 Übungsaufgaben</h2>
<blockquote>
<p>Lösungen: <a href="anhang-loesungen.html#sec:loesungen-cpsat">Abschnitt A.7</a>.</p>
</blockquote>
<p><strong>Aufgabe 7.1 ⭐ — Propagation nachvollziehen.</strong> <span class="math inline">x_1 \in \{1,\dots,6\}</span>, <span class="math inline">x_2 \in \{1,\dots,6\}</span>, Bedingungen: <span class="math inline">x_1 &lt; x_2</span> und <span class="math inline">x_1 + x_2 = 8</span>. Reduzieren Sie beide Wertebereiche von Hand so weit wie möglich.</p>
<p><strong>Aufgabe 7.2 ⭐ — Hart oder weich?</strong> Klassifizieren Sie und begründen Sie, welche Strafhöhe Sie ansetzen würden: (a) Gesetzliche Mindestruhezeit. (b) „Herr Bauer möchte montags frei.“ (c) „In jeder Schicht mindestens eine examinierte Kraft.“ (d) „Möglichst keine geteilten Dienste.“ (e) „Kein Dienst länger als 10 Stunden.“</p>
<p><strong>Aufgabe 7.3 ⭐⭐ — Regeln ergänzen.</strong> Erweitern Sie das Vertretungssystem um: (a) Frau Albrecht ist ab Slot 3 nicht mehr verfügbar. (b) Herr Bauer und Frau Koch sollen nicht beide am selben Tag eingesetzt werden. (c) Wer Slot 1 übernimmt, soll bevorzugt auch Slot 2 bekommen (Belohnung statt Strafe).</p>
<p><strong>Aufgabe 7.4 ⭐⭐ — Infeasibility erzeugen und diagnostizieren.</strong> Setzen Sie <code>MAX_VERTRETUNGEN = 0</code>. Was meldet das Programm? Bauen Sie anschließend eine <strong>Schlupfvariable</strong> je Slot ein („Stunde fällt aus“, Strafe 10 000) und beobachten Sie, wie der Solver statt einer Fehlermeldung einen Notfallplan liefert. Welche Stunde lässt er ausfallen und warum?</p>
<p><strong>Aufgabe 7.5 ⭐⭐ — Sudoku.</strong> Lösen Sie ein 9×9-Sudoku mit CP-SAT. Sie brauchen nur <code>NewIntVar(1, 9, ...)</code> und <code>AddAllDifferent</code> für Zeilen, Spalten und die neun 3×3-Blöcke. Wie viele Zeilen Modellcode benötigen Sie?</p>
<p><strong>Aufgabe 7.6 ⭐⭐ — Die Grenze der Parallelität finden.</strong> <code>Parallele_Suche.py</code> misst 1, 2, 4 und 8 Arbeiter. (a) Ergänzen Sie 16 und 24 (bzw. die Kernzahl Ihres Rechners). Ab wo bringt ein weiterer Arbeiter nichts mehr — und wird es irgendwann wieder schlechter? (b) Setzen Sie <code>random_seed</code> bei jedem Lauf auf einen <strong>anderen</strong> Wert, aber <code>num_workers = 1</code>. Wie viele verschiedene Pläne kommen jetzt heraus? Was folgt daraus für die Frage, welcher der beiden Parameter die Reproduzierbarkeit sichert? (c) Vergrößern Sie die Instanz auf 15 Aufträge. Ab welcher Größe erreicht der Lauf mit einem Arbeiter das Zeitlimit, während acht noch <code>OPTIMAL</code> melden?</p>
<p><strong>Aufgabe 7.7 ⭐⭐⭐ — Job-Shop erweitern.</strong> Ergänzen Sie <code>JobShop_Intervalle.py</code> um: (a) Rüstzeiten von 1 Zeiteinheit zwischen zwei Aufträgen auf derselben Maschine. (b) Fälligkeitstermine je Auftrag mit Strafkosten für Verspätung. (c) Eine zweite Fräse (Kapazität 2 statt 1) mit <code>AddCumulative</code>.</p>
<p><strong>Aufgabe 7.8 ⭐⭐⭐ — Eigener Dienstplan.</strong> Modellieren Sie einen Wochendienstplan: 7 Tage, 3 Schichten (früh/spät/nacht), 8 Mitarbeitende. Regeln: Jede Schicht 2 Personen; nach Nachtschicht mindestens 2 Tage frei; höchstens 5 Arbeitstage in Folge; jede Person mindestens 2 freie Tage pro Woche; Wünsche als weiche Ziele. Geben Sie den Plan als Wochentabelle aus.</p>
<hr />
<h2 id="sec:cpsat-denkfehler">7.10 Finde den Denkfehler</h2>
<div class="card card-denkfehler">
<blockquote>
<p><strong>🐛 Finde den Denkfehler: Die Betriebsvereinbarung, die niemanden interessiert</strong></p>
<p>Ein Klinikteam lässt seinen Schichtplan optimieren. Zwei Wünsche kommen aus dem Betrieb:</p>
<ul>
<li><strong>Betriebsvereinbarung:</strong> niemand mehr als zwei Dienste in Folge.</li>
<li><strong>Wunschfrei:</strong> die eingereichten freien Tage möglichst einhalten.</li>
</ul>
<p>Beides wird als weiche Regel modelliert. Bei der Gewichtung überlegt der Entwickler: <em>„Wunschfrei ist für die Zufriedenheit zentral, das gewichte ich mit 10. Die Serienregel ist eine Formalie, die kaum je greift — 1 genügt.“</em></p>
<p>Der Solver meldet <code>OPTIMAL</code>. Im ausgegebenen Plan stehen <strong>zwölf</strong> Drei-Tage-Serien. Die Personalvertretung lehnt den Plan ab.</p>
<p><strong>Ihre Aufgabe:</strong> (a) Der Solver hat korrekt gerechnet und die Regel steht im Modell — warum wird sie trotzdem systematisch verletzt? (b) Was besagt das Zahlenpaar <span class="math inline">(1, 10)</span>, wörtlich in Sätze übersetzt? (c) Ab welchem Gewicht verschwinden die Serien, und was kostet das an anderer Stelle? (d) Welche Frage hätte man dem Auftraggeber stellen müssen, <strong>bevor</strong> man eine Zahl einsetzt — und wann gehört eine Regel gar nicht erst in die Zielfunktion?</p>
<p><em>Auflösung: <a href="anhang-loesungen.html#sec:loesungen-cpsat">Abschnitt A.7</a>.</em></p>
</blockquote>
</div>
<p>Das folgende Programm rechnet den Plan für sechs verschiedene Gewichte durch. Es ist damit zugleich die Antwort auf (c) — und die Tabelle, die man in so einem Fall dem Auftraggeber vorlegt, statt ein Gewicht zu raten.</p>
<div class="sourceCode" id="cb14"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb14-1"><a href="#cb14-1" aria-hidden="true" tabindex="-1"></a><span class="co">#!/usr/bin/env python3</span></span>
<span id="cb14-2"><a href="#cb14-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-3"><a href="#cb14-3" aria-hidden="true" tabindex="-1"></a><span class="co"># Strafgewichte.py</span></span>
<span id="cb14-4"><a href="#cb14-4" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb14-5"><a href="#cb14-5" aria-hidden="true" tabindex="-1"></a><span class="co">Kapitel CP-SAT: Warum ein Strafgewicht keine Wichtigkeit ausdrueckt, sondern</span></span>
<span id="cb14-6"><a href="#cb14-6" aria-hidden="true" tabindex="-1"></a><span class="co">einen Wechselkurs.</span></span>
<span id="cb14-7"><a href="#cb14-7" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-8"><a href="#cb14-8" aria-hidden="true" tabindex="-1"></a><span class="co">Weiche Regeln kommen als Strafterm in die Zielfunktion. Die Frage, die dabei</span></span>
<span id="cb14-9"><a href="#cb14-9" aria-hidden="true" tabindex="-1"></a><span class="co">regelmaessig falsch beantwortet wird, lautet: &quot;Wie gross muss die Strafe sein?&quot;</span></span>
<span id="cb14-10"><a href="#cb14-10" aria-hidden="true" tabindex="-1"></a><span class="co">Die uebliche Antwort - &quot;je wichtiger, desto groesser&quot; - klingt vernuenftig und</span></span>
<span id="cb14-11"><a href="#cb14-11" aria-hidden="true" tabindex="-1"></a><span class="co">fuehrt zuverlaessig zu Plaenen, die niemand unterschreiben will.</span></span>
<span id="cb14-12"><a href="#cb14-12" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-13"><a href="#cb14-13" aria-hidden="true" tabindex="-1"></a><span class="co">Der Grund: Zwei Strafgewichte legen nicht Wichtigkeiten fest, sondern einen</span></span>
<span id="cb14-14"><a href="#cb14-14" aria-hidden="true" tabindex="-1"></a><span class="co">UMRECHNUNGSKURS. Steht die Serienregel bei 1 und der Wunschfrei-Tag bei 10,</span></span>
<span id="cb14-15"><a href="#cb14-15" aria-hidden="true" tabindex="-1"></a><span class="co">dann hat man dem Solver woertlich gesagt: &quot;Zehn Verstoesse gegen die</span></span>
<span id="cb14-16"><a href="#cb14-16" aria-hidden="true" tabindex="-1"></a><span class="co">Serienregel sind so schlimm wie ein abgelehnter Wunsch.&quot; Er nimmt das</span></span>
<span id="cb14-17"><a href="#cb14-17" aria-hidden="true" tabindex="-1"></a><span class="co">ernst - und handelt danach.</span></span>
<span id="cb14-18"><a href="#cb14-18" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-19"><a href="#cb14-19" aria-hidden="true" tabindex="-1"></a><span class="co">Szenario: Schichtplan, 6 Personen, 14 Tage, 4 Besetzungen pro Tag.</span></span>
<span id="cb14-20"><a href="#cb14-20" aria-hidden="true" tabindex="-1"></a><span class="co"> HART jeder Tag genau 4 Personen; hoechstens 11 Dienste je Person</span></span>
<span id="cb14-21"><a href="#cb14-21" aria-hidden="true" tabindex="-1"></a><span class="co"> WEICH keine drei Dienste in Folge (Betriebsvereinbarung)</span></span>
<span id="cb14-22"><a href="#cb14-22" aria-hidden="true" tabindex="-1"></a><span class="co"> WEICH Wunschfrei-Tage einhalten (26 Antraege)</span></span>
<span id="cb14-23"><a href="#cb14-23" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-24"><a href="#cb14-24" aria-hidden="true" tabindex="-1"></a><span class="co">Benoetigt: numpy, ortools</span></span>
<span id="cb14-25"><a href="#cb14-25" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb14-26"><a href="#cb14-26" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-27"><a href="#cb14-27" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> __future__ <span class="im">import</span> annotations</span>
<span id="cb14-28"><a href="#cb14-28" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-29"><a href="#cb14-29" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb14-30"><a href="#cb14-30" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> ortools.sat.python <span class="im">import</span> cp_model</span>
<span id="cb14-31"><a href="#cb14-31" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-32"><a href="#cb14-32" aria-hidden="true" tabindex="-1"></a>PERSONEN, TAGE, PRO_TAG <span class="op">=</span> <span class="dv">6</span>, <span class="dv">14</span>, <span class="dv">4</span></span>
<span id="cb14-33"><a href="#cb14-33" aria-hidden="true" tabindex="-1"></a>MAX_DIENSTE <span class="op">=</span> <span class="dv">11</span></span>
<span id="cb14-34"><a href="#cb14-34" aria-hidden="true" tabindex="-1"></a>STRAFE_WUNSCH <span class="op">=</span> <span class="dv">10</span> <span class="co"># bleibt fest - variiert wird die Serienstrafe</span></span>
<span id="cb14-35"><a href="#cb14-35" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-36"><a href="#cb14-36" aria-hidden="true" tabindex="-1"></a>RNG <span class="op">=</span> np.random.default_rng(<span class="dv">2</span>)</span>
<span id="cb14-37"><a href="#cb14-37" aria-hidden="true" tabindex="-1"></a>WUNSCHFREI <span class="op">=</span> <span class="bu">sorted</span>({(p, t) <span class="cf">for</span> p <span class="kw">in</span> <span class="bu">range</span>(PERSONEN) <span class="cf">for</span> t <span class="kw">in</span> <span class="bu">range</span>(TAGE)</span>
<span id="cb14-38"><a href="#cb14-38" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> RNG.random() <span class="op">&lt;</span> <span class="fl">0.35</span>})</span>
<span id="cb14-39"><a href="#cb14-39" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-40"><a href="#cb14-40" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-41"><a href="#cb14-41" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> plane(strafe_serie: <span class="bu">int</span>, zeitlimit: <span class="bu">float</span> <span class="op">=</span> <span class="fl">25.0</span>) <span class="op">-&gt;</span> <span class="bu">dict</span>:</span>
<span id="cb14-42"><a href="#cb14-42" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Baut den Schichtplan mit dem angegebenen Gewicht fuer die Serienregel.&quot;&quot;&quot;</span></span>
<span id="cb14-43"><a href="#cb14-43" aria-hidden="true" tabindex="-1"></a> modell <span class="op">=</span> cp_model.CpModel()</span>
<span id="cb14-44"><a href="#cb14-44" aria-hidden="true" tabindex="-1"></a> x <span class="op">=</span> {(p, t): modell.NewBoolVar(<span class="ss">f&quot;x_</span><span class="sc">{</span>p<span class="sc">}</span><span class="ss">_</span><span class="sc">{</span>t<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb14-45"><a href="#cb14-45" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> p <span class="kw">in</span> <span class="bu">range</span>(PERSONEN) <span class="cf">for</span> t <span class="kw">in</span> <span class="bu">range</span>(TAGE)}</span>
<span id="cb14-46"><a href="#cb14-46" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-47"><a href="#cb14-47" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- harte Regeln --------------------------------------------------</span></span>
<span id="cb14-48"><a href="#cb14-48" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> t <span class="kw">in</span> <span class="bu">range</span>(TAGE):</span>
<span id="cb14-49"><a href="#cb14-49" aria-hidden="true" tabindex="-1"></a> modell.Add(<span class="bu">sum</span>(x[p, t] <span class="cf">for</span> p <span class="kw">in</span> <span class="bu">range</span>(PERSONEN)) <span class="op">==</span> PRO_TAG)</span>
<span id="cb14-50"><a href="#cb14-50" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> p <span class="kw">in</span> <span class="bu">range</span>(PERSONEN):</span>
<span id="cb14-51"><a href="#cb14-51" aria-hidden="true" tabindex="-1"></a> modell.Add(<span class="bu">sum</span>(x[p, t] <span class="cf">for</span> t <span class="kw">in</span> <span class="bu">range</span>(TAGE)) <span class="op">&lt;=</span> MAX_DIENSTE)</span>
<span id="cb14-52"><a href="#cb14-52" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-53"><a href="#cb14-53" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- weiche Regel 1: keine drei Dienste in Folge --------------------</span></span>
<span id="cb14-54"><a href="#cb14-54" aria-hidden="true" tabindex="-1"></a> <span class="co"># serie[p, t] = 1 &lt;=&gt; Person p arbeitet an t, t+1 UND t+2.</span></span>
<span id="cb14-55"><a href="#cb14-55" aria-hidden="true" tabindex="-1"></a> <span class="co"># Die Ungleichung erzwingt das nur in eine Richtung; das genuegt, weil</span></span>
<span id="cb14-56"><a href="#cb14-56" aria-hidden="true" tabindex="-1"></a> <span class="co"># der Solver serie minimiert - er wuerde die Variable nie freiwillig</span></span>
<span id="cb14-57"><a href="#cb14-57" aria-hidden="true" tabindex="-1"></a> <span class="co"># auf 1 setzen.</span></span>
<span id="cb14-58"><a href="#cb14-58" aria-hidden="true" tabindex="-1"></a> strafterme <span class="op">=</span> []</span>
<span id="cb14-59"><a href="#cb14-59" aria-hidden="true" tabindex="-1"></a> serien <span class="op">=</span> []</span>
<span id="cb14-60"><a href="#cb14-60" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> p <span class="kw">in</span> <span class="bu">range</span>(PERSONEN):</span>
<span id="cb14-61"><a href="#cb14-61" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> t <span class="kw">in</span> <span class="bu">range</span>(TAGE <span class="op">-</span> <span class="dv">2</span>):</span>
<span id="cb14-62"><a href="#cb14-62" aria-hidden="true" tabindex="-1"></a> serie <span class="op">=</span> modell.NewBoolVar(<span class="ss">f&quot;serie_</span><span class="sc">{</span>p<span class="sc">}</span><span class="ss">_</span><span class="sc">{</span>t<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb14-63"><a href="#cb14-63" aria-hidden="true" tabindex="-1"></a> modell.Add(x[p, t] <span class="op">+</span> x[p, t <span class="op">+</span> <span class="dv">1</span>] <span class="op">+</span> x[p, t <span class="op">+</span> <span class="dv">2</span>] <span class="op">&lt;=</span> <span class="dv">2</span> <span class="op">+</span> serie)</span>
<span id="cb14-64"><a href="#cb14-64" aria-hidden="true" tabindex="-1"></a> serien.append(serie)</span>
<span id="cb14-65"><a href="#cb14-65" aria-hidden="true" tabindex="-1"></a> strafterme.append(serie <span class="op">*</span> strafe_serie)</span>
<span id="cb14-66"><a href="#cb14-66" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-67"><a href="#cb14-67" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- weiche Regel 2: Wunschfrei ------------------------------------</span></span>
<span id="cb14-68"><a href="#cb14-68" aria-hidden="true" tabindex="-1"></a> wunschverstoesse <span class="op">=</span> []</span>
<span id="cb14-69"><a href="#cb14-69" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> p, t <span class="kw">in</span> WUNSCHFREI:</span>
<span id="cb14-70"><a href="#cb14-70" aria-hidden="true" tabindex="-1"></a> wunschverstoesse.append(x[p, t])</span>
<span id="cb14-71"><a href="#cb14-71" aria-hidden="true" tabindex="-1"></a> strafterme.append(x[p, t] <span class="op">*</span> STRAFE_WUNSCH)</span>
<span id="cb14-72"><a href="#cb14-72" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-73"><a href="#cb14-73" aria-hidden="true" tabindex="-1"></a> modell.Minimize(<span class="bu">sum</span>(strafterme))</span>
<span id="cb14-74"><a href="#cb14-74" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-75"><a href="#cb14-75" aria-hidden="true" tabindex="-1"></a> loeser <span class="op">=</span> cp_model.CpSolver()</span>
<span id="cb14-76"><a href="#cb14-76" aria-hidden="true" tabindex="-1"></a> loeser.parameters.max_time_in_seconds <span class="op">=</span> zeitlimit</span>
<span id="cb14-77"><a href="#cb14-77" aria-hidden="true" tabindex="-1"></a> loeser.parameters.num_workers <span class="op">=</span> <span class="dv">1</span> <span class="co"># reproduzierbare Buchausgabe</span></span>
<span id="cb14-78"><a href="#cb14-78" aria-hidden="true" tabindex="-1"></a> loeser.parameters.random_seed <span class="op">=</span> <span class="dv">1</span></span>
<span id="cb14-79"><a href="#cb14-79" aria-hidden="true" tabindex="-1"></a> status <span class="op">=</span> loeser.Solve(modell)</span>
<span id="cb14-80"><a href="#cb14-80" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> status <span class="kw">not</span> <span class="kw">in</span> (cp_model.OPTIMAL, cp_model.FEASIBLE):</span>
<span id="cb14-81"><a href="#cb14-81" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">RuntimeError</span>(<span class="ss">f&quot;Kein Plan: </span><span class="sc">{</span>loeser<span class="sc">.</span>StatusName(status)<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb14-82"><a href="#cb14-82" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-83"><a href="#cb14-83" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> {</span>
<span id="cb14-84"><a href="#cb14-84" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;status&quot;</span>: loeser.StatusName(status),</span>
<span id="cb14-85"><a href="#cb14-85" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;zielwert&quot;</span>: loeser.ObjectiveValue(),</span>
<span id="cb14-86"><a href="#cb14-86" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;serien&quot;</span>: <span class="bu">sum</span>(loeser.Value(v) <span class="cf">for</span> v <span class="kw">in</span> serien),</span>
<span id="cb14-87"><a href="#cb14-87" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;wunsch_verletzt&quot;</span>: <span class="bu">sum</span>(loeser.Value(v) <span class="cf">for</span> v <span class="kw">in</span> wunschverstoesse),</span>
<span id="cb14-88"><a href="#cb14-88" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb14-89"><a href="#cb14-89" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-90"><a href="#cb14-90" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-91"><a href="#cb14-91" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> <span class="va">__name__</span> <span class="op">==</span> <span class="st">&quot;__main__&quot;</span>:</span>
<span id="cb14-92"><a href="#cb14-92" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb14-93"><a href="#cb14-93" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; STRAFGEWICHTE SIND WECHSELKURSE, KEINE WICHTIGKEITEN&quot;</span>)</span>
<span id="cb14-94"><a href="#cb14-94" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb14-95"><a href="#cb14-95" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>PERSONEN<span class="sc">}</span><span class="ss"> Personen, </span><span class="sc">{</span>TAGE<span class="sc">}</span><span class="ss"> Tage, </span><span class="sc">{</span>PRO_TAG<span class="sc">}</span><span class="ss"> Besetzungen pro Tag &quot;</span></span>
<span id="cb14-96"><a href="#cb14-96" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;= </span><span class="sc">{</span>TAGE <span class="op">*</span> PRO_TAG<span class="sc">}</span><span class="ss"> Dienste&quot;</span>)</span>
<span id="cb14-97"><a href="#cb14-97" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Das sind </span><span class="sc">{</span>TAGE <span class="op">*</span> PRO_TAG <span class="op">/</span> PERSONEN<span class="sc">:.1f}</span><span class="ss"> Dienste je Person - der Plan ist eng.&quot;</span>)</span>
<span id="cb14-98"><a href="#cb14-98" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span><span class="bu">len</span>(WUNSCHFREI)<span class="sc">}</span><span class="ss"> Wunschfrei-Antraege, Strafe je abgelehntem Wunsch: &quot;</span></span>
<span id="cb14-99"><a href="#cb14-99" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>STRAFE_WUNSCH<span class="sc">}</span><span class="ch">\n</span><span class="ss">&quot;</span>)</span>
<span id="cb14-100"><a href="#cb14-100" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-101"><a href="#cb14-101" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;Strafe je 3er-Serie&#39;</span><span class="sc">:&gt;20}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;3er-Serien&#39;</span><span class="sc">:&gt;12}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Wunsch verletzt&#39;</span><span class="sc">:&gt;17}</span><span class="ss"> &quot;</span></span>
<span id="cb14-102"><a href="#cb14-102" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;Zielwert&#39;</span><span class="sc">:&gt;10}</span><span class="ss">&quot;</span>)</span>
<span id="cb14-103"><a href="#cb14-103" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb14-104"><a href="#cb14-104" aria-hidden="true" tabindex="-1"></a> ergebnisse <span class="op">=</span> {}</span>
<span id="cb14-105"><a href="#cb14-105" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> strafe <span class="kw">in</span> (<span class="dv">1</span>, <span class="dv">2</span>, <span class="dv">5</span>, <span class="dv">10</span>, <span class="dv">30</span>, <span class="dv">100</span>):</span>
<span id="cb14-106"><a href="#cb14-106" aria-hidden="true" tabindex="-1"></a> e <span class="op">=</span> plane(strafe)</span>
<span id="cb14-107"><a href="#cb14-107" aria-hidden="true" tabindex="-1"></a> ergebnisse[strafe] <span class="op">=</span> e</span>
<span id="cb14-108"><a href="#cb14-108" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>strafe<span class="sc">:&gt;20}</span><span class="ss"> </span><span class="sc">{</span>e[<span class="st">&#39;serien&#39;</span>]<span class="sc">:&gt;12}</span><span class="ss"> </span><span class="sc">{</span>e[<span class="st">&#39;wunsch_verletzt&#39;</span>]<span class="sc">:&gt;17}</span><span class="ss"> &quot;</span></span>
<span id="cb14-109"><a href="#cb14-109" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>e[<span class="st">&#39;zielwert&#39;</span>]<span class="sc">:&gt;10.0f}</span><span class="ss">&quot;</span>)</span>
<span id="cb14-110"><a href="#cb14-110" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb14-111"><a href="#cb14-111" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-112"><a href="#cb14-112" aria-hidden="true" tabindex="-1"></a> niedrig, hoch <span class="op">=</span> ergebnisse[<span class="dv">1</span>], ergebnisse[<span class="dv">30</span>]</span>
<span id="cb14-113"><a href="#cb14-113" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="ch">\n</span><span class="ss">Bei Strafe 1 baut der Solver </span><span class="sc">{</span>niedrig[<span class="st">&#39;serien&#39;</span>]<span class="sc">}</span><span class="ss"> Drei-Tage-Serien ein.&quot;</span>)</span>
<span id="cb14-114"><a href="#cb14-114" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Die Betriebsvereinbarung steht im Modell - und wird trotzdem&quot;</span>)</span>
<span id="cb14-115"><a href="#cb14-115" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;systematisch verletzt. Das ist kein Fehler des Solvers: Bei einem&quot;</span>)</span>
<span id="cb14-116"><a href="#cb14-116" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Gewicht von 1 gegen </span><span class="sc">{</span>STRAFE_WUNSCH<span class="sc">}</span><span class="ss"> lohnt sich jede Serie, solange sie&quot;</span>)</span>
<span id="cb14-117"><a href="#cb14-117" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;auch nur einen Zehntel-Wunsch rettet.&quot;</span>)</span>
<span id="cb14-118"><a href="#cb14-118" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="ch">\n</span><span class="ss">Bei Strafe 30 sind es </span><span class="sc">{</span>hoch[<span class="st">&#39;serien&#39;</span>]<span class="sc">}</span><span class="ss"> Serien - dafuer werden&quot;</span>)</span>
<span id="cb14-119"><a href="#cb14-119" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>hoch[<span class="st">&#39;wunsch_verletzt&#39;</span>]<span class="sc">}</span><span class="ss"> statt </span><span class="sc">{</span>niedrig[<span class="st">&#39;wunsch_verletzt&#39;</span>]<span class="sc">}</span><span class="ss"> Wuensche abgelehnt.&quot;</span>)</span>
<span id="cb14-120"><a href="#cb14-120" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Beide Plaene sind optimal. Sie beantworten nur verschiedene Fragen.&quot;</span>)</span>
<span id="cb14-121"><a href="#cb14-121" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>()</span>
<span id="cb14-122"><a href="#cb14-122" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Die Frage lautet also nie &#39;wie wichtig ist mir diese Regel?&#39;, sondern:&quot;</span>)</span>
<span id="cb14-123"><a href="#cb14-123" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; &#39;Wie viele abgelehnte Wuensche bin ich bereit zu akzeptieren,&quot;</span>)</span>
<span id="cb14-124"><a href="#cb14-124" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; um eine Drei-Tage-Serie zu vermeiden?&#39;&quot;</span>)</span>
<span id="cb14-125"><a href="#cb14-125" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Wer darauf keine Zahl nennen kann, hat das Problem noch nicht&quot;</span>)</span>
<span id="cb14-126"><a href="#cb14-126" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;verstanden - und sollte diese Tabelle dem Auftraggeber vorlegen,&quot;</span>)</span>
<span id="cb14-127"><a href="#cb14-127" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;statt ein Gewicht zu raten.&quot;</span>)</span>
<span id="cb14-128"><a href="#cb14-128" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>()</span>
<span id="cb14-129"><a href="#cb14-129" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Und die wichtigste Konsequenz: Eine Regel, die NIE gebrochen werden&quot;</span>)</span>
<span id="cb14-130"><a href="#cb14-130" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;darf, gehoert nicht in die Zielfunktion, sondern unter die harten&quot;</span>)</span>
<span id="cb14-131"><a href="#cb14-131" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Nebenbedingungen. Alles, was einen Preis hat, wird irgendwann gekauft.&quot;</span>)</span>
<span id="cb14-132"><a href="#cb14-132" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span></code></pre></div>
<p><strong>Erwartete Ausgabe:</strong></p>
<pre><code>==============================================================================
STRAFGEWICHTE SIND WECHSELKURSE, KEINE WICHTIGKEITEN
==============================================================================
6 Personen, 14 Tage, 4 Besetzungen pro Tag = 56 Dienste
Das sind 9.3 Dienste je Person - der Plan ist eng.
26 Wunschfrei-Antraege, Strafe je abgelehntem Wunsch: 10
Strafe je 3er-Serie 3er-Serien Wunsch verletzt Zielwert
------------------------------------------------------------------------------
1 12 6 72
2 12 6 84
5 9 7 115
10 2 11 130
30 0 14 140
100 0 14 140
------------------------------------------------------------------------------
Bei Strafe 1 baut der Solver 12 Drei-Tage-Serien ein.
Die Betriebsvereinbarung steht im Modell - und wird trotzdem
systematisch verletzt. Das ist kein Fehler des Solvers: Bei einem
Gewicht von 1 gegen 10 lohnt sich jede Serie, solange sie
auch nur einen Zehntel-Wunsch rettet.
Bei Strafe 30 sind es 0 Serien - dafuer werden
14 statt 6 Wuensche abgelehnt.
Beide Plaene sind optimal. Sie beantworten nur verschiedene Fragen.
Die Frage lautet also nie &#39;wie wichtig ist mir diese Regel?&#39;, sondern:
&#39;Wie viele abgelehnte Wuensche bin ich bereit zu akzeptieren,
um eine Drei-Tage-Serie zu vermeiden?&#39;
Wer darauf keine Zahl nennen kann, hat das Problem noch nicht
verstanden - und sollte diese Tabelle dem Auftraggeber vorlegen,
statt ein Gewicht zu raten.
Und die wichtigste Konsequenz: Eine Regel, die NIE gebrochen werden
darf, gehoert nicht in die Zielfunktion, sondern unter die harten
Nebenbedingungen. Alles, was einen Preis hat, wird irgendwann gekauft.
==============================================================================</code></pre>
<blockquote>
<p><strong>🎯 Merksatz</strong> Strafgewichte drücken keine <strong>Wichtigkeit</strong> aus, sondern einen <strong>Wechselkurs</strong>. Wer der Serienregel 1 und dem Wunschfrei 10 gibt, hat wörtlich gesagt: „Zehn Verstöße gegen die Betriebsvereinbarung sind so schlimm wie ein abgelehnter Wunsch.“ Der Solver nimmt das ernst und handelt danach — das ist seine Aufgabe.</p>
</blockquote>
<blockquote>
<p><strong>⚠️ Typische Fehler bei weichen Regeln</strong></p>
<ul>
<li><strong>Gewichte nach Bauchgefühl setzen.</strong> Rechnen Sie stattdessen die Tabelle wie oben und lassen Sie den Auftraggeber die Zeile aussuchen. Das dauert eine Stunde und ersetzt drei Abstimmungsrunden.</li>
<li><strong>Eine unverhandelbare Regel weich modellieren.</strong> Alles, was einen Preis hat, wird irgendwann gekauft. Gesetzliche Ruhezeiten, Qualifikationsanforderungen und Betriebsvereinbarungen gehören unter die <strong>harten</strong> Nebenbedingungen — auch wenn das Modell dann <code>INFEASIBLE</code> melden kann. Dieses <code>INFEASIBLE</code> ist die ehrliche Antwort: Der Plan ist mit dem vorhandenen Personal nicht zulässig zu machen.</li>
<li><strong>Gewichte in verschiedenen Einheiten mischen.</strong> Euro neben „Unzufriedenheitspunkten“ neben Minuten ergibt eine Zielfunktion, die niemand interpretieren kann — und deren Konditionszahl obendrein leidet (<a href="fundament.html#sec:fundament-kondition">Abschnitt 2.7</a>).</li>
<li><strong>Nur ein Gewicht ausprobieren.</strong> Ein einziger Lauf sagt nichts darüber, ob Sie nahe an einem Kipppunkt stehen. Zwischen den Gewichten 5 und 10 ändert sich der Plan oben erheblich.</li>
</ul>
</blockquote>
<hr />
<h2 id="sec:cpsat-quiz">7.11 Micro-Quiz</h2>
<div class="card card-quiz">
<blockquote>
<p><strong>❓ Micro-Quiz 7: Drei Fragen zum Selbstcheck</strong></p>
<p>Genau eine Antwort ist jeweils richtig. Auflösung in <a href="anhang-loesungen.html#quiz-loesung-cpsat">Anhang A</a>.</p>
<p><strong>1. Ihr Dienstplanmodell meldet <code>INFEASIBLE</code>. Ein Kollege schlägt vor, das Zeitlimit von 10 auf 300 Sekunden zu erhöhen. Was halten Sie davon?</strong> (a) Sinnvoll — bei schweren Instanzen braucht CP-SAT einfach länger. (b) Wirkungslos: <code>INFEASIBLE</code> ist ein <strong>Beweis</strong>, dass keine zulässige Lösung existiert. Die Ursache liegt in den harten Regeln, nicht in der Rechenzeit. (c) Sinnvoll, aber nur zusammen mit mehr Arbeitern (<code>num_workers</code>).</p>
<p><strong>2. Sie modellieren „auf einer Maschine läuft immer nur ein Auftrag“. Was ist der Vorteil von <code>AddNoOverlap</code> gegenüber der MILP-Formulierung mit Big-M?</strong> (a) <code>AddNoOverlap</code> findet grundsätzlich bessere Lösungen. (b) Eine Zeile ersetzt bei <span class="math inline">k</span> Aufträgen <span class="math inline">\binom{k}{2}</span> Entweder-Oder-Konstruktionen mit je zwei Ungleichungen und einer Binärvariablen — und propagiert stärker, weil der Propagator die Struktur als Ganzes betrachtet. (c) <code>AddNoOverlap</code> funktioniert auch mit kontinuierlichen Zeiten, Big-M nicht.</p>
<p><strong>3. Zwei Läufe desselben Job-Shop-Programms liefern verschiedene Pläne, beide mit Makespan 11. Was folgt daraus für Ihre Tests?</strong> (a) Das Programm ist fehlerhaft; ein korrektes Modell hat genau eine Lösung. (b) Es gibt mehrere optimale Pläne. Tests dürfen deshalb nicht auf einen bestimmten Plan prüfen, sondern auf den Zielwert und die Einhaltung aller Regeln. (c) Der Solver arbeitet ungenau; man sollte die Toleranzen verschärfen.</p>
</blockquote>
</div>
<hr />
<h2 id="sec:cpsat-selbsttest">7.12 Selbsttest</h2>
<blockquote>
<p>Antworten: <a href="anhang-loesungen.html#selbsttest-loesung-cpsat">Anhang A</a>.</p>
</blockquote>
<ol type="1">
<li>Was macht Constraint Propagation, und warum ist sie effizienter als Ausprobieren?</li>
<li>Welchen Vorteil hat <code>AddAllDifferent(x)</code> gegenüber <span class="math inline">\binom{n}{2}</span> Einzelbedingungen?</li>
<li>Was ist eine Intervallvariable, und welche Bedingung garantiert sie automatisch?</li>
<li>Warum sollten möglichst wenige Bedingungen hart formuliert werden?</li>
<li>Warum ist die Aufschlüsselung der Strafkosten in der Praxis wichtiger als der Zielwert</li>
<li>Sie setzen <code>random_seed = 1</code>, bekommen aber trotzdem bei jedem Lauf einen anderen Plan. Woran liegt das, und was hilft?</li>
<li>Warum kann ein CP-SAT-Lauf mit acht Arbeitern <strong>mehr</strong> als achtmal schneller sein als mit einem? selbst?</li>
</ol>
<hr />
<h2 id="sec:cpsat-zusammenfassung">7.13 Zusammenfassung</h2>
<ul>
<li><p><strong>CP denkt in Wertebereichen und Regeln</strong>, nicht in Ungleichungen und Schranken.</p></li>
<li><p><strong>Propagation</strong> streicht unmögliche Werte, bevor gesucht wird; <strong>Conflict Learning</strong> verhindert, dieselbe Sackgasse zweimal zu betreten.</p></li>
<li><p><strong>Globale Constraints</strong> wie , und ersetzen ganze Blöcke von Big-M-Ungleichungen und propagieren stärker.</p></li>
<li><p><strong>Intervallvariablen</strong> sind das Werkzeug für Zeitplanung; <code>AddNoOverlap</code> erledigt in einer Zeile, was in MILP dutzende Disjunktionen kostet.</p></li>
<li><p><strong>Weiche Ziele mit Strafkosten</strong> machen Modelle robust gegen <code>INFEASIBLE</code> — und die <strong>Kostenzerlegung</strong> macht sie erklärbar.</p></li>
<li><p><strong>Strafgewichte sind Wechselkurse, keine Wichtigkeiten.</strong> Rechnen Sie mehrere Gewichte durch und lassen Sie den Auftraggeber eine Zeile der Tabelle wählen. Und was nie gebrochen werden darf, gehört unter die harten Nebenbedingungen: Alles, was einen Preis hat, wird irgendwann gekauft.</p></li>
<li><p><strong>Fünf Antworten, nicht zwei.</strong> <code>INFEASIBLE</code> (es gibt keine Lösung), <code>UNKNOWN</code> (es wurde keine gefunden) und <code>MODEL_INVALID</code> (Ihr Code ist fehlerhaft) bedeuten Grundverschiedenes. Wer sie in ein <code>else</code> zusammenfasst, verliert die Information zur Fehlersuche.</p></li>
<li><p><strong>Bei mehrdeutigen Optima nie auf einen bestimmten Plan testen</strong> — nur auf den Zielwert und die Einhaltung aller Regeln.</p></li>
<li><p><strong>Ein paralleler Solver hat zwei Zufallsquellen: den Seed und die Uhr.</strong> Nur die erste lässt sich setzen. Reproduzierbar wird ein Lauf erst mit <code>num_workers = 1</code> <strong>und</strong> Seed — gemessen in <a href="#sec:cpsat-parallele-suche">Abschnitt 7.7</a>: schon zwei Arbeiter liefern bei identischem Seed drei verschiedene Pläne zum selben Zielwert.</p></li>
<li><p><strong>Parallele Suche kann überlinear beschleunigen</strong>, weil CP-SAT nicht dieselbe Suche vervielfacht, sondern verschiedene Strategien nebeneinander laufen lässt, die einander ihre Schranken mitteilen. <strong>Ausblick.</strong> <a href="graphen.html#kap-graphen">Kapitel 8</a> behandelt Probleme, die sich am besten als <strong>Graph</strong> begreifen lassen: Flüsse durch Netzwerke, Zuordnungen und die Tourenplanung mit Fahrzeugen und Zeitfenstern.</p></li>
</ul>
</article>
<button type="button" class="fortschritt-knopf" data-kapitel="cpsat.html"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg> <span>Als gelesen markieren</span></button>
<nav class="prev-next"><a class="prev-next-knopf prev-next-prev" href="milp.html"><svg class="icon" aria-hidden="true"><use href="#icon-chevron-left"></use></svg><span><small>Zurück</small>Kapitel 6: Gemischt-ganzzahlige Optimierung — Diskrete Entscheidungen und Branch-and-Bound</span></a><a class="prev-next-knopf prev-next-next" href="graphen.html"><span><small>Weiter</small>Kapitel 8: Graphen, Flüsse und Touren — Min-Cost-Flow, Matching und VRP</span><svg class="icon" aria-hidden="true"><use href="#icon-chevron-right"></use></svg></a></nav>
</main>
</div>
<footer class="site-footer">
<p>© Dieter Schlüter · <a href="gesamtdokument.html">Gesamtdokument</a> ·
<a href="programme.html">Beispielprogramme</a></p>
</footer>
<script defer src="assets/plotly.min.js"></script>
<script defer src="assets/search-index.js"></script>
<script defer src="assets/programme.js"></script>
<script defer src="assets/site.js"></script>
</body>
</html>