diff --git a/AUTHORS.rst b/AUTHORS.rst index 7dc38191..03ddcd49 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -13,4 +13,5 @@ Authors * Sarah Berendes * Marie-Claire Gering * Julian Endres +* Sabine Haas * Felix Maurer diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fb6f999c..4e41163f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,9 @@ Unreleased Features +* Add tests for BEV facades developed in #94 `#142 `_ + + Fixes diff --git a/src/oemof/tabular/_facade.py b/src/oemof/tabular/_facade.py index 22b10bbe..67ad7619 100644 --- a/src/oemof/tabular/_facade.py +++ b/src/oemof/tabular/_facade.py @@ -151,7 +151,7 @@ def __init__(self, *args, **kwargs): add_subnodes, sender=self ) - def _nominal_value(self): + def _nominal_value(self, value=None): """Returns None if self.expandable ist True otherwise it returns the capacity """ @@ -168,18 +168,23 @@ def _nominal_value(self): "to_from": self.to_from_capacity, } else: - return self.capacity + if value: + return value + else: + return self.capacity - def _investment(self): + def _investment(self, bev=False): if not self.expandable: self.investment = None return self.investment + if self.capacity_cost is None: msg = ( "If you set `expandable`to True you need to set " "attribute `capacity_cost` of component {}!" ) raise ValueError(msg.format(self.label)) + # If storage component if isinstance(self, GenericStorage): # If invest costs/MWH are given @@ -207,7 +212,7 @@ def _investment(self): age=getattr(self, "age", 0), fixed_costs=getattr(self, "fixed_costs", None), ) - # If other component than storage + # If other component than storage or Bev else: self.investment = Investment( ep_costs=self.capacity_cost, diff --git a/src/oemof/tabular/constraint_facades.py b/src/oemof/tabular/constraint_facades.py index 50cb6307..68bd03d0 100644 --- a/src/oemof/tabular/constraint_facades.py +++ b/src/oemof/tabular/constraint_facades.py @@ -2,6 +2,38 @@ from dataclasses import dataclass from oemof.solph.constraints.integral_limit import generic_integral_limit +from pyomo.environ import Constraint + +from oemof.tabular.facades import Bev + + +def var2str(var): + return "_".join( + [i.label if not isinstance(i, int) else str(i) for i in var] + ) + + +def get_bev_label(var): + return var[1].label.split("-storage")[0] + + +def get_period(model, year, constraint_type=None): + if model.es.periods: + years = [period.year.min() for period in model.es.periods] + for period_index, period_year in enumerate(years): + if period_year == year: + return period_index + raise ValueError( + f"'{constraint_type}' constraint facade:\n" + f"Year {year} is not in model.PERIODS." + ) + elif year == model.es.timeindex.year.min(): + return 0 + else: + raise ValueError( + f"'{constraint_type}' constraint facade:\n" + f"Year {year} is not in model.timeindex." + ) class ConstraintFacade(abc.ABC): @@ -39,4 +71,150 @@ def build_constraint(self, model): ) -CONSTRAINT_TYPE_MAP = {"generic_integral_limit": GenericIntegralLimit} +@dataclass +class BevShareMob(ConstraintFacade): + # TODO: rework docstring + """This constraint is only feasible if the definition of one vehicle is the + same (charging_capacity/storage_capacity) for all three bev technologies""" + name: str + type: str + year: int + label: str + share_mob_flex_G2V: (int, float) + share_mob_flex_V2G: (int, float) + share_mob_inflex: (int, float) + + @staticmethod + def map_share2vars(model, share_mob, period): + invest_vars = [] + for node in model.es.nodes: + if isinstance(node, Bev): + invest_vars.extend( + [ + inv + for inv in model.InvestmentFlowBlock.invest + if node.electricity_bus.label == inv[0].label + and f"{node.facade_label}-storage" in inv[1].label + and period == inv[2] + ] + ) + + share_mob = { + inv_var: value + for key, value in share_mob.items() + for inv_var in invest_vars + if key in inv_var[1].label + } + return invest_vars, share_mob + + @staticmethod + def convert_share(share): + if 0 <= share <= 1: + return share + elif 0 <= share <= 100: + return share / 100 + else: + raise ValueError(f"Mob share: {share} not in [0,1] or [0,100]") + + def build_constraint(self, model): + period = get_period(model, self.year, self.__class__.__name__) + + if period > len(model.PERIODS): + raise ValueError( + f"Period {period} not in model.PERIODS {model.PERIODS}" + ) + + share_mob = { + f"{self.label}-G2V-storage": self.convert_share( + self.share_mob_flex_G2V + ), + f"{self.label}-V2G-storage": self.convert_share( + self.share_mob_flex_V2G + ), + f"{self.label}-inflex-storage": self.convert_share( + self.share_mob_inflex + ), + } + + invest_vars, share_mob = self.map_share2vars( + model=model, share_mob=share_mob, period=period + ) + + def investment_constraints_rule(InvestmentFlowBlock): + return InvestmentFlowBlock.invest[inv_var] == share_mob[ + inv_var + ] * sum(InvestmentFlowBlock.invest[iv] for iv in invest_vars) + + for inv_var in invest_vars: + name = f"mob_share_{get_bev_label(inv_var)}_{period}" + setattr( + model.InvestmentFlowBlock, + name, + Constraint(rule=investment_constraints_rule), + ) + + +# TODO maybe move inside facade +@dataclass +class BevEqualInvest(ConstraintFacade): + name: str + type: str + year: int + + @staticmethod + def double_with_offset(lst): + result = [] + for i in range(len(lst) - 1): + result.append((lst[i], lst[i + 1])) + return result + + @staticmethod + def get_bev_invest_vars(model, period): + all_invest_vars = {} + for node in model.es.nodes: + if isinstance(node, Bev): + invest_vars_bev = list( + set( + inv + for inv in model.InvestmentFlowBlock.invest + if inv[2] == period + for edge in inv[:2] + if node.facade_label in edge.label + ) + ) + all_invest_vars[node.facade_label] = invest_vars_bev + return all_invest_vars + + def build_constraint(self, model): + # TODO add checks + period = get_period(model, self.year, self.__class__.__name__) + if period > len(model.PERIODS): + raise ValueError( + f"Period {period} not in model.PERIODS {model.PERIODS}" + ) + + invest_vars = self.get_bev_invest_vars(model, period) + + def equate_variables_rule(InvestmentFlowBlock): + return ( + InvestmentFlowBlock.invest[var1] + == InvestmentFlowBlock.invest[var2] + ) + + for bev_label, invest_vars in invest_vars.items(): + for i, (var1, var2) in enumerate( + self.double_with_offset(invest_vars) + ): + name = f"{bev_label}_equal_invest_{i}_({period})" + setattr( + model.InvestmentFlowBlock, + name, + Constraint(rule=equate_variables_rule), + ) + + +CONSTRAINT_TYPE_MAP = { + "generic_integral_limit": GenericIntegralLimit, + "bev_equal_invest": BevEqualInvest, + "bev_share_mob": BevShareMob, +} diff --git a/src/oemof/tabular/examples/datapackages/private_transport/README.md b/src/oemof/tabular/examples/datapackages/private_transport/README.md new file mode 100644 index 00000000..1117ae81 --- /dev/null +++ b/src/oemof/tabular/examples/datapackages/private_transport/README.md @@ -0,0 +1,5 @@ +# Emission constraint example for oemof-tabular + +Run `scripts/infer.py` from the datapackage root directory to add the +meta data file `datapackage.json` after updating the resources of the +datapackage. diff --git a/src/oemof/tabular/examples/datapackages/private_transport/data/constraints/bev_mob_share.csv b/src/oemof/tabular/examples/datapackages/private_transport/data/constraints/bev_mob_share.csv new file mode 100644 index 00000000..f39d1726 --- /dev/null +++ b/src/oemof/tabular/examples/datapackages/private_transport/data/constraints/bev_mob_share.csv @@ -0,0 +1,3 @@ +name;type;year;label;share_mob_flex_G2V;share_mob_flex_V2G;share_mob_inflex +bev_share_mob_2020;bev_share_mob;2020;BEV;10;20;70 +bev_share_mob_2030;bev_share_mob;2030;BEV;20;30;50 diff --git a/src/oemof/tabular/examples/datapackages/private_transport/data/constraints/bev_total_invest.csv b/src/oemof/tabular/examples/datapackages/private_transport/data/constraints/bev_total_invest.csv new file mode 100644 index 00000000..ee73092e --- /dev/null +++ b/src/oemof/tabular/examples/datapackages/private_transport/data/constraints/bev_total_invest.csv @@ -0,0 +1,3 @@ +name;type;year +bev_equal_invest;bev_equal_invest;2020 +bev_equal_invest;bev_equal_invest;2030 diff --git a/src/oemof/tabular/examples/datapackages/private_transport/data/elements/bus.csv b/src/oemof/tabular/examples/datapackages/private_transport/data/elements/bus.csv new file mode 100644 index 00000000..78a0cc29 --- /dev/null +++ b/src/oemof/tabular/examples/datapackages/private_transport/data/elements/bus.csv @@ -0,0 +1,2 @@ +name;type;balanced +bus-electricity;bus;true diff --git a/src/oemof/tabular/examples/datapackages/private_transport/data/elements/dispatchable.csv b/src/oemof/tabular/examples/datapackages/private_transport/data/elements/dispatchable.csv new file mode 100644 index 00000000..f4d99f4f --- /dev/null +++ b/src/oemof/tabular/examples/datapackages/private_transport/data/elements/dispatchable.csv @@ -0,0 +1,4 @@ +name;type;carrier;tech;capacity;bus;marginal_cost;profile;output_parameters +gas;dispatchable;gas;gt;1000;bus-electricity;40;1;{"custom_attributes": {"emission_factor": 10}} +coal;dispatchable;coal;st;1000;bus-electricity;40;1;{"custom_attributes": {"emission_factor": 20}} +lignite;dispatchable;lignite;st;500;bus-electricity;20;1;{"custom_attributes": {"emission_factor": 30}} diff --git a/src/oemof/tabular/examples/datapackages/private_transport/data/elements/excess.csv b/src/oemof/tabular/examples/datapackages/private_transport/data/elements/excess.csv new file mode 100644 index 00000000..aec66cde --- /dev/null +++ b/src/oemof/tabular/examples/datapackages/private_transport/data/elements/excess.csv @@ -0,0 +1,2 @@ +name;type;bus +electricity-excess;excess;bus-electricity diff --git a/src/oemof/tabular/examples/datapackages/private_transport/data/elements/load.csv b/src/oemof/tabular/examples/datapackages/private_transport/data/elements/load.csv new file mode 100644 index 00000000..a527f0c8 --- /dev/null +++ b/src/oemof/tabular/examples/datapackages/private_transport/data/elements/load.csv @@ -0,0 +1,2 @@ +name;amount;profile;type;bus +electricity-demand;5000;electricity-load-profile;load;bus-electricity diff --git a/src/oemof/tabular/examples/datapackages/private_transport/data/elements/storage.csv b/src/oemof/tabular/examples/datapackages/private_transport/data/elements/storage.csv new file mode 100644 index 00000000..1c824889 --- /dev/null +++ b/src/oemof/tabular/examples/datapackages/private_transport/data/elements/storage.csv @@ -0,0 +1,2 @@ +name,carrier,tech,storage_capacity,capacity,capacity_cost,storage_capacity_initial,type,bus +el-storage,lithium,battery,100,10,10,0.5,storage,bus-electricity diff --git a/src/oemof/tabular/examples/datapackages/private_transport/data/elements/volatile.csv b/src/oemof/tabular/examples/datapackages/private_transport/data/elements/volatile.csv new file mode 100644 index 00000000..aaf5f273 --- /dev/null +++ b/src/oemof/tabular/examples/datapackages/private_transport/data/elements/volatile.csv @@ -0,0 +1,2 @@ +name;type;carrier;tech;capacity;capacity_cost;bus;marginal_cost;profile;output_parameters +wind;volatile;wind;onshore;50;;bus-electricity;0;wind-profile;{} diff --git a/src/oemof/tabular/examples/datapackages/private_transport/data/sequences/load_profile.csv b/src/oemof/tabular/examples/datapackages/private_transport/data/sequences/load_profile.csv new file mode 100644 index 00000000..a3f46a20 --- /dev/null +++ b/src/oemof/tabular/examples/datapackages/private_transport/data/sequences/load_profile.csv @@ -0,0 +1,4 @@ +timeindex,electricity-load-profile +2011-01-01T00:00:00Z,0.000745659236 +2011-01-01T01:00:00Z,0.000709651546 +2011-01-01T02:00:00Z,0.00068564642 diff --git a/src/oemof/tabular/examples/datapackages/private_transport/data/sequences/volatile_profile.csv b/src/oemof/tabular/examples/datapackages/private_transport/data/sequences/volatile_profile.csv new file mode 100644 index 00000000..8ab276f0 --- /dev/null +++ b/src/oemof/tabular/examples/datapackages/private_transport/data/sequences/volatile_profile.csv @@ -0,0 +1,4 @@ +timeindex,wind-profile +2011-01-01T00:00:00Z,0.147532 +2011-01-01T01:00:00Z,0.184181 +2011-01-01T02:00:00Z,0.223937 diff --git a/src/oemof/tabular/examples/datapackages/private_transport/datapackage.json b/src/oemof/tabular/examples/datapackages/private_transport/datapackage.json new file mode 100644 index 00000000..ee69b983 --- /dev/null +++ b/src/oemof/tabular/examples/datapackages/private_transport/datapackage.json @@ -0,0 +1,486 @@ +{ + "profile": "tabular-data-package", + "name": "oemof-tabular-dispatch-example", + "oemof_tabular_version": "0.0.5dev", + "resources": [ + { + "path": "data/elements/bus.csv", + "profile": "tabular-data-resource", + "name": "bus", + "format": "csv", + "mediatype": "text/csv", + "encoding": "utf-8", + "schema": { + "fields": [ + { + "name": "name", + "type": "string", + "format": "default" + }, + { + "name": "type", + "type": "string", + "format": "default" + }, + { + "name": "balanced", + "type": "boolean", + "format": "default" + } + ], + "missingValues": [ + "" + ], + "primaryKey": "name", + "foreignKeys": [] + } + }, + { + "path": "data/elements/dispatchable.csv", + "profile": "tabular-data-resource", + "name": "dispatchable", + "format": "csv", + "mediatype": "text/csv", + "encoding": "utf-8", + "schema": { + "fields": [ + { + "name": "name", + "type": "string", + "format": "default" + }, + { + "name": "type", + "type": "string", + "format": "default" + }, + { + "name": "carrier", + "type": "string", + "format": "default" + }, + { + "name": "tech", + "type": "string", + "format": "default" + }, + { + "name": "capacity", + "type": "integer", + "format": "default" + }, + { + "name": "bus", + "type": "string", + "format": "default" + }, + { + "name": "marginal_cost", + "type": "integer", + "format": "default" + }, + { + "name": "profile", + "type": "integer", + "format": "default" + }, + { + "name": "output_parameters", + "type": "object", + "format": "default" + } + ], + "missingValues": [ + "" + ], + "primaryKey": "name", + "foreignKeys": [ + { + "fields": "bus", + "reference": { + "resource": "bus", + "fields": "name" + } + } + ] + } + }, + { + "path": "data/elements/excess.csv", + "profile": "tabular-data-resource", + "name": "excess", + "format": "csv", + "mediatype": "text/csv", + "encoding": "utf-8", + "schema": { + "fields": [ + { + "name": "name", + "type": "string", + "format": "default" + }, + { + "name": "type", + "type": "string", + "format": "default" + }, + { + "name": "bus", + "type": "string", + "format": "default" + } + ], + "missingValues": [ + "" + ], + "primaryKey": "name", + "foreignKeys": [ + { + "fields": "bus", + "reference": { + "resource": "bus", + "fields": "name" + } + } + ] + } + }, + { + "path": "data/elements/load.csv", + "profile": "tabular-data-resource", + "name": "load", + "format": "csv", + "mediatype": "text/csv", + "encoding": "utf-8", + "schema": { + "fields": [ + { + "name": "name", + "type": "string", + "format": "default" + }, + { + "name": "amount", + "type": "integer", + "format": "default" + }, + { + "name": "profile", + "type": "string", + "format": "default" + }, + { + "name": "type", + "type": "string", + "format": "default" + }, + { + "name": "bus", + "type": "string", + "format": "default" + } + ], + "missingValues": [ + "" + ], + "primaryKey": "name", + "foreignKeys": [ + { + "fields": "bus", + "reference": { + "resource": "bus", + "fields": "name" + } + }, + { + "fields": "profile", + "reference": { + "resource": "load_profile" + } + } + ] + } + }, + { + "path": "data/elements/storage.csv", + "profile": "tabular-data-resource", + "name": "storage", + "format": "csv", + "mediatype": "text/csv", + "encoding": "utf-8", + "schema": { + "fields": [ + { + "name": "name", + "type": "string", + "format": "default" + }, + { + "name": "carrier", + "type": "string", + "format": "default" + }, + { + "name": "tech", + "type": "string", + "format": "default" + }, + { + "name": "storage_capacity", + "type": "integer", + "format": "default" + }, + { + "name": "capacity", + "type": "integer", + "format": "default" + }, + { + "name": "capacity_cost", + "type": "integer", + "format": "default" + }, + { + "name": "storage_capacity_initial", + "type": "number", + "format": "default" + }, + { + "name": "type", + "type": "string", + "format": "default" + }, + { + "name": "bus", + "type": "string", + "format": "default" + } + ], + "missingValues": [ + "" + ], + "primaryKey": "name", + "foreignKeys": [ + { + "fields": "bus", + "reference": { + "resource": "bus", + "fields": "name" + } + } + ] + } + }, + { + "path": "data/elements/volatile.csv", + "profile": "tabular-data-resource", + "name": "volatile", + "format": "csv", + "mediatype": "text/csv", + "encoding": "utf-8", + "schema": { + "fields": [ + { + "name": "name", + "type": "string", + "format": "default" + }, + { + "name": "type", + "type": "string", + "format": "default" + }, + { + "name": "carrier", + "type": "string", + "format": "default" + }, + { + "name": "tech", + "type": "string", + "format": "default" + }, + { + "name": "capacity", + "type": "integer", + "format": "default" + }, + { + "name": "capacity_cost", + "type": "string", + "format": "default" + }, + { + "name": "bus", + "type": "string", + "format": "default" + }, + { + "name": "marginal_cost", + "type": "integer", + "format": "default" + }, + { + "name": "profile", + "type": "string", + "format": "default" + }, + { + "name": "output_parameters", + "type": "object", + "format": "default" + } + ], + "missingValues": [ + "" + ], + "primaryKey": "name", + "foreignKeys": [ + { + "fields": "bus", + "reference": { + "resource": "bus", + "fields": "name" + } + }, + { + "fields": "profile", + "reference": { + "resource": "volatile_profile" + } + } + ] + } + }, + { + "path": "data/sequences/load_profile.csv", + "profile": "tabular-data-resource", + "name": "load_profile", + "format": "csv", + "mediatype": "text/csv", + "encoding": "utf-8", + "schema": { + "fields": [ + { + "name": "timeindex", + "type": "datetime", + "format": "default" + }, + { + "name": "electricity-load-profile", + "type": "number", + "format": "default" + } + ], + "missingValues": [ + "" + ] + } + }, + { + "path": "data/sequences/volatile_profile.csv", + "profile": "tabular-data-resource", + "name": "volatile_profile", + "format": "csv", + "mediatype": "text/csv", + "encoding": "utf-8", + "schema": { + "fields": [ + { + "name": "timeindex", + "type": "datetime", + "format": "default" + }, + { + "name": "wind-profile", + "type": "number", + "format": "default" + } + ], + "missingValues": [ + "" + ] + } + }, + { + "path": "data/constraints/bev_mob_share.csv", + "profile": "tabular-data-resource", + "name": "bev_mob_share", + "format": "csv", + "mediatype": "text/csv", + "encoding": "utf-8", + "schema": { + "fields": [ + { + "name": "name", + "type": "string", + "format": "default" + }, + { + "name": "type", + "type": "string", + "format": "default" + }, + { + "name": "year", + "type": "integer", + "format": "default" + }, + { + "name": "label", + "type": "string", + "format": "default" + }, + { + "name": "share_mob_flex_G2V", + "type": "integer", + "format": "default" + }, + { + "name": "share_mob_flex_V2G", + "type": "integer", + "format": "default" + }, + { + "name": "share_mob_inflex", + "type": "integer", + "format": "default" + } + ], + "missingValues": [ + "" + ] + } + }, + { + "path": "data/constraints/bev_total_invest.csv", + "profile": "tabular-data-resource", + "name": "bev_total_invest", + "format": "csv", + "mediatype": "text/csv", + "encoding": "utf-8", + "schema": { + "fields": [ + { + "name": "name", + "type": "string", + "format": "default" + }, + { + "name": "type", + "type": "string", + "format": "default" + }, + { + "name": "year", + "type": "integer", + "format": "default" + } + ], + "missingValues": [ + "" + ] + } + } + ] +} diff --git a/src/oemof/tabular/examples/datapackages/private_transport/scripts/infer.py b/src/oemof/tabular/examples/datapackages/private_transport/scripts/infer.py new file mode 100644 index 00000000..58321e9a --- /dev/null +++ b/src/oemof/tabular/examples/datapackages/private_transport/scripts/infer.py @@ -0,0 +1,20 @@ +""" +Run this script from the root directory of the datapackage to update +or create meta data. +""" +from oemof.tabular.datapackage import building + +# This part is for testing only: It allows to pass +# the filename of inferred metadata other than the default. +if "kwargs" not in locals(): + kwargs = {} + + +building.infer_metadata( + package_name="oemof-tabular-dispatch-example", + foreign_keys={ + "bus": ["volatile", "dispatchable", "storage", "load", "excess"], + "profile": ["load", "volatile"], + }, + **kwargs, +) diff --git a/src/oemof/tabular/facades/__init__.py b/src/oemof/tabular/facades/__init__.py index ff64bb39..0b4309a0 100644 --- a/src/oemof/tabular/facades/__init__.py +++ b/src/oemof/tabular/facades/__init__.py @@ -7,6 +7,7 @@ from .conversion import Conversion from .dispatchable import Dispatchable from .excess import Excess +from .experimental.battery_electric_vehicle import Bev from .extraction_turbine import ExtractionTurbine from .generator import Generator from .heatpump import HeatPump @@ -35,6 +36,7 @@ "shortage": Shortage, "storage": Storage, "volatile": Volatile, + "bev": Bev, } TECH_COLOR_MAP = { diff --git a/src/oemof/tabular/facades/experimental/battery_electric_vehicle.py b/src/oemof/tabular/facades/experimental/battery_electric_vehicle.py new file mode 100644 index 00000000..ed677303 --- /dev/null +++ b/src/oemof/tabular/facades/experimental/battery_electric_vehicle.py @@ -0,0 +1,455 @@ +from dataclasses import field +from typing import Sequence, Union + +from oemof.solph import Investment +from oemof.solph._plumbing import sequence as solph_sequence +from oemof.solph.buses import Bus +from oemof.solph.components import Converter, GenericStorage, Sink +from oemof.solph.flows import Flow + +from oemof.tabular._facade import Facade, dataclass_facade + + +@dataclass_facade +class Bev(GenericStorage, Facade): + r"""A fleet of Battery electric vehicles with controlled/flexible charging, + (G2V), vehicle-to-grid (V2G) or uncontrolled/fixed charging (inflex). + + This facade consists of mulitple oemof.solph components: + + - a GenericStorage as storage unit + - a Bus as internal bus + - a Sink to model the drive consumption (if no mobility bus is + given) + - a Converter to convert the energy to the electricity bus (optional V2G) + - a Converter to convert the energy to e.g. pkm (optional if mobility bus + is given) + + Charging and discharging capacity is assumed to be equal. + Multiple fleets can be modelled and connected to a common bus + (commodity_bus) to apply one demand for all modelled fleets. + + Parameters + ---------- + electricity_bus: oemof.solph.Bus + The electricity bus where the BEV is connected to. + commodity_bus: oemof.solph.Bus + A bus which is used to connect a common demand for multiple BEV + instances (optional). + charging_power : int + The total charging/discharging power of the fleet (e.g. in MW). + charging_potential: int + Maximum charging potential in investment optimization. + availability : float, array of float + Availability of the fleet at the charging stations (e.g. 0.8). + storage_capacity: int + The total storage capacity of the fleet (e.g. in MWh). + initial_storage_capacity: float + The relative storage content in the timestep before the first + time step of optimization (between 0 and 1). + + Note: When investment mode is used in a multi-period model, + `initial_storage_level` is not supported. + Storage output is forced to zero until the storage unit is invested in. + min_storage_level : array of float + Relative profile of minimum storage level (min SOC).The normed minimum + storage content as fraction of the storage capacity or the capacity + that has been invested into (between 0 and 1). + max_storage_level : array of float + Relative profile of maximum storage level (max SOC). + drive_power: int + The total driving capacity of the fleet (e.g. in MW) if no mobility_bus + is connected. + drive_consumption : array of float + Relative profile of drive consumption of the fleet + v2g: bool + If True, Vehicle-to-grid option is enabled, default: False + loss_rate: float + The relative loss/self discharge of the storage content per time unit, + default: 0 + efficiency_mob_g2v: float + Efficiency at the charging station (grid-to-vehicle), default: 1 + efficiency_mob_v2g: float + Efficiency at the charging station (vehicle-to-grid), default: 1 + efficiency_sto_in: float + Efficiency of charging the batteries, default: 1 + efficiency_sto_out: float + Efficiency of discharging the batteries, default: 1 + efficiency_mob_electrical: float + Efficiency of the electrical drive train per 100 km (optional). + default: 1 + pkm_conversion_rate: float + Conversion rate from energy to e.g. pkm if mobility_bus passed + (optional) default: 1 + expandable: bool + If True, the fleet is expandable, default: False + Charging_power and storage_capacity are then interpreted as existing + capacities at the first investment period. + lifetime: int + Total lifetime of the fleet in years. + age: int + Age of the existing fleet at the first investment period in years. + + invest_c_rate: float + Invested storage capacity per power rate + (e.g. 60/20 = 3h charging/discharging time) + bev_storage_capacity: int + Storage capacity of one vehicle in kWh. + bev_capacity: int + Charging capacity of one vehicle in kW. + + bev_invest_costs: float, array of float + Investment costs for new vehicle unit. EUR/vehicle + fixed_costs: float, array of float + Operation independent costs for existing and new vehicle units. + (e.g. EUR/(vehicle*a)) + variable_costs: float, array of float + Variable costs of the fleet (e.g. in EUR/MWh). + fixed_investment_costs + + + balanced : boolean + Couple storage level of first and last time step. + (Total inflow and total outflow are balanced.) + + input_parameters: dict + Dictionary to specify parameters on the input edge. You can use + all keys that are available for the oemof.solph.network.Flow class. + e.g. fixed charging timeseries for the storage can be passed with + {"fix": [1,0.5,...]} + output_parameters: dict + see: input_parameters + e.g. fixed discharging timeseries for the storage can be passed with + {"fix": [1,0.5,...]} + + + The vehicle fleet is modelled as a storage together with an internal + sink with fixed flow: + + todo check formula + .. math:: + + x^{level}(t) = + x^{level}(t-1) \cdot (1 - c^{loss\_rate}(t)) + + c^{efficiency\_charging}(t) \cdot x^{flow, in}(t) + - \frac{x^{drive\_power}(t)}{c^{efficiency\_discharging}(t)} + - \frac{x^{flow, v2g}(t)} + {c^{efficiency\_discharging}(t) \cdot c^{efficiency\_v2g}(t)} + \qquad \forall t \in T + + Note + ---- + As the Bev is a sub-class of `oemof.solph.GenericStorage` you also + pass all arguments of this class. + + The concept is similar to the one described in the following publications + with the difference that uncontrolled charging is not (yet) considered. + + Wulff, N., Steck, F., Gils, H. C., Hoyer-Klick, C., van den Adel, + B., & Anderson, J. E. (2020). + Comparing power-system and user-oriented battery electric vehicle + charging representation and + its implications on energy system modeling. + Energies, 13(5). https://doi.org/10.3390/en13051093 + + Diego Luca de Tena Costales. (2014). + Large Scale Renewable Power Integration with Electric Vehicles. + https://doi.org/10.04.2014 + + Examples + -------- + Basic usage example of the Bev class with an arbitrary selection of + attributes. + + >>> from oemof import solph + >>> from oemof.tabular import facades + >>> my_bus = solph.Bus('my_bus') + >>> my_bev = Bev( + ... label='my_bev', + ... bus=el_bus, + ... carrier='electricity', + ... tech='bev', + ... storage_capacity=1000, + ... capacity=50, + ... availability=[0.8, 0.7, 0.6], + ... drive_power=[0.3, 0.2, 0.5], + ... amount=450, + # ... loss_rate=0.01, + ... initial_storage_level=0, + ... min_storage_level=[0.1, 0.2, 0.15], + ... max_storage_level=[0.9, 0.95, 0.92], + ... efficiency=0.93 + ... ) + + """ + + electricity_bus: Bus + + commodity_bus: Bus = None + + charging_power: float = 0 + + minimum_charging_power: float = None + + charging_potential: float = None + + availability: Union[float, Sequence[float]] = 1 + + storage_capacity: float = 0 + + minimum_storage_capacity: float = 0 + + storage_capacity_potential: float = None + + initial_storage_capacity: float = 0 + + drive_power: int = 0 + + drive_consumption: Sequence[float] = None + + v2g: bool = False + + efficiency_mob_g2v: float = 1 + + efficiency_mob_v2g: float = 1 + + efficiency_mob_electrical: float = 1 + + efficiency_sto_in: float = 1 + + efficiency_sto_out: float = 1 + + commodity_conversion_rate: float = 1 + + expandable: bool = False + + lifetime: int = 20 + + age: int = 0 + + invest_c_rate: Sequence[float] = None + + bev_invest_costs: Sequence[float] = None + + variable_costs: Union[float, Sequence[float]] = 0 + + fixed_costs: Union[float, Sequence[float]] = 0 + + fixed_investment_costs: Union[float, Sequence[float]] = 0 + + balanced: bool = False + + input_parameters: dict = field(default_factory=dict) + + output_parameters: dict = field(default_factory=dict) + + def _converter_investment(self): + """All parameters are passed, but no investment cost is considered. + The investment cost will be considered by the storage inflow only. + """ + if self.expandable: + investment = Investment( + ep_costs=0, + maximum=self._get_maximum_additional_invest( + "charging_potential", "charging_power" + ), + minimum=getattr(self, "minimum_charging_power", 0), + existing=getattr(self, "charging_power", 0), + lifetime=getattr(self, "lifetime", None), + age=getattr(self, "age", 0), + fixed_costs=0, + ) + return investment + else: + return None + + def build_solph_components(self): + # use label as prefix for subnodes + self.facade_label = self.label + self.label = self.label + "-storage" + + # convert to solph sequences + self.availability = solph_sequence(self.availability) + + # TODO: check if this is correct + self.nominal_storage_capacity = self.storage_capacity + # self.nominal_storage_capacity = self._nominal_value( + # self.storage_capacity) + + self.balanced = self.balanced # TODO to be false in multi-period + + # create internal bus + internal_bus = Bus(label=self.facade_label + "-bus") + self.bus = internal_bus + subnodes = [internal_bus] + + self.investment = Investment( + ep_costs=0, + maximum=self._get_maximum_additional_invest( + "storage_capacity_potential", "storage_capacity" + ), + minimum=getattr(self, "minimum_storage_capacity", 0), + existing=getattr(self, "storage_capacity", 0), + lifetime=getattr(self, "lifetime", None), + age=getattr(self, "age", 0), + fixed_costs=0, + ) + + # ##### Vehicle2Grid Converter ##### + if self.v2g: + vehicle_to_grid = Converter( + label=self.facade_label + "-v2g", + inputs={ + internal_bus: Flow( + # variable_costs=self.carrier_cost, + # **self.input_parameters + ) + }, + outputs={ + self.electricity_bus: Flow( + nominal_value=self._nominal_value( + value=self.charging_power + ), + # max=self.availability, # doesn't work with investment + variable_costs=None, + # investment=self._investment(bev=True), + investment=self._converter_investment(), + ) + }, + # Includes storage charging efficiencies + conversion_factors={ + self.electricity_bus: (self.efficiency_mob_v2g) + }, + ) + subnodes.append(vehicle_to_grid) + + # Drive consumption + if self.commodity_bus: + # ##### Commodity Converter ##### + # converts energy to another commodity e.g. pkm + # connects it to a special mobility bus + commodity_converter = Converter( + label=self.facade_label + "-2com", + inputs={ + internal_bus: Flow( + # **self.output_parameters + ) + }, + outputs={ + self.commodity_bus: Flow( + nominal_value=self._nominal_value(self.charging_power), + # max=self.availability, + variable_costs=None, + # investment=self._investment(bev=True), + investment=self._converter_investment(), + ) + }, + conversion_factors={ + self.commodity_bus: self.commodity_conversion_rate + * self.efficiency_mob_electrical + # * 100 # TODO pro 100 km? + }, + ) + subnodes.append(commodity_converter) + + else: + # ##### Consumption Sink ##### + # fixed demand for this fleet only + if self.expandable: + raise NotImplementedError( + "Consumption sink for expandable BEV not implemented yet!" + "Please use a `mobility_bus` + `Sink` instead. Optimizing" + "one fleet alone may not yield meaningful results." + ) + else: + driving_consumption = Sink( + label=self.facade_label + "-consumption", + inputs={ + internal_bus: Flow( + nominal_value=self.drive_power, + fix=self.drive_consumption, + ) + }, + ) + subnodes.append(driving_consumption) + + # ##### Storage ######## + if self.expandable: + # self.capacity_cost = self.bev_invest_costs + self.storage_capacity_cost = 0 + # self.investment = self._investment(bev=False) + self.invest_relation_input_output = 1 # charge/discharge equal + # invest_c_rate = Energy/Power = h + self.invest_relation_input_capacity = ( + 1 / self.invest_c_rate + ) # Power/Energy + self.invest_relation_output_capacity = ( + 1 / self.invest_c_rate + ) # Power/Energy + + for attr in ["invest_relation_input_output"]: + if getattr(self, attr) is None: + raise AttributeError( + ( + "You need to set attr " "`{}` " "for component {}" + ).format(attr, self.label) + ) + + # ##### Grid2Vehicle ##### + # containts the whole investment costs for bev + flow_in = Flow( + # max=self.availability, + investment=Investment( + ep_costs=self.bev_invest_costs, + maximum=self._get_maximum_additional_invest( + "charging_potential", "charging_power" + ), + existing=getattr(self, "charging_power", 0), + lifetime=getattr(self, "lifetime", None), + age=getattr(self, "age", 0), + fixed_costs=getattr(self, "fixed_investment_costs", None), + ), + variable_costs=self.variable_costs, + **self.input_parameters, + ) + # set investment, but no costs (as relation input / output = 1) + flow_out = Flow( + investment=Investment( + existing=getattr(self, "charging_power", 0), + lifetime=getattr(self, "lifetime", None), + age=getattr(self, "age", 0), + ), + **self.output_parameters, + ) + # required for correct grouping in oemof.solph.components + self._invest_group = True + + else: + flow_in = Flow( + nominal_value=self._nominal_value(self.charging_power), + # max=self.availability, + variable_costs=self.variable_costs, + **self.input_parameters, + ) + flow_out = Flow( + nominal_value=self._nominal_value(self.charging_power), + # max=self.availability, + **self.output_parameters, + ) + + self.inflow_conversion_factor = solph_sequence( + self.efficiency_mob_g2v * self.efficiency_sto_in + ) + + self.outflow_conversion_factor = solph_sequence( + self.efficiency_sto_out + ) + + self.inputs.update({self.electricity_bus: flow_in}) + + self.outputs.update({self.bus: flow_out}) + + self._set_flows() + + # many components in facade + self.subnodes = subnodes diff --git a/tests/_files/lp_files/bev_trio_constraint.lp b/tests/_files/lp_files/bev_trio_constraint.lp new file mode 100644 index 00000000..1a42bd42 --- /dev/null +++ b/tests/_files/lp_files/bev_trio_constraint.lp @@ -0,0 +1,694 @@ +\* Source Pyomo model name=Model *\ + +min +objective: ++2 InvestmentFlowBlock_invest(el_bus_BEV_inflex_storage_0) ++2 InvestmentFlowBlock_invest(el_bus_BEV_V2G_storage_0) ++2 InvestmentFlowBlock_invest(el_bus_BEV_G2V_storage_0) ++10 flow(el_bus_BEV_V2G_storage_0_0) ++10 flow(el_bus_BEV_V2G_storage_0_1) ++10 flow(el_bus_BEV_V2G_storage_0_2) ++20 flow(el_bus_BEV_inflex_storage_0_0) ++20 flow(el_bus_BEV_inflex_storage_0_1) ++20 flow(el_bus_BEV_inflex_storage_0_2) ++4 flow(el_bus_BEV_G2V_storage_0_0) ++4 flow(el_bus_BEV_G2V_storage_0_1) ++4 flow(el_bus_BEV_G2V_storage_0_2) + +s.t. + +c_e_BusBlock_balance(BEV_G2V_bus_0_0)_: ++1 flow(BEV_G2V_storage_BEV_G2V_bus_0_0) +-1 flow(BEV_G2V_bus_BEV_G2V_2com_0_0) += 0 + +c_e_BusBlock_balance(BEV_G2V_bus_0_1)_: ++1 flow(BEV_G2V_storage_BEV_G2V_bus_0_1) +-1 flow(BEV_G2V_bus_BEV_G2V_2com_0_1) += 0 + +c_e_BusBlock_balance(BEV_G2V_bus_0_2)_: ++1 flow(BEV_G2V_storage_BEV_G2V_bus_0_2) +-1 flow(BEV_G2V_bus_BEV_G2V_2com_0_2) += 0 + +c_e_BusBlock_balance(BEV_V2G_bus_0_0)_: ++1 flow(BEV_V2G_storage_BEV_V2G_bus_0_0) +-1 flow(BEV_V2G_bus_BEV_V2G_v2g_0_0) +-1 flow(BEV_V2G_bus_BEV_V2G_2com_0_0) += 0 + +c_e_BusBlock_balance(BEV_V2G_bus_0_1)_: ++1 flow(BEV_V2G_storage_BEV_V2G_bus_0_1) +-1 flow(BEV_V2G_bus_BEV_V2G_v2g_0_1) +-1 flow(BEV_V2G_bus_BEV_V2G_2com_0_1) += 0 + +c_e_BusBlock_balance(BEV_V2G_bus_0_2)_: ++1 flow(BEV_V2G_storage_BEV_V2G_bus_0_2) +-1 flow(BEV_V2G_bus_BEV_V2G_v2g_0_2) +-1 flow(BEV_V2G_bus_BEV_V2G_2com_0_2) += 0 + +c_e_BusBlock_balance(BEV_inflex_bus_0_0)_: ++1 flow(BEV_inflex_storage_BEV_inflex_bus_0_0) +-1 flow(BEV_inflex_bus_BEV_inflex_2com_0_0) += 0 + +c_e_BusBlock_balance(BEV_inflex_bus_0_1)_: ++1 flow(BEV_inflex_storage_BEV_inflex_bus_0_1) +-1 flow(BEV_inflex_bus_BEV_inflex_2com_0_1) += 0 + +c_e_BusBlock_balance(BEV_inflex_bus_0_2)_: ++1 flow(BEV_inflex_storage_BEV_inflex_bus_0_2) +-1 flow(BEV_inflex_bus_BEV_inflex_2com_0_2) += 0 + +c_e_BusBlock_balance(el_bus_0_0)_: +-1 flow(el_bus_BEV_V2G_storage_0_0) +-1 flow(el_bus_BEV_inflex_storage_0_0) +-1 flow(el_bus_BEV_G2V_storage_0_0) ++1 flow(BEV_V2G_v2g_el_bus_0_0) += 0 + +c_e_BusBlock_balance(el_bus_0_1)_: +-1 flow(el_bus_BEV_V2G_storage_0_1) +-1 flow(el_bus_BEV_inflex_storage_0_1) +-1 flow(el_bus_BEV_G2V_storage_0_1) ++1 flow(BEV_V2G_v2g_el_bus_0_1) += 0 + +c_e_BusBlock_balance(el_bus_0_2)_: +-1 flow(el_bus_BEV_V2G_storage_0_2) +-1 flow(el_bus_BEV_inflex_storage_0_2) +-1 flow(el_bus_BEV_G2V_storage_0_2) ++1 flow(BEV_V2G_v2g_el_bus_0_2) += 0 + +c_e_BusBlock_balance(pkm_bus_0_0)_: ++1 flow(BEV_V2G_2com_pkm_bus_0_0) ++1 flow(BEV_inflex_2com_pkm_bus_0_0) ++1 flow(BEV_G2V_2com_pkm_bus_0_0) += 0 + +c_e_BusBlock_balance(pkm_bus_0_1)_: ++1 flow(BEV_V2G_2com_pkm_bus_0_1) ++1 flow(BEV_inflex_2com_pkm_bus_0_1) ++1 flow(BEV_G2V_2com_pkm_bus_0_1) += 200 + +c_e_BusBlock_balance(pkm_bus_0_2)_: ++1 flow(BEV_V2G_2com_pkm_bus_0_2) ++1 flow(BEV_inflex_2com_pkm_bus_0_2) ++1 flow(BEV_G2V_2com_pkm_bus_0_2) += 0 + +c_e_ConverterBlock_relation(BEV_V2G_2com_BEV_V2G_bus_pkm_bus_0_0)_: ++0.6944444444444445 flow(BEV_V2G_bus_BEV_V2G_2com_0_0) +-1 flow(BEV_V2G_2com_pkm_bus_0_0) += 0 + +c_e_ConverterBlock_relation(BEV_inflex_2com_BEV_inflex_bus_pkm_bus_0_0)_: ++0.6944444444444445 flow(BEV_inflex_bus_BEV_inflex_2com_0_0) +-1 flow(BEV_inflex_2com_pkm_bus_0_0) += 0 + +c_e_ConverterBlock_relation(BEV_V2G_v2g_BEV_V2G_bus_el_bus_0_0)_: ++0.8333333333333334 flow(BEV_V2G_bus_BEV_V2G_v2g_0_0) +-1 flow(BEV_V2G_v2g_el_bus_0_0) += 0 + +c_e_ConverterBlock_relation(BEV_G2V_2com_BEV_G2V_bus_pkm_bus_0_0)_: ++0.6944444444444445 flow(BEV_G2V_bus_BEV_G2V_2com_0_0) +-1 flow(BEV_G2V_2com_pkm_bus_0_0) += 0 + +c_e_ConverterBlock_relation(BEV_V2G_2com_BEV_V2G_bus_pkm_bus_0_1)_: ++0.6944444444444445 flow(BEV_V2G_bus_BEV_V2G_2com_0_1) +-1 flow(BEV_V2G_2com_pkm_bus_0_1) += 0 + +c_e_ConverterBlock_relation(BEV_inflex_2com_BEV_inflex_bus_pkm_bus_0_1)_: ++0.6944444444444445 flow(BEV_inflex_bus_BEV_inflex_2com_0_1) +-1 flow(BEV_inflex_2com_pkm_bus_0_1) += 0 + +c_e_ConverterBlock_relation(BEV_V2G_v2g_BEV_V2G_bus_el_bus_0_1)_: ++0.8333333333333334 flow(BEV_V2G_bus_BEV_V2G_v2g_0_1) +-1 flow(BEV_V2G_v2g_el_bus_0_1) += 0 + +c_e_ConverterBlock_relation(BEV_G2V_2com_BEV_G2V_bus_pkm_bus_0_1)_: ++0.6944444444444445 flow(BEV_G2V_bus_BEV_G2V_2com_0_1) +-1 flow(BEV_G2V_2com_pkm_bus_0_1) += 0 + +c_e_ConverterBlock_relation(BEV_V2G_2com_BEV_V2G_bus_pkm_bus_0_2)_: ++0.6944444444444445 flow(BEV_V2G_bus_BEV_V2G_2com_0_2) +-1 flow(BEV_V2G_2com_pkm_bus_0_2) += 0 + +c_e_ConverterBlock_relation(BEV_inflex_2com_BEV_inflex_bus_pkm_bus_0_2)_: ++0.6944444444444445 flow(BEV_inflex_bus_BEV_inflex_2com_0_2) +-1 flow(BEV_inflex_2com_pkm_bus_0_2) += 0 + +c_e_ConverterBlock_relation(BEV_V2G_v2g_BEV_V2G_bus_el_bus_0_2)_: ++0.8333333333333334 flow(BEV_V2G_bus_BEV_V2G_v2g_0_2) +-1 flow(BEV_V2G_v2g_el_bus_0_2) += 0 + +c_e_ConverterBlock_relation(BEV_G2V_2com_BEV_G2V_bus_pkm_bus_0_2)_: ++0.6944444444444445 flow(BEV_G2V_bus_BEV_G2V_2com_0_2) +-1 flow(BEV_G2V_2com_pkm_bus_0_2) += 0 + +c_e_InvestmentFlowBlock_total_rule(el_bus_BEV_inflex_storage_0)_: +-1 InvestmentFlowBlock_invest(el_bus_BEV_inflex_storage_0) ++1 InvestmentFlowBlock_total(el_bus_BEV_inflex_storage_0) += 0 + +c_e_InvestmentFlowBlock_total_rule(BEV_inflex_storage_BEV_inflex_bus_0)_: +-1 InvestmentFlowBlock_invest(BEV_inflex_storage_BEV_inflex_bus_0) ++1 InvestmentFlowBlock_total(BEV_inflex_storage_BEV_inflex_bus_0) += 0 + +c_e_InvestmentFlowBlock_total_rule(BEV_G2V_2com_pkm_bus_0)_: +-1 InvestmentFlowBlock_invest(BEV_G2V_2com_pkm_bus_0) ++1 InvestmentFlowBlock_total(BEV_G2V_2com_pkm_bus_0) += 0 + +c_e_InvestmentFlowBlock_total_rule(el_bus_BEV_V2G_storage_0)_: +-1 InvestmentFlowBlock_invest(el_bus_BEV_V2G_storage_0) ++1 InvestmentFlowBlock_total(el_bus_BEV_V2G_storage_0) += 0 + +c_e_InvestmentFlowBlock_total_rule(BEV_G2V_storage_BEV_G2V_bus_0)_: +-1 InvestmentFlowBlock_invest(BEV_G2V_storage_BEV_G2V_bus_0) ++1 InvestmentFlowBlock_total(BEV_G2V_storage_BEV_G2V_bus_0) += 0 + +c_e_InvestmentFlowBlock_total_rule(BEV_V2G_storage_BEV_V2G_bus_0)_: +-1 InvestmentFlowBlock_invest(BEV_V2G_storage_BEV_V2G_bus_0) ++1 InvestmentFlowBlock_total(BEV_V2G_storage_BEV_V2G_bus_0) += 0 + +c_e_InvestmentFlowBlock_total_rule(BEV_V2G_v2g_el_bus_0)_: +-1 InvestmentFlowBlock_invest(BEV_V2G_v2g_el_bus_0) ++1 InvestmentFlowBlock_total(BEV_V2G_v2g_el_bus_0) += 0 + +c_e_InvestmentFlowBlock_total_rule(BEV_V2G_2com_pkm_bus_0)_: +-1 InvestmentFlowBlock_invest(BEV_V2G_2com_pkm_bus_0) ++1 InvestmentFlowBlock_total(BEV_V2G_2com_pkm_bus_0) += 0 + +c_e_InvestmentFlowBlock_total_rule(el_bus_BEV_G2V_storage_0)_: +-1 InvestmentFlowBlock_invest(el_bus_BEV_G2V_storage_0) ++1 InvestmentFlowBlock_total(el_bus_BEV_G2V_storage_0) += 0 + +c_e_InvestmentFlowBlock_total_rule(BEV_inflex_2com_pkm_bus_0)_: +-1 InvestmentFlowBlock_invest(BEV_inflex_2com_pkm_bus_0) ++1 InvestmentFlowBlock_total(BEV_inflex_2com_pkm_bus_0) += 0 + +c_e_InvestmentFlowBlock_fixed(el_bus_BEV_inflex_storage_0_0)_: ++1 flow(el_bus_BEV_inflex_storage_0_0) +-0.89856 InvestmentFlowBlock_total(el_bus_BEV_inflex_storage_0) += 0 + +c_e_InvestmentFlowBlock_fixed(el_bus_BEV_inflex_storage_0_1)_: ++1 flow(el_bus_BEV_inflex_storage_0_1) += 0 + +c_e_InvestmentFlowBlock_fixed(el_bus_BEV_inflex_storage_0_2)_: ++1 flow(el_bus_BEV_inflex_storage_0_2) += 0 + +c_e_InvestmentFlowBlock_fixed(BEV_inflex_storage_BEV_inflex_bus_0_0)_: ++1 flow(BEV_inflex_storage_BEV_inflex_bus_0_0) +-0.16 InvestmentFlowBlock_total(BEV_inflex_storage_BEV_inflex_bus_0) += 0 + +c_e_InvestmentFlowBlock_fixed(BEV_inflex_storage_BEV_inflex_bus_0_1)_: ++1 flow(BEV_inflex_storage_BEV_inflex_bus_0_1) +-0.08 InvestmentFlowBlock_total(BEV_inflex_storage_BEV_inflex_bus_0) += 0 + +c_e_InvestmentFlowBlock_fixed(BEV_inflex_storage_BEV_inflex_bus_0_2)_: ++1 flow(BEV_inflex_storage_BEV_inflex_bus_0_2) +-0.16 InvestmentFlowBlock_total(BEV_inflex_storage_BEV_inflex_bus_0) += 0 + +c_u_InvestmentFlowBlock_max(BEV_G2V_2com_pkm_bus_0_0)_: ++1 flow(BEV_G2V_2com_pkm_bus_0_0) +-1 InvestmentFlowBlock_total(BEV_G2V_2com_pkm_bus_0) +<= 0 + +c_u_InvestmentFlowBlock_max(BEV_G2V_2com_pkm_bus_0_1)_: ++1 flow(BEV_G2V_2com_pkm_bus_0_1) +-1 InvestmentFlowBlock_total(BEV_G2V_2com_pkm_bus_0) +<= 0 + +c_u_InvestmentFlowBlock_max(BEV_G2V_2com_pkm_bus_0_2)_: ++1 flow(BEV_G2V_2com_pkm_bus_0_2) +-1 InvestmentFlowBlock_total(BEV_G2V_2com_pkm_bus_0) +<= 0 + +c_u_InvestmentFlowBlock_max(el_bus_BEV_V2G_storage_0_0)_: ++1 flow(el_bus_BEV_V2G_storage_0_0) +-1 InvestmentFlowBlock_total(el_bus_BEV_V2G_storage_0) +<= 0 + +c_u_InvestmentFlowBlock_max(el_bus_BEV_V2G_storage_0_1)_: ++1 flow(el_bus_BEV_V2G_storage_0_1) +-1 InvestmentFlowBlock_total(el_bus_BEV_V2G_storage_0) +<= 0 + +c_u_InvestmentFlowBlock_max(el_bus_BEV_V2G_storage_0_2)_: ++1 flow(el_bus_BEV_V2G_storage_0_2) +-1 InvestmentFlowBlock_total(el_bus_BEV_V2G_storage_0) +<= 0 + +c_u_InvestmentFlowBlock_max(BEV_G2V_storage_BEV_G2V_bus_0_0)_: ++1 flow(BEV_G2V_storage_BEV_G2V_bus_0_0) +-1 InvestmentFlowBlock_total(BEV_G2V_storage_BEV_G2V_bus_0) +<= 0 + +c_u_InvestmentFlowBlock_max(BEV_G2V_storage_BEV_G2V_bus_0_1)_: ++1 flow(BEV_G2V_storage_BEV_G2V_bus_0_1) +-1 InvestmentFlowBlock_total(BEV_G2V_storage_BEV_G2V_bus_0) +<= 0 + +c_u_InvestmentFlowBlock_max(BEV_G2V_storage_BEV_G2V_bus_0_2)_: ++1 flow(BEV_G2V_storage_BEV_G2V_bus_0_2) +-1 InvestmentFlowBlock_total(BEV_G2V_storage_BEV_G2V_bus_0) +<= 0 + +c_u_InvestmentFlowBlock_max(BEV_V2G_storage_BEV_V2G_bus_0_0)_: ++1 flow(BEV_V2G_storage_BEV_V2G_bus_0_0) +-1 InvestmentFlowBlock_total(BEV_V2G_storage_BEV_V2G_bus_0) +<= 0 + +c_u_InvestmentFlowBlock_max(BEV_V2G_storage_BEV_V2G_bus_0_1)_: ++1 flow(BEV_V2G_storage_BEV_V2G_bus_0_1) +-1 InvestmentFlowBlock_total(BEV_V2G_storage_BEV_V2G_bus_0) +<= 0 + +c_u_InvestmentFlowBlock_max(BEV_V2G_storage_BEV_V2G_bus_0_2)_: ++1 flow(BEV_V2G_storage_BEV_V2G_bus_0_2) +-1 InvestmentFlowBlock_total(BEV_V2G_storage_BEV_V2G_bus_0) +<= 0 + +c_u_InvestmentFlowBlock_max(BEV_V2G_v2g_el_bus_0_0)_: ++1 flow(BEV_V2G_v2g_el_bus_0_0) +-1 InvestmentFlowBlock_total(BEV_V2G_v2g_el_bus_0) +<= 0 + +c_u_InvestmentFlowBlock_max(BEV_V2G_v2g_el_bus_0_1)_: ++1 flow(BEV_V2G_v2g_el_bus_0_1) +-1 InvestmentFlowBlock_total(BEV_V2G_v2g_el_bus_0) +<= 0 + +c_u_InvestmentFlowBlock_max(BEV_V2G_v2g_el_bus_0_2)_: ++1 flow(BEV_V2G_v2g_el_bus_0_2) +-1 InvestmentFlowBlock_total(BEV_V2G_v2g_el_bus_0) +<= 0 + +c_u_InvestmentFlowBlock_max(BEV_V2G_2com_pkm_bus_0_0)_: ++1 flow(BEV_V2G_2com_pkm_bus_0_0) +-1 InvestmentFlowBlock_total(BEV_V2G_2com_pkm_bus_0) +<= 0 + +c_u_InvestmentFlowBlock_max(BEV_V2G_2com_pkm_bus_0_1)_: ++1 flow(BEV_V2G_2com_pkm_bus_0_1) +-1 InvestmentFlowBlock_total(BEV_V2G_2com_pkm_bus_0) +<= 0 + +c_u_InvestmentFlowBlock_max(BEV_V2G_2com_pkm_bus_0_2)_: ++1 flow(BEV_V2G_2com_pkm_bus_0_2) +-1 InvestmentFlowBlock_total(BEV_V2G_2com_pkm_bus_0) +<= 0 + +c_u_InvestmentFlowBlock_max(el_bus_BEV_G2V_storage_0_0)_: ++1 flow(el_bus_BEV_G2V_storage_0_0) +-1 InvestmentFlowBlock_total(el_bus_BEV_G2V_storage_0) +<= 0 + +c_u_InvestmentFlowBlock_max(el_bus_BEV_G2V_storage_0_1)_: ++1 flow(el_bus_BEV_G2V_storage_0_1) +-1 InvestmentFlowBlock_total(el_bus_BEV_G2V_storage_0) +<= 0 + +c_u_InvestmentFlowBlock_max(el_bus_BEV_G2V_storage_0_2)_: ++1 flow(el_bus_BEV_G2V_storage_0_2) +-1 InvestmentFlowBlock_total(el_bus_BEV_G2V_storage_0) +<= 0 + +c_u_InvestmentFlowBlock_max(BEV_inflex_2com_pkm_bus_0_0)_: ++1 flow(BEV_inflex_2com_pkm_bus_0_0) +-1 InvestmentFlowBlock_total(BEV_inflex_2com_pkm_bus_0) +<= 0 + +c_u_InvestmentFlowBlock_max(BEV_inflex_2com_pkm_bus_0_1)_: ++1 flow(BEV_inflex_2com_pkm_bus_0_1) +-1 InvestmentFlowBlock_total(BEV_inflex_2com_pkm_bus_0) +<= 0 + +c_u_InvestmentFlowBlock_max(BEV_inflex_2com_pkm_bus_0_2)_: ++1 flow(BEV_inflex_2com_pkm_bus_0_2) +-1 InvestmentFlowBlock_total(BEV_inflex_2com_pkm_bus_0) +<= 0 + +c_e_InvestmentFlowBlock_mob_share_BEV_V2G_0_: +-0.3 InvestmentFlowBlock_invest(el_bus_BEV_inflex_storage_0) ++0.7 InvestmentFlowBlock_invest(el_bus_BEV_V2G_storage_0) +-0.3 InvestmentFlowBlock_invest(el_bus_BEV_G2V_storage_0) += 0 + +c_e_InvestmentFlowBlock_mob_share_BEV_inflex_0_: ++0.8 InvestmentFlowBlock_invest(el_bus_BEV_inflex_storage_0) +-0.2 InvestmentFlowBlock_invest(el_bus_BEV_V2G_storage_0) +-0.2 InvestmentFlowBlock_invest(el_bus_BEV_G2V_storage_0) += 0 + +c_e_InvestmentFlowBlock_mob_share_BEV_G2V_0_: +-0.5 InvestmentFlowBlock_invest(el_bus_BEV_inflex_storage_0) +-0.5 InvestmentFlowBlock_invest(el_bus_BEV_V2G_storage_0) ++0.5 InvestmentFlowBlock_invest(el_bus_BEV_G2V_storage_0) += 0 + +c_e_InvestmentFlowBlock__BEV_V2G_equal_invest_0_(0)__: +-1 InvestmentFlowBlock_invest(BEV_V2G_storage_BEV_V2G_bus_0) ++1 InvestmentFlowBlock_invest(BEV_V2G_v2g_el_bus_0) += 0 + +c_e_InvestmentFlowBlock__BEV_V2G_equal_invest_1_(0)__: ++1 InvestmentFlowBlock_invest(BEV_V2G_storage_BEV_V2G_bus_0) +-1 InvestmentFlowBlock_invest(BEV_V2G_2com_pkm_bus_0) += 0 + +c_e_InvestmentFlowBlock__BEV_V2G_equal_invest_2_(0)__: +-1 InvestmentFlowBlock_invest(el_bus_BEV_V2G_storage_0) ++1 InvestmentFlowBlock_invest(BEV_V2G_2com_pkm_bus_0) += 0 + +c_e_InvestmentFlowBlock__BEV_inflex_equal_invest_0_(0)__: ++1 InvestmentFlowBlock_invest(el_bus_BEV_inflex_storage_0) +-1 InvestmentFlowBlock_invest(BEV_inflex_storage_BEV_inflex_bus_0) += 0 + +c_e_InvestmentFlowBlock__BEV_inflex_equal_invest_1_(0)__: ++1 InvestmentFlowBlock_invest(BEV_inflex_storage_BEV_inflex_bus_0) +-1 InvestmentFlowBlock_invest(BEV_inflex_2com_pkm_bus_0) += 0 + +c_e_InvestmentFlowBlock__BEV_G2V_equal_invest_0_(0)__: ++1 InvestmentFlowBlock_invest(BEV_G2V_2com_pkm_bus_0) +-1 InvestmentFlowBlock_invest(el_bus_BEV_G2V_storage_0) += 0 + +c_e_InvestmentFlowBlock__BEV_G2V_equal_invest_1_(0)__: +-1 InvestmentFlowBlock_invest(BEV_G2V_storage_BEV_G2V_bus_0) ++1 InvestmentFlowBlock_invest(el_bus_BEV_G2V_storage_0) += 0 + +c_e_GenericInvestmentStorageBlock_total_storage_rule(BEV_G2V_storage_0)_: ++1 GenericInvestmentStorageBlock_total(BEV_G2V_storage_0) +-1 GenericInvestmentStorageBlock_invest(BEV_G2V_storage_0) += 0 + +c_e_GenericInvestmentStorageBlock_total_storage_rule(BEV_V2G_storage_0)_: ++1 GenericInvestmentStorageBlock_total(BEV_V2G_storage_0) +-1 GenericInvestmentStorageBlock_invest(BEV_V2G_storage_0) += 0 + +c_e_GenericInvestmentStorageBlock_total_storage_rule(BEV_inflex_storage_0)_: ++1 GenericInvestmentStorageBlock_total(BEV_inflex_storage_0) +-1 GenericInvestmentStorageBlock_invest(BEV_inflex_storage_0) += 0 + +c_e_GenericInvestmentStorageBlock_init_content_fix(BEV_G2V_storage)_: ++1 GenericInvestmentStorageBlock_init_content(BEV_G2V_storage) += 0 + +c_e_GenericInvestmentStorageBlock_init_content_fix(BEV_V2G_storage)_: ++1 GenericInvestmentStorageBlock_init_content(BEV_V2G_storage) += 0 + +c_e_GenericInvestmentStorageBlock_init_content_fix(BEV_inflex_storage)_: ++1 GenericInvestmentStorageBlock_init_content(BEV_inflex_storage) += 0 + +c_e_GenericInvestmentStorageBlock_balance_first(BEV_G2V_storage)_: +-0.6944444444444445 flow(el_bus_BEV_G2V_storage_0_0) ++1.2 flow(BEV_G2V_storage_BEV_G2V_bus_0_0) +-1 GenericInvestmentStorageBlock_init_content(BEV_G2V_storage) ++1 GenericInvestmentStorageBlock_storage_content(BEV_G2V_storage_0) += 0 + +c_e_GenericInvestmentStorageBlock_balance_first(BEV_V2G_storage)_: +-0.6944444444444445 flow(el_bus_BEV_V2G_storage_0_0) ++1.2 flow(BEV_V2G_storage_BEV_V2G_bus_0_0) +-1 GenericInvestmentStorageBlock_init_content(BEV_V2G_storage) ++1 GenericInvestmentStorageBlock_storage_content(BEV_V2G_storage_0) += 0 + +c_e_GenericInvestmentStorageBlock_balance_first(BEV_inflex_storage)_: +-0.6944444444444445 flow(el_bus_BEV_inflex_storage_0_0) ++1.2 flow(BEV_inflex_storage_BEV_inflex_bus_0_0) +-1 GenericInvestmentStorageBlock_init_content(BEV_inflex_storage) ++1 GenericInvestmentStorageBlock_storage_content(BEV_inflex_storage_0) += 0 + +c_e_GenericInvestmentStorageBlock_balance(BEV_G2V_storage_0_1)_: +-0.6944444444444445 flow(el_bus_BEV_G2V_storage_0_1) ++1.2 flow(BEV_G2V_storage_BEV_G2V_bus_0_1) +-1 GenericInvestmentStorageBlock_storage_content(BEV_G2V_storage_0) ++1 GenericInvestmentStorageBlock_storage_content(BEV_G2V_storage_1) += 0 + +c_e_GenericInvestmentStorageBlock_balance(BEV_G2V_storage_0_2)_: +-0.6944444444444445 flow(el_bus_BEV_G2V_storage_0_2) ++1.2 flow(BEV_G2V_storage_BEV_G2V_bus_0_2) +-1 GenericInvestmentStorageBlock_storage_content(BEV_G2V_storage_1) ++1 GenericInvestmentStorageBlock_storage_content(BEV_G2V_storage_2) += 0 + +c_e_GenericInvestmentStorageBlock_balance(BEV_V2G_storage_0_1)_: +-0.6944444444444445 flow(el_bus_BEV_V2G_storage_0_1) ++1.2 flow(BEV_V2G_storage_BEV_V2G_bus_0_1) +-1 GenericInvestmentStorageBlock_storage_content(BEV_V2G_storage_0) ++1 GenericInvestmentStorageBlock_storage_content(BEV_V2G_storage_1) += 0 + +c_e_GenericInvestmentStorageBlock_balance(BEV_V2G_storage_0_2)_: +-0.6944444444444445 flow(el_bus_BEV_V2G_storage_0_2) ++1.2 flow(BEV_V2G_storage_BEV_V2G_bus_0_2) +-1 GenericInvestmentStorageBlock_storage_content(BEV_V2G_storage_1) ++1 GenericInvestmentStorageBlock_storage_content(BEV_V2G_storage_2) += 0 + +c_e_GenericInvestmentStorageBlock_balance(BEV_inflex_storage_0_1)_: +-0.6944444444444445 flow(el_bus_BEV_inflex_storage_0_1) ++1.2 flow(BEV_inflex_storage_BEV_inflex_bus_0_1) +-1 GenericInvestmentStorageBlock_storage_content(BEV_inflex_storage_0) ++1 GenericInvestmentStorageBlock_storage_content(BEV_inflex_storage_1) += 0 + +c_e_GenericInvestmentStorageBlock_balance(BEV_inflex_storage_0_2)_: +-0.6944444444444445 flow(el_bus_BEV_inflex_storage_0_2) ++1.2 flow(BEV_inflex_storage_BEV_inflex_bus_0_2) +-1 GenericInvestmentStorageBlock_storage_content(BEV_inflex_storage_1) ++1 GenericInvestmentStorageBlock_storage_content(BEV_inflex_storage_2) += 0 + +c_e_GenericInvestmentStorageBlock_balanced_cstr(BEV_G2V_storage)_: +-1 GenericInvestmentStorageBlock_init_content(BEV_G2V_storage) ++1 GenericInvestmentStorageBlock_storage_content(BEV_G2V_storage_2) += 0 + +c_e_GenericInvestmentStorageBlock_balanced_cstr(BEV_V2G_storage)_: +-1 GenericInvestmentStorageBlock_init_content(BEV_V2G_storage) ++1 GenericInvestmentStorageBlock_storage_content(BEV_V2G_storage_2) += 0 + +c_e_GenericInvestmentStorageBlock_balanced_cstr(BEV_inflex_storage)_: +-1 GenericInvestmentStorageBlock_init_content(BEV_inflex_storage) ++1 GenericInvestmentStorageBlock_storage_content(BEV_inflex_storage_2) += 0 + +c_e_GenericInvestmentStorageBlock_power_coupled(BEV_G2V_storage_0)_: ++1 InvestmentFlowBlock_total(BEV_G2V_storage_BEV_G2V_bus_0) +-1 InvestmentFlowBlock_total(el_bus_BEV_G2V_storage_0) += 0 + +c_e_GenericInvestmentStorageBlock_power_coupled(BEV_V2G_storage_0)_: +-1 InvestmentFlowBlock_total(el_bus_BEV_V2G_storage_0) ++1 InvestmentFlowBlock_total(BEV_V2G_storage_BEV_V2G_bus_0) += 0 + +c_e_GenericInvestmentStorageBlock_power_coupled(BEV_inflex_storage_0)_: +-1 InvestmentFlowBlock_total(el_bus_BEV_inflex_storage_0) ++1 InvestmentFlowBlock_total(BEV_inflex_storage_BEV_inflex_bus_0) += 0 + +c_e_GenericInvestmentStorageBlock_storage_capacity_inflow(BEV_G2V_storage_0)_: ++1 InvestmentFlowBlock_total(el_bus_BEV_G2V_storage_0) +-0.3333333333333333 GenericInvestmentStorageBlock_total(BEV_G2V_storage_0) += 0 + +c_e_GenericInvestmentStorageBlock_storage_capacity_inflow(BEV_V2G_storage_0)_: ++1 InvestmentFlowBlock_total(el_bus_BEV_V2G_storage_0) +-0.3333333333333333 GenericInvestmentStorageBlock_total(BEV_V2G_storage_0) += 0 + +c_e_GenericInvestmentStorageBlock_storage_capacity_inflow(BEV_inflex_storage_0)_: ++1 InvestmentFlowBlock_total(el_bus_BEV_inflex_storage_0) +-0.3333333333333333 GenericInvestmentStorageBlock_total(BEV_inflex_storage_0) += 0 + +c_e_GenericInvestmentStorageBlock_storage_capacity_outflow(BEV_G2V_storage_0)_: ++1 InvestmentFlowBlock_total(BEV_G2V_storage_BEV_G2V_bus_0) +-0.3333333333333333 GenericInvestmentStorageBlock_total(BEV_G2V_storage_0) += 0 + +c_e_GenericInvestmentStorageBlock_storage_capacity_outflow(BEV_V2G_storage_0)_: ++1 InvestmentFlowBlock_total(BEV_V2G_storage_BEV_V2G_bus_0) +-0.3333333333333333 GenericInvestmentStorageBlock_total(BEV_V2G_storage_0) += 0 + +c_e_GenericInvestmentStorageBlock_storage_capacity_outflow(BEV_inflex_storage_0)_: ++1 InvestmentFlowBlock_total(BEV_inflex_storage_BEV_inflex_bus_0) +-0.3333333333333333 GenericInvestmentStorageBlock_total(BEV_inflex_storage_0) += 0 + +c_u_GenericInvestmentStorageBlock_max_storage_content(BEV_G2V_storage_0_0)_: +-1 GenericInvestmentStorageBlock_total(BEV_G2V_storage_0) ++1 GenericInvestmentStorageBlock_storage_content(BEV_G2V_storage_0) +<= 0 + +c_u_GenericInvestmentStorageBlock_max_storage_content(BEV_G2V_storage_0_1)_: +-1 GenericInvestmentStorageBlock_total(BEV_G2V_storage_0) ++1 GenericInvestmentStorageBlock_storage_content(BEV_G2V_storage_1) +<= 0 + +c_u_GenericInvestmentStorageBlock_max_storage_content(BEV_G2V_storage_0_2)_: +-1 GenericInvestmentStorageBlock_total(BEV_G2V_storage_0) ++1 GenericInvestmentStorageBlock_storage_content(BEV_G2V_storage_2) +<= 0 + +c_u_GenericInvestmentStorageBlock_max_storage_content(BEV_V2G_storage_0_0)_: +-1 GenericInvestmentStorageBlock_total(BEV_V2G_storage_0) ++1 GenericInvestmentStorageBlock_storage_content(BEV_V2G_storage_0) +<= 0 + +c_u_GenericInvestmentStorageBlock_max_storage_content(BEV_V2G_storage_0_1)_: +-1 GenericInvestmentStorageBlock_total(BEV_V2G_storage_0) ++1 GenericInvestmentStorageBlock_storage_content(BEV_V2G_storage_1) +<= 0 + +c_u_GenericInvestmentStorageBlock_max_storage_content(BEV_V2G_storage_0_2)_: +-1 GenericInvestmentStorageBlock_total(BEV_V2G_storage_0) ++1 GenericInvestmentStorageBlock_storage_content(BEV_V2G_storage_2) +<= 0 + +c_u_GenericInvestmentStorageBlock_max_storage_content(BEV_inflex_storage_0_0)_: +-1 GenericInvestmentStorageBlock_total(BEV_inflex_storage_0) ++1 GenericInvestmentStorageBlock_storage_content(BEV_inflex_storage_0) +<= 0 + +c_u_GenericInvestmentStorageBlock_max_storage_content(BEV_inflex_storage_0_1)_: +-1 GenericInvestmentStorageBlock_total(BEV_inflex_storage_0) ++1 GenericInvestmentStorageBlock_storage_content(BEV_inflex_storage_1) +<= 0 + +c_u_GenericInvestmentStorageBlock_max_storage_content(BEV_inflex_storage_0_2)_: +-1 GenericInvestmentStorageBlock_total(BEV_inflex_storage_0) ++1 GenericInvestmentStorageBlock_storage_content(BEV_inflex_storage_2) +<= 0 + +bounds + 0 <= InvestmentFlowBlock_invest(el_bus_BEV_inflex_storage_0) <= +inf + 0 <= InvestmentFlowBlock_invest(BEV_inflex_storage_BEV_inflex_bus_0) <= +inf + 0 <= InvestmentFlowBlock_invest(BEV_G2V_2com_pkm_bus_0) <= +inf + 0 <= InvestmentFlowBlock_invest(el_bus_BEV_V2G_storage_0) <= +inf + 0 <= InvestmentFlowBlock_invest(BEV_G2V_storage_BEV_G2V_bus_0) <= +inf + 0 <= InvestmentFlowBlock_invest(BEV_V2G_storage_BEV_V2G_bus_0) <= +inf + 0 <= InvestmentFlowBlock_invest(BEV_V2G_v2g_el_bus_0) <= +inf + 0 <= InvestmentFlowBlock_invest(BEV_V2G_2com_pkm_bus_0) <= +inf + 0 <= InvestmentFlowBlock_invest(el_bus_BEV_G2V_storage_0) <= +inf + 0 <= InvestmentFlowBlock_invest(BEV_inflex_2com_pkm_bus_0) <= +inf + 0 <= flow(el_bus_BEV_V2G_storage_0_0) <= +inf + 0 <= flow(el_bus_BEV_V2G_storage_0_1) <= +inf + 0 <= flow(el_bus_BEV_V2G_storage_0_2) <= +inf + 0 <= flow(el_bus_BEV_inflex_storage_0_0) <= +inf + 0 <= flow(el_bus_BEV_inflex_storage_0_1) <= +inf + 0 <= flow(el_bus_BEV_inflex_storage_0_2) <= +inf + 0 <= flow(el_bus_BEV_G2V_storage_0_0) <= +inf + 0 <= flow(el_bus_BEV_G2V_storage_0_1) <= +inf + 0 <= flow(el_bus_BEV_G2V_storage_0_2) <= +inf + 0 <= flow(BEV_V2G_storage_BEV_V2G_bus_0_0) <= +inf + 0 <= flow(BEV_V2G_storage_BEV_V2G_bus_0_1) <= +inf + 0 <= flow(BEV_V2G_storage_BEV_V2G_bus_0_2) <= +inf + 0 <= flow(BEV_V2G_bus_BEV_V2G_v2g_0_0) <= +inf + 0 <= flow(BEV_V2G_bus_BEV_V2G_v2g_0_1) <= +inf + 0 <= flow(BEV_V2G_bus_BEV_V2G_v2g_0_2) <= +inf + 0 <= flow(BEV_V2G_bus_BEV_V2G_2com_0_0) <= +inf + 0 <= flow(BEV_V2G_bus_BEV_V2G_2com_0_1) <= +inf + 0 <= flow(BEV_V2G_bus_BEV_V2G_2com_0_2) <= +inf + 0 <= flow(BEV_V2G_v2g_el_bus_0_0) <= +inf + 0 <= flow(BEV_V2G_v2g_el_bus_0_1) <= +inf + 0 <= flow(BEV_V2G_v2g_el_bus_0_2) <= +inf + 0 <= flow(BEV_V2G_2com_pkm_bus_0_0) <= +inf + 0 <= flow(BEV_V2G_2com_pkm_bus_0_1) <= +inf + 0 <= flow(BEV_V2G_2com_pkm_bus_0_2) <= +inf + 0 <= flow(BEV_inflex_storage_BEV_inflex_bus_0_0) <= +inf + 0 <= flow(BEV_inflex_storage_BEV_inflex_bus_0_1) <= +inf + 0 <= flow(BEV_inflex_storage_BEV_inflex_bus_0_2) <= +inf + 0 <= flow(BEV_inflex_bus_BEV_inflex_2com_0_0) <= +inf + 0 <= flow(BEV_inflex_bus_BEV_inflex_2com_0_1) <= +inf + 0 <= flow(BEV_inflex_bus_BEV_inflex_2com_0_2) <= +inf + 0 <= flow(BEV_inflex_2com_pkm_bus_0_0) <= +inf + 0 <= flow(BEV_inflex_2com_pkm_bus_0_1) <= +inf + 0 <= flow(BEV_inflex_2com_pkm_bus_0_2) <= +inf + 0 <= flow(BEV_G2V_storage_BEV_G2V_bus_0_0) <= +inf + 0 <= flow(BEV_G2V_storage_BEV_G2V_bus_0_1) <= +inf + 0 <= flow(BEV_G2V_storage_BEV_G2V_bus_0_2) <= +inf + 0 <= flow(BEV_G2V_bus_BEV_G2V_2com_0_0) <= +inf + 0 <= flow(BEV_G2V_bus_BEV_G2V_2com_0_1) <= +inf + 0 <= flow(BEV_G2V_bus_BEV_G2V_2com_0_2) <= +inf + 0 <= flow(BEV_G2V_2com_pkm_bus_0_0) <= +inf + 0 <= flow(BEV_G2V_2com_pkm_bus_0_1) <= +inf + 0 <= flow(BEV_G2V_2com_pkm_bus_0_2) <= +inf + 0 <= InvestmentFlowBlock_total(el_bus_BEV_inflex_storage_0) <= +inf + 0 <= InvestmentFlowBlock_total(BEV_inflex_storage_BEV_inflex_bus_0) <= +inf + 0 <= InvestmentFlowBlock_total(BEV_G2V_2com_pkm_bus_0) <= +inf + 0 <= InvestmentFlowBlock_total(el_bus_BEV_V2G_storage_0) <= +inf + 0 <= InvestmentFlowBlock_total(BEV_G2V_storage_BEV_G2V_bus_0) <= +inf + 0 <= InvestmentFlowBlock_total(BEV_V2G_storage_BEV_V2G_bus_0) <= +inf + 0 <= InvestmentFlowBlock_total(BEV_V2G_v2g_el_bus_0) <= +inf + 0 <= InvestmentFlowBlock_total(BEV_V2G_2com_pkm_bus_0) <= +inf + 0 <= InvestmentFlowBlock_total(el_bus_BEV_G2V_storage_0) <= +inf + 0 <= InvestmentFlowBlock_total(BEV_inflex_2com_pkm_bus_0) <= +inf + 0 <= GenericInvestmentStorageBlock_total(BEV_G2V_storage_0) <= +inf + 0 <= GenericInvestmentStorageBlock_total(BEV_V2G_storage_0) <= +inf + 0 <= GenericInvestmentStorageBlock_total(BEV_inflex_storage_0) <= +inf + 0 <= GenericInvestmentStorageBlock_invest(BEV_G2V_storage_0) <= +inf + 0 <= GenericInvestmentStorageBlock_invest(BEV_V2G_storage_0) <= +inf + 0 <= GenericInvestmentStorageBlock_invest(BEV_inflex_storage_0) <= +inf + 0 <= GenericInvestmentStorageBlock_init_content(BEV_G2V_storage) <= +inf + 0 <= GenericInvestmentStorageBlock_init_content(BEV_V2G_storage) <= +inf + 0 <= GenericInvestmentStorageBlock_init_content(BEV_inflex_storage) <= +inf + 0 <= GenericInvestmentStorageBlock_storage_content(BEV_G2V_storage_0) <= +inf + 0 <= GenericInvestmentStorageBlock_storage_content(BEV_G2V_storage_1) <= +inf + 0 <= GenericInvestmentStorageBlock_storage_content(BEV_G2V_storage_2) <= +inf + 0 <= GenericInvestmentStorageBlock_storage_content(BEV_V2G_storage_0) <= +inf + 0 <= GenericInvestmentStorageBlock_storage_content(BEV_V2G_storage_1) <= +inf + 0 <= GenericInvestmentStorageBlock_storage_content(BEV_V2G_storage_2) <= +inf + 0 <= GenericInvestmentStorageBlock_storage_content(BEV_inflex_storage_0) <= +inf + 0 <= GenericInvestmentStorageBlock_storage_content(BEV_inflex_storage_1) <= +inf + 0 <= GenericInvestmentStorageBlock_storage_content(BEV_inflex_storage_2) <= +inf +end diff --git a/tests/mobility.py b/tests/mobility.py new file mode 100644 index 00000000..f8efb283 --- /dev/null +++ b/tests/mobility.py @@ -0,0 +1,229 @@ +import os + +import mobility_plotting as mp +import pandas as pd + +from oemof import solph +from oemof.tabular import __path__ as tabular_path +from oemof.tabular.constraint_facades import CONSTRAINT_TYPE_MAP +from oemof.tabular.datapackage.reading import deserialize_constraints +from oemof.tabular.facades import Excess, Load, Shortage, Volatile +from oemof.tabular.facades.experimental.battery_electric_vehicle import Bev +from oemof.tabular.postprocessing import calculations + +if __name__ == "__main__": + # Single-period example + # date_time_index = pd.date_range("1/1/2020", periods=3, freq="H") + # energysystem = solph.EnergySystem( + # timeindex=date_time_index, + # infer_last_interval=True, + # ) + # periods=[2020] + + # Multi-period example + t_idx_1 = pd.date_range("1/1/2020", periods=3, freq="H") + t_idx_2 = pd.date_range("1/1/2030", periods=3, freq="H") + t_idx_1_series = pd.Series(index=t_idx_1, dtype="float64") + t_idx_2_series = pd.Series(index=t_idx_2, dtype="float64") + date_time_index = pd.concat([t_idx_1_series, t_idx_2_series]).index + periods = [t_idx_1, t_idx_2] + + energysystem = solph.EnergySystem( + timeindex=date_time_index, + infer_last_interval=False, + timeincrement=[1] * len(date_time_index), + periods=periods, + ) + + el_bus = solph.Bus("el-bus") + el_bus.type = "bus" + energysystem.add(el_bus) + + indiv_mob = solph.Bus("pkm-bus") + indiv_mob.type = "bus" + energysystem.add(indiv_mob) + + volatile = Volatile( + type="volatile", + label="wind", + bus=el_bus, + carrier="wind", + tech="onshore", + capacity=200, + capacity_cost=1, + expandable=True, + # expandable=False, + # capacity_potential=1e8, + profile=len(periods) * [1, 0, 1], + lifetime=20, + ) + energysystem.add(volatile) + + load = Load( + label="load", + type="load", + carrier="electricity", + bus=el_bus, + amount=100, + profile=len(periods) * [1, 1, 1], + ) + energysystem.add(load) + + excess = Excess( + type="excess", + label="excess", + bus=el_bus, + carrier="electricity", + tech="excess", + capacity=100, + marginal_cost=10, + ) + energysystem.add(excess) + + shortage = Shortage( + type="shortage", + label="shortage", + bus=el_bus, + carrier="electricity", + tech="shortage", + capacity=1000, + marginal_cost=1e6, + ) + energysystem.add(shortage) + + pkm_demand = Load( + label="pkm_demand", + type="Load", + carrier="pkm", + bus=indiv_mob, + amount=200, # PKM + profile=len(periods) * [0, 1, 0], # drive consumption + ) + + energysystem.add(pkm_demand) + + bev_v2g = Bev( + type="bev", + label="BEV-V2G", + electricity_bus=el_bus, + commodity_bus=indiv_mob, + storage_capacity=150, + # drive_power=150, # nominal value sink + # drive_consumption=[1, 1, 1], # relative value sink + charging_power=150, # existing + availability=len(periods) * [1, 1, 1], + efficiency_charging=1, + v2g=True, + loss_rate=0.01, + min_storage_level=(len(date_time_index) + 0) * [0], + max_storage_level=(len(date_time_index) + 0) * [0.9], + expandable=True, + bev_invest_costs=2, + invest_c_rate=60 / 20, # Capacity/Power + variable_costs=3, + fixed_investment_costs=1, + commodity_conversion_rate=0.7, + lifetime=10, + ) + energysystem.add(bev_v2g) + + bev_flex = Bev( + type="bev", + label="BEV-inflex", + electricity_bus=el_bus, + commodity_bus=indiv_mob, + storage_capacity=200, + drive_power=100, + # drive_consumption=[0, 1, 0], + charging_power=200, + availability=len(periods) * [1, 1, 1], + v2g=False, + # loss_rate=0.01, + # min_storage_level=[0.1, 0.2, 0.15, 0.15], + # max_storage_level=[0.9, 0.95, 0.92, 0.92], + expandable=True, + bev_invest_costs=2, + invest_c_rate=60 / 20, + variable_costs=3, + fixed_investment_costs=1, + commodity_conversion_rate=0.7, + lifetime=10, + ) + energysystem.add(bev_flex) + + bev_fix = Bev( + type="bev", + label="BEV-G2V", + electricity_bus=el_bus, + commodity_bus=indiv_mob, + storage_capacity=200, + drive_power=100, + # drive_consumption=[0, 1, 0], + charging_power=200, + availability=len(periods) * [1, 1, 1], + v2g=False, + # loss_rate=0.01, + # min_storage_level=[0.1, 0.2, 0.15, 0.15], + # max_storage_level=[0.9, 0.95, 0.92, 0.92], + expandable=True, + bev_invest_costs=2, + invest_c_rate=60 / 20, # Capacity/Power + variable_costs=3, + fixed_investment_costs=1, + commodity_conversion_rate=0.7, + input_parameters={ + "fix": len(periods) * [0, 0, 0] + }, # fixed relative charging profile + lifetime=10, + ) + energysystem.add(bev_fix) + + mp.draw_graph(energysystem) + + model = solph.Model( + energysystem, + timeindex=energysystem.timeindex, + ) + + filepath = "./mobility.lp" + model.write(filepath, io_options={"symbolic_solver_labels": True}) + + datapackage_dir = os.path.join( + tabular_path[0], "examples/own_examples/bev" + ) + deserialize_constraints( + model=model, + path=os.path.join(datapackage_dir, "datapackage.json"), + constraint_type_map=CONSTRAINT_TYPE_MAP, + ) + + filepath = "./mobility_constrained.lp" + model.write(filepath, io_options={"symbolic_solver_labels": True}) + + # select solver 'gurobi', 'cplex', 'glpk' etc + model.solve("cbc", solve_kwargs={"tee": True}) + # model.display() + + energysystem.params = solph.processing.parameter_as_dict( + energysystem, exclude_attrs=["subnodes"] + ) + energysystem.results = model.results() + + # Rename results for easy access + energysystem.new_results = {} + for r in energysystem.results: + if r[1] is not None: + energysystem.new_results[ + f"{r[0].label}: {r[1].label}" + ] = energysystem.results[r] + + # postprocessing + postprocessed_results = calculations.run_postprocessing(energysystem) + + # # plot bev results + # mp.plot_bev_results( + # energysystem=energysystem, + # facade_label=["BEV-V2G", "BEV-FLEX"] + # ) + + print(postprocessed_results) diff --git a/tests/mobility_plotting.py b/tests/mobility_plotting.py new file mode 100644 index 00000000..b93cf12b --- /dev/null +++ b/tests/mobility_plotting.py @@ -0,0 +1,144 @@ +import matplotlib.pyplot as plt +import networkx as nx + + +def plot_bev_results(energysystem, facade_label): + if not isinstance(facade_label, list): + facade_label = [facade_label] + + # energysystem.results[list(energysystem.results)[0]] + fig1, ax1 = plt.subplots(figsize=(10, 8)) + fig2, ax2 = plt.subplots(figsize=(10, 8)) + for c, r in energysystem.results.items(): + if not r["sequences"].empty: + if isinstance(c, tuple): + try: + name = str([i.label for i in c]) + except AttributeError: + name = "None" + else: + name = c + try: + for key in facade_label: + if key in c[0].label or key in c[1].label: + ax = ax1 + else: + ax = ax2 + column = [ + i for i in r["sequences"].columns if ("flow" in i) + ] + # r["sequences"]["flow"].plot(ax=ax, label=c) + r["sequences"][column].plot(ax=ax, label=c) + except: + print(c) + pass + + ax1.legend(title="Legend", bbox_to_anchor=(0.7, 1), loc="upper left") + ax1.set_title("Controlled") + # plt.tight_layout() + fig1.show() + + ax2.legend(title="Legend", bbox_to_anchor=(0.7, 1), loc="upper left") + ax2.set_title("Other") + plt.tight_layout() + fig2.show() + + fig, ax = plt.subplots(figsize=(10, 8)) + energysystem.new_results["shortage: el-bus"]["sequences"].plot( + ax=ax, label="shortage" + ) + energysystem.new_results["el-bus: excess"]["sequences"].plot( + ax=ax, label="excess" + ) + ax.set_title("Excess and shortage") + ax.legend(title="Legend", bbox_to_anchor=(0.7, 1), loc="upper left") + plt.tight_layout() + fig.show() + + for key in facade_label: + energysystem.new_results[f"{key}-storage: {key}-bus"][ + "sequences" + ].plot(ax=ax, label="storage: internal-bus") + ax.legend(title="Legend", bbox_to_anchor=(0.7, 1), loc="upper left") + ax.set_title("Storage") + plt.tight_layout() + fig.show() + + fig, ax = plt.subplots(figsize=(10, 8)) + for key in facade_label: + if "V2G" in key: + energysystem.new_results[f"{key}-v2g: el-bus"]["sequences"].plot( + ax=ax, label=f"{key}-v2g" + ) + ax.legend(title="Legend", bbox_to_anchor=(0.7, 1), loc="upper left") + ax.set_title("V2G to el-bus") + plt.tight_layout() + fig.show() + + fig, ax = plt.subplots(figsize=(10, 8)) + for key in facade_label: + energysystem.new_results[f"{key}-2pkm: pkm-bus"]["sequences"].plot( + ax=ax, label=f"{key}-2pkm-bus" + ) + ax.legend(title="Legend", bbox_to_anchor=(0.7, 1), loc="upper left") + ax.set_title("Flows 2 pkm-bus") + plt.tight_layout() + fig.show() + + +def draw_graph(energysystem): + # Draw the graph + + from oemof.network.graph import create_nx_graph + + G = create_nx_graph(energysystem) + + # Specify layout and draw the graph + pos = nx.drawing.nx_agraph.graphviz_layout( + G, prog="neato", args="-Gepsilon=0.0001" + ) + + fig, ax = plt.subplots(figsize=(10, 8)) + node_colors = list() + for i in list(G.nodes()): + if "storage" in i: + node_colors.append("royalblue") + elif "BEV-V2G" in i: + node_colors.append("firebrick") + elif "BEV-G2V" in i: + node_colors.append("lightblue") + elif "BEV-inflex" in i: + node_colors.append("darkviolet") + elif "excess" in i: + node_colors.append("green") + elif "shortage" in i: + node_colors.append("yellow") + elif "load" in i: + node_colors.append("orange") + elif "wind" in i: + node_colors.append("pink") + elif "bus" in i: + node_colors.append("grey") + else: + node_colors.append("violet") + + nx.draw( + G, + pos, + # **options, + with_labels=True, + node_size=3000, + # node_color='lightblue', + font_size=10, + font_weight="bold", + node_color=node_colors, + # node_color=["red", "blue", "green", "yellow", "orange"], + ) + labels = nx.get_edge_attributes(G, "weight") + nx.draw_networkx_edge_labels(G, pos=pos, edge_labels=labels) + + # Customize the plot as needed + ax.set_title("OEMOF Energy System Graph") + + # Show the plot + plt.show() diff --git a/tests/test_constraints.py b/tests/test_constraints.py index 6111e93d..026cf3b8 100644 --- a/tests/test_constraints.py +++ b/tests/test_constraints.py @@ -7,9 +7,10 @@ from oemof.solph import helpers from oemof import solph -from oemof.tabular.constraint_facades import GenericIntegralLimit +from oemof.tabular.constraint_facades import CONSTRAINT_TYPE_MAP from oemof.tabular.facades import ( BackpressureTurbine, + Bev, Commodity, Conversion, Dispatchable, @@ -100,7 +101,8 @@ def setup_method(cls): def get_om(self): return solph.Model( - self.energysystem, timeindex=self.energysystem.timeindex + self.energysystem, + timeindex=self.energysystem.timeindex, ) def compare_to_reference_lp(self, ref_filename, my_om=None): @@ -475,7 +477,8 @@ def test_emission_constraint(self): output_parameters={"custom_attributes": {"emission_factor": 2.5}}, ) - emission_constraint = GenericIntegralLimit( + emission_constraint = CONSTRAINT_TYPE_MAP["generic_integral_limit"] + emission_constraint = emission_constraint( name="emission_constraint", type="e", limit=1000, @@ -487,4 +490,132 @@ def test_emission_constraint(self): emission_constraint.build_constraint(model) - self.compare_to_reference_lp("emission_constraint.lp", my_om=model) + def test_bev_trio(self): + el_bus = solph.Bus("el-bus") + el_bus.type = "bus" + self.energysystem.add(el_bus) + + indiv_mob = solph.Bus("pkm-bus") + indiv_mob.type = "bus" + self.energysystem.add(indiv_mob) + + pkm_demand = Load( + label="pkm_demand", + type="Load", + carrier="pkm", + bus=indiv_mob, + amount=200, # PKM + profile=[0, 1, 0], # drive consumption + ) + + self.energysystem.add(pkm_demand) + + bev_v2g = Bev( + type="bev", + label="BEV-V2G", + v2g=True, + electricity_bus=el_bus, + commodity_bus=indiv_mob, + storage_capacity=0, + loss_rate=0, # self discharge of storage + charging_power=0, + balanced=True, + expandable=True, + initial_storage_level=0, + availability=[1, 1, 1, 1], # Vehicle availability at charger + commodity_conversion_rate=5 / 6, # Energy to pkm + efficiency_mob_electrical=5 / 6, # Vehicle efficiency per 100km + efficiency_mob_v2g=5 / 6, # V2G charger efficiency + efficiency_mob_g2v=5 / 6, # Charger efficiency + efficiency_sto_in=5 / 6, # Storage charging efficiency + efficiency_sto_out=5 / 6, # Storage discharging efficiency, + variable_costs=10, # Charging costs + bev_invest_costs=2, + invest_c_rate=60 / 20, # Capacity/Power + fixed_investment_costs=1, + lifetime=10, + ) + + self.energysystem.add(bev_v2g) + + bev_flex = Bev( + type="bev", + label="BEV-inflex", + electricity_bus=el_bus, + commodity_bus=indiv_mob, + storage_capacity=0, + loss_rate=0, # self discharge of storage + charging_power=0, + availability=[1, 1, 1, 1], + v2g=False, + balanced=True, + expandable=True, + initial_storage_level=0, + input_parameters={ + "fix": [0.89856, 0, 0, 0] + }, # fixed relative charging profile + output_parameters={ + "fix": [0.16, 0.08, 0.16, 0.12] + }, # fixed relative discharging profile + commodity_conversion_rate=5 / 6, # Energy to pkm + efficiency_mob_electrical=5 / 6, # Vehicle efficiency per 100km + efficiency_mob_g2v=5 / 6, # Charger efficiency + efficiency_sto_in=5 / 6, # Storage charging efficiency + efficiency_sto_out=5 / 6, # Storage discharging efficiency, + variable_costs=20, # Charging costs + bev_invest_costs=2, + invest_c_rate=60 / 20, # Capacity/Power + fixed_investment_costs=1, + lifetime=10, + ) + self.energysystem.add(bev_flex) + + bev_fix = Bev( + type="bev", + label="BEV-G2V", + electricity_bus=el_bus, + commodity_bus=indiv_mob, + storage_capacity=0, + loss_rate=0, # self discharge of storage + charging_power=0, + availability=[1, 1, 1, 1], + v2g=False, + balanced=True, + expandable=True, + initial_storage_level=0, + commodity_conversion_rate=5 / 6, # Energy to pkm + efficiency_mob_electrical=5 / 6, # Vehicle efficiency per 100km + efficiency_mob_g2v=5 / 6, # Charger efficiency + efficiency_sto_in=5 / 6, # Storage charging efficiency + efficiency_sto_out=5 / 6, # Storage discharging efficiency, + variable_costs=4, # Charging costs + bev_invest_costs=2, + invest_c_rate=60 / 20, # Capacity/Power + fixed_investment_costs=1, + lifetime=10, + ) + self.energysystem.add(bev_fix) + + model = solph.Model(self.energysystem) + + year = self.date_time_index.year.min() + mob_share_constraint = CONSTRAINT_TYPE_MAP["bev_share_mob"] + mob_share_constraint = mob_share_constraint( + name=None, + type=None, + label="BEV", + year=year, + share_mob_flex_V2G=0.3, + share_mob_inflex=0.2, + share_mob_flex_G2V=0.5, + ) + mob_share_constraint.build_constraint(model) + + # these constraints are not mandatory as the energy flow through the + # facade is already limited by the in & outflow capactiy of the storage + invest_constraint = CONSTRAINT_TYPE_MAP["bev_equal_invest"] + invest_constraint = invest_constraint(name=None, type=None, year=year) + + invest_constraint.build_constraint(model) + + self.compare_to_reference_lp("bev_trio_constraint.lp", my_om=model) diff --git a/tests/test_facades.py b/tests/test_facades.py new file mode 100644 index 00000000..3e4d44e0 --- /dev/null +++ b/tests/test_facades.py @@ -0,0 +1,1350 @@ +import logging + +import pandas as pd +import pytest +from oemof.solph import helpers + +from oemof import solph +from oemof.tabular.constraint_facades import CONSTRAINT_TYPE_MAP +from oemof.tabular.facades import Bev, Excess, Load, Shortage, Volatile + +# todo remove constraint bev_equal_invest from tests when they are removed as +# feature + + +class TestBevFacades: + @classmethod + def setup_class(cls): + cls.date_time_index = pd.date_range("1/1/2020", periods=4, freq="H") + + cls.tmpdir = helpers.extend_basic_path("tmp") + logging.info(cls.tmpdir) + + @classmethod + def setup_method(cls): + cls.energysystem = solph.EnergySystem( + groupings=solph.GROUPINGS, + timeindex=cls.date_time_index, + ) + + def get_om(self): + self.model = solph.Model( + self.energysystem, + timeindex=self.energysystem.timeindex, + ) + + def solve_om(self): + opt_result = self.model.solve("cbc", solve_kwargs={"tee": True}) + self.results = self.model.results() + return opt_result + + def rename_results(self): + rename_mapping = { + oemof_tuple: f"{oemof_tuple[0]}->{oemof_tuple[1]}" + for oemof_tuple in self.results.keys() + } + for old_key, new_key in rename_mapping.items(): + self.results[new_key] = self.results.pop(old_key) + + def test_bev_v2g_dispatch(self): + """ + Tests v2g bev facade in dispatch optimization. + + The following energy quantities are used: + + | timestep | 0 | 1 | 2 | 3 | + |-------------|---------|-------|------|------| + | volatile | +725.76 | 0 | 0 | 0 | + | load | 0 | -10 | 0 | -100 | + | pkm_demand | -50 | -50 | -100 | 0 | + | V2g storage | 417.6 | 316.8 | 144 | 0 | + + The following efficiencies are taken into consideration: + volatile --> v2g_storage: efficiency_sto_in * efficiency_mob_g2v + storage --> load: efficiency_sto_out * efficiency_mob_v2g + storage --> pkm_demand: + efficiency_sto_out * efficiency_mob_electrical * + commodity_conversion_rate + + todo, optional: show as table + """ + el_bus = solph.Bus("el-bus") + el_bus.type = "bus" + self.energysystem.add(el_bus) + + indiv_mob = solph.Bus("pkm-bus") + indiv_mob.type = "bus" + self.energysystem.add(indiv_mob) + + volatile = Volatile( + label="wind", + bus=el_bus, + carrier="wind", + tech="onshore", + capacity=725.76, + profile=[1, 0, 0, 0], + variable_costs=10, + ) + self.energysystem.add(volatile) + + load = Load( + label="load", + carrier="electricity", + bus=el_bus, + amount=100, + profile=[0, 0.1, 0, 1], + ) + self.energysystem.add(load) + + pkm_demand = Load( + label="pkm_demand", + type="Load", + carrier="pkm", + bus=indiv_mob, + amount=100, # PKM + profile=[0.5, 0.5, 1, 0], # drive consumption + ) + + self.energysystem.add(pkm_demand) + + bev_v2g = Bev( + type="bev", + label="BEV-V2G", + v2g=True, + electricity_bus=el_bus, + commodity_bus=indiv_mob, + storage_capacity=800, + loss_rate=0, # self discharge of storage + charging_power=800, + balanced=True, + expandable=False, + availability=[1, 1, 1, 1], # Vehicle availability at charger + commodity_conversion_rate=5 / 6, # Energy to pkm + efficiency_mob_electrical=5 / 6, # Vehicle efficiency per 100km + efficiency_mob_v2g=5 / 6, # V2G charger efficiency + efficiency_mob_g2v=5 / 6, # Charger efficiency + efficiency_sto_in=5 / 6, # Storage charging efficiency + efficiency_sto_out=5 / 6, # Storage discharging efficiency, + variable_costs=10, # Charging costs + ) + + self.energysystem.add(bev_v2g) + + self.get_om() + + solver_stats = self.solve_om() + + # rename results to make them accessible + self.rename_results() + + assert solver_stats["Solver"][0]["Status"] == "ok" + + # Check Storage level + cn = "BEV-V2G-storage->None" + assert self.results[cn]["sequences"]["storage_content"].iloc[0] == 0 + assert ( + self.results[cn]["sequences"]["storage_content"].iloc[1] == 417.6 + ) + assert ( + self.results[cn]["sequences"]["storage_content"].iloc[2] == 316.8 + ) + assert self.results[cn]["sequences"]["storage_content"].iloc[3] == 144 + assert self.results[cn]["sequences"]["storage_content"].iloc[4] == 0 + + def test_bev_inflex_dispatch(self): + """ + Tests inflex bev facade in dispatch optimization. + + The following energy quantities are used: + | timestep | 0 | 1 | 2 | 3 | + |-------------|----------|-------|-------|-----| + | volatile | +908.704 | 0 | 0 | 0 | + | load | -100 | 0 | 0 | 0 | + | pkm_demand | -100 | -50 | -100 | -75 | + | V2g storage | 388.8 | 302.4 | 129.6 | 0 | + + The following efficiencies are taken into consideration: + volatile --> v2g_storage: efficiency_sto_in * efficiency_mob_g2v + storage --> load: efficiency_sto_out * efficiency_mob_v2g + storage --> pkm_demand: + efficiency_sto_out * efficiency_mob_electrical * + commodity_conversion_rate + + todo, optional: show as table + """ + el_bus = solph.Bus("el-bus") + el_bus.type = "bus" + self.energysystem.add(el_bus) + + indiv_mob = solph.Bus("pkm-bus") + indiv_mob.type = "bus" + self.energysystem.add(indiv_mob) + + volatile = Volatile( + label="wind", + bus=el_bus, + carrier="wind", + tech="onshore", + capacity=908.704, + profile=[1, 0, 0, 0], + variable_costs=10, + ) + self.energysystem.add(volatile) + + load = Load( + label="load", + carrier="electricity", + bus=el_bus, + amount=100, + profile=[1, 0, 0, 0], + ) + self.energysystem.add(load) + + pkm_demand = Load( + label="pkm_demand", + type="Load", + carrier="pkm", + bus=indiv_mob, + amount=100, # PKM + profile=[1, 0.5, 1, 0.75], # drive consumption + ) + self.energysystem.add(pkm_demand) + + bev_inflex = Bev( + type="bev", + label="BEV-inflex", + electricity_bus=el_bus, + commodity_bus=indiv_mob, + storage_capacity=900, + loss_rate=0, # self discharge of storage + charging_power=900, + availability=[1, 1, 1, 1], + v2g=False, + balanced=True, + expandable=False, + input_parameters={ + "fix": [0.89856, 0, 0, 0] + }, # fixed relative charging profile + output_parameters={ + "fix": [0.16, 0.08, 0.16, 0.12] + }, # fixed relative discharging profile + commodity_conversion_rate=5 / 6, # Energy to pkm + efficiency_mob_electrical=5 / 6, # Vehicle efficiency per 100km + efficiency_mob_g2v=5 / 6, # Charger efficiency + efficiency_sto_in=5 / 6, # Storage charging efficiency + efficiency_sto_out=5 / 6, # Storage discharging efficiency, + variable_costs=8, + ) + self.energysystem.add(bev_inflex) + + self.get_om() + + solver_stats = self.solve_om() + + # rename results to make them accessible + self.rename_results() + + assert solver_stats["Solver"][0]["Status"] == "ok" + + # Check Storage level + cn = "BEV-inflex-storage->None" + assert self.results[cn]["sequences"]["storage_content"].iloc[0] == 0 + assert ( + self.results[cn]["sequences"]["storage_content"].iloc[1] == 388.8 + ) + assert ( + self.results[cn]["sequences"]["storage_content"].iloc[2] == 302.4 + ) + assert ( + self.results[cn]["sequences"]["storage_content"].iloc[3] == 129.6 + ) + assert self.results[cn]["sequences"]["storage_content"].iloc[4] == 0 + + def test_bev_g2v_dispatch(self): + """ + Tests g2v bev facade in dispatch optimization. + + The same quantities as in `test_bev_inflex_dispatch()` are used. + """ + el_bus = solph.Bus("el-bus") + el_bus.type = "bus" + self.energysystem.add(el_bus) + + indiv_mob = solph.Bus("pkm-bus") + indiv_mob.type = "bus" + self.energysystem.add(indiv_mob) + + volatile = Volatile( + label="wind", + bus=el_bus, + carrier="wind", + tech="onshore", + capacity=908.704, + profile=[1, 0, 0, 0], + variable_costs=10, + ) + self.energysystem.add(volatile) + + load = Load( + label="load", + carrier="electricity", + bus=el_bus, + amount=100, + profile=[1, 0, 0, 0], + ) + self.energysystem.add(load) + + pkm_demand = Load( + label="pkm_demand", + type="Load", + carrier="pkm", + bus=indiv_mob, + amount=100, # PKM + profile=[1, 0.5, 1, 0.75], # drive consumption + ) + self.energysystem.add(pkm_demand) + + bev_g2v = Bev( + type="bev", + label="BEV-G2V", + electricity_bus=el_bus, + commodity_bus=indiv_mob, + storage_capacity=900, + loss_rate=0, # self discharge of storage + charging_power=900, + availability=[1, 1, 1, 1], + v2g=False, + balanced=True, + expandable=False, + commodity_conversion_rate=5 / 6, # Energy to pkm + efficiency_mob_electrical=5 / 6, # Vehicle efficiency per 100km + efficiency_mob_g2v=5 / 6, # Charger efficiency + efficiency_sto_in=5 / 6, # Storage charging efficiency + efficiency_sto_out=5 / 6, # Storage discharging efficiency, + variable_costs=8, + ) + self.energysystem.add(bev_g2v) + + self.get_om() + + solver_stats = self.solve_om() + + # rename results to make them accessible + self.rename_results() + + assert solver_stats["Solver"][0]["Status"] == "ok" + + # Check Storage level + cn = "BEV-G2V-storage->None" + assert self.results[cn]["sequences"]["storage_content"].iloc[0] == 0 + assert ( + self.results[cn]["sequences"]["storage_content"].iloc[1] == 388.8 + ) + assert ( + self.results[cn]["sequences"]["storage_content"].iloc[2] == 302.4 + ) + assert ( + self.results[cn]["sequences"]["storage_content"].iloc[3] == 129.6 + ) + assert self.results[cn]["sequences"]["storage_content"].iloc[4] == 0 + + def test_bev_trio_dispatch(self): + """ + Tests linked v2g, g2v and inflex bev facades in dispatch optimization. + + Energy quantities are taken from the single tests + (`test_bev_v2g_dispatch()`, `test_bev_inflex_dispatch()`, + `test_bev_g2v_dispatch()`) and summed up in this test. + + """ + el_bus = solph.Bus("el-bus") + el_bus.type = "bus" + self.energysystem.add(el_bus) + + indiv_mob = solph.Bus("pkm-bus") + indiv_mob.type = "bus" + self.energysystem.add(indiv_mob) + + volatile = Volatile( + label="wind", + bus=el_bus, + carrier="wind", + tech="onshore", + capacity=2543.168, + profile=[1, 0, 0, 0], + variable_costs=10, + ) + self.energysystem.add(volatile) + + load = Load( + label="load", + carrier="electricity", + bus=el_bus, + amount=200, + profile=[1, 0.05, 0, 0.5], + ) + self.energysystem.add(load) + + pkm_demand = Load( + label="pkm_demand", + type="Load", + carrier="pkm", + bus=indiv_mob, + amount=250, # PKM + profile=[1, 0.6, 1.2, 0.6], # drive consumption + ) + self.energysystem.add(pkm_demand) + + bev_v2g = Bev( + type="bev", + label="BEV-V2G", + v2g=True, + electricity_bus=el_bus, + commodity_bus=indiv_mob, + storage_capacity=800, + loss_rate=0, # self discharge of storage + charging_power=800, + balanced=True, + expandable=False, + availability=[1, 1, 1, 1], # Vehicle availability at charger + commodity_conversion_rate=5 / 6, # Energy to pkm + efficiency_mob_electrical=5 / 6, # Vehicle efficiency per 100km + efficiency_mob_v2g=5 / 6, # V2G charger efficiency + efficiency_mob_g2v=5 / 6, # Charger efficiency + efficiency_sto_in=5 / 6, # Storage charging efficiency + efficiency_sto_out=5 / 6, # Storage discharging efficiency, + variable_costs=10, # Charging costs + ) + self.energysystem.add(bev_v2g) + + bev_inflex = Bev( + type="bev", + label="BEV-inflex", + electricity_bus=el_bus, + commodity_bus=indiv_mob, + storage_capacity=900, + loss_rate=0, # self discharge of storage + charging_power=900, + availability=[1, 1, 1, 1], + v2g=False, + balanced=True, + expandable=False, + input_parameters={ + "fix": [0.89856, 0, 0, 0] + }, # fixed relative charging profile + output_parameters={ + "fix": [0.16, 0.08, 0.16, 0.12] + }, # fixed relative discharging profile + commodity_conversion_rate=5 / 6, # Energy to pkm + efficiency_mob_electrical=5 / 6, # Vehicle efficiency per 100km + efficiency_mob_g2v=5 / 6, # Charger efficiency + efficiency_sto_in=5 / 6, # Storage charging efficiency + efficiency_sto_out=5 / 6, # Storage discharging efficiency, + variable_costs=20, # Charging costs + ) + self.energysystem.add(bev_inflex) + + bev_g2v = Bev( + type="bev", + label="BEV-G2V", + electricity_bus=el_bus, + commodity_bus=indiv_mob, + storage_capacity=808.704, + loss_rate=0, # self discharge of storage + charging_power=808.704, + availability=[1, 1, 1, 1], + v2g=False, + balanced=True, + expandable=False, + commodity_conversion_rate=5 / 6, # Energy to pkm + efficiency_mob_electrical=5 / 6, # Vehicle efficiency per 100km + efficiency_mob_g2v=5 / 6, # Charger efficiency + efficiency_sto_in=5 / 6, # Storage charging efficiency + efficiency_sto_out=5 / 6, # Storage discharging efficiency, + variable_costs=4, # Charging costs + ) + self.energysystem.add(bev_g2v) + + self.get_om() + + solver_stats = self.solve_om() + + # rename results to make them accessible + self.rename_results() + + assert solver_stats["Solver"][0]["Status"] == "ok" + + # Check Storage level + cn = "BEV-V2G-storage->None" + assert self.results[cn]["sequences"]["storage_content"].iloc[0] == 0 + assert self.results[cn]["sequences"]["storage_content"].iloc[4] == 0 + + cn2 = "BEV-inflex-storage->None" + assert self.results[cn2]["sequences"]["storage_content"].iloc[0] == 0 + assert ( + self.results[cn2]["sequences"]["storage_content"].iloc[1] == 388.8 + ) + assert ( + self.results[cn2]["sequences"]["storage_content"].iloc[2] == 302.4 + ) + assert ( + self.results[cn2]["sequences"]["storage_content"].iloc[3] == 129.6 + ) + assert self.results[cn2]["sequences"]["storage_content"].iloc[4] == 0 + + cn3 = "BEV-G2V-storage->None" + assert self.results[cn3]["sequences"]["storage_content"].iloc[0] == 0 + assert self.results[cn3]["sequences"]["storage_content"].iloc[4] == 0 + + # Check storage input flows + cn4 = "el-bus->BEV-V2G-storage" + assert self.results[cn4]["sequences"]["flow"].iloc[0] == 725.76 + + cn5 = "el-bus->BEV-G2V-storage" + assert self.results[cn5]["sequences"]["flow"].iloc[0] == 808.704 + + def test_bev_v2g_invest(self): + """ + Tests v2g bev facade in investment optimization. + + Energy quantities and efficiencies are the same as in + `test_bev_v2g_dispatch`. + + The constraint "bev_equal_invest" is used. + """ + el_bus = solph.Bus("el-bus") + el_bus.type = "bus" + self.energysystem.add(el_bus) + + indiv_mob = solph.Bus("pkm-bus") + indiv_mob.type = "bus" + self.energysystem.add(indiv_mob) + + volatile = Volatile( + label="wind", + bus=el_bus, + carrier="wind", + tech="onshore", + capacity=725.76, + profile=[1, 0, 0, 0], + variable_costs=10, + ) + self.energysystem.add(volatile) + + load = Load( + label="load", + carrier="electricity", + bus=el_bus, + amount=100, + profile=[0, 0.1, 0, 1], + ) + self.energysystem.add(load) + + pkm_demand = Load( + label="pkm_demand", + type="Load", + carrier="pkm", + bus=indiv_mob, + amount=100, # PKM + profile=[0.5, 0.5, 1, 0], # drive consumption + ) + + self.energysystem.add(pkm_demand) + + bev_v2g = Bev( + type="bev", + label="BEV-V2G", + v2g=True, + electricity_bus=el_bus, + commodity_bus=indiv_mob, + storage_capacity=0, + loss_rate=0, # self discharge of storage + charging_power=0, + balanced=True, + expandable=True, + initial_storage_level=0, + availability=[1, 1, 1, 1], # Vehicle availability at charger + commodity_conversion_rate=5 / 6, # Energy to pkm + efficiency_mob_electrical=5 / 6, # Vehicle efficiency per 100km + efficiency_mob_v2g=5 / 6, # V2G charger efficiency + efficiency_mob_g2v=5 / 6, # Charger efficiency + efficiency_sto_in=5 / 6, # Storage charging efficiency + efficiency_sto_out=5 / 6, # Storage discharging efficiency, + variable_costs=10, # Charging costs + bev_invest_costs=2, + invest_c_rate=60 / 20, # Capacity/Power + fixed_investment_costs=1, + lifetime=10, + ) + self.energysystem.add(bev_v2g) + + self.get_om() + + # Add constraint bev_equal_invest + year = self.energysystem.timeindex.year[0] + constraint = CONSTRAINT_TYPE_MAP["bev_equal_invest"] + constraint = constraint(name=None, type=None, year=year) + constraint.build_constraint(self.model) + + solver_stats = self.solve_om() + + # rename results to make them accessible + self.rename_results() + + assert solver_stats["Solver"][0]["Status"] == "ok" + + # Check Storage level + cn = "BEV-V2G-storage->None" + # todo: why is first time step of storage level missing, but last time + # step is nan? + assert self.results[cn]["sequences"]["storage_content"][0] == 417.6 + assert self.results[cn]["sequences"]["storage_content"][1] == 316.8 + assert self.results[cn]["sequences"]["storage_content"][2] == 144 + assert self.results[cn]["sequences"]["storage_content"][3] == 0 + + # Check invested storage capacity + assert self.results[cn]["scalars"]["invest"] == 2177.28 + + # Check invested v2g capacity + cn2 = "BEV-V2G-v2g->el-bus" + assert self.results[cn2]["scalars"]["invest"] == 725.76 + + # Check invested v2g-2com capacity + cn3 = "BEV-V2G-2com->pkm-bus" + assert self.results[cn3]["scalars"]["invest"] == 725.76 + + def test_bev_trio_invest(self): + """ + Tests linked v2g, g2v and inflex bev facades in invest optimization. + + Energy quantities of load, pkm_demand and volatile and the efficiencies + are the same as in `test_bev_trio_dispatch`. + + The constraints "bev_equal_invest" and "bev_share_mob" are used. + + The checks include shares of invested storage capacities of the three + BEV components (v2g, g2v, inflex) and the invested capacities of the + inverters. + Storage levels were not calculated manually and therefore, are not + being checked. + """ + el_bus = solph.Bus("el-bus") + el_bus.type = "bus" + self.energysystem.add(el_bus) + + indiv_mob = solph.Bus("pkm-bus") + indiv_mob.type = "bus" + self.energysystem.add(indiv_mob) + + volatile = Volatile( + label="wind", + bus=el_bus, + carrier="wind", + tech="onshore", + capacity=2543.168, + profile=[1, 0, 0, 0], + variable_costs=10, + ) + self.energysystem.add(volatile) + + load = Load( + label="load", + carrier="electricity", + bus=el_bus, + amount=200, + profile=[1, 0.05, 0, 0.5], + ) + self.energysystem.add(load) + + pkm_demand = Load( + label="pkm_demand", + type="Load", + carrier="pkm", + bus=indiv_mob, + amount=250, # PKM + profile=[1, 0.6, 1.2, 0.6], # drive consumption + ) + self.energysystem.add(pkm_demand) + + bev_v2g = Bev( + type="bev", + label="BEV-V2G", + v2g=True, + electricity_bus=el_bus, + commodity_bus=indiv_mob, + storage_capacity=0, + loss_rate=0, # self discharge of storage + charging_power=0, + balanced=True, + expandable=True, + initial_storage_level=0, + availability=[1, 1, 1, 1], # Vehicle availability at charger + commodity_conversion_rate=5 / 6, # Energy to pkm + efficiency_mob_electrical=5 / 6, # Vehicle efficiency per 100km + efficiency_mob_v2g=5 / 6, # V2G charger efficiency + efficiency_mob_g2v=5 / 6, # Charger efficiency + efficiency_sto_in=5 / 6, # Storage charging efficiency + efficiency_sto_out=5 / 6, # Storage discharging efficiency, + variable_costs=10, # Charging costs + bev_invest_costs=2, + invest_c_rate=60 / 20, # Capacity/Power + fixed_investment_costs=1, + lifetime=10, + ) + self.energysystem.add(bev_v2g) + + bev_inflex = Bev( + type="bev", + label="BEV-inflex", + electricity_bus=el_bus, + commodity_bus=indiv_mob, + storage_capacity=0, + loss_rate=0, # self discharge of storage + charging_power=0, + availability=[1, 1, 1, 1], + v2g=False, + balanced=True, + expandable=True, + initial_storage_level=0, + input_parameters={ + "fix": [0.89856, 0, 0, 0] + }, # fixed relative charging profile + output_parameters={ + "fix": [0.16, 0.08, 0.16, 0.12] + }, # fixed relative discharging profile + commodity_conversion_rate=5 / 6, # Energy to pkm + efficiency_mob_electrical=5 / 6, # Vehicle efficiency per 100km + efficiency_mob_g2v=5 / 6, # Charger efficiency + efficiency_sto_in=5 / 6, # Storage charging efficiency + efficiency_sto_out=5 / 6, # Storage discharging efficiency, + variable_costs=20, # Charging costs + bev_invest_costs=2, + invest_c_rate=60 / 20, # Capacity/Power + fixed_investment_costs=1, + lifetime=10, + ) + self.energysystem.add(bev_inflex) + + bev_g2v = Bev( + type="bev", + label="BEV-G2V", + electricity_bus=el_bus, + commodity_bus=indiv_mob, + storage_capacity=0, + loss_rate=0, # self discharge of storage + charging_power=0, + availability=[1, 1, 1, 1], + v2g=False, + balanced=True, + expandable=True, + initial_storage_level=0, + commodity_conversion_rate=5 / 6, # Energy to pkm + efficiency_mob_electrical=5 / 6, # Vehicle efficiency per 100km + efficiency_mob_g2v=5 / 6, # Charger efficiency + efficiency_sto_in=5 / 6, # Storage charging efficiency + efficiency_sto_out=5 / 6, # Storage discharging efficiency, + variable_costs=4, # Charging costs + bev_invest_costs=2, + invest_c_rate=60 / 20, # Capacity/Power + fixed_investment_costs=1, + lifetime=10, + ) + self.energysystem.add(bev_g2v) + + self.get_om() + + # Add constraint bev_share_mob + year = self.energysystem.timeindex.year[0] + constraint = CONSTRAINT_TYPE_MAP["bev_share_mob"] + constraint = constraint( + name=None, + type=None, + label="BEV", + year=year, + share_mob_flex_V2G=0.3, + share_mob_inflex=0.2, + share_mob_flex_G2V=0.5, + ) + constraint.build_constraint(self.model) + + # Add constraint bev_equal_invest + constraint_eqin = CONSTRAINT_TYPE_MAP["bev_equal_invest"] + constraint_eqin = constraint_eqin(name=None, type=None, year=year) + constraint_eqin.build_constraint(self.model) + + solver_stats = self.solve_om() + + # rename results to make them accessible + self.rename_results() + + assert solver_stats["Solver"][0]["Status"] == "ok" + + # Check invested storage capacity shares + cn = "BEV-V2G-storage->None" + cn2 = "BEV-inflex-storage->None" + cn3 = "BEV-G2V-storage->None" + v2g_cap = self.results[cn]["scalars"]["invest"] + inflex_cap = self.results[cn2]["scalars"]["invest"] + g2v_cap = self.results[cn3]["scalars"]["invest"] + total_cap = v2g_cap + inflex_cap + g2v_cap + assert round(v2g_cap / total_cap, 1) == constraint.share_mob_flex_V2G + assert round(g2v_cap / total_cap, 1) == constraint.share_mob_flex_G2V + assert round(inflex_cap / total_cap, 1) == constraint.share_mob_inflex + + # Check invested v2g capacity + cn4 = "BEV-V2G-v2g->el-bus" + assert round(self.results[cn4]["scalars"]["invest"], 4) == v2g_cap / 3 + + # Check invested v2g-2com capacity + cn5 = "BEV-V2G-2com->pkm-bus" + assert round(self.results[cn5]["scalars"]["invest"], 4) == v2g_cap / 3 + cn6 = "BEV-G2V-2com->pkm-bus" + assert self.results[cn6]["scalars"]["invest"] == g2v_cap / 3 + + cn7 = "BEV-inflex-2com->pkm-bus" + assert self.results[cn7]["scalars"]["invest"] == pytest.approx( + inflex_cap / 3 + ) # difference at 5th decimal place + + +class TestBevFacadesMultiPeriodInvest: + @classmethod + def setup_class(cls): + t_idx_1 = pd.date_range("1/1/2020", periods=5, freq="H") + t_idx_2 = pd.date_range("1/1/2030", periods=5, freq="H") + t_idx_1_series = pd.Series(index=t_idx_1, dtype="float64") + t_idx_2_series = pd.Series(index=t_idx_2, dtype="float64") + cls.date_time_index = pd.concat([t_idx_1_series, t_idx_2_series]).index + cls.periods = [t_idx_1, t_idx_2] + + cls.tmpdir = helpers.extend_basic_path("tmp") + logging.info(cls.tmpdir) + + @classmethod + def setup_method(cls): + cls.energysystem = solph.EnergySystem( + groupings=solph.GROUPINGS, + timeindex=cls.date_time_index, + infer_last_interval=False, + timeincrement=[1] * len(cls.date_time_index), + periods=cls.periods, + ) + + # todo: identical functions can be @fixtures or else (outside of classes) + + def get_om(self): + self.model = solph.Model( + self.energysystem, + timeindex=self.energysystem.timeindex, + ) + + def solve_om(self): + opt_result = self.model.solve("cbc", solve_kwargs={"tee": True}) + self.results = self.model.results() + return opt_result + + def rename_results(self): + rename_mapping = { + oemof_tuple: f"{oemof_tuple[0]}->{oemof_tuple[1]}" + for oemof_tuple in self.results.keys() + } + for old_key, new_key in rename_mapping.items(): + self.results[new_key] = self.results.pop(old_key) + + @pytest.mark.skip( + reason="multi-period feature is not working as expected, see #144" + ) + def test_bev_v2g_invest_multi_period(self): + """ + Tests v2g bev facade with multi-period in investment optimization. + + Energy quantities of load, pkm_demand and volatile and the efficiencies + are the same as in `test_bev_v2g_invest`. It is added a 5th time step + with zero demand and production to reach a storage content of zero + after the first period, which is important for multi-period use. + + The constraints "bev_equal_invest" and "bev_share_mob" are used. + """ + el_bus = solph.Bus("el-bus") + el_bus.type = "bus" + self.energysystem.add(el_bus) + + indiv_mob = solph.Bus("pkm-bus") + indiv_mob.type = "bus" + self.energysystem.add(indiv_mob) + + volatile = Volatile( + label="wind", + bus=el_bus, + carrier="wind", + tech="onshore", + capacity=725.76, + # expandable=True, + profile=len(self.periods) * [1, 0, 0, 0, 0], + # lifetime=10, + variable_costs=10, + # capacity_cost=1, + ) + self.energysystem.add(volatile) + + load = Load( + label="load", + carrier="electricity", + bus=el_bus, + amount=100, + profile=len(self.periods) * [0, 0.1, 0, 1, 0], + ) + self.energysystem.add(load) + + pkm_demand = Load( + label="pkm_demand", + type="Load", + carrier="pkm", + bus=indiv_mob, + amount=100, # PKM + profile=len(self.periods) + * [0.5, 0.5, 1, 0, 0], # drive consumption + ) + self.energysystem.add(pkm_demand) + + bev_v2g = Bev( + type="bev", + label="BEV-V2G", + v2g=True, + electricity_bus=el_bus, + commodity_bus=indiv_mob, + storage_capacity=0, + loss_rate=0, # self discharge of storage + charging_power=0, + balanced=True, + expandable=True, + availability=len(self.periods) + * [1, 1, 1, 1, 1], # Vehicle availability at charger + commodity_conversion_rate=5 / 6, # Energy to pkm + efficiency_mob_electrical=5 / 6, # Vehicle efficiency per 100km + efficiency_mob_v2g=5 / 6, # V2G charger efficiency + efficiency_mob_g2v=5 / 6, # Charger efficiency + efficiency_sto_in=5 / 6, # Storage charging efficiency + efficiency_sto_out=5 / 6, # Storage discharging efficiency, + variable_costs=10, # Charging costs + bev_invest_costs=2, + invest_c_rate=60 / 20, # Capacity/Power + fixed_investment_costs=1, + lifetime=10, + ) + self.energysystem.add(bev_v2g) + + # todo remove shortage when multi-period and test is fixed + shortage = Shortage( + type="shortage", + label="shortage", + bus=el_bus, + carrier="electricity", + tech="shortage", + capacity=1000, + marginal_cost=1e12, + ) + self.energysystem.add(shortage) + + self.get_om() + + # Add constraint bev_equal_invest for each period + for period in self.energysystem.periods: + year = period.year.min() + constraint = CONSTRAINT_TYPE_MAP["bev_equal_invest"] + constraint = constraint(name=None, type=None, year=year) + # build constraint for each facade & period + constraint.build_constraint(self.model) + + solver_stats = self.solve_om() + + # rename results to make them accessible + self.rename_results() + + assert solver_stats["Solver"][0]["Status"] == "ok" + + # Check storage level + cn = "BEV-V2G-storage->None" + assert self.results[cn]["sequences"]["storage_content"].iloc[0] == 0 + assert ( + self.results[cn]["sequences"]["storage_content"].iloc[1] == 417.6 + ) + assert ( + self.results[cn]["sequences"]["storage_content"].iloc[2] == 316.8 + ) + assert self.results[cn]["sequences"]["storage_content"].iloc[3] == 144 + assert self.results[cn]["sequences"]["storage_content"].iloc[4] == 0 + assert ( + self.results[cn]["sequences"]["storage_content"].iloc[5] == 417.6 + ) + assert ( + self.results[cn]["sequences"]["storage_content"].iloc[6] == 316.8 + ) + assert self.results[cn]["sequences"]["storage_content"].iloc[7] == 144 + assert self.results[cn]["sequences"]["storage_content"].iloc[8] == 0 + assert self.results[cn]["sequences"]["storage_content"].iloc[9] == 0 + # Check invested storage capacity + assert self.results[cn]["period_scalars"]["invest"].iloc[0] == 2177.28 + assert self.results[cn]["period_scalars"]["invest"].iloc[1] == 2177.28 + + # Check invested v2g capacity + cn2 = "BEV-V2G-v2g->el-bus" + assert self.results[cn2]["period_scalars"]["invest"].iloc[0] == 725.76 + assert self.results[cn2]["period_scalars"]["invest"].iloc[1] == 725.76 + + # Check invested v2g capacity + cn2 = "BEV-V2G-2com->pkm-bus" + assert self.results[cn2]["period_scalars"]["invest"].iloc[0] == 725.76 + assert self.results[cn2]["period_scalars"]["invest"].iloc[1] == 725.76 + + @pytest.mark.skip( + reason="multi-period feature is not working as expected, see #144" + ) + def test_bev_trio_invest_multi_period(self): + """ + Tests linked bev facades with multi-period in invest optimization. + + Energy quantities of load, pkm_demand and volatile and the efficiencies + are the same as in `test_bev_trio_invest`. It is added a 5th time step + with zero demand and production to reach a storage content of zero + after the first period, which is important for multi-period use. + + The constraints "bev_equal_invest" and "bev_share_mob" are used. + """ + el_bus = solph.Bus("el-bus") + el_bus.type = "bus" + self.energysystem.add(el_bus) + + indiv_mob = solph.Bus("pkm-bus") + indiv_mob.type = "bus" + self.energysystem.add(indiv_mob) + + volatile = Volatile( + label="wind", + bus=el_bus, + carrier="wind", + tech="onshore", + capacity=2543.168, + # expandable=True, + profile=len(self.periods) * [1, 0, 0, 0, 0], + # lifetime=10, + variable_costs=10, + # capacity_cost=1, + ) + self.energysystem.add(volatile) + + load = Load( + label="load", + carrier="electricity", + bus=el_bus, + amount=200, + profile=len(self.periods) * [1, 0.05, 0, 0.5, 0], + ) + self.energysystem.add(load) + + pkm_demand = Load( + label="pkm_demand", + type="Load", + carrier="pkm", + bus=indiv_mob, + amount=250, # PKM + profile=len(self.periods) + * [1, 0.6, 1.2, 0.6, 0], # drive consumption + ) + self.energysystem.add(pkm_demand) + + bev_v2g = Bev( + type="bev", + label="BEV-V2G", + v2g=True, + electricity_bus=el_bus, + commodity_bus=indiv_mob, + storage_capacity=0, + loss_rate=0, # self discharge of storage + charging_power=0, + balanced=True, + expandable=True, + availability=len(self.periods) + * [1, 1, 1, 1, 1], # Vehicle availability at charger + commodity_conversion_rate=5 / 6, # Energy to pkm + efficiency_mob_electrical=5 / 6, # Vehicle efficiency per 100km + efficiency_mob_v2g=5 / 6, # V2G charger efficiency + efficiency_mob_g2v=5 / 6, # Charger efficiency + efficiency_sto_in=5 / 6, # Storage charging efficiency + efficiency_sto_out=5 / 6, # Storage discharging efficiency, + variable_costs=10, # Charging costs + bev_invest_costs=2, + invest_c_rate=60 / 20, # Capacity/Power + fixed_investment_costs=1, + lifetime=10, + ) + self.energysystem.add(bev_v2g) + + bev_inflex = Bev( + type="bev", + label="BEV-inflex", + electricity_bus=el_bus, + commodity_bus=indiv_mob, + storage_capacity=0, + loss_rate=0, # self discharge of storage + charging_power=0, + availability=len(self.periods) * [1, 1, 1, 1, 1], + v2g=False, + balanced=True, + expandable=True, + input_parameters={ + "fix": len(self.periods) * [0.89856, 0, 0, 0, 0] + }, # fixed relative charging profile + output_parameters={ + "fix": len(self.periods) * [0.16, 0.08, 0.16, 0.12, 0] + }, # fixed relative discharging profile + commodity_conversion_rate=5 / 6, # Energy to pkm + efficiency_mob_electrical=5 / 6, # Vehicle efficiency per 100km + efficiency_mob_g2v=5 / 6, # Charger efficiency + efficiency_sto_in=5 / 6, # Storage charging efficiency + efficiency_sto_out=5 / 6, # Storage discharging efficiency, + variable_costs=20, # Charging costs + bev_invest_costs=2, + invest_c_rate=60 / 20, # Capacity/Power + fixed_investment_costs=1, + lifetime=10, + ) + self.energysystem.add(bev_inflex) + + bev_g2v = Bev( + type="bev", + label="BEV-G2V", + electricity_bus=el_bus, + commodity_bus=indiv_mob, + storage_capacity=0, + loss_rate=0, # self discharge of storage + charging_power=0, + availability=len(self.periods) * [1, 1, 1, 1, 1], + v2g=False, + balanced=True, + expandable=True, + commodity_conversion_rate=5 / 6, # Energy to pkm + efficiency_mob_electrical=5 / 6, # Vehicle efficiency per 100km + efficiency_mob_g2v=5 / 6, # Charger efficiency + efficiency_sto_in=5 / 6, # Storage charging efficiency + efficiency_sto_out=5 / 6, # Storage discharging efficiency, + variable_costs=4, # Charging costs + bev_invest_costs=2, + invest_c_rate=60 / 20, # Capacity/Power + fixed_investment_costs=1, + lifetime=10, + ) + self.energysystem.add(bev_g2v) + + # todo remove shortage/excess when multi-period and test is fixed + shortage = Shortage( + type="shortage", + label="shortage", + bus=el_bus, + carrier="electricity", + tech="shortage", + capacity=1000, + marginal_cost=1e12, + ) + self.energysystem.add(shortage) + + excess = Excess( + type="excess", + label="excess", + bus=el_bus, + carrier="electricity", + tech="excess", + capacity=1000, + marginal_cost=1e12, + ) + self.energysystem.add(excess) + + self.get_om() + + # Add constraints for each period + for period in self.energysystem.periods: + year = period.year.min() + # Add constraint bev_share_mob + constraint = CONSTRAINT_TYPE_MAP["bev_share_mob"] + constraint = constraint( + name=None, + type=None, + label="BEV", + year=year, + share_mob_flex_V2G=0.3, + share_mob_inflex=0.2, + share_mob_flex_G2V=0.5, + ) + # build constraint for each facade & period + constraint.build_constraint(self.model) + + # Add constraint bev_equal_invest + constraint_eqin = CONSTRAINT_TYPE_MAP["bev_equal_invest"] + constraint_eqin = constraint_eqin(name=None, type=None, year=year) + # build constraint for each facade & period + constraint_eqin.build_constraint(self.model) + + solver_stats = self.solve_om() + + # rename results to make them accessible + self.rename_results() + + assert solver_stats["Solver"][0]["Status"] == "ok" + + # Check invested storage capacity shares + cn = "BEV-V2G-storage->None" + cn2 = "BEV-inflex-storage->None" + cn3 = "BEV-G2V-storage->None" + for i in range(len(self.energysystem.periods)): + v2g_cap = self.results[cn]["period_scalars"]["invest"].iloc[i] + inflex_cap = self.results[cn2]["period_scalars"]["invest"].iloc[i] + g2v_cap = self.results[cn3]["period_scalars"]["invest"].iloc[i] + total_cap = v2g_cap + inflex_cap + g2v_cap + assert ( + round(v2g_cap / total_cap, 1) == constraint.share_mob_flex_V2G + ) + assert ( + round(g2v_cap / total_cap, 1) == constraint.share_mob_flex_G2V + ) + assert ( + round(inflex_cap / total_cap, 1) == constraint.share_mob_inflex + ) + + # todo check if rounding / approx() is necessary here + # Check invested v2g capacity + cn4 = "BEV-V2G-v2g->el-bus" + assert ( + round(self.results[cn4]["period_scalars"]["invest"].iloc[i], 4) + == v2g_cap / 3 + ) + + # Check invested v2g-2com capacity + cn5 = "BEV-V2G-2com->pkm-bus" + assert ( + round(self.results[cn5]["period_scalars"]["invest"].iloc[i], 4) + == v2g_cap / 3 + ) + cn6 = "BEV-G2V-2com->pkm-bus" + assert ( + self.results[cn6]["period_scalars"]["invest"].iloc[i] + == g2v_cap / 3 + ) + + cn7 = "BEV-inflex-2com->pkm-bus" + assert self.results[cn7]["period_scalars"]["invest"].iloc[ + i + ] == pytest.approx( + inflex_cap / 3 + ) # difference at 5th decimal place + + def test_bev_v2g_dispatch_multi_period(self): + """ + Tests v2g bev facade with multi-period in dispatch optimization. + + Energy quantities of load, pkm_demand and volatile and the efficiencies + are the same as in `test_bev_v2g_invest_multi_period`. + """ + el_bus = solph.Bus("el-bus") + el_bus.type = "bus" + self.energysystem.add(el_bus) + + indiv_mob = solph.Bus("pkm-bus") + indiv_mob.type = "bus" + self.energysystem.add(indiv_mob) + + volatile = Volatile( + label="wind", + bus=el_bus, + carrier="wind", + tech="onshore", + capacity=725.76, + profile=len(self.periods) * [1, 0, 0, 0, 0], + variable_costs=10, + ) + self.energysystem.add(volatile) + + load = Load( + label="load", + carrier="electricity", + bus=el_bus, + amount=100, + profile=len(self.periods) * [0, 0.1, 0, 1, 0], + ) + self.energysystem.add(load) + + pkm_demand = Load( + label="pkm_demand", + type="Load", + carrier="pkm", + bus=indiv_mob, + amount=100, # PKM + profile=len(self.periods) + * [0.5, 0.5, 1, 0, 0], # drive consumption + ) + self.energysystem.add(pkm_demand) + + bev_v2g = Bev( + type="bev", + label="BEV-V2G", + v2g=True, + electricity_bus=el_bus, + commodity_bus=indiv_mob, + storage_capacity=800, + loss_rate=0, # self discharge of storage + charging_power=800, + balanced=True, + expandable=False, + initial_storage_level=0, + availability=len(self.periods) + * [1, 1, 1, 1, 1], # Vehicle availability at charger + commodity_conversion_rate=5 / 6, # Energy to pkm + efficiency_mob_electrical=5 / 6, # Vehicle efficiency per 100km + efficiency_mob_v2g=5 / 6, # V2G charger efficiency + efficiency_mob_g2v=5 / 6, # Charger efficiency + efficiency_sto_in=5 / 6, # Storage charging efficiency + efficiency_sto_out=5 / 6, # Storage discharging efficiency, + variable_costs=10, # Charging costs + ) + self.energysystem.add(bev_v2g) + + self.get_om() + + solver_stats = self.solve_om() + + # rename results to make them accessible + self.rename_results() + + assert solver_stats["Solver"][0]["Status"] == "ok" + + # Check storage level + cn = "BEV-V2G-storage->None" + assert ( + round(self.results[cn]["sequences"]["storage_content"].iloc[0], 12) + == 0 + ) + assert ( + self.results[cn]["sequences"]["storage_content"].iloc[1] == 417.6 + ) + assert ( + self.results[cn]["sequences"]["storage_content"].iloc[2] == 316.8 + ) + assert self.results[cn]["sequences"]["storage_content"].iloc[3] == 144 + assert ( + round(self.results[cn]["sequences"]["storage_content"].iloc[4], 12) + == 0 + ) + assert ( + round(self.results[cn]["sequences"]["storage_content"].iloc[5], 12) + == 0 + ) + assert ( + self.results[cn]["sequences"]["storage_content"].iloc[6] == 417.6 + ) + assert ( + self.results[cn]["sequences"]["storage_content"].iloc[7] == 316.8 + ) + assert self.results[cn]["sequences"]["storage_content"].iloc[8] == 144 + assert ( + round(self.results[cn]["sequences"]["storage_content"].iloc[9], 12) + == 0 + ) diff --git a/tests/test_multi_period_constraints.py b/tests/test_multi_period_constraints.py index 735785d7..8f53a2c6 100644 --- a/tests/test_multi_period_constraints.py +++ b/tests/test_multi_period_constraints.py @@ -7,7 +7,7 @@ from oemof.solph import buses, helpers from oemof import solph -from oemof.tabular.constraint_facades import GenericIntegralLimit +from oemof.tabular.constraint_facades import CONSTRAINT_TYPE_MAP from oemof.tabular.facades import ( BackpressureTurbine, Commodity, @@ -542,7 +542,8 @@ def test_emission_constraint(self): output_parameters={"custom_attributes": {"emission_factor": 2.5}}, ) - emission_constraint = GenericIntegralLimit( + emission_constraint = CONSTRAINT_TYPE_MAP["generic_integral_limit"] + emission_constraint = emission_constraint( name="emission_constraint", type="e", limit=1000,