Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
11 changes: 11 additions & 0 deletions activitysim/abm/models/park_and_ride_lot_choice.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ class ParkAndRideLotChoiceSettings(LogitComponentSettings, extra="forbid"):
TRACE_PNR_CAPACITIES_PER_ITERATION: bool = True
"""If True, output park-and-ride lot occupancy at each iteration to the trace folder."""

ALLOW_ZERO_PROBS: bool = False
"""If True, return -1 when a chooser has no lot with positive probability."""


def filter_chooser_to_transit_accessible_destinations(
state: workflow.State,
Expand Down Expand Up @@ -206,6 +209,12 @@ def run_park_and_ride_lot_choice(
pnr_alts = land_use[land_use[model_settings.LANDUSE_PNR_SPACES_COLUMN] > 0]
pnr_alts["pnr_zone_id"] = pnr_alts.index.values

if pnr_alts.empty and model_settings.ALLOW_ZERO_PROBS:
logger.info(
"No park-and-ride lots are available. Returning -1 for all choosers."
)
return pd.Series(data=-1, index=choosers.index)

# if we are running with capacitated pnr lots, we need to flag the lots that are over-capacitated
if pnr_capacity_cls is not None:
pnr_alts["pnr_lot_full"] = pnr_capacity_cls.flag_capacitated_pnr_zones(pnr_alts)
Expand Down Expand Up @@ -349,6 +358,8 @@ def run_park_and_ride_lot_choice(
estimator=estimator,
explicit_chunk_size=model_settings.explicit_chunk,
compute_settings=model_settings.compute_settings,
allow_zero_probs=model_settings.ALLOW_ZERO_PROBS,
zero_prob_choice_val=-1,
)

choices = choices.reindex(choosers.index, fill_value=-1)
Expand Down
70 changes: 70 additions & 0 deletions activitysim/abm/models/util/test/test_park_and_ride_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ def model_settings_base():
compute_settings=None,
preprocessor=_EmptyPreprocessor(),
alts_preprocessor=None,
ALLOW_ZERO_PROBS=False,
)


Expand Down Expand Up @@ -329,6 +330,75 @@ def test_run_pnr_handles_non_unique_index(state, network_los, model_settings_bas
assert (choices == 1).all()


@pytest.mark.parametrize("use_explicit_error_terms", [False, True])
def test_run_pnr_allows_zero_probabilities(
state, network_los, model_settings_base, use_explicit_error_terms
):
"""Return -1 only for choosers with no available lot."""
model_settings_base.ALLOW_ZERO_PROBS = True
state.settings.use_explicit_error_terms = use_explicit_error_terms
land_use = pd.DataFrame(
{"pnr_spaces": [10, 10]}, index=pd.Index([1, 2], name="zone_id")
)
choosers = pd.DataFrame(
{
"destination": [1, 5, 2],
"home_zone_id": [3, 3, 3],
"in_period": [1, 1, 1],
},
index=pd.Index([100, 100, 102], name="tour_id"),
)
_write_model_files(
state,
"pnr_zero_prob_spec.csv",
"""
Description,Expression,coefficient
availability,@-1000 * (df.destination != df.pnr_zone_id),coef_one
""",
"pnr_zero_prob_coeffs.csv",
"""
coefficient_name,value,constrain
coef_one,1.0,F
""",
)
model_settings_base.SPEC = "pnr_zero_prob_spec.csv"
model_settings_base.COEFFICIENTS = "pnr_zero_prob_coeffs.csv"

state.get_rn_generator().begin_step("test_run_pnr_allows_zero_probabilities")
choices = pnr_lot_choice.run_park_and_ride_lot_choice(
state=state,
choosers=choosers,
land_use=land_use,
network_los=network_los,
model_settings=model_settings_base,
)
state.get_rn_generator().end_step("test_run_pnr_allows_zero_probabilities")

assert choices.index.tolist() == [100, 100, 102]
assert choices.tolist() == [1, -1, 2]


def test_run_pnr_allows_no_lots(state, network_los, model_settings_base):
model_settings_base.ALLOW_ZERO_PROBS = True
land_use = pd.DataFrame(
{"pnr_spaces": [0, 0]}, index=pd.Index([1, 2], name="zone_id")
)
choosers = pd.DataFrame(
{"destination": [1], "home_zone_id": [3]},
index=pd.Index([100], name="tour_id"),
)

choices = pnr_lot_choice.run_park_and_ride_lot_choice(
state=state,
choosers=choosers,
land_use=land_use,
network_los=network_los,
model_settings=model_settings_base,
)

assert choices.to_dict() == {100: -1}


def test_run_pnr_calculates_drive_transit_and_capacity_utilities(
state, network_los, model_settings_base
):
Expand Down
38 changes: 36 additions & 2 deletions activitysim/core/interaction_simulate.py
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,8 @@ def _interaction_simulate(
estimator=None,
chunk_sizer=None,
compute_settings: ComputeSettings | None = None,
allow_zero_probs=False,
zero_prob_choice_val=None,
):
"""
Run a MNL simulation in the situation in which alternatives must
Expand Down Expand Up @@ -915,8 +917,19 @@ def _interaction_simulate(

if state.settings.use_explicit_error_terms:
utilities = logit.validate_utils(
state, utilities, trace_label=trace_label, trace_choosers=choosers
state,
utilities,
allow_zero_probs=allow_zero_probs,
trace_label=trace_label,
trace_choosers=choosers,
)
if allow_zero_probs:
zero_probs = (
utilities.sum(axis=1) <= utilities.shape[1] * logit.UTIL_UNAVAILABLE
)
if zero_probs.any():
# Give unavailable rows a temporary choice before replacing it below.
utilities.loc[zero_probs, 0] = logit.UTIL_LARGE_ENOUGH
positions, rands = logit.make_choices_utility_based(
state, utilities, trace_label=trace_label, trace_choosers=choosers
)
Expand All @@ -928,7 +941,12 @@ def _interaction_simulate(
# 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(
state, utilities, trace_label=trace_label, trace_choosers=choosers
state,
utilities,
allow_zero_probs=allow_zero_probs,
trace_label=trace_label,
trace_choosers=choosers,
overflow_protection=not allow_zero_probs,
)
chunk_sizer.log_df(trace_label, "probs", probs)

Expand All @@ -942,6 +960,12 @@ def _interaction_simulate(
column_labels=["alternative", "probability"],
)

if allow_zero_probs:
zero_probs = probs.sum(axis=1) == 0
if zero_probs.any():
# Give unavailable rows a temporary choice before replacing it below.
probs.loc[zero_probs, 0] = 1.0

# 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
Expand All @@ -962,6 +986,8 @@ def _interaction_simulate(

# create a series with index from choosers and the index of the chosen alternative
choices = pd.Series(choices, index=choosers.index)
if allow_zero_probs and zero_probs.any() and zero_prob_choice_val is not None:
choices.loc[zero_probs] = zero_prob_choice_val
chunk_sizer.log_df(trace_label, "choices", choices)

if have_trace_targets:
Expand Down Expand Up @@ -993,6 +1019,8 @@ def interaction_simulate(
estimator=None,
explicit_chunk_size=0,
compute_settings: ComputeSettings | None = None,
allow_zero_probs=False,
zero_prob_choice_val=None,
):
"""
Run a simulation in the situation in which alternatives must
Expand Down Expand Up @@ -1035,6 +1063,10 @@ def interaction_simulate(
explicit_chunk_size : float, optional
If > 0, specifies the chunk size to use when chunking the interaction
simulation. If < 1, specifies the fraction of the total number of choosers.
allow_zero_probs : bool, optional
Accept choosers for which every alternative has zero probability.
zero_prob_choice_val : optional
Choice value to return for those choosers when allow_zero_probs is True.

Returns
-------
Expand Down Expand Up @@ -1070,6 +1102,8 @@ def interaction_simulate(
estimator=estimator,
chunk_sizer=chunk_sizer,
compute_settings=compute_settings,
allow_zero_probs=allow_zero_probs,
zero_prob_choice_val=zero_prob_choice_val,
)

result_list.append(choices)
Expand Down
6 changes: 6 additions & 0 deletions docs/dev-guide/components/park_and_ride_lot_choice.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ names must be supplied for the implementing model.
COEFFICIENTS: park_and_ride_lot_choice_coefficients.csv
LANDUSE_PNR_SPACES_COLUMN: pnr_spaces
LANDUSE_COL_FOR_PNR_ELIGIBLE_DEST: pnr_eligible_destination
ALLOW_ZERO_PROBS: true
ITERATE_WITH_TOUR_MODE_CHOICE: false
```

Expand All @@ -76,6 +77,11 @@ This is a destination screen, so the utility specification must still handle
unavailable paths for individual lot/destination pairs. Filtered-out tours
receive `pnr_zone_id = -1`.

Set `ALLOW_ZERO_PROBS: true` to return `pnr_zone_id = -1` when a tour has no
lot with positive choice probability, including when no zones have positive
park-and-ride spaces. The default is `false`. The mode-choice specification
must make park-and-ride modes unavailable for tours with `pnr_zone_id == -1`.

### Skims and Utility Expressions

The following wrappers are available in lot choice and, when `pnr_zone_id` is
Expand Down
Loading