The assumptions this study's travel demand model ran with, as used for the
control scenario. Most are codebase defaults; some
are set for this study or fitted to its local data. Each shows where its
value comes from. The control
scenario is not built yet, so its values are the study's and the
codebase's files as they stand.
study set for this studycalibrated fitted to local datascenario set by the scenariorun time resolved by the model at run time
Set for this study15values in the study's parameter files
Fitted to local data61values written by calibration
Set by the scenario0in control's own files
Resolved at run time2the model overrode the files
Codebase defaults613of 691 values
Other scenarios0 of 3change a parameter (2 not built yet)
People [people]
Who will ride a bike, and on what streets
bike_lts44%
bike_lts29%
bike_lts151%
bike_lts036%
Shares of the population by how much traffic stress they will tolerate on a bike: 4% will ride anywhere, 9% will ride in painted bike lanes, 51% only on low-stress streets and paths, and 36% will not bike. Each group gets its own bike network, so a protected bike lane shows benefits for the cautious majority rather than for an average cyclist.
Basis: Dill & McNeil (2016), "Revisiting the Four Types of Cyclists: Findings from a National Survey", TRR 2587
Share of car commuters with free parking at work
0.35, 0.5, 0.6, 0.7, 0.8
share, lowest to highest income
By income group (low to high), the share of car-owning workers whose employer covers their parking. They pay nothing to park on work trips, which lowers the cost of driving to work in places with paid parking. These shares are a stated assumption, not a measurement, and are worth setting per study.
Destinations [destinations]
Minimum confidence for a mapped place to count
0.85
Shops, restaurants, schools, clinics and other places come from Overture Maps, which gives each place a confidence score. Places below this score are left out. It sets the whole inventory of non-work destinations, so it changes how many destinations each zone has. It is a judgment cut that has not been tested against alternatives.
The OpenStreetMap road classes that cars can route on. Service roads and "unclassified" roads are left out. In rural areas and small towns, "unclassified" is often the main through road, so a study outside a large metro can lose real road connections here.
Travel times and costs [routing]
Value of time as a share of the hourly wage
2
÷ hourly wage
Each traveler group's value of time is its hourly wage divided by this number, at the reference income. Value of time converts dollars (fares, parking, fuel, tolls) into minutes, so it decides how much money costs weigh against travel time in mode and destination choice.
Basis: Checked against USDOT's 2023 personal value of travel time (about $19.40/hr) at the 2023 ACS US median household income
How value of time rises with income
0.5
Value of time grows with income, but less than proportionally: at 0.5, a household with four times the income values its time twice as much. This sets how differently a fare or toll weighs on low- and high-income travelers.
Basis: Meta-analytic central estimate: Wardman (2001); Börjesson & Eliasson (2014); Binsuwadan et al. (2023)
Transit fare
2
$ per ride
One fare per transit ride, in dollars, the same for every trip. It shifts transit's cost up or down for each income group but cannot represent distance-based fares, zones, transfers or passes. A study with real fare policy should set it.
How much worse a minute of waiting is than riding
2.0
× a minute on board
A minute spent waiting for transit counts as this many minutes on board. Walking to, from and between stops uses the same weight by default (2.0), which is also the weight on ordinary short walks, so a walking minute costs the same on every mode.
Basis: US practice: MTC Travel Model One, SFCTA CHAMP and the TCRP synthesis weight out-of-vehicle time at 2× in-vehicle time
Penalty for each transfer
5.0, 10.0, 30.0
min, for the 1st, 2nd, 3rd+ transfer
Extra minutes charged for the act of changing vehicles, on top of the added walking and waiting: 5 minutes for the first transfer, 10 more for the second, 30 more for the third. Higher values favor one-seat rides.
Basis: MTC Travel Model One; within the TCRP US planning range of about 5–15 equivalent minutes per transfer
How riders value time on bus, light rail, metro and rail
BUS
1.0
TRAM_LRT
0.95
METRO
0.92
RAIL
0.88
FERRY
1.0
OTHER
1.0
× a minute on a bus
A minute on board each kind of transit, relative to a minute in a car. Bus is set equal to car; light rail, metro and commuter rail are slightly preferred (0.88–0.95). The differences are deliberately modest.
Basis: Wardman, Chintakayala & de Jong (2016), Table 7; Ben-Akiva & Morikawa (2002)
Car fuel cost per mile
0.13
$ per mile
Fuel cost per mile driven, including fuel taxes. Added to maintenance and tires (about 11 cents per mile) and any per-mile fee to give the all-in car operating cost. Insurance, registration and depreciation are left out because they do not change with each trip.
Basis: AAA, Your Driving Costs 2025 (regular gas at $3.151/gal, national average)
Longest time spent looking for parking
6.0
min
Minutes spent circling for a space in the densest zones; less dense zones get less, and low-density zones close to none. Search time counts double, like waiting. Together with the walk from the parked car, it makes driving to dense places slower than the road network alone suggests.
Ridehail fare per minute
1
$ per minute
Ridehail costs a base fare ($5) plus this amount per minute in the vehicle, with an average 5-minute pickup wait everywhere. There is no per-mile charge and no surge. These are judgment values; a study where ridehail matters should set them.
Walking speed
1.2
m/s
Walking speed in meters per second (1.2 m/s is about 2.7 mph). It sets walk travel times everywhere, including the walking parts of transit trips.
Cycling speed
4.17
m/s
Cycling speed in meters per second (4.17 m/s is about 9.3 mph). On streets more stressful than a rider will tolerate, they slow to a crawl, about the pace of walking a bike.
Walking environment [streetscape]
Penalty for walking in the sun
0.16
A fully sunlit walk feels up to this much longer than a shaded one (0.16 = 16%). It is scaled by the study's own climate in the analysis months, so it matters in hot places and close to nothing in cool ones. Trees and buildings provide the shade.
How much longer a walk feels beside fast, heavy traffic with little separation, per point of a pedestrian traffic-stress score; a quiet street with a sidewalk pays nothing. Busy arterials come out roughly 14% longer on average. This is what lets a road diet or traffic diversion change walking in a scenario.
Basis: Broach & Dill, Portland GPS route-choice study (busier streets treated ~14% longer); score form from the HCM 2010 pedestrian link score
Parking [parking]
How parking prices are set
study"census_cbsa"
observed_prices
"observed_prices" fits a price curve to researched garage and lot rates in the study area. "census_cbsa" (being retired) scales one metro-wide maximum rate by destination density. Parking price is one of the largest costs of driving into a dense center.
Density below which parking is free
study0
400
Zones with fewer trip destinations per square kilometer than this have free parking. It is measured per study from the parking price samples, because free and paid parking coexist at the same densities in car-oriented metros. 0 means no floor.
Travel survey [travel_survey]
Travel survey behind the calibration
nhts
The survey whose mode shares and trip lengths the model is calibrated to. "nhts" uses the national household survey for non-work trips and the Census commute data for work trips. A regional household survey can replace it where it has enough trips.
Basis: NHTS 2017 (trip-length and mode-share structure) and NHTS 2022 (ridehail); ACS journey-to-work; LODES work trip lengths
Mode and destination choice [logsums]
How closely related the mode choices are
AUTO
0.72
NONMOTORIZED
0.72
TRANSIT
1.0
RIDEHAIL
1.0
Mode choice is a nested logit: modes that substitute for each other share a nest. Below 1.0, a traveler who drops one mode in a nest more often switches to the other mode in it. In practice this governs how many displaced bike trips become walk trips rather than car or transit trips.
Basis: ActivitySim prototype_mtc (MTC)
Sensitivity to travel time, by trip purpose
WORK
-0.025
SCHOOL
-0.0224
DISCRETIONARY
-0.0175
ESCORT
-0.0175
utils per minute
How strongly travelers respond to a minute of generalized travel time, separately for work, school and other trips. Larger magnitudes make choices more sensitive to time and cost. These are held at standard practice values, not re-estimated per study.
Basis: Work: FTA New Starts mid-band (national range −0.019 to −0.044); others from ActivitySim prototype_mtc
How much mode options matter in choosing where to go
WORK
0.3
SCHOOL
0.5358
DISCRETIONARY
0.6755
ESCORT
0.6755
NHB_WORK
0.6755
NHB_OTHER
0.6755
NHB_ESCORT
0.6755
The weight that destination choice gives to how easy a place is to reach by all modes combined. Lower values mean travelers are less swayed by travel options when choosing a destination; work trips (0.3) are less swayed than other trips.
Adjustments by trip distance (under 1, 1–2, 2–5, 5–10, 10–20 and over 20 miles) that make the model reproduce observed trip lengths. They start at zero and are fitted for each study; the calibrated values are specific to that study and do not transfer.
Fitted to this study's local data by calibration; the codebase value is only its starting point.
Basis: Calibrated per study to NHTS non-work and LODES work trip-length distributions
Mode preferences beyond time and cost (calibrated)
calibrated48 of 48 values changed
CAR
TRANSIT
BICYCLE
RIDEHAIL
CAR_NONHH
car_owning · WORK
-0.4469
-1.9549
-0.7655
-5.3539
·
car_owning · SCHOOL
-1.2603
-2.7365
-1.6002
-2.688
·
car_owning · DISCRETIONARY
-0.2981
-3.1917
-1.5619
-2.1
·
car_owning · ESCORT
-0.87
-2.7
-3.22
-2.76
·
car_deficient · WORK
-0.3032
-1.6345
-0.7815
-5.3583
·
car_deficient · SCHOOL
-0.8562
-2.067
-1.6161
-2.688
·
car_deficient · DISCRETIONARY
-0.1824
-2.8965
-1.4286
-2.1
·
car_deficient · ESCORT
-1.86
-3.02
-3.18
-3.0
·
carless · WORK
·
-1.1773
-1.7621
-6.0
-4.2524
carless · SCHOOL
·
-2.0463
-2.5968
-2.688
-5.3344
carless · DISCRETIONARY
·
-1.7711
-1.4818
-2.1
-2.9872
carless · ESCORT
·
-1.34
-2.43
-1.01
-2.62
utils
A constant for each mode, by car ownership and trip purpose, that captures everything about a mode that time and cost do not. Walking is the reference. They are fitted for each study so the model reproduces observed mode shares; the codebase values are only starting points.
Fitted to this study's local data by calibration; the codebase value is only its starting point.
Basis: Starting values from ActivitySim prototype_mtc, re-anchored to walk; calibrated per study to ACS commute mode shares and NHTS non-work mode shares
How walking time is weighed
calibrated3 of 3 values changed
SLOPE1
2.02.0
THRESHOLD_MIN
25.025.0
SLOPE2
4.04.0
A minute of walking counts as 2 minutes in a vehicle up to 25 minutes of walking, and 4 minutes beyond that, so long walks drop off steeply. The walk curve is set, not fitted, by default; the long-walk weight was chosen on how well the mapped results read.
Fitted to this study's local data by calibration; the codebase value is only its starting point.
Basis: MTC precedent for the short-walk weight (2× in-vehicle time); threshold and long-walk weight are judgment
How cycling time is weighed (calibrated)
calibrated3 of 3 values changed
SLOPE1
1.01.0
SLOPE2
3.62691.0
THRESHOLD_MIN
39.0
Like the walk curve, but for cycling: beyond a threshold, each extra minute on a bike counts more. The long-ride weight is fitted per study to how bike use falls with distance in the national survey.
Fitted to this study's local data by calibration; the codebase value is only its starting point.
Basis: Fitted per study to NHTS 2017 bicycle mode share by distance
Dollar value of an hour, for reporting benefits
19.4
$ per hour
Converts time-equivalent benefits into dollars in reports. It is the same for everyone, by design, so a minute saved by a low-income traveler counts as much as one saved by a high-income traveler. It does not affect travel choices.
Basis: USDOT value of travel time, 2023 dollars
Overall sensitivity to money costs
1.0
A single multiplier on how strongly travelers respond to fares, tolls, parking and fuel costs. It is left at 1.0. As a result, transit ridership responds to fare changes less than the published average, so pricing scenarios likely understate the response. Each study's calibration report gives its own figure.
Travel diaries miss many short walk trips, so the survey's walk share for non-work trips is raised by this factor before calibration (1.5 = 50% more). It is a judgment call: a national derivation supports about 1.25, and one regional survey supports about 1.5.
Basis: MWCOG Regional Travel Survey 2017/18; national derivation from Stopher & Greaves (2009) and FHWA NHTS 2022 documentation
Regional adjustment of non-work mode shares
On
National survey mode shares are adjusted toward the region's own habits, using how far its commute mode shares depart from the national pattern. A region that commutes by transit more than average is assumed to use transit more for other trips too, at 70% of the commute difference.
Basis: ACS journey-to-work compared with NHTS commute shares
Extra walking in older neighborhoods
run timethe files said true (default)
Off
Car-owning households in neighborhoods with more housing built before 1940 walk more than the model otherwise predicts. This term adds that, capped at half pre-war housing. It moves where walking happens, not the regional walk share, and may partly reflect who chooses to live there.
Basis: ACS tract commute mode shares and housing age (B25034), pooled over five US metros
Emissions [emissions]
Car greenhouse gas emissions per kilometer
0.31
kg per vehicle-km
Kilograms of CO2-equivalent per vehicle-kilometer, counting fuel production as well as the tailpipe. Building vehicles and roads is not counted. A rough national average, worth localizing before reporting to clients.
Basis: EPA, Greenhouse Gas Emissions from a Typical Passenger Vehicle; Argonne GREET (fuel upstream)
Transit greenhouse gas emissions per passenger-kilometer
0.09
kg per passenger-km
One blended value for bus and rail, per passenger-kilometer, with typical occupancy included. A rail-heavy system would be lower (about 0.06) and a bus-only system higher (about 0.12–0.16).
Basis: FTA/APTA transit GHG reporting; US rail per-passenger-km figures
Trip demand [demand]
Induced demand (more trips where access improves)
Off
When on, places whose access improves generate more non-work trips. Off by default: the number of trips stays fixed and only where and how people travel changes.
Basis: DfT TAG Unit M2.1 and RAND PRISM bracket the response used when it is on
Road capacity [functional_classes] · traffic
Freeway lane capacity
1840
vehicles per hour per lane
Vehicles per hour one freeway lane can carry before congestion builds. Set below the textbook ideal to reflect observed urban conditions. Lower capacity means more congestion in traffic assignment. Not yet calibrated.
Basis: HCM 6th Edition (Exhibit 12-4) ideal of 2,250–2,400 per lane, reduced to an urban effective value
Traffic assignment [assignment] · traffic
Traffic assignment
studytrue
On
When on, car trips are loaded onto the road network until travel times and route choices settle (user equilibrium), and congested times feed back into mode and destination choice. When off, car times come from free-flow or observed speeds.
People per car
default
1.7
WORK
1.1
people per car
Average occupants per car trip, used to turn person trips into vehicle trips: 1.1 for work trips and 1.7 for everything else.
Basis: NHTS-derived
What each scenario changes
Values on this page are the control
scenario's (the control).
Other scenarios differ only where listed.
baseline baseline
Changes no parameter.
popgrowth15 not built yet
Not built yet, so it has no parameter files of its own; no scenario changes can be listed.
freq6x not built yet
Not built yet, so it has no parameter files of its own; no scenario changes can be listed.
All settings
Every key of the parameter files, in file order, with the
value the run used and the codebase file's own comment.
4 settings that hold a file path
are not shown.
analysis_parameters.toml Analysis parameters
[census_geometries]6 keys
zones: Census Geometries
Key
Effective value
Set by
TOML comment
GEO_LEVELS
["block", "block_group", "tract"]
default
—
WATER_THRESHOLD_PROPORTION
0.05
default
—
WATER_MIN_FEATURE_FRACTION
0.001
default
—
WATER_MIN_FEATURE_KM2_CAP
0.1
default
WATER_MIN_FEATURE_FRACTION is a fraction of the study area, so the derived
floor scales with the study — and on a large study it rises above the size of
the urban waterways that matter most for zone geometry. We measured this on a
large, heavily congested metro: a 5,636 km² study area derives a 5.6 km²
floor, and two waterways of 2.4 km² and 0.86 km² running through the dense
core were therefore kept as land, roughly halving the activity density of the
zones fronting them. Those densities feed parking cost and search time and the
density-class constants, so the error does not stay in the geometry.
The cap below bounds the derived floor with an absolute feature size, so a
large study still clips major rivers and basins while ponds and drainage
ditches stay as land. On that study's TIGER AREAWATER features a 0.1 km² cap
retains 9 of 141 features as water. Set <= 0 to disable the cap and use the
fraction-derived floor alone.
Zone geometry is built in zones, so changing any of these moves a study
only on a full rebuild.
WATER_FRAGMENT_MIN_RATIO
0.025
default
—
GEOID_LEN
tract
11
block_group
12
block
15
default
—
[h3]1 key
zones: H3 hexagonal tiling
Key
Effective value
Set by
TOML comment
SLIVER_HANDLING
"merge_to_neighbor"
default
How to handle the small "sliver" gap fragments that arise at boundaries
between H3 cells of different resolutions (a numerical artifact of H3's
independent per-resolution boundary math).
"merge_to_neighbor" (default): each sliver is unioned into the existing
zone with which it shares the longest boundary. Lowest zone count,
small per-cell shape distortion.
"standalone_zone": each sliver becomes its own analysis zone with a
synthetic ID (`gap_NNNN`) and `h3_resolution = NaN`. Keeps every H3
cell geometrically pure (clean hex) at the cost of a few hundred
tiny extra zones per study area.
[people]7 keys
people: People
Key
Effective value
Set by
TOML comment
NUM_INCOME_BINS
5
default
—
INCOME_BASIS
"household"
default
"per_capita" or "household" (default)
ACS_YEAR
2022
default
ACS5 vintage; pinned across Census API and NHGIS fallback for reproducibility
EMPLOYER_PARKING
true
default
── Employer-paid parking (user-class attribute) ────────────────────────────
When true, each car-owner user class splits into `employer` / `noemployer`
variants (nocar rows carry the inert sentinel "na"). The `employer` class
receives a parking-cost discount: under PARKING_CHOICE the discount applies
in the evaluation fold to WORK-purpose trips only (full 1 − subsidy — employer
parking is free at your workplace, not on a shopping trip downtown); on the
legacy additive path it is baked into routing cost, weighted per destination
by the zone's work_share (work_trip_ends / total_trip_ends) as a purpose
proxy, with override zones (e.g. an event venue) never discounted. This models
the fact that many downtown commuters park free through employer-provided,
validated or bundled-lease parking, which a flat transient-rate model
overstates. Enabled by default; set false to fall back to the flat
transient-rate model.
See docs/modelers/parking-and-hassle.md.
EMPLOYER_PARKING_SHARE_BY_BIN★
[0.35, 0.5, 0.6, 0.7, 0.8]
default
Population share (car-owning workers with employer parking) per income bin,
ordered low→high income; length should match NUM_INCOME_BINS (a scalar applies
to every bin; a mismatched list is clamped by index). Used only to split the
population between the two classes; the aggregate view reproduces
transient × (1 − share × work_share).
These levels are a declared assumption, not a measurement. They were adopted
for a study of a large polycentric multi-county region and have never been
checked against an employer-benefits or household survey. Treat them as a
study-level input to set, not a default to inherit.
EMPLOYER_PARKING_SUBSIDY
1.0
default
Fraction of parking the employer covers at a work destination (weighted by
work_share on the legacy path). 1.0 = fully free at work, which is set by
engineering judgement: employer parking that is provided at all is normally
provided in full rather than part-subsidised.
CYCLIST_DISTRIBUTION★
bike_lts4
0.04
bike_lts2
0.09
bike_lts1
0.51
bike_lts0
0.36
default
Cyclist population shares by Level of Traffic Stress tolerance. Each key maps
to one bike routing graph (bike_lts{1,2,4}); bike_lts0 = will not bike.
Weights must sum to 1.0. Zero-weight levels are dropped — no user class or
routing graph is built for them. Override per-study to change the bike model
(e.g. set bike_lts1 = 1.0 and the rest 0.0 to assume everyone is a cautious,
protected-infrastructure-only cyclist).
Source: Dill & McNeil (2016), "Revisiting the Four Types of Cyclists:
Findings from a National Survey", TRR 2587 — strong & fearless 4%, enthused
& confident 9%, interested but concerned 51%, no way no how 37%. The
published rounded shares sum to 101%; bike_lts0 absorbs the rounding
residual so the riding groups match the survey exactly.
bike_lts0 will not bike
[destinations]8 keys
destinations: Destinations
Key
Effective value
Set by
TOML comment
OVERTURE_CONFIDENCE_THRESHOLD★
0.85
default
Minimum Overture Places confidence for a POI to count as a destination.
Applied once, in populate_destinations_overture_places, to
places_gdf["confidence"].fillna(0); everything below it is dropped before
classification and downstream consumers deliberately do not re-filter. It
therefore sets the entire destination inventory for every Overture-backed
category — 8 of the 14 below (SCHOOL, CHILDCARE, HEALTHCARE, EAT_DRINK, SHOP,
RECREATION, EXERCISE, RELIGIOUS_OR_COMMUNITY).
0.85 is a judgment cut. No sweep is recorded and no retained/dropped counts
are on file, so nothing establishes it against the 0.5–0.9 range that is
equally defensible — and the choice materially changes zonal POI counts.
WHYTO 10 (escort — "drop off / pick up someone") is NOT in this exclusion
list: it is carried as a first-class ESCORT category. It is 7.9% of hour-8
trips, the single largest destination class that would otherwise go
unmodeled, and the morning school run is the anchor 82% of non-home-based
escort legs depart from. Note that the generation-rate denominator
(`total_weighted_trips_in_hour`) is taken BEFORE this exclusion, so admitting
a code adds its own category without moving any other category's rate.
ESCORT_ATTRACTOR_WEIGHTS
overture_school
0.78
overture_childcare
0.03
census_total_pop
0.19
default
Escort destination proxy attractors. You do not travel to an "escort" — you
drop someone at THEIR destination — so escort has no destination class of its
own. Its trip-ends are allocated over a weighted blend of these columns, each
normalized to a share of its own regional total first (the columns are in
different units — POI counts vs persons — so the weights are shares of escort
trip-ends, not raw multipliers).
The weights come from the escortee's own trip purpose, matched through the
NHTS joint-travel fields at a 37% match rate. They assume matched and
unmatched escorts share a purpose mix, and that assumption has not been
tested. They are the weakest number in this block, and the honest statement
is that they are UNVALIDATED: a count validation was intended as their test,
and no result from it is recorded anywhere. Do not quote them as measured.
ESCORT ESCORT has no destination of its own — you drop someone at THEIR destination —
so its size term is the proxy-attractor blend built in populate_destinations
from ESCORT_ATTRACTOR_WEIGHTS. See that key for the weights and their basis.
AIRPORTS
meeter_greeter_multiplier
1.0
enplanement_year
"latest"
default
Airports bypass the NHTS-derived trip-ends path. Trip-ends are computed
directly from FAA annual enplanements distributed by a static per-airport
hourly shape (TSA pre-COVID throughput). See
connectome/setup/populate_destinations.py:_apply_airports_trip_ends.
meeter_greeter_multiplier Multiplies passenger boardings to account for non-passenger airport trip-ends
(drop-offs, pickups, well-wishers). 1.0 = boardings only, i.e. the effect is
deliberately switched OFF by default rather than sized. No study-specific
value is recorded. For scale, airport ground-access studies commonly report
something like 1.2–1.6 ground trips per enplanement (a reading of that
literature, not a measurement made here) — so a study that cares about
airport access should set this rather than inherit it.
enplanement_year Year of FAA enplanement data to use. "latest" picks the largest year present in
faa_enplanements.csv.
EVENT
(empty table)
default
Events (stadiums, concerts, conventions) bypass the NHTS path too: a one-off
attendance surge isn't in NHTS averages. event_trip_ends is taken directly
from the per-zone `event_attendance` column (attendees arriving during the
analysis period), which is 0 everywhere by default and is populated per
scenario by the `add_event` mutation (see docs/developers/mutations.md). No
static knobs — the section exists to document the category alongside AIRPORTS.
[analysis_areas]2 keys
Key
Effective value
Set by
TOML comment
MIN_POPULATION_PER_GEOM
75
default
drop_sparse_zones retention bars. A zone is dropped only when it falls below EVERY
enabled bar — clearing either one retains it, so an employment-only zone (a
port, a mall, a refinery: zero residents, real destination mass) survives on
trip-ends alone, and a large sparse-residential hex survives on population
alone. 0 disables a bar (it then neither drops nor rescues; both 0 skips the
stage). Dropped zones' people + destinations snap to the nearest neighbor.
min census_total_pop to retain on population alone
MIN_TRIP_ENDS_PER_GEOM
5
default
min total trip-ends to retain on destinations alone
[physical_conditions]5 keys
physical_conditions: Physical Conditions
Key
Effective value
Set by
TOML comment
GTFS_MIN_OVERLAP
1e-05
default
min fraction of a feed's bbox that must overlap the study area
ROUTING_AREA_BUFFER_FROM_ANALYSIS_ZONES_M
5000
default
Buffer around the analysis zones that defines the routing area, i.e. the
region OSM and GTFS data are pulled for. The margin below is measured from
this, and its sweep was run at this value. 5000 m has no recorded derivation
of its own — it is a judgment call about how far outside the zone system a
modelled trip may legitimately route. Note that `boundary_sources.py` carries
the same 5000 m as a separate `buffer_m`, and nothing states that the two are
meant to track each other.
buffer around analysis zones for OSM + GTFS routing data
ROUTING_AREA_CLIP_MARGIN_M
2500
default
how far PAST the routing area the OSM extract is cut (the
osm_download step). The routing area and the gateway ring do
NOT move — only the extract widens. Nonzero because a cordon corridor
needs a routable junction node OUTSIDE the ring for
`detect_gateways` to see a crossing and for external demand
to attach to; clipping flush against the ring leaves some
corridors with both graph nodes inside it, and osmnx
simplification then collapses the whole outward excursion
into one interior-to-interior edge. Measured on a mid-size
metro with tolled facilities and a congested core: at a
10 m margin, 3 of 63 cordon corridors are lost that way
(including one secondary-class corridor with 48.6 km of
network behind it), and a divided freeway's carriageways
split into two one-sided gateways. The value is derived
from its own stopping rule — the smallest margin that
zeroes corridor loss — NOT from a percentile of the
outer-node overshoot (median 338 m but max 6,996 m on that
study; the tail is long and buying it is pointless). Sweep
on the same study, gateways lost against an unclipped
control: 10 m -> 4 lost + the freeway split; 1000 m -> 1
lost; 2000 m -> 1 lost; 2500 m -> 0; 3000 m -> 0. 1000 m
already un-splits the freeway. The cliff between 2000 and
2500 is explained independently: the lost secondary's
outer node sits 2,288 m beyond the ring. Raise it only
against a re-measured sweep; `setup.ring_crossing_reference`
gates whatever is left.
AIRPORT_REP_POINT_QA
false
default
Report-only airport rep_point QA. Default off — normal runs pay zero cost.
When true, physical_conditions setup writes
input_data/airport_rep_point_qa.gpkg, the human-review surface for setting
airport_rep_point_overrides.csv. Because the QA runs at the end of
physical_conditions(), flipping this on for a study with a cached
physical_conditions has no effect until setup re-runs
(force_from_stage="physical_conditions").
The OSM highway classes that count as the car network. Read everywhere the
road network is defined — speed conflation, the car routing graph, and
coarse-zone simplification — so this list is the car network, not a filter
applied to one of them.
The membership rule is judgment, not a derived cut, and nothing is recorded
about what the exclusions cost. `service` and `living_street` are excluded as
off-network access ways. The consequential exclusion is `unclassified`:
in much of rural and small-town USA `unclassified` IS the through road, so a
study outside a large metro can lose real connectivity here. That is a named
limitation for exactly the study type an MPO outside a large metro would run,
and it has not been measured.
[representation]1 key
perceived_conditions: Representation
Section notes (1)
HAS_TOLL_ROADS: set to false to skip toll road detection; omit (or comment out) to auto-detect.
HAS_TOLL_ROADS = false
Key
Effective value
Set by
TOML comment
TOLL_EXEMPT_FACILITIES
[]
default
—
[routing]45 keys · 2 overridden
routing: Routing
Key
Effective value
Set by
TOML comment
ROUTING_MODES
["CAR", "TRANSIT", "WALK", "BICYCLE"]
default
—
transit_ttm_stash
true
default
Reuse a transit routing across scenarios whose transit inputs are provably
identical, instead of re-running the decomposition per cell. Keyed on content
(feeds, OSM, rep points, r5 params, resolved departure, the walk price, and the
r5py revision); store lives at {study}/.transit_ttm_stash and survives re-runs.
Worth ~35-58 min on an 8-cell scenario ladder in a large metro, where 4 cells shared one
network. NOTE this changes numbers slightly on purpose: R5's path enumeration
is not bit-reproducible, so sharing one routing replaces N independent draws
with one -- which removes differential noise between twin cells rather than
adding any (bounded by the drift in docs/developers/gotchas.md: <=0.17% on
transit access metrics, <=0.0011% on total access). Set false to route every
cell independently.
DERIVED_MODES
["RIDEHAIL", "CAR_NONHH"]
default
—
WORKING_HOURS_PER_YEAR
2080
default
—
VOT_DIVISOR★
2
default
value-of-time = hourly_wage / VOT_DIVISOR (at the reference income)
VOT_INCOME_ELASTICITY★
0.5
default
Cross-sectional (inter-personal) income elasticity of VOT: VOT ∝ income^p,
anchored so that at VOT_REFERENCE_INCOME the map equals the linear one
(wage/VOT_DIVISOR). p=1.0 recovers that linear map exactly. p≈0.5 is the
meta-analytic central estimate (Wardman 2001; Börjesson & Eliasson 2014;
Binsuwadan et al. 2023). NB USDOT's income elasticity of 1.0 is *inter-temporal*
(VOT growth as incomes rise over years) and is not this quantity — do not
substitute one for the other.
VOT_REFERENCE_INCOME
80610
default
Anchor income. At income == this, VOT = hourly_wage/VOT_DIVISOR. Default
is the 2023 ACS US median household income; at it VOT ≈ $19.4/hr ≈ USDOT's 2023$
personal value of travel time — an external sanity check on the level.
TRANSIT_FLAT_FARE★
2
default
Flat transit fare, in dollars per ride. Applied as one scalar in every cell of
tcm_TRANSIT and converted to minutes at each user class's own value of time.
No source or vintage is recorded for $2, and none is reconstructible from the
tree. Note what the form can and cannot do: because it is a single scalar it
cannot differentiate any OD pair — no distance banding, no zone system, no
transfer or day-pass logic — so its only effect on the model is a per-income
-class level shift on transit. A study with real fare policy should replace
the value; a study that wants fare STRUCTURE needs more than this key.
$/ride
TRANSIT_OVT_MULTIPLIER_WAIT★
2.0
default
Out-of-vehicle transit time (OVT) multipliers — how much more onerous a minute
of waiting / walking-to-stop / walking-from-stop / transferring is than a minute
in the vehicle. Tier-2 frozen (transferred national defaults, not calibrated).
Consumed by generalized_impedance (generalized_impedance.py), which adds
the surcharge
HTM_TRANSIT = Σ_c (β_c − 1)·component_c over {wait, access, egress, transfer}
to gtm_TRANSIT, on top of the p50 clock matrix. Setting every β to 1 makes the
surcharge identically zero, so transit reduces to the p50 clock matrix alone.
The shipped values are the uniform 2.0 of US practice: MTC Travel Model One,
SFCTA CHAMP and the TCRP synthesis all weight every out-of-vehicle component
at 2× in-vehicle time.
The documented alternative is a per-component best-evidence set — wait 1.75 /
access 1.6 / egress 1.6 / transfer 1.9 — from Wardman, Chintakayala & de Jong
(2016) and the 2024–26 worldwide meta-analysis (2,872 multipliers, 606
studies, 52 countries). That literature puts the central walk/wait multiplier
nearer 1.5–1.8 than 2.0 and explicitly challenges the factor of two; the
ordering it supports is transfer ≥ wait ≥ egress ≈ access ≥ 1.
We ship the uniform 2.0 anyway, and the reason is internal consistency rather
than the US-practice provenance above: 2.0 is the WALK factor. Out-of-vehicle
transit time IS walking and waiting, so it is priced the way this model
already prices walking — LOGSUM_WALK_KINK's below-threshold slope SLOPE1.
That makes SLOPE1, LOGSUM_WALK_COMFORT_RATE and TRANSIT_OVT_MULTIPLIER_* one
numeraire, and it is a commitment, not a convention: moving SLOPE1 without
moving these prices the same minute two ways depending on which mode it is
booked to. PARKING_SEARCH_ONEROUSNESS below is the same numeraire.
If you retune to the per-component set, retune the walk side with it, and
preserve the ordering above.
TRANSIT_OVT_MULTIPLIER_ACCESS
2.0
default
—
TRANSIT_OVT_MULTIPLIER_EGRESS
2.0
default
—
TRANSIT_OVT_MULTIPLIER_TRANSFER
2.0
default
—
TRANSIT_TRANSFER_PENALTY_MARGINAL_MIN★
[5.0, 10.0, 30.0]
default
Fixed per-transfer penalty (equivalent in-vehicle minutes) — the disutility of
the *act* of interchanging, on top of the extra walk/wait time a transfer adds
(which the multipliers above already price). MARGINAL schedule: minutes for the
1st, 2nd, 3rd… transfer; the penalty for n transfers is the running sum. With
any β≠1 or a nonzero penalty, generalized_impedance selects the
minimum-generalized-cost path template per OD (so the penalty steers which
path is priced), instead of reweighting a mean over templates. Set to [] to disable; then β=1 everywhere
leaves gtm_TRANSIT equal to the p50 clock matrix.
The schedule is MTC Travel Model One's. It sits inside the TCRP US planning
range of ~5–15 equivalent in-vehicle minutes per transfer, and the ~13–18
minutes recent international studies converge on for multi-transfer trips.
TRANSIT_SUBMODE_CLASSES
BUS
[3, 11, "200-209", "700-716", 800]
TRAM_LRT
[0, 5, "900-906"]
METRO
[1, 12, "400-405"]
RAIL
[2, "100-117"]
FERRY
[4, 1000, 1200]
OTHER
[6, 7, "1300-1307", 1400, 1500, 1700]
default
---------------------------------------------------------------------------
Transit submode differentiation
---------------------------------------------------------------------------
Which GTFS route_type codes constitute each submode class. Entries are ints or
inclusive "lo-hi" range strings; every code must belong to exactly one class,
and any code not listed falls into OTHER (priced at the numeraire). The ranges
cover GTFS "extended route types" as well as the basic 0–7/11/12 set, because
real feeds use both. Consumed by connectome/transit_submodes.py.
BRT is deliberately absent: it is not expressible in GTFS route_type (a BRT line
is a 3 like any other bus, and the extended 700-series is used too inconsistently
to lean on). A study wanting a BRT class must tag its routes explicitly.
TRANSIT_IVT_MULTIPLIER★
BUS
1.0
TRAM_LRT
0.95
METRO
0.92
RAIL
0.88
FERRY
1.0
OTHER
1.0
default
Per-submode-class perception weight on IN-VEHICLE time, with car IVT ≡ 1 as the
framework numeraire. generalized_impedance adds Σ_k (w_k − 1)·ivt_k to each
template's generalized cost, so the weights steer *which* path is selected as well as what
it costs. All-1.0 reproduces the submode-blind matrix bit-for-bit.
Values are the midpoints of evidence-derived bands (see
docs/modelers/methodology/transit-impedance.md). The identifying evidence is
Wardman, Chintakayala & de Jong (2016) Table 7 — the only VOT meta-analysis that
separates the MODE-VALUED effect (how one traveler prices a minute in each
vehicle) from the MODE-USED effect (who rides it; income self-selection). Their
within-study urban ratios against car: bus 1.04 (n.s. from parity), train 0.88,
metro 0.94 (thin n=13), train/bus 0.76 — an ordering of train < metro < car ≈ bus.
Bus therefore sits AT the numeraire and the differential lands on the rail side;
that is evidence-consistent, not merely a compatibility convenience.
The band is deliberately modest (≥0.85). Ben-Akiva & Morikawa (2002) find most of
the apparent rail premium proxies measured service quality rather than an
intrinsic preference, so a perception weight should capture only the residual
ride-quality/reliability signal. These must stay far from MTC's 0.75 or SFCTA's
0.2 "target submode" weights, which are path-FORCING devices, not values.
TRAM_LRT is interpolated (no direct LRT evidence) and FERRY has none in either
direction, so it stays at the numeraire — argue it per study if it matters.
BUS_EXCLUSIVE_ROW_CORRIDORS
[]
default
Hand-maintained exclusive-bus-ROW corridors, read by Tier 3 of the bus-speed
coupling ([assignment.feedback.transit_coupling] level = "route"). Edges on
these streets are treated as congestion-invariant: they contribute to a
route's matched length but not to its theta, so a jam on the general-traffic
lanes does not slow the bus. Empty by default — most studies have none, and
grade-separated busways need no entry at all (highway=busway/bus_guideway is
outside CAR_WAY_TAGS, so it never reaches the car graph and falls out as
unmatched, i.e. theta = 0, for free).
This list SUPPLEMENTS OSM busway=lane / lanes:bus tags, which
transit_shape_match reads from the study's own PBF. OSM alone is thin: DC has
208 tagged ways / 20.1 km against its whole bus network, and 16th Street NW —
the city's flagship bus lane — is absent.
⚠️ Entries are matched case-insensitively against the edge's WHOLE OSM name
(any one component of a merged multi-name edge), NOT as a substring. Write
the full OSM spelling: "16th Street Northwest", not "16th St NW". A substring
match is unusable on real data — "H Street Northwest" is a substring of 4th,
8th and 16th Street Northwest and masks 316.9 km of DC instead of 6.7. An
entry matching no edge logs a warning, because it is otherwise a pure no-op.
⚠️ The mask is whole-street: naming a street zeroes theta along its entire
length, not just the lane's extent. Acceptable for a second-order correction;
say so when a result leans on it.
BUS_SPEED_FACTOR
1.0
default
Bus-speed/congestion coupling, Tier 1: a static multiplier on perceived bus
in-vehicle time (composes with TRANSIT_IVT_MULTIPLIER's BUS weight above,
rather than replacing it). 1.0 is the identity — no scenario sets this
directly; it is written per-scenario by the `bus_speed_factor` mutation.
CAR_FUEL_COST_PER_MILE★
0.13
default
Car operating cost ($/mile) x network miles on the routed path (the `dist_CAR`
component routing writes beside `ttm_CAR`, accumulated along the same
predecessor tree). The per-mile rate is ALL-IN: the sum of the three additive
components below (`analysis_parameters.car_operating_cost_per_mile`). That is
SACSIM's basis (fuel + AAA maintenance & tires) and the usual US MPO one.
Depreciation, insurance and registration are excluded on purpose: they are
per-year, not per-mile, and belong to car ownership, not trip choice.
Fuel, including fuel taxes. AAA "Your Driving Costs 2025" (regular gas at
$3.151/gal, the national average). Regionalise it at the study tier with the
ratio of the state's EIA annual retail price to $3.151 — retail includes tax.
https://newsroom.aaa.com/wp-content/uploads/2025/09/UPDATE-AAA-Fact-Sheet-Your-Driving-Cost-9.2025-1.pdf
CAR_MAINTENANCE_COST_PER_MILE
0.1104
default
Maintenance, repair and tyres. Same AAA source (5 yr / 75,000 mi, new vehicle).
The on-road fleet is older than AAA's, so this is probably a floor.
CAR_PER_MILE_FEE
0.0
default
Road-user / mileage charges and other per-mile policy fees. None by default;
kept separate from fuel so fee dollars stay separable.
CAR_EST_MILES_PER_MINUTE
0.75
default
Fallback only, and reachable in one case: a study whose routing ran before the
distance accumulator existed and so has no `dist_CAR` on disk. Then operating
cost is priced off travel TIME at this implied speed (0.75 mi/min = 45 mph
everywhere), which makes congested urban travel look dearer per mile and
uncongested travel cheaper. Re-route with force_from_stage="routing" to leave it
behind; `components.json` -> formula_components records which basis was used.
rough estimate; only used when no distance matrix exists
PARKING_SEARCH_TIME_MAX_MIN★
6.0
default
Parking cost is modular — see [parking] section below.
Parking time model (added to CAR HTM, then to GTM):
time = (PARKING_SEARCH_TIME_MAX_MIN + PARKING_WALK_TIME_MAX_MIN)
* clip((density / rho_ref) ^ [parking.density].density_power, 0, 1)
PARKING_TIME_DENSITY_COLUMN controls what density signal drives time:
"combined" → (census_total_pop + total_trip_ends) / area_km² [recommended]
captures both resident demand (fills street spots) and visitor demand
"trip_ends" → total_trip_ends / area_km² (same as parking cost)
"jobs" → lodes_jobs_per_sqkm
"population"→ census_total_pop / area_km²
PARKING_TIME_DENSITY_REF_PERCENTILE should be lower than
[parking.density].density_ref_percentile so that urban-residential zones —
not just CBD extremes — receive meaningful parking time values. Default 95:
top 5% of zones reach full time penalty.
max cruising/search time at ref density (min)
PARKING_WALK_TIME_MAX_MIN
3.0
default
max walk time from spot to destination (min)
PARKING_TIME_DENSITY_COLUMN
"combined"
default
see comment above
PARKING_TIME_DENSITY_REF_PERCENTILE
95
default
percentile used as time-scaling reference
PARKING_SEARCH_ONEROUSNESS
2.0
default
PARKING_SEARCH_ONEROUSNESS weights the search (cruising) leg into *perceived*
minutes before it enters generalized time; the walk-from-spot leg is ordinary
walking and stays at 1.0. Cruising is unproductive, uncertain time — the same
perception family as the transit out-of-vehicle components above, and 2.0
equals the uniform transit OVT multiplier, keeping one out-of-vehicle weight
across modes (the traditional 2x weighting of long-standing US mode-choice
practice, FTA guidance lineage; Wardman-meta wait/search range ~1.5–2.5);
parking-search SP work likewise values search time above
in-vehicle time (Axhausen & Polak 1991, Transportation 18). Tier-2 frozen,
not calibrated. 1.0 restores clock-time behavior. Applied on
both arms: baked into the routing htm_CAR on the legacy path
(force_from_stage="routing" to change it there), applied at
generalized_impedance under
PARKING_CHOICE (marker-invalidated, re-derives without re-routing).
PARKING_CHOICE
true
default
--- Paid-vs-free parking choice logsum -------------------------------------
With PARKING_CHOICE = true, parking stops being an additive cost+time penalty
and becomes a two-alternative discrete choice per destination zone, evaluated
in generalized minutes:
t_park_local(f) = search(f) + walk_own(f) # today's parking_time(f)
free_zone(z) == cost_base(z) <= PARKING_FREE_COST_THRESHOLD
V_free(z) = t_park_local(z) if free_zone(z)
min over free f of [ t_park_local(f) + walk(f→z) ] otherwise,
with walk(f→z) capped at PARKING_FREE_WALK_TIME_MAX_MIN
V_paid(z) = cost(z) * minutes_per_dollar + PARKING_GARAGE_WALK_TIME_MIN
available ONLY where NOT free_zone(z) — no paid supply, no garage
parking_gmin(z) = -(1/mu) * ln( sum_a exp(-mu * V_a) ) over available alternatives
A free zone parks locally (the min is restricted to f = z there — you do not
shop your neighbors for free parking you already have), so its parking_gmin is
today's search + walk apart from the removed cost_fraction term. The change is
therefore confined to zones with paid parking, which is what makes the A/B
partition test sharp. Gating V_paid on paid supply is the other half of that:
left ungated, cost = 0 gives V_paid = PARKING_GARAGE_WALK_TIME_MIN and every
free zone in the study would collapse to ~1.5 min, deleting the density-based
search model outside the paid core.
The cost_fraction search-time suppression in parking/time.py is REMOVED under
PARKING_CHOICE — the logsum now carries the cost-vs-time tradeoff that knob
approximated. Setting PARKING_CHOICE = false restores the legacy additive
model, cost_fraction and all, byte-identically.
See docs/modelers/parking-and-hassle.md.
false = legacy additive parking model
Walk from a structured/paid space to the door (min). A judgement call for a
paid garage: no measurement or source stands behind 1.5. For orientation, the
free-side walk literature reports roughly 250 m ≈ 3 min; a structured space
roughly halving that is plausible, and that is the whole argument.
PARKING_FREE_WALK_TIME_MAX_MIN
20.0
default
Cap on the BETWEEN-ZONE perceived walk leg (min). NOTE the basis: this is
rep-point-to-rep-point perceived walk time, NOT the door-to-parked-car walk the
parking-search literature reports (~5-10 min tolerated, even in CBDs). Those
are different quantities, and confusing them makes the cap structurally inert.
On the bundled example study — H3 zones with a median area under 1 km² —
adjacent zone centroids are ~14+ perceived minutes apart and only ~1% of all
zone pairs fall under 15 min, so a 10-min cap can essentially never be
satisfied by ANY neighbouring zone: every paid zone loses its free
alternative, p_paid pins at 1.0 for every income class, and the whole choice
model goes inert. 20 min on this basis is roughly 8-12 min of actual walking,
i.e. the literature range. The principled fix is to subtract an edge
adjustment per endpoint so the cap means what the literature means, and then
10 would be right. Known limitation: the value is zone-resolution-dependent,
and the bundled example study cannot calibrate it — nearly all of its zones
are free and its median posted rate is $0.00. A study with a substantial paid
core is where this gets settled.
PARKING_FREE_COST_THRESHOLD
1.0
default
$/day at or below which a zone counts as free supply. This is a TOLERANCE,
not a price: it exists so a near-zero fitted or tabulated prediction is
classified as free rather than as trivially paid. Any value below the lowest
genuine paid rate in the study behaves identically, which is why $1 is not
argued against $0 or $2 — the choice only matters if a study has real paid
supply priced under a dollar a day.
RIDEHAIL_WAIT_MINUTES
5
default
Ridehail cost model: fare = BASE_FARE + PER_MINUTE * travel_time.
All three levels are judgment. No source, no vintage and no market survey is
recorded for any of them, and they are not study-specific.
Two structural properties matter more than the levels, and both are model
form rather than parameter choice:
* There is NO per-mile term. The fare is a base plus a charge on TIME
alone, so a long fast trip and a short slow trip of equal duration cost
the same, and a 30-minute trip costs $30 in fare on top of the $5 base.
The per-minute rate is charged on IN-VEHICLE time only —
RIDEHAIL_WAIT_MINUTES is not metered, and reaches utility as time
(through ttm_RIDEHAIL and LOGSUM_RIDEHAIL_WAIT_MULTIPLIER) rather than
as fare. Real ridehail pricing is a time-and-distance blend plus surge.
* Wait time has no supply-side variation: the same 5 minutes applies in
the densest core and the outermost exurb, where real waits diverge
sharply. Nothing in the model makes ridehail scarcer where it is scarce.
A study for which ridehail is load-bearing should treat all three as inputs
to set, and should know that the functional form is the bigger limitation.
average pickup wait
RIDEHAIL_BASE_FARE
5
default
base fare ($)
RIDEHAIL_PER_MINUTE★
1
default
per-minute charge ($/min)
CAR_NONHH_SEGMENTS
["carless"]
default
--- CAR_NONHH: car travel without a household vehicle -----------------------
A derived mode (like RIDEHAIL) for user classes with no household car: being
driven and dropped off, riding along, borrowing, or a business-only company
car. A routing pass derives per-UC ttm/tcm_CAR_NONHH from an income-matched
car-owning donor class's persisted ttm_CAR (donors span the tolled routeenvs,
so the pass runs after the whole CAR mode iteration); generalized_impedance
folds the gtm like any other mode. Only user classes whose ASC segment is listed in
CAR_NONHH_SEGMENTS get the files.
Cost model: the traveler is predominantly a PASSENGER, so they bear no
parking cost and no parking search time (neither is in ttm_CAR — parking
cost lives in tcm_CAR, which this pass never reads; parking time is the
htm_CAR overlay, keyed by mode name so it never attaches to CAR_NONHH), and
a share of operating cost. Operating cost is computed on the UNDETOURED
donor ttm (cost of the direct route; the detour factor is time-only). The
donor's toll dollars are carried across as their own cost component.
Do NOT read B08141's drove-alone/carpooled split as a driver/passenger
split. The ACS Subject Definitions entry for "Private Vehicle Occupancy"
states that "Drove alone" includes "people who were driven to work by someone
who then drove back home or to a non-work destination" — so a drop-off
passenger is coded as having driven alone, and for a zero-vehicle-household
population that category is close to systematically mislabeled. Only the
combined car share is a usable target.
https://www2.census.gov/programs-surveys/acs/tech_docs/subject_definitions/2023_ACSSubjectDefinitions.pdf
ASC segments that get the mode
CAR_NONHH_OPERATING_COST_SHARE
0.5714
default
Passenger's share of operating cost. 1/1.75 = MTC costShareSr2
(tour_mode_choice.yaml), the divisor MTC applies to shared-ride parking and
tolls. A stated assumption.
CAR_NONHH_DETOUR_FACTOR
1.15
default
Multiplicative detour/scheduling factor on in-vehicle time. A FLAT penalty in
minutes would be exactly absorbed by the calibrated ASC (V = c·t + ASC, so a
constant Δt shifts V by the constant c·Δt); only a time-proportional term
carries information the ASC cannot. Between 1.0 (no detour) and MTC's
TNC_shared_IVTFactor of 1.5 (pooled service among strangers).
The bracket is argued; the value inside it is not. Nothing derives 1.15
rather than 1.10 or 1.25 — all of them are defensible readings of "a
household member goes out of their way for you, but less than a pooled ride
among strangers does." Judgment, stated as such.
DEPARTURE_HOUR
8
default
hour of day for routing (0–23); AM peak default
SPEED_WALKING_M_S★
1.2
default
Pedestrian routing speed. This is NOT only a graph edge weight: it also
defines the clock leg of the two-layer walk split (perceived time is priced
separately from it) and the intrazonal walk diagonal, so moving it moves
walk access at every distance, not just on the network.
1.2 m/s is a standard pedestrian engineering value in the same family as the
design walking speeds used in US traffic-engineering practice, adopted as a
convention. No specific edition, table or study is cited for it here.
4.4 km/h
SPEED_BIKING_M_S★
4.17
default
Bicycle routing speeds — used to compute bike_traversal_time_sec edge weights.
Both are judgment: 15 km/h as a comfortable utility-cycling speed, and the
overage speed as roughly "walking the bike" on a segment above the rider's
stress tolerance. Neither is fitted to observed cycling speeds, and neither
carries a source.
15.0 km/h — comfortable cycling speed
SPEED_BIKING_OVERAGE_M_S
0.6
default
slow cycling / walking a bike on stressful segments
LTS_OVERAGE_PENALTY_MIN
0.0
default
fixed time penalty per above-tolerance edge (minutes)
qa
min_finite_fraction
0.0010.5
max_bad_rowcols
250
on_failure
"warn""raise"
studyanalysis_parameters.toml3 of 3 values changed
Post-write QA on freshly computed raw TTMs. Runs only on writes (not on cache
loads). Catches silent corruption — the failure it exists for is an r5py cache
that builds a network with effectively no connectivity, producing a matrix
whose off-diagonal cells are almost all infinite and propagating it unflagged.
min_finite_fraction Off-diagonal finite-fraction floor below which a raw TTM is considered corrupt.
Tune downward for sparse rural studies. For scale: a corrupt walk matrix we
caught this way came in at a finite fraction of 0.0009 — 99.91% of its
off-diagonal cells were infinite — against the 0.50 default.
Note that this means the threshold is NOT derived from the failure it catches:
the recorded case sits three orders of magnitude below it, so anything in
0.01–0.5 would have caught it identically. Nor is there a reference point for
a legitimately low finite fraction on a sparse rural study, so an operator
lowering this value is doing so blind.
max_bad_rowcols Max number of origins (rows) or destinations (cols) allowed to be entirely
unreachable. 0 = strict (any fully-isolated zone fails QA).
on_failure Behavior on QA failure (TRANSIT path only — non-transit modes raise on failure regardless):
"warn" → log and proceed with the TTM as-is
"retry" → clear r5py cache, rebuild network, re-route; if QA still fails, warn and proceed
"raise" → retry once (as above); if QA still fails, raise TTMQualityError
Default raise, so silent r5py cache corruption is loud rather than silent.
─── physical_conditions: Coarse-zone routing-network simplification ───────
KNOWN HACK. For h3_layered geography mode with mixed fine/coarse cells:
drops residential-grade ways outside a buffer around fine-resolution
zones, keeping only a mode-specific allowlist elsewhere. Avoids physical_conditions
OOM on studies whose OSM extract spans a large area. Trade: routing bias
inside coarse zones is upward (longer paths), worst for walk, less for
bike, least for car. See docs/developers/gotchas.md before enabling.
keep_outside_buffer_car Car keeps `residential` — it is the lowest drivable tier, so dropping it
disconnects any coarse zone whose only road access is residential (car has
no footway/path fallback the way walk/bike do). This makes car simplification
effectively a no-op; that is intentional — car was never the physical_conditions OOM
source, walk/bike are. Tighten this only if you accept disconnected zones.
DEPARTURE_TIME_WINDOW_MIN
60
studyanalysis_parameters.tomlno codebase default
—
[pedestrian_impedance]12 keys · 1 overridden
Pedestrian comfort / impedance weighting of the walk network
Per-edge perceived-length multipliers applied to BOTH routers consistently:
the standalone WALK graph (baked at physical_conditions) and the walking legs of
TRANSIT (R5, via Edge.setWalkTimeFactor at routing time). See
connectome/pedestrian_impedance.py. The stack is
walk_sec = base × slope × streetscape + crossing_sec
slope is configured here; the streetscape factors (canopy / building shade
/ interest / enclosure / traffic exposure) and the crossing price are
configured in the [streetscape] section below. A new study gets every factor
and calibrates WITH them on, which is self-consistent. A study opts OUT by
pinning the *_ENABLED flags false in its own analysis_parameters.toml (this
codebase default falls through for keys a study does not set — see
load_analysis_params).
⚠ Turning a factor on or off changes walk cost and therefore INVALIDATES a
study's committed calibrated logsum ASCs: the constants are residuals against
the walk impedance they were fitted under. Change these only alongside a
re-calibration.
The walk graph records what it was built with (meta.walk_build_hash, over
every key here and every STREETSCAPE_* key plus the DEM / PBF / table
hashes). A change to any of them is detected on the next run: rebuilt when
nothing has been routed on the old graph, REFUSED with the differing keys
named once cached WALK matrices exist — then force_from_stage="physical_conditions". Slope
additionally needs a study-level DEM (SLOPE_DEM); with none present it
self-skips with a warning.
The class-keyed ARTERIAL_* and CROSSING_* factors that used to sit in this
section are retired: traffic exposure took the
arterial slot inside the streetscape product and the crossing price moved
onto the streetscape junction table. A study TOML that still sets one of
those keys gets a WARNING naming the replacement; the key itself is inert.
Section notes (2)
Optional cap on *generalized* minutes, applied as the last step of
generalized_impedance.
Intentionally absent: the logsum access model needs no hard horizon (utilities
decay smoothly), a cap here puts a cliff artifact in scenario deltas, and the
TLFD band calibration silently absorbs whatever cap is set into the fitted
distance-band constants. Set only for sensitivity tests.
MAX_GENERALIZED_TIME_MIN = 90
Legacy single-cap keys (MAX_TRAVEL_TIME_MIN, MAX_TRAVEL_TIME_MIN_WALK) are
still honored as a routing-guardrail fallback (where no MAX_ROUTING_TIME_MIN_*
applies), but NOT as a generalized-time cap: only
MAX_GENERALIZED_TIME_MIN prunes gtm.
Key
Effective value
Set by
TOML comment
SLOPE_IMPEDANCE_ENABLED
false
studyanalysis_parameters.tomltrue
—
SLOPE_COST_FUNCTION
"tobler"
default
Which slope→cost curve to use: "tobler" (hiking-speed time, default) |
"minetti" (metabolic effort, steeper) | "irmischer_clarke" (empirical GPS
speed, gentlest); see SLOPE_COST_FUNCTIONS in pedestrian_impedance.py.
Why the middle curve and not the empirical one: all three are derived from
OFF-ROAD walking, so none of them transfers cleanly to a graded urban
sidewalk. Given three imperfect transfers, Tobler's mid-range slope response
is the conservative choice — it neither over-penalises grade the way the
metabolic curve does nor nearly ignores it. Irmischer–Clarke being the only
observed-speed curve makes it the best-evidenced curve FOR ITS OWN SETTING;
empirical status in off-road terrain does not confer transferability to a
setting it was not fitted on. This is a functional-form choice, made by
judgement, and a study with graded urban walking data should revisit it.
SLOPE_DOWNHILL_FLOOR
1.0
default
Lower clamp on the slope multiplier: 1.0 = no downhill speedup is ever
credited, while uphill penalties apply in full. The asymmetry is deliberate
and conservative, but no reason for it is on record — in particular nothing
says whether it is a behavioural claim (walkers do not bank downhill savings)
or a guard against DEM noise on short edges. Those two readings imply
different values: a behavioural claim would argue for something near 0.9.
ACTIVE_POINT_INTERIOR_SNAP_M
20.0
default
Walk / bike / transit rep points (setup step active_points). Each zone routes these
modes from its trip-end-weighted activity centroid, snapped to the nearest node at
least this far inside the zone; failing that, inside the zone; failing that, the
nearest node. The inset keeps a block-based zone from routing off the street it
shares with its neighbour. 20 m is a prototype value, not tuned; on Burlington it
took boundary-road snaps from 8–13 % to 0–1 % with placement error unchanged
(±0.15 min).
INTRAZONAL_DISTANCE_FACTOR
0.5
default
Intrazonal (origin == destination) impedance.
The diagonal of each mode's travel-time matrix is set geometrically:
intrazonal distance ≈ INTRAZONAL_DISTANCE_FACTOR * sqrt(zone area), converted
to time by mode speed and floored at INTRAZONAL_MIN_MINUTES. Walk/bike-LTS4/
car/ridehail get a geometric value; transit and bike-LTS1/2 get NaN.
intrazonal dist ≈ FACTOR * sqrt(zone area)
INTRAZONAL_MIN_MINUTES
1.0
default
floor on intrazonal travel time (minutes)
SPEED_CAR_LOCAL_M_S
6.9
default
25 km/h — local/intrazonal driving speed
INTRAZONAL_CV
0.38
default
Coefficient of variation of intrazonal travel times, used to correct the
Jensen's-inequality bias in the decay/logsum evaluation. A point-estimate
exp(-λ·t̄) understates E[exp(-λ·T)] when T has positive variance; the
correction models T as gamma-distributed and replaces the point estimate
with the MGF (1 + λ·CV²·t̄)^(−1/CV²). The value 0.38 is the CV of the
distance from a uniformly distributed point to the centroid of a compact
convex region (Ghosh 1951, geometric probability); stable across square,
circular, and hexagonal shapes (range 0.35–0.40) and across zone sizes.
Set to 0 to disable the correction.
MAX_ROUTING_TIME_MIN_CAR
150
default
Per-mode routing guardrails (minutes on each mode's own search weight:
congested clock sec for CAR, perceived walk/bike sec, clock minutes for
TRANSIT via r5py max_time). These bound compute and matrix size only — they
are deliberately slack so the search never truncates trips the logsum access
model still values; the study region is the real horizon. Since every
generalized_impedance surcharge is non-negative, a routing cap of X can only lose trips
whose generalized time also exceeds X.
TRANSIT must stay explicit: with no max_time, R5 silently applies its own
internal 120-min default. Raising it grows R5 search cost — and the transit
component-decomposition pass scales ~linearly with reachable path templates,
so it grows even faster. On a large, heavily congested metro, moving the cap
from 90 to 180 minutes roughly tripled both the template count and the
decomposition wall clock (25 min at 90, 73 min at 180); the shipped default
was chosen partly on that measurement. The cap is baked into each routeenv's
r5_params.json at creation, and the raw-TTM cache carries no param hash — an
existing study picks a change up only on a run forced through
perceived_conditions through routing.
WALK/BICYCLE guardrails keep each origin's Dijkstra inside a bounded bubble;
uncapping them multiplies routing cost by the region/bubble area ratio for
trips whose utility is negligible.
MAX_ROUTING_TIME_MIN_TRANSIT
120
default
—
MAX_ROUTING_TIME_MIN_WALK
120
default
—
MAX_ROUTING_TIME_MIN_BICYCLE
120
default
—
[streetscape]74 keys · 1 overridden
Streetscape: link-level pedestrian-environment factors (canopy, building shade,
interest, enclosure, traffic exposure) and the arterial-crossing price. One shared
edge-context pass (connectome/streetscape.py, the streetscape step) writes a per-raw-segment,
per-side attribute table under input_data/streetscape/ (+ junctions.parquet for the
crossing side); the pricing functions in pedestrian_impedance.py read it. PENALTY FORM:
the best streetscape (full shade, interesting continuous frontage, well-enclosed, quiet
and separated) costs 1.0 and everything else pays >= 1.0 — so, unlike the older factors,
the AVERAGE street pays a surcharge (accepted; the physical_conditions log and
the walk artifact's meta report the length-weighted mean multiplier per factor and
stacked, and a global re-tune of the walk kink is decided on that measurement).
Evaluated per side of the street and reduced MIN over sides (the walker picks the
better sidewalk, whole), applied identically to the WALK graph and to R5's transit walk
legs; bike graphs never read it. On by default in every study; thermal terms are
climate-weighted so a temperate study carries little from them. Judgement-call
constants throughout — nothing here is fitted to tract mode shares, which cannot see
per-link factors.
The six *_ENABLED flags gate PRICING (and whether the streetscape step runs at all): the sampling
pass is one geometry job that computes every column whenever any flag is on. A disabled
factor contributes exactly 1.0. Every key below is in the walk graph's walk_build_hash;
the SAMPLING keys (marked) are additionally in the table's input_hash, so changing one
rebuilds the table.
Section notes (1)
A missing speed (a way absent from the car graph — every `service` way) or AADT is imputed
to its highway class's length-weighted mean, then the local-tier mean, then the study mean;
a footway with no parent road prices exactly 1.0. Counts are in the physical_conditions log.
Streetscape overrides -- the parametric road diet
and the additive amenity improvement. Normally written to a SCENARIO's
analysis_parameters.toml by the `road_diet` / `streetscape_improve` mutations
(configuration-reference.md), one entry per call; a study may also set them here by hand.
Each entry: a SELECTOR -- `match` (OSM name / any `;`-part of ref, case-insensitive, exact)
and/or lat/lon bounds on the segment midpoint (`north_of` / `south_of` / `east_of` /
`west_of`; both = intersection; bounds alone = every segment in the box, paths included;
an entry matching no row FAILS the build) -- plus any of the FIELDS: `lanes_total` (cap,
both directions), `speed_mph` (cap), `aadt_factor` (multiplier), `buffer_add_m` (added
between curb and walker, both sides; a curb-side walker gets half the NEW pavement + this),
`canopy_add` / `interest_add` / `dead_share_add` (ADDED per side to the sampled input,
clipped to its range). Caps never raise a value -- a diet does not widen a stretch already
narrower. Applied after every sampled column and before the junction table, so the Landis
factor, the PLTS crossing level and the amenity factors all respond. Enters the table's
input_hash and the walk graph's walk_build_hash only when non-empty. The legacy
name STREETSCAPE_TRAFFIC_OVERRIDES is still read, with a warning. NOTE these entries price
the WALK side only: the `road_diet` MUTATION also narrows and slows the matched ways in the
car routing graph (`propagate_car`, default on), which a study-tier entry set by
hand here does not do -- set the diet as a scenario mutation if assignment should divert.
Example:
STREETSCAPE_OVERRIDES = [
{ match = ["Stockton Boulevard"], lanes_total = 2, speed_mph = 25, buffer_add_m = 2.0, aadt_factor = 1.0 },
{ north_of = 44.475, canopy_add = 0.4, interest_add = 20.0, dead_share_add = -1.0, buffer_add_m = 2.0 },
]
Key
Effective value
Set by
TOML comment
STREETSCAPE_CANOPY_ENABLED
true
default
tree canopy: thermal shade + the aesthetic floor (K_GREEN)
STREETSCAPE_BUILDING_SHADE_ENABLED
true
default
static building shade, read off the enclosure H/W
STREETSCAPE_FRONTAGE_ENABLED
true
default
interest: fixation targets per 100 m, minus dead frontage
STREETSCAPE_ENCLOSURE_ENABLED
true
default
height-to-width "outdoor room"
STREETSCAPE_TRAFFIC_ENABLED
true
default
Landis-form speed x volume x separation
STREETSCAPE_CROSSING_ENABLED
true
default
ODOT PLTS crossing lookup on the junction table (additive)
STREETSCAPE_BAND_HALF_WIDTH_M
3.0
default
Sampling geometry (these feed the attribute table's input hash; changing one rebuilds
it). Bands are where the walker's environment is READ: two parallel chords per
centreline segment at the class offset in [streetscape.STREETSCAPE_BAND_OFFSET_M]
(last in this section), buffered to this half-width. 3 m = sidewalk (1.5-2.5 m) plus a
planting strip (1-2 m): a prototype needed ~4 m of lateral reach to catch
verge trees. Wider reaches back gardens and park canopy the walker does not stand
under; narrower misses the tree row. The band offset is NOT the Landis separation --
that is composed per segment from the lane count (below).
STREETSCAPE_RAY_LENGTH_M
60.0
default
Perpendicular rays from the centreline find the first building face on each side
(setback, height, built continuity, enclosure width). 60 m: a face further away than
that is not enclosing anything; raising it only lengthens the ray query. Spacing
20 m: one ray per side per typical segment, more on long ones -- halve it for finer
built_share resolution at roughly double the ray cost.
STREETSCAPE_RAY_SPACING_M
20.0
default
—
STREETSCAPE_BUILDING_DEFAULT_HEIGHT_M
4.0
default
Height fallback for footprints with neither `height` nor `num_floors` (Overture
`class` is 89% null in a large-metro test and is not read). 4.0 m ~= a single-storey house
(that study's height p50 4.6 m). Storey height for the num_floors route is 3.5 m.
STREETSCAPE_STOREY_HEIGHT_M
3.5
default
—
STREETSCAPE_FRONTAGE_DEPTH_M
25.0
default
Frontage strip: address points and places are counted from the band outward to this
depth (one parcel depth from the sidewalk; address points usually sit at the
building or parcel centroid, so this must reach the front of a setback house).
built_share (setback <= FRONTAGE_SETBACK_MAX_M) is computed as a diagnostic and not
priced.
STREETSCAPE_FRONTAGE_SETBACK_MAX_M
20.0
default
—
STREETSCAPE_PLACES_MIN_CONFIDENCE
0.5
default
—
STREETSCAPE_PLACE_DOOR_MAX_M
20.0
default
Frontage door rules. A place marks the ONE
address point nearest to it, within this many metres, as a commercial door (Overture
geocodes a tower's or mall's tenants onto the building's address point; 20 m reaches
the setback house whose address sits at the parcel centroid). Doors themselves count
only for the street they are addressed to (normalised street name == the way's name;
the parent road's for a mapped sidewalk) -- a frontage strip reaches ~28 m up every
cross street and would otherwise book the corner building's cross-street numbers
(16-33 % of downtown doors); the unfiltered count survives as `doors_geo_*`.
ACTIVE frontage = a place whose Overture taxonomy.hierarchy (broad -> specific) starts
with any of these prefixes: food and drink, shopping, personal/beauty services, arts and
entertainment, banks, laundries. Prefixes, not leaves -- `categories.primary` is the
most specific leaf (american_restaurant), so a leaf list matched 2 of 18 places on a
test storefront row. Adding a prefix widens what counts as active; the table is
rebuilt when the list changes.
STREETSCAPE_CANOPY_SOURCE
"usgs_tcc_30m"
studyanalysis_parameters.toml"meta_1m"
Canopy source (registry in streetscape.py; SAMPLING key): "meta_1m" — the Meta/WRI
~1 m canopy-height product, read by windowed point sampling from the tile pin
input_data/canopy/meta_1m.json, a pixel counting as canopy at or above
STREETSCAPE_CANOPY_MIN_HEIGHT_M — is the default: the
30 m NLCD/USFS percent-cover fallback ("usgs_tcc_30m", input_data/canopy/
usgs_tcc_30m.tif) under-reads street canopy ~2.5x in a large-metro test, cannot see which
side of the street a tree row is on, and flips the landscape-enclosure reading of a
tree-lined trail (0.39 vs 0.04 canopy on the W&OD). Cost: 1 m roughly doubles the
streetscape build (517 -> 1178 s and 1343 -> 2350 s on two large metros) and needs the network
at the streetscape step (tiles are read through /vsicurl/, never materialised) — a study that
must build offline pins "usgs_tcc_30m" (the bundled example does).
STREETSCAPE_CANOPY_MIN_HEIGHT_M
2.0
default
—
STREETSCAPE_R5_MATCH_TOLERANCE_M
0.5
default
R5 lookup: an R5 edge sub-segment matches the raw OSM segment of the same way whose
chord passes within this distance of its midpoint. 0.5 m is a representation-drift
tolerance, not a search radius; R5 and the study-area PBF share node coordinates.
Unmatched pairs are counted in the log and priced at the study mean.
STREETSCAPE_TRAFFIC_VOLUME_SOURCE
"hpms"
default
Traffic exposure: inputs come from the
car graph's posted-limit ladder and the HPMS AADT cache the car side already fetches
(car_graph runs BEFORE streetscape so both exist). This key sets the volume LEVEL;
a counterfactual scenario also multiplies that level by its own
assignment's CHANGE -- see STREETSCAPE_TRAFFIC_ASSIGNMENT_RESPONSE below. A sidewalk
footway with no parallel road within PARENT_ROAD_RADIUS_M prices exactly 1.0 (a park
path is not exposed) -- i.e. m_traffic = 1.0 exactly.
"hpms": observed AADT (bound sections; unbound arterial-tier ways take the study's
own median for their class; locals the DEFAULT_AADT table below). "assigned": the
baseline's frozen assigned volumes from `connectome freeze-traffic-volumes`, allowed
only when that study's VMT reconciliation ratio_matched on non-freeway majors is
within [0.8, 1.25] -- assigned volumes track the OSM class better than the observed
count on the edge and carry the class-level bias of the calibration they came from.
Composing "assigned" with ratio mode is ALLOWED: the baseline level is then
the baseline's own assigned volume, so baseline_assigned x scen/base IS the scenario's
assigned level -- ratio mode becomes level mode, under the gate "assigned" already
carries. The floor and clip below still apply on top.
STREETSCAPE_TRAFFIC_ASSIGNMENT_RESPONSE
"ratio"
default
---------------------------------------------------------------------------
Assignment -> walk pricing.
After a counterfactual scenario's traffic_equilibrium converges, its streetscape table's
AADT is re-priced by the per-OSM-way RATIO of its own assigned + background-
preload analysis-hour volume to the baseline's:
scenario_aadt = baseline_table_aadt x (scen_assigned + preload)
/ (base_assigned + preload)
so a road diet that pushes traffic onto the parallel street re-prices BOTH --
the corridor calmer, the parallel street busier -- instead of needing the
modeller to assert it by hand with `aadt_factor`. The table's volume LEVEL
still comes from VOLUME_SOURCE above; assignment supplies only the change.
The walk graph is then rebuilt and WALK **and TRANSIT** are re-routed (the
transit access/egress legs are priced off the same table) and evaluation re-run.
Cost: every counterfactual scenario in a study with assignment now pays a walk
+ transit re-route (~25 min a scenario on a large metro, a couple of minutes on
the bundled example). The baseline is always skipped -- its ratio against
itself is 1 -- as is a scenario whose link_volumes.parquet is byte-identical to
the baseline's (it never re-assigned).
"off" | "ratio". "level" (the scenario's assigned volume AS the level) and full
walk<->assignment feedback are RESERVED and raise NotImplementedError.
STREETSCAPE_TRAFFIC_ASSIGNMENT_ROUNDS
1
default
Rounds of assign -> re-price -> re-route -> re-evaluate. 1 (the default) runs the
re-price once, on the equilibrium traffic_equilibrium just solved; the demand that the
re-evaluation produces is then NOT re-assigned, so the scenario's reported
link_volumes are the pre-re-price ones the walk pricing was derived from while
its mode shares are one step ahead (accepted and documented; see
docs/developers/gotchas.md). >= 2 re-assigns between rounds: built, but only
round 1 is validated.
STREETSCAPE_TRAFFIC_ASSIGNMENT_MIN_BASE_VPH
50.0
default
Guards (study-wide -- no dead-band, no corridor radius). A way is
re-priced only when its BASELINE two-way analysis-hour volume (assigned +
preload) reaches this floor: below it the assigned volume is mostly solver
noise on a lightly loaded local street, and its ratio would be meaningless.
STREETSCAPE_TRAFFIC_ASSIGNMENT_RATIO_CLIP
[0.25, 4.0]
default
...and the ratio is clipped to this band, so one way whose baseline volume sat
just above the floor cannot swing its AADT by an order of magnitude. Must
bracket 1.0.
STREETSCAPE_TRAFFIC_PARENT_ROAD_RADIUS_M
25.0
default
NOTE: a STREETSCAPE_OVERRIDES entry with `aadt_factor` != 1.0 while
this is not "off" RAISES, naming both. `aadt_factor` is the MANUAL stand-in for
diversion; with the link on, traffic_equilibrium answers the same question from the
network and the two would multiply.
---------------------------------------------------------------------------
The shipped ARTERIAL_SIDEWALK_BUFFER_M value, retained with its meaning: the nearest
centreline segment of a road class within this radius, bearing within
MAX_BEARING_DEG (parallel, not crossing), is a sidewalk footway's parent road.
STREETSCAPE_TRAFFIC_PARENT_ROAD_MAX_BEARING_DEG
25.0
default
—
STREETSCAPE_CROSSING_CONTROL_RADIUS_M
20.0
default
Crossing side: the arterial-crossing junction detector (compute_arterial_crossings'
logic, re-keyed to the streetscape table's own lanes/speed/AADT row) looks for a
mapped signal/marked/unmarked crossing or island within this
distance of the junction node.
STREETSCAPE_TRAFFIC_SCORE_EQUATION
"hcm2010"
default
Traffic exposure PRICE (not in the table hash). Per side:
S = intercept - log_width ln(W_eff) + volume f(vol15) + speed mph^2
m = min(MAX_MULT, 1 + K_PER_POINT * max(0, S - SCORE_FLOOR))
vol15 is TOTAL directional 15-minute volume, aadt x hour_factor x 0.5 / 4 -- not per
lane (lanes leave the volume term, so a road diet at constant
AADT is not charged for concentrating traffic into fewer lanes). SCORE_EQUATION picks
the volume form f and its coefficients from [streetscape.STREETSCAPE_TRAFFIC_EQUATIONS]
at the end of this section:
"hcm2010" -- HCM 2010 pedestrian link score, f linear (default:
responds to the NUMBER of vehicles removed, so a busy corridor that
sheds traffic reads a large gain -- the "err toward overstating the
intervention response" direction).
"landis2001" -- Landis et al. 2001 (TRR 1773 eq. 2), f = ln, fitted on ADT 200-18,500:
concave, prices a proportional change the same at any volume, flatter
on big roads. vol15 is floored at ADT 200 (the fitted sample minimum).
The K anchor below was derived under HCM per-lane at speed 0.0004; total volume and the
raised speed coefficient move class means up, so the +14 % now reads higher (drift
accepted).
STREETSCAPE_TRAFFIC_K_PER_POINT★
0.15
default
0.15 perceived length per Landis point above the HCM LOS-A boundary (2.0: a quiet street
with a sidewalk pays nothing). Basis: Broach & Dill (Portland GPS) rate collector-and-
larger streets ~14 % longer than locals; the length-weighted mean score of tertiary-and-
up road length is ~2.8-2.9 in two large metros (0.8-0.9 above the floor), so 0.15/point
reproduces the +14 % on that mean (0.17 would in one of them). Judgement in the anchor: a
[med] figure from an abstract, and Portland's "collector or larger" set is probably
busier than OSM tertiary-and-up. Raising it steepens every arterial's walk decay;
lowering to 0.10 gives +8-9 % on the same mean; the cap holds a freeway-side path
(scores 8-20, outside Landis's fitted range) at the old motorway 2.0.
STREETSCAPE_TRAFFIC_SCORE_FLOOR
2.0
default
—
STREETSCAPE_TRAFFIC_MAX_MULT
2.0
default
—
STREETSCAPE_TRAFFIC_OUTSIDE_LANE_FT
12.0
default
Landis cross-section defaults for what OSM does not carry (`width` is tagged on ~1 % of
roads): outside lane 12 ft; a 5 ft sidewalk where one is present (contributes
(6 - 0.3*5)*5 = 22.5); a tagged parking lane counts as 50 % occupied (0.20 * 50 = 10);
a tagged bike lane adds 5 ft. Trees in the buffer currently get NO Landis credit
(f_b = 1). A planned replacement, NOT YET BUILT: a
proportional f_b = 1 + 4.37 x the walking band's canopy share (Landis's 5.37 at full
canopy), kept alongside the green factor on purpose -- green prices the tree as
aesthetics everywhere, f_b as separation only where a buffer is measured. Until it is
built, this scalar is the whole rule.
Separation: the curb sits at lanes_total x LANE_WIDTH_M / 2
plus PARKING_LANE_M where a parking lane is tagged on that side (OSM `parking:left|
right|both`, legacy `parking:lane:*`; a mapped sidewalk inherits its parent road's).
The sidewalk offset is the MEASURED one where a `footway=sidewalk` line was hit by the
perpendicular ray (or a mapped sidewalk's own distance to its parent road), else the
walker stands AT THE CURB -- no verge is credited that nobody measured. buffer =
max(0, offset - curb), so a parking lane is credited once, via the parking term, not
again as buffer. No national source measures the curb-to-sidewalk verge (OSM `width`
~1-3 % of ways, TIGER none, HPMS width items sample-panel only); the class band offset
above is sampling geometry and must not be read as one. 2.4 m = an 8 ft parallel
parking lane (AASHTO / NACTO 7-9 ft). Lowering LANE_WIDTH_M or PARKING_LANE_M moves
the curb inward and credits more buffer on measured sidewalks.
STREETSCAPE_TRAFFIC_SIDEWALK_WIDTH_FT
5.0
default
—
STREETSCAPE_TRAFFIC_PARKING_TERM
10.0
default
—
STREETSCAPE_TRAFFIC_BIKELANE_FT
5.0
default
—
STREETSCAPE_TRAFFIC_LANE_WIDTH_M
3.5
default
—
STREETSCAPE_TRAFFIC_PARKING_LANE_M
2.4
default
—
STREETSCAPE_TRAFFIC_BUFFER_TREE_FACTOR
1.0
default
—
STREETSCAPE_TRAFFIC_UNKNOWN_SIDEWALK
"present"
default
A centreline side with neither a sidewalk tag nor a mapped sidewalk footway (about half
of every study's segments): "present" (a sidewalk at the curb, no buffer; the
penalty form would charge ignorance heavily under "absent"),
"class_share" (the class's tagged-presence share as a probability), or "absent".
STREETSCAPE_CROSSING_SIGNAL_MIN
0.45
default
Crossing PRICE (additive minutes, not scaled by the multipliers). The
level comes from the ODOT Analysis Procedures Manual v2 ch. 14 (July 2025 edition)
pedestrian level-of-traffic-stress crossing exhibits — 14-31 (collector/local: <= 2 lanes
and < 5 000 vpd), 14-32 (arterial, no refuge), 14-34/14-35 (arterial with a median
refuge, by lanes per direction) — on total lanes crossed x posted speed band (<= 25 / 30
/ 35 / >= 40 mph) x AADT band, the manual's "ADT unavailable" mid column when AADT is
NaN; four or more lanes without a refuge is level 4. A mapped marked crossing within
CONTROL_RADIUS_M deducts MARKED_LEVEL_DEDUCTION level(s) on the arterial tables, floored
at 2 (the manual's floor for crosswalk enhancements); a signal prices at SIGNAL_MIN
regardless of level — the HCM signalised pedestrian delay (C-g)^2/2C at C = 90 s,
g = 20 s = 27 s. Minutes per level are the sub-table at the end of this section,
anchored on Broach & Dill's ~70 m (~1 min) detour to avoid an unsignalised arterial
crossing = level 3; level 4 at 2x is the 4-lane 40 mph case, level 1 a two-lane <= 25
mph local. Half a crossing is amortised onto each of the two incident non-arterial
links, so a through-crossing pays one value on both routers.
STREETSCAPE_CROSSING_MARKED_LEVEL_DEDUCTION
1
default
—
STREETSCAPE_CROSSING_R5_SNAP_TOLERANCE_M
5.0
default
R5-only: max distance to snap an OSM junction node to an R5 vertex (the walk graph
needs no snap — its nodes ARE OSM node ids). 5 m is a tolerance for representation
drift, not a search radius; a junction that does not match contributes NO penalty on
the transit-walk side, and the run log reports "N/M junctions matched".
STREETSCAPE_K_SUN★
0.16
default
Thermal (union rule; not in the table hash): per side
m = 1 + K_SUN * w_climate * (1 - shade),
shade = 1 - (1 - shade_bldg) * (1 - TREE_VS_BUILDING_SHADE * canopy),
shade_bldg = clip(hw / HW_FULL_SHADE, 0, 1) (static sky-view off the enclosure H/W)
K_SUN 0.16 = the Singapore forced-choice revealed-preference experiment (Sci Rep 2022): a
sunlit walk is perceived 16 % longer than a shaded one; tree shade rated half as effective
as building shade (TREE_VS_BUILDING_SHADE 0.5). HW_FULL_SHADE 1.0: a canyon at least as
tall as it is wide is taken as fully shaded in the daytime-average sense. Raising K_SUN
steepens the walk decay in every hot study; the climate weight below is what keeps a
cool northern city from paying a hot southern one's price. With only one of the canopy / building-shade flags
on, the other cover is held at 0 inside the union.
STREETSCAPE_TREE_VS_BUILDING_SHADE
0.5
default
—
STREETSCAPE_HW_FULL_SHADE
1.0
default
—
STREETSCAPE_CLIMATE_WEIGHT_SOURCE
"ncei_hourly_normals"
default
Climate weight on the thermal term, in [0, 1].
K_SUN above is the sun premium measured in ONE climate (Singapore, daytime, Jun-Dec); this
weight scales it to the study's own heat. "ncei_hourly_normals" (default) computes it from
data: the nearest NCEI 1991-2020 hourly climate-normals station (public domain, no key;
pinned under input_data/climate/ at the climate_pin step), daytime hours DAYTIME_HOURS of the
scenario's analysis_period.months -- never its hour, which is a demand choice -- each
hour's heat-index normal through a ramp from HEAT_INDEX_ZERO_C (0) to HEAT_INDEX_FULL_C
(1), averaged, divided by ANCHOR_EXPOSURE and clipped to 1. "declared" uses
WEIGHT_DECLARED instead (also the fallback when no station is within STATION_MAX_KM). The
resolved value is logged and enters walk_build_hash, so set_analysis_period rebuilds the
walk graph. June, for example: DC 0.28, California Central Valley 0.28,
north Texas 0.96, Pacific Northwest / New England 0.00; Jul-Aug 0.63 / 0.51 / 1.00 / <= 0.03.
STREETSCAPE_CLIMATE_WEIGHT_DECLARED
1.0
default
—
STREETSCAPE_CLIMATE_HEAT_INDEX_ZERO_C
26.0
default
The ramp is in SHADE-air heat index (a Stevenson-screen quantity: temperature + humidity,
no radiation -- radiation is what the (1 - shade) term prices, so it must not appear here).
ZERO 26.0 C = the top of UTCI's "no thermal stress" band and the onset of shade-seeking in
the route-choice / street-vitality literature (25-27 C); a sunlit walker at 26 C shade air is
already 4-9 C higher in UTCI, i.e. in moderate stress. FULL 33.6 C = Singapore Changi's
daytime-mean heat index over the anchor experiment's months and hours (Jun-Dec, 08-19,
1991-2020). ANCHOR_EXPOSURE 0.80 = this same rule evaluated at Changi: the experiment's
participants faced Singapore's spread of hours, so a study with Singapore's exposure must
weigh exactly 1. THE THREE ARE ONE TUPLE -- change ZERO or FULL and ANCHOR_EXPOSURE must be
re-derived (tests/test_climate_weight.py holds the Changi summary that checks it). Lowering
ZERO by 2 C raises a mid-latitude June weight by ~0.1; the cool-study zeros do not move.
STREETSCAPE_CLIMATE_HEAT_INDEX_FULL_C
33.6
default
—
STREETSCAPE_CLIMATE_ANCHOR_EXPOSURE
0.8
default
—
STREETSCAPE_CLIMATE_DAYTIME_HOURS
[8, 19]
default
Local hours, inclusive, over which the ramp is averaged: 08-19 holds 82 % of NHTS 2022 walk
trip starts; the rest are cooler early/late trips. Flat within the window (judgement).
STREETSCAPE_CLIMATE_STATION_WARN_KM
75.0
default
Nearest-station distance: WARNING above the first, fall back to WEIGHT_DECLARED above the
second. 467 stations nationally; every metro airport; a central-Massachusetts city's nearest is 65 km (Boston).
STREETSCAPE_CLIMATE_STATION_MAX_KM
150.0
default
—
STREETSCAPE_K_GREEN
0.05
default
Aesthetic floor on canopy, always on with the canopy flag, climate-independent:
m = 1 + K_GREEN * (1 - canopy). 0.05 = a treeless street pays 5 % more than a fully
canopied one in every climate. Judgement call: halfway between
an earlier prototype's 2 % and the 9.5 % greenery detour (LUP 2024, children's routes,
general greenery -- an upper bound for tree-only). This is the ONLY thing trees do in a
cool-climate study; do not raise it to make canopy matter there -- that is the thermal
channel under an aesthetic name.
STREETSCAPE_K_ENCLOSURE
0.1
default
Enclosure (not in the table hash): m = 1 + K_ENCLOSURE * (1 - clip(hw /
HW_SATURATION, 0, 1)), hw = mean face height / (setback_L + setback_R) from the
perpendicular rays, extended inside green polygons to canopy edge / hedge / wall / fence /
water (chord-inside only). HW_SATURATION 0.5 = a two-storey street wall
on a residential right-of-way reads as fully enclosed. K_ENCLOSURE 0.10 is the same order
as the other slots (a bare sports pitch reads 1.10 here and 1.21 with the interest
slot -- what a blank, open street pays in the same two slots). Judgement, unanchored: the
route-choice literature (sky-view in Sevtsuk's SF 2021 work; Sussman's thigmotaxis) gives the
sign and the mechanism, not a coefficient. Face validity note: in a large-metro test the most-enclosed
corridors are alleys (garages 4-6 m from a service centreline) -- this factor rewards them.
STREETSCAPE_HW_SATURATION
0.5
default
—
STREETSCAPE_INTEREST_WEIGHT_DOOR
1.0
default
Interest: ONE fixation-target function on every walk segment, streets and paths alike.
m = 1 + K_INTEREST * (1 - min(1, interest / INTEREST_SATURATION_PER_100M)) + K_DEAD_FRONTAGE * dead_share
interest = weighted targets per 100 m of WINDOW, per side, each feature counted ONCE inside
the window: doors (distinct Overture (number, street) address points in the side's frontage
strip whose street is the segment's -- the frontage door rule above), walkable junctions (below), water edge
within INTEREST_WATER_RADIUS_M, benches / fountains / artwork / memorials / playgrounds (nodes
or areas) and leisure=garden polygons within INTEREST_POINT_RADIUS_M. natural=tree weighs 0:
canopy is the tree term (K_GREEN). Doors and objects are sided; junctions, water and gardens
enter both sides; own-geometry segments (sidewalks, paths) are undirected throughout.
Gehl's A band (17.5 doors/100 m) and a park path grid coincide at weight 1
STREETSCAPE_INTEREST_WEIGHT_JUNCTION
3.0
default
above objects; Lynch's nodes
STREETSCAPE_INTEREST_WEIGHT_WATER
3.0
default
untested on any fixture within 30 m; route-choice water premium
STREETSCAPE_INTEREST_WEIGHT_OBJECT
1.0
default
bench / fountain / artwork / memorial / playground
STREETSCAPE_INTEREST_WEIGHT_GARDEN
1.0
default
—
STREETSCAPE_INTEREST_WEIGHT_TREE
0.0
default
OSM tree nodes are a mapper artefact (105 on the Mall, 0 on O St); canopy carries trees
STREETSCAPE_INTEREST_WATER_RADIUS_M
30.0
default
—
STREETSCAPE_INTEREST_POINT_RADIUS_M
20.0
default
—
STREETSCAPE_INTEREST_SATURATION_PER_100M
20.0
default
Saturation: 20 weighted targets / 100 m reads fully interesting (15 was the
alternative considered). Only the very best rows saturate (Church St Burlington 29); 7th St NW's
Chinatown row (14) pays 3 %, Kalorama Park's path grid (16.5) 2 %, a fine rowhouse / midtown
grid (10-12) 4-5 %, a cul-de-sac (7) 7 %, a strip arterial or superblock (3-6) 7-8 %, a blank
arterial (~1) 10 %.
Raising SAT makes every street pay a little more; K_INTEREST steepens the gradient. The
three price keys are read by pedestrian_impedance.py and are NOT in the table hash; the
weights / radii / window keys are SAMPLING keys (streetscape.py computes interest_L/R, the
unsaturated per-side density) and are.
STREETSCAPE_K_INTEREST
0.1
default
= the old K_FRONTAGE; anchor: a bare pitch reads 1.10 here, 1.21 with enclosure; a blank street 10 % longer than a fully interesting one -- the order of the 9.5 % greenery detour, below the 16 % sun premium; the intended anchor (Basu & Sevtsuk 2022) is not yet read
STREETSCAPE_K_DEAD_FRONTAGE
0.1
default
surface parking / fuel / industrial first hits pay more than a blank wall, additive
STREETSCAPE_INTEREST_WINDOW_M
200.0
default
The WINDOW: 200 m of the SAME STREET, sliding, centred on the
segment and shifted to stay inside the run. A street run is the contracted chain continued
through junctions into the chain carrying the same normalised name (the parent road's for a
mapped sidewalk; the straightest one if several share it); an unnamed chain (park path,
alley, connector) continues into the straightest unnamed chain of the same highway class when
the bearing change is under INTEREST_UNNAMED_MAX_TURN_DEG, else stops. The chain walk does
not cut at driveway / parking-aisle junctions. Why not the chain alone: with distinct-per-
chain counting a chain's junctions are its two ends, so the term is 2*w/L -- a 12 m sidewalk
corner or a 25 m alley piece reads saturated on junctions, and Arden Way and a blank
institutional wall (7th St NW 600) read as interesting as a storefront row.
was STREETSCAPE_MONOTONY_WINDOW_M; the monotony diagnostics share it
STREETSCAPE_INTEREST_UNNAMED_MAX_TURN_DEG
45.0
default
—
STREETSCAPE_INTEREST_JUNCTION_MERGE_M
20.0
default
Junctions: a walk-graph node of degree >= 3 counting only ways that are not
service=driveway|parking_aisle|drive-through|emergency_access; degree->=3 nodes within
INTEREST_JUNCTION_MERGE_M of each other are ONE intersection (a crossing with two mapped
sidewalks carries 5-9 such nodes). Study-wide r moves the mean term by <= 0.003; it is what
stops a sidewalk corner reading three intersections.
STREETSCAPE_BAND_OFFSET_M
motorway
18.0
motorway_link
18.0
trunk
14.0
trunk_link
14.0
primary
14.0
primary_link
14.0
secondary
12.0
secondary_link
12.0
tertiary
10.0
tertiary_link
10.0
residential
7.0
unclassified
7.0
living_street
7.0
road
7.0
service
4.0
track
4.0
default
--- Sub-tables of [streetscape] — must stay LAST in this section (the loader
flattens one level; a plain key written below this header vanishes into the
sub-table).
Sampling-band offset from the centreline by OSM highway class, metres: where the
canopy chord, frontage/interest window and enclosure setback are read. Half pavement
width plus a verge for a typical US cross-section of that class; a class absent here
takes the residential value. Own-geometry classes (footway, path, pedestrian, steps,
cycleway, bridleway, corridor, platform) are sampled on their own line and are not
listed. Judgement constants; NOT the Landis sidewalk offset, which is composed
per segment from the lane count — a wider read zone is harmless there, a
wider separation is not. trunk/trunk_link = primary: walkable
trunk is the same 2-3-lane one-way carriageway profile as primary (85 % of DC's walkable
trunk km); 18 m was sized for 6-8-lane freeways and now applies to motorway(_link) only,
which is in the table as a sidepath parent and is not a walk edge -- unless the way
carries an explicit foot=yes|designated (a bridge deck with a signed path),
which makes it walkable like any other class.
STREETSCAPE_TRAFFIC_DEFAULT_AADT
residential
1000
unclassified
1500
living_street
300
service
200
default
AADT (vehicles/day) for classes HPMS never covers (f_system 7: residential,
unclassified, living_street, service). Judgement constants; assigned-volume-implied
ADT on locals gives the order of magnitude, the HPMS f_system
6 median (2.8 k) the ceiling. An arterial-tier class with no bound section in the
study takes the study's own class_median instead of this table.
STREETSCAPE_CROSSING_MINUTES_BY_PLTS
1
0.2
2
0.5
3
1.0
4
2.0
default
Minutes for ONE crossing at each ODOT PLTS level (see STREETSCAPE_CROSSING_*
above for the level lookup and the Broach & Dill anchor at level 3). Keys are levels.
STREETSCAPE_TRAFFIC_EQUATIONS
intercept
log_width
volume
speed
hcm2010
6.0468
1.2276
0.0091
0.0009
landis2001
5.3876
1.2021
0.253
0.0005
default
hcm2010 ADVANCED -- traffic-score coefficients per STREETSCAPE_TRAFFIC_SCORE_EQUATION. Each
equation's four coefficients were fitted JOINTLY; editing one produces an equation
nobody estimated, so do it only as a documented sensitivity. A study or scenario can
override a single coefficient (tables deep-merge), e.g.
[streetscape.STREETSCAPE_TRAFFIC_EQUATIONS.hcm2010]
speed = 0.0004
The volume FORM (linear / ln) belongs to the equation name, not to this table.
hcm2010: HCM 2010 link score (Landis's 2010 update, NCHRP 3-70) -- except `speed`.
The fitted HCM speed coefficient is 0.0004; 0.0009 matches
Sevtsuk et al. 2021's revealed route choice (SF, 14,760 GPS traces: +10 mph route-average
speed = +8.2 % of route length; 0.0004 gives +3.6 % for 25 -> 35 mph, 0.0009 gives +8.1 %).
Flat, so a 25 mph local carries +0.25 points over the fitted value and may clear the floor.
Parking cost (pluggable)
Parking cost is computed per-destination in dollars and folded into the CAR
TCM via value-of-time. The active method is chosen by cost_method; register
new methods in connectome/parking/cost_methods.py.
[parking.density] describes how density scales in this study area and is
shared between cost methods (`census_cbsa`, `observed_prices`) and the
parking_time model's cost_fraction — so changing the cost method does not
silently break parking_time.
Back-compat: legacy flat keys (PARKING_MAX_RATE_OVERRIDE, PARKING_DENSITY_COLUMN,
PARKING_DENSITY_REF_PERCENTILE, PARKING_DENSITY_POWER) set under [routing] in a
study/scenario override are folded into [parking.census_cbsa] with a deprecation
warning.
Key
Effective value
Set by
TOML comment
cost_method★
"observed_prices"
studyanalysis_parameters.toml"census_cbsa"
DEPRECATED: "census_cbsa" emits a DeprecationWarning and will be removed. It
infers a whole metro's rate surface from one CBSA-level maximum and falls back
to a default rate when the CBSA is unlisted; "observed_prices" fits the same
power curve to researched (address, daily_rate) samples instead. The default
below flips to "observed_prices" once the sampling script exists — until then
it stays here so new studies are not blocked on hand-sampling.
"census_cbsa" (deprecated) or "observed_prices"
PARKING_HOURS_TO_DAILY_MAX
3.0
default
Hours of hourly-rate parking at which charges reach the daily max:
hourly_rate = daily_rate / PARKING_HOURS_TO_DAILY_MAX, and a stay of t hours
costs min(t · hourly_rate, daily_rate). Encodes the front-loading of posted
garage pricing (first hour ≈ 35–50% of the daily max; the max binds after
~2–4 h) — which is why linear t/8 amortization would understate short stays.
The front-loading pattern is the argued part; the value 3.0 is judgement.
The bracket it sits in comes from inspection of posted garage rate sheets,
and neither the sample nor the source is on record, so the shape of the
claim can be checked but its level cannot.
Consumed with LOGSUM_PARKING_DURATION_HOURS by the evaluation fold
(PARKING_CHOICE pathway only). A property of the rate structure, so it lives
here with the cost methods rather than with the behavioral knobs.
density★
density_column
"trip_ends""trip_ends"
density_ref_percentile
99.999.9
density_power
22
free_below_density
4000
studyanalysis_parameters.toml4 of 4 values changed
Shared density description of the study area, consumed by cost methods and
by parking_time's cost_fraction.
Neither the reference percentile nor the elasticity has a recorded basis:
nothing establishes 99.9 rather than 99 or 99.5, and nothing establishes
convexity 2 rather than 1.5 or 3. Both are judgement. Their reach is narrower
than it looks — `observed_prices` fits its own exponent to the samples, so the
declared power binds mainly on the deprecated `census_cbsa` method and on
parking_time's cost_fraction.
density_column "trip_ends" or "jobs"
density_ref_percentile percentile of density used as the scaling reference
free_below_density below this density (units of density_column), parking is free ($0); 0 disables.
In auto-oriented metros, free and paid parking coexist at the same trip-end
densities, so a density→price fit alone can't reproduce free parking; this
floor forces the low-density tail to $0. Applied by both cost methods.
PER STUDY: measured from the built zones against the parking samples (just
below the lowest ambient paid sample, above the free anchors) as the last
gate of parking sample collection — `python -m connectome.tools.
parking_sample_gates <baseline_dir>` must PASS. Never copy another
study's value; a floor from a larger metro forces real garages free.
census_cbsa
max_rate_override
(empty)
default
census_cbsa: scale a CBSA-resolved max daily rate by zone destination density.
cost = P_max * clip((density / rho_ref) ^ density_power, 0, 1)
where density and rho_ref come from [parking.density]. P_max is auto-resolved
by CBSA from connectome/data/parking_rates.json; set max_rate_override to skip
the CBSA lookup.
max_rate_override $/day — skip CBSA lookup if set
observed_prices
(empty table)
1 file-path setting inside, not shown
default
observed_prices: fit a power curve to observed (address, daily_rate) samples
and use those samples directly in zones that contain one. Sampled zones get
the mean observed rate; unsampled zones get a predicted rate from the fit.
samples_file is a TOML with one or more [[sample]] tables, each with fields
`address` (str) and `daily_rate` (float), resolved under input_data/.
[travel_survey]2 keys
Logsum access model (evaluation)
Starting values derive from
ActivitySim prototype_mtc; ASCs are walk-anchored (WALK ≡ 0, omitted) and
are CALIBRATION KNOBS, not adopted truths — `calibrate-logsums` rewrites
them into {study}/calibrated_logsum_parameters.toml (merged between the
study and scenario tiers).
Key
Effective value
Set by
TOML comment
TRAVEL_SURVEY_SOURCE★
"nhts"
default
Which household travel survey supplies the calibration targets (non-work and WORK
mode shares, TLFDs, the walk/bike curvature profile, ridehail). "nhts" is the
national pipeline — NHTS 2017 shape + 2022 ridehail, ACS WORK shares, LODES WORK
TLFD — and is the default. A regional survey is declared as a named entry with a
`type` (a registry key in connectome/logsums/survey_sources.py, or an import path
"package.module:ClassName" naming a RegionalHTSSource subclass) and selected here; it
substitutes for NHTS per (segment, purpose group) wherever its cells pass the
30-trip gate, NHTS covering the rest. Under a regional survey the walk reporting
factor and the 2022 ridehail splice apply only to NHTS-fallback cells, and the
work-residual transfer is off. Microdata is read by absolute path and never copied:
[travel_survey]
TRAVEL_SURVEY_SOURCE = "regional_hts"
[travel_survey.TRAVEL_SURVEY_SOURCES.regional_hts]
type = "some.module:SurveyClass"
path = "/abs/path/to/survey_release"
TRAVEL_SURVEY_SOURCES
(empty table)
default
—
[logsums]58 keys · 6 overriddenSection notes (3)
--- runtime ---
── LOGSUM_WORKERS ───────────────────────────────────────────────────────────
Width of evaluation's per-user-class process pool. Each user class is an
independent recompute — its own GTMs in, its own per-userclass files out —
so the pass fans out cleanly; results are still folded in study order, so a
parallel run produces the same artifacts as a serial one.
Commented out = the auto envelope: min(4, user classes, cores - 2). The cap
is a MEMORY decision, not a core-count one — every worker holds one user
class's GTM stack plus the destination-choice kernel's per-group full-frame
exponentials, on top of a parent that measured ~5 GB on a 1,425-zone study.
Raise it only with the box's headroom measured, not assumed.
Set to 1 to restore the in-process loop exactly (the reference path for any
parity check).
LOGSUM_WORKERS = 4
CAR_NONHH cost-model parameters (CAR_NONHH_SEGMENTS etc.) live in the
[routing] section beside the RIDEHAIL constants — they are routing impedance
parameters, baked into ttm/tcm_CAR_NONHH at derivation time. Only the ASC
entries (keyed on the mode name, below) and the nest placement are
logsum-layer concerns.
LOGSUM_PREWAR_WALK_UTILS_PER_UNIT = { car_owning = …, car_deficient = … }
k per segment, utils on WALK per unit min(share, CAP). WRITTEN by the
delivery calibration into calibrated_logsum_parameters.toml; a study that
does not calibrate sets it by hand in its study tier (both segments; 0.0
switches one off). Deliberately not set here: an unset k with the term on
and the calibration off raises rather than fall back to K_LIN.
Key
Effective value
Set by
TOML comment
LOGSUM_NEST_TREE
AUTO
["CAR", "CAR_NONHH"]
NONMOTORIZED
["WALK", "BICYCLE"]
TRANSIT
["TRANSIT"]
RIDEHAIL
["RIDEHAIL"]
default
--- structure ---
LOGSUM_NEST_COEFFICIENTS★
AUTO
0.72
NONMOTORIZED
0.72
TRANSIT
1.0
RIDEHAIL
1.0
default
μ_k ∈ (0, 1]; 1.0 = no correlation within the nest. A nest declared with ONE
mode has Γ_k = V_m for any μ, so its coefficient is unidentified and must be
1.0 (enforced in evaluation._validate_nest_structure). TRANSIT and RIDEHAIL
were carried over from ActivitySim prototype_mtc, where they head multi-mode
sub-nests; here they are singletons and their old 0.72 / 0.36 did nothing.
AUTO keeps 0.72 on principle — CAR and CAR_NONHH are close substitutes — but
is also inert TODAY, because a user class carries one or the other and never
both (car classes get CAR, carless get CAR_NONHH). NONMOTORIZED is the
only coefficient with live behavioral content: it governs how much of a
displaced BICYCLE trip lands on WALK rather than on car/transit.
LOGSUM_EVAL_DTYPE
"float32"
default
Float dtype for evaluation's exp/log evaluation kernels (mode-choice logsum and
the destination-choice exponent that follows its dtype). The calibration has
run float32 here since it was written; evaluation ran float64, but its inputs are
float32-quantized on disk, so the double width was never protecting the model
from the data. Gated on Burlington to within 0.1% of access metrics — well
inside the fixture's own identical-code noise floor. Set to "float64" for
double-precision evaluation.
The four NHB demand-layer groups below (ESCORT + the three NHB anchor
groups) are
declared unconditionally so their theta / band / annualization entries are
never "unknown purpose groups", but their CATEGORY MAPPINGS are live only
when [logsums.LOGSUM_NHB_DEMAND] enabled = true. With the layer off, ESCORT
is an unmapped category exactly as it was before the layer existed and
evaluation is unchanged — which is what makes the NHB demand layer a study
opt-in rather than a silent global change, and what its layer-off negative
control checks.
LOGSUM_SPECIAL_CATEGORIES
AIRPORTS
"DISCRETIONARY"
EVENT
"DISCRETIONARY"
default
AIRPORTS, EVENT: outside NHTS weighting; borrow the mapped group's parameters
LOGSUM_UTILITY_SCALE★
WORK
-0.025
SCHOOL
-0.0224
DISCRETIONARY
-0.0175
ESCORT
-0.0175
default
--- utility parameters (per purpose group) ---
utils/gen-min. WORK set to -0.025, the FTA New Starts mid-band (national HBW range
-0.019..-0.044; MTC's own -0.0134 sits below it). SCHOOL/DISC already in range. c is the
utils<->minutes exchange rate (1/|c|), so a flat WORK c inflated every util-native quantity
(ASCs, option value) in time terms. Existing calibrated studies pin WORK
back to -0.0134 (their ASCs were fit against it) until they are re-calibrated.
ESCORT's c is DISCRETIONARY's, because its seed ASCs are DISCRETIONARY's and
ASCs are only meaningful against the c they were fit at.
The three NHB_* anchor groups take NO entry here: c travels with the ASCs they
BORROW (LOGSUM_NHB_DEMAND.asc_groups), so setting one is an error, not an
override — same discipline as LOGSUM_NHB's asc_group.
LOGSUM_THETA★
WORK
0.3
SCHOOL
0.5358
DISCRETIONARY
0.6755
ESCORT
0.6755
NHB_WORK
0.6755
NHB_OTHER
0.6755
NHB_ESCORT
0.6755
default
theta for the four NHB demand-layer groups is UNCALIBRATED and seeded at
DISCRETIONARY's
0.6755 — the same choice, and the same caveat, as LOGSUM_NHB.theta. It is the
one destination-side parameter that does NOT travel with the ASC borrow, so a
study that calibrates the NHB TLFD targets fits these too.
LOGSUM_DISTANCE_BREAKPOINTS_MI
[1, 2, 5, 10, 20]
default
--- destination distance bands (utils; band 0 fixed at 0; default zeros = off until calibrated) ---
The NHB demand-layer groups get their OWN bands rather than borrowing
(unlike LOGSUM_NHB,
which borrows asc_group's): these groups are split out precisely because their
trip lengths differ — work-anchored NHB legs are twice the length of the
others and escort legs are the longest in the data (p50 5.7 mi to work) — so a
borrowed decay would misfit exactly the quantity the split exists to capture.
Zeros until the NHB targets are calibrated.
calibratedcalibrated_logsum_parameters.toml7 of 7 values changed
—
LOGSUM_VALUATION_VOT_USD_PER_HR★
19.4
default
--- reporting ---
USDOT 2023$, uniform across user classes (an equity choice)
LOGSUM_DAYS_PER_YEAR
(empty table)
default
── LOGSUM_DAYS_PER_YEAR ─────────────────────────────────────────────────────
The time-equivalent benefit is computed for one analysis period and reported
per period, per day, or per year (applet default: per year):
per day = per period × D_g D_g = NHTS daily trips_g / period trips_g
per year = per day × Y_g Y_g = NHTS annual trips_g / average-day trips_g
Both are derived per purpose group from NHTS 2022 on the study's day type, for
every study (connectome.demand.time_of_day.resolve_benefit_factors). The per-
period figure is an AVERAGE day of that type — holidays, vacation and summer
are already averaged in — so Y_g is not "working days": for a weekday study it
is 260 weekdays plus the group's weekend trips expressed in weekdays
(WORK ≈ 291, SCHOOL ≈ 262, DISCRETIONARY ≈ 423, ESCORT ≈ 314; above 365 means
a weekend day carries more of the group's trips than a weekday). D_g honours
[demand.time_of_day].factors overrides; it is NOT gated by that block's
`enabled`. Basis and the caveat (every hour's trips are credited with the
analysis period's per-trip change) in docs/modelers/indicators.md.
Empty = NHTS-derived; `{ GROUP = days }` pins a group's Y_g with local data.
Replaces LOGSUM_ANNUALIZATION_DAYS (WORK/SCHOOL 250, others 300, no period →
day step), now retired.
LOGSUM_PERSIST_MODE_LOGSUMS
false
default
Persist per-UC×group mode-choice logsum matrices (debug only — U·G large
matrices; mirrors PERSIST_EMISSIONS_MATRICES).
LOGSUM_ASC_OVERRIDES
[]
default
Per-userclass additive ASC overrides matched on user_classes.csv columns,
e.g. [{ match = { cyclist_type = "fearless" }, mode = "BICYCLE", group = "*", delta = 0.4 }]
LOGSUM_COST_RATE_FACTORS
WORK
1.0
SCHOOL
1.0
DISCRETIONARY
1.0
default
--- behavioral fold ---
The evaluation native fold builds each mode's generalized minutes as the legacy
gtm base PLUS these surcharge deltas; every knob below is an identity default
(delta skipped), so the fold reproduces the legacy gtm bit-for-bit until a
value is changed. All keys are LOGSUM_*-prefixed so the resolved-param guard
forces an evaluation recompute when any of them differs (no force needed).
Per-purpose minutes-per-dollar: dimensionless multipliers on the UC's
behavioral minutes-per-dollar (purpose VOT = VOT_u / factor_g). SCALE is the
single global cost-sensitivity level (the elasticity loop's free knob).
LOGSUM_COST_SCALE★
1.0
default
—
LOGSUM_PARKING_DURATION_HOURS
WORK
8.0
SCHOOL
7.0
DISCRETIONARY
1.5
default
How long each purpose group's activity parks, in hours. Priced at
min(hours · daily_rate / PARKING_HOURS_TO_DAILY_MAX, daily_rate) inside the
parking-choice logsum (PARKING_CHOICE only; the legacy additive path ignores
this with a warning). Values at or above PARKING_HOURS_TO_DAILY_MAX are
equivalent — the daily max binds — so WORK/SCHOOL pay the full
daily-rate charge exactly and only short-stay groups move. DISCRETIONARY
1.5 h ≈ NHTS trip-weighted dwell for the basket (shopping ~30–60 min, meals
~1 h, social/recreation/healthcare ~1–2 h) ⇒ 0.5 × daily at the default
rate curve. Live default: it changes DISCRETIONARY impedance, so a study
calibrated before it was adopted needs calibrate-logsums on its first rerun.
See docs/modelers/parking-and-hassle.md.
LOGSUM_WALK_KINK★
SLOPE1
2.02.0
THRESHOLD_MIN
25.025.0
SLOPE2
4.04.0
calibratedcalibrated_logsum_parameters.toml3 of 3 values changed
Piecewise walk/bike time (the kink, three-parameter form): below
THRESHOLD_MIN clock minutes count SLOPE1× (the fitted/
declared below-threshold walk weight — MTC weights short walk 2× IVT); above,
the marginal minute counts SLOPE2×. Inactive at SLOPE1 = SLOPE2 = 1.0 or with
no THRESHOLD_MIN (TOML has no inf literal — omit the threshold to keep it off).
SLOPE1 defaults to 1.0 (IVT parity) when absent. The calibrated
walk kink carries a fitted (SLOPE1, THRESHOLD_MIN, SLOPE2); bike carries a
fitted SLOPE2 with SLOPE1 = 1 and THRESHOLD ≈ 39 declared.
WALK is PINNED by default rather than fitted — see
LOGSUM_CURVATURE_FIT_MODES below for why.
CURRENT DEFAULT: 2x below the 25-min pivot, 4x beyond. SLOPE1 =
2.0 is the MTC below-threshold precedent (walk clock weighted 2x IVT at
ordinary walking distances); SLOPE2 = 4.0 keeps s2 > s1 so long walks still
decay. THRESHOLD_MIN 25.0 min ~= 1.12 mi at SPEED_WALKING_M_S 1.2.
SELECTED ON MAPPED-OUTPUT FACE VALIDITY, NOT THE NHTS PROFILE OBJECTIVE — read this
before "improving" it. The predecessor default (4.0, 25.0, 6.0) came from a
35-point (s1, T) grid on a mid-size tolled metro, which it won on the level-free odds
profile: grid winner s1=4.0/T=25/s2=6.274 at max|R| = 1.377, pinned (4,25,6) =
1.380. Sensitivity, same study: (4,25,4) = 1.395, (4,15,6) = 1.461,
(1,34,5) = 2.251. By that objective the current default is WORSE — a flat 2x
measured max|R| ~= 2.47 on Burlington, and 2/25/4 was not re-scored against
the grid. It was adopted because Burlington's per-zone access surface reads
correctly with it and the 4x below-threshold weight was judged to overprice
ordinary walking. Two defensible criteria disagree here; the profile evidence
is recorded so a future change is an informed one, not a rediscovery.
Re-derive per study if a study's walk
profile looks off. Changing this default does NOT reach an already-calibrated
study — see docs/developers/gotchas.md on the calibrated tier.
LOGSUM_BIKE_KINK★
SLOPE1
1.01.0
SLOPE2
3.62691.0
THRESHOLD_MIN
39.0
calibratedcalibrated_logsum_parameters.toml3 of 3 values changed
—
LOGSUM_WALK_AVAILABILITY_CAP_MIN
false
default
Optional hard availability cap (clock minutes): NaN out cells whose pre-kink
clock time exceeds it. false = off (routing support governs availability).
LOGSUM_BIKE_AVAILABILITY_CAP_MIN
false
default
—
LOGSUM_RIDEHAIL_WAIT_MULTIPLIER
1.0
default
Ridehail wait multiplier: the pickup wait (RIDEHAIL_WAIT_MINUTES, baked into
ttm_RIDEHAIL) is re-weighted at this rate in the fold. 1.0 = identity; MTC 1.5.
LOGSUM_WALK_COMFORT_RATE
2.0
default
Two-layer walk comfort: the walk router (when ped-impedance is on)
emits clock time + a perceived-minus-clock comfort surcharge; the fold prices
the surcharge at this rate.
2.0, matching LOGSUM_WALK_KINK's SLOPE1. The surcharge means
"extra perceived walking", so it belongs on the same numeraire as the walking
clock it surcharges. At 1.0 it would sit at IVT parity while the clock
it surcharges is weighted 2x IVT below the kink threshold — i.e. a minute of
walking discomfort would be priced at half a minute of ordinary short walking,
which systematically under-weights the whole pedestrian-comfort layer.
NOT a live reference to SLOPE1: this is a plain scalar and the two are set
independently. A study that calibrates or overrides its walk kink away from
SLOPE1 = 2.0 should set this key to match in its own analysis_parameters.toml
— nothing checks the two agree.
The absent-key fallback in code stays 1.0 (the identity default that reduces
the fold to the legacy gtm bit-for-bit); 2.0 is the shipped configuration, not
the code default. Changing it re-runs generalized_impedance and evaluation automatically —
the key is in the generalized_impedance marker and carries the LOGSUM_ prefix.
LOGSUM_TRANSIT_OVT_OVERRIDES
(empty table)
default
Per-purpose transit OVT β overrides: { GROUP = { wait = .., access
= .., egress = .., transfer = .. } }. Absent ⇒ the shared [routing] β values
(TRANSIT_OVT_MULTIPLIER_*) already folded into gtm_TRANSIT — identity.
LOGSUM_TRANSIT_WALK_FOLLOWS_KINK
true
default
Price transit ACCESS / EGRESS / TRANSFER walking through the study's own walk
kink (LOGSUM_WALK_KINK) instead of the scalar TRANSIT_OVT_MULTIPLIER_* β's, so a
walk minute costs the same whichever ledger it lands in.
Why on by default: the same physical act of walking a block entered generalized
minutes at s1 (2.0 shipped, 4.0 in Burlington's own fit) as a WALK trip minute
but at 1.6 as a transit access leg and 1.9 as a transfer walk (the per-component
β set). MPO and Nordic practice both hold ONE price per walk minute across ledgers
(MTC prices both at 2.0); we held three. In a generalized-minutes framework where
both feed one accessibility measure through one numeraire, that discount let
transit beat walking on identical minutes, and it biased every scenario that
moves the walk↔transit boundary — new routes, stop consolidation, pedestrian
projects.
The uniform-2.0 β set since closed the BELOW-threshold half of that
gap by coincidence of constants: at the shipped s1 = 2.0 the tie is an identity
under T. It still binds beyond T (the kink charges s2 there, a flat β does not)
and at any fitted s1 ≠ 2.0 — s1 is calibrated, 2.0 is only where it starts. The
structural argument is the point: one price per walk minute should not depend on
two independently-set constants happening to agree.
Applied PER EPISODE (access, egress, transfer each through pw() separately),
because T is an episode-level tolerance. β_wait is untouched: waiting is not
walking. The transfer-walk premium (1.9 vs 1.6, since flattened by uniform 2.0)
is NOT lost — it moves to the vehicle the framework already has for interchange
stress, the additive per-boarding penalty TRANSIT_TRANSFER_PENALTY_MARGINAL_MIN.
false restores scalar β pricing bit-for-bit. Setting it false is the A/B arm.
LOGSUM_WALK_SLOPE1_REVIEW_THRESHOLD
2.5
default
Fitted walk s1 above which the calibration report flags that the implied transit
access multiplier has left the practice envelope (WebTAG's ceiling; above every
regime surveyed — MTC sits at 2.0). Surfaced, never silently capped. Only
meaningful when LOGSUM_TRANSIT_WALK_FOLLOWS_KINK is on, since s1 then prices
transit walking too.
LOGSUM_ELASTICITY_CONVENTION
"short_run"
default
Elasticity-target convention selector for the elasticity loop: which
rows of elasticity_targets.toml the loop calibrates against. Not consumed by
evaluation; recorded here so the naming guard covers it and a change re-runs.
LOGSUM_CALIBRATION_CIRCUITY
1.2
default
--- calibration (consumed by calibrate-logsums only) ---
NHTS TRPMILES is reported route distance; the model's distance bands are
great-circle miles. Non-work TLFD targets divide TRPMILES by this network
circuity factor before binning. A stated approximation (typical US values
1.2-1.3); recorded in the calibrated file's provenance header.
LOGSUM_CALIBRATION_WORKERS
0
default
Worker processes for the calibration evaluation pass (calibrate-logsums and the
joint calibrate<->equilibrate loop alike). 0 = auto: min(user classes, cpu - 2).
Each spawn worker holds its own copy of the evaluation context, so peak memory
scales with this — the auto value OOM'd a large study on a 12 GB box; set a
small number there. Results are worker-invariant (executor.map keeps user-class
order; 1 runs the same code serially), so this is purely a speed/memory knob.
LOGSUM_TLFD_TRUNCATE_TO_STUDY_SUPPORT
true
default
Support truncation (ADR 0005). The NHTS pool is national and covers trips of
every length; the model represents only trips with both ends inside the study
area. With this on, the non-work pool is restricted to trips no longer than
the study's own maximum OD great-circle distance before targets are built, and
the shares renormalize over what remains — so the target is the distribution
CONDITIONAL on staying inside the study, which is what the model represents.
Applies to the non-work mode-share and TLFD targets alike (one basis);
WORK is exempt, its LODES target is already support-matched.
Bands lying entirely beyond the cutoff get target 0 and drop out of the fit.
Leave this on. Off restores the untruncated national pool, which in a compact
study hands the top distance band a target its geometry cannot deliver: the
band saturates, its constant runs away, and access inflates at the periphery
(docs/developers/gotchas.md, "the open top distance band SATURATES"). The key
exists so an A/B is one line, not because off is a supported configuration.
LOGSUM_NONWORK_REPORTING_FACTORS★
WALK
1.5
default
Survey reporting corrections on the non-work NHTS mode-share targets, applied
per NHTS cell as the survey is read (see the methodology page for why per-cell
and not on the assembled target). Per-mode multiplier on the cell's share,
renormalized; {} reproduces the uncorrected calibration exactly. Only WALK has
any evidence behind it — do not add modes here without some
(docs/modelers/methodology/survey-reporting-bias.md). WORK targets are ACS and
are never touched; the 2022 RIDEHAIL wave is never touched.
⚠️ WALK = 1.5 IS A JUDGEMENT CALL, AND THE EVIDENCE FOR IT IS ONE CITY.
It is set from MWCOG's Regional Travel Survey 2017/18, which puts
car_owning/DISCRETIONARY walk at 9.6% against this codebase's 6.5% NHTS-derived
target — a ratio of 1.48, and a floor, since RTS drops loop trips from the trip
file entirely. Configured 1.5 DELIVERS 1.40-1.50 (below, see "delivered"), so
1.5 is the setting that targets that 1.48.
Consider the more conservative WALK = 1.25 instead. That is what the national
derivation supports on its own: f = (1 + rho*u_bar)/(1 + u_bar) with rho ~ 3.2
(Stopher & Greaves 2009 GPS audit) and u_bar = 10-12% (FHWA 2022 STT App. A)
gives 1.20-1.24, and 1.25 configured delivers ~1.21-1.25. 1.25 was the value
first adopted on that derivation, before the RTS evidence arrived.
The reason this is a judgement call and not a measurement: RTS is a one-day
recall diary, the same instrument family as NHTS 2017, so diary under-reporting
of short unplanned walk trips sits on BOTH sides of that 1.48 and largely nets
out. The surviving gap may be DC genuinely walking more than the national pool
conditional on car ownership - a target-transferability problem - rather than
survey bias, and a national factor set from DC then over-corrects every study
that is not DC. The separating test is not expensive and has not been run:
restrict NHTS 2017 to HH_CBSA 47900 (6,001 trips in the public file; the
shipped extract drops all geography, but connectome/reference/
_build_nhts_2017.py takes a source path) and rebuild the same cell on this
codebase's exact basis. If NHTS-DC lands near 6.5% the gap IS instrument bias
and 1.5 is vindicated; if it lands near 9.6% the gap is regional and this key
should go back to 1.25. Beware basis error - it is the documented hazard here.
DELIVERED IS NOT CONFIGURED. The odds map is concave, so correcting per cell
and then mixing over the study's density composition always delivers less than
the configured factor. Measured on Burlington at 1.5: delivered odds ratio
1.4036-1.4989 classless (19.3% of the correction lost at the worst cell,
car_deficient/SCHOOL) and 1.4514-1.5000 with density classes on; ASC shift
-0.339 to -0.405 utils. The calibration report states the delivered range, not
this number. At 1.25 the same fixture delivered 1.207-1.250.
Two live exposures at this magnitude:
- GATES NOT RUN. The planned blocking validation gates (the
per-cell ASC-shift oracle, at-bound/bundling churn, fare elasticity, the
B08134 walk hold-out, on the bundled example then a larger fixture) have not been run at any
factor, and 1.5 raises the stakes on the at-bound/bundling-churn gate specifically: the correction
pushes carless walk targets hard (carless/SCHOOL 0.523 -> 0.622 on
Burlington), which drives CAR_NONHH and TRANSIT constants further down in
cells that already clamp on LOGSUM_ASC_UNIVERSAL_BOUND_MIN.
- RECALIBRATION EXPOSURE. Every study recalibrated after this flip absorbs
the correction silently; the calibrated artifact is not auto-invalidated.
Check each such study before trusting a
cross-vintage comparison, and see the longitudinal-comparability protocol
in docs/operators/configuration-reference.md, which names this key.
It corrects the WALK anchor, so with WALK = 0 it shifts every non-walk ASC down
by roughly the same amount — it does NOT close gaps AMONG non-walk modes, and
is not a fix for a car/transit discretionary gap.
LOGSUM_CALIBRATE_CURVATURE
true
default
--- curvature + elasticity calibration blocks ---
CURVATURE: the
curvature block fits the walk/bike kink against a level-free odds-PROFILE
target over fine distance bands (base-anchored, EXERCISE excluded, study-
weighted — replaces the two-point double log-ratio). WALK is fit on an
(s1, T) grid × damped s2 (35 warm IPFs over the full s1 ∈ [1,4] window,
profile max|R| objective); BICYCLE
fits s2 on its own <1 mi-based grid with s1 = 1 and T ≈ 39 declared.
A mode whose pooled support is too thin degrades to the
transferred MTC slope (5.0) with the banded target validation-only. The fitted
(SLOPE1, THRESHOLD_MIN, SLOPE2) is written into the calibrated file's
LOGSUM_WALK_KINK / LOGSUM_BIKE_KINK. false reproduces the plain ASC+band
calibration exactly.
LOGSUM_CURVATURE_FIT_MODES
["BICYCLE"]
default
Modes ELIGIBLE for curvature fitting. A mode listed here still degrades
to the transferred slope when its NHTS support is thin; a mode NOT
listed keeps the kink declared in LOGSUM_{WALK,BIKE}_KINK (or the transferred
slope if none is declared), with its banded target validation-only.
WALK is left out on cost, not identifiability. Fitting it means the
35-point (s1, T) profile grid, which measured **92% of a 36-minute mid-size-metro
joint loop** (33.2 min; 279 of 280 inner iterations) to move WALK profile
max|R| from a 1.763 median to 1.377. Pinning the declared kink instead costs
+0.2% on that objective and takes the joint loop to ~3 minutes. BICYCLE stays
fitted: it is s2-only, rides along inside the existing IPF, and needs no grid,
so it is effectively free. Restore ["WALK", "BICYCLE"] to re-enable the grid
— WALK remains identifiable (the odds-profile target the short band cannot
pollute, plus the s1 numeraire).
LOGSUM_ASC_UNIVERSAL_BOUND_MIN
240
default
Universal generous ASC bound: every calibrated ASC is clipped to
±X generalized minutes (× |c_g| utils) after each update sweep. Stops the
unbounded random walk of cells whose walk-anchored target is unreachable
(carless/WORK/RIDEHAIL reached -15 utils in perf validation;
carless/DISCRETIONARY/CAR_NONHH ran to ~305 min and 2-cycled the IPF).
Cells ON the bound are excluded from the convergence residual while there
and listed loudly in the calibration report + toml provenance header. 240 =
2x the 120-min review bound: the largest well-fitting genuine constant
observed (carless captive car access, ~123 min) sits far inside. The
120-min review WARNING and the thin-cell bundling clamp are unchanged.
LOGSUM_BAND_UNIVERSAL_BOUND_MIN
720
default
Universal distance-band constant bound: the same clip for the TLFD band
constants. Same failure mode one
level up — a band whose target the study's geometry cannot deliver ramps
forever, because the update rule only watches the modelled share and that
share SATURATES once the band's few reachable origins are fully committed.
One small study's DISCRETIONARY ">20 mi" band (a 20-23.5 mi sliver reachable from 7%
of the population) ran to +25.68 utils = +1,467 gen-min while the share it
controls moved in the tenth significant figure; because the band constant
sits inside L_dc, that inflated access 2.26x for the 38 zones that reach it.
720 = 2x the largest well-fitting genuine constant observed across the 19
calibrated studies (a large Pacific Northwest metro's DISCRETIONARY band 4, -371 gen-min) —
the same rule the ASC bound above uses. It clips no existing study but
that small one. NOTE this is a runaway BACKSTOP, not a fix: it cannot be set tight
enough to repair such a study without clipping legitimate large-metro
constants. The fix is to stop asking for unreachable trip lengths (truncate
non-work TLFD targets to the study's reachable support). Bands ON the bound
are reported loudly but, unlike ASCs, stay IN the convergence residual.
LOGSUM_CALIBRATE_ELASTICITY
false
default
ELASTICITY: an outermost loop perturbs the transit
fare ±10%, measures the modeled fare elasticity on the pure-function evaluation
path, and nudges the single global LOGSUM_COST_SCALE toward the published
target (row selected by LOGSUM_ELASTICITY_CONVENTION from
reference/elasticity_targets.toml), re-running the inner IPF after each scale
change. Skipped when the study has no transit fare. false leaves
LOGSUM_COST_SCALE at its configured value.
OFF by default: the fit cannot succeed and distorts
on the way: with trip generation and car ownership frozen, even the
friendliest defensible target (rail-blended ~-0.25) extrapolates past the 4.0
scale bound (DC pinned at 4.0, modeled -0.21 vs -0.40), and every reachable
scale value multiplies cost-only sensitivity — splitting the model into two
effective VOTs (routing at 1x, choice at scale x) and amplifying spatial cost
gradients (parking, fares, tolls) by the scale. A cost-only multiplier is the
wrong lever; the principled fixes are whole-utility (lambda) sensitivity or
response through the frozen margins (cf. UK TAG M2 realism tests, which
prefer lambda and sanction cost *damping*, not cost amplification). The
measurement is kept as validation via LOGSUM_REPORT_ELASTICITY below.
LOGSUM_REPORT_ELASTICITY
true
default
With the fit off, still measure the modeled fare elasticity once on the final
calibrated parameters and print it in the calibration report next to the
published value — so the accepted under-response of cost-based scenarios
(fares/tolls/parking pricing) stays loud instead of hidden. Never changes
LOGSUM_COST_SCALE.
LOGSUM_ELASTICITY_PERTURB_PCT
0.1
default
Elasticity outer-loop tuning knobs. Consumed by calibrate-logsums
only. PERTURB_PCT = the ±fare shift
used to measure the point elasticity; DAMPING = λ_e on the multiplicative
cost-scale update; SCALE_MIN/MAX clip the fitted LOGSUM_COST_SCALE (a hit
bound warns that the target may be unreachable with the model's fixed
margins).
LOGSUM_ELASTICITY_DAMPING
0.5
default
—
LOGSUM_ELASTICITY_SCALE_MIN
0.25
default
—
LOGSUM_ELASTICITY_SCALE_MAX
4.0
default
—
LOGSUM_WORK_RESIDUAL_TRANSFER★
true
default
--- work-residual transfer (regionalizes the non-work mode-share targets) ---
The non-work targets are a national-by-density NHTS prior; a region's own
commute behaviour (ACS journey-to-work) reveals how far its transit/walk/bike
affinity departs from that prior. This knob transfers that departure onto the
non-work targets before the ASC fit:
target'[seg,group,m] ∝ NHTS_nonwork[seg,group,m] · (ACS_work[seg,m]/NHTS_work[seg,m])^FRACTION
multiplicative + renormalized per (segment,group), so it is anchor-free and
walk transfers too. FRACTION damps the work→non-work carryover (peak-oriented
commute transit carries less than all-day walk/bike — a scalar is the v1
approximation; a per-mode fraction is a future refinement). CARLESS selects the
carless source: "regional" borrows the clean car_owning place-effect (CAR mapped
to CAR_NONHH) because the ACS-carless ("no vehicle kept at home") vs NHTS-carless
("zero vehicles") definitional mismatch makes the carless own-residual
unreliable; "own" uses it anyway; "none" leaves carless on the NHTS prior.
EXCLUDE_MODES drops named modes from the transfer (they stay on the prior).
TRANSFER=false reproduces the untransferred calibration exactly.
LOGSUM_WORK_RESIDUAL_FRACTION
0.7
default
—
LOGSUM_WORK_RESIDUAL_CARLESS
"regional"
default
"regional" | "own" | "none"
LOGSUM_WORK_RESIDUAL_EXCLUDE_MODES
["RIDEHAIL"]
default
RIDEHAIL excluded by default: ACS journey-to-work has ~no taxicab commuters, so
the work signal spuriously suppresses non-work ridehail (which skews non-work —
nightlife/airport) toward the floor and drove the worst extreme-ASC flag. Held
at the NHTS prior instead. Empty list transfers every mode.
LOGSUM_ASC_BUNDLING
true
default
--- thin-cell ASC bundling ---
Non-work mode-share targets rest on NHTS cells whose unweighted support can
be single-digit (car_deficient non-work TRANSIT: 9 trips nationally); free-
fitting those cells produces artifact constants (observed up to ≈ −347 min
of generalized-time equivalent) that hard-code thin segments out of the
scenarios equity analyses care about. When a non-work target cell has fewer
than MIN_TRIPS unweighted trips behind it (or a floored target), its ASC is
tied instead of fitted: car_deficient/carless cells take car_owning's
constant plus the ACS-observed WORK segment offset (the segment analog of
the work-residual place transfer above); where the WORK signal is itself
junk (RIDEHAIL) they track car_owning's constant directly; car_owning's own
thin cells are clamped at ±120 min equivalent. Bundled cells leave the
convergence residual. Every bundle is recorded in the calibrated file's
provenance header and the calibration report. BUNDLING=false reproduces the
unbundled calibration exactly.
LOGSUM_ASC_BUNDLE_MIN_TRIPS
30
default
—
LOGSUM_DENSITY_CLASSES
false
default
Density-class ASCs: calibrate mode constants per
(segment, purpose group, density class) — four urbanization classes (rural /
suburban / urban / dense) collapsed from the NHTS 8-bin population-density
scale, frozen per study at the baseline (input_data/density_classes.csv).
Targets come from the same ACS/NHTS families cut per class; thin classes
merge into a populated neighbour before targets are built; RIDEHAIL keeps a
single class-invariant constant per (segment, group). false ⇒ today's
regional calibration and evaluation exactly (the off-switch).
Kept false BY DECISION: implemented and functional, but a DC density ×
cost-scale 2×2 test showed no observed realism benefit, and both density-ON standalone
recalibrations failed to converge (limit cycle / bound-clamped thin per-class
cells) while the classless arms converged comfortably. Per-study opt-in
remains supported.
LOGSUM_PREWAR_WALK★
false
run timeconnectome/analysis_parameters.tomlthe files said true (default)
--- Pre-war walk term. A per-HOME-zone
constant in utils on WALK, for home-based trips of the car_owning and
car_deficient segments only:
term[o] = s · GROUP_SCALE[group] · k[segment] · min(census_pre1940_share[o], CAP)
census_pre1940_share is the zone's share of housing units built 1939 or
earlier (ACS B25034_011 / _001), written at the baseline's destinations stage and
frozen for scenarios (housing stock is not a scenario lever); a zone with no
housing units (NaN share) gets 0. The term is added after the ASCs resolve, at
every home-based mode-choice site (evaluation, calibration, validation, the
applet); it is not an ASC override, and WALK ≡ 0 still holds for the
constant part. Recalibration absorbs its population mean into the other
modes' constants, so what it moves is the SPATIAL pattern of walking across
zones of different housing age, not the regional walk share.
Basis (two calibrated large US metros). ACS tract
journey-to-work mode shares of car-owning workers, regressed on the
calibrated model's own predicted log-odds plus a generic control set — the
model's own destination-weighted walk time and the tract's within-household
car-deficiency share (B08203), no jurisdiction dummies — leave a positive
pre-1940 residual in both metros: WALK gains against CAR, and against
TRANSIT where transit is readable, a walk preference shift rather than a car
cost. It survives streetscape, self-employment and recent-mover controls, is
flat across walk length (a constant, not a multiplier) and flattens above
about half pre-war (hence the cap). The leading reading is residential
self-selection among car owners; SELF_SELECTION scales the term for the
sensitivity band. Carless households show no residual and get no term.
Re-estimation triggers: any change to the walk kink, the density-class ASCs,
the non-work walk reporting factor (LOGSUM_NONWORK_REPORTING_FACTORS), the
work-residual transfer, the cap, the segment definitions, or the control set
/ frame convention behind k_lin re-opens K_LIN and GROUP_SCALE.
On by default, with the delivery calibration below.
Off, no evaluation output changes. On, k must come from the delivery
calibration (LOGSUM_CALIBRATE_PREWAR_WALK) or be set by hand
(LOGSUM_PREWAR_WALK_UTILS_PER_UNIT); the run raises otherwise.
The default is GUARDED (load_analysis_params): when no study or scenario
tier sets this key, the term resolves OFF for a study whose calibrated tier
predates it (no LOGSUM_PREWAR_WALK_UTILS_PER_UNIT) — outside
calibrate-logsums, which adopts it — and for a study with
LOGSUM_DENSITY_CLASSES on (no combined design yet). Each logs one WARNING.
Set the key in a study tier to override the guard either way.
LOGSUM_PREWAR_WALK_K_LIN
car_owning
1.54
car_deficient
0.81
default
k_lin: the national TARGET slope, per segment, read with the model's own
logit held at slope 1 (λ ≡ 1): the coefficient on min(share, CAP) when the
ACS tract WALK-vs-CAR log-odds MINUS the model's own log-odds is regressed
on the generic controls above. It is the residual the model leaves, in
model-logit units per unit min(share, CAP), and so exactly what the term
must DELIVER (the delivery block's D is the same slope of the model's own
log-odds), not the utils that deliver it: destination choice dilutes a
per-origin constant, so the k that delivers k_lin is study-specific and
larger (≈ 1.3–2×). The delivery calibration below fits that k. (The free-λ
reading β/λ, 3.54, was the first shipped value; it assumed the model's own
gradient was mis-scaled, which the gate metros could not confirm.)
Estimate: car_owning 1.54, the inverse-variance pool of
five US metros' own slopes, each read with metro-specific constants and
controls, frame = tracts with ≥ 100 car-owning ACS workers and no transit
floor, weights from PUMA-clustered SEs: 1.54 ± 0.33. Per metro 0.80–3.29;
the spread is consistent with noise (Q = 4.9 on 4 df), so one national
value, not a per-study fit. History: 2.28 was a joint fit over the first
two metros; three out-of-sample gate metros then predicted an overshoot in
one (the term moves the slope by almost exactly the delivered D, so the
term-on residual is the term-off residual minus k_lin), and all five were
re-pooled. Leave-one-out, each held-out metro lands within 2 SE of 0.
The worker-weighted joint fit over the same five is 1.83 ± 0.35; the pool
is preferred because it weights metros by how precisely each is measured.
The specification range across other control sets was measured for the
free-λ β/λ reading only (pooled 2.68–4.92 there); expect a comparable
relative spread. The frame convention matters: a 2 % model-transit floor
gives a joint 2.25 over the same five metros (per metro 1.8–3.7).
What it fixes: the tract bias by building age (the model's walk share too
flat across pre-1940 share). It explains only ~2–8 % of tract-level
residual variance; it does not make tract shares accurate by itself.
car_deficient = 0.52 × car_owning (the car_deficient/car_owning ratio of
the first estimate in the largest metro; the direct five-metro joint fit
gives 1.02 ± 0.32, consistent).
Re-estimate on any trigger above. These were estimated on studies that set
some keys away from the codebase defaults, and a change to any of them
re-opens K_LIN too: the bicycle curvature fit (off there), the per-purpose
utility scales, NHB demand and generation response (on there), the transit
in-vehicle multipliers and transfer penalties, and parking cost method.
"Not rejected in N gate metros", never "validated".
LOGSUM_CALIBRATE_PREWAR_WALK
false
run timeconnectome/analysis_parameters.tomlthe files said true (default)
The delivery calibration (calibrate-logsums). On, with the term on: k per car
segment is fitted so the model's delivered tract slope D equals K_LIN — D is
the worker-weighted WLS coefficient of the change in the model's tract
WALK-vs-CAR log-odds (term on minus term off) on min(pre-1940 share, CAP),
controlling for the generic set above, on the term-off pass's frame. A
secant at fixed constants plus an ASC re-fit after each step (outer loop:
re-fitting the constants raises D 5–25 %); the result is written to the
calibrated tier as LOGSUM_PREWAR_WALK_UTILS_PER_UNIT with its provenance,
and the calibration report carries the study's own ACS residual with the
term off and on (out-of-sample, never fitted). Needs census/tract_housing.csv
(B25034), census_blocks.gpkg and one extra ACS pull (B08203 3+-vehicle
cells, cached as census/tract_calibration_acs_b08203.csv); without them the
block degrades to k = 0 — the term OFF for the study — with a WARNING
(never K_LIN as k). It also writes LOGSUM_PREWAR_WALK_ASC_SHIFT, what the
re-fit absorbed of the term into each constant; whatever BORROWS a
constant (NHB legs, thin-cell bundle ties) reads the term-free value.
Off: k is read as given. On by default with the term.
LOGSUM_PREWAR_WALK_WARM_REUSE
true
default
Warm-round reuse of the delivery block's term-off pass 0 in the joint
calibrate<->equilibrate loop. Every joint round
re-runs calibrate-logsums, and with the block on each one repeats the
term-off pass 0 — a full IPF — which made a block-on warm round cost ≈ 3× a
block-off one. On: from round 3, a round skips pass 0 and reuses the
previous round's term-off frame and constants when ALL hold — the car skims
moved less than 1e-3 (|ratio − 1| of the loop's own skim-drift ratio, since
the GTMs the reused pass ran on); k did not move in the previous round (the
secant took no step); the cached products match this study, segment set,
CAP, control set, zones and the calibrated tier the round starts from; and
the start is warm. The secant and the term-on re-fit run as before, so a
warm round costs ≈ 1.3× block-off instead; cold and round-2 runs are
unchanged. When the loop converges on a round that reused, one fresh
term-off pass on the final GTMs refreshes LOGSUM_PREWAR_WALK_ASC_SHIFT and
the report (k and the constants stay). The one-shot calibrate-logsums CLI
never reuses. Off: every round runs its own pass 0, exactly as before.
Inert unless the block is on; not an evaluation staleness input.
LOGSUM_PREWAR_WALK_CAP
0.5
default
f(share) = min(share, CAP). The residual flattens above about half pre-war.
LOGSUM_PREWAR_WALK_GROUP_SCALE
WORK
1.0
SCHOOL
0.6
DISCRETIONARY
0.6
ESCORT
0.4
default
Per-purpose-group scale on k. The evidence is WORK; non-work groups take a
smaller share of it (starting values, to be fitted against the NHTS walk-share
density gradient). A group absent here gets no term.
LOGSUM_PREWAR_WALK_SELF_SELECTION
1.0
default
s: the share of the residual treated as causal. 1.0 ships; 0.5 / 0.9 for the
self-selection sensitivity band. Evaluation only: calibrate-logsums fits k
and the constants at s = 1 whatever s is set, so the band cannot be
absorbed by a larger k (calibration._prewar_fit_at_full_scale).
--- Non-home-based (NHB) chain access.
Measures onward reach FROM an activity anchor: standing at zone d mid-chain,
how much further opportunity is available? Distinct from every other logsum
quantity here, which is indexed by the traveller's ORIGIN (home).
`enabled = false` is the identity default (test A0): with it off, not one
number in evaluation changes. Note this whole table is captured by
`evaluation_param_keys` (evaluation.py:2302) because the top-level key is
LOGSUM_-prefixed — so flipping any knob below forces an evaluation recompute on
its own, with no `force_from_stage=`. That prefix is load-bearing; a
lowercase section name would flatten to an unguarded key and silently reuse
stale results.
WAVE BASIS: the shape parameters below are 2017-wave quantities (NHB trip
lengths, chain ratios). This is NOT the whole of calibration.py's wave
policy, which is split (_SHAPE_WAVE = 2017, _RIDEHAIL_WAVE = 2022, non-work
mode-share LEVELS from 2022). Shape is the 2017 arm, so 2017 is right here —
but do not generalize the choice to any level-valued NHB parameter added
later. Cross-wave chain ratios differ ~2x; never average the waves.
chain_purposes Trip categories that count as an onward stop from the anchor. A hand-picked
SUBSET of the DISCRETIONARY category list, not that list itself — AIRPORTS and
EVENT belong to DISCRETIONARY and are deliberately not chain stops.
WORK and SCHOOL are excluded to avoid crediting the host group's own purpose
twice — NOT because mid-chain work legs are rare. Measured on the 2017 NHTS
weekday extract, they are 21.0% of NHB departures from WORK and 15.0% from
DISCRETIONARY.
category_weights Relative weight per chain category when combining their destination logsums
into a weighted MEAN (never a sum — a sum would grow with the number of chain
categories). Empty = derive the weights from the study's own destination trip
basis, i.e. each category's flat trips/person/period. Weights are relative, so
doubling them all changes nothing.
centering Each category's destination logsum enters as a deviation from its own
unweighted mean over reachable anchors. Only 'per_category' is supported.
This is a welfare decision, not a presentational one: rescaling any chain size
vector by k shifts every category logsum by ln k uniformly, which cancels in
the destination probabilities but moves consumer surplus by phi·ln k / r_g
minutes — about 256 min/person for WORK under a factor-1000 size
renormalization. Centering also neutralizes the objection that a mean over
eight incommensurable size units (childcare slots, restaurant seats) is
arbitrary: each cancels against its own mean.
Arrivals weighting is NOT available for the centering mean, for an ORDERING
reason rather than a conceptual one: the chain vector must exist before the
primary-group loop, and modeled arrivals are only complete after it. Do not
"fix" this to arrivals without moving the whole chain pass.
asc_group The mode-choice fold for the chain leg borrows this group's ASCs, its
LOGSUM_UTILITY_SCALE c, AND its distance-band constants. One key governs all
three ON PURPOSE: ASCs are fit against a specific c and are only meaningful
against it (a changed c silently inflates every
util-native quantity, ASCs included).
Borrowing the BANDS keeps the chain leg consistent with
the study's own calibration state — an uncalibrated study gets zeros, a
calibrated one gets its fitted values, with no separate NHB calibration step —
and avoids planting a hand-set national constant in a slot the calibrator
writes. Zero bands are not "decay off" in a mild sense:
measured on Burlington they spread onward reach 5x wider than the calibrated
discretionary behaviour NHB borrows every other parameter from (0.95% mean
intrazonal destination mass vs 4.80% home-based vs 5.36% under the borrow).
The transferability objection to band constants is about copying BETWEEN
studies; this is a transfer within one study, between purposes, and the trip
length distributions support it: NHB legs to discretionary purposes run 2.4 mi
median against 2.9 mi home-based. It would NOT hold for WORK (3.5 vs 8.0),
which is one reason WORK is not a chain purpose.
The borrow also fixes r_nhb = theta * |c|, and therefore the scale of phi
below. It is a scaling decision, not only a mode-split decision.
theta The ONLY destination-side parameter that is NHB's own. UNCALIBRATED — there
are no NHB calibration targets and none are planned. Currently DISCRETIONARY's
value. Must be in (0, 1].
phi Expected onward legs per home-based arrival, per purpose group, RESTRICTED to
the chain_purposes basis so the parameter measures what the chain logsum
measures. The published unrestricted 2017 ratios (0.79 WORK / 0.47 SCHOOL /
1.12 DISCRETIONARY) count legs to purposes this model cannot represent and are
NOT the right numbers here — using them would have overstated the WORK term
2.8x. The restriction discards real behaviour: 28-50% of genuine NHB chaining
goes to unrepresentable purposes, the largest single omission being escort
(drop-off/pick-up, 4.22% of all weekday trips), so this term UNDERSTATES chain
value by construction.
A pure trip-count ratio: the utils conversion r_g/r_nhb happens in code at
evaluation time, so these values survive a theta or asc_group change
untouched. 2017 wave, per the calibration policy of never taking trip RATES
from the web-only 2022 wave.
Empty = identity, no chain term — the codebase ship default, study opt-in.
This default is deliberate while the chain-term / NHB-demand double-count
question is open: with the NHB demand layer enabled, NHB legs get their own
generated trips and consumer surplus while this term already credits the host
traveller with the onward leg's expected utility. Treat this term's welfare
claim as provisional until that resolves.
Recommended opt-in values: WORK = 0.439, SCHOOL = 0.233, DISCRETIONARY = 0.668
SCHOOL's 0.233 describes NON-escort school chaining specifically.
An earlier DISCRETIONARY value of 0.814 is SUPERSEDED, not an
alternative. The ratio estimator behind it counted departures from mid-chain
anchors in its numerator with no matching denominator entry, and DISCRETIONARY
is reached mid-chain 49.5% of the time — the sequence-linked re-tabulation
puts it at 0.668. WORK and SCHOOL are within 4% either way and are left on
the original estimator; do not read that as validating it.
Watch item: at these values the chain term is 0.89x / 0.41x / 1.30x the
travel-time signal in the same exponent. A DISCRETIONARY ratio above ~1 means
onward reach outweighs travel time in discretionary destination choice — a
substantive modelling claim to review, not a pass.
LOGSUM_ASC★
CAR
TRANSIT
BICYCLE
RIDEHAIL
CAR_NONHH
car_owning · WORK
-0.4469
-1.9549
-0.7655
-5.3539
·
car_owning · SCHOOL
-1.2603
-2.7365
-1.6002
-2.688
·
car_owning · DISCRETIONARY
-0.2981
-3.1917
-1.5619
-2.1
·
car_owning · ESCORT
-0.87
-2.7
-3.22
-2.76
·
car_deficient · WORK
-0.3032
-1.6345
-0.7815
-5.3583
·
car_deficient · SCHOOL
-0.8562
-2.067
-1.6161
-2.688
·
car_deficient · DISCRETIONARY
-0.1824
-2.8965
-1.4286
-2.1
·
car_deficient · ESCORT
-1.86
-3.02
-3.18
-3.0
·
carless · WORK
·
-1.1773
-1.7621
-6.0
-4.2524
carless · SCHOOL
·
-2.0463
-2.5968
-2.688
-5.3344
carless · DISCRETIONARY
·
-1.7711
-1.4818
-2.1
-2.9872
carless · ESCORT
·
-1.34
-2.43
-1.01
-2.62
calibratedcalibrated_logsum_parameters.toml48 of 48 values changed
car_owning --- ASCs: utils, walk-anchored (WALK ≡ 0, omitted). Segments = car-ownership
classes (user_classes.csv `car_owner`: "car" → car_owning, "nocar" → carless,
"car_deficient" → car_deficient — a three-way split). Starting values
= MTC prototype_mtc ASCs re-anchored to walk (subtract walk ASC per segment
× purpose, unweighted mean over the purposes in each group — WORK = work,
SCHOOL = school+univ, DISC = eatout/escort/othdiscr/othmaint/shopping/social;
atwork excluded; RIDEHAIL = tnc_single; drive-alone is MTC's reference so
re-anchored CAR = -walk ASC).
ESCORT rows are SEEDED FROM DISCRETIONARY, not independently derived: MTC's
escort purpose is one of the six averaged into this file's DISCRETIONARY
column, so the seed is that average rather than a foreign constant. They are
a starting point for calibration, not a measurement — escort travel is 92.4%
household car in the NHTS, well above the discretionary mix, so expect the
calibrator to move CAR up and the rest down. Do not quote them as escort
mode-choice parameters before a study has fit them.
≈ MTC auto_sufficient
car_deficient.WORK Transit sits above CAR at WORK (-1.75 vs -2.40) — MTC's middle segment is
transit's core constituency; the deficient CAR constants are strongly
negative because the segment's walk ASCs (the anchor) are high.
carless ≈ MTC no_auto; CAR absent by availability
carless.WORK SCHOOL row distorted by MTC's +18.4 school walk ASC; calibration will rewrite it.
CAR_NONHH starts from MTC's no_auto shared-ride constants: sr2 and sr3p
collapsed into one alternative via their shared-ride nest logsum (μ = 0.35),
then walk-anchored like every other value here.
The SCHOOL value inherits MTC's distorted +18.4 school walk ASC (same
distortion as this block's TRANSIT/RIDEHAIL rows) and so starts far from any
plausible share. Re-seeding it from burlington's fitted values was tried and
REVERTED: measured against a generous iteration cap it made convergence
slower (15 iterations to tolerance vs 13), because the binding constraint is
carless/WORK/RIDEHAIL, not CAR_NONHH.
LOGSUM_NHB_DEMAND
enabled
false
asc_groups
NHB_WORK
"WORK"
NHB_OTHER
"DISCRETIONARY"
NHB_ESCORT
"ESCORT"
default
---------------------------------------------------------------------------
THE NHB DEMAND LAYER — LINKED NON-HOME-BASED DEMAND GENERATION.
Distinct from [logsums.LOGSUM_NHB] above, the chain-access term: that block
moves UTILITY (an onward-reach term inside home-based destination choice);
this one moves VEHICLES (it generates the onward legs themselves). They share
a subject and nothing else — the chain term credits the host traveller with
chain value, this layer puts a trip on the network. Do not merge them.
WHAT IS NOT HERE, AND WHY. The generation rates r_hb, the destination splits
and the HB escort rate are DERIVED AT RUNTIME in the same destination_weighting
pass that builds f_c, not configured. They are window-dependent:
an all-day rate applied to an hour-8 arrival mass over-generates by
5.69x / 73.32x / 1.92x. Shipping them as constants would reintroduce that
error one config level up and would not inherit future filter changes the way
f_c does. Setting one of those keys here is a hard error, not an override.
The derived values land in input_data/trip_generation.json under `nhb_demand`,
stamped with the window and trip-filter state they were derived against.
The escort destination proxy-attractor weights are a POLICY choice rather than
a window-dependent measurement, so they stay in config — but under
[destinations] ESCORT_ATTRACTOR_WEIGHTS, beside the other destination-weighting
knobs, because ESCORT became a first-class trip category
and destination_weighting is what reads them.
TRAFFIC-ONLY. The layer's groups contribute trips — p_veh, balancing,
materialization, feedback, VMT — and are held OUT of access_*, consumer
surplus and the zone summary. The motive is traffic; the access side of NHB is
already served by the LOGSUM_NHB chain term, and keeping this layer out of
the welfare arithmetic dissolves the double-count rather than resolving it.
ESCORT is
in the same seam for the same reason: an escort leg counted as *access*
is a welfare statement this design deliberately avoids.
asc_groups The purpose group each anchor group borrows its ASCs and utility scale c from.
One key governs both, for the LOGSUM_UTILITY_SCALE reason: ASCs are only meaningful against the
c they were fit at. Each anchor group borrows the group whose travel its own
legs most resemble — work-anchored legs are long and work-destined (85.1% to
WORK in the AM window), escort-anchored legs are 92.4% household car — so the
borrow also tracks that group's calibration state instead of planting a
national constant in a slot the calibrator writes.
UNCALIBRATED SEED, like theta above: NHB mode-choice ASCs of their own are a
calibration question, not a config one.
LOGSUM_ASC_BY_CLASS
(empty table)
default
--- Per-class ASCs (LOGSUM_DENSITY_CLASSES = true). Machine-written by
`calibrate-logsums` as [logsums.LOGSUM_ASC_BY_CLASS.<segment>] with one
d1..d4 table per purpose group. Empty here, and a SIBLING of LOGSUM_ASC
rather than a different leaf shape inside it: the tiers deep-merge
dict-against-dict, so a d1..d4 table written into LOGSUM_ASC above would
union with those scalars inside one leaf instead of replacing them, and the
resolved config would carry both schemas at once. When this key is
non-empty it wins outright and the LOGSUM_ASC block above is ignored.
[emissions]7 keys
GHG emissions (evaluation)
Well-to-wheel (WTW) OPERATIONAL emissions only — fuel/electricity production
+ combustion. Embodied vehicle/infrastructure emissions are intentionally
EXCLUDED. Emissions per O/D/mode = distance(km) × factor. Walk/bike = 0.
These are ROUGH national-average defaults — review/localize before client
reporting (e.g. bus/rail split, ERCOT grid for rail/EV). Sources cited inline.
Key
Effective value
Set by
TOML comment
EMISSION_FACTOR_CAR_KG_PER_KM★
0.31
default
Car: EPA "typical passenger vehicle" ~400 g CO2/mi tailpipe, ×~1.25 well-to-tank
upstream (gasoline, Argonne GREET) ≈ 500 g CO2e/mi ÷ 1.609 ≈ 0.31 kg/veh-km.
Single-occupant (per-vehicle = per-passenger). Source: EPA Greenhouse Gas
Emissions from a Typical Passenger Vehicle (epa.gov/greenvehicles).
EMISSION_FACTOR_TRANSIT_KG_PER_PKM★
0.09
default
Transit: single blended bus+rail factor (the model can't split bus vs rail per
trip without detailed itineraries). Between US rail (~0.05-0.06) and
diesel-bus (~0.12-0.16) WTW per passenger-km, occupancy baked in. The FTA
bus-dominated national average (~0.45 lb/pmi ≈ 0.13 kg/pkm) overstates rail; a
rail-only system would be ~0.06. Refine via a bus/rail split (r5py itineraries).
Source: FTA/APTA transit GHG reporting + US rail per-pkm figures.
Be precise about which midpoint 0.09 is: it is the midpoint of the two ranges'
INNER edges (0.06 and 0.12). The midpoint of the two range midpoints is 0.0975
and the midpoint of the full span is 0.105. And an unweighted midpoint is
itself a choice — no real system's bus/rail passenger-km split is 50/50, so a
study with a known mode mix should weight rather than inherit this.
EMISSION_FACTOR_WALK_KG_PER_KM
0.0
default
—
EMISSION_FACTOR_BICYCLE_KG_PER_KM
0.0
default
—
RIDEHAIL_DEADHEAD_FACTOR
1.4
default
Ridehail = car factor × deadhead. UCS "Ride-Hailing's Climate Risks": ~40%
of ride-hail VMT is deadhead (between fares) → ×1.4. Occupancy ~1.
Source: Union of Concerned Scientists (2020), blog.ucs.org.
Two consumers share this one constant so they cannot drift apart: the
emissions kernel (connectome/access_kernel.py:_load_emission_factors) and
logsums vehicle loading (logsums/evaluation.py:
resolve_ridehail_vehicle_factor), which loads 1.4 x person trips onto the
occupied O->D pair — a modelling simplification (deadhead miles actually
happen between fares, not on this path); see
docs/modelers/methodology/ridehail-vehicles.md.
TRANSIT_DETOUR_FACTOR
1.3
default
Transit in-vehicle distance ≈ great-circle O/D × detour factor (rough; no
transit network distance is computed). Engineering assumption, not a citation.
EMISSION_CAR_CIRCUITY_FACTOR
1.3
default
Car/ridehail road distance ≈ great-circle O/D × circuity factor. Reported
car distances are derived from straight-line O/D distance (no car network
distance is computed), so car needs its own circuity multiplier the way
transit has a detour factor. ~1.3 is a typical urban road-network circuity.
Rough; engineering assumption, not a citation.
⚠ The two factors above are both 1.3, and their EQUALITY is itself an
unexamined claim: a transit alignment's detour from the straight line and a
road network's circuity are different quantities with no reason to coincide.
Published urban road circuity is commonly around 1.2–1.4, so the car value is
at least in range; nothing supports the transit one beyond it being the same
number.
[refine_zones]12 keys
refine_zones: Refine zone geometry
Clips uninhabited OSM polygons (parks, airports, cemeteries, military bases,
water, woods, wetlands) out of analysis-zone geometry. Splits any zone that
the mask bisects into two or more substantial parts (e.g. Central Park
splitting an Upper-East/Upper-West tract). Drops zone parts below the
sliver threshold and fills tiny interior holes.
Runs BEFORE people and destinations so pop/dest land
directly on already-refined geometry — no late snap-and-redistribute is
needed for the geometric work.
phase D: fill zone-part holes below pct of original area
zone_sliver_threshold_pct
0.05
default
phase D: drop parts below pct of original area
zone_sliver_threshold_abs_m2
2500
default
phase D: floor for sliver threshold
empty_tract_rule
true
default
Empty-tract rule. OSM natural=wood is
landcover, not land use: it marks undeveloped land well in towns, but in rural
areas it is drawn over houses (Burlington: ~17,600 people lived in block area
under it, and refine dropped them). With the rule on, a zone loses a piece of
the mask only if the piece holds no Overture address (within the buffer) and
its area is at least `empty_tract_min_share_of_zone` × the zone's area. A piece
cut off from its zone by a cut is still dropped as a sliver, houses or not (no
islands); its people stay in the model through block apportionment.
Share 0.75: chosen from a Burlington sweep over 0.10/0.25/0.50/0.75,
in which routing accuracy was flat (walk mean error 8.57–8.60 min, car
1.52–1.55), so it rests on zone form: rural zones keep their woods, and only
zones that are mostly empty lose land.
⚠ The 100 m buffer is PROVISIONAL and was not swept. Its only basis: in
Burlington it freed every masked address for 7.6 % of the mask area.
empty_tract_address_buffer_m
100
default
—
empty_tract_min_share_of_zone
0.75
default
—
orphan_block_snap_max_m
2000
default
A census block with residents or jobs that refine left with no zone piece
goes to the nearest zone within this distance (block_apportion). Beyond the cap
it is dropped and logged. ⚠ PROVISIONAL: 2 km is judgement, not yet measured.
[drop_sparse_zones]3 keys
drop_sparse_zones: Drop sparse zones
Drops zones that fall below every enabled threshold — population
(`MIN_POPULATION_PER_GEOM`) AND trip-ends (`MIN_TRIP_ENDS_PER_GEOM`, both
under [analysis_areas]); clearing either bar retains the zone. Population
+ destination counts of dropped zones snap onto the nearest retained
neighbor.
Preservation invariant: every person and every destination present before
drop_sparse_zones is present after. Snap method is pluggable via
`snap_method`; today
only "nearest_zone" is registered.
diverging_scale_percentile symmetric anchor: Nth percentile of |B − A|
floors Comparison noise floors: a per-zone change smaller than its family's floor is
model noise (mostly where the demand-feedback loop stops inside its
convergence band), and a compare view treats it as no change. One scalar per
family, in the family's own per-person unit; resolved by
connectome/comparison_floors.py, which also maps each compare row to its
family. A study or scenario overrides any key here (write its basis beside
it); setting one to a value below the noise unmasks noise, so prefer raising.
These defaults are documented constants, not per-run measurements. A study-specific measurement was tried and
dropped: the backward twin measured 0.04-0.40 of the real delta noise, and
scenarios on one study differed 6x. Scaling by congestion delay was rejected
too (delay ratio 51x vs floor ratio 3-13x across two studies). Expect the
defaults to over-mask small or uncongested studies a little and to under-mask
a scenario that perturbs the network hard. Per-scenario floors and tighter
stopping are possible follow-ups.
The opt-in twin ([assignment.feedback] measure_noise_floor) never sets a
floor: it reports a measured lower bound and warns when that exceeds the
floor in use.
floors.access_min_per_person equivalent minutes per person per period. basis: a 2,061-zone congested northern Virginia study, far-field noise of a BRT scenario vs control, |delta| p99.5 over zones >20 km from the BRT stops (2026-10-14)
floors.trips_per_person trips per person per period, per mode. basis: burlington tap-b, real delta error vs an 8-outer-iteration reference, p95 over 304 zones, max over modes, larger of i189_toll (1.225e-5) and lake_toll (2.3e-6), rounded up (2026-10-18)
floors.vm_miles_per_person vehicle-miles per resident per period. basis: as trips; larger of lake_toll (1.315e-3) and i189_toll (1.185e-3), rounded up
[storage]3 keys
Storage
On-disk float precision for big square n×n OD matrices (written as numpy
.npz containers by connectome/matrix_io.py). "float32" halves file size and
I/O cost versus "float64" with no correctness concern (per-cell relative
error ~1e-7). "float16" is a selectable opt-in — unvalidated, and large
population×access products can overflow its ±65504 range to inf.
Does not apply to bool (checkpoint) matrices.
matrix_compress — adaptive on-disk compression for the same .npz matrices:
"adaptive" (default) — deflate(level 1) bool and SPARSE float
matrices (mostly-inf, e.g. WALK/BIKE/TRANSIT impedances ~30-85x);
STORE dense float (CAR/RIDEHAIL) raw, since
they barely shrink and cost ~29x more to write.
"off" — store every matrix uncompressed (historical behavior, memcpy-speed).
"always" — deflate(level 1) every matrix (rarely worth it on dense data).
Reads are transparent (np.load handles both); legacy uncompressed .npz still load.
matrix_read_dtype — the float dtype read_matrix RETURNS. Matrices are stored
float32 (matrix_dtype above), so a float64 read carries no extra information
about the data: it materializes a full-width copy of values that are already
float32-exact, and the double width protects only downstream ACCUMULATION from
rounding. Every accumulation site in the codebase already casts explicitly at
the point of use (demand/balancing.py, demand/trip_tables.py, demand/vmt.py,
traffic_utils/od_materialization.py, attachment_metrics.py all build float64
accumulators and call .to_numpy(dtype=np.float64)), so the read-time upcast is
pure cost. "float32" is therefore the default; a caller that genuinely needs
float64 out of read_matrix opts out per-call with `dtype=np.float64`.
⚠️ Legacy .csv/.parquet matrices were not float32-quantized at write, so under
"float32" they are downcast on read — set "float64" to read those verbatim.
Key
Effective value
Set by
TOML comment
matrix_dtype
"float32"
default
—
matrix_read_dtype
"float32"
default
—
matrix_compress
"adaptive"
default
—
[mutations]1 key
Scenario mutations
REQUIRE_FRESH_INPUTS: a mutation breaks copy-on-write by snapshotting the
baseline input it edits, and for a directory target (traffic/, GTFS/, walk/)
the snapshot freezes files the mutation never touches. If the baseline is
later rebuilt outside run_study's setup-force path, the mutated arm keeps the
old snapshot while its unmutated siblings track the new baseline — one study,
two input sets. Each scenario records what it copied from in
`.cow_provenance.json`; true (the default) raises when the source has since
changed, false downgrades that to a warning.
Key
Effective value
Set by
TOML comment
REQUIRE_FRESH_INPUTS
true
default
—
[demand]7 keys
Absolute demand
Generation, distribution, and attraction balancing (see
docs/modelers/methodology/absolute-demand.md). Every knob here defaults to identity/off: a study that sets none
of them runs bit-identically to pre-demand (acceptance test A0). Keys flatten
to module-level names like everything else, so they are UPPERCASE.
Key
Effective value
Set by
TOML comment
DEMAND_BALANCING_GROUPS
work
["WORK"]
default
Attraction-end balancing groups: {balancing_group -> [purpose_group, …]}. Only
constrained groups are named; every unlisted purpose group (SCHOOL,
DISCRETIONARY, …) falls into DEMAND_BALANCING_RESIDUAL_GROUP and is
production-only. v1 hard-constrains WORK to lodes_jobs; SCHOOL joins by adding
`school = ["SCHOOL"]` here once an enrollment ingestion exists.
DEMAND_BALANCING_RESIDUAL_GROUP
"nonwork"
default
—
DEMAND_BALANCING_TARGETS
work
"lodes_jobs"
default
Attraction target per constrained balancing group: {balancing_group -> zone
attractiveness column}. The column is shape-scaled to the group's own modeled
production total at balance time, so a modify_jobs mutation flows
straight through. Groups absent here (the residual, and any constrained group
without a target) are production-only — B_g ≡ 1. v1 constrains WORK to
lodes_jobs; SCHOOL joins by adding `school = "school_enrollment"` once an
enrollment ingestion exists.
balancing
enabled
false
max_iter
50
tol
0.0001
damping
1.0
default
IPF solver knobs. `enabled` is the master switch: off ⇒ the
post-pass writes no B_g factors and materialization applies none (identity,
test A0). tol is in trip units (max abs marginal error); damping in (0, 1]
slows oscillation (1.0 = undamped).
generation_response★
enabled
false
on_stale_a_ref
"warn"
epsilon
(empty table)
default
Generation response — accessibility-elastic trip frequency. The
induced-demand margin: f'_{u,c}[o] = f_c · (A[o]/A_ref[o])^ε_g, where A_ref is
the study baseline's converged per-origin access. `enabled` is the master
switch; every ε defaults to 0 (identity — f' = f, bit-identical to pre-demand,
test A0) until locally calibrated.
ε when enabled — DISCRETIONARY 0.1 (band 0.03–0.30), WORK/SCHOOL 0. Our ε is
exactly DfT TAG Unit M2.1's frequency scaling parameter θ_freq = λ_freq/λ_dest
(our power form is the P(travel)→0 limit of TAG's nested frequency layer), so
the guidance transfers structurally — but TAG §5.6.14 states "no explicit values
can be recommended", and no published number is directly transferable. 0.1 is
bracketed by TAG's Table A.1 back-out (0.04–0.11, car-only ⇒ biased low) and
RAND PRISM's direct logsum estimation (0.17–0.36, cross-sectional ⇒ biased high
via residential self-selection). It sits deliberately at the low end: TAG §4.7.8
warns that frequency elasticities are inflated in models that omit active modes,
because frequency then proxies for walk/cycle substitution — Connectome models
walk and bicycle explicitly, so that share is already endogenous and must not be
re-counted here. WORK/SCHOOL ≈ 0 is both estimated (PRISM commute ε ≈ 0.011) and
doctrinal (TAG §4.6.6, doubly-constrained travel).
SCALE CONDITION — ε is dimensionless only relative to the destination-choice
utility scale. Re-derive it whenever the logsum calibration changes what that
scale is (LOGSUM_COST_SCALE, LOGSUM_ELASTICITY_CONVENTION, or the binding target
in reference/elasticity_targets.toml). Holding the *number* fixed across a
recalibration does NOT hold the behavioural response fixed — most sharply for
cost-based policies such as tolls.
The band's derivation, sources, and known gaps are documented in
docs/modelers/methodology/absolute-demand.md.
`on_stale_a_ref` (warn|fail) fires when a scenario pivots off a baseline A_ref
built under different logsum parameters — mirrors traffic-flow's
on_stale_inheritance.
epsilon Per purpose-group elasticity {group -> ε}; unlisted groups are inelastic (ε=0).
time_of_day
enabled
false
factors
(empty table)
default
Time-of-day factoring — period→daily expansion. Connectome models
a single analysis period; these NHTS-derived per-category factors expand the
period's trips *out* to a daily total for the consumers that need one (VMT,
emissions, daily transit). `enabled = false` (default) ⇒ expansion is identity
(VMT/consumers report *period* quantities; the factor table is a new artifact,
never a perturbation of existing outputs — test A0). When enabled, factors come
from the NHTS departure-time profile for the scenario's analysis period; a study
may override any per-category factor (`factors = { WORK = 8.5, … }`) or the
category-summed `aggregate` with local data.
trip_tables
enabled
false
modes
[]
default
All-mode absolute person-trip tables. When enabled, evaluation
accumulates + persists a per-mode per-balancing-group person O/D per UC, and the
evaluation tail writes results/traffic/trips_{MODE}.npz (zone×zone period person
trips, balanced via B_g). `enabled = false` (default) — the per-UC per-mode
split is disk-heavy (modes × groups matrices per user class), so only a study
that wants the tables pays for them (test A0: additive, off by default). `modes`
restricts which modes get tables (default: all reported modes).
[debug]1 key
Debug
Lightweight per-stage snapshots of analysis_areas.gpkg, useful when
diagnosing where a column appears / disappears / mutates across the
pipeline. When enabled, each labelled setup stage copies the current
analysis_areas.gpkg into {input_dir}/debug_snapshots/analysis_areas_after_<stage>.gpkg.
No-op when the source file does not yet exist (early stages).
Key
Effective value
Set by
TOML comment
keep_stage_snapshots
false
default
—
Study-defined sections in the study's analysis_parameters.toml only
[study]study-defined7 keys · 7 overridden
Key
Effective value
Set by
TOML comment
baseline_dir
"baseline"
studyanalysis_parameters.tomlno codebase default
—
states
["VT"]
studyanalysis_parameters.tomlno codebase default
—
baseline_label
"Baseline (as observed)"
studyanalysis_parameters.tomlno codebase default
—
baseline_description
"The region as observed: population, land use and transit service as they are today, with nothing changed. Its difference from the control is the study's numerical noise floor."
traffic_analysis_parameters.toml Traffic assignment parameters · the study runs traffic assignment
[functional_classes]6 keys
Key
Effective value
Set by
TOML comment
freeway★
alpha
0.3
beta
8.0
capacity_perlane
1840
min_capacity_perlane
1700
max_capacity_perlane
2100
always_treat_as_signalized
0
signal_capacity_factor
0.5
must_be_grade_separated
1
osm_highway_tags
["motorway", "motorway_link"]
default
─── BPR / Functional Classes ────────────────────────────────────────────────
capacity_perlane is the HCM 6th Edition per-lane capacity of the UNINTERRUPTED
facility (or base saturation flow, for interrupted classes) — signal g/C enters
separately via signal_capacity_factor, and the peak-hour surge via
[capacity].phf. Keep each effect in its own knob; don't fold a derate into
these bases.
alpha Fully access-controlled freeways and motorways.
HCM 6th Ex. 12-4 basic freeway capacity is 2,250–2,400 pc/h/ln — an
uninterrupted-flow IDEAL. Observed urban breakdown/merge-influenced capacity
runs 1,800–2,100, and 1,840 (= 0.80 × the 2,300 ideal) sits in that band.
Chosen from a derate ladder (×0.85 vs ×0.80, two test fixtures): on a mid-size
tolled metro, freeway VMT ratio 1.222→1.130, arterial 0.718→0.749, majors −0.009;
Burlington (near free-flow guard) unmoved. This is the base's own value —
urban effective rather than ideal — NOT a folded phf/signal derate; those
still apply separately below. Uncalibrated stopgap pending a dedicated
congestion-grading calibration pass.
α/β match the HCM speed-flow shape: ~+30% travel time at capacity (speed ≈
0.77×FFS), near-flat below ~0.7 v/c. The 1964 BPR α=0.15 understates freeway
congestion at capacity.
capacity_perlane urban effective, not the 2300 HCM ideal
signal_capacity_factor signal_capacity_factor ≈ through-movement g/C, applied ONLY to edges ending at
a mapped signal node (identify_signal_approaches — same set the delay fold
uses). 0.5 is honest urban g/C territory; 0.6 would be generous.
For freeways this fires only on the rare mainline edge literally ending at a
signal (at-grade terminus, mapped ramp meter) — never near interchanges.
multilane_arterial
alpha
0.45
beta
4.5
capacity_perlane
1900
min_capacity_perlane
1600
max_capacity_perlane
2100
always_treat_as_signalized
0
signal_capacity_factor
0.5
must_be_grade_separated
0
min_lanes_per_direction
2
osm_highway_tags
["trunk", "primary"]
default
alpha High-capacity regional roads with at-grade signals; 2+ lanes per direction.
HCM base saturation flow 1,900 pc/h/ln (metro pop ≥ 250k); g/C is NOT baked in
here — signal_capacity_factor applies it where signals govern.
α/β upper-middle of MPO practice for signalized arterials: the curve carries
incremental signal delay growth with v/c on top of the fixed zero-volume delay
folded into t0 ([signal_delay]).
signal_capacity_factor ≈ g/C on signal-approach edges
smaller_arterial
alpha
0.7
beta
3
capacity_perlane
1700
min_capacity_perlane
1400
max_capacity_perlane
1900
always_treat_as_signalized
0
signal_capacity_factor
0.5
must_be_grade_separated
0
min_lanes_per_direction
1
osm_highway_tags
["trunk", "primary"]
default
alpha Lower-capacity urban/suburban arterials; 1 lane per direction.
Base saturation 1,900 × ~0.9 combined HCM friction adjustments (lane width,
parking, driveways, turns from the through lane) ≈ 1,700.
α set high within MPO practice range for signal-dense single-lane arterials.
signal_capacity_factor ≈ g/C on signal-approach edges
ramp
alpha
0.4
beta
4
capacity_perlane
2000
min_capacity_perlane
1800
max_capacity_perlane
2200
always_treat_as_signalized
0
signal_capacity_factor
0.5
must_be_grade_separated
0
osm_highway_tags
["trunk_link", "primary_link"]
default
alpha Trunk/primary on- and off-ramps. Short merge/diverge connectors; not lane-gated.
HCM 6th Ex. 14-12 single-lane ramp roadway capacity: 1,900–2,200 pc/h/ln by
ramp FFS; signalized terminals are handled by signal_capacity_factor.
signal_capacity_factor ≈ g/C at signalized ramp terminals
collector
alpha
0.8
beta
2.5
capacity_perlane
1500
min_capacity_perlane
1200
max_capacity_perlane
1700
always_treat_as_signalized
0
signal_capacity_factor
0.5
must_be_grade_separated
0
osm_highway_tags
["secondary", "secondary_link", "tertiary"]
default
alpha Secondary arterials and collectors.
Small-city base saturation 1,750 × ~0.85 friction ≈ 1,500.
always_treat_as_signalized: setting it to 1 applies the factor everywhere on
the class, an interrupted-flow friction proxy for unmapped control. Off by
default: stops are explicit ([stop_delay], tagged + imputed) and
the factor applies on stop approaches only. A study can
set it back to 1 to restore the whole-class proxy.
alpha Local streets, access roads, and minor roads.
Saturation flow heavily reduced by narrow lanes, parking, and pedestrians.
always_treat_as_signalized: off — see collector.
[capacity]1 key
Capacity Adjustments
Key
Effective value
Set by
TOML comment
phf
0.92
default
Peak hour factor: hourly volume / (4 × peak 15-min volume). Static assignment
solves one analysis hour against an hourly capacity, but breakdown is governed
by the peak 15-min flow rate, so effective capacity = capacity_perlane × phf
(HCM urban default ≈ 0.92). Applied at graph build to EVERY functional class,
multiplicatively with signal_capacity_factor. NOT applied to [custom_capacities]
overrides — those are operator-specified effective totals, used as-is.
[assignment]25 keys · 4 overridden
Static Traffic Assignment
Config for the UE assignment demand side: person car-trips → vehicle OD tables.
occupancy is applied IN-LOOP in evaluation (per trip category, the only place
category resolution exists); class_collapse groups the ~60 user classes into a
handful of assignment classes (VOT bands) for the solve.
Key
Effective value
Set by
TOML comment
enabled★
true
studytraffic_analysis_parameters.tomltrue
traffic_equilibrium ("Traffic Equilibrium") orchestration. ON by default
(joint calibration-equilibration default path): every new study
equilibrates with demand feedback unless it opts out at the study tier
(enabled = false).
false => traffic_equilibrium is a no-op
"raise" | "warn" when the rgap target is missed.
"warn" keeps the assignment side non-halting, but
non-convergence still fails LOUDLY: the warn path
writes a consequence banner to run.log (not only
warnings.warn, which never reaches run.log and
self-suppresses after one hit) — fatal → loud, not
loud → silent. Rationale: a study 40 minutes in should not
be destroyed by a solve that missed 1e-4 by a hair,
when the operator can read the final rgap and judge.
⚠️ That "by a hair" premise does NOT hold at regional
scale (measured 2026-08-21). At DC, BFW's sub-linear
tail means max_iter binds before the gap criterion
ever does: every solve stops at rgap 1.3-1.7e-3, i.e.
17x the declared target, and the delivered value is
uncontrolled (1.265e-3 and 1.717e-3 on two outers of
one run). So "warn" is not absorbing a near-miss
there — it is normalizing a systematic shortfall.
Worse for scenario work: base and scenario each stop
wherever the cap lands, so their DELTA is differenced
across two different convergence levels, which is the
specific practice the Caliper/FTA literature shows
produces biased project impacts (base-case VHT alone
drifts 1.43% across the gap range).
DELIVERABLE STUDIES SHOULD SET THIS TO "raise" AT THE
STUDY TIER, and use
engine = "tapb" where BFW cannot reach the target at
tolerable cost — at DC it reaches 7.2e-5. Background,
evidence and the two companion rules:
docs/modelers/methodology/assignment-convergence.md
on_stale_inheritance
"warn"
default
"warn" | "fail" on equilibrium-hash mismatch (child-side inheritance)
engine
"aequilibrae"
studytraffic_analysis_parameters.toml"tapb"
Which solver runs the user-equilibrium step: "tapb" (bush-based Algorithm B,
via the Ives fork of spartalab/tap-b — the default) or
"aequilibrae" (BFW, the permanent fallback). BFW's convergence tail is
sub-linear — DC's solves cap out at 100 iterations around rgap 1.5e-3, with
1e-4 an estimated 300-500 iterations further — where tap-b reaches 1e-4 in
minutes. Made the codebase default after a full DC tap-b run
and mileage on two smaller test metros.
The tapb engine is a SUPPLEMENT, not a replacement, and refuses what it cannot
do exactly: select-link solves (the ODME anchor phase) stay on AequilibraE
(the only pin in code, _pin_aequilibrae in equilibrium.py), and
differentiated per-class costs raise rather than silently collapsing.
Preloaded solves are NOT pinned — tap-b runs them directly, and background
preload IS carried through (fork PRELOAD FILE, same assigned+preload VDF
semantics), confirmed at DC scale (34,987 links /
1.44M veh preload through tap-b).
See docs/operators/traffic-assignment-runbook.md.
A configured tapb engine with no binary built degrades to "aequilibrae" with a
banner rather than raising; see the runbook's binary-resolution section.
tapb_threads
0
default
0 => all cores; set explicitly to benchmark engines on one footing
tapb_timeout_s
14400
default
hard subprocess ceiling; the solve's own stopping rules should bind first
tapb_rgap_target
1e-05
default
tap-b's own rgap target; replaces rgap_target on every tap-b solve (0 => use
rgap_target). AequilibraE solves (the anchor/ODME pin, the binary-missing
fallback) keep rgap_target: BFW cannot reach 1e-5 within max_iter.
Basis: at 1e-4 the target never binds on a warm
solve. One tap-b iteration already lands under it, so the achieved rgap is
incidental and differs across compared scenarios (one regional study: 3.5e-5
vs 1.5e-5 for two siblings). A scenario-minus-baseline delta is only as tight as its
looser side. Lane cuts on a mid-size tolled metro, abs veh-mi error of the delta vs a 1e-8
reference: Meeting St (138 veh-mi effect) 2,418 at 1e-4, 150-190 at 1e-5;
I-26 (116,856 veh-mi effect) 3,200-3,500 at 1e-4, 830-880 at 1e-5. Cost: a
warm solve 1 -> 2-4 iterations (13 -> 19-35 s at that scale); a cold
one 12-29 -> 32-43.
Every study's equilibrium moves by about the old noise floor on its next run.
tapb_seed_scenario_bushes
true
default
Seed a SCENARIO's cold tap-b bush cache from its BASELINE's. The warm
cache lives per-scenario (results/tapb_work/), so without this every scenario
pays a full cold bush build on its first solve even when its car network is
byte-identically the baseline's — measured on DC control: 33 iterations,
3,036.6 s. The baseline's bushes are COPIED in (never moved or linked: tap-b
mutates them in place) and only when the two exported problems hash equal —
the same identity key that governs bush reuse *within* a scenario, which is
what makes this safe rather than merely plausible. A scenario that does change
the car network (close_road, modify_capacity) misses the key and runs cold, as
it must. Either way the decision is one grep-able line in run.log:
grep 'tapb bush seed:' <scenario>/run.log
Costs disk: one bush-file copy per scenario (~1 GB each at DC scale), which
results/tapb_work/ already was. Set false to disable.
cores
0
default
Parallelism cap for the UE solve, both engines. 0 => every core on the
machine, which is each engine's own default — so a cross-engine A/B is
same-footing with nothing set here. Setting it caps whichever engine is
selected; `tapb_threads` above is the tap-b-specific override and wins there.
Asymmetry worth knowing: tap-b's threaded builds are not run-to-run
reproducible, so an A/B on tap-b *output* needs `tapb_threads = 1`;
AequilibraE is deterministic at any core count.
freeflow_car_ttm_stash
true
default
Free-flow car TTM stash. On the default path every
scenario re-routes car free-flow at routing — not because its network changed,
but because the pivot recipe declines to inherit the baseline's
assignment-coupled (congested) matrices. This keeps the free-flow matrices the
baseline computed just before its consistency pass overwrote them, and serves
them to any later car skim that is provably the SAME computation: byte-identical
routeenv graph, byte-identical rep points, same search limit and skim variant.
Anything else — a mutated car graph, a moved rep point (add_event), a changed
guardrail — misses and re-routes, with the reason named in run.log as
`car TTM (<routeenv>): recomputed (<reason>)`. Measured target: ~19 min per
car-untouched scenario at DC scale, plus the same again on the joint
baseline's finalization re-route (which hits round 1's snapshot).
Set false to disable both writing and reading; `force_from_stage="routing"` bypasses
it for a single run without touching config.
consistency_reroute_workers
3
default
Worker processes for the per-routeenv CAR fan-out. Named for the
traffic_equilibrium consistency pass, which is what motivated it (five toll routeenvs re-routed
strictly serially at ~3.1 min each on DC, once per joint round), but it caps
every car routeenv fan-out — ordinary routing goes through the same
loop. 0 => route in-process, one routeenv at a time (the fallback arm); the
effective count is also capped by the routeenv count and, when set, by `cores`
above.
The default is 3 rather than one-worker-per-routeenv because MEMORY, not
cores, is what binds: each worker holds its own routeenv graph plus a full
n_zones² matrix, and one machine ran a DC study at 95-99% swap
doing this work *serially*. Raise it on a machine with headroom; drop it to 0
if a big study starts swapping during routing.
Not used on the checkpoint (`track_volumes`) routing path, which is a
different routing function and is left in-process.
consistency_skip_intermediate_evals
true
default
Skip the consistency pass's evaluation re-evaluation inside
INTERMEDIATE joint-calibration rounds — the ones whose evaluation outputs
a consumer audit found nothing reads: calibration reads GTMs, the
warm feedback loop takes its iteration-0 demand from the snapshot OD, and the
next round's param guard deletes the results unread. The stale results are
still cleared (never left in place to be wrongly validated), so the next
round's evaluation re-runs unconditionally against the absent-results guard —
lazy re-evaluation, same end state, ~6.5 min per round saved at DC scale.
The final consistency passes (finalization, anchor, every scenario, and the
fixed-demand path) always evaluate: those outputs ARE the results.
false = evaluate in every consistency pass.
connectors
distributed
false
max_per_zone
4
exclude_freeways
true
prefer_non_arterial
true
non_arterial_radius_frac
0.5
default
distributed Distributed centroid connectors.
When on, each zone loads at the nearest S₄-eligible node of each of its
connector_seeds step's connector seeds (activity-placed; requires [setup.connector_seeds]
enabled + a built connector_seeds.parquet — silently stays single-node
otherwise). One synthetic centroid per zone regardless of connector count, so
the OD dimension never grows; the demand split across a zone's connectors is
endogenous (UE routes each trip through the entry point minimising congested
cost). Access geography (rep-point skims, all modes) is untouched:
connectors remain a loading-only device.
OFF by default during rollout: turn on together with
[setup.connector_seeds].enabled once the A/B validation gates pass.
max_per_zone cap on loading nodes per zone after seed dedupe
exclude_freeways Freeway exclusion. A node touching ANY
freeway edge never receives connector loading: OSM `motorway`, or an edge HPMS
binds to f_system 1-2 (Interstate / Other Freeways & Expressways), which is
the access-control test US models use and catches trunk expressways without
catching at-grade trunk. Basis: without it, a large northern Virginia study loaded 104 zones / 4.6 % of
connector volume onto freeway-touching nodes; a mid-size tolled metro 14 zones / 2.8 % of
trips. A floor re-snap with no eligible survivor may still land on a freeway
node — logged at WARNING and counted (floor_resnap_freeway_rows).
prefer_non_arterial Soft arterial preference. A zone loading on a node that touches an arterial
edge (HPMS f_system 3, or unbound OSM primary/trunk) moves to the nearest
eligible non-arterial node inside its own polygon and within
non_arterial_radius_frac x sqrt(area/pi) of the zone centroid; with none, it
stays on the arterial. Judgment, not codified practice (US practice codifies
only the freeway rule). 0.5 is a judgment call; on a mid-size tolled metro 59 of
902 zones load on an arterial node after the freeway rule, 31 move, median
143 m.
reduction
enabled
true
floor
enabled
true
bands
[
{ max_diameter_km = 0.5, floor = "service" },
{ max_diameter_km = 1.5, floor = "residential" },
{ max_diameter_km = 5.0, floor = "tertiary" },
{ floor = "secondary" },
]
fine_buffer_km
0.5
default
enabled Exact assignment-network reduction (Family 1): drop
flow-unreachable edges, merge strict degree-2 series (equal capacity/VDF/
preload), merge identical parallels — result-preserving by construction and
verified by the parity harness (tests/traffic_flow_harnesses/
verify_reduction_parity.py). Guards (loading nodes, counted/screenline,
tolled, preload) are always on, not configurable. Solver-basis only: every
persisted artifact stays in the full (u, v, key) basis. Every param here
enters the equilibrium config hash.
ON by default, decided on CORRECTNESS, not
speed. AequilibraE's degree-2 compression splices two inbound one-way edges
into a through-movement that does not exist (see gotchas), which produces
phantom shortcuts and parks flow on provable sinks — 93.9 veh/h in the
baseline of a mid-size tolled metro. The reachability prune removes those pockets before
aequilibrae ever sees them.
It is NOT a speed feature on its own: only ~1.5% of links merge, and the
Burlington joint-loop A/B measured identical per-iteration solve times in
both arms against 0.5s of build cost
(tests/traffic_flow_harnesses/verify_reduction_joint_ab.py). The performance
case arrives with the class floor (Family 2), where the compound is x3.2-6.2.
floor.enabled Family 2 — the ADAPTIVE CLASS FLOOR. Prunes streets whose class is below what
the local zone resolution can meaningfully load. The rationale is statistical
meaningfulness, not data availability: with the OD diagonal
zeroed and connector placement quantizing loading, a street finer than the
zone grain carries artifact, not signal. Conventional models bake an
equivalent floor into a hand-maintained master network; here it is a derived
per-scenario view over the complete all-streets graph, so it can vary with
local zone size instead of being uniform.
Pruned edges are ABSENT from link_volumes.parquet, never zero (the
missing-not-zero contract). congested_speeds/t_bar leave them at
free-flow, which is what routing needs and what the consistency pass means.
Per-edge dispositions land in results/traffic/reduction_debug.parquet.
DEFAULT ON — the acceptance gate passed on a Burlington joint-loop
A/B (converges; skim-ratio drift ≤0.0011 vs 0.005 tol; access 5.6e-05;
calibration 0.0024 gen-min) and a mid-size tolled metro's solve-level A/B (−34% wall
clock, arterial inflation +0.76%, anchor factor +0.05%, 0 of 56 counts
pruned). The default FLIP is a stale-calibration event: an already-calibrated
study that adopts it owes a recalibration pass. That is specific to the floor
— Family 1's default flip was measured calibration-neutral and
owes nothing. Known caveats: the
count-match question is unanswered (test ~100× underpowered), off-2-core
loading is a known unrepaired defect until distributed connectors land,
and the rat-run check awaits a congested fixture (docs/developers/gotchas.md).
floor.bands Zone-diameter bands -> the LOWEST road class retained there. Diameter is the
SHORT side of the zone's minimum rotated rectangle, so a long skinny tract
behaves like its narrow dimension. Exactly one band omits max_diameter_km:
it is the catch-all for the largest zones AND for network outside every zone.
Class ladder (reduction.CLASS_RANK): motorway > trunk > primary > secondary >
tertiary > unclassified > residential > living_street > service > track.
PILOT VALUES, set by a sweep for COMPACT metros —
to be adjusted from experience on real studies, not further sweeping. Known
limit: band edges are absolute while median zone size varies ~3× across
metros. On sprawling large-zone regions most zones land in the 1.5–5 km →
tertiary rung and the re-snap rate jumps (43% vs ~3% on compact fixtures);
one rung finer per band collapses it. Adjust per-study there.
floor.fine_buffer_km Seam rule: a finer band's floor extends this far outward
into coarser territory, so high-resolution areas keep a cushion of retained
network instead of a hard edge at the zone boundary. 0 recovers the
plain point-in-zone rule. The direction was settled empirically by an
A/B (buffer wins every axis: arterial inflation 6.29→3.81%, repairs 141→48,
off-2-core zones 48→24, and total VMT +0.83%→−0.08% — no cushion means
genuine detours); it is also runtime-positive, since repairs dominate build
cost at scale.
feedback
time_gap_tol
0.025
od_rmse_tol
0.005
total_trips_tol
0.005
max_outer
10
min_outer
2
warm_start
true
warm_start_weight
3
warm_restart_od_gap
0.0
measure_noise_floor
false
transit_coupling
enabled
false
level
"od"
theta
0.4
theta_source
"derived (McKnight et al. 2003); uncalibrated"
default
preload_keep_threshold_vph is deliberately NOT set: v1 keeps every
preload-bearing edge regardless of magnitude. The tuning
sweeps left this open — inert on Burlington (0 preload edges), and a
sweep showed the guard mainly matters under coarse bands (51.8% of the
`coarser` family's retention on a mid-size tolled metro is preload-carried, vs 1.2% for
finer bands), so the shipped bands don't need a threshold.
time_gap_tol Demand-feedback loop convergence (resolved by
traffic_utils/feedback.py:resolve_feedback_config — code fallbacks mirror these
values). All three criteria must clear (and k >= min_outer) for the loop to
stop by convergence: the VMT-weighted raw-vs-MSA-averaged link-time gap, the
consecutive-OD RMSE/mean, and the total-trips relative change.
time_gap_tol is looser than the 0.01 pilot value because in WARM-seeded
loops (joint rounds 2+, warm scenarios) the time gap is dominated by MSA lag,
not network movement: once the solves stabilise, |t_k − t̄_k| decays only
harmonically, ~C/(k + warm_start_weight), because t̄ is an average catching up
to an already-fixed point. Measured on the DC 17-h baseline (2026-08-13 run):
joint round 2's gap fit C/(k+4) to ~2% across k=2..10 — raw solve times were
constant from k≈2, yet the round burned all 10 outer solves (~1.9 h) and still
exited unconverged at 0.0169, with 0.01 another ~10 solves away. Genuine
movement is what od_rmse_tol guards (it was the binding criterion in the cold
round, which converged at k=6 with gap_time already at 0.008). At 0.025 that
round-2 lag-chase stops, converged, at k=6 (~75 min saved) while every
observed cold and small-warm stopping point is unchanged.
VMT-weighted |t_k − t̄_k|/t̄_k (loosened from 0.01, see above)
od_rmse_tol consecutive-OD RMSE / mean nonzero cell
total_trips_tol relative change in total vehicle trips (elastic generation)
max_outer outer-loop ceiling — loud DEGRADED banner on miss, not fatal
min_outer keeps the trivially-zero cold k=1 time gap from passing
warm_start scenarios seed OD/t̄ from the baseline's converged state
warm_start_weight K₀: the seed t̄ counts as this many prior MSA iterations
warm_restart_od_gap fixed_iterations = 4 # force exactly K outer passes (protocol symmetry); unset = run to convergence
── demand_workers ───────────────────────────────────────────────────────────
Steps 5-6 (the per-user-class logsum pass) run in a spawned process pool, one
UC per task. That pass is 780-930 s of EVERY iteration of every scenario on
the DC runs profiled 2026-09-01 — 87% of an iteration whose solve is 80 s —
and it was the last serial per-UC loop in the pipeline; calibration.py has
fanned the same chain out for months.
Sizing is a MEMORY decision, not a core-count one, and does not inherit
calibration's 10: that pass runs at ~2.7 GB RSS, while traffic_equilibrium was measured
at 10,978 MB with 8.3 GB of swap at DC, and each worker rebuilds its own
demand-side prep (matrices only — no graph, no skim). Unset = auto: at most 4,
and fewer with few user classes or few cores. Set 1 to run in-process (the
serial path exactly) if a box is tight.
demand_workers = 4
── warm_restart_od_gap ──────────────────────────────────────────────────────
Keep it in this table: a second [assignment.feedback] header is INVALID TOML.
warm_restart_od_gap guards a bad MSA prior. t̄_K expands to
[K₀·t̄_seed + Σ t_j]/(K + K₀), and under a self-seed t_1 is the seed
re-solved, so the seed's effective weight is (K₀+1)/(K+K₀) — still 31% at
k=10 with K₀=3. t̄ IS the shipped congested-speed vector, so that
weight is error, not damping, and gap_time measures it exactly. Measured
2026-08-21 across 9 joint-calibration rounds on a mid-size metro + DC:
gap_time_k = [(K₀+1)/(k+K₀)]·δ predicts every round's stopping point, and
rounds 2 of both studies needed k=16 / k=19 against a max_outer of 10 — they
could not have converged. Above this k=1 gap_od threshold the seed's demand
has been rejected outright, so t̄_seed and t_1 are dropped and the average
restarts at k=2 (min_outer rises to 3, since the restart iteration's time gap
is identically zero). Observed k=1 gap_od: 2.27 / 0.39 (mid-size metro rounds 2,
3), 1.04 / 0.42 (DC), vs 0.026-0.044 for rounds that seeded well — 0.1
separates them with no straddlers.
0.0 = OFF, deliberately: the mechanism is
derived and artifact-validated but not yet confirmed by a completed run, and
the derivation cannot settle whether a less-damped loop oscillates. A test
study tier sets 0.1 to enable it for exactly that confirming run.
measure_noise_floor
On time_gap_tol above: 0.025 was loosened on
the reading that the warm-start time gap is benign MSA lag. The analysis above
says that gap IS the shipped t̄'s own error, so the loosening ships a less
accurate congested-speed vector rather than fixing a measurement artifact.
Left standing here (a test study tier overrides it to 0.01); the
codebase-wide question is open.
── measure_noise_floor ──────────────────────────────────────────────────────
The backward noise-floor twin (connectome/noise_floor.py; opt-in, a
diagnostic). After the control scenario's traffic_equilibrium, run ONE
extra consistency pass (re-route car, re-assemble impedance, re-evaluate) in a
scratch copy on t̄_{K−1} — the MSA average one step before the shipped t̄_K,
persisted as results/traffic/t_bar_prev.parquet — and write the per-family
p95 of |control(K) − control(K−1)| per zone to <control>/results/
noise_floor.json. That is a LOWER BOUND on the real comparison noise (it
measured 0.04-0.40 of it on burlington), so it never sets the compare floor
([visualization.comparison.floors] in analysis_parameters.toml does); the
runtime report warns when it exceeds the floor in use, i.e. when the study's
floor is certainly too low.
Runs once per study, on the control only; does nothing in a study without
demand feedback or without a control. Cost: one consistency pass (~48 s on
burlington, ~+7% of its scenario phase).
transit_coupling.enabled Tier 2b: couples
gtm_TRANSIT's bus in-vehicle time to the loop's own OD-level congestion ratio
— f_bus,k = 1 + theta*(r_k/r_baseline - 1), where r_k is the loop's existing
skim_k/skim_0 congestion ratio and r_baseline is the same ratio evaluated on
the baseline's converged (warm-seed) t̄. Default OFF; the DC study tier turns
it on. Only ever activates on a *scenario* warm-seeded from a baseline's t̄
Two disarming cases, both to f_bus ≡ 1 with a logged
reason rather than raising: a run with no warm seed at all ("cold start"),
and a run warm-seeded from its OWN previous joint round ("self seed"), where
the ratio would measure round-to-round calibration drift instead of a
congestion response to a scenario.
The self-seed gate is what keeps a baseline uncoupled. A baseline does
NOT always run cold: the joint round-resume path hands it a warm state via
`self_seed_dir` (which ignores `warm_start = false` by design). Do not restate this as a property of some other
mechanism: it is an explicit check in `feedback.run_feedback_loop`, and it
holds only for as long as that check is there.
transit_coupling.level level: "od" (Tier 2b, default) or "route" (Tier 3 — per-route measured
theta off transit_shape_match's map-matched product; theta/theta_source
below are then ignored — a study switching to "route" should also set
theta_source = "measured" explicitly, since this file's theta_source is set
(not omitted), so resolve_transit_coupling_config's level-based default
never fires against it.
"route" also needs the transit template table's `feed_ids` leg column: a GTFS
route_id is unique only within its feed (40 collide at DC, 11 at Burlington),
so the route key is (feed_ids, routes). A routeenv routed before that column
existed disarms with status "inactive_no_feed_key" rather than joining route
ids against a blank feed — re-route it, or stay on "od", which needs no route
identity.
transit_coupling.theta theta — the elasticity of bus in-vehicle time to car congestion:
f_bus,k = 1 + theta * (r_k / r_baseline - 1)
theta = 1 would mean buses slow exactly in step with cars; 0.4 means a bus
absorbs 40% of the car congestion effect.
DECLARED DEFAULT, not calibrated for any study, and NOT a quoted literature
value — no published work states a theta in this ratio form. It is DERIVED
from McKnight, Levinson, Ozbay, Kamga & Paaswell (2003), "Impact of Congestion
on Bus Operations and Costs", FHWA-NJ-2003-008 (https://rosap.ntl.bts.gov/view/dot/15963;
journal version TRR 1884, 2004), whose preferred model over 690 NJ Transit
route-segment observations is
BTT = 0.52 + 0.73*CTT + 0.06*boardings/mi + 0.31*stops/mi (R^2 = 0.62)
with BTT/CTT in min/mile. Converting 0.73 * (t_car / t_bus) to this ratio form
gives ~0.35 at that study's free-flow-to-congested arc and ~0.42 at its sample
means; the SAME source evaluated at a congested base gives ~0.54, and its other
specifications span 0.2-0.7. Corroborating: McKnight et al. (1997), NYC Transit,
implies ~0.32. Pulling the other way, FSUTMS's default highway->transit speed
curves (Zhao & Li 2005, FDOT BD015-07) imply ~0.65-0.76, but those are
engineering constructions assuming the in-motion portion moves 1:1 with traffic.
So: 0.4 sits inside a 0.2-0.7 empirical range that clusters 0.35-0.55, and it
is the low-to-middle end of that. Measure it if the answer matters.
Tier 3 ("route") replaces this scalar with a per-route MEASURED value. Note the
two are not the same quantity: Tier 3's theta is an exposure share (bus time on
non-exclusive-ROW links, assumed to move 1:1 with car time there) while this one
is a behavioral elasticity, so Tier 3 will read systematically higher.
transit_coupling.theta_source printed verbatim in the run report / per-iteration log line
calibration_loop
enabled
true
max_rounds
63
param_drift_tol_min
1.0
skim_drift_tol
0.005
anchor_factor_band
[0.75, 1.33]
studytraffic_analysis_parameters.toml1 of 5 values changed
enabled Joint calibrate<->equilibrate baseline orchestration: rounds
of calibrate-logsums on congested GTMs + warm self-seeded re-equilibration,
run by scenario_management.run_joint_baseline whenever assignment + feedback
are on. Stops when BOTH drift metrics pass:
max calibrated-parameter change (ASCs + band constants, generalized minutes)
and the volume-weighted car link-time ratio round-over-round. The round cap
is a loud-warning backstop (best iterate stands), not a silent truncation.
param_drift_tol_min finalize — DEFAULT OFF, and deliberately NOT set here. Its default is
conditional (`not anchoring_enabled`), resolved in `_joint_calibration_cfg`;
pinning it in this tier would make every study's value explicit and turn the
anchoring guard below into a hard error for every anchored study.
After the drift loop converges, finalization re-solves the baseline on the
free-flow-referenced pivot basis every scenario uses (the joint warm rounds
leave it congested-referenced — two tangent lines through one fixed point, a
systematic ~0.07% baseline-vs-scenario access seam). It costs a full feedback
solve + consistency pass: ~90 min at DC scale, ~90 s at Burlington's.
It is off by default because nothing consumer-facing reads baseline levels:
`study_reference.json` points `level_reference` at the CONTROL, which is a
scenario on the free-flow basis like every other, so the seam is absent from
everything a client sees and cancels in scenario-minus-control regardless.
What skipping costs is the baseline's own Δ row — the study's noise floor —
which then carries the pivot seam on top of stopping noise and reads as an
inflated bar. A diagnostic, not a result.
Anchoring flips the default back on: anchoring fits on whatever state
finalization left, and a congested-basis fit is undesigned. Setting
`finalize = false` alongside `[assignment.anchoring] enabled = true` raises.
These two ARE the model's definition of "the baseline has converged": the joint
loop exits only when BOTH pass. Neither is derived; both are reasoned, and the
reasoning is stated here rather than implied by the number.
param_drift_tol_min = 1.0 generalized minute. Rationale, not derivation: the
universal ASC search is bounded at ±240 generalized minutes, so a round-over-
round move of 1 gen-min is ~0.4% of the parameter space's own scale, and a
calibrated constant still moving by more than a minute has not settled. It is
also comfortably above the joint loop's own reproducibility noise.
skim_drift_tol = 0.005 on |volume-weighted car link-time ratio - 1|. Rationale:
0.5% is the conventional order for a network-equilibrium relative gap, and the
quantity is a volume-weighted mean over the whole network, so it is far quieter
than any single link. Burlington measured 1.0066 after round 1.
⚠️ UNMEASURED: what fraction of real studies converge inside `max_rounds`
versus hitting the cap and warning is not known.
gen-min — see the rationale above
skim_drift_tol |ratio - 1| — see the rationale above
anchor_factor_band gated-anchoring comfort band (placeholder — tune)
occupancy★
default
1.7
WORK
1.1
default
default Vehicle occupancy (persons per car trip) by trip category. Person car-trips are
divided by occupancy to get vehicle trips; ``default`` covers any category with
no explicit entry. NHTS-derivable codebase defaults: ~1.1 work, ~1.7 non-work.
SCHOOL/CHILDCARE etc. fall back to ``default`` for now — tune per study if the
child-passenger occupancy of those trips matters. ◇
class_collapse
income_boundaries_usd
[50000, 100000]
class_labels
["vot_low", "vot_mid", "vot_high"]
default
income_boundaries_usd Assignment classes = value-of-time bands. Each user class joins a class by its
``mean_income`` (USD/yr): a class with mean_income <= a boundary joins the band
at that boundary, else the last band. len(class_labels) == len(boundaries) + 1.
Default: income quintiles collapsed to 3 bands ("handfuls not dozens").
anchoring
enabled
false
factor_bounds
[0.5, 2.0]
default
enabled Corridor anchoring: "anchor the corridor, not the region." Fit one
scalar anchor_factor = Σobserved / Σmodeled (volume-weighted) on operator-named
counted links, scale all OD by it, and carry it UNCHANGED into every scenario
(scenarios never re-anchor — that would absorb the project effect being measured).
Fit on the baseline only; the two-pass (fit → rescale → re-converge) runs inside
traffic_equilibrium. Select-link on the anchor corridor is the diagnostic; the fitted factor,
modeled-vs-observed table, and anchor-implied region-VMT change land in
results/traffic/anchoring_report.json.
OFF by default → factor 1.0, results are "globally relative" quality (fine for
non-capacity uses). A study opts in by adding counted links below. Counts must be
the study's analysis-hour vehicle volume (Burlington: the 8 AM peak hour). Source
for Burlington: VTrans MS2 Traffic Count Database System (vtrans.ms2soft.com) or
the VTrans continuous-counter Redbook; FHWA TMAS national volume files are the
fallback. See docs/operators/traffic-assignment-runbook.md for the calibration
ordering (converge → re-calibrate logsums → re-converge → THEN anchor).
factor_bounds warn if the fitted factor leaves this range
odme
mode
"auto"
min_counts
15
spread_min_km
3.0
residual_tol
0.15
residual_frac
0.25
quality_min
0.3
gamma
0.05
drift_cap
0.15
max_outer
8
min_rel_improvement
0.01
rgap_early
0.001
min_modeled_vph
25.0
max_mismatch_ratio
5.0
min_fittable
3
default
Each counted location is one [[assignment.anchoring.links]] table. Identify it by
OSM way id(s) (matches all directed graph edges carrying the way — BOTH directions,
so observed_count is the two-directional analysis-hour volume) OR by explicit graph
edges = [[u, v, key], ...] (pin a single orientation; read u/v/key from
results/traffic/routing_edges_volumes.gpkg). Exactly one identifier per entry.
[[assignment.anchoring.links]]
name = "US7 @ Shelburne Rd screenline"
osm_way_id = 12345678 # or osm_way_ids = [111, 222]
observed_count = 1850 # analysis-hour vehicles, both directions
[[assignment.anchoring.links]]
name = "Main St EB @ count 042"
edges = [[204086969, 204225283, 0]]
observed_count = 620
Instead of hand-entering counts, set `use_observed_counts = true` to pull them from
the observed_counts step's observed_counts.parquet artifact: with no links listed,
the scalar anchors on EVERY matched count; with links listed but observed_count
omitted (or from_observed_counts = true on a link), the count is summed from the
artifact over that anchor's member edges.
use_observed_counts = false
mode Scalar-vs-ODME calibration selector. SOURCE-AGNOSTIC: the decision keys
on count properties (how many usable, how spread out, functional class, post-scalar
residual), never on which provider produced them. The scalar anchor is always
fit first as the robust floor; ODME is the "grown-up" multi-count structural
calibration reading the same observed_counts artifact.
When the selector verdict is "odme", the hand-rolled Spiess (1990) optimizer
(traffic_utils/odme.py) refits the OD STRUCTURE on the select-link OD matrices —
freeway targets net of the background preload, arterial targets as observed. The per-cell
delta subsumes the scalar and is frozen into scenarios (inherit-never-refit).
Diagnostics land in anchoring_report.json's `odme` block (incl. the excluded-count
conflation diagnostic). "scalar" forces the scalar-only floor; "odme" forces the
optimizer (warns, not fails, if the identifiability gate misses).
auto | scalar | odme (auto = decide from the data)
min_counts --- selector (scalar-vs-ODME identifiability gate) ---
All five gates are round-number engineering judgement. Their mechanism is
confirmed — each is read and applied where documented — but no derivation is
recorded for any level, and there is no study on which the selector's verdict
was checked against a known-good answer. Read them as a coarse "is there
enough independent count information here to identify OD structure?" screen,
not as a calibrated test.
identifiability floor: usable counts needed for ODME
spread_min_km bounding-box diagonal of usable counts (spatial spread)
residual_tol rel. |obs-mod|/obs above which a count shows "real" structure
residual_frac fraction of usable counts that must exceed residual_tol
quality_min usable = quality >= this (match_confidence × measurement)
gamma prefer_classes = ["primary", "secondary", "tertiary"] # optional structure-fit hint
--- Spiess solver knobs (traffic_utils/odme.py) ---
GLS prior weight — pull the OD toward the demand-model prior
drift_cap hard bound on total OD change ‖g−g⁰‖₁/‖g⁰‖₁ (projected each iter)
max_outer --- cost knobs: ODME spends one full BFW assignment per gradient step ---
max_outer is a CEILING, not a schedule: each iterate is assigned and scored, and the
loop exits as soon as a step buys less than min_rel_improvement of the RMSE. Worst case
is max_outer+1 assignments — one more than the old fixed loop — because the accepted
iterate is now assigned rather than returned unscored. Measured on Burlington
(2026-08-01): it runs the full 8 and stops at the ceiling rather than converging
in ~4-6; raise min_rel_improvement if you want it to finish sooner.
ceiling on gradient steps (the loop normally converges first)
min_rel_improvement exit when a step improves RMSE by < this fraction
rgap_early Fit iterations solve BFW to this looser rgap instead of [assignment] rgap_target: a
first-order gradient probe does not need a tightly-converged assignment, and the BFW
tail is most of the solve. The probe (which drives the fittable/excluded gate) and the
final confirmation of the accepted iterate always run at the production rgap, so no
reported number comes off a loosened solve. Set to 0 or delete to never loosen. If the
run log warns that the confirmation is much worse than the loosened solve claimed,
tighten this — the gradient was fitting assignment noise.
BFW rgap for fit iterations (0 ⇒ always use rgap_target)
min_modeled_vph target gate: modeled SL below this ⇒ zero-modeled (no gradient), dropped
min_fittable fewer fittable counts after gating ⇒ abort ODME, keep the scalar
preload
through_fraction
population_bands
[50000, 200000, 1000000]
motorway
[0.65, 0.45, 0.3, 0.2]
trunk
[0.4, 0.28, 0.2, 0.15]
primary
[0.25, 0.15, 0.12, 0.1]
secondary
[0.12, 0.08, 0.06, 0.04]
tertiary
[0.05, 0.03, 0.02, 0.0]
default
[0.05, 0.03, 0.02, 0.0]
trucks
pce
2.0
default
Background-preload CONSUMPTION + behavioral constants. The derivation
(background_preload) lives under [setup.preload]; this block holds the constants that turn
HPMS/gateway volumes into the fixed background flow the assignment adds to the VDF.
When background_preload.parquet exists it is loaded automatically at traffic_equilibrium — this
block only tunes the numbers; no `enabled` flag here.
through_fraction.population_bands Share φ of gateway passenger volume that is THROUGH (E-E) traffic. Its complement
(1−φ) becomes the E-I/I-E access demand that terminates inside the cordon, so this
table sets both layers — it is load-bearing twice, and φ too high is the same
direction as the freeway-over/arterial-under distribution error on record.
Keyed by facility class × URBANIZED-AREA POPULATION. Population is the second
first-order predictor in the whole external-station literature and a class-only
scalar ignores it — it swings φ ~3× across our own
fixtures (Burlington 149k and two larger test metros at 761k and 2.2M would all read 0.5 on a freeway).
Anchors: NCHRP 365 / Modlin (interstate 70% @ 50k pop, 50% @ 100k; principal arterial
30% @ 50k, 10% @ 100k), VDOT/Richmond per-station at ~1.3M metro (interstate 23,
minor freeway 17, principal arterial 14, major 9, minor 7, collector/local 0), and
Talbot & Burris (TRR 2254, 2011) above the ~100k ceiling where Modlin's linear form
goes negative. ◇ The cells are an INTERPOLATION of those anchors — a better-grounded
placeholder, not a validated table. The sourced part is the shape: monotone
non-increasing in population within a class, every class below trunk far under the
freeway value. Per-gateway route continuity (does the road actually go somewhere on
both sides — an interstate can dead-end at a seaport) is the next refinement and is not
modeled here.
`population_bands` are the UPPER EDGES; each class then takes one φ per band, i.e.
len(population_bands)+1 values. A bare scalar instead of a vector pins that class at
one φ regardless of population — which is also what keeps an older study config
(`motorway = 0.5`) working unchanged.
Population comes from sum(census_total_pop) over the study's analysis zones unless
`[setup.preload] population` overrides it. That is the analysis area's population,
not the Census UA's — same band for every current fixture, but override it on a study
whose boundary is much bigger than the urbanized area (a large rural county).
through_fraction.tertiary = default; no independent citation
through_fraction.default `default` is the fallback for any cordon class without a row above. It is the LOWEST
row deliberately: falling back to the MOTORWAY value, the highest,
would be ~4× too high for a primary/secondary cordon and would silently
fabricate through traffic whenever gateway_classes is widened.
trucks.pce passenger-car-equivalent weight for trucks in the VDF (◇)
The legacy manual hook — [assignment.preload] links = [...] — still marks the preload
"populated" for anchoring when no background_preload artifact exists (method = "manual").
enabled Modeled VMT (assigned demand + background preload) vs HPMS section AADT, binned
by functional class — rung 1 of the calibration ladder. Runs after every solve
and writes results/traffic/vmt_reconciliation.json; re-runnable standalone
with `reconcile-vmt <study>`. Purely DIAGNOSTIC: nothing here gates anything.
It exists because every other objective in the stack (the scalar anchor, ODME's
RMSE) is weighted by count stations, and count stations are freeway-heavy — a
model can be badly wrong about the arterial/freeway VMT split and score well on
all of them. Measured in a mid-size metro (~550-760k urban-area population) with
tolled facilities and a congested core: majors 1.07x observed overall, but freeway
1.49x and arterials 0.85x.
hour_factor ~30 s on a 167k-edge basis (the section↔edge binding dominates), against solve
times measured in hours. Turn off only for throwaway runs.
hour_factor / truck_hour_factor default to [setup.preload.hour_factors] so the
observed side of this report and the preload cannot silently disagree. Override
here only to test sensitivity.
hour_factor = "measured" derives per-cell K-factors from the study's own TMAS
volumes (weekday analysis-hour volume / all-days mean daily volume, AADT-weighted,
per reconciliation cell) instead of assuming a number. It is the default, and is
strongly preferred wherever observed_counts has run: measured weekday hour-8 K is
0.060-0.070 in all three states measured so far, against the 0.08 otherwise
assumed, so an assumed basis inflates the observed side ~20-25% and deflates
every ratio in the report — concealing a substantially worse freeway
over-prediction than the numbers show. The demand side agrees with the
measurement: trips_per_person_in_period 0.191 at hour 8 is ~5.3% of an NHTS day.
per-cell K-factors from the study's own TMAS, matching the
preload seeding basis. When it cannot be measured,
THIS report always falls back to 0.08 with a
warning -- it is a diagnostic and must not kill a solve. The
PRICING consumers (streetscape table, preload seeds
background_preload) raise HourFactorUnavailable instead, but only when
TMAS was owed -- i.e. [setup.counts] configures a source AND
observed_counts completed. A degraded observed_counts (fail-soft on
purpose) or `sources = []` still falls back with a warning.
A study with no TMAS coverage sets a number here.
bind_classes truck_hour_factor = 0.05 # measured K is passenger-side; TMAS .VOL has no class split
Binding controls, mirroring the speed ladder's rung 2 — see
hpms_source.bind_sections_to_edges. `*_link` classes are excluded deliberately:
an HPMS ramp section inherits its mainline's f_system and geometry.
max_fsystem_rank matched section must be a top-3 candidate for the OSM class
sources Which count providers to ingest and union (setup/count_sources.py registry).
Providers are additive; the matcher dedups per directed edge (higher quality wins).
On by default: the TMAS download/match is fully automated and degrades to an empty
counts frame (with a warning) where no stations join. Opt out at study tier with
`sources = []`.
add "manual" to union operator counts
outside_area_buffer_km Stations beyond the analysis area (+ this buffer) are kept in the artifact but
tagged (km_outside_analysis_area > 0) and demoted to validation-only by the ODME
gate (reason "outside_area"): the routing graph extends well past the zone system
and TMAS pulls statewide, so out-of-area stations match real edges — but their
volumes are dominated by external traffic the demand model doesn't carry, so
fitting internal OD to them absorbs other people's trips. They are not dropped,
because inter-zone paths do legitimately load out-of-area corridors. Both halves
of that are measurable: in a mid-size congested metro, 13 of 23 gate exclusions
were out-of-area stations misread as conflation defects, while 5 other
out-of-area counts carried real modeled flow.
The buffer is slack for boundary-hugging roads; the area is hole-filled, so
sparse-zone gaps inside the study area don't count as outside.
The 0.5 km level itself is barely load-bearing on the evidence available: on
the one study where the out-of-area stations were measured they sat 4.5-32.6 km
beyond the area, so any buffer in this range classifies them identically.
Whether that generalises to a study with counts hugging its boundary is
untested.
tmas.use_latest_available National FHWA TMAS STA/VOL files. use_latest_available falls back to the most recent
published year when the analysis year is unavailable.
manual Operator CSV escape hatch (a state DOT count portal, a client's own counts, ...).
Required columns: station_id, lat, lon, observed_count (analysis-hour vehicles).
Optional: road_ref, direction, bearing_deg, f_system, volume_kind
(hourly|daily|aadt), confidence. Supplying road_ref and bearing_deg is worth the
effort: the matcher's route tier and carriageway tie-break both key on them.
Path is absolute or relative to the study dir.
file = "counts/manual_counts.csv"
matcher.route_match_func_discount Count → graph-edge matcher scoring (volume_utils.match_count_points_to_graph).
A matching signed route number is near-decisive identity evidence, so it scales the
functional-class penalty down by this factor — stops a TMAS/OSM class disagreement on
a signed route (e.g. TMAS "Local" vs OSM "secondary") from snapping the count to a
nameless parallel local road. 1.0 disables the discount; 0.0 ignores class entirely
once a route matches.
On top of the score, selection is two-tier: candidates whose OSM ref matches the
count's road_ref are preferred outright, and near-tied candidates (the two
carriageways of a divided road) are split by raw bearing difference so directional
counts land on their own carriageway. Not knobs — see match_count_points_to_graph.
The mechanism is unit-tested; the LEVEL is not derived. Nothing establishes 0.25
rather than 0.2 or 0.4, and its importance is reduced by the hard route tier
above it, which does the real work on any count carrying a road_ref.
matcher.guard_f_system_max Wrong-road guard. A candidate edge is rejected — the count
falls back to the next-best candidate, else stays unmatched with a reason in
observed_counts_report.json — when either holds:
* the count's HPMS f_system is ≤ guard_f_system_max (Interstate .. minor
arterial; TMAS codes are translated from TMG first) and the edge's OSM class is
in guard_minor_highways. Without it, a 1,000–2,300 vph freeway or arterial
station whose cabinet sits nearer a side street than its own carriageway can
bind to that street. Ramp counts are exempt (they bind to *_link edges by rule).
* the observed volume exceeds guard_max_obs_over_capacity × the edge's
capacity_vph (edge + reverse for a two-way count). 1.2 leaves room for capacity
under-estimates and short-term over-capacity flow while rejecting the 2–5×
binds a side-street bind produces; it is a judgement value, not fitted.
enabled Synthesizes the through (E-E) + truck flow the person-trip demand model omits on
freeways, from open data (HPMS + TMAS), into background_preload.parquet. Never
mutates the graph; frozen into scenarios via COW. ON by default:
HPMS fetch + gateway detection are fully automated, and a study with no boundary
freeways writes an empty-but-valid artifact (report notes why). Opt out at study
tier with `enabled = false`.
method "gateway" (detect + synthesize) | "manual"
conflation_classes which classes form the CORDON (external crossings).
Reaches past motorway/trunk because detection is a
ring-crossing test: a stub-detection
primitive could not carry arterials (primary/secondary
subgraphs terminate all over the interior for
class-change reasons), a crossing test can. Matches
conflation_classes, which must stay a superset.
cluster_radius_m which classes HPMS AADT is bound onto. Kept separate from
`gateway_classes`: one shared list would let widening the truck
canvas silently reshape the cordon (and vice versa).
Must be a SUPERSET of gateway_classes — gateway volumes
are read out of this frame. Wider than the freeway
classes: HPMS carries arterial AADT + truck
AADT the pipeline would otherwise discard, and the arterial VMT
cell is the one running short.
`tertiary` pairs with f_system 5 below —
multiplicative, 61% of the effect is the interaction.
Graded by verify_arterial_conflation_ab on Burlington
(bind rate 0.682, implausible binds 28 -> 0). RE-RUN
THAT HARNESS on any study you widen this further on:
the pinned VMT oracles are freeway-cell and blind to
arterial binding, which is how an earlier widening
shipped with un-retuned match caps.
population = 120000 # urbanized-area population, which selects the row in
[assignment.preload.through_fraction]. Defaults to
sum(census_total_pop) over the analysis zones — the same
quantity only when the study boundary IS the urbanized
area. Set it explicitly when they differ enough to cross
a band edge (a study drawn on a large rural county).
crossings within this distance of each other are ONE gateway.
Purely geometric — a divided highway's two carriageways
cross the ring within metres of each other, so this
replaces the old same-ref clustering plus its 3 km
complementary-pairing repair pass. Do NOT reintroduce
ref-based pairing: with the cordon through `tertiary`,
unsigned suburban arterials are common and a refless
cluster used to pair as a WILDCARD. Two vetoes on the
merge: two SIGNED crossings with no shared route never
merge (Burlington: I 89 + US 2), and a motorway/trunk
crossing never merges with a lower-class one (a New England
study: unsigned tertiary Washington Road anchored
a cluster 370 m from one I-90 carriageway and 429 m from
the other, split the pair, and tripped the cordon gate).
One repair pass after clustering, scoped to one-sided
motorway/trunk clusters only (the ones the gate refuses):
a FORK sharing its outer node with another divided
cluster is absorbed (e.g. MD 3 off I-97), and
complementary halves on the same SIGNED route within
2 x this radius pair up (e.g. I-270 at 422 m).
cordon_gate grade the detected cordon against a reference built from
the UNCLIPPED OSM source, and RAISE on disagreement.
background_preload is fail-soft — no gateways yields a valid
truck-only artifact and a study that completes looking
normal — and a `grep -c "STAGE background_preload DEGRADED"` check
fires on both test fixtures for
unrelated reasons, so it signals nothing. Two tiers:
a cordon way absent from the graph at the ring (data
loss, hard), and a divided-highway gateway crossing in
one direction only (a split carriageway pair — measured
on a mid-size tolled metro, I 95_W split into two one-sided
gateways 459.9 m apart). Escape hatch is an explicit
[[setup.preload.gateways]] table, not relaxing this.
cordon_gate_missing_way_tolerance allowance for "graph lost this corridor". Derive it
against a graph built with no clip, where loss is
impossible by construction — never by relaxing it until
the study at hand passes. Measured 0 on both fixtures in
both arms. It only reaches 0 because the gate separates
loss from an UNROUTABLE neighbourhood: Burlington's ring
crosses Lake Champlain and picks up NY 22, real and
pruned as a disconnected component, whose nearest cordon
network is 16.9 km away. If you find yourself wanting to
raise this, check which of the three states the ways are
in first (see the module docstring) — a nonzero value
here means real corridors are being dropped.
gateway_ring_inset_m how far INSIDE the routing-area clip the detection ring
sits. Ring radius = ROUTING_AREA_BUFFER_FROM_ANALYSIS_ZONES_M
(analysis_parameters.toml) minus this. Nonzero because
`osmium extract` keeps crossing ways whole
(`complete_ways`), so the graph reaches slightly past the
clip polygon and a ring exactly on it would read some
crossings on the wrong side.
200, not 10: 10 m was smaller
than OSM node spacing on a suburban arterial, so whether a
real corridor registered as crossing came down to whether a
node happened to fall in that 10 m band. DC lost the
one-way pair carrying Northstar Boulevard (SR 3171, ways
216903480 / 28461347) across the Fairfax/Loudoun line that
way, on two different zone systems — a hard
CordonIntegrityError that failed background_preload outright. At
200 m the ring found MORE crossings, not fewer: DC went
397 -> 414 crossings and 179 -> 183 gateways, and the
integrity check passed.
This is NOT DC-specific — every study was exposed at
whatever rate its own node spacing gave. It moves the ring
(5000 - 200 = 4800 m by default), so a study that rebuilds
background_preload can see a slightly different gateway SET.
Warm studies are untouched until they re-run background_preload.
hpms.year HPMS full-extent AADT + truck AADT from the FHWA ArcGIS FeatureServer. `state`
defaults to the study's first state; `year = 0` probes the latest available service.
state = "VT"
hour_factors f_systems = [1, 2, 3, 4, 5] # HPMS functional systems the preload FILTERS to
(1 Interstate / 2 other freeway / 3 principal arterial /
4 minor arterial — where OSM `secondary` mostly lands /
5 major collector — where OSM `tertiary` does).
Not a fetch key: the canonical query is `f_system <= 6`
for every consumer, so changing this never refetches.
5 is included: the VMT reconciliation already grades
the arterial cell on f_system 5 (cell_of_f_system(5) =
nonfreeway_major), so excluding it would make the modeled side
structurally zero on roads the observed side counts.
Off the cordon this seeds TRUCKS ONLY — passenger AADT is
never preloaded — so it is worth ~5% of the arterial
deficit, not the fix for it (measured end to end on Burlington).
file = "hpms/vt_2024.geojson" # manual fallback (operator-downloaded), portal-drift insurance
hour_factors.default ◇ passenger daily→analysis-hour share. Per-cell K-factors
derived from the study's own TMAS volumes (weekday
analysis-hour volume / all-days mean daily volume,
AADT-weighted, per reconciliation cell). Measured weekday
hour-8 K is 0.060–0.070 in MN/SC/VT; an assumed 0.08
seeds every through and access vehicle ~15–25% too numerous.
This SEEDS, so it raises
HourFactorUnavailable rather than falling back when TMAS was
owed ([setup.counts] on AND observed_counts completed); it still
falls back with a loud warning when counts are off or observed_counts
degraded. Override to a numeric value for studies without
observed_counts -- that is the escape hatch, and it is unchanged.
hour_factors.truck_default ◇ trucks are flatter across the day. Not measurable from
TMAS `.VOL` (no vehicle-class split), so "measured" on the
line above leaves this an assumption either way. The
direction is argued; the level is not. The only thing
standing behind 0.05 is that a perfectly flat 24-hour
truck profile gives 1/24 ≈ 0.042 and 0.05 sits just
above it — a reconstruction, not a source.
access.enabled External-internal (E-I / I-E) access trips: the (1 − through_fraction) complement of
each gateway's passenger margin, which terminates INSIDE the cordon. Synthesized as a
singly-constrained gateway↔zone gravity model and routed onto edges like the E-E layer
Closes the freeway gap the count triage exposed — a zero-modeled freeway
edge gives ODME no gradient, so leaving E-I to ODME does not work. through_fraction
(under [assignment.preload]) governs the split; no separate magnitude knob.
off ⇒ E-E + trucks only (legacy behavior)
access.beta ◇ gravity decay over free-flow minutes (mirrors E-E)
access.attraction_col E-I destination pull (◇ — non-home is the recommended refinement)
[[setup.preload.gateways]] # manual override/augment when detection misfires
name = "I 89_N"
ref = "I 89"
entry_edges = [[123, 456, 0]] # directed (u, v, key) carrying traffic inward
exit_edges = [[456, 123, 0]]
enabled connector_seeds — reconstructs the sub-zone activity geography the pipeline otherwise
aggregates away (block POP20 + block LODES WAC + classified Overture POI points)
and clusters it into per-zone connector-seed LOCATIONS. Writes connector_seeds.parquet
+ activity_surface.parquet; never mutates the graph, frozen into scenarios via COW.
OFF by default: nothing consumes the artifact until distributed connectors land
Turning it on only costs build time — it changes no model result.
Unlike observed_counts/background_preload the cache marker carries these params, so editing any key
below rebuilds on the next run without force_from_stage.
lodes_year LODES WAC vintage for block-level jobs
block_assignment "centroid" (blocks join zones by their centroid)
| "areal_overlay" (exact, slower; blocks do NOT
nest inside H3 zones, so this is a real choice)
exclude_poi_categories `other` maps to no trip category ⇒ no modeled
attraction. Clear it to use POIs as a last-resort
signal where Overture thins at the rural fringe.
seed_bands Seed count by zone-diameter band (km, min-rotated-rect short side) — the same ladder
shape as [assignment.reduction.floor].bands, so the two read against each other.
Exactly one band omits `max_diameter_km`: the catch-all for the largest zones.
[count_fit]4 keys
Key
Effective value
Set by
TOML comment
volume_groups_vph
[500, 1000, 2000, 4000]
default
Count-fit report (traffic_utils/count_fit.py → zone_attachment_summary.json
`count_match` + results/traffic/count_fit.parquet). Reporting only — nothing here
changes the model.
%RMSE / GEH volume groups, observed vph, lower edges. Hourly groups are not
standardised (FHWA's 2010 validation manual tabulates DAILY groups). These bins
split a typical TMAS set (half freeway) into groups of comparable size; changing
them breaks comparison with earlier reports.
near_zero_obs_vph
50.0
default
A count below this on a reversible / conditional-oneway edge is read as the lane's
closed direction (count_class = reversible_closed). A closed reversible lane
counts 0–30 vph, while a two-way-coded one can be assigned thousands.
express_corridor_m
75.0
default
express_hov class: a same-direction (bearing within the tolerance) tolled non-ramp
edge within this distance of the count. 75 m spans a freeway cross-section with
its express carriageway, without reaching a parallel arterial.
express_bearing_tol_deg
60.0
default
—
[speed_imputation]5 keys
Free-flow speed imputation
The speed ladder written to every edge's `speed_limit_kph`, and the control
delay folded into `ff_speed_kph` on top of it. Nation-scalable by design: no
per-study calibration, no local medians. See
docs/modelers/methodology/free-flow-speeds.md.
Key
Effective value
Set by
TOML comment
same_street_propagation
true
default
Ladder rung 3: untagged edges inherit posted limits from tagged edges on the
same contiguous named street. Only ~6% of a typical US extract's edges carry
maxspeed, and tagging is blockwise, so the same arterial otherwise alternates
between its posted limit and the flat class default block by block.
Seeded from rungs 1 AND 2, so HPMS limits also propagate outward.
max_propagation_m
1500.0
default
Both levels below are judgement. Each mechanism is confirmed — the cap is
enforced on BOTH endpoints of a receiving edge, and the spread test does
switch a run to nearest-tagged-neighbour — but neither level has a recorded
derivation and neither was swept. 1500 m has no anchor at all; it is a round
bound on how far a posted limit may be trusted along its own street. 10 kph is
one 5-mph posting step, i.e. the smallest disagreement a corridor can show
without a genuine change of regime — a reconstruction of the level, not a
source for it.
network distance ALONG the street a limit may travel
run_spread_fallback_kph
10.0
default
tagged limits disagreeing by more than this within one
run ⇒ each edge takes its nearest tagged neighbour's
limit instead of the run's length-weighted median
enabled Ladder rung 2 — state-DOT POSTED SPEED LIMITS from the FHWA HPMS full-extent
service, conflated onto graph edges at build time. A measurement, not an
imputation: it replaces flat class defaults on the arterials that carry the
travel. Measured on a mid-size tolled metro, it newly supplies ~20% of network VMT, cutting
the share of travel riding on a hand-set constant from 47% to ~27%.
This is the ONLY network call in graph build, and it is fail-soft by contract:
any fetch or conflation error logs a warning and falls through to propagation
and the class defaults. The response is cached under input_data/hpms/ and shared
with background_preload (same superset fetch, different rows).
year state = "SC" # else [setup.preload.hpms] state, else STUSPS from the
# study's census geometry. Set only to override.
0 = probe latest published HPMS service, falling back
f_systems 7 (Local) is zero-populated for speed_limit by
design — HPMS never helps local streets
min_mph The envelope is judgement: 15–85 mph brackets the US posted-limit range by
inspection, and no derivation or sensitivity test is recorded. It is a guard
against malformed sections rather than a calibrated filter, and it is applied
silently per section — only an ALL-sections-rejected outcome is warned about,
so a study losing legitimate sections at the edges gets no count.
sanity floor; a section outside [min, max] is ignored
max_mph sanity ceiling
bbox_pad_deg padding on the graph's node bbox for the section fetch
buffer_m --- matcher caps (wider than the freeway preload's 60 m: dense urban arterials
with one-way pairs need more slack than well-separated freeways) ---
score_cap Backstop only — NOT the binding constraint. At distance_cap_m = 80 the highest
score _generic_candidate_score can return is ≈2.8, so any cap above that is
inert. The class gate below is what actually filters.
max_fsystem_rank The real filter: a matched section's f_system must rank this highly in the
edge's [osm_highway_to_tmas_fsystem_rank] list (top 3). Reuses the count
matcher's table rather than adding a second class map. Measured on a mid-size tolled metro:
`tertiary` binds inside the top 3 agree with OSM 84.9% exactly, binds outside it
only 50.8% (they are nearby PRINCIPAL arterials — a different road), and the
gate costs 0.25 pp of network VMT. Distance caps do NOT separate these: at 30 m
the split is unchanged, while dropping from 80 m to 30 m costs 2.9 pp of VMT to
buy 1.1 pp of agreement.
classes Eligible OSM classes. Every `*_link` is deliberately absent: an HPMS ramp
section inherits its MAINLINE's f_system and geometry, so a ramp binds to the
freeway beside it (measured: `*_link` edges match at a median 14.5 m vs 2.4 m
for through classes, and 74% of `motorway_link` binds land on an Interstate
section at a median 60 mph against a 49.7 mph default). Ramps keep the flat
class default.
freeway Edges group for propagation by (street name, class family). Families — not raw
highway tags — because real corridors flip primary↔secondary on tag noise, while
grouping across families would let a collector inherit a motorway limit.
`*_link` tags map to their parent tag's family.
[signal_delay]6 keys
Key
Effective value
Set by
TOML comment
enabled
true
default
Fold HCM zero-volume uniform control delay into effective free-flow speed on
every signal-approach edge (edge ENDING at a mapped traffic-signal node):
v_eff = length / (length / speed_limit_kph + delay_sec)
The approach set (identify_signal_approaches) is the SINGLE signal-adjacency
definition: the same edges take signal_capacity_factor at graph build. The
fold gives every approach ≥15 s of t0, so the capacity cut prices delay at
signal-cycle scale even on short edges — which is why there is no 300 m
`near_signal` upstream propagation (it crossed ramp diverges onto
freeway mainlines and priced one signal on several edges in series).
delay_sec
15.0
default
~half of red at typical cycle length / green ratio;
the arterial's own approach (major rank, no second
major-rank road at the junction)
minor_approach_delay_sec
trunk
45.0
primary
45.0
secondary
35.0
tertiary
25.0
default
Delay by the approach's place in the junction
(intersection_control.approach_position). A minor approach (ranked below the
junction's major road, ramp terminals included) gets the cross street's short
green; keyed on the MAJOR road's class. Basis: HCM uniform delay
d1 = 0.5·C·(1 − g/C)² / (1 − X·g/C), peak-leaning C ≈ 135 s, X ≈ 0.5, minor
g/C 0.25 / 0.35 / 0.45 crossing primary / secondary / tertiary → 43 / 35 / 26 s.
The arterial's own approach at g/C ≈ 0.65 gives ~12 s, so delay_sec stands.
Absent ⇒ delay_sec.
equal_rank_crossing_delay_sec
25.0
default
two roads of the major rank cross (≥3
major-rank neighbours): shared green, g/C ≈ 0.45
at C ≈ 135 s → 26 s, on every major-rank approach
cluster_gap_m
50.0
default
The two levels below are judgement; only delay_sec above carries an anchor.
50 m is a round stand-in for "OSM mapped one intersection as several signal
nodes", and nothing establishes it against 30 or 80. The floor is not inert:
on a mid-size metro with a congested core it binds on 226 approach edges, and
a floored edge realizes less delay than the fold computed for it — so the
level decides how much delay the shortest approaches are allowed to carry.
u also a signal and edge shorter than this ⇒ not an
approach (no fold, no capacity cut); OSM maps one
intersection as several signal nodes
min_folded_speed_kph
5.0
default
floor, so a very short approach edge can't fold to ~0.
5, not 10: at
10 the cross-street charges realised 71 % in a
dense suburban extract (86 % Burlington), at 5 → 87 % / 96 %.
5 matches [clamps] min_speed_kph, so route choice
and the congested skims see the same charge.
[stop_delay]7 keys
Key
Effective value
Set by
TOML comment
enabled
true
default
Unsignalized control delay, folded into effective free-flow speed on every
stop / yield approach exactly like [signal_delay]:
v_eff = length / (length / speed_limit_kph + delay)
Approaches (intersection_control.identify_control_approaches): tagged OSM
control first — stop-line nodes resolved to their approach, highway=stop /
give_way on the junction node (stop=all → every approach; else the
lower-ranked approaches, every approach if all rank equal), roundabout and
mini-roundabout entries (yield). Junctions with no tagged control are then
imputed by `imputation`. Stop approaches also take the class's
signal_capacity_factor; yields do not.
stop_delay_sec
10.0
default
HCM two-way-stop minor approach at the LOS A/B
boundary. Zero-conflict floor is ~8 s (follow-up
headway 3.3 s + 5 s accel/decel); ~11.5 s at
500 veh/h cross traffic. All-way stops share it
(HCM zero-volume ~7–8 s). A routing-API comparison
in a large metro priced an imputed stop at ~12 s
at 08:00 and ~4–5 s at 03:00.
minor_approach_delay_sec
trunk
30.0
primary
30.0
secondary
18.0
tertiary
12.0
default
A minor approach's stop delay keyed on the class of the major
road it stops for (classes absent here, e.g. a local × service stop, keep
stop_delay_sec). Basis: HCM two-way-stop minor-street delay, critical headway
7.1 s, follow-up 3.5 s, ~50 veh/h on the minor approach, peak-leaning
conflicting flow ~1,000–1,200 / 700 / 400 veh/h on primary / secondary /
tertiary → 26–37 / ~17 / ~12 s.
yield_delay_sec
5.0
default
roundabout / yield entry at low volume: slow, no full stop
imputation
"class_hierarchy"
default
"class_hierarchy" | "none". class_hierarchy:
at an untagged unsignalized junction, approaches
ranked below the major road stop. Rank by OSM
highway: trunk > primary > secondary > tertiary >
residential/unclassified > service; *_link ramps
lowest; a motorway mainline at the junction ⇒
merge, no stop; equal ranks ⇒ uncontrolled.
median_gap_m
50.0
default
an imputed approach shorter than this whose tail
also touches the major road is the median of a
divided road: one stop, not two
min_folded_speed_kph
5.0
default
same floor and basis as [signal_delay]
[clamps]3 keys
Clamps
Key
Effective value
Set by
TOML comment
signal_capacity_factor_default
0.5
default
fallback when a functional class omits signal_capacity_factor
min_speed_kph
5.0
default
floor on congested speeds written to congested_speeds.parquet; v/c reported unclamped
inverse_volume_estimation
min_vc_after_inversion
0.05
max_vc_after_inversion
1.5
default
—
[congestion_index]2 keys
Congestion Index Speed Sources
Tunables for congestion-index car-speed sources (tomtom_traffic_index, and
later inrix_global_scorecard). These apply a published city-level congestion
ratio C (fraction of extra travel time vs free-flow) as a UNIFORM network-wide
slowdown: every edge's travel time becomes (1 + C) * t_freeflow. See
docs/modelers/methodology/congestion-speed-sources.md.
Key
Effective value
Set by
TOML comment
default_day_type
"weekday"
default
used when analysis_period.days_of_week is None
hour_aggregation
"first"
default
"first" or "mean" — how to reduce multiple analysis_period.hours
[toll_cost_per_km]1 key
Toll Costs
Key
Effective value
Set by
TOML comment
default
0.62
default
Dollars per km at peak hour, charged on any edge OSM tags `toll=yes`.
`default` is the fallback for a tolled edge with no per-facility row. It is a
rough average of current per-km charges on two priced urban-interstate
facilities in one US metro — roughly $1.00/mile, which sits at the TOP of the
ordinary US range. It is a stand-in, not a national average: a toll study that
does not override it is running at a high rate by default. Set a rate per
facility, keyed by the OSM `ref` string of its edges:
default = 0.62
"I 95 EXPR" = 0.35 # matches edges whose `ref` is exactly this
SR520 = 0.96
Facility rows are study-specific and belong in a study-level
traffic_analysis_parameters.toml, not here — the same convention
[custom_capacities] states below. `input_data/toll_links.csv` is the
alternative for per-edge (rather than per-`ref`) rates.
[toll_flat_usd]0 keys
Flat dollar charge added once per traversal of a matching (ref) edge — barrier /
gantry pricing. Keyed by OSM `ref` like
toll_cost_per_km, but with NO default (absent ⇒ 0). Tag only the gantry edge's
ref, or the flat charge multiplies across every edge of a long facility. A
study-level input_data/toll_links.csv can set per-edge flat/per-km rates for
facilities OSM's `toll` tag doesn't describe. See docs/operators/special_topics/toll-studies.md.
No keys.
[toll_hov_share]0 keys
Share of a facility's vehicles that ride free (HOV-3+ on an express lane),
keyed by OSM `ref` like toll_cost_per_km. The edge's whole charge (per-km and
flat) is multiplied by (1 - share), on the routeenvs and the assignment link
table alike. NO default: a facility without an entry is charged in full.
A fleet average, not an HOV class — every driver sees the discounted price.
toll_cost_per_km stays the POSTED price; the share carries the exemption.
A `hov_share` column in input_data/toll_links.csv overrides this per row.
Study-specific; set it in the study-level file with a basis comment:
"I 95 EXPR" = 0.30 # published facility HOV share, source + year
See docs/operators/special_topics/toll-studies.md §4.
No keys.
[toll_exemptions]1 key
Toll Exemptions
geom_ids to exempt from tolls, keyed by facility name. Populate at study or scenario level.
Key
Effective value
Set by
TOML comment
exemptions
(empty table)
default
e.g. MY_FACILITY = ["geom_id_1", "geom_id_2"]
[reversible_direction]0 keys
Reversible Facilities
Direction a reversible facility runs during the analysis period, by OSM `ref`
(case-insensitive), as a compass heading: north, south, east, west, northeast,
northwest, southeast, southwest. Needed only for ways mapped `oneway=reversible`
WITHOUT an `oneway:conditional` schedule. A conditional schedule is resolved at
the analysis period automatically (speed_utils.get_osm_graph), and it takes
precedence. An unconfigured reversible way builds two-way, with half its lanes
in each direction, and the car-graph build logs a WARNING naming its ref.
Each edge of the ref keeps the drawn direction within 60° of the heading. The
facility's ref-less ramps follow from the mainline at the junctions where they
meet it, and each one is logged. Study-specific: set at study level, e.g.
"I 95 EXPR" = "north" # AM peak
"I 395 Express" = "north"
Changing this, or the analysis period, takes effect only after a
physical_conditions re-run (the car graph is built once per study).
No keys.
[custom_capacities]0 keys
Custom Capacities
Hard-wired total capacities (not per lane) for specific roadways by OSM ref.
Study-specific — populate at study level (e.g. "I 270" = 3000).
No keys.
[custom_capacities_perlane]1 key
Key
Effective value
Set by
TOML comment
improved_lanes
1600
default
Hard-wired per-lane capacities for specific roadways by OSM ref.
[link_penalties]2 keys
Link Penalties
Key
Effective value
Set by
TOML comment
link_traversal_time_penalty_sec
12.0
default
—
link_traversal_time_penalty_max_length_m
300.0
default
—
[default_dir_lanes_by_highway]15 keys
Default Directional Lanes by Highway Type
Key
Effective value
Set by
TOML comment
motorway
3
default
—
motorway_link
1
default
—
trunk
2
default
—
trunk_link
1
default
—
primary
2
default
—
primary_link
1
default
—
secondary
2
default
—
secondary_link
1
default
—
tertiary
1
default
—
tertiary_link
1
default
—
unclassified
1
default
—
residential
1
default
—
living_street
1
default
—
service
1
default
—
track
1
default
—
[osm_highway_to_tmas_fsystem_rank]10 keys
OSM Highway → TMAS F-System Rank Mapping
Key
Effective value
Set by
TOML comment
motorway
[1, 2, 3]
default
Ordered list of candidate TMAS functional system codes for each OSM highway type.
trunk
[2, 1, 3]
default
—
primary
[3, 2, 4, 5]
default
—
secondary
[4, 3, 5, 2, 6]
default
—
tertiary
[5, 4, 6, 3, 7]
default
—
unclassified
[7, 6, 5]
default
—
residential
[7, 6, 5]
default
—
living_street
[7]
default
—
service
[7, 6, 5]
default
—
road
[4, 3, 5, 2, 6, 7]
default
—
[congestion_correction]5 keys
TTM-layer congestion correction (region-ratio)
Post-routing multiplicative correction of the zone x zone CAR (and derived
RIDEHAIL) travel-time matrix: TTM_corrected[i,j] = TTM_freeflow[i,j] * K(R(i),R(j)),
where K = mapbox_time / osm_freeflow_time per region pair (calibrated offline by
connectome/traffic_utils/congestion_calibration.py; applied by
congestion_correction.py at routing). OPT-IN, off by default.
This is a TTM-layer tier and is MUTUALLY EXCLUSIVE with the graph-layer speed
sources (tomtom_traffic_index etc.): K already contains congestion, so the base
traffic source MUST be osm_default (free-flow) or congestion is double-counted.
See docs/modelers/methodology/congestion-region-ratio.md.
Key
Effective value
Set by
TOML comment
enabled
false
default
—
method
"region_ratio"
default
—
artifact
(empty)
default
path to congestion_correction_artifact.json (abs, or relative to study dir)
zone_to_region
(empty)
default
path to zone_to_region.csv (abs, or relative to study dir)
require_freeflow_base
true
default
error at config load if base traffic source is not osm_default