Skip to content
Merged
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
1 change: 1 addition & 0 deletions .github/workflows/core_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ jobs:
matrix:
region:
- prototype_mtc
- prototype_arc
- placeholder_psrc
- prototype_marin
- prototype_mtc_extended
Expand Down
10 changes: 10 additions & 0 deletions activitysim/abm/models/location_choice.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from activitysim.core.exceptions import DuplicateWorkflowTableError
from activitysim.core.interaction_sample import interaction_sample
from activitysim.core.interaction_sample_simulate import interaction_sample_simulate
from activitysim.core.logit import AltsContext
from activitysim.core.util import reindex

"""
Expand Down Expand Up @@ -603,6 +604,7 @@ def run_location_simulate(
chunk_tag,
trace_label,
skip_choice=False,
alts_context: AltsContext | None = None,
):
"""
run location model on location_sample annotated with mode_choice logsum
Expand Down Expand Up @@ -712,6 +714,7 @@ def run_location_simulate(
compute_settings=model_settings.compute_settings.subcomponent_settings(
"simulate"
),
alts_context=alts_context,
)

if not want_logsums:
Expand All @@ -737,6 +740,7 @@ def run_location_choice(
chunk_tag,
trace_label,
skip_choice=False,
alts_context: AltsContext | None = None,
):
"""
Run the three-part location choice algorithm to generate a location choice for each chooser
Expand All @@ -756,6 +760,8 @@ def run_location_choice(
model_settings : dict
chunk_size : int
trace_label : str
skip_choice : bool
alts_context : AltsContext or None

Returns
-------
Expand Down Expand Up @@ -788,6 +794,9 @@ def run_location_choice(
if choosers.shape[0] == 0:
logger.info(f"{trace_label} skipping segment {segment_name}: no choosers")
continue
# dest_size_terms contains 0-attraction zones so using this directly here, important for stable error terms
# when a zone goes from 0 base -> nonzero project
alts_context = AltsContext.from_series(dest_size_terms.index)

# - location_sample
location_sample_df = run_location_sample(
Expand Down Expand Up @@ -841,6 +850,7 @@ def run_location_choice(
trace_label, "simulate.%s" % segment_name
),
skip_choice=skip_choice,
alts_context=alts_context,
)

if estimator:
Expand Down
7 changes: 7 additions & 0 deletions activitysim/abm/models/parking_location_choice.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from activitysim.core.configuration.base import PreprocessorSettings
from activitysim.core.configuration.logit import LogitComponentSettings
from activitysim.core.interaction_sample_simulate import interaction_sample_simulate
from activitysim.core.logit import AltsContext
from activitysim.core.tracing import print_elapsed_time
from activitysim.core.util import assign_in_place, drop_unused_columns
from activitysim.core.exceptions import DuplicateWorkflowTableError
Expand Down Expand Up @@ -112,6 +113,7 @@ def parking_destination_simulate(
chunk_size,
trace_hh_id,
trace_label,
alts_context: AltsContext | None = None,
):
"""
Chose destination from destination_sample (with od_logsum and dp_logsum columns added)
Expand Down Expand Up @@ -150,6 +152,7 @@ def parking_destination_simulate(
trace_label=trace_label,
trace_choice_name="parking_loc",
explicit_chunk_size=model_settings.explicit_chunk,
alts_context=alts_context,
)

# drop any failed zero_prob destinations
Expand Down Expand Up @@ -211,6 +214,9 @@ def choose_parking_location(
)
destination_sample.index = np.repeat(trips.index.values, len(alternatives))
destination_sample.index.name = trips.index.name
# use full land_use index to ensure AltsContext spans full range of potential zones
land_use = state.get_dataframe("land_use")
alts_context = AltsContext.from_series(land_use.index)

