From f32afa69658a72b50bf1e4e46ebe4f9be6365659 Mon Sep 17 00:00:00 2001 From: SutubraResearch Date: Wed, 11 Mar 2026 14:27:30 -0300 Subject: [PATCH 1/7] Fix limit_capacity_constraint missing index check The constraint index function did not verify that the (region, period, tech) tuple existed in the relevant capacity sets before building indices. This caused a KeyError when a technology appeared in MaxCapacity or MinCapacity but had no active vintages in the current optimization window. --- temoa/components/limits.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/temoa/components/limits.py b/temoa/components/limits.py index baa4b042..6a12ea29 100644 --- a/temoa/components/limits.py +++ b/temoa/components/limits.py @@ -1383,7 +1383,10 @@ def limit_capacity_constraint( techs = technology.gather_group_techs(model, t) cap_lim = value(model.limit_capacity[r, p, t, op]) capacity = quicksum( - model.v_capacity_available_by_period_and_tech[_r, p, _t] for _t in techs for _r in regions + model.v_capacity_available_by_period_and_tech[_r, p, _t] + for _t in techs + for _r in regions + if (_r, p, _t) in model.process_vintages ) expr = operator_expression(capacity, Operator(op), cap_lim) return expr From 278a0c997b7b0b4a61e651882858da46954dd4b2 Mon Sep 17 00:00:00 2001 From: SutubraResearch Date: Wed, 11 Mar 2026 14:27:41 -0300 Subject: [PATCH 2/7] Fix loan_lifetime_process KeyError in myopic mode In myopic window 2+, previously optimized vintages remain active in myopic_efficiency but were excluded from loan_lifetime_process by the v >= min(vintage_optimize) filter. This caused a KeyError during Pyomo param construction when filtered data included those vintages. Remove the min_period filter to match lifetime_process_indices, which already accepts all efficiency vintages without restriction. --- temoa/components/costs.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/temoa/components/costs.py b/temoa/components/costs.py index a4647e64..cd22729c 100644 --- a/temoa/components/costs.py +++ b/temoa/components/costs.py @@ -113,13 +113,14 @@ def cost_variable_indices(model: TemoaModel) -> set[tuple[Region, Period, Techno def lifetime_loan_process_indices(model: TemoaModel) -> set[tuple[Region, Technology, Vintage]]: """ - Based on the efficiency parameter's indices and time_future parameter, this - function returns the set of process indices that may be specified in the - cost_invest parameter. - """ - min_period = min(model.vintage_optimize) + Based on the efficiency parameter's indices, this function returns the set of + process indices that may be specified in the loan_lifetime_process parameter. - indices = {(r, t, v) for r, i, t, v, o in model.efficiency.sparse_iterkeys() if v >= min_period} + Note: We include all efficiency vintages (not just >= min optimization period) + because in myopic mode, previously optimized vintages remain active in later + windows and their data must be accepted by the param's index set. + """ + indices = {(r, t, v) for r, i, t, v, o in model.efficiency.sparse_iterkeys()} return indices From 6060e6a3290a8f3f9180eb53affbace6142b5ea0 Mon Sep 17 00:00:00 2001 From: SutubraResearch Date: Wed, 11 Mar 2026 14:29:56 -0300 Subject: [PATCH 3/7] Disable season ramp constraints for seasonal_timeslices In seasonal_timeslices mode, seasons are not temporally adjacent so inter-season ramp constraints are not meaningful. Previously only consecutive_days mode skipped these constraints. Add seasonal_timeslices to the skip condition in both ramp_up and ramp_down season constraint index functions. Update test expectations: mediumville constraint count drops by 8 (4 ramp_up + 4 ramp_down). Regenerate set cache files to reflect expanded loan_lifetime_process_rtv from the earlier fix. --- temoa/components/operations.py | 4 ++-- tests/legacy_test_values.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/temoa/components/operations.py b/temoa/components/operations.py index ee11f26c..67c475ac 100644 --- a/temoa/components/operations.py +++ b/temoa/components/operations.py @@ -74,7 +74,7 @@ def ramp_down_day_constraint_indices( def ramp_up_season_constraint_indices( model: TemoaModel, ) -> set[tuple[Region, Period, Season, Season, Technology, Vintage]]: - if model.time_sequencing.first() == 'consecutive_days': + if model.time_sequencing.first() in ('consecutive_days', 'seasonal_timeslices'): return set() # s, s_next indexing ensures we dont build redundant constraints @@ -94,7 +94,7 @@ def ramp_up_season_constraint_indices( def ramp_down_season_constraint_indices( model: TemoaModel, ) -> set[tuple[Region, Period, Season, Season, Technology, Vintage]]: - if model.time_sequencing.first() == 'consecutive_days': + if model.time_sequencing.first() in ('consecutive_days', 'seasonal_timeslices'): return set() # s, s_next indexing ensures we dont build redundant constraints diff --git a/tests/legacy_test_values.py b/tests/legacy_test_values.py index 2223cdb7..aacfd3d5 100644 --- a/tests/legacy_test_values.py +++ b/tests/legacy_test_values.py @@ -71,7 +71,8 @@ class ExpectedVals(Enum): # increased 2025/08/19 after making annual demands optional # increased by 2 after tying v_storage_level[d_last] to v_storage_init # reduced by 10 after dropping DAC for single-tech demands - ExpectedVals.CONSTR_COUNT: 232, + # reduced by 8 after disabling season ramp for seasonal_timeslices + ExpectedVals.CONSTR_COUNT: 224, # reduced 2025/07/25 by 18 after annualising demands # increased 2025/08/19 after making annual demands optional # increased by 2 after adding v_storage_init variable From 598c0fb99996724b7fb8f4a5aaef9be8797cd8cd Mon Sep 17 00:00:00 2001 From: SutubraResearch Date: Wed, 18 Mar 2026 12:02:29 -0300 Subject: [PATCH 4/7] Add bool guards to limit capacity constraints and sync schema FK --- data_files/temoa_schema_v4.sql | 2 +- temoa/components/limits.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/data_files/temoa_schema_v4.sql b/data_files/temoa_schema_v4.sql index 8efedde6..2019e7f0 100644 --- a/data_files/temoa_schema_v4.sql +++ b/data_files/temoa_schema_v4.sql @@ -708,7 +708,7 @@ CREATE TABLE IF NOT EXISTS output_curtailment period INTEGER REFERENCES time_period (period), season TEXT - REFERENCES time_period (period), + REFERENCES season_label (season), tod TEXT REFERENCES time_of_day (tod), input_comm TEXT diff --git a/temoa/components/limits.py b/temoa/components/limits.py index 6a12ea29..d9b47f78 100644 --- a/temoa/components/limits.py +++ b/temoa/components/limits.py @@ -1361,6 +1361,8 @@ def limit_new_capacity_constraint( cap_lim = value(model.limit_new_capacity[r, p, t, op]) new_cap = quicksum(model.v_new_capacity[_r, _t, p] for _t in techs for _r in regions) expr = operator_expression(new_cap, Operator(op), cap_lim) + if isinstance(expr, bool): + return Constraint.Skip return expr @@ -1389,6 +1391,8 @@ def limit_capacity_constraint( if (_r, p, _t) in model.process_vintages ) expr = operator_expression(capacity, Operator(op), cap_lim) + if isinstance(expr, bool): + return Constraint.Skip return expr From 527e008ab7d439df7a528dce6b8c94482d516b06 Mon Sep 17 00:00:00 2001 From: SutubraResearch Date: Wed, 18 Mar 2026 13:52:17 -0300 Subject: [PATCH 5/7] Add comment explaining seasonal_timeslices ramp skip --- temoa/components/operations.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/temoa/components/operations.py b/temoa/components/operations.py index 67c475ac..bcfe0356 100644 --- a/temoa/components/operations.py +++ b/temoa/components/operations.py @@ -74,6 +74,8 @@ def ramp_down_day_constraint_indices( def ramp_up_season_constraint_indices( model: TemoaModel, ) -> set[tuple[Region, Period, Season, Season, Technology, Vintage]]: + # Season-to-season ramp constraints require full inter-season ordering; + # skip for consecutive_days (no season links) and seasonal_timeslices (no TOD ordering). if model.time_sequencing.first() in ('consecutive_days', 'seasonal_timeslices'): return set() @@ -94,6 +96,8 @@ def ramp_up_season_constraint_indices( def ramp_down_season_constraint_indices( model: TemoaModel, ) -> set[tuple[Region, Period, Season, Season, Technology, Vintage]]: + # Season-to-season ramp constraints require full inter-season ordering; + # skip for consecutive_days (no season links) and seasonal_timeslices (no TOD ordering). if model.time_sequencing.first() in ('consecutive_days', 'seasonal_timeslices'): return set() From 0b0c68f23fdac404ee263484664b7a5df2566552 Mon Sep 17 00:00:00 2001 From: SutubraResearch Date: Fri, 20 Mar 2026 16:23:27 -0300 Subject: [PATCH 6/7] Update JSON set caches for bug-fix changes Expand loan_lifetime_process_rtv for utopia (+15) and test_system (+4) after KeyError fix. Remove season ramp indices from mediumville after disabling season ramps for seasonal_timeslices. Adjust mediumville constraint count to 224 (accounting for storage-init, DAC, and season ramp changes). --- tests/testing_data/mediumville_sets.json | 70 +--------------------- tests/testing_data/test_system_sets.json | 20 +++++++ tests/testing_data/utopia_sets.json | 75 ++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 68 deletions(-) diff --git a/tests/testing_data/mediumville_sets.json b/tests/testing_data/mediumville_sets.json index 2c07b6ba..5c406fed 100644 --- a/tests/testing_data/mediumville_sets.json +++ b/tests/testing_data/mediumville_sets.json @@ -3788,40 +3788,7 @@ 2025 ] ], - "ramp_down_season_constraint_rpsstv": [ - [ - "A", - 2025, - "s1", - "s2", - "EH", - 2025 - ], - [ - "A", - 2025, - "s2", - "s1", - "EH", - 2025 - ], - [ - "B", - 2025, - "s1", - "s2", - "EH", - 2025 - ], - [ - "B", - 2025, - "s2", - "s1", - "EH", - 2025 - ] - ], + "ramp_down_season_constraint_rpsstv": [], "ramp_up_day_constraint_rpsdtv": [ [ "B", @@ -3888,40 +3855,7 @@ 2025 ] ], - "ramp_up_season_constraint_rpsstv": [ - [ - "A", - 2025, - "s1", - "s2", - "EH", - 2025 - ], - [ - "A", - 2025, - "s2", - "s1", - "EH", - 2025 - ], - [ - "B", - 2025, - "s1", - "s2", - "EH", - 2025 - ], - [ - "B", - 2025, - "s2", - "s1", - "EH", - 2025 - ] - ], + "ramp_up_season_constraint_rpsstv": [], "regional_exchange_capacity_constraint_rrptv": [ [ "B", diff --git a/tests/testing_data/test_system_sets.json b/tests/testing_data/test_system_sets.json index df2014de..17bb457e 100644 --- a/tests/testing_data/test_system_sets.json +++ b/tests/testing_data/test_system_sets.json @@ -42293,6 +42293,26 @@ "R1", "S_OILREF", 2020 + ], + [ + "R1", + "E_NUCLEAR", + 2015 + ], + [ + "R2", + "E_NUCLEAR", + 2015 + ], + [ + "R1-R2", + "E_TRANS", + 2015 + ], + [ + "R2-R1", + "E_TRANS", + 2015 ] ], "new_capacity_var_rtv": [ diff --git a/tests/testing_data/utopia_sets.json b/tests/testing_data/utopia_sets.json index 8be5f4a3..77181f16 100644 --- a/tests/testing_data/utopia_sets.json +++ b/tests/testing_data/utopia_sets.json @@ -23130,6 +23130,81 @@ "utopia", "RL1", 2000 + ], + [ + "utopia", + "E01", + 1960 + ], + [ + "utopia", + "E01", + 1970 + ], + [ + "utopia", + "E01", + 1980 + ], + [ + "utopia", + "E70", + 1960 + ], + [ + "utopia", + "E70", + 1970 + ], + [ + "utopia", + "E70", + 1980 + ], + [ + "utopia", + "RHO", + 1970 + ], + [ + "utopia", + "RHO", + 1980 + ], + [ + "utopia", + "TXG", + 1970 + ], + [ + "utopia", + "TXG", + 1980 + ], + [ + "utopia", + "TXD", + 1970 + ], + [ + "utopia", + "TXD", + 1980 + ], + [ + "utopia", + "E31", + 1980 + ], + [ + "utopia", + "E51", + 1980 + ], + [ + "utopia", + "RL1", + 1980 ] ], "new_capacity_var_rtv": [ From 74357b53a7436ea6051446a6635185ca1d076a24 Mon Sep 17 00:00:00 2001 From: SutubraResearch Date: Fri, 20 Mar 2026 16:35:58 -0300 Subject: [PATCH 7/7] Guard limit_capacity against tech_uncap KeyError Use v_capacity_available_by_period_and_tech (which excludes tech_uncap) as the membership check instead of process_vintages (which includes them). Prevents KeyError when a capacity group contains uncapacitated technologies. --- temoa/components/limits.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/temoa/components/limits.py b/temoa/components/limits.py index d9b47f78..3f55985a 100644 --- a/temoa/components/limits.py +++ b/temoa/components/limits.py @@ -1388,7 +1388,7 @@ def limit_capacity_constraint( model.v_capacity_available_by_period_and_tech[_r, p, _t] for _t in techs for _r in regions - if (_r, p, _t) in model.process_vintages + if (_r, p, _t) in model.v_capacity_available_by_period_and_tech ) expr = operator_expression(capacity, Operator(op), cap_lim) if isinstance(expr, bool):