lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests domain schwerverkehrsaufteilung.test.ts
| 1 | import { describe, expect, it } from 'vitest'; |
| 2 | import { |
| 3 | aufteilungAusLastzuganteil, |
| 4 | istLastzuganteil, |
| 5 | lastzuganteilSpanne, |
| 6 | schwerverkehrsfaktor, |
| 7 | } from '@/domain/rilsa/capacity'; |
| 8 | import { |
| 9 | HBS_LASTZUGANTEIL_BEI_UNBEKANNTER_AUFTEILUNG, |
| 10 | HBS_PKW_GLEICHWERTE, |
| 11 | RILSA_DEFAULTS, |
| 12 | } from '@/domain/rilsa/constants'; |
| 13 | import { QUELLEN } from '@/domain/rilsa/quellen'; |
| 14 | import { settingFields } from '@/domain/rilsa/settings'; |
| 15 | import { buildSignalPlan } from '@/domain/plan/signalPlan'; |
| 16 | import { SCHWERVERKEHR_AUFTEILUNG_MELDESCHWELLE } from '@/domain/validation/rules'; |
| 17 | import { validateProject } from '@/domain/validation/engine'; |
| 18 | import { loadProject } from '@/domain/model/migrate'; |
| 19 | import { createStandardIntersectionProject } from '@/domain/model/factory'; |
| 20 | import { CURRENT_SCHEMA_VERSION, type Project, type TrafficDemand } from '@/domain/model/project'; |
| 21 | import { setDemand, setLastzugAnteil } from '@/app/actions'; |
| 22 | |
| 23 | /** |
| 24 | * Fassung 5.6.0 - die Aufteilung des Schwerverkehrs nach HBS 2015 Gl. 2-5. |
| 25 | * |
| 26 | * WORUM ES GEHT. Bis Fassung 5.5.0 kannte das Datenmodell nur einen |
| 27 | * Gesamt-Schwerverkehrsanteil. Das Programm rechnete deshalb jeden Strom nach |
| 28 | * Gl. 2-6 mit dem Pauschalwert 1,90 je Schwerfahrzeug - auch einen aus lauter |
| 29 | * Lastzuegen, der nach Gl. 2-5 mit 2,50 zu rechnen ist. Bei 20 % Schwerverkehr |
| 30 | * aus lauter Lastzuegen fiel die Saettigungsverkehrsstaerke damit 10,2 % zu |
| 31 | * gross aus, bei reinem Lastzugverkehr 31,6 %. Der Posten war im Programm |
| 32 | * ehrlich ausgewiesen - aber ausgewiesen ist nicht gerechnet. |
| 33 | * |
| 34 | * DIE BELEGE (BASt-Bericht V 400, 2025, S. 10), woertlich: |
| 35 | * "fSV = (qLV + 1,75 * qLkw+Bus + 2,5 * qLkwK) / qKfz (2-5)" |
| 36 | * "fSV = (qLV + 1,9 * qSV) / qKfz (2-6)" |
| 37 | * und zur Wahl zwischen beiden: "in Abhaengigkeit von der Datenverfuegbarkeit |
| 38 | * zur Aufteilung des Schwerverkehrs". |
| 39 | * |
| 40 | * DER KONSISTENZBELEG, an dem die Aufteilung haengt - V 400 zu Bild 2-1 d): |
| 41 | * "Bei einem Anteil der Lkw mit Anhaenger und Sattel-Kfz (Fahrzeugklasse LkwK) |
| 42 | * am Schwerverkehr von 20 % ergibt sich der Funktionsverlauf der Gleichung |
| 43 | * (2-6)." Nachgerechnet: 1,75 * 0,8 + 2,50 * 0,2 = 1,90 exakt. Der Pauschalwert |
| 44 | * IST Gl. 2-5 mit 20 % Lastzuganteil; die Erfassung ist eine Verfeinerung |
| 45 | * derselben Formel und kein anderes Verfahren. Genau deshalb darf eine Altdatei |
| 46 | * ihr Ergebnis nicht aendern. |
| 47 | */ |
| 48 | |
| 49 | const DATUM = new Date('2026-01-01T00:00:00Z'); |
| 50 | |
| 51 | /** Grundwert je Fahrstreifen unter Standardbedingungen: 2000 Kfz/h. */ |
| 52 | const QS_GRUND = RILSA_DEFAULTS.capacity.saturationFlow; |
| 53 | |
| 54 | /** |
| 55 | * Der Knotenpunkt der Vorlage mit EINER erfassten Verkehrsstaerke an K1 |
| 56 | * (geradeaus, ein Fahrstreifen - also Grundwert ohne Abbiegerfaktor). |
| 57 | */ |
| 58 | function knotenMitStrom(demand: Omit<TrafficDemand, 'signalGroupId'>): { |
| 59 | project: Project; |
| 60 | groupId: string; |
| 61 | } { |
| 62 | const roh = createStandardIntersectionProject('Schwerverkehr', DATUM); |
| 63 | const k1 = roh.signalGroups.find((g) => g.name === 'K1'); |
| 64 | if (!k1) throw new Error('K1 fehlt in der Vorlage'); |
| 65 | return { |
| 66 | project: { ...roh, demands: [{ signalGroupId: k1.id, ...demand }] }, |
| 67 | groupId: k1.id, |
| 68 | }; |
| 69 | } |
| 70 | |
| 71 | /** Die gerechnete Saettigungsverkehrsstaerke dieser Gruppe im Signalzeitenplan. */ |
| 72 | function qSimPlan(project: Project, groupId: string): number { |
| 73 | const gruppe = buildSignalPlan(project).groups.find((g) => g.groupId === groupId); |
| 74 | if (!gruppe?.capacity) throw new Error('keine Kapazitaet gerechnet'); |
| 75 | return gruppe.capacity.saturationFlow; |
| 76 | } |
| 77 | |
| 78 | // --- Die beiden Gleichungen ------------------------------------------------- |
| 79 | |
| 80 | describe('Gl. 2-5 und Gl. 2-6 sind dieselbe Gleichung', () => { |
| 81 | it('faellt bei 20 % Lastzuganteil exakt mit dem Pauschalwert zusammen', () => { |
| 82 | /* |
| 83 | * Die Kernaussage dieser Tests, und zugleich der schaerfste |
| 84 | * MUTATIONSNACHWEIS: Jede Verwechslung der Bezugsgroesse in |
| 85 | * aufteilungAusLastzuganteil (Lastzuganteil an qKfz statt am |
| 86 | * Schwerverkehr), jeder vertauschte Pkw-Gleichwert und jedes verrutschte |
| 87 | * Vorzeichen laesst die beiden Wege auseinanderfallen. Geprueft wird auf |
| 88 | * zwoelf Stellen, nicht "ungefaehr gleich" - 1,75 * 0,8 + 2,50 * 0,2 = 1,90 |
| 89 | * ist exakt und nicht gerundet. |
| 90 | */ |
| 91 | for (const pSV of [0, 0.03, 0.1, 0.2, 0.35, 0.5, 0.8, 1]) { |
| 92 | const nachAufteilung = schwerverkehrsfaktor( |
| 93 | pSV, |
| 94 | aufteilungAusLastzuganteil(pSV, HBS_LASTZUGANTEIL_BEI_UNBEKANNTER_AUFTEILUNG), |
| 95 | ); |
| 96 | expect(nachAufteilung, `pSV ${pSV}`).toBeCloseTo(schwerverkehrsfaktor(pSV), 12); |
| 97 | } |
| 98 | }); |
| 99 | |
| 100 | it('haelt den belegten Pauschalanteil und seine Nachrechnung fest', () => { |
| 101 | expect(HBS_LASTZUGANTEIL_BEI_UNBEKANNTER_AUFTEILUNG).toBe(0.2); |
| 102 | const nachgerechnet = |
| 103 | HBS_PKW_GLEICHWERTE.lkwUndBus * (1 - HBS_LASTZUGANTEIL_BEI_UNBEKANNTER_AUFTEILUNG) + |
| 104 | HBS_PKW_GLEICHWERTE.lkwMitAnhaenger * HBS_LASTZUGANTEIL_BEI_UNBEKANNTER_AUFTEILUNG; |
| 105 | expect(nachgerechnet).toBeCloseTo(HBS_PKW_GLEICHWERTE.schwerverkehrPauschal, 12); |
| 106 | }); |
| 107 | |
| 108 | it('rechnet den Lastzuganteil auf die Bezugsgroesse von Gl. 2-5 um', () => { |
| 109 | // Anteile AN qKfz, wie Gl. 2-5 sie braucht: pSV * anteil und |
| 110 | // pSV * (1 - anteil). Ihre Summe ist genau pSV - mehr Schwerverkehr als |
| 111 | // Schwerverkehr entsteht nicht. |
| 112 | const aufteilung = aufteilungAusLastzuganteil(0.3, 0.4); |
| 113 | expect(aufteilung).toEqual({ lkwUndBus: 0.3 * 0.6, lkwMitAnhaenger: 0.3 * 0.4 }); |
| 114 | expect(aufteilung!.lkwUndBus + aufteilung!.lkwMitAnhaenger).toBeCloseTo(0.3, 12); |
| 115 | }); |
| 116 | |
| 117 | it('fuehrt die Aufteilung nicht als Kennwert in der Vorgabenverwaltung', () => { |
| 118 | // Befund C20 gilt weiter: Waehlbar ist die Datenlage je Strom, nicht der |
| 119 | // Pkw-Gleichwert des Regelwerks. |
| 120 | const schluessel = settingFields().map((f) => String(f.key)); |
| 121 | for (const verboten of ['lastzugAnteil', 'pkwGleichwertLastzug', 'schwerverkehrPauschal']) { |
| 122 | expect(schluessel).not.toContain(verboten); |
| 123 | } |
| 124 | }); |
| 125 | }); |
| 126 | |
| 127 | // --- Der Lastzugstrom ------------------------------------------------------- |
| 128 | |
| 129 | describe('Ein Strom aus lauter Lastzuegen', () => { |
| 130 | it('kommt auf fSV 1,30 und 1538 statt 1695 Kfz/h', () => { |
| 131 | // Das Beispiel, das seit der Fassung 5.5.0 in Unterlage, Hilfe und |
| 132 | // Fundstellenverzeichnis steht - jetzt rechenbar statt nur benannt. |
| 133 | const nurLastzuege = schwerverkehrsfaktor(0.2, aufteilungAusLastzuganteil(0.2, 1)); |
| 134 | expect(nurLastzuege).toBeCloseTo(1.3, 12); |
| 135 | expect(schwerverkehrsfaktor(0.2)).toBeCloseTo(1.18, 12); |
| 136 | |
| 137 | const { project, groupId } = knotenMitStrom({ |
| 138 | volume: 600, |
| 139 | heavyVehicleShare: 0.2, |
| 140 | lastzugAnteil: 1, |
| 141 | }); |
| 142 | expect(Math.round(qSimPlan(project, groupId))).toBe(1538); |
| 143 | |
| 144 | // MUTATIONSNACHWEIS: Wer die Aufteilung im Signalzeitenplan nicht |
| 145 | // durchreicht, landet wieder bei 1695 - der Zahl bis Fassung 5.5.0. |
| 146 | const { project: ohne, groupId: id2 } = knotenMitStrom({ |
| 147 | volume: 600, |
| 148 | heavyVehicleShare: 0.2, |
| 149 | }); |
| 150 | expect(Math.round(qSimPlan(ohne, id2))).toBe(1695); |
| 151 | }); |
| 152 | |
| 153 | it('beziffert beide Fehlerrichtungen des Pauschalwerts fuer einen Anteil', () => { |
| 154 | // Bezugsgroesse ist die WAHRE Saettigungsverkehrsstaerke: qS = qS0 / fSV, |
| 155 | // also (qS_Programm - qS_wahr) / qS_wahr = fSV_wahr / fSV_pauschal - 1. |
| 156 | const spanne = lastzuganteilSpanne(0.2); |
| 157 | expect(spanne.pauschal).toBeCloseTo(1.18, 12); |
| 158 | expect(spanne.nurLastzuege).toBeCloseTo(1.3, 12); |
| 159 | expect(spanne.ohneLastzuege).toBeCloseTo(1.15, 12); |
| 160 | expect(spanne.hoechstensZuGross * 100).toBeCloseTo(10.169, 3); |
| 161 | expect(spanne.hoechstensZuKlein * 100).toBeCloseTo(2.542, 3); |
| 162 | |
| 163 | // Bei reinem Lastzugverkehr die 31,6 %, die im Fundstellenverzeichnis |
| 164 | // stehen - und die konservative Gegenrichtung dazu. |
| 165 | const voll = lastzuganteilSpanne(1); |
| 166 | expect(voll.hoechstensZuGross * 100).toBeCloseTo(31.579, 3); |
| 167 | expect(voll.hoechstensZuKlein * 100).toBeCloseTo(7.895, 3); |
| 168 | |
| 169 | // Ohne Schwerverkehr gibt es keine Spanne - und keinen Grund zu melden. |
| 170 | const leer = lastzuganteilSpanne(0); |
| 171 | expect(leer.hoechstensZuGross).toBe(0); |
| 172 | expect(leer.hoechstensZuKlein).toBe(0); |
| 173 | }); |
| 174 | }); |
| 175 | |
| 176 | // --- Randfaelle ------------------------------------------------------------- |
| 177 | |
| 178 | describe('Randfaelle des Lastzuganteils', () => { |
| 179 | it('nimmt 0 und 1 als Angaben an', () => { |
| 180 | expect(istLastzuganteil(0)).toBe(true); |
| 181 | expect(istLastzuganteil(1)).toBe(true); |
| 182 | // 0 heisst "gezaehlt, kein Lastzug dabei" - Gl. 2-5 mit 1,75 je |
| 183 | // Schwerfahrzeug, also GUENSTIGER als der Pauschalwert. |
| 184 | expect(schwerverkehrsfaktor(0.2, aufteilungAusLastzuganteil(0.2, 0))).toBeCloseTo(1.15, 12); |
| 185 | expect(schwerverkehrsfaktor(0.2, aufteilungAusLastzuganteil(0.2, 1))).toBeCloseTo(1.3, 12); |
| 186 | }); |
| 187 | |
| 188 | it('verwirft alles andere und faellt auf Gl. 2-6 zurueck', () => { |
| 189 | /* |
| 190 | * MUTATIONSNACHWEIS gegen ein Begrenzen auf 0..1: Wer -0,3 auf 0 kappt, |
| 191 | * macht aus einer Fehleingabe die GUENSTIGSTE aller Aufteilungen (fSV |
| 192 | * 1,150 statt 1,180) und weist den Strom danach als "Aufteilung erfasst" |
| 193 | * aus. Beide Erwartungen unten schlagen dann fehl. |
| 194 | */ |
| 195 | for (const unbrauchbar of [-0.3, 1.5, Number.NaN, Number.POSITIVE_INFINITY, undefined]) { |
| 196 | expect(istLastzuganteil(unbrauchbar), `${unbrauchbar}`).toBe(false); |
| 197 | expect(aufteilungAusLastzuganteil(0.2, unbrauchbar), `${unbrauchbar}`).toBeUndefined(); |
| 198 | expect( |
| 199 | schwerverkehrsfaktor(0.2, aufteilungAusLastzuganteil(0.2, unbrauchbar)), |
| 200 | `${unbrauchbar}`, |
| 201 | ).toBeCloseTo(schwerverkehrsfaktor(0.2), 12); |
| 202 | } |
| 203 | }); |
| 204 | |
| 205 | it('meldet einen eingetragenen, aber unbrauchbaren Anteil im Signalzeitenplan', () => { |
| 206 | // Ueber die Datei ist der Fall nicht erreichbar (das Schema verwirft ihn |
| 207 | // beim Lesen), ueber Oberflaeche und Fremdwerkzeuge schon. Ohne diese |
| 208 | // Notiz waere es eine stille Ersetzung: eingetragene Aufteilung, |
| 209 | // gerechnet ohne sie. |
| 210 | const { project, groupId } = knotenMitStrom({ |
| 211 | volume: 600, |
| 212 | heavyVehicleShare: 0.2, |
| 213 | lastzugAnteil: 1.5, |
| 214 | }); |
| 215 | const plan = buildSignalPlan(project); |
| 216 | const notiz = plan.notes.find((n) => n.code === 'lastzuganteil-ausserhalb'); |
| 217 | expect(notiz?.severity).toBe('warnung'); |
| 218 | expect(notiz?.signalGroupId).toBe(groupId); |
| 219 | expect(notiz?.message).toContain('150 %'); |
| 220 | expect(notiz?.message).toContain('1,90'); |
| 221 | // Gerechnet wird der Pauschalwert - nicht etwa mit 1,5 als Anteil. |
| 222 | expect(Math.round(qSimPlan(project, groupId))).toBe(1695); |
| 223 | |
| 224 | // Und im Pruefbericht steht sie mit Titel, Fundstelle und Rat. |
| 225 | const bericht = validateProject(project); |
| 226 | const befund = bericht.findings.find((f) => f.title.includes('Lastzuganteil außerhalb')); |
| 227 | expect(befund?.severity).toBe('warnung'); |
| 228 | expect(befund?.category).toBe('leistungsfaehigkeit'); |
| 229 | expect(befund?.reference).toContain('HBS 2015'); |
| 230 | expect(befund?.suggestion).toContain('entfernen'); |
| 231 | }); |
| 232 | }); |
| 233 | |
| 234 | // --- Schema und Bestandsdaten ---------------------------------------------- |
| 235 | |
| 236 | describe('Schemastand 13 und Bestandsdaten', () => { |
| 237 | it('fuehrt den Lastzuganteil ueber Speichern und Laden', () => { |
| 238 | expect(CURRENT_SCHEMA_VERSION).toBe(19); |
| 239 | |
| 240 | const { project, groupId } = knotenMitStrom({ |
| 241 | volume: 600, |
| 242 | heavyVehicleShare: 0.25, |
| 243 | lastzugAnteil: 0.6, |
| 244 | }); |
| 245 | const geladen = loadProject(JSON.parse(JSON.stringify(project))); |
| 246 | expect(geladen.issues.filter((i) => i.path.includes('lastzugAnteil'))).toEqual([]); |
| 247 | expect(geladen.project.schemaVersion).toBe(19); |
| 248 | expect(geladen.project.demands[0]?.lastzugAnteil).toBe(0.6); |
| 249 | // Und die Rechnung ueberlebt die Rundreise. |
| 250 | expect(qSimPlan(geladen.project, groupId)).toBeCloseTo(qSimPlan(project, groupId), 9); |
| 251 | }); |
| 252 | |
| 253 | it('laesst eine Altdatei ohne das Feld unveraendert rechnen', () => { |
| 254 | /* |
| 255 | * Die Zusage dabei: Schema 12 kannte den Lastzuganteil nicht, also |
| 256 | * darf keine Zahl einer alten Datei sich bewegen. 2000/1,180 = 1694,92 - |
| 257 | * derselbe Wert wie in Fassung 5.5.0. |
| 258 | * |
| 259 | * MUTATIONSNACHWEIS: Wuerde die Migration den belegten Pauschalanteil von |
| 260 | * 20 % vorbelegen, waere die ZAHL gleich (das ist der Konsistenzbeleg), |
| 261 | * aber `lastzugAnteil` stuende in der Datei - und der Strom gaelte als |
| 262 | * gezaehlt. Genau das prueft die zweite Erwartung. |
| 263 | */ |
| 264 | const { project, groupId } = knotenMitStrom({ volume: 600, heavyVehicleShare: 0.2 }); |
| 265 | const alt = { ...JSON.parse(JSON.stringify(project)), schemaVersion: 12 }; |
| 266 | const geladen = loadProject(alt); |
| 267 | expect(geladen.project.demands[0]?.lastzugAnteil).toBeUndefined(); |
| 268 | expect(qSimPlan(geladen.project, groupId)).toBeCloseTo(QS_GRUND / 1.18, 9); |
| 269 | }); |
| 270 | |
| 271 | it('verwirft einen unlesbaren Eintrag beim Lesen - mit Meldung', () => { |
| 272 | const { project } = knotenMitStrom({ volume: 600, heavyVehicleShare: 0.2 }); |
| 273 | for (const eingetragen of ['viele', -0.2, 2]) { |
| 274 | const roh = JSON.parse(JSON.stringify(project)); |
| 275 | roh.demands[0].lastzugAnteil = eingetragen; |
| 276 | const geladen = loadProject(roh); |
| 277 | const meldung = geladen.issues.find((i) => i.path === 'demands[0].lastzugAnteil'); |
| 278 | expect(meldung?.severity, `${eingetragen}`).toBe('warnung'); |
| 279 | expect(meldung?.message, `${eingetragen}`).toContain('1,90'); |
| 280 | expect(geladen.project.demands[0]?.lastzugAnteil, `${eingetragen}`).toBeUndefined(); |
| 281 | } |
| 282 | }); |
| 283 | |
| 284 | it('schreibt kein Feld, wo keines eingetragen wurde', () => { |
| 285 | // Ein leeres Feld ist keine Angabe: Weder das Schema noch die Aktion |
| 286 | // duerfen aus "nichts eingetragen" eine 0 machen - das waere ein |
| 287 | // Schwerverkehr ohne Lastzuege und damit die unsichere Richtung. |
| 288 | const { project, groupId } = knotenMitStrom({ volume: 600, heavyVehicleShare: 0.2 }); |
| 289 | const mitAnteil = setLastzugAnteil(project, groupId, 0.5); |
| 290 | expect(mitAnteil.demands[0]?.lastzugAnteil).toBe(0.5); |
| 291 | const wiederOhne = setLastzugAnteil(mitAnteil, groupId, null); |
| 292 | expect('lastzugAnteil' in (wiederOhne.demands[0] ?? {})).toBe(false); |
| 293 | expect(JSON.stringify(wiederOhne.demands[0])).not.toContain('lastzugAnteil'); |
| 294 | // Die Verkehrsstaerke selbst bleibt dabei stehen. |
| 295 | expect(wiederOhne.demands[0]?.volume).toBe(600); |
| 296 | // Und die Zahl ist wieder die des Pauschalwerts. |
| 297 | expect(qSimPlan(wiederOhne, groupId)).toBeCloseTo(QS_GRUND / 1.18, 9); |
| 298 | }); |
| 299 | |
| 300 | it('ruehrt die uebrigen Angaben der Verkehrsstaerke nicht an', () => { |
| 301 | const { project, groupId } = knotenMitStrom({ volume: 600, heavyVehicleShare: 0.2 }); |
| 302 | const geaendert = setDemand(setLastzugAnteil(project, groupId, 0.4), groupId, { volume: 900 }); |
| 303 | expect(geaendert.demands[0]).toEqual({ |
| 304 | signalGroupId: groupId, |
| 305 | volume: 900, |
| 306 | heavyVehicleShare: 0.2, |
| 307 | lastzugAnteil: 0.4, |
| 308 | }); |
| 309 | }); |
| 310 | }); |
| 311 | |
| 312 | // --- Der Pruefbericht ------------------------------------------------------- |
| 313 | |
| 314 | describe('Der Pruefbericht zur fehlenden Aufteilung', () => { |
| 315 | const REGEL = 'leistungsfaehigkeit.schwerverkehr-ohne-aufteilung'; |
| 316 | |
| 317 | it('schweigt, wo die Aufteilung erfasst ist', () => { |
| 318 | // Zweig (a): Es gibt keine Annahme zu benennen - der Strom rechnet nach |
| 319 | // Gl. 2-5. Auch dann, wenn der eingetragene Anteil genau der |
| 320 | // Pauschalannahme entspricht: Gezaehlt ist gezaehlt. |
| 321 | for (const anteil of [0, 0.2, 1]) { |
| 322 | const { project } = knotenMitStrom({ |
| 323 | volume: 600, |
| 324 | heavyVehicleShare: 0.4, |
| 325 | lastzugAnteil: anteil, |
| 326 | }); |
| 327 | const bericht = validateProject(project); |
| 328 | expect( |
| 329 | bericht.findings.filter((f) => f.rule === REGEL), |
| 330 | `${anteil}`, |
| 331 | ).toEqual([]); |
| 332 | } |
| 333 | }); |
| 334 | |
| 335 | it('meldet oberhalb der Schwelle mit Annahme und Fehlerrichtung', () => { |
| 336 | // Zweig (b). 25 % Schwerverkehr: 2000/1,225 = 1633 Kfz/h gerechnet, |
| 337 | // 2000/1,375 = 1455 Kfz/h bei lauter Lastzuegen (12,2 % zu gross), |
| 338 | // 2000/1,1875 = 1684 Kfz/h ohne Lastzuege (3,1 % zu klein). |
| 339 | const { project, groupId } = knotenMitStrom({ volume: 600, heavyVehicleShare: 0.25 }); |
| 340 | const befund = validateProject(project).findings.find((f) => f.rule === REGEL); |
| 341 | expect(befund?.severity).toBe('hinweis'); |
| 342 | expect(befund?.category).toBe('leistungsfaehigkeit'); |
| 343 | expect(befund?.target).toEqual({ kind: 'signalgruppe', id: groupId, label: 'K1' }); |
| 344 | // Die Annahme steht da, mit ihrer Nachrechnung. |
| 345 | expect(befund?.message).toContain('20 %'); |
| 346 | expect(befund?.message).toContain('1,75 · 0,80 + 2,50 · 0,20 = 1,90'); |
| 347 | // Und die Zahlen sind die DIESES Stroms, nicht die des Handbuchbeispiels. |
| 348 | expect(befund?.message).toContain('1633 Kfz/h'); |
| 349 | expect(befund?.message).toContain('1455 Kfz/h'); |
| 350 | expect(befund?.message).toContain('12,2 % ZU GROSS'); |
| 351 | expect(befund?.message).toContain('1684 Kfz/h'); |
| 352 | expect(befund?.message).toContain('3,1 % zu klein'); |
| 353 | expect(befund?.message).not.toContain('10,2 %'); |
| 354 | expect(befund?.reference).toContain('2-5'); |
| 355 | expect(befund?.suggestion).toContain('Lastzüge'); |
| 356 | }); |
| 357 | |
| 358 | it('schweigt unterhalb der Schwelle', () => { |
| 359 | /* |
| 360 | * Die Schwelle ist ein Programmwert und begruendet: Bei 10 % |
| 361 | * Schwerverkehr betraegt die moegliche Ueberschaetzung 5,5 %, bei 5 % nur |
| 362 | * 2,9 % - darunter liegt sie unter der Genauigkeit einer Zaehlung. Die |
| 363 | * Vorlage mit ihren 5 % soll keinen Hinweis erzeugen. |
| 364 | */ |
| 365 | expect(SCHWERVERKEHR_AUFTEILUNG_MELDESCHWELLE).toBe(0.1); |
| 366 | expect(lastzuganteilSpanne(0.1).hoechstensZuGross * 100).toBeCloseTo(5.505, 3); |
| 367 | expect(lastzuganteilSpanne(0.05).hoechstensZuGross * 100).toBeCloseTo(2.871, 3); |
| 368 | // Die uebrigen Zahlen der Begruendung an der Meldeschwelle - sie stehen im |
| 369 | // Kommentar und muessen dort stimmen. |
| 370 | expect(lastzuganteilSpanne(0.25).hoechstensZuGross * 100).toBeCloseTo(12.245, 3); |
| 371 | expect(lastzuganteilSpanne(0.5).hoechstensZuGross * 100).toBeCloseTo(20.69, 2); |
| 372 | |
| 373 | for (const pSV of [0, 0.05, 0.1]) { |
| 374 | const { project } = knotenMitStrom({ volume: 600, heavyVehicleShare: pSV }); |
| 375 | expect( |
| 376 | validateProject(project).findings.filter((f) => f.rule === REGEL), |
| 377 | `${pSV}`, |
| 378 | ).toEqual([]); |
| 379 | } |
| 380 | // Knapp darueber kommt sie. |
| 381 | const { project } = knotenMitStrom({ volume: 600, heavyVehicleShare: 0.11 }); |
| 382 | expect(validateProject(project).findings.filter((f) => f.rule === REGEL)).toHaveLength(1); |
| 383 | }); |
| 384 | |
| 385 | it('nennt den Anteil, mit dem gerechnet wird - nicht den eingetragenen', () => { |
| 386 | /* |
| 387 | * BEFUND (Fassung 5.6.0): Der Meldungstext nahm `demand.heavyVehicleShare` |
| 388 | * roh, waehrend `lastzuganteilSpanne` intern auf 0 bis 1 begrenzt. Eine |
| 389 | * Datei mit 150 % Schwerverkehr (ueber das Schema nicht erreichbar, ueber |
| 390 | * Fremdwerkzeuge sehr wohl) ergab damit eine Ueberschrift "... (150 %)" |
| 391 | * ueber Kfz/h-Zahlen, die fuer 100 % gelten - und derselbe Bericht meldet |
| 392 | * den Wert daneben als begrenzt. |
| 393 | * |
| 394 | * Gerechnet wird mit 100 %: fSV = 1,90, also 2000/1,90 = 1053 Kfz/h; bei |
| 395 | * lauter Lastzuegen 2000/2,50 = 800 Kfz/h (31,6 % zu gross), ohne |
| 396 | * Lastzuege 2000/1,75 = 1143 Kfz/h (7,9 % zu klein). |
| 397 | */ |
| 398 | const { project } = knotenMitStrom({ volume: 600, heavyVehicleShare: 1.5 }); |
| 399 | const befund = validateProject(project).findings.find((f) => f.rule === REGEL); |
| 400 | expect(befund?.title).toContain('(100 %)'); |
| 401 | expect(befund?.title).not.toContain('150'); |
| 402 | expect(befund?.message).toContain('100 % Schwerverkehr'); |
| 403 | expect(befund?.message).not.toContain('150 %'); |
| 404 | expect(befund?.message).toContain('1053 Kfz/h'); |
| 405 | expect(befund?.message).toContain('800 Kfz/h'); |
| 406 | expect(befund?.message).toContain('31,6 % ZU GROSS'); |
| 407 | expect(befund?.message).toContain('1143 Kfz/h'); |
| 408 | expect(befund?.message).toContain('7,9 % zu klein'); |
| 409 | }); |
| 410 | |
| 411 | it('schweigt ohne Verkehrsstaerke', () => { |
| 412 | // Ohne sie gibt es keinen Auslastungsgrad, und die Unterlage druckt an |
| 413 | // dieser Stelle einen Strich statt eines Faktors. |
| 414 | const { project } = knotenMitStrom({ volume: 0, heavyVehicleShare: 0.4 }); |
| 415 | expect(validateProject(project).findings.filter((f) => f.rule === REGEL)).toEqual([]); |
| 416 | }); |
| 417 | |
| 418 | it('haelt sich an die Kennwerte des Plans, nicht an 2000 Kfz/h', () => { |
| 419 | /* |
| 420 | * MUTATIONSNACHWEIS gegen eine fest verdrahtete 2000 im Meldungstext: Mit |
| 421 | * einem geaenderten Grundwert muss die Meldung mitwandern, sonst nennt |
| 422 | * der Bericht eine Zahl, die im selben Bericht anders steht. |
| 423 | */ |
| 424 | const { project } = knotenMitStrom({ volume: 600, heavyVehicleShare: 0.25 }); |
| 425 | const mitVorgabe: Project = { |
| 426 | ...project, |
| 427 | settings: { ...project.settings, rilsa: { ...project.settings.rilsa, saturationFlow: 1800 } }, |
| 428 | }; |
| 429 | const befund = validateProject(mitVorgabe).findings.find((f) => f.rule === REGEL); |
| 430 | // 1800/1,225 = 1469 Kfz/h - die Prozentzahlen bleiben, sie haengen allein |
| 431 | // am Schwerverkehrsanteil. |
| 432 | expect(befund?.message).toContain('1469 Kfz/h'); |
| 433 | expect(befund?.message).toContain('12,2 % ZU GROSS'); |
| 434 | }); |
| 435 | }); |
| 436 | |
| 437 | // --- Fundstellenverzeichnis ------------------------------------------------- |
| 438 | |
| 439 | describe('Das Fundstellenverzeichnis nach P14', () => { |
| 440 | const hinweis = QUELLEN['hbs-kapazitaet'].hinweis ?? ''; |
| 441 | |
| 442 | it('sagt nicht mehr, das Programm rechne immer nach Gl. 2-6', () => { |
| 443 | // Der Satz war bis Fassung 5.5.0 richtig und ist es jetzt nicht mehr. |
| 444 | expect(hinweis).not.toContain('erfasst keine Aufteilung des Schwerverkehrs'); |
| 445 | expect(hinweis).not.toContain('IMMER mit der zweiten Gleichung'); |
| 446 | expect(hinweis).toContain('Datenverfügbarkeit'); |
| 447 | }); |
| 448 | |
| 449 | it('fuehrt den Konsistenzbeleg mit seiner Nachrechnung', () => { |
| 450 | expect(hinweis).toContain('Funktionsverlauf der Gleichung (2-6)'); |
| 451 | expect(hinweis).toContain('1,75 · 0,80 + 2,5 · 0,20 = 1,9'); |
| 452 | }); |
| 453 | |
| 454 | it('haelt den unsicheren Posten fest - jetzt begrenzt auf Ströme ohne Aufteilung', () => { |
| 455 | expect(hinweis).toContain('10,2 %'); |
| 456 | expect(hinweis).toContain('31,6 %'); |
| 457 | expect(hinweis).toContain('ohne erfasste Aufteilung'); |
| 458 | // Die Gegenrichtung war bisher nirgends beziffert. |
| 459 | expect(hinweis).toContain('2,5 % zu klein'); |
| 460 | // Und die Grenzen der Rechnung stehen weiter da. |
| 461 | expect(hinweis).toContain('Standardbedingungen'); |
| 462 | expect(hinweis).toContain('Mischfahrstreifen'); |
| 463 | expect(hinweis).toContain('HCM 2010'); |
| 464 | }); |
| 465 | }); |