Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trip scheduling logic #660

Merged
merged 3 commits into from
Mar 28, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 42 additions & 16 deletions activitysim/abm/models/trip_scheduling.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
# ActivitySim
# See full license in LICENSE.txt.
import logging
import warnings
from builtins import range

import numpy as np
import pandas as pd

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__)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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}")
jpn-- marked this conversation as resolved.
Show resolved Hide resolved

trips["earliest"] = tmp_trips["earliest"].reindex(trips.index)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -332,7 +363,6 @@ def run_trip_scheduling(
trace_hh_id,
trace_label,
):

set_tour_hour(trips_chunk, tours)
set_stop_num(trips_chunk)

Expand Down Expand Up @@ -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]
Expand All @@ -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.

Expand Down Expand Up @@ -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

Expand Down
8 changes: 8 additions & 0 deletions activitysim/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Expand Down
13 changes: 12 additions & 1 deletion docs/models.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down