#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""This module is used to determine investment-related constraints.
The model computes the retirement of each technology and transmission line
with these considerations:
* The historical capacity of the technology and transmission line is
based on its capacity ratio.
* Each planning and scheduling period is based on the existing capacity.
The existing capacity for each year, in each zone, for each technology,
is as follows:
.. math::
{\\rm{cap}}_{y,z,e}^{\\rm{existingtech}}=
\\sum_{{\\rm{age}}=1}^{{{T}}_e
-(y-y_{\\rm{start}})}{{\\rm{CAP}}}_{{\\rm{age}},z,e}^{\\rm{inittech}}
+\\sum_{y_{\\rm{pre}}={\\max}(y_{\\rm{start}}, y-{{T}}_e)}^{y}
{{\\rm{cap}}_{y_{\\rm{pre}},z,e}^{\\rm{invtech}}}\\quad\\forall y,z,e
The existing capacity of the transmission lines for each year,
from :math:`z_{\\rm{from}}` zone to :math:`z_{\\rm{to}}`-th zone, is as
follows:
.. math::
{\\rm{cap}}_{y,z_{\\rm{from}},z_{\\rm{to}}}^{\\rm{existingline}}=
\\sum_{{\\rm{age}}=1}^{{T}_{\\rm{line}}
-(y-y_{\\rm{start}})}{{\\rm{CAP}}}_{{\\rm{age}},z_{\\rm{from}},
z_{\\rm{to}}}^{\\rm{initline}}
+\\sum_{y_{\\rm{pre}}={\\max}(y_{\\rm{start}}, y-{{T}}_{\\rm{line}})}^{y}
{{\\rm{cap}}_{y_{\\rm{pre}},
z_{\\rm{from}},z_{\\rm{to}}}^{\\rm{invline}}}\\quad\\forall
y,z_{\\rm{from}}\\neq z_{\\rm{to}}
"""
from typing import Union
import numpy as np
import pyoptinterface as poi
[docs]class AddInvestmentConstraints:
"""Add constraints for investment in the model.
"""
[docs] def __init__(self, model : object) -> None:
"""Initialize the class and add constraints.
Parameters
----------
model : object
Model object depending on the solver.
"""
self.model = model
# Pre-compute fast (zone, tech, year) -> bound dicts from the
# expansion_candidates DataFrame. The DataFrame is loaded as
# format:"table" because it has two value columns
# (capacity_min and capacity_max); we materialize it into dicts
# here for constant-time lookup inside the rule callbacks.
candidates = model.params['expansion_candidates']
self.candidate_min = {
(r.zone, r.tech, r.year): r.capacity_min
for _, r in candidates.iterrows()
}
self.candidate_max = {
(r.zone, r.tech, r.year): r.capacity_max
for _, r in candidates.iterrows()
}
model.remaining_technology = poi.make_tupledict(
model.year, model.zone, model.tech,
rule=self.tech_lifetime_rule
)
model.cap_existing = poi.make_tupledict(
model.year, model.zone, model.tech,
rule=self.remaining_capacity_rule
)
model.tech_up_bound_cons = poi.make_tupledict(
model.year, model.zone, model.tech,
rule=self.tech_up_bound_rule
)
model.tech_low_bound_cons = poi.make_tupledict(
model.year, model.zone, model.tech,
rule=self.tech_low_bound_rule
)
model.new_tech_up_bound_cons = poi.make_tupledict(
model.year, model.zone, model.tech,
rule=self.new_tech_up_bound_rule
)
model.new_tech_low_bound_cons = poi.make_tupledict(
model.year, model.zone, model.tech,
rule=self.new_tech_low_bound_rule
)
[docs] def tech_up_bound_rule(
self, y : int, z : str, te : str
) -> poi.ConstraintIndex:
"""Allowed capacity of commercial operation technology is less than or
equal to the predefined upper bound.
Parameters
----------
y : int
Year.
z : str
Zone.
te : str
Technology.
Returns
-------
poi.ConstraintIndex
The constraint of the model.
"""
model = self.model
tub = model.params['technology_capacity_max'][z, te, y]
if tub != np.inf:
lhs = model.cap_existing[y, z, te] - tub
return model.add_linear_constraint(lhs, poi.Leq, 0)
return None
[docs] def tech_low_bound_rule(
self, y : int, z : str, te : str
) -> poi.ConstraintIndex:
"""Allowed capacity of commercial operation technology is greater than
or equal to the predefined lower bound.
Parameters
----------
y : int
Year.
z : str
Zone.
te : str
Technology.
Returns
-------
poi.ConstraintIndex
The constraint of the model.
"""
model = self.model
tlb = model.params['technology_capacity_min'][z, te, y]
if tlb == 0:
return None
lhs = model.cap_existing[y, z, te] - tlb
return model.add_linear_constraint(lhs, poi.Geq, 0)
[docs] def new_tech_up_bound_rule(
self, y : int, z : str, te : str
) -> poi.ConstraintIndex:
"""New investment technology upper bound in specific year and zone.
Parameters
----------
y : int
Year.
z : str
Zone.
te : str
Technology.
Returns
-------
poi.ConstraintIndex
The constraint of the model.
"""
model = self.model
# If (z, te, y) is not in expansion_candidates, this combination
# cannot be expanded -- the file is the canonical list of
# buildable options. Default upper bound: 0.
ntub = self.candidate_max.get((z, te, y), 0)
if ntub != np.inf:
lhs = model.cap_newtech[y, z, te] - ntub
return model.add_linear_constraint(lhs, poi.Leq, 0)
return None
[docs] def new_tech_low_bound_rule(
self, y : int, z : str, te : str
) -> poi.ConstraintIndex:
"""New investment technology lower bound.
Parameters
----------
y : int
Year.
z : str
Zone.
te : str
Technology.
Returns
-------
poi.ConstraintIndex
The constraint of the model.
"""
model = self.model
ntlb = self.candidate_min.get((z, te, y), 0)
lhs = model.cap_newtech[y, z, te] - ntlb
return model.add_linear_constraint(lhs, poi.Geq, 0)
[docs] def tech_lifetime_rule(
self, y : int, z : str, te : str
) -> poi.ConstraintIndex:
"""Caculation of remaining technology capacity based on lifetime
constraints.
Parameters
----------
y : int
Year.
z : str
Zone.
te : str
Technology.
Returns
-------
poi.ExprBuilder
The expression of the model.
"""
model = self.model
lt = model.params['lifetime']
fleet = model.params['existing_fleet']
# An existing fleet entry (te, z, commission_year) -> capacity is
# in service in year y when commission_year <= y < commission_year
# + lifetime. Lifetime is looked up at the commissioning year
# cy, not the current year y -- once a unit is built, its
# lifetime is fixed at construction.
return poi.quicksum(
cap for (t, zz, cy), cap in fleet.items()
if t == te and zz == z and cy <= y < cy + lt[te, cy]
)
[docs] def remaining_capacity_rule(
self, y : int, z : str, te : str
) -> poi.ExprBuilder:
"""Remaining capacity of initial technology due to lifetime
restrictions. Where in modeled year y, the available technology
consists of the following.
1. The remaining in-service installed capacity from the initial
technology.
2. The remaining in-service installed capacity from newly built
technology in the previous modelled years.
Parameters
----------
y : int
Planned year.
z : str
Zone.
te : str
technology.
Returns
-------
poi.ExprBuilder
The expression of the model.
"""
model = self.model
year = model.params['year']
lt = model.params['lifetime']
cap_existing = poi.ExprBuilder()
new_tech = poi.quicksum(
model.cap_newtech[yy, z, te]
for yy in year[:year.index(y) + 1]
if y - yy < lt[te, yy]
)
cap_existing += new_tech
cap_existing += model.remaining_technology[y, z, te]
return cap_existing