Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add facades CommodityGHG and ConversionGHG #180

Merged
merged 19 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 202 additions & 0 deletions src/oemof/tabular/facades/commodity_ghg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import dataclasses

from oemof.solph._plumbing import sequence
from oemof.solph.flows import Flow
from pyomo.core import BuildAction, Constraint
from pyomo.core.base.block import ScalarBlock

from oemof import solph

from .commodity import Commodity


@dataclasses.dataclass(unsafe_hash=False, frozen=False, eq=False)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SabineHaas - Is there a reason why you used this instead of @dataclass_facade?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I opened a new issue for discussion - as I had this in my mind for quite a while:
#184

class CommodityGHG(Commodity):
r"""
Commodity element with one output and additionally emission outputs.

Parameters
----------
bus: oemof.solph.Bus
An oemof bus instance where the unit is connected to with its output
amount: numeric
Total available amount to be used within the complete time horizon
of the problem
marginal_cost: numeric
Marginal cost for one unit used commodity
output_parameters: dict (optional)
Parameters to set on the output edge of the component (see. oemof.solph
Edge/Flow class for possible arguments)


.. math::
\sum_{t} x^{flow}(t) \leq c^{amount}

Notes
-----
Emission buses are defined by starting with 'emission_bus', see Examples
section.
Emission factors are defined by the following naming convention:
'emission_factor_<label_of_emission_bus>.
The realation between the main output (`bus`) and the emissions are set via
:class:`~oemof.tabular.facades.commodity_ghg.CommodityGHGBlock`.

For additional constraints set through `output_parameters` see
oemof.solph.Flow class.

Examples
---------
Defining a ConversionGHG:

>>> from oemof import solph

>>> bus_gas = solph.Bus("gas")
>>> bus_co2 = solph.Bus("co2")
>>> bus_gas.type, bus_co2.type = "bus", "bus"

>>> commodity = CommodityGHG(
... label="gas-commodity",
... bus=bus_gas,
... emission_bus_0=bus_co2,
... carrier="gas",
... amount=1000,
... marginal_cost=10,
... output_parameters={"max": [0.9, 0.5, 0.4]},
... emission_factor_co2=56)

>>> commodity.emission_factors[bus_co2].default
56
"""

def __init__(self, **kwargs):

super().__init__(
**kwargs,
)

buses = {
key: value
for key, value in kwargs.items()
if type(value) is type(solph.Bus())
}

self.build_solph_components()
self.init_emission_buses(kwargs)
self.emission_factors = self.init_emission_factors(buses, kwargs)

def init_emission_buses(self, kwargs):
"""Adds emissions buses as output flows and drops them from kwargs"""
for key, bus in list(kwargs.items()):
if key.startswith("emission_bus"):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If Bus would have a carrier attribute, one could look for emission carriers, but it is not implemented in oemof.solph as long as this naming convention is documented this is fine. Maybe an error or warning if no occurence of emission bus is found in kwargs would make sense?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @Bachibouzouk ! I have added warnings in 51bb272.
I also renamed bus to value to avoid missunderstandings in c0f2038 as the value can be of different types (solph.Bus, string) depending on the item.

self.outputs.update({bus: Flow()})
kwargs.pop(key)

def init_emission_factors(self, buses, kwargs):
"""Returns emission factors as values in dict with buses as keys"""
emission_factors = {}
for key, value in list(kwargs.items()):
if key.startswith("emission_factor"):
bus_label = key.split("_")[-1]
bus = [
bus for bus in buses.items() if bus[1].label == bus_label
][0][1]
emission_factors.update({bus: sequence(value)})
kwargs.pop(key)
return emission_factors

def constraint_group(self):
return CommodityGHGBlock


class CommodityGHGBlock(ScalarBlock):
r"""
Block for the linear relation of nodes with type
:class:`~oemof.tabular.facades.commodity_ghg.CommodityGHGBlock`

**The following sets are created:**

CommodityGHGs
A set with all
:class:`~oemof.tabular.facades.commodity_ghg.CommodityGHGBlock`
objects.

**The following constraints are created:**

Linear relation :attr:`om.CommodityGHGBlock.relation[o,t]`
.. math::
P_{n.bus}(p, t) \cdot \eta_{o}(t) = P_{o}(p, t), \\
\forall p, t \in \textrm{TIMEINDEX}, \\
\forall n \in \textrm{CommodityGHGs}, \\
\forall o \in \textrm{OUTPUTS}

While OUPUTS the set of Bus objects connected with the output of
the CommodityGHG. The constraint above will be created for all OUTPUTS for
all TIMESTEPS. A CommodityGHG with two outflows for one day with an hourly
resolution will lead to 48 constraints.

The index :math: n is the index for the Source node itself. Therefore,
a `flow[i, n, p, t]` is a flow from the Bus i to the Source n at
time index p, t.

====================== ============================ ====================
symbol attribute explanation
====================== ============================ ====================
:math:`P_{n,n.bus}(p, t)` `flow[n, n.bus, p, t]` CommodityGHG, outflow

:math:`P_{n,o}(p, t)` `flow[n, o, p, t]` CommodityGHG, outflow

:math:`\eta_{o}(t)` `emission_factor[n, o, t]` Outflow, efficiency

====================== ============================ ====================

"""

