lsa-planer

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

/ src domain rilsa delay.ts

30,9 KB Rohdatei
src/domain/rilsa/delay.ts — 764 Zeilen
1 import type { Meters, Ratio, Seconds, VehiclesPerHour } from '../units';
2 import { isFiniteNumber, roundTo } from '../units';
3 import {
4 RILSA_DEFAULTS,
5 SERVICE_LEVEL_F_UEBERLASTET,
6 SERVICE_LEVEL_SCALES,
7 STAURAUMBEDARF_JE_FAHRZEUG,
8 } from './constants';
9 import type { Bewertungsverfahren, CalculationNote, ServiceLevel, TrafficMode } from './types';
10
11 /**
12 * Wartezeit und Qualitaetsstufe einer Signalgruppe.
13 *
14 * KORREKTUR (Fassung 5.4.0, Befund B2): Bis dahin rechnete diese
15 * Datei fuer beide Einstellungen dieselbe HCM-Formel (d1 + d2 mit T = 0,25 h,
16 * k = 0,5, I = 1,0), und nur die Stufentafel wechselte - waehrend der Ausdruck
17 * "Bewertung nach HBS 2015" behauptete. Bei hoher Auslastung (x ab etwa 0,9)
18 * liefert die HCM-Formel deutlich kleinere Wartezeiten als das HBS-Verfahren:
19 * zu gute Stufen genau dort, wo die Bewertung kippt.
20 *
21 * Jetzt gibt es zwei Verfahren, und jedes rechnet, was es behauptet:
22 *
23 * HBS 2015 (Leyn 2016, KIT-Dissertation, Kap. 2.3.3, Gl. 2-7 bis 2-10, und
24 * Kap. 3.3.2, Gl. 3-2 - die Dissertation gibt das HBS-Verfahren woertlich
25 * wieder; die Formelkette ist am LISA+-Ausdruck "HBS-Bewertung 2015"
26 * Kornwestheim 2017 nachgerechnet, siehe tests/domain/hbsWartezeit.test.ts):
27 *
28 * tW = tW,G + tW,R (2-7)
29 * tW,G = tU * (1 - fA)^2 / (2 * (1 - min(1; x) * fA)) (2-8)
30 * mit fA = tA / tU und tA = tF + 1 s (Abflusszeit)
31 * Diese geschlossene Form gilt fuer EIN Freigabezeitfenster je
32 * Umlauf. Allgemein steht im Zaehler Summe(r_j^2) / tU ueber die
33 * Sperrzeiten r_j des Umlaufs; siehe summeSperrzeitenQuadrate.
34 * tW,R = NGE * 3600 / C (2-9)
35 * NGE = max{ 0,58*T*C/4 * [ (fin*x - 1) + sqrt((fin*x - 1)^2 + 4*fin*x / (0,58*T*C)) ] ;
36 * T*C/4 * [ (x - 1) + sqrt((x - 1)^2 + 4*x / (T*C)) ] } (2-10)
37 * mit T = 1 h (Betrachtungszeitraum) und
38 * fin = 1 + ((q15/q) - 1) / 1,5 (Instationaritaetsfaktor) (3-2)
39 *
40 * HCM (Highway Capacity Manual, Kapitel signalisierte Knotenpunkte):
41 *
42 * d = d1 + d2
43 * d1 = 0,5 * tU * (1 - g/tU)^2 / (1 - min(1; x) * g/tU)
44 * d2 = 900 * T * [ (x - 1) + sqrt( (x - 1)^2 + 8*k*I*x / (c*T) ) ]
45 * mit T = 0,25 h, k = 0,5 (Festzeit), I = 1,0 (isoliert)
46 *
47 * Aeltere Korrekturen gegenueber dem Altbestand bleiben gueltig: Dort wurde
48 * nur d1 gerechnet, mit fest eingetragenem x = 0,85 statt dem tatsaechlichen,
49 * und die Funktion lief wegen eines nie geschriebenen Feldes faktisch nie.
50 */
51
52 /** Rechenweg der Wartezeit - fuer Ausdruck und Ansicht, damit sich tW nachrechnen laesst. */
53 export interface DelayRechenweg {
54 /** Abflusszeit tA (HBS: tF + 1 s je Fenster) bzw. Freigabezeit g (HCM). */
55 readonly abflusszeit: Seconds;
56 /** Abflusszeitanteil fA = tA/tU bzw. g/tU. */
57 readonly abflussanteil: Ratio;
58 /** Betrachtungszeitraum T in Stunden (HBS 1 h, HCM 0,25 h). */
59 readonly betrachtungszeitraum: number;
60 /** Instationaritaetsfaktor fin (nur HBS, sonst null). */
61 readonly instationaritaetsfaktor: Ratio | null;
62 /** Mittlerer Rueckstau bei Freigabezeitende NGE in Kfz (nur HBS, sonst null). */
63 readonly reststau: number | null;
64 /** HCM-Beiwerte k und I (nur HCM, sonst null). */
65 readonly hcmK: number | null;
66 readonly hcmI: number | null;
67 }
68
69 /** Aufteilung der mittleren Wartezeit in ihre Anteile. */
70 export interface DelayResult {
71 /** Verfahren, nach dem gerechnet und bewertet wurde. */
72 readonly verfahren: Bewertungsverfahren;
73 /** Mittlere Wartezeit je Fahrzeug in Sekunden (HBS: tW; HCM: d). */
74 readonly averageDelay: Seconds;
75 /** Grundwartezeit tW,G (HBS) bzw. gleichmaessiger Anteil d1 (HCM). */
76 readonly uniformDelay: Seconds;
77 /** Wartezeit im Reststau tW,R (HBS) bzw. Ueberlastungsanteil d2 (HCM). */
78 readonly incrementalDelay: Seconds;
79 /** Auslastungsgrad x = q / C. */
80 readonly degreeOfSaturation: Ratio;
81 /**
82 * Maximale Wartezeit: die laengste Sperrzeit im Umlauf. Fuer Fussgaenger
83 * und Radverkehr ist sie nach HBS 2015 das Bewertungskriterium.
84 */
85 readonly maximumDelay: Seconds;
86 /**
87 * Qualitaetsstufe; null, wenn das Verfahren fuer diese Verkehrsart keine
88 * Tafel hat (HCM fuer Fussgaenger und Radverkehr).
89 */
90 readonly serviceLevel: ServiceLevel | null;
91 readonly rechenweg: DelayRechenweg;
92 readonly notes: readonly CalculationNote[];
93 }
94
95 /** Bemessungszeitraum des HCM-Verfahrens in Stunden (Spitzenviertelstunde). */
96 const HCM_ANALYSIS_PERIOD_HOURS = 0.25;
97 /** HCM-Beiwert fuer Festzeitsteuerung. */
98 const HCM_PRETIMED_K = 0.5;
99 /** HCM-Beiwert fuer isolierte Knotenpunkte ohne Koordinierung. */
100 const HCM_ISOLATED_I = 1.0;
101 /** Betrachtungszeitraum des HBS-Verfahrens in Stunden (Bemessungsstunde). */
102 const HBS_BETRACHTUNGSZEITRAUM_HOURS = 1;
103 /** Koeffizient des ersten Terms der Rueckstauformel (HBS 2015, Gl. 2-10 bei Leyn). */
104 const HBS_RESTSTAU_KOEFFIZIENT = 0.58;
105
106 export interface DelayInput {
107 readonly cycleTime: Seconds;
108 /** Freigabezeit tF - Summe aller Freigabezeitfenster im Umlauf. */
109 readonly greenTime: Seconds;
110 /** Kapazitaet C in Fz/h, wie von computeCapacity ausgewiesen (ganzzahlig). */
111 readonly capacity: VehiclesPerHour;
112 /** Verkehrsstaerke q in Fz/h. */
113 readonly demand: VehiclesPerHour;
114 readonly verfahren: Bewertungsverfahren;
115 /**
116 * Verkehrsart - bestimmt die Bewertungstafel. Nur Kfz und OePNV: Fuer
117 * Fussgaenger und Radverkehr gibt es keine Kapazitaet und keine mittlere
118 * Wartezeit als Kriterium; sie laufen ueber computeFussRadDelay. Der Typ
119 * schliesst aus, dass jemand eine mittlere Wartezeit an der Fuss/Rad-Tafel
120 * (maximale Wartezeit) misst.
121 */
122 readonly mode: 'kfz' | 'oepnv';
123 /**
124 * Abflusszeit tA (HBS) bzw. Freigabezeit g (HCM), wie sie in die
125 * Kapazitaet eingegangen ist. Fehlt sie, gilt bei HBS tF + 1 s (ein
126 * Fenster), bei HCM tF.
127 */
128 readonly abflusszeit?: Seconds;
129 /** Laengste Sperrzeit im Umlauf; fehlt sie, gilt tU - tF. */
130 readonly longestRed?: Seconds;
131 /**
132 * Alle Sperrzeiten des Umlaufs (Rot-Abschnitte zwischen den
133 * Freigabezeitfenstern), wie sie sperrzeitenAusFenstern liefert. Nur mit
134 * ihnen laesst sich die Grundwartezeit bei mehr als einem Fenster je Umlauf
135 * richtig bilden; fehlen sie, wird ein einziges Fenster unterstellt.
136 */
137 readonly sperrzeiten?: readonly Seconds[];
138 /** Instationaritaetsfaktor fin (nur HBS); fehlt er, gilt der Vorgabewert. */
139 readonly instationaritaetsfaktor?: Ratio;
140 }
141
142 /**
143 * Mittlere Wartezeit und Qualitaetsstufe eines Kfz- oder OePNV-Stroms.
144 * Fuer Fussgaenger und Radverkehr siehe computeFussRadDelay.
145 */
146 export function computeDelay(input: DelayInput): DelayResult {
147 const notes: CalculationNote[] = [];
148 const { cycleTime, capacity, verfahren, mode } = input;
149
150 if (!isFiniteNumber(cycleTime) || cycleTime <= 0) {
151 notes.push({
152 severity: 'fehler',
153 code: 'umlaufzeit-ungueltig',
154 message: 'Die Umlaufzeit ist kein gültiger Wert; die Wartezeit ist nicht bestimmbar.',
155 });
156 return emptyResult(input, notes);
157 }
158
159 const green = clampGreen(input.greenTime, cycleTime, notes);
160 const tA = resolveAbflusszeit(input, green, cycleTime);
161 const fA = tA / cycleTime;
162 const maximumDelay = resolveLongestRed(input, green, cycleTime);
163
164 if (!isFiniteNumber(capacity) || capacity <= 0) {
165 notes.push({
166 severity: 'fehler',
167 code: 'kapazitaet-null',
168 message:
169 'Die Kapazität beträgt 0 Fz/h. Ohne Freigabezeit entsteht ein unbegrenzter Rückstau; ' +
170 'eine mittlere Wartezeit lässt sich nicht angeben.',
171 });
172 return emptyResult(input, notes, maximumDelay);
173 }
174
175 const q = isFiniteNumber(input.demand) && input.demand >= 0 ? input.demand : 0;
176 const x = q / capacity;
177 // Zaehler der Grundwartezeit; null, wenn keine brauchbaren Sperrzeiten
178 // vorliegen - dann gilt die geschlossene Einfensterform tU * (1 - fA)^2.
179 const sperrzeitenanteil = summeSperrzeitenQuadrate(input.sperrzeiten, tA, cycleTime);
180 const grundwartezeitZaehler = sperrzeitenanteil ?? cycleTime * (1 - fA) ** 2;
181
182 let uniform: number;
183 let incremental: number;
184 let rechenweg: DelayRechenweg;
185
186 if (verfahren === 'HBS') {
187 const fin = resolveInstationaritaet(input, notes);
188 const T = HBS_BETRACHTUNGSZEITRAUM_HOURS;
189
190 // Gl. 2-8: Grundwartezeit. Im Zaehler steht Summe(r_j^2) / tU ueber die
191 // Sperrzeiten des Umlaufs; bei einem Fenster je Umlauf ist das die
192 // geschlossene Form tU * (1 - fA)^2. Der Nenner wird bei x -> 1 und
193 // fA -> 1 klein; die Begrenzung von x auf 1 haelt ihn positiv.
194 const denominator = 2 * (1 - Math.min(1, x) * fA);
195 uniform = denominator > 1e-9 ? grundwartezeitZaehler / denominator : 0.5 * cycleTime * (1 - fA);
196
197 // Gl. 2-10: mittlerer Rueckstau bei Freigabezeitende, groesserer der
198 // beiden Terme (Instationaritaet bzw. reine Akcelik-Form).
199 const reststau = hbsReststau(x, capacity, T, fin);
200 // Gl. 2-9: Wartezeit im Reststau.
201 incremental = (reststau * 3600) / capacity;
202
203 rechenweg = {
204 abflusszeit: roundTo(tA, 3),
205 abflussanteil: roundTo(fA, 4),
206 betrachtungszeitraum: T,
207 instationaritaetsfaktor: fin,
208 reststau: roundTo(reststau, 3),
209 hcmK: null,
210 hcmI: null,
211 };
212 } else {
213 const T = HCM_ANALYSIS_PERIOD_HOURS;
214 // d1 ist dieselbe Webster-Form wie Gl. 2-8 und wird ebenso ueber die
215 // Sperrzeiten des Umlaufs gebildet.
216 const denominator = 1 - Math.min(1, x) * fA;
217 uniform =
218 denominator > 1e-9 ? (0.5 * grundwartezeitZaehler) / denominator : 0.5 * cycleTime * (1 - fA);
219 const radicand = (x - 1) ** 2 + (8 * HCM_PRETIMED_K * HCM_ISOLATED_I * x) / (capacity * T);
220 incremental = 900 * T * (x - 1 + Math.sqrt(Math.max(0, radicand)));
221
222 rechenweg = {
223 abflusszeit: roundTo(tA, 3),
224 abflussanteil: roundTo(fA, 4),
225 betrachtungszeitraum: T,
226 instationaritaetsfaktor: null,
227 reststau: null,
228 hcmK: HCM_PRETIMED_K,
229 hcmI: HCM_ISOLATED_I,
230 };
231 }
232
233 const averageDelay = uniform + incremental;
234
235 if (x > 1) {
236 notes.push({
237 severity: 'fehler',
238 code: 'ueberlastet',
239 message:
240 `Der Auslastungsgrad beträgt ${formatRatio(x)}. Die Wartezeit wächst während des ` +
241 'Bemessungszeitraums stetig an; der angegebene Wert gilt nur für diesen Zeitraum.',
242 });
243 }
244
245 /*
246 * Bewertet wird der Wert, der auch AUSGEGEBEN wird.
247 *
248 * Zuvor stand hier die ungerundete Wartezeit, waehrend darueber die
249 * gerundete zurueckgegeben wurde. Bei 35,02 s las der Pruefer dann "35,0 s"
250 * neben der Stufe C - waehrend die Stufentafel im selben Dokument bis
251 * einschliesslich 35 s die Stufe B ausweist. Die Zeile widerlegt sich damit
252 * selbst, und zwar zu Ungunsten der Anlage: Die Bewertung faellt eine Stufe
253 * zu STRENG aus.
254 *
255 * Das Fenster ist mit 0,05 s je Stufengrenze schmal. Eine Planunterlage, in
256 * der Zahl und Bewertung nicht zusammenpassen, ist an dieser Zeile aber
257 * nicht mehr zu gebrauchen - und niemand kann den Widerspruch aufloesen,
258 * weil der ungerundete Wert nirgends steht.
259 *
260 * Die Rundung auf eine Zehntelsekunde vor der Bewertung ist fachlich
261 * unbedenklich: Sie liegt weit unterhalb der Genauigkeit, mit der eine
262 * mittlere Wartezeit ueberhaupt ermittelt wird.
263 *
264 * Dasselbe gilt fuer die Stufe F bei Ueberlastung: Sie haengt an q > C, und
265 * beide Zahlen stehen ganzzahlig im Ausdruck.
266 */
267 const ausgegebeneWartezeit = roundTo(averageDelay, 1);
268 /*
269 * Die Anteile muessen auf den gedruckten Ziffern aufgehen: tW = tW,G + tW,R
270 * (Gl. 2-7) steht so im Ausdruck nebeneinander. Werden alle drei unabhaengig
271 * gerundet, ergibt 37,85 + 21,17 = 59,02 gedruckt "37,9 + 21,2 = 59,0" - eine
272 * Zeile, die sich selbst widerlegt. Deshalb wird nur die Grundwartezeit
273 * eigenstaendig gerundet und der Reststauanteil als Differenz gebildet.
274 */
275 const ausgegebeneGrundwartezeit = roundTo(uniform, 1);
276 const ausgegebenerReststauanteil = roundTo(ausgegebeneWartezeit - ausgegebeneGrundwartezeit, 1);
277
278 return {
279 verfahren,
280 averageDelay: ausgegebeneWartezeit,
281 uniformDelay: ausgegebeneGrundwartezeit,
282 incrementalDelay: ausgegebenerReststauanteil,
283 degreeOfSaturation: roundTo(x, 4),
284 maximumDelay: roundTo(maximumDelay, 1),
285 serviceLevel: serviceLevelFor(ausgegebeneWartezeit, verfahren, { mode, degreeOfSaturation: x }),
286 rechenweg,
287 notes,
288 };
289 }
290
291 /**
292 * Zaehler der Grundwartezeit: Summe(r_j^2) / tU ueber die Sperrzeiten r_j
293 * eines Umlaufs. Null, wenn keine brauchbaren Sperrzeiten vorliegen - dann
294 * gilt die geschlossene Einfensterform.
295 *
296 * KORREKTUR (Fassung 5.10.0): Gl. 2-8 ist die Webster-Form fuer EIN
297 * Freigabezeitfenster je Umlauf; dort ist r = tU - tA, und der Zaehler wird zu
298 * (tU - tA)^2 / tU = tU * (1 - fA)^2. Erhaelt eine Signalgruppe mehrere
299 * Fenster, wiederholt sich das Signalbild oefter als einmal je Umlauf, und die
300 * Grundwartezeit richtet sich nach der Summe der QUADRIERTEN Sperrzeiten, nicht
301 * nach dem Quadrat ihrer Summe. Weil Summe(r_j^2) <= (Summe r_j)^2 gilt - mit
302 * Gleichheit nur bei einem Fenster -, fiel die Einfensterform bei jeder
303 * Aufteilung zu gross aus, bei zwei gleich langen Sperrzeiten genau um das
304 * Doppelte. computeFussRadDelay rechnet dieselbe Verallgemeinerung fuer
305 * Fussgaenger und Radverkehr bereits (Grenzfall x * fA -> 0).
306 *
307 * Die uebergebenen Sperrzeiten sind aus den Freigabezeitfenstern gebildet und
308 * enthalten den Zeitzuschlag der Abflusszeit (HBS: 1 s je Fenster) noch. Er
309 * faellt am Ende jedes Fensters an und wird deshalb von jeder Sperrzeit
310 * einzeln abgezogen; damit bleibt Summe(r_j) = tU - tA, und der Einfensterfall
311 * geht unveraendert in die geschlossene Form ueber.
312 */
313 function summeSperrzeitenQuadrate(
314 sperrzeiten: readonly Seconds[] | undefined,
315 tA: Seconds,
316 cycleTime: Seconds,
317 ): number | null {
318 if (sperrzeiten === undefined) return null;
319 const gueltig = sperrzeiten.filter((s) => isFiniteNumber(s) && s > 0);
320 if (gueltig.length === 0) return null;
321 const summe = gueltig.reduce((wert, s) => wert + s, 0);
322 const wirksam = cycleTime - tA;
323 // Passen Sperrzeiten und Abflusszeit nicht zusammen, waere der Zuschlag
324 // negativ und wuerde die Sperrzeiten verlaengern. Dann lieber die
325 // geschlossene Form, die nur an tA haengt.
326 if (wirksam < 0 || summe < wirksam) return null;
327 const zuschlagJeFenster = (summe - wirksam) / gueltig.length;
328 let quadrate = 0;
329 for (const s of gueltig) {
330 const r = Math.max(0, s - zuschlagJeFenster);
331 quadrate += r * r;
332 }
333 return quadrate / cycleTime;
334 }
335
336 /**
337 * Mittlerer Rueckstau bei Freigabezeitende NGE nach HBS 2015 (Gl. 2-10 bei
338 * Leyn 2016): der groessere von zwei Termen. Der erste enthaelt den
339 * Instationaritaetsfaktor und den Koeffizienten 0,58 (nach Leyn im HBS selbst
340 * nicht hergeleitet, geht auf Wu 1990 zurueck), der zweite ist die Formel von
341 * Akcelik (1980) fuer den Betrachtungszeitraum T.
342 */
343 export function hbsReststau(x: Ratio, capacity: VehiclesPerHour, T: number, fin: Ratio): number {
344 const akcelik = (faktor: number, xEff: number): number => {
345 const a = (faktor * T * capacity) / 4;
346 const radicand = (xEff - 1) ** 2 + (4 * xEff) / (faktor * T * capacity);
347 return a * (xEff - 1 + Math.sqrt(Math.max(0, radicand)));
348 };
349 const instationaer = akcelik(HBS_RESTSTAU_KOEFFIZIENT, fin * x);
350 const stationaer = akcelik(1, x);
351 return Math.max(0, instationaer, stationaer);
352 }
353
354 export interface FussRadDelayInput {
355 readonly cycleTime: Seconds;
356 /** Sperrzeiten im Umlauf (Dauer jedes Rot-Abschnitts), siehe sperrzeitenAusFenstern. */
357 readonly sperrzeiten: readonly Seconds[];
358 readonly verfahren: Bewertungsverfahren;
359 readonly mode: 'fuss' | 'rad';
360 }
361
362 /**
363 * Wartezeit und Qualitaetsstufe fuer Fussgaenger und Radverkehr.
364 *
365 * KORREKTUR (Fassung 5.4.0, Befund B4): Das HBS 2015 bewertet
366 * Fussgaenger und Radverkehr an Lichtsignalanlagen nach der MAXIMALEN
367 * Wartezeit - der laengsten Sperrzeit im Umlauf - mit eigener Tafel. Eine
368 * Kapazitaet und ein Auslastungsgrad werden dafuer nicht gebildet.
369 *
370 * Als Kenngroesse wird zusaetzlich die mittlere Wartezeit eines ueber den
371 * Umlauf gleichverteilt eintreffenden Fussgaengers ausgewiesen:
372 * Summe(tS_i^2) / (2 * tU). Sie ist keine Bewertungsgroesse des HBS, aber
373 * eine nachrechenbare Zahl fuer den Vergleich von Varianten.
374 */
375 export function computeFussRadDelay(input: FussRadDelayInput): DelayResult {
376 const notes: CalculationNote[] = [];
377 const { cycleTime, verfahren, mode } = input;
378 const rechenweg: DelayRechenweg = {
379 abflusszeit: 0,
380 abflussanteil: 0,
381 betrachtungszeitraum: 0,
382 instationaritaetsfaktor: null,
383 reststau: null,
384 hcmK: null,
385 hcmI: null,
386 };
387
388 if (!isFiniteNumber(cycleTime) || cycleTime <= 0) {
389 notes.push({
390 severity: 'fehler',
391 code: 'umlaufzeit-ungueltig',
392 message: 'Die Umlaufzeit ist kein gültiger Wert; die Wartezeit ist nicht bestimmbar.',
393 });
394 return {
395 verfahren,
396 averageDelay: Number.POSITIVE_INFINITY,
397 uniformDelay: Number.POSITIVE_INFINITY,
398 incrementalDelay: 0,
399 degreeOfSaturation: 0,
400 maximumDelay: Number.POSITIVE_INFINITY,
401 serviceLevel: serviceLevelFor(Number.POSITIVE_INFINITY, verfahren, { mode }),
402 rechenweg,
403 notes,
404 };
405 }
406
407 const sperrzeiten = input.sperrzeiten.filter((s) => isFiniteNumber(s) && s > 0);
408 const laengste = sperrzeiten.length > 0 ? Math.max(...sperrzeiten) : 0;
409 const mittel = sperrzeiten.reduce((sum, s) => sum + s * s, 0) / (2 * cycleTime);
410
411 if (verfahren === 'HCM') {
412 notes.push({
413 severity: 'hinweis',
414 code: 'keine-fuss-rad-tafel',
415 message:
416 'Für Fußgänger und Radverkehr ist im HCM-Verfahren keine Bewertungstafel hinterlegt; ' +
417 'eine Qualitätsstufe wird nicht vergeben. Nach HBS 2015 würde die maximale Wartezeit bewertet.',
418 });
419 }
420
421 const ausgegebeneMaximale = roundTo(laengste, 1);
422 return {
423 verfahren,
424 averageDelay: roundTo(mittel, 1),
425 uniformDelay: roundTo(mittel, 1),
426 incrementalDelay: 0,
427 degreeOfSaturation: 0,
428 maximumDelay: ausgegebeneMaximale,
429 serviceLevel: serviceLevelFor(ausgegebeneMaximale, verfahren, { mode }),
430 rechenweg,
431 notes,
432 };
433 }
434
435 /**
436 * Sperrzeiten (Rot-Abschnitte) eines Umlaufs aus den Freigabezeitfenstern.
437 * Fenster duerfen ueber den Umlaufbeginn hinweg reichen; sie werden in den
438 * Umlauf gefaltet. Ohne Fenster ist der ganze Umlauf Sperrzeit; deckt ein
439 * Fenster den ganzen Umlauf, gibt es keine.
440 */
441 export function sperrzeitenAusFenstern(
442 fenster: readonly { readonly start: Seconds; readonly duration: Seconds }[],
443 cycleTime: Seconds,
444 ): Seconds[] {
445 if (!isFiniteNumber(cycleTime) || cycleTime <= 0) return [];
446 const gueltig = fenster.filter(
447 (f) => isFiniteNumber(f.start) && isFiniteNumber(f.duration) && f.duration > 0,
448 );
449 if (gueltig.length === 0) return [cycleTime];
450 if (gueltig.some((f) => f.duration >= cycleTime)) return [];
451
452 const modulo = (v: number): number => ((v % cycleTime) + cycleTime) % cycleTime;
453 // Belegte Abschnitte im Umlauf, ueberlaufende Fenster aufgeteilt.
454 const belegt: { von: number; bis: number }[] = [];
455 for (const f of gueltig) {
456 const von = modulo(f.start);
457 const bis = von + f.duration;
458 if (bis <= cycleTime) belegt.push({ von, bis });
459 else {
460 belegt.push({ von, bis: cycleTime });
461 belegt.push({ von: 0, bis: bis - cycleTime });
462 }
463 }
464 belegt.sort((a, b) => a.von - b.von);
465 // Ueberlappungen verschmelzen.
466 const verschmolzen: { von: number; bis: number }[] = [];
467 for (const b of belegt) {
468 const letzter = verschmolzen[verschmolzen.length - 1];
469 if (letzter && b.von <= letzter.bis + 1e-9) letzter.bis = Math.max(letzter.bis, b.bis);
470 else verschmolzen.push({ ...b });
471 }
472 const luecken: number[] = [];
473 for (let i = 0; i < verschmolzen.length; i += 1) {
474 const aktuell = verschmolzen[i]!;
475 const naechster = verschmolzen[(i + 1) % verschmolzen.length]!;
476 const luecke =
477 i + 1 < verschmolzen.length
478 ? naechster.von - aktuell.bis
479 : cycleTime - aktuell.bis + naechster.von;
480 if (luecke > 1e-9) luecken.push(roundTo(luecke, 3));
481 }
482 return luecken;
483 }
484
485 export interface ServiceLevelKontext {
486 /** Verkehrsart; Vorgabe 'kfz'. Bei 'fuss'/'rad' ist `wert` die maximale Wartezeit. */
487 readonly mode?: TrafficMode;
488 /** Auslastungsgrad x = q/C; ueber 1 ergibt fuer Kfz und OePNV die Stufe F (Ueberlastung). */
489 readonly degreeOfSaturation?: Ratio;
490 }
491
492 /**
493 * Qualitaetsstufe zu einem Wartezeitwert.
494 *
495 * Kfz und OePNV: `wert` ist die mittlere Wartezeit. Nach HBS 2015 ist die
496 * Stufe F fuer Kfz keine Wartezeitstufe, sondern gilt bei Ueberlastung
497 * (q > C); die Tafel selbst endet mit E ohne Obergrenze. Das HCM 2010 vergibt
498 * F ebenfalls, sobald v/c > 1, unabhaengig von der Wartezeit.
499 * Fussgaenger und Radverkehr: `wert` ist die maximale Wartezeit (laengste
500 * Sperrzeit); nach HBS 2015 mit eigener Tafel, nach HCM ohne Tafel (null).
501 *
502 * Das verwendete Verfahren und das Kriterium werden mit ausgegeben, damit im
503 * Bericht nicht offen bleibt, wonach bewertet wurde.
504 */
505 export function serviceLevelFor(
506 wert: Seconds,
507 verfahren: Bewertungsverfahren,
508 kontext: ServiceLevelKontext = {},
509 ): ServiceLevel | null {
510 const mode = kontext.mode ?? 'kfz';
511 const value = isFiniteNumber(wert) ? wert : Number.POSITIVE_INFINITY;
512
513 if (mode === 'fuss' || mode === 'rad') {
514 if (verfahren !== 'HBS') return null;
515 return stufeAusTafel(SERVICE_LEVEL_SCALES.HBS.fussRad, value, verfahren, 'maximale-wartezeit');
516 }
517
518 const x = kontext.degreeOfSaturation;
519 /*
520 * Der Linter haelt den Vergleich mit Infinity fuer unerreichbar ("types
521 * have no overlap"), weil `isFiniteNumber` das Praedikat `wert is number`
522 * traegt und der Uebersetzer im Nein-Zweig auf `undefined` verengt. Das
523 * Praedikat ist zu grosszuegig: Infinity und NaN sind Zahlen und landen
524 * ebenfalls hier.
525 *
526 * Erreicht wird der Zweig aus dem Programm selbst - `emptyResult` weiter
527 * unten setzt bei Nachfrage ohne Kapazitaet ausdruecklich
528 * `degreeOfSaturation: Number.POSITIVE_INFINITY` und verlaesst sich fuer
529 * die Stufe F auf diese Zeile. Gemessen: Ohne den Vergleich faellt die
530 * Bewertung auf die Tafel zurueck und druckt E statt F - die gefaehrliche
531 * Richtung. Bewacht ist das in hbsWartezeit, performance und
532 * dokumentationZahlen (tests/domain).
533 */
534 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- das Praedikat verengt den Nein-Zweig zu Unrecht; siehe darueber
535 if (isFiniteNumber(x) ? x > 1 : x === Number.POSITIVE_INFINITY) {
536 return {
537 grade: SERVICE_LEVEL_F_UEBERLASTET.grade,
538 label: SERVICE_LEVEL_F_UEBERLASTET.label,
539 scale: verfahren,
540 kriterium: 'ueberlastung',
541 };
542 }
543
544 const tafel =
545 verfahren === 'HBS'
546 ? mode === 'oepnv'
547 ? SERVICE_LEVEL_SCALES.HBS.oepnv
548 : SERVICE_LEVEL_SCALES.HBS.kfz
549 : SERVICE_LEVEL_SCALES.HCM.kfz;
550 return stufeAusTafel(tafel, value, verfahren, 'mittlere-wartezeit');
551 }
552
553 /**
554 * Qualitaetsstufe eines Kfz- oder OePNV-Stroms - nie null. Fuer Aufrufer,
555 * die keine Fussgaenger- oder Radgruppen bewerten (Simulation).
556 */
557 export function kfzServiceLevel(
558 averageDelay: Seconds,
559 verfahren: Bewertungsverfahren,
560 degreeOfSaturation?: Ratio,
561 mode: 'kfz' | 'oepnv' = 'kfz',
562 ): ServiceLevel {
563 const level = serviceLevelFor(
564 averageDelay,
565 verfahren,
566 degreeOfSaturation === undefined ? { mode } : { mode, degreeOfSaturation },
567 );
568 if (level === null) {
569 // Fuer kfz/oepnv liefert serviceLevelFor immer eine Stufe. Ein Rueckfall
570 // auf eine fremde Tafel waere schlimmer als ein Abbruch: Er truege das
571 // Etikett des einen Verfahrens ueber der Tafel des anderen.
572 throw new Error(`Keine Bewertungstafel fuer ${mode} nach ${verfahren}.`);
573 }
574 return level;
575 }
576
577 function stufeAusTafel(
578 tafel: readonly { grade: ServiceLevel['grade']; maxDelay: Seconds; label: string }[],
579 value: Seconds,
580 verfahren: Bewertungsverfahren,
581 kriterium: ServiceLevel['kriterium'],
582 ): ServiceLevel {
583 for (const step of tafel) {
584 if (value <= step.maxDelay) {
585 return { grade: step.grade, label: step.label, scale: verfahren, kriterium };
586 }
587 }
588 const last = tafel[tafel.length - 1]!;
589 return { grade: last.grade, label: last.label, scale: verfahren, kriterium };
590 }
591
592 function resolveAbflusszeit(input: DelayInput, green: Seconds, cycleTime: Seconds): Seconds {
593 if (isFiniteNumber(input.abflusszeit) && input.abflusszeit >= 0) {
594 return Math.min(cycleTime, input.abflusszeit);
595 }
596 if (input.verfahren === 'HBS' && green > 0) return Math.min(cycleTime, green + 1);
597 return green;
598 }
599
600 function resolveLongestRed(input: DelayInput, green: Seconds, cycleTime: Seconds): Seconds {
601 if (isFiniteNumber(input.longestRed) && input.longestRed >= 0) {
602 return Math.min(cycleTime, input.longestRed);
603 }
604 return Math.max(0, cycleTime - green);
605 }
606
607 function resolveInstationaritaet(input: DelayInput, notes: CalculationNote[]): Ratio {
608 const fin = input.instationaritaetsfaktor;
609 if (fin === undefined) return RILSA_DEFAULTS.capacity.instationaritaetsfaktor;
610 if (!isFiniteNumber(fin) || fin < 1) {
611 notes.push({
612 severity: 'warnung',
613 code: 'instationaritaetsfaktor-ungueltig',
614 message:
615 'Der Instationaritätsfaktor muss mindestens 1,0 betragen (gleichmäßiger Zufluss). ' +
616 `Angesetzt wird der Vorgabewert ${formatRatio(RILSA_DEFAULTS.capacity.instationaritaetsfaktor)}.`,
617 });
618 return RILSA_DEFAULTS.capacity.instationaritaetsfaktor;
619 }
620 return fin;
621 }
622
623 function clampGreen(greenTime: Seconds, cycleTime: Seconds, notes: CalculationNote[]): Seconds {
624 if (!isFiniteNumber(greenTime) || greenTime < 0) {
625 notes.push({
626 severity: 'fehler',
627 code: 'freigabezeit-ungueltig',
628 message: 'Die Freigabezeit ist kein gültiger Wert. Angesetzt wird 0 s.',
629 });
630 return 0;
631 }
632 if (greenTime > cycleTime) {
633 notes.push({
634 severity: 'fehler',
635 code: 'freigabezeit-ueber-umlaufzeit',
636 message: 'Die Freigabezeit ist länger als die Umlaufzeit. Angesetzt wird die Umlaufzeit.',
637 });
638 return cycleTime;
639 }
640 return greenTime;
641 }
642
643 function emptyResult(
644 input: DelayInput,
645 notes: CalculationNote[],
646 maximumDelay: Seconds = Number.POSITIVE_INFINITY,
647 ): DelayResult {
648 const q = isFiniteNumber(input.demand) && input.demand > 0 ? input.demand : 0;
649 // Ohne Kapazitaet und mit Nachfrage ist der Strom ueberlastet (q > C = 0):
650 // Stufe F wegen Ueberlastung. Ohne Nachfrage ist nichts zu bewerten - dann
651 // gibt es keine Stufe; eine Stufe aus einer unendlichen Wartezeit liesse
652 // sich aus keiner gedruckten Groesse nachvollziehen.
653 const x = q > 0 ? Number.POSITIVE_INFINITY : 0;
654 return {
655 verfahren: input.verfahren,
656 averageDelay: Number.POSITIVE_INFINITY,
657 uniformDelay: Number.POSITIVE_INFINITY,
658 incrementalDelay: Number.POSITIVE_INFINITY,
659 degreeOfSaturation: x,
660 maximumDelay,
661 serviceLevel:
662 q > 0
663 ? serviceLevelFor(Number.POSITIVE_INFINITY, input.verfahren, {
664 mode: input.mode,
665 degreeOfSaturation: x,
666 })
667 : null,
668 rechenweg: {
669 abflusszeit: 0,
670 abflussanteil: 0,
671 betrachtungszeitraum:
672 input.verfahren === 'HBS' ? HBS_BETRACHTUNGSZEITRAUM_HOURS : HCM_ANALYSIS_PERIOD_HOURS,
673 instationaritaetsfaktor: null,
674 reststau: null,
675 hcmK: null,
676 hcmI: null,
677 },
678 notes,
679 };
680 }
681
682 function formatRatio(value: number): string {
683 if (!Number.isFinite(value)) return 'unendlich';
684 return roundTo(value, 2).toFixed(2).replace('.', ',');
685 }
686
687 /**
688 * Rueckstaulaenge einer Signalgruppe: die wartenden Fahrzeuge, verteilt auf
689 * ihre Fahrstreifen.
690 *
691 * DIE FAHRSTREIFENZAHL GEHOERT DAZU (Fassung 5.31.0). Bis dahin rechnete die
692 * Simulation `queue * 7` und gab das als Meterangabe aus - fuer eine
693 * zweistreifige Zufahrt also die doppelte Laenge. Die
694 * Leistungsfaehigkeitsrechnung derselben Anwendung teilt an der entsprechenden
695 * Stelle (`saturationFlow` in `capacity.ts`) seit jeher durch die
696 * Fahrstreifenzahl; die beiden Rechnungen widersprachen sich.
697 *
698 * HIERHER GEZOGEN (Fassung 5.31.0): Sie stand in `simulation/simulation.ts`
699 * und wird jetzt von zwei Rechnungen gebraucht - von der Simulation fuer ihre
700 * Momentaufnahme und von der Bemessung fuer den mittleren Rueckstau NGE. Zwei
701 * Fassungen derselben Umrechnung waeren genau der Fehler, den der Altbestand
702 * mit seinen vier Zwischenzeitformeln vorgemacht hat.
703 *
704 * Der Kennwert selbst steht in `constants.ts`, seine Herkunft in `quellen.ts`
705 * unter `praxis-stauraum-je-fahrzeug`.
706 */
707 export function rueckstaulaenge(fahrzeuge: number, fahrstreifen: number): Meters {
708 if (!isFiniteNumber(fahrzeuge) || fahrzeuge <= 0) return 0;
709 // Dieselbe Behandlung wie in `saturationFlow`: Eine fehlende oder
710 // unsinnige Angabe heisst ein Fahrstreifen, nicht null.
711 const streifen = isFiniteNumber(fahrstreifen) && fahrstreifen > 0 ? fahrstreifen : 1;
712 return roundTo((fahrzeuge * STAURAUMBEDARF_JE_FAHRZEUG) / streifen, 1);
713 }
714
715 /** Der Rueckstau einer Signalgruppe, gemessen am vorhandenen Stauraum. */
716 export interface Rueckstaubewertung {
717 /** Mittlerer Rueckstau bei Freigabezeitende NGE in Kfz (HBS Gl. 2-10). */
718 readonly fahrzeuge: number;
719 /** Daraus gebildete Laenge je Fahrstreifen in Metern. */
720 readonly laenge: Meters;
721 /** Vorhandener Stauraum in Metern - null, wenn keiner erfasst ist. */
722 readonly stauraum: Meters | null;
723 /** Laenge geteilt durch Stauraum - null ohne Stauraum. */
724 readonly auslastung: Ratio | null;
725 /** Erreicht oder ueberschreitet der MITTLERE Rueckstau den Stauraum? */
726 readonly ueberlauf: boolean;
727 }
728
729 /**
730 * Den mittleren Rueckstau gegen den vorhandenen Stauraum halten.
731 *
732 * WOZU: Die Leistungsfaehigkeitsrechnung fuehrte den Rueckstau seit jeher als
733 * Fahrzeugzahl, und die Planunterlage druckte sie. Fuer sich genommen ist sie
734 * unbrauchbar - ob ein Aufstellstreifen ueberlaeuft oder der Rueckstau in den
735 * vorgelagerten Knotenpunkt reicht, entscheidet eine LAENGE. Genau daran haengt
736 * eine Abwaegung nach Paragraf 45 StVO: Ein Rueckstau, der den durchgehenden
737 * Fahrstreifen blockiert oder ueber einen Bahnuebergang reicht, ist ein
738 * Sicherheitsmangel und gehoert benannt.
739 *
740 * WAS DIESE GROESSE IST UND WAS NICHT - und das ist der wichtigste Satz hier:
741 * NGE ist der MITTLERE Rueckstau bei Freigabezeitende ueber die
742 * Betrachtungsstunde. In etwa der Haelfte der Umlaeufe steht mehr. Ein
743 * Stauraum, der gerade eben reicht, reicht also NICHT; er reicht in der
744 * Haelfte der Faelle. Ein 95-Prozent-Wert waere die richtige Groesse fuer
745 * diese Frage, und das HBS kennt einen - dieses Programm nicht: Es fuehrt
746 * allein NGE, und einen Faktor dorthin zu erfinden hiesse, eine Zahl zu
747 * bilden, die kein Regelwerk deckt. Die Meldung sagt deshalb, WELCHE Groesse
748 * verglichen wurde, statt eine genauere vorzutaeuschen.
749 */
750 export function rueckstaubewertung(
751 fahrzeuge: number,
752 fahrstreifen: number,
753 stauraum: Meters | null,
754 ): Rueckstaubewertung {
755 const laenge = rueckstaulaenge(fahrzeuge, fahrstreifen);
756 const brauchbar = stauraum !== null && isFiniteNumber(stauraum) && stauraum > 0;
757 return {
758 fahrzeuge: isFiniteNumber(fahrzeuge) && fahrzeuge > 0 ? roundTo(fahrzeuge, 3) : 0,
759 laenge,
760 stauraum: brauchbar ? stauraum : null,
761 auslastung: brauchbar ? roundTo(laenge / stauraum, 4) : null,
762 ueberlauf: brauchbar && laenge >= stauraum,
763 };
764 }