From 2b0907bec1c20003c6aa0c2bc4502fffc18b0731 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Wed, 24 Jan 2024 16:25:19 +0100 Subject: [PATCH 1/3] Add advanced example using NamedTuple as label --- examples/tuple_as_labels/tuple_as_label.ipynb | 402 ++++++++++++++++++ 1 file changed, 402 insertions(+) create mode 100644 examples/tuple_as_labels/tuple_as_label.ipynb diff --git a/examples/tuple_as_labels/tuple_as_label.ipynb b/examples/tuple_as_labels/tuple_as_label.ipynb new file mode 100644 index 000000000..796368afb --- /dev/null +++ b/examples/tuple_as_labels/tuple_as_label.ipynb @@ -0,0 +1,402 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This aims to be a tutorial for beginners that already introduces cocepts that until\n", + "now are mostly used by experienced users." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First, we define an energy system. Note that solph expects time points for its index.\n", + "Time intervals are defined between the points in time.\n", + "If your pints in time have a regular pattern, you can (at your option) infer the last interval.\n", + "Typically, it's better to explicitly give N+1 points in time to define N time intervals." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from oemof import solph\n", + "\n", + "n_time_points = 25\n", + "time_index = solph.create_time_index(2024, number=n_time_points)\n", + "\n", + "energy_system = solph.EnergySystem(\n", + " timeindex=time_index,\n", + " infer_last_interval=False,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The energy system is modelled as a mathematcal graph.\n", + "Often, `Bus`es are used to model commodities.\n", + "These are then connected by `Converter`s and to storages.\n", + "Each of these nodes needs a unique label to be identified.\n", + "Edges are directed and identified by the Nodes they connect to.\n", + "\n", + "Many users use strings as labels.\n", + "However, as energy systems become complex, keeping track of all the information can be hard.\n", + "In particular, manually managing string labels can be tedious.\n", + "Here, it comes handy that `Node`s (defined in oemof.network) accept every hashable type.\n", + "\n", + "A custom string representation is advised as the defauls (including type names) can be very long.\n", + "But labels should be easy to understand." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from typing import NamedTuple\n", + "from enum import IntEnum\n", + "\n", + "# A frozenset is an immutable set.\n", + "# Set: Ever sector present only once.\n", + "# Imutable:\n", + "# - sectors cannot be changed\n", + "# - makes the class hashable\n", + "class Label(NamedTuple):\n", + " location: str\n", + " sectors: frozenset[int]\n", + " component: str\n", + "\n", + " def __str__(self):\n", + " return f\"{self.location}/{sum(int(s) for s in self.sectors)}/{self.component}\"\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For the sectors, we create an Enum.\n", + "Enums are very useful to make sure, only predefined values are used.\n", + "This, e.g. prevents typos staying unnoticed.\n", + "The IntEnum in the example is defined in a way that any combination of sectors gives a unique ID.\n", + "This is a bit C-Style low-level encoding but can be useful, e.g. when saving the info to a file." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "class Sectors(IntEnum):\n", + " ELECTRICITY = 1\n", + " HEAT = 2\n", + " HYDROGEN = 4\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The Label above needs a frozenset.\n", + "To freeze sets in the background, we create a factory function." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "def label(\n", + " location: str,\n", + " sectors: set[int],\n", + " component: str,\n", + ") -> Label:\n", + " return Label(location, frozenset(sectors),component)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We now populate the energy system. It consists of several houses and a grid.\n", + "Let's start with the grid, as every house should be able to connect to it.\n", + "(Note that some of the functionality does ot rely on storing information in the label.\n", + "It could be placed elsewhere. However, it is convenient as you will see soon.)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "location=\"grid\"\n", + "\n", + "b_el_grid = solph.Bus(label(location, {Sectors.ELECTRICITY}, \"Bus\"))\n", + "energy_system.add(b_el_grid)\n", + "\n", + "energy_system.add(\n", + " solph.components.Source(\n", + " label(location, {Sectors.ELECTRICITY}, \"grid_connection\"),\n", + " outputs={b_el_grid: solph.Flow(variable_costs=0.4)},\n", + " # custom_attributes={\"sectors\": {Sectors.ELECTRICITY}}\n", + " )\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The houses start with an identical base: One Bus for electricity and one for heat.\n", + "Note that the busses are just called \"Bus\"." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "locations = [\"house_1\", \"house_2\"]\n", + "\n", + "for number, location in enumerate(locations):\n", + " b_el = solph.Bus(\n", + " label(location, {Sectors.ELECTRICITY}, \"Bus\"),\n", + " inputs={b_el_grid: solph.Flow()},\n", + " outputs={b_el_grid: solph.Flow()},\n", + " )\n", + " b_heat = solph.Bus(label(location, {Sectors.HEAT}, \"Bus\"))\n", + "\n", + " energy_system.add(b_el, b_heat)\n", + "\n", + " energy_system.add(\n", + " solph.components.Sink(\n", + " label(location, {Sectors.HEAT}, \"Demand\"),\n", + " inputs={b_heat: solph.Flow(nominal_value=1, fix=2*(1 + number * 0.1))},\n", + " )\n", + " )\n", + "\n", + " cop = 3\n", + " energy_system.add(\n", + " solph.components.Converter(\n", + " label(location, {Sectors.ELECTRICITY, Sectors.HEAT}, \"heat_pump\"),\n", + " inputs={b_el: solph.Flow()},\n", + " outputs={b_heat: solph.Flow()},\n", + " conversion_factors={b_el: 1 / cop},\n", + " )\n", + " )\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, we can add custom features to the houses.\n", + "House 1 receives additional demand for domestic hot water.\n", + "Note that we access the electricity bus using its label.\n", + "(The functionality will stay in oemof.network, however, the API is experimental.)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "location = \"house_1\"\n", + "\n", + "b_dhw = solph.Bus(label(location, {Sectors.HEAT}, \"Bus_DHW\"))\n", + "energy_system.add(b_dhw)\n", + "\n", + "energy_system.add(\n", + " solph.components.Sink(\n", + " label(location, {Sectors.HEAT}, \"DHW_Demand\"),\n", + " inputs={b_dhw: solph.Flow(\n", + " nominal_value=1,\n", + " fix=6*[0] + 1*[2] + 18*[0],\n", + " )},\n", + " )\n", + ")\n", + "\n", + "b_el = energy_system.node[label(location, {Sectors.ELECTRICITY}, \"Bus\")]\n", + "energy_system.add(\n", + " solph.components.Converter(\n", + " label(location, {Sectors.ELECTRICITY, Sectors.HEAT}, \"flow_heater\"),\n", + " inputs={b_el: solph.Flow()},\n", + " outputs={b_dhw: solph.Flow()},\n", + " )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "House 2 gets a PV system. Let's read in some data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "\n", + "pv_data = pd.read_csv(\n", + " \"tuple_as_label.csv\",\n", + " usecols=[2],\n", + ").head(n_time_points)[\"pv\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Instead of adding an excess think, we set the maximum possible Flow." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "energy_system.add(\n", + " solph.components.Source(\n", + " label(\"house_2\", {Sectors.ELECTRICITY}, \"PV\"),\n", + " outputs={\n", + " energy_system.node[label(\"house_2\", {Sectors.ELECTRICITY}, \"Bus\")]:\n", + " solph.Flow(nominal_value=20, max=pv_data),\n", + " },\n", + " )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Before we optimise the proble, we visually check the graph." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import networkx as nx\n", + "from oemof.network.graph import create_nx_graph\n", + "\n", + "graph = create_nx_graph(energy_system)\n", + "\n", + "nx.draw(graph, with_labels=True, font_size=8)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "model = solph.Model(energy_system)\n", + "model.solve(solver=\"cbc\", solve_kwargs={\"tee\": False})\n", + "results = solph.processing.results(model)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "\n", + "flows_to_heat = pd.DataFrame({\n", + " f\"{k[0].label.location}-{k[0].label.component}\": v[\"sequences\"][\"flow\"]\n", + " for k, v in results.items()\n", + " if (\n", + " isinstance(k[1], solph.Bus)\n", + " and Sectors.HEAT in k[1].label.sectors\n", + " )\n", + "})\n", + "\n", + "heat_demand = pd.DataFrame({\n", + " f\"{k[0].label.location}-{k[1].label.component}\": v[\"sequences\"][\"flow\"]\n", + " for k, v in results.items()\n", + " if isinstance(k[1], solph.components.Sink)\n", + "})\n", + "\n", + "heat_flows = pd.concat(\n", + " [flows_to_heat, heat_demand],\n", + " axis=1,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "heat_flows.plot(drawstyle=\"steps-post\")\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "electricity_sources = pd.DataFrame({\n", + " f\"{k[0].label.location}-{k[0].label.component}\": v[\"sequences\"][\"flow\"]\n", + " for k, v in results.items()\n", + " if isinstance(k[0], solph.components.Source)\n", + " and Sectors.ELECTRICITY in k[0].label.sectors\n", + "})\n", + "\n", + "electricity_sources.plot(drawstyle=\"steps-post\")\n", + "\n", + "plt.show()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From ccfd1cef426b9d19316a882fdb2e2390d53a6dec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Tue, 30 Jan 2024 20:30:22 +0100 Subject: [PATCH 2/3] Fix typo in advanced NamedTuple example Co-authored-by: Francesco Witte --- examples/tuple_as_labels/tuple_as_label.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/tuple_as_labels/tuple_as_label.ipynb b/examples/tuple_as_labels/tuple_as_label.ipynb index 796368afb..3e6bf4ba6 100644 --- a/examples/tuple_as_labels/tuple_as_label.ipynb +++ b/examples/tuple_as_labels/tuple_as_label.ipynb @@ -64,7 +64,7 @@ "from enum import IntEnum\n", "\n", "# A frozenset is an immutable set.\n", - "# Set: Ever sector present only once.\n", + "# Set: Every sector present only once.\n", "# Imutable:\n", "# - sectors cannot be changed\n", "# - makes the class hashable\n", From 1bb23724c5df8ef2a412f86639bd83dad57c3b35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Wed, 31 Jan 2024 09:26:43 +0100 Subject: [PATCH 3/3] Improve advanced NamedTuple example --- examples/tuple_as_labels/input_data.csv | 74 ++++++++++++++ examples/tuple_as_labels/tuple_as_label.ipynb | 98 +++++++++++++------ 2 files changed, 141 insertions(+), 31 deletions(-) create mode 100644 examples/tuple_as_labels/input_data.csv diff --git a/examples/tuple_as_labels/input_data.csv b/examples/tuple_as_labels/input_data.csv new file mode 100644 index 000000000..33a87d702 --- /dev/null +++ b/examples/tuple_as_labels/input_data.csv @@ -0,0 +1,74 @@ +MESS_DATUM,TT_TU,RF_TU,PV +2023020100,4.8,75,0 +2023020101,4.6,75,0 +2023020102,4.6,73,0 +2023020103,4.9,70,0 +2023020104,3.3,86,0 +2023020105,4.1,81,0 +2023020106,4.3,77,0 +2023020107,4.4,77,0 +2023020108,4.3,78,0 +2023020109,4.7,78,0.062188 +2023020110,4.9,78,0.15426 +2023020111,6.2,67,0.16016 +2023020112,6,69,0.1036 +2023020113,6.1,68,0.084862 +2023020114,6.5,66,0.052101 +2023020115,5.6,74,0.014673 +2023020116,5.8,73,0 +2023020117,1.9,95,0 +2023020118,3.4,87,0 +2023020119,4.2,80,0 +2023020120,4.5,77,0 +2023020121,4.7,76,0 +2023020122,5.1,72,0 +2023020123,4.5,74,0 +2023020200,4.5,73,0 +2023020201,4.2,73,0 +2023020202,4.2,74,0 +2023020203,4.3,72,0 +2023020204,4,72,0 +2023020205,4,75,0 +2023020206,4.6,74,0 +2023020207,4.9,76,0 +2023020208,3.8,74,0 +2023020209,2.6,87,0.047766 +2023020210,2.4,91,0.12973 +2023020211,2.8,89,0.15324 +2023020212,3.2,86,0.12837 +2023020213,3.5,87,0.10443 +2023020214,3.8,83,0.063981 +2023020215,3.5,84,0.019217 +2023020216,3.3,87,0 +2023020217,2.9,90,0 +2023020218,2.4,92,0 +2023020219,2.2,94,0 +2023020220,2.1,95,0 +2023020221,2,97,0 +2023020222,1.9,98,0 +2023020223,2,99,0 +2023020300,2.1,99,0 +2023020301,2.2,99,0 +2023020302,6.4,92,0 +2023020303,6.7,89,0 +2023020304,6.8,90,0 +2023020305,7.3,83,0 +2023020306,7.7,79,0 +2023020307,7.6,78,0 +2023020308,7.7,78,0 +2023020309,7.4,83,0.063357 +2023020310,7.6,85,0.1452 +2023020311,7.9,87,0.13022 +2023020312,8.5,83,0.052762 +2023020313,9,81,0.04271 +2023020314,7.9,78,0.025526 +2023020315,7.9,76,0.0066043 +2023020316,8.1,71,0 +2023020317,7.6,68,0 +2023020318,7,67,0 +2023020319,6.5,68,0 +2023020320,6.6,68,0 +2023020321,6.1,65,0 +2023020322,6,70,0 +2023020323,5.1,79,0 +2023020400,5,77,0 diff --git a/examples/tuple_as_labels/tuple_as_label.ipynb b/examples/tuple_as_labels/tuple_as_label.ipynb index 3e6bf4ba6..5794ca3a4 100644 --- a/examples/tuple_as_labels/tuple_as_label.ipynb +++ b/examples/tuple_as_labels/tuple_as_label.ipynb @@ -8,6 +8,15 @@ "now are mostly used by experienced users." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We want to model a group of buildings, each with demands for electricity and space heating.\n", + "Each has its individual heat pump and is connected to the electricity grid.\n", + "Some of the buildings have extra features, such as demand for domestic hot water or a PV system." + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -24,15 +33,44 @@ "metadata": {}, "outputs": [], "source": [ + "import datetime\n", "from oemof import solph\n", "\n", "n_time_points = 25\n", - "time_index = solph.create_time_index(2024, number=n_time_points)\n", + "\n", + "first_timepoint = datetime.datetime(2023,2,1,6)\n", + "time_index = solph.create_time_index(2024, number=n_time_points, start=first_timepoint)\n", "\n", "energy_system = solph.EnergySystem(\n", " timeindex=time_index,\n", " infer_last_interval=False,\n", - ")" + ")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "\n", + "temperatures = pd.read_csv(\n", + " \"input_data.csv\",\n", + " parse_dates=[0],\n", + " index_col=0,\n", + " date_format=\"%Y%m%d%H\",\n", + ")\n", + "\n", + "input_data = temperatures.reindex(time_index)\n", + "\n", + "locations = [\"house_1\", \"house_2\"]\n", + "heat_up_to = [12, 15]\n", + "power_per_degree = [0.7, 1.1]\n", + "\n", + "for location, temperature, power in zip(locations, heat_up_to, power_per_degree):\n", + " input_data[f\"heat_demand_{location}\"] = np.maximum(temperature - input_data[\"TT_TU\"], 0) * power" ] }, { @@ -131,7 +169,7 @@ "We now populate the energy system. It consists of several houses and a grid.\n", "Let's start with the grid, as every house should be able to connect to it.\n", "(Note that some of the functionality does ot rely on storing information in the label.\n", - "It could be placed elsewhere. However, it is convenient as you will see soon.)" + "It could be placed elsewhere. However, it is convenient to have it there, as you will see soon.)" ] }, { @@ -184,7 +222,9 @@ " energy_system.add(\n", " solph.components.Sink(\n", " label(location, {Sectors.HEAT}, \"Demand\"),\n", - " inputs={b_heat: solph.Flow(nominal_value=1, fix=2*(1 + number * 0.1))},\n", + " inputs={\n", + " b_heat: solph.Flow(nominal_value=1, fix=input_data[f\"heat_demand_{location}\"])\n", + " },\n", " )\n", " )\n", "\n", @@ -204,9 +244,12 @@ "metadata": {}, "source": [ "Now, we can add custom features to the houses.\n", - "House 1 receives additional demand for domestic hot water.\n", + "House 1 receives additional demand for domestic hot water in just one hour.\n", + "It's not in the unput data, so we create something manually.\n", + "(This is possible, but not advised. It's always good to have a time index.)\n", + "\n", "Note that we access the electricity bus using its label.\n", - "(The functionality will stay in oemof.network, however, the API is experimental.)" + "(The API is experimental, the way to do this might be changed with the next release of oemof.network.)" ] }, { @@ -225,7 +268,7 @@ " label(location, {Sectors.HEAT}, \"DHW_Demand\"),\n", " inputs={b_dhw: solph.Flow(\n", " nominal_value=1,\n", - " fix=6*[0] + 1*[2] + 18*[0],\n", + " fix=6*[0] + 1*[12] + 18*[0],\n", " )},\n", " )\n", ")\n", @@ -244,28 +287,10 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "House 2 gets a PV system. Let's read in some data." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import pandas as pd\n", - "\n", - "pv_data = pd.read_csv(\n", - " \"tuple_as_label.csv\",\n", - " usecols=[2],\n", - ").head(n_time_points)[\"pv\"]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Instead of adding an excess think, we set the maximum possible Flow." + "House 2 gets a PV system.\n", + "There are two common ways to model it:\n", + "* Seing PV generation as fix Flow, making an excess `Sink` obligatory.\n", + "* Setting the PV series as the maximum possible Flow." ] }, { @@ -279,7 +304,7 @@ " label(\"house_2\", {Sectors.ELECTRICITY}, \"PV\"),\n", " outputs={\n", " energy_system.node[label(\"house_2\", {Sectors.ELECTRICITY}, \"Bus\")]:\n", - " solph.Flow(nominal_value=20, max=pv_data),\n", + " solph.Flow(nominal_value=50, max=input_data[\"PV\"]),\n", " },\n", " )\n", ")" @@ -372,8 +397,19 @@ " and Sectors.ELECTRICITY in k[0].label.sectors\n", "})\n", "\n", - "electricity_sources.plot(drawstyle=\"steps-post\")\n", + "previous_sources = np.zeros(len(electricity_sources))\n", + "for source in electricity_sources:\n", + " current_source = previous_sources + electricity_sources[source]\n", + " plt.fill_between(\n", + " electricity_sources.index,\n", + " previous_sources,\n", + " current_source,\n", + " label=source,\n", + " step=\"post\",\n", + " )\n", + " previous_sources = current_source\n", "\n", + "plt.legend()\n", "plt.show()" ] }