Methodology, parameters and validation for run aad4ae941323,
service day 2026-07-29.
| Author | Dmitry Ustimov |
|---|---|
| Issued | 2026-08-20 |
| Document version | aad4ae941323 the run it describes |
Every figure in this document belongs to one run. A run is identified by a content hash of its inputs, its parameters and the source code that produced it, so two runs of different code cannot claim the same identity, and a re-run whose output changed cannot silently reuse an old one.
| Run | aad4ae941323 |
|---|---|
| Vintage | termtime-2026 |
| Service day | 2026-07-29 |
| Code version | 9962fb32ab2f digest of the pipeline source |
| Git commit | c8cc8c31 |
| Specification | 5195dd40ff93d3c87abe7d49… |
| Parameter set | transit_v1 |
| Input lock | 88e2237af8826e6407d1946b… |
| Licence register | b9ab15310e45bd9e9b42722b… |
| Reach engine | raptor_v1 |
| Rows written | 6,305 |
The licence register hash is recorded but is deliberately not part of the run identity: correcting a licence changes what may be done with the output, never the output itself, and folding it into identity would force a full recompute to record a URL.
Every figure describes scheduled service: what the operators' published timetables say runs, on one representative service day. That is a deliberate choice. Accessibility, planning and site-selection questions ask what service a location is provided with, and a schedule-based answer is stable, reproducible from public data, and recomputable by anyone who doubts it.
It does not measure whether services ran on time. No realtime feed, vehicle position or arrival observation is used anywhere in this layer. Punctuality is a different question and needs a different input. A reader who wants reliability will not find it here, and no figure in this document should be presented as if it answered that.
The unit of analysis is a 250 m cell of the INSPIRE reference grid in EPSG:3035, the statistical grid European reporting already uses, so results align with other layers and with Eurostat geographies without resampling.
Feed currency, stated plainly. 1 of 6 timetable feeds in this run had passed the end of their published validity before the service day: gtfs_metro_madrid, valid to 2026-05-27, 63 days before. The operator had not republished at the time the inputs were locked, so the most recent published timetable was used and the gap is recorded here rather than left for a reader to derive from §7.1. Where a lapsed feed still described unchanged service the effect is nil; where service changed in the interval, figures for that operator are out of date by that margin.
Each cell takes an analysis point, and walking times are computed over a pedestrian network derived from OpenStreetMap rather than as straight-line distance. Two quantities are published per cell: time to the nearest stop of any kind, and time to the nearest frequent stop, the second computed separately for each time window because a stop that is frequent at 08:00 need not be at 21:00.
For each stop and route, scheduled departures within a window are counted and divided by the window's length. Where a feed encodes service as headways rather than as individual trips, the headway is converted arithmetically. The figure is per direction: a rider travels one way, so their service is that direction's, not the sum of both. A cell's figure aggregates the stops within walking range.
The access index follows Transport for London's Public Transport Accessibility Level method.
Each stop within walking range contributes an equivalent doorstep frequency,
EDF = 30 / TAT, where TAT is total access time — walk time plus half the
average headway. Contributions are weighted and summed, so the index reads as
“departures per hour effectively arriving at this doorstep”. It is reported against
TfL's published band thresholds rather than a scale invented here, which is the point of using an
established method: the number is comparable to a body of existing practice.
Reach columns count residents reachable within 30, 45 and 60 minutes door to door, combining walking, waiting and in-vehicle time. Population is apportioned from the Global Human Settlement Layer's modelled residential grid.
Residential population comes from the Global Human Settlement Layer, a modelled 250 m grid, apportioned to cells. It is not a census count, and it does not match one:
| Modelled (GHS-POP), used for every figure here | 3,805,757 |
|---|---|
| Registered (INE), carried as a cross-check | 3,332,035 |
| Difference | +14.2% |
The modelled figure is the higher one, and that is the expected direction. A settlement-layer model estimates people present from built form and survey data; a municipal register counts people who have registered. Residents who have not registered, or who registered elsewhere, appear in the first and not the second.
Which one drives the map: the modelled figure, because it is the only one available per 250 m cell — a register total is a municipal number and cannot be apportioned to a grid without inventing a distribution. Every population-weighted statement in this layer, including the headline median and the share of residents in each class, is therefore modelled. The registered total is recorded beside it in every run so the gap is measurable rather than discovered.
Where this matters: absolute headcounts carry the model's error, so "N residents are worse than 10 minutes from frequent service" should be read as an estimate. Comparisons between cells are far more robust, because the same model applies to both.
A real zero, an absence of service and a data failure are three different states and are kept distinguishable. A cell with no nearby stop is served by nothing; a cell whose inputs failed carries an explicit reason rather than a zero. Collapsing these would make a blank cell unreadable — the reader could not tell good news from missing data.
Every parameter carries one of two labels, and the distinction is not cosmetic. A specification value was transcribed from the customer's own written specification, whose hash is recorded in §1. A plan default was chosen by us: it is changeable without code, and it is never presented as though the specification required it. A parameter that is neither is a defect, and the labels below are read from the run's own manifest, not asserted here.
6 specification-derived, 32 plan defaults.
| Parameter | Value in this run | Source |
|---|---|---|
anchors.walk_freq_m | at_0 800.0; at_100 400.0 | spec_§6 |
family_a_buffer_m | 12000 | spec_§5 |
frequent_dep_hr | 4 | spec_§3_A2 |
qa_conformance.window_monotonic_order | — | spec_§3_A4 |
reach_thresholds_min | 30, 45, 60 | spec_§2 |
stop_count_radius_m | 800 | spec_§3_A3 |
| Parameter | Value in this run | Source |
|---|---|---|
access_walk_cap_m | 1200 | plan_default |
anchors.freq_dep_hr | at_0 0.0; at_100 12.0 | plan_default |
anchors.opps_reference | 100000 | plan_default |
anchors.walk_freq_min | at_0 10.0; at_100 5.0 | plan_default |
dep_hr_clip | None | plan_default |
destination_buffer_m | 20000 | plan_default |
dijkstra_chunk_bytes | 2.09715e+08 | plan_default |
gtfs_fixes.carry_frequency_direction_id | — | plan_default |
gtfs_fixes.prefer_stop_times_for_per_trip_frequencies | — | plan_default |
gtfs_fixes.use_calendar_service_date | — | plan_default |
line_params | contained 0.8; grid_m 30.0; min_stops 5 | plan_default |
max_snap_m | 400 | plan_default |
max_transfer_neighbours_per_stop | — | plan_default |
max_transfer_walk_m | — | plan_default |
min_transfer_seconds | — | plan_default |
mode_tiers | — | plan_default |
populated_min_pop | 1 | plan_default |
ptal_window | day_kind wednesday; t0 08:00:00; t1 09:00:00 | plan_default |
qa_conformance.viewer_class_count | — | plan_default |
qa_plausibility.dep_hr_ceiling | — | plan_default |
qa_plausibility.feed_dep_stop_ratio_max | — | plan_default |
qa_plausibility.feed_share_max | — | plan_default |
qa_plausibility.frequent_share_band | — | plan_default |
qa_plausibility.window_ordering_max_violation_share | — | plan_default |
reach_chunk_bytes | 2.09715e+08 | plan_default |
routing_network_margin_m | 5000 | plan_default |
service_day | wednesday | plan_default |
stop_identity | grid_m 30.0; parent_station True | plan_default |
walk_speed_m_per_min | 80 | plan_default |
windows.ampeak | day_kind wednesday; t0 07:00:00; t1 09:00:00 | plan_default |
windows.evening | day_kind wednesday; t0 20:00:00; t1 22:00:00 | plan_default |
windows.sunday | day_kind sunday; t0 10:00:00; t1 14:00:00 | plan_default |
Internal tests check this pipeline against itself, which is the weakest kind of assurance available: a reference implementation written by the same author from the same assumptions can share a defect with the thing it validates. The comparison below is external. Every published figure comes from Metro de Madrid, EMT, Renfe, Metros Ligeros de Madrid or CRTM — recorded with its URL, a verbatim quote and the date it was retrieved, and hash-pinned so the comparison cannot be made to pass by editing what an operator said.
| Comparisons | 41 |
|---|---|
| Within the published range | 33 |
| Outside it | 8 — 5 higher than published, 3 lower |
| Externally sourced | 32 — the rest compare against the operator's own feed, which checks our parser rather than the data |
Operators publish a range — “every 7 to 10 minutes” — rather than a single figure, so a result counts as agreeing when it falls anywhere inside the published range. Bands are rounded conservatively: lower bounds up, upper bounds down, so rounding can never move one of our values into agreement.
| Mode | Compared | Within range | Ratio range |
|---|---|---|---|
| EMT bus | 10 | 7 / 10 | 0.625–1.026 |
| Metro Ligero | 3 | 2 / 3 | 0.723–1.583 |
| Metro de Madrid | 28 | 24 / 28 | 0.89–1.317 |
These are unresolved. They are published here rather than omitted, because a validation section that shows only agreement tells a reviewer nothing they can check.
| Operator | Line | Where | Window | Published | Ours | Ratio |
|---|---|---|---|---|---|---|
| EMT Madrid | 123 | Plaza de los Metales, Butarque (EMT stop 5074) | ampeak | 3.0–5.0 | 2.5 | 0.625× |
| EMT Madrid | 85 | Plaza de los Metales, Butarque (EMT stop 5074) | ampeak | 6.0–8.57 | 5.0 | 0.686× |
| Metros Ligeros de Madrid | ML1 | Pinar de Chamartín (Metro Ligero ML1) | sunday | 2.5–3.5 | 4.75 | 1.583× |
| Metro de Madrid | 5 | CANILLEJAS (Metro line 5 — single-line station) | evening | 8.0–10.9 | 10.909 | 1.154× |
| Metro de Madrid | 5 | CANILLEJAS (Metro line 5 — single-line station) | sunday | 7.5–8.6 | 8.798 | 1.093× |
| Metro de Madrid | 4 | PROSPERIDAD (Metro line 4) | evening | 9.2–10.9 | 12.0 | 1.194× |
| Metro de Madrid | R | PRINCIPE PIO (Metro line R) | sunday | 7.1–8.0 | 9.947 | 1.317× |
| EMT Madrid | 172 | Avenida Camino de Santiago (EMT stop 1841, line 172) | ampeak | 3.6–4.6 | 3.5 | 0.854× |
§5 checks departures per hour against what operators publish — that validates the input. These two check what is built on it.
Stale evidence. This evidence was measured on run 75fd42f29bfe. This document describes run aad4ae941323. The figures below are that other run's.
Every walk figure comes from our own router over OpenStreetMap footpaths. Valhalla — a separate implementation — was asked for the same origin and the same destination stop, over the same extract, at the same walking speed, so a disagreement is a router disagreement and nothing else.
| Walks compared | 118 |
|---|---|
| Median difference | 0.39 min |
| Within 1 minute | 84 |
| Within 2 minutes | 100 |
| Median ratio | 1.031 |
0 could not be routed by Valhalla, and 2 were excluded as routing artefacts — walks either router put beyond an hour to a bus stop, which is not a walk anyone takes. They are listed in the evidence file rather than averaged into the figures above.
Thirteen journeys across Madrid were recorded from EMT's and Renfe's own planners before this pipeline produced any reach output. A corpus compiled after seeing your own answer is not evidence; this one predates ours.
| Group | Journeys | Median ratio | Within 25% |
|---|---|---|---|
| Unaffected by engineering works | 5 | 0.948 | 4 |
| Affected by the Atocha works | 6 | 1.283 | 3 |
Two things this comparison is not. The planners answer “if I leave now”; we answer “typically”, as a percentile over a departure window, which makes us slower on long, infrequent journeys by construction. And the works grouping is confounded — every affected pair is also among the longest and most peripheral — so it separates neither. No pass/fail band is declared: a journey time is a point, not a published range, and choosing a tolerance after seeing the numbers is choosing one that passes.
§4 says which parameters are ours. This says whether they matter. Each row moves ONE parameter and leaves the rest identical; every figure is the population-weighted mean absolute change, because a cell with four residents moving 300% is not a finding and one with 4,000 residents moving 5% is.
| Parameter | Moved by | Access index | Walk to any stop | Departures/hr |
|---|---|---|---|---|
walk_speed_m_per_min | -10% | 4.30% | 11.11% | 0.00% |
walk_speed_m_per_min | +10% | 3.90% | 9.09% | 0.00% |
access_walk_cap_m | -10% | 0.00% | 0.00% | 0.38% |
access_walk_cap_m | +10% | 0.00% | 0.00% | 0.00% |
frequent_dep_hr | -25% | 0.00% | 0.00% | 5.67% |
frequent_dep_hr | +25% | 0.00% | 0.00% | 34.98% |
max_snap_m | -50% | 0.05% | 0.00% | 0.11% |
stop_count_radius_m | -10% | 0.00% | 0.00% | 0.00% |
stop_count_radius_m | +10% | 0.00% | 0.00% | 0.00% |
Scope: Family A only (walk, frequency, access index, stop count). Family B (opps_*) is not swept — it would cost a full RAPTOR sweep per arm.
The largest movement, read plainly. walk_speed_m_per_min is a plan_default — a value we chose, not one the specification fixed. Its value in this run is 80.0, and moving it by -10% moves the access index by 4.30% of its value, population-weighted. A reader who thinks that value should be different should read every access-index figure in this document as carrying about that much movement per -10% of disagreement.
A run records the checks that ran and what each returned. The gate distinguishes a check that passed from one that was never evaluated, because “not evaluated” reported as a pass is how an unverified number reaches a client.
| Status | pass_with_unevaluated |
|---|---|
| Checks recorded | 51 |
| Check | Severity | Outcome |
|---|---|---|
a1_cap_not_binding_for_anchors | warn | not_evaluated |
blend_ordering_stable_under_reweight | warn | not_evaluated |
classification_saturation | warn | pass |
dep_hr_plausibility_ceiling | warn | pass |
external_journey_planner_spotcheck | warn | not_evaluated |
feed_contribution_reconciliation | warn | pass |
frequent_by_route_summing | warn | not_evaluated |
frequent_share_plausible | warn | pass |
n_clipped_is_meaningful | warn | not_evaluated |
norm_opps_discriminates | warn | not_evaluated |
opps_departure_window_stability | warn | not_evaluated |
opps_plausibility_central_gt_peripheral | warn | not_evaluated |
opps_reference_is_final | warn | not_evaluated |
ptal_band_tracks_walk_and_freq | warn | not_evaluated |
stop_count_reconciles_to_network | warn | not_evaluated |
summary_digest_drift | warn | not_evaluated |
window_ordering_dep_hr | warn | warn |
window_ordering_walk_freq | warn | not_evaluated |
a4_stop_is_a2_stop | hard | not_evaluated |
all_destinations_on_network | hard | not_evaluated |
anchors_are_not_provisional | hard | pass |
aoi_population_reconciles_ine | hard | not_evaluated |
composite_rank_extremes | hard | not_evaluated |
declared_not_null_columns_populated | hard | pass |
declared_walk_cap_conformance | hard | pass |
dep_hr_matches_published_timetable | hard | not_evaluated |
edge_buffer_flagged | hard | not_evaluated |
every_mode_reports_departures | hard | pass |
frequent_set_headway_verified | hard | not_evaluated |
frequent_state_partition_complete | hard | not_evaluated |
known_corridor_mode_present | hard | not_evaluated |
mode_present_implies_stop_in_catchment | hard | not_evaluated |
mode_tier_score_matches_modes | hard | not_evaluated |
net_island_handled_not_infinite | hard | not_evaluated |
no_data_has_declared_cause | hard | not_evaluated |
no_nonfinite_published | hard | pass |
opps_threshold_monotonic | hard | not_evaluated |
provenance_completeness | hard | not_evaluated |
ptal_basis_is_network | hard | not_evaluated |
ptal_hand_calculation | hard | not_evaluated |
ptal_walk_consistent_with_a1 | hard | pass |
row_grain_unique_populated_cells | hard | not_evaluated |
schema_conformance | hard | not_evaluated |
seasonal_pair_unconfounded | hard | not_evaluated |
service_day_within_all_feed_validity | hard | not_evaluated |
service_state_partition_complete | hard | not_evaluated |
walk_any_le_walk_freq | hard | not_evaluated |
walk_ge_crowfly_lower_bound | hard | not_evaluated |
walk_monotonicity_lipschitz | hard | not_evaluated |
window_frequency_monotonicity | hard | pass |
zero_iff_no_service | hard | not_evaluated |
Each input carries a written licence position, held once in a register whose hash is recorded with the run.
1 source has no settled redistribution position: aoi_polygon. Until settled, this layer is provisional for redistribution: the figures stand, and the terms on which a third party may receive them do not. What each open position blocks is stated against that source in the table below. It is said here rather than left to be inferred from a table.
| Feed | Validity | Licence |
|---|---|---|
gtfs_crtm_interurbanos | 2026-06-13 → 2027-07-24 | https://www.crtm.es/licencia-de-uso |
gtfs_crtm_metro_ligero | 2026-06-22 → 2027-06-22 | https://www.crtm.es/licencia-de-uso |
gtfs_crtm_urban_bus | 2026-06-13 → 2027-07-24 | https://www.crtm.es/licencia-de-uso |
gtfs_emt_madrid | 2026-07-16 → 2026-12-31 | https://mobilitylabs.emtmadrid.es/sip/terms-of-use |
gtfs_metro_madrid | 2025-05-27 → 2026-05-27 lapsed 63 days before the service day | https://www.crtm.es/licencia-de-uso |
gtfs_renfe_cercanias | 2026-07-17 → 2026-08-15 | https://creativecommons.org/licenses/by/4.0/ |
| Source | What may be done with it | Required attribution |
|---|---|---|
aoi_polygon | No settled position. The boundary defining the study area has not been fixed: an OpenStreetMap administrative relation is in use, against the alternative of an official INE/IGN municipal boundary. No licence can be recorded until that choice is made. | — |
ghspop_madrid | Copy, redistribute, adapt and build upon for any purpose including commercial use, with attribution. No share-alike. | Pesaresi, M., Schiavina, M., Politis, P., Freire, S., Krasnodębska, K., Uhl, J. H., ... Kemper, T. (2024). Advances on the Global Human Settlement Layer by joint assessment of Earth Observation and population survey data. International Journal of Digital Earth, 17(1). https://doi.org/10.1080/17538947.2024.2390454 |
gtfs_crtm_interurbanos | Cell metrics convey no source record and ship as processed data (datos explotados); conveyed feed records (the stops/lines overlay) stay viewer-only or ship under CRTM terms. | Powered by CRTM — with a link to https://www.crtm.es/, noting that the data is processed (datos explotados) |
gtfs_crtm_metro_ligero | Cell metrics convey no source record and ship as processed data (datos explotados); conveyed feed records (the stops/lines overlay) stay viewer-only or ship under CRTM terms. | Powered by CRTM — with a link to https://www.crtm.es/, noting that the data is processed (datos explotados) |
gtfs_crtm_urban_bus | Cell metrics convey no source record and ship as processed data (datos explotados); conveyed feed records (the stops/lines overlay) stay viewer-only or ship under CRTM terms. | Powered by CRTM — with a link to https://www.crtm.es/, noting that the data is processed (datos explotados) |
gtfs_emt_madrid | Commercial redistribution of derived data is permitted with attribution. No share-alike clause exists in these terms, so EMT imposes no constraint on an exclusive deliverable. | Powered by EMT de Madrid — with a link to https://www.emtmadrid.es/ |
gtfs_metro_madrid | Cell metrics convey no source record and ship as processed data (datos explotados); conveyed feed records (the stops/lines overlay) stay viewer-only or ship under CRTM terms. | Powered by CRTM — with a link to https://www.crtm.es/, noting that the data is processed (datos explotados) |
gtfs_renfe_cercanias | Commercial use, redistribution and derived works permitted with attribution. No share-alike. | Renfe, CC BY 4.0 |
osm_castilla_la_mancha | The walking graph derived from OpenStreetMap is never conveyed: it is an intermediate that computes walk times and is not published, not in the GeoPackage and not in the viewer payload. What ships is a scalar in minutes per 250 m cell, carrying no OSM identifier, geometry or attribute. ODbL 4.4 attaches to a Derivative Database that is PUBLICLY USED, and 4.5(b) states that using the Database to create a Produced Work does not create a Derivative Database for 4.4's purposes. The shipped basemap geometry (roads.bin, buildings.bin, places.json) is a separate artefact already accepted as a Derivative Database offered onward under ODbL. | © OpenStreetMap contributors |
osm_castilla_y_leon | The walking graph derived from OpenStreetMap is never conveyed: it is an intermediate that computes walk times and is not published, not in the GeoPackage and not in the viewer payload. What ships is a scalar in minutes per 250 m cell, carrying no OSM identifier, geometry or attribute. ODbL 4.4 attaches to a Derivative Database that is PUBLICLY USED, and 4.5(b) states that using the Database to create a Produced Work does not create a Derivative Database for 4.4's purposes. The shipped basemap geometry (roads.bin, buildings.bin, places.json) is a separate artefact already accepted as a Derivative Database offered onward under ODbL. | © OpenStreetMap contributors |
osm_madrid | The walking graph derived from OpenStreetMap is never conveyed: it is an intermediate that computes walk times and is not published, not in the GeoPackage and not in the viewer payload. What ships is a scalar in minutes per 250 m cell, carrying no OSM identifier, geometry or attribute. ODbL 4.4 attaches to a Derivative Database that is PUBLICLY USED, and 4.5(b) states that using the Database to create a Produced Work does not create a Derivative Database for 4.4's purposes. The shipped basemap geometry (roads.bin, buildings.bin, places.json) is a separate artefact already accepted as a Derivative Database offered onward under ODbL. | © OpenStreetMap contributors |