|
| 1 | +# ActivitySim |
| 2 | +# See full license in LICENSE.txt. |
| 3 | + |
| 4 | +import logging |
| 5 | + |
| 6 | +import pandas as pd |
| 7 | +import numpy as np |
| 8 | +import itertools |
| 9 | +import os |
| 10 | + |
| 11 | +from activitysim.core.interaction_simulate import interaction_simulate |
| 12 | +from activitysim.core import simulate |
| 13 | +from activitysim.core import tracing |
| 14 | +from activitysim.core import config |
| 15 | +from activitysim.core import inject |
| 16 | +from activitysim.core import pipeline |
| 17 | +from activitysim.core import expressions |
| 18 | +from activitysim.core import logit |
| 19 | +from activitysim.core import assign |
| 20 | +from activitysim.core import los |
| 21 | + |
| 22 | +from activitysim.core.util import assign_in_place |
| 23 | + |
| 24 | +from .util.mode import mode_choice_simulate |
| 25 | +from .util import estimation |
| 26 | + |
| 27 | +logger = logging.getLogger(__name__) |
| 28 | + |
| 29 | + |
| 30 | +def annotate_vehicle_allocation(model_settings, trace_label): |
| 31 | + """ |
| 32 | + Add columns to the tours table in the pipeline according to spec. |
| 33 | +
|
| 34 | + Parameters |
| 35 | + ---------- |
| 36 | + model_settings : dict |
| 37 | + trace_label : str |
| 38 | + """ |
| 39 | + tours = inject.get_table('tours').to_frame() |
| 40 | + expressions.assign_columns( |
| 41 | + df=tours, |
| 42 | + model_settings=model_settings.get('annotate_tours'), |
| 43 | + trace_label=tracing.extend_trace_label(trace_label, 'annotate_tours')) |
| 44 | + pipeline.replace_table("tours", tours) |
| 45 | + |
| 46 | + |
| 47 | +def get_skim_dict(network_los, choosers): |
| 48 | + """ |
| 49 | + Returns a dictionary of skim wrappers to use in expression writing. |
| 50 | +
|
| 51 | + Skims have origin as home_zone_id and destination as the tour destination. |
| 52 | +
|
| 53 | + Parameters |
| 54 | + ---------- |
| 55 | + network_los : activitysim.core.los.Network_LOS object |
| 56 | + choosers : pd.DataFrame |
| 57 | +
|
| 58 | + Returns |
| 59 | + ------- |
| 60 | + skims : dict |
| 61 | + index is skim wrapper name, value is the skim wrapper |
| 62 | + """ |
| 63 | + skim_dict = network_los.get_default_skim_dict() |
| 64 | + orig_col_name = 'home_zone_id' |
| 65 | + dest_col_name = 'destination' |
| 66 | + |
| 67 | + out_time_col_name = 'start' |
| 68 | + in_time_col_name = 'end' |
| 69 | + odt_skim_stack_wrapper = skim_dict.wrap_3d(orig_key=orig_col_name, dest_key=dest_col_name, |
| 70 | + dim3_key='out_period') |
| 71 | + dot_skim_stack_wrapper = skim_dict.wrap_3d(orig_key=dest_col_name, dest_key=orig_col_name, |
| 72 | + dim3_key='in_period') |
| 73 | + |
| 74 | + choosers['in_period'] = network_los.skim_time_period_label(choosers[in_time_col_name]) |
| 75 | + choosers['out_period'] = network_los.skim_time_period_label(choosers[out_time_col_name]) |
| 76 | + |
| 77 | + skims = { |
| 78 | + "odt_skims": odt_skim_stack_wrapper.set_df(choosers), |
| 79 | + "dot_skims": dot_skim_stack_wrapper.set_df(choosers), |
| 80 | + } |
| 81 | + return skims |
| 82 | + |
| 83 | + |
| 84 | +@inject.step() |
| 85 | +def vehicle_allocation( |
| 86 | + persons, |
| 87 | + households, |
| 88 | + vehicles, |
| 89 | + tours, |
| 90 | + tours_merged, |
| 91 | + network_los, |
| 92 | + chunk_size, |
| 93 | + trace_hh_id): |
| 94 | + """Selects a vehicle for each occupancy level for each tour. |
| 95 | +
|
| 96 | + Alternatives consist of the up to the number of household vehicles plus one |
| 97 | + option for non-household vehicles. |
| 98 | +
|
| 99 | + The model will be run once for each tour occupancy defined in the model yaml. |
| 100 | + Output tour table will columns added for each occupancy level. |
| 101 | +
|
| 102 | + The user may also augment the `tours` tables with new vehicle |
| 103 | + type-based fields specified via the annotate_tours option. |
| 104 | +
|
| 105 | + Parameters |
| 106 | + ---------- |
| 107 | + persons : orca.DataFrameWrapper |
| 108 | + households : orca.DataFrameWrapper |
| 109 | + vehicles : orca.DataFrameWrapper |
| 110 | + vehicles_merged : orca.DataFrameWrapper |
| 111 | + tours : orca.DataFrameWrapper |
| 112 | + tours_merged : orca.DataFrameWrapper |
| 113 | + chunk_size : orca.injectable |
| 114 | + trace_hh_id : orca.injectable |
| 115 | + """ |
| 116 | + trace_label = 'vehicle_allocation' |
| 117 | + model_settings_file_name = 'vehicle_allocation.yaml' |
| 118 | + model_settings = config.read_model_settings(model_settings_file_name) |
| 119 | + |
| 120 | + logsum_column_name = model_settings.get('MODE_CHOICE_LOGSUM_COLUMN_NAME') |
| 121 | + |
| 122 | + estimator = estimation.manager.begin_estimation('vehicle_allocation') |
| 123 | + |
| 124 | + model_spec_raw = simulate.read_model_spec(file_name=model_settings['SPEC']) |
| 125 | + coefficients_df = simulate.read_model_coefficients(model_settings) |
| 126 | + model_spec = simulate.eval_coefficients(model_spec_raw, coefficients_df, estimator) |
| 127 | + |
| 128 | + nest_spec = config.get_logit_model_settings(model_settings) |
| 129 | + constants = config.get_model_constants(model_settings) |
| 130 | + |
| 131 | + locals_dict = {} |
| 132 | + locals_dict.update(constants) |
| 133 | + locals_dict.update(coefficients_df) |
| 134 | + |
| 135 | + # ------ constructing alternatives from model spec and joining to choosers |
| 136 | + vehicles_wide = vehicles.to_frame().pivot_table( |
| 137 | + index='household_id', columns='vehicle_num', |
| 138 | + values='vehicle_type', aggfunc=lambda x: ''.join(x)) |
| 139 | + |
| 140 | + alts_from_spec = model_spec.columns |
| 141 | + # renaming vehicle numbers to alternative names in spec |
| 142 | + vehicle_alt_columns_dict = {} |
| 143 | + for veh_num in range(1, len(alts_from_spec)): |
| 144 | + vehicle_alt_columns_dict[veh_num] = alts_from_spec[veh_num-1] |
| 145 | + vehicles_wide.rename(columns=vehicle_alt_columns_dict, inplace=True) |
| 146 | + |
| 147 | + # if the number of vehicles is less than the alternatives, fill with NA |
| 148 | + # e.g. all households only have 1 or 2 vehicles because of small sample size, |
| 149 | + # still need columns for alternatives 3 and 4 |
| 150 | + for veh_num, col_name in vehicle_alt_columns_dict.items(): |
| 151 | + if col_name not in vehicles_wide.columns: |
| 152 | + vehicles_wide[col_name] = '' |
| 153 | + |
| 154 | + # last entry in spec is the non-hh-veh option |
| 155 | + assert alts_from_spec[-1] == 'non_hh_veh', "Last option in spec needs to be non_hh_veh" |
| 156 | + vehicles_wide[alts_from_spec[-1]] = '' |
| 157 | + |
| 158 | + # merging vehicle alternatives to choosers |
| 159 | + choosers = tours_merged.to_frame().reset_index() |
| 160 | + choosers = pd.merge(choosers, vehicles_wide, how='left', on='household_id') |
| 161 | + choosers.set_index('tour_id', inplace=True) |
| 162 | + |
| 163 | + # ----- setup skim keys |
| 164 | + skims = get_skim_dict(network_los, choosers) |
| 165 | + locals_dict.update(skims) |
| 166 | + |
| 167 | + # ------ preprocessor |
| 168 | + preprocessor_settings = model_settings.get('preprocessor', None) |
| 169 | + if preprocessor_settings: |
| 170 | + expressions.assign_columns( |
| 171 | + df=choosers, |
| 172 | + model_settings=preprocessor_settings, |
| 173 | + locals_dict=locals_dict, |
| 174 | + trace_label=trace_label) |
| 175 | + |
| 176 | + logger.info("Running %s with %d tours", trace_label, len(choosers)) |
| 177 | + |
| 178 | + if estimator: |
| 179 | + estimator.write_model_settings(model_settings, model_settings_file_name) |
| 180 | + estimator.write_spec(model_settings) |
| 181 | + estimator.write_coefficients(coefficients_df, model_settings) |
| 182 | + estimator.write_choosers(choosers) |
| 183 | + |
| 184 | + tours = tours.to_frame() |
| 185 | + |
| 186 | + # ------ running for each occupancy level selected |
| 187 | + tours_veh_occup_cols = [] |
| 188 | + for occup in model_settings.get('OCCUPANCY_LEVELS', [1]): |
| 189 | + logger.info("Running for occupancy = %d", occup) |
| 190 | + # setting occup for access in spec expressions |
| 191 | + locals_dict.update({'occup': occup}) |
| 192 | + |
| 193 | + choices = simulate.simple_simulate( |
| 194 | + choosers=choosers, |
| 195 | + spec=model_spec, |
| 196 | + nest_spec=nest_spec, |
| 197 | + skims=skims, |
| 198 | + locals_d=locals_dict, |
| 199 | + chunk_size=chunk_size, |
| 200 | + trace_label=trace_label, |
| 201 | + trace_choice_name='vehicle_allocation', |
| 202 | + estimator=estimator) |
| 203 | + |
| 204 | + # matching alt names to choices |
| 205 | + choices = choices.map(dict(enumerate(alts_from_spec))).to_frame() |
| 206 | + choices.columns = ['alt_choice'] |
| 207 | + |
| 208 | + # last alternative is the non-household vehicle option |
| 209 | + for alt in alts_from_spec[:-1]: |
| 210 | + choices.loc[choices['alt_choice'] == alt, 'choice'] = \ |
| 211 | + choosers.loc[choices['alt_choice'] == alt, alt] |
| 212 | + choices.loc[choices['alt_choice'] == alts_from_spec[-1], 'choice'] = alts_from_spec[-1] |
| 213 | + |
| 214 | + # creating a column for choice of each occupancy level |
| 215 | + tours_veh_occup_col = f'vehicle_occup_{occup}' |
| 216 | + tours[tours_veh_occup_col] = choices['choice'] |
| 217 | + tours_veh_occup_cols.append(tours_veh_occup_col) |
| 218 | + |
| 219 | + if estimator: |
| 220 | + estimator.write_choices(choices) |
| 221 | + choices = estimator.get_survey_values(choices, 'households', 'vehicle_allocation') |
| 222 | + estimator.write_override_choices(choices) |
| 223 | + estimator.end_estimation() |
| 224 | + |
| 225 | + pipeline.replace_table("tours", tours) |
| 226 | + |
| 227 | + tracing.print_summary('vehicle_allocation', tours[tours_veh_occup_cols], value_counts=True) |
| 228 | + |
| 229 | + annotate_settings = model_settings.get('annotate_tours', None) |
| 230 | + if annotate_settings: |
| 231 | + annotate_vehicle_allocation(model_settings, trace_label) |
| 232 | + |
| 233 | + if trace_hh_id: |
| 234 | + tracing.trace_df(tours, |
| 235 | + label='vehicle_allocation', |
| 236 | + warn_if_empty=True) |
0 commit comments