CONSTRAINT_GROUP = True

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

def _create(self, group=None):
"""
Creates the linear constraint for the class:`CommodityGHGBlock` block.
"""
if group is None:
return None

m = self.parent_block()

out_flows = {n: [o for o in n.outputs.keys()] for n in group}

self.relation = Constraint(
[
(n, o, p, t)
for p, t in m.TIMEINDEX
for n in group
for o in out_flows[n]
],
noruleinit=True,
)

def _emission_relation(block):
for p, t in m.TIMEINDEX:
for n in group:
for o in out_flows[n]:
# only emission buses
if o is not n.bus:
try:
lhs = (
m.flow[n, n.bus, p, t]
* n.emission_factors[o][t]
)
rhs = m.flow[n, o, p, t]
block.relation.add((n, o, p, t), (lhs == rhs))
except KeyError:
pass
raise KeyError(
"Error in constraint creation",
"source: {0}, target: {1}".format(
n.label, o.label
),
)

self.relation_build = BuildAction(rule=_emission_relation)
118 changes: 118 additions & 0 deletions src/oemof/tabular/facades/conversion_ghg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import dataclasses

from oemof.solph._plumbing import sequence
from oemof.solph.flows import Flow

from oemof import solph

from .conversion import Conversion


@dataclasses.dataclass(unsafe_hash=False, frozen=False, eq=False)
class ConversionGHG(Conversion):
r"""
Conversion unit with one input, one output and emission outputs.

Cost parameters like `carrier_cost` are associated with `from_bus` like in
Conversion facade.
The emission factors are also associated to `from_bus`


Parameters
----------
from_bus: oemof.solph.Bus
An oemof bus instance where the conversion unit is connected to with
its input.
to_bus: oemof.solph.Bus
An oemof bus instance where the conversion unit is connected to with
its output.
capacity: numeric
The conversion capacity (output side) of the unit.
efficiency: numeric
Efficiency of the conversion unit (0 <= efficiency <= 1). Default: 1
marginal_cost: numeric
Marginal cost for one unit of produced output. Default: 0
carrier_cost: numeric
Carrier cost for one unit of used input. Default: 0
capacity_cost: numeric
Investment costs per unit of output capacity.
If capacity is not set, this value will be used for optimizing the
conversion output capacity.
expandable: boolean or numeric (binary)
True, if capacity can be expanded within optimization. Default: False.
capacity_potential: numeric
Maximum invest capacity in unit of output capacity.
capacity_minimum: numeric
Minimum invest capacity in unit of output capacity.
input_parameters: dict (optional)
Set parameters on the input edge of the conversion unit
(see oemof.solph for more information on possible parameters)
ouput_parameters: dict (optional)
Set parameters on the output edge of the conversion unit
(see oemof.solph for more information on possible parameters)

Notes
-----
Emission buses are defined by starting with 'emission_bus', see Examples
section.
Emission factors are defined by the following naming convention:
'emission_factor_<label_of_emission_bus>.

Examples
---------
Defining a ConversionGHG:

>>> from oemof import solph

>>> bus_biomass = solph.Bus("biomass")
>>> bus_heat = solph.Bus("heat")
>>> bus_co2 = solph.Bus("co2")

>>> bus_biomass.type, bus_heat.type, bus_co2.type = "bus", "bus", "bus"

>>> conversion = ConversionGHG(
... label="biomass_plant",
... carrier="biomass",
... tech="st",
... from_bus=bus_biomass,
... to_bus=bus_heat,
... emission_bus_0=bus_co2,
... capacity=100,
... efficiency=0.4,
... emission_factor_co2=56)
>>> conversion.conversion_factors[bus_co2].default
56
"""

def __init__(self, **kwargs):
super().__init__(
**kwargs,
)

buses = {
key: value
for key, value in kwargs.items()
if type(value) is type(solph.Bus())
}

self.build_solph_components() # inputs, outputs, conversion_factors
self.init_emission_buses(kwargs)
self.init_emission_factors(buses, kwargs)

def init_emission_buses(self, kwargs):
"""Adds emissions buses as output flows and drops them from kwargs"""
for key, bus in list(kwargs.items()):
if key.startswith("emission_bus"):
self.outputs.update({bus: Flow()})
kwargs.pop(key)

def init_emission_factors(self, buses, kwargs):
"""Adds emission factors as `conversion_factors"""
for key, value in list(kwargs.items()):
if key.startswith("emission_factor"):
bus_label = key.split("_")[-1]
bus = [
bus for bus in buses.items() if bus[1].label == bus_label
][0][1]
self.conversion_factors.update({bus: sequence(value)})
kwargs.pop(key)