burlington · control scenario

Model parameters

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.

Networks [physical_conditions]

Which roads make up the car network

motorwaymotorway_linktrunktrunk_linkprimaryprimary_linksecondarysecondary_linktertiarytertiary_linkresidential

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.

Basis: Singapore forced-choice route experiment (Scientific Reports, 2022)

Penalty for walking beside busy traffic

0.15

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.

Basis: ActivitySim prototype_mtc (starting values)

Trip-length adjustments (calibrated)

calibrated7 of 7 values changed
WORK
[0.0, -0.9929, -1.1705, -1.8549, -3.8149, -5.7143]
SCHOOL
[0.0, -0.775, -1.9005, -3.255, -4.5909, -5.9214]
DISCRETIONARY
[0.0, -0.9387, -2.0667, -3.1899, -4.4936, -5.0703]
ESCORT
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
NHB_WORK
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
NHB_OTHER
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
NHB_ESCORT
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0]

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
CARTRANSITBICYCLERIDEHAILCAR_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.0 2.0
THRESHOLD_MIN
25.0 25.0
SLOPE2
4.0 4.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.0 1.0
SLOPE2
3.6269 1.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.

Basis: Fare-elasticity check, report only: TCRP Report 95, ch. 12 (short-run transit fare elasticity)

Correction for walk trips missing from surveys

WALK
1.5

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)

[emissions] EMISSION_FACTOR_CAR_KG_PER_KMNo methodology page yet

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

