lsa-planer

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

/ tests domain raeumgeschwindigkeitVmaxDeckel.test.ts

11,8 KB Rohdatei
tests/domain/raeumgeschwindigkeitVmaxDeckel.test.ts — 253 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import { computeIntergreen, regelRaeumgeschwindigkeit } from '@/domain/rilsa/intergreen';
3 import { RILSA_DEFAULTS } from '@/domain/rilsa/constants';
4 import { buildSignalPlan } from '@/domain/plan/signalPlan';
5 import { validateProject } from '@/domain/validation';
6 import { ASSISTENT_VORGABE, projektAusAngaben } from '@/ui/assistent';
7 import type { ClearingStream, EnteringStream } from '@/domain/rilsa/types';
8
9 /**
10 * Nicht schneller raeumen, als gefahren werden darf.
11 *
12 * DER BEFUND KAM AUS EINEM ANGEORDNETEN PLAN, nicht aus dem Quelltext. Am
13 * 10.09.2026 sind zwei reale, behoerdlich abgesegnete Anordnungen gegen dieses
14 * Programm gerechnet worden:
15 *
16 * - Plan A: einstreifige Fuehrung, 100 m Engstelle, Raeumung mit 30 km/h,
17 * gerechnet mit fremder Software:
18 * **16 s**. Dieses Programm rechnete **14 s** - zwei Sekunden KUERZER als
19 * die Unterlage, die die Behoerde abgesegnet hat.
20 * - Plan B: 40 m Raeumweg, 50 km/h, angeordnet mit
21 * 6 s. Dieses Programm rechnet 8 s - zwei Sekunden laenger, und das ist die
22 * gewollte Korrektur gegenueber der Fassung 4.x.
23 *
24 * BEIDE ABWEICHUNGEN HABEN DIESELBE URSACHE, und sie zeigen in
25 * entgegengesetzte Richtungen: Die fremde Software und die Fassung 4.x rechnen
26 * mit vr = Vmax/3,6, dieses Programm mit dem festen Regelwert 10,0 m/s. Ueber
27 * 36 km/h ist der feste Wert der kleinere und damit der sichere - das ist die
28 * Korrektur, die 5.0 ausdruecklich eingefuehrt hat. UNTER 36 km/h kehrt sich
29 * das um: Bei 30 km/h raeumte ein Fahrzeug rechnerisch mit 36 km/h, also
30 * schneller, als es fahren darf, und die Zwischenzeit fiel zu kurz aus.
31 *
32 * DER GRUNDSATZ STAND SCHON IM PROGRAMM, nur nicht an dieser Verkehrsart:
33 * `SAFETY_FLOORS.maxClearingSpeedOepnv` traegt den Satz "nicht schneller
34 * raeumen, als gefahren werden darf" im Kommentar, und fuer den OePNV wird er
35 * mit `Math.min(..., vZul / 3.6)` auch angewandt. Fuer Kraftfahrzeuge galt
36 * allein der feste Deckel von 10,0 m/s, ohne Bezug zur Vmax des Stroms.
37 *
38 * WAS DIESE DATEI BEWACHT: dass der Deckel gilt, dass er nur nach unten wirkt,
39 * dass er die Einfahrgeschwindigkeit NICHT anfasst und dass er sich meldet.
40 */
41
42 const D = RILSA_DEFAULTS;
43
44 function raeumt(teil: Partial<ClearingStream> = {}): ClearingStream {
45 return {
46 mode: 'kfz',
47 movement: 'geradeaus',
48 clearingDistance: 100,
49 vehicleClass: 'pkw',
50 vZul: 30,
51 ...teil,
52 };
53 }
54
55 function faehrtEin(teil: Partial<EnteringStream> = {}): EnteringStream {
56 return { mode: 'kfz', movement: 'geradeaus', enteringDistance: 0, ...teil };
57 }
58
59 describe('Der Deckel an der zulaessigen Hoechstgeschwindigkeit', () => {
60 it('rechnet den angeordneten Plan A nach', () => {
61 /*
62 * DER FALL, DER DEN BEFUND AUSGELOEST HAT. 100 m Engstelle, 30 km/h.
63 *
64 * Rechenweg mit dem Deckel: sr = 100 m + 6 m Laengenzuschlag des Pkw =
65 * 106 m; vr = 30 / 3,6 = 8,333 m/s; tr = 12,72 s; te = 0 s (der
66 * Gegenverkehr steht an der Haltlinie); tue = 3 s (Gelbzeit bei 30 km/h).
67 * tz = 15,72 s, aufgerundet 16 s - genau die angeordnete Zahl.
68 *
69 * Ohne den Deckel waeren es 10,6 s Raeumzeit und 14 s Zwischenzeit.
70 */
71 const r = computeIntergreen(raeumt(), faehrtEin(), D);
72 expect(r.clearingSpeed, 'geraeumt wird mit der Vmax des Stroms').toBeCloseTo(30 / 3.6, 6);
73 expect(r.intergreen, 'die angeordnete Zwischenzeit').toBe(16);
74 });
75
76 it('laesst den Regelwert stehen, wo er ohnehin der kleinere ist', () => {
77 /*
78 * Die Gegenprobe zur Fassung 4.x. Bei 50 km/h waere Vmax/3,6 = 13,9 m/s -
79 * deutlich ueber dem Regelwert. Der Deckel darf dort NICHTS tun, sonst
80 * naehme er die Korrektur zurueck, die 5.0 eingefuehrt hat: Der
81 * angeordnete Plan B traegt 6 s, dieses Programm rechnet 8 s, und
82 * das soll so bleiben.
83 */
84 const r = computeIntergreen(raeumt({ clearingDistance: 40, vZul: 50 }), faehrtEin(), D);
85 expect(r.clearingSpeed, 'der Regelwert gilt weiter').toBe(D.clearingSpeed.kfzStraight);
86 expect(r.intergreen).toBe(8);
87 });
88
89 it('greift genau unterhalb von 36 km/h', () => {
90 // 10,0 m/s entsprechen 36 km/h. Darueber ist der Regelwert der kleinere,
91 // darunter die Vmax - die Grenze liegt also dort, wo beide gleich sind.
92 expect(regelRaeumgeschwindigkeit({ mode: 'kfz', movement: 'geradeaus', vZul: 36 }, D)).toBe(10);
93 expect(
94 regelRaeumgeschwindigkeit({ mode: 'kfz', movement: 'geradeaus', vZul: 35 }, D),
95 ).toBeCloseTo(35 / 3.6, 6);
96 expect(regelRaeumgeschwindigkeit({ mode: 'kfz', movement: 'geradeaus', vZul: 40 }, D)).toBe(10);
97 });
98
99 it('ruehrt den abbiegenden Strom nicht an, der ohnehin langsamer raeumt', () => {
100 // 7,0 m/s liegen unter 8,33 m/s; der Deckel darf nicht nach OBEN wirken.
101 expect(regelRaeumgeschwindigkeit({ mode: 'kfz', movement: 'links', vZul: 30 }, D)).toBe(
102 D.clearingSpeed.kfzTurning,
103 );
104 });
105
106 it('ruehrt Fussgaenger und Radverkehr nicht an', () => {
107 /*
108 * Ihre Raeumgeschwindigkeit hat mit der zulaessigen Hoechstgeschwindigkeit
109 * des Fahrverkehrs nichts zu tun: Ein Fussgaenger geht nicht schneller,
110 * weil dort 50 gefahren werden darf, und nicht langsamer, weil dort 30
111 * gilt.
112 *
113 * GEMESSEN AN EINER MUTATION: Mit 30 km/h bewies dieser Fall gar nichts -
114 * der Deckel laege bei 8,33 m/s, und 1,2 bzw. 4,0 m/s liegen ohnehin
115 * darunter. Wer die Verkehrsartabfrage aus `vmaxDeckel` entfernte, blieb
116 * gruen. Geprueft wird deshalb bei Schrittgeschwindigkeit: 4 km/h ergaeben
117 * einen Deckel von 1,11 m/s, und der laege UNTER der Gehgeschwindigkeit.
118 * Zeichen 325 gibt es wirklich; hier steht es fuer den Grenzfall, an dem
119 * sich die Abfrage ueberhaupt erst zeigt.
120 */
121 expect(regelRaeumgeschwindigkeit({ mode: 'fuss', movement: 'querung', vZul: 4 }, D)).toBe(
122 D.clearingSpeed.fuss,
123 );
124 expect(regelRaeumgeschwindigkeit({ mode: 'rad', movement: 'querung', vZul: 4 }, D)).toBe(
125 D.clearingSpeed.rad,
126 );
127 // Und beim gewoehnlichen Tempo 30 selbstverstaendlich auch.
128 expect(regelRaeumgeschwindigkeit({ mode: 'fuss', movement: 'querung', vZul: 30 }, D)).toBe(
129 D.clearingSpeed.fuss,
130 );
131 });
132
133 it('faellt ohne eingetragene Vmax auf den Regelwert zurueck', () => {
134 // Ohne Vmax gibt es keinen Deckel. Der Regelwert ist dann das Beste, was
135 // das Programm weiss - und er ist konservativer als jede Annahme.
136 expect(regelRaeumgeschwindigkeit({ mode: 'kfz', movement: 'geradeaus', vZul: 0 }, D)).toBe(
137 D.clearingSpeed.kfzStraight,
138 );
139 });
140
141 it('deckelt die EINFAHRgeschwindigkeit ausdruecklich nicht', () => {
142 /*
143 * DIE SCHRANKE LIEGT DORT UNTEN, NICHT OBEN, und wer die Symmetrie
144 * herstellen wollte, kehrte die Sicherheitsrichtung um: In
145 * tz = tue + tr − te wird die Einfahrzeit ABGEZOGEN. Eine niedrigere
146 * Einfahrgeschwindigkeit verlaengert te und VERKUERZT damit die
147 * Zwischenzeit. Bei 30 km/h bleibt es deshalb bei den 11,1 m/s.
148 */
149 const r = computeIntergreen(
150 raeumt({ clearingDistance: 20 }),
151 faehrtEin({ enteringDistance: 10 }),
152 D,
153 );
154 expect(r.enteringSpeed, 'die Einfahrgeschwindigkeit ist gedeckelt worden').toBe(
155 D.enteringSpeed.kfz,
156 );
157 });
158
159 it('sagt im Rechenweg, dass gedeckelt wurde', () => {
160 /*
161 * Eine stille Ersetzung waere hier besonders schlecht: Das
162 * Kennwerteverzeichnis der Planunterlage nennt fuer den geradeaus
163 * fahrenden Kfz-Strom 10,0 m/s, im Rechenweg derselben Unterlage stuenden
164 * 8,33 m/s, und nichts erklaerte den Unterschied.
165 */
166 const r = computeIntergreen(raeumt(), faehrtEin(), D);
167 const notiz = r.notes.find((n) => n.code === 'raeumgeschwindigkeit-auf-vmax-gedeckelt');
168 expect(notiz, 'die Kappung meldet sich nicht').toBeDefined();
169 expect(notiz?.severity, 'das ist der Regelfall und kein Fehler').toBe('hinweis');
170 expect(notiz?.message, 'der Regelwert fehlt in der Meldung').toContain('10 m/s');
171 expect(notiz?.message, 'der angesetzte Wert fehlt in der Meldung').toContain('8,33 m/s');
172 expect(notiz?.message, 'die Vmax fehlt in der Meldung').toContain('30 km/h');
173 });
174
175 it('meldet dabei keinen Fehler ueber die Vorgaben', () => {
176 /*
177 * Die Kappung darf nicht in die Meldung "Räumgeschwindigkeit aus den
178 * Vorgaben über der Obergrenze" laufen. Die waere doppelt falsch: Der Wert
179 * kommt nicht aus den Vorgaben, sondern aus dem Regelwerk, und er ist kein
180 * Fehler, sondern der Regelfall an jeder Anlage mit Tempo 30.
181 */
182 const r = computeIntergreen(raeumt(), faehrtEin(), D);
183 expect(r.notes.filter((n) => n.severity === 'fehler')).toEqual([]);
184 expect(r.notes.map((n) => n.code)).not.toContain('raeumgeschwindigkeit-vorgabe-zu-hoch');
185 });
186
187 it('steht die Kappung einmal je Projekt im Pruefbericht', () => {
188 /*
189 * DER RECHENWEG ALLEIN GENUEGT NICHT. Die Hinweise aus
190 * `computeIntergreen` verwirft der Planaufbau, bevor sie in `plan.notes`
191 * gelangen - zu Recht, denn an einer Anlage mit Tempo 30 traefe die
192 * Meldung jede einzelne Beziehung. Gedruckt wird der Pruefbericht aber
193 * aus `plan.notes` und den Regeln; ohne diesen Befund stuende im
194 * Kennwertnachweis der Unterlage 10,0 m/s und im Rechenweg daneben ein
195 * kleinerer Wert, ohne ein Wort dazu.
196 */
197 /*
198 * AM KNOTENPUNKT GEMESSEN (nachgezogen mit 5.27.0): Bis dahin stand hier
199 * eine einstreifige Verkehrsfuehrung mit Tempo 30. Die rechnet das
200 * Programm seit 5.27.0 nach RiLSA 2015, Abschnitt 5.2.2, und dort kommt
201 * die Raeumgeschwindigkeit aus der eigenen Staffel des Abschnitts - der
202 * Deckel aus 5.22.0 greift dann nicht mehr, weil es nichts zu deckeln
203 * gibt. Er gilt unveraendert am Knotenpunkt und an der
204 * Fussgaengerschutzanlage, und genau dort wird er jetzt gemessen. Fuer die
205 * Engstelle hat Abschnitt 5.2 einen eigenen Deckel bekommen (siehe
206 * `engstellenRaeumgeschwindigkeit`); er greift erst unter 30 km/h.
207 */
208 const anlage = projektAusAngaben(
209 { ...ASSISTENT_VORGABE, form: 'kreuzung', vZul: 30 },
210 'Tempo 30',
211 new Date('2025-03-11T12:00:00Z'),
212 );
213 const plan = buildSignalPlan(anlage);
214 const bericht = validateProject(anlage, plan, new Date('2025-03-11T12:00:00Z'));
215 const befunde = bericht.findings.filter(
216 (f) => f.rule === 'zwischenzeiten.raeumgeschwindigkeit-auf-vmax-gedeckelt',
217 );
218 expect(befunde, 'genau ein Befund, nicht einer je Beziehung').toHaveLength(1);
219 expect(befunde[0]?.severity).toBe('hinweis');
220 expect(befunde[0]?.message, 'die betroffenen Gruppen fehlen').toContain('"K1"');
221 expect(befunde[0]?.message, 'die betroffenen Gruppen fehlen').toContain('"K2"');
222
223 // Und bei 50 km/h steht er nicht da.
224 const schnell = projektAusAngaben(
225 { ...ASSISTENT_VORGABE, form: 'einstreifig', vZul: 50, engstellenlaenge: 100 },
226 'Vergleich',
227 new Date('2025-03-11T12:00:00Z'),
228 );
229 const planSchnell = buildSignalPlan(schnell);
230 expect(
231 validateProject(schnell, planSchnell, new Date('2025-03-11T12:00:00Z')).findings.map(
232 (f) => f.rule,
233 ),
234 'bei 50 km/h wird nichts gedeckelt',
235 ).not.toContain('zwischenzeiten.raeumgeschwindigkeit-auf-vmax-gedeckelt');
236 });
237
238 it('laesst eine ueberhoehte Vorgabe weiter auffallen', () => {
239 /*
240 * Die Gegenrichtung: Ein Kennwertsatz mit 12 m/s liegt sowohl ueber der
241 * harten Schranke von 10 m/s als auch ueber der Vmax. Beides sind eigene
242 * Sachverhalte, und der Fehler zur Vorgabe darf nicht dadurch verschwinden,
243 * dass der Deckel schon gegriffen hat.
244 */
245 const ueberhoeht = {
246 ...D,
247 clearingSpeed: { ...D.clearingSpeed, kfzStraight: 12 },
248 };
249 const r = computeIntergreen(raeumt(), faehrtEin(), ueberhoeht);
250 expect(r.notes.map((n) => n.code)).toContain('raeumgeschwindigkeit-vorgabe-zu-hoch');
251 expect(r.clearingSpeed, 'am Ende gilt trotzdem die Vmax').toBeCloseTo(30 / 3.6, 6);
252 });
253 });