lsa-planer

LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.

/ tests domain umlaufzeitempfehlung.test.ts

7,8 KB Rohdatei
tests/domain/umlaufzeitempfehlung.test.ts — 174 Zeilen
1 import { readFileSync, readdirSync } from 'node:fs';
2 import path from 'node:path';
3 import { describe, expect, it } from 'vitest';
4 import { buildSignalPlan } from '@/domain/plan/signalPlan';
5 import { createStandardIntersectionProject } from '@/domain/model/factory';
6 import type { Project } from '@/domain/model/project';
7
8 /*
9 * Befund 49 (Fassung 5.10.0).
10 *
11 * Die Unterlagen stellten der 4.x-Empfehlung ("addierte Objekte auf eine Zahl
12 * -> immer NaN") fuer Version 5 eine "begruendete Empfehlung" gegenueber und
13 * sagten zu, dass die Aenderungen "andere Zahlen" liefern - also eine Ausgabe,
14 * nicht bloss eine vorhandene Formel.
15 *
16 * `compareCycleTimeMethods` rechnete sie und war geprueft, hatte aber im ganzen
17 * `src/`-Baum keinen Aufrufer. Der Signalzeitenplan ging ueber
18 * `resolveCycleTime` und rechnete genau das eine gewaehlte Verfahren; die
19 * Auswahlliste der Phasenansicht kannte keine Empfehlung, und weder Pruefbericht
20 * noch Ausdruck noch Tabellenausgabe fuehrten einen Empfehlungstext.
21 *
22 * DER FALL IST GEFALLEN, UND ZWAR PLANMAESSIG (Fassung 5.29.0). Der Vergleich
23 * hat seit dieser Fassung einen Aufrufer: Der Planaufbau rechnet ihn und legt
24 * ihn als `cycleComparison` in den Plan, die Phasenansicht stellt die vier
25 * Ansaetze gegenueber, und die Planunterlage druckt sie in einem eigenen,
26 * abwaehlbaren Abschnitt. Die Faelle messen die Aufruferlage am Baum, nicht an
27 * einer Behauptung, und verlangen jetzt den Aufrufer.
28 */
29
30 /** Alle Quelldateien unter src/. */
31 function quelldateien(verzeichnis: string): string[] {
32 const gefunden: string[] = [];
33 for (const eintrag of readdirSync(verzeichnis, { withFileTypes: true })) {
34 const pfad = path.join(verzeichnis, eintrag.name);
35 if (eintrag.isDirectory()) gefunden.push(...quelldateien(pfad));
36 else if (eintrag.name.endsWith('.ts')) gefunden.push(pfad);
37 }
38 return gefunden;
39 }
40
41 /**
42 * Dateien unter src/, die den Namen nennen - ohne die Stelle der Definition
43 * und ohne Kommentare.
44 *
45 * KOMMENTARE ZAEHLEN NICHT (Fassung 5.28.0): Die Frage lautet "gibt es einen
46 * Aufrufer", und ein Satz ueber die Funktion ist keiner. Seit der Behebung
47 * erklaert der Kopfkommentar von `compareCycleTimeMethods`, warum die
48 * Empfehlung ein Wert und kein Verfahren ist, und nennt dabei den eigenen
49 * Namen; die Pruefung schlug daraufhin an, ohne dass ein Aufruf hinzugekommen
50 * waere. Ein Waechter, der sich an der eigenen Begruendung stoert, treibt dazu,
51 * die Begruendung wegzulassen.
52 */
53 function nennungenAusserDerDefinition(name: string): string[] {
54 const gefunden: string[] = [];
55 for (const datei of quelldateien('src')) {
56 const inhalt = readFileSync(datei, 'utf8')
57 .replace(/\/\*[\s\S]*?\*\//g, '')
58 .replace(/\/\/[^\n]*/g, '');
59 const ohneDefinition = inhalt.replace(`export function ${name}(`, '');
60 if (ohneDefinition.includes(name)) gefunden.push(datei);
61 }
62 return gefunden;
63 }
64
65 const DATUM = new Date('2026-01-01T00:00:00Z');
66
67 /**
68 * Ein Knotenpunkt, an dem NUR das HBS-Verfahren scheitert.
69 *
70 * Gemessen: 900 Kfz/h je Signalgruppe ergeben Y = 0,90 bei einer Verlustzeit
71 * von 24 s. Die Auslastungssumme liegt damit ueber dem Ziel-Auslastungsgrad
72 * des HBS-Verfahrens (0,85) und unter der Grenze 1 der wartezeitminimalen
73 * Ansaetze: Der Vergleich meldet fuer HBS 'uebersaettigt', Webster dagegen
74 * 'maximum' - rechenbar, aber ueber dem Hoechstwert.
75 *
76 * Genau diese Lage zeigt, ob fremde Meldungen in den Pruefbericht laufen: Der
77 * Kode 'uebersaettigt' entsteht ausschliesslich im HBS- und im HCM-Zweig, und
78 * gewaehlt ist keiner von beiden.
79 */
80 function uebersaettigtesHbs(): Project {
81 const roh = createStandardIntersectionProject('Nur HBS uebersaettigt', DATUM);
82 return {
83 ...roh,
84 program: { ...roh.program, method: 'webster' },
85 demands: roh.signalGroups
86 .filter((g) => g.mode === 'kfz')
87 .map((g) => ({ signalGroupId: g.id, volume: 900, heavyVehicleShare: 0 })),
88 };
89 }
90
91 /**
92 * Eine einstreifige Verkehrsfuehrung mit einer Umlaufzeit ueber 120 s.
93 *
94 * Der lange Raeumweg treibt die Zwischenzeiten und damit die Verlustzeit; die
95 * Anlagenart laesst bis 300 s zu. Gemessen mit 60 m Raeumweg je Beziehung und
96 * 300 Kfz/h: 230 s nach Webster, ohne Grenzfall. Am Knotenpunkt waere derselbe
97 * Wert bei 120 s gekappt - daran zeigt sich, mit welchen Schranken der
98 * Vergleich rechnet.
99 */
100 function langeEngstelle(): Project {
101 const roh = createStandardIntersectionProject('Lange Engstelle', DATUM);
102 return {
103 ...roh,
104 anlagenart: 'einstreifig',
105 conflicts: roh.conflicts.map((c) => ({ ...c, clearingDistance: 60 })),
106 demands: roh.signalGroups
107 .filter((g) => g.mode === 'kfz')
108 .map((g) => ({ signalGroupId: g.id, volume: 300, heavyVehicleShare: 0 })),
109 };
110 }
111
112 describe('Umlaufzeitempfehlung - der Sachstand', () => {
113 it('hat compareCycleTimeMethods jetzt einen Aufrufer im src-Baum', () => {
114 // Umgedreht mit Fassung 5.29.0: Der Planaufbau ruft den Vergleich, legt ihn als
115 // `cycleComparison` in den Plan, und Ansicht wie Ausdruck lesen ihn von
116 // dort. Faellt der Aufrufer wieder weg, gehoeren die Vermerke unten
117 // zurueck in die Unterlagen - dieser Fall sagt es dann.
118 expect(nennungenAusserDerDefinition('compareCycleTimeMethods')).toContain(
119 path.join('src', 'domain', 'plan', 'signalPlan.ts'),
120 );
121 });
122
123 it('nimmt der Plan den Vergleich mit, ohne fremde Meldungen einzusammeln', () => {
124 /*
125 * Die entscheidende Nebenbedingung des Ausbaus: Die drei nicht gewaehlten
126 * Verfahren rechnen an einer Umlaufzeit, die niemand schaltet. Ihre
127 * Beanstandungen duerfen den Pruefbericht dieser Planung nicht fuellen.
128 *
129 * Der Fall waehlt Webster bei einer Auslastungssumme, bei der das
130 * HBS-Verfahren uebersaettigt ist - die gemessenen Zahlen stehen am
131 * Projektaufbau `uebersaettigtesHbs`.
132 */
133 const plan = buildSignalPlan(uebersaettigtesHbs());
134 expect(plan.cycleComparison, 'der Plan traegt den Vergleich').not.toBeNull();
135 expect(plan.cycleComparison?.results.hbs.bounded).toBe('uebersaettigt');
136 expect(
137 plan.notes.some((n) => n.code === 'uebersaettigt'),
138 'die Uebersaettigung des HBS-Verfahrens steht nicht im Pruefbericht',
139 ).toBe(false);
140 expect(plan.cycleResult?.method, 'gerechnet wird das gewaehlte Verfahren').toBe('webster');
141 });
142
143 it('nimmt das gewaehlte Ergebnis aus dem Vergleich, statt es zweimal zu rechnen', () => {
144 // Sonst koennten Tafel und Signalzeitenplan verschiedene Zahlen zeigen.
145 const plan = buildSignalPlan(uebersaettigtesHbs());
146 expect(plan.cycleResult).not.toBeNull();
147 expect(plan.cycleComparison?.results.webster.cycleTime).toBe(plan.cycleResult?.cycleTime);
148 });
149
150 it('rechnet den Vergleich mit den Schranken der Anlagenart', () => {
151 /*
152 * DIE STELLE, AN DER ES STILL SCHIEFGEHT. `resolveCycleTime` baut sich aus
153 * den Vorgaben und der Anlagenart einen eigenen Kennwertsatz
154 * (`cycleDefaults`); der Vergleich muss denselben bekommen. Wer ihn
155 * stattdessen mit RILSA_DEFAULTS rechnet, bekommt die Grenzen des
156 * Knotenpunkts - und an einer einstreifigen Verkehrsfuehrung, wo bis 300 s
157 * zulaessig sind, staende in der Tafel ein auf 120 s gekapptes Ergebnis
158 * neben einer Umlaufzeit von 150 s im Plan.
159 *
160 * Gemessen: Die Grenzen der einstreifigen Fuehrung sind 30 bis 300 s, die
161 * des Knotenpunkts 30 bis 120 s.
162 */
163 const plan = buildSignalPlan(langeEngstelle());
164 expect(plan.anlagenart).toBe('einstreifig');
165 const webster = plan.cycleComparison?.results.webster;
166 expect(webster, 'der Vergleich ist da').toBeDefined();
167 expect(
168 webster?.cycleTime,
169 'mit den Knotenpunktgrenzen waere hier bei 120 s gekappt worden',
170 ).toBeGreaterThan(120);
171 expect(webster?.bounded, 'unter 300 s ist das kein Grenzfall').toBe('keine');
172 expect(webster?.cycleTime, 'dieselbe Zahl wie im Plan').toBe(plan.cycleResult?.cycleTime);
173 });
174 });