import type { Meters, Ratio, Seconds, VehiclesPerHour } from '../units'; import { isFiniteNumber, roundTo } from '../units'; import { RILSA_DEFAULTS, SERVICE_LEVEL_F_UEBERLASTET, SERVICE_LEVEL_SCALES, STAURAUMBEDARF_JE_FAHRZEUG, } from './constants'; import type { Bewertungsverfahren, CalculationNote, ServiceLevel, TrafficMode } from './types'; /** * Wartezeit und Qualitaetsstufe einer Signalgruppe. * * KORREKTUR (Fassung 5.4.0, Befund B2): Bis dahin rechnete diese * Datei fuer beide Einstellungen dieselbe HCM-Formel (d1 + d2 mit T = 0,25 h, * k = 0,5, I = 1,0), und nur die Stufentafel wechselte - waehrend der Ausdruck * "Bewertung nach HBS 2015" behauptete. Bei hoher Auslastung (x ab etwa 0,9) * liefert die HCM-Formel deutlich kleinere Wartezeiten als das HBS-Verfahren: * zu gute Stufen genau dort, wo die Bewertung kippt. * * Jetzt gibt es zwei Verfahren, und jedes rechnet, was es behauptet: * * HBS 2015 (Leyn 2016, KIT-Dissertation, Kap. 2.3.3, Gl. 2-7 bis 2-10, und * Kap. 3.3.2, Gl. 3-2 - die Dissertation gibt das HBS-Verfahren woertlich * wieder; die Formelkette ist am LISA+-Ausdruck "HBS-Bewertung 2015" * Kornwestheim 2017 nachgerechnet, siehe tests/domain/hbsWartezeit.test.ts): * * tW = tW,G + tW,R (2-7) * tW,G = tU * (1 - fA)^2 / (2 * (1 - min(1; x) * fA)) (2-8) * mit fA = tA / tU und tA = tF + 1 s (Abflusszeit) * Diese geschlossene Form gilt fuer EIN Freigabezeitfenster je * Umlauf. Allgemein steht im Zaehler Summe(r_j^2) / tU ueber die * Sperrzeiten r_j des Umlaufs; siehe summeSperrzeitenQuadrate. * tW,R = NGE * 3600 / C (2-9) * NGE = max{ 0,58*T*C/4 * [ (fin*x - 1) + sqrt((fin*x - 1)^2 + 4*fin*x / (0,58*T*C)) ] ; * T*C/4 * [ (x - 1) + sqrt((x - 1)^2 + 4*x / (T*C)) ] } (2-10) * mit T = 1 h (Betrachtungszeitraum) und * fin = 1 + ((q15/q) - 1) / 1,5 (Instationaritaetsfaktor) (3-2) * * HCM (Highway Capacity Manual, Kapitel signalisierte Knotenpunkte): * * d = d1 + d2 * d1 = 0,5 * tU * (1 - g/tU)^2 / (1 - min(1; x) * g/tU) * d2 = 900 * T * [ (x - 1) + sqrt( (x - 1)^2 + 8*k*I*x / (c*T) ) ] * mit T = 0,25 h, k = 0,5 (Festzeit), I = 1,0 (isoliert) * * Aeltere Korrekturen gegenueber dem Altbestand bleiben gueltig: Dort wurde * nur d1 gerechnet, mit fest eingetragenem x = 0,85 statt dem tatsaechlichen, * und die Funktion lief wegen eines nie geschriebenen Feldes faktisch nie. */ /** Rechenweg der Wartezeit - fuer Ausdruck und Ansicht, damit sich tW nachrechnen laesst. */ export interface DelayRechenweg { /** Abflusszeit tA (HBS: tF + 1 s je Fenster) bzw. Freigabezeit g (HCM). */ readonly abflusszeit: Seconds; /** Abflusszeitanteil fA = tA/tU bzw. g/tU. */ readonly abflussanteil: Ratio; /** Betrachtungszeitraum T in Stunden (HBS 1 h, HCM 0,25 h). */ readonly betrachtungszeitraum: number; /** Instationaritaetsfaktor fin (nur HBS, sonst null). */ readonly instationaritaetsfaktor: Ratio | null; /** Mittlerer Rueckstau bei Freigabezeitende NGE in Kfz (nur HBS, sonst null). */ readonly reststau: number | null; /** HCM-Beiwerte k und I (nur HCM, sonst null). */ readonly hcmK: number | null; readonly hcmI: number | null; } /** Aufteilung der mittleren Wartezeit in ihre Anteile. */ export interface DelayResult { /** Verfahren, nach dem gerechnet und bewertet wurde. */ readonly verfahren: Bewertungsverfahren; /** Mittlere Wartezeit je Fahrzeug in Sekunden (HBS: tW; HCM: d). */ readonly averageDelay: Seconds; /** Grundwartezeit tW,G (HBS) bzw. gleichmaessiger Anteil d1 (HCM). */ readonly uniformDelay: Seconds; /** Wartezeit im Reststau tW,R (HBS) bzw. Ueberlastungsanteil d2 (HCM). */ readonly incrementalDelay: Seconds; /** Auslastungsgrad x = q / C. */ readonly degreeOfSaturation: Ratio; /** * Maximale Wartezeit: die laengste Sperrzeit im Umlauf. Fuer Fussgaenger * und Radverkehr ist sie nach HBS 2015 das Bewertungskriterium. */ readonly maximumDelay: Seconds; /** * Qualitaetsstufe; null, wenn das Verfahren fuer diese Verkehrsart keine * Tafel hat (HCM fuer Fussgaenger und Radverkehr). */ readonly serviceLevel: ServiceLevel | null; readonly rechenweg: DelayRechenweg; readonly notes: readonly CalculationNote[]; } /** Bemessungszeitraum des HCM-Verfahrens in Stunden (Spitzenviertelstunde). */ const HCM_ANALYSIS_PERIOD_HOURS = 0.25; /** HCM-Beiwert fuer Festzeitsteuerung. */ const HCM_PRETIMED_K = 0.5; /** HCM-Beiwert fuer isolierte Knotenpunkte ohne Koordinierung. */ const HCM_ISOLATED_I = 1.0; /** Betrachtungszeitraum des HBS-Verfahrens in Stunden (Bemessungsstunde). */ const HBS_BETRACHTUNGSZEITRAUM_HOURS = 1; /** Koeffizient des ersten Terms der Rueckstauformel (HBS 2015, Gl. 2-10 bei Leyn). */ const HBS_RESTSTAU_KOEFFIZIENT = 0.58; export interface DelayInput { readonly cycleTime: Seconds; /** Freigabezeit tF - Summe aller Freigabezeitfenster im Umlauf. */ readonly greenTime: Seconds; /** Kapazitaet C in Fz/h, wie von computeCapacity ausgewiesen (ganzzahlig). */ readonly capacity: VehiclesPerHour; /** Verkehrsstaerke q in Fz/h. */ readonly demand: VehiclesPerHour; readonly verfahren: Bewertungsverfahren; /** * Verkehrsart - bestimmt die Bewertungstafel. Nur Kfz und OePNV: Fuer * Fussgaenger und Radverkehr gibt es keine Kapazitaet und keine mittlere * Wartezeit als Kriterium; sie laufen ueber computeFussRadDelay. Der Typ * schliesst aus, dass jemand eine mittlere Wartezeit an der Fuss/Rad-Tafel * (maximale Wartezeit) misst. */ readonly mode: 'kfz' | 'oepnv'; /** * Abflusszeit tA (HBS) bzw. Freigabezeit g (HCM), wie sie in die * Kapazitaet eingegangen ist. Fehlt sie, gilt bei HBS tF + 1 s (ein * Fenster), bei HCM tF. */ readonly abflusszeit?: Seconds; /** Laengste Sperrzeit im Umlauf; fehlt sie, gilt tU - tF. */ readonly longestRed?: Seconds; /** * Alle Sperrzeiten des Umlaufs (Rot-Abschnitte zwischen den * Freigabezeitfenstern), wie sie sperrzeitenAusFenstern liefert. Nur mit * ihnen laesst sich die Grundwartezeit bei mehr als einem Fenster je Umlauf * richtig bilden; fehlen sie, wird ein einziges Fenster unterstellt. */ readonly sperrzeiten?: readonly Seconds[]; /** Instationaritaetsfaktor fin (nur HBS); fehlt er, gilt der Vorgabewert. */ readonly instationaritaetsfaktor?: Ratio; } /** * Mittlere Wartezeit und Qualitaetsstufe eines Kfz- oder OePNV-Stroms. * Fuer Fussgaenger und Radverkehr siehe computeFussRadDelay. */ export function computeDelay(input: DelayInput): DelayResult { const notes: CalculationNote[] = []; const { cycleTime, capacity, verfahren, mode } = input; if (!isFiniteNumber(cycleTime) || cycleTime <= 0) { notes.push({ severity: 'fehler', code: 'umlaufzeit-ungueltig', message: 'Die Umlaufzeit ist kein gültiger Wert; die Wartezeit ist nicht bestimmbar.', }); return emptyResult(input, notes); } const green = clampGreen(input.greenTime, cycleTime, notes); const tA = resolveAbflusszeit(input, green, cycleTime); const fA = tA / cycleTime; const maximumDelay = resolveLongestRed(input, green, cycleTime); if (!isFiniteNumber(capacity) || capacity <= 0) { notes.push({ severity: 'fehler', code: 'kapazitaet-null', message: 'Die Kapazität beträgt 0 Fz/h. Ohne Freigabezeit entsteht ein unbegrenzter Rückstau; ' + 'eine mittlere Wartezeit lässt sich nicht angeben.', }); return emptyResult(input, notes, maximumDelay); } const q = isFiniteNumber(input.demand) && input.demand >= 0 ? input.demand : 0; const x = q / capacity; // Zaehler der Grundwartezeit; null, wenn keine brauchbaren Sperrzeiten // vorliegen - dann gilt die geschlossene Einfensterform tU * (1 - fA)^2. const sperrzeitenanteil = summeSperrzeitenQuadrate(input.sperrzeiten, tA, cycleTime); const grundwartezeitZaehler = sperrzeitenanteil ?? cycleTime * (1 - fA) ** 2; let uniform: number; let incremental: number; let rechenweg: DelayRechenweg; if (verfahren === 'HBS') { const fin = resolveInstationaritaet(input, notes); const T = HBS_BETRACHTUNGSZEITRAUM_HOURS; // Gl. 2-8: Grundwartezeit. Im Zaehler steht Summe(r_j^2) / tU ueber die // Sperrzeiten des Umlaufs; bei einem Fenster je Umlauf ist das die // geschlossene Form tU * (1 - fA)^2. Der Nenner wird bei x -> 1 und // fA -> 1 klein; die Begrenzung von x auf 1 haelt ihn positiv. const denominator = 2 * (1 - Math.min(1, x) * fA); uniform = denominator > 1e-9 ? grundwartezeitZaehler / denominator : 0.5 * cycleTime * (1 - fA); // Gl. 2-10: mittlerer Rueckstau bei Freigabezeitende, groesserer der // beiden Terme (Instationaritaet bzw. reine Akcelik-Form). const reststau = hbsReststau(x, capacity, T, fin); // Gl. 2-9: Wartezeit im Reststau. incremental = (reststau * 3600) / capacity; rechenweg = { abflusszeit: roundTo(tA, 3), abflussanteil: roundTo(fA, 4), betrachtungszeitraum: T, instationaritaetsfaktor: fin, reststau: roundTo(reststau, 3), hcmK: null, hcmI: null, }; } else { const T = HCM_ANALYSIS_PERIOD_HOURS; // d1 ist dieselbe Webster-Form wie Gl. 2-8 und wird ebenso ueber die // Sperrzeiten des Umlaufs gebildet. const denominator = 1 - Math.min(1, x) * fA; uniform = denominator > 1e-9 ? (0.5 * grundwartezeitZaehler) / denominator : 0.5 * cycleTime * (1 - fA); const radicand = (x - 1) ** 2 + (8 * HCM_PRETIMED_K * HCM_ISOLATED_I * x) / (capacity * T); incremental = 900 * T * (x - 1 + Math.sqrt(Math.max(0, radicand))); rechenweg = { abflusszeit: roundTo(tA, 3), abflussanteil: roundTo(fA, 4), betrachtungszeitraum: T, instationaritaetsfaktor: null, reststau: null, hcmK: HCM_PRETIMED_K, hcmI: HCM_ISOLATED_I, }; } const averageDelay = uniform + incremental; if (x > 1) { notes.push({ severity: 'fehler', code: 'ueberlastet', message: `Der Auslastungsgrad beträgt ${formatRatio(x)}. Die Wartezeit wächst während des ` + 'Bemessungszeitraums stetig an; der angegebene Wert gilt nur für diesen Zeitraum.', }); } /* * Bewertet wird der Wert, der auch AUSGEGEBEN wird. * * Zuvor stand hier die ungerundete Wartezeit, waehrend darueber die * gerundete zurueckgegeben wurde. Bei 35,02 s las der Pruefer dann "35,0 s" * neben der Stufe C - waehrend die Stufentafel im selben Dokument bis * einschliesslich 35 s die Stufe B ausweist. Die Zeile widerlegt sich damit * selbst, und zwar zu Ungunsten der Anlage: Die Bewertung faellt eine Stufe * zu STRENG aus. * * Das Fenster ist mit 0,05 s je Stufengrenze schmal. Eine Planunterlage, in * der Zahl und Bewertung nicht zusammenpassen, ist an dieser Zeile aber * nicht mehr zu gebrauchen - und niemand kann den Widerspruch aufloesen, * weil der ungerundete Wert nirgends steht. * * Die Rundung auf eine Zehntelsekunde vor der Bewertung ist fachlich * unbedenklich: Sie liegt weit unterhalb der Genauigkeit, mit der eine * mittlere Wartezeit ueberhaupt ermittelt wird. * * Dasselbe gilt fuer die Stufe F bei Ueberlastung: Sie haengt an q > C, und * beide Zahlen stehen ganzzahlig im Ausdruck. */ const ausgegebeneWartezeit = roundTo(averageDelay, 1); /* * Die Anteile muessen auf den gedruckten Ziffern aufgehen: tW = tW,G + tW,R * (Gl. 2-7) steht so im Ausdruck nebeneinander. Werden alle drei unabhaengig * gerundet, ergibt 37,85 + 21,17 = 59,02 gedruckt "37,9 + 21,2 = 59,0" - eine * Zeile, die sich selbst widerlegt. Deshalb wird nur die Grundwartezeit * eigenstaendig gerundet und der Reststauanteil als Differenz gebildet. */ const ausgegebeneGrundwartezeit = roundTo(uniform, 1); const ausgegebenerReststauanteil = roundTo(ausgegebeneWartezeit - ausgegebeneGrundwartezeit, 1); return { verfahren, averageDelay: ausgegebeneWartezeit, uniformDelay: ausgegebeneGrundwartezeit, incrementalDelay: ausgegebenerReststauanteil, degreeOfSaturation: roundTo(x, 4), maximumDelay: roundTo(maximumDelay, 1), serviceLevel: serviceLevelFor(ausgegebeneWartezeit, verfahren, { mode, degreeOfSaturation: x }), rechenweg, notes, }; } /** * Zaehler der Grundwartezeit: Summe(r_j^2) / tU ueber die Sperrzeiten r_j * eines Umlaufs. Null, wenn keine brauchbaren Sperrzeiten vorliegen - dann * gilt die geschlossene Einfensterform. * * KORREKTUR (Fassung 5.10.0): Gl. 2-8 ist die Webster-Form fuer EIN * Freigabezeitfenster je Umlauf; dort ist r = tU - tA, und der Zaehler wird zu * (tU - tA)^2 / tU = tU * (1 - fA)^2. Erhaelt eine Signalgruppe mehrere * Fenster, wiederholt sich das Signalbild oefter als einmal je Umlauf, und die * Grundwartezeit richtet sich nach der Summe der QUADRIERTEN Sperrzeiten, nicht * nach dem Quadrat ihrer Summe. Weil Summe(r_j^2) <= (Summe r_j)^2 gilt - mit * Gleichheit nur bei einem Fenster -, fiel die Einfensterform bei jeder * Aufteilung zu gross aus, bei zwei gleich langen Sperrzeiten genau um das * Doppelte. computeFussRadDelay rechnet dieselbe Verallgemeinerung fuer * Fussgaenger und Radverkehr bereits (Grenzfall x * fA -> 0). * * Die uebergebenen Sperrzeiten sind aus den Freigabezeitfenstern gebildet und * enthalten den Zeitzuschlag der Abflusszeit (HBS: 1 s je Fenster) noch. Er * faellt am Ende jedes Fensters an und wird deshalb von jeder Sperrzeit * einzeln abgezogen; damit bleibt Summe(r_j) = tU - tA, und der Einfensterfall * geht unveraendert in die geschlossene Form ueber. */ function summeSperrzeitenQuadrate( sperrzeiten: readonly Seconds[] | undefined, tA: Seconds, cycleTime: Seconds, ): number | null { if (sperrzeiten === undefined) return null; const gueltig = sperrzeiten.filter((s) => isFiniteNumber(s) && s > 0); if (gueltig.length === 0) return null; const summe = gueltig.reduce((wert, s) => wert + s, 0); const wirksam = cycleTime - tA; // Passen Sperrzeiten und Abflusszeit nicht zusammen, waere der Zuschlag // negativ und wuerde die Sperrzeiten verlaengern. Dann lieber die // geschlossene Form, die nur an tA haengt. if (wirksam < 0 || summe < wirksam) return null; const zuschlagJeFenster = (summe - wirksam) / gueltig.length; let quadrate = 0; for (const s of gueltig) { const r = Math.max(0, s - zuschlagJeFenster); quadrate += r * r; } return quadrate / cycleTime; } /** * Mittlerer Rueckstau bei Freigabezeitende NGE nach HBS 2015 (Gl. 2-10 bei * Leyn 2016): der groessere von zwei Termen. Der erste enthaelt den * Instationaritaetsfaktor und den Koeffizienten 0,58 (nach Leyn im HBS selbst * nicht hergeleitet, geht auf Wu 1990 zurueck), der zweite ist die Formel von * Akcelik (1980) fuer den Betrachtungszeitraum T. */ export function hbsReststau(x: Ratio, capacity: VehiclesPerHour, T: number, fin: Ratio): number { const akcelik = (faktor: number, xEff: number): number => { const a = (faktor * T * capacity) / 4; const radicand = (xEff - 1) ** 2 + (4 * xEff) / (faktor * T * capacity); return a * (xEff - 1 + Math.sqrt(Math.max(0, radicand))); }; const instationaer = akcelik(HBS_RESTSTAU_KOEFFIZIENT, fin * x); const stationaer = akcelik(1, x); return Math.max(0, instationaer, stationaer); } export interface FussRadDelayInput { readonly cycleTime: Seconds; /** Sperrzeiten im Umlauf (Dauer jedes Rot-Abschnitts), siehe sperrzeitenAusFenstern. */ readonly sperrzeiten: readonly Seconds[]; readonly verfahren: Bewertungsverfahren; readonly mode: 'fuss' | 'rad'; } /** * Wartezeit und Qualitaetsstufe fuer Fussgaenger und Radverkehr. * * KORREKTUR (Fassung 5.4.0, Befund B4): Das HBS 2015 bewertet * Fussgaenger und Radverkehr an Lichtsignalanlagen nach der MAXIMALEN * Wartezeit - der laengsten Sperrzeit im Umlauf - mit eigener Tafel. Eine * Kapazitaet und ein Auslastungsgrad werden dafuer nicht gebildet. * * Als Kenngroesse wird zusaetzlich die mittlere Wartezeit eines ueber den * Umlauf gleichverteilt eintreffenden Fussgaengers ausgewiesen: * Summe(tS_i^2) / (2 * tU). Sie ist keine Bewertungsgroesse des HBS, aber * eine nachrechenbare Zahl fuer den Vergleich von Varianten. */ export function computeFussRadDelay(input: FussRadDelayInput): DelayResult { const notes: CalculationNote[] = []; const { cycleTime, verfahren, mode } = input; const rechenweg: DelayRechenweg = { abflusszeit: 0, abflussanteil: 0, betrachtungszeitraum: 0, instationaritaetsfaktor: null, reststau: null, hcmK: null, hcmI: null, }; if (!isFiniteNumber(cycleTime) || cycleTime <= 0) { notes.push({ severity: 'fehler', code: 'umlaufzeit-ungueltig', message: 'Die Umlaufzeit ist kein gültiger Wert; die Wartezeit ist nicht bestimmbar.', }); return { verfahren, averageDelay: Number.POSITIVE_INFINITY, uniformDelay: Number.POSITIVE_INFINITY, incrementalDelay: 0, degreeOfSaturation: 0, maximumDelay: Number.POSITIVE_INFINITY, serviceLevel: serviceLevelFor(Number.POSITIVE_INFINITY, verfahren, { mode }), rechenweg, notes, }; } const sperrzeiten = input.sperrzeiten.filter((s) => isFiniteNumber(s) && s > 0); const laengste = sperrzeiten.length > 0 ? Math.max(...sperrzeiten) : 0; const mittel = sperrzeiten.reduce((sum, s) => sum + s * s, 0) / (2 * cycleTime); if (verfahren === 'HCM') { notes.push({ severity: 'hinweis', code: 'keine-fuss-rad-tafel', message: 'Für Fußgänger und Radverkehr ist im HCM-Verfahren keine Bewertungstafel hinterlegt; ' + 'eine Qualitätsstufe wird nicht vergeben. Nach HBS 2015 würde die maximale Wartezeit bewertet.', }); } const ausgegebeneMaximale = roundTo(laengste, 1); return { verfahren, averageDelay: roundTo(mittel, 1), uniformDelay: roundTo(mittel, 1), incrementalDelay: 0, degreeOfSaturation: 0, maximumDelay: ausgegebeneMaximale, serviceLevel: serviceLevelFor(ausgegebeneMaximale, verfahren, { mode }), rechenweg, notes, }; } /** * Sperrzeiten (Rot-Abschnitte) eines Umlaufs aus den Freigabezeitfenstern. * Fenster duerfen ueber den Umlaufbeginn hinweg reichen; sie werden in den * Umlauf gefaltet. Ohne Fenster ist der ganze Umlauf Sperrzeit; deckt ein * Fenster den ganzen Umlauf, gibt es keine. */ export function sperrzeitenAusFenstern( fenster: readonly { readonly start: Seconds; readonly duration: Seconds }[], cycleTime: Seconds, ): Seconds[] { if (!isFiniteNumber(cycleTime) || cycleTime <= 0) return []; const gueltig = fenster.filter( (f) => isFiniteNumber(f.start) && isFiniteNumber(f.duration) && f.duration > 0, ); if (gueltig.length === 0) return [cycleTime]; if (gueltig.some((f) => f.duration >= cycleTime)) return []; const modulo = (v: number): number => ((v % cycleTime) + cycleTime) % cycleTime; // Belegte Abschnitte im Umlauf, ueberlaufende Fenster aufgeteilt. const belegt: { von: number; bis: number }[] = []; for (const f of gueltig) { const von = modulo(f.start); const bis = von + f.duration; if (bis <= cycleTime) belegt.push({ von, bis }); else { belegt.push({ von, bis: cycleTime }); belegt.push({ von: 0, bis: bis - cycleTime }); } } belegt.sort((a, b) => a.von - b.von); // Ueberlappungen verschmelzen. const verschmolzen: { von: number; bis: number }[] = []; for (const b of belegt) { const letzter = verschmolzen[verschmolzen.length - 1]; if (letzter && b.von <= letzter.bis + 1e-9) letzter.bis = Math.max(letzter.bis, b.bis); else verschmolzen.push({ ...b }); } const luecken: number[] = []; for (let i = 0; i < verschmolzen.length; i += 1) { const aktuell = verschmolzen[i]!; const naechster = verschmolzen[(i + 1) % verschmolzen.length]!; const luecke = i + 1 < verschmolzen.length ? naechster.von - aktuell.bis : cycleTime - aktuell.bis + naechster.von; if (luecke > 1e-9) luecken.push(roundTo(luecke, 3)); } return luecken; } export interface ServiceLevelKontext { /** Verkehrsart; Vorgabe 'kfz'. Bei 'fuss'/'rad' ist `wert` die maximale Wartezeit. */ readonly mode?: TrafficMode; /** Auslastungsgrad x = q/C; ueber 1 ergibt fuer Kfz und OePNV die Stufe F (Ueberlastung). */ readonly degreeOfSaturation?: Ratio; } /** * Qualitaetsstufe zu einem Wartezeitwert. * * Kfz und OePNV: `wert` ist die mittlere Wartezeit. Nach HBS 2015 ist die * Stufe F fuer Kfz keine Wartezeitstufe, sondern gilt bei Ueberlastung * (q > C); die Tafel selbst endet mit E ohne Obergrenze. Das HCM 2010 vergibt * F ebenfalls, sobald v/c > 1, unabhaengig von der Wartezeit. * Fussgaenger und Radverkehr: `wert` ist die maximale Wartezeit (laengste * Sperrzeit); nach HBS 2015 mit eigener Tafel, nach HCM ohne Tafel (null). * * Das verwendete Verfahren und das Kriterium werden mit ausgegeben, damit im * Bericht nicht offen bleibt, wonach bewertet wurde. */ export function serviceLevelFor( wert: Seconds, verfahren: Bewertungsverfahren, kontext: ServiceLevelKontext = {}, ): ServiceLevel | null { const mode = kontext.mode ?? 'kfz'; const value = isFiniteNumber(wert) ? wert : Number.POSITIVE_INFINITY; if (mode === 'fuss' || mode === 'rad') { if (verfahren !== 'HBS') return null; return stufeAusTafel(SERVICE_LEVEL_SCALES.HBS.fussRad, value, verfahren, 'maximale-wartezeit'); } const x = kontext.degreeOfSaturation; /* * Der Linter haelt den Vergleich mit Infinity fuer unerreichbar ("types * have no overlap"), weil `isFiniteNumber` das Praedikat `wert is number` * traegt und der Uebersetzer im Nein-Zweig auf `undefined` verengt. Das * Praedikat ist zu grosszuegig: Infinity und NaN sind Zahlen und landen * ebenfalls hier. * * Erreicht wird der Zweig aus dem Programm selbst - `emptyResult` weiter * unten setzt bei Nachfrage ohne Kapazitaet ausdruecklich * `degreeOfSaturation: Number.POSITIVE_INFINITY` und verlaesst sich fuer * die Stufe F auf diese Zeile. Gemessen: Ohne den Vergleich faellt die * Bewertung auf die Tafel zurueck und druckt E statt F - die gefaehrliche * Richtung. Bewacht ist das in hbsWartezeit, performance und * dokumentationZahlen (tests/domain). */ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- das Praedikat verengt den Nein-Zweig zu Unrecht; siehe darueber if (isFiniteNumber(x) ? x > 1 : x === Number.POSITIVE_INFINITY) { return { grade: SERVICE_LEVEL_F_UEBERLASTET.grade, label: SERVICE_LEVEL_F_UEBERLASTET.label, scale: verfahren, kriterium: 'ueberlastung', }; } const tafel = verfahren === 'HBS' ? mode === 'oepnv' ? SERVICE_LEVEL_SCALES.HBS.oepnv : SERVICE_LEVEL_SCALES.HBS.kfz : SERVICE_LEVEL_SCALES.HCM.kfz; return stufeAusTafel(tafel, value, verfahren, 'mittlere-wartezeit'); } /** * Qualitaetsstufe eines Kfz- oder OePNV-Stroms - nie null. Fuer Aufrufer, * die keine Fussgaenger- oder Radgruppen bewerten (Simulation). */ export function kfzServiceLevel( averageDelay: Seconds, verfahren: Bewertungsverfahren, degreeOfSaturation?: Ratio, mode: 'kfz' | 'oepnv' = 'kfz', ): ServiceLevel { const level = serviceLevelFor( averageDelay, verfahren, degreeOfSaturation === undefined ? { mode } : { mode, degreeOfSaturation }, ); if (level === null) { // Fuer kfz/oepnv liefert serviceLevelFor immer eine Stufe. Ein Rueckfall // auf eine fremde Tafel waere schlimmer als ein Abbruch: Er truege das // Etikett des einen Verfahrens ueber der Tafel des anderen. throw new Error(`Keine Bewertungstafel fuer ${mode} nach ${verfahren}.`); } return level; } function stufeAusTafel( tafel: readonly { grade: ServiceLevel['grade']; maxDelay: Seconds; label: string }[], value: Seconds, verfahren: Bewertungsverfahren, kriterium: ServiceLevel['kriterium'], ): ServiceLevel { for (const step of tafel) { if (value <= step.maxDelay) { return { grade: step.grade, label: step.label, scale: verfahren, kriterium }; } } const last = tafel[tafel.length - 1]!; return { grade: last.grade, label: last.label, scale: verfahren, kriterium }; } function resolveAbflusszeit(input: DelayInput, green: Seconds, cycleTime: Seconds): Seconds { if (isFiniteNumber(input.abflusszeit) && input.abflusszeit >= 0) { return Math.min(cycleTime, input.abflusszeit); } if (input.verfahren === 'HBS' && green > 0) return Math.min(cycleTime, green + 1); return green; } function resolveLongestRed(input: DelayInput, green: Seconds, cycleTime: Seconds): Seconds { if (isFiniteNumber(input.longestRed) && input.longestRed >= 0) { return Math.min(cycleTime, input.longestRed); } return Math.max(0, cycleTime - green); } function resolveInstationaritaet(input: DelayInput, notes: CalculationNote[]): Ratio { const fin = input.instationaritaetsfaktor; if (fin === undefined) return RILSA_DEFAULTS.capacity.instationaritaetsfaktor; if (!isFiniteNumber(fin) || fin < 1) { notes.push({ severity: 'warnung', code: 'instationaritaetsfaktor-ungueltig', message: 'Der Instationaritätsfaktor muss mindestens 1,0 betragen (gleichmäßiger Zufluss). ' + `Angesetzt wird der Vorgabewert ${formatRatio(RILSA_DEFAULTS.capacity.instationaritaetsfaktor)}.`, }); return RILSA_DEFAULTS.capacity.instationaritaetsfaktor; } return fin; } function clampGreen(greenTime: Seconds, cycleTime: Seconds, notes: CalculationNote[]): Seconds { if (!isFiniteNumber(greenTime) || greenTime < 0) { notes.push({ severity: 'fehler', code: 'freigabezeit-ungueltig', message: 'Die Freigabezeit ist kein gültiger Wert. Angesetzt wird 0 s.', }); return 0; } if (greenTime > cycleTime) { notes.push({ severity: 'fehler', code: 'freigabezeit-ueber-umlaufzeit', message: 'Die Freigabezeit ist länger als die Umlaufzeit. Angesetzt wird die Umlaufzeit.', }); return cycleTime; } return greenTime; } function emptyResult( input: DelayInput, notes: CalculationNote[], maximumDelay: Seconds = Number.POSITIVE_INFINITY, ): DelayResult { const q = isFiniteNumber(input.demand) && input.demand > 0 ? input.demand : 0; // Ohne Kapazitaet und mit Nachfrage ist der Strom ueberlastet (q > C = 0): // Stufe F wegen Ueberlastung. Ohne Nachfrage ist nichts zu bewerten - dann // gibt es keine Stufe; eine Stufe aus einer unendlichen Wartezeit liesse // sich aus keiner gedruckten Groesse nachvollziehen. const x = q > 0 ? Number.POSITIVE_INFINITY : 0; return { verfahren: input.verfahren, averageDelay: Number.POSITIVE_INFINITY, uniformDelay: Number.POSITIVE_INFINITY, incrementalDelay: Number.POSITIVE_INFINITY, degreeOfSaturation: x, maximumDelay, serviceLevel: q > 0 ? serviceLevelFor(Number.POSITIVE_INFINITY, input.verfahren, { mode: input.mode, degreeOfSaturation: x, }) : null, rechenweg: { abflusszeit: 0, abflussanteil: 0, betrachtungszeitraum: input.verfahren === 'HBS' ? HBS_BETRACHTUNGSZEITRAUM_HOURS : HCM_ANALYSIS_PERIOD_HOURS, instationaritaetsfaktor: null, reststau: null, hcmK: null, hcmI: null, }, notes, }; } function formatRatio(value: number): string { if (!Number.isFinite(value)) return 'unendlich'; return roundTo(value, 2).toFixed(2).replace('.', ','); } /** * Rueckstaulaenge einer Signalgruppe: die wartenden Fahrzeuge, verteilt auf * ihre Fahrstreifen. * * DIE FAHRSTREIFENZAHL GEHOERT DAZU (Fassung 5.31.0). Bis dahin rechnete die * Simulation `queue * 7` und gab das als Meterangabe aus - fuer eine * zweistreifige Zufahrt also die doppelte Laenge. Die * Leistungsfaehigkeitsrechnung derselben Anwendung teilt an der entsprechenden * Stelle (`saturationFlow` in `capacity.ts`) seit jeher durch die * Fahrstreifenzahl; die beiden Rechnungen widersprachen sich. * * HIERHER GEZOGEN (Fassung 5.31.0): Sie stand in `simulation/simulation.ts` * und wird jetzt von zwei Rechnungen gebraucht - von der Simulation fuer ihre * Momentaufnahme und von der Bemessung fuer den mittleren Rueckstau NGE. Zwei * Fassungen derselben Umrechnung waeren genau der Fehler, den der Altbestand * mit seinen vier Zwischenzeitformeln vorgemacht hat. * * Der Kennwert selbst steht in `constants.ts`, seine Herkunft in `quellen.ts` * unter `praxis-stauraum-je-fahrzeug`. */ export function rueckstaulaenge(fahrzeuge: number, fahrstreifen: number): Meters { if (!isFiniteNumber(fahrzeuge) || fahrzeuge <= 0) return 0; // Dieselbe Behandlung wie in `saturationFlow`: Eine fehlende oder // unsinnige Angabe heisst ein Fahrstreifen, nicht null. const streifen = isFiniteNumber(fahrstreifen) && fahrstreifen > 0 ? fahrstreifen : 1; return roundTo((fahrzeuge * STAURAUMBEDARF_JE_FAHRZEUG) / streifen, 1); } /** Der Rueckstau einer Signalgruppe, gemessen am vorhandenen Stauraum. */ export interface Rueckstaubewertung { /** Mittlerer Rueckstau bei Freigabezeitende NGE in Kfz (HBS Gl. 2-10). */ readonly fahrzeuge: number; /** Daraus gebildete Laenge je Fahrstreifen in Metern. */ readonly laenge: Meters; /** Vorhandener Stauraum in Metern - null, wenn keiner erfasst ist. */ readonly stauraum: Meters | null; /** Laenge geteilt durch Stauraum - null ohne Stauraum. */ readonly auslastung: Ratio | null; /** Erreicht oder ueberschreitet der MITTLERE Rueckstau den Stauraum? */ readonly ueberlauf: boolean; } /** * Den mittleren Rueckstau gegen den vorhandenen Stauraum halten. * * WOZU: Die Leistungsfaehigkeitsrechnung fuehrte den Rueckstau seit jeher als * Fahrzeugzahl, und die Planunterlage druckte sie. Fuer sich genommen ist sie * unbrauchbar - ob ein Aufstellstreifen ueberlaeuft oder der Rueckstau in den * vorgelagerten Knotenpunkt reicht, entscheidet eine LAENGE. Genau daran haengt * eine Abwaegung nach Paragraf 45 StVO: Ein Rueckstau, der den durchgehenden * Fahrstreifen blockiert oder ueber einen Bahnuebergang reicht, ist ein * Sicherheitsmangel und gehoert benannt. * * WAS DIESE GROESSE IST UND WAS NICHT - und das ist der wichtigste Satz hier: * NGE ist der MITTLERE Rueckstau bei Freigabezeitende ueber die * Betrachtungsstunde. In etwa der Haelfte der Umlaeufe steht mehr. Ein * Stauraum, der gerade eben reicht, reicht also NICHT; er reicht in der * Haelfte der Faelle. Ein 95-Prozent-Wert waere die richtige Groesse fuer * diese Frage, und das HBS kennt einen - dieses Programm nicht: Es fuehrt * allein NGE, und einen Faktor dorthin zu erfinden hiesse, eine Zahl zu * bilden, die kein Regelwerk deckt. Die Meldung sagt deshalb, WELCHE Groesse * verglichen wurde, statt eine genauere vorzutaeuschen. */ export function rueckstaubewertung( fahrzeuge: number, fahrstreifen: number, stauraum: Meters | null, ): Rueckstaubewertung { const laenge = rueckstaulaenge(fahrzeuge, fahrstreifen); const brauchbar = stauraum !== null && isFiniteNumber(stauraum) && stauraum > 0; return { fahrzeuge: isFiniteNumber(fahrzeuge) && fahrzeuge > 0 ? roundTo(fahrzeuge, 3) : 0, laenge, stauraum: brauchbar ? stauraum : null, auslastung: brauchbar ? roundTo(laenge / stauraum, 4) : null, ueberlauf: brauchbar && laenge >= stauraum, }; }