plexams.go löst an zwei Stellen ein kombinatorisches Zuordnungsproblem, das sich
nicht mit einer einfachen „nimm der Reihe nach"-Heuristik zuverlässig lösen lässt:
- Aufsichtenplanung — Aufsichten (invigilations) auf Prüfungs-Slots/Räume verteilen → Simulated Annealing (plexams/invigplan/optimizer.go).
- Vorplanung SEB/EXaHM — vorgeplante Prüfungen auf MUC.DAI-Slots mit gebuchten Anny-Räumen verteilen → DSATUR-Konstruktion + SA-Reparatur (plexams/preplan_solve.go).
Beide teilen dieselbe Grundidee: ein Startplan wird konstruktiv erzeugt und dann durch lokale Veränderungen verbessert, wobei harte Nebenbedingungen zu jedem Zeitpunkt eingehalten werden und weiche Ziele über eine Kostenfunktion gegeneinander abgewogen werden.
- Harte Nebenbedingung (hard constraint): muss immer gelten (z. B. eine Aufsicht kann nicht gleichzeitig an zwei Orten sein; gleicher Studiengang nie im selben Slot). Ein Plan, der eine harte Bedingung verletzt, ist ungültig.
- Weiche Nebenbedingung (soft constraint): soll möglichst gelten (z. B. Aufsichten fair verteilen; gleichen Studiengang auf verschiedene Tage legen). Verletzungen kosten „Strafpunkte", werden aber toleriert, wenn nötig.
- Kostenfunktion: Summe aller gewichteten Strafpunkte eines Plans. Je kleiner, desto besser. Der Optimierer sucht ein Minimum.
- Move (Nachbarschaft): eine kleine Änderung an einem Plan (eine Zuordnung umsetzen, tauschen, entfernen). Die Menge der erreichbaren Pläne bestimmt, wie gut die Suche entkommen kann.
Simulated Annealing (SA) ist eine lokale Suche, die dem physikalischen Abkühlen („annealing") eines Metalls nachempfunden ist: bei hoher „Temperatur" werden auch verschlechternde Schritte akzeptiert (um lokalen Minima zu entkommen), bei sinkender Temperatur immer seltener, bis die Suche am Ende nur noch Verbesserungen annimmt.
Ablauf — Optimize in optimizer.go
-
Startplan (greedy):
Greedyfüllt die offenen Positionen am-stärksten- eingeschränkt zuerst (die Position mit den wenigsten in Frage kommenden Aufsichten) und wählt jeweils die zulässige Aufsicht, die ihrem Soll-Minuten-Ziel am weitesten hinterherhinkt. So ist der Start schon fair und hart-zulässig. -
Iterieren: in jeder Runde wird ein zufälliger Move vorgeschlagen (
proposeMove):- 70 % eine Position neu zuordnen,
- 10 % eine Position freigeben (unassign),
- 20 % zwei Positionen tauschen.
-
Zulässigkeit zuerst: der Move wird angewendet und sofort gegen die harten Bedingungen geprüft (
feasible/Registry.Allows). Verletzt er eine, wird er rückgängig gemacht (undo) — der Plan bleibt also immer hart-zulässig. -
Akzeptanzregel (Metropolis): ist
delta = neueKosten − alteKosten:delta ≤ 0(besser oder gleich): immer akzeptieren.delta > 0(schlechter): nur mit Wahrscheinlichkeitexp(−delta / T)akzeptieren. Bei hoher TemperaturTist das oft, bei niedriger fast nie.
-
Abkühlung:
temperatureist ein geometrisches Schema vonStartTempaufEndTemp. Anfangs darf die Suche „bergauf" wandern, am Ende wird sie gierig. -
Bestplan merken / früher Abbruch: der beste je gesehene Plan wird festgehalten. Mit
StopOnBalanceendet die Suche vorzeitig, sobald sie konvergiert ist — d. h. die Temperatur ist nahe am Boden, der Bestplan hat sich seitStagnationLimitRunden nicht verbessert, und der Plan ist ausbalanciert und vollständig belegt.
Die Aufsichtenverteilung hat viele, teils gegenläufige weiche Ziele (Fairness der Minuten, Verteilung über Tage, Spannweite, …) bei vielen harten Bedingungen. Eine rein gierige Lösung bleibt leicht in einem mittelmäßigen lokalen Optimum hängen; SA kann durch das gelegentliche Akzeptieren schlechterer Zwischenschritte entkommen und findet in der Praxis deutlich bessere Pläne.
Die Vorplanung verteilt vorgeplante Prüfungen (PreplanExam) auf die MUC.DAI-Slots,
für die wir bereits Anny-Räume gebucht haben. Formal ist es Bin-Packing mit weichen
Konflikten (verwandt mit Graphfärbung / Timetabling):
- Knoten: jede Prüfung (bzw. eine
same-slot-Gruppe als eine unteilbare Einheit). - Behälter (Slot): jeder reguläre Prüfungs-Slot (08:30/10:30/…), in dem wir
Anny-Räume gebucht haben — nicht nur die MUC.DAI-Slots. Sitzplatz-Limit =
preplanCapacityFactor× gebuchte physische Plätze (Default 1.0 = randvoll). Das ist die einzige harte Schranke. - MUC.DAI-Slots (reserviert): Prüfungen mit einem MUC.DAI-Studiengang (DE/GS/ID)
dürfen nur in (gebuchte) MUC.DAI-Slots; alle anderen Prüfungen in jeden gebuchten
Slot (
preplanUnit.allowedSlots). - Same-slot (hart): manche Prüfungen müssen zusammen (z. B. beide Varianten von „Betriebssysteme I") — sie werden zu einer Einheit verschmolzen.
- Studiengang-Überschneidung (weich, distanzbasiert): zwei Prüfungen desselben
Studiengangs dürfen im selben Slot liegen (verschiedene Anny-Räume) — erlaubt, weil
es nicht zwingend dieselben Studierenden sind. Es wird aber über die zeitliche
Distanz bestraft (
proximityPenalty): voll bei selbem Slot, weniger je größer der Slot-Abstand am selben Tag, 0 sobald an verschiedenen Tagen. So gilt „möglichst verschiedene Tage; wenn selber Tag, maximale Slot-Entfernung". - Explizit „nicht gleichzeitig" (weich, stärker): für Paare, die dieselben
Studierenden betreffen, ohne dass der Studiengang das zeigt, kann pro Prüfung ein
Konfliktpartner gesetzt werden (
PreplanExam.NotSameSlot, MutationsetPreplanExamNotSameSlot, symmetrisch). Gleiche Distanzlogik, aber mit dem höheren GewichtpreplanExplicitConflictWeight. - Explizit „darf zusammen / direkt nacheinander" (Ausnahme): teilen sich zwei
Prüfungen einen Studiengang, betreffen aber nicht dieselben Studierenden (z. B. zwei
Wirtschaftsinformatik-Prüfungen), kann das Paar von der Spreizung ausgenommen werden
(
PreplanExam.CanShareSlot, MutationsetPreplanExamCanShareSlot, symmetrisch) — die Studiengang-Strafe entfällt dann für genau dieses Paar, sie dürfen also denselben Slot (sofern die Kapazität reicht) oder direkt benachbarte Slots belegen. - Priorität: alle EXaHM und große SEB werden bevorzugt platziert und nie zugunsten kleinerer fallen gelassen (hoher Drop-Kostenzuschlag). Kleine SEB (die in einen einzelnen R-Bau-Laborraum passen, Größe ≤ größter Nicht-Anny-SEB-Raum) werden gar nicht erst in Anny gelegt, sondern mit der Anmerkung „im R-Bau planen" markiert.
- Fixiert: Prüfungen mit
isFixedbehalten ihren Slot und werden vorbelegt.
Die frühere Lösung war First-Fit, größte zuerst: jede Prüfung in den ersten Slot, der gerade passt. Das ist eine der schwächsten Heuristiken. Sobald die ersten Slots „verstopft" waren, fanden spätere Prüfungen keinen Slot mehr — obwohl eine andere Reihenfolge mehr untergebracht hätte.
Wichtig: Wenn der Gesamtbedarf die gebuchte Anny-Kapazität übersteigt, kann kein Algorithmus alles unterbringen — dann müssen erst mehr Anny-Slots gebucht werden. Der Algorithmus platziert in dem Fall alle EXaHM + die größten SEB und meldet den Engpass. (Anfänglich wurden in
Test26SSzu wenige Slots erkannt, weil die Kandidaten fälschlich auf MUC.DAI-Slots beschränkt waren — siehe oben: jetzt zählen alle gebuchten regulären Slots.)
DSATUR (degree of saturation) färbt immer den am stärksten eingeschränkten Knoten zuerst — nicht den größten. Hier: die Einheit mit den wenigsten Slots, in die sie kapazitiv noch passt.
In solvePreplan (preplan_solve.go):
- Berechne für jede noch nicht platzierte Einheit die Menge der Slots, in die sie kapazitiv passt.
- Wähle die Einheit mit den wenigsten passenden Slots zuerst (
dsaturBefore); bei Gleichstand: höhere Priorität (EXaHM/große SEB über die Drop-Kosten), dann kleinere ID — deterministisch. - Platziere sie im Slot, der die geringste Konflikt-Nähe verursacht (
chooseSlotüberproximityPenalty), bei Gleichstand in den mit der meisten Restkapazität. - Passt eine Einheit in keinen Slot, wird sie übersprungen und der SA-Phase überlassen.
DSATUR legt damit gezielt die „schwierigen" (großen) Prüfungen früh und packt den Rest dazu.
Nach DSATUR läuft immer eine SA-Phase — nicht nur, um Übriggebliebenes zu platzieren, sondern auch, um die weiche Spreizung zu optimieren (DSATUR-Greedy allein verteilt gleiche Studiengänge sonst nicht gut genug). Prinzip wie bei der Aufsichtenplanung, aber eigenständig:
- Kostenfunktion: Summe der Drop-Kosten aller nicht platzierten Einheiten plus
der distanzbasierten Konflikt-Nähe (
proximityPenalty) über alle konfligierenden Paare (Studiengang oder explizit). Die Drop-Kosten dominieren, also wird zuerst platziert; EXaHM (preplanExahmKeep) und große SEB sind nie die Fallenden. - Zwei Move-Typen: (1) Swap — zwei platzierte Einheiten tauschen ihre Slots
(alle bleiben platziert; optimiert die Spreizung in vollen Plänen); (2) Relocate
mit Ejection — eine Einheit in einen Slot verschieben; reicht die Kapazität
nicht, werden bis zu
preplanEjectDepth(= 3) der kleinsten dortigen Einheiten hinausgeworfen. Fixierte Belegungen werden nie verschoben/geworfen. - Akzeptanz & Abkühlung:
delta ≤ 0immer, sonst mitexp(−delta/T);Tkühlt geometrisch vonpreplanSAStartTempaufpreplanSAEndTemp. Der beste je gesehene Zustand wird zurückgegeben. Fester Seed (rand.NewSource(1)) → deterministisch.
Bewusst keine Wiederverwendung der invigplan-SA-Engine: deren Problem/Registry
ist auf Aufsichten zugeschnitten. Die Vorplanungs-Reparatur ist im selben Geist, aber
kompakt und in sich geschlossen. Eine generische „items→slots"-Engine ist erst
geplant, wenn der Terminplan-Generator als zweiter echter Abnehmer dazukommt (siehe
cli-to-gui-migration-plan.md).
Alle drei Planer trennen fix von zu optimieren: die Aufsichtenplanung über
Problem.Fixed, die Vorplanung über isFixed/vorbelegte Slots. Der künftige
Terminplan-Generator (eigener Schritt) wird dieselbe Linie nutzen:
Ein gesperrter Plan-Eintrag (
PlanEntry.Locked == true) ist hart fixiert — der Generator lässt ihn unverändert und optimiert nur die übrigen Prüfungen in die Slots.
„Echtes Vorplanen" einer generierten Prüfung = ein gesperrter Plan-Eintrag. Das passiert
heute schon an zwei Stellen, beide erzeugen Locked=true:
- Connect einer fixen Pre-Exam (
isFixed): die verknüpfte ZPA-Prüfung wird in ihren Slot gesperrt vorgeplant; Disconnect entfernt den Eintrag wieder. lockExam/unlockExam: generische pin/unpin-Operation für jede generierte Prüfung (auch manuell im GUI).
So muss der Generator später nur die gesperrten Einträge als feste Menge einlesen — kein neues Modell nötig.
Aufsichtenplanung — DefaultOptions() in optimizer.go
| Parameter | Default | Wirkung |
|---|---|---|
Iterations |
1 000 000 | Obergrenze der Suchschritte. |
StartTemp |
20 000 | Anfangstemperatur — wie „mutig" am Start. |
EndTemp |
0.5 | Endtemperatur — am Ende nahezu gierig. |
Seed |
1 | Zufalls-Seed (Reproduzierbarkeit). |
StopOnBalance |
true | Früher Abbruch nach Konvergenz. |
StagnationLimit |
30 000 | Runden ohne Bestplan-Verbesserung vor Abbruch. |
Vorplanung — Konstanten in preplan_solve.go
| Konstante | Default | Wirkung |
|---|---|---|
preplanDropBase |
10 000 | Grundkosten, eine Einheit nicht zu platzieren. |
preplanExahmKeep |
1 000 000 | Zuschlag, damit EXaHM nie gedroppt wird. |
preplanProgramConflictWeight |
100 | Basisstrafe für gemeinsamen Studiengang (× Nähe). |
preplanExplicitConflictWeight |
1 000 | Basisstrafe für explizites „nicht gleichzeitig" (× Nähe). |
preplanSAIterations |
20 000 | Schritte der SA-Suche. |
preplanSAStartTemp |
20 000 | Anfangstemperatur. |
preplanSAEndTemp |
1 | Endtemperatur. |
preplanEjectDepth |
3 | Max. Einheiten, die ein Relocate-Move aus einem Slot wirft. |
Und in preplan_assign.go:
| Konstante | Default | Wirkung |
|---|---|---|
preplanCapacityFactor |
1.0 | Nutzbarer Anteil der gebuchten Sitzplätze (1.0 = randvoll). |
Der Vorplanungs-Solver ist deterministisch und direkt testbar:
plexams/preplan_solve_test.go prüft u. a., dass bei
ausreichender Kapazität alle Einheiten platziert werden (auch bei gemeinsamem
Studiengang), dass bei Engpass die kleinste SEB gedroppt wird und EXaHM/große SEB
bleiben, die Spreizung gleicher Studiengänge bzw. expliziter „nicht gleichzeitig"-Paare
über Slots und Tage, die proximityPenalty-Distanzkurve sowie die allowedSlots-
Restriktion (MUC.DAI). Der invigplan-Optimizer hat eigene Tests im selben Paket.