├── sommerakademie-oxford ├── logo.png ├── _assets │ ├── networks.png │ ├── fuel-plant.jpeg │ ├── households.png │ └── single-node.png ├── _toc.yml ├── _static │ └── custom.css ├── _config.yml ├── intro.md ├── 01-python-introduction.ipynb ├── D-exploration-green-fuels.ipynb ├── B-exploration-networks.ipynb ├── A-exploration-single-node.ipynb └── 03-pypsa-dispatch.ipynb ├── .gitignore ├── requirements.txt ├── README.md ├── .github └── workflows │ └── deploy.yml ├── LICENSE └── .pre-commit-config.yaml /sommerakademie-oxford/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fneum/sommerakademie-oxford/main/sommerakademie-oxford/logo.png -------------------------------------------------------------------------------- /sommerakademie-oxford/_assets/networks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fneum/sommerakademie-oxford/main/sommerakademie-oxford/_assets/networks.png -------------------------------------------------------------------------------- /sommerakademie-oxford/_assets/fuel-plant.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fneum/sommerakademie-oxford/main/sommerakademie-oxford/_assets/fuel-plant.jpeg -------------------------------------------------------------------------------- /sommerakademie-oxford/_assets/households.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fneum/sommerakademie-oxford/main/sommerakademie-oxford/_assets/households.png -------------------------------------------------------------------------------- /sommerakademie-oxford/_assets/single-node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fneum/sommerakademie-oxford/main/sommerakademie-oxford/_assets/single-node.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .ipynb-checkpoints 2 | sommerakademie-oxford/data 3 | *.geojson 4 | *.nc 5 | *.tiff 6 | *.tif 7 | *.html 8 | sommerakademie-oxford/tmp/ 9 | sommerakademie-oxford/_build/ 10 | sommerakademie-oxford/tmp.csv 11 | *cleared.ipynb 12 | 13 | data 14 | data-preparation.ipynb 15 | 16 | .venv 17 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | pandas>=2 3 | pyyaml 4 | netcdf4 5 | pypsa==0.35 6 | ipython 7 | jupyterlab 8 | tabula-py 9 | xlrd 10 | lxml 11 | tables 12 | pyxlsb 13 | openpyxl 14 | cartopy>=0.22 15 | matplotlib>=3.6 16 | plotly<6 17 | jupyter-book 18 | ghp-import 19 | highspy>=1.11 20 | numexpr>2.8.5 21 | pre-commit 22 | -------------------------------------------------------------------------------- /sommerakademie-oxford/_toc.yml: -------------------------------------------------------------------------------- 1 | # Table of contents 2 | # Learn more at https://jupyterbook.org/customize/toc.html 3 | 4 | format: jb-book 5 | root: intro 6 | parts: 7 | - caption: Introductions 8 | chapters: 9 | - file: 01-python-introduction 10 | - file: 02-pandas-introduction 11 | - file: 03-pypsa-dispatch 12 | - file: 04-pypsa-investment 13 | - file: 05-pypsa-sectors 14 | - caption: Exploration Topics 15 | chapters: 16 | - file: A-exploration-single-node.ipynb 17 | - file: B-exploration-networks.ipynb 18 | - file: C-exploration-household.ipynb 19 | - file: D-exploration-green-fuels.ipynb -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Material for Sommerakademie Oxford 2 | 3 | ## Installation 4 | 5 | ```sh 6 | uv venv 7 | source .venv/bin/activate 8 | uv pip install -r requirements.txt 9 | ``` 10 | 11 | ## Build 12 | 13 | ```sh 14 | jupyter-book clean sommerakademie-oxford/ 15 | jupyter-book build sommerakademie-oxford/ 16 | ``` 17 | 18 | A fully-rendered HTML version of the book will be built in `sommerakademie-oxford/_build/html/`. 19 | 20 | ## Credits 21 | 22 | This project is created using the excellent open source [Jupyter Book project](https://jupyterbook.org/) and the [executablebooks/cookiecutter-jupyter-book template](https://github.com/executablebooks/cookiecutter-jupyter-book). 23 | -------------------------------------------------------------------------------- /sommerakademie-oxford/_static/custom.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Overpass:wght@400;600;700&display=swap'); 2 | 3 | /* Body + headings */ 4 | body, 5 | h1, h2, h3, h4, h5, h6, 6 | p, li, dt, dd, blockquote, 7 | .navbar, .sidebar, .caption, .admonition { 8 | font-family: 'Overpass', sans-serif !important; 9 | } 10 | 11 | @import url('https://fonts.googleapis.com/css2?family=Overpass+Mono:wght@400;600&display=swap'); 12 | 13 | code, pre, kbd, samp { 14 | font-family: 'Overpass Mono', monospace !important; 15 | } 16 | 17 | /* Change row hover color */ 18 | .table tbody tr:hover { 19 | background-color: #bbcde2 !important; /* your custom color */ 20 | } 21 | 22 | /* Pandas DataFrame hover rows */ 23 | .dataframe tbody tr:hover { 24 | background-color: #bdd1f6 !important; 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | # Trigger the workflow on push to main branch 8 | push: 9 | branches: 10 | - main 11 | 12 | # This job installs dependencies, build the book, and pushes it to `gh-pages` 13 | jobs: 14 | build-and-deploy-book: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | # Install dependencies 20 | - name: Setup Python 21 | uses: actions/setup-python@v4 22 | with: 23 | python-version: '3.13' 24 | cache: 'pip' # caching pip dependencies 25 | 26 | - name: Install environment with pip 27 | run: pip install -r requirements.txt 28 | 29 | # Build the book 30 | - name: Build the book 31 | run: | 32 | jupyter-book build sommerakademie-oxford 33 | 34 | # Deploy the book's HTML to gh-pages branch 35 | - name: GitHub Pages action 36 | uses: peaceiris/actions-gh-pages@v4 37 | with: 38 | github_token: ${{ secrets.GITHUB_TOKEN }} 39 | publish_dir: sommerakademie-oxford/_build/html 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-2025, Fabian Neumann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: : 2022 The PyPSA-Eur Authors 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | exclude: "^LICENSES" 5 | 6 | repos: 7 | - repo: https://github.com/pre-commit/pre-commit-hooks 8 | rev: v6.0.0 9 | hooks: 10 | - id: check-merge-conflict 11 | - id: end-of-file-fixer 12 | - id: fix-encoding-pragma 13 | - id: mixed-line-ending 14 | - id: trailing-whitespace 15 | # - id: check-added-large-files 16 | # args: ["--maxkb=6000"] 17 | 18 | # Formatting with "black" coding style 19 | - repo: https://github.com/psf/black 20 | rev: 25.1.0 21 | hooks: 22 | # Format Python files 23 | - id: black 24 | # Format Jupyter Python notebooks 25 | - id: black-jupyter 26 | 27 | # Remove output from Jupyter notebooks 28 | - repo: https://github.com/aflc/pre-commit-jupyter 29 | rev: v1.2.1 30 | hooks: 31 | - id: jupyter-notebook-cleanup 32 | args: ["--remove-kernel-metadata"] 33 | 34 | # Do YAML formatting (before the linter checks it for misses) 35 | - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks 36 | rev: v2.15.0 37 | hooks: 38 | - id: pretty-format-yaml 39 | args: [--autofix, --indent, "2", --preserve-quotes] 40 | -------------------------------------------------------------------------------- /sommerakademie-oxford/_config.yml: -------------------------------------------------------------------------------- 1 | ####################################################################################### 2 | # A default configuration that will be loaded for all jupyter books 3 | # See the documentation for help and more options: 4 | # https://jupyterbook.org/customize/config.html 5 | 6 | ####################################################################################### 7 | # Book settings 8 | title: PyPSA Introduction # The title of the book. Will be placed in the left navbar. 9 | author: Fabian Neumann and Jonas Finke # The author of the book 10 | copyright: "2022-2025" # Copyright year to be placed in the footer 11 | logo: logo.png # A path to the book logo 12 | 13 | # Force re-execution of notebooks on each build. 14 | # See https://jupyterbook.org/content/execute.html 15 | execute: 16 | execute_notebooks: force 17 | 18 | # Define the name of the latex output file for PDF builds 19 | latex: 20 | latex_documents: 21 | targetname: book.tex 22 | 23 | # Add a bibtex file so that we can create citations 24 | # bibtex_bibfiles: 25 | # - references.bib 26 | 27 | # Information about where the book exists on the web 28 | repository: 29 | url: https://github.com/fneum/sommerakademie-oxford # Online location of your book 30 | path_to_book: sommerakademie-oxford # Optional path to your book, relative to the repository root 31 | branch: main # Which branch of the repository should be used when creating links (optional) 32 | 33 | # Add GitHub buttons to your book 34 | # See https://jupyterbook.org/customize/config.html#add-a-link-to-your-repository 35 | html: 36 | use_issues_button: true 37 | use_repository_button: true 38 | 39 | launch_buttons: 40 | colab_url: "https://colab.research.google.com" 41 | 42 | parse: 43 | myst_enable_extensions: 44 | - amsmath 45 | - dollarmath 46 | - linkify 47 | - colon_fence 48 | - substitution 49 | 50 | sphinx: 51 | config: 52 | html_js_files: 53 | - https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.4/require.min.js 54 | html_static_path: ["_static"] 55 | html_css_files: 56 | - custom.css 57 | -------------------------------------------------------------------------------- /sommerakademie-oxford/intro.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This website introduces you to the basics of using PyPSA for energy system modelling. 4 | 5 | PyPSA is an open-source Python framework and stands for **Python for Power 6 | System Analysis**. 7 | 8 | PyPSA is made for optimising and simulating modern power and energy systems that 9 | include features such as conventional generators with operational constraints, 10 | variable wind and solar generation, hydro-electricity, storage, coupling to 11 | other energy sectors, and linearised power flow with loss approximations. 12 | 13 | :::{note} 14 | Checkout the [documentation](https://pypsa--1250.org.readthedocs.build/en/1250/) 15 | for more details. 16 | ::: 17 | 18 | For working with PyPSA, we will need to cover some Python and `pandas` basics. 19 | Python is a programming language that is widely used in scientific computing and 20 | data analysis. `pandas` is the core library for handling tabular data in Python. 21 | Think of it as Excel but with code. 22 | 23 | ## Installation 24 | 25 | Below you can find three installation options. You only need one! 26 | 27 | ### Google Colab 28 | 29 | You can run the examples without a local Python installation using [Google Colab 30 | (colab.google)](https://colab.google) which provides an online Python 31 | environment. This requires a Google account. Click on the rocket in the top 32 | right corner and launch "Colab". If that doesn't work download the `.ipynb` file 33 | and import it in [Google Colab](https://colab.research.google.com/). 34 | 35 | :::{note} 36 | This is the easiest option for Python beginners! 37 | ::: 38 | 39 | ### conda 40 | 41 | Install `conda` by following [these instructions](https://docs.conda.io/projects/conda/en/stable/user-guide/install/index.html). 42 | 43 | The `conda` environment specification can be downloaded here: 44 | 45 | https://github.com/fneum/sommerakademie-oxford/blob/main/environment.yaml 46 | 47 | Navigate to the directory containing the `environment.yaml` file and run: 48 | 49 | conda env create -f environment.yaml 50 | 51 | Activate the environment with: 52 | 53 | conda activate oxford 54 | 55 | 56 | ### uv / pip 57 | 58 | Install `uv` by following [these instructions](https://docs.astral.sh/uv/getting-started/installation/). 59 | 60 | The environment specification can be downloaded here: 61 | 62 | https://github.com/fneum/sommerakademie-oxford/blob/main/requirements.txt 63 | 64 | Navigate to the directory containing the `requirements.txt` file and run: 65 | 66 | uv venv 67 | source .venv/bin/activate 68 | uv pip install -r requirements.txt 69 | 70 | 71 | ## Usage 72 | 73 | If you chose Google Colab, you can run the notebook cells directly without any additional setup. 74 | 75 | If you chose a local installation, you can run the notebooks in the terminal with: 76 | 77 | # first navigate to the notebook directory with cd (stands for change directory) 78 | cd path/to/your/notebook 79 | 80 | # then start jupyter lab 81 | jupyter lab 82 | 83 | This will open the Jupyter Lab interface in your web browser, where you can interact with the notebooks. 84 | If the browser does not open automatically, you can manually navigate to `http://localhost:8888`. 85 | -------------------------------------------------------------------------------- /sommerakademie-oxford/01-python-introduction.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Basics `Python`" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | ":::{note}\n", 15 | "If you have not yet set up Python on your computer, you can execute this tutorial in your browser via [Google Colab](https://colab.research.google.com/). Click on the rocket in the top right corner and launch \"Colab\". If that does not work download the `.ipynb` file and import it in [Google Colab](https://colab.research.google.com/)\n", 16 | ":::" 17 | ] 18 | }, 19 | { 20 | "cell_type": "markdown", 21 | "metadata": { 22 | "tags": [] 23 | }, 24 | "source": [ 25 | "## Basics" 26 | ] 27 | }, 28 | { 29 | "cell_type": "markdown", 30 | "metadata": {}, 31 | "source": [ 32 | "Comments are anything that comes after the \"#\" symbol" 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": 1, 38 | "metadata": {}, 39 | "outputs": [], 40 | "source": [ 41 | "a = 1 # assign integer 1 to variable a\n", 42 | "b = \"hello\" # assign string \"hello\" to variable b" 43 | ] 44 | }, 45 | { 46 | "cell_type": "markdown", 47 | "metadata": {}, 48 | "source": [ 49 | "We can _print_ variables to see their values:" 50 | ] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "execution_count": 2, 55 | "metadata": {}, 56 | "outputs": [ 57 | { 58 | "name": "stdout", 59 | "output_type": "stream", 60 | "text": [ 61 | "1\n", 62 | "hello\n" 63 | ] 64 | } 65 | ], 66 | "source": [ 67 | "# how to we see our variables?\n", 68 | "print(a)\n", 69 | "print(b)" 70 | ] 71 | }, 72 | { 73 | "cell_type": "markdown", 74 | "metadata": {}, 75 | "source": [ 76 | "All variables are objects. Every object has a type. To find out what type your variables are" 77 | ] 78 | }, 79 | { 80 | "cell_type": "code", 81 | "execution_count": 3, 82 | "metadata": {}, 83 | "outputs": [ 84 | { 85 | "name": "stdout", 86 | "output_type": "stream", 87 | "text": [ 88 | "\n", 89 | "\n" 90 | ] 91 | } 92 | ], 93 | "source": [ 94 | "print(type(a))\n", 95 | "print(type(b))" 96 | ] 97 | }, 98 | { 99 | "cell_type": "markdown", 100 | "metadata": {}, 101 | "source": [ 102 | "Objects can have **methods**:" 103 | ] 104 | }, 105 | { 106 | "cell_type": "code", 107 | "execution_count": 4, 108 | "metadata": {}, 109 | "outputs": [ 110 | { 111 | "data": { 112 | "text/plain": [ 113 | "'Hello'" 114 | ] 115 | }, 116 | "execution_count": 4, 117 | "metadata": {}, 118 | "output_type": "execute_result" 119 | } 120 | ], 121 | "source": [ 122 | "b.capitalize()" 123 | ] 124 | }, 125 | { 126 | "cell_type": "markdown", 127 | "metadata": { 128 | "tags": [] 129 | }, 130 | "source": [ 131 | "## Math\n", 132 | "\n", 133 | "Basic arithmetic and boolean logic:" 134 | ] 135 | }, 136 | { 137 | "cell_type": "code", 138 | "execution_count": 5, 139 | "metadata": {}, 140 | "outputs": [ 141 | { 142 | "data": { 143 | "text/plain": [ 144 | "-3" 145 | ] 146 | }, 147 | "execution_count": 5, 148 | "metadata": {}, 149 | "output_type": "execute_result" 150 | } 151 | ], 152 | "source": [ 153 | "# addition / subtraction\n", 154 | "1 + 1 - 5" 155 | ] 156 | }, 157 | { 158 | "cell_type": "code", 159 | "execution_count": 6, 160 | "metadata": {}, 161 | "outputs": [ 162 | { 163 | "data": { 164 | "text/plain": [ 165 | "50" 166 | ] 167 | }, 168 | "execution_count": 6, 169 | "metadata": {}, 170 | "output_type": "execute_result" 171 | } 172 | ], 173 | "source": [ 174 | "# multiplication\n", 175 | "5 * 10" 176 | ] 177 | }, 178 | { 179 | "cell_type": "code", 180 | "execution_count": 7, 181 | "metadata": {}, 182 | "outputs": [ 183 | { 184 | "data": { 185 | "text/plain": [ 186 | "0.5" 187 | ] 188 | }, 189 | "execution_count": 7, 190 | "metadata": {}, 191 | "output_type": "execute_result" 192 | } 193 | ], 194 | "source": [ 195 | "# division\n", 196 | "1 / 2" 197 | ] 198 | }, 199 | { 200 | "cell_type": "code", 201 | "execution_count": 8, 202 | "metadata": {}, 203 | "outputs": [ 204 | { 205 | "data": { 206 | "text/plain": [ 207 | "16" 208 | ] 209 | }, 210 | "execution_count": 8, 211 | "metadata": {}, 212 | "output_type": "execute_result" 213 | } 214 | ], 215 | "source": [ 216 | "# exponentiation\n", 217 | "2**4" 218 | ] 219 | }, 220 | { 221 | "cell_type": "markdown", 222 | "metadata": { 223 | "tags": [] 224 | }, 225 | "source": [ 226 | "## Comparisons\n", 227 | "\n", 228 | "We can compare objects using comparison operators, and we'll get back a _boolean_ (i.e. True/False) result:" 229 | ] 230 | }, 231 | { 232 | "cell_type": "code", 233 | "execution_count": 9, 234 | "metadata": {}, 235 | "outputs": [ 236 | { 237 | "data": { 238 | "text/plain": [ 239 | "True" 240 | ] 241 | }, 242 | "execution_count": 9, 243 | "metadata": {}, 244 | "output_type": "execute_result" 245 | } 246 | ], 247 | "source": [ 248 | "2 < 3" 249 | ] 250 | }, 251 | { 252 | "cell_type": "code", 253 | "execution_count": 10, 254 | "metadata": {}, 255 | "outputs": [ 256 | { 257 | "data": { 258 | "text/plain": [ 259 | "False" 260 | ] 261 | }, 262 | "execution_count": 10, 263 | "metadata": {}, 264 | "output_type": "execute_result" 265 | } 266 | ], 267 | "source": [ 268 | "\"energy\" == \"power\"" 269 | ] 270 | }, 271 | { 272 | "cell_type": "markdown", 273 | "metadata": { 274 | "tags": [] 275 | }, 276 | "source": [ 277 | "## Booleans\n", 278 | "\n", 279 | "We also have so-called \"boolean operators\" or \"logical operators\" which also evaluate to either `True` or `False`:" 280 | ] 281 | }, 282 | { 283 | "cell_type": "code", 284 | "execution_count": 11, 285 | "metadata": {}, 286 | "outputs": [ 287 | { 288 | "data": { 289 | "text/plain": [ 290 | "True" 291 | ] 292 | }, 293 | "execution_count": 11, 294 | "metadata": {}, 295 | "output_type": "execute_result" 296 | } 297 | ], 298 | "source": [ 299 | "True and True" 300 | ] 301 | }, 302 | { 303 | "cell_type": "code", 304 | "execution_count": 12, 305 | "metadata": {}, 306 | "outputs": [ 307 | { 308 | "data": { 309 | "text/plain": [ 310 | "True" 311 | ] 312 | }, 313 | "execution_count": 12, 314 | "metadata": {}, 315 | "output_type": "execute_result" 316 | } 317 | ], 318 | "source": [ 319 | "True or not False" 320 | ] 321 | }, 322 | { 323 | "cell_type": "markdown", 324 | "metadata": {}, 325 | "source": [ 326 | "## Conditionals ##\n", 327 | "\n", 328 | "Conditionals allow a program to make decisions. They dictate the flow of execution based on whether certain conditions are met." 329 | ] 330 | }, 331 | { 332 | "cell_type": "markdown", 333 | "metadata": {}, 334 | "source": [ 335 | ":::{note}\n", 336 | "In Python, indentation is **mandatory** and blocks of code are closed by the indentation level.\n", 337 | "::::" 338 | ] 339 | }, 340 | { 341 | "cell_type": "code", 342 | "execution_count": 13, 343 | "metadata": {}, 344 | "outputs": [ 345 | { 346 | "name": "stdout", 347 | "output_type": "stream", 348 | "text": [ 349 | "Positive Number\n" 350 | ] 351 | } 352 | ], 353 | "source": [ 354 | "x = 100\n", 355 | "if x > 0:\n", 356 | " print(\"Positive Number\")\n", 357 | "elif x < 0:\n", 358 | " print(\"Negative Number\")\n", 359 | "else:\n", 360 | " print(\"Zero!\")" 361 | ] 362 | }, 363 | { 364 | "cell_type": "markdown", 365 | "metadata": {}, 366 | "source": [ 367 | "## Loops ##" 368 | ] 369 | }, 370 | { 371 | "cell_type": "markdown", 372 | "metadata": {}, 373 | "source": [ 374 | "**Loops** tell a program to perform repetitive tasks.\n", 375 | "They govern the flow of execution by repeatedly processing a block of code, often until a certain condition is reached.\n", 376 | "\n", 377 | ":::{note}\n", 378 | "In Python, we always count from 0!\n", 379 | ":::" 380 | ] 381 | }, 382 | { 383 | "cell_type": "code", 384 | "execution_count": 14, 385 | "metadata": {}, 386 | "outputs": [ 387 | { 388 | "name": "stdout", 389 | "output_type": "stream", 390 | "text": [ 391 | "electricity 11\n", 392 | "hydrogen 8\n", 393 | "methane 7\n" 394 | ] 395 | } 396 | ], 397 | "source": [ 398 | "for carrier in [\"electricity\", \"hydrogen\", \"methane\"]:\n", 399 | " print(carrier, len(carrier))" 400 | ] 401 | }, 402 | { 403 | "cell_type": "markdown", 404 | "metadata": {}, 405 | "source": [ 406 | "## Lists ##" 407 | ] 408 | }, 409 | { 410 | "cell_type": "code", 411 | "execution_count": 15, 412 | "metadata": {}, 413 | "outputs": [], 414 | "source": [ 415 | "l = [\"electricity\", \"hydrogen\", \"methane\"]" 416 | ] 417 | }, 418 | { 419 | "cell_type": "markdown", 420 | "metadata": {}, 421 | "source": [ 422 | "Accessing items from a list:" 423 | ] 424 | }, 425 | { 426 | "cell_type": "code", 427 | "execution_count": 16, 428 | "metadata": {}, 429 | "outputs": [ 430 | { 431 | "data": { 432 | "text/plain": [ 433 | "'electricity'" 434 | ] 435 | }, 436 | "execution_count": 16, 437 | "metadata": {}, 438 | "output_type": "execute_result" 439 | } 440 | ], 441 | "source": [ 442 | "l[0] # first" 443 | ] 444 | }, 445 | { 446 | "cell_type": "code", 447 | "execution_count": 17, 448 | "metadata": {}, 449 | "outputs": [ 450 | { 451 | "data": { 452 | "text/plain": [ 453 | "'methane'" 454 | ] 455 | }, 456 | "execution_count": 17, 457 | "metadata": {}, 458 | "output_type": "execute_result" 459 | } 460 | ], 461 | "source": [ 462 | "l[-1] # last" 463 | ] 464 | }, 465 | { 466 | "cell_type": "code", 467 | "execution_count": 18, 468 | "metadata": {}, 469 | "outputs": [ 470 | { 471 | "data": { 472 | "text/plain": [ 473 | "['electricity', 'hydrogen']" 474 | ] 475 | }, 476 | "execution_count": 18, 477 | "metadata": {}, 478 | "output_type": "execute_result" 479 | } 480 | ], 481 | "source": [ 482 | "l[:2] # first two" 483 | ] 484 | }, 485 | { 486 | "cell_type": "code", 487 | "execution_count": 19, 488 | "metadata": {}, 489 | "outputs": [ 490 | { 491 | "data": { 492 | "text/plain": [ 493 | "['electricity', 'methane']" 494 | ] 495 | }, 496 | "execution_count": 19, 497 | "metadata": {}, 498 | "output_type": "execute_result" 499 | } 500 | ], 501 | "source": [ 502 | "l[::2] # every other" 503 | ] 504 | }, 505 | { 506 | "cell_type": "markdown", 507 | "metadata": {}, 508 | "source": [ 509 | "## Dictionaries ##\n", 510 | "\n", 511 | "This is another useful data structure. It maps __keys__ to __values__." 512 | ] 513 | }, 514 | { 515 | "cell_type": "code", 516 | "execution_count": 20, 517 | "metadata": {}, 518 | "outputs": [], 519 | "source": [ 520 | "d = {\n", 521 | " \"name\": \"Reuter West\",\n", 522 | " \"capacity\": 564,\n", 523 | " \"fuel\": \"hard coal\",\n", 524 | "}" 525 | ] 526 | }, 527 | { 528 | "cell_type": "code", 529 | "execution_count": 21, 530 | "metadata": {}, 531 | "outputs": [ 532 | { 533 | "data": { 534 | "text/plain": [ 535 | "564" 536 | ] 537 | }, 538 | "execution_count": 21, 539 | "metadata": {}, 540 | "output_type": "execute_result" 541 | } 542 | ], 543 | "source": [ 544 | "# access a value\n", 545 | "d[\"capacity\"]" 546 | ] 547 | }, 548 | { 549 | "cell_type": "code", 550 | "execution_count": 22, 551 | "metadata": {}, 552 | "outputs": [ 553 | { 554 | "data": { 555 | "text/plain": [ 556 | "True" 557 | ] 558 | }, 559 | "execution_count": 22, 560 | "metadata": {}, 561 | "output_type": "execute_result" 562 | } 563 | ], 564 | "source": [ 565 | "# test for the presence of a key\n", 566 | "\"fuel\" in d" 567 | ] 568 | }, 569 | { 570 | "cell_type": "code", 571 | "execution_count": 23, 572 | "metadata": {}, 573 | "outputs": [ 574 | { 575 | "data": { 576 | "text/plain": [ 577 | "{'name': 'Reuter West',\n", 578 | " 'capacity': 564,\n", 579 | " 'fuel': 'hard coal',\n", 580 | " 'technology': 'CHP'}" 581 | ] 582 | }, 583 | "execution_count": 23, 584 | "metadata": {}, 585 | "output_type": "execute_result" 586 | } 587 | ], 588 | "source": [ 589 | "# add a new key\n", 590 | "d[\"technology\"] = \"CHP\"\n", 591 | "d" 592 | ] 593 | }, 594 | { 595 | "cell_type": "markdown", 596 | "metadata": {}, 597 | "source": [ 598 | "Now we have the building blocks we need to do basic programming in Python." 599 | ] 600 | }, 601 | { 602 | "cell_type": "markdown", 603 | "metadata": { 604 | "tags": [] 605 | }, 606 | "source": [ 607 | "## Exercises" 608 | ] 609 | }, 610 | { 611 | "cell_type": "markdown", 612 | "metadata": {}, 613 | "source": [ 614 | "**Task 1:** What is 5 to the power of 5?" 615 | ] 616 | }, 617 | { 618 | "cell_type": "code", 619 | "execution_count": 24, 620 | "metadata": { 621 | "tags": [ 622 | "hide-cell" 623 | ] 624 | }, 625 | "outputs": [ 626 | { 627 | "data": { 628 | "text/plain": [ 629 | "3125" 630 | ] 631 | }, 632 | "execution_count": 24, 633 | "metadata": {}, 634 | "output_type": "execute_result" 635 | } 636 | ], 637 | "source": [ 638 | "5**5" 639 | ] 640 | }, 641 | { 642 | "cell_type": "markdown", 643 | "metadata": {}, 644 | "source": [ 645 | "**Task 2:** Create a list with the names of every planet in the solar system (in order). Have Python tell you how many planets there are in the list." 646 | ] 647 | }, 648 | { 649 | "cell_type": "code", 650 | "execution_count": 25, 651 | "metadata": { 652 | "tags": [ 653 | "hide-cell" 654 | ] 655 | }, 656 | "outputs": [], 657 | "source": [ 658 | "planets = [\n", 659 | " \"Mercury\",\n", 660 | " \"Venus\",\n", 661 | " \"Earth\",\n", 662 | " \"Mars\",\n", 663 | " \"Jupyter\",\n", 664 | " \"Saturn\",\n", 665 | " \"Uranus\",\n", 666 | " \"Neptune\",\n", 667 | "]" 668 | ] 669 | }, 670 | { 671 | "cell_type": "markdown", 672 | "metadata": {}, 673 | "source": [ 674 | "**Task 3:** Create a dictionary that contains the main facts about the following\n", 675 | "power plant in Berlin. Use this dictionary to access the main fuel type.\n", 676 | "\n", 677 | "> https://powerplants.vattenfall.com/reuter-west/" 678 | ] 679 | }, 680 | { 681 | "cell_type": "code", 682 | "execution_count": 26, 683 | "metadata": { 684 | "tags": [ 685 | "hide-cell" 686 | ] 687 | }, 688 | "outputs": [], 689 | "source": [ 690 | "rw = {\n", 691 | " \"Country\": \"Germany\",\n", 692 | " \"Electricity Capacity\": 564,\n", 693 | " \"Heat Capacity\": 878,\n", 694 | " \"Technology\": \"Combined heat and power (CHP)\",\n", 695 | " \"Main Fuel\": \"Hard coal\",\n", 696 | " \"Vattenfall ownership share\": \"100%\",\n", 697 | " \"Status\": \"In Operation\",\n", 698 | "}" 699 | ] 700 | }, 701 | { 702 | "cell_type": "code", 703 | "execution_count": 27, 704 | "metadata": { 705 | "tags": [ 706 | "hide-cell" 707 | ] 708 | }, 709 | "outputs": [ 710 | { 711 | "data": { 712 | "text/plain": [ 713 | "'Hard coal'" 714 | ] 715 | }, 716 | "execution_count": 27, 717 | "metadata": {}, 718 | "output_type": "execute_result" 719 | } 720 | ], 721 | "source": [ 722 | "rw[\"Main Fuel\"]" 723 | ] 724 | }, 725 | { 726 | "cell_type": "code", 727 | "execution_count": null, 728 | "metadata": {}, 729 | "outputs": [], 730 | "source": [] 731 | } 732 | ], 733 | "metadata": { 734 | "kernelspec": { 735 | "display_name": "sommerakademie-oxford (3.12.7)", 736 | "language": "python", 737 | "name": "python3" 738 | }, 739 | "language_info": { 740 | "codemirror_mode": { 741 | "name": "ipython", 742 | "version": 3 743 | }, 744 | "file_extension": ".py", 745 | "mimetype": "text/x-python", 746 | "name": "python", 747 | "nbconvert_exporter": "python", 748 | "pygments_lexer": "ipython3", 749 | "version": "3.12.7" 750 | } 751 | }, 752 | "nbformat": 4, 753 | "nbformat_minor": 4 754 | } 755 | -------------------------------------------------------------------------------- /sommerakademie-oxford/D-exploration-green-fuels.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "b6bb4613", 6 | "metadata": {}, 7 | "source": [ 8 | "# Models for E-Fuel Production" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "id": "92dfdc27", 14 | "metadata": {}, 15 | "source": [ 16 | "This section builds the foundation of a simple model to assess the economics of islanded methanol production in a region of choice.\n", 17 | "The model can optimise investment and operation of wind and solar, electrolysers, turbine, hydrogen and battery storage, direct air capture, CO$_2$ storage, methanolisation and methanol stores to supply a constant methanol demand of 1 TWh/a.\n", 18 | "\n", 19 | "![Methanol production plant](_assets/fuel-plant.jpeg)\n", 20 | "\n", 21 | "## Model Foundation" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "id": "d376ffa5", 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "import pandas as pd\n", 32 | "\n", 33 | "import pypsa\n", 34 | "\n", 35 | "def annuity(r, n):\n", 36 | " return r / (1.0 - 1.0 / (1.0 + r) ** n)\n" 37 | ] 38 | }, 39 | { 40 | "cell_type": "markdown", 41 | "id": "9eba42de", 42 | "metadata": {}, 43 | "source": [ 44 | "### Cost Assumptions" 45 | ] 46 | }, 47 | { 48 | "cell_type": "markdown", 49 | "id": "aec2d62f", 50 | "metadata": {}, 51 | "source": [ 52 | "We take techno-economic assumptions from the [technology-data](https://github.com/PyPSA/technology-data) repository which collects assumptions on costs and efficiencies:" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": null, 58 | "id": "7bb2ba76", 59 | "metadata": {}, 60 | "outputs": [], 61 | "source": [ 62 | "YEAR = 2030\n", 63 | "url = f\"https://raw.githubusercontent.com/PyPSA/technology-data/master/outputs/costs_{YEAR}.csv\"\n", 64 | "costs = pd.read_csv(url, index_col=[0, 1])\n", 65 | "costs.loc[costs.unit.str.contains(\"/kW\"), \"value\"] *= 1e3\n", 66 | "costs = costs.value.unstack().fillna({\"discount rate\": 0.07, \"lifetime\": 20, \"FOM\": 0})" 67 | ] 68 | }, 69 | { 70 | "cell_type": "markdown", 71 | "id": "fb58c31d", 72 | "metadata": {}, 73 | "source": [ 74 | "We calculate the capital costs (i.e. annualised investment costs, €/MW/a or €/MWh/a for storage), using the discount rate and lifetime." 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": null, 80 | "id": "388bea76", 81 | "metadata": {}, 82 | "outputs": [], 83 | "source": [ 84 | "a = costs.apply(lambda x: annuity(x[\"discount rate\"], x[\"lifetime\"]), axis=1)\n", 85 | "costs[\"capital_cost\"] = (a + costs[\"FOM\"] / 100) * costs[\"investment\"]" 86 | ] 87 | }, 88 | { 89 | "cell_type": "markdown", 90 | "id": "55fae9cd", 91 | "metadata": {}, 92 | "source": [ 93 | "### Time Series" 94 | ] 95 | }, 96 | { 97 | "cell_type": "markdown", 98 | "id": "9ec2343e", 99 | "metadata": {}, 100 | "source": [ 101 | "The wind and solar capacity factor time series have been retrieved from [model.energy](https://model.energy). Go there to find more time series for other countries and plug them in here." 102 | ] 103 | }, 104 | { 105 | "cell_type": "code", 106 | "execution_count": null, 107 | "id": "d746edd8", 108 | "metadata": {}, 109 | "outputs": [], 110 | "source": [ 111 | "RESOLUTION = 3 # hours\n", 112 | "url = \"https://model.energy/data/time-series-ca2bcb9e843aeb286cd6295854c885b6.csv\" # South of Argentina\n", 113 | "ts = pd.read_csv(url, index_col=0, parse_dates=True)[::RESOLUTION]#\n", 114 | "ts.head(3)" 115 | ] 116 | }, 117 | { 118 | "cell_type": "markdown", 119 | "id": "c08fa0cc", 120 | "metadata": {}, 121 | "source": [ 122 | "### Model Initialisation\n", 123 | "\n", 124 | "Add buses, carriers and set snapshots." 125 | ] 126 | }, 127 | { 128 | "cell_type": "code", 129 | "execution_count": null, 130 | "id": "05b4892d", 131 | "metadata": {}, 132 | "outputs": [], 133 | "source": [ 134 | "n = pypsa.Network()\n", 135 | "for carrier in [\"electricity\", \"hydrogen\", \"co2\", \"methanol\"]:\n", 136 | " n.add(\"Bus\", carrier, carrier=carrier, unit=\"t/h\" if carrier == \"co2\" else \"MW\")\n", 137 | "n.set_snapshots(ts.index)\n", 138 | "n.snapshot_weightings.loc[:, :] = RESOLUTION" 139 | ] 140 | }, 141 | { 142 | "cell_type": "code", 143 | "execution_count": null, 144 | "id": "a3a06a0c", 145 | "metadata": {}, 146 | "outputs": [], 147 | "source": [ 148 | "carriers = {\n", 149 | " \"wind\": \"dodgerblue\",\n", 150 | " \"solar\": \"gold\",\n", 151 | " \"hydrogen storage\": \"blueviolet\",\n", 152 | " \"battery storage 3h\": \"yellowgreen\",\n", 153 | " \"battery storage 6h\": \"yellowgreen\",\n", 154 | " \"electrolysis\": \"magenta\",\n", 155 | " \"turbine\": \"darkorange\",\n", 156 | " \"methanolisation\": \"cyan\",\n", 157 | " \"direct air capture\": \"coral\",\n", 158 | " \"co2 storage\": \"black\",\n", 159 | " \"methanol storage\": \"cadetblue\",\n", 160 | " \"electricity\": \"grey\",\n", 161 | " \"hydrogen\": \"grey\",\n", 162 | " \"co2\": \"grey\",\n", 163 | " \"methanol\": \"grey\",\n", 164 | "}\n", 165 | "n.add(\"Carrier\", carriers.keys(), color=carriers.values());" 166 | ] 167 | }, 168 | { 169 | "cell_type": "markdown", 170 | "id": "2842db5f", 171 | "metadata": {}, 172 | "source": [ 173 | "### Demand\n", 174 | "\n", 175 | "Add a constant methanol demand adding up to an annual target production of 1 TWh of methanol:" 176 | ] 177 | }, 178 | { 179 | "cell_type": "code", 180 | "execution_count": null, 181 | "id": "fe0de406", 182 | "metadata": {}, 183 | "outputs": [], 184 | "source": [ 185 | "n.add(\n", 186 | " \"Load\",\n", 187 | " \"demand\",\n", 188 | " bus=\"methanol\",\n", 189 | " p_set=1e6 / 8760,\n", 190 | ");" 191 | ] 192 | }, 193 | { 194 | "cell_type": "markdown", 195 | "id": "f7c49ed4", 196 | "metadata": {}, 197 | "source": [ 198 | "### Wind and Solar" 199 | ] 200 | }, 201 | { 202 | "cell_type": "markdown", 203 | "id": "90c62220", 204 | "metadata": {}, 205 | "source": [ 206 | "Add the wind and solar generators:" 207 | ] 208 | }, 209 | { 210 | "cell_type": "code", 211 | "execution_count": null, 212 | "id": "41bd757e", 213 | "metadata": {}, 214 | "outputs": [], 215 | "source": [ 216 | "n.add(\n", 217 | " \"Generator\",\n", 218 | " \"wind\",\n", 219 | " bus=\"electricity\",\n", 220 | " carrier=\"wind\",\n", 221 | " p_max_pu=ts.onwind,\n", 222 | " capital_cost=costs.at[\"onwind\", \"capital_cost\"],\n", 223 | " p_nom_extendable=True,\n", 224 | ");" 225 | ] 226 | }, 227 | { 228 | "cell_type": "code", 229 | "execution_count": null, 230 | "id": "1516dd7d", 231 | "metadata": {}, 232 | "outputs": [], 233 | "source": [ 234 | "n.add(\n", 235 | " \"Generator\",\n", 236 | " \"solar\",\n", 237 | " bus=\"electricity\",\n", 238 | " carrier=\"solar\",\n", 239 | " p_max_pu=ts.solar,\n", 240 | " capital_cost=costs.at[\"solar\", \"capital_cost\"],\n", 241 | " p_nom_extendable=True,\n", 242 | ");" 243 | ] 244 | }, 245 | { 246 | "cell_type": "markdown", 247 | "id": "1c379d5d", 248 | "metadata": {}, 249 | "source": [ 250 | "### Batteries\n", 251 | "\n", 252 | "Add a 3-hour and a 6-hour battery storage:" 253 | ] 254 | }, 255 | { 256 | "cell_type": "code", 257 | "execution_count": null, 258 | "id": "b0d9c1d8", 259 | "metadata": {}, 260 | "outputs": [], 261 | "source": [ 262 | "for max_hours in [3, 6]:\n", 263 | " n.add(\n", 264 | " \"StorageUnit\",\n", 265 | " f\"battery storage {max_hours}h\",\n", 266 | " bus=\"electricity\",\n", 267 | " carrier=f\"battery storage {max_hours}h\",\n", 268 | " max_hours=max_hours,\n", 269 | " capital_cost=costs.at[\"battery inverter\", \"capital_cost\"]\n", 270 | " + max_hours * costs.at[\"battery storage\", \"capital_cost\"],\n", 271 | " efficiency_store=costs.at[\"battery inverter\", \"efficiency\"],\n", 272 | " efficiency_dispatch=costs.at[\"battery inverter\", \"efficiency\"],\n", 273 | " p_nom_extendable=True,\n", 274 | " cyclic_state_of_charge=True,\n", 275 | " );" 276 | ] 277 | }, 278 | { 279 | "cell_type": "markdown", 280 | "id": "3c8fa0e4", 281 | "metadata": {}, 282 | "source": [ 283 | "### Hydrogen\n", 284 | "\n", 285 | "Add electrolysers, hydrogen storage (steel tank), hydrogen turbine:" 286 | ] 287 | }, 288 | { 289 | "cell_type": "code", 290 | "execution_count": null, 291 | "id": "c126da23", 292 | "metadata": {}, 293 | "outputs": [], 294 | "source": [ 295 | "n.add(\n", 296 | " \"Link\",\n", 297 | " \"electrolysis\",\n", 298 | " bus0=\"electricity\",\n", 299 | " bus1=\"hydrogen\",\n", 300 | " carrier=\"electrolysis\",\n", 301 | " p_nom_extendable=True,\n", 302 | " efficiency=costs.at[\"electrolysis\", \"efficiency\"],\n", 303 | " capital_cost=costs.at[\"electrolysis\", \"capital_cost\"],\n", 304 | ");" 305 | ] 306 | }, 307 | { 308 | "cell_type": "code", 309 | "execution_count": null, 310 | "id": "9ec974b1", 311 | "metadata": {}, 312 | "outputs": [], 313 | "source": [ 314 | "n.add(\n", 315 | " \"Link\",\n", 316 | " \"turbine\",\n", 317 | " bus0=\"hydrogen\",\n", 318 | " bus1=\"electricity\",\n", 319 | " carrier=\"turbine\",\n", 320 | " p_nom_extendable=True,\n", 321 | " efficiency=costs.at[\"OCGT\", \"efficiency\"],\n", 322 | " capital_cost=costs.at[\"OCGT\", \"capital_cost\"] / costs.at[\"OCGT\", \"efficiency\"],\n", 323 | ");" 324 | ] 325 | }, 326 | { 327 | "cell_type": "code", 328 | "execution_count": null, 329 | "id": "1aa93520", 330 | "metadata": {}, 331 | "outputs": [], 332 | "source": [ 333 | "tech = \"hydrogen storage tank type 1 including compressor\"\n", 334 | "\n", 335 | "n.add(\n", 336 | " \"Store\",\n", 337 | " \"hydrogen storage\",\n", 338 | " bus=\"hydrogen\",\n", 339 | " carrier=\"hydrogen storage\",\n", 340 | " capital_cost=costs.at[tech, \"capital_cost\"],\n", 341 | " e_nom_extendable=True,\n", 342 | " e_cyclic=True,\n", 343 | ");" 344 | ] 345 | }, 346 | { 347 | "cell_type": "markdown", 348 | "id": "72a7094c", 349 | "metadata": {}, 350 | "source": [ 351 | "### Carbon Dioxide\n", 352 | "\n", 353 | "Add liquid carbon dioxide storage and direct air capture, assuming for simplicity an electricity demand 2 MWh/tCO2 for heat and electricity needs of the process.\n", 354 | "More detailed modelling would also model the heat supply for the direct air capture." 355 | ] 356 | }, 357 | { 358 | "cell_type": "code", 359 | "execution_count": null, 360 | "id": "490e24f9", 361 | "metadata": {}, 362 | "outputs": [], 363 | "source": [ 364 | "electricity_input = 2 # MWh/tCO2\n", 365 | "\n", 366 | "n.add(\n", 367 | " \"Link\",\n", 368 | " \"direct air capture\",\n", 369 | " bus0=\"electricity\",\n", 370 | " bus1=\"co2\",\n", 371 | " carrier=\"direct air capture\",\n", 372 | " p_nom_extendable=True,\n", 373 | " efficiency=1 / electricity_input,\n", 374 | " capital_cost=costs.at[\"direct air capture\", \"capital_cost\"] / electricity_input,\n", 375 | ");" 376 | ] 377 | }, 378 | { 379 | "cell_type": "code", 380 | "execution_count": null, 381 | "id": "ba0f7aec", 382 | "metadata": {}, 383 | "outputs": [], 384 | "source": [ 385 | "n.add(\n", 386 | " \"Store\",\n", 387 | " \"co2 storage\",\n", 388 | " bus=\"co2\",\n", 389 | " carrier=\"co2 storage\",\n", 390 | " capital_cost=costs.at[\"CO2 storage tank\", \"capital_cost\"],\n", 391 | " e_nom_extendable=True,\n", 392 | " e_cyclic=True,\n", 393 | ");" 394 | ] 395 | }, 396 | { 397 | "cell_type": "markdown", 398 | "id": "bdb9f578", 399 | "metadata": {}, 400 | "source": [ 401 | "### Methanol\n", 402 | "\n", 403 | "Add methanolisation unit, which takes hydrogen, electricity and carbon dioxide as input, and methanol storage.\n", 404 | "Efficiencies and capital costs need to be expressed in units of `bus0`." 405 | ] 406 | }, 407 | { 408 | "cell_type": "code", 409 | "execution_count": null, 410 | "id": "438264e2", 411 | "metadata": {}, 412 | "outputs": [], 413 | "source": [ 414 | "eff_h2 = 1 / costs.at[\"methanolisation\", \"hydrogen-input\"]\n", 415 | "\n", 416 | "n.add(\n", 417 | " \"Link\",\n", 418 | " \"methanolisation\",\n", 419 | " bus0=\"hydrogen\",\n", 420 | " bus1=\"methanol\",\n", 421 | " bus2=\"electricity\",\n", 422 | " bus3=\"co2\",\n", 423 | " carrier=\"methanolisation\",\n", 424 | " p_nom_extendable=True,\n", 425 | " capital_cost=costs.at[\"methanolisation\", \"capital_cost\"] * eff_h2,\n", 426 | " efficiency=eff_h2,\n", 427 | " efficiency2=-costs.at[\"methanolisation\", \"electricity-input\"] * eff_h2,\n", 428 | " efficiency3=-costs.at[\"methanolisation\", \"carbondioxide-input\"] * eff_h2,\n", 429 | ");" 430 | ] 431 | }, 432 | { 433 | "cell_type": "markdown", 434 | "id": "c7d4b553", 435 | "metadata": {}, 436 | "source": [ 437 | "Costs for g is given in €/m³. We need to convert it to €/MWh, using the volumetric energy density of methanol (15.6 MJ/L)." 438 | ] 439 | }, 440 | { 441 | "cell_type": "code", 442 | "execution_count": null, 443 | "id": "84c41767", 444 | "metadata": {}, 445 | "outputs": [], 446 | "source": [ 447 | "capital_cost = costs.at[\n", 448 | " \"General liquid hydrocarbon storage (crude)\", \"capital_cost\"\n", 449 | "] / (15.6 * 1000 / 3600)\n", 450 | "\n", 451 | "n.add(\n", 452 | " \"Store\",\n", 453 | " \"methanol storage\",\n", 454 | " bus=\"co2\",\n", 455 | " carrier=\"methanol storage\",\n", 456 | " capital_cost=capital_cost,\n", 457 | " e_nom_extendable=True,\n", 458 | " e_cyclic=True,\n", 459 | ");" 460 | ] 461 | }, 462 | { 463 | "cell_type": "markdown", 464 | "id": "dfafa487", 465 | "metadata": {}, 466 | "source": [ 467 | "## Exploration\n", 468 | "\n", 469 | "We could now already run the optimisation and look at a first set of results." 470 | ] 471 | }, 472 | { 473 | "cell_type": "code", 474 | "execution_count": null, 475 | "id": "6d830761", 476 | "metadata": {}, 477 | "outputs": [], 478 | "source": [ 479 | "# n.optimize(solver_name=\"highs\", log_to_console=False)" 480 | ] 481 | }, 482 | { 483 | "cell_type": "markdown", 484 | "id": "99dc8835", 485 | "metadata": {}, 486 | "source": [ 487 | "But the exploration is left to you! Here are some ideas for sensitivities to explore and metrics to look at:\n", 488 | "\n", 489 | "**Metrics:**\n", 490 | "\n", 491 | "- Cost per unit of methanol produced (€/MWh)\n", 492 | "- Installed capacities of wind, solar, electrolysers, hydrogen storage, methanol storage (MW or MWh)\n", 493 | "- Cost breakdown by technology (million €/a and % of total cost)\n", 494 | "- Operational patterns of key technologies (e.g. electrolysers, hydrogen turbine, methanolisation)\n", 495 | "- Energy balances of different carrier as time series (e.g. hydrogen, methanol, electricity)\n", 496 | "\n", 497 | "**Sensitivities:**\n", 498 | "\n", 499 | "- Choose different **locations** for wind and solar time series (e.g. Namibia, Argentina, Australia, Chile, Germany, UK, Morocco, etc.). Take these from [model.energy](https://model.energy).\n", 500 | "- Vary the cost assumptions for key technologies (e.g. electrolysers, direct air capture, methanolisation) or the overall projection year (e.g. 2030, 2040, 2050). This can be done when loading the cost data.\n", 501 | "- Vary the weighted average **cost of capital** (WACC). This can be done by changing the discount rate when loading the cost data.\n", 502 | "- Limit the **availability** of certain technologies (e.g. no hydrogen turbine, no battery storage, underground storage instead of steel tank storage) or their **flexibility** (e.g. by requiring a minimum part load for the electrolyser). This can be done by setting `p_min_pu` for the electrolyser. For instance, with `n.links.loc[\"electrolyser\", \"p_min_pu\"] = 0.5` for a minimum part load of 50%.\n", 503 | "- Add the option to use **CO$_2$ from biomass** in addition to direct air capture (e.g. at a cost of 50 €/tCO$_2$). This can be done by adding a generator to the `\"co2\"` bus with fixed `marginal_cost=50`.\n", 504 | "\n", 505 | "**Feel free to explore other ideas as well!**" 506 | ] 507 | }, 508 | { 509 | "cell_type": "markdown", 510 | "id": "52b58765", 511 | "metadata": {}, 512 | "source": [] 513 | } 514 | ], 515 | "metadata": { 516 | "kernelspec": { 517 | "display_name": "sommerakademie-oxford (3.12.7)", 518 | "language": "python", 519 | "name": "python3" 520 | }, 521 | "language_info": { 522 | "codemirror_mode": { 523 | "name": "ipython", 524 | "version": 3 525 | }, 526 | "file_extension": ".py", 527 | "mimetype": "text/x-python", 528 | "name": "python", 529 | "nbconvert_exporter": "python", 530 | "pygments_lexer": "ipython3", 531 | "version": "3.12.7" 532 | } 533 | }, 534 | "nbformat": 4, 535 | "nbformat_minor": 5 536 | } 537 | -------------------------------------------------------------------------------- /sommerakademie-oxford/B-exploration-networks.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "0324e4ca", 6 | "metadata": {}, 7 | "source": [ 8 | "# Models with Networks" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "id": "cb485fd5", 14 | "metadata": {}, 15 | "source": [ 16 | "This section builds the foundation of an electricity and hydrogen model with power lines and pipelines connecting different countries. The goal is to assess different balancing mechanisms for the large-scale integration of variable renewables and identifying trade-offs between balancing them spatially with networks or temporally with storage.\n", 17 | "\n", 18 | "![network-model](_assets/networks.png)\n", 19 | "\n", 20 | "## Model Foundation" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": null, 26 | "id": "0227b4dd", 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "import pandas as pd\n", 31 | "\n", 32 | "import pypsa\n", 33 | "\n", 34 | "RESOLUTION = 3 # 3-hourly\n", 35 | "SOLVER = \"highs\" # or 'gurobi'" 36 | ] 37 | }, 38 | { 39 | "cell_type": "markdown", 40 | "id": "f8439e9d", 41 | "metadata": {}, 42 | "source": [ 43 | "### Cost Assumptions" 44 | ] 45 | }, 46 | { 47 | "cell_type": "markdown", 48 | "id": "d3bfb4bf", 49 | "metadata": {}, 50 | "source": [ 51 | "We take techno-economic assumptions from the [technology-data](https://github.com/PyPSA/technology-data) repository which collects assumptions on costs and efficiencies:" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": 7, 57 | "id": "25591c68", 58 | "metadata": {}, 59 | "outputs": [], 60 | "source": [ 61 | "YEAR = 2040\n", 62 | "url = f\"https://raw.githubusercontent.com/PyPSA/technology-data/master/outputs/costs_{YEAR}.csv\"\n", 63 | "costs = pd.read_csv(url, index_col=[0, 1])\n", 64 | "costs.loc[costs.unit.str.contains(\"/kW\"), \"value\"] *= 1e3\n", 65 | "costs = costs.value.unstack().fillna({\"discount rate\": 0.07, \"lifetime\": 20, \"FOM\": 0})" 66 | ] 67 | }, 68 | { 69 | "cell_type": "markdown", 70 | "id": "27b18fa0", 71 | "metadata": {}, 72 | "source": [ 73 | "We calculate the capital costs (i.e. annualised investment costs, €/MW/a or €/MWh/a for storage), using the discount rate and lifetime." 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": null, 79 | "id": "2dac6dfc", 80 | "metadata": {}, 81 | "outputs": [], 82 | "source": [ 83 | "def annuity(r, n):\n", 84 | " return r / (1.0 - 1.0 / (1.0 + r) ** n)\n", 85 | "\n", 86 | "a = costs.apply(lambda x: annuity(x[\"discount rate\"], x[\"lifetime\"]), axis=1)\n", 87 | "costs[\"capital_cost\"] = (a + costs[\"FOM\"] / 100) * costs[\"investment\"]" 88 | ] 89 | }, 90 | { 91 | "cell_type": "markdown", 92 | "id": "1d5ea5e8", 93 | "metadata": {}, 94 | "source": [ 95 | "### Time Series" 96 | ] 97 | }, 98 | { 99 | "cell_type": "markdown", 100 | "id": "03a76fa9", 101 | "metadata": {}, 102 | "source": [ 103 | "The wind and solar capacity factor time series have been retrieved from [model.energy](https://model.energy). Go there to find more time series for other countries and plug them in here." 104 | ] 105 | }, 106 | { 107 | "cell_type": "code", 108 | "execution_count": 9, 109 | "id": "f42cd8d1", 110 | "metadata": {}, 111 | "outputs": [ 112 | { 113 | "data": { 114 | "text/html": [ 115 | "
\n", 116 | "\n", 129 | "\n", 130 | " \n", 131 | " \n", 132 | " \n", 133 | " \n", 134 | " \n", 135 | " \n", 136 | " \n", 137 | " \n", 138 | " \n", 139 | " \n", 140 | " \n", 141 | " \n", 142 | " \n", 143 | " \n", 144 | " \n", 145 | " \n", 146 | " \n", 147 | " \n", 148 | " \n", 149 | " \n", 150 | " \n", 151 | " \n", 152 | " \n", 153 | " \n", 154 | " \n", 155 | " \n", 156 | " \n", 157 | " \n", 158 | " \n", 159 | " \n", 160 | " \n", 161 | " \n", 162 | " \n", 163 | " \n", 164 | " \n", 165 | " \n", 166 | " \n", 167 | " \n", 168 | " \n", 169 | " \n", 170 | " \n", 171 | " \n", 172 | " \n", 173 | " \n", 174 | " \n", 175 | " \n", 176 | "
GermanySpainUnited Kingdom
onwindsolaronwindsolaronwindsolar
2013-01-01 00:00:000.8480.00.2360.00.6050.0
2013-01-01 01:00:000.8460.00.2260.00.6080.0
2013-01-01 02:00:000.8310.00.2190.00.6240.0
\n", 177 | "
" 178 | ], 179 | "text/plain": [ 180 | " Germany Spain United Kingdom \n", 181 | " onwind solar onwind solar onwind solar\n", 182 | "2013-01-01 00:00:00 0.848 0.0 0.236 0.0 0.605 0.0\n", 183 | "2013-01-01 01:00:00 0.846 0.0 0.226 0.0 0.608 0.0\n", 184 | "2013-01-01 02:00:00 0.831 0.0 0.219 0.0 0.624 0.0" 185 | ] 186 | }, 187 | "execution_count": 9, 188 | "metadata": {}, 189 | "output_type": "execute_result" 190 | } 191 | ], 192 | "source": [ 193 | "url = {\n", 194 | " \"Germany\": \"https://model.energy/data/time-series-dd07c6bb61a102ba3399f062230a24fc.csv\",\n", 195 | " \"Spain\": \"https://model.energy/data/time-series-bb901ba96566f929463616e013936637.csv\",\n", 196 | " \"United Kingdom\": \"https://model.energy/data/time-series-71c7fcaf08ed1eff68e14fdf0f28f738.csv\",\n", 197 | "}\n", 198 | "ts = pd.concat(\n", 199 | " {c: pd.read_csv(url[c], index_col=0, parse_dates=True) for c in url},\n", 200 | " axis=1,\n", 201 | ")\n", 202 | "ts.head(3)" 203 | ] 204 | }, 205 | { 206 | "cell_type": "markdown", 207 | "id": "51a6ccf8", 208 | "metadata": {}, 209 | "source": [ 210 | "The demand time series are exported from the [ENTSO-E Transparency Platform](https://transparency.entsoe.eu/)." 211 | ] 212 | }, 213 | { 214 | "cell_type": "code", 215 | "execution_count": 10, 216 | "id": "2cc89661", 217 | "metadata": {}, 218 | "outputs": [ 219 | { 220 | "data": { 221 | "text/html": [ 222 | "
\n", 223 | "\n", 236 | "\n", 237 | " \n", 238 | " \n", 239 | " \n", 240 | " \n", 241 | " \n", 242 | " \n", 243 | " \n", 244 | " \n", 245 | " \n", 246 | " \n", 247 | " \n", 248 | " \n", 249 | " \n", 250 | " \n", 251 | " \n", 252 | " \n", 253 | " \n", 254 | " \n", 255 | " \n", 256 | " \n", 257 | " \n", 258 | " \n", 259 | " \n", 260 | " \n", 261 | " \n", 262 | " \n", 263 | " \n", 264 | " \n", 265 | " \n", 266 | " \n", 267 | " \n", 268 | " \n", 269 | " \n", 270 | " \n", 271 | " \n", 272 | " \n", 273 | " \n", 274 | " \n", 275 | " \n", 276 | " \n", 277 | " \n", 278 | " \n", 279 | " \n", 280 | " \n", 281 | " \n", 282 | " \n", 283 | " \n", 284 | " \n", 285 | " \n", 286 | " \n", 287 | " \n", 288 | " \n", 289 | " \n", 290 | " \n", 291 | " \n", 292 | " \n", 293 | " \n", 294 | " \n", 295 | " \n", 296 | " \n", 297 | " \n", 298 | " \n", 299 | " \n", 300 | " \n", 301 | " \n", 302 | " \n", 303 | " \n", 304 | " \n", 305 | " \n", 306 | " \n", 307 | " \n", 308 | " \n", 309 | " \n", 310 | " \n", 311 | " \n", 312 | " \n", 313 | " \n", 314 | " \n", 315 | " \n", 316 | " \n", 317 | " \n", 318 | " \n", 319 | " \n", 320 | " \n", 321 | " \n", 322 | " \n", 323 | " \n", 324 | " \n", 325 | " \n", 326 | " \n", 327 | " \n", 328 | " \n", 329 | " \n", 330 | " \n", 331 | " \n", 332 | " \n", 333 | " \n", 334 | " \n", 335 | " \n", 336 | " \n", 337 | "
ALATBABEBGCHCZGermanyDKEE...NOPLPTRORSSESISKUAXK
2013-01-01 00:00:00845.1724146376.01186.9135808096.04388.88895995.05618.038617.58243339.0844.0...14741.013134.04750.05779.05580.014610.01161.02785.015823.0992.000000
2013-01-01 01:00:00794.0689666112.01104.7530867607.04166.66675790.05574.037058.24183118.0812.0...14555.012475.04557.05560.05288.014343.01110.02658.015357.0940.088889
2013-01-01 02:00:00741.0000005785.01041.6049387218.03985.85865448.05451.035576.92312970.0788.0...14389.011994.04262.05403.04963.014117.01090.02571.015215.0882.311111
\n", 338 | "

3 rows × 36 columns

\n", 339 | "
" 340 | ], 341 | "text/plain": [ 342 | " AL AT BA BE BG \\\n", 343 | "2013-01-01 00:00:00 845.172414 6376.0 1186.913580 8096.0 4388.8889 \n", 344 | "2013-01-01 01:00:00 794.068966 6112.0 1104.753086 7607.0 4166.6667 \n", 345 | "2013-01-01 02:00:00 741.000000 5785.0 1041.604938 7218.0 3985.8586 \n", 346 | "\n", 347 | " CH CZ Germany DK EE ... NO \\\n", 348 | "2013-01-01 00:00:00 5995.0 5618.0 38617.5824 3339.0 844.0 ... 14741.0 \n", 349 | "2013-01-01 01:00:00 5790.0 5574.0 37058.2418 3118.0 812.0 ... 14555.0 \n", 350 | "2013-01-01 02:00:00 5448.0 5451.0 35576.9231 2970.0 788.0 ... 14389.0 \n", 351 | "\n", 352 | " PL PT RO RS SE SI SK \\\n", 353 | "2013-01-01 00:00:00 13134.0 4750.0 5779.0 5580.0 14610.0 1161.0 2785.0 \n", 354 | "2013-01-01 01:00:00 12475.0 4557.0 5560.0 5288.0 14343.0 1110.0 2658.0 \n", 355 | "2013-01-01 02:00:00 11994.0 4262.0 5403.0 4963.0 14117.0 1090.0 2571.0 \n", 356 | "\n", 357 | " UA XK \n", 358 | "2013-01-01 00:00:00 15823.0 992.000000 \n", 359 | "2013-01-01 01:00:00 15357.0 940.088889 \n", 360 | "2013-01-01 02:00:00 15215.0 882.311111 \n", 361 | "\n", 362 | "[3 rows x 36 columns]" 363 | ] 364 | }, 365 | "execution_count": 10, 366 | "metadata": {}, 367 | "output_type": "execute_result" 368 | } 369 | ], 370 | "source": [ 371 | "url = \"https://tubcloud.tu-berlin.de/s/5ZcfnfC7mEwEGWt/download/electricity-demand.csv\"\n", 372 | "demand = pd.read_csv(url, index_col=0, parse_dates=True)\n", 373 | "demand.rename(\n", 374 | " columns={\"DE\": \"Germany\", \"ES\": \"Spain\", \"GB\": \"United Kingdom\"}, inplace=True\n", 375 | ")\n", 376 | "demand.head(3)" 377 | ] 378 | }, 379 | { 380 | "cell_type": "markdown", 381 | "id": "ef4ae053", 382 | "metadata": {}, 383 | "source": [ 384 | "### Electricity Components" 385 | ] 386 | }, 387 | { 388 | "cell_type": "markdown", 389 | "id": "64c3d8e3", 390 | "metadata": {}, 391 | "source": [ 392 | "In a fresh PyPSA network instance, we set the coordinates of each country as the coordinates of the electricity buses." 393 | ] 394 | }, 395 | { 396 | "cell_type": "code", 397 | "execution_count": 11, 398 | "id": "4b5942be", 399 | "metadata": {}, 400 | "outputs": [], 401 | "source": [ 402 | "n = pypsa.Network()\n", 403 | "\n", 404 | "REGIONS = pd.Index([\"Germany\", \"Spain\", \"United Kingdom\"])\n", 405 | "LAT = [51.17, 40.42, 54.77]\n", 406 | "LON = [10.45, -3.70, -2.02]\n", 407 | "n.add(\"Bus\", REGIONS, x=LON, y=LAT, carrier=\"AC\", unit=\"MW\", location=REGIONS)\n", 408 | "n.set_snapshots(demand.index)" 409 | ] 410 | }, 411 | { 412 | "cell_type": "markdown", 413 | "id": "e3885c6f", 414 | "metadata": {}, 415 | "source": [ 416 | "We also define a range of carriers (i.e. energy carriers or technologies) that we will use in the model. This will be useful later when plotting optimisation results." 417 | ] 418 | }, 419 | { 420 | "cell_type": "code", 421 | "execution_count": 12, 422 | "id": "d8e9ed07", 423 | "metadata": {}, 424 | "outputs": [], 425 | "source": [ 426 | "CARRIERS = {\n", 427 | " \"solar\": \"gold\",\n", 428 | " \"wind\": \"steelblue\",\n", 429 | " \"load shedding\": \"indianred\",\n", 430 | " \"battery charger\": \"lightgreen\",\n", 431 | " \"battery discharger\": \"lightgreen\",\n", 432 | " \"battery storage\": \"grey\",\n", 433 | " \"battery\": \"grey\",\n", 434 | " \"electrolysis\": \"violet\",\n", 435 | " \"turbine\": \"orange\",\n", 436 | " \"hydrogen storage\": \"orchid\",\n", 437 | " \"hydrogen\": \"orchid\",\n", 438 | " \"hydrogen load\": \"magenta\",\n", 439 | " \"hydrogen import\": \"black\",\n", 440 | " \"AC\": \"black\",\n", 441 | " \"HVDC\": \"lightseagreen\",\n", 442 | " \"pipeline\": \"cyan\",\n", 443 | " \"load\": \"slategrey\",\n", 444 | "}\n", 445 | "\n", 446 | "n.add(\n", 447 | " \"Carrier\",\n", 448 | " CARRIERS.keys(),\n", 449 | " color=CARRIERS.values(),\n", 450 | ");" 451 | ] 452 | }, 453 | { 454 | "cell_type": "markdown", 455 | "id": "f161f395", 456 | "metadata": {}, 457 | "source": [ 458 | "Then, we add the electricity demand time series for each region:" 459 | ] 460 | }, 461 | { 462 | "cell_type": "code", 463 | "execution_count": 13, 464 | "id": "7895166a", 465 | "metadata": {}, 466 | "outputs": [], 467 | "source": [ 468 | "n.add(\n", 469 | " \"Load\", REGIONS, suffix=\" load\", bus=REGIONS, p_set=demand[REGIONS], carrier=\"load\"\n", 470 | ");" 471 | ] 472 | }, 473 | { 474 | "cell_type": "markdown", 475 | "id": "549932f8", 476 | "metadata": {}, 477 | "source": [ 478 | "For the wind and solar generators, we pass the capacity factors as `p_max_pu` and add the annuitized capital costs.\n", 479 | "\n", 480 | "As a trick for modelling curtailment, we add a small `marginal_cost` to the wind generators to nudge those to be curtailed first before solar." 481 | ] 482 | }, 483 | { 484 | "cell_type": "code", 485 | "execution_count": 14, 486 | "id": "940aa248", 487 | "metadata": {}, 488 | "outputs": [], 489 | "source": [ 490 | "p_max_pu_wind = ts.xs(\"onwind\", level=1, axis=1).rename(columns=lambda s: s + \" wind\")\n", 491 | "\n", 492 | "n.add(\n", 493 | " \"Generator\",\n", 494 | " p_max_pu_wind.columns,\n", 495 | " bus=REGIONS,\n", 496 | " p_max_pu=p_max_pu_wind,\n", 497 | " p_nom_extendable=True,\n", 498 | " capital_cost=costs.at[\"onwind\", \"capital_cost\"],\n", 499 | " marginal_cost=0.5,\n", 500 | " carrier=\"wind\",\n", 501 | ")\n", 502 | "\n", 503 | "p_max_pu_solar = ts.xs(\"solar\", level=1, axis=1).rename(columns=lambda s: s + \" solar\")\n", 504 | "\n", 505 | "n.add(\n", 506 | " \"Generator\",\n", 507 | " p_max_pu_solar.columns,\n", 508 | " bus=REGIONS,\n", 509 | " p_max_pu=p_max_pu_solar,\n", 510 | " p_nom_extendable=True,\n", 511 | " capital_cost=costs.at[\"solar-utility\", \"capital_cost\"],\n", 512 | " carrier=\"solar\",\n", 513 | ");" 514 | ] 515 | }, 516 | { 517 | "cell_type": "markdown", 518 | "id": "33d3cb69", 519 | "metadata": {}, 520 | "source": [ 521 | "We also add a generator with a very high marginal cost to represent load shedding with a value of lost load (VoLL) of 3000 €/MWh." 522 | ] 523 | }, 524 | { 525 | "cell_type": "code", 526 | "execution_count": 15, 527 | "id": "73434d0c", 528 | "metadata": {}, 529 | "outputs": [], 530 | "source": [ 531 | "n.add(\n", 532 | " \"Generator\",\n", 533 | " REGIONS,\n", 534 | " suffix=\" load shedding\",\n", 535 | " bus=REGIONS,\n", 536 | " p_nom=demand[REGIONS].max(),\n", 537 | " marginal_cost=3000,\n", 538 | " carrier=\"load shedding\",\n", 539 | ");" 540 | ] 541 | }, 542 | { 543 | "cell_type": "markdown", 544 | "id": "36696f4a", 545 | "metadata": {}, 546 | "source": [ 547 | "### Storage" 548 | ] 549 | }, 550 | { 551 | "cell_type": "markdown", 552 | "id": "1ae9727d", 553 | "metadata": {}, 554 | "source": [ 555 | "Now, we add some storage options to each model region:\n", 556 | "\n", 557 | "- For **hydrogen storage**, we model the electrolyser, the hydrogen storage, and the turbine for re-electrification separately, so that these components can be independently sized. We also include some constant hydrogen demand at a rate of half of each regions' average electricity demand.\n", 558 | "\n", 559 | "- For **battery storage**, we separately model the battery inverter (charger and discharger) and the battery storage itself. This has the advantage that the power-to-energy ratio can be optimised." 560 | ] 561 | }, 562 | { 563 | "cell_type": "code", 564 | "execution_count": 16, 565 | "id": "67c9eeb9", 566 | "metadata": {}, 567 | "outputs": [], 568 | "source": [ 569 | "n.add(\"Bus\", REGIONS, suffix=\" hydrogen\", carrier=\"hydrogen\", x=LON, y=LAT, unit=\"MW\", location=REGIONS)\n", 570 | "\n", 571 | "n.add(\n", 572 | " \"Load\",\n", 573 | " REGIONS,\n", 574 | " suffix=\" hydrogen load\",\n", 575 | " bus=REGIONS + \" hydrogen\",\n", 576 | " p_set=demand[REGIONS].mean() / 2,\n", 577 | " carrier=\"hydrogen load\",\n", 578 | ");\n", 579 | "\n", 580 | "n.add(\n", 581 | " \"Link\",\n", 582 | " REGIONS,\n", 583 | " suffix=\" electrolysis\",\n", 584 | " bus0=REGIONS,\n", 585 | " bus1=pd.Index(REGIONS) + \" hydrogen\",\n", 586 | " carrier=\"electrolysis\",\n", 587 | " p_nom_extendable=True,\n", 588 | " efficiency=costs.at[\"electrolysis\", \"efficiency\"],\n", 589 | " capital_cost=costs.at[\"electrolysis\", \"capital_cost\"],\n", 590 | ")\n", 591 | "\n", 592 | "n.add(\n", 593 | " \"Link\",\n", 594 | " REGIONS,\n", 595 | " suffix=\" turbine\",\n", 596 | " bus0=pd.Index(REGIONS) + \" hydrogen\",\n", 597 | " bus1=REGIONS,\n", 598 | " carrier=\"turbine\",\n", 599 | " p_nom_extendable=True,\n", 600 | " efficiency=costs.at[\"OCGT\", \"efficiency\"],\n", 601 | " capital_cost=costs.at[\"OCGT\", \"capital_cost\"],\n", 602 | ")\n", 603 | "\n", 604 | "n.add(\n", 605 | " \"Store\",\n", 606 | " REGIONS,\n", 607 | " suffix=\" hydrogen storage\",\n", 608 | " bus=pd.Index(REGIONS) + \" hydrogen\",\n", 609 | " carrier=\"hydrogen storage\",\n", 610 | " capital_cost=costs.at[\"hydrogen storage underground\", \"capital_cost\"],\n", 611 | " e_nom_extendable=True,\n", 612 | " e_cyclic=True,\n", 613 | ")\n", 614 | "\n", 615 | "n.add(\"Bus\", REGIONS, suffix=\" battery\", carrier=\"battery\", x=LON, y=LAT, unit=\"MW\", location=REGIONS)\n", 616 | "\n", 617 | "n.add(\n", 618 | " \"Link\",\n", 619 | " REGIONS,\n", 620 | " suffix=\" battery charger\",\n", 621 | " bus0=REGIONS,\n", 622 | " bus1=REGIONS + \" battery\",\n", 623 | " carrier=\"battery charger\",\n", 624 | " p_nom_extendable=True,\n", 625 | " efficiency=0.95,\n", 626 | " capital_cost=costs.at[\"battery inverter\", \"capital_cost\"],\n", 627 | ")\n", 628 | "\n", 629 | "n.add(\n", 630 | " \"Link\",\n", 631 | " REGIONS,\n", 632 | " suffix=\" battery discharger\",\n", 633 | " bus0=REGIONS + \" battery\",\n", 634 | " bus1=REGIONS,\n", 635 | " carrier=\"battery discharger\",\n", 636 | " p_nom_extendable=True,\n", 637 | " efficiency=0.95,\n", 638 | ")\n", 639 | "\n", 640 | "n.add(\n", 641 | " \"Store\",\n", 642 | " REGIONS,\n", 643 | " suffix=\" battery storage\",\n", 644 | " bus=REGIONS + \" battery\",\n", 645 | " carrier=\"battery storage\",\n", 646 | " capital_cost=costs.at[\"battery storage\", \"capital_cost\"],\n", 647 | " e_nom_extendable=True,\n", 648 | " e_cyclic=True,\n", 649 | ");" 650 | ] 651 | }, 652 | { 653 | "cell_type": "markdown", 654 | "id": "74b56824", 655 | "metadata": {}, 656 | "source": [ 657 | "For the battery storage, we need to take special care of the inverter. As the same component can be used for charging and discharging, the capacities of our battery charger and discharger component must be linked.\n", 658 | "This is not done automatically by PyPSA, so we have to add an extra constraint, which we can pass as `extra_functionality` to the optimisation." 659 | ] 660 | }, 661 | { 662 | "cell_type": "code", 663 | "execution_count": 17, 664 | "id": "e49d591e", 665 | "metadata": {}, 666 | "outputs": [], 667 | "source": [ 668 | "def battery_constraint(n: pypsa.Network, sns: pd.Index) -> None:\n", 669 | " \"\"\"Constraint to ensure that the nominal capacity of battery chargers and dischargers are in a fixed ratio.\"\"\"\n", 670 | " dischargers_i = n.links[n.links.index.str.contains(\" discharger\")].index\n", 671 | " chargers_i = n.links[n.links.index.str.contains(\" charger\")].index\n", 672 | "\n", 673 | " eff = n.links.efficiency[dischargers_i].values\n", 674 | " lhs = n.model[\"Link-p_nom\"].loc[chargers_i]\n", 675 | " rhs = n.model[\"Link-p_nom\"].loc[dischargers_i] * eff\n", 676 | "\n", 677 | " n.model.add_constraints(lhs == rhs, name=\"Link-charger_ratio\")\n" 678 | ] 679 | }, 680 | { 681 | "cell_type": "markdown", 682 | "id": "f06e86b6", 683 | "metadata": {}, 684 | "source": [ 685 | "### Transmission and Pipelines" 686 | ] 687 | }, 688 | { 689 | "cell_type": "markdown", 690 | "id": "e09ed462", 691 | "metadata": {}, 692 | "source": [ 693 | "As the next modelling step, we will now add transmission links and hydrogen pipelines between the regions as expansion options. By modelling the interconnectors as bidirectional links, we will assume that they are controllable point-to-point HVDC connections. They are also assumed to be lossless, otherwise a similar workaround with two unidirectional links as for battery inverters has to be implemented. The cost of grids and pipelines is length-dependent." 694 | ] 695 | }, 696 | { 697 | "cell_type": "code", 698 | "execution_count": 18, 699 | "id": "b0593b1f", 700 | "metadata": {}, 701 | "outputs": [], 702 | "source": [ 703 | "# Define connections between regions with approximate distances\n", 704 | "connections = [\n", 705 | " (\"Germany\", \"United Kingdom\", 1000), # km\n", 706 | " (\"Germany\", \"Spain\", 1700), # km\n", 707 | " (\"Spain\", \"United Kingdom\", 1600), # km\n", 708 | "]\n", 709 | "\n", 710 | "for bus0, bus1, length in connections:\n", 711 | " n.add(\n", 712 | " \"Link\",\n", 713 | " f\"{bus0}-{bus1} HVDC\",\n", 714 | " bus0=bus0,\n", 715 | " bus1=bus1,\n", 716 | " carrier=\"HVDC\",\n", 717 | " p_nom_extendable=True,\n", 718 | " capital_cost=costs.at[\"HVDC overhead\", \"capital_cost\"] * length,\n", 719 | " p_min_pu=-1, # bidirectional\n", 720 | " )\n", 721 | "\n", 722 | " n.add(\n", 723 | " \"Link\",\n", 724 | " f\"{bus0}-{bus1} pipeline\",\n", 725 | " bus0=bus0 + \" hydrogen\",\n", 726 | " bus1=bus1 + \" hydrogen\",\n", 727 | " carrier=\"pipeline\",\n", 728 | " p_nom_extendable=True,\n", 729 | " capital_cost=costs.at[\"H2 (g) pipeline\", \"capital_cost\"] * length,\n", 730 | " p_min_pu=-1, # bidirectional\n", 731 | " )" 732 | ] 733 | }, 734 | { 735 | "cell_type": "markdown", 736 | "id": "6472616e", 737 | "metadata": {}, 738 | "source": [ 739 | "Finally, we add the option to import hydrogen from outside the model scope at a fixed cost. This could represent imports from dedicated renewable energy regions (e.g. North Africa, Australia, etc.)." 740 | ] 741 | }, 742 | { 743 | "cell_type": "code", 744 | "execution_count": 19, 745 | "id": "276fd222", 746 | "metadata": {}, 747 | "outputs": [], 748 | "source": [ 749 | "import_costs = pd.Series(\n", 750 | " {\n", 751 | " \"Germany\": 100,\n", 752 | " \"Spain\": 90,\n", 753 | " \"United Kingdom\": 105,\n", 754 | " }\n", 755 | ")\n", 756 | "\n", 757 | "n.add(\n", 758 | " \"Generator\",\n", 759 | " import_costs.index,\n", 760 | " suffix=\" hydrogen import\",\n", 761 | " bus=REGIONS,\n", 762 | " p_nom=10_000, # large value\n", 763 | " marginal_cost=import_costs,\n", 764 | " carrier=\"hydrogen import\",\n", 765 | ");" 766 | ] 767 | }, 768 | { 769 | "cell_type": "markdown", 770 | "id": "e610af5a", 771 | "metadata": {}, 772 | "source": [ 773 | "### Temporal Clustering" 774 | ] 775 | }, 776 | { 777 | "cell_type": "markdown", 778 | "id": "2e0d6109", 779 | "metadata": {}, 780 | "source": [ 781 | "To save some computation time, we only sample every third snapshot, which corresponds to a temporal resolution of 3 hours. Note that the snapshot weightings (the duration each time step represents) have to be adjusted accordingly." 782 | ] 783 | }, 784 | { 785 | "cell_type": "code", 786 | "execution_count": 20, 787 | "id": "77d82143", 788 | "metadata": {}, 789 | "outputs": [], 790 | "source": [ 791 | "n.set_snapshots(n.snapshots[::RESOLUTION])\n", 792 | "n.snapshot_weightings.loc[:, :] = RESOLUTION\n" 793 | ] 794 | }, 795 | { 796 | "cell_type": "markdown", 797 | "id": "ebbd5ae5", 798 | "metadata": {}, 799 | "source": [ 800 | "## Exploration\n", 801 | "\n", 802 | "We could now already run the optimisation and look at a first set of results." 803 | ] 804 | }, 805 | { 806 | "cell_type": "code", 807 | "execution_count": null, 808 | "id": "4214f5c6", 809 | "metadata": {}, 810 | "outputs": [], 811 | "source": [ 812 | "# n.optimize(\n", 813 | "# solver_name=SOLVER,\n", 814 | "# log_to_console=False,\n", 815 | "# extra_functionality=battery_constraint,\n", 816 | "# )" 817 | ] 818 | }, 819 | { 820 | "cell_type": "markdown", 821 | "id": "35e18d5f", 822 | "metadata": {}, 823 | "source": [ 824 | "But the exploration is left to you! Here are some ideas for sensitivities to explore and metrics to look at:\n", 825 | "\n", 826 | "**Metrics:**\n", 827 | "\n", 828 | "- Installed **capacities** of wind, solar, electrolysers, hydrogen storage, etc. (MW or MWh)\n", 829 | "- Total annual system **costs** and breakdown by technology (billion €/a and % of total cost). This can be built from `n.statistics.opex(groupby=[\"location\", \"carrier\"])` and `n.statistics.capex(groupby=[\"location\", \"carrier\"])`.\n", 830 | "- **Energy balances** of different carrier as time series (electricity, hydrogen) to identify key operational patterns. This can be accessed from `n.statistics.energy_balance(groupby=[\"bus\", \"carrier\"])` and plotted intereactively with `n.statistics.energy_balance.iplot.area(bus_carrier=\"AC\")`.\n", 831 | "- Electricity and hydrogen **prices** in each country. These can be accessed under `n.buses_t.marginal_price`.\n", 832 | "- Net **imports** of electricity and hydrogen by country and size of transmission/pipeline capacities. This can be accessed from `n.links_t.p0` and `n.links.p_nom_opt`. Hydrogen imports from outside the model scope can be accessed from `n.statistics.energy_balance(groupby=[\"bus\", \"carrier\"])`.\n", 833 | "- Storage **filling levels** per region over time. This can be accessed from `n.stores_t.e`.\n", 834 | "\n", 835 | "**Sensitivities:**\n", 836 | "\n", 837 | "- Choose a different triplet of **countries** (e.g. Belgium, Netherlands, Germany) and see how it affects the level of electricity and hydrogen trade. This can be done by retrieving alternative wind and solar time series form [model.energy](https://model.energy), changing the coordinates of the buses, and accessing different columns in the `demand` dataframe.\n", 838 | "- Omit the option to build **transmission or hydrogen pipelines** or both. This can be done by removing the respective `Link` components with `n.remove(\"Link\", ...)`.\n", 839 | "- Vary the **cost assumptions** by changing the overall projection year (e.g. 2030, 2040, 2050). This can be done when loading the cost data.\n", 840 | "- Vary the **import costs** for hydrogen and observe the impact on domestic electrolysis and storage. This can be done by changing `n.generators.loc[\"hydrogen import\", \"marginal_cost\"] *= 0.7` (to reduce import costs by 30%).\n", 841 | "- Make **hydrogen storage** more expensive (e.g. by assuming steel tank storage instead of underground storage). This can be done by changing `n.stores.loc[\"hydrogen storage\", \"capital_cost\"] *= 10` (to increase the capital costs by factor 10).\n", 842 | "- Vary the **cost of hydrogen pipelines and power transmission lines**. Do you see tipping points where suddenly a lot more or less capacity is built? This can be done by changing `n.links.loc[\"pipeline, \"capital_cost\"] *= 0.5` (to reduce the capital costs of pipeline by 50%).\n", 843 | "- Increase or decrease the **hydrogen demand**. What effect does it have on electricity prices? This can be done by changing `n.loads.loc[\"hydrogen load\", \"p_set\"] *= 1.5` (to increase the demand by 50%).\n", 844 | "\n", 845 | "**Feel free to explore other ideas as well!**" 846 | ] 847 | } 848 | ], 849 | "metadata": { 850 | "kernelspec": { 851 | "display_name": "sommerakademie-oxford (3.12.7)", 852 | "language": "python", 853 | "name": "python3" 854 | }, 855 | "language_info": { 856 | "codemirror_mode": { 857 | "name": "ipython", 858 | "version": 3 859 | }, 860 | "file_extension": ".py", 861 | "mimetype": "text/x-python", 862 | "name": "python", 863 | "nbconvert_exporter": "python", 864 | "pygments_lexer": "ipython3", 865 | "version": "3.12.7" 866 | } 867 | }, 868 | "nbformat": 4, 869 | "nbformat_minor": 5 870 | } 871 | -------------------------------------------------------------------------------- /sommerakademie-oxford/A-exploration-single-node.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "65d21713", 6 | "metadata": {}, 7 | "source": [ 8 | "# Models without Networks" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "id": "ff92a0c4", 14 | "metadata": {}, 15 | "source": [ 16 | "This section builds a simple country-scale energy system model without any networks (\"copperplate\") to explore the role of different generation, conversion, storage and flexibility technologies, as well as the impact of different policy constraints, such as carbon budgets, carbon pricing, and/or renewable capacity targets.\n", 17 | " \n", 18 | "\n", 19 | "![network-model](_assets/single-node.png)\n", 20 | "\n", 21 | "## Model Foundation\n", 22 | "\n" 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": null, 28 | "id": "c389e43c", 29 | "metadata": {}, 30 | "outputs": [], 31 | "source": [ 32 | "import pandas as pd\n", 33 | "import pypsa\n", 34 | "\n", 35 | "RESOLUTION = 3 # hours\n", 36 | "SOLVER = \"highs\" # or 'gurobi'\n", 37 | "CO2_PRICE = 0" 38 | ] 39 | }, 40 | { 41 | "cell_type": "markdown", 42 | "id": "59007564", 43 | "metadata": {}, 44 | "source": [ 45 | "### Cost Assumptions" 46 | ] 47 | }, 48 | { 49 | "cell_type": "markdown", 50 | "id": "c0566383", 51 | "metadata": {}, 52 | "source": [ 53 | "We take techno-economic assumptions from the [technology-data](https://github.com/PyPSA/technology-data) repository which collects assumptions on costs and efficiencies:" 54 | ] 55 | }, 56 | { 57 | "cell_type": "code", 58 | "execution_count": 140, 59 | "id": "9d399db1", 60 | "metadata": {}, 61 | "outputs": [], 62 | "source": [ 63 | "YEAR = 2030\n", 64 | "url = f\"https://raw.githubusercontent.com/PyPSA/technology-data/master/outputs/costs_{YEAR}.csv\"\n", 65 | "costs = pd.read_csv(url, index_col=[0, 1])\n", 66 | "costs.loc[costs.unit.str.contains(\"/kW\"), \"value\"] *= 1e3\n", 67 | "costs = costs.value.unstack().fillna({\"discount rate\": 0.07, \"lifetime\": 20, \"FOM\": 0})\n", 68 | "costs.loc[[\"OCGT\", \"CCGT\"], \"CO2 intensity\"] = costs.at[\"gas\", \"CO2 intensity\"]\n", 69 | "costs.loc[[\"OCGT\", \"CCGT\"], \"fuel\"] = costs.at[\"gas\", \"fuel\"]" 70 | ] 71 | }, 72 | { 73 | "cell_type": "markdown", 74 | "id": "763e0cee", 75 | "metadata": {}, 76 | "source": [ 77 | "We calculate the capital costs (i.e. annualised investment costs, €/MW/a or €/MWh/a for storage), using the discount rate and lifetime." 78 | ] 79 | }, 80 | { 81 | "cell_type": "code", 82 | "execution_count": 141, 83 | "id": "f7eb0b3c", 84 | "metadata": {}, 85 | "outputs": [], 86 | "source": [ 87 | "def annuity(r, n):\n", 88 | " return r / (1.0 - 1.0 / (1.0 + r) ** n)\n", 89 | "\n", 90 | "\n", 91 | "a = costs.apply(lambda x: annuity(x[\"discount rate\"], x[\"lifetime\"]), axis=1)\n", 92 | "costs[\"capital_cost\"] = (a + costs[\"FOM\"] / 100) * costs[\"investment\"]" 93 | ] 94 | }, 95 | { 96 | "cell_type": "markdown", 97 | "id": "9d5aff5d", 98 | "metadata": {}, 99 | "source": [ 100 | "### Time Series" 101 | ] 102 | }, 103 | { 104 | "cell_type": "markdown", 105 | "id": "7ea32947", 106 | "metadata": {}, 107 | "source": [ 108 | "The wind and solar capacity factor time series have been retrieved from [model.energy](https://model.energy). Go there to find more time series for other countries and plug them in here." 109 | ] 110 | }, 111 | { 112 | "cell_type": "code", 113 | "execution_count": 142, 114 | "id": "5630f010", 115 | "metadata": {}, 116 | "outputs": [ 117 | { 118 | "data": { 119 | "text/html": [ 120 | "
\n", 121 | "\n", 134 | "\n", 135 | " \n", 136 | " \n", 137 | " \n", 138 | " \n", 139 | " \n", 140 | " \n", 141 | " \n", 142 | " \n", 143 | " \n", 144 | " \n", 145 | " \n", 146 | " \n", 147 | " \n", 148 | " \n", 149 | " \n", 150 | " \n", 151 | " \n", 152 | " \n", 153 | " \n", 154 | " \n", 155 | " \n", 156 | " \n", 157 | " \n", 158 | " \n", 159 | "
onwindsolar
2013-01-01 00:00:000.8480.0
2013-01-01 01:00:000.8460.0
2013-01-01 02:00:000.8310.0
\n", 160 | "
" 161 | ], 162 | "text/plain": [ 163 | " onwind solar\n", 164 | "2013-01-01 00:00:00 0.848 0.0\n", 165 | "2013-01-01 01:00:00 0.846 0.0\n", 166 | "2013-01-01 02:00:00 0.831 0.0" 167 | ] 168 | }, 169 | "execution_count": 142, 170 | "metadata": {}, 171 | "output_type": "execute_result" 172 | } 173 | ], 174 | "source": [ 175 | "url = \"https://model.energy/data/time-series-dd07c6bb61a102ba3399f062230a24fc.csv\"\n", 176 | "ts = pd.read_csv(url, index_col=0, parse_dates=True)\n", 177 | "ts.head(3)" 178 | ] 179 | }, 180 | { 181 | "cell_type": "markdown", 182 | "id": "945c756d", 183 | "metadata": {}, 184 | "source": [ 185 | "The demand time series are exported from the [ENTSO-E Transparency Platform](https://transparency.entsoe.eu/)." 186 | ] 187 | }, 188 | { 189 | "cell_type": "code", 190 | "execution_count": 143, 191 | "id": "27d4192f", 192 | "metadata": {}, 193 | "outputs": [ 194 | { 195 | "data": { 196 | "text/plain": [ 197 | "2013-01-01 00:00:00 38617.5824\n", 198 | "2013-01-01 01:00:00 37058.2418\n", 199 | "2013-01-01 02:00:00 35576.9231\n", 200 | "Name: DE, dtype: float64" 201 | ] 202 | }, 203 | "execution_count": 143, 204 | "metadata": {}, 205 | "output_type": "execute_result" 206 | } 207 | ], 208 | "source": [ 209 | "url = \"https://tubcloud.tu-berlin.de/s/5ZcfnfC7mEwEGWt/download/electricity-demand.csv\"\n", 210 | "demand = pd.read_csv(url, index_col=0, parse_dates=True)[\"DE\"]\n", 211 | "demand.head(3)" 212 | ] 213 | }, 214 | { 215 | "cell_type": "markdown", 216 | "id": "c41f9b4b", 217 | "metadata": {}, 218 | "source": [ 219 | "### Model Initialisation\n", 220 | "\n", 221 | "Add buses, carriers with their associated emissions and set snapshots." 222 | ] 223 | }, 224 | { 225 | "cell_type": "code", 226 | "execution_count": 144, 227 | "id": "4e795829", 228 | "metadata": {}, 229 | "outputs": [], 230 | "source": [ 231 | "n = pypsa.Network()\n", 232 | "n.add(\"Bus\", \"electricity\")\n", 233 | "\n", 234 | "n.set_snapshots(ts.index)\n", 235 | "\n", 236 | "carriers = {\n", 237 | " \"wind\": \"dodgerblue\",\n", 238 | " \"solar\": \"gold\",\n", 239 | " \"AC\": \"navy\",\n", 240 | " \"electricity demand\": \"navy\",\n", 241 | " \"electrified heating\": \"steelblue\",\n", 242 | " \"oil\": \"black\",\n", 243 | " \"coal\": \"darkgray\",\n", 244 | " \"lignite\": \"sienna\",\n", 245 | " \"OCGT\": \"indianred\",\n", 246 | " \"CCGT\": \"firebrick\",\n", 247 | " \"biomass\": \"olivedrab\",\n", 248 | " \"nuclear\": \"orange\",\n", 249 | " \"battery storage 1h\": \"lightgreen\",\n", 250 | " \"battery storage 3h\": \"lightgreen\",\n", 251 | " \"battery storage 6h\": \"lightgreen\",\n", 252 | " \"hydrogen turbine\": \"blueviolet\",\n", 253 | " \"hydrogen import\": \"lavenderblush\",\n", 254 | " \"electrolysis\": \"orchid\",\n", 255 | " \"hydrogen storage\": \"hotpink\",\n", 256 | " \"hydrogen\": \"hotpink\",\n", 257 | " \"EV\": \"cadetblue\",\n", 258 | " \"EV demand\": \"cadetblue\",\n", 259 | " \"EV charger\": \"limegreen\",\n", 260 | " \"V2G\": \"limegreen\",\n", 261 | " \"EV battery\": \"teal\",\n", 262 | "}\n", 263 | "\n", 264 | "emissions = {\n", 265 | " \"wind\": 0,\n", 266 | " \"solar\": 0,\n", 267 | " \"AC\": 0,\n", 268 | " \"electricity demand\": 0,\n", 269 | " \"electrified heating\": 0,\n", 270 | " \"oil\": 0.257,\n", 271 | " \"coal\": 0.336,\n", 272 | " \"lignite\": 0.407,\n", 273 | " \"OCGT\": 0.198,\n", 274 | " \"CCGT\": 0.198,\n", 275 | " \"biomass\": 0,\n", 276 | " \"nuclear\": 0,\n", 277 | " \"battery storage 1h\": 0,\n", 278 | " \"battery storage 3h\": 0,\n", 279 | " \"battery storage 6h\": 0,\n", 280 | " \"hydrogen import\": 0,\n", 281 | " \"hydrogen turbine\": 0,\n", 282 | " \"electrolysis\": 0,\n", 283 | " \"hydrogen storage\": 0,\n", 284 | " \"hydrogen\": 0,\n", 285 | " \"EV\": 0,\n", 286 | " \"EV demand\": 0,\n", 287 | " \"EV charger\": 0,\n", 288 | " \"V2G\": 0,\n", 289 | " \"EV battery\": 0,\n", 290 | "}\n", 291 | "n.add(\"Carrier\", carriers.keys(), color=carriers.values(), co2_emissions=emissions);" 292 | ] 293 | }, 294 | { 295 | "cell_type": "markdown", 296 | "id": "23675cf9", 297 | "metadata": {}, 298 | "source": [ 299 | "### Demand\n", 300 | "\n", 301 | "We add an electricity demand time series with a total annual demand of around 500 TWh/a for Germany." 302 | ] 303 | }, 304 | { 305 | "cell_type": "code", 306 | "execution_count": 145, 307 | "id": "1fa193c1", 308 | "metadata": {}, 309 | "outputs": [], 310 | "source": [ 311 | "n.add(\n", 312 | " \"Load\",\n", 313 | " \"electricity demand\",\n", 314 | " bus=\"electricity\",\n", 315 | " p_set=demand,\n", 316 | " carrier=\"electricity\",\n", 317 | ");" 318 | ] 319 | }, 320 | { 321 | "cell_type": "markdown", 322 | "id": "3ea806be", 323 | "metadata": {}, 324 | "source": [ 325 | "### Fossil Power Plants\n", 326 | "\n", 327 | "Here, we add fossil power plants with their respective investment costs, marginal costs, efficiencies, and CO2 emissions." 328 | ] 329 | }, 330 | { 331 | "cell_type": "code", 332 | "execution_count": 146, 333 | "id": "de65fbbd", 334 | "metadata": {}, 335 | "outputs": [], 336 | "source": [ 337 | "for fossil in [\"coal\", \"lignite\", \"OCGT\", \"CCGT\", \"oil\"]:\n", 338 | "\n", 339 | " marginal_cost = (\n", 340 | " costs.at[fossil, \"fuel\"] / costs.at[fossil, \"efficiency\"]\n", 341 | " + costs.at[fossil, \"VOM\"]\n", 342 | " + CO2_PRICE * costs.at[fossil, \"CO2 intensity\"] / costs.at[fossil, \"efficiency\"]\n", 343 | " )\n", 344 | "\n", 345 | " n.add(\n", 346 | " \"Generator\",\n", 347 | " fossil,\n", 348 | " bus=\"electricity\",\n", 349 | " p_nom_extendable=True,\n", 350 | " efficiency=costs.at[fossil, \"efficiency\"],\n", 351 | " marginal_cost=marginal_cost,\n", 352 | " capital_cost=costs.at[fossil, \"capital_cost\"],\n", 353 | " carrier=fossil,\n", 354 | " );" 355 | ] 356 | }, 357 | { 358 | "cell_type": "markdown", 359 | "id": "0f05e30f", 360 | "metadata": {}, 361 | "source": [ 362 | "### Nuclear Power Plant\n", 363 | "\n", 364 | "Here, we set `p_min_pu=1` to force the plant to always run at full capacity. This is a simplification, as in reality nuclear plants can be ramped down, but not very flexibly." 365 | ] 366 | }, 367 | { 368 | "cell_type": "code", 369 | "execution_count": 147, 370 | "id": "5e06e1a1", 371 | "metadata": {}, 372 | "outputs": [], 373 | "source": [ 374 | "marginal_cost = (\n", 375 | " costs.at[\"nuclear\", \"fuel\"] / costs.at[\"nuclear\", \"efficiency\"]\n", 376 | " + costs.at[\"nuclear\", \"VOM\"]\n", 377 | ")\n", 378 | "\n", 379 | "n.add(\n", 380 | " \"Generator\",\n", 381 | " \"nuclear\",\n", 382 | " bus=\"electricity\",\n", 383 | " p_nom_extendable=True,\n", 384 | " efficiency=costs.at[\"nuclear\", \"efficiency\"],\n", 385 | " marginal_cost=marginal_cost,\n", 386 | " capital_cost=costs.at[\"nuclear\", \"capital_cost\"],\n", 387 | " carrier=\"nuclear\",\n", 388 | " p_min_pu=1, # forced baseload operation\n", 389 | ");" 390 | ] 391 | }, 392 | { 393 | "cell_type": "markdown", 394 | "id": "d28ebd94", 395 | "metadata": {}, 396 | "source": [ 397 | "### Biomass Power Plant\n", 398 | "\n", 399 | "Here, we limit the total energy output of the biomass plant by setting `e_sum_max` to represent a limited sustainable biomass potential, e.g. 50 TWh/a." 400 | ] 401 | }, 402 | { 403 | "cell_type": "code", 404 | "execution_count": 148, 405 | "id": "7a02b823", 406 | "metadata": {}, 407 | "outputs": [], 408 | "source": [ 409 | "marginal_cost = costs.at[\"biomass\", \"fuel\"] / costs.at[\"biomass\", \"efficiency\"]\n", 410 | "\n", 411 | "n.add(\n", 412 | " \"Generator\",\n", 413 | " \"biomass\",\n", 414 | " bus=\"electricity\",\n", 415 | " p_nom_extendable=True,\n", 416 | " efficiency=costs.at[\"biomass\", \"efficiency\"],\n", 417 | " marginal_cost=marginal_cost,\n", 418 | " capital_cost=costs.at[\"biomass\", \"capital_cost\"],\n", 419 | " carrier=\"biomass\",\n", 420 | " e_sum_max=50e6, # limit\n", 421 | ");" 422 | ] 423 | }, 424 | { 425 | "cell_type": "markdown", 426 | "id": "a32c9339", 427 | "metadata": {}, 428 | "source": [ 429 | "### Wind and Solar\n", 430 | "\n", 431 | "Here, we set `p_max_pu` to the time series of the renewable capacity factors." 432 | ] 433 | }, 434 | { 435 | "cell_type": "code", 436 | "execution_count": 149, 437 | "id": "b7564d62", 438 | "metadata": {}, 439 | "outputs": [], 440 | "source": [ 441 | "n.add(\n", 442 | " \"Generator\",\n", 443 | " \"wind\",\n", 444 | " bus=\"electricity\",\n", 445 | " carrier=\"wind\",\n", 446 | " p_max_pu=ts.onwind,\n", 447 | " capital_cost=costs.at[\"onwind\", \"capital_cost\"],\n", 448 | " p_nom_extendable=True,\n", 449 | ")\n", 450 | "\n", 451 | "n.add(\n", 452 | " \"Generator\",\n", 453 | " \"solar\",\n", 454 | " bus=\"electricity\",\n", 455 | " carrier=\"solar\",\n", 456 | " p_max_pu=ts.solar,\n", 457 | " capital_cost=costs.at[\"solar\", \"capital_cost\"],\n", 458 | " p_nom_extendable=True,\n", 459 | ");" 460 | ] 461 | }, 462 | { 463 | "cell_type": "markdown", 464 | "id": "7d795029", 465 | "metadata": {}, 466 | "source": [ 467 | "### Batteries\n", 468 | "\n", 469 | "Here, we add batteries with energy-to-power ratios of 1, 3, and 6 hours." 470 | ] 471 | }, 472 | { 473 | "cell_type": "code", 474 | "execution_count": 150, 475 | "id": "42d08b04", 476 | "metadata": {}, 477 | "outputs": [], 478 | "source": [ 479 | "for max_hours in [1, 3, 6]:\n", 480 | " n.add(\n", 481 | " \"StorageUnit\",\n", 482 | " f\"battery storage {max_hours}h\",\n", 483 | " bus=\"electricity\",\n", 484 | " carrier=f\"battery storage {max_hours}h\",\n", 485 | " max_hours=max_hours,\n", 486 | " capital_cost=costs.at[\"battery inverter\", \"capital_cost\"]\n", 487 | " + max_hours * costs.at[\"battery storage\", \"capital_cost\"],\n", 488 | " efficiency_store=costs.at[\"battery inverter\", \"efficiency\"],\n", 489 | " efficiency_dispatch=costs.at[\"battery inverter\", \"efficiency\"],\n", 490 | " p_nom_extendable=True,\n", 491 | " cyclic_state_of_charge=True,\n", 492 | " )" 493 | ] 494 | }, 495 | { 496 | "cell_type": "markdown", 497 | "id": "3145e0e6", 498 | "metadata": {}, 499 | "source": [ 500 | "### Hydrogen\n", 501 | "\n", 502 | "Here, we add a hydrogen storage system with electrolysers, underground storage, and a hydrogen turbine. Additionally, there is a constant hydrogen demand in the order of 50% of the average electricity demand. There is also the option to import hydrogen at high cost." 503 | ] 504 | }, 505 | { 506 | "cell_type": "code", 507 | "execution_count": 151, 508 | "id": "d4ca9042", 509 | "metadata": {}, 510 | "outputs": [], 511 | "source": [ 512 | "n.add(\"Bus\", \"hydrogen\", carrier=\"hydrogen\")\n", 513 | "\n", 514 | "n.add(\n", 515 | " \"Load\",\n", 516 | " \"hydrogen demand\",\n", 517 | " bus=\"hydrogen\",\n", 518 | " p_set=demand.mean() / 2,\n", 519 | " carrier=\"hydrogen\",\n", 520 | ")\n", 521 | "\n", 522 | "n.add(\n", 523 | " \"Link\",\n", 524 | " \"electrolysis\",\n", 525 | " bus0=\"electricity\",\n", 526 | " bus1=\"hydrogen\",\n", 527 | " carrier=\"electrolysis\",\n", 528 | " p_nom_extendable=True,\n", 529 | " efficiency=costs.at[\"electrolysis\", \"efficiency\"],\n", 530 | " capital_cost=costs.at[\"electrolysis\", \"capital_cost\"],\n", 531 | ")\n", 532 | "n.add(\n", 533 | " \"Link\",\n", 534 | " \"hydrogen turbine\",\n", 535 | " bus0=\"hydrogen\",\n", 536 | " bus1=\"electricity\",\n", 537 | " carrier=\"hydrogen turbine\",\n", 538 | " p_nom_extendable=True,\n", 539 | " efficiency=costs.at[\"OCGT\", \"efficiency\"],\n", 540 | " capital_cost=costs.at[\"OCGT\", \"capital_cost\"] / costs.at[\"OCGT\", \"efficiency\"],\n", 541 | ")\n", 542 | "\n", 543 | "tech = \"hydrogen storage tank type 1 including compressor\"\n", 544 | "tech = \"hydrogen storage underground\"\n", 545 | "\n", 546 | "n.add(\n", 547 | " \"Store\",\n", 548 | " \"hydrogen storage\",\n", 549 | " bus=\"hydrogen\",\n", 550 | " carrier=\"hydrogen storage\",\n", 551 | " capital_cost=costs.at[tech, \"capital_cost\"],\n", 552 | " e_nom_extendable=True,\n", 553 | " e_cyclic=True,\n", 554 | ");\n", 555 | "\n", 556 | "n.add(\n", 557 | " \"Generator\",\n", 558 | " \"hydrogen import\",\n", 559 | " bus=\"hydrogen\",\n", 560 | " p_nom=10_000, # large value\n", 561 | " marginal_cost=200,\n", 562 | " carrier=\"hydrogen import\",\n", 563 | ");\n" 564 | ] 565 | }, 566 | { 567 | "cell_type": "markdown", 568 | "id": "b847d467", 569 | "metadata": {}, 570 | "source": [ 571 | "### Electric Vehicles\n", 572 | "\n", 573 | "Here, we add electric vehicles (EV) as previously shown in the sector-coupling tutorial. Electric vehicles have an availability profile for when they are connected to the grid, and a requirement profile for the minimum state of charge they should have at certain times of the day (e.g. 75% at 6 AM). Fractions of the total fleet participate in smart charging and vehicle-to-grid (V2G). The charging power and battery sizes are based on a total of 40 million EVs with 50 kWh batteries and a maximum charging power of 11 kW." 574 | ] 575 | }, 576 | { 577 | "cell_type": "code", 578 | "execution_count": 152, 579 | "id": "51e2b7e7", 580 | "metadata": {}, 581 | "outputs": [], 582 | "source": [ 583 | "number_cars = 40e6 # number of EV cars\n", 584 | "bev_charger_rate = 0.011 # 3-phase EV charger with 11 kW\n", 585 | "p_nom = number_cars * bev_charger_rate\n", 586 | "\n", 587 | "bev_energy = 0.05 # average battery size of EV in MWh\n", 588 | "bev_dsm_participants = 0.5 # share of cars that do smart charging\n", 589 | "e_nom = number_cars * bev_energy * bev_dsm_participants\n", 590 | "\n", 591 | "url = \"https://tubcloud.tu-berlin.de/s/9r5bMSbzzQiqG7H/download/electric-vehicle-profile-example.csv\"\n", 592 | "p_set = pd.read_csv(url, index_col=0, parse_dates=True).squeeze()\n", 593 | "p_set = p_set.resample(\"1h\").mean().interpolate()\n", 594 | "p_set.index = p_set.index.shift(-(2 * 365), freq=\"D\")\n", 595 | "p_set = p_set.reindex(n.snapshots).ffill()\n", 596 | "\n", 597 | "url = \"https://tubcloud.tu-berlin.de/s/E3PBWPfYaWwCq7a/download/electric-vehicle-availability-example.csv\"\n", 598 | "available = pd.read_csv(url, index_col=0, parse_dates=True).squeeze()\n", 599 | "available = available.resample(\"1h\").mean().interpolate()\n", 600 | "available.index = available.index.shift(-(2 * 365), freq=\"D\")\n", 601 | "available = available.reindex(n.snapshots).ffill()\n", 602 | "\n", 603 | "n.add(\"Bus\", \"EV\", carrier=\"EV\")\n", 604 | "\n", 605 | "n.add(\"Load\", \"EV demand\", bus=\"EV\", carrier=\"EV demand\", p_set=p_set)\n", 606 | "\n", 607 | "n.add(\n", 608 | " \"Link\",\n", 609 | " \"EV charger\",\n", 610 | " bus0=\"electricity\",\n", 611 | " bus1=\"EV\",\n", 612 | " p_nom=p_nom,\n", 613 | " carrier=\"EV charger\",\n", 614 | " p_max_pu=available,\n", 615 | " efficiency=0.9,\n", 616 | ")\n", 617 | "\n", 618 | "n.add(\n", 619 | " \"Link\",\n", 620 | " \"V2G\",\n", 621 | " bus0=\"EV\",\n", 622 | " bus1=\"electricity\",\n", 623 | " p_nom=p_nom,\n", 624 | " carrier=\"V2G\",\n", 625 | " p_max_pu=available,\n", 626 | " efficiency=0.9,\n", 627 | ")\n", 628 | "\n", 629 | "requirement = pd.Series(0., index=n.snapshots)\n", 630 | "requirement.where(requirement.index.hour != 6, 0.75, inplace=True)\n", 631 | "\n", 632 | "n.add(\n", 633 | " \"Store\",\n", 634 | " \"EV battery\",\n", 635 | " bus=\"EV\",\n", 636 | " carrier=\"EV battery\",\n", 637 | " e_cyclic=True, # state of charge at beginning = state of charge at the end\n", 638 | " e_nom=e_nom,\n", 639 | " e_min_pu=requirement,\n", 640 | ");" 641 | ] 642 | }, 643 | { 644 | "cell_type": "markdown", 645 | "id": "14d2deda", 646 | "metadata": {}, 647 | "source": [ 648 | "### Emission Limit\n", 649 | "\n", 650 | "Here, we could set a CO2 emission limit for the whole system, e.g. 100 MtCO2/a." 651 | ] 652 | }, 653 | { 654 | "cell_type": "code", 655 | "execution_count": 153, 656 | "id": "56addd9e", 657 | "metadata": {}, 658 | "outputs": [], 659 | "source": [ 660 | "# n.add(\n", 661 | "# \"GlobalConstraint\",\n", 662 | "# \"emission_limit\",\n", 663 | "# carrier_attribute=\"co2_emissions\",\n", 664 | "# sense=\"<=\",\n", 665 | "# constant=0,\n", 666 | "# );" 667 | ] 668 | }, 669 | { 670 | "cell_type": "markdown", 671 | "id": "19c83789", 672 | "metadata": {}, 673 | "source": [ 674 | "### Electrified Heating\n", 675 | "\n", 676 | "Here, we could add an additional load representing electricity demand from electrified heating (e.g. heat pumps) with a seasonal profile." 677 | ] 678 | }, 679 | { 680 | "cell_type": "code", 681 | "execution_count": 154, 682 | "id": "08164374", 683 | "metadata": {}, 684 | "outputs": [], 685 | "source": [ 686 | "# url = \"https://tubcloud.tu-berlin.de/s/8KWqTAHEM9m8dFj/download/heat-demand.csv\"\n", 687 | "# p_set = pd.read_csv(url, index_col=0, parse_dates=True).squeeze()\n", 688 | "# p_set = p_set / p_set.max() * demand.mean() / 2\n", 689 | "# n.add(\n", 690 | "# \"Load\",\n", 691 | "# \"electrified heating\",\n", 692 | "# bus=\"electricity\",\n", 693 | "# p_set=p_set,\n", 694 | "# carrier=\"electrified heating\",\n", 695 | "# );" 696 | ] 697 | }, 698 | { 699 | "cell_type": "markdown", 700 | "id": "cf6bd4ee", 701 | "metadata": {}, 702 | "source": [ 703 | "### Temporal Clustering" 704 | ] 705 | }, 706 | { 707 | "cell_type": "markdown", 708 | "id": "7c050873", 709 | "metadata": {}, 710 | "source": [ 711 | "To save some computation time, we only sample every third snapshot, which corresponds to a temporal resolution of 3 hours. Note that the snapshot weightings (the duration each time step represents) have to be adjusted accordingly." 712 | ] 713 | }, 714 | { 715 | "cell_type": "code", 716 | "execution_count": 155, 717 | "id": "648ebb3f", 718 | "metadata": {}, 719 | "outputs": [], 720 | "source": [ 721 | "n.set_snapshots(n.snapshots[::RESOLUTION])\n", 722 | "n.snapshot_weightings.loc[:, :] = RESOLUTION\n" 723 | ] 724 | }, 725 | { 726 | "cell_type": "markdown", 727 | "id": "ac6d740f", 728 | "metadata": {}, 729 | "source": [ 730 | "## Exploration\n", 731 | "\n", 732 | "We could now already run the optimisation and look at a first set of results." 733 | ] 734 | }, 735 | { 736 | "cell_type": "code", 737 | "execution_count": 156, 738 | "id": "40633f4e", 739 | "metadata": {}, 740 | "outputs": [ 741 | { 742 | "name": "stderr", 743 | "output_type": "stream", 744 | "text": [ 745 | "WARNING:pypsa.consistency:The following loads have carriers which are not defined:\n", 746 | "Index(['electricity demand'], dtype='object', name='Load')\n", 747 | "INFO:linopy.model: Solve problem using Gurobi solver\n", 748 | "INFO:linopy.model:Solver options:\n", 749 | " - log_to_console: False\n", 750 | "INFO:linopy.io:Writing objective.\n", 751 | "Writing constraints.: 100%|\u001b[38;2;128;191;255m██████████\u001b[0m| 30/30 [00:00<00:00, 34.25it/s]\n", 752 | "Writing continuous variables.: 100%|\u001b[38;2;128;191;255m██████████\u001b[0m| 11/11 [00:00<00:00, 78.29it/s]\n", 753 | "INFO:linopy.io: Writing time: 1.06s\n" 754 | ] 755 | }, 756 | { 757 | "name": "stdout", 758 | "output_type": "stream", 759 | "text": [ 760 | "Set parameter TokenServer to value \"gurobi-license.pypsa.org\"\n" 761 | ] 762 | }, 763 | { 764 | "name": "stderr", 765 | "output_type": "stream", 766 | "text": [ 767 | "INFO:gurobipy:Set parameter TokenServer to value \"gurobi-license.pypsa.org\"\n" 768 | ] 769 | }, 770 | { 771 | "name": "stdout", 772 | "output_type": "stream", 773 | "text": [ 774 | "Read LP format model from file /tmp/linopy-problem-_c60ry4g.lp\n" 775 | ] 776 | }, 777 | { 778 | "name": "stderr", 779 | "output_type": "stream", 780 | "text": [ 781 | "INFO:gurobipy:Read LP format model from file /tmp/linopy-problem-_c60ry4g.lp\n" 782 | ] 783 | }, 784 | { 785 | "name": "stdout", 786 | "output_type": "stream", 787 | "text": [ 788 | "Reading time = 0.21 seconds\n" 789 | ] 790 | }, 791 | { 792 | "name": "stderr", 793 | "output_type": "stream", 794 | "text": [ 795 | "INFO:gurobipy:Reading time = 0.21 seconds\n" 796 | ] 797 | }, 798 | { 799 | "name": "stdout", 800 | "output_type": "stream", 801 | "text": [ 802 | "obj: 169376 rows, 78855 columns, 340188 nonzeros\n" 803 | ] 804 | }, 805 | { 806 | "name": "stderr", 807 | "output_type": "stream", 808 | "text": [ 809 | "INFO:gurobipy:obj: 169376 rows, 78855 columns, 340188 nonzeros\n" 810 | ] 811 | }, 812 | { 813 | "name": "stdout", 814 | "output_type": "stream", 815 | "text": [ 816 | "Set parameter LogToConsole to value 0\n" 817 | ] 818 | }, 819 | { 820 | "name": "stderr", 821 | "output_type": "stream", 822 | "text": [ 823 | "INFO:gurobipy:Set parameter LogToConsole to value 0\n", 824 | "INFO:gurobipy:Gurobi Optimizer version 12.0.3 build v12.0.3rc0 (linux64 - \"Ubuntu 24.10\")\n", 825 | "INFO:gurobipy:\n", 826 | "INFO:gurobipy:CPU model: AMD Ryzen 7 PRO 7840U w/ Radeon 780M Graphics, instruction set [SSE2|AVX|AVX2|AVX512]\n", 827 | "INFO:gurobipy:Thread count: 8 physical cores, 16 logical processors, using up to 16 threads\n", 828 | "INFO:gurobipy:\n", 829 | "INFO:gurobipy:Non-default parameters:\n", 830 | "INFO:gurobipy:LogToConsole 0\n", 831 | "INFO:gurobipy:\n", 832 | "INFO:gurobipy:Optimize a model with 169376 rows, 78855 columns and 340188 nonzeros\n", 833 | "INFO:gurobipy:Model fingerprint: 0x23ccacb3\n", 834 | "INFO:gurobipy:Coefficient statistics:\n", 835 | "INFO:gurobipy: Matrix range [1e-03, 6e+00]\n", 836 | "INFO:gurobipy: Objective range [4e+01, 8e+05]\n", 837 | "INFO:gurobipy: Bounds range [0e+00, 0e+00]\n", 838 | "INFO:gurobipy: RHS range [2e+03, 5e+07]\n", 839 | "INFO:gurobipy:Presolve removed 94922 rows and 10227 columns\n", 840 | "INFO:gurobipy:Presolve time: 0.28s\n", 841 | "INFO:gurobipy:Presolved: 74454 rows, 68628 columns, 232119 nonzeros\n", 842 | "INFO:gurobipy:\n", 843 | "INFO:gurobipy:Concurrent LP optimizer: primal simplex, dual simplex, and barrier\n", 844 | "INFO:gurobipy:Showing barrier log only...\n", 845 | "INFO:gurobipy:\n", 846 | "INFO:gurobipy:Ordering time: 0.05s\n", 847 | "INFO:gurobipy:\n", 848 | "INFO:gurobipy:Barrier statistics:\n", 849 | "INFO:gurobipy: Dense cols : 15\n", 850 | "INFO:gurobipy: AA' NZ : 1.869e+05\n", 851 | "INFO:gurobipy: Factor NZ : 8.790e+05 (roughly 70 MB of memory)\n", 852 | "INFO:gurobipy: Factor Ops : 1.732e+07 (less than 1 second per iteration)\n", 853 | "INFO:gurobipy: Threads : 6\n", 854 | "INFO:gurobipy:\n", 855 | "INFO:gurobipy: Objective Residual\n", 856 | "INFO:gurobipy:Iter Primal Dual Primal Dual Compl Time\n", 857 | "INFO:gurobipy: 0 9.18909941e+12 -5.29509868e+13 5.52e+09 1.20e+02 2.01e+10 1s\n", 858 | "INFO:gurobipy: 1 9.06240463e+12 -4.63210919e+13 5.05e+09 1.58e+03 7.57e+09 1s\n", 859 | "INFO:gurobipy: 2 1.12636667e+13 -3.97834184e+13 7.00e+08 4.37e+02 1.87e+09 1s\n", 860 | "INFO:gurobipy: 3 7.83211937e+12 -8.91560047e+12 3.44e+05 8.16e+00 1.40e+08 1s\n", 861 | "INFO:gurobipy: 4 2.31674373e+12 -1.71552242e+12 6.52e+04 1.45e+00 3.13e+07 1s\n", 862 | "INFO:gurobipy: 5 1.08122715e+12 -5.69832892e+11 2.83e+04 4.80e-01 1.24e+07 1s\n", 863 | "INFO:gurobipy: 6 4.79952709e+11 -2.70749040e+11 1.17e+04 2.19e-01 5.56e+06 1s\n", 864 | "INFO:gurobipy: 7 3.04605266e+11 -9.83505819e+10 6.98e+03 8.18e-02 2.96e+06 1s\n", 865 | "INFO:gurobipy: 8 2.35036540e+11 -2.50041013e+10 4.91e+03 3.66e-02 1.90e+06 1s\n", 866 | "INFO:gurobipy: 9 1.26922340e+11 3.26379625e+10 1.51e+03 1.14e-13 6.89e+05 2s\n", 867 | "INFO:gurobipy: 10 8.99083008e+10 5.45864776e+10 5.43e+02 1.14e-13 2.58e+05 2s\n", 868 | "INFO:gurobipy: 11 7.99663920e+10 5.82200377e+10 3.25e+02 1.14e-13 1.59e+05 2s\n", 869 | "INFO:gurobipy: 12 7.44201607e+10 6.06103761e+10 2.11e+02 1.14e-13 1.01e+05 2s\n", 870 | "INFO:gurobipy: 13 7.01571523e+10 6.16981393e+10 1.22e+02 1.14e-13 6.17e+04 2s\n", 871 | "INFO:gurobipy: 14 6.82657683e+10 6.25628306e+10 8.29e+01 1.14e-13 4.16e+04 2s\n", 872 | "INFO:gurobipy: 15 6.71069172e+10 6.29803163e+10 5.82e+01 1.14e-13 3.01e+04 2s\n", 873 | "INFO:gurobipy: 16 6.62162277e+10 6.34123137e+10 3.93e+01 1.14e-13 2.04e+04 2s\n", 874 | "INFO:gurobipy: 17 6.54971142e+10 6.37474804e+10 2.43e+01 1.14e-13 1.28e+04 2s\n", 875 | "INFO:gurobipy: 18 6.50745946e+10 6.39603171e+10 1.53e+01 1.14e-13 8.12e+03 2s\n", 876 | "INFO:gurobipy: 19 6.48273371e+10 6.40431940e+10 1.01e+01 1.14e-13 5.71e+03 3s\n", 877 | "INFO:gurobipy: 20 6.46321778e+10 6.41301083e+10 6.03e+00 1.14e-13 3.66e+03 3s\n", 878 | "INFO:gurobipy: 21 6.44953015e+10 6.41679172e+10 3.22e+00 6.82e-14 2.39e+03 3s\n", 879 | "INFO:gurobipy: 22 6.44040054e+10 6.42790848e+10 1.43e+00 1.14e-13 9.10e+02 3s\n", 880 | "INFO:gurobipy: 23 6.43876232e+10 6.42876546e+10 1.13e+00 1.14e-13 7.29e+02 3s\n", 881 | "INFO:gurobipy: 24 6.43609830e+10 6.42985455e+10 6.38e-01 1.14e-13 4.55e+02 3s\n", 882 | "INFO:gurobipy: 25 6.43456311e+10 6.43038100e+10 3.61e-01 1.14e-13 3.05e+02 3s\n", 883 | "INFO:gurobipy: 26 6.43375516e+10 6.43126877e+10 2.23e-01 1.14e-13 1.81e+02 3s\n", 884 | "INFO:gurobipy: 27 6.43318167e+10 6.43195810e+10 1.24e-01 1.14e-13 8.92e+01 3s\n", 885 | "INFO:gurobipy: 28 6.43307689e+10 6.43230062e+10 1.07e-01 1.14e-13 5.66e+01 3s\n", 886 | "INFO:gurobipy: 29 6.43259191e+10 6.43239595e+10 2.89e-02 1.14e-13 1.43e+01 4s\n", 887 | "INFO:gurobipy: 30 6.43241075e+10 6.43241053e+10 5.99e-06 1.82e-10 1.55e-02 4s\n", 888 | "INFO:gurobipy: 31 6.43241056e+10 6.43241056e+10 5.96e-08 3.20e-10 1.56e-05 4s\n", 889 | "INFO:gurobipy: 32 6.43241056e+10 6.43241056e+10 8.94e-08 1.28e-09 1.56e-08 4s\n", 890 | "INFO:gurobipy: 33 6.43241056e+10 6.43241056e+10 2.31e-07 8.73e-10 3.91e-13 4s\n", 891 | "INFO:gurobipy:\n", 892 | "INFO:gurobipy:Barrier solved model in 33 iterations and 4.01 seconds (1.17 work units)\n", 893 | "INFO:gurobipy:Optimal objective 6.43241056e+10\n", 894 | "INFO:gurobipy:\n", 895 | "INFO:gurobipy:Crossover log...\n", 896 | "INFO:gurobipy:\n", 897 | "INFO:gurobipy: 45265 DPushes remaining with DInf 0.0000000e+00 4s\n", 898 | "INFO:gurobipy: 0 DPushes remaining with DInf 0.0000000e+00 4s\n", 899 | "INFO:gurobipy:Warning: Markowitz tolerance tightened to 0.5\n", 900 | "INFO:gurobipy:\n", 901 | "INFO:gurobipy: 4228 PPushes remaining with PInf 0.0000000e+00 4s\n", 902 | "INFO:gurobipy: 0 PPushes remaining with PInf 0.0000000e+00 5s\n", 903 | "INFO:gurobipy:\n", 904 | "INFO:gurobipy: Push phase complete: Pinf 0.0000000e+00, Dinf 1.0349524e-08 5s\n", 905 | "INFO:gurobipy:\n", 906 | "INFO:gurobipy:\n", 907 | "INFO:gurobipy:Solved with barrier\n", 908 | "INFO:gurobipy:Iteration Objective Primal Inf. Dual Inf. Time\n", 909 | "INFO:gurobipy: 22200 6.4324106e+10 0.000000e+00 0.000000e+00 5s\n", 910 | "INFO:gurobipy:\n", 911 | "INFO:gurobipy:Solved in 22200 iterations and 4.92 seconds (2.06 work units)\n", 912 | "INFO:gurobipy:Optimal objective 6.432410565e+10\n", 913 | "INFO:linopy.constants: Optimization successful: \n", 914 | "Status: ok\n", 915 | "Termination condition: optimal\n", 916 | "Solution: 78855 primals, 169376 duals\n", 917 | "Objective: 6.43e+10\n", 918 | "Solver model: available\n", 919 | "Solver message: 2\n", 920 | "\n", 921 | "INFO:pypsa.optimization.optimize:The shadow-prices of the constraints Generator-fix-p-lower, Generator-fix-p-upper, Generator-ext-p-lower, Generator-ext-p-upper, Link-fix-p-lower, Link-fix-p-upper, Link-ext-p-lower, Link-ext-p-upper, Store-fix-e-lower, Store-fix-e-upper, Store-ext-e-lower, Store-ext-e-upper, StorageUnit-ext-p_dispatch-lower, StorageUnit-ext-p_dispatch-upper, StorageUnit-ext-p_store-lower, StorageUnit-ext-p_store-upper, StorageUnit-ext-state_of_charge-lower, StorageUnit-ext-state_of_charge-upper, StorageUnit-energy_balance, Store-energy_balance were not assigned to the network.\n" 922 | ] 923 | }, 924 | { 925 | "data": { 926 | "text/plain": [ 927 | "('ok', 'optimal')" 928 | ] 929 | }, 930 | "execution_count": 156, 931 | "metadata": {}, 932 | "output_type": "execute_result" 933 | } 934 | ], 935 | "source": [ 936 | "n.optimize(\n", 937 | " solver_name=SOLVER,\n", 938 | " log_to_console=False,\n", 939 | ")" 940 | ] 941 | }, 942 | { 943 | "cell_type": "markdown", 944 | "id": "1a43af54", 945 | "metadata": {}, 946 | "source": [ 947 | "\n", 948 | "But the exploration is left to you! Here are some ideas for sensitivities to explore and metrics to look at:\n", 949 | "\n", 950 | "**Metrics:**\n", 951 | "\n", 952 | "- Total annual system **costs** and breakdown by technology (billion €/a and % of total cost). This can be built from `n.statistics.opex()` and `n.statistics.capex()`.\n", 953 | "- Installed **capacities** of the different technologies (fossil power plants, wind, solar, battery, electrolysis, etc.). This can be accessed from `n.statistics.optimal_capacity()`.\n", 954 | "- **Energy mix** and balances of different carrier as time series. This can be accessed from `n.statistics.energy_balance(groupby=[\"bus\", \"carrier\"])` and plotted intereactively with `n.statistics.energy_balance.iplot.area(bus_carrier=\"AC\")`.\n", 955 | "- Electricity **prices** (average and duration curve). These can be accessed under `n.buses_t.marginal_price`.\n", 956 | "- Level of CO2 **emissions**. This can be calculated using `n.generators_t.p / n.generators.efficiency * n.generators.carrier.map(n.carriers.co2_emissions)`.\n", 957 | "- Storage **filling levels** per technology over time. This can be accessed from `n.stores_t.e`.\n", 958 | "\n", 959 | "**Sensitivities:**\n", 960 | "\n", 961 | "- Choose a different **country** for demand and renewables time series. This can be done by exchanging the data inputs at the top.\n", 962 | "- Vary the **cost** assumptions by changing the overall projection year (e.g. 2030, 2040, 2050). This can be done when loading the cost data.\n", 963 | "- Successively increase/decrease the investment cost of **nuclear** power. This can be done by modifying `n.generators.loc[\"nuclear\", \"capital_cost\"]`.\n", 964 | "- Limit the **flexibility of nuclear power plants**, e.g. by requiring a minimum part load of 90%. This can be done by setting `n.generators.loc[\"nuclear\", \"p_min_pu\"] = 0.9`. You can also set ramp limits for other fossil generators (`ramp_limit_up` and `ramp_limit_down`).\n", 965 | "- Vary the **cost of individual technologies** along their learning curves (solar, battery, electrolysis, hydrogen storage, etc.). This can be done by changing their `capital_cost` attribute and/or their `efficiency` attribute.\n", 966 | "- Add a **new storage technology** with cost, efficiency and loss parameters. This can be done with `n.add(\"StorageUnit\", ...)`. Look how it's done for batteries.\n", 967 | "- Successively increase the **CO2 price** and observe the emissions. This can be done by modifying the marginal cost of emitting generators, `n.generators.loc[\"hard coal\", \"marginal_cost\"]`.\n", 968 | "- Successively constrain the **CO2 emission limit**. This can be done by modifying the constant in `n.global_constraints`. The endogenous CO2 price can be found under `n.global_constraints.mu` (negative).\n", 969 | "- Enforce **renewable capacity targets** for wind and/or solar. This can be done by setting the `p_nom_min` attribute of the respective generators, e.g. `n.generators.loc[\"wind\", \"p_nom\"] = 100e3` for 100 GW.\n", 970 | "- Vary the **gas price**. This can be done by modifying the marginal cost of gas generators, `n.generators.loc[\"OCGT\", \"marginal_cost\"]` and `n.generators.loc[\"CCGT\", \"marginal_cost\"]`.\n", 971 | "- Constrain the availability of **sustainable biomass**. This can be done by setting the `e_sum_max` attribute of the biomass generator `n.generators.loc[\"biomass\", \"e_sum_max\"]`.\n", 972 | "- Add **upstream emissions** for the biomass (so that it is not fully carbon-neutral). You can set this in `n.carriers.loc[\"biomass\", \"co2_emissions\"]`.\n", 973 | "- Allow the **import of green hydrogen** at a given price, e.g. 100 €/MWh. You can also set a volume limit be setting `e_sum_max` (as for the biomass generator).\n", 974 | "- Vary the **hydrogen demand** relative to the electricity demand. This can be done by modifying `n.loads.loc[\"hydrogen demand\", \"p_set\"]`.\n", 975 | "- Add electricity demand from the **electrification of heating** with strong seasonal demand variation. This can be done with the commented-out code cell above.\n", 976 | "- Vary the **charging power** of the electric vehicles.\n", 977 | "- Vary the **battery size** of the electric vehicles.\n", 978 | "- Vary the **total number** of electric vehicles.\n", 979 | "- Vary the **electric vehicle state-of-charge** requirements for the mornings.\n", 980 | "- Vary the **share** of electric vehicles participating in smart charging and vehicle-to-grid.\n", 981 | "- Run the optimisation for different **weather years**. You can retrieve 44 years of wind and solar capacity factor time series for European countries from [renewables.ninja](https://renewables.ninja).\n", 982 | "\n", 983 | "\n", 984 | "**Feel free to explore other ideas as well!**" 985 | ] 986 | }, 987 | { 988 | "cell_type": "markdown", 989 | "id": "62884a86", 990 | "metadata": {}, 991 | "source": [] 992 | } 993 | ], 994 | "metadata": { 995 | "kernelspec": { 996 | "display_name": "sommerakademie-oxford (3.12.7)", 997 | "language": "python", 998 | "name": "python3" 999 | }, 1000 | "language_info": { 1001 | "codemirror_mode": { 1002 | "name": "ipython", 1003 | "version": 3 1004 | }, 1005 | "file_extension": ".py", 1006 | "mimetype": "text/x-python", 1007 | "name": "python", 1008 | "nbconvert_exporter": "python", 1009 | "pygments_lexer": "ipython3", 1010 | "version": "3.12.7" 1011 | } 1012 | }, 1013 | "nbformat": 4, 1014 | "nbformat_minor": 5 1015 | } 1016 | -------------------------------------------------------------------------------- /sommerakademie-oxford/03-pypsa-dispatch.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Electricity Markets" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | ":::{note}\n", 15 | "If you have not yet set up Python on your computer, you can execute this tutorial in your browser via [Google Colab](https://colab.research.google.com/). Click on the rocket in the top right corner and launch \"Colab\". If that doesn't work download the `.ipynb` file and import it in [Google Colab](https://colab.research.google.com/).\n", 16 | "Then install the following packages by executing the following command in a Jupyter cell at the top of the notebook.\n", 17 | "\n", 18 | "```sh\n", 19 | "!pip install -q pypsa highspy\n", 20 | "```\n", 21 | ":::" 22 | ] 23 | }, 24 | { 25 | "cell_type": "markdown", 26 | "metadata": {}, 27 | "source": [ 28 | "## What is PyPSA?\n", 29 | "\n", 30 | "PyPSA stands for Python for Power System Analysis. The [documentation](https://pypsa--1250.org.readthedocs.build/en/1250/) describes it as follows:\n", 31 | "\n", 32 | "> PyPSA is an open-source Python framework for optimising and simulating modern power and energy systems that include features such as conventional generators with unit commitment, variable wind and solar generation, hydro-electricity, inter-temporal storage, coupling to other energy sectors, elastic demands, and linearised power flow with loss approximations in DC and AC networks. PyPSA is designed to scale well with large networks and long time series. It is made for researchers, planners and utilities with basic coding aptitude who need a fast, easy-to-use and transparent tool for power and energy system analysis.\n", 33 | "\n", 34 | "For us, it's a tool to model electricity markets and investment planning as optimisation problems as discussed in the slides.\n", 35 | "\n", 36 | "PyPSA is a way to describe energy systems in Python code, interface with solver software, and analyse and visualise the results.\n", 37 | "\n", 38 | ":::{note}\n", 39 | "For a full reference to the optimisation problem description, see the [PyPSA User Guide](https://pypsa--1250.org.readthedocs.build/en/1250/user-guide/optimization/overview/)\n", 40 | ":::" 41 | ] 42 | }, 43 | { 44 | "cell_type": "markdown", 45 | "metadata": {}, 46 | "source": [ 47 | "## Input Data" 48 | ] 49 | }, 50 | { 51 | "cell_type": "markdown", 52 | "metadata": {}, 53 | "source": [ 54 | "Let's dive straight in and get acquainted with PyPSA by building simple electricity market models.\n", 55 | "\n", 56 | "We have the following data:\n", 57 | "\n", 58 | "- fuel costs in € / MWh$_{th}$ " 59 | ] 60 | }, 61 | { 62 | "cell_type": "code", 63 | "execution_count": 1, 64 | "metadata": { 65 | "tags": [] 66 | }, 67 | "outputs": [], 68 | "source": [ 69 | "fuel_cost = dict(\n", 70 | " coal=8,\n", 71 | " gas=100,\n", 72 | " oil=48,\n", 73 | " hydro=1,\n", 74 | " wind=0,\n", 75 | ")" 76 | ] 77 | }, 78 | { 79 | "cell_type": "markdown", 80 | "metadata": {}, 81 | "source": [ 82 | "- efficiencies of thermal power plants in MWh$_{el}$ / MWh$_{th}$" 83 | ] 84 | }, 85 | { 86 | "cell_type": "code", 87 | "execution_count": 2, 88 | "metadata": { 89 | "tags": [] 90 | }, 91 | "outputs": [], 92 | "source": [ 93 | "efficiency = dict(\n", 94 | " coal=0.33,\n", 95 | " gas=0.58,\n", 96 | " oil=0.35,\n", 97 | " hydro=1,\n", 98 | " wind=1,\n", 99 | ")" 100 | ] 101 | }, 102 | { 103 | "cell_type": "markdown", 104 | "metadata": {}, 105 | "source": [ 106 | "- specific emissions in t$_{CO_2}$ / MWh$_{th}$" 107 | ] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "execution_count": 3, 112 | "metadata": { 113 | "tags": [] 114 | }, 115 | "outputs": [], 116 | "source": [ 117 | "# t/MWh thermal\n", 118 | "emissions = dict(\n", 119 | " coal=0.34,\n", 120 | " gas=0.2,\n", 121 | " oil=0.26,\n", 122 | " hydro=0,\n", 123 | " wind=0,\n", 124 | ")" 125 | ] 126 | }, 127 | { 128 | "cell_type": "markdown", 129 | "metadata": {}, 130 | "source": [ 131 | "- power plant capacities in MW" 132 | ] 133 | }, 134 | { 135 | "cell_type": "code", 136 | "execution_count": 4, 137 | "metadata": { 138 | "tags": [] 139 | }, 140 | "outputs": [], 141 | "source": [ 142 | "power_plants = {\n", 143 | " \"A\": {\"coal\": 35000, \"wind\": 3000, \"gas\": 8000, \"oil\": 2000},\n", 144 | " \"B\": {\"hydro\": 1200},\n", 145 | "}" 146 | ] 147 | }, 148 | { 149 | "cell_type": "markdown", 150 | "metadata": {}, 151 | "source": [ 152 | "- fixed electrical load in MW" 153 | ] 154 | }, 155 | { 156 | "cell_type": "code", 157 | "execution_count": 5, 158 | "metadata": { 159 | "tags": [] 160 | }, 161 | "outputs": [], 162 | "source": [ 163 | "loads = {\n", 164 | " \"A\": 42000,\n", 165 | " \"B\": 650,\n", 166 | "}" 167 | ] 168 | }, 169 | { 170 | "cell_type": "markdown", 171 | "metadata": {}, 172 | "source": [ 173 | "## Building the Network" 174 | ] 175 | }, 176 | { 177 | "cell_type": "markdown", 178 | "metadata": {}, 179 | "source": [ 180 | "By convention, PyPSA is imported without an alias:" 181 | ] 182 | }, 183 | { 184 | "cell_type": "code", 185 | "execution_count": 6, 186 | "metadata": { 187 | "tags": [] 188 | }, 189 | "outputs": [], 190 | "source": [ 191 | "import pypsa" 192 | ] 193 | }, 194 | { 195 | "cell_type": "markdown", 196 | "metadata": {}, 197 | "source": [ 198 | "First, we create a new network object which serves as the overall container for all components." 199 | ] 200 | }, 201 | { 202 | "cell_type": "code", 203 | "execution_count": 7, 204 | "metadata": { 205 | "tags": [] 206 | }, 207 | "outputs": [], 208 | "source": [ 209 | "n = pypsa.Network()" 210 | ] 211 | }, 212 | { 213 | "cell_type": "markdown", 214 | "metadata": {}, 215 | "source": [ 216 | "The second component we need are buses. **Buses** are the fundamental nodes of the network, to which all other components like loads, generators and transmission lines attach. They enforce energy conservation for all elements feeding in and out of it.\n", 217 | "\n", 218 | "Components can be added to the network `n` using the `n.add()` function. It takes the component name as a first argument, the name of the component as a second argument and possibly further parameters as keyword arguments. Let's use this function, to add buses for each country to our network:" 219 | ] 220 | }, 221 | { 222 | "cell_type": "code", 223 | "execution_count": 9, 224 | "metadata": { 225 | "tags": [] 226 | }, 227 | "outputs": [ 228 | { 229 | "name": "stderr", 230 | "output_type": "stream", 231 | "text": [ 232 | "WARNING:pypsa.network.io:The following buses are already defined and will be skipped (use overwrite=True to overwrite): A\n", 233 | "WARNING:pypsa.network.io:The following buses are already defined and will be skipped (use overwrite=True to overwrite): B\n" 234 | ] 235 | } 236 | ], 237 | "source": [ 238 | "n.add(\"Bus\", \"A\", v_nom=380, carrier=\"AC\")\n", 239 | "n.add(\"Bus\", \"B\", v_nom=380, carrier=\"AC\");" 240 | ] 241 | }, 242 | { 243 | "cell_type": "markdown", 244 | "metadata": {}, 245 | "source": [ 246 | "For each class of components, the data describing the components is stored in a `pandas.DataFrame`. For example, all static data for buses is stored in `n.buses`" 247 | ] 248 | }, 249 | { 250 | "cell_type": "code", 251 | "execution_count": 10, 252 | "metadata": { 253 | "tags": [] 254 | }, 255 | "outputs": [ 256 | { 257 | "data": { 258 | "text/html": [ 259 | "
\n", 260 | "\n", 273 | "\n", 274 | " \n", 275 | " \n", 276 | " \n", 277 | " \n", 278 | " \n", 279 | " \n", 280 | " \n", 281 | " \n", 282 | " \n", 283 | " \n", 284 | " \n", 285 | " \n", 286 | " \n", 287 | " \n", 288 | " \n", 289 | " \n", 290 | " \n", 291 | " \n", 292 | " \n", 293 | " \n", 294 | " \n", 295 | " \n", 296 | " \n", 297 | " \n", 298 | " \n", 299 | " \n", 300 | " \n", 301 | " \n", 302 | " \n", 303 | " \n", 304 | " \n", 305 | " \n", 306 | " \n", 307 | " \n", 308 | " \n", 309 | " \n", 310 | " \n", 311 | " \n", 312 | " \n", 313 | " \n", 314 | " \n", 315 | " \n", 316 | " \n", 317 | " \n", 318 | " \n", 319 | " \n", 320 | " \n", 321 | " \n", 322 | " \n", 323 | " \n", 324 | " \n", 325 | " \n", 326 | " \n", 327 | " \n", 328 | " \n", 329 | " \n", 330 | " \n", 331 | " \n", 332 | " \n", 333 | " \n", 334 | " \n", 335 | " \n", 336 | " \n", 337 | " \n", 338 | " \n", 339 | " \n", 340 | " \n", 341 | " \n", 342 | "
v_nomtypexycarrierunitlocationv_mag_pu_setv_mag_pu_minv_mag_pu_maxcontrolgeneratorsub_network
name
A380.00.00.0AC1.00.0infPQ
B380.00.00.0AC1.00.0infPQ
\n", 343 | "
" 344 | ], 345 | "text/plain": [ 346 | " v_nom type x y carrier unit location v_mag_pu_set v_mag_pu_min \\\n", 347 | "name \n", 348 | "A 380.0 0.0 0.0 AC 1.0 0.0 \n", 349 | "B 380.0 0.0 0.0 AC 1.0 0.0 \n", 350 | "\n", 351 | " v_mag_pu_max control generator sub_network \n", 352 | "name \n", 353 | "A inf PQ \n", 354 | "B inf PQ " 355 | ] 356 | }, 357 | "execution_count": 10, 358 | "metadata": {}, 359 | "output_type": "execute_result" 360 | } 361 | ], 362 | "source": [ 363 | "n.buses" 364 | ] 365 | }, 366 | { 367 | "cell_type": "markdown", 368 | "metadata": {}, 369 | "source": [ 370 | "You see there are many more attributes than we specified while adding the buses; many of them are filled with default parameters which were added. You can look up the field description, defaults and status (required input, optional input, output) for buses here https://pypsa--1250.org.readthedocs.build/en/1250/user-guide/components/buses/, and analogous for all other components. " 371 | ] 372 | }, 373 | { 374 | "cell_type": "markdown", 375 | "metadata": {}, 376 | "source": [ 377 | "The method `n.add()` also allows you to add multiple components at once. For instance, multiple **carriers** for the fuels with information on specific carbon dioxide emissions, a nice name, and colors for plotting. For this, the function takes the component name as the first argument and then a list of component names and then optional arguments for the parameters. Here, scalar values, lists, dictionary or `pandas.Series` are allowed. The latter two needs keys or indices with the component names." 378 | ] 379 | }, 380 | { 381 | "cell_type": "code", 382 | "execution_count": 11, 383 | "metadata": { 384 | "tags": [] 385 | }, 386 | "outputs": [], 387 | "source": [ 388 | "n.add(\n", 389 | " \"Carrier\",\n", 390 | " [\"coal\", \"gas\", \"oil\", \"hydro\", \"wind\"],\n", 391 | " co2_emissions=emissions,\n", 392 | " nice_name=[\"Coal\", \"Gas\", \"Oil\", \"Hydro\", \"Onshore Wind\"],\n", 393 | " color=[\"grey\", \"indianred\", \"black\", \"aquamarine\", \"dodgerblue\"],\n", 394 | ");" 395 | ] 396 | }, 397 | { 398 | "cell_type": "markdown", 399 | "metadata": {}, 400 | "source": [ 401 | "The `n.add()` function is very general. It lets you add any component to the network object `n`. For instance, in the next step we add **generators** for all the different power plants." 402 | ] 403 | }, 404 | { 405 | "cell_type": "markdown", 406 | "metadata": {}, 407 | "source": [ 408 | "In Region B:" 409 | ] 410 | }, 411 | { 412 | "cell_type": "code", 413 | "execution_count": 12, 414 | "metadata": { 415 | "tags": [] 416 | }, 417 | "outputs": [], 418 | "source": [ 419 | "n.add(\n", 420 | " \"Generator\",\n", 421 | " \"B hydro\",\n", 422 | " bus=\"B\",\n", 423 | " carrier=\"hydro\",\n", 424 | " p_nom=1200, # MW\n", 425 | ");" 426 | ] 427 | }, 428 | { 429 | "cell_type": "markdown", 430 | "metadata": {}, 431 | "source": [ 432 | "In Region A (in a loop):" 433 | ] 434 | }, 435 | { 436 | "cell_type": "code", 437 | "execution_count": 13, 438 | "metadata": { 439 | "tags": [] 440 | }, 441 | "outputs": [], 442 | "source": [ 443 | "for tech, p_nom in power_plants[\"A\"].items():\n", 444 | " n.add(\n", 445 | " \"Generator\",\n", 446 | " f\"A {tech}\",\n", 447 | " bus=\"A\",\n", 448 | " carrier=tech,\n", 449 | " efficiency=efficiency.get(tech, 1),\n", 450 | " p_nom=p_nom,\n", 451 | " marginal_cost=fuel_cost.get(tech, 0) / efficiency.get(tech, 1),\n", 452 | " )" 453 | ] 454 | }, 455 | { 456 | "cell_type": "markdown", 457 | "metadata": {}, 458 | "source": [ 459 | "As a result, the `n.generators` DataFrame looks like this:" 460 | ] 461 | }, 462 | { 463 | "cell_type": "code", 464 | "execution_count": 14, 465 | "metadata": { 466 | "tags": [] 467 | }, 468 | "outputs": [ 469 | { 470 | "data": { 471 | "text/plain": [ 472 | "name\n", 473 | "B hydro 0.000000\n", 474 | "A coal 24.242424\n", 475 | "A wind 0.000000\n", 476 | "A gas 172.413793\n", 477 | "A oil 137.142857\n", 478 | "Name: marginal_cost, dtype: float64" 479 | ] 480 | }, 481 | "execution_count": 14, 482 | "metadata": {}, 483 | "output_type": "execute_result" 484 | } 485 | ], 486 | "source": [ 487 | "n.generators[\"marginal_cost\"]" 488 | ] 489 | }, 490 | { 491 | "cell_type": "markdown", 492 | "metadata": {}, 493 | "source": [ 494 | "Next, we're going to add the electricity demand.\n", 495 | "\n", 496 | "A positive value for `p_set` means consumption of power from the bus." 497 | ] 498 | }, 499 | { 500 | "cell_type": "code", 501 | "execution_count": 15, 502 | "metadata": { 503 | "tags": [] 504 | }, 505 | "outputs": [], 506 | "source": [ 507 | "n.add(\n", 508 | " \"Load\",\n", 509 | " \"A electricity demand\",\n", 510 | " bus=\"A\",\n", 511 | " p_set=loads[\"A\"],\n", 512 | ");" 513 | ] 514 | }, 515 | { 516 | "cell_type": "code", 517 | "execution_count": 16, 518 | "metadata": { 519 | "tags": [] 520 | }, 521 | "outputs": [], 522 | "source": [ 523 | "n.add(\n", 524 | " \"Load\",\n", 525 | " \"B electricity demand\",\n", 526 | " bus=\"B\",\n", 527 | " p_set=loads[\"B\"],\n", 528 | ");" 529 | ] 530 | }, 531 | { 532 | "cell_type": "code", 533 | "execution_count": 17, 534 | "metadata": { 535 | "tags": [] 536 | }, 537 | "outputs": [ 538 | { 539 | "data": { 540 | "text/html": [ 541 | "
\n", 542 | "\n", 555 | "\n", 556 | " \n", 557 | " \n", 558 | " \n", 559 | " \n", 560 | " \n", 561 | " \n", 562 | " \n", 563 | " \n", 564 | " \n", 565 | " \n", 566 | " \n", 567 | " \n", 568 | " \n", 569 | " \n", 570 | " \n", 571 | " \n", 572 | " \n", 573 | " \n", 574 | " \n", 575 | " \n", 576 | " \n", 577 | " \n", 578 | " \n", 579 | " \n", 580 | " \n", 581 | " \n", 582 | " \n", 583 | " \n", 584 | " \n", 585 | " \n", 586 | " \n", 587 | " \n", 588 | " \n", 589 | " \n", 590 | " \n", 591 | " \n", 592 | " \n", 593 | " \n", 594 | " \n", 595 | " \n", 596 | " \n", 597 | " \n", 598 | " \n", 599 | " \n", 600 | "
buscarriertypep_setq_setsignactive
name
A electricity demandA42000.00.0-1.0True
B electricity demandB650.00.0-1.0True
\n", 601 | "
" 602 | ], 603 | "text/plain": [ 604 | " bus carrier type p_set q_set sign active\n", 605 | "name \n", 606 | "A electricity demand A 42000.0 0.0 -1.0 True\n", 607 | "B electricity demand B 650.0 0.0 -1.0 True" 608 | ] 609 | }, 610 | "execution_count": 17, 611 | "metadata": {}, 612 | "output_type": "execute_result" 613 | } 614 | ], 615 | "source": [ 616 | "n.loads" 617 | ] 618 | }, 619 | { 620 | "cell_type": "markdown", 621 | "metadata": {}, 622 | "source": [ 623 | "Finally, we add the connection between Region B and Region A with a 500 MW transmission line, modelled as a link:" 624 | ] 625 | }, 626 | { 627 | "cell_type": "code", 628 | "execution_count": null, 629 | "metadata": { 630 | "tags": [] 631 | }, 632 | "outputs": [], 633 | "source": [ 634 | "n.add(\n", 635 | " \"Link\",\n", 636 | " \"A-B\",\n", 637 | " bus0=\"A\",\n", 638 | " bus1=\"B\",\n", 639 | " p_min_pu=-1,\n", 640 | " p_nom=500,\n", 641 | ");" 642 | ] 643 | }, 644 | { 645 | "cell_type": "code", 646 | "execution_count": null, 647 | "metadata": { 648 | "tags": [] 649 | }, 650 | "outputs": [ 651 | { 652 | "data": { 653 | "text/html": [ 654 | "
\n", 655 | "\n", 668 | "\n", 669 | " \n", 670 | " \n", 671 | " \n", 672 | " \n", 673 | " \n", 674 | " \n", 675 | " \n", 676 | " \n", 677 | " \n", 678 | " \n", 679 | " \n", 680 | " \n", 681 | " \n", 682 | " \n", 683 | " \n", 684 | " \n", 685 | " \n", 686 | " \n", 687 | " \n", 688 | " \n", 689 | " \n", 690 | " \n", 691 | " \n", 692 | " \n", 693 | " \n", 694 | " \n", 695 | " \n", 696 | " \n", 697 | " \n", 698 | " \n", 699 | " \n", 700 | " \n", 701 | " \n", 702 | " \n", 703 | " \n", 704 | " \n", 705 | " \n", 706 | " \n", 707 | " \n", 708 | " \n", 709 | " \n", 710 | " \n", 711 | " \n", 712 | " \n", 713 | " \n", 714 | " \n", 715 | " \n", 716 | " \n", 717 | " \n", 718 | " \n", 719 | " \n", 720 | " \n", 721 | " \n", 722 | " \n", 723 | " \n", 724 | " \n", 725 | " \n", 726 | " \n", 727 | " \n", 728 | " \n", 729 | " \n", 730 | " \n", 731 | " \n", 732 | " \n", 733 | " \n", 734 | " \n", 735 | " \n", 736 | " \n", 737 | " \n", 738 | " \n", 739 | " \n", 740 | " \n", 741 | " \n", 742 | " \n", 743 | " \n", 744 | " \n", 745 | "
bus0bus1typexrgbs_noms_nom_mods_nom_extendable...v_ang_minv_ang_maxsub_networkx_pur_pug_pub_pux_pu_effr_pu_effs_nom_opt
name
A-BAB1.01.00.00.0500.00.0False...-infinf0.00.00.00.00.00.00.0
\n", 746 | "

1 rows × 31 columns

\n", 747 | "
" 748 | ], 749 | "text/plain": [ 750 | " bus0 bus1 type x r g b s_nom s_nom_mod s_nom_extendable \\\n", 751 | "name \n", 752 | "A-B A B 1.0 1.0 0.0 0.0 500.0 0.0 False \n", 753 | "\n", 754 | " ... v_ang_min v_ang_max sub_network x_pu r_pu g_pu b_pu \\\n", 755 | "name ... \n", 756 | "A-B ... -inf inf 0.0 0.0 0.0 0.0 \n", 757 | "\n", 758 | " x_pu_eff r_pu_eff s_nom_opt \n", 759 | "name \n", 760 | "A-B 0.0 0.0 0.0 \n", 761 | "\n", 762 | "[1 rows x 31 columns]" 763 | ] 764 | }, 765 | "execution_count": 19, 766 | "metadata": {}, 767 | "output_type": "execute_result" 768 | } 769 | ], 770 | "source": [ 771 | "n.links" 772 | ] 773 | }, 774 | { 775 | "cell_type": "markdown", 776 | "metadata": {}, 777 | "source": [ 778 | "## Optimisation" 779 | ] 780 | }, 781 | { 782 | "cell_type": "markdown", 783 | "metadata": {}, 784 | "source": [ 785 | "With all input data transferred into PyPSA's data structure, we can now build and run the resulting optimisation problem. In PyPSA, building, solving and retrieving results from the optimisation model is contained in a single function call `n.optimize()`. This function optimizes dispatch and investment decisions for least cost.\n", 786 | "\n", 787 | "The `n.optimize()` function can take a variety of arguments. The most relevant for the moment is the choice of the solver (e.g. \"highs\" and \"gurobi\"). They need to be installed on your computer, to use them here!" 788 | ] 789 | }, 790 | { 791 | "cell_type": "code", 792 | "execution_count": 20, 793 | "metadata": { 794 | "tags": [ 795 | "hide-output" 796 | ] 797 | }, 798 | "outputs": [ 799 | { 800 | "name": "stderr", 801 | "output_type": "stream", 802 | "text": [ 803 | "WARNING:pypsa.consistency:The following buses have carriers which are not defined:\n", 804 | "Index(['A', 'B'], dtype='object', name='name')\n", 805 | "WARNING:pypsa.consistency:The following lines have carriers which are not defined:\n", 806 | "Index(['A-B'], dtype='object', name='name')\n", 807 | "INFO:linopy.model: Solve problem using Highs solver\n", 808 | "INFO:linopy.model:Solver options:\n", 809 | " - log_to_console: False\n", 810 | "INFO:linopy.io: Writing time: 0.01s\n", 811 | "INFO:linopy.constants: Optimization successful: \n", 812 | "Status: ok\n", 813 | "Termination condition: optimal\n", 814 | "Solution: 6 primals, 14 duals\n", 815 | "Objective: 1.38e+06\n", 816 | "Solver model: available\n", 817 | "Solver message: Optimal\n", 818 | "\n", 819 | "INFO:pypsa.optimization.optimize:The shadow-prices of the constraints Generator-fix-p-lower, Generator-fix-p-upper, Line-fix-s-lower, Line-fix-s-upper were not assigned to the network.\n" 820 | ] 821 | }, 822 | { 823 | "name": "stdout", 824 | "output_type": "stream", 825 | "text": [ 826 | "Running HiGHS 1.11.0 (git hash: 364c83a): Copyright (c) 2025 HiGHS under MIT licence terms\n" 827 | ] 828 | }, 829 | { 830 | "data": { 831 | "text/plain": [ 832 | "('ok', 'optimal')" 833 | ] 834 | }, 835 | "execution_count": 20, 836 | "metadata": {}, 837 | "output_type": "execute_result" 838 | } 839 | ], 840 | "source": [ 841 | "n.optimize(log_to_console=False)" 842 | ] 843 | }, 844 | { 845 | "cell_type": "markdown", 846 | "metadata": {}, 847 | "source": [ 848 | "Let's have a look at the results.\n", 849 | "\n", 850 | "Since the power flow and dispatch are generally time-varying quantities, these are stored in a different location than e.g. `n.generators`. They are stored in `n.generators_t`. Thus, to find out the dispatch of the generators, run" 851 | ] 852 | }, 853 | { 854 | "cell_type": "code", 855 | "execution_count": 21, 856 | "metadata": {}, 857 | "outputs": [ 858 | { 859 | "data": { 860 | "text/html": [ 861 | "
\n", 862 | "\n", 875 | "\n", 876 | " \n", 877 | " \n", 878 | " \n", 879 | " \n", 880 | " \n", 881 | " \n", 882 | " \n", 883 | " \n", 884 | " \n", 885 | " \n", 886 | " \n", 887 | " \n", 888 | " \n", 889 | " \n", 890 | " \n", 891 | " \n", 892 | " \n", 893 | " \n", 894 | " \n", 895 | " \n", 896 | " \n", 897 | " \n", 898 | " \n", 899 | " \n", 900 | " \n", 901 | " \n", 902 | " \n", 903 | " \n", 904 | "
nameB hydroA coalA windA gasA oil
snapshot
now1150.035000.03000.01500.02000.0
\n", 905 | "
" 906 | ], 907 | "text/plain": [ 908 | "name B hydro A coal A wind A gas A oil\n", 909 | "snapshot \n", 910 | "now 1150.0 35000.0 3000.0 1500.0 2000.0" 911 | ] 912 | }, 913 | "execution_count": 21, 914 | "metadata": {}, 915 | "output_type": "execute_result" 916 | } 917 | ], 918 | "source": [ 919 | "n.generators_t.p" 920 | ] 921 | }, 922 | { 923 | "cell_type": "markdown", 924 | "metadata": {}, 925 | "source": [ 926 | "or if you prefer it in relation to the generators nominal capacity" 927 | ] 928 | }, 929 | { 930 | "cell_type": "code", 931 | "execution_count": 22, 932 | "metadata": {}, 933 | "outputs": [ 934 | { 935 | "data": { 936 | "text/html": [ 937 | "
\n", 938 | "\n", 951 | "\n", 952 | " \n", 953 | " \n", 954 | " \n", 955 | " \n", 956 | " \n", 957 | " \n", 958 | " \n", 959 | " \n", 960 | " \n", 961 | " \n", 962 | " \n", 963 | " \n", 964 | " \n", 965 | " \n", 966 | " \n", 967 | " \n", 968 | " \n", 969 | " \n", 970 | " \n", 971 | " \n", 972 | " \n", 973 | " \n", 974 | " \n", 975 | " \n", 976 | " \n", 977 | " \n", 978 | " \n", 979 | " \n", 980 | "
nameB hydroA coalA windA gasA oil
snapshot
now0.9583331.01.00.18751.0
\n", 981 | "
" 982 | ], 983 | "text/plain": [ 984 | "name B hydro A coal A wind A gas A oil\n", 985 | "snapshot \n", 986 | "now 0.958333 1.0 1.0 0.1875 1.0" 987 | ] 988 | }, 989 | "execution_count": 22, 990 | "metadata": {}, 991 | "output_type": "execute_result" 992 | } 993 | ], 994 | "source": [ 995 | "n.generators_t.p / n.generators.p_nom" 996 | ] 997 | }, 998 | { 999 | "cell_type": "markdown", 1000 | "metadata": {}, 1001 | "source": [ 1002 | "You see that the time index has the value 'now'. This is the default index when no time series data has been specified and the network only covers a single state (e.g. a particular hour). \n", 1003 | "\n", 1004 | "Similarly you will find the power flow in transmission lines at" 1005 | ] 1006 | }, 1007 | { 1008 | "cell_type": "code", 1009 | "execution_count": 23, 1010 | "metadata": {}, 1011 | "outputs": [ 1012 | { 1013 | "data": { 1014 | "text/html": [ 1015 | "
\n", 1016 | "\n", 1029 | "\n", 1030 | " \n", 1031 | " \n", 1032 | " \n", 1033 | " \n", 1034 | " \n", 1035 | " \n", 1036 | " \n", 1037 | " \n", 1038 | " \n", 1039 | " \n", 1040 | " \n", 1041 | " \n", 1042 | " \n", 1043 | " \n", 1044 | " \n", 1045 | " \n", 1046 | "
nameA-B
snapshot
now-500.0
\n", 1047 | "
" 1048 | ], 1049 | "text/plain": [ 1050 | "name A-B\n", 1051 | "snapshot \n", 1052 | "now -500.0" 1053 | ] 1054 | }, 1055 | "execution_count": 23, 1056 | "metadata": {}, 1057 | "output_type": "execute_result" 1058 | } 1059 | ], 1060 | "source": [ 1061 | "n.lines_t.p0" 1062 | ] 1063 | }, 1064 | { 1065 | "cell_type": "code", 1066 | "execution_count": 24, 1067 | "metadata": {}, 1068 | "outputs": [ 1069 | { 1070 | "data": { 1071 | "text/html": [ 1072 | "
\n", 1073 | "\n", 1086 | "\n", 1087 | " \n", 1088 | " \n", 1089 | " \n", 1090 | " \n", 1091 | " \n", 1092 | " \n", 1093 | " \n", 1094 | " \n", 1095 | " \n", 1096 | " \n", 1097 | " \n", 1098 | " \n", 1099 | " \n", 1100 | " \n", 1101 | " \n", 1102 | " \n", 1103 | "
nameA-B
snapshot
now500.0
\n", 1104 | "
" 1105 | ], 1106 | "text/plain": [ 1107 | "name A-B\n", 1108 | "snapshot \n", 1109 | "now 500.0" 1110 | ] 1111 | }, 1112 | "execution_count": 24, 1113 | "metadata": {}, 1114 | "output_type": "execute_result" 1115 | } 1116 | ], 1117 | "source": [ 1118 | "n.lines_t.p1" 1119 | ] 1120 | }, 1121 | { 1122 | "cell_type": "markdown", 1123 | "metadata": {}, 1124 | "source": [ 1125 | "The `p0` will tell you the flow from `bus0` to `bus1`. `p1` will tell you the flow from `bus1` to `bus0`." 1126 | ] 1127 | }, 1128 | { 1129 | "cell_type": "markdown", 1130 | "metadata": {}, 1131 | "source": [ 1132 | "What about the _market clearing prices_ in the electricity market?" 1133 | ] 1134 | }, 1135 | { 1136 | "cell_type": "code", 1137 | "execution_count": 25, 1138 | "metadata": {}, 1139 | "outputs": [ 1140 | { 1141 | "data": { 1142 | "text/html": [ 1143 | "
\n", 1144 | "\n", 1157 | "\n", 1158 | " \n", 1159 | " \n", 1160 | " \n", 1161 | " \n", 1162 | " \n", 1163 | " \n", 1164 | " \n", 1165 | " \n", 1166 | " \n", 1167 | " \n", 1168 | " \n", 1169 | " \n", 1170 | " \n", 1171 | " \n", 1172 | " \n", 1173 | " \n", 1174 | " \n", 1175 | " \n", 1176 | " \n", 1177 | "
nameAB
snapshot
now172.413793-0.0
\n", 1178 | "
" 1179 | ], 1180 | "text/plain": [ 1181 | "name A B\n", 1182 | "snapshot \n", 1183 | "now 172.413793 -0.0" 1184 | ] 1185 | }, 1186 | "execution_count": 25, 1187 | "metadata": {}, 1188 | "output_type": "execute_result" 1189 | } 1190 | ], 1191 | "source": [ 1192 | "n.buses_t.marginal_price" 1193 | ] 1194 | }, 1195 | { 1196 | "cell_type": "markdown", 1197 | "metadata": { 1198 | "tags": [] 1199 | }, 1200 | "source": [ 1201 | "## Modifying networks" 1202 | ] 1203 | }, 1204 | { 1205 | "cell_type": "markdown", 1206 | "metadata": {}, 1207 | "source": [ 1208 | "Modifying data of components in an existing PyPSA network can be done by modifying the entries of a `pandas.DataFrame`. For instance, if we want to reduce the cross-border transmission capacity between Region A and Region B, we'd run:" 1209 | ] 1210 | }, 1211 | { 1212 | "cell_type": "code", 1213 | "execution_count": null, 1214 | "metadata": {}, 1215 | "outputs": [], 1216 | "source": [ 1217 | "n.links.loc[\"A-B\", \"p_nom\"] = 400" 1218 | ] 1219 | }, 1220 | { 1221 | "cell_type": "code", 1222 | "execution_count": null, 1223 | "metadata": {}, 1224 | "outputs": [ 1225 | { 1226 | "data": { 1227 | "text/html": [ 1228 | "
\n", 1229 | "\n", 1242 | "\n", 1243 | " \n", 1244 | " \n", 1245 | " \n", 1246 | " \n", 1247 | " \n", 1248 | " \n", 1249 | " \n", 1250 | " \n", 1251 | " \n", 1252 | " \n", 1253 | " \n", 1254 | " \n", 1255 | " \n", 1256 | " \n", 1257 | " \n", 1258 | " \n", 1259 | " \n", 1260 | " \n", 1261 | " \n", 1262 | " \n", 1263 | " \n", 1264 | " \n", 1265 | " \n", 1266 | " \n", 1267 | " \n", 1268 | " \n", 1269 | " \n", 1270 | " \n", 1271 | " \n", 1272 | " \n", 1273 | " \n", 1274 | " \n", 1275 | " \n", 1276 | " \n", 1277 | " \n", 1278 | " \n", 1279 | " \n", 1280 | " \n", 1281 | " \n", 1282 | " \n", 1283 | " \n", 1284 | " \n", 1285 | " \n", 1286 | " \n", 1287 | " \n", 1288 | " \n", 1289 | " \n", 1290 | " \n", 1291 | " \n", 1292 | " \n", 1293 | " \n", 1294 | " \n", 1295 | " \n", 1296 | " \n", 1297 | " \n", 1298 | " \n", 1299 | " \n", 1300 | " \n", 1301 | " \n", 1302 | " \n", 1303 | " \n", 1304 | " \n", 1305 | " \n", 1306 | " \n", 1307 | " \n", 1308 | " \n", 1309 | " \n", 1310 | " \n", 1311 | " \n", 1312 | " \n", 1313 | " \n", 1314 | " \n", 1315 | " \n", 1316 | " \n", 1317 | " \n", 1318 | " \n", 1319 | "
bus0bus1typexrgbs_noms_nom_mods_nom_extendable...v_ang_maxsub_networkx_pur_pug_pub_pux_pu_effr_pu_effs_nom_optv_nom
name
A-BAB1.01.00.00.0400.00.0False...inf00.0000070.0000070.00.00.0000070.000007500.0380.0
\n", 1320 | "

1 rows × 32 columns

\n", 1321 | "
" 1322 | ], 1323 | "text/plain": [ 1324 | " bus0 bus1 type x r g b s_nom s_nom_mod s_nom_extendable \\\n", 1325 | "name \n", 1326 | "A-B A B 1.0 1.0 0.0 0.0 400.0 0.0 False \n", 1327 | "\n", 1328 | " ... v_ang_max sub_network x_pu r_pu g_pu b_pu x_pu_eff \\\n", 1329 | "name ... \n", 1330 | "A-B ... inf 0 0.000007 0.000007 0.0 0.0 0.000007 \n", 1331 | "\n", 1332 | " r_pu_eff s_nom_opt v_nom \n", 1333 | "name \n", 1334 | "A-B 0.000007 500.0 380.0 \n", 1335 | "\n", 1336 | "[1 rows x 32 columns]" 1337 | ] 1338 | }, 1339 | "execution_count": 27, 1340 | "metadata": {}, 1341 | "output_type": "execute_result" 1342 | } 1343 | ], 1344 | "source": [ 1345 | "n.links" 1346 | ] 1347 | }, 1348 | { 1349 | "cell_type": "code", 1350 | "execution_count": 28, 1351 | "metadata": { 1352 | "tags": [ 1353 | "hide-output" 1354 | ] 1355 | }, 1356 | "outputs": [ 1357 | { 1358 | "name": "stderr", 1359 | "output_type": "stream", 1360 | "text": [ 1361 | "WARNING:pypsa.consistency:The following sub_networks have carriers which are not defined:\n", 1362 | "Index(['0'], dtype='object', name='name')\n", 1363 | "WARNING:pypsa.consistency:The following buses have carriers which are not defined:\n", 1364 | "Index(['A', 'B'], dtype='object', name='name')\n", 1365 | "WARNING:pypsa.consistency:The following lines have carriers which are not defined:\n", 1366 | "Index(['A-B'], dtype='object', name='name')\n", 1367 | "INFO:linopy.model: Solve problem using Highs solver\n", 1368 | "INFO:linopy.io: Writing time: 0.01s\n", 1369 | "INFO:linopy.constants: Optimization successful: \n", 1370 | "Status: ok\n", 1371 | "Termination condition: optimal\n", 1372 | "Solution: 6 primals, 14 duals\n", 1373 | "Objective: 1.40e+06\n", 1374 | "Solver model: available\n", 1375 | "Solver message: Optimal\n", 1376 | "\n", 1377 | "INFO:pypsa.optimization.optimize:The shadow-prices of the constraints Generator-fix-p-lower, Generator-fix-p-upper, Line-fix-s-lower, Line-fix-s-upper were not assigned to the network.\n" 1378 | ] 1379 | }, 1380 | { 1381 | "name": "stdout", 1382 | "output_type": "stream", 1383 | "text": [ 1384 | "Running HiGHS 1.11.0 (git hash: 364c83a): Copyright (c) 2025 HiGHS under MIT licence terms\n", 1385 | "LP linopy-problem-fdmy6yxt has 14 rows; 6 cols; 19 nonzeros\n", 1386 | "Coefficient ranges:\n", 1387 | " Matrix [1e+00, 1e+00]\n", 1388 | " Cost [2e+01, 2e+02]\n", 1389 | " Bound [0e+00, 0e+00]\n", 1390 | " RHS [4e+02, 4e+04]\n", 1391 | "Presolving model\n", 1392 | "1 rows, 2 cols, 2 nonzeros 0s\n", 1393 | "0 rows, 0 cols, 0 nonzeros 0s\n", 1394 | "Presolve : Reductions: rows 0(-14); columns 0(-6); elements 0(-19) - Reduced to empty\n", 1395 | "Solving the original LP from the solution after postsolve\n", 1396 | "Model name : linopy-problem-fdmy6yxt\n", 1397 | "Model status : Optimal\n", 1398 | "Objective value : 1.3986326317e+06\n", 1399 | "P-D objective error : 1.6647013314e-16\n", 1400 | "HiGHS run time : 0.00\n", 1401 | "Writing the solution to /tmp/linopy-solve-h2oo98_s.sol\n" 1402 | ] 1403 | }, 1404 | { 1405 | "data": { 1406 | "text/plain": [ 1407 | "('ok', 'optimal')" 1408 | ] 1409 | }, 1410 | "execution_count": 28, 1411 | "metadata": {}, 1412 | "output_type": "execute_result" 1413 | } 1414 | ], 1415 | "source": [ 1416 | "n.optimize(solver_name=\"highs\")" 1417 | ] 1418 | }, 1419 | { 1420 | "cell_type": "markdown", 1421 | "metadata": {}, 1422 | "source": [ 1423 | "You can see that the production of the hydro power plant was reduced and that of the gas power plant increased owing to the reduced transmission capacity." 1424 | ] 1425 | }, 1426 | { 1427 | "cell_type": "code", 1428 | "execution_count": 29, 1429 | "metadata": {}, 1430 | "outputs": [ 1431 | { 1432 | "data": { 1433 | "text/html": [ 1434 | "
\n", 1435 | "\n", 1448 | "\n", 1449 | " \n", 1450 | " \n", 1451 | " \n", 1452 | " \n", 1453 | " \n", 1454 | " \n", 1455 | " \n", 1456 | " \n", 1457 | " \n", 1458 | " \n", 1459 | " \n", 1460 | " \n", 1461 | " \n", 1462 | " \n", 1463 | " \n", 1464 | " \n", 1465 | " \n", 1466 | " \n", 1467 | " \n", 1468 | " \n", 1469 | " \n", 1470 | " \n", 1471 | " \n", 1472 | " \n", 1473 | " \n", 1474 | " \n", 1475 | " \n", 1476 | " \n", 1477 | "
nameB hydroA coalA windA gasA oil
snapshot
now1050.035000.03000.01600.02000.0
\n", 1478 | "
" 1479 | ], 1480 | "text/plain": [ 1481 | "name B hydro A coal A wind A gas A oil\n", 1482 | "snapshot \n", 1483 | "now 1050.0 35000.0 3000.0 1600.0 2000.0" 1484 | ] 1485 | }, 1486 | "execution_count": 29, 1487 | "metadata": {}, 1488 | "output_type": "execute_result" 1489 | } 1490 | ], 1491 | "source": [ 1492 | "n.generators_t.p" 1493 | ] 1494 | }, 1495 | { 1496 | "cell_type": "markdown", 1497 | "metadata": {}, 1498 | "source": [ 1499 | "## Exploring the optimization model" 1500 | ] 1501 | }, 1502 | { 1503 | "cell_type": "code", 1504 | "execution_count": 30, 1505 | "metadata": {}, 1506 | "outputs": [ 1507 | { 1508 | "data": { 1509 | "text/plain": [ 1510 | "Linopy LP model\n", 1511 | "===============\n", 1512 | "\n", 1513 | "Variables:\n", 1514 | "----------\n", 1515 | " * Generator-p (snapshot, name)\n", 1516 | " * Line-s (snapshot, name)\n", 1517 | "\n", 1518 | "Constraints:\n", 1519 | "------------\n", 1520 | " * Generator-fix-p-lower (snapshot, name)\n", 1521 | " * Generator-fix-p-upper (snapshot, name)\n", 1522 | " * Line-fix-s-lower (snapshot, name)\n", 1523 | " * Line-fix-s-upper (snapshot, name)\n", 1524 | " * Bus-nodal_balance (name, snapshot)\n", 1525 | "\n", 1526 | "Status:\n", 1527 | "-------\n", 1528 | "ok" 1529 | ] 1530 | }, 1531 | "execution_count": 30, 1532 | "metadata": {}, 1533 | "output_type": "execute_result" 1534 | } 1535 | ], 1536 | "source": [ 1537 | "n.model" 1538 | ] 1539 | }, 1540 | { 1541 | "cell_type": "code", 1542 | "execution_count": 31, 1543 | "metadata": {}, 1544 | "outputs": [ 1545 | { 1546 | "data": { 1547 | "text/plain": [ 1548 | "Constraint `Generator-fix-p-upper` [snapshot: 1, name: 5]:\n", 1549 | "----------------------------------------------------------\n", 1550 | "[now, B hydro]: +1 Generator-p[now, B hydro] ≤ 1200.0\n", 1551 | "[now, A coal]: +1 Generator-p[now, A coal] ≤ 35000.0\n", 1552 | "[now, A wind]: +1 Generator-p[now, A wind] ≤ 3000.0\n", 1553 | "[now, A gas]: +1 Generator-p[now, A gas] ≤ 8000.0\n", 1554 | "[now, A oil]: +1 Generator-p[now, A oil] ≤ 2000.0" 1555 | ] 1556 | }, 1557 | "execution_count": 31, 1558 | "metadata": {}, 1559 | "output_type": "execute_result" 1560 | } 1561 | ], 1562 | "source": [ 1563 | "n.model.constraints[\"Generator-fix-p-upper\"]" 1564 | ] 1565 | }, 1566 | { 1567 | "cell_type": "code", 1568 | "execution_count": 32, 1569 | "metadata": {}, 1570 | "outputs": [ 1571 | { 1572 | "data": { 1573 | "text/plain": [ 1574 | "Constraint `Bus-nodal_balance` [name: 2, snapshot: 1]:\n", 1575 | "------------------------------------------------------\n", 1576 | "[A, now]: +1 Generator-p[now, A coal] + 1 Generator-p[now, A wind] + 1 Generator-p[now, A gas] + 1 Generator-p[now, A oil] - 1 Line-s[now, A-B] = 42000.0\n", 1577 | "[B, now]: +1 Generator-p[now, B hydro] + 1 Line-s[now, A-B] = 650.0" 1578 | ] 1579 | }, 1580 | "execution_count": 32, 1581 | "metadata": {}, 1582 | "output_type": "execute_result" 1583 | } 1584 | ], 1585 | "source": [ 1586 | "n.model.constraints[\"Bus-nodal_balance\"]" 1587 | ] 1588 | }, 1589 | { 1590 | "cell_type": "code", 1591 | "execution_count": 33, 1592 | "metadata": {}, 1593 | "outputs": [ 1594 | { 1595 | "data": { 1596 | "text/plain": [ 1597 | "Objective:\n", 1598 | "----------\n", 1599 | "LinearExpression: +0 Generator-p[now, B hydro] + 24.24 Generator-p[now, A coal] + 0 Generator-p[now, A wind] + 172.4 Generator-p[now, A gas] + 137.1 Generator-p[now, A oil]\n", 1600 | "Sense: min\n", 1601 | "Value: 1398632.6317348" 1602 | ] 1603 | }, 1604 | "execution_count": 33, 1605 | "metadata": {}, 1606 | "output_type": "execute_result" 1607 | } 1608 | ], 1609 | "source": [ 1610 | "n.model.objective" 1611 | ] 1612 | }, 1613 | { 1614 | "cell_type": "markdown", 1615 | "metadata": {}, 1616 | "source": [ 1617 | "## Exercises" 1618 | ] 1619 | }, 1620 | { 1621 | "cell_type": "markdown", 1622 | "metadata": {}, 1623 | "source": [ 1624 | "**Task 1:** Model an outage of the transmission line by setting it's capacity to zero. How does the model compensate for the lack of transmission? " 1625 | ] 1626 | }, 1627 | { 1628 | "cell_type": "code", 1629 | "execution_count": null, 1630 | "metadata": { 1631 | "tags": [ 1632 | "hide-cell" 1633 | ] 1634 | }, 1635 | "outputs": [], 1636 | "source": [ 1637 | "n.lines[\"s_nom\"] = 0.0\n", 1638 | "n.optimize(log_to_console=False)\n", 1639 | "n.generators_t.p" 1640 | ] 1641 | }, 1642 | { 1643 | "cell_type": "markdown", 1644 | "metadata": {}, 1645 | "source": [ 1646 | "**Task 2:** Double the wind capacity. How is the electricity price affected? What generator is price-setting?" 1647 | ] 1648 | }, 1649 | { 1650 | "cell_type": "code", 1651 | "execution_count": null, 1652 | "metadata": { 1653 | "tags": [ 1654 | "hide-cell" 1655 | ] 1656 | }, 1657 | "outputs": [], 1658 | "source": [ 1659 | "n.generators.loc[\"A wind\", \"p_nom\"] *= 2\n", 1660 | "n.optimize(log_to_console=False)\n", 1661 | "n.buses_t.marginal_price" 1662 | ] 1663 | }, 1664 | { 1665 | "cell_type": "markdown", 1666 | "metadata": {}, 1667 | "source": [ 1668 | "**Task 3:** Region A brings a new 1000 MW nuclear power plant into operation with an estimated marginal electricity generation cost of 20 €/MWh. Will that affect the electricity price?" 1669 | ] 1670 | }, 1671 | { 1672 | "cell_type": "code", 1673 | "execution_count": null, 1674 | "metadata": { 1675 | "tags": [ 1676 | "hide-cell" 1677 | ] 1678 | }, 1679 | "outputs": [], 1680 | "source": [ 1681 | "n.add(\n", 1682 | " \"Generator\",\n", 1683 | " \"A nuclear\",\n", 1684 | " bus=\"A\",\n", 1685 | " carrier=\"nuclear\",\n", 1686 | " p_nom=1000,\n", 1687 | " marginal_cost=20,\n", 1688 | ")\n", 1689 | "n.optimize(log_to_console=False)\n", 1690 | "n.buses_t.marginal_price" 1691 | ] 1692 | }, 1693 | { 1694 | "cell_type": "markdown", 1695 | "metadata": {}, 1696 | "source": [] 1697 | } 1698 | ], 1699 | "metadata": { 1700 | "kernelspec": { 1701 | "display_name": "sommerakademie-oxford (3.12.7)", 1702 | "language": "python", 1703 | "name": "python3" 1704 | }, 1705 | "language_info": { 1706 | "codemirror_mode": { 1707 | "name": "ipython", 1708 | "version": 3 1709 | }, 1710 | "file_extension": ".py", 1711 | "mimetype": "text/x-python", 1712 | "name": "python", 1713 | "nbconvert_exporter": "python", 1714 | "pygments_lexer": "ipython3", 1715 | "version": "3.12.7" 1716 | } 1717 | }, 1718 | "nbformat": 4, 1719 | "nbformat_minor": 4 1720 | } 1721 | --------------------------------------------------------------------------------