"""
Collection of bus models.
"""
from __future__ import annotations
from typing import List
import pyomo.environ as pyo
from pyomo.core import ConcreteModel, Expression, quicksum
from commonpower.core import Bus, Node, StructureNode
from commonpower.modeling.base import ElementTypes as et
from commonpower.modeling.base import ModelElement
from commonpower.modeling.mip_builder import MIPExpressionBuilder
from commonpower.modeling.robust_cost import CostScenario
from commonpower.utils.cp_exceptions import EntityError
[docs]
class OptSelfSufficiencyNode(Bus):
"""
Class for creating a household that optimizes its self-sufficiency, i.e., aims at importing as little power from the
grid as possible (we do not currently consider the reactive power).
"""
[docs]
def cost_fcn(self, scenario: CostScenario, model: ConcreteModel, t: int) -> Expression:
"""
Defines a cost function that contains the costs from the household components (self.nodes) plus the total power
of the household to maximize self-sufficiency. Since the total power p is positive if the household has to
import power from the grid, we minimize p.
"""
if self.nodes:
grid_import_cost = scenario(self, "p", model)
return quicksum([n.cost_fcn(scenario, model, t) for n in self.nodes]) + grid_import_cost[t]
else:
raise EntityError(self, "Cannot define self-sufficiency cost function for an entity without components")
[docs]
class RTPricedBus(Bus):
"""
Bus which can directly trade its energy in real-time in stand-alone mode.
It can also be child of a StructureNode (e.g., energy community, P2P market).
In that case, the parent structure determines the cost of the bus.
.. runblock:: pycon
>>> from commonpower.models.busses import RTPricedBus
>>> RTPricedBus.info()
"""
[docs]
@classmethod
def _get_model_elements(cls) -> List[ModelElement]:
model_elements = super()._get_model_elements()
# additionally define buying and selling price
model_elements += [
ModelElement("psib", et.DATA, "buying price", pyo.Reals),
ModelElement("psis", et.DATA, "selling price", pyo.Reals),
]
return model_elements
[docs]
def _get_additional_constraints(self) -> List[ModelElement]:
"""
Sets a binary buying indicator. \\
.. math::
p_{eb} = \\left\\{
\\begin{array}{ll}
1 & p \\geq 0 \\\\
0 & \\, \\textrm{otherwise} \\\\
\\end{array}
\\right.
"""
model_elements = super()._get_additional_constraints() # fetch internal power balance constraints
if self.stand_alone is True:
mb = MIPExpressionBuilder(self)
mb.from_geq("p", 0, "p_eb", is_new=True)
return model_elements + mb.model_elements
else:
return model_elements
[docs]
def cost_fcn(self, scenario: CostScenario, model: ConcreteModel, t: int) -> Expression:
"""
.. math::
cost = \\sum_{i \\in components} cost_i + p * psib * p_{eb} + p * psis * (1 - p_{eb})
"""
if self.nodes:
if self.stand_alone is True:
return (
quicksum([n.cost_fcn(scenario, model, t) for n in self.nodes])
+ (
scenario(self, "p", model)[t]
* (1 - scenario(self, "p_eb", model)[t])
* scenario(self, "psis", model)[t]
* self.tau
)
+ (
scenario(self, "p", model)[t]
* scenario(self, "p_eb", model)[t]
* scenario(self, "psib", model)[t]
* self.tau
)
)
else:
return quicksum([n.cost_fcn(scenario, model, t) for n in self.nodes])
else:
return 0.0
[docs]
class RTPricedBusLinear(Bus):
"""
RTPricedBus which assumes selling and buying prices are identical.
.. runblock:: pycon
>>> from commonpower.models.busses import RTPricedBusLinear
>>> RTPricedBusLinear.info()
"""
[docs]
@classmethod
def _get_model_elements(cls) -> List[ModelElement]:
model_elements = super()._get_model_elements()
model_elements += [ModelElement("psi", et.DATA, "market price", pyo.Reals)]
return model_elements
[docs]
def cost_fcn(self, scenario: CostScenario, model: ConcreteModel, t: int) -> Expression:
"""
.. math::
cost = \\sum_{i \\in components} cost_i + p * psi
"""
if self.nodes:
if self.stand_alone:
return quicksum([n.cost_fcn(scenario, model, t) for n in self.nodes]) + (
scenario(self, "p", model)[t] * scenario(self, "psi", model)[t] * self.tau
)
else:
return quicksum([n.cost_fcn(scenario, model, t) for n in self.nodes])
else:
return 0.0
[docs]
class TradingBus(Bus):
"""
Bus which trades energy with an external market.
.. runblock:: pycon
>>> from commonpower.models.busses import TradingBus
>>> TradingBus.info()
"""
[docs]
@classmethod
def _get_model_elements(cls) -> List[ModelElement]:
model_elements = [
ModelElement("p", et.INPUT, "active power", bounds=(-1e6, 1e6)),
ModelElement("q", et.VAR, "reactive power", bounds=(-1e6, 1e6)),
ModelElement("v", et.VAR, "voltage magnitude", bounds=(0.9, 1.1)),
ModelElement("d", et.VAR, "voltage angle", bounds=(-15, 15)),
ModelElement("psib", et.DATA, "buying price", pyo.Reals),
ModelElement("psis", et.DATA, "selling price", pyo.Reals),
]
return model_elements
[docs]
def _get_additional_constraints(self) -> List[ModelElement]:
"""
Sets a binary selling indicator. \\
.. math::
p_{es} = \\left\\{
\\begin{array}{ll}
1 & p \\geq 0 \\\\
0 & \\, \\textrm{otherwise} \\\\
\\end{array}
\\right.
"""
mb = MIPExpressionBuilder(self)
mb.from_geq("p", 0, "p_es", is_new=True)
return mb.model_elements
[docs]
def cost_fcn(self, scenario: CostScenario, model: ConcreteModel, t: int) -> Expression:
"""
.. math::
cost = -p * psis * p_{es} -p * psib * (1 - p_{es})
"""
return -(
scenario(self, "p", model)[t]
* (1 - scenario(self, "p_es", model)[t])
* scenario(self, "psib", model)[t]
* self.tau
) - (
scenario(self, "p", model)[t]
* scenario(self, "p_es", model)[t]
* scenario(self, "psis", model)[t]
* self.tau
)
[docs]
def _get_internal_power_balance_constraints(self) -> List[ModelElement]:
# overwrites super(), because otherwise p=0 would be enforced
return []
[docs]
def add_node(self, node: Node) -> Node:
raise NotImplementedError("Trading busses cannot have sub-nodes")
[docs]
class TradingBusLinear(Bus):
"""
TradingBus which assumes selling and buying prices are identical.
.. runblock:: pycon
>>> from commonpower.models.busses import TradingBusLinear
>>> TradingBusLinear.info()
"""
[docs]
@classmethod
def _get_model_elements(cls) -> List[ModelElement]:
model_elements = [
ModelElement("p", et.INPUT, "active power", bounds=(-1e6, 1e6)),
ModelElement("q", et.VAR, "reactive power", bounds=(-1e6, 1e6)),
ModelElement("v", et.VAR, "voltage magnitude", bounds=(0.9, 1.1)),
ModelElement("d", et.VAR, "voltage angle", bounds=(-15, 15)),
ModelElement("psi", et.DATA, "market price", pyo.Reals),
]
return model_elements
[docs]
def cost_fcn(self, scenario: CostScenario, model: ConcreteModel, t: int) -> Expression:
"""
.. math::
cost = -p * psi
"""
return -(scenario(self, "p", model)[t] * scenario(self, "psi", model)[t] * self.tau)
[docs]
def _get_internal_power_balance_constraints(self) -> List[ModelElement]:
# overwrites super(), because otherwise p=0 would be enforced
return []
[docs]
def add_node(self, node: Node) -> Node:
raise NotImplementedError("Trading busses cannot have sub-nodes")
[docs]
class CarbonAwareTradingBus(TradingBus):
"""
Carbon Aware Trading Bus.
.. runblock:: pycon
>>> from commonpower.models.busses import CarbonAwareTradingBus
>>> CarbonAwareTradingBus.info()
"""
[docs]
@classmethod
def _get_model_elements(cls) -> List[ModelElement]:
model_elements = super()._get_model_elements()
model_elements += [
ModelElement("ci", et.DATA, "carbon intensity", pyo.NonNegativeReals),
ModelElement("a", et.CONSTANT, "cost parameter a", pyo.NonNegativeReals),
ModelElement("b", et.CONSTANT, "cost parameter b", domain=pyo.NonNegativeIntegers, bounds=(1, 2)),
]
return model_elements
[docs]
def cost_fcn(self, scenario: CostScenario, model: ConcreteModel, t: int) -> Expression:
"""
.. math::
cost = (-p * psis * p_{es} -p * psib * (1 - p_{es})) + (a * p^b) / ci
Note that ci >= 0 for carbon intensity and p represents active power.
"""
return (
-(
scenario(self, "p", model)[t]
* (1 - scenario(self, "p_es", model)[t])
* scenario(self, "psib", model)[t]
* self.tau
)
- (
scenario(self, "p", model)[t]
* scenario(self, "p_es", model)[t]
* scenario(self, "psis", model)[t]
* self.tau
)
) + (scenario(self, "a", model) * scenario(self, "p", model)[t] ** scenario(self, "b", model)) / scenario(
self, "ci", model
)[
t
] * self.tau
[docs]
class CarbonAwareTradingBusLinear(TradingBusLinear):
"""
Carbon Aware Bus which assumes selling and buying prices are identical.
.. runblock:: pycon
>>> from commonpower.models.busses import CarbonAwareTradingBusLinear
>>> CarbonAwareTradingBusLinear.info()
"""
[docs]
@classmethod
def _get_model_elements(cls) -> List[ModelElement]:
model_elements = super()._get_model_elements()
model_elements += [
ModelElement("ci", et.DATA, "carbon intensity", pyo.NonNegativeReals),
ModelElement("a", et.CONSTANT, "cost parameter a", pyo.NonNegativeReals),
ModelElement("b", et.CONSTANT, "cost parameter b", domain=pyo.NonNegativeIntegers, bounds=(1, 2)),
]
return model_elements
[docs]
def cost_fcn(self, scenario: CostScenario, model: ConcreteModel, t: int) -> Expression:
"""
.. math::
cost = - p * psi + (a * p^b) / ci)
"""
return (-scenario(self, "p", model)[t] * scenario(self, "psi", model)[t] * self.tau) + (
scenario(self, "a", model) * scenario(self, "p", model)[t] ** scenario(self, "b", model)
) * self.tau
[docs]
class ExternalGrid(Bus):
"""
Bus with a connection to an external grid.
Does not have costs or dynamics, merely relevant for power flow calculations.
.. runblock:: pycon
>>> from commonpower.models.busses import ExternalGrid
>>> ExternalGrid.info()
"""
[docs]
def _get_internal_power_balance_constraints(self) -> List[ModelElement]:
# overwrites super(), because otherwise p=0 would be enforced
return []
[docs]
def add_node(self, node: Node) -> Node:
raise NotImplementedError("External grid nodes cannot have sub-nodes")