From fd33457c59c4ba4b84fc3d02e305a6f4f648584f Mon Sep 17 00:00:00 2001 From: David Hensle <51132108+dhensle@users.noreply.github.com> Date: Tue, 19 May 2026 11:34:49 -0700 Subject: [PATCH 1/3] Updated Readme --- README.md | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 81 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 17b3626..5813bd4 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,39 @@ Simulate Oregon (SimOR) - Oregon's Jointly Estimated ActivitySim Model ![image](SimOR.png) +> [!WARNING] +> This model is under active development, and the following instructions are subject to change. + +Please see this repo's Wiki page for more detailed information on this model. + +## Repository structure + +The repository is organized into the following major components: + +| Path | Purpose | +|------|---------| +| `runSIMOR.bat` | Top-level batch script that runs environment setup, skimming, preprocessing, and the current ActivitySim example run | +| `setup_environment.bat` | Installs UV if needed, clones and updates external dependencies, creates Python environments, and installs required Visum Python packages | +| `ext_dependencies/` | Cloned external repositories and their virtual environments, including ActivitySim, maz_skimming, and optionally sandag_parking | +| `resident/` | ActivitySim model code, configurations, test cases, model input data, and outputs | +| `resident/configs/` | Shared ActivitySim settings used across regions | +| `resident/configs_*` | Region-specific ActivitySim settings, constants, and overrides such as `configs_skats`; additional region folders such as `configs_metro` or `configs_lcog` may be added over time | +| `resident/model_data/` | Region-specific model inputs, including cropped example datasets and full datasets | +| `skimming_and_assignment/visum/` | Visum runner scripts, procedure sequences, and version files used to build motorized skims and export network inputs | +| `skimming_and_assignment/maz_maz_stop_skims/` | Non-motorized skim preprocessor and skim settings | +| `misc/` | Miscellaneous analysis and support scripts | + ## Running the model +### Prerequisites + +Before running the model, make sure the following are available: + +1. PTV Visum 2026 with its bundled Python installation +2. Git available on your system PATH or installed in a standard Windows location +3. PowerShell with permission to run the UV installer invoked by `setup_environment.bat` +4. Local write access to the Visum Python `site-packages` directory, or Administrator privileges if package installation there is restricted + ### Setup 1. **Configure `setup_environment.bat`** @@ -12,14 +43,18 @@ Simulate Oregon (SimOR) - Oregon's Jointly Estimated ActivitySim Model | Variable | Description | Default | |----------|-------------|---------| - | `VISUM_PYTHON_DIR` | Folder containing your Visum 2026 Python interpreter | `C:\Program Files\PTV Vision\PTV Visum 2026\Exe\Junction_Preview\Python` | - | `INSTALL_PARKING` | Clone and install sandag_parking (`Y` or `N`) | `Y` | + | `VISUM_PYTHON_DIR` | Folder containing your Visum 2026 Python interpreter | `C:\Program Files\PTV Vision\PTV Visum 2026\Exe\Python` | + | `INSTALL_PARKING` | Clone and install sandag_parking (`Y` or `N`) | `N` | You can run this script on its own to install all dependencies without running the model: ``` setup_environment.bat ``` - It will install UV (if needed), clone and build the required repositories into `ext_dependencies/`, create the MAZ skimming Python environment from `ext_dependencies/maz_skimming/pyproject.toml`, and install the necessary Python packages into Visum's Python environment. On subsequent runs it detects existing installs and pulls the latest changes instead of re-cloning. + It will install UV (if needed), verify Git is available, clone and build the required repositories into `ext_dependencies/`, create the ActivitySim and MAZ skimming Python environments, and install the necessary Python packages into Visum's Python environment. On subsequent runs it detects existing installs and pulls the latest changes instead of re-cloning. + + `sandag_parking` is not required for a typical model run. It is only needed when preparing land use inputs that require expected parking costs. In that workflow, it can be run once to generate the parking cost input file used by the preprocessor. + + If permissions errors arise, try running the setup with Administrator privileges. The Visum Python package install in particular may require elevated permissions. 2. **Place the Visum version file** @@ -27,12 +62,19 @@ Simulate Oregon (SimOR) - Oregon's Jointly Estimated ActivitySim Model 3. **Update configuration files** - Edit the following files to match your local data paths and project settings: + Edit the following files to match your local data paths, region, and project settings: | File | Purpose | |------|---------| - | `skimming_and_assignment/maz_maz_stop_skims/2zoneSkim_params.yaml` | Non-motorized skim settings and file paths | - | `resident/preprocessor_settings.yaml` | Land use preprocessor input/output paths and network settings | + | `skimming_and_assignment/maz_maz_stop_skims/2zoneSkim_params.yaml` | Non-motorized skim settings, Visum export locations, network inputs, and skim outputs | + | `resident/preprocessor_settings.yaml` | Land use preprocessor input/output paths, network shapefiles, skim inputs, and optional parking or fare inputs | + + At minimum, confirm that these files point to the correct local locations for: + + 1. Household, person, and land use CSV inputs + 2. MAZ, node, and link shapefiles + 3. Non-motorized skim inputs and outputs + 4. Optional expected parking cost and transit fare inputs 4. **Set user-defined variables in `runSIMOR.bat`** @@ -42,6 +84,22 @@ Simulate Oregon (SimOR) - Oregon's Jointly Estimated ActivitySim Model |----------|-------------| | `VISUM_VERSION_FILE` | Filename of the Visum version file | | `PROCEDURE_SEQ` | Path to the Visum procedure sequence XML | + | `VISUM_PED_VERSION_FILE` | Optional pedestrian network Visum version file used when a separate pedestrian export is needed | + | `PED_PROCEDURE_SEQ` | Optional Visum procedure sequence for the pedestrian network export | + +### ActivitySim configuration layout + +ActivitySim settings are split into a shared configuration folder and optional region-specific configuration folders. + +- `resident/configs/` contains shared settings used across model regions. +- Region-specific folders such as `resident/configs_skats/`, and future folders such as `resident/configs_metro/` or `resident/configs_lcog/`, contain region-specific constants and overrides. +- When running a region-specific model, pass the region-specific config directory first and then the shared config directory so region overrides are applied before shared settings. + +For example: + +```bat +python resident\simulation.py -c resident\configs_skats -c resident\configs -d resident\model_data\skats\data_cropped -o resident\outputs\test +``` ### Running the Pipeline @@ -52,8 +110,23 @@ runSIMOR.bat The script automatically calls `setup_environment.bat` to ensure all dependencies are installed and Python paths are set, then runs the following steps in sequence: -1. **Motorized skims** — Using Visum `Visum_Runner.py`. Automatically outputs required files to run non-motorized skims. +1. **Motorized skims** — Using Visum `Visum_Runner.py`. Automatically outputs required files to run non-motorized skims. 2. **Non-motorized skim preprocessor** — Prepares walk network inputs via `2zoneSkim_preprocessor.py`. 3. **Non-motorized skims** — Computes MAZ-to-MAZ and MAZ-to-stop walk skims via `2zoneSkim.py`. 4. **Land use preprocessor** — Builds ActivitySim-ready land use table via `preprocessor.py`. -5. **Run ActivitySim** -- Runs ActivitySim -- (not yet implemented) \ No newline at end of file +5. **Run ActivitySim** — Runs the current ActivitySim example configured in `runSIMOR.bat`. + +At present, the ActivitySim command in `runSIMOR.bat` runs the Metro cropped example dataset: + +```bat +python resident\simulation.py -c resident\configs -d resident\model_data\metro\data_cropped -o outputs\cropped +``` + +This is a smoke-test style example run, not yet a generalized full-region launcher for all supported scenarios. + +### Expected inputs and outputs + +- Visum produces the motorized skims and exported network files consumed by the non-motorized skim workflow. +- The non-motorized skim tools produce files such as `maz_maz_walk.csv` and `maz_stop_walk.csv` for use by the land use preprocessor and ActivitySim. +- The land use preprocessor reads the configured raw model inputs and writes updated ActivitySim-ready tables to the configured output directory. +- ActivitySim writes model outputs to the output folder passed on the command line. \ No newline at end of file From 8b756649905b70dfa44a04fc6b4d86e11e9b5c4c Mon Sep 17 00:00:00 2001 From: David Hensle <51132108+dhensle@users.noreply.github.com> Date: Tue, 19 May 2026 17:12:37 -0700 Subject: [PATCH 2/3] Adding TRANSIT_FF --- resident/configs/park_and_ride_lot_choice.csv | 2 +- resident/configs/tour_mode_choice.csv | 8 +- resident/configs/trip_mode_choice.csv | 8 +- resident/preprocessor.py | 92 +++++++++++++++++-- resident/preprocessor_settings.yaml | 15 ++- 5 files changed, 106 insertions(+), 19 deletions(-) diff --git a/resident/configs/park_and_ride_lot_choice.csv b/resident/configs/park_and_ride_lot_choice.csv index 872d21c..3f346e0 100644 --- a/resident/configs/park_and_ride_lot_choice.csv +++ b/resident/configs/park_and_ride_lot_choice.csv @@ -10,7 +10,7 @@ util_ivtt,in vehcile transit time,@(ldt_skims['WTW_TIV'] + dlt_skims['WTW_TIV']) util_xfer_penalty,transfer penalty,"@(-23+23*np.exp(0.414*np.clip(ldt_skims['WTW_XFR'], a_min=None,a_max=4))) + (-23+23*np.exp(0.414*np.clip(dlt_skims['WTW_XFR'], a_min=None,a_max=4)))",coef_xfer_work util_stop_type_constant,stop type constant,@(ldt_skims['WTW_RST'] + dlt_skims['WTW_RST']),coef_ivt_work util_vehicle_type_constant,vehicle type constant,@(ldt_skims['WTW_VTC'] + dlt_skims['WTW_VTC']),coef_ivt_work -util_fare,transit fare,"@df.transitSubsidyPassDiscount * (ldt_skims['fare'] + dlt_skims['fare']) * 100*df.num_participants/df.cost_sensitivity",coef_income_work +util_fare,transit fare,"@df.transitSubsidyPassDiscount * df.get('TRANSIT_FF', 1) * (ldt_skims['fare'] + dlt_skims['fare']) * 100*df.num_participants/df.cost_sensitivity",coef_income_work util_small_lot,Small lot penalty -- 10 min penalty,"@np.where(df['PNR_SPACES'] < 100,10,0)",coef_ivt_work #util_med_lot,med lot penalty -- no penalty,"@np.where((df['PNR_SPACES'] >= 100) & (df['PNR_SPACES'] < 500),1,0) * 0",coef_ivt_work util_large_lot,large lot penalty -- 10 min back,"@np.where(df['PNR_SPACES'] >= 500,-10,0)",coef_ivt_work diff --git a/resident/configs/tour_mode_choice.csv b/resident/configs/tour_mode_choice.csv index f296dff..3107c60 100644 --- a/resident/configs/tour_mode_choice.csv +++ b/resident/configs/tour_mode_choice.csv @@ -70,7 +70,7 @@ util_WALK_Walk_egress_time,WALK_LOC - egress time,"@np.where(df.nev_local_egress util_WALK_wait_egress_time,WALK_LOC - Egress MT/NEV wait time,"@np.where(df.nev_local_egress_available, 2*nevWaitTime, np.where(df.microtransit_local_egress_available, 2*microtransitWaitTime, 0)) * df.time_factor",,,,,,coef_wait,,,,,,,,, util_WALK_stop_type_constant,WALK_LOC - Stop type constant,@(odt_skims['WTW_RST'] + dot_skims['WTW_RST'])* df.time_factor,,,,,,coef_ivt,,,,,,,,, util_WALK_vehicle_type_constant,WALK_LOC - Vehicle type constant,@(odt_skims['WTW_VTC'] + dot_skims['WTW_VTC'])* df.time_factor,,,,,,coef_ivt,,,,,,,,, -util_WTW_FARE,WALK_LOC - Fare,@df.transitSubsidyPassDiscount*(odt_skims['fare'] + dot_skims['fare'])*100*df.num_participants/df.cost_sensitivity,,,,,,coef_income,,,,,,,,, +util_WTW_FARE,WALK_LOC - Fare,"@df.transitSubsidyPassDiscount*df.get('TRANSIT_FF', 1)*(odt_skims['fare'] + dot_skims['fare'])*100*df.num_participants/df.cost_sensitivity",,,,,,coef_income,,,,,,,,, util_WALK_LOC_Age 16 to 24,WALK_LOC - Age 16 to 24,@(df.age > 15) & (df.age < 25),,,,,,coef_age1624_tran,,,,,,,,, util_WALK_LOC_Age 41 to 55,WALK_LOC - Age 41 to 55,@(df.age > 40) & (df.age < 56),,,,,,coef_age4155_tran,,,,,,,,, util_WALK_LOC_Age 56 to 64,WALK_LOC - Age 56 to 64,@(df.age > 55) & (df.age < 65),,,,,,coef_age5664_tran,,,,,,,,, @@ -95,7 +95,7 @@ util_PNR_Walk_other_time,PNR_LOC - Walk other time,@df.xfer_walk_pnr * df.time_f util_PNR_stop_type_constant,PNR_LOC - Stop type constant,@df.stop_type_pnr * df.time_factor,,,,,,,coef_ivt,,,,,,,, util_PNR_vehicle_type_constant,PNR_LOC - Vehicle type constant,@df.vehicle_type_pnr * df.time_factor,,,,,,,coef_ivt,,,,,,,, util_PNR_parking_cost,PNR_LOC - Parking cost,@df.parking_cost_pnr * 100 / df.cost_sensitivity,,,,,,,coef_income,,,,,,,, -util_PNR_Fare_and_operating_cost,PNR_LOC - Fare ,@df.transitSubsidyPassDiscount * df.fare_pnr * 100 * df.num_participants / df.cost_sensitivity,,,,,,,coef_income,,,,,,,, +util_PNR_Fare_and_operating_cost,PNR_LOC - Fare,"@df.transitSubsidyPassDiscount * df.get('TRANSIT_FF', 1) * df.fare_pnr * 100 * df.num_participants / df.cost_sensitivity",,,,,,coef_income,,,,,,,, util_PNR_LOC - Age 16 to 24,PNR_LOC - Age 16 to 24,@(df.age > 15) & (df.age < 25),,,,,,,coef_age1624_tran,,,,,,,, util_PNR_LOC - Age 41 to 55,PNR_LOC - Age 41 to 55,@(df.age > 40) & (df.age < 56),,,,,,,coef_age4155_tran,,,,,,,, util_PNR_LOC - Age 56 to 64,PNR_LOC - Age 56 to 64,@(df.age > 55) & (df.age < 65),,,,,,,coef_age5664_tran,,,,,,,, @@ -116,7 +116,7 @@ util_KNR_wait_egress_time_(at_attraction_end),KNR_LOC - Egress mt/nev wait time util_KNR_Walk_other_time,KNR_LOC - Walk other time,@df.knr_other_walk_time*df.time_factor,,,,,,,,coef_xwalk,,,,,,, util_KNR_stop_type_constant,KNR_LOC - Stop type constant,@df.knr_stop_constant* df.time_factor,,,,,,,,coef_ivt,,,,,,, util_KNR_vehicle_type_constant,KNR_LOC - Vehicle type constant,@df.knr_veh_constant* df.time_factor,,,,,,,,coef_ivt,,,,,,, -util_KNR_Fare_and_operating_cost,KNR_LOC - Fare ,@df.transitSubsidyPassDiscount*(odt_skims['fare'] + dot_skims['fare'])*100*df.num_participants/df.cost_sensitivity,,,,,,,,coef_income,,,,,,, +util_KNR_Fare_and_operating_cost,KNR_LOC - Fare ,@df.transitSubsidyPassDiscount*df.get('TRANSIT_FF', 1)*(odt_skims['fare'] + dot_skims['fare'])*100*df.num_participants/df.cost_sensitivity,,,,,,,,coef_income,,,,,,, util_KNR_LOC - Age 16 to 24,KNR_LOC - Age 16 to 24,@(df.age > 15) & (df.age < 25),,,,,,,,coef_age1624_tran,,,,,,, util_KNR_LOC - Age 41 to 55,KNR_LOC - Age 41 to 55,@(df.age > 40) & (df.age < 56),,,,,,,,coef_age4155_tran,,,,,,, util_KNR_LOC - Age 56 to 64,KNR_LOC - Age 56 to 64,@(df.age > 55) & (df.age < 65),,,,,,,,coef_age5664_tran,,,,,,, @@ -138,7 +138,7 @@ util_TNC_wait_egress_time_(at_attraction_end),TNC_LOC - Egress mt/nev wait time util_TNC_Walk_other_time,TNC_LOC - Walk other time,@(df.knr_other_walk_time) *df.time_factor,,,,,,,,,coef_xwalk,,,,,, util_TNC_stop_type_constant,TNC_LOC - Stop type constant,@(df.knr_stop_constant)* df.time_factor,,,,,,,,,coef_ivt,,,,,, util_TNC_vehicle_type_constant,TNC_LOC - Vehicle type constant,@(df.knr_veh_constant)* df.time_factor,,,,,,,,,coef_ivt,,,,,, -util_TNC_Fare_and_operating_cost,TNC_LOC - Fare ,@df.transitSubsidyPassDiscount*(odt_skims['fare'] + dot_skims['fare'])*100*df.num_participants/df.cost_sensitivity,,,,,,,,,coef_income,,,,,, +util_TNC_Fare_and_operating_cost,TNC_LOC - Fare ,"@df.transitSubsidyPassDiscount*df.get('TRANSIT_FF', 1)*(odt_skims['fare'] + dot_skims['fare'])*100*df.num_participants/df.cost_sensitivity",,,,,,coef_income,,,,,, util_TNC_LOC - Age 16 to 24,TNC_LOC - Age 16 to 24,@(df.age > 15) & (df.age < 25),,,,,,,,,coef_age1624_tran,,,,,, util_TNC_LOC - Age 41 to 55,TNC_LOC - Age 41 to 55,@(df.age > 40) & (df.age < 56),,,,,,,,,coef_age4155_tran,,,,,, util_TNC_LOC - Age 56 to 64,TNC_LOC - Age 56 to 64,@(df.age > 55) & (df.age < 65),,,,,,,,,coef_age5664_tran,,,,,, diff --git a/resident/configs/trip_mode_choice.csv b/resident/configs/trip_mode_choice.csv index 6a5fb10..49fe768 100644 --- a/resident/configs/trip_mode_choice.csv +++ b/resident/configs/trip_mode_choice.csv @@ -54,7 +54,7 @@ util_WALK_LOC_Walk_egress_time,WALK_LOC - Walk egress time,"@np.where(df.nev_loc util_WALK_LOC_wait_egress_time,WALK_LOC - Egress mt/nev wait time,"@np.where(df.nev_local_egress_available_out & df.outbound, nevWaitTime, np.where(df.microtransit_local_egress_available_out & df.outbound, microtransitWaitTime, 0))* df.time_factor",,,,,,coef_wait,,,,,,,,, util_WALK_LOC_transfer_walk_time,WALK_LOC - transfer walk time,@(odt_skims['WTW_AUX'])* df.time_factor,,,,,,coef_xwalk,,,,,,,,, util_WALK_LOC_transfers_penalty,WALK_LOC - number of transfers,"@(-23+23*np.exp(0.414*np.clip(odt_skims['WTW_XFR'] + df.outbound*df.mtnev_egr_xfer_out + ~df.outbound*df.mtnev_acc_xfer_in, a_min=None,a_max=4))) * df.time_factor",,,,,,coef_xfer,,,,,,,,, -util_WTW_FARE,WALK_LOC - Fare,"@df.transitSubsidyPassDiscount*(odt_skims['fare'])*100*df.number_of_participants/(np.maximum(df.income,1000)**df.income_exponent)",,,,,,coef_income,,,,,,,,, +util_WTW_FARE,WALK_LOC - Fare,"@df.transitSubsidyPassDiscount*df.get('TRANSIT_FF', 1)*(odt_skims['fare'])*100*df.number_of_participants/(np.maximum(df.income,1000)**df.income_exponent)",,,,,,coef_income,,,,,,,,, util_WALK_LOC - Female,WALK_LOC - Female,@(df.female),,,,,,coef_female_tran,,,,,,,,, util_WALK_LOC - Origin Mix,WALK_LOC - Origin Mix,oMGRAMix,,,,,,coef_oMix_wTran,,,,,,,,, util_WALK_LOC - Origin Intersection Density,WALK_LOC - Origin Intersection Density,oMGRATotInt,,,,,,coef_oIntDen_wTran,,,,,,,,, @@ -96,7 +96,7 @@ util_KNR_LOC_KNR_time,KNR_LOC - KNR time,@(df.ktw_odt_acc) * df.time_factor * d util_KNR_LOC_Walk_egress_time_(at_attraction_end),KNR_LOC - Walk egress time (at attraction end),"@np.where(df.nev_local_egress_available_out, df.nev_local_egress_time_out, np.where(df.microtransit_local_egress_available_out, df.microtransit_local_egress_time_out, df.dest_local_time)) * df.time_factor * df.outbound",,,,,,,,coef_acctime,,,,,,, util_KNR_LOC_wait_egress_time_(at_attraction_end),KNR_LOC - Egress mt/nev wait time (at attraction end),"@np.where(df.nev_local_egress_available_out, nevWaitTime, np.where(df.microtransit_local_egress_available_out, microtransitWaitTime, 0)) * df.time_factor * df.outbound",,,,,,,,coef_wait,,,,,,, util_KNR_LOC_Walk_other_time,KNR_LOC - Walk other time,@(df.ktw_odt_aux) * df.time_factor * df.outbound,,,,,,,,coef_xwalk,,,,,,, -util_KNR_LOC_Fare,KNR_LOC - Fare,"@df.transitSubsidyPassDiscount*(odt_skims['fare'])*100*df.number_of_participants/(np.maximum(df.income,1000)**df.income_exponent) * df.outbound",,,,,,,,coef_income,,,,,,, +util_KNR_LOC_Fare,KNR_LOC - Fare,"@df.transitSubsidyPassDiscount*df.get('TRANSIT_FF', 1)*(odt_skims['fare'])*100*df.number_of_participants/(np.maximum(df.income,1000)**df.income_exponent) * df.outbound",,,,,,,,coef_income,,,,,,, util_KNR_LOC_KNR_cost,KNR_LOC - KNR cost,"@(df.auto_op_cost * df.autoCPMFactor * (df.ktw_odt_acc/60) *driveSpeed )*100*df.outbound/(np.maximum(df.income,1000)**df.income_exponent)",,,,,,,,coef_income,,,,,,, util_KNRIN_LOC_KNR_cost,KNR_LOC - KNR cost,"@(df.auto_op_cost * df.autoCPMFactor * (df.ktw_odt_egr/60) *driveSpeed)*100*~df.outbound/(np.maximum(df.income,1000)**df.income_exponent)",,,,,,,,coef_income,,,,,,, util_KNRIN_LOC_In_vehicle_time,KNRIN_LOC - In-vehicle time,@(df.wtk_odt_tiv) * df.time_factor * ~df.outbound,,,,,,,,coef_ivt,,,,,,, @@ -107,7 +107,7 @@ util_KNRIN_LOC_KNRIN_time,KNRIN_LOC - KNR time,@(df.wtk_odt_egr) * df.time_facto util_KNRIN_LOC_Walk_access_time,KNRIN_LOC - Walk access time,"@np.where(df.nev_local_access_available_in, df.nev_local_access_time_in, np.where(df.microtransit_local_access_available_in, df.microtransit_local_access_time_in, df.origin_local_time)) * df.time_factor * ~df.outbound",,,,,,,,coef_acctime,,,,,,, util_KNRIN_LOC_wait_access_time,KNRIN_LOC - Egress mt/nev wait time,"@np.where(df.nev_local_access_available_in, nevWaitTime, np.where(df.microtransit_local_access_available_in, microtransitWaitTime, 0)) * df.time_factor * ~df.outbound",,,,,,,,coef_wait,,,,,,, util_KNRIN_LOC_Walk_other_time,KNRIN_LOC - Walk other time,@(df.wtk_odt_aux) * df.time_factor * ~df.outbound,,,,,,,,coef_xwalk,,,,,,, -util_KNRIN_LOC_Fare_and_operating_cost,KNRIN_LOC - Fare ,"@df.transitSubsidyPassDiscount*(odt_skims['fare'])*100*df.number_of_participants/(np.maximum(df.income,1000)**df.income_exponent) * ~df.outbound",,,,,,,,coef_income,,,,,,, +util_KNRIN_LOC_Fare_and_operating_cost,KNRIN_LOC - Fare ,"@df.transitSubsidyPassDiscount*df.get('TRANSIT_FF', 1)*(odt_skims['fare'])*100*df.number_of_participants/(np.maximum(df.income,1000)**df.income_exponent) * ~df.outbound",,,,,,,,coef_income,,,,,,, util_KNR_LOC - Female,KNR_LOC - Female,@(df.female),,,,,,,,coef_female_tran,,,,,,, util_KNR_LOC - Destination Employment Density,KNR_LOC - Destination Employment Density,dMGRAEmpDen,,,,,,,,coef_dEmpDen_dTran,,,,,,, #,BiketoTransit,,,,,,,,,,,,,,,, @@ -121,7 +121,7 @@ util_BIKE_TRANSIT_Walk_egress_time,BIKE_TRANSIT - Walk egress time,"@np.where(df util_BIKE_TRANSIT_wait_egress_time,BIKE_TRANSIT - Egress mt/nev wait time,"@np.where(df.nev_local_egress_available_out & df.outbound, nevWaitTime, np.where(df.microtransit_local_egress_available_out & df.outbound, microtransitWaitTime, 0))* df.time_factor",,,,,,,,,coef_wait,,,,,, util_BIKE_TRANSIT_transfer_walk_time,BIKE_TRANSIT - transfer walk time,@(odt_skims['WTW_AUX'])* df.time_factor,,,,,,,,,coef_xwalk,,,,,, util_BIKE_TRANSIT_transfers_penalty,BIKE_TRANSIT - number of transfers,"@(-23+23*np.exp(0.414*np.clip(odt_skims['WTW_XFR'] + df.outbound*df.mtnev_egr_xfer_out + ~df.outbound*df.mtnev_acc_xfer_in, a_min=None,a_max=4))) * df.time_factor",,,,,,,,,coef_xfer,,,,,, -util_BIKE_TRANSIT_Fare,BIKE_TRANSIT - Fare,"@df.transitSubsidyPassDiscount*(odt_skims['fare'])*100*df.number_of_participants/(np.maximum(df.income,1000)**df.income_exponent)",,,,,,,,,coef_income,,,,,, +util_BIKE_TRANSIT_Fare,BIKE_TRANSIT - Fare,"@df.transitSubsidyPassDiscount*df.get('TRANSIT_FF', 1)*(odt_skims['fare'])*100*df.number_of_participants/(np.maximum(df.income,1000)**df.income_exponent)",,,,,,,,,coef_income,,,,,, util_BIKE_TRANSIT - Female,BIKE_TRANSIT - Female,@(df.female),,,,,,,,,coef_female_tran,,,,,, util_BIKE_TRANSIT - Origin Mix,BIKE_TRANSIT - Origin Mix,oMGRAMix,,,,,,,,,coef_oMix_wTran,,,,,, util_BIKE_TRANSIT - Origin Intersection Density,BIKE_TRANSIT - Origin Intersection Density,oMGRATotInt,,,,,,,,,coef_oIntDen_wTran,,,,,, diff --git a/resident/preprocessor.py b/resident/preprocessor.py index e741828..4bfb5c9 100644 --- a/resident/preprocessor.py +++ b/resident/preprocessor.py @@ -28,7 +28,7 @@ class PreprocessorSettings: maz_stop_walk_file: str = None # Path to MAZ stop walk distances file fare_skim_input_file: str = None # Path to non-TOD segmented fare skim matrix file times_of_day: list[str] = field(default_factory=lambda: ["EA", "AM", "MD", "PM", "EV"]) - flat_fare_rate: float = None + transit_fare_system: dict = None maz_maz_walk_file: str = None nodes_file: str = None links_file: str = None @@ -214,6 +214,69 @@ def check_ids( return households, persons +def add_transit_fare_factor( + persons: pd.DataFrame, + settings: PreprocessorSettings, +) -> pd.DataFrame: + """Add person-level TRANSIT_FF using the Oregon transit fare rules.""" + if "TRANSIT_FF" in persons.columns: + print("TRANSIT_FF column already exists in persons, skipping fare factor calculation") + return persons + + fare_system = settings.transit_fare_system.get("name", "").upper() if settings.transit_fare_system else None + valid_fare_systems = ["LTD", "CHERRIOTS", "TRIMET", "RVTD", "GRANTS_PASS", "FREE"] + assert fare_system is None or fare_system in valid_fare_systems, f"Invalid transit_fare_system '{fare_system}'. Valid options are: {valid_fare_systems}" + + if fare_system is None: + print("No transit_fare_system configured or inferred. Defaulting TRANSIT_FF to 1.0.") + persons["TRANSIT_FF"] = 1.0 + return persons + + if fare_system == "FREE": + persons["TRANSIT_FF"] = 0.0 + print("Added TRANSIT_FF to persons for free-fare transit service") + return persons + + full_fare = settings.transit_fare_system["flat_fare_rate"] + + for column_name in ["age", "AGE", "AGEP"]: + if column_name in persons.columns: + age = pd.to_numeric(persons[column_name], errors="coerce") + break + else: + raise RuntimeError("persons table must include an age column (one of: age, AGE, AGEP)") + + is_k12_student = persons.SCHG.fillna(-1).between(1, 14, inclusive="both") + transit_ff = pd.Series(1.0, index=persons.index, dtype=float) + + if fare_system == "LTD": + transit_ff.loc[age <= 18] = 0.85 / full_fare + transit_ff.loc[(age <= 18) & is_k12_student] = 0.0 + transit_ff.loc[age <= 5] = 0.0 + transit_ff.loc[age >= 65] = 0.0 + elif fare_system == "CHERRIOTS": + transit_ff.loc[age <= 18] = 0.0 + transit_ff.loc[age >= 60] = 0.80 / full_fare + elif fare_system == "TRIMET": + transit_ff.loc[age.between(7, 17, inclusive="both")] = 1.40 / full_fare + transit_ff.loc[age <= 6] = 0.0 + transit_ff.loc[age >= 65] = 1.40 / full_fare + elif fare_system == "RVTD": + transit_ff.loc[age.between(10, 17, inclusive="both")] = 1.00 / full_fare + transit_ff.loc[age <= 9] = 1.00 / full_fare + transit_ff.loc[age >= 62] = 1.00 / full_fare + elif fare_system == "GRANTS_PASS": + transit_ff.loc[age.between(6, 16, inclusive="both")] = 0.50 / full_fare + transit_ff.loc[age <= 5] = 0.0 + transit_ff.loc[age >= 62] = 0.50 / full_fare + else: + raise RuntimeError(f"Unsupported transit_fare_system '{fare_system}'") + + persons["TRANSIT_FF"] = transit_ff.round(3) + print(f"Added TRANSIT_FF to persons using transit_fare_system='{fare_system}'") + return persons + + def add_tothhs(land_use: pd.DataFrame, households: pd.DataFrame) -> pd.DataFrame: """Add TOTHHS (total households per zone) to land_use if not present. Ensures TOTHHS values match the count of households in each MAZ, @@ -736,16 +799,20 @@ def create_flat_fare_skim(settings: PreprocessorSettings, land_use: pd.DataFrame omx with matrices fare__[time_of_day] """ print("Preprocessing flat-fare skim matrix.") - if settings.flat_fare_rate is None: + if settings.transit_fare_system["flat_fare_rate"] is None: print("No flat-fare rate provided, skipping flat-fare skim creation.") return + flat_fare_rate = settings.transit_fare_system["flat_fare_rate"] + assert flat_fare_rate >= 0, "Flat fare rate must be non-negative" + print(f"Using flat fare rate of ${flat_fare_rate} for skim matrix") + # Get TAZ IDs from land_use taz_ids = np.sort(land_use['TAZ'].unique()) n_zones = len(taz_ids) - # Create falt fare matrix - fare_matrix = np.full((n_zones, n_zones), settings.flat_fare_rate, dtype=np.float32) + # Create flat fare matrix + fare_matrix = np.full((n_zones, n_zones), flat_fare_rate, dtype=np.float32) output_fare_skim_file_name = settings.output_dir / "fares.omx" with omx.open_file(output_fare_skim_file_name, 'w') as flat_fare_skim: @@ -818,6 +885,9 @@ def preprocess(settings: PreprocessorSettings) -> tuple[pd.DataFrame, pd.DataFra # Fix duplicate household IDs, check for correct names households, persons = check_ids(households, persons) + + # Add person-level transit fare factor from fare policy rules + persons = add_transit_fare_factor(persons, settings) # Add TOTHHS and TOTPOP land_use = add_tothhs(land_use, households) @@ -846,10 +916,16 @@ def preprocess(settings: PreprocessorSettings) -> tuple[pd.DataFrame, pd.DataFra original_land_use, ) - # Preprocess fare skim matrix into TOD format if needed - preprocess_fare_skim(settings) - # Create flat-fare skim with TOD format if needed - create_flat_fare_skim(settings, land_use) + fare_skim_file = settings.fare_skim_input_file if hasattr(settings, 'fare_skim_input_file') else None + + if fare_skim_file is not None: + # Preprocess fare skim matrix into TOD format if needed + print(f"Preprocessing fare skim matrix from {fare_skim_file}") + preprocess_fare_skim(settings) + else: + print("No fare skim input file provided, creating flat-fare skim.") + # Create flat-fare skim with TOD format if needed + create_flat_fare_skim(settings, land_use) return households, persons, land_use diff --git a/resident/preprocessor_settings.yaml b/resident/preprocessor_settings.yaml index b9619bd..7508dfe 100644 --- a/resident/preprocessor_settings.yaml +++ b/resident/preprocessor_settings.yaml @@ -12,12 +12,23 @@ maz_maz_walk_file: C:\projects\odot_joint_estimation\pnr\SimOR\resident\model_d # Variable-fare transit skims # Non-TOD segmented transit fare skim file processed into fares.omx segmented by TOD with matrices fare__[time_of_day] +# If None, fare skims will be created with a flat fare rate for all time periods as defined below in the transit_fare_system section fare_skim_input_file: times_of_day: ["EA", "AM", "MD", "PM", "EV"] # Flat-fare transit skims -# Add value to create flat-rate fares.omx, leave empty if skim does not need to be created -flat_fare_rate: 1.60 +transit_fare_system: + # name: LTD + # flat_fare_rate: 1.75 + # name: CHERRIOTS + # flat_fare_rate: 1.60 + name: TRIMET + flat_fare_rate: 2.80 + # name: RVTD + # flat_fare_rate: 2.00 + # name: GRANTS_PASS + # flat_fare_rate: 1.00 + # Land use settings nodes_file: C:\projects\odot_joint_estimation\pnr\SimOR\resident\model_data\metro\data_full\raw\Network_node.shp From 6a5ae68e8ebd015f86182af4e39304e7d1726741 Mon Sep 17 00:00:00 2001 From: David Hensle <51132108+dhensle@users.noreply.github.com> Date: Tue, 19 May 2026 17:20:30 -0700 Subject: [PATCH 3/3] missed quotes --- resident/configs/tour_mode_choice.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resident/configs/tour_mode_choice.csv b/resident/configs/tour_mode_choice.csv index 3107c60..ab45549 100644 --- a/resident/configs/tour_mode_choice.csv +++ b/resident/configs/tour_mode_choice.csv @@ -116,7 +116,7 @@ util_KNR_wait_egress_time_(at_attraction_end),KNR_LOC - Egress mt/nev wait time util_KNR_Walk_other_time,KNR_LOC - Walk other time,@df.knr_other_walk_time*df.time_factor,,,,,,,,coef_xwalk,,,,,,, util_KNR_stop_type_constant,KNR_LOC - Stop type constant,@df.knr_stop_constant* df.time_factor,,,,,,,,coef_ivt,,,,,,, util_KNR_vehicle_type_constant,KNR_LOC - Vehicle type constant,@df.knr_veh_constant* df.time_factor,,,,,,,,coef_ivt,,,,,,, -util_KNR_Fare_and_operating_cost,KNR_LOC - Fare ,@df.transitSubsidyPassDiscount*df.get('TRANSIT_FF', 1)*(odt_skims['fare'] + dot_skims['fare'])*100*df.num_participants/df.cost_sensitivity,,,,,,,,coef_income,,,,,,, +util_KNR_Fare_and_operating_cost,KNR_LOC - Fare ,"@df.transitSubsidyPassDiscount*df.get('TRANSIT_FF', 1)*(odt_skims['fare'] + dot_skims['fare'])*100*df.num_participants/df.cost_sensitivity",,,,,,,,coef_income,,,,,,, util_KNR_LOC - Age 16 to 24,KNR_LOC - Age 16 to 24,@(df.age > 15) & (df.age < 25),,,,,,,,coef_age1624_tran,,,,,,, util_KNR_LOC - Age 41 to 55,KNR_LOC - Age 41 to 55,@(df.age > 40) & (df.age < 56),,,,,,,,coef_age4155_tran,,,,,,, util_KNR_LOC - Age 56 to 64,KNR_LOC - Age 56 to 64,@(df.age > 55) & (df.age < 65),,,,,,,,coef_age5664_tran,,,,,,,