From f326c018401b01e689161bf6141576c3481bcb55 Mon Sep 17 00:00:00 2001 From: bobbyxng Date: Mon, 4 Nov 2024 18:04:33 +0100 Subject: [PATCH] Added balance plotting https://github.com/PyPSA/pypsa-eur/pull/1285/ --- .gitignore | 1 + Snakefile | 2 + config/plotting.default.yaml | 483 +++++++++++++++++++++++++++++++++++ rules/collect.smk | 41 +++ rules/postprocess.smk | 26 ++ scripts/_helpers.py | 1 + scripts/plot_balance_map.py | 283 ++++++++++++++++++++ 7 files changed, 837 insertions(+) create mode 100644 config/plotting.default.yaml create mode 100644 scripts/plot_balance_map.py diff --git a/.gitignore b/.gitignore index 27a2fb182..98cff7eaf 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ doc/_build config/config.yaml config/scenarios.yaml +config/plotting.yaml config.yaml config/config.yaml diff --git a/Snakefile b/Snakefile index 90a44c9d9..d71b160f9 100644 --- a/Snakefile +++ b/Snakefile @@ -17,7 +17,9 @@ copy_default_files(workflow) configfile: "config/config.default.yaml" +configfile: "config/plotting.default.yaml" configfile: "config/config.yaml" +configfile: "config/plotting.yaml" run = config["run"] diff --git a/config/plotting.default.yaml b/config/plotting.default.yaml new file mode 100644 index 000000000..acf2560ca --- /dev/null +++ b/config/plotting.default.yaml @@ -0,0 +1,483 @@ +# SPDX-FileCopyrightText: : 2017-2024 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: CC0-1.0 +# docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#plotting +plotting: + theme: + style: white + context: paper + font: sans-serif + rc: + patch.linewidth: 0.1 + savefig.bbox: "tight" + savefig.dpi: 350 + ytick.left: true + ytick.major.width: 0.4 + ytick.major.size: 2.5 + xtick.major.pad: 0 + map: + boundaries: [-11, 30, 34, 71] + color_geomap: + ocean: white + land: white + projection: + name: "EqualEarth" + # See https://scitools.org.uk/cartopy/docs/latest/reference/projections.html for alternatives, for example: + # name: "LambertConformal" + # central_longitude: 10. + # central_latitude: 50. + # standard_parallels: [35, 65] + eu_node_location: + x: -5.5 + y: 46. + costs_max: 1000 + costs_threshold: 1 + energy_max: 20000 + energy_min: -20000 + energy_threshold: 50. + # settings for energy balance maps + balance_map: + ext: pdf + carriers_to_plot: ["AC", "H2", "gas", "co2_stored", "urban_central_heat"] + alpha: 1 + region_alpha: 0.6 + figsize: [5, 6.5] + boundaries: [-11, 30, 34, 71] + AC: + unit: TWh$_{el}$ + unit_conversion: 1e6 + region_cmap: Greens + region_unit: €/MWh$_{el}$ + bus_factor: 2e-3 + branch_factor: 1e-2 + flow_factor: 1e+2 + bus_sizes: + - 200 + - 100 + branch_sizes: + - 100 + - 20 + gas: + unit: TWh$_{th}$ + unit_conversion: 1e6 + region_cmap: Purples + region_unit: €/MWh$_{th}$ + bus_factor: 2e-3 + branch_factor: 5e-2 + flow_factor: 6e+1 + bus_sizes: + - 200 + - 100 + branch_sizes: + - 100 + - 50 + H2: + vmin: 60 + vmax: 95 + unit: TWh$_{H_2}$ + unit_conversion: 1e6 + region_cmap: Blues + region_unit: €/MWh$_{H_2}$ + bus_factor: 2e-3 + branch_factor: 7e-2 + flow_factor: 5e+1 + bus_sizes: + - 200 + - 100 + branch_sizes: + - 40 + - 20 + co2_stored: + bus_carrier: co2 stored + unit: Mt + unit_conversion: 1e6 + region_cmap: Purples + region_unit: €/t_${CO_2}$ + bus_factor: 3e-2 + branch_factor: 1 + flow_factor: 2e+3 + bus_sizes: + - 50 + - 10 + branch_sizes: + - 5 + - 2 + urban_central_heat: + bus_carrier: urban central heat + unit: TW$_{th}$ + unit_conversion: 1e6 + region_cmap: Oranges + region_unit: €/MWh_{th}$ + bus_factor: 5e-3 + branch_factor: 1e-1 + flow_factor: 1e+2 + bus_sizes: + - 300 + - 100 + branch_sizes: + methanol: + unit: TWh$_{MeOH}$ + unit_conversion: 1e6 + region_cmap: Greens + region_unit: €/MWh$_{MeOH}$ + bus_factor: 5e-3 + branch_factor: 1e-1 + flow_factor: 1e+2 + bus_sizes: + - 20 + - 10 + branch_sizes: + biogas: + unit: TWh$_{th}$ + unit_conversion: 1e6 + region_cmap: Greens + region_unit: €/MWh + bus_factor: 1e-1 + branch_factor: 1e-1 + flow_factor: 1e+2 + bus_sizes: + - 100 + - 50 + branch_sizes: + solid_biomass: + bus_carrier: solid biomass + unit: TWh$_{th}$ + unit_conversion: 1e6 + region_cmap: Greens + region_unit: €/MWh + bus_factor: 1e-2 + branch_factor: 1e-1 + flow_factor: 1e+2 + bus_sizes: + - 100 + - 50 + branch_sizes: + oil: + unit: TWh$_{th}$ + unit_conversion: 1e6 + region_cmap: Greys + region_unit: €/MWh + bus_factor: 2e-3 + branch_factor: 1e-1 + flow_factor: 1e+2 + bus_sizes: + - 200 + - 100 + branch_sizes: + + nice_names: + OCGT: "Open-Cycle Gas" + CCGT: "Combined-Cycle Gas" + offwind-ac: "Offshore Wind (AC)" + offwind-dc: "Offshore Wind (DC)" + offwind-float: "Offshore Wind (Floating)" + onwind: "Onshore Wind" + solar: "Solar" + PHS: "Pumped Hydro Storage" + hydro: "Reservoir & Dam" + battery: "Battery Storage" + H2: "Hydrogen Storage" + lines: "Transmission Lines" + ror: "Run of River" + load: "Load Shedding" + ac: "AC" + dc: "DC" + + tech_colors: + # wind + onwind: "#235ebc" + onshore wind: "#235ebc" + offwind: "#6895dd" + offshore wind: "#6895dd" + offwind-ac: "#6895dd" + offshore wind (AC): "#6895dd" + offshore wind ac: "#6895dd" + offwind-dc: "#74c6f2" + offshore wind (DC): "#74c6f2" + offshore wind dc: "#74c6f2" + offwind-float: "#b5e2fa" + offshore wind (Float): "#b5e2fa" + offshore wind float: "#b5e2fa" + # water + hydro: '#298c81' + hydro reservoir: '#298c81' + ror: '#3dbfb0' + run of river: '#3dbfb0' + hydroelectricity: '#298c81' + PHS: '#51dbcc' + hydro+PHS: "#08ad97" + # solar + solar: "#f9d002" + solar PV: "#f9d002" + solar-hsat: "#fdb915" + solar thermal: '#ffbf2b' + residential rural solar thermal: '#f1c069' + services rural solar thermal: '#eabf61' + residential urban decentral solar thermal: '#e5bc5a' + services urban decentral solar thermal: '#dfb953' + urban central solar thermal: '#d7b24c' + solar rooftop: '#ffea80' + # gas + OCGT: '#e0986c' + OCGT marginal: '#e0986c' + OCGT-heat: '#e0986c' + gas boiler: '#db6a25' + gas boilers: '#db6a25' + gas boiler marginal: '#db6a25' + residential rural gas boiler: '#d4722e' + residential urban decentral gas boiler: '#cb7a36' + services rural gas boiler: '#c4813f' + services urban decentral gas boiler: '#ba8947' + urban central gas boiler: '#b0904f' + gas: '#e05b09' + fossil gas: '#e05b09' + natural gas: '#e05b09' + biogas to gas: '#e36311' + biogas to gas CC: '#e51245' + CCGT: '#a85522' + CCGT marginal: '#a85522' + allam: '#B98F76' + gas for industry co2 to atmosphere: '#692e0a' + gas for industry co2 to stored: '#8a3400' + gas for industry: '#853403' + gas for industry CC: '#692e0a' + gas pipeline: '#ebbca0' + gas pipeline new: '#a87c62' + # oil + oil: '#c9c9c9' + oil primary: '#d2d2d2' + oil refining: '#e6e6e6' + imported oil: '#a3a3a3' + oil boiler: '#adadad' + residential rural oil boiler: '#a9a9a9' + services rural oil boiler: '#a5a5a5' + residential urban decentral oil boiler: '#a1a1a1' + urban central oil boiler: '#9d9d9d' + services urban decentral oil boiler: '#999999' + agriculture machinery oil: '#949494' + shipping oil: "#808080" + land transport oil: '#afafaf' + # nuclear + Nuclear: '#ff8c00' + Nuclear marginal: '#ff8c00' + nuclear: '#ff8c00' + uranium: '#ff8c00' + # coal + Coal: '#545454' + coal: '#545454' + Coal marginal: '#545454' + coal for industry: '#343434' + solid: '#545454' + Lignite: '#826837' + lignite: '#826837' + Lignite marginal: '#826837' + # biomass + biogas: '#e3d37d' + biomass: '#baa741' + solid biomass: '#baa741' + municipal solid waste: '#91ba41' + solid biomass import: '#d5ca8d' + solid biomass transport: '#baa741' + solid biomass for industry: '#7a6d26' + solid biomass for industry CC: '#47411c' + solid biomass for industry co2 from atmosphere: '#736412' + solid biomass for industry co2 to stored: '#47411c' + urban central solid biomass CHP: '#9d9042' + urban central solid biomass CHP CC: '#6c5d28' + biomass boiler: '#8A9A5B' + residential rural biomass boiler: '#a1a066' + residential urban decentral biomass boiler: '#b0b87b' + services rural biomass boiler: '#c6cf98' + services urban decentral biomass boiler: '#dde5b5' + biomass to liquid: '#32CD32' + unsustainable solid biomass: '#998622' + unsustainable bioliquids: '#32CD32' + electrobiofuels: 'red' + BioSNG: '#123456' + solid biomass to hydrogen: '#654321' + # power transmission + lines: '#6c9459' + transmission lines: '#6c9459' + electricity distribution grid: '#97ad8c' + low voltage: '#97ad8c' + # electricity demand + Electric load: '#110d63' + electric demand: '#110d63' + electricity: '#110d63' + industry electricity: '#2d2a66' + industry new electricity: '#2d2a66' + agriculture electricity: '#494778' + # battery + EVs + battery: '#ace37f' + battery storage: '#ace37f' + battery charger: '#88a75b' + battery discharger: '#5d4e29' + home battery: '#80c944' + home battery storage: '#80c944' + home battery charger: '#5e8032' + home battery discharger: '#3c5221' + BEV charger: '#baf238' + V2G: '#e5ffa8' + land transport EV: '#baf238' + land transport demand: '#38baf2' + EV battery: '#baf238' + # hot water storage + water tanks: '#e69487' + residential rural water tanks: '#f7b7a3' + services rural water tanks: '#f3afa3' + residential urban decentral water tanks: '#f2b2a3' + services urban decentral water tanks: '#f1b4a4' + urban central water tanks: '#e9977d' + hot water storage: '#e69487' + hot water charging: '#e8998b' + urban central water tanks charger: '#b57a67' + residential rural water tanks charger: '#b4887c' + residential urban decentral water tanks charger: '#b39995' + services rural water tanks charger: '#b3abb0' + services urban decentral water tanks charger: '#b3becc' + hot water discharging: '#e99c8e' + urban central water tanks discharger: '#b9816e' + residential rural water tanks discharger: '#ba9685' + residential urban decentral water tanks discharger: '#baac9e' + services rural water tanks discharger: '#bbc2b8' + services urban decentral water tanks discharger: '#bdd8d3' + # heat demand + Heat load: '#cc1f1f' + heat: '#cc1f1f' + heat vent: '#aa3344' + heat demand: '#cc1f1f' + rural heat: '#ff5c5c' + residential rural heat: '#ff7c7c' + services rural heat: '#ff9c9c' + central heat: '#cc1f1f' + urban central heat: '#d15959' + urban central heat vent: '#a74747' + decentral heat: '#750606' + residential urban decentral heat: '#a33c3c' + services urban decentral heat: '#cc1f1f' + low-temperature heat for industry: '#8f2727' + process heat: '#ff0000' + agriculture heat: '#d9a5a5' + # heat supply + heat pumps: '#2fb537' + heat pump: '#2fb537' + air heat pump: '#36eb41' + residential urban decentral air heat pump: '#48f74f' + services urban decentral air heat pump: '#5af95d' + services rural air heat pump: '#5af95d' + urban central air heat pump: '#6cfb6b' + ground heat pump: '#2fb537' + residential rural ground heat pump: '#48f74f' + residential rural air heat pump: '#48f74f' + services rural ground heat pump: '#5af95d' + Ambient: '#98eb9d' + CHP: '#8a5751' + urban central gas CHP: '#8d5e56' + CHP CC: '#634643' + urban central gas CHP CC: '#6e4e4c' + CHP heat: '#8a5751' + CHP electric: '#8a5751' + district heating: '#e8beac' + resistive heater: '#d8f9b8' + residential rural resistive heater: '#bef5b5' + residential urban decentral resistive heater: '#b2f1a9' + services rural resistive heater: '#a5ed9d' + services urban decentral resistive heater: '#98e991' + urban central resistive heater: '#8cdf85' + retrofitting: '#8487e8' + building retrofitting: '#8487e8' + # hydrogen + H2 for industry: "#f073da" + H2 for shipping: "#ebaee0" + H2: '#bf13a0' + hydrogen: '#bf13a0' + retrofitted H2 boiler: '#e5a0d9' + SMR: '#870c71' + SMR CC: '#4f1745' + H2 liquefaction: '#d647bd' + hydrogen storage: '#bf13a0' + H2 Store: '#bf13a0' + H2 storage: '#bf13a0' + land transport fuel cell: '#6b3161' + H2 pipeline: '#f081dc' + H2 pipeline retrofitted: '#ba99b5' + H2 Fuel Cell: '#c251ae' + H2 fuel cell: '#c251ae' + H2 turbine: '#991f83' + H2 Electrolysis: '#ff29d9' + H2 electrolysis: '#ff29d9' + # ammonia + NH3: '#46caf0' + ammonia: '#46caf0' + ammonia store: '#00ace0' + ammonia cracker: '#87d0e6' + Haber-Bosch: '#076987' + # syngas + Sabatier: '#9850ad' + methanation: '#c44ce6' + methane: '#c44ce6' + # synfuels + Fischer-Tropsch: '#25c49a' + liquid: '#25c49a' + kerosene for aviation: '#a1ffe6' + naphtha for industry: '#57ebc4' + methanol-to-kerosene: '#C98468' + methanol-to-olefins/aromatics: '#FFA07A' + Methanol steam reforming: '#FFBF00' + Methanol steam reforming CC: '#A2EA8A' + methanolisation: '#00FFBF' + biomass-to-methanol: '#EAD28A' + biomass-to-methanol CC: '#EADBAD' + allam methanol: '#B98F76' + CCGT methanol: '#B98F76' + CCGT methanol CC: '#B98F76' + OCGT methanol: '#B98F76' + methanol: '#FF7B00' + methanol transport: '#FF7B00' + shipping methanol: '#468c8b' + industry methanol: '#468c8b' + # co2 + CC: '#f29dae' + CCS: '#f29dae' + CO2 sequestration: '#f29dae' + DAC: '#ff5270' + co2 stored: '#f2385a' + co2 sequestered: '#f2682f' + co2: '#f29dae' + co2 vent: '#ffd4dc' + CO2 pipeline: '#f5627f' + # emissions + process emissions CC: '#000000' + process emissions: '#222222' + process emissions to stored: '#444444' + process emissions to atmosphere: '#888888' + oil emissions: '#aaaaaa' + shipping oil emissions: "#555555" + shipping methanol emissions: '#666666' + land transport oil emissions: '#777777' + agriculture machinery oil emissions: '#333333' + # other + shipping: '#03a2ff' + power-to-heat: '#2fb537' + power-to-gas: '#c44ce6' + power-to-H2: '#ff29d9' + power-to-liquid: '#25c49a' + gas-to-power/heat: '#ee8340' + waste: '#e3d37d' + other: '#000000' + geothermal: '#ba91b1' + geothermal heat: '#ba91b1' + geothermal district heat: '#d19D00' + geothermal organic rankine cycle: '#ffbf00' + AC: "#70af1d" + AC-AC: "#70af1d" + AC line: "#70af1d" + links: "#8a1caf" + HVDC links: "#8a1caf" + DC: "#8a1caf" + DC-DC: "#8a1caf" + DC link: "#8a1caf" + load: "#dd2e23" + waste CHP: '#e3d37d' + waste CHP CC: '#e3d3ff' + HVC to air: 'k' \ No newline at end of file diff --git a/rules/collect.smk b/rules/collect.smk index ca58e17c3..c53ac6196 100644 --- a/rules/collect.smk +++ b/rules/collect.smk @@ -83,3 +83,44 @@ rule validate_elec_networks: run=config["run"]["name"], kind=["production", "prices", "cross_border"], ), + + +rule plot_balance_map: + params: + plotting=config_provider("plotting"), + input: + network=RESULTS + + "postnetworks/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + regions=resources("regions_onshore_base_s_{clusters}.geojson"), + output: + map=RESULTS + + "maps/base_s_{clusters}_l{ll}_{opts}_{sector_opts}-balance_map_{carrier}_{planning_horizons}.{ext}", + threads: 2 + resources: + mem_mb=10000, + log: + RESULTS + + "logs/plot_balance_map/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{carrier}_{planning_horizons}_{ext}.log", + benchmark: + ( + RESULTS + + "benchmarks/plot_balance_map/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{carrier}_{planning_horizons}_{ext}" + ) + conda: + "../envs/environment.yaml" + script: + "../scripts/plot_balance_map.py" + + +rule plot_balance_maps: + input: + balance_maps=lambda w: expand( + ( + RESULTS + + "maps/base_s_{clusters}_l{ll}_{opts}_{sector_opts}-balance_map_{carrier}_{planning_horizons}.{ext}" + ), + **config["scenario"], + carrier=config_provider("plotting", "balance_map", "carriers_to_plot")(w), + ext=config_provider("plotting", "balance_map", "ext")(w), + allow_missing=True, + ), \ No newline at end of file diff --git a/rules/postprocess.smk b/rules/postprocess.smk index 24a7ac34c..0e57748ed 100644 --- a/rules/postprocess.smk +++ b/rules/postprocess.smk @@ -102,6 +102,32 @@ if config["foresight"] != "perfect": script: "../scripts/plot_gas_network.py" + rule plot_balance_map: + params: + plotting=config_provider("plotting"), + input: + network=RESULTS + + "postnetworks/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + regions=resources("regions_onshore_base_s_{clusters}.geojson"), + output: + map=RESULTS + + "maps/base_s_{clusters}_l{ll}_{opts}_{sector_opts}-balance_map_{carrier}_{planning_horizons}.{ext}", + threads: 2 + resources: + mem_mb=10000, + log: + RESULTS + + "logs/plot_balance_map/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{carrier}_{planning_horizons}_{ext}.log", + benchmark: + ( + RESULTS + + "benchmarks/plot_balance_map/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{carrier}_{planning_horizons}_{ext}" + ) + conda: + "../envs/environment.yaml" + script: + "../scripts/plot_balance_map.py" + if config["foresight"] == "perfect": diff --git a/scripts/_helpers.py b/scripts/_helpers.py index 6c7f1a675..10f4cf440 100644 --- a/scripts/_helpers.py +++ b/scripts/_helpers.py @@ -31,6 +31,7 @@ def copy_default_files(workflow): default_files = { "config/config.default.yaml": "config/config.yaml", "config/scenarios.template.yaml": "config/scenarios.yaml", + "config/plotting.default.yaml": "config/plotting.yaml", } for template, target in default_files.items(): target = os.path.join(workflow.current_basedir, target) diff --git a/scripts/plot_balance_map.py b/scripts/plot_balance_map.py new file mode 100644 index 000000000..11f550a4e --- /dev/null +++ b/scripts/plot_balance_map.py @@ -0,0 +1,283 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2020-2024 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT +""" +Create energy balance maps for the defined carriers. +""" +import cartopy.crs as ccrs +import geopandas as gpd +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +import pypsa +import seaborn as sns +from _helpers import ( + configure_logging, + set_scenario_config, + update_config_from_wildcards, +) +from pypsa.plot import add_legend_circles, add_legend_lines, add_legend_patches +from pypsa.statistics import get_transmission_carriers + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake( + "plot_balance_map", + simpl="", + opts="", + clusters="70", + ll="vopt", + sector_opts="", + planning_horizons="2050", + run="maps", + carrier="oil", + ext="pdf", + ) + + configure_logging(snakemake) + set_scenario_config(snakemake) + update_config_from_wildcards(snakemake.config, snakemake.wildcards) + n = pypsa.Network(snakemake.input.network) + regions = gpd.read_file(snakemake.input.regions).set_index("name") + plotting = snakemake.params.plotting + carrier = snakemake.wildcards.carrier + + # set plotting style + sns.set_theme(**plotting.get("theme", {})) + + # fill empty colors or "" with light grey + mask = n.carriers.color.isna() | n.carriers.color.eq("") + n.carriers["color"] = n.carriers.color.mask(mask, "lightgrey") + + # set EU location with location from config + eu_location = plotting["eu_node_location"] + n.buses.loc["EU", ["x", "y"]] = eu_location["x"], eu_location["y"] + + # get balance map plotting parameters + plotting = plotting.get("balance_map", {}) + fig_size = plotting.get("fig_size", (6, 6)) + alpha = plotting.get("alpha", 1) + region_alpha = plotting.get("region_alpha", 0.6) + boundaries = plotting.get("boundaries", None) + carrier_plotting = plotting.get(carrier, {}) + # use bus carrier from config if defined + carrier = carrier_plotting.get("bus_carrier", carrier) + # check if carrier is in network + if carrier not in n.buses.carrier.unique(): + raise ValueError(f"Carrier {carrier} is not in the network.") + + fig, ax = plt.subplots( + figsize=fig_size, + subplot_kw={"projection": ccrs.EqualEarth()}, + layout="constrained", + ) + + # for plotting change bus to location + n.buses["location"] = n.buses["location"].replace("", "EU").fillna("EU") + + # set location of buses to EU if location is empty and set x and y coordinates to bus location + n.buses["x"] = n.buses.location.map(n.buses.x) + n.buses["y"] = n.buses.location.map(n.buses.y) + + s = n.statistics + s.set_parameters(round=3, drop_zero=True) + grouper = s.groupers.get_bus_and_carrier + + # bus_sizes according to energy balance of bus carrier + conversion = float(carrier_plotting.get("unit_conversion", 1e6)) + energy_balance_df = s.energy_balance( + nice_names=True, bus_carrier=carrier, groupby=grouper + ).round(2) + + # remove energy balance of transmission carriers which relate to losses + transmission_carriers = get_transmission_carriers(n, bus_carrier=carrier).rename( + {"name": "carrier"} + ) + # TODO change get_transmission carriers in pypsa that dropping in energy balance is easier + energy_balance_df.loc[transmission_carriers.unique(0)] = energy_balance_df.loc[ + transmission_carriers.unique(0) + ].drop(index=transmission_carriers.unique(1), level="carrier") + energy_balance_df = energy_balance_df.dropna() + bus_sizes = ( + energy_balance_df.groupby(level=["bus", "carrier"]).sum().div(conversion) + ) + bus_sizes = bus_sizes.sort_values(ascending=False) + + colors = ( + bus_sizes.index.get_level_values("carrier") + .unique() + .to_series() + .map(n.carriers.set_index("nice_name").color) + ) + + # line and links widths according to optimal capacity + flow = s.transmission(groupby=False, bus_carrier=carrier).div(conversion).round(2) + + if not flow.index.get_level_values(1).empty: + flow_reversed_mask = flow.index.get_level_values(1).str.contains("reversed") + flow_reversed = flow[flow_reversed_mask].rename( + lambda x: x.replace("-reversed", "") + ) + flow = flow[~flow_reversed_mask].subtract(flow_reversed, fill_value=0) + + # if there are not lines or links for the bus carrier, use fallback for plotting + fallback = pd.Series() + line_widths = flow.get("Line", fallback).abs() + link_widths = flow.get("Link", fallback).abs() + + # define maximal size of buses and branch width + bus_size_factor = float(carrier_plotting.get("bus_factor", 2e-5)) + branch_width_factor = float(carrier_plotting.get("branch_factor", 2e-4)) + flow_size_factor = float(carrier_plotting.get("flow_factor", 2e-4)) + + # get prices per region as colormap + buses = n.buses.query("carrier in @carrier").index + price = ( + n.buses_t.marginal_price.mean() + .reindex(buses) + .rename(n.buses.location) + .groupby(level=0) + .mean() + ) + + # if only one price is available, use this price for all regions + if price.size == 1: + regions["price"] = price.values[0] + else: + regions["price"] = price.reindex(regions.index).fillna(0) + + vmin_default = regions.price.min() + vmax_default = regions.price.max() + vmin = carrier_plotting.get("vmin", vmin_default) + vmax = carrier_plotting.get("vmax", vmax_default) + cmap = carrier_plotting.get("region_cmap", "Greens") + + regions.plot( + ax=ax, + column="price", + cmap=cmap, + vmin=vmin, + vmax=vmax, + edgecolor="None", + linewidth=0, + alpha=region_alpha, + transform=ccrs.PlateCarree(), + aspect="equal", + ) + + # plot map + n.plot( + bus_sizes=bus_sizes * bus_size_factor, + bus_colors=colors, + bus_alpha=alpha, + bus_split_circles=True, + line_widths=line_widths * branch_width_factor, + link_widths=link_widths * branch_width_factor, + flow=flow * flow_size_factor, + ax=ax, + margin=0.2, + color_geomap={"border": "darkgrey", "coastline": "darkgrey"}, + geomap="10m", + boundaries=boundaries, + ) + + # TODO maybe do with config + ax.set_title("Balance Map of carrier " + carrier) + + # Add legend + legend_kwargs = { + "loc": "upper left", + "frameon": True, + "framealpha": 0.5, + "edgecolor": "None", + } + + # Add colorbar + sm = plt.cm.ScalarMappable(cmap=cmap, norm=plt.Normalize(vmin=vmin, vmax=vmax)) + price_unit = carrier_plotting.get("region_unit", "€/MWh") + carrier = n.carriers.loc[carrier, "nice_name"] + cbr = fig.colorbar( + sm, + ax=ax, + label=f"Average Marginal Price {carrier} [{price_unit}]", + shrink=0.95, + pad=0.03, + aspect=50, + alpha=region_alpha, + orientation="horizontal", + ) + cbr.outline.set_edgecolor("None") + + pad = 0.18 + carriers = n.carriers.set_index("nice_name") + carriers.loc["", "color"] = "None" + prod_carriers = bus_sizes[bus_sizes > 0].index.unique("carrier").sort_values() + cons_carriers = ( + bus_sizes[bus_sizes < 0] + .index.unique("carrier") + .difference(prod_carriers) + .sort_values() + ) + + # Add production carriers + add_legend_patches( + ax, + carriers.color[prod_carriers], + prod_carriers, + patch_kw={"alpha": alpha}, + legend_kw={ + "bbox_to_anchor": (0, -pad), + "ncol": 1, + "title": "Production", + **legend_kwargs, + }, + ) + + # Add consumption carriers + add_legend_patches( + ax, + carriers.color[cons_carriers], + cons_carriers, + patch_kw={"alpha": alpha}, + legend_kw={ + "bbox_to_anchor": (0.5, -pad), + "ncol": 1, + "title": "Consumption", + **legend_kwargs, + }, + ) + + # Add bus legend + legend_bus_sizes = np.array(carrier_plotting.get("bus_sizes", [10, 50])) + carrier_unit = carrier_plotting.get("unit", "TWh") + if legend_bus_sizes is not None: + add_legend_circles( + ax, + [s * bus_size_factor for s in legend_bus_sizes], + [f"{s} {carrier_unit}" for s in legend_bus_sizes], + legend_kw={ + "bbox_to_anchor": (0, 1), + "title": "Supply/Demand", + **legend_kwargs, + }, + ) + + legend_branch_sizes = carrier_plotting.get("branch_sizes", [1, 10]) + if legend_branch_sizes: + # Add branch legend + if legend_branch_sizes is not None: + add_legend_lines( + ax, + [s * branch_width_factor for s in legend_branch_sizes], + [f"{s} {carrier_unit}" for s in legend_branch_sizes], + legend_kw={"bbox_to_anchor": (0, 0.85), **legend_kwargs}, + ) + + fig.savefig( + snakemake.output.map, + dpi=300, + bbox_inches="tight", + ) \ No newline at end of file