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
3 changes: 2 additions & 1 deletion activitysim/core/interaction_simulate.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ def eval_interaction_utilities(
assert len(spec.columns) == 1

# avoid altering caller's passed-in locals_d parameter (they may be looping)
locals_d = locals_d.copy() if locals_d is not None else {}
# global constants are always available, but can be overridden by locals_d
locals_d = {**state.get_global_constants(), **(locals_d or {})}

utilities = None

Expand Down
60 changes: 59 additions & 1 deletion activitysim/core/test/test_interaction_sample_simulate.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import pandas as pd
import pytest

from activitysim.core import interaction_sample_simulate, workflow
from activitysim.core import interaction_sample, interaction_sample_simulate, workflow
from activitysim.core.logit import AltsContext


Expand All @@ -18,6 +18,64 @@ def state() -> workflow.State:
return state


def test_global_constants_available_in_sampling_and_simulation(tmp_path):
"""Global constants are available in both destination-choice substeps."""
configs_dir = tmp_path.joinpath("configs")
configs_dir.mkdir()
configs_dir.joinpath("constants.yaml").write_text(
"SAMPLE_SCALE: 3.0\nSIMULATE_SCALE: 2.0\n"
)
tmp_path.joinpath("data").mkdir()

state = workflow.State()
state.initialize_filesystem(
working_dir=tmp_path, configs_dir=("configs",)
).default_settings()
state.settings.check_for_variability = False

choosers = pd.DataFrame(
{"chooser_attr": [1.0, 2.0]},
index=pd.Index([0, 1], name="person_id"),
)
alternatives = pd.DataFrame(
{"alt_attr": [1.0, 2.0]},
index=pd.Index([10, 20], name="alt_id"),
)

# Sampling and simulation use separate specifications in location and
# destination choice, so exercise each expression-evaluation path.
sample_spec = pd.DataFrame(
{"coefficient": [1.0]},
index=pd.Index(["alt_attr * SAMPLE_SCALE"], name="Expression"),
)
sample = interaction_sample.interaction_sample(
state,
choosers,
alternatives,
sample_spec,
sample_size=0,
alt_col_name="alt_id",
)
sampled_alternatives = sample.join(alternatives, on="alt_id")

simulate_spec = pd.DataFrame(
{"coefficient": [1.0]},
index=pd.Index(["alt_attr * SIMULATE_SCALE"], name="Expression"),
)
results = interaction_sample_simulate.interaction_sample_simulate(
state,
choosers,
sampled_alternatives,
simulate_spec,
choice_column="alt_id",
want_logsums=True,
skip_choice=True,
)

expected_logsum = np.logaddexp(2.0, 4.0)
np.testing.assert_allclose(results["logsums"], expected_logsum)


def test_interaction_sample_simulate_parity(state):
# Run interaction_sample_simulate with and without explicit error terms and check that results are similar.

Expand Down
72 changes: 72 additions & 0 deletions activitysim/core/test/test_interaction_simulate.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# ActivitySim
# See full license in LICENSE.txt.

from __future__ import annotations

import numpy as np
import pandas as pd
import pytest
Expand Down Expand Up @@ -172,3 +174,73 @@ def test_interaction_simulate_eet_large_utilities(state):
assert not choices_eet.isna().any()
# With such a large difference, Alt 1 should be the dominant choice
assert (choices_eet == 1).all()


def test_eval_interaction_utilities_global_constants(tmp_path):
# global constants (from constants.yaml) should be available to expressions
# evaluated for interaction models (e.g. location choice, destination choice,
# tour scheduling), see issue #1015

configs_dir = tmp_path.joinpath("configs")
configs_dir.mkdir()
configs_dir.joinpath("constants.yaml").write_text("KM_TO_MILE: 0.621371\n")
tmp_path.joinpath("data").mkdir()

state = workflow.State()
state.initialize_filesystem(
working_dir=tmp_path, configs_dir=("configs",)
).default_settings()
state.settings.check_for_variability = False

df = pd.DataFrame({"distance_km": [1.0, 10.0]}, index=[0, 1])

spec = pd.DataFrame(
{"coefficient": [1.0]},
index=pd.Index(["distance_km * KM_TO_MILE"], name="Expression"),
)

utilities, _ = interaction_simulate.eval_interaction_utilities(
state,
spec,
df,
locals_d=None,
trace_label="test_global_constants",
trace_rows=None,
)

np.testing.assert_allclose(
utilities.utility.to_numpy(), df.distance_km.to_numpy() * 0.621371
)


def test_eval_interaction_utilities_locals_override_global_constants(tmp_path):
# values passed in locals_d take precedence over global constants

configs_dir = tmp_path.joinpath("configs")
configs_dir.mkdir()
configs_dir.joinpath("constants.yaml").write_text("KM_TO_MILE: 0.621371\n")
tmp_path.joinpath("data").mkdir()

state = workflow.State()
state.initialize_filesystem(
working_dir=tmp_path, configs_dir=("configs",)
).default_settings()
state.settings.check_for_variability = False

df = pd.DataFrame({"distance_km": [1.0, 10.0]}, index=[0, 1])

spec = pd.DataFrame(
{"coefficient": [1.0]},
index=pd.Index(["distance_km * KM_TO_MILE"], name="Expression"),
)

utilities, _ = interaction_simulate.eval_interaction_utilities(
state,
spec,
df,
locals_d={"KM_TO_MILE": 1.0},
trace_label="test_global_constants_override",
trace_rows=None,
)

np.testing.assert_allclose(utilities.utility.to_numpy(), df.distance_km.to_numpy())
Loading