From 55aa6b505725d5fc2fc47d77976680a446ec60f3 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Fri, 7 Aug 2026 10:44:19 -0400 Subject: [PATCH 01/24] Make some group region/tech indexing more clear Signed-off-by: Davey Elder --- temoa/components/capacity.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/temoa/components/capacity.py b/temoa/components/capacity.py index 78b68134..1492e3ef 100644 --- a/temoa/components/capacity.py +++ b/temoa/components/capacity.py @@ -657,34 +657,34 @@ def create_capacity_and_retirement_sets(model: TemoaModel) -> None: def gather_group_active_processes( - model: TemoaModel, r: Region, p: Period, t: Technology + model: TemoaModel, r_g: Region, p: Period, t_g: Technology ) -> set[tuple[Region, Technology]]: """ A utility to get the valid active processes for a group-region, tech-group pair in period p. Caches the result if its a new call so we don't repeat these O(n2) lookups. """ - if (r, p, t) not in model.group_active_processes: - model.group_active_processes[r, p, t] = { - (_r, _t) - for _r in geography.gather_group_regions(model, r) - for _t in technology.gather_group_techs(model, t) - if (_r, p, _t) in model.process_vintages + if (r_g, p, t_g) not in model.group_active_processes: + model.group_active_processes[r_g, p, t_g] = { + (r, t) + for r in geography.gather_group_regions(model, r_g) + for t in technology.gather_group_techs(model, t_g) + if (r, p, t) in model.process_vintages } - return model.group_active_processes[r, p, t] + return model.group_active_processes[r_g, p, t_g] def gather_group_built_processes( - model: TemoaModel, r: Region, t: Technology, v: Vintage + model: TemoaModel, r_g: Region, t_g: Technology, v: Vintage ) -> set[tuple[Region, Technology]]: """ A utility to get the valid built processes for a group-region, tech-group, vintage index. Caches the result if its a new call so we don't repeat these O(n2) lookups. """ - if (r, t, v) not in model.group_built_processes: - model.group_built_processes[r, t, v] = { - (_r, _t) - for _r in geography.gather_group_regions(model, r) - for _t in technology.gather_group_techs(model, t) - if (_r, _t, v) in model.process_periods + if (r_g, t_g, v) not in model.group_built_processes: + model.group_built_processes[r_g, t_g, v] = { + (r, t) + for r in geography.gather_group_regions(model, r_g) + for t in technology.gather_group_techs(model, t_g) + if (r, t, v) in model.process_periods } - return model.group_built_processes[r, t, v] + return model.group_built_processes[r_g, t_g, v] From 9bddd1b9be0a092cb963ecab9e3fbfdb940293ae Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Fri, 7 Aug 2026 10:45:53 -0400 Subject: [PATCH 02/24] Return region groups as sets for O(1) ownership checks and guaranteed unique elements Signed-off-by: Davey Elder --- temoa/components/geography.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/temoa/components/geography.py b/temoa/components/geography.py index 45e8f788..892f7697 100644 --- a/temoa/components/geography.py +++ b/temoa/components/geography.py @@ -19,8 +19,6 @@ from pyomo.environ import value if TYPE_CHECKING: - from collections.abc import Iterable - from temoa.core.model import TemoaModel from temoa.types import ExprLike, Period, Region, Technology, Vintage @@ -33,15 +31,13 @@ # ============================================================================ -def gather_group_regions(model: TemoaModel, region: Region) -> Iterable[Region]: - regions: list[Region] +def gather_group_regions(model: TemoaModel, region: Region) -> set[Region]: if region == 'global': - regions = list(model.regions) + return set(model.regional_indices) elif '+' in region: - regions = [cast('Region', r) for r in region.split('+')] + return {cast('Region', r) for r in region.split('+')} else: - regions = [region] - return regions + return {region} # ============================================================================ From ec9c85ffd89964efac58801029f4e7929b92615e Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Fri, 7 Aug 2026 11:01:45 -0400 Subject: [PATCH 03/24] Remove + delineated tech groups as loading does not support them. Also change to sets Signed-off-by: Davey Elder --- temoa/components/technology.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/temoa/components/technology.py b/temoa/components/technology.py index 9957db97..d7666b4e 100644 --- a/temoa/components/technology.py +++ b/temoa/components/technology.py @@ -20,8 +20,6 @@ from temoa.components.utils import get_adjusted_existing_capacity if TYPE_CHECKING: - from collections.abc import Iterable - from temoa.core.model import TemoaModel from temoa.types import Period, Region, Technology, Vintage @@ -32,13 +30,11 @@ # ============================================================================ -def gather_group_techs(model: TemoaModel, t_or_g: Technology) -> Iterable[Technology]: +def gather_group_techs(model: TemoaModel, t_or_g: Technology) -> set[Technology]: if t_or_g in model.tech_group_names: - return model.tech_group_members[t_or_g] - elif '+' in t_or_g: - return [cast('Technology', tech) for tech in t_or_g.split('+')] + return set(model.tech_group_members[t_or_g]) else: - return (t_or_g,) + return {t_or_g} # ============================================================================ From f861d0969fcc06c206b05e0bfed081feab1915b6 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Fri, 7 Aug 2026 13:31:51 -0400 Subject: [PATCH 04/24] Generalise reserve margin constraints to any group of regions and techs Signed-off-by: Davey Elder --- temoa/components/reserves.py | 663 ++++++++++++++++------------ temoa/core/model.py | 30 +- temoa/data_io/component_manifest.py | 36 +- temoa/data_io/hybrid_loader.py | 2 + temoa/model_checking/validators.py | 12 - temoa/types/__init__.py | 6 +- temoa/types/dict_types.py | 4 +- 7 files changed, 420 insertions(+), 333 deletions(-) diff --git a/temoa/components/reserves.py b/temoa/components/reserves.py index 9b519d89..febd1366 100644 --- a/temoa/components/reserves.py +++ b/temoa/components/reserves.py @@ -2,10 +2,15 @@ """ Defines the reserve margin components of the Temoa model. -This module is responsible for ensuring the energy system maintains a sufficient -planning reserve margin to ensure reliability. It supports both a 'static' -(based on installed capacity) and a 'dynamic' (based on available generation -in a time slice) formulation for calculating available reserves. +This module ensures the energy system maintains sufficient reserves for +reliability. It supports both a 'planning' (based on installed capacity and +a capacity credit) and an 'operating' (based on available, derated generation +in a time slice) formulation. + +Both formulations are indexed by an arbitrary region-group and tech-group. +Reserve margins are the only constraints in Temoa that, given a region group, +automatically pull in exchange processes connecting a region inside the group +to a region outside it -- see `initialize_reserve_margins`. """ from __future__ import annotations @@ -13,14 +18,24 @@ from logging import getLogger from typing import TYPE_CHECKING -from pyomo.environ import Constraint, Expression, value +from pyomo.environ import Constraint, quicksum, value + +from temoa.components import geography +from temoa.components.capacity import gather_group_active_processes from .utils import get_available_output, get_variable_efficiency if TYPE_CHECKING: from temoa.core.model import TemoaModel from temoa.types import ExprLike - from temoa.types.core_types import Period, Region, Season, TimeOfDay + from temoa.types.core_types import ( + Period, + Region, + Season, + Technology, + TimeOfDay, + Vintage, + ) logger = getLogger(__name__) @@ -29,348 +44,418 @@ # ============================================================================ -def reserve_margin_indices(model: TemoaModel) -> set[tuple[Region, Period, Season, TimeOfDay]]: +def operating_reserve_indices( + model: TemoaModel, +) -> set[tuple[Region, Period, Season, TimeOfDay, Technology]]: return { - (r, p, s, d) - for r in model.planning_reserve_margin.sparse_keys() + (r_g, p, s, d, t_g) + for r_g, t_g in model.operating_reserve_margin.sparse_keys() for p in model.time_optimize - if (r, p) in model.process_reserve_periods + if model.operating_reserve_processes.get((r_g, p, t_g), set()) for s in model.time_season for d in model.time_of_day } -# ============================================================================ -# HELPER FUNCTIONS FOR CONSTRAINT LOGIC -# ============================================================================ - - -def _available_activity_dynamic( - model: TemoaModel, r: Region, p: Period, s: Season, d: TimeOfDay -) -> Expression: - - # Everything but storage and exchange techs - # Derated available generation - available = sum( - get_available_output(model, r, p, s, d, t, v) - * value(model.reserve_capacity_derate[r, s, t, v]) - for (t, v) in model.process_reserve_periods[r, p] - if t not in model.tech_uncap and t not in model.tech_storage - ) - - # Storage - # Derated net output flow - available += sum( - model.v_flow_out[r, p, s, d, i, t, v, o] * value(model.reserve_capacity_derate[r, s, t, v]) - for (t, v) in model.process_reserve_periods[r, p] - if t in model.tech_storage - for i in model.process_inputs[r, p, t, v] - for o in model.process_outputs_by_input[r, p, t, v, i] - ) - available -= sum( - model.v_flow_in[r, p, s, d, i, t, v, o] * value(model.reserve_capacity_derate[r, s, t, v]) - for (t, v) in model.process_reserve_periods[r, p] - if t in model.tech_storage - for i in model.process_inputs[r, p, t, v] - for o in model.process_outputs_by_input[r, p, t, v, i] - ) - - # The above code does not consider exchange techs, e.g. electricity - # transmission between two distinct regions. - # We take exchange takes into account below. - # Note that a single exchange tech linking regions Ri and Rj is twice - # defined: once for region "Ri-Rj" and once for region "Rj-Ri". - - # First, determine the amount of firm capacity each exchange tech - # contributes. - for r1r2 in model.regional_indices: - if '-' not in r1r2: - continue - if ( - r1r2, - p, - ) not in model.process_reserve_periods: # ensure r1r2 is a valid reserve provider in p - continue - r1, r2 = r1r2.split('-') - - output = sum( - get_available_output(model, r1r2, p, s, d, t, v) - * value(model.reserve_capacity_derate[r1r2, s, t, v]) - for (t, v) in model.process_reserve_periods[r1r2, p] - ) - - # Only consider exchange technologies connecting to this region - if r2 == r: - # Add the firm capacity commitment TO this region - # (this region was guaranteed an import of power) - available += output - elif r1 == r: - # Subtract the firm capacity commitment FROM this region - # (this region guaranteed an export of power) - available -= output - - return available +def planning_reserve_indices( + model: TemoaModel, +) -> set[tuple[Region, Period, Season, TimeOfDay, Technology]]: + return { + (r_g, p, s, d, t_g) + for r_g, t_g in model.planning_reserve_margin.sparse_keys() + for p in model.time_optimize + if model.planning_reserve_processes.get((r_g, p, t_g), set()) + for s in model.time_season + for d in model.time_of_day + } -def _available_activity_static( - model: TemoaModel, r: Region, p: Period, s: Season, d: TimeOfDay -) -> Expression: +# ============================================================================ +# INITIALIZATION FUNCTIONS +# ============================================================================ - available = sum( - value(model.capacity_credit[r, p, t, v]) - * model.v_capacity[r, p, t, v] - * value(model.capacity_to_activity[r, t]) - * value(model.segment_fraction[s, d]) - for (t, v) in model.process_reserve_periods[r, p] - if t not in model.tech_uncap - ) - # The above code does not consider exchange techs, e.g. electricity - # transmission between two distinct regions. - # We take exchange takes into account below. - # Note that a single exchange tech linking regions Ri and Rj is twice - # defined: once for region "Ri-Rj" and once for region "Rj-Ri". +def initialize_reserve_margins(model: TemoaModel) -> None: + """Build dictionaries of processes contributing to each reserve margin and log any issues. - # First, determine the amount of firm capacity each exchange tech - # contributes. - for r1r2 in model.regional_indices: - if '-' not in r1r2: - continue - if ( - r1r2, - p, - ) not in model.process_reserve_periods: # ensure r1r2 is a valid reserve provider in p - continue - r1, r2 = r1r2.split('-') + For each (region-group, tech-group) key, resolves the group's base regions + via `geography.gather_group_regions`, then appends any exchange region-pair + (`r1-r2`) with exactly one endpoint inside the group, so exchange processes + crossing the group boundary are counted as contributors. This is the only + place in Temoa where a group constraint auto-includes connected exchange + regions; the corresponding import/export sign handling is applied later in + `reserve_margin_proxy_demand` and the two constraint rules below. + """ - # Only consider exchange technologies connecting to this region - if r2 == r: - # Add the firm capacity commitment TO this region - # (this region was guaranteed an import of power) - available += sum( - value(model.capacity_credit[r1r2, p, t, v]) - * model.v_capacity[r1r2, p, t, v] - * value(model.capacity_to_activity[r1r2, t]) - * value(model.segment_fraction[s, d]) - for (t, v) in model.process_reserve_periods[r1r2, p] + for r_g, t_g in model.operating_reserve_margin.sparse_keys(): + _r_g = r_g + regions = geography.gather_group_regions(model, r_g) + # Append any connected exchange region pairs + for r1r2 in model.regional_indices: + if r1r2 in regions or '-' not in r1r2: + continue + r1, r2 = r1r2.split('-') + if (r1 in regions) != (r2 in regions): + _r_g += '+' + r1r2 + # Get all contributing valid processes in each period + for p in model.time_optimize: + valid_rtv = { + (r, t, v) + for r, t in gather_group_active_processes(model, _r_g, p, t_g) + for v in model.process_vintages.get((r, p, t), set()) + } + if valid_rtv: + model.operating_reserve_processes[(r_g, p, t_g)] = valid_rtv + else: + logger.info( + 'Operating reserve margin %s has no contributors in period %s', + ((r_g, t_g), p), + ) + + if not any( + model.operating_reserve_processes.get((r_g, p, t_g), set()) for p in model.time_optimize + ): + logger.warning( + 'Operating reserve margin has no contributors in any period: %s', + ((r_g, t_g), value(model.operating_reserve_margin[r_g, t_g])), ) - elif r1 == r: - # Subtract the firm capacity commitment FROM this region - # (this region guaranteed an export of power) - available -= sum( - value(model.capacity_credit[r1r2, p, t, v]) - * model.v_capacity[r1r2, p, t, v] - * value(model.capacity_to_activity[r1r2, t]) - * value(model.segment_fraction[s, d]) - for (t, v) in model.process_reserve_periods[r1r2, p] + + for r_g, t_g in model.planning_reserve_margin.sparse_keys(): + _r_g = r_g + regions = geography.gather_group_regions(model, r_g) + # Append any connected exchange region pairs + for r1r2 in model.regional_indices: + if r1r2 in regions or '-' not in r1r2: + continue + r1, r2 = r1r2.split('-') + if (r1 in regions) != (r2 in regions): + _r_g += '+' + r1r2 + # Get all contributing valid processes in each period + for p in model.time_optimize: + valid_rtv = { + (r, t, v) + for r, t in gather_group_active_processes(model, _r_g, p, t_g) + for v in model.process_vintages.get((r, p, t), set()) + } + if valid_rtv: + model.planning_reserve_processes[(r_g, p, t_g)] = valid_rtv + else: + logger.info( + 'Planning reserve margin %s has no contributors in period %s', + ((r_g, t_g), p), + ) + + if not any( + model.planning_reserve_processes.get((r_g, p, t_g), set()) for p in model.time_optimize + ): + logger.warning( + 'Planning reserve margin has no contributors in any period: %s', + ((r_g, t_g), value(model.planning_reserve_margin[r_g, t_g])), ) - return available + +# ============================================================================ +# HELPER FUNCTIONS FOR CONSTRAINT LOGIC +# ============================================================================ -def _required_available_activity( - model: TemoaModel, r: Region, p: Period, s: Season, d: TimeOfDay +def reserve_margin_proxy_demand( + model: TemoaModel, + processes: set[tuple[Region, Technology, Vintage]], + r_g: Region, + p: Period, + s: Season, + d: TimeOfDay, ) -> ExprLike: + r"""In Temoa, demand for a particular commodity (e.g., electricity) may be endogenous to + decisions in the model. So, we estimate demand as the net production of processes + in the reserve group, :math:`\Theta^{res}_{r_g,p,t_g}`. This provides the RHS demand for + both reserve constraints. + + The region group :math:`r_g` indicates the regions in which demand is met, and the + tech group :math:`t_g` indicates which technologies supply the demanded commodity. The + technology group should include all technologies that produce, store, or import/export + the commodity of interest but **not** the technologies that demand it downstream. + + .. note:: + In Temoa, we are not reasonably able to disaggregate the **available** output of a + process for individual commodities when that process outputs multiple commodities. As a + result, a reserve margin constraint cannot be applied to a single commodity where supplying + processes output multiple different commodities (e.g., if a co-generation plant outputs both + heat and electricity, the heat will be included in the equations). This can be avoided by + adding an intermediate "dummy" process that throughputs only the output of interest and then + adding this dummy process to the tech group instead of the original process, but this + requires careful cloning of other technoeconomic parameters so the reserve contributions + remain the same. + + .. math:: + :label: reserve_margin_proxy_demand + + \begin{aligned} + D^{proxy}_{r_g,p,s,d} =& + \sum_{(r,t,v) \in \Theta^{res} \setminus T^a \setminus T^x,\, I, O} + \mathbf{FO}_{r, p, s, d, i, t, v, o} + && \text{(production, non-annual, includes storage)} \\ + &+ \sum_{(r,t,v) \in \Theta^{res} \cap T^a,\, I, O} + \begin{cases} DSD_{r,s,d,o} & o \in C^d \\ SEG_{s,d} & \text{otherwise} + \end{cases} \cdot \mathbf{FOA}_{r, p, i, t, v, o} + && \text{(production, annual)} \\ + &- \sum_{(r,t,v) \in \Theta^{res} \cap T^s,\, I, O} + \mathbf{FI}_{r, p, s, d, i, t, v, o} + && \text{(storage inputs)} \\ + &+ \sum_{\substack{(r,t,v) \in \Theta^{res} \cap T^x \\ r_2 \in r_g,\, I, O}} + \mathbf{FO}_{r, p, s, d, i, t, v, o} + && \text{(imports)} \\ + &- \sum_{\substack{(r,t,v) \in \Theta^{res} \cap T^x \\ r_1 \in r_g,\, I, O}} + \mathbf{FO}_{r, p, s, d, i, t, v, o} / EFF_{r,p,s,d,i,t,v,o} + && \text{(exports)} + \end{aligned} + + where :math:`\Theta^{res} = \Theta^{res}_{r_g,p,t_g}` is the set of all :math:`(r,t,v)` + processes contributing to this reserve margin in this period, and for an exchange process + :math:`r = r_1{-}r_2`, imports (:math:`r_2 \in r_g,\ r_1 \notin r_g`) add delivered energy + while exports (:math:`r_1 \in r_g,\ r_2 \notin r_g`) subtract the energy drawn from + :math:`r_1`. + """ - # In most Temoa input databases, demand is endogenous, so we use electricity - # generation instead as a proxy for electricity demand. - # Non-annual generation - total_generation = sum( - model.v_flow_out[r, p, s, d, S_i, t, S_v, S_o] - for (t, S_v) in model.process_reserve_periods[r, p] - if t not in model.tech_annual - 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] + # Non-annual activity + activity = quicksum( + model.v_flow_out[r, p, s, d, i, t, v, o] + for (r, t, v) in processes + if t not in model.tech_annual and t not in model.tech_exchange + for i in model.process_inputs[r, p, t, v] + for o in model.process_outputs_by_input[r, p, t, v, i] ) - # Generators might serve demands directly - # Annual generation - total_generation += sum( + # Annual activity (could also just be a demand tech) + activity += quicksum( ( - value(model.demand_specific_distribution[r, p, s, d, S_o]) - if S_o in model.commodity_demand + value(model.demand_specific_distribution[r, p, s, d, o]) + if o in model.commodity_demand else value(model.segment_fraction[s, d]) ) - * model.v_flow_out_annual[r, p, S_i, t, S_v, S_o] - for (t, S_v) in model.process_reserve_periods[r, p] - if t in model.tech_annual - 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] + * model.v_flow_out_annual[r, p, i, t, v, o] + for (r, t, v) in processes + if t in model.tech_annual and t not in model.tech_exchange + for i in model.process_inputs[r, p, t, v] + for o in model.process_outputs_by_input[r, p, t, v, i] ) # We must take into account flows into storage technologies. # Flows into storage technologies need to be subtracted from the - # load calculation. - total_generation -= sum( - model.v_flow_in[r, p, s, d, S_i, t, S_v, S_o] - for (t, S_v) in model.process_reserve_periods[r, p] - if t in model.tech_storage - 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] + # load calculation. Flow_out already summed above. + activity -= quicksum( + model.v_flow_in[r, p, s, d, i, t, v, o] + for (r, t, v) in processes + if t in model.tech_storage and t not in model.tech_exchange + for i in model.process_inputs[r, p, t, v] + for o in model.process_outputs_by_input[r, p, t, v, i] ) - # Electricity imports and exports via exchange techs are accounted - # for below: - for r1r2 in model.regional_indices: # ensure the region is of the form r1-r2 - if '-' not in r1r2: - continue - if ( - r1r2, - p, - ) not in model.process_reserve_periods: # ensure r1r2 is a valid reserve provider in p + # Exchange technologies + # Add imports into the group, subtract exports out of it + regions = geography.gather_group_regions(model, r_g) + for r1r2, t, v in processes: + if t not in model.tech_exchange: continue + r1, r2 = r1r2.split('-') - # First, determine the exports, and subtract this value from the - # total generation. - if r1 == r: - total_generation -= sum( - model.v_flow_out[r1r2, p, s, d, S_i, t, S_v, S_o] - / get_variable_efficiency(model, r1r2, p, s, d, S_i, t, S_v, S_o) - for (t, S_v) in model.process_reserve_periods[r1r2, p] - for S_i in model.process_inputs[r1r2, p, t, S_v] - for S_o in model.process_outputs_by_input[r1r2, p, t, S_v, S_i] + if r2 in regions and r1 not in regions: + # Import into the group: add the energy delivered to r2 + activity += quicksum( + model.v_flow_out[r1r2, p, s, d, i, t, v, o] + for i in model.process_inputs[r1r2, p, t, v] + for o in model.process_outputs_by_input[r1r2, p, t, v, i] ) - # Second, determine the imports, and add this value from the - # total generation. - elif r2 == r: - total_generation += sum( - model.v_flow_out[r1r2, p, s, d, S_i, t, S_v, S_o] - for (t, S_v) in model.process_reserve_periods[r1r2, p] - for S_i in model.process_inputs[r1r2, p, t, S_v] - for S_o in model.process_outputs_by_input[r1r2, p, t, S_v, S_i] + elif r1 in regions and r2 not in regions: + # Export out of the group: subtract the energy drawn from r1 + activity -= quicksum( + model.v_flow_out[r1r2, p, s, d, i, t, v, o] + / get_variable_efficiency(model, r1r2, p, s, d, i, t, v, o) + for i in model.process_inputs[r1r2, p, t, v] + for o in model.process_outputs_by_input[r1r2, p, t, v, i] ) - requirement = total_generation * (1 + value(model.planning_reserve_margin[r])) - return requirement + return activity # ============================================================================ -# PYOMO CONSTRAINT RULE +# PYOMO CONSTRAINT RULES # ============================================================================ -def reserve_margin_dynamic( - model: TemoaModel, r: Region, p: Period, s: Season, d: TimeOfDay +def operating_reserve_margin_constraint( + model: TemoaModel, r_g: Region, p: Period, s: Season, d: TimeOfDay, t_g: Technology ) -> Constraint: r""" - A dynamic alternative to the traditional, static reserve margin constraint. Capacity values - are calculated from availability of generation in each hour—like an operating reserve margin—\ - accounting for a capacity derate factor subtracting, for example, forced outage due to icing. + A dynamic alternative to the planning reserve margin constraint. Capacity values are + calculated from process output availability in each time slice, accounting for capacity + factors, unit commitment (if the extension is enabled), and a seasonal derating factor + which may adjust for, for example, seasonal forced outage rates. A derate factor of 1 + indicates no derating while a factor of 0 indicates zero dependable output in that season. + Technologies in the tech group are used to calculate the proxy demand and so must include + these fully derated processes as well. + + **The default derate factor is 1** if not set (i.e., we assume technologies are fully + available, up to their capacity factor, by default). + + For exchange technologies (e.g., inter-regional transmission), reserve + contributions are added for available output into the region-group but *subtracted* + for capacity out of it. + + The availability of storage technologies is a non-trivial problem as it depends on state + of charge, which has a temporal dependency (i.e., if we consider a storage technology to + contribute reserve in time t can it also contribute in time t+1?). In this implementation, + we let storage contribute only what it actually outputs (net) in each time slice, as a + conservative but tractable approach (use it or lose it). .. math:: - :label: reserve_margin_dynamic - - &\sum_{t \in T^{res} \setminus T^{x} \setminus T^s,\ V} CFP_{r,s^*,d^*,t,v}\ - \cdot RCD_{r,s^*,t,v}\ - \cdot \mathbf{CAP}_{r,p,t,v} \cdot SEG_{s^*,d^*}\ - \cdot C2A_{r,t} \\ - &+ \sum_{t \in T^{res} \cap T^{x} \setminus T^s,\ V} CFP_{r_i - r, s^*, d^*, t, v}\ - \cdot RCD_{r_i - r, s^*, t, v}\ - \cdot \mathbf{CAP}_{r_i - r,p,t,v} \cdot SEG_{s^*,d^*}\ - \cdot C2A_{r_i - r, t} \\ - &- \sum_{t \in T^{res} \cap T^{x} \setminus T^s,\ V} CFP_{r - r_i, s^*, d^*, t, v}\ - \cdot RCD_{r - r_i, s^*, t, v}\ - \cdot \mathbf{CAP}_{r - r_i,p,t,v}\ - \cdot SEG_{s^*,d^*} \cdot C2A_{r - r_i, t} \\ - &+ \sum_{t \in (T^s \cap T^{res}), V, I, O} \ - \left(\ - \mathbf{FO}_{r,p,s,d,i,t,v,o} - \mathbf{FI}_{r,p,s,d,i,t,v,o}\ - \right)\ - \cdot RCD_{r,s,t,v} \\ - &\geq\ - \left[\ - \sum_{t \in T^{res} \setminus T^{x} \setminus T^a, V, I, O}\ - \mathbf{FO}_{r, p, s, d, i, t, v, o}\ - \right. \\ - &+ \sum_{t \in T^{res} \cap T^a, V, I, O} - \begin{cases} DSD_{r,s,d,o} & \text{if } o \in C^d \\ - SEG_{s,d} & \text{otherwise} \end{cases} - \cdot \mathbf{FOA}_{r, p, i, t, v, o} \\ - &+ \sum_{t \in T^{res} \cap T^{x}, V, I, O} \ - \mathbf{FO}_{r_i - r, p, s, d, i, t, v, o} \\ - &- \sum_{t \in T^{res} \cap T^{x}, V, I, O} \ - \mathbf{FI}_{r - r_i, p, s, d, i, t, v, o} \\ - &- \left. \sum_{t \in T^{res} \cap T^{s}, V, I, O} \ - \mathbf{FI}_{r, p, s, d, i, t, v, o} \right] \cdot (1 + PRM_r) \\ - \\ - &\qquad \qquad \forall \{r, p, s, d\} \in \ - \Theta_{\text{ReserveMargin}} \text{ and } \forall r_i \in R + :label: operating_reserve_margin + + \begin{aligned} + &\sum_{(r,t,v) \in \Theta^{res} \setminus T^x \setminus T^s} + CFP_{r,s,d,t,v} \cdot ORD_{r,s,t} \cdot \mathbf{CAP}_{r,p,t,v} + \cdot SEG_{s,d} \cdot C2A_{r,t} + && \text{(firm production)} \\ + &+ \sum_{(r,t,v) \in \Theta^{res} \cap T^s,\, I, O} + \left( \mathbf{FO}_{r,p,s,d,i,t,v,o} - \mathbf{FI}_{r,p,s,d,i,t,v,o} \right) + \cdot ORD_{r,s,t} + && \text{(net storage output)} \\ + &+ \sum_{(r,t,v) \in \Theta^{res} \cap T^x} + \sigma_{r,r_g} \cdot CFP_{r,s,d,t,v} \cdot ORD_{r,s,t} + \cdot \mathbf{CAP}_{r,p,t,v} \cdot SEG_{s,d} \cdot C2A_{r,t} + && \text{(net firm imports)} \\ + &\geq D^{proxy}_{r_g,p,s,d} \cdot (1 + ORM_{r_g,t_g}) \\ + &\forall \{r_g, p, s, d, t_g\} \in \Theta_{\text{OperatingReserveMargin}} + \end{aligned} + + where :math:`\sigma_{r,r_g} = +1` for an exchange process delivering into + region-group :math:`r_g` and :math:`-1` for one delivering out of it, and + :math:`D^{proxy}_{r_g,p,s,d}` is the group's proxy demand defined in + :eq:`reserve_margin_proxy_demand`. """ - return _available_activity_dynamic(model, r, p, s, d) >= _required_available_activity( - model, r, p, s, d + processes = model.operating_reserve_processes[r_g, p, t_g] + + # Everything but storage and exchange techs + # Derated available generation + available = quicksum( + get_available_output(model, r, p, s, d, t, v) + * value(model.operating_reserve_derate[r, s, t]) + for (r, t, v) in processes + if not (t in model.tech_uncap or t in model.tech_storage or t in model.tech_exchange) ) + # Storage + # Derated net output flow + available += quicksum( + model.v_flow_out[r, p, s, d, i, t, v, o] * value(model.operating_reserve_derate[r, s, t]) + for (r, t, v) in processes + if t in model.tech_storage and t not in model.tech_exchange + for i in model.process_inputs[r, p, t, v] + for o in model.process_outputs_by_input[r, p, t, v, i] + ) + available -= quicksum( + model.v_flow_in[r, p, s, d, i, t, v, o] * value(model.operating_reserve_derate[r, s, t]) + for (r, t, v) in processes + if t in model.tech_storage and t not in model.tech_exchange + for i in model.process_inputs[r, p, t, v] + for o in model.process_outputs_by_input[r, p, t, v, i] + ) + + # Exchange technologies + # Add available imports into the group, subtract available exports out of it + regions = geography.gather_group_regions(model, r_g) + for r1r2, t, v in processes: + if t not in model.tech_exchange: + continue + + _available = get_available_output(model, r1r2, p, s, d, t, v) * value( + model.operating_reserve_derate[r1r2, s, t] + ) + + r1, r2 = r1r2.split('-') + if r2 in regions and r1 not in regions: + available += _available + elif r1 in regions and r2 not in regions: + available -= _available -def reserve_margin_static( - model: TemoaModel, r: Region, p: Period, s: Season, d: TimeOfDay + demand = reserve_margin_proxy_demand(model, processes, r_g, p, s, d) + return available >= demand * (1 + value(model.operating_reserve_margin[r_g, t_g])) + + +def planning_reserve_margin_constraint( + model: TemoaModel, r_g: Region, p: Period, s: Season, d: TimeOfDay, t_g: Technology ) -> Constraint: r""" During each period :math:`p`, the sum of capacity values of all reserve - technologies :math:`\sum_{t \in T^{res}} \textbf{CAP}_{r,p,t,v}`, which are - defined in the set :math:`\textbf{T}^{res}`, should exceed the peak load by - :math:`PRM`, the regional reserve margin. Note that the reserve - margin is expressed in percentage of the peak load. Generally speaking, in - a database we may not know the peak demand before running the model, therefore, - we write this equation for all the time-slices defined in the database in each region. - Each generator is allowed to contribute its available capacity times a pre-defined - capacity credit, :math:`CC_{r,p,t,v}`. - - For exchange technologies (i.e., inter-regional transmission), reserve contributions - are added to the downstream region but *subtracted* from the upstream region. This is - because, since they are not generating any power, their summed contribution across - regions should be zero. + technologies, weighted by their planning reserve credit, must exceed the + region-group's proxy demand by :math:`PRM_{r_g,t_g}` in every time slice. + + This credit represents the expected availability of nameplate capacity + during peak demand conditions and is usually determined from stochastic + modelling of demand/supply scenarios. A credit of 1 indicates the technology is + expected to be able to output its full nameplate capacity at high demand, + while 0 indicates it offers no reliable capacity. Technologies in the tech + group are used to calculate the proxy demand and so must include these zero + credit processes as well. + + **The default credit is 0** if not set (i.e., we assume technologies offer + no capacity value by default). + + For exchange technologies (e.g., inter-regional transmission), reserve + contributions are added for capacity into the region-group but *subtracted* + for capacity out of it. .. math:: :label: reserve_margin_static - &\sum_{t \in T^{res} \setminus T^{x}, V} {CC_{r,p,t,v} - \cdot \textbf{CAP}_{r,p,t,v} \cdot - SEG_{s^*,d^*} \cdot C2A_{r,t} }\\ - &+ \sum_{t \in T^{res} \cap T^{x}, V} {CC_{r_i-r,p,t,v} - \cdot \textbf{CAP}_{r_i-r,p,t,v} \cdot - SEG_{s^*,d^*} \cdot C2A_{r_i-r,t} }\\ - &- \sum_{t \in T^{res} \cap T^{x}, V} {CC_{r-r_i,p,t,v} - \cdot \textbf{CAP}_{r-r_i,p,t,v} \cdot - SEG_{s^*,d^*} \cdot C2A_{r-r_i,t} }\\ - &\geq \left [ \sum_{ t \in T^{res} \setminus T^{x} \setminus T^a,V,I,O } - \textbf{FO}_{r, p, s, d, i, t, v, o}\right.\\ - &+ \sum_{ t \in T^{res} \cap T^a,V,I,O } - \begin{cases} DSD_{r,s,d,o} & \text{if } o \in C^d \\ - SEG_{s,d} & \text{otherwise} \end{cases} - \cdot \textbf{FOA}_{r, p, i, t, v, o}\\ - &+ \sum_{ t \in T^{res} \cap T^{x},V,I,O } \textbf{FO}_{r_i-r, p, s, d, i, t, v, o}\\ - &- \sum_{ t \in T^{res} \cap T^{x},V,I,O } \textbf{FI}_{r-r_i, p, s, d, i, t, v, o}\\ - &- \left.\sum_{ t \in T^{res} \cap T^{s},V,I,O } \textbf{FI}_{r, p, s, d, i, t, v, o} - \right] - \cdot (1 + PRM_r)\\ - - \\ - &\qquad\qquad\forall \{r, p, s, d\} \in \Theta_{\text{ReserveMargin}} \text{and} \forall - r_i \in R + \begin{aligned} + &\sum_{(r,t,v) \in \Theta^{res} \setminus T^x} + PRC_{r,t} \cdot \mathbf{CAP}_{r,p,t,v} \cdot SEG_{s,d} \cdot C2A_{r,t} + && \text{(production capacity, includes storage)} \\ + &+ \sum_{(r,t,v) \in \Theta^{res} \cap T^x} + \sigma_{r,r_g} \cdot PRC_{r,t} \cdot \mathbf{CAP}_{r,p,t,v} + \cdot SEG_{s,d} \cdot C2A_{r,t} + && \text{(net import capacity)} \\ + &\geq D^{proxy}_{r_g,p,s,d} \cdot (1 + PRM_{r_g,t_g}) \\ + &\forall \{r_g, p, s, d, t_g\} \in \Theta_{\text{PlanningReserveMargin}} + \end{aligned} + + where :math:`\sigma_{r,r_g} = +1` for an exchange process delivering into + region-group :math:`r_g` and :math:`-1` for one delivering out of it, and + :math:`D^{proxy}_{r_g,p,s,d}` is the group's proxy demand defined in + :eq:`reserve_margin_proxy_demand`. """ - return _available_activity_static(model, r, p, s, d) >= _required_available_activity( - model, r, p, s, d + processes = model.planning_reserve_processes[r_g, p, t_g] + + available = quicksum( + value(model.planning_reserve_credit[r, t]) + * model.v_capacity[r, p, t, v] + * value(model.capacity_to_activity[r, t]) + * value(model.segment_fraction[s, d]) + for (r, t, v) in processes + if t not in model.tech_uncap and t not in model.tech_exchange ) + # Exchange technologies + # Add credited imports into the group, subtract credited exports out of it + regions = geography.gather_group_regions(model, r_g) + for r1r2, t, v in processes: + if t not in model.tech_exchange: + continue -def reserve_margin_constraint( - model: TemoaModel, - r: Region, - p: Period, - s: Season, - d: TimeOfDay, -) -> ExprLike: - """Returns the appropriate reserve margin constraint rule.""" - if (not model.tech_reserve) or ( - (r, p) not in model.process_reserve_periods - ): # If reserve set empty or if r,p not in M.processReservePeriod, skip the constraint - return Constraint.Skip - mode = model.reserve_margin_method.first() - if mode == 'dynamic': - return reserve_margin_dynamic(model, r, p, s, d) - elif mode == 'static': - return reserve_margin_static(model, r, p, s, d) - else: - raise ValueError( - f"Invalid reserve margin method: {mode}. Must be either 'dynamic' or 'static'." + _available = ( + value(model.planning_reserve_credit[r1r2, t]) + * model.v_capacity[r1r2, p, t, v] + * value(model.capacity_to_activity[r1r2, t]) + * value(model.segment_fraction[s, d]) ) + + r1, r2 = r1r2.split('-') + if r2 in regions and r1 not in regions: + available += _available + elif r1 in regions and r2 not in regions: + available -= _available + + demand = reserve_margin_proxy_demand(model, processes, r_g, p, s, d) + return available >= demand * (1 + value(model.planning_reserve_margin[r_g, t_g])) diff --git a/temoa/core/model.py b/temoa/core/model.py index b52ec177..88720de0 100755 --- a/temoa/core/model.py +++ b/temoa/core/model.py @@ -48,7 +48,6 @@ validate_0to1, validate_efficiency, validate_linked_tech, - validate_reserve_margin, validate_tech_sets, ) @@ -162,8 +161,9 @@ def __init__( self.retirement_production_processes: t.RetirementProductionProcessesDict = {} self.process_inputs_by_output: t.ProcessInputsByOutputDict = {} self.process_outputs_by_input: t.ProcessOutputsByInputDict = {} - self.process_reserve_periods: t.ProcessReservePeriodsDict = {} self.process_periods: t.ProcessPeriodsDict = {} # {(r, t, v): set(p)} + self.planning_reserve_processes: t.ReserveProcessesDict = {} + self.operating_reserve_processes: t.ReserveProcessesDict = {} # {(r, t, v): set(p)} periods in which a process can economically or naturally retire self.retirement_periods: t.RetirementPeriodsDict = {} self.process_vintages: t.ProcessVintagesDict = {} @@ -722,23 +722,21 @@ def __init__( # Define parameters associated with electric sector operation self.reserve_margin_method = Set() # How contributions to the reserve margin are calculated - self.capacity_credit = Param( + self.planning_reserve_margin = Param(self.regional_global_indices, self.tech_or_group) + self.planning_reserve_credit = Param( self.regional_indices, - self.time_optimize, - self.tech_reserve, - self.vintage_all, + self.tech_all, default=0, validate=validate_0to1, ) - self.reserve_capacity_derate = Param( + self.operating_reserve_margin = Param(self.regional_global_indices, self.tech_or_group) + self.operating_reserve_derate = Param( self.regional_indices, self.time_season, - self.tech_reserve, - self.vintage_all, + self.tech_all, default=1, validate=validate_0to1, ) - self.planning_reserve_margin = Param(self.regions) self.emission_embodied = Param( self.regions, @@ -982,10 +980,14 @@ def __init__( self.ramp_up_constraint_rpsdtv, rule=operations.ramp_up_constraint ) - self.reserve_margin_rpsd = Set(dimen=4, initialize=reserves.reserve_margin_indices) - self.validate_reserve_margin = BuildAction(rule=validate_reserve_margin) - self.reserve_margin_constraint = Constraint( - self.reserve_margin_rpsd, rule=reserves.reserve_margin_constraint + self.initialize_reserve_margins = BuildAction(rule=reserves.initialize_reserve_margins) + self.operating_reserve_rpsdt = Set(dimen=5, initialize=reserves.operating_reserve_indices) + self.operating_reserve_margin_constraint = Constraint( + self.operating_reserve_rpsdt, rule=reserves.operating_reserve_margin_constraint + ) + self.planning_reserve_rpsdt = Set(dimen=5, initialize=reserves.planning_reserve_indices) + self.planning_reserve_margin_constraint = Constraint( + self.planning_reserve_rpsdt, rule=reserves.planning_reserve_margin_constraint ) self.limit_emission_constraint = Constraint( diff --git a/temoa/data_io/component_manifest.py b/temoa/data_io/component_manifest.py index 7521b34c..1372c30d 100644 --- a/temoa/data_io/component_manifest.py +++ b/temoa/data_io/component_manifest.py @@ -507,26 +507,36 @@ def build_manifest(model: TemoaModel, extension_ids: Sequence[str] | None = None index_set=model.renewable_portfolio_standard_constraint_rpg, ), LoadItem( - component=model.capacity_credit, - table='capacity_credit', - columns=['region', 'period', 'tech', 'vintage', 'credit'], - validator_name='viable_rtv', - validation_map=(0, 2, 3), + component=model.planning_reserve_credit, + table='planning_reserve_credit', + columns=['region', 'tech', 'credit'], + validator_name='viable_rt', + validation_map=(0, 1), is_table_required=False, ), LoadItem( - component=model.reserve_capacity_derate, - table='reserve_capacity_derate', - columns=['region', 'season', 'tech', 'vintage', 'factor'], - validator_name='viable_rtv', - validation_map=(0, 2, 3), + component=model.planning_reserve_margin, + table='planning_reserve_margin', + columns=['region', 'tech_or_group', 'margin'], + validator_name='viable_rt', + validation_map=(0, 1), is_period_filtered=False, is_table_required=False, ), LoadItem( - component=model.planning_reserve_margin, - table='planning_reserve_margin', - columns=['region', 'margin'], + component=model.operating_reserve_derate, + table='operating_reserve_derate', + columns=['region', 'season', 'tech', 'factor'], + validator_name='viable_rt', + validation_map=(0, 2), + is_table_required=False, + ), + LoadItem( + component=model.operating_reserve_margin, + table='operating_reserve_margin', + columns=['region', 'tech_or_group', 'margin'], + validator_name='viable_rt', + validation_map=(0, 1), 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 6f7e26b1..31b77078 100644 --- a/temoa/data_io/hybrid_loader.py +++ b/temoa/data_io/hybrid_loader.py @@ -66,6 +66,8 @@ 'limit_capacity_share': 'region', 'limit_new_capacity_share': 'region', 'limit_resource': 'region', + 'planning_reserve_margin': 'region', + 'operating_reserve_margin': 'region', } diff --git a/temoa/model_checking/validators.py b/temoa/model_checking/validators.py index 4db893b2..c76659c4 100644 --- a/temoa/model_checking/validators.py +++ b/temoa/model_checking/validators.py @@ -39,7 +39,6 @@ 'validate_0to1', 'validate_efficiency', 'validate_linked_tech', - 'validate_reserve_margin', 'validate_tech_sets', ] @@ -337,17 +336,6 @@ def validate_efficiency( return False -def validate_reserve_margin(model: TemoaModel) -> None: - for r in model.planning_reserve_margin.sparse_keys(): - if all((r, p) not in model.process_reserve_periods for p in model.time_optimize): - logger.warning( - 'Planning reserve margin provided but there are no reserve technologies serving ' - 'this ' - 'region: %s', - (r, model.planning_reserve_margin[r]), - ) - - def validate_tech_sets(model: TemoaModel) -> None: """ Check tech sets for any forbidden intersections diff --git a/temoa/types/__init__.py b/temoa/types/__init__.py index 78d4a98a..e6f7e913 100644 --- a/temoa/types/__init__.py +++ b/temoa/types/__init__.py @@ -37,8 +37,7 @@ 'ProcessOutputsByInputDict', 'ProcessOutputsDict', 'ProcessPeriodsDict', - 'ProcessReservePeriodsDict', - 'ProcessTechsDict', + 'ReserveProcessesDict', 'ProcessVintagesDict', 'GroupBuiltProcessesDict', 'GroupActiveProcessesDict', @@ -117,11 +116,10 @@ ProcessOutputsByInputDict, ProcessOutputsDict, ProcessPeriodsDict, - ProcessReservePeriodsDict, - ProcessTechsDict, ProcessVintagesDict, RampDownVintagesDict, RampUpVintagesDict, + ReserveProcessesDict, RetirementPeriodsDict, RetirementProductionProcessesDict, SeasonalStorageDict, diff --git a/temoa/types/dict_types.py b/temoa/types/dict_types.py index 380e4f77..3ba604c9 100644 --- a/temoa/types/dict_types.py +++ b/temoa/types/dict_types.py @@ -18,7 +18,9 @@ tuple[Region, Period, Technology, Vintage, Commodity], set[Commodity] ] ProcessTechsDict = dict[tuple[Region, Period, Commodity], set[Technology]] -ProcessReservePeriodsDict = dict[tuple[Region, Period], set[tuple[Technology, Vintage]]] +ReserveProcessesDict = dict[ + tuple[Region, Period, Technology], set[tuple[Region, Technology, Vintage]] +] ProcessPeriodsDict = dict[tuple[Region, Technology, Vintage], set[Period]] RetirementPeriodsDict = dict[tuple[Region, Technology, Vintage], set[Period]] ProcessVintagesDict = dict[tuple[Region, Period, Technology], set[Vintage]] From 3defb1696e7f2b8d2aaa3d64d62be6a3b90e36e2 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Fri, 7 Aug 2026 14:40:01 -0400 Subject: [PATCH 05/24] Remove reserve margin method from config Signed-off-by: Davey Elder --- temoa/core/config.py | 3 --- temoa/core/model.py | 1 - temoa/data_io/hybrid_loader.py | 3 --- temoa/tutorial_assets/config_sample.toml | 13 ------------- tests/testing_configs/config_annualised_demand.toml | 1 - tests/testing_configs/config_emissions.toml | 1 - tests/testing_configs/config_link_test.toml | 1 - tests/testing_configs/config_materials.toml | 1 - tests/testing_configs/config_mediumville.toml | 1 - tests/testing_configs/config_myopic_capacities.toml | 1 - tests/testing_configs/config_seasonal_storage.toml | 1 - tests/testing_configs/config_storageville.toml | 1 - tests/testing_configs/config_survival_curve.toml | 1 - tests/testing_configs/config_test_system.toml | 1 - tests/testing_configs/config_test_week.toml | 1 - tests/testing_configs/config_utopia.toml | 1 - tests/testing_configs/config_utopia_gv.toml | 1 - tests/testing_configs/config_utopia_mc.toml | 1 - tests/testing_configs/config_utopia_myopic.toml | 1 - 19 files changed, 35 deletions(-) diff --git a/temoa/core/config.py b/temoa/core/config.py index 3e68c922..50015d09 100644 --- a/temoa/core/config.py +++ b/temoa/core/config.py @@ -50,7 +50,6 @@ def __init__( save_lp_file: bool = False, time_sequencing: str | None = None, days_per_period: int = 365, - reserve_margin: str | None = None, MGA: dict[str, object] | None = None, SVMGA: dict[str, object] | None = None, myopic: dict[str, object] | None = None, @@ -137,7 +136,6 @@ def __init__( self.save_lp_file = save_lp_file self.time_sequencing = time_sequencing self.days_per_period = days_per_period - self.reserve_margin = reserve_margin self.mga_inputs = MGA self.svmga_inputs = SVMGA @@ -374,7 +372,6 @@ def __repr__(self) -> str: msg += spacer msg += '{:>{}s}: {}\n'.format('Time sequencing', width, self.time_sequencing) msg += '{:>{}s}: {}\n'.format('Days per period', width, self.days_per_period) - msg += '{:>{}s}: {}\n'.format('Planning reserve margin', width, self.reserve_margin) if self.scenario_mode == TemoaMode.MYOPIC and self.myopic_inputs is not None: msg += spacer diff --git a/temoa/core/model.py b/temoa/core/model.py index 88720de0..af90abba 100755 --- a/temoa/core/model.py +++ b/temoa/core/model.py @@ -721,7 +721,6 @@ def __init__( self.linked_techs = Param(self.regional_indices, self.tech_all, self.commodity_emissions) # Define parameters associated with electric sector operation - self.reserve_margin_method = Set() # How contributions to the reserve margin are calculated self.planning_reserve_margin = Param(self.regional_global_indices, self.tech_or_group) self.planning_reserve_credit = Param( self.regional_indices, diff --git a/temoa/data_io/hybrid_loader.py b/temoa/data_io/hybrid_loader.py index 31b77078..6735093d 100644 --- a/temoa/data_io/hybrid_loader.py +++ b/temoa/data_io/hybrid_loader.py @@ -286,9 +286,6 @@ def create_data_dict(self, myopic_index: MyopicIndex | None = None) -> dict[str, # Load simple config-based or myopic-specific values self._load_component_data(data, model.time_sequencing, [(self.config.time_sequencing,)]) self._load_component_data(data, model.days_per_period, [(self.config.days_per_period,)]) - self._load_component_data( - data, model.reserve_margin_method, [(self.config.reserve_margin,)] - ) if myopic_index: p0_result = cur.execute( "SELECT min(period) FROM time_period WHERE flag == 'f'" diff --git a/temoa/tutorial_assets/config_sample.toml b/temoa/tutorial_assets/config_sample.toml index df3d7b20..02bcf672 100644 --- a/temoa/tutorial_assets/config_sample.toml +++ b/temoa/tutorial_assets/config_sample.toml @@ -136,19 +136,6 @@ time_sequencing = 'seasonal_timeslices' # E.g. 365 if all seasons collectively represent a year, 7 if modelling a single representative week. days_per_period = 365 -# How contributions to the planning reserve margin are calculated -# Options: -# 'static' -# Traditional planning reserve formulation. Contributions are independent of hourly availability: -# capacity value = net capacity * capacity credit -# 'dynamic' -# Contributions are available output including a capacity derate factor (e.g., forced outage rate). -# For most generators, contributions are available (derated) output in each time slice: -# capacity value = net capacity * reserve capacity derate * capacity factor -# For storage, contributions are (derated) actual output in each time slice: -# capacity value = flow out * reserve capacity derate -reserve_margin = 'dynamic' - # ------------------------------------ # SQLITE PERFORMANCE TUNING # ------------------------------------ diff --git a/tests/testing_configs/config_annualised_demand.toml b/tests/testing_configs/config_annualised_demand.toml index 198a7982..847b1de3 100644 --- a/tests/testing_configs/config_annualised_demand.toml +++ b/tests/testing_configs/config_annualised_demand.toml @@ -9,7 +9,6 @@ save_duals = false save_lp_file = false time_sequencing = "representative_periods" days_per_period = 365 -reserve_margin = "static" [MGA] cost_epsilon = 0.03 diff --git a/tests/testing_configs/config_emissions.toml b/tests/testing_configs/config_emissions.toml index a3ec86a4..b63e2c38 100644 --- a/tests/testing_configs/config_emissions.toml +++ b/tests/testing_configs/config_emissions.toml @@ -9,7 +9,6 @@ save_duals = false save_lp_file = false time_sequencing = "seasonal_timeslices" days_per_period = 365 -reserve_margin = "static" [MGA] slack = 0.1 diff --git a/tests/testing_configs/config_link_test.toml b/tests/testing_configs/config_link_test.toml index 88192e4f..93356f66 100644 --- a/tests/testing_configs/config_link_test.toml +++ b/tests/testing_configs/config_link_test.toml @@ -12,7 +12,6 @@ save_duals = false save_lp_file = false time_sequencing = "seasonal_timeslices" days_per_period = 365 -reserve_margin = "static" [MGA] slack = 0.1 diff --git a/tests/testing_configs/config_materials.toml b/tests/testing_configs/config_materials.toml index 3eae4926..6567dd30 100644 --- a/tests/testing_configs/config_materials.toml +++ b/tests/testing_configs/config_materials.toml @@ -9,7 +9,6 @@ save_duals = false save_lp_file = false time_sequencing = "seasonal_timeslices" days_per_period = 365 -reserve_margin = "static" [MGA] slack = 0.1 diff --git a/tests/testing_configs/config_mediumville.toml b/tests/testing_configs/config_mediumville.toml index 935abdba..e6d214ed 100644 --- a/tests/testing_configs/config_mediumville.toml +++ b/tests/testing_configs/config_mediumville.toml @@ -9,7 +9,6 @@ save_duals = false save_lp_file = false time_sequencing = "seasonal_timeslices" days_per_period = 365 -reserve_margin = "static" [MGA] slack = 0.1 diff --git a/tests/testing_configs/config_myopic_capacities.toml b/tests/testing_configs/config_myopic_capacities.toml index 77b49abd..5787108d 100644 --- a/tests/testing_configs/config_myopic_capacities.toml +++ b/tests/testing_configs/config_myopic_capacities.toml @@ -10,7 +10,6 @@ save_duals = false save_lp_file = false time_sequencing = "seasonal_timeslices" days_per_period = 365 -reserve_margin = "static" [MGA] slack = 0.1 diff --git a/tests/testing_configs/config_seasonal_storage.toml b/tests/testing_configs/config_seasonal_storage.toml index 92744655..5c61a78a 100644 --- a/tests/testing_configs/config_seasonal_storage.toml +++ b/tests/testing_configs/config_seasonal_storage.toml @@ -9,7 +9,6 @@ save_duals = false save_lp_file = false time_sequencing = "representative_periods" days_per_period = 365 -reserve_margin = "static" [MGA] cost_epsilon = 0.03 diff --git a/tests/testing_configs/config_storageville.toml b/tests/testing_configs/config_storageville.toml index 79a2585d..a8216a6a 100644 --- a/tests/testing_configs/config_storageville.toml +++ b/tests/testing_configs/config_storageville.toml @@ -9,7 +9,6 @@ save_duals = false save_lp_file = false time_sequencing = "seasonal_timeslices" days_per_period = 365 -reserve_margin = "static" [MGA] slack = 0.1 diff --git a/tests/testing_configs/config_survival_curve.toml b/tests/testing_configs/config_survival_curve.toml index fa28822a..6796bb64 100644 --- a/tests/testing_configs/config_survival_curve.toml +++ b/tests/testing_configs/config_survival_curve.toml @@ -9,7 +9,6 @@ save_duals = false save_lp_file = false time_sequencing = "seasonal_timeslices" days_per_period = 365 -reserve_margin = "static" [MGA] cost_epsilon = 0.03 diff --git a/tests/testing_configs/config_test_system.toml b/tests/testing_configs/config_test_system.toml index 25acdfe5..7b5072a9 100644 --- a/tests/testing_configs/config_test_system.toml +++ b/tests/testing_configs/config_test_system.toml @@ -9,7 +9,6 @@ save_duals = false save_lp_file = false time_sequencing = "seasonal_timeslices" days_per_period = 365 -reserve_margin = "static" [MGA] slack = 0.1 diff --git a/tests/testing_configs/config_test_week.toml b/tests/testing_configs/config_test_week.toml index 484272ea..7e7a1fac 100644 --- a/tests/testing_configs/config_test_week.toml +++ b/tests/testing_configs/config_test_week.toml @@ -9,7 +9,6 @@ save_excel = false save_duals = false save_lp_file = false days_per_period = 7 -reserve_margin = "static" [MGA] slack = 0.1 diff --git a/tests/testing_configs/config_utopia.toml b/tests/testing_configs/config_utopia.toml index 1e7080c4..ec7d69f3 100644 --- a/tests/testing_configs/config_utopia.toml +++ b/tests/testing_configs/config_utopia.toml @@ -9,7 +9,6 @@ save_duals = false save_lp_file = false time_sequencing = "seasonal_timeslices" days_per_period = 365 -reserve_margin = "static" [MGA] cost_epsilon = 0.03 diff --git a/tests/testing_configs/config_utopia_gv.toml b/tests/testing_configs/config_utopia_gv.toml index cc5d93a5..deb7bfdf 100644 --- a/tests/testing_configs/config_utopia_gv.toml +++ b/tests/testing_configs/config_utopia_gv.toml @@ -9,7 +9,6 @@ save_duals = false save_lp_file = false time_sequencing = "seasonal_timeslices" days_per_period = 365 -reserve_margin = "static" graphviz_output = true [MGA] diff --git a/tests/testing_configs/config_utopia_mc.toml b/tests/testing_configs/config_utopia_mc.toml index 40f8993d..3eae138b 100644 --- a/tests/testing_configs/config_utopia_mc.toml +++ b/tests/testing_configs/config_utopia_mc.toml @@ -11,7 +11,6 @@ solver_name = "appsi_highs" time_sequencing = 'seasonal_timeslices' days_per_period = 365 -reserve_margin = 'static' [monte_carlo] run_settings = "tests/testing_data/mc_settings_utopia.csv" diff --git a/tests/testing_configs/config_utopia_myopic.toml b/tests/testing_configs/config_utopia_myopic.toml index 2744e980..4077dafc 100644 --- a/tests/testing_configs/config_utopia_myopic.toml +++ b/tests/testing_configs/config_utopia_myopic.toml @@ -9,7 +9,6 @@ save_duals = false save_lp_file = false time_sequencing = "seasonal_timeslices" days_per_period = 365 -reserve_margin = "static" [MGA] slack = 0.1 From 3e48b678ad12193218756ea29cf77d357e2a0ad7 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Fri, 7 Aug 2026 14:41:27 -0400 Subject: [PATCH 06/24] Remove tech_reserve set and also therefore rps_requirement constraint (replaced by limit_activity_share) Signed-off-by: Davey Elder --- temoa/components/limits.py | 36 ----------------------------- temoa/components/operations.py | 3 --- temoa/core/model.py | 15 +----------- temoa/data_io/component_manifest.py | 17 -------------- temoa/data_io/hybrid_loader.py | 16 ------------- temoa/model_checking/validators.py | 1 - temoa/types/model_types.py | 2 -- 7 files changed, 1 insertion(+), 89 deletions(-) diff --git a/temoa/components/limits.py b/temoa/components/limits.py index 35eacb05..3ce20101 100644 --- a/temoa/components/limits.py +++ b/temoa/components/limits.py @@ -160,42 +160,6 @@ def limit_annual_capacity_factor_indices( # ============================================================================ -# @deprecated('Deprecated. Use limit_activityGroupShare instead') # doesn't play well with pyomo -def renewable_portfolio_standard_constraint( - model: TemoaModel, r: Region, p: Period, g: str -) -> ExprLike: - r""" - Allows users to specify the share of electricity generation in a region - coming from RPS-eligible technologies. - """ - # devnote: this formulation leans on the reserve set, which is not necessarily - # the super set we want. We can also generalise this to all groups and so - # it has been deprecated in favour of the limit_activityGroupShare constraint. - - inp = quicksum( - model.v_flow_out[r, p, s, d, S_i, t, v, S_o] - for t in model.tech_group_members[g] - for (_t, v) in model.process_reserve_periods.get((r, p), []) - if _t == t - for s in model.time_season - for d in model.time_of_day - for S_i in model.process_inputs[r, p, t, v] - for S_o in model.process_outputs_by_input[r, p, t, v, S_i] - ) - - total_inp = quicksum( - model.v_flow_out[r, p, s, d, S_i, t, v, S_o] - for (t, v) in model.process_reserve_periods[r, p] - for s in model.time_season - for d in model.time_of_day - for S_i in model.process_inputs[r, p, t, v] - for S_o in model.process_outputs_by_input[r, p, t, v, S_i] - ) - - expr = inp >= (value(model.renewable_portfolio_standard[r, p, g]) * total_inp) - return expr - - def limit_resource_constraint(model: TemoaModel, r: Region, t: Technology, op: str) -> ExprLike: r""" diff --git a/temoa/components/operations.py b/temoa/components/operations.py index 3d6ab7df..0df5879e 100644 --- a/temoa/components/operations.py +++ b/temoa/components/operations.py @@ -175,7 +175,6 @@ def create_operational_vintage_sets(model: TemoaModel) -> None: for r, p, t in model.process_vintages: for v in model.process_vintages[r, p, t]: key_rpt = (r, p, t) - key_rp = (r, p) if t in model.tech_curtailment: model.curtailment_vintages.setdefault(key_rpt, set()).add(v) if t in model.tech_baseload: @@ -186,8 +185,6 @@ def create_operational_vintage_sets(model: TemoaModel) -> None: model.ramp_up_vintages.setdefault(key_rpt, set()).add(v) if t in model.tech_downramping: model.ramp_down_vintages.setdefault(key_rpt, set()).add(v) - if t in model.tech_reserve: - model.process_reserve_periods.setdefault(key_rp, set()).add((t, v)) # A dictionary of whether a storage tech is seasonal, just to speed things up for t in model.tech_storage: diff --git a/temoa/core/model.py b/temoa/core/model.py index af90abba..203fa68f 100755 --- a/temoa/core/model.py +++ b/temoa/core/model.py @@ -263,7 +263,6 @@ def __init__( self.tech_demand = Set(within=self.tech_all) # annual storage not supported in Storage constraint or TableWriter, so exclude from domain self.tech_storage = Set(within=self.tech_all) - self.tech_reserve = Set(within=self.tech_all) self.tech_upramping = Set(within=self.tech_all) self.tech_downramping = Set(within=self.tech_all) self.tech_curtailment = Set(within=self.tech_all) @@ -279,7 +278,7 @@ def __init__( self.tech_seasonal_storage = Set(within=self.tech_storage) """storage technologies using the interseasonal storage feature""" - self.tech_uncap = Set(within=self.tech_all - self.tech_reserve) + self.tech_uncap = Set(within=self.tech_all) """techs with unlimited capacity, ALWAYS available within lifespan""" self.tech_exist = Set() @@ -517,13 +516,6 @@ def __init__( validate=validate_0to1, ) - self.renewable_portfolio_standard_constraint_rpg = Set( - within=self.regions * self.time_optimize * self.tech_group_names - ) - self.renewable_portfolio_standard = Param( - self.renewable_portfolio_standard_constraint_rpg, validate=validate_0to1 - ) - # The method below creates a series of helper functions that are used to # perform the sparse matrix of indexing for the parameters, variables, and # equations below. @@ -1092,11 +1084,6 @@ def __init__( rule=limits.limit_tech_output_split_average_constraint, ) - self.renewable_portfolio_standard_constraint = Constraint( - self.renewable_portfolio_standard_constraint_rpg, - rule=limits.renewable_portfolio_standard_constraint, - ) - self.linked_emissions_tech_constraint_rpsdtve = Set( dimen=7, initialize=emissions.linked_tech_constraint_indices ) diff --git a/temoa/data_io/component_manifest.py b/temoa/data_io/component_manifest.py index 1372c30d..0db52163 100644 --- a/temoa/data_io/component_manifest.py +++ b/temoa/data_io/component_manifest.py @@ -101,15 +101,6 @@ def build_manifest(model: TemoaModel, extension_ids: Sequence[str] | None = None validation_map=(0,), is_period_filtered=False, ), - LoadItem( - component=model.tech_reserve, - table='technology', - columns=['tech'], - where_clause='reserve > 0', - validator_name='viable_techs', - validation_map=(0,), - is_period_filtered=False, - ), LoadItem( component=model.tech_curtailment, table='technology', @@ -498,14 +489,6 @@ def build_manifest(model: TemoaModel, extension_ids: Sequence[str] | None = None validation_map=(0,), is_period_filtered=False, ), - LoadItem( - component=model.renewable_portfolio_standard, - table='rps_requirement', - 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.planning_reserve_credit, table='planning_reserve_credit', diff --git a/temoa/data_io/hybrid_loader.py b/temoa/data_io/hybrid_loader.py index 6735093d..2c129af0 100644 --- a/temoa/data_io/hybrid_loader.py +++ b/temoa/data_io/hybrid_loader.py @@ -855,22 +855,6 @@ def _load_ramping_up( ) self._load_component_data(data, model.tech_upramping, tech_filtered) - def _load_rps_requirement( - self, - data: dict[str, object], - raw_data: Sequence[tuple[object, ...]], - filtered_data: Sequence[tuple[object, ...]], - ) -> None: - """Handles deprecation warning for renewable_portfolio_standard.""" - model = self.model - self._load_component_data(data, model.renewable_portfolio_standard, filtered_data) - if filtered_data: - logger.warning( - 'The renewable_portfolio_standard constraint is deprecated. Use ' - 'limit_activity_share instead. ' - 'The constraint has been applied but this feature may be removed in the future.' - ) - def _load_limit_tech_input_split( self, data: dict[str, object], diff --git a/temoa/model_checking/validators.py b/temoa/model_checking/validators.py index c76659c4..e84b524c 100644 --- a/temoa/model_checking/validators.py +++ b/temoa/model_checking/validators.py @@ -349,7 +349,6 @@ def validate_tech_sets(model: TemoaModel) -> None: check_no_intersection(model.tech_annual, model.tech_curtailment), check_no_intersection(model.tech_curtailment, model.tech_flex), check_no_intersection(model.tech_all, model.tech_group_names), - check_no_intersection(model.tech_uncap, model.tech_reserve), ) ): raise ValueError('Technology sets failed to validate. Check log file for details.') diff --git a/temoa/types/model_types.py b/temoa/types/model_types.py index 108a70f6..491ebb24 100644 --- a/temoa/types/model_types.py +++ b/temoa/types/model_types.py @@ -105,7 +105,6 @@ class TemoaModelProtocol(Protocol): tech_all: Set tech_production: Set tech_storage: Set - tech_reserve: Set tech_exchange: Set # Commodity sets @@ -176,7 +175,6 @@ class TemoaModel(AbstractModel): tech_baseload: Set tech_annual: Set tech_storage: Set - tech_reserve: Set tech_exchange: Set tech_uncap: Set tech_with_capacity: Set From c2fb278e3d4081385c2ff65eb7d64c7f22ffb1a4 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Fri, 7 Aug 2026 14:51:39 -0400 Subject: [PATCH 07/24] Create 4.1 schema with reserve update Signed-off-by: Davey Elder --- docs/source/database_schema.mmd | 67 +- temoa/db_schema/temoa_schema_v4_1.sql | 994 ++++++++++++++++++++++++++ tests/conftest.py | 2 +- 3 files changed, 1021 insertions(+), 42 deletions(-) create mode 100644 temoa/db_schema/temoa_schema_v4_1.sql diff --git a/docs/source/database_schema.mmd b/docs/source/database_schema.mmd index 84f58014..fef1f47a 100644 --- a/docs/source/database_schema.mmd +++ b/docs/source/database_schema.mmd @@ -1,10 +1,27 @@ erDiagram -capacity_credit { - INTEGER period PK +planning_reserve_credit { TEXT region PK TEXT tech PK - INTEGER vintage PK - REAL credit + REAL factor + TEXT notes +} +operating_reserve_derate { + TEXT region PK + TEXT season PK + TEXT tech PK + REAL factor + TEXT notes +} +planning_reserve_margin { + TEXT region PK + TEXT tech_or_group PK + REAL margin + TEXT notes +} +operating_reserve_margin { + TEXT region PK + TEXT tech_or_group PK + REAL margin TEXT notes } technology { @@ -16,7 +33,6 @@ technology { INTEGER exchange TEXT flag INTEGER flex - INTEGER reserve INTEGER retire INTEGER seas_stor TEXT sector @@ -539,11 +555,6 @@ output_storage_level { REAL level TEXT sector } -planning_reserve_margin { - TEXT region PK - REAL margin - TEXT notes -} ramp_down_hourly { TEXT region PK TEXT tech PK @@ -556,22 +567,6 @@ ramp_up_hourly { TEXT notes REAL rate } -reserve_capacity_derate { - INTEGER period PK - TEXT region PK - TEXT season PK - TEXT tech PK - INTEGER vintage PK - REAL factor - TEXT notes -} -rps_requirement { - TEXT notes - INTEGER period - TEXT region - REAL requirement - TEXT tech_group -} tech_group { TEXT group_name PK TEXT notes @@ -621,8 +616,11 @@ time_segment_fraction { TEXT notes REAL segment_fraction } -technology one or zero--0+ capacity_credit : has -time_period one or zero--0+ capacity_credit : has +region one or zero--0+ planning_reserve_credit : has +technology one or zero--0+ planning_reserve_credit : has +region one or zero--0+ operating_reserve_derate : has +technology one or zero--0+ operating_reserve_derate : has +time_season one or zero--0+ operating_reserve_derate : has technology_type 1--0+ technology : has time_period_type one or zero--0+ time_period : has technology one or zero--0+ capacity_factor_process : has @@ -698,15 +696,9 @@ operator 1--0+ limit_capacity : has time_period one or zero--0+ limit_capacity : has time_period one or zero--0+ limit_capacity_share : has operator 1--0+ limit_capacity_share : has -operator 1--0+ limit_degrowth_capacity : has -operator 1--0+ limit_degrowth_new_capacity : has -operator 1--0+ limit_degrowth_new_capacity_delta : has commodity one or zero--0+ limit_emission : has operator 1--0+ limit_emission : has time_period one or zero--0+ limit_emission : has -operator 1--0+ limit_growth_capacity : has -operator 1--0+ limit_growth_new_capacity : has -operator 1--0+ limit_growth_new_capacity_delta : has time_period one or zero--0+ limit_new_capacity : has operator 1--0+ limit_new_capacity : has operator 1--0+ limit_new_capacity_share : has @@ -797,15 +789,8 @@ sector_label one or zero--0+ output_storage_level : has time_period one or zero--0+ output_storage_level : has season_label one or zero--0+ output_storage_level : has time_of_day one or zero--0+ output_storage_level : has -region one or zero--1 planning_reserve_margin : has technology one or zero--0+ ramp_down_hourly : has technology one or zero--0+ ramp_up_hourly : has -technology one or zero--0+ reserve_capacity_derate : has -time_period one or zero--0+ reserve_capacity_derate : has -season_label one or zero--0+ reserve_capacity_derate : has -time_period 1--0+ rps_requirement : has -tech_group 1--0+ rps_requirement : has -region 1--0+ rps_requirement : has tech_group one or zero--0+ tech_group_member : has technology one or zero--0+ tech_group_member : has time_period one or zero--0+ time_season : has diff --git a/temoa/db_schema/temoa_schema_v4_1.sql b/temoa/db_schema/temoa_schema_v4_1.sql new file mode 100644 index 00000000..28040777 --- /dev/null +++ b/temoa/db_schema/temoa_schema_v4_1.sql @@ -0,0 +1,994 @@ +PRAGMA foreign_keys = OFF; +BEGIN TRANSACTION; + +CREATE TABLE IF NOT EXISTS metadata +( + element TEXT, + value INT, + notes TEXT, + PRIMARY KEY (element) +); +REPLACE INTO metadata +VALUES ('DB_MAJOR', 4, 'DB major version number'); +REPLACE INTO metadata +VALUES ('DB_MINOR', 0, 'DB minor version number'); + +CREATE TABLE IF NOT EXISTS metadata_real +( + element TEXT, + value REAL, + notes TEXT, + + PRIMARY KEY (element) +); +REPLACE INTO metadata_real +VALUES ('global_discount_rate', 0.05, 'Discount Rate for future costs'); +REPLACE INTO metadata_real +VALUES ('default_loan_rate', 0.05, 'Default Loan Rate if not specified in LoanRate table'); + +CREATE TABLE IF NOT EXISTS output_dual_variable +( + scenario TEXT, + constraint_name TEXT, + dual REAL, + PRIMARY KEY (constraint_name, scenario) +); +CREATE TABLE IF NOT EXISTS output_objective +( + scenario TEXT, + objective_name TEXT, + total_system_cost REAL +); +CREATE TABLE IF NOT EXISTS sector_label +( + sector TEXT PRIMARY KEY, + notes TEXT +); +CREATE TABLE IF NOT EXISTS 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) +); +CREATE TABLE IF NOT EXISTS 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) +); +CREATE TABLE IF NOT EXISTS capacity_to_activity +( + region TEXT, + tech TEXT + REFERENCES technology (tech), + c2a REAL, + units TEXT, + notes TEXT, + PRIMARY KEY (region, tech) +); +CREATE TABLE IF NOT EXISTS commodity +( + name TEXT + PRIMARY KEY, + flag TEXT + REFERENCES commodity_type (label), + description TEXT, + units TEXT +); +CREATE TABLE IF NOT EXISTS commodity_type +( + label TEXT + PRIMARY KEY, + description TEXT +); +REPLACE INTO commodity_type +VALUES ('s', 'source commodity'); +REPLACE INTO commodity_type +VALUES ('a', 'annual commodity'); +REPLACE INTO commodity_type +VALUES ('p', 'physical commodity'); +REPLACE INTO commodity_type +VALUES ('d', 'demand commodity'); +REPLACE INTO commodity_type +VALUES ('e', 'emissions 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'); +CREATE TABLE IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS 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) +); +CREATE TABLE IF NOT EXISTS 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) +); +CREATE TABLE IF NOT EXISTS 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) +); +CREATE TABLE IF NOT EXISTS 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) +); +CREATE TABLE IF NOT EXISTS 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) +); +CREATE TABLE IF NOT EXISTS 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 IF NOT EXISTS 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) +); +CREATE TABLE IF NOT EXISTS 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 IF NOT EXISTS 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) +); +CREATE TABLE IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS 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) +); +CREATE TABLE IF NOT EXISTS tech_group +( + group_name TEXT + PRIMARY KEY, + notes TEXT +); +CREATE TABLE IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS lifetime_tech +( + region TEXT, + tech TEXT + REFERENCES technology (tech), + lifetime REAL, + units TEXT, + notes TEXT, + PRIMARY KEY (region, tech) +); +CREATE TABLE IF NOT EXISTS operating_reserve_derate +( + region TEXT, + season TEXT + REFERENCES time_season (season), + tech TEXT + REFERENCES technology (tech), + factor REAL, + notes TEXT, + PRIMARY KEY (region, season, tech), + CHECK (factor >= 0 AND factor <= 1) +); +CREATE TABLE IF NOT EXISTS operating_reserve_margin +( + region TEXT, + tech_or_group TEXT, + margin REAL, + notes TEXT, + PRIMARY KEY (region, tech_or_group), + CHECK (margin >= 0) +); +CREATE TABLE IF NOT EXISTS operator +( + operator TEXT PRIMARY KEY, + notes TEXT +); +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_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 IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS 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) +); +CREATE TABLE IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS limit_seasonal_capacity_factor +( + region TEXT, + 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 IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS 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) +); +CREATE TABLE IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS 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 IF NOT EXISTS planning_reserve_credit +( + region TEXT, + tech TEXT + REFERENCES technology (tech), + credit REAL, + notes TEXT, + PRIMARY KEY (region, tech), + CHECK (credit >= 0 AND credit <= 1) +); +CREATE TABLE IF NOT EXISTS planning_reserve_margin +( + region TEXT, + tech_or_group TEXT, + margin REAL, + notes TEXT, + PRIMARY KEY (region, tech_or_group), + CHECK (margin >= 0) +); +CREATE TABLE IF NOT EXISTS ramp_down_hourly +( + region TEXT, + tech TEXT + REFERENCES technology (tech), + rate REAL, + notes TEXT, + PRIMARY KEY (region, tech) +); +CREATE TABLE IF NOT EXISTS ramp_up_hourly +( + region TEXT, + tech TEXT + REFERENCES technology (tech), + rate REAL, + notes TEXT, + PRIMARY KEY (region, tech) +); +CREATE TABLE IF NOT EXISTS region +( + region TEXT + PRIMARY KEY, + notes TEXT +); +CREATE TABLE IF NOT EXISTS storage_duration +( + region TEXT, + tech TEXT, + duration REAL, + notes TEXT, + PRIMARY KEY (region, tech) +); +CREATE TABLE IF NOT EXISTS 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 IF NOT EXISTS technology_type +( + label TEXT + PRIMARY KEY, + description TEXT +); +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'); +-- CREATE TABLE IF NOT EXISTS time_manual +-- ( +-- season TEXT +-- REFERENCES time_season (season), +-- tod TEXT +-- REFERENCES time_of_day (tod), +-- season_next TEXT +-- REFERENCES time_season (season), +-- tod_next TEXT +-- REFERENCES time_of_day (tod), +-- notes TEXT, +-- PRIMARY KEY (season, tod) +-- ); +CREATE TABLE IF NOT EXISTS time_of_day +( + sequence INTEGER UNIQUE, + tod TEXT + PRIMARY KEY, + hours REAL NOT NULL DEFAULT 1, + notes TEXT, + CHECK (hours > 0) +); +CREATE TABLE IF NOT EXISTS time_period +( + sequence INTEGER UNIQUE, + period INTEGER + PRIMARY KEY, + flag TEXT + REFERENCES time_period_type (label) +); +CREATE TABLE IF NOT EXISTS time_period_type +( + label TEXT + PRIMARY KEY, + description TEXT +); +REPLACE INTO time_period_type +VALUES('e', 'existing vintages'); +REPLACE INTO time_period_type +VALUES('f', 'future'); +CREATE TABLE IF NOT EXISTS 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 IF NOT EXISTS tech_group_member +( + group_name TEXT + REFERENCES tech_group (group_name), + tech TEXT + REFERENCES technology (tech), + PRIMARY KEY (group_name, tech) +); +CREATE TABLE IF NOT EXISTS 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, + 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) +); +CREATE TABLE IF NOT EXISTS output_cost +( + scenario TEXT, + region TEXT, + sector TEXT REFERENCES sector_label (sector), + period INTEGER REFERENCES time_period (period), + tech TEXT, + 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) +); + +CREATE TABLE IF NOT EXISTS 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) +); + +CREATE TABLE IF NOT EXISTS 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 TABLE IF NOT EXISTS 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) +); +-- for efficient searching by rtv: +CREATE INDEX IF NOT EXISTS region_tech_vintage ON myopic_efficiency (region, tech, vintage); + +CREATE TABLE IF NOT EXISTS 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) +); + +COMMIT; +PRAGMA foreign_keys = ON; diff --git a/tests/conftest.py b/tests/conftest.py index 444aebd5..8bd2ca76 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -37,7 +37,7 @@ # Central paths TEST_DATA_PATH = Path(__file__).parent / 'testing_data' TEST_OUTPUT_PATH = Path(__file__).parent / 'testing_outputs' -SCHEMA_PATH = Path(__file__).parent.parent / 'temoa' / 'db_schema' / 'temoa_schema_v4.sql' +SCHEMA_PATH = Path(__file__).parent.parent / 'temoa' / 'db_schema' / 'temoa_schema_v4_1.sql' def _build_test_db( From e1b3049260f230ba70e954f5bf56f844601b5207 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Fri, 7 Aug 2026 14:52:37 -0400 Subject: [PATCH 08/24] Update testing data Signed-off-by: Davey Elder --- temoa/tutorial_assets/utopia.sql | 38 +++++++++++------------ tests/testing_data/annualised_demand.sql | 6 ++-- tests/testing_data/emissions.sql | 16 +++++----- tests/testing_data/materials.sql | 38 +++++++++++------------ tests/testing_data/mediumville.sql | 26 ++++++++-------- tests/testing_data/mediumville_sets.json | 8 ++--- tests/testing_data/myopic_capacities.sql | 18 +++++------ tests/testing_data/seasonal_storage.sql | 8 ++--- tests/testing_data/simple_linked_tech.sql | 8 ++--- tests/testing_data/storageville.sql | 8 ++--- tests/testing_data/survival_curve.sql | 8 ++--- tests/testing_data/test_system.sql | 32 +++++++++---------- tests/testing_data/test_system_sets.json | 6 ++-- tests/testing_data/test_week.sql | 11 ++++--- tests/testing_data/utopia_data.sql | 38 +++++++++++------------ tests/testing_data/utopia_sets.json | 6 ++-- 16 files changed, 135 insertions(+), 140 deletions(-) diff --git a/temoa/tutorial_assets/utopia.sql b/temoa/tutorial_assets/utopia.sql index 1814f34f..8117d2c4 100644 --- a/temoa/tutorial_assets/utopia.sql +++ b/temoa/tutorial_assets/utopia.sql @@ -415,25 +415,25 @@ 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 "technology" VALUES('IMPDSL1','p','supply','petroleum','',1,0,0,0,0,0,0,' imported diesel'); +REPLACE INTO "technology" VALUES('IMPGSL1','p','supply','petroleum','',1,0,0,0,0,0,0,' imported gasoline'); +REPLACE INTO "technology" VALUES('IMPHCO1','p','supply','coal','',1,0,0,0,0,0,0,' imported coal'); +REPLACE INTO "technology" VALUES('IMPOIL1','p','supply','petroleum','',1,0,0,0,0,0,0,' imported crude oil'); +REPLACE INTO "technology" VALUES('IMPURN1','p','supply','nuclear','',1,0,0,0,0,0,0,' imported uranium'); +REPLACE INTO "technology" VALUES('IMPFEQ','p','supply','petroleum','',1,0,0,0,0,0,0,' imported fossil equivalent'); +REPLACE INTO "technology" VALUES('IMPHYD','p','supply','hydro','',1,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,' coal power plant'); +REPLACE INTO "technology" VALUES('E21','pb','electric','nuclear','',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,' hydro power'); +REPLACE INTO "technology" VALUES('E51','ps','electric','electric','',0,0,0,0,0,0,0,' electric storage'); +REPLACE INTO "technology" VALUES('E70','p','electric','petroleum','',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,' electric residential heating'); +REPLACE INTO "technology" VALUES('RHO','p','residential','petroleum','',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,' residential lighting'); +REPLACE INTO "technology" VALUES('SRE','p','supply','petroleum','',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,' diesel powered vehicles'); +REPLACE INTO "technology" VALUES('TXE','p','transport','electric','',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,' 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'); diff --git a/tests/testing_data/annualised_demand.sql b/tests/testing_data/annualised_demand.sql index 76d91092..0be2f5eb 100644 --- a/tests/testing_data/annualised_demand.sql +++ b/tests/testing_data/annualised_demand.sql @@ -39,9 +39,9 @@ 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 "region" VALUES('region',NULL); -REPLACE INTO "technology" VALUES('annual','p','energy',NULL,NULL,0,1,0,0,0,0,0,0,NULL); -REPLACE INTO "technology" VALUES('import','p','energy',NULL,NULL,0,0,0,0,0,0,0,0,NULL); -REPLACE INTO "technology" VALUES('non_annual','p','energy',NULL,NULL,0,0,0,0,0,0,0,0,NULL); +REPLACE INTO "technology" VALUES('annual','p','energy',NULL,NULL,0,1,0,0,0,0,0,NULL); +REPLACE INTO "technology" VALUES('import','p','energy',NULL,NULL,0,0,0,0,0,0,0,NULL); +REPLACE INTO "technology" VALUES('non_annual','p','energy',NULL,NULL,0,0,0,0,0,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'); diff --git a/tests/testing_data/emissions.sql b/tests/testing_data/emissions.sql index 0aaa11b0..5b6a6596 100644 --- a/tests/testing_data/emissions.sql +++ b/tests/testing_data/emissions.sql @@ -75,14 +75,14 @@ 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 "region" VALUES('Testregion',NULL); -REPLACE INTO "technology" VALUES('TechAnnual','p','energy',NULL,NULL,0,1,0,0,0,0,0,0,NULL); -REPLACE INTO "technology" VALUES('TechFlex','p','energy',NULL,NULL,0,0,0,0,0,1,0,0,NULL); -REPLACE INTO "technology" VALUES('TechOrdinary','p','energy',NULL,NULL,0,0,0,0,0,0,0,0,NULL); -REPLACE INTO "technology" VALUES('TechCurtailment','p','energy',NULL,NULL,0,0,0,1,0,0,0,0,NULL); -REPLACE INTO "technology" VALUES('TechFlexNull','p','energy',NULL,NULL,0,0,0,0,0,0,0,0,NULL); -REPLACE INTO "technology" VALUES('TechAnnualFlex','p','energy',NULL,NULL,0,1,0,0,0,1,0,0,NULL); -REPLACE INTO "technology" VALUES('TechEmbodied','p','energy',NULL,NULL,0,0,0,0,0,0,0,0,NULL); -REPLACE INTO "technology" VALUES('TechEndOfLife','p','energy',NULL,NULL,0,0,0,0,0,0,0,0,NULL); +REPLACE INTO "technology" VALUES('TechAnnual','p','energy',NULL,NULL,0,1,0,0,0,0,0,NULL); +REPLACE INTO "technology" VALUES('TechFlex','p','energy',NULL,NULL,0,0,0,0,1,0,0,NULL); +REPLACE INTO "technology" VALUES('TechOrdinary','p','energy',NULL,NULL,0,0,0,0,0,0,0,NULL); +REPLACE INTO "technology" VALUES('TechCurtailment','p','energy',NULL,NULL,0,0,1,0,0,0,0,NULL); +REPLACE INTO "technology" VALUES('TechFlexNull','p','energy',NULL,NULL,0,0,0,0,0,0,0,NULL); +REPLACE INTO "technology" VALUES('TechAnnualFlex','p','energy',NULL,NULL,0,1,0,0,1,0,0,NULL); +REPLACE INTO "technology" VALUES('TechEmbodied','p','energy',NULL,NULL,0,0,0,0,0,0,0,NULL); +REPLACE INTO "technology" VALUES('TechEndOfLife','p','energy',NULL,NULL,0,0,0,0,0,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'); diff --git a/tests/testing_data/materials.sql b/tests/testing_data/materials.sql index 95f24b4f..90837130 100644 --- a/tests/testing_data/materials.sql +++ b/tests/testing_data/materials.sql @@ -345,25 +345,25 @@ REPLACE INTO "sector_label" VALUES('residential',NULL); REPLACE INTO "sector_label" VALUES('fuels',NULL); REPLACE INTO "storage_duration" VALUES('regionA','BATT_GRID',2.0,'2 hours energy storage'); REPLACE INTO "storage_duration" VALUES('regionB','BATT_GRID',2.0,'2 hours energy storage'); -REPLACE INTO "technology" VALUES('IMPORT_LI','p','materials',NULL,NULL,1,1,0,0,0,0,0,0,'lithium importer'); -REPLACE INTO "technology" VALUES('IMPORT_CO','p','materials',NULL,NULL,1,1,0,0,0,0,0,0,'cobalt importer'); -REPLACE INTO "technology" VALUES('IMPORT_P','p','materials',NULL,NULL,1,1,0,0,0,0,0,0,'phosphorous importer'); -REPLACE INTO "technology" VALUES('CAR_BEV','p','transportation',NULL,NULL,0,0,0,0,0,0,0,0,'car - battery electric'); -REPLACE INTO "technology" VALUES('CAR_PHEV','p','transportation',NULL,NULL,0,0,0,0,0,0,0,0,'car - plug in hybrid'); -REPLACE INTO "technology" VALUES('CAR_ICE','p','transportation',NULL,NULL,0,0,0,0,0,0,0,0,'car - internal combustion'); -REPLACE INTO "technology" VALUES('RECYCLE_NMC','p','materials',NULL,NULL,0,1,0,0,0,0,0,0,'nmc battery recycler'); -REPLACE INTO "technology" VALUES('RECYCLE_LFP','p','materials',NULL,NULL,0,1,0,0,0,0,0,0,'lfp battery recycler'); -REPLACE INTO "technology" VALUES('MANUFAC_NMC','p','materials',NULL,NULL,0,1,0,0,0,0,0,0,'nmc battery manufacturing'); -REPLACE INTO "technology" VALUES('MANUFAC_LFP','p','materials',NULL,NULL,0,1,0,0,0,0,0,0,'lfp battery manufacturing'); -REPLACE INTO "technology" VALUES('IMPORT_NI','p','materials',NULL,NULL,1,1,0,0,0,0,0,0,'nickel importer'); -REPLACE INTO "technology" VALUES('DOMESTIC_NI','p','materials',NULL,NULL,1,1,0,0,0,0,0,0,'domestic nickel production'); -REPLACE INTO "technology" VALUES('GEN_DSL','p','electricity',NULL,NULL,0,0,0,0,0,0,0,0,'diesel generators'); -REPLACE INTO "technology" VALUES('SOL_PV','p','electricity',NULL,NULL,0,0,0,1,0,0,0,0,'solar panels'); -REPLACE INTO "technology" VALUES('BATT_GRID','ps','electricity',NULL,NULL,0,0,0,0,0,0,0,0,'grid battery storage'); -REPLACE INTO "technology" VALUES('FURNACE','p','residential',NULL,NULL,1,0,0,0,0,0,0,0,'diesel furnace heater'); -REPLACE INTO "technology" VALUES('HEATPUMP','p','residential',NULL,NULL,1,0,0,0,0,0,0,0,'heat pump'); -REPLACE INTO "technology" VALUES('IMPORT_DSL','p','fuels',NULL,NULL,1,1,0,0,0,0,0,0,'diesel importer'); -REPLACE INTO "technology" VALUES('ELEC_INTERTIE','p','electricity',NULL,NULL,0,0,0,0,0,0,1,0,'dummy tech to make landfill feasible'); +REPLACE INTO "technology" VALUES('IMPORT_LI','p','materials',NULL,NULL,1,1,0,0,0,0,0,'lithium importer'); +REPLACE INTO "technology" VALUES('IMPORT_CO','p','materials',NULL,NULL,1,1,0,0,0,0,0,'cobalt importer'); +REPLACE INTO "technology" VALUES('IMPORT_P','p','materials',NULL,NULL,1,1,0,0,0,0,0,'phosphorous importer'); +REPLACE INTO "technology" VALUES('CAR_BEV','p','transportation',NULL,NULL,0,0,0,0,0,0,0,'car - battery electric'); +REPLACE INTO "technology" VALUES('CAR_PHEV','p','transportation',NULL,NULL,0,0,0,0,0,0,0,'car - plug in hybrid'); +REPLACE INTO "technology" VALUES('CAR_ICE','p','transportation',NULL,NULL,0,0,0,0,0,0,0,'car - internal combustion'); +REPLACE INTO "technology" VALUES('RECYCLE_NMC','p','materials',NULL,NULL,0,1,0,0,0,0,0,'nmc battery recycler'); +REPLACE INTO "technology" VALUES('RECYCLE_LFP','p','materials',NULL,NULL,0,1,0,0,0,0,0,'lfp battery recycler'); +REPLACE INTO "technology" VALUES('MANUFAC_NMC','p','materials',NULL,NULL,0,1,0,0,0,0,0,'nmc battery manufacturing'); +REPLACE INTO "technology" VALUES('MANUFAC_LFP','p','materials',NULL,NULL,0,1,0,0,0,0,0,'lfp battery manufacturing'); +REPLACE INTO "technology" VALUES('IMPORT_NI','p','materials',NULL,NULL,1,1,0,0,0,0,0,'nickel importer'); +REPLACE INTO "technology" VALUES('DOMESTIC_NI','p','materials',NULL,NULL,1,1,0,0,0,0,0,'domestic nickel production'); +REPLACE INTO "technology" VALUES('GEN_DSL','p','electricity',NULL,NULL,0,0,0,0,0,0,0,'diesel generators'); +REPLACE INTO "technology" VALUES('SOL_PV','p','electricity',NULL,NULL,0,0,1,0,0,0,0,'solar panels'); +REPLACE INTO "technology" VALUES('BATT_GRID','ps','electricity',NULL,NULL,0,0,0,0,0,0,0,'grid battery storage'); +REPLACE INTO "technology" VALUES('FURNACE','p','residential',NULL,NULL,1,0,0,0,0,0,0,'diesel furnace heater'); +REPLACE INTO "technology" VALUES('HEATPUMP','p','residential',NULL,NULL,1,0,0,0,0,0,0,'heat pump'); +REPLACE INTO "technology" VALUES('IMPORT_DSL','p','fuels',NULL,NULL,1,1,0,0,0,0,0,'diesel importer'); +REPLACE INTO "technology" VALUES('ELEC_INTERTIE','p','electricity',NULL,NULL,0,0,0,0,0,1,0,'dummy tech to make landfill feasible'); 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'); diff --git a/tests/testing_data/mediumville.sql b/tests/testing_data/mediumville.sql index 2f253a8c..42b4af67 100644 --- a/tests/testing_data/mediumville.sql +++ b/tests/testing_data/mediumville.sql @@ -1,4 +1,4 @@ -REPLACE INTO "capacity_credit" VALUES('A',2025,'EF',2025,0.6,NULL); +REPLACE INTO "planning_reserve_credit" VALUES('A','EF',0.6,NULL); REPLACE INTO "capacity_factor_process" VALUES('A','s2','d1','EFL',2025,0.8,NULL); REPLACE INTO "capacity_factor_process" VALUES('A','s1','d2','EFL',2025,0.9,NULL); REPLACE INTO "capacity_factor_tech" VALUES('A','s1','d1','EF',0.8,NULL); @@ -108,6 +108,7 @@ REPLACE INTO "limit_activity" VALUES('B',2025,'EH','le',10000.0,'stuff',NULL); REPLACE INTO "limit_activity" VALUES('A',2025,'EF','le',10000.0,'stuff',NULL); REPLACE INTO "limit_activity" VALUES('A',2025,'A_tech_grp_1','ge',0.05,'',NULL); REPLACE INTO "limit_activity" VALUES('A',2025,'A_tech_grp_1','le',10000.0,'',NULL); +REPLACE INTO "limit_activity_share" VALUES('B',2025,'EF','RPS_common','ge',0.3,NULL); REPLACE INTO "limit_capacity" VALUES('A',2025,'EH','ge',0.1,'',''); REPLACE INTO "limit_capacity" VALUES('B',2025,'batt','ge',0.1,'',''); REPLACE INTO "limit_capacity" VALUES('A',2025,'EH','le',20000.0,'',''); @@ -146,14 +147,13 @@ REPLACE INTO "metadata_real" VALUES('global_discount_rate',4.2000000000000004e-0 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 "planning_reserve_margin" VALUES('A',0.05,NULL); +REPLACE INTO "planning_reserve_margin" VALUES('A','EF',0.05,NULL); REPLACE INTO "ramp_down_hourly" VALUES('A','EH',0.05,NULL); REPLACE INTO "ramp_down_hourly" VALUES('B','EH',0.05,NULL); REPLACE INTO "ramp_up_hourly" VALUES('B','EH',0.05,NULL); REPLACE INTO "ramp_up_hourly" VALUES('A','EH',0.05,NULL); REPLACE INTO "region" VALUES('A','main region'); REPLACE INTO "region" VALUES('B','just a 2nd region'); -REPLACE INTO "rps_requirement" VALUES('B',2025,'RPS_common',0.3,NULL); REPLACE INTO "sector_label" VALUES('supply',NULL); REPLACE INTO "sector_label" VALUES('electric',NULL); REPLACE INTO "sector_label" VALUES('transport',NULL); @@ -166,16 +166,16 @@ REPLACE INTO "tech_group" VALUES('A_tech_grp_1','converted from old db'); REPLACE INTO "tech_group_member" VALUES('RPS_common','EF'); REPLACE INTO "tech_group_member" VALUES('A_tech_grp_1','EH'); REPLACE INTO "tech_group_member" VALUES('A_tech_grp_1','EF'); -REPLACE INTO "technology" VALUES('well','p','supply','water','',0,0,0,0,0,0,0,0,'plain old water'); -REPLACE INTO "technology" VALUES('bulbs','p','residential','electric','',0,0,0,0,0,0,0,0,'residential lighting'); -REPLACE INTO "technology" VALUES('EH','pb','electric','hydro','',0,0,0,1,1,0,0,0,'hydro power electric plant'); -REPLACE INTO "technology" VALUES('batt','ps','electric','electric','',0,0,0,0,0,0,0,0,'big battery'); -REPLACE INTO "technology" VALUES('EF','p','electric','electric','',0,0,1,0,0,0,0,0,'fusion plant'); -REPLACE INTO "technology" VALUES('EFL','p','electric','electric','',0,0,0,0,0,1,0,0,'linked (to Fusion) producer'); -REPLACE INTO "technology" VALUES('heater','p','residential','electric','',0,0,0,0,0,0,0,0,'heater'); -REPLACE INTO "technology" VALUES('FGF_pipe','p','transport',NULL,'',0,0,0,0,0,0,1,0,'transportation line A->B'); -REPLACE INTO "technology" VALUES('GeoThermal','p','residential','hydro','',0,1,0,0,0,0,0,0,'geothermal hot water source'); -REPLACE INTO "technology" VALUES('GeoHeater','p','residential','hydro','',0,0,0,0,0,0,0,0,'geothermal heater from geo hyd'); +REPLACE INTO "technology" VALUES('well','p','supply','water','',0,0,0,0,0,0,0,'plain old water'); +REPLACE INTO "technology" VALUES('bulbs','p','residential','electric','',0,0,0,0,0,0,0,'residential lighting'); +REPLACE INTO "technology" VALUES('EH','pb','electric','hydro','',0,0,1,1,0,0,0,'hydro power electric plant'); +REPLACE INTO "technology" VALUES('batt','ps','electric','electric','',0,0,0,0,0,0,0,'big battery'); +REPLACE INTO "technology" VALUES('EF','p','electric','electric','',0,0,0,0,0,0,0,'fusion plant'); +REPLACE INTO "technology" VALUES('EFL','p','electric','electric','',0,0,0,0,1,0,0,'linked (to Fusion) producer'); +REPLACE INTO "technology" VALUES('heater','p','residential','electric','',0,0,0,0,0,0,0,'heater'); +REPLACE INTO "technology" VALUES('FGF_pipe','p','transport',NULL,'',0,0,0,0,0,1,0,'transportation line A->B'); +REPLACE INTO "technology" VALUES('GeoThermal','p','residential','hydro','',0,1,0,0,0,0,0,'geothermal hot water source'); +REPLACE INTO "technology" VALUES('GeoHeater','p','residential','hydro','',0,0,0,0,0,0,0,'geothermal heater from geo hyd'); 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'); diff --git a/tests/testing_data/mediumville_sets.json b/tests/testing_data/mediumville_sets.json index 3aca6a68..a56a77ab 100644 --- a/tests/testing_data/mediumville_sets.json +++ b/tests/testing_data/mediumville_sets.json @@ -34,7 +34,7 @@ "lifetime_tech_rt": "6786f2e9acdccce907ebad50d2ea70481bff72a749253e78168d29b480b07f65", "lifetime_process_rtv": "212639322f29aa7687eb9962fb9d74ef60961d720a2a753e2af53473b9e1648f", "limit_activity_constraint_rpt": "90461349933d0b32167abdca242233004b66212b57ed57213dce6f6818203963", - "limit_activity_share_constraint_rpgg": "(empty)", + "limit_activity_share_constraint_rpgg": "0df34850f785baf3f30a829f838bb853edd65e1969b3df81885a2ef40c130495", "limit_annual_capacity_factor_constraint_rptvo": "(empty)", "limit_annual_capacity_factor_constraint_rtvo": "(empty)", "limit_capacity_constraint_rpt": "118826dabd14af75ae09adbd5bdbba750528b3d59907cbfba3690223ffc7def7", @@ -56,8 +56,10 @@ "linked_emissions_tech_constraint_rpsdtve": "f5accc0ee9eaaf16e584566a50e576405f8b5e1eb7d13815b87cb2f8b33a19b8", "loan_lifetime_process_rtv": "212639322f29aa7687eb9962fb9d74ef60961d720a2a753e2af53473b9e1648f", "new_capacity_var_rtv": "212639322f29aa7687eb9962fb9d74ef60961d720a2a753e2af53473b9e1648f", + "operating_reserve_rpsdt": "(empty)", "operator": "74d830836f1399fb336a0432dde7d7bd36cffa3ff76b1c42d7945350cfb9bf91", "ordered_season_sequential": "(empty)", + "planning_reserve_rpsdt": "e39f44175bf0c956bfc8083bfaeef1ab450d2cf9c0963327a62e52595e902bf4", "process_life_frac_rptv": "fdb177f119430de12c8b1083c9275e3a256f45f859d6b6819a84bff5c8e92184", "ramp_down_constraint_rpsdtv": "d766f05fa377b69768214641fda5d15617b697a43e64c863fe53503f11db4a89", "ramp_up_constraint_rpsdtv": "d766f05fa377b69768214641fda5d15617b697a43e64c863fe53503f11db4a89", @@ -65,9 +67,6 @@ "regional_global_indices": "a4bd2969735cb437072971a2fa02a2d8fbb20a707a1bdfa695b92922daeb5d10", "regional_indices": "6d8dc3bc6dc8cd485bcf2a59d752df20974b0fd78cc623e3a51e705a01edeea4", "regions": "f2314439597c11e286fd3c2fdf8eb70d3739e136f83e9533249ce3ecc43ece3e", - "renewable_portfolio_standard_constraint_rpg": "ab39415b5df1f906b11f06034cbfca390f7332b6a17adae60a71a74d7b421f67", - "reserve_margin_method": "7869283c0d14273f720716309207a8f0c24606d03c679d6b68e656ed8d86241d", - "reserve_margin_rpsd": "9bd91053e57055456e04648c00d8e581addc9aa503cf31cf44c7a439c6f4fe0f", "retired_capacity_var_rptv": "(empty)", "seasonal_storage_constraints_rpsdtv": "(empty)", "seasonal_storage_level_rpstv": "(empty)", @@ -87,7 +86,6 @@ "tech_group_names": "6b923047f11d1c00a828b0f02aed79e853d81fd35ffd4667fbdc828ce356e515", "tech_or_group": "4899ca300eb8ed5ba168b1f2ee669cacd36d041f493fb3dd00218d6764598cf9", "tech_production": "24f75b6f705a36033456d02638e7c50667908c45c474151fb8490666d928c63f", - "tech_reserve": "b28d0fd7ec11abdd0e645c2a9d83b05c08ecadcfb944d4a7bbd844ff4b83bfbf", "tech_retirement": "ddcf6ff7665c2a8acc4dff1b43655fae1b5a265135cbee18cec638df4e954346", "tech_seasonal_storage": "(empty)", "tech_storage": "e4e91128cadba8c633bf98029cf6666adc3959c63967c6f396028b78e2e9f0cf", diff --git a/tests/testing_data/myopic_capacities.sql b/tests/testing_data/myopic_capacities.sql index ca11c415..65c122ab 100644 --- a/tests/testing_data/myopic_capacities.sql +++ b/tests/testing_data/myopic_capacities.sql @@ -234,18 +234,18 @@ 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); 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 technology VALUES('tech_ancient','p','energy',NULL,NULL,0,0,0,1,0,0,0,NULL); +REPLACE INTO technology VALUES('tech_old','p','energy',NULL,NULL,0,0,0,1,0,0,0,NULL); +REPLACE INTO technology VALUES('tech_current','p','energy',NULL,NULL,0,0,0,1,0,0,0,NULL); +REPLACE INTO technology VALUES('tech_future','p','energy',NULL,NULL,0,0,0,1,0,0,0,NULL); +REPLACE INTO technology VALUES('tech_retire','p','energy',NULL,NULL,0,0,0,1,0,0,0,NULL); +REPLACE INTO technology VALUES('tech_discrete_cap','p','energy',NULL,NULL,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,NULL); +REPLACE INTO technology VALUES('tech_discrete_new_cap','p','energy',NULL,NULL,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,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); diff --git a/tests/testing_data/seasonal_storage.sql b/tests/testing_data/seasonal_storage.sql index 19b394b3..a1cbad53 100644 --- a/tests/testing_data/seasonal_storage.sql +++ b/tests/testing_data/seasonal_storage.sql @@ -53,10 +53,10 @@ REPLACE INTO "region" VALUES('region',NULL); REPLACE INTO "sector_label" VALUES('electricity',NULL); REPLACE INTO "storage_duration" VALUES('region','dly_stor',4.0,NULL); REPLACE INTO "storage_duration" VALUES('region','seas_stor',8760.0,NULL); -REPLACE INTO "technology" VALUES('generator','p','electricity',NULL,NULL,0,0,0,0,0,0,0,0,NULL); -REPLACE INTO "technology" VALUES('dly_stor','ps','electricity',NULL,NULL,0,0,0,0,0,0,0,0,NULL); -REPLACE INTO "technology" VALUES('seas_stor','ps','electricity',NULL,NULL,0,0,0,0,0,0,0,1,NULL); -REPLACE INTO "technology" VALUES('demand','p','electricity',NULL,NULL,0,0,0,0,0,0,0,0,NULL); +REPLACE INTO "technology" VALUES('generator','p','electricity',NULL,NULL,0,0,0,0,0,0,0,NULL); +REPLACE INTO "technology" VALUES('dly_stor','ps','electricity',NULL,NULL,0,0,0,0,0,0,0,NULL); +REPLACE INTO "technology" VALUES('seas_stor','ps','electricity',NULL,NULL,0,0,0,0,0,0,1,NULL); +REPLACE INTO "technology" VALUES('demand','p','electricity',NULL,NULL,0,0,0,0,0,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'); diff --git a/tests/testing_data/simple_linked_tech.sql b/tests/testing_data/simple_linked_tech.sql index 25760ebc..025443e6 100644 --- a/tests/testing_data/simple_linked_tech.sql +++ b/tests/testing_data/simple_linked_tech.sql @@ -41,10 +41,10 @@ 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('PLANT','p','supply',NULL,NULL,0,0,0,0,0,0,0,0,NULL); -REPLACE INTO "technology" VALUES('CCS','p','supply',NULL,NULL,0,0,0,0,0,0,0,0,NULL); -REPLACE INTO "technology" VALUES('MINE','p','supply',NULL,NULL,0,0,0,0,0,0,0,0,NULL); -REPLACE INTO "technology" VALUES('FAKE_SOURCE','p','supply',NULL,NULL,1,0,0,0,0,0,0,0,NULL); +REPLACE INTO "technology" VALUES('PLANT','p','supply',NULL,NULL,0,0,0,0,0,0,0,NULL); +REPLACE INTO "technology" VALUES('CCS','p','supply',NULL,NULL,0,0,0,0,0,0,0,NULL); +REPLACE INTO "technology" VALUES('MINE','p','supply',NULL,NULL,0,0,0,0,0,0,0,NULL); +REPLACE INTO "technology" VALUES('FAKE_SOURCE','p','supply',NULL,NULL,1,0,0,0,0,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'); diff --git a/tests/testing_data/storageville.sql b/tests/testing_data/storageville.sql index e643378b..c7c68125 100644 --- a/tests/testing_data/storageville.sql +++ b/tests/testing_data/storageville.sql @@ -56,10 +56,10 @@ REPLACE INTO "sector_label" VALUES('commercial',NULL); REPLACE INTO "sector_label" VALUES('residential',NULL); REPLACE INTO "sector_label" VALUES('industrial',NULL); REPLACE INTO "storage_duration" VALUES('electricville','batt',10.0,NULL); -REPLACE INTO "technology" VALUES('well','p','supply','water','',0,0,0,0,0,0,0,0,'plain old water'); -REPLACE INTO "technology" VALUES('bulbs','p','residential','electric','',0,0,0,0,0,0,0,0,' residential lighting'); -REPLACE INTO "technology" VALUES('EH','pb','electric','hydro','',0,0,0,0,0,0,0,0,'hydro power electric plant'); -REPLACE INTO "technology" VALUES('batt','ps','electric','electric','',0,0,0,0,0,0,0,0,'big battery'); +REPLACE INTO "technology" VALUES('well','p','supply','water','',0,0,0,0,0,0,0,'plain old water'); +REPLACE INTO "technology" VALUES('bulbs','p','residential','electric','',0,0,0,0,0,0,0,' residential lighting'); +REPLACE INTO "technology" VALUES('EH','pb','electric','hydro','',0,0,0,0,0,0,0,'hydro power electric plant'); +REPLACE INTO "technology" VALUES('batt','ps','electric','electric','',0,0,0,0,0,0,0,'big battery'); 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'); diff --git a/tests/testing_data/survival_curve.sql b/tests/testing_data/survival_curve.sql index a0f32dc1..cd2e5a4e 100644 --- a/tests/testing_data/survival_curve.sql +++ b/tests/testing_data/survival_curve.sql @@ -150,10 +150,10 @@ 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 "region" VALUES('region',NULL); -REPLACE INTO "technology" VALUES('tech_ancient','p','energy',NULL,NULL,0,0,0,0,0,0,0,0,NULL); -REPLACE INTO "technology" VALUES('tech_old','p','energy',NULL,NULL,0,0,0,0,0,0,0,0,NULL); -REPLACE INTO "technology" VALUES('tech_current','p','energy',NULL,NULL,0,0,0,0,0,0,0,0,NULL); -REPLACE INTO "technology" VALUES('tech_future','p','energy',NULL,NULL,0,0,0,0,0,0,0,0,NULL); +REPLACE INTO "technology" VALUES('tech_ancient','p','energy',NULL,NULL,0,0,0,0,0,0,0,NULL); +REPLACE INTO "technology" VALUES('tech_old','p','energy',NULL,NULL,0,0,0,0,0,0,0,NULL); +REPLACE INTO "technology" VALUES('tech_current','p','energy',NULL,NULL,0,0,0,0,0,0,0,NULL); +REPLACE INTO "technology" VALUES('tech_future','p','energy',NULL,NULL,0,0,0,0,0,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'); diff --git a/tests/testing_data/test_system.sql b/tests/testing_data/test_system.sql index a50226c8..2d9b11de 100644 --- a/tests/testing_data/test_system.sql +++ b/tests/testing_data/test_system.sql @@ -454,22 +454,22 @@ REPLACE INTO "sector_label" VALUES('residential',NULL); REPLACE INTO "sector_label" VALUES('industrial',NULL); REPLACE INTO "storage_duration" VALUES('R1','E_BATT',8.0,'8-hour duration specified as fraction of a day'); REPLACE INTO "storage_duration" VALUES('R2','E_BATT',8.0,'8-hour duration specified as fraction of a day'); -REPLACE INTO "technology" VALUES('S_IMPETH','p','supply','','',1,0,0,0,0,0,0,0,' imported ethanol'); -REPLACE INTO "technology" VALUES('S_IMPOIL','p','supply','','',1,0,0,0,0,0,0,0,' imported crude oil'); -REPLACE INTO "technology" VALUES('S_IMPNG','p','supply','','',1,0,0,0,0,0,0,0,' imported natural gas'); -REPLACE INTO "technology" VALUES('S_IMPURN','p','supply','','',1,0,0,0,0,0,0,0,' imported uranium'); -REPLACE INTO "technology" VALUES('S_OILREF','p','supply','','',0,0,0,1,0,0,0,0,' crude oil refinery'); -REPLACE INTO "technology" VALUES('E_NGCC','p','electric','','',0,0,0,0,0,0,0,0,' natural gas combined-cycle'); -REPLACE INTO "technology" VALUES('E_SOLPV','p','electric','','',0,0,0,0,0,0,0,0,' solar photovoltaic'); -REPLACE INTO "technology" VALUES('E_BATT','ps','electric','','',0,0,0,0,0,0,0,0,' lithium-ion battery'); -REPLACE INTO "technology" VALUES('E_NUCLEAR','pb','electric','','',0,0,0,0,0,0,0,0,' nuclear power plant'); -REPLACE INTO "technology" VALUES('T_BLND','p','transport','','',0,0,0,0,0,0,0,0,'ethanol - gasoline blending process'); -REPLACE INTO "technology" VALUES('T_DSL','p','transport','','',0,0,0,0,0,0,0,0,'diesel vehicle'); -REPLACE INTO "technology" VALUES('T_GSL','p','transport','','',0,0,0,0,0,0,0,0,'gasoline vehicle'); -REPLACE INTO "technology" VALUES('T_EV','p','transport','','',0,0,0,0,0,0,0,0,'electric vehicle'); -REPLACE INTO "technology" VALUES('R_EH','p','residential','','',0,0,0,0,0,0,0,0,' electric residential heating'); -REPLACE INTO "technology" VALUES('R_NGH','p','residential','','',0,0,0,0,0,0,0,0,' natural gas residential heating'); -REPLACE INTO "technology" VALUES('E_TRANS','p','electric','','',0,0,0,0,0,0,1,0,'electric transmission'); +REPLACE INTO "technology" VALUES('S_IMPETH','p','supply','','',1,0,0,0,0,0,0,' imported ethanol'); +REPLACE INTO "technology" VALUES('S_IMPOIL','p','supply','','',1,0,0,0,0,0,0,' imported crude oil'); +REPLACE INTO "technology" VALUES('S_IMPNG','p','supply','','',1,0,0,0,0,0,0,' imported natural gas'); +REPLACE INTO "technology" VALUES('S_IMPURN','p','supply','','',1,0,0,0,0,0,0,' imported uranium'); +REPLACE INTO "technology" VALUES('S_OILREF','p','supply','','',0,0,1,0,0,0,0,' crude oil refinery'); +REPLACE INTO "technology" VALUES('E_NGCC','p','electric','','',0,0,0,0,0,0,0,' natural gas combined-cycle'); +REPLACE INTO "technology" VALUES('E_SOLPV','p','electric','','',0,0,0,0,0,0,0,' solar photovoltaic'); +REPLACE INTO "technology" VALUES('E_BATT','ps','electric','','',0,0,0,0,0,0,0,' lithium-ion battery'); +REPLACE INTO "technology" VALUES('E_NUCLEAR','pb','electric','','',0,0,0,0,0,0,0,' nuclear power plant'); +REPLACE INTO "technology" VALUES('T_BLND','p','transport','','',0,0,0,0,0,0,0,'ethanol - gasoline blending process'); +REPLACE INTO "technology" VALUES('T_DSL','p','transport','','',0,0,0,0,0,0,0,'diesel vehicle'); +REPLACE INTO "technology" VALUES('T_GSL','p','transport','','',0,0,0,0,0,0,0,'gasoline vehicle'); +REPLACE INTO "technology" VALUES('T_EV','p','transport','','',0,0,0,0,0,0,0,'electric vehicle'); +REPLACE INTO "technology" VALUES('R_EH','p','residential','','',0,0,0,0,0,0,0,' electric residential heating'); +REPLACE INTO "technology" VALUES('R_NGH','p','residential','','',0,0,0,0,0,0,0,' natural gas residential heating'); +REPLACE INTO "technology" VALUES('E_TRANS','p','electric','','',0,0,0,0,0,1,0,'electric transmission'); 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'); diff --git a/tests/testing_data/test_system_sets.json b/tests/testing_data/test_system_sets.json index 40e5b7ab..4b6d3bba 100644 --- a/tests/testing_data/test_system_sets.json +++ b/tests/testing_data/test_system_sets.json @@ -56,8 +56,10 @@ "linked_emissions_tech_constraint_rpsdtve": "(empty)", "loan_lifetime_process_rtv": "5033502364848a3a3f295f1b3d051531ecd5c1e5f8bbaecd61afcd18181225ab", "new_capacity_var_rtv": "bc0ec9e7f812410cb2af924cbc99a31c6e99cd95fc2de26193daad38f33cc132", + "operating_reserve_rpsdt": "(empty)", "operator": "74d830836f1399fb336a0432dde7d7bd36cffa3ff76b1c42d7945350cfb9bf91", "ordered_season_sequential": "(empty)", + "planning_reserve_rpsdt": "(empty)", "process_life_frac_rptv": "d1b75ede9f90899b1c1cbc56489bf9d12c52abc6c0466eb5fe4dccaf6f3be800", "ramp_down_constraint_rpsdtv": "(empty)", "ramp_up_constraint_rpsdtv": "(empty)", @@ -65,9 +67,6 @@ "regional_global_indices": "92fa6c5d5745d765d6e16ad1bca7e1fc72f4377273be7cfbfde626ca1967d81b", "regional_indices": "f74187f92c4fdb3c12d5610304c7ac9696001433150bdaa9ff20793fb6365b32", "regions": "0ddd05d695b255ac719dfa85de1e900a3036d547ffc7261c9c9ca2c81bfda029", - "renewable_portfolio_standard_constraint_rpg": "(empty)", - "reserve_margin_method": "7869283c0d14273f720716309207a8f0c24606d03c679d6b68e656ed8d86241d", - "reserve_margin_rpsd": "(empty)", "retired_capacity_var_rptv": "(empty)", "seasonal_storage_constraints_rpsdtv": "(empty)", "seasonal_storage_level_rpstv": "(empty)", @@ -87,7 +86,6 @@ "tech_group_names": "(empty)", "tech_or_group": "85a3645929dbeaf6b7eb17e8085c8923ef86949eaa3fb4fd81724dcdcf38fd30", "tech_production": "85a3645929dbeaf6b7eb17e8085c8923ef86949eaa3fb4fd81724dcdcf38fd30", - "tech_reserve": "(empty)", "tech_retirement": "(empty)", "tech_seasonal_storage": "(empty)", "tech_storage": "7109b89425e6707adc8a5e571bf70fc64475d2d76471e5afe93110fd86bbcec8", diff --git a/tests/testing_data/test_week.sql b/tests/testing_data/test_week.sql index a0384dc2..ad80b36c 100644 --- a/tests/testing_data/test_week.sql +++ b/tests/testing_data/test_week.sql @@ -90,11 +90,12 @@ REPLACE INTO time_of_day VALUES(24,'h24',1.0,NULL); REPLACE INTO time_period VALUES(0,0,'f'); REPLACE INTO time_period VALUES(1,1,'f'); REPLACE INTO time_period_type VALUES('e','existing vintages'); -REPLACE INTO time_period_type VALUES('f','future');REPLACE INTO technology VALUES('uc_cheap','p','energy',NULL,NULL,0,0,0,0,0,0,0,0,NULL); -REPLACE INTO technology VALUES('uc_expensive','p','energy',NULL,NULL,0,0,0,0,0,0,0,0,NULL); -REPLACE INTO technology VALUES('demand_sink','p','energy',NULL,NULL,1,0,0,0,0,0,0,0,NULL); -REPLACE INTO technology VALUES('daily_storage','ps','energy',NULL,NULL,0,0,0,0,0,0,0,0,NULL); -REPLACE INTO technology VALUES('seasonal_storage','ps','energy',NULL,NULL,0,0,0,0,0,0,0,1,NULL); +REPLACE INTO time_period_type VALUES('f','future'); +REPLACE INTO technology VALUES('uc_cheap','p','energy',NULL,NULL,0,0,0,0,0,0,0,NULL); +REPLACE INTO technology VALUES('uc_expensive','p','energy',NULL,NULL,0,0,0,0,0,0,0,NULL); +REPLACE INTO technology VALUES('demand_sink','p','energy',NULL,NULL,1,0,0,0,0,0,0,NULL); +REPLACE INTO technology VALUES('daily_storage','ps','energy',NULL,NULL,0,0,0,0,0,0,0,NULL); +REPLACE INTO technology VALUES('seasonal_storage','ps','energy',NULL,NULL,0,0,0,0,0,0,1,NULL); REPLACE INTO time_season VALUES(1,'d01',0.1428571428571428493,'1/7'); REPLACE INTO time_season VALUES(2,'d02',0.1428571428571428493,'1/7'); REPLACE INTO time_season VALUES(3,'d03',0.1428571428571428493,'1/7'); diff --git a/tests/testing_data/utopia_data.sql b/tests/testing_data/utopia_data.sql index d0f131cb..0c552165 100644 --- a/tests/testing_data/utopia_data.sql +++ b/tests/testing_data/utopia_data.sql @@ -427,25 +427,25 @@ 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 "technology" VALUES('IMPDSL1','p','supply','petroleum','',1,0,0,0,0,0,0,' imported diesel'); +REPLACE INTO "technology" VALUES('IMPGSL1','p','supply','petroleum','',1,0,0,0,0,0,0,' imported gasoline'); +REPLACE INTO "technology" VALUES('IMPHCO1','p','supply','coal','',1,0,0,0,0,0,0,' imported coal'); +REPLACE INTO "technology" VALUES('IMPOIL1','p','supply','petroleum','',1,0,0,0,0,0,0,' imported crude oil'); +REPLACE INTO "technology" VALUES('IMPURN1','p','supply','nuclear','',1,0,0,0,0,0,0,' imported uranium'); +REPLACE INTO "technology" VALUES('IMPFEQ','p','supply','petroleum','',1,0,0,0,0,0,0,' imported fossil equivalent'); +REPLACE INTO "technology" VALUES('IMPHYD','p','supply','hydro','',1,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,' coal power plant'); +REPLACE INTO "technology" VALUES('E21','pb','electric','nuclear','',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,' hydro power'); +REPLACE INTO "technology" VALUES('E51','ps','electric','electric','',0,0,0,0,0,0,0,' electric storage'); +REPLACE INTO "technology" VALUES('E70','p','electric','petroleum','',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,' electric residential heating'); +REPLACE INTO "technology" VALUES('RHO','p','residential','petroleum','',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,' residential lighting'); +REPLACE INTO "technology" VALUES('SRE','p','supply','petroleum','',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,' diesel powered vehicles'); +REPLACE INTO "technology" VALUES('TXE','p','transport','electric','',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,' gasoline powered vehicles'); 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'); diff --git a/tests/testing_data/utopia_sets.json b/tests/testing_data/utopia_sets.json index dded6303..99a816e2 100644 --- a/tests/testing_data/utopia_sets.json +++ b/tests/testing_data/utopia_sets.json @@ -56,8 +56,10 @@ "linked_emissions_tech_constraint_rpsdtve": "(empty)", "loan_lifetime_process_rtv": "2cfc288b15f25957dfc70f6396d97ad655ecbed91c5a11582329749f1fb3dbd7", "new_capacity_var_rtv": "d4f1cc8b432075001befddab648d54d1f82a29099236948845393a193b6add5b", + "operating_reserve_rpsdt": "(empty)", "operator": "74d830836f1399fb336a0432dde7d7bd36cffa3ff76b1c42d7945350cfb9bf91", "ordered_season_sequential": "(empty)", + "planning_reserve_rpsdt": "(empty)", "process_life_frac_rptv": "63d17957ee2bebf664ffa6a39cea5e16ec65ad07db1df24dcea6b15bb9ebf589", "ramp_down_constraint_rpsdtv": "(empty)", "ramp_up_constraint_rpsdtv": "(empty)", @@ -65,9 +67,6 @@ "regional_global_indices": "ce00905893c23bc59c15dcf61d7e261c2e97ab1fc298ed0e0ccb1344f1cace37", "regional_indices": "ce00905893c23bc59c15dcf61d7e261c2e97ab1fc298ed0e0ccb1344f1cace37", "regions": "ce00905893c23bc59c15dcf61d7e261c2e97ab1fc298ed0e0ccb1344f1cace37", - "renewable_portfolio_standard_constraint_rpg": "(empty)", - "reserve_margin_method": "7869283c0d14273f720716309207a8f0c24606d03c679d6b68e656ed8d86241d", - "reserve_margin_rpsd": "(empty)", "retired_capacity_var_rptv": "(empty)", "seasonal_storage_constraints_rpsdtv": "(empty)", "seasonal_storage_level_rpstv": "(empty)", @@ -87,7 +86,6 @@ "tech_group_names": "(empty)", "tech_or_group": "5c321f60e5d16e60c5063b83d59ac9e184ab78e7fc469e41b21fd8aea83f600c", "tech_production": "5c321f60e5d16e60c5063b83d59ac9e184ab78e7fc469e41b21fd8aea83f600c", - "tech_reserve": "(empty)", "tech_retirement": "(empty)", "tech_seasonal_storage": "(empty)", "tech_storage": "245737c06f3e838e63da08a47a7d2a8605340ea5cad86397e1bb66461a574358", From f5e56e1443856c58ba82d9ecb4cf0049eb4246ed Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Fri, 7 Aug 2026 14:58:10 -0400 Subject: [PATCH 09/24] Update docs for reserve margin generalisation Signed-off-by: Davey Elder --- docs/source/database.rst | 48 ++++++++------ docs/source/mathematical_formulation.rst | 79 ++++++++++++++---------- docs/source/param_desc_and_tables.rst | 8 +-- docs/source/set_desc_and_tables.rst | 5 +- 4 files changed, 82 insertions(+), 58 deletions(-) diff --git a/docs/source/database.rst b/docs/source/database.rst index 3f6e7c31..2b2340a9 100644 --- a/docs/source/database.rst +++ b/docs/source/database.rst @@ -110,18 +110,26 @@ recommend that you populate input tables in the following order: Group Region and Technology Constraints --------------------------------------- -Some constraint tables support summation over groups of regions or technologies. Note that each row in these tables will still only create one constraint, but that constraint will be a summation over the defined group. For example, the ``limit_capacity`` table will limit the total summed capacity of all technologies in the technology group (if used) and over all the regions in the region group (if used). Consider behaviour carefully. For example, the ``limit_annual_capacity_factor`` table will constrain the total summed capacity factor of the group, which would allow for varying capacity factors of processes within that group as long as the limit is met in aggregate. +Some constraint tables support summation over groups of regions or technologies. +Note that each row in these tables will still only create one constraint, but +that constraint will be a summation over the defined group. For example, the +``limit_capacity`` table will limit the total summed capacity of all technologies +in the technology group (if used) and over all the regions in the region group (if used). +Consider behaviour carefully. For example, the ``limit_annual_capacity_factor`` table will +constrain the total summed capacity factor of the group, which would allow for varying +capacity factors of processes within that group as long as the limit is met in aggregate. **Group Regions:** For the supported tables, the ``region`` column can be populated with either a single region (e.g., ``"east"``), a subset of regions delineated with a ``+`` (e.g., ``"east+west"``), or ``"global"`` to indicate summation over all model regions. .. important:: - When grouped or global region constraints are used with exchange technologies, - exchange flows are only counted if the relevant exchange-technology keys are + When ``+`` delineated region summations are used with exchange technologies, + exchange flows are only counted if the relevant exchange-technology region pairs are explicitly included in the region string used by the constraint row. - Exchange-technology keys are directional and use a hyphen to separate the - two regions (e.g., ``east-west`` and ``west-east`` are distinct keys). + Exchange-technology regions are directional and use a hyphen to separate the + two regions (e.g., ``east-west`` and ``west-east`` are distinct keys). ``"global"`` + *does* include all exchange flows by default. For example, to constrain activity across the ``east`` and ``west`` regions including all exchange flows between them, the region string should be: @@ -132,7 +140,12 @@ For the supported tables, the ``region`` column can be populated with either a s If exchange keys are omitted (e.g., using only ``east+west``), flows through exchange technologies between those regions will *not* be included in the - constrained summation. + constraint summation. + + The only exception to this rule is reserve margin constraints, + :code:`planning_reserve_margin` and :code:`operating_reserve_margin`, + which automatically include all exchange flows into and out of the region + or region group due to their specific logic requiring this behaviour. Supported tables: @@ -146,18 +159,17 @@ Supported tables: * limit_capacity_share * limit_new_capacity_share * limit_emission -* limit_growth_capacity -* limit_growth_new_capacity -* limit_growth_new_capacity_delta -* limit_degrowth_capacity -* limit_degrowth_new_capacity -* limit_degrowth_new_capacity_delta +* planning_reserve_margin +* operating_reserve_margin **Technology Groups:** -For the supported tables, the following columns accept either technologies or technology groups, over which the constraint is summed. Technology groups are defined in the ``tech_group`` and ``tech_group_member`` tables. +For the supported tables, the following columns accept either technologies or +technology groups, over which the constraint is summed. Technology groups are +defined in the ``tech_group`` and ``tech_group_member`` tables. + +Technology group columns: -* tech * tech_or_group * sub_group * super_group @@ -173,12 +185,8 @@ Supported tables: * limit_activity_share * limit_capacity_share * limit_new_capacity_share -* limit_growth_capacity -* limit_growth_new_capacity -* limit_growth_new_capacity_delta -* limit_degrowth_capacity -* limit_degrowth_new_capacity -* limit_degrowth_new_capacity_delta +* planning_reserve_margin +* operating_reserve_margin For help getting started, consider using the ``temoa tutorial`` diff --git a/docs/source/mathematical_formulation.rst b/docs/source/mathematical_formulation.rst index 8bbd7a21..3e5fd928 100644 --- a/docs/source/mathematical_formulation.rst +++ b/docs/source/mathematical_formulation.rst @@ -338,19 +338,8 @@ efficiency values. If not specified for a given process, it defaults to 1, meaning the base :code:`efficiency` value applies uniformly. Note that there is no period index: the time-varying efficiency applies to all periods. - .. _capacity_factor_tech: -capacity_credit -~~~~~~~~~~~~~~~ - -:math:`{CC}_{r \in R, p \in P, t \in T, v \in V}` - -The capacity credit represents the fraction of total installed capacity of -a process that can be relied upon during the time slice in which peak -electricity demand occurs. This parameter is used in the 'static' version of -the :math:`reserve_margin` constraint. - capacity_factor_tech ~~~~~~~~~~~~~~~~~~~~ @@ -822,13 +811,51 @@ level to vary. planning_reserve_margin ~~~~~~~~~~~~~~~~~~~~~~~ -:math:`{PRM}_{r \in R}` +:math:`{PRM}_{r_g \in R, t_g \in T}` + +The required excess of credited installed capacity above demand, expressed +as a fraction of demand, keyed by a region-or-group :math:`r_g` and a +technology-or-group :math:`t_g`. For example, a value of 0.2 requires that +credited capacity be at least 120% of demand. Demand is estimated from production +by time slice. When a region group is used, any exchange region (e.g. ``r1-r2``) +where exactly one endpoint belongs to the group is automatically included in the +reserve calculation; this auto-inclusion is unique to the reserve margin constraints. + + +planning_reserve_credit +~~~~~~~~~~~~~~~~~~~~~~~ + +:math:`{PRC}_{r \in R, t \in T}` + +The fraction of a technology's installed capacity that can be reliably counted +toward the reserve margin. A firm, fully dispatchable process (e.g. a gas turbine) +typically receives a credit near 1, while a weather-dependent process (e.g. wind +or solar) receives a lower value. + + +operating_reserve_margin +~~~~~~~~~~~~~~~~~~~~~~~~ + +:math:`{ORM}_{r_g \in R, t_g \in T}` + +The dynamic counterpart to :code:`planning_reserve_margin`, indexed the same +way by region-or-group and technology-or-group. Rather than crediting +installed capacity, it requires that available (derated) generation in each +time slice exceed the region-group's proxy demand by this margin. When a region +group is used, any exchange region (e.g. ``r1-r2``) +where exactly one endpoint belongs to the group is automatically included in the +reserve calculation; this auto-inclusion is unique to the reserve margin constraints. + + +operating_reserve_derate +~~~~~~~~~~~~~~~~~~~~~~~~ + +:math:`{ORD}_{r \in R, s \in S, t \in T}` -The :code:`planning_reserve_margin` parameter specifies the capacity reserve margin -in the electric sector by region. The capacity reserve margin represents the -installed generating capacity — expressed as a share of peak load — that must be -available in reserve to meet contingencies. Temoa estimates peak demand from electricity -production by time slice. +The fraction of a technology's available output that can be depended upon in a +given season. A value less than 1 reflects the fact that not all of a process's +capacity is reliably available — for example, due to scheduled maintenance or +seasonal resource constraints. Defaults to 1. ramp_down_hourly @@ -851,18 +878,6 @@ by which a technology can ramp output up per hour. This is used in the :code:`ramp_up_constraint`. -reserve_capacity_derate -~~~~~~~~~~~~~~~~~~~~~~~ - -:math:`{RCD}_{r \in R, s \in S, t \in T^{res}, v \in V}` - -The :code:`reserve_capacity_derate` parameter allows the modeler to derate -the capacity of a reserve technology in specific seasons — for example, to -account for seasonal availability. Values default to 1 (no derate). This -parameter is used in the 'dynamic' version of the :code:`reserve_margin` -constraint. - - .. _segment_fraction: segment_fraction @@ -1313,9 +1328,11 @@ various physical and operational real-world phenomena. .. autofunction:: temoa.components.operations.ramp_down_constraint -.. autofunction:: temoa.components.reserves.reserve_margin_static +.. autofunction:: temoa.components.reserves.planning_reserve_margin_constraint + +.. autofunction:: temoa.components.reserves.operating_reserve_margin_constraint -.. autofunction:: temoa.components.reserves.reserve_margin_dynamic +.. autofunction:: temoa.components.reserves.reserve_margin_proxy_demand .. autofunction:: temoa.components.emissions.linked_emissions_tech_constraint diff --git a/docs/source/param_desc_and_tables.rst b/docs/source/param_desc_and_tables.rst index aef95320..57cdea17 100644 --- a/docs/source/param_desc_and_tables.rst +++ b/docs/source/param_desc_and_tables.rst @@ -24,12 +24,13 @@ characteristics**. :widths: 15, 20, 25, 40 ":math:`\text{C2A}_{r,t}`", ":code:`capacity_to_activity`", ":code:`capacity_to_activity`", "converts from capacity to activity units" - ":math:`\text{CC}_{r,p,t,v}`", ":code:`capacity_credit`", ":code:`capacity_credit`", "process-specific capacity credit used in the static reserve margin constraint" + ":math:`\text{PRC}_{r,t}`", ":code:`planning_reserve_credit`", ":code:`planning_reserve_credit`", "fraction of installed capacity that can be relied upon (default 0)" ":math:`\text{CFT}_{r,s,d,t}`", ":code:`capacity_factor_tech`", ":code:`capacity_factor_tech`", "technology-specific capacity factor" ":math:`\text{CFP}_{r,s,d,t,v}`", ":code:`capacity_factor_process`", ":code:`capacity_factor_process`", "process-specific capacity factor; allows capacity factor to change with technology vintage" ":math:`\text{ECAP}_{r,t,v}`", ":code:`existing_capacity`", ":code:`existing_capacity`", "installed capacity that exists prior to first model time period" - ":math:`\text{PRM}_{r}`", ":code:`planning_reserve_margin`", ":code:`planning_reserve_margin`", "planning reserve margin used to ensure sufficient generating capacity" - ":math:`\text{RCD}_{r,s,t,v}`", ":code:`reserve_capacity_derate`", ":code:`reserve_capacity_derate`", "capacity derate factor for dynamic reserve margin constraint" + ":math:`\text{PRM}_{r_g,t_g}`", ":code:`planning_reserve_margin`", ":code:`planning_reserve_margin`", "required excess of credited capacity above demand in each time slice, as a fraction of demand" + ":math:`\text{ORM}_{r_g,t_g}`", ":code:`operating_reserve_margin`", ":code:`operating_reserve_margin`", "required excess of available (derated) output above demand in each time slice, as a fraction of that demand" + ":math:`\text{ORD}_{r,s,t}`", ":code:`operating_reserve_derate`", ":code:`operating_reserve_derate`", "fraction of available output that can be relied upon in a given season (default 1)" ":math:`\text{RUH}_{r,t}`", ":code:`ramp_up_hourly`", ":code:`ramp_up_hourly`", "hourly rate at which generation techs can ramp output up" ":math:`\text{RDH}_{r,t}`", ":code:`ramp_down_hourly`", ":code:`ramp_down_hourly`", "hourly rate at which generation techs can ramp output down" @@ -125,7 +126,6 @@ Parameters in the table below relate to the specification of **policy**. :header: "Parameter", "Database Table", "Model Element", "Notes" :widths: 15, 20, 25, 40 - "", ":code:`rps_requirement`", ":code:`renewable_portfolio_standard`", "**[Deprecated]** RPS requirements; use :code:`limit_activity_share` instead" ":math:`\text{LIT}_{r,t,e,t'}`", ":code:`linked_tech`", ":code:`linked_techs`", "dummy techs used to convert CO2 emissions to physical commodity" Parameters in the table below relate to the specification of **construction and diff --git a/docs/source/set_desc_and_tables.rst b/docs/source/set_desc_and_tables.rst index 29818d48..150eb259 100644 --- a/docs/source/set_desc_and_tables.rst +++ b/docs/source/set_desc_and_tables.rst @@ -51,14 +51,13 @@ number of **technology subsets**. ":math:`\text{T}^b`", ":code:`technology, flag = pb`", ":code:`tech_baseload`", "baseload electric generators, which have constant output across intraday time segments (:math:`{T}^b \subset T`)" ":math:`\text{T}^s`", ":code:`technology, flag = ps`", ":code:`tech_storage`", "all storage technologies (:math:`{T}^s \subset T`)" ":math:`\text{T}^a`", ":code:`technology, annual = 1`", ":code:`tech_annual`", "technologies that produce constant annual output (:math:`{T}^a \subset T`)" - ":math:`\text{T}^{res}`", ":code:`technology, reserve = 1`", ":code:`tech_reserve`", "electric generators contributing to the reserve margin requirement (:math:`{T}^{res} \subset T`)" - ":math:`\text{T}^c`", ":code:`technology, curtail = 1`", ":code:`tech_curtailment`", "technologies with curtailable output and no upstream cost (:math:`{T}^c \subset (T - T^{res})`)" + ":math:`\text{T}^c`", ":code:`technology, curtail = 1`", ":code:`tech_curtailment`", "technologies with curtailable output and no upstream cost (:math:`{T}^c \subset T`)" ":math:`\text{T}^f`", ":code:`technology, flex = 1`", ":code:`tech_flex`", "technologies producing excess commodity flows (:math:`{T}^f \subset T`)" ":math:`\text{T}^x`", ":code:`technology, exchange = 1`", ":code:`tech_exchange`", "technologies used for interregional commodity flows (:math:`{T}^x \subset T`)" ":math:`\text{T}^{ur}`", ":code:`ramp_up_hourly`", ":code:`tech_upramping`", "electric generators with a ramp up hourly rate limit; derived from :code:`ramp_up_hourly` table (:math:`{T}^{ur} \subset T`)" ":math:`\text{T}^{dr}`", ":code:`ramp_down_hourly`", ":code:`tech_downramping`", "electric generators with a ramp down hourly rate limit; derived from :code:`ramp_down_hourly` table (:math:`{T}^{dr} \subset T`)" ":math:`\text{T}^{ret}`", ":code:`technology, retire = 1`", ":code:`tech_retirement`", "technologies allowed to retire before end of life (:math:`{T}^{ret} \subset (T - T^{u})`)" - ":math:`\text{T}^u`", ":code:`technology, unlim_cap = 1`", ":code:`tech_uncap`", "technologies that have no bound on capacity (:math:`{T}^u \subset (T - T^{res})`)" + ":math:`\text{T}^u`", ":code:`technology, unlim_cap = 1`", ":code:`tech_uncap`", "technologies that have no bound on capacity (:math:`{T}^u \subset T`)" ":math:`\text{T}^{ss}`", ":code:`technology, flag = 'ps' AND seas_stor = 1`", ":code:`tech_seasonal_storage`", "seasonal storage technologies; requires both storage flag and seas_stor column (:math:`{T}^{ss} \subset T^s`)" "", ":code:`tech_group`", ":code:`tech_group_names`", "named groups for use in group constraints" "", ":code:`tech_group_member`", ":code:`tech_group_members`", "each technology belonging to each group" From b49bdb1857c2f9dfb8307bacfe03df98be515b54 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Fri, 7 Aug 2026 21:48:12 -0400 Subject: [PATCH 10/24] Remember that annual exchange techs exist Signed-off-by: Davey Elder --- temoa/components/reserves.py | 107 ++++++++++++++++++++++------------- 1 file changed, 68 insertions(+), 39 deletions(-) diff --git a/temoa/components/reserves.py b/temoa/components/reserves.py index febd1366..5e0f51d2 100644 --- a/temoa/components/reserves.py +++ b/temoa/components/reserves.py @@ -159,6 +159,30 @@ def initialize_reserve_margins(model: TemoaModel) -> None: # ============================================================================ +def _into_region(model: TemoaModel, r: Region, t: Technology, regions: set[Region]) -> int: + """ + Returns +1 if not an exchange tech or is an exchange tech importing into the region group. + """ + if t not in model.tech_exchange: + return 1 + r1, r2 = r.split('-') + if r2 in regions and r1 not in regions: + return 1 + return 0 + + +def _out_of_region(model: TemoaModel, r: Region, t: Technology, regions: set[Region]) -> int: + """ + Returns +1 if and only if is an exchange tech exporting out of the region group. + """ + if t not in model.tech_exchange: + return 0 + r1, r2 = r.split('-') + if r1 in regions and r2 not in regions: + return 1 + return 0 + + def reserve_margin_proxy_demand( model: TemoaModel, processes: set[tuple[Region, Technology, Vintage]], @@ -193,36 +217,40 @@ def reserve_margin_proxy_demand( \begin{aligned} D^{proxy}_{r_g,p,s,d} =& - \sum_{(r,t,v) \in \Theta^{res} \setminus T^a \setminus T^x,\, I, O} + \sum_{\substack{(r,t,v) \in \Theta^{res} \setminus T^a \\ \uparrow_{r,r_g},\, I, O}} \mathbf{FO}_{r, p, s, d, i, t, v, o} - && \text{(production, non-annual, includes storage)} \\ - &+ \sum_{(r,t,v) \in \Theta^{res} \cap T^a,\, I, O} + && \text{(non-annual production and imports)} \\ + &+ \sum_{\substack{(r,t,v) \in \Theta^{res} \cap T^a \\ \uparrow_{r,r_g},\, I, O}} \begin{cases} DSD_{r,s,d,o} & o \in C^d \\ SEG_{s,d} & \text{otherwise} \end{cases} \cdot \mathbf{FOA}_{r, p, i, t, v, o} - && \text{(production, annual)} \\ + && \text{(annual production and imports)} \\ &- \sum_{(r,t,v) \in \Theta^{res} \cap T^s,\, I, O} \mathbf{FI}_{r, p, s, d, i, t, v, o} && \text{(storage inputs)} \\ - &+ \sum_{\substack{(r,t,v) \in \Theta^{res} \cap T^x \\ r_2 \in r_g,\, I, O}} - \mathbf{FO}_{r, p, s, d, i, t, v, o} - && \text{(imports)} \\ - &- \sum_{\substack{(r,t,v) \in \Theta^{res} \cap T^x \\ r_1 \in r_g,\, I, O}} + &- \sum_{\substack{(r,t,v) \in \Theta^{res} \cap T^x \setminus T^a \\ + \downarrow_{r,r_g},\, I, O}} \mathbf{FO}_{r, p, s, d, i, t, v, o} / EFF_{r,p,s,d,i,t,v,o} - && \text{(exports)} + && \text{(non-annual exports)} \\ + &- \sum_{\substack{(r,t,v) \in \Theta^{res} \cap T^x \cap T^a \\ + \downarrow_{r,r_g},\, I, O}} + \begin{cases} DSD_{r,s,d,o} & o \in C^d \\ SEG_{s,d} & \text{otherwise} + \end{cases} \cdot \mathbf{FOA}_{r, p, i, t, v, o} / EFF_{r,p,i,t,v,o} + && \text{(annual exports)} \end{aligned} - where :math:`\Theta^{res} = \Theta^{res}_{r_g,p,t_g}` is the set of all :math:`(r,t,v)` - processes contributing to this reserve margin in this period, and for an exchange process - :math:`r = r_1{-}r_2`, imports (:math:`r_2 \in r_g,\ r_1 \notin r_g`) add delivered energy - while exports (:math:`r_1 \in r_g,\ r_2 \notin r_g`) subtract the energy drawn from - :math:`r_1`. + where :math:`\uparrow_{r,r_g}` selects non-exchange processes and exchange imports + (:math:`r_2 \in r_g,\ r_1 \notin r_g`), and :math:`\downarrow_{r,r_g}` selects exchange + exports (:math:`r_1 \in r_g,\ r_2 \notin r_g`). :math:`\Theta^{res} = \Theta^{res}_{r_g,p,t_g}` + is the set of all :math:`(r,t,v)` processes contributing to this reserve margin in this period. """ + regions = geography.gather_group_regions(model, r_g) + # Non-annual activity activity = quicksum( model.v_flow_out[r, p, s, d, i, t, v, o] for (r, t, v) in processes - if t not in model.tech_annual and t not in model.tech_exchange + if t not in model.tech_annual and _into_region(model, r, t, regions) for i in model.process_inputs[r, p, t, v] for o in model.process_outputs_by_input[r, p, t, v, i] ) @@ -236,7 +264,7 @@ def reserve_margin_proxy_demand( ) * model.v_flow_out_annual[r, p, i, t, v, o] for (r, t, v) in processes - if t in model.tech_annual and t not in model.tech_exchange + if t in model.tech_annual and _into_region(model, r, t, regions) for i in model.process_inputs[r, p, t, v] for o in model.process_outputs_by_input[r, p, t, v, i] ) @@ -252,29 +280,30 @@ def reserve_margin_proxy_demand( for o in model.process_outputs_by_input[r, p, t, v, i] ) - # Exchange technologies - # Add imports into the group, subtract exports out of it - regions = geography.gather_group_regions(model, r_g) - for r1r2, t, v in processes: - if t not in model.tech_exchange: - continue - - r1, r2 = r1r2.split('-') - if r2 in regions and r1 not in regions: - # Import into the group: add the energy delivered to r2 - activity += quicksum( - model.v_flow_out[r1r2, p, s, d, i, t, v, o] - for i in model.process_inputs[r1r2, p, t, v] - for o in model.process_outputs_by_input[r1r2, p, t, v, i] - ) - elif r1 in regions and r2 not in regions: - # Export out of the group: subtract the energy drawn from r1 - activity -= quicksum( - model.v_flow_out[r1r2, p, s, d, i, t, v, o] - / get_variable_efficiency(model, r1r2, p, s, d, i, t, v, o) - for i in model.process_inputs[r1r2, p, t, v] - for o in model.process_outputs_by_input[r1r2, p, t, v, i] - ) + # Subtract exchange exports + # Non-annual exports + activity -= quicksum( + model.v_flow_out[r, p, s, d, i, t, v, o] + / get_variable_efficiency(model, r, p, s, d, i, t, v, o) + for (r, t, v) in processes + if t not in model.tech_annual and _out_of_region(model, r, t, regions) + for i in model.process_inputs[r, p, t, v] + for o in model.process_outputs_by_input[r, p, t, v, i] + ) + # Annual exports (could feed a demand) + activity -= quicksum( + ( + value(model.demand_specific_distribution[r, p, s, d, o]) + if o in model.commodity_demand + else value(model.segment_fraction[s, d]) + ) + * model.v_flow_out_annual[r, p, i, t, v, o] + / value(model.efficiency[r, i, t, v, o]) + for (r, t, v) in processes + if t in model.tech_annual and _out_of_region(model, r, t, regions) + for i in model.process_inputs[r, p, t, v] + for o in model.process_outputs_by_input[r, p, t, v, i] + ) return activity From 93eaae1644ba09b8b98e0e8f741a8cd873085f0c Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Sat, 8 Aug 2026 08:26:47 -0400 Subject: [PATCH 11/24] Add reserve margin test that builds and checks for an identical LP file Signed-off-by: Davey Elder --- temoa/_internal/temoa_sequencer.py | 2 + tests/test_reserve_margins.py | 103 + .../config_reserve_margins.toml | 11 + tests/testing_data/reserve_margins.lp | 3127 +++++++++++++++++ tests/testing_data/reserve_margins.sql | 162 + tests/utilities/compare_lp.py | 275 ++ 6 files changed, 3680 insertions(+) create mode 100644 tests/test_reserve_margins.py create mode 100644 tests/testing_configs/config_reserve_margins.toml create mode 100644 tests/testing_data/reserve_margins.lp create mode 100644 tests/testing_data/reserve_margins.sql create mode 100644 tests/utilities/compare_lp.py diff --git a/temoa/_internal/temoa_sequencer.py b/temoa/_internal/temoa_sequencer.py index bd44044e..d49d15ab 100644 --- a/temoa/_internal/temoa_sequencer.py +++ b/temoa/_internal/temoa_sequencer.py @@ -144,6 +144,8 @@ def build_model(self) -> TemoaModel: data_portal, silent=self.config.silent, extensions=self.config.extensions, + keep_lp_file=self.config.save_lp_file, + lp_path=self.config.output_path, ) logger.info('Model build process complete.') diff --git a/tests/test_reserve_margins.py b/tests/test_reserve_margins.py new file mode 100644 index 00000000..cfc3f435 --- /dev/null +++ b/tests/test_reserve_margins.py @@ -0,0 +1,103 @@ +from __future__ import annotations + +import logging +from pathlib import Path +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from temoa.core.model import TemoaModel + +import pytest + +from temoa._internal.temoa_sequencer import TemoaSequencer +from temoa.core.config import TemoaConfig +from temoa.core.modes import TemoaMode +from tests.utilities.compare_lp import LpDiff, compare_lp_files + +logger = logging.getLogger(__name__) + +TEST_CONFIG = Path(__file__).parent / 'testing_configs' / 'config_reserve_margins.toml' +CACHED_LP = Path(__file__).parent / 'testing_data' / 'reserve_margins.lp' + + +# --------------------------------------------------------------------------- +# Fixture +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope='module') +def reserve_run(tmp_path_factory: pytest.TempPathFactory) -> tuple[TemoaModel, Path]: + """Build, solve, and return (model, lp_path) for the reserve_margins scenario.""" + tmp = tmp_path_factory.mktemp('reserve_margins') + config = TemoaConfig.build_config( + config_file=TEST_CONFIG, + output_path=tmp, + silent=True, + ) + config.save_lp_file = True + + seq = TemoaSequencer(config=config, mode_override=TemoaMode.BUILD_ONLY) + instance = seq.build_model() + + lp_files = list(tmp.glob('*.lp')) + assert lp_files, 'No LP file was written to the output directory' + return instance, lp_files[0] + + +def test_planning_ab_group_includes_exchange(reserve_run: tuple[TemoaModel, Path]): + model, _ = reserve_run + exchange_regions = { + r + for r_g, p, t_g in model.planning_reserve_processes + if r_g == 'A+B' + for r, t, v in model.planning_reserve_processes[r_g, p, t_g] + if '-' in r + } + assert all(r in exchange_regions for r in ['A-C', 'C-A', 'B-C', 'C-B']), ( + 'Exchange regions A-C / C-A / B-C / C-B not auto-included in planning reserve group A+B' + ) + + +def test_single_region_a_includes_exchange(reserve_run: tuple[TemoaModel, Path]): + model, _ = reserve_run + exchange_regions = { + r + for r_g, p, t_g in model.operating_reserve_processes + if r_g == 'A' and t_g == 'elec_A' + for r, t, v in model.operating_reserve_processes[r_g, p, t_g] + if '-' in r + } + assert all(r in exchange_regions for r in ['A-C', 'C-A', 'B-A', 'A-B']), ( + 'Exchange regions A-C / C-A / B-A / A-B not auto-included in operating reserve group A' + ) + + +def test_single_tech_region_a_not_includes_exchange(reserve_run: tuple[TemoaModel, Path]): + model, _ = reserve_run + exchange_regions = { + r + for r_g, p, t_g in model.planning_reserve_processes + if r_g == 'A' and t_g == 'NGCC' + for r, t, v in model.planning_reserve_processes[r_g, p, t_g] + if '-' in r + } + assert not exchange_regions, ( + 'Single-region planning margin on A should ' + f'not include exchange regions: {exchange_regions}' + ) + + +def test_lp_matches(reserve_run: tuple[TemoaModel, Path]): + _, lp_path = reserve_run + + if not CACHED_LP.exists(): + import shutil + + shutil.copy(lp_path, CACHED_LP) + pytest.skip( + f'No cached LP found — saved current output as new cache: {CACHED_LP}. ' + 'Re-run the test to validate against it.' + ) + + diff: LpDiff = compare_lp_files(CACHED_LP, lp_path) + assert diff.is_identical, f'LP file differs from cached ({CACHED_LP.name}):\n{diff.summary()}' diff --git a/tests/testing_configs/config_reserve_margins.toml b/tests/testing_configs/config_reserve_margins.toml new file mode 100644 index 00000000..7cf68aee --- /dev/null +++ b/tests/testing_configs/config_reserve_margins.toml @@ -0,0 +1,11 @@ +scenario = "reserve_margins" +scenario_mode = "perfect_foresight" +input_database = "tests/testing_outputs/reserve_margins.sqlite" +output_database = "tests/testing_outputs/reserve_margins.sqlite" +neos = false +solver_name = "appsi_highs" +save_excel = false +save_duals = false +save_lp_file = true +time_sequencing = "seasonal_timeslices" +days_per_period = 365 diff --git a/tests/testing_data/reserve_margins.lp b/tests/testing_data/reserve_margins.lp new file mode 100644 index 00000000..1bf0565c --- /dev/null +++ b/tests/testing_data/reserve_margins.lp @@ -0,0 +1,3127 @@ +\* Source Pyomo model name=unknown *\ + +min +total_cost: ++0.0 ONE_VAR_CONSTANT + +s.t. + +c_u_capacity_constraint(C_A_2020_summer_night_E_TRANS_2015)_: ++1 v_flow_out(C_A_2020_summer_night_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(C_A_2020_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(C_2020_winter_night_HEAT_ANN_2020)_: +-0.25 v_capacity(C_2020_HEAT_ANN_2020) +<= -0.25 + +c_u_capacity_constraint(B_C_2020_winter_day_E_TRANS_2015)_: ++1 v_flow_out(B_C_2020_winter_day_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(B_C_2020_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(B_2025_winter_night_HEAT_SYS_2020)_: ++1 v_flow_out(B_2025_winter_night_ELC_HEAT_SYS_2020_HEAT) +-0.25 v_capacity(B_2025_HEAT_SYS_2020) +<= 0 + +c_u_capacity_constraint(A_C_2020_winter_day_E_TRANS_2015)_: ++1 v_flow_out(A_C_2020_winter_day_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(A_C_2020_E_TRANS_2015) +<= 0 + +c_e_capacity_constraint(A_2020_winter_night_SOLPV_2020)_: +-1 v_flow_out(A_2020_winter_night_ethos_SOLPV_2020_ELC) +-1 v_curtailment(A_2020_winter_night_ethos_SOLPV_2020_ELC) += 0 + +c_u_capacity_constraint(B_2020_winter_night_NGCC_2020)_: ++1 v_flow_out(B_2020_winter_night_NG_NGCC_2020_ELC) +-2.19 v_capacity(B_2020_NGCC_2020) +<= 0 + +c_u_capacity_constraint(A_2025_summer_day_HEAT_SYS_2025)_: ++1 v_flow_out(A_2025_summer_day_ELC_HEAT_SYS_2025_HEAT) +-0.25 v_capacity(A_2025_HEAT_SYS_2025) +<= 0 + +c_u_capacity_constraint(B_A_2020_summer_day_E_TRANS_2015)_: ++1 v_flow_out(B_A_2020_summer_day_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(B_A_2020_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(C_B_2025_summer_night_E_TRANS_2015)_: ++1 v_flow_out(C_B_2025_summer_night_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(C_B_2025_E_TRANS_2015) +<= 0 + +c_e_capacity_constraint(A_2020_summer_day_SOLPV_2020)_: +-1 v_flow_out(A_2020_summer_day_ethos_SOLPV_2020_ELC) ++1.3139999999999998 v_capacity(A_2020_SOLPV_2020) +-1 v_curtailment(A_2020_summer_day_ethos_SOLPV_2020_ELC) += 0 + +c_u_capacity_constraint(A_2025_summer_night_NGCC_2025)_: ++1 v_flow_out(A_2025_summer_night_NG_NGCC_2025_ELC) +-2.19 v_capacity(A_2025_NGCC_2025) +<= 0 + +c_e_capacity_constraint(A_2025_summer_day_SOLPV_2015)_: +-1 v_flow_out(A_2025_summer_day_ethos_SOLPV_2015_ELC) ++1.3139999999999998 v_capacity(A_2025_SOLPV_2015) +-1 v_curtailment(A_2025_summer_day_ethos_SOLPV_2015_ELC) += 0 + +c_e_capacity_constraint(A_2025_winter_night_SOLPV_2015)_: +-1 v_flow_out(A_2025_winter_night_ethos_SOLPV_2015_ELC) +-1 v_curtailment(A_2025_winter_night_ethos_SOLPV_2015_ELC) += 0 + +c_e_capacity_constraint(A_2025_summer_night_SOLPV_2015)_: +-1 v_flow_out(A_2025_summer_night_ethos_SOLPV_2015_ELC) +-1 v_curtailment(A_2025_summer_night_ethos_SOLPV_2015_ELC) += 0 + +c_u_capacity_constraint(A_2025_summer_day_NGCC_2015)_: ++1 v_flow_out(A_2025_summer_day_NG_NGCC_2015_ELC) +-2.19 v_capacity(A_2025_NGCC_2015) +<= 0 + +c_u_capacity_constraint(B_2025_winter_night_NGCC_2020)_: ++1 v_flow_out(B_2025_winter_night_NG_NGCC_2020_ELC) +-2.19 v_capacity(B_2025_NGCC_2020) +<= 0 + +c_u_capacity_constraint(A_B_2025_summer_day_E_TRANS_2015)_: ++1 v_flow_out(A_B_2025_summer_day_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(A_B_2025_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(A_2025_winter_night_NGCC_2015)_: ++1 v_flow_out(A_2025_winter_night_NG_NGCC_2015_ELC) +-2.19 v_capacity(A_2025_NGCC_2015) +<= 0 + +c_u_capacity_constraint(C_2020_summer_night_HEAT_ANN_2020)_: +-0.25 v_capacity(C_2020_HEAT_ANN_2020) +<= -0.25 + +c_u_capacity_constraint(C_2025_winter_day_HEAT_ANN_2020)_: +-0.25 v_capacity(C_2025_HEAT_ANN_2020) +<= -0.275 + +c_u_capacity_constraint(C_A_2025_winter_night_E_TRANS_2015)_: ++1 v_flow_out(C_A_2025_winter_night_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(C_A_2025_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(B_2025_summer_day_NGCC_2025)_: ++1 v_flow_out(B_2025_summer_day_NG_NGCC_2025_ELC) +-2.19 v_capacity(B_2025_NGCC_2025) +<= 0 + +c_u_capacity_constraint(A_2025_winter_day_NGCC_2025)_: ++1 v_flow_out(A_2025_winter_day_NG_NGCC_2025_ELC) +-2.19 v_capacity(A_2025_NGCC_2025) +<= 0 + +c_u_capacity_constraint(B_A_2020_winter_day_E_TRANS_2015)_: ++1 v_flow_out(B_A_2020_winter_day_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(B_A_2020_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(C_B_2025_summer_day_E_TRANS_2015)_: ++1 v_flow_out(C_B_2025_summer_day_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(C_B_2025_E_TRANS_2015) +<= 0 + +c_e_capacity_constraint(A_2025_winter_day_SOLPV_2015)_: +-1 v_flow_out(A_2025_winter_day_ethos_SOLPV_2015_ELC) ++0.6569999999999999 v_capacity(A_2025_SOLPV_2015) +-1 v_curtailment(A_2025_winter_day_ethos_SOLPV_2015_ELC) += 0 + +c_u_capacity_constraint(B_C_2020_summer_day_E_TRANS_2015)_: ++1 v_flow_out(B_C_2020_summer_day_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(B_C_2020_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(B_2020_winter_night_NGCC_2015)_: ++1 v_flow_out(B_2020_winter_night_NG_NGCC_2015_ELC) +-2.19 v_capacity(B_2020_NGCC_2015) +<= 0 + +c_u_capacity_constraint(A_2025_summer_day_HEAT_SYS_2020)_: ++1 v_flow_out(A_2025_summer_day_ELC_HEAT_SYS_2020_HEAT) +-0.25 v_capacity(A_2025_HEAT_SYS_2020) +<= 0 + +c_e_capacity_constraint(A_2020_summer_day_SOLPV_2015)_: +-1 v_flow_out(A_2020_summer_day_ethos_SOLPV_2015_ELC) ++1.3139999999999998 v_capacity(A_2020_SOLPV_2015) +-1 v_curtailment(A_2020_summer_day_ethos_SOLPV_2015_ELC) += 0 + +c_u_capacity_constraint(A_B_2025_winter_day_E_TRANS_2015)_: ++1 v_flow_out(A_B_2025_winter_day_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(A_B_2025_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(A_2025_summer_night_NGCC_2020)_: ++1 v_flow_out(A_2025_summer_night_NG_NGCC_2020_ELC) +-2.19 v_capacity(A_2025_NGCC_2020) +<= 0 + +c_u_capacity_constraint(C_2020_winter_day_HEAT_ANN_2020)_: +-0.25 v_capacity(C_2020_HEAT_ANN_2020) +<= -0.25 + +c_u_capacity_constraint(C_2020_summer_day_HEAT_ANN_2020)_: +-0.25 v_capacity(C_2020_HEAT_ANN_2020) +<= -0.25 + +c_u_capacity_constraint(A_B_2020_summer_night_E_TRANS_2015)_: ++1 v_flow_out(A_B_2020_summer_night_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(A_B_2020_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(B_2025_winter_night_NGCC_2015)_: ++1 v_flow_out(B_2025_winter_night_NG_NGCC_2015_ELC) +-2.19 v_capacity(B_2025_NGCC_2015) +<= 0 + +c_u_capacity_constraint(A_2025_winter_night_HEAT_SYS_2025)_: ++1 v_flow_out(A_2025_winter_night_ELC_HEAT_SYS_2025_HEAT) +-0.25 v_capacity(A_2025_HEAT_SYS_2025) +<= 0 + +c_u_capacity_constraint(C_B_2020_summer_day_E_TRANS_2015)_: ++1 v_flow_out(C_B_2020_summer_day_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(C_B_2020_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(B_2025_summer_day_NGCC_2020)_: ++1 v_flow_out(B_2025_summer_day_NG_NGCC_2020_ELC) +-2.19 v_capacity(B_2025_NGCC_2020) +<= 0 + +c_e_capacity_constraint(A_2020_summer_night_SOLPV_2020)_: +-1 v_flow_out(A_2020_summer_night_ethos_SOLPV_2020_ELC) +-1 v_curtailment(A_2020_summer_night_ethos_SOLPV_2020_ELC) += 0 + +c_u_capacity_constraint(A_2025_winter_day_NGCC_2020)_: ++1 v_flow_out(A_2025_winter_day_NG_NGCC_2020_ELC) +-2.19 v_capacity(A_2025_NGCC_2020) +<= 0 + +c_u_capacity_constraint(A_B_2025_winter_night_E_TRANS_2015)_: ++1 v_flow_out(A_B_2025_winter_night_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(A_B_2025_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(B_2020_winter_day_NGCC_2020)_: ++1 v_flow_out(B_2020_winter_day_NG_NGCC_2020_ELC) +-2.19 v_capacity(B_2020_NGCC_2020) +<= 0 + +c_u_capacity_constraint(A_2025_winter_day_HEAT_SYS_2025)_: ++1 v_flow_out(A_2025_winter_day_ELC_HEAT_SYS_2025_HEAT) +-0.25 v_capacity(A_2025_HEAT_SYS_2025) +<= 0 + +c_u_capacity_constraint(A_2020_summer_day_NGCC_2020)_: ++1 v_flow_out(A_2020_summer_day_NG_NGCC_2020_ELC) +-2.19 v_capacity(A_2020_NGCC_2020) +<= 0 + +c_u_capacity_constraint(A_2025_summer_night_NGCC_2015)_: ++1 v_flow_out(A_2025_summer_night_NG_NGCC_2015_ELC) +-2.19 v_capacity(A_2025_NGCC_2015) +<= 0 + +c_u_capacity_constraint(B_2025_summer_night_NGCC_2025)_: ++1 v_flow_out(B_2025_summer_night_NG_NGCC_2025_ELC) +-2.19 v_capacity(B_2025_NGCC_2025) +<= 0 + +c_e_capacity_constraint(A_2020_winter_day_SOLPV_2020)_: +-1 v_flow_out(A_2020_winter_day_ethos_SOLPV_2020_ELC) ++0.6569999999999999 v_capacity(A_2020_SOLPV_2020) +-1 v_curtailment(A_2020_winter_day_ethos_SOLPV_2020_ELC) += 0 + +c_u_capacity_constraint(B_2020_summer_night_NGCC_2020)_: ++1 v_flow_out(B_2020_summer_night_NG_NGCC_2020_ELC) +-2.19 v_capacity(B_2020_NGCC_2020) +<= 0 + +c_u_capacity_constraint(A_2025_winter_night_HEAT_SYS_2020)_: ++1 v_flow_out(A_2025_winter_night_ELC_HEAT_SYS_2020_HEAT) +-0.25 v_capacity(A_2025_HEAT_SYS_2020) +<= 0 + +c_u_capacity_constraint(A_C_2020_summer_day_E_TRANS_2015)_: ++1 v_flow_out(A_C_2020_summer_day_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(A_C_2020_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(C_B_2020_summer_night_E_TRANS_2015)_: ++1 v_flow_out(C_B_2020_summer_night_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(C_B_2020_E_TRANS_2015) +<= 0 + +c_e_capacity_constraint(A_2020_winter_night_SOLPV_2015)_: +-1 v_flow_out(A_2020_winter_night_ethos_SOLPV_2015_ELC) +-1 v_curtailment(A_2020_winter_night_ethos_SOLPV_2015_ELC) += 0 + +c_u_capacity_constraint(B_2025_summer_day_NGCC_2015)_: ++1 v_flow_out(B_2025_summer_day_NG_NGCC_2015_ELC) +-2.19 v_capacity(B_2025_NGCC_2015) +<= 0 + +c_u_capacity_constraint(A_C_2025_summer_day_E_TRANS_2015)_: ++1 v_flow_out(A_C_2025_summer_day_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(A_C_2025_E_TRANS_2015) +<= 0 + +c_e_capacity_constraint(A_2020_summer_night_SOLPV_2015)_: +-1 v_flow_out(A_2020_summer_night_ethos_SOLPV_2015_ELC) +-1 v_curtailment(A_2020_summer_night_ethos_SOLPV_2015_ELC) += 0 + +c_u_capacity_constraint(A_2025_winter_day_NGCC_2015)_: ++1 v_flow_out(A_2025_winter_day_NG_NGCC_2015_ELC) +-2.19 v_capacity(A_2025_NGCC_2015) +<= 0 + +c_u_capacity_constraint(A_B_2020_summer_day_E_TRANS_2015)_: ++1 v_flow_out(A_B_2020_summer_day_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(A_B_2020_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(B_2020_winter_night_HEAT_SYS_2020)_: +-0.25 v_capacity(B_2020_HEAT_SYS_2020) +<= -0.75 + +c_u_capacity_constraint(C_B_2025_winter_day_E_TRANS_2015)_: ++1 v_flow_out(C_B_2025_winter_day_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(C_B_2025_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(B_2020_winter_day_NGCC_2015)_: ++1 v_flow_out(B_2020_winter_day_NG_NGCC_2015_ELC) +-2.19 v_capacity(B_2020_NGCC_2015) +<= 0 + +c_u_capacity_constraint(C_A_2025_winter_day_E_TRANS_2015)_: ++1 v_flow_out(C_A_2025_winter_day_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(C_A_2025_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(B_2020_summer_day_NGCC_2020)_: ++1 v_flow_out(B_2020_summer_day_NG_NGCC_2020_ELC) +-2.19 v_capacity(B_2020_NGCC_2020) +<= 0 + +c_u_capacity_constraint(B_A_2025_winter_day_E_TRANS_2015)_: ++1 v_flow_out(B_A_2025_winter_day_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(B_A_2025_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(A_2020_summer_day_NGCC_2015)_: ++1 v_flow_out(A_2020_summer_day_NG_NGCC_2015_ELC) +-2.19 v_capacity(A_2020_NGCC_2015) +<= 0 + +c_u_capacity_constraint(A_C_2020_winter_night_E_TRANS_2015)_: ++1 v_flow_out(A_C_2020_winter_night_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(A_C_2020_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(A_2025_summer_night_HEAT_SYS_2025)_: ++1 v_flow_out(A_2025_summer_night_ELC_HEAT_SYS_2025_HEAT) +-0.25 v_capacity(A_2025_HEAT_SYS_2025) +<= 0 + +c_e_capacity_constraint(A_2020_winter_day_SOLPV_2015)_: +-1 v_flow_out(A_2020_winter_day_ethos_SOLPV_2015_ELC) ++0.6569999999999999 v_capacity(A_2020_SOLPV_2015) +-1 v_curtailment(A_2020_winter_day_ethos_SOLPV_2015_ELC) += 0 + +c_u_capacity_constraint(B_2025_summer_night_NGCC_2020)_: ++1 v_flow_out(B_2025_summer_night_NG_NGCC_2020_ELC) +-2.19 v_capacity(B_2025_NGCC_2020) +<= 0 + +c_u_capacity_constraint(B_2020_summer_night_NGCC_2015)_: ++1 v_flow_out(B_2020_summer_night_NG_NGCC_2015_ELC) +-2.19 v_capacity(B_2020_NGCC_2015) +<= 0 + +c_u_capacity_constraint(C_2025_winter_night_HEAT_ANN_2020)_: +-0.25 v_capacity(C_2025_HEAT_ANN_2020) +<= -0.275 + +c_u_capacity_constraint(B_A_2025_summer_night_E_TRANS_2015)_: ++1 v_flow_out(B_A_2025_summer_night_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(B_A_2025_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(A_2020_winter_night_NGCC_2020)_: ++1 v_flow_out(A_2020_winter_night_NG_NGCC_2020_ELC) +-2.19 v_capacity(A_2020_NGCC_2020) +<= 0 + +c_e_capacity_constraint(A_2025_winter_day_SOLPV_2025)_: +-1 v_flow_out(A_2025_winter_day_ethos_SOLPV_2025_ELC) ++0.6569999999999999 v_capacity(A_2025_SOLPV_2025) +-1 v_curtailment(A_2025_winter_day_ethos_SOLPV_2025_ELC) += 0 + +c_u_capacity_constraint(B_2020_winter_day_HEAT_SYS_2020)_: +-0.25 v_capacity(B_2020_HEAT_SYS_2020) +<= -0.75 + +c_u_capacity_constraint(B_2025_winter_day_HEAT_SYS_2025)_: ++1 v_flow_out(B_2025_winter_day_ELC_HEAT_SYS_2025_HEAT) +-0.25 v_capacity(B_2025_HEAT_SYS_2025) +<= 0 + +c_u_capacity_constraint(A_2020_summer_night_NGCC_2020)_: ++1 v_flow_out(A_2020_summer_night_NG_NGCC_2020_ELC) +-2.19 v_capacity(A_2020_NGCC_2020) +<= 0 + +c_u_capacity_constraint(C_A_2025_summer_night_E_TRANS_2015)_: ++1 v_flow_out(C_A_2025_summer_night_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(C_A_2025_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(B_2025_summer_day_HEAT_SYS_2025)_: ++1 v_flow_out(B_2025_summer_day_ELC_HEAT_SYS_2025_HEAT) +-0.25 v_capacity(B_2025_HEAT_SYS_2025) +<= 0 + +c_u_capacity_constraint(B_C_2025_winter_day_E_TRANS_2015)_: ++1 v_flow_out(B_C_2025_winter_day_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(B_C_2025_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(B_2025_winter_day_NGCC_2025)_: ++1 v_flow_out(B_2025_winter_day_NG_NGCC_2025_ELC) +-2.19 v_capacity(B_2025_NGCC_2025) +<= 0 + +c_u_capacity_constraint(B_2025_summer_night_HEAT_SYS_2025)_: ++1 v_flow_out(B_2025_summer_night_ELC_HEAT_SYS_2025_HEAT) +-0.25 v_capacity(B_2025_HEAT_SYS_2025) +<= 0 + +c_u_capacity_constraint(B_2020_summer_day_NGCC_2015)_: ++1 v_flow_out(B_2020_summer_day_NG_NGCC_2015_ELC) +-2.19 v_capacity(B_2020_NGCC_2015) +<= 0 + +c_u_capacity_constraint(C_B_2025_winter_night_E_TRANS_2015)_: ++1 v_flow_out(C_B_2025_winter_night_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(C_B_2025_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(B_A_2025_summer_day_E_TRANS_2015)_: ++1 v_flow_out(B_A_2025_summer_day_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(B_A_2025_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(A_2020_winter_day_NGCC_2020)_: ++1 v_flow_out(A_2020_winter_day_NG_NGCC_2020_ELC) +-2.19 v_capacity(A_2020_NGCC_2020) +<= 0 + +c_u_capacity_constraint(A_B_2020_winter_night_E_TRANS_2015)_: ++1 v_flow_out(A_B_2020_winter_night_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(A_B_2020_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(A_2025_summer_night_HEAT_SYS_2020)_: ++1 v_flow_out(A_2025_summer_night_ELC_HEAT_SYS_2020_HEAT) +-0.25 v_capacity(A_2025_HEAT_SYS_2020) +<= 0 + +c_u_capacity_constraint(B_A_2025_winter_night_E_TRANS_2015)_: ++1 v_flow_out(B_A_2025_winter_night_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(B_A_2025_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(A_C_2025_summer_night_E_TRANS_2015)_: ++1 v_flow_out(A_C_2025_summer_night_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(A_C_2025_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(A_C_2025_winter_night_E_TRANS_2015)_: ++1 v_flow_out(A_C_2025_winter_night_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(A_C_2025_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(B_A_2020_summer_night_E_TRANS_2015)_: ++1 v_flow_out(B_A_2020_summer_night_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(B_A_2020_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(A_2020_winter_night_NGCC_2015)_: ++1 v_flow_out(A_2020_winter_night_NG_NGCC_2015_ELC) +-2.19 v_capacity(A_2020_NGCC_2015) +<= 0 + +c_e_capacity_constraint(A_2025_winter_day_SOLPV_2020)_: +-1 v_flow_out(A_2025_winter_day_ethos_SOLPV_2020_ELC) ++0.6569999999999999 v_capacity(A_2025_SOLPV_2020) +-1 v_curtailment(A_2025_winter_day_ethos_SOLPV_2020_ELC) += 0 + +c_u_capacity_constraint(B_2025_winter_day_HEAT_SYS_2020)_: ++1 v_flow_out(B_2025_winter_day_ELC_HEAT_SYS_2020_HEAT) +-0.25 v_capacity(B_2025_HEAT_SYS_2020) +<= 0 + +c_e_capacity_constraint(A_2025_summer_day_SOLPV_2025)_: +-1 v_flow_out(A_2025_summer_day_ethos_SOLPV_2025_ELC) ++1.3139999999999998 v_capacity(A_2025_SOLPV_2025) +-1 v_curtailment(A_2025_summer_day_ethos_SOLPV_2025_ELC) += 0 + +c_u_capacity_constraint(A_2020_summer_night_NGCC_2015)_: ++1 v_flow_out(A_2020_summer_night_NG_NGCC_2015_ELC) +-2.19 v_capacity(A_2020_NGCC_2015) +<= 0 + +c_u_capacity_constraint(B_2025_summer_day_HEAT_SYS_2020)_: ++1 v_flow_out(B_2025_summer_day_ELC_HEAT_SYS_2020_HEAT) +-0.25 v_capacity(B_2025_HEAT_SYS_2020) +<= 0 + +c_u_capacity_constraint(C_B_2020_winter_night_E_TRANS_2015)_: ++1 v_flow_out(C_B_2020_winter_night_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(C_B_2020_E_TRANS_2015) +<= 0 + +c_e_capacity_constraint(A_2025_winter_night_SOLPV_2025)_: +-1 v_flow_out(A_2025_winter_night_ethos_SOLPV_2025_ELC) +-1 v_curtailment(A_2025_winter_night_ethos_SOLPV_2025_ELC) += 0 + +c_u_capacity_constraint(C_2025_summer_night_HEAT_ANN_2020)_: +-0.25 v_capacity(C_2025_HEAT_ANN_2020) +<= -0.275 + +c_e_capacity_constraint(A_2025_summer_night_SOLPV_2025)_: +-1 v_flow_out(A_2025_summer_night_ethos_SOLPV_2025_ELC) +-1 v_curtailment(A_2025_summer_night_ethos_SOLPV_2025_ELC) += 0 + +c_u_capacity_constraint(A_2025_winter_day_HEAT_SYS_2020)_: ++1 v_flow_out(A_2025_winter_day_ELC_HEAT_SYS_2020_HEAT) +-0.25 v_capacity(A_2025_HEAT_SYS_2020) +<= 0 + +c_u_capacity_constraint(A_2025_summer_day_NGCC_2025)_: ++1 v_flow_out(A_2025_summer_day_NG_NGCC_2025_ELC) +-2.19 v_capacity(A_2025_NGCC_2025) +<= 0 + +c_u_capacity_constraint(C_A_2020_winter_night_E_TRANS_2015)_: ++1 v_flow_out(C_A_2020_winter_night_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(C_A_2020_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(A_2025_winter_night_NGCC_2025)_: ++1 v_flow_out(A_2025_winter_night_NG_NGCC_2025_ELC) +-2.19 v_capacity(A_2025_NGCC_2025) +<= 0 + +c_u_capacity_constraint(A_2020_summer_night_HEAT_SYS_2020)_: +-0.25 v_capacity(A_2020_HEAT_SYS_2020) +<= -1.25 + +c_u_capacity_constraint(B_A_2020_winter_night_E_TRANS_2015)_: ++1 v_flow_out(B_A_2020_winter_night_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(B_A_2020_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(B_2025_winter_day_NGCC_2020)_: ++1 v_flow_out(B_2025_winter_day_NG_NGCC_2020_ELC) +-2.19 v_capacity(B_2025_NGCC_2020) +<= 0 + +c_u_capacity_constraint(B_2025_summer_night_HEAT_SYS_2020)_: ++1 v_flow_out(B_2025_summer_night_ELC_HEAT_SYS_2020_HEAT) +-0.25 v_capacity(B_2025_HEAT_SYS_2020) +<= 0 + +c_u_capacity_constraint(A_C_2025_winter_day_E_TRANS_2015)_: ++1 v_flow_out(A_C_2025_winter_day_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(A_C_2025_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(C_A_2020_summer_day_E_TRANS_2015)_: ++1 v_flow_out(C_A_2020_summer_day_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(C_A_2020_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(B_C_2025_summer_night_E_TRANS_2015)_: ++1 v_flow_out(B_C_2025_summer_night_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(B_C_2025_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(A_2020_summer_day_HEAT_SYS_2020)_: +-0.25 v_capacity(A_2020_HEAT_SYS_2020) +<= -1.25 + +c_u_capacity_constraint(A_2020_winter_night_HEAT_SYS_2020)_: +-0.25 v_capacity(A_2020_HEAT_SYS_2020) +<= -1.25 + +c_u_capacity_constraint(A_2020_winter_day_NGCC_2015)_: ++1 v_flow_out(A_2020_winter_day_NG_NGCC_2015_ELC) +-2.19 v_capacity(A_2020_NGCC_2015) +<= 0 + +c_u_capacity_constraint(B_C_2020_summer_night_E_TRANS_2015)_: ++1 v_flow_out(B_C_2020_summer_night_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(B_C_2020_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(B_2025_winter_night_HEAT_SYS_2025)_: ++1 v_flow_out(B_2025_winter_night_ELC_HEAT_SYS_2025_HEAT) +-0.25 v_capacity(B_2025_HEAT_SYS_2025) +<= 0 + +c_u_capacity_constraint(C_2025_summer_day_HEAT_ANN_2020)_: +-0.25 v_capacity(C_2025_HEAT_ANN_2020) +<= -0.275 + +c_u_capacity_constraint(A_B_2020_winter_day_E_TRANS_2015)_: ++1 v_flow_out(A_B_2020_winter_day_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(A_B_2020_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(A_B_2025_summer_night_E_TRANS_2015)_: ++1 v_flow_out(A_B_2025_summer_night_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(A_B_2025_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(A_2020_winter_day_HEAT_SYS_2020)_: +-0.25 v_capacity(A_2020_HEAT_SYS_2020) +<= -1.25 + +c_u_capacity_constraint(B_2020_summer_night_HEAT_SYS_2020)_: +-0.25 v_capacity(B_2020_HEAT_SYS_2020) +<= -0.75 + +c_u_capacity_constraint(C_B_2020_winter_day_E_TRANS_2015)_: ++1 v_flow_out(C_B_2020_winter_day_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(C_B_2020_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(B_C_2025_winter_night_E_TRANS_2015)_: ++1 v_flow_out(B_C_2025_winter_night_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(B_C_2025_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(C_A_2020_winter_day_E_TRANS_2015)_: ++1 v_flow_out(C_A_2020_winter_day_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(C_A_2020_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(B_C_2020_winter_night_E_TRANS_2015)_: ++1 v_flow_out(B_C_2020_winter_night_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(B_C_2020_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(A_C_2020_summer_night_E_TRANS_2015)_: ++1 v_flow_out(A_C_2020_summer_night_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(A_C_2020_E_TRANS_2015) +<= 0 + +c_u_capacity_constraint(B_C_2025_summer_day_E_TRANS_2015)_: ++1 v_flow_out(B_C_2025_summer_day_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(B_C_2025_E_TRANS_2015) +<= 0 + +c_e_capacity_constraint(A_2025_summer_day_SOLPV_2020)_: +-1 v_flow_out(A_2025_summer_day_ethos_SOLPV_2020_ELC) ++1.3139999999999998 v_capacity(A_2025_SOLPV_2020) +-1 v_curtailment(A_2025_summer_day_ethos_SOLPV_2020_ELC) += 0 + +c_u_capacity_constraint(C_A_2025_summer_day_E_TRANS_2015)_: ++1 v_flow_out(C_A_2025_summer_day_ELC_E_TRANS_2015_ELC) +-2.19 v_capacity(C_A_2025_E_TRANS_2015) +<= 0 + +c_e_capacity_constraint(A_2025_winter_night_SOLPV_2020)_: +-1 v_flow_out(A_2025_winter_night_ethos_SOLPV_2020_ELC) +-1 v_curtailment(A_2025_winter_night_ethos_SOLPV_2020_ELC) += 0 + +c_e_capacity_constraint(A_2025_summer_night_SOLPV_2020)_: +-1 v_flow_out(A_2025_summer_night_ethos_SOLPV_2020_ELC) +-1 v_curtailment(A_2025_summer_night_ethos_SOLPV_2020_ELC) += 0 + +c_u_capacity_constraint(B_2025_winter_night_NGCC_2025)_: ++1 v_flow_out(B_2025_winter_night_NG_NGCC_2025_ELC) +-2.19 v_capacity(B_2025_NGCC_2025) +<= 0 + +c_u_capacity_constraint(A_2025_summer_day_NGCC_2020)_: ++1 v_flow_out(A_2025_summer_day_NG_NGCC_2020_ELC) +-2.19 v_capacity(A_2025_NGCC_2020) +<= 0 + +c_u_capacity_constraint(A_2025_winter_night_NGCC_2020)_: ++1 v_flow_out(A_2025_winter_night_NG_NGCC_2020_ELC) +-2.19 v_capacity(A_2025_NGCC_2020) +<= 0 + +c_u_capacity_constraint(B_2025_winter_day_NGCC_2015)_: ++1 v_flow_out(B_2025_winter_day_NG_NGCC_2015_ELC) +-2.19 v_capacity(B_2025_NGCC_2015) +<= 0 + +c_u_capacity_constraint(B_2025_summer_night_NGCC_2015)_: ++1 v_flow_out(B_2025_summer_night_NG_NGCC_2015_ELC) +-2.19 v_capacity(B_2025_NGCC_2015) +<= 0 + +c_u_capacity_constraint(B_2020_summer_day_HEAT_SYS_2020)_: +-0.25 v_capacity(B_2020_HEAT_SYS_2020) +<= -0.75 + +c_u_capacity_annual_constraint(C_2025_ANN_IMP_2020)_: +-1 v_capacity(C_2025_ANN_IMP_2020) ++1 v_flow_out_annual(C_2025_ethos_ANN_IMP_2020_ELC_C) +<= 0 + +c_u_capacity_annual_constraint(C_2020_ANN_IMP_2020)_: +-1 v_capacity(C_2020_ANN_IMP_2020) ++1 v_flow_out_annual(C_2020_ethos_ANN_IMP_2020_ELC_C) +<= 0 + +c_e_capacity_available_by_period_and_tech_constraint(B_2025_NGCC)_: +-1 v_capacity(B_2025_NGCC_2025) +-1 v_capacity(B_2025_NGCC_2015) +-1 v_capacity(B_2025_NGCC_2020) ++1 v_capacity_available_by_period_and_tech(B_2025_NGCC) += 0 + +c_e_capacity_available_by_period_and_tech_constraint(A_2020_E_BATT)_: +-1 v_capacity(A_2020_E_BATT_2020) ++1 v_capacity_available_by_period_and_tech(A_2020_E_BATT) += 0 + +c_e_capacity_available_by_period_and_tech_constraint(B_2025_HEAT_SYS)_: +-1 v_capacity(B_2025_HEAT_SYS_2020) +-1 v_capacity(B_2025_HEAT_SYS_2025) ++1 v_capacity_available_by_period_and_tech(B_2025_HEAT_SYS) += 0 + +c_e_capacity_available_by_period_and_tech_constraint(C_2025_HEAT_ANN)_: +-1 v_capacity(C_2025_HEAT_ANN_2020) ++1 v_capacity_available_by_period_and_tech(C_2025_HEAT_ANN) += 0 + +c_e_capacity_available_by_period_and_tech_constraint(A_B_2025_E_TRANS)_: +-1 v_capacity(A_B_2025_E_TRANS_2015) ++1 v_capacity_available_by_period_and_tech(A_B_2025_E_TRANS) += 0 + +c_e_capacity_available_by_period_and_tech_constraint(B_C_2025_E_TRANS)_: +-1 v_capacity(B_C_2025_E_TRANS_2015) ++1 v_capacity_available_by_period_and_tech(B_C_2025_E_TRANS) += 0 + +c_e_capacity_available_by_period_and_tech_constraint(A_C_2020_E_TRANS)_: +-1 v_capacity(A_C_2020_E_TRANS_2015) ++1 v_capacity_available_by_period_and_tech(A_C_2020_E_TRANS) += 0 + +c_e_capacity_available_by_period_and_tech_constraint(C_2020_HEAT_ANN)_: +-1 v_capacity(C_2020_HEAT_ANN_2020) ++1 v_capacity_available_by_period_and_tech(C_2020_HEAT_ANN) += 0 + +c_e_capacity_available_by_period_and_tech_constraint(C_B_2025_E_TRANS)_: +-1 v_capacity(C_B_2025_E_TRANS_2015) ++1 v_capacity_available_by_period_and_tech(C_B_2025_E_TRANS) += 0 + +c_e_capacity_available_by_period_and_tech_constraint(A_2025_SOLPV)_: +-1 v_capacity(A_2025_SOLPV_2015) +-1 v_capacity(A_2025_SOLPV_2020) +-1 v_capacity(A_2025_SOLPV_2025) ++1 v_capacity_available_by_period_and_tech(A_2025_SOLPV) += 0 + +c_e_capacity_available_by_period_and_tech_constraint(B_2020_NGCC)_: +-1 v_capacity(B_2020_NGCC_2015) +-1 v_capacity(B_2020_NGCC_2020) ++1 v_capacity_available_by_period_and_tech(B_2020_NGCC) += 0 + +c_e_capacity_available_by_period_and_tech_constraint(A_B_2020_E_TRANS)_: +-1 v_capacity(A_B_2020_E_TRANS_2015) ++1 v_capacity_available_by_period_and_tech(A_B_2020_E_TRANS) += 0 + +c_e_capacity_available_by_period_and_tech_constraint(B_A_2025_E_TRANS)_: +-1 v_capacity(B_A_2025_E_TRANS_2015) ++1 v_capacity_available_by_period_and_tech(B_A_2025_E_TRANS) += 0 + +c_e_capacity_available_by_period_and_tech_constraint(B_C_2020_E_TRANS)_: +-1 v_capacity(B_C_2020_E_TRANS_2015) ++1 v_capacity_available_by_period_and_tech(B_C_2020_E_TRANS) += 0 + +c_e_capacity_available_by_period_and_tech_constraint(B_2020_HEAT_SYS)_: +-1 v_capacity(B_2020_HEAT_SYS_2020) ++1 v_capacity_available_by_period_and_tech(B_2020_HEAT_SYS) += 0 + +c_e_capacity_available_by_period_and_tech_constraint(A_2025_NGCC)_: +-1 v_capacity(A_2025_NGCC_2015) +-1 v_capacity(A_2025_NGCC_2020) +-1 v_capacity(A_2025_NGCC_2025) ++1 v_capacity_available_by_period_and_tech(A_2025_NGCC) += 0 + +c_e_capacity_available_by_period_and_tech_constraint(C_2025_ANN_IMP)_: +-1 v_capacity(C_2025_ANN_IMP_2020) ++1 v_capacity_available_by_period_and_tech(C_2025_ANN_IMP) += 0 + +c_e_capacity_available_by_period_and_tech_constraint(C_B_2020_E_TRANS)_: +-1 v_capacity(C_B_2020_E_TRANS_2015) ++1 v_capacity_available_by_period_and_tech(C_B_2020_E_TRANS) += 0 + +c_e_capacity_available_by_period_and_tech_constraint(A_2025_HEAT_SYS)_: +-1 v_capacity(A_2025_HEAT_SYS_2020) +-1 v_capacity(A_2025_HEAT_SYS_2025) ++1 v_capacity_available_by_period_and_tech(A_2025_HEAT_SYS) += 0 + +c_e_capacity_available_by_period_and_tech_constraint(C_A_2025_E_TRANS)_: +-1 v_capacity(C_A_2025_E_TRANS_2015) ++1 v_capacity_available_by_period_and_tech(C_A_2025_E_TRANS) += 0 + +c_e_capacity_available_by_period_and_tech_constraint(A_2020_SOLPV)_: +-1 v_capacity(A_2020_SOLPV_2015) +-1 v_capacity(A_2020_SOLPV_2020) ++1 v_capacity_available_by_period_and_tech(A_2020_SOLPV) += 0 + +c_e_capacity_available_by_period_and_tech_constraint(B_A_2020_E_TRANS)_: +-1 v_capacity(B_A_2020_E_TRANS_2015) ++1 v_capacity_available_by_period_and_tech(B_A_2020_E_TRANS) += 0 + +c_e_capacity_available_by_period_and_tech_constraint(A_2025_E_BATT)_: +-1 v_capacity(A_2025_E_BATT_2020) +-1 v_capacity(A_2025_E_BATT_2025) ++1 v_capacity_available_by_period_and_tech(A_2025_E_BATT) += 0 + +c_e_capacity_available_by_period_and_tech_constraint(A_C_2025_E_TRANS)_: +-1 v_capacity(A_C_2025_E_TRANS_2015) ++1 v_capacity_available_by_period_and_tech(A_C_2025_E_TRANS) += 0 + +c_e_capacity_available_by_period_and_tech_constraint(A_2020_NGCC)_: +-1 v_capacity(A_2020_NGCC_2020) +-1 v_capacity(A_2020_NGCC_2015) ++1 v_capacity_available_by_period_and_tech(A_2020_NGCC) += 0 + +c_e_capacity_available_by_period_and_tech_constraint(C_2020_ANN_IMP)_: +-1 v_capacity(C_2020_ANN_IMP_2020) ++1 v_capacity_available_by_period_and_tech(C_2020_ANN_IMP) += 0 + +c_e_capacity_available_by_period_and_tech_constraint(C_A_2020_E_TRANS)_: +-1 v_capacity(C_A_2020_E_TRANS_2015) ++1 v_capacity_available_by_period_and_tech(C_A_2020_E_TRANS) += 0 + +c_e_capacity_available_by_period_and_tech_constraint(A_2020_HEAT_SYS)_: +-1 v_capacity(A_2020_HEAT_SYS_2020) ++1 v_capacity_available_by_period_and_tech(A_2020_HEAT_SYS) += 0 + +c_e_adjusted_capacity_constraint(C_2025_HEAT_ANN_2020)_: ++1 v_capacity(C_2025_HEAT_ANN_2020) +-1 v_new_capacity(C_HEAT_ANN_2020) += 0 + +c_e_adjusted_capacity_constraint(A_2020_HEAT_SYS_2020)_: ++1 v_capacity(A_2020_HEAT_SYS_2020) +-1 v_new_capacity(A_HEAT_SYS_2020) += 0 + +c_e_adjusted_capacity_constraint(A_2025_SOLPV_2015)_: ++1 v_capacity(A_2025_SOLPV_2015) += 0.2 + +c_e_adjusted_capacity_constraint(A_2025_E_BATT_2020)_: ++1 v_capacity(A_2025_E_BATT_2020) +-1 v_new_capacity(A_E_BATT_2020) += 0 + +c_e_adjusted_capacity_constraint(A_2025_NGCC_2015)_: ++1 v_capacity(A_2025_NGCC_2015) += 0.5 + +c_e_adjusted_capacity_constraint(A_B_2025_E_TRANS_2015)_: ++1 v_capacity(A_B_2025_E_TRANS_2015) += 1.0 + +c_e_adjusted_capacity_constraint(A_2020_NGCC_2020)_: ++1 v_capacity(A_2020_NGCC_2020) +-1 v_new_capacity(A_NGCC_2020) += 0 + +c_e_adjusted_capacity_constraint(B_2025_NGCC_2025)_: ++1 v_capacity(B_2025_NGCC_2025) +-1 v_new_capacity(B_NGCC_2025) += 0 + +c_e_adjusted_capacity_constraint(B_C_2020_E_TRANS_2015)_: ++1 v_capacity(B_C_2020_E_TRANS_2015) += 1.0 + +c_e_adjusted_capacity_constraint(C_B_2025_E_TRANS_2015)_: ++1 v_capacity(C_B_2025_E_TRANS_2015) += 1.0 + +c_e_adjusted_capacity_constraint(A_2025_SOLPV_2020)_: ++1 v_capacity(A_2025_SOLPV_2020) +-1 v_new_capacity(A_SOLPV_2020) += 0 + +c_e_adjusted_capacity_constraint(C_A_2025_E_TRANS_2015)_: ++1 v_capacity(C_A_2025_E_TRANS_2015) += 1.0 + +c_e_adjusted_capacity_constraint(A_2025_NGCC_2020)_: ++1 v_capacity(A_2025_NGCC_2020) +-1 v_new_capacity(A_NGCC_2020) += 0 + +c_e_adjusted_capacity_constraint(A_2025_E_BATT_2025)_: ++1 v_capacity(A_2025_E_BATT_2025) +-1 v_new_capacity(A_E_BATT_2025) += 0 + +c_e_adjusted_capacity_constraint(A_2020_SOLPV_2015)_: ++1 v_capacity(A_2020_SOLPV_2015) += 0.2 + +c_e_adjusted_capacity_constraint(B_2020_NGCC_2015)_: ++1 v_capacity(B_2020_NGCC_2015) += 0.3 + +c_e_adjusted_capacity_constraint(A_2025_HEAT_SYS_2020)_: ++1 v_capacity(A_2025_HEAT_SYS_2020) +-1 v_new_capacity(A_HEAT_SYS_2020) += 0 + +c_e_adjusted_capacity_constraint(B_2025_NGCC_2015)_: ++1 v_capacity(B_2025_NGCC_2015) += 0.3 + +c_e_adjusted_capacity_constraint(B_2025_HEAT_SYS_2020)_: ++1 v_capacity(B_2025_HEAT_SYS_2020) +-1 v_new_capacity(B_HEAT_SYS_2020) += 0 + +c_e_adjusted_capacity_constraint(B_A_2025_E_TRANS_2015)_: ++1 v_capacity(B_A_2025_E_TRANS_2015) += 1.0 + +c_e_adjusted_capacity_constraint(C_2020_ANN_IMP_2020)_: ++1 v_capacity(C_2020_ANN_IMP_2020) +-1 v_new_capacity(C_ANN_IMP_2020) += 0 + +c_e_adjusted_capacity_constraint(C_2020_HEAT_ANN_2020)_: ++1 v_capacity(C_2020_HEAT_ANN_2020) +-1 v_new_capacity(C_HEAT_ANN_2020) += 0 + +c_e_adjusted_capacity_constraint(A_B_2020_E_TRANS_2015)_: ++1 v_capacity(A_B_2020_E_TRANS_2015) += 1.0 + +c_e_adjusted_capacity_constraint(A_C_2020_E_TRANS_2015)_: ++1 v_capacity(A_C_2020_E_TRANS_2015) += 1.0 + +c_e_adjusted_capacity_constraint(A_2020_E_BATT_2020)_: ++1 v_capacity(A_2020_E_BATT_2020) +-1 v_new_capacity(A_E_BATT_2020) += 0 + +c_e_adjusted_capacity_constraint(A_C_2025_E_TRANS_2015)_: ++1 v_capacity(A_C_2025_E_TRANS_2015) += 1.0 + +c_e_adjusted_capacity_constraint(B_2020_HEAT_SYS_2020)_: ++1 v_capacity(B_2020_HEAT_SYS_2020) +-1 v_new_capacity(B_HEAT_SYS_2020) += 0 + +c_e_adjusted_capacity_constraint(A_2025_SOLPV_2025)_: ++1 v_capacity(A_2025_SOLPV_2025) +-1 v_new_capacity(A_SOLPV_2025) += 0 + +c_e_adjusted_capacity_constraint(B_C_2025_E_TRANS_2015)_: ++1 v_capacity(B_C_2025_E_TRANS_2015) += 1.0 + +c_e_adjusted_capacity_constraint(A_2025_NGCC_2025)_: ++1 v_capacity(A_2025_NGCC_2025) +-1 v_new_capacity(A_NGCC_2025) += 0 + +c_e_adjusted_capacity_constraint(A_2025_HEAT_SYS_2025)_: ++1 v_capacity(A_2025_HEAT_SYS_2025) +-1 v_new_capacity(A_HEAT_SYS_2025) += 0 + +c_e_adjusted_capacity_constraint(A_2020_SOLPV_2020)_: ++1 v_capacity(A_2020_SOLPV_2020) +-1 v_new_capacity(A_SOLPV_2020) += 0 + +c_e_adjusted_capacity_constraint(A_2020_NGCC_2015)_: ++1 v_capacity(A_2020_NGCC_2015) += 0.5 + +c_e_adjusted_capacity_constraint(B_2020_NGCC_2020)_: ++1 v_capacity(B_2020_NGCC_2020) +-1 v_new_capacity(B_NGCC_2020) += 0 + +c_e_adjusted_capacity_constraint(B_2025_NGCC_2020)_: ++1 v_capacity(B_2025_NGCC_2020) +-1 v_new_capacity(B_NGCC_2020) += 0 + +c_e_adjusted_capacity_constraint(C_B_2020_E_TRANS_2015)_: ++1 v_capacity(C_B_2020_E_TRANS_2015) += 1.0 + +c_e_adjusted_capacity_constraint(B_2025_HEAT_SYS_2025)_: ++1 v_capacity(B_2025_HEAT_SYS_2025) +-1 v_new_capacity(B_HEAT_SYS_2025) += 0 + +c_e_adjusted_capacity_constraint(B_A_2020_E_TRANS_2015)_: ++1 v_capacity(B_A_2020_E_TRANS_2015) += 1.0 + +c_e_adjusted_capacity_constraint(C_A_2020_E_TRANS_2015)_: ++1 v_capacity(C_A_2020_E_TRANS_2015) += 1.0 + +c_e_adjusted_capacity_constraint(C_2025_ANN_IMP_2020)_: ++1 v_capacity(C_2025_ANN_IMP_2020) +-1 v_new_capacity(C_ANN_IMP_2020) += 0 + +c_e_demand_constraint(A_2025_HEAT)_: ++1 v_flow_out_annual(A_2025_ELC_HEAT_SYS_2025_HEAT) ++1 v_flow_out_annual(A_2025_ELC_HEAT_SYS_2020_HEAT) += 5.5 + +c_e_demand_constraint(B_2025_HEAT)_: ++1 v_flow_out_annual(B_2025_ELC_HEAT_SYS_2020_HEAT) ++1 v_flow_out_annual(B_2025_ELC_HEAT_SYS_2025_HEAT) += 3.3 + +c_e_demand_activity_constraint(A_2025_winter_day_HEAT_SYS_2020_HEAT)_: +-1 v_flow_out(A_2025_winter_day_ELC_HEAT_SYS_2020_HEAT) ++0.25 v_flow_out_annual(A_2025_ELC_HEAT_SYS_2020_HEAT) += 0 + +c_e_demand_activity_constraint(B_2025_summer_night_HEAT_SYS_2025_HEAT)_: +-1 v_flow_out(B_2025_summer_night_ELC_HEAT_SYS_2025_HEAT) ++0.25 v_flow_out_annual(B_2025_ELC_HEAT_SYS_2025_HEAT) += 0 + +c_e_demand_activity_constraint(B_2025_winter_night_HEAT_SYS_2025_HEAT)_: +-1 v_flow_out(B_2025_winter_night_ELC_HEAT_SYS_2025_HEAT) ++0.25 v_flow_out_annual(B_2025_ELC_HEAT_SYS_2025_HEAT) += 0 + +c_e_demand_activity_constraint(B_2025_summer_night_HEAT_SYS_2020_HEAT)_: +-1 v_flow_out(B_2025_summer_night_ELC_HEAT_SYS_2020_HEAT) ++0.25 v_flow_out_annual(B_2025_ELC_HEAT_SYS_2020_HEAT) += 0 + +c_e_demand_activity_constraint(B_2025_winter_night_HEAT_SYS_2020_HEAT)_: +-1 v_flow_out(B_2025_winter_night_ELC_HEAT_SYS_2020_HEAT) ++0.25 v_flow_out_annual(B_2025_ELC_HEAT_SYS_2020_HEAT) += 0 + +c_e_demand_activity_constraint(B_2025_summer_day_HEAT_SYS_2025_HEAT)_: +-1 v_flow_out(B_2025_summer_day_ELC_HEAT_SYS_2025_HEAT) ++0.25 v_flow_out_annual(B_2025_ELC_HEAT_SYS_2025_HEAT) += 0 + +c_e_demand_activity_constraint(A_2025_winter_night_HEAT_SYS_2025_HEAT)_: +-1 v_flow_out(A_2025_winter_night_ELC_HEAT_SYS_2025_HEAT) ++0.25 v_flow_out_annual(A_2025_ELC_HEAT_SYS_2025_HEAT) += 0 + +c_e_demand_activity_constraint(A_2025_winter_night_HEAT_SYS_2020_HEAT)_: +-1 v_flow_out(A_2025_winter_night_ELC_HEAT_SYS_2020_HEAT) ++0.25 v_flow_out_annual(A_2025_ELC_HEAT_SYS_2020_HEAT) += 0 + +c_e_demand_activity_constraint(B_2025_summer_day_HEAT_SYS_2020_HEAT)_: +-1 v_flow_out(B_2025_summer_day_ELC_HEAT_SYS_2020_HEAT) ++0.25 v_flow_out_annual(B_2025_ELC_HEAT_SYS_2020_HEAT) += 0 + +c_e_demand_activity_constraint(A_2025_winter_day_HEAT_SYS_2025_HEAT)_: +-1 v_flow_out(A_2025_winter_day_ELC_HEAT_SYS_2025_HEAT) ++0.25 v_flow_out_annual(A_2025_ELC_HEAT_SYS_2025_HEAT) += 0 + +c_e_demand_activity_constraint(B_2025_winter_day_HEAT_SYS_2025_HEAT)_: +-1 v_flow_out(B_2025_winter_day_ELC_HEAT_SYS_2025_HEAT) ++0.25 v_flow_out_annual(B_2025_ELC_HEAT_SYS_2025_HEAT) += 0 + +c_e_demand_activity_constraint(B_2025_winter_day_HEAT_SYS_2020_HEAT)_: +-1 v_flow_out(B_2025_winter_day_ELC_HEAT_SYS_2020_HEAT) ++0.25 v_flow_out_annual(B_2025_ELC_HEAT_SYS_2020_HEAT) += 0 + +c_e_demand_activity_constraint(A_2025_summer_night_HEAT_SYS_2025_HEAT)_: +-1 v_flow_out(A_2025_summer_night_ELC_HEAT_SYS_2025_HEAT) ++0.25 v_flow_out_annual(A_2025_ELC_HEAT_SYS_2025_HEAT) += 0 + +c_e_demand_activity_constraint(A_2025_summer_night_HEAT_SYS_2020_HEAT)_: +-1 v_flow_out(A_2025_summer_night_ELC_HEAT_SYS_2020_HEAT) ++0.25 v_flow_out_annual(A_2025_ELC_HEAT_SYS_2020_HEAT) += 0 + +c_e_demand_activity_constraint(A_2025_summer_day_HEAT_SYS_2025_HEAT)_: +-1 v_flow_out(A_2025_summer_day_ELC_HEAT_SYS_2025_HEAT) ++0.25 v_flow_out_annual(A_2025_ELC_HEAT_SYS_2025_HEAT) += 0 + +c_e_demand_activity_constraint(A_2025_summer_day_HEAT_SYS_2020_HEAT)_: +-1 v_flow_out(A_2025_summer_day_ELC_HEAT_SYS_2020_HEAT) ++0.25 v_flow_out_annual(A_2025_ELC_HEAT_SYS_2020_HEAT) += 0 + +c_e_commodity_balance_constraint(B_2020_winter_day_NG)_: +-1.8181818181818181 v_flow_out(B_2020_winter_day_NG_NGCC_2020_ELC) ++1 v_flow_out(B_2020_winter_day_ethos_IMP_NG_2020_NG) +-1.8181818181818181 v_flow_out(B_2020_winter_day_NG_NGCC_2015_ELC) += 0 + +c_e_commodity_balance_constraint(B_2025_summer_night_ELC)_: ++1 v_flow_out(B_2025_summer_night_NG_NGCC_2015_ELC) +-1.0526315789473684 v_flow_out(B_C_2025_summer_night_ELC_E_TRANS_2015_ELC) +-1 v_flow_out(B_2025_summer_night_ELC_HEAT_SYS_2020_HEAT) +-1.0526315789473684 v_flow_out(B_A_2025_summer_night_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(B_2025_summer_night_NG_NGCC_2020_ELC) +-1 v_flow_out(B_2025_summer_night_ELC_HEAT_SYS_2025_HEAT) ++1 v_flow_out(C_B_2025_summer_night_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(A_B_2025_summer_night_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(B_2025_summer_night_NG_NGCC_2025_ELC) += 0 + +c_e_commodity_balance_constraint(C_2020_winter_night_ELC)_: +-1.0526315789473684 v_flow_out(C_B_2020_winter_night_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(B_C_2020_winter_night_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(A_C_2020_winter_night_ELC_E_TRANS_2015_ELC) +-1.0526315789473684 v_flow_out(C_A_2020_winter_night_ELC_E_TRANS_2015_ELC) += 0 + +c_e_commodity_balance_constraint(A_2025_winter_night_NG)_: +-1.8181818181818181 v_flow_out(A_2025_winter_night_NG_NGCC_2015_ELC) +-1.8181818181818181 v_flow_out(A_2025_winter_night_NG_NGCC_2020_ELC) +-1.8181818181818181 v_flow_out(A_2025_winter_night_NG_NGCC_2025_ELC) ++1 v_flow_out(A_2025_winter_night_ethos_IMP_NG_2020_NG) += 0 + +c_e_commodity_balance_constraint(A_2025_summer_night_ELC)_: ++1 v_flow_out(A_2025_summer_night_ethos_SOLPV_2015_ELC) ++1 v_flow_out(C_A_2025_summer_night_ELC_E_TRANS_2015_ELC) +-1 v_flow_out(A_2025_summer_night_ELC_HEAT_SYS_2020_HEAT) +-1.0526315789473684 v_flow_out(A_C_2025_summer_night_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(B_A_2025_summer_night_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(A_2025_summer_night_NG_NGCC_2020_ELC) ++1 v_flow_out(A_2025_summer_night_ethos_SOLPV_2020_ELC) ++1 v_flow_out(A_2025_summer_night_ELC_E_BATT_2020_ELC) ++1 v_flow_out(A_2025_summer_night_ELC_E_BATT_2025_ELC) +-1 v_flow_out(A_2025_summer_night_ELC_HEAT_SYS_2025_HEAT) +-1.0526315789473684 v_flow_out(A_B_2025_summer_night_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(A_2025_summer_night_NG_NGCC_2025_ELC) ++1 v_flow_out(A_2025_summer_night_ethos_SOLPV_2025_ELC) ++1 v_flow_out(A_2025_summer_night_NG_NGCC_2015_ELC) +-1 v_flow_in(A_2025_summer_night_ELC_E_BATT_2020_ELC) +-1 v_flow_in(A_2025_summer_night_ELC_E_BATT_2025_ELC) += 0 + +c_e_commodity_balance_constraint(A_2025_winter_day_ELC)_: ++1 v_flow_out(B_A_2025_winter_day_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(A_2025_winter_day_ELC_E_BATT_2020_ELC) +-1 v_flow_out(A_2025_winter_day_ELC_HEAT_SYS_2020_HEAT) +-1.0526315789473684 v_flow_out(A_C_2025_winter_day_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(C_A_2025_winter_day_ELC_E_TRANS_2015_ELC) +-1.0526315789473684 v_flow_out(A_B_2025_winter_day_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(A_2025_winter_day_NG_NGCC_2020_ELC) ++1 v_flow_out(A_2025_winter_day_ethos_SOLPV_2015_ELC) ++1 v_flow_out(A_2025_winter_day_ELC_E_BATT_2025_ELC) +-1 v_flow_out(A_2025_winter_day_ELC_HEAT_SYS_2025_HEAT) ++1 v_flow_out(A_2025_winter_day_ethos_SOLPV_2020_ELC) ++1 v_flow_out(A_2025_winter_day_NG_NGCC_2025_ELC) ++1 v_flow_out(A_2025_winter_day_ethos_SOLPV_2025_ELC) ++1 v_flow_out(A_2025_winter_day_NG_NGCC_2015_ELC) +-1 v_flow_in(A_2025_winter_day_ELC_E_BATT_2020_ELC) +-1 v_flow_in(A_2025_winter_day_ELC_E_BATT_2025_ELC) += 0 + +c_e_commodity_balance_constraint(B_2025_winter_day_NG)_: +-1.8181818181818181 v_flow_out(B_2025_winter_day_NG_NGCC_2025_ELC) ++1 v_flow_out(B_2025_winter_day_ethos_IMP_NG_2020_NG) +-1.8181818181818181 v_flow_out(B_2025_winter_day_NG_NGCC_2015_ELC) +-1.8181818181818181 v_flow_out(B_2025_winter_day_NG_NGCC_2020_ELC) += 0 + +c_e_commodity_balance_constraint(C_2020_summer_night_ELC)_: +-1.0526315789473684 v_flow_out(C_A_2020_summer_night_ELC_E_TRANS_2015_ELC) +-1.0526315789473684 v_flow_out(C_B_2020_summer_night_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(A_C_2020_summer_night_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(B_C_2020_summer_night_ELC_E_TRANS_2015_ELC) += 0 + +c_e_commodity_balance_constraint(A_2020_summer_night_NG)_: +-1.8181818181818181 v_flow_out(A_2020_summer_night_NG_NGCC_2015_ELC) +-1.8181818181818181 v_flow_out(A_2020_summer_night_NG_NGCC_2020_ELC) ++1 v_flow_out(A_2020_summer_night_ethos_IMP_NG_2020_NG) += 0 + +c_e_commodity_balance_constraint(A_2020_winter_day_NG)_: +-1.8181818181818181 v_flow_out(A_2020_winter_day_NG_NGCC_2015_ELC) +-1.8181818181818181 v_flow_out(A_2020_winter_day_NG_NGCC_2020_ELC) ++1 v_flow_out(A_2020_winter_day_ethos_IMP_NG_2020_NG) += 0 + +c_e_commodity_balance_constraint(C_2025_summer_day_ELC)_: ++1 v_flow_out(B_C_2025_summer_day_ELC_E_TRANS_2015_ELC) +-1.0526315789473684 v_flow_out(C_B_2025_summer_day_ELC_E_TRANS_2015_ELC) +-1.0526315789473684 v_flow_out(C_A_2025_summer_day_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(A_C_2025_summer_day_ELC_E_TRANS_2015_ELC) += 0 + +c_e_commodity_balance_constraint(B_2025_winter_night_NG)_: ++1 v_flow_out(B_2025_winter_night_ethos_IMP_NG_2020_NG) +-1.8181818181818181 v_flow_out(B_2025_winter_night_NG_NGCC_2015_ELC) +-1.8181818181818181 v_flow_out(B_2025_winter_night_NG_NGCC_2020_ELC) +-1.8181818181818181 v_flow_out(B_2025_winter_night_NG_NGCC_2025_ELC) += 0 + +c_e_commodity_balance_constraint(A_2020_summer_day_ELC)_: +-1.0526315789473684 v_flow_out(A_C_2020_summer_day_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(A_2020_summer_day_NG_NGCC_2020_ELC) ++1 v_flow_out(A_2020_summer_day_ethos_SOLPV_2020_ELC) ++1 v_flow_out(A_2020_summer_day_ELC_E_BATT_2020_ELC) ++1 v_flow_out(A_2020_summer_day_NG_NGCC_2015_ELC) ++1 v_flow_out(B_A_2020_summer_day_ELC_E_TRANS_2015_ELC) +-1.0526315789473684 v_flow_out(A_B_2020_summer_day_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(C_A_2020_summer_day_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(A_2020_summer_day_ethos_SOLPV_2015_ELC) +-1 v_flow_in(A_2020_summer_day_ELC_E_BATT_2020_ELC) += 1.25 + +c_e_commodity_balance_constraint(B_2020_summer_day_ELC)_: ++1 v_flow_out(B_2020_summer_day_NG_NGCC_2020_ELC) ++1 v_flow_out(C_B_2020_summer_day_ELC_E_TRANS_2015_ELC) +-1.0526315789473684 v_flow_out(B_A_2020_summer_day_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(B_2020_summer_day_NG_NGCC_2015_ELC) +-1.0526315789473684 v_flow_out(B_C_2020_summer_day_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(A_B_2020_summer_day_ELC_E_TRANS_2015_ELC) += 0.75 + +c_e_commodity_balance_constraint(B_2025_summer_day_NG)_: +-1.8181818181818181 v_flow_out(B_2025_summer_day_NG_NGCC_2020_ELC) ++1 v_flow_out(B_2025_summer_day_ethos_IMP_NG_2020_NG) +-1.8181818181818181 v_flow_out(B_2025_summer_day_NG_NGCC_2025_ELC) +-1.8181818181818181 v_flow_out(B_2025_summer_day_NG_NGCC_2015_ELC) += 0 + +c_e_commodity_balance_constraint(B_2020_summer_night_NG)_: +-1.8181818181818181 v_flow_out(B_2020_summer_night_NG_NGCC_2015_ELC) +-1.8181818181818181 v_flow_out(B_2020_summer_night_NG_NGCC_2020_ELC) ++1 v_flow_out(B_2020_summer_night_ethos_IMP_NG_2020_NG) += 0 + +c_e_commodity_balance_constraint(B_2020_winter_day_ELC)_: +-1.0526315789473684 v_flow_out(B_A_2020_winter_day_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(B_2020_winter_day_NG_NGCC_2020_ELC) +-1.0526315789473684 v_flow_out(B_C_2020_winter_day_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(B_2020_winter_day_NG_NGCC_2015_ELC) ++1 v_flow_out(C_B_2020_winter_day_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(A_B_2020_winter_day_ELC_E_TRANS_2015_ELC) += 0.75 + +c_e_commodity_balance_constraint(B_2020_winter_night_NG)_: +-1.8181818181818181 v_flow_out(B_2020_winter_night_NG_NGCC_2015_ELC) +-1.8181818181818181 v_flow_out(B_2020_winter_night_NG_NGCC_2020_ELC) ++1 v_flow_out(B_2020_winter_night_ethos_IMP_NG_2020_NG) += 0 + +c_e_commodity_balance_constraint(A_2025_summer_day_NG)_: +-1.8181818181818181 v_flow_out(A_2025_summer_day_NG_NGCC_2020_ELC) ++1 v_flow_out(A_2025_summer_day_ethos_IMP_NG_2020_NG) +-1.8181818181818181 v_flow_out(A_2025_summer_day_NG_NGCC_2025_ELC) +-1.8181818181818181 v_flow_out(A_2025_summer_day_NG_NGCC_2015_ELC) += 0 + +c_e_commodity_balance_constraint(A_2025_winter_night_ELC)_: ++1 v_flow_out(A_2025_winter_night_NG_NGCC_2015_ELC) ++1 v_flow_out(C_A_2025_winter_night_ELC_E_TRANS_2015_ELC) +-1.0526315789473684 v_flow_out(A_B_2025_winter_night_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(A_2025_winter_night_ethos_SOLPV_2015_ELC) ++1 v_flow_out(A_2025_winter_night_ELC_E_BATT_2020_ELC) +-1 v_flow_out(A_2025_winter_night_ELC_HEAT_SYS_2020_HEAT) +-1.0526315789473684 v_flow_out(A_C_2025_winter_night_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(A_2025_winter_night_NG_NGCC_2020_ELC) ++1 v_flow_out(A_2025_winter_night_ethos_SOLPV_2020_ELC) ++1 v_flow_out(B_A_2025_winter_night_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(A_2025_winter_night_NG_NGCC_2025_ELC) ++1 v_flow_out(A_2025_winter_night_ELC_E_BATT_2025_ELC) +-1 v_flow_out(A_2025_winter_night_ELC_HEAT_SYS_2025_HEAT) ++1 v_flow_out(A_2025_winter_night_ethos_SOLPV_2025_ELC) +-1 v_flow_in(A_2025_winter_night_ELC_E_BATT_2025_ELC) +-1 v_flow_in(A_2025_winter_night_ELC_E_BATT_2020_ELC) += 0 + +c_e_commodity_balance_constraint(C_2025_winter_night_ELC)_: ++1 v_flow_out(B_C_2025_winter_night_ELC_E_TRANS_2015_ELC) +-1.0526315789473684 v_flow_out(C_A_2025_winter_night_ELC_E_TRANS_2015_ELC) +-1.0526315789473684 v_flow_out(C_B_2025_winter_night_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(A_C_2025_winter_night_ELC_E_TRANS_2015_ELC) += 0 + +c_e_commodity_balance_constraint(B_2025_winter_day_ELC)_: +-1.0526315789473684 v_flow_out(B_A_2025_winter_day_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(B_2025_winter_day_NG_NGCC_2025_ELC) +-1.0526315789473684 v_flow_out(B_C_2025_winter_day_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(A_B_2025_winter_day_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(C_B_2025_winter_day_ELC_E_TRANS_2015_ELC) +-1 v_flow_out(B_2025_winter_day_ELC_HEAT_SYS_2020_HEAT) ++1 v_flow_out(B_2025_winter_day_NG_NGCC_2015_ELC) +-1 v_flow_out(B_2025_winter_day_ELC_HEAT_SYS_2025_HEAT) ++1 v_flow_out(B_2025_winter_day_NG_NGCC_2020_ELC) += 0 + +c_e_commodity_balance_constraint(C_2020_winter_day_ELC)_: ++1 v_flow_out(A_C_2020_winter_day_ELC_E_TRANS_2015_ELC) +-1.0526315789473684 v_flow_out(C_A_2020_winter_day_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(B_C_2020_winter_day_ELC_E_TRANS_2015_ELC) +-1.0526315789473684 v_flow_out(C_B_2020_winter_day_ELC_E_TRANS_2015_ELC) += 0 + +c_e_commodity_balance_constraint(A_2020_summer_night_ELC)_: ++1 v_flow_out(A_2020_summer_night_ELC_E_BATT_2020_ELC) ++1 v_flow_out(C_A_2020_summer_night_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(A_2020_summer_night_NG_NGCC_2015_ELC) ++1 v_flow_out(A_2020_summer_night_ethos_SOLPV_2015_ELC) ++1 v_flow_out(A_2020_summer_night_NG_NGCC_2020_ELC) +-1.0526315789473684 v_flow_out(A_C_2020_summer_night_ELC_E_TRANS_2015_ELC) +-1.0526315789473684 v_flow_out(A_B_2020_summer_night_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(A_2020_summer_night_ethos_SOLPV_2020_ELC) ++1 v_flow_out(B_A_2020_summer_night_ELC_E_TRANS_2015_ELC) +-1 v_flow_in(A_2020_summer_night_ELC_E_BATT_2020_ELC) += 1.25 + +c_e_commodity_balance_constraint(A_2020_winter_day_ELC)_: ++1 v_flow_out(B_A_2020_winter_day_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(A_2020_winter_day_ELC_E_BATT_2020_ELC) +-1.0526315789473684 v_flow_out(A_C_2020_winter_day_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(A_2020_winter_day_NG_NGCC_2015_ELC) ++1 v_flow_out(A_2020_winter_day_ethos_SOLPV_2015_ELC) ++1 v_flow_out(C_A_2020_winter_day_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(A_2020_winter_day_NG_NGCC_2020_ELC) +-1.0526315789473684 v_flow_out(A_B_2020_winter_day_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(A_2020_winter_day_ethos_SOLPV_2020_ELC) +-1 v_flow_in(A_2020_winter_day_ELC_E_BATT_2020_ELC) += 1.25 + +c_e_commodity_balance_constraint(A_2020_winter_night_NG)_: +-1.8181818181818181 v_flow_out(A_2020_winter_night_NG_NGCC_2020_ELC) ++1 v_flow_out(A_2020_winter_night_ethos_IMP_NG_2020_NG) +-1.8181818181818181 v_flow_out(A_2020_winter_night_NG_NGCC_2015_ELC) += 0 + +c_e_commodity_balance_constraint(A_2025_summer_night_NG)_: +-1.8181818181818181 v_flow_out(A_2025_summer_night_NG_NGCC_2020_ELC) ++1 v_flow_out(A_2025_summer_night_ethos_IMP_NG_2020_NG) +-1.8181818181818181 v_flow_out(A_2025_summer_night_NG_NGCC_2025_ELC) +-1.8181818181818181 v_flow_out(A_2025_summer_night_NG_NGCC_2015_ELC) += 0 + +c_e_commodity_balance_constraint(B_2025_summer_night_NG)_: +-1.8181818181818181 v_flow_out(B_2025_summer_night_NG_NGCC_2015_ELC) +-1.8181818181818181 v_flow_out(B_2025_summer_night_NG_NGCC_2020_ELC) ++1 v_flow_out(B_2025_summer_night_ethos_IMP_NG_2020_NG) +-1.8181818181818181 v_flow_out(B_2025_summer_night_NG_NGCC_2025_ELC) += 0 + +c_e_commodity_balance_constraint(A_2025_winter_day_NG)_: +-1.8181818181818181 v_flow_out(A_2025_winter_day_NG_NGCC_2020_ELC) +-1.8181818181818181 v_flow_out(A_2025_winter_day_NG_NGCC_2025_ELC) ++1 v_flow_out(A_2025_winter_day_ethos_IMP_NG_2020_NG) +-1.8181818181818181 v_flow_out(A_2025_winter_day_NG_NGCC_2015_ELC) += 0 + +c_e_commodity_balance_constraint(C_2025_summer_night_ELC)_: +-1.0526315789473684 v_flow_out(C_A_2025_summer_night_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(B_C_2025_summer_night_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(A_C_2025_summer_night_ELC_E_TRANS_2015_ELC) +-1.0526315789473684 v_flow_out(C_B_2025_summer_night_ELC_E_TRANS_2015_ELC) += 0 + +c_e_commodity_balance_constraint(B_2025_winter_night_ELC)_: +-1.0526315789473684 v_flow_out(B_C_2025_winter_night_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(B_2025_winter_night_NG_NGCC_2015_ELC) ++1 v_flow_out(A_B_2025_winter_night_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(C_B_2025_winter_night_ELC_E_TRANS_2015_ELC) +-1 v_flow_out(B_2025_winter_night_ELC_HEAT_SYS_2020_HEAT) +-1.0526315789473684 v_flow_out(B_A_2025_winter_night_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(B_2025_winter_night_NG_NGCC_2020_ELC) +-1 v_flow_out(B_2025_winter_night_ELC_HEAT_SYS_2025_HEAT) ++1 v_flow_out(B_2025_winter_night_NG_NGCC_2025_ELC) += 0 + +c_e_commodity_balance_constraint(B_2020_summer_night_ELC)_: ++1 v_flow_out(B_2020_summer_night_NG_NGCC_2015_ELC) ++1 v_flow_out(C_B_2020_summer_night_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(B_2020_summer_night_NG_NGCC_2020_ELC) +-1.0526315789473684 v_flow_out(B_C_2020_summer_night_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(A_B_2020_summer_night_ELC_E_TRANS_2015_ELC) +-1.0526315789473684 v_flow_out(B_A_2020_summer_night_ELC_E_TRANS_2015_ELC) += 0.75 + +c_e_commodity_balance_constraint(B_2020_winter_night_ELC)_: ++1 v_flow_out(B_2020_winter_night_NG_NGCC_2015_ELC) ++1 v_flow_out(C_B_2020_winter_night_ELC_E_TRANS_2015_ELC) +-1.0526315789473684 v_flow_out(B_A_2020_winter_night_ELC_E_TRANS_2015_ELC) +-1.0526315789473684 v_flow_out(B_C_2020_winter_night_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(B_2020_winter_night_NG_NGCC_2020_ELC) ++1 v_flow_out(A_B_2020_winter_night_ELC_E_TRANS_2015_ELC) += 0.75 + +c_e_commodity_balance_constraint(A_2025_summer_day_ELC)_: +-1.0526315789473684 v_flow_out(A_B_2025_summer_day_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(A_2025_summer_day_NG_NGCC_2020_ELC) ++1 v_flow_out(A_2025_summer_day_ethos_SOLPV_2020_ELC) ++1 v_flow_out(B_A_2025_summer_day_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(A_2025_summer_day_ELC_E_BATT_2025_ELC) +-1 v_flow_out(A_2025_summer_day_ELC_HEAT_SYS_2025_HEAT) ++1 v_flow_out(A_2025_summer_day_NG_NGCC_2025_ELC) ++1 v_flow_out(A_2025_summer_day_ethos_SOLPV_2025_ELC) ++1 v_flow_out(C_A_2025_summer_day_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(A_2025_summer_day_NG_NGCC_2015_ELC) ++1 v_flow_out(A_2025_summer_day_ethos_SOLPV_2015_ELC) ++1 v_flow_out(A_2025_summer_day_ELC_E_BATT_2020_ELC) +-1 v_flow_out(A_2025_summer_day_ELC_HEAT_SYS_2020_HEAT) +-1.0526315789473684 v_flow_out(A_C_2025_summer_day_ELC_E_TRANS_2015_ELC) +-1 v_flow_in(A_2025_summer_day_ELC_E_BATT_2025_ELC) +-1 v_flow_in(A_2025_summer_day_ELC_E_BATT_2020_ELC) += 0 + +c_e_commodity_balance_constraint(B_2025_summer_day_ELC)_: ++1 v_flow_out(A_B_2025_summer_day_ELC_E_TRANS_2015_ELC) +-1.0526315789473684 v_flow_out(B_A_2025_summer_day_ELC_E_TRANS_2015_ELC) +-1 v_flow_out(B_2025_summer_day_ELC_HEAT_SYS_2020_HEAT) ++1 v_flow_out(B_2025_summer_day_NG_NGCC_2020_ELC) +-1.0526315789473684 v_flow_out(B_C_2025_summer_day_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(C_B_2025_summer_day_ELC_E_TRANS_2015_ELC) +-1 v_flow_out(B_2025_summer_day_ELC_HEAT_SYS_2025_HEAT) ++1 v_flow_out(B_2025_summer_day_NG_NGCC_2025_ELC) ++1 v_flow_out(B_2025_summer_day_NG_NGCC_2015_ELC) += 0 + +c_e_commodity_balance_constraint(C_2020_summer_day_ELC)_: ++1 v_flow_out(A_C_2020_summer_day_ELC_E_TRANS_2015_ELC) +-1.0526315789473684 v_flow_out(C_B_2020_summer_day_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(B_C_2020_summer_day_ELC_E_TRANS_2015_ELC) +-1.0526315789473684 v_flow_out(C_A_2020_summer_day_ELC_E_TRANS_2015_ELC) += 0 + +c_e_commodity_balance_constraint(A_2020_summer_day_NG)_: +-1.8181818181818181 v_flow_out(A_2020_summer_day_NG_NGCC_2020_ELC) ++1 v_flow_out(A_2020_summer_day_ethos_IMP_NG_2020_NG) +-1.8181818181818181 v_flow_out(A_2020_summer_day_NG_NGCC_2015_ELC) += 0 + +c_e_commodity_balance_constraint(C_2025_winter_day_ELC)_: ++1 v_flow_out(A_C_2025_winter_day_ELC_E_TRANS_2015_ELC) +-1.0526315789473684 v_flow_out(C_A_2025_winter_day_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(B_C_2025_winter_day_ELC_E_TRANS_2015_ELC) +-1.0526315789473684 v_flow_out(C_B_2025_winter_day_ELC_E_TRANS_2015_ELC) += 0 + +c_e_commodity_balance_constraint(A_2020_winter_night_ELC)_: ++1 v_flow_out(B_A_2020_winter_night_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(A_2020_winter_night_NG_NGCC_2020_ELC) ++1 v_flow_out(A_2020_winter_night_ethos_SOLPV_2015_ELC) ++1 v_flow_out(A_2020_winter_night_ELC_E_BATT_2020_ELC) +-1.0526315789473684 v_flow_out(A_C_2020_winter_night_ELC_E_TRANS_2015_ELC) +-1.0526315789473684 v_flow_out(A_B_2020_winter_night_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(A_2020_winter_night_NG_NGCC_2015_ELC) ++1 v_flow_out(C_A_2020_winter_night_ELC_E_TRANS_2015_ELC) ++1 v_flow_out(A_2020_winter_night_ethos_SOLPV_2020_ELC) +-1 v_flow_in(A_2020_winter_night_ELC_E_BATT_2020_ELC) += 1.25 + +c_e_commodity_balance_constraint(B_2020_summer_day_NG)_: +-1.8181818181818181 v_flow_out(B_2020_summer_day_NG_NGCC_2020_ELC) ++1 v_flow_out(B_2020_summer_day_ethos_IMP_NG_2020_NG) +-1.8181818181818181 v_flow_out(B_2020_summer_day_NG_NGCC_2015_ELC) += 0 + +c_e_annual_commodity_balance_constraint(C_2020_ELC_C)_: ++1 v_flow_out_annual(C_2020_ethos_ANN_IMP_2020_ELC_C) += 1.0 + +c_e_annual_commodity_balance_constraint(C_2025_ELC_C)_: ++1 v_flow_out_annual(C_2025_ethos_ANN_IMP_2020_ELC_C) += 1.1 + +c_e_regional_exchange_capacity_constraint(A_C_2020_E_TRANS_2015)_: ++1 v_capacity(A_C_2020_E_TRANS_2015) +-1 v_capacity(C_A_2020_E_TRANS_2015) += 0 + +c_e_regional_exchange_capacity_constraint(B_A_2020_E_TRANS_2015)_: +-1 v_capacity(A_B_2020_E_TRANS_2015) ++1 v_capacity(B_A_2020_E_TRANS_2015) += 0 + +c_e_regional_exchange_capacity_constraint(C_B_2020_E_TRANS_2015)_: +-1 v_capacity(B_C_2020_E_TRANS_2015) ++1 v_capacity(C_B_2020_E_TRANS_2015) += 0 + +c_e_regional_exchange_capacity_constraint(A_B_2025_E_TRANS_2015)_: ++1 v_capacity(A_B_2025_E_TRANS_2015) +-1 v_capacity(B_A_2025_E_TRANS_2015) += 0 + +c_e_regional_exchange_capacity_constraint(C_A_2025_E_TRANS_2015)_: ++1 v_capacity(C_A_2025_E_TRANS_2015) +-1 v_capacity(A_C_2025_E_TRANS_2015) += 0 + +c_e_regional_exchange_capacity_constraint(C_A_2020_E_TRANS_2015)_: +-1 v_capacity(A_C_2020_E_TRANS_2015) ++1 v_capacity(C_A_2020_E_TRANS_2015) += 0 + +c_e_regional_exchange_capacity_constraint(A_B_2020_E_TRANS_2015)_: ++1 v_capacity(A_B_2020_E_TRANS_2015) +-1 v_capacity(B_A_2020_E_TRANS_2015) += 0 + +c_e_regional_exchange_capacity_constraint(B_C_2025_E_TRANS_2015)_: +-1 v_capacity(C_B_2025_E_TRANS_2015) ++1 v_capacity(B_C_2025_E_TRANS_2015) += 0 + +c_e_regional_exchange_capacity_constraint(A_C_2025_E_TRANS_2015)_: +-1 v_capacity(C_A_2025_E_TRANS_2015) ++1 v_capacity(A_C_2025_E_TRANS_2015) += 0 + +c_e_regional_exchange_capacity_constraint(B_A_2025_E_TRANS_2015)_: +-1 v_capacity(A_B_2025_E_TRANS_2015) ++1 v_capacity(B_A_2025_E_TRANS_2015) += 0 + +c_e_regional_exchange_capacity_constraint(C_B_2025_E_TRANS_2015)_: ++1 v_capacity(C_B_2025_E_TRANS_2015) +-1 v_capacity(B_C_2025_E_TRANS_2015) += 0 + +c_e_regional_exchange_capacity_constraint(B_C_2020_E_TRANS_2015)_: ++1 v_capacity(B_C_2020_E_TRANS_2015) +-1 v_capacity(C_B_2020_E_TRANS_2015) += 0 + +c_e_storage_energy_constraint(A_2020_winter_night_E_BATT_2020)_: +-1 v_flow_out(A_2020_winter_night_ELC_E_BATT_2020_ELC) ++0.85 v_flow_in(A_2020_winter_night_ELC_E_BATT_2020_ELC) ++1 v_storage_init(A_2020_winter_E_BATT_2020) +-1 v_storage_level(A_2020_winter_day_E_BATT_2020) += 0 + +c_e_storage_energy_constraint(A_2020_summer_night_E_BATT_2020)_: +-1 v_flow_out(A_2020_summer_night_ELC_E_BATT_2020_ELC) ++0.85 v_flow_in(A_2020_summer_night_ELC_E_BATT_2020_ELC) ++1 v_storage_init(A_2020_summer_E_BATT_2020) +-1 v_storage_level(A_2020_summer_day_E_BATT_2020) += 0 + +c_e_storage_energy_constraint(A_2025_winter_day_E_BATT_2020)_: +-1 v_flow_out(A_2025_winter_day_ELC_E_BATT_2020_ELC) ++0.85 v_flow_in(A_2025_winter_day_ELC_E_BATT_2020_ELC) ++1 v_storage_level(A_2025_winter_day_E_BATT_2020) +-1 v_storage_level(A_2025_winter_night_E_BATT_2020) += 0 + +c_e_storage_energy_constraint(A_2025_summer_night_E_BATT_2025)_: +-1 v_flow_out(A_2025_summer_night_ELC_E_BATT_2025_ELC) ++0.85 v_flow_in(A_2025_summer_night_ELC_E_BATT_2025_ELC) ++1 v_storage_init(A_2025_summer_E_BATT_2025) +-1 v_storage_level(A_2025_summer_day_E_BATT_2025) += 0 + +c_e_storage_energy_constraint(A_2025_summer_day_E_BATT_2025)_: +-1 v_flow_out(A_2025_summer_day_ELC_E_BATT_2025_ELC) ++0.85 v_flow_in(A_2025_summer_day_ELC_E_BATT_2025_ELC) +-1 v_storage_level(A_2025_summer_night_E_BATT_2025) ++1 v_storage_level(A_2025_summer_day_E_BATT_2025) += 0 + +c_e_storage_energy_constraint(A_2025_winter_night_E_BATT_2020)_: +-1 v_flow_out(A_2025_winter_night_ELC_E_BATT_2020_ELC) ++0.85 v_flow_in(A_2025_winter_night_ELC_E_BATT_2020_ELC) ++1 v_storage_init(A_2025_winter_E_BATT_2020) +-1 v_storage_level(A_2025_winter_day_E_BATT_2020) += 0 + +c_e_storage_energy_constraint(A_2025_winter_day_E_BATT_2025)_: +-1 v_flow_out(A_2025_winter_day_ELC_E_BATT_2025_ELC) ++0.85 v_flow_in(A_2025_winter_day_ELC_E_BATT_2025_ELC) ++1 v_storage_level(A_2025_winter_day_E_BATT_2025) +-1 v_storage_level(A_2025_winter_night_E_BATT_2025) += 0 + +c_e_storage_energy_constraint(A_2020_winter_day_E_BATT_2020)_: +-1 v_flow_out(A_2020_winter_day_ELC_E_BATT_2020_ELC) ++0.85 v_flow_in(A_2020_winter_day_ELC_E_BATT_2020_ELC) +-1 v_storage_level(A_2020_winter_night_E_BATT_2020) ++1 v_storage_level(A_2020_winter_day_E_BATT_2020) += 0 + +c_e_storage_energy_constraint(A_2025_summer_night_E_BATT_2020)_: +-1 v_flow_out(A_2025_summer_night_ELC_E_BATT_2020_ELC) ++0.85 v_flow_in(A_2025_summer_night_ELC_E_BATT_2020_ELC) ++1 v_storage_init(A_2025_summer_E_BATT_2020) +-1 v_storage_level(A_2025_summer_day_E_BATT_2020) += 0 + +c_e_storage_energy_constraint(A_2025_summer_day_E_BATT_2020)_: +-1 v_flow_out(A_2025_summer_day_ELC_E_BATT_2020_ELC) ++0.85 v_flow_in(A_2025_summer_day_ELC_E_BATT_2020_ELC) +-1 v_storage_level(A_2025_summer_night_E_BATT_2020) ++1 v_storage_level(A_2025_summer_day_E_BATT_2020) += 0 + +c_e_storage_energy_constraint(A_2020_summer_day_E_BATT_2020)_: +-1 v_flow_out(A_2020_summer_day_ELC_E_BATT_2020_ELC) ++0.85 v_flow_in(A_2020_summer_day_ELC_E_BATT_2020_ELC) +-1 v_storage_level(A_2020_summer_night_E_BATT_2020) ++1 v_storage_level(A_2020_summer_day_E_BATT_2020) += 0 + +c_e_storage_energy_constraint(A_2025_winter_night_E_BATT_2025)_: +-1 v_flow_out(A_2025_winter_night_ELC_E_BATT_2025_ELC) ++0.85 v_flow_in(A_2025_winter_night_ELC_E_BATT_2025_ELC) ++1 v_storage_init(A_2025_winter_E_BATT_2025) +-1 v_storage_level(A_2025_winter_day_E_BATT_2025) += 0 + +c_e_storage_level_last_tod_constraint(A_2025_summer_E_BATT_2020)_: +-1 v_storage_init(A_2025_summer_E_BATT_2020) ++1 v_storage_level(A_2025_summer_night_E_BATT_2020) += 0 + +c_e_storage_level_last_tod_constraint(A_2025_winter_E_BATT_2020)_: +-1 v_storage_init(A_2025_winter_E_BATT_2020) ++1 v_storage_level(A_2025_winter_night_E_BATT_2020) += 0 + +c_e_storage_level_last_tod_constraint(A_2020_summer_E_BATT_2020)_: +-1 v_storage_init(A_2020_summer_E_BATT_2020) ++1 v_storage_level(A_2020_summer_night_E_BATT_2020) += 0 + +c_e_storage_level_last_tod_constraint(A_2020_winter_E_BATT_2020)_: +-1 v_storage_init(A_2020_winter_E_BATT_2020) ++1 v_storage_level(A_2020_winter_night_E_BATT_2020) += 0 + +c_e_storage_level_last_tod_constraint(A_2025_summer_E_BATT_2025)_: +-1 v_storage_init(A_2025_summer_E_BATT_2025) ++1 v_storage_level(A_2025_summer_night_E_BATT_2025) += 0 + +c_e_storage_level_last_tod_constraint(A_2025_winter_E_BATT_2025)_: +-1 v_storage_init(A_2025_winter_E_BATT_2025) ++1 v_storage_level(A_2025_winter_night_E_BATT_2025) += 0 + +c_u_storage_energy_upper_bound_constraint(A_2020_winter_night_E_BATT_2020)_: +-0.73 v_capacity(A_2020_E_BATT_2020) ++1 v_storage_level(A_2020_winter_night_E_BATT_2020) +<= 0 + +c_u_storage_energy_upper_bound_constraint(A_2020_summer_night_E_BATT_2020)_: +-0.73 v_capacity(A_2020_E_BATT_2020) ++1 v_storage_level(A_2020_summer_night_E_BATT_2020) +<= 0 + +c_u_storage_energy_upper_bound_constraint(A_2025_winter_day_E_BATT_2020)_: +-0.73 v_capacity(A_2025_E_BATT_2020) ++1 v_storage_level(A_2025_winter_day_E_BATT_2020) +<= 0 + +c_u_storage_energy_upper_bound_constraint(A_2025_summer_night_E_BATT_2025)_: +-0.73 v_capacity(A_2025_E_BATT_2025) ++1 v_storage_level(A_2025_summer_night_E_BATT_2025) +<= 0 + +c_u_storage_energy_upper_bound_constraint(A_2025_summer_day_E_BATT_2025)_: +-0.73 v_capacity(A_2025_E_BATT_2025) ++1 v_storage_level(A_2025_summer_day_E_BATT_2025) +<= 0 + +c_u_storage_energy_upper_bound_constraint(A_2025_winter_night_E_BATT_2020)_: +-0.73 v_capacity(A_2025_E_BATT_2020) ++1 v_storage_level(A_2025_winter_night_E_BATT_2020) +<= 0 + +c_u_storage_energy_upper_bound_constraint(A_2025_winter_day_E_BATT_2025)_: +-0.73 v_capacity(A_2025_E_BATT_2025) ++1 v_storage_level(A_2025_winter_day_E_BATT_2025) +<= 0 + +c_u_storage_energy_upper_bound_constraint(A_2020_winter_day_E_BATT_2020)_: +-0.73 v_capacity(A_2020_E_BATT_2020) ++1 v_storage_level(A_2020_winter_day_E_BATT_2020) +<= 0 + +c_u_storage_energy_upper_bound_constraint(A_2025_summer_night_E_BATT_2020)_: +-0.73 v_capacity(A_2025_E_BATT_2020) ++1 v_storage_level(A_2025_summer_night_E_BATT_2020) +<= 0 + +c_u_storage_energy_upper_bound_constraint(A_2025_summer_day_E_BATT_2020)_: +-0.73 v_capacity(A_2025_E_BATT_2020) ++1 v_storage_level(A_2025_summer_day_E_BATT_2020) +<= 0 + +c_u_storage_energy_upper_bound_constraint(A_2020_summer_day_E_BATT_2020)_: +-0.73 v_capacity(A_2020_E_BATT_2020) ++1 v_storage_level(A_2020_summer_day_E_BATT_2020) +<= 0 + +c_u_storage_energy_upper_bound_constraint(A_2025_winter_night_E_BATT_2025)_: +-0.73 v_capacity(A_2025_E_BATT_2025) ++1 v_storage_level(A_2025_winter_night_E_BATT_2025) +<= 0 + +c_u_storage_charge_rate_constraint(A_2020_winter_night_E_BATT_2020)_: +-2.19 v_capacity(A_2020_E_BATT_2020) ++0.85 v_flow_in(A_2020_winter_night_ELC_E_BATT_2020_ELC) +<= 0 + +c_u_storage_charge_rate_constraint(A_2020_summer_night_E_BATT_2020)_: +-2.19 v_capacity(A_2020_E_BATT_2020) ++0.85 v_flow_in(A_2020_summer_night_ELC_E_BATT_2020_ELC) +<= 0 + +c_u_storage_charge_rate_constraint(A_2025_winter_day_E_BATT_2020)_: +-2.19 v_capacity(A_2025_E_BATT_2020) ++0.85 v_flow_in(A_2025_winter_day_ELC_E_BATT_2020_ELC) +<= 0 + +c_u_storage_charge_rate_constraint(A_2025_summer_night_E_BATT_2025)_: +-2.19 v_capacity(A_2025_E_BATT_2025) ++0.85 v_flow_in(A_2025_summer_night_ELC_E_BATT_2025_ELC) +<= 0 + +c_u_storage_charge_rate_constraint(A_2025_summer_day_E_BATT_2025)_: +-2.19 v_capacity(A_2025_E_BATT_2025) ++0.85 v_flow_in(A_2025_summer_day_ELC_E_BATT_2025_ELC) +<= 0 + +c_u_storage_charge_rate_constraint(A_2025_winter_night_E_BATT_2020)_: +-2.19 v_capacity(A_2025_E_BATT_2020) ++0.85 v_flow_in(A_2025_winter_night_ELC_E_BATT_2020_ELC) +<= 0 + +c_u_storage_charge_rate_constraint(A_2025_winter_day_E_BATT_2025)_: +-2.19 v_capacity(A_2025_E_BATT_2025) ++0.85 v_flow_in(A_2025_winter_day_ELC_E_BATT_2025_ELC) +<= 0 + +c_u_storage_charge_rate_constraint(A_2020_winter_day_E_BATT_2020)_: +-2.19 v_capacity(A_2020_E_BATT_2020) ++0.85 v_flow_in(A_2020_winter_day_ELC_E_BATT_2020_ELC) +<= 0 + +c_u_storage_charge_rate_constraint(A_2025_summer_night_E_BATT_2020)_: +-2.19 v_capacity(A_2025_E_BATT_2020) ++0.85 v_flow_in(A_2025_summer_night_ELC_E_BATT_2020_ELC) +<= 0 + +c_u_storage_charge_rate_constraint(A_2025_summer_day_E_BATT_2020)_: +-2.19 v_capacity(A_2025_E_BATT_2020) ++0.85 v_flow_in(A_2025_summer_day_ELC_E_BATT_2020_ELC) +<= 0 + +c_u_storage_charge_rate_constraint(A_2020_summer_day_E_BATT_2020)_: +-2.19 v_capacity(A_2020_E_BATT_2020) ++0.85 v_flow_in(A_2020_summer_day_ELC_E_BATT_2020_ELC) +<= 0 + +c_u_storage_charge_rate_constraint(A_2025_winter_night_E_BATT_2025)_: +-2.19 v_capacity(A_2025_E_BATT_2025) ++0.85 v_flow_in(A_2025_winter_night_ELC_E_BATT_2025_ELC) +<= 0 + +c_u_storage_discharge_rate_constraint(A_2020_winter_night_E_BATT_2020)_: ++1 v_flow_out(A_2020_winter_night_ELC_E_BATT_2020_ELC) +-2.19 v_capacity(A_2020_E_BATT_2020) +<= 0 + +c_u_storage_discharge_rate_constraint(A_2020_summer_night_E_BATT_2020)_: ++1 v_flow_out(A_2020_summer_night_ELC_E_BATT_2020_ELC) +-2.19 v_capacity(A_2020_E_BATT_2020) +<= 0 + +c_u_storage_discharge_rate_constraint(A_2025_winter_day_E_BATT_2020)_: ++1 v_flow_out(A_2025_winter_day_ELC_E_BATT_2020_ELC) +-2.19 v_capacity(A_2025_E_BATT_2020) +<= 0 + +c_u_storage_discharge_rate_constraint(A_2025_summer_night_E_BATT_2025)_: ++1 v_flow_out(A_2025_summer_night_ELC_E_BATT_2025_ELC) +-2.19 v_capacity(A_2025_E_BATT_2025) +<= 0 + +c_u_storage_discharge_rate_constraint(A_2025_summer_day_E_BATT_2025)_: ++1 v_flow_out(A_2025_summer_day_ELC_E_BATT_2025_ELC) +-2.19 v_capacity(A_2025_E_BATT_2025) +<= 0 + +c_u_storage_discharge_rate_constraint(A_2025_winter_night_E_BATT_2020)_: ++1 v_flow_out(A_2025_winter_night_ELC_E_BATT_2020_ELC) +-2.19 v_capacity(A_2025_E_BATT_2020) +<= 0 + +c_u_storage_discharge_rate_constraint(A_2025_winter_day_E_BATT_2025)_: ++1 v_flow_out(A_2025_winter_day_ELC_E_BATT_2025_ELC) +-2.19 v_capacity(A_2025_E_BATT_2025) +<= 0 + +c_u_storage_discharge_rate_constraint(A_2020_winter_day_E_BATT_2020)_: ++1 v_flow_out(A_2020_winter_day_ELC_E_BATT_2020_ELC) +-2.19 v_capacity(A_2020_E_BATT_2020) +<= 0 + +c_u_storage_discharge_rate_constraint(A_2025_summer_night_E_BATT_2020)_: ++1 v_flow_out(A_2025_summer_night_ELC_E_BATT_2020_ELC) +-2.19 v_capacity(A_2025_E_BATT_2020) +<= 0 + +c_u_storage_discharge_rate_constraint(A_2025_summer_day_E_BATT_2020)_: ++1 v_flow_out(A_2025_summer_day_ELC_E_BATT_2020_ELC) +-2.19 v_capacity(A_2025_E_BATT_2020) +<= 0 + +c_u_storage_discharge_rate_constraint(A_2020_summer_day_E_BATT_2020)_: ++1 v_flow_out(A_2020_summer_day_ELC_E_BATT_2020_ELC) +-2.19 v_capacity(A_2020_E_BATT_2020) +<= 0 + +c_u_storage_discharge_rate_constraint(A_2025_winter_night_E_BATT_2025)_: ++1 v_flow_out(A_2025_winter_night_ELC_E_BATT_2025_ELC) +-2.19 v_capacity(A_2025_E_BATT_2025) +<= 0 + +c_u_storage_throughput_constraint(A_2020_winter_night_E_BATT_2020)_: ++1 v_flow_out(A_2020_winter_night_ELC_E_BATT_2020_ELC) +-2.19 v_capacity(A_2020_E_BATT_2020) ++0.85 v_flow_in(A_2020_winter_night_ELC_E_BATT_2020_ELC) +<= 0 + +c_u_storage_throughput_constraint(A_2020_summer_night_E_BATT_2020)_: ++1 v_flow_out(A_2020_summer_night_ELC_E_BATT_2020_ELC) +-2.19 v_capacity(A_2020_E_BATT_2020) ++0.85 v_flow_in(A_2020_summer_night_ELC_E_BATT_2020_ELC) +<= 0 + +c_u_storage_throughput_constraint(A_2025_winter_day_E_BATT_2020)_: ++1 v_flow_out(A_2025_winter_day_ELC_E_BATT_2020_ELC) +-2.19 v_capacity(A_2025_E_BATT_2020) ++0.85 v_flow_in(A_2025_winter_day_ELC_E_BATT_2020_ELC) +<= 0 + +c_u_storage_throughput_constraint(A_2025_summer_night_E_BATT_2025)_: ++1 v_flow_out(A_2025_summer_night_ELC_E_BATT_2025_ELC) +-2.19 v_capacity(A_2025_E_BATT_2025) ++0.85 v_flow_in(A_2025_summer_night_ELC_E_BATT_2025_ELC) +<= 0 + +c_u_storage_throughput_constraint(A_2025_summer_day_E_BATT_2025)_: ++1 v_flow_out(A_2025_summer_day_ELC_E_BATT_2025_ELC) +-2.19 v_capacity(A_2025_E_BATT_2025) ++0.85 v_flow_in(A_2025_summer_day_ELC_E_BATT_2025_ELC) +<= 0 + +c_u_storage_throughput_constraint(A_2025_winter_night_E_BATT_2020)_: ++1 v_flow_out(A_2025_winter_night_ELC_E_BATT_2020_ELC) +-2.19 v_capacity(A_2025_E_BATT_2020) ++0.85 v_flow_in(A_2025_winter_night_ELC_E_BATT_2020_ELC) +<= 0 + +c_u_storage_throughput_constraint(A_2025_winter_day_E_BATT_2025)_: ++1 v_flow_out(A_2025_winter_day_ELC_E_BATT_2025_ELC) +-2.19 v_capacity(A_2025_E_BATT_2025) ++0.85 v_flow_in(A_2025_winter_day_ELC_E_BATT_2025_ELC) +<= 0 + +c_u_storage_throughput_constraint(A_2020_winter_day_E_BATT_2020)_: ++1 v_flow_out(A_2020_winter_day_ELC_E_BATT_2020_ELC) +-2.19 v_capacity(A_2020_E_BATT_2020) ++0.85 v_flow_in(A_2020_winter_day_ELC_E_BATT_2020_ELC) +<= 0 + +c_u_storage_throughput_constraint(A_2025_summer_night_E_BATT_2020)_: ++1 v_flow_out(A_2025_summer_night_ELC_E_BATT_2020_ELC) +-2.19 v_capacity(A_2025_E_BATT_2020) ++0.85 v_flow_in(A_2025_summer_night_ELC_E_BATT_2020_ELC) +<= 0 + +c_u_storage_throughput_constraint(A_2025_summer_day_E_BATT_2020)_: ++1 v_flow_out(A_2025_summer_day_ELC_E_BATT_2020_ELC) +-2.19 v_capacity(A_2025_E_BATT_2020) ++0.85 v_flow_in(A_2025_summer_day_ELC_E_BATT_2020_ELC) +<= 0 + +c_u_storage_throughput_constraint(A_2020_summer_day_E_BATT_2020)_: ++1 v_flow_out(A_2020_summer_day_ELC_E_BATT_2020_ELC) +-2.19 v_capacity(A_2020_E_BATT_2020) ++0.85 v_flow_in(A_2020_summer_day_ELC_E_BATT_2020_ELC) +<= 0 + +c_u_storage_throughput_constraint(A_2025_winter_night_E_BATT_2025)_: ++1 v_flow_out(A_2025_winter_night_ELC_E_BATT_2025_ELC) +-2.19 v_capacity(A_2025_E_BATT_2025) ++0.85 v_flow_in(A_2025_winter_night_ELC_E_BATT_2025_ELC) +<= 0 + +c_u_operating_reserve_margin_constraint(A_B_2025_summer_night_elec_AB)_: ++1.08 v_flow_out(A_2025_summer_night_ethos_SOLPV_2015_ELC) ++1.08 v_flow_out(B_2025_summer_night_NG_NGCC_2015_ELC) ++1.08 v_flow_out(C_A_2025_summer_night_ELC_E_TRANS_2015_ELC) +-1.1368421052631579 v_flow_out(B_C_2025_summer_night_ELC_E_TRANS_2015_ELC) ++1.08 v_flow_out(A_2025_summer_night_ELC_HEAT_SYS_2020_HEAT) ++1.08 v_flow_out(B_2025_summer_night_ELC_HEAT_SYS_2020_HEAT) +-1.1368421052631579 v_flow_out(A_C_2025_summer_night_ELC_E_TRANS_2015_ELC) ++1.08 v_flow_out(A_2025_summer_night_NG_NGCC_2020_ELC) ++1.08 v_flow_out(A_2025_summer_night_ethos_SOLPV_2020_ELC) ++1.08 v_flow_out(B_2025_summer_night_NG_NGCC_2020_ELC) ++0.18000000000000005 v_flow_out(A_2025_summer_night_ELC_E_BATT_2020_ELC) ++1.08 v_flow_out(B_2025_summer_night_ELC_HEAT_SYS_2025_HEAT) ++0.18000000000000005 v_flow_out(A_2025_summer_night_ELC_E_BATT_2025_ELC) ++1.08 v_flow_out(A_2025_summer_night_ELC_HEAT_SYS_2025_HEAT) ++1.08 v_flow_out(C_B_2025_summer_night_ELC_E_TRANS_2015_ELC) ++1.08 v_flow_out(A_2025_summer_night_NG_NGCC_2025_ELC) ++1.08 v_flow_out(A_2025_summer_night_ethos_SOLPV_2025_ELC) ++1.08 v_flow_out(A_2025_summer_night_NG_NGCC_2015_ELC) ++1.08 v_flow_out(B_2025_summer_night_NG_NGCC_2025_ELC) +-2.0805 v_capacity(A_2025_NGCC_2015) +-2.0805 v_capacity(B_2025_NGCC_2025) +-2.19 v_capacity(C_B_2025_E_TRANS_2015) +-2.19 v_capacity(C_A_2025_E_TRANS_2015) +-2.0805 v_capacity(A_2025_NGCC_2020) +-0.25 v_capacity(A_2025_HEAT_SYS_2020) +-2.0805 v_capacity(B_2025_NGCC_2015) +-0.25 v_capacity(B_2025_HEAT_SYS_2020) ++2.19 v_capacity(A_C_2025_E_TRANS_2015) ++2.19 v_capacity(B_C_2025_E_TRANS_2015) +-2.0805 v_capacity(A_2025_NGCC_2025) +-0.25 v_capacity(A_2025_HEAT_SYS_2025) +-2.0805 v_capacity(B_2025_NGCC_2020) +-0.25 v_capacity(B_2025_HEAT_SYS_2025) +-0.18000000000000005 v_flow_in(A_2025_summer_night_ELC_E_BATT_2020_ELC) +-0.18000000000000005 v_flow_in(A_2025_summer_night_ELC_E_BATT_2025_ELC) +<= 0 + +c_u_operating_reserve_margin_constraint(A_2025_summer_day_elec_A)_: +-1.1578947368421053 v_flow_out(A_B_2025_summer_day_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(A_2025_summer_day_NG_NGCC_2020_ELC) ++1.1 v_flow_out(A_2025_summer_day_ethos_SOLPV_2020_ELC) ++1.1 v_flow_out(B_A_2025_summer_day_ELC_E_TRANS_2015_ELC) ++0.20000000000000007 v_flow_out(A_2025_summer_day_ELC_E_BATT_2025_ELC) ++1.1 v_flow_out(A_2025_summer_day_ELC_HEAT_SYS_2025_HEAT) ++1.1 v_flow_out(A_2025_summer_day_NG_NGCC_2025_ELC) ++1.1 v_flow_out(A_2025_summer_day_ethos_SOLPV_2025_ELC) ++1.1 v_flow_out(C_A_2025_summer_day_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(A_2025_summer_day_NG_NGCC_2015_ELC) ++1.1 v_flow_out(A_2025_summer_day_ethos_SOLPV_2015_ELC) ++0.20000000000000007 v_flow_out(A_2025_summer_day_ELC_E_BATT_2020_ELC) ++1.1 v_flow_out(A_2025_summer_day_ELC_HEAT_SYS_2020_HEAT) +-1.1578947368421053 v_flow_out(A_C_2025_summer_day_ELC_E_TRANS_2015_ELC) +-1.0512 v_capacity(A_2025_SOLPV_2015) +-2.0805 v_capacity(A_2025_NGCC_2015) ++2.19 v_capacity(A_B_2025_E_TRANS_2015) +-1.0512 v_capacity(A_2025_SOLPV_2020) +-2.19 v_capacity(C_A_2025_E_TRANS_2015) +-2.0805 v_capacity(A_2025_NGCC_2020) +-0.25 v_capacity(A_2025_HEAT_SYS_2020) +-2.19 v_capacity(B_A_2025_E_TRANS_2015) ++2.19 v_capacity(A_C_2025_E_TRANS_2015) +-1.0512 v_capacity(A_2025_SOLPV_2025) +-2.0805 v_capacity(A_2025_NGCC_2025) +-0.25 v_capacity(A_2025_HEAT_SYS_2025) +-0.20000000000000007 v_flow_in(A_2025_summer_day_ELC_E_BATT_2025_ELC) +-0.20000000000000007 v_flow_in(A_2025_summer_day_ELC_E_BATT_2020_ELC) +<= 0 + +c_u_operating_reserve_margin_constraint(B_2020_summer_day_NGCC)_: ++1.1 v_flow_out(B_2020_summer_day_NG_NGCC_2020_ELC) ++1.1 v_flow_out(B_2020_summer_day_NG_NGCC_2015_ELC) +-2.0805 v_capacity(B_2020_NGCC_2015) +-2.0805 v_capacity(B_2020_NGCC_2020) +<= 0 + +c_u_operating_reserve_margin_constraint(A_2020_winter_night_elec_A)_: ++1.1 v_flow_out(B_A_2020_winter_night_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(A_2020_winter_night_NG_NGCC_2020_ELC) ++1.1 v_flow_out(A_2020_winter_night_ethos_SOLPV_2015_ELC) ++0.20000000000000007 v_flow_out(A_2020_winter_night_ELC_E_BATT_2020_ELC) +-1.1578947368421053 v_flow_out(A_C_2020_winter_night_ELC_E_TRANS_2015_ELC) +-1.1578947368421053 v_flow_out(A_B_2020_winter_night_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(A_2020_winter_night_NG_NGCC_2015_ELC) ++1.1 v_flow_out(C_A_2020_winter_night_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(A_2020_winter_night_ethos_SOLPV_2020_ELC) +-0.25 v_capacity(A_2020_HEAT_SYS_2020) +-1.971 v_capacity(A_2020_NGCC_2020) ++2.19 v_capacity(A_B_2020_E_TRANS_2015) ++2.19 v_capacity(A_C_2020_E_TRANS_2015) +-1.971 v_capacity(A_2020_NGCC_2015) +-2.19 v_capacity(B_A_2020_E_TRANS_2015) +-2.19 v_capacity(C_A_2020_E_TRANS_2015) +-0.20000000000000007 v_flow_in(A_2020_winter_night_ELC_E_BATT_2020_ELC) +<= -1.375 + +c_u_operating_reserve_margin_constraint(B_2020_winter_day_NGCC)_: ++1.1 v_flow_out(B_2020_winter_day_NG_NGCC_2020_ELC) ++1.1 v_flow_out(B_2020_winter_day_NG_NGCC_2015_ELC) +-1.971 v_capacity(B_2020_NGCC_2015) +-1.971 v_capacity(B_2020_NGCC_2020) +<= 0 + +c_u_operating_reserve_margin_constraint(A_B_2025_winter_day_elec_AB)_: ++0.18000000000000005 v_flow_out(A_2025_winter_day_ELC_E_BATT_2020_ELC) ++1.08 v_flow_out(A_2025_winter_day_ELC_HEAT_SYS_2020_HEAT) ++1.08 v_flow_out(B_2025_winter_day_NG_NGCC_2025_ELC) +-1.1368421052631579 v_flow_out(A_C_2025_winter_day_ELC_E_TRANS_2015_ELC) ++1.08 v_flow_out(C_A_2025_winter_day_ELC_E_TRANS_2015_ELC) +-1.1368421052631579 v_flow_out(B_C_2025_winter_day_ELC_E_TRANS_2015_ELC) ++1.08 v_flow_out(A_2025_winter_day_NG_NGCC_2020_ELC) ++1.08 v_flow_out(A_2025_winter_day_ethos_SOLPV_2015_ELC) ++1.08 v_flow_out(C_B_2025_winter_day_ELC_E_TRANS_2015_ELC) ++1.08 v_flow_out(B_2025_winter_day_ELC_HEAT_SYS_2020_HEAT) ++0.18000000000000005 v_flow_out(A_2025_winter_day_ELC_E_BATT_2025_ELC) ++1.08 v_flow_out(A_2025_winter_day_ELC_HEAT_SYS_2025_HEAT) ++1.08 v_flow_out(B_2025_winter_day_NG_NGCC_2015_ELC) ++1.08 v_flow_out(A_2025_winter_day_ethos_SOLPV_2020_ELC) ++1.08 v_flow_out(A_2025_winter_day_NG_NGCC_2025_ELC) ++1.08 v_flow_out(A_2025_winter_day_ethos_SOLPV_2025_ELC) ++1.08 v_flow_out(B_2025_winter_day_ELC_HEAT_SYS_2025_HEAT) ++1.08 v_flow_out(A_2025_winter_day_NG_NGCC_2015_ELC) ++1.08 v_flow_out(B_2025_winter_day_NG_NGCC_2020_ELC) +-0.32849999999999996 v_capacity(A_2025_SOLPV_2015) +-1.971 v_capacity(A_2025_NGCC_2015) +-1.971 v_capacity(B_2025_NGCC_2025) +-2.19 v_capacity(C_B_2025_E_TRANS_2015) +-0.32849999999999996 v_capacity(A_2025_SOLPV_2020) +-2.19 v_capacity(C_A_2025_E_TRANS_2015) +-1.971 v_capacity(A_2025_NGCC_2020) +-0.25 v_capacity(A_2025_HEAT_SYS_2020) +-1.971 v_capacity(B_2025_NGCC_2015) +-0.25 v_capacity(B_2025_HEAT_SYS_2020) ++2.19 v_capacity(A_C_2025_E_TRANS_2015) +-0.32849999999999996 v_capacity(A_2025_SOLPV_2025) ++2.19 v_capacity(B_C_2025_E_TRANS_2015) +-1.971 v_capacity(A_2025_NGCC_2025) +-0.25 v_capacity(A_2025_HEAT_SYS_2025) +-1.971 v_capacity(B_2025_NGCC_2020) +-0.25 v_capacity(B_2025_HEAT_SYS_2025) +-0.18000000000000005 v_flow_in(A_2025_winter_day_ELC_E_BATT_2020_ELC) +-0.18000000000000005 v_flow_in(A_2025_winter_day_ELC_E_BATT_2025_ELC) +<= 0 + +c_u_operating_reserve_margin_constraint(A_2025_summer_night_elec_A)_: ++1.1 v_flow_out(A_2025_summer_night_ethos_SOLPV_2015_ELC) ++1.1 v_flow_out(C_A_2025_summer_night_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(A_2025_summer_night_ELC_HEAT_SYS_2020_HEAT) +-1.1578947368421053 v_flow_out(A_C_2025_summer_night_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(B_A_2025_summer_night_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(A_2025_summer_night_NG_NGCC_2020_ELC) ++1.1 v_flow_out(A_2025_summer_night_ethos_SOLPV_2020_ELC) ++0.20000000000000007 v_flow_out(A_2025_summer_night_ELC_E_BATT_2020_ELC) ++0.20000000000000007 v_flow_out(A_2025_summer_night_ELC_E_BATT_2025_ELC) ++1.1 v_flow_out(A_2025_summer_night_ELC_HEAT_SYS_2025_HEAT) +-1.1578947368421053 v_flow_out(A_B_2025_summer_night_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(A_2025_summer_night_NG_NGCC_2025_ELC) ++1.1 v_flow_out(A_2025_summer_night_ethos_SOLPV_2025_ELC) ++1.1 v_flow_out(A_2025_summer_night_NG_NGCC_2015_ELC) +-2.0805 v_capacity(A_2025_NGCC_2015) ++2.19 v_capacity(A_B_2025_E_TRANS_2015) +-2.19 v_capacity(C_A_2025_E_TRANS_2015) +-2.0805 v_capacity(A_2025_NGCC_2020) +-0.25 v_capacity(A_2025_HEAT_SYS_2020) +-2.19 v_capacity(B_A_2025_E_TRANS_2015) ++2.19 v_capacity(A_C_2025_E_TRANS_2015) +-2.0805 v_capacity(A_2025_NGCC_2025) +-0.25 v_capacity(A_2025_HEAT_SYS_2025) +-0.20000000000000007 v_flow_in(A_2025_summer_night_ELC_E_BATT_2020_ELC) +-0.20000000000000007 v_flow_in(A_2025_summer_night_ELC_E_BATT_2025_ELC) +<= 0 + +c_u_operating_reserve_margin_constraint(A_2025_winter_day_elec_A)_: ++1.1 v_flow_out(B_A_2025_winter_day_ELC_E_TRANS_2015_ELC) ++0.20000000000000007 v_flow_out(A_2025_winter_day_ELC_E_BATT_2020_ELC) ++1.1 v_flow_out(A_2025_winter_day_ELC_HEAT_SYS_2020_HEAT) +-1.1578947368421053 v_flow_out(A_C_2025_winter_day_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(C_A_2025_winter_day_ELC_E_TRANS_2015_ELC) +-1.1578947368421053 v_flow_out(A_B_2025_winter_day_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(A_2025_winter_day_NG_NGCC_2020_ELC) ++1.1 v_flow_out(A_2025_winter_day_ethos_SOLPV_2015_ELC) ++0.20000000000000007 v_flow_out(A_2025_winter_day_ELC_E_BATT_2025_ELC) ++1.1 v_flow_out(A_2025_winter_day_ELC_HEAT_SYS_2025_HEAT) ++1.1 v_flow_out(A_2025_winter_day_ethos_SOLPV_2020_ELC) ++1.1 v_flow_out(A_2025_winter_day_NG_NGCC_2025_ELC) ++1.1 v_flow_out(A_2025_winter_day_ethos_SOLPV_2025_ELC) ++1.1 v_flow_out(A_2025_winter_day_NG_NGCC_2015_ELC) +-0.32849999999999996 v_capacity(A_2025_SOLPV_2015) +-1.971 v_capacity(A_2025_NGCC_2015) ++2.19 v_capacity(A_B_2025_E_TRANS_2015) +-0.32849999999999996 v_capacity(A_2025_SOLPV_2020) +-2.19 v_capacity(C_A_2025_E_TRANS_2015) +-1.971 v_capacity(A_2025_NGCC_2020) +-0.25 v_capacity(A_2025_HEAT_SYS_2020) +-2.19 v_capacity(B_A_2025_E_TRANS_2015) ++2.19 v_capacity(A_C_2025_E_TRANS_2015) +-0.32849999999999996 v_capacity(A_2025_SOLPV_2025) +-1.971 v_capacity(A_2025_NGCC_2025) +-0.25 v_capacity(A_2025_HEAT_SYS_2025) +-0.20000000000000007 v_flow_in(A_2025_winter_day_ELC_E_BATT_2020_ELC) +-0.20000000000000007 v_flow_in(A_2025_winter_day_ELC_E_BATT_2025_ELC) +<= 0 + +c_u_operating_reserve_margin_constraint(B_2025_winter_day_NGCC)_: ++1.1 v_flow_out(B_2025_winter_day_NG_NGCC_2025_ELC) ++1.1 v_flow_out(B_2025_winter_day_NG_NGCC_2015_ELC) ++1.1 v_flow_out(B_2025_winter_day_NG_NGCC_2020_ELC) +-1.971 v_capacity(B_2025_NGCC_2025) +-1.971 v_capacity(B_2025_NGCC_2015) +-1.971 v_capacity(B_2025_NGCC_2020) +<= 0 + +c_u_operating_reserve_margin_constraint(A_B_2020_winter_day_elec_AB)_: ++0.18000000000000005 v_flow_out(A_2020_winter_day_ELC_E_BATT_2020_ELC) ++1.08 v_flow_out(B_2020_winter_day_NG_NGCC_2020_ELC) +-1.1368421052631579 v_flow_out(A_C_2020_winter_day_ELC_E_TRANS_2015_ELC) ++1.08 v_flow_out(A_2020_winter_day_NG_NGCC_2015_ELC) ++1.08 v_flow_out(A_2020_winter_day_ethos_SOLPV_2015_ELC) ++1.08 v_flow_out(C_A_2020_winter_day_ELC_E_TRANS_2015_ELC) +-1.1368421052631579 v_flow_out(B_C_2020_winter_day_ELC_E_TRANS_2015_ELC) ++1.08 v_flow_out(B_2020_winter_day_NG_NGCC_2015_ELC) ++1.08 v_flow_out(C_B_2020_winter_day_ELC_E_TRANS_2015_ELC) ++1.08 v_flow_out(A_2020_winter_day_NG_NGCC_2020_ELC) ++1.08 v_flow_out(A_2020_winter_day_ethos_SOLPV_2020_ELC) +-0.25 v_capacity(A_2020_HEAT_SYS_2020) +-1.971 v_capacity(A_2020_NGCC_2020) ++2.19 v_capacity(B_C_2020_E_TRANS_2015) +-0.32849999999999996 v_capacity(A_2020_SOLPV_2015) +-1.971 v_capacity(B_2020_NGCC_2015) ++2.19 v_capacity(A_C_2020_E_TRANS_2015) +-0.25 v_capacity(B_2020_HEAT_SYS_2020) +-0.32849999999999996 v_capacity(A_2020_SOLPV_2020) +-1.971 v_capacity(A_2020_NGCC_2015) +-1.971 v_capacity(B_2020_NGCC_2020) +-2.19 v_capacity(C_B_2020_E_TRANS_2015) +-2.19 v_capacity(C_A_2020_E_TRANS_2015) +-0.18000000000000005 v_flow_in(A_2020_winter_day_ELC_E_BATT_2020_ELC) +<= -2.16 + +c_u_operating_reserve_margin_constraint(A_B_2025_summer_day_elec_AB)_: ++1.08 v_flow_out(A_2025_summer_day_NG_NGCC_2020_ELC) ++1.08 v_flow_out(A_2025_summer_day_ethos_SOLPV_2020_ELC) ++1.08 v_flow_out(B_2025_summer_day_ELC_HEAT_SYS_2020_HEAT) ++0.18000000000000005 v_flow_out(A_2025_summer_day_ELC_E_BATT_2025_ELC) ++1.08 v_flow_out(A_2025_summer_day_ELC_HEAT_SYS_2025_HEAT) ++1.08 v_flow_out(B_2025_summer_day_NG_NGCC_2020_ELC) +-1.1368421052631579 v_flow_out(B_C_2025_summer_day_ELC_E_TRANS_2015_ELC) ++1.08 v_flow_out(A_2025_summer_day_NG_NGCC_2025_ELC) ++1.08 v_flow_out(A_2025_summer_day_ethos_SOLPV_2025_ELC) ++1.08 v_flow_out(C_B_2025_summer_day_ELC_E_TRANS_2015_ELC) ++1.08 v_flow_out(C_A_2025_summer_day_ELC_E_TRANS_2015_ELC) ++1.08 v_flow_out(B_2025_summer_day_ELC_HEAT_SYS_2025_HEAT) ++1.08 v_flow_out(A_2025_summer_day_NG_NGCC_2015_ELC) ++1.08 v_flow_out(A_2025_summer_day_ethos_SOLPV_2015_ELC) ++1.08 v_flow_out(B_2025_summer_day_NG_NGCC_2025_ELC) ++0.18000000000000005 v_flow_out(A_2025_summer_day_ELC_E_BATT_2020_ELC) ++1.08 v_flow_out(A_2025_summer_day_ELC_HEAT_SYS_2020_HEAT) +-1.1368421052631579 v_flow_out(A_C_2025_summer_day_ELC_E_TRANS_2015_ELC) ++1.08 v_flow_out(B_2025_summer_day_NG_NGCC_2015_ELC) +-1.0512 v_capacity(A_2025_SOLPV_2015) +-2.0805 v_capacity(A_2025_NGCC_2015) +-2.0805 v_capacity(B_2025_NGCC_2025) +-2.19 v_capacity(C_B_2025_E_TRANS_2015) +-1.0512 v_capacity(A_2025_SOLPV_2020) +-2.19 v_capacity(C_A_2025_E_TRANS_2015) +-2.0805 v_capacity(A_2025_NGCC_2020) +-0.25 v_capacity(A_2025_HEAT_SYS_2020) +-2.0805 v_capacity(B_2025_NGCC_2015) +-0.25 v_capacity(B_2025_HEAT_SYS_2020) ++2.19 v_capacity(A_C_2025_E_TRANS_2015) +-1.0512 v_capacity(A_2025_SOLPV_2025) ++2.19 v_capacity(B_C_2025_E_TRANS_2015) +-2.0805 v_capacity(A_2025_NGCC_2025) +-0.25 v_capacity(A_2025_HEAT_SYS_2025) +-2.0805 v_capacity(B_2025_NGCC_2020) +-0.25 v_capacity(B_2025_HEAT_SYS_2025) +-0.18000000000000005 v_flow_in(A_2025_summer_day_ELC_E_BATT_2025_ELC) +-0.18000000000000005 v_flow_in(A_2025_summer_day_ELC_E_BATT_2020_ELC) +<= 0 + +c_u_operating_reserve_margin_constraint(B_2025_winter_night_NGCC)_: ++1.1 v_flow_out(B_2025_winter_night_NG_NGCC_2015_ELC) ++1.1 v_flow_out(B_2025_winter_night_NG_NGCC_2020_ELC) ++1.1 v_flow_out(B_2025_winter_night_NG_NGCC_2025_ELC) +-1.971 v_capacity(B_2025_NGCC_2025) +-1.971 v_capacity(B_2025_NGCC_2015) +-1.971 v_capacity(B_2025_NGCC_2020) +<= 0 + +c_u_operating_reserve_margin_constraint(B_2020_summer_night_NGCC)_: ++1.1 v_flow_out(B_2020_summer_night_NG_NGCC_2015_ELC) ++1.1 v_flow_out(B_2020_summer_night_NG_NGCC_2020_ELC) +-2.0805 v_capacity(B_2020_NGCC_2015) +-2.0805 v_capacity(B_2020_NGCC_2020) +<= 0 + +c_u_operating_reserve_margin_constraint(B_2020_winter_night_NGCC)_: ++1.1 v_flow_out(B_2020_winter_night_NG_NGCC_2015_ELC) ++1.1 v_flow_out(B_2020_winter_night_NG_NGCC_2020_ELC) +-1.971 v_capacity(B_2020_NGCC_2015) +-1.971 v_capacity(B_2020_NGCC_2020) +<= 0 + +c_u_operating_reserve_margin_constraint(A_B_2020_summer_night_elec_AB)_: ++0.18000000000000005 v_flow_out(A_2020_summer_night_ELC_E_BATT_2020_ELC) ++1.08 v_flow_out(C_A_2020_summer_night_ELC_E_TRANS_2015_ELC) ++1.08 v_flow_out(B_2020_summer_night_NG_NGCC_2015_ELC) ++1.08 v_flow_out(C_B_2020_summer_night_ELC_E_TRANS_2015_ELC) ++1.08 v_flow_out(A_2020_summer_night_NG_NGCC_2015_ELC) ++1.08 v_flow_out(A_2020_summer_night_ethos_SOLPV_2015_ELC) ++1.08 v_flow_out(A_2020_summer_night_NG_NGCC_2020_ELC) ++1.08 v_flow_out(B_2020_summer_night_NG_NGCC_2020_ELC) +-1.1368421052631579 v_flow_out(A_C_2020_summer_night_ELC_E_TRANS_2015_ELC) +-1.1368421052631579 v_flow_out(B_C_2020_summer_night_ELC_E_TRANS_2015_ELC) ++1.08 v_flow_out(A_2020_summer_night_ethos_SOLPV_2020_ELC) +-0.25 v_capacity(A_2020_HEAT_SYS_2020) +-2.0805 v_capacity(A_2020_NGCC_2020) ++2.19 v_capacity(B_C_2020_E_TRANS_2015) +-2.0805 v_capacity(B_2020_NGCC_2015) ++2.19 v_capacity(A_C_2020_E_TRANS_2015) +-0.25 v_capacity(B_2020_HEAT_SYS_2020) +-2.0805 v_capacity(A_2020_NGCC_2015) +-2.0805 v_capacity(B_2020_NGCC_2020) +-2.19 v_capacity(C_B_2020_E_TRANS_2015) +-2.19 v_capacity(C_A_2020_E_TRANS_2015) +-0.18000000000000005 v_flow_in(A_2020_summer_night_ELC_E_BATT_2020_ELC) +<= -2.16 + +c_u_operating_reserve_margin_constraint(B_2025_summer_day_NGCC)_: ++1.1 v_flow_out(B_2025_summer_day_NG_NGCC_2020_ELC) ++1.1 v_flow_out(B_2025_summer_day_NG_NGCC_2025_ELC) ++1.1 v_flow_out(B_2025_summer_day_NG_NGCC_2015_ELC) +-2.0805 v_capacity(B_2025_NGCC_2025) +-2.0805 v_capacity(B_2025_NGCC_2015) +-2.0805 v_capacity(B_2025_NGCC_2020) +<= 0 + +c_u_operating_reserve_margin_constraint(A_2020_summer_day_elec_A)_: +-1.1578947368421053 v_flow_out(A_C_2020_summer_day_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(A_2020_summer_day_NG_NGCC_2020_ELC) ++1.1 v_flow_out(A_2020_summer_day_ethos_SOLPV_2020_ELC) ++0.20000000000000007 v_flow_out(A_2020_summer_day_ELC_E_BATT_2020_ELC) ++1.1 v_flow_out(A_2020_summer_day_NG_NGCC_2015_ELC) ++1.1 v_flow_out(B_A_2020_summer_day_ELC_E_TRANS_2015_ELC) +-1.1578947368421053 v_flow_out(A_B_2020_summer_day_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(C_A_2020_summer_day_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(A_2020_summer_day_ethos_SOLPV_2015_ELC) +-0.25 v_capacity(A_2020_HEAT_SYS_2020) +-2.0805 v_capacity(A_2020_NGCC_2020) +-1.0512 v_capacity(A_2020_SOLPV_2015) ++2.19 v_capacity(A_B_2020_E_TRANS_2015) ++2.19 v_capacity(A_C_2020_E_TRANS_2015) +-1.0512 v_capacity(A_2020_SOLPV_2020) +-2.0805 v_capacity(A_2020_NGCC_2015) +-2.19 v_capacity(B_A_2020_E_TRANS_2015) +-2.19 v_capacity(C_A_2020_E_TRANS_2015) +-0.20000000000000007 v_flow_in(A_2020_summer_day_ELC_E_BATT_2020_ELC) +<= -1.375 + +c_u_operating_reserve_margin_constraint(A_B_2020_winter_night_elec_AB)_: ++1.08 v_flow_out(B_2020_winter_night_NG_NGCC_2015_ELC) ++1.08 v_flow_out(C_B_2020_winter_night_ELC_E_TRANS_2015_ELC) ++1.08 v_flow_out(A_2020_winter_night_NG_NGCC_2020_ELC) +-1.1368421052631579 v_flow_out(B_C_2020_winter_night_ELC_E_TRANS_2015_ELC) ++1.08 v_flow_out(A_2020_winter_night_ethos_SOLPV_2015_ELC) ++0.18000000000000005 v_flow_out(A_2020_winter_night_ELC_E_BATT_2020_ELC) +-1.1368421052631579 v_flow_out(A_C_2020_winter_night_ELC_E_TRANS_2015_ELC) ++1.08 v_flow_out(B_2020_winter_night_NG_NGCC_2020_ELC) ++1.08 v_flow_out(A_2020_winter_night_NG_NGCC_2015_ELC) ++1.08 v_flow_out(C_A_2020_winter_night_ELC_E_TRANS_2015_ELC) ++1.08 v_flow_out(A_2020_winter_night_ethos_SOLPV_2020_ELC) +-0.25 v_capacity(A_2020_HEAT_SYS_2020) +-1.971 v_capacity(A_2020_NGCC_2020) ++2.19 v_capacity(B_C_2020_E_TRANS_2015) +-1.971 v_capacity(B_2020_NGCC_2015) ++2.19 v_capacity(A_C_2020_E_TRANS_2015) +-0.25 v_capacity(B_2020_HEAT_SYS_2020) +-1.971 v_capacity(A_2020_NGCC_2015) +-1.971 v_capacity(B_2020_NGCC_2020) +-2.19 v_capacity(C_B_2020_E_TRANS_2015) +-2.19 v_capacity(C_A_2020_E_TRANS_2015) +-0.18000000000000005 v_flow_in(A_2020_winter_night_ELC_E_BATT_2020_ELC) +<= -2.16 + +c_u_operating_reserve_margin_constraint(A_B_2025_winter_night_elec_AB)_: ++1.08 v_flow_out(A_2025_winter_night_NG_NGCC_2015_ELC) +-1.1368421052631579 v_flow_out(B_C_2025_winter_night_ELC_E_TRANS_2015_ELC) ++1.08 v_flow_out(C_A_2025_winter_night_ELC_E_TRANS_2015_ELC) ++1.08 v_flow_out(B_2025_winter_night_NG_NGCC_2015_ELC) ++1.08 v_flow_out(A_2025_winter_night_ethos_SOLPV_2015_ELC) ++1.08 v_flow_out(C_B_2025_winter_night_ELC_E_TRANS_2015_ELC) ++0.18000000000000005 v_flow_out(A_2025_winter_night_ELC_E_BATT_2020_ELC) ++1.08 v_flow_out(A_2025_winter_night_ELC_HEAT_SYS_2020_HEAT) +-1.1368421052631579 v_flow_out(A_C_2025_winter_night_ELC_E_TRANS_2015_ELC) ++1.08 v_flow_out(B_2025_winter_night_ELC_HEAT_SYS_2020_HEAT) ++1.08 v_flow_out(A_2025_winter_night_NG_NGCC_2020_ELC) ++1.08 v_flow_out(A_2025_winter_night_ethos_SOLPV_2020_ELC) ++1.08 v_flow_out(A_2025_winter_night_NG_NGCC_2025_ELC) ++1.08 v_flow_out(B_2025_winter_night_NG_NGCC_2020_ELC) ++1.08 v_flow_out(B_2025_winter_night_ELC_HEAT_SYS_2025_HEAT) ++0.18000000000000005 v_flow_out(A_2025_winter_night_ELC_E_BATT_2025_ELC) ++1.08 v_flow_out(A_2025_winter_night_ELC_HEAT_SYS_2025_HEAT) ++1.08 v_flow_out(B_2025_winter_night_NG_NGCC_2025_ELC) ++1.08 v_flow_out(A_2025_winter_night_ethos_SOLPV_2025_ELC) +-1.971 v_capacity(A_2025_NGCC_2015) +-1.971 v_capacity(B_2025_NGCC_2025) +-2.19 v_capacity(C_B_2025_E_TRANS_2015) +-2.19 v_capacity(C_A_2025_E_TRANS_2015) +-1.971 v_capacity(A_2025_NGCC_2020) +-0.25 v_capacity(A_2025_HEAT_SYS_2020) +-1.971 v_capacity(B_2025_NGCC_2015) +-0.25 v_capacity(B_2025_HEAT_SYS_2020) ++2.19 v_capacity(A_C_2025_E_TRANS_2015) ++2.19 v_capacity(B_C_2025_E_TRANS_2015) +-1.971 v_capacity(A_2025_NGCC_2025) +-0.25 v_capacity(A_2025_HEAT_SYS_2025) +-1.971 v_capacity(B_2025_NGCC_2020) +-0.25 v_capacity(B_2025_HEAT_SYS_2025) +-0.18000000000000005 v_flow_in(A_2025_winter_night_ELC_E_BATT_2025_ELC) +-0.18000000000000005 v_flow_in(A_2025_winter_night_ELC_E_BATT_2020_ELC) +<= 0 + +c_u_operating_reserve_margin_constraint(A_2025_winter_night_elec_A)_: ++1.1 v_flow_out(A_2025_winter_night_NG_NGCC_2015_ELC) ++1.1 v_flow_out(C_A_2025_winter_night_ELC_E_TRANS_2015_ELC) +-1.1578947368421053 v_flow_out(A_B_2025_winter_night_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(A_2025_winter_night_ethos_SOLPV_2015_ELC) ++0.20000000000000007 v_flow_out(A_2025_winter_night_ELC_E_BATT_2020_ELC) ++1.1 v_flow_out(A_2025_winter_night_ELC_HEAT_SYS_2020_HEAT) +-1.1578947368421053 v_flow_out(A_C_2025_winter_night_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(A_2025_winter_night_NG_NGCC_2020_ELC) ++1.1 v_flow_out(A_2025_winter_night_ethos_SOLPV_2020_ELC) ++1.1 v_flow_out(B_A_2025_winter_night_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(A_2025_winter_night_NG_NGCC_2025_ELC) ++0.20000000000000007 v_flow_out(A_2025_winter_night_ELC_E_BATT_2025_ELC) ++1.1 v_flow_out(A_2025_winter_night_ELC_HEAT_SYS_2025_HEAT) ++1.1 v_flow_out(A_2025_winter_night_ethos_SOLPV_2025_ELC) +-1.971 v_capacity(A_2025_NGCC_2015) ++2.19 v_capacity(A_B_2025_E_TRANS_2015) +-2.19 v_capacity(C_A_2025_E_TRANS_2015) +-1.971 v_capacity(A_2025_NGCC_2020) +-0.25 v_capacity(A_2025_HEAT_SYS_2020) +-2.19 v_capacity(B_A_2025_E_TRANS_2015) ++2.19 v_capacity(A_C_2025_E_TRANS_2015) +-1.971 v_capacity(A_2025_NGCC_2025) +-0.25 v_capacity(A_2025_HEAT_SYS_2025) +-0.20000000000000007 v_flow_in(A_2025_winter_night_ELC_E_BATT_2025_ELC) +-0.20000000000000007 v_flow_in(A_2025_winter_night_ELC_E_BATT_2020_ELC) +<= 0 + +c_u_operating_reserve_margin_constraint(A_2020_winter_day_elec_A)_: ++1.1 v_flow_out(B_A_2020_winter_day_ELC_E_TRANS_2015_ELC) ++0.20000000000000007 v_flow_out(A_2020_winter_day_ELC_E_BATT_2020_ELC) +-1.1578947368421053 v_flow_out(A_C_2020_winter_day_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(A_2020_winter_day_NG_NGCC_2015_ELC) ++1.1 v_flow_out(A_2020_winter_day_ethos_SOLPV_2015_ELC) ++1.1 v_flow_out(C_A_2020_winter_day_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(A_2020_winter_day_NG_NGCC_2020_ELC) +-1.1578947368421053 v_flow_out(A_B_2020_winter_day_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(A_2020_winter_day_ethos_SOLPV_2020_ELC) +-0.25 v_capacity(A_2020_HEAT_SYS_2020) +-1.971 v_capacity(A_2020_NGCC_2020) +-0.32849999999999996 v_capacity(A_2020_SOLPV_2015) ++2.19 v_capacity(A_B_2020_E_TRANS_2015) ++2.19 v_capacity(A_C_2020_E_TRANS_2015) +-0.32849999999999996 v_capacity(A_2020_SOLPV_2020) +-1.971 v_capacity(A_2020_NGCC_2015) +-2.19 v_capacity(B_A_2020_E_TRANS_2015) +-2.19 v_capacity(C_A_2020_E_TRANS_2015) +-0.20000000000000007 v_flow_in(A_2020_winter_day_ELC_E_BATT_2020_ELC) +<= -1.375 + +c_u_operating_reserve_margin_constraint(B_2025_summer_night_NGCC)_: ++1.1 v_flow_out(B_2025_summer_night_NG_NGCC_2015_ELC) ++1.1 v_flow_out(B_2025_summer_night_NG_NGCC_2020_ELC) ++1.1 v_flow_out(B_2025_summer_night_NG_NGCC_2025_ELC) +-2.0805 v_capacity(B_2025_NGCC_2025) +-2.0805 v_capacity(B_2025_NGCC_2015) +-2.0805 v_capacity(B_2025_NGCC_2020) +<= 0 + +c_u_operating_reserve_margin_constraint(A_2020_summer_night_elec_A)_: ++0.20000000000000007 v_flow_out(A_2020_summer_night_ELC_E_BATT_2020_ELC) ++1.1 v_flow_out(C_A_2020_summer_night_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(A_2020_summer_night_NG_NGCC_2015_ELC) ++1.1 v_flow_out(A_2020_summer_night_ethos_SOLPV_2015_ELC) ++1.1 v_flow_out(A_2020_summer_night_NG_NGCC_2020_ELC) +-1.1578947368421053 v_flow_out(A_C_2020_summer_night_ELC_E_TRANS_2015_ELC) +-1.1578947368421053 v_flow_out(A_B_2020_summer_night_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(A_2020_summer_night_ethos_SOLPV_2020_ELC) ++1.1 v_flow_out(B_A_2020_summer_night_ELC_E_TRANS_2015_ELC) +-0.25 v_capacity(A_2020_HEAT_SYS_2020) +-2.0805 v_capacity(A_2020_NGCC_2020) ++2.19 v_capacity(A_B_2020_E_TRANS_2015) ++2.19 v_capacity(A_C_2020_E_TRANS_2015) +-2.0805 v_capacity(A_2020_NGCC_2015) +-2.19 v_capacity(B_A_2020_E_TRANS_2015) +-2.19 v_capacity(C_A_2020_E_TRANS_2015) +-0.20000000000000007 v_flow_in(A_2020_summer_night_ELC_E_BATT_2020_ELC) +<= -1.375 + +c_u_operating_reserve_margin_constraint(A_B_2020_summer_day_elec_AB)_: +-1.1368421052631579 v_flow_out(A_C_2020_summer_day_ELC_E_TRANS_2015_ELC) ++1.08 v_flow_out(A_2020_summer_day_NG_NGCC_2020_ELC) ++1.08 v_flow_out(A_2020_summer_day_ethos_SOLPV_2020_ELC) ++1.08 v_flow_out(B_2020_summer_day_NG_NGCC_2020_ELC) ++0.18000000000000005 v_flow_out(A_2020_summer_day_ELC_E_BATT_2020_ELC) ++1.08 v_flow_out(C_B_2020_summer_day_ELC_E_TRANS_2015_ELC) ++1.08 v_flow_out(A_2020_summer_day_NG_NGCC_2015_ELC) ++1.08 v_flow_out(B_2020_summer_day_NG_NGCC_2015_ELC) +-1.1368421052631579 v_flow_out(B_C_2020_summer_day_ELC_E_TRANS_2015_ELC) ++1.08 v_flow_out(C_A_2020_summer_day_ELC_E_TRANS_2015_ELC) ++1.08 v_flow_out(A_2020_summer_day_ethos_SOLPV_2015_ELC) +-0.25 v_capacity(A_2020_HEAT_SYS_2020) +-2.0805 v_capacity(A_2020_NGCC_2020) ++2.19 v_capacity(B_C_2020_E_TRANS_2015) +-1.0512 v_capacity(A_2020_SOLPV_2015) +-2.0805 v_capacity(B_2020_NGCC_2015) ++2.19 v_capacity(A_C_2020_E_TRANS_2015) +-0.25 v_capacity(B_2020_HEAT_SYS_2020) +-1.0512 v_capacity(A_2020_SOLPV_2020) +-2.0805 v_capacity(A_2020_NGCC_2015) +-2.0805 v_capacity(B_2020_NGCC_2020) +-2.19 v_capacity(C_B_2020_E_TRANS_2015) +-2.19 v_capacity(C_A_2020_E_TRANS_2015) +-0.18000000000000005 v_flow_in(A_2020_summer_day_ELC_E_BATT_2020_ELC) +<= -2.16 + +c_u_planning_reserve_margin_constraint(A_B_2025_summer_night_elec_AB)_: ++1.1 v_flow_out(A_2025_summer_night_ethos_SOLPV_2015_ELC) ++1.1 v_flow_out(B_2025_summer_night_NG_NGCC_2015_ELC) ++1.1 v_flow_out(C_A_2025_summer_night_ELC_E_TRANS_2015_ELC) +-1.1578947368421053 v_flow_out(B_C_2025_summer_night_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(A_2025_summer_night_ELC_HEAT_SYS_2020_HEAT) ++1.1 v_flow_out(B_2025_summer_night_ELC_HEAT_SYS_2020_HEAT) +-1.1578947368421053 v_flow_out(A_C_2025_summer_night_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(A_2025_summer_night_NG_NGCC_2020_ELC) ++1.1 v_flow_out(A_2025_summer_night_ethos_SOLPV_2020_ELC) ++1.1 v_flow_out(B_2025_summer_night_NG_NGCC_2020_ELC) ++1.1 v_flow_out(A_2025_summer_night_ELC_E_BATT_2020_ELC) ++1.1 v_flow_out(B_2025_summer_night_ELC_HEAT_SYS_2025_HEAT) ++1.1 v_flow_out(A_2025_summer_night_ELC_E_BATT_2025_ELC) ++1.1 v_flow_out(A_2025_summer_night_ELC_HEAT_SYS_2025_HEAT) ++1.1 v_flow_out(C_B_2025_summer_night_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(A_2025_summer_night_NG_NGCC_2025_ELC) ++1.1 v_flow_out(A_2025_summer_night_ethos_SOLPV_2025_ELC) ++1.1 v_flow_out(A_2025_summer_night_NG_NGCC_2015_ELC) ++1.1 v_flow_out(B_2025_summer_night_NG_NGCC_2025_ELC) +-0.32849999999999996 v_capacity(A_2025_SOLPV_2015) +-1.971 v_capacity(A_2025_E_BATT_2020) +-2.19 v_capacity(A_2025_NGCC_2015) +-2.19 v_capacity(B_2025_NGCC_2025) +-2.19 v_capacity(C_B_2025_E_TRANS_2015) +-0.32849999999999996 v_capacity(A_2025_SOLPV_2020) +-2.19 v_capacity(C_A_2025_E_TRANS_2015) +-2.19 v_capacity(A_2025_NGCC_2020) +-1.971 v_capacity(A_2025_E_BATT_2025) +-2.19 v_capacity(B_2025_NGCC_2015) ++2.19 v_capacity(A_C_2025_E_TRANS_2015) +-0.32849999999999996 v_capacity(A_2025_SOLPV_2025) ++2.19 v_capacity(B_C_2025_E_TRANS_2015) +-2.19 v_capacity(A_2025_NGCC_2025) +-2.19 v_capacity(B_2025_NGCC_2020) +-1.1 v_flow_in(A_2025_summer_night_ELC_E_BATT_2020_ELC) +-1.1 v_flow_in(A_2025_summer_night_ELC_E_BATT_2025_ELC) +<= 0 + +c_u_planning_reserve_margin_constraint(A_2020_summer_day_NGCC)_: ++1.15 v_flow_out(A_2020_summer_day_NG_NGCC_2020_ELC) ++1.15 v_flow_out(A_2020_summer_day_NG_NGCC_2015_ELC) +-2.19 v_capacity(A_2020_NGCC_2020) +-2.19 v_capacity(A_2020_NGCC_2015) +<= 0 + +c_u_planning_reserve_margin_constraint(C_2020_summer_day_elec_C_ann)_: +-0.25 v_capacity(C_2020_ANN_IMP_2020) ++0.3 v_flow_out_annual(C_2020_ethos_ANN_IMP_2020_ELC_C) +<= -0.3 + +c_u_planning_reserve_margin_constraint(A_B_2025_winter_day_elec_AB)_: ++1.1 v_flow_out(A_2025_winter_day_ELC_E_BATT_2020_ELC) ++1.1 v_flow_out(A_2025_winter_day_ELC_HEAT_SYS_2020_HEAT) ++1.1 v_flow_out(B_2025_winter_day_NG_NGCC_2025_ELC) +-1.1578947368421053 v_flow_out(A_C_2025_winter_day_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(C_A_2025_winter_day_ELC_E_TRANS_2015_ELC) +-1.1578947368421053 v_flow_out(B_C_2025_winter_day_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(A_2025_winter_day_NG_NGCC_2020_ELC) ++1.1 v_flow_out(A_2025_winter_day_ethos_SOLPV_2015_ELC) ++1.1 v_flow_out(C_B_2025_winter_day_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(B_2025_winter_day_ELC_HEAT_SYS_2020_HEAT) ++1.1 v_flow_out(A_2025_winter_day_ELC_E_BATT_2025_ELC) ++1.1 v_flow_out(A_2025_winter_day_ELC_HEAT_SYS_2025_HEAT) ++1.1 v_flow_out(B_2025_winter_day_NG_NGCC_2015_ELC) ++1.1 v_flow_out(A_2025_winter_day_ethos_SOLPV_2020_ELC) ++1.1 v_flow_out(A_2025_winter_day_NG_NGCC_2025_ELC) ++1.1 v_flow_out(A_2025_winter_day_ethos_SOLPV_2025_ELC) ++1.1 v_flow_out(B_2025_winter_day_ELC_HEAT_SYS_2025_HEAT) ++1.1 v_flow_out(A_2025_winter_day_NG_NGCC_2015_ELC) ++1.1 v_flow_out(B_2025_winter_day_NG_NGCC_2020_ELC) +-0.32849999999999996 v_capacity(A_2025_SOLPV_2015) +-1.971 v_capacity(A_2025_E_BATT_2020) +-2.19 v_capacity(A_2025_NGCC_2015) +-2.19 v_capacity(B_2025_NGCC_2025) +-2.19 v_capacity(C_B_2025_E_TRANS_2015) +-0.32849999999999996 v_capacity(A_2025_SOLPV_2020) +-2.19 v_capacity(C_A_2025_E_TRANS_2015) +-2.19 v_capacity(A_2025_NGCC_2020) +-1.971 v_capacity(A_2025_E_BATT_2025) +-2.19 v_capacity(B_2025_NGCC_2015) ++2.19 v_capacity(A_C_2025_E_TRANS_2015) +-0.32849999999999996 v_capacity(A_2025_SOLPV_2025) ++2.19 v_capacity(B_C_2025_E_TRANS_2015) +-2.19 v_capacity(A_2025_NGCC_2025) +-2.19 v_capacity(B_2025_NGCC_2020) +-1.1 v_flow_in(A_2025_winter_day_ELC_E_BATT_2020_ELC) +-1.1 v_flow_in(A_2025_winter_day_ELC_E_BATT_2025_ELC) +<= 0 + +c_u_planning_reserve_margin_constraint(A_2025_winter_night_NGCC)_: ++1.15 v_flow_out(A_2025_winter_night_NG_NGCC_2015_ELC) ++1.15 v_flow_out(A_2025_winter_night_NG_NGCC_2020_ELC) ++1.15 v_flow_out(A_2025_winter_night_NG_NGCC_2025_ELC) +-2.19 v_capacity(A_2025_NGCC_2015) +-2.19 v_capacity(A_2025_NGCC_2020) +-2.19 v_capacity(A_2025_NGCC_2025) +<= 0 + +c_u_planning_reserve_margin_constraint(C_2025_winter_day_elec_C_ann)_: +-0.25 v_capacity(C_2025_ANN_IMP_2020) ++0.3 v_flow_out_annual(C_2025_ethos_ANN_IMP_2020_ELC_C) +<= -0.33 + +c_u_planning_reserve_margin_constraint(A_2020_summer_night_NGCC)_: ++1.15 v_flow_out(A_2020_summer_night_NG_NGCC_2015_ELC) ++1.15 v_flow_out(A_2020_summer_night_NG_NGCC_2020_ELC) +-2.19 v_capacity(A_2020_NGCC_2020) +-2.19 v_capacity(A_2020_NGCC_2015) +<= 0 + +c_u_planning_reserve_margin_constraint(A_B_2020_winter_day_elec_AB)_: ++1.1 v_flow_out(A_2020_winter_day_ELC_E_BATT_2020_ELC) ++1.1 v_flow_out(B_2020_winter_day_NG_NGCC_2020_ELC) +-1.1578947368421053 v_flow_out(A_C_2020_winter_day_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(A_2020_winter_day_NG_NGCC_2015_ELC) ++1.1 v_flow_out(A_2020_winter_day_ethos_SOLPV_2015_ELC) ++1.1 v_flow_out(C_A_2020_winter_day_ELC_E_TRANS_2015_ELC) +-1.1578947368421053 v_flow_out(B_C_2020_winter_day_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(B_2020_winter_day_NG_NGCC_2015_ELC) ++1.1 v_flow_out(C_B_2020_winter_day_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(A_2020_winter_day_NG_NGCC_2020_ELC) ++1.1 v_flow_out(A_2020_winter_day_ethos_SOLPV_2020_ELC) +-2.19 v_capacity(A_2020_NGCC_2020) ++2.19 v_capacity(B_C_2020_E_TRANS_2015) +-0.32849999999999996 v_capacity(A_2020_SOLPV_2015) +-2.19 v_capacity(B_2020_NGCC_2015) ++2.19 v_capacity(A_C_2020_E_TRANS_2015) +-1.971 v_capacity(A_2020_E_BATT_2020) +-0.32849999999999996 v_capacity(A_2020_SOLPV_2020) +-2.19 v_capacity(A_2020_NGCC_2015) +-2.19 v_capacity(B_2020_NGCC_2020) +-2.19 v_capacity(C_B_2020_E_TRANS_2015) +-2.19 v_capacity(C_A_2020_E_TRANS_2015) +-1.1 v_flow_in(A_2020_winter_day_ELC_E_BATT_2020_ELC) +<= -2.2 + +c_u_planning_reserve_margin_constraint(A_2020_winter_day_NGCC)_: ++1.15 v_flow_out(A_2020_winter_day_NG_NGCC_2015_ELC) ++1.15 v_flow_out(A_2020_winter_day_NG_NGCC_2020_ELC) +-2.19 v_capacity(A_2020_NGCC_2020) +-2.19 v_capacity(A_2020_NGCC_2015) +<= 0 + +c_u_planning_reserve_margin_constraint(C_2020_winter_night_elec_C_ann)_: +-0.25 v_capacity(C_2020_ANN_IMP_2020) ++0.3 v_flow_out_annual(C_2020_ethos_ANN_IMP_2020_ELC_C) +<= -0.3 + +c_u_planning_reserve_margin_constraint(A_B_2025_summer_day_elec_AB)_: ++1.1 v_flow_out(A_2025_summer_day_NG_NGCC_2020_ELC) ++1.1 v_flow_out(A_2025_summer_day_ethos_SOLPV_2020_ELC) ++1.1 v_flow_out(B_2025_summer_day_ELC_HEAT_SYS_2020_HEAT) ++1.1 v_flow_out(A_2025_summer_day_ELC_E_BATT_2025_ELC) ++1.1 v_flow_out(A_2025_summer_day_ELC_HEAT_SYS_2025_HEAT) ++1.1 v_flow_out(B_2025_summer_day_NG_NGCC_2020_ELC) +-1.1578947368421053 v_flow_out(B_C_2025_summer_day_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(A_2025_summer_day_NG_NGCC_2025_ELC) ++1.1 v_flow_out(A_2025_summer_day_ethos_SOLPV_2025_ELC) ++1.1 v_flow_out(C_B_2025_summer_day_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(C_A_2025_summer_day_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(B_2025_summer_day_ELC_HEAT_SYS_2025_HEAT) ++1.1 v_flow_out(A_2025_summer_day_NG_NGCC_2015_ELC) ++1.1 v_flow_out(A_2025_summer_day_ethos_SOLPV_2015_ELC) ++1.1 v_flow_out(B_2025_summer_day_NG_NGCC_2025_ELC) ++1.1 v_flow_out(A_2025_summer_day_ELC_E_BATT_2020_ELC) ++1.1 v_flow_out(A_2025_summer_day_ELC_HEAT_SYS_2020_HEAT) +-1.1578947368421053 v_flow_out(A_C_2025_summer_day_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(B_2025_summer_day_NG_NGCC_2015_ELC) +-0.32849999999999996 v_capacity(A_2025_SOLPV_2015) +-1.971 v_capacity(A_2025_E_BATT_2020) +-2.19 v_capacity(A_2025_NGCC_2015) +-2.19 v_capacity(B_2025_NGCC_2025) +-2.19 v_capacity(C_B_2025_E_TRANS_2015) +-0.32849999999999996 v_capacity(A_2025_SOLPV_2020) +-2.19 v_capacity(C_A_2025_E_TRANS_2015) +-2.19 v_capacity(A_2025_NGCC_2020) +-1.971 v_capacity(A_2025_E_BATT_2025) +-2.19 v_capacity(B_2025_NGCC_2015) ++2.19 v_capacity(A_C_2025_E_TRANS_2015) +-0.32849999999999996 v_capacity(A_2025_SOLPV_2025) ++2.19 v_capacity(B_C_2025_E_TRANS_2015) +-2.19 v_capacity(A_2025_NGCC_2025) +-2.19 v_capacity(B_2025_NGCC_2020) +-1.1 v_flow_in(A_2025_summer_day_ELC_E_BATT_2025_ELC) +-1.1 v_flow_in(A_2025_summer_day_ELC_E_BATT_2020_ELC) +<= 0 + +c_u_planning_reserve_margin_constraint(A_B_2020_summer_night_elec_AB)_: ++1.1 v_flow_out(A_2020_summer_night_ELC_E_BATT_2020_ELC) ++1.1 v_flow_out(C_A_2020_summer_night_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(B_2020_summer_night_NG_NGCC_2015_ELC) ++1.1 v_flow_out(C_B_2020_summer_night_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(A_2020_summer_night_NG_NGCC_2015_ELC) ++1.1 v_flow_out(A_2020_summer_night_ethos_SOLPV_2015_ELC) ++1.1 v_flow_out(A_2020_summer_night_NG_NGCC_2020_ELC) ++1.1 v_flow_out(B_2020_summer_night_NG_NGCC_2020_ELC) +-1.1578947368421053 v_flow_out(A_C_2020_summer_night_ELC_E_TRANS_2015_ELC) +-1.1578947368421053 v_flow_out(B_C_2020_summer_night_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(A_2020_summer_night_ethos_SOLPV_2020_ELC) +-2.19 v_capacity(A_2020_NGCC_2020) ++2.19 v_capacity(B_C_2020_E_TRANS_2015) +-0.32849999999999996 v_capacity(A_2020_SOLPV_2015) +-2.19 v_capacity(B_2020_NGCC_2015) ++2.19 v_capacity(A_C_2020_E_TRANS_2015) +-1.971 v_capacity(A_2020_E_BATT_2020) +-0.32849999999999996 v_capacity(A_2020_SOLPV_2020) +-2.19 v_capacity(A_2020_NGCC_2015) +-2.19 v_capacity(B_2020_NGCC_2020) +-2.19 v_capacity(C_B_2020_E_TRANS_2015) +-2.19 v_capacity(C_A_2020_E_TRANS_2015) +-1.1 v_flow_in(A_2020_summer_night_ELC_E_BATT_2020_ELC) +<= -2.2 + +c_u_planning_reserve_margin_constraint(C_2020_summer_night_elec_C_ann)_: +-0.25 v_capacity(C_2020_ANN_IMP_2020) ++0.3 v_flow_out_annual(C_2020_ethos_ANN_IMP_2020_ELC_C) +<= -0.3 + +c_u_planning_reserve_margin_constraint(A_B_2020_winter_night_elec_AB)_: ++1.1 v_flow_out(B_2020_winter_night_NG_NGCC_2015_ELC) ++1.1 v_flow_out(C_B_2020_winter_night_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(A_2020_winter_night_NG_NGCC_2020_ELC) +-1.1578947368421053 v_flow_out(B_C_2020_winter_night_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(A_2020_winter_night_ethos_SOLPV_2015_ELC) ++1.1 v_flow_out(A_2020_winter_night_ELC_E_BATT_2020_ELC) +-1.1578947368421053 v_flow_out(A_C_2020_winter_night_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(B_2020_winter_night_NG_NGCC_2020_ELC) ++1.1 v_flow_out(A_2020_winter_night_NG_NGCC_2015_ELC) ++1.1 v_flow_out(C_A_2020_winter_night_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(A_2020_winter_night_ethos_SOLPV_2020_ELC) +-2.19 v_capacity(A_2020_NGCC_2020) ++2.19 v_capacity(B_C_2020_E_TRANS_2015) +-0.32849999999999996 v_capacity(A_2020_SOLPV_2015) +-2.19 v_capacity(B_2020_NGCC_2015) ++2.19 v_capacity(A_C_2020_E_TRANS_2015) +-1.971 v_capacity(A_2020_E_BATT_2020) +-0.32849999999999996 v_capacity(A_2020_SOLPV_2020) +-2.19 v_capacity(A_2020_NGCC_2015) +-2.19 v_capacity(B_2020_NGCC_2020) +-2.19 v_capacity(C_B_2020_E_TRANS_2015) +-2.19 v_capacity(C_A_2020_E_TRANS_2015) +-1.1 v_flow_in(A_2020_winter_night_ELC_E_BATT_2020_ELC) +<= -2.2 + +c_u_planning_reserve_margin_constraint(C_2025_summer_day_elec_C_ann)_: +-0.25 v_capacity(C_2025_ANN_IMP_2020) ++0.3 v_flow_out_annual(C_2025_ethos_ANN_IMP_2020_ELC_C) +<= -0.33 + +c_u_planning_reserve_margin_constraint(A_B_2025_winter_night_elec_AB)_: ++1.1 v_flow_out(A_2025_winter_night_NG_NGCC_2015_ELC) +-1.1578947368421053 v_flow_out(B_C_2025_winter_night_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(C_A_2025_winter_night_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(B_2025_winter_night_NG_NGCC_2015_ELC) ++1.1 v_flow_out(A_2025_winter_night_ethos_SOLPV_2015_ELC) ++1.1 v_flow_out(C_B_2025_winter_night_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(A_2025_winter_night_ELC_E_BATT_2020_ELC) ++1.1 v_flow_out(A_2025_winter_night_ELC_HEAT_SYS_2020_HEAT) +-1.1578947368421053 v_flow_out(A_C_2025_winter_night_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(B_2025_winter_night_ELC_HEAT_SYS_2020_HEAT) ++1.1 v_flow_out(A_2025_winter_night_NG_NGCC_2020_ELC) ++1.1 v_flow_out(A_2025_winter_night_ethos_SOLPV_2020_ELC) ++1.1 v_flow_out(A_2025_winter_night_NG_NGCC_2025_ELC) ++1.1 v_flow_out(B_2025_winter_night_NG_NGCC_2020_ELC) ++1.1 v_flow_out(B_2025_winter_night_ELC_HEAT_SYS_2025_HEAT) ++1.1 v_flow_out(A_2025_winter_night_ELC_E_BATT_2025_ELC) ++1.1 v_flow_out(A_2025_winter_night_ELC_HEAT_SYS_2025_HEAT) ++1.1 v_flow_out(B_2025_winter_night_NG_NGCC_2025_ELC) ++1.1 v_flow_out(A_2025_winter_night_ethos_SOLPV_2025_ELC) +-0.32849999999999996 v_capacity(A_2025_SOLPV_2015) +-1.971 v_capacity(A_2025_E_BATT_2020) +-2.19 v_capacity(A_2025_NGCC_2015) +-2.19 v_capacity(B_2025_NGCC_2025) +-2.19 v_capacity(C_B_2025_E_TRANS_2015) +-0.32849999999999996 v_capacity(A_2025_SOLPV_2020) +-2.19 v_capacity(C_A_2025_E_TRANS_2015) +-2.19 v_capacity(A_2025_NGCC_2020) +-1.971 v_capacity(A_2025_E_BATT_2025) +-2.19 v_capacity(B_2025_NGCC_2015) ++2.19 v_capacity(A_C_2025_E_TRANS_2015) +-0.32849999999999996 v_capacity(A_2025_SOLPV_2025) ++2.19 v_capacity(B_C_2025_E_TRANS_2015) +-2.19 v_capacity(A_2025_NGCC_2025) +-2.19 v_capacity(B_2025_NGCC_2020) +-1.1 v_flow_in(A_2025_winter_night_ELC_E_BATT_2025_ELC) +-1.1 v_flow_in(A_2025_winter_night_ELC_E_BATT_2020_ELC) +<= 0 + +c_u_planning_reserve_margin_constraint(A_2025_summer_day_NGCC)_: ++1.15 v_flow_out(A_2025_summer_day_NG_NGCC_2020_ELC) ++1.15 v_flow_out(A_2025_summer_day_NG_NGCC_2025_ELC) ++1.15 v_flow_out(A_2025_summer_day_NG_NGCC_2015_ELC) +-2.19 v_capacity(A_2025_NGCC_2015) +-2.19 v_capacity(A_2025_NGCC_2020) +-2.19 v_capacity(A_2025_NGCC_2025) +<= 0 + +c_u_planning_reserve_margin_constraint(A_2020_winter_night_NGCC)_: ++1.15 v_flow_out(A_2020_winter_night_NG_NGCC_2020_ELC) ++1.15 v_flow_out(A_2020_winter_night_NG_NGCC_2015_ELC) +-2.19 v_capacity(A_2020_NGCC_2020) +-2.19 v_capacity(A_2020_NGCC_2015) +<= 0 + +c_u_planning_reserve_margin_constraint(A_2025_summer_night_NGCC)_: ++1.15 v_flow_out(A_2025_summer_night_NG_NGCC_2020_ELC) ++1.15 v_flow_out(A_2025_summer_night_NG_NGCC_2025_ELC) ++1.15 v_flow_out(A_2025_summer_night_NG_NGCC_2015_ELC) +-2.19 v_capacity(A_2025_NGCC_2015) +-2.19 v_capacity(A_2025_NGCC_2020) +-2.19 v_capacity(A_2025_NGCC_2025) +<= 0 + +c_u_planning_reserve_margin_constraint(A_2025_winter_day_NGCC)_: ++1.15 v_flow_out(A_2025_winter_day_NG_NGCC_2020_ELC) ++1.15 v_flow_out(A_2025_winter_day_NG_NGCC_2025_ELC) ++1.15 v_flow_out(A_2025_winter_day_NG_NGCC_2015_ELC) +-2.19 v_capacity(A_2025_NGCC_2015) +-2.19 v_capacity(A_2025_NGCC_2020) +-2.19 v_capacity(A_2025_NGCC_2025) +<= 0 + +c_u_planning_reserve_margin_constraint(C_2025_winter_night_elec_C_ann)_: +-0.25 v_capacity(C_2025_ANN_IMP_2020) ++0.3 v_flow_out_annual(C_2025_ethos_ANN_IMP_2020_ELC_C) +<= -0.33 + +c_u_planning_reserve_margin_constraint(C_2020_winter_day_elec_C_ann)_: +-0.25 v_capacity(C_2020_ANN_IMP_2020) ++0.3 v_flow_out_annual(C_2020_ethos_ANN_IMP_2020_ELC_C) +<= -0.3 + +c_u_planning_reserve_margin_constraint(C_2025_summer_night_elec_C_ann)_: +-0.25 v_capacity(C_2025_ANN_IMP_2020) ++0.3 v_flow_out_annual(C_2025_ethos_ANN_IMP_2020_ELC_C) +<= -0.33 + +c_u_planning_reserve_margin_constraint(A_B_2020_summer_day_elec_AB)_: +-1.1578947368421053 v_flow_out(A_C_2020_summer_day_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(A_2020_summer_day_NG_NGCC_2020_ELC) ++1.1 v_flow_out(A_2020_summer_day_ethos_SOLPV_2020_ELC) ++1.1 v_flow_out(B_2020_summer_day_NG_NGCC_2020_ELC) ++1.1 v_flow_out(A_2020_summer_day_ELC_E_BATT_2020_ELC) ++1.1 v_flow_out(C_B_2020_summer_day_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(A_2020_summer_day_NG_NGCC_2015_ELC) ++1.1 v_flow_out(B_2020_summer_day_NG_NGCC_2015_ELC) +-1.1578947368421053 v_flow_out(B_C_2020_summer_day_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(C_A_2020_summer_day_ELC_E_TRANS_2015_ELC) ++1.1 v_flow_out(A_2020_summer_day_ethos_SOLPV_2015_ELC) +-2.19 v_capacity(A_2020_NGCC_2020) ++2.19 v_capacity(B_C_2020_E_TRANS_2015) +-0.32849999999999996 v_capacity(A_2020_SOLPV_2015) +-2.19 v_capacity(B_2020_NGCC_2015) ++2.19 v_capacity(A_C_2020_E_TRANS_2015) +-1.971 v_capacity(A_2020_E_BATT_2020) +-0.32849999999999996 v_capacity(A_2020_SOLPV_2020) +-2.19 v_capacity(A_2020_NGCC_2015) +-2.19 v_capacity(B_2020_NGCC_2020) +-2.19 v_capacity(C_B_2020_E_TRANS_2015) +-2.19 v_capacity(C_A_2020_E_TRANS_2015) +-1.1 v_flow_in(A_2020_summer_day_ELC_E_BATT_2020_ELC) +<= -2.2 + +bounds + 1 <= ONE_VAR_CONSTANT <= 1 + 0 <= v_flow_out(B_A_2025_winter_day_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(A_B_2025_summer_day_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(A_2025_winter_night_NG_NGCC_2015_ELC) <= +inf + 0 <= v_flow_out(A_2025_summer_day_NG_NGCC_2020_ELC) <= +inf + 0 <= v_flow_out(A_2025_summer_night_ethos_SOLPV_2015_ELC) <= +inf + 0 <= v_flow_out(A_2025_winter_day_ELC_E_BATT_2020_ELC) <= +inf + 0 <= v_flow_out(B_C_2025_winter_night_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(A_2020_summer_night_ELC_E_BATT_2020_ELC) <= +inf + 0 <= v_flow_out(B_A_2020_winter_day_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(A_C_2020_summer_day_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(A_2025_summer_day_ethos_SOLPV_2020_ELC) <= +inf + 0 <= v_flow_out(B_2020_winter_night_NG_NGCC_2015_ELC) <= +inf + 0 <= v_flow_out(A_2025_winter_day_ELC_HEAT_SYS_2020_HEAT) <= +inf + 0 <= v_flow_out(B_A_2025_summer_day_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(C_A_2025_winter_night_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(B_2025_winter_day_NG_NGCC_2025_ELC) <= +inf + 0 <= v_flow_out(B_2025_summer_night_NG_NGCC_2015_ELC) <= +inf + 0 <= v_flow_out(B_2025_summer_day_ELC_HEAT_SYS_2020_HEAT) <= +inf + 0 <= v_flow_out(A_C_2025_winter_day_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(C_B_2020_winter_night_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(A_2020_summer_day_NG_NGCC_2020_ELC) <= +inf + 0 <= v_flow_out(C_A_2025_summer_night_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(B_2025_winter_night_ethos_IMP_NG_2020_NG) <= +inf + 0 <= v_flow_out(C_A_2020_summer_night_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(B_A_2020_winter_night_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(A_2020_winter_night_NG_NGCC_2020_ELC) <= +inf + 0 <= v_flow_out(A_2020_winter_day_ELC_E_BATT_2020_ELC) <= +inf + 0 <= v_flow_out(A_2020_summer_day_ethos_SOLPV_2020_ELC) <= +inf + 0 <= v_flow_out(B_2025_winter_night_NG_NGCC_2015_ELC) <= +inf + 0 <= v_flow_out(B_2020_summer_day_NG_NGCC_2020_ELC) <= +inf + 0 <= v_flow_out(B_2025_winter_day_ethos_IMP_NG_2020_NG) <= +inf + 0 <= v_flow_out(B_C_2020_winter_night_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(A_2020_summer_day_ELC_E_BATT_2020_ELC) <= +inf + 0 <= v_flow_out(B_C_2025_summer_night_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(A_B_2025_winter_night_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(B_2020_winter_day_NG_NGCC_2020_ELC) <= +inf + 0 <= v_flow_out(A_2025_summer_day_ethos_IMP_NG_2020_NG) <= +inf + 0 <= v_flow_out(A_2025_winter_night_ethos_SOLPV_2015_ELC) <= +inf + 0 <= v_flow_out(C_B_2025_winter_night_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(B_2020_summer_night_NG_NGCC_2015_ELC) <= +inf + 0 <= v_flow_out(C_A_2025_winter_day_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(B_C_2025_winter_day_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(A_B_2025_winter_day_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(A_2025_winter_night_ELC_E_BATT_2020_ELC) <= +inf + 0 <= v_flow_out(A_2025_summer_night_ELC_HEAT_SYS_2020_HEAT) <= +inf + 0 <= v_flow_out(A_2025_summer_day_ELC_E_BATT_2025_ELC) <= +inf + 0 <= v_flow_out(B_2020_winter_day_ethos_IMP_NG_2020_NG) <= +inf + 0 <= v_flow_out(B_2025_summer_night_ELC_HEAT_SYS_2020_HEAT) <= +inf + 0 <= v_flow_out(A_C_2025_summer_night_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(A_2025_winter_night_ELC_HEAT_SYS_2020_HEAT) <= +inf + 0 <= v_flow_out(A_2025_summer_day_ELC_HEAT_SYS_2025_HEAT) <= +inf + 0 <= v_flow_out(A_2025_winter_day_NG_NGCC_2020_ELC) <= +inf + 0 <= v_flow_out(B_A_2025_summer_night_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(C_B_2020_summer_night_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(B_2025_summer_day_NG_NGCC_2020_ELC) <= +inf + 0 <= v_flow_out(A_2020_summer_night_NG_NGCC_2015_ELC) <= +inf + 0 <= v_flow_out(B_2025_summer_day_ethos_IMP_NG_2020_NG) <= +inf + 0 <= v_flow_out(A_2020_summer_day_ethos_IMP_NG_2020_NG) <= +inf + 0 <= v_flow_out(A_C_2025_winter_night_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(A_2025_summer_night_NG_NGCC_2020_ELC) <= +inf + 0 <= v_flow_out(A_2020_winter_night_ethos_SOLPV_2015_ELC) <= +inf + 0 <= v_flow_out(A_2020_summer_night_ethos_SOLPV_2015_ELC) <= +inf + 0 <= v_flow_out(A_2020_winter_night_ELC_E_BATT_2020_ELC) <= +inf + 0 <= v_flow_out(B_C_2025_summer_day_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(B_2025_winter_night_ELC_HEAT_SYS_2020_HEAT) <= +inf + 0 <= v_flow_out(A_2025_winter_night_NG_NGCC_2020_ELC) <= +inf + 0 <= v_flow_out(A_C_2020_winter_night_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(A_2025_summer_day_NG_NGCC_2025_ELC) <= +inf + 0 <= v_flow_out(A_2025_winter_day_ethos_SOLPV_2015_ELC) <= +inf + 0 <= v_flow_out(C_B_2025_winter_day_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(B_2025_winter_day_ELC_HEAT_SYS_2020_HEAT) <= +inf + 0 <= v_flow_out(A_C_2020_winter_day_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(A_2025_summer_night_ethos_SOLPV_2020_ELC) <= +inf + 0 <= v_flow_out(A_2025_winter_day_ELC_E_BATT_2025_ELC) <= +inf + 0 <= v_flow_out(A_2025_winter_night_ethos_SOLPV_2020_ELC) <= +inf + 0 <= v_flow_out(C_B_2020_summer_day_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(B_A_2025_winter_night_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(A_2020_winter_day_NG_NGCC_2015_ELC) <= +inf + 0 <= v_flow_out(A_2025_summer_day_ethos_SOLPV_2025_ELC) <= +inf + 0 <= v_flow_out(B_2020_winter_night_NG_NGCC_2020_ELC) <= +inf + 0 <= v_flow_out(C_B_2025_summer_day_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(A_2025_winter_day_ELC_HEAT_SYS_2025_HEAT) <= +inf + 0 <= v_flow_out(A_2020_summer_night_NG_NGCC_2020_ELC) <= +inf + 0 <= v_flow_out(B_2025_winter_day_NG_NGCC_2015_ELC) <= +inf + 0 <= v_flow_out(B_2020_winter_night_ethos_IMP_NG_2020_NG) <= +inf + 0 <= v_flow_out(C_A_2025_summer_day_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(B_2025_summer_night_NG_NGCC_2020_ELC) <= +inf + 0 <= v_flow_out(B_2025_summer_day_ELC_HEAT_SYS_2025_HEAT) <= +inf + 0 <= v_flow_out(A_2020_winter_night_ethos_IMP_NG_2020_NG) <= +inf + 0 <= v_flow_out(A_2020_winter_day_ethos_SOLPV_2015_ELC) <= +inf + 0 <= v_flow_out(A_2025_summer_day_NG_NGCC_2015_ELC) <= +inf + 0 <= v_flow_out(A_2025_summer_night_ELC_E_BATT_2020_ELC) <= +inf + 0 <= v_flow_out(A_2025_winter_night_NG_NGCC_2025_ELC) <= +inf + 0 <= v_flow_out(C_A_2020_winter_day_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(A_2025_summer_day_ethos_SOLPV_2015_ELC) <= +inf + 0 <= v_flow_out(B_2025_winter_night_NG_NGCC_2020_ELC) <= +inf + 0 <= v_flow_out(B_2020_summer_day_ethos_IMP_NG_2020_NG) <= +inf + 0 <= v_flow_out(A_B_2020_winter_night_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(B_2025_winter_night_ELC_HEAT_SYS_2025_HEAT) <= +inf + 0 <= v_flow_out(A_2025_winter_day_ethos_SOLPV_2020_ELC) <= +inf + 0 <= v_flow_out(B_2020_summer_night_NG_NGCC_2020_ELC) <= +inf + 0 <= v_flow_out(A_C_2020_summer_night_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(A_2025_summer_night_ethos_IMP_NG_2020_NG) <= +inf + 0 <= v_flow_out(A_2020_summer_day_NG_NGCC_2015_ELC) <= +inf + 0 <= v_flow_out(A_2025_winter_night_ELC_E_BATT_2025_ELC) <= +inf + 0 <= v_flow_out(B_A_2020_summer_day_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(B_2025_summer_night_ELC_HEAT_SYS_2025_HEAT) <= +inf + 0 <= v_flow_out(A_2020_winter_night_NG_NGCC_2015_ELC) <= +inf + 0 <= v_flow_out(A_2025_summer_night_ELC_E_BATT_2025_ELC) <= +inf + 0 <= v_flow_out(B_C_2020_winter_day_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(B_2020_summer_night_ethos_IMP_NG_2020_NG) <= +inf + 0 <= v_flow_out(A_2025_winter_night_ELC_HEAT_SYS_2025_HEAT) <= +inf + 0 <= v_flow_out(A_2025_winter_day_NG_NGCC_2025_ELC) <= +inf + 0 <= v_flow_out(A_2020_summer_night_ethos_IMP_NG_2020_NG) <= +inf + 0 <= v_flow_out(B_2020_summer_day_NG_NGCC_2015_ELC) <= +inf + 0 <= v_flow_out(A_2025_summer_night_ELC_HEAT_SYS_2025_HEAT) <= +inf + 0 <= v_flow_out(B_2025_winter_night_NG_NGCC_2025_ELC) <= +inf + 0 <= v_flow_out(B_C_2020_summer_day_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(C_B_2025_summer_night_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(B_2025_summer_day_NG_NGCC_2025_ELC) <= +inf + 0 <= v_flow_out(B_C_2020_summer_night_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(A_B_2025_summer_night_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(A_2025_winter_day_ethos_SOLPV_2025_ELC) <= +inf + 0 <= v_flow_out(B_2020_winter_day_NG_NGCC_2015_ELC) <= +inf + 0 <= v_flow_out(A_B_2020_summer_night_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(C_A_2020_winter_night_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(A_2025_winter_day_ethos_IMP_NG_2020_NG) <= +inf + 0 <= v_flow_out(A_2020_winter_night_ethos_SOLPV_2020_ELC) <= +inf + 0 <= v_flow_out(B_2025_summer_night_ethos_IMP_NG_2020_NG) <= +inf + 0 <= v_flow_out(A_2020_summer_night_ethos_SOLPV_2020_ELC) <= +inf + 0 <= v_flow_out(A_2025_summer_day_ELC_E_BATT_2020_ELC) <= +inf + 0 <= v_flow_out(A_B_2020_summer_day_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(C_A_2020_summer_day_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(B_2025_winter_day_ELC_HEAT_SYS_2025_HEAT) <= +inf + 0 <= v_flow_out(A_2025_summer_day_ELC_HEAT_SYS_2020_HEAT) <= +inf + 0 <= v_flow_out(A_2025_summer_night_NG_NGCC_2025_ELC) <= +inf + 0 <= v_flow_out(A_2025_winter_day_NG_NGCC_2015_ELC) <= +inf + 0 <= v_flow_out(A_2025_summer_night_ethos_SOLPV_2025_ELC) <= +inf + 0 <= v_flow_out(A_C_2025_summer_day_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(C_B_2020_winter_day_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(B_2025_summer_day_NG_NGCC_2015_ELC) <= +inf + 0 <= v_flow_out(A_2025_winter_night_ethos_SOLPV_2025_ELC) <= +inf + 0 <= v_flow_out(A_2020_winter_day_NG_NGCC_2020_ELC) <= +inf + 0 <= v_flow_out(B_2025_winter_day_NG_NGCC_2020_ELC) <= +inf + 0 <= v_flow_out(A_2025_winter_night_ethos_IMP_NG_2020_NG) <= +inf + 0 <= v_flow_out(B_A_2020_summer_night_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(A_B_2020_winter_day_ELC_E_TRANS_2015_ELC) <= +inf + 0 <= v_flow_out(A_2020_winter_day_ethos_IMP_NG_2020_NG) <= +inf + 0 <= v_flow_out(A_2025_summer_night_NG_NGCC_2015_ELC) <= +inf + 0 <= v_flow_out(B_2025_summer_night_NG_NGCC_2025_ELC) <= +inf + 0 <= v_flow_out(A_2020_summer_day_ethos_SOLPV_2015_ELC) <= +inf + 0 <= v_flow_out(A_2020_winter_day_ethos_SOLPV_2020_ELC) <= +inf + 0 <= v_capacity(C_2025_HEAT_ANN_2020) <= +inf + 0 <= v_capacity(A_2020_HEAT_SYS_2020) <= +inf + 0 <= v_capacity(A_2025_SOLPV_2015) <= +inf + 0 <= v_capacity(A_2025_E_BATT_2020) <= +inf + 0 <= v_capacity(A_2025_NGCC_2015) <= +inf + 0 <= v_capacity(A_B_2025_E_TRANS_2015) <= +inf + 0 <= v_capacity(A_2020_NGCC_2020) <= +inf + 0 <= v_capacity(B_2025_NGCC_2025) <= +inf + 0 <= v_capacity(B_C_2020_E_TRANS_2015) <= +inf + 0 <= v_capacity(C_B_2025_E_TRANS_2015) <= +inf + 0 <= v_capacity(A_2025_SOLPV_2020) <= +inf + 0 <= v_capacity(C_A_2025_E_TRANS_2015) <= +inf + 0 <= v_capacity(A_2025_NGCC_2020) <= +inf + 0 <= v_capacity(A_2025_E_BATT_2025) <= +inf + 0 <= v_capacity(A_2020_SOLPV_2015) <= +inf + 0 <= v_capacity(B_2020_NGCC_2015) <= +inf + 0 <= v_capacity(A_2025_HEAT_SYS_2020) <= +inf + 0 <= v_capacity(B_2025_NGCC_2015) <= +inf + 0 <= v_capacity(B_2025_HEAT_SYS_2020) <= +inf + 0 <= v_capacity(B_A_2025_E_TRANS_2015) <= +inf + 0 <= v_capacity(C_2020_ANN_IMP_2020) <= +inf + 0 <= v_capacity(C_2020_HEAT_ANN_2020) <= +inf + 0 <= v_capacity(A_B_2020_E_TRANS_2015) <= +inf + 0 <= v_capacity(A_C_2020_E_TRANS_2015) <= +inf + 0 <= v_capacity(A_2020_E_BATT_2020) <= +inf + 0 <= v_capacity(A_C_2025_E_TRANS_2015) <= +inf + 0 <= v_capacity(B_2020_HEAT_SYS_2020) <= +inf + 0 <= v_capacity(A_2025_SOLPV_2025) <= +inf + 0 <= v_capacity(B_C_2025_E_TRANS_2015) <= +inf + 0 <= v_capacity(A_2025_NGCC_2025) <= +inf + 0 <= v_capacity(A_2025_HEAT_SYS_2025) <= +inf + 0 <= v_capacity(A_2020_SOLPV_2020) <= +inf + 0 <= v_capacity(A_2020_NGCC_2015) <= +inf + 0 <= v_capacity(B_2020_NGCC_2020) <= +inf + 0 <= v_capacity(B_2025_NGCC_2020) <= +inf + 0 <= v_capacity(C_B_2020_E_TRANS_2015) <= +inf + 0 <= v_capacity(B_2025_HEAT_SYS_2025) <= +inf + 0 <= v_capacity(B_A_2020_E_TRANS_2015) <= +inf + 0 <= v_capacity(C_A_2020_E_TRANS_2015) <= +inf + 0 <= v_capacity(C_2025_ANN_IMP_2020) <= +inf + 0 <= v_curtailment(A_2025_summer_day_ethos_SOLPV_2025_ELC) <= +inf + 0 <= v_curtailment(A_2025_winter_night_ethos_SOLPV_2015_ELC) <= +inf + 0 <= v_curtailment(A_2025_summer_night_ethos_SOLPV_2015_ELC) <= +inf + 0 <= v_curtailment(A_2020_winter_day_ethos_SOLPV_2015_ELC) <= +inf + 0 <= v_curtailment(A_2025_summer_day_ethos_SOLPV_2020_ELC) <= +inf + 0 <= v_curtailment(A_2025_winter_day_ethos_SOLPV_2025_ELC) <= +inf + 0 <= v_curtailment(A_2020_winter_night_ethos_SOLPV_2020_ELC) <= +inf + 0 <= v_curtailment(A_2020_summer_night_ethos_SOLPV_2020_ELC) <= +inf + 0 <= v_curtailment(A_2025_summer_day_ethos_SOLPV_2015_ELC) <= +inf + 0 <= v_curtailment(A_2025_summer_night_ethos_SOLPV_2025_ELC) <= +inf + 0 <= v_curtailment(A_2020_winter_night_ethos_SOLPV_2015_ELC) <= +inf + 0 <= v_curtailment(A_2020_summer_night_ethos_SOLPV_2015_ELC) <= +inf + 0 <= v_curtailment(A_2025_winter_day_ethos_SOLPV_2020_ELC) <= +inf + 0 <= v_curtailment(A_2025_winter_night_ethos_SOLPV_2025_ELC) <= +inf + 0 <= v_curtailment(A_2025_winter_day_ethos_SOLPV_2015_ELC) <= +inf + 0 <= v_curtailment(A_2020_summer_day_ethos_SOLPV_2020_ELC) <= +inf + 0 <= v_curtailment(A_2025_summer_night_ethos_SOLPV_2020_ELC) <= +inf + 0 <= v_curtailment(A_2020_summer_day_ethos_SOLPV_2015_ELC) <= +inf + 0 <= v_curtailment(A_2020_winter_day_ethos_SOLPV_2020_ELC) <= +inf + 0 <= v_curtailment(A_2025_winter_night_ethos_SOLPV_2020_ELC) <= +inf + 0 <= v_flow_out_annual(A_2025_ELC_HEAT_SYS_2025_HEAT) <= +inf + 0 <= v_flow_out_annual(B_2025_ELC_HEAT_SYS_2020_HEAT) <= +inf + 0 <= v_flow_out_annual(A_2025_ELC_HEAT_SYS_2020_HEAT) <= +inf + 0 <= v_flow_out_annual(C_2025_ethos_ANN_IMP_2020_ELC_C) <= +inf + 0 <= v_flow_out_annual(B_2025_ELC_HEAT_SYS_2025_HEAT) <= +inf + 0 <= v_flow_out_annual(C_2020_ethos_ANN_IMP_2020_ELC_C) <= +inf + 0 <= v_capacity_available_by_period_and_tech(B_2025_NGCC) <= +inf + 0 <= v_capacity_available_by_period_and_tech(A_2020_E_BATT) <= +inf + 0 <= v_capacity_available_by_period_and_tech(B_2025_HEAT_SYS) <= +inf + 0 <= v_capacity_available_by_period_and_tech(C_2025_HEAT_ANN) <= +inf + 0 <= v_capacity_available_by_period_and_tech(A_B_2025_E_TRANS) <= +inf + 0 <= v_capacity_available_by_period_and_tech(B_C_2025_E_TRANS) <= +inf + 0 <= v_capacity_available_by_period_and_tech(A_C_2020_E_TRANS) <= +inf + 0 <= v_capacity_available_by_period_and_tech(C_2020_HEAT_ANN) <= +inf + 0 <= v_capacity_available_by_period_and_tech(C_B_2025_E_TRANS) <= +inf + 0 <= v_capacity_available_by_period_and_tech(A_2025_SOLPV) <= +inf + 0 <= v_capacity_available_by_period_and_tech(B_2020_NGCC) <= +inf + 0 <= v_capacity_available_by_period_and_tech(A_B_2020_E_TRANS) <= +inf + 0 <= v_capacity_available_by_period_and_tech(B_A_2025_E_TRANS) <= +inf + 0 <= v_capacity_available_by_period_and_tech(B_C_2020_E_TRANS) <= +inf + 0 <= v_capacity_available_by_period_and_tech(B_2020_HEAT_SYS) <= +inf + 0 <= v_capacity_available_by_period_and_tech(A_2025_NGCC) <= +inf + 0 <= v_capacity_available_by_period_and_tech(C_2025_ANN_IMP) <= +inf + 0 <= v_capacity_available_by_period_and_tech(C_B_2020_E_TRANS) <= +inf + 0 <= v_capacity_available_by_period_and_tech(A_2025_HEAT_SYS) <= +inf + 0 <= v_capacity_available_by_period_and_tech(C_A_2025_E_TRANS) <= +inf + 0 <= v_capacity_available_by_period_and_tech(A_2020_SOLPV) <= +inf + 0 <= v_capacity_available_by_period_and_tech(B_A_2020_E_TRANS) <= +inf + 0 <= v_capacity_available_by_period_and_tech(A_2025_E_BATT) <= +inf + 0 <= v_capacity_available_by_period_and_tech(A_C_2025_E_TRANS) <= +inf + 0 <= v_capacity_available_by_period_and_tech(A_2020_NGCC) <= +inf + 0 <= v_capacity_available_by_period_and_tech(C_2020_ANN_IMP) <= +inf + 0 <= v_capacity_available_by_period_and_tech(C_A_2020_E_TRANS) <= +inf + 0 <= v_capacity_available_by_period_and_tech(A_2020_HEAT_SYS) <= +inf + 0 <= v_new_capacity(A_SOLPV_2020) <= +inf + 0 <= v_new_capacity(B_NGCC_2020) <= +inf + 0 <= v_new_capacity(A_HEAT_SYS_2025) <= +inf + 0 <= v_new_capacity(A_NGCC_2020) <= +inf + 0 <= v_new_capacity(A_E_BATT_2020) <= +inf + 0 <= v_new_capacity(B_HEAT_SYS_2020) <= +inf + 0 <= v_new_capacity(A_SOLPV_2025) <= +inf + 0 <= v_new_capacity(B_NGCC_2025) <= +inf + 0 <= v_new_capacity(C_ANN_IMP_2020) <= +inf + 0 <= v_new_capacity(A_HEAT_SYS_2020) <= +inf + 0 <= v_new_capacity(A_NGCC_2025) <= +inf + 0 <= v_new_capacity(A_E_BATT_2025) <= +inf + 0 <= v_new_capacity(C_HEAT_ANN_2020) <= +inf + 0 <= v_new_capacity(B_HEAT_SYS_2025) <= +inf + 0 <= v_flow_in(A_2020_winter_day_ELC_E_BATT_2020_ELC) <= +inf + 0 <= v_flow_in(A_2025_winter_day_ELC_E_BATT_2020_ELC) <= +inf + 0 <= v_flow_in(A_2020_summer_night_ELC_E_BATT_2020_ELC) <= +inf + 0 <= v_flow_in(A_2025_summer_night_ELC_E_BATT_2020_ELC) <= +inf + 0 <= v_flow_in(A_2025_winter_night_ELC_E_BATT_2025_ELC) <= +inf + 0 <= v_flow_in(A_2025_winter_night_ELC_E_BATT_2020_ELC) <= +inf + 0 <= v_flow_in(A_2025_winter_day_ELC_E_BATT_2025_ELC) <= +inf + 0 <= v_flow_in(A_2025_summer_day_ELC_E_BATT_2025_ELC) <= +inf + 0 <= v_flow_in(A_2025_summer_day_ELC_E_BATT_2020_ELC) <= +inf + 0 <= v_flow_in(A_2020_winter_night_ELC_E_BATT_2020_ELC) <= +inf + 0 <= v_flow_in(A_2025_summer_night_ELC_E_BATT_2025_ELC) <= +inf + 0 <= v_flow_in(A_2020_summer_day_ELC_E_BATT_2020_ELC) <= +inf + 0 <= v_storage_init(A_2025_summer_E_BATT_2020) <= +inf + 0 <= v_storage_init(A_2025_winter_E_BATT_2020) <= +inf + 0 <= v_storage_init(A_2020_summer_E_BATT_2020) <= +inf + 0 <= v_storage_init(A_2020_winter_E_BATT_2020) <= +inf + 0 <= v_storage_init(A_2025_summer_E_BATT_2025) <= +inf + 0 <= v_storage_init(A_2025_winter_E_BATT_2025) <= +inf + 0 <= v_storage_level(A_2020_winter_night_E_BATT_2020) <= +inf + 0 <= v_storage_level(A_2020_summer_night_E_BATT_2020) <= +inf + 0 <= v_storage_level(A_2025_winter_day_E_BATT_2020) <= +inf + 0 <= v_storage_level(A_2025_summer_night_E_BATT_2025) <= +inf + 0 <= v_storage_level(A_2025_summer_day_E_BATT_2025) <= +inf + 0 <= v_storage_level(A_2025_winter_night_E_BATT_2020) <= +inf + 0 <= v_storage_level(A_2025_winter_day_E_BATT_2025) <= +inf + 0 <= v_storage_level(A_2020_winter_day_E_BATT_2020) <= +inf + 0 <= v_storage_level(A_2025_summer_night_E_BATT_2020) <= +inf + 0 <= v_storage_level(A_2025_summer_day_E_BATT_2020) <= +inf + 0 <= v_storage_level(A_2020_summer_day_E_BATT_2020) <= +inf + 0 <= v_storage_level(A_2025_winter_night_E_BATT_2025) <= +inf +end diff --git a/tests/testing_data/reserve_margins.sql b/tests/testing_data/reserve_margins.sql new file mode 100644 index 00000000..3e1a5606 --- /dev/null +++ b/tests/testing_data/reserve_margins.sql @@ -0,0 +1,162 @@ +REPLACE INTO "metadata" VALUES('DB_MAJOR',4,''); +REPLACE INTO "metadata" VALUES('DB_MINOR',1,''); +REPLACE INTO "metadata_real" VALUES('global_discount_rate',0.05,''); +REPLACE INTO "metadata_real" VALUES('default_loan_rate',0.05,''); +REPLACE INTO "time_period" VALUES(0,2015,'e'); +REPLACE INTO "time_period" VALUES(1,2020,'f'); +REPLACE INTO "time_period" VALUES(2,2025,'f'); +REPLACE INTO "time_period" VALUES(3,2030,'f'); +REPLACE INTO "time_period_type" VALUES('e','existing'); +REPLACE INTO "time_period_type" VALUES('f','future'); +REPLACE INTO "time_season" VALUES(0,'summer',0.5,NULL); +REPLACE INTO "time_season" VALUES(1,'winter',0.5,NULL); +REPLACE INTO "time_of_day" VALUES(0,'day',12,NULL); +REPLACE INTO "time_of_day" VALUES(1,'night',12,NULL); +REPLACE INTO "region" VALUES('A',NULL); +REPLACE INTO "region" VALUES('B',NULL); +REPLACE INTO "region" VALUES('C',NULL); +REPLACE INTO "commodity_type" VALUES('s','source'); +REPLACE INTO "commodity_type" VALUES('a','annual'); +REPLACE INTO "commodity_type" VALUES('p','physical'); +REPLACE INTO "commodity_type" VALUES('d','demand'); +REPLACE INTO "commodity_type" VALUES('e','emissions'); +REPLACE INTO "commodity_type" VALUES('w','waste'); +REPLACE INTO "commodity_type" VALUES('wa','waste annual'); +REPLACE INTO "commodity_type" VALUES('wp','waste physical'); +REPLACE INTO "commodity" VALUES('ethos','s',NULL,NULL); +REPLACE INTO "commodity" VALUES('NG','p',NULL,NULL); +REPLACE INTO "commodity" VALUES('ELC','p',NULL,NULL); +REPLACE INTO "commodity" VALUES('ELC_C','a',NULL,NULL); -- annual electricity in C +REPLACE INTO "commodity" VALUES('HEAT','d',NULL,NULL); +REPLACE INTO "commodity" VALUES('HEAT_C','d',NULL,NULL); +REPLACE INTO "technology_type" VALUES('p','production'); +REPLACE INTO "technology_type" VALUES('pb','baseload production'); +REPLACE INTO "technology_type" VALUES('ps','storage production'); +REPLACE INTO "technology" VALUES('NGCC','p',NULL,NULL,NULL, 0,0,0,0,0,0,0, 'combined-cycle gas'); +REPLACE INTO "technology" VALUES('SOLPV','p',NULL,NULL,NULL, 0,0,1,0,0,0,0, 'solar photovoltaic'); +REPLACE INTO "technology" VALUES('E_BATT','ps',NULL,NULL,NULL,0,0,0,0,0,0,0, 'grid battery'); +REPLACE INTO "technology" VALUES('ANN_IMP','p',NULL,NULL,NULL,0,1,0,0,0,0,0, 'capacitated annual import'); +REPLACE INTO "technology" VALUES('HEAT_SYS','p',NULL,NULL,NULL,0,0,0,0,0,0,0,'electric heater'); +REPLACE INTO "technology" VALUES('HEAT_ANN','p',NULL,NULL,NULL,0,1,0,0,0,0,0,'annual electric heater'); +REPLACE INTO "technology" VALUES('IMP_NG','p',NULL,NULL,NULL, 1,0,0,0,0,0,0, 'unlimited NG import'); +REPLACE INTO "technology" VALUES('E_TRANS','p',NULL,NULL,NULL,0,0,0,0,0,1,0, 'interregional transmission'); +REPLACE INTO "tech_group" VALUES('elec_A', 'generators + storage + load in A'); +REPLACE INTO "tech_group" VALUES('elec_AB', 'generators + storage + exchange + load across A and B'); +REPLACE INTO "tech_group" VALUES('elec_C_ann', 'annual import + load in C'); +REPLACE INTO "tech_group_member" VALUES('elec_A', 'NGCC'); +REPLACE INTO "tech_group_member" VALUES('elec_A', 'SOLPV'); +REPLACE INTO "tech_group_member" VALUES('elec_A', 'E_BATT'); +REPLACE INTO "tech_group_member" VALUES('elec_A', 'HEAT_SYS'); +REPLACE INTO "tech_group_member" VALUES('elec_A', 'E_TRANS'); +REPLACE INTO "tech_group_member" VALUES('elec_AB', 'NGCC'); +REPLACE INTO "tech_group_member" VALUES('elec_AB', 'SOLPV'); +REPLACE INTO "tech_group_member" VALUES('elec_AB', 'E_BATT'); +REPLACE INTO "tech_group_member" VALUES('elec_AB', 'HEAT_SYS'); +REPLACE INTO "tech_group_member" VALUES('elec_AB', 'E_TRANS'); +REPLACE INTO "tech_group_member" VALUES('elec_C_ann', 'ANN_IMP'); +REPLACE INTO "tech_group_member" VALUES('elec_C_ann', 'HEAT_ANN'); +REPLACE INTO "efficiency" VALUES('A','ethos','IMP_NG', 2020,'NG', 1.0,NULL,NULL); +REPLACE INTO "efficiency" VALUES('A','NG', 'NGCC', 2015,'ELC',0.55,NULL,NULL); +REPLACE INTO "efficiency" VALUES('A','NG', 'NGCC', 2020,'ELC',0.55,NULL,NULL); +REPLACE INTO "efficiency" VALUES('A','NG', 'NGCC', 2025,'ELC',0.55,NULL,NULL); +REPLACE INTO "efficiency" VALUES('A','ethos','SOLPV', 2015,'ELC',1.0,NULL,NULL); +REPLACE INTO "efficiency" VALUES('A','ethos','SOLPV', 2020,'ELC',1.0,NULL,NULL); +REPLACE INTO "efficiency" VALUES('A','ethos','SOLPV', 2025,'ELC',1.0,NULL,NULL); +REPLACE INTO "efficiency" VALUES('A','ELC', 'E_BATT', 2020,'ELC',0.85,NULL,NULL); +REPLACE INTO "efficiency" VALUES('A','ELC', 'E_BATT', 2025,'ELC',0.85,NULL,NULL); +REPLACE INTO "efficiency" VALUES('A','ELC', 'HEAT_SYS',2020,'HEAT',1.0,NULL,NULL); +REPLACE INTO "efficiency" VALUES('A','ELC', 'HEAT_SYS',2025,'HEAT',1.0,NULL,NULL); +REPLACE INTO "efficiency" VALUES('B','ethos','IMP_NG', 2020,'NG', 1.0,NULL,NULL); +REPLACE INTO "efficiency" VALUES('B','NG', 'NGCC', 2015,'ELC',0.55,NULL,NULL); +REPLACE INTO "efficiency" VALUES('B','NG', 'NGCC', 2020,'ELC',0.55,NULL,NULL); +REPLACE INTO "efficiency" VALUES('B','NG', 'NGCC', 2025,'ELC',0.55,NULL,NULL); +REPLACE INTO "efficiency" VALUES('B','ELC', 'HEAT_SYS',2020,'HEAT',1.0,NULL,NULL); +REPLACE INTO "efficiency" VALUES('B','ELC', 'HEAT_SYS',2025,'HEAT',1.0,NULL,NULL); +REPLACE INTO "efficiency" VALUES('C','ethos','ANN_IMP', 2020,'ELC_C',1.0,NULL,NULL); +REPLACE INTO "efficiency" VALUES('C','ELC_C','HEAT_ANN', 2020,'HEAT_C',1.0,NULL,NULL); +REPLACE INTO "efficiency" VALUES('A-B','ELC','E_TRANS',2015,'ELC',0.95,NULL,NULL); +REPLACE INTO "efficiency" VALUES('B-A','ELC','E_TRANS',2015,'ELC',0.95,NULL,NULL); +REPLACE INTO "efficiency" VALUES('A-C','ELC','E_TRANS',2015,'ELC',0.95,NULL,NULL); +REPLACE INTO "efficiency" VALUES('C-A','ELC','E_TRANS',2015,'ELC',0.95,NULL,NULL); +REPLACE INTO "efficiency" VALUES('B-C','ELC','E_TRANS',2015,'ELC',0.95,NULL,NULL); +REPLACE INTO "efficiency" VALUES('C-B','ELC','E_TRANS',2015,'ELC',0.95,NULL,NULL); +REPLACE INTO "capacity_to_activity" VALUES('A','IMP_NG', 1.0,NULL,NULL); +REPLACE INTO "capacity_to_activity" VALUES('A','NGCC', 8.76,NULL,NULL); -- GW → PJ/yr (8760 h) +REPLACE INTO "capacity_to_activity" VALUES('A','SOLPV', 8.76,NULL,NULL); +REPLACE INTO "capacity_to_activity" VALUES('A','E_BATT', 8.76,NULL,NULL); +REPLACE INTO "capacity_to_activity" VALUES('A','HEAT_SYS',1.0,NULL,NULL); +REPLACE INTO "capacity_to_activity" VALUES('B','IMP_NG', 1.0,NULL,NULL); +REPLACE INTO "capacity_to_activity" VALUES('B','NGCC', 8.76,NULL,NULL); +REPLACE INTO "capacity_to_activity" VALUES('B','HEAT_SYS',1.0,NULL,NULL); +REPLACE INTO "capacity_to_activity" VALUES('C','ANN_IMP',1.0,NULL,NULL); +REPLACE INTO "capacity_to_activity" VALUES('C','HEAT_ANN',1.0,NULL,NULL); +REPLACE INTO "capacity_to_activity" VALUES('A-B','E_TRANS',8.76,NULL,NULL); +REPLACE INTO "capacity_to_activity" VALUES('B-A','E_TRANS',8.76,NULL,NULL); +REPLACE INTO "capacity_to_activity" VALUES('A-C','E_TRANS',8.76,NULL,NULL); +REPLACE INTO "capacity_to_activity" VALUES('C-A','E_TRANS',8.76,NULL,NULL); +REPLACE INTO "capacity_to_activity" VALUES('B-C','E_TRANS',8.76,NULL,NULL); +REPLACE INTO "capacity_to_activity" VALUES('C-B','E_TRANS',8.76,NULL,NULL); +REPLACE INTO "storage_duration" VALUES('A','E_BATT',4.0,NULL); +REPLACE INTO "capacity_factor_tech" VALUES('A','summer','day', 'SOLPV',0.6,NULL); +REPLACE INTO "capacity_factor_tech" VALUES('A','summer','night','SOLPV',0.0,NULL); +REPLACE INTO "capacity_factor_tech" VALUES('A','winter','day', 'SOLPV',0.3,NULL); +REPLACE INTO "capacity_factor_tech" VALUES('A','winter','night','SOLPV',0.0,NULL); +REPLACE INTO "existing_capacity" VALUES('A','NGCC', 2015,0.5,'GW',NULL); +REPLACE INTO "existing_capacity" VALUES('A','SOLPV', 2015,0.2,'GW',NULL); +REPLACE INTO "existing_capacity" VALUES('B','NGCC', 2015,0.3,'GW',NULL); +REPLACE INTO "existing_capacity" VALUES('A-B','E_TRANS',2015,1.0,'GW',NULL); +REPLACE INTO "existing_capacity" VALUES('B-A','E_TRANS',2015,1.0,'GW',NULL); +REPLACE INTO "existing_capacity" VALUES('A-C','E_TRANS',2015,1.0,'GW',NULL); +REPLACE INTO "existing_capacity" VALUES('C-A','E_TRANS',2015,1.0,'GW',NULL); +REPLACE INTO "existing_capacity" VALUES('B-C','E_TRANS',2015,1.0,'GW',NULL); +REPLACE INTO "existing_capacity" VALUES('C-B','E_TRANS',2015,1.0,'GW',NULL); +REPLACE INTO "lifetime_tech" VALUES('A','IMP_NG', 100.0,NULL,NULL); +REPLACE INTO "lifetime_tech" VALUES('A','NGCC', 30.0, NULL,NULL); +REPLACE INTO "lifetime_tech" VALUES('A','SOLPV', 25.0, NULL,NULL); +REPLACE INTO "lifetime_tech" VALUES('A','E_BATT', 15.0, NULL,NULL); +REPLACE INTO "lifetime_tech" VALUES('A','HEAT_SYS',20.0, NULL,NULL); +REPLACE INTO "lifetime_tech" VALUES('B','IMP_NG', 100.0,NULL,NULL); +REPLACE INTO "lifetime_tech" VALUES('B','NGCC', 30.0, NULL,NULL); +REPLACE INTO "lifetime_tech" VALUES('B','HEAT_SYS',20.0, NULL,NULL); +REPLACE INTO "lifetime_tech" VALUES('C','ANN_IMP', 100.0,NULL,NULL); +REPLACE INTO "lifetime_tech" VALUES('C','HEAT_ANN',20.0, NULL,NULL); +REPLACE INTO "lifetime_tech" VALUES('A-B','E_TRANS',50.0,NULL,NULL); +REPLACE INTO "lifetime_tech" VALUES('B-A','E_TRANS',50.0,NULL,NULL); +REPLACE INTO "lifetime_tech" VALUES('C-A','E_TRANS',50.0,NULL,NULL); +REPLACE INTO "lifetime_tech" VALUES('A-C','E_TRANS',50.0,NULL,NULL); +REPLACE INTO "lifetime_tech" VALUES('B-C','E_TRANS',50.0,NULL,NULL); +REPLACE INTO "lifetime_tech" VALUES('C-B','E_TRANS',50.0,NULL,NULL); +REPLACE INTO "demand" VALUES('A',2020,'HEAT',5.0,NULL,NULL); +REPLACE INTO "demand" VALUES('A',2025,'HEAT',5.5,NULL,NULL); +REPLACE INTO "demand" VALUES('B',2020,'HEAT',3.0,NULL,NULL); +REPLACE INTO "demand" VALUES('B',2025,'HEAT',3.3,NULL,NULL); +REPLACE INTO "demand" VALUES('C',2020,'HEAT_C',1.0,NULL,NULL); +REPLACE INTO "demand" VALUES('C',2025,'HEAT_C',1.1,NULL,NULL); +REPLACE INTO "planning_reserve_credit" VALUES('A', 'NGCC', 1.0, NULL); +REPLACE INTO "planning_reserve_credit" VALUES('A', 'SOLPV', 0.15,NULL); +REPLACE INTO "planning_reserve_credit" VALUES('A', 'E_BATT', 0.9, NULL); +REPLACE INTO "planning_reserve_credit" VALUES('A', 'HEAT_SYS',0.0,NULL); +REPLACE INTO "planning_reserve_credit" VALUES('B', 'NGCC', 1.0, NULL); +REPLACE INTO "planning_reserve_credit" VALUES('B', 'HEAT_SYS',0.0,NULL); +REPLACE INTO "planning_reserve_credit" VALUES('A-B', 'E_TRANS',1.0, NULL); +REPLACE INTO "planning_reserve_credit" VALUES('B-A', 'E_TRANS',1.0, NULL); +REPLACE INTO "planning_reserve_credit" VALUES('C-B', 'E_TRANS',1.0, NULL); +REPLACE INTO "planning_reserve_credit" VALUES('B-C', 'E_TRANS',1.0, NULL); +REPLACE INTO "planning_reserve_credit" VALUES('C-A', 'E_TRANS',1.0, NULL); +REPLACE INTO "planning_reserve_credit" VALUES('A-C', 'E_TRANS',1.0, NULL); +REPLACE INTO "planning_reserve_credit" VALUES('C', 'ANN_IMP',1.0, NULL); +REPLACE INTO "planning_reserve_credit" VALUES('C', 'HEAT_ANN',0.0,NULL); +REPLACE INTO "planning_reserve_margin" VALUES('A', 'NGCC', 0.15,NULL); +REPLACE INTO "planning_reserve_margin" VALUES('A+B', 'elec_AB', 0.10,NULL); +REPLACE INTO "planning_reserve_margin" VALUES('C', 'elec_C_ann',0.20,NULL); +REPLACE INTO "operating_reserve_derate" VALUES('A','summer','NGCC', 0.95,NULL); +REPLACE INTO "operating_reserve_derate" VALUES('A','winter','NGCC', 0.90,NULL); +REPLACE INTO "operating_reserve_derate" VALUES('A','summer','SOLPV', 0.80,NULL); +REPLACE INTO "operating_reserve_derate" VALUES('A','winter','SOLPV', 0.50,NULL); +REPLACE INTO "operating_reserve_derate" VALUES('A','summer','E_BATT',0.90,NULL); +REPLACE INTO "operating_reserve_derate" VALUES('A','winter','E_BATT',0.90,NULL); +REPLACE INTO "operating_reserve_derate" VALUES('B','summer','NGCC', 0.95,NULL); +REPLACE INTO "operating_reserve_derate" VALUES('B','winter','NGCC', 0.90,NULL); +REPLACE INTO "operating_reserve_margin" VALUES('A', 'elec_A', 0.10,NULL); +REPLACE INTO "operating_reserve_margin" VALUES('B', 'NGCC', 0.10,NULL); +REPLACE INTO "operating_reserve_margin" VALUES('A+B','elec_AB', 0.08,NULL); diff --git a/tests/utilities/compare_lp.py b/tests/utilities/compare_lp.py new file mode 100644 index 00000000..f147f93f --- /dev/null +++ b/tests/utilities/compare_lp.py @@ -0,0 +1,275 @@ +""" +Lightweight LP comparison helper, extracted from compare_lp.py. + +Parses two LP files and returns a structured diff, without writing reports +to disk. Intended for use by the reserve margin regression test. +""" + +from __future__ import annotations + +import re +from collections import defaultdict +from dataclasses import dataclass, field +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from pathlib import Path + +_CAMEL_RE = re.compile(r'([a-z0-9])([A-Z])') +_TERM_RE = re.compile(r'^([+\-]?\s*\d*\.?\d+(?:[eE][+\-]?\d+)?)\s+(\S+)$') +_RELATION_RE = re.compile(r'^([<>=]=?)\s*([\d.eE+\-]+)$') + + +def _camel_to_snake(s: str) -> str: + return _CAMEL_RE.sub(r'\1_\2', s).lower() + + +def _norm(token: str) -> str: + paren = token.find('(') + if paren == -1: + return _camel_to_snake(token) + return _camel_to_snake(token[:paren]) + token[paren:] + + +def _needs_normalization(path: Path, sample: int = 2000) -> bool: + checked = 0 + with path.open(encoding='utf-8', errors='replace') as fh: + for raw in fh: + s = raw.strip() + if not s or s.startswith('\\') or s.startswith('*'): + continue + line = re.sub(r'^([+\-])\s+', r'\1', s) + m = _TERM_RE.match(line) + token = None + if m: + token = m.group(2) + elif s.endswith(':') and not s.startswith(('+', '-')): + token = s.rstrip(':').rstrip('_') + if token: + paren = token.find('(') + name = token[:paren] if paren != -1 else token + if _CAMEL_RE.search(name): + return True + checked += 1 + if checked >= sample: + break + return False + + +def _stream(path: Path, normalize: bool): + tok = _norm if normalize else (lambda t: t) + section = 'preamble' + obj_name = None + obj_terms: list[tuple[float, str]] = [] + con_label = None + con_terms: list[tuple[float, str]] = [] + con_rel = None + con_rhs = None + + def _flush(): + nonlocal con_label, con_terms, con_rel, con_rhs + result = None + if con_label and con_rel is not None: + result = ('con', con_label, list(con_terms), con_rel, con_rhs) + con_label = None + con_terms = [] + con_rel = None + con_rhs = None + return result + + with path.open(encoding='utf-8', errors='replace') as fh: + for raw in fh: + s = raw.strip() + if not s: + continue + lower = s.lower() + + if lower in ('s.t.', 'subject to', 'st'): + if section == 'objective' and obj_terms: + yield ('obj', obj_name, obj_terms) + section = 'constraints' + continue + + if lower in ('bounds', 'generals', 'general', 'binary', 'binaries', 'end'): + r = _flush() + if r: + yield r + if section == 'objective' and obj_terms: + yield ('obj', obj_name, obj_terms) + yield ('bounds_start',) + section = 'done' + continue + + if section == 'done' or s.startswith('\\') or s.startswith('*'): + continue + + if section == 'preamble': + if lower in ('min', 'max'): + section = 'objective' + continue + + if section == 'objective': + if s.endswith(':') and not s.startswith(('+', '-')): + obj_name = tok(s.rstrip(':')) + continue + line = re.sub(r'^([+\-])\s+', r'\1', s) + m = _TERM_RE.match(line) + if m: + obj_terms.append((float(m.group(1).replace(' ', '')), tok(m.group(2)))) + continue + + if section == 'constraints': + if s.endswith(':') and not s.startswith(('+', '-')): + r = _flush() + if r: + yield r + con_label = tok(s.rstrip(':').rstrip('_')) + continue + m = _RELATION_RE.match(s) + if m: + con_rel, con_rhs = m.group(1), float(m.group(2)) + continue + line = re.sub(r'^([+\-])\s+', r'\1', s) + m = _TERM_RE.match(line) + if m: + con_terms.append((float(m.group(1).replace(' ', '')), tok(m.group(2)))) + + r = _flush() + if r: + yield r + if section == 'objective' and obj_terms: + yield ('obj', obj_name, obj_terms) + + +def _to_dict(terms: list[tuple[float, str]]) -> dict[str, float]: + d: dict[str, float] = defaultdict(float) + for c, v in terms: + d[v] += c + return dict(d) + + +@dataclass +class ConstraintDiff: + label: str + relation_changed: tuple[str, str] | None = None # (rel1, rel2) + rhs_changed: tuple[float, float] | None = None # (rhs1, rhs2) + terms_added: dict[str, float] = field(default_factory=dict) + terms_removed: dict[str, float] = field(default_factory=dict) + terms_changed: dict[str, tuple[float, float]] = field(default_factory=dict) + + def __bool__(self) -> bool: + return bool( + self.relation_changed + or self.rhs_changed + or self.terms_added + or self.terms_removed + or self.terms_changed + ) + + +@dataclass +class LpDiff: + only_in_a: list[str] = field(default_factory=list) + only_in_b: list[str] = field(default_factory=list) + changed: list[ConstraintDiff] = field(default_factory=list) + obj_diff: list[str] = field(default_factory=list) # human-readable lines + + @property + def is_identical(self) -> bool: + return not (self.only_in_a or self.only_in_b or self.changed or self.obj_diff) + + def summary(self) -> str: + lines = [ + f'only in A : {len(self.only_in_a)}', + f'only in B : {len(self.only_in_b)}', + f'changed : {len(self.changed)}', + f'obj diffs : {len(self.obj_diff)}', + ] + if self.only_in_a: + lines.append(' A-only (first 10): ' + ', '.join(self.only_in_a[:10])) + if self.only_in_b: + lines.append(' B-only (first 10): ' + ', '.join(self.only_in_b[:10])) + for cd in self.changed[:5]: + lines.append(f' changed: {cd.label}') + if cd.relation_changed: + lines.append(f' relation: {cd.relation_changed[0]} → {cd.relation_changed[1]}') + if cd.rhs_changed: + lines.append(f' rhs: {cd.rhs_changed[0]:+g} → {cd.rhs_changed[1]:+g}') + for v, (c1, c2) in list(cd.terms_changed.items())[:3]: + lines.append(f' coeff {v}: {c1:+g} → {c2:+g}') + return '\n'.join(lines) + + +def compare_lp_files(path_a: Path, path_b: Path, rtol: float = 1e-6) -> LpDiff: + """ + Compare two LP files and return a structured diff. + + Auto-detects CamelCase vs snake_case naming and normalises both sides + when either file uses CamelCase, so mixed-format comparisons work correctly. + """ + apply_norm = _needs_normalization(path_a) or _needs_normalization(path_b) + + # Load file A into memory + cons_a: dict[str, tuple[dict[str, float], str, float]] = {} + obj_a: tuple[str | None, dict[str, float]] | None = None + for block in _stream(path_a, apply_norm): + if block[0] == 'obj': + obj_a = (block[1], _to_dict(block[2])) + elif block[0] == 'con': + _, label, terms, rel, rhs = block + cons_a[label] = (_to_dict(terms), rel, rhs) + elif block[0] == 'bounds_start': + break + + diff = LpDiff() + seen: set[str] = set() + obj_b: tuple[str | None, dict[str, float]] | None = None + + for block in _stream(path_b, apply_norm): + if block[0] == 'obj': + obj_b = (block[1], _to_dict(block[2])) + elif block[0] == 'con': + _, label, terms, rel, rhs = block + td = _to_dict(terms) + if label not in cons_a: + diff.only_in_b.append(label) + else: + seen.add(label) + td_a, rel_a, rhs_a = cons_a[label] + cd = ConstraintDiff(label=label) + if rel_a != rel: + cd.relation_changed = (rel_a, rel) + if abs(rhs_a - rhs) > rtol * max(abs(rhs_a), abs(rhs), 1e-15): + cd.rhs_changed = (rhs_a, rhs) + all_vars = set(td_a) | set(td) + for v in all_vars: + c1, c2 = td_a.get(v), td.get(v) + if c1 is None: + cd.terms_added[v] = c2 # type: ignore[assignment] + elif c2 is None: + cd.terms_removed[v] = c1 + else: + tol = rtol * max(abs(c1), abs(c2), 1e-15) + if abs(c1 - c2) > tol: + cd.terms_changed[v] = (c1, c2) + if cd: + diff.changed.append(cd) + elif block[0] == 'bounds_start': + break + + diff.only_in_a = [lbl for lbl in cons_a if lbl not in seen] + + if obj_a and obj_b: + all_vars = sorted(set(obj_a[1]) | set(obj_b[1])) + for v in all_vars: + c1, c2 = obj_a[1].get(v), obj_b[1].get(v) + if c1 is None: + diff.obj_diff.append(f'ADDED {v} coeff={c2:+g}') + elif c2 is None: + diff.obj_diff.append(f'REMOVED {v} coeff={c1:+g}') + else: + tol = rtol * max(abs(c1), abs(c2), 1e-15) + if abs(c1 - c2) > tol: + diff.obj_diff.append(f'CHANGED {v} {c1:+g} → {c2:+g}') + + return diff From 0442f6b78a66c1554a28c84a4a6406200520727e Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Sat, 8 Aug 2026 08:27:23 -0400 Subject: [PATCH 12/24] Remember to increment db minor version Signed-off-by: Davey Elder --- temoa/db_schema/temoa_schema_v4_1.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/temoa/db_schema/temoa_schema_v4_1.sql b/temoa/db_schema/temoa_schema_v4_1.sql index 28040777..eca18c1b 100644 --- a/temoa/db_schema/temoa_schema_v4_1.sql +++ b/temoa/db_schema/temoa_schema_v4_1.sql @@ -11,7 +11,7 @@ CREATE TABLE IF NOT EXISTS metadata REPLACE INTO metadata VALUES ('DB_MAJOR', 4, 'DB major version number'); REPLACE INTO metadata -VALUES ('DB_MINOR', 0, 'DB minor version number'); +VALUES ('DB_MINOR', 1, 'DB minor version number'); CREATE TABLE IF NOT EXISTS metadata_real ( From abb291ee8c88d2c925d6777e29135d42ba6fad4d Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Sat, 8 Aug 2026 09:03:25 -0400 Subject: [PATCH 13/24] Add v4 to v4.1 migrator and test for it Signed-off-by: Davey Elder --- temoa/utilities/migrate_v4_to_v4_1.py | 313 +++++++++++++++++++++++ tests/test_v4_1_migration.py | 155 +++++++++++ tests/testing_data/migration_v4_mock.sql | 56 ++++ 3 files changed, 524 insertions(+) create mode 100644 temoa/utilities/migrate_v4_to_v4_1.py create mode 100644 tests/test_v4_1_migration.py create mode 100644 tests/testing_data/migration_v4_mock.sql diff --git a/temoa/utilities/migrate_v4_to_v4_1.py b/temoa/utilities/migrate_v4_to_v4_1.py new file mode 100644 index 00000000..918add55 --- /dev/null +++ b/temoa/utilities/migrate_v4_to_v4_1.py @@ -0,0 +1,313 @@ +#!/usr/bin/env python3 +""" +migrate_v4_to_v4_1.py + +Migrates a Temoa v4 database (SQLite or SQL dump) to v4.1 schema format. + +Key changes from v4 to v4.1: + - capacity_credit -> planning_reserve_credit (period, vintage dropped; AVG credit) + - reserve_capacity_derate -> operating_reserve_derate (vintage dropped; AVG factor) + - planning_reserve_margin -> planning_reserve_margin (tech_or_group from reserve-flagged techs) + - rps_requirement -> limit_activity_share (tech_group=sub_group, reserve=super_group) + - operating_reserve_margin is new (no v4 equivalent) + - DB_MINOR bumped: 0 -> 1 +""" + +from __future__ import annotations + +import argparse +import os +import sqlite3 +import tempfile +from pathlib import Path + + +def get_table_cols(conn: sqlite3.Connection, table: str) -> list[str]: + return [r[1] for r in conn.execute(f'PRAGMA table_info({table})').fetchall()] + + +def _migrate_planning_reserve_credit( + con_old: sqlite3.Connection, con_new: sqlite3.Connection +) -> int: + """Migrate capacity_credit -> planning_reserve_credit, dropping period and vintage.""" + try: + rows = con_old.execute( + 'SELECT region, tech, AVG(credit), notes FROM capacity_credit GROUP BY region, tech' + ).fetchall() + except sqlite3.OperationalError: + return 0 + if not rows: + return 0 + con_new.executemany( + 'INSERT OR REPLACE INTO planning_reserve_credit (region, tech, credit, notes) ' + 'VALUES (?, ?, ?, ?)', + rows, + ) + print(f'Migrated {len(rows)} rows: capacity_credit -> planning_reserve_credit') + return len(rows) + + +def _migrate_operating_reserve_derate( + con_old: sqlite3.Connection, con_new: sqlite3.Connection +) -> int: + """Migrate reserve_capacity_derate -> operating_reserve_derate, dropping vintage.""" + try: + rows = con_old.execute( + 'SELECT region, season, tech, AVG(factor), notes ' + 'FROM reserve_capacity_derate GROUP BY region, season, tech' + ).fetchall() + except sqlite3.OperationalError: + return 0 + if not rows: + return 0 + con_new.executemany( + 'INSERT OR REPLACE INTO operating_reserve_derate (region, season, tech, factor, notes) ' + 'VALUES (?, ?, ?, ?, ?)', + rows, + ) + print(f'Migrated {len(rows)} rows: reserve_capacity_derate -> operating_reserve_derate') + return len(rows) + + +RESERVE_GROUP_NAME = 'migrated_reserve_techs' + + +def _build_reserve_tech_group( + con_old: sqlite3.Connection, con_new: sqlite3.Connection +) -> list[str]: + """Create a tech_group from techs with reserve=1; return the list of reserve tech names.""" + try: + reserve_techs = [ + r[0] + for r in con_old.execute('SELECT tech FROM technology WHERE reserve = 1').fetchall() + ] + except sqlite3.OperationalError: + return [] + if not reserve_techs: + return [] + con_new.execute( + 'INSERT OR IGNORE INTO tech_group (group_name) VALUES (?)', (RESERVE_GROUP_NAME,) + ) + con_new.executemany( + 'INSERT OR IGNORE INTO tech_group_member (group_name, tech) VALUES (?, ?)', + [(RESERVE_GROUP_NAME, t) for t in reserve_techs], + ) + print(f'Built reserve tech group "{RESERVE_GROUP_NAME}" with {len(reserve_techs)} member(s)') + return reserve_techs + + +def _migrate_planning_reserve_margin( + con_old: sqlite3.Connection, con_new: sqlite3.Connection, reserve_group_built: bool +) -> int: + """Migrate planning_reserve_margin using the reserve tech group as tech_or_group.""" + try: + rows = con_old.execute( + 'SELECT region, margin, notes FROM planning_reserve_margin' + ).fetchall() + except sqlite3.OperationalError: + return 0 + if not rows: + return 0 + if not reserve_group_built: + print( + f'WARNING: planning_reserve_margin has {len(rows)} row(s) but no reserve-flagged ' + 'techs found; skipping migration. Populate planning_reserve_margin manually.' + ) + return 0 + migrated = [(region, RESERVE_GROUP_NAME, margin, notes) for region, margin, notes in rows] + con_new.executemany( + 'INSERT OR REPLACE INTO planning_reserve_margin (region, tech_or_group, margin, notes) ' + 'VALUES (?, ?, ?, ?)', + migrated, + ) + print( + f'Migrated {len(migrated)} rows: planning_reserve_margin' + f' (tech_or_group={RESERVE_GROUP_NAME!r})' + ) + return len(migrated) + + +def _migrate_rps_requirement( + con_old: sqlite3.Connection, con_new: sqlite3.Connection, reserve_group_built: bool +) -> int: + """Migrate rps_requirement -> limit_activity_share. + + sub_group = tech_group (the RPS-eligible group) + super_group = RESERVE_GROUP_NAME (all reserve-flagged techs) + operator = 'ge' (must meet minimum share) + """ + try: + rows = con_old.execute( + 'SELECT region, period, tech_group, requirement, notes FROM rps_requirement' + ).fetchall() + except sqlite3.OperationalError: + return 0 + if not rows: + return 0 + if not reserve_group_built: + print( + 'WARNING: rps_requirement has rows but no reserve-flagged techs found to use as ' + 'super_group; skipping migration. Populate limit_activity_share manually.' + ) + return 0 + migrated = [ + (region, period, sub_group, RESERVE_GROUP_NAME, 'ge', requirement, notes) + for region, period, sub_group, requirement, notes in rows + ] + con_new.executemany( + 'INSERT OR REPLACE INTO limit_activity_share ' + '(region, period, sub_group, super_group, operator, share, notes) ' + 'VALUES (?, ?, ?, ?, ?, ?, ?)', + migrated, + ) + print(f'Migrated {len(migrated)} rows: rps_requirement -> limit_activity_share') + return len(migrated) + + +def _migrate_common_tables(con_old: sqlite3.Connection, con_new: sqlite3.Connection) -> int: + """Copy all tables that exist in both schemas, skipping custom-handled ones.""" + skip = { + 'capacity_credit', + 'reserve_capacity_derate', + 'rps_requirement', + 'planning_reserve_margin', + 'metadata', + 'metadata_real', + 'operator', + 'commodity_type', + } + old_tables = { + r[0] + for r in con_old.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall() + } + new_tables = { + r[0] + for r in con_new.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall() + } + total = 0 + for table in sorted(old_tables & new_tables): + if table.startswith('sqlite_') or table in skip: + continue + old_cols = set(get_table_cols(con_old, table)) + new_cols = get_table_cols(con_new, table) + shared = [c for c in new_cols if c in old_cols] + if not shared: + continue + col_list = ', '.join(shared) + rows = con_old.execute(f'SELECT {col_list} FROM {table}').fetchall() + if not rows: + continue + placeholders = ', '.join(['?'] * len(shared)) + con_new.executemany( + f'INSERT OR REPLACE INTO {table} ({col_list}) VALUES ({placeholders})', rows + ) + print(f'Copied {len(rows)} rows: {table}') + total += len(rows) + return total + + +def execute_v4_to_v4_1_migration(con_old: sqlite3.Connection, con_new: sqlite3.Connection) -> None: + """Run all v4 -> v4.1 migration steps.""" + total = 0 + print('--- Migrating common tables ---') + total += _migrate_common_tables(con_old, con_new) + + print('--- Migrating restructured tables ---') + total += _migrate_planning_reserve_credit(con_old, con_new) + total += _migrate_operating_reserve_derate(con_old, con_new) + + print('--- Building reserve tech group ---') + reserve_techs = _build_reserve_tech_group(con_old, con_new) + reserve_group_built = len(reserve_techs) > 0 + total += _migrate_planning_reserve_margin(con_old, con_new, reserve_group_built) + total += _migrate_rps_requirement(con_old, con_new, reserve_group_built) + + con_new.execute("INSERT OR REPLACE INTO metadata VALUES ('DB_MAJOR', 4, 'DB major version')") + con_new.execute("INSERT OR REPLACE INTO metadata VALUES ('DB_MINOR', 1, 'DB minor version')") + print(f'Total rows successfully copied: {total}') + + +def migrate_database(source_path: Path, schema_path: Path, output_path: Path) -> None: + if not source_path.is_file(): + raise FileNotFoundError(f'Input database not found: {source_path}') + if not schema_path.is_file(): + raise FileNotFoundError(f'Schema file not found: {schema_path}') + + fd, temp_str = tempfile.mkstemp( + suffix='.sqlite', prefix='temp_v4_1_migration_', dir=output_path.parent + ) + os.close(fd) + temp_path = Path(temp_str) + + con_old = sqlite3.connect(source_path) + con_new = sqlite3.connect(temp_path) + try: + con_new.executescript(schema_path.read_text(encoding='utf-8')) + con_new.execute('PRAGMA foreign_keys = 0;') + execute_v4_to_v4_1_migration(con_old, con_new) + con_new.commit() + con_new.execute('PRAGMA foreign_keys = 1;') + con_old.close() + con_new.close() + os.replace(temp_path, output_path) + except Exception: + if temp_path.exists(): + os.remove(temp_path) + raise + finally: + con_old.close() + con_new.close() + + +def migrate_sql_dump(source_path: Path, schema_path: Path, output_path: Path) -> None: + if not source_path.is_file(): + raise FileNotFoundError(f'Input SQL dump not found: {source_path}') + if not schema_path.is_file(): + raise FileNotFoundError(f'Schema file not found: {schema_path}') + + con_old = sqlite3.connect(':memory:') + con_old.executescript(source_path.read_text(encoding='utf-8')) + + con_new = sqlite3.connect(':memory:') + con_new.executescript(schema_path.read_text(encoding='utf-8')) + + con_new.execute('PRAGMA foreign_keys = 0;') + execute_v4_to_v4_1_migration(con_old, con_new) + con_new.commit() + con_new.execute('PRAGMA foreign_keys = 1;') + + fd, temp_str = tempfile.mkstemp(suffix='.sql', prefix='temp_v4_1_sql_', dir=output_path.parent) + temp_path = Path(temp_str) + try: + with os.fdopen(fd, 'w', encoding='utf-8') as f: + for line in con_new.iterdump(): + f.write(line + '\n') + f.flush() + os.fsync(f.fileno()) + os.replace(temp_path, output_path) + except Exception: + if temp_path.exists(): + os.remove(temp_path) + raise + finally: + con_old.close() + con_new.close() + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Migrate Temoa database from v4 to v4.1') + parser.add_argument('--input', '-i', required=True, help='Input DB or SQL file') + parser.add_argument('--schema', '-s', required=True, help='Path to v4.1 schema SQL') + parser.add_argument('--output', '-o', required=True, help='Output DB or SQL file') + parser.add_argument('--type', choices=['db', 'sql'], required=True, help='Migration type') + args = parser.parse_args() + + input_path = Path(args.input) + schema_path = Path(args.schema) + output_path = Path(args.output) + + if args.type == 'db': + migrate_database(input_path, schema_path, output_path) + else: + migrate_sql_dump(input_path, schema_path, output_path) + print('Migration complete.') diff --git a/tests/test_v4_1_migration.py b/tests/test_v4_1_migration.py new file mode 100644 index 00000000..a71bb23e --- /dev/null +++ b/tests/test_v4_1_migration.py @@ -0,0 +1,155 @@ +import contextlib +import sqlite3 +import subprocess +import sys +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).parents[1] +UTILITIES_DIR = REPO_ROOT / 'temoa' / 'utilities' +SCHEMA_V4 = REPO_ROOT / 'temoa' / 'db_schema' / 'temoa_schema_v4.sql' +SCHEMA_V4_1 = REPO_ROOT / 'temoa' / 'db_schema' / 'temoa_schema_v4_1.sql' +MOCK_DATA_V4 = REPO_ROOT / 'tests' / 'testing_data' / 'migration_v4_mock.sql' +MIGRATION_SCRIPT = UTILITIES_DIR / 'migrate_v4_to_v4_1.py' + + +def _make_v4_db(tmp_path: Path) -> Path: + """Create a v4 SQLite database populated with mock data.""" + db = tmp_path / 'test_v4.sqlite' + with contextlib.closing(sqlite3.connect(db)) as conn: + conn.execute('PRAGMA foreign_keys = OFF') + conn.executescript(SCHEMA_V4.read_text()) + conn.executescript(MOCK_DATA_V4.read_text()) + conn.execute('PRAGMA foreign_keys = ON') + return db + + +def _verify_migrated_db(conn: sqlite3.Connection) -> None: + # Metadata version updated + major = conn.execute("SELECT value FROM metadata WHERE element='DB_MAJOR'").fetchone()[0] + minor = conn.execute("SELECT value FROM metadata WHERE element='DB_MINOR'").fetchone()[0] + assert major == 4 + assert minor == 1 + + # planning_reserve_credit: capacity_credit aggregated by (region, tech), period/vintage dropped + credits = { + (r[0], r[1]): r[2] + for r in conn.execute('SELECT region, tech, credit FROM planning_reserve_credit').fetchall() + } + # GasTurbine had credit 0.85 (2030) and 0.80 (2040) -> AVG = 0.825 + assert ('R1', 'GasTurbine') in credits + assert credits[('R1', 'GasTurbine')] == pytest.approx(0.825) + assert credits[('R1', 'WindFarm')] == pytest.approx(0.25) + + # operating_reserve_derate: reserve_capacity_derate aggregated by (region, season, tech) + derates = { + (r[0], r[1], r[2]): r[3] + for r in conn.execute( + 'SELECT region, season, tech, factor FROM operating_reserve_derate' + ).fetchall() + } + assert ('R1', 'summer', 'GasTurbine') in derates + assert derates[('R1', 'summer', 'GasTurbine')] == pytest.approx(0.95) + assert derates[('R1', 'summer', 'WindFarm')] == pytest.approx(0.20) + assert derates[('R1', 'winter', 'WindFarm')] == pytest.approx(0.10) + + # Removed v4 tables must not be present + tables = { + r[0] for r in conn.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall() + } + assert 'capacity_credit' not in tables + assert 'reserve_capacity_derate' not in tables + assert 'rps_requirement' not in tables + + # planning_reserve_margin: migrated using reserve tech group as tech_or_group + from temoa.utilities.migrate_v4_to_v4_1 import RESERVE_GROUP_NAME + + margins = conn.execute( + 'SELECT region, tech_or_group, margin FROM planning_reserve_margin' + ).fetchall() + assert len(margins) == 1 + assert margins[0] == ('R1', RESERVE_GROUP_NAME, pytest.approx(0.15)) + + # reserve tech group must exist with GasTurbine and WindFarm (reserve=1), not CoalPlant + members = { + r[0] + for r in conn.execute( + 'SELECT tech FROM tech_group_member WHERE group_name = ?', (RESERVE_GROUP_NAME,) + ).fetchall() + } + assert 'GasTurbine' in members + assert 'WindFarm' in members + assert 'CoalPlant' not in members + + # rps_requirement: migrated to limit_activity_share, one row per period + activity_shares = { + (r[0], r[1], r[2]): r[4] + for r in conn.execute( + 'SELECT region, period, sub_group, super_group, share FROM limit_activity_share' + ).fetchall() + } + assert ('R1', 2030, 'renewables') in activity_shares + assert activity_shares[('R1', 2030, 'renewables')] == pytest.approx(0.30) + assert activity_shares[('R1', 2040, 'renewables')] == pytest.approx(0.40) + + # Common table data preserved + efficiencies = conn.execute('SELECT region, tech, vintage FROM efficiency').fetchall() + assert len(efficiencies) == 3 + + +def test_v4_1_migration_db(tmp_path: Path) -> None: + """Test SQLite DB migration from v4 to v4.1.""" + db_v4 = _make_v4_db(tmp_path) + db_v4_1 = tmp_path / 'test_v4_1.sqlite' + + subprocess.run( + [ + sys.executable, + str(MIGRATION_SCRIPT), + '--type', + 'db', + '--input', + str(db_v4), + '--schema', + str(SCHEMA_V4_1), + '--output', + str(db_v4_1), + ], + check=True, + ) + + with contextlib.closing(sqlite3.connect(db_v4_1)) as conn: + _verify_migrated_db(conn) + + +def test_v4_1_migration_sql(tmp_path: Path) -> None: + """Test SQL dump migration from v4 to v4.1.""" + db_v4 = _make_v4_db(tmp_path) + + sql_v4 = tmp_path / 'test_v4.sql' + with open(sql_v4, 'w') as f: + with contextlib.closing(sqlite3.connect(db_v4)) as conn: + for line in conn.iterdump(): + f.write(line + '\n') + + sql_v4_1 = tmp_path / 'test_v4_1.sql' + subprocess.run( + [ + sys.executable, + str(MIGRATION_SCRIPT), + '--type', + 'sql', + '--input', + str(sql_v4), + '--schema', + str(SCHEMA_V4_1), + '--output', + str(sql_v4_1), + ], + check=True, + ) + + with contextlib.closing(sqlite3.connect(':memory:')) as conn: + conn.executescript(sql_v4_1.read_text()) + _verify_migrated_db(conn) diff --git a/tests/testing_data/migration_v4_mock.sql b/tests/testing_data/migration_v4_mock.sql new file mode 100644 index 00000000..2379b482 --- /dev/null +++ b/tests/testing_data/migration_v4_mock.sql @@ -0,0 +1,56 @@ +-- Mock data for v4 -> v4.1 migration testing +PRAGMA foreign_keys = OFF; +INSERT OR IGNORE INTO commodity_type (label, description) VALUES ('p', 'physical'); +INSERT OR IGNORE INTO commodity_type (label, description) VALUES ('e', 'emissions'); +INSERT OR IGNORE INTO sector_label (sector) VALUES ('electric'); + +INSERT INTO commodity (name, flag) VALUES ('FuelIn', 'p'); +INSERT INTO commodity (name, flag) VALUES ('EnergyOut', 'p'); + +INSERT INTO time_period (period, flag) VALUES (2020, 'e'); +INSERT INTO time_period (period, flag) VALUES (2030, 'f'); +INSERT INTO time_period (period, flag) VALUES (2040, 'f'); + +INSERT INTO time_season (season, segment_fraction) VALUES ('summer', 0.6); +INSERT INTO time_season (season, segment_fraction) VALUES ('winter', 0.4); + +INSERT INTO time_of_day (tod, hours) VALUES ('day', 14.4); +INSERT INTO time_of_day (tod, hours) VALUES ('night', 9.6); + +INSERT OR IGNORE INTO technology_type (label, description) VALUES ('p', 'production'); +INSERT INTO technology (tech, flag, reserve) VALUES ('GasTurbine', 'p', 1); +INSERT INTO technology (tech, flag, reserve) VALUES ('WindFarm', 'p', 1); +INSERT INTO technology (tech, flag, reserve) VALUES ('CoalPlant', 'p', 0); + +INSERT INTO efficiency (region, input_comm, tech, vintage, output_comm, efficiency) + VALUES ('R1', 'FuelIn', 'GasTurbine', 2030, 'EnergyOut', 0.4); +INSERT INTO efficiency (region, input_comm, tech, vintage, output_comm, efficiency) + VALUES ('R1', 'FuelIn', 'WindFarm', 2030, 'EnergyOut', 1.0); +INSERT INTO efficiency (region, input_comm, tech, vintage, output_comm, efficiency) + VALUES ('R1', 'FuelIn', 'CoalPlant', 2030, 'EnergyOut', 0.35); + +-- capacity_credit will migrate to planning_reserve_credit (dropping period, vintage) +INSERT INTO capacity_credit (region, period, tech, vintage, credit) + VALUES ('R1', 2030, 'GasTurbine', 2030, 0.85); +INSERT INTO capacity_credit (region, period, tech, vintage, credit) + VALUES ('R1', 2040, 'GasTurbine', 2030, 0.80); +INSERT INTO capacity_credit (region, period, tech, vintage, credit) + VALUES ('R1', 2030, 'WindFarm', 2030, 0.25); + +-- reserve_capacity_derate will migrate to operating_reserve_derate (dropping vintage) +INSERT INTO reserve_capacity_derate (region, season, tech, vintage, factor) + VALUES ('R1', 'summer', 'GasTurbine', 2030, 0.95); +INSERT INTO reserve_capacity_derate (region, season, tech, vintage, factor) + VALUES ('R1', 'summer', 'WindFarm', 2030, 0.20); +INSERT INTO reserve_capacity_derate (region, season, tech, vintage, factor) + VALUES ('R1', 'winter', 'WindFarm', 2030, 0.10); + +-- planning_reserve_margin will migrate using reserve-flagged tech group as tech_or_group +INSERT INTO planning_reserve_margin (region, margin) VALUES ('R1', 0.15); + +-- rps_requirement will migrate to limit_activity_share +INSERT INTO tech_group (group_name) VALUES ('renewables'); +INSERT INTO rps_requirement (region, period, tech_group, requirement) + VALUES ('R1', 2030, 'renewables', 0.30); +INSERT INTO rps_requirement (region, period, tech_group, requirement) + VALUES ('R1', 2040, 'renewables', 0.40); From 5dff78bc91f8e1274d21a8db1ef6f17c51dc9928 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Sat, 8 Aug 2026 09:37:58 -0400 Subject: [PATCH 14/24] Fix typing errors in testing folder Signed-off-by: Davey Elder --- tests/conftest.py | 1 + tests/test_reserve_margins.py | 8 ++++---- tests/utilities/compare_lp.py | 19 ++++++++++--------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 8bd2ca76..91c00c21 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -101,6 +101,7 @@ def refresh_databases() -> None: # Feature tests (separate for temporal consistency) ('emissions.sql', 'emissions.sqlite'), ('materials.sql', 'materials.sqlite'), + ('reserve_margins.sql', 'reserve_margins.sqlite'), ('simple_linked_tech.sql', 'simple_linked_tech.sqlite'), ('storageville.sql', 'storageville.sqlite'), ('test_week.sql', 'test_week.sqlite'), diff --git a/tests/test_reserve_margins.py b/tests/test_reserve_margins.py index cfc3f435..0dcab64a 100644 --- a/tests/test_reserve_margins.py +++ b/tests/test_reserve_margins.py @@ -44,7 +44,7 @@ def reserve_run(tmp_path_factory: pytest.TempPathFactory) -> tuple[TemoaModel, P return instance, lp_files[0] -def test_planning_ab_group_includes_exchange(reserve_run: tuple[TemoaModel, Path]): +def test_planning_ab_group_includes_exchange(reserve_run: tuple[TemoaModel, Path]) -> None: model, _ = reserve_run exchange_regions = { r @@ -58,7 +58,7 @@ def test_planning_ab_group_includes_exchange(reserve_run: tuple[TemoaModel, Path ) -def test_single_region_a_includes_exchange(reserve_run: tuple[TemoaModel, Path]): +def test_single_region_a_includes_exchange(reserve_run: tuple[TemoaModel, Path]) -> None: model, _ = reserve_run exchange_regions = { r @@ -72,7 +72,7 @@ def test_single_region_a_includes_exchange(reserve_run: tuple[TemoaModel, Path]) ) -def test_single_tech_region_a_not_includes_exchange(reserve_run: tuple[TemoaModel, Path]): +def test_single_tech_region_a_not_includes_exchange(reserve_run: tuple[TemoaModel, Path]) -> None: model, _ = reserve_run exchange_regions = { r @@ -87,7 +87,7 @@ def test_single_tech_region_a_not_includes_exchange(reserve_run: tuple[TemoaMode ) -def test_lp_matches(reserve_run: tuple[TemoaModel, Path]): +def test_lp_matches(reserve_run: tuple[TemoaModel, Path]) -> None: _, lp_path = reserve_run if not CACHED_LP.exists(): diff --git a/tests/utilities/compare_lp.py b/tests/utilities/compare_lp.py index f147f93f..5090a7f5 100644 --- a/tests/utilities/compare_lp.py +++ b/tests/utilities/compare_lp.py @@ -10,9 +10,10 @@ import re from collections import defaultdict from dataclasses import dataclass, field -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: + from collections.abc import Generator from pathlib import Path _CAMEL_RE = re.compile(r'([a-z0-9])([A-Z])') @@ -56,20 +57,20 @@ def _needs_normalization(path: Path, sample: int = 2000) -> bool: return False -def _stream(path: Path, normalize: bool): +def _stream(path: Path, normalize: bool) -> Generator[tuple[Any, ...]]: tok = _norm if normalize else (lambda t: t) section = 'preamble' obj_name = None obj_terms: list[tuple[float, str]] = [] - con_label = None + con_label: str | None = None con_terms: list[tuple[float, str]] = [] - con_rel = None - con_rhs = None + con_rel: str | None = None + con_rhs: float | None = None - def _flush(): + def _flush() -> tuple[str, str, list[tuple[float, str]], str | None, float | None] | None: nonlocal con_label, con_terms, con_rel, con_rhs - result = None - if con_label and con_rel is not None: + result: tuple[str, str, list[tuple[float, str]], str | None, float | None] | None = None + if con_label is not None and con_rel is not None: result = ('con', con_label, list(con_terms), con_rel, con_rhs) con_label = None con_terms = [] @@ -241,7 +242,7 @@ def compare_lp_files(path_a: Path, path_b: Path, rtol: float = 1e-6) -> LpDiff: cd.relation_changed = (rel_a, rel) if abs(rhs_a - rhs) > rtol * max(abs(rhs_a), abs(rhs), 1e-15): cd.rhs_changed = (rhs_a, rhs) - all_vars = set(td_a) | set(td) + all_vars = sorted(set(td_a) | set(td)) for v in all_vars: c1, c2 = td_a.get(v), td.get(v) if c1 is None: From 42bcc8c7bebb26dc7139e1bfeeed8963151a78db Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Thu, 20 Aug 2026 13:17:47 -0400 Subject: [PATCH 15/24] Fix a log format Signed-off-by: Davey Elder --- temoa/components/reserves.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/temoa/components/reserves.py b/temoa/components/reserves.py index 5e0f51d2..3fa12a6a 100644 --- a/temoa/components/reserves.py +++ b/temoa/components/reserves.py @@ -142,7 +142,8 @@ def initialize_reserve_margins(model: TemoaModel) -> None: else: logger.info( 'Planning reserve margin %s has no contributors in period %s', - ((r_g, t_g), p), + (r_g, t_g), + p, ) if not any( From 3cbbe84a3b5abced14df5f30a47b5ae573141120 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Thu, 20 Aug 2026 13:18:35 -0400 Subject: [PATCH 16/24] Increment minor version of test dbs Signed-off-by: Davey Elder --- tests/testing_data/annualised_demand.sql | 2 +- tests/testing_data/emissions.sql | 2 +- tests/testing_data/materials.sql | 2 +- tests/testing_data/mediumville.sql | 2 +- tests/testing_data/myopic_capacities.sql | 2 +- tests/testing_data/seasonal_storage.sql | 2 +- tests/testing_data/simple_linked_tech.sql | 2 +- tests/testing_data/storageville.sql | 2 +- tests/testing_data/survival_curve.sql | 2 +- tests/testing_data/test_system.sql | 2 +- tests/testing_data/test_week.sql | 2 +- tests/testing_data/utopia_data.sql | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/testing_data/annualised_demand.sql b/tests/testing_data/annualised_demand.sql index 0be2f5eb..f2407ac4 100644 --- a/tests/testing_data/annualised_demand.sql +++ b/tests/testing_data/annualised_demand.sql @@ -32,7 +32,7 @@ REPLACE INTO "limit_activity" VALUES('region',2000,'non_annual','le',0.5,NULL,NU REPLACE INTO "loan_lifetime_process" VALUES('region', 'annual', 2000, 1.0, NULL, NULL); REPLACE INTO "loan_lifetime_process" VALUES('region', 'non_annual', 2000, 1.0, NULL, NULL); REPLACE INTO "metadata" VALUES('DB_MAJOR',4,''); -REPLACE INTO "metadata" VALUES('DB_MINOR',0,''); +REPLACE INTO "metadata" VALUES('DB_MINOR',1,''); REPLACE INTO "metadata_real" VALUES('global_discount_rate',0.05,'Discount Rate for future costs'); REPLACE INTO "metadata_real" VALUES('default_loan_rate',0.05,'Default Loan Rate if not specified in loan_rate table'); REPLACE INTO "operator" VALUES('e','equal to'); diff --git a/tests/testing_data/emissions.sql b/tests/testing_data/emissions.sql index 5b6a6596..1d2bcc6c 100644 --- a/tests/testing_data/emissions.sql +++ b/tests/testing_data/emissions.sql @@ -68,7 +68,7 @@ REPLACE INTO "limit_capacity" VALUES('Testregion',2000,'TechCurtailment','ge',1. REPLACE INTO "limit_capacity" VALUES('Testregion',2000,'TechOrdinary','le',1.0,NULL,NULL); REPLACE INTO "limit_capacity" VALUES('Testregion',2000,'TechCurtailment','le',1.0,NULL,NULL); REPLACE INTO "metadata" VALUES('DB_MAJOR',4,''); -REPLACE INTO "metadata" VALUES('DB_MINOR',0,''); +REPLACE INTO "metadata" VALUES('DB_MINOR',1,''); REPLACE INTO "metadata_real" VALUES('global_discount_rate',0.05,'Discount Rate for future costs'); REPLACE INTO "metadata_real" VALUES('default_loan_rate',0.05,'Default Loan Rate if not specified in loan_rate table'); REPLACE INTO "operator" VALUES('e','equal to'); diff --git a/tests/testing_data/materials.sql b/tests/testing_data/materials.sql index 90837130..b9631c9f 100644 --- a/tests/testing_data/materials.sql +++ b/tests/testing_data/materials.sql @@ -330,7 +330,7 @@ REPLACE INTO "limit_tech_input_split_annual" VALUES('regionB',2010,'diesel','CAR REPLACE INTO "limit_tech_input_split_annual" VALUES('regionB',2020,'electricity','CAR_PHEV','le',0.2,''); REPLACE INTO "limit_tech_input_split_annual" VALUES('regionB',2020,'diesel','CAR_PHEV','le',0.8,NULL); REPLACE INTO "metadata" VALUES('DB_MAJOR',4,''); -REPLACE INTO "metadata" VALUES('DB_MINOR',0,''); +REPLACE INTO "metadata" VALUES('DB_MINOR',1,''); REPLACE INTO "metadata_real" VALUES('global_discount_rate',0.05,'Discount Rate for future costs'); REPLACE INTO "metadata_real" VALUES('default_loan_rate',0.05,'Default Loan Rate if not specified in loan_rate table'); REPLACE INTO "operator" VALUES('e','equal to'); diff --git a/tests/testing_data/mediumville.sql b/tests/testing_data/mediumville.sql index 42b4af67..4538c788 100644 --- a/tests/testing_data/mediumville.sql +++ b/tests/testing_data/mediumville.sql @@ -141,7 +141,7 @@ REPLACE INTO "loan_lifetime_process" VALUES('B', 'GeoThermal', 2025, 10.0, NULL, REPLACE INTO "loan_lifetime_process" VALUES('A-B', 'FGF_pipe', 2025, 10.0, NULL, NULL); REPLACE INTO "loan_lifetime_process" VALUES('B-A', 'FGF_pipe', 2025, 10.0, NULL, NULL); REPLACE INTO "metadata" VALUES('DB_MAJOR',4,''); -REPLACE INTO "metadata" VALUES('DB_MINOR',0,''); +REPLACE INTO "metadata" VALUES('DB_MINOR',1,''); 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',4.2000000000000004e-01,''); REPLACE INTO "operator" VALUES('e','equal to'); diff --git a/tests/testing_data/myopic_capacities.sql b/tests/testing_data/myopic_capacities.sql index 65c122ab..d9f03b2e 100644 --- a/tests/testing_data/myopic_capacities.sql +++ b/tests/testing_data/myopic_capacities.sql @@ -1,5 +1,5 @@ REPLACE INTO metadata VALUES('DB_MAJOR',4,''); -REPLACE INTO metadata VALUES('DB_MINOR',0,''); +REPLACE INTO metadata VALUES('DB_MINOR',1,''); 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); diff --git a/tests/testing_data/seasonal_storage.sql b/tests/testing_data/seasonal_storage.sql index a1cbad53..55ba285d 100644 --- a/tests/testing_data/seasonal_storage.sql +++ b/tests/testing_data/seasonal_storage.sql @@ -43,7 +43,7 @@ REPLACE INTO "efficiency" VALUES('region', 'electricity', 'demand', 2000, 'deman REPLACE INTO "limit_storage_level_fraction" VALUES('region','winter','b','seas_stor','e',0.5,NULL); REPLACE INTO "limit_storage_level_fraction" VALUES('region','charge','b','dly_stor','e',0.5,NULL); REPLACE INTO "metadata" VALUES('DB_MAJOR',4,''); -REPLACE INTO "metadata" VALUES('DB_MINOR',0,''); +REPLACE INTO "metadata" VALUES('DB_MINOR',1,''); REPLACE INTO "metadata_real" VALUES('global_discount_rate',0.05,'Discount Rate for future costs'); REPLACE INTO "metadata_real" VALUES('default_loan_rate',0.05,'Default Loan Rate if not specified in loan_rate table'); REPLACE INTO "operator" VALUES('e','equal to'); diff --git a/tests/testing_data/simple_linked_tech.sql b/tests/testing_data/simple_linked_tech.sql index 025443e6..34e502b4 100644 --- a/tests/testing_data/simple_linked_tech.sql +++ b/tests/testing_data/simple_linked_tech.sql @@ -28,7 +28,7 @@ REPLACE INTO "lifetime_tech" VALUES('linkville', 'CCS', 100.0, NULL, ''); REPLACE INTO "lifetime_tech" VALUES('linkville', 'PLANT', 100.0, NULL, ''); REPLACE INTO "linked_tech" VALUES('linkville','PLANT','CO2','CCS',NULL); REPLACE INTO "metadata" VALUES('DB_MAJOR',4,''); -REPLACE INTO "metadata" VALUES('DB_MINOR',0,''); +REPLACE INTO "metadata" VALUES('DB_MINOR',1,''); 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 "operator" VALUES('e','equal to'); diff --git a/tests/testing_data/storageville.sql b/tests/testing_data/storageville.sql index c7c68125..3d33f461 100644 --- a/tests/testing_data/storageville.sql +++ b/tests/testing_data/storageville.sql @@ -42,7 +42,7 @@ REPLACE INTO "limit_capacity" VALUES('electricville',2025,'EH','le',200.0,'','') REPLACE INTO "limit_capacity" VALUES('electricville',2025,'batt','le',100.0,'',''); REPLACE INTO "limit_storage_level_fraction" VALUES('electricville','s1','d1','batt','e',0.5,NULL); REPLACE INTO "metadata" VALUES('DB_MAJOR',4,''); -REPLACE INTO "metadata" VALUES('DB_MINOR',0,''); +REPLACE INTO "metadata" VALUES('DB_MINOR',1,''); 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 "operator" VALUES('e','equal to'); diff --git a/tests/testing_data/survival_curve.sql b/tests/testing_data/survival_curve.sql index cd2e5a4e..864d9de3 100644 --- a/tests/testing_data/survival_curve.sql +++ b/tests/testing_data/survival_curve.sql @@ -143,7 +143,7 @@ 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 "metadata" VALUES('DB_MAJOR',4,''); -REPLACE INTO "metadata" VALUES('DB_MINOR',0,''); +REPLACE INTO "metadata" VALUES('DB_MINOR',1,''); REPLACE INTO "metadata_real" VALUES('global_discount_rate',0.05,'Discount Rate for future costs'); REPLACE INTO "metadata_real" VALUES('default_loan_rate',0.05,'Default Loan Rate if not specified in loan_rate table'); REPLACE INTO "operator" VALUES('e','equal to'); diff --git a/tests/testing_data/test_system.sql b/tests/testing_data/test_system.sql index 2d9b11de..7bf6bbbd 100644 --- a/tests/testing_data/test_system.sql +++ b/tests/testing_data/test_system.sql @@ -438,7 +438,7 @@ REPLACE INTO "limit_tech_output_split" VALUES('R2',2025,'S_OILREF','DSL','ge',0. REPLACE INTO "limit_tech_output_split" VALUES('R2',2030,'S_OILREF','GSL','ge',0.72,''); REPLACE INTO "limit_tech_output_split" VALUES('R2',2030,'S_OILREF','DSL','ge',0.08,''); REPLACE INTO "metadata" VALUES('DB_MAJOR',4,''); -REPLACE INTO "metadata" VALUES('DB_MINOR',0,''); +REPLACE INTO "metadata" VALUES('DB_MINOR',1,''); 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 "operator" VALUES('e','equal to'); diff --git a/tests/testing_data/test_week.sql b/tests/testing_data/test_week.sql index ad80b36c..d22900ef 100644 --- a/tests/testing_data/test_week.sql +++ b/tests/testing_data/test_week.sql @@ -1,5 +1,5 @@ REPLACE INTO metadata VALUES('DB_MAJOR',4,'DB major version number'); -REPLACE INTO metadata VALUES('DB_MINOR',0,'DB minor version number'); +REPLACE INTO metadata VALUES('DB_MINOR',1,'DB minor version number'); 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 LoanRate table'); REPLACE INTO sector_label VALUES('energy',NULL); diff --git a/tests/testing_data/utopia_data.sql b/tests/testing_data/utopia_data.sql index 0c552165..66afeaef 100644 --- a/tests/testing_data/utopia_data.sql +++ b/tests/testing_data/utopia_data.sql @@ -414,7 +414,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" VALUES('DB_MAJOR',4,''); -REPLACE INTO "metadata" VALUES('DB_MINOR',0,''); +REPLACE INTO "metadata" VALUES('DB_MINOR',1,''); 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 "operator" VALUES('e','equal to'); From 5dce07e11cdab4b61de693fbe0947440afb612aa Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Thu, 20 Aug 2026 13:27:23 -0400 Subject: [PATCH 17/24] Remove period filter from derate/credit tables Signed-off-by: Davey Elder --- temoa/data_io/component_manifest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/temoa/data_io/component_manifest.py b/temoa/data_io/component_manifest.py index 0db52163..620c7f8d 100644 --- a/temoa/data_io/component_manifest.py +++ b/temoa/data_io/component_manifest.py @@ -495,6 +495,7 @@ def build_manifest(model: TemoaModel, extension_ids: Sequence[str] | None = None columns=['region', 'tech', 'credit'], validator_name='viable_rt', validation_map=(0, 1), + is_period_filtered=False, is_table_required=False, ), LoadItem( @@ -512,6 +513,7 @@ def build_manifest(model: TemoaModel, extension_ids: Sequence[str] | None = None columns=['region', 'season', 'tech', 'factor'], validator_name='viable_rt', validation_map=(0, 2), + is_period_filtered=False, is_table_required=False, ), LoadItem( From 1ba59d90ff074c89c2b57a719394d81f8454f28a Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Thu, 20 Aug 2026 13:27:37 -0400 Subject: [PATCH 18/24] Remove removed dict type Signed-off-by: Davey Elder --- temoa/types/dict_types.py | 1 - 1 file changed, 1 deletion(-) diff --git a/temoa/types/dict_types.py b/temoa/types/dict_types.py index 3ba604c9..477c1f11 100644 --- a/temoa/types/dict_types.py +++ b/temoa/types/dict_types.py @@ -17,7 +17,6 @@ ProcessOutputsByInputDict = dict[ tuple[Region, Period, Technology, Vintage, Commodity], set[Commodity] ] -ProcessTechsDict = dict[tuple[Region, Period, Commodity], set[Technology]] ReserveProcessesDict = dict[ tuple[Region, Period, Technology], set[tuple[Region, Technology, Vintage]] ] From b3e81d7431cd68ad4e86acf7e4b6007d50261f2e Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Thu, 20 Aug 2026 13:27:52 -0400 Subject: [PATCH 19/24] Fix a couple errors in docs Signed-off-by: Davey Elder --- docs/source/database_schema.mmd | 2 +- docs/source/mathematical_formulation.rst | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/source/database_schema.mmd b/docs/source/database_schema.mmd index fef1f47a..421a3ba6 100644 --- a/docs/source/database_schema.mmd +++ b/docs/source/database_schema.mmd @@ -2,7 +2,7 @@ erDiagram planning_reserve_credit { TEXT region PK TEXT tech PK - REAL factor + REAL credit TEXT notes } operating_reserve_derate { diff --git a/docs/source/mathematical_formulation.rst b/docs/source/mathematical_formulation.rst index 3e5fd928..8af0b9f6 100644 --- a/docs/source/mathematical_formulation.rst +++ b/docs/source/mathematical_formulation.rst @@ -817,8 +817,8 @@ The required excess of credited installed capacity above demand, expressed as a fraction of demand, keyed by a region-or-group :math:`r_g` and a technology-or-group :math:`t_g`. For example, a value of 0.2 requires that credited capacity be at least 120% of demand. Demand is estimated from production -by time slice. When a region group is used, any exchange region (e.g. ``r1-r2``) -where exactly one endpoint belongs to the group is automatically included in the +by time slice. Any exchange region (e.g. ``r1-r2``) where exactly one endpoint +connects to the region or group is automatically included in the reserve calculation; this auto-inclusion is unique to the reserve margin constraints. @@ -841,9 +841,9 @@ operating_reserve_margin The dynamic counterpart to :code:`planning_reserve_margin`, indexed the same way by region-or-group and technology-or-group. Rather than crediting installed capacity, it requires that available (derated) generation in each -time slice exceed the region-group's proxy demand by this margin. When a region -group is used, any exchange region (e.g. ``r1-r2``) -where exactly one endpoint belongs to the group is automatically included in the +time slice exceed the region-group's proxy demand by this margin. +Any exchange region (e.g. ``r1-r2``) where exactly one endpoint +connects to the region or group is automatically included in the reserve calculation; this auto-inclusion is unique to the reserve margin constraints. From 4d5260e0a9da231e87c101c3a7ec3808cf1756e9 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Thu, 20 Aug 2026 13:32:32 -0400 Subject: [PATCH 20/24] Fix some issues in v4 to 4.1 migrator Signed-off-by: Davey Elder --- temoa/utilities/migrate_v4_to_v4_1.py | 36 +++++++++++++++++---------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/temoa/utilities/migrate_v4_to_v4_1.py b/temoa/utilities/migrate_v4_to_v4_1.py index 918add55..7d5ed7ef 100644 --- a/temoa/utilities/migrate_v4_to_v4_1.py +++ b/temoa/utilities/migrate_v4_to_v4_1.py @@ -38,6 +38,10 @@ def _migrate_planning_reserve_credit( return 0 if not rows: return 0 + print( + 'WARNING: Dropping period and vintage from capacity_credit; ' + 'using average credit for each region/tech' + ) con_new.executemany( 'INSERT OR REPLACE INTO planning_reserve_credit (region, tech, credit, notes) ' 'VALUES (?, ?, ?, ?)', @@ -60,6 +64,10 @@ def _migrate_operating_reserve_derate( return 0 if not rows: return 0 + print( + 'WARNING: Dropping vintage from reserve_capacity_derate; ' + 'using average factor for each region/season/tech' + ) con_new.executemany( 'INSERT OR REPLACE INTO operating_reserve_derate (region, season, tech, factor, notes) ' 'VALUES (?, ?, ?, ?, ?)', @@ -79,7 +87,7 @@ def _build_reserve_tech_group( try: reserve_techs = [ r[0] - for r in con_old.execute('SELECT tech FROM technology WHERE reserve = 1').fetchall() + for r in con_old.execute('SELECT tech FROM technology WHERE reserve > 0').fetchall() ] except sqlite3.OperationalError: return [] @@ -172,7 +180,6 @@ def _migrate_common_tables(con_old: sqlite3.Connection, con_new: sqlite3.Connect 'rps_requirement', 'planning_reserve_margin', 'metadata', - 'metadata_real', 'operator', 'commodity_type', } @@ -266,19 +273,22 @@ def migrate_sql_dump(source_path: Path, schema_path: Path, output_path: Path) -> raise FileNotFoundError(f'Schema file not found: {schema_path}') con_old = sqlite3.connect(':memory:') - con_old.executescript(source_path.read_text(encoding='utf-8')) - con_new = sqlite3.connect(':memory:') - con_new.executescript(schema_path.read_text(encoding='utf-8')) - - con_new.execute('PRAGMA foreign_keys = 0;') - execute_v4_to_v4_1_migration(con_old, con_new) - con_new.commit() - con_new.execute('PRAGMA foreign_keys = 1;') + temp_path: Path | None = None - fd, temp_str = tempfile.mkstemp(suffix='.sql', prefix='temp_v4_1_sql_', dir=output_path.parent) - temp_path = Path(temp_str) try: + con_old.executescript(source_path.read_text(encoding='utf-8')) + con_new.executescript(schema_path.read_text(encoding='utf-8')) + + con_new.execute('PRAGMA foreign_keys = 0;') + execute_v4_to_v4_1_migration(con_old, con_new) + con_new.commit() + con_new.execute('PRAGMA foreign_keys = 1;') + + fd, temp_str = tempfile.mkstemp( + suffix='.sql', prefix='temp_v4_1_sql_', dir=output_path.parent + ) + temp_path = Path(temp_str) with os.fdopen(fd, 'w', encoding='utf-8') as f: for line in con_new.iterdump(): f.write(line + '\n') @@ -286,7 +296,7 @@ def migrate_sql_dump(source_path: Path, schema_path: Path, output_path: Path) -> os.fsync(f.fileno()) os.replace(temp_path, output_path) except Exception: - if temp_path.exists(): + if temp_path is not None and temp_path.exists(): os.remove(temp_path) raise finally: From 43725b31f4a3ba35cdaf03164923d5fa2a4502f1 Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Thu, 20 Aug 2026 14:54:16 -0400 Subject: [PATCH 21/24] Fix some docs notation Signed-off-by: Davey Elder --- docs/source/mathematical_formulation.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/mathematical_formulation.rst b/docs/source/mathematical_formulation.rst index 8af0b9f6..0dc22926 100644 --- a/docs/source/mathematical_formulation.rst +++ b/docs/source/mathematical_formulation.rst @@ -811,11 +811,11 @@ level to vary. planning_reserve_margin ~~~~~~~~~~~~~~~~~~~~~~~ -:math:`{PRM}_{r_g \in R, t_g \in T}` +:math:`{PRM}_{r \in R, t \in T}` The required excess of credited installed capacity above demand, expressed -as a fraction of demand, keyed by a region-or-group :math:`r_g` and a -technology-or-group :math:`t_g`. For example, a value of 0.2 requires that +as a fraction of demand, keyed by a region-or-group :math:`r` and a +technology-or-group :math:`t`. For example, a value of 0.2 requires that credited capacity be at least 120% of demand. Demand is estimated from production by time slice. Any exchange region (e.g. ``r1-r2``) where exactly one endpoint connects to the region or group is automatically included in the @@ -836,7 +836,7 @@ or solar) receives a lower value. operating_reserve_margin ~~~~~~~~~~~~~~~~~~~~~~~~ -:math:`{ORM}_{r_g \in R, t_g \in T}` +:math:`{ORM}_{r \in R, t \in T}` The dynamic counterpart to :code:`planning_reserve_margin`, indexed the same way by region-or-group and technology-or-group. Rather than crediting From e547a28209b8de68ff3bee2119c1b6c33bddbd1e Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Thu, 20 Aug 2026 17:07:00 -0400 Subject: [PATCH 22/24] Add in-thread migration test for code coverage Signed-off-by: Davey Elder --- tests/test_v4_1_migration.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/test_v4_1_migration.py b/tests/test_v4_1_migration.py index a71bb23e..47e47192 100644 --- a/tests/test_v4_1_migration.py +++ b/tests/test_v4_1_migration.py @@ -6,6 +6,8 @@ import pytest +from temoa.utilities.migrate_v4_to_v4_1 import migrate_database, migrate_sql_dump + REPO_ROOT = Path(__file__).parents[1] UTILITIES_DIR = REPO_ROOT / 'temoa' / 'utilities' SCHEMA_V4 = REPO_ROOT / 'temoa' / 'db_schema' / 'temoa_schema_v4.sql' @@ -153,3 +155,32 @@ def test_v4_1_migration_sql(tmp_path: Path) -> None: with contextlib.closing(sqlite3.connect(':memory:')) as conn: conn.executescript(sql_v4_1.read_text()) _verify_migrated_db(conn) + + +def test_v4_1_migration_db_inthread(tmp_path: Path) -> None: + """In-process variant of test_v4_1_migration_db for coverage.""" + db_v4 = _make_v4_db(tmp_path) + db_v4_1 = tmp_path / 'test_v4_1.sqlite' + + migrate_database(db_v4, SCHEMA_V4_1, db_v4_1) + + with contextlib.closing(sqlite3.connect(db_v4_1)) as conn: + _verify_migrated_db(conn) + + +def test_v4_1_migration_sql_inthread(tmp_path: Path) -> None: + """In-process variant of test_v4_1_migration_sql for coverage.""" + db_v4 = _make_v4_db(tmp_path) + + sql_v4 = tmp_path / 'test_v4.sql' + with open(sql_v4, 'w') as f: + with contextlib.closing(sqlite3.connect(db_v4)) as conn: + for line in conn.iterdump(): + f.write(line + '\n') + + sql_v4_1 = tmp_path / 'test_v4_1.sql' + migrate_sql_dump(sql_v4, SCHEMA_V4_1, sql_v4_1) + + with contextlib.closing(sqlite3.connect(':memory:')) as conn: + conn.executescript(sql_v4_1.read_text()) + _verify_migrated_db(conn) From e8e5274028a73ea2c2635d8bddb34c3253b1d76a Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Fri, 21 Aug 2026 11:59:12 -0400 Subject: [PATCH 23/24] Consolidate schema version updating Signed-off-by: Davey Elder --- docs/source/database.rst | 2 +- temoa/__about__.py | 5 ++++- temoa/cli.py | 10 +++++----- tests/conftest.py | 3 ++- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/docs/source/database.rst b/docs/source/database.rst index 2b2340a9..4f407271 100644 --- a/docs/source/database.rst +++ b/docs/source/database.rst @@ -192,7 +192,7 @@ Supported tables: For help getting started, consider using the ``temoa tutorial`` command to generate a template project or inspect the example SQL file at ``temoa/tutorial_assets/utopia.sql``. To begin building your own database file, use -``temoa/db_schema/temoa_schema_v4.sql``, which is a database file with the requisite +``temoa/db_schema/temoa_schema_v4_1.sql``, which is a database file with the requisite structure but no data added. We recommend leaving the database structure intact, and simply adding data to the schema file, or constructing an empty database from the schema file and then using a script or database editor to import data. diff --git a/temoa/__about__.py b/temoa/__about__.py index 28c809a0..954c4971 100644 --- a/temoa/__about__.py +++ b/temoa/__about__.py @@ -21,4 +21,7 @@ # db is tested for match on major and >= on minor DB_MAJOR_VERSION = 4 -MIN_DB_MINOR_VERSION = 0 +MIN_DB_MINOR_VERSION = 1 + +# Also needs updating in database.rst +DB_SCHEMA = 'temoa_schema_v4_1.sql' diff --git a/temoa/cli.py b/temoa/cli.py index 9feee14d..90cf91f5 100644 --- a/temoa/cli.py +++ b/temoa/cli.py @@ -11,7 +11,7 @@ from rich.logging import RichHandler from rich.text import Text -from temoa.__about__ import __version__ +from temoa.__about__ import DB_SCHEMA, __version__ from temoa._internal.temoa_sequencer import TemoaSequencer from temoa.core.config import TemoaConfig from temoa.core.modes import TemoaMode @@ -137,7 +137,7 @@ def _cite_callback(value: bool) -> None: def get_default_schema() -> Path: """Get the default path to the v4 schema file, handling both installed and development cases.""" try: - schema_path = resources.files('temoa.db_schema') / 'temoa_schema_v4.sql' + schema_path = resources.files('temoa.db_schema') / DB_SCHEMA if not schema_path.is_file(): raise FileNotFoundError( @@ -149,7 +149,7 @@ def get_default_schema() -> Path: # The fallback for development needs to reflect the current repository structure # assuming `cli.py` is in `temoa/` and `db_schema/` is a sibling of `cli.py` within # `temoa/`. - fallback_path = Path(__file__).parent / 'db_schema' / 'temoa_schema_v4.sql' + fallback_path = Path(__file__).parent / 'db_schema' / DB_SCHEMA if fallback_path.is_file(): logger.warning( 'Using fallback schema path: %s. ' @@ -573,7 +573,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' + schema_resource = resources.files('temoa.db_schema') / DB_SCHEMA # Copy configuration file with config_resource.open('rb') as source: @@ -612,7 +612,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' + fallback_schema = Path(__file__).parent / 'db_schema' / DB_SCHEMA if not fallback_config.exists(): raise FileNotFoundError( diff --git a/tests/conftest.py b/tests/conftest.py index 91c00c21..c7c97098 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,6 +7,7 @@ from _pytest.config import Config from pyomo.opt import SolverResults +from temoa.__about__ import DB_SCHEMA from temoa._internal.temoa_sequencer import TemoaSequencer from temoa.core.config import TemoaConfig from temoa.core.model import TemoaModel @@ -37,7 +38,7 @@ # Central paths TEST_DATA_PATH = Path(__file__).parent / 'testing_data' TEST_OUTPUT_PATH = Path(__file__).parent / 'testing_outputs' -SCHEMA_PATH = Path(__file__).parent.parent / 'temoa' / 'db_schema' / 'temoa_schema_v4_1.sql' +SCHEMA_PATH = Path(__file__).parent.parent / 'temoa' / 'db_schema' / DB_SCHEMA def _build_test_db( From dd5baf60e9fb256f44b979d14577af0dad0d43be Mon Sep 17 00:00:00 2001 From: Davey Elder Date: Fri, 21 Aug 2026 12:02:37 -0400 Subject: [PATCH 24/24] Fix tutorial config so it doesnt ask appsi for duals Signed-off-by: Davey Elder --- temoa/tutorial_assets/config_sample.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/temoa/tutorial_assets/config_sample.toml b/temoa/tutorial_assets/config_sample.toml index 02bcf672..4fd91766 100644 --- a/temoa/tutorial_assets/config_sample.toml +++ b/temoa/tutorial_assets/config_sample.toml @@ -87,7 +87,7 @@ solver_name = "appsi_highs" save_excel = true # save the duals in the output Database (may slow execution slightly?) -save_duals = true +save_duals = false # save storage levels by time slice (may be a large amount of data) save_storage_levels = true