lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ src domain rilsa capacity.ts
| 1 | import type { Meters, Ratio, Seconds, VehiclesPerHour } from '../units'; |
| 2 | import { isFiniteNumber, roundTo } from '../units'; |
| 3 | import { |
| 4 | HBS_ABFLUSSZUSCHLAG, |
| 5 | HBS_GEOMETRIE, |
| 6 | HBS_PKW_GLEICHWERTE, |
| 7 | HBS_STANDARDBEDINGUNGEN, |
| 8 | RILSA_DEFAULTS, |
| 9 | type RilsaDefaults, |
| 10 | } from './constants'; |
| 11 | import type { Bewertungsverfahren, CalculationNote, CapacityResult, Movement } from './types'; |
| 12 | |
| 13 | /** |
| 14 | * Aufteilung des Schwerverkehrs auf die beiden Fahrzeugklassen des HBS, |
| 15 | * jeweils als Anteil an der GESAMTVERKEHRSSTAERKE qKfz (nicht am |
| 16 | * Schwerverkehr). |
| 17 | * |
| 18 | * Erfassbar seit Schema 13 - allerdings NICHT in dieser Form: Das Projekt |
| 19 | * fuehrt je Verkehrsstaerke einen Lastzuganteil AM SCHWERVERKEHR |
| 20 | * (`TrafficDemand.lastzugAnteil`), und `aufteilungAusLastzuganteil` rechnet ihn |
| 21 | * auf die beiden Anteile an qKfz um, mit denen Gl. 2-5 arbeitet. Warum die |
| 22 | * Eingabe an einer anderen Bezugsgroesse haengt als die Rechnung, steht dort. |
| 23 | */ |
| 24 | export interface Schwerverkehrsaufteilung { |
| 25 | /** Anteil der Lkw und Busse an qKfz. */ |
| 26 | readonly lkwUndBus: Ratio; |
| 27 | /** Anteil der Lkw mit Anhaenger und Sattel-Kfz an qKfz. */ |
| 28 | readonly lkwMitAnhaenger: Ratio; |
| 29 | } |
| 30 | |
| 31 | /** |
| 32 | * Rechnet einen Lastzuganteil AM SCHWERVERKEHR in die Aufteilung um, mit der |
| 33 | * Gl. 2-5 arbeitet - oder gibt `undefined` zurueck, wenn keine brauchbare |
| 34 | * Angabe vorliegt. |
| 35 | * |
| 36 | * WARUM ZWEI BEZUGSGROESSEN: Gl. 2-5 setzt qLkw+Bus und qLkwK ins |
| 37 | * Verhaeltnis zu qKfz. Aus einer Zaehlung liest ein Planer aber die Aufteilung |
| 38 | * DES SCHWERVERKEHRS ab ("ein Fuenftel der Lastwagen sind Lastzuege"), und |
| 39 | * diese Groesse ist vom Schwerverkehrsanteil unabhaengig: Sie bleibt stehen, |
| 40 | * wenn die Verkehrsstaerke einer anderen Tageszeit eingetragen wird. Zwei |
| 41 | * Anteile an qKfz waeren dagegen zusammen an den Schwerverkehrsanteil gebunden |
| 42 | * und muessten bei jeder Aenderung nachgezogen werden - ein Feld, das |
| 43 | * stillschweigend falsch wird, ist schlimmer als keines. |
| 44 | * |
| 45 | * Umrechnung: lkwMitAnhaenger = pSV * lastzugAnteil, |
| 46 | * lkwUndBus = pSV * (1 - lastzugAnteil). Die Summe ist genau pSV; mehr |
| 47 | * Schwerverkehr als Schwerverkehr kann so nicht entstehen. |
| 48 | * |
| 49 | * KEIN RUECKFALL AUF 0 BEI UNBRAUCHBARER ANGABE: `undefined` fuehrt beim |
| 50 | * Aufrufer auf Gl. 2-6, also auf den Stand vor Schema 13. Ein Rueckfall auf |
| 51 | * lastzugAnteil = 0 waere die unsichere Richtung - er unterstellte einen |
| 52 | * Schwerverkehr ganz ohne Lastzuege (fSV = 1 + 0,75 * pSV, kleiner als der |
| 53 | * Pauschalwert) und damit eine groessere Kapazitaet, aus einer Angabe, die |
| 54 | * niemand gemacht hat. Dieselbe Regel wie bei `boolGemeldet` im Schema. |
| 55 | */ |
| 56 | export function aufteilungAusLastzuganteil( |
| 57 | schwerverkehrsanteil: Ratio, |
| 58 | lastzugAnteil: number | undefined, |
| 59 | ): Schwerverkehrsaufteilung | undefined { |
| 60 | if (!istLastzuganteil(lastzugAnteil)) return undefined; |
| 61 | const pSV = anteil(schwerverkehrsanteil); |
| 62 | return { |
| 63 | lkwUndBus: pSV * (1 - lastzugAnteil), |
| 64 | lkwMitAnhaenger: pSV * lastzugAnteil, |
| 65 | }; |
| 66 | } |
| 67 | |
| 68 | /** |
| 69 | * Ist die Angabe ein Lastzuganteil? Nur eine endliche Zahl von 0 bis 1 ist |
| 70 | * einer. |
| 71 | * |
| 72 | * NICHT BEGRENZT, SONDERN VERWORFEN: `anteil` unten kappt auf 0 bis 1, weil |
| 73 | * eine Rechnung mit einem Anteil ueber 1 kein sinnvolles Ergebnis hat. Hier |
| 74 | * geht es um etwas anderes - um die Frage, OB eine Aufteilung erfasst ist. Ein |
| 75 | * eingetragenes -0,3 auf 0 zu kappen hiesse, aus einer unbrauchbaren Angabe |
| 76 | * die guenstigste aller Aufteilungen zu machen (kein einziger Lastzug) und den |
| 77 | * Strom danach als "Aufteilung erfasst" zu behandeln, sodass auch der Hinweis |
| 78 | * im Pruefbericht entfiele. Wer nichts Brauchbares eingetragen hat, bekommt |
| 79 | * den Pauschalwert und den Hinweis dazu. |
| 80 | */ |
| 81 | export function istLastzuganteil(wert: number | undefined): wert is number { |
| 82 | return wert !== undefined && isFiniteNumber(wert) && wert >= 0 && wert <= 1; |
| 83 | } |
| 84 | |
| 85 | /** |
| 86 | * Wie weit die Saettigungsverkehrsstaerke eines Stroms OHNE erfasste Aufteilung |
| 87 | * daneben liegen kann - beziffert fuer genau diesen Schwerverkehrsanteil. |
| 88 | * |
| 89 | * WARUM DIESE FUNKTION UND NICHT ZWEI ZAHLEN IM MELDUNGSTEXT: Der |
| 90 | * Pruefbericht soll dem Bearbeiter sagen, was die Annahme "20 % Lastzuege" fuer |
| 91 | * SEINEN Strom bedeutet. Die bisher gedruckten 10,2 % und 31,6 % gelten fuer |
| 92 | * 20 % bzw. 100 % Schwerverkehr und sind bei 12 % schlicht falsch. Gerechnet |
| 93 | * wird deshalb aus derselben Funktion, mit der auch der Plan rechnet. |
| 94 | * |
| 95 | * Bezugsgroesse ist die WAHRE Saettigungsverkehrsstaerke: qS = qS0 / fSV, also |
| 96 | * ist (qS_Programm - qS_wahr) / qS_wahr = fSV_wahr / fSV_pauschal - 1. Bei |
| 97 | * pSV = 20 % sind das 1,300/1,180 - 1 = 10,2 % zu gross (lauter Lastzuege) und |
| 98 | * 1 - 1,150/1,180 = 2,5 % zu klein (kein einziger Lastzug). |
| 99 | */ |
| 100 | export interface Lastzuganteilspanne { |
| 101 | /** fSV nach Gl. 2-6, also mit dem Pauschalwert 1,90 - womit gerechnet wird. */ |
| 102 | readonly pauschal: Ratio; |
| 103 | /** fSV nach Gl. 2-5, wenn der Schwerverkehr keine Lastzuege enthaelt. */ |
| 104 | readonly ohneLastzuege: Ratio; |
| 105 | /** fSV nach Gl. 2-5, wenn er ausschliesslich aus Lastzuegen besteht. */ |
| 106 | readonly nurLastzuege: Ratio; |
| 107 | /** Um so viel faellt qS hoechstens ZU GROSS aus (lauter Lastzuege), 0..1. */ |
| 108 | readonly hoechstensZuGross: Ratio; |
| 109 | /** Um so viel faellt qS hoechstens ZU KLEIN aus (kein Lastzug), 0..1. */ |
| 110 | readonly hoechstensZuKlein: Ratio; |
| 111 | } |
| 112 | |
| 113 | export function lastzuganteilSpanne(schwerverkehrsanteil: Ratio): Lastzuganteilspanne { |
| 114 | const pSV = anteil(schwerverkehrsanteil); |
| 115 | const pauschal = schwerverkehrsfaktor(pSV); |
| 116 | const ohneLastzuege = schwerverkehrsfaktor(pSV, { lkwUndBus: pSV, lkwMitAnhaenger: 0 }); |
| 117 | const nurLastzuege = schwerverkehrsfaktor(pSV, { lkwUndBus: 0, lkwMitAnhaenger: pSV }); |
| 118 | return { |
| 119 | pauschal, |
| 120 | ohneLastzuege, |
| 121 | nurLastzuege, |
| 122 | hoechstensZuGross: nurLastzuege / pauschal - 1, |
| 123 | hoechstensZuKlein: 1 - ohneLastzuege / pauschal, |
| 124 | }; |
| 125 | } |
| 126 | |
| 127 | /** |
| 128 | * Anpassungsfaktor fSV fuer den Schwerverkehr nach HBS 2015. |
| 129 | * |
| 130 | * KORREKTUR (Fassung 5.5.0): Die Saettigungsverkehrsstaerke wurde in |
| 131 | * plan/signalPlan.ts mit `base / (1 + Schwerverkehrsanteil)` gerechnet. Das ist |
| 132 | * ein Pkw-Gleichwert von 2,0 - nachweislich der Wert des HCM 2010 und nicht der |
| 133 | * des HBS, unter dessen Quelle das Programm ihn fuehrte (Belege siehe |
| 134 | * HBS_PKW_GLEICHWERTE). |
| 135 | * |
| 136 | * Der Faktor geht in den Zeitbedarfswert ein: tB = fSV * f1 * f2 * 1,8 und |
| 137 | * qS = 3600 / tB. Bei Standardgeometrie (f1 = f2 = 1) heisst das |
| 138 | * qS = 2000 / fSV. |
| 139 | * |
| 140 | * BEIDE GLEICHUNGEN, EINE IM EINSATZ. Woertlich (BASt V 400, S. 10): |
| 141 | * |
| 142 | * fSV = (qLV + 1,75 * qLkw+Bus + 2,5 * qLkwK) / qKfz (2-5) |
| 143 | * fSV = (qLV + 1,9 * qSV) / qKfz (2-6) |
| 144 | * |
| 145 | * "wird der Anpassungsfaktor fSV nach Gleichung (2-5) bzw. (2-6) in |
| 146 | * Abhaengigkeit von der Datenverfuegbarkeit zur Aufteilung des Schwerverkehrs |
| 147 | * bestimmt." |
| 148 | * |
| 149 | * BEIDE GLEICHUNGEN IM EINSATZ - SEIT SCHEMA 13. Bis dahin kannte das |
| 150 | * Datenmodell nur `TrafficDemand.heavyVehicleShare` und damit keine Aufteilung; |
| 151 | * gerechnet wurde deshalb IMMER nach Gl. 2-6, und `aufteilung` war ein Zweig, |
| 152 | * den keine Ansicht befuellte. Jetzt fuehrt jede Verkehrsstaerke einen |
| 153 | * optionalen Lastzuganteil am Schwerverkehr (`TrafficDemand.lastzugAnteil`); |
| 154 | * ist er erfasst, rechnet der Plan ueber `aufteilungAusLastzuganteil` nach |
| 155 | * Gl. 2-5, sonst unveraendert nach Gl. 2-6. |
| 156 | * |
| 157 | * DAS IST KEIN ANDERES VERFAHREN, SONDERN DIESELBE GLEICHUNG MIT EINER |
| 158 | * ANGABE MEHR. V 400, S. 10, zu Bild 2-1 d) woertlich: "Bei einem Anteil der |
| 159 | * Lkw mit Anhaenger und Sattel-Kfz (Fahrzeugklasse LkwK) am Schwerverkehr von |
| 160 | * 20 % ergibt sich der Funktionsverlauf der Gleichung (2-6)." Nachgerechnet: |
| 161 | * 1,75 * 0,8 + 2,50 * 0,2 = 1,90 - exakt. Gl. 2-6 ist Gl. 2-5 mit 20 % |
| 162 | * Lastzuegen; wer 20 % eintraegt, aendert an seinen Zahlen nichts (der Nachweis |
| 163 | * steht in tests/domain/schwerverkehrsaufteilung.test.ts, die Konstante in |
| 164 | * constants.ts als HBS_LASTZUGANTEIL_BEI_UNBEKANNTER_AUFTEILUNG). |
| 165 | * |
| 166 | * Die Pkw-Gleichwerte stehen ausdruecklich NICHT als Kennwerte in der |
| 167 | * Vorgabenverwaltung (Befund C20: keine Zahl unter der Ueberschrift "Vorgaben", |
| 168 | * die der Bearbeiter nicht waehlen darf) - sie sind Regelwerkswerte. Waehlbar |
| 169 | * ist die Datenlage, nicht der Gleichwert. |
| 170 | * |
| 171 | * FEHLERRICHTUNG GEGENUEBER DEM ALTSTAND: Mit Gl. 2-6 ist |
| 172 | * fSV = 1 + 0,90 * pSV statt bisher 1 + 1,00 * pSV. Die Kapazitaet faellt damit |
| 173 | * groesser aus, und zwar umso mehr, je hoeher der Schwerverkehrsanteil ist: |
| 174 | * (1 + pSV)/(1 + 0,9 * pSV) - 1, also 0,5 % bei 5 %, 2,4 % bei 30 % und 5,3 % |
| 175 | * bei einem reinen Schwerverkehrsstrom. Das ist WENIGER konservativ als bisher |
| 176 | * und keine Verschlechterung der Sicherheit (Zwischenzeiten sind nicht |
| 177 | * betroffen), aber es ist die unsichere Richtung der Bemessung und gehoert |
| 178 | * deshalb offen benannt. |
| 179 | * |
| 180 | * KORREKTUR DIESES VERMERKS (Fassung 5.5.0): Hier stand "bei 30 % Schwerverkehr |
| 181 | * um 2,3 %" als Obergrenze - das ist weder die Obergrenze (der Zuwachs waechst |
| 182 | * streng monoton bis 5,3 %) noch bei 30 % richtig gerundet (2,36 %, also |
| 183 | * 2,4 %). Die 2,3 % waren die relative Aenderung von fSV, nicht die der |
| 184 | * Kapazitaet; zwei Bezugsgroessen vertauscht. |
| 185 | * |
| 186 | * DER GROESSERE POSTEN GILT JETZT NUR NOCH FUER STROEME OHNE ERFASSTE |
| 187 | * AUFTEILUNG. Hier stand vor der Fassung 5.6.0, dieser Posten "bleibt bestehen, |
| 188 | * bis die Aufteilung erfassbar ist". Sie ist es. Wo sie erfasst ist, gibt es |
| 189 | * ihn nicht mehr; wo sie fehlt, ist er unveraendert: Gl. 2-6 rechnet JEDES |
| 190 | * Schwerfahrzeug mit dem Pkw-Gleichwert 1,9 - ein Lastzug traegt nach Gl. 2-5 |
| 191 | * aber 2,5. Bei 20 % Schwerverkehr aus lauter Lastzuegen ergibt das |
| 192 | * 2000/1,180 = 1695 Kfz/h statt 2000/1,300 = 1538 Kfz/h, also 10,2 % ZU GROSS; |
| 193 | * der Fehler waechst mit dem Anteil bis auf 31,6 % bei reinem Lastzugverkehr. |
| 194 | * In die andere Richtung ist die Annahme konservativ: Ein Schwerverkehr ohne |
| 195 | * Lastzuege ergaebe 2000/1,150 = 1739 Kfz/h, das Programm rechnet dort 2,5 % zu |
| 196 | * klein. `lastzuganteilSpanne` beziffert beide Richtungen fuer den jeweiligen |
| 197 | * Schwerverkehrsanteil; der Pruefbericht druckt sie, solange die Aufteilung |
| 198 | * fehlt und der Anteil ins Gewicht faellt. |
| 199 | * |
| 200 | * NICHT ABGEBILDET: die uebrigen Anpassungsfaktoren f1 = max(fb, fR, fs) und |
| 201 | * f2 = min(1, fs) (Fahrstreifenbreite, Abbiegeradius, Laengsneigung) sowie die |
| 202 | * Mischfahrstreifen-Formel. Siehe HBS_STANDARDBEDINGUNGEN. |
| 203 | */ |
| 204 | export function schwerverkehrsfaktor( |
| 205 | schwerverkehrsanteil: Ratio, |
| 206 | aufteilung?: Schwerverkehrsaufteilung, |
| 207 | ): Ratio { |
| 208 | if (aufteilung !== undefined) { |
| 209 | // Gl. 2-5. qLV/qKfz = 1 - (Anteil Lkw+Bus) - (Anteil LkwK), also |
| 210 | // fSV = 1 + (1,75 - 1) * aLkw+Bus + (2,50 - 1) * aLkwK. |
| 211 | // |
| 212 | // Zusammen hoechstens 1: Mehr Schwerverkehr als Verkehr gibt es nicht, und |
| 213 | // ohne diese Begrenzung liesse sich ein fSV ueber 2,5 erzeugen - also eine |
| 214 | // Saettigungsverkehrsstaerke, die kein Verkehrsstrom haben kann. Gekuerzt |
| 215 | // wird der leichtere Anteil: Der schwerere bleibt stehen, das ist die |
| 216 | // konservative Seite. |
| 217 | const lkwMitAnhaenger = anteil(aufteilung.lkwMitAnhaenger); |
| 218 | const lkwUndBus = Math.min(anteil(aufteilung.lkwUndBus), 1 - lkwMitAnhaenger); |
| 219 | return ( |
| 220 | 1 + |
| 221 | (HBS_PKW_GLEICHWERTE.lkwUndBus - 1) * lkwUndBus + |
| 222 | (HBS_PKW_GLEICHWERTE.lkwMitAnhaenger - 1) * lkwMitAnhaenger |
| 223 | ); |
| 224 | } |
| 225 | // Gl. 2-6: fSV = (1 - pSV) + 1,90 * pSV = 1 + 0,90 * pSV. |
| 226 | return 1 + (HBS_PKW_GLEICHWERTE.schwerverkehrPauschal - 1) * anteil(schwerverkehrsanteil); |
| 227 | } |
| 228 | |
| 229 | /** Anteil 0 bis 1; alles andere ist kein Anteil und zaehlt als 0. */ |
| 230 | function anteil(wert: number): Ratio { |
| 231 | return isFiniteNumber(wert) ? Math.min(1, Math.max(0, wert)) : 0; |
| 232 | } |
| 233 | |
| 234 | /** |
| 235 | * Der Schwerverkehrsanteil, mit dem WIRKLICH gerechnet wird: der eingetragene |
| 236 | * Anteil, begrenzt auf 0 bis 1. |
| 237 | * |
| 238 | * BEGRENZT UND NICHT VERWORFEN - anders als beim Lastzuganteil, und das ist |
| 239 | * kein Widerspruch: Ein Anteil ueber 100 % ist eine unmoegliche Menge, aber |
| 240 | * eine Angabe; "sehr viel Schwerverkehr" auf 100 % zu begrenzen bleibt in der |
| 241 | * Sache. Beim Lastzuganteil entscheidet der Bereich dagegen darueber, OB eine |
| 242 | * Aufteilung erfasst ist (istLastzuganteil). Gemeldet wird beides |
| 243 | * (`schwerverkehrsanteil-ausserhalb` bzw. `lastzuganteil-ausserhalb`). |
| 244 | * |
| 245 | * HIER UND NICHT DREIMAL: Die Grenze stand als `anteil()` in diesem Modul, ein |
| 246 | * zweites Mal als eigene Funktion in plan/signalPlan.ts - und im Pruefbericht |
| 247 | * gar nicht: Der Hinweis `leistungsfaehigkeit.schwerverkehr-ohne-aufteilung` |
| 248 | * nannte den ROHEN Anteil im Text und rechnete die Kfz/h-Zahlen daneben mit dem |
| 249 | * begrenzten. Eine Datei mit 150 % Schwerverkehr ergab damit einen Bericht, der |
| 250 | * "150 %" schreibt, Zahlen fuer 100 % druckt und den Wert einen Absatz weiter |
| 251 | * als begrenzt meldet. |
| 252 | */ |
| 253 | export function wirksamerSchwerverkehrsanteil(heavyVehicleShare: number): Ratio { |
| 254 | return anteil(heavyVehicleShare); |
| 255 | } |
| 256 | |
| 257 | /** |
| 258 | * Saettigungsverkehrsstaerke einer Zufahrt. |
| 259 | * |
| 260 | * Abbiegende Stroeme erreichen eine geringere Saettigungsverkehrsstaerke als |
| 261 | * geradeausfahrende. Der Altbestand rechnete durchgaengig mit dem Grundwert je |
| 262 | * Fahrstreifen und ueberschaetzte damit die Kapazitaet von Abbiegestreifen. |
| 263 | */ |
| 264 | export function saturationFlow( |
| 265 | movement: Movement, |
| 266 | lanes: number, |
| 267 | defaults: RilsaDefaults = RILSA_DEFAULTS, |
| 268 | geometrie?: Fahrstreifengeometrie, |
| 269 | ): VehiclesPerHour { |
| 270 | const effectiveLanes = isFiniteNumber(lanes) && lanes > 0 ? lanes : 1; |
| 271 | const base = defaults.capacity.saturationFlow * effectiveLanes; |
| 272 | const abgemindert = |
| 273 | movement === 'links' || movement === 'rechts' ? base * defaults.capacity.turningFactor : base; |
| 274 | // Der Geometriefaktor wirkt auf den ZEITBEDARFSWERT tB, und qS = 3600 / tB - |
| 275 | // ein groesserer Zeitbedarf ergibt also eine kleinere |
| 276 | // Saettigungsverkehrsstaerke. Deshalb geteilt und nicht multipliziert. |
| 277 | return abgemindert / geometriefaktor(geometrie, movement).gesamt; |
| 278 | } |
| 279 | |
| 280 | /** |
| 281 | * Geometrie eines Fahrstreifens, soweit sie erfasst ist. |
| 282 | * |
| 283 | * Jedes Feld darf fehlen; dann gilt die Standardbedingung |
| 284 | * (`HBS_STANDARDBEDINGUNGEN`) und der zugehoerige Faktor ist 1. |
| 285 | */ |
| 286 | export interface Fahrstreifengeometrie { |
| 287 | /** Fahrstreifenbreite in m. */ |
| 288 | readonly breite?: Meters | undefined; |
| 289 | /** Kurvenradius in m. */ |
| 290 | readonly radius?: Meters | undefined; |
| 291 | /** Laengsneigung in Prozent, positiv ist die Steigung. */ |
| 292 | readonly neigung?: number | undefined; |
| 293 | } |
| 294 | |
| 295 | /** Die geometrischen Anpassungsfaktoren des Zeitbedarfswerts, aufgeschluesselt. */ |
| 296 | export interface GeometriefaktorErgebnis { |
| 297 | /** Fahrstreifenbreite: fb = -0,375 b + 2,125 unter 3,00 m, sonst 1. */ |
| 298 | readonly fb: Ratio; |
| 299 | /** Kurvenradius: fR = -0,015 R + 1,3 unter 20 m, sonst 1. */ |
| 300 | readonly fR: Ratio; |
| 301 | /** Laengsneigung: fs = 0,03 s + 1. */ |
| 302 | readonly fs: Ratio; |
| 303 | readonly f1: Ratio; |
| 304 | readonly f2: Ratio; |
| 305 | /** Der angesetzte Faktor auf tB: max(1; f1 * f2). */ |
| 306 | readonly gesamt: Ratio; |
| 307 | /** |
| 308 | * Hat der Deckel gegriffen, also haette die Geometrie die |
| 309 | * Saettigungsverkehrsstaerke nach der geschlossenen Formel angehoben? |
| 310 | */ |
| 311 | readonly gedeckelt: boolean; |
| 312 | /** Wurde fR unterdrueckt, weil der pauschale Abbiegerfaktor ihn schon deckt? */ |
| 313 | readonly radiusVomAbbiegerfaktorGedeckt: boolean; |
| 314 | } |
| 315 | |
| 316 | /** |
| 317 | * Die geometrischen Anpassungsfaktoren des Zeitbedarfswerts nach BASt V 400. |
| 318 | * |
| 319 | * WOERTLICH (V 400): fb = -0,375 * b + 2,125 fuer b < 3,00 m; |
| 320 | * fR = -0,015 * R + 1,3 fuer R < 20 m; fs = 0,03 * s + 1,0. Daraus |
| 321 | * f1 = max(fb, fR, fs) und f2 = min(1, fs); der Zeitbedarfswert ist |
| 322 | * tB = fSV * f1 * f2 * 1,8 und qS = 3600 / tB. |
| 323 | * |
| 324 | * WARUM ES SIE BIS 5.29.0 NICHT GAB: Das Programm erfasste weder Breite noch |
| 325 | * Radius noch Neigung und rechnete deshalb IMMER mit Standardbedingungen. Die |
| 326 | * eigene Unterlage nannte das die unsichere Richtung, und sie hatte recht: An |
| 327 | * einer 2,50 m breiten Zufahrt faellt die Saettigungsverkehrsstaerke ohne fb um |
| 328 | * 18,8 % zu gross aus (2000 statt 1684 Kfz/h), an einer 4-%-Steigung um 12 % |
| 329 | * (2000 statt 1786). Nicht die Aenderung ist gefaehrlich, sondern ihr |
| 330 | * Ausbleiben. |
| 331 | * |
| 332 | * ZWEI ABWEICHUNGEN VON DER GESCHLOSSENEN FORMEL, beide in die sichere |
| 333 | * Richtung, beide entschieden und nicht uebersehen: |
| 334 | * |
| 335 | * ERSTENS DER DECKEL BEI 1. Nach der Formel senkt ein Gefaelle den |
| 336 | * Zeitbedarfswert: f1 = 1, f2 = fs < 1, und bei s = -4 % ergaebe das |
| 337 | * 2000/0,88 = 2273 Kfz/h, also 13,6 % ueber dem Grundwert. Dieses Programm |
| 338 | * folgt dem NICHT. Die Geometrie darf die Saettigungsverkehrsstaerke nur |
| 339 | * senken, nie heben - `gesamt` ist deshalb `max(1; f1 * f2)`. |
| 340 | * |
| 341 | * Der Grund ist nicht Vorsicht um ihrer selbst willen, sondern die Summe der |
| 342 | * bekannten Auslassungen dieser Bemessung: Sie rechnet mit EINEM pauschalen |
| 343 | * Abbiegerfaktor statt der differenzierten HBS-Faktoren, kennt keine |
| 344 | * Ersatz-Saettigungsverkehrsstaerke des Mischfahrstreifens und keine |
| 345 | * 15-min-Ganglinie je Strom. Alle drei Luecken wirken nach oben. Eine vierte |
| 346 | * Zahl obendrauf, die die Kapazitaet ueber den Grundwert hebt, waere die |
| 347 | * gefaehrliche Richtung - dasselbe Muster wie die Raeumgeschwindigkeit der |
| 348 | * Fussgaenger, die dieses Programm hart beim Regelwert deckelt, obwohl das |
| 349 | * Regelwerk mehr zulaesst (SAFETY_FLOORS.maxClearingSpeedFuss). Wo der Deckel |
| 350 | * greift, sagt es der Pruefbericht; verschwiegen wird die Abweichung nicht. |
| 351 | * |
| 352 | * ZWEITENS KEIN fR AN ABBIEGENDEN STROEMEN. `quellen.ts` fuehrt den pauschalen |
| 353 | * Abbiegerfaktor 0,90 ausdruecklich als Ersatz fuer die differenzierten |
| 354 | * HBS-Faktoren nach Abbiegeradius, Gegenverkehr und Fussgaengerquerung. Wer fR |
| 355 | * daneben multipliziert, mindert den Radius zweimal ab: Ein Rechtsabbieger mit |
| 356 | * 10 m Radius fiele auf 0,90/1,15 = 0,783 des Grundwerts, also auf |
| 357 | * 1565 Kfz/h. Der pauschale Faktor bleibt, fR entfaellt dort - und `gedeckt` |
| 358 | * sagt es, damit die Ansicht das Feld nicht als wirksam ausgibt. |
| 359 | * |
| 360 | * Fuer geradeaus fahrende Stroeme greift fR weiterhin: Ein enger Bogen im |
| 361 | * durchgehenden Fahrstreifen ist von keinem Abbiegerfaktor gedeckt. |
| 362 | */ |
| 363 | export function geometriefaktor( |
| 364 | geometrie: Fahrstreifengeometrie | undefined, |
| 365 | movement: Movement, |
| 366 | ): GeometriefaktorErgebnis { |
| 367 | const abbiegend = movement === 'links' || movement === 'rechts'; |
| 368 | const breite = zahlOderNull(geometrie?.breite); |
| 369 | const radius = zahlOderNull(geometrie?.radius); |
| 370 | const neigung = zahlOderNull(geometrie?.neigung); |
| 371 | |
| 372 | const fb = |
| 373 | breite !== null && breite < HBS_STANDARDBEDINGUNGEN.fahrstreifenbreiteAb |
| 374 | ? HBS_GEOMETRIE.breite.steigung * breite + HBS_GEOMETRIE.breite.achsenabschnitt |
| 375 | : 1; |
| 376 | const engerBogen = radius !== null && radius < HBS_STANDARDBEDINGUNGEN.kurvenradiusAb; |
| 377 | const fR = |
| 378 | engerBogen && !abbiegend |
| 379 | ? HBS_GEOMETRIE.radius.steigung * radius + HBS_GEOMETRIE.radius.achsenabschnitt |
| 380 | : 1; |
| 381 | const fs = neigung !== null ? HBS_GEOMETRIE.neigung.steigung * neigung + 1 : 1; |
| 382 | |
| 383 | const f1 = Math.max(fb, fR, fs); |
| 384 | const f2 = Math.min(1, fs); |
| 385 | const roh = f1 * f2; |
| 386 | |
| 387 | return { |
| 388 | fb: roundTo(fb, 6), |
| 389 | fR: roundTo(fR, 6), |
| 390 | fs: roundTo(fs, 6), |
| 391 | f1: roundTo(f1, 6), |
| 392 | f2: roundTo(f2, 6), |
| 393 | gesamt: roundTo(Math.max(1, roh), 6), |
| 394 | gedeckelt: roh < 1, |
| 395 | radiusVomAbbiegerfaktorGedeckt: abbiegend && engerBogen, |
| 396 | }; |
| 397 | } |
| 398 | |
| 399 | /** Ein erfasster Zahlenwert - oder null, wenn keiner brauchbar vorliegt. */ |
| 400 | function zahlOderNull(wert: number | undefined): number | null { |
| 401 | if (wert === undefined || !isFiniteNumber(wert)) return null; |
| 402 | return wert; |
| 403 | } |
| 404 | |
| 405 | /** |
| 406 | * Massgebender Saettigungsgrad y = q / qS eines Verkehrsstroms. |
| 407 | * Diese Groesse geht als Summe Y in die Umlaufzeitermittlung ein. |
| 408 | */ |
| 409 | export function criticalFlowRatio(demand: VehiclesPerHour, saturation: VehiclesPerHour): Ratio { |
| 410 | if (!isFiniteNumber(demand) || demand < 0) return 0; |
| 411 | if (!isFiniteNumber(saturation) || saturation <= 0) return Number.POSITIVE_INFINITY; |
| 412 | return demand / saturation; |
| 413 | } |
| 414 | |
| 415 | export interface CapacityOptions { |
| 416 | /** Bewertungsverfahren; bestimmt, ob mit tA = tF + 1 s (HBS) oder g = tF (HCM) gerechnet wird. */ |
| 417 | readonly verfahren?: Bewertungsverfahren; |
| 418 | /** |
| 419 | * Anzahl der Freigabezeitfenster im Umlauf. Nach HBS 2015 endet jedes |
| 420 | * Fenster mit einer Gelbzeit, in die der Abfluss noch 1 s hineinlaeuft; der |
| 421 | * Zuschlag faellt deshalb je Fenster an. Regelfall 1. |
| 422 | */ |
| 423 | readonly freigabefenster?: number; |
| 424 | /** |
| 425 | * Hoechster Auslastungsgrad aus den Projektvorgaben, ab dem gewarnt wird. |
| 426 | * Fehlt er, gilt der Regelwert - dann meldet die Notiz bei geaenderten |
| 427 | * Vorgaben eine andere Schwelle als der Pruefbericht. |
| 428 | */ |
| 429 | readonly maxDegreeOfSaturation?: Ratio; |
| 430 | } |
| 431 | |
| 432 | /** |
| 433 | * Abflusszeit tA aus der Freigabezeit. |
| 434 | * |
| 435 | * HBS 2015: tA = tF + 1 s je Freigabezeitfenster (Belege siehe HBS_ABFLUSSZUSCHLAG). |
| 436 | * Die Abflusszeit kann die Umlaufzeit nicht ueberschreiten. |
| 437 | * HCM: die Freigabezeit selbst (Anfahrverlust und Gelbnutzung heben sich im |
| 438 | * Regelfall auf). |
| 439 | */ |
| 440 | export function abflusszeit( |
| 441 | greenTime: Seconds, |
| 442 | cycleTime: Seconds, |
| 443 | options: CapacityOptions = {}, |
| 444 | ): Seconds { |
| 445 | const verfahren = options.verfahren ?? 'HBS'; |
| 446 | const green = isFiniteNumber(greenTime) && greenTime > 0 ? greenTime : 0; |
| 447 | if (verfahren !== 'HBS' || green === 0) return Math.min(green, cycleTime); |
| 448 | const fenster = |
| 449 | isFiniteNumber(options.freigabefenster) && options.freigabefenster > 0 |
| 450 | ? Math.floor(options.freigabefenster) |
| 451 | : 1; |
| 452 | return Math.min(cycleTime, green + fenster * HBS_ABFLUSSZUSCHLAG); |
| 453 | } |
| 454 | |
| 455 | /** |
| 456 | * Kapazitaet einer Signalgruppe: c = qS * tA / tU. |
| 457 | * |
| 458 | * KORREKTUR (Fassung 5.4.0, Befund B2): Das HBS 2015 rechnet die |
| 459 | * Kapazitaet mit der Abflusszeit tA = tF + 1 s, nicht mit der Freigabezeit. |
| 460 | * Nachgerechnet am LISA+-Ausdruck "HBS-Bewertung 2015" (Kornwestheim 2017): |
| 461 | * tF = 10 s, tA = 11 s, qS = 1992 Kfz/h, tU = 90 s -> C = 1992 * 11 / 90 = |
| 462 | * 243 Kfz/h, wie dort ausgewiesen. |
| 463 | * |
| 464 | * Der Altbestand meldete zusaetzlich pauschal "reserve: 15 % der Kapazitaet" |
| 465 | * und einen Auslastungsgrad von konstant 0, unabhaengig von der Verkehrsstaerke. |
| 466 | * Beides ist entfallen; der Auslastungsgrad wird jetzt tatsaechlich gerechnet, |
| 467 | * sofern eine Verkehrsstaerke vorliegt. |
| 468 | */ |
| 469 | export function computeCapacity( |
| 470 | greenTime: Seconds, |
| 471 | cycleTime: Seconds, |
| 472 | saturation: VehiclesPerHour, |
| 473 | demand?: VehiclesPerHour, |
| 474 | options: CapacityOptions = {}, |
| 475 | ): CapacityResult { |
| 476 | const notes: CalculationNote[] = []; |
| 477 | const verfahren = options.verfahren ?? 'HBS'; |
| 478 | |
| 479 | if (!isFiniteNumber(cycleTime) || cycleTime <= 0) { |
| 480 | notes.push({ |
| 481 | severity: 'fehler', |
| 482 | code: 'umlaufzeit-ungueltig', |
| 483 | message: |
| 484 | 'Die Umlaufzeit ist kein gültiger Wert größer als 0; die Kapazität ist nicht bestimmbar.', |
| 485 | }); |
| 486 | return { |
| 487 | verfahren, |
| 488 | capacity: 0, |
| 489 | abflusszeit: 0, |
| 490 | greenRatio: 0, |
| 491 | saturationFlow: saturation, |
| 492 | notes, |
| 493 | }; |
| 494 | } |
| 495 | |
| 496 | const green = isFiniteNumber(greenTime) && greenTime > 0 ? greenTime : 0; |
| 497 | if (green === 0) { |
| 498 | notes.push({ |
| 499 | severity: 'warnung', |
| 500 | code: 'freigabezeit-null', |
| 501 | message: 'Die Freigabezeit beträgt 0 s; die Signalgruppe hat keine Kapazität.', |
| 502 | }); |
| 503 | } |
| 504 | if (green > cycleTime) { |
| 505 | notes.push({ |
| 506 | severity: 'fehler', |
| 507 | code: 'freigabezeit-ueber-umlaufzeit', |
| 508 | message: |
| 509 | `Die Freigabezeit ${green} s ist länger als die Umlaufzeit ${cycleTime} s. ` + |
| 510 | 'Der Signalzeitenplan ist in sich widersprüchlich.', |
| 511 | }); |
| 512 | } |
| 513 | |
| 514 | const tA = abflusszeit(green, cycleTime, options); |
| 515 | const greenRatio = Math.min(1, tA / cycleTime); |
| 516 | const capacity = saturation * greenRatio; |
| 517 | |
| 518 | const result: CapacityResult = { |
| 519 | verfahren, |
| 520 | capacity: Math.round(capacity), |
| 521 | abflusszeit: roundTo(tA, 3), |
| 522 | greenRatio: roundTo(greenRatio, 4), |
| 523 | saturationFlow: saturation, |
| 524 | notes, |
| 525 | }; |
| 526 | |
| 527 | if (demand === undefined) return result; |
| 528 | |
| 529 | if (!isFiniteNumber(demand) || demand < 0) { |
| 530 | notes.push({ |
| 531 | severity: 'warnung', |
| 532 | code: 'verkehrsstaerke-ungueltig', |
| 533 | message: |
| 534 | 'Die angegebene Verkehrsstärke ist kein gültiger Wert; der Auslastungsgrad entfällt.', |
| 535 | }); |
| 536 | return result; |
| 537 | } |
| 538 | |
| 539 | // Der Auslastungsgrad wird aus der AUSGEWIESENEN (ganzzahligen) Kapazitaet |
| 540 | // gebildet, nicht aus dem ungerundeten Zwischenwert: Nach HBS 2015 haengt |
| 541 | // die Stufe F an q > C, und beide Zahlen stehen ganzzahlig im Ausdruck. Wer |
| 542 | // dort 244 Fz/h neben 243 Fz/h liest, muss auf dieselbe Aussage kommen wie |
| 543 | // das Programm. (So rechnet auch der LISA+-Ausdruck: x = q / C_gerundet.) |
| 544 | // Ohne Nachfrage und ohne Kapazitaet ist nichts ueberlastet (q > C gilt |
| 545 | // nicht fuer 0 > 0): Auslastungsgrad 0, keine Meldung. |
| 546 | const degree = |
| 547 | result.capacity > 0 ? demand / result.capacity : demand > 0 ? Number.POSITIVE_INFINITY : 0; |
| 548 | const hoechsterAuslastungsgrad = |
| 549 | options.maxDegreeOfSaturation ?? RILSA_DEFAULTS.capacity.maxDegreeOfSaturation; |
| 550 | if (degree > 1) { |
| 551 | notes.push({ |
| 552 | severity: 'fehler', |
| 553 | code: 'auslastung-ueber-100', |
| 554 | message: |
| 555 | `Der Auslastungsgrad beträgt ${formatRatio(degree)} und liegt über 1,0. ` + |
| 556 | 'Die Nachfrage übersteigt die Kapazität; es entsteht ein wachsender Rückstau.', |
| 557 | }); |
| 558 | } else if (degree > hoechsterAuslastungsgrad) { |
| 559 | notes.push({ |
| 560 | severity: 'warnung', |
| 561 | code: 'auslastung-kritisch', |
| 562 | message: |
| 563 | `Der Auslastungsgrad beträgt ${formatRatio(degree)} und liegt über dem Höchstwert ` + |
| 564 | `${formatRatio(hoechsterAuslastungsgrad)}. Der Verkehrsablauf ist instabil.`, |
| 565 | }); |
| 566 | } |
| 567 | |
| 568 | return { ...result, degreeOfSaturation: roundTo(degree, 4) }; |
| 569 | } |
| 570 | |
| 571 | function formatRatio(value: number): string { |
| 572 | if (!Number.isFinite(value)) return 'unendlich'; |
| 573 | return roundTo(value, 2).toFixed(2).replace('.', ','); |
| 574 | } |