# temoa/components/limits.py
"""
Defines the various limit-related components of the Temoa model.
This module contains a wide variety of constraints that enforce
limits on the energy system. These include, but are not limited to:
- Input/Output splits for technologies like refineries.
- Growth and degrowth rates for capacity deployment.
- Shares of capacity or activity for technology groups (e.g., for RPS policies).
- Absolute limits on capacity, new investment, or emissions.
"""
from __future__ import annotations
import sys
from logging import getLogger
from typing import TYPE_CHECKING, cast
from pyomo.environ import Constraint, quicksum, value
import temoa.components.geography as geography
import temoa.components.technology as technology
from temoa.components.utils import Operator, get_variable_efficiency, operator_expression
if TYPE_CHECKING:
from pyomo.core import Expression
from temoa.core.model import TemoaModel
from temoa.types import ExprLike, Period, Region, Technology, Vintage
from temoa.types.core_types import Commodity, Season, TimeOfDay
logger = getLogger(__name__)
# ============================================================================
# PYOMO INDEX SET FUNCTIONS
# ============================================================================
def limit_tech_input_split_constraint_indices(
model: TemoaModel,
) -> set[tuple[Region, Period, Season, TimeOfDay, Commodity, Technology, Vintage, str]]:
indices = {
(r, p, s, d, i, t, v, op)
for r, p, i, t, op in model.input_split_vintages
if t not in model.tech_annual
for v in model.input_split_vintages[r, p, i, t, op]
for s in model.time_season
for d in model.time_of_day
}
ann_indices = {
(r, p, i, t, op) for r, p, i, t, op in model.input_split_vintages if t in model.tech_annual
}
if len(ann_indices) > 0:
msg = (
'Warning: Annual technologies included in limit_tech_input_split table. '
'Use limit_tech_input_split_annual table instead or these constraints will '
'be ignored: {}'
)
logger.warning(msg.format(ann_indices))
return indices
def limit_tech_input_split_annual_constraint_indices(
model: TemoaModel,
) -> set[tuple[Region, Period, Commodity, Technology, Vintage, str]]:
return {
(r, p, i, t, v, op)
for r, p, i, t, op in model.input_split_annual_vintages
if t in model.tech_annual
for v in model.input_split_annual_vintages[r, p, i, t, op]
}
def limit_tech_input_split_average_constraint_indices(
model: TemoaModel,
) -> set[tuple[Region, Period, Commodity, Technology, Vintage, str]]:
return {
(r, p, i, t, v, op)
for r, p, i, t, op in model.input_split_annual_vintages
if t not in model.tech_annual
for v in model.input_split_annual_vintages[r, p, i, t, op]
}
def limit_tech_output_split_constraint_indices(
model: TemoaModel,
) -> set[tuple[Region, Period, Season, TimeOfDay, Technology, Vintage, Commodity, str]]:
indices = {
(r, p, s, d, t, v, o, op)
for r, p, t, o, op in model.output_split_vintages
if t not in model.tech_annual
for v in model.output_split_vintages[r, p, t, o, op]
for s in model.time_season
for d in model.time_of_day
}
ann_indices = {
(r, p, t, o, op) for r, p, t, o, op in model.output_split_vintages if t in model.tech_annual
}
if len(ann_indices) > 0:
msg = (
'Warning: Annual technologies included in limit_tech_output_split table. '
'Use limit_tech_output_split_annual table instead or these constraints will '
'be ignored: {}'
)
logger.warning(msg.format(ann_indices))
return indices
def limit_tech_output_split_annual_constraint_indices(
model: TemoaModel,
) -> set[tuple[Region, Period, Technology, Vintage, Commodity, str]]:
return {
(r, p, t, v, o, op)
for r, p, t, o, op in model.output_split_annual_vintages
if t in model.tech_annual
for v in model.output_split_annual_vintages[r, p, t, o, op]
}
def limit_tech_output_split_average_constraint_indices(
model: TemoaModel,
) -> set[tuple[Region, Period, Technology, Vintage, Commodity, str]]:
return {
(r, p, t, v, o, op)
for r, p, t, o, op in model.output_split_annual_vintages
if t not in model.tech_annual
for v in model.output_split_annual_vintages[r, p, t, o, op]
}
def limit_growth_capacity_indices(model: TemoaModel) -> set[tuple[Region, Period, Technology, str]]:
return {
(r, p, t, op)
for r, t, op in model.limit_growth_capacity.sparse_keys()
for p in model.time_optimize
}
def limit_degrowth_capacity_indices(
model: TemoaModel,
) -> set[tuple[Region, Period, Technology, str]]:
return {
(r, p, t, op)
for r, t, op in model.limit_degrowth_capacity.sparse_keys()
for p in model.time_optimize
}
def limit_growth_new_capacity_indices(
model: TemoaModel,
) -> set[tuple[Region, Period, Technology, str]]:
return {
(r, p, t, op)
for r, t, op in model.limit_growth_new_capacity.sparse_keys()
for p in model.time_optimize
}
def limit_degrowth_new_capacity_indices(
model: TemoaModel,
) -> set[tuple[Region, Period, Technology, str]]:
return {
(r, p, t, op)
for r, t, op in model.limit_degrowth_new_capacity.sparse_keys()
for p in model.time_optimize
}
def limit_growth_new_capacity_delta_indices(
model: TemoaModel,
) -> set[tuple[Region, Period, Technology, str]]:
return {
(r, p, t, op)
for r, t, op in model.limit_growth_new_capacity_delta.sparse_keys()
for p in model.time_optimize
}
def limit_degrowth_new_capacity_delta_indices(
model: TemoaModel,
) -> set[tuple[Region, Period, Technology, str]]:
return {
(r, p, t, op)
for r, t, op in model.limit_degrowth_new_capacity_delta.sparse_keys()
for p in model.time_optimize
}
def limit_seasonal_capacity_factor_constraint_indices(
model: TemoaModel,
) -> set[tuple[Region, Period, Season, Technology, str]]:
"""Expand the period-free param set to include all time_optimize periods."""
return {
(r, p, s, t, op)
for r, s, t, op in model.limit_seasonal_capacity_factor_constraint_rst
for p in model.time_optimize
}
def limit_annual_capacity_factor_indices(
model: TemoaModel,
) -> set[tuple[Region, Period, Technology, Vintage, Commodity, str]]:
return {
(r, p, t, v, o, op)
for r, t, v, o, op in model.limit_annual_capacity_factor_constraint_rtvo
for _r in geography.gather_group_regions(model, r)
for _t in technology.gather_group_techs(model, t)
for p in model.time_optimize
if o in model.process_outputs.get((_r, p, _t, v), [])
}
# ============================================================================
# PYOMO CONSTRAINT RULES
# ============================================================================
# @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
[docs]
def limit_resource_constraint(model: TemoaModel, r: Region, t: Technology, op: str) -> ExprLike:
r"""
The limit_resource constraint sets a limit on the available resource of a
given technology across all model time periods. Note that the indices for these
constraints are region and tech.
.. math::
:label: limit_resource
\sum_{P,S,D,I,V,O} \textbf{FO}_{r, p, s, d, i, t \notin T^a, v, o}
+\sum_{P,I,V,O} \textbf{FOA}_{r, p, i, t \in T^a, v, o}
\quad \le, \ge, \text{or} = \quad LS_{r, t}
\forall \{r, t\} \in \Theta_{\text{limit\_resource}}"""
# dev note: this constraint is a misnomer. It is actually a "global activity constraint on a
# tech" regardless of whatever "resources" are consumed.
# dev note: this would generally be applied to a "dummy import" technology to restrict
# something like oil/mineral extraction across all model periods. Looks fine to me.
regions = geography.gather_group_regions(model, r)
techs = technology.gather_group_techs(model, t)
activity = quicksum(
model.v_flow_out_annual[_r, p, S_i, _t, S_v, S_o]
for _t in techs
if _t in model.tech_annual
for p in model.time_optimize
for _r in regions
if (_r, p, _t) in model.process_vintages
for S_v in model.process_vintages[_r, p, _t]
for S_i in model.process_inputs[_r, p, _t, S_v]
for S_o in model.process_outputs_by_input[_r, p, _t, S_v, S_i]
)
activity += quicksum(
model.v_flow_out[_r, p, s, d, S_i, _t, S_v, S_o]
for _t in techs
if _t not in model.tech_annual
for p in model.time_optimize
for _r in regions
if (_r, p, _t) in model.process_vintages
for S_v in model.process_vintages[_r, p, _t]
for S_i in model.process_inputs[_r, p, _t, S_v]
for S_o in model.process_outputs_by_input[_r, p, _t, S_v, S_i]
for s in model.time_season
for d in model.time_of_day
)
resource_lim = value(model.limit_resource[r, t, op])
expr = operator_expression(activity, Operator(op), resource_lim)
return expr
[docs]
def limit_activity_share_constraint(
model: TemoaModel, r: Region, p: Period, g1: Technology, g2: Technology, op: str
) -> ExprLike:
r"""
Limits the activity of a given technology or group as a fraction of another
technology or group, summed over a period. This can be used to set, for example,
a renewable portfolio scheme constraint.
.. math::
:label: Limit Activity Share
\sum_{R_g \subseteq R,\ S,\ D,\ I,\ (T^{g_1} \setminus T^a) \subseteq T,\ V,\ O}
\mathbf{FO}_{r,p,s,d,i,t,v,o}
+ \sum_{R_g \subseteq R,\ I,\ (T^{g_1} \cap T^a) \subseteq T,\ V,\ O}
\mathbf{FOA}_{r,p,i,t,v,o}
\\
\quad \le, \ge, \text{or} = \quad
\\
LAS_{r,p,g_1,g_2} \cdot
\sum_{R_g \subseteq R,\ S,\ D,\ I,\ (T^{g_2} \setminus T^a) \subseteq T,\ V,\ O}
\mathbf{FO}_{r,p,s,d,i,t,v,o}
+ \sum_{R_g \subseteq R,\ I,\ (T^{g_2} \cap T^a) \subseteq T,\ V,\ O}
\mathbf{FOA}_{r,p,i,t,v,o}
\qquad \forall \{r, p, g_1, g_2\} \in \Theta_{\text{limit\_activity\_share}}
"""
regions = geography.gather_group_regions(model, r)
sub_group = technology.gather_group_techs(model, g1)
sub_activity = quicksum(
model.v_flow_out[_r, p, s, d, S_i, S_t, S_v, S_o]
for S_t in sub_group
if S_t not in model.tech_annual
for _r in regions
for S_v in model.process_vintages.get((_r, p, S_t), [])
for S_i in model.process_inputs[_r, p, S_t, S_v]
for S_o in model.process_outputs_by_input[_r, p, S_t, S_v, S_i]
for s in model.time_season
for d in model.time_of_day
)
sub_activity += quicksum(
model.v_flow_out_annual[_r, p, S_i, S_t, S_v, S_o]
for S_t in sub_group
if S_t in model.tech_annual
for _r in regions
for S_v in model.process_vintages.get((_r, p, S_t), [])
for S_i in model.process_inputs[_r, p, S_t, S_v]
for S_o in model.process_outputs_by_input[_r, p, S_t, S_v, S_i]
)
super_group = technology.gather_group_techs(model, g2)
super_activity = quicksum(
model.v_flow_out[_r, p, s, d, S_i, S_t, S_v, S_o]
for S_t in super_group
if S_t not in model.tech_annual
for _r in regions
for S_v in model.process_vintages.get((_r, p, S_t), [])
for S_i in model.process_inputs[_r, p, S_t, S_v]
for S_o in model.process_outputs_by_input[_r, p, S_t, S_v, S_i]
for s in model.time_season
for d in model.time_of_day
)
super_activity += quicksum(
model.v_flow_out_annual[_r, p, S_i, S_t, S_v, S_o]
for S_t in super_group
if S_t in model.tech_annual
for _r in regions
for S_v in model.process_vintages.get((_r, p, S_t), [])
for S_i in model.process_inputs[_r, p, S_t, S_v]
for S_o in model.process_outputs_by_input[_r, p, S_t, S_v, S_i]
)
share_lim = value(model.limit_activity_share[r, p, g1, g2, op])
expr = operator_expression(sub_activity, Operator(op), share_lim * super_activity)
# in the case that there is nothing to sum, skip
if isinstance(expr, bool): # an empty list was generated
return Constraint.Skip
logger.debug(
'created limit activity share constraint for (%s, %d, %s, %s) of %0.2f',
r,
p,
g1,
g2,
share_lim,
)
return expr
[docs]
def limit_capacity_share_constraint(
model: TemoaModel, r: Region, p: Period, g1: Technology, g2: Technology, op: str
) -> ExprLike:
r"""
The limit_capacity_share constraint limits the available capacity of a given
technology or technology group as a fraction of another technology or group.
"""
regions = geography.gather_group_regions(model, r)
sub_group = technology.gather_group_techs(model, g1)
sub_capacity = quicksum(
model.v_capacity_available_by_period_and_tech[_r, p, _t]
for _t in sub_group
for _r in regions
if (_r, p, _t) in model.process_vintages
)
super_group = technology.gather_group_techs(model, g2)
super_capacity = quicksum(
model.v_capacity_available_by_period_and_tech[_r, p, _t]
for _t in super_group
for _r in regions
if (_r, p, _t) in model.process_vintages
)
share_lim = value(model.limit_capacity_share[r, p, g1, g2, op])
expr = operator_expression(sub_capacity, Operator(op), share_lim * super_capacity)
if isinstance(expr, bool):
return Constraint.Skip
return expr
[docs]
def limit_new_capacity_share_constraint(
model: TemoaModel, r: Region, g1: Technology, g2: Technology, v: Vintage, op: str
) -> ExprLike:
r"""
The limit_new_capacity_share constraint limits the share of new capacity
of a given technology or group as a fraction of another technology or
group."""
regions = geography.gather_group_regions(model, r)
sub_group = technology.gather_group_techs(model, g1)
sub_new_cap = quicksum(
model.v_new_capacity[_r, _t, v]
for _t in sub_group
for _r in regions
if (_r, _t, v) in model.process_periods
)
super_group = technology.gather_group_techs(model, g2)
super_new_cap = quicksum(
model.v_new_capacity[_r, _t, v]
for _t in super_group
for _r in regions
if (_r, _t, v) in model.process_periods
)
share_lim = value(model.limit_new_capacity_share[r, g1, g2, v, op])
expr = operator_expression(sub_new_cap, Operator(op), share_lim * super_new_cap)
if isinstance(expr, bool):
return Constraint.Skip
return expr
[docs]
def limit_annual_capacity_factor_constraint(
model: TemoaModel, r: Region, p: Period, t: Technology, v: Vintage, o: Commodity, op: str
) -> ExprLike:
r"""
The limit_annual_capacity_factor sets an upper bound on the annual capacity factor
from a specific process. The first portion of the constraint pertains to
technologies with variable output at the time slice level, and the second portion
pertains to technologies with constant annual output belonging to the
:code:`tech_annual` set.
.. math::
:label: limit_annual_capacity_factor
\sum_{S,D,I} \textbf{FO}_{r, p, s, d, i, t, v, o}
\quad \le, \ge, \text{or} = \quad
LIMACF_{r, t, v, o} \cdot
\textbf{CAP}_{r, p, t, v} \cdot \text{C2A}_{r, t}
\forall \{r, p, t \notin T^{a}, v, o\}
\in \Theta_{\text{limit\_annual\_capacity\_factor}}
\\\sum_{I} \textbf{FOA}_{r, p, i, t, v, o}
\quad \le, \ge, \text{or} = \quad
LIMACF_{r, t, v, o} \cdot
\textbf{CAP}_{r, p, t, v} \cdot \text{C2A}_{r, t}
\forall \{r, p, t \in T^{a}, v, o\} \in \Theta_{\text{limit\_annual\_capacity\_factor}}
"""
# r can be an individual region (r='US'), or a combination of regions separated by plus
# (r='Mexico+US+Canada'), or 'global'.
# if r == 'global', the constraint is system-wide
regions = geography.gather_group_regions(model, r)
techs = technology.gather_group_techs(model, t)
if TYPE_CHECKING:
activity_rptvo = cast('Expression', 0)
else:
activity_rptvo = 0
for _t in techs:
if _t not in model.tech_annual:
activity_rptvo += quicksum(
model.v_flow_out[_r, p, s, d, S_i, _t, v, o]
for _r in regions
for S_i in model.process_inputs_by_output.get((_r, p, _t, v, o), [])
for s in model.time_season
for d in model.time_of_day
)
else:
activity_rptvo += quicksum(
model.v_flow_out_annual[_r, p, S_i, _t, v, o]
for _r in regions
for S_i in model.process_inputs_by_output.get((_r, p, _t, v, o), [])
)
possible_activity_rptvo = quicksum(
model.v_capacity[_r, p, _t, v] * value(model.capacity_to_activity[_r, _t])
for _r in regions
for _t in techs
if v in model.process_vintages.get((_r, p, _t), [])
)
annual_cf = value(model.limit_annual_capacity_factor[r, t, v, o, op])
expr = operator_expression(activity_rptvo, Operator(op), annual_cf * possible_activity_rptvo)
# in the case that there is nothing to sum, skip
if isinstance(expr, bool): # an empty list was generated
return Constraint.Skip
return expr
[docs]
def limit_seasonal_capacity_factor_constraint(
model: TemoaModel, r: Region, p: Period, s: Season, t: Technology, op: str
) -> ExprLike:
r"""
The limit_seasonal_capacity_factor sets an upper bound on the seasonal capacity factor
from a specific technology. The first portion of the constraint pertains to
technologies with variable output at the time slice level, and the second portion
pertains to technologies with constant annual output belonging to the
:code:`tech_annual` set.
.. math::
:label: Limit Seasonal Capacity Factor
\sum_{D,I,V,O} \textbf{FO}_{r, p, s, d, i, t, v, o}
\quad \le, \ge, \text{or} = \quad
LIMSCF_{r, s, t} \cdot
\textbf{CAPAVL}_{r, p, t} \cdot \text{C2A}_{r, t} \cdot SFS_s
\forall \{r, p, s, t \notin T^{a}\} \in \Theta_{\text{limit\_seasonal\_capacity\_factor}}
\\\sum_{I,V,O} \textbf{FOA}_{r, p, i, t, v, o} \cdot SFS_s
\quad \le, \ge, \text{or} = \quad
LIMSCF_{r, s, t} \cdot
\textbf{CAPAVL}_{r, p, t} \cdot \text{C2A}_{r, t} \cdot SFS_s
\forall \{r, p, s, t \in T^{a}\} \in \Theta_{\text{limit\_seasonal\_capacity\_factor}}
"""
# r can be an individual region (r='US'), or a combination of regions separated by plus
# (r='Mexico+US+Canada'), or 'global'.
# if r == 'global', the constraint is system-wide
regions = geography.gather_group_regions(model, r)
techs = technology.gather_group_techs(model, t)
# we need to screen here because it is possible that the restriction extends beyond the
# lifetime of any vintage of the tech...
if all(
(_r, p, _t) not in model.v_capacity_available_by_period_and_tech
for _r in regions
for _t in techs
):
return Constraint.Skip
if TYPE_CHECKING:
activity_rpst = cast('Expression', 0)
else:
activity_rpst = 0
for _t in techs:
if _t not in model.tech_annual:
activity_rpst += quicksum(
model.v_flow_out[_r, p, s, d, S_i, _t, S_v, S_o]
for _r in regions
for S_v in model.process_vintages.get((_r, p, _t), [])
for S_i in model.process_inputs.get((_r, p, _t, S_v), [])
for S_o in model.process_outputs_by_input.get((_r, p, _t, S_v, S_i), [])
for d in model.time_of_day
)
else:
activity_rpst += quicksum(
model.v_flow_out_annual[_r, p, S_i, _t, S_v, S_o]
* model.segment_fraction_per_season[s]
for _r in regions
for S_v in model.process_vintages.get((_r, p, _t), [])
for S_i in model.process_inputs.get((_r, p, _t, S_v), [])
for S_o in model.process_outputs_by_input.get((_r, p, _t, S_v, S_i), [])
)
possible_activity_rpst = quicksum(
model.v_capacity_available_by_period_and_tech[_r, p, _t]
* value(model.capacity_to_activity[_r, _t])
* value(model.segment_fraction_per_season[s])
for _r in regions
for _t in techs
if (_r, p, _t) in model.v_capacity_available_by_period_and_tech
)
seasonal_cf = value(model.limit_seasonal_capacity_factor[r, s, t, op])
expr = operator_expression(activity_rpst, Operator(op), seasonal_cf * possible_activity_rpst)
# in the case that there is nothing to sum, skip
if isinstance(expr, bool): # an empty list was generated
return Constraint.Skip
return expr
[docs]
def limit_tech_output_split_constraint(
model: TemoaModel,
r: Region,
p: Period,
s: Season,
d: TimeOfDay,
t: Technology,
v: Vintage,
o: Commodity,
op: str,
) -> ExprLike:
r"""
Some processes take a single input and make multiple outputs, and the user would like to
specify either a constant or time-varying ratio of outputs per unit input. The most
canonical example is an oil refinery. Crude oil is used to produce many different refined
products. In many cases, the modeler would like to limit the share of each refined
product produced by the refinery.
For example, a hypothetical (and highly simplified) refinery might have a crude oil input
that produces 4 parts diesel, 3 parts gasoline, and 2 parts kerosene. The relative
ratios to the output then are:
.. math::
d = \tfrac{4}{9} \cdot \text{total output}, \qquad
g = \tfrac{3}{9} \cdot \text{total output}, \qquad
k = \tfrac{2}{9} \cdot \text{total output}
Note that it is possible to specify output shares that sum to less than unity. In such
cases, the model optimizes the remaining share. In addition, it is possible to change the
specified shares by model time period. Under this constraint, only the
technologies with variable output at the timeslice level (i.e., NOT in the
:code:`tech_annual` set) are considered.
The constraint is formulated as follows:
.. math::
:label: limit_tech_output_split
\sum_{I, t \not \in T^{a}} \textbf{FO}_{r, p, s, d, i, t, v, o}
\quad \le, \ge, \text{or} = \quad
TOS_{r, p, t, o} \cdot \sum_{I, O, t \not \in T^{a}} \textbf{FO}_{r, p, s, d, i, t, v, o}
\forall \{r, p, s, d, t, v, o\} \in \Theta_{\text{limit\_tech\_output\_split}}"""
out = quicksum(
model.v_flow_out[r, p, s, d, S_i, t, v, o]
for S_i in model.process_inputs_by_output[r, p, t, v, o]
)
total_out = quicksum(
model.v_flow_out[r, p, s, d, S_i, t, v, S_o]
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 = operator_expression(
out, Operator(op), value(model.limit_tech_output_split[r, p, t, o, op]) * total_out
)
return expr
[docs]
def limit_tech_output_split_annual_constraint(
model: TemoaModel, r: Region, p: Period, t: Technology, v: Vintage, o: Commodity, op: str
) -> ExprLike:
r"""
This constraint operates similarly to limit_tech_output_split_constraint.
However, under this function, only the technologies with constant annual
output (i.e., members of the :code:`tech_annual` set) are considered.
.. math::
:label: limit_tech_output_split_annual
\sum_{I, T^{a}} \textbf{FOA}_{r, p, i, t \in T^{a}, v, o}
\quad \le, \ge, \text{or} = \quad
TOSA_{r, p, t, o} \cdot
\sum_{I, O, T^{a}} \textbf{FOA}_{r, p, i, t \in T^{a}, v, o}
\forall \{r, p, t \in T^{a}, v, o\} \in
\Theta_{\text{limit\_tech\_output\_split\_annual}}"""
out = quicksum(
model.v_flow_out_annual[r, p, S_i, t, v, o]
for S_i in model.process_inputs_by_output[r, p, t, v, o]
)
total_out = quicksum(
model.v_flow_out_annual[r, p, S_i, t, v, S_o]
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 = operator_expression(
out, Operator(op), value(model.limit_tech_output_split_annual[r, p, t, o, op]) * total_out
)
return expr
[docs]
def limit_tech_output_split_average_constraint(
model: TemoaModel, r: Region, p: Period, t: Technology, v: Vintage, o: Commodity, op: str
) -> ExprLike:
r"""
Allows users to limit shares of commodity outputs from a process.
Under this constraint, only the technologies with variable
output at the timeslice level (i.e., NOT in the :code:`tech_annual` set) are considered.
This constraint differs from limit_tech_output_split as it specifies shares on an annual basis,
so even though it applies to technologies with variable output at the timeslice level,
the constraint only fixes the output shares over the course of a year."""
out = quicksum(
model.v_flow_out[r, p, S_s, S_d, S_i, t, v, o]
for S_i in model.process_inputs_by_output[r, p, t, v, o]
for S_s in model.time_season
for S_d in model.time_of_day
)
total_out = quicksum(
model.v_flow_out[r, p, S_s, S_d, S_i, t, v, S_o]
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]
for S_s in model.time_season
for S_d in model.time_of_day
)
expr = operator_expression(
out, Operator(op), value(model.limit_tech_output_split_annual[r, p, t, o, op]) * total_out
)
return expr
[docs]
def limit_emission_constraint(
model: TemoaModel, r: Region, p: Period, e: Commodity, op: str
) -> ExprLike:
r"""
A modeler can track emissions through use of the :code:`commodity_emissions`
set and :code:`emission_activity` parameter. The :math:`EAC` parameter is
analogous to the efficiency table, tying emissions to a unit of activity. The
limit_emission constraint allows the modeler to assign an upper bound per period
to each emission commodity. Note that this constraint sums emissions from
technologies with output varying at the time slice and those with constant annual
output in separate terms. It also includes embodied emissions from new capacity
and end-of-life emissions from retiring capacity.
.. math::
:label: limit_emission
\sum_{S,D,I,T,V,O|{r,e,i,t,v,o} \in EAC} \left (
EAC_{r, e, i, t, v, o} \cdot \textbf{FO}_{r, p, s, d, i, t, v, o}
\right ) & \\
+
\sum_{I,T,V,O|{r,e,i,t \in T^{a},v,o} \in EAC} (
EAC_{r, e, i, t, v, o} \cdot & \textbf{FOA}_{r, p, i, t \in T^{a}, v, o}
) \\
+
\sum_{T} \frac{EE_{r, e, t, v=p} \cdot \textbf{NCAP}_{r, t, v=p}}{LEN_p} & \\
+
\sum_{T,V} EEOL_{r, e, t, v} \cdot \textbf{ART}_{r, p, t, v} &
\\
\quad \le, \ge, \text{or} = \quad
LE_{r, p, e}
\\
& \forall \{r, p, e\} \in \Theta_{\text{limit\_emission}}
"""
emission_limit = value(model.limit_emission[r, p, e, op])
# r can be an individual region (r='US'), or a combination of regions separated by a +
# (r='Mexico+US+Canada'), or 'global'. Note that regions!=M.regions. We iterate over regions
# to find actual_emissions and actual_emissions_annual.
# if r == 'global', the constraint is system-wide
regions = geography.gather_group_regions(model, r)
# ================= Emissions and Flex and Curtailment =================
# Flex flows are deducted from v_flow_out, so it is NOT NEEDED to tax them again.
# (See commodity balance constr)
# Curtailment does not draw any inputs, so it seems logical that curtailed flows not be taxed
# either
process_emissions = quicksum(
model.v_flow_out[reg, p, S_s, S_d, S_i, S_t, S_v, S_o]
* value(model.emission_activity[reg, e, S_i, S_t, S_v, S_o])
for reg in regions
for tmp_r, tmp_e, S_i, S_t, S_v, S_o in model.emission_activity.sparse_keys()
if tmp_e == e and tmp_r == reg and S_t not in model.tech_annual
# EmissionsActivity not indexed by p, so make sure (r,p,t,v) combos valid
if (reg, p, S_t, S_v) in model.process_inputs
for S_s in model.time_season
for S_d in model.time_of_day
)
process_emissions_annual = quicksum(
model.v_flow_out_annual[reg, p, S_i, S_t, S_v, S_o]
* value(model.emission_activity[reg, e, S_i, S_t, S_v, S_o])
for reg in regions
for tmp_r, tmp_e, S_i, S_t, S_v, S_o in model.emission_activity.sparse_keys()
if tmp_e == e and tmp_r == reg and S_t in model.tech_annual
# EmissionsActivity not indexed by p, so make sure (r,p,t,v) combos valid
if (reg, p, S_t, S_v) in model.process_inputs
)
embodied_emissions = quicksum(
model.v_new_capacity[reg, t, v]
* value(model.emission_embodied[reg, e, t, v])
/ value(model.period_length[v])
for reg in regions
for (S_r, S_e, t, v) in model.emission_embodied.sparse_keys()
if v == p and S_r == reg and S_e == e
)
retirement_emissions = quicksum(
model.v_annual_retirement[reg, p, t, v] * value(model.emission_end_of_life[reg, e, t, v])
for reg in regions
for (S_r, S_e, t, v) in model.emission_end_of_life.sparse_keys()
if (reg, t, v) in model.retirement_periods and p in model.retirement_periods[reg, t, v]
if S_r == reg and S_e == e
)
lhs = (
process_emissions + process_emissions_annual + embodied_emissions + retirement_emissions
# + emissions_flex # NO! flex is subtracted from flowout, already accounted by flowout
# + emissions_curtail # NO! curtailed flows are not actual flows, just an accounting tool
# + emissions_flex_annual # NO! flexannual is subtracted from flowoutannual, already
# accounted
)
expr = operator_expression(lhs, Operator(op), emission_limit)
# in the case that there is nothing to sum, skip
if isinstance(expr, bool): # an empty list was generated
msg = "Warning: No technology produces emission '%s', though limit was specified as %s.\n"
logger.warning(msg, (e, emission_limit))
sys.stderr.write(msg % (e, emission_limit))
return Constraint.Skip
return expr
def limit_growth_capacity_constraint_rule(
model: TemoaModel, r: Region, p: Period, t: Technology, op: str
) -> ExprLike:
r"""Constrain ramp up rate of available capacity"""
return limit_growth_capacity(model, r, p, t, op, False)
def limit_degrowth_capacity_constraint_rule(
model: TemoaModel, r: Region, p: Period, t: Technology, op: str
) -> ExprLike:
r"""Constrain ramp down rate of available capacity"""
return limit_growth_capacity(model, r, p, t, op, True)
[docs]
def limit_growth_capacity(
model: TemoaModel, r: Region, p: Period, t: Technology, op: str, degrowth: bool = False
) -> ExprLike:
r"""
Constrain the change of capacity available between periods.
Forces the model to ramp up and down the availability of new technologies
more smoothly. Has constant (seed, :math:`S_{r,t}`) and proportional
(rate, :math:`R_{r,t}`) terms. This can be defined for a technology group
instead of one technology, in which case, capacity available is summed over
all technologies in the group. In the first period, previous available
capacity :math:`\mathbf{CAPAVL}_{r,p,t}` is replaced by previous existing
capacity, if any can be found.
.. math::
:label: Limit (De)Growth Capacity
\begin{aligned}\text{Growth:}\\
&\mathbf{CAPAVL}_{r,p,t}
\quad \le, \ge, \text{or} = \quad
S_{r,t} + (1+R_{r,t}) \cdot \mathbf{CAPAVL}_{r,p_{prev},t}
\end{aligned}
\qquad \forall \{r, p, t\} \in \Theta_{\text{limit\_growth\_capacity}}
\begin{aligned}\text{Degrowth:}\\
&\mathbf{CAPAVL}_{r,p_{prev},t}
\quad \le, \ge, \text{or} = \quad S_{r,t} + (1+R_{r,t}) \cdot \mathbf{CAPAVL}_{r,p,t}
\end{aligned}
\qquad \forall \{r, p, t\} \in \Theta_{\text{limit\_degrowth\_capacity}}
"""
regions = geography.gather_group_regions(model, r)
techs = technology.gather_group_techs(model, t)
growth = model.limit_degrowth_capacity if degrowth else model.limit_growth_capacity
rate = 1 + value(growth[r, t, op][0])
seed = value(growth[r, t, op][1])
cap_rpt = model.v_capacity_available_by_period_and_tech
# relevant r, p, t indices
cap_indices = {(_r, _p, _t) for _r, _p, _t in cap_rpt.keys() if _t in techs and _r in regions}
# periods the technology can have capacity in this region (sorted)
periods = sorted({_p for _r, _p, _t in cap_rpt})
if len(periods) == 0:
if p == model.time_optimize.first():
msg = (
'Tried to set {}rowthCapacity constraint {} but there are no periods where this '
'technology is available in this region. Constraint skipped.'
).format('Deg' if degrowth else 'G', (r, t))
logger.warning(msg)
return Constraint.Skip
# Only warn in p0 so we dont dump multiple warnings
if p == periods[0]:
if seed == 0:
msg = (
'No constant term (seed) provided for {}rowthCapacity constraint {}. '
'No capacity will be built in any period following one with zero capacity.'
).format('Deg' if degrowth else 'G', (r, t))
logger.info(msg)
gaps = [
_p
for _p in model.time_optimize
if _p not in periods and min(periods) < _p < max(periods)
]
if gaps:
msg = (
'Constructing {}rowthCapacity constraint {} and there are period gaps in which'
'capacity cannot exist in this region ({}). Capacity in these periods '
'will be treated as zero which may cause infeasibility or other problems.'
).format('Deg' if degrowth else 'G', (r, t), gaps)
logger.warning(msg)
# sum available capacity in this period
capacity = quicksum(cap_rpt[_r, _p, _t] for _r, _p, _t in cap_indices if _p == p)
if p == model.time_optimize.first():
# First future period. Grab available capacity in last existing period
# Adjust in-line for past PLF because we are constraining available capacity
p_prev = model.time_exist.last()
capacity_prev = sum(
value(model.existing_capacity[_r, _t, _v])
* min(1.0, (_v + value(model.lifetime_process[_r, _t, _v]) - p_prev) / (p - p_prev))
for _r, _t, _v in model.existing_capacity.sparse_keys()
if _r in regions
and _t in techs
and _v + value(model.lifetime_process[_r, _t, _v]) > p_prev
)
else:
# Otherwise, grab previous future period
p_prev = model.time_optimize.prev(p)
capacity_prev = quicksum(cap_rpt[_r, _p, _t] for _r, _p, _t in cap_indices if _p == p_prev)
if degrowth:
expr = operator_expression(capacity_prev, Operator(op), seed + capacity * rate)
else:
expr = operator_expression(capacity, Operator(op), seed + capacity_prev * rate)
# Check if any variables are actually included before returning
if isinstance(expr, bool):
return Constraint.Skip
return expr
def limit_growth_new_capacity_constraint_rule(
model: TemoaModel, r: Region, p: Period, t: Technology, op: str
) -> ExprLike:
r"""Constrain ramp up rate of new capacity deployment"""
return limit_growth_new_capacity(model, r, p, t, op, False)
def limit_degrowth_new_capacity_constraint_rule(
model: TemoaModel, r: Region, p: Period, t: Technology, op: str
) -> ExprLike:
r"""Constrain ramp down rate of new capacity deployment"""
return limit_growth_new_capacity(model, r, p, t, op, True)
[docs]
def limit_growth_new_capacity(
model: TemoaModel, r: Region, p: Period, t: Technology, op: str, degrowth: bool = False
) -> ExprLike:
r"""
Constrain the change of new capacity deployed between periods.
Forces the model to ramp up and down the deployment of new technologies
more smoothly. Has constant (seed, :math:`S_{r,t}`) and proportional
(rate, :math:`R_{r,t}`) terms. This can be defined for a technology group
instead of one technology, in which case, new capacity is summed over
all technologies in the group. In the first period, previous new capacity
:math:`\mathbf{NCAP}_{r,t,v_prev}` is replaced by previous existing capacity,
if any can be found.
.. math::
:label: Limit (De)Growth New Capacity
\begin{aligned}\text{Growth:}\\
&\mathbf{NCAP}_{r,t,v}
\quad \le, \ge, \text{or} = \quad
S_{r,t} + (1+R_{r,t}) \cdot \mathbf{NCAP}_{r,t,v_{prev}}
\text{ where } v=p
\end{aligned}
\qquad \forall \{r, p, t\} \in \Theta_{\text{limit\_growth\_capacity}}
\begin{aligned}\text{Degrowth:}\\
&\mathbf{NCAP}_{r,t,v_{prev}}
\quad \le, \ge, \text{or} = \quad S_{r,t} + (1+R_{r,t}) \cdot \mathbf{NCAP}_{r,t,v}
\text{ where } v=p
\end{aligned}
\qquad \forall \{r, p, t\} \in \Theta_{\text{limit\_degrowth\_capacity}}
"""
regions = geography.gather_group_regions(model, r)
techs = technology.gather_group_techs(model, t)
growth = model.limit_degrowth_new_capacity if degrowth else model.limit_growth_new_capacity
rate = 1 + value(growth[r, t, op][0])
seed = value(growth[r, t, op][1])
new_cap_rtv = model.v_new_capacity
# relevant r, t, v indices
cap_rtv = {(_r, _t, _v) for _r, _t, _v in new_cap_rtv.keys() if _t in techs and _r in regions}
# periods the technology can be built in this region (sorted)
periods = sorted({_v for _r, _t, _v in cap_rtv})
if len(periods) == 0:
if p == model.time_optimize.first():
msg = (
'Tried to set {}rowthNewCapacity constraint {} but there are no periods where this '
'technology can be built in this region. Constraint skipped.'
).format('Deg' if degrowth else 'G', (r, t))
logger.warning(msg)
return Constraint.Skip
# Only warn in p0 so we dont dump multiple warnings
if p == periods[0]:
if seed == 0:
msg = (
'No constant term (seed) provided for {}rowthNewCapacity constraint {}. '
'No capacity will be built in any period following one with zero new capacity.'
).format('Deg' if degrowth else 'G', (r, t))
logger.info(msg)
gaps = [
_p
for _p in model.time_optimize
if _p not in periods and min(periods) < _p < max(periods)
]
if gaps:
msg = (
'Constructing {}rowthNewCapacity constraint {} and there are period gaps in which'
'new capacity cannot be built in this region ({}). New capacity in these periods '
'will be treated as zero which may cause infeasibility or other problems.'
).format('Deg' if degrowth else 'G', (r, t), gaps)
logger.warning(msg)
# sum new capacity in this period
new_cap = quicksum(new_cap_rtv[_r, _t, _v] for _r, _t, _v in cap_rtv if _v == p)
if p == model.time_optimize.first():
# First future period. Grab last existing vintage
p_prev = model.time_exist.last()
new_cap_prev = sum(
value(model.existing_capacity[_r, _t, _v])
for _r, _t, _v in model.existing_capacity.sparse_keys()
if _r in regions and _t in techs and _v == p_prev
)
else:
# Otherwise, grab previous future vintage
p_prev = model.time_optimize.prev(p)
new_cap_prev = sum(new_cap_rtv[_r, _t, _v] for _r, _t, _v in cap_rtv if _v == p_prev)
if degrowth:
expr = operator_expression(new_cap_prev, Operator(op), seed + new_cap * rate)
else:
expr = operator_expression(new_cap, Operator(op), seed + new_cap_prev * rate)
# Check if any variables are actually included before returning
if isinstance(expr, bool):
return Constraint.Skip
return expr
def limit_growth_new_capacity_delta_constraint_rule(
model: TemoaModel, r: Region, p: Period, t: Technology, op: str
) -> ExprLike:
r"""Constrain ramp up rate of change in new capacity deployment"""
return limit_growth_new_capacity_delta(model, r, p, t, op, False)
def limit_degrowth_new_capacity_delta_constraint_rule(
model: TemoaModel, r: Region, p: Period, t: Technology, op: str
) -> ExprLike:
r"""Constrain ramp down rate of change in new capacity deployment"""
return limit_growth_new_capacity_delta(model, r, p, t, op, True)
[docs]
def limit_growth_new_capacity_delta(
model: TemoaModel, r: Region, p: Period, t: Technology, op: str, degrowth: bool = False
) -> ExprLike:
r"""
Constrain the acceleration of new capacity deployed between periods.
Forces the model to ramp up and down the change in deployment of new technologies
more smoothly. Has constant (seed, :math:`S_{r,t}`) and proportional
(rate, :math:`R_{r,t}`) terms. It is recommended to leave the rate term empty
as it would prevent the possibility of inflection in the rate of deployment.
This constraint can be defined for a technology group instead of one technology,
in which case, new capacity is summed over all technologies in the group. In the
first period, previous new capacities are replaced by previous existing capacities,
if any can be found.
.. math::
:label: Limit (De)Growth New Capacity Delta
\begin{aligned}\text{Growth:}\\
&\mathbf{NCAP}_{r,t,v_i} - \mathbf{NCAP}_{r,t,v_{i-1}}
\quad \le, \ge, \text{or} = \quad S_{r,t} + (1+R_{r,t}) \cdot
(\mathbf{NCAP}_{r,t,v_{i-1}} - \mathbf{NCAP}_{r,t,v_{i-2}})
\end{aligned}
\text{ where } v_i=p
\qquad \forall \{r, p, t\} \in \Theta_{\text{limit\_growth\_capacityDelta}}
\begin{aligned}\text{Degrowth:}\\
&\mathbf{NCAP}_{r,t,v_{i-1}} - \mathbf{NCAP}_{r,t,v_{i-2}}
\quad \le, \ge, \text{or} = \quad
S_{r,t} + (1+R_{r,t}) \cdot (\mathbf{NCAP}_{r,t,v_i} - \mathbf{NCAP}_{r,t,v_{i-1}})
\end{aligned}
\text{ where } v_i=p
\qquad \forall \{r, p, t\} \in \Theta_{\text{limit\_degrowth\_capacityDelta}}
"""
regions = geography.gather_group_regions(model, r)
techs = technology.gather_group_techs(model, t)
growth = (
model.limit_degrowth_new_capacity_delta
if degrowth
else model.limit_growth_new_capacity_delta
)
rate = 1 + value(growth[r, t, op][0])
seed = value(growth[r, t, op][1])
new_cap_rtv = model.v_new_capacity
# relevant r, t, v indices
cap_rtv = {(_r, _t, _v) for _r, _t, _v in new_cap_rtv.keys() if _t in techs and _r in regions}
# periods the technology can be built in this region (sorted)
periods = sorted({_v for _r, _t, _v in cap_rtv})
if len(periods) == 0:
if p == model.time_optimize.first():
msg = (
'Tried to set {}rowthNewCapacityDelta constraint {} but there are no periods where '
'this technology can be built in this region. Constraint skipped.'
).format('Deg' if degrowth else 'G', (r, t))
logger.warning(msg)
return Constraint.Skip
# Only warn in p0 so we dont dump multiple warnings
if p == periods[0]:
if seed == 0:
msg = (
'No constant term (seed) provided for {}rowthNewCapacityDelta constraint {}. '
'This is not recommended as deployment rates cannot inflect (change from '
'accelerating to decelerating or vice-versa).'
).format('Deg' if degrowth else 'G', (r, t))
logger.warning(msg)
gaps = [
_p
for _p in model.time_optimize
if _p not in periods and min(periods) < _p < max(periods)
]
if gaps:
msg = (
'Constructing {}rowthNewCapacityDelta constraint {} and there are period gaps in '
'which new capacity cannot be built in this region ({}). New capacity in these '
'periods will be treated as zero which may cause infeasibility or other problems.'
).format('Deg' if degrowth else 'G', (r, t), gaps)
logger.warning(msg)
# sum new capacity in this period
new_cap = sum(new_cap_rtv[_r, _t, _v] for _r, _t, _v in cap_rtv if _v == p)
if p == model.time_optimize.first():
# First planning period, pull last two existing vintages
p_prev = model.time_exist.last()
new_cap_prev = sum(
value(model.existing_capacity[_r, _t, _v])
for _r, _t, _v in model.existing_capacity.sparse_keys()
if _r in regions and _t in techs and _v == p_prev
)
p_prev2 = model.time_exist.prev(p_prev)
new_cap_prev2 = sum(
value(model.existing_capacity[_r, _t, _v])
for _r, _t, _v in model.existing_capacity.sparse_keys()
if _r in regions and _t in techs and _v == p_prev2
)
else:
# Not the first future period. Grab previous future period
p_prev = model.time_optimize.prev(p)
new_cap_prev = sum(new_cap_rtv[_r, _t, _v] for _r, _t, _v in cap_rtv if _v == p_prev)
if p == model.time_optimize.at(2): # apparently pyomo sets are indexed 1-based
# Second future period, grab last existing vintage
p_prev2 = model.time_exist.last()
new_cap_prev2 = sum(
value(model.existing_capacity[_r, _t, _v])
for _r, _t, _v in model.existing_capacity.sparse_keys()
if _r in regions and _t in techs and _v == p_prev2
)
else:
# At least the third future period. Grab last two future vintages
p_prev2 = model.time_optimize.prev(p_prev)
new_cap_prev2 = sum(new_cap_rtv[_r, _t, _v] for _r, _t, _v in cap_rtv if _v == p_prev2)
nc_delta_prev = new_cap_prev - new_cap_prev2
nc_delta = new_cap - new_cap_prev
if degrowth:
expr = operator_expression(nc_delta_prev, Operator(op), seed + nc_delta * rate)
else:
expr = operator_expression(nc_delta, Operator(op), seed + nc_delta_prev * rate)
# Check if any variables are actually included before returning
if isinstance(expr, bool):
return Constraint.Skip
return expr
[docs]
def limit_activity_constraint(
model: TemoaModel, r: Region, p: Period, t: Technology, op: str
) -> ExprLike:
r"""
Sets a limit on the activity from a specific technology.
Note that the indices for these constraints are region, period and tech, not tech
and vintage. The first version of the constraint pertains to technologies with
variable output at the time slice level, and the second version pertains to
technologies with constant annual output belonging to the :code:`tech_annual`
set.
.. math::
:label: limit_activity
\sum_{S,D,I,V,O} \textbf{FO}_{r, p, s, d, i, t, v, o}
\forall \{r, p, t \notin T^{a}\} \in \Theta_{\text{limit\_activity}}
+\sum_{I,V,O} \textbf{FOA}_{r, p, i, t \in T^{a}, v, o}
\forall \{r, p, t \in T^{a}\} \in \Theta_{\text{limit\_activity}}
\quad \le, \ge, \text{or} = \quad LA_{r, p, t}
"""
# r can be an individual region (r='US'), or a combination of regions separated by
# a + (r='Mexico+US+Canada'), or 'global'.
# if r == 'global', the constraint is system-wide
regions = geography.gather_group_regions(model, r)
techs = technology.gather_group_techs(model, t)
activity = quicksum(
model.v_flow_out[_r, p, s, d, S_i, _t, S_v, S_o]
for _t in techs
if _t not in model.tech_annual
for _r in regions
for S_v in model.process_vintages.get((_r, p, _t), [])
for S_i in model.process_inputs[_r, p, _t, S_v]
for S_o in model.process_outputs_by_input[_r, p, _t, S_v, S_i]
for s in model.time_season
for d in model.time_of_day
)
activity += quicksum(
model.v_flow_out_annual[_r, p, S_i, _t, S_v, S_o]
for _t in techs
if _t in model.tech_annual
for _r in regions
for S_v in model.process_vintages.get((_r, p, _t), [])
for S_i in model.process_inputs[_r, p, _t, S_v]
for S_o in model.process_outputs_by_input[_r, p, _t, S_v, S_i]
)
act_lim = value(model.limit_activity[r, p, t, op])
expr = operator_expression(activity, Operator(op), act_lim)
# in the case that there is nothing to sum, skip
if isinstance(expr, bool): # an empty list was generated
return Constraint.Skip
return expr
[docs]
def limit_new_capacity_constraint(
model: TemoaModel, r: Region, t: Technology, v: Vintage, op: str
) -> ExprLike:
r"""
The limit_new_capacity constraint sets a limit on the newly installed capacity of a
given technology or group in a given vintage year.
.. math::
:label: limit_new_capacity
\textbf{NCAP}_{r, t, v} \quad \le, \ge, \text{or} = \quad LNC_{r, t, v}
"""
regions = geography.gather_group_regions(model, r)
techs = technology.gather_group_techs(model, t)
cap_lim = value(model.limit_new_capacity[r, t, v, op])
new_cap = quicksum(
model.v_new_capacity[_r, _t, v]
for _t in techs
for _r in regions
if (_r, _t, v) in model.process_periods
)
expr = operator_expression(new_cap, Operator(op), cap_lim)
if isinstance(expr, bool):
return Constraint.Skip
return expr
[docs]
def limit_capacity_constraint(
model: TemoaModel, r: Region, p: Period, t: Technology, op: str
) -> ExprLike:
r"""
The limit_capacity constraint sets a limit on the available capacity of a
given technology. Note that the indices for these constraints are region, period and
tech, not tech and vintage.
.. math::
:label: limit_capacity
\textbf{CAPAVL}_{r, p, t} \quad \le, \ge, \text{or} = \quad LC_{r, p, t}
\forall \{r, p, t\} \in \Theta_{\text{limit\_capacity}}"""
regions = geography.gather_group_regions(model, r)
techs = technology.gather_group_techs(model, t)
cap_lim = value(model.limit_capacity[r, p, t, op])
capacity = quicksum(
model.v_capacity_available_by_period_and_tech[_r, p, _t]
for _t in techs
for _r in regions
if (_r, p, _t) in model.v_capacity_available_by_period_and_tech
)
expr = operator_expression(capacity, Operator(op), cap_lim)
if isinstance(expr, bool):
return Constraint.Skip
return expr
# ============================================================================
# PRE-COMPUTATION FUNCTION
# ============================================================================
def create_limit_vintage_sets(model: TemoaModel) -> None:
"""
Populates vintage-specific dictionaries for input/output split limit constraints.
This function iterates through active processes and identifies which vintages are
subject to split constraints, populating dictionaries that are then used by
the index set functions below.
Populates:
- M.input_split_vintages: dict mapping (r, p, i, t, op) to a set of vintages `v`.
- M.input_split_annual_vintages: dict for annual-specific input splits.
- M.output_split_vintages: dict mapping (r, p, t, o, op) to a set of vintages `v`.
- M.output_split_annual_vintages: dict for annual-specific output splits.
"""
logger.debug('Creating vintage sets for split limits.')
# Assuming M.process_vintages is already populated
for r, p, t in model.process_vintages:
for v in model.process_vintages[r, p, t]:
for i in model.process_inputs.get((r, p, t, v), []):
for op in model.operator:
if (r, p, i, t, op) in model.limit_tech_input_split:
model.input_split_vintages.setdefault((r, p, i, t, op), set()).add(v)
if (r, p, i, t, op) in model.limit_tech_input_split_annual:
model.input_split_annual_vintages.setdefault((r, p, i, t, op), set()).add(v)
for o in model.process_outputs.get((r, p, t, v), []):
for op in model.operator:
if (r, p, t, o, op) in model.limit_tech_output_split:
model.output_split_vintages.setdefault((r, p, t, o, op), set()).add(v)
if (r, p, t, o, op) in model.limit_tech_output_split_annual:
model.output_split_annual_vintages.setdefault((r, p, t, o, op), set()).add(
v
)