lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests domain umlaufzeitRasterung.test.ts
| 1 | import { describe, expect, it } from 'vitest'; |
| 2 | import { computeCycleTime } from '@/domain/rilsa/cycle'; |
| 3 | import { RILSA_DEFAULTS } from '@/domain/rilsa/constants'; |
| 4 | |
| 5 | /** |
| 6 | * Die Rasterung entscheidet nicht mehr ueber eine Beanstandung. |
| 7 | * |
| 8 | * Vor Fassung 5.13.0 prueft `computeCycleTime` den GERASTERTEN Wert |
| 9 | * gegen den Hoechstwert. Damit entschied die Rasterung - ein in der |
| 10 | * Vorgabenverwaltung von 1 bis 10 s frei einstellbarer Praxiswert ohne |
| 11 | * Regelwerksbezug - darueber, ob der Pruefbericht "Die Anlage ist mit dieser |
| 12 | * Phaseneinteilung nicht leistungsfaehig" fuehrt. Gemessen mit demselben |
| 13 | * rechnerischen Wert 119,48 s: bei den Rasterungen 1 bis 6, 8 und 10 s die |
| 14 | * Umlaufzeit 120 s ohne Beanstandung, bei 7 und 9 s dieselben 120 s mit dem |
| 15 | * Fehler 'umlaufzeit-ueber-hoechstwert'. |
| 16 | * |
| 17 | * Beurteilt wird jetzt der ERFORDERLICHE Umlauf vor der Rasterung: das Maximum |
| 18 | * aus dem rechnerischen Wert, der Mindestumlaufzeit aus Mindestfreigabe- und |
| 19 | * Zwischenzeiten und der kleinsten zulaessigen Umlaufzeit. |
| 20 | * |
| 21 | * GEGEN DEN ALTSTAND (gemessen: `cycle.ts` und `signalPlan.ts` auf den Stand |
| 22 | * vor Fassung 5.13.0 zurueckgespielt) sind fuenf der elf Faelle rot: die |
| 23 | * Messreihe ueber alle zehn Rasterungen, der Regelbereichshinweis, die |
| 24 | * Meldung mit dem erforderlichen statt dem gerasterten Wert und die beiden |
| 25 | * Faelle zum dichten Zahlenpaar. Die uebrigen sechs sind Gegenproben und |
| 26 | * vorher wie nachher gruen - darunter ein erforderlicher Umlauf ueber dem |
| 27 | * Hoechstwert, eine Mindestumlaufzeit ueber dem Hoechstwert und der |
| 28 | * uebersaettigte Knoten; sie halten fest, was sich NICHT aendern darf. |
| 29 | */ |
| 30 | |
| 31 | const HOECHSTWERT = RILSA_DEFAULTS.cycleTime.max; |
| 32 | const RASTERUNGEN = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] as const; |
| 33 | |
| 34 | /** |
| 35 | * Eingang, dessen rechnerischer Wert nach Webster genau `raw` betraegt: |
| 36 | * tU = (1,5 * L + 5) / (1 - Y) nach L aufgeloest, mit Y = 0,5. |
| 37 | */ |
| 38 | function eingang(raw: number, step: number, minimumCycle = 30) { |
| 39 | const Y = 0.5; |
| 40 | return { |
| 41 | lostTime: (raw * (1 - Y) - 5) / 1.5, |
| 42 | criticalFlowRatios: [Y], |
| 43 | minimumCycle, |
| 44 | step, |
| 45 | }; |
| 46 | } |
| 47 | |
| 48 | function codes(raw: number, step: number, minimumCycle?: number): string[] { |
| 49 | return computeCycleTime('webster', eingang(raw, step, minimumCycle)).notes.map((n) => n.code); |
| 50 | } |
| 51 | |
| 52 | describe('Die Rasterung der Umlaufzeit entscheidet nicht ueber die Beanstandung', () => { |
| 53 | it('haelt der rechnerische Wert 119,48 s den Hoechstwert bei jeder Rasterung ein', () => { |
| 54 | for (const step of RASTERUNGEN) { |
| 55 | const ergebnis = computeCycleTime('webster', eingang(119.48, step)); |
| 56 | expect(ergebnis.raw, `Rasterung ${step} s: rechnerischer Wert`).toBeCloseTo(119.48, 2); |
| 57 | expect(ergebnis.cycleTime, `Rasterung ${step} s: Umlaufzeit`).toBe(HOECHSTWERT); |
| 58 | expect(ergebnis.bounded, `Rasterung ${step} s: Zustand`).toBe('keine'); |
| 59 | expect( |
| 60 | ergebnis.notes.map((n) => n.code), |
| 61 | `Rasterung ${step} s: Meldungen`, |
| 62 | ).not.toContain('umlaufzeit-ueber-hoechstwert'); |
| 63 | } |
| 64 | }); |
| 65 | |
| 66 | it('steht bei jeder Rasterung derselbe Hinweis zum Regelbereich', () => { |
| 67 | // Bis zur Aenderung gab es zwei Ausgaenge: 'keine' mit dem |
| 68 | // Regelbereichshinweis (Rasterung 5 s) und 'maximum' mit dem Fehler |
| 69 | // (Rasterung 7 s) - beide bei derselben Umlaufzeit von 120 s. |
| 70 | for (const step of RASTERUNGEN) { |
| 71 | expect(codes(119.48, step), `Rasterung ${step} s`).toEqual([ |
| 72 | 'umlaufzeit-ausserhalb-regelbereich', |
| 73 | ]); |
| 74 | } |
| 75 | }); |
| 76 | |
| 77 | it('meldet ein erforderlicher Umlauf ueber dem Hoechstwert bei jeder Rasterung', () => { |
| 78 | // Gegenprobe: 121 s sind wirklich mehr als 120 s. Hier darf nichts |
| 79 | // verschwinden - und die Umlaufzeit bleibt der gekappte Hoechstwert. |
| 80 | for (const step of RASTERUNGEN) { |
| 81 | const ergebnis = computeCycleTime('webster', eingang(121, step)); |
| 82 | expect( |
| 83 | ergebnis.notes.map((n) => n.code), |
| 84 | `Rasterung ${step} s`, |
| 85 | ).toContain('umlaufzeit-ueber-hoechstwert'); |
| 86 | expect(ergebnis.bounded, `Rasterung ${step} s`).toBe('maximum'); |
| 87 | expect(ergebnis.cycleTime, `Rasterung ${step} s`).toBe(HOECHSTWERT); |
| 88 | } |
| 89 | }); |
| 90 | |
| 91 | it('bleibt die Meldung, wenn die Mindestumlaufzeit ueber dem Hoechstwert liegt', () => { |
| 92 | /* |
| 93 | * Die wichtigste Gegenprobe: `minimumCycle` folgt zwingend aus |
| 94 | * Mindestfreigabe- und Zwischenzeiten. Liegt SIE ueber dem Hoechstwert, |
| 95 | * ist die Anlage nicht schaltbar - der rechnerische Wert allein (hier |
| 96 | * 60 s) sagt davon nichts. Waere nur `raw` beurteilt worden, fiele diese |
| 97 | * Meldung fort. |
| 98 | */ |
| 99 | for (const step of RASTERUNGEN) { |
| 100 | const ergebnis = computeCycleTime('webster', eingang(60, step, 125)); |
| 101 | expect( |
| 102 | ergebnis.notes.map((n) => n.code), |
| 103 | `Rasterung ${step} s`, |
| 104 | ).toContain('umlaufzeit-ueber-hoechstwert'); |
| 105 | expect(ergebnis.bounded, `Rasterung ${step} s`).toBe('maximum'); |
| 106 | } |
| 107 | }); |
| 108 | |
| 109 | it('bleibt eine Mindestumlaufzeit dicht unter dem Hoechstwert unbeanstandet', () => { |
| 110 | // 119 s sind schaltbar. Bei Rasterung 7 s trifft die Aufrundung sie genau, |
| 111 | // bei 5 s hebt sie auf 120 s - beides ohne Beanstandung. |
| 112 | expect(codes(60, 7, 119)).toEqual(['mindestumlauf-massgebend']); |
| 113 | expect(codes(60, 5, 119)).toEqual(['mindestumlauf-massgebend']); |
| 114 | expect(computeCycleTime('webster', eingang(60, 7, 119)).cycleTime).toBe(119); |
| 115 | expect(computeCycleTime('webster', eingang(60, 5, 119)).cycleTime).toBe(120); |
| 116 | }); |
| 117 | |
| 118 | it('nennt die Meldung den erforderlichen und nicht den gerasterten Wert', () => { |
| 119 | /* |
| 120 | * "Die erforderliche Umlaufzeit beträgt {value} s" stand bis zur Aenderung |
| 121 | * mit dem GERASTERTEN Wert im Satz: bei 121 s Bedarf und Rasterung 7 s |
| 122 | * las der Bearbeiter "126,0 s" - eine Zahl, die in keiner Rechnung |
| 123 | * erforderlich ist. |
| 124 | */ |
| 125 | const meldung = computeCycleTime('webster', eingang(121, 7)).notes.find( |
| 126 | (n) => n.code === 'umlaufzeit-ueber-hoechstwert', |
| 127 | ); |
| 128 | expect(meldung, 'Meldung zum Hoechstwert').toBeDefined(); |
| 129 | expect(meldung!.message).toContain('Die erforderliche Umlaufzeit beträgt 121,0 s'); |
| 130 | expect(meldung!.message, 'der gerasterte Wert gehoert nicht in diesen Satz').not.toContain( |
| 131 | '126', |
| 132 | ); |
| 133 | }); |
| 134 | |
| 135 | it('nennt die Meldung die Mindestumlaufzeit, wo diese massgebend ist', () => { |
| 136 | const meldung = computeCycleTime('webster', eingang(60, 5, 125)).notes.find( |
| 137 | (n) => n.code === 'umlaufzeit-ueber-hoechstwert', |
| 138 | ); |
| 139 | expect(meldung!.message).toContain('Die erforderliche Umlaufzeit beträgt 125,0 s'); |
| 140 | }); |
| 141 | |
| 142 | it('schreibt die Meldung die beiden Zahlen unterscheidbar, wenn sie dicht beieinanderliegen', () => { |
| 143 | /* |
| 144 | * Mit dem gerasterten Wert konnte das nicht vorkommen - er lag stets |
| 145 | * mindestens eine Rasterstufe ueber dem Hoechstwert. Der erforderliche |
| 146 | * Umlauf kann ihn um Hundertstelsekunden ueberschreiten; mit einer |
| 147 | * Nachkommastelle stuende dann "beträgt 120,0 s und überschreitet den |
| 148 | * Höchstwert von 120,0 s". |
| 149 | */ |
| 150 | const meldung = computeCycleTime('webster', eingang(120.01, 7)).notes.find( |
| 151 | (n) => n.code === 'umlaufzeit-ueber-hoechstwert', |
| 152 | ); |
| 153 | expect(meldung, 'auch 0,01 s ueber dem Hoechstwert werden gemeldet').toBeDefined(); |
| 154 | expect(meldung!.message).toContain('beträgt 120,01 s'); |
| 155 | expect(meldung!.message).toContain('Höchstwert von 120,00 s'); |
| 156 | }); |
| 157 | |
| 158 | it('sagt sie "mehr als", wo auch drei Stellen den Unterschied nicht mehr zeigen', () => { |
| 159 | /* |
| 160 | * Der Rueckfall des Helfers `unterscheidbar`. Er ist erreichbar, weil der |
| 161 | * erforderliche Umlauf aus dem rechnerischen Wert stammt und stetig ist: |
| 162 | * 120,0001 s runden auf drei Stellen wie der Hoechstwert selbst. Statt |
| 163 | * eine Zahl zu erfinden, die dann beide Male gleich lautete, sagt der Satz |
| 164 | * "mehr als". Die Zusage steht im Kopfkommentar des Helfers; ohne diesen |
| 165 | * Fall bewacht sie nichts. |
| 166 | */ |
| 167 | const meldung = computeCycleTime('webster', eingang(120.0001, 7)).notes.find( |
| 168 | (n) => n.code === 'umlaufzeit-ueber-hoechstwert', |
| 169 | ); |
| 170 | expect(meldung, 'auch ein Zehntausendstel ueber dem Hoechstwert wird gemeldet').toBeDefined(); |
| 171 | expect(meldung!.message).toContain('beträgt mehr als 120,0 s'); |
| 172 | expect(meldung!.message).toContain('Höchstwert von 120,0 s'); |
| 173 | expect(meldung!.message, 'keine erfundene Zahl').not.toContain('beträgt 120,0 s'); |
| 174 | }); |
| 175 | |
| 176 | it('geht der Hoechstwert der Rasterung vor - die ausgegebene Umlaufzeit steht dann nicht im Raster', () => { |
| 177 | /* |
| 178 | * Die Rasterung ist keine harte Nebenbedingung des Ergebnisses: Die |
| 179 | * Kappung bricht sie, und zwar auch schon vor dieser Aenderung (120 ist |
| 180 | * kein Vielfaches von 7). Genau deshalb darf sie der Beurteilung nicht |
| 181 | * vorangehen. |
| 182 | */ |
| 183 | const ergebnis = computeCycleTime('webster', eingang(119.48, 7)); |
| 184 | expect(ergebnis.cycleTime).toBe(120); |
| 185 | expect(ergebnis.cycleTime % 7, '120 ist kein Vielfaches von 7').not.toBe(0); |
| 186 | }); |
| 187 | |
| 188 | it('bleibt der Zustand "uebersaettigt" unberuehrt', () => { |
| 189 | // Gegenprobe an der anderen Ausnahme: Y >= 1 kehrt vor der Rasterung |
| 190 | // zurueck und traegt weiterhin den eigenen Zustand. |
| 191 | const ergebnis = computeCycleTime('webster', { |
| 192 | lostTime: 20, |
| 193 | criticalFlowRatios: [0.6, 0.5], |
| 194 | step: 7, |
| 195 | }); |
| 196 | expect(ergebnis.bounded).toBe('uebersaettigt'); |
| 197 | expect(ergebnis.notes.map((n) => n.code)).toContain('uebersaettigt'); |
| 198 | }); |
| 199 | }); |