{ "cells": [ { "attachments": {}, "cell_type": "markdown", "id": "c3b31311-c4da-43d1-b074-6b0432da7ea3", "metadata": {}, "source": [ "# Optimizing the design and operation of a two-nodal energy system with networks\n", "In this study, we want to look at a two-nodal system in a brownfield manner. We assume two fictous regions with a heat demand and an electricity demand and we want to study the decarbonization of these two regions and the impact of a carbon tax. First, we will optimize the design and operation of the system without a carbon tax and then study the effect of a carbon tax. The topology of the energy system is depicted below. Brown rectangles mean that the technology is already installed, and green rectangles mean that the technology can be newly built. Both colours mean that the technology can be expanded.\n", "\n", "We assume that there are two nodes: one depicting a city with no options to install wind turbines and no power plant and a rural node with a gas-fired power plant and the option to install wind turbines.\n", "\n", "
\n", "\n", "
" ] }, { "cell_type": "markdown", "id": "c76a7dea-3a71-4286-b28b-1259124a8df0", "metadata": {}, "source": [ "## Create templates\n", "We set the input data path and in this directory we can add input data templates for the model configuration and the topology with the function create_optimization_templates.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "35fa511b-d815-43d0-bf9f-21ca339151cf", "metadata": {}, "outputs": [], "source": [ "import adopt_net0 as adopt\n", "import json\n", "from pathlib import Path\n", "import os\n", "import pandas as pd\n", "import numpy as np\n", "\n", "# Create folder for results\n", "results_data_path = Path(\"./userData\")\n", "results_data_path.mkdir(parents=True, exist_ok=True)\n", "# Create input data path and optimization templates\n", "input_data_path = Path(\"./caseStudies/network\")\n", "input_data_path.mkdir(parents=True, exist_ok=True)\n", "adopt.create_optimization_templates(input_data_path)" ] }, { "cell_type": "markdown", "id": "f0d8ee0c-234f-4222-a2b0-700540c1292a", "metadata": {}, "source": [ "## Adapt Topology\n", "We need to adapt the topology as well as the model configuration file to our case study. This can be done either in the file itself (Topology.json) or, as we do it here via some lines of code.\n", "For the topology, we need to change the following:\n", "- Change nodes: city and urban area\n", "- Change carriers: electricity, heat and natural gas (we need to add hydrogen as a carrier as well, as the gas turbine also allows for a hydrogen input, even though we do not use it in this case study)\n", "- Change investment periods: period1\n", "- The options regarding the time frame we can leave at the default (one year with hourly operation)" ] }, { "cell_type": "code", "execution_count": null, "id": "5ad41f8f-43cc-4edd-a663-e66c8969339a", "metadata": {}, "outputs": [], "source": [ "# Load json template\n", "with open(input_data_path / \"Topology.json\", \"r\") as json_file:\n", " topology = json.load(json_file)\n", "# Nodes\n", "topology[\"nodes\"] = [\"city\", \"rural\"]\n", "# Carriers:\n", "topology[\"carriers\"] = [\"electricity\", \"heat\", \"gas\", \"hydrogen\"]\n", "# Investment periods:\n", "topology[\"investment_periods\"] = [\"period1\"]\n", "# Save json template\n", "with open(input_data_path / \"Topology.json\", \"w\") as json_file:\n", " json.dump(topology, json_file, indent=4)" ] }, { "cell_type": "markdown", "id": "ad6ca662-97ac-4899-a03c-7a9fa591d28c", "metadata": {}, "source": [ "## Adapt Model Configurations\n", "The model configuration we leave as it is, except spcifying a number of typical days to speed up the optimization (this system also solves on full resolution, but here, we want to only show how the model works at the expense of precision). Additionally, we set the MILP gap to 2%.\n", "- Change the number of typical days to 30 and select time aggregation method 1 (see [here](https://adopt-net0.readthedocs.io/en/latest/advanced_topics/time_aggregation.html#clustering-into-typical-days))\n", "- Change the MILP gap to 2%" ] }, { "cell_type": "code", "execution_count": null, "id": "f459d0aa-b2b6-40a9-b63c-1f55bfbdd07f", "metadata": {}, "outputs": [], "source": [ "# Load json template\n", "with open(input_data_path / \"ConfigModel.json\", \"r\") as json_file:\n", " configuration = json.load(json_file)\n", "# Set time aggregation settings:\n", "configuration[\"optimization\"][\"typicaldays\"][\"N\"][\"value\"] = 30\n", "configuration[\"optimization\"][\"typicaldays\"][\"method\"][\"value\"] = 1\n", "# Set MILP gap\n", "configuration[\"solveroptions\"][\"mipgap\"][\"value\"] = 0.02\n", "# Save json template\n", "with open(input_data_path / \"ConfigModel.json\", \"w\") as json_file:\n", " json.dump(configuration, json_file, indent=4)" ] }, { "cell_type": "markdown", "id": "eeba77ce-2e8f-451e-9f58-408121561959", "metadata": {}, "source": [ "## Define node locations as well as new and existing technologies at each node\n", "First, we create the required folder structure based on the Topology.json file and add the node locations. We can then show all available technologies." ] }, { "cell_type": "code", "execution_count": null, "id": "5de55d8c-0970-4154-91ad-d32b451eca31", "metadata": {}, "outputs": [], "source": [ "adopt.create_input_data_folder_template(input_data_path)\n", "\n", "# Define node locations (here two exemplary location in the Netherlands)\n", "node_location = pd.read_csv(input_data_path / \"NodeLocations.csv\", sep=';', index_col=0, header=0)\n", "node_lon = {'city': 5.1214, 'rural': 5.24}\n", "node_lat = {'city': 52.0907, 'rural': 51.9561}\n", "node_alt = {'city': 5, 'rural': 10}\n", "for node in ['city', 'rural']:\n", " node_location.at[node, 'lon'] = node_lon[node]\n", " node_location.at[node, 'lat'] = node_lat[node]\n", " node_location.at[node, 'alt'] = node_alt[node]\n", "\n", "node_location = node_location.reset_index()\n", "node_location.to_csv(input_data_path / \"NodeLocations.csv\", sep=';', index=False)\n", "\n", "adopt.show_available_technologies()" ] }, { "cell_type": "markdown", "id": "249550b9-a5a6-4663-9f08-eec442bc4e97", "metadata": {}, "source": [ "... And we can add the technologies that we need to the Technologies.json file at the respective nodes and copy over the required technology data" ] }, { "cell_type": "code", "execution_count": null, "id": "090672de-cd62-46ed-af4b-86c70e69f659", "metadata": {}, "outputs": [], "source": [ "# Add required technologies for node 'city'\n", "with open(input_data_path / \"period1\" / \"node_data\" / \"city\" / \"Technologies.json\", \"r\") as json_file:\n", " technologies = json.load(json_file)\n", "technologies[\"new\"] = [\"HeatPump_AirSourced\", \"Storage_Battery\", \"Photovoltaic\"]\n", "technologies[\"existing\"] = {\"Boiler_Small_NG\": 1000}\n", "\n", "with open(input_data_path / \"period1\" / \"node_data\" / \"city\" / \"Technologies.json\", \"w\") as json_file:\n", " json.dump(technologies, json_file, indent=4)\n", "\n", "# Add required technologies for node 'rural'\n", "with open(input_data_path / \"period1\" / \"node_data\" / \"rural\" / \"Technologies.json\", \"r\") as json_file:\n", " technologies = json.load(json_file)\n", "technologies[\"new\"] = [\"HeatPump_AirSourced\", \"Storage_Battery\", \"Photovoltaic\", \"WindTurbine_Onshore_4000\"]\n", "technologies[\"existing\"] = {\"Boiler_Small_NG\": 350, \"GasTurbine_simple\": 1000}\n", "\n", "with open(input_data_path / \"period1\" / \"node_data\" / \"rural\" / \"Technologies.json\", \"w\") as json_file:\n", " json.dump(technologies, json_file, indent=4)\n", "\n", "# Copy over technology files\n", "adopt.copy_technology_data(input_data_path)" ] }, { "cell_type": "markdown", "id": "0011d19f-1d99-4853-a2b5-ee16ed52e255", "metadata": {}, "source": [ "## Change maximum sizes of heat pumps and boilers\n", "As these technologies are by default set to a household level, we need to change the maximum sizes to make them suitable to meet a larger energy demand" ] }, { "cell_type": "code", "execution_count": null, "id": "e0f4505b-6aa8-46fc-95eb-0359681ae6c1", "metadata": {}, "outputs": [], "source": [ "for node in [\"city\", \"rural\"]:\n", " with open(input_data_path / \"period1\" / \"node_data\" / node / \"technology_data\"/ \"Boiler_Small_NG.json\", \"r\") as json_file:\n", " tec_data = json.load(json_file)\n", " tec_data[\"size_max\"] = 2000\n", "\n", " with open(input_data_path / \"period1\" / \"node_data\" /node / \"technology_data\"/ \"Boiler_Small_NG.json\", \"w\") as json_file:\n", " json.dump(tec_data, json_file, indent=4)\n", " \n", " with open(input_data_path / \"period1\" / \"node_data\" / node / \"technology_data\"/ \"HeatPump_AirSourced.json\", \"r\") as json_file:\n", " tec_data = json.load(json_file)\n", " tec_data[\"size_max\"] = 3000\n", " with open(input_data_path / \"period1\" / \"node_data\" / node / \"technology_data\"/ \"HeatPump_AirSourced.json\", \"w\") as json_file:\n", " json.dump(tec_data, json_file, indent=4)\n", " " ] }, { "cell_type": "markdown", "id": "6a206a60-1645-4f5f-858d-8525514647e4", "metadata": {}, "source": [ "## Define the existing and new electricity network between the two nodes\n", "We can see available networks that ship with the model with:" ] }, { "cell_type": "code", "execution_count": null, "id": "426663a4-7f03-4c5d-a14c-9cbe74a129a9", "metadata": {}, "outputs": [], "source": [ "adopt.show_available_networks()" ] }, { "cell_type": "markdown", "id": "8a257b9b-8323-4b35-9329-a28478df1a06", "metadata": {}, "source": [ "In this case, we will use 'electricityOnshore' for both the existing and new network:" ] }, { "cell_type": "code", "execution_count": null, "id": "b0316d03-9063-405e-9dd1-ab6ef26b6e41", "metadata": {}, "outputs": [], "source": [ "# Add networks\n", "with open(input_data_path / \"period1\" / \"Networks.json\", \"r\") as json_file:\n", " networks = json.load(json_file)\n", "networks[\"new\"] = [\"electricityOnshore\"]\n", "networks[\"existing\"] = [\"electricityOnshore\"]\n", "\n", "with open(input_data_path / \"period1\" / \"Networks.json\", \"w\") as json_file:\n", " json.dump(networks, json_file, indent=4)" ] }, { "cell_type": "markdown", "id": "ee28dd85-159c-4325-aab7-bd937090e7f6", "metadata": {}, "source": [ "Now we need to specify the network topologies for both the existing and the new network and copy them in the respective directory. This is easier done manually, but here we do it using Python.\n", "New networks need the following files (size_max_arcs is optional):\n", "- connection.csv (1 or 0)\n", "- distance.csv (distance in km): 50km between the two nodes\n", " \n", "Existing networks need the following files:\n", "- connection.csv (1 or 0)\n", "- distance.csv (distance in km): 50km between the two nodes\n", "- size.csv (size in MW): 1GW connection" ] }, { "cell_type": "code", "execution_count": null, "id": "7ed4c3d4-0921-473e-b3f8-f971b142bbaf", "metadata": {}, "outputs": [], "source": [ "# Make a new folder for the existing network\n", "os.makedirs(input_data_path / \"period1\" / \"network_topology\" / \"existing\" / \"electricityOnshore\", exist_ok=True)\n", "\n", "print(\"Existing network\")\n", "# Use the templates, fill and save them to the respective directory\n", "# Connection\n", "connection = pd.read_csv(input_data_path / \"period1\" / \"network_topology\" / \"existing\" / \"connection.csv\", sep=\";\", index_col=0)\n", "connection.loc[\"city\", \"rural\"] = 1\n", "connection.loc[\"rural\", \"city\"] = 1\n", "connection.to_csv(input_data_path / \"period1\" / \"network_topology\" / \"existing\" / \"electricityOnshore\" / \"connection.csv\", sep=\";\")\n", "print(\"Connection:\", connection)\n", "\n", "# Delete the template\n", "os.remove(input_data_path / \"period1\" / \"network_topology\" / \"existing\" / \"connection.csv\")\n", "\n", "# Distance\n", "distance = pd.read_csv(input_data_path / \"period1\" / \"network_topology\" / \"existing\" / \"distance.csv\", sep=\";\", index_col=0)\n", "distance.loc[\"city\", \"rural\"] = 50\n", "distance.loc[\"rural\", \"city\"] = 50\n", "distance.to_csv(input_data_path / \"period1\" / \"network_topology\" / \"existing\" / \"electricityOnshore\" / \"distance.csv\", sep=\";\")\n", "print(\"Distance:\", distance)\n", "\n", "# Delete the template\n", "os.remove(input_data_path / \"period1\" / \"network_topology\" / \"existing\" / \"distance.csv\")\n", "\n", "# Size\n", "size = pd.read_csv(input_data_path / \"period1\" / \"network_topology\" / \"existing\" / \"size.csv\", sep=\";\", index_col=0)\n", "size.loc[\"city\", \"rural\"] = 1000\n", "size.loc[\"rural\", \"city\"] = 1000\n", "size.to_csv(input_data_path / \"period1\" / \"network_topology\" / \"existing\" / \"electricityOnshore\" / \"size.csv\", sep=\";\")\n", "print(\"Size:\", size)\n", "\n", "# Delete the template\n", "os.remove(input_data_path / \"period1\" / \"network_topology\" / \"existing\" / \"size.csv\")\n", "\n", "\n", "print(\"New network\")\n", "# Make a new folder for the new network\n", "os.makedirs(input_data_path / \"period1\" / \"network_topology\" / \"new\" / \"electricityOnshore\", exist_ok=True)\n", "\n", "\n", "# max size arc\n", "arc_size = pd.read_csv(input_data_path / \"period1\" / \"network_topology\" / \"new\" / \"size_max_arcs.csv\", sep=\";\", index_col=0)\n", "arc_size.loc[\"city\", \"rural\"] = 3000\n", "arc_size.loc[\"rural\", \"city\"] = 3000\n", "arc_size.to_csv(input_data_path / \"period1\" / \"network_topology\" / \"new\" / \"electricityOnshore\" / \"size_max_arcs.csv\", sep=\";\")\n", "print(\"Max size per arc:\", arc_size)\n", "\n", "# Use the templates, fill and save them to the respective directory\n", "# Connection\n", "connection = pd.read_csv(input_data_path / \"period1\" / \"network_topology\" / \"new\" / \"connection.csv\", sep=\";\", index_col=0)\n", "connection.loc[\"city\", \"rural\"] = 1\n", "connection.loc[\"rural\", \"city\"] = 1\n", "connection.to_csv(input_data_path / \"period1\" / \"network_topology\" / \"new\" / \"electricityOnshore\" / \"connection.csv\", sep=\";\")\n", "print(\"Connection:\", connection)\n", "\n", "# Delete the template\n", "os.remove(input_data_path / \"period1\" / \"network_topology\" / \"new\" / \"connection.csv\")\n", "\n", "# Distance\n", "distance = pd.read_csv(input_data_path / \"period1\" / \"network_topology\" / \"new\" / \"distance.csv\", sep=\";\", index_col=0)\n", "distance.loc[\"city\", \"rural\"] = 50\n", "distance.loc[\"rural\", \"city\"] = 50\n", "distance.to_csv(input_data_path / \"period1\" / \"network_topology\" / \"new\" / \"electricityOnshore\" / \"distance.csv\", sep=\";\")\n", "print(\"Distance:\", distance)\n", "\n", "# Delete the template\n", "os.remove(input_data_path / \"period1\" / \"network_topology\" / \"new\" / \"distance.csv\")\n", "\n", "# Delete the max_size_arc template\n", "os.remove(input_data_path / \"period1\" / \"network_topology\" / \"new\" / \"size_max_arcs.csv\")" ] }, { "cell_type": "markdown", "id": "ae9cd684-e3b2-43df-8cd1-9dc75cc23456", "metadata": {}, "source": [ "## Copy over network data\n", "Copy over network data, change cost data." ] }, { "cell_type": "code", "execution_count": null, "id": "824373ec-45bc-437a-8eef-e29b37f71f45", "metadata": {}, "outputs": [], "source": [ "adopt.copy_network_data(input_data_path)\n", "\n", "with open(input_data_path / \"period1\" / \"network_data\"/ \"electricityOnshore.json\", \"r\") as json_file:\n", " network_data = json.load(json_file)\n", "\n", "network_data[\"Economics\"][\"gamma2\"] = 40000\n", "network_data[\"Economics\"][\"gamma4\"] = 300\n", "\n", "with open(input_data_path / \"period1\" / \"network_data\"/ \"electricityOnshore.json\", \"w\") as json_file:\n", " json.dump(network_data, json_file, indent=4)" ] }, { "cell_type": "markdown", "id": "5021ac9f-1839-4047-aeab-db4ca2904403", "metadata": {}, "source": [ "## Define demand, climate data, import limits, import prices" ] }, { "cell_type": "code", "execution_count": null, "id": "a06d2519-1d5c-4cc3-9f44-509c7af6257a", "metadata": {}, "outputs": [], "source": [ "# Read hourly data from Excel (Examplary demand profiles are provided with the package)\n", "rural_hourly_data = adopt.load_network_rural_data()\n", "city_hourly_data = adopt.load_network_city_data()\n", "\n", "# Save the hourly data to the carrier's file in the case study folder\n", "# electricity demand and price\n", "hourly_data = {\n", " 'city': city_hourly_data,\n", " 'rural': rural_hourly_data\n", "}\n", "\n", "el_demand = {}\n", "heat_demand = {}\n", "\n", "for node in ['city', 'rural']:\n", " el_demand[node] = hourly_data[node].iloc[:, 1]\n", " heat_demand[node] = hourly_data[node].iloc[:, 0]\n", " adopt.fill_carrier_data(input_data_path, value_or_data=el_demand[node], columns=['Demand'], carriers=['electricity'], nodes=[node])\n", " adopt.fill_carrier_data(input_data_path, value_or_data=heat_demand[node], columns=['Demand'], carriers=['heat'], nodes=[node])\n", "\n", " # Set import limits/cost\n", " adopt.fill_carrier_data(input_data_path, value_or_data=4000, columns=['Import limit'], carriers=['gas'], nodes=[node])\n", " adopt.fill_carrier_data(input_data_path, value_or_data=2000, columns=['Import limit'], carriers=['electricity'], nodes=[node])\n", " adopt.fill_carrier_data(input_data_path, value_or_data=0.25, columns=['Import emission factor'], carriers=['electricity'], nodes=[node])\n", " adopt.fill_carrier_data(input_data_path, value_or_data=40, columns=['Import price'], carriers=['gas'], nodes=[node])\n", " adopt.fill_carrier_data(input_data_path, value_or_data=120, columns=['Import price'], carriers=['electricity'], nodes=[node])\n", "\n", "# Define climate data\n", "adopt.load_climate_data_from_api(input_data_path)" ] }, { "cell_type": "markdown", "id": "23bc28fa-a5a9-4106-b462-e3ca284cd781", "metadata": {}, "source": [ "## Run without carbon costs" ] }, { "cell_type": "code", "execution_count": null, "id": "60748bbe-2cee-451a-abc4-529397038a38", "metadata": {}, "outputs": [], "source": [ "m = adopt.ModelHub()\n", "m.read_data(input_data_path)\n", "m.quick_solve()" ] }, { "attachments": {}, "cell_type": "markdown", "id": "21d56eda-909e-4be8-8f54-5439f31c11c6", "metadata": { "scrolled": true }, "source": [ "The results of the optimizations, which are saved in the userData folder, can be (partially) visualized using the provided visualization platform (https://resultvisualization.streamlit.app/). Here we just report some screenshots, specifically the sizes (all in MW) of the technologies installed in the network. It can be noticed that, in addition to the existing technologies (gas boiler in the city node, gas boiler and gas plant in the rural node), it is cost-efficient to install heat pumps in both nodes and PV at both nodes. Note that the heat pump sizes seem quite small, but it is given in terms of electricity input, so the heat output needs to be multiplied by the COP.\n", "\n", "### Sizes heating technologies\n", "
\n", "\n", "
\n", "\n", "### Sizes electricity technologies\n", "
\n", "\n", "
" ] }, { "attachments": {}, "cell_type": "markdown", "id": "3ac05bfa-f38f-497b-ab30-bda8cca55737", "metadata": {}, "source": [ "### Network operation\n", "From a network perspective, no transmission capacity has been added. The electricity exchange happens only from the rural to the city node.\n", "\n", "
\n", "\n", "
" ] }, { "cell_type": "markdown", "id": "7c10bead-fbfa-4710-a5d6-76af94726363", "metadata": {}, "source": [ "## Run with carbon costs" ] }, { "cell_type": "code", "execution_count": null, "id": "79174d5d-7d15-41a0-8cb8-84057c47d87c", "metadata": {}, "outputs": [], "source": [ "# Set carbon emission price\n", "carbon_price = np.ones(8760)*100\n", "for node in [\"city\", \"rural\"]:\n", " carbon_cost_path = \"./caseStudies/network/period1/node_data/\" + node + \"/CarbonCost.csv\"\n", " carbon_cost_template = pd.read_csv(carbon_cost_path, sep=';', index_col=0, header=0)\n", " carbon_cost_template['price'] = carbon_price\n", " carbon_cost_template = carbon_cost_template.reset_index()\n", " carbon_cost_template.to_csv(carbon_cost_path, sep=';', index=False)\n", "\n", "m = adopt.ModelHub()\n", "m.read_data(input_data_path)\n", "m.quick_solve()" ] }, { "attachments": {}, "cell_type": "markdown", "id": "5bc1a2d9-2f1f-4b07-a318-7c57227737bd", "metadata": { "scrolled": true }, "source": [ "The introduction of a CO2 tax leads to higher deployment of PV at the rural node (and no PV deployment at the city node), leading to reduced use of the gas turbine. Regarding the heating technologies, the size of the heat pump increases by a few MW in both the nodes\n", "\n", "### Sizes heating technologies\n", "
\n", "\n", "
\n", "\n", "### Sizes electricity technologies\n", "
\n", "\n", "
\n" ] }, { "attachments": {}, "cell_type": "markdown", "id": "73db5554-2a1c-4a47-be51-f0a093a07de8", "metadata": {}, "source": [ "### Network operation\n", "\n", "When the CO2 tax is included, the electricity flow from the rural node to the city node increases almost 5-fold.\n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "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.12.3" } }, "nbformat": 4, "nbformat_minor": 5 }