destinations = parking_destination_simulate(
state,
Expand All @@ -223,6 +229,7 @@ def choose_parking_location(
chunk_size=chunk_size,
trace_hh_id=trace_hh_id,
trace_label=trace_label,
alts_context=alts_context,
)

if want_sample_table:
Expand Down
9 changes: 8 additions & 1 deletion activitysim/abm/models/trip_destination.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from activitysim.core.configuration.logit import LocationComponentSettings
from activitysim.core.interaction_sample import interaction_sample
from activitysim.core.interaction_sample_simulate import interaction_sample_simulate
from activitysim.core.logit import AltsContext
from activitysim.core.skim_dictionary import DataFrameMatrix
from activitysim.core.tracing import print_elapsed_time
from activitysim.core.util import assign_in_place, reindex
Expand Down Expand Up @@ -950,6 +951,7 @@ def trip_destination_simulate(
skim_hotel,
estimator,
trace_label,
alts_context: AltsContext | None = None,
):
"""
Chose destination from destination_sample (with od_logsum and dp_logsum columns added)
Expand Down Expand Up @@ -1036,6 +1038,7 @@ def trip_destination_simulate(
trace_choice_name="trip_dest",
estimator=estimator,
explicit_chunk_size=model_settings.explicit_chunk,
alts_context=alts_context,
)

if not want_logsums:
Expand Down Expand Up @@ -1080,6 +1083,10 @@ def choose_trip_destination(

t0 = print_elapsed_time()

# use full index (including zero-size zones) to ensure stable random results
# fetch alts_context early so we don't worry about mutating alternatives first
alts_context = AltsContext.from_series(alternatives.index)

# - trip_destination_sample
destination_sample = trip_destination_sample(
state,
Expand Down Expand Up @@ -1126,7 +1133,6 @@ def choose_trip_destination(
destination_sample["dp_logsum"] = 0.0

t0 = print_elapsed_time("%s.compute_logsums" % trace_label, t0, debug=True)

destinations = trip_destination_simulate(
state,
primary_purpose=primary_purpose,
Expand All @@ -1138,6 +1144,7 @@ def choose_trip_destination(
skim_hotel=skim_hotel,
estimator=estimator,
trace_label=trace_label,
alts_context=alts_context,
)

dropped_trips = ~trips.index.isin(destinations.index)
Expand Down
9 changes: 9 additions & 0 deletions activitysim/abm/models/trip_scheduling_choice.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,15 @@ def run_trip_scheduling_choice(
) in chunk.adaptive_chunked_choosers(state, indirect_tours, trace_label):
# Sort the choosers and get the schedule alternatives
choosers = choosers.sort_index()
# FIXME-EET: For explicit error term choices, we need a stable alternative ID. Currently, we use
# SCHEDULE_ID, which justs enumerates all schedule alternatives, of which there are choosers times
# alternative, in the order they are processed, which depends on if there stops on outward/return leg.
# We might want to change SCHEDULE_ID to a fixed pattern of all possible combinations of
# (outbound, main, inbound) duration for the maximum possible tour duration (max time window). For
# 30min intervals, this leads to 1225 alternatives and therefore reasonable memory-wise for random numbers.
# It looks like all that would need to change for this is the generation of the schedule alternatives and
# the lookup of choices as elements in schedule after simulation because choosers are indexed by tour_id.

schedules = generate_schedule_alternatives(choosers).sort_index()

# preprocessing alternatives
Expand Down
6 changes: 6 additions & 0 deletions activitysim/abm/models/util/tour_destination.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from activitysim.core.configuration.logit import TourLocationComponentSettings
from activitysim.core.interaction_sample import interaction_sample
from activitysim.core.interaction_sample_simulate import interaction_sample_simulate
from activitysim.core.logit import AltsContext
from activitysim.core.util import reindex

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -873,6 +874,10 @@ def run_destination_simulate(
state.tracing.dump_df(DUMP, choosers, trace_label, "choosers")

log_alt_losers = state.settings.log_alt_losers
# use full land_use index to ensure AltsContext spans full range of potential destinations
# (maintains stable random number generation even if zones flip zero/non-zero size)
land_use = state.get_dataframe("land_use")
alts_context = AltsContext.from_series(land_use.index)

choices = interaction_sample_simulate(
state,
Expand All @@ -891,6 +896,7 @@ def run_destination_simulate(
estimator=estimator,
skip_choice=skip_choice,
compute_settings=model_settings.compute_settings,
alts_context=alts_context,
)

if not want_logsums:
Expand Down
5 changes: 5 additions & 0 deletions activitysim/abm/models/util/vectorize_tour_scheduling.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from activitysim.core.configuration.base import ComputeSettings, PreprocessorSettings
from activitysim.core.configuration.logit import LogitComponentSettings
from activitysim.core.interaction_sample_simulate import interaction_sample_simulate
from activitysim.core.logit import AltsContext
from activitysim.core.util import reindex

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -849,6 +850,9 @@ def _schedule_tours(
estimator.write_interaction_sample_alternatives(alt_tdd)

log_alt_losers = state.settings.log_alt_losers
# use full TDD alternatives index to ensure AltsContext spans full range of potential slots
tdd_alts = state.get_injectable("tdd_alts")
alts_context = AltsContext.from_series(tdd_alts.index)

choices = interaction_sample_simulate(
state,
Expand All @@ -862,6 +866,7 @@ def _schedule_tours(
trace_label=tour_trace_label,
estimator=estimator,
compute_settings=compute_settings,
alts_context=alts_context,
)
chunk_sizer.log_df(tour_trace_label, "choices", choices)

Expand Down
8 changes: 5 additions & 3 deletions activitysim/abm/test/test_misc/test_trip_scheduling_choice.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import numpy as np
import pandas as pd
import pytest
from __future__ import annotations

import os
from pathlib import Path

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
Expand Down
50 changes: 42 additions & 8 deletions activitysim/core/interaction_sample_simulate.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@

from activitysim.core import chunk, interaction_simulate, logit, tracing, util, workflow
from activitysim.core.configuration.base import ComputeSettings
from activitysim.core.simulate import set_skim_wrapper_targets
from activitysim.core.exceptions import SegmentedSpecificationError
from activitysim.core.logit import AltsContext
from activitysim.core.simulate import set_skim_wrapper_targets

logger = logging.getLogger(__name__)

Expand All @@ -34,6 +35,7 @@ def _interaction_sample_simulate(
*,
chunk_sizer: chunk.ChunkSizer,
compute_settings: ComputeSettings | None = None,
alts_context: AltsContext | None = None,
):
"""
Run a MNL simulation in the situation in which alternatives must
Expand Down Expand Up @@ -220,9 +222,6 @@ def _interaction_sample_simulate(
)
chunk_sizer.log_df(trace_label, "interaction_utilities", interaction_utilities)

del interaction_df
chunk_sizer.log_df(trace_label, "interaction_df", None)

if have_trace_targets:
state.tracing.trace_interaction_eval_results(
trace_eval_results,
Expand Down Expand Up @@ -264,19 +263,29 @@ def _interaction_sample_simulate(

# insert the zero-prob utilities to pad each alternative set to same size
padded_utilities = np.insert(interaction_utilities.utility.values, inserts, -999)
padded_alt_nrs = np.insert(interaction_df[choice_column], inserts, -999)
chunk_sizer.log_df(trace_label, "padded_utilities", padded_utilities)
del inserts

del interaction_utilities
chunk_sizer.log_df(trace_label, "interaction_utilities", None)
del interaction_df
chunk_sizer.log_df(trace_label, "interaction_df", None)

del inserts

# reshape to array with one row per chooser, one column per alternative
padded_utilities = padded_utilities.reshape(-1, max_sample_count)
padded_alt_nrs = padded_alt_nrs.reshape(-1, max_sample_count)

# convert to a dataframe with one row per chooser and one column per alternative
utilities_df = pd.DataFrame(padded_utilities, index=choosers.index)
chunk_sizer.log_df(trace_label, "utilities_df", utilities_df)

# alt_nrs_df has columns for each alt in the choice set, with values indicating which alt_id
# they correspond to (as opposed to the 0-n index implied by the column number).
if alts_context is not None:
alt_nrs_df = pd.DataFrame(padded_alt_nrs, index=choosers.index)
else:
alt_nrs_df = None # if we don't provide the number of dense alternatives, assume that we'll use the old approach

del padded_utilities
chunk_sizer.log_df(trace_label, "padded_utilities", None)

Expand Down Expand Up @@ -320,7 +329,12 @@ def _interaction_sample_simulate(
# positions is series with the chosen alternative represented as a column index in utilities_df
# which is an integer between zero and num alternatives in the alternative sample
positions, rands = logit.make_choices_utility_based(
state, utilities_df, trace_label=trace_label, trace_choosers=choosers
state,
utilities_df,
trace_label=trace_label,
trace_choosers=choosers,
alts_context=alts_context,
alt_nrs_df=alt_nrs_df,
)

del utilities_df
Expand Down Expand Up @@ -451,6 +465,7 @@ def interaction_sample_simulate(
skip_choice=False,
explicit_chunk_size=0,
*,
alts_context: AltsContext | None = None,
compute_settings: ComputeSettings | None = None,
):
"""
Expand Down Expand Up @@ -496,6 +511,12 @@ def interaction_sample_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.
alts_context: AltsContext, optional
Representation of the full alternatives domain (min and max alternative id)
in the absence of sampling.
This is used with EET simulation to ensure consistent random numbers across the whole alternative set
( as the sampled set may change between base and project). When not provided,
EET with integer-coded choice ids will raise an error.

Returns
-------
Expand All @@ -517,6 +538,18 @@ def interaction_sample_simulate(
trace_label = tracing.extend_trace_label(trace_label, "interaction_sample_simulate")
chunk_tag = chunk_tag or trace_label

if state.settings.use_explicit_error_terms:
choice_ids_are_int = pd.api.types.is_integer_dtype(alternatives[choice_column])
if alts_context is None and choice_ids_are_int:
raise ValueError(
"alts_context is required for interaction_sample_simulate when "
"use_explicit_error_terms is True and choice_column is integer-coded"
)
if alts_context is not None and not choice_ids_are_int:
raise ValueError(
"alts_context can only be used with integer-coded choice_column values"
)

result_list = []
for (
i,
Expand Down Expand Up @@ -551,6 +584,7 @@ def interaction_sample_simulate(
skip_choice,
chunk_sizer=chunk_sizer,
compute_settings=compute_settings,
alts_context=alts_context,
)

result_list.append(choices)
Expand Down
Loading
Loading