lsa-planer

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

/ tests domain engstellensignalisierung.test.ts

12,4 KB Rohdatei
tests/domain/engstellensignalisierung.test.ts — 290 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import {
3 engstellenFreigabezeit,
4 engstellenRaeumgeschwindigkeit,
5 engstellenUmlaufzeit,
6 engstellenZwischenzeit,
7 } from '@/domain/rilsa/engstellensignalisierung';
8 import { computeEngstellenIntergreen, computeIntergreen } from '@/domain/rilsa/intergreen';
9 import { RILSA_ENGSTELLE } from '@/domain/rilsa/constants';
10 import { buildSignalPlan } from '@/domain/plan/signalPlan';
11 import { validateProject } from '@/domain/validation';
12 import {
13 createConflict,
14 createEmptyProject,
15 createPhase,
16 createSignalGroup,
17 } from '@/domain/model/factory';
18 import type { Project } from '@/domain/model/project';
19
20 /**
21 * Das eigene Rechenverfahren der Engstellensignalisierung
22 * (RiLSA 2015, Abschnitt 5.2), eingebaut mit der Fassung 5.27.0.
23 *
24 * WAS DIESE WACHE HAELT, ist nicht nur ein Satz Zahlen, sondern die
25 * Entscheidung dahinter: An einer einstreifigen Verkehrsfuehrung gilt ein
26 * ANDERES Verfahren als am Knotenpunkt, und zwar fuer den
27 * Kraftfahrzeugverkehr. Bis 5.26.0 rechnete das Programm dort die
28 * Knotenpunktformel. Jeder Fall nennt deshalb beide Zahlen - die von heute und
29 * die von damals -, sonst laesst sich nicht erkennen, was sich geaendert hat.
30 */
31
32 const T = new Date('2026-01-01T00:00:00Z');
33 const OHNE_EINFAHRWEG = { mode: 'kfz', movement: 'geradeaus', enteringDistance: 0 } as const;
34
35 const raeumt = (clearingDistance: number, vZul: number) =>
36 ({ mode: 'kfz', movement: 'geradeaus', clearingDistance, vehicleClass: 'pkw', vZul }) as const;
37
38 describe('Raeumgeschwindigkeit der Engstelle (Abschnitt 5.2.2)', () => {
39 it.each([
40 [60, 50],
41 [70, 50],
42 [50, 40],
43 [40, 30],
44 ])('setzt bei zulaessig %s km/h eine Raeumgeschwindigkeit von %s km/h an', (vZul, erwartet) => {
45 const ergebnis = engstellenRaeumgeschwindigkeit(vZul);
46 expect(ergebnis.vrKmh).toBe(erwartet);
47 expect(ergebnis.grund).toBe('vzul');
48 });
49
50 it('nimmt unterhalb der Staffel die kleinste Stufe', () => {
51 // Die Staffel endet bei 40 km/h; darunter nennt die RiLSA keinen Wert.
52 expect(engstellenRaeumgeschwindigkeit(35)).toEqual({ vrKmh: 30, grund: 'unter-staffel' });
53 expect(engstellenRaeumgeschwindigkeit(30)).toEqual({ vrKmh: 30, grund: 'unter-staffel' });
54 });
55
56 it('raeumt nie schneller, als gefahren werden darf', () => {
57 /*
58 * Der Deckel aus 5.22.0, hier noch einmal: Bei Tempo 20 laege die
59 * kleinste Stufe der Staffel (30 km/h) ueber der zulaessigen
60 * Hoechstgeschwindigkeit. Ein Ansatz DIESES Programms - die RiLSA nennt
61 * fuer diesen Fall nichts - und die sichere Richtung.
62 */
63 expect(engstellenRaeumgeschwindigkeit(20)).toEqual({ vrKmh: 20, grund: 'vmax-deckel' });
64 });
65
66 it('setzt bei schlechter Fahrbahn 30 km/h an, unabhaengig von der Vzul', () => {
67 expect(engstellenRaeumgeschwindigkeit(60, { schlechteFahrbahn: true })).toEqual({
68 vrKmh: 30,
69 grund: 'fahrbahn',
70 });
71 });
72
73 it('macht den Radverkehr mit 18 km/h massgebend, wo er es ist', () => {
74 expect(engstellenRaeumgeschwindigkeit(60, { radverkehrMassgebend: true })).toEqual({
75 vrKmh: 18,
76 grund: 'radverkehr',
77 });
78 });
79
80 it('nimmt bei zwei Sonderfaellen den langsameren', () => {
81 // Eine Engstelle wird nicht dadurch schneller, dass zwei Gruende fuer
82 // Langsamkeit zusammentreffen.
83 expect(
84 engstellenRaeumgeschwindigkeit(60, { schlechteFahrbahn: true, radverkehrMassgebend: true })
85 .vrKmh,
86 ).toBe(18);
87 });
88 });
89
90 describe('Zwischenzeit der Engstelle (Abschnitt 5.2.2)', () => {
91 it('rechnet tz = tue + sr/Vr * 3,6 mit fester Ueberfahrzeit von 4 s', () => {
92 const ergebnis = engstellenZwischenzeit(400, 50);
93 expect(ergebnis.ueberfahrzeit).toBe(RILSA_ENGSTELLE.ueberfahrzeit);
94 expect(ergebnis.ueberfahrzeit).toBe(4);
95 expect(ergebnis.raeumzeit).toBe(36);
96 expect(ergebnis.roh).toBe(40);
97 expect(ergebnis.zwischenzeit).toBe(40);
98 });
99
100 it('nimmt den Raeumweg OHNE Fahrzeuglaenge', () => {
101 /*
102 * Der Unterschied, der am leichtesten uebersehen wird: Am Knotenpunkt ist
103 * sr = s0 + lFZ, an der Engstelle ist sr der Abstand der Haltlinien. Die
104 * Gegenprobe rechnet dieselbe Geometrie nach beiden Verfahren.
105 */
106 const engstelle = computeEngstellenIntergreen(raeumt(400, 50), OHNE_EINFAHRWEG);
107 const knotenpunkt = computeIntergreen(raeumt(400, 50), OHNE_EINFAHRWEG);
108 expect(engstelle.clearingPath, 'ohne Fahrzeuglaenge').toBe(400);
109 expect(knotenpunkt.clearingPath, 'mit 6 m Pkw').toBe(406);
110 expect(engstelle.vehicleLength).toBe(0);
111 expect(knotenpunkt.vehicleLength).toBe(6);
112 });
113
114 it('zieht keine Einfahrzeit ab und sagt es', () => {
115 // "Die Zwischenzeit tz wird ohne Beruecksichtigung einer Einfahrzeit
116 // ermittelt." Ein erfasster Einfahrweg geht damit in keine Zahl ein.
117 const mitWeg = computeEngstellenIntergreen(raeumt(400, 50), {
118 mode: 'kfz',
119 movement: 'geradeaus',
120 enteringDistance: 20,
121 });
122 expect(mitWeg.enteringTime).toBe(0);
123 expect(mitWeg.intergreen).toBe(40);
124 const meldung = mitWeg.notes.find((n) => n.code === 'engstelle-einfahrweg-ohne-wirkung');
125 expect(meldung?.severity).toBe('hinweis');
126 expect(meldung?.message).toContain('5.2.2');
127 });
128
129 it('kennzeichnet das Verfahren im Ergebnis', () => {
130 expect(computeEngstellenIntergreen(raeumt(400, 50), OHNE_EINFAHRWEG).verfahren).toBe(
131 'engstelle',
132 );
133 expect(computeIntergreen(raeumt(400, 50), OHNE_EINFAHRWEG).verfahren).toBe('knotenpunkt');
134 });
135
136 it('faellt bei 40 km/h laenger aus als die Knotenpunktrechnung, bei 50 km/h kuerzer', () => {
137 /*
138 * DIE RICHTUNG HAENGT AN DER ZULAESSIGEN HOECHSTGESCHWINDIGKEIT, und das
139 * ist der Grund, warum diese Umstellung nicht pauschal "sicherer" oder
140 * "unsicherer" ist:
141 *
142 * 50 km/h: Vr 40 km/h = 11,11 m/s gegen 10,0 m/s - schneller, und der
143 * Raeumweg ist 6 m kuerzer. Oberhalb von 40 m Engstelle faellt
144 * die Zwischenzeit deshalb KUERZER aus.
145 * 40 km/h: Vr 30 km/h = 8,33 m/s gegen 10,0 m/s - langsamer. Die
146 * Zwischenzeit faellt LAENGER aus.
147 */
148 expect(computeEngstellenIntergreen(raeumt(200, 50), OHNE_EINFAHRWEG).intergreen).toBe(22);
149 expect(computeIntergreen(raeumt(200, 50), OHNE_EINFAHRWEG).intergreen).toBe(24);
150
151 expect(computeEngstellenIntergreen(raeumt(200, 40), OHNE_EINFAHRWEG).intergreen).toBe(28);
152 expect(computeIntergreen(raeumt(200, 40), OHNE_EINFAHRWEG).intergreen).toBe(24);
153 });
154
155 it('trifft bei 40 m und 50 km/h dieselbe Sekunde wie die Knotenpunktrechnung', () => {
156 // Ein nachgerechneter angeordneter Plan. Beide Wege ergeben 7,6 s -> 8 s.
157 expect(computeEngstellenIntergreen(raeumt(40, 50), OHNE_EINFAHRWEG).intergreen).toBe(8);
158 expect(computeIntergreen(raeumt(40, 50), OHNE_EINFAHRWEG).intergreen).toBe(8);
159 });
160 });
161
162 describe('Die Handeingabe wirkt an der Engstelle nur nach unten', () => {
163 it('nimmt eine langsamere Raeumgeschwindigkeit an der Beziehung an', () => {
164 const langsam = computeEngstellenIntergreen(
165 { ...raeumt(400, 50), clearingSpeedOverride: 8 },
166 OHNE_EINFAHRWEG,
167 );
168 expect(langsam.clearingSpeed).toBe(8);
169 expect(langsam.intergreen).toBe(54);
170 });
171
172 it('verwirft eine schnellere und meldet das', () => {
173 const schnell = computeEngstellenIntergreen(
174 { ...raeumt(400, 50), clearingSpeedOverride: 13 },
175 OHNE_EINFAHRWEG,
176 );
177 expect(schnell.intergreen).toBe(40);
178 const meldung = schnell.notes.find(
179 (n) => n.code === 'engstelle-raeumgeschwindigkeit-ueber-regelwert',
180 );
181 expect(meldung?.severity).toBe('warnung');
182 });
183 });
184
185 describe('Umlaufzeit und Freigabezeit der Engstelle (Abschnitt 5.2.3)', () => {
186 it('rechnet tU = (1,3 * TZ + 4) / (1 - Summe der Auslastungsgrade)', () => {
187 // TZ = 2 x 20 s, q1/qS1 + q2/qS2 = 0,4: (1,3 * 40 + 4) / 0,6 = 93,33 s.
188 expect(engstellenUmlaufzeit(40, 0.4)).toBeCloseTo(93.333, 3);
189 // Andere Beiwerte als Webster (1,5 und 5) und eine andere Bezugsgroesse:
190 // TZ ist die Summe der ZWISCHENzeiten, nicht die Verlustzeit je Umlauf.
191 expect(RILSA_ENGSTELLE.umlaufzeitFaktor).toBe(1.3);
192 expect(RILSA_ENGSTELLE.umlaufzeitZuschlag).toBe(4);
193 });
194
195 it('liefert unendlich, wo die Summe der Auslastungsgrade 1 erreicht', () => {
196 // "Dabei muss (q1/qS1 + q2/qS2) < 1 sein."
197 expect(engstellenUmlaufzeit(40, 1)).toBe(Number.POSITIVE_INFINITY);
198 expect(engstellenUmlaufzeit(40, 1.2)).toBe(Number.POSITIVE_INFINITY);
199 });
200
201 it('verteilt die verbleibende Zeit im Verhaeltnis der Auslastungsgrade', () => {
202 // tU 100 s, TZ 40 s, Auslastungsgrade 0,3 und 0,1: 45 s und 15 s.
203 expect(engstellenFreigabezeit(0.3, 0.4, 100, 40)).toBeCloseTo(45, 6);
204 expect(engstellenFreigabezeit(0.1, 0.4, 100, 40)).toBeCloseTo(15, 6);
205 });
206
207 it('nennt 1500 Kfz/h als ueberschlaegliche Saettigungsverkehrsstaerke', () => {
208 // Am Knotenpunkt rechnet dieses Programm mit 2000 Kfz/h nach HBS; fuer die
209 // Engstelle nennt die RiLSA diesen kleineren Anhaltswert.
210 expect(RILSA_ENGSTELLE.saettigungUeberschlaeglich).toBe(1500);
211 expect(RILSA_ENGSTELLE.maxUmlaufzeit).toBe(300);
212 });
213 });
214
215 describe('Der Plan waehlt das Verfahren nach der Anlagenart', () => {
216 function baustelle(anlagenart: 'einstreifig' | 'knotenpunkt'): Project {
217 const basis = createEmptyProject('Baustelle', T, anlagenart);
218 const a = createSignalGroup({ name: 'K1', mode: 'kfz', vZul: 50, index: 0 });
219 const b = createSignalGroup({ name: 'K2', mode: 'kfz', vZul: 50, index: 1 });
220 const p1 = createPhase('Seite A', [a.id]);
221 const p2 = createPhase('Seite B', [b.id]);
222 return {
223 ...basis,
224 signalGroups: [a, b],
225 conflicts: [
226 { ...createConflict(a.id, b.id), clearingDistance: 200, enteringDistance: 0 },
227 { ...createConflict(b.id, a.id), clearingDistance: 200, enteringDistance: 0 },
228 ],
229 phases: [p1, p2],
230 program: { ...basis.program, phaseOrder: [p1.id, p2.id] },
231 };
232 }
233
234 it('rechnet die einstreifige Verkehrsfuehrung nach Abschnitt 5.2', () => {
235 const plan = buildSignalPlan(baustelle('einstreifig'));
236 for (const uebergang of plan.transitions) expect(uebergang.duration).toBe(22);
237 for (const zwischenzeit of plan.intergreens.values()) {
238 expect(zwischenzeit.calculation?.verfahren).toBe('engstelle');
239 }
240 });
241
242 it('laesst den Knotenpunkt bei Abschnitt 2', () => {
243 // Dieselbe Geometrie, andere Anlagenart: 3 + 206/10 = 23,6 -> 24 s.
244 const plan = buildSignalPlan(baustelle('knotenpunkt'));
245 for (const uebergang of plan.transitions) expect(uebergang.duration).toBe(24);
246 for (const zwischenzeit of plan.intergreens.values()) {
247 expect(zwischenzeit.calculation?.verfahren).toBe('knotenpunkt');
248 }
249 });
250
251 it('nennt der Pruefbericht das Verfahren und die beiden ungeprueften Annahmen', () => {
252 /*
253 * Das Ergebnis haengt an zwei Aussagen ueber die Oertlichkeit, die dieses
254 * Programm nicht pruefen kann. Beide senken die Raeumgeschwindigkeit; wer
255 * sie uebersieht, schaltet zu kurz. Der Bericht nennt sie mit den Zahlen
256 * DIESER Planung.
257 */
258 const bericht = validateProject(baustelle('einstreifig'), undefined, T);
259 const befund = bericht.findings.find((f) => f.rule === 'einstreifig.verfahren-abschnitt-5-2');
260 expect(befund?.severity).toBe('hinweis');
261 expect(befund?.message).toContain('40 km/h');
262 // Schlechte Fahrbahn: 30 km/h -> 4 + 200/30 * 3,6 = 28 s.
263 expect(befund?.message).toContain('30 km/h');
264 expect(befund?.message).toContain('28 s');
265 // Radverkehr massgebend: 18 km/h -> 4 + 200/18 * 3,6 = 44 s.
266 expect(befund?.message).toContain('18 km/h');
267 expect(befund?.message).toContain('44 s');
268 expect(befund?.reference).toContain('5.2.2');
269 });
270
271 it('meldet das Verfahren am Knotenpunkt nicht', () => {
272 const bericht = validateProject(baustelle('knotenpunkt'), undefined, T);
273 expect(bericht.findings.some((f) => f.rule === 'einstreifig.verfahren-abschnitt-5-2')).toBe(
274 false,
275 );
276 });
277
278 it('haelt die Untergrenze aus Abschnitt 2.5.2 von der Engstelle fern', () => {
279 /*
280 * Die Untergrenze tue + tr >= tG + 1 s gilt den Faellen 1 und 2 des
281 * Abschnitts 2.5.2 - also der Knotenpunktrechnung. Das Engstellenverfahren
282 * hat eine eigene, laengere Ueberfahrzeit und keine solche Bedingung; sie
283 * dorthin zu uebertragen hiesse, das Regelwerk zu ergaenzen.
284 */
285 const kurz = computeEngstellenIntergreen(raeumt(1, 70), OHNE_EINFAHRWEG);
286 expect(kurz.ueberfahrRaeumzeitUntergrenze).toBeNull();
287 // 4 + 1/50 * 3,6 = 4,07 -> 5 s. Mit der Untergrenze waeren es 6 s.
288 expect(kurz.intergreen).toBe(5);
289 });
290 });