From 1994bab54f40c1d36c381411eb4a5dd4d95494a9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Jul 2026 21:06:38 +0000 Subject: [PATCH 1/4] Initial plan From 09d5748aa1e036767748b49c4e1136551c8a8ee3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Jul 2026 21:12:32 +0000 Subject: [PATCH 2/4] Make global constants available in interaction utility expressions --- activitysim/core/interaction_simulate.py | 3 +- .../core/test/test_interaction_simulate.py | 72 +++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/activitysim/core/interaction_simulate.py b/activitysim/core/interaction_simulate.py index bb3213498b..b06e5a8130 100644 --- a/activitysim/core/interaction_simulate.py +++ b/activitysim/core/interaction_simulate.py @@ -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 diff --git a/activitysim/core/test/test_interaction_simulate.py b/activitysim/core/test/test_interaction_simulate.py index af9442e228..b30493d6dc 100644 --- a/activitysim/core/test/test_interaction_simulate.py +++ b/activitysim/core/test/test_interaction_simulate.py @@ -172,3 +172,75 @@ 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() + ) From 55523303d60fb46741f85d8528bc304670228d74 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Jul 2026 21:44:12 +0000 Subject: [PATCH 3/4] Apply remaining changes --- activitysim/core/test/test_interaction_simulate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/activitysim/core/test/test_interaction_simulate.py b/activitysim/core/test/test_interaction_simulate.py index b30493d6dc..88779e96f3 100644 --- a/activitysim/core/test/test_interaction_simulate.py +++ b/activitysim/core/test/test_interaction_simulate.py @@ -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 @@ -241,6 +243,4 @@ def test_eval_interaction_utilities_locals_override_global_constants(tmp_path): trace_rows=None, ) - np.testing.assert_allclose( - utilities.utility.to_numpy(), df.distance_km.to_numpy() - ) + np.testing.assert_allclose(utilities.utility.to_numpy(), df.distance_km.to_numpy()) From 6fedb03385dc8215ebb5b2a295ac8dc7f5bf9ab7 Mon Sep 17 00:00:00 2001 From: Jeff Newman Date: Mon, 3 Aug 2026 16:05:26 -0500 Subject: [PATCH 4/4] :sparkles: Refactor `test_interaction_sample_simulate.py` to include tests for global constants Refactor the test file to include a new test case that verifies global constants (`SAMPLE_SCALE` and `SIMULATE_SCALE`) are available in both destination-choice substeps. This ensures consistency across sampling and simulation operations, enhancing reliability and maintainability of the codebase. --- .../test/test_interaction_sample_simulate.py | 60 ++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/activitysim/core/test/test_interaction_sample_simulate.py b/activitysim/core/test/test_interaction_sample_simulate.py index 40a8dfca5b..bb173eecad 100644 --- a/activitysim/core/test/test_interaction_sample_simulate.py +++ b/activitysim/core/test/test_interaction_sample_simulate.py @@ -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 @@ -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.