lsa-planer

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

/ tests domain umlaufzeitvergleichEhrlichkeit.test.ts

8,4 KB Rohdatei
tests/domain/umlaufzeitvergleichEhrlichkeit.test.ts — 180 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import { compareCycleTimeMethods } from '@/domain/rilsa/cycle';
3 import { RILSA_DEFAULTS } from '@/domain/rilsa/constants';
4
5 /**
6 * Fassung 5.28.0, Punkte A1 bis A3.
7 *
8 * `compareCycleTimeMethods` hat bis heute keinen Aufrufer - und genau deshalb
9 * standen drei Fehler unbemerkt darin. Ein Test, der die Funktion unmittelbar
10 * ruft, ist die einzige Wache, die sie derzeit haben kann; dass sie unbenutzt
11 * ist, bewacht `umlaufzeitempfehlung.test.ts` gesondert und
12 * planmaessig.
13 *
14 * Alle Rechenwerte hier sind aus den Formeln in `cycle.ts` und den Vorgaben in
15 * `constants.ts` von Hand nachgerechnet und stehen als Rechenweg im Kommentar:
16 *
17 * webster tU = (1,5 L + 5) / (1 - Y)
18 * akcelik tU = (1,4 L + 6) / (1 - Y)
19 * hbs tU = L x / (x - Y) mit x = 0,85 (Ziel-Auslastungsgrad)
20 * hcm tU = L Xc / (Xc - Y) mit Xc = 0,90 (hoechster Auslastungsgrad)
21 *
22 * gerastert auf 5 s, begrenzt auf 30 bis 120 s.
23 */
24 describe('Umlaufzeitvergleich - ein Wert, kein Gewinner (A1)', () => {
25 it('nennt bei Gleichstand alle Verfahren, die den Wert erreichen', () => {
26 /*
27 * L = 10 ist die Verlustzeit, bei der Webster und Akcelik zusammenfallen:
28 * 1,5 L + 5 = 1,4 L + 6 <=> L = 10. Beide rechnen dann 20 / (1 - Y).
29 *
30 * Bei Y = 0,60:
31 * webster 20 / 0,40 = 50,0 -> 50 s
32 * akcelik 20 / 0,40 = 50,0 -> 50 s
33 * hbs 8,5 / 0,25 = 34,0 -> 35 s
34 * hcm 9,0 / 0,30 = 30,0 -> 30 s
35 * Kapazitaetsbedarf 35 s, Wartezeitoptimum 50 s, empfohlen 50 s.
36 *
37 * BIS ZUR BEHEBUNG stand hier ein einzelnes Ergebnis, herausgesucht mit
38 * `Object.values(results).find(...)`. Webster gewann, weil es im
39 * Objektliteral zuerst steht - nicht, weil es etwas besser koennte.
40 */
41 const vergleich = compareCycleTimeMethods({ lostTime: 10, criticalFlowRatios: [0.6] });
42
43 expect(vergleich.results.webster.cycleTime).toBe(50);
44 expect(vergleich.results.akcelik.cycleTime).toBe(50);
45 expect(vergleich.recommendedCycleTime).toBe(50);
46 expect(vergleich.recommendedMethods).toEqual(['webster', 'akcelik']);
47 });
48
49 it('sagt dazu, dass die Reihenfolge der Aufzaehlung nichts bedeutet', () => {
50 // Die Hausregel verlangt beides: eine stabile Ordnung UND den Satz, dass
51 // sie nichts aussagt. Ohne ihn liest der erste Eintrag sich als Gewinner.
52 const vergleich = compareCycleTimeMethods({ lostTime: 10, criticalFlowRatios: [0.6] });
53 expect(vergleich.reason).toContain('keinen Vorrang');
54 });
55
56 it('gibt kein einzelnes Verfahrensergebnis mehr als Empfehlung aus', () => {
57 // Der empfohlene Wert entsteht aus dem Maximum zweier Zielgroessen und
58 // muss mit keinem Einzelergebnis zusammenfallen. Ein Feld, das so tut,
59 // als gaebe es ein massgebendes Verfahren, ist die Unwahrheit.
60 const vergleich = compareCycleTimeMethods({ lostTime: 20, criticalFlowRatios: [0.6] });
61 expect('recommended' in vergleich).toBe(false);
62 });
63
64 it('laesst die Empfehlung im fehlerfreien Fall unveraendert', () => {
65 /*
66 * L = 20, Y = 0,60 - der Fall aus cycle.test.ts:
67 * webster 35 / 0,40 = 87,5 -> 90 s
68 * akcelik 34 / 0,40 = 85,0 -> 85 s
69 * hbs 17 / 0,25 = 68,0 -> 70 s
70 * hcm 18 / 0,30 = 60,0 -> 60 s
71 * Kapazitaetsbedarf 70 s, Wartezeitoptimum 85 s, empfohlen 85 s - und das
72 * erreicht allein Akcelik.
73 */
74 const vergleich = compareCycleTimeMethods({ lostTime: 20, criticalFlowRatios: [0.6] });
75
76 expect(vergleich.recommendedCycleTime).toBe(85);
77 expect(vergleich.recommendedMethods).toEqual(['akcelik']);
78 expect(vergleich.reason).toContain('Empfohlen wird 85,0 s');
79 });
80 });
81
82 describe('Umlaufzeitvergleich - jeder Fehler zaehlt (A2)', () => {
83 it('empfiehlt nichts, wo der erforderliche Umlauf ueber dem Hoechstwert liegt', () => {
84 /*
85 * DER GEFAEHRLICHSTE FALL DIESER FUNKTION. L = 40, Y = 0,80:
86 * webster 65 / 0,20 = 325 s
87 * akcelik 62 / 0,20 = 310 s
88 * hbs 34 / 0,05 = 680 s
89 * hcm 36 / 0,10 = 360 s
90 * Alle vier ueberschreiten den Hoechstwert von 120 s und melden
91 * 'umlaufzeit-ueber-hoechstwert' als Fehler. Uebersaettigt ist keines:
92 * Y = 0,80 liegt unter allen vier Grenzen (1, 1, 0,85 und 0,90).
93 *
94 * BIS ZUR BEHEBUNG fragte die Funktion nur nach dem Kode 'uebersaettigt'.
95 * Sie schrieb deshalb "Empfohlen wird 120,0 s" - neben vier Meldungen, die
96 * sagten, die Anlage sei mit dieser Phaseneinteilung nicht
97 * leistungsfaehig.
98 */
99 const vergleich = compareCycleTimeMethods({ lostTime: 40, criticalFlowRatios: [0.8] });
100
101 for (const verfahren of ['webster', 'akcelik', 'hbs', 'hcm'] as const) {
102 expect(
103 vergleich.results[verfahren].notes.some((n) => n.code === 'umlaufzeit-ueber-hoechstwert'),
104 verfahren,
105 ).toBe(true);
106 }
107 expect(vergleich.recommendedCycleTime).toBeNull();
108 expect(vergleich.recommendedMethods).toEqual([]);
109 expect(vergleich.reason).not.toContain('Empfohlen wird');
110 expect(vergleich.reason).toContain('Höchstwert');
111 expect(vergleich.reason).toContain('alle vier Verfahren');
112 });
113
114 it('empfiehlt auch dann nichts, wenn nur ein Verfahren beanstandet ist', () => {
115 /*
116 * L = 5, Y = 0,86 - nur das HBS-Verfahren scheitert:
117 * hbs Y = 0,86 >= x = 0,85 -> uebersaettigt
118 * hcm 4,5 / 0,04 = 112,5 s -> 115 s -> rechenbar
119 * webster 12,5 / 0,14 = 89,3 s -> 90 s -> rechenbar
120 * akcelik 13,0 / 0,14 = 92,9 s -> 95 s -> rechenbar
121 *
122 * Warum das genuegt: Der Kapazitaetsbedarf ist das Maximum aus HBS und
123 * HCM, und das uebersaettigte HBS-Ergebnis traegt den Hoechstwert als
124 * ERSATZWERT. Er ginge in das Maximum ein, und die gebildete Zahl saehe
125 * gueltig aus, ohne den erforderlichen Umlauf zu decken.
126 */
127 const vergleich = compareCycleTimeMethods({ lostTime: 5, criticalFlowRatios: [0.86] });
128
129 expect(vergleich.results.hbs.bounded).toBe('uebersaettigt');
130 expect(vergleich.results.hcm.notes.some((n) => n.severity === 'fehler')).toBe(false);
131 expect(vergleich.results.webster.notes.some((n) => n.severity === 'fehler')).toBe(false);
132
133 expect(vergleich.recommendedCycleTime).toBeNull();
134 expect(vergleich.reason).toContain('übersättigt');
135 expect(vergleich.reason).toContain('Beanstandet ist HBS');
136 expect(vergleich.reason).not.toContain('alle vier');
137 });
138
139 it('nennt jeden aufgetretenen Grund genau einmal', () => {
140 /*
141 * L = 20, Y = 0,90 - zwei verschiedene Gruende zugleich:
142 * hbs, hcm Y erreicht die Grenze -> uebersaettigt
143 * webster 35 / 0,10 = 350 s -> ueber dem Hoechstwert
144 * akcelik 34 / 0,10 = 340 s -> ueber dem Hoechstwert
145 * Vier Verfahren, zwei Gruende, zwei Saetze - nicht vier.
146 */
147 const vergleich = compareCycleTimeMethods({ lostTime: 20, criticalFlowRatios: [0.5, 0.4] });
148
149 expect(vergleich.recommendedCycleTime).toBeNull();
150 expect(vergleich.reason).toContain('übersättigt');
151 expect(vergleich.reason).toContain('Höchstwert');
152 expect(vergleich.reason.split('übersättigt').length - 1, 'Grund nur einmal').toBe(1);
153 expect(vergleich.reason.split('Höchstwert').length - 1, 'Grund nur einmal').toBe(1);
154 });
155
156 it('empfiehlt nichts ohne Saettigungsgrade', () => {
157 const vergleich = compareCycleTimeMethods({ lostTime: 20, criticalFlowRatios: [] });
158 expect(vergleich.recommendedCycleTime).toBeNull();
159 expect(vergleich.reason).toContain('Sättigungsgrade');
160 });
161 });
162
163 describe('Umlaufzeitvergleich - kein Scheinergebnis im Fehlerfall (A3)', () => {
164 it('fuehrt im Fehlerfall keinen Wert und kein Verfahren', () => {
165 /*
166 * BIS ZUR BEHEBUNG gab die Funktion hier `results.webster` zurueck - ein
167 * vollstaendiges Ergebnis mit der Zahl 120 s, in einem Feld, von dem der
168 * Satz unmittelbar daneben sagte, es gebe keines. Wer das Feld las und den
169 * Satz nicht, bekam einen Ersatzwert als Empfehlung.
170 */
171 const vergleich = compareCycleTimeMethods({ lostTime: 20, criticalFlowRatios: [0.5, 0.4] });
172
173 expect(vergleich.recommendedCycleTime).toBeNull();
174 expect(vergleich.recommendedMethods).toEqual([]);
175 // Die Einzelergebnisse bleiben erhalten - sie sind die Auskunft, aus der
176 // hervorgeht, WORAN es liegt.
177 expect(vergleich.results.hbs.cycleTime).toBe(RILSA_DEFAULTS.cycleTime.max);
178 expect(vergleich.results.hbs.bounded).toBe('uebersaettigt');
179 });
180 });