lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ src domain simulation simulation.ts
| 1 | import type { Ratio, Seconds, VehiclesPerHour } from '../units'; |
| 2 | import { isFiniteNumber, roundTo } from '../units'; |
| 3 | import { isGreenAt } from '../plan/aspects'; |
| 4 | import { hatSaettigungsverkehrsstaerke, type SignalPlan } from '../plan/signalPlan'; |
| 5 | import { kfzServiceLevel, rueckstaulaenge } from '../rilsa/delay'; |
| 6 | import type { Bewertungsverfahren, ServiceLevel } from '../rilsa/types'; |
| 7 | import { demandFor, type Project } from '../model/project'; |
| 8 | |
| 9 | /** |
| 10 | * Verkehrsablaufsimulation. |
| 11 | * |
| 12 | * Grundsatz: Die Simulation trifft keine eigenen Annahmen ueber die |
| 13 | * Signalisierung. Wann eine Signalgruppe Freigabe zeigt, entnimmt sie |
| 14 | * ausschliesslich dem fertigen Signalzeitenplan ueber `isGreenAt`. Der |
| 15 | * Altbestand fuehrte hier eine zweite, vereinfachte Steuerung mit fest |
| 16 | * eingetragenen 30 s je Phase; die Simulation zeigte damit einen Ablauf, den |
| 17 | * die geplante Anlage gar nicht geschaltet haette. |
| 18 | * |
| 19 | * Modell: zeitdiskrete Warteschlange je Signalgruppe. |
| 20 | * |
| 21 | * Zufluss q / 3600 Fahrzeuge je Sekunde. Bei eingeschalteter |
| 22 | * Ankunftsschwankung wird der Bruchteil eines Fahrzeugs |
| 23 | * stochastisch gerundet; der Erwartungswert bleibt q / 3600. |
| 24 | * Abfluss nur bei Freigabe und nur so viele Fahrzeuge, wie in der |
| 25 | * Warteschlange stehen. Je Umlauf kann genau so viel abfliessen, |
| 26 | * wie die Kapazitaet desselben Plans hergibt: qS * tA / 3600 mit der |
| 27 | * Abflusszeit tA aus rilsa/capacity.ts. Zu Beginn jeder Freigabezeit |
| 28 | * unterbindet die Anfahrverlustzeit den Abfluss. |
| 29 | * |
| 30 | * Auch beim Abfluss fuehrt die Simulation keine eigene Formel: Wie viel je |
| 31 | * Umlauf durchkommt, sagt die Abflusszeit tA des Signalzeitenplans (HBS: |
| 32 | * tF + 1 s je Freigabezeitfenster, HCM: tF). Zuvor liess die Simulation nur |
| 33 | * waehrend tF minus Anfahrverlustzeit abfliessen - bei HBS 3 s je Fenster |
| 34 | * weniger als die Bemessung. Der Rueckstau wuchs dadurch schon bei |
| 35 | * Auslastungsgraden unter 1 unbegrenzt, und die Simulation widersprach der |
| 36 | * Leistungsfaehigkeitsrechnung derselben Anwendung. |
| 37 | * |
| 38 | * Die Fahrzeugbilanz ist geschlossen: angekommen = abgeflossen + Warteschlange. |
| 39 | * Diese Invariante wird im Test geprueft; im Altbestand konnten Fahrzeuge |
| 40 | * verschwinden, weil der Abfluss nicht gegen die Warteschlangenlaenge begrenzt |
| 41 | * war und negative Laengen entstanden. |
| 42 | */ |
| 43 | |
| 44 | /** |
| 45 | * Obergrenze, die ein einzelner Aufruf von `advance` nachrechnet - damit ein |
| 46 | * langer Zeitsprung (etwa nach dem Wechsel in einen Hintergrund-Tab) die |
| 47 | * Oberflaeche nicht einfriert. Wer mehr rechnen will, ruft mehrfach auf. |
| 48 | */ |
| 49 | export const MAX_NACHRECHENZEIT_JE_AUFRUF: Seconds = 60; |
| 50 | |
| 51 | export interface SimulationOptions { |
| 52 | /** Zeitschritt in Sekunden. */ |
| 53 | readonly timeStep?: Seconds; |
| 54 | /** Zufaellige Ankunftsschwankung zulassen (mit festem Startwert, reproduzierbar). */ |
| 55 | readonly stochastic?: boolean; |
| 56 | /** Startwert des Zufallsgenerators. */ |
| 57 | readonly seed?: number; |
| 58 | /** Anfahrverlustzeit zu Beginn jeder Freigabezeit. */ |
| 59 | readonly startUpLostTime?: Seconds; |
| 60 | } |
| 61 | |
| 62 | export interface GroupSimulationState { |
| 63 | readonly groupId: string; |
| 64 | readonly name: string; |
| 65 | readonly color: string; |
| 66 | /** Fahrzeuge in der Warteschlange. */ |
| 67 | readonly queue: number; |
| 68 | /** Rueckstaulaenge in Metern. */ |
| 69 | readonly queueLength: number; |
| 70 | readonly maxQueue: number; |
| 71 | readonly arrived: number; |
| 72 | readonly departed: number; |
| 73 | /** Mittlere Wartezeit ueber die bisher angekommenen Fahrzeuge. */ |
| 74 | readonly averageDelay: Seconds; |
| 75 | readonly serviceLevel: ServiceLevel; |
| 76 | readonly demand: VehiclesPerHour; |
| 77 | readonly saturationFlow: VehiclesPerHour; |
| 78 | readonly green: boolean; |
| 79 | } |
| 80 | |
| 81 | export interface SimulationSnapshot { |
| 82 | /** Verstrichene Simulationszeit. */ |
| 83 | readonly time: Seconds; |
| 84 | /** Position im Umlauf. */ |
| 85 | readonly cyclePosition: Seconds; |
| 86 | readonly completedCycles: number; |
| 87 | readonly groups: readonly GroupSimulationState[]; |
| 88 | /** Mittlere Wartezeit ueber alle Signalgruppen, gewichtet mit der Verkehrsstaerke. */ |
| 89 | readonly overallDelay: Seconds; |
| 90 | readonly overallServiceLevel: ServiceLevel; |
| 91 | /** Angekommene Fahrzeuge insgesamt. */ |
| 92 | readonly totalArrived: number; |
| 93 | readonly totalDeparted: number; |
| 94 | readonly totalQueue: number; |
| 95 | } |
| 96 | |
| 97 | interface GroupRuntime { |
| 98 | readonly groupId: string; |
| 99 | readonly name: string; |
| 100 | readonly color: string; |
| 101 | /** Verkehrsart fuer die Bewertungstafel (OePNV hat nach HBS 2015 eine eigene). */ |
| 102 | readonly mode: 'kfz' | 'oepnv'; |
| 103 | readonly demand: VehiclesPerHour; |
| 104 | /** |
| 105 | * Auslastungsgrad aus dem Signalzeitenplan (q / C). Nach HBS 2015 und |
| 106 | * HCM 2010 ist die Stufe F an q > C gebunden, nicht an eine Wartezeit; die |
| 107 | * Simulation bewertet deshalb mit demselben Auslastungsgrad wie der Plan. |
| 108 | */ |
| 109 | readonly degreeOfSaturation: Ratio | undefined; |
| 110 | readonly saturationFlow: VehiclesPerHour; |
| 111 | /** |
| 112 | * Fahrstreifen der Signalgruppe - massgebend fuer die Rueckstaulaenge. |
| 113 | * |
| 114 | * Sie steht am Projekt und nicht am Plan: `PlannedSignalGroup` fuehrt sie |
| 115 | * nicht, und sie dafuer aufzunehmen hiesse, die Planstruktur fuer eine reine |
| 116 | * Anzeigegroesse zu erweitern. Die Simulation liest ohnehin aus beiden. |
| 117 | */ |
| 118 | readonly lanes: number; |
| 119 | /** Anfahrverlustzeit zu Beginn jedes Freigabezeitfensters dieser Gruppe. */ |
| 120 | readonly startUpLostTime: Seconds; |
| 121 | /** |
| 122 | * Streckung des Abflussfensters: Die nach der Anfahrverlustzeit verbleibende |
| 123 | * Freigabezeit traegt die volle Abflusszeit tA des Plans. Abgeflossen wird |
| 124 | * nur bei Freigabe - der Anteil, der nach HBS in die Gelbzeit hineinlaeuft, |
| 125 | * hat dort keinen eigenen Platz. |
| 126 | */ |
| 127 | readonly abflussStreckung: number; |
| 128 | queue: number; |
| 129 | maxQueue: number; |
| 130 | arrived: number; |
| 131 | departed: number; |
| 132 | /** Aufsummierte Wartezeit in Fahrzeugsekunden. */ |
| 133 | delayIntegral: number; |
| 134 | /** Restanteil eines Fahrzeugs aus dem letzten Zeitschritt. */ |
| 135 | arrivalRemainder: number; |
| 136 | departureRemainder: number; |
| 137 | /** Wie lange zeigt die Gruppe schon Freigabe? Fuer die Anfahrverlustzeit. */ |
| 138 | greenElapsed: Seconds; |
| 139 | wasGreen: boolean; |
| 140 | } |
| 141 | |
| 142 | /** Reproduzierbarer Zufallsgenerator (mulberry32). */ |
| 143 | function createRandom(seed: number): () => number { |
| 144 | let state = seed >>> 0; |
| 145 | return () => { |
| 146 | state += 0x6d2b79f5; |
| 147 | let t = state; |
| 148 | t = Math.imul(t ^ (t >>> 15), t | 1); |
| 149 | t ^= t + Math.imul(t ^ (t >>> 7), t | 61); |
| 150 | return ((t ^ (t >>> 14)) >>> 0) / 4294967296; |
| 151 | }; |
| 152 | } |
| 153 | |
| 154 | export class TrafficSimulation { |
| 155 | private readonly runtimes: GroupRuntime[] = []; |
| 156 | private readonly timeStep: Seconds; |
| 157 | private readonly stochastic: boolean; |
| 158 | private readonly startUpLostTime: Seconds; |
| 159 | private random: () => number; |
| 160 | private readonly seed: number; |
| 161 | private time = 0; |
| 162 | /** Noch nicht verrechnete Zeit, die fuer keinen ganzen Schritt mehr reicht. */ |
| 163 | private pendingTime = 0; |
| 164 | /* |
| 165 | * Bewertungsverfahren des Projekts. Die Simulation rechnet keine |
| 166 | * HBS-Wartezeit; sie legt die Stufentafel des gewaehlten Verfahrens an |
| 167 | * den simulierten Mittelwert an - die Ansicht sagt das so. |
| 168 | * |
| 169 | * serviceLevelFor wurde ohne sie gerufen und fiel damit auf HBS zurueck, |
| 170 | * waehrend der Signalzeitenplan die eingestellte Skala korrekt weiterreicht. |
| 171 | * Dieselbe Wartezeit erhielt so in der Simulation eine andere Qualitaetsstufe |
| 172 | * als in der Leistungsfaehigkeit - bei 31,9 s Stufe B statt C, also eine |
| 173 | * Stufe zu gut, neben einer Tabelle, die "Bewertung nach HCM" ausweist. |
| 174 | * Sichtbar wurde das erst, seit HCM das Speichern ueberlebt. |
| 175 | */ |
| 176 | private readonly serviceLevelScale: Bewertungsverfahren; |
| 177 | |
| 178 | constructor( |
| 179 | private readonly plan: SignalPlan, |
| 180 | project: Project, |
| 181 | options: SimulationOptions = {}, |
| 182 | ) { |
| 183 | this.serviceLevelScale = project.settings.serviceLevelScale; |
| 184 | this.timeStep = clampStep(options.timeStep ?? 0.5); |
| 185 | this.stochastic = options.stochastic ?? false; |
| 186 | this.seed = options.seed ?? 20260101; |
| 187 | this.startUpLostTime = options.startUpLostTime ?? plan.defaults.startUpLostTime; |
| 188 | this.random = createRandom(this.seed); |
| 189 | |
| 190 | for (const group of plan.groups) { |
| 191 | // Fussgaenger und Radverkehr werden nicht als Warteschlange modelliert: |
| 192 | // Fuer sie gibt es keine Saettigungsverkehrsstaerke im Sinne des HBS, |
| 193 | // und bewertet werden sie ueber die maximale Wartezeit aus den |
| 194 | // Signalzeiten (Fassung 5.4.0, Befund B4). Bis dahin liefen |
| 195 | // Radgruppen hier mit einer Kfz-Saettigungsverkehrsstaerke. |
| 196 | if (!hatSaettigungsverkehrsstaerke(group.mode)) continue; |
| 197 | const demand = demandFor(project, group.groupId); |
| 198 | if (!demand || demand.volume <= 0) continue; |
| 199 | |
| 200 | // Abflussfenster je Umlauf: die Abflusszeit tA, mit der die |
| 201 | // Leistungsfaehigkeit desselben Plans rechnet. Fehlt eine |
| 202 | // Kapazitaetsrechnung (nur bei Umlaufzeit 0 - dann ist die Simulation |
| 203 | // ohnehin nicht lauffaehig), bleibt es bei der Freigabezeit. |
| 204 | const abflusszeit = group.capacity?.abflusszeit ?? group.totalGreen; |
| 205 | const fenster = Math.max(1, group.greens.length); |
| 206 | // Zehrt die Anfahrverlustzeit die Freigabe ganz auf, entfaellt sie - |
| 207 | // sonst bliebe kein Fenster, ueber das sich tA verteilen liesse. |
| 208 | const nachAnfahrverlust = group.totalGreen - fenster * this.startUpLostTime; |
| 209 | const startUpLostTime = nachAnfahrverlust > 0 ? this.startUpLostTime : 0; |
| 210 | const abflussfenster = nachAnfahrverlust > 0 ? nachAnfahrverlust : group.totalGreen; |
| 211 | |
| 212 | this.runtimes.push({ |
| 213 | groupId: group.groupId, |
| 214 | name: group.name, |
| 215 | color: group.color, |
| 216 | mode: group.mode === 'oepnv' ? 'oepnv' : 'kfz', |
| 217 | demand: demand.volume, |
| 218 | degreeOfSaturation: group.capacity?.degreeOfSaturation, |
| 219 | saturationFlow: group.capacity?.saturationFlow ?? plan.defaults.capacity.saturationFlow, |
| 220 | lanes: project.signalGroups.find((g) => g.id === group.groupId)?.lanes ?? 1, |
| 221 | startUpLostTime, |
| 222 | abflussStreckung: abflussfenster > 0 ? abflusszeit / abflussfenster : 0, |
| 223 | queue: 0, |
| 224 | maxQueue: 0, |
| 225 | arrived: 0, |
| 226 | departed: 0, |
| 227 | delayIntegral: 0, |
| 228 | arrivalRemainder: 0, |
| 229 | departureRemainder: 0, |
| 230 | greenElapsed: 0, |
| 231 | wasGreen: false, |
| 232 | }); |
| 233 | } |
| 234 | } |
| 235 | |
| 236 | /** Sind ueberhaupt Daten fuer eine Simulation vorhanden? */ |
| 237 | get runnable(): boolean { |
| 238 | return this.runtimes.length > 0 && this.plan.cycleTime > 0; |
| 239 | } |
| 240 | |
| 241 | /** |
| 242 | * Angefangene Zeit, die fuer keinen ganzen Schritt mehr gereicht hat - |
| 243 | * immer kleiner als `timeStep`. |
| 244 | * |
| 245 | * Gerechnet wird damit nichts; der Wert ist fuer die Anzeige da. Eine |
| 246 | * Laufmarke, die sich gleichmaessig bewegen soll, schlaegt ihn auf die |
| 247 | * Position im Umlauf auf: Die Rechnung bleibt in ganzen Schritten, nur das |
| 248 | * Bild fuellt die Luecke dazwischen. |
| 249 | */ |
| 250 | get anstehendeZeit(): Seconds { |
| 251 | return this.pendingTime; |
| 252 | } |
| 253 | |
| 254 | reset(): void { |
| 255 | this.time = 0; |
| 256 | this.pendingTime = 0; |
| 257 | this.random = createRandom(this.seed); |
| 258 | for (const runtime of this.runtimes) { |
| 259 | runtime.queue = 0; |
| 260 | runtime.maxQueue = 0; |
| 261 | runtime.arrived = 0; |
| 262 | runtime.departed = 0; |
| 263 | runtime.delayIntegral = 0; |
| 264 | runtime.arrivalRemainder = 0; |
| 265 | runtime.departureRemainder = 0; |
| 266 | runtime.greenElapsed = 0; |
| 267 | runtime.wasGreen = false; |
| 268 | } |
| 269 | } |
| 270 | |
| 271 | /** |
| 272 | * Rechnet `duration` Sekunden Simulationszeit in festen Schritten von |
| 273 | * `timeStep`. |
| 274 | * |
| 275 | * Was nicht mehr fuer einen ganzen Schritt reicht, bleibt im Restzeitkonto |
| 276 | * stehen und wird beim naechsten Aufruf mitgerechnet. Ohne dieses Konto |
| 277 | * bestimmte der Aufrufer die Schrittweite: Die Simulationsansicht uebergibt je |
| 278 | * Einzelbild die verstrichene Zeit mal Ablaufgeschwindigkeit, bei 60 Bildern |
| 279 | * je Sekunde und Echtzeit also 0,017 s statt der vorgegebenen 0,5 s. Dieselbe |
| 280 | * Anlage lieferte damit je nach Bildwiederholrate und Geschwindigkeit andere |
| 281 | * Rueckstaus und Wartezeiten - und Schrittweiten, die `clampStep` als Vorgabe |
| 282 | * gar nicht zugelassen haette. |
| 283 | */ |
| 284 | advance(duration: Seconds): void { |
| 285 | if (!this.runnable || !isFiniteNumber(duration) || duration <= 0) return; |
| 286 | this.pendingTime += Math.min(duration, MAX_NACHRECHENZEIT_JE_AUFRUF); |
| 287 | while (this.pendingTime >= this.timeStep - 1e-9) { |
| 288 | this.stepOnce(this.timeStep); |
| 289 | this.pendingTime -= this.timeStep; |
| 290 | } |
| 291 | if (this.pendingTime < 0) this.pendingTime = 0; |
| 292 | } |
| 293 | |
| 294 | private stepOnce(dt: Seconds): void { |
| 295 | const cycle = this.plan.cycleTime; |
| 296 | const position = ((this.time % cycle) + cycle) % cycle; |
| 297 | |
| 298 | for (const runtime of this.runtimes) { |
| 299 | const planned = this.plan.groups.find((g) => g.groupId === runtime.groupId); |
| 300 | const green = planned ? isGreenAt(planned, position, cycle) : false; |
| 301 | |
| 302 | if (green && !runtime.wasGreen) runtime.greenElapsed = 0; |
| 303 | runtime.wasGreen = green; |
| 304 | |
| 305 | // --- Zufluss --------------------------------------------------------- |
| 306 | const expectedArrivals = (runtime.demand / 3600) * dt + runtime.arrivalRemainder; |
| 307 | let arrivals = Math.floor(expectedArrivals); |
| 308 | const rest = expectedArrivals - arrivals; |
| 309 | if (this.stochastic) { |
| 310 | /* |
| 311 | * Stochastisches Runden: Mit der Wahrscheinlichkeit `rest` kommt ein |
| 312 | * Fahrzeug mehr an, sonst keines - und der Rest ist in BEIDEN Faellen |
| 313 | * verbraucht. Der Erwartungswert bleibt damit genau q / 3600 je Sekunde. |
| 314 | * |
| 315 | * Zuvor wurde der Rest nur beim Feuern der Ziehung auf 0 gesetzt, beim |
| 316 | * Nichtfeuern aber weitergetragen: Der Zufluss lief doppelt, je Schritt |
| 317 | * mit einem Ueberschuss von rest * (1 - rest) >= 0. Nach einer Stunde |
| 318 | * kamen bei 620 Fz/h rund 1800 Fahrzeuge an, und der Rueckstau wuchs |
| 319 | * auch bei deutlich unterausgelasteten Gruppen unbegrenzt. |
| 320 | */ |
| 321 | if (rest > 0 && this.random() < rest) arrivals += 1; |
| 322 | runtime.arrivalRemainder = 0; |
| 323 | } else { |
| 324 | runtime.arrivalRemainder = rest; |
| 325 | } |
| 326 | runtime.queue += arrivals; |
| 327 | runtime.arrived += arrivals; |
| 328 | |
| 329 | // --- Abfluss --------------------------------------------------------- |
| 330 | if (green) { |
| 331 | runtime.greenElapsed += dt; |
| 332 | // Waehrend der Anfahrverlustzeit fliesst noch nichts ab; die |
| 333 | // verbleibende Freigabezeit traegt dafuer die volle Abflusszeit tA des |
| 334 | // Plans (siehe `abflussStreckung`). |
| 335 | const offen = Math.max(0, Math.min(dt, runtime.greenElapsed - runtime.startUpLostTime)); |
| 336 | const effective = offen * runtime.abflussStreckung; |
| 337 | const capacity = (runtime.saturationFlow / 3600) * effective + runtime.departureRemainder; |
| 338 | const departures = Math.min(runtime.queue, Math.floor(capacity)); |
| 339 | runtime.departureRemainder = capacity - Math.floor(capacity); |
| 340 | runtime.queue -= departures; |
| 341 | runtime.departed += departures; |
| 342 | } else { |
| 343 | // Der aufgelaufene Bruchteil eines Fahrzeugs bleibt stehen. Wurde er am |
| 344 | // Freigabezeitende verworfen, fehlte je Umlauf bis zu ein Fahrzeug, und |
| 345 | // der Abfluss blieb hinter der Kapazitaet zurueck. |
| 346 | runtime.greenElapsed = 0; |
| 347 | } |
| 348 | |
| 349 | // Die Warteschlange kann durch die Begrenzung oben nie negativ werden; |
| 350 | // die Zusicherung haelt einen kuenftigen Fehler frueh auf. |
| 351 | if (runtime.queue < 0) runtime.queue = 0; |
| 352 | |
| 353 | runtime.maxQueue = Math.max(runtime.maxQueue, runtime.queue); |
| 354 | runtime.delayIntegral += runtime.queue * dt; |
| 355 | } |
| 356 | |
| 357 | this.time += dt; |
| 358 | } |
| 359 | |
| 360 | snapshot(): SimulationSnapshot { |
| 361 | const cycle = this.plan.cycleTime; |
| 362 | const position = cycle > 0 ? ((this.time % cycle) + cycle) % cycle : 0; |
| 363 | |
| 364 | const groups: GroupSimulationState[] = this.runtimes.map((runtime) => { |
| 365 | const planned = this.plan.groups.find((g) => g.groupId === runtime.groupId); |
| 366 | const roh = runtime.arrived > 0 ? runtime.delayIntegral / runtime.arrived : 0; |
| 367 | // Bewertet wird der Wert, der auch angezeigt wird - dieselbe Ueberlegung |
| 368 | // wie in rilsa/delay.ts. Eine Zeile "35,0 s | Stufe B" neben einer |
| 369 | // Stufengrenze bei 35 s ist fuer den Betrachter nicht aufloesbar. |
| 370 | const averageDelay = roundTo(roh, 1); |
| 371 | return { |
| 372 | groupId: runtime.groupId, |
| 373 | name: runtime.name, |
| 374 | color: runtime.color, |
| 375 | queue: runtime.queue, |
| 376 | queueLength: rueckstaulaenge(runtime.queue, runtime.lanes), |
| 377 | maxQueue: runtime.maxQueue, |
| 378 | arrived: runtime.arrived, |
| 379 | departed: runtime.departed, |
| 380 | averageDelay, |
| 381 | serviceLevel: kfzServiceLevel( |
| 382 | averageDelay, |
| 383 | this.serviceLevelScale, |
| 384 | runtime.degreeOfSaturation, |
| 385 | runtime.mode, |
| 386 | ), |
| 387 | demand: runtime.demand, |
| 388 | saturationFlow: runtime.saturationFlow, |
| 389 | green: planned ? isGreenAt(planned, position, cycle) : false, |
| 390 | }; |
| 391 | }); |
| 392 | |
| 393 | const totalArrived = groups.reduce((a, g) => a + g.arrived, 0); |
| 394 | const weightedDelay = groups.reduce((a, g) => a + g.averageDelay * g.arrived, 0); |
| 395 | const overallDelay = roundTo(totalArrived > 0 ? weightedDelay / totalArrived : 0, 1); |
| 396 | // Ist ein Strom ueberlastet, ist es der Knotenpunkt: Der hoechste |
| 397 | // Auslastungsgrad entscheidet ueber die Stufe F der Gesamtbewertung. |
| 398 | const hoechsteAuslastung = this.runtimes.reduce<Ratio | undefined>( |
| 399 | (max, r) => |
| 400 | r.degreeOfSaturation === undefined |
| 401 | ? max |
| 402 | : max === undefined |
| 403 | ? r.degreeOfSaturation |
| 404 | : Math.max(max, r.degreeOfSaturation), |
| 405 | undefined, |
| 406 | ); |
| 407 | |
| 408 | return { |
| 409 | time: roundTo(this.time, 2), |
| 410 | cyclePosition: roundTo(position, 2), |
| 411 | completedCycles: cycle > 0 ? Math.floor(this.time / cycle) : 0, |
| 412 | groups, |
| 413 | overallDelay, |
| 414 | overallServiceLevel: kfzServiceLevel( |
| 415 | overallDelay, |
| 416 | this.serviceLevelScale, |
| 417 | hoechsteAuslastung, |
| 418 | ), |
| 419 | totalArrived, |
| 420 | totalDeparted: groups.reduce((a, g) => a + g.departed, 0), |
| 421 | totalQueue: groups.reduce((a, g) => a + g.queue, 0), |
| 422 | }; |
| 423 | } |
| 424 | } |
| 425 | |
| 426 | function clampStep(value: Seconds): Seconds { |
| 427 | if (!isFiniteNumber(value) || value <= 0) return 0.5; |
| 428 | return Math.min(2, Math.max(0.05, value)); |
| 429 | } |