Skip to content

Solver verstehen & UNSAT lösen

Wie Prilog rechnet, was die Score-Werte bedeuten, und was Du tust wenn der Solver „Lösung nicht möglich" meldet.


Was macht der Solver?

Wenn Du Auto-Plan klickst, geht Prilog so vor:

  1. Backend sammelt alle Stammdaten (Klassen, Fächer, Räume, Lehrer, Zeitraster, Stundentafel, Regelwerk, Verfügbarkeiten, Couplings …) und baut daraus einen Snapshot.
  2. Der Snapshot wird per HTTP an den Solver-Service geschickt (eigener Python-Service mit Google OR-Tools CP-SAT).
  3. Der Solver baut ein mathematisches Modell mit Variablen (welche Stunde liegt wann?) und Constraints (welche Kombinationen sind erlaubt?).
  4. CP-SAT sucht eine Lösung mit minimalem Soft-Score, bis das Zeitbudget abläuft oder die optimale Lösung gefunden ist.
  5. Das Ergebnis kommt als Liste von Einträgen (Klasse + Fach + Lehrer + Raum + Slot) zurück.

CP-SAT (Constraint Programming - Boolean Satisfiability) ist das gleiche Mathe wie SAT-Solver für Software-Verifikation: ein Such-Algorithmus, der sehr viele Kombinationen schnell durchprobiert.


Hard- vs. Soft-Constraints

Hard = Verletzung verboten. Der Solver würde lieber gar keine Lösung finden als eine Hard-Verletzung produzieren.