[emissions] EMISSION_FACTOR_TRANSIT_KG_PER_PKMNo methodology page yet

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
KeyEffective valueSet byTOML comment
GEO_LEVELS["block", "block_group", "tract"]default—
WATER_THRESHOLD_PROPORTION0.05default—
WATER_MIN_FEATURE_FRACTION0.001default—
WATER_MIN_FEATURE_KM2_CAP0.1default
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_RATIO0.025default—
GEOID_LEN
tract
11
block_group
12
block
15
default—
[h3]1 key
zones: H3 hexagonal tiling
KeyEffective valueSet byTOML 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
KeyEffective valueSet byTOML comment
NUM_INCOME_BINS5default—
INCOME_BASIS"household"default
"per_capita" or "household" (default)
ACS_YEAR2022default
ACS5 vintage; pinned across Census API and NHGIS fallback for reproducibility
EMPLOYER_PARKINGtruedefault
── 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_SUBSIDY1.0default
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
KeyEffective valueSet byTOML comment
OVERTURE_CONFIDENCE_THRESHOLD★0.85default
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.
NHTS_TRIP_URL"https://nhts.ornl.gov/assets/2022/csv/tripv2pub.csv"default—
NHTS_PERSON_URL"https://nhts.ornl.gov/assets/2022/csv/perv2pub.csv"default—
NHTS_CATEGORIES_NOT_INCLUDED[4, 5, 8, 9, 18, 97, -9]default
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.
TRIP_CATEGORIES
HOME
nhts_WHY
[1, 2]
analysis_area_attributes
["census_total_pop"]
WORK
nhts_WHY
[3]
analysis_area_attributes
["lodes_jobs"]
SCHOOL
nhts_WHY
[6]
analysis_area_attributes
[]
overture_taxonomy
[["education"]]
CHILDCARE
nhts_WHY
[7]
analysis_area_attributes
[]
overture_taxonomy
[["services_and_business", "family_service", "child_care_and_day_care"]]
ESCORT
nhts_WHY
[10]
analysis_area_attributes
["escort_attractors"]
HEALTHCARE
nhts_WHY
[11]
analysis_area_attributes
[]
overture_taxonomy
[["health_care"]]
overture_taxonomy_exclude
[["health_care", "medical_service", "home_health_care"], ["health_care", "medical_service", "health_insurance_office"]]
EAT_DRINK
nhts_WHY
[12]
analysis_area_attributes
[]
overture_taxonomy
[["food_and_drink"]]
SHOP
nhts_WHY
[13, 14]
analysis_area_attributes
[]
overture_taxonomy
[["shopping"], ["lifestyle_services"]]
RECREATION
nhts_WHY
[15]
analysis_area_attributes
[]
overture_taxonomy
[["arts_and_entertainment"], ["arts_and_entertainment", "event_venue", "auditorium"]]
overture_taxonomy_exclude
[["arts_and_entertainment", "event_venue"]]
EXERCISE
nhts_WHY
[16]
analysis_area_attributes
[]
overture_taxonomy
[["sports_and_recreation"]]
SOCIAL_VISIT
nhts_WHY
[17]
analysis_area_attributes
["census_total_pop"]
RELIGIOUS_OR_COMMUNITY
nhts_WHY
[19]
analysis_area_attributes
[]
overture_taxonomy
[["cultural_and_historic"], ["community_and_government"]]
overture_taxonomy_exclude
[["community_and_government", "government_office"], ["community_and_government", "public_safety_service"], ["cultural_and_historic", "historic_site"], ["cultural_and_historic", "memorial_site", "cemetery"]]
AIRPORTS
nhts_WHY
[]
analysis_area_attributes
["airport_enplanements_annual"]
EVENT
nhts_WHY
[]
analysis_area_attributes
["event_attendance"]
default
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
KeyEffective valueSet byTOML comment
MIN_POPULATION_PER_GEOM75default
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_GEOM5default
min total trip-ends to retain on destinations alone
[physical_conditions]5 keys
physical_conditions: Physical Conditions
KeyEffective valueSet byTOML comment
GTFS_MIN_OVERLAP1e-05default
min fraction of a feed's bbox that must overlap the study area
ROUTING_AREA_BUFFER_FROM_ANALYSIS_ZONES_M5000default
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_M2500default
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_QAfalsedefault
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").
DRIVABLE_HIGHWAY_TYPES★["motorway", "motorway_link", "trunk", "trunk_link", "primary", "primary_link", "secondary", "secondary_link", "tertiary", "tertiary_link", "residential"]default
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
KeyEffective valueSet byTOML comment
TOLL_EXEMPT_FACILITIES[]default—
[routing]45 keys · 2 overridden
routing: Routing
KeyEffective valueSet byTOML comment
ROUTING_MODES["CAR", "TRANSIT", "WALK", "BICYCLE"]default—
transit_ttm_stashtruedefault
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_YEAR2080default—
VOT_DIVISOR★2default
value-of-time = hourly_wage / VOT_DIVISOR (at the reference income)
VOT_INCOME_ELASTICITY★0.5default
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_INCOME80610default
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★2default
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.0default
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_ACCESS2.0default—
TRANSIT_OVT_MULTIPLIER_EGRESS2.0default—
TRANSIT_OVT_MULTIPLIER_TRANSFER2.0default—
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_FACTOR1.0default
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.13default
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_MILE0.1104default
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_FEE0.0default
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_MINUTE0.75default
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.0default
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_MIN3.0default
max walk time from spot to destination (min)
PARKING_TIME_DENSITY_COLUMN"combined"default
see comment above
PARKING_TIME_DENSITY_REF_PERCENTILE95default
percentile used as time-scaling reference
PARKING_SEARCH_ONEROUSNESS2.0default
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_CHOICEtruedefault
--- 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
PARKING_CHOICE_SCALE2.0default
logit scale mu; Hunt & Teply (1993), 1.5-2x mode choice
PARKING_GARAGE_WALK_TIME_MIN1.5default
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_MIN20.0default
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_THRESHOLD1.0default
$/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_MINUTES5default
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_FARE5default
base fare ($)
RIDEHAIL_PER_MINUTE★1default
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_SHARE0.5714default
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_FACTOR1.15default
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_HOUR8default
hour of day for routing (0–23); AM peak default
SPEED_WALKING_M_S★1.2default
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.17default
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_S0.6default
slow cycling / walking a bike on stressful segments
LTS_OVERAGE_PENALTY_MIN0.0default
fixed time penalty per above-tolerance edge (minutes)
qa
min_finite_fraction
0.001 0.5
max_bad_rowcols
25 0
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.
coarse_zone_simplification
enabled
false
fine_resolution_threshold
8
buffer_meters
1000
keep_outside_buffer_walk
["primary", "primary_link", "secondary", "secondary_link", "tertiary", "tertiary_link", "footway", "path", "cycleway", "pedestrian", "steps", "track"]
keep_outside_buffer_bike
["primary", "primary_link", "secondary", "secondary_link", "tertiary", "tertiary_link", "cycleway", "path", "track"]
keep_outside_buffer_car
["motorway", "motorway_link", "trunk", "trunk_link", "primary", "primary_link", "secondary", "secondary_link", "tertiary", "tertiary_link", "residential"]
default
─── 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_MIN60studyanalysis_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.
KeyEffective valueSet byTOML comment
SLOPE_IMPEDANCE_ENABLEDfalsestudyanalysis_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_FLOOR1.0default
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_M20.0default
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_FACTOR0.5default
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_MINUTES1.0default
floor on intrazonal travel time (minutes)
SPEED_CAR_LOCAL_M_S6.9default
25 km/h — local/intrazonal driving speed
INTRAZONAL_CV0.38default
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_CAR150default
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_TRANSIT120default—
MAX_ROUTING_TIME_MIN_WALK120default—
MAX_ROUTING_TIME_MIN_BICYCLE120default—
[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 }, ]
KeyEffective valueSet byTOML comment
STREETSCAPE_CANOPY_ENABLEDtruedefault
tree canopy: thermal shade + the aesthetic floor (K_GREEN)
STREETSCAPE_BUILDING_SHADE_ENABLEDtruedefault
static building shade, read off the enclosure H/W
STREETSCAPE_FRONTAGE_ENABLEDtruedefault
interest: fixation targets per 100 m, minus dead frontage
STREETSCAPE_ENCLOSURE_ENABLEDtruedefault
height-to-width "outdoor room"
STREETSCAPE_TRAFFIC_ENABLEDtruedefault
Landis-form speed x volume x separation
STREETSCAPE_CROSSING_ENABLEDtruedefault
ODOT PLTS crossing lookup on the junction table (additive)
STREETSCAPE_BAND_HALF_WIDTH_M3.0default
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_M60.0default
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_M20.0default—
STREETSCAPE_BUILDING_DEFAULT_HEIGHT_M4.0default
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_M3.5default—
STREETSCAPE_FRONTAGE_DEPTH_M25.0default
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_M20.0default—
STREETSCAPE_PLACES_MIN_CONFIDENCE0.5default—
STREETSCAPE_PLACE_DOOR_MAX_M20.0default
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_*`.
STREETSCAPE_FRONTAGE_ACTIVE_TAXONOMY[["food_and_drink"], ["shopping"], ["lifestyle_services", "personal_or_beauty_service"], ["arts_and_entertainment"], ["services_and_business", "financial_service", "bank_or_credit_union"], ["services_and_business", "laundry_service"]]default
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_M2.0default—
STREETSCAPE_R5_MATCH_TOLERANCE_M0.5default
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_ROUNDS1default
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_VPH50.0default
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_M25.0default
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_DEG25.0default—
STREETSCAPE_CROSSING_CONTROL_RADIUS_M20.0default
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.15default
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_FLOOR2.0default—
STREETSCAPE_TRAFFIC_MAX_MULT2.0default—
STREETSCAPE_TRAFFIC_OUTSIDE_LANE_FT12.0default
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_FT5.0default—
STREETSCAPE_TRAFFIC_PARKING_TERM10.0default—
STREETSCAPE_TRAFFIC_BIKELANE_FT5.0default—
STREETSCAPE_TRAFFIC_LANE_WIDTH_M3.5default—
STREETSCAPE_TRAFFIC_PARKING_LANE_M2.4default—
STREETSCAPE_TRAFFIC_BUFFER_TREE_FACTOR1.0default—
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_MIN0.45default
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_DEDUCTION1default—
STREETSCAPE_CROSSING_R5_SNAP_TOLERANCE_M5.0default
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.16default
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_SHADE0.5default—
STREETSCAPE_HW_FULL_SHADE1.0default—
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_DECLARED1.0default—
STREETSCAPE_CLIMATE_HEAT_INDEX_ZERO_C26.0default
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_C33.6default—
STREETSCAPE_CLIMATE_ANCHOR_EXPOSURE0.8default—
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_KM75.0default
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_KM150.0default—
STREETSCAPE_K_GREEN0.05default
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_ENCLOSURE0.1default
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_SATURATION0.5default—
STREETSCAPE_INTEREST_WEIGHT_DOOR1.0default
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_JUNCTION3.0default
above objects; Lynch's nodes
STREETSCAPE_INTEREST_WEIGHT_WATER3.0default
untested on any fixture within 30 m; route-choice water premium
STREETSCAPE_INTEREST_WEIGHT_OBJECT1.0default
bench / fountain / artwork / memorial / playground
STREETSCAPE_INTEREST_WEIGHT_GARDEN1.0default—
STREETSCAPE_INTEREST_WEIGHT_TREE0.0default
OSM tree nodes are a mapper artefact (105 on the Mall, 0 on O St); canopy carries trees
STREETSCAPE_INTEREST_WATER_RADIUS_M30.0default—
STREETSCAPE_INTEREST_POINT_RADIUS_M20.0default—
STREETSCAPE_INTEREST_SATURATION_PER_100M20.0default
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_INTEREST0.1default
= 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_FRONTAGE0.1default
surface parking / fuel / industrial first hits pay more than a blank wall, additive
STREETSCAPE_INTEREST_WINDOW_M200.0default
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_DEG45.0default—
STREETSCAPE_INTEREST_JUNCTION_MERGE_M20.0default
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
interceptlog_widthvolumespeed
hcm20106.04681.22760.00910.0009
landis20015.38761.20210.2530.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.
landis2001 landis2001: Landis, Vattikuti, Ottenberg, McLeod & Guttenplan 2001, eq. 2, as fitted.
[parking]5 keys · 2 overridden
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.
KeyEffective valueSet byTOML 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_MAX3.0default
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.9 99.9
density_power
2 2
free_below_density
400 0
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
density_power elasticity: 1 = linear, <1 = concave, >1 = convex
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).
KeyEffective valueSet byTOML 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 overridden
Section 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.
KeyEffective valueSet byTOML 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.
LOGSUM_PURPOSE_GROUPS
WORK
["WORK"]
SCHOOL
["SCHOOL"]
DISCRETIONARY
["CHILDCARE", "HEALTHCARE", "EAT_DRINK", "SHOP", "RECREATION", "EXERCISE", "SOCIAL_VISIT", "RELIGIOUS_OR_COMMUNITY"]
ESCORT
["ESCORT"]
NHB_WORK
[]
NHB_OTHER
[]
NHB_ESCORT
[]
default
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.
LOGSUM_DISTANCE_BAND_CONSTANTS★
WORK
[0.0, -0.9929, -1.1705, -1.8549, -3.8149, -5.7143]
SCHOOL
[0.0, -0.775, -1.9005, -3.255, -4.5909, -5.9214]
DISCRETIONARY
[0.0, -0.9387, -2.0667, -3.1899, -4.4936, -5.0703]
ESCORT
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
NHB_WORK
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
NHB_OTHER
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
NHB_ESCORT
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
calibratedcalibrated_logsum_parameters.toml7 of 7 values changed—
LOGSUM_VALUATION_VOT_USD_PER_HR★19.4default
--- 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_LOGSUMSfalsedefault
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.0default—
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.0 2.0
THRESHOLD_MIN
25.0 25.0
SLOPE2
4.0 4.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.0 1.0
SLOPE2
3.6269 1.0
THRESHOLD_MIN
39.0
calibratedcalibrated_logsum_parameters.toml3 of 3 values changed—
LOGSUM_WALK_AVAILABILITY_CAP_MINfalsedefault
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_MINfalsedefault—
LOGSUM_RIDEHAIL_WAIT_MULTIPLIER1.0default
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_RATE2.0default
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_KINKtruedefault
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_THRESHOLD2.5default
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_CIRCUITY1.2default
--- 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_WORKERS0default
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_SUPPORTtruedefault
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_CURVATUREtruedefault
--- 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_MIN240default
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_MIN720default
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_ELASTICITYfalsedefault
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_ELASTICITYtruedefault
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_PCT0.1default
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_DAMPING0.5default—
LOGSUM_ELASTICITY_SCALE_MIN0.25default—
LOGSUM_ELASTICITY_SCALE_MAX4.0default—
LOGSUM_WORK_RESIDUAL_TRANSFER★truedefault
--- 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_FRACTION0.7default—
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_BUNDLINGtruedefault
--- 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_TRIPS30default—
LOGSUM_DENSITY_CLASSESfalsedefault
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★falserun 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_WALKfalserun 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_REUSEtruedefault
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_CAP0.5default
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_SELECTION1.0default
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).
LOGSUM_NHB
enabled
false
chain_purposes
["CHILDCARE", "HEALTHCARE", "EAT_DRINK", "SHOP", "RECREATION", "EXERCISE", "SOCIAL_VISIT", "RELIGIOUS_OR_COMMUNITY"]
category_weights
(empty table)
centering
"per_category"
asc_group
"DISCRETIONARY"
theta
0.6755
phi
(empty table)
default
--- 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★
CARTRANSITBICYCLERIDEHAILCAR_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 ≈ MTC auto_deficient (vehicles < workers)
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.
KeyEffective valueSet byTOML comment
EMISSION_FACTOR_CAR_KG_PER_KM★0.31default
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.09default
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_KM0.0default—
EMISSION_FACTOR_BICYCLE_KG_PER_KM0.0default—
RIDEHAIL_DEADHEAD_FACTOR1.4default
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_FACTOR1.3default
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_FACTOR1.3default
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.
KeyEffective valueSet byTOML comment
enabledtruedefault—
osm_tags["leisure=park,nature_reserve,golf_course", "aeroway=aerodrome", "landuse=cemetery,military,forest,recreation_ground", "natural=water,wood,wetland"]default—
min_feature_area_pct_of_mean_zone0.25default
phase A: skip mask features below this
mask_close_radius_m25default
phase B: morphological close radius
mask_hole_close_threshold_m25000default
phase B: fill interior mask holes below this
zone_hole_fill_threshold_pct0.1default
phase D: fill zone-part holes below pct of original area
zone_sliver_threshold_pct0.05default
phase D: drop parts below pct of original area
zone_sliver_threshold_abs_m22500default
phase D: floor for sliver threshold
empty_tract_ruletruedefault
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_m100default—
empty_tract_min_share_of_zone0.75default—
orphan_block_snap_max_m2000default
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.
KeyEffective valueSet byTOML comment
enabledtruedefault—
snap_method"nearest_zone"default—
nearest_zone(empty table)default
(no params today; placeholder for future)
[visualization]1 key
KeyEffective valueSet byTOML comment
comparison
diverging_scale_percentile
95
floors
access_min_per_person
0.0185
trips_per_person
1.3e-05
vm_miles_per_person
0.0014
default
─── Visualization ───────────────────────────────────────────────────────────
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.
KeyEffective valueSet byTOML 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.
KeyEffective valueSet byTOML comment
REQUIRE_FRESH_INPUTStruedefault—
[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.
KeyEffective valueSet byTOML 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).
KeyEffective valueSet byTOML comment
keep_stage_snapshotsfalsedefault—

Study-defined sections in the study's analysis_parameters.toml only

[study]study-defined7 keys · 7 overridden
KeyEffective valueSet byTOML 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."studyanalysis_parameters.tomlno codebase default—
default_traffic_source"osm_default"studyanalysis_parameters.tomlno codebase default—
incremental_evaluationtruestudyanalysis_parameters.tomlno codebase default—
h3_adaptive
outer_buffer
0
base_resolution
6
max_resolution
8
split_threshold
5000
weight_population
1.0
weight_trip_ends
10.0
1 file-path setting inside, not shown
studyanalysis_parameters.tomlno codebase default—
[traffic_sources]study-defined2 keys · 2 overridden
KeyEffective valueSet byTOML comment
osm_default
type
"osm_default"
studyanalysis_parameters.tomlno codebase default—
burlington_congested
type
"tomtom_traffic_index"
city
"Burlington"
studyanalysis_parameters.tomlno codebase default—
[analysis_period]study-defined4 keys · 4 overridden
KeyEffective valueSet byTOML comment
year2026studyanalysis_parameters.tomlno codebase default—
months[6]studyanalysis_parameters.tomlno codebase default—
hours[8]studyanalysis_parameters.tomlno codebase default—
days_of_week[2]studyanalysis_parameters.tomlno codebase default—

traffic_analysis_parameters.toml Traffic assignment parameters · the study runs traffic assignment

[functional_classes]6 keys
KeyEffective valueSet byTOML 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.
local
alpha
0.9
beta
2.0
capacity_perlane
1200
min_capacity_perlane
900
max_capacity_perlane
1400
always_treat_as_signalized
0
signal_capacity_factor
0.5
must_be_grade_separated
0
osm_highway_tags
["residential", "unclassified", "service", "living_street", "road", "track"]
default
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
KeyEffective valueSet byTOML comment
phf0.92default
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.
KeyEffective valueSet byTOML comment
enabled★truestudytraffic_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
demand_feedbacktruestudytraffic_analysis_parameters.tomltrue
false => fixed-demand (v1); true => in-memory MSA/logsum loop
rgap_target0.0001default
BFW relative-gap convergence target
max_iter100default
BFW max iterations
on_nonconvergence"warn"default
"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_threads0default
0 => all cores; set explicitly to benchmark engines on one footing
tapb_timeout_s14400default
hard subprocess ceiling; the solve's own stopping rules should bind first
tapb_rgap_target1e-05default
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_bushestruedefault
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.
cores0default
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_stashtruedefault
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_workers3default
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_evalstruedefault
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
6 3
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
max_mismatch_ratio target gate: max(modeled/target, target/modeled) above ⇒ gross-mismatch, 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.motorway <50k 50k-200k 200k-1M >1M
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 (◇)
vmt_reconciliation
enabled
true
hour_factor
"measured"
bind_classes
["motorway", "trunk", "primary", "secondary", "tertiary"]
max_fsystem_rank
3
distance_cap_m
80.0
buffer_m
80.0
default
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
[setup]3 keys
observed_counts — observed traffic counts (demand side)
KeyEffective valueSet byTOML comment
counts
sources
["tmas"]
outside_area_buffer_km
0.5
tmas
use_latest_available
true
manual
(empty table)
matcher
route_match_func_discount
0.25
guard_f_system_max
4
guard_minor_highways
["residential", "unclassified", "living_street", "service"]
guard_max_obs_over_capacity
1.2
default
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.
preload
enabled
true
method
"gateway"
gateway_classes
["motorway", "trunk", "primary", "secondary", "tertiary"]
conflation_classes
["motorway", "trunk", "primary", "secondary", "tertiary"]
cluster_radius_m
400
cordon_gate
true
cordon_gate_missing_way_tolerance
0
gateway_ring_inset_m
200
hpms
year
0
hour_factors
default
"measured"
truck_default
0.05
access
enabled
true
beta
0.03
attraction_col
"total_trip_ends"
production_col
"home_trip_ends"
default
─── background_preload — background preload (externals/trucks) ───
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)
access.production_col I-E origin pull (home-based external departures)
connector_seeds
enabled
false
lodes_year
2022
block_assignment
"centroid"
exclude_poi_categories
["other"]
suppression_radius_frac
1.0
seed_bands
[
{ max_diameter_km = 0.75, n_seeds = 1 },
{ max_diameter_km = 3.0, n_seeds = 2 },
{ n_seeds = 4 },
]
default
[[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.
suppression_radius_frac cluster radius = frac × zone_diameter / n_seeds
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
KeyEffective valueSet byTOML 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_vph50.0default
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_m75.0default
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_deg60.0default—
[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.
KeyEffective valueSet byTOML comment
same_street_propagationtruedefault
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_m1500.0default
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_kph10.0default
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
hpms
enabled
true
year
0
f_systems
[1, 2, 3, 4, 5, 6]
min_mph
15
max_mph
85
bbox_pad_deg
0.02
buffer_m
80.0
distance_cap_m
80.0
score_cap
5.0
max_fsystem_rank
3
classes
["motorway", "trunk", "primary", "secondary", "tertiary"]
default
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.
class_families
freeway
["motorway", "trunk"]
arterial
["primary", "secondary", "tertiary", "unclassified"]
local
["residential", "living_street", "service"]
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
KeyEffective valueSet byTOML comment
enabledtruedefault
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_sec15.0default
~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_sec25.0default
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_m50.0default
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_kph5.0default
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
KeyEffective valueSet byTOML comment
enabledtruedefault
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_sec10.0default
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_sec5.0default
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_m50.0default
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_kph5.0default
same floor and basis as [signal_delay]
[clamps]3 keys
Clamps
KeyEffective valueSet byTOML comment
signal_capacity_factor_default0.5default
fallback when a functional class omits signal_capacity_factor
min_speed_kph5.0default
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.
KeyEffective valueSet byTOML 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
KeyEffective valueSet byTOML comment
default0.62default
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.
KeyEffective valueSet byTOML 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
KeyEffective valueSet byTOML comment
improved_lanes1600default
Hard-wired per-lane capacities for specific roadways by OSM ref.
[default_dir_lanes_by_highway]15 keys
Default Directional Lanes by Highway Type
KeyEffective valueSet byTOML comment
motorway3default—
motorway_link1default—
trunk2default—
trunk_link1default—
primary2default—
primary_link1default—
secondary2default—
secondary_link1default—
tertiary1default—
tertiary_link1default—
unclassified1default—
residential1default—
living_street1default—
service1default—
track1default—
[osm_highway_to_tmas_fsystem_rank]10 keys
OSM Highway → TMAS F-System Rank Mapping
KeyEffective valueSet byTOML 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.
KeyEffective valueSet byTOML comment
enabledfalsedefault—
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_basetruedefault
error at config load if base traffic source is not osm_default