Status: ✅ zaimplementowane, 2026-09-17. metadata.txt 0.3.0. Każda sekcja zweryfikowana
przez QGIS MCP na sieci łódzkiej (tools/f1_smoke_test_lodz): CheckTransitData na realnym GTFS,
scenariusz z narysowaną linią tramwajową (para 953→587: 63 → 14 min), porównanie dostępności
przed/po, 2SFCA zgodne z niezależnym przeliczeniem z macierzy, podsumowanie równościowe zgodne
z ręcznym. 256 testów pytest zielonych, flake8 czysto.
Data: 2026-09-17
Autor: Michał Kaczorowski
Kontekst wymagany do pracy: ten plik + CLAUDE.md + CONTEXT.md +
docs/notes/roadmap-candidates.md (T1-B, T1-C, T2-E, T2-F, T2-G) +
docs/notes/r5-engine-primer.md + docs/prd/PR_easy-R5_v02_service-minutes.md (wzorzec
rozszerzania komendy matrix).
Ten PRD obejmuje pięć pozycji z
roadmap-candidates.md, wybranych 2026-09-17 jako największy wpływ na użytkownika i funkcje unikalne w ekosystemie QGIS. Każda sekcja R-X jest samodzielna. Kolejność implementacji: R-3 → R-1 → R-2 → R-5 → R-4.
| # | Funkcja | Pytanie użytkownika | Dlaczego unikalne |
|---|---|---|---|
| R-1 | BuildScenario + parametr SCENARIO |
„co jeśli puścimy tu tramwaj / zlikwidujemy linię 86 / autobusy pojadą wolniej?” | OTP 1.5 tego nie umie wcale. R5 stosuje modyfikacje w pamięci, bez przebudowy sieci. Jedyna wtyczka QGIS z podobną funkcją (Accessibility Calculator) edytuje pliki GTFS i przelicza wszystko od nowa. |
| R-2 | CompareScenarios |
„która dzielnica straciła po zmianie rozkładu?” | Bez tego scenariusz daje dwie mapy, które trzeba porównywać wzrokiem. |
| R-3 | CheckTransitData |
„czy ten GTFS w ogóle nadaje się do analizy na 24 sierpnia?” | Łapie najgorszy incydent projektu (cichy walk-only, GZM 08.2026) zanim ktoś zbuduje sieć. |
| R-4 | SummarizeAccessibilityEquity |
„ilu mieszkańców dociera do szpitala w 30 min?” | Pytanie głównej persony (product-scope.md) i drugi wymiar ubóstwa transportowego z Zalecenia Komisji (UE) 2025/1021. Dziś kończy się w arkuszu. |
| R-5 | RunCompetitiveAccessibility (2SFCA) |
„ile lekarzy przypada na mieszkańca, licząc konkurencję o te same przychodnie?” | Standard w literaturze zdrowia/edukacji; w QGIS brak implementacji transportowej (tylko ArcGIS/R/Python). |
Poza zakresem całego v0.3: DownloadTransitData, natywna powierzchnia R5
TRAVEL_TIME_SURFACE, taryfy, wysokości, tłumaczenia PL nowych stringów (issue #1).
javap na r5-v7.6-all.jar + spike na sieci łódzkiej (tools/f1_smoke_test_lodz,
855 wzorców, 138 tras):
Scenario{id, modifications};scenario.applyToTransportNetwork(network)zwraca kopię sieci (spike: 1,5 s) —TravelTimeComputersam scenariusza nie stosuje, liczy na sieci, którą dostanie.- JSON modyfikacji: Jackson z
type(add-trips,remove-trips,adjust-speed,adjust-frequency, …). TrzebaJsonUtilities.lenientObjectMapper— zwykły mapper odrzuca poletype(UnrecognizedPropertyException). - Id tras i kursów w sieci mają prefiks feedu:
lodz_static_gtfs_2026-08-21:1,lodz_static_gtfs_2026-08-21:11493_1.RouteInfo.route_idjest bez prefiksu (1),route_short_name=1.TripPattern.routeIdma prefiks,routeIndexwskazujeRouteInfo. add-trips: przystanek nowy =StopSpecz samymilat/lon(podanieid/namerazem ze współrzędnymi to błąd „A reference to an existing id should not include coordinates”).frequencies[]=PatternTimetable{hopTimes[n-1], dwellTimes[n], headwaySecs, startTime, endTime, monday..sunday, entryId}.mode= GTFSroute_type.adjust-frequency{route, entries[{sourceTrip, headwaySecs, startTime, endTime, dni, entryId}], retainTripsOutsideFrequencyEntries}— kopiuje czasy przejazdu zsourceTrip.- Błędy modyfikacji lądują w
modification.errors, aapplyToTransportNetworkrzucaScenarioApplicationException. - Spike: remove-trips + adjust-speed (×0,5, „Changed speed on 1089 trips”) + adjust-frequency (5 min) + add-trips (2 przystanki, dwukierunkowo) — wszystko bez błędów.
- Nie nowa komenda Javy i nie nowy algorytm analizy. Opcjonalny pole job-speca
scenariow komendziematrixi opcjonalny, zaawansowany parametrSCENARIOwMatrixBase. Dzięki temu scenariusz działa od razu wRunTravelTimeMatrix,RunAccessibility,RunServiceMinutes,GenerateIsochronesiRunCompetitiveAccessibility, a detektor walk-only zostaje jedno-źródłowy (liczy na zmienionej sieci). - Plik scenariusza = natywny JSON R5 + trzy typy „easy-” rozwijane w Javie, bo tylko Java zna
id wzorców i kursów:
easy-remove-routes {routes}→remove-trips {routes: [pełne id]};easy-adjust-speed {routes, scale}→adjust-speed {routes, scale};easy-set-headway {routes, headway_minutes, start, end}→ jedenadjust-frequencyna trasę, jeden wpis na kierunek, na wzorcu z największą liczbą kursów (sourceTrip= jego pierwszy kurs), wszystkie dni,retainTripsOutsideFrequencyEntries=true.AdjustFrequencyczyści wszystkie wzorce trasy i odtwarza kursy tylko z wpisów (javap), więc warianty (skrócone, zjazdowe) znikają w oknie zamiast dostać pełną częstotliwość każdy — poprawka po review; pierwsza wersja dawała wpis każdemu wzorcowi i zwielokrotniała kursowanie. Łódź, linia 86: „Cleared 6 patterns, creating 2 new trip schedules”.- Wpis w
routesjest dopasowywany warstwami, pierwsza z trafieniem wygrywa: pełne id (feed:route_id) →route_id→route_short_name. Zero dopasowań →ERROR SCENARIO_INVALID; więcej niż jedna trasa →WARN SCENARIOz listą; zawszeINFOz rozwiązanym id. - Natywne typy R5 przechodzą bez zmian — zaawansowany użytkownik może napisać plik ręcznie.
- Nowa linia z warstwy liniowej QGIS: każdy wierzchołek = przystanek. Czasy między przystankami z odległości po kole wielkim / prędkości (min. 1 s). Brak własnego snapowania do ulic — R5 linkuje nowe przystanki do sieci ulic sam.
- Metoda w wyniku:
meta["scenario"]="<nazwa pliku>:<sha256[:8]>"albo"baseline"; polescenariow warstwachRunAccessibility/RunCompetitiveAccessibility. - Detektor walk-only schodzi z błędu do ostrzeżenia tylko dla scenariusza, który usuwa
kursy (
easy-remove-routes,remove-trips,remove-stops). Każdy inny scenariusz zostawia twardy błąd — to klasa incydentu GZM. - Monte Carlo (poprawka znaleziona przy review, dotyczy całej wtyczki): R5 traktuje
monteCarloDrawsjako łączną liczbę losowań w oknie (iterationsPerMinute = ceil(draws / minuty_okna), javapProfileRequest). Parametr „draws per minute” był więc w praktyce 1 losowaniem na minutę. Runner mnoży teraz przez długość okna (jak r5r), a histogram minut obsługi dzieli przez liczbę iteracji na minutę, żeby wynik został w 0–120. Dotyczy tylko sieci z kursami częstotliwościowymi (każda nowa linia ze scenariusza,frequencies.txtWarszawy): tam liczenie jest dłuższe — zmierzone 1,4× na Łodzi z jedną dodaną linią (okno 120 min), teoretycznie do 5× dla sieci złożonej głównie z kursów częstotliwościowych — ale powtarzalne i zgodne z opisem parametru. Sieci bez częstotliwości (Gdańsk, walidacja r5r) — bez zmian. - Znany artefakt silnika: dodanie trasy częstotliwościowej może zmienić P50 pojedynczej, niezwiązanej pary o 1 min (Łódź: 1 z 144 par, 74 → 75), bo R5 przełącza sposób iterowania dla sieci z częstotliwościami. Wynik jest powtarzalny (dwa przebiegi = identyczny CSV).
| Parametr | Typ | Domyślnie | Uwagi |
|---|---|---|---|
NEW_LINES |
warstwa liniowa, opcj. | — | każdy obiekt = nowa linia, wierzchołki = przystanki |
NEW_LINE_MODE |
enum TRAM/SUBWAY/RAIL/BUS/FERRY | BUS | → route_type 0/1/2/3/4 |
SPEED_KMH |
double | 25 | średnia prędkość komercyjna między przystankami |
DWELL_SECONDS |
int | 30 | postój na przystanku |
HEADWAY_MINUTES |
double | 10 | co ile kursuje |
SERVICE_START / SERVICE_END |
HH:mm | 05:00 / 23:00 | zakres kursowania, każdy dzień tygodnia |
BIDIRECTIONAL |
bool | True | |
REMOVE_ROUTES |
string opcj. | — | lista po przecinku (86, Z2 albo pełne id) |
SPEED_ROUTES + SPEED_SCALE |
string + double | — / 1.0 | 0.8 = 20% wolniej |
HEADWAY_ROUTES + NEW_HEADWAY_MINUTES + HEADWAY_START / HEADWAY_END |
— / 10 / 05:00 / 23:00 | nowa częstotliwość istniejących tras | |
OUTPUT_SCENARIO |
plik .json |
Brak jakiejkolwiek modyfikacji → błąd. SPEED_SCALE ≤ 0 → błąd. Nazwy tras bierzesz z
OUTPUT_ROUTES algorytmu CheckTransitData (R-3).
| Sytuacja | Zachowanie |
|---|---|
| Linia z < 2 wierzchołkami | błąd z id obiektu |
| Multilinia | każda część = osobna linia |
Nieznana trasa w REMOVE_ROUTES |
Java: ERROR SCENARIO_INVALID Route 'X' not found in the network |
| Błąd modyfikacji R5 | ERROR SCENARIO_INVALID <typ>: <błędy R5> |
Plik scenariusza nie jest JSON / brak modifications |
QgsProcessingException przed startem Javy |
| Przystanek poza zasięgiem sieci ulic | ostrzeżenie R5 → WARN SCENARIO |
- pytest (
test_scenario.py,test_job_spec.py) zielone, flake8 czysto. - QGIS MCP, sieć Łodzi: nowa szybka linia między dwoma odległymi punktami skraca czas tej pary;
REMOVE_ROUTESiSPEED_SCALE=0.5nie skracają czasu żadnej pary względem baseline.
- Jedno pole na przebieg, stałe nazwy wyjścia (
value_a,value_b,diff,pct_change,status). Kilka pól = kilka przebiegów. Styl: renderer kategorii nastatusbudowany w kodzie (styling.apply_categories), bo wynik może być punktowy albo poligonowy, a stały QML ma jeden typ symbolu. - Złączenie po polu id, nie po geometrii.
- Pola metody muszą się zgadzać, jeśli są w obu warstwach:
percentile,decay,time_window,departure_time,modes,catchment,max_trip_duration_minutes,max_walk_time_minutes,walk_speed_kmh,max_rides,monte_carlo_draws(te pięć ostatnich warstwy wynikowe niosą od v0.3). HIGHER_IS_BETTER(domyślnie tak): dostępność, minuty obsługi, 2SFCA — pusta wartość = 0. Wyłączone dla czasów przejazdu: niższy = lepszy, pusta wartość = nieosiągalne, więc utrata połączenia toworsebez liczbowegodiff, nigdy „poprawa”. Id złączenia normalizowane (12,12.0,"12"to ten sam klucz); obiektyonly_bw innym CRS są reprojektowane. Różnica → błąd z listą (alboALLOW_METHOD_MISMATCH). Mogą się różnić (to jest cel porównania):run_date,network_hash,scenario,transit_submodes,r5_version(ostatnie → ostrzeżenie).
LAYER_A (przed), LAYER_B (po), JOIN_FIELD, JOIN_FIELD_B (opcj.; domyślnie ta sama
nazwa), FIELD (numeryczne), FIELD_B (opcj.), ALLOW_METHOD_MISMATCH (zaawansowany),
OUTPUT.
Geometria i atrybuty A (albo B dla only_b) + value_a, value_b, diff = B − A,
pct_change = 100·diff/A (NULL gdy A = 0 lub brak), status: better (diff > 0), worse,
same, only_a, only_b. Log: liczba lepiej/gorzej/bez zmian, suma i średnia diff.
Kryteria: test_compare.py zielony; w QGIS porównanie dostępności baseline vs scenariusz
daje warstwę ze stylem, a porównanie dwóch przebiegów o różnym percentylu kończy się błędem.
- Czysty Python (
core/gtfs_check.py), reużywa parserów zcore/gtfs_calendar.py. Zero R5. - Raport powstaje zawsze; błędy nie przerywają algorytmu (chyba że
FAIL_ON_ERROR). - Obsługiwane
route_typew R5 7.6 (zweryfikowanejavap,TransitLayer.getTransitModes): 0–7, 11, 12, 100–1499. Pozostałe (8–10, 13–99, ≥ 1500) → R5 rzucaIllegalArgumentException→ ERROR.
| Poziom | Sprawdzenie |
|---|---|
| ERROR | brak stops.txt / routes.txt / trips.txt / stop_times.txt |
| ERROR | brak calendar.txt i calendar_dates.txt albo zero dni z kursami |
| ERROR | DATE podana i 0 kursów tego dnia (+ 3 najbliższe dni z kursami) |
| ERROR | route_type nieobsługiwany przez R5 7.6 |
| ERROR | ≥ 50% wspólnych trip_id między dwoma zipami (P50/P85 i static w jednym folderze); mniej → WARN |
| ERROR | zduplikowany feed_id między zipami (brak feed_id = nazwa pliku, jak w R5; DuplicateFeedException) |
| ERROR | EXTENT podany, a żaden przystanek w nim nie leży |
| WARN | trips.route_id / trips.service_id / stop_times.trip_id / stop_times.stop_id bez rekordu |
| WARN | kursy bez stop_times |
| WARN | przystanki z 0,0 / poza zakresem / bez współrzędnych |
| WARN | brak agency_timezone albo różne strefy w feedach |
| WARN | EXTENT podany, część przystanków poza nim |
| WARN | dni w zakresie kalendarza bez kursów (liczba) |
| INFO | zakres kalendarza, kursy/dzień min/mediana/max, liczba tras wg typu, bbox przystanków, brak shapes.txt, obecny frequencies.txt |
GTFS (plik .zip albo folder z zipami), DATE (opcj.), EXTENT (opcj.), FAIL_ON_ERROR
(zaawansowany, False), OUTPUT_REPORT (HTML), OUTPUT_SERVICE_DAYS (CSV date,trips),
OUTPUT_ROUTES (CSV feed,route_id,short_name,long_name,route_type,trips). Wynik ERRORS,
WARNINGS (liczby).
Kryteria: test_gtfs_check.py (zipy budowane w teście) zielony; w QGIS na GTFS Łodzi:
data z kursami → 0 błędów kalendarza, data bez kursów → ERROR z najbliższymi dniami.
- Czysty Python (
core/equity.py), działa na dowolnej warstwie z polem populacji i polami liczbowymi — wyjścieRunAccessibility,RunCompetitiveAccessibilityalbo cokolwiek innego. - Wszystkie statystyki ważone populacją. NULL / ujemna populacja → wiersz pominięty (licznik w logu); NULL dostępność → traktowana jako 0 (osoba bez dostępu, nie brak danych).
- Gini ważony:
G = Σ_i Σ_j w_i w_j |x_i − x_j| / (2 · W² · μ)liczony w O(n log n) po sortowaniu. μ = 0 albo jakakolwiek wartość ujemna (np. polediff) → Gini = NULL. - Kwantyle ważone: najmniejsze x, dla którego skumulowana waga ≥ q·W.
INPUT, POPULATION_FIELD, ACCESSIBILITY_FIELDS (wiele), THRESHOLD (1.0),
GROUP_FIELD (opcj.), OUTPUT_TABLE (warstwa bez geometrii / CSV:
grp, field, population, pop_at_least, share_at_least, pop_zero, share_zero, mean, p10, p25, p50, p75, p90, gini), OUTPUT_REPORT (HTML: jedno zdanie po ludzku na pole, tabela, pola metody
z warstwy wejściowej). Wiersz grp = "ALL" zawsze, plus po jednym na grupę.
Kryteria: test_equity.py (Gini = 0 dla równego rozkładu, znany przypadek, kwantyle,
pomijanie NULL) zielony; w QGIS udział populacji zgadza się z ręcznym przeliczeniem.
Luo & Wang (2003) 2SFCA, z opcjonalnym zanikiem (wariant E2SFCA-like, jak w
accessibility.decay_weight):
- krok 1 (podaż):
R_j = S_j / Σ_k P_k · w(t_kj)dlat_kj < d₀; - krok 2 (popyt):
A_i = Σ_j R_j · w(t_ij); w= STEP (klasyczny 2SFCA), LOGISTIC albo EXPONENTIAL;d₀=CATCHMENT_MINUTES;- wynik skalowany:
A_i · PER_POPULATION(np. lekarzy na 1000 mieszkańców). - Podaż bez popytu w zlewni →
R_j = 0, ostrzeżenie z liczbą.
- Jeden bieg macierzy (origins = lokalizacje popytu, destinations = podaż).
MatrixBase, jakRunAccessibility;SCENARIOdziała automatycznie. - Jeden percentyl (wspólny parametr
PERCENTILES, walidowany do dokładnie jednej wartości, domyślnie 50) — mieszanie percentyli w jednym indeksie nie ma interpretacji. - Populacja trafia do CSV origins jako dodatkowa kolumna (
origin_extra_fieldsw_run_matrix). - Suma kontrolna w logu:
Σ_i A_i·P_i = Σ_j S_jdla podaży z niezerowym popytem (własność 2SFCA — cała podaż jest rozdzielona).
Wspólne z MatrixBase (PERCENTILES = jedna wartość), POPULATION_FIELD
(origins), CAPACITY_FIELD (destinations), CATCHMENT_MINUTES (30), DECAY (STEP),
PER_POPULATION (1000), OUTPUT_LAYER (origins + fca, meta), OUTPUT_SUPPLY_LAYER
(opcj.: destinations + supply_ratio, demand_in_catchment), OUTPUT_CSV
(id,population,fca).
Kryteria: test_fca.py (ręcznie policzony przykład 2×2 i własność zachowania podaży)
zielony; w QGIS wartości dla 2 origins zgadzają się z ręcznym przeliczeniem z macierzy.