import type { Meters, Ratio, Seconds, VehiclesPerHour } from '../units'; import { isFiniteNumber, roundTo } from '../units'; import { HBS_ABFLUSSZUSCHLAG, HBS_GEOMETRIE, HBS_PKW_GLEICHWERTE, HBS_STANDARDBEDINGUNGEN, RILSA_DEFAULTS, type RilsaDefaults, } from './constants'; import type { Bewertungsverfahren, CalculationNote, CapacityResult, Movement } from './types'; /** * Aufteilung des Schwerverkehrs auf die beiden Fahrzeugklassen des HBS, * jeweils als Anteil an der GESAMTVERKEHRSSTAERKE qKfz (nicht am * Schwerverkehr). * * Erfassbar seit Schema 13 - allerdings NICHT in dieser Form: Das Projekt * fuehrt je Verkehrsstaerke einen Lastzuganteil AM SCHWERVERKEHR * (`TrafficDemand.lastzugAnteil`), und `aufteilungAusLastzuganteil` rechnet ihn * auf die beiden Anteile an qKfz um, mit denen Gl. 2-5 arbeitet. Warum die * Eingabe an einer anderen Bezugsgroesse haengt als die Rechnung, steht dort. */ export interface Schwerverkehrsaufteilung { /** Anteil der Lkw und Busse an qKfz. */ readonly lkwUndBus: Ratio; /** Anteil der Lkw mit Anhaenger und Sattel-Kfz an qKfz. */ readonly lkwMitAnhaenger: Ratio; } /** * Rechnet einen Lastzuganteil AM SCHWERVERKEHR in die Aufteilung um, mit der * Gl. 2-5 arbeitet - oder gibt `undefined` zurueck, wenn keine brauchbare * Angabe vorliegt. * * WARUM ZWEI BEZUGSGROESSEN: Gl. 2-5 setzt qLkw+Bus und qLkwK ins * Verhaeltnis zu qKfz. Aus einer Zaehlung liest ein Planer aber die Aufteilung * DES SCHWERVERKEHRS ab ("ein Fuenftel der Lastwagen sind Lastzuege"), und * diese Groesse ist vom Schwerverkehrsanteil unabhaengig: Sie bleibt stehen, * wenn die Verkehrsstaerke einer anderen Tageszeit eingetragen wird. Zwei * Anteile an qKfz waeren dagegen zusammen an den Schwerverkehrsanteil gebunden * und muessten bei jeder Aenderung nachgezogen werden - ein Feld, das * stillschweigend falsch wird, ist schlimmer als keines. * * Umrechnung: lkwMitAnhaenger = pSV * lastzugAnteil, * lkwUndBus = pSV * (1 - lastzugAnteil). Die Summe ist genau pSV; mehr * Schwerverkehr als Schwerverkehr kann so nicht entstehen. * * KEIN RUECKFALL AUF 0 BEI UNBRAUCHBARER ANGABE: `undefined` fuehrt beim * Aufrufer auf Gl. 2-6, also auf den Stand vor Schema 13. Ein Rueckfall auf * lastzugAnteil = 0 waere die unsichere Richtung - er unterstellte einen * Schwerverkehr ganz ohne Lastzuege (fSV = 1 + 0,75 * pSV, kleiner als der * Pauschalwert) und damit eine groessere Kapazitaet, aus einer Angabe, die * niemand gemacht hat. Dieselbe Regel wie bei `boolGemeldet` im Schema. */ export function aufteilungAusLastzuganteil( schwerverkehrsanteil: Ratio, lastzugAnteil: number | undefined, ): Schwerverkehrsaufteilung | undefined { if (!istLastzuganteil(lastzugAnteil)) return undefined; const pSV = anteil(schwerverkehrsanteil); return { lkwUndBus: pSV * (1 - lastzugAnteil), lkwMitAnhaenger: pSV * lastzugAnteil, }; } /** * Ist die Angabe ein Lastzuganteil? Nur eine endliche Zahl von 0 bis 1 ist * einer. * * NICHT BEGRENZT, SONDERN VERWORFEN: `anteil` unten kappt auf 0 bis 1, weil * eine Rechnung mit einem Anteil ueber 1 kein sinnvolles Ergebnis hat. Hier * geht es um etwas anderes - um die Frage, OB eine Aufteilung erfasst ist. Ein * eingetragenes -0,3 auf 0 zu kappen hiesse, aus einer unbrauchbaren Angabe * die guenstigste aller Aufteilungen zu machen (kein einziger Lastzug) und den * Strom danach als "Aufteilung erfasst" zu behandeln, sodass auch der Hinweis * im Pruefbericht entfiele. Wer nichts Brauchbares eingetragen hat, bekommt * den Pauschalwert und den Hinweis dazu. */ export function istLastzuganteil(wert: number | undefined): wert is number { return wert !== undefined && isFiniteNumber(wert) && wert >= 0 && wert <= 1; } /** * Wie weit die Saettigungsverkehrsstaerke eines Stroms OHNE erfasste Aufteilung * daneben liegen kann - beziffert fuer genau diesen Schwerverkehrsanteil. * * WARUM DIESE FUNKTION UND NICHT ZWEI ZAHLEN IM MELDUNGSTEXT: Der * Pruefbericht soll dem Bearbeiter sagen, was die Annahme "20 % Lastzuege" fuer * SEINEN Strom bedeutet. Die bisher gedruckten 10,2 % und 31,6 % gelten fuer * 20 % bzw. 100 % Schwerverkehr und sind bei 12 % schlicht falsch. Gerechnet * wird deshalb aus derselben Funktion, mit der auch der Plan rechnet. * * Bezugsgroesse ist die WAHRE Saettigungsverkehrsstaerke: qS = qS0 / fSV, also * ist (qS_Programm - qS_wahr) / qS_wahr = fSV_wahr / fSV_pauschal - 1. Bei * pSV = 20 % sind das 1,300/1,180 - 1 = 10,2 % zu gross (lauter Lastzuege) und * 1 - 1,150/1,180 = 2,5 % zu klein (kein einziger Lastzug). */ export interface Lastzuganteilspanne { /** fSV nach Gl. 2-6, also mit dem Pauschalwert 1,90 - womit gerechnet wird. */ readonly pauschal: Ratio; /** fSV nach Gl. 2-5, wenn der Schwerverkehr keine Lastzuege enthaelt. */ readonly ohneLastzuege: Ratio; /** fSV nach Gl. 2-5, wenn er ausschliesslich aus Lastzuegen besteht. */ readonly nurLastzuege: Ratio; /** Um so viel faellt qS hoechstens ZU GROSS aus (lauter Lastzuege), 0..1. */ readonly hoechstensZuGross: Ratio; /** Um so viel faellt qS hoechstens ZU KLEIN aus (kein Lastzug), 0..1. */ readonly hoechstensZuKlein: Ratio; } export function lastzuganteilSpanne(schwerverkehrsanteil: Ratio): Lastzuganteilspanne { const pSV = anteil(schwerverkehrsanteil); const pauschal = schwerverkehrsfaktor(pSV); const ohneLastzuege = schwerverkehrsfaktor(pSV, { lkwUndBus: pSV, lkwMitAnhaenger: 0 }); const nurLastzuege = schwerverkehrsfaktor(pSV, { lkwUndBus: 0, lkwMitAnhaenger: pSV }); return { pauschal, ohneLastzuege, nurLastzuege, hoechstensZuGross: nurLastzuege / pauschal - 1, hoechstensZuKlein: 1 - ohneLastzuege / pauschal, }; } /** * Anpassungsfaktor fSV fuer den Schwerverkehr nach HBS 2015. * * KORREKTUR (Fassung 5.5.0): Die Saettigungsverkehrsstaerke wurde in * plan/signalPlan.ts mit `base / (1 + Schwerverkehrsanteil)` gerechnet. Das ist * ein Pkw-Gleichwert von 2,0 - nachweislich der Wert des HCM 2010 und nicht der * des HBS, unter dessen Quelle das Programm ihn fuehrte (Belege siehe * HBS_PKW_GLEICHWERTE). * * Der Faktor geht in den Zeitbedarfswert ein: tB = fSV * f1 * f2 * 1,8 und * qS = 3600 / tB. Bei Standardgeometrie (f1 = f2 = 1) heisst das * qS = 2000 / fSV. * * BEIDE GLEICHUNGEN, EINE IM EINSATZ. Woertlich (BASt V 400, S. 10): * * fSV = (qLV + 1,75 * qLkw+Bus + 2,5 * qLkwK) / qKfz (2-5) * fSV = (qLV + 1,9 * qSV) / qKfz (2-6) * * "wird der Anpassungsfaktor fSV nach Gleichung (2-5) bzw. (2-6) in * Abhaengigkeit von der Datenverfuegbarkeit zur Aufteilung des Schwerverkehrs * bestimmt." * * BEIDE GLEICHUNGEN IM EINSATZ - SEIT SCHEMA 13. Bis dahin kannte das * Datenmodell nur `TrafficDemand.heavyVehicleShare` und damit keine Aufteilung; * gerechnet wurde deshalb IMMER nach Gl. 2-6, und `aufteilung` war ein Zweig, * den keine Ansicht befuellte. Jetzt fuehrt jede Verkehrsstaerke einen * optionalen Lastzuganteil am Schwerverkehr (`TrafficDemand.lastzugAnteil`); * ist er erfasst, rechnet der Plan ueber `aufteilungAusLastzuganteil` nach * Gl. 2-5, sonst unveraendert nach Gl. 2-6. * * DAS IST KEIN ANDERES VERFAHREN, SONDERN DIESELBE GLEICHUNG MIT EINER * ANGABE MEHR. V 400, S. 10, zu Bild 2-1 d) woertlich: "Bei einem Anteil der * Lkw mit Anhaenger und Sattel-Kfz (Fahrzeugklasse LkwK) am Schwerverkehr von * 20 % ergibt sich der Funktionsverlauf der Gleichung (2-6)." Nachgerechnet: * 1,75 * 0,8 + 2,50 * 0,2 = 1,90 - exakt. Gl. 2-6 ist Gl. 2-5 mit 20 % * Lastzuegen; wer 20 % eintraegt, aendert an seinen Zahlen nichts (der Nachweis * steht in tests/domain/schwerverkehrsaufteilung.test.ts, die Konstante in * constants.ts als HBS_LASTZUGANTEIL_BEI_UNBEKANNTER_AUFTEILUNG). * * Die Pkw-Gleichwerte stehen ausdruecklich NICHT als Kennwerte in der * Vorgabenverwaltung (Befund C20: keine Zahl unter der Ueberschrift "Vorgaben", * die der Bearbeiter nicht waehlen darf) - sie sind Regelwerkswerte. Waehlbar * ist die Datenlage, nicht der Gleichwert. * * FEHLERRICHTUNG GEGENUEBER DEM ALTSTAND: Mit Gl. 2-6 ist * fSV = 1 + 0,90 * pSV statt bisher 1 + 1,00 * pSV. Die Kapazitaet faellt damit * groesser aus, und zwar umso mehr, je hoeher der Schwerverkehrsanteil ist: * (1 + pSV)/(1 + 0,9 * pSV) - 1, also 0,5 % bei 5 %, 2,4 % bei 30 % und 5,3 % * bei einem reinen Schwerverkehrsstrom. Das ist WENIGER konservativ als bisher * und keine Verschlechterung der Sicherheit (Zwischenzeiten sind nicht * betroffen), aber es ist die unsichere Richtung der Bemessung und gehoert * deshalb offen benannt. * * KORREKTUR DIESES VERMERKS (Fassung 5.5.0): Hier stand "bei 30 % Schwerverkehr * um 2,3 %" als Obergrenze - das ist weder die Obergrenze (der Zuwachs waechst * streng monoton bis 5,3 %) noch bei 30 % richtig gerundet (2,36 %, also * 2,4 %). Die 2,3 % waren die relative Aenderung von fSV, nicht die der * Kapazitaet; zwei Bezugsgroessen vertauscht. * * DER GROESSERE POSTEN GILT JETZT NUR NOCH FUER STROEME OHNE ERFASSTE * AUFTEILUNG. Hier stand vor der Fassung 5.6.0, dieser Posten "bleibt bestehen, * bis die Aufteilung erfassbar ist". Sie ist es. Wo sie erfasst ist, gibt es * ihn nicht mehr; wo sie fehlt, ist er unveraendert: Gl. 2-6 rechnet JEDES * Schwerfahrzeug mit dem Pkw-Gleichwert 1,9 - ein Lastzug traegt nach Gl. 2-5 * aber 2,5. Bei 20 % Schwerverkehr aus lauter Lastzuegen ergibt das * 2000/1,180 = 1695 Kfz/h statt 2000/1,300 = 1538 Kfz/h, also 10,2 % ZU GROSS; * der Fehler waechst mit dem Anteil bis auf 31,6 % bei reinem Lastzugverkehr. * In die andere Richtung ist die Annahme konservativ: Ein Schwerverkehr ohne * Lastzuege ergaebe 2000/1,150 = 1739 Kfz/h, das Programm rechnet dort 2,5 % zu * klein. `lastzuganteilSpanne` beziffert beide Richtungen fuer den jeweiligen * Schwerverkehrsanteil; der Pruefbericht druckt sie, solange die Aufteilung * fehlt und der Anteil ins Gewicht faellt. * * NICHT ABGEBILDET: die uebrigen Anpassungsfaktoren f1 = max(fb, fR, fs) und * f2 = min(1, fs) (Fahrstreifenbreite, Abbiegeradius, Laengsneigung) sowie die * Mischfahrstreifen-Formel. Siehe HBS_STANDARDBEDINGUNGEN. */ export function schwerverkehrsfaktor( schwerverkehrsanteil: Ratio, aufteilung?: Schwerverkehrsaufteilung, ): Ratio { if (aufteilung !== undefined) { // Gl. 2-5. qLV/qKfz = 1 - (Anteil Lkw+Bus) - (Anteil LkwK), also // fSV = 1 + (1,75 - 1) * aLkw+Bus + (2,50 - 1) * aLkwK. // // Zusammen hoechstens 1: Mehr Schwerverkehr als Verkehr gibt es nicht, und // ohne diese Begrenzung liesse sich ein fSV ueber 2,5 erzeugen - also eine // Saettigungsverkehrsstaerke, die kein Verkehrsstrom haben kann. Gekuerzt // wird der leichtere Anteil: Der schwerere bleibt stehen, das ist die // konservative Seite. const lkwMitAnhaenger = anteil(aufteilung.lkwMitAnhaenger); const lkwUndBus = Math.min(anteil(aufteilung.lkwUndBus), 1 - lkwMitAnhaenger); return ( 1 + (HBS_PKW_GLEICHWERTE.lkwUndBus - 1) * lkwUndBus + (HBS_PKW_GLEICHWERTE.lkwMitAnhaenger - 1) * lkwMitAnhaenger ); } // Gl. 2-6: fSV = (1 - pSV) + 1,90 * pSV = 1 + 0,90 * pSV. return 1 + (HBS_PKW_GLEICHWERTE.schwerverkehrPauschal - 1) * anteil(schwerverkehrsanteil); } /** Anteil 0 bis 1; alles andere ist kein Anteil und zaehlt als 0. */ function anteil(wert: number): Ratio { return isFiniteNumber(wert) ? Math.min(1, Math.max(0, wert)) : 0; } /** * Der Schwerverkehrsanteil, mit dem WIRKLICH gerechnet wird: der eingetragene * Anteil, begrenzt auf 0 bis 1. * * BEGRENZT UND NICHT VERWORFEN - anders als beim Lastzuganteil, und das ist * kein Widerspruch: Ein Anteil ueber 100 % ist eine unmoegliche Menge, aber * eine Angabe; "sehr viel Schwerverkehr" auf 100 % zu begrenzen bleibt in der * Sache. Beim Lastzuganteil entscheidet der Bereich dagegen darueber, OB eine * Aufteilung erfasst ist (istLastzuganteil). Gemeldet wird beides * (`schwerverkehrsanteil-ausserhalb` bzw. `lastzuganteil-ausserhalb`). * * HIER UND NICHT DREIMAL: Die Grenze stand als `anteil()` in diesem Modul, ein * zweites Mal als eigene Funktion in plan/signalPlan.ts - und im Pruefbericht * gar nicht: Der Hinweis `leistungsfaehigkeit.schwerverkehr-ohne-aufteilung` * nannte den ROHEN Anteil im Text und rechnete die Kfz/h-Zahlen daneben mit dem * begrenzten. Eine Datei mit 150 % Schwerverkehr ergab damit einen Bericht, der * "150 %" schreibt, Zahlen fuer 100 % druckt und den Wert einen Absatz weiter * als begrenzt meldet. */ export function wirksamerSchwerverkehrsanteil(heavyVehicleShare: number): Ratio { return anteil(heavyVehicleShare); } /** * Saettigungsverkehrsstaerke einer Zufahrt. * * Abbiegende Stroeme erreichen eine geringere Saettigungsverkehrsstaerke als * geradeausfahrende. Der Altbestand rechnete durchgaengig mit dem Grundwert je * Fahrstreifen und ueberschaetzte damit die Kapazitaet von Abbiegestreifen. */ export function saturationFlow( movement: Movement, lanes: number, defaults: RilsaDefaults = RILSA_DEFAULTS, geometrie?: Fahrstreifengeometrie, ): VehiclesPerHour { const effectiveLanes = isFiniteNumber(lanes) && lanes > 0 ? lanes : 1; const base = defaults.capacity.saturationFlow * effectiveLanes; const abgemindert = movement === 'links' || movement === 'rechts' ? base * defaults.capacity.turningFactor : base; // Der Geometriefaktor wirkt auf den ZEITBEDARFSWERT tB, und qS = 3600 / tB - // ein groesserer Zeitbedarf ergibt also eine kleinere // Saettigungsverkehrsstaerke. Deshalb geteilt und nicht multipliziert. return abgemindert / geometriefaktor(geometrie, movement).gesamt; } /** * Geometrie eines Fahrstreifens, soweit sie erfasst ist. * * Jedes Feld darf fehlen; dann gilt die Standardbedingung * (`HBS_STANDARDBEDINGUNGEN`) und der zugehoerige Faktor ist 1. */ export interface Fahrstreifengeometrie { /** Fahrstreifenbreite in m. */ readonly breite?: Meters | undefined; /** Kurvenradius in m. */ readonly radius?: Meters | undefined; /** Laengsneigung in Prozent, positiv ist die Steigung. */ readonly neigung?: number | undefined; } /** Die geometrischen Anpassungsfaktoren des Zeitbedarfswerts, aufgeschluesselt. */ export interface GeometriefaktorErgebnis { /** Fahrstreifenbreite: fb = -0,375 b + 2,125 unter 3,00 m, sonst 1. */ readonly fb: Ratio; /** Kurvenradius: fR = -0,015 R + 1,3 unter 20 m, sonst 1. */ readonly fR: Ratio; /** Laengsneigung: fs = 0,03 s + 1. */ readonly fs: Ratio; readonly f1: Ratio; readonly f2: Ratio; /** Der angesetzte Faktor auf tB: max(1; f1 * f2). */ readonly gesamt: Ratio; /** * Hat der Deckel gegriffen, also haette die Geometrie die * Saettigungsverkehrsstaerke nach der geschlossenen Formel angehoben? */ readonly gedeckelt: boolean; /** Wurde fR unterdrueckt, weil der pauschale Abbiegerfaktor ihn schon deckt? */ readonly radiusVomAbbiegerfaktorGedeckt: boolean; } /** * Die geometrischen Anpassungsfaktoren des Zeitbedarfswerts nach BASt V 400. * * WOERTLICH (V 400): fb = -0,375 * b + 2,125 fuer b < 3,00 m; * fR = -0,015 * R + 1,3 fuer R < 20 m; fs = 0,03 * s + 1,0. Daraus * f1 = max(fb, fR, fs) und f2 = min(1, fs); der Zeitbedarfswert ist * tB = fSV * f1 * f2 * 1,8 und qS = 3600 / tB. * * WARUM ES SIE BIS 5.29.0 NICHT GAB: Das Programm erfasste weder Breite noch * Radius noch Neigung und rechnete deshalb IMMER mit Standardbedingungen. Die * eigene Unterlage nannte das die unsichere Richtung, und sie hatte recht: An * einer 2,50 m breiten Zufahrt faellt die Saettigungsverkehrsstaerke ohne fb um * 18,8 % zu gross aus (2000 statt 1684 Kfz/h), an einer 4-%-Steigung um 12 % * (2000 statt 1786). Nicht die Aenderung ist gefaehrlich, sondern ihr * Ausbleiben. * * ZWEI ABWEICHUNGEN VON DER GESCHLOSSENEN FORMEL, beide in die sichere * Richtung, beide entschieden und nicht uebersehen: * * ERSTENS DER DECKEL BEI 1. Nach der Formel senkt ein Gefaelle den * Zeitbedarfswert: f1 = 1, f2 = fs < 1, und bei s = -4 % ergaebe das * 2000/0,88 = 2273 Kfz/h, also 13,6 % ueber dem Grundwert. Dieses Programm * folgt dem NICHT. Die Geometrie darf die Saettigungsverkehrsstaerke nur * senken, nie heben - `gesamt` ist deshalb `max(1; f1 * f2)`. * * Der Grund ist nicht Vorsicht um ihrer selbst willen, sondern die Summe der * bekannten Auslassungen dieser Bemessung: Sie rechnet mit EINEM pauschalen * Abbiegerfaktor statt der differenzierten HBS-Faktoren, kennt keine * Ersatz-Saettigungsverkehrsstaerke des Mischfahrstreifens und keine * 15-min-Ganglinie je Strom. Alle drei Luecken wirken nach oben. Eine vierte * Zahl obendrauf, die die Kapazitaet ueber den Grundwert hebt, waere die * gefaehrliche Richtung - dasselbe Muster wie die Raeumgeschwindigkeit der * Fussgaenger, die dieses Programm hart beim Regelwert deckelt, obwohl das * Regelwerk mehr zulaesst (SAFETY_FLOORS.maxClearingSpeedFuss). Wo der Deckel * greift, sagt es der Pruefbericht; verschwiegen wird die Abweichung nicht. * * ZWEITENS KEIN fR AN ABBIEGENDEN STROEMEN. `quellen.ts` fuehrt den pauschalen * Abbiegerfaktor 0,90 ausdruecklich als Ersatz fuer die differenzierten * HBS-Faktoren nach Abbiegeradius, Gegenverkehr und Fussgaengerquerung. Wer fR * daneben multipliziert, mindert den Radius zweimal ab: Ein Rechtsabbieger mit * 10 m Radius fiele auf 0,90/1,15 = 0,783 des Grundwerts, also auf * 1565 Kfz/h. Der pauschale Faktor bleibt, fR entfaellt dort - und `gedeckt` * sagt es, damit die Ansicht das Feld nicht als wirksam ausgibt. * * Fuer geradeaus fahrende Stroeme greift fR weiterhin: Ein enger Bogen im * durchgehenden Fahrstreifen ist von keinem Abbiegerfaktor gedeckt. */ export function geometriefaktor( geometrie: Fahrstreifengeometrie | undefined, movement: Movement, ): GeometriefaktorErgebnis { const abbiegend = movement === 'links' || movement === 'rechts'; const breite = zahlOderNull(geometrie?.breite); const radius = zahlOderNull(geometrie?.radius); const neigung = zahlOderNull(geometrie?.neigung); const fb = breite !== null && breite < HBS_STANDARDBEDINGUNGEN.fahrstreifenbreiteAb ? HBS_GEOMETRIE.breite.steigung * breite + HBS_GEOMETRIE.breite.achsenabschnitt : 1; const engerBogen = radius !== null && radius < HBS_STANDARDBEDINGUNGEN.kurvenradiusAb; const fR = engerBogen && !abbiegend ? HBS_GEOMETRIE.radius.steigung * radius + HBS_GEOMETRIE.radius.achsenabschnitt : 1; const fs = neigung !== null ? HBS_GEOMETRIE.neigung.steigung * neigung + 1 : 1; const f1 = Math.max(fb, fR, fs); const f2 = Math.min(1, fs); const roh = f1 * f2; return { fb: roundTo(fb, 6), fR: roundTo(fR, 6), fs: roundTo(fs, 6), f1: roundTo(f1, 6), f2: roundTo(f2, 6), gesamt: roundTo(Math.max(1, roh), 6), gedeckelt: roh < 1, radiusVomAbbiegerfaktorGedeckt: abbiegend && engerBogen, }; } /** Ein erfasster Zahlenwert - oder null, wenn keiner brauchbar vorliegt. */ function zahlOderNull(wert: number | undefined): number | null { if (wert === undefined || !isFiniteNumber(wert)) return null; return wert; } /** * Massgebender Saettigungsgrad y = q / qS eines Verkehrsstroms. * Diese Groesse geht als Summe Y in die Umlaufzeitermittlung ein. */ export function criticalFlowRatio(demand: VehiclesPerHour, saturation: VehiclesPerHour): Ratio { if (!isFiniteNumber(demand) || demand < 0) return 0; if (!isFiniteNumber(saturation) || saturation <= 0) return Number.POSITIVE_INFINITY; return demand / saturation; } export interface CapacityOptions { /** Bewertungsverfahren; bestimmt, ob mit tA = tF + 1 s (HBS) oder g = tF (HCM) gerechnet wird. */ readonly verfahren?: Bewertungsverfahren; /** * Anzahl der Freigabezeitfenster im Umlauf. Nach HBS 2015 endet jedes * Fenster mit einer Gelbzeit, in die der Abfluss noch 1 s hineinlaeuft; der * Zuschlag faellt deshalb je Fenster an. Regelfall 1. */ readonly freigabefenster?: number; /** * Hoechster Auslastungsgrad aus den Projektvorgaben, ab dem gewarnt wird. * Fehlt er, gilt der Regelwert - dann meldet die Notiz bei geaenderten * Vorgaben eine andere Schwelle als der Pruefbericht. */ readonly maxDegreeOfSaturation?: Ratio; } /** * Abflusszeit tA aus der Freigabezeit. * * HBS 2015: tA = tF + 1 s je Freigabezeitfenster (Belege siehe HBS_ABFLUSSZUSCHLAG). * Die Abflusszeit kann die Umlaufzeit nicht ueberschreiten. * HCM: die Freigabezeit selbst (Anfahrverlust und Gelbnutzung heben sich im * Regelfall auf). */ export function abflusszeit( greenTime: Seconds, cycleTime: Seconds, options: CapacityOptions = {}, ): Seconds { const verfahren = options.verfahren ?? 'HBS'; const green = isFiniteNumber(greenTime) && greenTime > 0 ? greenTime : 0; if (verfahren !== 'HBS' || green === 0) return Math.min(green, cycleTime); const fenster = isFiniteNumber(options.freigabefenster) && options.freigabefenster > 0 ? Math.floor(options.freigabefenster) : 1; return Math.min(cycleTime, green + fenster * HBS_ABFLUSSZUSCHLAG); } /** * Kapazitaet einer Signalgruppe: c = qS * tA / tU. * * KORREKTUR (Fassung 5.4.0, Befund B2): Das HBS 2015 rechnet die * Kapazitaet mit der Abflusszeit tA = tF + 1 s, nicht mit der Freigabezeit. * Nachgerechnet am LISA+-Ausdruck "HBS-Bewertung 2015" (Kornwestheim 2017): * tF = 10 s, tA = 11 s, qS = 1992 Kfz/h, tU = 90 s -> C = 1992 * 11 / 90 = * 243 Kfz/h, wie dort ausgewiesen. * * Der Altbestand meldete zusaetzlich pauschal "reserve: 15 % der Kapazitaet" * und einen Auslastungsgrad von konstant 0, unabhaengig von der Verkehrsstaerke. * Beides ist entfallen; der Auslastungsgrad wird jetzt tatsaechlich gerechnet, * sofern eine Verkehrsstaerke vorliegt. */ export function computeCapacity( greenTime: Seconds, cycleTime: Seconds, saturation: VehiclesPerHour, demand?: VehiclesPerHour, options: CapacityOptions = {}, ): CapacityResult { const notes: CalculationNote[] = []; const verfahren = options.verfahren ?? 'HBS'; if (!isFiniteNumber(cycleTime) || cycleTime <= 0) { notes.push({ severity: 'fehler', code: 'umlaufzeit-ungueltig', message: 'Die Umlaufzeit ist kein gültiger Wert größer als 0; die Kapazität ist nicht bestimmbar.', }); return { verfahren, capacity: 0, abflusszeit: 0, greenRatio: 0, saturationFlow: saturation, notes, }; } const green = isFiniteNumber(greenTime) && greenTime > 0 ? greenTime : 0; if (green === 0) { notes.push({ severity: 'warnung', code: 'freigabezeit-null', message: 'Die Freigabezeit beträgt 0 s; die Signalgruppe hat keine Kapazität.', }); } if (green > cycleTime) { notes.push({ severity: 'fehler', code: 'freigabezeit-ueber-umlaufzeit', message: `Die Freigabezeit ${green} s ist länger als die Umlaufzeit ${cycleTime} s. ` + 'Der Signalzeitenplan ist in sich widersprüchlich.', }); } const tA = abflusszeit(green, cycleTime, options); const greenRatio = Math.min(1, tA / cycleTime); const capacity = saturation * greenRatio; const result: CapacityResult = { verfahren, capacity: Math.round(capacity), abflusszeit: roundTo(tA, 3), greenRatio: roundTo(greenRatio, 4), saturationFlow: saturation, notes, }; if (demand === undefined) return result; if (!isFiniteNumber(demand) || demand < 0) { notes.push({ severity: 'warnung', code: 'verkehrsstaerke-ungueltig', message: 'Die angegebene Verkehrsstärke ist kein gültiger Wert; der Auslastungsgrad entfällt.', }); return result; } // Der Auslastungsgrad wird aus der AUSGEWIESENEN (ganzzahligen) Kapazitaet // gebildet, nicht aus dem ungerundeten Zwischenwert: Nach HBS 2015 haengt // die Stufe F an q > C, und beide Zahlen stehen ganzzahlig im Ausdruck. Wer // dort 244 Fz/h neben 243 Fz/h liest, muss auf dieselbe Aussage kommen wie // das Programm. (So rechnet auch der LISA+-Ausdruck: x = q / C_gerundet.) // Ohne Nachfrage und ohne Kapazitaet ist nichts ueberlastet (q > C gilt // nicht fuer 0 > 0): Auslastungsgrad 0, keine Meldung. const degree = result.capacity > 0 ? demand / result.capacity : demand > 0 ? Number.POSITIVE_INFINITY : 0; const hoechsterAuslastungsgrad = options.maxDegreeOfSaturation ?? RILSA_DEFAULTS.capacity.maxDegreeOfSaturation; if (degree > 1) { notes.push({ severity: 'fehler', code: 'auslastung-ueber-100', message: `Der Auslastungsgrad beträgt ${formatRatio(degree)} und liegt über 1,0. ` + 'Die Nachfrage übersteigt die Kapazität; es entsteht ein wachsender Rückstau.', }); } else if (degree > hoechsterAuslastungsgrad) { notes.push({ severity: 'warnung', code: 'auslastung-kritisch', message: `Der Auslastungsgrad beträgt ${formatRatio(degree)} und liegt über dem Höchstwert ` + `${formatRatio(hoechsterAuslastungsgrad)}. Der Verkehrsablauf ist instabil.`, }); } return { ...result, degreeOfSaturation: roundTo(degree, 4) }; } function formatRatio(value: number): string { if (!Number.isFinite(value)) return 'unendlich'; return roundTo(value, 2).toFixed(2).replace('.', ','); }