lsa-planer

LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.

/ tests domain freigabezeitverteilung.test.ts

7,9 KB Rohdatei
tests/domain/freigabezeitverteilung.test.ts — 187 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import { distributeGreenTimes } from '@/domain/rilsa/greenSplit';
3
4 /**
5 * Freigabezeitverteilung, behoben in Fassung 5.9.0.
6 *
7 * Befund 5: Die Restzeit ging an die Phase mit dem groessten Gewicht - also
8 * gerade an die Phase, die soeben an ihrer Hoechstfreigabezeit
9 * gekappt worden war -, waehrend eine Phase mit freiem Spielraum
10 * an ihrer Mindestfreigabezeit stehen blieb.
11 * Befund 31: Die Schlussabsicherung der Mindestfreigabezeit hob einen Wert an,
12 * nachdem die Summe bereits ausgeglichen war; Freigabe- und
13 * Uebergangszeiten ergaben dann mehr als die Umlaufzeit.
14 */
15
16 /** Ausloeser des Befundes 5, Fall A: Knotenpunkt mit fester Freigabezeit. */
17 function knotenpunktMitHandvorgabe(): ReturnType<typeof distributeGreenTimes> {
18 // Feste Umlaufzeit 95 s, zwei Phasenuebergaenge zu je 10 s -> 75 s verfuegbar.
19 // Phase 1: Kfz-Gruppe, Saettigungsgrad 0,35, "Feste Freigabezeit" 40 s
20 // (eine Handvorgabe heisst minGreen = maxGreen = 40 s).
21 // Phase 2: reine Fussgaengerphase. Fussgaenger tragen keine
22 // Saettigungsverkehrsstaerke, ihr Gewicht ist deshalb 0.
23 return distributeGreenTimes(
24 95,
25 [
26 { id: 'p1', weight: 0.35, minGreen: 40, maxGreen: 40 },
27 { id: 'p2', weight: 0, minGreen: 20, maxGreen: 120 },
28 ],
29 [10, 10],
30 );
31 }
32
33 describe('Freigabezeitverteilung: Restzeit und Sekundenraster', () => {
34 it('gibt die Restzeit der Phase mit Spielraum statt der bereits gekappten Phase', () => {
35 const result = knotenpunktMitHandvorgabe();
36
37 expect(result.feasible).toBe(true);
38 // Bis hierher erhielt der Anwender 55 s und 20 s: seine eingetragenen 40 s
39 // waren um 15 s ueberschrieben, obwohl die Fussgaengerphase bis 120 s
40 // haette reichen duerfen.
41 expect(result.phases[0]!.green).toBe(40);
42 expect(result.phases[1]!.green).toBe(35);
43 const sum = result.phases.reduce((a, p) => a + p.green, 0);
44 expect(sum + result.transitionTime).toBe(95);
45 });
46
47 it('behauptet nicht, alle Phasen laegen an ihrer Hoechstfreigabezeit, solange eine an ihrer Mindestfreigabezeit steht', () => {
48 const result = knotenpunktMitHandvorgabe();
49
50 for (const note of result.notes) {
51 expect(note.message).not.toContain('Alle Phasen erreichen ihre Höchstfreigabezeit');
52 }
53 });
54
55 it('haelt die Hoechstfreigabezeit ein, solange eine andere Phase noch Spielraum hat', () => {
56 // Einstreifige Verkehrsfuehrung, feste Umlaufzeit 300 s, zwei Uebergaenge
57 // zu je 10 s -> 280 s verfuegbar; Vorgabe-Hoechstfreigabezeit 120 s.
58 const result = distributeGreenTimes(
59 300,
60 [
61 { id: 'p1', weight: 0.35, minGreen: 5, maxGreen: 120 },
62 { id: 'p2', weight: 0, minGreen: 20, maxGreen: 120 },
63 ],
64 [10, 10],
65 );
66
67 // Bis hierher: 260 s und 20 s - die Hoechstfreigabezeit war um 140 s
68 // ueberschritten, und die Fussgaengerphase bekam nichts.
69 expect(result.phases[1]!.green).toBe(120);
70 expect(result.phases[0]!.green).toBe(160);
71 const sum = result.phases.reduce((a, p) => a + p.green, 0);
72 expect(sum + result.transitionTime).toBe(300);
73 // Jetzt liegen tatsaechlich alle Phasen an ihrer Hoechstfreigabezeit; erst
74 // hier ist die Warnung am Platz.
75 const warnung = result.notes.find(
76 (n) => n.code === 'freigabezeit-rest' && n.severity === 'warnung',
77 );
78 expect(warnung).toBeDefined();
79 expect(warnung!.message).toContain('Alle Phasen erreichen ihre Höchstfreigabezeit');
80 });
81
82 it('nennt die Phase, die die nicht nach Verkehrsstaerken vergebene Zeit erhaelt', () => {
83 const result = knotenpunktMitHandvorgabe();
84
85 // Seit der Fassung 5.9.0 traegt die Umlegung eine eigene Kennung: Unter
86 // 'freigabezeit-rest' standen zwei Sachverhalte, und der Bericht setzte
87 // ueber die Umlegung den Titel "Freigabezeit konnte nicht vollständig
88 // verteilt werden". Die Erwartung "Phase 2" bleibt unveraendert.
89 const hinweis = result.notes.find((n) => n.code === 'freigabezeit-rest-umgelegt');
90 expect(hinweis).toBeDefined();
91 expect(hinweis!.message).toContain('Phase 2');
92 });
93
94 it('bleibt bei gebrochener Mindestfreigabezeit im Sekundenraster und haelt die Umlaufzeit ein', () => {
95 // Kfz-Phase mit Verkehrsstaerke, Fussgaengerphase mit eingetragener
96 // Mindestfreigabezeit 12,5 s; feste Umlaufzeit 60 s, Uebergangszeiten
97 // 2 s und 7 s -> 51 s verfuegbar.
98 const result = distributeGreenTimes(
99 60,
100 [
101 { id: 'p1', weight: 0.4, minGreen: 5 },
102 { id: 'p2', weight: 0, minGreen: 12.5 },
103 ],
104 [2, 7],
105 );
106
107 expect(result.feasible).toBe(true);
108 // Bis hierher: 39 s und 12,5 s - zusammen 51,5 s statt 51 s. Der Plan lief
109 // 60,5 s statt der eingetragenen 60 s, und 12,5 s sind nicht schaltbar.
110 for (const phase of result.phases) {
111 expect(Number.isInteger(phase.green)).toBe(true);
112 }
113 expect(result.phases[0]!.green).toBe(38);
114 expect(result.phases[1]!.green).toBe(13);
115 const sum = result.phases.reduce((a, p) => a + p.green, 0);
116 expect(sum).toBe(result.availableGreen);
117 expect(sum + result.transitionTime).toBe(60);
118 });
119
120 it('rundet eine gebrochene Mindestfreigabezeit einmal auf und nicht zweimal', () => {
121 // Vier Phasen, feste Umlaufzeit 76 s, vier Phasenuebergaenge zu je 5 s
122 // -> 56 s verfuegbar. Phase 1 ist eine Fussgaengerphase (Gewicht 0) mit
123 // eingetragener Mindestfreigabezeit 12,5 s, die drei Kfz-Phasen tragen je
124 // den Saettigungsgrad 0,3.
125 const result = distributeGreenTimes(
126 76,
127 [
128 { id: 'p1', weight: 0, minGreen: 12.5 },
129 { id: 'p2', weight: 0.3, minGreen: 5 },
130 { id: 'p3', weight: 0.3, minGreen: 5 },
131 { id: 'p4', weight: 0.3, minGreen: 5 },
132 ],
133 [5, 5, 5, 5],
134 );
135
136 // Die Phase steht nach dem Aufrunden bereits eine halbe Sekunde ueber
137 // ihrem Ausgangswert. Zaehlt der Summenausgleich sie trotzdem zu den
138 // Phasen mit dem groessten Rest, bekommt sie eine zweite Sekunde: Der
139 // Anwender traegt 12,5 s ein und liest 14 s, waehrend einer Kfz-Phase
140 // ohne Grund eine Sekunde fehlt.
141 expect(result.phases.map((p) => p.green)).toEqual([13, 15, 14, 14]);
142 const sum = result.phases.reduce((a, p) => a + p.green, 0);
143 expect(sum + result.transitionTime).toBe(76);
144 });
145
146 it('laesst eine aufgerundete Handvorgabe im Summenausgleich in Ruhe', () => {
147 // Fuenf Phasen, verfuegbar 47 s. Phase 1 traegt eine "Feste Freigabezeit"
148 // von 5,5 s (minGreen = maxGreen), die vier uebrigen je 0,3.
149 const result = distributeGreenTimes(
150 47,
151 [
152 { id: 'p1', weight: 0, minGreen: 5.5, maxGreen: 5.5 },
153 { id: 'p2', weight: 0.3, minGreen: 5 },
154 { id: 'p3', weight: 0.3, minGreen: 5 },
155 { id: 'p4', weight: 0.3, minGreen: 5 },
156 { id: 'p5', weight: 0.3, minGreen: 5 },
157 ],
158 [],
159 );
160
161 // 5,5 s sind nicht schaltbar und werden zu 6 s. Eine zweite Sekunde
162 // obendrauf - 7 s bei eingetragenen 5,5 s - waere kein Sekundenraster
163 // mehr, sondern eine Aufstockung.
164 expect(result.phases.map((p) => p.green)).toEqual([6, 11, 10, 10, 10]);
165 });
166
167 it('meldet eine zu kurze Umlaufzeit, wenn gebrochene Vorgaben im Sekundenraster nicht hineinpassen', () => {
168 // Zwei feste Freigabezeiten 20,5 s und 19,5 s bei 60 s Umlauf und 20 s
169 // Uebergangszeit. Im Sekundenraster braucht das 21 s und 20 s, also 41 s -
170 // 1 s mehr, als verfuegbar ist.
171 const result = distributeGreenTimes(
172 60,
173 [
174 { id: 'p1', weight: 0.4, minGreen: 20.5, maxGreen: 20.5 },
175 { id: 'p2', weight: 0.2, minGreen: 19.5, maxGreen: 19.5 },
176 ],
177 [10, 10],
178 );
179
180 // Bis hierher: 21 s und 19,5 s, zusammen 40,5 s - der Plan lief still 60,5 s.
181 expect(result.feasible).toBe(false);
182 expect(result.notes.some((n) => n.code === 'umlaufzeit-zu-kurz')).toBe(true);
183 for (const phase of result.phases) {
184 expect(Number.isInteger(phase.green)).toBe(true);
185 }
186 });
187 });