From 0cd3fab98753fa10412e04a20da10bd92a4e1701 Mon Sep 17 00:00:00 2001 From: Juliette-Gerbaux Date: Fri, 10 Jul 2026 16:50:26 +0200 Subject: [PATCH] fix: use underscores instead of hyphens in simulation table column names Column names like absolute-time-index or scenario-index are not valid Python identifiers, which prevents attribute-style access on the resulting DataFrame (e.g. via query() or itertuples()). Co-Authored-By: Claude Sonnet 5 --- docs/user-guide/outputs.md | 8 ++++---- src/gems/simulation/simulation_table.py | 18 +++++++++--------- tests/e2e/functional/test_optim_modes.py | 4 ++-- .../test_rolling_horizon_suboptimality.py | 2 +- .../e2e/functional/test_simtable_timeblock.py | 8 ++++---- .../simulation/test_simulation_table_export.py | 4 ++-- 6 files changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/user-guide/outputs.md b/docs/user-guide/outputs.md index d82b943d..19fd0e47 100644 --- a/docs/user-guide/outputs.md +++ b/docs/user-guide/outputs.md @@ -56,7 +56,7 @@ df = results.data ~~~ The DataFrame has columns: `block`, `component`, `output`, -`absolute-time-index`, `block-time-index`, `scenario-index`, `value`, `basis-status`. +`absolute_time_index`, `block_time_index`, `scenario_index`, `value`, `basis_status`. Reading the value of the optimisation variable `var_id` of component `component_id` for a single time step and scenario: @@ -65,12 +65,12 @@ for a single time step and scenario: value = df[(df["component"] == component_id) & (df["output"] == var_id)]["value"].iloc[0] ~~~ -For multi-time or multi-scenario results, filter additionally by `block-time-index` -and `scenario-index`: +For multi-time or multi-scenario results, filter additionally by `block_time_index` +and `scenario_index`: ~~~ python sub = df[(df["component"] == component_id) & (df["output"] == var_id)] -value_s0_t1 = sub[(sub["scenario-index"] == 0) & (sub["block-time-index"] == 1)]["value"].iloc[0] +value_s0_t1 = sub[(sub["scenario_index"] == 0) & (sub["block_time_index"] == 1)]["value"].iloc[0] ~~~ --- diff --git a/src/gems/simulation/simulation_table.py b/src/gems/simulation/simulation_table.py index e662079a..bb98d303 100644 --- a/src/gems/simulation/simulation_table.py +++ b/src/gems/simulation/simulation_table.py @@ -15,7 +15,7 @@ class OutputView: """ def __init__(self, df: pd.DataFrame) -> None: - # df: index = absolute-time-index, columns = scenario-index + # df: index = absolute_time_index, columns = scenario_index self._df = df @property @@ -32,8 +32,8 @@ def value( Called with no arguments returns the full Time × Scenario DataFrame. Called with one argument returns a ``pd.Series``: - - ``value(scenario_index=s)`` → Series indexed by absolute-time-index - - ``value(time_index=t)`` → Series indexed by scenario-index + - ``value(scenario_index=s)`` → Series indexed by absolute_time_index + - ``value(time_index=t)`` → Series indexed by scenario_index Called with both arguments returns a scalar ``float``. """ if time_index is None and scenario_index is None: @@ -142,7 +142,7 @@ def to_dataset(self) -> xr.Dataset: """Return simulation results as an xr.Dataset. Each output variable becomes a DataArray with dimensions - (component, absolute-time-index, scenario-index). + (component, absolute_time_index, scenario_index). Scalar rows without component/time/scenario (e.g. objective-value) are stored as zero-dimensional variables. """ @@ -174,11 +174,11 @@ class SimulationColumns(str, Enum): BLOCK = "block" COMPONENT = "component" OUTPUT = "output" - ABSOLUTE_TIME_INDEX = "absolute-time-index" - BLOCK_TIME_INDEX = "block-time-index" - SCENARIO_INDEX = "scenario-index" + ABSOLUTE_TIME_INDEX = "absolute_time_index" + BLOCK_TIME_INDEX = "block_time_index" + SCENARIO_INDEX = "scenario_index" VALUE = "value" - BASIS_STATUS = "basis-status" + BASIS_STATUS = "basis_status" class SimulationTableBuilder: @@ -441,7 +441,7 @@ def _da_to_df( ) -> pd.DataFrame: """Vectorize a [component?, time?, scenario?] DataArray into a DataFrame. - Index columns (absolute-time-index, block-time-index, scenario-index) are + Index columns (absolute_time_index, block_time_index, scenario_index) are set to None for dimensions that are absent from the original DataArray, signalling that the output is independent of that dimension. """ diff --git a/tests/e2e/functional/test_optim_modes.py b/tests/e2e/functional/test_optim_modes.py index 9f314e11..ad895863 100644 --- a/tests/e2e/functional/test_optim_modes.py +++ b/tests/e2e/functional/test_optim_modes.py @@ -83,8 +83,8 @@ _KEY_COLS = [ "component", "output", - "absolute-time-index", - "scenario-index", + "absolute_time_index", + "scenario_index", ] diff --git a/tests/e2e/functional/test_rolling_horizon_suboptimality.py b/tests/e2e/functional/test_rolling_horizon_suboptimality.py index 316d6a18..38d65dd3 100644 --- a/tests/e2e/functional/test_rolling_horizon_suboptimality.py +++ b/tests/e2e/functional/test_rolling_horizon_suboptimality.py @@ -120,7 +120,7 @@ def _get_value(raw, component: str, output: str, timestep: int) -> float: mask = ( (raw["component"] == component) & (raw["output"] == output) - & (raw["absolute-time-index"] == timestep) + & (raw["absolute_time_index"] == timestep) ) rows = raw[mask] assert ( diff --git a/tests/e2e/functional/test_simtable_timeblock.py b/tests/e2e/functional/test_simtable_timeblock.py index c4a4822c..728be0a2 100644 --- a/tests/e2e/functional/test_simtable_timeblock.py +++ b/tests/e2e/functional/test_simtable_timeblock.py @@ -18,7 +18,7 @@ - System: 3 components — node, generator (pmax=200), demand (timevarying: demand[t]=t) - Data horizon: 150 timesteps - TimeBlock: [40, 90) → 50 timesteps (indices 40–89) - - Checks that SimulationTable absolute-time-index, block-time-index, and + - Checks that SimulationTable absolute_time_index, block_time_index, and generation values are all consistent with the partial block. """ @@ -52,8 +52,8 @@ def test_simtable_on_partial_timeblock(lib_dict_unittest: dict[str, Library]) -> With demand[t] = t and pmax = 200, the optimizer sets generation[t] = t at every timestep, making all three assertions self-consistent: 1. generation values equal their absolute timestep index. - 2. absolute-time-index runs from 40 to 89 (not 0–149 or 0–49). - 3. block-time-index runs from 0 to 49 and equals absolute-time-index − 40. + 2. absolute_time_index runs from 40 to 89 (not 0–149 or 0–49). + 3. block_time_index runs from 0 to 49 and equals absolute_time_index − 40. """ node_model = lib_dict_unittest["basic"].models["basic.node"] generator_model = lib_dict_unittest["basic"].models["basic.generator"] @@ -120,7 +120,7 @@ def test_simtable_on_partial_timeblock(lib_dict_unittest: dict[str, Library]) -> assert abs_times == list(range(BLOCK_START, BLOCK_END)) assert block_times == list(range(BLOCK_END - BLOCK_START)) - # absolute-time-index = block-time-index + BLOCK_START for every row + # absolute_time_index = block_time_index + BLOCK_START for every row offset = ( gen_rows[SimulationColumns.ABSOLUTE_TIME_INDEX.value].astype(int) - gen_rows[SimulationColumns.BLOCK_TIME_INDEX.value].astype(int) diff --git a/tests/unittests/simulation/test_simulation_table_export.py b/tests/unittests/simulation/test_simulation_table_export.py index 75c3459d..cc73a440 100644 --- a/tests/unittests/simulation/test_simulation_table_export.py +++ b/tests/unittests/simulation/test_simulation_table_export.py @@ -126,7 +126,7 @@ def test_to_dataset_values_match_data_single_scenario() -> None: expected = float(row[SimulationColumns.VALUE.value]) actual = float( ds[output].sel( - component=comp, **{"absolute-time-index": t, "scenario-index": s} + component=comp, **{"absolute_time_index": t, "scenario_index": s} ) ) assert actual == pytest.approx(expected) @@ -144,7 +144,7 @@ def test_to_dataset_values_match_data_multi_scenario() -> None: expected = float(row[SimulationColumns.VALUE.value]) actual = float( ds[output].sel( - component=comp, **{"absolute-time-index": t, "scenario-index": s} + component=comp, **{"absolute_time_index": t, "scenario_index": s} ) ) assert actual == pytest.approx(expected)