Skip to content

Standard values ​

Route model ​

ConstantValueMeaning
DEFAULT_TIMETABLE_MODE'simpleAutomatic'—
DEFAULT_DAYS_PER_WEEK3Days a week a request without a schedule block runs, in every month — three, the middle of the 1..7 range a first result is read on. A daily service was the old default (schedule_mode 'alwaysDaily', gone with 0.9.40); at a fixed potential demand it fills each train half as full as this one does, which is the wrong first impression of a corridor.
DEFAULT_ROUTING_MODE'fullRouting'—
DEFAULT_AUTO_STOP_ADDITION'off'—
DEFAULT_COMPOSITION_ID'NEW-BAL-7'Composition a request without composition_id is computed with — the seven-coach new-fleet balanced train. It is the middle of the catalog on every axis a first result is read on (places, length, cost per place-km), so a first evaluation neither flatters the concept with the cheapest formation nor burdens it with the largest. The frontend posts no composition until the user picks one; it reads back which one was used from route.trip_pairs[].composition_id. db/dev/seed.py asserts the id exists once the catalog is seeded.
GTFS_SERVICE_START'2032-12-12'—
GTFS_SERVICE_END'2033-12-10'Nominal GTFS calendar window for persisted services — the project's target timetable year, 2032 (per the December-to-December European rail timetable-change convention: 2nd Sunday of December through the day before the following year's 2nd Sunday). GTFS requires concrete dates; the model itself only knows seasonal frequencies, so every saved service is pinned to this window until real timetable-year handling exists. Changing it changes persisted GTFS calendars, hence a version bump. If "2032" means the timetable period covering most of calendar year 2032 (starting Dec 2031) rather than the one starting Dec 2032, use "2031-12-14" / "2032-12-11".
MIRROR_MIN26 * 60 + 3002:30, expressed 'next day' (1590) on the continuous minutes-from-midnight scale used throughout (see models.utils.hhmm_to_min). Fixed constant that timetable_mode='simpleAutomatic' schedules are mirrored around, and that 'simpleAutomaticWithFixedNight' centers the fixed night interval on.
NIGHT_START_MIN24 * 6000:00 next day (1440) — threshold X of the night window. Boarding is judged on DEPARTURE time: a stop departing strictly before this classifies boarding; the fixed-night interval's start stop must depart strictly before this (23:59 at the latest).
NIGHT_END_MIN29 * 6005:00 next day (1740) — threshold Y of the night window. Alighting is judged on ARRIVAL time: a stop arriving at/after this classifies alighting; anything neither boarding nor alighting is a night stop. The fixed-night interval's end stop must arrive no earlier than this.
FIXED_NIGHT_MIN_SPEED_RATIO0.7timetable_mode='simpleAutomaticWithFixedNight' only: minimum acceptable ratio of the fixed interval's timetable speed (incl. slack + dwell) to its pure routing speed (driving + dynamics + buffer). Stretching a short interval to cover the night window can make it arbitrarily slow — below this ratio the trip carries a 'fixed_night_stretch_slow' entry in general_parameters.timetable_warnings (a warning, never an error).
DEFAULT_MIN_TURNAROUND_MIN180Shortest stand a rake is given at a terminal between arriving and departing again, minutes. Default for the request's min_turnaround_min; decides trainsets via TripPair.cycle_days().
EVALUATION_YEAR2032Calendar year the schedule counts days in — the same year every price is expressed in. Only the month lengths matter (whether February has 29 days).
MAX_COMPOSITION_SPEED_KMH230Speed ceiling baked into the routing graph — the fastest composition the catalog can hold. Not a per-trip value: fullRouting caps every trip at its own composition.max_speed_kmh in the request custom model, and that is always at most this, so the baked rule never binds there. What it does bound is the paths that send no composition at all — route_geometry() (ONTD map lines, which have no composition by design) and simpleRouting. 230 rather than a higher number because above it the composition parameter breaks rather than scales: true high speed means distributed-traction trainsets, a different concept outside this model's scope (compositions calibration step 6b). It equals HSR_TRACK_SPEED_THRESHOLD_KMH by construction — track a night train could physically use is never treated as forbidden high-speed infrastructure. This value has two homes by necessity: GraphHopper reads the JSON at import time, not this constant. The JSON is the sanctioned mirror and carries a comment pointing back here — keep the two equal, and note that changing either requires a graph re-import.
STANDARD_GAUGE_MM1435The European mainline gauge — the tie-break winner when a trip's stops support several gauges, and the fallback when every stop's gauge is unknown (routing/gauge.py's resolution rules). Also the one gauge whose routing profile carries no suffix (see SUPPORTED_GAUGES_MM).
SUPPORTED_GAUGES_MM(1435, 1520, 1600, 1668)Every gauge FAMILY with a routing profile in the graph. NAMING CONTRACT with docker/config.yml: STANDARD_GAUGE_MM routes on the bare OPENRAILROUTING_PROFILE (night_train); every other member on <profile>_<gauge_mm> (night_train_1520, ...). Sanctioned mirror of the profile list in config.yml — keep the two equal; adding a gauge means a new profile there, a graph re-import, and the member here.
GAUGE_FAMILY_MM{1524: 1520}Gauges folded into another gauge's profile — {tagged: representative}. 1524 (Finnish) and 1520 (ex-Soviet) are 4 mm apart, historically the same gauge, and interoperable in practice: Finnish and ex-Soviet stock runs on both, and VR services crossed the border for decades. OSM tags them separately, and treating the tags as separate networks made the seam impassable exactly where the target network cares: Estonia, whose track is tagged both ways, ended up with stops the resolver put on one profile and platform track the graph put on the other. The night_train_1520 profile accepts BOTH tags (docker/custom_models/nt_gauge_1520.json — the other half of this mapping; keep them in step), and routing/gauge.py normalizes every stop's gauge set through this table before intersecting, so 1524 never reaches profile selection. Trips on the family report track_gauge_mm=1520.
BLOCKED_COUNTRIES('BY', 'RU')Countries no route may pass through, under any routing mode — a project decision (Back-on-Track EU, 2026-08), not an infrastructure fact. Enforced at request time: rail_router attaches a speed-0 area rule over each country's border polygon to every routing request (the graph-side country encoded value is not registered by this OpenRailRouting fork, so the block cannot be baked in — see docker/config.yml). input_params.countries carries rows for these codes SOLELY to hold the polygons; they are deliberately NOT in seed.py's placeholder tuple, so if the block ever failed, the route would still 422 on country coverage rather than silently pricing Belarusian kilometres — defence in depth, not redundancy.
HSR_TRACK_SPEED_THRESHOLD_KMH230A track segment counts as high-speed infrastructure when its permitted track speed (GraphHopper's max_speed encoded value, from OSM maxspeed) STRICTLY exceeds this. 230 deliberately targets dedicated NEW-BUILD high-speed lines only (250+ per the UIC/EU convention, e.g. LGV, NBS, AV): upgraded conventional lines (200-230, e.g. German ABS corridors at 230) stay fully usable for night trains regardless of hsr_allowed, and 230 also matches the fastest seeded composition's own max_speed_kmh — track a night train could physically exploit is never treated as forbidden high-speed infrastructure.
HSR_TRACK_SPEED_SANITY_MAX_KMH500Upper guard on the same condition — excludes segments whose maxspeed is untagged in OSM. GraphHopper encodes a missing maxspeed as a sentinel (0 or infinity depending on version); the two-sided range (THRESHOLD, SANITY_MAX) excludes both conventions, so unknown-speed track is never mistaken for high-speed infrastructure. No real European rail line exceeds this value.
HSR_AVOIDANCE_PRIORITY_FACTOR0.01GraphHopper custom-model priority multiplier applied to high-speed segments where HSR is not allowed — a strong penalty (100x) rather than a hard block, so a route is still found if high-speed track is genuinely the only physical connection.
HSR_AVOIDANCE_RING_SIMPLIFY_DEG0.01Douglas-Peucker tolerance in degrees (~1.1km) applied to a country's outer ring before it is sent to the routing engine as an HSR-avoidance area (rail_router.CountryIndex.get_largest_polygon). The area exists to say "this country's high-speed lines are off-limits", so it only has to contain the rail network — border precision is irrelevant, ring size is not: the raw EEZ rings total ~165k vertices across the seeded countries and would be serialized into every mixed-avoidance routing request. At this tolerance the same set costs ~10k vertices.
NON_ELECTRIFIED_PRIORITY_FACTOR0.1GraphHopper custom-model priority multiplier applied to track OSM tags electrified=no — a 10x penalty, deliberately an order of magnitude softer than HSR_AVOIDANCE_PRIORITY_FACTOR's 100x. The difference is intentional and worth keeping. HSR avoidance encodes a PERMISSION: high-speed track is off-limits where it is off-limits, so the penalty is set high enough to make it a veto in all but the no-alternative case. This rule encodes a PREFERENCE: unelectrified track is what the catalog cannot exploit, not what it is forbidden to touch. At 100x whole diesel-worked regions — the Baltics, parts of the Balkans, Scandinavian inland lines — would attract detours far longer than an operator would ever accept in exchange for catenary. At 10x an unelectrified alignment loses to any reasonable electrified alternative and still wins where the alternative is absurd or absent. A penalty rather than a hard block for the further reason that a short unelectrified station throat must not fail a whole trip. Applied unconditionally, in every routing mode, because it is not merely a realism preference. Every seeded locomotive is 'electric multi-system' (input_params.loco_types) and the energy domain prices catenary electricity on every kilometre, so a route over unelectrified track was being costed as something the train could not physically have done. It becomes composition-dependent the day a diesel locomotive enters the catalog — see OPEN_TODOS['diesel_traction']. The condition matches the enum member NO of the routing engine's electrified encoded value (already in graph.encoded_values, so no re-import), never UNSET: a missing electrified tag means unknown, and unknown is never treated as forbidden — the same discipline HSR_TRACK_SPEED_SANITY_MAX_KMH enforces for untagged maxspeed.
AUTO_STOP_BUFFER_M10000Max distance (metres) from a stop to the already-routed path for that stop to be considered a candidate — covers both stops that sit right on the line and ones merely 'close by'.
AUTO_STOP_ANALYTIC_DETOUR_M100Perpendicular distance to the routed geometry under which an auto-stop candidate is costed purely analytically (dwell + the dynamics model's accel/brake pair + out-and-back detour at cruise speed) with no router call — at this distance the stop sits on the routed line and a mini-reroute measures the same number at ~1.5s of router time (introduced 2026-08-06 after the 575-stop catalog made per-candidate routing the dominant calc cost). Candidates further out are refined by a real 3-point mini-reroute, since their true track detour can exceed the straight-line bound — e.g. a station the initial routing bypassed on a parallel line, which is also why AUTO_STOP_BUFFER_M is wide.
AUTO_STOP_MAX_DETOUR_PER0.05Max allowed increase in TECHNICAL trip time (driving + dynamics + dwell), as a fraction of the original trip's technical time, before mode 'add' stops adding further candidates. Mode 'suggest' deliberately ignores this budget. The schedule supplement is excluded from the basis: a detour costs real running and stopping minutes, while the supplement is margin, and margin should not fund extra stops. It also kept stop selection independent of the route-context calibration — with per-country supplements of 0.35 to 0.71, measuring against padded time would have given the same physical route a quarter more detour budget in France than in Austria.
TRACTION_LOCO_POWER_KW6400.0Assumed locomotive continuous power at the wheel (Siemens Vectron AC: 6.4 MW). Governs the constant-power phase of acceleration above P / F ≈ 77 km/h.
TRACTION_LOCO_TRACTIVE_EFFORT_KN300.0Assumed locomotive starting tractive effort (Siemens Vectron: 300 kN). Governs the constant-force phase of acceleration from standstill.
TRACTION_BRAKE_DECELERATION_MS20.5Service braking deceleration. Rail braking is effectively mass-independent (brake systems are dimensioned per vehicle to a standard deceleration); 0.5 m/s² is a comfortable service value appropriate for sleeping passengers — full emergency capability is far higher and irrelevant for timetabling.
NEUTRAL_PROPOSAL_ID0—

Demand model ​

ConstantValueMeaning
CLASS_ORDER('Seat', 'Couchette', 'Sleeper', 'Capsule')The four accommodation classes a traveller can book, in the order the model reports them — the same order the frontend's CLASS_ICONS use. Every per-class map in a demand result is keyed and ordered by this.
DEMAND_LEVELS{'small': 100000, 'medium': 200000, 'large': 300000, 'xl': 500000}Potential demand presets, passengers per year over both directions (D11). Defaults, not a scenario switch: the level writes the total and is saved with the proposal as demand.level; a total matching none of them is custom.
DEFAULT_DEMAND_LEVEL'medium'The level a request without a demand block runs at (§2.8).
DEMAND_LEVEL_CUSTOM'custom'—
GROUP_ORDER('comfort', 'group', 'senior', 'budget', 'business')The five traveller groups in allocation order (D14): every round walks them in this order, which is what keeps the first group from taking a class outright before a later group with the same first preference gets a turn.
GROUP_CLASS_PREFERENCES{'comfort': ('Sleeper', 'Capsule', 'Couchette'), 'group': ('Couchette', 'Seat'), 'senior': ('Couchette', 'Sleeper'), 'budget': ('Seat',), 'business': ('Sleeper', 'Capsule')}Which classes each group books, first preference first — and ONLY those (D14, D17): demand no listed class can seat is not served, there is no overflow into other classes and no spill between groups.
GROUP_LABELS{'comfort': 'Leisure – comfort', 'group': 'Leisure – group', 'senior': 'Senior leisure', 'budget': 'Leisure – budget', 'business': 'Business'}Display names of the groups, for the models registry.
DEFAULT_GROUP_SHARES_PCT{'comfort': 50.0, 'group': 25.0, 'senior': 10.0, 'budget': 10.0, 'business': 5.0}Default share of the potential demand per group, percent (D14). A request may post its own; a sum other than 100 is allowed and means the groups ask for total x sum (D15), no rebalancing.
ROUNDS_PCT(50.0, 20.0, 20.0, 10.0)Share of each group's demand released in each allocation round (D16). The rounds interleave the groups — see GROUP_ORDER.
RULE_SHARE0.8Within a round, the share of a group's released demand that follows the rule — first listed class until full, then the next. The rest spreads evenly over the group's OTHER listed classes; a single-class group has none, so it is 100 % rule. Expected values, not random draws (D18).
OD_PRESETS('even', 'long', 'mid', 'short')Fill presets of the OD matrix (D27): each writes one weight per boarding and alighting stop from its position along the route (0 = first, 1 = last of that list): even (all 1), long journeys (board 1−x, alight x), mid distance (1−
OD_PRESET_CUSTOM'custom'—
OD_WEIGHT_MIN0.3—
OD_WEIGHT_SPAN2.7A preset maps its position factor f in 0..1 to the weight OD_WEIGHT_MIN + OD_WEIGHT_SPAN x f, one decimal (0.3 … 3.0). Even is special-cased to 1 so the margins read naturally (finding 5).
DEFAULT_OD_WEIGHT1.0Weight of a stop no request weight names — the even spread (D30).
AIR_SHIFT_FLOOR_KM300.0Below this journey length nobody would have flown: the air share is 0.
AIR_SHIFT_AT_FLOOR0.25The air share at exactly AIR_SHIFT_FLOOR_KM, from where it rises linearly to 1 at AIR_SHIFT_FULL_KM.
AIR_SHIFT_FULL_KM1200.0From this journey length on every passenger is a shift from the plane; the other sources are negligible on a night train that long.
OTHER_CAR_SHARE0.5Of the passengers who would NOT have flown, the share that would have driven; the rest is induced — a trip that would not have been made. Car shift saves the car's emissions less the train's, an induced trip adds the train's (models/emissions/model.py).
FARE_PER_KM_BY_CLASS{'Seat': 0.06, 'Couchette': 0.03, 'Sleeper': 0.04, 'Capsule': 0.03, 'Catering': 0.0}Distance part of the base fare by class_main, EUR per passenger-km, net of VAT, 2032 price year (DEMAND 0.1.0, D31 — decided 2026-09-18 on the sketch: seat 10 + 0.06/km, couchette 75 + 0.03, sleeper 125 + 0.04, capsule 75 + 0.03). Deliberately small: a night-train tariff is nearly flat over distance. Since CALC 0.9.27 these are the DEFAULTS: a request may carry its own fares_eur_per_km (api/helpers/member_compute.py), per proposal and part of the family key. Catering is not a fare class and is pinned to 0 whatever the request says.
FARE_CLASS_MAINS('Seat', 'Couchette', 'Sleeper', 'Capsule')The class_mains a request may price. Everything else in FARE_PER_KM_BY_CLASS is fixed.
FARE_PER_PAX_BY_CLASS{'Seat': 10.0, 'Couchette': 75.0, 'Sleeper': 125.0, 'Capsule': 75.0}FIXED part of the base fare, EUR per passenger carried, net of VAT, 2032 price year (DEMAND 0.1.0, D31). A ticket costs fare_per_pax + fare_per_km x km: a berth has a price of admission a 300 km journey pays as surely as a 1,300 km one, so most of the fare sits here. Implied fares at 300 / 700 / 1,200 km: seat 28 / 52 / 82 — couchette 84 / 96 / 111 — capsule 84 / 96 / 111 — sleeper 137 / 153 / 173. Overridable per proposal as fares_eur_per_pax. Ticket revenue: inside every overhead and margin base. The 0.0.5 basis (realised 2025-26 fares of Nightjet, European Sleeper, Nox, ICN, Intercités de nuit, net of VAT, 2032 price year) still frames these; the 2026-09-18 decision moved the seat down and the berths up to where the sketch's revenues read right.
SERVICES_EUR_PER_PAX_BY_CLASS{'Seat': 1.5, 'Couchette': 2.5, 'Sleeper': 4.0, 'Capsule': 2.5}Revenue from additional services, EUR per passenger carried, net of VAT, 2032 price year (DEMAND 0.0.5, kept by 0.1.0). Bicycle carriage, oversized luggage, pets — set at ~2.5-3% of the class's base fare, the usual ancillary share in long-distance rail; no operator publishes the figure per passenger. Interrail/pass reservation fees are NOT here: a pass holder's reservation IS that passenger's fare and belongs in avg_price. NOT signed and NOT a net figure, which is what separates it from catering: its cost is either zero or already paid for in the lower place density of the coach that carries the bikes, so it is already in the cost model elsewhere and must not be netted here again. Being ordinary ticket revenue, it stays INSIDE the variable-overhead and EBIT-margin bases. Overridable as services_eur_per_pax.
CATERING_EUR_PER_PAX_BY_CLASS{'Seat': 1.5, 'Couchette': 2.0, 'Sleeper': 1.0, 'Capsule': 2.0}NET catering contribution per passenger carried, EUR, by class, 2032 price year (DEMAND 0.0.5, kept by 0.1.0). Signed, and a NET figure: the restaurant is not modelled as a business of its own, so one number per class carries its sales less its goods, provisioning and logistics costs. Positive means the service pays for itself and contributes; negative means the tickets it helps sell carry it. It EXCLUDES attendant time — the dining-coach attendant is already in the crew line (compositions/calib/CALIBRATION.md: one attendant per dining or bistro coach), so netting staff here again would count it twice; a train with no catering coach sells from the trolley with the same crew it has anyway. Basis: DB's day trains take under 1 EUR gross per passenger (2023 on-board revenue ~110 M EUR on ~130 M passengers). A night train captures an evening and a morning, so ~4-6 EUR gross per seat/couchette passenger, netted at ~45% after goods and logistics, gives ~2 EUR. Sleeper is lower because breakfast and a welcome drink are already in its fare. Per class since DEMAND 0.0.4 because the classes differ in what their base fare already includes. A class at 0.00 is a claim that its passengers buy nothing, not an absence of data. Deliberately outside the variable-overhead and EBIT-margin bases, which stay on ticket revenue (models/evaluation/model.py): charging distribution overhead on a figure that already nets its own overhead would count it twice. Overridable as catering_eur_per_pax.

Infrastructure model ​

ConstantValueMeaning
WEEKDAY_BLEND5.0 / 7.0Share of departures assumed to fall on a weekday. Austria and Switzerland levy their congestion surcharge and peak multiplier Monday to Friday only, but a Segment carries clock minutes and no service date, so a weekday-only tariff window is priced at five sevenths of its overlap rather than all or nothing — see calc_tac.py and OPEN_TODOS['tac_weekday_blend'].

Published by Back-on-Track. Every number on this site is generated from the model that produces the tool’s results. Text, figures and charts: CC BY 4.0 · code: GPL-3.0-or-later · map data © OpenStreetMap contributors.