lsa-planer

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

/ tests domain stufeZurWartezeit.test.ts

6,8 KB Rohdatei
tests/domain/stufeZurWartezeit.test.ts — 159 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import { computeCapacity } from '@/domain/rilsa/capacity';
3 import { computeDelay, kfzServiceLevel, serviceLevelFor } from '@/domain/rilsa/delay';
4 import { TrafficSimulation } from '@/domain/simulation/simulation';
5 import { buildSignalPlan } from '@/domain/plan/signalPlan';
6 import { createStandardIntersectionProject } from '@/domain/model/factory';
7 import { SERVICE_LEVEL_SCALES } from '@/domain/rilsa/constants';
8 import type { Project } from '@/domain/model/project';
9
10 /*
11 * Die ausgegebene Wartezeit und die ausgegebene Qualitaetsstufe muessen
12 * zusammenpassen.
13 *
14 * Ausgegeben wurde die gerundete Wartezeit, bewertet aber die ungerundete. Bei
15 * 35,02 s stand in der Unterlage "35,0 s" neben der Stufe C, waehrend die
16 * Stufentafel im selben Dokument bis einschliesslich 35 s die Stufe B ausweist
17 * - eine Zeile, die sich selbst widerlegt, und zwar zu Ungunsten der Anlage.
18 * Auflosen konnte den Widerspruch niemand, weil der ungerundete Wert nirgends
19 * steht.
20 *
21 * Geprueft wird deshalb nicht ein einzelner Wert, sondern die Eigenschaft: Aus
22 * den ausgegebenen Zahlen muss sich die ausgegebene Stufe ergeben. Seit
23 * Fassung 5.4.0 (Befund B1) gehoert dazu neben der Wartezeit der
24 * Auslastungsgrad: Nach HBS 2015 ist F genau dann erreicht, wenn q > C - und
25 * beide Zahlen stehen ganzzahlig im Ausdruck.
26 */
27
28 const STICHTAG = new Date('2026-01-01T00:00:00Z');
29
30 describe('Ausgegebene Wartezeit und ausgegebene Stufe passen zusammen', () => {
31 it('gilt fuer die Leistungsfaehigkeit ueber den ganzen Wertebereich', () => {
32 let geprueft = 0;
33 for (const verfahren of ['HBS', 'HCM'] as const) {
34 for (let demand = 100; demand <= 1000; demand += 0.5) {
35 for (const green of [18, 27, 36]) {
36 const kapazitaet = computeCapacity(green, 90, 2000, demand, { verfahren });
37 const ergebnis = computeDelay({
38 cycleTime: 90,
39 greenTime: green,
40 capacity: kapazitaet.capacity,
41 demand,
42 verfahren,
43 mode: 'kfz',
44 abflusszeit: kapazitaet.abflusszeit,
45 });
46 // Aus den gedruckten Groessen: Wartezeit (0,1 s) und q > C (ganzzahlig).
47 const erwartet = serviceLevelFor(ergebnis.averageDelay, verfahren, {
48 degreeOfSaturation: demand > kapazitaet.capacity ? 1.5 : 0.5,
49 });
50 expect(
51 ergebnis.serviceLevel?.grade,
52 `${verfahren}, q=${demand}, g=${green}: ${ergebnis.averageDelay} s, C=${kapazitaet.capacity}`,
53 ).toBe(erwartet?.grade);
54 geprueft += 1;
55 }
56 }
57 }
58 expect(geprueft).toBeGreaterThan(5000);
59 });
60
61 /*
62 * Der Fall, an dem sich alles entscheidet: Die ausgegebene Wartezeit liegt
63 * GENAU auf einer Stufengrenze. Die Tafel weist dort die bessere Stufe aus.
64 * Lag der ungerundete Wert knapp darueber, vergab der alte Stand die
65 * schlechtere - und die Zeile widersprach der Tafel im selben Dokument.
66 */
67 it('trifft die Tafel, wenn die ausgegebene Zeit auf einer Grenze liegt', () => {
68 let treffer = 0;
69 for (const verfahren of ['HBS', 'HCM'] as const) {
70 const grenzen = SERVICE_LEVEL_SCALES[verfahren].kfz
71 .filter((s) => Number.isFinite(s.maxDelay))
72 .map((s) => ({ wert: s.maxDelay, stufe: s.grade }));
73
74 for (let demand = 100; demand <= 1000; demand += 0.1) {
75 for (const green of [18, 27, 36]) {
76 const kapazitaet = computeCapacity(green, 90, 2000, demand, { verfahren });
77 if (demand > kapazitaet.capacity) continue; // ueberlastet: F unabhaengig von der Zeit
78 const ergebnis = computeDelay({
79 cycleTime: 90,
80 greenTime: green,
81 capacity: kapazitaet.capacity,
82 demand,
83 verfahren,
84 mode: 'kfz',
85 abflusszeit: kapazitaet.abflusszeit,
86 });
87 const auf = grenzen.find((g) => g.wert === ergebnis.averageDelay);
88 if (auf === undefined) continue;
89 treffer += 1;
90 expect(
91 ergebnis.serviceLevel?.grade,
92 `${verfahren}: ausgegeben ${ergebnis.averageDelay} s, Tafel sagt ${auf.stufe}`,
93 ).toBe(auf.stufe);
94 }
95 }
96 }
97 // Ohne Treffer prueft der Test nichts - dann ist der Suchbereich falsch.
98 expect(treffer, 'kein Wert lag genau auf einer Stufengrenze').toBeGreaterThan(0);
99 });
100
101 it('trifft an einer Stufengrenze die Stufe, die die Tafel nennt', () => {
102 // Die Tafel sagt: bis EINSCHLIESSLICH der Grenze gilt die bessere Stufe.
103 const tafeln = [
104 { verfahren: 'HBS' as const, mode: 'kfz' as const, tafel: SERVICE_LEVEL_SCALES.HBS.kfz },
105 { verfahren: 'HBS' as const, mode: 'oepnv' as const, tafel: SERVICE_LEVEL_SCALES.HBS.oepnv },
106 { verfahren: 'HBS' as const, mode: 'fuss' as const, tafel: SERVICE_LEVEL_SCALES.HBS.fussRad },
107 { verfahren: 'HBS' as const, mode: 'rad' as const, tafel: SERVICE_LEVEL_SCALES.HBS.fussRad },
108 { verfahren: 'HCM' as const, mode: 'kfz' as const, tafel: SERVICE_LEVEL_SCALES.HCM.kfz },
109 ];
110 for (const { verfahren, mode, tafel } of tafeln) {
111 for (const stufe of tafel) {
112 if (!Number.isFinite(stufe.maxDelay)) continue;
113 expect(
114 serviceLevelFor(stufe.maxDelay, verfahren, { mode })?.grade,
115 `${verfahren}/${mode} bei ${stufe.maxDelay} s`,
116 ).toBe(stufe.grade);
117 }
118 }
119 });
120
121 it('gilt ebenso in der Simulation', () => {
122 for (const verfahren of ['HBS', 'HCM'] as const) {
123 const roh = createStandardIntersectionProject('Stufen', STICHTAG);
124 const projekt: Project = {
125 ...roh,
126 settings: { ...roh.settings, serviceLevelScale: verfahren },
127 };
128 const plan = buildSignalPlan(projekt);
129 const sim = new TrafficSimulation(plan, projekt, { seed: 7 });
130
131 // Mehrfach abgreifen: Die Wartezeit waechst waehrend des Laufs und
132 // durchschreitet dabei mehrere Stufengrenzen.
133 for (let i = 0; i < 12; i += 1) {
134 sim.advance(50);
135 const stand = sim.snapshot();
136 for (const gruppe of stand.groups) {
137 const geplant = plan.groups.find((g) => g.groupId === gruppe.groupId);
138 const mode = geplant?.mode === 'oepnv' ? 'oepnv' : 'kfz';
139 expect(
140 gruppe.serviceLevel.grade,
141 `${verfahren} / ${gruppe.name}: ${gruppe.averageDelay} s`,
142 ).toBe(
143 kfzServiceLevel(
144 gruppe.averageDelay,
145 verfahren,
146 geplant?.capacity?.degreeOfSaturation,
147 mode,
148 ).grade,
149 );
150 }
151 const hoechste = Math.max(...plan.groups.map((g) => g.capacity?.degreeOfSaturation ?? 0));
152 expect(
153 stand.overallServiceLevel.grade,
154 `${verfahren} gesamt: ${stand.overallDelay} s`,
155 ).toBe(kfzServiceLevel(stand.overallDelay, verfahren, hoechste).grade);
156 }
157 }
158 });
159 });