Soft = Verletzung wird mit Penalty-Punkten („Score") bestraft. Solver versucht, den Score zu minimieren.

Aktive Hard-Constraints (Stand 2026-05-28)

#ConstraintWas es verbietet
H1Klasse-Slot-KonfliktEine Klasse hat nicht zwei Stunden zur selben Zeit (außer Halbgruppen)
H2Lehrer-Slot-KonfliktEin Lehrer kann nicht zwei Stunden gleichzeitig geben
H3Raum-Slot-KonfliktEin Raum ist nicht doppelt belegt
H4QualifikationSolver setzt nur qualifizierte Lehrer (TeacherQualification)
H5Raum-TagsSport braucht sporthalle-Tag, etc. (Subject.requiredResourceTags)
H6PeriodSlots gefiltertPausen-Slots werden nicht belegt
H7Manuelle PinsWas fixiert ist, bleibt fixiert (TimetablePinConstraint)
H8Auto-Pins aus Assignmentimportance=100 setzt Lehrer hart
H9Stammraum-DefaultKlassen-Stammraum-Auto-Pin
H10Lock-Level CellsBoard-Cells mit pinLevel != soft → Position/Lehrer/Raum fix
H11Lehrer-Deputatsum(Stunden) ≤ contractedHoursWeek − reductionHoursWeek
H12Lehrer-VerfügbarkeitLehrer wird NIE in gesperrte Slots gesetzt
H13Klassen-Regelwerk5 Regeltypen (subject_in_window etc.) als hard
H14A/B-WochenA und B konfligieren nicht im gleichen Slot, both blockiert beide
H15CouplingsCells mit gleichem couplingKey landen im gleichen Slot
H16Stundentafel-Vollständigkeitjede SGH-Stunde MUSS platziert werden

Aktive Soft-Constraints (Score-Buckets)

#BucketPenalty pro VerstoßWas gut ist
S1free_periods1Wenig Hohlstunden für Klassen
S2pref_grade_mismatch1Lehrer in Wunsch-Stufen (TeacherPreference)
S3missing_preferred_tags1Fach in präferiertem Raum-Tag
S4double_slot_misses2preferDoubleSlot erfüllt
S5teacher_day_overload(geplant)Max. Stunden pro Tag pro Lehrer
S6room_changes1Wenig Räume pro Klasse-Tag (Raum-Kontinuität)
S7preferred_room_mismatch1Lehrer im Lieblings-Raum

score.total = Summe aller. Niedriger = besser.


Status-Werte verstehen

Der Solver liefert einen der folgenden Status:

StatusBedeutungWas tun?
optimalBeste Lösung gefunden, kein Constraint verletztVorschlag übernehmen 🎉
feasibleFunktionierende Lösung gefunden, vielleicht nicht optimalVorschlag prüfen, ggf. nochmal mit längerem Timeout
infeasibleMathematisch keine Lösung — irgendwo widersprechen sich ConstraintsDiagnose lesen, siehe unten
timeoutZeitbudget abgelaufen, keine Lösung gefundenTimeout erhöhen oder Hard-Constraints lockern
errorModell-Aufbau gescheitert (z.B. kein qualifizierter Lehrer)Stammdaten-Fehler — Bereitschafts-Check

UNSAT-Diagnose lesen

Wenn Status infeasible ist, schaut Prilog automatisch im Job-Result-Log nach Diagnose-Hinweisen. Die Diagnose-Funktion diagnoseInfeasible prüft die zwei häufigsten Ursachen:

Diagnose-Typ 1: Deputat-Engpass

Auslöser: Die Summe aller Lehrer-Caps für ein Fach reicht nicht für den Stundentafel-Bedarf.

Beispiel-Meldung:

Fach „Mathematik": 24h Bedarf, qualifizierte Lehrer mit Cap-Summe 18h:
Frau Schmidt (12h), Herr Müller (6h). Deputat erhöhen oder Quali ergänzen.

Was tun:

  1. Deputat-Wert erhöhen bei Frau Schmidt oder Herrn Müller (Sub-Panel „Deputat")
  2. Weitere Mathe-Lehrer qualifizieren (Sub-Panel „Qualifikationen")
  3. Stundentafel reduzieren in einer Klasse (1h weniger Mathe)

Diagnose-Typ 2: Verfügbarkeits-Engpass

Auslöser: Lehrer-Cap > verbleibende Slots nach Sperren.

Beispiel-Meldung:

Lehrer „Frau Müller": Cap 24h/Woche, aber nur 20 Slots nach Sperren
verfügbar (15 blockiert). Sperren reduzieren oder Cap nachjustieren.

Was tun:

  1. Sperren reduzieren in der Verfügbarkeitsmatrix
  2. Cap nachjustieren falls realistisch (Teilzeit 50% bei 28h Vollzeit-Vertrag = 14h ist OK)

Wenn keine Diagnose im Log steht

Dann ist die Ursache woanders. Häufige Fälle:

Klassen-Regelwerk widersprüchlich

Du hast „Hauptunterricht 1./2." als Tenant-Default UND „Mathe nicht nach Hauptunterricht" als Reihenfolge-Verbot. → Mathe darf dann nie in Slot 3 liegen — und das wird schnell unerfüllbar.

Check: Klassen-Regelwerk → alle aktiven Regeln durchgehen. Wenn keine widersprechen sollten, eine deaktivieren und nochmal solven.

Cell-Locks widersprüchlich

Du hast 5a Mo-1 + 5a Mo-2 + 5a Mo-3 als 🔒 alles fix mit verschiedenen Lehrern, die alle nur Mo-1 verfügbar sind.

Check: Klassen-Vorlage → fixierte Cells durchgehen. Mit 📌 Tag+Slot statt 🔒 Alles fix arbeiten lassen.

Stundentafel größer als Raster

Klasse 5a hat 38h Wochenstunden Bedarf, aber das Zeitraster bietet nur 35 Slots Mo–Fr.

Check: Bereitschafts-Diagnose → „Klassen-Slots vs. Stundentafel-Soll".

Coupling unerfüllbar

5a Mi-4 mit rel-5er, 5b Mi-4 mit rel-5er — aber 5b Mi-4 ist schon mit 🔒 Alles fix für ein anderes Fach belegt.

Check: Klassen-Vorlage → Coupling-Badge (🔗) auf allen Klassen prüfen, ob die Slots wirklich frei sind.


Score-Werte richtig interpretieren

„Score 0" ist nicht immer erreichbar

Manche Score-Buckets sind strukturell nicht-0:

  • room_changes ist mindestens (Klassen × Tage), weil jede Klasse pro Tag mindestens 1 Raum benutzt. Bei 20 Klassen × 5 Tagen = 100 Min-Wert.
  • preferred_room_mismatch kann hoch sein wenn viele Lehrer denselben Wunsch-Raum haben — nur eine Person bekommt ihn.

Beim Vergleichen mehrerer Auto-Pläne ist die Differenz wichtig, nicht der absolute Wert.

Welche Bucket-Werte sind „kritisch"?

Bucket„Kritisch" abWas es bedeutet
free_periods> 0.5 × Klassen-AnzahlMehr als eine Hohlstunde pro Klasse pro Woche im Schnitt
pref_grade_mismatch> 0.3 × Stunden-Anzahl30%+ der Stunden ignoriert Lehrer-Stufen-Wunsch
missing_preferred_tags> 5Pro 5 Stunden eine Soft-Tag-Missachtung
double_slot_misses> 2 × preferDoubleSlot-FächerDoppelstunde-Wunsch nicht erfüllt
room_changes> 2 × (Klassen × Tage)Klassen wechseln >2 Räume pro Tag im Schnitt
preferred_room_mismatch> 0.2 × Lehrer-mit-Pref-Raum-Stunden20%+ Wunsch-Raum nicht erfüllt

Diese Schwellen sind grobe Faustregeln. Solver-Output optimal ist immer akzeptabel — die Schwellen sind nur Indikatoren für „könnte besser sein".


Performance-Tipps

Zeitbudget wählen

SchulgrößeEmpfohlenes Timeout
< 100 Schüler, ≤ 10 Klassen30s
100–500 Schüler, 10–25 Klassen60–120s
500–1000 Schüler, 25–50 Klassen180–300s
> 1000 Schüler, > 50 Klassen300–600s

Faustregel: ab score.total sich nicht mehr ändert über 2 Minuten ist das Optimum vermutlich erreicht.

Was reduziert Solve-Zeit?

  • Mehr Pins: jeder fixierte Cell ist ein nicht-Such-Punkt.
  • Stammraum-Default: pinnt automatisch Klassen-Räume.
  • Strikte Qualifikationen: weniger Lehrer-Optionen pro Fach.
  • Größeres Zeitraster: mehr freie Slots pro Klasse.

Was erhöht Solve-Zeit?

  • Viele Halbgruppen mit Rotation: produziert doppelte/dreifache Plan-Slots.
  • Viele Couplings: jede Coupling-Gruppe erzeugt Constraint-Block.
  • Raum-Wechsel-Minimierung: viele BoolVars pro (Klasse × Tag × Raum).
  • Lehrer-Verfügbarkeits-Sperren: jeder Sperr-Slot ist ein Constraint.

Output verstehen — was zurückkommt

SolverResponse enthält:

  • status — siehe Tabelle oben
  • entries[] — die platzierten Stunden, jeder mit:
    • classSpaceId, subjectId, teacherId, roomId, slotId
    • weekday, orderIndex (Mo=0…Fr=4, Slot-Position im Tag)
    • weekParityboth/A/B
  • unplaced[] — Stunden, die NICHT platziert werden konnten
    • classSpaceId, subjectId, requiredHours, placedHours, reason
  • score — Soft-Score-Buckets (siehe oben)
  • log[] — Diagnose-Text, vor allem bei infeasible interessant
  • solverWallSeconds — wie lange CP-SAT gerechnet hat

Im Backend wird entries[] über den Accept-Workflow (Knopf „Vorschlag übernehmen") in TimetableEntry-Rows umgesetzt — mit planningStatus=draft.


Anhang: Architektur

  • Backend (prilog-backend-api): bauteilt das Payload in buildSolveInput (solve-job.service.ts).
  • Solver-Worker (separater pm2-Service): pollt TimetableSolveJob-Tabelle alle 2s, übernimmt queued-Jobs, ruft runSolve().
  • Solver-Service (prilog-stundenplan-solver, Python): läuft als Docker-Container prilog/stundenplan-solver:X.Y.Z-mt auf Whisper-Box. Health-Check via /health.
  • CP-SAT: Google OR-Tools, Pure-Python-Binding.
  • Job-Lifecycle: queuedrunningdone / failed / cancelled.

Versions-Anker per 2026-05-28: Solver 0.7.1-mt, Backend SHA af3a36d, Frontend Bundle index-DAcp3Df7.js.

Verwandt: