lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests domain stauraum.test.ts
| 1 | import { describe, expect, it } from 'vitest'; |
| 2 | import { rueckstaubewertung, rueckstaulaenge } from '@/domain/rilsa/delay'; |
| 3 | import { STAURAUMBEDARF_JE_FAHRZEUG } from '@/domain/rilsa/constants'; |
| 4 | import { buildSignalPlan } from '@/domain/plan/signalPlan'; |
| 5 | import { validateProject } from '@/domain/validation'; |
| 6 | import { createStandardIntersectionProject } from '@/domain/model/factory'; |
| 7 | import { loadProject } from '@/domain/model/migrate'; |
| 8 | import type { Project } from '@/domain/model/project'; |
| 9 | |
| 10 | /** |
| 11 | * Fassung 5.31.0 - der Rueckstau als Laenge, gehalten gegen den vorhandenen |
| 12 | * Stauraum. |
| 13 | * |
| 14 | * WARUM: Die Leistungsfaehigkeitsrechnung fuehrte den Rueckstau seit jeher als |
| 15 | * Fahrzeugzahl NGE, und die Planunterlage druckte sie. Fuer sich genommen ist |
| 16 | * sie unbrauchbar - ob ein Aufstellstreifen ueberlaeuft, entscheidet eine |
| 17 | * Laenge. |
| 18 | * |
| 19 | * DIE WICHTIGSTE ZUSAGE DIESER DATEI ist nicht die Umrechnung, sondern die |
| 20 | * Ehrlichkeit ueber die verglichene Groesse: NGE ist ein MITTELWERT. Ein |
| 21 | * Stauraum, der gerade eben reicht, reicht in der Haelfte der Umlaeufe nicht, |
| 22 | * und das muss in jeder Ausgabe stehen. |
| 23 | */ |
| 24 | |
| 25 | const DATUM = new Date('2026-01-01T00:00:00Z'); |
| 26 | |
| 27 | describe('Rueckstau gegen Stauraum - die reine Rechnung', () => { |
| 28 | it('bildet die Länge aus NGE und den Fahrstreifen', () => { |
| 29 | // 6 Kfz * 7 m / 2 Fahrstreifen = 21,0 m |
| 30 | const bewertung = rueckstaubewertung(6, 2, 50); |
| 31 | expect(bewertung.fahrzeuge).toBe(6); |
| 32 | expect(bewertung.laenge).toBe(21); |
| 33 | expect(bewertung.laenge).toBe(rueckstaulaenge(6, 2)); |
| 34 | expect(bewertung.stauraum).toBe(50); |
| 35 | expect(bewertung.auslastung).toBeCloseTo(0.42, 4); |
| 36 | expect(bewertung.ueberlauf).toBe(false); |
| 37 | }); |
| 38 | |
| 39 | it('meldet den Überlauf, sobald die Länge den Stauraum erreicht', () => { |
| 40 | // Genau erreicht zaehlt schon: Der MITTLERE Rueckstau fuellt den Stauraum |
| 41 | // dann aus, und in der Haelfte der Umlaeufe steht mehr. |
| 42 | expect(rueckstaubewertung(10, 1, 70).ueberlauf).toBe(true); |
| 43 | expect(rueckstaubewertung(10, 1, 70.1).ueberlauf).toBe(false); |
| 44 | expect(rueckstaubewertung(10, 1, 60).ueberlauf).toBe(true); |
| 45 | }); |
| 46 | |
| 47 | it('vergleicht nicht, wo kein Stauraum erfasst ist', () => { |
| 48 | for (const stauraum of [null, 0, -5, Number.NaN]) { |
| 49 | const bewertung = rueckstaubewertung(10, 1, stauraum); |
| 50 | expect(bewertung.stauraum, String(stauraum)).toBeNull(); |
| 51 | expect(bewertung.auslastung, String(stauraum)).toBeNull(); |
| 52 | expect(bewertung.ueberlauf, String(stauraum)).toBe(false); |
| 53 | // Die Laenge steht trotzdem da - sie ist die Auskunft, die auch ohne |
| 54 | // Vergleich weiterhilft. |
| 55 | expect(bewertung.laenge).toBe(70); |
| 56 | } |
| 57 | }); |
| 58 | |
| 59 | it('kommt mit einem leeren Rückstau zurecht', () => { |
| 60 | const bewertung = rueckstaubewertung(0, 2, 40); |
| 61 | expect(bewertung.fahrzeuge).toBe(0); |
| 62 | expect(bewertung.laenge).toBe(0); |
| 63 | expect(bewertung.ueberlauf).toBe(false); |
| 64 | }); |
| 65 | }); |
| 66 | |
| 67 | describe('Rueckstau gegen Stauraum - im Plan', () => { |
| 68 | it('trägt die Bewertung an jeder bemessenen Signalgruppe', () => { |
| 69 | const plan = buildSignalPlan(mitStauraum(null)); |
| 70 | const gruppe = plan.groups.find((g) => g.delay !== null); |
| 71 | expect(gruppe?.rueckstau, 'die Bewertung steht am Plan').toBeDefined(); |
| 72 | expect(gruppe?.rueckstau?.laenge ?? -1).toBeGreaterThan(0); |
| 73 | expect(gruppe?.rueckstau?.stauraum, 'ohne Eintrag kein Stauraum').toBeNull(); |
| 74 | }); |
| 75 | |
| 76 | it('führt keine Bewertung ohne Rückstaugröße', () => { |
| 77 | /* |
| 78 | * NGE entsteht allein im HBS-Verfahren. Im HCM-Verfahren gibt es ihn |
| 79 | * nicht, und eine 0 stuende dort fuer ein Ergebnis, das es nicht gibt. |
| 80 | */ |
| 81 | const basis = mitStauraum(null); |
| 82 | const plan = buildSignalPlan({ |
| 83 | ...basis, |
| 84 | settings: { ...basis.settings, serviceLevelScale: 'HCM' }, |
| 85 | }); |
| 86 | for (const gruppe of plan.groups) expect(gruppe.rueckstau, gruppe.name).toBeNull(); |
| 87 | }); |
| 88 | |
| 89 | it('rechnet die Länge je Fahrstreifen, nicht je Signalgruppe', () => { |
| 90 | /* |
| 91 | * GEPRUEFT WIRD DIE UMRECHNUNG IM PLAN, nicht ein Vergleich zweier |
| 92 | * Plaene. Ein zweistreifiger Knotenpunkt hat die doppelte Kapazitaet und |
| 93 | * dadurch ohnehin ein kleineres NGE; ein "kuerzer als einstreifig" waere |
| 94 | * also auch dann erfuellt, wenn die Fahrstreifenzahl bei der Umrechnung |
| 95 | * uebergangen wuerde. Genau so stand dieser Fall zuerst da. |
| 96 | * |
| 97 | * Der Fehler, den Punkt A5 an der Simulation behoben hat, darf |
| 98 | * hier nicht wiederkehren. |
| 99 | */ |
| 100 | const basis = mitStauraum(null); |
| 101 | const projekt: Project = { |
| 102 | ...basis, |
| 103 | signalGroups: basis.signalGroups.map((g) => ({ ...g, lanes: 2 })), |
| 104 | }; |
| 105 | const plan = buildSignalPlan(projekt); |
| 106 | let geprueft = 0; |
| 107 | for (const gruppe of plan.groups) { |
| 108 | if (gruppe.rueckstau === null) continue; |
| 109 | geprueft += 1; |
| 110 | expect(gruppe.rueckstau.laenge, gruppe.name).toBe( |
| 111 | rueckstaulaenge(gruppe.rueckstau.fahrzeuge, 2), |
| 112 | ); |
| 113 | // Kein zweiter Vergleich gegen `rueckstaulaenge(f, 1) / 2`: Beide Wege |
| 114 | // runden auf eine Nachkommastelle, und 1,55 gegen 1,5 ist kein Befund, |
| 115 | // sondern die Rundung. Die Zeile darueber ist die genaue Zusage. |
| 116 | } |
| 117 | expect(geprueft, 'kein Strom mit Rückstau').toBeGreaterThan(0); |
| 118 | }); |
| 119 | }); |
| 120 | |
| 121 | describe('Rueckstau gegen Stauraum - die Meldung', () => { |
| 122 | it('warnt, wo der mittlere Rückstau den Stauraum erreicht', () => { |
| 123 | const bericht = validateProject(mitStauraum(5)); |
| 124 | const meldung = bericht.findings.find( |
| 125 | (f) => f.rule === 'leistungsfaehigkeit.rueckstau-ueber-stauraum', |
| 126 | ); |
| 127 | expect(meldung, 'die Warnung fehlt').toBeDefined(); |
| 128 | // Warnung und nicht Hinweis: Ein ueberlaufender Aufstellstreifen blockiert |
| 129 | // den durchgehenden Fahrstreifen. |
| 130 | expect(meldung?.severity).toBe('warnung'); |
| 131 | expect(meldung?.target?.kind).toBe('signalgruppe'); |
| 132 | }); |
| 133 | |
| 134 | it('sagt in der Meldung, dass ein Mittelwert verglichen wurde', () => { |
| 135 | /* |
| 136 | * DIE ZUSAGE, AN DER ALLES HAENGT. Ohne diesen Satz liest ein Bearbeiter |
| 137 | * "Stauraum reicht" aus einer Zahl, die in der Haelfte der Umlaeufe |
| 138 | * ueberschritten wird. |
| 139 | */ |
| 140 | const bericht = validateProject(mitStauraum(5)); |
| 141 | const meldung = bericht.findings.find( |
| 142 | (f) => f.rule === 'leistungsfaehigkeit.rueckstau-ueber-stauraum', |
| 143 | ); |
| 144 | expect(meldung?.message).toContain('MITTLERE'); |
| 145 | expect(meldung?.message).toContain('Hälfte der Umläufe'); |
| 146 | expect(meldung?.message).toContain('95-Prozent-Wert'); |
| 147 | }); |
| 148 | |
| 149 | it('schweigt ohne Stauraum und bei ausreichendem Stauraum', () => { |
| 150 | for (const stauraum of [null, 500]) { |
| 151 | const bericht = validateProject(mitStauraum(stauraum)); |
| 152 | expect( |
| 153 | bericht.findings.some((f) => f.rule === 'leistungsfaehigkeit.rueckstau-ueber-stauraum'), |
| 154 | `Stauraum ${String(stauraum)}`, |
| 155 | ).toBe(false); |
| 156 | } |
| 157 | }); |
| 158 | }); |
| 159 | |
| 160 | describe('Rueckstau gegen Stauraum - in der Datei', () => { |
| 161 | it('trägt den Stauraum über Speichern und Laden', () => { |
| 162 | const geladen = loadProject(JSON.parse(JSON.stringify(mitStauraum(45)))); |
| 163 | expect(geladen.project.schemaVersion).toBe(19); |
| 164 | expect(geladen.project.signalGroups[0]?.stauraumlaenge).toBe(45); |
| 165 | expect(geladen.issues.filter((i) => i.path.includes('stauraumlaenge'))).toEqual([]); |
| 166 | }); |
| 167 | |
| 168 | it('erfindet für eine Altdatei keinen Stauraum', () => { |
| 169 | const geladen = loadProject(JSON.parse(JSON.stringify(mitStauraum(null)))); |
| 170 | expect(geladen.project.signalGroups[0]?.stauraumlaenge).toBeUndefined(); |
| 171 | }); |
| 172 | |
| 173 | it('meldet einen unlesbaren Eintrag mit der richtigen Folge', () => { |
| 174 | /* |
| 175 | * Die Folge ist hier eine ANDERE als bei den Geometriefeldern: Der |
| 176 | * Stauraum geht in keine Rechnung ein, es entfaellt allein der Vergleich. |
| 177 | * Ein gemeinsamer Satz haette an einer der beiden Stellen gelogen. |
| 178 | */ |
| 179 | const roh = JSON.parse(JSON.stringify(mitStauraum(45))) as Record<string, unknown>; |
| 180 | const gruppen = roh['signalGroups'] as Record<string, unknown>[]; |
| 181 | (gruppen[0] as Record<string, unknown>)['stauraumlaenge'] = 'kurz'; |
| 182 | const geladen = loadProject(roh); |
| 183 | expect(geladen.project.signalGroups[0]?.stauraumlaenge).toBeUndefined(); |
| 184 | const meldung = geladen.issues.find((i) => i.path.endsWith('.stauraumlaenge')); |
| 185 | expect(meldung?.message).toContain('mit keiner Länge verglichen'); |
| 186 | expect(meldung?.message).not.toContain('Sättigungsverkehrsstärke'); |
| 187 | }); |
| 188 | }); |
| 189 | |
| 190 | /** |
| 191 | * Standardknotenpunkt mit hoher Auslastung - so entsteht ueberhaupt ein |
| 192 | * nennenswerter Rueckstau. Der Stauraum steht an jeder Kfz-Gruppe. |
| 193 | */ |
| 194 | function mitStauraum(stauraum: number | null): Project { |
| 195 | const roh = createStandardIntersectionProject('Stauraum', DATUM); |
| 196 | return { |
| 197 | ...roh, |
| 198 | signalGroups: roh.signalGroups.map((gruppe) => |
| 199 | gruppe.mode === 'kfz' && stauraum !== null ? { ...gruppe, stauraumlaenge: stauraum } : gruppe, |
| 200 | ), |
| 201 | demands: roh.signalGroups |
| 202 | .filter((g) => g.mode === 'kfz') |
| 203 | .map((g) => ({ signalGroupId: g.id, volume: 700, heavyVehicleShare: 0.05 })), |
| 204 | }; |
| 205 | } |
| 206 | |
| 207 | /** Der Kennwert steht dort, wo ihn beide Rechnungen finden. */ |
| 208 | describe('Rueckstau gegen Stauraum - eine Umrechnung, nicht zwei', () => { |
| 209 | it('nimmt für Simulation und Bemessung dieselbe Länge je Fahrzeug', () => { |
| 210 | expect(STAURAUMBEDARF_JE_FAHRZEUG).toBe(7); |
| 211 | expect(rueckstaubewertung(3, 1, null).laenge).toBe(3 * STAURAUMBEDARF_JE_FAHRZEUG); |
| 212 | }); |
| 213 | }); |