import { describe, expect, it } from 'vitest'; import { computeCapacity } from '@/domain/rilsa/capacity'; import { computeDelay, sperrzeitenAusFenstern } from '@/domain/rilsa/delay'; /* * Befund 26, behoben in Fassung 5.10.0. * * Gl. 2-8 des HBS 2015 ist die geschlossene Form fuer EIN Freigabezeitfenster * je Umlauf. Erhaelt eine Signalgruppe zwei getrennte Fenster, bildete * capacity.ts die Abflusszeit richtig als Summe (tA = tF + n * 1 s), und * computeDelay setzte genau diese Summe als ein einziges Fenster in Gl. 2-8 * ein. Der gleichmaessige Anteil ist aber Summe(r_j^2) / (2 * tU * (1 - x*fA)) * ueber die Sperrzeiten r_j des Umlaufs; die Einfensterform setzt dafuer * (Summe r_j)^2 an und faellt bei jeder Aufteilung zu gross aus - bei zwei * gleich langen Sperrzeiten genau um das Doppelte. * * Nachgerechnet (tU 90 s, zwei Fenster zu je 15 s bei 0 s und 45 s, * qS 2000 Fz/h, q 300 Fz/h): tA = 32 s, C = 711 Fz/h, x = 0,4219, * fA = 0,3556; Sperrzeiten je 30 s, wirksam je 29 s. * Einfensterform: 90 * 0,64444^2 / 1,69995 = 21,99 s * Summe(r_j^2): (29^2 + 29^2) / 90 / 1,69995 = 10,99 s * Mit tW,R = 2,2 s druckte das Programm tW = 24,2 s (Stufe B) statt 13,2 s * (Stufe A). * * Dieselbe Verallgemeinerung rechnet computeFussRadDelay fuer Fussgaenger und * Radverkehr seit Befund B4 bereits (Grenzfall x * fA -> 0). */ const ZWEI_FENSTER = [ { start: 0, duration: 15 }, { start: 45, duration: 15 }, ] as const; describe('Grundwartezeit bei mehreren Freigabezeitfenstern (Befund 26)', () => { it('wertet Gl. 2-8 ueber die Sperrzeiten des Umlaufs aus, nicht ueber ihre Summe', () => { const sperrzeiten = sperrzeitenAusFenstern(ZWEI_FENSTER, 90); expect(sperrzeiten).toEqual([30, 30]); const kapazitaet = computeCapacity(30, 90, 2000, 300, { verfahren: 'HBS', freigabefenster: 2, }); expect(kapazitaet.abflusszeit).toBe(32); expect(kapazitaet.capacity).toBe(711); const gemeinsam = { cycleTime: 90, greenTime: 30, capacity: kapazitaet.capacity, demand: 300, verfahren: 'HBS' as const, mode: 'kfz' as const, abflusszeit: kapazitaet.abflusszeit, }; const zweiFenster = computeDelay({ ...gemeinsam, sperrzeiten }); expect(zweiFenster.rechenweg.abflusszeit).toBe(32); expect(zweiFenster.rechenweg.abflussanteil).toBeCloseTo(0.3556, 4); expect(zweiFenster.degreeOfSaturation).toBeCloseTo(0.4219, 4); expect(zweiFenster.uniformDelay).toBe(11); expect(zweiFenster.averageDelay).toBe(13.2); expect(zweiFenster.serviceLevel?.grade).toBe('A'); // Ohne Sperrzeiten bleibt es bei der Einfensterform - das war der Fehler: // dieselbe Signalgruppe, doppelte Grundwartezeit, eine Stufe schlechter. const einFenster = computeDelay(gemeinsam); expect(einFenster.uniformDelay).toBe(22); expect(einFenster.averageDelay).toBe(24.2); expect(einFenster.serviceLevel?.grade).toBe('B'); }); it('rechnet ungleich verteilte Fenster ueber ihre einzelnen Sperrzeiten', () => { // Fenster bei 0 s und 30 s: Sperrzeiten 15 s und 45 s, wirksam 14 s und // 44 s. (14^2 + 44^2) / 90 / 1,69995 = 13,94 s - zwischen der gleichmaessig // aufgeteilten Lage (10,99 s) und der Einfensterform (21,99 s). const sperrzeiten = sperrzeitenAusFenstern( [ { start: 0, duration: 15 }, { start: 30, duration: 15 }, ], 90, ); expect(sperrzeiten).toEqual([15, 45]); const ungleich = computeDelay({ cycleTime: 90, greenTime: 30, capacity: 711, demand: 300, verfahren: 'HBS', mode: 'kfz', abflusszeit: 32, sperrzeiten, }); expect(ungleich.uniformDelay).toBe(13.9); expect(ungleich.averageDelay).toBe(16.1); }); it('laesst den Einfensterfall unveraendert - die Sperrzeit deckt den ganzen Umlauf ab', () => { // K5 des LISA+-Ausdrucks (tF 10 s, tA 11 s, C 243 Fz/h, q 167 Fz/h): ein // Fenster, Sperrzeit 80 s, wirksam 79 s. 79^2 / 90 ist identisch mit // 90 * (1 - 11/90)^2 - die Verallgemeinerung darf hier nichts verschieben. const gemeinsam = { cycleTime: 90, greenTime: 10, capacity: 243, demand: 167, verfahren: 'HBS' as const, mode: 'kfz' as const, abflusszeit: 11, }; const mit = computeDelay({ ...gemeinsam, sperrzeiten: sperrzeitenAusFenstern([{ start: 20, duration: 10 }], 90), }); const ohne = computeDelay(gemeinsam); expect(mit.uniformDelay).toBe(ohne.uniformDelay); expect(mit.averageDelay).toBe(ohne.averageDelay); expect(ohne.uniformDelay).toBe(37.9); }); it('ueberschaetzt nie: Summe(r_j^2) bleibt unter (Summe r_j)^2', () => { for (const anzahl of [2, 3, 4, 5]) { const dauer = 60 / anzahl / 2; const fenster = Array.from({ length: anzahl }, (_, i) => ({ start: (i * 120) / anzahl, duration: dauer, })); const sperrzeiten = sperrzeitenAusFenstern(fenster, 120); expect(sperrzeiten).toHaveLength(anzahl); const gemeinsam = { cycleTime: 120, greenTime: anzahl * dauer, capacity: 800, demand: 400, verfahren: 'HBS' as const, mode: 'kfz' as const, abflusszeit: anzahl * dauer + anzahl, }; const geteilt = computeDelay({ ...gemeinsam, sperrzeiten }); const einFenster = computeDelay(gemeinsam); expect(geteilt.uniformDelay).toBeLessThan(einFenster.uniformDelay); // Bei gleich langen Sperrzeiten ist der Faktor genau die Fensterzahl. expect(geteilt.uniformDelay * anzahl).toBeCloseTo(einFenster.uniformDelay, 0); } }); it('verallgemeinert auch den gleichmaessigen Anteil d1 des HCM-Verfahrens', () => { // d1 des HCM ist dieselbe Webster-Form; der Fehler war dort derselbe. const sperrzeiten = sperrzeitenAusFenstern(ZWEI_FENSTER, 90); const gemeinsam = { cycleTime: 90, greenTime: 30, capacity: 667, demand: 300, verfahren: 'HCM' as const, mode: 'kfz' as const, abflusszeit: 30, }; const zweiFenster = computeDelay({ ...gemeinsam, sperrzeiten }); const einFenster = computeDelay(gemeinsam); expect(zweiFenster.uniformDelay * 2).toBeCloseTo(einFenster.uniformDelay, 0); }); it('faellt auf die Einfensterform zurueck, wenn die Sperrzeiten nicht zur Abflusszeit passen', () => { const gemeinsam = { cycleTime: 90, greenTime: 30, capacity: 711, demand: 300, verfahren: 'HBS' as const, mode: 'kfz' as const, abflusszeit: 32, }; // Dauerfreigabe: keine Sperrzeit im Umlauf. expect(computeDelay({ ...gemeinsam, sperrzeiten: [] }).uniformDelay).toBe( computeDelay(gemeinsam).uniformDelay, ); // Sperrzeiten, deren Summe kleiner ist als tU - tA: kein Zeitzuschlag zu // verteilen, also nicht auswertbar. expect(computeDelay({ ...gemeinsam, sperrzeiten: [10, 10] }).uniformDelay).toBe( computeDelay(gemeinsam).uniformDelay, ); }); });