lsa-planer

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

/ tests domain stauraum.test.ts

9,0 KB Rohdatei
tests/domain/stauraum.test.ts — 213 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import { rueckstaubewertung, rueckstaulaenge } from '@/domain/rilsa/delay';
3 import { STAURAUMBEDARF_JE_FAHRZEUG } from '@/domain/rilsa/constants';
4 import { buildSignalPlan } from '@/domain/plan/signalPlan';
5 import { validateProject } from '@/domain/validation';
6 import { createStandardIntersectionProject } from '@/domain/model/factory';
7 import { loadProject } from '@/domain/model/migrate';
8 import type { Project } from '@/domain/model/project';
9
10 /**
11 * Fassung 5.31.0 - der Rueckstau als Laenge, gehalten gegen den vorhandenen
12 * Stauraum.
13 *
14 * WARUM: Die Leistungsfaehigkeitsrechnung fuehrte den Rueckstau seit jeher als
15 * Fahrzeugzahl NGE, und die Planunterlage druckte sie. Fuer sich genommen ist
16 * sie unbrauchbar - ob ein Aufstellstreifen ueberlaeuft, entscheidet eine
17 * Laenge.
18 *
19 * DIE WICHTIGSTE ZUSAGE DIESER DATEI ist nicht die Umrechnung, sondern die
20 * Ehrlichkeit ueber die verglichene Groesse: NGE ist ein MITTELWERT. Ein
21 * Stauraum, der gerade eben reicht, reicht in der Haelfte der Umlaeufe nicht,
22 * und das muss in jeder Ausgabe stehen.
23 */
24
25 const DATUM = new Date('2026-01-01T00:00:00Z');
26
27 describe('Rueckstau gegen Stauraum - die reine Rechnung', () => {
28 it('bildet die Länge aus NGE und den Fahrstreifen', () => {
29 // 6 Kfz * 7 m / 2 Fahrstreifen = 21,0 m
30 const bewertung = rueckstaubewertung(6, 2, 50);
31 expect(bewertung.fahrzeuge).toBe(6);
32 expect(bewertung.laenge).toBe(21);
33 expect(bewertung.laenge).toBe(rueckstaulaenge(6, 2));
34 expect(bewertung.stauraum).toBe(50);
35 expect(bewertung.auslastung).toBeCloseTo(0.42, 4);
36 expect(bewertung.ueberlauf).toBe(false);
37 });
38
39 it('meldet den Überlauf, sobald die Länge den Stauraum erreicht', () => {
40 // Genau erreicht zaehlt schon: Der MITTLERE Rueckstau fuellt den Stauraum
41 // dann aus, und in der Haelfte der Umlaeufe steht mehr.
42 expect(rueckstaubewertung(10, 1, 70).ueberlauf).toBe(true);
43 expect(rueckstaubewertung(10, 1, 70.1).ueberlauf).toBe(false);
44 expect(rueckstaubewertung(10, 1, 60).ueberlauf).toBe(true);
45 });
46
47 it('vergleicht nicht, wo kein Stauraum erfasst ist', () => {
48 for (const stauraum of [null, 0, -5, Number.NaN]) {
49 const bewertung = rueckstaubewertung(10, 1, stauraum);
50 expect(bewertung.stauraum, String(stauraum)).toBeNull();
51 expect(bewertung.auslastung, String(stauraum)).toBeNull();
52 expect(bewertung.ueberlauf, String(stauraum)).toBe(false);
53 // Die Laenge steht trotzdem da - sie ist die Auskunft, die auch ohne
54 // Vergleich weiterhilft.
55 expect(bewertung.laenge).toBe(70);
56 }
57 });
58
59 it('kommt mit einem leeren Rückstau zurecht', () => {
60 const bewertung = rueckstaubewertung(0, 2, 40);
61 expect(bewertung.fahrzeuge).toBe(0);
62 expect(bewertung.laenge).toBe(0);
63 expect(bewertung.ueberlauf).toBe(false);
64 });
65 });
66
67 describe('Rueckstau gegen Stauraum - im Plan', () => {
68 it('trägt die Bewertung an jeder bemessenen Signalgruppe', () => {
69 const plan = buildSignalPlan(mitStauraum(null));
70 const gruppe = plan.groups.find((g) => g.delay !== null);
71 expect(gruppe?.rueckstau, 'die Bewertung steht am Plan').toBeDefined();
72 expect(gruppe?.rueckstau?.laenge ?? -1).toBeGreaterThan(0);
73 expect(gruppe?.rueckstau?.stauraum, 'ohne Eintrag kein Stauraum').toBeNull();
74 });
75
76 it('führt keine Bewertung ohne Rückstaugröße', () => {
77 /*
78 * NGE entsteht allein im HBS-Verfahren. Im HCM-Verfahren gibt es ihn
79 * nicht, und eine 0 stuende dort fuer ein Ergebnis, das es nicht gibt.
80 */
81 const basis = mitStauraum(null);
82 const plan = buildSignalPlan({
83 ...basis,
84 settings: { ...basis.settings, serviceLevelScale: 'HCM' },
85 });
86 for (const gruppe of plan.groups) expect(gruppe.rueckstau, gruppe.name).toBeNull();
87 });
88
89 it('rechnet die Länge je Fahrstreifen, nicht je Signalgruppe', () => {
90 /*
91 * GEPRUEFT WIRD DIE UMRECHNUNG IM PLAN, nicht ein Vergleich zweier
92 * Plaene. Ein zweistreifiger Knotenpunkt hat die doppelte Kapazitaet und
93 * dadurch ohnehin ein kleineres NGE; ein "kuerzer als einstreifig" waere
94 * also auch dann erfuellt, wenn die Fahrstreifenzahl bei der Umrechnung
95 * uebergangen wuerde. Genau so stand dieser Fall zuerst da.
96 *
97 * Der Fehler, den Punkt A5 an der Simulation behoben hat, darf
98 * hier nicht wiederkehren.
99 */
100 const basis = mitStauraum(null);
101 const projekt: Project = {
102 ...basis,
103 signalGroups: basis.signalGroups.map((g) => ({ ...g, lanes: 2 })),
104 };
105 const plan = buildSignalPlan(projekt);
106 let geprueft = 0;
107 for (const gruppe of plan.groups) {
108 if (gruppe.rueckstau === null) continue;
109 geprueft += 1;
110 expect(gruppe.rueckstau.laenge, gruppe.name).toBe(
111 rueckstaulaenge(gruppe.rueckstau.fahrzeuge, 2),
112 );
113 // Kein zweiter Vergleich gegen `rueckstaulaenge(f, 1) / 2`: Beide Wege
114 // runden auf eine Nachkommastelle, und 1,55 gegen 1,5 ist kein Befund,
115 // sondern die Rundung. Die Zeile darueber ist die genaue Zusage.
116 }
117 expect(geprueft, 'kein Strom mit Rückstau').toBeGreaterThan(0);
118 });
119 });
120
121 describe('Rueckstau gegen Stauraum - die Meldung', () => {
122 it('warnt, wo der mittlere Rückstau den Stauraum erreicht', () => {
123 const bericht = validateProject(mitStauraum(5));
124 const meldung = bericht.findings.find(
125 (f) => f.rule === 'leistungsfaehigkeit.rueckstau-ueber-stauraum',
126 );
127 expect(meldung, 'die Warnung fehlt').toBeDefined();
128 // Warnung und nicht Hinweis: Ein ueberlaufender Aufstellstreifen blockiert
129 // den durchgehenden Fahrstreifen.
130 expect(meldung?.severity).toBe('warnung');
131 expect(meldung?.target?.kind).toBe('signalgruppe');
132 });
133
134 it('sagt in der Meldung, dass ein Mittelwert verglichen wurde', () => {
135 /*
136 * DIE ZUSAGE, AN DER ALLES HAENGT. Ohne diesen Satz liest ein Bearbeiter
137 * "Stauraum reicht" aus einer Zahl, die in der Haelfte der Umlaeufe
138 * ueberschritten wird.
139 */
140 const bericht = validateProject(mitStauraum(5));
141 const meldung = bericht.findings.find(
142 (f) => f.rule === 'leistungsfaehigkeit.rueckstau-ueber-stauraum',
143 );
144 expect(meldung?.message).toContain('MITTLERE');
145 expect(meldung?.message).toContain('Hälfte der Umläufe');
146 expect(meldung?.message).toContain('95-Prozent-Wert');
147 });
148
149 it('schweigt ohne Stauraum und bei ausreichendem Stauraum', () => {
150 for (const stauraum of [null, 500]) {
151 const bericht = validateProject(mitStauraum(stauraum));
152 expect(
153 bericht.findings.some((f) => f.rule === 'leistungsfaehigkeit.rueckstau-ueber-stauraum'),
154 `Stauraum ${String(stauraum)}`,
155 ).toBe(false);
156 }
157 });
158 });
159
160 describe('Rueckstau gegen Stauraum - in der Datei', () => {
161 it('trägt den Stauraum über Speichern und Laden', () => {
162 const geladen = loadProject(JSON.parse(JSON.stringify(mitStauraum(45))));
163 expect(geladen.project.schemaVersion).toBe(19);
164 expect(geladen.project.signalGroups[0]?.stauraumlaenge).toBe(45);
165 expect(geladen.issues.filter((i) => i.path.includes('stauraumlaenge'))).toEqual([]);
166 });
167
168 it('erfindet für eine Altdatei keinen Stauraum', () => {
169 const geladen = loadProject(JSON.parse(JSON.stringify(mitStauraum(null))));
170 expect(geladen.project.signalGroups[0]?.stauraumlaenge).toBeUndefined();
171 });
172
173 it('meldet einen unlesbaren Eintrag mit der richtigen Folge', () => {
174 /*
175 * Die Folge ist hier eine ANDERE als bei den Geometriefeldern: Der
176 * Stauraum geht in keine Rechnung ein, es entfaellt allein der Vergleich.
177 * Ein gemeinsamer Satz haette an einer der beiden Stellen gelogen.
178 */
179 const roh = JSON.parse(JSON.stringify(mitStauraum(45))) as Record<string, unknown>;
180 const gruppen = roh['signalGroups'] as Record<string, unknown>[];
181 (gruppen[0] as Record<string, unknown>)['stauraumlaenge'] = 'kurz';
182 const geladen = loadProject(roh);
183 expect(geladen.project.signalGroups[0]?.stauraumlaenge).toBeUndefined();
184 const meldung = geladen.issues.find((i) => i.path.endsWith('.stauraumlaenge'));
185 expect(meldung?.message).toContain('mit keiner Länge verglichen');
186 expect(meldung?.message).not.toContain('Sättigungsverkehrsstärke');
187 });
188 });
189
190 /**
191 * Standardknotenpunkt mit hoher Auslastung - so entsteht ueberhaupt ein
192 * nennenswerter Rueckstau. Der Stauraum steht an jeder Kfz-Gruppe.
193 */
194 function mitStauraum(stauraum: number | null): Project {
195 const roh = createStandardIntersectionProject('Stauraum', DATUM);
196 return {
197 ...roh,
198 signalGroups: roh.signalGroups.map((gruppe) =>
199 gruppe.mode === 'kfz' && stauraum !== null ? { ...gruppe, stauraumlaenge: stauraum } : gruppe,
200 ),
201 demands: roh.signalGroups
202 .filter((g) => g.mode === 'kfz')
203 .map((g) => ({ signalGroupId: g.id, volume: 700, heavyVehicleShare: 0.05 })),
204 };
205 }
206
207 /** Der Kennwert steht dort, wo ihn beide Rechnungen finden. */
208 describe('Rueckstau gegen Stauraum - eine Umrechnung, nicht zwei', () => {
209 it('nimmt für Simulation und Bemessung dieselbe Länge je Fahrzeug', () => {
210 expect(STAURAUMBEDARF_JE_FAHRZEUG).toBe(7);
211 expect(rueckstaubewertung(3, 1, null).laenge).toBe(3 * STAURAUMBEDARF_JE_FAHRZEUG);
212 });
213 });