import { describe, expect, it } from 'vitest'; import { computeCapacity } from '@/domain/rilsa/capacity'; import { computeDelay, kfzServiceLevel, serviceLevelFor } from '@/domain/rilsa/delay'; import { TrafficSimulation } from '@/domain/simulation/simulation'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { createStandardIntersectionProject } from '@/domain/model/factory'; import { SERVICE_LEVEL_SCALES } from '@/domain/rilsa/constants'; import type { Project } from '@/domain/model/project'; /* * Die ausgegebene Wartezeit und die ausgegebene Qualitaetsstufe muessen * zusammenpassen. * * Ausgegeben wurde die gerundete Wartezeit, bewertet aber die ungerundete. Bei * 35,02 s stand in der Unterlage "35,0 s" neben der Stufe C, waehrend die * Stufentafel im selben Dokument bis einschliesslich 35 s die Stufe B ausweist * - eine Zeile, die sich selbst widerlegt, und zwar zu Ungunsten der Anlage. * Auflosen konnte den Widerspruch niemand, weil der ungerundete Wert nirgends * steht. * * Geprueft wird deshalb nicht ein einzelner Wert, sondern die Eigenschaft: Aus * den ausgegebenen Zahlen muss sich die ausgegebene Stufe ergeben. Seit * Fassung 5.4.0 (Befund B1) gehoert dazu neben der Wartezeit der * Auslastungsgrad: Nach HBS 2015 ist F genau dann erreicht, wenn q > C - und * beide Zahlen stehen ganzzahlig im Ausdruck. */ const STICHTAG = new Date('2026-01-01T00:00:00Z'); describe('Ausgegebene Wartezeit und ausgegebene Stufe passen zusammen', () => { it('gilt fuer die Leistungsfaehigkeit ueber den ganzen Wertebereich', () => { let geprueft = 0; for (const verfahren of ['HBS', 'HCM'] as const) { for (let demand = 100; demand <= 1000; demand += 0.5) { for (const green of [18, 27, 36]) { const kapazitaet = computeCapacity(green, 90, 2000, demand, { verfahren }); const ergebnis = computeDelay({ cycleTime: 90, greenTime: green, capacity: kapazitaet.capacity, demand, verfahren, mode: 'kfz', abflusszeit: kapazitaet.abflusszeit, }); // Aus den gedruckten Groessen: Wartezeit (0,1 s) und q > C (ganzzahlig). const erwartet = serviceLevelFor(ergebnis.averageDelay, verfahren, { degreeOfSaturation: demand > kapazitaet.capacity ? 1.5 : 0.5, }); expect( ergebnis.serviceLevel?.grade, `${verfahren}, q=${demand}, g=${green}: ${ergebnis.averageDelay} s, C=${kapazitaet.capacity}`, ).toBe(erwartet?.grade); geprueft += 1; } } } expect(geprueft).toBeGreaterThan(5000); }); /* * Der Fall, an dem sich alles entscheidet: Die ausgegebene Wartezeit liegt * GENAU auf einer Stufengrenze. Die Tafel weist dort die bessere Stufe aus. * Lag der ungerundete Wert knapp darueber, vergab der alte Stand die * schlechtere - und die Zeile widersprach der Tafel im selben Dokument. */ it('trifft die Tafel, wenn die ausgegebene Zeit auf einer Grenze liegt', () => { let treffer = 0; for (const verfahren of ['HBS', 'HCM'] as const) { const grenzen = SERVICE_LEVEL_SCALES[verfahren].kfz .filter((s) => Number.isFinite(s.maxDelay)) .map((s) => ({ wert: s.maxDelay, stufe: s.grade })); for (let demand = 100; demand <= 1000; demand += 0.1) { for (const green of [18, 27, 36]) { const kapazitaet = computeCapacity(green, 90, 2000, demand, { verfahren }); if (demand > kapazitaet.capacity) continue; // ueberlastet: F unabhaengig von der Zeit const ergebnis = computeDelay({ cycleTime: 90, greenTime: green, capacity: kapazitaet.capacity, demand, verfahren, mode: 'kfz', abflusszeit: kapazitaet.abflusszeit, }); const auf = grenzen.find((g) => g.wert === ergebnis.averageDelay); if (auf === undefined) continue; treffer += 1; expect( ergebnis.serviceLevel?.grade, `${verfahren}: ausgegeben ${ergebnis.averageDelay} s, Tafel sagt ${auf.stufe}`, ).toBe(auf.stufe); } } } // Ohne Treffer prueft der Test nichts - dann ist der Suchbereich falsch. expect(treffer, 'kein Wert lag genau auf einer Stufengrenze').toBeGreaterThan(0); }); it('trifft an einer Stufengrenze die Stufe, die die Tafel nennt', () => { // Die Tafel sagt: bis EINSCHLIESSLICH der Grenze gilt die bessere Stufe. const tafeln = [ { verfahren: 'HBS' as const, mode: 'kfz' as const, tafel: SERVICE_LEVEL_SCALES.HBS.kfz }, { verfahren: 'HBS' as const, mode: 'oepnv' as const, tafel: SERVICE_LEVEL_SCALES.HBS.oepnv }, { verfahren: 'HBS' as const, mode: 'fuss' as const, tafel: SERVICE_LEVEL_SCALES.HBS.fussRad }, { verfahren: 'HBS' as const, mode: 'rad' as const, tafel: SERVICE_LEVEL_SCALES.HBS.fussRad }, { verfahren: 'HCM' as const, mode: 'kfz' as const, tafel: SERVICE_LEVEL_SCALES.HCM.kfz }, ]; for (const { verfahren, mode, tafel } of tafeln) { for (const stufe of tafel) { if (!Number.isFinite(stufe.maxDelay)) continue; expect( serviceLevelFor(stufe.maxDelay, verfahren, { mode })?.grade, `${verfahren}/${mode} bei ${stufe.maxDelay} s`, ).toBe(stufe.grade); } } }); it('gilt ebenso in der Simulation', () => { for (const verfahren of ['HBS', 'HCM'] as const) { const roh = createStandardIntersectionProject('Stufen', STICHTAG); const projekt: Project = { ...roh, settings: { ...roh.settings, serviceLevelScale: verfahren }, }; const plan = buildSignalPlan(projekt); const sim = new TrafficSimulation(plan, projekt, { seed: 7 }); // Mehrfach abgreifen: Die Wartezeit waechst waehrend des Laufs und // durchschreitet dabei mehrere Stufengrenzen. for (let i = 0; i < 12; i += 1) { sim.advance(50); const stand = sim.snapshot(); for (const gruppe of stand.groups) { const geplant = plan.groups.find((g) => g.groupId === gruppe.groupId); const mode = geplant?.mode === 'oepnv' ? 'oepnv' : 'kfz'; expect( gruppe.serviceLevel.grade, `${verfahren} / ${gruppe.name}: ${gruppe.averageDelay} s`, ).toBe( kfzServiceLevel( gruppe.averageDelay, verfahren, geplant?.capacity?.degreeOfSaturation, mode, ).grade, ); } const hoechste = Math.max(...plan.groups.map((g) => g.capacity?.degreeOfSaturation ?? 0)); expect( stand.overallServiceLevel.grade, `${verfahren} gesamt: ${stand.overallDelay} s`, ).toBe(kfzServiceLevel(stand.overallDelay, verfahren, hoechste).grade); } } }); });