diff --git a/activitysim/abm/models/trip_scheduling_choice.py b/activitysim/abm/models/trip_scheduling_choice.py index 510d4ece8d..81d908ef1b 100644 --- a/activitysim/abm/models/trip_scheduling_choice.py +++ b/activitysim/abm/models/trip_scheduling_choice.py @@ -18,6 +18,7 @@ PreprocessorSettings, PydanticReadable, ) +from activitysim.core.configuration.logit import LogitComponentSettings from activitysim.core.interaction_sample_simulate import _interaction_sample_simulate from activitysim.core.skim_dataset import SkimDataset from activitysim.core.skim_dictionary import SkimDict @@ -81,6 +82,8 @@ def generate_schedule_alternatives(tours): schedules = pd.concat([no_stops, one_way, two_way], sort=True) schedules[SCHEDULE_ID] = np.arange(1, schedules.shape[0] + 1) + # this sort is necessary to keep single process and multiprocess results the same! + schedules.sort_values(by=["tour_id", SCHEDULE_ID], inplace=True) return schedules @@ -207,9 +210,7 @@ def get_spec_for_segment( :return: array of utility equations """ - omnibus_spec = state.filesystem.read_model_spec( - file_name=model_settings.SPECIFICATION - ) + omnibus_spec = state.filesystem.read_model_spec(file_name=model_settings.SPEC) spec = omnibus_spec[[segment]] @@ -344,21 +345,12 @@ def run_trip_scheduling_choice( return tours -class TripSchedulingChoiceSettings(PydanticReadable, extra="forbid"): +class TripSchedulingChoiceSettings(LogitComponentSettings, extra="forbid"): """ Settings for the `trip_scheduling_choice` component. """ - PREPROCESSOR: PreprocessorSettings | None = None - """Setting for the preprocessor.""" - alts_preprocessor: PreprocessorSettings | None = None - """Setting for the alternatives preprocessor.""" - - SPECIFICATION: str - """file name of specification file""" - - compute_settings: ComputeSettings = ComputeSettings() - """Compute settings for this component.""" + pass @workflow.step diff --git a/activitysim/abm/test/test_misc/configs_test_misc/network_los.yaml b/activitysim/abm/test/test_misc/configs_test_misc/network_los.yaml new file mode 100644 index 0000000000..391125a38e --- /dev/null +++ b/activitysim/abm/test/test_misc/configs_test_misc/network_los.yaml @@ -0,0 +1,14 @@ +# read cached skims (using numpy memmap) from output directory (memmap is faster than omx ) +read_skim_cache: False +# write memmapped cached skims to output directory after reading from omx, for use in subsequent runs +write_skim_cache: True + +zone_system: 1 + +taz_skims: skims.omx + +skim_time_periods: + time_window: 1440 + period_minutes: 60 + periods: [0, 3, 5, 9, 14, 18, 24] # 3=3:00-3:59, 5=5:00-5:59, 9=9:00-9:59, 14=2:00-2:59, 18=6:00-6:59 + labels: ['EA', 'EA', 'AM', 'MD', 'PM', 'EV'] \ No newline at end of file diff --git a/activitysim/abm/test/test_misc/configs_test_misc/settings_60_min.yaml b/activitysim/abm/test/test_misc/configs_test_misc/settings_60_min.yaml new file mode 100644 index 0000000000..a9ca5a0816 --- /dev/null +++ b/activitysim/abm/test/test_misc/configs_test_misc/settings_60_min.yaml @@ -0,0 +1,9 @@ +zone_system: 1 + +taz_skims: z1_taz_skims.omx + +skim_time_periods: + time_window: 1440 + period_minutes: 60 + periods: [0, 6, 11, 16, 20, 24] + labels: ['EA', 'AM', 'MD', 'PM', 'EV'] diff --git a/activitysim/abm/test/test_misc/data/z1_taz_skims.omx b/activitysim/abm/test/test_misc/data/z1_taz_skims.omx new file mode 100644 index 0000000000..18c6dc4408 Binary files /dev/null and b/activitysim/abm/test/test_misc/data/z1_taz_skims.omx differ diff --git a/activitysim/abm/test/test_misc/test_trip_departure_choice.py b/activitysim/abm/test/test_misc/test_trip_departure_choice.py index 6d462c0bd0..94d47f57ac 100644 --- a/activitysim/abm/test/test_misc/test_trip_departure_choice.py +++ b/activitysim/abm/test/test_misc/test_trip_departure_choice.py @@ -1,152 +1,189 @@ -# 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 .setup_utils 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(state, model_spec, trips): -# setup_dirs() -# departures = tdc.apply_stage_two_model( -# state, 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) +import numpy as np +import pandas as pd +import pytest +import os + +import activitysim.abm.models.trip_departure_choice as tdc +from activitysim.abm.models.util.trip import get_time_windows +from activitysim.core import workflow + +from .setup_utils import setup_dirs + + +@pytest.fixture(scope="module") +def trips(): + 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": [ + True, + True, + False, + False, + False, + True, + True, + False, + False, + True, + ], + "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"]}, + } + + +def add_canonical_dirs(configs_dir_name): + state = workflow.State() + configs_dir = os.path.join(os.path.dirname(__file__), f"{configs_dir_name}") + data_dir = os.path.join(os.path.dirname(__file__), f"data") + output_dir = os.path.join(os.path.dirname(__file__), "output") + state.initialize_filesystem( + working_dir=os.path.dirname(__file__), + configs_dir=(configs_dir,), + output_dir=output_dir, + data_dir=(data_dir,), + ) + return state + + +@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() + state = add_canonical_dirs("configs_test_misc").default_settings() + + # A settings object is needed to pass to the model application function, + # but for testing we can just use the default settings. + # In non-testing use cases, the SPEC would actually be read from the yaml file + # instead of being passed directly as a dataframe. + model_settings = tdc.TripDepartureChoiceSettings() + + departures = tdc.apply_stage_two_model( + state, + model_spec, + trips, + 0, + "TEST Trip Departure", + model_settings=model_settings, + ) + 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_misc/test_trip_scheduling_choice.py b/activitysim/abm/test/test_misc/test_trip_scheduling_choice.py index 24fdebde3e..6823a5b123 100644 --- a/activitysim/abm/test/test_misc/test_trip_scheduling_choice.py +++ b/activitysim/abm/test/test_misc/test_trip_scheduling_choice.py @@ -1,199 +1,287 @@ -# 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, workflow -# -# from .setup_utils 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( -# state: workflow.State, model_spec, tours, skims, locals_dict -# ): -# """ -# Test run the model. -# """ -# -# out_tours = tsc.run_trip_scheduling_choice( -# state, -# model_spec, -# tours, -# skims, -# locals_dict, -# trace_label="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) +import numpy as np +import pandas as pd +import pytest +import os +from pathlib import Path + + +from activitysim.abm.models import trip_scheduling_choice as tsc +from activitysim.abm.tables.skims import skim_dict +from activitysim.core import los, workflow + + +@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 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"]).rename_axis( + "Expression" + ) + + +def add_canonical_dirs(configs_dir_name): + state = workflow.State() + configs_dir = os.path.join(os.path.dirname(__file__), f"{configs_dir_name}") + data_dir = os.path.join(os.path.dirname(__file__), "data") + output_dir = os.path.join(os.path.dirname(__file__), "output") + state.initialize_filesystem( + working_dir=os.path.dirname(__file__), + configs_dir=(configs_dir,), + output_dir=output_dir, + data_dir=(data_dir,), + ) + return state + + +@pytest.fixture(scope="module") +def skims(): + state = add_canonical_dirs("configs_test_misc").default_settings() + nw_los = los.Network_LOS(state, los_settings_file_name="settings_60_min.yaml") + nw_los.load_data() + skim_d = skim_dict(state, 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]} + + +@pytest.fixture(scope="module") +def base_dir() -> Path: + """ + A pytest fixture that returns the data folder location. + :return: folder location for any necessary data to initialize the tests + """ + return Path(__file__).parent + + +@pytest.fixture(scope="module") +def module() -> str: + """ + A pytest fixture that returns the module name string used in test setup. + :return: module name string ("summarize") + """ + return "summarize" + + +# Used by conftest.py initialize_pipeline method +@pytest.fixture(scope="module") +def tables() -> dict[str, str]: + """ + A pytest fixture that returns the "mock" tables to build pipeline dataframes. The + key-value pair is the name of the table and the index column. + :return: dict + """ + return { + "land_use": "zone_id", + "tours": "tour_id", + "trips": "trip_id", + "persons": "person_id", + "households": "household_id", + } + + +# Used by conftest.py initialize_pipeline method +# Set to true if you need to read skims into the pipeline +@pytest.fixture(scope="module") +def initialize_network_los() -> bool: + """ + A pytest boolean fixture indicating whether network skims should be read from the + fixtures test data folder. + :return: bool + """ + return True + + +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): + # create a temporary workflow state with no content + state = workflow.State.make_temp() + + # Define model settings for this test. + # The settings for this model requires a filename for the spec, but in this test we + # are passing the spec dataframe directly, so the filename is just a placeholder. + # In non-testing use cases, the SPEC would actually be read from the yaml file + # instead of being passed directly as a dataframe. + model_settings = tsc.TripSchedulingChoiceSettings( + **{ + "SPEC": "placeholder.csv", + "compute_settings": { + "protect_columns": ["origin", "destination", "schedule_id"] + }, + } + ) + + # As is common in ActivitySim the component will modify the input dataframe in-place. + # For testing we make a copy of the input tours to compare against after running the model. + in_tours = tours.copy(deep=True) + + # run the trip scheduling choice model + out_tours = tsc.run_trip_scheduling_choice( + state, + model_spec, + tours, + skims, + locals_dict, + trace_label="PyTest Trip Scheduling", + model_settings=model_settings, + ) + + # check that the number of tours is unchanged + assert len(in_tours) == len(out_tours) + pd.testing.assert_index_equal( + in_tours.sort_index().index, out_tours.sort_index().index + ) + + # check that the expected output columns are not present in input tours + output_columns = [tsc.MAIN_LEG_DURATION, tsc.OB_DURATION, tsc.IB_DURATION] + for col in output_columns: + assert col not in in_tours.columns + + # check that the expected output columns *are* present in output tours + assert set(output_columns).issubset(out_tours.columns) + + # check that the sum of the output durations equals the tour duration + assert len( + out_tours[ + out_tours[output_columns].sum(axis=1) == out_tours[tsc.TOUR_DURATION_COLUMN] + ] + ) == len(in_tours) + + # check that tours with no outbound stops have zero outbound duration + assert out_tours[tsc.OB_DURATION].mask(in_tours[tsc.HAS_OB_STOPS], 0).sum() == 0 + + # check that tours with no inbound stops have zero inbound duration + assert out_tours[tsc.IB_DURATION].mask(in_tours[tsc.HAS_IB_STOPS], 0).sum() == 0 diff --git a/activitysim/core/test/test_util.py b/activitysim/core/test/test_util.py index 940c9a081d..ffb9e1ad04 100644 --- a/activitysim/core/test/test_util.py +++ b/activitysim/core/test/test_util.py @@ -67,7 +67,9 @@ def test_df_from_dict(): df = pd.DataFrame({"attrib": [1, 2, 2, 3, 1]}, index=index) # scramble index order for one expression and not the other - sorted = df.eval("attrib.sort_values()") + # use mergesort to ensure stable sort, if not specified the default is to + # use quicksort which is unstable and will cause this test to fail intermittently + sorted = df.eval("attrib.sort_values(kind='mergesort')") not_sorted = df.eval("attrib * 1") # check above expressions diff --git a/activitysim/examples/prototype_arc/configs/settings.yaml b/activitysim/examples/prototype_arc/configs/settings.yaml index 6ff25b94e0..185f67a103 100644 --- a/activitysim/examples/prototype_arc/configs/settings.yaml +++ b/activitysim/examples/prototype_arc/configs/settings.yaml @@ -98,6 +98,10 @@ input_table_list: # assume enough RAM to not chunk chunk_training_mode: disabled +households_sample_size: 100 + +check_model_settings: False + models: - initialize_landuse - initialize_households @@ -129,14 +133,15 @@ models: - trip_scheduling_choice - trip_departure_choice - trip_mode_choice - - parking_location +# - parking_location - write_data_dictionary - track_skim_usage - write_trip_matrices - write_tables -# resume_after: +#resume_after: trip_purpose_and_destination +num_processes: 2 multiprocess: False fail_fast: True diff --git a/activitysim/examples/prototype_arc/configs/trip_scheduling_choice.yaml b/activitysim/examples/prototype_arc/configs/trip_scheduling_choice.yaml index 3d1231f0a9..a0e4f1ee75 100644 --- a/activitysim/examples/prototype_arc/configs/trip_scheduling_choice.yaml +++ b/activitysim/examples/prototype_arc/configs/trip_scheduling_choice.yaml @@ -9,12 +9,16 @@ # - start_period # - end_period -SPECIFICATION: trip_scheduling_choice.csv +SPEC: trip_scheduling_choice.csv #COEFFICIENTS: trip_scheduling_choice_coeff.csv #SAMPLE_ALTERNATIVES: trip_departure_sample_patterns.csv -PREPROCESSOR: +compute_settings: + protect_columns: + - schedule_id + +preprocessor: SPEC: trip_scheduling_choice_preprocessor DF: tours TABLES: diff --git a/activitysim/examples/prototype_arc/test/output/.gitignore b/activitysim/examples/prototype_arc/test/output/.gitignore deleted file mode 100644 index bf5bf15e3e..0000000000 --- a/activitysim/examples/prototype_arc/test/output/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -*.csv -*.log -*.prof -*.h5 -*.txt -*.yaml -*.omx diff --git a/activitysim/examples/prototype_arc/test/output/cache/.gitignore b/activitysim/examples/prototype_arc/test/output/cache/.gitignore deleted file mode 100644 index 1d085cacc9..0000000000 --- a/activitysim/examples/prototype_arc/test/output/cache/.gitignore +++ /dev/null @@ -1 +0,0 @@ -** diff --git a/activitysim/examples/prototype_arc/test/output/trace/.gitignore b/activitysim/examples/prototype_arc/test/output/trace/.gitignore deleted file mode 100644 index 8edb806780..0000000000 --- a/activitysim/examples/prototype_arc/test/output/trace/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.csv -*.log -*.txt diff --git a/activitysim/examples/prototype_arc/test/simulation.py b/activitysim/examples/prototype_arc/test/simulation.py old mode 100755 new mode 100644