import { describe, expect, it } from 'vitest'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { createStandardIntersectionProject } from '@/domain/model/factory'; import { TrafficSimulation } from '@/domain/simulation/simulation'; import type { Project } from '@/domain/model/project'; /** * Das Abflussfenster der Simulation muss dieselbe Abflusszeit tA tragen, mit * der die Leistungsfaehigkeit desselben Plans rechnet (Befund 26, behoben in * Fassung 5.9.0). * * Was der Anwender sonst erlebt: Der Signalzeitenplan weist eine Kapazitaet * C = qS * tA / tU aus und meldet keine Ueberlastung, waehrend die Simulation * daneben einen unbegrenzt wachsenden Rueckstau und eine nicht konvergierende * mittlere Wartezeit zeigt. Die Simulation liess je Freigabefenster nur * tF minus Anfahrverlustzeit abfliessen - bei HBS 3 s je Fenster weniger als * die Bemessung - und verwarf am Freigabezeitende zusaetzlich den * aufgelaufenen Bruchteil eines Fahrzeugs. */ const FESTES_DATUM = new Date('2026-01-01T00:00:00Z'); /** Knotenpunkt mit fester Umlaufzeit 90 s und dauerhaft gefuellter Warteschlange. */ function ueberlasteterKnotenpunkt(verfahren: 'HBS' | 'HCM' = 'HBS'): Project { const basis = createStandardIntersectionProject('Abflussfenster', FESTES_DATUM); return { ...basis, settings: { ...basis.settings, serviceLevelScale: verfahren }, program: { ...basis.program, method: 'manuell', manualCycleTime: 90 }, demands: basis.demands.map((d) => ({ ...d, volume: 1500 })), }; } describe('Abflussfenster der Simulation', () => { it.each(['HBS', 'HCM'] as const)( 'laesst je Umlauf so viele Fahrzeuge abfliessen, wie die Kapazitaet des Plans hergibt (%s)', (verfahren) => { const project = ueberlasteterKnotenpunkt(verfahren); const plan = buildSignalPlan(project); expect(plan.cycleTime).toBe(90); const sim = new TrafficSimulation(plan, project, { timeStep: 0.5 }); // Vier Umlaeufe einlaufen lassen, danach genau 20 Umlaeufe messen. for (let i = 0; i < 6; i += 1) sim.advance(60); const vorher = new Map(sim.snapshot().groups.map((g) => [g.groupId, g.departed])); for (let i = 0; i < 30; i += 1) sim.advance(60); for (const group of sim.snapshot().groups) { const planned = plan.groups.find((p) => p.groupId === group.groupId); const kapazitaet = planned?.capacity; if (!kapazitaet) continue; // Die Warteschlange darf in keinem Umlauf leergelaufen sein, sonst // misst der Vergleich den Zufluss statt des Abflussfensters. expect(group.queue).toBeGreaterThan(0); const abgeflossen = group.departed - (vorher.get(group.groupId) ?? 0); const erwartet = (kapazitaet.saturationFlow / 3600) * kapazitaet.abflusszeit * 20; // Vor der Behebung flossen bei HBS 340 statt 383 Fahrzeuge ab. expect( Math.abs(abgeflossen - erwartet), `${group.name}: ${abgeflossen} statt ${erwartet.toFixed(1)}`, ).toBeLessThanOrEqual(1.5); } }, ); it('haelt die Fahrzeugbilanz auch bei Ueberlastung geschlossen', () => { const project = ueberlasteterKnotenpunkt(); const plan = buildSignalPlan(project); const sim = new TrafficSimulation(plan, project, { timeStep: 0.5 }); for (let i = 0; i < 30; i += 1) sim.advance(60); for (const group of sim.snapshot().groups) { expect(group.arrived).toBe(group.departed + group.queue); expect(group.queue).toBeGreaterThanOrEqual(0); } }); });