diff --git a/activitysim/abm/models/__init__.py b/activitysim/abm/models/__init__.py index a45c1ff76..005496d4d 100644 --- a/activitysim/abm/models/__init__.py +++ b/activitysim/abm/models/__init__.py @@ -22,6 +22,7 @@ from . import non_mandatory_destination from . import non_mandatory_scheduling from . import non_mandatory_tour_frequency +from . import parking_location_choice from . import stop_frequency from . import tour_mode_choice from . import trip_destination @@ -29,5 +30,7 @@ from . import trip_purpose from . import trip_purpose_and_destination from . import trip_scheduling +from . import trip_departure_choice +from . import trip_scheduling_choice from . import trip_matrices from . import summarize diff --git a/activitysim/abm/models/parking_location_choice.py b/activitysim/abm/models/parking_location_choice.py new file mode 100644 index 000000000..42bc8bb14 --- /dev/null +++ b/activitysim/abm/models/parking_location_choice.py @@ -0,0 +1,311 @@ +# ActivitySim +# See full license in LICENSE.txt. +import logging + +import numpy as np +import pandas as pd + +from activitysim.core import config +from activitysim.core import inject +from activitysim.core import pipeline +from activitysim.core import simulate +from activitysim.core import tracing + +from activitysim.core import expressions +from activitysim.core.interaction_sample_simulate import interaction_sample_simulate +from activitysim.core.logit import interaction_dataset +from activitysim.core.util import assign_in_place +from activitysim.core.tracing import print_elapsed_time + +from .util import estimation + + +logger = logging.getLogger(__name__) + +NO_DESTINATION = -1 + + +def wrap_skims(model_settings): + """ + wrap skims of trip destination using origin, dest column names from model settings. + Various of these are used by destination_sample, compute_logsums, and destination_simulate + so we create them all here with canonical names. + + Note that compute_logsums aliases their names so it can use the same equations to compute + logsums from origin to alt_dest, and from alt_dest to primarly destination + + odt_skims - SkimStackWrapper: trip origin, trip alt_dest, time_of_day + dot_skims - SkimStackWrapper: trip alt_dest, trip origin, time_of_day + dpt_skims - SkimStackWrapper: trip alt_dest, trip primary_dest, time_of_day + pdt_skims - SkimStackWrapper: trip primary_dest,trip alt_dest, time_of_day + od_skims - SkimDictWrapper: trip origin, trip alt_dest + dp_skims - SkimDictWrapper: trip alt_dest, trip primary_dest + + Parameters + ---------- + model_settings + + Returns + ------- + dict containing skims, keyed by canonical names relative to tour orientation + """ + + network_los = inject.get_injectable('network_los') + skim_dict = network_los.get_default_skim_dict() + + origin = model_settings['TRIP_ORIGIN'] + park_zone = model_settings['ALT_DEST_COL_NAME'] + destination = model_settings['TRIP_DESTINATION'] + time_period = model_settings['TRIP_DEPARTURE_PERIOD'] + + skims = { + "odt_skims": skim_dict.wrap_3d(orig_key=origin, dest_key=destination, dim3_key=time_period), + "dot_skims": skim_dict.wrap_3d(orig_key=destination, dest_key=origin, dim3_key=time_period), + "opt_skims": skim_dict.wrap_3d(orig_key=origin, dest_key=park_zone, dim3_key=time_period), + "pdt_skims": skim_dict.wrap_3d(orig_key=park_zone, dest_key=destination, dim3_key=time_period), + "od_skims": skim_dict.wrap(origin, destination), + "do_skims": skim_dict.wrap(destination, origin), + "op_skims": skim_dict.wrap(origin, park_zone), + "pd_skims": skim_dict.wrap(park_zone, destination), + } + + return skims + + +def get_spec_for_segment(model_settings, spec_name, segment): + + omnibus_spec = simulate.read_model_spec(file_name=model_settings[spec_name]) + + spec = omnibus_spec[[segment]] + + # might as well ignore any spec rows with 0 utility + spec = spec[spec.iloc[:, 0] != 0] + assert spec.shape[0] > 0 + + return spec + + +def parking_destination_simulate( + segment_name, + trips, + destination_sample, + model_settings, + skims, + chunk_size, trace_hh_id, + trace_label): + """ + Chose destination from destination_sample (with od_logsum and dp_logsum columns added) + + + Returns + ------- + choices - pandas.Series + destination alt chosen + """ + trace_label = tracing.extend_trace_label(trace_label, 'trip_destination_simulate') + + spec = get_spec_for_segment(model_settings, 'SPECIFICATION', segment_name) + + alt_dest_col_name = model_settings['ALT_DEST_COL_NAME'] + + logger.info("Running trip_destination_simulate with %d trips", len(trips)) + + locals_dict = config.get_model_constants(model_settings).copy() + locals_dict.update(skims) + + parking_locations = interaction_sample_simulate( + choosers=trips, + alternatives=destination_sample, + spec=spec, + choice_column=alt_dest_col_name, + want_logsums=False, + allow_zero_probs=True, zero_prob_choice_val=NO_DESTINATION, + skims=skims, + locals_d=locals_dict, + chunk_size=chunk_size, + trace_label=trace_label, + trace_choice_name='parking_loc') + + # drop any failed zero_prob destinations + if (parking_locations == NO_DESTINATION).any(): + logger.debug("dropping %s failed parking locations", (parking_locations == NO_DESTINATION).sum()) + parking_locations = parking_locations[parking_locations != NO_DESTINATION] + + return parking_locations + + +def choose_parking_location( + segment_name, + trips, + alternatives, + model_settings, + want_sample_table, + skims, + chunk_size, trace_hh_id, + trace_label): + + logger.info("choose_parking_location %s with %d trips", trace_label, trips.shape[0]) + + t0 = print_elapsed_time() + + alt_dest_col_name = model_settings['ALT_DEST_COL_NAME'] + destination_sample = interaction_dataset(trips, alternatives, alt_index_id=alt_dest_col_name) + destination_sample.index = np.repeat(trips.index.values, len(alternatives)) + destination_sample.index.name = trips.index.name + destination_sample = destination_sample[[alt_dest_col_name]].copy() + + # # - trip_destination_simulate + destinations = parking_destination_simulate( + segment_name=segment_name, + trips=trips, + destination_sample=destination_sample, + model_settings=model_settings, + skims=skims, + chunk_size=chunk_size, trace_hh_id=trace_hh_id, + trace_label=trace_label) + + if want_sample_table: + # FIXME - sample_table + destination_sample.set_index(model_settings['ALT_DEST_COL_NAME'], append=True, inplace=True) + else: + destination_sample = None + + t0 = print_elapsed_time("%s.parking_location_simulate" % trace_label, t0) + + return destinations, destination_sample + + +def run_parking_destination( + model_settings, + trips, land_use, + chunk_size, trace_hh_id, + trace_label, + fail_some_trips_for_testing=False): + + chooser_filter_column = model_settings.get('CHOOSER_FILTER_COLUMN_NAME') + chooser_segment_column = model_settings.get('CHOOSER_SEGMENT_COLUMN_NAME') + + parking_location_column_name = model_settings['ALT_DEST_COL_NAME'] + sample_table_name = model_settings.get('DEST_CHOICE_SAMPLE_TABLE_NAME') + want_sample_table = config.setting('want_dest_choice_sample_tables') and sample_table_name is not None + + choosers = trips[trips[chooser_filter_column]] + choosers = choosers.sort_index() + + # Placeholder for trips without a parking choice + trips[parking_location_column_name] = -1 + + skims = wrap_skims(model_settings) + + alt_column_filter_name = model_settings.get('ALTERNATIVE_FILTER_COLUMN_NAME') + alternatives = land_use[land_use[alt_column_filter_name]] + + # don't need size terms in alternatives, just TAZ index + alternatives = alternatives.drop(alternatives.columns, axis=1) + alternatives.index.name = parking_location_column_name + + choices_list = [] + sample_list = [] + for segment_name, chooser_segment in choosers.groupby(chooser_segment_column): + if chooser_segment.shape[0] == 0: + logger.info("%s skipping segment %s: no choosers", trace_label, segment_name) + continue + + choices, destination_sample = choose_parking_location( + segment_name, + chooser_segment, + alternatives, + model_settings, + want_sample_table, + skims, + chunk_size, trace_hh_id, + trace_label=tracing.extend_trace_label(trace_label, segment_name)) + + choices_list.append(choices) + if want_sample_table: + assert destination_sample is not None + sample_list.append(destination_sample) + + if len(choices_list) > 0: + parking_df = pd.concat(choices_list) + + if fail_some_trips_for_testing: + parking_df = parking_df.drop(parking_df.index[0]) + + assign_in_place(trips, parking_df.to_frame(parking_location_column_name)) + trips[parking_location_column_name] = trips[parking_location_column_name].fillna(-1) + else: + trips[parking_location_column_name] = -1 + + save_sample_df = pd.concat(sample_list) if len(sample_list) > 0 else None + + return trips[parking_location_column_name], save_sample_df + + +@inject.step() +def parking_location( + trips, + trips_merged, + land_use, + network_los, + chunk_size, + trace_hh_id): + """ + Given a set of trips, each trip needs to have a parking location if + it is eligible for remote parking. + """ + + trace_label = 'parking_location' + model_settings = config.read_model_settings('parking_location_choice.yaml') + alt_destination_col_name = model_settings['ALT_DEST_COL_NAME'] + + preprocessor_settings = model_settings.get('PREPROCESSOR', None) + + trips_df = trips.to_frame() + trips_merged_df = trips_merged.to_frame() + land_use_df = land_use.to_frame() + + locals_dict = { + 'network_los': network_los + } + locals_dict.update(config.get_model_constants(model_settings)) + + if preprocessor_settings: + expressions.assign_columns( + df=trips_merged_df, + model_settings=preprocessor_settings, + locals_dict=locals_dict, + trace_label=trace_label) + + parking_locations, save_sample_df = run_parking_destination( + model_settings, + trips_merged_df, land_use_df, + chunk_size=chunk_size, + trace_hh_id=trace_hh_id, + trace_label=trace_label, + ) + + assign_in_place(trips_df, parking_locations.to_frame(alt_destination_col_name)) + + pipeline.replace_table("trips", trips_df) + + if trace_hh_id: + tracing.trace_df(trips_df, + label=trace_label, + slicer='trip_id', + index_label='trip_id', + warn_if_empty=True) + + if save_sample_df is not None: + assert len(save_sample_df.index.get_level_values(0).unique()) == \ + len(trips_df[trips_df.trip_num < trips_df.trip_count]) + + sample_table_name = model_settings.get('PARKING_LOCATION_SAMPLE_TABLE_NAME') + assert sample_table_name is not None + + logger.info("adding %s samples to %s" % (len(save_sample_df), sample_table_name)) + + # lest they try to put tour samples into the same table + if pipeline.is_table(sample_table_name): + raise RuntimeError("sample table %s already exists" % sample_table_name) + pipeline.extend_table(sample_table_name, save_sample_df) diff --git a/activitysim/abm/models/trip_departure_choice.py b/activitysim/abm/models/trip_departure_choice.py new file mode 100644 index 000000000..3fe50fb99 --- /dev/null +++ b/activitysim/abm/models/trip_departure_choice.py @@ -0,0 +1,461 @@ +import logging + +import numpy as np +import pandas as pd + +from activitysim.core import chunk +from activitysim.core import config +from activitysim.core import expressions +from activitysim.core import inject +from activitysim.core import logit +from activitysim.core import pipeline +from activitysim.core import simulate +from activitysim.core import tracing + +from activitysim.abm.models.util.trip import get_time_windows +from activitysim.core.interaction_sample_simulate import eval_interaction_utilities +from activitysim.core.simulate import set_skim_wrapper_targets +from activitysim.core.util import reindex + + +logger = logging.getLogger(__name__) + +MAIN_LEG_DURATION = 'main_leg_duration' +IB_DURATION = 'inbound_duration' +OB_DURATION = 'outbound_duration' + +TOUR_ID = 'tour_id' +TRIP_ID = 'trip_id' +TOUR_LEG_ID = 'tour_leg_id' +PATTERN_ID = 'pattern_id' +TRIP_DURATION = 'trip_duration' +STOP_TIME_DURATION = 'stop_time_duration' +TRIP_NUM = 'trip_num' +TRIP_COUNT = 'trip_count' +OUTBOUND = 'outbound' + +MAX_TOUR_ID = int(1e9) + + +def generate_tour_leg_id(tour_leg_row): + return tour_leg_row.tour_id + (int(MAX_TOUR_ID) if tour_leg_row.outbound else int(2 * MAX_TOUR_ID)) + + +def get_tour_legs(trips): + tour_legs = trips.groupby([TOUR_ID, OUTBOUND], as_index=False)[TRIP_NUM].max() + tour_legs[TOUR_LEG_ID] = tour_legs.apply(generate_tour_leg_id, axis=1) + tour_legs = tour_legs.set_index(TOUR_LEG_ID) + return tour_legs + + +def trip_departure_rpc(chunk_size, choosers, trace_label): + + # NOTE we chunk chunk_id + num_choosers = choosers['chunk_id'].max() + 1 + + chooser_row_size = choosers.shape[1] + 1 + + # scale row_size by average number of chooser rows per chunk_id + rows_per_chunk_id = choosers.shape[0] / num_choosers + row_size = (rows_per_chunk_id * chooser_row_size) + + return chunk.rows_per_chunk(chunk_size, row_size, num_choosers, trace_label) + + +def generate_alternatives(trips, alternative_col_name): + """ + This method creates an alternatives list of all possible + trip durations less than the total trip leg duration. If + the trip only has one trip on the leg, the trip alternative + only has one alternative for that trip equal to the trip + duration. + :param trips: pd.DataFrame + :param alternative_col_name: column name for the alternative column + :return: pd.DataFrame + """ + legs = trips[trips[TRIP_COUNT] > 1] + + leg_alts = None + durations = np.where(legs[OUTBOUND], legs[OB_DURATION], legs[IB_DURATION]) + if len(durations) > 0: + leg_alts = pd.Series(np.concatenate([np.arange(0, duration + 1) for duration in durations]), + np.repeat(legs.index, durations + 1), + name=alternative_col_name).to_frame() + + single_trips = trips[trips[TRIP_COUNT] == 1] + single_alts = None + durations = np.where(single_trips[OUTBOUND], single_trips[OB_DURATION], single_trips[IB_DURATION]) + if len(durations) > 0: + single_alts = pd.Series(durations, single_trips.index, + name=alternative_col_name).to_frame() + + if not legs.empty and not single_trips.empty: + return pd.concat([leg_alts, single_alts]) + + return leg_alts if not legs.empty else single_alts + + +def build_patterns(trips, time_windows): + tours = trips.groupby([TOUR_ID])[[TRIP_DURATION, TRIP_COUNT]].first() + duration_and_counts = tours[[TRIP_DURATION, TRIP_COUNT]].values + + # We subtract 1 here, because we already know + # the one trip of the tour leg based on main tour + # leg duration + max_trip_count = trips[TRIP_COUNT].max() - 1 + + patterns = [] + pattern_sizes = [] + + for duration, trip_count in duration_and_counts: + possible_windows = time_windows[:trip_count-1, np.where(time_windows[:trip_count-1].sum(axis=0) == duration)[0]] + possible_windows = np.unique(possible_windows, axis=1).transpose() + filler = np.full((possible_windows.shape[0], max_trip_count), np.nan) + filler[:possible_windows.shape[0], :possible_windows.shape[1]] = possible_windows + patterns.append(filler) + pattern_sizes.append(filler.shape[0]) + + patterns = np.concatenate(patterns) + pattern_names = ['_'.join('%0.0f' % x for x in y[~np.isnan(y)]) for y in patterns] + indexes = np.repeat(tours.index, pattern_sizes) + + # If we've done everything right, the indexes + # calculated above should be the same length as + # the pattern options + assert patterns.shape[0] == len(indexes) + + patterns = pd.DataFrame(index=indexes, data=patterns) + patterns.index.name = tours.index.name + patterns[PATTERN_ID] = pattern_names + + patterns = patterns.melt(id_vars=PATTERN_ID, value_name=STOP_TIME_DURATION, + var_name=TRIP_NUM, ignore_index=False).reset_index() + patterns = patterns[~patterns[STOP_TIME_DURATION].isnull()].copy() + + patterns[TRIP_NUM] = patterns[TRIP_NUM] + 1 + patterns[STOP_TIME_DURATION] = patterns[STOP_TIME_DURATION].astype(np.int) + + patterns = pd.merge(patterns, trips.reset_index()[[TOUR_ID, TRIP_ID, TRIP_NUM, OUTBOUND]], + on=[TOUR_ID, TRIP_NUM]) + + patterns.index = patterns.apply(generate_tour_leg_id, axis=1) + patterns.index.name = TOUR_LEG_ID + + return patterns + + +def get_spec_for_segment(omnibus_spec, segment): + + spec = omnibus_spec[[segment]] + + # might as well ignore any spec rows with 0 utility + spec = spec[spec.iloc[:, 0] != 0] + assert spec.shape[0] > 0 + + return spec + + +def trip_departure_calc_row_size(choosers, trace_label): + """ + rows_per_chunk calculator for trip_scheduler + """ + + sizer = chunk.RowSizeEstimator(trace_label) + + chooser_row_size = len(choosers.columns) + spec_columns = 3 + + sizer.add_elements(chooser_row_size + spec_columns, 'choosers') + + row_size = sizer.get_hwm() + return row_size + + +def choose_tour_leg_pattern(trip_segment, + patterns, spec, + trace_label='trace_label'): + alternatives = generate_alternatives(trip_segment, STOP_TIME_DURATION).sort_index() + have_trace_targets = tracing.has_trace_targets(trip_segment) + + if have_trace_targets: + tracing.trace_df(trip_segment, tracing.extend_trace_label(trace_label, 'choosers')) + tracing.trace_df(alternatives, tracing.extend_trace_label(trace_label, 'alternatives'), + transpose=False) + + if len(spec.columns) > 1: + raise RuntimeError('spec must have only one column') + + # - join choosers and alts + # in vanilla interaction_simulate interaction_df is cross join of choosers and alternatives + # interaction_df = logit.interaction_dataset(choosers, alternatives, sample_size) + # here, alternatives is sparsely repeated once for each (non-dup) sample + # we expect alternatives to have same index of choosers (but with duplicate index values) + # so we just need to left join alternatives with choosers + assert alternatives.index.name == trip_segment.index.name + + interaction_df = alternatives.join(trip_segment, how='left', rsuffix='_chooser') + + chunk.log_df(trace_label, 'interaction_df', interaction_df) + + if have_trace_targets: + trace_rows, trace_ids = tracing.interaction_trace_rows(interaction_df, trip_segment) + + tracing.trace_df(interaction_df, + tracing.extend_trace_label(trace_label, 'interaction_df'), + transpose=False) + else: + trace_rows = trace_ids = None + + interaction_utilities, trace_eval_results \ + = eval_interaction_utilities(spec, interaction_df, None, trace_label, trace_rows, None) + + interaction_utilities = pd.concat([interaction_df[STOP_TIME_DURATION], interaction_utilities], axis=1) + chunk.log_df(trace_label, 'interaction_utilities', interaction_utilities) + + interaction_utilities = pd.merge(interaction_utilities.reset_index(), + patterns[patterns[TRIP_ID].isin(interaction_utilities.index)], + on=[TRIP_ID, STOP_TIME_DURATION], how='left') + + if have_trace_targets: + tracing.trace_interaction_eval_results(trace_eval_results, trace_ids, + tracing.extend_trace_label(trace_label, 'eval')) + + tracing.trace_df(interaction_utilities, + tracing.extend_trace_label(trace_label, 'interaction_utilities'), + transpose=False) + + del interaction_df + chunk.log_df(trace_label, 'interaction_df', None) + + interaction_utilities = interaction_utilities.groupby([TOUR_ID, OUTBOUND, PATTERN_ID], + as_index=False)[['utility']].sum() + + interaction_utilities[TOUR_LEG_ID] = \ + interaction_utilities.apply(generate_tour_leg_id, axis=1) + + tour_choosers = interaction_utilities.set_index(TOUR_LEG_ID) + interaction_utilities = tour_choosers[['utility']].copy() + + # reshape utilities (one utility column and one row per row in model_design) + # to a dataframe with one row per chooser and one column per alternative + # interaction_utilities is sparse because duplicate sampled alternatives were dropped + # so we need to pad with dummy utilities so low that they are never chosen + + # number of samples per chooser + sample_counts = interaction_utilities.groupby(interaction_utilities.index).size().values + chunk.log_df(trace_label, 'sample_counts', sample_counts) + + # max number of alternatvies for any chooser + max_sample_count = sample_counts.max() + + # offsets of the first and last rows of each chooser in sparse interaction_utilities + last_row_offsets = sample_counts.cumsum() + first_row_offsets = np.insert(last_row_offsets[:-1], 0, 0) + + # repeat the row offsets once for each dummy utility to insert + # (we want to insert dummy utilities at the END of the list of alternative utilities) + # inserts is a list of the indices at which we want to do the insertions + inserts = np.repeat(last_row_offsets, max_sample_count - sample_counts) + + del sample_counts + chunk.log_df(trace_label, 'sample_counts', None) + + # insert the zero-prob utilities to pad each alternative set to same size + padded_utilities = np.insert(interaction_utilities.utility.values, inserts, -999) + del inserts + + del interaction_utilities + chunk.log_df(trace_label, 'interaction_utilities', None) + + # reshape to array with one row per chooser, one column per alternative + padded_utilities = padded_utilities.reshape(-1, max_sample_count) + chunk.log_df(trace_label, 'padded_utilities', padded_utilities) + + # convert to a dataframe with one row per chooser and one column per alternative + utilities_df = pd.DataFrame( + padded_utilities, + index=tour_choosers.index.unique()) + chunk.log_df(trace_label, 'utilities_df', utilities_df) + + del padded_utilities + chunk.log_df(trace_label, 'padded_utilities', None) + + if have_trace_targets: + tracing.trace_df(utilities_df, tracing.extend_trace_label(trace_label, 'utilities'), + column_labels=['alternative', 'utility']) + + # convert to probabilities (utilities exponentiated and normalized to probs) + # probs is same shape as utilities, one row per chooser and one column for alternative + probs = logit.utils_to_probs(utilities_df, + trace_label=trace_label, trace_choosers=trip_segment) + + chunk.log_df(trace_label, 'probs', probs) + + del utilities_df + chunk.log_df(trace_label, 'utilities_df', None) + + if have_trace_targets: + tracing.trace_df(probs, tracing.extend_trace_label(trace_label, 'probs'), + column_labels=['alternative', 'probability']) + + # make choices + # positions is series with the chosen alternative represented as a column index in probs + # which is an integer between zero and num alternatives in the alternative sample + positions, rands = \ + logit.make_choices(probs, trace_label=trace_label, trace_choosers=trip_segment) + + chunk.log_df(trace_label, 'positions', positions) + chunk.log_df(trace_label, 'rands', rands) + + del probs + chunk.log_df(trace_label, 'probs', None) + + # shouldn't have chosen any of the dummy pad utilities + assert positions.max() < max_sample_count + + # need to get from an integer offset into the alternative sample to the alternative index + # that is, we want the index value of the row that is offset by rows into the + # tranche of this choosers alternatives created by cross join of alternatives and choosers + + # resulting pandas Int64Index has one element per chooser row and is in same order as choosers + choices = tour_choosers[PATTERN_ID].take(positions + first_row_offsets) + + chunk.log_df(trace_label, 'choices', choices) + + if have_trace_targets: + tracing.trace_df(choices, tracing.extend_trace_label(trace_label, 'choices'), + columns=[None, PATTERN_ID]) + tracing.trace_df(rands, tracing.extend_trace_label(trace_label, 'rands'), + columns=[None, 'rand']) + + return choices + + +def apply_stage_two_model(omnibus_spec, trips, chunk_size, trace_label): + + if not trips.index.is_monotonic: + trips = trips.sort_index() + + # Assign the duration of the appropriate leg to the trip + trips[TRIP_DURATION] = np.where(trips[OUTBOUND], trips[OB_DURATION], trips[IB_DURATION]) + + trips['depart'] = -1 + + # If this is the first outbound trip, the choice is easy, assign the depart time + # to equal the tour start time. + trips.loc[(trips['trip_num'] == 1) & (trips[OUTBOUND]), 'depart'] = trips['start'] + + # If its the first return leg, it is easy too. Just assign the trip start time to the + # end time minus the IB duration + trips.loc[(trips['trip_num'] == 1) & (~trips[OUTBOUND]), 'depart'] = trips['end'] - trips[IB_DURATION] + + # The last leg of the outbound tour needs to begin at the start plus OB duration + trips.loc[(trips['trip_count'] == trips['trip_num']) & (trips[OUTBOUND]), 'depart'] = \ + trips['start'] + trips[OB_DURATION] + + # The last leg of the inbound tour needs to begin at the end time of the tour + trips.loc[(trips['trip_count'] == trips['trip_num']) & (~trips[OUTBOUND]), 'depart'] = \ + trips['end'] + + # Slice off the remaining trips with an intermediate stops to deal with. + # Hopefully, with the tricks above we've sliced off a lot of choices. + # This slice should only include trip numbers greater than 2 since the + side_trips = trips[(trips['trip_num'] != 1) & (trips['trip_count'] != trips['trip_num'])] + + # No processing needs to be done because we have simple trips / tours + if side_trips.empty: + assert trips['depart'].notnull().all + return trips['depart'].astype(int) + + # Get the potential time windows + time_windows = get_time_windows(side_trips[TRIP_DURATION].max(), side_trips[TRIP_COUNT].max() - 1) + + row_size = chunk_size and trip_departure_calc_row_size(trips, trace_label) + + trip_list = [] + + for i, chooser_chunk, chunk_trace_label in \ + chunk.adaptive_chunked_choosers_by_chunk_id(side_trips, chunk_size, row_size, trace_label): + + for is_outbound, trip_segment in chooser_chunk.groupby(OUTBOUND): + direction = OUTBOUND if is_outbound else 'inbound' + spec = get_spec_for_segment(omnibus_spec, direction) + segment_trace_label = '{}_{}'.format(direction, chunk_trace_label) + + patterns = build_patterns(trip_segment, time_windows) + + choices = choose_tour_leg_pattern(trip_segment, + patterns, spec, trace_label=segment_trace_label) + + choices = pd.merge(choices.reset_index(), patterns.reset_index(), + on=[TOUR_LEG_ID, PATTERN_ID], how='left') + + choices = choices[['trip_id', 'stop_time_duration']].copy() + + trip_list.append(choices) + + trip_list = pd.concat(trip_list, sort=True).set_index('trip_id') + trips['stop_time_duration'] = 0 + trips.update(trip_list) + trips.loc[trips['trip_num'] == 1, 'stop_time_duration'] = trips['depart'] + trips.sort_values(['tour_id', 'outbound', 'trip_num']) + trips['stop_time_duration'] = trips.groupby(['tour_id', 'outbound'])['stop_time_duration'].cumsum() + trips.loc[trips['trip_num'] != trips['trip_count'], 'depart'] = trips['stop_time_duration'] + return trips['depart'].astype(int) + + +@inject.step() +def trip_departure_choice( + trips, + trips_merged, + skim_dict, + chunk_size, + trace_hh_id): + + trace_label = 'trip_departure_choice' + model_settings = config.read_model_settings('trip_departure_choice.yaml') + + spec = simulate.read_model_spec(file_name=model_settings['SPECIFICATION']) + + trips_merged_df = trips_merged.to_frame() + # add tour-based chunk_id so we can chunk all trips in tour together + tour_ids = trips_merged[TOUR_ID].unique() + trips_merged_df['chunk_id'] = reindex(pd.Series(list(range(len(tour_ids))), tour_ids), trips_merged_df.tour_id) + + max_tour_id = trips_merged[TOUR_ID].max() + + trip_departure_choice.MAX_TOUR_ID = int(np.power(10, np.ceil(np.log10(max_tour_id)))) + locals_d = config.get_model_constants(model_settings).copy() + + preprocessor_settings = model_settings.get('PREPROCESSOR', None) + tour_legs = get_tour_legs(trips_merged_df) + pipeline.get_rn_generator().add_channel('tour_legs', tour_legs) + + if preprocessor_settings: + od_skim = skim_dict.wrap('origin', 'destination') + do_skim = skim_dict.wrap('destination', 'origin') + + skims = [od_skim, do_skim] + + simulate.set_skim_wrapper_targets(trips_merged_df, skims) + + locals_d.update({ + "od_skims": od_skim, + "do_skims": do_skim, + }) + + expressions.assign_columns( + df=trips_merged_df, + model_settings=preprocessor_settings, + locals_dict=locals_d, + trace_label=trace_label) + + choices = apply_stage_two_model(spec, trips_merged_df, chunk_size, trace_label) + + trips_df = trips.to_frame() + trip_length = len(trips_df) + trips_df = pd.concat([trips_df, choices], axis=1) + assert len(trips_df) == trip_length + assert trips_df[trips_df['depart'].isnull()].empty + + pipeline.replace_table("trips", trips_df) diff --git a/activitysim/abm/models/trip_matrices.py b/activitysim/abm/models/trip_matrices.py index 074d5fac0..661e97541 100644 --- a/activitysim/abm/models/trip_matrices.py +++ b/activitysim/abm/models/trip_matrices.py @@ -31,6 +31,13 @@ def write_trip_matrices(trips, network_los): if bool(model_settings.get('SAVE_TRIPS_TABLE')): pipeline.replace_table('trips', trips_df) + if 'parking_location' in config.setting('models'): + parking_settings = config.read_model_settings('parking_location_choice.yaml') + parking_taz_col_name = parking_settings['ALT_DEST_COL_NAME'] + if parking_taz_col_name in trips_df: + trips_df.loc[trips_df[parking_taz_col_name] > 0, 'destination'] = trips_df[parking_taz_col_name] + # Also need address the return trip + logger.info('Aggregating trips...') aggregate_trips = trips_df.groupby(['origin', 'destination'], sort=False).sum() @@ -72,8 +79,8 @@ def annotate_trips(trips, network_los, model_settings): skim_dict = network_los.get_default_skim_dict() # setup skim keys - assert ('trip_period' not in trips_df) - trips_df['trip_period'] = network_los.skim_time_period_label(trips_df.depart) + if 'trip_period' not in trips_df: + trips_df['trip_period'] = network_los.skim_time_period_label(trips_df.depart) od_skim_wrapper = skim_dict.wrap('origin', 'destination') odt_skim_stack_wrapper = skim_dict.wrap_3d(orig_key='origin', dest_key='destination', dim3_key='trip_period') skims = { diff --git a/activitysim/abm/models/trip_scheduling_choice.py b/activitysim/abm/models/trip_scheduling_choice.py new file mode 100644 index 000000000..70156726a --- /dev/null +++ b/activitysim/abm/models/trip_scheduling_choice.py @@ -0,0 +1,368 @@ +import logging + +import numpy as np +import pandas as pd + +from activitysim.core import chunk +from activitysim.core import config +from activitysim.core import expressions +from activitysim.core import inject +from activitysim.core import pipeline +from activitysim.core import simulate +from activitysim.core import tracing + +from activitysim.abm.models.util.trip import generate_alternative_sizes, get_time_windows +from activitysim.core.interaction_sample_simulate import _interaction_sample_simulate +from activitysim.core.mem import force_garbage_collect + + +logger = logging.getLogger(__name__) + +TOUR_DURATION_COLUMN = 'duration' +NUM_ALTERNATIVES = 'num_alts' +MAIN_LEG_DURATION = 'main_leg_duration' +IB_DURATION = 'inbound_duration' +OB_DURATION = 'outbound_duration' +NUM_OB_STOPS = 'num_outbound_stops' +NUM_IB_STOPS = 'num_inbound_stops' +HAS_OB_STOPS = 'has_outbound_stops' +HAS_IB_STOPS = 'has_inbound_stops' +LAST_OB_STOP = 'last_outbound_stop' +FIRST_IB_STOP = 'last_inbound_stop' + +SCHEDULE_ID = 'schedule_id' + +OUTBOUND_FLAG = 'outbound' + +TEMP_COLS = [NUM_OB_STOPS, LAST_OB_STOP, + NUM_IB_STOPS, FIRST_IB_STOP, + NUM_ALTERNATIVES + ] + + +def generate_schedule_alternatives(tours): + """ + For a set of tours, build out the potential schedule alternatives + for the main leg, outbound leg, and inbound leg. This process handles + the change in three steps. + + Definitions: + - Main Leg: The time from last outbound stop to the first inbound stop. + If the tour does not include any intermediate stops this + will represent the full tour duration. + - Outbound Leg: The time from the tour origin to the last outbound stop + - Inbound Leg: The time from the first inbound stop to the tour origin + + 1. For tours with no intermediate stops, it simple asserts a main leg + duration equal to the tour duration. + + 2. For tours with an intermediate stop on one of the legs, calculate + all possible time combinations that are allowed in the duration + + 3. For tours with an intermediate stop on both legs, calculate + all possible time combinations that are allowed in the tour + duration + + :param tours: pd.Dataframe: Must include a field for tour duration + and boolean fields indicating intermediate inbound or outbound + stops. + :return: pd.Dataframe: Potential time duration windows. + """ + assert set([NUM_IB_STOPS, NUM_OB_STOPS, TOUR_DURATION_COLUMN]).issubset(tours.columns) + + stop_pattern = tours[HAS_OB_STOPS].astype(int) + tours[HAS_IB_STOPS].astype(int) + + no_stops = no_stops_patterns(tours[stop_pattern == 0]) + one_way = stop_one_way_only_patterns(tours[stop_pattern == 1]) + two_way = stop_two_way_only_patterns(tours[stop_pattern > 1]) + + schedules = pd.concat([no_stops, one_way, two_way], sort=True) + schedules[SCHEDULE_ID] = np.arange(1, schedules.shape[0] + 1) + + return schedules + + +def no_stops_patterns(tours): + """ + Asserts the tours with no intermediate stops have a main leg duration equal + to the tour duration and set inbound and outbound windows equal to zero. + :param tours: pd.Dataframe: Tours with no intermediate stops. + :return: pd.Dataframe: Main leg duration, outbound leg duration, and inbound leg duration + """ + alternatives = tours[[TOUR_DURATION_COLUMN]].rename(columns={TOUR_DURATION_COLUMN: MAIN_LEG_DURATION}) + alternatives[[IB_DURATION, OB_DURATION]] = 0 + return alternatives.astype(int) + + +def stop_one_way_only_patterns(tours, travel_duration_col=TOUR_DURATION_COLUMN): + """ + Calculates potential time windows for tours with a single leg with intermediate + stops. It calculates all possibilities for the main leg and one tour leg to sum to + the tour duration. The other leg is asserted with a duration of zero. + :param tours: pd.Dataframe: Tours with no intermediate stops. + :return: pd.Dataframe: Main leg duration, outbound leg duration, and inbound leg duration + The return dataframe is indexed to the tour input index + """ + if tours.empty: + return None + + assert travel_duration_col in tours.columns + + indexes, patterns, pattern_sizes = get_pattern_index_and_arrays(tours.index, tours[travel_duration_col], + one_way=True) + direction = np.repeat(tours[HAS_OB_STOPS], pattern_sizes) + + inbound = np.where(direction == 0, patterns[:, 1], 0) + outbound = np.where(direction == 1, patterns[:, 1], 0) + + patterns = pd.DataFrame(index=indexes, data=np.column_stack((patterns[:, 0], outbound, inbound)), + columns=[MAIN_LEG_DURATION, OB_DURATION, IB_DURATION]) + patterns.index.name = tours.index.name + + return patterns + + +def stop_two_way_only_patterns(tours, travel_duration_col=TOUR_DURATION_COLUMN): + """ + Calculates potential time windows for tours with intermediate stops on both + legs. It calculates all possibilities for the main leg and both tour legs to + sum to the tour duration. + :param tours: pd.Dataframe: Tours with no intermediate stops. + :return: pd.Dataframe: Main leg duration, outbound leg duration, and inbound leg duration + The return dataframe is indexed to the tour input index + """ + if tours.empty: + return None + + assert travel_duration_col in tours.columns + + indexes, patterns, _ = get_pattern_index_and_arrays(tours.index, tours[travel_duration_col], one_way=False) + + patterns = pd.DataFrame(index=indexes, data=patterns, + columns=[MAIN_LEG_DURATION, OB_DURATION, IB_DURATION]) + patterns.index.name = tours.index.name + + return patterns + + +def trip_schedule_calc_row_size(choosers, trace_label): + """ + rows_per_chunk calculator for trip_scheduler + """ + + sizer = chunk.RowSizeEstimator(trace_label) + + chooser_row_size = len(choosers.columns) + spec_columns = 3 + + sizer.add_elements(chooser_row_size + spec_columns, 'choosers') + + row_size = sizer.get_hwm() + return row_size + + +def get_pattern_index_and_arrays(tour_indexes, durations, one_way=True): + """ + A helper method to quickly calculate all of the potential time windows + for a given set of tour indexes and durations. + :param tour_indexes: List of tour indexes + :param durations: List of tour durations + :param one_way: If True, calculate windows for only one tour leg. If False, + calculate tour windows for both legs + :return: np.array: Tour indexes repeated for valid pattern + np.array: array with a column for main tour leg, outbound leg, and inbound leg + np.array: array with the number of patterns for each tour + """ + max_columns = 2 if one_way else 3 + max_duration = np.max(durations) + time_windows = get_time_windows(max_duration, max_columns) + + patterns = [] + pattern_sizes = [] + + for duration in durations: + possible_windows = time_windows[:max_columns, np.where(time_windows.sum(axis=0) == duration)[0]] + possible_windows = np.unique(possible_windows, axis=1).transpose() + patterns.append(possible_windows) + pattern_sizes.append(possible_windows.shape[0]) + + indexes = np.repeat(tour_indexes, pattern_sizes) + + patterns = np.concatenate(patterns) + # If we've done everything right, the indexes + # calculated above should be the same length as + # the pattern options + assert patterns.shape[0] == len(indexes) + + return indexes, patterns, pattern_sizes + + +def get_spec_for_segment(model_settings, spec_name, segment): + """ + Read in the model spec + :param model_settings: model settings file + :param spec_name: name of the key in the settings file + :param segment: which segment of the spec file do you want to read + :return: array of utility equations + """ + + omnibus_spec = simulate.read_model_spec(file_name=model_settings[spec_name]) + + spec = omnibus_spec[[segment]] + + # might as well ignore any spec rows with 0 utility + spec = spec[spec.iloc[:, 0] != 0] + assert spec.shape[0] > 0 + + return spec + + +def run_trip_scheduling_choice(spec, tours, skims, locals_dict, + chunk_size, trace_hh_id, trace_label): + + NUM_TOUR_LEGS = 3 + trace_label = tracing.extend_trace_label(trace_label, 'interaction_sample_simulate') + + # FIXME: The duration, start, and end should be ints well before we get here... + tours[TOUR_DURATION_COLUMN] = tours[TOUR_DURATION_COLUMN].astype(np.int8) + + # Setup boolean columns to make it easier to identify + # intermediate stops later in the model. + tours[HAS_OB_STOPS] = tours[NUM_OB_STOPS] >= 1 + tours[HAS_IB_STOPS] = tours[NUM_IB_STOPS] >= 1 + + # Calculate a matrix with the appropriate alternative sizes + # based on the total tour duration. This is used to calculate + # chunk sizes. + max_duration = tours[TOUR_DURATION_COLUMN].max() + alt_sizes = generate_alternative_sizes(max_duration, NUM_TOUR_LEGS) + + # Assert the number of tour leg schedule alternatives for each tour + tours[NUM_ALTERNATIVES] = 1 + tours.loc[tours[HAS_OB_STOPS] != tours[HAS_IB_STOPS], NUM_ALTERNATIVES] = tours[TOUR_DURATION_COLUMN] + 1 + tours.loc[tours[HAS_OB_STOPS] & tours[HAS_IB_STOPS], NUM_ALTERNATIVES] = \ + tours.apply(lambda x: alt_sizes[1, x.duration], axis=1) + + # If no intermediate stops on the tour, then then main leg duration + # equals the tour duration and the intermediate durations are zero + tours.loc[~tours[HAS_OB_STOPS] & ~tours[HAS_IB_STOPS], MAIN_LEG_DURATION] = tours[TOUR_DURATION_COLUMN] + tours.loc[~tours[HAS_OB_STOPS] & ~tours[HAS_IB_STOPS], [IB_DURATION, OB_DURATION]] = 0 + + # We only need to determine schedules for tours with intermediate stops + indirect_tours = tours.loc[tours[HAS_OB_STOPS] | tours[HAS_IB_STOPS]] + + # Crudely calculate a chunk size + # 5=number of columns in the alternatives (3 leg times + index) + tour_row_size = (4 + len(tours.columns)) * indirect_tours[NUM_ALTERNATIVES].mean() + + # rpc, effective_chunk_size = chunk.rows_per_chunk(chunk_size, tour_row_size, indirect_tours.shape[0], trace_label) + row_size = chunk_size and trip_schedule_calc_row_size(indirect_tours, trace_label) + # Iterate through the chunks + result_list = [] + for i, choosers, chunk_trace_label in \ + chunk.adaptive_chunked_choosers(indirect_tours, chunk_size, row_size, trace_label): + + # Sort the choosers and get the schedule alternatives + choosers = choosers.sort_index() + schedules = generate_schedule_alternatives(choosers).sort_index() + + # Assuming we did the max_alt_size calculation correctly, + # we should get the same sizes here. + assert choosers[NUM_ALTERNATIVES].sum() == schedules.shape[0] + + # Run the simulation + choices = _interaction_sample_simulate( + choosers=choosers, + alternatives=schedules, + spec=spec, + choice_column=SCHEDULE_ID, + allow_zero_probs=True, zero_prob_choice_val=-999, + want_logsums=False, + skims=skims, + locals_d=locals_dict, + trace_label=chunk_trace_label, + trace_choice_name='trip_schedule_stage_1', + estimator=None + ) + + assert len(choices.index) == len(choosers.index) + + choices = schedules[schedules[SCHEDULE_ID].isin(choices)].drop(columns='tour_id') + + result_list.append(choices) + + force_garbage_collect() + + # FIXME: this will require 2X RAM + # if necessary, could append to hdf5 store on disk: + # http://pandas.pydata.org/pandas-docs/stable/io.html#id2 + if len(result_list) > 1: + choices = pd.concat(result_list) + + assert len(choices.index) == len(indirect_tours.index) + + # The choices here are only the indirect tours, so the durations + # need to be updated on the main tour dataframe. + tours.update(choices[[MAIN_LEG_DURATION, OB_DURATION, IB_DURATION]]) + + # Cleanup data types and drop temporary columns + tours[[MAIN_LEG_DURATION, OB_DURATION, IB_DURATION]] = \ + tours[[MAIN_LEG_DURATION, OB_DURATION, IB_DURATION]].astype(np.int8) + tours = tours.drop(columns=TEMP_COLS) + + return tours + + +@inject.step() +def trip_scheduling_choice( + trips, + tours, + skim_dict, + chunk_size, + trace_hh_id): + + trace_label = 'trip_scheduling_choice' + model_settings = config.read_model_settings('trip_scheduling_choice.yaml') + spec = get_spec_for_segment(model_settings, 'SPECIFICATION', 'stage_one') + + trips_df = trips.to_frame() + tours_df = tours.to_frame() + + outbound_trips = trips_df[trips_df[OUTBOUND_FLAG]] + inbound_trips = trips_df[~trips_df[OUTBOUND_FLAG]] + + last_outbound_trip = trips_df.loc[outbound_trips.groupby('tour_id')['trip_num'].idxmax()] + first_inbound_trip = trips_df.loc[inbound_trips.groupby('tour_id')['trip_num'].idxmin()] + + tours_df[NUM_OB_STOPS] = outbound_trips.groupby('tour_id').size().reindex(tours.index) - 1 + tours_df[NUM_IB_STOPS] = inbound_trips.groupby('tour_id').size().reindex(tours.index) - 1 + tours_df[LAST_OB_STOP] = last_outbound_trip[['tour_id', 'origin']].set_index('tour_id').reindex(tours.index) + tours_df[FIRST_IB_STOP] = first_inbound_trip[['tour_id', 'destination']].set_index('tour_id').reindex(tours.index) + + preprocessor_settings = model_settings.get('PREPROCESSOR', None) + + if preprocessor_settings: + # hack: preprocessor adds origin column in place if it does not exist already + od_skim_stack_wrapper = skim_dict.wrap('origin', 'destination') + do_skim_stack_wrapper = skim_dict.wrap('destination', 'origin') + obib_skim_stack_wrapper = skim_dict.wrap(LAST_OB_STOP, FIRST_IB_STOP) + + skims = [od_skim_stack_wrapper, do_skim_stack_wrapper, obib_skim_stack_wrapper] + + locals_dict = { + "od_skims": od_skim_stack_wrapper, + "do_skims": do_skim_stack_wrapper, + "obib_skims": obib_skim_stack_wrapper + } + + simulate.set_skim_wrapper_targets(tours_df, skims) + + expressions.assign_columns( + df=tours_df, + model_settings=preprocessor_settings, + locals_dict=locals_dict, + trace_label=trace_label) + + tours_df = run_trip_scheduling_choice(spec, tours_df, skims, locals_dict, chunk_size, trace_hh_id, trace_label) + + pipeline.replace_table("tours", tours_df) diff --git a/activitysim/abm/models/util/logsums.py b/activitysim/abm/models/util/logsums.py index b164be1bb..cb28aaa96 100644 --- a/activitysim/abm/models/util/logsums.py +++ b/activitysim/abm/models/util/logsums.py @@ -17,7 +17,7 @@ def filter_chooser_columns(choosers, logsum_settings, model_settings): chooser_columns = logsum_settings.get('LOGSUM_CHOOSER_COLUMNS', []) - if 'CHOOSER_ORIG_COL_NAME' in model_settings: + if 'CHOOSER_ORIG_COL_NAME' in model_settings and model_settings['CHOOSER_ORIG_COL_NAME'] not in chooser_columns: chooser_columns.append(model_settings['CHOOSER_ORIG_COL_NAME']) missing_columns = [c for c in chooser_columns if c not in choosers] diff --git a/activitysim/abm/models/util/trip.py b/activitysim/abm/models/util/trip.py index 1208a1c30..f7f97151e 100644 --- a/activitysim/abm/models/util/trip.py +++ b/activitysim/abm/models/util/trip.py @@ -2,6 +2,8 @@ # See full license in LICENSE.txt. import logging +import numpy as np + from activitysim.core.util import assign_in_place @@ -76,3 +78,47 @@ def cleanup_failed_trips(trips): del trips['failed'] return trips + + +def generate_alternative_sizes(max_duration, max_trips): + """ + Builds a lookup Numpy array pattern sizes based on the + number of trips in the leg and the duration available + to the leg. + :param max_duration: + :param max_trips: + :return: + """ + def np_shift(xs, n, fill_zero=True): + if n >= 0: + shift_array = np.concatenate((np.full(n, np.nan), xs[:-n])) + else: + shift_array = np.concatenate((xs[-n:], np.full(-n, np.nan))) + return np.nan_to_num(shift_array, np.nan).astype(np.int) if fill_zero else shift_array + + levels = np.empty([max_trips, max_duration + max_trips]) + levels[0] = np.arange(1, max_duration + max_trips + 1) + + for level in np.arange(1, max_trips): + levels[level] = np_shift(np.cumsum(np_shift(levels[level - 1], 1)), -1, fill_zero=False) + + return levels[:, :max_duration+1].astype(int) + + +def get_time_windows(residual, level): + """ + + :param residual: + :param level: + :return: + """ + ranges = [] + + for a in np.arange(residual + 1): + if level > 1: + windows = get_time_windows(residual - a, level - 1) + width_dim = len(windows.shape) - 1 + ranges.append(np.vstack([np.repeat(a, windows.shape[width_dim]), windows])) + else: + return np.arange(residual + 1) + return np.concatenate(ranges, axis=1) diff --git a/activitysim/abm/models/util/vectorize_tour_scheduling.py b/activitysim/abm/models/util/vectorize_tour_scheduling.py index 3909510b2..d8056d602 100644 --- a/activitysim/abm/models/util/vectorize_tour_scheduling.py +++ b/activitysim/abm/models/util/vectorize_tour_scheduling.py @@ -389,6 +389,15 @@ def _schedule_tours( if constants is not None: locals_d.update(constants) + preprocessor_settings = model_settings.get('ALTS_PREPROCESSOR', None) + + if preprocessor_settings and preprocessor_settings.get(logsum_tour_purpose): + expressions.assign_columns( + df=alt_tdd, + model_settings=preprocessor_settings.get(logsum_tour_purpose), + locals_dict=locals_d, + trace_label=tour_trace_label) + if estimator: # write choosers after annotation estimator.write_choosers(tours) diff --git a/activitysim/abm/test/run_multi_zone_mp.py b/activitysim/abm/test/run_multi_zone_mp.py index c3246fbe0..ddbe85916 100644 --- a/activitysim/abm/test/run_multi_zone_mp.py +++ b/activitysim/abm/test/run_multi_zone_mp.py @@ -20,6 +20,7 @@ # household with WALK_TRANSIT tours and trips HH_ID_3_ZONE = 2848373 + def test_mp_run(): configs_dir = [example_path('configs_3_zone'), mtc_example_path('configs')] @@ -34,7 +35,8 @@ def test_mp_run(): mp_tasks.print_run_list(run_list) # do this after config.handle_standard_args, as command line args may override injectables - injectables = ['data_dir', 'configs_dir', 'output_dir', 'settings_file_name', 'households_sample_size', 'trace_hh_id'] + injectables = ['data_dir', 'configs_dir', 'output_dir', 'settings_file_name', + 'households_sample_size', 'trace_hh_id'] injectables = {k: inject.get_injectable(k) for k in injectables} mp_tasks.run_multiprocess(run_list, injectables) diff --git a/activitysim/abm/test/test_pipeline.py b/activitysim/abm/test/test_pipeline.py index 7585babf2..738b40eb0 100644 --- a/activitysim/abm/test/test_pipeline.py +++ b/activitysim/abm/test/test_pipeline.py @@ -116,7 +116,7 @@ def regress_mini_auto(): # regression test: these are among the middle households in households table # should be the same results as in run_mp (multiprocessing) test case - hh_ids = [1099626, 1173905, 1196298, 1286259] + hh_ids = [1099626, 1173905, 1196298, 1286259] choices = [1, 1, 0, 0] expected_choice = pd.Series(choices, index=pd.Index(hh_ids, name="household_id"), name='auto_ownership') diff --git a/activitysim/abm/test/test_trip_departure_choice.py b/activitysim/abm/test/test_trip_departure_choice.py new file mode 100644 index 000000000..a3f55e682 --- /dev/null +++ b/activitysim/abm/test/test_trip_departure_choice.py @@ -0,0 +1,99 @@ + +import numpy as np +import pandas as pd +import pytest + +import activitysim.abm.models.trip_departure_choice as tdc +from activitysim.abm.models.util.trip import get_time_windows +from activitysim.core import los +from .test_pipeline import setup_dirs + + +@pytest.fixture(scope='module') +def trips(): + outbound_array = [True, True, False, False, False, True, True, False, False, True] + + trips = pd.DataFrame(data={'tour_id': [1, 1, 2, 2, 2, 2, 2, 3, 3, 4], + 'trip_duration': [2, 2, 7, 7, 7, 12, 12, 4, 4, 5], + 'inbound_duration': [0, 0, 7, 7, 7, 0, 0, 4, 4, 5], + 'main_leg_duration': [4, 4, 2, 2, 2, 2, 2, 1, 1, 2], + 'outbound_duration': [2, 2, 0, 0, 0, 12, 12, 0, 0, 5], + 'trip_count': [2, 2, 3, 3, 3, 2, 2, 2, 2, 1], + 'trip_num': [1, 2, 1, 2, 3, 1, 2, 1, 2, 1], + 'outbound': outbound_array, + 'chunk_id': [1, 1, 2, 2, 2, 2, 2, 3, 3, 4], + 'is_work': [True, True, False, False, False, False, False, False, False, True], + 'is_school': [False, False, False, False, False, False, False, True, True, False], + 'is_eatout': [False, False, True, True, True, True, True, False, False, False], + 'start': [8, 8, 18, 18, 18, 18, 18, 24, 24, 19], + 'end': [14, 14, 39, 39, 39, 39, 39, 29, 29, 26], + 'origin': [3, 5, 15, 12, 24, 8, 17, 8, 9, 6], + 'destination': [5, 9, 12, 24, 20, 17, 18, 9, 11, 14], + }, index=range(10)) + + trips.index.name = 'trip_id' + return trips + + +@pytest.fixture(scope='module') +def settings(): + return {"skims_file": "skims.omx", + "skim_time_periods": { + "labels": ['EA', 'AM', 'MD', 'PM', 'NT']} + } + + +@pytest.fixture(scope='module') +def model_spec(): + index = ["@(df['stop_time_duration'] * df['is_work'].astype(int)).astype(int)", + "@(df['stop_time_duration'] * df['is_school'].astype(int)).astype(int)", + "@(df['stop_time_duration'] * df['is_eatout'].astype(int)).astype(int)"] + + values = {'inbound': [0.933020, 0.370260, 0.994840], + 'outbound': [0.933020, 0.370260, 0.994840] + } + + return pd.DataFrame(index=index, data=values) + + +def test_build_patterns(trips): + time_windows = get_time_windows(48, 3) + patterns = tdc.build_patterns(trips, time_windows) + patterns = patterns.sort_values(['tour_id', 'outbound', 'trip_num']) + + assert patterns.shape[0] == 34 + assert patterns.shape[1] == 6 + assert patterns.index.name == tdc.TOUR_LEG_ID + + output_columns = [tdc.TOUR_ID, tdc.PATTERN_ID, tdc.TRIP_NUM, + tdc.STOP_TIME_DURATION, tdc.TOUR_ID, tdc.OUTBOUND] + + assert set(output_columns).issubset(patterns.columns) + + +def test_get_tour_legs(trips): + tour_legs = tdc.get_tour_legs(trips) + assert tour_legs.index.name == tdc.TOUR_LEG_ID + assert np.unique(tour_legs[tdc.TOUR_ID].values).shape[0] == np.unique(trips[tdc.TOUR_ID].values).shape[0] + + +def test_generate_alternative(trips): + alts = tdc.generate_alternatives(trips, tdc.STOP_TIME_DURATION) + assert alts.shape[0] == 67 + assert alts.shape[1] == 1 + + assert alts.index.name == tdc.TRIP_ID + assert alts.columns[0] == tdc.STOP_TIME_DURATION + + pd.testing.assert_series_equal(trips.groupby(trips.index)['trip_duration'].max(), + alts.groupby(alts.index)[tdc.STOP_TIME_DURATION].max(), + check_names=False) + + +def test_apply_stage_two_model(model_spec, trips): + setup_dirs() + departures = tdc.apply_stage_two_model(model_spec, trips, 0, 'TEST Trip Departure') + assert len(departures) == len(trips) + pd.testing.assert_index_equal(departures.index, trips.index) + + departures = pd.concat([trips, departures], axis=1) diff --git a/activitysim/abm/test/test_trip_scheduling_choice.py b/activitysim/abm/test/test_trip_scheduling_choice.py new file mode 100644 index 000000000..e8797b966 --- /dev/null +++ b/activitysim/abm/test/test_trip_scheduling_choice.py @@ -0,0 +1,161 @@ + +import numpy as np +import pandas as pd +import pytest + +from activitysim.abm.models import trip_scheduling_choice as tsc +from activitysim.abm.tables.skims import skim_dict +from activitysim.core import los +from .test_pipeline import setup_dirs + + +@pytest.fixture(scope='module') +def tours(): + tours = pd.DataFrame(data={'duration': [2, 44, 32, 12, 11, 16], + 'num_outbound_stops': [2, 4, 0, 0, 1, 3], + 'num_inbound_stops': [1, 0, 0, 2, 1, 2], + 'tour_type': ['othdisc'] * 2 + ['eatout'] * 4, + 'origin': [3, 10, 15, 23, 5, 8], + 'destination': [5, 9, 12, 24, 20, 17], + tsc.LAST_OB_STOP: [1, 3, 0, 0, 12, 14], + tsc.FIRST_IB_STOP: [2, 0, 0, 4, 6, 20], + }, index=range(6)) + + tours.index.name = 'tour_id' + + tours[tsc.HAS_OB_STOPS] = tours[tsc.NUM_OB_STOPS] >= 1 + tours[tsc.HAS_IB_STOPS] = tours[tsc.NUM_IB_STOPS] >= 1 + + return tours + + +@pytest.fixture(scope='module') +def settings(): + return {"skims_file": "skims.omx", + "skim_time_periods": { + "labels": ['MD']} + } + + +@pytest.fixture(scope='module') +def model_spec(): + index = ["@(df['main_leg_duration']>df['duration']).astype(int)", + "@(df['main_leg_duration'] == 0)&(df['tour_type']=='othdiscr')", + "@(df['main_leg_duration'] == 1)&(df['tour_type']=='othdiscr')", + "@(df['main_leg_duration'] == 2)&(df['tour_type']=='othdiscr')", + "@(df['main_leg_duration'] == 3)&(df['tour_type']=='othdiscr')", + "@(df['main_leg_duration'] == 4)&(df['tour_type']=='othdiscr')", + "@df['tour_type']=='othdiscr'", + "@df['tour_type']=='eatout'", + "@df['tour_type']=='eatout'" + ] + + values = [-999, -6.5884, -5.0326, -2.0526, -1.0313, -0.46489, 0.060382, -0.7508, 0.53247] + + return pd.DataFrame(index=index, data=values, columns=['stage_one']) + + +@pytest.fixture(scope='module') +def skims(settings): + setup_dirs() + nw_los = los.Network_LOS() + nw_los.load_data() + skim_d = skim_dict(nw_los) + + od_skim_stack_wrapper = skim_d.wrap('origin', 'destination') + do_skim_stack_wrapper = skim_d.wrap('destination', 'origin') + obib_skim_stack_wrapper = skim_d.wrap(tsc.LAST_OB_STOP, tsc.FIRST_IB_STOP) + + skims = [od_skim_stack_wrapper, do_skim_stack_wrapper, obib_skim_stack_wrapper] + + return skims + + +@pytest.fixture(scope='module') +def locals_dict(skims): + return { + "od_skims": skims[0], + "do_skims": skims[1], + "obib_skims": skims[2] + } + + +def test_generate_schedule_alternatives(tours): + windows = tsc.generate_schedule_alternatives(tours) + assert windows.shape[0] == 296 + assert windows.shape[1] == 4 + + output_columns = [tsc.SCHEDULE_ID, tsc.MAIN_LEG_DURATION, + tsc.OB_DURATION, tsc.IB_DURATION] + + assert set(output_columns).issubset(windows.columns) + + +def test_no_stops_patterns(tours): + no_stops = tours[(tours['num_outbound_stops'] == 0) & (tours['num_inbound_stops'] == 0)].copy() + windows = tsc.no_stops_patterns(no_stops) + + assert windows.shape[0] == 1 + assert windows.shape[1] == 3 + + output_columns = [tsc.MAIN_LEG_DURATION, + tsc.OB_DURATION, tsc.IB_DURATION] + + assert set(output_columns).issubset(windows.columns) + + pd.testing.assert_series_equal(windows[tsc.MAIN_LEG_DURATION], no_stops['duration'], + check_names=False, check_dtype=False) + assert windows[windows[tsc.IB_DURATION] > 0].empty + assert windows[windows[tsc.OB_DURATION] > 0].empty + + +def test_one_way_stop_patterns(tours): + one_way_stops = tours[((tours['num_outbound_stops'] > 0).astype(int) + + (tours['num_inbound_stops'] > 0).astype(int)) == 1].copy() + windows = tsc.stop_one_way_only_patterns(one_way_stops) + + assert windows.shape[0] == 58 + assert windows.shape[1] == 3 + + output_columns = [tsc.MAIN_LEG_DURATION, + tsc.OB_DURATION, tsc.IB_DURATION] + + assert set(output_columns).issubset(windows.columns) + + inbound_options = windows[(windows[tsc.IB_DURATION] > 0)] + outbound_options = windows[windows[tsc.OB_DURATION] > 0] + assert np.unique(inbound_options.index).shape[0] == 1 + assert np.unique(outbound_options.index).shape[0] == 1 + + +def test_two_way_stop_patterns(tours): + two_way_stops = tours[((tours['num_outbound_stops'] > 0).astype(int) + + (tours['num_inbound_stops'] > 0).astype(int)) == 2].copy() + windows = tsc.stop_two_way_only_patterns(two_way_stops) + + assert windows.shape[0] == 237 + assert windows.shape[1] == 3 + + output_columns = [tsc.MAIN_LEG_DURATION, + tsc.OB_DURATION, tsc.IB_DURATION] + + assert set(output_columns).issubset(windows.columns) + + +def test_run_trip_scheduling_choice(model_spec, tours, skims, locals_dict): + """ + Test run the model. + """ + + out_tours = tsc.run_trip_scheduling_choice(model_spec, tours, skims, locals_dict, + 2, None, "PyTest Trip Scheduling") + + assert len(tours) == len(out_tours) + pd.testing.assert_index_equal(tours.sort_index().index, out_tours.sort_index().index) + + output_columns = [tsc.MAIN_LEG_DURATION, + tsc.OB_DURATION, tsc.IB_DURATION] + + assert set(output_columns).issubset(out_tours.columns) + + assert len(out_tours[out_tours[output_columns].sum(axis=1) == out_tours[tsc.TOUR_DURATION_COLUMN]]) == len(tours) diff --git a/activitysim/abm/test/test_trip_utils.py b/activitysim/abm/test/test_trip_utils.py new file mode 100644 index 000000000..bf908986e --- /dev/null +++ b/activitysim/abm/test/test_trip_utils.py @@ -0,0 +1,25 @@ +import numpy as np +import pandas as pd +import pytest + +from activitysim.abm.models.util.trip import get_time_windows + + +@pytest.mark.parametrize("duration, levels, expected", + [(24, 3, 2925), (24, 2, 325), (24, 1, 25), + (48, 3, 20825), (48, 2, 1225), (48, 1, 49)]) +def test_get_time_windows(duration, levels, expected): + time_windows = get_time_windows(duration, levels) + + if levels == 1: + assert time_windows.ndim == 1 + assert len(time_windows) == expected + assert np.sum(time_windows <= duration) == expected + else: + assert len(time_windows) == levels + assert len(time_windows[0]) == expected + total_duration = np.sum(time_windows, axis=0) + assert np.sum(total_duration <= duration) == expected + + df = pd.DataFrame(np.transpose(time_windows)) + assert len(df) == len(df.drop_duplicates()) diff --git a/activitysim/core/assign.py b/activitysim/core/assign.py index 65462b247..435d7780d 100644 --- a/activitysim/core/assign.py +++ b/activitysim/core/assign.py @@ -11,6 +11,7 @@ from activitysim.core import util from activitysim.core import config +from activitysim.core import expressions from activitysim.core import pipeline from activitysim.core import inject @@ -150,6 +151,7 @@ def local_utilities(): 'reindex_i': util.reindex_i, 'setting': config.setting, 'other_than': util.other_than, + 'skim_time_period_label': expressions.skim_time_period_label, 'rng': pipeline.get_rn_generator(), } diff --git a/activitysim/core/interaction_simulate.py b/activitysim/core/interaction_simulate.py index 00dc0df81..4ed13da02 100644 --- a/activitysim/core/interaction_simulate.py +++ b/activitysim/core/interaction_simulate.py @@ -75,6 +75,8 @@ def eval_interaction_utilities(spec, df, locals_d, trace_label, trace_rows, esti def to_series(x): if np.isscalar(x): return pd.Series([x] * len(df), index=df.index) + if isinstance(x, np.ndarray): + return pd.Series(x, index=df.index) return x if trace_rows is not None and trace_rows.any(): diff --git a/activitysim/examples/example_mtc/configs/annotate_households.csv b/activitysim/examples/example_mtc/configs/annotate_households.csv index e7b590be1..5b99afe96 100644 --- a/activitysim/examples/example_mtc/configs/annotate_households.csv +++ b/activitysim/examples/example_mtc/configs/annotate_households.csv @@ -28,4 +28,4 @@ num_young_adults,num_young_adults,"_PERSON_COUNT('25 <= age <= 34', persons, hou non_family,non_family,households.HHT.isin(HHT_NONFAMILY) family,family,households.HHT.isin(HHT_FAMILY) home_is_urban,home_is_urban,"reindex(land_use.area_type, households.home_zone_id) < setting('urban_threshold')" -home_is_rural,home_is_rural,"reindex(land_use.area_type, households.home_zone_id) > setting('urban_threshold')" \ No newline at end of file +home_is_rural,home_is_rural,"reindex(land_use.area_type, households.home_zone_id) > setting('rural_threshold')" \ No newline at end of file diff --git a/activitysim/examples/example_mtc/configs/annotate_landuse.csv b/activitysim/examples/example_mtc/configs/annotate_landuse.csv index 229833a50..91e092774 100644 --- a/activitysim/examples/example_mtc/configs/annotate_landuse.csv +++ b/activitysim/examples/example_mtc/configs/annotate_landuse.csv @@ -3,3 +3,5 @@ Description,Target,Expression household_density,household_density,land_use.TOTHH / (land_use.RESACRE + land_use.CIACRE) employment_density,employment_density,land_use.TOTEMP / (land_use.RESACRE + land_use.CIACRE) density_index,density_index,(household_density *employment_density) / (household_density + employment_density).clip(lower=1) +,is_cbd,land_use.area_type == 1 + diff --git a/activitysim/examples/example_mtc/configs/joint_tour_destination.yaml b/activitysim/examples/example_mtc/configs/joint_tour_destination.yaml index 7cd164587..8b9ff6352 100644 --- a/activitysim/examples/example_mtc/configs/joint_tour_destination.yaml +++ b/activitysim/examples/example_mtc/configs/joint_tour_destination.yaml @@ -1 +1,3 @@ -include_settings: non_mandatory_tour_destination.yaml \ No newline at end of file +include_settings: non_mandatory_tour_destination.yaml +#IN_PERIOD: 14 +#OUT_PERIOD: 14 \ No newline at end of file diff --git a/activitysim/examples/example_mtc/configs/parking_location_choice.csv b/activitysim/examples/example_mtc/configs/parking_location_choice.csv new file mode 100644 index 000000000..78d345ce1 --- /dev/null +++ b/activitysim/examples/example_mtc/configs/parking_location_choice.csv @@ -0,0 +1,2 @@ +Description,Expression,mandatory_free,mandatory_pay,nonmandatory +Distance-Parking Zone to Destination,@pd_skims['DIST'],-0.4048,-4.366,-0.2572 diff --git a/activitysim/examples/example_mtc/configs/parking_location_choice.yaml b/activitysim/examples/example_mtc/configs/parking_location_choice.yaml new file mode 100644 index 000000000..3cf661d97 --- /dev/null +++ b/activitysim/examples/example_mtc/configs/parking_location_choice.yaml @@ -0,0 +1,36 @@ +METADATA: + CHOOSER: trips + INPUT: + persons: + trips: + tours: + OUTPUT: + trips: + - parking_taz + +SPECIFICATION: parking_location_choice.csv +COEFFICIENTS: parking_location_choice_coeffs.csv + +PREPROCESSOR: + SPEC: parking_location_choice_annotate_trips_preprocessor + DF: trips + TABLES: + - land_use + - persons + - tours + +# boolean column to filter choosers (True means keep) +CHOOSER_FILTER_COLUMN_NAME: is_park_eligible +CHOOSER_SEGMENT_COLUMN_NAME: parking_segment + +ALTERNATIVE_FILTER_COLUMN_NAME: is_cbd +TRIP_DEPARTURE_PERIOD: depart + +SEGMENTS: + - mandatory_free + - mandatory_pay + - nonmandatory + +ALT_DEST_COL_NAME: parking_taz +TRIP_ORIGIN: origin +TRIP_DESTINATION: destination diff --git a/activitysim/examples/example_mtc/configs/parking_location_choice_annotate_trips_preprocessor.csv b/activitysim/examples/example_mtc/configs/parking_location_choice_annotate_trips_preprocessor.csv new file mode 100644 index 000000000..291d28138 --- /dev/null +++ b/activitysim/examples/example_mtc/configs/parking_location_choice_annotate_trips_preprocessor.csv @@ -0,0 +1,10 @@ +Description,Target,Expression +#,, +,_area_type,"reindex(land_use.area_type, df.destination)" +,is_cbd,_area_type == 1 +,is_drive,"df.trip_mode.isin(['DRIVEALONEFREE', 'DRIVEALONEPAY', 'SHARED2FREE', 'SHARED2PAY', 'SHARED3FREE', 'SHARED3PAY'])" +,is_park_eligible, is_cbd & is_drive +,tour_category,"reindex(tours.tour_category, df.tour_id)" +,_free_parking,"reindex(persons.free_parking_at_work, df.person_id)" +,parking_segment,"np.where(tour_category == 'mandatory', np.where(_free_parking,'mandatory_free', 'mandatory_pay'),'nonmandatory')" +,trip_period,network_los.skim_time_period_label(df.depart) diff --git a/activitysim/examples/example_mtc/configs/parking_location_choice_coeffs.csv b/activitysim/examples/example_mtc/configs/parking_location_choice_coeffs.csv new file mode 100644 index 000000000..1e3f0fbf4 --- /dev/null +++ b/activitysim/examples/example_mtc/configs/parking_location_choice_coeffs.csv @@ -0,0 +1 @@ +coefficient_name,value,constrain diff --git a/activitysim/examples/example_mtc/configs/settings.yaml b/activitysim/examples/example_mtc/configs/settings.yaml index 88e650e79..95c9e7e08 100644 --- a/activitysim/examples/example_mtc/configs/settings.yaml +++ b/activitysim/examples/example_mtc/configs/settings.yaml @@ -156,7 +156,10 @@ models: - trip_destination - trip_purpose_and_destination - trip_scheduling + # - trip_scheduling_choice + # - trip_departure_choice - trip_mode_choice + - parking_location - write_data_dictionary - track_skim_usage - write_trip_matrices @@ -179,7 +182,6 @@ output_tables: # area_types less than this are considered urban urban_threshold: 4 cbd_threshold: 2 - # - value of time min_value_of_time: 1 max_value_of_time: 50 diff --git a/activitysim/examples/example_mtc/configs/trip_departure_choice.csv b/activitysim/examples/example_mtc/configs/trip_departure_choice.csv new file mode 100644 index 000000000..a460044f2 --- /dev/null +++ b/activitysim/examples/example_mtc/configs/trip_departure_choice.csv @@ -0,0 +1,8 @@ +Description,Expression,outbound,inbound +StopTimeWork,@(df['stop_time_duration'] * df['is_work'].astype(int)).astype(int),0.933020,0.933020 +StopTimeSchool,@(df['stop_time_duration'] * df['is_school'].astype(int)).astype(int),0.370260,0.370260 +StopTimeEatOut,@(df['stop_time_duration'] * df['is_eatout'].astype(int)).astype(int),0.994840,0.994840 +StopTimeMainen,@(df['stop_time_duration'] * df['is_other_maintenance'].astype(int)).astype(int),0.254180,0.254180 +StopTimeShop,@(df['stop_time_duration'] * df['is_shopping'].astype(int)).astype(int),0.619340,0.619340 +StopTimeSocial,@(df['stop_time_duration'] * df['is_social'].astype(int)).astype(int),0.784420,0.784420 +StopTimeDiscre,@(df['stop_time_duration'] * df['is_othdisc'].astype(int)).astype(int),1.277000,1.277000 diff --git a/activitysim/examples/example_mtc/configs/trip_departure_choice.yaml b/activitysim/examples/example_mtc/configs/trip_departure_choice.yaml new file mode 100644 index 000000000..daf657bcc --- /dev/null +++ b/activitysim/examples/example_mtc/configs/trip_departure_choice.yaml @@ -0,0 +1,20 @@ +METADATA: + CHOOSER: tours + INPUT: + persons: + trips: + tours: + OUTPUT: + trips: + - start_period + - end_period + +SPECIFICATION: trip_departure_choice.csv +COEFFICIENTS: trip_departure_choice_coeff.csv + + +PREPROCESSOR: + SPEC: trip_departure_choice_preprocessor + DF: trips + TABLES: + - tours \ No newline at end of file diff --git a/activitysim/examples/example_mtc/configs/trip_departure_choice_preprocessor.csv b/activitysim/examples/example_mtc/configs/trip_departure_choice_preprocessor.csv new file mode 100644 index 000000000..1a2ecccab --- /dev/null +++ b/activitysim/examples/example_mtc/configs/trip_departure_choice_preprocessor.csv @@ -0,0 +1,9 @@ +Description,Target,Expression +,tripFFT,"od_skims['SOV_TIME', 'MD']" +,is_work,trips['purpose']=='work' +,is_school,trips['purpose']=='school' +,is_eatout,trips['purpose']=='eatout' +,is_other_maintenance,trips['purpose']=='othmaint' +,is_shopping,trips['purpose']=='shopping' +,is_social,trips['purpose']=='social' +,is_othdisc,trips['purpose']=='othdisc' diff --git a/activitysim/examples/example_mtc/configs/trip_scheduling_choice.csv b/activitysim/examples/example_mtc/configs/trip_scheduling_choice.csv new file mode 100644 index 000000000..be457f8a3 --- /dev/null +++ b/activitysim/examples/example_mtc/configs/trip_scheduling_choice.csv @@ -0,0 +1,128 @@ +Description,Expression,stage_one +Alternative is Invalid if leg time is longer than total time,@(df['main_leg_duration']>df['duration']).astype(int),-999 +Discretionary tour-ASC for Legtime = 0,@(df['main_leg_duration'] == 0)&(df['tour_type']=='othdiscr'),-6.5884 +"Discretionary tour,ASC for Legtime = 1",@(df['main_leg_duration'] == 1)&(df['tour_type']=='othdiscr'),-5.0326 +"Discretionary tour,ASC for Legtime = 2",@(df['main_leg_duration'] == 2)&(df['tour_type']=='othdiscr'),-2.0526 +"Discretionary tour,ASC for Legtime = 3",@(df['main_leg_duration'] == 3)&(df['tour_type']=='othdiscr'),-1.0313 +"Discretionary tour,ASC for Legtime = 4",@(df['main_leg_duration'] == 4)&(df['tour_type']=='othdiscr'),-0.46489 +Discretionary tour - Main Leg time,@df['tour_type']=='othdiscr',0.060382 +Eatout tour - Shift,@df['tour_type']=='eatout',-0.7508 +Eatout tour - Main leg time,@df['tour_type']=='eatout',0.53247 +"Maintenance tour,ASC for Legtime = 0",@(df['main_leg_duration'] == 0)&(df['tour_type']=='othmaint'),-3.6079 +"Maintenance tour,ASC for Legtime = 1",@(df['main_leg_duration'] == 1)&(df['tour_type']=='othmaint'),-1.9376 +"Maintenance tour,ASC for Legtime = 2",@(df['main_leg_duration'] == 2)&(df['tour_type']=='othmaint'),-0.99484 +"Maintenance tour,ASC for Legtime = 3",@(df['main_leg_duration'] == 3)&(df['tour_type']=='othmaint'),-0.29166 +"Maintenance tour,ASC for Legtime = 4",@(df['main_leg_duration'] == 4)&(df['tour_type']=='othmaint'),0.18669 +Maintenance tour - Main leg time,@df['tour_type']=='othmaint',-0.03572 +"School tour,ASC for Legtime = 14",@(df['main_leg_duration'] == 14)&(df['tour_type']=='school'),1.2449 +"School tour,ASC for Legtime = 15",@(df['main_leg_duration'] == 15)&(df['tour_type']=='school'),1.8492 +"School tour,ASC for Legtime = 16",@(df['main_leg_duration'] == 16)&(df['tour_type']=='school'),2.0672 +"School tour,ASC for Legtime = 17",@(df['main_leg_duration'] == 17)&(df['tour_type']=='school'),1.8571 +"School tour,ASC for Legtime = 18",@(df['main_leg_duration'] == 18)&(df['tour_type']=='school'),1.3826 +"School tour,ASC for Legtime = 19",@(df['main_leg_duration'] == 19)&(df['tour_type']=='school'),0.92034 +"School tour,ASC for Legtime = 20",@(df['main_leg_duration'] == 20)&(df['tour_type']=='school'),0.37001 +School tour - Main Leg time,@df['tour_type']=='school',1.7393 +School tour - Shift,@df['tour_type']=='school',-1.5696 +School tour - Shift,@df['tour_type']=='school',-0.43764 +"Escort tour,ASC for Legtime = 0",@(df['main_leg_duration'] == 0).astype(int)*(df['tour_type']=='escort'),-1.2273 +"Escort tour,ASC for Legtime = 1",@(df['main_leg_duration'] == 1)&(df['tour_type']=='escort'),0.48815 +"Escort tour,ASC for Legtime = 2",@(df['main_leg_duration'] == 2)&(df['tour_type']=='escort'),0.37136 +"Escort tour,ASC for Legtime = 3",@(df['main_leg_duration'] == 3)&(df['tour_type']=='escort'),-0.29005 +Escort tour - Main Leg time,@df['tour_type']=='escort',-0.005499 +"Shopping tour,ASC for Legtime = 0",@(df['main_leg_duration'] == 0)&(df['tour_type']=='shopping'),-4.5136 +"Shopping tour,ASC for Legtime = 1",@(df['main_leg_duration'] == 1)&(df['tour_type']=='shopping'),-1.8461 +"Shopping tour,ASC for Legtime = 2",@(df['main_leg_duration'] == 2)&(df['tour_type']=='shopping'),-0.81101 +"Shopping tour,ASC for Legtime = 3",@(df['main_leg_duration'] == 3)&(df['tour_type']=='shopping'),-0.42265 +"Shopping tour,ASC for Legtime = 4",@(df['main_leg_duration'] == 4)&(df['tour_type']=='shopping'),-0.25089 +Shopping tour - Main Leg time,@df['tour_type']=='shopping',-0.30597 +Social tour - Main Leg time,@df['tour_type']=='social',1.1482 +Social tour - Shift,@df['tour_type']=='social',-0.94185 +University tour - Main Leg time,@df['tour_type']=='univ',0.56244 +University tour - Shift,@df['tour_type']=='univ',-0.55984 +University tour - Shift,@df['tour_type']=='univ',-0.22445 +Work tour - Main Leg time,@df['tour_type']=='work',0.45055 +Work tour - Shift,@df['tour_type']=='work',-0.27206 +Work tour - Shift,@df['tour_type']=='work',0.009149 +"Work tour,ASC for Legtime = 17",@(df['main_leg_duration'] == 17)&(df['tour_type']=='work'),0.12954 +"Work tour,ASC for Legtime = 18",@(df['main_leg_duration'] == 18)&(df['tour_type']=='work'),0.54498 +"Work tour,ASC for Legtime = 19",@(df['main_leg_duration'] == 19)&(df['tour_type']=='work'),0.64445 +"Work tour,ASC for Legtime = 20",@(df['main_leg_duration'] == 20)&(df['tour_type']=='work'),0.56793 +"Work tour,ASC for Legtime = 21",@(df['main_leg_duration'] == 21)&(df['tour_type']=='work'),0.16153 +"Work tour,ASC for Legtime = 22",@(df['main_leg_duration'] == 22)&(df['tour_type']=='work'),-0.15183 +Work tour - Shift,@df['tour_type']=='work',-0.57964 +# main leg time - main leg free flow travel time,,0.00387 +# main leg time *SIN(2?*TourStartPeriod/48),,-0.007568 +# main leg time *COS(2?*TourStartPeriod/48),,0.11681 +# main leg time *SIN(4?*TourStartPeriod/48),,0.019579 +# main leg time *COS(4?*TourStartPeriod/48),,0.01919 +# main leg time - full time worker's work tour ,fullTimeWorker&df['tour_type']=='work',0.037065 +Calibration,@(df['main_leg_duration'] == 19)&(df['tour_type']=='work'),0.5253 +Calibration,@(df['main_leg_duration'] == 20)&(df['tour_type']=='work'),0.7719 +Calibration,@(df['main_leg_duration'] == 21)&(df['tour_type']=='work'),1.0697 +Calibration,@(df['main_leg_duration'] == 22)&(df['tour_type']=='work'),1.2412 +Calibration,@(df['main_leg_duration'] == 23)&(df['tour_type']=='work'),1.1888 +Calibration,@(df['main_leg_duration'] == 16)&(df['tour_type']=='school'),0.3565 +Calibration,@(df['main_leg_duration'] == 17)&(df['tour_type']=='school'),0.5677 +Calibration,@(df['main_leg_duration'] == 18)&(df['tour_type']=='school'),0.8005 +Calibration,@(df['main_leg_duration'] == 19)&(df['tour_type']=='school'),0.7861 +Calibration,@(df['main_leg_duration'] == 0)&(df['tour_type']=='escort'),-2.8173 +Calibration,@(df['main_leg_duration'] == 1)&(df['tour_type']=='escort'),-0.359 +Calibration,@(df['main_leg_duration'] == 2)&(df['tour_type']=='escort'),1.2018 +Calibration,@(df['main_leg_duration'] == 3)&(df['tour_type']=='escort'),1.6866 +Calibration,@(df['main_leg_duration'] == 0)&(df['tour_type']=='othmaint'),-3.3465 +Calibration,@(df['main_leg_duration'] == 1)&(df['tour_type']=='othmaint'),-1.511 +Calibration,@(df['main_leg_duration'] == 2)&(df['tour_type']=='othmaint'),-0.4784 +Calibration,@(df['main_leg_duration'] == 3)&(df['tour_type']=='othmaint'),0.0637 +Calibration,@(df['main_leg_duration'] == 4)&(df['tour_type']=='othmaint'),0.4645 +Calibration,@(df['main_leg_duration'] == 0)&(df['tour_type']=='shopping'),-2.0645 +Calibration,@(df['main_leg_duration'] == 1)&(df['tour_type']=='shopping'),-1.0205 +Calibration,@(df['main_leg_duration'] == 2)&(df['tour_type']=='shopping'),-0.0582 +Calibration,@(df['main_leg_duration'] == 3)&(df['tour_type']=='shopping'),0.5533 +Calibration,@(df['main_leg_duration'] == 0)&(df['tour_type']=='eatout'),-100 +Calibration,@(df['main_leg_duration'] == 1)&(df['tour_type']=='eatout'),-100 +Calibration,@(df['main_leg_duration'] == 2)&(df['tour_type']=='eatout'),-6.8372 +Calibration,@(df['main_leg_duration'] == 3)&(df['tour_type']=='eatout'),-0.3319 +Calibration,@(df['main_leg_duration'] == 4)&(df['tour_type']=='eatout'),0.8709 +Calibration,@(df['main_leg_duration'] == 5)&(df['tour_type']=='eatout'),1.2215 +Calibration,@(df['main_leg_duration'] == 6)&(df['tour_type']=='eatout'),1.0655 +Calibration,@(df['main_leg_duration'] == 0)&(df['tour_type']=='social'),-5.9111 +Calibration,@(df['main_leg_duration'] == 1)&(df['tour_type']=='social'),-2.9703 +Calibration,@(df['main_leg_duration'] == 2)&(df['tour_type']=='social'),-1.5087 +Calibration,@(df['main_leg_duration'] == 0)&(df['tour_type']=='at_work'),-1.988 +Calibration,@(df['main_leg_duration'] == 1)&(df['tour_type']=='at_work'),0.1619 +Calibration,@(df['main_leg_duration'] == 2)&(df['tour_type']=='at_work'),0.335 +Calibration,@(df['main_leg_duration'] == 3)&(df['tour_type']=='at_work'),1.0155 +# OUTBOUND LEG COMPONENTS,, +alternative is invalid if leg time is longer than total tour time,@(df['outbound_duration']>df['duration']).astype(int),-999 +alternative is invalid if leg time>0 yet there is no stop on the leg,@(df['num_outbound_stops']==0)&(df['outbound_duration']>0),-999 +outbound leg time * outbound leg free flow travel time,"@(df['outbound_duration']*od_skims['SOV_TIME', 'MD'])",0.0058104 +# outbound leg time *SIN(2?*TourStartPeriod/48),outboundLegTime*@fourierSin1,-0.20702 +# outbound leg time *COS(2?*TourStartPeriod/48),outboundLegTime*@fourierCos1,0.18594 +# outbound leg time *SIN(4?*TourStartPeriod/48),outboundLegTime*@fourierSin2,-0.11703 +# outbound leg time *COS(4?*TourStartPeriod/48),outboundLegTime*@fourierCos2,-0.014628 +Average Stop Time,"@np.where(df['num_outbound_stops'] > 0,df['outbound_duration'] / df['num_outbound_stops'],0)",-0.31564 +Calibration,@(df['num_outbound_stops']==1)&(df['outbound_duration'] ==0),-0.723010589 +Calibration,@(df['num_outbound_stops']==1)&(df['outbound_duration'] ==1),0.792121459 +Calibration,@(df['num_outbound_stops']==2)&(df['outbound_duration'] ==0),-4.854181844 +Calibration,@(df['num_outbound_stops']==2)&(df['outbound_duration'] ==1),-0.181033741 +Calibration,@(df['num_outbound_stops']==2)&(df['outbound_duration'] ==2),0.967315884 +Calibration,@(df['num_outbound_stops']==2)&(df['outbound_duration'] ==3),0.467052643 +Calibration,@(df['num_outbound_stops']==3)&(df['outbound_duration'] ==0),-15.05439781 +Calibration,@(df['num_outbound_stops']==3)&(df['outbound_duration'] ==1),-4.807075147 +Calibration,@(df['num_outbound_stops']==3)&(df['outbound_duration'] ==2),-0.127915425 +Calibration,@(df['num_outbound_stops']==3)&(df['outbound_duration'] ==3),0.30556271 +# INBOUND LEG COMPONENTS,, +alternative is invalid if leg time is longer than total tour time,@(df['inbound_duration']>df['duration']).astype(int),-999 +alternative is invalid if leg time>0 yet there is no stop on the leg,@(df['num_inbound_stops']==0)&(df['inbound_duration']>0),-999 +inbound leg time * inbound leg free flow travel time,"@(df['inbound_duration']*do_skims['SOV_TIME', 'MD'])",0.002936 +Average Stop Time,"@np.where(df['num_inbound_stops'] > 0,df['inbound_duration'] / df['num_inbound_stops'],0)",-0.446440 +Calibration,@(df['num_inbound_stops']==1)&(df['inbound_duration'] ==0),-1.927130 +Calibration,@(df['num_inbound_stops']==1)&(df['inbound_duration'] ==1),0.291882 +Calibration,@(df['num_inbound_stops']==2)&(df['inbound_duration'] ==0),-6.934284 +Calibration,@(df['num_inbound_stops']==2)&(df['inbound_duration'] ==1),-1.325881 +Calibration,@(df['num_inbound_stops']==2)&(df['inbound_duration'] ==2),0.479435 +Calibration,@(df['num_inbound_stops']==2)&(df['inbound_duration'] ==3),0.474259 +Calibration,@(df['num_inbound_stops']==3)&(df['inbound_duration'] ==0),-14.253409 +Calibration,@(df['num_inbound_stops']==3)&(df['inbound_duration'] ==1),-8.055671 +Calibration,@(df['num_inbound_stops']==3)&(df['inbound_duration'] ==2),-2.151257 +Calibration,@(df['num_inbound_stops']==3)&(df['inbound_duration'] ==3),0.378101 diff --git a/activitysim/examples/example_mtc/configs/trip_scheduling_choice.yaml b/activitysim/examples/example_mtc/configs/trip_scheduling_choice.yaml new file mode 100644 index 000000000..e4b545a4a --- /dev/null +++ b/activitysim/examples/example_mtc/configs/trip_scheduling_choice.yaml @@ -0,0 +1,19 @@ +METADATA: + CHOOSER: tours + INPUT: + persons: + trips: + tours: + OUTPUT: + trips: + - start_period + - end_period + +SPECIFICATION: trip_scheduling_choice.csv +COEFFICIENTS: trip_scheduling_choice_coeff.csv + +PREPROCESSOR: + SPEC: trip_scheduling_choice_preprocessor + DF: tours + TABLES: + - trips \ No newline at end of file diff --git a/activitysim/examples/example_mtc/configs/trip_scheduling_choice_preprocessor.csv b/activitysim/examples/example_mtc/configs/trip_scheduling_choice_preprocessor.csv new file mode 100644 index 000000000..909d2f725 --- /dev/null +++ b/activitysim/examples/example_mtc/configs/trip_scheduling_choice_preprocessor.csv @@ -0,0 +1,4 @@ +Description,Target,Expression +,tour_outbound_dist,"od_skims['DIST']" +,tour_inbound_dist,"do_skims['DIST']" +,main_leg_dist,"obib_skims['DIST']" diff --git a/docs/abmexample.rst b/docs/abmexample.rst index 4ba741a29..f42e9a293 100644 --- a/docs/abmexample.rst +++ b/docs/abmexample.rst @@ -539,6 +539,11 @@ columns indicates the number of non-mandatory tours by purpose. The current set | | - trip_mode_choice_coeffs.csv | | | - trip_mode_choice.csv | +------------------------------------------------+--------------------------------------------------------------------+ +| :ref:`parking_location_choice` | - parking_location_choice.yaml | +| (Optional Model) | - parking_location_choice_annotate_trips_preprocessor.csv | +| (Not Included in Example Pipeline | - parking_location_choice_coeffs.csv | +| | - parking_location_choice.csv | ++------------------------------------------------+--------------------------------------------------------------------+ | :ref:`write_trip_matrices` | - write_trip_matrices.yaml | | | - write_trip_matrices_annotate_trips_preprocessor.csv | +------------------------------------------------+--------------------------------------------------------------------+ diff --git a/docs/models.rst b/docs/models.rst index b66521b78..b5840774c 100644 --- a/docs/models.rst +++ b/docs/models.rst @@ -707,7 +707,7 @@ Core Table: ``trips`` | Result Field: ``purpose, destination`` | Skims Keys: ``o .. _trip_scheduling: -Trip Scheduling +Trip Scheduling (Probablistic) --------------- For each trip, assign a departure hour based on an input lookup table of percents by tour purpose, @@ -739,6 +739,46 @@ Core Table: ``trips`` | Result Field: ``depart`` | Skims Keys: NA .. _trip_mode_choice: + +Trip Scheduling Choice (Logit Choice) +------------------------------------- +This model uses a logit-based formulation to determine potential trip windows for the three +main components of a tour. + +- Outbound Leg: The time from leaving the origin location to the time second to last outbound stop. +- Main Leg: The time window from the last outbound stop through the main tour destination to the first inbound stop. +- Inbound Leg: The time window from the first inbound stop to the tour origin location. + +Core Table: ``tours`` | Result Field: ``outbound_duration``, ``main_leg_duration``, ``inbound_duration`` | Skims Keys: NA + + +**Required YAML attributes:** + +- ``SPECIFICATION`` + This file defines the logit specification for each chooser segment. +- ``COEFFICIENTS`` + Specification coefficients +- ``PREPROCESSOR``: + Preprocessor definitions to run on the chooser dataframe (trips) before the model is run + + +Trip Departure Choice (Logit Choice) +------------------------------------- +Used in conjuction with Trip Scheduling Choice (Logit Choice), this model chooses departure +time periods consistent with the time windows for the appropriate leg of the trip. + +Core Table: ``trips`` | Result Field: ``depart`` | Skims Keys: NA + +**Required YAML attributes:** + +- ``SPECIFICATION`` + This file defines the logit specification for each chooser segment. +- ``COEFFICIENTS`` + Specification coefficients +- ``PREPROCESSOR``: + Preprocessor definitions to run on the chooser dataframe (trips) before the model is run + + Trip Mode Choice ---------------- @@ -765,6 +805,69 @@ Core Table: ``trips`` | Result Field: ``trip_mode`` | Skims Keys: ``origin, dest .. automodule:: activitysim.abm.models.trip_mode_choice :members: +.. _parking_location_choice: + +Parking Location Choice +----------------------- + +The parking location choice model selects a parking location for specified trips. While the model does not +require parking location be applied to any specific set of trips, it is usually applied for drive trips to +specific zones (e.g., CBD) in the model. + +The model provides provides a filter for both the eligible choosers and eligible parking location zone. The +trips dataframe is the chooser of this model. The zone selection filter is applied to the land use zones +dataframe. + +If this model is specified in the pipeline, the `Write Trip Matrices`_ step will using the parking location +choice results to build trip tables in lieu of the trip destination. + +The main interface to the trip mode choice model is the +:py:func:`~activitysim.abm.models.parking_location_choice.parking_location_choice` function. This function +is registered as an orca step, and it is available from the pipeline. See :ref:`writing_logsums` for how to write +logsums for estimation. + +**Skims** + +- ``odt_skims``: Origin to Destination by Time of Day +- ``dot_skims``: Destination to Origin by Time of Day +- ``opt_skims``: Origin to Parking Zone by Time of Day +- ``pdt_skims``: Parking Zone to Destination by Time of Day +- ``od_skims``: Origin to Destination +- ``do_skims``: Destination to Origin +- ``op_skims``: Origin to Parking Zone +- ``pd_skims``: Parking Zone to Destination + +Core Table: ``trips`` + +**Required YAML attributes:** + +- ``SPECIFICATION`` + This file defines the logit specification for each chooser segment. +- ``COEFFICIENTS`` + Specification coefficients +- ``PREPROCESSOR``: + Preprocessor definitions to run on the chooser dataframe (trips) before the model is run +- ``CHOOSER_FILTER_COLUMN_NAME`` + Boolean field on the chooser table defining which choosers are eligible to parking location choice model. If no + filter is specified, all choosers (trips) are eligible for the model. +- ``CHOOSER_SEGMENT_COLUMN_NAME`` + Column on the chooser table defining the parking segment for the logit model +- ``SEGMENTS`` + List of eligible chooser segments in the logit specification +- ``ALTERNATIVE_FILTER_COLUMN_NAME`` + Boolean field used to filter land use zones as eligible parking location choices. If no filter is specified, + then all land use zones are considered as viable choices. +- ``ALT_DEST_COL_NAME`` + The column name to append with the parking location choice results. For choosers (trips) ineligible for this + model, a -1 value will be placed in column. +- ``TRIP_ORIGIN`` + Origin field on the chooser trip table +- ``TRIP_DESTINATION`` + Destination field on the chooser trip table + +.. automodule:: activitysim.abm.models.parking_location_choice + :members: + .. _write_trip_matrices: Write Trip Matrices @@ -777,6 +880,9 @@ households are read in at the beginning of a model run. The main interface to w matrices is the :py:func:`~activitysim.abm.models.trip_matrices.write_trip_matrices` function. This function is registered as an orca step in the example Pipeline. +If the `Parking Location Choice`_ model is defined in the pipeline, the parking location zone will be used in +lieu of the destination zone. + Core Table: ``trips`` | Result: ``omx trip matrices`` | Skims Keys: ``origin, destination`` .. automodule:: activitysim.abm.models.trip_matrices