import { describe, expect, it } from 'vitest'; import { distributeGreenTimes } from '@/domain/rilsa/greenSplit'; /** * Freigabezeitverteilung, behoben in Fassung 5.9.0. * * Befund 5: Die Restzeit ging an die Phase mit dem groessten Gewicht - also * gerade an die Phase, die soeben an ihrer Hoechstfreigabezeit * gekappt worden war -, waehrend eine Phase mit freiem Spielraum * an ihrer Mindestfreigabezeit stehen blieb. * Befund 31: Die Schlussabsicherung der Mindestfreigabezeit hob einen Wert an, * nachdem die Summe bereits ausgeglichen war; Freigabe- und * Uebergangszeiten ergaben dann mehr als die Umlaufzeit. */ /** Ausloeser des Befundes 5, Fall A: Knotenpunkt mit fester Freigabezeit. */ function knotenpunktMitHandvorgabe(): ReturnType { // Feste Umlaufzeit 95 s, zwei Phasenuebergaenge zu je 10 s -> 75 s verfuegbar. // Phase 1: Kfz-Gruppe, Saettigungsgrad 0,35, "Feste Freigabezeit" 40 s // (eine Handvorgabe heisst minGreen = maxGreen = 40 s). // Phase 2: reine Fussgaengerphase. Fussgaenger tragen keine // Saettigungsverkehrsstaerke, ihr Gewicht ist deshalb 0. return distributeGreenTimes( 95, [ { id: 'p1', weight: 0.35, minGreen: 40, maxGreen: 40 }, { id: 'p2', weight: 0, minGreen: 20, maxGreen: 120 }, ], [10, 10], ); } describe('Freigabezeitverteilung: Restzeit und Sekundenraster', () => { it('gibt die Restzeit der Phase mit Spielraum statt der bereits gekappten Phase', () => { const result = knotenpunktMitHandvorgabe(); expect(result.feasible).toBe(true); // Bis hierher erhielt der Anwender 55 s und 20 s: seine eingetragenen 40 s // waren um 15 s ueberschrieben, obwohl die Fussgaengerphase bis 120 s // haette reichen duerfen. expect(result.phases[0]!.green).toBe(40); expect(result.phases[1]!.green).toBe(35); const sum = result.phases.reduce((a, p) => a + p.green, 0); expect(sum + result.transitionTime).toBe(95); }); it('behauptet nicht, alle Phasen laegen an ihrer Hoechstfreigabezeit, solange eine an ihrer Mindestfreigabezeit steht', () => { const result = knotenpunktMitHandvorgabe(); for (const note of result.notes) { expect(note.message).not.toContain('Alle Phasen erreichen ihre Höchstfreigabezeit'); } }); it('haelt die Hoechstfreigabezeit ein, solange eine andere Phase noch Spielraum hat', () => { // Einstreifige Verkehrsfuehrung, feste Umlaufzeit 300 s, zwei Uebergaenge // zu je 10 s -> 280 s verfuegbar; Vorgabe-Hoechstfreigabezeit 120 s. const result = distributeGreenTimes( 300, [ { id: 'p1', weight: 0.35, minGreen: 5, maxGreen: 120 }, { id: 'p2', weight: 0, minGreen: 20, maxGreen: 120 }, ], [10, 10], ); // Bis hierher: 260 s und 20 s - die Hoechstfreigabezeit war um 140 s // ueberschritten, und die Fussgaengerphase bekam nichts. expect(result.phases[1]!.green).toBe(120); expect(result.phases[0]!.green).toBe(160); const sum = result.phases.reduce((a, p) => a + p.green, 0); expect(sum + result.transitionTime).toBe(300); // Jetzt liegen tatsaechlich alle Phasen an ihrer Hoechstfreigabezeit; erst // hier ist die Warnung am Platz. const warnung = result.notes.find( (n) => n.code === 'freigabezeit-rest' && n.severity === 'warnung', ); expect(warnung).toBeDefined(); expect(warnung!.message).toContain('Alle Phasen erreichen ihre Höchstfreigabezeit'); }); it('nennt die Phase, die die nicht nach Verkehrsstaerken vergebene Zeit erhaelt', () => { const result = knotenpunktMitHandvorgabe(); // Seit der Fassung 5.9.0 traegt die Umlegung eine eigene Kennung: Unter // 'freigabezeit-rest' standen zwei Sachverhalte, und der Bericht setzte // ueber die Umlegung den Titel "Freigabezeit konnte nicht vollständig // verteilt werden". Die Erwartung "Phase 2" bleibt unveraendert. const hinweis = result.notes.find((n) => n.code === 'freigabezeit-rest-umgelegt'); expect(hinweis).toBeDefined(); expect(hinweis!.message).toContain('Phase 2'); }); it('bleibt bei gebrochener Mindestfreigabezeit im Sekundenraster und haelt die Umlaufzeit ein', () => { // Kfz-Phase mit Verkehrsstaerke, Fussgaengerphase mit eingetragener // Mindestfreigabezeit 12,5 s; feste Umlaufzeit 60 s, Uebergangszeiten // 2 s und 7 s -> 51 s verfuegbar. const result = distributeGreenTimes( 60, [ { id: 'p1', weight: 0.4, minGreen: 5 }, { id: 'p2', weight: 0, minGreen: 12.5 }, ], [2, 7], ); expect(result.feasible).toBe(true); // Bis hierher: 39 s und 12,5 s - zusammen 51,5 s statt 51 s. Der Plan lief // 60,5 s statt der eingetragenen 60 s, und 12,5 s sind nicht schaltbar. for (const phase of result.phases) { expect(Number.isInteger(phase.green)).toBe(true); } expect(result.phases[0]!.green).toBe(38); expect(result.phases[1]!.green).toBe(13); const sum = result.phases.reduce((a, p) => a + p.green, 0); expect(sum).toBe(result.availableGreen); expect(sum + result.transitionTime).toBe(60); }); it('rundet eine gebrochene Mindestfreigabezeit einmal auf und nicht zweimal', () => { // Vier Phasen, feste Umlaufzeit 76 s, vier Phasenuebergaenge zu je 5 s // -> 56 s verfuegbar. Phase 1 ist eine Fussgaengerphase (Gewicht 0) mit // eingetragener Mindestfreigabezeit 12,5 s, die drei Kfz-Phasen tragen je // den Saettigungsgrad 0,3. const result = distributeGreenTimes( 76, [ { id: 'p1', weight: 0, minGreen: 12.5 }, { id: 'p2', weight: 0.3, minGreen: 5 }, { id: 'p3', weight: 0.3, minGreen: 5 }, { id: 'p4', weight: 0.3, minGreen: 5 }, ], [5, 5, 5, 5], ); // Die Phase steht nach dem Aufrunden bereits eine halbe Sekunde ueber // ihrem Ausgangswert. Zaehlt der Summenausgleich sie trotzdem zu den // Phasen mit dem groessten Rest, bekommt sie eine zweite Sekunde: Der // Anwender traegt 12,5 s ein und liest 14 s, waehrend einer Kfz-Phase // ohne Grund eine Sekunde fehlt. expect(result.phases.map((p) => p.green)).toEqual([13, 15, 14, 14]); const sum = result.phases.reduce((a, p) => a + p.green, 0); expect(sum + result.transitionTime).toBe(76); }); it('laesst eine aufgerundete Handvorgabe im Summenausgleich in Ruhe', () => { // Fuenf Phasen, verfuegbar 47 s. Phase 1 traegt eine "Feste Freigabezeit" // von 5,5 s (minGreen = maxGreen), die vier uebrigen je 0,3. const result = distributeGreenTimes( 47, [ { id: 'p1', weight: 0, minGreen: 5.5, maxGreen: 5.5 }, { id: 'p2', weight: 0.3, minGreen: 5 }, { id: 'p3', weight: 0.3, minGreen: 5 }, { id: 'p4', weight: 0.3, minGreen: 5 }, { id: 'p5', weight: 0.3, minGreen: 5 }, ], [], ); // 5,5 s sind nicht schaltbar und werden zu 6 s. Eine zweite Sekunde // obendrauf - 7 s bei eingetragenen 5,5 s - waere kein Sekundenraster // mehr, sondern eine Aufstockung. expect(result.phases.map((p) => p.green)).toEqual([6, 11, 10, 10, 10]); }); it('meldet eine zu kurze Umlaufzeit, wenn gebrochene Vorgaben im Sekundenraster nicht hineinpassen', () => { // Zwei feste Freigabezeiten 20,5 s und 19,5 s bei 60 s Umlauf und 20 s // Uebergangszeit. Im Sekundenraster braucht das 21 s und 20 s, also 41 s - // 1 s mehr, als verfuegbar ist. const result = distributeGreenTimes( 60, [ { id: 'p1', weight: 0.4, minGreen: 20.5, maxGreen: 20.5 }, { id: 'p2', weight: 0.2, minGreen: 19.5, maxGreen: 19.5 }, ], [10, 10], ); // Bis hierher: 21 s und 19,5 s, zusammen 40,5 s - der Plan lief still 60,5 s. expect(result.feasible).toBe(false); expect(result.notes.some((n) => n.code === 'umlaufzeit-zu-kurz')).toBe(true); for (const phase of result.phases) { expect(Number.isInteger(phase.green)).toBe(true); } }); });