Globaler Optimierer (A-3 / A-5)
Bezug: Lastenheft §6.2 (weiche Ziele), §9 A-3/A-5 · Code: server/src/solver/ · Demo: cd server && npm run solve
Der Greedy-Scheduler (
docs/datamodel.md§5) liefert je Auftrag das frühest-mögliche, §6.1-zulässige Set — aber nicht das global beste. Dieser Optimierer schließt die Lücke zu §6.2.
1. Die Problematik: Warum Taktung schwer ist
Kernaussage: Eine optimale Werkstatt-Taktung zu finden ist ein NP-schweres Kombinatorik-Problem (verwandt mit Job-Shop-Scheduling) mit widersprüchlichen Zielen und stark gekoppelten harten Constraints. Es gibt keinen Algorithmus, der zugleich beweisbar optimal, in Millisekunden und überall lauffähig ist — diese drei Eigenschaften stehen im Konflikt. Genau daraus folgt (§2), warum mehrere Optimierer existieren.
Vier Treiber machen das Problem hart:
- Kombinatorische Explosion. Schon die reine Reihenfolge von N Aufträgen hat N! Varianten (7 Aufträge → 5 040, 10 → 3,6 Mio.). Der volle Entscheidungsraum ist weit größer: zusätzlich Startzeit, Bucht und Mitarbeiter-Zuweisung je Teilschritt. Brute-Force skaliert nicht.
- Widersprüchliche Ziele (§6.2). Kurze mittlere Durchlaufzeit (Kundensicht, SPT-artig) ↔ kleiner Makespan (Batch-Durchsatz) ↔ wenige Mitarbeiter-Wechsel (G-2) ↔ keine kleinen Lücken (G-3). Kein Plan ist in allem zugleich am besten — die Demo (§5) zeigt es: SPT senkt die Durchlaufzeit, erhöht aber den Makespan. „Optimal" braucht daher eine bewusste, lexikografische Reihenfolge der Kriterien (§4).
- Harte, gekoppelte Constraints (§6.1). Skill-Match, Bucht-No-Overlap inkl. Cool-down, Mitarbeiter-No-Overlap, Schichten/Sperren, Abhängigkeiten (
nach_vorherigem), Standzeit, getypte Parkplatz-Pools (Luxus → nur Indoor). Diese Entscheidungen koppeln: eine Bucht-Zuweisung verschiebt Cool-downs, blockiert Parkplätze und verschiebt abhängige Schritte — lokale Optima sind selten global gut. - Dynamik & Laufzeit-Umgebung. Laufende harte Reservierungen (aktiv/pausiert/blockiert) sind fix und müssen respektiert werden; Eilaufträge (R7-F06) drängeln vor; neu geplant wird live. Und der Code läuft im Cloudflare Worker / Durable Object — kein nativer C++/Python-Code, begrenzte CPU-Zeit, synchron erwartete Antwort. Der mathematisch stärkste Solver (OR-Tools) ist dort gar nicht ausführbar.
2. Warum es mehrere Optimierer gibt
Kernaussage: Weil sich Optimalität, Antwortzeit und Lauffähigkeit im Worker nicht gleichzeitig maximieren lassen, staffelt Taktano drei Optimierer entlang dieser Achsen zu einer Kaskade mit Fallback. So bekommt der Normalfall das beste Ergebnis, der Störfall behält ein lauffähiges System — graceful degradation statt Totalausfall.
| Optimierer | Code | Freiheitsgrade | Optimalität | Tempo | Im Worker? | Rolle |
|---|---|---|---|---|---|---|
| Greedy-Scheduler | scheduleSequence (datamodel.md §5) | je Auftrag frühest-möglich | zulässig (§6.1), nicht global optimal | O(n), sub-ms | ✅ | Baustein & Decoder für die beiden anderen |
| TS-Decoder-Optimierer | solver/solve.ts | nur Reihenfolge | exakt für N ≤ 7, sonst Heuristik | < 1 s | ✅ | Worker-Zwischenstufe · Fallback · Referenz-Testfall |
| CP-SAT-Solver | solver-service/model.py (OR-Tools) | voll: Reihenfolge + Startzeit + Bucht + Mitarbeiter | optimal/feasible (timeout-begrenzt) | ~Sek., netzabhängig | ❌ (separater Fly.io-Service) | Bestes Ergebnis, unterstützt Pins (3b) |
Die Stufen bauen aufeinander auf: Der Greedy ist der Decoder, der jede Reihenfolge in einen zulässigen Plan übersetzt (§3); der TS-Decoder sucht darüber die beste Reihenfolge und dient zugleich als Worker-kompatibler Fallback und als gegenseitige Validierung des CP-SAT (beide liefern in der Demo Σ Durchlauf 78.5 h, §6); der CP-SAT öffnet den vollen Entscheidungsraum für das real beste Ergebnis, lebt aber als eigener Service, weil OR-Tools nicht im Worker läuft.
Der Leitstand (§7) wählt zur Laufzeit:
Warum nicht nur einer? Ein einziger Solver müsste einen der Konflikte aus §1 verlieren: Nur CP-SAT → kein Plan, sobald der Service/das Netz weg ist (oder im lokalen/Worker-Pfad gar nicht ausführbar). Nur TS-Decoder → nie das real beste Ergebnis, da Startzeiten/Zuweisungen fix bleiben und Pins (3b) fehlen. Nur Greedy → schnell und robust, aber blind gegenüber §6.2. Die Kaskade kombiniert die Stärken: CP-SAT wenn möglich, TS-Decoder wenn nötig, Greedy als gemeinsames Fundament.
Die Detail-Abschnitte unten vertiefen die Kaskade von unten nach oben: §3–§5 der TS-Decoder (Ansatz, Zielfunktion, Demo), §6 der CP-SAT-Service, §7 die Verdrahtung im Leitstand inkl. Fallback.
3. Ansatz: Decoder-basierte exakte Suche
Entscheidungsvariable ist die Einplan-Reihenfolge der Aufträge. Der Greedy (scheduleSequence) ist der Decoder: jede Reihenfolge → ein vollständiger, harter-Constraints-zulässiger Plan (§6.1). Bewertet wird das Ergebnis mit der Zielfunktion (§6.2).
- Exakt für kleine N: alle Permutationen der nicht-Eil-Aufträge (
MAX_EXAKT = 7). - Eilaufträge (R7-F06) werden als harte Vorrang-Bedingung vorangestellt und nicht permutiert.
- Größeres N → Fallback auf die Priority-Reihenfolge (Heuristik), klar markiert (
exakt: false).
4. Zielfunktion (§6.2, objective.ts)
Lexikografisch (istBesser): mehr machbar → kleinere Σ Durchlaufzeit → kleinerer Makespan → kleinere Wechsel-Pönale (G-2) → weniger Lücken < 15 Min (G-3).
- Σ Durchlaufzeit = Σ (Fertigstellung − Anlieferung) — mittlere Durchlaufzeit/Kundensicht; SPT-artig.
- Makespan = späteste Fertigstellung — Batch-Durchsatz.
- Wechsel-Pönale (G-2, 5 Min) und Lücken < 15 Min (G-3) aus den rekonstruierten Mitarbeiter-Timelines.
Die Reihenfolge der Kriterien ist die Geschäftsentscheidung „was heißt optimal" — der Komparator ist explizit und austauschbar.
5. Demo-Ergebnis
3 Aufträge, gleicher Termin, gemischter Umfang (1 Premium mit Keramik, 2 Basic ohne):
| Reihenfolge | Σ Durchlaufzeit | Makespan | |
|---|---|---|---|
| Priority-Greedy | Premium → Basic → Basic | 82.5 h | Mi 15:00 |
| Optimum | Basic → Basic → Premium | 78.5 h | Do 08:30 |
Der Optimierer findet die SPT-Reihenfolge (kurze Jobs zuerst) und senkt die mittlere Durchlaufzeit um 4 h. Trade-off: der Makespan steigt (Premium rutscht ans Ende) — genau die Abwägung, die die Zielfunktions-Reihenfolge festlegt. Wer Batch-Durchsatz priorisiert, stellt Makespan vor Σ Durchlaufzeit; das ist eine Ein-Zeilen-Änderung in istBesser.
6. Echter CP-SAT-Solver — separater Service (A-5) ✅
Der TS-Decoder lässt nur die Reihenfolge frei. Der vollwertige Solver hat Startzeiten und Zuweisungen als freie Variablen — realisiert als separater Python-Service mit OR-Tools CP-SAT in solver-service/, da OR-Tools (C++/Python) nicht in Cloudflare Workers läuft.
- Arbeitsteilung:
server/src/solver/problem.tsexpandiert die Domäne (Schichten−Abwesenheiten, Sperren, Prozess→Schritte) zu einem flachenCpSatProblem(Zeiten ab Horizont-Start);model.pyist ein reiner CP-SAT-Solver;cpsat-client.tsruft per HTTP und rebasiert die Zeiten zurück. - Modelliert (§6.1): Skill-Match, distinkte Skills/Helfer, Bay-No-Overlap inkl. Cool-down (Cumulative bei Kapazität > 1), Mitarbeiter-No-Overlap, Schichten/Sperren als blockierte Intervalle, Abhängigkeit, Standzeit, getypte Parkplatz-Pools (siehe unten). Ziel §6.2:
flow/makespan+ Wechsel-Pönale (G-2). - Getypte Parkplatz-Pools: Ein Fahrzeug parkt in den Lücken seines on-site-Aufenthalts (vor erstem Schritt, zwischen Schritten, nach letztem Schritt = späte Abholung). Jede Lücke wird einem erlaubten Pool zugewiesen (
order.allowedParkTypes, Luxus → nur Indoor); pro Pool eine Kapazitäts-Cumulative. Verschiedene Lücken dürfen verschiedene Pools nutzen (= Umparken fällt automatisch heraus). Self-Test: 2 Luxusfahrzeuge bei Indoor-Kapazität 1 → INFEASIBLE, bei 2 → OK. Annahme: Schritte sequenziell (gilt fürnach_vorherigem-Prozesse). Noch nicht: Parkplatz-Sperren. - G-2 (Wechsel-Pönale) als Ziel-Term: Proxy „distinkte Fahrzeuge je Mitarbeiter − 1" × 5 Min (tractabel ohne Sequencing). Die exakten Kennzahlen
wechsel/smallGaps(G-3) werden zusätzlich aus der Lösung berechnet und immetrics-Feld zurückgegeben (Parität zur TS-objective.ts). - Verifiziert: CP-SAT liefert Σ Durchlauf 78.5 h wie der TS-Optimierer (gegenseitige Validierung) plus konkrete Zuweisung und
metrics(Parkplatz-Kapazität 10, Wechsel 4 = 20 min, Lücken<15 = 0). Self-Tests: Skill-Distinktheit, Abhängigkeit, getypte Parkplatz-Kapazität (Luxus/Indoor).npm run solve:cpsat(mit Service) bzw. Fallback ohne. - Indoor-Trocknung-Pflicht (F07a) ✅ (OP-OPT-5 Slice 1): ein Schritt mit
coolDownIndoorPflicht(Aushärtung muss indoor, Auto bleibt in der Bucht) wird im CP-SAT auf Indoor-Buchten beschränkt —buildProblemverengtallowedBays(geteiltesINDOOR_TYP_REmit dem TS-Scheduler); kein Modell-Eingriff, da der Cool-down die Bucht ohnehin hält. Self-Testnpm run test:indoor. - Folierbucht-als-Überlauf-Parkplatz ✅ (OP-OPT-5 Slice 2): freie Folierbuchten dienen als Indoor-Stellplatz.
buildProblemführt sie als zusätzliche Pools (mitbayId, nach den dedizierten);model.pylegt ihre Park-Intervalle in die bestehende Bucht-No-Overlap → eine Bucht hostet zu einem Zeitpunkt Arbeit ODER einen Parker. Self-Testsnpm run test:overflow(Builder) +python model.py(Modell: Standzeit-Lücke ohne Pool INFEASIBLE, mit Überlauf parkbar). Bewusst (noch) nicht: Präferenz „dedizierte vor Überlauf" als Pönale (heute nur Reihenfolge, der Solver ist indifferent solange machbar). - Noch nicht im CP-SAT-Modell (→ Ausbau, OP-OPT-5): G-3 als harte/penalisierte Bedingung (nur gemeldet), Nacht-Scheduling — im TS-Greedy/-Optimierer bereits abgebildet.
Der TS-Decoder-Optimierer bleibt als Worker-kompatible Zwischenstufe, Fallback und Referenz/Testfall erhalten.
7. Anbindung im Leitstand (Durable Object)
Der DO (server/party/leitstand.ts) ruft den Solver bei der Mehrauftrags-Planung selbst auf — Wire-Message auftraege.optimieren, Methode optimiereOffene():
- Sammelt die offenen (unbestätigten) Aufträge; harte Reservierungen (aktiv/pausiert/blockiert) werden via
belegeWorkshopzu belegten Ressourcen und inbuildProblem(…, belegt)aus der Verfügbarkeit geschnitten (§6.3 — harte gewinnen, bleiben fix). - Versucht CP-SAT über
env.SOLVER_URL(solveViaCpSat); bei Erfolg wird die Lösung je Auftrag mitapplyPlanungübernommen (Status/IDs bleiben). - Fallback ohne erreichbaren Service: TS-Scheduler gegen dieselben belegten harten Ressourcen.
Der Client zeigt solverInfo (welcher Pfad lief) und einen Button „Offene optimieren (CP-SAT)". SOLVER_URL wird als Worker-Var gesetzt (lokal z. B. http://127.0.0.1:8090); ohne Var läuft der Fallback. Verifiziert: Glue (toAuftragInput → buildProblem(belegt) → CP-SAT → applyPlanung) end-to-end gegen den laufenden Service; Server-tsc, Client-ng build, Worker-Bundle (wrangler --dry-run). Live-Verhalten im Browser nicht visuell geprüft.
↩ Zurück zur Doku-Landkarte · Lesepfade · Register (alle OPs)