<aclass="brand"href="index.html"><svgclass="icon"aria-hidden="true"><usehref="#icon-book"></use></svg><span>Optimierte Entscheidungsfindung mit Python</span></a>
<navclass="sidebar"id="sidebar"aria-label="Kapitelnavigation"><divclass="sidebar-inhalt"><detailsclass="sidebar-gruppe"><summary>Einstieg</summary><ul><lidata-kapitel="vorwort.html"><ahref="vorwort.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Vorwort & Lesehilfe</span></a></li><lidata-kapitel="notation.html"><ahref="notation.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Notation & Abkürzungen</span></a></li></ul></details><detailsclass="sidebar-gruppe"><summary>Teil I: Grundlagen des Operations Research</summary><ul><lidata-kapitel="einfuehrung.html"><ahref="einfuehrung.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 1: Einführung in Operations Research — Vom Ursprung zur mathematischen Entscheidungsfindung</span></a></li><lidata-kapitel="fundament.html"><ahref="fundament.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 2: Das mathematische Fundament — Vektoren, Matrizen, Konvexität</span></a></li><lidata-kapitel="oekosystem.html"><ahref="oekosystem.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 3: Das Python-Ökosystem für OR — Solver, Bindings und Modellierungsschichten</span></a></li><lidata-kapitel="modellierung.html"><ahref="modellierung.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 4: Vom Management-Wunsch zum Modell</span></a></li><lidata-kapitel="synthese-grundlagen.html"><ahref="synthese-grundlagen.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Synthese Teil I</span></a></li></ul></details><detailsclass="sidebar-gruppe"open><summary>Teil II: Die Kernverfahren der deterministischen Optimierung</summary><ul><lidata-kapitel="lp.html"><ahref="lp.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 5: Lineare Programmierung — Simplex, Dualität und Schattenpreise</span></a></li><lidata-kapitel="milp.html"><ahref="milp.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 6: Gemischt-ganzzahlige Optimierung — Diskrete Entscheidungen und Branch-and-Bound</span></a></li><lidata-kapitel="cpsat.html"class="aktiv"><ahref="cpsat.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 7: Constraint Programming mit CP-SAT — Logik, Scheduling und Zuweisung</span></a></li><lidata-kapitel="graphen.html"><ahref="graphen.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 8: Graphen, Flüsse und Touren — Min-Cost-Flow, Matching und VRP</span></a></li><lidata-kapitel="metaheuristiken.html"><ahref="metaheuristiken.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 9: Metaheuristiken — wenn der exakte Solver aussteigt</span></a></li><lidata-kapitel="dekomposition.html"><ahref="dekomposition.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 10: Spaltengenerierung — das Modell umbauen statt die Lösung raten</span></a></li><lidata-kapitel="synthese-kernverfahren.html"><ahref="synthese-kernverfahren.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Synthese Teil II</span></a></li></ul></details><detailsclass="sidebar-gruppe"><summary>Teil III: Nichtlinearität, Unsicherheit
<navclass="breadcrumb"aria-label="Breadcrumb"><ahref="index.html">Start</a>›<span>Teil II</span>›<span>Kapitel 7: Constraint Programming mit CP-SAT — Logik, Scheduling und Zuweisung</span></nav>
<navclass="prev-next"><aclass="prev-next-knopf prev-next-prev"href="milp.html"><svgclass="icon"aria-hidden="true"><usehref="#icon-chevron-left"></use></svg><span><small>Zurück</small>Kapitel 6: Gemischt-ganzzahlige Optimierung — Diskrete Entscheidungen und Branch-and-Bound</span></a><aclass="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><svgclass="icon"aria-hidden="true"><usehref="#icon-chevron-right"></use></svg></a></nav>
<article>
<h1id="kap-cpsat">Kapitel 7: Constraint Programming mit CP-SAT — Logik, Scheduling und Zuweisung</h1>
<divclass="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>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>
<ahref="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/>
<h2id="sec:cpsat-schnellstart">7.1 In 5 Minuten gelöst</h2>
<divclass="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>
<spanid="cb1-8"><ahref="#cb1-8"aria-hidden="true"tabindex="-1"></a>x <spanclass="op">=</span> {(p, d): m.NewBoolVar(<spanclass="ss">f"</span><spanclass="sc">{</span>p<spanclass="sc">}</span><spanclass="ss">_</span><spanclass="sc">{</span>d<spanclass="sc">}</span><spanclass="ss">"</span>) <spanclass="cf">for</span> p <spanclass="kw">in</span> personen <spanclass="cf">for</span> d <spanclass="kw">in</span> dienste}</span>
<spanid="cb1-9"><ahref="#cb1-9"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> d <spanclass="kw">in</span> dienste:</span>
<spanid="cb1-10"><ahref="#cb1-10"aria-hidden="true"tabindex="-1"></a> m.AddExactlyOne(x[p, d] <spanclass="cf">for</span> p <spanclass="kw">in</span> personen) <spanclass="co"># jeder Tag genau einmal besetzt</span></span>
<spanid="cb1-11"><ahref="#cb1-11"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> p <spanclass="kw">in</span> personen:</span>
<spanid="cb1-12"><ahref="#cb1-12"aria-hidden="true"tabindex="-1"></a> m.Add(<spanclass="bu">sum</span>(x[p, d] <spanclass="cf">for</span> d <spanclass="kw">in</span> dienste) <spanclass="op"><=</span><spanclass="dv">2</span>) <spanclass="co"># niemand mehr als zwei Dienste</span></span>
<spanid="cb1-13"><ahref="#cb1-13"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> p, d <spanclass="kw">in</span> nicht_verfuegbar:</span>
<spanid="cb1-18"><ahref="#cb1-18"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> d <spanclass="kw">in</span> dienste:</span>
<spanid="cb1-19"><ahref="#cb1-19"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(d, <spanclass="st">"->"</span>, <spanclass="bu">next</span>(p <spanclass="cf">for</span> p <spanclass="kw">in</span> personen <spanclass="cf">if</span> s.Value(x[p, d])))</span></code></pre></div>
<p><strong>Ausgabe:</strong></p>
<pre><code>OPTIMAL
Mo -> Dane
Di -> Dane
Mi -> Abel
Do -> Bux
Fr -> 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 <spanclass="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>
<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 <ahref="#sec:cpsat-praxisfall-dynamisches-vertretungssystem">Abschnitt 7.5</a> — und <ahref="#sec:cpsat-denkfehler">Abschnitt 7.10</a> zeigt, was passiert, wenn man ihr Gewicht falsch wählt.</p>
<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/>
<h2id="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>
<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>
<p>Aus <spanclass="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>
<spanid="cb3-6"><ahref="#cb3-6"aria-hidden="true"tabindex="-1"></a><spanclass="co">Vergleicht die Zahl der geprueften Kombinationen bei roher Aufzaehlung</span></span>
<spanid="cb3-7"><ahref="#cb3-7"aria-hidden="true"tabindex="-1"></a><spanclass="co">mit der Zahl der Verzweigungen, die CP-SAT tatsaechlich braucht.</span></span>
<spanid="cb3-16"><ahref="#cb3-16"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Zaehlt ALLE Kombinationen und prueft jede einzeln (Brute Force)."""</span></span>
<spanid="cb3-27"><ahref="#cb3-27"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Dasselbe Problem deklarativ: alle verschieden, Summe = 3n."""</span></span>
<spanid="cb3-28"><ahref="#cb3-28"aria-hidden="true"tabindex="-1"></a> modell <spanclass="op">=</span> cp_model.CpModel()</span>
<spanid="cb3-29"><ahref="#cb3-29"aria-hidden="true"tabindex="-1"></a> x <spanclass="op">=</span> [modell.NewIntVar(<spanclass="dv">1</span>, obergrenze, <spanclass="ss">f"x</span><spanclass="sc">{</span>i<spanclass="sc">}</span><spanclass="ss">"</span>) <spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(n)]</span>
<spanid="cb3-51"><ahref="#cb3-51"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" PROPAGATION: WIE VIEL ARBEIT SPART SICH DER SOLVER?"</span>)</span>
<spanid="cb3-52"><ahref="#cb3-52"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" Aufgabe: n Zahlen aus 1..G, alle verschieden, Summe = 3n"</span>)</span>
<spanid="cb3-67"><ahref="#cb3-67"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"'Brute Force' = alle Kombinationen. 'CP-SAT' = tatsaechliche Verzweigungen."</span>)</span>
<spanid="cb3-68"><ahref="#cb3-68"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Der Rest wurde durch Propagation ausgeschlossen, ohne ihn anzusehen."</span>)</span>
<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/>
<h2id="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>
<tablestyle="width:100%;">
<colgroup>
<colstyle="width: 60%"/>
<colstyle="width: 18%"/>
<colstyle="width: 21%"/>
</colgroup>
<thead>
<trclass="header">
<th>Constraint</th>
<th>Bedeutung</th>
<th>Typische Anwendung</th>
</tr>
</thead>
<tbody>
<trclass="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>
<trclass="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>
<trclass="odd">
<td><code>AddAtMostOne(bools)</code></td>
<td><strong>Höchstens eine</strong> ist wahr</td>
<td>Exklusive Ressourcennutzung</td>
</tr>
<trclass="even">
<td><code>AddBoolOr(bools)</code></td>
<td><strong>Mindestens eine</strong> ist wahr</td>
<td>Reifizierung: Regel an-/abschaltbar machen</td>
</tr>
</tbody>
</table>
<divclass="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>
<td>„Diese Ressource kann immer nur eines auf einmal — was sich zeitlich überschneidet, ist verboten.“</td>
</tr>
<trclass="even">
<td><spanclass="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 <spanclass="math inline">C</span> Einheiten gleichzeitig gebraucht werden“ — Personalstärke, Stromlast, Kranstunden.</td>
</tr>
<trclass="odd">
<td>Bedingung gilt nur falls <spanclass="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 <spanclass="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>
<spanid="cb4-5"><ahref="#cb4-5"aria-hidden="true"tabindex="-1"></a><spanclass="co">Kapitel CP-SAT: Dynamisches Vertretungs- und Einsatzplanungssystem mit CP-SAT.</span></span>
<spanid="cb4-8"><ahref="#cb4-8"aria-hidden="true"tabindex="-1"></a><spanclass="co"> * Solver-Status wird ueber SolverStatus aus or_kern.py ausgewertet - dieselbe</span></span>
<spanid="cb4-9"><ahref="#cb4-9"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Fallunterscheidung wie bei jedem anderen Solver im Buch</span></span>
<spanid="cb4-10"><ahref="#cb4-10"aria-hidden="true"tabindex="-1"></a><spanclass="co"> * Strafkosten-Zerlegung wird ausgewiesen (Erklaerbarkeit, Kapitel Praxisfallen)</span></span>
<spanid="cb4-11"><ahref="#cb4-11"aria-hidden="true"tabindex="-1"></a><spanclass="co"> * Fairness-Kriterium ergaenzt (Spannweite der Belastung minimieren)</span></span>
<spanid="cb4-12"><ahref="#cb4-12"aria-hidden="true"tabindex="-1"></a><spanclass="co"> * Vorabdiagnose auf offensichtliche Unloesbarkeit</span></span>
<spanid="cb4-13"><ahref="#cb4-13"aria-hidden="true"tabindex="-1"></a><spanclass="co"> * Abnahmepruefung des fertigen Plans OHNE den Solver zu fragen</span></span>
<spanid="cb4-46"><ahref="#cb4-46"aria-hidden="true"tabindex="-1"></a>STRAFE_VORBELASTUNG <spanclass="op">=</span><spanclass="dv">50</span><spanclass="co"># je Stunde Vorbelastung und Zuweisung</span></span>
<spanid="cb4-47"><ahref="#cb4-47"aria-hidden="true"tabindex="-1"></a>STRAFE_LOCH <spanclass="op">=</span><spanclass="dv">80</span><spanclass="co"># je Freistundenloch</span></span>
<spanid="cb4-48"><ahref="#cb4-48"aria-hidden="true"tabindex="-1"></a>STRAFE_UNFAIRNESS <spanclass="op">=</span><spanclass="dv">30</span><spanclass="co"># je Einheit Spannweite der Gesamtbelastung</span></span>
<spanid="cb4-52"><ahref="#cb4-52"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Vorabdiagnose: Gibt es fuer jeden Slot ueberhaupt qualifiziertes Personal?"""</span></span>
<spanid="cb4-53"><ahref="#cb4-53"aria-hidden="true"tabindex="-1"></a> ok <spanclass="op">=</span><spanclass="va">True</span></span>
<spanid="cb4-54"><ahref="#cb4-54"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> s, fach <spanclass="kw">in</span><spanclass="bu">enumerate</span>(FAECHER):</span>
<spanid="cb4-55"><ahref="#cb4-55"aria-hidden="true"tabindex="-1"></a> kandidaten <spanclass="op">=</span> [p <spanclass="cf">for</span> p <spanclass="kw">in</span> PERSONAL <spanclass="cf">if</span> fach <spanclass="kw">in</span> QUALIFIKATION[p]]</span>
<spanid="cb4-57"><ahref="#cb4-57"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" UNLOESBAR: Fuer Slot </span><spanclass="sc">{</span>SLOTS[s]<spanclass="sc">}</span><spanclass="ss"> (</span><spanclass="sc">{</span>fach<spanclass="sc">}</span><spanclass="ss">) gibt es niemanden."</span>)</span>
<spanid="cb4-58"><ahref="#cb4-58"aria-hidden="true"tabindex="-1"></a> ok <spanclass="op">=</span><spanclass="va">False</span></span>
<spanid="cb4-67"><ahref="#cb4-67"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Prueft den fertigen Plan gegen die harten Regeln - ohne den Solver.</span></span>
<spanid="cb4-69"><ahref="#cb4-69"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Der Solver kann nur pruefen, was ihm gesagt wurde. Diese Funktion prueft</span></span>
<spanid="cb4-70"><ahref="#cb4-70"aria-hidden="true"tabindex="-1"></a><spanclass="co"> gegen die ANFORDERUNG und benutzt dafuer bewusst keine Modellvariable</span></span>
<spanid="cb4-71"><ahref="#cb4-71"aria-hidden="true"tabindex="-1"></a><spanclass="co"> (siehe Kapitel Praxisfallen). Leere Liste heisst bestanden.</span></span>
<spanid="cb4-77"><ahref="#cb4-77"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> s, person <spanclass="kw">in</span> plan.items():</span>
<spanid="cb4-79"><ahref="#cb4-79"aria-hidden="true"tabindex="-1"></a> beanstandungen.append(<spanclass="ss">f"</span><spanclass="sc">{</span>person<spanclass="sc">}</span><spanclass="ss"> ist nicht fuer </span><spanclass="sc">{</span>FAECHER[s]<spanclass="sc">}</span><spanclass="ss"> qualifiziert"</span>)</span>
<spanid="cb4-80"><ahref="#cb4-80"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> person <spanclass="kw">in</span> PERSONAL:</span>
<spanid="cb4-81"><ahref="#cb4-81"aria-hidden="true"tabindex="-1"></a> anzahl <spanclass="op">=</span><spanclass="bu">sum</span>(<spanclass="dv">1</span><spanclass="cf">for</span> p <spanclass="kw">in</span> plan.values() <spanclass="cf">if</span> p <spanclass="op">==</span> person)</span>
<spanid="cb4-94"><ahref="#cb4-94"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Entscheidungsvariablen: x[person, slot] = 1 <=> Person uebernimmt Slot</span></span>
<spanid="cb4-95"><ahref="#cb4-95"aria-hidden="true"tabindex="-1"></a> x <spanclass="op">=</span> {(p, s): modell.NewBoolVar(<spanclass="ss">f"zuweisung_</span><spanclass="sc">{</span>p<spanclass="sc">}</span><spanclass="ss">_slot</span><spanclass="sc">{</span>s<spanclass="op">+</span><spanclass="dv">1</span><spanclass="sc">}</span><spanclass="ss">"</span>)</span>
<spanid="cb4-96"><ahref="#cb4-96"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> p <spanclass="kw">in</span> PERSONAL <spanclass="cf">for</span> s <spanclass="kw">in</span><spanclass="bu">range</span>(<spanclass="bu">len</span>(SLOTS))}</span>
<spanid="cb4-99"><ahref="#cb4-99"aria-hidden="true"tabindex="-1"></a><spanclass="co"># H1: Jede Stunde genau einmal besetzen</span></span>
<spanid="cb4-100"><ahref="#cb4-100"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> s <spanclass="kw">in</span><spanclass="bu">range</span>(<spanclass="bu">len</span>(SLOTS)):</span>
<spanid="cb4-101"><ahref="#cb4-101"aria-hidden="true"tabindex="-1"></a> modell.AddExactlyOne(x[p, s] <spanclass="cf">for</span> p <spanclass="kw">in</span> PERSONAL)</span>
<spanid="cb4-104"><ahref="#cb4-104"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> s, fach <spanclass="kw">in</span><spanclass="bu">enumerate</span>(FAECHER):</span>
<spanid="cb4-105"><ahref="#cb4-105"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> p <spanclass="kw">in</span> PERSONAL:</span>
<spanid="cb4-106"><ahref="#cb4-106"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> fach <spanclass="kw">not</span><spanclass="kw">in</span> QUALIFIKATION[p]:</span>
<spanid="cb4-113"><ahref="#cb4-113"aria-hidden="true"tabindex="-1"></a><spanclass="co"># (H4 "nicht an zwei Orten gleichzeitig" ist durch H1 bereits erfuellt:</span></span>
<spanid="cb4-114"><ahref="#cb4-114"aria-hidden="true"tabindex="-1"></a><spanclass="co"># jeder Slot hat genau eine Person, und eine Person kann pro Slot nur</span></span>
<spanid="cb4-115"><ahref="#cb4-115"aria-hidden="true"tabindex="-1"></a><spanclass="co"># eine Variable auf 1 setzen.)</span></span>
<spanid="cb4-129"><ahref="#cb4-129"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> p <spanclass="kw">in</span> PERSONAL:</span>
<spanid="cb4-130"><ahref="#cb4-130"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> s <spanclass="kw">in</span><spanclass="bu">range</span>(<spanclass="bu">len</span>(SLOTS) <spanclass="op">-</span><spanclass="dv">2</span>):</span>
<spanid="cb4-132"><ahref="#cb4-132"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Reifizierung: loch == 1 <=> (x_s AND NOT x_{s+1} AND x_{s+2})</span></span>
<spanid="cb4-133"><ahref="#cb4-133"aria-hidden="true"tabindex="-1"></a> modell.AddBoolAnd([x[p, s], x[p, s <spanclass="op">+</span><spanclass="dv">1</span>].Not(), x[p, s <spanclass="op">+</span><spanclass="dv">2</span>]]).OnlyEnforceIf(loch)</span>
<spanid="cb4-134"><ahref="#cb4-134"aria-hidden="true"tabindex="-1"></a> modell.AddBoolOr([x[p, s].Not(), x[p, s <spanclass="op">+</span><spanclass="dv">1</span>], x[p, s <spanclass="op">+</span><spanclass="dv">2</span>].Not()]) <spanclass="op">\</span></span>
<spanid="cb4-158"><ahref="#cb4-158"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Reproduzierbarkeit: ein Arbeiter, fester Startwert. Ohne das kann CP-SAT</span></span>
<spanid="cb4-159"><ahref="#cb4-159"aria-hidden="true"tabindex="-1"></a><spanclass="co"># bei mehreren gleich guten Plaenen von Lauf zu Lauf einen anderen liefern</span></span>
<spanid="cb4-160"><ahref="#cb4-160"aria-hidden="true"tabindex="-1"></a><spanclass="co"># (siehe JobShop_Intervalle.py im naechsten Abschnitt).</span></span>
<spanid="cb4-165"><ahref="#cb4-165"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Der Rueckgabewert wird in die gemeinsame Sprache uebersetzt. Ab hier</span></span>
<spanid="cb4-166"><ahref="#cb4-166"aria-hidden="true"tabindex="-1"></a><spanclass="co"># sieht die Auswertung genauso aus wie bei HiGHS, GLOP oder CVXPY.</span></span>
<spanid="cb4-167"><ahref="#cb4-167"aria-hidden="true"tabindex="-1"></a> status <spanclass="op">=</span> status_von_cpsat(rohstatus)</span>
<spanid="cb4-179"><ahref="#cb4-179"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"Kein zulaessiger Plan moeglich (Status: </span><spanclass="sc">{</span>status<spanclass="sc">.</span>value<spanclass="sc">}</span><spanclass="ss">)."</span>)</span>
<spanid="cb4-180"><ahref="#cb4-180"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Die harten Regeln widersprechen sich. Naechster Schritt: eine Regel"</span>)</span>
<spanid="cb4-181"><ahref="#cb4-181"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"weich machen (Kapitel Praxisfallen) oder den Konflikt eingrenzen"</span>)</span>
<spanid="cb4-185"><ahref="#cb4-185"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"Solver lieferte kein Ergebnis (Status: </span><spanclass="sc">{</span>status<spanclass="sc">.</span>value<spanclass="sc">}</span><spanclass="ss">)."</span>)</span>
<spanid="cb4-186"><ahref="#cb4-186"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Zeitlimit erhoehen oder das Modell vereinfachen."</span>)</span>
<spanid="cb4-189"><ahref="#cb4-189"aria-hidden="true"tabindex="-1"></a> guete <spanclass="op">=</span> (<spanclass="st">"beweisbar optimal"</span><spanclass="cf">if</span> status <spanclass="kw">is</span> SolverStatus.OPTIMAL</span>
<spanid="cb4-190"><ahref="#cb4-190"aria-hidden="true"tabindex="-1"></a><spanclass="cf">else</span><spanclass="st">"zulaessig, aber nicht bewiesen - Gap beachten"</span>)</span>
<spanid="cb4-194"><ahref="#cb4-194"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- Plan ausgeben ----------------------------------------------------</span></span>
<spanid="cb4-195"><ahref="#cb4-195"aria-hidden="true"tabindex="-1"></a> zuweisung <spanclass="op">=</span> {s: <spanclass="bu">next</span>(p <spanclass="cf">for</span> p <spanclass="kw">in</span> PERSONAL <spanclass="cf">if</span> loeser.Value(x[p, s]) <spanclass="op">==</span><spanclass="dv">1</span>)</span>
<spanid="cb4-196"><ahref="#cb4-196"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> s <spanclass="kw">in</span><spanclass="bu">range</span>(<spanclass="bu">len</span>(SLOTS))}</span>
<spanid="cb4-197"><ahref="#cb4-197"aria-hidden="true"tabindex="-1"></a> plan <spanclass="op">=</span> [{</span>
<spanid="cb4-203"><ahref="#cb4-203"aria-hidden="true"tabindex="-1"></a> } <spanclass="cf">for</span> s <spanclass="kw">in</span><spanclass="bu">range</span>(<spanclass="bu">len</span>(SLOTS))]</span>
<spanid="cb4-206"><ahref="#cb4-206"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- Abnahmepruefung gegen die Anforderung ----------------------------</span></span>
<spanid="cb4-217"><ahref="#cb4-217"aria-hidden="true"tabindex="-1"></a><spanclass="op">*</span><spanclass="bu">sum</span>(loeser.Value(x[p, s]) <spanclass="cf">for</span> s <spanclass="kw">in</span><spanclass="bu">range</span>(<spanclass="bu">len</span>(SLOTS)))</span>
<spanid="cb4-218"><ahref="#cb4-218"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> p <spanclass="kw">in</span> PERSONAL)</span>
<spanid="cb4-219"><ahref="#cb4-219"aria-hidden="true"tabindex="-1"></a> kosten_loecher <spanclass="op">=</span><spanclass="bu">sum</span>(loeser.Value(v) <spanclass="cf">for</span> v <spanclass="kw">in</span> loch_variablen) <spanclass="op">*</span> STRAFE_LOCH</span>
<spanid="cb4-224"><ahref="#cb4-224"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" Freistundenloecher: </span><spanclass="sc">{</span>kosten_loecher<spanclass="sc">:5.0f}</span><spanclass="ss"> Punkte "</span></span>
<spanid="cb4-225"><ahref="#cb4-225"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"(</span><spanclass="sc">{</span><spanclass="bu">sum</span>(loeser.Value(v) <spanclass="cf">for</span> v <spanclass="kw">in</span> loch_variablen)<spanclass="sc">}</span><spanclass="ss"> Loch/Loecher)"</span>)</span>
<spanid="cb4-229"><ahref="#cb4-229"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"</span><spanclass="ch">\n</span><spanclass="st">--- Auslastung nach Optimierung ---"</span>)</span>
<spanid="cb4-230"><ahref="#cb4-230"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> p <spanclass="kw">in</span> PERSONAL:</span>
<spanid="cb4-231"><ahref="#cb4-231"aria-hidden="true"tabindex="-1"></a> heute <spanclass="op">=</span><spanclass="bu">sum</span>(loeser.Value(x[p, s]) <spanclass="cf">for</span> s <spanclass="kw">in</span><spanclass="bu">range</span>(<spanclass="bu">len</span>(SLOTS)))</span>
<p>koppeln die Hilfsvariable <code>loch</code><strong>in beide Richtungen</strong> an das Muster „arbeitet–frei–arbeitet“. 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 <spanclass="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 (<ahref="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>
<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 <ahref="milp.html#kap-milp">Kapitel 6</a> und bei GLOP in <ahref="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 — <ahref="praxisfallen.html#sec:praxisfallen-solverwechsel">Abschnitt 22.7</a> führt genau diesen Wechsel vor.</p>
<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>
<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>
<spanid="cb8-5"><ahref="#cb8-5"aria-hidden="true"tabindex="-1"></a><spanclass="co">Kapitel CP-SAT: Job-Shop-Scheduling mit Intervallvariablen.</span></span>
<spanid="cb8-6"><ahref="#cb8-6"aria-hidden="true"tabindex="-1"></a><spanclass="co">Zeigt NewIntervalVar, AddNoOverlap und die Minimierung des Makespan.</span></span>
<spanid="cb8-13"><ahref="#cb8-13"aria-hidden="true"tabindex="-1"></a><spanclass="co"># (Maschine, Dauer) je Arbeitsgang, in der Reihenfolge des Auftrags</span></span>
<spanid="cb8-26"><ahref="#cb8-26"aria-hidden="true"tabindex="-1"></a> modell <spanclass="op">=</span> cp_model.CpModel()</span>
<spanid="cb8-27"><ahref="#cb8-27"aria-hidden="true"tabindex="-1"></a> Gang <spanclass="op">=</span> collections.namedtuple(<spanclass="st">"Gang"</span>, <spanclass="st">"start ende intervall"</span>)</span>
<spanid="cb8-32"><ahref="#cb8-32"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> a, auftrag <spanclass="kw">in</span><spanclass="bu">enumerate</span>(AUFTRAEGE):</span>
<spanid="cb8-35"><ahref="#cb8-35"aria-hidden="true"tabindex="-1"></a> ende <spanclass="op">=</span> modell.NewIntVar(<spanclass="dv">0</span>, horizont, <spanclass="ss">f"ende_</span><spanclass="sc">{</span>a<spanclass="sc">}</span><spanclass="ss">_</span><spanclass="sc">{</span>g<spanclass="sc">}</span><spanclass="ss">"</span>)</span>
<spanid="cb8-36"><ahref="#cb8-36"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Ein Intervall koppelt start + dauer == ende automatisch</span></span>
<spanid="cb8-41"><ahref="#cb8-41"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- H1: eine Maschine bearbeitet nur einen Gang gleichzeitig --------</span></span>
<spanid="cb8-45"><ahref="#cb8-45"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- H2: Reihenfolge innerhalb eines Auftrags einhalten -------------</span></span>
<spanid="cb8-46"><ahref="#cb8-46"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> a, auftrag <spanclass="kw">in</span><spanclass="bu">enumerate</span>(AUFTRAEGE):</span>
<spanid="cb8-47"><ahref="#cb8-47"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> g <spanclass="kw">in</span><spanclass="bu">range</span>(<spanclass="bu">len</span>(auftrag) <spanclass="op">-</span><spanclass="dv">1</span>):</span>
<spanid="cb8-48"><ahref="#cb8-48"aria-hidden="true"tabindex="-1"></a> modell.Add(plaene[a, g <spanclass="op">+</span><spanclass="dv">1</span>].start <spanclass="op">>=</span> plaene[a, g].ende)</span>
<spanid="cb8-53"><ahref="#cb8-53"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> a, auftrag <spanclass="kw">in</span><spanclass="bu">enumerate</span>(AUFTRAEGE)])</span>
<spanid="cb8-58"><ahref="#cb8-58"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Es gibt mehrere Plaene mit demselben kuerzesten Makespan. Welchen CP-SAT</span></span>
<spanid="cb8-59"><ahref="#cb8-59"aria-hidden="true"tabindex="-1"></a><spanclass="co"># findet, haengt sonst davon ab, welcher seiner parallelen Suchstraenge</span></span>
<spanid="cb8-60"><ahref="#cb8-60"aria-hidden="true"tabindex="-1"></a><spanclass="co"># zuerst fertig wird - dasselbe Programm liefert dann von Lauf zu Lauf</span></span>
<spanid="cb8-61"><ahref="#cb8-61"aria-hidden="true"tabindex="-1"></a><spanclass="co"># verschiedene (gleich gute) Plaene. Fuer ein reproduzierbares Buchbeispiel</span></span>
<spanid="cb8-62"><ahref="#cb8-62"aria-hidden="true"tabindex="-1"></a><spanclass="co"># fixieren wir beides. Im Produktivbetrieb laesst man die Standardwerte</span></span>
<spanid="cb8-63"><ahref="#cb8-63"aria-hidden="true"tabindex="-1"></a><spanclass="co"># stehen: Mehrere Arbeiter sind dort deutlich schneller.</span></span>
<spanid="cb8-68"><ahref="#cb8-68"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> status <spanclass="kw">not</span><spanclass="kw">in</span> (cp_model.OPTIMAL, cp_model.FEASIBLE):</span>
<spanid="cb8-69"><ahref="#cb8-69"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"Kein Plan gefunden: </span><spanclass="sc">{</span>loeser<spanclass="sc">.</span>StatusName(status)<spanclass="sc">}</span><spanclass="ss">"</span>)</span>
<spanid="cb8-73"><ahref="#cb8-73"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" JOB-SHOP-SCHEDULING MIT INTERVALLVARIABLEN"</span>)</span>
<spanid="cb8-78"><ahref="#cb8-78"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- Gantt-Diagramm als Text ------------------------------------------</span></span>
<spanid="cb8-80"><ahref="#cb8-80"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"</span><spanclass="sc">{</span><spanclass="st">'Maschine'</span><spanclass="sc">:<14}</span><spanclass="ss">|"</span><spanclass="op">+</span><spanclass="st">""</span>.join(<spanclass="ss">f"</span><spanclass="sc">{</span>t<spanclass="sc">:<3}</span><spanclass="ss">"</span><spanclass="cf">for</span> t <spanclass="kw">in</span><spanclass="bu">range</span>(dauer_gesamt)))</span>
<spanid="cb8-84"><ahref="#cb8-84"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> a, auftrag <spanclass="kw">in</span><spanclass="bu">enumerate</span>(AUFTRAEGE):</span>
<spanid="cb8-86"><ahref="#cb8-86"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> m <spanclass="op">==</span> maschine:</span>
<spanid="cb8-87"><ahref="#cb8-87"aria-hidden="true"tabindex="-1"></a> beginn <spanclass="op">=</span> loeser.Value(plaene[a, g].start)</span>
<spanid="cb8-88"><ahref="#cb8-88"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> t <spanclass="kw">in</span><spanclass="bu">range</span>(beginn, beginn <spanclass="op">+</span> dauer):</span>
<spanid="cb8-93"><ahref="#cb8-93"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> a, auftrag <spanclass="kw">in</span><spanclass="bu">enumerate</span>(AUFTRAEGE):</span>
<spanid="cb8-94"><ahref="#cb8-94"aria-hidden="true"tabindex="-1"></a> teile <spanclass="op">=</span> []</span>
<spanid="cb8-100"><ahref="#cb8-100"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- Untere Schranke zum Vergleich ------------------------------------</span></span>
<spanid="cb8-101"><ahref="#cb8-101"aria-hidden="true"tabindex="-1"></a> laengster_auftrag <spanclass="op">=</span><spanclass="bu">max</span>(<spanclass="bu">sum</span>(d <spanclass="cf">for</span> _, d <spanclass="kw">in</span> a) <spanclass="cf">for</span> a <spanclass="kw">in</span> AUFTRAEGE)</span>
<spanid="cb8-103"><ahref="#cb8-103"aria-hidden="true"tabindex="-1"></a><spanclass="bu">sum</span>(d <spanclass="cf">for</span> auftrag <spanclass="kw">in</span> AUFTRAEGE <spanclass="cf">for</span> m, d <spanclass="kw">in</span> auftrag <spanclass="cf">if</span> m <spanclass="op">==</span> maschine)</span>
<p><strong>Lesen Sie das Gantt-Diagramm.</strong> Auftrag 1 kommt zuerst an die Fräse (0–2), 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 <ahref="#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 <spanclass="math inline">k</span> Gänge auf einer Maschine <spanclass="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>
<h2id="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 <ahref="#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>
<spanid="cb10-5"><ahref="#cb10-5"aria-hidden="true"tabindex="-1"></a><spanclass="co">Kapitel CP-SAT: Was mehrere Arbeiter wirklich bringen - und was sie kosten.</span></span>
<spanid="cb10-7"><ahref="#cb10-7"aria-hidden="true"tabindex="-1"></a><spanclass="co">Das Buch setzt an mehreren Stellen 'num_workers = 1', damit die abgedruckte</span></span>
<spanid="cb10-8"><ahref="#cb10-8"aria-hidden="true"tabindex="-1"></a><spanclass="co">Ausgabe reproduzierbar ist. Behauptet wird dabei zweierlei: dass parallele</span></span>
<spanid="cb10-9"><ahref="#cb10-9"aria-hidden="true"tabindex="-1"></a><spanclass="co">Suche schneller ist, und dass sie von Lauf zu Lauf verschiedene, gleich gute</span></span>
<spanid="cb10-10"><ahref="#cb10-10"aria-hidden="true"tabindex="-1"></a><spanclass="co">Loesungen findet. Beides wird hier gemessen statt geglaubt.</span></span>
<spanid="cb10-12"><ahref="#cb10-12"aria-hidden="true"tabindex="-1"></a><spanclass="co">Gerechnet wird auf einem Job-Shop wie in JobShop_Intervalle.py, nur gross</span></span>
<spanid="cb10-13"><ahref="#cb10-13"aria-hidden="true"tabindex="-1"></a><spanclass="co">genug, dass die Suche wirklich arbeitet: 12 Auftraege auf 10 Maschinen,</span></span>
<spanid="cb10-17"><ahref="#cb10-17"aria-hidden="true"tabindex="-1"></a><spanclass="co"> 1. Dieselbe Aufgabe mit 1, 2, 4 und 8 Arbeitern - Laufzeit und Ergebnis.</span></span>
<spanid="cb10-18"><ahref="#cb10-18"aria-hidden="true"tabindex="-1"></a><spanclass="co"> 2. Dieselbe Konfiguration mehrfach, bei FESTEM random_seed - wie oft kommt</span></span>
<spanid="cb10-19"><ahref="#cb10-19"aria-hidden="true"tabindex="-1"></a><spanclass="co"> derselbe Plan heraus?</span></span>
<spanid="cb10-21"><ahref="#cb10-21"aria-hidden="true"tabindex="-1"></a><spanclass="co">Die zweite ist die wichtigere: Sie entscheidet, wie man Tests schreibt.</span></span>
<spanid="cb10-23"><ahref="#cb10-23"aria-hidden="true"tabindex="-1"></a><spanclass="co">Achtung, Laufzeiten sind hardwareabhaengig. Die ZAHL der verschiedenen</span></span>
<spanid="cb10-24"><ahref="#cb10-24"aria-hidden="true"tabindex="-1"></a><spanclass="co">Plaene ist es auch - und genau das ist die Aussage.</span></span>
<spanid="cb10-40"><ahref="#cb10-40"aria-hidden="true"tabindex="-1"></a>SOLVER_SEED <spanclass="op">=</span><spanclass="dv">1</span><spanclass="co"># bleibt ueber ALLE Laeufe gleich - das ist der Punkt</span></span>
<spanid="cb10-48"><ahref="#cb10-48"aria-hidden="true"tabindex="-1"></a><spanclass="co"> in einer eigenen zufaelligen Reihenfolge."""</span></span>
<spanid="cb10-55"><ahref="#cb10-55"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Minimiert den Makespan. Gibt Status, Zielwert, Laufzeit und den Plan</span></span>
<spanid="cb10-56"><ahref="#cb10-56"aria-hidden="true"tabindex="-1"></a><spanclass="co"> zurueck - den Plan als Tupel aller Startzeiten, damit sich zwei Laeufe</span></span>
<spanid="cb10-59"><ahref="#cb10-59"aria-hidden="true"tabindex="-1"></a> modell <spanclass="op">=</span> cp_model.CpModel()</span>
<spanid="cb10-60"><ahref="#cb10-60"aria-hidden="true"tabindex="-1"></a> Gang <spanclass="op">=</span> collections.namedtuple(<spanclass="st">"Gang"</span>, <spanclass="st">"start ende intervall"</span>)</span>
<spanid="cb10-64"><ahref="#cb10-64"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> a, auftrag <spanclass="kw">in</span><spanclass="bu">enumerate</span>(auftraege):</span>
<spanid="cb10-74"><ahref="#cb10-74"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> a, auftrag <spanclass="kw">in</span><spanclass="bu">enumerate</span>(auftraege):</span>
<spanid="cb10-75"><ahref="#cb10-75"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> g <spanclass="kw">in</span><spanclass="bu">range</span>(<spanclass="bu">len</span>(auftrag) <spanclass="op">-</span><spanclass="dv">1</span>):</span>
<spanid="cb10-76"><ahref="#cb10-76"aria-hidden="true"tabindex="-1"></a> modell.Add(plaene[a, g <spanclass="op">+</span><spanclass="dv">1</span>].start <spanclass="op">>=</span> plaene[a, g].ende)</span>
<spanid="cb10-81"><ahref="#cb10-81"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> a, auftrag <spanclass="kw">in</span><spanclass="bu">enumerate</span>(auftraege)])</span>
<spanid="cb10-93"><ahref="#cb10-93"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> status <spanclass="kw">not</span><spanclass="kw">in</span> (cp_model.OPTIMAL, cp_model.FEASIBLE):</span>
<spanid="cb10-95"><ahref="#cb10-95"aria-hidden="true"tabindex="-1"></a> plan <spanclass="op">=</span><spanclass="bu">tuple</span>(loeser.Value(plaene[a, g].start)</span>
<spanid="cb10-96"><ahref="#cb10-96"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> a, auftrag <spanclass="kw">in</span><spanclass="bu">enumerate</span>(auftraege)</span>
<spanid="cb10-97"><ahref="#cb10-97"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> g <spanclass="kw">in</span><spanclass="bu">range</span>(<spanclass="bu">len</span>(auftrag)))</span>
<spanid="cb10-106"><ahref="#cb10-106"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" WAS MEHRERE ARBEITER WIRKLICH BRINGEN"</span>)</span>
<spanid="cb10-110"><ahref="#cb10-110"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"random_seed = </span><spanclass="sc">{</span>SOLVER_SEED<spanclass="sc">}</span><spanclass="ss"> in JEDEM Lauf, "</span></span>
<spanid="cb10-111"><ahref="#cb10-111"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"</span><spanclass="sc">{</span>WIEDERHOLUNGEN<spanclass="sc">}</span><spanclass="ss"> Wiederholungen je Arbeiterzahl.</span><spanclass="ch">\n</span><spanclass="ss">"</span>)</span>
<spanid="cb10-113"><ahref="#cb10-113"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Jede Konfiguration mehrfach - ein einzelner Zeitwert sagt nichts.</span></span>
<spanid="cb10-120"><ahref="#cb10-120"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" (1) Laufzeit: was die Arbeiter bringen"</span>)</span>
<spanid="cb10-132"><ahref="#cb10-132"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"</span><spanclass="sc">{</span><spanclass="st">'/'</span><spanclass="sc">.</span>join(<spanclass="bu">str</span>(z) <spanclass="cf">for</span> z <spanclass="kw">in</span><spanclass="bu">sorted</span>(ziele))<spanclass="sc">:>9}</span><spanclass="ss">"</span></span>
<spanid="cb10-138"><ahref="#cb10-138"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"</span><spanclass="ch">\n</span><spanclass="ss"> Am schnellsten sind </span><spanclass="sc">{</span>beste<spanclass="sc">}</span><spanclass="ss"> Arbeiter: Faktor </span><spanclass="sc">{</span>faktor<spanclass="sc">:.1f}</span><spanclass="ss"> gegenueber"</span>)</span>
<spanid="cb10-139"><ahref="#cb10-139"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" einem einzigen - also MEHR, als </span><spanclass="sc">{</span>beste<spanclass="sc">}</span><spanclass="ss"> Kerne hergeben sollten."</span>)</span>
<spanid="cb10-140"><ahref="#cb10-140"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" Das ist kein Messfehler. CP-SAT laesst nicht </span><spanclass="sc">{</span>beste<spanclass="sc">}</span><spanclass="ss">-mal dieselbe Suche"</span>)</span>
<spanid="cb10-141"><ahref="#cb10-141"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" laufen, sondern verschiedene Strategien nebeneinander, die einander"</span>)</span>
<spanid="cb10-142"><ahref="#cb10-142"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" ihre Schranken zurufen. Der zweite Arbeiter ist deshalb nicht der"</span>)</span>
<spanid="cb10-143"><ahref="#cb10-143"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"'zweite Kern', sondern ein anderes Verfahren."</span>)</span>
<spanid="cb10-144"><ahref="#cb10-144"aria-hidden="true"tabindex="-1"></a> letzte <spanclass="op">=</span> ARBEITERZAHLEN[<spanclass="op">-</span><spanclass="dv">1</span>]</span>
<spanid="cb10-145"><ahref="#cb10-145"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> beste <spanclass="op">!=</span> letzte:</span>
<spanid="cb10-147"><ahref="#cb10-147"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"</span><spanclass="ch">\n</span><spanclass="ss"> Und mehr ist nicht immer besser: </span><spanclass="sc">{</span>letzte<spanclass="sc">}</span><spanclass="ss"> Arbeiter brauchen "</span></span>
<spanid="cb10-150"><ahref="#cb10-150"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"</span><spanclass="sc">{</span>beste<spanclass="sc">}</span><spanclass="ss">. Ab einer gewissen Zahl kosten Abstimmung"</span>)</span>
<spanid="cb10-151"><ahref="#cb10-151"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" und Speicherbandbreite mehr, als ein weiterer Suchstrang einbringt."</span>)</span>
<spanid="cb10-163"><ahref="#cb10-163"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"</span><spanclass="sc">{</span><spanclass="st">'/'</span><spanclass="sc">.</span>join(<spanclass="bu">str</span>(z) <spanclass="cf">for</span> z <spanclass="kw">in</span><spanclass="bu">sorted</span>(ziele))<spanclass="sc">:>10}</span><spanclass="ss">"</span>)</span>
<spanid="cb10-165"><ahref="#cb10-165"aria-hidden="true"tabindex="-1"></a> einer <spanclass="op">=</span><spanclass="bu">len</span>({p <spanclass="cf">for</span> _, _, _, p <spanclass="kw">in</span> ergebnisse[<spanclass="dv">1</span>]})</span>
<spanid="cb10-166"><ahref="#cb10-166"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Ab welcher Arbeiterzahl bricht die Reproduzierbarkeit? Das ist die</span></span>
<spanid="cb10-167"><ahref="#cb10-167"aria-hidden="true"tabindex="-1"></a><spanclass="co"># eigentliche Zahl - nicht, was acht Arbeiter anrichten.</span></span>
<spanid="cb10-168"><ahref="#cb10-168"aria-hidden="true"tabindex="-1"></a> ab <spanclass="op">=</span><spanclass="bu">next</span>((w <spanclass="cf">for</span> w <spanclass="kw">in</span> ARBEITERZAHLEN</span>
<spanid="cb10-169"><ahref="#cb10-169"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span><spanclass="bu">len</span>({p <spanclass="cf">for</span> _, _, _, p <spanclass="kw">in</span> ergebnisse[w]}) <spanclass="op">></span><spanclass="dv">1</span>), <spanclass="va">None</span>)</span>
<spanid="cb10-170"><ahref="#cb10-170"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"</span><spanclass="ch">\n</span><spanclass="ss"> Mit einem Arbeiter: </span><spanclass="sc">{</span>einer<spanclass="sc">}</span><spanclass="ss"> Plan aus </span><spanclass="sc">{</span>WIEDERHOLUNGEN<spanclass="sc">}</span><spanclass="ss"> Laeufen."</span>)</span>
<spanid="cb10-171"><ahref="#cb10-171"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> ab <spanclass="kw">is</span><spanclass="kw">not</span><spanclass="va">None</span>:</span>
<spanid="cb10-172"><ahref="#cb10-172"aria-hidden="true"tabindex="-1"></a> viele <spanclass="op">=</span><spanclass="bu">len</span>({p <spanclass="cf">for</span> _, _, _, p <spanclass="kw">in</span> ergebnisse[ab]})</span>
<spanid="cb10-173"><ahref="#cb10-173"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" Schon mit </span><spanclass="sc">{</span>ab<spanclass="sc">}</span><spanclass="ss"> Arbeitern: </span><spanclass="sc">{</span>viele<spanclass="sc">}</span><spanclass="ss"> verschiedene Plaene aus "</span></span>
<spanid="cb10-175"><ahref="#cb10-175"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" bei identischem random_seed und identischem Zielwert."</span>)</span>
<spanid="cb10-176"><ahref="#cb10-176"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" Es braucht also keine acht Arbeiter, um die Reproduzierbarkeit"</span>)</span>
<spanid="cb10-177"><ahref="#cb10-177"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" zu verlieren. </span><spanclass="sc">{</span>ab<spanclass="sc">}</span><spanclass="ss"> genuegen."</span>)</span>
<spanid="cb10-178"><ahref="#cb10-178"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"</span><spanclass="ch">\n</span><spanclass="st"> Der Seed allein macht einen Lauf also NICHT reproduzierbar. Er legt"</span>)</span>
<spanid="cb10-179"><ahref="#cb10-179"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" fest, wie ein einzelner Suchstrang wuerfelt - nicht, welcher von"</span>)</span>
<spanid="cb10-180"><ahref="#cb10-180"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" mehreren zuerst fertig wird. Das entscheidet die Uhr."</span>)</span>
<spanid="cb10-185"><ahref="#cb10-185"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" Ein Test der Form assert plan == erwarteter_plan besteht mal und"</span>)</span>
<spanid="cb10-186"><ahref="#cb10-186"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" scheitert mal, ohne dass sich am Code etwas geaendert haette."</span>)</span>
<spanid="cb10-187"><ahref="#cb10-187"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" Zu pruefen sind stattdessen:"</span>)</span>
<spanid="cb10-188"><ahref="#cb10-188"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" * der ZIELWERT (hier in allen Laeufen gleich),"</span>)</span>
<spanid="cb10-189"><ahref="#cb10-189"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" * die Einhaltung aller Regeln (Abnahmepruefung, Kapitel Praxisfallen),"</span>)</span>
<spanid="cb10-190"><ahref="#cb10-190"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" * und der Status - nicht die Gestalt der Loesung."</span>)</span>
<spanid="cb10-191"><ahref="#cb10-191"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"</span><spanclass="ch">\n</span><spanclass="st"> Wer doch einen bestimmten Plan braucht - fuer eine abgedruckte"</span>)</span>
<spanid="cb10-192"><ahref="#cb10-192"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" Ausgabe, fuer einen Regressionstest -, setzt num_workers = 1."</span>)</span>
<spanid="cb10-193"><ahref="#cb10-193"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" Das kostet hier den Faktor </span><spanclass="sc">{</span>faktor<spanclass="sc">:.1f}</span><spanclass="ss"> an Laufzeit und ist genau"</span>)</span>
<spanid="cb10-194"><ahref="#cb10-194"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" deshalb eine Entscheidung fuer den Test, nicht fuer den Betrieb."</span>)</span>
<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>
<h3id="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>
<h3id="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>
<colstyle="width: 50%"/>
<colstyle="width: 50%"/>
</colgroup>
<thead>
<trclass="header">
<th>Prüfen</th>
<th>Nicht prüfen</th>
</tr>
</thead>
<tbody>
<trclass="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>
<trclass="even">
<td>die <strong>Einhaltung aller Regeln</strong> über eine Abnahmeprüfung (<ahref="praxisfallen.html#sec:praxisfallen-or-kern">Abschnitt 22.6</a>)</td>
<td>die Reihenfolge, in der gleichwertige Aufträge zugewiesen wurden</td>
<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/>
<h2id="sec:cpsat-statusfaelle">7.8 Die fünf Antworten von CP-SAT</h2>
<p><ahref="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>
<colstyle="width: 33%"/>
<colstyle="width: 33%"/>
<colstyle="width: 33%"/>
</colgroup>
<thead>
<trclass="header">
<th>Status</th>
<th>Was er über <em>was</em> aussagt</th>
<th>Richtige Reaktion</th>
</tr>
</thead>
<tbody>
<trclass="odd">
<td><code>OPTIMAL</code></td>
<td>über die <strong>Lösung</strong> — sie ist beweisbar bestmöglich</td>
<td>ausführen</td>
</tr>
<trclass="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>
<trclass="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>
<trclass="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>
<trclass="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>
<spanid="cb12-5"><ahref="#cb12-5"aria-hidden="true"tabindex="-1"></a><spanclass="co">Kapitel CP-SAT: Alle fuenf Antworten von CP-SAT - und was jede bedeutet.</span></span>
<spanid="cb12-7"><ahref="#cb12-7"aria-hidden="true"tabindex="-1"></a><spanclass="co">Der haeufigste Fehler beim Einsatz von CP-SAT im Betrieb ist eine Zeile wie</span></span>
<spanid="cb12-12"><ahref="#cb12-12"aria-hidden="true"tabindex="-1"></a><spanclass="co">Sie wirft in drei von fuenf Faellen etwas weg, das man haette gebrauchen</span></span>
<spanid="cb12-13"><ahref="#cb12-13"aria-hidden="true"tabindex="-1"></a><spanclass="co">koennen, und verschweigt in einem weiteren Fall einen Modellierungsfehler.</span></span>
<spanid="cb12-15"><ahref="#cb12-15"aria-hidden="true"tabindex="-1"></a><spanclass="co">Dieses Programm erzeugt jeden Statusfall absichtlich und zeigt die passende</span></span>
<spanid="cb12-19"><ahref="#cb12-19"aria-hidden="true"tabindex="-1"></a><spanclass="co"> FEASIBLE zulaessig, Zeit war um -> Gap pruefen, dann entscheiden</span></span>
<spanid="cb12-20"><ahref="#cb12-20"aria-hidden="true"tabindex="-1"></a><spanclass="co"> INFEASIBLE kein Plan existiert -> harte Regeln lockern</span></span>
<spanid="cb12-21"><ahref="#cb12-21"aria-hidden="true"tabindex="-1"></a><spanclass="co"> MODEL_INVALID das Modell ist fehlerhaft -> Programmierfehler beheben</span></span>
<spanid="cb12-22"><ahref="#cb12-22"aria-hidden="true"tabindex="-1"></a><spanclass="co"> UNKNOWN nichts gefunden, Zeit war um -> mehr Zeit oder Heuristik</span></span>
<spanid="cb12-37"><ahref="#cb12-37"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Was nach einem CP-SAT-Lauf tatsaechlich feststeht."""</span></span>
<spanid="cb12-49"><ahref="#cb12-49"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Uebersetzt einen CP-SAT-Status in eine Handlungsempfehlung.</span></span>
<spanid="cb12-51"><ahref="#cb12-51"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Genau diese Funktion fehlt in den meisten Projekten. Sie ist kurz, aber</span></span>
<spanid="cb12-52"><ahref="#cb12-52"aria-hidden="true"tabindex="-1"></a><spanclass="co"> sie ist der Unterschied zwischen einem Prototyp und einem System, das</span></span>
<spanid="cb12-53"><ahref="#cb12-53"aria-hidden="true"tabindex="-1"></a><spanclass="co"> nachts ohne Aufsicht laeuft.</span></span>
<spanid="cb12-65"><ahref="#cb12-65"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> status <spanclass="op">==</span> cp_model.OPTIMAL:</span>
<spanid="cb12-66"><ahref="#cb12-66"aria-hidden="true"tabindex="-1"></a> empfehlung <spanclass="op">=</span><spanclass="st">"Ausfuehren. Besser geht es nachweislich nicht."</span></span>
<spanid="cb12-67"><ahref="#cb12-67"aria-hidden="true"tabindex="-1"></a><spanclass="cf">elif</span> status <spanclass="op">==</span> cp_model.FEASIBLE:</span>
<spanid="cb12-68"><ahref="#cb12-68"aria-hidden="true"tabindex="-1"></a> empfehlung <spanclass="op">=</span> (<spanclass="ss">f"Brauchbar. Der Plan ist hoechstens </span><spanclass="sc">{</span>gap <spanclass="op">*</span><spanclass="dv">100</span><spanclass="sc">:.2f}</span><spanclass="ss"> % "</span></span>
<spanid="cb12-69"><ahref="#cb12-69"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"schlechter als das theoretisch Bestmoegliche."</span></span>
<spanid="cb12-70"><ahref="#cb12-70"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> gap <spanclass="kw">is</span><spanclass="kw">not</span><spanclass="va">None</span><spanclass="cf">else</span></span>
<spanid="cb12-71"><ahref="#cb12-71"aria-hidden="true"tabindex="-1"></a><spanclass="st">"Brauchbar, aber nicht als optimal bewiesen."</span>)</span>
<spanid="cb12-72"><ahref="#cb12-72"aria-hidden="true"tabindex="-1"></a><spanclass="cf">elif</span> status <spanclass="op">==</span> cp_model.INFEASIBLE:</span>
<spanid="cb12-73"><ahref="#cb12-73"aria-hidden="true"tabindex="-1"></a> empfehlung <spanclass="op">=</span> (<spanclass="st">"KEIN Plan existiert. Das ist eine Aussage ueber Ihre "</span></span>
<spanid="cb12-74"><ahref="#cb12-74"aria-hidden="true"tabindex="-1"></a><spanclass="st">"harten Regeln, nicht ueber die Rechenzeit: Auch "</span></span>
<spanid="cb12-75"><ahref="#cb12-75"aria-hidden="true"tabindex="-1"></a><spanclass="st">"unendlich viel Zeit wuerde nichts aendern. Regeln "</span></span>
<spanid="cb12-76"><ahref="#cb12-76"aria-hidden="true"tabindex="-1"></a><spanclass="st">"lockern oder weich machen (Anhang C)."</span>)</span>
<spanid="cb12-77"><ahref="#cb12-77"aria-hidden="true"tabindex="-1"></a><spanclass="cf">elif</span> status <spanclass="op">==</span> cp_model.MODEL_INVALID:</span>
<spanid="cb12-78"><ahref="#cb12-78"aria-hidden="true"tabindex="-1"></a> empfehlung <spanclass="op">=</span> (<spanclass="st">"Das MODELL ist fehlerhaft, nicht das Problem. "</span></span>
<spanid="cb12-79"><ahref="#cb12-79"aria-hidden="true"tabindex="-1"></a><spanclass="st">"Typisch: eine Variable mit leerem Wertebereich. "</span></span>
<spanid="cb12-80"><ahref="#cb12-80"aria-hidden="true"tabindex="-1"></a><spanclass="st">"model.Validate() nennt die Ursache."</span>)</span>
<spanid="cb12-82"><ahref="#cb12-82"aria-hidden="true"tabindex="-1"></a> empfehlung <spanclass="op">=</span> (<spanclass="st">"Nichts gefunden - aber auch nicht widerlegt. Mehr Zeit "</span></span>
<spanid="cb12-83"><ahref="#cb12-83"aria-hidden="true"tabindex="-1"></a><spanclass="st">"geben, das Modell vereinfachen oder mit einer "</span></span>
<spanid="cb12-100"><ahref="#cb12-100"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Fuer reproduzierbare Buchausgaben; im Betrieb weglassen.</span></span>
<spanid="cb12-110"><ahref="#cb12-110"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Vier Personen, fuenf Dienste, hoechstens zwei je Person - loesbar."""</span></span>
<spanid="cb12-112"><ahref="#cb12-112"aria-hidden="true"tabindex="-1"></a> x <spanclass="op">=</span> [[modell.NewBoolVar(<spanclass="ss">f"x_</span><spanclass="sc">{</span>i<spanclass="sc">}</span><spanclass="ss">_</span><spanclass="sc">{</span>j<spanclass="sc">}</span><spanclass="ss">"</span>) <spanclass="cf">for</span> j <spanclass="kw">in</span><spanclass="bu">range</span>(dienste)]</span>
<spanid="cb12-113"><ahref="#cb12-113"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(personen)]</span>
<spanid="cb12-115"><ahref="#cb12-115"aria-hidden="true"tabindex="-1"></a> modell.AddExactlyOne(x[i][j] <spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(personen))</span>
<spanid="cb12-116"><ahref="#cb12-116"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(personen):</span>
<spanid="cb12-121"><ahref="#cb12-121"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Dieselben fuenf Dienste, aber hoechstens EIN Dienst je Person.</span></span>
<spanid="cb12-123"><ahref="#cb12-123"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Vier Personen koennen zusammen hoechstens vier Dienste uebernehmen - fuer</span></span>
<spanid="cb12-124"><ahref="#cb12-124"aria-hidden="true"tabindex="-1"></a><spanclass="co"> fuenf Dienste reicht das nicht. Kein Solver der Welt findet hier etwas.</span></span>
<spanid="cb12-127"><ahref="#cb12-127"aria-hidden="true"tabindex="-1"></a> x <spanclass="op">=</span> [[modell.NewBoolVar(<spanclass="ss">f"x_</span><spanclass="sc">{</span>i<spanclass="sc">}</span><spanclass="ss">_</span><spanclass="sc">{</span>j<spanclass="sc">}</span><spanclass="ss">"</span>) <spanclass="cf">for</span> j <spanclass="kw">in</span><spanclass="bu">range</span>(dienste)]</span>
<spanid="cb12-128"><ahref="#cb12-128"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(personen)]</span>
<spanid="cb12-130"><ahref="#cb12-130"aria-hidden="true"tabindex="-1"></a> modell.AddExactlyOne(x[i][j] <spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(personen))</span>
<spanid="cb12-131"><ahref="#cb12-131"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(personen):</span>
<spanid="cb12-138"><ahref="#cb12-138"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Im Alltag entsteht das durch einen Rechenfehler in den Grenzen, etwa</span></span>
<spanid="cb12-139"><ahref="#cb12-139"aria-hidden="true"tabindex="-1"></a><spanclass="co"> NewIntVar(mindestbesetzung, kapazitaet, ...) bei falsch sortierten Daten.</span></span>
<spanid="cb12-146"><ahref="#cb12-146"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""120 Auftraege auf 11 Maschinen - gross genug, dass der Optimalitaets-</span></span>
<spanid="cb12-147"><ahref="#cb12-147"aria-hidden="true"tabindex="-1"></a><spanclass="co"> beweis laenger dauert als das Finden einer sehr guten Loesung."""</span></span>
<spanid="cb12-150"><ahref="#cb12-150"aria-hidden="true"tabindex="-1"></a> n, maschinen <spanclass="op">=</span><spanclass="bu">len</span>(dauer), <spanclass="dv">11</span></span>
<spanid="cb12-153"><ahref="#cb12-153"aria-hidden="true"tabindex="-1"></a> x <spanclass="op">=</span> [[modell.NewBoolVar(<spanclass="ss">f"x_</span><spanclass="sc">{</span>i<spanclass="sc">}</span><spanclass="ss">_</span><spanclass="sc">{</span>k<spanclass="sc">}</span><spanclass="ss">"</span>) <spanclass="cf">for</span> k <spanclass="kw">in</span><spanclass="bu">range</span>(maschinen)]</span>
<spanid="cb12-154"><ahref="#cb12-154"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(n)]</span>
<spanid="cb12-155"><ahref="#cb12-155"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(n):</span>
<spanid="cb12-157"><ahref="#cb12-157"aria-hidden="true"tabindex="-1"></a> belegung <spanclass="op">=</span> [modell.NewIntVar(<spanclass="dv">0</span>, obergrenze, <spanclass="ss">f"l_</span><spanclass="sc">{</span>k<spanclass="sc">}</span><spanclass="ss">"</span>) <spanclass="cf">for</span> k <spanclass="kw">in</span><spanclass="bu">range</span>(maschinen)]</span>
<spanid="cb12-158"><ahref="#cb12-158"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> k <spanclass="kw">in</span><spanclass="bu">range</span>(maschinen):</span>
<spanid="cb12-159"><ahref="#cb12-159"aria-hidden="true"tabindex="-1"></a> modell.Add(belegung[k] <spanclass="op">==</span><spanclass="bu">sum</span>(<spanclass="bu">int</span>(dauer[i]) <spanclass="op">*</span> x[i][k] <spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(n)))</span>
<spanid="cb12-166"><ahref="#cb12-166"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""46 grosse Zahlen exakt in zwei gleich schwere Haelften teilen.</span></span>
<spanid="cb12-168"><ahref="#cb12-168"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Ein beruehmt schweres Problem: Es gibt keine Zielfunktion, entweder es</span></span>
<spanid="cb12-169"><ahref="#cb12-169"aria-hidden="true"tabindex="-1"></a><spanclass="co"> geht auf oder nicht - und beides zu entscheiden dauert lange.</span></span>
<spanid="cb12-173"><ahref="#cb12-173"aria-hidden="true"tabindex="-1"></a> x <spanclass="op">=</span> [modell.NewBoolVar(<spanclass="ss">f"x_</span><spanclass="sc">{</span>i<spanclass="sc">}</span><spanclass="ss">"</span>) <spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(<spanclass="bu">len</span>(gewichte))]</span>
<spanid="cb12-174"><ahref="#cb12-174"aria-hidden="true"tabindex="-1"></a> modell.Add(<spanclass="bu">sum</span>(<spanclass="bu">int</span>(gewichte[i]) <spanclass="op">*</span> x[i] <spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(<spanclass="bu">len</span>(gewichte)))</span>
<spanid="cb12-180"><ahref="#cb12-180"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" Status </span><spanclass="sc">{</span>a<spanclass="sc">.</span>status<spanclass="sc">}</span><spanclass="ss">"</span>)</span>
<spanid="cb12-190"><ahref="#cb12-190"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" DIE FUENF ANTWORTEN VON CP-SAT"</span>)</span>
<spanid="cb12-199"><ahref="#cb12-199"aria-hidden="true"tabindex="-1"></a> zeige(<spanclass="st">"[3] Derselbe Fall mit 10 s Limit"</span>,</span>
<spanid="cb12-208"><ahref="#cb12-208"aria-hidden="true"tabindex="-1"></a> zeige(<spanclass="st">"[6] Zahlpartition mit 46 grossen Zahlen, 2 s Limit"</span>,</span>
<spanid="cb12-212"><ahref="#cb12-212"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" DIE DREI, DIE MAN NICHT VERWECHSELN DARF"</span>)</span>
<spanid="cb12-214"><ahref="#cb12-214"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"INFEASIBLE Es gibt keine Loesung. Eine Aussage ueber Ihr MODELL."</span>)</span>
<spanid="cb12-215"><ahref="#cb12-215"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" Mehr Rechenzeit aendert daran nichts."</span>)</span>
<spanid="cb12-216"><ahref="#cb12-216"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"UNKNOWN Es wurde keine gefunden. Eine Aussage ueber die ZEIT."</span>)</span>
<spanid="cb12-217"><ahref="#cb12-217"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" Ob es eine gibt, ist offen."</span>)</span>
<spanid="cb12-218"><ahref="#cb12-218"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"MODEL_INVALID Das Modell ist gar kein gueltiges Modell. Eine"</span>)</span>
<spanid="cb12-219"><ahref="#cb12-219"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" Aussage ueber Ihren CODE - immer ein Programmierfehler."</span>)</span>
<spanid="cb12-221"><ahref="#cb12-221"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Wer diese drei in einem 'else: return None' zusammenfasst, verliert"</span>)</span>
<spanid="cb12-222"><ahref="#cb12-222"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"genau die Information, die zur Fehlersuche noetig waere."</span>)</span>
-> 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 %)
-> 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 %)
-> Brauchbar. Der Plan ist hoechstens 0.14 % schlechter als das theoretisch Bestmoegliche.
[4] Dienstplan, hoechstens 1 Dienst je Person
Status INFEASIBLE
Loesung vorhanden nein
-> 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
-> 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
-> Nichts gefunden - aber auch nicht widerlegt. Mehr Zeit geben, das Modell vereinfachen oder mit einer Heuristik vorbelegen (AddHint).
<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>
<trclass="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>
<trclass="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>
<trclass="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>
<trclass="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>
<p><strong>Aufgabe 7.1 ⭐ — Propagation nachvollziehen.</strong><spanclass="math inline">x_1 \in \{1,\dots,6\}</span>, <spanclass="math inline">x_2 \in \{1,\dots,6\}</span>, Bedingungen: <spanclass="math inline">x_1 < x_2</span> und <spanclass="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>
<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 <spanclass="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>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>
<spanid="cb14-5"><ahref="#cb14-5"aria-hidden="true"tabindex="-1"></a><spanclass="co">Kapitel CP-SAT: Warum ein Strafgewicht keine Wichtigkeit ausdrueckt, sondern</span></span>
<spanid="cb14-8"><ahref="#cb14-8"aria-hidden="true"tabindex="-1"></a><spanclass="co">Weiche Regeln kommen als Strafterm in die Zielfunktion. Die Frage, die dabei</span></span>
<spanid="cb14-9"><ahref="#cb14-9"aria-hidden="true"tabindex="-1"></a><spanclass="co">regelmaessig falsch beantwortet wird, lautet: "Wie gross muss die Strafe sein?"</span></span>
<spanid="cb14-11"><ahref="#cb14-11"aria-hidden="true"tabindex="-1"></a><spanclass="co">fuehrt zuverlaessig zu Plaenen, die niemand unterschreiben will.</span></span>
<spanid="cb14-13"><ahref="#cb14-13"aria-hidden="true"tabindex="-1"></a><spanclass="co">Der Grund: Zwei Strafgewichte legen nicht Wichtigkeiten fest, sondern einen</span></span>
<spanid="cb14-14"><ahref="#cb14-14"aria-hidden="true"tabindex="-1"></a><spanclass="co">UMRECHNUNGSKURS. Steht die Serienregel bei 1 und der Wunschfrei-Tag bei 10,</span></span>
<spanid="cb14-15"><ahref="#cb14-15"aria-hidden="true"tabindex="-1"></a><spanclass="co">dann hat man dem Solver woertlich gesagt: "Zehn Verstoesse gegen die</span></span>
<spanid="cb14-16"><ahref="#cb14-16"aria-hidden="true"tabindex="-1"></a><spanclass="co">Serienregel sind so schlimm wie ein abgelehnter Wunsch." Er nimmt das</span></span>
<spanid="cb14-17"><ahref="#cb14-17"aria-hidden="true"tabindex="-1"></a><spanclass="co">ernst - und handelt danach.</span></span>
<spanid="cb14-19"><ahref="#cb14-19"aria-hidden="true"tabindex="-1"></a><spanclass="co">Szenario: Schichtplan, 6 Personen, 14 Tage, 4 Besetzungen pro Tag.</span></span>
<spanid="cb14-20"><ahref="#cb14-20"aria-hidden="true"tabindex="-1"></a><spanclass="co"> HART jeder Tag genau 4 Personen; hoechstens 11 Dienste je Person</span></span>
<spanid="cb14-21"><ahref="#cb14-21"aria-hidden="true"tabindex="-1"></a><spanclass="co"> WEICH keine drei Dienste in Folge (Betriebsvereinbarung)</span></span>
<spanid="cb14-34"><ahref="#cb14-34"aria-hidden="true"tabindex="-1"></a>STRAFE_WUNSCH <spanclass="op">=</span><spanclass="dv">10</span><spanclass="co"># bleibt fest - variiert wird die Serienstrafe</span></span>
<spanid="cb14-37"><ahref="#cb14-37"aria-hidden="true"tabindex="-1"></a>WUNSCHFREI <spanclass="op">=</span><spanclass="bu">sorted</span>({(p, t) <spanclass="cf">for</span> p <spanclass="kw">in</span><spanclass="bu">range</span>(PERSONEN) <spanclass="cf">for</span> t <spanclass="kw">in</span><spanclass="bu">range</span>(TAGE)</span>
<spanid="cb14-42"><ahref="#cb14-42"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Baut den Schichtplan mit dem angegebenen Gewicht fuer die Serienregel."""</span></span>
<spanid="cb14-43"><ahref="#cb14-43"aria-hidden="true"tabindex="-1"></a> modell <spanclass="op">=</span> cp_model.CpModel()</span>
<spanid="cb14-44"><ahref="#cb14-44"aria-hidden="true"tabindex="-1"></a> x <spanclass="op">=</span> {(p, t): modell.NewBoolVar(<spanclass="ss">f"x_</span><spanclass="sc">{</span>p<spanclass="sc">}</span><spanclass="ss">_</span><spanclass="sc">{</span>t<spanclass="sc">}</span><spanclass="ss">"</span>)</span>
<spanid="cb14-45"><ahref="#cb14-45"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> p <spanclass="kw">in</span><spanclass="bu">range</span>(PERSONEN) <spanclass="cf">for</span> t <spanclass="kw">in</span><spanclass="bu">range</span>(TAGE)}</span>
<spanid="cb14-48"><ahref="#cb14-48"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> t <spanclass="kw">in</span><spanclass="bu">range</span>(TAGE):</span>
<spanid="cb14-49"><ahref="#cb14-49"aria-hidden="true"tabindex="-1"></a> modell.Add(<spanclass="bu">sum</span>(x[p, t] <spanclass="cf">for</span> p <spanclass="kw">in</span><spanclass="bu">range</span>(PERSONEN)) <spanclass="op">==</span> PRO_TAG)</span>
<spanid="cb14-50"><ahref="#cb14-50"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> p <spanclass="kw">in</span><spanclass="bu">range</span>(PERSONEN):</span>
<spanid="cb14-51"><ahref="#cb14-51"aria-hidden="true"tabindex="-1"></a> modell.Add(<spanclass="bu">sum</span>(x[p, t] <spanclass="cf">for</span> t <spanclass="kw">in</span><spanclass="bu">range</span>(TAGE)) <spanclass="op"><=</span> MAX_DIENSTE)</span>
<spanid="cb14-53"><ahref="#cb14-53"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- weiche Regel 1: keine drei Dienste in Folge --------------------</span></span>
<spanid="cb14-54"><ahref="#cb14-54"aria-hidden="true"tabindex="-1"></a><spanclass="co"># serie[p, t] = 1 <=> Person p arbeitet an t, t+1 UND t+2.</span></span>
<spanid="cb14-55"><ahref="#cb14-55"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Die Ungleichung erzwingt das nur in eine Richtung; das genuegt, weil</span></span>
<spanid="cb14-56"><ahref="#cb14-56"aria-hidden="true"tabindex="-1"></a><spanclass="co"># der Solver serie minimiert - er wuerde die Variable nie freiwillig</span></span>
<spanid="cb14-57"><ahref="#cb14-57"aria-hidden="true"tabindex="-1"></a><spanclass="co"># auf 1 setzen.</span></span>
<spanid="cb14-59"><ahref="#cb14-59"aria-hidden="true"tabindex="-1"></a> serien <spanclass="op">=</span> []</span>
<spanid="cb14-60"><ahref="#cb14-60"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> p <spanclass="kw">in</span><spanclass="bu">range</span>(PERSONEN):</span>
<spanid="cb14-61"><ahref="#cb14-61"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> t <spanclass="kw">in</span><spanclass="bu">range</span>(TAGE <spanclass="op">-</span><spanclass="dv">2</span>):</span>
<spanid="cb14-62"><ahref="#cb14-62"aria-hidden="true"tabindex="-1"></a> serie <spanclass="op">=</span> modell.NewBoolVar(<spanclass="ss">f"serie_</span><spanclass="sc">{</span>p<spanclass="sc">}</span><spanclass="ss">_</span><spanclass="sc">{</span>t<spanclass="sc">}</span><spanclass="ss">"</span>)</span>
<spanid="cb14-63"><ahref="#cb14-63"aria-hidden="true"tabindex="-1"></a> modell.Add(x[p, t] <spanclass="op">+</span> x[p, t <spanclass="op">+</span><spanclass="dv">1</span>] <spanclass="op">+</span> x[p, t <spanclass="op">+</span><spanclass="dv">2</span>] <spanclass="op"><=</span><spanclass="dv">2</span><spanclass="op">+</span> serie)</span>
<spanid="cb14-79"><ahref="#cb14-79"aria-hidden="true"tabindex="-1"></a> status <spanclass="op">=</span> loeser.Solve(modell)</span>
<spanid="cb14-80"><ahref="#cb14-80"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> status <spanclass="kw">not</span><spanclass="kw">in</span> (cp_model.OPTIMAL, cp_model.FEASIBLE):</span>
<spanid="cb14-93"><ahref="#cb14-93"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" STRAFGEWICHTE SIND WECHSELKURSE, KEINE WICHTIGKEITEN"</span>)</span>
<spanid="cb14-95"><ahref="#cb14-95"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"</span><spanclass="sc">{</span>PERSONEN<spanclass="sc">}</span><spanclass="ss"> Personen, </span><spanclass="sc">{</span>TAGE<spanclass="sc">}</span><spanclass="ss"> Tage, </span><spanclass="sc">{</span>PRO_TAG<spanclass="sc">}</span><spanclass="ss"> Besetzungen pro Tag "</span></span>
<spanid="cb14-97"><ahref="#cb14-97"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"Das sind </span><spanclass="sc">{</span>TAGE <spanclass="op">*</span> PRO_TAG <spanclass="op">/</span> PERSONEN<spanclass="sc">:.1f}</span><spanclass="ss"> Dienste je Person - der Plan ist eng."</span>)</span>
<spanid="cb14-98"><ahref="#cb14-98"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"</span><spanclass="sc">{</span><spanclass="bu">len</span>(WUNSCHFREI)<spanclass="sc">}</span><spanclass="ss"> Wunschfrei-Antraege, Strafe je abgelehntem Wunsch: "</span></span>
<spanid="cb14-101"><ahref="#cb14-101"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"</span><spanclass="sc">{</span><spanclass="st">'Strafe je 3er-Serie'</span><spanclass="sc">:>20}</span><spanclass="ss"></span><spanclass="sc">{</span><spanclass="st">'3er-Serien'</span><spanclass="sc">:>12}</span><spanclass="ss"></span><spanclass="sc">{</span><spanclass="st">'Wunsch verletzt'</span><spanclass="sc">:>17}</span><spanclass="ss">"</span></span>
<spanid="cb14-113"><ahref="#cb14-113"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"</span><spanclass="ch">\n</span><spanclass="ss">Bei Strafe 1 baut der Solver </span><spanclass="sc">{</span>niedrig[<spanclass="st">'serien'</span>]<spanclass="sc">}</span><spanclass="ss"> Drei-Tage-Serien ein."</span>)</span>
<spanid="cb14-114"><ahref="#cb14-114"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Die Betriebsvereinbarung steht im Modell - und wird trotzdem"</span>)</span>
<spanid="cb14-115"><ahref="#cb14-115"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"systematisch verletzt. Das ist kein Fehler des Solvers: Bei einem"</span>)</span>
<spanid="cb14-116"><ahref="#cb14-116"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"Gewicht von 1 gegen </span><spanclass="sc">{</span>STRAFE_WUNSCH<spanclass="sc">}</span><spanclass="ss"> lohnt sich jede Serie, solange sie"</span>)</span>
<spanid="cb14-117"><ahref="#cb14-117"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"auch nur einen Zehntel-Wunsch rettet."</span>)</span>
<spanid="cb14-118"><ahref="#cb14-118"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"</span><spanclass="ch">\n</span><spanclass="ss">Bei Strafe 30 sind es </span><spanclass="sc">{</span>hoch[<spanclass="st">'serien'</span>]<spanclass="sc">}</span><spanclass="ss"> Serien - dafuer werden"</span>)</span>
<spanid="cb14-120"><ahref="#cb14-120"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Beide Plaene sind optimal. Sie beantworten nur verschiedene Fragen."</span>)</span>
<spanid="cb14-122"><ahref="#cb14-122"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Die Frage lautet also nie 'wie wichtig ist mir diese Regel?', sondern:"</span>)</span>
<spanid="cb14-123"><ahref="#cb14-123"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"'Wie viele abgelehnte Wuensche bin ich bereit zu akzeptieren,"</span>)</span>
<spanid="cb14-124"><ahref="#cb14-124"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" um eine Drei-Tage-Serie zu vermeiden?'"</span>)</span>
<spanid="cb14-125"><ahref="#cb14-125"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Wer darauf keine Zahl nennen kann, hat das Problem noch nicht"</span>)</span>
<spanid="cb14-126"><ahref="#cb14-126"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"verstanden - und sollte diese Tabelle dem Auftraggeber vorlegen,"</span>)</span>
<spanid="cb14-127"><ahref="#cb14-127"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"statt ein Gewicht zu raten."</span>)</span>
<spanid="cb14-129"><ahref="#cb14-129"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Und die wichtigste Konsequenz: Eine Regel, die NIE gebrochen werden"</span>)</span>
<spanid="cb14-130"><ahref="#cb14-130"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"darf, gehoert nicht in die Zielfunktion, sondern unter die harten"</span>)</span>
<spanid="cb14-131"><ahref="#cb14-131"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Nebenbedingungen. Alles, was einen Preis hat, wird irgendwann gekauft."</span>)</span>
<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 (<ahref="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>
<p>Genau eine Antwort ist jeweils richtig. Auflösung in <ahref="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 <spanclass="math inline">k</span> Aufträgen <spanclass="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>
<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 <ahref="#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><ahref="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>