lsa-planer

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

/ src domain rilsa capacity.ts

24,7 KB Rohdatei
src/domain/rilsa/capacity.ts — 574 Zeilen
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 }