From c0d1be096da9d736cf2bb556033bd2ec16bc2a8f Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Thu, 23 Mar 2023 09:49:55 -0500 Subject: [PATCH 1/3] trip scheduling logic_version --- activitysim/abm/models/trip_scheduling.py | 58 ++++++++++++++++------- docs/models.rst | 13 ++++- 2 files changed, 54 insertions(+), 17 deletions(-) diff --git a/activitysim/abm/models/trip_scheduling.py b/activitysim/abm/models/trip_scheduling.py index fdf1b1076..48ff7791c 100644 --- a/activitysim/abm/models/trip_scheduling.py +++ b/activitysim/abm/models/trip_scheduling.py @@ -1,6 +1,7 @@ # ActivitySim # See full license in LICENSE.txt. import logging +import warnings from builtins import range import numpy as np @@ -8,11 +9,11 @@ from activitysim.abm.models.util import estimation from activitysim.abm.models.util.trip import cleanup_failed_trips, failed_trip_cohorts -from activitysim.core import chunk, config, inject, pipeline, tracing, expressions +from activitysim.core import chunk, config, expressions, inject, pipeline, tracing from activitysim.core.util import reindex -from .util.school_escort_tours_trips import split_out_school_escorting_trips from .util import probabilistic_scheduling as ps +from .util.school_escort_tours_trips import split_out_school_escorting_trips logger = logging.getLogger(__name__) @@ -47,6 +48,22 @@ PROBS_JOIN_COLUMNS_RELATIVE_BASED = ["outbound", "periods_left"] +def _logic_version(model_settings): + logic_version = model_settings.get("logic_version", None) + if logic_version is None: + warnings.warn( + "The trip_scheduling component now has a logic_version setting " + "to control how the scheduling rules are applied. The default " + "logic_version is currently set at `1` but may be moved up in " + "the future. Explicitly set `logic_version` to 2 in the model " + "settings to upgrade your model logic now, or set it to 1 to " + "suppress this message.", + FutureWarning, + ) + logic_version = 1 + return logic_version + + def set_tour_hour(trips, tours): """ add columns 'tour_hour', 'earliest', 'latest' to trips @@ -108,7 +125,7 @@ def set_stop_num(trips): trips["stop_num"] = trips.stop_num.where(trips["outbound"], trips["trip_num"]) -def update_tour_earliest(trips, outbound_choices): +def update_tour_earliest(trips, outbound_choices, logic_version: int): """ Updates "earliest" column for inbound trips based on the maximum outbound trip departure time of the tour. @@ -121,6 +138,14 @@ def update_tour_earliest(trips, outbound_choices): outbound_choices: pd.Series time periods depart choices, one per trip (except for trips with zero probs) + logic_version : int + Logic version 1 is the original ActivitySim implementation, which + sets the "earliest" value to the max outbound departure for all + inbound trips, regardless of what that max outbound departure value + is (even if it is NA). Logic version 2 introduces a change whereby + that assignment is only made if the max outbound departure value is + not NA. + Returns ------- modifies trips in place @@ -145,11 +170,18 @@ def update_tour_earliest(trips, outbound_choices): # set the trips "earliest" column equal to the max outbound departure # time for all inbound trips. preserve values that were used for outbound trips # FIXME - extra logic added because max_outbound_departure can be NA if previous failed trip was removed - tmp_trips["earliest"] = np.where( - ~tmp_trips["outbound"] & ~tmp_trips["max_outbound_departure"].isna(), - tmp_trips["max_outbound_departure"], - tmp_trips["earliest"], - ) + if logic_version == 1: + tmp_trips["earliest"] = tmp_trips["earliest"].where( + tmp_trips["outbound"], tmp_trips["max_outbound_departure"] + ) + elif logic_version > 1: + tmp_trips["earliest"] = np.where( + ~tmp_trips["outbound"] & ~tmp_trips["max_outbound_departure"].isna(), + tmp_trips["max_outbound_departure"], + tmp_trips["earliest"], + ) + else: + raise ValueError(f"bad logic_version: {logic_version}") trips["earliest"] = tmp_trips["earliest"].reindex(trips.index) @@ -248,7 +280,6 @@ def schedule_trips_in_leg( first_trip_in_leg = True for i in range(trips.trip_num.min(), trips.trip_num.max() + 1): - nth_trace_label = tracing.extend_trace_label(trace_label, "num_%s" % i) # - annotate trips @@ -296,7 +327,7 @@ def schedule_trips_in_leg( # choices are relative to the previous departure time choices = nth_trips.earliest + choices # need to update the departure time based on the choice - update_tour_earliest(trips, choices) + update_tour_earliest(trips, choices, _logic_version(model_settings)) # adjust allowed depart range of next trip has_next_trip = nth_trips.next_trip_id != NO_TRIP_ID @@ -332,7 +363,6 @@ def run_trip_scheduling( trace_hh_id, trace_label, ): - set_tour_hour(trips_chunk, tours) set_stop_num(trips_chunk) @@ -361,7 +391,7 @@ def run_trip_scheduling( # departure time of last outbound trips must constrain # departure times for initial inbound trips - update_tour_earliest(trips_chunk, choices) + update_tour_earliest(trips_chunk, choices, _logic_version(model_settings)) if (~trips_chunk.outbound).any(): leg_chunk = trips_chunk[~trips_chunk.outbound] @@ -386,7 +416,6 @@ def run_trip_scheduling( @inject.step() def trip_scheduling(trips, tours, chunk_size, trace_hh_id): - """ Trip scheduling assigns depart times for trips within the start, end limits of the tour. @@ -498,13 +527,10 @@ def trip_scheduling(trips, tours, chunk_size, trace_hh_id): ) in chunk.adaptive_chunked_choosers_by_chunk_id( trips_df, chunk_size, trace_label, trace_label ): - i = 0 while (i < max_iterations) and not trips_chunk.empty: - # only chunk log first iteration since memory use declines with each iteration with chunk.chunk_log(trace_label) if i == 0 else chunk.chunk_log_skip(): - i += 1 is_last_iteration = i == max_iterations diff --git a/docs/models.rst b/docs/models.rst index ca2c1122a..7ac25776f 100644 --- a/docs/models.rst +++ b/docs/models.rst @@ -1232,7 +1232,7 @@ The trip scheduling model does not use mode choice logsums. Alternatives: Available time periods in the tour window (i.e. tour start and end period). When processing stops on work tours, the available time periods is constrained by the at-work subtour start and end period as well. -In order to avoid trip failing, a new probabalisitic trip scheduling mode was developed named "relative". +In order to avoid trip failing, a new probabilistic trip scheduling mode was developed named "relative". When setting the _scheduling_mode_ option to relative, trips are scheduled relative to the previously scheduled trips. The first trip still departs when the tour starts and for every subsequet trip, the choices are selected with respect to the previous trip depart time. Inbound trips are no longer handled in reverse order. The key to this relative mode is to @@ -1251,6 +1251,17 @@ scheduling probabilities are indexed by the following columns: Each of these variables are listed as merge columns in the trip_scheduling.yaml file and are declared in the trip scheduling preprocessor. The variables above attempt to balance the statistics available for probability creation with the amount of segmentation of trip characteristics. +Earlier versions of ActivitySim contained a logic error in this model, whereby +the earliest departure time for inbound legs was bounded by the maximum outbound +departure time, even if there was a scheduling failure and one or more outbound +leg departures and that bound was NA. For continuity, this process has been +retained in this ActivitySim component as *logic_version* 1, and it remains the +default process if the user does not explicitly specify a logic version in the +model settings yaml file. The revised logic includes bounding inbound legs only +when the maximum outbound departure time is well defined. This version of the +model can be used by explicitly setting `logic_version: 2` (or greater) in the +model settings yaml file. + The main interface to the trip scheduling model is the :py:func:`~activitysim.abm.models.trip_scheduling.trip_scheduling` function. This function is registered as an Inject step in the example Pipeline. From 56596f1f0d140e2a083533621b51b3544a8fe797 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Mon, 27 Mar 2023 14:31:45 -0500 Subject: [PATCH 2/3] in testing ignore the warning we just added --- activitysim/core/config.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/activitysim/core/config.py b/activitysim/core/config.py index 7024d0512..aaa7ed2fb 100644 --- a/activitysim/core/config.py +++ b/activitysim/core/config.py @@ -750,6 +750,14 @@ def filter_warnings(): else: warnings.filterwarnings("default", category=CacheMissWarning) + # beginning from PR #660 (after 1.2.0), a FutureWarning is emitted when the trip + # scheduling component lacks a logic_version setting + warnings.filterwarnings( + "ignore", + category=FutureWarning, + message="The trip_scheduling component now has a logic_version setting.*", + ) + def handle_standard_args(parser=None): From 9aa8dd9709282854a7cbd762d904936804ddc29f Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Mon, 27 Mar 2023 15:46:56 -0500 Subject: [PATCH 3/3] cannot use logic_version 1 with relative scheduling --- activitysim/abm/models/trip_scheduling.py | 7 ++++- docs/models.rst | 33 +++++++++++++---------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/activitysim/abm/models/trip_scheduling.py b/activitysim/abm/models/trip_scheduling.py index 48ff7791c..54c3eb201 100644 --- a/activitysim/abm/models/trip_scheduling.py +++ b/activitysim/abm/models/trip_scheduling.py @@ -327,7 +327,12 @@ def schedule_trips_in_leg( # choices are relative to the previous departure time choices = nth_trips.earliest + choices # need to update the departure time based on the choice - update_tour_earliest(trips, choices, _logic_version(model_settings)) + logic_version = _logic_version(model_settings) + if logic_version == 1: + raise ValueError( + "cannot use logic version 1 with 'relative' scheduling mode" + ) + update_tour_earliest(trips, choices, logic_version) # adjust allowed depart range of next trip has_next_trip = nth_trips.next_trip_id != NO_TRIP_ID diff --git a/docs/models.rst b/docs/models.rst index 7ac25776f..d7249a353 100644 --- a/docs/models.rst +++ b/docs/models.rst @@ -1211,8 +1211,8 @@ Core Table: ``trips`` | Result Field: ``purpose, destination`` | Skims Keys: ``o .. _trip_scheduling: -Trip Scheduling (Probablistic) ------------------------------- +Trip Scheduling (Probabilistic) +------------------------------- For each trip, assign a departure hour based on an input lookup table of percents by tour purpose, direction (inbound/outbound), tour hour, and trip index. @@ -1234,12 +1234,13 @@ work tours, the available time periods is constrained by the at-work subtour sta In order to avoid trip failing, a new probabilistic trip scheduling mode was developed named "relative". When setting the _scheduling_mode_ option to relative, trips are scheduled relative to the previously scheduled trips. -The first trip still departs when the tour starts and for every subsequet trip, the choices are selected with respect to +The first trip still departs when the tour starts and for every subsequent trip, the choices are selected with respect to the previous trip depart time. Inbound trips are no longer handled in reverse order. The key to this relative mode is to index the probabilities based on how much time is remaining on the tour. For tours that include subtours, the time remaining will be based on the subtour start time for outbound trips and will resume again for inbound trips after the subtour ends. By indexing the probabilities based on time remaining and scheduling relative to the previous trip, scheduling trips in relative -mode will not fail. +mode will not fail. Note also that relative scheduling mode requires the use of logic +version 2 (see warning about logic versions, below). An example of trip scheduling in relative mode is included in the :ref:`prototype_mwcog` example. In this example, trip scheduling probabilities are indexed by the following columns: @@ -1251,16 +1252,20 @@ scheduling probabilities are indexed by the following columns: Each of these variables are listed as merge columns in the trip_scheduling.yaml file and are declared in the trip scheduling preprocessor. The variables above attempt to balance the statistics available for probability creation with the amount of segmentation of trip characteristics. -Earlier versions of ActivitySim contained a logic error in this model, whereby -the earliest departure time for inbound legs was bounded by the maximum outbound -departure time, even if there was a scheduling failure and one or more outbound -leg departures and that bound was NA. For continuity, this process has been -retained in this ActivitySim component as *logic_version* 1, and it remains the -default process if the user does not explicitly specify a logic version in the -model settings yaml file. The revised logic includes bounding inbound legs only -when the maximum outbound departure time is well defined. This version of the -model can be used by explicitly setting `logic_version: 2` (or greater) in the -model settings yaml file. +.. warning:: + + Earlier versions of ActivitySim contained a logic error in this model, whereby + the earliest departure time for inbound legs was bounded by the maximum outbound + departure time, even if there was a scheduling failure for one or more outbound + leg departures and that bound was NA. For continuity, this process has been + retained in this ActivitySim component as *logic_version* 1, and it remains the + default process if the user does not explicitly specify a logic version in the + model settings yaml file. The revised logic includes bounding inbound legs only + when the maximum outbound departure time is well defined. This version of the + model can be used by explicitly setting `logic_version: 2` (or greater) in the + model settings yaml file. It is strongly recommended that all new model + development efforts use logic version 2; a future version of ActivitySim may + make this the default for this component, and/or remove logic version 1 entirely. The main interface to the trip scheduling model is the :py:func:`~activitysim.abm.models.trip_scheduling.trip_scheduling` function.