diff --git a/temoa/components/capacity.py b/temoa/components/capacity.py index e583d11d..cee04d3f 100644 --- a/temoa/components/capacity.py +++ b/temoa/components/capacity.py @@ -284,8 +284,11 @@ def annual_retirement_constraint( \\\text{where EOL when } p \leq v + LTP_{r,t,v} < p + LEN_p """ + p_end = p + value(model.period_length[p]) + eol_year = v + value(model.lifetime_process[r, t, v]) + ## Get the capacity at the start of this period - if p == v + value(model.lifetime_process[r, t, v]): + if p == eol_year: # Exact EOL. No v_capacity or v_retired_capacity for this period. if p == model.time_optimize.first(): # Must be existing capacity. Apply survival curve to existing cap @@ -307,30 +310,35 @@ def annual_retirement_constraint( ) ## Get the capacity at the end of this period - if p <= v + value(model.lifetime_process[r, t, v]) < p + value(model.period_length[p]): - # EOL so capacity ends on zero + if p <= eol_year < p_end: + # EOL so capacity ends on zero no matter what cap_end = 0 + elif p == model.time_optimize.last() or p_end == eol_year: + # No v_capacity or v_retired_capacity for next period so just continue down the + # survival curve. If eol_year = p_end then eol would be dumped in the next period + cap_end = ( + cap_begin + * value(model.lifetime_survival_curve[r, p_end, t, v]) + / value(model.lifetime_survival_curve[r, p, t, v]) + ) else: - # Mid-life period, ending capacity is beginning capacity of next period - p_next = model.time_future.next(p) - - if p == model.time_optimize.last() or p_next == v + value(model.lifetime_process[r, t, v]): - # No v_capacity or v_retired_capacity for next period so just continue down the - # survival curve - cap_end = ( - cap_begin - * value(model.lifetime_survival_curve[r, p_next, t, v]) - / value(model.lifetime_survival_curve[r, p, t, v]) - ) - else: - # Get the next period's beginning capacity - cap_end = ( - model.v_capacity[r, p_next, t, v] - * value(model.lifetime_survival_curve[r, p_next, t, v]) - / value(model.process_life_frac[r, p_next, t, v]) - ) + # Get the next period's beginning capacity + p_next = model.time_optimize.next(p) + cap_end = ( + model.v_capacity[r, p_next, t, v] + * value(model.lifetime_survival_curve[r, p_next, t, v]) + / value(model.process_life_frac[r, p_next, t, v]) + ) + # next v_capacity also accounts for decision retirement so need to undo that again + p_next_end = p_next + value(model.period_length[p_next]) + if t in model.tech_retirement and p_next_end <= eol_year: + cap_end += model.v_retired_capacity[r, p_next, t, v] + + # v_capacity already accounts for decision retirement so need to undo that for beginning cap + if t in model.tech_retirement and p_end <= eol_year: + cap_begin += model.v_retired_capacity[r, p, t, v] - annualised_retirement = (cap_begin - cap_end) / model.period_length[p] + annualised_retirement = (cap_begin - cap_end) / value(model.period_length[p]) return model.v_annual_retirement[r, p, t, v] == annualised_retirement @@ -546,10 +554,10 @@ def adjusted_capacity_constraint( if t in model.tech_retirement: early_retirements = sum( model.v_retired_capacity[r, S_p, t, v] - / value(model.lifetime_survival_curve[r, S_p, t, v]) + / value(model.lifetime_survival_curve[r, S_p, t, v]) # relative survival since then for S_p in model.time_optimize if v < S_p <= p - and S_p < v + value(model.lifetime_process[r, t, v]) - value(model.period_length[S_p]) + and S_p + value(model.period_length[S_p]) <= v + value(model.lifetime_process[r, t, v]) ) remaining_capacity = (built_capacity - early_retirements) * value( diff --git a/temoa/components/utils.py b/temoa/components/utils.py index 2fd417a3..943b9018 100644 --- a/temoa/components/utils.py +++ b/temoa/components/utils.py @@ -8,7 +8,7 @@ from __future__ import annotations -from enum import Enum +from enum import StrEnum from logging import getLogger from typing import TYPE_CHECKING @@ -33,7 +33,7 @@ logger = getLogger(__name__) -class Operator(str, Enum): +class Operator(StrEnum): EQUAL = 'e' LESS_EQUAL = 'le' GREATER_EQUAL = 'ge' diff --git a/temoa/data_io/component_manifest.py b/temoa/data_io/component_manifest.py index 65093d33..e22fdf67 100644 --- a/temoa/data_io/component_manifest.py +++ b/temoa/data_io/component_manifest.py @@ -292,10 +292,9 @@ def build_manifest(model: TemoaModel) -> list[LoadItem]: component=model.cost_invest, table='cost_invest', columns=['region', 'tech', 'vintage', 'cost'], - validator_name='viable_rtv', + validator_name='viable_rtv_new', validation_map=(0, 1, 2), - custom_loader_name='_load_cost_invest', - is_period_filtered=False, # Custom loader handles this + is_period_filtered=False, is_table_required=False, ), LoadItem( @@ -322,10 +321,9 @@ def build_manifest(model: TemoaModel) -> list[LoadItem]: component=model.loan_rate, table='loan_rate', columns=['region', 'tech', 'vintage', 'rate'], - validator_name='viable_rtv', + validator_name='viable_rtv_new', validation_map=(0, 1, 2), - custom_loader_name='_load_loan_rate', - is_period_filtered=False, # Custom loader handles this + is_period_filtered=False, is_table_required=False, ), # ========================================================================= @@ -448,7 +446,7 @@ def build_manifest(model: TemoaModel) -> list[LoadItem]: component=model.loan_lifetime_process, table='loan_lifetime_process', columns=['region', 'tech', 'vintage', 'lifetime'], - validator_name='viable_rtv', + validator_name='viable_rtv_new', validation_map=(0, 1, 2), is_period_filtered=False, is_table_required=False, @@ -566,9 +564,8 @@ def build_manifest(model: TemoaModel) -> list[LoadItem]: component=model.emission_embodied, table='emission_embodied', columns=['region', 'emis_comm', 'tech', 'vintage', 'value'], - validator_name='viable_rtv', + validator_name='viable_rtv_new', validation_map=(0, 2, 3), - custom_loader_name='_load_emission_embodied', is_period_filtered=False, is_table_required=False, ), @@ -585,9 +582,8 @@ def build_manifest(model: TemoaModel) -> list[LoadItem]: component=model.construction_input, table='construction_input', columns=['region', 'input_comm', 'tech', 'vintage', 'value'], - validator_name='viable_rtv', + validator_name='viable_rtv_new', validation_map=(0, 2, 3), - custom_loader_name='_load_construction_input', is_period_filtered=False, is_table_required=False, ), @@ -615,7 +611,7 @@ def build_manifest(model: TemoaModel) -> list[LoadItem]: component=model.limit_new_capacity, table='limit_new_capacity', columns=['region', 'tech_or_group', 'vintage', 'operator', 'new_cap'], - validator_name='viable_rtv', + validator_name='viable_rtv_new', validation_map=(0, 1, 2), is_period_filtered=False, is_table_required=False, @@ -632,7 +628,7 @@ def build_manifest(model: TemoaModel) -> list[LoadItem]: component=model.limit_new_capacity_share, table='limit_new_capacity_share', columns=['region', 'sub_group', 'super_group', 'vintage', 'operator', 'share'], - validator_name='viable_rtv', + validator_name='viable_rtv_new', validation_map=(0, 1, 3), is_period_filtered=False, is_table_required=False, @@ -665,7 +661,7 @@ def build_manifest(model: TemoaModel) -> list[LoadItem]: LoadItem( component=model.limit_seasonal_capacity_factor, table='limit_seasonal_capacity_factor', - columns=['region', 'season', 'tech', 'operator', 'factor'], + columns=['region', 'season', 'tech_or_group', 'operator', 'factor'], validator_name='viable_rt', validation_map=(0, 2), is_period_filtered=False, diff --git a/temoa/data_io/hybrid_loader.py b/temoa/data_io/hybrid_loader.py index 3bf3b5e8..a4e2ce4d 100644 --- a/temoa/data_io/hybrid_loader.py +++ b/temoa/data_io/hybrid_loader.py @@ -107,9 +107,11 @@ def __init__(self, db_connection: Connection, config: TemoaConfig) -> None: self.viable_vintages: ViableSet | None = None self.viable_ritvo: ViableSet | None = None self.viable_rtvo: ViableSet | None = None + self.viable_rtv_new: ViableSet | None = None self.viable_rpt: ViableSet | None = None self.viable_rpto: ViableSet | None = None self.viable_rtv: ViableSet | None = None + self.viable_rtv_eol: ViableSet | None = None self.viable_rt: ViableSet | None = None self.viable_rpit: ViableSet | None = None self.viable_rtt: ViableSet | None = None @@ -470,6 +472,7 @@ def _build_efficiency_dataset( filts = self.manager.build_filters(tech_groups) self.viable_ritvo = filts['ritvo'] self.viable_rtvo = filts['rtvo'] + self.viable_rtv_new = filts['rtv_new'] self.viable_rpt = filts['rpt'] self.viable_rtv = filts['rtv'] self.viable_rt = filts['rt'] @@ -627,60 +630,6 @@ def _load_existing_capacity( tech_exist_data = sorted({(row[1],) for row in rows_to_load}) self._load_component_data(data, model.tech_exist, tech_exist_data) - def _load_cost_invest( - self, - data: dict[str, object], - raw_data: Sequence[tuple[object, ...]], - filtered_data: Sequence[tuple[object, ...]], - ) -> None: - """Handles myopic period filtering for cost_invest.""" - model = TemoaModel() - base_year = self.myopic_index.base_year if self.myopic_index else None - data_to_load = [ - row for row in filtered_data if base_year is None or cast('int', row[2]) >= base_year - ] - self._load_component_data(data, model.cost_invest, data_to_load) - - def _load_loan_rate( - self, - data: dict[str, object], - raw_data: Sequence[tuple[object, ...]], - filtered_data: Sequence[tuple[object, ...]], - ) -> None: - """Handles myopic period filtering for loan_rate.""" - model = TemoaModel() - mi = self.myopic_index - data_to_load = [row for row in filtered_data if not mi or row[2] >= mi.base_year] # type: ignore[operator] - self._load_component_data(data, model.loan_rate, data_to_load) - - def _load_construction_input( - self, - data: dict[str, object], - raw_data: Sequence[tuple[object, ...]], - filtered_data: Sequence[tuple[object, ...]], - ) -> None: - """Handles myopic period filtering for construction_input.""" - model = TemoaModel() - base_year = self.myopic_index.base_year if self.myopic_index else None - data_to_load = [ - row for row in filtered_data if base_year is None or cast('int', row[3]) >= base_year - ] - self._load_component_data(data, model.construction_input, data_to_load) - - def _load_emission_embodied( - self, - data: dict[str, object], - raw_data: Sequence[tuple[object, ...]], - filtered_data: Sequence[tuple[object, ...]], - ) -> None: - """Handles myopic period filtering for emission_embodied.""" - model = TemoaModel() - base_year = self.myopic_index.base_year if self.myopic_index else None - data_to_load = [ - row for row in filtered_data if base_year is None or cast('int', row[3]) >= base_year - ] - self._load_component_data(data, model.emission_embodied, data_to_load) - # --- Singleton and Configuration-based Components --- def _load_global_discount_rate( self, diff --git a/temoa/model_checking/commodity_network_manager.py b/temoa/model_checking/commodity_network_manager.py index 75afc390..b3eb91dc 100644 --- a/temoa/model_checking/commodity_network_manager.py +++ b/temoa/model_checking/commodity_network_manager.py @@ -12,7 +12,7 @@ from collections import defaultdict from collections.abc import Iterable from logging import getLogger -from typing import Any +from typing import Any, cast from temoa.core.config import TemoaConfig from temoa.model_checking.commodity_graph import visualize_graph @@ -143,6 +143,11 @@ def build_filters(self, tech_groups: defaultdict[str, set[str]]) -> dict[str, Vi valid_elements['ic'].add(edge_tuple.input_comm) valid_elements['oc'].add(edge_tuple.output_comm) + if cast('int', p) == cast('int', edge_tuple.vintage): + valid_elements['rtv_new'].add( + (edge_tuple.region, edge_tuple.tech, edge_tuple.vintage) + ) + for tech_group in tech_groups.get(edge_tuple.tech, {}): valid_elements['rtvo'].add( ( @@ -162,6 +167,11 @@ def build_filters(self, tech_groups: defaultdict[str, set[str]]) -> dict[str, Vi (edge_tuple.region, p, tech_group, edge_tuple.output_comm) ) + if cast('int', p) == cast('int', edge_tuple.vintage): + valid_elements['rtv_new'].add( + (edge_tuple.region, tech_group, edge_tuple.vintage) + ) + # Good processes that we dont want in the network diagram for r, p, t, v in self.orig_data.silent_rptv: valid_elements['rpt'].add((r, p, t)) @@ -170,6 +180,9 @@ def build_filters(self, tech_groups: defaultdict[str, set[str]]) -> dict[str, Vi valid_elements['t'].add(t) valid_elements['v'].add(v) + if cast('int', p) == cast('int', v): + valid_elements['rtv_new'].add((r, t, v)) + return { 'ritvo': ViableSet( elements=valid_elements['ritvo'], @@ -181,6 +194,11 @@ def build_filters(self, tech_groups: defaultdict[str, set[str]]) -> dict[str, Vi exception_loc=0, exception_vals=ViableSet.REGION_REGEXES, ), + 'rtv_new': ViableSet( + elements=valid_elements['rtv_new'], + exception_loc=0, + exception_vals=ViableSet.REGION_REGEXES, + ), 'rtv': ViableSet( elements=valid_elements['rtv'], exception_loc=0, diff --git a/temoa/types/solver_types.py b/temoa/types/solver_types.py index 1893f8fa..9b7d2cff 100644 --- a/temoa/types/solver_types.py +++ b/temoa/types/solver_types.py @@ -6,7 +6,7 @@ """ from collections.abc import Mapping -from enum import Enum +from enum import Enum, StrEnum from typing import TYPE_CHECKING, Any, Protocol if TYPE_CHECKING: @@ -18,7 +18,7 @@ PyomoTerminationCondition = Any -class SolverStatusEnum(str, Enum): +class SolverStatusEnum(StrEnum): """ Enumeration of possible solver status values. diff --git a/temoa/types/validation_types.py b/temoa/types/validation_types.py index 029bca15..a5971cab 100644 --- a/temoa/types/validation_types.py +++ b/temoa/types/validation_types.py @@ -6,11 +6,11 @@ """ from dataclasses import dataclass -from enum import Enum +from enum import StrEnum from typing import Any -class ValidationSeverity(str, Enum): +class ValidationSeverity(StrEnum): """ Enumeration of validation message severity levels.