From 62045a7c0fe0800047c8ef3c85e7dd433b23e0c1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 16:57:34 +0000 Subject: [PATCH 01/28] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/uv-pre-commit: 0.11.19 → 0.11.21](https://github.com/astral-sh/uv-pre-commit/compare/0.11.19...0.11.21) - [github.com/astral-sh/ruff-pre-commit: v0.15.16 → v0.15.17](https://github.com/astral-sh/ruff-pre-commit/compare/v0.15.16...v0.15.17) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5b931f51..2f9b6dd8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.11.19 + rev: 0.11.21 hooks: # Dependency management - id: uv-lock @@ -47,7 +47,7 @@ repos: # Python Linting & Formatting with Ruff - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: "v0.15.16" + rev: "v0.15.17" hooks: - id: ruff name: ruff (linter) From 403e4d353401fc663ccd1390add73eab8209cf08 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 16:56:35 +0000 Subject: [PATCH 02/28] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/uv-pre-commit: 0.11.21 → 0.11.23](https://github.com/astral-sh/uv-pre-commit/compare/0.11.21...0.11.23) - [github.com/astral-sh/ruff-pre-commit: v0.15.17 → v0.15.18](https://github.com/astral-sh/ruff-pre-commit/compare/v0.15.17...v0.15.18) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2f9b6dd8..5f170d2a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.11.21 + rev: 0.11.23 hooks: # Dependency management - id: uv-lock @@ -47,7 +47,7 @@ repos: # Python Linting & Formatting with Ruff - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: "v0.15.17" + rev: "v0.15.18" hooks: - id: ruff name: ruff (linter) From 7c566966f275f4310cdc1e8864dea1a5f1747430 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Wed, 24 Jun 2026 17:37:20 -0400 Subject: [PATCH 03/28] Load early retirements from previous planning periods Signed-off-by: Davey Elder --- temoa/core/model.py | 4 ++++ temoa/data_io/component_manifest.py | 8 ++++++++ temoa/data_io/hybrid_loader.py | 31 +++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/temoa/core/model.py b/temoa/core/model.py index 6c492e2a..a288b83e 100755 --- a/temoa/core/model.py +++ b/temoa/core/model.py @@ -383,6 +383,10 @@ def __init__(self, *args: object, **kwargs: object) -> None: self.capacity_to_activity = Param(self.regional_indices, self.tech_all, default=1) self.existing_capacity = Param(self.regional_indices, self.tech_exist, self.vintage_exist) + # This is needed to handle past retirements in myopic mode. Maybe it will find other uses. + self.retired_existing_capacity = Param( + self.regional_indices, self.time_exist, self.tech_exist, self.vintage_exist, default=0 + ) # Dev Note: The below is temporarily useful for passing down to validator to find # set violations diff --git a/temoa/data_io/component_manifest.py b/temoa/data_io/component_manifest.py index e22fdf67..12426048 100644 --- a/temoa/data_io/component_manifest.py +++ b/temoa/data_io/component_manifest.py @@ -288,6 +288,14 @@ def build_manifest(model: TemoaModel) -> list[LoadItem]: is_period_filtered=False, # Custom loader handles all logic is_table_required=False, ), + LoadItem( + component=model.retired_existing_capacity, + table='output_retired_capacity', + columns=['region', 'period', 'tech', 'vintage', 'cap_early'], + custom_loader_name='_load_retired_existing_capacity', + is_period_filtered=False, # Custom loader handles all logic + is_table_required=False, + ), LoadItem( component=model.cost_invest, table='cost_invest', diff --git a/temoa/data_io/hybrid_loader.py b/temoa/data_io/hybrid_loader.py index a4e2ce4d..d47a3635 100644 --- a/temoa/data_io/hybrid_loader.py +++ b/temoa/data_io/hybrid_loader.py @@ -630,6 +630,37 @@ 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_retired_existing_capacity( + self, + data: dict[str, object], + raw_data: Sequence[tuple[object, ...]], + filtered_data: Sequence[tuple[object, ...]], + ) -> None: + """ + Handles different queries for myopic vs. standard runs and also + populates the `tech_exist` set. + """ + model = TemoaModel() + cur = self.con.cursor() + mi = self.myopic_index + + if not mi: + # for now, we only use this in myopic mode + return + + rows_to_load = [] + prev_period_res = cur.execute( + 'SELECT MAX(period) FROM time_period WHERE period < ?', (mi.base_year,) + ).fetchone() + prev_period = prev_period_res[0] if prev_period_res else -1 + rows_to_load = cur.execute( + 'SELECT region, period, tech, vintage, cap_early FROM output_retired_capacity WHERE ' + 'period <= ? AND scenario = ? AND cap_early > 0 ', + (prev_period, self.config.scenario), + ).fetchall() + + self._load_component_data(data, model.retired_existing_capacity, rows_to_load) + # --- Singleton and Configuration-based Components --- def _load_global_discount_rate( self, From 02663b59a40f4871ff1c81fc580cbc6db6e120e6 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Wed, 24 Jun 2026 17:55:26 -0400 Subject: [PATCH 04/28] Adjust existing capacity for past early retirement in myopic Signed-off-by: Davey Elder --- temoa/components/capacity.py | 9 ++++++--- temoa/components/limits.py | 9 +++++++-- temoa/components/technology.py | 11 ++++++++--- temoa/components/utils.py | 17 +++++++++++++++++ 4 files changed, 38 insertions(+), 8 deletions(-) diff --git a/temoa/components/capacity.py b/temoa/components/capacity.py index 1383e09f..82e55312 100644 --- a/temoa/components/capacity.py +++ b/temoa/components/capacity.py @@ -18,7 +18,7 @@ from deprecated import deprecated from pyomo.environ import value -from .utils import get_capacity_factor +from .utils import get_adjusted_existing_capacity, get_capacity_factor if TYPE_CHECKING: from temoa.core.model import TemoaModel @@ -292,7 +292,9 @@ def annual_retirement_constraint( # 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 - cap_begin = model.existing_capacity[r, t, v] * model.lifetime_survival_curve[r, p, t, v] + cap_begin = get_adjusted_existing_capacity(model, r, t, v) * value( + model.lifetime_survival_curve[r, p, t, v] + ) else: # Get previous capacity and continue survival curve p_prev = model.time_optimize.prev(p) @@ -545,8 +547,9 @@ def adjusted_capacity_constraint( the time when that retirement occurred (treated here as at the beginning of each period). """ + built_capacity: ExprLike if v in model.time_exist: - built_capacity = value(model.existing_capacity[r, t, v]) + built_capacity = get_adjusted_existing_capacity(model, r, t, v) else: built_capacity = model.v_new_capacity[r, t, v] diff --git a/temoa/components/limits.py b/temoa/components/limits.py index f7f5d5d9..4d35e8b6 100644 --- a/temoa/components/limits.py +++ b/temoa/components/limits.py @@ -20,7 +20,12 @@ import temoa.components.geography as geography import temoa.components.technology as technology -from temoa.components.utils import Operator, get_variable_efficiency, operator_expression +from temoa.components.utils import ( + Operator, + get_adjusted_existing_capacity, + get_variable_efficiency, + operator_expression, +) if TYPE_CHECKING: from pyomo.core import Expression @@ -1046,7 +1051,7 @@ def limit_growth_capacity( # Adjust in-line for past PLF because we are constraining available capacity p_prev = model.time_exist.last() capacity_prev = sum( - value(model.existing_capacity[_r, _t, _v]) + get_adjusted_existing_capacity(model, _r, _t, _v) * min(1.0, (_v + value(model.lifetime_process[_r, _t, _v]) - p_prev) / (p - p_prev)) for _r, _t, _v in model.existing_capacity.sparse_keys() if _r in regions diff --git a/temoa/components/technology.py b/temoa/components/technology.py index e9cc254d..404e2cb2 100644 --- a/temoa/components/technology.py +++ b/temoa/components/technology.py @@ -17,6 +17,8 @@ from pyomo.environ import value +from temoa.components.utils import get_adjusted_existing_capacity + if TYPE_CHECKING: from collections.abc import Iterable @@ -426,12 +428,15 @@ def check_existing_capacity(model: TemoaModel) -> None: continue if t not in model.tech_all: continue + if get_adjusted_existing_capacity(model, r, t, v) <= 0: + # It was just retired earlier + continue life = value(model.lifetime_process[r, t, v]) if (r, t, v) not in model.process_periods and v + life > model.time_optimize.first(): msg = ( f'Existing capacity {r, t, v} with lifetime {life} and capacity {cap} ' 'should extend into future periods but it is not in process periods. ' - 'Was it included in the Efficiency table?' + 'May be missing from Efficiency table or too small to carry forward ' + 'if running in myopic mode.' ) - logger.error(msg) - raise ValueError(msg) + logger.warning(msg) diff --git a/temoa/components/utils.py b/temoa/components/utils.py index 943b9018..daa2b421 100644 --- a/temoa/components/utils.py +++ b/temoa/components/utils.py @@ -87,3 +87,20 @@ def get_capacity_factor( if model.is_capacity_factor_process[r, t, v]: return value(model.capacity_factor_process[r, s, d, t, v]) return value(model.capacity_factor_tech[r, s, d, t]) + + +def get_adjusted_existing_capacity( + model: TemoaModel, r: Region, t: Technology, v: Vintage +) -> float: + """ + Returns the built existing capacity adjusted for any early retirements. + + Needed for early retirements in myopic mode. Takes into account survival curves + and any early retirements that may have occurred prior to this planning step. + """ + capacity_adjustment = sum( + value(model.retired_existing_capacity[r, _p, t, v]) + / (value(model.lifetime_survival_curve[r, _p, t, v]) or 1) + for _p in model.time_exist + ) + return value(model.existing_capacity[r, t, v]) - capacity_adjustment From 5c91331acf9e93d8244d0585361c4f3490e2c469 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Wed, 24 Jun 2026 17:56:27 -0400 Subject: [PATCH 05/28] Fix handling of existing capacity for p0 Signed-off-by: Davey Elder --- temoa/components/limits.py | 3 +-- temoa/components/time.py | 5 +++++ temoa/core/model.py | 4 +++- temoa/data_io/component_manifest.py | 1 - 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/temoa/components/limits.py b/temoa/components/limits.py index 4d35e8b6..857cb148 100644 --- a/temoa/components/limits.py +++ b/temoa/components/limits.py @@ -1048,11 +1048,10 @@ def limit_growth_capacity( if p == model.time_optimize.first(): # First future period. Grab available capacity in last existing period - # Adjust in-line for past PLF because we are constraining available capacity p_prev = model.time_exist.last() capacity_prev = sum( get_adjusted_existing_capacity(model, _r, _t, _v) - * min(1.0, (_v + value(model.lifetime_process[_r, _t, _v]) - p_prev) / (p - p_prev)) + * value(model.process_life_frac[_r, p_prev, _t, _v]) for _r, _t, _v in model.existing_capacity.sparse_keys() if _r in regions and _t in techs diff --git a/temoa/components/time.py b/temoa/components/time.py index 0877a40b..c34a6bed 100644 --- a/temoa/components/time.py +++ b/temoa/components/time.py @@ -196,6 +196,11 @@ def init_set_vintage_optimize(model: TemoaModel) -> list[int]: def param_period_length(model: TemoaModel, p: Period) -> int: """Rule to calculate the length of each optimization period in years.""" + if model.time_exist and p == model.time_exist.last(): + # Need this for one specific use case (capacity growth constraints) + return model.time_future.first() - model.time_exist.last() + elif p in model.time_exist: + return -1 # Period length is not defined for existing periods except the last periods: list[int] = sorted(model.time_future) i: int = periods.index(p) return periods[i + 1] - periods[i] diff --git a/temoa/core/model.py b/temoa/core/model.py index a288b83e..ac7146b3 100755 --- a/temoa/core/model.py +++ b/temoa/core/model.py @@ -335,7 +335,9 @@ def __init__(self, *args: object, **kwargs: object) -> None: # Define time-related parameters # Basic period construction self.time_sequencing = Set() # How do states carry between time segments? - self.period_length = Param(self.time_optimize, initialize=time.param_period_length) + self.period_length = Param( + self.time_optimize | self.time_exist, initialize=time.param_period_length + ) self.days_per_period = Param(domain=PositiveReals, default=365.0) self.time_of_day_hours = Param(self.time_of_day, domain=PositiveReals, default=1.0) self.segment_fraction_per_season = Param(self.time_season) diff --git a/temoa/data_io/component_manifest.py b/temoa/data_io/component_manifest.py index 12426048..72688e3b 100644 --- a/temoa/data_io/component_manifest.py +++ b/temoa/data_io/component_manifest.py @@ -445,7 +445,6 @@ def build_manifest(model: TemoaModel) -> list[LoadItem]: component=model.lifetime_survival_curve, table='lifetime_survival_curve', columns=['region', 'period', 'tech', 'vintage', 'fraction'], - validator_name='viable_rtv', validation_map=(0, 2, 3), is_period_filtered=False, is_table_required=False, From 1fee42f182ec1a8683105ed908cee320a827ce5b Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Fri, 26 Jun 2026 10:04:31 -0400 Subject: [PATCH 06/28] Pull lifetimes data for previously retired existing capacities for accounting purposes Signed-off-by: Davey Elder --- temoa/components/technology.py | 27 +++++++---- temoa/components/time.py | 5 --- temoa/core/model.py | 11 +++-- temoa/data_io/component_manifest.py | 8 ++-- temoa/data_io/hybrid_loader.py | 70 ++++++++++++++++++++++++++++- 5 files changed, 95 insertions(+), 26 deletions(-) diff --git a/temoa/components/technology.py b/temoa/components/technology.py index 404e2cb2..4e594b52 100644 --- a/temoa/components/technology.py +++ b/temoa/components/technology.py @@ -17,8 +17,6 @@ from pyomo.environ import value -from temoa.components.utils import get_adjusted_existing_capacity - if TYPE_CHECKING: from collections.abc import Iterable @@ -54,7 +52,23 @@ def model_process_life_indices( the periods in which a process is active, distinct from TechLifeFracIndices that returns indices only for processes that EOL mid-period. """ - return model.active_activity_rptv + indices = { + (r, model.time_exist.last(), t, v) for r, t, v in model.existing_capacity.sparse_keys() + } + indices = indices | model.active_activity_rptv + + return indices + + +def lifetime_tech_indices(model: TemoaModel) -> set[tuple[Region, Technology]]: + """ + Based on the efficiency parameter's indices, this function returns the set of + process indices that may be specified in the lifetime_tech parameter. + """ + indices = {(r, t) for r, _, t, _, _ in set(model.efficiency.sparse_keys())} + indices = indices | {(r, t) for r, t, _ in model.existing_capacity.sparse_keys()} + + return indices def lifetime_process_indices(model: TemoaModel) -> set[tuple[Region, Technology, Vintage]]: @@ -62,7 +76,7 @@ def lifetime_process_indices(model: TemoaModel) -> set[tuple[Region, Technology, Based on the efficiency parameter's indices, this function returns the set of process indices that may be specified in the lifetime_process parameter. """ - indices = {(r, t, v) for r, i, t, v, o in set(model.efficiency.sparse_keys())} + indices = {(r, t, v) for r, _, t, v, _ in set(model.efficiency.sparse_keys())} indices = indices | set(model.existing_capacity.sparse_keys()) return indices @@ -428,14 +442,11 @@ def check_existing_capacity(model: TemoaModel) -> None: continue if t not in model.tech_all: continue - if get_adjusted_existing_capacity(model, r, t, v) <= 0: - # It was just retired earlier - continue life = value(model.lifetime_process[r, t, v]) if (r, t, v) not in model.process_periods and v + life > model.time_optimize.first(): msg = ( f'Existing capacity {r, t, v} with lifetime {life} and capacity {cap} ' - 'should extend into future periods but it is not in process periods. ' + 'should extend into future periods but is not an active process. ' 'May be missing from Efficiency table or too small to carry forward ' 'if running in myopic mode.' ) diff --git a/temoa/components/time.py b/temoa/components/time.py index c34a6bed..cdbb05db 100644 --- a/temoa/components/time.py +++ b/temoa/components/time.py @@ -184,11 +184,6 @@ def init_set_time_optimize(model: TemoaModel) -> list[int]: return sorted(model.time_future)[:-1] -def init_set_vintage_exist(model: TemoaModel) -> list[int]: - """Initializes the `vintage_exist` set.""" - return sorted(model.time_exist) - - def init_set_vintage_optimize(model: TemoaModel) -> list[int]: """Initializes the `vintage_optimize` set.""" return sorted(model.time_optimize) diff --git a/temoa/core/model.py b/temoa/core/model.py index ac7146b3..fc223db3 100755 --- a/temoa/core/model.py +++ b/temoa/core/model.py @@ -212,9 +212,9 @@ def __init__(self, *args: object, **kwargs: object) -> None: ordered=True, initialize=time.init_set_time_optimize, within=self.time_future ) # Define time period vintages to track capacity installation - self.vintage_exist = Set(ordered=True, initialize=time.init_set_vintage_exist) + self.vintage_exist = Set(ordered=True) self.vintage_optimize = Set(ordered=True, initialize=time.init_set_vintage_optimize) - self.vintage_all = Set(initialize=self.time_exist | self.time_optimize) + self.vintage_all = Set(initialize=self.vintage_exist | self.time_optimize) # Perform some basic validation on the specified time periods. self.validate_time = BuildAction(rule=time.validate_time) @@ -437,9 +437,8 @@ def __init__(self, *args: object, **kwargs: object) -> None: default=1, ) - self.lifetime_tech = Param( - self.regional_indices, self.tech_all, default=TemoaModel.default_lifetime_tech - ) + self.lifetime_tech_rt = Set(dimen=2, initialize=technology.lifetime_tech_indices) + self.lifetime_tech = Param(self.lifetime_tech_rt, default=TemoaModel.default_lifetime_tech) self.lifetime_process_rtv = Set(dimen=3, initialize=technology.lifetime_process_indices) self.lifetime_process = Param( @@ -449,7 +448,7 @@ def __init__(self, *args: object, **kwargs: object) -> None: self.lifetime_survival_curve = Param( self.regional_indices, Integers, - self.tech_all, + self.tech_all | self.tech_exist, self.vintage_all, default=technology.get_default_survival, validate=validate_0to1, diff --git a/temoa/data_io/component_manifest.py b/temoa/data_io/component_manifest.py index 72688e3b..a5848c2e 100644 --- a/temoa/data_io/component_manifest.py +++ b/temoa/data_io/component_manifest.py @@ -427,8 +427,7 @@ def build_manifest(model: TemoaModel) -> list[LoadItem]: component=model.lifetime_tech, table='lifetime_tech', columns=['region', 'tech', 'lifetime'], - validator_name='viable_rt', - validation_map=(0, 1), + custom_loader_name='_load_lifetime_tech', is_period_filtered=False, is_table_required=False, ), @@ -436,8 +435,7 @@ def build_manifest(model: TemoaModel) -> list[LoadItem]: component=model.lifetime_process, table='lifetime_process', columns=['region', 'tech', 'vintage', 'lifetime'], - validator_name='viable_rtv', - validation_map=(0, 1, 2), + custom_loader_name='_load_lifetime_process', is_period_filtered=False, is_table_required=False, ), @@ -445,7 +443,7 @@ def build_manifest(model: TemoaModel) -> list[LoadItem]: component=model.lifetime_survival_curve, table='lifetime_survival_curve', columns=['region', 'period', 'tech', 'vintage', 'fraction'], - validation_map=(0, 2, 3), + custom_loader_name='_load_lifetime_survival_curve', is_period_filtered=False, is_table_required=False, ), diff --git a/temoa/data_io/hybrid_loader.py b/temoa/data_io/hybrid_loader.py index d47a3635..71024b5b 100644 --- a/temoa/data_io/hybrid_loader.py +++ b/temoa/data_io/hybrid_loader.py @@ -24,6 +24,7 @@ import time from collections import defaultdict from logging import getLogger +from operator import itemgetter from sqlite3 import Connection, Cursor, OperationalError from typing import TYPE_CHECKING, cast @@ -98,6 +99,7 @@ def __init__(self, db_connection: Connection, config: TemoaConfig) -> None: self.manager: CommodityNetworkManager | None = None self.efficiency_values: list[tuple[object, ...]] = [] self.data: dict[str, object] | None = None + self.tech_exist_data: set[str] = set() # --- Viable sets for source-trace filtering --- self.viable_techs: ViableSet | None = None @@ -629,6 +631,9 @@ def _load_existing_capacity( if rows_to_load: tech_exist_data = sorted({(row[1],) for row in rows_to_load}) self._load_component_data(data, model.tech_exist, tech_exist_data) + self.tech_exist_data = {row[1] for row in rows_to_load} + vintage_exist_data = sorted({(row[2],) for row in rows_to_load}) + self._load_component_data(data, model.vintage_exist, vintage_exist_data) def _load_retired_existing_capacity( self, @@ -637,8 +642,7 @@ def _load_retired_existing_capacity( filtered_data: Sequence[tuple[object, ...]], ) -> None: """ - Handles different queries for myopic vs. standard runs and also - populates the `tech_exist` set. + Only needed in myopic to bring past early retirement decisions forward """ model = TemoaModel() cur = self.con.cursor() @@ -661,6 +665,68 @@ def _load_retired_existing_capacity( self._load_component_data(data, model.retired_existing_capacity, rows_to_load) + # --- Lifetime components --- + def _load_lifetime_tech( + self, + data: dict[str, object], + raw_data: Sequence[tuple[object, ...]], + filtered_data: Sequence[tuple[object, ...]], + ) -> None: + """Loads the lifetime_tech component.""" + model = TemoaModel() + cur = self.con.cursor() + rows_to_load = cur.execute('SELECT region, tech, lifetime FROM lifetime_tech').fetchall() + tech_getter = itemgetter(1) + if self.viable_techs: + valid_techs = self.viable_techs.members | self.tech_exist_data + rows_to_load = [item for item in rows_to_load if tech_getter(item) in valid_techs] + self._load_component_data(data, model.lifetime_tech, rows_to_load) + + def _load_lifetime_process( + self, + data: dict[str, object], + raw_data: Sequence[tuple[object, ...]], + filtered_data: Sequence[tuple[object, ...]], + ) -> None: + """Loads the lifetime_process component.""" + model = TemoaModel() + cur = self.con.cursor() + mi = self.myopic_index + + if mi: + rows_to_load = cur.execute( + 'SELECT region, tech, vintage, lifetime FROM lifetime_process WHERE vintage <= ?', + (mi.last_demand_year,), + ).fetchall() + else: + rows_to_load = cur.execute( + 'SELECT region, tech, vintage, lifetime FROM lifetime_process' + ).fetchall() + self._load_component_data(data, model.lifetime_process, rows_to_load) + + def _load_lifetime_survival_curve( + self, + data: dict[str, object], + raw_data: Sequence[tuple[object, ...]], + filtered_data: Sequence[tuple[object, ...]], + ) -> None: + """Loads the lifetime_survival_curve component.""" + model = TemoaModel() + cur = self.con.cursor() + mi = self.myopic_index + + if mi: + rows_to_load = cur.execute( + 'SELECT region, period, tech, vintage, fraction FROM lifetime_survival_curve ' + 'WHERE vintage <= ?', + (mi.last_demand_year,), + ).fetchall() + else: + rows_to_load = cur.execute( + 'SELECT region, period, tech, vintage, fraction FROM lifetime_survival_curve' + ).fetchall() + self._load_component_data(data, model.lifetime_survival_curve, rows_to_load) + # --- Singleton and Configuration-based Components --- def _load_global_discount_rate( self, From f8f55dde3f0b33ddab3f6d3a1b337d9df7a466b0 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Fri, 26 Jun 2026 10:05:09 -0400 Subject: [PATCH 07/28] Actually load growthrate constraints Signed-off-by: Davey Elder --- temoa/core/model.py | 13 ++++--- temoa/data_io/component_manifest.py | 60 +++++++++++++++++++++++++++++ temoa/data_io/hybrid_loader.py | 4 ++ temoa/data_io/loader_manifest.py | 1 + 4 files changed, 72 insertions(+), 6 deletions(-) diff --git a/temoa/core/model.py b/temoa/core/model.py index fc223db3..85f48ea3 100755 --- a/temoa/core/model.py +++ b/temoa/core/model.py @@ -14,6 +14,7 @@ from pyomo.core import BuildCheck, Set, Var from pyomo.environ import ( AbstractModel, + Any, BuildAction, Constraint, Integers, @@ -626,22 +627,22 @@ def __init__(self, *args: object, **kwargs: object) -> None: ) self.limit_growth_capacity = Param( - self.regional_global_indices, self.tech_or_group, self.operator + self.regional_global_indices, self.tech_or_group, self.operator, within=Any ) self.limit_degrowth_capacity = Param( - self.regional_global_indices, self.tech_or_group, self.operator + self.regional_global_indices, self.tech_or_group, self.operator, within=Any ) self.limit_growth_new_capacity = Param( - self.regional_global_indices, self.tech_or_group, self.operator + self.regional_global_indices, self.tech_or_group, self.operator, within=Any ) self.limit_degrowth_new_capacity = Param( - self.regional_global_indices, self.tech_or_group, self.operator + self.regional_global_indices, self.tech_or_group, self.operator, within=Any ) self.limit_growth_new_capacity_delta = Param( - self.regional_global_indices, self.tech_or_group, self.operator + self.regional_global_indices, self.tech_or_group, self.operator, within=Any ) self.limit_degrowth_new_capacity_delta = Param( - self.regional_global_indices, self.tech_or_group, self.operator + self.regional_global_indices, self.tech_or_group, self.operator, within=Any ) self.limit_emission_constraint_rpe = Set( diff --git a/temoa/data_io/component_manifest.py b/temoa/data_io/component_manifest.py index a5848c2e..7a97e2ba 100644 --- a/temoa/data_io/component_manifest.py +++ b/temoa/data_io/component_manifest.py @@ -654,6 +654,66 @@ def build_manifest(model: TemoaModel) -> list[LoadItem]: validation_map=(0, 1, 2), is_table_required=False, ), + LoadItem( + component=model.limit_growth_capacity, + table='limit_growth_capacity', + columns=['region', 'tech_or_group', 'operator', 'rate', 'seed'], + index_length=3, + validator_name='viable_rt', + validation_map=(0, 1), + is_period_filtered=False, + is_table_required=False, + ), + LoadItem( + component=model.limit_growth_new_capacity, + table='limit_growth_new_capacity', + columns=['region', 'tech_or_group', 'operator', 'rate', 'seed'], + index_length=3, + validator_name='viable_rt', + validation_map=(0, 1), + is_period_filtered=False, + is_table_required=False, + ), + LoadItem( + component=model.limit_growth_new_capacity_delta, + table='limit_growth_new_capacity_delta', + columns=['region', 'tech_or_group', 'operator', 'rate', 'seed'], + index_length=3, + validator_name='viable_rt', + validation_map=(0, 1), + is_period_filtered=False, + is_table_required=False, + ), + LoadItem( + component=model.limit_degrowth_capacity, + table='limit_degrowth_capacity', + columns=['region', 'tech_or_group', 'operator', 'rate', 'seed'], + index_length=3, + validator_name='viable_rt', + validation_map=(0, 1), + is_period_filtered=False, + is_table_required=False, + ), + LoadItem( + component=model.limit_degrowth_new_capacity, + table='limit_degrowth_new_capacity', + columns=['region', 'tech_or_group', 'operator', 'rate', 'seed'], + index_length=3, + validator_name='viable_rt', + validation_map=(0, 1), + is_period_filtered=False, + is_table_required=False, + ), + LoadItem( + component=model.limit_degrowth_new_capacity_delta, + table='limit_degrowth_new_capacity_delta', + columns=['region', 'tech_or_group', 'operator', 'rate', 'seed'], + index_length=3, + validator_name='viable_rt', + validation_map=(0, 1), + is_period_filtered=False, + is_table_required=False, + ), LoadItem( component=model.limit_resource, table='limit_resource', diff --git a/temoa/data_io/hybrid_loader.py b/temoa/data_io/hybrid_loader.py index 71024b5b..9452f2e3 100644 --- a/temoa/data_io/hybrid_loader.py +++ b/temoa/data_io/hybrid_loader.py @@ -233,6 +233,10 @@ def create_data_dict(self, myopic_index: MyopicIndex | None = None) -> dict[str, for item in self.manifest: # 1. Fetch data from the database raw_data = self._fetch_data(cur, item, myopic_index) + if item.index_length: + raw_data = [ + (*row[0 : item.index_length], row[item.index_length :]) for row in raw_data + ] # 2. Validate/filter data filtered_data = self._filter_data(raw_data, item, use_raw_data) diff --git a/temoa/data_io/loader_manifest.py b/temoa/data_io/loader_manifest.py index ad6ce930..5723d9e9 100644 --- a/temoa/data_io/loader_manifest.py +++ b/temoa/data_io/loader_manifest.py @@ -50,6 +50,7 @@ class LoadItem: component: ComponentType table: str columns: list[str] + index_length: int | None = None validator_name: str | None = None validation_map: tuple[int, ...] = field(default_factory=tuple) where_clause: str | None = None From d3bcea4271bbfb1a24f8e8f5759f467c172d86ea Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Fri, 26 Jun 2026 10:31:48 -0400 Subject: [PATCH 08/28] Add a broad stress test for myopic that includes survival curves, early retirement, and growth rate constraints all together Signed-off-by: Davey Elder --- tests/conftest.py | 6 + tests/legacy_test_values.py | 3 +- tests/test_full_runs.py | 45 ++++ .../config_myopic_capacities.toml | 23 ++ tests/testing_data/mediumville.sql | 1 - tests/testing_data/myopic_capacities.sql | 242 ++++++++++++++++++ 6 files changed, 318 insertions(+), 2 deletions(-) create mode 100644 tests/testing_configs/config_myopic_capacities.toml create mode 100644 tests/testing_data/myopic_capacities.sql diff --git a/tests/conftest.py b/tests/conftest.py index 3f813d5b..0e263b9c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -88,6 +88,7 @@ def refresh_databases() -> None: ('mediumville.sql', 'mediumville.sqlite'), ('seasonal_storage.sql', 'seasonal_storage.sqlite'), ('survival_curve.sql', 'survival_curve.sqlite'), + ('myopic_capacities.sql', 'myopic_capacities.sqlite'), ('annualised_demand.sql', 'annualised_demand.sqlite'), # Feature tests (separate for temporal consistency) ('emissions.sql', 'emissions.sqlite'), @@ -207,6 +208,11 @@ def system_test_run( silent=True, ) + # Allow parametrized tests to override myopic settings per case. + myopic_overrides = request.param.get('myopic') + if myopic_overrides is not None: + config.myopic_inputs = {**(config.myopic_inputs or {}), **myopic_overrides} + sequencer = TemoaSequencer(config=config) sequencer.start() diff --git a/tests/legacy_test_values.py b/tests/legacy_test_values.py index af31669c..3fb7d2e1 100644 --- a/tests/legacy_test_values.py +++ b/tests/legacy_test_values.py @@ -68,7 +68,8 @@ class ExpectedVals(Enum): # added 2025/06/12 prior to addition of dynamic reserve margin # reduced 2025/06/16 after fixing bug in db ExpectedVals.OBJ_VALUE: 7035.7275, - ExpectedVals.EFF_DOMAIN_SIZE: 2800, + # halved after realising mediumville had an unused existing time period + ExpectedVals.EFF_DOMAIN_SIZE: 1400, ExpectedVals.EFF_INDEX_SIZE: 18, # increased after reviving RampSeason constraints # reduced 2025/07/25 by 24 after annualising demands diff --git a/tests/test_full_runs.py b/tests/test_full_runs.py index 45ca7d1b..5decd266 100644 --- a/tests/test_full_runs.py +++ b/tests/test_full_runs.py @@ -36,6 +36,24 @@ mc_files = [{'name': 'utopia mc', 'filename': 'config_utopia_mc.toml'}] stochastic_files = [{'name': 'stochastic utopia', 'filename': 'config_utopia_stochastic.toml'}] +myopic_stress_tests = [ + { + 'name': ( + f'myopic capacities | {"evolving" if evolving else "non-evolving"}' + f' | view={view_depth} step={step_size}' + ), + 'filename': 'config_myopic_capacities.toml', + 'myopic': { + 'view_depth': view_depth, + 'step_size': step_size, + 'evolving': evolving, + }, + } + for evolving in (False, True) + for view_depth in [1, 3, 6] + for step_size in range(1, view_depth + 1, 2) +] + @pytest.mark.parametrize( 'system_test_run', @@ -129,6 +147,33 @@ def test_myopic_utopia( ) +@pytest.mark.parametrize( + 'system_test_run', + argvalues=myopic_stress_tests, + indirect=True, + ids=[d['name'] for d in myopic_stress_tests], +) +def test_myopic_stress_tests( + system_test_run: tuple[str, SolverResults | None, TemoaModel | None, TemoaSequencer], +) -> None: + """ + The idea of these is that they should be tightly constrained so that if anything + is wrong the model will fail to find a feasible solution. Use lots of equality constraints + """ + _, _, _, sequencer = system_test_run + import contextlib + + with contextlib.closing(sqlite3.connect(sequencer.config.output_database)) as con: + cur = con.cursor() + res = cur.execute('SELECT SUM(total_system_cost) FROM main.output_objective').fetchone() + obj = res[0] + # This part is just a very rough check on the objective function. Constraints inside the + # model are extremely tight so any other changes will lead to infeasibility + assert obj == pytest.approx(32, abs=1), ( + 'objective function value did not match expected for myopic stress test' + ) + + @pytest.mark.parametrize( 'system_test_run', argvalues=stochastic_files, diff --git a/tests/testing_configs/config_myopic_capacities.toml b/tests/testing_configs/config_myopic_capacities.toml new file mode 100644 index 00000000..3ab763c4 --- /dev/null +++ b/tests/testing_configs/config_myopic_capacities.toml @@ -0,0 +1,23 @@ +scenario = "test myopic capacities" +scenario_mode = "myopic" +input_database = "tests/testing_outputs/myopic_capacities.sqlite" +output_database = "tests/testing_outputs/myopic_capacities.sqlite" +neos = false +solver_name = "appsi_highs" +save_excel = true +save_duals = true +save_lp_file = false +time_sequencing = "seasonal_timeslices" +days_per_period = 365 +reserve_margin = "static" + +[MGA] +slack = 0.1 +iterations = 4 +weight = "integer" + +[myopic] +view_depth = 1 +step_size = 1 +evolving = false +evolution_script = '' diff --git a/tests/testing_data/mediumville.sql b/tests/testing_data/mediumville.sql index 9df6b3f7..2f253a8c 100644 --- a/tests/testing_data/mediumville.sql +++ b/tests/testing_data/mediumville.sql @@ -181,7 +181,6 @@ REPLACE INTO "technology_type" VALUES('pb','baseload production technology'); REPLACE INTO "technology_type" VALUES('ps','storage production technology'); REPLACE INTO "time_of_day" VALUES(1,'d1',12,NULL); REPLACE INTO "time_of_day" VALUES(2,'d2',12,NULL); -REPLACE INTO "time_period" VALUES(1,2020,'e'); REPLACE INTO "time_period" VALUES(2,2025,'f'); REPLACE INTO "time_period" VALUES(3,2030,'f'); REPLACE INTO "time_period_type" VALUES('e','existing vintages'); diff --git a/tests/testing_data/myopic_capacities.sql b/tests/testing_data/myopic_capacities.sql new file mode 100644 index 00000000..f561e073 --- /dev/null +++ b/tests/testing_data/myopic_capacities.sql @@ -0,0 +1,242 @@ +REPLACE INTO metadata VALUES('DB_MAJOR',4,''); +REPLACE INTO metadata VALUES('DB_MINOR',0,''); +REPLACE INTO metadata_real VALUES('global_discount_rate',0.05000000000000000277,'Discount Rate for future costs'); +REPLACE INTO metadata_real VALUES('default_loan_rate',0.05000000000000000277,'Default Loan Rate if not specified in loan_rate table'); +REPLACE INTO sector_label VALUES('energy',NULL); +REPLACE INTO commodity VALUES('source','s',NULL,NULL); +REPLACE INTO commodity VALUES('demand','d',NULL,NULL); +REPLACE INTO commodity_type VALUES('p','physical commodity'); +REPLACE INTO commodity_type VALUES('a','annual commodity'); +REPLACE INTO commodity_type VALUES('e','emissions commodity'); +REPLACE INTO commodity_type VALUES('d','demand commodity'); +REPLACE INTO commodity_type VALUES('s','source commodity'); +REPLACE INTO commodity_type VALUES('w','waste commodity'); +REPLACE INTO commodity_type VALUES('wa','waste annual commodity'); +REPLACE INTO commodity_type VALUES('wp','waste physical commodity'); +REPLACE INTO cost_fixed VALUES('region',2025,'tech_ancient',1994,1.0,NULL,NULL); +REPLACE INTO cost_fixed VALUES('region',2025,'tech_old',2010,1.0,NULL,NULL); +REPLACE INTO cost_fixed VALUES('region',2030,'tech_old',2010,1.0,NULL,NULL); +REPLACE INTO cost_fixed VALUES('region',2035,'tech_old',2010,1.0,NULL,NULL); +REPLACE INTO cost_fixed VALUES('region',2040,'tech_old',2010,1.0,NULL,NULL); +REPLACE INTO cost_fixed VALUES('region',2025,'tech_current',2025,1.0,NULL,NULL); +REPLACE INTO cost_fixed VALUES('region',2030,'tech_current',2025,1.0,NULL,NULL); +REPLACE INTO cost_fixed VALUES('region',2035,'tech_current',2025,1.0,NULL,NULL); +REPLACE INTO cost_fixed VALUES('region',2040,'tech_current',2025,1.0,NULL,NULL); +REPLACE INTO cost_fixed VALUES('region',2045,'tech_current',2025,1.0,NULL,NULL); +REPLACE INTO cost_fixed VALUES('region',2050,'tech_current',2025,1.0,NULL,NULL); +REPLACE INTO cost_fixed VALUES('region',2030,'tech_future',2030,1.0,NULL,NULL); +REPLACE INTO cost_fixed VALUES('region',2035,'tech_future',2035,1.0,NULL,NULL); +REPLACE INTO cost_fixed VALUES('region',2040,'tech_future',2040,1.0,NULL,NULL); +REPLACE INTO cost_fixed VALUES('region',2045,'tech_future',2045,1.0,NULL,NULL); +REPLACE INTO cost_fixed VALUES('region',2050,'tech_future',2050,1.0,NULL,NULL); +REPLACE INTO cost_fixed VALUES('region',2035,'tech_future',2030,1.0,NULL,NULL); +REPLACE INTO cost_fixed VALUES('region',2040,'tech_future',2035,1.0,NULL,NULL); +REPLACE INTO cost_fixed VALUES('region',2045,'tech_future',2040,1.0,NULL,NULL); +REPLACE INTO cost_fixed VALUES('region',2050,'tech_future',2045,1.0,NULL,NULL); +REPLACE INTO cost_fixed VALUES('region',2040,'tech_future',2030,1.0,NULL,NULL); +REPLACE INTO cost_fixed VALUES('region',2045,'tech_future',2035,1.0,NULL,NULL); +REPLACE INTO cost_fixed VALUES('region',2050,'tech_future',2040,1.0,NULL,NULL); +REPLACE INTO cost_fixed VALUES('region',2045,'tech_future',2030,1.0,NULL,NULL); +REPLACE INTO cost_fixed VALUES('region',2050,'tech_future',2035,1.0,NULL,NULL); +REPLACE INTO cost_fixed VALUES('region',2050,'tech_future',2030,1.0,NULL,NULL); +REPLACE INTO cost_invest VALUES('region','tech_current',2025,1.0,NULL,NULL); +REPLACE INTO cost_invest VALUES('region','tech_future',2030,1.0,NULL,NULL); +REPLACE INTO cost_invest VALUES('region','tech_future',2035,1.0,NULL,NULL); +REPLACE INTO cost_invest VALUES('region','tech_future',2040,1.0,NULL,NULL); +REPLACE INTO cost_invest VALUES('region','tech_future',2045,1.0,NULL,NULL); +REPLACE INTO cost_invest VALUES('region','tech_future',2050,1.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2025,'tech_ancient',1994,1.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2025,'tech_old',2010,1.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2030,'tech_old',2010,1.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2035,'tech_old',2010,1.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2040,'tech_old',2010,1.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2025,'tech_current',2025,1.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2030,'tech_current',2025,1.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2035,'tech_current',2025,1.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2040,'tech_current',2025,1.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2045,'tech_current',2025,1.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2050,'tech_current',2025,1.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2030,'tech_future',2030,1.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2035,'tech_future',2035,1.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2040,'tech_future',2040,1.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2045,'tech_future',2045,1.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2050,'tech_future',2050,1.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2035,'tech_future',2030,1.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2040,'tech_future',2035,1.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2045,'tech_future',2040,1.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2050,'tech_future',2045,1.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2040,'tech_future',2030,1.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2045,'tech_future',2035,1.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2050,'tech_future',2040,1.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2045,'tech_future',2030,1.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2050,'tech_future',2035,1.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2050,'tech_future',2030,1.0,NULL,NULL); +REPLACE INTO demand VALUES('region',2025,'demand',1.0,NULL,NULL); +REPLACE INTO demand VALUES('region',2030,'demand',1.0,NULL,NULL); +REPLACE INTO demand VALUES('region',2035,'demand',1.0,NULL,NULL); +REPLACE INTO demand VALUES('region',2040,'demand',1.0,NULL,NULL); +REPLACE INTO demand VALUES('region',2045,'demand',1.0,NULL,NULL); +REPLACE INTO demand VALUES('region',2050,'demand',1.0,NULL,NULL); +REPLACE INTO efficiency VALUES('region','source','tech_old',2010,'demand',1.0,NULL,NULL); +REPLACE INTO efficiency VALUES('region','source','tech_current',2025,'demand',1.0,NULL,NULL); +REPLACE INTO efficiency VALUES('region','source','tech_future',2030,'demand',1.0,NULL,NULL); +REPLACE INTO efficiency VALUES('region','source','tech_future',2035,'demand',1.0,NULL,NULL); +REPLACE INTO efficiency VALUES('region','source','tech_future',2040,'demand',1.0,NULL,NULL); +REPLACE INTO efficiency VALUES('region','source','tech_future',2045,'demand',1.0,NULL,NULL); +REPLACE INTO efficiency VALUES('region','source','tech_future',2050,'demand',1.0,NULL,NULL); +REPLACE INTO efficiency VALUES('region','source','tech_ancient',1994,'demand',1.0,NULL,NULL); +REPLACE INTO efficiency VALUES('region','source','tech_retire',2010,'demand',1.0,NULL,NULL); +REPLACE INTO existing_capacity VALUES('region','tech_ancient',1990,3.0,NULL,NULL); +REPLACE INTO existing_capacity VALUES('region','tech_old',2010,0.6999999999999999556,NULL,NULL); +REPLACE INTO existing_capacity VALUES('region','tech_ancient',1994,3.0,NULL,NULL); +REPLACE INTO existing_capacity VALUES('region','tech_retire',2010,1.0,NULL,NULL); +REPLACE INTO lifetime_tech VALUES('region','tech_ancient',35.0,NULL,NULL); +REPLACE INTO lifetime_tech VALUES('region','tech_old',35.0,NULL,NULL); +REPLACE INTO lifetime_tech VALUES('region','tech_current',35.0,NULL,NULL); +REPLACE INTO lifetime_tech VALUES('region','tech_future',35.0,NULL,NULL); +REPLACE INTO lifetime_tech VALUES('region','tech_retire',35.0,NULL,NULL); +REPLACE INTO operator VALUES('e','equal to'); +REPLACE INTO operator VALUES('le','less than or equal to'); +REPLACE INTO operator VALUES('ge','greater than or equal to'); +REPLACE INTO limit_growth_capacity VALUES('region','tech_ancient','le',1.0,1.0,NULL,NULL); +REPLACE INTO limit_growth_capacity VALUES('region','tech_old','le',1.0,1.0,NULL,NULL); +REPLACE INTO limit_growth_capacity VALUES('region','tech_current','le',1.0,1.0,NULL,NULL); +REPLACE INTO limit_growth_capacity VALUES('region','tech_future','le',1.0,1.0,NULL,NULL); +REPLACE INTO limit_degrowth_capacity VALUES('region','tech_ancient','le',1.0,2.0,NULL,NULL); +REPLACE INTO limit_degrowth_capacity VALUES('region','tech_old','le',1.0,1.0,NULL,NULL); +REPLACE INTO limit_degrowth_capacity VALUES('region','tech_current','le',1.0,1.0,NULL,NULL); +REPLACE INTO limit_degrowth_capacity VALUES('region','tech_future','le',1.0,1.0,NULL,NULL); +REPLACE INTO limit_growth_new_capacity VALUES('region','tech_ancient','le',1.0,1.0,NULL,NULL); +REPLACE INTO limit_growth_new_capacity VALUES('region','tech_old','le',1.0,1.0,NULL,NULL); +REPLACE INTO limit_growth_new_capacity VALUES('region','tech_current','le',1.0,1.0,NULL,NULL); +REPLACE INTO limit_growth_new_capacity VALUES('region','tech_future','le',1.0,1.0,NULL,NULL); +REPLACE INTO limit_degrowth_new_capacity VALUES('region','tech_ancient','le',1.0,1.0,NULL,NULL); +REPLACE INTO limit_degrowth_new_capacity VALUES('region','tech_old','le',1.0,1.0,NULL,NULL); +REPLACE INTO limit_degrowth_new_capacity VALUES('region','tech_current','le',1.0,1.0,NULL,NULL); +REPLACE INTO limit_degrowth_new_capacity VALUES('region','tech_future','le',1.0,1.0,NULL,NULL); +REPLACE INTO limit_growth_new_capacity_delta VALUES('region','tech_ancient','le',0.0,2.0,NULL,NULL); +REPLACE INTO limit_growth_new_capacity_delta VALUES('region','tech_old','le',0.0,2.0,NULL,NULL); +REPLACE INTO limit_growth_new_capacity_delta VALUES('region','tech_current','le',0.0,2.0,NULL,NULL); +REPLACE INTO limit_growth_new_capacity_delta VALUES('region','tech_future','le',0.0,2.0,NULL,NULL); +REPLACE INTO limit_degrowth_new_capacity_delta VALUES('region','tech_ancient','le',0.0,2.0,NULL,NULL); +REPLACE INTO limit_degrowth_new_capacity_delta VALUES('region','tech_old','le',0.0,2.0,NULL,NULL); +REPLACE INTO limit_degrowth_new_capacity_delta VALUES('region','tech_current','le',0.0,2.0,NULL,NULL); +REPLACE INTO limit_degrowth_new_capacity_delta VALUES('region','tech_future','le',0.0,2.0,NULL,NULL); +REPLACE INTO limit_activity VALUES('region',2025,'tech_ancient','e',0.04800000000000000099,NULL,NULL); +REPLACE INTO limit_activity VALUES('region',2025,'tech_current','e',0.6520000000000000239,NULL,NULL); +REPLACE INTO limit_activity VALUES('region',2030,'tech_current','e',0.5823319839999999692,NULL,NULL); +REPLACE INTO limit_activity VALUES('region',2035,'tech_current','e',0.4500000000000000111,NULL,NULL); +REPLACE INTO limit_activity VALUES('region',2040,'tech_current','e',0.25,NULL,NULL); +REPLACE INTO limit_activity VALUES('region',2030,'tech_future','e',0.2964180160000000063,NULL,NULL); +REPLACE INTO limit_activity VALUES('region',2035,'tech_future','e',0.5100000000000000088,NULL,NULL); +REPLACE INTO limit_activity VALUES('region',2040,'tech_future','e',0.7349999999999999867,NULL,NULL); +REPLACE INTO limit_activity VALUES('region',2045,'tech_future','e',1.0,NULL,NULL); +REPLACE INTO limit_activity VALUES('region',2050,'tech_future','e',1.0,NULL,NULL); +REPLACE INTO limit_activity VALUES('region',2025,'tech_old','e',0.2999910000000000076,NULL,NULL); +REPLACE INTO limit_activity VALUES('region',2030,'tech_old','e',0.1212499999999999967,NULL,NULL); +REPLACE INTO limit_activity VALUES('region',2035,'tech_old','e',0.04000000000000000083,NULL,NULL); +REPLACE INTO limit_activity VALUES('region',2040,'tech_old','e',0.01499999999999999945,NULL,NULL); +REPLACE INTO limit_capacity VALUES('region',2025,'tech_ancient','e',0.04800000000000000099,NULL,NULL); +REPLACE INTO limit_capacity VALUES('region',2025,'tech_current','e',0.6520000000000000239,NULL,NULL); +REPLACE INTO limit_capacity VALUES('region',2030,'tech_current','e',0.5823319839999999692,NULL,NULL); +REPLACE INTO limit_capacity VALUES('region',2035,'tech_current','e',0.4500000000000000111,NULL,NULL); +REPLACE INTO limit_capacity VALUES('region',2040,'tech_current','e',0.25,NULL,NULL); +REPLACE INTO limit_capacity VALUES('region',2045,'tech_current','e',0.0,NULL,NULL); +REPLACE INTO limit_capacity VALUES('region',2050,'tech_current','e',0.0,NULL,NULL); +REPLACE INTO limit_capacity VALUES('region',2030,'tech_future','e',0.2964180160000000063,NULL,NULL); +REPLACE INTO limit_capacity VALUES('region',2035,'tech_future','e',0.5100000000000000088,NULL,NULL); +REPLACE INTO limit_capacity VALUES('region',2040,'tech_future','e',0.7349999999999999867,NULL,NULL); +REPLACE INTO limit_capacity VALUES('region',2045,'tech_future','e',1.0,NULL,NULL); +REPLACE INTO limit_capacity VALUES('region',2050,'tech_future','e',1.0,NULL,NULL); +REPLACE INTO limit_capacity VALUES('region',2025,'tech_old','e',0.2999999999999999889,NULL,NULL); +REPLACE INTO limit_capacity VALUES('region',2025,'tech_retire','e',9.000000000000000228e-06,NULL,NULL); +REPLACE INTO limit_capacity VALUES('region',2030,'tech_old','e',0.1212499999999999967,NULL,NULL); +REPLACE INTO limit_capacity VALUES('region',2035,'tech_old','e',0.04000000000000000083,NULL,NULL); +REPLACE INTO limit_capacity VALUES('region',2040,'tech_old','e',0.01499999999999999945,NULL,NULL); +REPLACE INTO limit_new_capacity VALUES('region','tech_current',2025,'e',0.659919028340081093,NULL,NULL); +REPLACE INTO limit_new_capacity VALUES('region','tech_future',2030,'e',0.3000182348178138114,NULL,NULL); +REPLACE INTO limit_new_capacity VALUES('region','tech_future',2035,'e',0.232573854939435165,NULL,NULL); +REPLACE INTO limit_new_capacity VALUES('region','tech_future',2040,'e',0.2884229446031822408,NULL,NULL); +REPLACE INTO limit_new_capacity VALUES('region','tech_future',2045,'e',0.4110596210476472612,NULL,NULL); +REPLACE INTO limit_new_capacity VALUES('region','tech_future',2050,'e',0.2251165192346591404,NULL,NULL); +REPLACE INTO region VALUES('region',NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',1990,'tech_ancient',1990,1.0,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2025,'tech_ancient',1990,0.0,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2010,'tech_old',2010,1.0,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2015,'tech_old',2010,0.969999999999999974,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2020,'tech_old',2010,0.8800000000000001154,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2025,'tech_old',2010,0.6199999999999999956,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2030,'tech_old',2010,0.2700000000000000177,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2035,'tech_old',2010,0.08000000000000000166,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2045,'tech_old',2010,0.0,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2025,'tech_current',2025,1.0,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2030,'tech_current',2025,0.969999999999999974,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2035,'tech_current',2025,0.8800000000000001154,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2040,'tech_current',2025,0.6199999999999999956,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2045,'tech_current',2025,0.2700000000000000177,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2050,'tech_current',2025,0.08000000000000000166,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2060,'tech_current',2025,0.0,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2030,'tech_future',2030,1.0,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2035,'tech_future',2030,0.969999999999999974,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2040,'tech_future',2030,0.8800000000000001154,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2045,'tech_future',2030,0.6199999999999999956,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2050,'tech_future',2030,0.2700000000000000177,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2055,'tech_future',2030,0.08000000000000000166,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2065,'tech_future',2030,0.0,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2035,'tech_future',2035,1.0,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2040,'tech_future',2035,0.969999999999999974,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2045,'tech_future',2035,0.8800000000000001154,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2050,'tech_future',2035,0.6199999999999999956,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2055,'tech_future',2035,0.2700000000000000177,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2060,'tech_future',2035,0.08000000000000000166,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2070,'tech_future',2035,0.0,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2040,'tech_future',2040,1.0,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2045,'tech_future',2040,0.969999999999999974,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2050,'tech_future',2040,0.8800000000000001154,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2055,'tech_future',2040,0.6199999999999999956,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2060,'tech_future',2040,0.2700000000000000177,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2065,'tech_future',2040,0.08000000000000000166,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2075,'tech_future',2040,0.0,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2045,'tech_future',2045,1.0,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2050,'tech_future',2045,0.969999999999999974,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2055,'tech_future',2045,0.8800000000000001154,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2060,'tech_future',2045,0.6199999999999999956,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2065,'tech_future',2045,0.2700000000000000177,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2070,'tech_future',2045,0.08000000000000000166,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2080,'tech_future',2045,0.0,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2050,'tech_future',2050,1.0,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2055,'tech_future',2050,0.969999999999999974,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2060,'tech_future',2050,0.8800000000000001154,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2065,'tech_future',2050,0.6199999999999999956,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2070,'tech_future',2050,0.2700000000000000177,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2075,'tech_future',2050,0.08000000000000000166,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2085,'tech_future',2050,0.0,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',1994,'tech_ancient',1994,1.0,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',1999,'tech_ancient',1994,0.969999999999999974,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2004,'tech_ancient',1994,0.8800000000000000044,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2009,'tech_ancient',1994,0.6199999999999999956,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2014,'tech_ancient',1994,0.2700000000000000177,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2019,'tech_ancient',1994,0.08000000000000000166,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2029,'tech_ancient',1994,0.0,NULL); +REPLACE INTO technology_type VALUES('p','production technology'); +REPLACE INTO technology_type VALUES('pb','baseload production technology'); +REPLACE INTO technology_type VALUES('ps','storage production technology'); +REPLACE INTO time_of_day VALUES(0,'d',24.0,NULL); +REPLACE INTO time_period VALUES(-3,1990,'e'); +REPLACE INTO time_period VALUES(-2,1994,'e'); +REPLACE INTO time_period VALUES(-1,2010,'e'); +REPLACE INTO time_period VALUES(0,2025,'f'); +REPLACE INTO time_period VALUES(1,2030,'f'); +REPLACE INTO time_period VALUES(2,2035,'f'); +REPLACE INTO time_period VALUES(3,2040,'f'); +REPLACE INTO time_period VALUES(4,2045,'f'); +REPLACE INTO time_period VALUES(5,2050,'f'); +REPLACE INTO time_period VALUES(6,2055,'f'); +REPLACE INTO time_period_type VALUES('e','existing vintages'); +REPLACE INTO time_period_type VALUES('f','future'); +REPLACE INTO technology VALUES('tech_ancient','p','energy',NULL,NULL,0,0,0,0,1,0,0,0,NULL); +REPLACE INTO technology VALUES('tech_old','p','energy',NULL,NULL,0,0,0,0,1,0,0,0,NULL); +REPLACE INTO technology VALUES('tech_current','p','energy',NULL,NULL,0,0,0,0,1,0,0,0,NULL); +REPLACE INTO technology VALUES('tech_future','p','energy',NULL,NULL,0,0,0,0,1,0,0,0,NULL); +REPLACE INTO technology VALUES('tech_retire','p','energy',NULL,NULL,0,0,0,0,1,0,0,0,NULL); +REPLACE INTO time_season VALUES(0,'s',1.0,NULL); From e36fed6c22e74425786d99e58c2c1354d4a7e25e Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Fri, 26 Jun 2026 10:45:20 -0400 Subject: [PATCH 09/28] Update test set hashes Signed-off-by: Davey Elder --- tests/testing_data/mediumville_sets.json | 7 ++++--- tests/testing_data/test_system_sets.json | 3 ++- tests/testing_data/utopia_sets.json | 3 ++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/testing_data/mediumville_sets.json b/tests/testing_data/mediumville_sets.json index 82bf63af..c86baf5c 100644 --- a/tests/testing_data/mediumville_sets.json +++ b/tests/testing_data/mediumville_sets.json @@ -31,6 +31,7 @@ "flow_in_storage_rpsditvo": "ff680754a218ee843941e77bd07cbc5e5afd74e2b7b29a76ea86621624e8c0fd", "flow_var_annual_rpitvo": "6f5c569787fbf1e0a4076011f75fcd50a840bac85bb57b026f4fc5682739f888", "flow_var_rpsditvo": "e8c35c3b7b1f0c0e10e6f53e94d0c6b8a884c0dd048675cbeb9e7df33794161b", + "lifetime_tech_rt": "6786f2e9acdccce907ebad50d2ea70481bff72a749253e78168d29b480b07f65", "lifetime_process_rtv": "212639322f29aa7687eb9962fb9d74ef60961d720a2a753e2af53473b9e1648f", "limit_activity_constraint_rpt": "90461349933d0b32167abdca242233004b66212b57ed57213dce6f6818203963", "limit_activity_share_constraint_rpgg": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", @@ -101,7 +102,7 @@ "tech_uncap": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", "tech_upramping": "ddcf6ff7665c2a8acc4dff1b43655fae1b5a265135cbee18cec638df4e954346", "tech_with_capacity": "24f75b6f705a36033456d02638e7c50667908c45c474151fb8490666d928c63f", - "time_exist": "91a2461c25439830d94bf4b3d3a3b020343f75e74561a913b1f972b2ac42e943", + "time_exist": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", "time_future": "6b150df8e12ac4dd15396b52f304c7935a41d2cc4da498186552819973171389", "time_manual": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", "time_of_day": "f9bbfe9130c510cba59a13e8b385b4d0206196abbdfe8032b995502bb7215f76", @@ -109,7 +110,7 @@ "time_season": "1e413891065e24bbf66543d4747ff12b1c65bf23c4ff9857b0bf0520eccd7f21", "time_season_sequential": "1e413891065e24bbf66543d4747ff12b1c65bf23c4ff9857b0bf0520eccd7f21", "time_sequencing": "91f69c8abab9959c1f8c90f5aaa56db29bccc67e37d12673ab41c54e4179d7ca", - "vintage_all": "947dd08ad4812a98649b4306e4a0ca1b51374d86f1a8e27c3a8af3b189bcc0f6", - "vintage_exist": "91a2461c25439830d94bf4b3d3a3b020343f75e74561a913b1f972b2ac42e943", + "vintage_all": "9c9380fb50cd4f4f9e2032bd9a18645ae4fc1d30335672c62c897bcb9e099ba5", + "vintage_exist": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", "vintage_optimize": "9c9380fb50cd4f4f9e2032bd9a18645ae4fc1d30335672c62c897bcb9e099ba5" } diff --git a/tests/testing_data/test_system_sets.json b/tests/testing_data/test_system_sets.json index 35cd0b4c..6d73af97 100644 --- a/tests/testing_data/test_system_sets.json +++ b/tests/testing_data/test_system_sets.json @@ -31,6 +31,7 @@ "flow_in_storage_rpsditvo": "8c97fbeacbca1a772ba04f63d0a82f7703500f83c0dc602114b17c64ad4d2e28", "flow_var_annual_rpitvo": "f98f5f41c5cba8ce2a894734abbc5e90310b695bee3c24c52acd8b4800c5ab85", "flow_var_rpsditvo": "784d98e451a8dcf0122f475c2e229162d99e403bdea85f7c608a3d86e850db44", + "lifetime_tech_rt": "984cc175a7aa58e77bb626e3237ca9bf625f5d9b15250870bef1cddc5f57f642", "lifetime_process_rtv": "5033502364848a3a3f295f1b3d051531ecd5c1e5f8bbaecd61afcd18181225ab", "limit_activity_constraint_rpt": "2561103e7e6dd3dee3ba5832290ab5e933f4af08661dff4f2e661b6f4ffefc86", "limit_activity_share_constraint_rpgg": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", @@ -63,7 +64,7 @@ "new_capacity_var_rtv": "bc0ec9e7f812410cb2af924cbc99a31c6e99cd95fc2de26193daad38f33cc132", "operator": "74d830836f1399fb336a0432dde7d7bd36cffa3ff76b1c42d7945350cfb9bf91", "ordered_season_sequential": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", - "process_life_frac_rptv": "5501652d0145dbf7c2e8c006aabd3dbb85ca64ed5caf1d8f45a1957274af81ca", + "process_life_frac_rptv": "d1b75ede9f90899b1c1cbc56489bf9d12c52abc6c0466eb5fe4dccaf6f3be800", "ramp_down_day_constraint_rpsdtv": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", "ramp_down_season_constraint_rpsstv": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", "ramp_up_day_constraint_rpsdtv": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", diff --git a/tests/testing_data/utopia_sets.json b/tests/testing_data/utopia_sets.json index 628c177d..53d8b2a8 100644 --- a/tests/testing_data/utopia_sets.json +++ b/tests/testing_data/utopia_sets.json @@ -31,6 +31,7 @@ "flow_in_storage_rpsditvo": "99eb864a26adcb6633c95462649bca3ef7096f67682702915d7769b54b5de386", "flow_var_annual_rpitvo": "4053866a304af0a6b9a1d293ddaf358dfee5b170dcb01d41e6bc52437908a98a", "flow_var_rpsditvo": "350739a59b14969da094cf1c95524134b9c9acd0989710456a8dcc6596d0a401", + "lifetime_tech_rt": "7b3703e1f021594993a54c55427a599ed03fc512cf34826b7a7670eb93edfb9a", "lifetime_process_rtv": "2cfc288b15f25957dfc70f6396d97ad655ecbed91c5a11582329749f1fb3dbd7", "limit_activity_constraint_rpt": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", "limit_activity_share_constraint_rpgg": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", @@ -63,7 +64,7 @@ "new_capacity_var_rtv": "d4f1cc8b432075001befddab648d54d1f82a29099236948845393a193b6add5b", "operator": "74d830836f1399fb336a0432dde7d7bd36cffa3ff76b1c42d7945350cfb9bf91", "ordered_season_sequential": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", - "process_life_frac_rptv": "37ad263f78986f4d38c63278a0d1a5a85b3eb8b833e8bfabbbea254eaf3a3efb", + "process_life_frac_rptv": "63d17957ee2bebf664ffa6a39cea5e16ec65ad07db1df24dcea6b15bb9ebf589", "ramp_down_day_constraint_rpsdtv": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", "ramp_down_season_constraint_rpsstv": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", "ramp_up_day_constraint_rpsdtv": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", From a79fbb0ec80dbfddbbe983a1753d9d26b97927e1 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Fri, 26 Jun 2026 14:01:08 -0400 Subject: [PATCH 10/28] Improve custom loader filtering for lifetime data Signed-off-by: Davey Elder --- temoa/data_io/hybrid_loader.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/temoa/data_io/hybrid_loader.py b/temoa/data_io/hybrid_loader.py index 9452f2e3..7ccf56cf 100644 --- a/temoa/data_io/hybrid_loader.py +++ b/temoa/data_io/hybrid_loader.py @@ -44,6 +44,7 @@ from temoa.core.config import TemoaConfig from temoa.data_io.loader_manifest import LoadItem + from temoa.types.core_types import Region, Technology, Vintage logger = getLogger(__name__) @@ -99,7 +100,8 @@ def __init__(self, db_connection: Connection, config: TemoaConfig) -> None: self.manager: CommodityNetworkManager | None = None self.efficiency_values: list[tuple[object, ...]] = [] self.data: dict[str, object] | None = None - self.tech_exist_data: set[str] = set() + self.viable_existing_rt: set[tuple[Region, Technology]] = set() + self.viable_existing_rtv: set[tuple[Region, Technology, Vintage]] = set() # --- Viable sets for source-trace filtering --- self.viable_techs: ViableSet | None = None @@ -635,10 +637,14 @@ def _load_existing_capacity( if rows_to_load: tech_exist_data = sorted({(row[1],) for row in rows_to_load}) self._load_component_data(data, model.tech_exist, tech_exist_data) - self.tech_exist_data = {row[1] for row in rows_to_load} vintage_exist_data = sorted({(row[2],) for row in rows_to_load}) self._load_component_data(data, model.vintage_exist, vintage_exist_data) + # Collect existing capacity data indices + self.viable_existing_techs = {row[1] for row in rows_to_load} + self.viable_existing_rt = {(row[0], row[1]) for row in rows_to_load} + self.viable_existing_rtv = {(row[0], row[1], row[2]) for row in rows_to_load} + def _load_retired_existing_capacity( self, data: dict[str, object], @@ -680,10 +686,10 @@ def _load_lifetime_tech( model = TemoaModel() cur = self.con.cursor() rows_to_load = cur.execute('SELECT region, tech, lifetime FROM lifetime_tech').fetchall() - tech_getter = itemgetter(1) - if self.viable_techs: - valid_techs = self.viable_techs.members | self.tech_exist_data - rows_to_load = [item for item in rows_to_load if tech_getter(item) in valid_techs] + rt_getter = itemgetter(0, 1) + if self.viable_rt: + valid_rt = self.viable_rt.members | self.viable_existing_rt + rows_to_load = [item for item in rows_to_load if rt_getter(item) in valid_rt] self._load_component_data(data, model.lifetime_tech, rows_to_load) def _load_lifetime_process( @@ -706,6 +712,10 @@ def _load_lifetime_process( rows_to_load = cur.execute( 'SELECT region, tech, vintage, lifetime FROM lifetime_process' ).fetchall() + rtv_getter = itemgetter(0, 1, 2) + if self.viable_rtv: + valid_rtv = self.viable_rtv.member_tuples | self.viable_existing_rtv + rows_to_load = [item for item in rows_to_load if rtv_getter(item) in valid_rtv] self._load_component_data(data, model.lifetime_process, rows_to_load) def _load_lifetime_survival_curve( @@ -729,6 +739,10 @@ def _load_lifetime_survival_curve( rows_to_load = cur.execute( 'SELECT region, period, tech, vintage, fraction FROM lifetime_survival_curve' ).fetchall() + rtv_getter = itemgetter(0, 2, 3) + if self.viable_rtv: + valid_rtv = self.viable_rtv.member_tuples | self.viable_existing_rtv + rows_to_load = [item for item in rows_to_load if rtv_getter(item) in valid_rtv] self._load_component_data(data, model.lifetime_survival_curve, rows_to_load) # --- Singleton and Configuration-based Components --- From b8b7def17188f8d07608284a5e8f679c951a5e7f Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Fri, 26 Jun 2026 14:01:30 -0400 Subject: [PATCH 11/28] Update existing capacity check to specifically look for tiny dropped capacities Signed-off-by: Davey Elder --- temoa/components/technology.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/temoa/components/technology.py b/temoa/components/technology.py index 4e594b52..9957db97 100644 --- a/temoa/components/technology.py +++ b/temoa/components/technology.py @@ -17,6 +17,8 @@ from pyomo.environ import value +from temoa.components.utils import get_adjusted_existing_capacity + if TYPE_CHECKING: from collections.abc import Iterable @@ -440,14 +442,18 @@ def check_existing_capacity(model: TemoaModel) -> None: ) logger.warning(msg) continue - if t not in model.tech_all: + adjusted_cap = get_adjusted_existing_capacity(model, r, t, v) + if adjusted_cap <= 0.0: + # Was retired in a previous period (myopic mode) continue + p = model.time_optimize.first() life = value(model.lifetime_process[r, t, v]) - if (r, t, v) not in model.process_periods and v + life > model.time_optimize.first(): + if (r, t, v) not in model.process_periods and v + life > p: + surviving_cap = adjusted_cap * value(model.lifetime_survival_curve[r, p, t, v]) msg = ( - f'Existing capacity {r, t, v} with lifetime {life} and capacity {cap} ' - 'should extend into future periods but is not an active process. ' - 'May be missing from Efficiency table or too small to carry forward ' - 'if running in myopic mode.' + f'Existing capacity {r, t, v} with lifetime {life} and surviving capacity ' + f'{surviving_cap} should extend into future periods but is not an active ' + 'process. It may be missing from the Efficiency table or have too little ' + 'capacity to output and carry forward if running in myopic mode.' ) logger.warning(msg) From 49c82b384c160bcaf09d0b0f87ef576bbe7f1954 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Fri, 26 Jun 2026 14:14:12 -0400 Subject: [PATCH 12/28] Try to fix python 3.12 type error Signed-off-by: Davey Elder --- tests/test_full_runs.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/test_full_runs.py b/tests/test_full_runs.py index 5decd266..c71f9b4c 100644 --- a/tests/test_full_runs.py +++ b/tests/test_full_runs.py @@ -5,7 +5,7 @@ import logging import sqlite3 from pathlib import Path -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypedDict import pytest from pyomo.core import Constraint, Var @@ -36,7 +36,20 @@ mc_files = [{'name': 'utopia mc', 'filename': 'config_utopia_mc.toml'}] stochastic_files = [{'name': 'stochastic utopia', 'filename': 'config_utopia_stochastic.toml'}] -myopic_stress_tests = [ + +class MyopicSettings(TypedDict): + view_depth: int + step_size: int + evolving: bool + + +class MyopicStressCase(TypedDict): + name: str + filename: str + myopic: MyopicSettings + + +myopic_stress_tests: list[MyopicStressCase] = [ { 'name': ( f'myopic capacities | {"evolving" if evolving else "non-evolving"}' From ae023f7b6660513fd4fed8103417b967564ff9ad Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Fri, 26 Jun 2026 14:22:21 -0400 Subject: [PATCH 13/28] Check if output retirement table exists Signed-off-by: Davey Elder --- temoa/data_io/hybrid_loader.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/temoa/data_io/hybrid_loader.py b/temoa/data_io/hybrid_loader.py index 7ccf56cf..f657ed68 100644 --- a/temoa/data_io/hybrid_loader.py +++ b/temoa/data_io/hybrid_loader.py @@ -654,6 +654,13 @@ def _load_retired_existing_capacity( """ Only needed in myopic to bring past early retirement decisions forward """ + if not self.table_exists('output_retired_capacity'): + logger.info( + "Table 'output_retired_capacity' not found. Skipping loading " + 'of retired existing capacity.' + ) + return + model = TemoaModel() cur = self.con.cursor() mi = self.myopic_index From be2287bea9d331b358fcfb353b45c50ef0abf26a Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Mon, 29 Jun 2026 16:00:36 -0400 Subject: [PATCH 14/28] Move schema components out of tutorial database for SSOT Signed-off-by: Davey Elder --- temoa/cli.py | 36 +- temoa/tutorial_assets/utopia.sql | 1925 +++++++----------------------- tests/test_cli.py | 2 +- tests/testing_data/utopia_v3.sql | 1558 ++++++++++++++++++++++++ 4 files changed, 2038 insertions(+), 1483 deletions(-) create mode 100644 tests/testing_data/utopia_v3.sql diff --git a/temoa/cli.py b/temoa/cli.py index 371b0d73..cbcc234c 100644 --- a/temoa/cli.py +++ b/temoa/cli.py @@ -557,8 +557,8 @@ def _copy_tutorial_resources(target_config: Path, target_database: Path) -> None """ Copy tutorial resource files directly to target locations. - The database is generated from the SQL source file to ensure it uses - the latest schema with unit-compliant data (single source of truth). + The database is generated by applying the canonical v4 schema first, + then loading tutorial data SQL. Args: target_config: Path where configuration file should be copied @@ -572,6 +572,7 @@ def _copy_tutorial_resources(target_config: Path, target_database: Path) -> None config_resource = base / 'config_sample.toml' sql_resource = base / 'utopia.sql' mc_settings_resource = base / 'mc_settings.csv' + schema_resource = resources.files('temoa.db_schema') / 'temoa_schema_v4.sql' # Copy configuration file with config_resource.open('rb') as source: @@ -582,11 +583,20 @@ def _copy_tutorial_resources(target_config: Path, target_database: Path) -> None if target_database.exists(): target_database.unlink() - # Generate database from SQL source (single source of truth) + # Generate database from canonical schema and tutorial data source + schema_content = schema_resource.read_text(encoding='utf-8') sql_content = sql_resource.read_text(encoding='utf-8') with sqlite3.connect(target_database) as conn: + conn.executescript(schema_content) + conn.execute('PRAGMA foreign_keys = OFF;') conn.executescript(sql_content) + conn.execute('PRAGMA foreign_keys = ON;') + fk_violations = conn.execute('PRAGMA foreign_key_check;').fetchall() + if fk_violations: + raise sqlite3.IntegrityError( + f'Foreign key check failed after tutorial data load: {fk_violations[:5]}' + ) # Copy Monte Carlo settings with mc_settings_resource.open('rb') as source: @@ -600,6 +610,7 @@ def _copy_tutorial_resources(target_config: Path, target_database: Path) -> None fallback_config = Path(__file__).parent / 'tutorial_assets' / 'config_sample.toml' fallback_sql = Path(__file__).parent / 'tutorial_assets' / 'utopia.sql' fallback_mc = Path(__file__).parent / 'tutorial_assets' / 'mc_settings.csv' + fallback_schema = Path(__file__).parent / 'db_schema' / 'temoa_schema_v4.sql' if not fallback_config.exists(): raise FileNotFoundError( @@ -613,6 +624,12 @@ def _copy_tutorial_resources(target_config: Path, target_database: Path) -> None f'SQL: {fallback_sql}' ) from e + if not fallback_schema.exists(): + raise FileNotFoundError( + f'Schema not found. Tried package resources and fallback path:\n' + f'Schema: {fallback_schema}' + ) from e + # Copy config file using fallback path shutil.copy2(fallback_config, target_config) @@ -620,10 +637,19 @@ def _copy_tutorial_resources(target_config: Path, target_database: Path) -> None if target_database.exists(): target_database.unlink() - # Generate database from SQL source + # Generate database from canonical schema and tutorial data source + sql_content = fallback_sql.read_text(encoding='utf-8') with sqlite3.connect(target_database) as conn: - conn.executescript(fallback_sql.read_text(encoding='utf-8')) + conn.executescript(fallback_schema.read_text(encoding='utf-8')) + conn.execute('PRAGMA foreign_keys = OFF;') + conn.executescript(sql_content) + conn.execute('PRAGMA foreign_keys = ON;') + fk_violations = conn.execute('PRAGMA foreign_key_check;').fetchall() + if fk_violations: + raise sqlite3.IntegrityError( + f'Foreign key check failed after tutorial data load: {fk_violations[:5]}' + ) from e # Copy mc_settings from fallback shutil.copy2(fallback_mc, target_config.parent / 'mc_settings.csv') diff --git a/temoa/tutorial_assets/utopia.sql b/temoa/tutorial_assets/utopia.sql index a58fc991..1814f34f 100644 --- a/temoa/tutorial_assets/utopia.sql +++ b/temoa/tutorial_assets/utopia.sql @@ -1,1478 +1,449 @@ -BEGIN TRANSACTION; -CREATE TABLE capacity_credit -( - region TEXT, - period INTEGER - REFERENCES time_period (period), - tech TEXT - REFERENCES technology (tech), - vintage INTEGER, - credit REAL, - notes TEXT, - PRIMARY KEY (region, period, tech, vintage), - CHECK (credit >= 0 AND credit <= 1) -); -CREATE TABLE capacity_factor_process -( - region TEXT, - season TEXT - REFERENCES time_season (season), - tod TEXT - REFERENCES time_of_day (tod), - tech TEXT - REFERENCES technology (tech), - vintage INTEGER, - factor REAL, - notes TEXT, - PRIMARY KEY (region, season, tod, tech, vintage), - CHECK (factor >= 0 AND factor <= 1) -); -INSERT INTO "capacity_factor_process" VALUES('utopia','inter','day','E31',2000,0.2753,''); -INSERT INTO "capacity_factor_process" VALUES('utopia','inter','night','E31',2000,0.2753,''); -INSERT INTO "capacity_factor_process" VALUES('utopia','winter','day','E31',2000,0.2753,''); -INSERT INTO "capacity_factor_process" VALUES('utopia','winter','night','E31',2000,0.2753,''); -INSERT INTO "capacity_factor_process" VALUES('utopia','summer','day','E31',2000,0.2753,''); -INSERT INTO "capacity_factor_process" VALUES('utopia','summer','night','E31',2000,0.2753,''); -INSERT INTO "capacity_factor_process" VALUES('utopia','inter','day','E31',2010,0.2756,''); -INSERT INTO "capacity_factor_process" VALUES('utopia','inter','night','E31',2010,0.2756,''); -INSERT INTO "capacity_factor_process" VALUES('utopia','winter','day','E31',2010,0.2756,''); -INSERT INTO "capacity_factor_process" VALUES('utopia','winter','night','E31',2010,0.2756,''); -INSERT INTO "capacity_factor_process" VALUES('utopia','summer','day','E31',2010,0.2756,''); -INSERT INTO "capacity_factor_process" VALUES('utopia','summer','night','E31',2010,0.2756,''); -CREATE TABLE capacity_factor_tech -( - region TEXT, - season TEXT - REFERENCES time_season (season), - tod TEXT - REFERENCES time_of_day (tod), - tech TEXT - REFERENCES technology (tech), - factor REAL, - notes TEXT, - PRIMARY KEY (region, season, tod, tech), - CHECK (factor >= 0 AND factor <= 1) -); -INSERT INTO "capacity_factor_tech" VALUES('utopia','inter','day','E01',0.8,''); -INSERT INTO "capacity_factor_tech" VALUES('utopia','inter','night','E01',0.8,''); -INSERT INTO "capacity_factor_tech" VALUES('utopia','winter','day','E01',0.8,''); -INSERT INTO "capacity_factor_tech" VALUES('utopia','winter','night','E01',0.8,''); -INSERT INTO "capacity_factor_tech" VALUES('utopia','summer','day','E01',0.8,''); -INSERT INTO "capacity_factor_tech" VALUES('utopia','summer','night','E01',0.8,''); -INSERT INTO "capacity_factor_tech" VALUES('utopia','inter','day','E21',0.8,''); -INSERT INTO "capacity_factor_tech" VALUES('utopia','inter','night','E21',0.8,''); -INSERT INTO "capacity_factor_tech" VALUES('utopia','winter','day','E21',0.8,''); -INSERT INTO "capacity_factor_tech" VALUES('utopia','winter','night','E21',0.8,''); -INSERT INTO "capacity_factor_tech" VALUES('utopia','summer','day','E21',0.8,''); -INSERT INTO "capacity_factor_tech" VALUES('utopia','summer','night','E21',0.8,''); -INSERT INTO "capacity_factor_tech" VALUES('utopia','inter','day','E31',0.275,''); -INSERT INTO "capacity_factor_tech" VALUES('utopia','inter','night','E31',0.275,''); -INSERT INTO "capacity_factor_tech" VALUES('utopia','winter','day','E31',0.275,''); -INSERT INTO "capacity_factor_tech" VALUES('utopia','winter','night','E31',0.275,''); -INSERT INTO "capacity_factor_tech" VALUES('utopia','summer','day','E31',0.275,''); -INSERT INTO "capacity_factor_tech" VALUES('utopia','summer','night','E31',0.275,''); -INSERT INTO "capacity_factor_tech" VALUES('utopia','inter','day','E51',0.17,''); -INSERT INTO "capacity_factor_tech" VALUES('utopia','inter','night','E51',0.17,''); -INSERT INTO "capacity_factor_tech" VALUES('utopia','winter','day','E51',0.17,''); -INSERT INTO "capacity_factor_tech" VALUES('utopia','winter','night','E51',0.17,''); -INSERT INTO "capacity_factor_tech" VALUES('utopia','summer','day','E51',0.17,''); -INSERT INTO "capacity_factor_tech" VALUES('utopia','summer','night','E51',0.17,''); -INSERT INTO "capacity_factor_tech" VALUES('utopia','inter','day','E70',0.8,''); -INSERT INTO "capacity_factor_tech" VALUES('utopia','inter','night','E70',0.8,''); -INSERT INTO "capacity_factor_tech" VALUES('utopia','winter','day','E70',0.8,''); -INSERT INTO "capacity_factor_tech" VALUES('utopia','winter','night','E70',0.8,''); -INSERT INTO "capacity_factor_tech" VALUES('utopia','summer','day','E70',0.8,''); -INSERT INTO "capacity_factor_tech" VALUES('utopia','summer','night','E70',0.8,''); -CREATE TABLE capacity_to_activity -( - region TEXT, - tech TEXT - REFERENCES technology (tech), - c2a REAL, - units TEXT, - notes TEXT, - PRIMARY KEY (region, tech) -); -INSERT INTO "capacity_to_activity" VALUES('utopia','E01',31.54,'PJ / (GW * year)',''); -INSERT INTO "capacity_to_activity" VALUES('utopia','E21',31.54,'PJ / (GW * year)',''); -INSERT INTO "capacity_to_activity" VALUES('utopia','E31',31.54,'PJ / (GW * year)',''); -INSERT INTO "capacity_to_activity" VALUES('utopia','E51',31.54,'PJ / (GW * year)',''); -INSERT INTO "capacity_to_activity" VALUES('utopia','E70',31.54,'PJ / (GW * year)',''); -INSERT INTO "capacity_to_activity" VALUES('utopia','RHE',1.0,'PJ / (GW * year)',''); -INSERT INTO "capacity_to_activity" VALUES('utopia','RHO',1.0,'PJ / (GW * year)',''); -INSERT INTO "capacity_to_activity" VALUES('utopia','RL1',1.0,'PJ / (GW * year)',''); -INSERT INTO "capacity_to_activity" VALUES('utopia','SRE',1.0,'PJ / (GW * year)',''); -INSERT INTO "capacity_to_activity" VALUES('utopia','TXD',1.0,'PJ / (GW * year)',''); -INSERT INTO "capacity_to_activity" VALUES('utopia','TXE',1.0,'PJ / (GW * year)',''); -INSERT INTO "capacity_to_activity" VALUES('utopia','TXG',1.0,'PJ / (GW * year)',''); -CREATE TABLE commodity -( - name TEXT - PRIMARY KEY, - flag TEXT - REFERENCES commodity_type (label), - description TEXT, - units TEXT -); -INSERT INTO "commodity" VALUES('ethos','s','# dummy commodity to supply inputs','PJ'); -INSERT INTO "commodity" VALUES('DSL','p','# diesel','PJ'); -INSERT INTO "commodity" VALUES('ELC','p','# electricity','PJ'); -INSERT INTO "commodity" VALUES('FEQ','p','# fossil equivalent','PJ'); -INSERT INTO "commodity" VALUES('GSL','p','# gasoline','PJ'); -INSERT INTO "commodity" VALUES('HCO','p','# coal','PJ'); -INSERT INTO "commodity" VALUES('HYD','p','# water','PJ'); -INSERT INTO "commodity" VALUES('OIL','p','# crude oil','PJ'); -INSERT INTO "commodity" VALUES('URN','p','# uranium','PJ'); -INSERT INTO "commodity" VALUES('co2','e','#CO2 emissions','Mt'); -INSERT INTO "commodity" VALUES('nox','e','#NOX emissions','Mt'); -INSERT INTO "commodity" VALUES('RH','d','# residential heating','PJ'); -INSERT INTO "commodity" VALUES('RL','d','# residential lighting','PJ'); -INSERT INTO "commodity" VALUES('TX','d','# transportation','PJ'); -CREATE TABLE commodity_type -( - label TEXT - PRIMARY KEY, - description TEXT -); -INSERT INTO "commodity_type" VALUES('w','waste commodity'); -INSERT INTO "commodity_type" VALUES('wa','waste annual commodity'); -INSERT INTO "commodity_type" VALUES('wp','waste physical commodity'); -INSERT INTO "commodity_type" VALUES('a','annual commodity'); -INSERT INTO "commodity_type" VALUES('s','source commodity'); -INSERT INTO "commodity_type" VALUES('p','physical commodity'); -INSERT INTO "commodity_type" VALUES('e','emissions commodity'); -INSERT INTO "commodity_type" VALUES('d','demand commodity'); -CREATE TABLE construction_input -( - region TEXT, - input_comm TEXT - REFERENCES commodity (name), - tech TEXT - REFERENCES technology (tech), - vintage INTEGER - REFERENCES time_period (period), - value REAL, - units TEXT, - notes TEXT, - PRIMARY KEY (region, input_comm, tech, vintage) -); -CREATE TABLE cost_emission -( - region TEXT, - period INTEGER - REFERENCES time_period (period), - emis_comm TEXT NOT NULL - REFERENCES commodity (name), - cost REAL NOT NULL, - units TEXT, - notes TEXT, - PRIMARY KEY (region, period, emis_comm) -); -CREATE TABLE cost_fixed -( - region TEXT NOT NULL, - period INTEGER NOT NULL - REFERENCES time_period (period), - tech TEXT NOT NULL - REFERENCES technology (tech), - vintage INTEGER NOT NULL - REFERENCES time_period (period), - cost REAL, - units TEXT, - notes TEXT, - PRIMARY KEY (region, period, tech, vintage) -); -INSERT INTO "cost_fixed" VALUES('utopia',1990,'E01',1960,40.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',1990,'E01',1970,40.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',1990,'E01',1980,40.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',1990,'E01',1990,40.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2000,'E01',1970,70.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2000,'E01',1980,70.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2000,'E01',1990,70.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2000,'E01',2000,70.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2010,'E01',1980,100.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2010,'E01',1990,100.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2010,'E01',2000,100.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2010,'E01',2010,100.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',1990,'E21',1990,500.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2000,'E21',1990,500.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2010,'E21',1990,500.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2000,'E21',2000,500.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2010,'E21',2000,500.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2010,'E21',2010,500.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',1990,'E31',1980,75.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',1990,'E31',1990,75.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2000,'E31',1980,75.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2000,'E31',1990,75.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2000,'E31',2000,75.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2010,'E31',1980,75.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2010,'E31',1990,75.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2010,'E31',2000,75.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2010,'E31',2010,75.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',1990,'E51',1980,30.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',1990,'E51',1990,30.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2000,'E51',1980,30.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2000,'E51',1990,30.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2000,'E51',2000,30.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2010,'E51',1980,30.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2010,'E51',1990,30.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2010,'E51',2000,30.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2010,'E51',2010,30.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',1990,'E70',1960,30.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',1990,'E70',1970,30.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',1990,'E70',1980,30.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',1990,'E70',1990,30.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2000,'E70',1970,30.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2000,'E70',1980,30.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2000,'E70',1990,30.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2000,'E70',2000,30.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2010,'E70',1980,30.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2010,'E70',1990,30.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2010,'E70',2000,30.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2010,'E70',2010,30.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',1990,'RHO',1970,1.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',1990,'RHO',1980,1.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',1990,'RHO',1990,1.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2000,'RHO',1980,1.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2000,'RHO',1990,1.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2000,'RHO',2000,1.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2010,'RHO',1990,1.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2010,'RHO',2000,1.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2010,'RHO',2010,1.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',1990,'RL1',1980,9.46,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',1990,'RL1',1990,9.46,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2000,'RL1',2000,9.46,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2010,'RL1',2010,9.46,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',1990,'TXD',1970,52.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',1990,'TXD',1980,52.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',1990,'TXD',1990,52.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2000,'TXD',1980,52.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2000,'TXD',1990,52.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2000,'TXD',2000,52.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2010,'TXD',2000,52.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2010,'TXD',2010,52.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',1990,'TXE',1990,100.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2000,'TXE',1990,90.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2000,'TXE',2000,90.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2010,'TXE',2000,80.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2010,'TXE',2010,80.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',1990,'TXG',1970,48.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',1990,'TXG',1980,48.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',1990,'TXG',1990,48.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2000,'TXG',1980,48.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2000,'TXG',1990,48.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2000,'TXG',2000,48.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2010,'TXG',2000,48.0,'Mdollar / (PJ^2 / GW / year)',''); -INSERT INTO "cost_fixed" VALUES('utopia',2010,'TXG',2010,48.0,'Mdollar / (PJ^2 / GW / year)',''); -CREATE TABLE cost_invest -( - region TEXT, - tech TEXT - REFERENCES technology (tech), - vintage INTEGER - REFERENCES time_period (period), - cost REAL, - units TEXT, - notes TEXT, - PRIMARY KEY (region, tech, vintage) -); -INSERT INTO "cost_invest" VALUES('utopia','E01',1990,2000.0,'Mdollar / (PJ^2 / GW)',''); -INSERT INTO "cost_invest" VALUES('utopia','E01',2000,1300.0,'Mdollar / (PJ^2 / GW)',''); -INSERT INTO "cost_invest" VALUES('utopia','E01',2010,1200.0,'Mdollar / (PJ^2 / GW)',''); -INSERT INTO "cost_invest" VALUES('utopia','E21',1990,5000.0,'Mdollar / (PJ^2 / GW)',''); -INSERT INTO "cost_invest" VALUES('utopia','E21',2000,5000.0,'Mdollar / (PJ^2 / GW)',''); -INSERT INTO "cost_invest" VALUES('utopia','E21',2010,5000.0,'Mdollar / (PJ^2 / GW)',''); -INSERT INTO "cost_invest" VALUES('utopia','E31',1990,3000.0,'Mdollar / (PJ^2 / GW)',''); -INSERT INTO "cost_invest" VALUES('utopia','E31',2000,3000.0,'Mdollar / (PJ^2 / GW)',''); -INSERT INTO "cost_invest" VALUES('utopia','E31',2010,3000.0,'Mdollar / (PJ^2 / GW)',''); -INSERT INTO "cost_invest" VALUES('utopia','E51',1990,900.0,'Mdollar / (PJ^2 / GW)',''); -INSERT INTO "cost_invest" VALUES('utopia','E51',2000,900.0,'Mdollar / (PJ^2 / GW)',''); -INSERT INTO "cost_invest" VALUES('utopia','E51',2010,900.0,'Mdollar / (PJ^2 / GW)',''); -INSERT INTO "cost_invest" VALUES('utopia','E70',1990,1000.0,'Mdollar / (PJ^2 / GW)',''); -INSERT INTO "cost_invest" VALUES('utopia','E70',2000,1000.0,'Mdollar / (PJ^2 / GW)',''); -INSERT INTO "cost_invest" VALUES('utopia','E70',2010,1000.0,'Mdollar / (PJ^2 / GW)',''); -INSERT INTO "cost_invest" VALUES('utopia','RHE',1990,90.0,'Mdollar / (PJ^2 / GW)',''); -INSERT INTO "cost_invest" VALUES('utopia','RHE',2000,90.0,'Mdollar / (PJ^2 / GW)',''); -INSERT INTO "cost_invest" VALUES('utopia','RHE',2010,90.0,'Mdollar / (PJ^2 / GW)',''); -INSERT INTO "cost_invest" VALUES('utopia','RHO',1990,100.0,'Mdollar / (PJ^2 / GW)',''); -INSERT INTO "cost_invest" VALUES('utopia','RHO',2000,100.0,'Mdollar / (PJ^2 / GW)',''); -INSERT INTO "cost_invest" VALUES('utopia','RHO',2010,100.0,'Mdollar / (PJ^2 / GW)',''); -INSERT INTO "cost_invest" VALUES('utopia','SRE',1990,100.0,'Mdollar / (PJ^2 / GW)',''); -INSERT INTO "cost_invest" VALUES('utopia','SRE',2000,100.0,'Mdollar / (PJ^2 / GW)',''); -INSERT INTO "cost_invest" VALUES('utopia','SRE',2010,100.0,'Mdollar / (PJ^2 / GW)',''); -INSERT INTO "cost_invest" VALUES('utopia','TXD',1990,1044.0,'Mdollar / (PJ^2 / GW)',''); -INSERT INTO "cost_invest" VALUES('utopia','TXD',2000,1044.0,'Mdollar / (PJ^2 / GW)',''); -INSERT INTO "cost_invest" VALUES('utopia','TXD',2010,1044.0,'Mdollar / (PJ^2 / GW)',''); -INSERT INTO "cost_invest" VALUES('utopia','TXE',1990,2000.0,'Mdollar / (PJ^2 / GW)',''); -INSERT INTO "cost_invest" VALUES('utopia','TXE',2000,1750.0,'Mdollar / (PJ^2 / GW)',''); -INSERT INTO "cost_invest" VALUES('utopia','TXE',2010,1500.0,'Mdollar / (PJ^2 / GW)',''); -INSERT INTO "cost_invest" VALUES('utopia','TXG',1990,1044.0,'Mdollar / (PJ^2 / GW)',''); -INSERT INTO "cost_invest" VALUES('utopia','TXG',2000,1044.0,'Mdollar / (PJ^2 / GW)',''); -INSERT INTO "cost_invest" VALUES('utopia','TXG',2010,1044.0,'Mdollar / (PJ^2 / GW)',''); -CREATE TABLE cost_variable -( - region TEXT NOT NULL, - period INTEGER NOT NULL - REFERENCES time_period (period), - tech TEXT NOT NULL - REFERENCES technology (tech), - vintage INTEGER NOT NULL - REFERENCES time_period (period), - cost REAL, - units TEXT, - notes TEXT, - PRIMARY KEY (region, period, tech, vintage) -); -INSERT INTO "cost_variable" VALUES('utopia',1990,'IMPDSL1',1990,10.0,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2000,'IMPDSL1',1990,10.0,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2010,'IMPDSL1',1990,10.0,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',1990,'IMPGSL1',1990,15.0,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2000,'IMPGSL1',1990,15.0,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2010,'IMPGSL1',1990,15.0,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',1990,'IMPHCO1',1990,2.0,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2000,'IMPHCO1',1990,2.0,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2010,'IMPHCO1',1990,2.0,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',1990,'IMPOIL1',1990,8.0,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2000,'IMPOIL1',1990,8.0,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2010,'IMPOIL1',1990,8.0,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',1990,'IMPURN1',1990,2.0,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2000,'IMPURN1',1990,2.0,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2010,'IMPURN1',1990,2.0,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',1990,'E01',1960,0.3,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',1990,'E01',1970,0.3,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',1990,'E01',1980,0.3,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',1990,'E01',1990,0.3,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2000,'E01',1970,0.3,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2000,'E01',1980,0.3,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2000,'E01',1990,0.3,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2000,'E01',2000,0.3,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2010,'E01',1980,0.3,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2010,'E01',1990,0.3,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2010,'E01',2000,0.3,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2010,'E01',2010,0.3,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',1990,'E21',1990,1.5,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2000,'E21',1990,1.5,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2010,'E21',1990,1.5,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2000,'E21',2000,1.5,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2010,'E21',2000,1.5,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2010,'E21',2010,1.5,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',1990,'E70',1960,0.4,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',1990,'E70',1970,0.4,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',1990,'E70',1980,0.4,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',1990,'E70',1990,0.4,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2000,'E70',1970,0.4,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2000,'E70',1980,0.4,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2000,'E70',1990,0.4,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2000,'E70',2000,0.4,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2010,'E70',1980,0.4,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2010,'E70',1990,0.4,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2010,'E70',2000,0.4,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2010,'E70',2010,0.4,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',1990,'SRE',1990,10.0,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2000,'SRE',1990,10.0,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2000,'SRE',2000,10.0,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2010,'SRE',1990,10.0,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2010,'SRE',2000,10.0,'Mdollar / (PJ)',''); -INSERT INTO "cost_variable" VALUES('utopia',2010,'SRE',2010,10.0,'Mdollar / (PJ)',''); -CREATE TABLE demand -( - region TEXT, - period INTEGER - REFERENCES time_period (period), - commodity TEXT - REFERENCES commodity (name), - demand REAL, - units TEXT, - notes TEXT, - PRIMARY KEY (region, period, commodity) -); -INSERT INTO "demand" VALUES('utopia',1990,'RH',25.2,'PJ',''); -INSERT INTO "demand" VALUES('utopia',2000,'RH',37.8,'PJ',''); -INSERT INTO "demand" VALUES('utopia',2010,'RH',56.7,'PJ',''); -INSERT INTO "demand" VALUES('utopia',1990,'RL',5.6,'PJ',''); -INSERT INTO "demand" VALUES('utopia',2000,'RL',8.4,'PJ',''); -INSERT INTO "demand" VALUES('utopia',2010,'RL',12.6,'PJ',''); -INSERT INTO "demand" VALUES('utopia',1990,'TX',5.2,'PJ',''); -INSERT INTO "demand" VALUES('utopia',2000,'TX',7.8,'PJ',''); -INSERT INTO "demand" VALUES('utopia',2010,'TX',11.69,'PJ',''); -CREATE TABLE demand_specific_distribution -( - region TEXT, - period INTEGER - REFERENCES time_period (period), - season TEXT - REFERENCES time_season (season), - tod TEXT - REFERENCES time_of_day (tod), - demand_name TEXT - REFERENCES commodity (name), - dsd REAL, - notes TEXT, - PRIMARY KEY (region, period, season, tod, demand_name), - CHECK (dsd >= 0 AND dsd <= 1) -); -INSERT INTO "demand_specific_distribution" VALUES('utopia',1990,'inter','day','RH',0.12,''); -INSERT INTO "demand_specific_distribution" VALUES('utopia',1990,'inter','night','RH',0.06,''); -INSERT INTO "demand_specific_distribution" VALUES('utopia',1990,'winter','day','RH',0.5467,''); -INSERT INTO "demand_specific_distribution" VALUES('utopia',1990,'winter','night','RH',0.2733,''); -INSERT INTO "demand_specific_distribution" VALUES('utopia',1990,'inter','day','RL',0.15,''); -INSERT INTO "demand_specific_distribution" VALUES('utopia',1990,'inter','night','RL',0.05,''); -INSERT INTO "demand_specific_distribution" VALUES('utopia',1990,'summer','day','RL',0.15,''); -INSERT INTO "demand_specific_distribution" VALUES('utopia',1990,'summer','night','RL',0.05,''); -INSERT INTO "demand_specific_distribution" VALUES('utopia',1990,'winter','day','RL',0.5,''); -INSERT INTO "demand_specific_distribution" VALUES('utopia',1990,'winter','night','RL',0.1,''); -INSERT INTO "demand_specific_distribution" VALUES('utopia',2000,'inter','day','RH',0.12,''); -INSERT INTO "demand_specific_distribution" VALUES('utopia',2000,'inter','night','RH',0.06,''); -INSERT INTO "demand_specific_distribution" VALUES('utopia',2000,'winter','day','RH',0.5467,''); -INSERT INTO "demand_specific_distribution" VALUES('utopia',2000,'winter','night','RH',0.2733,''); -INSERT INTO "demand_specific_distribution" VALUES('utopia',2000,'inter','day','RL',0.15,''); -INSERT INTO "demand_specific_distribution" VALUES('utopia',2000,'inter','night','RL',0.05,''); -INSERT INTO "demand_specific_distribution" VALUES('utopia',2000,'summer','day','RL',0.15,''); -INSERT INTO "demand_specific_distribution" VALUES('utopia',2000,'summer','night','RL',0.05,''); -INSERT INTO "demand_specific_distribution" VALUES('utopia',2000,'winter','day','RL',0.5,''); -INSERT INTO "demand_specific_distribution" VALUES('utopia',2000,'winter','night','RL',0.1,''); -INSERT INTO "demand_specific_distribution" VALUES('utopia',2010,'inter','day','RH',0.12,''); -INSERT INTO "demand_specific_distribution" VALUES('utopia',2010,'inter','night','RH',0.06,''); -INSERT INTO "demand_specific_distribution" VALUES('utopia',2010,'winter','day','RH',0.5467,''); -INSERT INTO "demand_specific_distribution" VALUES('utopia',2010,'winter','night','RH',0.2733,''); -INSERT INTO "demand_specific_distribution" VALUES('utopia',2010,'inter','day','RL',0.15,''); -INSERT INTO "demand_specific_distribution" VALUES('utopia',2010,'inter','night','RL',0.05,''); -INSERT INTO "demand_specific_distribution" VALUES('utopia',2010,'summer','day','RL',0.15,''); -INSERT INTO "demand_specific_distribution" VALUES('utopia',2010,'summer','night','RL',0.05,''); -INSERT INTO "demand_specific_distribution" VALUES('utopia',2010,'winter','day','RL',0.5,''); -INSERT INTO "demand_specific_distribution" VALUES('utopia',2010,'winter','night','RL',0.1,''); -CREATE TABLE efficiency -( - region TEXT, - input_comm TEXT - REFERENCES commodity (name), - tech TEXT - REFERENCES technology (tech), - vintage INTEGER - REFERENCES time_period (period), - output_comm TEXT - REFERENCES commodity (name), - efficiency REAL, - units TEXT, - notes TEXT, - PRIMARY KEY (region, input_comm, tech, vintage, output_comm), - CHECK (efficiency > 0) -); -INSERT INTO "efficiency" VALUES('utopia','ethos','IMPDSL1',1990,'DSL',1.0,'PJ / (PJ)',''); -INSERT INTO "efficiency" VALUES('utopia','ethos','IMPGSL1',1990,'GSL',1.0,'PJ / (PJ)',''); -INSERT INTO "efficiency" VALUES('utopia','ethos','IMPHCO1',1990,'HCO',1.0,'PJ / (PJ)',''); -INSERT INTO "efficiency" VALUES('utopia','ethos','IMPOIL1',1990,'OIL',1.0,'PJ / (PJ)',''); -INSERT INTO "efficiency" VALUES('utopia','ethos','IMPURN1',1990,'URN',1.0,'PJ / (PJ)',''); -INSERT INTO "efficiency" VALUES('utopia','ethos','IMPFEQ',1990,'FEQ',1.0,'PJ / (PJ)',''); -INSERT INTO "efficiency" VALUES('utopia','ethos','IMPHYD',1990,'HYD',1.0,'PJ / (PJ)',''); -INSERT INTO "efficiency" VALUES('utopia','HCO','E01',1960,'ELC',0.32,'PJ / (PJ)','# 1/3.125'); -INSERT INTO "efficiency" VALUES('utopia','HCO','E01',1970,'ELC',0.32,'PJ / (PJ)','# 1/3.125'); -INSERT INTO "efficiency" VALUES('utopia','HCO','E01',1980,'ELC',0.32,'PJ / (PJ)','# 1/3.125'); -INSERT INTO "efficiency" VALUES('utopia','HCO','E01',1990,'ELC',0.32,'PJ / (PJ)','# 1/3.125'); -INSERT INTO "efficiency" VALUES('utopia','HCO','E01',2000,'ELC',0.32,'PJ / (PJ)','# 1/3.125'); -INSERT INTO "efficiency" VALUES('utopia','HCO','E01',2010,'ELC',0.32,'PJ / (PJ)','# 1/3.125'); -INSERT INTO "efficiency" VALUES('utopia','FEQ','E21',1990,'ELC',0.32,'PJ / (PJ)','# 1/3.125'); -INSERT INTO "efficiency" VALUES('utopia','FEQ','E21',2000,'ELC',0.32,'PJ / (PJ)','# 1/3.125'); -INSERT INTO "efficiency" VALUES('utopia','FEQ','E21',2010,'ELC',0.32,'PJ / (PJ)','# 1/3.125'); -INSERT INTO "efficiency" VALUES('utopia','URN','E21',1990,'ELC',0.4,'PJ / (PJ)','# 1/2.5'); -INSERT INTO "efficiency" VALUES('utopia','URN','E21',2000,'ELC',0.4,'PJ / (PJ)','# 1/2.5'); -INSERT INTO "efficiency" VALUES('utopia','URN','E21',2010,'ELC',0.4,'PJ / (PJ)','# 1/2.5'); -INSERT INTO "efficiency" VALUES('utopia','HYD','E31',1980,'ELC',0.32,'PJ / (PJ)','# 1/3.125'); -INSERT INTO "efficiency" VALUES('utopia','HYD','E31',1990,'ELC',0.32,'PJ / (PJ)','# 1/3.125'); -INSERT INTO "efficiency" VALUES('utopia','HYD','E31',2000,'ELC',0.32,'PJ / (PJ)','# 1/3.125'); -INSERT INTO "efficiency" VALUES('utopia','HYD','E31',2010,'ELC',0.32,'PJ / (PJ)','# 1/3.125'); -INSERT INTO "efficiency" VALUES('utopia','DSL','E70',1960,'ELC',0.294,'PJ / (PJ)','# 1/3.4'); -INSERT INTO "efficiency" VALUES('utopia','DSL','E70',1970,'ELC',0.294,'PJ / (PJ)','# 1/3.4'); -INSERT INTO "efficiency" VALUES('utopia','DSL','E70',1980,'ELC',0.294,'PJ / (PJ)','# 1/3.4'); -INSERT INTO "efficiency" VALUES('utopia','DSL','E70',1990,'ELC',0.294,'PJ / (PJ)','# 1/3.4'); -INSERT INTO "efficiency" VALUES('utopia','DSL','E70',2000,'ELC',0.294,'PJ / (PJ)','# 1/3.4'); -INSERT INTO "efficiency" VALUES('utopia','DSL','E70',2010,'ELC',0.294,'PJ / (PJ)','# 1/3.4'); -INSERT INTO "efficiency" VALUES('utopia','ELC','E51',1980,'ELC',0.72,'PJ / (PJ)','# 1/1.3889'); -INSERT INTO "efficiency" VALUES('utopia','ELC','E51',1990,'ELC',0.72,'PJ / (PJ)','# 1/1.3889'); -INSERT INTO "efficiency" VALUES('utopia','ELC','E51',2000,'ELC',0.72,'PJ / (PJ)','# 1/1.3889'); -INSERT INTO "efficiency" VALUES('utopia','ELC','E51',2010,'ELC',0.72,'PJ / (PJ)','# 1/1.3889'); -INSERT INTO "efficiency" VALUES('utopia','ELC','RHE',1990,'RH',1.0,'PJ / (PJ)','# direct translation from DMD_EFF'); -INSERT INTO "efficiency" VALUES('utopia','ELC','RHE',2000,'RH',1.0,'PJ / (PJ)','# direct translation from DMD_EFF'); -INSERT INTO "efficiency" VALUES('utopia','ELC','RHE',2010,'RH',1.0,'PJ / (PJ)','# direct translation from DMD_EFF'); -INSERT INTO "efficiency" VALUES('utopia','DSL','RHO',1970,'RH',0.7,'PJ / (PJ)','# direct translation from DMD_EFF'); -INSERT INTO "efficiency" VALUES('utopia','DSL','RHO',1980,'RH',0.7,'PJ / (PJ)','# direct translation from DMD_EFF'); -INSERT INTO "efficiency" VALUES('utopia','DSL','RHO',1990,'RH',0.7,'PJ / (PJ)','# direct translation from DMD_EFF'); -INSERT INTO "efficiency" VALUES('utopia','DSL','RHO',2000,'RH',0.7,'PJ / (PJ)','# direct translation from DMD_EFF'); -INSERT INTO "efficiency" VALUES('utopia','DSL','RHO',2010,'RH',0.7,'PJ / (PJ)','# direct translation from DMD_EFF'); -INSERT INTO "efficiency" VALUES('utopia','ELC','RL1',1980,'RL',1.0,'PJ / (PJ)','# direct translation from DMD_EFF'); -INSERT INTO "efficiency" VALUES('utopia','ELC','RL1',1990,'RL',1.0,'PJ / (PJ)','# direct translation from DMD_EFF'); -INSERT INTO "efficiency" VALUES('utopia','ELC','RL1',2000,'RL',1.0,'PJ / (PJ)','# direct translation from DMD_EFF'); -INSERT INTO "efficiency" VALUES('utopia','ELC','RL1',2010,'RL',1.0,'PJ / (PJ)','# direct translation from DMD_EFF'); -INSERT INTO "efficiency" VALUES('utopia','OIL','SRE',1990,'DSL',1.0,'PJ / (PJ)','# direct translation from PRC_INP2, PRC_OUT'); -INSERT INTO "efficiency" VALUES('utopia','OIL','SRE',2000,'DSL',1.0,'PJ / (PJ)','# direct translation from PRC_INP2, PRC_OUT'); -INSERT INTO "efficiency" VALUES('utopia','OIL','SRE',2010,'DSL',1.0,'PJ / (PJ)','# direct translation from PRC_INP2, PRC_OUT'); -INSERT INTO "efficiency" VALUES('utopia','OIL','SRE',1990,'GSL',1.0,'PJ / (PJ)','# direct translation from PRC_INP2, PRC_OUT'); -INSERT INTO "efficiency" VALUES('utopia','OIL','SRE',2000,'GSL',1.0,'PJ / (PJ)','# direct translation from PRC_INP2, PRC_OUT'); -INSERT INTO "efficiency" VALUES('utopia','OIL','SRE',2010,'GSL',1.0,'PJ / (PJ)','# direct translation from PRC_INP2, PRC_OUT'); -INSERT INTO "efficiency" VALUES('utopia','DSL','TXD',1970,'TX',0.231,'PJ / (PJ)','# direct translation from DMD_EFF'); -INSERT INTO "efficiency" VALUES('utopia','DSL','TXD',1980,'TX',0.231,'PJ / (PJ)','# direct translation from DMD_EFF'); -INSERT INTO "efficiency" VALUES('utopia','DSL','TXD',1990,'TX',0.231,'PJ / (PJ)','# direct translation from DMD_EFF'); -INSERT INTO "efficiency" VALUES('utopia','DSL','TXD',2000,'TX',0.231,'PJ / (PJ)','# direct translation from DMD_EFF'); -INSERT INTO "efficiency" VALUES('utopia','DSL','TXD',2010,'TX',0.231,'PJ / (PJ)','# direct translation from DMD_EFF'); -INSERT INTO "efficiency" VALUES('utopia','ELC','TXE',1990,'TX',0.827,'PJ / (PJ)','# direct translation from DMD_EFF'); -INSERT INTO "efficiency" VALUES('utopia','ELC','TXE',2000,'TX',0.827,'PJ / (PJ)','# direct translation from DMD_EFF'); -INSERT INTO "efficiency" VALUES('utopia','ELC','TXE',2010,'TX',0.827,'PJ / (PJ)','# direct translation from DMD_EFF'); -INSERT INTO "efficiency" VALUES('utopia','GSL','TXG',1970,'TX',0.231,'PJ / (PJ)','# direct translation from DMD_EFF'); -INSERT INTO "efficiency" VALUES('utopia','GSL','TXG',1980,'TX',0.231,'PJ / (PJ)','# direct translation from DMD_EFF'); -INSERT INTO "efficiency" VALUES('utopia','GSL','TXG',1990,'TX',0.231,'PJ / (PJ)','# direct translation from DMD_EFF'); -INSERT INTO "efficiency" VALUES('utopia','GSL','TXG',2000,'TX',0.231,'PJ / (PJ)','# direct translation from DMD_EFF'); -INSERT INTO "efficiency" VALUES('utopia','GSL','TXG',2010,'TX',0.231,'PJ / (PJ)','# direct translation from DMD_EFF'); -CREATE TABLE efficiency_variable -( - region TEXT, - season TEXT - REFERENCES time_season (season), - tod TEXT - REFERENCES time_of_day (tod), - input_comm TEXT - REFERENCES commodity (name), - tech TEXT - REFERENCES technology (tech), - vintage INTEGER - REFERENCES time_period (period), - output_comm TEXT - REFERENCES commodity (name), - efficiency REAL, - notes TEXT, - PRIMARY KEY (region, season, tod, input_comm, tech, vintage, output_comm), - CHECK (efficiency > 0) -); -CREATE TABLE emission_activity -( - region TEXT, - emis_comm TEXT - REFERENCES commodity (name), - input_comm TEXT - REFERENCES commodity (name), - tech TEXT - REFERENCES technology (tech), - vintage INTEGER - REFERENCES time_period (period), - output_comm TEXT - REFERENCES commodity (name), - activity REAL, - units TEXT, - notes TEXT, - PRIMARY KEY (region, emis_comm, input_comm, tech, vintage, output_comm) -); -INSERT INTO "emission_activity" VALUES('utopia','co2','ethos','IMPDSL1',1990,'DSL',0.075,'Mt / (PJ)',''); -INSERT INTO "emission_activity" VALUES('utopia','co2','ethos','IMPGSL1',1990,'GSL',0.075,'Mt / (PJ)',''); -INSERT INTO "emission_activity" VALUES('utopia','co2','ethos','IMPHCO1',1990,'HCO',8.9e-02,'Mt / (PJ)',''); -INSERT INTO "emission_activity" VALUES('utopia','co2','ethos','IMPOIL1',1990,'OIL',0.075,'Mt / (PJ)',''); -INSERT INTO "emission_activity" VALUES('utopia','nox','DSL','TXD',1970,'TX',1.0,'Mt / (PJ)',''); -INSERT INTO "emission_activity" VALUES('utopia','nox','DSL','TXD',1980,'TX',1.0,'Mt / (PJ)',''); -INSERT INTO "emission_activity" VALUES('utopia','nox','DSL','TXD',1990,'TX',1.0,'Mt / (PJ)',''); -INSERT INTO "emission_activity" VALUES('utopia','nox','DSL','TXD',2000,'TX',1.0,'Mt / (PJ)',''); -INSERT INTO "emission_activity" VALUES('utopia','nox','DSL','TXD',2010,'TX',1.0,'Mt / (PJ)',''); -INSERT INTO "emission_activity" VALUES('utopia','nox','GSL','TXG',1970,'TX',1.0,'Mt / (PJ)',''); -INSERT INTO "emission_activity" VALUES('utopia','nox','GSL','TXG',1980,'TX',1.0,'Mt / (PJ)',''); -INSERT INTO "emission_activity" VALUES('utopia','nox','GSL','TXG',1990,'TX',1.0,'Mt / (PJ)',''); -INSERT INTO "emission_activity" VALUES('utopia','nox','GSL','TXG',2000,'TX',1.0,'Mt / (PJ)',''); -INSERT INTO "emission_activity" VALUES('utopia','nox','GSL','TXG',2010,'TX',1.0,'Mt / (PJ)',''); -CREATE TABLE emission_embodied -( - region TEXT, - emis_comm TEXT - REFERENCES commodity (name), - tech TEXT - REFERENCES technology (tech), - vintage INTEGER - REFERENCES time_period (period), - value REAL, - units TEXT, - notes TEXT, - PRIMARY KEY (region, emis_comm, tech, vintage) -); -CREATE TABLE emission_end_of_life -( - region TEXT, - emis_comm TEXT - REFERENCES commodity (name), - tech TEXT - REFERENCES technology (tech), - vintage INTEGER - REFERENCES time_period (period), - value REAL, - units TEXT, - notes TEXT, - PRIMARY KEY (region, emis_comm, tech, vintage) -); -CREATE TABLE end_of_life_output -( - region TEXT, - tech TEXT - REFERENCES technology (tech), - vintage INTEGER - REFERENCES time_period (period), - output_comm TEXT - REFERENCES commodity (name), - value REAL, - units TEXT, - notes TEXT, - PRIMARY KEY (region, tech, vintage, output_comm) -); -CREATE TABLE existing_capacity -( - region TEXT, - tech TEXT - REFERENCES technology (tech), - vintage INTEGER - REFERENCES time_period (period), - capacity REAL, - units TEXT, - notes TEXT, - PRIMARY KEY (region, tech, vintage) -); -INSERT INTO "existing_capacity" VALUES('utopia','E01',1960,0.175,'GW',''); -INSERT INTO "existing_capacity" VALUES('utopia','E01',1970,0.175,'GW',''); -INSERT INTO "existing_capacity" VALUES('utopia','E01',1980,0.15,'GW',''); -INSERT INTO "existing_capacity" VALUES('utopia','E31',1980,0.1,'GW',''); -INSERT INTO "existing_capacity" VALUES('utopia','E51',1980,0.5,'GW',''); -INSERT INTO "existing_capacity" VALUES('utopia','E70',1960,0.05,'GW',''); -INSERT INTO "existing_capacity" VALUES('utopia','E70',1970,0.05,'GW',''); -INSERT INTO "existing_capacity" VALUES('utopia','E70',1980,0.2,'GW',''); -INSERT INTO "existing_capacity" VALUES('utopia','RHO',1970,12.5,'GW',''); -INSERT INTO "existing_capacity" VALUES('utopia','RHO',1980,12.5,'GW',''); -INSERT INTO "existing_capacity" VALUES('utopia','RL1',1980,5.6,'GW',''); -INSERT INTO "existing_capacity" VALUES('utopia','TXD',1970,0.4,'GW',''); -INSERT INTO "existing_capacity" VALUES('utopia','TXD',1980,0.2,'GW',''); -INSERT INTO "existing_capacity" VALUES('utopia','TXG',1970,3.1,'GW',''); -INSERT INTO "existing_capacity" VALUES('utopia','TXG',1980,1.5,'GW',''); -CREATE TABLE lifetime_process -( - region TEXT, - tech TEXT - REFERENCES technology (tech), - vintage INTEGER - REFERENCES time_period (period), - lifetime REAL, - units TEXT, - notes TEXT, - PRIMARY KEY (region, tech, vintage) -); -INSERT INTO "lifetime_process" VALUES('utopia','RL1',1980,20.0,'year','#forexistingcap'); -INSERT INTO "lifetime_process" VALUES('utopia','TXD',1970,30.0,'year','#forexistingcap'); -INSERT INTO "lifetime_process" VALUES('utopia','TXD',1980,30.0,'year','#forexistingcap'); -INSERT INTO "lifetime_process" VALUES('utopia','TXG',1970,30.0,'year','#forexistingcap'); -INSERT INTO "lifetime_process" VALUES('utopia','TXG',1980,30.0,'year','#forexistingcap'); -CREATE TABLE lifetime_survival_curve -( - region TEXT NOT NULL, - period INTEGER NOT NULL, - tech TEXT NOT NULL - REFERENCES technology (tech), - vintage INTEGER NOT NULL - REFERENCES time_period (period), - fraction REAL, - notes TEXT, - PRIMARY KEY (region, period, tech, vintage) -); -CREATE TABLE lifetime_tech -( - region TEXT, - tech TEXT - REFERENCES technology (tech), - lifetime REAL, - units TEXT, - notes TEXT, - PRIMARY KEY (region, tech) -); -INSERT INTO "lifetime_tech" VALUES('utopia','E01',40.0,'year',''); -INSERT INTO "lifetime_tech" VALUES('utopia','E21',40.0,'year',''); -INSERT INTO "lifetime_tech" VALUES('utopia','E31',100.0,'year',''); -INSERT INTO "lifetime_tech" VALUES('utopia','E51',100.0,'year',''); -INSERT INTO "lifetime_tech" VALUES('utopia','E70',40.0,'year',''); -INSERT INTO "lifetime_tech" VALUES('utopia','RHE',30.0,'year',''); -INSERT INTO "lifetime_tech" VALUES('utopia','RHO',30.0,'year',''); -INSERT INTO "lifetime_tech" VALUES('utopia','RL1',10.0,'year',''); -INSERT INTO "lifetime_tech" VALUES('utopia','SRE',50.0,'year',''); -INSERT INTO "lifetime_tech" VALUES('utopia','TXD',15.0,'year',''); -INSERT INTO "lifetime_tech" VALUES('utopia','TXE',15.0,'year',''); -INSERT INTO "lifetime_tech" VALUES('utopia','TXG',15.0,'year',''); -INSERT INTO "lifetime_tech" VALUES('utopia','IMPDSL1',1000.0,'year',''); -INSERT INTO "lifetime_tech" VALUES('utopia','IMPGSL1',1000.0,'year',''); -INSERT INTO "lifetime_tech" VALUES('utopia','IMPHCO1',1000.0,'year',''); -INSERT INTO "lifetime_tech" VALUES('utopia','IMPOIL1',1000.0,'year',''); -INSERT INTO "lifetime_tech" VALUES('utopia','IMPURN1',1000.0,'year',''); -INSERT INTO "lifetime_tech" VALUES('utopia','IMPHYD',1000.0,'year',''); -INSERT INTO "lifetime_tech" VALUES('utopia','IMPFEQ',1000.0,'year',''); -CREATE TABLE limit_activity -( - region TEXT, - period INTEGER - REFERENCES time_period (period), - tech_or_group TEXT, - operator TEXT NOT NULL DEFAULT "le" - REFERENCES operator (operator), - activity REAL, - units TEXT, - notes TEXT, - PRIMARY KEY (region, period, tech_or_group, operator) -); -CREATE TABLE limit_activity_share -( - region TEXT, - period INTEGER - REFERENCES time_period (period), - sub_group TEXT, - super_group TEXT, - operator TEXT NOT NULL DEFAULT "le" - REFERENCES operator (operator), - share REAL, - notes TEXT, - PRIMARY KEY (region, period, sub_group, super_group, operator) -); -CREATE TABLE limit_annual_capacity_factor -( - region TEXT, - tech_or_group TEXT, - vintage INTEGER - REFERENCES time_period (period), - output_comm TEXT - REFERENCES commodity (name), - operator TEXT NOT NULL DEFAULT "le" - REFERENCES operator (operator), - factor REAL, - notes TEXT, - PRIMARY KEY (region, tech_or_group, vintage, output_comm, operator), - CHECK (factor >= 0 AND factor <= 1) -); -CREATE TABLE limit_capacity -( - region TEXT, - period INTEGER - REFERENCES time_period (period), - tech_or_group TEXT, - operator TEXT NOT NULL DEFAULT "le" - REFERENCES operator (operator), - capacity REAL, - units TEXT, - notes TEXT, - PRIMARY KEY (region, period, tech_or_group, operator) -); -INSERT INTO "limit_capacity" VALUES('utopia',1990,'E31','ge',0.13,'GW',''); -INSERT INTO "limit_capacity" VALUES('utopia',2000,'E31','ge',0.13,'GW',''); -INSERT INTO "limit_capacity" VALUES('utopia',2010,'E31','ge',0.13,'GW',''); -INSERT INTO "limit_capacity" VALUES('utopia',1990,'SRE','ge',0.1,'GW',''); -INSERT INTO "limit_capacity" VALUES('utopia',1990,'E31','le',0.13,'GW',''); -INSERT INTO "limit_capacity" VALUES('utopia',2000,'E31','le',0.17,'GW',''); -INSERT INTO "limit_capacity" VALUES('utopia',2010,'E31','le',2.1e-01,'GW',''); -INSERT INTO "limit_capacity" VALUES('utopia',1990,'RHE','le',0.0,'GW',''); -INSERT INTO "limit_capacity" VALUES('utopia',1990,'TXD','le',0.6,'GW',''); -INSERT INTO "limit_capacity" VALUES('utopia',2000,'TXD','le',1.76,'GW',''); -INSERT INTO "limit_capacity" VALUES('utopia',2010,'TXD','le',4.76,'GW',''); -CREATE TABLE limit_capacity_share -( - region TEXT, - period INTEGER - REFERENCES time_period (period), - sub_group TEXT, - super_group TEXT, - operator TEXT NOT NULL DEFAULT "le" - REFERENCES operator (operator), - share REAL, - notes TEXT, - PRIMARY KEY (region, period, sub_group, super_group, operator) -); -CREATE TABLE limit_degrowth_capacity -( - region TEXT, - tech_or_group TEXT, - operator TEXT NOT NULL DEFAULT "le" - REFERENCES operator (operator), - rate REAL NOT NULL DEFAULT 0, - seed REAL NOT NULL DEFAULT 0, - seed_units TEXT, - notes TEXT, - PRIMARY KEY (region, tech_or_group, operator) -); -CREATE TABLE limit_degrowth_new_capacity -( - region TEXT, - tech_or_group TEXT, - operator TEXT NOT NULL DEFAULT "le" - REFERENCES operator (operator), - rate REAL NOT NULL DEFAULT 0, - seed REAL NOT NULL DEFAULT 0, - seed_units TEXT, - notes TEXT, - PRIMARY KEY (region, tech_or_group, operator) -); -CREATE TABLE limit_degrowth_new_capacity_delta -( - region TEXT, - tech_or_group TEXT, - operator TEXT NOT NULL DEFAULT "le" - REFERENCES operator (operator), - rate REAL NOT NULL DEFAULT 0, - seed REAL NOT NULL DEFAULT 0, - seed_units TEXT, - notes TEXT, - PRIMARY KEY (region, tech_or_group, operator) -); -CREATE TABLE limit_emission -( - region TEXT, - period INTEGER - REFERENCES time_period (period), - emis_comm TEXT - REFERENCES commodity (name), - operator TEXT NOT NULL DEFAULT "le" - REFERENCES operator (operator), - value REAL, - units TEXT, - notes TEXT, - PRIMARY KEY (region, period, emis_comm, operator) -); -CREATE TABLE limit_growth_capacity -( - region TEXT, - tech_or_group TEXT, - operator TEXT NOT NULL DEFAULT "le" - REFERENCES operator (operator), - rate REAL NOT NULL DEFAULT 0, - seed REAL NOT NULL DEFAULT 0, - seed_units TEXT, - notes TEXT, - PRIMARY KEY (region, tech_or_group, operator) -); -CREATE TABLE limit_growth_new_capacity -( - region TEXT, - tech_or_group TEXT, - operator TEXT NOT NULL DEFAULT "le" - REFERENCES operator (operator), - rate REAL NOT NULL DEFAULT 0, - seed REAL NOT NULL DEFAULT 0, - seed_units TEXT, - notes TEXT, - PRIMARY KEY (region, tech_or_group, operator) -); -CREATE TABLE limit_growth_new_capacity_delta -( - region TEXT, - tech_or_group TEXT, - operator TEXT NOT NULL DEFAULT "le" - REFERENCES operator (operator), - rate REAL NOT NULL DEFAULT 0, - seed REAL NOT NULL DEFAULT 0, - seed_units TEXT, - notes TEXT, - PRIMARY KEY (region, tech_or_group, operator) -); -CREATE TABLE limit_new_capacity -( - region TEXT, - tech_or_group TEXT, - vintage INTEGER - REFERENCES time_period (period), - operator TEXT NOT NULL DEFAULT "le" - REFERENCES operator (operator), - new_cap REAL, - units TEXT, - notes TEXT, - PRIMARY KEY (region, tech_or_group, vintage, operator) -); -CREATE TABLE limit_new_capacity_share -( - region TEXT, - sub_group TEXT, - super_group TEXT, - vintage INTEGER - REFERENCES time_period (period), - operator TEXT NOT NULL DEFAULT "le" - REFERENCES operator (operator), - share REAL, - notes TEXT, - PRIMARY KEY (region, sub_group, super_group, vintage, operator) -); -CREATE TABLE limit_resource -( - region TEXT, - tech_or_group TEXT, - operator TEXT NOT NULL DEFAULT "le" - REFERENCES operator (operator), - cum_act REAL, - units TEXT, - notes TEXT, - PRIMARY KEY (region, tech_or_group, operator) -); -CREATE TABLE limit_seasonal_capacity_factor -( - region TEXT - REFERENCES region (region), - season TEXT - REFERENCES time_season (season), - tech_or_group TEXT, - operator TEXT NOT NULL DEFAULT "le" - REFERENCES operator (operator), - factor REAL, - notes TEXT, - PRIMARY KEY(region, season, tech_or_group, operator) -); -CREATE TABLE limit_storage_level_fraction -( - region TEXT, - season TEXT, - tod TEXT - REFERENCES time_of_day (tod), - tech TEXT - REFERENCES technology (tech), - operator TEXT NOT NULL DEFAULT "le" - REFERENCES operator (operator), - fraction REAL, - notes TEXT, - CHECK (fraction >= 0 AND fraction <= 1), - PRIMARY KEY(region, season, tod, tech, operator) -); -CREATE TABLE limit_tech_input_split -( - region TEXT, - period INTEGER - REFERENCES time_period (period), - input_comm TEXT - REFERENCES commodity (name), - tech TEXT - REFERENCES technology (tech), - operator TEXT NOT NULL DEFAULT "le" - REFERENCES operator (operator), - proportion REAL, - notes TEXT, - PRIMARY KEY (region, period, input_comm, tech, operator) -); -CREATE TABLE limit_tech_input_split_annual -( - region TEXT, - period INTEGER - REFERENCES time_period (period), - input_comm TEXT - REFERENCES commodity (name), - tech TEXT - REFERENCES technology (tech), - operator TEXT NOT NULL DEFAULT "le" - REFERENCES operator (operator), - proportion REAL, - notes TEXT, - PRIMARY KEY (region, period, input_comm, tech, operator) -); -CREATE TABLE limit_tech_output_split -( - region TEXT, - period INTEGER - REFERENCES time_period (period), - tech TEXT - REFERENCES technology (tech), - output_comm TEXT - REFERENCES commodity (name), - operator TEXT NOT NULL DEFAULT "le" - REFERENCES operator (operator), - proportion REAL, - notes TEXT, - PRIMARY KEY (region, period, tech, output_comm, operator) -); -INSERT INTO "limit_tech_output_split" VALUES('utopia',1990,'SRE','DSL','ge',0.7,''); -INSERT INTO "limit_tech_output_split" VALUES('utopia',2000,'SRE','DSL','ge',0.7,''); -INSERT INTO "limit_tech_output_split" VALUES('utopia',2010,'SRE','DSL','ge',0.7,''); -INSERT INTO "limit_tech_output_split" VALUES('utopia',1990,'SRE','GSL','ge',0.3,''); -INSERT INTO "limit_tech_output_split" VALUES('utopia',2000,'SRE','GSL','ge',0.3,''); -INSERT INTO "limit_tech_output_split" VALUES('utopia',2010,'SRE','GSL','ge',0.3,''); -CREATE TABLE limit_tech_output_split_annual -( - region TEXT, - period INTEGER - REFERENCES time_period (period), - tech TEXT - REFERENCES technology (tech), - output_comm TEXT - REFERENCES commodity (name), - operator TEXT NOT NULL DEFAULT "le" - REFERENCES operator (operator), - proportion REAL, - notes TEXT, - PRIMARY KEY (region, period, tech, output_comm, operator) -); -CREATE TABLE linked_tech -( - primary_region TEXT, - primary_tech TEXT - REFERENCES technology (tech), - emis_comm TEXT - REFERENCES commodity (name), - driven_tech TEXT - REFERENCES technology (tech), - notes TEXT, - PRIMARY KEY (primary_region, primary_tech, emis_comm) -); -CREATE TABLE loan_lifetime_process -( - region TEXT, - tech TEXT - REFERENCES technology (tech), - vintage INTEGER - REFERENCES time_period (period), - lifetime REAL, - units TEXT, - notes TEXT, - PRIMARY KEY (region, tech, vintage) -); -CREATE TABLE loan_rate -( - region TEXT, - tech TEXT - REFERENCES technology (tech), - vintage INTEGER - REFERENCES time_period (period), - rate REAL, - notes TEXT, - PRIMARY KEY (region, tech, vintage) -); -CREATE TABLE metadata -( - element TEXT, - value INT, - notes TEXT, - PRIMARY KEY (element) -); -INSERT INTO "metadata" VALUES('DB_MAJOR',4,''); -INSERT INTO "metadata" VALUES('DB_MINOR',0,''); -CREATE TABLE metadata_real -( - element TEXT, - value REAL, - notes TEXT, - - PRIMARY KEY (element) -); -INSERT INTO "metadata_real" VALUES('default_loan_rate',0.05,'Default Loan Rate if not specified in loan_rate table'); -INSERT INTO "metadata_real" VALUES('global_discount_rate',0.05,''); -CREATE TABLE myopic_efficiency -( - base_year integer, - region text, - input_comm TEXT - REFERENCES commodity (name), - tech TEXT - REFERENCES technology (tech), - vintage INTEGER - REFERENCES time_period (period), - output_comm TEXT - REFERENCES commodity (name), - efficiency real, - lifetime integer, - PRIMARY KEY (region, input_comm, tech, vintage, output_comm) -); -CREATE TABLE operator -( - operator TEXT PRIMARY KEY, - notes TEXT -); -INSERT INTO "operator" VALUES('e','equal to'); -INSERT INTO "operator" VALUES('le','less than or equal to'); -INSERT INTO "operator" VALUES('ge','greater than or equal to'); -CREATE TABLE output_built_capacity -( - scenario TEXT, - region TEXT, - sector TEXT - REFERENCES sector_label (sector), - tech TEXT - REFERENCES technology (tech), - vintage INTEGER - REFERENCES time_period (period), - capacity REAL, - units TEXT, - PRIMARY KEY (region, scenario, tech, vintage) -); -CREATE TABLE output_cost -( - scenario TEXT, - region TEXT, - sector TEXT REFERENCES sector_label (sector), - period INTEGER REFERENCES time_period (period), - tech TEXT REFERENCES technology (tech), - vintage INTEGER REFERENCES time_period (period), - d_invest REAL, - d_fixed REAL, - d_var REAL, - d_emiss REAL, - invest REAL, - fixed REAL, - var REAL, - emiss REAL, - units TEXT, - PRIMARY KEY (scenario, region, period, tech, vintage), - FOREIGN KEY (vintage) REFERENCES time_period (period), - FOREIGN KEY (tech) REFERENCES technology (tech) -); -CREATE TABLE output_curtailment -( - scenario TEXT, - region TEXT, - sector TEXT, - period INTEGER - REFERENCES time_period (period), - season TEXT - REFERENCES time_season (season), - tod TEXT - REFERENCES time_of_day (tod), - input_comm TEXT - REFERENCES commodity (name), - tech TEXT - REFERENCES technology (tech), - vintage INTEGER - REFERENCES time_period (period), - output_comm TEXT - REFERENCES commodity (name), - curtailment REAL, - units TEXT, - PRIMARY KEY (region, scenario, period, season, tod, input_comm, tech, vintage, output_comm) -); -CREATE TABLE output_dual_variable -( - scenario TEXT, - constraint_name TEXT, - dual REAL, - PRIMARY KEY (constraint_name, scenario) -); -CREATE TABLE output_emission -( - scenario TEXT, - region TEXT, - sector TEXT - REFERENCES sector_label (sector), - period INTEGER - REFERENCES time_period (period), - emis_comm TEXT - REFERENCES commodity (name), - tech TEXT - REFERENCES technology (tech), - vintage INTEGER - REFERENCES time_period (period), - emission REAL, - units TEXT, - PRIMARY KEY (region, scenario, period, emis_comm, tech, vintage) -); -CREATE TABLE output_flow_in -( - scenario TEXT, - region TEXT, - sector TEXT - REFERENCES sector_label (sector), - period INTEGER - REFERENCES time_period (period), - season TEXT - REFERENCES time_season (season), - tod TEXT - REFERENCES time_of_day (tod), - input_comm TEXT - REFERENCES commodity (name), - tech TEXT - REFERENCES technology (tech), - vintage INTEGER - REFERENCES time_period (period), - output_comm TEXT - REFERENCES commodity (name), - flow REAL, - units TEXT, - PRIMARY KEY (region, scenario, period, season, tod, input_comm, tech, vintage, output_comm) -); -CREATE TABLE output_flow_out -( - scenario TEXT, - region TEXT, - sector TEXT - REFERENCES sector_label (sector), - period INTEGER - REFERENCES time_period (period), - season TEXT - REFERENCES time_season (season), - tod TEXT - REFERENCES time_of_day (tod), - input_comm TEXT - REFERENCES commodity (name), - tech TEXT - REFERENCES technology (tech), - vintage INTEGER - REFERENCES time_period (period), - output_comm TEXT - REFERENCES commodity (name), - flow REAL, - units TEXT, - PRIMARY KEY (region, scenario, period, season, tod, input_comm, tech, vintage, output_comm) -); -CREATE TABLE output_flow_out_summary -( - scenario TEXT, - region TEXT, - sector TEXT - REFERENCES sector_label (sector), - period INTEGER - REFERENCES time_period (period), - input_comm TEXT - REFERENCES commodity (name), - tech TEXT - REFERENCES technology (tech), - vintage INTEGER - REFERENCES time_period (period), - output_comm TEXT - REFERENCES commodity (name), - flow REAL, - PRIMARY KEY (scenario, region, period, input_comm, tech, vintage, output_comm) -); -CREATE TABLE output_net_capacity -( - scenario TEXT, - region TEXT, - sector TEXT - REFERENCES sector_label (sector), - period INTEGER - REFERENCES time_period (period), - tech TEXT - REFERENCES technology (tech), - vintage INTEGER - REFERENCES time_period (period), - capacity REAL, - units TEXT, - PRIMARY KEY (region, scenario, period, tech, vintage) -); -CREATE TABLE output_objective -( - scenario TEXT, - objective_name TEXT, - total_system_cost REAL -); -CREATE TABLE output_retired_capacity -( - scenario TEXT, - region TEXT, - sector TEXT - REFERENCES sector_label (sector), - period INTEGER - REFERENCES time_period (period), - tech TEXT - REFERENCES technology (tech), - vintage INTEGER - REFERENCES time_period (period), - cap_eol REAL, - cap_early REAL, - units TEXT, - PRIMARY KEY (region, scenario, period, tech, vintage) -); -CREATE TABLE output_storage_level -( - scenario TEXT, - region TEXT, - sector TEXT - REFERENCES sector_label (sector), - period INTEGER - REFERENCES time_period (period), - season TEXT, - tod TEXT - REFERENCES time_of_day (tod), - tech TEXT - REFERENCES technology (tech), - vintage INTEGER - REFERENCES time_period (period), - level REAL, - units TEXT, - PRIMARY KEY (scenario, region, period, season, tod, tech, vintage) -); -CREATE TABLE planning_reserve_margin -( - region TEXT - PRIMARY KEY - REFERENCES region (region), - margin REAL, - notes TEXT -); -CREATE TABLE ramp_down_hourly -( - region TEXT, - tech TEXT - REFERENCES technology (tech), - rate REAL, - notes TEXT, - PRIMARY KEY (region, tech) -); -CREATE TABLE ramp_up_hourly -( - region TEXT, - tech TEXT - REFERENCES technology (tech), - rate REAL, - notes TEXT, - PRIMARY KEY (region, tech) -); -CREATE TABLE region -( - region TEXT - PRIMARY KEY, - notes TEXT -); -INSERT INTO "region" VALUES('utopia',NULL); -CREATE TABLE reserve_capacity_derate -( - region TEXT, - season TEXT - REFERENCES time_season (season), - tech TEXT - REFERENCES technology (tech), - vintage INTEGER, - factor REAL, - notes TEXT, - PRIMARY KEY (region, season, tech, vintage), - CHECK (factor >= 0 AND factor <= 1) -); -CREATE TABLE rps_requirement -( - region TEXT NOT NULL - REFERENCES region (region), - period INTEGER NOT NULL - REFERENCES time_period (period), - tech_group TEXT NOT NULL - REFERENCES tech_group (group_name), - requirement REAL NOT NULL, - notes TEXT -); -CREATE TABLE sector_label -( - sector TEXT PRIMARY KEY, - notes TEXT -); -INSERT INTO "sector_label" VALUES('supply',NULL); -INSERT INTO "sector_label" VALUES('electric',NULL); -INSERT INTO "sector_label" VALUES('transport',NULL); -INSERT INTO "sector_label" VALUES('commercial',NULL); -INSERT INTO "sector_label" VALUES('residential',NULL); -INSERT INTO "sector_label" VALUES('industrial',NULL); -CREATE TABLE storage_duration -( - region TEXT, - tech TEXT, - duration REAL, - notes TEXT, - PRIMARY KEY (region, tech) -); -CREATE TABLE tech_group -( - group_name TEXT - PRIMARY KEY, - notes TEXT -); -CREATE TABLE tech_group_member -( - group_name TEXT - REFERENCES tech_group (group_name), - tech TEXT - REFERENCES technology (tech), - PRIMARY KEY (group_name, tech) -); -CREATE TABLE technology -( - tech TEXT NOT NULL PRIMARY KEY, - flag TEXT NOT NULL, - sector TEXT, - category TEXT, - sub_category TEXT, - unlim_cap INTEGER NOT NULL DEFAULT 0, - annual INTEGER NOT NULL DEFAULT 0, - reserve INTEGER NOT NULL DEFAULT 0, - curtail INTEGER NOT NULL DEFAULT 0, - retire INTEGER NOT NULL DEFAULT 0, - flex INTEGER NOT NULL DEFAULT 0, - exchange INTEGER NOT NULL DEFAULT 0, - seas_stor INTEGER NOT NULL DEFAULT 0, - description TEXT, - FOREIGN KEY (flag) REFERENCES technology_type (label) -); -INSERT INTO "technology" VALUES('IMPDSL1','p','supply','petroleum','',1,0,0,0,0,0,0,0,' imported diesel'); -INSERT INTO "technology" VALUES('IMPGSL1','p','supply','petroleum','',1,0,0,0,0,0,0,0,' imported gasoline'); -INSERT INTO "technology" VALUES('IMPHCO1','p','supply','coal','',1,0,0,0,0,0,0,0,' imported coal'); -INSERT INTO "technology" VALUES('IMPOIL1','p','supply','petroleum','',1,0,0,0,0,0,0,0,' imported crude oil'); -INSERT INTO "technology" VALUES('IMPURN1','p','supply','nuclear','',1,0,0,0,0,0,0,0,' imported uranium'); -INSERT INTO "technology" VALUES('IMPFEQ','p','supply','petroleum','',1,0,0,0,0,0,0,0,' imported fossil equivalent'); -INSERT INTO "technology" VALUES('IMPHYD','p','supply','hydro','',1,0,0,0,0,0,0,0,' imported water -- doesnt exist in Utopia'); -INSERT INTO "technology" VALUES('E01','pb','electric','coal','',0,0,0,0,0,0,0,0,' coal power plant'); -INSERT INTO "technology" VALUES('E21','pb','electric','nuclear','',0,0,0,0,0,0,0,0,' nuclear power plant'); -INSERT INTO "technology" VALUES('E31','pb','electric','hydro','',0,0,0,0,0,0,0,0,' hydro power'); -INSERT INTO "technology" VALUES('E51','ps','electric','electric','',0,0,0,0,0,0,0,0,' electric storage'); -INSERT INTO "technology" VALUES('E70','p','electric','petroleum','',0,0,0,0,0,0,0,0,' diesel power plant'); -INSERT INTO "technology" VALUES('RHE','p','residential','electric','',0,0,0,0,0,0,0,0,' electric residential heating'); -INSERT INTO "technology" VALUES('RHO','p','residential','petroleum','',0,0,0,0,0,0,0,0,' diesel residential heating'); -INSERT INTO "technology" VALUES('RL1','p','residential','electric','',0,0,0,0,0,0,0,0,' residential lighting'); -INSERT INTO "technology" VALUES('SRE','p','supply','petroleum','',0,0,0,0,0,0,0,0,' crude oil processor'); -INSERT INTO "technology" VALUES('TXD','p','transport','petroleum','',0,0,0,0,0,0,0,0,' diesel powered vehicles'); -INSERT INTO "technology" VALUES('TXE','p','transport','electric','',0,0,0,0,0,0,0,0,' electric powered vehicles'); -INSERT INTO "technology" VALUES('TXG','p','transport','petroleum','',0,0,0,0,0,0,0,0,' gasoline powered vehicles'); -CREATE TABLE technology_type -( - label TEXT - PRIMARY KEY, - description TEXT -); -INSERT INTO "technology_type" VALUES('p','production technology'); -INSERT INTO "technology_type" VALUES('pb','baseload production technology'); -INSERT INTO "technology_type" VALUES('ps','storage production technology'); -CREATE TABLE time_of_day -( - sequence INTEGER UNIQUE, - tod TEXT - PRIMARY KEY, - hours REAL NOT NULL DEFAULT 1, - notes TEXT, - CHECK (hours > 0) -); -INSERT INTO "time_of_day" (sequence, tod, hours) VALUES(1,'day',16); -INSERT INTO "time_of_day" (sequence, tod, hours) VALUES(2,'night',8); -CREATE TABLE time_period -( - sequence INTEGER UNIQUE, - period INTEGER - PRIMARY KEY, - flag TEXT - REFERENCES time_period_type (label) -); -INSERT INTO "time_period" VALUES(1,1960,'e'); -INSERT INTO "time_period" VALUES(2,1970,'e'); -INSERT INTO "time_period" VALUES(3,1980,'e'); -INSERT INTO "time_period" VALUES(4,1990,'f'); -INSERT INTO "time_period" VALUES(5,2000,'f'); -INSERT INTO "time_period" VALUES(6,2010,'f'); -INSERT INTO "time_period" VALUES(7,2020,'f'); -CREATE TABLE time_period_type -( - label TEXT - PRIMARY KEY, - description TEXT -); -INSERT INTO "time_period_type" VALUES('e','existing vintages'); -INSERT INTO "time_period_type" VALUES('f','future'); -CREATE TABLE time_season -( - sequence INTEGER UNIQUE, - season TEXT, - segment_fraction REAL NOT NULL, - notes TEXT, - PRIMARY KEY (season), - CHECK (segment_fraction >= 0 AND segment_fraction <= 1) -); -INSERT INTO "time_season" VALUES(0,'inter',0.25,NULL); -INSERT INTO "time_season" VALUES(1,'summer',0.25,NULL); -INSERT INTO "time_season" VALUES(2,'winter',0.5,NULL); -CREATE TABLE time_season_sequential -( - sequence INTEGER UNIQUE, - seas_seq TEXT, - season TEXT REFERENCES time_season(season), - segment_fraction REAL NOT NULL, - notes TEXT, - PRIMARY KEY (seas_seq), - CHECK (segment_fraction >= 0 AND segment_fraction <= 1) -); -CREATE INDEX region_tech_vintage ON myopic_efficiency (region, tech, vintage); +BEGIN TRANSACTION; +REPLACE INTO "capacity_factor_process" VALUES('utopia','inter','day','E31',2000,0.2753,''); +REPLACE INTO "capacity_factor_process" VALUES('utopia','inter','night','E31',2000,0.2753,''); +REPLACE INTO "capacity_factor_process" VALUES('utopia','winter','day','E31',2000,0.2753,''); +REPLACE INTO "capacity_factor_process" VALUES('utopia','winter','night','E31',2000,0.2753,''); +REPLACE INTO "capacity_factor_process" VALUES('utopia','summer','day','E31',2000,0.2753,''); +REPLACE INTO "capacity_factor_process" VALUES('utopia','summer','night','E31',2000,0.2753,''); +REPLACE INTO "capacity_factor_process" VALUES('utopia','inter','day','E31',2010,0.2756,''); +REPLACE INTO "capacity_factor_process" VALUES('utopia','inter','night','E31',2010,0.2756,''); +REPLACE INTO "capacity_factor_process" VALUES('utopia','winter','day','E31',2010,0.2756,''); +REPLACE INTO "capacity_factor_process" VALUES('utopia','winter','night','E31',2010,0.2756,''); +REPLACE INTO "capacity_factor_process" VALUES('utopia','summer','day','E31',2010,0.2756,''); +REPLACE INTO "capacity_factor_process" VALUES('utopia','summer','night','E31',2010,0.2756,''); +REPLACE INTO "capacity_factor_tech" VALUES('utopia','inter','day','E01',0.8,''); +REPLACE INTO "capacity_factor_tech" VALUES('utopia','inter','night','E01',0.8,''); +REPLACE INTO "capacity_factor_tech" VALUES('utopia','winter','day','E01',0.8,''); +REPLACE INTO "capacity_factor_tech" VALUES('utopia','winter','night','E01',0.8,''); +REPLACE INTO "capacity_factor_tech" VALUES('utopia','summer','day','E01',0.8,''); +REPLACE INTO "capacity_factor_tech" VALUES('utopia','summer','night','E01',0.8,''); +REPLACE INTO "capacity_factor_tech" VALUES('utopia','inter','day','E21',0.8,''); +REPLACE INTO "capacity_factor_tech" VALUES('utopia','inter','night','E21',0.8,''); +REPLACE INTO "capacity_factor_tech" VALUES('utopia','winter','day','E21',0.8,''); +REPLACE INTO "capacity_factor_tech" VALUES('utopia','winter','night','E21',0.8,''); +REPLACE INTO "capacity_factor_tech" VALUES('utopia','summer','day','E21',0.8,''); +REPLACE INTO "capacity_factor_tech" VALUES('utopia','summer','night','E21',0.8,''); +REPLACE INTO "capacity_factor_tech" VALUES('utopia','inter','day','E31',0.275,''); +REPLACE INTO "capacity_factor_tech" VALUES('utopia','inter','night','E31',0.275,''); +REPLACE INTO "capacity_factor_tech" VALUES('utopia','winter','day','E31',0.275,''); +REPLACE INTO "capacity_factor_tech" VALUES('utopia','winter','night','E31',0.275,''); +REPLACE INTO "capacity_factor_tech" VALUES('utopia','summer','day','E31',0.275,''); +REPLACE INTO "capacity_factor_tech" VALUES('utopia','summer','night','E31',0.275,''); +REPLACE INTO "capacity_factor_tech" VALUES('utopia','inter','day','E51',0.17,''); +REPLACE INTO "capacity_factor_tech" VALUES('utopia','inter','night','E51',0.17,''); +REPLACE INTO "capacity_factor_tech" VALUES('utopia','winter','day','E51',0.17,''); +REPLACE INTO "capacity_factor_tech" VALUES('utopia','winter','night','E51',0.17,''); +REPLACE INTO "capacity_factor_tech" VALUES('utopia','summer','day','E51',0.17,''); +REPLACE INTO "capacity_factor_tech" VALUES('utopia','summer','night','E51',0.17,''); +REPLACE INTO "capacity_factor_tech" VALUES('utopia','inter','day','E70',0.8,''); +REPLACE INTO "capacity_factor_tech" VALUES('utopia','inter','night','E70',0.8,''); +REPLACE INTO "capacity_factor_tech" VALUES('utopia','winter','day','E70',0.8,''); +REPLACE INTO "capacity_factor_tech" VALUES('utopia','winter','night','E70',0.8,''); +REPLACE INTO "capacity_factor_tech" VALUES('utopia','summer','day','E70',0.8,''); +REPLACE INTO "capacity_factor_tech" VALUES('utopia','summer','night','E70',0.8,''); +REPLACE INTO "capacity_to_activity" VALUES('utopia','E01',31.54,'PJ / (GW * year)',''); +REPLACE INTO "capacity_to_activity" VALUES('utopia','E21',31.54,'PJ / (GW * year)',''); +REPLACE INTO "capacity_to_activity" VALUES('utopia','E31',31.54,'PJ / (GW * year)',''); +REPLACE INTO "capacity_to_activity" VALUES('utopia','E51',31.54,'PJ / (GW * year)',''); +REPLACE INTO "capacity_to_activity" VALUES('utopia','E70',31.54,'PJ / (GW * year)',''); +REPLACE INTO "capacity_to_activity" VALUES('utopia','RHE',1.0,'PJ / (GW * year)',''); +REPLACE INTO "capacity_to_activity" VALUES('utopia','RHO',1.0,'PJ / (GW * year)',''); +REPLACE INTO "capacity_to_activity" VALUES('utopia','RL1',1.0,'PJ / (GW * year)',''); +REPLACE INTO "capacity_to_activity" VALUES('utopia','SRE',1.0,'PJ / (GW * year)',''); +REPLACE INTO "capacity_to_activity" VALUES('utopia','TXD',1.0,'PJ / (GW * year)',''); +REPLACE INTO "capacity_to_activity" VALUES('utopia','TXE',1.0,'PJ / (GW * year)',''); +REPLACE INTO "capacity_to_activity" VALUES('utopia','TXG',1.0,'PJ / (GW * year)',''); +REPLACE INTO "commodity" VALUES('ethos','s','# dummy commodity to supply inputs','PJ'); +REPLACE INTO "commodity" VALUES('DSL','p','# diesel','PJ'); +REPLACE INTO "commodity" VALUES('ELC','p','# electricity','PJ'); +REPLACE INTO "commodity" VALUES('FEQ','p','# fossil equivalent','PJ'); +REPLACE INTO "commodity" VALUES('GSL','p','# gasoline','PJ'); +REPLACE INTO "commodity" VALUES('HCO','p','# coal','PJ'); +REPLACE INTO "commodity" VALUES('HYD','p','# water','PJ'); +REPLACE INTO "commodity" VALUES('OIL','p','# crude oil','PJ'); +REPLACE INTO "commodity" VALUES('URN','p','# uranium','PJ'); +REPLACE INTO "commodity" VALUES('co2','e','#CO2 emissions','Mt'); +REPLACE INTO "commodity" VALUES('nox','e','#NOX emissions','Mt'); +REPLACE INTO "commodity" VALUES('RH','d','# residential heating','PJ'); +REPLACE INTO "commodity" VALUES('RL','d','# residential lighting','PJ'); +REPLACE INTO "commodity" VALUES('TX','d','# transportation','PJ'); +REPLACE INTO "cost_fixed" VALUES('utopia',1990,'E01',1960,40.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',1990,'E01',1970,40.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',1990,'E01',1980,40.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',1990,'E01',1990,40.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2000,'E01',1970,70.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2000,'E01',1980,70.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2000,'E01',1990,70.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2000,'E01',2000,70.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2010,'E01',1980,100.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2010,'E01',1990,100.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2010,'E01',2000,100.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2010,'E01',2010,100.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',1990,'E21',1990,500.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2000,'E21',1990,500.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2010,'E21',1990,500.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2000,'E21',2000,500.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2010,'E21',2000,500.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2010,'E21',2010,500.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',1990,'E31',1980,75.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',1990,'E31',1990,75.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2000,'E31',1980,75.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2000,'E31',1990,75.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2000,'E31',2000,75.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2010,'E31',1980,75.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2010,'E31',1990,75.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2010,'E31',2000,75.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2010,'E31',2010,75.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',1990,'E51',1980,30.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',1990,'E51',1990,30.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2000,'E51',1980,30.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2000,'E51',1990,30.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2000,'E51',2000,30.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2010,'E51',1980,30.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2010,'E51',1990,30.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2010,'E51',2000,30.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2010,'E51',2010,30.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',1990,'E70',1960,30.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',1990,'E70',1970,30.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',1990,'E70',1980,30.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',1990,'E70',1990,30.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2000,'E70',1970,30.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2000,'E70',1980,30.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2000,'E70',1990,30.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2000,'E70',2000,30.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2010,'E70',1980,30.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2010,'E70',1990,30.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2010,'E70',2000,30.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2010,'E70',2010,30.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',1990,'RHO',1970,1.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',1990,'RHO',1980,1.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',1990,'RHO',1990,1.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2000,'RHO',1980,1.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2000,'RHO',1990,1.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2000,'RHO',2000,1.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2010,'RHO',1990,1.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2010,'RHO',2000,1.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2010,'RHO',2010,1.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',1990,'RL1',1980,9.46,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',1990,'RL1',1990,9.46,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2000,'RL1',2000,9.46,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2010,'RL1',2010,9.46,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',1990,'TXD',1970,52.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',1990,'TXD',1980,52.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',1990,'TXD',1990,52.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2000,'TXD',1980,52.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2000,'TXD',1990,52.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2000,'TXD',2000,52.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2010,'TXD',2000,52.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2010,'TXD',2010,52.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',1990,'TXE',1990,100.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2000,'TXE',1990,90.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2000,'TXE',2000,90.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2010,'TXE',2000,80.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2010,'TXE',2010,80.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',1990,'TXG',1970,48.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',1990,'TXG',1980,48.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',1990,'TXG',1990,48.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2000,'TXG',1980,48.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2000,'TXG',1990,48.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2000,'TXG',2000,48.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2010,'TXG',2000,48.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_fixed" VALUES('utopia',2010,'TXG',2010,48.0,'Mdollar / (PJ^2 / GW / year)',''); +REPLACE INTO "cost_invest" VALUES('utopia','E01',1990,2000.0,'Mdollar / (PJ^2 / GW)',''); +REPLACE INTO "cost_invest" VALUES('utopia','E01',2000,1300.0,'Mdollar / (PJ^2 / GW)',''); +REPLACE INTO "cost_invest" VALUES('utopia','E01',2010,1200.0,'Mdollar / (PJ^2 / GW)',''); +REPLACE INTO "cost_invest" VALUES('utopia','E21',1990,5000.0,'Mdollar / (PJ^2 / GW)',''); +REPLACE INTO "cost_invest" VALUES('utopia','E21',2000,5000.0,'Mdollar / (PJ^2 / GW)',''); +REPLACE INTO "cost_invest" VALUES('utopia','E21',2010,5000.0,'Mdollar / (PJ^2 / GW)',''); +REPLACE INTO "cost_invest" VALUES('utopia','E31',1990,3000.0,'Mdollar / (PJ^2 / GW)',''); +REPLACE INTO "cost_invest" VALUES('utopia','E31',2000,3000.0,'Mdollar / (PJ^2 / GW)',''); +REPLACE INTO "cost_invest" VALUES('utopia','E31',2010,3000.0,'Mdollar / (PJ^2 / GW)',''); +REPLACE INTO "cost_invest" VALUES('utopia','E51',1990,900.0,'Mdollar / (PJ^2 / GW)',''); +REPLACE INTO "cost_invest" VALUES('utopia','E51',2000,900.0,'Mdollar / (PJ^2 / GW)',''); +REPLACE INTO "cost_invest" VALUES('utopia','E51',2010,900.0,'Mdollar / (PJ^2 / GW)',''); +REPLACE INTO "cost_invest" VALUES('utopia','E70',1990,1000.0,'Mdollar / (PJ^2 / GW)',''); +REPLACE INTO "cost_invest" VALUES('utopia','E70',2000,1000.0,'Mdollar / (PJ^2 / GW)',''); +REPLACE INTO "cost_invest" VALUES('utopia','E70',2010,1000.0,'Mdollar / (PJ^2 / GW)',''); +REPLACE INTO "cost_invest" VALUES('utopia','RHE',1990,90.0,'Mdollar / (PJ^2 / GW)',''); +REPLACE INTO "cost_invest" VALUES('utopia','RHE',2000,90.0,'Mdollar / (PJ^2 / GW)',''); +REPLACE INTO "cost_invest" VALUES('utopia','RHE',2010,90.0,'Mdollar / (PJ^2 / GW)',''); +REPLACE INTO "cost_invest" VALUES('utopia','RHO',1990,100.0,'Mdollar / (PJ^2 / GW)',''); +REPLACE INTO "cost_invest" VALUES('utopia','RHO',2000,100.0,'Mdollar / (PJ^2 / GW)',''); +REPLACE INTO "cost_invest" VALUES('utopia','RHO',2010,100.0,'Mdollar / (PJ^2 / GW)',''); +REPLACE INTO "cost_invest" VALUES('utopia','SRE',1990,100.0,'Mdollar / (PJ^2 / GW)',''); +REPLACE INTO "cost_invest" VALUES('utopia','SRE',2000,100.0,'Mdollar / (PJ^2 / GW)',''); +REPLACE INTO "cost_invest" VALUES('utopia','SRE',2010,100.0,'Mdollar / (PJ^2 / GW)',''); +REPLACE INTO "cost_invest" VALUES('utopia','TXD',1990,1044.0,'Mdollar / (PJ^2 / GW)',''); +REPLACE INTO "cost_invest" VALUES('utopia','TXD',2000,1044.0,'Mdollar / (PJ^2 / GW)',''); +REPLACE INTO "cost_invest" VALUES('utopia','TXD',2010,1044.0,'Mdollar / (PJ^2 / GW)',''); +REPLACE INTO "cost_invest" VALUES('utopia','TXE',1990,2000.0,'Mdollar / (PJ^2 / GW)',''); +REPLACE INTO "cost_invest" VALUES('utopia','TXE',2000,1750.0,'Mdollar / (PJ^2 / GW)',''); +REPLACE INTO "cost_invest" VALUES('utopia','TXE',2010,1500.0,'Mdollar / (PJ^2 / GW)',''); +REPLACE INTO "cost_invest" VALUES('utopia','TXG',1990,1044.0,'Mdollar / (PJ^2 / GW)',''); +REPLACE INTO "cost_invest" VALUES('utopia','TXG',2000,1044.0,'Mdollar / (PJ^2 / GW)',''); +REPLACE INTO "cost_invest" VALUES('utopia','TXG',2010,1044.0,'Mdollar / (PJ^2 / GW)',''); +REPLACE INTO "cost_variable" VALUES('utopia',1990,'IMPDSL1',1990,10.0,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2000,'IMPDSL1',1990,10.0,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2010,'IMPDSL1',1990,10.0,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',1990,'IMPGSL1',1990,15.0,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2000,'IMPGSL1',1990,15.0,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2010,'IMPGSL1',1990,15.0,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',1990,'IMPHCO1',1990,2.0,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2000,'IMPHCO1',1990,2.0,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2010,'IMPHCO1',1990,2.0,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',1990,'IMPOIL1',1990,8.0,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2000,'IMPOIL1',1990,8.0,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2010,'IMPOIL1',1990,8.0,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',1990,'IMPURN1',1990,2.0,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2000,'IMPURN1',1990,2.0,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2010,'IMPURN1',1990,2.0,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',1990,'E01',1960,0.3,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',1990,'E01',1970,0.3,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',1990,'E01',1980,0.3,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',1990,'E01',1990,0.3,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2000,'E01',1970,0.3,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2000,'E01',1980,0.3,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2000,'E01',1990,0.3,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2000,'E01',2000,0.3,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2010,'E01',1980,0.3,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2010,'E01',1990,0.3,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2010,'E01',2000,0.3,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2010,'E01',2010,0.3,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',1990,'E21',1990,1.5,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2000,'E21',1990,1.5,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2010,'E21',1990,1.5,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2000,'E21',2000,1.5,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2010,'E21',2000,1.5,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2010,'E21',2010,1.5,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',1990,'E70',1960,0.4,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',1990,'E70',1970,0.4,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',1990,'E70',1980,0.4,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',1990,'E70',1990,0.4,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2000,'E70',1970,0.4,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2000,'E70',1980,0.4,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2000,'E70',1990,0.4,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2000,'E70',2000,0.4,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2010,'E70',1980,0.4,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2010,'E70',1990,0.4,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2010,'E70',2000,0.4,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2010,'E70',2010,0.4,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',1990,'SRE',1990,10.0,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2000,'SRE',1990,10.0,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2000,'SRE',2000,10.0,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2010,'SRE',1990,10.0,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2010,'SRE',2000,10.0,'Mdollar / (PJ)',''); +REPLACE INTO "cost_variable" VALUES('utopia',2010,'SRE',2010,10.0,'Mdollar / (PJ)',''); +REPLACE INTO "demand" VALUES('utopia',1990,'RH',25.2,'PJ',''); +REPLACE INTO "demand" VALUES('utopia',2000,'RH',37.8,'PJ',''); +REPLACE INTO "demand" VALUES('utopia',2010,'RH',56.7,'PJ',''); +REPLACE INTO "demand" VALUES('utopia',1990,'RL',5.6,'PJ',''); +REPLACE INTO "demand" VALUES('utopia',2000,'RL',8.4,'PJ',''); +REPLACE INTO "demand" VALUES('utopia',2010,'RL',12.6,'PJ',''); +REPLACE INTO "demand" VALUES('utopia',1990,'TX',5.2,'PJ',''); +REPLACE INTO "demand" VALUES('utopia',2000,'TX',7.8,'PJ',''); +REPLACE INTO "demand" VALUES('utopia',2010,'TX',11.69,'PJ',''); +REPLACE INTO "demand_specific_distribution" VALUES('utopia',1990,'inter','day','RH',0.12,''); +REPLACE INTO "demand_specific_distribution" VALUES('utopia',1990,'inter','night','RH',0.06,''); +REPLACE INTO "demand_specific_distribution" VALUES('utopia',1990,'winter','day','RH',0.5467,''); +REPLACE INTO "demand_specific_distribution" VALUES('utopia',1990,'winter','night','RH',0.2733,''); +REPLACE INTO "demand_specific_distribution" VALUES('utopia',1990,'inter','day','RL',0.15,''); +REPLACE INTO "demand_specific_distribution" VALUES('utopia',1990,'inter','night','RL',0.05,''); +REPLACE INTO "demand_specific_distribution" VALUES('utopia',1990,'summer','day','RL',0.15,''); +REPLACE INTO "demand_specific_distribution" VALUES('utopia',1990,'summer','night','RL',0.05,''); +REPLACE INTO "demand_specific_distribution" VALUES('utopia',1990,'winter','day','RL',0.5,''); +REPLACE INTO "demand_specific_distribution" VALUES('utopia',1990,'winter','night','RL',0.1,''); +REPLACE INTO "demand_specific_distribution" VALUES('utopia',2000,'inter','day','RH',0.12,''); +REPLACE INTO "demand_specific_distribution" VALUES('utopia',2000,'inter','night','RH',0.06,''); +REPLACE INTO "demand_specific_distribution" VALUES('utopia',2000,'winter','day','RH',0.5467,''); +REPLACE INTO "demand_specific_distribution" VALUES('utopia',2000,'winter','night','RH',0.2733,''); +REPLACE INTO "demand_specific_distribution" VALUES('utopia',2000,'inter','day','RL',0.15,''); +REPLACE INTO "demand_specific_distribution" VALUES('utopia',2000,'inter','night','RL',0.05,''); +REPLACE INTO "demand_specific_distribution" VALUES('utopia',2000,'summer','day','RL',0.15,''); +REPLACE INTO "demand_specific_distribution" VALUES('utopia',2000,'summer','night','RL',0.05,''); +REPLACE INTO "demand_specific_distribution" VALUES('utopia',2000,'winter','day','RL',0.5,''); +REPLACE INTO "demand_specific_distribution" VALUES('utopia',2000,'winter','night','RL',0.1,''); +REPLACE INTO "demand_specific_distribution" VALUES('utopia',2010,'inter','day','RH',0.12,''); +REPLACE INTO "demand_specific_distribution" VALUES('utopia',2010,'inter','night','RH',0.06,''); +REPLACE INTO "demand_specific_distribution" VALUES('utopia',2010,'winter','day','RH',0.5467,''); +REPLACE INTO "demand_specific_distribution" VALUES('utopia',2010,'winter','night','RH',0.2733,''); +REPLACE INTO "demand_specific_distribution" VALUES('utopia',2010,'inter','day','RL',0.15,''); +REPLACE INTO "demand_specific_distribution" VALUES('utopia',2010,'inter','night','RL',0.05,''); +REPLACE INTO "demand_specific_distribution" VALUES('utopia',2010,'summer','day','RL',0.15,''); +REPLACE INTO "demand_specific_distribution" VALUES('utopia',2010,'summer','night','RL',0.05,''); +REPLACE INTO "demand_specific_distribution" VALUES('utopia',2010,'winter','day','RL',0.5,''); +REPLACE INTO "demand_specific_distribution" VALUES('utopia',2010,'winter','night','RL',0.1,''); +REPLACE INTO "efficiency" VALUES('utopia','ethos','IMPDSL1',1990,'DSL',1.0,'PJ / (PJ)',''); +REPLACE INTO "efficiency" VALUES('utopia','ethos','IMPGSL1',1990,'GSL',1.0,'PJ / (PJ)',''); +REPLACE INTO "efficiency" VALUES('utopia','ethos','IMPHCO1',1990,'HCO',1.0,'PJ / (PJ)',''); +REPLACE INTO "efficiency" VALUES('utopia','ethos','IMPOIL1',1990,'OIL',1.0,'PJ / (PJ)',''); +REPLACE INTO "efficiency" VALUES('utopia','ethos','IMPURN1',1990,'URN',1.0,'PJ / (PJ)',''); +REPLACE INTO "efficiency" VALUES('utopia','ethos','IMPFEQ',1990,'FEQ',1.0,'PJ / (PJ)',''); +REPLACE INTO "efficiency" VALUES('utopia','ethos','IMPHYD',1990,'HYD',1.0,'PJ / (PJ)',''); +REPLACE INTO "efficiency" VALUES('utopia','HCO','E01',1960,'ELC',0.32,'PJ / (PJ)','# 1/3.125'); +REPLACE INTO "efficiency" VALUES('utopia','HCO','E01',1970,'ELC',0.32,'PJ / (PJ)','# 1/3.125'); +REPLACE INTO "efficiency" VALUES('utopia','HCO','E01',1980,'ELC',0.32,'PJ / (PJ)','# 1/3.125'); +REPLACE INTO "efficiency" VALUES('utopia','HCO','E01',1990,'ELC',0.32,'PJ / (PJ)','# 1/3.125'); +REPLACE INTO "efficiency" VALUES('utopia','HCO','E01',2000,'ELC',0.32,'PJ / (PJ)','# 1/3.125'); +REPLACE INTO "efficiency" VALUES('utopia','HCO','E01',2010,'ELC',0.32,'PJ / (PJ)','# 1/3.125'); +REPLACE INTO "efficiency" VALUES('utopia','FEQ','E21',1990,'ELC',0.32,'PJ / (PJ)','# 1/3.125'); +REPLACE INTO "efficiency" VALUES('utopia','FEQ','E21',2000,'ELC',0.32,'PJ / (PJ)','# 1/3.125'); +REPLACE INTO "efficiency" VALUES('utopia','FEQ','E21',2010,'ELC',0.32,'PJ / (PJ)','# 1/3.125'); +REPLACE INTO "efficiency" VALUES('utopia','URN','E21',1990,'ELC',0.4,'PJ / (PJ)','# 1/2.5'); +REPLACE INTO "efficiency" VALUES('utopia','URN','E21',2000,'ELC',0.4,'PJ / (PJ)','# 1/2.5'); +REPLACE INTO "efficiency" VALUES('utopia','URN','E21',2010,'ELC',0.4,'PJ / (PJ)','# 1/2.5'); +REPLACE INTO "efficiency" VALUES('utopia','HYD','E31',1980,'ELC',0.32,'PJ / (PJ)','# 1/3.125'); +REPLACE INTO "efficiency" VALUES('utopia','HYD','E31',1990,'ELC',0.32,'PJ / (PJ)','# 1/3.125'); +REPLACE INTO "efficiency" VALUES('utopia','HYD','E31',2000,'ELC',0.32,'PJ / (PJ)','# 1/3.125'); +REPLACE INTO "efficiency" VALUES('utopia','HYD','E31',2010,'ELC',0.32,'PJ / (PJ)','# 1/3.125'); +REPLACE INTO "efficiency" VALUES('utopia','DSL','E70',1960,'ELC',0.294,'PJ / (PJ)','# 1/3.4'); +REPLACE INTO "efficiency" VALUES('utopia','DSL','E70',1970,'ELC',0.294,'PJ / (PJ)','# 1/3.4'); +REPLACE INTO "efficiency" VALUES('utopia','DSL','E70',1980,'ELC',0.294,'PJ / (PJ)','# 1/3.4'); +REPLACE INTO "efficiency" VALUES('utopia','DSL','E70',1990,'ELC',0.294,'PJ / (PJ)','# 1/3.4'); +REPLACE INTO "efficiency" VALUES('utopia','DSL','E70',2000,'ELC',0.294,'PJ / (PJ)','# 1/3.4'); +REPLACE INTO "efficiency" VALUES('utopia','DSL','E70',2010,'ELC',0.294,'PJ / (PJ)','# 1/3.4'); +REPLACE INTO "efficiency" VALUES('utopia','ELC','E51',1980,'ELC',0.72,'PJ / (PJ)','# 1/1.3889'); +REPLACE INTO "efficiency" VALUES('utopia','ELC','E51',1990,'ELC',0.72,'PJ / (PJ)','# 1/1.3889'); +REPLACE INTO "efficiency" VALUES('utopia','ELC','E51',2000,'ELC',0.72,'PJ / (PJ)','# 1/1.3889'); +REPLACE INTO "efficiency" VALUES('utopia','ELC','E51',2010,'ELC',0.72,'PJ / (PJ)','# 1/1.3889'); +REPLACE INTO "efficiency" VALUES('utopia','ELC','RHE',1990,'RH',1.0,'PJ / (PJ)','# direct translation from DMD_EFF'); +REPLACE INTO "efficiency" VALUES('utopia','ELC','RHE',2000,'RH',1.0,'PJ / (PJ)','# direct translation from DMD_EFF'); +REPLACE INTO "efficiency" VALUES('utopia','ELC','RHE',2010,'RH',1.0,'PJ / (PJ)','# direct translation from DMD_EFF'); +REPLACE INTO "efficiency" VALUES('utopia','DSL','RHO',1970,'RH',0.7,'PJ / (PJ)','# direct translation from DMD_EFF'); +REPLACE INTO "efficiency" VALUES('utopia','DSL','RHO',1980,'RH',0.7,'PJ / (PJ)','# direct translation from DMD_EFF'); +REPLACE INTO "efficiency" VALUES('utopia','DSL','RHO',1990,'RH',0.7,'PJ / (PJ)','# direct translation from DMD_EFF'); +REPLACE INTO "efficiency" VALUES('utopia','DSL','RHO',2000,'RH',0.7,'PJ / (PJ)','# direct translation from DMD_EFF'); +REPLACE INTO "efficiency" VALUES('utopia','DSL','RHO',2010,'RH',0.7,'PJ / (PJ)','# direct translation from DMD_EFF'); +REPLACE INTO "efficiency" VALUES('utopia','ELC','RL1',1980,'RL',1.0,'PJ / (PJ)','# direct translation from DMD_EFF'); +REPLACE INTO "efficiency" VALUES('utopia','ELC','RL1',1990,'RL',1.0,'PJ / (PJ)','# direct translation from DMD_EFF'); +REPLACE INTO "efficiency" VALUES('utopia','ELC','RL1',2000,'RL',1.0,'PJ / (PJ)','# direct translation from DMD_EFF'); +REPLACE INTO "efficiency" VALUES('utopia','ELC','RL1',2010,'RL',1.0,'PJ / (PJ)','# direct translation from DMD_EFF'); +REPLACE INTO "efficiency" VALUES('utopia','OIL','SRE',1990,'DSL',1.0,'PJ / (PJ)','# direct translation from PRC_INP2, PRC_OUT'); +REPLACE INTO "efficiency" VALUES('utopia','OIL','SRE',2000,'DSL',1.0,'PJ / (PJ)','# direct translation from PRC_INP2, PRC_OUT'); +REPLACE INTO "efficiency" VALUES('utopia','OIL','SRE',2010,'DSL',1.0,'PJ / (PJ)','# direct translation from PRC_INP2, PRC_OUT'); +REPLACE INTO "efficiency" VALUES('utopia','OIL','SRE',1990,'GSL',1.0,'PJ / (PJ)','# direct translation from PRC_INP2, PRC_OUT'); +REPLACE INTO "efficiency" VALUES('utopia','OIL','SRE',2000,'GSL',1.0,'PJ / (PJ)','# direct translation from PRC_INP2, PRC_OUT'); +REPLACE INTO "efficiency" VALUES('utopia','OIL','SRE',2010,'GSL',1.0,'PJ / (PJ)','# direct translation from PRC_INP2, PRC_OUT'); +REPLACE INTO "efficiency" VALUES('utopia','DSL','TXD',1970,'TX',0.231,'PJ / (PJ)','# direct translation from DMD_EFF'); +REPLACE INTO "efficiency" VALUES('utopia','DSL','TXD',1980,'TX',0.231,'PJ / (PJ)','# direct translation from DMD_EFF'); +REPLACE INTO "efficiency" VALUES('utopia','DSL','TXD',1990,'TX',0.231,'PJ / (PJ)','# direct translation from DMD_EFF'); +REPLACE INTO "efficiency" VALUES('utopia','DSL','TXD',2000,'TX',0.231,'PJ / (PJ)','# direct translation from DMD_EFF'); +REPLACE INTO "efficiency" VALUES('utopia','DSL','TXD',2010,'TX',0.231,'PJ / (PJ)','# direct translation from DMD_EFF'); +REPLACE INTO "efficiency" VALUES('utopia','ELC','TXE',1990,'TX',0.827,'PJ / (PJ)','# direct translation from DMD_EFF'); +REPLACE INTO "efficiency" VALUES('utopia','ELC','TXE',2000,'TX',0.827,'PJ / (PJ)','# direct translation from DMD_EFF'); +REPLACE INTO "efficiency" VALUES('utopia','ELC','TXE',2010,'TX',0.827,'PJ / (PJ)','# direct translation from DMD_EFF'); +REPLACE INTO "efficiency" VALUES('utopia','GSL','TXG',1970,'TX',0.231,'PJ / (PJ)','# direct translation from DMD_EFF'); +REPLACE INTO "efficiency" VALUES('utopia','GSL','TXG',1980,'TX',0.231,'PJ / (PJ)','# direct translation from DMD_EFF'); +REPLACE INTO "efficiency" VALUES('utopia','GSL','TXG',1990,'TX',0.231,'PJ / (PJ)','# direct translation from DMD_EFF'); +REPLACE INTO "efficiency" VALUES('utopia','GSL','TXG',2000,'TX',0.231,'PJ / (PJ)','# direct translation from DMD_EFF'); +REPLACE INTO "efficiency" VALUES('utopia','GSL','TXG',2010,'TX',0.231,'PJ / (PJ)','# direct translation from DMD_EFF'); +REPLACE INTO "emission_activity" VALUES('utopia','co2','ethos','IMPDSL1',1990,'DSL',0.075,'Mt / (PJ)',''); +REPLACE INTO "emission_activity" VALUES('utopia','co2','ethos','IMPGSL1',1990,'GSL',0.075,'Mt / (PJ)',''); +REPLACE INTO "emission_activity" VALUES('utopia','co2','ethos','IMPHCO1',1990,'HCO',8.9e-02,'Mt / (PJ)',''); +REPLACE INTO "emission_activity" VALUES('utopia','co2','ethos','IMPOIL1',1990,'OIL',0.075,'Mt / (PJ)',''); +REPLACE INTO "emission_activity" VALUES('utopia','nox','DSL','TXD',1970,'TX',1.0,'Mt / (PJ)',''); +REPLACE INTO "emission_activity" VALUES('utopia','nox','DSL','TXD',1980,'TX',1.0,'Mt / (PJ)',''); +REPLACE INTO "emission_activity" VALUES('utopia','nox','DSL','TXD',1990,'TX',1.0,'Mt / (PJ)',''); +REPLACE INTO "emission_activity" VALUES('utopia','nox','DSL','TXD',2000,'TX',1.0,'Mt / (PJ)',''); +REPLACE INTO "emission_activity" VALUES('utopia','nox','DSL','TXD',2010,'TX',1.0,'Mt / (PJ)',''); +REPLACE INTO "emission_activity" VALUES('utopia','nox','GSL','TXG',1970,'TX',1.0,'Mt / (PJ)',''); +REPLACE INTO "emission_activity" VALUES('utopia','nox','GSL','TXG',1980,'TX',1.0,'Mt / (PJ)',''); +REPLACE INTO "emission_activity" VALUES('utopia','nox','GSL','TXG',1990,'TX',1.0,'Mt / (PJ)',''); +REPLACE INTO "emission_activity" VALUES('utopia','nox','GSL','TXG',2000,'TX',1.0,'Mt / (PJ)',''); +REPLACE INTO "emission_activity" VALUES('utopia','nox','GSL','TXG',2010,'TX',1.0,'Mt / (PJ)',''); +REPLACE INTO "existing_capacity" VALUES('utopia','E01',1960,0.175,'GW',''); +REPLACE INTO "existing_capacity" VALUES('utopia','E01',1970,0.175,'GW',''); +REPLACE INTO "existing_capacity" VALUES('utopia','E01',1980,0.15,'GW',''); +REPLACE INTO "existing_capacity" VALUES('utopia','E31',1980,0.1,'GW',''); +REPLACE INTO "existing_capacity" VALUES('utopia','E51',1980,0.5,'GW',''); +REPLACE INTO "existing_capacity" VALUES('utopia','E70',1960,0.05,'GW',''); +REPLACE INTO "existing_capacity" VALUES('utopia','E70',1970,0.05,'GW',''); +REPLACE INTO "existing_capacity" VALUES('utopia','E70',1980,0.2,'GW',''); +REPLACE INTO "existing_capacity" VALUES('utopia','RHO',1970,12.5,'GW',''); +REPLACE INTO "existing_capacity" VALUES('utopia','RHO',1980,12.5,'GW',''); +REPLACE INTO "existing_capacity" VALUES('utopia','RL1',1980,5.6,'GW',''); +REPLACE INTO "existing_capacity" VALUES('utopia','TXD',1970,0.4,'GW',''); +REPLACE INTO "existing_capacity" VALUES('utopia','TXD',1980,0.2,'GW',''); +REPLACE INTO "existing_capacity" VALUES('utopia','TXG',1970,3.1,'GW',''); +REPLACE INTO "existing_capacity" VALUES('utopia','TXG',1980,1.5,'GW',''); +REPLACE INTO "lifetime_process" VALUES('utopia','RL1',1980,20.0,'year','#forexistingcap'); +REPLACE INTO "lifetime_process" VALUES('utopia','TXD',1970,30.0,'year','#forexistingcap'); +REPLACE INTO "lifetime_process" VALUES('utopia','TXD',1980,30.0,'year','#forexistingcap'); +REPLACE INTO "lifetime_process" VALUES('utopia','TXG',1970,30.0,'year','#forexistingcap'); +REPLACE INTO "lifetime_process" VALUES('utopia','TXG',1980,30.0,'year','#forexistingcap'); +REPLACE INTO "lifetime_tech" VALUES('utopia','E01',40.0,'year',''); +REPLACE INTO "lifetime_tech" VALUES('utopia','E21',40.0,'year',''); +REPLACE INTO "lifetime_tech" VALUES('utopia','E31',100.0,'year',''); +REPLACE INTO "lifetime_tech" VALUES('utopia','E51',100.0,'year',''); +REPLACE INTO "lifetime_tech" VALUES('utopia','E70',40.0,'year',''); +REPLACE INTO "lifetime_tech" VALUES('utopia','RHE',30.0,'year',''); +REPLACE INTO "lifetime_tech" VALUES('utopia','RHO',30.0,'year',''); +REPLACE INTO "lifetime_tech" VALUES('utopia','RL1',10.0,'year',''); +REPLACE INTO "lifetime_tech" VALUES('utopia','SRE',50.0,'year',''); +REPLACE INTO "lifetime_tech" VALUES('utopia','TXD',15.0,'year',''); +REPLACE INTO "lifetime_tech" VALUES('utopia','TXE',15.0,'year',''); +REPLACE INTO "lifetime_tech" VALUES('utopia','TXG',15.0,'year',''); +REPLACE INTO "lifetime_tech" VALUES('utopia','IMPDSL1',1000.0,'year',''); +REPLACE INTO "lifetime_tech" VALUES('utopia','IMPGSL1',1000.0,'year',''); +REPLACE INTO "lifetime_tech" VALUES('utopia','IMPHCO1',1000.0,'year',''); +REPLACE INTO "lifetime_tech" VALUES('utopia','IMPOIL1',1000.0,'year',''); +REPLACE INTO "lifetime_tech" VALUES('utopia','IMPURN1',1000.0,'year',''); +REPLACE INTO "lifetime_tech" VALUES('utopia','IMPHYD',1000.0,'year',''); +REPLACE INTO "lifetime_tech" VALUES('utopia','IMPFEQ',1000.0,'year',''); +REPLACE INTO "limit_capacity" VALUES('utopia',1990,'E31','ge',0.13,'GW',''); +REPLACE INTO "limit_capacity" VALUES('utopia',2000,'E31','ge',0.13,'GW',''); +REPLACE INTO "limit_capacity" VALUES('utopia',2010,'E31','ge',0.13,'GW',''); +REPLACE INTO "limit_capacity" VALUES('utopia',1990,'SRE','ge',0.1,'GW',''); +REPLACE INTO "limit_capacity" VALUES('utopia',1990,'E31','le',0.13,'GW',''); +REPLACE INTO "limit_capacity" VALUES('utopia',2000,'E31','le',0.17,'GW',''); +REPLACE INTO "limit_capacity" VALUES('utopia',2010,'E31','le',2.1e-01,'GW',''); +REPLACE INTO "limit_capacity" VALUES('utopia',1990,'RHE','le',0.0,'GW',''); +REPLACE INTO "limit_capacity" VALUES('utopia',1990,'TXD','le',0.6,'GW',''); +REPLACE INTO "limit_capacity" VALUES('utopia',2000,'TXD','le',1.76,'GW',''); +REPLACE INTO "limit_capacity" VALUES('utopia',2010,'TXD','le',4.76,'GW',''); +REPLACE INTO "limit_tech_output_split" VALUES('utopia',1990,'SRE','DSL','ge',0.7,''); +REPLACE INTO "limit_tech_output_split" VALUES('utopia',2000,'SRE','DSL','ge',0.7,''); +REPLACE INTO "limit_tech_output_split" VALUES('utopia',2010,'SRE','DSL','ge',0.7,''); +REPLACE INTO "limit_tech_output_split" VALUES('utopia',1990,'SRE','GSL','ge',0.3,''); +REPLACE INTO "limit_tech_output_split" VALUES('utopia',2000,'SRE','GSL','ge',0.3,''); +REPLACE INTO "limit_tech_output_split" VALUES('utopia',2010,'SRE','GSL','ge',0.3,''); +REPLACE INTO "metadata_real" VALUES('default_loan_rate',0.05,'Default Loan Rate if not specified in loan_rate table'); +REPLACE INTO "metadata_real" VALUES('global_discount_rate',0.05,''); +REPLACE INTO "region" VALUES('utopia',NULL); +REPLACE INTO "sector_label" VALUES('supply',NULL); +REPLACE INTO "sector_label" VALUES('electric',NULL); +REPLACE INTO "sector_label" VALUES('transport',NULL); +REPLACE INTO "sector_label" VALUES('commercial',NULL); +REPLACE INTO "sector_label" VALUES('residential',NULL); +REPLACE INTO "sector_label" VALUES('industrial',NULL); +REPLACE INTO "technology" VALUES('IMPDSL1','p','supply','petroleum','',1,0,0,0,0,0,0,0,' imported diesel'); +REPLACE INTO "technology" VALUES('IMPGSL1','p','supply','petroleum','',1,0,0,0,0,0,0,0,' imported gasoline'); +REPLACE INTO "technology" VALUES('IMPHCO1','p','supply','coal','',1,0,0,0,0,0,0,0,' imported coal'); +REPLACE INTO "technology" VALUES('IMPOIL1','p','supply','petroleum','',1,0,0,0,0,0,0,0,' imported crude oil'); +REPLACE INTO "technology" VALUES('IMPURN1','p','supply','nuclear','',1,0,0,0,0,0,0,0,' imported uranium'); +REPLACE INTO "technology" VALUES('IMPFEQ','p','supply','petroleum','',1,0,0,0,0,0,0,0,' imported fossil equivalent'); +REPLACE INTO "technology" VALUES('IMPHYD','p','supply','hydro','',1,0,0,0,0,0,0,0,' imported water -- doesnt exist in Utopia'); +REPLACE INTO "technology" VALUES('E01','pb','electric','coal','',0,0,0,0,0,0,0,0,' coal power plant'); +REPLACE INTO "technology" VALUES('E21','pb','electric','nuclear','',0,0,0,0,0,0,0,0,' nuclear power plant'); +REPLACE INTO "technology" VALUES('E31','pb','electric','hydro','',0,0,0,0,0,0,0,0,' hydro power'); +REPLACE INTO "technology" VALUES('E51','ps','electric','electric','',0,0,0,0,0,0,0,0,' electric storage'); +REPLACE INTO "technology" VALUES('E70','p','electric','petroleum','',0,0,0,0,0,0,0,0,' diesel power plant'); +REPLACE INTO "technology" VALUES('RHE','p','residential','electric','',0,0,0,0,0,0,0,0,' electric residential heating'); +REPLACE INTO "technology" VALUES('RHO','p','residential','petroleum','',0,0,0,0,0,0,0,0,' diesel residential heating'); +REPLACE INTO "technology" VALUES('RL1','p','residential','electric','',0,0,0,0,0,0,0,0,' residential lighting'); +REPLACE INTO "technology" VALUES('SRE','p','supply','petroleum','',0,0,0,0,0,0,0,0,' crude oil processor'); +REPLACE INTO "technology" VALUES('TXD','p','transport','petroleum','',0,0,0,0,0,0,0,0,' diesel powered vehicles'); +REPLACE INTO "technology" VALUES('TXE','p','transport','electric','',0,0,0,0,0,0,0,0,' electric powered vehicles'); +REPLACE INTO "technology" VALUES('TXG','p','transport','petroleum','',0,0,0,0,0,0,0,0,' gasoline powered vehicles'); +REPLACE INTO "time_of_day" (sequence, tod, hours) VALUES(1,'day',16); +REPLACE INTO "time_of_day" (sequence, tod, hours) VALUES(2,'night',8); +REPLACE INTO "time_period" VALUES(1,1960,'e'); +REPLACE INTO "time_period" VALUES(2,1970,'e'); +REPLACE INTO "time_period" VALUES(3,1980,'e'); +REPLACE INTO "time_period" VALUES(4,1990,'f'); +REPLACE INTO "time_period" VALUES(5,2000,'f'); +REPLACE INTO "time_period" VALUES(6,2010,'f'); +REPLACE INTO "time_period" VALUES(7,2020,'f'); +REPLACE INTO "time_season" VALUES(0,'inter',0.25,NULL); +REPLACE INTO "time_season" VALUES(1,'summer',0.25,NULL); +REPLACE INTO "time_season" VALUES(2,'winter',0.5,NULL); COMMIT; diff --git a/tests/test_cli.py b/tests/test_cli.py index 19144fd4..5ba1fa1e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -12,7 +12,7 @@ # Path to the configuration file template we will use for tests. TESTING_CONFIGS_DIR = Path(__file__).parent / 'testing_configs' -UTOPIA_SQL_FIXTURE = Path(__file__).parent.parent / 'temoa' / 'tutorial_assets' / 'utopia.sql' +UTOPIA_SQL_FIXTURE = Path(__file__).parent / 'testing_data' / 'utopia_v3.sql' UTOPIA_CONFIG_TEMPLATE = TESTING_CONFIGS_DIR / 'config_utopia.toml' diff --git a/tests/testing_data/utopia_v3.sql b/tests/testing_data/utopia_v3.sql new file mode 100644 index 00000000..c480d93b --- /dev/null +++ b/tests/testing_data/utopia_v3.sql @@ -0,0 +1,1558 @@ +PRAGMA foreign_keys=OFF; +BEGIN TRANSACTION; +CREATE TABLE MetaData +( + element TEXT, + value INT, + notes TEXT, + PRIMARY KEY (element) +); +INSERT INTO MetaData VALUES('DB_MAJOR',3,'DB major version number'); +INSERT INTO MetaData VALUES('DB_MINOR',1,'DB minor version number'); +INSERT INTO MetaData VALUES ('days_per_period', 365, 'count of days in each period'); +CREATE TABLE MetaDataReal +( + element TEXT, + value REAL, + notes TEXT, + + PRIMARY KEY (element) +); +INSERT INTO MetaDataReal VALUES('default_loan_rate',0.05000000000000000277,'Default Loan Rate if not specified in LoanRate table'); +INSERT INTO MetaDataReal VALUES('global_discount_rate',0.05000000000000000277,''); +CREATE TABLE OutputDualVariable +( + scenario TEXT, + constraint_name TEXT, + dual REAL, + PRIMARY KEY (constraint_name, scenario) +); +CREATE TABLE OutputObjective +( + scenario TEXT, + objective_name TEXT, + total_system_cost REAL +); +CREATE TABLE SeasonLabel +( + season TEXT PRIMARY KEY, + notes TEXT +); +INSERT INTO SeasonLabel VALUES('inter',NULL); +INSERT INTO SeasonLabel VALUES('summer',NULL); +INSERT INTO SeasonLabel VALUES('winter',NULL); +CREATE TABLE SectorLabel +( + sector TEXT PRIMARY KEY, + notes TEXT +); +INSERT INTO SectorLabel VALUES('supply',NULL); +INSERT INTO SectorLabel VALUES('electric',NULL); +INSERT INTO SectorLabel VALUES('transport',NULL); +INSERT INTO SectorLabel VALUES('commercial',NULL); +INSERT INTO SectorLabel VALUES('residential',NULL); +INSERT INTO SectorLabel VALUES('industrial',NULL); +CREATE TABLE CapacityCredit +( + region TEXT, + period INTEGER + REFERENCES TimePeriod (period), + tech TEXT + REFERENCES Technology (tech), + vintage INTEGER, + credit REAL, + notes TEXT, + PRIMARY KEY (region, period, tech, vintage), + CHECK (credit >= 0 AND credit <= 1) +); +CREATE TABLE CapacityFactorProcess +( + region TEXT, + period INTEGER + REFERENCES TimePeriod (period), + season TEXT + REFERENCES SeasonLabel (season), + tod TEXT + REFERENCES TimeOfDay (tod), + tech TEXT + REFERENCES Technology (tech), + vintage INTEGER, + factor REAL, + notes TEXT, + PRIMARY KEY (region, period, season, tod, tech, vintage), + CHECK (factor >= 0 AND factor <= 1) +); +INSERT INTO CapacityFactorProcess VALUES('utopia',2000,'inter','day','E31',2000,0.2752999999999999892,''); +INSERT INTO CapacityFactorProcess VALUES('utopia',2000,'inter','night','E31',2000,0.2752999999999999892,''); +INSERT INTO CapacityFactorProcess VALUES('utopia',2000,'winter','day','E31',2000,0.2752999999999999892,''); +INSERT INTO CapacityFactorProcess VALUES('utopia',2000,'winter','night','E31',2000,0.2752999999999999892,''); +INSERT INTO CapacityFactorProcess VALUES('utopia',2000,'summer','day','E31',2000,0.2752999999999999892,''); +INSERT INTO CapacityFactorProcess VALUES('utopia',2000,'summer','night','E31',2000,0.2752999999999999892,''); +INSERT INTO CapacityFactorProcess VALUES('utopia',2010,'inter','day','E31',2000,0.2752999999999999892,''); +INSERT INTO CapacityFactorProcess VALUES('utopia',2010,'inter','night','E31',2000,0.2752999999999999892,''); +INSERT INTO CapacityFactorProcess VALUES('utopia',2010,'winter','day','E31',2000,0.2752999999999999892,''); +INSERT INTO CapacityFactorProcess VALUES('utopia',2010,'winter','night','E31',2000,0.2752999999999999892,''); +INSERT INTO CapacityFactorProcess VALUES('utopia',2010,'summer','day','E31',2000,0.2752999999999999892,''); +INSERT INTO CapacityFactorProcess VALUES('utopia',2010,'summer','night','E31',2000,0.2752999999999999892,''); +INSERT INTO CapacityFactorProcess VALUES('utopia',2010,'inter','day','E31',2010,0.2756000000000000116,''); +INSERT INTO CapacityFactorProcess VALUES('utopia',2010,'inter','night','E31',2010,0.2756000000000000116,''); +INSERT INTO CapacityFactorProcess VALUES('utopia',2010,'winter','day','E31',2010,0.2756000000000000116,''); +INSERT INTO CapacityFactorProcess VALUES('utopia',2010,'winter','night','E31',2010,0.2756000000000000116,''); +INSERT INTO CapacityFactorProcess VALUES('utopia',2010,'summer','day','E31',2010,0.2756000000000000116,''); +INSERT INTO CapacityFactorProcess VALUES('utopia',2010,'summer','night','E31',2010,0.2756000000000000116,''); +CREATE TABLE CapacityFactorTech +( + region TEXT, + period INTEGER + REFERENCES TimePeriod (period), + season TEXT + REFERENCES SeasonLabel (season), + tod TEXT + REFERENCES TimeOfDay (tod), + tech TEXT + REFERENCES Technology (tech), + factor REAL, + notes TEXT, + PRIMARY KEY (region, period, season, tod, tech), + CHECK (factor >= 0 AND factor <= 1) +); +INSERT INTO CapacityFactorTech VALUES('utopia',1990,'inter','day','E01',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',1990,'inter','night','E01',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',1990,'winter','day','E01',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',1990,'winter','night','E01',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',1990,'summer','day','E01',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',1990,'summer','night','E01',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',1990,'inter','day','E21',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',1990,'inter','night','E21',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',1990,'winter','day','E21',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',1990,'winter','night','E21',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',1990,'summer','day','E21',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',1990,'summer','night','E21',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',1990,'inter','day','E31',0.2750000000000000222,''); +INSERT INTO CapacityFactorTech VALUES('utopia',1990,'inter','night','E31',0.2750000000000000222,''); +INSERT INTO CapacityFactorTech VALUES('utopia',1990,'winter','day','E31',0.2750000000000000222,''); +INSERT INTO CapacityFactorTech VALUES('utopia',1990,'winter','night','E31',0.2750000000000000222,''); +INSERT INTO CapacityFactorTech VALUES('utopia',1990,'summer','day','E31',0.2750000000000000222,''); +INSERT INTO CapacityFactorTech VALUES('utopia',1990,'summer','night','E31',0.2750000000000000222,''); +INSERT INTO CapacityFactorTech VALUES('utopia',1990,'inter','day','E51',0.1700000000000000122,''); +INSERT INTO CapacityFactorTech VALUES('utopia',1990,'inter','night','E51',0.1700000000000000122,''); +INSERT INTO CapacityFactorTech VALUES('utopia',1990,'winter','day','E51',0.1700000000000000122,''); +INSERT INTO CapacityFactorTech VALUES('utopia',1990,'winter','night','E51',0.1700000000000000122,''); +INSERT INTO CapacityFactorTech VALUES('utopia',1990,'summer','day','E51',0.1700000000000000122,''); +INSERT INTO CapacityFactorTech VALUES('utopia',1990,'summer','night','E51',0.1700000000000000122,''); +INSERT INTO CapacityFactorTech VALUES('utopia',1990,'inter','day','E70',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',1990,'inter','night','E70',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',1990,'winter','day','E70',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',1990,'winter','night','E70',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',1990,'summer','day','E70',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',1990,'summer','night','E70',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2000,'inter','day','E01',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2000,'inter','night','E01',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2000,'winter','day','E01',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2000,'winter','night','E01',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2000,'summer','day','E01',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2000,'summer','night','E01',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2000,'inter','day','E21',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2000,'inter','night','E21',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2000,'winter','day','E21',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2000,'winter','night','E21',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2000,'summer','day','E21',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2000,'summer','night','E21',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2000,'inter','day','E31',0.2750000000000000222,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2000,'inter','night','E31',0.2750000000000000222,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2000,'winter','day','E31',0.2750000000000000222,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2000,'winter','night','E31',0.2750000000000000222,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2000,'summer','day','E31',0.2750000000000000222,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2000,'summer','night','E31',0.2750000000000000222,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2000,'inter','day','E51',0.1700000000000000122,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2000,'inter','night','E51',0.1700000000000000122,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2000,'winter','day','E51',0.1700000000000000122,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2000,'winter','night','E51',0.1700000000000000122,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2000,'summer','day','E51',0.1700000000000000122,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2000,'summer','night','E51',0.1700000000000000122,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2000,'inter','day','E70',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2000,'inter','night','E70',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2000,'winter','day','E70',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2000,'winter','night','E70',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2000,'summer','day','E70',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2000,'summer','night','E70',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2010,'inter','day','E01',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2010,'inter','night','E01',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2010,'winter','day','E01',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2010,'winter','night','E01',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2010,'summer','day','E01',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2010,'summer','night','E01',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2010,'inter','day','E21',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2010,'inter','night','E21',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2010,'winter','day','E21',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2010,'winter','night','E21',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2010,'summer','day','E21',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2010,'summer','night','E21',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2010,'inter','day','E31',0.2750000000000000222,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2010,'inter','night','E31',0.2750000000000000222,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2010,'winter','day','E31',0.2750000000000000222,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2010,'winter','night','E31',0.2750000000000000222,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2010,'summer','day','E31',0.2750000000000000222,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2010,'summer','night','E31',0.2750000000000000222,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2010,'inter','day','E51',0.1700000000000000122,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2010,'inter','night','E51',0.1700000000000000122,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2010,'winter','day','E51',0.1700000000000000122,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2010,'winter','night','E51',0.1700000000000000122,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2010,'summer','day','E51',0.1700000000000000122,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2010,'summer','night','E51',0.1700000000000000122,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2010,'inter','day','E70',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2010,'inter','night','E70',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2010,'winter','day','E70',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2010,'winter','night','E70',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2010,'summer','day','E70',0.8000000000000000444,''); +INSERT INTO CapacityFactorTech VALUES('utopia',2010,'summer','night','E70',0.8000000000000000444,''); +CREATE TABLE CapacityToActivity +( + region TEXT, + tech TEXT + REFERENCES Technology (tech), + c2a REAL, + notes TEXT, + PRIMARY KEY (region, tech) +); +INSERT INTO CapacityToActivity VALUES('utopia','E01',31.53999999999999915,''); +INSERT INTO CapacityToActivity VALUES('utopia','E21',31.53999999999999915,''); +INSERT INTO CapacityToActivity VALUES('utopia','E31',31.53999999999999915,''); +INSERT INTO CapacityToActivity VALUES('utopia','E51',31.53999999999999915,''); +INSERT INTO CapacityToActivity VALUES('utopia','E70',31.53999999999999915,''); +INSERT INTO CapacityToActivity VALUES('utopia','RHE',1.0,''); +INSERT INTO CapacityToActivity VALUES('utopia','RHO',1.0,''); +INSERT INTO CapacityToActivity VALUES('utopia','RL1',1.0,''); +INSERT INTO CapacityToActivity VALUES('utopia','SRE',1.0,''); +INSERT INTO CapacityToActivity VALUES('utopia','TXD',1.0,''); +INSERT INTO CapacityToActivity VALUES('utopia','TXE',1.0,''); +INSERT INTO CapacityToActivity VALUES('utopia','TXG',1.0,''); +CREATE TABLE Commodity +( + name TEXT + PRIMARY KEY, + flag TEXT + REFERENCES CommodityType (label), + description TEXT +); +INSERT INTO Commodity VALUES('ethos','s','# dummy commodity to supply inputs (makes graph easier to read)'); +INSERT INTO Commodity VALUES('DSL','p','# diesel'); +INSERT INTO Commodity VALUES('ELC','p','# electricity'); +INSERT INTO Commodity VALUES('FEQ','p','# fossil equivalent'); +INSERT INTO Commodity VALUES('GSL','p','# gasoline'); +INSERT INTO Commodity VALUES('HCO','p','# coal'); +INSERT INTO Commodity VALUES('HYD','p','# water'); +INSERT INTO Commodity VALUES('OIL','p','# crude oil'); +INSERT INTO Commodity VALUES('URN','p','# uranium'); +INSERT INTO Commodity VALUES('co2','e','#CO2 emissions'); +INSERT INTO Commodity VALUES('nox','e','#NOX emissions'); +INSERT INTO Commodity VALUES('RH','d','# residential heating'); +INSERT INTO Commodity VALUES('RL','d','# residential lighting'); +INSERT INTO Commodity VALUES('TX','d','# transportation'); +CREATE TABLE CommodityType +( + label TEXT + PRIMARY KEY, + description TEXT +); +INSERT INTO CommodityType VALUES('w','waste commodity'); +INSERT INTO CommodityType VALUES('wa','waste annual commodity'); +INSERT INTO CommodityType VALUES('wp','waste physical commodity'); +INSERT INTO CommodityType VALUES('a','annual commodity'); +INSERT INTO CommodityType VALUES('s','source commodity'); +INSERT INTO CommodityType VALUES('p','physical commodity'); +INSERT INTO CommodityType VALUES('e','emissions commodity'); +INSERT INTO CommodityType VALUES('d','demand commodity'); +CREATE TABLE ConstructionInput +( + region TEXT, + input_comm TEXT + REFERENCES Commodity (name), + tech TEXT + REFERENCES Technology (tech), + vintage INTEGER + REFERENCES TimePeriod (period), + value REAL, + units TEXT, + notes TEXT, + PRIMARY KEY (region, input_comm, tech, vintage) +); +CREATE TABLE CostEmission +( + region TEXT, + period INTEGER + REFERENCES TimePeriod (period), + emis_comm TEXT NOT NULL + REFERENCES Commodity (name), + cost REAL NOT NULL, + units TEXT, + notes TEXT, + PRIMARY KEY (region, period, emis_comm) +); +CREATE TABLE CostFixed +( + region TEXT NOT NULL, + period INTEGER NOT NULL + REFERENCES TimePeriod (period), + tech TEXT NOT NULL + REFERENCES Technology (tech), + vintage INTEGER NOT NULL + REFERENCES TimePeriod (period), + cost REAL, + units TEXT, + notes TEXT, + PRIMARY KEY (region, period, tech, vintage) +); +INSERT INTO CostFixed VALUES('utopia',1990,'E01',1960,40.0,'',''); +INSERT INTO CostFixed VALUES('utopia',1990,'E01',1970,40.0,'',''); +INSERT INTO CostFixed VALUES('utopia',1990,'E01',1980,40.0,'',''); +INSERT INTO CostFixed VALUES('utopia',1990,'E01',1990,40.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2000,'E01',1970,70.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2000,'E01',1980,70.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2000,'E01',1990,70.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2000,'E01',2000,70.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2010,'E01',1980,100.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2010,'E01',1990,100.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2010,'E01',2000,100.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2010,'E01',2010,100.0,'',''); +INSERT INTO CostFixed VALUES('utopia',1990,'E21',1990,500.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2000,'E21',1990,500.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2010,'E21',1990,500.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2000,'E21',2000,500.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2010,'E21',2000,500.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2010,'E21',2010,500.0,'',''); +INSERT INTO CostFixed VALUES('utopia',1990,'E31',1980,75.0,'',''); +INSERT INTO CostFixed VALUES('utopia',1990,'E31',1990,75.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2000,'E31',1980,75.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2000,'E31',1990,75.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2000,'E31',2000,75.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2010,'E31',1980,75.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2010,'E31',1990,75.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2010,'E31',2000,75.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2010,'E31',2010,75.0,'',''); +INSERT INTO CostFixed VALUES('utopia',1990,'E51',1980,30.0,'',''); +INSERT INTO CostFixed VALUES('utopia',1990,'E51',1990,30.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2000,'E51',1980,30.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2000,'E51',1990,30.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2000,'E51',2000,30.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2010,'E51',1980,30.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2010,'E51',1990,30.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2010,'E51',2000,30.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2010,'E51',2010,30.0,'',''); +INSERT INTO CostFixed VALUES('utopia',1990,'E70',1960,30.0,'',''); +INSERT INTO CostFixed VALUES('utopia',1990,'E70',1970,30.0,'',''); +INSERT INTO CostFixed VALUES('utopia',1990,'E70',1980,30.0,'',''); +INSERT INTO CostFixed VALUES('utopia',1990,'E70',1990,30.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2000,'E70',1970,30.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2000,'E70',1980,30.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2000,'E70',1990,30.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2000,'E70',2000,30.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2010,'E70',1980,30.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2010,'E70',1990,30.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2010,'E70',2000,30.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2010,'E70',2010,30.0,'',''); +INSERT INTO CostFixed VALUES('utopia',1990,'RHO',1970,1.0,'',''); +INSERT INTO CostFixed VALUES('utopia',1990,'RHO',1980,1.0,'',''); +INSERT INTO CostFixed VALUES('utopia',1990,'RHO',1990,1.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2000,'RHO',1980,1.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2000,'RHO',1990,1.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2000,'RHO',2000,1.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2010,'RHO',1990,1.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2010,'RHO',2000,1.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2010,'RHO',2010,1.0,'',''); +INSERT INTO CostFixed VALUES('utopia',1990,'RL1',1980,9.46000000000000086,'',''); +INSERT INTO CostFixed VALUES('utopia',1990,'RL1',1990,9.46000000000000086,'',''); +INSERT INTO CostFixed VALUES('utopia',2000,'RL1',2000,9.46000000000000086,'',''); +INSERT INTO CostFixed VALUES('utopia',2010,'RL1',2010,9.46000000000000086,'',''); +INSERT INTO CostFixed VALUES('utopia',1990,'TXD',1970,52.0,'',''); +INSERT INTO CostFixed VALUES('utopia',1990,'TXD',1980,52.0,'',''); +INSERT INTO CostFixed VALUES('utopia',1990,'TXD',1990,52.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2000,'TXD',1980,52.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2000,'TXD',1990,52.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2000,'TXD',2000,52.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2010,'TXD',2000,52.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2010,'TXD',2010,52.0,'',''); +INSERT INTO CostFixed VALUES('utopia',1990,'TXE',1990,100.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2000,'TXE',1990,90.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2000,'TXE',2000,90.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2010,'TXE',2000,80.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2010,'TXE',2010,80.0,'',''); +INSERT INTO CostFixed VALUES('utopia',1990,'TXG',1970,48.0,'',''); +INSERT INTO CostFixed VALUES('utopia',1990,'TXG',1980,48.0,'',''); +INSERT INTO CostFixed VALUES('utopia',1990,'TXG',1990,48.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2000,'TXG',1980,48.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2000,'TXG',1990,48.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2000,'TXG',2000,48.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2010,'TXG',2000,48.0,'',''); +INSERT INTO CostFixed VALUES('utopia',2010,'TXG',2010,48.0,'',''); +CREATE TABLE CostInvest +( + region TEXT, + tech TEXT + REFERENCES Technology (tech), + vintage INTEGER + REFERENCES TimePeriod (period), + cost REAL, + units TEXT, + notes TEXT, + PRIMARY KEY (region, tech, vintage) +); +INSERT INTO CostInvest VALUES('utopia','E01',1990,2000.0,'',''); +INSERT INTO CostInvest VALUES('utopia','E01',2000,1300.0,'',''); +INSERT INTO CostInvest VALUES('utopia','E01',2010,1200.0,'',''); +INSERT INTO CostInvest VALUES('utopia','E21',1990,5000.0,'',''); +INSERT INTO CostInvest VALUES('utopia','E21',2000,5000.0,'',''); +INSERT INTO CostInvest VALUES('utopia','E21',2010,5000.0,'',''); +INSERT INTO CostInvest VALUES('utopia','E31',1990,3000.0,'',''); +INSERT INTO CostInvest VALUES('utopia','E31',2000,3000.0,'',''); +INSERT INTO CostInvest VALUES('utopia','E31',2010,3000.0,'',''); +INSERT INTO CostInvest VALUES('utopia','E51',1990,900.0,'',''); +INSERT INTO CostInvest VALUES('utopia','E51',2000,900.0,'',''); +INSERT INTO CostInvest VALUES('utopia','E51',2010,900.0,'',''); +INSERT INTO CostInvest VALUES('utopia','E70',1990,1000.0,'',''); +INSERT INTO CostInvest VALUES('utopia','E70',2000,1000.0,'',''); +INSERT INTO CostInvest VALUES('utopia','E70',2010,1000.0,'',''); +INSERT INTO CostInvest VALUES('utopia','RHE',1990,90.0,'',''); +INSERT INTO CostInvest VALUES('utopia','RHE',2000,90.0,'',''); +INSERT INTO CostInvest VALUES('utopia','RHE',2010,90.0,'',''); +INSERT INTO CostInvest VALUES('utopia','RHO',1990,100.0,'',''); +INSERT INTO CostInvest VALUES('utopia','RHO',2000,100.0,'',''); +INSERT INTO CostInvest VALUES('utopia','RHO',2010,100.0,'',''); +INSERT INTO CostInvest VALUES('utopia','SRE',1990,100.0,'',''); +INSERT INTO CostInvest VALUES('utopia','SRE',2000,100.0,'',''); +INSERT INTO CostInvest VALUES('utopia','SRE',2010,100.0,'',''); +INSERT INTO CostInvest VALUES('utopia','TXD',1990,1044.0,'',''); +INSERT INTO CostInvest VALUES('utopia','TXD',2000,1044.0,'',''); +INSERT INTO CostInvest VALUES('utopia','TXD',2010,1044.0,'',''); +INSERT INTO CostInvest VALUES('utopia','TXE',1990,2000.0,'',''); +INSERT INTO CostInvest VALUES('utopia','TXE',2000,1750.0,'',''); +INSERT INTO CostInvest VALUES('utopia','TXE',2010,1500.0,'',''); +INSERT INTO CostInvest VALUES('utopia','TXG',1990,1044.0,'',''); +INSERT INTO CostInvest VALUES('utopia','TXG',2000,1044.0,'',''); +INSERT INTO CostInvest VALUES('utopia','TXG',2010,1044.0,'',''); +CREATE TABLE CostVariable +( + region TEXT NOT NULL, + period INTEGER NOT NULL + REFERENCES TimePeriod (period), + tech TEXT NOT NULL + REFERENCES Technology (tech), + vintage INTEGER NOT NULL + REFERENCES TimePeriod (period), + cost REAL, + units TEXT, + notes TEXT, + PRIMARY KEY (region, period, tech, vintage) +); +INSERT INTO CostVariable VALUES('utopia',1990,'IMPDSL1',1990,10.0,'',''); +INSERT INTO CostVariable VALUES('utopia',2000,'IMPDSL1',1990,10.0,'',''); +INSERT INTO CostVariable VALUES('utopia',2010,'IMPDSL1',1990,10.0,'',''); +INSERT INTO CostVariable VALUES('utopia',1990,'IMPGSL1',1990,15.0,'',''); +INSERT INTO CostVariable VALUES('utopia',2000,'IMPGSL1',1990,15.0,'',''); +INSERT INTO CostVariable VALUES('utopia',2010,'IMPGSL1',1990,15.0,'',''); +INSERT INTO CostVariable VALUES('utopia',1990,'IMPHCO1',1990,2.0,'',''); +INSERT INTO CostVariable VALUES('utopia',2000,'IMPHCO1',1990,2.0,'',''); +INSERT INTO CostVariable VALUES('utopia',2010,'IMPHCO1',1990,2.0,'',''); +INSERT INTO CostVariable VALUES('utopia',1990,'IMPOIL1',1990,8.0,'',''); +INSERT INTO CostVariable VALUES('utopia',2000,'IMPOIL1',1990,8.0,'',''); +INSERT INTO CostVariable VALUES('utopia',2010,'IMPOIL1',1990,8.0,'',''); +INSERT INTO CostVariable VALUES('utopia',1990,'IMPURN1',1990,2.0,'',''); +INSERT INTO CostVariable VALUES('utopia',2000,'IMPURN1',1990,2.0,'',''); +INSERT INTO CostVariable VALUES('utopia',2010,'IMPURN1',1990,2.0,'',''); +INSERT INTO CostVariable VALUES('utopia',1990,'E01',1960,0.2999999999999999889,'',''); +INSERT INTO CostVariable VALUES('utopia',1990,'E01',1970,0.2999999999999999889,'',''); +INSERT INTO CostVariable VALUES('utopia',1990,'E01',1980,0.2999999999999999889,'',''); +INSERT INTO CostVariable VALUES('utopia',1990,'E01',1990,0.2999999999999999889,'',''); +INSERT INTO CostVariable VALUES('utopia',2000,'E01',1970,0.2999999999999999889,'',''); +INSERT INTO CostVariable VALUES('utopia',2000,'E01',1980,0.2999999999999999889,'',''); +INSERT INTO CostVariable VALUES('utopia',2000,'E01',1990,0.2999999999999999889,'',''); +INSERT INTO CostVariable VALUES('utopia',2000,'E01',2000,0.2999999999999999889,'',''); +INSERT INTO CostVariable VALUES('utopia',2010,'E01',1980,0.2999999999999999889,'',''); +INSERT INTO CostVariable VALUES('utopia',2010,'E01',1990,0.2999999999999999889,'',''); +INSERT INTO CostVariable VALUES('utopia',2010,'E01',2000,0.2999999999999999889,'',''); +INSERT INTO CostVariable VALUES('utopia',2010,'E01',2010,0.2999999999999999889,'',''); +INSERT INTO CostVariable VALUES('utopia',1990,'E21',1990,1.5,'',''); +INSERT INTO CostVariable VALUES('utopia',2000,'E21',1990,1.5,'',''); +INSERT INTO CostVariable VALUES('utopia',2010,'E21',1990,1.5,'',''); +INSERT INTO CostVariable VALUES('utopia',2000,'E21',2000,1.5,'',''); +INSERT INTO CostVariable VALUES('utopia',2010,'E21',2000,1.5,'',''); +INSERT INTO CostVariable VALUES('utopia',2010,'E21',2010,1.5,'',''); +INSERT INTO CostVariable VALUES('utopia',1990,'E70',1960,0.4000000000000000222,'',''); +INSERT INTO CostVariable VALUES('utopia',1990,'E70',1970,0.4000000000000000222,'',''); +INSERT INTO CostVariable VALUES('utopia',1990,'E70',1980,0.4000000000000000222,'',''); +INSERT INTO CostVariable VALUES('utopia',1990,'E70',1990,0.4000000000000000222,'',''); +INSERT INTO CostVariable VALUES('utopia',2000,'E70',1970,0.4000000000000000222,'',''); +INSERT INTO CostVariable VALUES('utopia',2000,'E70',1980,0.4000000000000000222,'',''); +INSERT INTO CostVariable VALUES('utopia',2000,'E70',1990,0.4000000000000000222,'',''); +INSERT INTO CostVariable VALUES('utopia',2000,'E70',2000,0.4000000000000000222,'',''); +INSERT INTO CostVariable VALUES('utopia',2010,'E70',1980,0.4000000000000000222,'',''); +INSERT INTO CostVariable VALUES('utopia',2010,'E70',1990,0.4000000000000000222,'',''); +INSERT INTO CostVariable VALUES('utopia',2010,'E70',2000,0.4000000000000000222,'',''); +INSERT INTO CostVariable VALUES('utopia',2010,'E70',2010,0.4000000000000000222,'',''); +INSERT INTO CostVariable VALUES('utopia',1990,'SRE',1990,10.0,'',''); +INSERT INTO CostVariable VALUES('utopia',2000,'SRE',1990,10.0,'',''); +INSERT INTO CostVariable VALUES('utopia',2000,'SRE',2000,10.0,'',''); +INSERT INTO CostVariable VALUES('utopia',2010,'SRE',1990,10.0,'',''); +INSERT INTO CostVariable VALUES('utopia',2010,'SRE',2000,10.0,'',''); +INSERT INTO CostVariable VALUES('utopia',2010,'SRE',2010,10.0,'',''); +CREATE TABLE Demand +( + region TEXT, + period INTEGER + REFERENCES TimePeriod (period), + commodity TEXT + REFERENCES Commodity (name), + demand REAL, + units TEXT, + notes TEXT, + PRIMARY KEY (region, period, commodity) +); +INSERT INTO Demand VALUES('utopia',1990,'RH',25.19999999999999929,'',''); +INSERT INTO Demand VALUES('utopia',2000,'RH',37.79999999999999715,'',''); +INSERT INTO Demand VALUES('utopia',2010,'RH',56.69999999999999574,'',''); +INSERT INTO Demand VALUES('utopia',1990,'RL',5.599999999999999645,'',''); +INSERT INTO Demand VALUES('utopia',2000,'RL',8.400000000000000355,'',''); +INSERT INTO Demand VALUES('utopia',2010,'RL',12.59999999999999965,'',''); +INSERT INTO Demand VALUES('utopia',1990,'TX',5.200000000000000177,'',''); +INSERT INTO Demand VALUES('utopia',2000,'TX',7.799999999999999823,'',''); +INSERT INTO Demand VALUES('utopia',2010,'TX',11.68999999999999951,'',''); +CREATE TABLE DemandSpecificDistribution +( + region TEXT, + period INTEGER + REFERENCES TimePeriod (period), + season TEXT + REFERENCES SeasonLabel (season), + tod TEXT + REFERENCES TimeOfDay (tod), + demand_name TEXT + REFERENCES Commodity (name), + dsd REAL, + notes TEXT, + PRIMARY KEY (region, period, season, tod, demand_name), + CHECK (dsd >= 0 AND dsd <= 1) +); +INSERT INTO DemandSpecificDistribution VALUES('utopia',1990,'inter','day','RH',0.1199999999999999956,''); +INSERT INTO DemandSpecificDistribution VALUES('utopia',1990,'inter','night','RH',0.05999999999999999778,''); +INSERT INTO DemandSpecificDistribution VALUES('utopia',1990,'winter','day','RH',0.5466999999999999638,''); +INSERT INTO DemandSpecificDistribution VALUES('utopia',1990,'winter','night','RH',0.2732999999999999874,''); +INSERT INTO DemandSpecificDistribution VALUES('utopia',1990,'inter','day','RL',0.1499999999999999945,''); +INSERT INTO DemandSpecificDistribution VALUES('utopia',1990,'inter','night','RL',0.05000000000000000277,''); +INSERT INTO DemandSpecificDistribution VALUES('utopia',1990,'summer','day','RL',0.1499999999999999945,''); +INSERT INTO DemandSpecificDistribution VALUES('utopia',1990,'summer','night','RL',0.05000000000000000277,''); +INSERT INTO DemandSpecificDistribution VALUES('utopia',1990,'winter','day','RL',0.5,''); +INSERT INTO DemandSpecificDistribution VALUES('utopia',1990,'winter','night','RL',0.1000000000000000055,''); +INSERT INTO DemandSpecificDistribution VALUES('utopia',2000,'inter','day','RH',0.1199999999999999956,''); +INSERT INTO DemandSpecificDistribution VALUES('utopia',2000,'inter','night','RH',0.05999999999999999778,''); +INSERT INTO DemandSpecificDistribution VALUES('utopia',2000,'winter','day','RH',0.5466999999999999638,''); +INSERT INTO DemandSpecificDistribution VALUES('utopia',2000,'winter','night','RH',0.2732999999999999874,''); +INSERT INTO DemandSpecificDistribution VALUES('utopia',2000,'inter','day','RL',0.1499999999999999945,''); +INSERT INTO DemandSpecificDistribution VALUES('utopia',2000,'inter','night','RL',0.05000000000000000277,''); +INSERT INTO DemandSpecificDistribution VALUES('utopia',2000,'summer','day','RL',0.1499999999999999945,''); +INSERT INTO DemandSpecificDistribution VALUES('utopia',2000,'summer','night','RL',0.05000000000000000277,''); +INSERT INTO DemandSpecificDistribution VALUES('utopia',2000,'winter','day','RL',0.5,''); +INSERT INTO DemandSpecificDistribution VALUES('utopia',2000,'winter','night','RL',0.1000000000000000055,''); +INSERT INTO DemandSpecificDistribution VALUES('utopia',2010,'inter','day','RH',0.1199999999999999956,''); +INSERT INTO DemandSpecificDistribution VALUES('utopia',2010,'inter','night','RH',0.05999999999999999778,''); +INSERT INTO DemandSpecificDistribution VALUES('utopia',2010,'winter','day','RH',0.5466999999999999638,''); +INSERT INTO DemandSpecificDistribution VALUES('utopia',2010,'winter','night','RH',0.2732999999999999874,''); +INSERT INTO DemandSpecificDistribution VALUES('utopia',2010,'inter','day','RL',0.1499999999999999945,''); +INSERT INTO DemandSpecificDistribution VALUES('utopia',2010,'inter','night','RL',0.05000000000000000277,''); +INSERT INTO DemandSpecificDistribution VALUES('utopia',2010,'summer','day','RL',0.1499999999999999945,''); +INSERT INTO DemandSpecificDistribution VALUES('utopia',2010,'summer','night','RL',0.05000000000000000277,''); +INSERT INTO DemandSpecificDistribution VALUES('utopia',2010,'winter','day','RL',0.5,''); +INSERT INTO DemandSpecificDistribution VALUES('utopia',2010,'winter','night','RL',0.1000000000000000055,''); +CREATE TABLE EndOfLifeOutput +( + region TEXT, + tech TEXT + REFERENCES Technology (tech), + vintage INTEGER + REFERENCES TimePeriod (period), + output_comm TEXT + REFERENCES Commodity (name), + value REAL, + units TEXT, + notes TEXT, + PRIMARY KEY (region, tech, vintage, output_comm) +); +CREATE TABLE Efficiency +( + region TEXT, + input_comm TEXT + REFERENCES Commodity (name), + tech TEXT + REFERENCES Technology (tech), + vintage INTEGER + REFERENCES TimePeriod (period), + output_comm TEXT + REFERENCES Commodity (name), + efficiency REAL, + notes TEXT, + PRIMARY KEY (region, input_comm, tech, vintage, output_comm), + CHECK (efficiency > 0) +); +INSERT INTO Efficiency VALUES('utopia','ethos','IMPDSL1',1990,'DSL',1.0,''); +INSERT INTO Efficiency VALUES('utopia','ethos','IMPGSL1',1990,'GSL',1.0,''); +INSERT INTO Efficiency VALUES('utopia','ethos','IMPHCO1',1990,'HCO',1.0,''); +INSERT INTO Efficiency VALUES('utopia','ethos','IMPOIL1',1990,'OIL',1.0,''); +INSERT INTO Efficiency VALUES('utopia','ethos','IMPURN1',1990,'URN',1.0,''); +INSERT INTO Efficiency VALUES('utopia','ethos','IMPFEQ',1990,'FEQ',1.0,''); +INSERT INTO Efficiency VALUES('utopia','ethos','IMPHYD',1990,'HYD',1.0,''); +INSERT INTO Efficiency VALUES('utopia','HCO','E01',1960,'ELC',0.3200000000000000066,'# 1/3.125'); +INSERT INTO Efficiency VALUES('utopia','HCO','E01',1970,'ELC',0.3200000000000000066,'# 1/3.125'); +INSERT INTO Efficiency VALUES('utopia','HCO','E01',1980,'ELC',0.3200000000000000066,'# 1/3.125'); +INSERT INTO Efficiency VALUES('utopia','HCO','E01',1990,'ELC',0.3200000000000000066,'# 1/3.125'); +INSERT INTO Efficiency VALUES('utopia','HCO','E01',2000,'ELC',0.3200000000000000066,'# 1/3.125'); +INSERT INTO Efficiency VALUES('utopia','HCO','E01',2010,'ELC',0.3200000000000000066,'# 1/3.125'); +INSERT INTO Efficiency VALUES('utopia','FEQ','E21',1990,'ELC',0.3200000000000000066,'# 1/3.125'); +INSERT INTO Efficiency VALUES('utopia','FEQ','E21',2000,'ELC',0.3200000000000000066,'# 1/3.125'); +INSERT INTO Efficiency VALUES('utopia','FEQ','E21',2010,'ELC',0.3200000000000000066,'# 1/3.125'); +INSERT INTO Efficiency VALUES('utopia','URN','E21',1990,'ELC',0.4000000000000000222,'# 1/2.5'); +INSERT INTO Efficiency VALUES('utopia','URN','E21',2000,'ELC',0.4000000000000000222,'# 1/2.5'); +INSERT INTO Efficiency VALUES('utopia','URN','E21',2010,'ELC',0.4000000000000000222,'# 1/2.5'); +INSERT INTO Efficiency VALUES('utopia','HYD','E31',1980,'ELC',0.3200000000000000066,'# 1/3.125'); +INSERT INTO Efficiency VALUES('utopia','HYD','E31',1990,'ELC',0.3200000000000000066,'# 1/3.125'); +INSERT INTO Efficiency VALUES('utopia','HYD','E31',2000,'ELC',0.3200000000000000066,'# 1/3.125'); +INSERT INTO Efficiency VALUES('utopia','HYD','E31',2010,'ELC',0.3200000000000000066,'# 1/3.125'); +INSERT INTO Efficiency VALUES('utopia','DSL','E70',1960,'ELC',0.2939999999999999836,'# 1/3.4'); +INSERT INTO Efficiency VALUES('utopia','DSL','E70',1970,'ELC',0.2939999999999999836,'# 1/3.4'); +INSERT INTO Efficiency VALUES('utopia','DSL','E70',1980,'ELC',0.2939999999999999836,'# 1/3.4'); +INSERT INTO Efficiency VALUES('utopia','DSL','E70',1990,'ELC',0.2939999999999999836,'# 1/3.4'); +INSERT INTO Efficiency VALUES('utopia','DSL','E70',2000,'ELC',0.2939999999999999836,'# 1/3.4'); +INSERT INTO Efficiency VALUES('utopia','DSL','E70',2010,'ELC',0.2939999999999999836,'# 1/3.4'); +INSERT INTO Efficiency VALUES('utopia','ELC','E51',1980,'ELC',0.7199999999999999734,'# 1/1.3889'); +INSERT INTO Efficiency VALUES('utopia','ELC','E51',1990,'ELC',0.7199999999999999734,'# 1/1.3889'); +INSERT INTO Efficiency VALUES('utopia','ELC','E51',2000,'ELC',0.7199999999999999734,'# 1/1.3889'); +INSERT INTO Efficiency VALUES('utopia','ELC','E51',2010,'ELC',0.7199999999999999734,'# 1/1.3889'); +INSERT INTO Efficiency VALUES('utopia','ELC','RHE',1990,'RH',1.0,'# direct translation from DMD_EFF'); +INSERT INTO Efficiency VALUES('utopia','ELC','RHE',2000,'RH',1.0,'# direct translation from DMD_EFF'); +INSERT INTO Efficiency VALUES('utopia','ELC','RHE',2010,'RH',1.0,'# direct translation from DMD_EFF'); +INSERT INTO Efficiency VALUES('utopia','DSL','RHO',1970,'RH',0.6999999999999999556,'# direct translation from DMD_EFF'); +INSERT INTO Efficiency VALUES('utopia','DSL','RHO',1980,'RH',0.6999999999999999556,'# direct translation from DMD_EFF'); +INSERT INTO Efficiency VALUES('utopia','DSL','RHO',1990,'RH',0.6999999999999999556,'# direct translation from DMD_EFF'); +INSERT INTO Efficiency VALUES('utopia','DSL','RHO',2000,'RH',0.6999999999999999556,'# direct translation from DMD_EFF'); +INSERT INTO Efficiency VALUES('utopia','DSL','RHO',2010,'RH',0.6999999999999999556,'# direct translation from DMD_EFF'); +INSERT INTO Efficiency VALUES('utopia','ELC','RL1',1980,'RL',1.0,'# direct translation from DMD_EFF'); +INSERT INTO Efficiency VALUES('utopia','ELC','RL1',1990,'RL',1.0,'# direct translation from DMD_EFF'); +INSERT INTO Efficiency VALUES('utopia','ELC','RL1',2000,'RL',1.0,'# direct translation from DMD_EFF'); +INSERT INTO Efficiency VALUES('utopia','ELC','RL1',2010,'RL',1.0,'# direct translation from DMD_EFF'); +INSERT INTO Efficiency VALUES('utopia','OIL','SRE',1990,'DSL',1.0,'# direct translation from PRC_INP2, PRC_OUT'); +INSERT INTO Efficiency VALUES('utopia','OIL','SRE',2000,'DSL',1.0,'# direct translation from PRC_INP2, PRC_OUT'); +INSERT INTO Efficiency VALUES('utopia','OIL','SRE',2010,'DSL',1.0,'# direct translation from PRC_INP2, PRC_OUT'); +INSERT INTO Efficiency VALUES('utopia','OIL','SRE',1990,'GSL',1.0,'# direct translation from PRC_INP2, PRC_OUT'); +INSERT INTO Efficiency VALUES('utopia','OIL','SRE',2000,'GSL',1.0,'# direct translation from PRC_INP2, PRC_OUT'); +INSERT INTO Efficiency VALUES('utopia','OIL','SRE',2010,'GSL',1.0,'# direct translation from PRC_INP2, PRC_OUT'); +INSERT INTO Efficiency VALUES('utopia','DSL','TXD',1970,'TX',0.2310000000000000108,'# direct translation from DMD_EFF'); +INSERT INTO Efficiency VALUES('utopia','DSL','TXD',1980,'TX',0.2310000000000000108,'# direct translation from DMD_EFF'); +INSERT INTO Efficiency VALUES('utopia','DSL','TXD',1990,'TX',0.2310000000000000108,'# direct translation from DMD_EFF'); +INSERT INTO Efficiency VALUES('utopia','DSL','TXD',2000,'TX',0.2310000000000000108,'# direct translation from DMD_EFF'); +INSERT INTO Efficiency VALUES('utopia','DSL','TXD',2010,'TX',0.2310000000000000108,'# direct translation from DMD_EFF'); +INSERT INTO Efficiency VALUES('utopia','ELC','TXE',1990,'TX',0.8269999999999999574,'# direct translation from DMD_EFF'); +INSERT INTO Efficiency VALUES('utopia','ELC','TXE',2000,'TX',0.8269999999999999574,'# direct translation from DMD_EFF'); +INSERT INTO Efficiency VALUES('utopia','ELC','TXE',2010,'TX',0.8269999999999999574,'# direct translation from DMD_EFF'); +INSERT INTO Efficiency VALUES('utopia','GSL','TXG',1970,'TX',0.2310000000000000108,'# direct translation from DMD_EFF'); +INSERT INTO Efficiency VALUES('utopia','GSL','TXG',1980,'TX',0.2310000000000000108,'# direct translation from DMD_EFF'); +INSERT INTO Efficiency VALUES('utopia','GSL','TXG',1990,'TX',0.2310000000000000108,'# direct translation from DMD_EFF'); +INSERT INTO Efficiency VALUES('utopia','GSL','TXG',2000,'TX',0.2310000000000000108,'# direct translation from DMD_EFF'); +INSERT INTO Efficiency VALUES('utopia','GSL','TXG',2010,'TX',0.2310000000000000108,'# direct translation from DMD_EFF'); +CREATE TABLE EfficiencyVariable +( + region TEXT, + period INTEGER + REFERENCES TimePeriod (period), + season TEXT + REFERENCES SeasonLabel (season), + tod TEXT + REFERENCES TimeOfDay (tod), + input_comm TEXT + REFERENCES Commodity (name), + tech TEXT + REFERENCES Technology (tech), + vintage INTEGER + REFERENCES TimePeriod (period), + output_comm TEXT + REFERENCES Commodity (name), + efficiency REAL, + notes TEXT, + PRIMARY KEY (region, period, season, tod, input_comm, tech, vintage, output_comm), + CHECK (efficiency > 0) +); +CREATE TABLE EmissionActivity +( + region TEXT, + emis_comm TEXT + REFERENCES Commodity (name), + input_comm TEXT + REFERENCES Commodity (name), + tech TEXT + REFERENCES Technology (tech), + vintage INTEGER + REFERENCES TimePeriod (period), + output_comm TEXT + REFERENCES Commodity (name), + activity REAL, + units TEXT, + notes TEXT, + PRIMARY KEY (region, emis_comm, input_comm, tech, vintage, output_comm) +); +INSERT INTO EmissionActivity VALUES('utopia','co2','ethos','IMPDSL1',1990,'DSL',0.07499999999999999723,'',''); +INSERT INTO EmissionActivity VALUES('utopia','co2','ethos','IMPGSL1',1990,'GSL',0.07499999999999999723,'',''); +INSERT INTO EmissionActivity VALUES('utopia','co2','ethos','IMPHCO1',1990,'HCO',0.0889999999999999819,'',''); +INSERT INTO EmissionActivity VALUES('utopia','co2','ethos','IMPOIL1',1990,'OIL',0.07499999999999999723,'',''); +INSERT INTO EmissionActivity VALUES('utopia','nox','DSL','TXD',1970,'TX',1.0,'',''); +INSERT INTO EmissionActivity VALUES('utopia','nox','DSL','TXD',1980,'TX',1.0,'',''); +INSERT INTO EmissionActivity VALUES('utopia','nox','DSL','TXD',1990,'TX',1.0,'',''); +INSERT INTO EmissionActivity VALUES('utopia','nox','DSL','TXD',2000,'TX',1.0,'',''); +INSERT INTO EmissionActivity VALUES('utopia','nox','DSL','TXD',2010,'TX',1.0,'',''); +INSERT INTO EmissionActivity VALUES('utopia','nox','GSL','TXG',1970,'TX',1.0,'',''); +INSERT INTO EmissionActivity VALUES('utopia','nox','GSL','TXG',1980,'TX',1.0,'',''); +INSERT INTO EmissionActivity VALUES('utopia','nox','GSL','TXG',1990,'TX',1.0,'',''); +INSERT INTO EmissionActivity VALUES('utopia','nox','GSL','TXG',2000,'TX',1.0,'',''); +INSERT INTO EmissionActivity VALUES('utopia','nox','GSL','TXG',2010,'TX',1.0,'',''); +CREATE TABLE EmissionEmbodied +( + region TEXT, + emis_comm TEXT + REFERENCES Commodity (name), + tech TEXT + REFERENCES Technology (tech), + vintage INTEGER + REFERENCES TimePeriod (period), + value REAL, + units TEXT, + notes TEXT, + PRIMARY KEY (region, emis_comm, tech, vintage) +); +CREATE TABLE EmissionEndOfLife +( + region TEXT, + emis_comm TEXT + REFERENCES Commodity (name), + tech TEXT + REFERENCES Technology (tech), + vintage INTEGER + REFERENCES TimePeriod (period), + value REAL, + units TEXT, + notes TEXT, + PRIMARY KEY (region, emis_comm, tech, vintage) +); +CREATE TABLE ExistingCapacity +( + region TEXT, + tech TEXT + REFERENCES Technology (tech), + vintage INTEGER + REFERENCES TimePeriod (period), + capacity REAL, + units TEXT, + notes TEXT, + PRIMARY KEY (region, tech, vintage) +); +INSERT INTO ExistingCapacity VALUES('utopia','E01',1960,0.1749999999999999889,'',''); +INSERT INTO ExistingCapacity VALUES('utopia','E01',1970,0.1749999999999999889,'',''); +INSERT INTO ExistingCapacity VALUES('utopia','E01',1980,0.1499999999999999945,'',''); +INSERT INTO ExistingCapacity VALUES('utopia','E31',1980,0.1000000000000000055,'',''); +INSERT INTO ExistingCapacity VALUES('utopia','E51',1980,0.5,'',''); +INSERT INTO ExistingCapacity VALUES('utopia','E70',1960,0.05000000000000000277,'',''); +INSERT INTO ExistingCapacity VALUES('utopia','E70',1970,0.05000000000000000277,'',''); +INSERT INTO ExistingCapacity VALUES('utopia','E70',1980,0.2000000000000000111,'',''); +INSERT INTO ExistingCapacity VALUES('utopia','RHO',1970,12.5,'',''); +INSERT INTO ExistingCapacity VALUES('utopia','RHO',1980,12.5,'',''); +INSERT INTO ExistingCapacity VALUES('utopia','RL1',1980,5.599999999999999645,'',''); +INSERT INTO ExistingCapacity VALUES('utopia','TXD',1970,0.4000000000000000222,'',''); +INSERT INTO ExistingCapacity VALUES('utopia','TXD',1980,0.2000000000000000111,'',''); +INSERT INTO ExistingCapacity VALUES('utopia','TXG',1970,3.100000000000000088,'',''); +INSERT INTO ExistingCapacity VALUES('utopia','TXG',1980,1.5,'',''); +CREATE TABLE TechGroup +( + group_name TEXT + PRIMARY KEY, + notes TEXT +); +CREATE TABLE LoanLifetimeProcess +( + region TEXT, + tech TEXT + REFERENCES Technology (tech), + vintage INTEGER + REFERENCES TimePeriod (period), + lifetime REAL, + notes TEXT, + PRIMARY KEY (region, tech, vintage) +); +CREATE TABLE LoanRate +( + region TEXT, + tech TEXT + REFERENCES Technology (tech), + vintage INTEGER + REFERENCES TimePeriod (period), + rate REAL, + notes TEXT, + PRIMARY KEY (region, tech, vintage) +); +CREATE TABLE LifetimeProcess +( + region TEXT, + tech TEXT + REFERENCES Technology (tech), + vintage INTEGER + REFERENCES TimePeriod (period), + lifetime REAL, + notes TEXT, + PRIMARY KEY (region, tech, vintage) +); +INSERT INTO LifetimeProcess VALUES('utopia','RL1',1980,20.0,'#forexistingcap'); +INSERT INTO LifetimeProcess VALUES('utopia','TXD',1970,30.0,'#forexistingcap'); +INSERT INTO LifetimeProcess VALUES('utopia','TXD',1980,30.0,'#forexistingcap'); +INSERT INTO LifetimeProcess VALUES('utopia','TXG',1970,30.0,'#forexistingcap'); +INSERT INTO LifetimeProcess VALUES('utopia','TXG',1980,30.0,'#forexistingcap'); +CREATE TABLE LifetimeTech +( + region TEXT, + tech TEXT + REFERENCES Technology (tech), + lifetime REAL, + notes TEXT, + PRIMARY KEY (region, tech) +); +INSERT INTO LifetimeTech VALUES('utopia','E01',40.0,''); +INSERT INTO LifetimeTech VALUES('utopia','E21',40.0,''); +INSERT INTO LifetimeTech VALUES('utopia','E31',100.0,''); +INSERT INTO LifetimeTech VALUES('utopia','E51',100.0,''); +INSERT INTO LifetimeTech VALUES('utopia','E70',40.0,''); +INSERT INTO LifetimeTech VALUES('utopia','RHE',30.0,''); +INSERT INTO LifetimeTech VALUES('utopia','RHO',30.0,''); +INSERT INTO LifetimeTech VALUES('utopia','RL1',10.0,''); +INSERT INTO LifetimeTech VALUES('utopia','SRE',50.0,''); +INSERT INTO LifetimeTech VALUES('utopia','TXD',15.0,''); +INSERT INTO LifetimeTech VALUES('utopia','TXE',15.0,''); +INSERT INTO LifetimeTech VALUES('utopia','TXG',15.0,''); +INSERT INTO LifetimeTech VALUES('utopia','IMPDSL1',1000.0,''); +INSERT INTO LifetimeTech VALUES('utopia','IMPGSL1',1000.0,''); +INSERT INTO LifetimeTech VALUES('utopia','IMPHCO1',1000.0,''); +INSERT INTO LifetimeTech VALUES('utopia','IMPOIL1',1000.0,''); +INSERT INTO LifetimeTech VALUES('utopia','IMPURN1',1000.0,''); +INSERT INTO LifetimeTech VALUES('utopia','IMPHYD',1000.0,''); +INSERT INTO LifetimeTech VALUES('utopia','IMPFEQ',1000.0,''); +CREATE TABLE Operator +( + operator TEXT PRIMARY KEY, + notes TEXT +); +INSERT INTO Operator VALUES('e','equal to'); +INSERT INTO Operator VALUES('le','less than or equal to'); +INSERT INTO Operator VALUES('ge','greater than or equal to'); +CREATE TABLE LimitGrowthCapacity +( + region TEXT, + tech_or_group TEXT, + operator TEXT NOT NULL DEFAULT "le" + REFERENCES Operator (operator), + rate REAL NOT NULL DEFAULT 0, + seed REAL NOT NULL DEFAULT 0, + seed_units TEXT, + notes TEXT, + PRIMARY KEY (region, tech_or_group, operator) +); +CREATE TABLE LimitDegrowthCapacity +( + region TEXT, + tech_or_group TEXT, + operator TEXT NOT NULL DEFAULT "le" + REFERENCES Operator (operator), + rate REAL NOT NULL DEFAULT 0, + seed REAL NOT NULL DEFAULT 0, + seed_units TEXT, + notes TEXT, + PRIMARY KEY (region, tech_or_group, operator) +); +CREATE TABLE LimitGrowthNewCapacity +( + region TEXT, + tech_or_group TEXT, + operator TEXT NOT NULL DEFAULT "le" + REFERENCES Operator (operator), + rate REAL NOT NULL DEFAULT 0, + seed REAL NOT NULL DEFAULT 0, + seed_units TEXT, + notes TEXT, + PRIMARY KEY (region, tech_or_group, operator) +); +CREATE TABLE LimitDegrowthNewCapacity +( + region TEXT, + tech_or_group TEXT, + operator TEXT NOT NULL DEFAULT "le" + REFERENCES Operator (operator), + rate REAL NOT NULL DEFAULT 0, + seed REAL NOT NULL DEFAULT 0, + seed_units TEXT, + notes TEXT, + PRIMARY KEY (region, tech_or_group, operator) +); +CREATE TABLE LimitGrowthNewCapacityDelta +( + region TEXT, + tech_or_group TEXT, + operator TEXT NOT NULL DEFAULT "le" + REFERENCES Operator (operator), + rate REAL NOT NULL DEFAULT 0, + seed REAL NOT NULL DEFAULT 0, + seed_units TEXT, + notes TEXT, + PRIMARY KEY (region, tech_or_group, operator) +); +CREATE TABLE LimitDegrowthNewCapacityDelta +( + region TEXT, + tech_or_group TEXT, + operator TEXT NOT NULL DEFAULT "le" + REFERENCES Operator (operator), + rate REAL NOT NULL DEFAULT 0, + seed REAL NOT NULL DEFAULT 0, + seed_units TEXT, + notes TEXT, + PRIMARY KEY (region, tech_or_group, operator) +); +CREATE TABLE LimitStorageLevelFraction +( + region TEXT, + period INTEGER + REFERENCES TimePeriod (period), + season TEXT + REFERENCES SeasonLabel (season), + tod TEXT + REFERENCES TimeOfDay (tod), + tech TEXT + REFERENCES Technology (tech), + vintage INTEGER + REFERENCES TimePeriod (period), + operator TEXT NOT NULL DEFAULT "le" + REFERENCES Operator (operator), + fraction REAL, + notes TEXT, + PRIMARY KEY(region, period, season, tod, tech, vintage, operator) +); +CREATE TABLE LimitActivity +( + region TEXT, + period INTEGER + REFERENCES TimePeriod (period), + tech_or_group TEXT, + operator TEXT NOT NULL DEFAULT "le" + REFERENCES Operator (operator), + activity REAL, + units TEXT, + notes TEXT, + PRIMARY KEY (region, period, tech_or_group, operator) +); +CREATE TABLE LimitActivityShare +( + region TEXT, + period INTEGER + REFERENCES TimePeriod (period), + sub_group TEXT, + super_group TEXT, + operator TEXT NOT NULL DEFAULT "le" + REFERENCES Operator (operator), + share REAL, + notes TEXT, + PRIMARY KEY (region, period, sub_group, super_group, operator) +); +CREATE TABLE LimitAnnualCapacityFactor +( + region TEXT, + tech TEXT + REFERENCES Technology (tech), + vintage INTEGER + REFERENCES TimePeriod (period), + output_comm TEXT + REFERENCES Commodity (name), + operator TEXT NOT NULL DEFAULT "le" + REFERENCES Operator (operator), + factor REAL, + notes TEXT, + PRIMARY KEY (region, tech, vintage, operator), + CHECK (factor >= 0 AND factor <= 1) +); +CREATE TABLE LimitCapacity +( + region TEXT, + period INTEGER + REFERENCES TimePeriod (period), + tech_or_group TEXT, + operator TEXT NOT NULL DEFAULT "le" + REFERENCES Operator (operator), + capacity REAL, + units TEXT, + notes TEXT, + PRIMARY KEY (region, period, tech_or_group, operator) +); +INSERT INTO LimitCapacity VALUES('utopia',1990,'E31','ge',0.1300000000000000044,'',''); +INSERT INTO LimitCapacity VALUES('utopia',2000,'E31','ge',0.1300000000000000044,'',''); +INSERT INTO LimitCapacity VALUES('utopia',2010,'E31','ge',0.1300000000000000044,'',''); +INSERT INTO LimitCapacity VALUES('utopia',1990,'SRE','ge',0.1000000000000000055,'',''); +INSERT INTO LimitCapacity VALUES('utopia',1990,'E31','le',0.1300000000000000044,'',''); +INSERT INTO LimitCapacity VALUES('utopia',2000,'E31','le',0.1700000000000000122,'',''); +INSERT INTO LimitCapacity VALUES('utopia',2010,'E31','le',0.21000000000000002,'',''); +INSERT INTO LimitCapacity VALUES('utopia',1990,'RHE','le',0.0,'',''); +INSERT INTO LimitCapacity VALUES('utopia',1990,'TXD','le',0.5999999999999999778,'',''); +INSERT INTO LimitCapacity VALUES('utopia',2000,'TXD','le',1.760000000000000008,'',''); +INSERT INTO LimitCapacity VALUES('utopia',2010,'TXD','le',4.759999999999999787,'',''); +CREATE TABLE LimitCapacityShare +( + region TEXT, + period INTEGER + REFERENCES TimePeriod (period), + sub_group TEXT, + super_group TEXT, + operator TEXT NOT NULL DEFAULT "le" + REFERENCES Operator (operator), + share REAL, + notes TEXT, + PRIMARY KEY (region, period, sub_group, super_group, operator) +); +CREATE TABLE LimitNewCapacity +( + region TEXT, + period INTEGER + REFERENCES TimePeriod (period), + tech_or_group TEXT, + operator TEXT NOT NULL DEFAULT "le" + REFERENCES Operator (operator), + new_cap REAL, + units TEXT, + notes TEXT, + PRIMARY KEY (region, period, tech_or_group, operator) +); +CREATE TABLE LimitNewCapacityShare +( + region TEXT, + period INTEGER + REFERENCES TimePeriod (period), + sub_group TEXT, + super_group TEXT, + operator TEXT NOT NULL DEFAULT "le" + REFERENCES Operator (operator), + share REAL, + notes TEXT, + PRIMARY KEY (region, period, sub_group, super_group, operator) +); +CREATE TABLE LimitResource +( + region TEXT, + tech_or_group TEXT, + operator TEXT NOT NULL DEFAULT "le" + REFERENCES Operator (operator), + cum_act REAL, + units TEXT, + notes TEXT, + PRIMARY KEY (region, tech_or_group, operator) +); +CREATE TABLE LimitSeasonalCapacityFactor +( + region TEXT + REFERENCES Region (region), + period INTEGER + REFERENCES TimePeriod (period), + season TEXT + REFERENCES SeasonLabel (season), + tech TEXT + REFERENCES Technology (tech), + operator TEXT NOT NULL DEFAULT "le" + REFERENCES Operator (operator), + factor REAL, + notes TEXT, + PRIMARY KEY(region, period, season, tech, operator) +); +CREATE TABLE LimitTechInputSplit +( + region TEXT, + period INTEGER + REFERENCES TimePeriod (period), + input_comm TEXT + REFERENCES Commodity (name), + tech TEXT + REFERENCES Technology (tech), + operator TEXT NOT NULL DEFAULT "le" + REFERENCES Operator (operator), + proportion REAL, + notes TEXT, + PRIMARY KEY (region, period, input_comm, tech, operator) +); +CREATE TABLE LimitTechInputSplitAnnual +( + region TEXT, + period INTEGER + REFERENCES TimePeriod (period), + input_comm TEXT + REFERENCES Commodity (name), + tech TEXT + REFERENCES Technology (tech), + operator TEXT NOT NULL DEFAULT "le" + REFERENCES Operator (operator), + proportion REAL, + notes TEXT, + PRIMARY KEY (region, period, input_comm, tech, operator) +); +CREATE TABLE LimitTechOutputSplit +( + region TEXT, + period INTEGER + REFERENCES TimePeriod (period), + tech TEXT + REFERENCES Technology (tech), + output_comm TEXT + REFERENCES Commodity (name), + operator TEXT NOT NULL DEFAULT "le" + REFERENCES Operator (operator), + proportion REAL, + notes TEXT, + PRIMARY KEY (region, period, tech, output_comm, operator) +); +INSERT INTO LimitTechOutputSplit VALUES('utopia',1990,'SRE','DSL','ge',0.6999999999999999556,''); +INSERT INTO LimitTechOutputSplit VALUES('utopia',2000,'SRE','DSL','ge',0.6999999999999999556,''); +INSERT INTO LimitTechOutputSplit VALUES('utopia',2010,'SRE','DSL','ge',0.6999999999999999556,''); +INSERT INTO LimitTechOutputSplit VALUES('utopia',1990,'SRE','GSL','ge',0.2999999999999999889,''); +INSERT INTO LimitTechOutputSplit VALUES('utopia',2000,'SRE','GSL','ge',0.2999999999999999889,''); +INSERT INTO LimitTechOutputSplit VALUES('utopia',2010,'SRE','GSL','ge',0.2999999999999999889,''); +CREATE TABLE LimitTechOutputSplitAnnual +( + region TEXT, + period INTEGER + REFERENCES TimePeriod (period), + tech TEXT + REFERENCES Technology (tech), + output_comm TEXT + REFERENCES Commodity (name), + operator TEXT NOT NULL DEFAULT "le" + REFERENCES Operator (operator), + proportion REAL, + notes TEXT, + PRIMARY KEY (region, period, tech, output_comm, operator) +); +CREATE TABLE LimitEmission +( + region TEXT, + period INTEGER + REFERENCES TimePeriod (period), + emis_comm TEXT + REFERENCES Commodity (name), + operator TEXT NOT NULL DEFAULT "le" + REFERENCES Operator (operator), + value REAL, + units TEXT, + notes TEXT, + PRIMARY KEY (region, period, emis_comm, operator) +); +CREATE TABLE LinkedTech +( + primary_region TEXT, + primary_tech TEXT + REFERENCES Technology (tech), + emis_comm TEXT + REFERENCES Commodity (name), + driven_tech TEXT + REFERENCES Technology (tech), + notes TEXT, + PRIMARY KEY (primary_region, primary_tech, emis_comm) +); +CREATE TABLE OutputCurtailment +( + scenario TEXT, + region TEXT, + sector TEXT, + period INTEGER + REFERENCES TimePeriod (period), + season TEXT + REFERENCES TimePeriod (period), + tod TEXT + REFERENCES TimeOfDay (tod), + input_comm TEXT + REFERENCES Commodity (name), + tech TEXT + REFERENCES Technology (tech), + vintage INTEGER + REFERENCES TimePeriod (period), + output_comm TEXT + REFERENCES Commodity (name), + curtailment REAL, + PRIMARY KEY (region, scenario, period, season, tod, input_comm, tech, vintage, output_comm) +); +CREATE TABLE OutputNetCapacity +( + scenario TEXT, + region TEXT, + sector TEXT + REFERENCES SectorLabel (sector), + period INTEGER + REFERENCES TimePeriod (period), + tech TEXT + REFERENCES Technology (tech), + vintage INTEGER + REFERENCES TimePeriod (period), + capacity REAL, + PRIMARY KEY (region, scenario, period, tech, vintage) +); +CREATE TABLE OutputBuiltCapacity +( + scenario TEXT, + region TEXT, + sector TEXT + REFERENCES SectorLabel (sector), + tech TEXT + REFERENCES Technology (tech), + vintage INTEGER + REFERENCES TimePeriod (period), + capacity REAL, + PRIMARY KEY (region, scenario, tech, vintage) +); +CREATE TABLE OutputRetiredCapacity +( + scenario TEXT, + region TEXT, + sector TEXT + REFERENCES SectorLabel (sector), + period INTEGER + REFERENCES TimePeriod (period), + tech TEXT + REFERENCES Technology (tech), + vintage INTEGER + REFERENCES TimePeriod (period), + cap_eol REAL, + cap_early REAL, + PRIMARY KEY (region, scenario, period, tech, vintage) +); +CREATE TABLE OutputFlowIn +( + scenario TEXT, + region TEXT, + sector TEXT + REFERENCES SectorLabel (sector), + period INTEGER + REFERENCES TimePeriod (period), + season TEXT + REFERENCES SeasonLabel (season), + tod TEXT + REFERENCES TimeOfDay (tod), + input_comm TEXT + REFERENCES Commodity (name), + tech TEXT + REFERENCES Technology (tech), + vintage INTEGER + REFERENCES TimePeriod (period), + output_comm TEXT + REFERENCES Commodity (name), + flow REAL, + PRIMARY KEY (region, scenario, period, season, tod, input_comm, tech, vintage, output_comm) +); +CREATE TABLE OutputFlowOut +( + scenario TEXT, + region TEXT, + sector TEXT + REFERENCES SectorLabel (sector), + period INTEGER + REFERENCES TimePeriod (period), + season TEXT + REFERENCES SeasonLabel (season), + tod TEXT + REFERENCES TimeOfDay (tod), + input_comm TEXT + REFERENCES Commodity (name), + tech TEXT + REFERENCES Technology (tech), + vintage INTEGER + REFERENCES TimePeriod (period), + output_comm TEXT + REFERENCES Commodity (name), + flow REAL, + PRIMARY KEY (region, scenario, period, season, tod, input_comm, tech, vintage, output_comm) +); +CREATE TABLE OutputStorageLevel +( + scenario TEXT, + region TEXT, + sector TEXT + REFERENCES SectorLabel (sector), + period INTEGER + REFERENCES TimePeriod (period), + season TEXT + REFERENCES SeasonLabel (season), + tod TEXT + REFERENCES TimeOfDay (tod), + tech TEXT + REFERENCES Technology (tech), + vintage INTEGER + REFERENCES TimePeriod (period), + level REAL, + PRIMARY KEY (scenario, region, period, season, tod, tech, vintage) +); +CREATE TABLE PlanningReserveMargin +( + region TEXT + PRIMARY KEY + REFERENCES Region (region), + margin REAL, + notes TEXT +); +CREATE TABLE RampDownHourly +( + region TEXT, + tech TEXT + REFERENCES Technology (tech), + rate REAL, + notes TEXT, + PRIMARY KEY (region, tech) +); +CREATE TABLE RampUpHourly +( + region TEXT, + tech TEXT + REFERENCES Technology (tech), + rate REAL, + notes TEXT, + PRIMARY KEY (region, tech) +); +CREATE TABLE Region +( + region TEXT + PRIMARY KEY, + notes TEXT +); +INSERT INTO Region VALUES('utopia',NULL); +CREATE TABLE ReserveCapacityDerate +( + region TEXT, + period INTEGER + REFERENCES TimePeriod (period), + season TEXT + REFERENCES SeasonLabel (season), + tech TEXT + REFERENCES Technology (tech), + vintage INTEGER, + factor REAL, + notes TEXT, + PRIMARY KEY (region, period, season, tech, vintage), + CHECK (factor >= 0 AND factor <= 1) +); +CREATE TABLE TimeSegmentFraction +( + period INTEGER + REFERENCES TimePeriod (period), + season TEXT + REFERENCES SeasonLabel (season), + tod TEXT + REFERENCES TimeOfDay (tod), + segfrac REAL, + notes TEXT, + PRIMARY KEY (period, season, tod), + CHECK (segfrac >= 0 AND segfrac <= 1) +); +INSERT INTO TimeSegmentFraction VALUES(1990,'inter','day',0.166699999999999987,'# I-D'); +INSERT INTO TimeSegmentFraction VALUES(1990,'inter','night',0.08329999999999999905,'# I-N'); +INSERT INTO TimeSegmentFraction VALUES(1990,'summer','day',0.166699999999999987,'# S-D'); +INSERT INTO TimeSegmentFraction VALUES(1990,'summer','night',0.08329999999999999905,'# S-N'); +INSERT INTO TimeSegmentFraction VALUES(1990,'winter','day',0.3332999999999999852,'# W-D'); +INSERT INTO TimeSegmentFraction VALUES(1990,'winter','night',0.166699999999999987,'# W-N'); +INSERT INTO TimeSegmentFraction VALUES(2000,'inter','day',0.166699999999999987,'# I-D'); +INSERT INTO TimeSegmentFraction VALUES(2000,'inter','night',0.08329999999999999905,'# I-N'); +INSERT INTO TimeSegmentFraction VALUES(2000,'summer','day',0.166699999999999987,'# S-D'); +INSERT INTO TimeSegmentFraction VALUES(2000,'summer','night',0.08329999999999999905,'# S-N'); +INSERT INTO TimeSegmentFraction VALUES(2000,'winter','day',0.3332999999999999852,'# W-D'); +INSERT INTO TimeSegmentFraction VALUES(2000,'winter','night',0.166699999999999987,'# W-N'); +INSERT INTO TimeSegmentFraction VALUES(2010,'inter','day',0.166699999999999987,'# I-D'); +INSERT INTO TimeSegmentFraction VALUES(2010,'inter','night',0.08329999999999999905,'# I-N'); +INSERT INTO TimeSegmentFraction VALUES(2010,'summer','day',0.166699999999999987,'# S-D'); +INSERT INTO TimeSegmentFraction VALUES(2010,'summer','night',0.08329999999999999905,'# S-N'); +INSERT INTO TimeSegmentFraction VALUES(2010,'winter','day',0.3332999999999999852,'# W-D'); +INSERT INTO TimeSegmentFraction VALUES(2010,'winter','night',0.166699999999999987,'# W-N'); +CREATE TABLE StorageDuration +( + region TEXT, + tech TEXT, + duration REAL, + notes TEXT, + PRIMARY KEY (region, tech) +); +CREATE TABLE LifetimeSurvivalCurve +( + region TEXT NOT NULL, + period INTEGER NOT NULL, + tech TEXT NOT NULL + REFERENCES Technology (tech), + vintage INTEGER NOT NULL + REFERENCES TimePeriod (period), + fraction REAL, + notes TEXT, + PRIMARY KEY (region, period, tech, vintage) +); +CREATE TABLE TechnologyType +( + label TEXT + PRIMARY KEY, + description TEXT +); +INSERT INTO TechnologyType VALUES('p','production technology'); +INSERT INTO TechnologyType VALUES('pb','baseload production technology'); +INSERT INTO TechnologyType VALUES('ps','storage production technology'); +CREATE TABLE TimeOfDay +( + sequence INTEGER UNIQUE, + tod TEXT + PRIMARY KEY +); +INSERT INTO TimeOfDay VALUES(1,'day'); +INSERT INTO TimeOfDay VALUES(2,'night'); +CREATE TABLE TimePeriod +( + sequence INTEGER UNIQUE, + period INTEGER + PRIMARY KEY, + flag TEXT + REFERENCES TimePeriodType (label) +); +INSERT INTO TimePeriod VALUES(1,1960,'e'); +INSERT INTO TimePeriod VALUES(2,1970,'e'); +INSERT INTO TimePeriod VALUES(3,1980,'e'); +INSERT INTO TimePeriod VALUES(4,1990,'f'); +INSERT INTO TimePeriod VALUES(5,2000,'f'); +INSERT INTO TimePeriod VALUES(6,2010,'f'); +INSERT INTO TimePeriod VALUES(7,2020,'f'); +CREATE TABLE TimeSeason +( + period INTEGER + REFERENCES TimePeriod (period), + sequence INTEGER, + season TEXT + REFERENCES SeasonLabel (season), + notes TEXT, + PRIMARY KEY (period, sequence, season) +); +INSERT INTO TimeSeason VALUES(1990,1,'inter',NULL); +INSERT INTO TimeSeason VALUES(1990,2,'summer',NULL); +INSERT INTO TimeSeason VALUES(1990,3,'winter',NULL); +INSERT INTO TimeSeason VALUES(2000,1,'inter',NULL); +INSERT INTO TimeSeason VALUES(2000,2,'summer',NULL); +INSERT INTO TimeSeason VALUES(2000,3,'winter',NULL); +INSERT INTO TimeSeason VALUES(2010,1,'inter',NULL); +INSERT INTO TimeSeason VALUES(2010,2,'summer',NULL); +INSERT INTO TimeSeason VALUES(2010,3,'winter',NULL); +CREATE TABLE TimeSeasonSequential +( + period INTEGER + REFERENCES TimePeriod (period), + sequence INTEGER, + seas_seq TEXT, + season TEXT + REFERENCES SeasonLabel (season), + num_days REAL NOT NULL, + notes TEXT, + PRIMARY KEY (period, sequence, seas_seq, season), + CHECK (num_days > 0) +); +CREATE TABLE TimePeriodType +( + label TEXT + PRIMARY KEY, + description TEXT +); +INSERT INTO TimePeriodType VALUES('e','existing vintages'); +INSERT INTO TimePeriodType VALUES('f','future'); +CREATE TABLE OutputEmission +( + scenario TEXT, + region TEXT, + sector TEXT + REFERENCES SectorLabel (sector), + period INTEGER + REFERENCES TimePeriod (period), + emis_comm TEXT + REFERENCES Commodity (name), + tech TEXT + REFERENCES Technology (tech), + vintage INTEGER + REFERENCES TimePeriod (period), + emission REAL, + PRIMARY KEY (region, scenario, period, emis_comm, tech, vintage) +); +CREATE TABLE RPSRequirement +( + region TEXT NOT NULL + REFERENCES Region (region), + period INTEGER NOT NULL + REFERENCES TimePeriod (period), + tech_group TEXT NOT NULL + REFERENCES TechGroup (group_name), + requirement REAL NOT NULL, + notes TEXT +); +CREATE TABLE TechGroupMember +( + group_name TEXT + REFERENCES TechGroup (group_name), + tech TEXT + REFERENCES Technology (tech), + PRIMARY KEY (group_name, tech) +); +CREATE TABLE Technology +( + tech TEXT NOT NULL PRIMARY KEY, + flag TEXT NOT NULL, + sector TEXT, + category TEXT, + sub_category TEXT, + unlim_cap INTEGER NOT NULL DEFAULT 0, + annual INTEGER NOT NULL DEFAULT 0, + reserve INTEGER NOT NULL DEFAULT 0, + curtail INTEGER NOT NULL DEFAULT 0, + retire INTEGER NOT NULL DEFAULT 0, + flex INTEGER NOT NULL DEFAULT 0, + exchange INTEGER NOT NULL DEFAULT 0, + seas_stor INTEGER NOT NULL DEFAULT 0, + description TEXT, + FOREIGN KEY (flag) REFERENCES TechnologyType (label) +); +INSERT INTO Technology VALUES('IMPDSL1','p','supply','petroleum','',1,0,0,0,0,0,0,0,' imported diesel'); +INSERT INTO Technology VALUES('IMPGSL1','p','supply','petroleum','',1,0,0,0,0,0,0,0,' imported gasoline'); +INSERT INTO Technology VALUES('IMPHCO1','p','supply','coal','',1,0,0,0,0,0,0,0,' imported coal'); +INSERT INTO Technology VALUES('IMPOIL1','p','supply','petroleum','',1,0,0,0,0,0,0,0,' imported crude oil'); +INSERT INTO Technology VALUES('IMPURN1','p','supply','nuclear','',1,0,0,0,0,0,0,0,' imported uranium'); +INSERT INTO Technology VALUES('IMPFEQ','p','supply','petroleum','',1,0,0,0,0,0,0,0,' imported fossil equivalent'); +INSERT INTO Technology VALUES('IMPHYD','p','supply','hydro','',1,0,0,0,0,0,0,0,' imported water -- doesnt exist in Utopia'); +INSERT INTO Technology VALUES('E01','pb','electric','coal','',0,0,0,0,0,0,0,0,' coal power plant'); +INSERT INTO Technology VALUES('E21','pb','electric','nuclear','',0,0,0,0,0,0,0,0,' nuclear power plant'); +INSERT INTO Technology VALUES('E31','pb','electric','hydro','',0,0,0,0,0,0,0,0,' hydro power'); +INSERT INTO Technology VALUES('E51','ps','electric','electric','',0,0,0,0,0,0,0,0,' electric storage'); +INSERT INTO Technology VALUES('E70','p','electric','petroleum','',0,0,0,0,0,0,0,0,' diesel power plant'); +INSERT INTO Technology VALUES('RHE','p','residential','electric','',0,0,0,0,0,0,0,0,' electric residential heating'); +INSERT INTO Technology VALUES('RHO','p','residential','petroleum','',0,0,0,0,0,0,0,0,' diesel residential heating'); +INSERT INTO Technology VALUES('RL1','p','residential','electric','',0,0,0,0,0,0,0,0,' residential lighting'); +INSERT INTO Technology VALUES('SRE','p','supply','petroleum','',0,0,0,0,0,0,0,0,' crude oil processor'); +INSERT INTO Technology VALUES('TXD','p','transport','petroleum','',0,0,0,0,0,0,0,0,' diesel powered vehicles'); +INSERT INTO Technology VALUES('TXE','p','transport','electric','',0,0,0,0,0,0,0,0,' electric powered vehicles'); +INSERT INTO Technology VALUES('TXG','p','transport','petroleum','',0,0,0,0,0,0,0,0,' gasoline powered vehicles'); +CREATE TABLE OutputCost +( + scenario TEXT, + region TEXT, + sector TEXT REFERENCES SectorLabel (sector), + period INTEGER REFERENCES TimePeriod (period), + tech TEXT REFERENCES Technology (tech), + vintage INTEGER REFERENCES TimePeriod (period), + d_invest REAL, + d_fixed REAL, + d_var REAL, + d_emiss REAL, + invest REAL, + fixed REAL, + var REAL, + emiss REAL, + PRIMARY KEY (scenario, region, period, tech, vintage), + FOREIGN KEY (vintage) REFERENCES TimePeriod (period), + FOREIGN KEY (tech) REFERENCES Technology (tech) +); +COMMIT; From ddaf3cffc3eaba9140d6bb565374aa874a17a30e Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Mon, 29 Jun 2026 16:07:14 -0400 Subject: [PATCH 15/28] Create a framework for extending model components Signed-off-by: Davey Elder --- docs/source/computational_implementation.rst | 5 + docs/source/extensions.rst | 232 ++++++++++++++++++ docs/source/index.rst | 1 + temoa/_internal/run_actions.py | 3 +- temoa/_internal/temoa_sequencer.py | 8 +- temoa/core/config.py | 6 + temoa/core/model.py | 13 +- temoa/data_io/component_manifest.py | 8 +- temoa/data_io/hybrid_loader.py | 29 ++- temoa/extensions/framework.py | 218 ++++++++++++++++ temoa/extensions/method_of_morris/morris.py | 2 +- .../method_of_morris/morris_evaluate.py | 6 +- .../mga_sequencer.py | 5 +- temoa/extensions/myopic/myopic_sequencer.py | 1 + .../single_vector_mga/sv_mga_sequencer.py | 1 + .../stochastics/scenario_creator.py | 2 +- temoa/extensions/template/__init__.py | 21 ++ .../template/components/__init__.py | 0 .../template/components/example_limit.py | 62 +++++ temoa/extensions/template/core/__init__.py | 0 temoa/extensions/template/core/model.py | 71 ++++++ temoa/extensions/template/data_manifest.py | 44 ++++ temoa/extensions/template/extension.py | 34 +++ temoa/extensions/template/tables.sql | 14 ++ temoa/tutorial_assets/config_sample.toml | 6 + tests/conftest.py | 8 + 26 files changed, 785 insertions(+), 15 deletions(-) create mode 100644 docs/source/extensions.rst create mode 100644 temoa/extensions/framework.py create mode 100644 temoa/extensions/template/__init__.py create mode 100644 temoa/extensions/template/components/__init__.py create mode 100644 temoa/extensions/template/components/example_limit.py create mode 100644 temoa/extensions/template/core/__init__.py create mode 100644 temoa/extensions/template/core/model.py create mode 100644 temoa/extensions/template/data_manifest.py create mode 100644 temoa/extensions/template/extension.py create mode 100644 temoa/extensions/template/tables.sql diff --git a/docs/source/computational_implementation.rst b/docs/source/computational_implementation.rst index 023964f3..94345276 100644 --- a/docs/source/computational_implementation.rst +++ b/docs/source/computational_implementation.rst @@ -320,6 +320,11 @@ dimensionality of 3, and (following the :ref:`naming scheme :code:`region`, the second an element of :code:`time_optimize`, and third a demand commodity. +.. seealso:: + The same component pattern (index set, parameter, and constraint declared in a + ``model.py`` and implemented in component modules) can be packaged as an + optional, self-contained add-on. See :ref:`extensions` for the extension + framework and a copy-from template. diff --git a/docs/source/extensions.rst b/docs/source/extensions.rst new file mode 100644 index 00000000..5e8e1a9c --- /dev/null +++ b/docs/source/extensions.rst @@ -0,0 +1,232 @@ +.. _extensions: + +Extension Framework +=================== + +Temoa includes a lightweight framework for adding **optional model components** +without modifying the core model. An extension can contribute its own database +tables, Pyomo components (sets, parameters, and constraints), and data-loading +rules. Extensions are declared once and then enabled per run through +configuration. + +This page describes how the framework works and how to author a new extension. +A ready-to-copy scaffold lives at ``temoa/extensions/template``. + +Overview +-------- + +Use an extension when you want to add modeling capability that is: + +* **Optional** -- only active when explicitly enabled. +* **Self-contained** -- owns its own tables and model components. +* **Non-invasive** -- adds to the core model rather than editing it. + +If a feature is fundamental to every Temoa run, it belongs in +``temoa/components`` (a core component) instead of an extension. + +Each extension is described by a single :class:`ExtensionSpec` (declarative +metadata plus hook functions). The spec is registered with the framework, after +which users can enable the extension by id. + +Lifecycle +--------- + +When a run is configured with ``extensions = ["..."]``, the framework threads the +enabled specs through configuration, model construction, and data loading: + +.. code-block:: text + + config: extensions = ["my_ext"] + | + v + resolve_extension_specs() validate ids -> ExtensionSpec list + | + v + TemoaModel(extensions=[...]) + | + +--> apply_model_extension_hooks() + | calls spec.register_model_components(model) + | -> attaches Params / Sets / Constraints to the model + | + v + HybridLoader + +--> ensure_enabled_extension_tables_exist() (offer to append schema) + +--> assert_disabled_extension_tables_are_empty() + +--> merge_regional_group_tables() + +--> build_manifest() -> appends spec.build_manifest_items(model) + -> loads each owned table into its component + +The relevant code lives in :mod:`temoa.extensions.framework`, +:mod:`temoa.core.model`, and :mod:`temoa.data_io.hybrid_loader`. + +``ExtensionSpec`` reference +--------------------------- + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Field + - Purpose + * - ``extension_id`` + - Unique, lowercase id. This is what users put in ``extensions = [...]``. + * - ``owned_tables`` + - Tuple of database tables owned exclusively by this extension. Used by the + disabled/enabled table guards. + * - ``regional_group_tables`` + - Map of ``table -> column`` for tables whose region column may hold a + regional *group* name. Merged into the loader's regional-group handling. + * - ``register_model_components`` + - Hook ``Callable[[TemoaModel], None]`` that attaches model components. + * - ``build_manifest_items`` + - Hook ``Callable[[TemoaModel], list[LoadItem]]`` describing how to load the + extension's data. + * - ``schema_sql_path`` + - Path to a ``.sql`` file applied (with consent) when the extension is + enabled but its tables are missing. + * - ``fail_if_tables_populated_when_disabled`` + - When ``True``, loading fails if the extension is disabled but its owned + tables contain data, preventing silently-ignored inputs. + +Recommended package layout +-------------------------- + +Mirror the structure of the core model (``temoa/core`` + ``temoa/components``) so +extension code is organized the same way as the rest of the codebase: + +.. code-block:: text + + temoa/extensions// + __init__.py # re-export the ExtensionSpec + extension.py # the ExtensionSpec definition + data_manifest.py # build_manifest_items() + tables.sql # CREATE TABLE IF NOT EXISTS for owned tables + core/ + __init__.py + model.py # typing subtype + register_model_components() + components/ + __init__.py + .py # one module per constraint family + +Centralize the component *declarations* in ``core/model.py`` (just as +``temoa/core/model.py`` does) and keep the index-set and constraint-rule logic in +``components/`` modules (just as ``temoa/components`` does). + +.. _extensions-typing: + +The typing pattern +------------------ + +Core model components are declared as attribute assignments inside +``TemoaModel.__init__`` (for example ``self.time_optimize = Set(...)``), which is +why the type checker knows about them. An extension instead adds attributes from +*outside* the class, so without help the type checker reports +``"TemoaModel" has no attribute ...`` and provides no autocomplete. + +Three rules make typing carry over cleanly: + +1. **Declare a ``TYPE_CHECKING``-only subtype.** In ``core/model.py``, define a + subclass of ``TemoaModel`` that annotates every component the extension adds. + It inherits all core attributes, so component code sees both core and + extension members. + + .. code-block:: python + + if TYPE_CHECKING: + from temoa.core.model import TemoaModel + + class ExampleModel(TemoaModel): + example_new_capacity_limit: Param + example_new_capacity_limit_constraint_rpt: Set + example_new_capacity_limit_constraint: Constraint + +2. **Annotate component functions with the subtype.** Index-set and + constraint-rule functions take ``model: ExampleModel``. This restores both + mypy coverage and editor autocomplete. + +3. **Keep spec hooks on the base type and ``cast`` internally.** Functions stored + on the ``ExtensionSpec`` (``register_model_components`` and + ``build_manifest_items``) must keep ``model: TemoaModel`` to match the hook + callable types. Narrowing the parameter is a contravariance error. ``cast`` + once at the top: + + .. code-block:: python + + def register_model_components(model: TemoaModel) -> None: + m = cast('ExampleModel', model) + m.example_new_capacity_limit = Param(...) + +.. note:: + Extension attribute names share the single ``TemoaModel`` namespace at + runtime. Keep them unique across extensions to avoid collisions. + +Adding a new extension +---------------------- + +#. **Copy the template.** Duplicate ``temoa/extensions/template`` to + ``temoa/extensions//``. +#. **Rename ids and tables.** Update ``extension_id``, ``owned_tables``, + ``regional_group_tables``, params, sets, and constraints to your domain. +#. **Declare components.** Add annotations to the typing subtype in + ``core/model.py`` and create the components in ``register_model_components``. +#. **Write the constraint logic.** Add index-set and rule functions under + ``components/`` and annotate them with your subtype. +#. **Describe data loading.** Add one ``LoadItem`` per owned table in + ``data_manifest.py``. +#. **Define the schema.** Add a ``CREATE TABLE IF NOT EXISTS`` per owned table to + ``tables.sql``. +#. **Register the spec.** Import your spec in + :func:`temoa.extensions.framework.get_known_extension_specs` and add it to the + ``specs`` list. (Until you do this, the extension is inert -- enabling it + raises an "Unknown extension id" error.) +#. **Enable it.** Add the id to your configuration TOML: + + .. code-block:: toml + + extensions = [""] + +Data loading and ``LoadItem`` +----------------------------- + +``build_manifest_items`` returns one :class:`temoa.data_io.loader_manifest.LoadItem` +per database table the extension reads. Common fields: + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Field + - Purpose + * - ``component`` + - The Pyomo ``Set`` or ``Param`` to populate. + * - ``table`` + - Source table name in the database. + * - ``columns`` + - Columns to select; for a ``Param`` the final column is the value. + * - ``index_length`` + - Number of leading columns that form the index. + * - ``validator_name`` / ``validation_map`` + - Source-trace validation: a viable-set name on the loader and which index + columns it applies to. + * - ``is_table_required`` + - Set ``False`` for optional extension inputs so a missing table is not an + error. + +Verification +------------ + +After authoring an extension, confirm: + +#. **Types** -- ``mypy temoa/extensions/`` reports no issues. +#. **Imports** -- ``python -c "import temoa.extensions..extension"``. +#. **Wiring** -- a model built with the extension enabled attaches the expected + components, and the test suite passes. + +The template extension +----------------------- + +``temoa/extensions/template`` is a complete, type-checked, but deliberately +**unregistered** scaffold. Because it is not listed in +``get_known_extension_specs``, it cannot be enabled until you register it, so it +never affects normal runs. Copy the folder as the starting point for a new +extension; every file carries ``# TEMPLATE:`` comments explaining what to change. diff --git a/docs/source/index.rst b/docs/source/index.rst index f9043f39..3d7f9e3b 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -27,6 +27,7 @@ Temoa Project Documentation myopic stochastics unit_checking + extensions .. toctree:: :maxdepth: 1 diff --git a/temoa/_internal/run_actions.py b/temoa/_internal/run_actions.py index d0299a1b..af2bc0dd 100644 --- a/temoa/_internal/run_actions.py +++ b/temoa/_internal/run_actions.py @@ -124,6 +124,7 @@ def build_instance( silent: bool = False, keep_lp_file: bool = False, lp_path: Path | None = None, + extensions: Iterable[str] | None = None, ) -> TemoaModel: """ Build a Temoa Instance from data @@ -134,7 +135,7 @@ def build_instance( :param model_name: Optional name for this instance :return: a built TemoaModel """ - model = TemoaModel() + model = TemoaModel(extensions=tuple(extensions or ())) model.dual = Suffix(direction=Suffix.IMPORT) # self.model.rc = Suffix(direction=Suffix.IMPORT) diff --git a/temoa/_internal/temoa_sequencer.py b/temoa/_internal/temoa_sequencer.py index 08ae1290..bd44044e 100644 --- a/temoa/_internal/temoa_sequencer.py +++ b/temoa/_internal/temoa_sequencer.py @@ -140,7 +140,11 @@ def build_model(self) -> TemoaModel: tune_sqlite_connection(con, self.config) hybrid_loader = HybridLoader(db_connection=con, config=self.config) data_portal = hybrid_loader.load_data_portal(myopic_index=None) - instance = build_instance(data_portal, silent=self.config.silent) + instance = build_instance( + data_portal, + silent=self.config.silent, + extensions=self.config.extensions, + ) logger.info('Model build process complete.') return instance @@ -220,6 +224,7 @@ def _run_check_mode(self) -> None: silent=self.config.silent, keep_lp_file=self.config.save_lp_file, lp_path=self.config.output_path, + extensions=self.config.extensions, ) if not self.config.price_check: logger.warning('Price check is automatically enabled for CHECK mode.') @@ -238,6 +243,7 @@ def _run_perfect_foresight(self) -> None: silent=self.config.silent, keep_lp_file=self.config.save_lp_file, lp_path=self.config.output_path, + extensions=self.config.extensions, ) if self.config.price_check: price_checker(instance) diff --git a/temoa/core/config.py b/temoa/core/config.py index de3e3430..449024bd 100644 --- a/temoa/core/config.py +++ b/temoa/core/config.py @@ -5,6 +5,7 @@ from pathlib import Path from temoa.core.modes import TemoaMode +from temoa.extensions.framework import normalize_extension_ids, resolve_extension_specs logger = getLogger(__name__) @@ -71,6 +72,7 @@ def __init__( output_threshold_emission: float | None = None, output_threshold_cost: float | None = None, sqlite: dict[str, object] | None = None, + extensions: list[str] | tuple[str, ...] | None = None, ): if '-' in scenario: raise ValueError( @@ -160,6 +162,9 @@ def __init__( self.output_threshold_emission = output_threshold_emission self.output_threshold_cost = output_threshold_cost self.sqlite_inputs = sqlite or {} + self.extensions = normalize_extension_ids(extensions) + # Validate extension ids eagerly so config failures happen before model build. + resolve_extension_specs(self.extensions) # SQLite performance settings # journal_mode: DELETE | TRUNCATE | PERSIST | MEMORY | WAL | OFF @@ -326,6 +331,7 @@ def __repr__(self) -> str: msg += '{:>{}s}: {}\n'.format('Scenario', width, self.scenario) msg += '{:>{}s}: {}\n'.format('Scenario mode', width, self.scenario_mode.name) + msg += '{:>{}s}: {}\n'.format('Enabled extensions', width, ', '.join(self.extensions)) msg += '{:>{}s}: {}\n'.format('Config file', width, self.config_file) msg += '{:>{}s}: {}\n'.format('Data source', width, self.input_database) msg += '{:>{}s}: {}\n'.format('Output database target', width, self.output_database) diff --git a/temoa/core/model.py b/temoa/core/model.py index 85f48ea3..29a801de 100755 --- a/temoa/core/model.py +++ b/temoa/core/model.py @@ -9,6 +9,7 @@ """ import logging +from collections.abc import Sequence from typing import TYPE_CHECKING from pyomo.core import BuildCheck, Set, Var @@ -39,6 +40,7 @@ technology, time, ) +from temoa.extensions.framework import apply_model_extension_hooks, resolve_extension_specs from temoa.model_checking.validators import ( no_slash_or_pipe, region_check, @@ -100,8 +102,15 @@ class TemoaModel(AbstractModel): # this is used in several places outside this class, and this provides no-build access to it default_lifetime_tech = 40 - def __init__(self, *args: object, **kwargs: object) -> None: + def __init__( + self, + *args: object, + extensions: Sequence[str] | None = None, + **kwargs: object, + ) -> None: AbstractModel.__init__(self, *args, **kwargs) + self.enabled_extensions = tuple(extensions or ()) + self.extension_specs = resolve_extension_specs(self.enabled_extensions) ################################################ # Internally used Data Containers # @@ -1150,6 +1159,8 @@ def __init__(self, *args: object, **kwargs: object) -> None: rule=emissions.linked_emissions_tech_constraint, ) + apply_model_extension_hooks(self, self.extension_specs) + self.progress_marker_9 = BuildAction(['Finished Constraints'], rule=progress_check) diff --git a/temoa/data_io/component_manifest.py b/temoa/data_io/component_manifest.py index 7a97e2ba..4f7334c4 100644 --- a/temoa/data_io/component_manifest.py +++ b/temoa/data_io/component_manifest.py @@ -16,11 +16,14 @@ to add a new `LoadItem` to this manifest. """ +from collections.abc import Sequence + from temoa.core.model import TemoaModel from temoa.data_io.loader_manifest import LoadItem +from temoa.extensions.framework import append_extension_manifest_items, resolve_extension_specs -def build_manifest(model: TemoaModel) -> list[LoadItem]: +def build_manifest(model: TemoaModel, extension_ids: Sequence[str] | None = None) -> list[LoadItem]: """ Builds the manifest of all data components to be loaded into the Pyomo model. @@ -794,4 +797,5 @@ def build_manifest(model: TemoaModel) -> list[LoadItem]: is_table_required=False, ), ] - return manifest + extension_specs = resolve_extension_specs(extension_ids) + return append_extension_manifest_items(model, manifest, extension_specs) diff --git a/temoa/data_io/hybrid_loader.py b/temoa/data_io/hybrid_loader.py index f657ed68..1a70ccba 100644 --- a/temoa/data_io/hybrid_loader.py +++ b/temoa/data_io/hybrid_loader.py @@ -34,6 +34,12 @@ from temoa.core.model import TemoaModel from temoa.core.modes import TemoaMode from temoa.data_io.component_manifest import build_manifest +from temoa.extensions.framework import ( + assert_disabled_extension_tables_are_empty, + ensure_enabled_extension_tables_exist, + merge_regional_group_tables, + resolve_extension_specs, +) from temoa.extensions.myopic.myopic_index import MyopicIndex from temoa.model_checking import element_checker, network_model_data from temoa.model_checking.commodity_network_manager import CommodityNetworkManager @@ -49,7 +55,7 @@ logger = getLogger(__name__) # A manifest of tables that may contain region groups, used by a custom loader. -tables_with_regional_groups = { +BASE_REGIONAL_GROUP_TABLES = { 'limit_annual_capacity_factor': 'region', 'limit_emission': 'region', 'limit_seasonal_capacity_factor': 'region', @@ -90,10 +96,21 @@ def __init__(self, db_connection: Connection, config: TemoaConfig) -> None: self.con = db_connection self.config = config self.myopic_index: MyopicIndex | None = None + self.extension_specs = resolve_extension_specs(self.config.extensions) + self.model = TemoaModel(extensions=self.config.extensions) + self.tables_with_regional_groups = merge_regional_group_tables( + BASE_REGIONAL_GROUP_TABLES, self.extension_specs + ) + ensure_enabled_extension_tables_exist( + self.con, + self.extension_specs, + input_database=str(self.config.input_database), + silent=self.config.silent, + ) + assert_disabled_extension_tables_are_empty(self.con, self.extension_specs) # Build the data loading manifest and a name-based map for quick lookup - model = TemoaModel() - self.manifest = build_manifest(model) + self.manifest = build_manifest(self.model, extension_ids=self.config.extensions) self.manifest_map = {item.component.name: item for item in self.manifest} # --- Data containers and filters populated during loading --- @@ -205,7 +222,7 @@ def create_data_dict(self, myopic_index: MyopicIndex | None = None) -> dict[str, data: dict[str, object] = {} cur = self.con.cursor() - model = TemoaModel() + model = self.model # Load critical time sets first, as they index other components if myopic_index: @@ -525,7 +542,7 @@ def _load_regional_global_indices( """ Aggregates region and group names from the Region table and all Limit tables. """ - model = TemoaModel() + model = self.model cur = self.con.cursor() regions_and_groups: set[str] = set() @@ -534,7 +551,7 @@ def _load_regional_global_indices( t[0] for t in cur.execute('SELECT region FROM main.region').fetchall() ) - for table, field_name in tables_with_regional_groups.items(): + for table, field_name in self.tables_with_regional_groups.items(): if self.table_exists(table): regions_and_groups.update( t[0] for t in cur.execute(f'SELECT {field_name} FROM main.{table}').fetchall() diff --git a/temoa/extensions/framework.py b/temoa/extensions/framework.py new file mode 100644 index 00000000..9a655111 --- /dev/null +++ b/temoa/extensions/framework.py @@ -0,0 +1,218 @@ +from __future__ import annotations + +from pathlib import Path +from dataclasses import dataclass, field +from sqlite3 import Connection +from typing import TYPE_CHECKING + +from collections.abc import Callable + +if TYPE_CHECKING: + from collections.abc import Mapping, Sequence + + from temoa.core.model import TemoaModel + from temoa.data_io.loader_manifest import LoadItem + +ModelHook = Callable[['TemoaModel'], None] +ManifestHook = Callable[['TemoaModel'], list['LoadItem']] + + +@dataclass(frozen=True) +class ExtensionSpec: + """Declarative metadata and hooks for an optional modeling extension.""" + + extension_id: str + owned_tables: tuple[str, ...] = () + regional_group_tables: dict[str, str] = field(default_factory=dict) + register_model_components: ModelHook | None = None + build_manifest_items: ManifestHook | None = None + schema_sql_path: str | None = None + fail_if_tables_populated_when_disabled: bool = False + + +def normalize_extension_ids(extension_ids: Sequence[str] | None) -> tuple[str, ...]: + """Normalize configured extension ids while preserving user-provided order.""" + if not extension_ids: + return () + + normalized: list[str] = [] + seen: set[str] = set() + for ext_id in extension_ids: + if not isinstance(ext_id, str): + raise ValueError(f'Extension ids must be strings. Received: {type(ext_id).__name__}') + cleaned = ext_id.strip().lower() + if not cleaned: + continue + if cleaned not in seen: + normalized.append(cleaned) + seen.add(cleaned) + + return tuple(normalized) + + +def get_known_extension_specs() -> dict[str, ExtensionSpec]: + """Return all extension specs known to this installation.""" + from temoa.extensions.growth_rates.extension import GROWTH_RATES_EXTENSION + + # TEMPLATE: To activate a new extension copied from ``temoa/extensions/template``, + # import its spec here and add it to ``specs`` below, e.g.: + # from temoa.extensions..extension import + # specs = [GROWTH_RATES_EXTENSION, ] + specs = [GROWTH_RATES_EXTENSION] + return {spec.extension_id: spec for spec in specs} + + +def resolve_extension_specs(extension_ids: Sequence[str] | None) -> tuple[ExtensionSpec, ...]: + """Validate enabled extension ids and return corresponding specs in user order.""" + normalized_ids = normalize_extension_ids(extension_ids) + known = get_known_extension_specs() + unknown = [ext_id for ext_id in normalized_ids if ext_id not in known] + if unknown: + known_ids = ', '.join(sorted(known)) + unknown_ids = ', '.join(sorted(unknown)) + raise ValueError( + f'Unknown extension id(s): {unknown_ids}. Known extension ids: {known_ids}.' + ) + + return tuple(known[ext_id] for ext_id in normalized_ids) + + +def apply_model_extension_hooks(model: TemoaModel, specs: Sequence[ExtensionSpec]) -> None: + """Attach extension-owned model components to a model instance.""" + for spec in specs: + if spec.register_model_components is not None: + spec.register_model_components(model) + + +def append_extension_manifest_items( + model: TemoaModel, manifest: list[LoadItem], specs: Sequence[ExtensionSpec] +) -> list[LoadItem]: + """Append extension-specific manifest items to the base manifest.""" + merged = list(manifest) + for spec in specs: + if spec.build_manifest_items is not None: + merged.extend(spec.build_manifest_items(model)) + return merged + + +def merge_regional_group_tables( + base_tables: Mapping[str, str], specs: Sequence[ExtensionSpec] +) -> dict[str, str]: + """Merge base regional-group table map with extension-contributed entries.""" + merged = dict(base_tables) + for spec in specs: + for table_name, field_name in spec.regional_group_tables.items(): + existing = merged.get(table_name) + if existing is not None and existing != field_name: + raise ValueError( + f"Regional-group table '{table_name}' has conflicting field mappings: " + f"'{existing}' vs '{field_name}' from extension '{spec.extension_id}'." + ) + merged[table_name] = field_name + return merged + + +def assert_disabled_extension_tables_are_empty( + con: Connection, enabled_specs: Sequence[ExtensionSpec] +) -> None: + """Fail if disabled extensions with strict guards own tables populated with data.""" + enabled_ids = {spec.extension_id for spec in enabled_specs} + for spec in get_known_extension_specs().values(): + if spec.extension_id in enabled_ids: + continue + if not spec.fail_if_tables_populated_when_disabled: + continue + + populated: list[str] = [] + for table in spec.owned_tables: + if _table_has_rows(con, table): + populated.append(table) + + if populated: + table_list = ', '.join(sorted(populated)) + raise RuntimeError( + f"Extension '{spec.extension_id}' is not enabled, but extension-owned table(s) " + f'contain data: {table_list}. Enable the extension or remove those rows.' + ) + + +def ensure_enabled_extension_tables_exist( + con: Connection, + enabled_specs: Sequence[ExtensionSpec], + *, + input_database: str, + silent: bool, +) -> None: + """Ensure enabled extensions have required tables, offering to append schema if missing.""" + for spec in enabled_specs: + missing_tables = [table for table in spec.owned_tables if not _table_exists(con, table)] + if not missing_tables: + continue + + missing_list = ', '.join(sorted(missing_tables)) + if not spec.schema_sql_path: + raise RuntimeError( + f"Extension '{spec.extension_id}' is enabled, but required table(s) are missing: " + f'{missing_list}. No schema SQL path is registered for this extension.' + ) + + should_apply = False + if not silent: + prompt = ( + f"Extension '{spec.extension_id}' is enabled but missing table(s): {missing_list}. " + f"Append schema from '{spec.schema_sql_path}' to input database '{input_database}' now? " + '[y/N]: ' + ) + response = input(prompt).strip().lower() + should_apply = response in {'y', 'yes'} + + if not should_apply: + raise RuntimeError( + f"Extension '{spec.extension_id}' is enabled, but required table(s) are missing: " + f"{missing_list}. Re-run and accept the prompt, or append schema manually from " + f"'{spec.schema_sql_path}' to '{input_database}'." + ) + + _append_extension_schema(con, spec) + still_missing = [table for table in spec.owned_tables if not _table_exists(con, table)] + if still_missing: + still_missing_list = ', '.join(sorted(still_missing)) + raise RuntimeError( + f"Schema append for extension '{spec.extension_id}' completed but table(s) are still " + f'missing: {still_missing_list}.' + ) + + +def _table_has_rows(con: Connection, table_name: str) -> bool: + cur = con.cursor() + table_exists = cur.execute( + "SELECT 1 FROM sqlite_master WHERE type='table' AND name = ?", (table_name,) + ).fetchone() + if not table_exists: + return False + + query = f'SELECT 1 FROM main.{table_name} LIMIT 1' + return cur.execute(query).fetchone() is not None + + +def _table_exists(con: Connection, table_name: str) -> bool: + cur = con.cursor() + exists = cur.execute( + "SELECT 1 FROM sqlite_master WHERE type='table' AND name = ?", (table_name,) + ).fetchone() + return bool(exists) + + +def _append_extension_schema(con: Connection, spec: ExtensionSpec) -> None: + if spec.schema_sql_path is None: + raise RuntimeError(f"Extension '{spec.extension_id}' has no schema SQL path configured.") + + schema_path = Path(spec.schema_sql_path) + if not schema_path.is_file(): + raise FileNotFoundError( + f"Schema SQL file for extension '{spec.extension_id}' not found: {schema_path}" + ) + + sql = schema_path.read_text(encoding='utf-8') + con.executescript(sql) + con.commit() diff --git a/temoa/extensions/method_of_morris/morris.py b/temoa/extensions/method_of_morris/morris.py index 77d770f1..4bb14ddc 100644 --- a/temoa/extensions/method_of_morris/morris.py +++ b/temoa/extensions/method_of_morris/morris.py @@ -38,7 +38,7 @@ def evaluate(param_names: dict[int, list[Any]], param_values: Any, raise ValueError(f'Unrecognized parameter: {names[0]}') dp = DataPortal(data_dict={None: data}) - instance = run_actions.build_instance(loaded_portal=dp) + instance = run_actions.build_instance(loaded_portal=dp, extensions=config.extensions) mdl, res = run_actions.solve_instance(instance=instance, solver_name=config.solver_name) status = run_actions.check_solve_status(res) if not status: diff --git a/temoa/extensions/method_of_morris/morris_evaluate.py b/temoa/extensions/method_of_morris/morris_evaluate.py index 21b03feb..1460b775 100644 --- a/temoa/extensions/method_of_morris/morris_evaluate.py +++ b/temoa/extensions/method_of_morris/morris_evaluate.py @@ -67,7 +67,11 @@ def evaluate(param_info: dict[int, list[Any]], mm_sample: Any, data: dict[str, A logger.debug('\n '.join(log_entry)) dp = DataPortal(data_dict={None: data}) - instance = run_actions.build_instance(loaded_portal=dp, silent=True) + instance = run_actions.build_instance( + loaded_portal=dp, + silent=True, + extensions=config.extensions, + ) mdl, res = run_actions.solve_instance( instance=instance, solver_name=config.solver_name, silent=True ) diff --git a/temoa/extensions/modeling_to_generate_alternatives/mga_sequencer.py b/temoa/extensions/modeling_to_generate_alternatives/mga_sequencer.py index 83b228fe..080d0697 100644 --- a/temoa/extensions/modeling_to_generate_alternatives/mga_sequencer.py +++ b/temoa/extensions/modeling_to_generate_alternatives/mga_sequencer.py @@ -164,7 +164,10 @@ def start(self) -> None: hybrid_loader = HybridLoader(db_connection=self.con, config=self.config) data_portal: DataPortal = hybrid_loader.load_data_portal(myopic_index=None) instance: TemoaModel = build_instance( - loaded_portal=data_portal, model_name=self.config.scenario, silent=self.config.silent + loaded_portal=data_portal, + model_name=self.config.scenario, + silent=self.config.silent, + extensions=self.config.extensions, ) if self.config.price_check: good_prices = price_checker(instance) diff --git a/temoa/extensions/myopic/myopic_sequencer.py b/temoa/extensions/myopic/myopic_sequencer.py index fbee15c4..e671c375 100644 --- a/temoa/extensions/myopic/myopic_sequencer.py +++ b/temoa/extensions/myopic/myopic_sequencer.py @@ -252,6 +252,7 @@ def start(self) -> None: keep_lp_file=self.config.save_lp_file, lp_path=self.config.output_path / ''.join(('LP', str(idx.base_year))), # base year folder + extensions=self.config.extensions, ) # 8. Run checks... diff --git a/temoa/extensions/single_vector_mga/sv_mga_sequencer.py b/temoa/extensions/single_vector_mga/sv_mga_sequencer.py index 8498390b..937aeb65 100644 --- a/temoa/extensions/single_vector_mga/sv_mga_sequencer.py +++ b/temoa/extensions/single_vector_mga/sv_mga_sequencer.py @@ -79,6 +79,7 @@ def start(self) -> None: silent=self.config.silent, keep_lp_file=self.config.save_lp_file, lp_path=lp_path, + extensions=self.config.extensions, ) if self.config.price_check: good_prices = price_checker(instance) diff --git a/temoa/extensions/stochastics/scenario_creator.py b/temoa/extensions/stochastics/scenario_creator.py index 757fa2a7..3dc0b659 100644 --- a/temoa/extensions/stochastics/scenario_creator.py +++ b/temoa/extensions/stochastics/scenario_creator.py @@ -96,7 +96,7 @@ def scenario_creator(scenario_name: str, **kwargs: Any) -> Any: # 3. Build instance data_portal = HybridLoader.data_portal_from_data(data_dict) - instance = build_instance(data_portal, silent=True) + instance = build_instance(data_portal, silent=True, extensions=temoa_config.extensions) # 4. Attach root node (Stage 1) periods = sorted(instance.time_optimize) diff --git a/temoa/extensions/template/__init__.py b/temoa/extensions/template/__init__.py new file mode 100644 index 00000000..fb211977 --- /dev/null +++ b/temoa/extensions/template/__init__.py @@ -0,0 +1,21 @@ +"""Template extension package. + +TEMPLATE: This is a copy-from scaffold for building a new optional Temoa +modeling extension. It is intentionally NOT registered in +``temoa.extensions.framework.get_known_extension_specs``, so it is inert: +importing it works and it type-checks, but it cannot be enabled until you +register it. + +To create a real extension: + 1. Copy this whole folder to ``temoa/extensions//``. + 2. Rename the ``extension_id``, tables, params, and constraints. + 3. Register the spec in ``get_known_extension_specs`` (see framework.py). + 4. Add your tables to the database schema / ``tables.sql``. + 5. Enable it via config: ``extensions = [""]``. + +See ``docs/source/extensions.rst`` for the full guide. +""" + +from temoa.extensions.template.extension import EXAMPLE_EXTENSION + +__all__ = ['EXAMPLE_EXTENSION'] diff --git a/temoa/extensions/template/components/__init__.py b/temoa/extensions/template/components/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/temoa/extensions/template/components/example_limit.py b/temoa/extensions/template/components/example_limit.py new file mode 100644 index 00000000..a00570bc --- /dev/null +++ b/temoa/extensions/template/components/example_limit.py @@ -0,0 +1,62 @@ +"""Example constraint family for the template extension. + +TEMPLATE: A component module holds the index-set function(s) and constraint +rule(s) for a single family of constraints, mirroring the modules under +``temoa/components``. Group related constraints together; create one module per +family. + +This example caps the new capacity built in each period for a technology (or +technology group) in a region. Replace it with your own logic. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from pyomo.environ import Constraint, quicksum, value + +import temoa.components.geography as geography +import temoa.components.technology as technology + +if TYPE_CHECKING: + from temoa.extensions.template.core.model import ExampleModel + from temoa.types import ExprLike, Period, Region, Technology + + +def example_new_capacity_limit_indices( + model: ExampleModel, +) -> set[tuple[Region, Period, Technology]]: + """Build the sparse (region, period, tech-or-group) index for the constraint. + + TEMPLATE: Annotate ``model`` with the extension subtype (``ExampleModel``) so + the extension-owned param ``example_new_capacity_limit`` and its + ``sparse_keys()`` method are visible to the type checker. + """ + return { + (r, p, t) + for r, t in model.example_new_capacity_limit.sparse_keys() + for p in model.time_optimize + } + + +def example_new_capacity_limit_constraint_rule( + model: ExampleModel, r: Region, p: Period, t: Technology +) -> ExprLike: + """Cap total new capacity built in period ``p`` for region/group ``r``/``t``.""" + regions = geography.gather_group_regions(model, r) + techs = technology.gather_group_techs(model, t) + + limit = value(model.example_new_capacity_limit[r, t]) + + new_cap_rtv = model.v_new_capacity + new_cap = quicksum( + new_cap_rtv[_r, _t, _v] + for _r, _t, _v in new_cap_rtv.keys() + if _r in regions and _t in techs and _v == p + ) + + if isinstance(new_cap, (int, float)): + # No decision variables in this period/region/group: nothing to constrain. + return Constraint.Skip + + return new_cap <= limit diff --git a/temoa/extensions/template/core/__init__.py b/temoa/extensions/template/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/temoa/extensions/template/core/model.py b/temoa/extensions/template/core/model.py new file mode 100644 index 00000000..e66a9291 --- /dev/null +++ b/temoa/extensions/template/core/model.py @@ -0,0 +1,71 @@ +"""Type-checking model subtype and component registration for the template extension. + +TEMPLATE: This file plays the same role as ``temoa/core/model.py`` does for the +core model: it declares the extension-owned model components (so the type +checker knows about them) and attaches them to a live ``TemoaModel`` instance. + +Two things live here: + 1. ``ExampleModel`` - a ``TYPE_CHECKING``-only subtype of ``TemoaModel`` that + declares every Param/Set/Constraint this extension adds. It exists purely + for static typing; nothing instantiates it. + 2. ``register_model_components`` - the runtime hook that attaches those + components to the core model. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, cast + +from pyomo.environ import Any, Constraint, Param, Set + +from temoa.extensions.template.components import example_limit + +if TYPE_CHECKING: + from temoa.core.model import TemoaModel + + class ExampleModel(TemoaModel): + """TemoaModel extended with template-extension components. + + TEMPLATE: Declare one annotation per component you create in + ``register_model_components`` below. Inheriting from ``TemoaModel`` means + all core sets/params/vars (e.g. ``time_optimize``, ``v_new_capacity``) + are already visible to the type checker here and in your component + modules. + """ + + # Params (loaded from the database via data_manifest.py) + example_new_capacity_limit: Param + + # Constraint index sets + example_new_capacity_limit_constraint_rpt: Set + + # Constraints + example_new_capacity_limit_constraint: Constraint + + +def register_model_components(model: TemoaModel) -> None: + """Attach template-extension components to the core Temoa model. + + TEMPLATE: Keep the public signature as ``model: TemoaModel`` so this stays + assignable to ``ExtensionSpec.register_model_components`` (a + ``Callable[[TemoaModel], None]``). Narrowing the parameter to ``ExampleModel`` + would be a contravariance error in mypy. Instead, ``cast`` once and operate on + the typed alias ``m`` below. + """ + m = cast('ExampleModel', model) + + # Param: a per-(region, tech-or-group) cap on cumulative new capacity. + m.example_new_capacity_limit = Param( + m.regional_global_indices, m.tech_or_group, within=Any + ) + + # Sparse index set for the constraint, built from the param's populated keys. + m.example_new_capacity_limit_constraint_rpt = Set( + dimen=3, initialize=example_limit.example_new_capacity_limit_indices + ) + + # The constraint itself. + m.example_new_capacity_limit_constraint = Constraint( + m.example_new_capacity_limit_constraint_rpt, + rule=example_limit.example_new_capacity_limit_constraint_rule, + ) diff --git a/temoa/extensions/template/data_manifest.py b/temoa/extensions/template/data_manifest.py new file mode 100644 index 00000000..e9c8ac02 --- /dev/null +++ b/temoa/extensions/template/data_manifest.py @@ -0,0 +1,44 @@ +"""Data-loading manifest for the template extension. + +TEMPLATE: ``build_manifest_items`` returns one ``LoadItem`` per database table +this extension reads. The loader uses each item to query, validate, and populate +the matching Pyomo component declared in core/model.py. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, cast + +from temoa.data_io.loader_manifest import LoadItem + +if TYPE_CHECKING: + from temoa.core.model import TemoaModel + from temoa.extensions.template.core.model import ExampleModel + + +def build_manifest_items(model: TemoaModel) -> list[LoadItem]: + """Return the LoadItems for the template extension. + + TEMPLATE: As with ``register_model_components``, keep the public parameter + typed as ``TemoaModel`` (to match ``ExtensionSpec.build_manifest_items``) and + ``cast`` to the extension subtype so the extension-owned components type-check. + """ + m = cast('ExampleModel', model) + return [ + LoadItem( + # The Pyomo component to populate. + component=m.example_new_capacity_limit, + # Source table name. + table='example_new_capacity_limit', + # Columns to select; for a Param the final column is the value. + columns=['region', 'tech_or_group', 'value'], + # Number of leading columns that form the index (here: region, tech). + index_length=2, + # Source-trace validator on the loader (filters to viable r/t pairs). + validator_name='viable_rt', + # Which index columns to validate (region=0, tech=1). + validation_map=(0, 1), + # Extension tables are optional inputs, so do not require the table. + is_table_required=False, + ), + ] diff --git a/temoa/extensions/template/extension.py b/temoa/extensions/template/extension.py new file mode 100644 index 00000000..9c8509d8 --- /dev/null +++ b/temoa/extensions/template/extension.py @@ -0,0 +1,34 @@ +"""Declarative spec that wires the template extension into Temoa. + +TEMPLATE: ``ExtensionSpec`` is pure metadata plus hook functions. Temoa reads it +to know which tables the extension owns, how to guard them, which model +components to attach, and how to load the extension's data. +""" + +from __future__ import annotations + +from pathlib import Path + +from temoa.extensions.framework import ExtensionSpec +from temoa.extensions.template.core.model import register_model_components +from temoa.extensions.template.data_manifest import build_manifest_items + +EXAMPLE_EXTENSION = ExtensionSpec( + # Unique, lowercase id. This is what users put in ``extensions = [...]``. + extension_id='template', + # Database tables owned exclusively by this extension. + owned_tables=('example_new_capacity_limit',), + # Tables whose ``region`` column may contain a regional group name. Maps + # table -> the column that holds the region/group. Merged into the loader's + # regional-group handling. + regional_group_tables={'example_new_capacity_limit': 'region'}, + # Hook: attach model components to the core model (see core/model.py). + register_model_components=register_model_components, + # Hook: describe how to load this extension's data (see data_manifest.py). + build_manifest_items=build_manifest_items, + # Schema applied (with consent) when enabled but tables are missing. + schema_sql_path=str(Path(__file__).parents[0] / 'tables.sql'), + # If True, loading fails when this extension is DISABLED but its tables hold + # data, preventing silently-ignored inputs. + fail_if_tables_populated_when_disabled=True, +) diff --git a/temoa/extensions/template/tables.sql b/temoa/extensions/template/tables.sql new file mode 100644 index 00000000..bc8e3a75 --- /dev/null +++ b/temoa/extensions/template/tables.sql @@ -0,0 +1,14 @@ +-- TEMPLATE: Schema for the template extension's owned tables. +-- One CREATE TABLE per entry in ``owned_tables`` (see extension.py). Use +-- ``IF NOT EXISTS`` so the schema can be appended to an existing database when +-- the extension is enabled but its tables are missing. + +CREATE TABLE IF NOT EXISTS example_new_capacity_limit +( + region TEXT, + tech_or_group TEXT, + value REAL NOT NULL DEFAULT 0, + units TEXT, + notes TEXT, + PRIMARY KEY (region, tech_or_group) +); diff --git a/temoa/tutorial_assets/config_sample.toml b/temoa/tutorial_assets/config_sample.toml index 6b522212..df3d7b20 100644 --- a/temoa/tutorial_assets/config_sample.toml +++ b/temoa/tutorial_assets/config_sample.toml @@ -18,6 +18,12 @@ scenario = "zulu" # [perfect_foresight, MGA, myopic, method_of_morris, build_only, check, monte_carlo] scenario_mode = "perfect_foresight" +# Optional extensions list +# Use this to enable modular model features that are not part of the base core model. +# Example: extensions = ["unit_commitment", "growth_rates"] +# Leave empty for default core-only behavior. +extensions = [] + # Input database (Mandatory) input_database = "utopia.sqlite" diff --git a/tests/conftest.py b/tests/conftest.py index 0e263b9c..5714aa84 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,6 +10,7 @@ from temoa._internal.temoa_sequencer import TemoaSequencer from temoa.core.config import TemoaConfig from temoa.core.model import TemoaModel +from temoa.extensions.framework import get_known_extension_specs logger = logging.getLogger(__name__) @@ -57,6 +58,13 @@ def _build_test_db( # Force FK OFF again as schema file might turn it on at the end con.execute('PRAGMA foreign_keys = OFF') + # 1b. Load extension-owned tables so data scripts can populate them. + # Extension tables are not part of the central schema, so apply each + # known extension's schema (CREATE TABLE IF NOT EXISTS) here. + for spec in get_known_extension_specs().values(): + if spec.schema_sql_path: + con.executescript(Path(spec.schema_sql_path).read_text(encoding='utf-8')) + # 2. Load data scripts for script_path in data_scripts: with open(script_path) as f: From 8da58575e53d6e58605942ad48d5a6447edd3315 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Mon, 6 Jul 2026 10:37:03 -0400 Subject: [PATCH 16/28] Move growth rate constraints to an extension Signed-off-by: Davey Elder --- docs/source/extensions.rst | 14 + docs/source/extensions/growth_rates.rst | 99 ++++ docs/source/mathematical_formulation.rst | 64 --- docs/source/param_desc_and_tables.rst | 14 - temoa/components/limits.py | 445 ------------------ temoa/core/model.py | 65 --- temoa/data_io/component_manifest.py | 60 --- temoa/data_io/hybrid_loader.py | 6 - temoa/db_schema/temoa_schema_v4.sql | 72 --- temoa/extensions/growth_rates/__init__.py | 3 + .../growth_rates/components/__init__.py | 0 .../components/growth_capacity.py | 123 +++++ .../components/growth_new_capacity.py | 121 +++++ .../components/growth_new_capacity_delta.py | 148 ++++++ .../extensions/growth_rates/core/__init__.py | 0 temoa/extensions/growth_rates/core/model.py | 115 +++++ .../extensions/growth_rates/data_manifest.py | 76 +++ temoa/extensions/growth_rates/extension.py | 32 ++ temoa/extensions/growth_rates/tables.sql | 72 +++ .../config_myopic_capacities.toml | 1 + tests/testing_data/mediumville_sets.json | 6 - tests/testing_data/test_system_sets.json | 6 - tests/testing_data/utopia_sets.json | 6 - 23 files changed, 804 insertions(+), 744 deletions(-) create mode 100644 docs/source/extensions/growth_rates.rst create mode 100644 temoa/extensions/growth_rates/__init__.py create mode 100644 temoa/extensions/growth_rates/components/__init__.py create mode 100644 temoa/extensions/growth_rates/components/growth_capacity.py create mode 100644 temoa/extensions/growth_rates/components/growth_new_capacity.py create mode 100644 temoa/extensions/growth_rates/components/growth_new_capacity_delta.py create mode 100644 temoa/extensions/growth_rates/core/__init__.py create mode 100644 temoa/extensions/growth_rates/core/model.py create mode 100644 temoa/extensions/growth_rates/data_manifest.py create mode 100644 temoa/extensions/growth_rates/extension.py create mode 100644 temoa/extensions/growth_rates/tables.sql diff --git a/docs/source/extensions.rst b/docs/source/extensions.rst index 5e8e1a9c..32f72454 100644 --- a/docs/source/extensions.rst +++ b/docs/source/extensions.rst @@ -230,3 +230,17 @@ The template extension ``get_known_extension_specs``, it cannot be enabled until you register it, so it never affects normal runs. Copy the folder as the starting point for a new extension; every file carries ``# TEMPLATE:`` comments explaining what to change. + +.. _extension-catalog: + +Available extensions +-------------------- + +The extensions that ship with Temoa are documented on their own pages below. +Each page describes the extension's parameters and constraints. This list grows +as new extensions are added. + +.. toctree:: + :maxdepth: 1 + + extensions/growth_rates diff --git a/docs/source/extensions/growth_rates.rst b/docs/source/extensions/growth_rates.rst new file mode 100644 index 00000000..261b1e74 --- /dev/null +++ b/docs/source/extensions/growth_rates.rst @@ -0,0 +1,99 @@ +.. _extension-growth-rates: + +Growth Rate Limits +================================== + +The **growth_rates** extension adds optional constraints that bound how quickly +the capacity (or new capacity) of a technology or technology group may change +between consecutive model periods. It is disabled by default and enabled per +run through configuration: + +.. code-block:: toml + + extensions = ["growth_rates"] + +Its parameters live in the extension-owned tables of the same name and are loaded +only when the extension is enabled. See :ref:`extensions` for how the extension +framework wires these components into the model. + +Parameters +---------- + +.. csv-table:: + :header: "Parameter", "Database Table", "Model Element", "Notes" + :widths: 15, 20, 25, 40 + + ":math:`\text{LGC}_{r,t}`", ":code:`limit_growth_capacity`", ":code:`limit_growth_capacity`", "capacity growth rate limits; :code:`tech_or_group` column accepts a technology name or group name" + ":math:`\text{LDGC}_{r,t}`", ":code:`limit_degrowth_capacity`", ":code:`limit_degrowth_capacity`", "capacity degrowth rate limits; :code:`tech_or_group` column accepts a technology name or group name" + ":math:`\text{LGNC}_{r,t}`", ":code:`limit_growth_new_capacity`", ":code:`limit_growth_new_capacity`", "new capacity growth rate limits; :code:`tech_or_group` column accepts a technology name or group name" + ":math:`\text{LDGNC}_{r,t}`", ":code:`limit_degrowth_new_capacity`", ":code:`limit_degrowth_new_capacity`", "new capacity degrowth rate limits; :code:`tech_or_group` column accepts a technology name or group name" + ":math:`\mathrm{LGNC}_{\Delta,r,t}`", ":code:`limit_growth_new_capacity_delta`", ":code:`limit_growth_new_capacity_delta`", "new capacity growth acceleration limits; :code:`tech_or_group` column accepts a technology name or group name" + ":math:`\mathrm{LDGNC}_{\Delta,r,t}`", ":code:`limit_degrowth_new_capacity_delta`", ":code:`limit_degrowth_new_capacity_delta`", "new capacity degrowth deceleration limits; :code:`tech_or_group` column accepts a technology name or group name" + + +limit_growth_capacity +~~~~~~~~~~~~~~~~~~~~~~~ + +:math:`{LGC}_{r \in R, t \in T}` + +The :code:`limit_growth_capacity` parameter defines the maximum annual rate at +which the total capacity of a technology (or group) can grow between periods. +The :code:`tech_or_group` column accepts a technology name or group name. + + +limit_degrowth_capacity +~~~~~~~~~~~~~~~~~~~~~~~~~ + +:math:`{LDGC}_{r \in R, t \in T}` + +The :code:`limit_degrowth_capacity` parameter defines the maximum annual rate +at which the total capacity of a technology (or group) can shrink between +periods. The :code:`tech_or_group` column accepts a technology name or group name. + + +limit_growth_new_capacity +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:math:`{LGNC}_{r \in R, t \in T}` + +The :code:`limit_growth_new_capacity` parameter constrains the rate of increase +in new capacity deployment between consecutive periods. + + +limit_degrowth_new_capacity +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:math:`{LDGNC}_{r \in R, t \in T}` + +The :code:`limit_degrowth_new_capacity` parameter constrains the rate of decrease +in new capacity deployment between consecutive periods. + + +limit_growth_new_capacity_delta +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:math:`\mathrm{LGNC}_{\Delta,r \in R, t \in T}` + +The :code:`limit_growth_new_capacity_delta` parameter constrains the acceleration +of new capacity growth between periods. This essentially adds "inertia" to the +growth of new capacity deployment. + + +limit_degrowth_new_capacity_delta +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:math:`\mathrm{LDGNC}_{\Delta,r \in R, t \in T}` + +The :code:`limit_degrowth_new_capacity_delta` parameter constrains the +deceleration of new capacity degrowth between periods, essentially adding +"inertia" to the degrowth of new capacity deployment. + + +Constraints +----------- + +.. autofunction:: temoa.extensions.growth_rates.components.growth_capacity.limit_growth_capacity + +.. autofunction:: temoa.extensions.growth_rates.components.growth_new_capacity.limit_growth_new_capacity + +.. autofunction:: temoa.extensions.growth_rates.components.growth_new_capacity_delta.limit_growth_new_capacity_delta diff --git a/docs/source/mathematical_formulation.rst b/docs/source/mathematical_formulation.rst index cc4d3da1..86bd4b4f 100644 --- a/docs/source/mathematical_formulation.rst +++ b/docs/source/mathematical_formulation.rst @@ -711,35 +711,6 @@ controls the bound direction. The group columns accept either a single technology name or a technology group name. -limit_degrowth_capacity -~~~~~~~~~~~~~~~~~~~~~~~ - -:math:`{LDGC}_{r \in R, t \in T}` - -The :code:`limit_degrowth_capacity` parameter defines the maximum annual rate -at which the total capacity of a technology (or group) can shrink between -periods. The :code:`tech_or_group` column accepts a technology name or group name. - - -limit_degrowth_new_capacity -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -:math:`{LDGNC}_{r \in R, t \in T}` - -The :code:`limit_degrowth_new_capacity` parameter constrains the rate of decrease -in new capacity deployment between consecutive periods. - - -limit_degrowth_new_capacity_delta -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -:math:`\mathrm{LDGNC}_{\Delta,r \in R, t \in T}` - -The :code:`limit_degrowth_new_capacity_delta` parameter constrains the -deceleration of new capacity degrowth between periods, essentially adding -"intertia" to the degrowth of new capacity deployment. - - limit_emission ~~~~~~~~~~~~~~ @@ -751,35 +722,6 @@ fits within the modeler-specified limit on emission :math:`e` in time period bound, or equality. -limit_growth_capacity -~~~~~~~~~~~~~~~~~~~~~ - -:math:`{LGC}_{r \in R, t \in T}` - -The :code:`limit_growth_capacity` parameter defines the maximum annual rate at -which the total capacity of a technology (or group) can grow between periods. -The :code:`tech_or_group` column accepts a technology name or group name. - - -limit_growth_new_capacity -~~~~~~~~~~~~~~~~~~~~~~~~~ - -:math:`{LGNC}_{r \in R, t \in T}` - -The :code:`limit_growth_new_capacity` parameter constrains the rate of increase -in new capacity deployment between consecutive periods. - - -limit_growth_new_capacity_delta -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -:math:`\mathrm{LGNC}_{\Delta,r \in R, t \in T}` - -The :code:`limit_growth_new_capacity_delta` parameter constrains the acceleration -of new capacity growth between periods. This essentially adds "inertia" to the -growth of new capacity deployment. - - limit_new_capacity ~~~~~~~~~~~~~~~~~~ @@ -1437,12 +1379,6 @@ User-Specific Constraints .. autofunction:: temoa.components.storage.limit_storage_fraction_constraint -.. autofunction:: temoa.components.limits.limit_growth_capacity - -.. autofunction:: temoa.components.limits.limit_growth_new_capacity - -.. autofunction:: temoa.components.limits.limit_growth_new_capacity_delta - General Caveats --------------- diff --git a/docs/source/param_desc_and_tables.rst b/docs/source/param_desc_and_tables.rst index 40ff0737..aef95320 100644 --- a/docs/source/param_desc_and_tables.rst +++ b/docs/source/param_desc_and_tables.rst @@ -94,20 +94,6 @@ capacity, activity and emissions**. ":math:`\text{LE}_{r,p,e}`", ":code:`limit_emission`", ":code:`limit_emission`", "limit on emissions by region and period" ":math:`\text{LS}_{r,t}`", ":code:`limit_resource`", ":code:`limit_resource`", "cumulative activity limit across time periods (not supported in myopic); :code:`tech_or_group` column accepts a technology name or group name" -Parameters in the table below relate to the specification of **growth and degrowth -limits**. - -.. csv-table:: - :header: "Parameter", "Database Table", "Model Element", "Notes" - :widths: 15, 20, 25, 40 - - ":math:`\text{LGC}_{r,t}`", ":code:`limit_growth_capacity`", ":code:`limit_growth_capacity`", "capacity growth rate limits; :code:`tech_or_group` column accepts a technology name or group name" - ":math:`\text{LDGC}_{r,t}`", ":code:`limit_degrowth_capacity`", ":code:`limit_degrowth_capacity`", "capacity degrowth rate limits; :code:`tech_or_group` column accepts a technology name or group name" - ":math:`\text{LGNC}_{r,t}`", ":code:`limit_growth_new_capacity`", ":code:`limit_growth_new_capacity`", "new capacity growth rate limits; :code:`tech_or_group` column accepts a technology name or group name" - ":math:`\text{LDGNC}_{r,t}`", ":code:`limit_degrowth_new_capacity`", ":code:`limit_degrowth_new_capacity`", "new capacity degrowth rate limits; :code:`tech_or_group` column accepts a technology name or group name" - ":math:`\mathrm{LGNC}_{\Delta,r,t}`", ":code:`limit_growth_new_capacity_delta`", ":code:`limit_growth_new_capacity_delta`", "new capacity growth acceleration limits; :code:`tech_or_group` column accepts a technology name or group name" - ":math:`\mathrm{LDGNC}_{\Delta,r,t}`", ":code:`limit_degrowth_new_capacity_delta`", ":code:`limit_degrowth_new_capacity_delta`", "new capacity degrowth deceleration limits; :code:`tech_or_group` column accepts a technology name or group name" - Parameters in the table below relate to the specification of **operational and split limits**. diff --git a/temoa/components/limits.py b/temoa/components/limits.py index 857cb148..0145073e 100644 --- a/temoa/components/limits.py +++ b/temoa/components/limits.py @@ -22,7 +22,6 @@ import temoa.components.technology as technology from temoa.components.utils import ( Operator, - get_adjusted_existing_capacity, get_variable_efficiency, operator_expression, ) @@ -135,64 +134,6 @@ def limit_tech_output_split_average_constraint_indices( } -def limit_growth_capacity_indices(model: TemoaModel) -> set[tuple[Region, Period, Technology, str]]: - return { - (r, p, t, op) - for r, t, op in model.limit_growth_capacity.sparse_keys() - for p in model.time_optimize - } - - -def limit_degrowth_capacity_indices( - model: TemoaModel, -) -> set[tuple[Region, Period, Technology, str]]: - return { - (r, p, t, op) - for r, t, op in model.limit_degrowth_capacity.sparse_keys() - for p in model.time_optimize - } - - -def limit_growth_new_capacity_indices( - model: TemoaModel, -) -> set[tuple[Region, Period, Technology, str]]: - return { - (r, p, t, op) - for r, t, op in model.limit_growth_new_capacity.sparse_keys() - for p in model.time_optimize - } - - -def limit_degrowth_new_capacity_indices( - model: TemoaModel, -) -> set[tuple[Region, Period, Technology, str]]: - return { - (r, p, t, op) - for r, t, op in model.limit_degrowth_new_capacity.sparse_keys() - for p in model.time_optimize - } - - -def limit_growth_new_capacity_delta_indices( - model: TemoaModel, -) -> set[tuple[Region, Period, Technology, str]]: - return { - (r, p, t, op) - for r, t, op in model.limit_growth_new_capacity_delta.sparse_keys() - for p in model.time_optimize - } - - -def limit_degrowth_new_capacity_delta_indices( - model: TemoaModel, -) -> set[tuple[Region, Period, Technology, str]]: - return { - (r, p, t, op) - for r, t, op in model.limit_degrowth_new_capacity_delta.sparse_keys() - for p in model.time_optimize - } - - def limit_seasonal_capacity_factor_constraint_indices( model: TemoaModel, ) -> set[tuple[Region, Period, Season, Technology, str]]: @@ -953,392 +894,6 @@ def limit_emission_constraint( return expr -def limit_growth_capacity_constraint_rule( - model: TemoaModel, r: Region, p: Period, t: Technology, op: str -) -> ExprLike: - r"""Constrain ramp up rate of available capacity""" - return limit_growth_capacity(model, r, p, t, op, False) - - -def limit_degrowth_capacity_constraint_rule( - model: TemoaModel, r: Region, p: Period, t: Technology, op: str -) -> ExprLike: - r"""Constrain ramp down rate of available capacity""" - return limit_growth_capacity(model, r, p, t, op, True) - - -def limit_growth_capacity( - model: TemoaModel, r: Region, p: Period, t: Technology, op: str, degrowth: bool = False -) -> ExprLike: - r""" - Constrain the change of capacity available between periods. - Forces the model to ramp up and down the availability of new technologies - more smoothly. Has constant (seed, :math:`S_{r,t}`) and proportional - (rate, :math:`R_{r,t}`) terms. This can be defined for a technology group - instead of one technology, in which case, capacity available is summed over - all technologies in the group. In the first period, previous available - capacity :math:`\mathbf{CAPAVL}_{r,p,t}` is replaced by previous existing - capacity, if any can be found. - - .. math:: - :label: Limit (De)Growth Capacity - - \begin{aligned}\text{Growth:}\\ - &\mathbf{CAPAVL}_{r,p,t} - \quad \le, \ge, \text{or} = \quad - S_{r,t} + (1+R_{r,t}) \cdot \mathbf{CAPAVL}_{r,p_{prev},t} - \end{aligned} - - \qquad \forall \{r, p, t\} \in \Theta_{\text{limit\_growth\_capacity}} - - - \begin{aligned}\text{Degrowth:}\\ - &\mathbf{CAPAVL}_{r,p_{prev},t} - \quad \le, \ge, \text{or} = \quad S_{r,t} + (1+R_{r,t}) \cdot \mathbf{CAPAVL}_{r,p,t} - \end{aligned} - - \qquad \forall \{r, p, t\} \in \Theta_{\text{limit\_degrowth\_capacity}} - """ - - regions = geography.gather_group_regions(model, r) - techs = technology.gather_group_techs(model, t) - - growth = model.limit_degrowth_capacity if degrowth else model.limit_growth_capacity - rate = 1 + value(growth[r, t, op][0]) - seed = value(growth[r, t, op][1]) - cap_rpt = model.v_capacity_available_by_period_and_tech - - # relevant r, p, t indices - cap_indices = {(_r, _p, _t) for _r, _p, _t in cap_rpt.keys() if _t in techs and _r in regions} - # periods the technology can have capacity in this region (sorted) - periods = sorted({_p for _r, _p, _t in cap_rpt}) - - if len(periods) == 0: - if p == model.time_optimize.first(): - msg = ( - 'Tried to set {}rowthCapacity constraint {} but there are no periods where this ' - 'technology is available in this region. Constraint skipped.' - ).format('Deg' if degrowth else 'G', (r, t)) - logger.warning(msg) - return Constraint.Skip - - # Only warn in p0 so we dont dump multiple warnings - if p == periods[0]: - if seed == 0: - msg = ( - 'No constant term (seed) provided for {}rowthCapacity constraint {}. ' - 'No capacity will be built in any period following one with zero capacity.' - ).format('Deg' if degrowth else 'G', (r, t)) - logger.info(msg) - gaps = [ - _p - for _p in model.time_optimize - if _p not in periods and min(periods) < _p < max(periods) - ] - if gaps: - msg = ( - 'Constructing {}rowthCapacity constraint {} and there are period gaps in which' - 'capacity cannot exist in this region ({}). Capacity in these periods ' - 'will be treated as zero which may cause infeasibility or other problems.' - ).format('Deg' if degrowth else 'G', (r, t), gaps) - logger.warning(msg) - - # sum available capacity in this period - capacity = quicksum(cap_rpt[_r, _p, _t] for _r, _p, _t in cap_indices if _p == p) - - if p == model.time_optimize.first(): - # First future period. Grab available capacity in last existing period - p_prev = model.time_exist.last() - capacity_prev = sum( - get_adjusted_existing_capacity(model, _r, _t, _v) - * value(model.process_life_frac[_r, p_prev, _t, _v]) - for _r, _t, _v in model.existing_capacity.sparse_keys() - if _r in regions - and _t in techs - and _v + value(model.lifetime_process[_r, _t, _v]) > p_prev - ) - else: - # Otherwise, grab previous future period - p_prev = model.time_optimize.prev(p) - capacity_prev = quicksum(cap_rpt[_r, _p, _t] for _r, _p, _t in cap_indices if _p == p_prev) - - if degrowth: - expr = operator_expression(capacity_prev, Operator(op), seed + capacity * rate) - else: - expr = operator_expression(capacity, Operator(op), seed + capacity_prev * rate) - - # Check if any variables are actually included before returning - if isinstance(expr, bool): - return Constraint.Skip - return expr - - -def limit_growth_new_capacity_constraint_rule( - model: TemoaModel, r: Region, p: Period, t: Technology, op: str -) -> ExprLike: - r"""Constrain ramp up rate of new capacity deployment""" - return limit_growth_new_capacity(model, r, p, t, op, False) - - -def limit_degrowth_new_capacity_constraint_rule( - model: TemoaModel, r: Region, p: Period, t: Technology, op: str -) -> ExprLike: - r"""Constrain ramp down rate of new capacity deployment""" - return limit_growth_new_capacity(model, r, p, t, op, True) - - -def limit_growth_new_capacity( - model: TemoaModel, r: Region, p: Period, t: Technology, op: str, degrowth: bool = False -) -> ExprLike: - r""" - Constrain the change of new capacity deployed between periods. - Forces the model to ramp up and down the deployment of new technologies - more smoothly. Has constant (seed, :math:`S_{r,t}`) and proportional - (rate, :math:`R_{r,t}`) terms. This can be defined for a technology group - instead of one technology, in which case, new capacity is summed over - all technologies in the group. In the first period, previous new capacity - :math:`\mathbf{NCAP}_{r,t,v_prev}` is replaced by previous existing capacity, - if any can be found. - - .. math:: - :label: Limit (De)Growth New Capacity - - \begin{aligned}\text{Growth:}\\ - &\mathbf{NCAP}_{r,t,v} - \quad \le, \ge, \text{or} = \quad - S_{r,t} + (1+R_{r,t}) \cdot \mathbf{NCAP}_{r,t,v_{prev}} - \text{ where } v=p - \end{aligned} - - \qquad \forall \{r, p, t\} \in \Theta_{\text{limit\_growth\_capacity}} - - \begin{aligned}\text{Degrowth:}\\ - &\mathbf{NCAP}_{r,t,v_{prev}} - \quad \le, \ge, \text{or} = \quad S_{r,t} + (1+R_{r,t}) \cdot \mathbf{NCAP}_{r,t,v} - \text{ where } v=p - \end{aligned} - - \qquad \forall \{r, p, t\} \in \Theta_{\text{limit\_degrowth\_capacity}} - """ - - regions = geography.gather_group_regions(model, r) - techs = technology.gather_group_techs(model, t) - - growth = model.limit_degrowth_new_capacity if degrowth else model.limit_growth_new_capacity - rate = 1 + value(growth[r, t, op][0]) - seed = value(growth[r, t, op][1]) - new_cap_rtv = model.v_new_capacity - - # relevant r, t, v indices - cap_rtv = {(_r, _t, _v) for _r, _t, _v in new_cap_rtv.keys() if _t in techs and _r in regions} - # periods the technology can be built in this region (sorted) - periods = sorted({_v for _r, _t, _v in cap_rtv}) - - if len(periods) == 0: - if p == model.time_optimize.first(): - msg = ( - 'Tried to set {}rowthNewCapacity constraint {} but there are no periods where this ' - 'technology can be built in this region. Constraint skipped.' - ).format('Deg' if degrowth else 'G', (r, t)) - logger.warning(msg) - return Constraint.Skip - - # Only warn in p0 so we dont dump multiple warnings - if p == periods[0]: - if seed == 0: - msg = ( - 'No constant term (seed) provided for {}rowthNewCapacity constraint {}. ' - 'No capacity will be built in any period following one with zero new capacity.' - ).format('Deg' if degrowth else 'G', (r, t)) - logger.info(msg) - gaps = [ - _p - for _p in model.time_optimize - if _p not in periods and min(periods) < _p < max(periods) - ] - if gaps: - msg = ( - 'Constructing {}rowthNewCapacity constraint {} and there are period gaps in which' - 'new capacity cannot be built in this region ({}). New capacity in these periods ' - 'will be treated as zero which may cause infeasibility or other problems.' - ).format('Deg' if degrowth else 'G', (r, t), gaps) - logger.warning(msg) - - # sum new capacity in this period - new_cap = quicksum(new_cap_rtv[_r, _t, _v] for _r, _t, _v in cap_rtv if _v == p) - - if p == model.time_optimize.first(): - # First future period. Grab last existing vintage - p_prev = model.time_exist.last() - new_cap_prev = sum( - value(model.existing_capacity[_r, _t, _v]) - for _r, _t, _v in model.existing_capacity.sparse_keys() - if _r in regions and _t in techs and _v == p_prev - ) - else: - # Otherwise, grab previous future vintage - p_prev = model.time_optimize.prev(p) - new_cap_prev = sum(new_cap_rtv[_r, _t, _v] for _r, _t, _v in cap_rtv if _v == p_prev) - - if degrowth: - expr = operator_expression(new_cap_prev, Operator(op), seed + new_cap * rate) - else: - expr = operator_expression(new_cap, Operator(op), seed + new_cap_prev * rate) - - # Check if any variables are actually included before returning - if isinstance(expr, bool): - return Constraint.Skip - return expr - - -def limit_growth_new_capacity_delta_constraint_rule( - model: TemoaModel, r: Region, p: Period, t: Technology, op: str -) -> ExprLike: - r"""Constrain ramp up rate of change in new capacity deployment""" - return limit_growth_new_capacity_delta(model, r, p, t, op, False) - - -def limit_degrowth_new_capacity_delta_constraint_rule( - model: TemoaModel, r: Region, p: Period, t: Technology, op: str -) -> ExprLike: - r"""Constrain ramp down rate of change in new capacity deployment""" - return limit_growth_new_capacity_delta(model, r, p, t, op, True) - - -def limit_growth_new_capacity_delta( - model: TemoaModel, r: Region, p: Period, t: Technology, op: str, degrowth: bool = False -) -> ExprLike: - r""" - Constrain the acceleration of new capacity deployed between periods. - Forces the model to ramp up and down the change in deployment of new technologies - more smoothly. Has constant (seed, :math:`S_{r,t}`) and proportional - (rate, :math:`R_{r,t}`) terms. It is recommended to leave the rate term empty - as it would prevent the possibility of inflection in the rate of deployment. - This constraint can be defined for a technology group instead of one technology, - in which case, new capacity is summed over all technologies in the group. In the - first period, previous new capacities are replaced by previous existing capacities, - if any can be found. - - .. math:: - :label: Limit (De)Growth New Capacity Delta - - \begin{aligned}\text{Growth:}\\ - &\mathbf{NCAP}_{r,t,v_i} - \mathbf{NCAP}_{r,t,v_{i-1}} - \quad \le, \ge, \text{or} = \quad S_{r,t} + (1+R_{r,t}) \cdot - (\mathbf{NCAP}_{r,t,v_{i-1}} - \mathbf{NCAP}_{r,t,v_{i-2}}) - \end{aligned} - - \text{ where } v_i=p - - \qquad \forall \{r, p, t\} \in \Theta_{\text{limit\_growth\_capacityDelta}} - - \begin{aligned}\text{Degrowth:}\\ - &\mathbf{NCAP}_{r,t,v_{i-1}} - \mathbf{NCAP}_{r,t,v_{i-2}} - \quad \le, \ge, \text{or} = \quad - S_{r,t} + (1+R_{r,t}) \cdot (\mathbf{NCAP}_{r,t,v_i} - \mathbf{NCAP}_{r,t,v_{i-1}}) - \end{aligned} - - \text{ where } v_i=p - - \qquad \forall \{r, p, t\} \in \Theta_{\text{limit\_degrowth\_capacityDelta}} - """ - - regions = geography.gather_group_regions(model, r) - techs = technology.gather_group_techs(model, t) - - growth = ( - model.limit_degrowth_new_capacity_delta - if degrowth - else model.limit_growth_new_capacity_delta - ) - rate = 1 + value(growth[r, t, op][0]) - seed = value(growth[r, t, op][1]) - new_cap_rtv = model.v_new_capacity - - # relevant r, t, v indices - cap_rtv = {(_r, _t, _v) for _r, _t, _v in new_cap_rtv.keys() if _t in techs and _r in regions} - # periods the technology can be built in this region (sorted) - periods = sorted({_v for _r, _t, _v in cap_rtv}) - - if len(periods) == 0: - if p == model.time_optimize.first(): - msg = ( - 'Tried to set {}rowthNewCapacityDelta constraint {} but there are no periods where ' - 'this technology can be built in this region. Constraint skipped.' - ).format('Deg' if degrowth else 'G', (r, t)) - logger.warning(msg) - return Constraint.Skip - - # Only warn in p0 so we dont dump multiple warnings - if p == periods[0]: - if seed == 0: - msg = ( - 'No constant term (seed) provided for {}rowthNewCapacityDelta constraint {}. ' - 'This is not recommended as deployment rates cannot inflect (change from ' - 'accelerating to decelerating or vice-versa).' - ).format('Deg' if degrowth else 'G', (r, t)) - logger.warning(msg) - gaps = [ - _p - for _p in model.time_optimize - if _p not in periods and min(periods) < _p < max(periods) - ] - if gaps: - msg = ( - 'Constructing {}rowthNewCapacityDelta constraint {} and there are period gaps in ' - 'which new capacity cannot be built in this region ({}). New capacity in these ' - 'periods will be treated as zero which may cause infeasibility or other problems.' - ).format('Deg' if degrowth else 'G', (r, t), gaps) - logger.warning(msg) - - # sum new capacity in this period - new_cap = sum(new_cap_rtv[_r, _t, _v] for _r, _t, _v in cap_rtv if _v == p) - - if p == model.time_optimize.first(): - # First planning period, pull last two existing vintages - p_prev = model.time_exist.last() - new_cap_prev = sum( - value(model.existing_capacity[_r, _t, _v]) - for _r, _t, _v in model.existing_capacity.sparse_keys() - if _r in regions and _t in techs and _v == p_prev - ) - p_prev2 = model.time_exist.prev(p_prev) - new_cap_prev2 = sum( - value(model.existing_capacity[_r, _t, _v]) - for _r, _t, _v in model.existing_capacity.sparse_keys() - if _r in regions and _t in techs and _v == p_prev2 - ) - else: - # Not the first future period. Grab previous future period - p_prev = model.time_optimize.prev(p) - new_cap_prev = sum(new_cap_rtv[_r, _t, _v] for _r, _t, _v in cap_rtv if _v == p_prev) - if p == model.time_optimize.at(2): # apparently pyomo sets are indexed 1-based - # Second future period, grab last existing vintage - p_prev2 = model.time_exist.last() - new_cap_prev2 = sum( - value(model.existing_capacity[_r, _t, _v]) - for _r, _t, _v in model.existing_capacity.sparse_keys() - if _r in regions and _t in techs and _v == p_prev2 - ) - else: - # At least the third future period. Grab last two future vintages - p_prev2 = model.time_optimize.prev(p_prev) - new_cap_prev2 = sum(new_cap_rtv[_r, _t, _v] for _r, _t, _v in cap_rtv if _v == p_prev2) - - nc_delta_prev = new_cap_prev - new_cap_prev2 - nc_delta = new_cap - new_cap_prev - - if degrowth: - expr = operator_expression(nc_delta_prev, Operator(op), seed + nc_delta * rate) - else: - expr = operator_expression(nc_delta, Operator(op), seed + nc_delta_prev * rate) - - # Check if any variables are actually included before returning - if isinstance(expr, bool): - return Constraint.Skip - return expr - - def limit_activity_constraint( model: TemoaModel, r: Region, p: Period, t: Technology, op: str ) -> ExprLike: diff --git a/temoa/core/model.py b/temoa/core/model.py index 29a801de..10f0a77b 100755 --- a/temoa/core/model.py +++ b/temoa/core/model.py @@ -15,7 +15,6 @@ from pyomo.core import BuildCheck, Set, Var from pyomo.environ import ( AbstractModel, - Any, BuildAction, Constraint, Integers, @@ -635,25 +634,6 @@ def __init__( self.limit_annual_capacity_factor_constraint_rtvo, validate=validate_0to1 ) - self.limit_growth_capacity = Param( - self.regional_global_indices, self.tech_or_group, self.operator, within=Any - ) - self.limit_degrowth_capacity = Param( - self.regional_global_indices, self.tech_or_group, self.operator, within=Any - ) - self.limit_growth_new_capacity = Param( - self.regional_global_indices, self.tech_or_group, self.operator, within=Any - ) - self.limit_degrowth_new_capacity = Param( - self.regional_global_indices, self.tech_or_group, self.operator, within=Any - ) - self.limit_growth_new_capacity_delta = Param( - self.regional_global_indices, self.tech_or_group, self.operator, within=Any - ) - self.limit_degrowth_new_capacity_delta = Param( - self.regional_global_indices, self.tech_or_group, self.operator, within=Any - ) - self.limit_emission_constraint_rpe = Set( within=self.regional_global_indices * self.time_optimize @@ -1002,51 +982,6 @@ def __init__( ['Starting LimitGrowth and Activity Constraints'], rule=progress_check ) - self.limit_growth_capacity_constraint_rpt = Set( - dimen=4, initialize=limits.limit_growth_capacity_indices - ) - self.limit_growth_capacity_constraint = Constraint( - self.limit_growth_capacity_constraint_rpt, - rule=limits.limit_growth_capacity_constraint_rule, - ) - self.limit_degrowth_capacity_constraint_rpt = Set( - dimen=4, initialize=limits.limit_degrowth_capacity_indices - ) - self.limit_degrowth_capacity_constraint = Constraint( - self.limit_degrowth_capacity_constraint_rpt, - rule=limits.limit_degrowth_capacity_constraint_rule, - ) - - self.limit_growth_new_capacity_constraint_rpt = Set( - dimen=4, initialize=limits.limit_growth_new_capacity_indices - ) - self.limit_growth_new_capacity_constraint = Constraint( - self.limit_growth_new_capacity_constraint_rpt, - rule=limits.limit_growth_new_capacity_constraint_rule, - ) - self.limit_degrowth_new_capacity_constraint_rpt = Set( - dimen=4, initialize=limits.limit_degrowth_new_capacity_indices - ) - self.limit_degrowth_new_capacity_constraint = Constraint( - self.limit_degrowth_new_capacity_constraint_rpt, - rule=limits.limit_degrowth_new_capacity_constraint_rule, - ) - - self.limit_growth_new_capacity_delta_constraint_rpt = Set( - dimen=4, initialize=limits.limit_growth_new_capacity_delta_indices - ) - self.limit_growth_new_capacity_delta_constraint = Constraint( - self.limit_growth_new_capacity_delta_constraint_rpt, - rule=limits.limit_growth_new_capacity_delta_constraint_rule, - ) - self.limit_degrowth_new_capacity_delta_constraint_rpt = Set( - dimen=4, initialize=limits.limit_degrowth_new_capacity_delta_indices - ) - self.limit_degrowth_new_capacity_delta_constraint = Constraint( - self.limit_degrowth_new_capacity_delta_constraint_rpt, - rule=limits.limit_degrowth_new_capacity_delta_constraint_rule, - ) - self.limit_activity_constraint = Constraint( self.limit_activity_constraint_rpt, rule=limits.limit_activity_constraint ) diff --git a/temoa/data_io/component_manifest.py b/temoa/data_io/component_manifest.py index 4f7334c4..c02513f9 100644 --- a/temoa/data_io/component_manifest.py +++ b/temoa/data_io/component_manifest.py @@ -657,66 +657,6 @@ def build_manifest(model: TemoaModel, extension_ids: Sequence[str] | None = None validation_map=(0, 1, 2), is_table_required=False, ), - LoadItem( - component=model.limit_growth_capacity, - table='limit_growth_capacity', - columns=['region', 'tech_or_group', 'operator', 'rate', 'seed'], - index_length=3, - validator_name='viable_rt', - validation_map=(0, 1), - is_period_filtered=False, - is_table_required=False, - ), - LoadItem( - component=model.limit_growth_new_capacity, - table='limit_growth_new_capacity', - columns=['region', 'tech_or_group', 'operator', 'rate', 'seed'], - index_length=3, - validator_name='viable_rt', - validation_map=(0, 1), - is_period_filtered=False, - is_table_required=False, - ), - LoadItem( - component=model.limit_growth_new_capacity_delta, - table='limit_growth_new_capacity_delta', - columns=['region', 'tech_or_group', 'operator', 'rate', 'seed'], - index_length=3, - validator_name='viable_rt', - validation_map=(0, 1), - is_period_filtered=False, - is_table_required=False, - ), - LoadItem( - component=model.limit_degrowth_capacity, - table='limit_degrowth_capacity', - columns=['region', 'tech_or_group', 'operator', 'rate', 'seed'], - index_length=3, - validator_name='viable_rt', - validation_map=(0, 1), - is_period_filtered=False, - is_table_required=False, - ), - LoadItem( - component=model.limit_degrowth_new_capacity, - table='limit_degrowth_new_capacity', - columns=['region', 'tech_or_group', 'operator', 'rate', 'seed'], - index_length=3, - validator_name='viable_rt', - validation_map=(0, 1), - is_period_filtered=False, - is_table_required=False, - ), - LoadItem( - component=model.limit_degrowth_new_capacity_delta, - table='limit_degrowth_new_capacity_delta', - columns=['region', 'tech_or_group', 'operator', 'rate', 'seed'], - index_length=3, - validator_name='viable_rt', - validation_map=(0, 1), - is_period_filtered=False, - is_table_required=False, - ), LoadItem( component=model.limit_resource, table='limit_resource', diff --git a/temoa/data_io/hybrid_loader.py b/temoa/data_io/hybrid_loader.py index 1a70ccba..8bb73ba2 100644 --- a/temoa/data_io/hybrid_loader.py +++ b/temoa/data_io/hybrid_loader.py @@ -66,12 +66,6 @@ 'limit_capacity_share': 'region', 'limit_new_capacity_share': 'region', 'limit_resource': 'region', - 'limit_growth_capacity': 'region', - 'limit_degrowth_capacity': 'region', - 'limit_growth_new_capacity': 'region', - 'limit_degrowth_new_capacity': 'region', - 'limit_growth_new_capacity_delta': 'region', - 'limit_degrowth_new_capacity_delta': 'region', } diff --git a/temoa/db_schema/temoa_schema_v4.sql b/temoa/db_schema/temoa_schema_v4.sql index 77b14635..69eef34a 100644 --- a/temoa/db_schema/temoa_schema_v4.sql +++ b/temoa/db_schema/temoa_schema_v4.sql @@ -389,78 +389,6 @@ CREATE TABLE IF NOT EXISTS operator REPLACE INTO operator VALUES('e','equal to'); REPLACE INTO operator VALUES('le','less than or equal to'); REPLACE INTO operator VALUES('ge','greater than or equal to'); -CREATE TABLE IF NOT EXISTS limit_growth_capacity -( - region TEXT, - tech_or_group TEXT, - operator TEXT NOT NULL DEFAULT "le" - REFERENCES operator (operator), - rate REAL NOT NULL DEFAULT 0, - seed REAL NOT NULL DEFAULT 0, - seed_units TEXT, - notes TEXT, - PRIMARY KEY (region, tech_or_group, operator) -); -CREATE TABLE IF NOT EXISTS limit_degrowth_capacity -( - region TEXT, - tech_or_group TEXT, - operator TEXT NOT NULL DEFAULT "le" - REFERENCES operator (operator), - rate REAL NOT NULL DEFAULT 0, - seed REAL NOT NULL DEFAULT 0, - seed_units TEXT, - notes TEXT, - PRIMARY KEY (region, tech_or_group, operator) -); -CREATE TABLE IF NOT EXISTS limit_growth_new_capacity -( - region TEXT, - tech_or_group TEXT, - operator TEXT NOT NULL DEFAULT "le" - REFERENCES operator (operator), - rate REAL NOT NULL DEFAULT 0, - seed REAL NOT NULL DEFAULT 0, - seed_units TEXT, - notes TEXT, - PRIMARY KEY (region, tech_or_group, operator) -); -CREATE TABLE IF NOT EXISTS limit_degrowth_new_capacity -( - region TEXT, - tech_or_group TEXT, - operator TEXT NOT NULL DEFAULT "le" - REFERENCES operator (operator), - rate REAL NOT NULL DEFAULT 0, - seed REAL NOT NULL DEFAULT 0, - seed_units TEXT, - notes TEXT, - PRIMARY KEY (region, tech_or_group, operator) -); -CREATE TABLE IF NOT EXISTS limit_growth_new_capacity_delta -( - region TEXT, - tech_or_group TEXT, - operator TEXT NOT NULL DEFAULT "le" - REFERENCES operator (operator), - rate REAL NOT NULL DEFAULT 0, - seed REAL NOT NULL DEFAULT 0, - seed_units TEXT, - notes TEXT, - PRIMARY KEY (region, tech_or_group, operator) -); -CREATE TABLE IF NOT EXISTS limit_degrowth_new_capacity_delta -( - region TEXT, - tech_or_group TEXT, - operator TEXT NOT NULL DEFAULT "le" - REFERENCES operator (operator), - rate REAL NOT NULL DEFAULT 0, - seed REAL NOT NULL DEFAULT 0, - seed_units TEXT, - notes TEXT, - PRIMARY KEY (region, tech_or_group, operator) -); CREATE TABLE IF NOT EXISTS limit_storage_level_fraction ( region TEXT, diff --git a/temoa/extensions/growth_rates/__init__.py b/temoa/extensions/growth_rates/__init__.py new file mode 100644 index 00000000..7156dc7f --- /dev/null +++ b/temoa/extensions/growth_rates/__init__.py @@ -0,0 +1,3 @@ +from temoa.extensions.growth_rates.extension import GROWTH_RATES_EXTENSION + +__all__ = ['GROWTH_RATES_EXTENSION'] diff --git a/temoa/extensions/growth_rates/components/__init__.py b/temoa/extensions/growth_rates/components/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/temoa/extensions/growth_rates/components/growth_capacity.py b/temoa/extensions/growth_rates/components/growth_capacity.py new file mode 100644 index 00000000..14a15ca6 --- /dev/null +++ b/temoa/extensions/growth_rates/components/growth_capacity.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from pyomo.environ import Constraint, quicksum, value + +import temoa.components.geography as geography +import temoa.components.technology as technology +from temoa.components.utils import Operator, get_adjusted_existing_capacity, operator_expression + +if TYPE_CHECKING: + from temoa.extensions.growth_rates.core.model import GrowthRatesModel + from temoa.types import ExprLike, Period, Region, Technology + + +def limit_growth_capacity_indices( + model: GrowthRatesModel, +) -> set[tuple[Region, Period, Technology, str]]: + return { + (r, p, t, op) + for r, t, op in model.limit_growth_capacity.sparse_keys() + for p in model.time_optimize + } + + +def limit_degrowth_capacity_indices( + model: GrowthRatesModel, +) -> set[tuple[Region, Period, Technology, str]]: + return { + (r, p, t, op) + for r, t, op in model.limit_degrowth_capacity.sparse_keys() + for p in model.time_optimize + } + + +def limit_growth_capacity_constraint_rule( + model: GrowthRatesModel, r: Region, p: Period, t: Technology, op: str +) -> ExprLike: + return limit_growth_capacity(model, r, p, t, op, False) + + +def limit_degrowth_capacity_constraint_rule( + model: GrowthRatesModel, r: Region, p: Period, t: Technology, op: str +) -> ExprLike: + return limit_growth_capacity(model, r, p, t, op, True) + + +def limit_growth_capacity( + model: GrowthRatesModel, + r: Region, + p: Period, + t: Technology, + op: str, + degrowth: bool = False, +) -> ExprLike: + r""" + Constrain the change of capacity available between periods. + Forces the model to ramp up and down the availability of new technologies + more smoothly. Has constant (seed, :math:`S_{r,t}`) and proportional + (rate, :math:`R_{r,t}`) terms. This can be defined for a technology group + instead of one technology, in which case, capacity available is summed over + all technologies in the group. In the first period, previous available + capacity :math:`\mathbf{CAPAVL}_{r,p,t}` is replaced by previous existing + capacity, if any can be found. + + .. math:: + :label: Limit (De)Growth Capacity + + \begin{aligned}\text{Growth:}\\ + &\mathbf{CAPAVL}_{r,p,t} + \quad \le, \ge, \text{or} = \quad + S_{r,t} + (1+R_{r,t}) \cdot \mathbf{CAPAVL}_{r,p_{prev},t} + \end{aligned} + + \qquad \forall \{r, p, t\} \in \Theta_{\text{limit\_growth\_capacity}} + + + \begin{aligned}\text{Degrowth:}\\ + &\mathbf{CAPAVL}_{r,p_{prev},t} + \quad \le, \ge, \text{or} = \quad S_{r,t} + (1+R_{r,t}) \cdot \mathbf{CAPAVL}_{r,p,t} + \end{aligned} + + \qquad \forall \{r, p, t\} \in \Theta_{\text{limit\_degrowth\_capacity}} + """ + + regions = geography.gather_group_regions(model, r) + techs = technology.gather_group_techs(model, t) + + growth = model.limit_degrowth_capacity if degrowth else model.limit_growth_capacity + rate = 1 + value(growth[r, t, op][0]) + seed = value(growth[r, t, op][1]) + cap_rpt = model.v_capacity_available_by_period_and_tech + + cap_indices = {(_r, _p, _t) for _r, _p, _t in cap_rpt.keys() if _t in techs and _r in regions} + periods = sorted({_p for _r, _p, _t in cap_rpt}) + + if len(periods) == 0: + return Constraint.Skip + + capacity = quicksum(cap_rpt[_r, _p, _t] for _r, _p, _t in cap_indices if _p == p) + + if p == model.time_optimize.first(): + p_prev = model.time_exist.last() + capacity_prev = sum( + get_adjusted_existing_capacity(model, _r, _t, _v) + * value(model.process_life_frac[_r, p_prev, _t, _v]) + for _r, _t, _v in model.existing_capacity.sparse_keys() + if _r in regions + and _t in techs + and _v + value(model.lifetime_process[_r, _t, _v]) > p_prev + ) + else: + p_prev = model.time_optimize.prev(p) + capacity_prev = quicksum(cap_rpt[_r, _p, _t] for _r, _p, _t in cap_indices if _p == p_prev) + + if degrowth: + expr = operator_expression(capacity_prev, Operator(op), seed + capacity * rate) + else: + expr = operator_expression(capacity, Operator(op), seed + capacity_prev * rate) + + if isinstance(expr, bool): + return Constraint.Skip + return expr diff --git a/temoa/extensions/growth_rates/components/growth_new_capacity.py b/temoa/extensions/growth_rates/components/growth_new_capacity.py new file mode 100644 index 00000000..e0115be0 --- /dev/null +++ b/temoa/extensions/growth_rates/components/growth_new_capacity.py @@ -0,0 +1,121 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from pyomo.environ import Constraint, quicksum, value + +import temoa.components.geography as geography +import temoa.components.technology as technology +from temoa.components.utils import Operator, operator_expression + +if TYPE_CHECKING: + from temoa.extensions.growth_rates.core.model import GrowthRatesModel + from temoa.types import ExprLike, Period, Region, Technology + + +def limit_growth_new_capacity_indices( + model: GrowthRatesModel, +) -> set[tuple[Region, Period, Technology, str]]: + return { + (r, p, t, op) + for r, t, op in model.limit_growth_new_capacity.sparse_keys() + for p in model.time_optimize + } + + +def limit_degrowth_new_capacity_indices( + model: GrowthRatesModel, +) -> set[tuple[Region, Period, Technology, str]]: + return { + (r, p, t, op) + for r, t, op in model.limit_degrowth_new_capacity.sparse_keys() + for p in model.time_optimize + } + + +def limit_growth_new_capacity_constraint_rule( + model: GrowthRatesModel, r: Region, p: Period, t: Technology, op: str +) -> ExprLike: + return limit_growth_new_capacity(model, r, p, t, op, False) + + +def limit_degrowth_new_capacity_constraint_rule( + model: GrowthRatesModel, r: Region, p: Period, t: Technology, op: str +) -> ExprLike: + return limit_growth_new_capacity(model, r, p, t, op, True) + + +def limit_growth_new_capacity( + model: GrowthRatesModel, + r: Region, + p: Period, + t: Technology, + op: str, + degrowth: bool = False, +) -> ExprLike: + r""" + Constrain the change of new capacity deployed between periods. + Forces the model to ramp up and down the deployment of new technologies + more smoothly. Has constant (seed, :math:`S_{r,t}`) and proportional + (rate, :math:`R_{r,t}`) terms. This can be defined for a technology group + instead of one technology, in which case, new capacity is summed over + all technologies in the group. In the first period, previous new capacity + :math:`\mathbf{NCAP}_{r,t,v_prev}` is replaced by previous existing capacity, + if any can be found. + + .. math:: + :label: Limit (De)Growth New Capacity + + \begin{aligned}\text{Growth:}\\ + &\mathbf{NCAP}_{r,t,v} + \quad \le, \ge, \text{or} = \quad + S_{r,t} + (1+R_{r,t}) \cdot \mathbf{NCAP}_{r,t,v_{prev}} + \text{ where } v=p + \end{aligned} + + \qquad \forall \{r, p, t\} \in \Theta_{\text{limit\_growth\_capacity}} + + \begin{aligned}\text{Degrowth:}\\ + &\mathbf{NCAP}_{r,t,v_{prev}} + \quad \le, \ge, \text{or} = \quad S_{r,t} + (1+R_{r,t}) \cdot \mathbf{NCAP}_{r,t,v} + \text{ where } v=p + \end{aligned} + + \qquad \forall \{r, p, t\} \in \Theta_{\text{limit\_degrowth\_capacity}} + """ + + regions = geography.gather_group_regions(model, r) + techs = technology.gather_group_techs(model, t) + + growth = model.limit_degrowth_new_capacity if degrowth else model.limit_growth_new_capacity + rate = 1 + value(growth[r, t, op][0]) + seed = value(growth[r, t, op][1]) + new_cap_rtv = model.v_new_capacity + + cap_rtv = {(_r, _t, _v) for _r, _t, _v in new_cap_rtv.keys() if _t in techs and _r in regions} + periods = sorted({_v for _r, _t, _v in cap_rtv}) + + if len(periods) == 0: + return Constraint.Skip + + new_cap = quicksum(new_cap_rtv[_r, _t, _v] for _r, _t, _v in cap_rtv if _v == p) + + if p == model.time_optimize.first(): + p_prev = model.time_exist.last() + new_cap_prev = sum( + value(model.existing_capacity[_r, _t, _v]) + for _r, _t, _v in model.existing_capacity.sparse_keys() + if _r in regions and _t in techs and _v == p_prev + ) + else: + p_prev = model.time_optimize.prev(p) + new_cap_prev = sum(new_cap_rtv[_r, _t, _v] for _r, _t, _v in cap_rtv if _v == p_prev) + + if degrowth: + expr = operator_expression(new_cap_prev, Operator(op), seed + new_cap * rate) + else: + expr = operator_expression(new_cap, Operator(op), seed + new_cap_prev * rate) + + if isinstance(expr, bool): + return Constraint.Skip + return expr diff --git a/temoa/extensions/growth_rates/components/growth_new_capacity_delta.py b/temoa/extensions/growth_rates/components/growth_new_capacity_delta.py new file mode 100644 index 00000000..c06b1290 --- /dev/null +++ b/temoa/extensions/growth_rates/components/growth_new_capacity_delta.py @@ -0,0 +1,148 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from pyomo.environ import Constraint, value + +import temoa.components.geography as geography +import temoa.components.technology as technology +from temoa.components.utils import Operator, operator_expression + +if TYPE_CHECKING: + from temoa.extensions.growth_rates.core.model import GrowthRatesModel + from temoa.types import ExprLike, Period, Region, Technology + + +def limit_growth_new_capacity_delta_indices( + model: GrowthRatesModel, +) -> set[tuple[Region, Period, Technology, str]]: + return { + (r, p, t, op) + for r, t, op in model.limit_growth_new_capacity_delta.sparse_keys() + for p in model.time_optimize + } + + +def limit_degrowth_new_capacity_delta_indices( + model: GrowthRatesModel, +) -> set[tuple[Region, Period, Technology, str]]: + return { + (r, p, t, op) + for r, t, op in model.limit_degrowth_new_capacity_delta.sparse_keys() + for p in model.time_optimize + } + + +def limit_growth_new_capacity_delta_constraint_rule( + model: GrowthRatesModel, r: Region, p: Period, t: Technology, op: str +) -> ExprLike: + return limit_growth_new_capacity_delta(model, r, p, t, op, False) + + +def limit_degrowth_new_capacity_delta_constraint_rule( + model: GrowthRatesModel, r: Region, p: Period, t: Technology, op: str +) -> ExprLike: + return limit_growth_new_capacity_delta(model, r, p, t, op, True) + + +def limit_growth_new_capacity_delta( + model: GrowthRatesModel, + r: Region, + p: Period, + t: Technology, + op: str, + degrowth: bool = False, +) -> ExprLike: + r""" + Constrain the acceleration of new capacity deployed between periods. + Forces the model to ramp up and down the change in deployment of new technologies + more smoothly. Has constant (seed, :math:`S_{r,t}`) and proportional + (rate, :math:`R_{r,t}`) terms. It is recommended to leave the rate term empty + as it would prevent the possibility of inflection in the rate of deployment. + This constraint can be defined for a technology group instead of one technology, + in which case, new capacity is summed over all technologies in the group. In the + first period, previous new capacities are replaced by previous existing capacities, + if any can be found. + + .. math:: + :label: Limit (De)Growth New Capacity Delta + + \begin{aligned}\text{Growth:}\\ + &\mathbf{NCAP}_{r,t,v_i} - \mathbf{NCAP}_{r,t,v_{i-1}} + \quad \le, \ge, \text{or} = \quad S_{r,t} + (1+R_{r,t}) \cdot + (\mathbf{NCAP}_{r,t,v_{i-1}} - \mathbf{NCAP}_{r,t,v_{i-2}}) + \end{aligned} + + \text{ where } v_i=p + + \qquad \forall \{r, p, t\} \in \Theta_{\text{limit\_growth\_capacityDelta}} + + \begin{aligned}\text{Degrowth:}\\ + &\mathbf{NCAP}_{r,t,v_{i-1}} - \mathbf{NCAP}_{r,t,v_{i-2}} + \quad \le, \ge, \text{or} = \quad + S_{r,t} + (1+R_{r,t}) \cdot (\mathbf{NCAP}_{r,t,v_i} - \mathbf{NCAP}_{r,t,v_{i-1}}) + \end{aligned} + + \text{ where } v_i=p + + \qquad \forall \{r, p, t\} \in \Theta_{\text{limit\_degrowth\_capacityDelta}} + """ + + regions = geography.gather_group_regions(model, r) + techs = technology.gather_group_techs(model, t) + + growth = ( + model.limit_degrowth_new_capacity_delta + if degrowth + else model.limit_growth_new_capacity_delta + ) + rate = 1 + value(growth[r, t, op][0]) + seed = value(growth[r, t, op][1]) + new_cap_rtv = model.v_new_capacity + + cap_rtv = {(_r, _t, _v) for _r, _t, _v in new_cap_rtv.keys() if _t in techs and _r in regions} + periods = sorted({_v for _r, _t, _v in cap_rtv}) + + if len(periods) == 0: + return Constraint.Skip + + new_cap = sum(new_cap_rtv[_r, _t, _v] for _r, _t, _v in cap_rtv if _v == p) + + if p == model.time_optimize.first(): + p_prev = model.time_exist.last() + new_cap_prev = sum( + value(model.existing_capacity[_r, _t, _v]) + for _r, _t, _v in model.existing_capacity.sparse_keys() + if _r in regions and _t in techs and _v == p_prev + ) + p_prev2 = model.time_exist.prev(p_prev) + new_cap_prev2 = sum( + value(model.existing_capacity[_r, _t, _v]) + for _r, _t, _v in model.existing_capacity.sparse_keys() + if _r in regions and _t in techs and _v == p_prev2 + ) + else: + p_prev = model.time_optimize.prev(p) + new_cap_prev = sum(new_cap_rtv[_r, _t, _v] for _r, _t, _v in cap_rtv if _v == p_prev) + if p == model.time_optimize.at(2): + p_prev2 = model.time_exist.last() + new_cap_prev2 = sum( + value(model.existing_capacity[_r, _t, _v]) + for _r, _t, _v in model.existing_capacity.sparse_keys() + if _r in regions and _t in techs and _v == p_prev2 + ) + else: + p_prev2 = model.time_optimize.prev(p_prev) + new_cap_prev2 = sum(new_cap_rtv[_r, _t, _v] for _r, _t, _v in cap_rtv if _v == p_prev2) + + nc_delta_prev = new_cap_prev - new_cap_prev2 + nc_delta = new_cap - new_cap_prev + + if degrowth: + expr = operator_expression(nc_delta_prev, Operator(op), seed + nc_delta * rate) + else: + expr = operator_expression(nc_delta, Operator(op), seed + nc_delta_prev * rate) + + if isinstance(expr, bool): + return Constraint.Skip + return expr diff --git a/temoa/extensions/growth_rates/core/__init__.py b/temoa/extensions/growth_rates/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/temoa/extensions/growth_rates/core/model.py b/temoa/extensions/growth_rates/core/model.py new file mode 100644 index 00000000..30d846d9 --- /dev/null +++ b/temoa/extensions/growth_rates/core/model.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, cast + +from pyomo.environ import Any, Constraint, Param, Set + +from temoa.extensions.growth_rates.components import ( + growth_capacity, + growth_new_capacity, + growth_new_capacity_delta, +) + +if TYPE_CHECKING: + from temoa.core.model import TemoaModel + + class GrowthRatesModel(TemoaModel): + """TemoaModel extended with growth-rates components. + + This subtype exists only for static type checking. At runtime the + growth-rates components are attached to the core ``TemoaModel`` + instance by :func:`register_model_components`. + """ + + # Params + limit_growth_capacity: Param + limit_degrowth_capacity: Param + limit_growth_new_capacity: Param + limit_degrowth_new_capacity: Param + limit_growth_new_capacity_delta: Param + limit_degrowth_new_capacity_delta: Param + + # Constraint index sets + limit_growth_capacity_constraint_rpt: Set + limit_degrowth_capacity_constraint_rpt: Set + limit_growth_new_capacity_constraint_rpt: Set + limit_degrowth_new_capacity_constraint_rpt: Set + limit_growth_new_capacity_delta_constraint_rpt: Set + limit_degrowth_new_capacity_delta_constraint_rpt: Set + + # Constraints + limit_growth_capacity_constraint: Constraint + limit_degrowth_capacity_constraint: Constraint + limit_growth_new_capacity_constraint: Constraint + limit_degrowth_new_capacity_constraint: Constraint + limit_growth_new_capacity_delta_constraint: Constraint + limit_degrowth_new_capacity_delta_constraint: Constraint + + +def register_model_components(model: TemoaModel) -> None: + """Register growth rates model components on the core Temoa model.""" + m = cast('GrowthRatesModel', model) + + m.limit_growth_capacity = Param( + m.regional_global_indices, m.tech_or_group, m.operator, within=Any + ) + m.limit_degrowth_capacity = Param( + m.regional_global_indices, m.tech_or_group, m.operator, within=Any + ) + m.limit_growth_new_capacity = Param( + m.regional_global_indices, m.tech_or_group, m.operator, within=Any + ) + m.limit_degrowth_new_capacity = Param( + m.regional_global_indices, m.tech_or_group, m.operator, within=Any + ) + m.limit_growth_new_capacity_delta = Param( + m.regional_global_indices, m.tech_or_group, m.operator, within=Any + ) + m.limit_degrowth_new_capacity_delta = Param( + m.regional_global_indices, m.tech_or_group, m.operator, within=Any + ) + + m.limit_growth_capacity_constraint_rpt = Set( + dimen=4, initialize=growth_capacity.limit_growth_capacity_indices + ) + m.limit_growth_capacity_constraint = Constraint( + m.limit_growth_capacity_constraint_rpt, + rule=growth_capacity.limit_growth_capacity_constraint_rule, + ) + m.limit_degrowth_capacity_constraint_rpt = Set( + dimen=4, initialize=growth_capacity.limit_degrowth_capacity_indices + ) + m.limit_degrowth_capacity_constraint = Constraint( + m.limit_degrowth_capacity_constraint_rpt, + rule=growth_capacity.limit_degrowth_capacity_constraint_rule, + ) + + m.limit_growth_new_capacity_constraint_rpt = Set( + dimen=4, initialize=growth_new_capacity.limit_growth_new_capacity_indices + ) + m.limit_growth_new_capacity_constraint = Constraint( + m.limit_growth_new_capacity_constraint_rpt, + rule=growth_new_capacity.limit_growth_new_capacity_constraint_rule, + ) + m.limit_degrowth_new_capacity_constraint_rpt = Set( + dimen=4, initialize=growth_new_capacity.limit_degrowth_new_capacity_indices + ) + m.limit_degrowth_new_capacity_constraint = Constraint( + m.limit_degrowth_new_capacity_constraint_rpt, + rule=growth_new_capacity.limit_degrowth_new_capacity_constraint_rule, + ) + + m.limit_growth_new_capacity_delta_constraint_rpt = Set( + dimen=4, initialize=growth_new_capacity_delta.limit_growth_new_capacity_delta_indices + ) + m.limit_growth_new_capacity_delta_constraint = Constraint( + m.limit_growth_new_capacity_delta_constraint_rpt, + rule=growth_new_capacity_delta.limit_growth_new_capacity_delta_constraint_rule, + ) + m.limit_degrowth_new_capacity_delta_constraint_rpt = Set( + dimen=4, initialize=growth_new_capacity_delta.limit_degrowth_new_capacity_delta_indices + ) + m.limit_degrowth_new_capacity_delta_constraint = Constraint( + m.limit_degrowth_new_capacity_delta_constraint_rpt, + rule=growth_new_capacity_delta.limit_degrowth_new_capacity_delta_constraint_rule, + ) diff --git a/temoa/extensions/growth_rates/data_manifest.py b/temoa/extensions/growth_rates/data_manifest.py new file mode 100644 index 00000000..d81b1d35 --- /dev/null +++ b/temoa/extensions/growth_rates/data_manifest.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, cast + +from temoa.data_io.loader_manifest import LoadItem + +if TYPE_CHECKING: + from temoa.core.model import TemoaModel + from temoa.extensions.growth_rates.core.model import GrowthRatesModel + + +def build_manifest_items(model: TemoaModel) -> list[LoadItem]: + """Return LoadItems for growth/degrowth constraints.""" + m = cast('GrowthRatesModel', model) + return [ + LoadItem( + component=m.limit_growth_capacity, + table='limit_growth_capacity', + columns=['region', 'tech_or_group', 'operator', 'rate', 'seed'], + index_length=3, + validator_name='viable_rt', + validation_map=(0, 1), + is_period_filtered=False, + is_table_required=False, + ), + LoadItem( + component=m.limit_growth_new_capacity, + table='limit_growth_new_capacity', + columns=['region', 'tech_or_group', 'operator', 'rate', 'seed'], + index_length=3, + validator_name='viable_rt', + validation_map=(0, 1), + is_period_filtered=False, + is_table_required=False, + ), + LoadItem( + component=m.limit_growth_new_capacity_delta, + table='limit_growth_new_capacity_delta', + columns=['region', 'tech_or_group', 'operator', 'rate', 'seed'], + index_length=3, + validator_name='viable_rt', + validation_map=(0, 1), + is_period_filtered=False, + is_table_required=False, + ), + LoadItem( + component=m.limit_degrowth_capacity, + table='limit_degrowth_capacity', + columns=['region', 'tech_or_group', 'operator', 'rate', 'seed'], + index_length=3, + validator_name='viable_rt', + validation_map=(0, 1), + is_period_filtered=False, + is_table_required=False, + ), + LoadItem( + component=m.limit_degrowth_new_capacity, + table='limit_degrowth_new_capacity', + columns=['region', 'tech_or_group', 'operator', 'rate', 'seed'], + index_length=3, + validator_name='viable_rt', + validation_map=(0, 1), + is_period_filtered=False, + is_table_required=False, + ), + LoadItem( + component=m.limit_degrowth_new_capacity_delta, + table='limit_degrowth_new_capacity_delta', + columns=['region', 'tech_or_group', 'operator', 'rate', 'seed'], + index_length=3, + validator_name='viable_rt', + validation_map=(0, 1), + is_period_filtered=False, + is_table_required=False, + ), + ] diff --git a/temoa/extensions/growth_rates/extension.py b/temoa/extensions/growth_rates/extension.py new file mode 100644 index 00000000..5c148b61 --- /dev/null +++ b/temoa/extensions/growth_rates/extension.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +from pathlib import Path + +from temoa.extensions.framework import ExtensionSpec +from temoa.extensions.growth_rates.core.model import register_model_components +from temoa.extensions.growth_rates.data_manifest import build_manifest_items + + +GROWTH_RATES_EXTENSION = ExtensionSpec( + extension_id='growth_rates', + owned_tables=( + 'limit_growth_capacity', + 'limit_degrowth_capacity', + 'limit_growth_new_capacity', + 'limit_degrowth_new_capacity', + 'limit_growth_new_capacity_delta', + 'limit_degrowth_new_capacity_delta', + ), + regional_group_tables={ + 'limit_growth_capacity': 'region', + 'limit_degrowth_capacity': 'region', + 'limit_growth_new_capacity': 'region', + 'limit_degrowth_new_capacity': 'region', + 'limit_growth_new_capacity_delta': 'region', + 'limit_degrowth_new_capacity_delta': 'region', + }, + register_model_components=register_model_components, + build_manifest_items=build_manifest_items, + schema_sql_path=str(Path(__file__).parents[0] / 'tables.sql'), + fail_if_tables_populated_when_disabled=True, +) diff --git a/temoa/extensions/growth_rates/tables.sql b/temoa/extensions/growth_rates/tables.sql new file mode 100644 index 00000000..e2fece83 --- /dev/null +++ b/temoa/extensions/growth_rates/tables.sql @@ -0,0 +1,72 @@ +CREATE TABLE IF NOT EXISTS limit_growth_capacity +( + region TEXT, + tech_or_group TEXT, + operator TEXT NOT NULL DEFAULT "le" + REFERENCES operator (operator), + rate REAL NOT NULL DEFAULT 0, + seed REAL NOT NULL DEFAULT 0, + seed_units TEXT, + notes TEXT, + PRIMARY KEY (region, tech_or_group, operator) +); +CREATE TABLE IF NOT EXISTS limit_degrowth_capacity +( + region TEXT, + tech_or_group TEXT, + operator TEXT NOT NULL DEFAULT "le" + REFERENCES operator (operator), + rate REAL NOT NULL DEFAULT 0, + seed REAL NOT NULL DEFAULT 0, + seed_units TEXT, + notes TEXT, + PRIMARY KEY (region, tech_or_group, operator) +); +CREATE TABLE IF NOT EXISTS limit_growth_new_capacity +( + region TEXT, + tech_or_group TEXT, + operator TEXT NOT NULL DEFAULT "le" + REFERENCES operator (operator), + rate REAL NOT NULL DEFAULT 0, + seed REAL NOT NULL DEFAULT 0, + seed_units TEXT, + notes TEXT, + PRIMARY KEY (region, tech_or_group, operator) +); +CREATE TABLE IF NOT EXISTS limit_degrowth_new_capacity +( + region TEXT, + tech_or_group TEXT, + operator TEXT NOT NULL DEFAULT "le" + REFERENCES operator (operator), + rate REAL NOT NULL DEFAULT 0, + seed REAL NOT NULL DEFAULT 0, + seed_units TEXT, + notes TEXT, + PRIMARY KEY (region, tech_or_group, operator) +); +CREATE TABLE IF NOT EXISTS limit_growth_new_capacity_delta +( + region TEXT, + tech_or_group TEXT, + operator TEXT NOT NULL DEFAULT "le" + REFERENCES operator (operator), + rate REAL NOT NULL DEFAULT 0, + seed REAL NOT NULL DEFAULT 0, + seed_units TEXT, + notes TEXT, + PRIMARY KEY (region, tech_or_group, operator) +); +CREATE TABLE IF NOT EXISTS limit_degrowth_new_capacity_delta +( + region TEXT, + tech_or_group TEXT, + operator TEXT NOT NULL DEFAULT "le" + REFERENCES operator (operator), + rate REAL NOT NULL DEFAULT 0, + seed REAL NOT NULL DEFAULT 0, + seed_units TEXT, + notes TEXT, + PRIMARY KEY (region, tech_or_group, operator) +); diff --git a/tests/testing_configs/config_myopic_capacities.toml b/tests/testing_configs/config_myopic_capacities.toml index 3ab763c4..db2d1153 100644 --- a/tests/testing_configs/config_myopic_capacities.toml +++ b/tests/testing_configs/config_myopic_capacities.toml @@ -1,5 +1,6 @@ scenario = "test myopic capacities" scenario_mode = "myopic" +extensions= ["growth_rates"] input_database = "tests/testing_outputs/myopic_capacities.sqlite" output_database = "tests/testing_outputs/myopic_capacities.sqlite" neos = false diff --git a/tests/testing_data/mediumville_sets.json b/tests/testing_data/mediumville_sets.json index c86baf5c..acc5b16e 100644 --- a/tests/testing_data/mediumville_sets.json +++ b/tests/testing_data/mediumville_sets.json @@ -39,13 +39,7 @@ "limit_annual_capacity_factor_constraint_rtvo": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", "limit_capacity_constraint_rpt": "118826dabd14af75ae09adbd5bdbba750528b3d59907cbfba3690223ffc7def7", "limit_capacity_share_constraint_rpgg": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", - "limit_degrowth_capacity_constraint_rpt": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", - "limit_degrowth_new_capacity_constraint_rpt": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", - "limit_degrowth_new_capacity_delta_constraint_rpt": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", "limit_emission_constraint_rpe": "9bf30514ecc261370f67cd0c2e18194f45ff14c15764d274cc342a5a27eaf2a0", - "limit_growth_capacity_constraint_rpt": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", - "limit_growth_new_capacity_constraint_rpt": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", - "limit_growth_new_capacity_delta_constraint_rpt": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", "limit_new_capacity_constraint_rtv": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", "limit_new_capacity_share_constraint_rggv": "296e4148565f752973655692bae04ebf50877bd13fe9cec8f9563f347a61d885", "limit_resource_constraint_rt": "b9b44ae8bc9642da0a81202877bd69f0b35ce193e31590dd3325ac0828edb5cf", diff --git a/tests/testing_data/test_system_sets.json b/tests/testing_data/test_system_sets.json index 6d73af97..f5a33a54 100644 --- a/tests/testing_data/test_system_sets.json +++ b/tests/testing_data/test_system_sets.json @@ -39,13 +39,7 @@ "limit_annual_capacity_factor_constraint_rtvo": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", "limit_capacity_constraint_rpt": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", "limit_capacity_share_constraint_rpgg": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", - "limit_degrowth_capacity_constraint_rpt": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", - "limit_degrowth_new_capacity_constraint_rpt": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", - "limit_degrowth_new_capacity_delta_constraint_rpt": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", "limit_emission_constraint_rpe": "6f11e4ee041970584903d5afba7ee1147824b233260422564fdf0c701e9fabde", - "limit_growth_capacity_constraint_rpt": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", - "limit_growth_new_capacity_constraint_rpt": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", - "limit_growth_new_capacity_delta_constraint_rpt": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", "limit_new_capacity_constraint_rtv": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", "limit_new_capacity_share_constraint_rggv": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", "limit_resource_constraint_rt": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", diff --git a/tests/testing_data/utopia_sets.json b/tests/testing_data/utopia_sets.json index 53d8b2a8..6d9adc1a 100644 --- a/tests/testing_data/utopia_sets.json +++ b/tests/testing_data/utopia_sets.json @@ -39,13 +39,7 @@ "limit_annual_capacity_factor_constraint_rtvo": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", "limit_capacity_constraint_rpt": "5a43de033187da68c612c5bcef6a76edb6ab9806464b6d865e46d289ccbbd815", "limit_capacity_share_constraint_rpgg": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", - "limit_degrowth_capacity_constraint_rpt": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", - "limit_degrowth_new_capacity_constraint_rpt": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", - "limit_degrowth_new_capacity_delta_constraint_rpt": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", "limit_emission_constraint_rpe": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", - "limit_growth_capacity_constraint_rpt": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", - "limit_growth_new_capacity_constraint_rpt": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", - "limit_growth_new_capacity_delta_constraint_rpt": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", "limit_new_capacity_constraint_rtv": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", "limit_new_capacity_share_constraint_rggv": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", "limit_resource_constraint_rt": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", From 6dabfb19935d75b8b51479bd9ab8b5e4b5d9b384 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Mon, 6 Jul 2026 13:52:29 -0400 Subject: [PATCH 17/28] Fix bugs for Bugs Signed-off-by: Davey Elder --- temoa/cli.py | 9 ++-- temoa/components/limits.py | 6 +-- temoa/components/utils.py | 10 ++-- temoa/data_io/hybrid_loader.py | 2 +- temoa/data_io/loader_manifest.py | 2 + .../components/growth_capacity.py | 21 ++++---- .../components/growth_new_capacity.py | 17 ++++--- .../components/growth_new_capacity_delta.py | 48 +++++++++++-------- .../stochastics/scenario_creator.py | 5 +- 9 files changed, 71 insertions(+), 49 deletions(-) diff --git a/temoa/cli.py b/temoa/cli.py index cbcc234c..9feee14d 100644 --- a/temoa/cli.py +++ b/temoa/cli.py @@ -564,6 +564,7 @@ def _copy_tutorial_resources(target_config: Path, target_database: Path) -> None target_config: Path where configuration file should be copied target_database: Path where database file should be created """ + import contextlib import sqlite3 try: @@ -587,7 +588,7 @@ def _copy_tutorial_resources(target_config: Path, target_database: Path) -> None schema_content = schema_resource.read_text(encoding='utf-8') sql_content = sql_resource.read_text(encoding='utf-8') - with sqlite3.connect(target_database) as conn: + with contextlib.closing(sqlite3.connect(target_database)) as conn: conn.executescript(schema_content) conn.execute('PRAGMA foreign_keys = OFF;') conn.executescript(sql_content) @@ -597,6 +598,7 @@ def _copy_tutorial_resources(target_config: Path, target_database: Path) -> None raise sqlite3.IntegrityError( f'Foreign key check failed after tutorial data load: {fk_violations[:5]}' ) + conn.commit() # Copy Monte Carlo settings with mc_settings_resource.open('rb') as source: @@ -640,7 +642,7 @@ def _copy_tutorial_resources(target_config: Path, target_database: Path) -> None # Generate database from canonical schema and tutorial data source sql_content = fallback_sql.read_text(encoding='utf-8') - with sqlite3.connect(target_database) as conn: + with contextlib.closing(sqlite3.connect(target_database)) as conn: conn.executescript(fallback_schema.read_text(encoding='utf-8')) conn.execute('PRAGMA foreign_keys = OFF;') conn.executescript(sql_content) @@ -649,7 +651,8 @@ def _copy_tutorial_resources(target_config: Path, target_database: Path) -> None if fk_violations: raise sqlite3.IntegrityError( f'Foreign key check failed after tutorial data load: {fk_violations[:5]}' - ) from e + ) from None + conn.commit() # Copy mc_settings from fallback shutil.copy2(fallback_mc, target_config.parent / 'mc_settings.csv') diff --git a/temoa/components/limits.py b/temoa/components/limits.py index 0145073e..702adb17 100644 --- a/temoa/components/limits.py +++ b/temoa/components/limits.py @@ -444,10 +444,8 @@ def limit_annual_capacity_factor_constraint( regions = geography.gather_group_regions(model, r) techs = technology.gather_group_techs(model, t) - if TYPE_CHECKING: - activity_rptvo = cast('Expression', 0) - else: - activity_rptvo = 0 + activity_rptvo = 0 + for _t in techs: if _t not in model.tech_annual: activity_rptvo += quicksum( diff --git a/temoa/components/utils.py b/temoa/components/utils.py index daa2b421..7a8c91fa 100644 --- a/temoa/components/utils.py +++ b/temoa/components/utils.py @@ -12,10 +12,10 @@ from logging import getLogger from typing import TYPE_CHECKING -from pyomo.environ import value +from pyomo.environ import Expression, value if TYPE_CHECKING: - from pyomo.core import Expression + from pyomo.core.expr.numeric_expr import NumericValue from temoa.core.model import TemoaModel from temoa.types import ( @@ -39,7 +39,11 @@ class Operator(StrEnum): GREATER_EQUAL = 'ge' -def operator_expression(lhs: Expression, operator: Operator, rhs: Expression) -> ExprLike: +def operator_expression( + lhs: Expression | NumericValue | float, + operator: Operator, + rhs: Expression | NumericValue | float, +) -> ExprLike: match operator: case Operator.EQUAL: return lhs == rhs diff --git a/temoa/data_io/hybrid_loader.py b/temoa/data_io/hybrid_loader.py index 8bb73ba2..6dad5030 100644 --- a/temoa/data_io/hybrid_loader.py +++ b/temoa/data_io/hybrid_loader.py @@ -246,7 +246,7 @@ def create_data_dict(self, myopic_index: MyopicIndex | None = None) -> dict[str, for item in self.manifest: # 1. Fetch data from the database raw_data = self._fetch_data(cur, item, myopic_index) - if item.index_length: + if item.index_length and len(item.columns) - item.index_length > 1: raw_data = [ (*row[0 : item.index_length], row[item.index_length :]) for row in raw_data ] diff --git a/temoa/data_io/loader_manifest.py b/temoa/data_io/loader_manifest.py index 5723d9e9..886aaa4e 100644 --- a/temoa/data_io/loader_manifest.py +++ b/temoa/data_io/loader_manifest.py @@ -32,6 +32,8 @@ class LoadItem: table: The name of the source table in the SQLite database. columns: A list of column names to select from the table. The last column is assumed to be the value for a `Param`. + index_length: Optional. The number of columns that form the index for + a `Param`, for tables with multiple value columns. validator_name: Optional. The name of a `ViableSet` attribute on the `HybridLoader` instance, used for source-trace filtering. validation_map: A tuple indicating which column indices in the data diff --git a/temoa/extensions/growth_rates/components/growth_capacity.py b/temoa/extensions/growth_rates/components/growth_capacity.py index 14a15ca6..a091e260 100644 --- a/temoa/extensions/growth_rates/components/growth_capacity.py +++ b/temoa/extensions/growth_rates/components/growth_capacity.py @@ -99,16 +99,19 @@ def limit_growth_capacity( capacity = quicksum(cap_rpt[_r, _p, _t] for _r, _p, _t in cap_indices if _p == p) + capacity_prev = 0.0 + if p == model.time_optimize.first(): - p_prev = model.time_exist.last() - capacity_prev = sum( - get_adjusted_existing_capacity(model, _r, _t, _v) - * value(model.process_life_frac[_r, p_prev, _t, _v]) - for _r, _t, _v in model.existing_capacity.sparse_keys() - if _r in regions - and _t in techs - and _v + value(model.lifetime_process[_r, _t, _v]) > p_prev - ) + if model.time_exist: + p_prev = model.time_exist.last() + capacity_prev = quicksum( + get_adjusted_existing_capacity(model, _r, _t, _v) + * value(model.process_life_frac[_r, p_prev, _t, _v]) + for _r, _t, _v in model.existing_capacity.sparse_keys() + if _r in regions + and _t in techs + and _v + value(model.lifetime_process[_r, _t, _v]) > p_prev + ) else: p_prev = model.time_optimize.prev(p) capacity_prev = quicksum(cap_rpt[_r, _p, _t] for _r, _p, _t in cap_indices if _p == p_prev) diff --git a/temoa/extensions/growth_rates/components/growth_new_capacity.py b/temoa/extensions/growth_rates/components/growth_new_capacity.py index e0115be0..51c279fd 100644 --- a/temoa/extensions/growth_rates/components/growth_new_capacity.py +++ b/temoa/extensions/growth_rates/components/growth_new_capacity.py @@ -100,16 +100,19 @@ def limit_growth_new_capacity( new_cap = quicksum(new_cap_rtv[_r, _t, _v] for _r, _t, _v in cap_rtv if _v == p) + new_cap_prev = 0.0 + if p == model.time_optimize.first(): - p_prev = model.time_exist.last() - new_cap_prev = sum( - value(model.existing_capacity[_r, _t, _v]) - for _r, _t, _v in model.existing_capacity.sparse_keys() - if _r in regions and _t in techs and _v == p_prev - ) + if model.time_exist: + p_prev = model.time_exist.last() + new_cap_prev = quicksum( + value(model.existing_capacity[_r, _t, _v]) + for _r, _t, _v in model.existing_capacity.sparse_keys() + if _r in regions and _t in techs and _v == p_prev + ) else: p_prev = model.time_optimize.prev(p) - new_cap_prev = sum(new_cap_rtv[_r, _t, _v] for _r, _t, _v in cap_rtv if _v == p_prev) + new_cap_prev = quicksum(new_cap_rtv[_r, _t, _v] for _r, _t, _v in cap_rtv if _v == p_prev) if degrowth: expr = operator_expression(new_cap_prev, Operator(op), seed + new_cap * rate) diff --git a/temoa/extensions/growth_rates/components/growth_new_capacity_delta.py b/temoa/extensions/growth_rates/components/growth_new_capacity_delta.py index c06b1290..d813d54c 100644 --- a/temoa/extensions/growth_rates/components/growth_new_capacity_delta.py +++ b/temoa/extensions/growth_rates/components/growth_new_capacity_delta.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING -from pyomo.environ import Constraint, value +from pyomo.environ import Constraint, quicksum, value import temoa.components.geography as geography import temoa.components.technology as technology @@ -106,34 +106,40 @@ def limit_growth_new_capacity_delta( if len(periods) == 0: return Constraint.Skip - new_cap = sum(new_cap_rtv[_r, _t, _v] for _r, _t, _v in cap_rtv if _v == p) + new_cap = quicksum(new_cap_rtv[_r, _t, _v] for _r, _t, _v in cap_rtv if _v == p) + + new_cap_prev = 0.0 + new_cap_prev2 = 0.0 if p == model.time_optimize.first(): - p_prev = model.time_exist.last() - new_cap_prev = sum( - value(model.existing_capacity[_r, _t, _v]) - for _r, _t, _v in model.existing_capacity.sparse_keys() - if _r in regions and _t in techs and _v == p_prev - ) - p_prev2 = model.time_exist.prev(p_prev) - new_cap_prev2 = sum( - value(model.existing_capacity[_r, _t, _v]) - for _r, _t, _v in model.existing_capacity.sparse_keys() - if _r in regions and _t in techs and _v == p_prev2 - ) - else: - p_prev = model.time_optimize.prev(p) - new_cap_prev = sum(new_cap_rtv[_r, _t, _v] for _r, _t, _v in cap_rtv if _v == p_prev) - if p == model.time_optimize.at(2): - p_prev2 = model.time_exist.last() - new_cap_prev2 = sum( + if model.time_exist: + p_prev = model.time_exist.last() + new_cap_prev = quicksum( + value(model.existing_capacity[_r, _t, _v]) + for _r, _t, _v in model.existing_capacity.sparse_keys() + if _r in regions and _t in techs and _v == p_prev + ) + if len(model.time_exist) >= 2: + p_prev2 = model.time_exist.prev(p_prev) + new_cap_prev2 = quicksum( value(model.existing_capacity[_r, _t, _v]) for _r, _t, _v in model.existing_capacity.sparse_keys() if _r in regions and _t in techs and _v == p_prev2 ) + else: + p_prev = model.time_optimize.prev(p) + new_cap_prev = quicksum(new_cap_rtv[_r, _t, _v] for _r, _t, _v in cap_rtv if _v == p_prev) + if p == model.time_optimize.at(2): + if model.time_exist: + p_prev2 = model.time_exist.last() + new_cap_prev2 = quicksum( + value(model.existing_capacity[_r, _t, _v]) + for _r, _t, _v in model.existing_capacity.sparse_keys() + if _r in regions and _t in techs and _v == p_prev2 + ) else: p_prev2 = model.time_optimize.prev(p_prev) - new_cap_prev2 = sum(new_cap_rtv[_r, _t, _v] for _r, _t, _v in cap_rtv if _v == p_prev2) + new_cap_prev2 = quicksum(new_cap_rtv[_r, _t, _v] for _r, _t, _v in cap_rtv if _v == p_prev2) nc_delta_prev = new_cap_prev - new_cap_prev2 nc_delta = new_cap - new_cap_prev diff --git a/temoa/extensions/stochastics/scenario_creator.py b/temoa/extensions/stochastics/scenario_creator.py index 3dc0b659..a1ffcccc 100644 --- a/temoa/extensions/stochastics/scenario_creator.py +++ b/temoa/extensions/stochastics/scenario_creator.py @@ -45,7 +45,10 @@ def scenario_creator(scenario_name: str, **kwargs: Any) -> Any: table_index_map: dict[str, list[str]] = {} for item in cast('Iterable[LoadItem]', hybrid_loader.manifest): if item.table not in table_index_map and item.columns: - table_index_map[item.table] = list(item.columns[:-1]) + if item.index_length: + table_index_map[item.table] = list(item.columns[:item.index_length]) + else: + table_index_map[item.table] = list(item.columns[:-1]) except Exception as e: logger.exception('Failed to connect to database %s', temoa_config.input_database) raise RuntimeError(f'Failed to connect to database {temoa_config.input_database}') from e From bdfa92b609ef19da8553eb82b405fbf8f5a60318 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Tue, 7 Jul 2026 19:19:57 -0400 Subject: [PATCH 18/28] Add integer capacity extension Signed-off-by: Davey Elder --- docs/source/extensions.rst | 1 + docs/source/extensions/discrete_capacity.rst | 93 +++++++++++++++ .../extensions/discrete_capacity/__init__.py | 3 + .../discrete_capacity/components/__init__.py | 0 .../components/discrete_capacity.py | 108 ++++++++++++++++++ .../discrete_capacity/core/__init__.py | 0 .../discrete_capacity/core/model.py | 55 +++++++++ .../discrete_capacity/data_manifest.py | 33 ++++++ .../extensions/discrete_capacity/extension.py | 20 ++++ temoa/extensions/discrete_capacity/tables.sql | 16 +++ temoa/extensions/framework.py | 7 +- .../components/growth_capacity.py | 2 +- tests/test_extensions.py | 95 +++++++++++++++ 13 files changed, 427 insertions(+), 6 deletions(-) create mode 100644 docs/source/extensions/discrete_capacity.rst create mode 100644 temoa/extensions/discrete_capacity/__init__.py create mode 100644 temoa/extensions/discrete_capacity/components/__init__.py create mode 100644 temoa/extensions/discrete_capacity/components/discrete_capacity.py create mode 100644 temoa/extensions/discrete_capacity/core/__init__.py create mode 100644 temoa/extensions/discrete_capacity/core/model.py create mode 100644 temoa/extensions/discrete_capacity/data_manifest.py create mode 100644 temoa/extensions/discrete_capacity/extension.py create mode 100644 temoa/extensions/discrete_capacity/tables.sql create mode 100644 tests/test_extensions.py diff --git a/docs/source/extensions.rst b/docs/source/extensions.rst index 32f72454..06cc597c 100644 --- a/docs/source/extensions.rst +++ b/docs/source/extensions.rst @@ -244,3 +244,4 @@ as new extensions are added. :maxdepth: 1 extensions/growth_rates + extensions/discrete_capacity diff --git a/docs/source/extensions/discrete_capacity.rst b/docs/source/extensions/discrete_capacity.rst new file mode 100644 index 00000000..aa3d988b --- /dev/null +++ b/docs/source/extensions/discrete_capacity.rst @@ -0,0 +1,93 @@ +.. _extension-discrete-capacity: + +Discrete Capacity +================================ + +The **discrete_capacity** extension adds optional constraints that force the +capacity of a technology or technology group to be deployed in discrete, +indivisible units rather than as a continuous quantity. This is useful for +modeling lumpy investments or retirements such as a power plant, reactor, or +other facility that can only exist in whole units of a fixed size. + +Enabling the extension introduces integer decision variables, which turns the +model into a mixed-integer program (MIP). It is disabled by default and enabled +per run through configuration: + +.. code-block:: toml + + extensions = ["discrete_capacity"] + +Its parameters live in the extension-owned tables of the same name and are loaded +only when the extension is enabled. See :ref:`extensions` for how the extension +framework wires these components into the model. + +Parameters +---------- + +.. csv-table:: + :header: "Parameter", "Database Table", "Model Element", "Notes" + :widths: 15, 20, 25, 40 + + ":math:`\text{LDNC}_{r,t}`", ":code:`limit_discrete_new_capacity`", ":code:`limit_discrete_new_capacity`", "unit size for new capacity additions; new capacity is forced to an discrete multiple of this size; :code:`tech_or_group` column accepts a technology name or group name" + ":math:`\text{LDC}_{r,t}`", ":code:`limit_discrete_capacity`", ":code:`limit_discrete_capacity`", "unit size for total available (retirement-adjusted) capacity; net capacity is forced to an integer multiple of this size; :code:`tech_or_group` column accepts a technology name or group name" + + +limit_discrete_new_capacity +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:math:`{LDNC}_{r \in R, t \in T}` + +The :code:`limit_discrete_new_capacity` parameter specifies the unit size for new +capacity additions of a technology (or group). The new capacity built in each +vintage is forced to be an integer multiple of this unit size. The +:code:`tech_or_group` column accepts a technology name or group name, and the +:code:`capacity` column gives the size of a single unit. + + +limit_discrete_capacity +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:math:`{LDC}_{r \in R, t \in T}` + +The :code:`limit_discrete_capacity` parameter specifies the unit size for the +total available (retirement-adjusted) capacity of a technology (or group). The +net capacity available in each period is forced to be an integer multiple of this +unit size. The :code:`tech_or_group` column accepts a technology name or group +name, and the :code:`capacity` column gives the size of a single unit. + + +Decision Variables +------------------ + +Enabling the extension adds two integer decision variables, one per constraint. +Each counts the number of discrete units, which is multiplied by the +corresponding unit size to recover the capacity quantity. + +v_discrete_new_capacity +~~~~~~~~~~~~~~~~~~~~~~~~ + +:math:`DNCAP_{r, t, v}` + +The number of discrete units of new capacity built for technology (or group) +:math:`t` in region :math:`r` and vintage :math:`v`. It is restricted to +non-negative integers, and the total new capacity equals this count multiplied by +the unit size :math:`LDNC_{r, t}`. + + +v_discrete_capacity +~~~~~~~~~~~~~~~~~~~~~~~~ + +:math:`DCAP_{r, p, t}` + +The number of discrete units of total available (retirement-adjusted) capacity +for technology (or group) :math:`t` in region :math:`r` and period :math:`p`. It +is restricted to non-negative integers, and the net available capacity equals this +count multiplied by the unit size :math:`LDC_{r, t}`. + + +Constraints +----------- + +.. autofunction:: temoa.extensions.discrete_capacity.components.discrete_capacity.limit_discrete_new_capacity_constraint_rule + +.. autofunction:: temoa.extensions.discrete_capacity.components.discrete_capacity.limit_discrete_capacity_constraint_rule diff --git a/temoa/extensions/discrete_capacity/__init__.py b/temoa/extensions/discrete_capacity/__init__.py new file mode 100644 index 00000000..c1a499cd --- /dev/null +++ b/temoa/extensions/discrete_capacity/__init__.py @@ -0,0 +1,3 @@ +from temoa.extensions.discrete_capacity.extension import DISCRETE_CAPACITY_EXTENSION + +__all__ = ['DISCRETE_CAPACITY_EXTENSION'] diff --git a/temoa/extensions/discrete_capacity/components/__init__.py b/temoa/extensions/discrete_capacity/components/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/temoa/extensions/discrete_capacity/components/discrete_capacity.py b/temoa/extensions/discrete_capacity/components/discrete_capacity.py new file mode 100644 index 00000000..6d0c6bbd --- /dev/null +++ b/temoa/extensions/discrete_capacity/components/discrete_capacity.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING + +from pyomo.environ import Constraint, quicksum, value + +import temoa.components.geography as geography +import temoa.components.technology as technology + +logger = logging.getLogger(__name__) + +if TYPE_CHECKING: + from temoa.extensions.discrete_capacity.core.model import DiscreteCapacityModel + from temoa.types import ExprLike, Period, Region, Technology, Vintage + + +def limit_discrete_new_capacity_indices( + model: DiscreteCapacityModel, +) -> set[tuple[Region, Technology, Vintage]]: + + indices = { + (r, t, v) + for r, t in model.limit_discrete_new_capacity.sparse_keys() + for _r in geography.gather_group_regions(model, r) + for _t in technology.gather_group_techs(model, t) + for v in model.vintage_optimize + if (_r, _t, v) in model.process_periods + } + return indices + + +def limit_discrete_new_capacity_constraint_rule( + model: DiscreteCapacityModel, r: Region, t: Technology, v: Vintage +) -> ExprLike: + r""" + The limit_discrete_new_capacity constraint requires the total new capacity + for a technology (or technology group) in a region to be discrete, equal + to some integer multiple of the specified capacity. + + .. math:: + :label: limit_discrete_new_capacity + + \textbf{NCAP}_{r, t, v} = LDNC_{r, t} \cdot \textbf{DNCAP}_{r, v, t} + + \forall \{r, t, v\} \in \Theta_{\text{limit\_discrete\_new\_capacity}} + """ + + regions = geography.gather_group_regions(model, r) + techs = technology.gather_group_techs(model, t) + unit_cap = value(model.limit_discrete_new_capacity[r, t]) + + new_capacity = quicksum( + model.v_new_capacity[_r, _t, v] + for _r in regions + for _t in techs + if (_r, _t, v) in model.process_periods + ) + expr = new_capacity == unit_cap * model.v_discrete_new_capacity[r, t, v] + if isinstance(expr, bool): + return Constraint.Skip + return expr + + +def limit_discrete_capacity_indices( + model: DiscreteCapacityModel, +) -> set[tuple[Region, Period, Technology]]: + indices = { + (r, p, t) + for r, t in model.limit_discrete_capacity.sparse_keys() + for _r in geography.gather_group_regions(model, r) + for _t in technology.gather_group_techs(model, t) + for p in model.time_optimize + if (_r, p, _t) in model.v_capacity_available_by_period_and_tech + } + return indices + + +def limit_discrete_capacity_constraint_rule( + model: DiscreteCapacityModel, r: Region, p: Period, t: Technology +) -> ExprLike: + r""" + The limit_discrete_capacity constraint requires the total available capacity + for a technology (or technology group) in a region to be discrete, equal + to some integer multiple of the specified capacity. + + .. math:: + :label: limit_discrete_capacity + + \textbf{CAPAVL}_{r, p, t} = LDC_{r, t} \cdot \textbf{DCAP}_{r, p, t} + + \forall \{r, p, t\} \in \Theta_{\text{limit\_discrete\_net\_capacity}} + """ + + regions = geography.gather_group_regions(model, r) + techs = technology.gather_group_techs(model, t) + capacity = value(model.limit_discrete_capacity[r, t]) + + net_capacity = quicksum( + model.v_capacity_available_by_period_and_tech[_r, p, _t] + for _r in regions + for _t in techs + if (_r, p, _t) in model.v_capacity_available_by_period_and_tech + ) + expr = net_capacity == capacity * model.v_discrete_capacity[r, p, t] + if isinstance(expr, bool): + return Constraint.Skip + return expr diff --git a/temoa/extensions/discrete_capacity/core/__init__.py b/temoa/extensions/discrete_capacity/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/temoa/extensions/discrete_capacity/core/model.py b/temoa/extensions/discrete_capacity/core/model.py new file mode 100644 index 00000000..6cc127b8 --- /dev/null +++ b/temoa/extensions/discrete_capacity/core/model.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, cast + +from pyomo.environ import Constraint, Integers, NonNegativeReals, Param, Set, Var + +from temoa.extensions.discrete_capacity.components import discrete_capacity + +if TYPE_CHECKING: + from temoa.core.model import TemoaModel + + class DiscreteCapacityModel(TemoaModel): + + limit_discrete_new_capacity: Param + limit_discrete_new_capacity_constraint_rtv: Set + limit_discrete_new_capacity_constraint: Constraint + v_discrete_new_capacity: Var + + limit_discrete_capacity: Param + limit_discrete_capacity_constraint_rpt: Set + limit_discrete_capacity_constraint: Constraint + v_discrete_capacity: Var + + +def register_model_components(model: TemoaModel) -> None: + m = cast('DiscreteCapacityModel', model) + + m.limit_discrete_new_capacity = Param( + m.regional_global_indices, m.tech_or_group, within=NonNegativeReals + ) + m.limit_discrete_new_capacity_constraint_rtv = Set( + dimen=3, initialize=discrete_capacity.limit_discrete_new_capacity_indices + ) + m.v_discrete_new_capacity = Var( + m.limit_discrete_new_capacity_constraint_rtv, within=Integers, bounds=(0, None) + ) + m.limit_discrete_new_capacity_constraint = Constraint( + m.limit_discrete_new_capacity_constraint_rtv, + rule=discrete_capacity.limit_discrete_new_capacity_constraint_rule, + ) + + + m.limit_discrete_capacity = Param( + m.regional_global_indices, m.tech_or_group, within=NonNegativeReals + ) + m.limit_discrete_capacity_constraint_rpt = Set( + dimen=3, initialize=discrete_capacity.limit_discrete_capacity_indices + ) + m.v_discrete_capacity = Var( + m.limit_discrete_capacity_constraint_rpt, within=Integers, bounds=(0, None) + ) + m.limit_discrete_capacity_constraint = Constraint( + m.limit_discrete_capacity_constraint_rpt, + rule=discrete_capacity.limit_discrete_capacity_constraint_rule, + ) diff --git a/temoa/extensions/discrete_capacity/data_manifest.py b/temoa/extensions/discrete_capacity/data_manifest.py new file mode 100644 index 00000000..35702e20 --- /dev/null +++ b/temoa/extensions/discrete_capacity/data_manifest.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, cast + +from temoa.data_io.loader_manifest import LoadItem + +if TYPE_CHECKING: + from temoa.core.model import TemoaModel + from temoa.extensions.discrete_capacity.core.model import DiscreteCapacityModel + + +def build_manifest_items(model: TemoaModel) -> list[LoadItem]: + m = cast('DiscreteCapacityModel', model) + return [ + LoadItem( + component=m.limit_discrete_new_capacity, + table='limit_discrete_new_capacity', + columns=['region', 'tech_or_group', 'capacity'], + validator_name='viable_rt', + validation_map=(0, 1), + is_table_required=False, + is_period_filtered=False, + ), + LoadItem( + component=m.limit_discrete_capacity, + table='limit_discrete_capacity', + columns=['region', 'tech_or_group', 'capacity'], + validator_name='viable_rt', + validation_map=(0, 1), + is_table_required=False, + is_period_filtered=False, + ), + ] diff --git a/temoa/extensions/discrete_capacity/extension.py b/temoa/extensions/discrete_capacity/extension.py new file mode 100644 index 00000000..2a0c141e --- /dev/null +++ b/temoa/extensions/discrete_capacity/extension.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +from pathlib import Path + +from temoa.extensions.discrete_capacity.core.model import register_model_components +from temoa.extensions.discrete_capacity.data_manifest import build_manifest_items +from temoa.extensions.framework import ExtensionSpec + +DISCRETE_CAPACITY_EXTENSION = ExtensionSpec( + extension_id='discrete_capacity', + owned_tables=('limit_discrete_new_capacity', 'limit_discrete_capacity'), + regional_group_tables={ + 'limit_discrete_new_capacity': 'region', + 'limit_discrete_capacity': 'region' + }, + register_model_components=register_model_components, + build_manifest_items=build_manifest_items, + schema_sql_path=str(Path(__file__).parents[0] / 'tables.sql'), + fail_if_tables_populated_when_disabled=True, +) diff --git a/temoa/extensions/discrete_capacity/tables.sql b/temoa/extensions/discrete_capacity/tables.sql new file mode 100644 index 00000000..210308d9 --- /dev/null +++ b/temoa/extensions/discrete_capacity/tables.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS limit_discrete_new_capacity( + region TEXT NOT NULL, + tech_or_group TEXT NOT NULL, + capacity REAL NOT NULL DEFAULT 0, + units TEXT, + notes TEXT, + PRIMARY KEY (region, tech_or_group) +); +CREATE TABLE IF NOT EXISTS limit_discrete_capacity( + region TEXT NOT NULL, + tech_or_group TEXT NOT NULL, + capacity REAL NOT NULL DEFAULT 0, + units TEXT, + notes TEXT, + PRIMARY KEY (region, tech_or_group) +); diff --git a/temoa/extensions/framework.py b/temoa/extensions/framework.py index 9a655111..4a1abd3a 100644 --- a/temoa/extensions/framework.py +++ b/temoa/extensions/framework.py @@ -53,12 +53,9 @@ def normalize_extension_ids(extension_ids: Sequence[str] | None) -> tuple[str, . def get_known_extension_specs() -> dict[str, ExtensionSpec]: """Return all extension specs known to this installation.""" from temoa.extensions.growth_rates.extension import GROWTH_RATES_EXTENSION + from temoa.extensions.discrete_capacity.extension import DISCRETE_CAPACITY_EXTENSION - # TEMPLATE: To activate a new extension copied from ``temoa/extensions/template``, - # import its spec here and add it to ``specs`` below, e.g.: - # from temoa.extensions..extension import - # specs = [GROWTH_RATES_EXTENSION, ] - specs = [GROWTH_RATES_EXTENSION] + specs = [GROWTH_RATES_EXTENSION, DISCRETE_CAPACITY_EXTENSION] return {spec.extension_id: spec for spec in specs} diff --git a/temoa/extensions/growth_rates/components/growth_capacity.py b/temoa/extensions/growth_rates/components/growth_capacity.py index a091e260..434ba90e 100644 --- a/temoa/extensions/growth_rates/components/growth_capacity.py +++ b/temoa/extensions/growth_rates/components/growth_capacity.py @@ -92,7 +92,7 @@ def limit_growth_capacity( cap_rpt = model.v_capacity_available_by_period_and_tech cap_indices = {(_r, _p, _t) for _r, _p, _t in cap_rpt.keys() if _t in techs and _r in regions} - periods = sorted({_p for _r, _p, _t in cap_rpt}) + periods = sorted({_p for _r, _p, _t in cap_indices}) if len(periods) == 0: return Constraint.Skip diff --git a/tests/test_extensions.py b/tests/test_extensions.py new file mode 100644 index 00000000..e283dd92 --- /dev/null +++ b/tests/test_extensions.py @@ -0,0 +1,95 @@ +"""Tests for the optional model extensions and their wiring into TemoaModel.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest +from pyomo.environ import Constraint + +from temoa.core.model import TemoaModel +from temoa.extensions.framework import ( + get_known_extension_specs, + resolve_extension_specs, +) + +# Parametrize over every registered extension so new extensions are covered +# automatically without listing their components here. +EXTENSION_IDS = sorted(get_known_extension_specs()) + + +def _component_map(model: TemoaModel) -> dict[str, object]: + """Map of component name -> component object declared on a model.""" + return {component.name: component for component in model.component_objects()} + + +# Cache abstract models so each extension is only built once per test session. +_abstract_models: dict[str | None, TemoaModel] = {} + + +def _model_with(extension_id: str | None) -> TemoaModel: + if extension_id not in _abstract_models: + extensions = [extension_id] if extension_id else None + _abstract_models[extension_id] = TemoaModel(extensions=extensions) + return _abstract_models[extension_id] + + +def _added_components(extension_id: str) -> dict[str, object]: + """Components present with the extension enabled but absent without it.""" + base = _component_map(_model_with(None)) + enabled = _component_map(_model_with(extension_id)) + return {name: comp for name, comp in enabled.items() if name not in base} + + +@pytest.mark.parametrize('extension_id', EXTENSION_IDS) +def test_extension_registered(extension_id: str) -> None: + """The extension is discoverable by its id and resolves to its spec.""" + spec = get_known_extension_specs()[extension_id] + assert spec.extension_id == extension_id + assert resolve_extension_specs([extension_id]) == (spec,) + + +@pytest.mark.parametrize('extension_id', EXTENSION_IDS) +def test_extension_spec_fields(extension_id: str) -> None: + """The spec declares consistent region-group columns and a registration hook.""" + spec = get_known_extension_specs()[extension_id] + # Region-group tables must refer to declared owned tables via string columns. + assert set(spec.regional_group_tables) <= set(spec.owned_tables) + assert all(isinstance(column, str) and column for column in spec.regional_group_tables.values()) + # Every extension attaches model components; data loading is optional. + assert spec.register_model_components is not None + + +@pytest.mark.parametrize('extension_id', EXTENSION_IDS) +def test_extension_schema_matches_owned_tables(extension_id: str) -> None: + """If the extension ships a schema, it defines every owned table. + + Extensions that only add components feeding from existing tables own no + tables and need not provide a schema file. + """ + spec = get_known_extension_specs()[extension_id] + if spec.schema_sql_path is None: + # No schema means the extension introduces no tables of its own. + assert not spec.owned_tables + return + schema_path = Path(spec.schema_sql_path) + assert schema_path.is_file() + schema_sql = schema_path.read_text(encoding='utf-8') + for table in spec.owned_tables: + assert table in schema_sql + + +@pytest.mark.parametrize('extension_id', EXTENSION_IDS) +def test_extension_adds_components(extension_id: str) -> None: + """Enabling the extension attaches new model components, including constraints.""" + added = _added_components(extension_id) + assert added, f'{extension_id} should attach components to the model' + assert any(isinstance(component, Constraint) for component in added.values()), ( + f'{extension_id} should contribute at least one constraint' + ) + + +def test_unknown_extension_id_rejected() -> None: + """Resolving an unregistered extension id raises a descriptive error.""" + with pytest.raises(ValueError, match='Unknown extension id'): + resolve_extension_specs(['not_a_real_extension']) From 5f6fd64514eab8be56e78fb4a6cc52169969eaca Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Wed, 8 Jul 2026 09:56:59 -0400 Subject: [PATCH 19/28] Stop ignoring extensions in ruff Signed-off-by: Davey Elder --- pyproject.toml | 4 +- temoa/extensions/framework.py | 63 +++++++++------- .../components/growth_new_capacity_delta.py | 4 +- temoa/extensions/growth_rates/extension.py | 1 - temoa/extensions/method_of_morris/morris.py | 20 ++++-- .../method_of_morris/morris_sequencer.py | 9 +-- .../modeling_to_generate_alternatives/hull.py | 5 +- .../mga_sequencer.py | 19 +++-- .../example_builds/scenario_analyzer.py | 8 +-- .../example_builds/scenario_maker.py | 4 +- temoa/extensions/monte_carlo/mc_run.py | 15 ++-- temoa/extensions/monte_carlo/mc_sequencer.py | 17 +++-- temoa/extensions/myopic/evolution_updater.py | 16 +++-- .../myopic/myopic_progress_mapper.py | 8 +-- temoa/extensions/myopic/myopic_sequencer.py | 72 +++++++++---------- .../single_vector_mga/sv_mga_sequencer.py | 2 +- .../stochastics/scenario_creator.py | 8 ++- .../stochastics/stochastic_sequencer.py | 4 +- 18 files changed, 151 insertions(+), 128 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0e4b3557..602f9066 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,9 +86,7 @@ path = "temoa/__about__.py" [tool.ruff] line-length = 100 indent-width = 4 -exclude = ["stubs", - "temoa/extensions/" -] +exclude = ["stubs"] [tool.ruff.format] quote-style = "single" diff --git a/temoa/extensions/framework.py b/temoa/extensions/framework.py index 4a1abd3a..179ddf09 100644 --- a/temoa/extensions/framework.py +++ b/temoa/extensions/framework.py @@ -1,14 +1,14 @@ from __future__ import annotations -from pathlib import Path +from collections.abc import Callable from dataclasses import dataclass, field -from sqlite3 import Connection +from logging import getLogger +from pathlib import Path from typing import TYPE_CHECKING -from collections.abc import Callable - if TYPE_CHECKING: from collections.abc import Mapping, Sequence + from sqlite3 import Connection from temoa.core.model import TemoaModel from temoa.data_io.loader_manifest import LoadItem @@ -16,6 +16,8 @@ ModelHook = Callable[['TemoaModel'], None] ManifestHook = Callable[['TemoaModel'], list['LoadItem']] +logger = getLogger(__name__) + @dataclass(frozen=True) class ExtensionSpec: @@ -30,7 +32,7 @@ class ExtensionSpec: fail_if_tables_populated_when_disabled: bool = False -def normalize_extension_ids(extension_ids: Sequence[str] | None) -> tuple[str, ...]: +def normalize_extension_ids(extension_ids: Sequence[str | object] | None) -> tuple[str, ...]: """Normalize configured extension ids while preserving user-provided order.""" if not extension_ids: return () @@ -39,7 +41,9 @@ def normalize_extension_ids(extension_ids: Sequence[str] | None) -> tuple[str, . seen: set[str] = set() for ext_id in extension_ids: if not isinstance(ext_id, str): - raise ValueError(f'Extension ids must be strings. Received: {type(ext_id).__name__}') + msg = f'Extension ids must be strings. Received: {type(ext_id).__name__}' + logger.error(msg) + raise ValueError(msg) cleaned = ext_id.strip().lower() if not cleaned: continue @@ -52,8 +56,8 @@ def normalize_extension_ids(extension_ids: Sequence[str] | None) -> tuple[str, . def get_known_extension_specs() -> dict[str, ExtensionSpec]: """Return all extension specs known to this installation.""" - from temoa.extensions.growth_rates.extension import GROWTH_RATES_EXTENSION from temoa.extensions.discrete_capacity.extension import DISCRETE_CAPACITY_EXTENSION + from temoa.extensions.growth_rates.extension import GROWTH_RATES_EXTENSION specs = [GROWTH_RATES_EXTENSION, DISCRETE_CAPACITY_EXTENSION] return {spec.extension_id: spec for spec in specs} @@ -67,9 +71,9 @@ def resolve_extension_specs(extension_ids: Sequence[str] | None) -> tuple[Extens if unknown: known_ids = ', '.join(sorted(known)) unknown_ids = ', '.join(sorted(unknown)) - raise ValueError( - f'Unknown extension id(s): {unknown_ids}. Known extension ids: {known_ids}.' - ) + msg = f'Unknown extension id(s): {unknown_ids}. Known extension ids: {known_ids}.' + logger.error(msg) + raise ValueError(msg) return tuple(known[ext_id] for ext_id in normalized_ids) @@ -101,10 +105,12 @@ def merge_regional_group_tables( for table_name, field_name in spec.regional_group_tables.items(): existing = merged.get(table_name) if existing is not None and existing != field_name: - raise ValueError( + msg = ( f"Regional-group table '{table_name}' has conflicting field mappings: " f"'{existing}' vs '{field_name}' from extension '{spec.extension_id}'." ) + logger.error(msg) + raise ValueError(msg) merged[table_name] = field_name return merged @@ -127,10 +133,11 @@ def assert_disabled_extension_tables_are_empty( if populated: table_list = ', '.join(sorted(populated)) - raise RuntimeError( + msg = ( f"Extension '{spec.extension_id}' is not enabled, but extension-owned table(s) " f'contain data: {table_list}. Enable the extension or remove those rows.' ) + logger.warning(msg) def ensure_enabled_extension_tables_exist( @@ -148,36 +155,42 @@ def ensure_enabled_extension_tables_exist( missing_list = ', '.join(sorted(missing_tables)) if not spec.schema_sql_path: - raise RuntimeError( + msg = ( f"Extension '{spec.extension_id}' is enabled, but required table(s) are missing: " f'{missing_list}. No schema SQL path is registered for this extension.' ) + logger.error(msg) + raise RuntimeError(msg) should_apply = False if not silent: prompt = ( f"Extension '{spec.extension_id}' is enabled but missing table(s): {missing_list}. " - f"Append schema from '{spec.schema_sql_path}' to input database '{input_database}' now? " - '[y/N]: ' + f"Append schema from '{spec.schema_sql_path}' to input database '{input_database} " + 'now? [y/N]: ' ) response = input(prompt).strip().lower() should_apply = response in {'y', 'yes'} if not should_apply: - raise RuntimeError( + msg = ( f"Extension '{spec.extension_id}' is enabled, but required table(s) are missing: " - f"{missing_list}. Re-run and accept the prompt, or append schema manually from " + f'{missing_list}. Re-run and accept the prompt, or append schema manually from ' f"'{spec.schema_sql_path}' to '{input_database}'." ) + logger.error(msg) + raise RuntimeError(msg) _append_extension_schema(con, spec) still_missing = [table for table in spec.owned_tables if not _table_exists(con, table)] if still_missing: still_missing_list = ', '.join(sorted(still_missing)) - raise RuntimeError( - f"Schema append for extension '{spec.extension_id}' completed but table(s) are still " - f'missing: {still_missing_list}.' + msg = ( + f"Schema append for extension '{spec.extension_id}' completed " + f'but table(s) are still missing: {still_missing_list}.' ) + logger.error(msg) + raise RuntimeError(msg) def _table_has_rows(con: Connection, table_name: str) -> bool: @@ -202,13 +215,15 @@ def _table_exists(con: Connection, table_name: str) -> bool: def _append_extension_schema(con: Connection, spec: ExtensionSpec) -> None: if spec.schema_sql_path is None: - raise RuntimeError(f"Extension '{spec.extension_id}' has no schema SQL path configured.") + msg = f"Extension '{spec.extension_id}' has no schema SQL path configured." + logger.error(msg) + raise RuntimeError(msg) schema_path = Path(spec.schema_sql_path) if not schema_path.is_file(): - raise FileNotFoundError( - f"Schema SQL file for extension '{spec.extension_id}' not found: {schema_path}" - ) + msg = f"Schema SQL file for extension '{spec.extension_id}' not found: {schema_path}" + logger.error(msg) + raise FileNotFoundError(msg) sql = schema_path.read_text(encoding='utf-8') con.executescript(sql) diff --git a/temoa/extensions/growth_rates/components/growth_new_capacity_delta.py b/temoa/extensions/growth_rates/components/growth_new_capacity_delta.py index d813d54c..a158ce6b 100644 --- a/temoa/extensions/growth_rates/components/growth_new_capacity_delta.py +++ b/temoa/extensions/growth_rates/components/growth_new_capacity_delta.py @@ -139,7 +139,9 @@ def limit_growth_new_capacity_delta( ) else: p_prev2 = model.time_optimize.prev(p_prev) - new_cap_prev2 = quicksum(new_cap_rtv[_r, _t, _v] for _r, _t, _v in cap_rtv if _v == p_prev2) + new_cap_prev2 = quicksum( + new_cap_rtv[_r, _t, _v] for _r, _t, _v in cap_rtv if _v == p_prev2 + ) nc_delta_prev = new_cap_prev - new_cap_prev2 nc_delta = new_cap - new_cap_prev diff --git a/temoa/extensions/growth_rates/extension.py b/temoa/extensions/growth_rates/extension.py index 5c148b61..19aec700 100644 --- a/temoa/extensions/growth_rates/extension.py +++ b/temoa/extensions/growth_rates/extension.py @@ -6,7 +6,6 @@ from temoa.extensions.growth_rates.core.model import register_model_components from temoa.extensions.growth_rates.data_manifest import build_manifest_items - GROWTH_RATES_EXTENSION = ExtensionSpec( extension_id='growth_rates', owned_tables=( diff --git a/temoa/extensions/method_of_morris/morris.py b/temoa/extensions/method_of_morris/morris.py index 4bb14ddc..dd981d79 100644 --- a/temoa/extensions/method_of_morris/morris.py +++ b/temoa/extensions/method_of_morris/morris.py @@ -1,8 +1,8 @@ from __future__ import annotations +import contextlib import csv import sqlite3 -from importlib import resources from pathlib import Path from typing import Any @@ -21,8 +21,9 @@ seed = 42 -def evaluate(param_names: dict[int, list[Any]], param_values: Any, - data: dict[str, Any], k: int) -> list[Any]: +def evaluate( + param_names: dict[int, list[Any]], param_values: Any, data: dict[str, Any], k: int +) -> list[Any]: m = len(param_values) for j in range(0, m): names = param_names[j] @@ -47,6 +48,7 @@ def evaluate(param_names: dict[int, list[Any]], param_values: Any, table_writer.write_results(model=mdl) import contextlib + with contextlib.closing(sqlite3.connect(db_file)) as con: cur = con.cursor() cur.execute('SELECT * FROM output_objective') @@ -73,12 +75,11 @@ def evaluate(param_names: dict[int, list[Any]], param_values: Any, if not db_file.exists() or not config_path.exists(): raise FileNotFoundError( - "Example Morris data files not found in current directory. " - "Please see MM_README.md for instructions on how to generate " + 'Example Morris data files not found in current directory. ' + 'Please see MM_README.md for instructions on how to generate ' "or provide the required 'morris_utopia.sqlite' and 'morris_utopia.toml'." ) -import contextlib with contextlib.closing(sqlite3.connect(str(db_file))) as con: with open(param_file, 'w') as file: param_names = {} @@ -152,7 +153,12 @@ def evaluate(param_names: dict[int, list[Any]], param_values: Any, problem = read_param_file(str(param_file), delimiter=' ') param_values = sample( - problem, N=10, num_levels=8, optimal_trajectories=False, local_optimization=False, seed=seed + problem, + N=10, + num_levels=8, + optimal_trajectories=False, + local_optimization=False, + seed=seed, ) print(param_values) print(param_names) diff --git a/temoa/extensions/method_of_morris/morris_sequencer.py b/temoa/extensions/method_of_morris/morris_sequencer.py index 6ae6bdf3..6c3d1bc7 100644 --- a/temoa/extensions/method_of_morris/morris_sequencer.py +++ b/temoa/extensions/method_of_morris/morris_sequencer.py @@ -2,6 +2,7 @@ An event sequencer to control the flow of a Method of Morris calculation. This code uses multiprocessing via the joblib library """ + from __future__ import annotations import csv @@ -30,7 +31,6 @@ from temoa.core.config import TemoaConfig - logger = logging.getLogger(__name__) solver_options_file = ( @@ -87,7 +87,7 @@ def __init__(self, config: TemoaConfig): # the amount to perturb the marked params pert = morris_inputs.get('perturbation') if pert: - self.mm_perturbation = float(cast(str | float, pert)) + self.mm_perturbation = float(cast('str | float', pert)) logger.info('Morris perturbation: %0.2f', self.mm_perturbation) else: self.mm_perturbation = 0.10 @@ -201,8 +201,9 @@ def start(self) -> Any: # 7. Return the cost objective Mu_Star for testing purposes... return cost_mu_star - def process_results(self, problem: dict[str, Any], mm_samples: Any, - morris_results: list[Any]) -> Any: + def process_results( + self, problem: dict[str, Any], mm_samples: Any, morris_results: list[Any] + ) -> Any: """ Process the results of the runs on the mm_samples :param problem: the problem structure diff --git a/temoa/extensions/modeling_to_generate_alternatives/hull.py b/temoa/extensions/modeling_to_generate_alternatives/hull.py index d0f79c13..f03909ed 100644 --- a/temoa/extensions/modeling_to_generate_alternatives/hull.py +++ b/temoa/extensions/modeling_to_generate_alternatives/hull.py @@ -1,6 +1,7 @@ """ A thin wrapper on Scipy's ConvexHull to make it more manageable """ + from __future__ import annotations from logging import getLogger @@ -119,8 +120,8 @@ def update(self) -> None: def add_point(self, point: np.ndarray) -> None: if len(point) != self.dim: msg = ( - f'Tried adding a point to hull (dim: {self.dim}) with wrong dimensions {len(point)}. ' - f'Point: {point}' + f'Tried adding a point to hull (dim: {self.dim}) with ' + f'wrong dimensions {len(point)}. Point: {point}' ) logger.error(msg) raise ValueError(msg) diff --git a/temoa/extensions/modeling_to_generate_alternatives/mga_sequencer.py b/temoa/extensions/modeling_to_generate_alternatives/mga_sequencer.py index 080d0697..a261ba45 100644 --- a/temoa/extensions/modeling_to_generate_alternatives/mga_sequencer.py +++ b/temoa/extensions/modeling_to_generate_alternatives/mga_sequencer.py @@ -1,6 +1,7 @@ """ Performs top-level control over an MGA model run """ + from __future__ import annotations import logging @@ -26,10 +27,6 @@ from temoa.extensions.modeling_to_generate_alternatives.vector_manager import VectorManager - - - - import pyomo.environ as pyo from pyomo.opt import check_optimal_termination @@ -124,13 +121,13 @@ def __init__(self, config: TemoaConfig): except KeyError: logger.warning('No/bad MGA Weighting specified. Using default: Hull Expansion') self.mga_weighting = MgaWeighting.HULL_EXPANSION - self.num_workers = int(cast(str | int, all_options.get('num_workers', 1))) + self.num_workers = int(cast('str | int', all_options.get('num_workers', 1))) logger.info('MGA workers are set to %s', self.num_workers) - self.iteration_limit = int(cast(str | int, mga_inputs.get('iteration_limit', 20))) + self.iteration_limit = int(cast('str | int', mga_inputs.get('iteration_limit', 20))) logger.info('Set MGA iteration limit to: %d', self.iteration_limit) - self.time_limit_hrs = float(cast(str | float, mga_inputs.get('time_limit_hrs', 12))) + self.time_limit_hrs = float(cast('str | float', mga_inputs.get('time_limit_hrs', 12))) logger.info('Set MGA time limit hours to: %0.1f', self.time_limit_hrs) - self.cost_epsilon = float(cast(str | float, mga_inputs.get('cost_epsilon', 0.05))) + self.cost_epsilon = float(cast('str | float', mga_inputs.get('cost_epsilon', 0.05))) logger.info('Set MGA cost (relaxation) epsilon to: %0.3f', self.cost_epsilon) # internal records @@ -358,8 +355,10 @@ def solve_instance(self, instance: TemoaModel) -> bool: elapsed.total_seconds(), status.name, ) - return status == pyo.TerminationCondition.optimal or \ - str(status) == 'convergenceCriteriaSatisfied' + return ( + status == pyo.TerminationCondition.optimal + or str(status) == 'convergenceCriteriaSatisfied' + ) def process_solve_results(self, instance: TemoaModel) -> None: """write the results as required""" diff --git a/temoa/extensions/monte_carlo/example_builds/scenario_analyzer.py b/temoa/extensions/monte_carlo/example_builds/scenario_analyzer.py index 08b68894..a025b110 100644 --- a/temoa/extensions/monte_carlo/example_builds/scenario_analyzer.py +++ b/temoa/extensions/monte_carlo/example_builds/scenario_analyzer.py @@ -1,7 +1,7 @@ """ Simple analyzer--example only """ -from importlib import resources + from math import sqrt from pathlib import Path from sqlite3 import Connection @@ -25,15 +25,15 @@ cur = conn.cursor() # Check if results exist before attempting to plot obj_values = cur.execute( - "SELECT total_system_cost FROM output_objective WHERE scenario LIKE ?", - (f"{scenario_name}-%",) + 'SELECT total_system_cost FROM output_objective WHERE scenario LIKE ?', + (f'{scenario_name}-%',), ).fetchall() if len(obj_values) == 0: raise RuntimeError( f"No results found for scenario '{scenario_name}-*' in '{db_resource}'. " "Please run 'temoa run tutorial_config.toml' or run the tutorial model " - "to populate output_objective with results first." + 'to populate output_objective with results first.' ) obj_values_tuple = tuple(t[0] for t in obj_values) diff --git a/temoa/extensions/monte_carlo/example_builds/scenario_maker.py b/temoa/extensions/monte_carlo/example_builds/scenario_maker.py index 0e72dd48..8561f556 100644 --- a/temoa/extensions/monte_carlo/example_builds/scenario_maker.py +++ b/temoa/extensions/monte_carlo/example_builds/scenario_maker.py @@ -21,10 +21,10 @@ """ from pathlib import Path +from typing import cast import matplotlib.pyplot as plt import numpy as np -from typing import cast # distro for the related cost vars @@ -34,7 +34,7 @@ num_runs = 1000 cov = np.array([[0.4, -0.1], [-0.1, 0.1]]) price_devs = np.random.multivariate_normal([0, 0], cov, size=num_runs) -corr_matrix = cast(np.typing.NDArray[np.float64], np.corrcoef(price_devs.T)) +corr_matrix = cast('np.typing.NDArray[np.float64]', np.corrcoef(price_devs.T)) print(f'correlation check: {corr_matrix[0, 1]}') # verify with a peek... diff --git a/temoa/extensions/monte_carlo/mc_run.py b/temoa/extensions/monte_carlo/mc_run.py index 7c2874a7..cca3d8b5 100644 --- a/temoa/extensions/monte_carlo/mc_run.py +++ b/temoa/extensions/monte_carlo/mc_run.py @@ -1,6 +1,5 @@ -""" +""" """ -""" from __future__ import annotations from collections import defaultdict, namedtuple @@ -27,8 +26,6 @@ """a record of a data element change, for an element acted on by a Tweak""" - - class Tweak: """ objects of this class represent individual tweaks to single (or wildcard) @@ -235,9 +232,11 @@ def __init__(self, config: TemoaConfig, data_store: dict[str, Any]): "Monte Carlo mode requires 'run_settings' path in 'monte_carlo_inputs'." ) - self.settings_file = Path(cast(str, settings_path)) + self.settings_file = Path(cast('str', settings_path)) if not self.settings_file.exists(): - raise FileNotFoundError(f'Monte Carlo run settings file not found: {self.settings_file}') + raise FileNotFoundError( + f'Monte Carlo run settings file not found: {self.settings_file}' + ) def prescreen_input_file(self) -> bool: """ @@ -330,9 +329,7 @@ def element_locator( ) raw_indices = param_data.keys() matches = [ - k - for k in raw_indices - if all(k[idx] == target_index[idx] for idx in non_wildcard_locs) + k for k in raw_indices if all(k[idx] == target_index[idx] for idx in non_wildcard_locs) ] return matches diff --git a/temoa/extensions/monte_carlo/mc_sequencer.py b/temoa/extensions/monte_carlo/mc_sequencer.py index 0df6fd25..eb99da89 100644 --- a/temoa/extensions/monte_carlo/mc_sequencer.py +++ b/temoa/extensions/monte_carlo/mc_sequencer.py @@ -3,6 +3,7 @@ A sequencer for Monte Carlo Runs """ + from __future__ import annotations import logging @@ -31,12 +32,9 @@ from temoa.core.config import TemoaConfig - logger = getLogger(__name__) -solver_options_path = ( - resources.files('temoa.extensions.monte_carlo') / 'MC_solver_options.toml' -) +solver_options_path = resources.files('temoa.extensions.monte_carlo') / 'MC_solver_options.toml' class MCSequencer: @@ -117,7 +115,6 @@ def start(self) -> None: # 4. copy & modify the base data to make per-dataset runs # 5. farm out the runs to workers - # 0. Set up database for scenario self.writer.clear_scenario() self.writer.make_mc_tweaks_table() # add the output table for tweaks, if not exists @@ -125,6 +122,7 @@ def start(self) -> None: # 1. Load data import contextlib + with contextlib.closing(sqlite3.connect(self.config.input_database)) as con: hybrid_loader = HybridLoader(db_connection=con, config=self.config) data_store = hybrid_loader.create_data_dict(myopic_index=None) @@ -138,6 +136,7 @@ def start(self) -> None: # 4. Set up the workers import multiprocessing + ctx = multiprocessing.get_context('spawn') num_workers = self.num_workers @@ -151,10 +150,10 @@ def start(self) -> None: log_queue: Queue[logging.LogRecord] = ctx.Queue() # make workers workers: list[BaseProcess] = [] - kwargs = { - 'solver_name': self.config.solver_name, - 'solver_options': self.worker_solver_options, - } + # kwargs = { + # 'solver_name': self.config.solver_name, + # 'solver_options': self.worker_solver_options, + # } # construct path for the solver logs s_path = self.config.output_path / 'solver_logs' if not s_path.exists(): diff --git a/temoa/extensions/myopic/evolution_updater.py b/temoa/extensions/myopic/evolution_updater.py index 20808e79..efeb9946 100644 --- a/temoa/extensions/myopic/evolution_updater.py +++ b/temoa/extensions/myopic/evolution_updater.py @@ -1,15 +1,17 @@ -import sqlite3 import logging +import sqlite3 + from temoa.extensions.myopic.myopic_index import MyopicIndex logger = logging.getLogger(__name__) + def iterate( - idx: MyopicIndex | None = None, - prev_base_year: int | None = None, - last_instance_status: str | None = None, - db_con: sqlite3.Connection | None = None, - ) -> None: + idx: MyopicIndex | None = None, + prev_base_year: int | None = None, + last_instance_status: str | None = None, + db_con: sqlite3.Connection | None = None, +) -> None: """ This function is called at the end of each myopic iteration, after the results have been recorded to the myopic database. @@ -29,7 +31,7 @@ def iterate( """ assert idx is not None - logger.info(f"Running myopic iteration updater for base year {idx.base_year}") + logger.info('Running myopic iteration updater for base year %s', idx.base_year) # Update your myopic database here. diff --git a/temoa/extensions/myopic/myopic_progress_mapper.py b/temoa/extensions/myopic/myopic_progress_mapper.py index 3dc3c295..e474702a 100644 --- a/temoa/extensions/myopic/myopic_progress_mapper.py +++ b/temoa/extensions/myopic/myopic_progress_mapper.py @@ -3,7 +3,6 @@ """ from datetime import datetime, timedelta - from typing import Literal from temoa.extensions.myopic.myopic_index import MyopicIndex @@ -38,7 +37,7 @@ def draw_header(self) -> None: print(time_buffer, end='') print('*' * tot_length) print() - print(f"{'HH:MM:SS':10s}", end='') + print(f'{"HH:MM:SS":10s}', end='') print(' ', end='') for year in self.years: print(f'{self.leader}{year}{self.trailer} ', end='') @@ -47,8 +46,9 @@ def draw_header(self) -> None: def timestamp(self) -> str: delta = datetime.now() - self.hack return ( - f'Elapsed: {int(delta.total_seconds()//3600):02d}:' - f'{int(delta.total_seconds()%3600//60):02d}:{int(delta.total_seconds())%60:02d} ' + f'Elapsed: {int(delta.total_seconds() // 3600):02d}:' + f'{int(delta.total_seconds() % 3600 // 60):02d}:' + f'{int(delta.total_seconds()) % 60:02d} ' ) def report( diff --git a/temoa/extensions/myopic/myopic_sequencer.py b/temoa/extensions/myopic/myopic_sequencer.py index e671c375..e32ed347 100644 --- a/temoa/extensions/myopic/myopic_sequencer.py +++ b/temoa/extensions/myopic/myopic_sequencer.py @@ -9,7 +9,7 @@ from collections import deque from importlib import resources, util from pathlib import Path -from sqlite3 import Connection, Cursor +from sqlite3 import Connection from typing import Any, cast from temoa._internal import run_actions @@ -87,7 +87,7 @@ def __init__(self, config: TemoaConfig | None) -> None: default_cap_threshold = 1e-3 if config and config.myopic_inputs: self.capacity_epsilon = float( - cast(Any, config.myopic_inputs.get('capacity_threshold', default_cap_threshold)) + cast('Any', config.myopic_inputs.get('capacity_threshold', default_cap_threshold)) ) else: self.capacity_epsilon = default_cap_threshold @@ -110,10 +110,10 @@ def __init__(self, config: TemoaConfig | None) -> None: ) raise RuntimeError('No myopic options received. See log file.') else: - self.view_depth = int(cast(Any, myopic_options.get('view_depth'))) + self.view_depth = int(cast('Any', myopic_options.get('view_depth'))) if not isinstance(self.view_depth, int) or self.view_depth < 1: raise ValueError(f'view_depth is not a positive integer {self.view_depth}') - self.step_size = int(cast(Any, myopic_options.get('step_size'))) + self.step_size = int(cast('Any', myopic_options.get('step_size'))) if not isinstance(self.step_size, int) or self.step_size < 1: raise ValueError(f'step_size is not a positive integer {self.step_size}') if self.step_size > self.view_depth: @@ -224,8 +224,9 @@ def start(self) -> None: raise RuntimeError('Illegal state in myopic iteration.') logger.info('Processing Myopic Index: %s', idx) - # 4. If evolving, call the evolution script and pass it the myopic index and last instance status - if self.evolving and last_instance_status is not None: # don't evolve before first iteration (pointless) + # 4. If evolving, call the evolution script and pass it the myopic index + # and last instance status, unless is first iteration (pointless) + if self.evolving and last_instance_status is not None: self.run_evolution_script( idx=idx, last_base_year=last_base_year, @@ -298,7 +299,7 @@ def start(self) -> None: self.table_writer.write_dual_variables(results=results, iteration=idx.base_year) # prep next loop - last_base_year = idx.base_year if idx else last_base_year # update + last_base_year = idx.base_year if idx else last_base_year # update # delete anything in the output_objective table, it is nonsensical... assert self.output_con is not None @@ -307,13 +308,15 @@ def start(self) -> None: ) self.output_con.commit() - # Total system cost is, theoretically, sum of discounted costs from output_cost table - total_cost = self.get_current_total_cost(last_base_year if last_base_year is not None else 0) + total_cost = self.get_current_total_cost( + last_base_year if last_base_year is not None else 0 + ) assert self.output_con is not None self.output_con.execute( - f"INSERT INTO output_objective(scenario, objective_name, total_system_cost) VALUES('{self.config.scenario}', 'total_cost', {total_cost})" + 'INSERT INTO output_objective(scenario, objective_name, total_system_cost) ' + f"VALUES('{self.config.scenario}', 'total_cost', {total_cost})" ) self.output_con.commit() @@ -385,12 +388,12 @@ def initialize_myopic_efficiency_table(self) -> None: print(list(res)) def run_evolution_script( - self, - idx: MyopicIndex | None, - last_base_year: int | None, - last_instance_status: str | None, - con: sqlite3.Connection | None - ) -> None: + self, + idx: MyopicIndex | None, + last_base_year: int | None, + last_instance_status: str | None, + con: sqlite3.Connection | None, + ) -> None: """ Run the evolution script to update the myopic database before the next iteration. """ @@ -402,21 +405,21 @@ def run_evolution_script( # import the script as a module and call the iterate function script_path = Path(self.evolution_script).expanduser() if not script_path.is_file(): - msg = f"Myopic evolution script not found: {script_path}" + msg = f'Myopic evolution script not found: {script_path}' logger.error(msg) raise FileNotFoundError(msg) - spec = util.spec_from_file_location("evolution_script", script_path) + spec = util.spec_from_file_location('evolution_script', script_path) if spec is None or spec.loader is None: - msg = f"Could not load evolution script module spec from: {script_path}" + msg = f'Could not load evolution script module spec from: {script_path}' logger.error(msg) raise RuntimeError(msg) evolution_module = util.module_from_spec(spec) spec.loader.exec_module(evolution_module) - iterate = getattr(evolution_module, "iterate", None) + iterate = getattr(evolution_module, 'iterate', None) if not callable(iterate): - msg = f"Evolution script must define callable iterate(...): {script_path}" + msg = f'Evolution script must define callable iterate(...): {script_path}' logger.error(msg) raise AttributeError(msg) @@ -571,21 +574,22 @@ def characterize_run(self, future_periods: list[int] | None = None) -> None: raise RuntimeError('view_depth not initialized') if len(future_periods) < self.view_depth + 1: msg = ( - 'Not enough future periods for view depth. Need {} including end period. Got {}.' - ).format(self.view_depth+1, len(future_periods)) + 'Not enough future periods for view depth. ' + f'Need {self.view_depth + 1} including end period. Got {len(future_periods)}.' + ) logger.error(msg) raise RuntimeError(msg) self.optimization_periods = future_periods.copy() if self.step_size is None: raise RuntimeError('step_size not initialized') last_base_year = ((len(future_periods) - 2) // self.step_size) * self.step_size - base_years = list(range(0, last_base_year+1, self.step_size)) + base_years = list(range(0, last_base_year + 1, self.step_size)) if not self.evolving: # Remove redundant iterations near end of horizon if not evolving - catch_Pe = [i for i in base_years if i + self.view_depth >= len(future_periods) - 1] - if len(catch_Pe) > 1: + catch_pe = [i for i in base_years if i + self.view_depth >= len(future_periods) - 1] + if len(catch_pe) > 1: # keep only one iteration that captures the end of the horizon - base_years = base_years[:-len(catch_Pe) + 1] + base_years = base_years[: -len(catch_pe) + 1] for n, idx in enumerate(base_years): depth = min(self.view_depth, len(future_periods) - idx - 1) if idx == base_years[-1]: @@ -593,13 +597,13 @@ def characterize_run(self, future_periods: list[int] | None = None) -> None: step = depth else: # record to next base year - step = base_years[n+1] - idx + step = base_years[n + 1] - idx if depth < 1: msg = ( 'Calculated MyopicIndex with non-positive depth. ' 'This should never happen. Code error likely. ' - 'idx: {}, step: {}, depth: {}, future_periods: {}' - ).format(idx, step, depth, future_periods) + f'idx: {idx}, step: {step}, depth: {depth}, future_periods: {future_periods}' + ) logger.error(msg) raise RuntimeError(msg) myopic_idx = MyopicIndex( @@ -708,9 +712,7 @@ def clear_results_after(self, period: int) -> None: def report_total_demand(self, mi: MyopicIndex) -> None: assert self.output_con is not None assert self.cursor is not None - self.cursor.execute( - "SELECT SUM(demand) FROM output_demand WHERE scenario='original'" - ) + self.cursor.execute("SELECT SUM(demand) FROM output_demand WHERE scenario='original'") self.output_con.commit() def write_myopic_efficiency(self, mi: MyopicIndex, status: str) -> None: @@ -737,9 +739,7 @@ def write_myopic_efficiency(self, mi: MyopicIndex, status: str) -> None: def report_cumulative_capacity(self, mi: MyopicIndex) -> None: assert self.output_con is not None assert self.cursor is not None - self.cursor.execute( - "SELECT SUM(capacity) FROM output_capacity WHERE scenario='original'" - ) + self.cursor.execute("SELECT SUM(capacity) FROM output_capacity WHERE scenario='original'") self.output_con.commit() def __del__(self) -> None: diff --git a/temoa/extensions/single_vector_mga/sv_mga_sequencer.py b/temoa/extensions/single_vector_mga/sv_mga_sequencer.py index 937aeb65..543af667 100644 --- a/temoa/extensions/single_vector_mga/sv_mga_sequencer.py +++ b/temoa/extensions/single_vector_mga/sv_mga_sequencer.py @@ -9,7 +9,7 @@ import sqlite3 import sys from collections.abc import Iterable -from typing import Any, cast, TYPE_CHECKING +from typing import TYPE_CHECKING, Any, cast from pyomo.core import Constraint, Expression, Objective, value from pyomo.opt import check_optimal_termination diff --git a/temoa/extensions/stochastics/scenario_creator.py b/temoa/extensions/stochastics/scenario_creator.py index a1ffcccc..ac54bcb7 100644 --- a/temoa/extensions/stochastics/scenario_creator.py +++ b/temoa/extensions/stochastics/scenario_creator.py @@ -3,7 +3,6 @@ import logging import sqlite3 from typing import TYPE_CHECKING, Any, cast -from collections.abc import Iterable from mpisppy.utils.sputils import attach_root_node # type: ignore[import-untyped] @@ -12,6 +11,8 @@ from temoa.data_io.hybrid_loader import HybridLoader if TYPE_CHECKING: + from collections.abc import Iterable + from temoa.core.config import TemoaConfig from temoa.data_io.hybrid_loader import LoadItem from temoa.extensions.stochastics.stochastic_config import StochasticConfig @@ -36,6 +37,7 @@ def scenario_creator(scenario_name: str, **kwargs: Any) -> Any: # 1. Load base data try: import contextlib + with contextlib.closing(sqlite3.connect(temoa_config.input_database)) as con: hybrid_loader = HybridLoader(db_connection=con, config=temoa_config) data_dict = hybrid_loader.create_data_dict(myopic_index=None) @@ -46,7 +48,7 @@ def scenario_creator(scenario_name: str, **kwargs: Any) -> Any: for item in cast('Iterable[LoadItem]', hybrid_loader.manifest): if item.table not in table_index_map and item.columns: if item.index_length: - table_index_map[item.table] = list(item.columns[:item.index_length]) + table_index_map[item.table] = list(item.columns[: item.index_length]) else: table_index_map[item.table] = list(item.columns[:-1]) except Exception as e: @@ -58,7 +60,7 @@ def scenario_creator(scenario_name: str, **kwargs: Any) -> Any: if p.scenario != scenario_name: continue - target_param = cast(dict[Any, Any] | None, data_dict.get(p.table)) + target_param = cast('dict[Any, Any] | None', data_dict.get(p.table)) if target_param is None: logger.warning( 'Table %s not found in data_dict for scenario %s', p.table, scenario_name diff --git a/temoa/extensions/stochastics/stochastic_sequencer.py b/temoa/extensions/stochastics/stochastic_sequencer.py index 9950ee6c..5c904ce3 100644 --- a/temoa/extensions/stochastics/stochastic_sequencer.py +++ b/temoa/extensions/stochastics/stochastic_sequencer.py @@ -34,7 +34,9 @@ def __init__(self, config: TemoaConfig) -> None: self.stoch_config = StochasticConfig.from_toml(sc_path) except Exception as e: logger.exception('Failed to load stochastic config from %s', sc_path) - raise ValueError(f'Error parsing stochastic config {sc_path}. Original error: {e}') from e + raise ValueError( + f'Error parsing stochastic config {sc_path}. Original error: {e}' + ) from e def start(self) -> None: """ From 34c71368a90d61a84e34f278885e419e4c3d8bf7 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Wed, 8 Jul 2026 09:57:58 -0400 Subject: [PATCH 20/28] Bypass duals in highs because they dont work and raise errors for MILP models Signed-off-by: Davey Elder --- temoa/_internal/run_actions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/temoa/_internal/run_actions.py b/temoa/_internal/run_actions.py index af2bc0dd..6aecc7dc 100644 --- a/temoa/_internal/run_actions.py +++ b/temoa/_internal/run_actions.py @@ -244,7 +244,10 @@ def solve_instance( with task_timer(f'Solving model {instance.name}', silent=silent): try: # currently, the highs solver call will puke if the suffixes are passed + # it also doesn't seem to support duals properly so disable those if solver_name == 'appsi_highs': + dual_suffix = instance.component('dual') + dual_suffix._direction = Suffix.LOCAL result = optimizer.solve(instance) else: result = optimizer.solve(instance, suffixes=solver_suffixes_list) From 7ae32ba06ae7caa6f8058e5b27d48365bd745282 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Wed, 8 Jul 2026 09:59:25 -0400 Subject: [PATCH 21/28] Move index set loading to manifest Signed-off-by: Davey Elder --- temoa/data_io/component_manifest.py | 15 +++++++++++++ temoa/data_io/hybrid_loader.py | 35 +++++------------------------ temoa/data_io/loader_manifest.py | 4 ++++ 3 files changed, 24 insertions(+), 30 deletions(-) diff --git a/temoa/data_io/component_manifest.py b/temoa/data_io/component_manifest.py index c02513f9..7521b34c 100644 --- a/temoa/data_io/component_manifest.py +++ b/temoa/data_io/component_manifest.py @@ -307,6 +307,7 @@ def build_manifest(model: TemoaModel, extension_ids: Sequence[str] | None = None validation_map=(0, 1, 2), is_period_filtered=False, is_table_required=False, + index_set=model.cost_invest_rtv, ), LoadItem( component=model.cost_fixed, @@ -327,6 +328,7 @@ def build_manifest(model: TemoaModel, extension_ids: Sequence[str] | None = None table='cost_emission', columns=['region', 'period', 'emis_comm', 'cost'], is_table_required=False, + index_set=model.cost_emission_rpe, ), LoadItem( component=model.loan_rate, @@ -391,6 +393,7 @@ def build_manifest(model: TemoaModel, extension_ids: Sequence[str] | None = None component=model.demand, table='demand', columns=['region', 'period', 'commodity', 'demand'], + index_set=model.demand_constraint_rpc, ), LoadItem( component=model.demand_specific_distribution, @@ -501,6 +504,7 @@ def build_manifest(model: TemoaModel, extension_ids: Sequence[str] | None = None columns=['region', 'period', 'tech_group', 'requirement'], custom_loader_name='_load_rps_requirement', is_table_required=False, + index_set=model.renewable_portfolio_standard_constraint_rpg, ), LoadItem( component=model.capacity_credit, @@ -550,6 +554,7 @@ def build_manifest(model: TemoaModel, extension_ids: Sequence[str] | None = None validation_map=(0, 3), is_period_filtered=False, is_table_required=False, + index_set=model.limit_storage_fraction_param_rsdt, ), LoadItem( component=model.emission_activity, @@ -614,6 +619,7 @@ def build_manifest(model: TemoaModel, extension_ids: Sequence[str] | None = None validator_name='viable_rpt', validation_map=(0, 1, 2), is_table_required=False, + index_set=model.limit_capacity_constraint_rpt, ), LoadItem( component=model.limit_new_capacity, @@ -623,6 +629,7 @@ def build_manifest(model: TemoaModel, extension_ids: Sequence[str] | None = None validation_map=(0, 1, 2), is_period_filtered=False, is_table_required=False, + index_set=model.limit_new_capacity_constraint_rtv, ), LoadItem( component=model.limit_capacity_share, @@ -631,6 +638,7 @@ def build_manifest(model: TemoaModel, extension_ids: Sequence[str] | None = None validator_name='viable_rpt', validation_map=(0, 1, 2), is_table_required=False, + index_set=model.limit_capacity_share_constraint_rpgg, ), LoadItem( component=model.limit_new_capacity_share, @@ -640,6 +648,7 @@ def build_manifest(model: TemoaModel, extension_ids: Sequence[str] | None = None validation_map=(0, 1, 3), is_period_filtered=False, is_table_required=False, + index_set=model.limit_new_capacity_share_constraint_rggv, ), LoadItem( component=model.limit_activity, @@ -648,6 +657,7 @@ def build_manifest(model: TemoaModel, extension_ids: Sequence[str] | None = None validator_name='viable_rpt', validation_map=(0, 1, 2), is_table_required=False, + index_set=model.limit_activity_constraint_rpt, ), LoadItem( component=model.limit_activity_share, @@ -656,6 +666,7 @@ def build_manifest(model: TemoaModel, extension_ids: Sequence[str] | None = None validator_name='viable_rpt', validation_map=(0, 1, 2), is_table_required=False, + index_set=model.limit_activity_share_constraint_rpgg, ), LoadItem( component=model.limit_resource, @@ -665,6 +676,7 @@ def build_manifest(model: TemoaModel, extension_ids: Sequence[str] | None = None validation_map=(0, 1), is_period_filtered=False, is_table_required=False, + index_set=model.limit_resource_constraint_rt, ), LoadItem( component=model.limit_seasonal_capacity_factor, @@ -674,6 +686,7 @@ def build_manifest(model: TemoaModel, extension_ids: Sequence[str] | None = None validation_map=(0, 2), is_period_filtered=False, is_table_required=False, + index_set=model.limit_seasonal_capacity_factor_constraint_rst, ), LoadItem( component=model.limit_annual_capacity_factor, @@ -683,12 +696,14 @@ def build_manifest(model: TemoaModel, extension_ids: Sequence[str] | None = None validation_map=(0, 1, 2, 3), is_period_filtered=False, is_table_required=False, + index_set=model.limit_annual_capacity_factor_constraint_rtvo, ), LoadItem( component=model.limit_emission, table='limit_emission', columns=['region', 'period', 'emis_comm', 'operator', 'value'], is_table_required=False, + index_set=model.limit_emission_constraint_rpe, ), LoadItem( component=model.limit_tech_input_split, diff --git a/temoa/data_io/hybrid_loader.py b/temoa/data_io/hybrid_loader.py index 6dad5030..42ad02e9 100644 --- a/temoa/data_io/hybrid_loader.py +++ b/temoa/data_io/hybrid_loader.py @@ -970,36 +970,11 @@ def load_param_idx_sets(self, data: dict[str, object]) -> dict[str, object]: :param data: The main data dictionary. :return: A dictionary of the new index sets to be added. """ - model = TemoaModel() - param_idx_sets = { - model.cost_invest.name: model.cost_invest_rtv.name, - model.cost_emission.name: model.cost_emission_rpe.name, - model.demand.name: model.demand_constraint_rpc.name, - model.limit_emission.name: model.limit_emission_constraint_rpe.name, - model.limit_activity.name: model.limit_activity_constraint_rpt.name, - model.limit_seasonal_capacity_factor.name: ( - model.limit_seasonal_capacity_factor_constraint_rst.name - ), - model.limit_activity_share.name: model.limit_activity_share_constraint_rpgg.name, - model.limit_annual_capacity_factor.name: ( - model.limit_annual_capacity_factor_constraint_rtvo.name - ), - model.limit_capacity.name: model.limit_capacity_constraint_rpt.name, - model.limit_capacity_share.name: model.limit_capacity_share_constraint_rpgg.name, - model.limit_new_capacity.name: model.limit_new_capacity_constraint_rtv.name, - model.limit_new_capacity_share.name: ( - model.limit_new_capacity_share_constraint_rggv.name - ), - model.limit_resource.name: model.limit_resource_constraint_rt.name, - model.limit_storage_fraction.name: model.limit_storage_fraction_param_rsdt.name, - model.renewable_portfolio_standard.name: ( - model.renewable_portfolio_standard_constraint_rpg.name - ), - } - res: dict[str, object] = {} - for p_name, s_name in param_idx_sets.items(): - param_data = data.get(p_name) + for item in self.manifest: + if item.index_set is None: + continue + param_data = data.get(item.component.name) if isinstance(param_data, dict): - res[s_name] = list(param_data.keys()) + res[item.index_set.name] = list(param_data.keys()) return res diff --git a/temoa/data_io/loader_manifest.py b/temoa/data_io/loader_manifest.py index 886aaa4e..a43d44cb 100644 --- a/temoa/data_io/loader_manifest.py +++ b/temoa/data_io/loader_manifest.py @@ -47,6 +47,9 @@ class LoadItem: `HybridLoader` to handle non-standard loading logic for this component. fallback_data: Optional. A list of default data tuples to use if the table is missing or returns no data. + index_set: Optional. The Pyomo `Set` that is indexed by the keys of + this `Param`. When set, the loader will automatically derive the + set's data from the param's loaded keys during finalization. """ component: ComponentType @@ -61,3 +64,4 @@ class LoadItem: custom_loader_name: str | None = None fallback_data: list[tuple[object, ...]] | None = None order_by: list[str] | None = None + index_set: ComponentType | None = None From 9b4c0e9c55156a7ac48257f342719177486405ce Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Wed, 8 Jul 2026 13:57:54 -0400 Subject: [PATCH 22/28] Tidy up for Bugs Signed-off-by: Davey Elder --- temoa/extensions/discrete_capacity/extension.py | 4 ++-- temoa/extensions/growth_rates/extension.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/temoa/extensions/discrete_capacity/extension.py b/temoa/extensions/discrete_capacity/extension.py index 2a0c141e..d9a062c8 100644 --- a/temoa/extensions/discrete_capacity/extension.py +++ b/temoa/extensions/discrete_capacity/extension.py @@ -11,10 +11,10 @@ owned_tables=('limit_discrete_new_capacity', 'limit_discrete_capacity'), regional_group_tables={ 'limit_discrete_new_capacity': 'region', - 'limit_discrete_capacity': 'region' + 'limit_discrete_capacity': 'region', }, register_model_components=register_model_components, build_manifest_items=build_manifest_items, - schema_sql_path=str(Path(__file__).parents[0] / 'tables.sql'), + schema_sql_path=str(Path(__file__).parent / 'tables.sql'), fail_if_tables_populated_when_disabled=True, ) diff --git a/temoa/extensions/growth_rates/extension.py b/temoa/extensions/growth_rates/extension.py index 19aec700..15847fc0 100644 --- a/temoa/extensions/growth_rates/extension.py +++ b/temoa/extensions/growth_rates/extension.py @@ -26,6 +26,6 @@ }, register_model_components=register_model_components, build_manifest_items=build_manifest_items, - schema_sql_path=str(Path(__file__).parents[0] / 'tables.sql'), + schema_sql_path=str(Path(__file__).parent / 'tables.sql'), fail_if_tables_populated_when_disabled=True, ) From 66a39d37aeeab444223e96c7d73641e8767ae0d8 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Wed, 8 Jul 2026 13:59:02 -0400 Subject: [PATCH 23/28] Build main extension components for economies of scale Signed-off-by: Davey Elder --- temoa/core/model.py | 6 + .../extensions/economies_of_scale/__init__.py | 3 + .../economies_of_scale/components/__init__.py | 0 .../components/cost_fixed_eos.py | 313 ++++++++++++ .../components/cost_invest_eos.py | 474 ++++++++++++++++++ .../components/cost_variable_eos.py | 334 ++++++++++++ .../economies_of_scale/core/__init__.py | 0 .../economies_of_scale/core/model.py | 208 ++++++++ .../economies_of_scale/data_manifest.py | 74 +++ .../economies_of_scale/extension.py | 21 + .../extensions/economies_of_scale/tables.sql | 53 ++ temoa/extensions/framework.py | 3 +- 12 files changed, 1488 insertions(+), 1 deletion(-) create mode 100644 temoa/extensions/economies_of_scale/__init__.py create mode 100644 temoa/extensions/economies_of_scale/components/__init__.py create mode 100644 temoa/extensions/economies_of_scale/components/cost_fixed_eos.py create mode 100644 temoa/extensions/economies_of_scale/components/cost_invest_eos.py create mode 100644 temoa/extensions/economies_of_scale/components/cost_variable_eos.py create mode 100644 temoa/extensions/economies_of_scale/core/__init__.py create mode 100644 temoa/extensions/economies_of_scale/core/model.py create mode 100644 temoa/extensions/economies_of_scale/data_manifest.py create mode 100644 temoa/extensions/economies_of_scale/extension.py create mode 100644 temoa/extensions/economies_of_scale/tables.sql diff --git a/temoa/core/model.py b/temoa/core/model.py index 10f0a77b..24d4dbb0 100755 --- a/temoa/core/model.py +++ b/temoa/core/model.py @@ -39,6 +39,9 @@ technology, time, ) +from temoa.extensions.economies_of_scale.core.model import ( + register_early_eos_components, +) from temoa.extensions.framework import apply_model_extension_hooks, resolve_extension_specs from temoa.model_checking.validators import ( no_slash_or_pipe, @@ -563,6 +566,9 @@ def __init__( ) self.cost_invest = Param(self.cost_invest_rtv) + # Inject cost_invest_eos extension components prior to loan params, if active + register_early_eos_components(self) + self.default_loan_rate = Param(domain=NonNegativeReals) self.loan_rate = Param( self.cost_invest_rtv, domain=NonNegativeReals, default=costs.get_default_loan_rate diff --git a/temoa/extensions/economies_of_scale/__init__.py b/temoa/extensions/economies_of_scale/__init__.py new file mode 100644 index 00000000..42728b34 --- /dev/null +++ b/temoa/extensions/economies_of_scale/__init__.py @@ -0,0 +1,3 @@ +from temoa.extensions.economies_of_scale.extension import EOS_EXTENSION + +__all__ = ['EOS_EXTENSION'] diff --git a/temoa/extensions/economies_of_scale/components/__init__.py b/temoa/extensions/economies_of_scale/components/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/temoa/extensions/economies_of_scale/components/cost_fixed_eos.py b/temoa/extensions/economies_of_scale/components/cost_fixed_eos.py new file mode 100644 index 00000000..9a902eb4 --- /dev/null +++ b/temoa/extensions/economies_of_scale/components/cost_fixed_eos.py @@ -0,0 +1,313 @@ +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, cast + +from pyomo.environ import quicksum, value + +import temoa.components.geography as geography +import temoa.components.technology as technology +from temoa.components.costs import fixed_or_variable_cost + +if TYPE_CHECKING: + from pyomo.core import Expression + from pyomo.core.base.objective import ObjectiveData + from pyomo.core.base.var import VarData + + from temoa.extensions.economies_of_scale.core.model import EOSModel + from temoa.types import ExprLike, Period, Region, Technology + +logger = logging.getLogger(__name__) + + +def period_cost_indices(model: EOSModel) -> set[tuple[Region, Period, Technology]]: + return {(r, p, t) for r, p, t, _ in model.cost_fixed_eos_rptn} + + +def initialize_components(model: EOSModel) -> None: + """ + Organise some data and put up some guard rails + """ + + # Collect segment indices n for each (r,p,t) cluster; order doesn't matter + for r, p, t, n in model.cost_fixed_eos_rptn: + model.cost_fixed_eos_segments.setdefault((r, p, t), set()).add(n) + + # Check that costs and capacities are monotonically increasing + for (r, p, t), segs in model.cost_fixed_eos_segments.items(): + sorted_segs = sorted(segs) + for i, n in enumerate(sorted_segs): + cap_lower, cap_upper, cost_lower, cost_upper = model.cost_fixed_eos[r, p, t, n] + + # Check cost curve is nonnegative and monotonically increasing + # If someone wants to break this assumption they should have the + # skills to edit this code. + if not all( + ( + cap_lower >= 0, + cap_upper >= 0, + cost_lower >= 0, + cost_upper >= 0, + cap_upper > cap_lower, + cost_upper > cost_lower, + ) + ): + msg = ( + 'Negative values or non-increasing segment bounds found ' + f'in cost_fixed_eos table for {r, p, t, n}' + ) + logger.error(msg) + raise ValueError(msg) + + # Backcheck for monotonic growth + if i == 0: + continue + + _, prev_cap_upper, _, prev_cost_upper = model.cost_fixed_eos[ + r, p, t, sorted_segs[i - 1] + ] + + if not all( + ( + abs(cap_lower - prev_cap_upper) <= 0.001, + abs(cost_lower - prev_cost_upper) <= 0.001, + ) + ): + msg = ( + 'Segments in cost_fixed_eos table do not align on their bounds. This would ' + 'leave gaps in the cost curve and could lead to infeasibility. Check ' + f'({r, p, t, sorted_segs[i - 1]}) to ({r, p, t, n})' + ) + logger.error(msg) + raise ValueError(msg) + + +# --- Enforce the rules of cumulative capacity progression up the cost curve --- +def cost_fixed_eos_segment_binary_constraint( + model: EOSModel, r: Region, p: Period, t: Technology +) -> ExprLike: + r""" + Enforce exactly one active segment of the cost curve for each cluster + in each period. + + Each technology cluster :math:`(r, p, t)` has a piecewise-linear cost curve + split into :math:`N_{r,p,t}` segments. The binary variable + :math:`\textbf{CFEB}_{r,p,t,n}` selects which segment is currently active. + Exactly one segment must be active at all times: + + + .. math:: + :label: Cost Fixed EOS Segment Binary Constraint + + \sum_{n \in N_{r,p,t}} \textbf{CFEB}_{r,p,t,n} = 1 + + \qquad \forall \{r, p, t\} \in \Theta_{\text{cost\_fixed\_eos\_period\_rpt}} + + where :math:`\textbf{CFEB}_{r,p,t,n}` is + :code:`v_cost_fixed_eos_segment_binary[r, p, t, n]` (:math:`\in \{0, 1\}`) and + :math:`N_{r,p,t}` is the set of segment indices declared for cluster + :math:`(r, p, t)` in the :code:`cost_fixed_eos` table. + """ + count_active_segments = quicksum( + model.v_cost_fixed_eos_segment_binary[r, p, t, n] + for n in model.cost_fixed_eos_segments[r, p, t] + ) + return count_active_segments == 1 + + +def cost_fixed_eos_capacity_lower_bound_constraint( + model: EOSModel, r: Region, p: Period, t: Technology, n: int +) -> ExprLike: + r""" + Enforce the lower bound of the active EOS cost-curve segment on the + capacity variable for that segment. + + When segment :math:`n` is inactive (:math:`\textbf{CFEB}_{r,p,t,n} = 0`), the + right-hand side collapses to zero so the constraint is non-binding. When + segment :math:`n` is active (:math:`\textbf{CFEB}_{r,p,t,n} = 1`), it + enforces that the segment capacity is at least the lower boundary + :math:`\underline{K}_{r,p,t,n}` of that segment: + + .. math:: + :label: EOS Capacity Lower Bound + + \textbf{CFECAP}_{r,p,t,n} + \ge \textbf{CFEB}_{r,p,t,n} \cdot \underline{K}_{r,p,t,n} + + \qquad \forall \{r, p, t, n\} \in \Theta_{\text{cost\_fixed\_eos\_rptn}} + + where :math:`\textbf{CFECAP}_{r,p,t,n}` is + :code:`v_cost_fixed_eos_capacity[r, p, t, n]`, :math:`\textbf{CFEB}_{r,p,t,n}` + is :code:`v_cost_fixed_eos_segment_binary[r, p, t, n]`, and + :math:`\underline{K}_{r,p,t,n}` is :code:`cost_fixed_eos[r, p, t, n][0]` + (the :code:`capacity_lower` column). + """ + cap_lower = model.v_cost_fixed_eos_segment_binary[r, p, t, n] * value( + model.cost_fixed_eos[r, p, t, n][0] + ) + return model.v_cost_fixed_eos_capacity[r, p, t, n] >= cap_lower + + +def cost_fixed_eos_capacity_upper_bound_constraint( + model: EOSModel, r: Region, p: Period, t: Technology, n: int +) -> ExprLike: + r""" + Enforce the upper bound of the active EOS cost-curve segment on the + capacity variable for that segment. + + This is the mirror of the lower bound. When segment :math:`n` is inactive + the right-hand side collapses to zero, driving + :math:`\textbf{CFECAP}_{r,p,t,n}` to zero. When it is active, + the segment capacity cannot exceed the upper boundary + :math:`\overline{K}_{r,p,t,n}` of that segment: + + .. math:: + :label: EOS Capacity Upper Bound + + \textbf{CFECAP}_{r,p,t,n} + \le \textbf{CFEB}_{r,p,t,n} \cdot \overline{K}_{r,p,t,n} + + \qquad \forall \{r, p, t, n\} \in \Theta_{\text{cost\_fixed\_eos\_rptn}} + + where :math:`\overline{K}_{r,p,t,n}` is :code:`cost_fixed_eos[r, p, t, n][1]` + (the :code:`capacity_upper` column). + + Together the lower and upper bound constraints implement a "big-M"-style + activation: only the single active segment can carry non-zero capacity, + and that capacity is pinned within the segment's range. + """ + cap_upper = model.v_cost_fixed_eos_segment_binary[r, p, t, n] * value( + model.cost_fixed_eos[r, p, t, n][1] + ) + return model.v_cost_fixed_eos_capacity[r, p, t, n] <= cap_upper + + +def cost_fixed_eos_capacity_constraint( + model: EOSModel, r: Region, p: Period, t: Technology +) -> ExprLike: + r""" + Equate the EOS capacity expression to the actual capacity available + in the base model. + + The EOS formulation tracks capacity internally through + :math:`\textbf{CFECAP}_{r,p,t,n}`. This constraint ties those + internal variables to the real decision variables + :math:`\textbf{CAPAVL}_{r,p,t}` from the base Temoa model, + so the cost curve reflects actually-available capacity + rather than a free parameter: + + .. math:: + :label: Cost Fixed EOS Capacity + + \sum_{n \in N_{r,p,t}} \textbf{CFECAP}_{r,p,t,n} + = + \sum_{\substack{r' \in R(r),\, t' \in T(t)}} + \textbf{CAPAVL}_{r',p,t'} + + \qquad \forall \{r, p, t\} \in \Theta_{\text{cost\_fixed\_eos\_period\_rpt}} + + where :math:`R(r)` and :math:`T(t)` expand any group labels to the + individual regions and technologies they contain, and + :math:`\textbf{CAPAVL}_{r',p,t'}` is + :code:`v_capacity_available_by_period_and_tech[r', p, t']`. + """ + + regions = geography.gather_group_regions(model, r) + techs = technology.gather_group_techs(model, t) + + cap_eos = quicksum( + model.v_cost_fixed_eos_capacity[r, p, t, n] for n in model.cost_fixed_eos_segments[r, p, t] + ) + capacity = quicksum( + 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.v_capacity_available_by_period_and_tech + ) + return capacity == cap_eos + + +# --- Calculating costs --- +def cost_fixed_eos_segment_cost( + model: EOSModel, + r: Region, + p: Period, + t: Technology, + n: int, + capacity: ExprLike, + binary: VarData | bool = True, +) -> ExprLike: + """Fixed cost within a segment, if capacity were in that segment""" + cap_lower, cap_upper, cost_lower, cost_upper = model.cost_fixed_eos[r, p, t, n] + m = (value(cost_upper) - value(cost_lower)) / (value(cap_upper) - value(cap_lower)) + return m * capacity + binary * (value(cost_lower) - m * value(cap_lower)) + + +def period_cost(model: EOSModel, r: Region, p: Period, t: Technology) -> Expression: + r""" + EOS clusters do not contribute to the base-model + :math:`\text{CostFixed}` sum. Instead, the extension adds a separate + discounted fixed-cost term to :code:`total_cost` via a + :code:`BuildAction` (``append_cost_fixed_eos_to_total_cost``). + + The *period cost* for cluster :math:`(r, p, t)` is evaluated from the + piecewise-linear cost curve at the capacity available in period :math:`p`: + + .. math:: + + \text{EOS\_PeriodCost}_{r,p,t} = + \sum_{n \in N_{r,p,t}} + \left[ + m_{r,p,t,n} \cdot \textbf{CFECAP}_{r,p,t,n} + + \textbf{CFEB}_{r,p,t,n} + \left(\underline{C}_{r,p,t,n} - m_{r,p,t,n} + \cdot \underline{K}_{r,p,t,n}\right) + \right] + + \text{with slope }m_{r,p,t,n} = \dfrac{\overline{C}_{r,p,t,n} - + \underline{C}_{r,p,t,n}}{\overline{K}_{r,p,t,n} - \underline{K}_{r,p,t,n}} + + Note the terms in this summation are only non-zero for the active segment. + + This cost is then discounted and amortised using the same + :func:`~temoa.components.costs.fixed_or_variable_cost` function as for + cost_fixed in the base model. The result is added to :code:`total_cost` + once per cluster-period combination in + :math:`\Theta_{\text{cost\_fixed\_eos\_period\_rpt}}`. + """ + return quicksum( + cost_fixed_eos_segment_cost( + model, + r, + p, + t, + n, + model.v_cost_fixed_eos_capacity[r, p, t, n], + model.v_cost_fixed_eos_segment_binary[r, p, t, n], + ) + for n in model.cost_fixed_eos_segments[r, p, t] + ) + + +def total_cost(model: EOSModel) -> None: + """ + Discounted fixed costs for all EOS clusters in the planning horizon + """ + + p_0 = min(model.time_optimize) + global_discount_rate = value(model.global_discount_rate) + + total_cost = quicksum( + fixed_or_variable_cost( + 1, + period_cost(model, r, p, t), + value(model.period_length[p]), + global_discount_rate, + p_0, + p=p, + ) + for r, p, t in model.cost_fixed_eos_period_rpt + ) + + # Append to total cost objective + model.total_cost.set_value(cast('ObjectiveData', model.total_cost).expr + total_cost) diff --git a/temoa/extensions/economies_of_scale/components/cost_invest_eos.py b/temoa/extensions/economies_of_scale/components/cost_invest_eos.py new file mode 100644 index 00000000..a59c60e7 --- /dev/null +++ b/temoa/extensions/economies_of_scale/components/cost_invest_eos.py @@ -0,0 +1,474 @@ +from __future__ import annotations + +import logging +from itertools import product as cross_product +from typing import TYPE_CHECKING, cast + +from pyomo.environ import quicksum, value + +import temoa.components.geography as geography +import temoa.components.technology as technology +from temoa.components.costs import loan_cost, loan_cost_survival_curve + +if TYPE_CHECKING: + from pyomo.core import Expression + from pyomo.core.base.objective import ObjectiveData + from pyomo.core.base.var import VarData + + from temoa.extensions.economies_of_scale.core.model import EOSModel + from temoa.types import ExprLike, Period, Region, Technology + +logger = logging.getLogger(__name__) + + +# --- Initialise EOS invest model components --- +def cost_invest_eos_cumulative_capacity_indices( + model: EOSModel, +) -> set[tuple[Region, Period, Technology, int]]: + return { + (r, cast('Period', v), t, n) + for r, t, n in model.cost_invest_eos.sparse_keys() + for _r in geography.gather_group_regions(model, r) + for _t in technology.gather_group_techs(model, t) + for _p in model.time_optimize + for v in model.process_vintages.get((_r, _p, _t), set()) + if v == _p + } + + +def cost_invest_eos_period_cost_indices(model: EOSModel) -> set[tuple[Region, Period, Technology]]: + return {(r, p, t) for r, p, t, _ in model.cost_invest_eos_segment_rptn} + + +def append_cost_invest_eos_rtv(model: EOSModel) -> None: + """ + EOS invest processes most likely do not have a cost_invest parameter, as + that is handled by the EOS cost curve. As a result they would not have + loan parameters either. We add these rtv indices to the base + cost_invest param so EOS invest processes have loan parameters for + discounting in the objective function. + """ + for r, p, t in model.cost_invest_eos_period_rpt: + regions = geography.gather_group_regions(model, r) + techs = technology.gather_group_techs(model, t) + + valid_rtv = { + (_r, _t, p) for _r in regions for _t in techs if (_r, _t, p) in model.process_periods + } + for rtv in valid_rtv: + if rtv not in model.cost_invest_rtv: + model.cost_invest_rtv.add(rtv) + + +def initialize_cost_invest_eos(model: EOSModel) -> None: + """ + Gather segment data and validate guard rails for EOS invest clusters. + """ + + # Collect segment indices n for each (r,t) cluster; order doesn't matter + for r, t, n in model.cost_invest_eos.sparse_keys(): + model.cost_invest_eos_segments.setdefault((r, t), set()).add(n) + + # Check that costs and capacities are monotonically increasing + for (r, t), segs in model.cost_invest_eos_segments.items(): + sorted_segs = sorted(segs) + for i, n in enumerate(sorted_segs): + cap_lower, cap_upper, cost_lower, cost_upper = model.cost_invest_eos[r, t, n] + + # Check cost curve is nonnegative and monotonically increasing. + # If someone wants to break this assumption they should have the + # skills to edit this code. + if not all( + ( + cap_lower >= 0, + cap_upper >= 0, + cost_lower >= 0, + cost_upper >= 0, + cap_upper > cap_lower, + cost_upper > cost_lower, + ) + ): + msg = ( + 'Negative values or non-increasing segment bounds found ' + f'in cost_invest_eos table for {r, t, n}' + ) + logger.error(msg) + raise ValueError(msg) + + # Backcheck for monotonic growth + if i == 0: + continue + + _, prev_cap_upper, _, prev_cost_upper = model.cost_invest_eos[r, t, sorted_segs[i - 1]] + + if not all( + ( + abs(cap_lower - prev_cap_upper) <= 0.001, + abs(cost_lower - prev_cost_upper) <= 0.001, + ) + ): + msg = ( + 'Segments in cost_invest_eos table do not align on their bounds. This would ' + 'leave gaps in the cost curve and could lead to infeasibility. Check ' + f'({r, t, sorted_segs[i - 1]}) to ({r, t, n})' + ) + logger.error(msg) + raise ValueError(msg) + + # Shortens some lines below in error checks + life = model.lifetime_process + loan_life = model.loan_lifetime_process + loan_rate = model.loan_rate + + # Get a viable built process for each EOS invest period, for discounting + # parameters. Check that all r, t combos in the cluster share the same + # lifetime and loan parameters. + for r, p, t in model.cost_invest_eos_period_rpt: + regions = geography.gather_group_regions(model, r) + techs = technology.gather_group_techs(model, t) + + for _r, _t in cross_product(regions, techs): + if (_r, _t, p) in model.process_periods: + # Store first valid process as our reference for this period + if (r, p, t) not in model.cost_invest_eos_reference_process: + model.cost_invest_eos_reference_process[r, p, t] = _r, _t + + # Check that the assumptions hold + r0, t0 = model.cost_invest_eos_reference_process[r, p, t] + if any( + ( + abs(life[r0, t0, p] - life[_r, _t, p]) >= 0.001, + abs(loan_life[r0, t0, p] - loan_life[_r, _t, p]) >= 0.001, + abs(loan_rate[r0, t0, p] - loan_rate[_r, _t, p]) >= 0.001, + ) + ): + msg = ( + 'Processes assigned to the same cost_invest_eos cost curve must all have ' + 'the same process lifetime, loan lifetime, and loan rate. These two ' + 'processes do not match:\n ' + f'({r0}, {t0}, {p}) : lifetime = {life[r0, t0, p]}, ' + f'loan life = {loan_life[r0, t0, p]}, loan rate = {loan_rate[r0, t0, p]}\n' + f'({_r}, {_t}, {p}) : lifetime = {life[_r, _t, p]}, ' + f'loan life = {loan_life[_r, _t, p]}, loan rate = {loan_rate[_r, _t, p]}' + ) + logger.error(msg) + raise ValueError(msg) + + +# --- Enforce the rules of cumulative capacity progression up the cost curve --- +def cost_invest_eos_segment_binary_constraint( + model: EOSModel, r: Region, p: Period, t: Technology +) -> ExprLike: + r""" + Enforce exactly one active segment of the EOS invest cost curve for each + cluster in each period. + + Each technology cluster :math:`(r, t)` has a piecewise-linear investment + cost curve split into :math:`N_{r,t}` segments. The binary variable + :math:`\textbf{CIEB}_{r,p,t,n}` selects which segment is currently active. + Exactly one segment must be active at all times: + + .. math:: + :label: Cost Invest EOS Segment Binary Constraint + + \sum_{n \in N_{r,t}} \textbf{CIEB}_{r,p,t,n} = 1 + + \qquad \forall \{r, p, t\} \in \Theta_{\text{cost\_invest\_eos\_period\_rpt}} + + where :math:`\textbf{CIEB}_{r,p,t,n}` is + :code:`v_cost_invest_eos_segment_binary[r, p, t, n]` (:math:`\in \{0, 1\}`) and + :math:`N_{r,t}` is the set of segment indices declared for cluster + :math:`(r, t)` in the :code:`cost_invest_eos` table. + """ + count_active_segments = quicksum( + model.v_cost_invest_eos_segment_binary[r, p, t, n] + for n in model.cost_invest_eos_segments[r, t] + ) + return count_active_segments == 1 + + +def cost_invest_eos_capacity_lower_bound_constraint( + model: EOSModel, r: Region, p: Period, t: Technology, n: int +) -> ExprLike: + r""" + Enforce the lower bound of the active EOS invest cost-curve segment on + cumulative capacity. + + When segment :math:`n` is inactive (:math:`\textbf{CIEB}_{r,p,t,n} = 0`), the + right-hand side collapses to zero so the constraint is non-binding. When + segment :math:`n` is active (:math:`\textbf{CIEB}_{r,p,t,n} = 1`), it + enforces that cumulative capacity has reached at least the lower boundary + :math:`\underline{K}_{r,t,n}` of that segment: + + .. math:: + :label: Cost Invest EOS Capacity Lower Bound + + \textbf{CIECAP}_{r,p,t,n} + \ge \textbf{CIEB}_{r,p,t,n} \cdot \underline{K}_{r,t,n} + + \qquad \forall \{r, p, t, n\} \in \Theta_{\text{cost\_invest\_eos\_segment\_rptn}} + + where :math:`\textbf{CIECAP}_{r,p,t,n}` is + :code:`v_cost_invest_eos_cumulative_capacity[r, p, t, n]`, + :math:`\textbf{CIEB}_{r,p,t,n}` is + :code:`v_cost_invest_eos_segment_binary[r, p, t, n]`, and + :math:`\underline{K}_{r,t,n}` is :code:`cost_invest_eos[r, t, n][0]` + (the :code:`capacity_lower` column). + """ + cap_lower = model.v_cost_invest_eos_segment_binary[r, p, t, n] * value( + model.cost_invest_eos[r, t, n][0] + ) + return model.v_cost_invest_eos_cumulative_capacity[r, p, t, n] >= cap_lower + + +def cost_invest_eos_capacity_upper_bound_constraint( + model: EOSModel, r: Region, p: Period, t: Technology, n: int +) -> ExprLike: + r""" + Enforce the upper bound of the active EOS invest cost-curve segment on + cumulative capacity. + + This is the mirror of the lower bound. When segment :math:`n` is inactive + the right-hand side collapses to zero, driving + :math:`\textbf{CIECAP}_{r,p,t,n}` to zero. When it is active, + cumulative capacity cannot exceed the upper boundary + :math:`\overline{K}_{r,t,n}` of that segment: + + .. math:: + :label: Cost Invest EOS Capacity Upper Bound + + \textbf{CIECAP}_{r,p,t,n} + \le \textbf{CIEB}_{r,p,t,n} \cdot \overline{K}_{r,t,n} + + \qquad \forall \{r, p, t, n\} \in \Theta_{\text{cost\_invest\_eos\_segment\_rptn}} + + where :math:`\overline{K}_{r,t,n}` is :code:`cost_invest_eos[r, t, n][1]` + (the :code:`capacity_upper` column). + + Together the lower and upper bound constraints implement a "big-M"-style + activation: only the single active segment can carry non-zero capacity, + and that capacity is pinned within the segment's range. + """ + cap_upper = model.v_cost_invest_eos_segment_binary[r, p, t, n] * value( + model.cost_invest_eos[r, t, n][1] + ) + return model.v_cost_invest_eos_cumulative_capacity[r, p, t, n] <= cap_upper + + +def cost_invest_eos_cumulative_capacity_constraint( + model: EOSModel, r: Region, p: Period, t: Technology +) -> ExprLike: + r""" + Equate the EOS invest cumulative-capacity expression to the actual capacity + built in the base model. + + The EOS invest formulation tracks cumulative capacity internally through + :math:`\textbf{CIECAP}_{r,p,t,n}`. This constraint ties those internal + variables to the real decision variables :math:`\textbf{NCAP}_{r,t,v}` and + the parameter :math:`\textbf{ECAP}_{r,t,v}` from the base Temoa model, so + the cost curve reflects actually-built capacity rather than a free + parameter: + + .. math:: + :label: Cost Invest EOS Cumulative Capacity + + \sum_{n \in N_{r,t}} \textbf{CIECAP}_{r,p,t,n} + = + \sum_{\substack{r' \in R(r),\, t' \in T(t),\\ v \in V}} + \textbf{ECAP}_{r',t',v} + + + \sum_{\substack{r' \in R(r),\, t' \in T(t),\\ v \le p,\, v \in \mathcal{V}(r',t')}} + \textbf{NCAP}_{r',t',v} + + \qquad \forall \{r, p, t\} \in \Theta_{\text{cost\_invest\_eos\_period\_rpt}} + + where :math:`R(r)` and :math:`T(t)` expand any group labels to the + individual regions and technologies they contain, and + :math:`\mathcal{V}(r', t')` is the set of all optimised vintages for that + process. + """ + + regions = geography.gather_group_regions(model, r) + techs = technology.gather_group_techs(model, t) + + # What EOS invest thinks the cumulative capacity is for this cluster in this period + cap_eos = quicksum( + model.v_cost_invest_eos_cumulative_capacity[r, p, t, n] + for n in model.cost_invest_eos_segments[r, t] + ) + + # Sum all actually built capacity so far + cap_deployed = quicksum( + value(model.existing_capacity[_r, _t, _v]) + for _r, _t, _v in model.existing_capacity.sparse_keys() + if _r in regions and _t in techs + ) + cap_deployed += quicksum( + model.v_new_capacity[_r, _t, _v] + for _v in model.vintage_optimize + if _v <= p + for _r in regions + for _t in techs + if (_r, _t, _v) in model.v_new_capacity + ) + + return cap_deployed == cap_eos + + +# --- Calculating investment costs --- +def cost_invest_eos_segment_cost( + model: EOSModel, + r: Region, + t: Technology, + n: int, + cumulative_capacity: ExprLike, + binary: VarData | bool = True, +) -> ExprLike: + """Cumulative investment cost within an EOS invest segment, if capacity were in that segment""" + cap_lower, cap_upper, cost_lower, cost_upper = model.cost_invest_eos[r, t, n] + m = (value(cost_upper) - value(cost_lower)) / (value(cap_upper) - value(cap_lower)) + return m * cumulative_capacity + binary * (value(cost_lower) - m * value(cap_lower)) + + +def cost_invest_eos_cluster_cumulative_cost( + model: EOSModel, r: Region, p: Period, t: Technology +) -> Expression: + """ + Cumulative investment cost for an EOS invest cluster up to period p, + accounting for which segment of the cost curve is active. + """ + return quicksum( + cost_invest_eos_segment_cost( + model, + r, + t, + n, + model.v_cost_invest_eos_cumulative_capacity[r, p, t, n], + model.v_cost_invest_eos_segment_binary[r, p, t, n], + ) + for n in model.cost_invest_eos_segments[r, t] + ) + + +def period_cost(model: EOSModel, r: Region, p: Period, t: Technology) -> Expression: + r""" + EOS invest clusters do not contribute to the base-model + :math:`\text{CostInvest}` sum. Instead, the extension adds a separate + discounted-investment term to :code:`total_cost` via a + :code:`BuildAction` (``append_cost_invest_eos_to_total_cost``). + + The *period cost* for cluster :math:`(r, p, t)` is the *incremental* + cumulative cost: the cost of all capacity built up to period :math:`p` + minus the cost of all capacity built up to the preceding EOS invest period + (or the cost attributable to pre-existing capacity in the first period): + + .. math:: + + \text{EOS\_Invest\_PeriodCost}_{r,p,t} = + \mathcal{C}_{r,p,t} - \mathcal{C}_{r,p_{\text{prev}},t} + + where the cumulative cost for a cluster in a given period is: + + .. math:: + + \mathcal{C}_{r,p,t} = + \sum_{n \in N_{r,t}} + \left[ + m_{r,t,n} \cdot \textbf{CIECAP}_{r,p,t,n} + + \textbf{CIEB}_{r,p,t,n} + \left(\underline{C}_{r,t,n} - m_{r,t,n} \cdot \underline{K}_{r,t,n}\right) + \right] + + \text{with slope }m_{r,p,t,n} = \dfrac{\overline{C}_{r,p,t,n} - + \underline{C}_{r,p,t,n}}{\overline{K}_{r,p,t,n} - \underline{K}_{r,p,t,n}} + + Note the terms in this summation are only non-zero for the active segment. + + This incremental cost is then discounted and amortised using the same + :func:`~temoa.components.costs.loan_cost` function as for cost_invest in + the base model, using the reference process :math:`(r_0, t_0)` for loan + parameters. The result is added to :code:`total_cost` once per + cluster-period combination in + :math:`\Theta_{\text{cost\_invest\_eos\_period\_rpt}}`. + """ + + cumulative_cost = cost_invest_eos_cluster_cumulative_cost(model, r, p, t) + prev_cum_cost: ExprLike = 0.0 + + # Subtract previously accumulated investment costs to get the incremental for discounting + prev_periods = { + _p for _r, _p, _t in model.cost_invest_eos_period_rpt if _r == r and _t == t and _p < p + } + if len(prev_periods) > 0: + # Endogenous previous cumulative investment costs to subtract + p_prev = max(prev_periods) + prev_cum_cost = cost_invest_eos_cluster_cumulative_cost(model, r, p_prev, t) + else: + # Existing capacity costs to subtract (needed for myopic) + regions = geography.gather_group_regions(model, r) + techs = technology.gather_group_techs(model, t) + existing_capacity = quicksum( + value(model.existing_capacity[_r, _t, _v]) + for _r, _t, _v in model.existing_capacity + if _r in regions and _t in techs + ) + if existing_capacity: + for n in model.cost_invest_eos_segments[r, t]: + cap_lower, cap_upper, _, _ = model.cost_invest_eos[r, t, n] + if value(cap_lower) <= existing_capacity <= value(cap_upper): + prev_cum_cost = cost_invest_eos_segment_cost(model, r, t, n, existing_capacity) + continue # in case we're exactly on the boundary of two segments + if not prev_cum_cost: + msg = ( + 'Existing capacity for a cost_invest_eos cluster is outside the bounds of ' + 'the cost curve. Check the cost_invest_eos table and existing_capacity ' + f'for {r, t}: {existing_capacity}' + ) + logger.error(msg) + raise ValueError(msg) + + return cumulative_cost - prev_cum_cost + + +def total_cost(model: EOSModel) -> None: + """ + Discounted investment costs for all EOS invest clusters in the planning horizon + """ + + p_0 = min(model.time_optimize) + p_e = model.time_future.last() + global_discount_rate = value(model.global_discount_rate) + + total_cost = 0.0 + + for _r, _p, _t in model.cost_invest_eos_period_rpt: + _r0, _t0 = model.cost_invest_eos_reference_process[_r, _p, _t] + if model.is_survival_curve_process[_r0, _t0, _p]: + total_cost += loan_cost_survival_curve( + model, + _r0, + _t0, + _p, + 1, + period_cost(model, _r, _p, _t), + value(model.loan_annualize[_r0, _t0, _p]), + value(model.loan_lifetime_process[_r0, _t0, _p]), + p_0, + p_e, + global_discount_rate, + ) + else: + total_cost += loan_cost( + 1, + period_cost(model, _r, _p, _t), + value(model.loan_annualize[_r0, _t0, _p]), + value(model.loan_lifetime_process[_r0, _t0, _p]), + value(model.lifetime_process[_r0, _t0, _p]), + p_0, + p_e, + global_discount_rate, + vintage=_p, + ) + + # Append to total cost objective + model.total_cost.set_value(cast('ObjectiveData', model.total_cost).expr + total_cost) diff --git a/temoa/extensions/economies_of_scale/components/cost_variable_eos.py b/temoa/extensions/economies_of_scale/components/cost_variable_eos.py new file mode 100644 index 00000000..e65868cc --- /dev/null +++ b/temoa/extensions/economies_of_scale/components/cost_variable_eos.py @@ -0,0 +1,334 @@ +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, cast + +from pyomo.environ import quicksum, value + +import temoa.components.geography as geography +import temoa.components.technology as technology +from temoa.components.costs import fixed_or_variable_cost + +if TYPE_CHECKING: + from pyomo.core import Expression + from pyomo.core.base.objective import ObjectiveData + from pyomo.core.base.var import VarData + + from temoa.extensions.economies_of_scale.core.model import EOSModel + from temoa.types import ExprLike, Period, Region, Technology + +logger = logging.getLogger(__name__) + + +def period_cost_indices(model: EOSModel) -> set[tuple[Region, Period, Technology]]: + return {(r, p, t) for r, p, t, _ in model.cost_variable_eos_rptn} + + +def initialize_components(model: EOSModel) -> None: + """ + Organise some data and put up some guard rails + """ + + # Collect segment indices n for each (r,p,t) cluster; order doesn't matter + for r, p, t, n in model.cost_variable_eos_rptn: + model.cost_variable_eos_segments.setdefault((r, p, t), set()).add(n) + + # Check that costs and activities are monotonically increasing + for (r, p, t), segs in model.cost_variable_eos_segments.items(): + sorted_segs = sorted(segs) + for i, n in enumerate(sorted_segs): + act_lower, act_upper, cost_lower, cost_upper = model.cost_variable_eos[r, p, t, n] + + # Check cost curve is nonnegative and monotonically increasing + # If someone wants to break this assumption they should have the + # skills to edit this code. + if not all( + ( + act_lower >= 0, + act_upper >= 0, + cost_lower >= 0, + cost_upper >= 0, + act_upper > act_lower, + cost_upper > cost_lower, + ) + ): + msg = ( + 'Negative values or non-increasing segment bounds found ' + f'in cost_variable_eos table for {r, p, t, n}' + ) + logger.error(msg) + raise ValueError(msg) + + # Backcheck for monotonic growth + if i == 0: + continue + + _, prev_act_upper, _, prev_cost_upper = model.cost_variable_eos[ + r, p, t, sorted_segs[i - 1] + ] + + if not all( + ( + abs(act_lower - prev_act_upper) <= 0.001, + abs(cost_lower - prev_cost_upper) <= 0.001, + ) + ): + msg = ( + 'Segments in cost_variable_eos table do not align on their bounds. This would ' + 'leave gaps in the cost curve and could lead to infeasibility. Check ' + f'({r, p, t, sorted_segs[i - 1]}) to ({r, p, t, n})' + ) + logger.error(msg) + raise ValueError(msg) + + +# --- Enforce the rules of activity progression up the cost curve --- +def cost_variable_eos_segment_binary_constraint( + model: EOSModel, r: Region, p: Period, t: Technology +) -> ExprLike: + r""" + Enforce exactly one active segment of the cost curve for each cluster + in each period. + + Each technology cluster :math:`(r, p, t)` has a piecewise-linear cost curve + split into :math:`N_{r,p,t}` segments. The binary variable + :math:`\textbf{CVEB}_{r,p,t,n}` selects which segment is currently active. + Exactly one segment must be active at all times: + + .. math:: + :label: Cost Variable EOS Segment Binary Constraint + + \sum_{n \in N_{r,p,t}} \textbf{CVEB}_{r,p,t,n} = 1 + + \qquad \forall \{r, p, t\} \in \Theta_{\text{cost\_variable\_eos\_period\_rpt}} + + where :math:`\textbf{CVEB}_{r,p,t,n}` is + :code:`v_cost_variable_eos_segment_binary[r, p, t, n]` (:math:`\in \{0, 1\}`) and + :math:`N_{r,p,t}` is the set of segment indices declared for cluster + :math:`(r, p, t)` in the :code:`cost_variable_eos` table. + """ + count_active_segments = quicksum( + model.v_cost_variable_eos_segment_binary[r, p, t, n] + for n in model.cost_variable_eos_segments[r, p, t] + ) + return count_active_segments == 1 + + +def cost_variable_eos_activity_lower_bound_constraint( + model: EOSModel, r: Region, p: Period, t: Technology, n: int +) -> ExprLike: + r""" + Enforce the lower bound of the active EOS cost-curve segment on the + activity variable for that segment. + + When segment :math:`n` is inactive (:math:`\textbf{CVEB}_{r,p,t,n} = 0`), the + right-hand side collapses to zero so the constraint is non-binding. When + segment :math:`n` is active (:math:`\textbf{CVEB}_{r,p,t,n} = 1`), it + enforces that the segment activity is at least the lower boundary + :math:`\underline{A}_{r,p,t,n}` of that segment: + + .. math:: + :label: Cost Variable EOS Activity Lower Bound + + \textbf{CVEACT}_{r,p,t,n} + \ge \textbf{CVEB}_{r,p,t,n} \cdot \underline{A}_{r,p,t,n} + + \qquad \forall \{r, p, t, n\} \in \Theta_{\text{cost\_variable\_eos\_rptn}} + + where :math:`\textbf{CVEACT}_{r,p,t,n}` is + :code:`v_cost_variable_eos_activity[r, p, t, n]`, :math:`\textbf{CVEB}_{r,p,t,n}` + is :code:`v_cost_variable_eos_segment_binary[r, p, t, n]`, and + :math:`\underline{A}_{r,p,t,n}` is :code:`cost_variable_eos[r, p, t, n][0]` + (the :code:`activity_lower` column). + """ + act_lower = model.v_cost_variable_eos_segment_binary[r, p, t, n] * value( + model.cost_variable_eos[r, p, t, n][0] + ) + return model.v_cost_variable_eos_activity[r, p, t, n] >= act_lower + + +def cost_variable_eos_activity_upper_bound_constraint( + model: EOSModel, r: Region, p: Period, t: Technology, n: int +) -> ExprLike: + r""" + Enforce the upper bound of the active EOS cost-curve segment on the + activity variable for that segment. + + This is the mirror of the lower bound. When segment :math:`n` is inactive + the right-hand side collapses to zero, driving + :math:`\textbf{CVEACT}_{r,p,t,n}` to zero. When it is active, + the segment activity cannot exceed the upper boundary + :math:`\overline{A}_{r,p,t,n}` of that segment: + + .. math:: + :label: Cost Variable EOS Activity Upper Bound + + \textbf{CVEACT}_{r,p,t,n} + \le \textbf{CVEB}_{r,p,t,n} \cdot \overline{A}_{r,p,t,n} + + \qquad \forall \{r, p, t, n\} \in \Theta_{\text{cost\_variable\_eos\_rptn}} + + where :math:`\overline{A}_{r,p,t,n}` is :code:`cost_variable_eos[r, p, t, n][1]` + (the :code:`activity_upper` column). + + Together the lower and upper bound constraints implement a "big-M"-style + activation: only the single active segment can carry non-zero activity, + and that activity is pinned within the segment's range. + """ + act_upper = model.v_cost_variable_eos_segment_binary[r, p, t, n] * value( + model.cost_variable_eos[r, p, t, n][1] + ) + return model.v_cost_variable_eos_activity[r, p, t, n] <= act_upper + + +def cost_variable_eos_activity_constraint( + model: EOSModel, r: Region, p: Period, t: Technology +) -> ExprLike: + r""" + Equate the EOS activity expression to the actual total activity in the + base model. + + The EOS formulation tracks activity internally through + :math:`\textbf{CVEACT}_{r,p,t,n}`. This constraint ties those + internal variables to the real flow decision variables from the base + Temoa model, so the cost curve reflects actual activity rather than + a free parameter: + + .. math:: + :label: Cost Variable EOS Activity + + \sum_{n \in N_{r,p,t}} \textbf{CVEACT}_{r,p,t,n} + = + \sum_{\substack{r' \in R(r),\, t' \in T(t) \setminus T_a,\\ + v,\, s,\, d,\, i,\, o}} + \textbf{FO}_{r',p,s,d,i,t',v,o} + + + \sum_{\substack{r' \in R(r),\, t' \in T(t) \cap T_a,\\ + v,\, i,\, o}} + \textbf{FOA}_{r',p,i,t',v,o} + + \qquad \forall \{r, p, t\} \in \Theta_{\text{cost\_variable\_eos\_period\_rpt}} + + where :math:`R(r)` and :math:`T(t)` expand any group labels to the + individual regions and technologies they contain, :math:`T_a` is the set + of annual technologies (:code:`tech_annual`), + :math:`\textbf{FO}` is :code:`v_flow_out` summed over all vintages, + seasons :math:`s`, times of day :math:`d`, inputs :math:`i`, and outputs + :math:`o` active in period :math:`p`, and :math:`\textbf{FOA}` is + :code:`v_flow_out_annual` for annual technologies (no :math:`s,d` indices). + """ + + regions = geography.gather_group_regions(model, r) + techs = technology.gather_group_techs(model, t) + + activity_eos = quicksum( + model.v_cost_variable_eos_activity[r, p, t, n] + for n in model.cost_variable_eos_segments[r, p, t] + ) + activity = quicksum( + model.v_flow_out[_r, p, s, d, S_i, _t, S_v, S_o] + for _t in techs + if _t not in model.tech_annual + for _r in regions + for S_v in model.process_vintages.get((_r, p, _t), []) + for S_i in model.process_inputs[_r, p, _t, S_v] + for S_o in model.process_outputs_by_input[_r, p, _t, S_v, S_i] + for s in model.time_season + for d in model.time_of_day + ) + activity += quicksum( + model.v_flow_out_annual[_r, p, S_i, _t, S_v, S_o] + for _t in techs + if _t in model.tech_annual + for _r in regions + for S_v in model.process_vintages.get((_r, p, _t), []) + for S_i in model.process_inputs[_r, p, _t, S_v] + for S_o in model.process_outputs_by_input[_r, p, _t, S_v, S_i] + ) + return activity == activity_eos + + +# --- Calculating costs --- +def cost_variable_eos_segment_cost( + model: EOSModel, + r: Region, + p: Period, + t: Technology, + n: int, + activity: ExprLike, + binary: VarData | bool = True, +) -> ExprLike: + """Variable cost within a segment, if activity were in that segment""" + act_lower, act_upper, cost_lower, cost_upper = model.cost_variable_eos[r, p, t, n] + m = (value(cost_upper) - value(cost_lower)) / (value(act_upper) - value(act_lower)) + return m * activity + binary * (value(cost_lower) - m * value(act_lower)) + + +def period_cost(model: EOSModel, r: Region, p: Period, t: Technology) -> Expression: + r""" + EOS clusters do not contribute to the base-model + :math:`\text{CostVariable}` sum. Instead, the extension adds a separate + discounted variable-cost term to :code:`total_cost` via a + :code:`BuildAction` (``append_cost_variable_eos_to_total_cost``). + + The *period cost* for cluster :math:`(r, p, t)` is evaluated from the + piecewise-linear cost curve at the total activity in period :math:`p`: + + .. math:: + + \text{EOS\_PeriodCost}_{r,p,t} = + \sum_{n \in N_{r,p,t}} + \left[ + m_{r,p,t,n} \cdot \textbf{CVEACT}_{r,p,t,n} + + \textbf{CVEB}_{r,p,t,n} + \left(\underline{C}_{r,p,t,n} - m_{r,p,t,n} + \cdot \underline{A}_{r,p,t,n}\right) + \right] + + \text{with slope }m_{r,p,t,n} = \dfrac{\overline{C}_{r,p,t,n} - + \underline{C}_{r,p,t,n}}{\overline{A}_{r,p,t,n} - \underline{A}_{r,p,t,n}} + + Note the terms in this summation are only non-zero for the active segment. + + This cost is then discounted and amortised using the same + :func:`~temoa.components.costs.fixed_or_variable_cost` function as for + cost_variable in the base model. The result is added to :code:`total_cost` + once per cluster-period combination in + :math:`\Theta_{\text{cost\_variable\_eos\_period\_rpt}}`. + """ + return quicksum( + cost_variable_eos_segment_cost( + model, + r, + p, + t, + n, + model.v_cost_variable_eos_activity[r, p, t, n], + model.v_cost_variable_eos_segment_binary[r, p, t, n], + ) + for n in model.cost_variable_eos_segments[r, p, t] + ) + + +def total_cost(model: EOSModel) -> None: + """ + Discounted fixed costs for all EOS clusters in the planning horizon + """ + + p_0 = min(model.time_optimize) + global_discount_rate = value(model.global_discount_rate) + + total_cost = quicksum( + fixed_or_variable_cost( + 1, + period_cost(model, r, p, t), + value(model.period_length[p]), + global_discount_rate, + p_0, + p=p, + ) + for r, p, t in model.cost_variable_eos_period_rpt + ) + + # Append to total cost objective + model.total_cost.set_value(cast('ObjectiveData', model.total_cost).expr + total_cost) diff --git a/temoa/extensions/economies_of_scale/core/__init__.py b/temoa/extensions/economies_of_scale/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/temoa/extensions/economies_of_scale/core/model.py b/temoa/extensions/economies_of_scale/core/model.py new file mode 100644 index 00000000..82c0d0e2 --- /dev/null +++ b/temoa/extensions/economies_of_scale/core/model.py @@ -0,0 +1,208 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, cast + +from pyomo.environ import ( + Any, + Binary, + BuildAction, + Constraint, + Integers, + NonNegativeReals, + Param, + Set, + Var, +) + +import temoa.extensions.economies_of_scale.components.cost_fixed_eos as cost_fixed_eos +import temoa.extensions.economies_of_scale.components.cost_invest_eos as cost_invest_eos +import temoa.extensions.economies_of_scale.components.cost_variable_eos as cost_variable_eos + +if TYPE_CHECKING: + from temoa.core.model import TemoaModel + from temoa.types.core_types import Period, Region, Technology + + class EOSModel(TemoaModel): + """Inherits the base TemoaModel class and declares EOS extension + components for type checking/hinting purposes""" + + # Param (data from database table) + cost_invest_eos: Param + cost_fixed_eos: Param + cost_variable_eos: Param + + # Primitive components (instantiation helpers) + cost_invest_eos_segments: dict[tuple[Region, Technology], set[int]] + cost_invest_eos_reference_process: dict[ + tuple[Region, Period, Technology], tuple[Region, Technology] + ] + cost_fixed_eos_segments: dict[tuple[Region, Period, Technology], set[int]] + cost_variable_eos_segments: dict[tuple[Region, Period, Technology], set[int]] + + # Sets (indexing sets for variables and constraints) + cost_invest_eos_rtn: Set + cost_invest_eos_segment_rptn: Set + cost_invest_eos_period_rpt: Set + cost_fixed_eos_rptn: Set + cost_variable_eos_rptn: Set + cost_fixed_eos_period_rpt: Set + cost_variable_eos_period_rpt: Set + + # Instantiation build actions + append_cost_invest_eos_to_cost_invest_rtv: BuildAction + initialize_cost_invest_eos: BuildAction + append_cost_invest_eos_to_total_cost: BuildAction + initialize_cost_fixed_eos: BuildAction + append_cost_fixed_eos_to_total_cost: BuildAction + initialize_cost_variable_eos: BuildAction + append_cost_variable_eos_to_total_cost: BuildAction + + # Decision variables + v_cost_invest_eos_cumulative_capacity: Var + v_cost_invest_eos_segment_binary: Var + v_cost_fixed_eos_capacity: Var + v_cost_fixed_eos_segment_binary: Var + v_cost_variable_eos_activity: Var + v_cost_variable_eos_segment_binary: Var + + # Constraints + cost_invest_eos_segment_binary_constraint: Constraint + cost_invest_eos_capacity_lower_bound_constraint: Constraint + cost_invest_eos_capacity_upper_bound_constraint: Constraint + cost_invest_eos_cumulative_capacity_constraint: Constraint + cost_fixed_eos_segment_binary_constraint: Constraint + cost_fixed_eos_capacity_lower_bound_constraint: Constraint + cost_fixed_eos_capacity_upper_bound_constraint: Constraint + cost_fixed_eos_capacity_constraint: Constraint + cost_variable_eos_segment_binary_constraint: Constraint + cost_variable_eos_activity_lower_bound_constraint: Constraint + cost_variable_eos_activity_upper_bound_constraint: Constraint + cost_variable_eos_activity_constraint: Constraint + + +def register_early_eos_components(model: TemoaModel) -> None: + """Build cost_invest_eos components that must be instantiated before loan parameters.""" + if 'eos' not in model.enabled_extensions: + return + m = cast('EOSModel', model) + + m.cost_invest_eos_rtn = Set( + within=model.regional_global_indices * model.tech_or_group * Integers + ) + m.cost_invest_eos = Param(m.cost_invest_eos_rtn, domain=Any) + + m.cost_invest_eos_segments = {} + m.cost_invest_eos_reference_process = {} + + m.cost_invest_eos_segment_rptn = Set( + dimen=4, initialize=cost_invest_eos.cost_invest_eos_cumulative_capacity_indices + ) + m.cost_invest_eos_period_rpt = Set( + dimen=3, initialize=cost_invest_eos.cost_invest_eos_period_cost_indices + ) + + m.append_cost_invest_eos_to_cost_invest_rtv = BuildAction( + rule=cost_invest_eos.append_cost_invest_eos_rtv + ) + + +def register_model_components(model: TemoaModel) -> None: + """Build remaining components that can be instantiated at the end of model instantiation""" + m = cast('EOSModel', model) + + # --- cost_invest_eos --- + m.initialize_cost_invest_eos = BuildAction(rule=cost_invest_eos.initialize_cost_invest_eos) + + m.v_cost_invest_eos_cumulative_capacity = Var( + m.cost_invest_eos_segment_rptn, domain=NonNegativeReals, initialize=0 + ) + m.v_cost_invest_eos_segment_binary = Var( + m.cost_invest_eos_segment_rptn, domain=Binary, initialize=0 + ) + + m.cost_invest_eos_segment_binary_constraint = Constraint( + m.cost_invest_eos_period_rpt, + rule=cost_invest_eos.cost_invest_eos_segment_binary_constraint, + ) + m.cost_invest_eos_cumulative_capacity_constraint = Constraint( + m.cost_invest_eos_period_rpt, + rule=cost_invest_eos.cost_invest_eos_cumulative_capacity_constraint, + ) + m.cost_invest_eos_capacity_lower_bound_constraint = Constraint( + m.cost_invest_eos_segment_rptn, + rule=cost_invest_eos.cost_invest_eos_capacity_lower_bound_constraint, + ) + m.cost_invest_eos_capacity_upper_bound_constraint = Constraint( + m.cost_invest_eos_segment_rptn, + rule=cost_invest_eos.cost_invest_eos_capacity_upper_bound_constraint, + ) + + m.append_cost_invest_eos_to_total_cost = BuildAction(rule=cost_invest_eos.total_cost) + + # --- cost_fixed_eos --- + m.cost_fixed_eos_rptn = Set( + within=model.regional_global_indices * model.time_optimize * model.tech_or_group * Integers + ) + m.cost_fixed_eos = Param(m.cost_fixed_eos_rptn, domain=Any) + + m.cost_fixed_eos_segments = {} + + m.cost_fixed_eos_period_rpt = Set(dimen=3, initialize=cost_fixed_eos.period_cost_indices) + + m.initialize_cost_fixed_eos = BuildAction(rule=cost_fixed_eos.initialize_components) + + m.v_cost_fixed_eos_capacity = Var(m.cost_fixed_eos_rptn, domain=NonNegativeReals, initialize=0) + m.v_cost_fixed_eos_segment_binary = Var(m.cost_fixed_eos_rptn, domain=Binary, initialize=0) + + m.cost_fixed_eos_segment_binary_constraint = Constraint( + m.cost_fixed_eos_period_rpt, rule=cost_fixed_eos.cost_fixed_eos_segment_binary_constraint + ) + m.cost_fixed_eos_capacity_constraint = Constraint( + m.cost_fixed_eos_period_rpt, rule=cost_fixed_eos.cost_fixed_eos_capacity_constraint + ) + m.cost_fixed_eos_capacity_lower_bound_constraint = Constraint( + m.cost_fixed_eos_rptn, + rule=cost_fixed_eos.cost_fixed_eos_capacity_lower_bound_constraint, + ) + m.cost_fixed_eos_capacity_upper_bound_constraint = Constraint( + m.cost_fixed_eos_rptn, + rule=cost_fixed_eos.cost_fixed_eos_capacity_upper_bound_constraint, + ) + + # -- cost_variable_eos --- + m.cost_variable_eos_segments = {} + + m.cost_variable_eos_rptn = Set( + within=model.regional_global_indices * model.time_optimize * model.tech_or_group * Integers + ) + m.cost_variable_eos = Param(m.cost_variable_eos_rptn, domain=Any) + + m.cost_variable_eos_period_rpt = Set(dimen=3, initialize=cost_variable_eos.period_cost_indices) + + m.initialize_cost_variable_eos = BuildAction(rule=cost_variable_eos.initialize_components) + + m.v_cost_variable_eos_activity = Var( + m.cost_variable_eos_rptn, domain=NonNegativeReals, initialize=0 + ) + m.v_cost_variable_eos_segment_binary = Var( + m.cost_variable_eos_rptn, domain=Binary, initialize=0 + ) + + m.cost_variable_eos_segment_binary_constraint = Constraint( + m.cost_variable_eos_period_rpt, + rule=cost_variable_eos.cost_variable_eos_segment_binary_constraint, + ) + m.cost_variable_eos_activity_constraint = Constraint( + m.cost_variable_eos_period_rpt, + rule=cost_variable_eos.cost_variable_eos_activity_constraint, + ) + m.cost_variable_eos_activity_lower_bound_constraint = Constraint( + m.cost_variable_eos_rptn, + rule=cost_variable_eos.cost_variable_eos_activity_lower_bound_constraint, + ) + m.cost_variable_eos_activity_upper_bound_constraint = Constraint( + m.cost_variable_eos_rptn, + rule=cost_variable_eos.cost_variable_eos_activity_upper_bound_constraint, + ) + + m.append_cost_variable_eos_to_total_cost = BuildAction(rule=cost_variable_eos.total_cost) diff --git a/temoa/extensions/economies_of_scale/data_manifest.py b/temoa/extensions/economies_of_scale/data_manifest.py new file mode 100644 index 00000000..a74bd11d --- /dev/null +++ b/temoa/extensions/economies_of_scale/data_manifest.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, cast + +from temoa.data_io.loader_manifest import LoadItem + +if TYPE_CHECKING: + from temoa.core.model import TemoaModel + from temoa.extensions.economies_of_scale.core.model import EOSModel + + +def build_manifest_items(model: TemoaModel) -> list[LoadItem]: + m = cast('EOSModel', model) + return [ + LoadItem( + component=m.cost_invest_eos, + table='cost_invest_eos', + columns=[ + 'region', + 'tech_or_group', + 'segment', + 'capacity_lower', + 'capacity_upper', + 'cost_lower', + 'cost_upper', + ], + index_length=3, + validator_name='viable_rt', + validation_map=(0, 1), + is_table_required=False, + is_period_filtered=False, + index_set=m.cost_invest_eos_rtn, + ), + LoadItem( + component=m.cost_fixed_eos, + table='cost_fixed_eos', + columns=[ + 'region', + 'period', + 'tech_or_group', + 'segment', + 'capacity_lower', + 'capacity_upper', + 'cost_lower', + 'cost_upper', + ], + index_length=4, + validator_name='viable_rpt', + validation_map=(0, 1, 2), + is_table_required=False, + is_period_filtered=True, + index_set=m.cost_fixed_eos_rptn, + ), + LoadItem( + component=m.cost_variable_eos, + table='cost_variable_eos', + columns=[ + 'region', + 'period', + 'tech_or_group', + 'segment', + 'activity_lower', + 'activity_upper', + 'cost_lower', + 'cost_upper', + ], + index_length=4, + validator_name='viable_rpt', + validation_map=(0, 1, 2), + is_table_required=False, + is_period_filtered=True, + index_set=m.cost_variable_eos_rptn, + ), + ] diff --git a/temoa/extensions/economies_of_scale/extension.py b/temoa/extensions/economies_of_scale/extension.py new file mode 100644 index 00000000..c0b5d8cf --- /dev/null +++ b/temoa/extensions/economies_of_scale/extension.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from pathlib import Path + +from temoa.extensions.economies_of_scale.core.model import register_model_components +from temoa.extensions.economies_of_scale.data_manifest import build_manifest_items +from temoa.extensions.framework import ExtensionSpec + +EOS_EXTENSION = ExtensionSpec( + extension_id='eos', + owned_tables=('cost_invest_eos', 'cost_fixed_eos', 'cost_variable_eos'), + regional_group_tables={ + 'cost_invest_eos': 'region', + 'cost_fixed_eos': 'region', + 'cost_variable_eos': 'region', + }, + register_model_components=register_model_components, + build_manifest_items=build_manifest_items, + schema_sql_path=str(Path(__file__).parent / 'tables.sql'), + fail_if_tables_populated_when_disabled=True, +) diff --git a/temoa/extensions/economies_of_scale/tables.sql b/temoa/extensions/economies_of_scale/tables.sql new file mode 100644 index 00000000..2efd7fa1 --- /dev/null +++ b/temoa/extensions/economies_of_scale/tables.sql @@ -0,0 +1,53 @@ +CREATE TABLE IF NOT EXISTS cost_invest_eos +( + region TEXT NOT NULL, + tech_or_group TEXT NOT NULL, + segment INTEGER NOT NULL, + capacity_lower REAL NOT NULL, + capacity_upper REAL NOT NULL, + cost_lower REAL NOT NULL, + cost_upper REAL NOT NULL, + units TEXT, + notes TEXT, + CHECK (capacity_lower >= 0), + CHECK (capacity_upper >= 0), + CHECK (cost_lower >= 0), + CHECK (cost_upper >= 0), + PRIMARY KEY (region, tech_or_group, segment) +); +CREATE TABLE IF NOT EXISTS cost_fixed_eos +( + region TEXT NOT NULL, + period INTEGER NOT NULL, + tech_or_group TEXT NOT NULL, + segment INTEGER NOT NULL, + capacity_lower REAL NOT NULL, + capacity_upper REAL NOT NULL, + cost_lower REAL NOT NULL, + cost_upper REAL NOT NULL, + units TEXT, + notes TEXT, + CHECK (capacity_lower >= 0), + CHECK (capacity_upper >= 0), + CHECK (cost_lower >= 0), + CHECK (cost_upper >= 0), + PRIMARY KEY (region, period, tech_or_group, segment) +); +CREATE TABLE IF NOT EXISTS cost_variable_eos +( + region TEXT NOT NULL, + period INTEGER NOT NULL, + tech_or_group TEXT NOT NULL, + segment INTEGER NOT NULL, + activity_lower REAL NOT NULL, + activity_upper REAL NOT NULL, + cost_lower REAL NOT NULL, + cost_upper REAL NOT NULL, + units TEXT, + notes TEXT, + CHECK (activity_lower >= 0), + CHECK (activity_upper >= 0), + CHECK (cost_lower >= 0), + CHECK (cost_upper >= 0), + PRIMARY KEY (region, period, tech_or_group, segment) +); diff --git a/temoa/extensions/framework.py b/temoa/extensions/framework.py index 179ddf09..7927e3bf 100644 --- a/temoa/extensions/framework.py +++ b/temoa/extensions/framework.py @@ -57,9 +57,10 @@ def normalize_extension_ids(extension_ids: Sequence[str | object] | None) -> tup def get_known_extension_specs() -> dict[str, ExtensionSpec]: """Return all extension specs known to this installation.""" from temoa.extensions.discrete_capacity.extension import DISCRETE_CAPACITY_EXTENSION + from temoa.extensions.economies_of_scale.extension import EOS_EXTENSION from temoa.extensions.growth_rates.extension import GROWTH_RATES_EXTENSION - specs = [GROWTH_RATES_EXTENSION, DISCRETE_CAPACITY_EXTENSION] + specs = [GROWTH_RATES_EXTENSION, DISCRETE_CAPACITY_EXTENSION, EOS_EXTENSION] return {spec.extension_id: spec for spec in specs} From de1086b4710ddf94a4c0e2f4795d897e75257f86 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Wed, 8 Jul 2026 14:44:44 -0400 Subject: [PATCH 24/28] Enable output cost results for economies of scale extension Signed-off-by: Davey Elder --- temoa/__init__.py | 4 +- temoa/_internal/exchange_tech_cost_ledger.py | 6 +- temoa/_internal/table_data_puller.py | 130 ++---------- temoa/components/costs.py | 105 +++++++++- temoa/db_schema/temoa_schema_v4.sql | 5 +- .../economies_of_scale/core/data_puller.py | 187 ++++++++++++++++++ tests/test_table_writer.py | 12 +- 7 files changed, 325 insertions(+), 124 deletions(-) create mode 100644 temoa/extensions/economies_of_scale/core/data_puller.py diff --git a/temoa/__init__.py b/temoa/__init__.py index 67ef283b..75a8d241 100644 --- a/temoa/__init__.py +++ b/temoa/__init__.py @@ -19,7 +19,6 @@ solve_instance, ) from temoa._internal.table_data_puller import ( - loan_costs, poll_capacity_results, poll_cost_results, poll_emissions, @@ -27,6 +26,7 @@ ) from temoa._internal.table_writer import TableWriter from temoa._internal.temoa_sequencer import TemoaSequencer +from temoa.components.costs import poll_loan_costs from temoa.core.config import TemoaConfig from temoa.core.model import TemoaModel from temoa.core.modes import TemoaMode @@ -48,7 +48,7 @@ 'solve_instance', 'handle_results', 'save_lp', - 'loan_costs', + 'poll_loan_costs', 'poll_capacity_results', 'poll_emissions', 'poll_flow_results', diff --git a/temoa/_internal/exchange_tech_cost_ledger.py b/temoa/_internal/exchange_tech_cost_ledger.py index 12b170f5..361ad8dd 100644 --- a/temoa/_internal/exchange_tech_cost_ledger.py +++ b/temoa/_internal/exchange_tech_cost_ledger.py @@ -57,7 +57,11 @@ def add_cost_record( if not r1 and r2: raise ValueError(f'problem splitting region-region: {link}') # add to the "seen" records for appropriate cost type - self.cost_records[cost_type][r1, r2, tech, vintage, period] = cost + key = (r1, r2, tech, vintage, period) + if key in self.cost_records[cost_type]: + self.cost_records[cost_type][key] += cost + else: + self.cost_records[cost_type][key] = cost def get_use_ratio( self, exporter: Region, importer: Region, period: Period, tech: Technology, vintage: Vintage diff --git a/temoa/_internal/table_data_puller.py b/temoa/_internal/table_data_puller.py index 832d3557..3973832a 100644 --- a/temoa/_internal/table_data_puller.py +++ b/temoa/_internal/table_data_puller.py @@ -20,6 +20,7 @@ from temoa._internal.exchange_tech_cost_ledger import CostType, ExchangeTechCostLedger from temoa.components import costs from temoa.components.utils import get_variable_efficiency +from temoa.extensions.economies_of_scale.core import data_puller as eos from temoa.types.model_types import EI, FI, SLI, CapData, FlowType if TYPE_CHECKING: @@ -340,30 +341,30 @@ def poll_cost_results( cap = value(model.v_new_capacity[r, t, v]) if abs(cap) < epsilon: continue + cost_invest = value(model.cost_invest[r, t, v]) loan_life = value(loan_lifetime_process[r, t, v]) - loan_rate = value(model.loan_rate[r, t, v]) + loan_annualize = value(model.loan_annualize[r, t, v]) if model.is_survival_curve_process[r, t, v]: - model_loan_cost, undiscounted_cost = loan_costs_survival_curve( + discounted_cost, undiscounted_cost = costs.poll_loan_costs_survival_curve( model=model, r=r, t=t, v=v, - loan_rate=loan_rate, loan_life=loan_life, + loan_annualize=loan_annualize, capacity=cap, - invest_cost=value(model.cost_invest[r, t, v]), + invest_cost=cost_invest, p_0=p_0_true, p_e=p_e, global_discount_rate=global_discount_rate, - vintage=v, ) else: - model_loan_cost, undiscounted_cost = loan_costs( - loan_rate=loan_rate, + discounted_cost, undiscounted_cost = costs.poll_loan_costs( loan_life=loan_life, + loan_annualize=loan_annualize, capacity=cap, - invest_cost=value(model.cost_invest[r, t, v]), + invest_cost=cost_invest, process_life=value(model.lifetime_process[r, t, v]), p_0=p_0_true, p_e=p_e, @@ -377,7 +378,7 @@ def poll_cost_results( period=v, tech=t, vintage=v, - cost=model_loan_cost, + cost=discounted_cost, cost_type=CostType.D_INVEST, ) exchange_costs.add_cost_record( @@ -392,7 +393,7 @@ def poll_cost_results( # The period `p` for an investment cost is its vintage `v`. key = (cast('Region', r), cast('Period', v), cast('Technology', t), cast('Vintage', v)) entries[key].update( - {CostType.D_INVEST: model_loan_cost, CostType.INVEST: undiscounted_cost} + {CostType.D_INVEST: discounted_cost, CostType.INVEST: undiscounted_cost} ) for r, p, t, v in model.cost_fixed.sparse_keys(): @@ -489,107 +490,18 @@ def poll_cost_results( CostType.VARIABLE: float(value(undiscounted_var_cost)), } ) - exchange_entries = exchange_costs.get_entries() - return entries, exchange_entries - -def loan_costs( - loan_rate: float, # this is referred to as loan_rate in parameters - loan_life: float, - capacity: float, - invest_cost: float, - process_life: int, - p_0: int, - p_e: int, - global_discount_rate: float, - vintage: int, - **kwargs: object, -) -> tuple[float, float]: - """ - Calculate Loan costs by calling the loan annualize and loan cost functions in temoa_rules - :return: tuple of [model-view discounted cost, un-discounted annuity] - """ - # dev note: this is a passthrough function. Sole intent is to use the EXACT formula the - # model uses for these costs - loan_ar = costs.pv_to_annuity(rate=loan_rate, periods=int(loan_life)) - model_ic = costs.loan_cost( - capacity, - invest_cost, - loan_annualize=float(value(loan_ar)), - lifetime_loan_process=loan_life, - lifetime_process=process_life, - p_0=p_0, - p_e=p_e, - global_discount_rate=global_discount_rate, - vintage=vintage, - ) - # Override the GDR to get the undiscounted value - global_discount_rate = 0 - undiscounted_cost = costs.loan_cost( - capacity, - invest_cost, - loan_annualize=float(value(loan_ar)), - lifetime_loan_process=loan_life, - lifetime_process=process_life, - p_0=p_0, - p_e=p_e, - global_discount_rate=global_discount_rate, - vintage=vintage, + # Get nonlinear costs from the EOS extension, if active + eos.poll_costs( + model=model, + exchange_costs=exchange_costs, + entries=entries, + p_0=p_0_true, + epsilon=epsilon, ) - return float(value(model_ic)), float(value(undiscounted_cost)) - - -def loan_costs_survival_curve( - model: TemoaModel, - r: Region, - t: Technology, - v: Vintage, - loan_rate: float, # this is referred to as loan_rate in parameters - loan_life: float, - capacity: float, - invest_cost: float, - p_0: Period, - p_e: Period, - global_discount_rate: float, - vintage: Vintage, - **kwargs: object, -) -> tuple[float, float]: - """ - Calculate Loan costs by calling the loan annualize and loan cost functions in temoa_rules - :return: tuple of [model-view discounted cost, un-discounted annuity] - """ - # dev note: this is a passthrough function. Sole intent is to use the EXACT formula the - # model uses for these costs - loan_ar = costs.pv_to_annuity(rate=loan_rate, periods=int(loan_life)) - model_ic = costs.loan_cost_survival_curve( - model, - r, - t, - v, - capacity, - invest_cost, - loan_annualize=float(value(loan_ar)), - lifetime_loan_process=loan_life, - p_0=p_0, - p_e=p_e, - global_discount_rate=global_discount_rate, - ) - # Override the GDR to get the undiscounted value - global_discount_rate = 0 - undiscounted_cost = costs.loan_cost_survival_curve( - model, - r, - t, - v, - capacity, - invest_cost, - loan_annualize=float(value(loan_ar)), - lifetime_loan_process=loan_life, - p_0=p_0, - p_e=p_e, - global_discount_rate=global_discount_rate, - ) - return float(value(model_ic)), float(value(undiscounted_cost)) + + exchange_entries = exchange_costs.get_entries() + return entries, exchange_entries def poll_emissions( diff --git a/temoa/components/costs.py b/temoa/components/costs.py index 7814c2c4..9edaf1a2 100644 --- a/temoa/components/costs.py +++ b/temoa/components/costs.py @@ -124,7 +124,7 @@ def lifetime_loan_process_indices(model: TemoaModel) -> set[tuple[Region, Techno def loan_cost( capacity: float | Var | ComponentData, - invest_cost: float, + invest_cost: float | Expression, loan_annualize: float, lifetime_loan_process: float | int, lifetime_process: float, @@ -190,7 +190,7 @@ def loan_cost_survival_curve( t: Technology, v: Vintage, capacity: float | Var | ComponentData, - invest_cost: float, + invest_cost: float | Expression, loan_annualize: float, lifetime_loan_process: float | int, p_0: Period, @@ -266,7 +266,7 @@ def loan_cost_survival_curve( def fixed_or_variable_cost( cap_or_flow: float | Var | ComponentData, - cost_factor: float, + cost_factor: float | Expression, cost_years: float | ComponentData, global_discount_rate: float | None, p_0: float, @@ -715,3 +715,102 @@ def param_loan_annualize_rule( lln = value(model.loan_lifetime_process[r, t, v]) annualized_rate = pv_to_annuity(dr, lln) return annualized_rate + + +# ============================================================================ +# DATA PULLING UTILITIES +# ============================================================================ + + +def poll_loan_costs( + loan_life: float, + loan_annualize: float, + capacity: float, + invest_cost: float, + process_life: int, + p_0: int, + p_e: int, + global_discount_rate: float, + vintage: int, + **kwargs: object, +) -> tuple[float, float]: + """ + Calculate Loan costs by calling the loan annualize and loan cost functions in temoa_rules + :return: tuple of [model-view discounted cost, un-discounted annuity] + """ + # dev note: this is a passthrough function. Sole intent is to use the EXACT formula the + # model uses for these costs + discounted_cost = loan_cost( + capacity, + invest_cost, + loan_annualize=loan_annualize, + lifetime_loan_process=loan_life, + lifetime_process=process_life, + p_0=p_0, + p_e=p_e, + global_discount_rate=global_discount_rate, + vintage=vintage, + ) + # Override the GDR to get the undiscounted value + undiscounted_cost = loan_cost( + capacity, + invest_cost, + loan_annualize=loan_annualize, + lifetime_loan_process=loan_life, + lifetime_process=process_life, + p_0=p_0, + p_e=p_e, + global_discount_rate=0, + vintage=vintage, + ) + return float(value(discounted_cost)), float(value(undiscounted_cost)) + + +def poll_loan_costs_survival_curve( + model: TemoaModel, + r: Region, + t: Technology, + v: Vintage, + loan_life: float, + loan_annualize: float, + capacity: float, + invest_cost: float, + p_0: Period, + p_e: Period, + global_discount_rate: float, + **kwargs: object, +) -> tuple[float, float]: + """ + Calculate Loan costs by calling the loan annualize and loan cost functions in temoa_rules + :return: tuple of [model-view discounted cost, un-discounted annuity] + """ + # dev note: this is a passthrough function. Sole intent is to use the EXACT formula the + # model uses for these costs + discounted_cost = loan_cost_survival_curve( + model, + r, + t, + v, + capacity, + invest_cost, + loan_annualize, + lifetime_loan_process=loan_life, + p_0=p_0, + p_e=p_e, + global_discount_rate=global_discount_rate, + ) + # Override the GDR to get the undiscounted value + undiscounted_cost = loan_cost_survival_curve( + model, + r, + t, + v, + capacity, + invest_cost, + loan_annualize, + lifetime_loan_process=loan_life, + p_0=p_0, + p_e=p_e, + global_discount_rate=0, + ) + return float(value(discounted_cost)), float(value(undiscounted_cost)) diff --git a/temoa/db_schema/temoa_schema_v4.sql b/temoa/db_schema/temoa_schema_v4.sql index 69eef34a..c97f741b 100644 --- a/temoa/db_schema/temoa_schema_v4.sql +++ b/temoa/db_schema/temoa_schema_v4.sql @@ -922,7 +922,7 @@ CREATE TABLE IF NOT EXISTS output_cost region TEXT, sector TEXT REFERENCES sector_label (sector), period INTEGER REFERENCES time_period (period), - tech TEXT REFERENCES technology (tech), + tech TEXT, vintage INTEGER REFERENCES time_period (period), d_invest REAL, d_fixed REAL, @@ -934,8 +934,7 @@ CREATE TABLE IF NOT EXISTS output_cost emiss REAL, units TEXT, PRIMARY KEY (scenario, region, period, tech, vintage), - FOREIGN KEY (vintage) REFERENCES time_period (period), - FOREIGN KEY (tech) REFERENCES technology (tech) + FOREIGN KEY (vintage) REFERENCES time_period (period) ); CREATE TABLE IF NOT EXISTS time_season diff --git a/temoa/extensions/economies_of_scale/core/data_puller.py b/temoa/extensions/economies_of_scale/core/data_puller.py new file mode 100644 index 00000000..2713483e --- /dev/null +++ b/temoa/extensions/economies_of_scale/core/data_puller.py @@ -0,0 +1,187 @@ +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, cast + +from pyomo.environ import value + +from temoa._internal.exchange_tech_cost_ledger import CostType, ExchangeTechCostLedger +from temoa.components import costs +from temoa.extensions.economies_of_scale.components import ( + cost_fixed_eos, + cost_invest_eos, + cost_variable_eos, +) + +if TYPE_CHECKING: + from temoa.core.model import TemoaModel + from temoa.extensions.economies_of_scale.core.model import EOSModel + from temoa.types import Period, Region, Technology, Vintage + +logger = logging.getLogger(__name__) + + +# --- Poll cost results --- +def poll_costs( + model: TemoaModel, + exchange_costs: ExchangeTechCostLedger, + entries: dict[tuple[Region, Period, Technology, Vintage], dict[CostType, float]], + p_0: Period, + epsilon: float, +) -> None: + """ + Poll the fixed and variable costs for all EOS clusters in the planning horizon + and add them to the cost entries for the model. + """ + if 'eos' not in model.enabled_extensions: + return + model = cast('EOSModel', model) + + global_discount_rate = value(model.global_discount_rate) + + for r, p, t in model.cost_invest_eos_period_rpt: + cost = value(cost_invest_eos.period_cost(model, r, p, t)) + if cost < epsilon: + continue + + # gather details... + r0, t0 = model.cost_invest_eos_reference_process[r, p, t] + loan_life = value(model.loan_lifetime_process[r0, t0, p]) + loan_annualize = value(model.loan_annualize[r0, t0, p]) + life = value(model.lifetime_process[r0, t0, p]) + + if model.is_survival_curve_process[r0, t0, p]: + discounted_cost, undiscounted_cost = costs.poll_loan_costs_survival_curve( + model=model, + r=r0, + t=t0, + v=p, + loan_life=loan_life, + loan_annualize=loan_annualize, + capacity=1, + invest_cost=cost, + p_0=p_0, + p_e=model.time_future.last(), + global_discount_rate=global_discount_rate, + ) + else: + discounted_cost, undiscounted_cost = costs.poll_loan_costs( + loan_life=loan_life, + loan_annualize=loan_annualize, + capacity=1, + invest_cost=cost, + process_life=life, + p_0=p_0, + p_e=model.time_future.last(), + global_discount_rate=global_discount_rate, + vintage=p, + ) + + # screen for linked region... + if '-' in r: + exchange_costs.add_cost_record( + r, + period=p, + tech=t, + vintage=p, + cost=discounted_cost, + cost_type=CostType.D_INVEST, + ) + exchange_costs.add_cost_record( + r, + period=p, + tech=t, + vintage=p, + cost=undiscounted_cost, + cost_type=CostType.INVEST, + ) + else: + # The period `p` for an investment cost is its vintage `v`. + key = (cast('Region', r), cast('Period', p), cast('Technology', t), cast('Vintage', p)) + if CostType.INVEST in entries.get(key, {}): + entries[key][CostType.D_INVEST] += discounted_cost + entries[key][CostType.INVEST] += undiscounted_cost + else: + entries[key].update( + {CostType.D_INVEST: discounted_cost, CostType.INVEST: undiscounted_cost} + ) + + for r, p, t in model.cost_fixed_eos_period_rpt: + fixed_cost = value(cost_fixed_eos.period_cost(model, r, p, t)) + if fixed_cost < epsilon: + continue + + undiscounted_fixed_cost = fixed_cost * value(model.period_length[p]) + discounted_fixed_cost = costs.fixed_or_variable_cost( + 1, + fixed_cost, + value(model.period_length[p]), + global_discount_rate=global_discount_rate, + p_0=p_0, + p=p, + ) + + if '-' in r: + exchange_costs.add_cost_record( + r, + period=p, + tech=t, + vintage=p, + cost=float(value(discounted_fixed_cost)), + cost_type=CostType.D_FIXED, + ) + exchange_costs.add_cost_record( + r, + period=p, + tech=t, + vintage=p, + cost=float(undiscounted_fixed_cost), + cost_type=CostType.FIXED, + ) + else: + entries[r, p, t, p].update( + { + CostType.D_FIXED: float(value(discounted_fixed_cost)), + CostType.FIXED: float(undiscounted_fixed_cost), + } + ) + + for r, p, t in model.cost_fixed_eos_period_rpt: + variable_cost = value(cost_variable_eos.period_cost(model, r, p, t)) + if variable_cost < epsilon: + continue + + undiscounted_variable_cost = variable_cost * value(model.period_length[p]) + discounted_variable_cost = costs.fixed_or_variable_cost( + 1, + variable_cost, + value(model.period_length[p]), + global_discount_rate=global_discount_rate, + p_0=p_0, + p=p, + ) + + if '-' in r: + exchange_costs.add_cost_record( + r, + period=p, + tech=t, + vintage=p, + cost=float(value(discounted_variable_cost)), + cost_type=CostType.D_VARIABLE, + ) + exchange_costs.add_cost_record( + r, + period=p, + tech=t, + vintage=p, + cost=float(undiscounted_variable_cost), + cost_type=CostType.VARIABLE, + ) + else: + entries[r, p, t, p].update( + { + CostType.D_VARIABLE: float(value(discounted_variable_cost)), + CostType.VARIABLE: float(undiscounted_variable_cost), + } + ) diff --git a/tests/test_table_writer.py b/tests/test_table_writer.py index 3f30d2e1..646c0a47 100644 --- a/tests/test_table_writer.py +++ b/tests/test_table_writer.py @@ -2,7 +2,7 @@ import pytest -from temoa._internal.table_data_puller import loan_costs +from temoa.components.costs import poll_loan_costs class LoanCostInput(TypedDict): @@ -31,7 +31,7 @@ class LoanCostTestCase(TypedDict): 'capacity': 100_000.0, # units 'invest_cost': 1.0, # $/unit of capacity 'loan_life': 40.0, - 'loan_rate': 0.10, + 'loan_annualize': 0.102259414, 'global_discount_rate': 0.000000000001, 'process_life': 40, 'p_0': 2020, # the "myopic base year" to which all prices are discounted @@ -47,7 +47,7 @@ class LoanCostTestCase(TypedDict): 'capacity': 100_000.0, 'invest_cost': 1.0, 'loan_life': 40.0, - 'loan_rate': 0.08, + 'loan_annualize': 0.083860162, 'global_discount_rate': 0.05, 'process_life': 50, 'p_0': 2020, @@ -65,7 +65,7 @@ class LoanCostTestCase(TypedDict): 'capacity': 100_000.0, # units 'invest_cost': 1.0, # $/unit of capacity 'loan_life': 40.0, - 'loan_rate': 0.10, + 'loan_annualize': 0.102259414, 'global_discount_rate': 0, 'process_life': 40, 'p_0': 2020, # the "myopic base year" to which all prices are discounted @@ -84,7 +84,7 @@ def test_loan_costs(test_case: LoanCostTestCase) -> None: Test the loan cost calculations """ # we will test with a 1% error to accommodate the approximation of GDR=0 - model_cost, undiscounted_cost = loan_costs(**test_case['input']) + model_cost, undiscounted_cost = poll_loan_costs(**test_case['input']) assert model_cost == pytest.approx(test_case['expected_model_cost'], rel=0.01) assert undiscounted_cost == pytest.approx(test_case['expected_undiscounted_cost'], rel=0.01) @@ -99,6 +99,6 @@ def test_loan_costs_with_zero_gdr(test_case: LoanCostTestCase) -> None: Test the formula with zero for GDR to make sure it is handled correctly. The formula risks division by zero if this is not correct. """ - model_cost, undiscounted_cost = loan_costs(**test_case['input']) + model_cost, undiscounted_cost = poll_loan_costs(**test_case['input']) assert model_cost == pytest.approx(test_case['expected_model_cost'], abs=0.01) assert undiscounted_cost == pytest.approx(test_case['expected_undiscounted_cost'], abs=0.01) From b311769cdbc61c9076eadc3689c38ae16e16b607 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Wed, 8 Jul 2026 14:44:57 -0400 Subject: [PATCH 25/28] Create documentation for economies of scale extension Signed-off-by: Davey Elder --- docs/source/extensions.rst | 1 + docs/source/extensions/eos.rst | 306 +++++++++++ docs/source/images/eos_cost_curve.png | Bin 0 -> 75291 bytes docs/source/images/eos_cost_curve.svg | 702 ++++++++++++++++++++++++++ docs/source/images/etl_cost_curve.png | Bin 0 -> 75182 bytes docs/source/images/etl_cost_curve.svg | 683 +++++++++++++++++++++++++ scripts/generate_etl_figure.py | 223 ++++++++ 7 files changed, 1915 insertions(+) create mode 100644 docs/source/extensions/eos.rst create mode 100644 docs/source/images/eos_cost_curve.png create mode 100644 docs/source/images/eos_cost_curve.svg create mode 100644 docs/source/images/etl_cost_curve.png create mode 100644 docs/source/images/etl_cost_curve.svg create mode 100644 scripts/generate_etl_figure.py diff --git a/docs/source/extensions.rst b/docs/source/extensions.rst index 06cc597c..47c0fd7d 100644 --- a/docs/source/extensions.rst +++ b/docs/source/extensions.rst @@ -245,3 +245,4 @@ as new extensions are added. extensions/growth_rates extensions/discrete_capacity + extensions/eos diff --git a/docs/source/extensions/eos.rst b/docs/source/extensions/eos.rst new file mode 100644 index 00000000..d71d34e5 --- /dev/null +++ b/docs/source/extensions/eos.rst @@ -0,0 +1,306 @@ +.. _extension-eos: + +Economies of Scale (EOS) +======================== + +The **eos** extension adds piecewise-linear cost curves to three cost +types: investment (:code:`cost_invest_eos`), fixed O&M +(:code:`cost_fixed_eos`), and variable O&M (:code:`cost_variable_eos`). +It is disabled by default and enabled per run through configuration: + +.. code-block:: toml + + extensions = ["eos"] + +All three features share the same binary-integer formulation: a set of +contiguous segments, exactly one of which is active per cluster per period. +They differ in what quantity drives the cost curve (cumulative capacity, +available capacity, or activity) and in how the resulting cost is +discounted and added to the objective. + +.. figure:: ../images/eos_cost_curve.png + :align: center + :width: 90% + :alt: EOS piecewise-linear cost curve + + **EOS piecewise-linear cost curve.** The curve is composed of contiguous + segments :math:`n=0,1,2` (three shown here), each with its own quantity + bounds :math:`[\underline{Q}_{n}, \overline{Q}_{n}]` and corresponding + total-cost bounds :math:`[\underline{C}_{n}, \overline{C}_{n}]`. The + slope :math:`m_n` is the marginal cost within segment :math:`n`. In this + period, segment :math:`n=1` is **active** (red shading): the binary + variable is 1, and the quantity variable :math:`\textbf{Q}_p` lies within + its bounds. + +Overview +-------- + +.. list-table:: + :header-rows: 1 + :widths: 22 20 20 38 + + * - Feature + - Table + - Cost type + - Curve driven by + * - :ref:`cost_invest_eos ` + - :code:`cost_invest_eos` + - Investment (:code:`cost_invest`) + - **Cumulative capacity** built up to period :math:`p` (learning curve) + * - :ref:`cost_fixed_eos ` + - :code:`cost_fixed_eos` + - Fixed O&M (:code:`cost_fixed`) + - **Available capacity** in period :math:`p` + * - :ref:`cost_variable_eos ` + - :code:`cost_variable_eos` + - Variable O&M (:code:`cost_variable`) + - **Total activity** (output flow) in period :math:`p` + +Technologies subject to EOS are specified as *clusters* identified by a +region/group label :math:`r` and a technology/group label :math:`t`. +Each cluster has a piecewise-linear cost curve defined by contiguous +segments :math:`n \in N_{r,t}` (for invest) or +:math:`n \in N_{r,p,t}` (for fixed/variable, which are +period-specific). + +.. note:: + + The :code:`tech_or_group` column accepts either an individual technology + name or a technology-group name. When a group is used, all member + technologies share the same cost curve and their contributions to the + driving quantity (capacity or activity) are summed. For + :code:`cost_invest_eos`, all technologies in a cluster must share the + same :code:`lifetime_process`, :code:`lifetime_survival_curve`, + :code:`loan_lifetime_process`, and :code:`loan_rate` in every period + they appear together. + +.. _eos-invest: + +cost_invest_eos — Investment Cost Curve +---------------------------------------- + +Concept +~~~~~~~ + +:code:`cost_invest_eos` replaces (or supplements) the flat per-unit +investment cost for a cluster with a piecewise-linear curve whose marginal +cost changes with *cumulative* installed capacity — a learning-curve +effect. The curve is global across time: each successive period inherits +the capacity built in all previous periods. The incremental investment cost +charged in period :math:`p` is the difference between the curve evaluated at +cumulative capacity up to :math:`p` and the curve evaluated at the end of +the previous period, so that each unit of new capacity is charged at the +marginal cost appropriate to the total deployment level at the time of +construction. + +.. note:: + + The cumulative capacity **includes pre-existing capacity** (from the + :code:`existing_capacity` table), but pre-existing capacity does not + incur an investment cost — it is subtracted out when computing the + incremental period cost. The first segment's lower bound can almost + always be set to 0 regardless of how much capacity already exists. + +.. note:: + + EOS invest processes can carry a flat investment cost via + :code:`cost_invest` in addition to the piecewise curve. + +Table: cost_invest_eos +~~~~~~~~~~~~~~~~~~~~~~ + +:math:`{CIEOS}_{r \in R,\, t \in T,\, n \in \mathbb{Z}^+}` + +The segment index :math:`n` is an integer; segments must be numbered +consecutively and their capacity bounds must be contiguous. All four bound +columns must be non-negative and strictly increasing within a segment. + +.. csv-table:: + :header: "Column", "Symbol", "Description" + :widths: 22, 18, 60 + + ":code:`region`", ":math:`r`", "region or region-group label" + ":code:`tech_or_group`", ":math:`t`", "technology or technology-group label" + ":code:`segment`", ":math:`n`", "integer segment index (contiguous)" + ":code:`capacity_lower`", ":math:`\underline{K}_{r,t,n}`", "lower cumulative-capacity bound of the segment" + ":code:`capacity_upper`", ":math:`\overline{K}_{r,t,n}`", "upper cumulative-capacity bound of the segment" + ":code:`cost_lower`", ":math:`\underline{C}_{r,t,n}`", "investment cost at :math:`\underline{K}_{r,t,n}` (same units as :code:`cost_invest`)" + ":code:`cost_upper`", ":math:`\overline{C}_{r,t,n}`", "investment cost at :math:`\overline{K}_{r,t,n}`" + +Sets +~~~~ + +.. csv-table:: + :header: "Set", "Indices", "Description" + :widths: 38, 18, 44 + + ":code:`cost_invest_eos_rtn`", ":math:`(r, t, n)`", "valid cluster–segment combinations; populated directly from the table" + ":code:`cost_invest_eos_segment_rptn`", ":math:`(r, p, t, n)`", "valid cluster–period–segment combinations; derived from :code:`cost_invest_eos_rtn` and :code:`process_vintages`" + ":code:`cost_invest_eos_period_rpt`", ":math:`(r, p, t)`", "projection of :code:`cost_invest_eos_segment_rptn` onto :math:`(r, p, t)`" + +Variables +~~~~~~~~~ + +.. csv-table:: + :header: "Variable", "Domain", "Indices", "Description" + :widths: 36, 12, 24, 28 + + ":math:`\textbf{CIECAP}_{r,p,t,n}` (:code:`v_cost_invest_eos_cumulative_capacity`)", ":math:`\mathbb{R}_{\ge 0}`", ":math:`(r, p, t, n) \in \Theta_{\text{cost\_invest\_eos\_segment\_rptn}}`", "cumulative capacity assigned to segment :math:`n` of cluster :math:`(r,t)` in period :math:`p`" + ":math:`\textbf{CIEB}_{r,p,t,n}` (:code:`v_cost_invest_eos_segment_binary`)", ":math:`\{0, 1\}`", ":math:`(r, p, t, n) \in \Theta_{\text{cost\_invest\_eos\_segment\_rptn}}`", "1 if segment :math:`n` is the active segment for cluster :math:`(r,t)` in period :math:`p`" + +Constraints +~~~~~~~~~~~ + +.. autofunction:: temoa.extensions.economies_of_scale.components.cost_invest_eos.cost_invest_eos_segment_binary_constraint + +.. autofunction:: temoa.extensions.economies_of_scale.components.cost_invest_eos.cost_invest_eos_capacity_lower_bound_constraint + +.. autofunction:: temoa.extensions.economies_of_scale.components.cost_invest_eos.cost_invest_eos_capacity_upper_bound_constraint + +.. autofunction:: temoa.extensions.economies_of_scale.components.cost_invest_eos.cost_invest_eos_cumulative_capacity_constraint + +Objective Contribution +~~~~~~~~~~~~~~~~~~~~~~ + +.. autofunction:: temoa.extensions.economies_of_scale.components.cost_invest_eos.period_cost + + +.. _eos-fixed: + +cost_fixed_eos — Fixed O&M Cost Curve +-------------------------------------- + +Concept +~~~~~~~ + +:code:`cost_fixed_eos` replaces (or supplements) the flat per-unit fixed +O&M cost for a cluster with a piecewise-linear curve whose unit cost +changes with *available capacity* in each period independently. Unlike +:code:`cost_invest_eos`, the cost curve is period-specific: segments can +differ from one period to the next, allowing the modeller to represent +O&M cost trends over time separately from capacity-scale effects. + +Table: cost_fixed_eos +~~~~~~~~~~~~~~~~~~~~~ + +:math:`{CFEOS}_{r \in R,\, p \in P,\, t \in T,\, n \in \mathbb{Z}^+}` + +.. csv-table:: + :header: "Column", "Symbol", "Description" + :widths: 22, 18, 60 + + ":code:`region`", ":math:`r`", "region or region-group label" + ":code:`period`", ":math:`p`", "model period" + ":code:`tech_or_group`", ":math:`t`", "technology or technology-group label" + ":code:`segment`", ":math:`n`", "integer segment index (contiguous)" + ":code:`capacity_lower`", ":math:`\underline{K}_{r,p,t,n}`", "lower capacity bound of the segment" + ":code:`capacity_upper`", ":math:`\overline{K}_{r,p,t,n}`", "upper capacity bound of the segment" + ":code:`cost_lower`", ":math:`\underline{C}_{r,p,t,n}`", "fixed O&M cost at :math:`\underline{K}_{r,p,t,n}` (same units as :code:`cost_fixed`)" + ":code:`cost_upper`", ":math:`\overline{C}_{r,p,t,n}`", "fixed O&M cost at :math:`\overline{K}_{r,p,t,n}`" + +Sets +~~~~ + +.. csv-table:: + :header: "Set", "Indices", "Description" + :widths: 36, 20, 44 + + ":code:`cost_fixed_eos_rptn`", ":math:`(r, p, t, n)`", "valid cluster–period–segment combinations" + ":code:`cost_fixed_eos_period_rpt`", ":math:`(r, p, t)`", "projection of :code:`cost_fixed_eos_rptn` onto :math:`(r, p, t)`" + +Variables +~~~~~~~~~ + +.. csv-table:: + :header: "Variable", "Domain", "Indices", "Description" + :widths: 36, 12, 24, 28 + + ":math:`\textbf{CFECAP}_{r,p,t,n}` (:code:`v_cost_fixed_eos_capacity`)", ":math:`\mathbb{R}_{\ge 0}`", ":math:`(r, p, t, n) \in \Theta_{\text{cost\_fixed\_eos\_rptn}}`", "capacity assigned to segment :math:`n` of cluster :math:`(r,p,t)`" + ":math:`\textbf{CFEB}_{r,p,t,n}` (:code:`v_cost_fixed_eos_segment_binary`)", ":math:`\{0, 1\}`", ":math:`(r, p, t, n) \in \Theta_{\text{cost\_fixed\_eos\_rptn}}`", "1 if segment :math:`n` is active for cluster :math:`(r,p,t)`" + +Constraints +~~~~~~~~~~~ + +.. autofunction:: temoa.extensions.economies_of_scale.components.cost_fixed_eos.cost_fixed_eos_segment_binary_constraint + +.. autofunction:: temoa.extensions.economies_of_scale.components.cost_fixed_eos.cost_fixed_eos_capacity_lower_bound_constraint + +.. autofunction:: temoa.extensions.economies_of_scale.components.cost_fixed_eos.cost_fixed_eos_capacity_upper_bound_constraint + +.. autofunction:: temoa.extensions.economies_of_scale.components.cost_fixed_eos.cost_fixed_eos_capacity_constraint + +Objective Contribution +~~~~~~~~~~~~~~~~~~~~~~ + +.. autofunction:: temoa.extensions.economies_of_scale.components.cost_fixed_eos.period_cost + + +.. _eos-variable: + +cost_variable_eos — Variable O&M Cost Curve +-------------------------------------------- + +Concept +~~~~~~~ + +:code:`cost_variable_eos` replaces (or supplements) the flat per-unit +variable O&M cost for a cluster with a piecewise-linear curve whose unit +cost changes with *total activity* (output flow) in each period +independently. Like :code:`cost_fixed_eos`, the cost curve is +period-specific. This allows the modeller to represent dispatch-scale +economies (e.g. fuel-handling efficiency at high utilisation rates) that +vary over time. + +Table: cost_variable_eos +~~~~~~~~~~~~~~~~~~~~~~~~ + +:math:`{CVEOS}_{r \in R,\, p \in P,\, t \in T,\, n \in \mathbb{Z}^+}` + +.. csv-table:: + :header: "Column", "Symbol", "Description" + :widths: 22, 18, 60 + + ":code:`region`", ":math:`r`", "region or region-group label" + ":code:`period`", ":math:`p`", "model period" + ":code:`tech_or_group`", ":math:`t`", "technology or technology-group label" + ":code:`segment`", ":math:`n`", "integer segment index (contiguous)" + ":code:`activity_lower`", ":math:`\underline{A}_{r,p,t,n}`", "lower activity bound of the segment" + ":code:`activity_upper`", ":math:`\overline{A}_{r,p,t,n}`", "upper activity bound of the segment" + ":code:`cost_lower`", ":math:`\underline{C}_{r,p,t,n}`", "variable O&M cost at :math:`\underline{A}_{r,p,t,n}` (same units as :code:`cost_variable`)" + ":code:`cost_upper`", ":math:`\overline{C}_{r,p,t,n}`", "variable O&M cost at :math:`\overline{A}_{r,p,t,n}`" + +Sets +~~~~ + +.. csv-table:: + :header: "Set", "Indices", "Description" + :widths: 36, 20, 44 + + ":code:`cost_variable_eos_rptn`", ":math:`(r, p, t, n)`", "valid cluster–period–segment combinations" + ":code:`cost_variable_eos_period_rpt`", ":math:`(r, p, t)`", "projection of :code:`cost_variable_eos_rptn` onto :math:`(r, p, t)`" + +Variables +~~~~~~~~~ + +.. csv-table:: + :header: "Variable", "Domain", "Indices", "Description" + :widths: 36, 12, 24, 28 + + ":math:`\textbf{CVEACT}_{r,p,t,n}` (:code:`v_cost_variable_eos_activity`)", ":math:`\mathbb{R}_{\ge 0}`", ":math:`(r, p, t, n) \in \Theta_{\text{cost\_variable\_eos\_rptn}}`", "activity assigned to segment :math:`n` of cluster :math:`(r,p,t)`" + ":math:`\textbf{CVEB}_{r,p,t,n}` (:code:`v_cost_variable_eos_segment_binary`)", ":math:`\{0, 1\}`", ":math:`(r, p, t, n) \in \Theta_{\text{cost\_variable\_eos\_rptn}}`", "1 if segment :math:`n` is active for cluster :math:`(r,p,t)`" + +Constraints +~~~~~~~~~~~ + +.. autofunction:: temoa.extensions.economies_of_scale.components.cost_variable_eos.cost_variable_eos_segment_binary_constraint + +.. autofunction:: temoa.extensions.economies_of_scale.components.cost_variable_eos.cost_variable_eos_activity_lower_bound_constraint + +.. autofunction:: temoa.extensions.economies_of_scale.components.cost_variable_eos.cost_variable_eos_activity_upper_bound_constraint + +.. autofunction:: temoa.extensions.economies_of_scale.components.cost_variable_eos.cost_variable_eos_activity_constraint + +Objective Contribution +~~~~~~~~~~~~~~~~~~~~~~ + +.. autofunction:: temoa.extensions.economies_of_scale.components.cost_variable_eos.period_cost diff --git a/docs/source/images/eos_cost_curve.png b/docs/source/images/eos_cost_curve.png new file mode 100644 index 0000000000000000000000000000000000000000..9772ea4e97bf69ba4ecb5d13fe6b15c2ec5e9912 GIT binary patch literal 75291 zcmce;cTiJp*f$taL_x(zK@_B^NK;U0(oqBir4x{@B1rEY0)h{sNK=~BNDT>1kPabM zP>PWnA%uWPhlG{{2qAmNXZM|*-JSXNukXw_o`H~@bMA8e+Lae3MtVm%`8gpF$Wi^< zx6B}r18@k0jd187_(^Qvr%T{Jl|Y?)f#yCgfgun4ogsz~0)0Jw0zKUxiUd3R2e|oo z%gJ25Ds$zMh-+Y=Z-9!dtk?g%LdM7cv1~?HsSkLU!@jqz10WE-6Rdyx9>WLBAp0N? z{aZIILi3gh9HD8>_|CP8%Ms+vqp>0)bu`_~-v?Fw# zo{CmVM=RA6{U+A`+`*RwmKf&lMt8kB@3nrRWkXH!!sBXstJCYWw6vq(W5MxlU#+qq z;=gmq)XD!&{CP5r|2zA3_22&uXYBr@>U|I`iDi5fHhB5ht3t1yELk6Ru2X8W!SUR} zjrpn}HrdylFDWW+5OoBV{l6q3Z@oBSS#BcA6_1Xfwa%GT-n%LdMWyRhw08){Cdt;M zsE)0SzV8ZN8T-1n5AyE;*8TkPbA(Ycpw)KD+6c_vj`Td^l4HlXH=1IpN@||AyZ?tB zn#8YA#~~c~Q_*WIopk+}pvt2fG&QHeb95kqNAkCK&BoSNsOt8gw3V4G$42L#Ol$A3 zrK+a!fR3f=wo5rP6vlDS$S~M=KPkh?!LMtsgHGb2nLr@_Jn0sPK-LdKC7|F!30|>K z(S@gb`5_Z-9Na>In69s1Tij1VW&3iKu=(|UkKr&E1UxD{2VZfs)T+io@>5lj&P$uS zcN4wFoBKNx&rBOlcP70Tj^4eB$MS-4diC_bw`Vtg@#4ioflZTl{%kO3hIhxd3!hqp z2kwV%ijWtbMsBLIZE8g-Xzs2)&G-FuSmwS8efj+g&N13BK$u31#&8vI@<`m@fAGjV z$`Y>4&AgS7dD+%(J`k%;yGLjZ0@L{oudKs3)ILMTeyqqkiom@cG!B7?Y?Mc`K}2FN z+cp1Gs?z^B0N!zh?LtafUw+VtwfC4^ijcZzce-To_DmKY+=-p0gua16-N&aKZEz-? z$ZVAi-|Nnhew?oo>XsL7w0I`)QY+?D z99NrrRj-K%0>6-M+7*5Ynu|=ylERv$5SE@^IDGi9n|E8-h6}Ey;$|FY$Ml<5jJK1f z^cK(2x@w!t7FaRW*d$dn?%(i4=Mscq!J zSGc;ww-HJa4i!b44?^G^Ezd5sxM3tu4am>m#*4^_iIq#EQ+GA-A2{ zM)i?kzic`ph_Z9Pbs{0`Tsgzat5k)EP8BGDT^R6CX| zMoT(B4}hUcO(r7|1^zJSg3#ZGZQtFG$q$>&CVkG+*pf`YP8$!fuR&=g56iiJ`uDIm zXQ4*oqIM(6IKw*pF6_fm-6nGpgzs(P*r2u?1&`LQ!-|=`Y9n`K(SJG_-_EC$w7}cQ z9e5n(&c3&rf?bQl6}A~t(8ztzaQY45x5LFtUrrj|?Jur~V(gG=!L$<`U}Rc<>|9~y z+-vbKwGW+&lL;K~xLI>Fu7tU}Vcbf~ADw&q^Y?U$W}~^W`mhuf^~W1AUGro*LAv@K zue9ZX$g|8^8He^?Xa@yLbr%|mAlEK?jhB+VIXY+ulEY)KYWb=eAYrncAz^eK?8w!c$t$d-HDx`c1^%3W~sTPV$H zT>@)*_^p%udzU0GSOv6VR!aLb1w8zJB+q~q@5XxKb$dO@DJj^YmE^O^uk9K=twZqf z!co_?o0Q4z8{^c=HubCajgPSfe}4VC7qa~!W`COY!XBL*LW}5@L|n*{f_5|P>RtQ) zoDef;h6kbU)jnJYGjMR68pr40i(`HMzM7XWUaS$~NwP9BKWGtj>Pi}I1)nt=4A#=^ z0M(=}I%8+421|8HIwRLCC{Kf8qF}@oGxAccQ&($GuDsjpLWXaj0&a_RXIk34 z?5=+d8+&9HA&R~tiuYMKtk`isE28Gbppok3mdNdO(&MB;a3LLj=`!{zVcY97H-u&1 zW2bU`I@auK(i~pyH6cw3Z+^hNRT^OpPLjSRpQPCrMitl)6}0K%?`v90N=p8`l3O0+{qrFe3-mj9eVJ`7t!5^4j?j@({SRBkDDkYE58HwDfQs<>y)5D>KSptgbOf z7?q`b1-sOujOyB#lTVXrqmSW1_|1huajQK=h)9mf_5p~NMxo^S1xkbg+9qG>MWYvf zHfp54HF34kJT9gT|Ga}hD;q-Aq@j?p(AY;AJ9phUd$5|!Wt549TdCB3DDQljeekGr zc56)p8I6O6x#hg2CXtM>b6Jj2wPOt)czE=lTZ+bxYXTl=y7QS^a~WkI(>i~yi5aL$ zX|QQ<&#>(?mS%UM)eeQ+!3K>~T1%p&hxG0YUO;zPJ9Q-+uHBm;pFwJv^Y(xbnB(uk ztaS=_-wnT9uHv*XP)hBU!!|FbnJhQu72#r0&hj#X`_VF+IVRAAGMVrU<-*Wo;duhy z%^&R%sA~j_RNrPB%2R&$j?qzkRWWhg0bFVq# z=t8|WLB)zNO>rIU=w4j9F^z*g-=pp_^Ur8T?OugjS;B244yc~W`Hf5{RSuYK#-O#@ z6Qq4KXNNIIA>Xn-QugvbauKPU#7|Ph*@xJFU@i2eYBIjbr_q5M>W*bra1c zq~HUpr?MQvk@(qy2$FsPp*LF&#%qY8_9lTR^!%cQ#y&`@ae4N;a3ywCq*an8Y>C{j zw0C9-laKSg$FJnQjAHk0UASlk6%kPM^c|LT*P19dJ+C^QC^znG-T&@^HqJ*}LERg! z0UO5_>n5qpGRlh{g4$cno$Sq5e)Dd{!4VX+M;$Zz{&5=~x#Kj^9!7c$ReW^IQuxkL zuHJ5iQ#*VZu8Z?<40ke%i?b9M@^~9C_oM5qEx;J#E#bs+A2TD3th^$|Tj*(X^O-j# zBx~O8xwn}7Bmt#Xe7SPd&IM$w_`R=p!ERCot+31vkFpNyLtZn&nwu%@@jFQa$XA!NjPoDGr=TE2uQ2tEz$XlQ|X$Z#DSv z-0!L@nKp~OU3H&wqu%5+bz3hU#EvQp6}h?Ra4LF_U&lStJZ^~LDse!EXIg_t5*EFG z2XFc7pWB0FhOn5@vfpkh_AZXxqZt`k|Z{<*ig%BmFpTjbLNL+82*JeeZ_t?f9Hl)JI zKAJ(vJ~omZlpif-b!9@zcjrp}t9OQ2opy-Ml(DWerS=x=U!I=}PpdR4!_gu2uQMAr zY7$$wXLDhq86jc1<_DfN?i8{ zwHyXp-YpZE#E6dRb7;fA`(jK98L9VY;owF&>F3?OY0W}n; zNsMNqJx0)vzMr5|@jmYgC#Jj&BR`Y3wTNz8Gg5KH)x5{lV}?`urAM zn6|u^zaO$EtPaT9^ZOG#Ct%GgBb-4wW2Hz5e}F+B@6^_FW{1LDu4$W7jtOg=T~(o| zoSJYSEzDhXn1Q~V@SEUiUcSCik~XeCY;E(( zm~M6>-Om5Jm?%?~RYlJ@4WDhc46Lo zu#RWk!a}Pp=*@vUr#KzHm0XCty(Ff|SySJpv?YMe49ci|?p&mh-|kA$+OoZ^7q94>9MfnkMQ1F2~vc1sf-N@t*lXktM9=EbR2M@v0L0R{ec7bj5DaoV{hRS=H zW}FKl1XJv>$H=oA;5`JGlIjyY?f;zDeH7lEuVK*ySf!u@@^iBv5KWn`G8^X^g}EvrM1kR4 zK}2!t7`41trC3*iz4xt6_~s4IZZ+T<#F z<)}vG94qHt4y2o`mf_qScgfLAu3xU>j(?x{FR}fQCu%DIQCaLyKa%edHk0v$b~Su+ z{*U`Gsx$%*3bYtDTTZ;si?%$Pc2+b-$t zJ#M0+Py3o~&Ju(6{oRAlY!iBK8LJBjc$sPepmk!=^pz%2n9Romszx7WXbvYsh_H|k zbQUx4xfJtgM!6jTnHxAxpUGrE&KK#JV5BU{puV^gW)_w@m-z# z_#`?FP+n~RTyW3jrUT=RuKh(Fb5(w&nWIiq%qBiQzN~g{LTg|X_QhXxA%B(wB9dax ztNpg@M@*=>Gu!2>)d`27TII;*s1Ye6APfWzo1miRiJ0gQ895gD#qJ!Gr)I-x9cxz8 z_5_Ec2nDFF`4@{di^7|6m(=y3mPn;Ul$garvF_K_yVGVtx8BZ`_S8GF8R`t0t-HUi z%s^YT6dIf|d||pjSmB}7{spbUEsO0U*v4$GPwQO898&jlYf{uQU<*nZi5ZbQQavzxn2W9;h-@!L3wL zq1Td9w0DZqgebW0UQiJ3+ygc>i_1t&~pn!h~b^PKBZ_AwI-ZHM4w^I#?H_J z@eN=kqc9p_u54mqN6YEka&7Y>sv7m$Bp%En8&0f9W3-~6zj zyE&tAh)-AA1gK;p>pwRR|EE?dKLZ8*@A3aU``1u{UvWQpu}|^T_5WVtf8>9!$o0Px z(!Sw-T%SG?!uI#-!pviQxgp<@OYdaW?T(ELT z{r`FPph5Lk#-BfrLfBIM^!4?ZtB*W?@xl)X6cC8yKN_M^EbYy*#=#3H6(W&r*Ce;U z{X?}rC=n`kT)?9=fv46$>`p5jJZ{Kw4N-$w1eoKa|7d9Kw?3i${PlzQv^1}t33MZj zs8L~EoyRL{@!Sf4O-PYYhVk=IRH)giMZK$Og=z6KUI~*Y$g5fxO}5ixVCi|wW#S|} zhIm5B^WAi)6o!p>)Ocm=u8(W8tqbmyIN!Ci%j3Ot z?|!cJEFBF+ z61MG17F?<8k?F~5+hws`u{&8~+T0qa>*%}Sw_eOWMlj7jByJh790;T&W%aP>osVfo z>?corQc2c+@j|o(&gU4}ufg06c$t*cz$MfCA0z zXBoVG0Ex#(KTSLG%$Ug_M^}N3In&WTqEDgGY3_$FH&`_gEk!UzP?Djjjo&XH2!0-j zAbqF8Jn@Zv9n8ImHc*p1CqKkcfvocj)L`#Iiy8;@kehq29Hk4?5C!4x&GFzpAR-42 z!JiyrZQ*0D&g(6Mt7!m>ym@s&lewKz5o_QD0DN_<%i8a+mO!Ij8hwvo39rY5)aE}` zs7FwLmFTC9Z>oU3ESV5CmtQ@XR_3R^GhggIMgTk513)CHL*~Y%VTnK%7}ZTY{lHk0 zQJ-f!f* zwrcKKEVe><6TP>MSRu6G*qu=jpe|b^tklCh_I5nzbTXz2h%mw5#j=`f-3;)6HhBVw z;fr4gTL^Oif)I73i8uztkSfb6o8Yg!R`>dgbS_IuepiP}nwQ-xHOLqb1`EhgjtUf( zF+iFhv<0u!O((1VNYYPP>c1Y?E)=jI+add z$=#nBGt#%+TJkBDYrM@shWY_HB(U1PA+I^|NFJ4x?K0`R4%voAfjo=rjbZ%5bH% zoDNnT2H|5D1CpEX$D8il>YJ%t744I500YY$8IZpMbCtCyPbK(u363Dm#qsEfkXEeW zgT&6lRNV)uY!d8RIWF^~tw9xSH2|pmj8o%`8DbjT7y!xz3@<6MFjAGvqJPyPugl)$ zgNfnoxX@B%*K+Oa^f3X&UiHkISwJ5A1u&cwP&w)Zuls(8yA4oXFxK=&-Y0niT#rso zH#e#Uq>P~GGvUcAW#L?Z zWr3s`7k6GSd6UuMIeLwD4=6Ob0vI8^eXoRQIUd!uPT9&HnP|$0txRC+!fY z$_%<}Xk+1}Eat8sre=>EJyP!HlzZhtEFJ+C=KHi9MZ>{2yNE(uk?f)mZ~um4A?TE3CkF#)el~a)MPDtP^#9|p)Y$2)bSLj*DifIEh4vK z(wX&Qae82rs?7%?k~h12O}7rO1}Eocp(C$CPBjh)FpfcnS2!;X{jw8H`NWqJ%bTRf zt5|eJhu3IPT^`5)eBs{#OkZS62U_kgs33dshq?BR9{~?i^*&~GF1L_cmpp&uDa-s> z|5OK-E3q|r#hs-WaY@_~&ArLB@*b?rmYyL^eLQ;+suZY*-`FDtntU8R`itW7{W{Oc zRQ7l9sADb~<}@1=dDNgsLNJ_Fo4*{O$vG_^w(OexnCRgw?~3EScumeRo)Kh@VN4+^ z%R;pD$9*?XILJlzt2R_`X$oTf@{{>-iyLJUZOZBE5^(9I#p&fl8cf{RgBPfkBaVFPXB z{AiTH4+)B<(EG_}wz1PFFC z`!XpW*$e#vLz_x*Hyld1u-p5-lIdlTbBROeGdCd&Gl4M($E0rqW&`$)$Tfsx7#N$<&Ql zadIuse3&Sq)($odl`PA*_uHPSUNkB!lL*XJ@+lxhVAVS&^({<8ww<$Dx{=!P$F^mx zw8H3BO*>IEsaZ3+=jwf>AMxgfpCcys==MJ;YY2ENF@A6NJ=fd%qI{+Y1E|8+XNPVw zSM|G?6r95yzudI8kdQep#;3pD=t8j@duj^u;h&0{4*$WL-HHdB4;%)&d7hLP>BuuG z0otza7PH{l0@4Owb$oj41w#+|dXKFAm1*`vsM{yJicM}##TZR+L>lUwm)?2z;f6V# zr?(uJ#-^ArpV7w^H2WieiDV2l&gq^bH2na=>QayrE8h`Tol|N=Ys4ik&K;qqY}J)I`9bRT>FG}VJ4x=>}!r@oE#-tbK;b&_3tRS z9816p$|zgh7T!<>N+#~b&CkFXYU`0dYCde=Rm4DQIHNc>5V&NL$M@e~I~c;Qd7R}# zA=2kvdh-^-y|?2Z7h$B=xXG*;#KIxpx_EZW{WhUi_8GG((2a6VeRHIRU$Laa1jn3- zDf0sC4Q^T@(#%*AnpeKv*yZu2sz8VHm|46CKO%bn;?NFdX^suy7|j?q%#N7L53%FT zFv__$Pr$GhwgxSgwL~YGivHVZe*1*0@BzH+UdC-9g5J9xDDn?84|+5`{BDuU@0^aT z38%1O#R=FmvtZ#KjQe-yH6Q*$+V1dMG+2(3;IB(fx-kE=e>zWAT+k;gRH8J&(nU6kQ#%TdpxHlwgxyur7bIc)nCX9{=gLJr7Rg5sxDQ&uiI3E zLQ9YjRopU^0qf&SXOp1V47^bgB;7-&ZnmR`sk=x$sDzua>tZ3wr4A35tmwKZ&X9{} z^nq%bW@MEWQq*jqBkk65#HgR??IxxTx%@9GV*4<$ZUDN0` zZg8XO32tki(zQrSS4lMYE$uK3FE~^6mQ`@Iu;L=b0S*rZ8fg$={9%drSUJaQHYwBd zo)kATY$5R6cN)#3i-2DLnbs!t{F>bGor;f0|Jhf1LVmQgR?a)p9mvD2Qt!-&H9Oal zO3?73liipxos?448EE9f-1tAsRm2}O?^ z%5EYc^If9ab(_t=V~@i(acgX*k`)RWNFv3inKYjO1O913u7k?t0)l{gT7Wos(&&r^Z5c8i!7l#(H>8&=8 zf4zH9?XIt$ZV^{nppHs6;}(vSUjVYBqg`B@6e3Vnf!k`eY$0S~v)0NX>}%Zqa3J9a z`a&g9&rC5$5}}Qr1F!g}jX#w}CK92E$bF5Sen9GI8~$@`EK%jWP{i_8+;jU1JPRAI zY+SZ&Y_44b(@tEjx_#Y1Y1j-_(Y}ZRn%9JXE`LbK!bqQy`}nUFjDDIpesKT6W~ZfX zYOc>Rs-mXxnRtOKp)KhBzug&L6cn`4b?qUaS$@!Lc6OAV;e*}C)%$HbIUbd68?z&I zI$St&gMC?+*g=n5o2^^36*bGf@3D=YhB@~T$%6L6yFGb(q~)OotAwW(D2!2X_#|=?nS$f<6>$opp zs*uOTut05etln+9(ezE3!POPjXwGPhu;C^jB2QMuT3f-(oI*Oj-YsnjS{e`+mh)_$ z_|C5IIe);?nt~8BLEg}5c?g-trKy&qNCm^xx~QPAXnF5+q9uHAZxL`Fy1i&ptd^ne zrq5W_^cty+Yy%7+*x&_*m~eg+b&@;W8w(1%>-ya|h=u5R*OR`T>wf$iGMV+bs zZ>Gg|8J4*tRRj#5=I&N8y={S5yR&)G64J=9% zF%`#W7xyOmyypRK_|RST6OzrBtO{cpkwwv@jUVw5o)7x!!3uUgKg<3}9Rev_ z16GSI2o4#-H3*$a0*}F_{$qeKuN{2_l#8CuL;nDSthoJt#38nBc>J-W-|w-m?_Xhn z1rNNDdz{Rg>3cK`(|TpB!RzPS8_Mg~fglwuE`nIIVaWpLIHpb>-^Zi)wW|x?GFRXM zf~!m?jG-DwB0&E$QV97tV?>&fili$RaYv@*3%|VU zeZAzf`Wx4qh*538hU@`%IM?PmP@+GAiH>H4vns})l~2jgfd)XJ z^kiNRptpl-87b1?q5}bcf3oKJ>2)Cw5ON9zGqkm9pd%Vc`R1M>l}%&eU&lQ9e2s}=*X;@B_!}k7MVvU}b z>(!NGrfz+5*k3Uj(wA!_?cSF`BUV4&CCq|!3KbCK(Ilk_SCEgZ2at1&RT4r*FE@?9 zerRi7rZpAICF84Ug|hf=IB0INiTA;@a4!)6V1}%aDc6FStcZg;5TU7m(u`XtZ_}bdM6OlxrEQb9;o2`C z&<9{0aYP`ESvI&sL+Bc?oo2}54^?tKD|lBBHhU%{dQ2FkgZ!u09|hWlUs|yh)Dvt1 zqJ1;091g+;o^yEt$#~MeE{Rkp!H_U8Uduo?d(R;h`e7n`=`u?%Ap~I6tK)ycPhMQiOtSh#gc)mg7$HSC1MDN%hni^-qC%RUp!gx}rpU?d#3*4;B_3jw; z?pU}OvG`qlr5J>pY<=2Rh_F9=may$%tKbFatWkh&orzHl3Wg5*DY7(BmCI1^y!R6) z0EX_#&zJf}Mn*qk=C+q-vOSU{-=U;9YC|oid5o5OsUfNeL9~^Z-5^1 zHe<)~@#F_EwqR~H^~kjKLZ`}Yba{eVgcCvW`z&i=S0B-pW`aN*|=SP+zM+^p!WondK;Dz-AM*?!5HJMhRw@dvhVmSMN!txzh0rdiF5R0n zLLFGYc@CQGCY&U!IRQQQg5PDyKhA8n4w6p7x!0T% z$aPnpI-9v?PzUt;xQ<@ClVSN+AyD914xIpICSSKi=(>>YRAO01Y49Qv(VFVHG26FK zSi3N#GlYz$jSV5q2^{nx)`OFlH@}}5UV7b-d#Juei)RlI+Vsn|BkcE3LM)!ko0|Ob z_t%%OEN5!iQthrbuNwdmIY^)ESFfh48AY5bhO`aDlRMLA6~gOtZLFJ&1VeJ^B#h>K z?Kii5G}E+;jgJ=0h-9GI1ko8qFJ%k@=;lVsVub&5&YcGm%hk9OKrD){c(S&`oDb|g z^SF757(rGiHZI|6Lgw~Fp%Qi=L?`oedvnm3K<0F`_@`v?TuPb8iyc$33m2LVgyNJ` z)YreAC#gV*2Z}WA3P-NSRNM!Pyn!ov2(bWE=S#)wAjwbkukqgdPT46H-k3uUO8aSCO3Gc(k_m(S~iUd zZVSqG7BPUNzG@=+G_UDa*fjtH4Eb;^L%MCSn7M7-TB8J(6QocND}GCiVwv!-pQN4H zNpA(Xk=M6WQI`4rt49{&DQ1xV4(Qb_Jqp9p>6=?C_@pN<#~T4o)d#Rv9T>5uQ*tH#_UnvJn-eU*o6a3d@#T>HTyoNXfzj z6wlH?=}WS|0&Yag8|1y}O*n@)e*Cx!E*-adJ5B{CwiQb{Gp1kD!VN)MZp5(mNa&>> znnZ&PNvF?xiK}tOIxl&?Z+b|i4=sIfA~Gx=v+&7rp}TGS*O7(V@ng@93Yh)NzSo^4 zOD;o(nUm#LmiqOCgQimXAA0a-pmgN(4Mj7e#nLRVn2V&D0=izn^;CWs7^Oini3aMW zD=)+g-m$E{g6O?4+&3F}9mAw%U_g((rB$uDiz!3LT-z7rNmC zc5&FsF(~7~qEiX1T*z8HKk#*&O>jX-__ct4W0iyH15OmN8wF8Od@n{TeZF^3pyz;a#xziSN<7u??zWwr!Ecq`r;Nk1{ zgLe-cdJoL~mW`Hxxr6Nv8D~(dtCrsrO2n|x?4r48O4H$6yppORq`(K-z+*7CYl>z@ zDY0Ls%#r|rs7Q7qm2D( z@`3w+6v9|D(m6+E-t=o?LG$z}f);g!MrSWUDadz-P~es!O?yJwVjyZkGsWmJ+| zC%C3xr=0yhQ2T9uiw#}PW}le>1K}WB%Y_DokO{RrMh11dhY$ZlAs+jr`JLYVyzuj?N@k)bS%D8rVmu0rjG@}T?WwCvsoMx= zVd3Nak%C+zs6;{45f%)wQofjDQzrS8^9P9G&-!w6gO@2bSEW_~6q4zQs#+ z%~!^?YaOGuzb4JBPJIU2m?!Gudl|VB2S1N(?e)1!(dHYptAtAbkya zvI=<2dMzu!YVsJEPE$e&h9G*7Bxd6Kv%*R?fD$pk6)EcUPd5EDZmB^^~lGC#$t#nU#eH{B1`D4g%g0jKx}yk;(<3Hc+y<#)W~c641oL zt1QmLqJtsyp=-H3E`BTB6k2VvY52IGu-q&&h2ApPI!$7AT)^Xx2H+QJLEzPO zj!Gsw7`r8Z1-liqb{>xlxuhYW_6vYoTy$3mjlk6Gpp!Lcw{}}_BR>nN>mZ$o%fah` zq5#B#b?SUUyZ;|Y@?6FuY(XkvloCk9yrJgv7+jH_VAt4M*x9JDg=okV6S3q z(=Hm?`BFgpKpUfyAqOI~As7`}R==O~W~lJeY4+cOG$Y{DbUY+AW8XaR8fyqh5e?4| zTpS;j!Tja~z6qS`G8{%y_V`);D)-c)>ZufUm;AQihKveJM#QC4vY^#M75MdQUC~`x zsjJNC3M;J9&(k2fw7J=$r4Fx83CzBU0LYO@K-~Q74G1gw7=OA8#?f~wZUn8zZHsnE z!jnO}#k-xO{AV{Pf7OO7Qt+bZ8ub&P{z%H)vClq!+2UiJ`><4xhx<_Z1Dyn(!{1>^ zU|WN}E-%t&+0cCd+JQ(|HK$%Xv02s3&*;45|-hiTa#_=$*|&7`NM(=EDabe$2srw+-j>& zDiT{)KXI|R`@V{Lo^xuT=S1exzy>usQNd$mD+)W$@W4p;vLeW`r}52x#SMUN%x_qT z{RCODNcu2cU}z7TdV$r}qi9t}*$!83jnyZNxqiLpPy? z1Eu>MeONnp?D@!qb3lR&67RPVHq2Pl@YNAuH+GNB$GPsLvn7}`gBk!6uA51e`{B#0 z8BPLmx;6U_&^n+LbmwEjP31Vwoaz#HF>*r@XAWrdP|$&hI%uWyS~&I}N==2C+Fh#q?@wdmcl?b5bHYV!iI^6J+{SpARo@0?xaW{~m z@F%|j(rE5^N5ymGbKpVd$dq#bM0Nh;h=g621x*PRpMG*cVIM&H{MNG}(ahKrvYzXo z4#l11vk!cgOC1IY08n>Zzjk)ofXIn`je!2$yY+xTaIEO3Lvzgj=4%(=4qGK~D5I2_ z`Ur?`)!n*tVGx zbyAz#dIklpifHn##tmdY&YMy_)gs!bfx36~tH$~Y?aQsU>FTy?6c+B#GT_X!U|_%` z0_yXL=nqf!d1uDbI)q=Pl!zLX6Tj;T!~S%P(omNz_Pyn{S&T1A+sQ6}xbKxHMd>hS zMtj03EXzd)29AEfV#gh!nzi@Q-1#OZA4(wxur7{_mGc#2 z8#K_&gg~0CfjSY=R-ofvuMS{>Qo{=9#PEaaY81WUM+t#w(Th6an%^KWN)>3hZXB4jI$~CjBw1lAPsd)<63h{6#m(Oz7oOb%40w z%V0s3UunkbGhFbxb^h*JM$|^%gEOoFjY440ph7uMsf7ii8_h)zn6I7_o-+S;a9|eX z=rX>mlH1dYTIxF}xcEEy;WUz0|1D;79IDAIYJy|YakEP}x|y4f+A(2&&i@2KnkngF z!4^Iq$t&ySa5Kdu6vZ39V9R~kgWUgyrO^M?(tSh~#;C5n zQlnnD>imq$wu*f)pRxMyRo~{AW0sH#m;N2@tM6$wdxo+dTSd8ziwb9O-9he~g6^YX z6ARy$udWNyXv8LuWa^XwY@GLkI-^(4Tj#ouW5t;r0iO;xmSNGQG+!%WGZb>E?pjCQ zFS5H#6dh1{KF26G&{TKhHdPfo33<~L#E|()Y=3Z0>D#aV$4B?_1D-sm0C~Te+i*uD zHyrwYx5Ttqcc1ZuKe@$F_{}i9s~C=UAvh$|5E+Q`Zv3xN>S~dE73v~bWu8>?CSlyv*qd+Rl z_4a2(SSSot*(zD->=)SxK`MRXVK%g2R z!H)J(Aj0sf$$QQVK_Bk+;WQpnGQgTT{S2_!@9WdM*Y&-vm#OeO`YszSLdra(gld4kMTi3;cOZY&J}4eAW8d5c$KO1nb-+lp(xs8@#_UGlLI%l7C$l-I06jJ9bV1VcqO69i^`N;ng~K#j`kVR2{uVtP-6~{}I;KmT zD;WIvloX;~pf~-~xnr(AsDe(2X16!o7B^a8W~JLZQVn!{RNS$1sh~%8mja=G~fSiTkgcxCC&gl3$jF-+8|z0$rEYFp?7_ zz1q)^o;2|-F}MnQSsbMUnz;O-nq$!u1|c>rpNr@InLq71bDdy>9oB^O&x}PQt>8n= z^>Cc45$1qc&bR{M$t)C2K{~carGva0TB+x4K>Ibai$M{gGfpnBQMB5CFSA6dwWe?K=k5d~q{H%i?Gu#7Lge<#FZ6zX?*E zoA64@)XmuD+i7`U%Vzow{_jR98xUoD|D7qnfc6$?7`4Gh$uq<9Kn2QMZ{6_#(muUoW-g}vE;AdI*9_VTJ9;*&l5aXd zcW$|_vc$Vki~Q;j`f_aYak4xOmr!PjvZim1m{K7a6%BSRF&5lZmb}ni-!KQeF!Wv z%kzWojyAvTDl+Ur&y;1c?ok?de7D$eY9w&6y~W;vj71xsr5-3)MS(cA{h+g9ih0`X zP+)`t@|f4MwA75)cmFgXE@39#+lD<{Zl}3tBK+NCV;+X>d&@87VP8DhA5ByD;bTgHz_1lck zQyIG5K)xP7YoEe)ky$#X4hYqIKzi9>cJ{*0KV4FIY&qx$+U3CEj|I^$7_Sjn?gmP} ztcW;e1&@PyZ*uZn)83bsa13N5rlV*s<03o9HFRP?m}(TT_=w(>q0cVhU4uUz7T=Pw zZT5ZeAyra3!x&)mfI%jBl{2@-*8Q@Azn>xE0kYi-TzM|of_!h`GL{C|eSuf(k(e*% zE%xKjz5g{{wFV-+^wUbvzMinU020o|@3VMB)Hi=>XOBgssI5P*vT5k9Iz|PKY)g+t z%y6MLNAllZh<~>>^7*d!<*O>q2ZMCCcW2^`lBDlCQ|+`A>g~7E}Ki9@dF|05&hJEhMv=?|Lcj zWz+qSu_M3~R4O%!NyBZap5?U$Qv&zb~O02TM_K=(`5 zmk+#SH7}o9V989fnB6);=yXztR9pKkv0>CQm-E&pi1d#EM;EV2ju zkqSUE$4UbQ!0VRdQanPTllzv47)%@Sb`v1B5x{mQ4Ag~PjclFG@$dvQS$XHQP4=G= zTkl#XVDa`K<7o-LjNvRy4b0fx=*beS6-7B%J4pAoAJ=J+Ho zB5c0;_L@yiVgt~C{^s?g$DzC%K&fdvNZ#Rg1^EG>BsF=ePq559`AQ~;JBWrK{B|tv zVawFO9T~6-Mvt?y>}|i-dje*zg6CbN;5%?+zmGw_W#t`N-^Hs|zOiHm z3ck=~GwDqK3sw|Tp3oh=w+Y$+U_}3M(4Y4A3s6|8j`(AO{==p_vlmoh5iD_$&r`CO z^G^+CuZ|c+?_+&Yf>beZ#MOxH>j2)4riyAWgYTNSZK_`E77LWY_KM%YKScf?RDE?o zl-u?;q8Olv3UU+?#GnKe0ckK0K|&aY4h88{x1G(ZW9Vk! zTd$sb@9(?++dNCZW1Y z(z@0T{8F8?lzlrxNN)d}nHKqtR`AT>i^l^O-EzDkEFWt30R%R6+&@1FA2UTR%46a> z9;VjceDDZ$!SFY~ve;(v9gB@vS7yo`z7z!NL4-nsZYzPau}L>g-n+-`zbaH*y}*ag zgFY}8BWJrJ2DTVsBG^9Z^X)Cb829WGUjOPMK3!tI9fi$M&rOJi55x|8m~F4iZDfxk zh15nrdAde^iim*l-XEn@{K=>Sua5&8GgM=UnFs*~NHMV@E2U#v%+w>DO?j4j8b}Sr zem*yzPV+FwDy!dgT6{9NKxwG_saqT@dMZ5>fvT7H%&#U4p?W84p#0b3W_2^L{ z$77{tcAg6;uLO1r=)ydFjz?7Yzetl=oHR;KFCeOvkV!xNLp8AnSPDy(M;nUdM}dax zbG<}ic`;ESrOd&>aInRLv5C2Hw0*b^@Oz08_RT=*BH}6#2K8prkW4%@Z{;nZTVlmP z(2ai!$`}H_dN+H8p@}gt3S-{>$P`yPA6avQ!ho`ScY2G1M%nsmV3yO;$a;@KkOu63 zL>0rnm)K?NfjAAVQKl5|NLee;9!<9T2OFnQhXE}lQ83D`O~w?{xjONH6lI5<#E^nr z-tEyUkc3Q?BSs-?d;M1qBZ|m5Dq`IL_a(TBxZlM1 zdM-a&9IJ@!I@bEO0h%Yu_P?+?Wo}iRQ!-i58d5EJVW``yjoQ8tyIo<_WF*1}6cM5BD@EoJLObL@g_Hfn*&6T(>@kYEUC zJNC?Fwt_a9;W$i=-46KTbhw+R&@@qMPISAJ(W^08Aimjhxjpkp@&0Y-@)lZt^TV$vwx|2BD;HtWmE&gz z7j{5d^!@gL;Dmd($66F#6>LTIY(kkCm*Pt&%E0+|GO?R;VK z@o#G`05`o;99Z_-6A$;lW~8DW(tYYZT}J8Dw4a`o*v+9Tq!mxse-z&iXt=T{56KOV zcOQ^sjFB!sXU19i$Z`;n93)bCQW9t_rgL}GITSXwpxIk8nP$cca-W`Yz1kGnTNH1+ zo$A`EIFPh#y502_x{lLAxwCYmQj~^5&LumAM|`a^!dI6@2BdqP)K{yz96WtXh!)1})Wecu~+*Es-*(pHR}SG9{!eI^U7Lq5CjChkb3- zYOsBRSN`R;MW9QJ1zIm&A-_85mY12$0^ z`L98H1{?HK5)Quiy?s9YR!&xq`&I5fjy6I%NAhcr#Gr!Xglr-JliA^pBX1Q*-^iA( zt1COXAlf5XA^voa&$o?g3dt3wNs$B6(_;5#HiV8U4kcAaQAQO!4y@cg{zI!;+8YQs z(U%2mmW!Y{X>;+%6gNhWiH;0_`RN`yz`;~TS9e17Je%qT!Gfl zz?xy3yDRNuOsZE=sejRO^p9z@+M5(E%&F3SOEFBnG8!Eha1)S15tRy2msOLEN^?M{ zD|rf62c+G(r!zvzR&`x~epAVAzo)mIl7c(t!jo1lv+yg3f0nr|r-4y@p1Qv7qVw1( z^Y)a$9gXpFGgbGm6!gWqGL#?6*`I-Ga~=2@n^vhUcLwuBAu%ONh=)~isHlVaL1|E` z&=kid1uof={{)p|tkj!}-a0;_zCg-)5>&&L$dJ+42eX(PosJVWxSq;31; zwS8APK2%jG=C<|4GI4tHRGE-#CE{OP6jLdzJGyT@UQ99ovZh4$FVv+lmr%?Id!G-h zBqHOw$$zPXfrkIHYP&W6rG3O>!NL}pMm?a9nsC{3QrSvE5AU)+X=V6q;LD&%4Zk_w z0W5;Hxw~g9Fj%F<#A|)qUGl%an~7>}HDm9B?N0TV4T{bcz1%YitP zD%0fCL3eNS(|1?4jnA|vk#e7WC8tlCe$M*{EeGxEAl&sC&YJTP-2aH@hmFUdL@azy=N=V-hHGulinwXngPk_u(|80OtJgfx)s3n-x%lI(8UB|`4Y?=b-Q;+1X9U{Jrx=&JKD^mJkfeLpX zOo4xu+{!a79Ii8xZ$BU!j&F_ondGqhyO8@d`AS4IO${D2 z{r&kS_x-kJO(7oj#P1N>I}!L;%{OJqoGD#aZdf7b^)RLTs?mcw#bKok)vW#=QA|%; z(U(2WHD|m1ncAf4##oLjT3NFTn)8o`!#z*32Vr7X56fC>t`)oJ96!hVUH(i5twOU7 zcppkopVNPP_Ilhh`@#Ri-eV*^I4}9*`;WT(j9}JjQh>N1D%@|`A^Z$sEvuG!``wAe^K$J$X8|vi0{aXWtx+Z^8&Nf@Wm8g{ zuiOF`*^8n1PoR)Zko`ygO}pO{3byT&%7!&Awh4=)m<()Q(SQr5JiiJvqZP`Pluha>HzLsJkNVxyreeOTWy!=Wa2OGY$i5d#f1=bF7~1V5l zXo)+v7xLbduSb)zVm{``9dX=NMtg^FL@5_BJtR!r@eaXE@~`De9uQYBrj0+dX?}o3 zea6&qk2BZQP$W8UB+9nw*w?9Grm04|%T-}Jz%>`3v@ekK*Zjvssu$6nK*c{EnYWLi z^gLTWzKWROrG*OT>tC{5l&^LH)kMcoco_BLAnV$N&QRgC<7;|s*Z2zqTJ*UvHp)Za zM>r+Bpj=dX(3l7u?^iY_FSDc?@jT8S4}j8$FawqA$HSkR~zZ&fT;0&5|Wc+E;~^(TS-#dy1-*q^G$v% z7ufRQW8Zy~n-}EM+dIAkGF6zcNS+EtCNzQvEwb@CC5nrAZH-WbL&qb2-y`euHo9b!co!DmV98RRrrxi>`OF^}ke&N?0 z6;bcMjuvg{Zf$lr(7+vzT2D!7k61q|m*y&-4!jH4lP4Gi3gEwEhkpK+f2%T-#BOi! z<6SOD<_{4(G0H0(QD6u8VCc};X~AsX(x}<;WoUUc_#55j>(UkZvC(3-*M1y9wRd0Surg zmtU;C5bmNai8ATStn+|2Be@l*P4u*ql%+y^uEF}2yqCX$Fgn_&X$7O9$k~m(V1$@) zO}C$->f9 zp{gF4mitB)tZLyVT1Mfe6$Xe5m>qs*Vb`ZUfUI#Yk^P-!w1Rhg@$*rHwsohd)72Gy zJN>!S%fly*MtQk;ffd9VNmFd44=aElwjcQdkDnvngbI+?2qYuG8#lR~Y_+?&1kjeI zWlM$)Mz2bIe{$j$P(o}b0T!U%cuaHcm7+g~o8X~5FUpI+Ypba?9jpJqZKHf}4Jb4e z1XaC@8Kp941@DN;%lrPun)V$)KZdlKZ0s{aoJvzl><=w{bB47Po%T{__|+LVxKW~M z&P51Vo+n;n{{62q<`Pows0}Aon}W;(zRe7NOgZq)32BD+0h zRGXHiCTBalg>qQPdH>6Kf2?wETl4@g?apVoFObjAdj_R)Wwq(@*w66LP+B`YkV(pN zWa3eP81{@R-RBIV8b+-GB@OxGXFc$5b&#`yG=2f@Y)g8>V}nsjJnSkY3*B_C{(F+2 zPyK!4wnEq_4B~FZR8io5X$GX0X@O7cDIMr+-Doi?PL(to*Gmom`P+xUBKRXSsM92;1Plo;hiEd9nA%7V~afX)vH(9jI7Jx>o@bt_2|)C zz!Zf4_}6sSsUS-X#Vaf^oI3~9%=YTE0i>`c!V33a!+rAl|NJ>Q1oRi}({|f%jr&;9 zGccH2_7yybjVlB9AiG-KG3MpJOD*|IGgsf2$0p*Iu>$-;Ft6yh6Q)oAoaT4ubCx`W zP%qDvhYF8*&DF*9kFJLOu-5)eby6H%aco+3{LuB~Qoo}sOM z6x2I^Bv)IKFJ9yUELKl1$nHj&g21FussKz%rvQ4Urp{dQz0+sT5S;JcB^S9S`}avo zUZjpv=}gLzLCu77>zud41JI%Y<&n-%2Y6GFvyPoZ+G79SL&&dORv+R+WH7$4K~w1k z?Wfyn<uQ@R8fiL2v zeLSjT|G`s;=@8$dt%j1~WatzqV?eUs^!#omAK<`_*zQ|M1Vp~~n9UHM!H?s5Lj?el z#^E83b+}7Hkj;+NR0Fr__a~OcIM)7M$V+gr#>iQHcgwPA5qRUBvo@kh99?9`t(n@7~w28>>Hs*pb1W z?*q$+7Ry2o9Vpgdjp6x_v)&K9`Y613<;Let$ z3?uwN*aZ36r|jWz%`_d0{m-7d@C_i$KPMgG`@zL=gn)w>Us;V<0U#dY>pTe&d(m?ZS0p@qgm6T_K}!f* z13Rq`XK7ABx%h!nFdRYVZAtUY!b?0?;T2wKncp8HhXI^G3gE{AAPO52UgV8}bw3v6 zC5O?i5Z<}|?>*#C39x3)F%Upin|7or;h#6mxx*O3bY7cyZ-m-ojN6mq!3oJ1Yn*TxvtM0E44&ysJ&5e zoFIZ-z`~qwN9hQvaGvuHe;A{!|C`slFT$4ZHh?DL8-F3{0?X!= zIk1YNPhd5P<=Q1LhA&dqy!vy0Nya2z8=fH~Rm)UFQC|?GvBLv&1(zmj6JPr=LP=>Jx|28qm-l&Q~0 zeZj3qN#rE&hY6mCRDXJ*oH}X7^m8C45 z2lDb5Rg_s*=Ffq{3BUhOE{PnMbJS5FQ6#zuB9R4#7reB>!*hk74h_qEKW{cfe(0zG zt**bj^HA95&nAfaMMOSYn4eG$eFY+5F%GJ~x_ z+Or5SzJ8I6S?AADk_^?Xa4CcU3DmDr2%wFt&`G6G_#1bjiaE;~3=BfQ{~inG269&Z z<7!iUP<6!N5lSL#L%6d}zJs&T(D|I)|H7fJzgG@RKe*z#d$-T^=0n7T3u88=gWU#k zC5Kol`OFNEtzti~8-F(qOAFXA5bT7b!9#@MZ|DU^7gZE&8AFg3*TXxL0lN%|12mVA zdy^^(fb&?Zp^i#-S2$$eXE>D()XP8$V>=0V#WQJX_CJ>}Gm}={vo{cT;d=A2Qj3#3 zFxwLU65e-x&J>oU5&6bMU0bC4_-E<7+>F5HsIpo{;s^?8>$5$%iIK|C*9I*kBqC(N z5Xo{c|NBIO>#nl0;^8iLN6o0r)5<|nf-tkWZDv{%{b2HdFkCHU-g`PoOBXp9%O(J0 z%44jQ!`dOuXd^x8F0nPfolFGvDRO&_&!`_@7AtdHaF_(K*b`}~e}BPA3I)wCkd<>p z73k}@c%$IZ!gT?pQ0U`5gUgYO*9#w#%qWD>|>>s^zaltAG-~h;gvfrX)|(?JyQcRdhZ)0 zMDh+G9o%vgzrO%33A+HUMOZP^AqD8HkN$IjY#HQa@HfnUSDf>)(8|T>Dd^hm=73Ejf>7+4I* zq+u(n?Ij~*5%2+r53!v-w6rUOQUsVe}lDo zHx|Mi2)OA5;l>K6K1P;@%sZL;#wLwi@m~G~pu0kPR8&!5YFh+35R4_E7ciC8fhf)O zH~5l$$b>3p_#%1Db&i@;a4E+_wD>%d2PdKzI)4{&NGqG!m^>A1dkOS{^S$}3Q#de? zU#C!j6=$`9fgXPMr8Wy(*37`)=3&GOGi3jH?OH=v{Ttk7IrIKcJjTKu3tJ?Z3N#-h zO>let+Iz38bV?j4+Sifk6UCX}xcZdgB>dhy=>zROxduAEkPcEjhQ2-EnD%rE@)3Nw zpFjWOAVD^k0BL!tZB6PF9o;Z;Yxhq>7a4~%$lNw_BDp^63(H7h09nr**pKBaLe4SuP(zN05usIOGnE|iGuy( z=bwpy*MY6hbg`i8%zKece3)A4fIJedsGU!Vt)3aqo!kV;&%TO7Kh zw{$Q8bziZJ;l>j$su6!!NxJu+C>bpODJSh z6`0*c=G3^0g77683C_s1=dlk|07M+lU5|=`-r3=9^Gwi2gGqu&QD8(OMzIz#HS~y; z@T^3D3;9zfEpgsubHMJjA=Rx;XaWiN8625>n0HcoZ40ATDrn^PADo0rTtx%#{sdp;CLQ)|PT|rftYI ztH{>&R6MrAb$%|9G@_cRXMGF$(wfJPbU!k3BQMr13^V>REC!FSlpi-1uB~*A%O~1(R$Vc=i%f&%P}|D%&rA8?k7bhr>+0 zVT0HKBJ%^l=sfs^x!i4nT?mp+B7O;oF|8EK`S%Bff?ki3-MwQiBtdsvgPzYg-vpE7us~U|sd8$!_X^F+|Zrt;jb%n#llGtYo zy#JopK%Tu)m6MJF_Q*h27VZ}N^ydvFVeY1Fup*}pq9qFsQ6Hl`bxyo9ORGEjB%^Q# z|5NHN6W9Gck}BjT8~JzRtt+>F$fHmzlK@ThO|>S@hroO8_N`2V;L!0z2^|J8O26}x zoBc5}iyp`^nB}M~2P|ffoX8W)H%czSKDNjtD5ZZC`|{ajdd<`SW_~KM_*+2u^)HDA z@I{3k_(bs;CV1J%KROW zAZYVoF5P=QCc6pXh9=^=M_9ebRp^Wuw{{LeZuAT-V###thk(cWm#g!5dYecVP|8yP zcYVC>UGBe^^)dCOq@n3mX1zE_!-62UDqR6I$`_LN5dY(`Dve1rGAHWjB*j3KX^Lz8 zIHH?xrU8M0>U}wS7;fVcpNJ=RQrSXZOmF@{T_#kV7ka9@xgpl0bZ2XoGrQm- zX$QXnCdK#a-aXIg3**%3TXSkt5#fj-W9F5M7VeC`MdEgmdbW9GCO7HHDD0Ag>GssJ z8!}%%Mpze_wAP?So$rKn5A9^;H!YK}vT0BmB$w!+6&dA>OZ0SNKScp+2+I_Ti3Agn zCs3~ELP4Pg5jhi5LPWGri#NxNKo<+KIkf7F|2U1eD&xW*vxkO)CD=Sx4q*Wl?n+f; zQga8tk~#|l*g71|9!t7{7k4SJ+}*W()K?`jBp@}@k;?(g?wg^e_`HNX!U~%){HmIP?iXGP^rM^+?=?8CEGsVt^x~?+x1V z0GYqpD@EyVA3@$BRQrA!N+TrGy43qqw=h(7ctv%D3P-LdXeu0ElY%&fNWENxCQwQ* z4ps&s>OM=~o`v~y&%L{VH^txstLxS8aN}lcSJle<`Dh6Vr^0p$di4@;to?jk)puXi z^y9$&o^!@gr$$=gxBmeVqQW_$YWVpZD*Jhby@JzPX=%+WMj6R@kKAK;XJOEZZKodO z_3Rwg1)5LHM(cu_mB&L;7Lm&0@QL%G32ny#dkiv5&8pfni>%yk`1o?Q`nqOSCAxCY zh4Xg+4lEbhYFLxhmzt}Z!@^~$yPU*;RZLmLM|vEl7%>43V9P-VXS;g0y$@OrCP*E~ z4q{iS=g@JA51S{5eCt%$Q!wv-4-Q^5@o7%vAfQ6(`xNUHxC2s6O0n#Y%KVbChjq?Q zEw66tR=`*SY->L(^i2kWiz zK{m{KO=FzP>kH7qY5;>B&XDbc;dNATK4?M19{{BhXe&G5h=gZxuw_5p>SyMhBdf1W zI>FErC>bi8V-%RhAlS0bqj4IxT8(d3TP?j-??tgZmH9F2A0xGAQJJ6ltp`N4HV%^2 zyB2JQ=gjD=CM+Klhr=hEdz<#YFN@Riwxp>&D5Ziot}+SfYrUo}Y#j73{|NvNe9^=Q z`>G;n4DH7{ZHVk=&mi_=r%d)*DL9V)2OD);2Oj6_pa*`PN9(Lj_V=?~GnnWG@QqVFVn8!?MeoC?~3G zE){a9rGUx){j@)~rJg<_ce)BE6`i(lKBV@rlBcr3e^M7-F zq5hX-dC>Fu+s&K157o70Pjw{)%fq(#d)Vyu@Jsi8fBZ;pg#yQhN5$J_oS%P*3F~h` zaU^*k9tCU<**UG!)j_jVesgkNCbfiDwR+Ee!)98%2Nd96KKp}xuxPh^eTmz`TU-8W zi8s0vFz^UxWqAxNq$nCfb8Y^)Z601r%WVOIjAOq>;gV}AcV6j7rf&5%Dg_`#IkV5a zPC)JJ56bYK_=&GD&JS=zHz(H`E$!P$TVPPMy*MNNh;) z=T5;}Pd7{%p3-}oB6G1WTy8GjM%xM6`NRc~$qwJL9?1>RUmu~UHo zaztF!a#p~U{)5uUQ&Bg=~gxCRJuavxZRFCHdiJ_6{jAL;zK{>PA)c5%PGAew1_ z@*@)%y63p#5h5+jQF|)jNYoZ4T*ja0HydkOb0H7D_3`uJgJT-5M$Ndjg-UZj0ZU3J zcketyu3>O}ezr0Z&IAk>KPU^yn6ua1`s`K%74F`>``UUG{qO{%FrUatAR^KfzMmtQ zgNswM>WP17_?$Qhg&B++BX75^IRWnVfm5rUD9fl5e*0G7H@Iacnm}-FF z{o3nQLN-&uy~E%Bu3AnTV;-G&EetlD7zP`(JR}=Oj8F^)9y|?W%Y^S=ojV_M^IO-+ z^UwT>tWG$&f2E|$Twd$eTKW;VGEWrEzjwq%{D9=E8Nf;5-r4P6k>M_z<8|BvT68aj zviBt#xy9Aq}UcL;h&J8yP%td|#!dd$&8O2)rb3 z-wuu=W>CCU>}-LerJ49=DWkrXIrP2;hq>r8*A5XOjia1)q``yokAXFm;(A!)TZ|&(p&%yU|J*O78eU2Sd9p9eK zQa^N<591cVrSTE&AC_RXwflhM-aRWWqpbYV9cD$WgJOTjMI`x;o&og8qc!ZzWiZ_T^f*$B7o_H5CiISenr-f@$Fc46uC7}F~-3!gCoinTYHv+cFFH)JwUob zFhQXA@9sy2={5m2+*BE;+-oHWlR`kyvf&}JzuPY0w)%mww~%v4z!&%@7103~J>5Fy z`f4bsB5G@WUH<_?&&LV(kFA@BCN3Y_IuDkaP4GXCM&HBZN?wnYrOH#4-vRQ5#OU#i zmh9MCj(2Z(!B`8jawWtNG?<}NVYE(b1V(+d#fTj!V%M%HMOQ98-~`u}3xJW)DE)~Y zFIwWWw78zPFyqAx9t@or8Of~CFxKz|=iSfbtvg*7%OKpzxA+2@cJBUrFz2`jk$O2J zpmLEChs)qFZ0RwZ!glcY%Q*qM6B*Zy!~H2XjS;HjKNzweW@##UUhlH*9r+tV+L9mr zFN7S;y$3!g+jm}CG-qmj&4>m)2}hK(k&2qgmbvH1o3lVRF7`VBb z>s3ylDa=2?E#)^~Hyy0>!qcYf;MW{DloJo{5EP-}hrvl+M{ED>o*wmn38ISu1-K-s zohkr_5760aS4{0=_aeA%XwDG{ZjuMQjv`Q$qi)*ccmUI$-bL`w;1fXb!MU%R;5WA> z!wWn;t1SaM(hblsiZjc^ulj*}EYU?A#yJJ)K*D1**OU7P#Q!h8%={LT{3o-6&UNce zk@Ww6ntzS^Jt^w_a?Xlo$8hjLdhlySWP}bfmIBV7u>@%q6%NyL$y!rQqJ>qrQX(_&x2HHb z&8lkcRUaI___5jBe%68nsM)lacbb|~G8K1DUWF@aK2)k~ooN5{GE8HFczCJ*p5zr@ zMFiCbJ6285yX6%^K&uyvm`FKEx*EavylO$KvL9}pr@k5Jd>=ZFu|7droW(Tw#G+!- z9nK5<7H}sOUS$%dwTaX}!Js)li_O1#>{y9-<-->asS2xZ!_2cWyEW1zKXMr|Rjpxjy#Pw;JHY@Tria zv3HGre7tJ4%?g+6{TLXxQ)-(sD3u+oBJNCzEPn!a`knwq7C{fZ_v$BYeb5X$vt2w7 zS>sFkJe9e@!3Mk8@R8EH(ix!!RP@v?JD8&W)2DXy=oBLTj^{QGdS)xn!^L3T_MO5m zzbm`Q_ws|DLEDKQ8P=%aS%W~8!qca(Uwi1w2rkHf!=KmgMe%8=0{0zE?9Z^bD)6kO)VggG$EcpmGG+I5^AQhe z9|Kd(ZQb9s#(}q2yUhhM78RJp3Q_RBGR(4xgWQAxQdm2-1)$51k)d}ks8{q+cBeh1 zkPVZyB~{kGMea*(KFVtn8*Y$WN$fs}gaPa(g{U`1jn^To)48>7f^!@ccb!~m&XR9W z>8g}^>xIwMEo$1^-Lb}lJwOD1-A6{neP|QW%q*q@F$f%v_v5;=b;`Pco?~U$ z5c+fy=6f0K06K6$F4twPP22GaL0BZu{1e@L2RmV>K3gnyol9%NMTAkkkYi{qxAv{p z?)WFIX@zJvv7Ch7F=!1I7kG^R@EUFpR!X#(nriLJf?h72(t!>UJ-4TyV%%~3KZjJ_ z#pNh`~DI=nS4rCeG=@*ce3!&;73YWd3K*T$8hz z1op{s=v}dA5Yl@$|HD7c04oU{B(vIhFapV*T}51s+p>%+jJ~={f^?giiWC!Y_-L%k z8tq&u@M-OwCKb9jwW%%pRu>F`HQ4ksgwTu>MEbpRxuJGb!IpbO5&UwyqM7PBo5<5# zTADSTYFb|br&&|~Fct(#Nh+|+7KZ{C zARX~VcI*XQ-ztPv(V-#^!N1^@aY*Xy&?dv`yFCmL%{7c?k~#>Jvb758qv^pPq%{m*!oE%EhtIWZr_^7iVXsSf_%AfW?4C zMt62m$_a+P1UgO&koZ>qy>_;OFLmvQqLdFQCh%N1$R&B^x9NOe^7Pg!v=cM{2XN!$ zoOJACLTsF=ekcfy(`I}(dqb9Rh)&V}#2WV|n8yPZ=g;J*4Od{mxx*9!L{``fCFUIP zv+X{4LN@p3;!KmAZc{C_HRc2!LB#BcZ0P1jDJ9(tYf%V=KfAJC#^h5ggU+$cbOa>x4 zy5m>6w5&@g+2F{ChjAA(Z`#xAnv`VOtj(y-W6SQk#JbXRy@0|Wt3_YXCxe=k9hY=s zi7c`zJhw7YBK~lRj-%&vR%X%jM8;IW^75?9vhYQCpX5hbCLdV3Hea2|l+=zIr`SII ze77Urq0@P?WDvciQf{IYDP$c}Xwu))W!WUDe&KlDl7YLs>WtJ=4Wl1+Kdlk1O?`Juf5rpd;mje>pcRkLF^_{+L`QmG1lTMihain&= z7L-hji%%Ur^ebFc6P%v!xHetTDt9X(i8*2;XTs_9U|di}afAOXcliy%qRb^!$<5qQ13s<2bXbu4%aU@hrhmQYV+;9jHRUuP^$iqJ)*%}lsIHa==_pJpGCGF_8 z)A2GU3>BX`Nf_T(u&=@xDI10pwdnT2ze_J*r?a@? zqygY>33rlKiT5rZiv>o#1uT3XFcR=!#%l@u+ZTa#Odgw02L-l+G!*J5%=lra?R{Kd zC<+9}Cj5hUM|I~rauaLyJWA-;Gr2Uh@1vKI=Vv3cVq5Fwr_We;-;(e#?8ctj?ylWNA1tr#ZVsghvEzQ4n+ zTVBlb8z>>?Ux~W41zd?pL>k_uC5!aBPVv^*%7$q06rHMkZWehjPz<7CC|bmQuf&o; zh<8drvyf*m?^i#iM~TeWknKzCX-g+Xo^@`osg^#Iq3=to3{?5`z}4sRnj42kc`;Qu z@7;=6J_;TYkAEZv*4$>gvt#KXLsfGQ?V|uks=MhR6dXi9oPMjoe^~8t=)mpSnO{i) zG-k{(5gh5|mb(;Y`ozNbi#S51dzr{SGzWvur6dzWX2cZ9{OvBQgjq~bqG_UEJaazb zPL+%@b4k1bt@tRXhwz9D*MG;5tP9)0;ys(^F*XWaxvt6i;VaqIj&P>v7iv`s(X^k; zg?sbDIJ1VA>QYTPu$_xQ0h}v7&+qn043fEZcPGI^79-wE$7E*9_4>fC*r{^iXOirXNq4vyCN&6pTVkVkWhYWEza!)u^=>;37Jj>P zo}ZpgZ-7J3F*?ko?aG{pYU2;iP%d-B_c*4->pCTE(cNpaTvs9zie5CHSL+>XU5IFN zSV{h*nVv{`>2ZO-!GSoy$md3hZe|TS??1S*+Kw&ROiovQx$;A7H%MX*$w#>W_tAZf z`8`7@v5jcJ{x<0^&ZY9?TT?(kx`@7wPJ8?nap(ZX$cqJj=t_=ZR|dKH(bv+sUe!SN zpsSd}9j(eP*hZhM$tL}FaucO^ElVX8y+Nx0m3kvWs=CV|RykD)xA0f-?WxO-72Ilt zRd=~8Gw7BPy9?UD-<@&VjjhK3BWPtA1FsflSf@a(W!2RbNmjHZ;+kVsv8EcyZ+^CY z0dR!ZjNsTU;I^GUze(DN!-r0{e&f)wQ-Y>~C?;5=#375{vgbyl{UcSZ+^s@|^4Ekt zI;B!M_xSQ5nQm)*?zht%ss;VLRt>b~eR1BiSM8NBokJZeBxiYdQMVqf_`aK(OrcvR zhwChi8?aB^nEJ3(_l5tLJ|#Vl8w_v<3tjr5u(kwCp)R0n4mw~2+gxR`UFs7j2B+6f zv+GrsyD}loeLOPm<)L}Wp_xW-R=A12X8ELs zNVsoBxWpoqL+npym#62zJ&Ix};bM!w!lje3D$vN6Nv_)tmmWOqJQx60^ybLG)E?M1 z#>gP`o!3aeMXYRZ4}Th9cj_b41}ZuFQfl)m<?>bx-MhO+(_@w$J+Qr+cvNZJR7T*^HtR9IPx=_w-V4?2Qa9@1Kl`= z#mxnBb%JXn;-+opnkAWP$)EhKDsK;HuIuv6D+N}Bz6vIEa%klZbaDtsnDStkM2-tg zxWPmL+7eU2+)JbQMNI0mAHFZi)9m&!x>ng>Wh9tak&QfX2ux*1> z+}*&uJHeN9(=dR?tuNm(JJQOro4-5Bv_L0W`6@ooy)1+5*HlMY3|u&vgjt~T;DE@` zFOOP@O;F7@l6RDC21if2#TWdyBw+3gS7L`J26&2f{R+(}PT|?m7{oQ8LVDfh!2wCW z$NZcLT3xIoM`UjC8|x0PZXQwG`{TV`2?jHd#i6b1&x|wyk~jL#K+i=z{bQ%os?sBv zN-jz->Kl+^oP{rLHQV-;pSg_AH|x}9c)2ZDC{n5!*JRujZC)0%z7kbsm!5I9t)RMt zk3Yxm=f^J~AFWnE%k8b6*pi~x`NQ;j7$?i+t3cUF#vEZ1kf-3UOL12xg3GM_pLZKR z?YEyI*%kFdpCJRxC3R4U`p2p6`*-zTarex&tx0K%sj>aanGfKIr0n7SF2ZAHKPH6w zf~bjSW0Yyo;B&v73XADBEcs0HbUQ)Nf!Jp_{wwsDppIpO#J>8s(V*szQOh1&qdRAW zix4rtN%$9wZ_2!mBf%c&+>8lx4Kb%zJ(FBLYC4XH)(HJuz`fA|(ss3FL4v=Dl{jS2 z#*?A!xVactpTmm2r%z{2fKyG)_$O(%#WD9U-Emd~+E1Nn%2yj_oP`W$GBw}^`9d3} zIv;u4@kD*He#r}*B^3V-m)6WYZcQ}VyJzu^HVfHWfG{DP-h{sb$jR!^3q$%Ir=B7d z>{nBCZHqFT4oajmVxRzd?oU6y`0O+rh z9DuGIKFm4MZ6VyB(imgO171UGARNO^4GyxjhFjw2!oh}5EK(m;p$8(`oM<37>DX*f zhZmcMxG$47us635eQUhFRN2zUN13`9^aL-5KxZg$0%*?N?OTzV>B^c7&ql2Jf%NN< z4?_7JEYQ|g6K*CM5$=}a;t8gNSMXMs!*W8rRz^( z6|9Jf?r4{huMN6jDITF258R=K6Svh{RY>oHU1_=L8otZgx1~ev`F!jl!%eHGXpiNe zUp#&fQ?iVXxN%*829H5QD^DcfBg$5!)1wHMRfJ>yb}~m!_N~Kcbl%jMhTE%TG~eSb z@z<9sHY_$)r@e3c2No{Fly5p{Tj;lI2+=#=&T(0a_q(1!xAPv764gIk?cdrIZ)b;z$)5-5Tdh&S{SIfM{1Gmxtl5v3Dj_7WY+O)bF!oA z^92UGPWZ*8y6^&H#cDB?;s+vE5*j`cuFlhUoUyha%q1gjxEO@Bzsm zjD6)>z=Y7q|NE79*7BE)qdxFtL}S#_^%uakjE-Gfu>am0Ht21>$hdGCy1Qdv>{rDO z0BeDAT0}g{jMcB*^z5`w5m3wEA`ZTMG@6Xrwo?J`xzi{>ebGW@XB`MQ_0Q`{1{9>s zI_kU2!iU&b71ETx=|2kp?d^J)>=k0a{|D0DR7sYl<9r$gjhm^9qu=|!e>0^%_4$zUBxxh7rPXskT-&{nu`lP+ki!}b zE&`CxMq;+`v_~4{(@36~Y-Sah& z!4i5}_<^mJc8mN{$pevE1!;6bdrr4&xgY%EV$yjbRK=#5rm&x4ISh#+K~ zkQC2C28&ozssh0=Qp^=cmEkpKg7{G)t^FAFPV85nD)sBx=J!7*F^6eYe}&M!Pb|YZ z4wQR|Tuym%C&OvLSvAdEO{+lP*LU@VG&iRz{I18T??H10D$P^o{v5{)^yh#j0|Fv4 zvP%MpSQsc6jzB!1bpF%~m1iH!Ae;w)1D6e?z3AOetPUPgCit+9QRRXXq9ng55ANiM zbQ(%B7L6CmA1xVf*2p^^SDpWVguQn>)&2iJeoBi5iL{J{WYyK6VWcgRO?DwN4pH_@ zgEYvh$kDR1IaWqRk|Y_&CXviU_WIqQx~|XX`y030@BQC(yWSVh>pWkN=i_mI+y{rw z>#Q2*q0_>;k-k%T#g(xMGXLCMzV>SQ&jpkR(M6RSYXGGrxekdCZ=UfsCgNR41wfHF z7bx_6XTh{Benr^F=ua9)l_ze<9`9T;&8=I|QoC}~r75pJ(Y}IeKU;aUk@K=b{pg36 zb(EQ-ghKhy2tFUTzg49i1)1>pmbU_q_%1wVYQ$q3pegZ`i2TVca4vYGRWjC#efx}= z|1t_Xss=nS6<(F3RCW|LWYG0%&fei#`dT{b@`cRFry48NG6FTEIjs2)^KIQ;5oWr( zEJQ2!MSKvnbS3{S;NMAc+`tcGi(~6y7<8*I;CsG}Q1raPC2VzFlim;uexAlePVX!K z)~m5K6^DO)?)4Zvl6a4Eq7@8z-FG%HBT?4j**q_Al8HGQ11BPbg+A++VqVV+ALVsR zd!$Wvk7Qd38ctOs^XAG~#0%0bwMMs7jz5Ji`G1@M{GT_Nl_?&s|AG0V~xjIc&sPG&2eNmz5qYiT;Ddj)-agqtIm*PQ*J z+$kSyD=dHqtq6$Zf7Irc+Wmjf47CfM!#eIqx#%}Ilb_GFeVp@)y*%{hr68Z}|Bq$- zhoo;+P_6Z6F!alYm`bqhLJC4?XCr8^uj(_e#`gBV=~SIfvTyiPzMnqQ6;bXr{m>v)VWWK3keo2v^If;r z>e~fNB+b29x;gvsalg9paah2SWhnVsY|ln_>i)#mQMW+4ZNvtPYeD3}2Nkeoar%2f`itlnf0Ed%w#) zq~Ac0wF(?GChI)^d^(p}G~x<6sON{hfSCwC^cNoI19oY6CVYk5@Vv$Kefvf@1XZ?G zgJ369E!R-cDh^vP>PBhFpW=~+?9<1~c4aQ)m;cdI=Xp0K*#G?QV&9ZB3g1F>MSLJN zJSx65x$4(z1S&{|+tq4`!(z}u3tUa&sy-SqH(kPlY!u?|j|c&Nw66YAEWJ?B;{&%T%kNzgjibshysT7niP8&Z=QO*HgsndtFK^$hy8oipD&$9_g7I5yO|@XeGqnO?pqz@!%-;R3t2@^V z|NSuM{jk2^`UiOB=lh!u8p1&D^1Tmu78f&#BE6_(DTR;eK8UHkOID@VR7h{c*I0$6 z^4Rq71yapCcbLV!m@{0$vt`Mzf~BwXL50@%{@~QBAu+~3<*Ozd?iSzCTDcxtL44vS=1Sq0RRbLX@;hw= z6#aOlEXN1GjS_vptqureGLY;NbaCwhifqY)=3MH#65hu9zrSC%Ql&ds$6x(+bFI$H zWmD`EExtv^MITr~Ljk=hxzLGJULT$Z^B4L!hb%7GW_*TCjft_U)EPKVKj?47xBibb z;8Kq~PU|}V=G#7%udMX$2UNp zDowiJ;RC$O-nitxCxc5)Hu)n0kBfU474!9m;^f{CUhwkeHU}Oqu7{1}U8Y@eELp{M zs6SNlAQ;cB`5M1p4~x*1HkYw}+*8A&HF$k8thneLxWaWFC{)C&kK8(-+wf_>jmF=S zGmN>WV-zRK-*g-F=ZU8S!GLb~%l}h@Hl^hfQC|$L5j(PUPrPx8;ab&XK8aP7ElRGg zu7sedPLCkvfdsHXK|Pmja|VTn?`8I9rB(KX3l<<%nDc}g5dVa!dIQwHv5my$m(-lEipF8i-t)Ij z*nhXpmdRZXT}8JOW5;nE$e5=shA|H;t_k}BYmn5!ANJP>!!1dAP1*kMs0S}CHKl7X zI}zf`*f7d0mgxiYBupr4o|w$r-2gP!J;%9bO<*%<#eA&?Kw^ovZ;*0x#-L(C@&rchXusgRSP@x zmOv@4Dbc@f|7UU6q1*iZ^=mGNy%jUZJFJ##pXkT>L6~sdqn_Z(_hV7NLAJZ;-y9Lr zNX6Um?6o{v=7eLoIOds(pB`iKzmvvd8bKu^vmr~zyWFd52O+H;)FL*<|D4sZUtVvSukf_Y>6%9 zP9Nua7SA9~Zw?rn4U_oodGi6GoM$;dOEEr1K&Mqyto5=g&P~QdeWyW-p)bdUk_9r0 z;{G*6xO2KzGh=fq^xI^H0V}*7N>@UmH=|D=ZM+XfEuGl-1poebn$hIv_I&an{Q^_2 zxDU~aiJJ$_YsLPmpn7WU^1@j(6F#~xy1!Xmn&!3$0u>gsPGT_76oK& zA;q7U`^=f?%mxfio{SO>kxQHv5)~gu`Ym>+^cZ7HL^&P}4kSsn??NuNJxmQW7jRzU zl$@i2+=?t=I6J?G+kVIOrFf^(F$=>gTO&)3^Ug`o*=)KvAGv#1 z_}-qWEck6w3-1)td$36(UHKfCW+Xa(6xTxoHM2RSeGjgS17Q6`99BSFkAj@O=_E`#%XESM{Y|gt6#%0OOyUKnzY|fV}V;+ z#f3A}bfbv}HgWOL$R($l!24tfWpw5vozNa%?5&=I=t;hl3235N97mtOm&uk=ykXzv znzI0M4{VTG`=tIElT-wS@2K6c7Ae$c_f-*agffOw-d-W1>ynf1-dv=|^~3Wr1;)}} zkO(K{FhqQ!rm#RyFewkM--zFl-A2YH_cocvOh=o-UL$OM! zy`_9QSXk7)SEIFkA0k4Z<$uwS=fM~1tw;lkoCR|DMEPt@JTwclk@D!7bzb_A7DM>T zQ0ag?7o|S)e2tLXozvK2tiA+k>A(||m(FweM;Z4(7!8olUEFOdq5i>EfVC}IDeCzH zhX_Lob;-wA0O5!!{PMTvso-DWM*}K2aU_vJG>2xx-w8%zQ4?>cy?^cn7zi!X-Lt9$kKT_EMO~_-1_O zswj;K2uIFO41q5?9b=iK4?yv6mjn}#hoB#csAg8OH~|@~n(ZtDnMEb(F~#r>SLS%x z&a|&E8}DD#kqj+%4owkHT6}5)Dl;eoO-{I>NM-ICB#t+u*gaqD5hn$um>fy7)5y&H z0hW;Hv0IP$`GLM2zq2;v@<@$FvhKmr%iD=E(Lula?ecnu56OR-@hK(fmVoLig4Ic zh^B5;q8{X%Spoc_-#!W_80Xr9%X?vuQ;XysE&3-suaeyq&t+-GrR)y+$eh4iI?pTj z`AI}m9#ox-9VcyV3#oFj*71s77%AU1IZl}_7}23;K^*S+A?<3n%DmYS1TKV|Ks-g3 z!`HH_PcQ3BZG3~-%p@ueaA254?7@Rio>F8MHssjF=blwm9CO|=xD3Dv-$18zy`eBLj*J&4 zIi12n^PIJQ9PBy=ob>IKb(GG32&8{Ntsc)Rra zX=1d)#$_9&n28uT!$!s{=BW{uk)9dV{`4+dr?fcbV&?As3^5$~ZM42E8e2{d5hIG>$brbkWZcTYUs zt-k@Ertayri-^5cham?(4PKe2^WwzaTK(zmBMj=h@!$9mqpMGyyRNF3QT;Kv%+8YX z+^BM>GbA9)0)x3R7n6*a*06Qs4OmP%N^v+6Ycw`h@w1N&%U}|1|H^=Ow3l=5-h@i1 zSWixk;)4X8c~Q2@RP$MN6rvcMrJMK}6Eo218uZ$G=j<9ZNB z{FROe8lbz1WT-?A@$kq!b7my=?F!Pqb8DsOO?9CKe48`6j=g(HJa7h^Zt=P&a~``@ zcOOb7cPT01GU#Tj>C|k$l`<1`@d_J(S;%<7PV6z`m}dzQPSEP}-Sd`*i_eZ5Ro0v> z*<>`LC|$2oRaNHVSa-rhdiZXxm1{9)-!VKJ0%m-SsOU;t8Ms|*)FP?={=JRwTDIre z7@?z<3lr~@mJU+*{HMF`Xi$SG#^A`P2AdM@k`^Y zan$hKcIC|201-SaI#Ilad=TYKYWW88)rRb)YL9Px8L5@`-7hm|W>U+%*jI8Z{(hN- z6Kq;zAA&2G!)U?(E&JyZY^#P^}WeuxYC0)~<#7(B^QV&& z9($>sN%`hsCZBfXG;|lO(JNM!TRgjIL8(=xFQ?T_dAu&Nk^sPL7&jD4?>J1slSoq{ zVdLKS8gUI~!#|QUm4gGxuWNad7vUM&;`oAbOlDILZXXERY`-8IfG-M1rFY`Gjnwolv)TlUcBJ^1o@Q{l( z=<%+@t+j5$_Yz#EsAa9G{C!lRO!H1>39}Q}e^Mx=TLs@nV`D|=&PVBy(b0_SCCU!dj@V{hfeWQ1~7p^Nf{#ai&^ZS&1V`{NI3etL){)^2O2TSO8d98(knz-s@-kNC;|%p zIM7QS(0A1nb?f_=F1~`YXEV3d^B3&Egeqbm3Et(8MBA+ zPIlDyH)u64k_e)_>7lYw>{PWeY>6sQxJtXgF{HnIe``AR&@~a6yd*dWS{=K!;hty86Hkhfoo9P z-Mz0yFL*2WFM4`rGNwy)X0q=DMOJ1huWK?XW-6{icTRsX9ziHHF-L1u*x#(yk(%e^ zCc1Xzmf9T2p+Ws0g0k%&L2sKs5JEqo{!h$aJ>wVxQ%JOsC*&sI+d*+SEtM> zW!!ywkKKCy%!?=jdf8fg?KjHt2iN#hD6+E5Iun2HZs`2+gKT~7te|aM%fzK_5^mpM z#-BffF?9X13tobbRqjfh?~MGUG>iv1;-@yB%ZgAWatXi(LSw&Of#W1=j2US*wbB^iy#RvQ?I^O0qM)7hR()xwa2`;Z#x&U z&n$q@x@yryWJ;QyJ$oNR)kokKTt9C=lh%(j94`3GwNv*JhQKsLnQ6n%uUT9@|F%%; zaRWyGAG_xJ5x2WeC*>s=m1vu3--wQU$tJm3488B4BnagR3S?w%$IKlX~Us;gx zw(0Yyh{FX%V+b-giibtpv;O@knr<+68%4^7HVEz_t9SRW@_C|oU*fa2bcAIPmgyd#Adg7ct{!AzX*P*lDhE`TL%2IJVZQ$pKp1!lll(H22c zV{S8Sgt`+480d6L4Z~bdwLi4Qw7)xpmr_K(J2P4(x01hLM&IRxourR})7&ig z5;>!}zl;WE-!II}>!o9p?pxx=t1WnNRCuJIQcXng6M@FyY zMtV9Aouu6EFDnf!|N3>|9V-fjy~LRNd$}d*&3vUldP{LBkK@y@T7tk?Ab%~~=KcC> zA0wuxU3JngJF#?61UYTuf@Q!`_&{h*xDudh1v!78VFJJ(5)4R-Ubz+x$ot4(7k zcf~%B)B0jtH93ZNQ?F$1a`P-)#LpvX%<8s>zLd!UZyx8E8CWij;*8}^Rrg#HoTDkl z9YO-C$NbtgF{dZaR?F@@;r78hxKOR|c>l4*MPcN1r#ZGWkNqk|WthR;7i`z7eNQbx zLOrYf^c&TRw2IO5FDdL=w6(i;biQ4RDKt(qrKo;h9}yJ6cky8Xvl9V8Z$4TWcb;hC2$iHi3|To^5J&WR zGOhCY;7E4#4tlzdOP{*#xT}faoEs7M{Q%3*X;LmZbG-Itq63ExON2$k{EAYzQ@(Dn z^l<+fF7?S6@4&db98u%Zw8arO;7P8}d3NPnRE9AhHEFVpP_XH(iuRkDBTk=2SHgdu z2-9HX4!&L9d_vs-LjP3QG-=_RdDo)9xd!5YJ*H8Zt2e?_k=j;xEz;a`6(1emUpPUz zJxYhwO@$m`o_gv7wd>Pkqlu0CwWofRORW=Zud#yR%?KNt_YbH)V=ZdWVG8>@lT8=% zJ>nF9r)T81G3MElKr2?nVHp@V`>{3piM z0`!kVn!*z6qtZzvid$^wK;+GYdPr|)y2@NMb}sw3zgr(3wDVkY;fS}&;I_D+IKaQp z8sh@a%!Iu?t*GcXd2T^p!>_Ehdpa$mAwym}m&>bsq? z?IpZCUFJv4(ocZ)e(Zch!@C@FX2IQyaTL{`8kZ-ZX_N}fUQ+{V>if!dA)CvaDw6ZD zEANlWhxVmC%v}Z}dp9X~KoTz3>j&)}<4nOZvrEp_<|f-ER#Mopm*R7kQ;MSti7FI> zxvlWDr<7OvX4n?l&NXZXZ>pGBCC|ep#a4Fk52|49;&q;~jdXKP2~HfW1veC#drPqS$uARpjX9@Q>OHKG?Hs8Ao{2& z6c;`4w9O0+d42bo#p;D4u3y=IQrHwC5XtvfNYA~J!u$iPSv!XJCR-nJpCI4OKREaz z@;o)$=0IXQUx;A#%PNl|rIlyR+$J3$esfylxnqfEn4j$Kb*jaoOj5asYI}M?k{`Jv$-22 zCn|@TBgGsFDGtrzJH{8ukwpn*+jfjtd<(7%BqYZ^Elt8pF)#;%kCCf{BS#5PjAH=d zJudD@QUO^OE5d}UcIRS66p2*alRpF(ve_M~{rQLqvkb`;;o7!~{J0|>U0u0tX2`C> zd7N#95uRR71vCj|n91c{jKDwUosv0tr1n^2QEr=p12=#JQLE3o7;FuUY0NaM+N{8>T@?5Q_S-Zp*^^57eJ?^gY+E>*E$j5@Cam-6SEkEN$! zVL@T%bId^;AL`A8YgA|h(m~y9#N4eGHTC(^r}XJ%cUM#FonOc5g#nQAe~P>^G9Mmb zr4fe}|F&4{=T7e&%}Zdtn0b(!l%4tUf#s!J5ASE_d4BzpaJI(^ z?|u=T@P8I7!Ue*o?mIGFDqNkMR_7k3?2!8pr(yfQ<23F6IOcqV`0$0FG>atyKkhH5 zHDp@zR+jWM=Vo@Anx6V2y@2ltlY~PQ%oDc7$hsf!9VgD_HftsIJZ46SY_yOB;pC_f z5cBKqI`UQXZ6andPe^Y=GG_6`MHR=^JEnU@Cl$;#oU9sVmO1#}2bqUr_?(ChhYg1$ zdiCc@gVg|%Vx$JJdH>{^AJcr8GEP+soDAZyUxq*4vf;qCJdOyzG?tYiBi^z};Me5XmpMb zb>_pmDHn+${caw~dz%jKNrtQ9lFkj1qPkgM)`}YF@AO;Dm)(8th0!Vs-)|ThzSr}A zO~$<48zl8?iaE)|vxI)p#3YADk-X>G_l%&10jhTA)hk<BnvXt0{WW zJ=~Q`gOw%ykMj;dpJ4=r8EDMhH6?zUz%qC10pu;Gp%dqBl@3(c23l zixS-+o*r~}#pqr<3iRZEiy$+_P{c8(FSYDw$i|9~A8p?d-~W$pl6dq{{_uCGrc!k^7=fa2fN^%L0TMV0|DMFtGu^Q?`e30$L+UBep^ zn9LXx+*c@g<&aP?;MGwKta7jpt$rUi>ZQw`kqNB0enVf)|JF!GnWRW7~vQ1JEO6IZq?!Di6Aj-vo93HP8SOWbE z78Sm01E{?#9YWK^bYj^3lEUJnNu{~f_HHJp?ba838c7z~l2x15u}hk9vMFTTiEJ~& zHz1B1F^^}7=eDQ+y=nwm`TMHb(qWFD+vc^r55zWBOI1~y#fm*69P(>1wcOEoHR!y~ zXRn0qmFC4eR&rZh{j>l8AQ5{k58$dApcGzjGXulo8WrN;%c}k8j132}e#)g94kHbm49@OhxJrewGgX z>Kj$erba_fU&e>h(&qp4FI$A~%omoP>e%o9f}=a4+sq0*7mUIoZxOwp-e|Sc@#CjY z9a!}Mx=zNU=KBa2RAUr9ngUJo;5J7Uzi8BzqCc~9Xkp7;rF$9i4;jZ6__y7PXVsVU z9r@MT+WQPN4@;y|4CGk1=Jz#jBdI}dIHFXo?CI#r*QNrpX!_TU15 z8fW4?#-Y^BN={6`lP8z9$1B>|#$H7lJdfed&Uz@0UEoE=Y}NGXafnK6z{o&z^A+~@-oMRkb+@=Ae@I^nY8#+e*VKbeDa@V2R z{sUW*x7UH^{u04$c!=!J)~M7krYMcJb;B=|g_))n%=PO{ps}f`&S*o%{MYnfl~oqY z+bOHBCcQwGh2Ud?s!>@cZ9RD;^=m(W;wz-bm?B6zY6i{BH7fr8RBX+)(Qb>h+(XVX zQGWh=Ubgof2n{ftfhzWI?SixV|53YSFKK@n#_})&^Om&n6mhJDrZ*F#dj6R=RfL?B z+R)D9(uZ3ZN)7ETgeyBixBQI@H$Xu$XOB0PS`-k%J z%`Eq!b7<_aW@G=7_`3C^A=*CA8>16l5xKqbN%;-;rAE*7;^iekN@CAxb>-Qlhwt$R zHjN25|GgU?$aJppj6shqpqqjHs9uNAz5J;G3C5#b@Tu%S?JmhmIuUl1_@rxVXoMha zh`+-EhbaX!1jFRhBjva$T_jM%J5ECPv9R%KD+>+1$c5YQZ(fnGuUkDe&`Z`JLSob& z(uU+ek*pUHEx&)LXGfCvLW1v7WL?cnxjO40J&1fIA3OjpL?rGMSlDSifG3QGQ^fDW@NEWwh zKS`jf!`a*K0w`&XxMKijT$Bfy>;w6ySm$qnLmmFd{;d;y=HFC{eL4G|dGn+a*}%RY zIQ;tQ*{Lais{+1gIWaN8@BE5|(^vZLM11tuuV3G~Hy5~>s%x;6b4U_jwrf<_5D#)p zr!5#S34t+}U8hNT9Y481FdsNAUn_E%(e>oY*=LtcaM=yypa7h3_lQ0lzGcrUg@iy9 z=NmIQ9=UV&-(rp;+fXQLdY`5nrrXo2C1d48J-(l`Xnmc{ICf-{mMcSVAgA9Slkj32 zY9cU6EH*-1R4~zP^5^XN}Fa&9TlQ8N=^wfQ_jFU=$*v{32~h41mr zDWwgiH*qSCPsHCKfN-)wV~UY?2F_XCl#g14z)d-G7HOOI<6Il`Y)knQD*~ftiJwbz z6jG=zlzDNIHtV0{1Teq`Tml3g*%drWD?XDhJis$*EXu^>-&i3uw5HGUDdChX{Gnm0^j_V;8O%C&766HiHg`?6GAZ#X;)O6prgu( z3KG}w)p8o0Y^tiFl-M>$bSkDQOBG3+mE!g@=o|27-xoXd1CUh(MupzLh1Vwrhr1q& zDK7iP^H`aig4?5yfpN*im>Fe8Q4hK9S?kYz!@wG zatO~n3-S_BC>fR*EMy{rr?Wk6rGDf-UJlfTf{46aCjV-V17>l0mj|oi;`(nJ$c9Tj zqz^|uVtSSI_<0w^{>C^?GG3kz?DMWq%X9W+i?)Rr=>M2X9>Y=}Qwe#8@kDw?IvhT1 z_O1iis5BrIYxp=cd1G3dvYb4xdmZVGZn$}~6TcuWITn`{D9uQ(X|l^Yaoc|rI+$(T zVw{?T9bUm6-1c=BMw_lwtd`__S~EMuNEH1Qy-bGv&{I_b`2@Yj`Tf;XoTNU+HY_9g zVnsl-S*%$0a2Km3L2Z`PzE)L9aqEbnLrQl|TDpKw(6}~H0hE4OrC0XE)>3u@y5&^j z%e>9;(DdLtW}5GVz(Ave+1Z`H|4?KRiTQUb1z2H+sT5D;^aa?Z?{q}$-ih;#t50n@ zGMK)G_-ZCapS?F)#Y7qScTxaeL?#8K$MOko*hIti0{@VYh4ig{l3!`=lV*e$6m4|J z4kW3)2KqEyeg2DjYGW=4ii}ez%&~ZZ{@t$me@TDY+Z`Rj!!F6~_NQ_bGGlhhU>V5a zL=Q_ygetCPSIWp69U{BArS=17z}S#g!VoygbXw@>gOA=<*@dX8ePY!7@3a5g}o8GzT%H+dT&xI9QQuGl5P?xlEolXG+f zuO|Qma1^hBP&<}+ipS}OZ}gsXS(l%QYUfL`E3T$+$p1UbocdLHy)}C41O^J3rmFNZ zxKC9fHezF){ZOeuBLF1XLA8@r)wv~IX=}B#F6IeScGUiv7}l$Z#`T&SjU5?$*NgSW z5HCe`WGKk(4JZ^nUeSOg7SqFRKMIcGaz1A+R2+UWGEgQWl)@wvlyRxCwy#t~tmeMNz=8R0WIh?E7l3N2bs)z|WlB*nu!t-k#W!yP zy<*UnJVa}X{~0RzR&)(JNi93C2_EO}mmtq&O?z@KIKa?s{d$d*BTVua zj@(FA*}J!FN(*0fr>B_%rTs0gNWCvXfq`r4NXmHw)?udyZ`fwo!Fz6aqKp2mgqM0s zDb0n0-s3T6lzHQUMtlgmaD#Zf5Fss37fdY!f+dNU+h)Ba{#z%)8OCF&$87QGVOX!m zGcT8c1Vd@3SmopOn@3_7*b5zQwYRKm6M9$cJ`zlw?iR3cFX0Yp zN!~1>Sd&mNXC>${+pST*h_VfxM@W{1MxH#$Niu9emPv;f4By0YI)Gr>H&QcKO9o#D zpFT(M;tj=ymC`zsUyjk;W}GctJA*YK{nEc)$G`Hkbffs0N9R4H#-50}jbCW&PbqN} zeOwQ}vZm_e_zeFC9RT<8r%*|!)G+Ob@w7v88<3_#f@z+_1g{BSH2FyThvnnLt@Mkg zI^i$;=%6Ag=_@W^_jj(Y>8r02uZXa=fEhGhME8=qePek*vwnh(j$C;th6@VY;;Ftg z)+w>l-HH^MGn1$Q%NHwh>0Mp@M)jZtl2bE2d=&cSeeZPW7JjiD2h0nyY_klN&(*Q| zto+-5{#zmOrInyUBF>)e3R!J4sz6vMzea^uPzn)1_zF0hOofUK-e)JS}&R{&kk61t97ZVNdo4`{fMSkP#%$UBE>zUuw)`3LTuVlJnzcH85(MT`v zeO9#SziA#uu57XNX;yy@|Q^eFgk53}0XKeO<7GL!!m!Cn-|{ zxJ-R~de*NGQAye2vWCL0WMUHaFGx{zgCs_&_YDmVEo>y}gIr_|ETc(QBW$4z-Hh$B z)1qUUts?xQ<#Fj|sy{7+Z>_K~uyC52q1|1XJKMDR^}8IqG_|v5DSVqrSZVL(LL=PE zZP`FQI9X^|F$v{j8^TQUuMR1gPECMfJlUY^Qe$nL(g?@H3b<&=q&^#B_KYUFN2aB# z(m~i;Le~SJJm6L5=nfpv{(RSGe1OIKHvQx2=5=F_)sHjsv1+9xN^_odBJMJ3>CpV_ z*wRsI0I=bJa5e=aQU*t5enKX5A&!h2J0AY9 zp_`aC($(Wd@{TaYpDW0x8f1Q^i)b4MJ2aLHRmc3UsE}}>M@B{K1lJ}syIEU=tqD@A={6lgitMs}$p6Oq;utkWq>s{M3 zk^ZA>mma0&M=_VE=+>n~pOk3JB8 zc&1tdj$$9iG|d`oH_$xtm%GSK9bmDXg%rB%@gcF~gp(CC4HS6l1XQ?HVOFW;bE9{^ z%yFKnIXHP~s&}G`X-fGU{)IJt`nBZL@b41p@UMd0{uxwCRzLxBb!#n!Xr!G>PR@JcZ%xw#ogJ{km(S7hDCrQDn>}4?jv~Y1>P&KT)S;$ z5UD2-uarw)t~V62Gb5;4jmv$Q&>$Rk2_KV%oHLuLVOg^Vy^pk8q`nWwNz`PSKxrH$*m|9>BqaLHf5U&XD z9Wc`5=}O|#zdR*#*r-z@;T5P}bmY|Q>@_@Ix7$)Duc6;S^LI^Rc)X=b-OHKWzLXNn zvH<>_Y;xAs9~5}?b~`4=pJUNzm}%p|vny9oopWkIbP6{XJ~lBuW|}mK>HlC-0V%2G zzlBaI@uVLQ^`WlqZ7O2zlBsYNx7ui^Xm3yNOwzM}d$fyG&?A`n&Aj^N)TQP8?$$Nx z!M&ypU%h$l->B72bb*F(mTuA^%XMa7_NaDzL#%4s=*H-?cB;SjsVCK!?{D<7NV3bU zDsYH$&a~R;s{BQ**Z+2tJ<(jjjV8J&e<=d*3 z#Bo;j%By}p-kMZ0syd$by_MzpSIM(2Aj-T)QjIU1f72k@ZW4EZ+ul$i(}j{`YNuSh zL;G!wUoQi7f2M5}D4AWi?*ka0)LLCF3r3FO39b7ZDeQ`%mE`_FU=Xl2PupR@`>2H2 zs+f31uOkK#VdK3?`m9@b?if_Ac<~wonL?V$OjR$svpf z=KR}EbNI$Im}>4g7uS-P8!y|CV|(%Z)TGV#CcE;d+ds3YS%UgNXXw*Mk_wv6+1396 z7{>XB;~eLynTN)>gc93J6oXIJj5eiJ`vp!}Z~Zzc5YDme+VNo7Hy5=3QNJ^BT|jyK z2Wv=AgsmK(qwV3jYfE!_sYj}09WEZe#4V6v-fvM z+`&bJe_Z+v(a% zc`o(l&o+qP<^3h(H9bQU5;HSY*QT#{d}w>m!ASW7aa)A$ZwNQlv%Q@#Ie1KGVjx%J zVrFyGzVr>vxl1SIf>BTrx|;azruFQs^0(3W!;VApfF$%R8jOUy4WI!lObtfm&pqpL`Nger;yXOH-26N;4!J2P6v-c}Dw}m5 z(_^@lDrC!_wh?}RbK|iZsnO)gxe3K*Q(xA3MSs2dLN1a;%wkF0wX2nJ?mfFRdakqT zj;n>1{MOtbs}gTsa(}KsacH=ie#xiEeaFW{NvNDCw-GTSQ+;>S!LR5bKKi$H6A>5| z=MVFEPJMfJGN3D^{Qz4m_kJbCX7UN$17MC&v3Qw?Vxm8Z7`XbJYBR;FGPGt2= z{rVMmy?W|W8Ey`mYEIHu>HF_vVMg(2x7EcQbA+OsSvGj`Hfp=t*PKk)!5DhxZTary>7D;H zrr5k#9wa1YZ6)UNXW72WopRufWSHya=A7)+c__MkA4Rh>*gap*X;VS~W9Zsa%l@C0 zR-FgjtoxDPy*OZ8IIgLp70JS*EHk=VT02+#8N0%=Dj;1dYr@5>M6=q+JXoqZGcH_i z2p!aSmp@PEcgDwj&ti8SUhZ>+Htn>@-(l9 z^hnV19SwQIXP?!4W?PT-8`DhU(Mc0MmxNC12IE{-p1z|Q#~&2f#g|J>Suz6Se|J7~ zk$Un-251h2eGOXqEhZ2m63g0~ixh!(6ol)aj}$HP!{+YX?E8DQ`A%1WGHiQVGo;}$ z`ncw2OS4dIl69kILTUyAdKzIzrR~iVs`O?JOIVs?N@Svz5faZ$$0%Ui|F&N4b*tyP z&r7Vo`L>WbBNutuqFIlIh~XXP^I)k>B_{D64=PTL->`iqXoW(TEIQofZK-J8REIl( zrQ)d96t$F{v6l!W*ef6;Z5I2cO7{LrCcLhL*P1WCTY0VRw1O+I^UuvwijN}8RvQpS z1iS)M(Hq=-rElqFDkWlU~tBh6Bq_u57Eql2?R4lE!rv@8C3LncMhk;M- z3M5zeV+X+7Ozg5(OY-kf=L4?M8T-*H7juta)EKduU|&O+E(?>(K*pBJ?T!k=P8wXx2?NB%#Q zjF_$h1H?lAX?2TJ7k6%Fh|a<}MFj=BHzXXWE{r)UV6v~T?%T*eN=KJl=aw&4O5kzZ z7d!UYx@47uaz#v#nzZR1E-(@I8J#!=(kqBnAf3APk%70va*RX`scj}nbjJ7leRF1CPDAE-jVfh{2Xb`ZJXHvt&^18Rj*GAr996QF$ zo$@Pxp2nX6gydh*ip*^=QgrM=^YmV%i1+m+Hz;#`Ckl<6FuX5}ky8jgw@3Tn)tCg{ z{htZyNa0&yUb!IX1J)+1*(D<*BkOSrP7DUMk^Ri!Pqcn~!i73(cV1<@`4(6pptDCd zUilpn!iZ&mTM}RLe5uXp|90r`kiia}v^n7f-0vI6{k4jjMj%3o$)7LkU6=;|BdK_i zXNI4+%bwAFZ(}1FEy}qJ>h3?;6#u%>s`hdrCBEsk)0u%&48FV@Nh$5l2QJc_OxjwV z%0f<9y6!j(noMmEXP+&25cBH}jEJLu#X_Ej1ds=8LYq5$3EamD$>F=)P8|>WvbTK! z!?(;Jx3>4f(?vsTMQzJN8TU6_>^+qJEwD-BWP4FOZXGBxmZ0G-*LxvPtI<-V4? z`)w2G`}6>{9x)>Z^enz-Wg#Vcl8Yx^w6B-LLTw1zg@-ig$R z>!3$t?j*{gVuz*+jT(t}CwDgHxMp{L4)2`}hh2G1oyT0byJGJC7N5y1HeL{VM8yQW z*Kpv85_Fi+-QA55n=p@*J`i6=R>?t`wmD8=jjL~}zIpS0ZjswV^a`q(lVG@xC@I|) z4Yf5j8JQc7kQwZFsF4wudp1FZzh~+)WGW$;VySrW3Hh1AdRSbz+tn~Av8<>xc+qh113iN54_b*iUhAmjZ}RyORsZpVf3pvn}ZXGzr1 zgH?L*zq2~JI_pVSzd_1Hd*I1Ix=|J0jqEGWzL;wor(6Hpd2p{u-OxjWot0xK8t!%$ z)yYB2Uawj}Ns0bnRGRH+PZWy9wn2~JVj;&vf4&^6mp>hc(BP1WaB}zqOun;(h*S5( zs|qB$P2E~aHy^(ouaud1G2NnzPM!G9TISxxt#jDsd2eR=NX21lKbOI|VToUVj1Pk% z~ z72c{b`eq8rZs^qP3;Z0EzUU55`F%QcR|Z`O43 z&lcXa9-51G@zbXknkVAA0^k2?%%6#v>#O@+LFbkbnQP@)!;-C%zK{>^{zq;QI9!N> z9DpxYW*Uh;me{dG^Zn5YVw9?re(Ni zK8k8P{<9~j`X|6n**aiL_8TbM9uWk#7e=oCO2K4pN%hKIv{X^M`)pN$Cj_ZMS?l+` ze)i94fj)KZOnMJ){2qaXLl0@eonFz}N8SV$$i)q$l+4bJ?b5dl4iyu=S`Z7EOZm)g zSD++oYksha|LzOC_p=_(x|^}A?IVozxj$nrZK4<#3t z3I#mW2!Gy%r0<5T()33t}47oUFtXdVH-GB0x~w`}hkfqy;w$E>ru*NZjjh*YWwr=o`2d`TCAZbM5id2NQ@kLP5j(bKiIG-3kbgI=_-I!@^sNY~N zSZF;tJpp1EEDq}h!2pRm^SC|nvHQc<2<+O%LgT^B@3Q-nSd0)_`?gqYL}~X|rpOCi zlNMV50;fe~^0P=|lb!?4L zmC2z!>NHFer~VafX*~+Id!S2x;W8T8DP;O$??-s7jo>tZ!snUQ{wpvj!a78*?D%m{ zeffdNkvXX>JW%%4zi8JMvC;<6w&8gai8694*d(7F2Or}?ghFUh%U9k{9??hu&3Z6% z&8O}`;F_TZIs5-w0|-nAtrPiR*RDs;&5jj_I}WxL((!ZZ;pT3JJHRj6h^bu}P(ag2 z)Z@JbxIGT!aC4FoKxq2=GqAK$DOQDkSdx2V zyP~^2opH?Pu~4uRWbVPC;u$|s&dt=~pNb>_Yq$K}?9f;XxIlbY!KdDCgV@K9UngP@ zDg%BD^E%09q&$NXhn<{W@tQYll6$|#si)#_{&u_-k7j)|{80TPXm5SFiXlnTS_D`y5Wg) zT+P8D*+#&XRNDRu2z00Ct}T4>1je*>*d7}CaX8MzWIF~oJ7o4u^%-%RSD&9-qy=lb zZ3e_r{f#PU?l^DkcbvU(x8Rob*$aL$y2+wv zf0qQ$|F%6Q;2w#UVhpsw=|Yn~T6v0hF+J8M!Yi6kI1h<$qe;6>lBHwzfmb^F|MDNd zl17yP7oG>8Ps$xQGzGc~QzI;7EnsTSeTyAdo$4UY9^I{EVN~552@nW*V{S z0*`R3AR2Ap?@E;K*7a!Nq}N{oVcaRCgV2Z)2{8#uWa(B3Dd93{+(CtwY9l_Bo0tXUOp&N@7|Cg!D zy)mGXfL(xC`pCDj;J=+09EWrNGFbhv?J8cZ2zNI@4oNX;Lp2T)%gORCqf->0oY&OG zPjqWjMJAbrN%r0y_$`i`B=xRVo)EY1t1F~qzQMwGk)qGydGrF)HD1l#?2@3;e*zW9c4?9Cf8#l=#c0RPW*+xP zGdI8TTEk$kTFl}Vu+Tm!2z_Cce3^k(2CQk~Z=m!Ux}rX>Xsq3q(qm(@pcJuMS`c%#-00I>MS%p#YG$fOQU*T%*eX%1x5T?s#3^jV% zt4vGkG1z?n>VZYoghDbv1N&9tNa|FOA`p7NoFb%5~wIy|7H8dW|cazdO7T!h^ zn!P$`!Lxn0SY+N8ENGD=T<;JN-x+S2WVDNVcd}{}8;mQ2kA`d>S`HMZto7C9L?=0o zn6%w4X(29JnbOh#BN9r1Eyw^EZBL=edgUu$gyvbg(FqCeF#M*ziA7Ct5%V_3<=xTx z;~zMjWp|Xm)WtuZ^<(xJ!aDVi-YG$D!3NW}P-)s^68y9$gM*{cki2>GhgLrpF1y9J zDji>4k9;ao|7k__8sUK-4OF&oPoZ%|yF^0(Exy9hG`0@`-90$jc8LK>Lvy^W7wAhj5yCbmmN_6v-y2huL; zspv`aJhcKX*_Pw(y0^k;x>6BNI5p)o3!QwpTdc|qu-N=ntJg*^%=HGz{4BbeQVL1O zx07g#h0dBWUU<8c0AIZWHG;JgsWWCyH*S(Y^dmdqh}zC883D<|{LYXn&ZFZZ)*vW5r&HpQw~cpLys@kai#}C*9&$!P z*d_8Q=u2_JVNpwV@nQ@dbG^_du09-wzG<8Y*qTA8>%-_JW;w^&Ox_4HGuN3)mQUUb z!3|<2OK3%=S5t3?P$$EPEmw^QhvxJ@D*wNv-)m6A+Sz)RGrozkV%3 zRceK($~MI^Ft2upC9~5yg!465?cVWUziVLXLbTy-d4TI|-_L-&J9y(2AtQCruUdfI zun^n(K+J+c!F|QaFrUZAOFYYd3?UEpuvC&!Yv7-b(y2RKY2~Ak#-HOvZ@39EGvU~Z z0yq=xAe6TwR#dcPv^YUCV>V~Y_;#(BB(>x|y0xGNR{lkxYq zm#ijjPhL9!0!zU~E;ueNhHekX%mU;oa#yn#FDvKRq@fmY$s#a# zu5XTBGfI>zz)L~zsx@*)!8yd+0fkOw1Y%v1I>7CtJc_~BoahbSHVTfVJn-GtLQ;`4Sy_|$afhI>vb^BqP;}O0y+lp4(dy#Te4s-E&2ZH@CRu7F8~u(h^}cD`4hMH zi=DWYdI0)$==OJzvt?1e^11YO*;RtEP>hZwvAXhE(Q7NI}8hn?5 zptn9f&V>y(B(zq5Vots&AgY*`eW|vc;XoJ*dX4C^tv}&Lwwp{13qp5tTn{>qik)}h zUygxrb`t+cwg+NU5cU&%J!P1G3b0m*Tm+nOx%PEd!Mc1sY?cFKP-z0)P^PZ25Adkw*^0@TiV~PLFLc2?OjqA z{1ZU(|2(CQ4EzS)q!(Rtmh29Z&-=H)G5YQwl<68bizdDsAh3q4auB8CSuW_#Sy;U} zWqM}jwOCz+{(7`rSQr~n*me62_0}|9s0?%nj9pQK$ZA%so*aH@<1#N#@{>yCHk`S} zo~wW3ErHXa+LxESxy68%_Z>fM%aU{lY^H+B6P7<|AZ&dQs(q+C0rvG;TUb~mjbVN^ z{~3lf5&kvCpv=Nr4pX*Tu~v?QVPh{$n#jlf6Ux9f6hRKws&;T;AYZPV*f;P;OmO1% z&#Fzrvs>a}UKMO#i(it&&tMDB6g=5)V(o~aeHtX zthAT*!}7{o?k;I*$5bIwhP@dexdD!1`oxQX%8O&}7xQ4Cu1ClwTG3wH3vsX30Qih* zau*|!C&6A&kf-n1^(EBnT|vtoKTN{Lll+Iz#GG}ZWKP$6K(G*a z)w)>FRqt&Y zA0RVarF3l?AJxL-HeqGzA^5R!E*m($i5J}E>-DyA;uSGd((A`b{>oBMPtP$FsPxyl z=ir1%*Lc-5h(e8Avx+~_Z}MuDEyCgM>K60gEaB(Z@^II%%5mo=sZubH+kO@t-O#)3 z;`6Kbho9t{dgE3LpO#Tc#*nSdlC=3sArT6CRrmm$q~-VvgQ5v<Kz$CP%1Z()J?B^ zQ7wzLwB+%IS+&$Sm5QUjcxQZ%cxQMb!(y545>8PZYfO2#+))HKD}vnY8gy}E&ZSG2 zR`J|65>R(e5-)c*6DAW|M%=^d8}edvBdEhu>Gn_8iF*X9tzBKS$4~ogQ(f57cJhhb z*l^Knuv2fm1TYK{SMa2D-YfYxn5w0<*k}-;EsZPfg2y|As3m{rF2vQ2f~(_p%x=}Y zkMkeA$N1m>5E9oI{UUk%v@jME4@UKCD&2mb`V9)WBQ6e1AKQdiMM!I?H?mQRJ8kAOYv8-zXaqUFSf97VWjtIq-)o^z`8$PgUL1T1|3*%^$2#IXRpR<+)sEvkum?k#t$5%ggMU%o7xCApijB z#yz*mFQ4+BCO9Q}WT#UK9c>QrK1;zFwRr06F|6vTGp_WEbt9eICqk&4Ei}RDOQ%&l z=*5Q(`JY<6r;|pPE}P*_g~H~pbpf!g+hG)pX8en>Mv6i1t0!VzMm}D< z7UR}SH2Q>z`zV6@ZrVKdVTXoaQ7Hl_!`bDt-1vVqhKkL->jX7ddN`m}#7792O20`U znk_9)lXba}CROlNvGm(6QSS|wDW;hNq0i|`M;a>*X7wq*AXFOqX2{9EZ*8{QMF5|` z?8V5;?6QLZZ7+6zC5IIh^?UEHr0I?Rec*QL#l1_RFVFM}e-elE zr>sh&{bBH(VCBqP?imf|8L!BA#tx^&M0lAN!3om6-YFaRca5QXobJe7`3P>*JS@_9 z34($V-Ezm7mxN@aTaZ=Z*JCFR;pWPd*{_}siPuqMqqUIAOi|@H)qL+6K7qY4 zP4_b7K3oT*d@i((^j)>hW#$Uc$&Vd`|1Gh)Qr;Owtl3m3U_vy(6I4zXY#<+jBYvy5F1%5<%JZS^*}yXT-exXPqY!86-x zPhfkil%ICbV-YRZ6+@6p31#nrxRq`$N9F4BF2vs&{mL_KfUaZWx7mo`y-g)>kKCk> zE7?UxsZ1=8c8rY*(tdv*$%kEO>|54jMl$zdPKcxKHgDJ#k{6rc&^&QQ3a`Q3c+Vof z{b#iHfetF=)a7fZczC3J4RlYM^Chi2vjTbmwq5@7$_o`@pQxP`0Qk6x{es}1E?2CO z2BonqA;=TwMCpF1X>Q3E9l;c@H2fuKLRc14b}=6vn-!N`)BNz*VD_2<&M8-6;{!a& zQL6~v7J0rf?wj1`wr*q9$s`oHDqma2BV;U`w&a^4sCv7V_sYjdTLy3Y;TrG7zBvf` zboNGOYv!AgT*HxP3nv$XU!}=8xp;}{0?0BC35lj zjm~1`RbP&W@Kn)UajS;gtR--Pq1xfDvKjP;hubG8#})L$JrHy|e!EKY?9iHCLqk~}ajXD5m()KMxN3E$udV;_#7Z?vbi~JIf+qV3-0zbQ9Ul3^ zhk6&EyWQ^xy{??NEj@z^y|%2EZ+TrazaOLtbdsngy2Beuo05lplD7un)SX>=fXBIr zw74-Zpp#K zDckOpS706(s}?gxN@OWl*KfLSt_YhL-*7UuQ1OxHqMc64kD%-O$9+T1kI6{DO%zSf-hgG-3A-{I$ z-%IUGyu=kox|{VOyvh7x=BL?jM;nUd2sS6p2+ORcCZQjYxAwLC+Bso~Cu6G;y(R`{ zPd?CCXkE6!mNFhvF)R7Q!w`cpQeTX+i#h;Q(B`x{4Hrh#EWJq;Tos|oxl;-u2Rg)B zrh1OZUb(Oe!nK&jCADO-&m&3rF4T~1S*@-ydKFGmIHTf+pCc{;9<Hp>>N0_3yWee}aFSD{zS356a)Vxnd4G$)%RyoCIZ01&KoO20CS1|@x{AW!1J z?7Xe;m!%)Fwk@X6s&t;PuA<16({NmwxPyz_)*;&i3+j|~(~_bY87!H;9FpubTuGrc zoAOTGpX9%JT83VQy)MwS^Sb{q`O7xx8BNYY%9##<2gZhj;Z!M;)ty1ux!Jl~9(5$% z)iE!JgTrod28i1#V2mp^Zf8BTp`9#O^m?8t{}g7@;ve%vW|HN^pf^gxsp__ZQaEkq zK!q%y1C?X^QmA5S`3SRnurb;Mx7{UzwoRM8$)zNNa$$BxUZ{2ND!%Su-8V+9?DD2O z&7mlfaz|adBCAA{C1#d5?0zguBLJV9&bZ*ADI5J(gFgU9O*_3b{0?&R*-D4@Klof) zKelLX_EN|8f0eOZL7xm~7%!<(@?qQ$FRg9@Uta8@hdpQ|TYa>txzXsn!YOVuRHhaaT?i89!m?#cEd>`v~t7 z%8;6Jm+=eYoO8j16U4&pFHu~Yys-Y$_V2!EB|WxRv9`9pcSH3qW?s(USs}rznr56P zRI)(LM2qcwE?V2W+ju39X#T`aruA5uT{h1t&uy99i!DzV(mx2$66^Xueqoykxl=*2 zzv0!K^(9>vsIQdDtz%9>n#yhA z?=DIb70&g4ZR?2D>^@Pj%JA zeS4^O(v-h)>GT4#TVDD=QJ-iJ;R#caN(r1jZHn6i4+|*6!c_N|51JPZllzMU7cY-5{kn`4*nor zk#$hrMCCt`tdp=?LC7?n!rWr4&NBZT?ZxUmw>kMr?)^yRUJC;K=9yK7&I`-B%*)!s z5tL7+ZCPuXgIM*Cc?Z3MWwRK~lf&o|>W$zM#> zUuewAFWeKj2=ao9O<8k6jbZD0HM%zX{OE}JB9pm4mDt2FHf?b*G&3;FbrF0XPVo;S zxvZscEvS03l|{fA%SnX>&(y0m;8qRgXJ8aUH0&+*r^_uv9z@tmNq{8~{bwxWQk|7B z--Lc`5vkE+shRPF4mD0ZCaye4A!fxE?Ck8cqsy#(@B3$qB88bDYu-AMNi0_f;_Npc z-#Eo%f0)RnI{-v?b$en%>7-@Kxu#I8wWa~1PmnL<&02n*DSgTHE;cuhFD2?s90ibT zM;r0WyoaRMD2BDQWy(%+{{mAzS1jcpjnsu%lkHD;1JL`M5)bHa>3dRzd`Gjqij7!V zMjk#wtav!;3hpGiF<#ur@|Ji&B z=6}meyI<<+iZ9~3kh9-PYC=>AWjqvjlWe}0z#y^!T+e3dS zh);k(8=l=ir%X0%4U^vo8ZogWU0QUW>hA`>gz-`2m~SYzl;L4~uT_rpcI&g{wa?>- zdn9gt+@g5rIoi);_%4XuY~E*QUL?htSQqG#Cl*C9Vpm0JJZ$j|>EZYW)a-Iq>dKal z4Y&pLMyc~-q*#FFy~>HjXBWhb`mW#fM*o*SZzF$#67EouK^n2-ff|`!cgX}`KiC(- zQ)9HPS;%aU17bYLL^xw=4S z-O(~qITlY%T+X8#37lvR7AWyan>N_UP|ml+dQ6l~XfTX1JUr(hyY12LxVAA&=CQOW zwCpP6?Piz!(xB$+Ie9G)UFL&{IDpX(NGWN^YIU-(hQC_bsW!Jg^Yne|%SD%Qz2@WB%X7bjU$6aZ#Fp#dYXa z_vHTw=c1H+^xEwLr|<7;voazK(ChyJ9sa++-RAk(CRG5)$C0jvoGV{D+1G!S{Y++pVA6}W4fbiGAMrF1Xp|dhT0{$!KrAPX z(#8>{d?0@4gf)=FD7gm#>*?zC3se1>Km*PJs(5qQsMO#6~Lp@)Z z6$jhQZ!S7lP^8VrCc$1Hba!Wn0wj)Y_*nocjr0E57E?!Enq}3Xu?M-C%L~YeL4jpx zcL+o`xWkN`2TL9PxfnS(vp8A;ynUo60>Q)FBJ&5uvN*qxzct}N-whNyF>mRfW_0au=wPFPdywnJco5u1w;^&CF@TwTempj>vyWS*$$Y?tNWA; z9fLk`L-{FlkZ2z`a3HhoF!SaLy=S&v*6nQ7tZyQNQX$hha*m$srlL@Rs@JZ+$QYI4HDIAaogv30UEB({~Qlt6pdRS7+aa?c8;n2BEihtfg zmZ~EEn3*!Y)zhTY)AFgrnaWZ=!fAG81OC8J#r^p>w?Bk9*KQ|C=aQ7U@&Nl?WtpeW!#*tn7AekcNEvag>gn{5E^#Ty%exr2OyJ5#~}2C zw?p#oz?Q980pP^t`v7lE{rWu-7>WR)uF4cHRK} z;Q+$wX|UI2AE)D>Kb1^j_5a}Byfn2=;Laju7JAemSG&M8p8iP5w_K5oU z(>*%8%O$c1)k_q}xzLlxtdH-NMCyY}|x z19!mEL&iI}oyZ+i!LaxJj^do&&$ncrXUsz_h4hsfor-Iqd947kV+#Yy`_tM+*EuM! z)4)(28EwdRrtj;9rD?xiAswT~*VNS1512WJ&m*<~Z~MW?*t7yx&xV83`L|kdmWxnK z`aU&5rdXk5^%OS)3_moQl$R#pp&p+#8t;@sb~6&og!>)PfJtxsk2941o@ETjkIz}M za_ZC03SeuFd%T^jlrTOqDAWxVV#nMvh=%%>CkWgT$bENCI?RFp6j=1%&rbSCh2$bQPk!*Hvybg&mw7m&TKl~!!ys73|QDR z+K&S5$468}gl7TvFQs)sX{i~%#Phx$*ms`Ie~=p3=qUvh5d)#N?@-)*m;EZN^#_@d z^_=%W;fRb6bW6q$xpJbgn%uhKJrBkEs`qvkKs;>)qy?`r5<6Hrn)7}v!FT+G1IS=k zpF9mJ!qqQ;WP&7^1&L{&{1dc^Jrkv1MC-bN){M`ys_w6Q$bPK`M!-+=a3d{z zxd^T%??_LWL9;){U-VQ2|9@1(W%~|ZUi+2(ljB6D2V6? z*UCYmi{t2mVkL7&P8?M9L-Ea>W8NK5frHB|B>W~%`n-XO_ou(z$%`l97xSd~s>gqF z*ou~@eQqGL`?%#M?^E6>+AgGa@9Z)vY@By`4ENrR?8vl3g*_F{a7&a5-k@{y1@Ajc z0`kS#b1%n!C#R=fa#VJual4m8JFRM2@wm9+c8xla(pwH_#6W~|?vKMH#|0%w`kc4o zBpz9qLXVWs8+gDP{IR`Pj&u40c}s$jwZ4MbFUga59koS|JR!6@N9`n%#u<9~;vNmn zaTgm1U&J*~4sKSE-0@kP3cq4dxLD<%duQ4Qu+4@O)(rka&Cd0kwtH1m#wZu%467y$ zAoqORdd=!ahIdmZFwEKxbpG;+c^Cj^V`|`0;H~-!pSo(>5uLp($12<0q?e_#Sk=2lPo}Kx*~a>J67Rb(e~T<-bzCGue=Fw zeu)Ha#gJ8ise69z|CXTH&3CW_f-COq8MygZLSrYaifLbIF<1(~m3I2ge6M+6oP0=U zevA04HFQ^7hxv74RY;dIulyuM3RbX4+=;71ZjmX<@OQ3n%2SyIpAV86i#pLDn_DoH zKX|ag2Uw#8=tV?Ki_;iHc_Hrvyhad8b9O7$rXc1zpfcYW(hh?ZSkA@7`+bUJU9ro1CbOXL!uw>8?7L_HW-btW54!VZUuR<|AvLi6jMz^PEG$ z#5}v}$tqz&wgF|boQ}mx+WvSrBqKn>jqFL0FdQbOXefbZi<}1c4Yd9{=a!oZgFpZZ z%+j`Gt|^2R%+d-Sv;f@6jJtzXZmKEDx`dg7vQj#NPS@GU!)~2L?;EHRYHBIJbZmr; zkuUIwdI4cD8%}1h-Hw|ePspl#i9bg^7@dHhc53+uLIL@ASoM(RJ9mMs@M?(sME;|y zqQX$DSh(o(COLit0_Zyc!`wP_CUqI&$VbKyHCZ0?3zQ}AZ?w$lF9$#z+WNcVzaPw#*-`n)w$xdI;A4j^=Q4N3ndO$}!U&0F4luxZzD z77bMYg<-LTvSA%?$~YoPQ)d8+7G^c6OvSY|>rhjhm-Uw&=Ba$^nh6HW=IrNCF`jEL zo0mr_Z9+s;9DbO5PeXs{L>l60Nr-Xn%S18h54KkvdL zVC#_>6~Jfeva-tv0_i>+dpf5wXYo(}bdTT$q2xt!_6$_5(dq{vy?w#tv~*e>7r;S;xcJ#E=zuo6*?l^GElNRWDR0ceY097e{yZ-A(+^|JpIoSbe;lh_E#b3 z(}xXWjo|B+k5Uh1^z^U%(d5d_9(UPSe&wr(Xc%+r7$dCuNapS#($iV!%suy9y}2>- z{vzQQ$V;++FodRMwG`U^?J!o@_abi!e<`$$PBB= z`BglIC^SCsI0F7q-C}!h_#xMtr{A{}kE8IeTMw7w8^@riJ?o&I0oV`^DZ9SwvCoi-h7SdkR zrY^InR&K1pLkUoOxC(}tE!-5Fm>vH zq-1_2}7OFG&6Yt=jF#6h<8PCWv9X_7WoQ&iv-94tFvUe$371fHJIzX#*<@UQqvLM4s`} z#?nsl8A!#&46d( zl;0P-o45!LIUhPK?`Pfp+^(sKZ9MO@&=R%X^>X?skH52WqWF)5N)0hx&Jp!(%Y{p0 zPD@V`xC1VIA44xJi=gn+AGi57bL8WKV$M=7KQd=MuY^_eqNb^p6CnP+9kX%RIaNaq z(}-LyTDvVpV)x;ojYTpwP+068y`hwL`a%>@%-NrokkbzZVLSn8hxE|uAfYb$IO~x0>t@(+PSft@^foV9QNZb z$ep_jGv4hb&uCVDGNUBkm~Z=1vC!}60_}S4jK=ime?oY3(UAH@UT!41A*dm4&bdnN zRqq@*$dWG+Kr^{HqUBM@ zZN62j#L8{|>1Dv3h~WCb;=Z+4u3K#a_e7vYc9Ws)5AC&m(Le%$%H z8DIpsC!CtMf`5kyYX1a6NC`x&6;B{Dde`&}hsH;{t>V%1{yp27j|P2WoeC@%>PCX2 z7grgESK)%(P}<%FF|E}caZXD`1=Rkh37Jp~-D--URVz&}=(hrmO*XX)Ic%Qjh^l8< z;OO%q=e@@fFcEnk1nGrMZD7&zf0o?R8f3XZ*Jx@u1fW7Uv^D*%FikDT&c)2#O$B+p z=y(9jFWB?vm6ydd5{Xp$$CeuD9CP>pmUeSkc>o!pOFJYW{Q&-&?Z}A<2F;--zY#|0 zchoNCU3$ZDn7a#?I$>8?k6KUQ>Q06WBWnx=cJ}zfb`D{yRbx;}y&_YDN1?DTyzdvX zT{Q;hi+|K1^8s<0(SVkhEoXrcvT(S&-lCMgeU^=nb7rY>UG)@HL{iy(gv=q+NgwBH zd0&#}iV4!E*naH?K{!b{kHdC!%4OJPyddrKRb^DjH5!D8+_W6i~ZZQVd^ zi?@*wwz4RZ>G7wVB=goIs6(cmq)O}r#c2iHun#-YE$UI&GNnCF^Ct;o!q1P@8&khI zYeK;E&&+hIPRbj-%-eqmU7cQZYz2xnuT4NB+5{KQ0B=KF7E4T_{y<@8pBw4VTIe+p zA||yyZjC~j$xy%M%?%Ypx-;;APeRtH#^}7lX3V_5>f$J!WSlhV{HyFPnyfs;it{@7Txjo*#)mN(uHJ>O&;{=0 z;Lr7>YyG3CsX1!E1q8x;;FV%gPtf}t!eNW6qk7Jui4A@~D|o+Z3}JGIpGjNdZiP0G z`Se#;dqmD4qx;5$0cgGzroAiS`J7Pl!)$U6I*1v>#2&tMs%HdoaqgB#%Q)IVJ_D;% z{6OA{DfYLnCWXR!;qE6vktydU;ae2ksSzr1rjTFdRLG?&a2Y3Q7xHgP4--fjh;_~4 z_e=OBCa(WN$`88Wml*W`0OSPx;fjMk$!DGgzQTv>^7TYUL_7(i41|2uU?z$QDBb|G zb(9j;fO?AjFm$Q^EQq2r14(iY!_uk3SY8vTK9~&BxXpQYmP%BLeuo{pa}b8V)6>E5}|v{~1VeI-v4 z4H^utfmv)GD+(FdAlZJiC1Vqdw+N5y^+eM?FGT_5`(l@Y$g!@O^Rt^F5N zf;xD6=h%4SZs!b_rVMsEBg}%|tXl$`J4-X)?&k6~;3`YPwBfM2&#)Ph>B8C97l8Ci zY`7iA3-C9bhlvuW%U?7z&YCslVa}h7b)&TO&n_`8G4tDC6jpH?n|Ca!^w>zu7o4?e zt!SD_Nif;?NnjYl>j9~WG^`XY)uubJ@O1N&W81Lv^ochC0oXArOp>3+T2{`}`t6vj z(_&V$-ZG2no<*?CM5X0yWQr@xs%hfZNzhM4xgMeYv8dfzZo%?RW(M*tA9ikc2dJS- z{#k4RrFMOQ`jB#(y|vyhUnQ0iO{Y;qmh!V$?1!tS&|8gkXKL&w3L}^3NV$hE`aK%k zL}r9Wc&i!>bM;In4jiB)cCSWmh@WY)_rl(@0gK$8_5%P-#qrd0dmb8Tn=I&>$>h5P ztN?`a;_JA{WRIY`P@d3enh^hRQi|~RyyTaaFVEdIxjzEkSqDov%xlK!AS|i)37Q2> z(cK$&sWy-FCJG4|%K0@PHmUv47w`v}ff?EjJH34e_k0e9tEf2p@%T~(pIlc(7 z^@O>f*X#x5_NBM$Dg>YSEOMnq#LPyztPb`k%>O&U!V2Ii0T)A_xEeXr@Ib0YGqf^(P}ueySRWY z|E|)y{1>`jPZINC>W;}LS3RH{Nj6}+ zG_NC + + + + + + + 2026-07-08T13:23:55.421448 + image/svg+xml + + + Matplotlib v3.11.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Q + n + = + 0 + = + 0 + + + + + + + + + + + + + + + + ̄ + = + Q + Q + n + n + = + 0 + = + 1 + + + + + + + + + + + + + + + + ̄ + = + Q + Q + n + n + = + 1 + = + 2 + + + + + + + + + + + + + + + + ̄ + Q + n + = + 2 + + + + + + + + + + + + + + + Q + p + + + + + + + + + Q + u + a + n + t + i + t + y +   +   +   +   + ( + c + a + p + a + c + i + t + y +   + o + r +   + a + c + t + i + v + i + t + y + ) + Q + + + + + + + + + + + + + + + + + + + C + n + = + 0 + = + 0 + + + + + + + + + + + + + + + + ̄ + = + C + C + n + n + = + 0 + = + 1 + + + + + + + + + + + + + + + + ̄ + = + C + C + n + n + = + 1 + = + 2 + + + + + + + + + + + + + + + + ̄ + C + n + = + 2 + + + + + + + + + T + o + t + a + l +   + c + o + s + t +   +   + C + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + m + n + C + Q + = +   +   + ( + s + l + o + p + e + ) + Δ + Δ + + + + + + + + + n + = + 0 + + + + + + + + n + = + 1 + + + + + + + + n + = + 2 + + + + + + + + active segment + + + + b + n + = + 1 + = + 1 + + + + + EOS piecewise-linear cost curve + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A + c + t + i + v + e +   + s + e + g + m + e + n + t +   + i + n +   + p + e + r + i + o + d +   + p + + + + + + + + Active segment cost line + + + + + + Inactive segment cost line + + + + + + + + + Q + p +   + ( + c + u + r + r + e + n + t +   + p + e + r + i + o + d + ) + + + + + + + + + + + + diff --git a/docs/source/images/etl_cost_curve.png b/docs/source/images/etl_cost_curve.png new file mode 100644 index 0000000000000000000000000000000000000000..af24dd9142ab064933af49ca07afb1bb9e3ef9c7 GIT binary patch literal 75182 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 + + + + + + + 2026-07-07T18:07:34.827185 + image/svg+xml + + + Matplotlib v3.11.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + + + + + + + + + + + ̄ + = + K + K + t + t + , + 1 + , + 2 + + + + + + + + + + + + + + + + ̄ + = + K + K + t + t + , + 2 + , + 3 + + + + + + + + + + + + + + + + ̄ + K + t + , + 3 + + + + + + + + + + + + + + + E + T + L + C + A + P + p + + + + + + + + + C + u + m + u + l + a + t + i + v + e +   + i + n + s + t + a + l + l + e + d +   + c + a + p + a + c + i + t + y +   +   + K + + + + + + + + + + + + + + + + 0 + + + + + + + + + + + + + C + t + , + 1 + + + + + + + + + + + + + + + + C + t + , + 2 + + + + + + + + + + + + + + + + C + t + , + 3 + + + + + + + + + + T + o + t + a + l +   + i + n + v + e + s + t + m + e + n + t +   + c + o + s + t +   +   + C + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + m + t + n + C + K + , + Δ + Δ + = +   +   + ( + s + l + o + p + e + ) + + + + + + + + + n + = + 1 + + + + + + + + n + = + 2 + + + + + + + + n + = + 3 + + + + + + + + active + segment + + + ETL piecewise-linear cost curve + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A + c + t + i + v + e +   + s + e + g + m + e + n + t +   + i + n +   + p + e + r + i + o + d +   + p + + + + + + + + Active segment cost line + + + + + + Inactive segment cost line + + + + + + + + + E + T + L + C + A + P + p +   + ( + c + u + r + r + e + n + t +   + p + e + r + i + o + d + ) + + + + + + + + + + + + diff --git a/scripts/generate_etl_figure.py b/scripts/generate_etl_figure.py new file mode 100644 index 00000000..20079f58 --- /dev/null +++ b/scripts/generate_etl_figure.py @@ -0,0 +1,223 @@ +""" +Generates docs/source/images/eos_cost_curve.{png,svg} — a labelled diagram +of the piecewise-linear cost curve shared by all three features of the +economies of scale (EOS) extension: cost_invest_eos, cost_fixed_eos, and +cost_variable_eos. +""" + +from __future__ import annotations + +from pathlib import Path + +import matplotlib +import matplotlib.patches as mpatches +import matplotlib.pyplot as plt +import matplotlib.ticker as ticker + +matplotlib.rcParams.update( + { + 'font.family': 'sans-serif', + 'font.size': 10, + 'axes.spines.top': False, + 'axes.spines.right': False, + 'pdf.fonttype': 42, + 'svg.fonttype': 'none', + } +) + +OUT_DIR = Path(__file__).parent.parent / 'docs' / 'source' / 'images' + +# ——— Segment data (decreasing marginal cost — economies of scale) ————————————— +# Each entry: (q_lower, q_upper, cost_lower, cost_upper) +segments = [ + (0.0, 2.0, 0.0, 6.0), # n=1 slope = 3.0 + (2.0, 5.0, 6.0, 12.0), # n=2 slope = 2.0 + (5.0, 9.0, 12.0, 16.0), # n=3 slope = 1.0 +] + +PALETTE = { + 'curve': '#1f1f1f', + 'active': '#d44040', + 'inactive': '#aaaaaa', + 'fill': '#fde8e8', + 'grid': '#e8e8e8', + 'annot': '#555555', + 'tick': '#333333', +} + +# Active segment (0-indexed) — the one the optimizer has selected in this period +ACTIVE = 1 +# Quantity realised in this period (sits inside the active segment) +Q_P = 3.5 + +fig, ax = plt.subplots(figsize=(8.0, 5.0)) +fig.patch.set_facecolor('white') +ax.set_facecolor('white') + +# ——— Shade the active segment background —————————————————————————————————————— +ql_a, qu_a, cl_a, cu_a = segments[ACTIVE] +ax.axvspan(ql_a, qu_a, color=PALETTE['fill'], zorder=0, linewidth=0) + +# ——— Draw all segment lines ———————————————————————————————————————————————————— +for i, (ql, qu, cl, cu) in enumerate(segments): + color = PALETTE['active'] if i == ACTIVE else PALETTE['curve'] + lw = 2.2 if i == ACTIVE else 1.6 + ax.plot([ql, qu], [cl, cu], color=color, linewidth=lw, solid_capstyle='round', zorder=3) + dot_color = PALETTE['active'] if i == ACTIVE else PALETTE['curve'] + ax.plot(ql, cl, 'o', ms=5, color=dot_color, zorder=4) + ax.plot(qu, cu, 'o', ms=5, color=dot_color, zorder=4) + +# ——— Vertical line at current-period quantity ————————————————————————————————— +slope_a = (cu_a - cl_a) / (qu_a - ql_a) +cost_p = cl_a + slope_a * (Q_P - ql_a) + +# ——— Capacity/activity bound tick labels on x-axis ——————————————————————————— +boundaries = [] +for i, (ql, _qu, _cl, _cu) in enumerate(segments): + n = i # 0-based + if ql == 0: + boundaries.append((ql, r'$\underline{Q}_{n=0} = 0$')) + else: + boundaries.append((ql, rf'$\bar{{Q}}_{{n={n - 1}}} = \underline{{Q}}_{{n={n}}}$')) + +_, qu_last, _, _ = segments[-1] +boundaries.append((qu_last, rf'$\bar{{Q}}_{{n={len(segments) - 1}}}$')) + +x_ticks = [b[0] for b in boundaries] + [Q_P] +x_labels = [b[1] for b in boundaries] + [r'$\mathbf{Q}_p$'] + +ax.set_xticks(x_ticks) +ax.set_xticklabels(x_labels, fontsize=8.5, color=PALETTE['tick']) + +# ——— Cost bound tick labels on y-axis ———————————————————————————————————————— +y_boundaries = [] +for i, (_ql, _qu, cl, _cu) in enumerate(segments): + n = i # 0-based + if cl == 0: + y_boundaries.append((cl, r'$\underline{C}_{n=0} = 0$')) + else: + # shared upper bound of prev segment / lower bound of this segment + y_boundaries.append((cl, rf'$\bar{{C}}_{{n={n - 1}}} = \underline{{C}}_{{n={n}}}$')) + +_, _, _, cu_last = segments[-1] +y_boundaries.append((cu_last, rf'$\bar{{C}}_{{n={len(segments) - 1}}}$')) + +y_ticks = [b[0] for b in y_boundaries] +y_labels = [b[1] for b in y_boundaries] + +ax.set_yticks(y_ticks) +ax.set_yticklabels(y_labels, fontsize=8.5, color=PALETTE['tick']) + +# ——— Dashed reference lines to axes from segment endpoints ——————————————————— +dash_kw = {'color': PALETTE['inactive'], 'linewidth': 0.8, 'linestyle': ':', 'zorder': 1} +for ql, qu, cl, cu in segments: + ax.plot([ql, ql], [0, cl], **dash_kw) + ax.plot([qu, qu], [0, cu], **dash_kw) + ax.plot([0, ql], [cl, cl], **dash_kw) + ax.plot([0, qu], [cu, cu], **dash_kw) + +# dashed lines from Q_P up to cost curve, then across to y-axis +ax.plot([Q_P, Q_P], [0, cost_p], color=PALETTE['active'], linewidth=0.9, linestyle='--', zorder=2) +ax.plot( + [0, Q_P], [cost_p, cost_p], color=PALETTE['active'], linewidth=0.9, linestyle='--', zorder=2 +) +ax.plot(0, cost_p, '<', ms=5, color=PALETTE['active'], zorder=4, clip_on=False) +ax.plot(Q_P, 0, 'v', ms=5, color=PALETTE['active'], zorder=4, clip_on=False) + +# ——— Slope annotation on active segment —————————————————————————————————————— +mid_q = (ql_a + qu_a) / 2 +mid_c = cl_a + slope_a * (mid_q - ql_a) +dq, dc = 0.6, slope_a * 0.6 +ax.annotate( + '', + xy=(mid_q + dq, mid_c + dc), + xytext=(mid_q + dq, mid_c), + arrowprops={'arrowstyle': '-', 'color': PALETTE['annot'], 'lw': 0.9}, +) +ax.annotate( + '', + xy=(mid_q + dq, mid_c), + xytext=(mid_q, mid_c), + arrowprops={'arrowstyle': '-', 'color': PALETTE['annot'], 'lw': 0.9}, +) +ax.text( + mid_q + dq + 0.08, + mid_c + dc / 2, + r'$m_n = \frac{\Delta C}{\Delta Q}$ (slope)', + va='center', + ha='left', + fontsize=8.5, + color=PALETTE['annot'], +) + +# ——— Segment labels ——————————————————————————————————————————————————————————— +for i, (ql, qu, cl, cu) in enumerate(segments): + n = i # 0-based + mid_q = (ql + qu) / 2 + mid_c = (cl + cu) / 2 + color = PALETTE['active'] if i == ACTIVE else PALETTE['annot'] + weight = 'bold' if i == ACTIVE else 'normal' + ax.text( + mid_q, + mid_c + 0.7, + f'$n={n}$', + ha='center', + va='bottom', + fontsize=9, + color=color, + fontweight=weight, + ) + +# ——— Active-segment label ————————————————————————————————————————————————————— +ax.text( + (ql_a + qu_a) / 2, + 1.5, + 'active segment\n' r'$b_{n=1} = 1$', + ha='center', + va='bottom', + fontsize=8.5, + color=PALETTE['active'], + bbox={'boxstyle': 'round,pad=0.3', 'fc': PALETTE['fill'], 'ec': PALETTE['active'], 'lw': 0.8}, +) + +# ——— Axes formatting —————————————————————————————————————————————————————————— +ax.set_xlim(-0.3, 10.2) +ax.set_ylim(-0.3, 18.5) +ax.set_xlabel('Quantity $Q$ (capacity or activity)', labelpad=8) +ax.set_ylabel('Total cost $C$', labelpad=8) +ax.set_title('EOS piecewise-linear cost curve', pad=10, fontsize=11) + +ax.xaxis.set_minor_locator(ticker.NullLocator()) +ax.yaxis.set_minor_locator(ticker.NullLocator()) + +# ——— Legend ——————————————————————————————————————————————————————————————————— +handles = [ + mpatches.Patch( + facecolor=PALETTE['fill'], + edgecolor=PALETTE['active'], + lw=0.8, + label='Active segment in period $p$', + ), + plt.Line2D([0], [0], color=PALETTE['active'], lw=2.2, label='Active segment cost line'), + plt.Line2D([0], [0], color=PALETTE['curve'], lw=1.6, label='Inactive segment cost line'), + plt.Line2D( + [0], + [0], + color=PALETTE['active'], + lw=1.2, + linestyle='--', + label=r'$\mathbf{Q}_p$ (current period)', + ), +] +ax.legend( + handles=handles, fontsize=8, loc='upper left', frameon=True, framealpha=0.9, edgecolor='#cccccc' +) + +fig.tight_layout() + +for ext in ('png', 'svg'): + out = OUT_DIR / f'eos_cost_curve.{ext}' + fig.savefig(out, dpi=150, bbox_inches='tight', facecolor='white') + print(f'Saved {out}') + +plt.close(fig) From 3123bf9a8127fa44b5873d8010dbcfbe2d86a9ca Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Wed, 8 Jul 2026 17:48:24 -0400 Subject: [PATCH 26/28] Cleanup for Bugs Signed-off-by: Davey Elder --- scripts/generate_etl_figure.py | 8 ++- temoa/_internal/run_actions.py | 3 +- .../economies_of_scale/core/data_puller.py | 50 ++++++++----------- .../economies_of_scale/core/model.py | 2 + temoa/extensions/method_of_morris/morris.py | 2 - tests/test_table_writer.py | 2 +- 6 files changed, 34 insertions(+), 33 deletions(-) diff --git a/scripts/generate_etl_figure.py b/scripts/generate_etl_figure.py index 20079f58..4e42ee48 100644 --- a/scripts/generate_etl_figure.py +++ b/scripts/generate_etl_figure.py @@ -8,6 +8,7 @@ from __future__ import annotations from pathlib import Path +from typing import Any import matplotlib import matplotlib.patches as mpatches @@ -109,7 +110,12 @@ ax.set_yticklabels(y_labels, fontsize=8.5, color=PALETTE['tick']) # ——— Dashed reference lines to axes from segment endpoints ——————————————————— -dash_kw = {'color': PALETTE['inactive'], 'linewidth': 0.8, 'linestyle': ':', 'zorder': 1} +dash_kw: dict[str, Any] = { + 'color': PALETTE['inactive'], + 'linewidth': 0.8, + 'linestyle': ':', + 'zorder': 1, +} for ql, qu, cl, cu in segments: ax.plot([ql, ql], [0, cl], **dash_kw) ax.plot([qu, qu], [0, cu], **dash_kw) diff --git a/temoa/_internal/run_actions.py b/temoa/_internal/run_actions.py index 6aecc7dc..7e7b5ec5 100644 --- a/temoa/_internal/run_actions.py +++ b/temoa/_internal/run_actions.py @@ -247,7 +247,8 @@ def solve_instance( # it also doesn't seem to support duals properly so disable those if solver_name == 'appsi_highs': dual_suffix = instance.component('dual') - dual_suffix._direction = Suffix.LOCAL + if dual_suffix is not None: + dual_suffix.direction = Suffix.LOCAL result = optimizer.solve(instance) else: result = optimizer.solve(instance, suffixes=solver_suffixes_list) diff --git a/temoa/extensions/economies_of_scale/core/data_puller.py b/temoa/extensions/economies_of_scale/core/data_puller.py index 2713483e..d34a6279 100644 --- a/temoa/extensions/economies_of_scale/core/data_puller.py +++ b/temoa/extensions/economies_of_scale/core/data_puller.py @@ -98,21 +98,17 @@ def poll_costs( else: # The period `p` for an investment cost is its vintage `v`. key = (cast('Region', r), cast('Period', p), cast('Technology', t), cast('Vintage', p)) - if CostType.INVEST in entries.get(key, {}): - entries[key][CostType.D_INVEST] += discounted_cost - entries[key][CostType.INVEST] += undiscounted_cost - else: - entries[key].update( - {CostType.D_INVEST: discounted_cost, CostType.INVEST: undiscounted_cost} - ) + entry = entries[key] + entry[CostType.D_INVEST] = entry.get(CostType.D_INVEST, 0.0) + discounted_cost + entry[CostType.INVEST] = entry.get(CostType.INVEST, 0.0) + undiscounted_cost for r, p, t in model.cost_fixed_eos_period_rpt: fixed_cost = value(cost_fixed_eos.period_cost(model, r, p, t)) if fixed_cost < epsilon: continue - undiscounted_fixed_cost = fixed_cost * value(model.period_length[p]) - discounted_fixed_cost = costs.fixed_or_variable_cost( + ud_fixed = float(fixed_cost * value(model.period_length[p])) + d_fixed = costs.fixed_or_variable_cost( 1, fixed_cost, value(model.period_length[p]), @@ -120,6 +116,7 @@ def poll_costs( p_0=p_0, p=p, ) + d_fixed = float(value(d_fixed)) if '-' in r: exchange_costs.add_cost_record( @@ -127,7 +124,7 @@ def poll_costs( period=p, tech=t, vintage=p, - cost=float(value(discounted_fixed_cost)), + cost=d_fixed, cost_type=CostType.D_FIXED, ) exchange_costs.add_cost_record( @@ -135,24 +132,22 @@ def poll_costs( period=p, tech=t, vintage=p, - cost=float(undiscounted_fixed_cost), + cost=ud_fixed, cost_type=CostType.FIXED, ) else: - entries[r, p, t, p].update( - { - CostType.D_FIXED: float(value(discounted_fixed_cost)), - CostType.FIXED: float(undiscounted_fixed_cost), - } - ) + key = (cast('Region', r), cast('Period', p), cast('Technology', t), cast('Vintage', p)) + entry = entries[key] + entry[CostType.D_FIXED] = entry.get(CostType.D_FIXED, 0.0) + d_fixed + entry[CostType.FIXED] = entry.get(CostType.FIXED, 0.0) + ud_fixed - for r, p, t in model.cost_fixed_eos_period_rpt: + for r, p, t in model.cost_variable_eos_period_rpt: variable_cost = value(cost_variable_eos.period_cost(model, r, p, t)) if variable_cost < epsilon: continue - undiscounted_variable_cost = variable_cost * value(model.period_length[p]) - discounted_variable_cost = costs.fixed_or_variable_cost( + ud_variable = float(variable_cost * value(model.period_length[p])) + d_variable = costs.fixed_or_variable_cost( 1, variable_cost, value(model.period_length[p]), @@ -160,6 +155,7 @@ def poll_costs( p_0=p_0, p=p, ) + d_variable = float(value(d_variable)) if '-' in r: exchange_costs.add_cost_record( @@ -167,7 +163,7 @@ def poll_costs( period=p, tech=t, vintage=p, - cost=float(value(discounted_variable_cost)), + cost=d_variable, cost_type=CostType.D_VARIABLE, ) exchange_costs.add_cost_record( @@ -175,13 +171,11 @@ def poll_costs( period=p, tech=t, vintage=p, - cost=float(undiscounted_variable_cost), + cost=ud_variable, cost_type=CostType.VARIABLE, ) else: - entries[r, p, t, p].update( - { - CostType.D_VARIABLE: float(value(discounted_variable_cost)), - CostType.VARIABLE: float(undiscounted_variable_cost), - } - ) + key = (cast('Region', r), cast('Period', p), cast('Technology', t), cast('Vintage', p)) + entry = entries[key] + entry[CostType.D_VARIABLE] = entry.get(CostType.D_VARIABLE, 0.0) + d_variable + entry[CostType.VARIABLE] = entry.get(CostType.VARIABLE, 0.0) + ud_variable diff --git a/temoa/extensions/economies_of_scale/core/model.py b/temoa/extensions/economies_of_scale/core/model.py index 82c0d0e2..6b1298a4 100644 --- a/temoa/extensions/economies_of_scale/core/model.py +++ b/temoa/extensions/economies_of_scale/core/model.py @@ -169,6 +169,8 @@ def register_model_components(model: TemoaModel) -> None: rule=cost_fixed_eos.cost_fixed_eos_capacity_upper_bound_constraint, ) + m.append_cost_fixed_eos_to_total_cost = BuildAction(rule=cost_fixed_eos.total_cost) + # -- cost_variable_eos --- m.cost_variable_eos_segments = {} diff --git a/temoa/extensions/method_of_morris/morris.py b/temoa/extensions/method_of_morris/morris.py index dd981d79..4ae1f308 100644 --- a/temoa/extensions/method_of_morris/morris.py +++ b/temoa/extensions/method_of_morris/morris.py @@ -47,8 +47,6 @@ def evaluate( with TableWriter(config) as table_writer: table_writer.write_results(model=mdl) - import contextlib - with contextlib.closing(sqlite3.connect(db_file)) as con: cur = con.cursor() cur.execute('SELECT * FROM output_objective') diff --git a/tests/test_table_writer.py b/tests/test_table_writer.py index 646c0a47..fdfe741f 100644 --- a/tests/test_table_writer.py +++ b/tests/test_table_writer.py @@ -9,7 +9,7 @@ class LoanCostInput(TypedDict): capacity: float invest_cost: float loan_life: float - loan_rate: float + loan_annualize: float global_discount_rate: float process_life: int p_0: int From 04f9a16ae297aae045d27783e1bd2b2bc3c8c2ca Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Wed, 8 Jul 2026 21:00:46 -0400 Subject: [PATCH 27/28] Add economies of scale and discrete capacities to myopic capacities test Signed-off-by: Davey Elder --- tests/test_full_runs.py | 31 +++++- .../config_myopic_capacities.toml | 2 +- tests/testing_data/myopic_capacities.sql | 95 +++++++++++++++++++ 3 files changed, 123 insertions(+), 5 deletions(-) diff --git a/tests/test_full_runs.py b/tests/test_full_runs.py index c71f9b4c..9956449e 100644 --- a/tests/test_full_runs.py +++ b/tests/test_full_runs.py @@ -171,18 +171,41 @@ def test_myopic_stress_tests( ) -> None: """ The idea of these is that they should be tightly constrained so that if anything - is wrong the model will fail to find a feasible solution. Use lots of equality constraints + is wrong the model will fail to find a feasible solution. Use lots of equality constraints. + + Two new paths test limit_discrete_capacity and limit_discrete_new_capacity with EoS costs + and survival curves. """ _, _, _, sequencer = system_test_run import contextlib with contextlib.closing(sqlite3.connect(sequencer.config.output_database)) as con: cur = con.cursor() + dc_fixed = cur.execute( + "SELECT SUM(fixed) FROM output_cost WHERE tech='tech_discrete_cap'" + ).fetchone()[0] + dc_variable = cur.execute( + "SELECT SUM(var) FROM output_cost WHERE tech='tech_discrete_cap'" + ).fetchone()[0] + dnc_invest = cur.execute( + "SELECT SUM(invest) FROM output_cost WHERE tech='tech_discrete_new_cap'" + ).fetchone()[0] + + assert dc_fixed == pytest.approx(37, rel=1e-5), ( + 'tech_discrete_cap fixed cost did not match expected' + ) + assert dc_variable == pytest.approx(24, rel=1e-5), ( + 'tech_discrete_cap variable cost did not match expected' + ) + assert dnc_invest == pytest.approx(2.5, rel=1e-5), ( + 'tech_discrete_new_cap invest cost did not match expected' + ) + + # This part is just a very rough check on the objective function. Constraints inside the + # model are extremely tight so other changes will likely lead to infeasibility res = cur.execute('SELECT SUM(total_system_cost) FROM main.output_objective').fetchone() obj = res[0] - # This part is just a very rough check on the objective function. Constraints inside the - # model are extremely tight so any other changes will lead to infeasibility - assert obj == pytest.approx(32, abs=1), ( + assert obj == pytest.approx(309, rel=0.01), ( 'objective function value did not match expected for myopic stress test' ) diff --git a/tests/testing_configs/config_myopic_capacities.toml b/tests/testing_configs/config_myopic_capacities.toml index db2d1153..4ff76c5b 100644 --- a/tests/testing_configs/config_myopic_capacities.toml +++ b/tests/testing_configs/config_myopic_capacities.toml @@ -1,6 +1,6 @@ scenario = "test myopic capacities" scenario_mode = "myopic" -extensions= ["growth_rates"] +extensions= ["growth_rates", "discrete_capacity", "eos"] input_database = "tests/testing_outputs/myopic_capacities.sqlite" output_database = "tests/testing_outputs/myopic_capacities.sqlite" neos = false diff --git a/tests/testing_data/myopic_capacities.sql b/tests/testing_data/myopic_capacities.sql index f561e073..ca11c415 100644 --- a/tests/testing_data/myopic_capacities.sql +++ b/tests/testing_data/myopic_capacities.sql @@ -240,3 +240,98 @@ REPLACE INTO technology VALUES('tech_current','p','energy',NULL,NULL,0,0,0,0,1,0 REPLACE INTO technology VALUES('tech_future','p','energy',NULL,NULL,0,0,0,0,1,0,0,0,NULL); REPLACE INTO technology VALUES('tech_retire','p','energy',NULL,NULL,0,0,0,0,1,0,0,0,NULL); REPLACE INTO time_season VALUES(0,'s',1.0,NULL); +REPLACE INTO commodity VALUES('demand_dc','d',NULL,NULL); +REPLACE INTO commodity VALUES('demand_dnc','d',NULL,NULL); +REPLACE INTO technology VALUES('tech_discrete_cap','p','energy',NULL,NULL,0,0,0,0,1,0,0,0,NULL); +REPLACE INTO technology VALUES('tech_dc_dummy','p','energy',NULL,NULL,1,0,0,0,0,0,0,0,NULL); +REPLACE INTO technology VALUES('tech_discrete_new_cap','p','energy',NULL,NULL,0,0,0,0,1,0,0,0,NULL); +REPLACE INTO technology VALUES('tech_dnc_dummy','p','energy',NULL,NULL,1,0,0,0,0,0,0,0,NULL); +REPLACE INTO lifetime_tech VALUES('region','tech_discrete_cap',35.0,NULL,NULL); +REPLACE INTO lifetime_tech VALUES('region','tech_dc_dummy',35.0,NULL,NULL); +REPLACE INTO lifetime_tech VALUES('region','tech_discrete_new_cap',5.0,NULL,NULL); +REPLACE INTO lifetime_tech VALUES('region','tech_dnc_dummy',35.0,NULL,NULL); +REPLACE INTO loan_rate VALUES('region','tech_discrete_new_cap',2025,0.0,NULL); +REPLACE INTO loan_rate VALUES('region','tech_discrete_new_cap',2030,0.0,NULL); +REPLACE INTO loan_rate VALUES('region','tech_discrete_new_cap',2035,0.0,NULL); +REPLACE INTO loan_rate VALUES('region','tech_discrete_new_cap',2040,0.0,NULL); +REPLACE INTO loan_rate VALUES('region','tech_discrete_new_cap',2045,0.0,NULL); +REPLACE INTO loan_rate VALUES('region','tech_discrete_new_cap',2050,0.0,NULL); +REPLACE INTO efficiency VALUES('region','source','tech_discrete_cap',2025,'demand_dc',1.0,NULL,NULL); +REPLACE INTO efficiency VALUES('region','source','tech_dc_dummy',2025,'demand_dc',1.0,NULL,NULL); +REPLACE INTO efficiency VALUES('region','source','tech_discrete_new_cap',2025,'demand_dnc',1.0,NULL,NULL); +REPLACE INTO efficiency VALUES('region','source','tech_discrete_new_cap',2030,'demand_dnc',1.0,NULL,NULL); +REPLACE INTO efficiency VALUES('region','source','tech_discrete_new_cap',2035,'demand_dnc',1.0,NULL,NULL); +REPLACE INTO efficiency VALUES('region','source','tech_discrete_new_cap',2040,'demand_dnc',1.0,NULL,NULL); +REPLACE INTO efficiency VALUES('region','source','tech_discrete_new_cap',2045,'demand_dnc',1.0,NULL,NULL); +REPLACE INTO efficiency VALUES('region','source','tech_discrete_new_cap',2050,'demand_dnc',1.0,NULL,NULL); +REPLACE INTO efficiency VALUES('region','source','tech_dnc_dummy',2025,'demand_dnc',1.0,NULL,NULL); +REPLACE INTO demand VALUES('region',2025,'demand_dc',2.0,NULL,NULL); +REPLACE INTO demand VALUES('region',2030,'demand_dc',2.0,NULL,NULL); +REPLACE INTO demand VALUES('region',2035,'demand_dc',2.0,NULL,NULL); +REPLACE INTO demand VALUES('region',2040,'demand_dc',2.0,NULL,NULL); +REPLACE INTO demand VALUES('region',2045,'demand_dc',2.0,NULL,NULL); +REPLACE INTO demand VALUES('region',2050,'demand_dc',2.0,NULL,NULL); +REPLACE INTO demand VALUES('region',2025,'demand_dnc',1.0,NULL,NULL); +REPLACE INTO demand VALUES('region',2030,'demand_dnc',1.0,NULL,NULL); +REPLACE INTO demand VALUES('region',2035,'demand_dnc',1.0,NULL,NULL); +REPLACE INTO demand VALUES('region',2040,'demand_dnc',1.0,NULL,NULL); +REPLACE INTO demand VALUES('region',2045,'demand_dnc',1.0,NULL,NULL); +REPLACE INTO demand VALUES('region',2050,'demand_dnc',1.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2025,'tech_dc_dummy',2025,10.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2030,'tech_dc_dummy',2025,10.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2035,'tech_dc_dummy',2025,10.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2040,'tech_dc_dummy',2025,10.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2045,'tech_dc_dummy',2025,10.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2050,'tech_dc_dummy',2025,10.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2025,'tech_dnc_dummy',2025,10.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2030,'tech_dnc_dummy',2025,10.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2035,'tech_dnc_dummy',2025,10.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2040,'tech_dnc_dummy',2025,10.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2045,'tech_dnc_dummy',2025,10.0,NULL,NULL); +REPLACE INTO cost_variable VALUES('region',2050,'tech_dnc_dummy',2025,10.0,NULL,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2025,'tech_discrete_cap',2025,1.0,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2030,'tech_discrete_cap',2025,0.8,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2035,'tech_discrete_cap',2025,0.6,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2040,'tech_discrete_cap',2025,0.4,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2045,'tech_discrete_cap',2025,0.2,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2060,'tech_discrete_cap',2025,0.0,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2025,'tech_discrete_new_cap',2025,1.0,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2030,'tech_discrete_new_cap',2025,0.0,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2030,'tech_discrete_new_cap',2030,1.0,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2035,'tech_discrete_new_cap',2030,0.0,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2035,'tech_discrete_new_cap',2035,1.0,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2040,'tech_discrete_new_cap',2035,0.0,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2040,'tech_discrete_new_cap',2040,1.0,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2045,'tech_discrete_new_cap',2040,0.0,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2045,'tech_discrete_new_cap',2045,1.0,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2050,'tech_discrete_new_cap',2045,0.0,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2050,'tech_discrete_new_cap',2050,1.0,NULL); +REPLACE INTO lifetime_survival_curve VALUES('region',2055,'tech_discrete_new_cap',2050,0.0,NULL); +REPLACE INTO limit_discrete_capacity VALUES('region','tech_discrete_cap',0.2,NULL,NULL); +REPLACE INTO limit_discrete_new_capacity VALUES('region','tech_discrete_new_cap',0.2,NULL,NULL); +REPLACE INTO cost_invest_eos VALUES('region','tech_discrete_new_cap',1,0.0,0.5,0.0,1.0,NULL,NULL); +REPLACE INTO cost_invest_eos VALUES('region','tech_discrete_new_cap',2,0.5,2.0,1.0,2.5,NULL,NULL); +REPLACE INTO cost_fixed_eos VALUES('region',2025,'tech_discrete_cap',1,0.0,0.5,0.0,1.0,NULL,NULL); +REPLACE INTO cost_fixed_eos VALUES('region',2025,'tech_discrete_cap',2,0.5,2.0,1.0,2.5,NULL,NULL); +REPLACE INTO cost_fixed_eos VALUES('region',2030,'tech_discrete_cap',1,0.0,0.5,0.0,1.0,NULL,NULL); +REPLACE INTO cost_fixed_eos VALUES('region',2030,'tech_discrete_cap',2,0.5,2.0,1.0,2.5,NULL,NULL); +REPLACE INTO cost_fixed_eos VALUES('region',2035,'tech_discrete_cap',1,0.0,0.5,0.0,1.0,NULL,NULL); +REPLACE INTO cost_fixed_eos VALUES('region',2035,'tech_discrete_cap',2,0.5,2.0,1.0,2.5,NULL,NULL); +REPLACE INTO cost_fixed_eos VALUES('region',2040,'tech_discrete_cap',1,0.0,0.5,0.0,1.0,NULL,NULL); +REPLACE INTO cost_fixed_eos VALUES('region',2040,'tech_discrete_cap',2,0.5,2.0,1.0,2.5,NULL,NULL); +REPLACE INTO cost_fixed_eos VALUES('region',2045,'tech_discrete_cap',1,0.0,0.5,0.0,1.0,NULL,NULL); +REPLACE INTO cost_fixed_eos VALUES('region',2045,'tech_discrete_cap',2,0.5,2.0,1.0,2.5,NULL,NULL); +REPLACE INTO cost_fixed_eos VALUES('region',2050,'tech_discrete_cap',1,0.0,0.5,0.0,1.0,NULL,NULL); +REPLACE INTO cost_fixed_eos VALUES('region',2050,'tech_discrete_cap',2,0.5,2.0,1.0,2.5,NULL,NULL); +REPLACE INTO cost_variable_eos VALUES('region',2025,'tech_discrete_cap',1,0.0,0.5,0.0,0.75,NULL,NULL); +REPLACE INTO cost_variable_eos VALUES('region',2025,'tech_discrete_cap',2,0.5,2.0,0.75,1.5,NULL,NULL); +REPLACE INTO cost_variable_eos VALUES('region',2030,'tech_discrete_cap',1,0.0,0.5,0.0,0.75,NULL,NULL); +REPLACE INTO cost_variable_eos VALUES('region',2030,'tech_discrete_cap',2,0.5,2.0,0.75,1.5,NULL,NULL); +REPLACE INTO cost_variable_eos VALUES('region',2035,'tech_discrete_cap',1,0.0,0.5,0.0,0.75,NULL,NULL); +REPLACE INTO cost_variable_eos VALUES('region',2035,'tech_discrete_cap',2,0.5,2.0,0.75,1.5,NULL,NULL); +REPLACE INTO cost_variable_eos VALUES('region',2040,'tech_discrete_cap',1,0.0,0.5,0.0,0.75,NULL,NULL); +REPLACE INTO cost_variable_eos VALUES('region',2040,'tech_discrete_cap',2,0.5,2.0,0.75,1.5,NULL,NULL); +REPLACE INTO cost_variable_eos VALUES('region',2045,'tech_discrete_cap',1,0.0,0.5,0.0,0.75,NULL,NULL); +REPLACE INTO cost_variable_eos VALUES('region',2045,'tech_discrete_cap',2,0.5,2.0,0.75,1.5,NULL,NULL); +REPLACE INTO cost_variable_eos VALUES('region',2050,'tech_discrete_cap',1,0.0,0.5,0.0,0.75,NULL,NULL); +REPLACE INTO cost_variable_eos VALUES('region',2050,'tech_discrete_cap',2,0.5,2.0,0.75,1.5,NULL,NULL); From 35e928bb5b0cc98f0ac153701cf29e0445b9aa3e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 9 Jul 2026 01:01:18 +0000 Subject: [PATCH 28/28] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- temoa/extensions/discrete_capacity/core/model.py | 2 -- temoa/extensions/get_comm_tech.py | 4 +--- .../extensions/method_of_morris/morris_evaluate.py | 14 +++++++++++--- .../manager_factory.py | 2 -- .../tech_activity_vector_manager.py | 9 ++++++--- .../vector_manager.py | 1 + .../modeling_to_generate_alternatives/worker.py | 11 +++++++---- temoa/extensions/monte_carlo/mc_worker.py | 3 ++- .../extensions/single_vector_mga/output_summary.py | 1 + temoa/extensions/stochastics/stochastic_config.py | 1 - temoa/extensions/template/core/model.py | 4 +--- 11 files changed, 30 insertions(+), 22 deletions(-) diff --git a/temoa/extensions/discrete_capacity/core/model.py b/temoa/extensions/discrete_capacity/core/model.py index 6cc127b8..d968a243 100644 --- a/temoa/extensions/discrete_capacity/core/model.py +++ b/temoa/extensions/discrete_capacity/core/model.py @@ -10,7 +10,6 @@ from temoa.core.model import TemoaModel class DiscreteCapacityModel(TemoaModel): - limit_discrete_new_capacity: Param limit_discrete_new_capacity_constraint_rtv: Set limit_discrete_new_capacity_constraint: Constraint @@ -39,7 +38,6 @@ def register_model_components(model: TemoaModel) -> None: rule=discrete_capacity.limit_discrete_new_capacity_constraint_rule, ) - m.limit_discrete_capacity = Param( m.regional_global_indices, m.tech_or_group, within=NonNegativeReals ) diff --git a/temoa/extensions/get_comm_tech.py b/temoa/extensions/get_comm_tech.py index 8c7167c2..717ee9e5 100644 --- a/temoa/extensions/get_comm_tech.py +++ b/temoa/extensions/get_comm_tech.py @@ -31,9 +31,7 @@ def get_tperiods(inp_f: str) -> dict[str, list[int]]: for row in cur: x.append(row[0]) for y in x: - cur.execute( - 'SELECT DISTINCT period FROM output_flow_out WHERE scenario = ?', (y,) - ) + cur.execute('SELECT DISTINCT period FROM output_flow_out WHERE scenario = ?', (y,)) periods_list[y] = [] for per in cur: z = per[0] diff --git a/temoa/extensions/method_of_morris/morris_evaluate.py b/temoa/extensions/method_of_morris/morris_evaluate.py index 1460b775..9695b140 100644 --- a/temoa/extensions/method_of_morris/morris_evaluate.py +++ b/temoa/extensions/method_of_morris/morris_evaluate.py @@ -2,6 +2,7 @@ This module contains the core "evaluation" function for Method Of Morris. It needs to be isolated (outside of class) to enable parallelization. """ + from __future__ import annotations import logging @@ -19,7 +20,6 @@ from temoa.core.config import TemoaConfig - def configure_worker_logger(log_queue: Any, log_level: int) -> logging.Logger: """configure the logger""" worker_logger = logging.getLogger('MM evaluate') @@ -35,8 +35,15 @@ def configure_worker_logger(log_queue: Any, log_level: int) -> logging.Logger: return worker_logger -def evaluate(param_info: dict[int, list[Any]], mm_sample: Any, data: dict[str, Any], - i: int, config: TemoaConfig, log_queue: Any, log_level: int) -> list[float]: +def evaluate( + param_info: dict[int, list[Any]], + mm_sample: Any, + data: dict[str, Any], + i: int, + config: TemoaConfig, + log_queue: Any, + log_level: int, +) -> list[float]: """ Run model for params provided and return objective value and emission value Note: This function needs to be a static instance to enable the parallel @@ -81,6 +88,7 @@ def evaluate(param_info: dict[int, list[Any]], mm_sample: Any, data: dict[str, A with TableWriter(config) as table_writer: table_writer.write_mm_results(model=mdl, iteration=i) import contextlib + with contextlib.closing(sqlite3.connect(config.input_database)) as con: cur = con.cursor() scenario_name = config.scenario + f'-{i}' diff --git a/temoa/extensions/modeling_to_generate_alternatives/manager_factory.py b/temoa/extensions/modeling_to_generate_alternatives/manager_factory.py index 3970c4e8..16b41cbc 100644 --- a/temoa/extensions/modeling_to_generate_alternatives/manager_factory.py +++ b/temoa/extensions/modeling_to_generate_alternatives/manager_factory.py @@ -14,8 +14,6 @@ from temoa.extensions.modeling_to_generate_alternatives.vector_manager import VectorManager - - def get_manager( axis: MgaAxis, weighting: MgaWeighting, diff --git a/temoa/extensions/modeling_to_generate_alternatives/tech_activity_vector_manager.py b/temoa/extensions/modeling_to_generate_alternatives/tech_activity_vector_manager.py index 62f9a99a..5f6c1946 100644 --- a/temoa/extensions/modeling_to_generate_alternatives/tech_activity_vector_manager.py +++ b/temoa/extensions/modeling_to_generate_alternatives/tech_activity_vector_manager.py @@ -137,7 +137,9 @@ def initialize(self) -> None: for idx_annual in self.base_model.active_flow_rpitvo or set(): tech = idx_annual[3] self.technology_size[tech] += 1 - self.variable_index_mapping[tech][self.base_model.v_flow_out_annual.name].append(idx_annual) + self.variable_index_mapping[tech][self.base_model.v_flow_out_annual.name].append( + idx_annual + ) logger.debug('Catalogued %d Technology Variables', sum(self.technology_size.values())) @property @@ -364,8 +366,9 @@ def tracker(self) -> None: A little function to track the size of the hull, after it is built initially Note: This hull is a "throw away" and only used for volume calc, but it is pretty quick """ - if self.hull is not None and \ - self.hull_points is not None: # don't try until after first hull is built + if ( + self.hull is not None and self.hull_points is not None + ): # don't try until after first hull is built hull = Hull(self.hull_points) volume = hull.volume logger.info('Tracking hull at %0.2f', volume) diff --git a/temoa/extensions/modeling_to_generate_alternatives/vector_manager.py b/temoa/extensions/modeling_to_generate_alternatives/vector_manager.py index 3b0d1ea2..e3649e76 100644 --- a/temoa/extensions/modeling_to_generate_alternatives/vector_manager.py +++ b/temoa/extensions/modeling_to_generate_alternatives/vector_manager.py @@ -1,6 +1,7 @@ """ An ABC to serve as a framework for future Vector Managers """ + from __future__ import annotations from abc import ABC, abstractmethod diff --git a/temoa/extensions/modeling_to_generate_alternatives/worker.py b/temoa/extensions/modeling_to_generate_alternatives/worker.py index efb1c3ff..d42dc95a 100644 --- a/temoa/extensions/modeling_to_generate_alternatives/worker.py +++ b/temoa/extensions/modeling_to_generate_alternatives/worker.py @@ -1,6 +1,7 @@ """ Class to contain Workers that execute solves in separate processes """ + from __future__ import annotations import logging.handlers @@ -52,15 +53,16 @@ def __init__( self.solver_name = solver_name self.solver_options = solver_options or {} self.solver_log_path = solver_log_path - self.opt = None # Initialize in run() + self.opt = None # Initialize in run() self.log_root_name = log_root_name self.log_level = log_level self.solve_count = 0 def run(self) -> None: - logger: logging.Logger = getLogger('.'.join( - (self.log_root_name, 'worker', str(self.worker_number)))) + logger: logging.Logger = getLogger( + '.'.join((self.log_root_name, 'worker', str(self.worker_number))) + ) logger.propagate = False # prevent duplicate logs # add a handler that pushes to the queue handler = logging.handlers.QueueHandler(self.log_queue) @@ -133,7 +135,8 @@ def run(self) -> None: status = solve_res['Solver'].termination_condition logger.info( 'Worker %d did not solve. Results status: %s', - self.worker_number, status + self.worker_number, + status, ) except AttributeError: pass diff --git a/temoa/extensions/monte_carlo/mc_worker.py b/temoa/extensions/monte_carlo/mc_worker.py index 7bf987ea..5ab992fa 100644 --- a/temoa/extensions/monte_carlo/mc_worker.py +++ b/temoa/extensions/monte_carlo/mc_worker.py @@ -9,6 +9,7 @@ new obj functions """ + from __future__ import annotations import logging.handlers @@ -60,7 +61,7 @@ def __init__( self.results_queue = results_queue self.solver_name = solver_name self.solver_options = solver_options - self.opt = None # Initialize in run() + self.opt = None # Initialize in run() self.log_queue = log_queue self.log_level = log_level self.root_logger_name = log_root_name diff --git a/temoa/extensions/single_vector_mga/output_summary.py b/temoa/extensions/single_vector_mga/output_summary.py index 54e387b1..80369ae7 100644 --- a/temoa/extensions/single_vector_mga/output_summary.py +++ b/temoa/extensions/single_vector_mga/output_summary.py @@ -1,6 +1,7 @@ """ A tabular summation of the results from an SVMGA run """ + from __future__ import annotations import sqlite3 diff --git a/temoa/extensions/stochastics/stochastic_config.py b/temoa/extensions/stochastics/stochastic_config.py index a46df1e3..3164be08 100644 --- a/temoa/extensions/stochastics/stochastic_config.py +++ b/temoa/extensions/stochastics/stochastic_config.py @@ -33,7 +33,6 @@ class StochasticConfig: perturbations: list[Perturbation] = field(default_factory=list) solver_options: dict[str, Any] = field(default_factory=dict) - @classmethod def from_toml(cls, path: Path) -> Self: with open(path, 'rb') as f: diff --git a/temoa/extensions/template/core/model.py b/temoa/extensions/template/core/model.py index e66a9291..2e3e5a12 100644 --- a/temoa/extensions/template/core/model.py +++ b/temoa/extensions/template/core/model.py @@ -55,9 +55,7 @@ def register_model_components(model: TemoaModel) -> None: m = cast('ExampleModel', model) # Param: a per-(region, tech-or-group) cap on cumulative new capacity. - m.example_new_capacity_limit = Param( - m.regional_global_indices, m.tech_or_group, within=Any - ) + m.example_new_capacity_limit = Param(m.regional_global_indices, m.tech_or_group, within=Any) # Sparse index set for the constraint, built from the param's populated keys. m.example_new_capacity_limit_constraint_rpt = Set(