From 0c556d349227eda1de7181e42920bff98b39e954 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Thu, 12 Mar 2026 13:38:02 -0400 Subject: [PATCH 01/24] Update myopic filtering for embodied emissions and construction inputs --- temoa/temoa_model/hybrid_loader.py | 33 +++++++++++++++---- .../model_checking/network_model_data.py | 4 ++- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/temoa/temoa_model/hybrid_loader.py b/temoa/temoa_model/hybrid_loader.py index e321dd049..2a403b06f 100644 --- a/temoa/temoa_model/hybrid_loader.py +++ b/temoa/temoa_model/hybrid_loader.py @@ -977,10 +977,19 @@ def load_indexed_set(indexed_set: Set, index_value, element, element_validator=N # EmissionEmbodied if self.table_exists('EmissionEmbodied'): - raw = cur.execute( - 'SELECT region, emis_comm, tech, vintage, value ' - 'FROM main.EmissionEmbodied' - ).fetchall() + if mi: + qry = ( + 'SELECT region, emis_comm, tech, vintage, value FROM main.EmissionEmbodied' + ' WHERE vintage >= ? AND vintage <= ?' + ) + raw = cur.execute( + qry, + (mi.base_year, mi.last_demand_year), + ).fetchall() + else: + raw = cur.execute( + 'SELECT region, emis_comm, tech, vintage, value FROM main.EmissionEmbodied' + ).fetchall() load_element(M.EmissionEmbodied, raw, self.viable_rtv, (0, 2, 3)) # EmissionEndOfLife @@ -993,9 +1002,19 @@ def load_indexed_set(indexed_set: Set, index_value, element, element_validator=N # ConstructionInput if self.table_exists('ConstructionInput'): - raw = cur.execute( - 'SELECT region, input_comm, tech, vintage, value FROM main.ConstructionInput' - ).fetchall() + if mi: + qry = ( + 'SELECT region, input_comm, tech, vintage, value FROM main.ConstructionInput' + ' WHERE vintage >= ? AND vintage <= ?' + ) + raw = cur.execute( + qry, + (mi.base_year, mi.last_demand_year), + ).fetchall() + else: + raw = cur.execute( + 'SELECT region, input_comm, tech, vintage, value FROM main.ConstructionInput' + ).fetchall() load_element(M.ConstructionInput, raw, self.viable_rtv, (0, 2, 3)) # EndOfLifeOutput diff --git a/temoa/temoa_model/model_checking/network_model_data.py b/temoa/temoa_model/model_checking/network_model_data.py index dd36937ad..97708af05 100644 --- a/temoa/temoa_model/model_checking/network_model_data.py +++ b/temoa/temoa_model/model_checking/network_model_data.py @@ -299,7 +299,9 @@ def _build_from_db( try: raw = cur.execute('SELECT region, input_comm, tech, vintage FROM ConstructionInput').fetchall() for r, ic, tech, v in raw: - techs[r, v].add(Tech(r, ic, 'Construction', v, tech)) + if v not in periods: + continue + techs[r, v].add(Tech(r, ic, tech, v, tech)) demand_dict[r, v].add(tech) res.capacity_commodities.add(tech) living_techs.add(tech) From 9cf4a7cfdde782931d1f58b84455c1049ae3f780 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Thu, 12 Mar 2026 13:40:00 -0400 Subject: [PATCH 02/24] Update filtering for endoflife output and emissions --- temoa/temoa_model/hybrid_loader.py | 12 ++++++++++-- .../model_checking/commodity_network_manager.py | 5 +++++ .../temoa_model/model_checking/network_model_data.py | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/temoa/temoa_model/hybrid_loader.py b/temoa/temoa_model/hybrid_loader.py index 2a403b06f..3b1a69537 100644 --- a/temoa/temoa_model/hybrid_loader.py +++ b/temoa/temoa_model/hybrid_loader.py @@ -101,6 +101,7 @@ def __init__(self, db_connection: Connection, config: TemoaConfig): self.viable_rt: ViableSet | None = None self.viable_rpit: ViableSet | None = None self.viable_rtt: ViableSet | None = None # to support scanning LinkedTech + self.viable_rtv_eol: ViableSet | None = None # to support scanning EndOfLifeOutput self.efficiency_values: list[tuple] = [] # container for loaded data @@ -175,6 +176,7 @@ def _build_efficiency_dataset( self.viable_rt = filts['rt'] self.viable_rpit = filts['rpit'] self.viable_rpto = filts['rpto'] + self.viable_rtv_eol = filts['rtv_eol'] self.viable_techs = filts['t'] self.viable_input_comms = filts['ic'] self.viable_vintages = filts['v'] @@ -730,6 +732,12 @@ def load_indexed_set(indexed_set: Set, index_value, element, element_validator=N raw = cur.execute('SELECT region, period, tech, vintage, fraction FROM main.LifetimeSurvivalCurve').fetchall() load_element(M.LifetimeSurvivalCurve, raw, self.viable_rtv, val_loc=(0, 2, 3)) + print([ + (r, i, t, v, o) + for r, i, t, v, o in self.viable_ritvo.member_tuples + if v > mi.base_year + ]) + # LoanLifetimeProcess if self.table_exists("LoanLifetimeProcess"): raw = cur.execute('SELECT region, tech, vintage, lifetime FROM main.LoanLifetimeProcess').fetchall() @@ -998,7 +1006,7 @@ def load_indexed_set(indexed_set: Set, index_value, element, element_validator=N 'SELECT region, emis_comm, tech, vintage, value ' 'FROM main.EmissionEndOfLife' ).fetchall() - load_element(M.EmissionEndOfLife, raw, self.viable_rtv, (0, 2, 3)) + load_element(M.EmissionEndOfLife, raw, self.viable_rtv_eol, (0, 1, 2)) # ConstructionInput if self.table_exists('ConstructionInput'): @@ -1022,7 +1030,7 @@ def load_indexed_set(indexed_set: Set, index_value, element, element_validator=N raw = cur.execute( 'SELECT region, tech, vintage, output_comm, value FROM main.EndOfLifeOutput' ).fetchall() - load_element(M.EndOfLifeOutput, raw, self.viable_rtv, (0, 1, 2)) + load_element(M.EndOfLifeOutput, raw, self.viable_rtv_eol, (0, 1, 2)) # LinkedTechs # Note: Both of the linked techs must be viable. As this is non period/vintage diff --git a/temoa/temoa_model/model_checking/commodity_network_manager.py b/temoa/temoa_model/model_checking/commodity_network_manager.py index bff1568be..d508d021a 100644 --- a/temoa/temoa_model/model_checking/commodity_network_manager.py +++ b/temoa/temoa_model/model_checking/commodity_network_manager.py @@ -117,6 +117,7 @@ def build_filters(self) -> dict[str, ViableSet]: valid_rpit = set() valid_rpto = set() valid_t = set() + valid_rtv_eol = set() valid_input_commodities = set() valid_output_commodities = set() valid_vintages = set() @@ -128,6 +129,7 @@ def build_filters(self) -> dict[str, ViableSet]: valid_rpit.add((tech.region, p, tech.ic, tech.name)) valid_rpto.add((tech.region, p, tech.name, tech.oc)) valid_t.add(tech.name) + valid_rtv_eol.add((tech.region, tech.ic, tech.vintage)) valid_input_commodities.add(tech.ic) valid_output_commodities.add(tech.oc) valid_vintages.add(tech.vintage) @@ -148,6 +150,9 @@ def build_filters(self) -> dict[str, ViableSet]: 'v': ViableSet(elements=valid_vintages), 'ic': ViableSet(elements=valid_input_commodities), 'oc': ViableSet(elements=valid_output_commodities), + 'rtv_eol': ViableSet( + elements=valid_rtv_eol, exception_loc=0, exception_vals=ViableSet.REGION_REGEXES + ), } return filts diff --git a/temoa/temoa_model/model_checking/network_model_data.py b/temoa/temoa_model/model_checking/network_model_data.py index 97708af05..87a82729b 100644 --- a/temoa/temoa_model/model_checking/network_model_data.py +++ b/temoa/temoa_model/model_checking/network_model_data.py @@ -285,7 +285,7 @@ def _build_from_db( ).fetchall() for _r, _tech, _v, _oc in raw_eol: - techs[_r, p].add(Tech(_r, _tech, 'EndOfLife', _v, _oc)) + techs[_r, p].add(Tech(_r, _tech, _tech, _v, _oc)) source_dict[_r, p].add(_tech) res.capacity_commodities.add(_tech) living_techs.add(_tech) From 13637b96c2faaae98dd9df16771dd52edad32072 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Thu, 12 Mar 2026 13:40:25 -0400 Subject: [PATCH 03/24] Fix efficiency indices check --- temoa/temoa_model/temoa_initialize.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/temoa/temoa_model/temoa_initialize.py b/temoa/temoa_model/temoa_initialize.py index 163e9ee1b..0be8b67bd 100644 --- a/temoa/temoa_model/temoa_initialize.py +++ b/temoa/temoa_model/temoa_initialize.py @@ -263,7 +263,6 @@ def CheckEfficiencyIndices(M: 'TemoaModel'): # by checking by REGION and PERIOD... Each region/period is unique. c_physical = set(i for r, i, t, v, o in M.Efficiency.sparse_iterkeys()) c_physical = c_physical | set(i for r, i, t, v in M.ConstructionInput.sparse_iterkeys()) - techs = set(t for r, i, t, v, o in M.Efficiency.sparse_iterkeys()) c_outputs = set(o for r, i, t, v, o in M.Efficiency.sparse_iterkeys()) c_outputs = c_outputs | set(o for r, t, v, o in M.EndOfLifeOutput.sparse_iterkeys()) c_physical = c_physical | c_outputs @@ -279,6 +278,10 @@ def CheckEfficiencyIndices(M: 'TemoaModel'): f_msg = msg.format(', '.join(symdiff)) logger.error(f_msg) raise ValueError(f_msg) + + techs = set(t for r, i, t, v, o in M.Efficiency.sparse_iterkeys()) + techs = techs | set(t for r, t, v, o in M.EndOfLifeOutput.sparse_iterkeys()) + techs = techs | set(t for r, i, t, v in M.ConstructionInput.sparse_iterkeys()) symdiff = techs.symmetric_difference(M.tech_all) if symdiff: From 07e0b64283a9cdce294ef8673c5af46f31b0b0b3 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Thu, 12 Mar 2026 13:40:37 -0400 Subject: [PATCH 04/24] Fix used techs warning --- temoa/temoa_model/temoa_initialize.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/temoa/temoa_model/temoa_initialize.py b/temoa/temoa_model/temoa_initialize.py index 0be8b67bd..2765c336e 100644 --- a/temoa/temoa_model/temoa_initialize.py +++ b/temoa/temoa_model/temoa_initialize.py @@ -950,6 +950,7 @@ def CreateSparseDicts(M: 'TemoaModel'): if (r, v, i) not in M.capacityConsumptionTechs: M.capacityConsumptionTechs[r, v, i] = set() M.capacityConsumptionTechs[r, v, i].add(t) + l_used_techs.add(t) for r, t, v, o in M.EndOfLifeOutput.sparse_iterkeys(): if (r, t, v) not in M.retirementPeriods: continue # might be running myopic @@ -958,13 +959,11 @@ def CreateSparseDicts(M: 'TemoaModel'): if (r, p, o) not in M.retirementProductionProcesses: M.retirementProductionProcesses[r, p, o] = set() M.retirementProductionProcesses[r, p, o].add((t, v)) + l_used_techs.add(t) l_unused_techs = M.tech_all - l_used_techs if l_unused_techs: - msg = ( - "Notice: '{}' specified as technology, but it is not utilized in " - 'the Efficiency parameter.\n' - ) + msg = "Notice: '{}' specified as technology, but it is not used" for i in sorted(l_unused_techs): SE.write(msg.format(i)) From cf6cc58b3d8ea8950fc75b50172793fce8bb94bc Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Thu, 12 Mar 2026 13:43:26 -0400 Subject: [PATCH 05/24] REMOVE PRINT STATEMENT --- temoa/temoa_model/hybrid_loader.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/temoa/temoa_model/hybrid_loader.py b/temoa/temoa_model/hybrid_loader.py index 3b1a69537..d7010d012 100644 --- a/temoa/temoa_model/hybrid_loader.py +++ b/temoa/temoa_model/hybrid_loader.py @@ -732,12 +732,6 @@ def load_indexed_set(indexed_set: Set, index_value, element, element_validator=N raw = cur.execute('SELECT region, period, tech, vintage, fraction FROM main.LifetimeSurvivalCurve').fetchall() load_element(M.LifetimeSurvivalCurve, raw, self.viable_rtv, val_loc=(0, 2, 3)) - print([ - (r, i, t, v, o) - for r, i, t, v, o in self.viable_ritvo.member_tuples - if v > mi.base_year - ]) - # LoanLifetimeProcess if self.table_exists("LoanLifetimeProcess"): raw = cur.execute('SELECT region, tech, vintage, lifetime FROM main.LoanLifetimeProcess').fetchall() From aa428682ce5212ce05d5df147725949a90f846f5 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Thu, 12 Mar 2026 13:54:40 -0400 Subject: [PATCH 06/24] Fix filter index for emissionendoflife --- temoa/temoa_model/hybrid_loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/temoa/temoa_model/hybrid_loader.py b/temoa/temoa_model/hybrid_loader.py index d7010d012..7ceecd983 100644 --- a/temoa/temoa_model/hybrid_loader.py +++ b/temoa/temoa_model/hybrid_loader.py @@ -1000,7 +1000,7 @@ def load_indexed_set(indexed_set: Set, index_value, element, element_validator=N 'SELECT region, emis_comm, tech, vintage, value ' 'FROM main.EmissionEndOfLife' ).fetchall() - load_element(M.EmissionEndOfLife, raw, self.viable_rtv_eol, (0, 1, 2)) + load_element(M.EmissionEndOfLife, raw, self.viable_rtv_eol, (0, 2, 3)) # ConstructionInput if self.table_exists('ConstructionInput'): From 434d6f3bd9f701324cd89e6fd2e0393646e9b9d6 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Sat, 14 Mar 2026 10:33:33 -0400 Subject: [PATCH 07/24] Remove abs() from data puller as negative values are possible for unstable models --- temoa/temoa_model/table_data_puller.py | 30 +++++++++++++------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/temoa/temoa_model/table_data_puller.py b/temoa/temoa_model/table_data_puller.py index 9cd1889e6..58c9037be 100644 --- a/temoa/temoa_model/table_data_puller.py +++ b/temoa/temoa_model/table_data_puller.py @@ -99,7 +99,7 @@ def poll_capacity_results(M: TemoaModel, epsilon=1e-5) -> CapData: for r, t, v in M.V_NewCapacity.keys(): if v in M.time_optimize: val = value(M.V_NewCapacity[r, t, v]) - if abs(val) < epsilon: + if val < epsilon: continue new_cap = (r, t, v, val) built.append(new_cap) @@ -108,7 +108,7 @@ def poll_capacity_results(M: TemoaModel, epsilon=1e-5) -> CapData: net = [] for r, p, t, v in M.V_Capacity.keys(): val = value(M.V_Capacity[r, p, t, v]) - if abs(val) < epsilon: + if val < epsilon: continue new_net_cap = (r, p, t, v, val) net.append(new_net_cap) @@ -124,8 +124,8 @@ def poll_capacity_results(M: TemoaModel, epsilon=1e-5) -> CapData: if t in M.tech_retirement and v < p <= v + lifetime - value(M.PeriodLength[p]): early = value(M.V_RetiredCapacity[r, p, t, v]) eol -= early - early = 0 if abs(early) < epsilon else early - eol = 0 if abs(eol) < epsilon else eol + early = 0 if early < epsilon else early + eol = 0 if eol < epsilon else eol if early == 0 and eol == 0: continue new_retired_cap = (r, p, t, v, eol, early) @@ -150,7 +150,7 @@ def poll_flow_results(M: TemoaModel, epsilon=1e-5) -> dict[FI, dict[FlowType, fl for key in M.V_FlowIn.keys(): fi = FI(*key) flow = value(M.V_FlowIn[fi]) - if abs(flow) < epsilon: + if flow < epsilon: continue res[fi][FlowType.IN] = flow res[fi][FlowType.LOST] = (1 - temoa_rules.get_variable_efficiency(M, *key)) * flow @@ -159,7 +159,7 @@ def poll_flow_results(M: TemoaModel, epsilon=1e-5) -> dict[FI, dict[FlowType, fl for key in M.V_FlowOut.keys(): fi = FI(*key) flow = value(M.V_FlowOut[fi]) - if abs(flow) < epsilon: + if flow < epsilon: continue res[fi][FlowType.OUT] = flow @@ -172,7 +172,7 @@ def poll_flow_results(M: TemoaModel, epsilon=1e-5) -> dict[FI, dict[FlowType, fl for key in M.V_Curtailment.keys(): fi = FI(*key) val = value(M.V_Curtailment[fi]) - if abs(val) < epsilon: + if val < epsilon: continue res[fi][FlowType.CURTAIL] = val @@ -180,7 +180,7 @@ def poll_flow_results(M: TemoaModel, epsilon=1e-5) -> dict[FI, dict[FlowType, fl for key in M.V_Flex.keys(): fi = FI(*key) flow = value(M.V_Flex[fi]) - if abs(flow) < epsilon: + if flow < epsilon: continue res[fi][FlowType.FLEX] = flow res[fi][FlowType.OUT] -= flow @@ -200,7 +200,7 @@ def poll_flow_results(M: TemoaModel, epsilon=1e-5) -> dict[FI, dict[FlowType, fl distribution = value(M.SegFrac[p, s, d]) fi = FI(r, p, s, d, i, t, v, o) flow = value(M.V_FlowOutAnnual[r, p, i, t, v, o]) * distribution - if abs(flow) < epsilon: + if flow < epsilon: continue res[fi][FlowType.OUT] = flow res[fi][FlowType.IN] = flow / value(M.Efficiency[ritvo(fi)]) @@ -212,7 +212,7 @@ def poll_flow_results(M: TemoaModel, epsilon=1e-5) -> dict[FI, dict[FlowType, fl for d in M.time_of_day: fi = FI(r, p, s, d, i, t, v, o) flow = value(M.V_FlexAnnual[r, p, i, t, v, o]) * value(M.SegFrac[p, s, d]) - if abs(flow) < epsilon: + if flow < epsilon: continue res[fi][FlowType.FLEX] = flow res[fi][FlowType.OUT] -= flow @@ -260,7 +260,7 @@ def poll_storage_level_results(M: TemoaModel, epsilon=1e-5) -> dict[SLI, float]: continue state = value(M.V_StorageLevel[r, p, s, d, t, v]) / (value(M.SegFracPerSeason[p, s]) * value(M.DaysPerPeriod)) sli = SLI(r, p, s, d, t, v) - if abs(state) < epsilon: state = 0 # still want to know but decimals are ugly + if state < epsilon: state = 0 # still want to know but decimals are ugly res[sli] = state for r, p, s_seq, t, v in M.SeasonalStorageLevel_rpstv: @@ -272,7 +272,7 @@ def poll_storage_level_results(M: TemoaModel, epsilon=1e-5) -> dict[SLI, float]: for d in M.time_of_day: state = value(M.V_SeasonalStorageLevel[r, p, s_seq, t, v]) + value(M.V_StorageLevel[r, p, s, d, t, v]) * days_adjust sli = SLI(r, p, s_seq, d, t, v) - if abs(state) < epsilon: state = 0 # still want to know but decimals are ugly + if state < epsilon: state = 0 # still want to know but decimals are ugly res[sli] = state return res @@ -316,7 +316,7 @@ def poll_cost_results( for r, t, v in M.CostInvest.sparse_iterkeys(): # Returns only non-zero values # gather details... cap = value(M.V_NewCapacity[r, t, v]) - if abs(cap) < epsilon: + if cap < epsilon: continue loan_life = value(LLN[r, t, v]) loan_rate = value(M.LoanRate[r, t, v]) @@ -374,7 +374,7 @@ def poll_cost_results( for r, p, t, v in M.CostFixed.sparse_iterkeys(): cap = value(M.V_Capacity[r, p, t, v]) - if abs(cap) < epsilon: + if cap < epsilon: continue fixed_cost = value(M.CostFixed[r, p, t, v]) @@ -420,7 +420,7 @@ def poll_cost_results( for S_i in M.processInputs[r, p, t, v] for S_o in M.processOutputsByInput[r, p, t, v, S_i] ) - if abs(activity) < epsilon: + if activity < epsilon: continue var_cost = value(M.CostVariable[r, p, t, v]) From db005c66127ee5905d020d395c707395673c8c6f Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Sat, 14 Mar 2026 10:35:52 -0400 Subject: [PATCH 08/24] Fix retirement process period filtering --- .../model_checking/network_model_data.py | 66 +++++++++++-------- temoa/temoa_model/temoa_initialize.py | 36 +++++++--- 2 files changed, 65 insertions(+), 37 deletions(-) diff --git a/temoa/temoa_model/model_checking/network_model_data.py b/temoa/temoa_model/model_checking/network_model_data.py index 87a82729b..c5cf63951 100644 --- a/temoa/temoa_model/model_checking/network_model_data.py +++ b/temoa/temoa_model/model_checking/network_model_data.py @@ -183,7 +183,7 @@ def _build_from_db( tech_retire = {t[0] for t in raw} raw = cur.execute('SELECT DISTINCT region, tech, vintage FROM LifetimeSurvivalCurve').fetchall() tech_survival_curve = set(raw) - raw = cur.execute('SELECT period FROM TimePeriod').fetchall() + raw = cur.execute('SELECT period FROM TimePeriod WHERE flag == "f"').fetchall() periods = [p[0] for p in sorted(raw)] period_length = {periods[i]: periods[i+1] - periods[i] for i in range(len(periods)-1)} periods = periods[:-1] @@ -233,7 +233,7 @@ def _build_from_db( ' AND main.MyopicEfficiency.region = main.LifeTimeTech.region ' ' JOIN TimePeriod ' ' ON MyopicEfficiency.vintage = TimePeriod.period ' - # f' WHERE main.MyopicEfficiency.vintage <= {myopic_index.last_demand_year}' + f' WHERE main.MyopicEfficiency.vintage <= {myopic_index.last_demand_year}' ) raw = cur.execute(query).fetchall() # need to exclude the final year which is a non-demand year and should have no tech data @@ -241,11 +241,12 @@ def _build_from_db( # filter further if myopic if myopic_index: - periods = { + periods = [ p for p in periods if myopic_index.base_year <= p <= myopic_index.last_demand_year - } + ] techs = defaultdict(set) living_techs = set() # for screening the linked techs below + living_rtv = set() # filter out the dead ones... for element in raw: (r, ic, tech, v, oc, lifetime) = element @@ -267,33 +268,45 @@ def _build_from_db( else: techs[r, p].add(Tech(r, ic, tech, v, oc)) living_techs.add(tech) + living_rtv.add((r, tech, v)) if ic in source_comms: source_dict[r, p].add(ic) if oc in waste_comms: waste_dict[r, p].add(oc) - # End of life output - if any(( - p <= v+lifetime < p + period_length[p], # natural eol this period - tech in tech_retire and v < p <= v+lifetime - period_length[p], # allowed early retirement - (r, tech, v) in tech_survival_curve and v <= p <= v+lifetime - )): - try: - raw_eol = cur.execute( - 'SELECT region, tech, vintage, output_comm FROM EndOfLifeOutput ' - f' WHERE region == "{r}" AND tech == "{tech}" AND vintage == {v}' - ).fetchall() - - for _r, _tech, _v, _oc in raw_eol: - techs[_r, p].add(Tech(_r, _tech, _tech, _v, _oc)) - source_dict[_r, p].add(_tech) - res.capacity_commodities.add(_tech) - living_techs.add(_tech) - if _oc in waste_comms: - waste_dict[_r, p].add(_oc) - except: - # EndOfLifeOutput table did not exist TODO remove this eventually - pass + # End of life output + query = ( + ' SELECT main.EndOfLifeOutput.region, EndOfLifeOutput.tech, EndOfLifeOutput.vintage, EndOfLifeOutput.output_comm, ' + f' coalesce(main.LifetimeProcess.lifetime, main.LifetimeTech.lifetime, {default_lifetime}) AS lifetime ' + ' FROM main.EndOfLifeOutput ' + ' LEFT JOIN main.LifetimeProcess ' + ' ON main.EndOfLifeOutput.tech = LifetimeProcess.tech ' + ' AND main.EndOfLifeOutput.vintage = LifetimeProcess.vintage ' + ' AND main.EndOfLifeOutput.region = LifetimeProcess.region ' + ' LEFT JOIN main.LifetimeTech ' + ' ON main.EndOfLifeOutput.tech = main.LifetimeTech.tech ' + ' AND main.EndOfLifeOutput.region = main.LifeTimeTech.region ' + ' JOIN TimePeriod ' + ' ON EndOfLifeOutput.vintage = TimePeriod.period ' + ) + raw = cur.execute(query).fetchall() + for (r, tech, v, oc, lifetime) in raw: + for p in periods: + if ( + (p == periods[0] and v + lifetime == p) # retires on start of horizon + or ( + (r, tech, v) in living_rtv and any(( + p <= v+lifetime < p + period_length[p], # natural eol this period + tech in tech_retire and v < p <= v+lifetime - period_length[p], # allowed early retirement + (r, tech, v) in tech_survival_curve and v <= p <= v+lifetime # survival curve retirement + )) + ) + ): + techs[r, p].add(Tech(r, tech, tech, v, oc)) + source_dict[r, p].add(tech) + res.capacity_commodities.add(tech) + if oc in waste_comms: + waste_dict[r, p].add(oc) # Construction input try: @@ -304,7 +317,6 @@ def _build_from_db( techs[r, v].add(Tech(r, ic, tech, v, tech)) demand_dict[r, v].add(tech) res.capacity_commodities.add(tech) - living_techs.add(tech) except: # ConstructionInput table did not exist TODO remove this eventually pass diff --git a/temoa/temoa_model/temoa_initialize.py b/temoa/temoa_model/temoa_initialize.py index 2765c336e..ffac37043 100644 --- a/temoa/temoa_model/temoa_initialize.py +++ b/temoa/temoa_model/temoa_initialize.py @@ -808,16 +808,6 @@ def CreateSparseDicts(M: 'TemoaModel'): # l_loan_life = value(M.LoanLifetimeProcess[l_process]) # if v + l_loan_life >= p: # M.processLoans[pindex] = True - - # Get all periods where the process can retire - if t not in M.tech_uncap and any(( - p <= v+l_lifetime < p + value(M.PeriodLength[p]), # natural eol this period - t in M.tech_retirement and v < p <= v+l_lifetime - value(M.PeriodLength[p]), # allowed early retirement - M.isSurvivalCurveProcess[r, t, v] and v <= p <= v+l_lifetime - )): - if (r, t, v) not in M.retirementPeriods: - M.retirementPeriods[r, t, v] = set() - M.retirementPeriods[r, t, v].add(p) # if tech is no longer active, don't include it if v + l_lifetime <= p: @@ -945,6 +935,29 @@ def CreateSparseDicts(M: 'TemoaModel'): # logger.error(f_msg) # raise ValueError(f_msg) + # Get all periods where processes can retire + unique_rtv = { + (r, t, v) for r, _i, t, v, _o in M.Efficiency.sparse_iterkeys() + } | set(M.ExistingCapacity.sparse_iterkeys()) + for r, t, v in unique_rtv: + if t not in M.tech_all: + continue + for p in M.time_optimize: + lifetime = value(M.LifetimeProcess[r, t, v]) + if ( + (p == M.time_optimize.first() and v + lifetime == p) # retires on start of horizon + or ( + (r, t, v) in M.processPeriods and any(( + p <= v+lifetime < p + value(M.PeriodLength[p]), # natural eol this period + t in M.tech_retirement and v < p <= v+lifetime - value(M.PeriodLength[p]), # allowed early retirement + M.isSurvivalCurveProcess[r, t, v] and v <= p <= v+lifetime # survival curve retirement + )) + ) + ): + if (r, t, v) not in M.retirementPeriods: + M.retirementPeriods[r, t, v] = set() + M.retirementPeriods[r, t, v].add(p) + # Need this here for the commodity balance rpc set for r, i, t, v in M.ConstructionInput.sparse_iterkeys(): if (r, v, i) not in M.capacityConsumptionTechs: @@ -1269,6 +1282,8 @@ def CreateSurvivalCurve(M: 'TemoaModel'): for (r, _, t, v, _) in M.Efficiency.sparse_iterkeys(): M.isSurvivalCurveProcess[r, t, v] = False # by default + for (r, t, v) in M.ExistingCapacity.sparse_iterkeys(): + M.isSurvivalCurveProcess[r, t, v] = False # by default # Collect rptv indices into (r, t, v): p dictionary for r, p, t, v in M.LifetimeSurvivalCurve.sparse_iterkeys(): @@ -1468,6 +1483,7 @@ def LifetimeProcessIndices(M: 'TemoaModel'): process indices that may be specified in the LifetimeProcess parameter. """ indices = set((r, t, v) for r, i, t, v, o in M.Efficiency.sparse_iterkeys()) + indices = indices | set(M.ExistingCapacity.sparse_iterkeys()) return indices From 5879deea991a1925feecaada120934b4ce0fa13d Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Sat, 14 Mar 2026 10:36:07 -0400 Subject: [PATCH 09/24] Update network data test --- tests/test_network_model_data.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_network_model_data.py b/tests/test_network_model_data.py index e9b4e7a19..f842c495d 100644 --- a/tests/test_network_model_data.py +++ b/tests/test_network_model_data.py @@ -74,6 +74,7 @@ ('R1', 'p2', 't3', 2000, 'd1', 100), ('R1', 'p2', 't5', 2000, 'd2', 100), ], # techs + [], # no eol output [ (2020,), (2025,), @@ -120,6 +121,7 @@ ('R1', 'p1', 'driven', 1990, 'd2', 100), ('R1', 's1', 't1', 2000, 'd1', 100), ], # techs + [], # no eol output [ (2020,), (2025,), @@ -163,6 +165,7 @@ ('R1', 's2', 'driven', 1990, 'd2', 100), ('R1', 's1', 't1', 2000, 'd1', 100), ], # techs + [], # no eol output [ (2020,), (2025,), From a0f9d0dfae6c7a021f69c9542014996ac89ac660 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Sat, 14 Mar 2026 10:36:19 -0400 Subject: [PATCH 10/24] Remove weird unused existing cap in mediumville --- tests/testing_data/mediumville.sql | 1 - tests/testing_data/mediumville_sets.json | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/testing_data/mediumville.sql b/tests/testing_data/mediumville.sql index 6739e0d03..33e4352c9 100644 --- a/tests/testing_data/mediumville.sql +++ b/tests/testing_data/mediumville.sql @@ -426,7 +426,6 @@ CREATE TABLE ExistingCapacity notes TEXT, PRIMARY KEY (region, tech, vintage) ); -INSERT INTO ExistingCapacity VALUES('A','EH',2020,200.0,'things',NULL); CREATE TABLE TechGroup ( group_name TEXT diff --git a/tests/testing_data/mediumville_sets.json b/tests/testing_data/mediumville_sets.json index aa5267582..23638a207 100644 --- a/tests/testing_data/mediumville_sets.json +++ b/tests/testing_data/mediumville_sets.json @@ -4225,9 +4225,7 @@ "tech_exchange": [ "FGF_pipe" ], - "tech_exist": [ - "EH" - ], + "tech_exist": [], "tech_flex": [ "EFL" ], From 21ebdc9ce78c263213d95ba7d85998a5a5eee5d5 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Sat, 14 Mar 2026 10:36:56 -0400 Subject: [PATCH 11/24] Make materials test myopic to stress it more --- tests/testing_configs/config_materials.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/testing_configs/config_materials.toml b/tests/testing_configs/config_materials.toml index 71f23bf7b..6898a28ed 100644 --- a/tests/testing_configs/config_materials.toml +++ b/tests/testing_configs/config_materials.toml @@ -1,6 +1,6 @@ # this config is used for testing in test_full_runs.py scenario = "test run" -scenario_mode = "perfect_foresight" +scenario_mode = "myopic" input_database = "tests/testing_outputs/materials.sqlite" output_database = "tests/testing_outputs/materials.sqlite" @@ -69,6 +69,8 @@ weight = "integer" # currently supported: [integer, normalized] [myopic] myopic_view = 2 # number of periods seen at one iteration +view_depth = 1 +step_size = 1 From f8e3777511622b8f4b3c8f93d2b4f67d88b0d60b Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Sat, 14 Mar 2026 10:43:40 -0400 Subject: [PATCH 12/24] pedantry --- temoa/temoa_model/temoa_initialize.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/temoa/temoa_model/temoa_initialize.py b/temoa/temoa_model/temoa_initialize.py index ffac37043..78b6a0b26 100644 --- a/temoa/temoa_model/temoa_initialize.py +++ b/temoa/temoa_model/temoa_initialize.py @@ -942,8 +942,8 @@ def CreateSparseDicts(M: 'TemoaModel'): for r, t, v in unique_rtv: if t not in M.tech_all: continue + lifetime = value(M.LifetimeProcess[r, t, v]) for p in M.time_optimize: - lifetime = value(M.LifetimeProcess[r, t, v]) if ( (p == M.time_optimize.first() and v + lifetime == p) # retires on start of horizon or ( From 5ea1bdb25111e5718c49fee1ebf075f14bf28adc Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Mon, 16 Mar 2026 11:53:08 -0400 Subject: [PATCH 13/24] Cant retire unlim_cap techs --- temoa/temoa_model/temoa_initialize.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/temoa/temoa_model/temoa_initialize.py b/temoa/temoa_model/temoa_initialize.py index 78b6a0b26..d2bf58840 100644 --- a/temoa/temoa_model/temoa_initialize.py +++ b/temoa/temoa_model/temoa_initialize.py @@ -940,7 +940,11 @@ def CreateSparseDicts(M: 'TemoaModel'): (r, t, v) for r, _i, t, v, _o in M.Efficiency.sparse_iterkeys() } | set(M.ExistingCapacity.sparse_iterkeys()) for r, t, v in unique_rtv: + if t in M.tech_uncap: + # No capacity to retire + continue if t not in M.tech_all: + # Not an active technology so wont have a lifetime continue lifetime = value(M.LifetimeProcess[r, t, v]) for p in M.time_optimize: From adbd164c5a63aec2a482609cac99cbae427ad8f2 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Mon, 16 Mar 2026 12:01:29 -0400 Subject: [PATCH 14/24] Also check for uncap techs in network_model_data --- temoa/temoa_model/model_checking/network_model_data.py | 8 ++++++++ tests/test_network_model_data.py | 3 +++ 2 files changed, 11 insertions(+) diff --git a/temoa/temoa_model/model_checking/network_model_data.py b/temoa/temoa_model/model_checking/network_model_data.py index c5cf63951..995c1bcdc 100644 --- a/temoa/temoa_model/model_checking/network_model_data.py +++ b/temoa/temoa_model/model_checking/network_model_data.py @@ -179,6 +179,8 @@ def _build_from_db( # re-use some of the hybrid loader code in a clear way. Not too much overlap, though res = NetworkModelData() cur = con.cursor() + raw = cur.execute('SELECT tech FROM Technology WHERE unlim_cap==1').fetchall() + tech_uncap = {t[0] for t in raw} raw = cur.execute('SELECT tech FROM Technology WHERE retire==1').fetchall() tech_retire = {t[0] for t in raw} raw = cur.execute('SELECT DISTINCT region, tech, vintage FROM LifetimeSurvivalCurve').fetchall() @@ -291,6 +293,9 @@ def _build_from_db( ) raw = cur.execute(query).fetchall() for (r, tech, v, oc, lifetime) in raw: + if tech in tech_uncap: + # No capacity to retire + continue for p in periods: if ( (p == periods[0] and v + lifetime == p) # retires on start of horizon @@ -312,6 +317,9 @@ def _build_from_db( try: raw = cur.execute('SELECT region, input_comm, tech, vintage FROM ConstructionInput').fetchall() for r, ic, tech, v in raw: + if tech in tech_uncap: + # No capacity to construct + continue if v not in periods: continue techs[r, v].add(Tech(r, ic, tech, v, tech)) diff --git a/tests/test_network_model_data.py b/tests/test_network_model_data.py index f842c495d..e1e7f9767 100644 --- a/tests/test_network_model_data.py +++ b/tests/test_network_model_data.py @@ -51,6 +51,7 @@ { 'name': 'basic', 'data': [ + [], # unlimited capacity techs [], # retirement techs [], # survival curve techs [ @@ -101,6 +102,7 @@ { 'name': 'bad linked tech', 'data': [ + [], # unlimited capacity techs [], # retirement techs [], # survival curve techs [ @@ -150,6 +152,7 @@ # 'name': 'good linked tech', 'data': [ + [], # unlimited capacity techs [], # retirement techs [], # survival curve techs [ From 00d90b178627e65d7a0adce3e2535953b919d31f Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Mon, 16 Mar 2026 12:12:52 -0400 Subject: [PATCH 15/24] Clarify a comment --- temoa/temoa_model/temoa_initialize.py | 1 + 1 file changed, 1 insertion(+) diff --git a/temoa/temoa_model/temoa_initialize.py b/temoa/temoa_model/temoa_initialize.py index d2bf58840..893c4b316 100644 --- a/temoa/temoa_model/temoa_initialize.py +++ b/temoa/temoa_model/temoa_initialize.py @@ -945,6 +945,7 @@ def CreateSparseDicts(M: 'TemoaModel'): continue if t not in M.tech_all: # Not an active technology so wont have a lifetime + # If it has an EOLoutput it will be in tech_all continue lifetime = value(M.LifetimeProcess[r, t, v]) for p in M.time_optimize: From 0ec3b71969f438d0ff64643342e91d4b0984fa40 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Mon, 16 Mar 2026 16:25:09 -0400 Subject: [PATCH 16/24] Fix edges for material flows --- temoa/temoa_model/model_checking/commodity_graph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/temoa/temoa_model/model_checking/commodity_graph.py b/temoa/temoa_model/model_checking/commodity_graph.py index b103f7a0e..17492e3d5 100644 --- a/temoa/temoa_model/model_checking/commodity_graph.py +++ b/temoa/temoa_model/model_checking/commodity_graph.py @@ -89,7 +89,7 @@ def generate_graph( } cap_edges = { (tech.ic, tech.name, tech.oc) for tech in network_data.available_techs[region, period] - if tech.name in ('Construction','EndOfLife') + if tech.name == tech.ic or tech.name == tech.oc } exc_edges = { (tech.ic, tech.name, tech.oc) for tech in network_data.available_techs[region, period] From 43ca0a6548c97eab4440e02dfe3272f87a6fe62c Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Mon, 16 Mar 2026 16:25:54 -0400 Subject: [PATCH 17/24] Update constructioninput in network model data --- .../model_checking/network_model_data.py | 24 ++++++++----------- tests/test_network_model_data.py | 15 +++--------- 2 files changed, 13 insertions(+), 26 deletions(-) diff --git a/temoa/temoa_model/model_checking/network_model_data.py b/temoa/temoa_model/model_checking/network_model_data.py index 995c1bcdc..abc788ba9 100644 --- a/temoa/temoa_model/model_checking/network_model_data.py +++ b/temoa/temoa_model/model_checking/network_model_data.py @@ -314,20 +314,16 @@ def _build_from_db( waste_dict[r, p].add(oc) # Construction input - try: - raw = cur.execute('SELECT region, input_comm, tech, vintage FROM ConstructionInput').fetchall() - for r, ic, tech, v in raw: - if tech in tech_uncap: - # No capacity to construct - continue - if v not in periods: - continue - techs[r, v].add(Tech(r, ic, tech, v, tech)) - demand_dict[r, v].add(tech) - res.capacity_commodities.add(tech) - except: - # ConstructionInput table did not exist TODO remove this eventually - pass + raw = cur.execute('SELECT region, input_comm, tech, vintage FROM ConstructionInput').fetchall() + for r, ic, tech, v in raw: + if tech in tech_uncap: + # No capacity to construct + continue + if v not in periods: + continue + techs[r, v].add(Tech(r, ic, tech, v, tech)) + demand_dict[r, v].add(tech) + res.capacity_commodities.add(tech) res.available_techs = techs res.demand_commodities = demand_dict diff --git a/tests/test_network_model_data.py b/tests/test_network_model_data.py index e1e7f9767..d20e58917 100644 --- a/tests/test_network_model_data.py +++ b/tests/test_network_model_data.py @@ -76,10 +76,7 @@ ('R1', 'p2', 't5', 2000, 'd2', 100), ], # techs [], # no eol output - [ - (2020,), - (2025,), - ], # periods + [], # no construction input [], # no linked techs [], # no negative cost techs ], @@ -124,10 +121,7 @@ ('R1', 's1', 't1', 2000, 'd1', 100), ], # techs [], # no eol output - [ - (2020,), - (2025,), - ], # periods + [], # no construction input [('R1', 't4', 'nox', 'driven')], # t4 drives 'driven' with 'nox' emission [], # no negative cost techs ], @@ -169,10 +163,7 @@ ('R1', 's1', 't1', 2000, 'd1', 100), ], # techs [], # no eol output - [ - (2020,), - (2025,), - ], # periods + [], # no construction input [('R1', 't4', 'nox', 'driven')], # t4 drives 'driven' with 'nox' emission [], # no negative cost techs ], From 4cba5f107de1b0a557978441db7ec41e4aa07207 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Mon, 16 Mar 2026 18:55:02 -0400 Subject: [PATCH 18/24] Add silent_rptv validation set to support emissionendoflife --- temoa/temoa_model/hybrid_loader.py | 2 +- .../commodity_network_manager.py | 6 ++++ .../model_checking/network_model_data.py | 28 +++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/temoa/temoa_model/hybrid_loader.py b/temoa/temoa_model/hybrid_loader.py index 7ceecd983..babadae1c 100644 --- a/temoa/temoa_model/hybrid_loader.py +++ b/temoa/temoa_model/hybrid_loader.py @@ -1000,7 +1000,7 @@ def load_indexed_set(indexed_set: Set, index_value, element, element_validator=N 'SELECT region, emis_comm, tech, vintage, value ' 'FROM main.EmissionEndOfLife' ).fetchall() - load_element(M.EmissionEndOfLife, raw, self.viable_rtv_eol, (0, 2, 3)) + load_element(M.EmissionEndOfLife, raw, self.viable_rtv, (0, 2, 3)) # ConstructionInput if self.table_exists('ConstructionInput'): diff --git a/temoa/temoa_model/model_checking/commodity_network_manager.py b/temoa/temoa_model/model_checking/commodity_network_manager.py index d508d021a..e3b9657a9 100644 --- a/temoa/temoa_model/model_checking/commodity_network_manager.py +++ b/temoa/temoa_model/model_checking/commodity_network_manager.py @@ -134,6 +134,12 @@ def build_filters(self) -> dict[str, ViableSet]: valid_output_commodities.add(tech.oc) valid_vintages.add(tech.vintage) + for r, p, t, v in self.filtered_data.silent_rptv: + valid_rtv.add((r, t, v)) + valid_rt.add((r, t)) + valid_t.add(t) + valid_vintages.add(v) + filts = { 'ritvo': ViableSet( elements=valid_ritvo, exception_loc=0, exception_vals=ViableSet.REGION_REGEXES diff --git a/temoa/temoa_model/model_checking/network_model_data.py b/temoa/temoa_model/model_checking/network_model_data.py index abc788ba9..b74a2af20 100644 --- a/temoa/temoa_model/model_checking/network_model_data.py +++ b/temoa/temoa_model/model_checking/network_model_data.py @@ -65,6 +65,7 @@ def __init__(self, **kwargs): 'available_techs' ) self.available_linked_techs: set[LinkedTech] = kwargs.get('available_linked_techs', set()) + self.silent_rptv: set[str] = kwargs.get('silent_rptv', set()) # a catch-all for indicators for techs...growth potential # dev note: this is indexed by tech name, and is blind to vintage. The intended use is in the # network graph, which is also blind to vintage. So it is interpreted as "at least one" @@ -82,6 +83,7 @@ def clone(self) -> Self: all_commodities=self.physical_commodities.copy(), available_techs=self.available_techs.copy(), available_linked_techs=self.available_linked_techs.copy(), + silent_rptv=self.silent_rptv.copy(), ) @property @@ -193,6 +195,7 @@ def _build_from_db( res.physical_commodities = {c[0] for c in raw} res.capacity_commodities = set() res.exchange_commodities = set() + res.silent_rptv = set() raw = cur.execute("SELECT Commodity.name FROM Commodity WHERE flag LIKE '%w%'").fetchall() waste_comms = {c[0] for c in raw} waste_dict = defaultdict(set) @@ -313,6 +316,31 @@ def _build_from_db( if oc in waste_comms: waste_dict[r, p].add(oc) + # Emission end of life + query = ( + ' SELECT main.EmissionEndOfLife.region, EmissionEndOfLife.tech, EmissionEndOfLife.vintage, ' + f' coalesce(main.LifetimeProcess.lifetime, main.LifetimeTech.lifetime, {default_lifetime}) AS lifetime ' + ' FROM main.EmissionEndOfLife ' + ' LEFT JOIN main.LifetimeProcess ' + ' ON main.EmissionEndOfLife.tech = LifetimeProcess.tech ' + ' AND main.EmissionEndOfLife.vintage = LifetimeProcess.vintage ' + ' AND main.EmissionEndOfLife.region = LifetimeProcess.region ' + ' LEFT JOIN main.LifetimeTech ' + ' ON main.EmissionEndOfLife.tech = main.LifetimeTech.tech ' + ' AND main.EmissionEndOfLife.region = main.LifeTimeTech.region ' + ' JOIN TimePeriod ' + ' ON EmissionEndOfLife.vintage = TimePeriod.period ' + ) + raw = cur.execute(query).fetchall() + for (r, tech, v, lifetime) in raw: + if tech in tech_uncap: + # No capacity to retire + continue + if exs_cap.get((r, tech, v), 0) <= 0: + continue + if v + lifetime == periods[0]: + res.silent_rptv.add((r, periods[0], tech, v)) + # Construction input raw = cur.execute('SELECT region, input_comm, tech, vintage FROM ConstructionInput').fetchall() for r, ic, tech, v in raw: From 0fff517f697e087a9604db938c63c09bf923a57e Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Mon, 16 Mar 2026 18:55:30 -0400 Subject: [PATCH 19/24] Add emission end of life to checks --- temoa/temoa_model/temoa_initialize.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/temoa/temoa_model/temoa_initialize.py b/temoa/temoa_model/temoa_initialize.py index 893c4b316..7bc58a721 100644 --- a/temoa/temoa_model/temoa_initialize.py +++ b/temoa/temoa_model/temoa_initialize.py @@ -282,6 +282,7 @@ def CheckEfficiencyIndices(M: 'TemoaModel'): techs = set(t for r, i, t, v, o in M.Efficiency.sparse_iterkeys()) techs = techs | set(t for r, t, v, o in M.EndOfLifeOutput.sparse_iterkeys()) techs = techs | set(t for r, i, t, v in M.ConstructionInput.sparse_iterkeys()) + techs = techs | set(t for r, e, t, v in M.EmissionEndOfLife.sparse_iterkeys()) symdiff = techs.symmetric_difference(M.tech_all) if symdiff: @@ -978,6 +979,11 @@ def CreateSparseDicts(M: 'TemoaModel'): M.retirementProductionProcesses[r, p, o] = set() M.retirementProductionProcesses[r, p, o].add((t, v)) l_used_techs.add(t) + for r, e, t, v in M.EmissionEndOfLife.sparse_iterkeys(): + if (r, t, v) not in M.retirementPeriods: + continue # might be running myopic + l_used_techs.add(t) + l_unused_techs = M.tech_all - l_used_techs if l_unused_techs: From 040375a80cb354c5a24a5ebcba45a510e69dccc8 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Mon, 16 Mar 2026 18:57:17 -0400 Subject: [PATCH 20/24] Add existing capacity check to p0 retirement --- temoa/temoa_model/temoa_initialize.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/temoa/temoa_model/temoa_initialize.py b/temoa/temoa_model/temoa_initialize.py index 7bc58a721..c12d41e15 100644 --- a/temoa/temoa_model/temoa_initialize.py +++ b/temoa/temoa_model/temoa_initialize.py @@ -951,7 +951,11 @@ def CreateSparseDicts(M: 'TemoaModel'): lifetime = value(M.LifetimeProcess[r, t, v]) for p in M.time_optimize: if ( - (p == M.time_optimize.first() and v + lifetime == p) # retires on start of horizon + ( + value(M.ExistingCapacity[r, t, v]) > 0 + and p == M.time_optimize.first() + and v + lifetime == p + ) # retires on start of horizon or ( (r, t, v) in M.processPeriods and any(( p <= v+lifetime < p + value(M.PeriodLength[p]), # natural eol this period From 00f703744320b3844c07ce4c315504fe0615c0e0 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Mon, 16 Mar 2026 18:57:32 -0400 Subject: [PATCH 21/24] Add existing capacity check to network model data --- .../temoa_model/model_checking/network_model_data.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/temoa/temoa_model/model_checking/network_model_data.py b/temoa/temoa_model/model_checking/network_model_data.py index b74a2af20..539c32bfb 100644 --- a/temoa/temoa_model/model_checking/network_model_data.py +++ b/temoa/temoa_model/model_checking/network_model_data.py @@ -279,6 +279,15 @@ def _build_from_db( if oc in waste_comms: waste_dict[r, p].add(oc) + # ExistingCapacity for checking + query = ( + 'SELECT region, tech, vintage, capacity FROM main.ExistingCapacity' + ) + raw = cur.execute(query).fetchall() + exs_cap = dict() + for r, tech, v, cap in raw: + exs_cap[r, tech, v] = cap + # End of life output query = ( ' SELECT main.EndOfLifeOutput.region, EndOfLifeOutput.tech, EndOfLifeOutput.vintage, EndOfLifeOutput.output_comm, ' @@ -299,6 +308,8 @@ def _build_from_db( if tech in tech_uncap: # No capacity to retire continue + if exs_cap.get((r, tech, v), 0) <= 0: + continue for p in periods: if ( (p == periods[0] and v + lifetime == p) # retires on start of horizon From 67aa9e2b61bf059601fd095f052b4f950dae5f36 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Mon, 16 Mar 2026 19:18:28 -0400 Subject: [PATCH 22/24] Rearrange existing capacity check so it doesnt call on new capacity --- temoa/temoa_model/temoa_initialize.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/temoa/temoa_model/temoa_initialize.py b/temoa/temoa_model/temoa_initialize.py index c12d41e15..e3a517f40 100644 --- a/temoa/temoa_model/temoa_initialize.py +++ b/temoa/temoa_model/temoa_initialize.py @@ -952,9 +952,9 @@ def CreateSparseDicts(M: 'TemoaModel'): for p in M.time_optimize: if ( ( - value(M.ExistingCapacity[r, t, v]) > 0 - and p == M.time_optimize.first() + p == M.time_optimize.first() and v + lifetime == p + and value(M.ExistingCapacity[r, t, v]) > 0 ) # retires on start of horizon or ( (r, t, v) in M.processPeriods and any(( From 779d66c610c993b86433c94c91eb9782efd53f43 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Mon, 16 Mar 2026 19:18:37 -0400 Subject: [PATCH 23/24] Add existing capacity check --- temoa/temoa_model/temoa_initialize.py | 21 +++++++++++++++++++++ temoa/temoa_model/temoa_model.py | 1 + 2 files changed, 22 insertions(+) diff --git a/temoa/temoa_model/temoa_initialize.py b/temoa/temoa_model/temoa_initialize.py index e3a517f40..81a472a48 100644 --- a/temoa/temoa_model/temoa_initialize.py +++ b/temoa/temoa_model/temoa_initialize.py @@ -360,6 +360,27 @@ def CheckEfficiencyVariable(M: 'TemoaModel'): ) +def CheckExistingCapacity(M: 'TemoaModel'): + """ + Check that all existing capacity vintages have a valid lifetime and are properly accounted for in the model. + """ + for r, t, v in M.ExistingCapacity.sparse_iterkeys(): + cap = value(M.ExistingCapacity[r, t, v]) + if t not in M.tech_all: + continue + if cap <= 0: + msg = f"Existing capacity {r, t, v} has non-positive capacity {cap}. This entry will be ignored." + logger.warning(msg) + life = value(M.LifetimeProcess[r, t, v]) + if (r, t, v) not in M.processPeriods and v + life > M.time_optimize.first(): + msg = ( + f"Existing capacity {r, t, v} with lifetime {life} and capacity {cap} should extend into " + "future periods but it not in process periods. Was it included in the Efficiency table?" + ) + logger.error(msg) + raise ValueError(msg) + + def CheckCapacityFactorProcess(M: 'TemoaModel'): count_rptv = dict() diff --git a/temoa/temoa_model/temoa_model.py b/temoa/temoa_model/temoa_model.py index 81e554fb9..8ff609ade 100755 --- a/temoa/temoa_model/temoa_model.py +++ b/temoa/temoa_model/temoa_model.py @@ -384,6 +384,7 @@ def __init__(M, *args, **kwargs): # equations below. M.Create_SparseDicts = BuildAction(rule=CreateSparseDicts) M.initialize_Demands = BuildAction(rule=CreateDemands) + M.validate_ExistingCapacity = BuildAction(rule=CheckExistingCapacity) M.CapacityFactor_rpsdt = Set(dimen=5, initialize=CapacityFactorTechIndices) M.CapacityFactorTech = Param(M.CapacityFactor_rpsdt, default=1, validate=validate_0to1) From 601602f540c4bcee2b06b92b3cf8720da1064b3a Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Mon, 16 Mar 2026 19:23:03 -0400 Subject: [PATCH 24/24] Fix network model data test --- tests/test_network_model_data.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_network_model_data.py b/tests/test_network_model_data.py index d20e58917..ff06c6166 100644 --- a/tests/test_network_model_data.py +++ b/tests/test_network_model_data.py @@ -75,7 +75,9 @@ ('R1', 'p2', 't3', 2000, 'd1', 100), ('R1', 'p2', 't5', 2000, 'd2', 100), ], # techs + [], # no existing capacity [], # no eol output + [], # no emission end of life [], # no construction input [], # no linked techs [], # no negative cost techs @@ -120,7 +122,9 @@ ('R1', 'p1', 'driven', 1990, 'd2', 100), ('R1', 's1', 't1', 2000, 'd1', 100), ], # techs + [], # no existing capacity [], # no eol output + [], # no emission end of life [], # no construction input [('R1', 't4', 'nox', 'driven')], # t4 drives 'driven' with 'nox' emission [], # no negative cost techs @@ -162,7 +166,9 @@ ('R1', 's2', 'driven', 1990, 'd2', 100), ('R1', 's1', 't1', 2000, 'd1', 100), ], # techs + [], # no existing capacity [], # no eol output + [], # no emission end of life [], # no construction input [('R1', 't4', 'nox', 'driven')], # t4 drives 'driven' with 'nox' emission [], # no negative cost techs