From 19691a92778d83c678fdf9deafa0819480277ed8 Mon Sep 17 00:00:00 2001 From: Michael Lindner Date: Fri, 15 Nov 2024 14:29:56 +0100 Subject: [PATCH 1/4] use underground costs only for DC projects from NEP --- workflow/scripts/export_ariadne_variables.py | 36 ++++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/workflow/scripts/export_ariadne_variables.py b/workflow/scripts/export_ariadne_variables.py index 2361915b..1bb9e859 100644 --- a/workflow/scripts/export_ariadne_variables.py +++ b/workflow/scripts/export_ariadne_variables.py @@ -1688,7 +1688,7 @@ def get_secondary_energy(n, region, _industry_demand): "Sabatier", 0 ) - var["Secondary Energy Input|Hydrogen|Efuel"] = hydrogen_withdrawal.reindex( + var["Secondary Energy Input|Hydrogen|Liquids"] = hydrogen_withdrawal.reindex( ["Fischer-Tropsch", "methanolisation"] ).sum() @@ -4592,22 +4592,7 @@ def hack_DC_projects(n, p_nom_start, p_nom_planned, model_year, snakemake, costs n.links.loc[future_projects, "p_nom"] = 0 n.links.loc[future_projects, "p_nom_min"] = 0 - if (snakemake.params.NEP_year == 2021) or ( - snakemake.params.NEP_transmission == "overhead" - ): - logger.warning("Switching DC projects to NEP23 and underground costs.") - n.links.loc[current_projects, "overnight_cost"] = ( - n.links.loc[current_projects, "length"] - * ( - (1.0 - n.links.loc[current_projects, "underwater_fraction"]) - * costs[0].at["HVDC underground", "investment"] - / 1e-9 - + n.links.loc[current_projects, "underwater_fraction"] - * costs[0].at["HVDC submarine", "investment"] - / 1e-9 - ) - + costs[0].at["HVDC inverter pair", "investment"] / 1e-9 - ) + # Current projects should have their p_nom_opt bigger or equal to p_nom until the year 2030 (Startnetz that we force in) # TODO 2030 is hard coded but should be read from snakemake config if model_year <= 2030: @@ -4618,7 +4603,22 @@ def hack_DC_projects(n, p_nom_start, p_nom_planned, model_year, snakemake, costs n.links.loc[current_projects, "p_nom"] -= p_nom_start[current_projects] n.links.loc[current_projects, "p_nom_min"] -= p_nom_start[current_projects] - + if (snakemake.params.NEP_year == 2021) or ( + snakemake.params.NEP_transmission == "overhead" + ): + logger.warning("Switching DC projects to NEP23 and underground costs.") + n.links.loc[current_projects, "overnight_cost"] = ( + n.links.loc[current_projects, "length"] + * ( + (1.0 - n.links.loc[current_projects, "underwater_fraction"]) + * costs[0].at["HVDC underground", "investment"] + / 1e-9 + + n.links.loc[current_projects, "underwater_fraction"] + * costs[0].at["HVDC submarine", "investment"] + / 1e-9 + ) + + costs[0].at["HVDC inverter pair", "investment"] / 1e-9 + ) else: n.links.loc[current_projects, "p_nom"] = n.links.loc[ current_projects, "p_nom_min" From be41c8cf4f4260c01f9b8b0b54951c8daf3a702a Mon Sep 17 00:00:00 2001 From: Michael Lindner Date: Fri, 15 Nov 2024 16:50:44 +0100 Subject: [PATCH 2/4] bugfix: post-discretize only links with carrier 'DC' --- workflow/scripts/export_ariadne_variables.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/workflow/scripts/export_ariadne_variables.py b/workflow/scripts/export_ariadne_variables.py index 1bb9e859..9a7b331d 100644 --- a/workflow/scripts/export_ariadne_variables.py +++ b/workflow/scripts/export_ariadne_variables.py @@ -4663,13 +4663,14 @@ def process_postnetworks(n, n_start, model_year, snakemake, costs): snakemake.params.post_discretization["link_unit_size"]["DC"], snakemake.params.post_discretization["link_threshold"]["DC"], ) + dc_links = n.links.query("carrier == 'DC'").index for attr in ["p_nom_opt", "p_nom", "p_nom_min"]: # The values in p_nom_opt may already be discretized, here we make sure that # the same logic is applied to p_nom and p_nom_min - n.links[attr] = n.links[attr].apply(_dc_lambda) + n.links.loc[dc_links, attr] = n.links.loc[dc_links, attr].apply(_dc_lambda) - p_nom_planned = n_start.links["p_nom"] - p_nom_start = n_start.links["p_nom"].apply(_dc_lambda) + p_nom_planned = n_start.links.loc[dc_links, "p_nom"] + p_nom_start = n_start.links.loc[dc_links, "p_nom"].apply(_dc_lambda) logger.info("Post-Discretizing AC lines") _ac_lambda = lambda x: get_discretized_value( From 88cebfd7aef66ce1b16388c20e888041be8d24a3 Mon Sep 17 00:00:00 2001 From: Julian Geis Date: Mon, 18 Nov 2024 16:27:55 +0100 Subject: [PATCH 3/4] calc Kernnetz invest and plot it (#281) * calc Kernnetz invest and plot it * change unit_size of H2 post-discretisation, change pipeline diamter to p_nom mapping, exclude Kernnetz from post-discretization, add H2 capacity to exporter and function for domestic length * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * add functionality for post_discretizing h2 pipes right at the start * adress comments * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: Michael Lindner Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- config/config.yaml | 10 +- workflow/Snakefile | 1 + .../scripts/build_wasserstoff_kernnetz.py | 12 +- .../scripts/cluster_wasserstoff_kernnetz.py | 101 +++- workflow/scripts/export_ariadne_variables.py | 431 ++++++++++++++++-- workflow/scripts/modify_prenetwork.py | 16 +- workflow/scripts/plot_ariadne_variables.py | 100 +++- 7 files changed, 598 insertions(+), 73 deletions(-) diff --git a/config/config.yaml b/config/config.yaml index f1e2dc7e..6ba1093b 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -4,7 +4,7 @@ # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#run run: - prefix: 20241108-limit-ft-meoh + prefix: 20241113-export-kernnetz-invest name: # - CurrentPolicies @@ -253,6 +253,8 @@ wasserstoff_kernnetz: pipes_segment_length: 10 border_crossing: true aggregate_build_years: "mean" + recalculate_length: true + aggregate_parallel_pipes: true ipcei_pci_only: false cutoff_year: 2028 force_all_ipcei_pci: true @@ -415,15 +417,13 @@ solving: DC: 1000 gas pipeline: 1500 gas pipeline new: 1500 - H2 pipeline: 13000 - H2 pipeline (Kernnetz): 13000 - H2 pipeline retrofitted: 13000 + H2 pipeline: 4700 + H2 pipeline retrofitted: 4700 link_threshold: DC: 0.3 gas pipeline: 0.3 gas pipeline new: 0.3 H2 pipeline: 0.05 - H2 pipeline (Kernnetz): 0.05 H2 pipeline retrofitted: 0.05 fractional_last_unit_size: true constraints: diff --git a/workflow/Snakefile b/workflow/Snakefile index 240e20f7..594df894 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -565,6 +565,7 @@ rule plot_ariadne_variables: NEP_Trassen_plot=RESULTS + "ariadne/NEP_Trassen_plot.png", transmission_investment_csv=RESULTS + "ariadne/transmission_investment.csv", trassenlaenge_csv=RESULTS + "ariadne/trassenlaenge.csv", + Kernnetz_Investment_plot=RESULTS + "ariadne/Kernnetz_Investment_plot.png", log: RESULTS + "logs/plot_ariadne_variables.log", script: diff --git a/workflow/scripts/build_wasserstoff_kernnetz.py b/workflow/scripts/build_wasserstoff_kernnetz.py index da19739a..58efea8b 100644 --- a/workflow/scripts/build_wasserstoff_kernnetz.py +++ b/workflow/scripts/build_wasserstoff_kernnetz.py @@ -67,17 +67,18 @@ def diameter_to_capacity_h2(pipe_diameter_mm): Calculate pipe capacity in MW based on diameter in mm. Linear interpolation. - 20 inch (500 mm) 50 bar -> 1.2 GW H2 pipe capacity (LHV) 36 inch - (900 mm) 50 bar -> 4.7 GW H2 pipe capacity (LHV) 48 inch (1200 - mm) 80 bar -> 16.9 GW H2 pipe capacity (LHV) + 20 inch (500 mm) 50 bar -> 1.2 GW H2 pipe capacity (LHV) + 36 inch (900 mm) 50 bar -> 4.7 GW H2 pipe capacity (LHV) + 48 inch (1200mm) 80 bar -> 13.0 GW H2 pipe capacity (LHV) - Based on table 4 of + old source: table 4 of https://ehb.eu/files/downloads/EHB-Analysing-the-future-demand-supply-and-transport-of-hydrogen-June-2021-v3.pdf + new source: https://github.com/PyPSA/pypsa-ariadne/pull/167 """ # slopes definitions m0 = (1200 - 0) / (500 - 0) m1 = (4700 - 1200) / (900 - 500) - m2 = (16900 - 4700) / (1200 - 900) + m2 = (13000 - 4700) / (1200 - 900) # intercepts a0 = 0 a1 = 1200 - m1 * 500 @@ -569,6 +570,7 @@ def filter_kernnetz( snakemake.input.wasserstoff_kernnetz_3, ) logger.info("Data retrievel successful. Preparing dataset ...") + wasserstoff_kernnetz = prepare_dataset(wasserstoff_kernnetz) if kernnetz_cf["reload_locations"]: diff --git a/workflow/scripts/cluster_wasserstoff_kernnetz.py b/workflow/scripts/cluster_wasserstoff_kernnetz.py index 16d5d1ae..e203d56e 100644 --- a/workflow/scripts/cluster_wasserstoff_kernnetz.py +++ b/workflow/scripts/cluster_wasserstoff_kernnetz.py @@ -13,9 +13,10 @@ import os import sys +import geopandas as gpd import pandas as pd import pyproj -from _helpers import configure_logging +from pypsa.geo import haversine_pts from shapely import wkt from shapely.geometry import LineString, Point from shapely.ops import transform @@ -23,11 +24,8 @@ paths = ["workflow/submodules/pypsa-eur/scripts", "../submodules/pypsa-eur/scripts"] for path in paths: sys.path.insert(0, os.path.abspath(path)) -from cluster_gas_network import ( - build_clustered_gas_network, - load_bus_regions, - reindex_pipes, -) +from _helpers import configure_logging +from cluster_gas_network import load_bus_regions, reindex_pipes # Define a function for projecting points to meters project_to_meters = pyproj.Transformer.from_proj( @@ -125,6 +123,48 @@ def divide_pipes(df, segment_length=10): return result +def build_clustered_h2_network( + df, bus_regions, recalculate_length=True, length_factor=1.25 +): + for i in [0, 1]: + gdf = gpd.GeoDataFrame(geometry=df[f"point{i}"], crs="EPSG:4326") + + bus_mapping = gpd.sjoin(gdf, bus_regions, how="left", predicate="within")[ + "name" + ] + bus_mapping = bus_mapping.groupby(bus_mapping.index).first() + + df[f"bus{i}"] = bus_mapping + + df[f"point{i}"] = df[f"bus{i}"].map( + bus_regions.to_crs(3035).centroid.to_crs(4326) + ) + + # drop pipes where not both buses are inside regions + df = df.loc[~df.bus0.isna() & ~df.bus1.isna()] + + # drop pipes within the same region + df = df.loc[df.bus1 != df.bus0] + + if df.empty: + return df + + if recalculate_length: + logger.info("Recalculating pipe lengths as center to center * length factor") + # recalculate lengths as center to center * length factor + df["length"] = df.apply( + lambda p: length_factor + * haversine_pts([p.point0.x, p.point0.y], [p.point1.x, p.point1.y]), + axis=1, + ) + + # tidy and create new numbered index + df.drop(["point0", "point1"], axis=1, inplace=True) + df.reset_index(drop=True, inplace=True) + + return df + + def aggregate_parallel_pipes(df, aggregate_build_years="mean"): strategies = { "bus0": "first", @@ -137,6 +177,7 @@ def aggregate_parallel_pipes(df, aggregate_build_years="mean"): "length": "mean", "name": " ".join, "p_min_pu": "min", + "investment_costs (Mio. Euro)": "sum", "removed_gas_cap": "sum", "ipcei": " ".join, "pci": " ".join, @@ -158,7 +199,12 @@ def aggregate_parallel_pipes(df, aggregate_build_years="mean"): snakemake = mock_snakemake( "cluster_wasserstoff_kernnetz", simpl="", - clusters=22, + clusters=27, + run="KN2045_Bal_v4", + opts="", + ll="vopt", + sector_opts="none", + planning_horizons="2020", ) configure_logging(snakemake) @@ -178,7 +224,31 @@ def aggregate_parallel_pipes(df, aggregate_build_years="mean"): segment_length = kernnetz_cf["pipes_segment_length"] df = divide_pipes(df, segment_length=segment_length) - wasserstoff_kernnetz = build_clustered_gas_network(df, bus_regions) + wasserstoff_kernnetz = build_clustered_h2_network( + df, + bus_regions, + recalculate_length=kernnetz_cf["recalculate_length"], + length_factor=1.25, + ) + + if kernnetz_cf["divide_pipes"] & (not kernnetz_cf["aggregate_parallel_pipes"]): + # Set length to 0 for duplicates from the 2nd occurrence onwards and make name unique + logger.info( + f"Setting length to 0 for splitted pipes as Kernnetz pipes are segmented (divide pipes: {kernnetz_cf["divide_pipes"]}) and paralle pipes not aggregated (aggregate_parallel_pipes: {kernnetz_cf["aggregate_parallel_pipes"]})." + ) + wasserstoff_kernnetz["occurrence"] = ( + wasserstoff_kernnetz.groupby("name").cumcount() + 1 + ) + wasserstoff_kernnetz.loc[wasserstoff_kernnetz["occurrence"] > 1, "length"] = 0 + wasserstoff_kernnetz["name"] = wasserstoff_kernnetz.apply( + lambda row: ( + f"{row['name']}-split{row['occurrence']}" + if row["occurrence"] > 1 + else row["name"] + ), + axis=1, + ) + wasserstoff_kernnetz = wasserstoff_kernnetz.drop(columns="occurrence") if not wasserstoff_kernnetz.empty: wasserstoff_kernnetz[["bus0", "bus1"]] = ( @@ -187,12 +257,17 @@ def aggregate_parallel_pipes(df, aggregate_build_years="mean"): .apply(pd.Series) ) - reindex_pipes(wasserstoff_kernnetz, prefix="H2 pipeline") - wasserstoff_kernnetz["p_min_pu"] = 0 wasserstoff_kernnetz["p_nom_diameter"] = 0 - wasserstoff_kernnetz = aggregate_parallel_pipes( - wasserstoff_kernnetz, kernnetz_cf["aggregate_build_years"] - ) + + if kernnetz_cf["aggregate_parallel_pipes"]: + + reindex_pipes(wasserstoff_kernnetz, prefix="H2 pipeline") + wasserstoff_kernnetz = aggregate_parallel_pipes( + wasserstoff_kernnetz, kernnetz_cf["aggregate_build_years"] + ) + + else: + wasserstoff_kernnetz.index = wasserstoff_kernnetz.name.astype(str) wasserstoff_kernnetz.to_csv(snakemake.output.clustered_h2_network) diff --git a/workflow/scripts/export_ariadne_variables.py b/workflow/scripts/export_ariadne_variables.py index 9a7b331d..535c0fe1 100644 --- a/workflow/scripts/export_ariadne_variables.py +++ b/workflow/scripts/export_ariadne_variables.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import ast import logging import math import os @@ -29,6 +30,7 @@ TWh2PJ = 3.6 MWh2TJ = 3.6e-3 MW2GW = 1e-3 +MW2TW = 1e-6 t2Mt = 1e-6 MWh2GJ = 3.6 @@ -40,6 +42,72 @@ EUR20TOEUR23 = 1.1076 +def domestic_length_factor(n, carriers, region="DE"): + """ + Calculate the length factor for specified carriers within a PyPSA network. + + Parameters: + n (pypsa.Network): The PyPSA network object. + carriers (list or str): List of carrier types to filter, or a single carrier as a string. + region (str): The region code to match in the buses (e.g., "DE"). + + Returns: + float or dict: A single length factor if one carrier is provided; otherwise, a dictionary + of length factors for each carrier and component type. + """ + # If a single carrier is provided as a string, wrap it in a list + if isinstance(carriers, str): + carriers = [carriers] + + length_factors = {} + + # Check if carriers exist in network components + for carrier in carriers: + if carrier not in ( + n.links.carrier.unique().tolist() + n.lines.carrier.unique().tolist() + ): + print(f"Carrier '{carrier}' is neither in lines nor links.") + continue # Skip this carrier if not found in both links and lines + + # Loop through relevant components + for c in n.iterate_components(): + if c.name in ["Link", "Line"] and carrier in c.df["carrier"].unique(): + # Filter based on carrier and region, excluding reversed links + all_i = c.df[ + (c.df["carrier"] == carrier) + & (c.df.bus0 + c.df.bus1).str.contains(region) + & ~c.df.index.str.contains("reversed") + ].index + + # Separate domestic and cross-border links + domestic_i = all_i[ + c.df.loc[all_i, "bus0"].str.contains(region) + & c.df.loc[all_i, "bus1"].str.contains(region) + ] + cross_border_i = all_i.difference(domestic_i) + + # Ensure indices match expected totals + assert len(all_i) == len(domestic_i) + len(cross_border_i) + + # Calculate length factor if both sets are non-empty + if len(domestic_i) > 0 and len(cross_border_i) > 0: + length_factor = ( + c.df.loc[domestic_i, "length"].mean() + / c.df.loc[cross_border_i, "length"].mean() + ) + length_factors[(carrier, c.name)] = length_factor + else: + print( + f"No domestic or cross-border links found for {carrier} in {c.name}." + ) + + # Return single length factor if only one carrier was provided and has a length factor + if len(carriers) == 1 and len(length_factors) == 1: + return next(iter(length_factors.values())) + + return length_factors + + def _get_fuel_fractions(n, region, fuel): kwargs = { "groupby": n.statistics.groupers.get_name_bus_and_carrier, @@ -1394,20 +1462,30 @@ def get_secondary_energy(n, region, _industry_demand): .values.sum() ) - electricity_balance = n.statistics.energy_balance( - bus_carrier=["AC", "low voltage"], **kwargs - ).filter(like=region).groupby(["carrier"]).sum() + electricity_balance = ( + n.statistics.energy_balance(bus_carrier=["AC", "low voltage"], **kwargs) + .filter(like=region) + .groupby(["carrier"]) + .sum() + ) if "V2G" in electricity_balance.index: - logger.error("The exporter requires changes to correctly account vehicle to grid technology.") + logger.error( + "The exporter requires changes to correctly account vehicle to grid technology." + ) var["Secondary Energy|Electricity|Storage Losses"] = ( - -1 * electricity_balance.reindex([ - "battery charger", - "battery discharger", - "home battery charger", - "home battery discharger", - "PHS", - ]).multiply(MWh2PJ).sum() + -1 + * electricity_balance.reindex( + [ + "battery charger", + "battery discharger", + "home battery charger", + "home battery discharger", + "PHS", + ] + ) + .multiply(MWh2PJ) + .sum() ) # TODO Compute transmission losses via links_t @@ -1567,8 +1645,9 @@ def get_secondary_energy(n, region, _industry_demand): .sum() .drop(["renewable oil", "methanol"], errors="ignore") # Drop trade links ) - var["Secondary Energy|Liquids|Fossil"] =\ - var["Secondary Energy|Liquids|Oil"] = liquids_production.get("oil refining", 0) + var["Secondary Energy|Liquids|Fossil"] = var["Secondary Energy|Liquids|Oil"] = ( + liquids_production.get("oil refining", 0) + ) var["Secondary Energy|Methanol"] = liquids_production.get("methanolisation", 0) var["Secondary Energy|Liquids|Hydrogen"] = liquids_production.get( "Fischer-Tropsch", 0 @@ -1655,8 +1734,8 @@ def get_secondary_energy(n, region, _industry_demand): like="urban central" ).sum() - var["Secondary Energy Input|Electricity|Liquids"] = ( - electricity_withdrawal.get("methanolisation", 0) + var["Secondary Energy Input|Electricity|Liquids"] = electricity_withdrawal.get( + "methanolisation", 0 ) hydrogen_withdrawal = ( @@ -2109,8 +2188,8 @@ def get_final_energy( # var["Final Energy|Transportation|Other"] = \ - var["Final Energy|Transportation|Electricity"] = ( - low_voltage_electricity.get("BEV charger", 0) + var["Final Energy|Transportation|Electricity"] = low_voltage_electricity.get( + "BEV charger", 0 ) # var["Final Energy|Transportation|Gases"] = \ @@ -2326,8 +2405,8 @@ def get_final_energy( var["Final Energy|Waste"] = waste_withdrawal.filter(like="waste CHP").sum() - var["Final Energy|Carbon Dioxide Removal|Heat"] = ( - decentral_heat_withdrawal.get("DAC", 0) + var["Final Energy|Carbon Dioxide Removal|Heat"] = decentral_heat_withdrawal.get( + "DAC", 0 ) electricity = ( @@ -2343,9 +2422,7 @@ def get_final_energy( .multiply(MWh2PJ) ) - var["Final Energy|Carbon Dioxide Removal|Electricity"] = ( - electricity.get("DAC", 0) - ) + var["Final Energy|Carbon Dioxide Removal|Electricity"] = electricity.get("DAC", 0) var["Final Energy|Carbon Dioxide Removal"] = ( var["Final Energy|Carbon Dioxide Removal|Electricity"] @@ -3739,14 +3816,19 @@ def get_grid_investments(n, costs, region): # https://www.netzentwicklungsplan.de/sites/default/files/2023-07/NEP_2037_2045_V2023_2_Entwurf_Teil1_1.pdf # Tabelle 30, Abbildung 70, Kostenannahmen NEP + eigene Berechnungen, gerundet year = n.generators.build_year.max() - reactive_power_compensation = pd.Series({ - 2020: 0, - 2025: 4.4, - 2030: 8, - 2035: 15, - 2040: 10, - 2045: 1.5, - }) / EUR20TOEUR23 + reactive_power_compensation = ( + pd.Series( + { + 2020: 0, + 2025: 4.4, + 2030: 8, + 2035: 15, + 2040: 10, + 2045: 1.5, + } + ) + / EUR20TOEUR23 + ) var[var_name + "AC|Übernahme|Reactive Power Compensation"] = ( reactive_power_compensation.get(year, 0) / 5 ) @@ -3811,26 +3893,185 @@ def get_grid_investments(n, costs, region): new_h2_links = h2_links[ ((year - 5) < h2_links.build_year) & (h2_links.build_year <= year) ] - h2_expansion = new_h2_links.p_nom_opt.apply( - lambda x: get_discretized_value( - x, - post_discretization["link_unit_size"]["H2 pipeline"], - post_discretization["link_threshold"]["H2 pipeline"], - ) - ) + h2_expansion = new_h2_links.p_nom_opt + # h2_expansion = new_h2_links.p_nom_opt.apply( + # lambda x: get_discretized_value( + # x, + # post_discretization["link_unit_size"]["H2 pipeline"], + # post_discretization["link_threshold"]["H2 pipeline"], + # ) + # ) h2_investments = h2_expansion * new_h2_links.overnight_cost * 1e-9 - # International h2_projects are only accounted with half the costs + # International h2_projects are only accounted with domestic_length_factor * costs + if len(h2_links.carrier.unique()) == 1: + dlf = domestic_length_factor(n, h2_links.carrier.unique().tolist(), region) + else: + dlf = np.array( + list( + domestic_length_factor( + n, h2_links.carrier.unique().tolist(), region + ).values() + ) + ).mean() + h2_investments[ ~( new_h2_links.bus0.str.contains(region) & new_h2_links.bus1.str.contains(region) ) - ] *= 0.5 + ] *= dlf var["Investment|Energy Supply|Hydrogen|Transmission and Distribution"] = var[ "Investment|Energy Supply|Hydrogen|Transmission" ] = (h2_investments.sum() / 5) + new_h2_links_kernnetz_i = new_h2_links[ + (new_h2_links.index.str.contains("kernnetz")) + ].index + + new_h2_links_endogen_i = new_h2_links[ + ~(new_h2_links.index.str.contains("kernnetz")) + ].index + + var["Investment|Energy Supply|Hydrogen|Transmission and Distribution|Endogen"] = ( + h2_investments[new_h2_links_endogen_i].sum() / 5 + ) + var["Investment|Energy Supply|Hydrogen|Transmission and Distribution|Kernnetz"] = ( + h2_investments[new_h2_links_kernnetz_i].sum() / 5 + ) + + assert isclose( + var["Investment|Energy Supply|Hydrogen|Transmission and Distribution"], + var["Investment|Energy Supply|Hydrogen|Transmission and Distribution|Endogen"] + + var[ + "Investment|Energy Supply|Hydrogen|Transmission and Distribution|Kernnetz" + ], + ) + + if "retrofitted" in new_h2_links.columns: + new_h2_links_retrofitted_i = new_h2_links[ + (new_h2_links.retrofitted == 1.0) + | (new_h2_links.index.str.contains("retrofitted")) + ].index + else: + new_h2_links_retrofitted_i = new_h2_links[ + (new_h2_links.index.str.contains("retrofitted")) + ].index + + new_h2_links_newbuild_i = new_h2_links.index.difference(new_h2_links_retrofitted_i) + + var["Investment|Energy Supply|Hydrogen|Transmission and Distribution|New-build"] = ( + h2_investments[new_h2_links_newbuild_i].sum() / 5 + ) + var[ + "Investment|Energy Supply|Hydrogen|Transmission and Distribution|Retrofitted" + ] = (h2_investments[new_h2_links_retrofitted_i].sum() / 5) + + assert isclose( + var["Investment|Energy Supply|Hydrogen|Transmission and Distribution"], + var["Investment|Energy Supply|Hydrogen|Transmission and Distribution|New-build"] + + var[ + "Investment|Energy Supply|Hydrogen|Transmission and Distribution|Retrofitted" + ], + ) + + var[ + "Investment|Energy Supply|Hydrogen|Transmission and Distribution|Endogen|New-build" + ] = ( + h2_investments[ + new_h2_links_newbuild_i.intersection(new_h2_links_endogen_i) + ].sum() + / 5 + ) + var[ + "Investment|Energy Supply|Hydrogen|Transmission and Distribution|Endogen|Retrofitted" + ] = ( + h2_investments[ + new_h2_links_retrofitted_i.intersection(new_h2_links_endogen_i) + ].sum() + / 5 + ) + var[ + "Investment|Energy Supply|Hydrogen|Transmission and Distribution|Kernnetz|New-build" + ] = ( + h2_investments[ + new_h2_links_newbuild_i.intersection(new_h2_links_kernnetz_i) + ].sum() + / 5 + ) + var[ + "Investment|Energy Supply|Hydrogen|Transmission and Distribution|Kernnetz|Retrofitted" + ] = ( + h2_investments[ + new_h2_links_retrofitted_i.intersection(new_h2_links_kernnetz_i) + ].sum() + / 5 + ) + + assert isclose( + var["Investment|Energy Supply|Hydrogen|Transmission and Distribution|Endogen"], + var[ + "Investment|Energy Supply|Hydrogen|Transmission and Distribution|Endogen|New-build" + ] + + var[ + "Investment|Energy Supply|Hydrogen|Transmission and Distribution|Endogen|Retrofitted" + ], + ) + + assert isclose( + var["Investment|Energy Supply|Hydrogen|Transmission and Distribution|Kernnetz"], + var[ + "Investment|Energy Supply|Hydrogen|Transmission and Distribution|Kernnetz|New-build" + ] + + var[ + "Investment|Energy Supply|Hydrogen|Transmission and Distribution|Kernnetz|Retrofitted" + ], + ) + + if "tags" in new_h2_links.columns: + # extract infos from tags + tags = new_h2_links.loc[new_h2_links_kernnetz_i].tags.values + df = pd.DataFrame(tags, columns=["info"]) + df["info"] = df["info"].apply(ast.literal_eval) + df["pci"] = df["info"].apply(lambda x: x["pci"]) + df["ipcei"] = df["info"].apply(lambda x: x["ipcei"]) + df["investment_costs (Mio. Euro)"] = df["info"].apply( + lambda x: x["investment_costs (Mio. Euro)"] + ) + df.index = new_h2_links_kernnetz_i + + pci_i = df[df["pci"] != "no"].index + ipcei_i = df[df["ipcei"] != "no"].index + else: + pci_i = [] + ipcei_i = [] + + var[ + "Investment|Energy Supply|Hydrogen|Transmission and Distribution|Kernnetz|PCI" + ] = (h2_investments[pci_i].sum() / 5) + var[ + "Investment|Energy Supply|Hydrogen|Transmission and Distribution|Kernnetz|IPCEI" + ] = (h2_investments[ipcei_i].sum() / 5) + var[ + "Investment|Energy Supply|Hydrogen|Transmission and Distribution|Kernnetz|PCI+IPCEI" + ] = (h2_investments[pci_i.union(ipcei_i)].sum() / 5) + var[ + "Investment|Energy Supply|Hydrogen|Transmission and Distribution|Kernnetz|NOT-PCI+IPCEI" + ] = ( + h2_investments[new_h2_links_kernnetz_i.difference(pci_i.union(ipcei_i))].sum() + / 5 + ) + + assert isclose( + var["Investment|Energy Supply|Hydrogen|Transmission and Distribution|Kernnetz"], + var[ + "Investment|Energy Supply|Hydrogen|Transmission and Distribution|Kernnetz|PCI+IPCEI" + ] + + var[ + "Investment|Energy Supply|Hydrogen|Transmission and Distribution|Kernnetz|NOT-PCI+IPCEI" + ], + ) + # TODO add retrofitted costs!! if "gas pipeline" in n.links.carrier.unique(): @@ -4555,6 +4796,87 @@ def get_grid_capacity(n, region, year): distr_grid.eval("(p_nom_opt - p_nom_min)").sum() * MW2GW ) + # Hydrogen : TW*km + # TODO: add missing variables and make nice plot + + h2_links = n.links[ + n.links.carrier.str.contains("H2 pipeline") + & ~n.links.reversed + & (n.links.bus0 + n.links.bus1).str.contains(region) + ] + + # Count length of internationl links according to domestic length factor + if len(h2_links.carrier.unique()) == 1: + dlf = domestic_length_factor(n, h2_links.carrier.unique().tolist(), region) + else: + dlf = np.array( + list( + domestic_length_factor( + n, h2_links.carrier.unique().tolist(), region + ).values() + ) + ).mean() + h2_links.loc[ + ~(h2_links.bus0.str.contains(region) & h2_links.bus1.str.contains(region)), + "length", + ] *= dlf + + # Kernnetz + h2_links_kern = h2_links[h2_links.index.str.contains("kernnetz")] + + # Endogeneous + endo_carriers = ["H2 pipeline", "H2 pipeline retrofitted"] + h2_links_endo = h2_links[h2_links.carrier.isin(endo_carriers)] + + var["Capacity|Hydrogen|Transmission"] = ( + h2_links.eval("p_nom_opt * length").sum() * MW2TW + ) + var["Capacity|Hydrogen|Transmission|Kernnetz"] = ( + h2_links_kern.eval("p_nom_opt * length").sum() * MW2TW + ) + # var["Capacity|Hydrogen|Transmission|Kernnetz|Newbuild"] = + # var["Capacity|Hydrogen|Transmission|Kernnetz|Retrofitted"] = + var["Capacity|Hydrogen|Transmission|Endogenous"] = ( + h2_links_endo.eval("p_nom_opt * length").sum() * MW2TW + ) + # var["Capacity|Hydrogen|Transmission|Endogenous|Newbuild"] = + # var["Capacity|Hydrogen|Transmission|Endogenous|Retrofitted"] = + + assert isclose( + var["Capacity|Hydrogen|Transmission"], + var["Capacity|Hydrogen|Transmission|Kernnetz"] + + var["Capacity|Hydrogen|Transmission|Endogenous"], + ), "Hydrogen transmission capacity is not correctly split into Kernnetz and Endogenous" + + year = h2_links.build_year.max() + new_h2_links = h2_links[ + ((year - 5) < h2_links.build_year) & (h2_links.build_year <= year) + ] + new_h2_links_kern = new_h2_links[new_h2_links.index.str.contains("kernnetz")] + new_h2_links_endo = new_h2_links[new_h2_links.carrier.isin(endo_carriers)] + + var["Capacity Additions|Hydrogen|Transmission"] = ( + new_h2_links.eval("(p_nom_opt - p_nom_min) * length").sum() * MW2TW + ) + var["Capacity Additions|Hydrogen|Transmission|Kernnetz"] = ( + new_h2_links_kern.eval("(p_nom_opt - p_nom_min) * length").sum() * MW2TW + ) + # var["Capacity Additions|Hydrogen|Transmission|Kernnetz|Newbuild"] = + # var["Capacity Additions|Hydrogen|Transmission|Kernnetz|Retrofitted"] = + var["Capacity Additions|Hydrogen|Transmission|Endogenous"] = ( + new_h2_links_endo.eval("(p_nom_opt - p_nom_min) * length").sum() * MW2TW + ) + # var["Capacity Additions|Hydrogen|Transmission|Endogenous|Newbuild"] = + # var["Capacity Additions|Hydrogen|Transmission|Endogenous|Retrofitted"] = + + assert isclose( + var["Capacity Additions|Hydrogen|Transmission"], + var["Capacity Additions|Hydrogen|Transmission|Kernnetz"] + + var["Capacity Additions|Hydrogen|Transmission|Endogenous"], + ), "Hydrogen transmission capacity additions are not correctly split into Kernnetz and Endogenous" + + # TODO: add length additions + return var @@ -4592,7 +4914,6 @@ def hack_DC_projects(n, p_nom_start, p_nom_planned, model_year, snakemake, costs n.links.loc[future_projects, "p_nom"] = 0 n.links.loc[future_projects, "p_nom_min"] = 0 - # Current projects should have their p_nom_opt bigger or equal to p_nom until the year 2030 (Startnetz that we force in) # TODO 2030 is hard coded but should be read from snakemake config if model_year <= 2030: @@ -4656,12 +4977,29 @@ def hack_AC_projects(n, s_nom_start, model_year, snakemake): def process_postnetworks(n, n_start, model_year, snakemake, costs): + post_discretization = snakemake.params.post_discretization + + # logger.info("Post-Discretizing H2 pipeline") + + # assert post_discretization["link_unit_size"]["H2 pipeline"] == post_discretization["link_unit_size"]["H2 pipeline retrofitted"] + # assert post_discretization["link_threshold"]["H2 pipeline"] == post_discretization["link_threshold"]["H2 pipeline retrofitted"] + + # _h2_lambda = lambda x: get_discretized_value( + # x, + # post_discretization["link_unit_size"]["H2 pipeline"], + # post_discretization["link_threshold"]["H2 pipeline"], + # ) + # h2_links = n.links.query("carrier == 'H2 pipeline' or carrier == 'H2 pipeline retrofitted'").index + # for attr in ["p_nom_opt", "p_nom", "p_nom_min"]: + # # The values in p_nom_opt may already be discretized, here we make sure that + # # the same logic is applied to p_nom and p_nom_min + # n.links.loc[h2_links, attr] = n.links.loc[h2_links, attr].apply(_h2_lambda) logger.info("Post-Discretizing DC links") _dc_lambda = lambda x: get_discretized_value( x, - snakemake.params.post_discretization["link_unit_size"]["DC"], - snakemake.params.post_discretization["link_threshold"]["DC"], + post_discretization["link_unit_size"]["DC"], + post_discretization["link_threshold"]["DC"], ) dc_links = n.links.query("carrier == 'DC'").index for attr in ["p_nom_opt", "p_nom", "p_nom_min"]: @@ -4675,8 +5013,8 @@ def process_postnetworks(n, n_start, model_year, snakemake, costs): logger.info("Post-Discretizing AC lines") _ac_lambda = lambda x: get_discretized_value( x, - snakemake.params.post_discretization["line_unit_size"], - snakemake.params.post_discretization["line_threshold"], + post_discretization["line_unit_size"], + post_discretization["line_threshold"], ) for attr in ["s_nom_opt", "s_nom", "s_nom_min"]: # The values in s_nom_opt may already be discretized, here we make sure that @@ -4899,7 +5237,7 @@ def get_data( if "debug" == "debug": # For debugging var = pd.Series() - idx = -1 + idx = 2 n = networks[idx] c = costs[idx] _industry_demand = industry_demands[idx] @@ -4944,7 +5282,7 @@ def get_data( print("Gleichschaltung of AC-Startnetz with investments for AC projects") # In this hacky part of the code we assure that the investments for the AC projects, match those of the NEP-AC-Startnetz # Thus the variable 'Investment|Energy Supply|Electricity|Transmission|AC' is equal to the sum of exogeneous AC projects, endogenous AC expansion and Übernahme of NEP costs (mainly Systemdienstleistungen (Reactive Power Compensation) and lines that are below our spatial resolution) - ac_startnetz = 14.5 / 5 / EUR20TOEUR23 # billion EUR + ac_startnetz = 14.5 / 5 / EUR20TOEUR23 # billion EUR ac_projects_invest = df.query( "Variable == 'Investment|Energy Supply|Electricity|Transmission|AC|NEP|Onshore'" @@ -4965,7 +5303,6 @@ def get_data( [2025, 2030, 2035, 2040], ] += (ac_startnetz - ac_projects_invest) / 4 - print("Assigning mean investments of year and year + 5 to year.") investment_rows = df.loc[df["Variable"].str.contains("Investment")] average_investments = ( diff --git a/workflow/scripts/modify_prenetwork.py b/workflow/scripts/modify_prenetwork.py index 074fd638..0a12aec5 100644 --- a/workflow/scripts/modify_prenetwork.py +++ b/workflow/scripts/modify_prenetwork.py @@ -7,7 +7,6 @@ import numpy as np import pandas as pd import pypsa -from _helpers import configure_logging from shapely.geometry import Point logger = logging.getLogger(__name__) @@ -15,6 +14,7 @@ paths = ["workflow/submodules/pypsa-eur/scripts", "../submodules/pypsa-eur/scripts"] for path in paths: sys.path.insert(0, os.path.abspath(path)) +from _helpers import configure_logging from add_electricity import load_costs from prepare_sector_network import lossy_bidirectional_links, prepare_costs @@ -240,7 +240,19 @@ def add_wasserstoff_kernnetz(n, wkn, costs): overnight_cost=overnight_costs, carrier="H2 pipeline (Kernnetz)", lifetime=lifetime, + retrofitted=wkn_new.retrofitted.values, + ) + + # add tags + tags = wkn_new.apply( + lambda row: { + "pci": row["pci"], + "ipcei": row["ipcei"], + "investment_costs (Mio. Euro)": row["investment_costs (Mio. Euro)"], + }, + axis=1, ) + n.links.loc[names, "tags"] = tags.values.astype(str) # add reversed pipes and losses losses = snakemake.params.H2_transmission_efficiency @@ -1172,7 +1184,7 @@ def drop_duplicate_transmission_projects(n): opts="", ll="vopt", sector_opts="none", - planning_horizons="2020", + planning_horizons="2025", run="KN2045_Bal_v4", ) diff --git a/workflow/scripts/plot_ariadne_variables.py b/workflow/scripts/plot_ariadne_variables.py index 713e3fa9..f660aa7e 100644 --- a/workflow/scripts/plot_ariadne_variables.py +++ b/workflow/scripts/plot_ariadne_variables.py @@ -6,6 +6,100 @@ import pandas as pd +def plot_Kernnetz(df, savepath=None, currency_year=2020): + key = "Investment|Energy Supply|Hydrogen|Transmission and Distribution|" + + data = { + "Kategorie": ["FNB", "PyPSA"], + "Kernnetz-Zubau": [ + 12.3 + 0.6, + df.loc[key + "Kernnetz|New-build"].values.sum() * 5, + ], + "Kernnetz-Umstellung": [ + 3.2 + 0.2, + df.loc[key + "Kernnetz|Retrofitted"].values.sum() * 5, + ], + "Endogen-Zubau": [ + None, + df.loc[key + "Endogen|New-build"].values.sum() * 5, + ], + "Endogen-Umstellung": [ + None, + df.loc[key + "Endogen|Retrofitted"].values.sum() * 5, + ], + "PCI+IPCEI": [ + 7.8, + df.loc[key + "Kernnetz|PCI+IPCEI"].values.sum() * 5, + ], + "Not PCI+IPCEI": [ + 8.4, + df.loc[key + "Kernnetz|NOT-PCI+IPCEI"].values.sum() * 5, + ], + } + + plotframe = pd.DataFrame(data) + plotframe.set_index("Kategorie", inplace=True) + + if currency_year == 2023: + plotframe.loc["PyPSA"] *= 1.1076 + elif currency_year == 2020: + plotframe.loc["FNB"] /= 1.1076 + else: + raise ValueError("Currency year not supported") + + # Set up the plot + fig, ax = plt.subplots(1, 2, figsize=(10, 6)) + + # Create bars + bar_width = 0.35 + x = np.arange(2) # Three groups + + group1 = [ + "Kernnetz-Zubau", + "Kernnetz-Umstellung", + "Endogen-Zubau", + "Endogen-Umstellung", + ] + group2 = ["PCI+IPCEI", "Not PCI+IPCEI"] + + colors_dict = { + # Group 1 - Using turquoise to pink spectrum + "Kernnetz-Zubau": "#00A4B4", # Turquoise + "Kernnetz-Umstellung": "#20CFB4", # Bright turquoise-mint + "Endogen-Zubau": "#FF69B4", # Hot pink + "Endogen-Umstellung": "#BA55D3", # Medium orchid (purple-pink) + # Group 2 - Keeping warm colors + "PCI+IPCEI": "#D95F02", # Orange + "Not PCI+IPCEI": "#E7B031", # Golden yellow + } + # Create grouped bars + y_limit = plotframe[group1].sum(axis=1).max() + 3 + plotframe[group1].plot( + kind="bar", + stacked=True, + ax=ax[0], + ylim=(0, y_limit), + ylabel="Investment (Mrd. €)", + color=[colors_dict.get(x, "#333333") for x in group1], + ) + plotframe[group2].plot( + kind="bar", + stacked=True, + ax=ax[1], + ylim=(0, y_limit), + color=[colors_dict.get(x, "#333333") for x in group2], + ) + + plt.suptitle("Investitionen ins Wasserstoffnetz") + + # Adjust layout + plt.tight_layout() + + if savepath: + plt.savefig(savepath, bbox_inches="tight") + else: + plt.show() + def plot_NEP_Trassen(df, savepath=None, gleichschaltung=True): @@ -109,7 +203,7 @@ def plot_NEP_Trassen(df, savepath=None, gleichschaltung=True): def plot_NEP(df, savepath=None, gleichschaltung=True, currency_year=2020): - + key = "Investment|Energy Supply|Electricity|Transmission|" data = { @@ -833,3 +927,7 @@ def elec_val_plot(df, savepath): plot_NEP(df, savepath=snakemake.output.NEP_plot) plot_NEP_Trassen(df, savepath=snakemake.output.NEP_Trassen_plot) + + plot_Kernnetz( + df, savepath=snakemake.output.Kernnetz_Investment_plot, currency_year=2020 + ) From f406e1b4baa4d0aeaa4a0004b985039fd54af869 Mon Sep 17 00:00:00 2001 From: Michael Lindner Date: Mon, 18 Nov 2024 17:07:47 +0100 Subject: [PATCH 4/4] upstream: bugfix: correct historical boiler efficiencies --- workflow/submodules/pypsa-eur | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workflow/submodules/pypsa-eur b/workflow/submodules/pypsa-eur index 778b60f8..8a597817 160000 --- a/workflow/submodules/pypsa-eur +++ b/workflow/submodules/pypsa-eur @@ -1 +1 @@ -Subproject commit 778b60f821993bc4aa1a385edfb1bcdd1dbace39 +Subproject commit 8a5978177dce5b64cc60c0136663a5bde21e9b5e