lsa-planer

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

/ tests domain hbsWartezeit.test.ts

15,7 KB Rohdatei
tests/domain/hbsWartezeit.test.ts — 410 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import { abflusszeit, computeCapacity, saturationFlow } from '@/domain/rilsa/capacity';
3 import {
4 computeDelay,
5 computeFussRadDelay,
6 hbsReststau,
7 kfzServiceLevel,
8 serviceLevelFor,
9 sperrzeitenAusFenstern,
10 } from '@/domain/rilsa/delay';
11 import { RILSA_DEFAULTS, SERVICE_LEVEL_SCALES } from '@/domain/rilsa/constants';
12
13 /*
14 * Befunde B1 bis B4.
15 *
16 * B2: Die Wartezeit wurde fuer beide Einstellungen nach der HCM-Formel
17 * gerechnet, waehrend der Ausdruck "Bewertung nach HBS 2015" behauptete.
18 * Jetzt rechnet die Einstellung HBS das Verfahren des HBS 2015 (Leyn 2016,
19 * Gl. 2-7 bis 2-10 und 3-2). Als Referenz dient ein LISA+-Ausdruck
20 * "HBS-Bewertung 2015" (Kornwestheim, 17.01.2017, tU = 90 s, T = 3600 s),
21 * dessen Zeilen sich mit der Formelkette nachrechnen lassen:
22 *
23 * SG tF tA fA q qS C x tW NGE QSV
24 * K5 10 11 0,122 167 1992 243 0,687 59,019 1,428 D
25 * K7 6 7 0,078 101 1986 155 0,652 67,291 1,162 D
26 * K2/M 7 8 0,089 88 1949 173 0,509 52,000 0,619 D
27 *
28 * LISA+ rundet die Kapazitaet auf ganze Kfz/h und bildet x = q / C_gerundet;
29 * die Rueckstauformel trifft mit dem Vorgabewert des Instationaritaetsfaktors
30 * fin = 1,1 (FGSV-Dialog zum HBS 2015, Kapitel S4).
31 */
32
33 const LISA_REFERENZ = [
34 {
35 name: 'K5',
36 tF: 10,
37 tA: 11,
38 q: 167,
39 qS: 1992,
40 C: 243,
41 x: 0.687,
42 tW: 59.019,
43 NGE: 1.428,
44 qsv: 'D',
45 },
46 {
47 name: 'K7',
48 tF: 6,
49 tA: 7,
50 q: 101,
51 qS: 1986,
52 C: 155,
53 x: 0.652,
54 tW: 67.291,
55 NGE: 1.162,
56 qsv: 'D',
57 },
58 { name: 'K2/M', tF: 7, tA: 8, q: 88, qS: 1949, C: 173, x: 0.509, tW: 52.0, NGE: 0.619, qsv: 'D' },
59 ] as const;
60
61 describe('HBS-2015-Wartezeitverfahren (Befund B2)', () => {
62 it('rechnet die Abflusszeit tA = tF + 1 s je Freigabezeitfenster, gedeckelt auf tU', () => {
63 expect(abflusszeit(10, 90, { verfahren: 'HBS' })).toBe(11);
64 expect(abflusszeit(10, 90, { verfahren: 'HBS', freigabefenster: 2 })).toBe(12);
65 expect(abflusszeit(89.5, 90, { verfahren: 'HBS' })).toBe(90);
66 expect(abflusszeit(0, 90, { verfahren: 'HBS' })).toBe(0);
67 // HCM: die Freigabezeit selbst.
68 expect(abflusszeit(10, 90, { verfahren: 'HCM' })).toBe(10);
69 });
70
71 it('trifft Kapazitaet, Auslastungsgrad, Rueckstau und Wartezeit des LISA+-Ausdrucks', () => {
72 for (const zeile of LISA_REFERENZ) {
73 const kapazitaet = computeCapacity(zeile.tF, 90, zeile.qS, zeile.q, { verfahren: 'HBS' });
74 expect(kapazitaet.abflusszeit, `${zeile.name}: tA`).toBe(zeile.tA);
75 // K5 und K2/M treffen exakt. K7: 1986 * 7 / 90 = 154,47 -> 154, LISA+
76 // druckt 155; mit dem gedruckten (gerundeten) qS ist das nicht
77 // reproduzierbar, die Ursache bleibt offen (Toleranz 1 Kfz/h). Auf die
78 // Wartezeitkette hat es keinen Einfluss - sie wird unten mit dem
79 // LISA+-C nachgerechnet.
80 expect(Math.abs(kapazitaet.capacity - zeile.C), `${zeile.name}: C`).toBeLessThanOrEqual(1);
81 expect(kapazitaet.degreeOfSaturation, `${zeile.name}: x`).toBeCloseTo(zeile.x, 2);
82
83 // Die Wartezeitkette wird mit der von LISA+ ausgewiesenen Kapazitaet
84 // nachgerechnet - so, wie ein Pruefer es mit dem Ausdruck taete.
85 const wartezeit = computeDelay({
86 cycleTime: 90,
87 greenTime: zeile.tF,
88 capacity: zeile.C,
89 demand: zeile.q,
90 verfahren: 'HBS',
91 mode: 'kfz',
92 abflusszeit: kapazitaet.abflusszeit,
93 });
94 expect(wartezeit.rechenweg.instationaritaetsfaktor).toBe(1.1);
95 expect(wartezeit.rechenweg.betrachtungszeitraum).toBe(1);
96 expect(wartezeit.rechenweg.reststau, `${zeile.name}: NGE`).toBeCloseTo(zeile.NGE, 2);
97 // Auf eine Zehntelsekunde: LISA+ druckt drei Nachkommastellen aus
98 // ungerundeten Zwischenwerten, das Programm rundet C und tW.
99 expect(Math.abs(wartezeit.averageDelay - zeile.tW), `${zeile.name}: tW`).toBeLessThanOrEqual(
100 0.1,
101 );
102 expect(wartezeit.serviceLevel?.grade, `${zeile.name}: QSV`).toBe(zeile.qsv);
103 expect(wartezeit.serviceLevel?.scale).toBe('HBS');
104 expect(wartezeit.serviceLevel?.kriterium).toBe('mittlere-wartezeit');
105 }
106 });
107
108 it('bildet den Rueckstau als groesseren von Instationaritaets- und Akcelik-Term', () => {
109 // Bei fin = 1 gewinnt der Akcelik-Term (T*C statt 0,58*T*C).
110 const akcelik = (x: number, C: number): number =>
111 (C / 4) * (x - 1 + Math.sqrt((x - 1) ** 2 + (4 * x) / C));
112 expect(hbsReststau(0.8, 600, 1, 1)).toBeCloseTo(akcelik(0.8, 600), 9);
113 // Mit fin > 1 kann der erste Term ueberwiegen - er ist nie kleiner als 0.
114 expect(hbsReststau(0.9, 600, 1, 1.3)).toBeGreaterThan(akcelik(0.9, 600));
115 expect(hbsReststau(0.1, 600, 1, 1.1)).toBeGreaterThanOrEqual(0);
116 });
117
118 it('liefert bei hoher Auslastung deutlich laengere Wartezeiten als das HCM-Verfahren', () => {
119 // Die Kernaussage von Befund B2: Die HCM-Formel (T = 0,25 h) unterschaetzt
120 // die Wartezeit im Bereich x ab etwa 0,9 - genau dort, wo die Stufe kippt.
121 const gemeinsam = { cycleTime: 90, greenTime: 30, demand: 620, mode: 'kfz' as const };
122 const hbsKap = computeCapacity(30, 90, 2000, 620, { verfahren: 'HBS' });
123 const hcmKap = computeCapacity(30, 90, 2000, 620, { verfahren: 'HCM' });
124 const hbs = computeDelay({
125 ...gemeinsam,
126 capacity: hbsKap.capacity,
127 verfahren: 'HBS',
128 abflusszeit: hbsKap.abflusszeit,
129 });
130 const hcm = computeDelay({
131 ...gemeinsam,
132 capacity: hcmKap.capacity,
133 verfahren: 'HCM',
134 abflusszeit: hcmKap.abflusszeit,
135 });
136 expect(hcm.degreeOfSaturation).toBeGreaterThan(0.9);
137 expect(hbs.averageDelay).toBeGreaterThan(hcm.averageDelay * 1.2);
138 expect(hbs.verfahren).toBe('HBS');
139 expect(hcm.verfahren).toBe('HCM');
140 expect(hcm.rechenweg.betrachtungszeitraum).toBe(0.25);
141 expect(hcm.rechenweg.hcmK).toBe(0.5);
142 });
143
144 it('haelt die Anteile konsistent: tW = tW,G + tW,R', () => {
145 const wartezeit = computeDelay({
146 cycleTime: 90,
147 greenTime: 30,
148 capacity: 689,
149 demand: 400,
150 verfahren: 'HBS',
151 mode: 'kfz',
152 });
153 expect(wartezeit.averageDelay).toBeCloseTo(
154 wartezeit.uniformDelay + wartezeit.incrementalDelay,
155 0,
156 );
157 // Ohne uebergebene Abflusszeit gilt tF + 1 s.
158 expect(wartezeit.rechenweg.abflusszeit).toBe(31);
159 });
160
161 it('laesst die Anteile auf den ausgegebenen Ziffern aufgehen: tW = tW,G + tW,R', () => {
162 // Unabhaengig gerundet ergaebe K5 "37,9 + 21,2 = 59,0" - eine Zeile, die
163 // sich selbst widerlegt. Der Reststauanteil ist deshalb die Differenz der
164 // ausgegebenen Werte.
165 for (const zeile of LISA_REFERENZ) {
166 const w = computeDelay({
167 cycleTime: 90,
168 greenTime: zeile.tF,
169 capacity: zeile.C,
170 demand: zeile.q,
171 verfahren: 'HBS',
172 mode: 'kfz',
173 abflusszeit: zeile.tA,
174 });
175 expect(Math.round((w.uniformDelay + w.incrementalDelay) * 10)).toBe(
176 Math.round(w.averageDelay * 10),
177 );
178 }
179 const k5 = computeDelay({
180 cycleTime: 90,
181 greenTime: 10,
182 capacity: 243,
183 demand: 167,
184 verfahren: 'HBS',
185 mode: 'kfz',
186 abflusszeit: 11,
187 });
188 expect(k5.uniformDelay).toBe(37.9);
189 expect(k5.incrementalDelay).toBe(21.1);
190 expect(k5.averageDelay).toBe(59);
191 });
192
193 it('vergibt ohne Nachfrage und ohne Kapazitaet keine Stufe (nichts zu bewerten)', () => {
194 const leer = computeDelay({
195 cycleTime: 90,
196 greenTime: 0,
197 capacity: 0,
198 demand: 0,
199 verfahren: 'HBS',
200 mode: 'kfz',
201 });
202 expect(leer.serviceLevel).toBeNull();
203 expect(leer.degreeOfSaturation).toBe(0);
204 // ... aber mit Nachfrage ist C = 0 Ueberlastung.
205 const voll = computeDelay({
206 cycleTime: 90,
207 greenTime: 0,
208 capacity: 0,
209 demand: 10,
210 verfahren: 'HBS',
211 mode: 'kfz',
212 });
213 expect(voll.serviceLevel?.grade).toBe('F');
214 expect(voll.serviceLevel?.kriterium).toBe('ueberlastung');
215 // Dieselbe Aussage in der Kapazitaetsrechnung: 0 > 0 ist keine Ueberlastung.
216 const kap = computeCapacity(0, 90, 2000, 0, { verfahren: 'HBS' });
217 expect(kap.degreeOfSaturation).toBe(0);
218 expect(kap.notes.some((n) => n.code === 'auslastung-ueber-100')).toBe(false);
219 });
220
221 it('warnt vor hoher Auslastung an der Schwelle der Projektvorgaben, nicht der Regelwerte', () => {
222 // Regelwert 0,95; Projekt setzt 0,90 -> x = 0,92 muss gemeldet werden.
223 const kap = computeCapacity(29, 90, 1800, 552, {
224 verfahren: 'HBS',
225 maxDegreeOfSaturation: 0.9,
226 });
227 expect(kap.degreeOfSaturation).toBeCloseTo(0.92, 2);
228 expect(kap.notes.some((n) => n.code === 'auslastung-kritisch')).toBe(true);
229 expect(kap.notes.find((n) => n.code === 'auslastung-kritisch')?.message).toContain('0,90');
230 });
231
232 it('lehnt einen Instationaritaetsfaktor unter 1 ab und nimmt den Vorgabewert', () => {
233 const wartezeit = computeDelay({
234 cycleTime: 90,
235 greenTime: 30,
236 capacity: 689,
237 demand: 400,
238 verfahren: 'HBS',
239 mode: 'kfz',
240 instationaritaetsfaktor: 0.8,
241 });
242 expect(wartezeit.notes.some((n) => n.code === 'instationaritaetsfaktor-ungueltig')).toBe(true);
243 expect(wartezeit.rechenweg.instationaritaetsfaktor).toBe(
244 RILSA_DEFAULTS.capacity.instationaritaetsfaktor,
245 );
246 });
247 });
248
249 describe('Saettigungsverkehrsstaerke (Befund B3)', () => {
250 it('rechnet mit 2000 Kfz/h je Fahrstreifen (HBS, BASt V 400)', () => {
251 expect(RILSA_DEFAULTS.capacity.saturationFlow).toBe(2000);
252 expect(saturationFlow('geradeaus', 1)).toBe(2000);
253 expect(saturationFlow('links', 1)).toBeCloseTo(1800, 6);
254 });
255
256 it('bildet den Auslastungsgrad aus der ausgewiesenen, ganzzahligen Kapazitaet', () => {
257 // 1992 * 11 / 90 = 243,47 -> ausgewiesen 243; x = 167 / 243, nicht 167 / 243,47.
258 const kapazitaet = computeCapacity(10, 90, 1992, 167, { verfahren: 'HBS' });
259 expect(kapazitaet.capacity).toBe(243);
260 expect(kapazitaet.degreeOfSaturation).toBeCloseTo(167 / 243, 4);
261 });
262 });
263
264 describe('Qualitaetsstufen nach HBS 2015 (Befund B1)', () => {
265 it('endet fuer Kfz bei E ohne Obergrenze und vergibt F nur bei Ueberlastung', () => {
266 expect(serviceLevelFor(20, 'HBS')?.grade).toBe('A');
267 expect(serviceLevelFor(35, 'HBS')?.grade).toBe('B');
268 expect(serviceLevelFor(50, 'HBS')?.grade).toBe('C');
269 expect(serviceLevelFor(70, 'HBS')?.grade).toBe('D');
270 expect(serviceLevelFor(70.1, 'HBS')?.grade).toBe('E');
271 // Bis hierher hiess es: 100 s -> E, 100,1 s -> F. Nach HBS 2015 bleibt es E.
272 expect(serviceLevelFor(100.1, 'HBS')?.grade).toBe('E');
273 expect(serviceLevelFor(500, 'HBS')?.grade).toBe('E');
274 expect(serviceLevelFor(500, 'HBS', { degreeOfSaturation: 0.99 })?.grade).toBe('E');
275 // ... und ein ueberlasteter Strom ist F, egal wie klein die Wartezeit ausfaellt.
276 const f = serviceLevelFor(30, 'HBS', { degreeOfSaturation: 1.01 });
277 expect(f?.grade).toBe('F');
278 expect(f?.kriterium).toBe('ueberlastung');
279 expect(f?.label).toContain('überlastet');
280 // Genau 1,0 ist keine Ueberlastung (q > C, nicht q >= C).
281 expect(serviceLevelFor(30, 'HBS', { degreeOfSaturation: 1 })?.grade).toBe('B');
282 expect(SERVICE_LEVEL_SCALES.HBS.kfz.some((s) => s.grade === 'F')).toBe(false);
283 });
284
285 it('bewertet den OePNV nach der eigenen HBS-Tafel', () => {
286 expect(serviceLevelFor(5, 'HBS', { mode: 'oepnv' })?.grade).toBe('A');
287 expect(serviceLevelFor(15, 'HBS', { mode: 'oepnv' })?.grade).toBe('B');
288 expect(serviceLevelFor(25, 'HBS', { mode: 'oepnv' })?.grade).toBe('C');
289 expect(serviceLevelFor(40, 'HBS', { mode: 'oepnv' })?.grade).toBe('D');
290 expect(serviceLevelFor(60, 'HBS', { mode: 'oepnv' })?.grade).toBe('E');
291 expect(serviceLevelFor(60.1, 'HBS', { mode: 'oepnv' })?.grade).toBe('F');
292 // Dieselben 25 s sind fuer Kfz Stufe B.
293 expect(serviceLevelFor(25, 'HBS', { mode: 'kfz' })?.grade).toBe('B');
294 });
295
296 it('bewertet nach HCM weiter nach der HCM-Tafel, F ausserdem bei Ueberlastung', () => {
297 expect(serviceLevelFor(15, 'HCM')?.grade).toBe('B');
298 expect(serviceLevelFor(80, 'HCM')?.grade).toBe('E');
299 expect(serviceLevelFor(80.1, 'HCM')?.grade).toBe('F');
300 expect(serviceLevelFor(80.1, 'HCM')?.kriterium).toBe('mittlere-wartezeit');
301 expect(serviceLevelFor(20, 'HCM', { degreeOfSaturation: 1.2 })?.kriterium).toBe('ueberlastung');
302 });
303
304 it('koppelt die Stufe F der Wartezeitrechnung an q > C', () => {
305 const ueberlastet = computeDelay({
306 cycleTime: 90,
307 greenTime: 30,
308 capacity: 600,
309 demand: 601,
310 verfahren: 'HBS',
311 mode: 'kfz',
312 });
313 expect(ueberlastet.serviceLevel?.grade).toBe('F');
314 expect(ueberlastet.serviceLevel?.kriterium).toBe('ueberlastung');
315 expect(ueberlastet.notes.some((n) => n.code === 'ueberlastet')).toBe(true);
316
317 const gerade = computeDelay({
318 cycleTime: 90,
319 greenTime: 30,
320 capacity: 600,
321 demand: 600,
322 verfahren: 'HBS',
323 mode: 'kfz',
324 });
325 expect(gerade.serviceLevel?.grade).not.toBe('F');
326 });
327
328 it('kfzServiceLevel liefert nie null', () => {
329 expect(kfzServiceLevel(30, 'HBS').grade).toBe('B');
330 expect(kfzServiceLevel(30, 'HBS', 1.5).grade).toBe('F');
331 expect(kfzServiceLevel(10, 'HBS', undefined, 'oepnv').grade).toBe('B');
332 });
333 });
334
335 describe('Fussgaenger und Radverkehr: maximale Wartezeit (Befund B4)', () => {
336 it('ermittelt die Sperrzeiten aus den Freigabezeitfenstern, auch ueber den Umlaufbeginn hinweg', () => {
337 expect(sperrzeitenAusFenstern([{ start: 10, duration: 20 }], 90)).toEqual([70]);
338 expect(
339 sperrzeitenAusFenstern(
340 [
341 { start: 0, duration: 20 },
342 { start: 50, duration: 10 },
343 ],
344 90,
345 ),
346 ).toEqual([30, 30]);
347 // Fenster von 80 bis 100 = 80..90 und 0..10: Sperrzeit 10..80 = 70 s.
348 expect(sperrzeitenAusFenstern([{ start: 80, duration: 20 }], 90)).toEqual([70]);
349 expect(sperrzeitenAusFenstern([], 90)).toEqual([90]);
350 expect(sperrzeitenAusFenstern([{ start: 0, duration: 90 }], 90)).toEqual([]);
351 });
352
353 it('bewertet nach der HBS-Fuss/Rad-Tafel ueber die laengste Sperrzeit', () => {
354 // Beispiel aus der VCD-Darstellung zum HBS S4: Umlauf 110 s, Rot 41 bis
355 // 108 s -> maximale Wartezeit 67 s -> Stufe D.
356 const furt = computeFussRadDelay({
357 cycleTime: 110,
358 sperrzeiten: [67],
359 verfahren: 'HBS',
360 mode: 'fuss',
361 });
362 expect(furt.maximumDelay).toBe(67);
363 expect(furt.serviceLevel?.grade).toBe('D');
364 expect(furt.serviceLevel?.kriterium).toBe('maximale-wartezeit');
365 expect(furt.degreeOfSaturation).toBe(0);
366 // Mittlere Wartezeit eines gleichverteilt eintreffenden Fussgaengers: 67^2 / (2*110).
367 expect(furt.averageDelay).toBeCloseTo((67 * 67) / 220, 1);
368
369 for (const [wert, stufe] of [
370 [30, 'A'],
371 [40, 'B'],
372 [55, 'C'],
373 [70, 'D'],
374 [85, 'E'],
375 [85.1, 'F'],
376 ] as const) {
377 expect(serviceLevelFor(wert, 'HBS', { mode: 'fuss' })?.grade, `${wert} s`).toBe(stufe);
378 expect(serviceLevelFor(wert, 'HBS', { mode: 'rad' })?.grade, `${wert} s`).toBe(stufe);
379 }
380 });
381
382 it('bewertet bei zwei Freigaben die laengere Sperrzeit, nicht die Summe', () => {
383 const zweimal = computeFussRadDelay({
384 cycleTime: 90,
385 sperrzeiten: sperrzeitenAusFenstern(
386 [
387 { start: 0, duration: 15 },
388 { start: 45, duration: 15 },
389 ],
390 90,
391 ),
392 verfahren: 'HBS',
393 mode: 'rad',
394 });
395 expect(zweimal.maximumDelay).toBe(30);
396 expect(zweimal.serviceLevel?.grade).toBe('A');
397 });
398
399 it('vergibt nach HCM fuer Fussgaenger und Radverkehr keine Stufe und sagt das', () => {
400 const furt = computeFussRadDelay({
401 cycleTime: 90,
402 sperrzeiten: [60],
403 verfahren: 'HCM',
404 mode: 'fuss',
405 });
406 expect(furt.serviceLevel).toBeNull();
407 expect(furt.notes.some((n) => n.code === 'keine-fuss-rad-tafel')).toBe(true);
408 expect(serviceLevelFor(60, 'HCM', { mode: 'rad' })).toBeNull();
409 });
410 });