From 63724279a465ae46959a9feb0fd8c93e21aa4761 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 24 Nov 2021 16:01:58 +0100 Subject: [PATCH 001/234] Snakefile: export conda environment --- Snakefile | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Snakefile b/Snakefile index 76dca9ab..7d7acb70 100644 --- a/Snakefile +++ b/Snakefile @@ -411,6 +411,14 @@ rule copy_config: script: "scripts/copy_config.py" +rule copy_conda_env: + output: SDIR + '/configs/environment.yaml' + threads: 1 + resources: mem_mb=500 + benchmark: SDIR + "/benchmarks/copy_conda_env" + shell: "conda env export -f {output} --no-builds" + + rule make_summary: input: overrides="data/override_component_attrs", @@ -468,6 +476,7 @@ if config["foresight"] == "overnight": network=RDIR + "/prenetworks/elec_s{simpl}_{clusters}_lv{lv}_{opts}_{sector_opts}_{planning_horizons}.nc", costs=CDIR + "costs_{planning_horizons}.csv", config=SDIR + '/configs/config.yaml' + env=SDIR + '/configs/environment.yaml' output: RDIR + "/postnetworks/elec_s{simpl}_{clusters}_lv{lv}_{opts}_{sector_opts}_{planning_horizons}.nc" shadow: "shallow" log: From 9573f33860d9e0981ad73b1a07bfaf8d45181bb5 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 10 Jun 2022 14:44:36 +0200 Subject: [PATCH 002/234] add ammonia as carrier: with Haber-Bosch, crackers, store, load --- config.default.yaml | 2 ++ ...uild_industrial_energy_demand_per_country_today.py | 11 ++++++++--- scripts/build_industrial_energy_demand_per_node.py | 1 + scripts/build_industry_sector_ratios.py | 8 ++++++-- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/config.default.yaml b/config.default.yaml index d48879c6..ae8f90c8 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -244,6 +244,7 @@ sector: # - onshore # more than 50 km from sea - nearshore # within 50 km of sea # - offshore + ammonia: false use_fischer_tropsch_waste_heat: true use_fuel_cell_waste_heat: true electricity_distribution_grid: true @@ -291,6 +292,7 @@ industry: # 2040: 0.3 # 2045: 0.25 # 2050: 0.2 + MWh_NH3_per_tNH3: 5.166 # LHV MWh_CH4_per_tNH3_SMR: 10.8 # 2012's demand from https://ec.europa.eu/docsroom/documents/4165/attachments/1/translations/en/renditions/pdf MWh_elec_per_tNH3_SMR: 0.7 # same source, assuming 94-6% split methane-elec of total energy demand 11.5 MWh/tNH3 MWh_H2_per_tNH3_electrolysis: 6.5 # from https://doi.org/10.1016/j.joule.2018.04.017, around 0.197 tH2/tHN3 (>3/17 since some H2 lost and used for energy) diff --git a/scripts/build_industrial_energy_demand_per_country_today.py b/scripts/build_industrial_energy_demand_per_country_today.py index 0adf84e7..a3fbf466 100644 --- a/scripts/build_industrial_energy_demand_per_country_today.py +++ b/scripts/build_industrial_energy_demand_per_country_today.py @@ -65,6 +65,8 @@ def industrial_energy_demand_per_country(country): df = df_dict[sheet][year].groupby(fuels).sum() + df["ammonia"] = 0. + df['other'] = df['all'] - df.loc[df.index != 'all'].sum() return df @@ -89,18 +91,21 @@ def add_ammonia_energy_demand(demand): fn = snakemake.input.ammonia_production ammonia = pd.read_csv(fn, index_col=0)[str(year)] / 1e3 - def ammonia_by_fuel(x): + def get_ammonia_by_fuel(x): fuels = {'gas': config['MWh_CH4_per_tNH3_SMR'], 'electricity': config['MWh_elec_per_tNH3_SMR']} return pd.Series({k: x*v for k,v in fuels.items()}) - ammonia = ammonia.apply(ammonia_by_fuel).T + ammonia_by_fuel = ammonia.apply(get_ammonia_by_fuel).T + ammonia_by_fuel = ammonia_by_fuel.unstack().reindex(index=demand.index, fill_value=0.) + + ammonia = pd.DataFrame({"ammonia": ammonia * config['MWh_NH3_per_tNH3']}).T demand['Ammonia'] = ammonia.unstack().reindex(index=demand.index, fill_value=0.) - demand['Basic chemicals (without ammonia)'] = demand["Basic chemicals"] - demand["Ammonia"] + demand['Basic chemicals (without ammonia)'] = demand["Basic chemicals"] - ammonia_by_fuel demand['Basic chemicals (without ammonia)'].clip(lower=0, inplace=True) diff --git a/scripts/build_industrial_energy_demand_per_node.py b/scripts/build_industrial_energy_demand_per_node.py index cb085ad1..d665f18e 100644 --- a/scripts/build_industrial_energy_demand_per_node.py +++ b/scripts/build_industrial_energy_demand_per_node.py @@ -9,6 +9,7 @@ if __name__ == '__main__': 'build_industrial_energy_demand_per_node', simpl='', clusters=48, + planning_horizons=2030, ) # import EU ratios df as csv diff --git a/scripts/build_industry_sector_ratios.py b/scripts/build_industry_sector_ratios.py index c8cac055..d1dbe9d8 100644 --- a/scripts/build_industry_sector_ratios.py +++ b/scripts/build_industry_sector_ratios.py @@ -60,6 +60,7 @@ index = [ "hydrogen", "heat", "naphtha", + "ammonia", "process emission", "process emission from feedstock", ] @@ -432,8 +433,11 @@ def chemicals_industry(): sector = "Ammonia" df[sector] = 0.0 - df.loc["hydrogen", sector] = config["MWh_H2_per_tNH3_electrolysis"] - df.loc["elec", sector] = config["MWh_elec_per_tNH3_electrolysis"] + if snakemake.config["sector"].get("ammonia", False): + df.loc["ammonia", sector] = config["MWh_NH3_per_tNH3"] + else: + df.loc["hydrogen", sector] = config["MWh_H2_per_tNH3_electrolysis"] + df.loc["elec", sector] = config["MWh_elec_per_tNH3_electrolysis"] # Chlorine From 6cfee1f98a647509f2e2cab0d36348e8cbcbe08d Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 10 Jun 2022 14:44:36 +0200 Subject: [PATCH 003/234] add ammonia as carrier: with Haber-Bosch, crackers, store, load --- config.default.yaml | 2 + ...ustrial_energy_demand_per_country_today.py | 11 +++- ...build_industrial_energy_demand_per_node.py | 1 + scripts/build_industry_sector_ratios.py | 8 ++- scripts/prepare_sector_network.py | 65 +++++++++++++++++++ 5 files changed, 82 insertions(+), 5 deletions(-) diff --git a/config.default.yaml b/config.default.yaml index d48879c6..ae8f90c8 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -244,6 +244,7 @@ sector: # - onshore # more than 50 km from sea - nearshore # within 50 km of sea # - offshore + ammonia: false use_fischer_tropsch_waste_heat: true use_fuel_cell_waste_heat: true electricity_distribution_grid: true @@ -291,6 +292,7 @@ industry: # 2040: 0.3 # 2045: 0.25 # 2050: 0.2 + MWh_NH3_per_tNH3: 5.166 # LHV MWh_CH4_per_tNH3_SMR: 10.8 # 2012's demand from https://ec.europa.eu/docsroom/documents/4165/attachments/1/translations/en/renditions/pdf MWh_elec_per_tNH3_SMR: 0.7 # same source, assuming 94-6% split methane-elec of total energy demand 11.5 MWh/tNH3 MWh_H2_per_tNH3_electrolysis: 6.5 # from https://doi.org/10.1016/j.joule.2018.04.017, around 0.197 tH2/tHN3 (>3/17 since some H2 lost and used for energy) diff --git a/scripts/build_industrial_energy_demand_per_country_today.py b/scripts/build_industrial_energy_demand_per_country_today.py index 0adf84e7..a3fbf466 100644 --- a/scripts/build_industrial_energy_demand_per_country_today.py +++ b/scripts/build_industrial_energy_demand_per_country_today.py @@ -65,6 +65,8 @@ def industrial_energy_demand_per_country(country): df = df_dict[sheet][year].groupby(fuels).sum() + df["ammonia"] = 0. + df['other'] = df['all'] - df.loc[df.index != 'all'].sum() return df @@ -89,18 +91,21 @@ def add_ammonia_energy_demand(demand): fn = snakemake.input.ammonia_production ammonia = pd.read_csv(fn, index_col=0)[str(year)] / 1e3 - def ammonia_by_fuel(x): + def get_ammonia_by_fuel(x): fuels = {'gas': config['MWh_CH4_per_tNH3_SMR'], 'electricity': config['MWh_elec_per_tNH3_SMR']} return pd.Series({k: x*v for k,v in fuels.items()}) - ammonia = ammonia.apply(ammonia_by_fuel).T + ammonia_by_fuel = ammonia.apply(get_ammonia_by_fuel).T + ammonia_by_fuel = ammonia_by_fuel.unstack().reindex(index=demand.index, fill_value=0.) + + ammonia = pd.DataFrame({"ammonia": ammonia * config['MWh_NH3_per_tNH3']}).T demand['Ammonia'] = ammonia.unstack().reindex(index=demand.index, fill_value=0.) - demand['Basic chemicals (without ammonia)'] = demand["Basic chemicals"] - demand["Ammonia"] + demand['Basic chemicals (without ammonia)'] = demand["Basic chemicals"] - ammonia_by_fuel demand['Basic chemicals (without ammonia)'].clip(lower=0, inplace=True) diff --git a/scripts/build_industrial_energy_demand_per_node.py b/scripts/build_industrial_energy_demand_per_node.py index cb085ad1..d665f18e 100644 --- a/scripts/build_industrial_energy_demand_per_node.py +++ b/scripts/build_industrial_energy_demand_per_node.py @@ -9,6 +9,7 @@ if __name__ == '__main__': 'build_industrial_energy_demand_per_node', simpl='', clusters=48, + planning_horizons=2030, ) # import EU ratios df as csv diff --git a/scripts/build_industry_sector_ratios.py b/scripts/build_industry_sector_ratios.py index c8cac055..d1dbe9d8 100644 --- a/scripts/build_industry_sector_ratios.py +++ b/scripts/build_industry_sector_ratios.py @@ -60,6 +60,7 @@ index = [ "hydrogen", "heat", "naphtha", + "ammonia", "process emission", "process emission from feedstock", ] @@ -432,8 +433,11 @@ def chemicals_industry(): sector = "Ammonia" df[sector] = 0.0 - df.loc["hydrogen", sector] = config["MWh_H2_per_tNH3_electrolysis"] - df.loc["elec", sector] = config["MWh_elec_per_tNH3_electrolysis"] + if snakemake.config["sector"].get("ammonia", False): + df.loc["ammonia", sector] = config["MWh_NH3_per_tNH3"] + else: + df.loc["hydrogen", sector] = config["MWh_H2_per_tNH3_electrolysis"] + df.loc["elec", sector] = config["MWh_elec_per_tNH3_electrolysis"] # Chlorine diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index f0f60934..a524e882 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -654,6 +654,59 @@ def add_generation(n, costs): ) +def add_ammonia(n, costs): + + logger.info("adding ammonia carrier") + + nodes = pop_layout.index + + n.add("Carrier", "NH3") + + n.madd("Bus", + nodes + " NH3", + location=nodes, + carrier="NH3" + ) + + n.madd("Link", + nodes, + suffix=" Haber-Bosch", + bus0=nodes, + bus1=nodes + " NH3", + bus2=nodes + " H2", + p_nom_extendable=True, + carrier="Haber-Bosch", + efficiency=+0.221, #MWh_e/MWh_NH3 0.247 https://github.com/euronion/trace/blob/44a5ff8401762edbef80eff9cfe5a47c8d3c8be4/data/efficiencies.csv + efficiency2=-1.226, #MWh_H2/MWh_NH3 1.148 https://github.com/euronion/trace/blob/44a5ff8401762edbef80eff9cfe5a47c8d3c8be4/data/efficiencies.csv + capital_cost=costs.at["Haber-Bosch synthesis", "fixed"], + lifetime=costs.at["Haber-Bosch synthesis", 'lifetime'] + ) + + n.madd("Link", + nodes, + suffix=" ammonia cracker", + bus0=nodes + " NH3", + bus1=nodes + " H2", + p_nom_extendable=True, + carrier ="ammonia cracker", + efficiency=0.685, #MWh_H2/MWh_NH3 https://github.com/euronion/trace/blob/44a5ff8401762edbef80eff9cfe5a47c8d3c8be4/data/efficiencies.csv + capital_cost=costs.at["Ammonia cracker", "fixed"] * 0.685, # given per MWh_H2 + lifetime=costs.at['Ammonia cracker', 'lifetime'] + ) + + # Ammonia Storage + n.madd("Store", + nodes, + suffix=" ammonia store", + bus=nodes + " NH3", + e_nom_extendable=True, + e_cyclic=True, + carrier="ammonia store", + capital_cost=costs.at["NH3 (l) storage tank incl. liquefaction", "fixed"], + lifetime=costs.at['NH3 (l) storage tank incl. liquefaction', 'lifetime'] + ) + + def add_wave(n, wave_cost_factor): # TODO: handle in Snakefile @@ -2148,6 +2201,15 @@ def add_industry(n, costs): lifetime=costs.at['cement capture', 'lifetime'] ) + if options["ammonia"]: + n.madd("Load", + nodes, + suffix=" NH3", + bus=nodes + " NH3", + carrier="NH3", + p_set=industrial_demand.loc[nodes, "ammonia"] / 8760 + ) + def add_waste_heat(n): # TODO options? @@ -2377,6 +2439,9 @@ if __name__ == "__main__": if options['dac']: add_dac(n, costs) + if options['ammonia']: + add_ammonia(n, costs) + if "decentral" in opts: decentral(n) From 2d562c13495a21cbba504ceb09f9614b2365d58a Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 10 Jun 2022 14:57:03 +0200 Subject: [PATCH 004/234] add coloring for ammonia --- config.default.yaml | 5 +++++ scripts/plot_network.py | 2 ++ scripts/plot_summary.py | 2 ++ 3 files changed, 9 insertions(+) diff --git a/config.default.yaml b/config.default.yaml index ae8f90c8..b510780e 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -573,6 +573,11 @@ plotting: H2 pipeline retrofitted: '#ba99b5' H2 Fuel Cell: '#c251ae' H2 Electrolysis: '#ff29d9' + # ammonia + NH3: '#46caf0' + ammonia store: '#00ace0' + ammonia cracker: '#87d0e6' + Haber-Bosch: '#076987' # syngas Sabatier: '#9850ad' methanation: '#c44ce6' diff --git a/scripts/plot_network.py b/scripts/plot_network.py index 4a1bc6d0..61f91df1 100644 --- a/scripts/plot_network.py +++ b/scripts/plot_network.py @@ -23,6 +23,8 @@ def rename_techs_tyndp(tech): return "power-to-gas" elif tech == "H2": return "H2 storage" + elif tech in ["NH3", "Haber-Bosch", "ammonia cracker", "ammonia store"]: + return "ammonia" elif tech in ["OCGT", "CHP", "gas boiler", "H2 Fuel Cell"]: return "gas-to-power/heat" elif "solar" in tech: diff --git a/scripts/plot_summary.py b/scripts/plot_summary.py index 8b073b17..97166f54 100644 --- a/scripts/plot_summary.py +++ b/scripts/plot_summary.py @@ -51,6 +51,7 @@ def rename_techs(label): "ror": "hydroelectricity", "hydro": "hydroelectricity", "PHS": "hydroelectricity", + "NH3": "ammonia" "co2 Store": "DAC", "co2 stored": "CO2 sequestration", "AC": "transmission lines", @@ -106,6 +107,7 @@ preferred_order = pd.Index([ "natural gas", "helmeth", "methanation", + "ammonia", "hydrogen storage", "power-to-gas", "power-to-liquid", From a2a4cf7c023eccdbb5d5de2043fac2036a16347f Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 10 Jun 2022 16:43:29 +0200 Subject: [PATCH 005/234] use config to manage conversion efficiencies --- config.default.yaml | 1 + scripts/prepare_sector_network.py | 12 +++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/config.default.yaml b/config.default.yaml index b510780e..d2291f58 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -297,6 +297,7 @@ industry: MWh_elec_per_tNH3_SMR: 0.7 # same source, assuming 94-6% split methane-elec of total energy demand 11.5 MWh/tNH3 MWh_H2_per_tNH3_electrolysis: 6.5 # from https://doi.org/10.1016/j.joule.2018.04.017, around 0.197 tH2/tHN3 (>3/17 since some H2 lost and used for energy) MWh_elec_per_tNH3_electrolysis: 1.17 # from https://doi.org/10.1016/j.joule.2018.04.017 Table 13 (air separation and HB) + MWh_NH3_per_MWh_H2_cracker: 1.46 # https://github.com/euronion/trace/blob/44a5ff8401762edbef80eff9cfe5a47c8d3c8be4/data/efficiencies.csv NH3_process_emissions: 24.5 # in MtCO2/a from SMR for H2 production for NH3 from UNFCCC for 2015 for EU28 petrochemical_process_emissions: 25.5 # in MtCO2/a for petrochemical and other from UNFCCC for 2015 for EU28 HVC_primary_fraction: 1. # fraction of today's HVC produced via primary route diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index a524e882..dd4e6e39 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -660,6 +660,8 @@ def add_ammonia(n, costs): nodes = pop_layout.index + cf_industry = snakemake.config["industry"] + n.add("Carrier", "NH3") n.madd("Bus", @@ -676,8 +678,8 @@ def add_ammonia(n, costs): bus2=nodes + " H2", p_nom_extendable=True, carrier="Haber-Bosch", - efficiency=+0.221, #MWh_e/MWh_NH3 0.247 https://github.com/euronion/trace/blob/44a5ff8401762edbef80eff9cfe5a47c8d3c8be4/data/efficiencies.csv - efficiency2=-1.226, #MWh_H2/MWh_NH3 1.148 https://github.com/euronion/trace/blob/44a5ff8401762edbef80eff9cfe5a47c8d3c8be4/data/efficiencies.csv + efficiency=1 / (cf_industry["MWh_elec_per_tNH3_electrolysis"] / cf_industry["MWh_NH3_per_tNH3"]) # output: MW_NH3 per MW_elec + efficiency2=-cf_industry["MWh_H2_per_tNH3_electrolysis"] / cf_industry["MWh_elec_per_tNH3_electrolysis"] # input: MW_H2 per MW_elec capital_cost=costs.at["Haber-Bosch synthesis", "fixed"], lifetime=costs.at["Haber-Bosch synthesis", 'lifetime'] ) @@ -688,9 +690,9 @@ def add_ammonia(n, costs): bus0=nodes + " NH3", bus1=nodes + " H2", p_nom_extendable=True, - carrier ="ammonia cracker", - efficiency=0.685, #MWh_H2/MWh_NH3 https://github.com/euronion/trace/blob/44a5ff8401762edbef80eff9cfe5a47c8d3c8be4/data/efficiencies.csv - capital_cost=costs.at["Ammonia cracker", "fixed"] * 0.685, # given per MWh_H2 + carrier="ammonia cracker", + efficiency=1 / cf_industry["MWh_NH3_per_MWh_H2_cracker"] + capital_cost=costs.at["Ammonia cracker", "fixed"] / cf_industry["MWh_NH3_per_MWh_H2_cracker"], # given per MW_H2 lifetime=costs.at['Ammonia cracker', 'lifetime'] ) From 4984ba199e2e2a41bf8183cbd7832b5262521b53 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 10 Jun 2022 16:53:37 +0200 Subject: [PATCH 006/234] use spatial namespace to manage ammonia resolution --- config.default.yaml | 2 +- scripts/prepare_sector_network.py | 26 +++++++++++++++++++------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/config.default.yaml b/config.default.yaml index d2291f58..d0887bc8 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -244,7 +244,7 @@ sector: # - onshore # more than 50 km from sea - nearshore # within 50 km of sea # - offshore - ammonia: false + ammonia: false # can be false (no NH3 carrier), true (copperplated NH3), "regional" (regionalised NH3 without network) use_fischer_tropsch_waste_heat: true use_fuel_cell_waste_heat: true electricity_distribution_grid: true diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index dd4e6e39..9411a282 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -93,6 +93,18 @@ def define_spatial(nodes, options): spatial.gas.df = pd.DataFrame(vars(spatial.gas), index=nodes) + # ammonia + + spatial.ammonia = SimpleNamespace() + if options["ammonia"] == "regional": + spatial.ammonia.nodes = nodes + " NH3" + spatial.ammonia.locations = nodes + else: + spatial.ammonia.nodes = ["EU ammonia"] + spatial.ammonia.locations = ["EU"] + + spatial.ammonia.df = pd.DataFrame(vars(spatial.ammonia), index=nodes) + # oil spatial.oil = SimpleNamespace() spatial.oil.nodes = ["EU oil"] @@ -656,7 +668,7 @@ def add_generation(n, costs): def add_ammonia(n, costs): - logger.info("adding ammonia carrier") + logger.info("adding ammonia carrier with synthesis, cracking and storage") nodes = pop_layout.index @@ -665,8 +677,8 @@ def add_ammonia(n, costs): n.add("Carrier", "NH3") n.madd("Bus", - nodes + " NH3", - location=nodes, + spatial.ammonia.nodes, + location=spatial.ammonia.locations, carrier="NH3" ) @@ -674,7 +686,7 @@ def add_ammonia(n, costs): nodes, suffix=" Haber-Bosch", bus0=nodes, - bus1=nodes + " NH3", + bus1=spatial.ammonia.nodes, bus2=nodes + " H2", p_nom_extendable=True, carrier="Haber-Bosch", @@ -687,7 +699,7 @@ def add_ammonia(n, costs): n.madd("Link", nodes, suffix=" ammonia cracker", - bus0=nodes + " NH3", + bus0=spatial.ammonia.nodes, bus1=nodes + " H2", p_nom_extendable=True, carrier="ammonia cracker", @@ -698,9 +710,9 @@ def add_ammonia(n, costs): # Ammonia Storage n.madd("Store", - nodes, + spatial.ammonia.nodes, suffix=" ammonia store", - bus=nodes + " NH3", + bus=spatial.ammonia.nodes, e_nom_extendable=True, e_cyclic=True, carrier="ammonia store", From 27ac40d2eaa1308830b862391c69a6c2e8961922 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 10 Jun 2022 16:53:37 +0200 Subject: [PATCH 007/234] use spatial namespace to manage ammonia resolution --- config.default.yaml | 2 +- scripts/prepare_sector_network.py | 31 +++++++++++++++++++++---------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/config.default.yaml b/config.default.yaml index d2291f58..d0887bc8 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -244,7 +244,7 @@ sector: # - onshore # more than 50 km from sea - nearshore # within 50 km of sea # - offshore - ammonia: false + ammonia: false # can be false (no NH3 carrier), true (copperplated NH3), "regional" (regionalised NH3 without network) use_fischer_tropsch_waste_heat: true use_fuel_cell_waste_heat: true electricity_distribution_grid: true diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index dd4e6e39..f0e94c19 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -93,6 +93,18 @@ def define_spatial(nodes, options): spatial.gas.df = pd.DataFrame(vars(spatial.gas), index=nodes) + # ammonia + + spatial.ammonia = SimpleNamespace() + if options["ammonia"] == "regional": + spatial.ammonia.nodes = nodes + " NH3" + spatial.ammonia.locations = nodes + else: + spatial.ammonia.nodes = ["EU ammonia"] + spatial.ammonia.locations = ["EU"] + + spatial.ammonia.df = pd.DataFrame(vars(spatial.ammonia), index=nodes) + # oil spatial.oil = SimpleNamespace() spatial.oil.nodes = ["EU oil"] @@ -656,7 +668,7 @@ def add_generation(n, costs): def add_ammonia(n, costs): - logger.info("adding ammonia carrier") + logger.info("adding ammonia carrier with synthesis, cracking and storage") nodes = pop_layout.index @@ -665,8 +677,8 @@ def add_ammonia(n, costs): n.add("Carrier", "NH3") n.madd("Bus", - nodes + " NH3", - location=nodes, + spatial.ammonia.nodes, + location=spatial.ammonia.locations, carrier="NH3" ) @@ -674,7 +686,7 @@ def add_ammonia(n, costs): nodes, suffix=" Haber-Bosch", bus0=nodes, - bus1=nodes + " NH3", + bus1=spatial.ammonia.nodes, bus2=nodes + " H2", p_nom_extendable=True, carrier="Haber-Bosch", @@ -687,7 +699,7 @@ def add_ammonia(n, costs): n.madd("Link", nodes, suffix=" ammonia cracker", - bus0=nodes + " NH3", + bus0=spatial.ammonia.nodes, bus1=nodes + " H2", p_nom_extendable=True, carrier="ammonia cracker", @@ -698,9 +710,9 @@ def add_ammonia(n, costs): # Ammonia Storage n.madd("Store", - nodes, + spatial.ammonia.nodes, suffix=" ammonia store", - bus=nodes + " NH3", + bus=spatial.ammonia.nodes, e_nom_extendable=True, e_cyclic=True, carrier="ammonia store", @@ -2205,9 +2217,8 @@ def add_industry(n, costs): if options["ammonia"]: n.madd("Load", - nodes, - suffix=" NH3", - bus=nodes + " NH3", + spatial.ammonia.nodes, + bus=spatial.ammonia.nodes, carrier="NH3", p_set=industrial_demand.loc[nodes, "ammonia"] / 8760 ) From 4ecfccea6cce75e39507c3f95dae0102dd4706aa Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 10 Jun 2022 17:07:48 +0200 Subject: [PATCH 008/234] handle ammonia demand both regionalised and copperplated --- scripts/prepare_sector_network.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index f0e94c19..64c1868f 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2216,11 +2216,17 @@ def add_industry(n, costs): ) if options["ammonia"]: + + if options["ammonia"] == 'regional': + p_set = industrial_demand.loc[spatial.ammonia.locations, "ammonia"].rename(index=lambda x: x + " NH3") / 8760 + else: + p_set = industrial_demand["ammonia"].sum() / 8760 + n.madd("Load", spatial.ammonia.nodes, bus=spatial.ammonia.nodes, carrier="NH3", - p_set=industrial_demand.loc[nodes, "ammonia"] / 8760 + p_set=p_set ) From 37c052667a78c1cd473575c9f2eacd70252997c5 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 22 Jun 2022 15:50:32 +0200 Subject: [PATCH 009/234] handle absent ammonia config flag --- scripts/prepare_sector_network.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 64c1868f..64207f0c 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -96,7 +96,7 @@ def define_spatial(nodes, options): # ammonia spatial.ammonia = SimpleNamespace() - if options["ammonia"] == "regional": + if options.get("ammonia") == "regional": spatial.ammonia.nodes = nodes + " NH3" spatial.ammonia.locations = nodes else: @@ -2215,7 +2215,7 @@ def add_industry(n, costs): lifetime=costs.at['cement capture', 'lifetime'] ) - if options["ammonia"]: + if options.get("ammonia"): if options["ammonia"] == 'regional': p_set = industrial_demand.loc[spatial.ammonia.locations, "ammonia"].rename(index=lambda x: x + " NH3") / 8760 From 9f91af28e727e77285c9761876b6e5657dd71ed4 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 23 Jun 2022 15:17:41 +0200 Subject: [PATCH 010/234] fix syntax errors --- scripts/plot_network.py | 6 +++--- scripts/plot_summary.py | 4 ++-- scripts/prepare_sector_network.py | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/scripts/plot_network.py b/scripts/plot_network.py index 61f91df1..3f7c6960 100644 --- a/scripts/plot_network.py +++ b/scripts/plot_network.py @@ -309,7 +309,7 @@ def plot_h2_map(network): ) n.plot( - geomap=False, + # geomap=False, bus_sizes=0, link_colors='#72d3d6', link_widths=link_widths_retro, @@ -443,7 +443,7 @@ def plot_ch4_map(network): ) n.plot( - geomap=False, + # geomap=False, ax=ax, bus_sizes=0., link_colors='#e8d1d1', @@ -453,7 +453,7 @@ def plot_ch4_map(network): ) n.plot( - geomap=False, + # geomap=False, ax=ax, bus_sizes=0., link_colors=link_color_used, diff --git a/scripts/plot_summary.py b/scripts/plot_summary.py index 97166f54..f58c81af 100644 --- a/scripts/plot_summary.py +++ b/scripts/plot_summary.py @@ -51,7 +51,7 @@ def rename_techs(label): "ror": "hydroelectricity", "hydro": "hydroelectricity", "PHS": "hydroelectricity", - "NH3": "ammonia" + "NH3": "ammonia", "co2 Store": "DAC", "co2 stored": "CO2 sequestration", "AC": "transmission lines", @@ -256,7 +256,7 @@ def plot_balances(): df = df / 1e6 #remove trailing link ports - df.index = [i[:-1] if ((i != "co2") and (i[-1:] in ["0","1","2","3"])) else i for i in df.index] + df.index = [i[:-1] if ((i not in ["co2", "NH3"]) and (i[-1:] in ["0","1","2","3"])) else i for i in df.index] df = df.groupby(df.index.map(rename_techs)).sum() diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 64207f0c..f56b3606 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -100,7 +100,7 @@ def define_spatial(nodes, options): spatial.ammonia.nodes = nodes + " NH3" spatial.ammonia.locations = nodes else: - spatial.ammonia.nodes = ["EU ammonia"] + spatial.ammonia.nodes = ["EU NH3"] spatial.ammonia.locations = ["EU"] spatial.ammonia.df = pd.DataFrame(vars(spatial.ammonia), index=nodes) @@ -690,8 +690,8 @@ def add_ammonia(n, costs): bus2=nodes + " H2", p_nom_extendable=True, carrier="Haber-Bosch", - efficiency=1 / (cf_industry["MWh_elec_per_tNH3_electrolysis"] / cf_industry["MWh_NH3_per_tNH3"]) # output: MW_NH3 per MW_elec - efficiency2=-cf_industry["MWh_H2_per_tNH3_electrolysis"] / cf_industry["MWh_elec_per_tNH3_electrolysis"] # input: MW_H2 per MW_elec + efficiency=1 / (cf_industry["MWh_elec_per_tNH3_electrolysis"] / cf_industry["MWh_NH3_per_tNH3"]), # output: MW_NH3 per MW_elec + efficiency2=-cf_industry["MWh_H2_per_tNH3_electrolysis"] / cf_industry["MWh_elec_per_tNH3_electrolysis"], # input: MW_H2 per MW_elec capital_cost=costs.at["Haber-Bosch synthesis", "fixed"], lifetime=costs.at["Haber-Bosch synthesis", 'lifetime'] ) @@ -703,7 +703,7 @@ def add_ammonia(n, costs): bus1=nodes + " H2", p_nom_extendable=True, carrier="ammonia cracker", - efficiency=1 / cf_industry["MWh_NH3_per_MWh_H2_cracker"] + efficiency=1 / cf_industry["MWh_NH3_per_MWh_H2_cracker"], capital_cost=costs.at["Ammonia cracker", "fixed"] / cf_industry["MWh_NH3_per_MWh_H2_cracker"], # given per MW_H2 lifetime=costs.at['Ammonia cracker', 'lifetime'] ) From d69efe3d1fc817f0709e4b8d892cde8f3381acb4 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 23 Jun 2022 15:22:21 +0200 Subject: [PATCH 011/234] add ammonia color to config.default.yaml --- config.default.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/config.default.yaml b/config.default.yaml index d0887bc8..3621b072 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -576,6 +576,7 @@ plotting: H2 Electrolysis: '#ff29d9' # ammonia NH3: '#46caf0' + ammonia: '#46caf0' ammonia store: '#00ace0' ammonia cracker: '#87d0e6' Haber-Bosch: '#076987' From 203753455770d2b82b56c7d621a4a7e189b34128 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 23 Jun 2022 15:24:44 +0200 Subject: [PATCH 012/234] prepare: only add ammonia to spatial if config selected --- scripts/prepare_sector_network.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index f56b3606..d6269413 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -95,15 +95,16 @@ def define_spatial(nodes, options): # ammonia - spatial.ammonia = SimpleNamespace() - if options.get("ammonia") == "regional": - spatial.ammonia.nodes = nodes + " NH3" - spatial.ammonia.locations = nodes - else: - spatial.ammonia.nodes = ["EU NH3"] - spatial.ammonia.locations = ["EU"] + if options.get('ammonia'): + spatial.ammonia = SimpleNamespace() + if options.get("ammonia") == "regional": + spatial.ammonia.nodes = nodes + " NH3" + spatial.ammonia.locations = nodes + else: + spatial.ammonia.nodes = ["EU NH3"] + spatial.ammonia.locations = ["EU"] - spatial.ammonia.df = pd.DataFrame(vars(spatial.ammonia), index=nodes) + spatial.ammonia.df = pd.DataFrame(vars(spatial.ammonia), index=nodes) # oil spatial.oil = SimpleNamespace() From e1a6e13f750e84e7b0630d7303c675ab423a024e Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 23 Jun 2022 15:28:00 +0200 Subject: [PATCH 013/234] add release note [no ci] --- doc/release_notes.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 7808d2ba..552ffccb 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -62,6 +62,11 @@ incorporates retrofitting options to hydrogen. * Add option to sweep the global CO2 sequestration potentials with keyword ``seq200`` in the ``{sector_opts}`` wildcard (for limit of 200 Mt CO2). +* Add option to resolve ammonia as separate energy carrier with Haber-Bosch + synthesis, ammonia cracking, storage and industrial demand. The ammonia + carrier can be nodally resolved or copperplated across Europe. This feature is + controlled by ``sector: ammonia:``. + * Updated `data bundle `_ that includes the hydrogan salt cavern storage potentials. **Bugfixes** From b96207c73e900f235eb8df5db7f89928057a1034 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 30 Jun 2022 08:42:18 +0200 Subject: [PATCH 014/234] store config and wildcards in n.meta --- scripts/add_brownfield.py | 1 + scripts/add_existing_baseyear.py | 1 + scripts/matplotlibrc | 4 + scripts/plot_network.py.bak | 737 ++++++++++++++++++++++++++++++ scripts/prepare_sector_network.py | 1 + scripts/solve_network.py | 1 + 6 files changed, 745 insertions(+) create mode 100644 scripts/matplotlibrc create mode 100644 scripts/plot_network.py.bak diff --git a/scripts/add_brownfield.py b/scripts/add_brownfield.py index d7418a79..9f13da1f 100644 --- a/scripts/add_brownfield.py +++ b/scripts/add_brownfield.py @@ -137,4 +137,5 @@ if __name__ == "__main__": add_brownfield(n, n_p, year) + n.meta = dict(snakemake.config, **dict(wildcards=dict(snakemake.wildcards))) n.export_to_netcdf(snakemake.output[0]) diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index 7bb88a00..b4f9dc6e 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -493,4 +493,5 @@ if __name__ == "__main__": default_lifetime = snakemake.config['costs']['lifetime'] add_heating_capacities_installed_before_baseyear(n, baseyear, grouping_years, ashp_cop, gshp_cop, time_dep_hp_cop, costs, default_lifetime) + n.meta = dict(snakemake.config, **dict(wildcards=dict(snakemake.wildcards))) n.export_to_netcdf(snakemake.output[0]) diff --git a/scripts/matplotlibrc b/scripts/matplotlibrc new file mode 100644 index 00000000..db5e7ce8 --- /dev/null +++ b/scripts/matplotlibrc @@ -0,0 +1,4 @@ +backend: Agg +font.family: sans-serif +font.sans-serif: Ubuntu, DejaVu Sans +image.cmap: viridis \ No newline at end of file diff --git a/scripts/plot_network.py.bak b/scripts/plot_network.py.bak new file mode 100644 index 00000000..4a1bc6d0 --- /dev/null +++ b/scripts/plot_network.py.bak @@ -0,0 +1,737 @@ +import pypsa + +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt +import cartopy.crs as ccrs + +from matplotlib.legend_handler import HandlerPatch +from matplotlib.patches import Circle, Ellipse + +from make_summary import assign_carriers +from plot_summary import rename_techs, preferred_order +from helper import override_component_attrs + +plt.style.use('ggplot') + + +def rename_techs_tyndp(tech): + tech = rename_techs(tech) + if "heat pump" in tech or "resistive heater" in tech: + return "power-to-heat" + elif tech in ["H2 Electrolysis", "methanation", "helmeth", "H2 liquefaction"]: + return "power-to-gas" + elif tech == "H2": + return "H2 storage" + elif tech in ["OCGT", "CHP", "gas boiler", "H2 Fuel Cell"]: + return "gas-to-power/heat" + elif "solar" in tech: + return "solar" + elif tech == "Fischer-Tropsch": + return "power-to-liquid" + elif "offshore wind" in tech: + return "offshore wind" + elif "CC" in tech or "sequestration" in tech: + return "CCS" + else: + return tech + + +def make_handler_map_to_scale_circles_as_in(ax, dont_resize_actively=False): + fig = ax.get_figure() + + def axes2pt(): + return np.diff(ax.transData.transform([(0, 0), (1, 1)]), axis=0)[0] * (72. / fig.dpi) + + ellipses = [] + if not dont_resize_actively: + def update_width_height(event): + dist = axes2pt() + for e, radius in ellipses: + e.width, e.height = 2. * radius * dist + fig.canvas.mpl_connect('resize_event', update_width_height) + ax.callbacks.connect('xlim_changed', update_width_height) + ax.callbacks.connect('ylim_changed', update_width_height) + + def legend_circle_handler(legend, orig_handle, xdescent, ydescent, + width, height, fontsize): + w, h = 2. * orig_handle.get_radius() * axes2pt() + e = Ellipse(xy=(0.5 * width - 0.5 * xdescent, 0.5 * + height - 0.5 * ydescent), width=w, height=w) + ellipses.append((e, orig_handle.get_radius())) + return e + return {Circle: HandlerPatch(patch_func=legend_circle_handler)} + + +def make_legend_circles_for(sizes, scale=1.0, **kw): + return [Circle((0, 0), radius=(s / scale)**0.5, **kw) for s in sizes] + + +def assign_location(n): + for c in n.iterate_components(n.one_port_components | n.branch_components): + ifind = pd.Series(c.df.index.str.find(" ", start=4), c.df.index) + for i in ifind.value_counts().index: + # these have already been assigned defaults + if i == -1: continue + names = ifind.index[ifind == i] + c.df.loc[names, 'location'] = names.str[:i] + + +def plot_map(network, components=["links", "stores", "storage_units", "generators"], + bus_size_factor=1.7e10, transmission=False): + + n = network.copy() + assign_location(n) + # Drop non-electric buses so they don't clutter the plot + n.buses.drop(n.buses.index[n.buses.carrier != "AC"], inplace=True) + + costs = pd.DataFrame(index=n.buses.index) + + for comp in components: + df_c = getattr(n, comp) + df_c["nice_group"] = df_c.carrier.map(rename_techs_tyndp) + + attr = "e_nom_opt" if comp == "stores" else "p_nom_opt" + + costs_c = ((df_c.capital_cost * df_c[attr]) + .groupby([df_c.location, df_c.nice_group]).sum() + .unstack().fillna(0.)) + costs = pd.concat([costs, costs_c], axis=1) + + print(comp, costs) + + costs = costs.groupby(costs.columns, axis=1).sum() + + costs.drop(list(costs.columns[(costs == 0.).all()]), axis=1, inplace=True) + + new_columns = (preferred_order.intersection(costs.columns) + .append(costs.columns.difference(preferred_order))) + costs = costs[new_columns] + + for item in new_columns: + if item not in snakemake.config['plotting']['tech_colors']: + print("Warning!",item,"not in config/plotting/tech_colors") + + costs = costs.stack() # .sort_index() + + # hack because impossible to drop buses... + eu_location = snakemake.config["plotting"].get("eu_node_location", dict(x=-5.5, y=46)) + n.buses.loc["EU gas", "x"] = eu_location["x"] + n.buses.loc["EU gas", "y"] = eu_location["y"] + + n.links.drop(n.links.index[(n.links.carrier != "DC") & ( + n.links.carrier != "B2B")], inplace=True) + + # drop non-bus + to_drop = costs.index.levels[0].symmetric_difference(n.buses.index) + if len(to_drop) != 0: + print("dropping non-buses", to_drop) + costs.drop(to_drop, level=0, inplace=True, axis=0, errors="ignore") + + # make sure they are removed from index + costs.index = pd.MultiIndex.from_tuples(costs.index.values) + + # PDF has minimum width, so set these to zero + line_lower_threshold = 500. + line_upper_threshold = 1e4 + linewidth_factor = 2e3 + ac_color = "gray" + dc_color = "m" + + if snakemake.wildcards["lv"] == "1.0": + # should be zero + line_widths = n.lines.s_nom_opt - n.lines.s_nom + link_widths = n.links.p_nom_opt - n.links.p_nom + title = "Transmission reinforcement" + + if transmission: + line_widths = n.lines.s_nom_opt + link_widths = n.links.p_nom_opt + linewidth_factor = 2e3 + line_lower_threshold = 0. + title = "Today's transmission" + else: + line_widths = n.lines.s_nom_opt - n.lines.s_nom_min + link_widths = n.links.p_nom_opt - n.links.p_nom_min + title = "Transmission reinforcement" + + if transmission: + line_widths = n.lines.s_nom_opt + link_widths = n.links.p_nom_opt + title = "Total transmission" + + line_widths[line_widths < line_lower_threshold] = 0. + link_widths[link_widths < line_lower_threshold] = 0. + + line_widths[line_widths > line_upper_threshold] = line_upper_threshold + link_widths[link_widths > line_upper_threshold] = line_upper_threshold + + fig, ax = plt.subplots(subplot_kw={"projection": ccrs.PlateCarree()}) + fig.set_size_inches(7, 6) + + n.plot( + bus_sizes=costs / bus_size_factor, + bus_colors=snakemake.config['plotting']['tech_colors'], + line_colors=ac_color, + link_colors=dc_color, + line_widths=line_widths / linewidth_factor, + link_widths=link_widths / linewidth_factor, + ax=ax, **map_opts + ) + + handles = make_legend_circles_for( + [5e9, 1e9], + scale=bus_size_factor, + facecolor="gray" + ) + + labels = ["{} bEUR/a".format(s) for s in (5, 1)] + + l2 = ax.legend( + handles, labels, + loc="upper left", + bbox_to_anchor=(0.01, 1.01), + labelspacing=1.0, + frameon=False, + title='System cost', + handler_map=make_handler_map_to_scale_circles_as_in(ax) + ) + + ax.add_artist(l2) + + handles = [] + labels = [] + + for s in (10, 5): + handles.append(plt.Line2D([0], [0], color=ac_color, + linewidth=s * 1e3 / linewidth_factor)) + labels.append("{} GW".format(s)) + + l1_1 = ax.legend( + handles, labels, + loc="upper left", + bbox_to_anchor=(0.22, 1.01), + frameon=False, + labelspacing=0.8, + handletextpad=1.5, + title=title + ) + + ax.add_artist(l1_1) + + fig.savefig( + snakemake.output.map, + transparent=True, + bbox_inches="tight" + ) + +def group_pipes(df, drop_direction=False): + """Group pipes which connect same buses and return overall capacity. + """ + if drop_direction: + positive_order = df.bus0 < df.bus1 + df_p = df[positive_order] + swap_buses = {"bus0": "bus1", "bus1": "bus0"} + df_n = df[~positive_order].rename(columns=swap_buses) + df = pd.concat([df_p, df_n]) + + # there are pipes for each investment period rename to AC buses name for plotting + df.index = df.apply( + lambda x: f"H2 pipeline {x.bus0.replace(' H2', '')} -> {x.bus1.replace(' H2', '')}", + axis=1 + ) + # group pipe lines connecting the same buses and rename them for plotting + pipe_capacity = df["p_nom_opt"].groupby(level=0).sum() + + return pipe_capacity + + +def plot_h2_map(network): + + n = network.copy() + if "H2 pipeline" not in n.links.carrier.unique(): + return + + assign_location(n) + + bus_size_factor = 1e5 + linewidth_factor = 1e4 + # MW below which not drawn + line_lower_threshold = 1e2 + + # Drop non-electric buses so they don't clutter the plot + n.buses.drop(n.buses.index[n.buses.carrier != "AC"], inplace=True) + + elec = n.links[n.links.carrier.isin(["H2 Electrolysis", "H2 Fuel Cell"])].index + + bus_sizes = n.links.loc[elec,"p_nom_opt"].groupby([n.links["bus0"], n.links.carrier]).sum() / bus_size_factor + + # make a fake MultiIndex so that area is correct for legend + bus_sizes.rename(index=lambda x: x.replace(" H2", ""), level=0, inplace=True) + # drop all links which are not H2 pipelines + n.links.drop(n.links.index[~n.links.carrier.str.contains("H2 pipeline")], inplace=True) + + h2_new = n.links.loc[n.links.carrier=="H2 pipeline"] + h2_retro = n.links.loc[n.links.carrier=='H2 pipeline retrofitted'] + # sum capacitiy for pipelines from different investment periods + h2_new = group_pipes(h2_new) + h2_retro = group_pipes(h2_retro, drop_direction=True).reindex(h2_new.index).fillna(0) + + + n.links.rename(index=lambda x: x.split("-2")[0], inplace=True) + n.links = n.links.groupby(level=0).first() + link_widths_total = (h2_new + h2_retro) / linewidth_factor + link_widths_total = link_widths_total.reindex(n.links.index).fillna(0.) + link_widths_total[n.links.p_nom_opt < line_lower_threshold] = 0. + + retro = n.links.p_nom_opt.where(n.links.carrier=='H2 pipeline retrofitted', other=0.) + link_widths_retro = retro / linewidth_factor + link_widths_retro[n.links.p_nom_opt < line_lower_threshold] = 0. + + n.links.bus0 = n.links.bus0.str.replace(" H2", "") + n.links.bus1 = n.links.bus1.str.replace(" H2", "") + + fig, ax = plt.subplots( + figsize=(7, 6), + subplot_kw={"projection": ccrs.PlateCarree()} + ) + + n.plot( + bus_sizes=bus_sizes, + bus_colors=snakemake.config['plotting']['tech_colors'], + link_colors='#a2f0f2', + link_widths=link_widths_total, + branch_components=["Link"], + ax=ax, + **map_opts + ) + + n.plot( + geomap=False, + bus_sizes=0, + link_colors='#72d3d6', + link_widths=link_widths_retro, + branch_components=["Link"], + ax=ax, + **map_opts + ) + + handles = make_legend_circles_for( + [50000, 10000], + scale=bus_size_factor, + facecolor='grey' + ) + + labels = ["{} GW".format(s) for s in (50, 10)] + + l2 = ax.legend( + handles, labels, + loc="upper left", + bbox_to_anchor=(-0.03, 1.01), + labelspacing=1.0, + frameon=False, + title='Electrolyzer capacity', + handler_map=make_handler_map_to_scale_circles_as_in(ax) + ) + + ax.add_artist(l2) + + handles = [] + labels = [] + + for s in (50, 10): + handles.append(plt.Line2D([0], [0], color="grey", + linewidth=s * 1e3 / linewidth_factor)) + labels.append("{} GW".format(s)) + + l1_1 = ax.legend( + handles, labels, + loc="upper left", + bbox_to_anchor=(0.28, 1.01), + frameon=False, + labelspacing=0.8, + handletextpad=1.5, + title='H2 pipeline capacity' + ) + + ax.add_artist(l1_1) + + fig.savefig( + snakemake.output.map.replace("-costs-all","-h2_network"), + bbox_inches="tight" + ) + + +def plot_ch4_map(network): + + n = network.copy() + + if "gas pipeline" not in n.links.carrier.unique(): + return + + assign_location(n) + + bus_size_factor = 8e7 + linewidth_factor = 1e4 + # MW below which not drawn + line_lower_threshold = 500 + + # Drop non-electric buses so they don't clutter the plot + n.buses.drop(n.buses.index[n.buses.carrier != "AC"], inplace=True) + + fossil_gas_i = n.generators[n.generators.carrier=="gas"].index + fossil_gas = n.generators_t.p.loc[:,fossil_gas_i].mul(n.snapshot_weightings.generators, axis=0).sum().groupby(n.generators.loc[fossil_gas_i,"bus"]).sum() / bus_size_factor + fossil_gas.rename(index=lambda x: x.replace(" gas", ""), inplace=True) + fossil_gas = fossil_gas.reindex(n.buses.index).fillna(0) + # make a fake MultiIndex so that area is correct for legend + fossil_gas.index = pd.MultiIndex.from_product([fossil_gas.index, ["fossil gas"]]) + + methanation_i = n.links[n.links.carrier.isin(["helmeth", "Sabatier"])].index + methanation = abs(n.links_t.p1.loc[:,methanation_i].mul(n.snapshot_weightings.generators, axis=0)).sum().groupby(n.links.loc[methanation_i,"bus1"]).sum() / bus_size_factor + methanation = methanation.groupby(methanation.index).sum().rename(index=lambda x: x.replace(" gas", "")) + # make a fake MultiIndex so that area is correct for legend + methanation.index = pd.MultiIndex.from_product([methanation.index, ["methanation"]]) + + biogas_i = n.stores[n.stores.carrier=="biogas"].index + biogas = n.stores_t.p.loc[:,biogas_i].mul(n.snapshot_weightings.generators, axis=0).sum().groupby(n.stores.loc[biogas_i,"bus"]).sum() / bus_size_factor + biogas = biogas.groupby(biogas.index).sum().rename(index=lambda x: x.replace(" biogas", "")) + # make a fake MultiIndex so that area is correct for legend + biogas.index = pd.MultiIndex.from_product([biogas.index, ["biogas"]]) + + bus_sizes = pd.concat([fossil_gas, methanation, biogas]) + bus_sizes.sort_index(inplace=True) + + to_remove = n.links.index[~n.links.carrier.str.contains("gas pipeline")] + n.links.drop(to_remove, inplace=True) + + link_widths_rem = n.links.p_nom_opt / linewidth_factor + link_widths_rem[n.links.p_nom_opt < line_lower_threshold] = 0. + + link_widths_orig = n.links.p_nom / linewidth_factor + link_widths_orig[n.links.p_nom < line_lower_threshold] = 0. + + max_usage = n.links_t.p0.abs().max(axis=0) + link_widths_used = max_usage / linewidth_factor + link_widths_used[max_usage < line_lower_threshold] = 0. + + link_color_used = n.links.carrier.map({"gas pipeline": "#f08080", + "gas pipeline new": "#c46868"}) + + n.links.bus0 = n.links.bus0.str.replace(" gas", "") + n.links.bus1 = n.links.bus1.str.replace(" gas", "") + + tech_colors = snakemake.config['plotting']['tech_colors'] + + bus_colors = { + "fossil gas": tech_colors["fossil gas"], + "methanation": tech_colors["methanation"], + "biogas": "seagreen" + } + + fig, ax = plt.subplots(figsize=(7,6), subplot_kw={"projection": ccrs.PlateCarree()}) + + n.plot( + bus_sizes=bus_sizes, + bus_colors=bus_colors, + link_colors='lightgrey', + link_widths=link_widths_orig, + branch_components=["Link"], + ax=ax, + **map_opts + ) + + n.plot( + geomap=False, + ax=ax, + bus_sizes=0., + link_colors='#e8d1d1', + link_widths=link_widths_rem, + branch_components=["Link"], + **map_opts + ) + + n.plot( + geomap=False, + ax=ax, + bus_sizes=0., + link_colors=link_color_used, + link_widths=link_widths_used, + branch_components=["Link"], + **map_opts + ) + + handles = make_legend_circles_for( + [10e6, 100e6], + scale=bus_size_factor, + facecolor='grey' + ) + labels = ["{} TWh".format(s) for s in (10, 100)] + + l2 = ax.legend( + handles, labels, + loc="upper left", + bbox_to_anchor=(-0.03, 1.01), + labelspacing=1.0, + frameon=False, + title='gas generation', + handler_map=make_handler_map_to_scale_circles_as_in(ax) + ) + + ax.add_artist(l2) + + handles = [] + labels = [] + + for s in (50, 10): + handles.append(plt.Line2D([0], [0], color="grey", linewidth=s * 1e3 / linewidth_factor)) + labels.append("{} GW".format(s)) + + l1_1 = ax.legend( + handles, labels, + loc="upper left", + bbox_to_anchor=(0.28, 1.01), + frameon=False, + labelspacing=0.8, + handletextpad=1.5, + title='gas pipeline used capacity' + ) + + ax.add_artist(l1_1) + + fig.savefig( + snakemake.output.map.replace("-costs-all","-ch4_network"), + bbox_inches="tight" + ) + + +def plot_map_without(network): + + n = network.copy() + assign_location(n) + + # Drop non-electric buses so they don't clutter the plot + n.buses.drop(n.buses.index[n.buses.carrier != "AC"], inplace=True) + + fig, ax = plt.subplots( + figsize=(7, 6), + subplot_kw={"projection": ccrs.PlateCarree()} + ) + + # PDF has minimum width, so set these to zero + line_lower_threshold = 200. + line_upper_threshold = 1e4 + linewidth_factor = 2e3 + ac_color = "gray" + dc_color = "m" + + # hack because impossible to drop buses... + if "EU gas" in n.buses.index: + eu_location = snakemake.config["plotting"].get("eu_node_location", dict(x=-5.5, y=46)) + n.buses.loc["EU gas", "x"] = eu_location["x"] + n.buses.loc["EU gas", "y"] = eu_location["y"] + + to_drop = n.links.index[(n.links.carrier != "DC") & (n.links.carrier != "B2B")] + n.links.drop(to_drop, inplace=True) + + if snakemake.wildcards["lv"] == "1.0": + line_widths = n.lines.s_nom + link_widths = n.links.p_nom + else: + line_widths = n.lines.s_nom_min + link_widths = n.links.p_nom_min + + line_widths[line_widths < line_lower_threshold] = 0. + link_widths[link_widths < line_lower_threshold] = 0. + + line_widths[line_widths > line_upper_threshold] = line_upper_threshold + link_widths[link_widths > line_upper_threshold] = line_upper_threshold + + n.plot( + bus_colors="k", + line_colors=ac_color, + link_colors=dc_color, + line_widths=line_widths / linewidth_factor, + link_widths=link_widths / linewidth_factor, + ax=ax, **map_opts + ) + + handles = [] + labels = [] + + for s in (10, 5): + handles.append(plt.Line2D([0], [0], color=ac_color, + linewidth=s * 1e3 / linewidth_factor)) + labels.append("{} GW".format(s)) + l1_1 = ax.legend(handles, labels, + loc="upper left", bbox_to_anchor=(0.05, 1.01), + frameon=False, + labelspacing=0.8, handletextpad=1.5, + title='Today\'s transmission') + ax.add_artist(l1_1) + + fig.savefig( + snakemake.output.today, + transparent=True, + bbox_inches="tight" + ) + + +def plot_series(network, carrier="AC", name="test"): + + n = network.copy() + assign_location(n) + assign_carriers(n) + + buses = n.buses.index[n.buses.carrier.str.contains(carrier)] + + supply = pd.DataFrame(index=n.snapshots) + for c in n.iterate_components(n.branch_components): + n_port = 4 if c.name=='Link' else 2 + for i in range(n_port): + supply = pd.concat((supply, + (-1) * c.pnl["p" + str(i)].loc[:, + c.df.index[c.df["bus" + str(i)].isin(buses)]].groupby(c.df.carrier, + axis=1).sum()), + axis=1) + + for c in n.iterate_components(n.one_port_components): + comps = c.df.index[c.df.bus.isin(buses)] + supply = pd.concat((supply, ((c.pnl["p"].loc[:, comps]).multiply( + c.df.loc[comps, "sign"])).groupby(c.df.carrier, axis=1).sum()), axis=1) + + supply = supply.groupby(rename_techs_tyndp, axis=1).sum() + + both = supply.columns[(supply < 0.).any() & (supply > 0.).any()] + + positive_supply = supply[both] + negative_supply = supply[both] + + positive_supply[positive_supply < 0.] = 0. + negative_supply[negative_supply > 0.] = 0. + + supply[both] = positive_supply + + suffix = " charging" + + negative_supply.columns = negative_supply.columns + suffix + + supply = pd.concat((supply, negative_supply), axis=1) + + # 14-21.2 for flaute + # 19-26.1 for flaute + + start = "2013-02-19" + stop = "2013-02-26" + + threshold = 10e3 + + to_drop = supply.columns[(abs(supply) < threshold).all()] + + if len(to_drop) != 0: + print("dropping", to_drop) + supply.drop(columns=to_drop, inplace=True) + + supply.index.name = None + + supply = supply / 1e3 + + supply.rename(columns={"electricity": "electric demand", + "heat": "heat demand"}, + inplace=True) + supply.columns = supply.columns.str.replace("residential ", "") + supply.columns = supply.columns.str.replace("services ", "") + supply.columns = supply.columns.str.replace("urban decentral ", "decentral ") + + preferred_order = pd.Index(["electric demand", + "transmission lines", + "hydroelectricity", + "hydro reservoir", + "run of river", + "pumped hydro storage", + "CHP", + "onshore wind", + "offshore wind", + "solar PV", + "solar thermal", + "building retrofitting", + "ground heat pump", + "air heat pump", + "resistive heater", + "OCGT", + "gas boiler", + "gas", + "natural gas", + "methanation", + "hydrogen storage", + "battery storage", + "hot water storage"]) + + new_columns = (preferred_order.intersection(supply.columns) + .append(supply.columns.difference(preferred_order))) + + supply = supply.groupby(supply.columns, axis=1).sum() + fig, ax = plt.subplots() + fig.set_size_inches((8, 5)) + + (supply.loc[start:stop, new_columns] + .plot(ax=ax, kind="area", stacked=True, linewidth=0., + color=[snakemake.config['plotting']['tech_colors'][i.replace(suffix, "")] + for i in new_columns])) + + handles, labels = ax.get_legend_handles_labels() + + handles.reverse() + labels.reverse() + + new_handles = [] + new_labels = [] + + for i, item in enumerate(labels): + if "charging" not in item: + new_handles.append(handles[i]) + new_labels.append(labels[i]) + + ax.legend(new_handles, new_labels, ncol=3, loc="upper left", frameon=False) + ax.set_xlim([start, stop]) + ax.set_ylim([-1300, 1900]) + ax.grid(True) + ax.set_ylabel("Power [GW]") + fig.tight_layout() + + fig.savefig("{}{}/maps/series-{}-{}-{}-{}-{}.pdf".format( + snakemake.config['results_dir'], snakemake.config['run'], + snakemake.wildcards["lv"], + carrier, start, stop, name), + transparent=True) + + +if __name__ == "__main__": + if 'snakemake' not in globals(): + from helper import mock_snakemake + snakemake = mock_snakemake( + 'plot_network', + simpl='', + clusters="45", + lv=1.0, + opts='', + sector_opts='168H-T-H-B-I-A-solar+p3-dist1', + planning_horizons="2050", + ) + + overrides = override_component_attrs(snakemake.input.overrides) + n = pypsa.Network(snakemake.input.network, override_component_attrs=overrides) + + map_opts = snakemake.config['plotting']['map'] + + plot_map(n, + components=["generators", "links", "stores", "storage_units"], + bus_size_factor=1.5e10, + transmission=False + ) + + plot_h2_map(n) + plot_ch4_map(n) + plot_map_without(n) + + #plot_series(n, carrier="AC", name=suffix) + #plot_series(n, carrier="heat", name=suffix) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 5f75b1db..a1916c05 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2447,4 +2447,5 @@ if __name__ == "__main__": if options['electricity_grid_connection']: add_electricity_grid_connection(n, costs) + n.meta = dict(snakemake.config, **dict(wildcards=dict(snakemake.wildcards))) n.export_to_netcdf(snakemake.output[0]) diff --git a/scripts/solve_network.py b/scripts/solve_network.py index 4f6cc2c4..3c815e12 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -313,6 +313,7 @@ if __name__ == "__main__": n.line_volume_limit = n.global_constraints.at["lv_limit", "constant"] n.line_volume_limit_dual = n.global_constraints.at["lv_limit", "mu"] + n.meta = dict(snakemake.config, **dict(wildcards=dict(snakemake.wildcards))) n.export_to_netcdf(snakemake.output[0]) logger.info("Maximum memory usage: {}".format(mem.mem_usage)) From 3134bbb5c5da97b43b3e1039846c669ed76b7a97 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 30 Jun 2022 08:43:46 +0200 Subject: [PATCH 015/234] remove accidental file additions --- scripts/matplotlibrc | 4 - scripts/plot_network.py.bak | 737 ------------------------------------ 2 files changed, 741 deletions(-) delete mode 100644 scripts/matplotlibrc delete mode 100644 scripts/plot_network.py.bak diff --git a/scripts/matplotlibrc b/scripts/matplotlibrc deleted file mode 100644 index db5e7ce8..00000000 --- a/scripts/matplotlibrc +++ /dev/null @@ -1,4 +0,0 @@ -backend: Agg -font.family: sans-serif -font.sans-serif: Ubuntu, DejaVu Sans -image.cmap: viridis \ No newline at end of file diff --git a/scripts/plot_network.py.bak b/scripts/plot_network.py.bak deleted file mode 100644 index 4a1bc6d0..00000000 --- a/scripts/plot_network.py.bak +++ /dev/null @@ -1,737 +0,0 @@ -import pypsa - -import numpy as np -import pandas as pd -import matplotlib.pyplot as plt -import cartopy.crs as ccrs - -from matplotlib.legend_handler import HandlerPatch -from matplotlib.patches import Circle, Ellipse - -from make_summary import assign_carriers -from plot_summary import rename_techs, preferred_order -from helper import override_component_attrs - -plt.style.use('ggplot') - - -def rename_techs_tyndp(tech): - tech = rename_techs(tech) - if "heat pump" in tech or "resistive heater" in tech: - return "power-to-heat" - elif tech in ["H2 Electrolysis", "methanation", "helmeth", "H2 liquefaction"]: - return "power-to-gas" - elif tech == "H2": - return "H2 storage" - elif tech in ["OCGT", "CHP", "gas boiler", "H2 Fuel Cell"]: - return "gas-to-power/heat" - elif "solar" in tech: - return "solar" - elif tech == "Fischer-Tropsch": - return "power-to-liquid" - elif "offshore wind" in tech: - return "offshore wind" - elif "CC" in tech or "sequestration" in tech: - return "CCS" - else: - return tech - - -def make_handler_map_to_scale_circles_as_in(ax, dont_resize_actively=False): - fig = ax.get_figure() - - def axes2pt(): - return np.diff(ax.transData.transform([(0, 0), (1, 1)]), axis=0)[0] * (72. / fig.dpi) - - ellipses = [] - if not dont_resize_actively: - def update_width_height(event): - dist = axes2pt() - for e, radius in ellipses: - e.width, e.height = 2. * radius * dist - fig.canvas.mpl_connect('resize_event', update_width_height) - ax.callbacks.connect('xlim_changed', update_width_height) - ax.callbacks.connect('ylim_changed', update_width_height) - - def legend_circle_handler(legend, orig_handle, xdescent, ydescent, - width, height, fontsize): - w, h = 2. * orig_handle.get_radius() * axes2pt() - e = Ellipse(xy=(0.5 * width - 0.5 * xdescent, 0.5 * - height - 0.5 * ydescent), width=w, height=w) - ellipses.append((e, orig_handle.get_radius())) - return e - return {Circle: HandlerPatch(patch_func=legend_circle_handler)} - - -def make_legend_circles_for(sizes, scale=1.0, **kw): - return [Circle((0, 0), radius=(s / scale)**0.5, **kw) for s in sizes] - - -def assign_location(n): - for c in n.iterate_components(n.one_port_components | n.branch_components): - ifind = pd.Series(c.df.index.str.find(" ", start=4), c.df.index) - for i in ifind.value_counts().index: - # these have already been assigned defaults - if i == -1: continue - names = ifind.index[ifind == i] - c.df.loc[names, 'location'] = names.str[:i] - - -def plot_map(network, components=["links", "stores", "storage_units", "generators"], - bus_size_factor=1.7e10, transmission=False): - - n = network.copy() - assign_location(n) - # Drop non-electric buses so they don't clutter the plot - n.buses.drop(n.buses.index[n.buses.carrier != "AC"], inplace=True) - - costs = pd.DataFrame(index=n.buses.index) - - for comp in components: - df_c = getattr(n, comp) - df_c["nice_group"] = df_c.carrier.map(rename_techs_tyndp) - - attr = "e_nom_opt" if comp == "stores" else "p_nom_opt" - - costs_c = ((df_c.capital_cost * df_c[attr]) - .groupby([df_c.location, df_c.nice_group]).sum() - .unstack().fillna(0.)) - costs = pd.concat([costs, costs_c], axis=1) - - print(comp, costs) - - costs = costs.groupby(costs.columns, axis=1).sum() - - costs.drop(list(costs.columns[(costs == 0.).all()]), axis=1, inplace=True) - - new_columns = (preferred_order.intersection(costs.columns) - .append(costs.columns.difference(preferred_order))) - costs = costs[new_columns] - - for item in new_columns: - if item not in snakemake.config['plotting']['tech_colors']: - print("Warning!",item,"not in config/plotting/tech_colors") - - costs = costs.stack() # .sort_index() - - # hack because impossible to drop buses... - eu_location = snakemake.config["plotting"].get("eu_node_location", dict(x=-5.5, y=46)) - n.buses.loc["EU gas", "x"] = eu_location["x"] - n.buses.loc["EU gas", "y"] = eu_location["y"] - - n.links.drop(n.links.index[(n.links.carrier != "DC") & ( - n.links.carrier != "B2B")], inplace=True) - - # drop non-bus - to_drop = costs.index.levels[0].symmetric_difference(n.buses.index) - if len(to_drop) != 0: - print("dropping non-buses", to_drop) - costs.drop(to_drop, level=0, inplace=True, axis=0, errors="ignore") - - # make sure they are removed from index - costs.index = pd.MultiIndex.from_tuples(costs.index.values) - - # PDF has minimum width, so set these to zero - line_lower_threshold = 500. - line_upper_threshold = 1e4 - linewidth_factor = 2e3 - ac_color = "gray" - dc_color = "m" - - if snakemake.wildcards["lv"] == "1.0": - # should be zero - line_widths = n.lines.s_nom_opt - n.lines.s_nom - link_widths = n.links.p_nom_opt - n.links.p_nom - title = "Transmission reinforcement" - - if transmission: - line_widths = n.lines.s_nom_opt - link_widths = n.links.p_nom_opt - linewidth_factor = 2e3 - line_lower_threshold = 0. - title = "Today's transmission" - else: - line_widths = n.lines.s_nom_opt - n.lines.s_nom_min - link_widths = n.links.p_nom_opt - n.links.p_nom_min - title = "Transmission reinforcement" - - if transmission: - line_widths = n.lines.s_nom_opt - link_widths = n.links.p_nom_opt - title = "Total transmission" - - line_widths[line_widths < line_lower_threshold] = 0. - link_widths[link_widths < line_lower_threshold] = 0. - - line_widths[line_widths > line_upper_threshold] = line_upper_threshold - link_widths[link_widths > line_upper_threshold] = line_upper_threshold - - fig, ax = plt.subplots(subplot_kw={"projection": ccrs.PlateCarree()}) - fig.set_size_inches(7, 6) - - n.plot( - bus_sizes=costs / bus_size_factor, - bus_colors=snakemake.config['plotting']['tech_colors'], - line_colors=ac_color, - link_colors=dc_color, - line_widths=line_widths / linewidth_factor, - link_widths=link_widths / linewidth_factor, - ax=ax, **map_opts - ) - - handles = make_legend_circles_for( - [5e9, 1e9], - scale=bus_size_factor, - facecolor="gray" - ) - - labels = ["{} bEUR/a".format(s) for s in (5, 1)] - - l2 = ax.legend( - handles, labels, - loc="upper left", - bbox_to_anchor=(0.01, 1.01), - labelspacing=1.0, - frameon=False, - title='System cost', - handler_map=make_handler_map_to_scale_circles_as_in(ax) - ) - - ax.add_artist(l2) - - handles = [] - labels = [] - - for s in (10, 5): - handles.append(plt.Line2D([0], [0], color=ac_color, - linewidth=s * 1e3 / linewidth_factor)) - labels.append("{} GW".format(s)) - - l1_1 = ax.legend( - handles, labels, - loc="upper left", - bbox_to_anchor=(0.22, 1.01), - frameon=False, - labelspacing=0.8, - handletextpad=1.5, - title=title - ) - - ax.add_artist(l1_1) - - fig.savefig( - snakemake.output.map, - transparent=True, - bbox_inches="tight" - ) - -def group_pipes(df, drop_direction=False): - """Group pipes which connect same buses and return overall capacity. - """ - if drop_direction: - positive_order = df.bus0 < df.bus1 - df_p = df[positive_order] - swap_buses = {"bus0": "bus1", "bus1": "bus0"} - df_n = df[~positive_order].rename(columns=swap_buses) - df = pd.concat([df_p, df_n]) - - # there are pipes for each investment period rename to AC buses name for plotting - df.index = df.apply( - lambda x: f"H2 pipeline {x.bus0.replace(' H2', '')} -> {x.bus1.replace(' H2', '')}", - axis=1 - ) - # group pipe lines connecting the same buses and rename them for plotting - pipe_capacity = df["p_nom_opt"].groupby(level=0).sum() - - return pipe_capacity - - -def plot_h2_map(network): - - n = network.copy() - if "H2 pipeline" not in n.links.carrier.unique(): - return - - assign_location(n) - - bus_size_factor = 1e5 - linewidth_factor = 1e4 - # MW below which not drawn - line_lower_threshold = 1e2 - - # Drop non-electric buses so they don't clutter the plot - n.buses.drop(n.buses.index[n.buses.carrier != "AC"], inplace=True) - - elec = n.links[n.links.carrier.isin(["H2 Electrolysis", "H2 Fuel Cell"])].index - - bus_sizes = n.links.loc[elec,"p_nom_opt"].groupby([n.links["bus0"], n.links.carrier]).sum() / bus_size_factor - - # make a fake MultiIndex so that area is correct for legend - bus_sizes.rename(index=lambda x: x.replace(" H2", ""), level=0, inplace=True) - # drop all links which are not H2 pipelines - n.links.drop(n.links.index[~n.links.carrier.str.contains("H2 pipeline")], inplace=True) - - h2_new = n.links.loc[n.links.carrier=="H2 pipeline"] - h2_retro = n.links.loc[n.links.carrier=='H2 pipeline retrofitted'] - # sum capacitiy for pipelines from different investment periods - h2_new = group_pipes(h2_new) - h2_retro = group_pipes(h2_retro, drop_direction=True).reindex(h2_new.index).fillna(0) - - - n.links.rename(index=lambda x: x.split("-2")[0], inplace=True) - n.links = n.links.groupby(level=0).first() - link_widths_total = (h2_new + h2_retro) / linewidth_factor - link_widths_total = link_widths_total.reindex(n.links.index).fillna(0.) - link_widths_total[n.links.p_nom_opt < line_lower_threshold] = 0. - - retro = n.links.p_nom_opt.where(n.links.carrier=='H2 pipeline retrofitted', other=0.) - link_widths_retro = retro / linewidth_factor - link_widths_retro[n.links.p_nom_opt < line_lower_threshold] = 0. - - n.links.bus0 = n.links.bus0.str.replace(" H2", "") - n.links.bus1 = n.links.bus1.str.replace(" H2", "") - - fig, ax = plt.subplots( - figsize=(7, 6), - subplot_kw={"projection": ccrs.PlateCarree()} - ) - - n.plot( - bus_sizes=bus_sizes, - bus_colors=snakemake.config['plotting']['tech_colors'], - link_colors='#a2f0f2', - link_widths=link_widths_total, - branch_components=["Link"], - ax=ax, - **map_opts - ) - - n.plot( - geomap=False, - bus_sizes=0, - link_colors='#72d3d6', - link_widths=link_widths_retro, - branch_components=["Link"], - ax=ax, - **map_opts - ) - - handles = make_legend_circles_for( - [50000, 10000], - scale=bus_size_factor, - facecolor='grey' - ) - - labels = ["{} GW".format(s) for s in (50, 10)] - - l2 = ax.legend( - handles, labels, - loc="upper left", - bbox_to_anchor=(-0.03, 1.01), - labelspacing=1.0, - frameon=False, - title='Electrolyzer capacity', - handler_map=make_handler_map_to_scale_circles_as_in(ax) - ) - - ax.add_artist(l2) - - handles = [] - labels = [] - - for s in (50, 10): - handles.append(plt.Line2D([0], [0], color="grey", - linewidth=s * 1e3 / linewidth_factor)) - labels.append("{} GW".format(s)) - - l1_1 = ax.legend( - handles, labels, - loc="upper left", - bbox_to_anchor=(0.28, 1.01), - frameon=False, - labelspacing=0.8, - handletextpad=1.5, - title='H2 pipeline capacity' - ) - - ax.add_artist(l1_1) - - fig.savefig( - snakemake.output.map.replace("-costs-all","-h2_network"), - bbox_inches="tight" - ) - - -def plot_ch4_map(network): - - n = network.copy() - - if "gas pipeline" not in n.links.carrier.unique(): - return - - assign_location(n) - - bus_size_factor = 8e7 - linewidth_factor = 1e4 - # MW below which not drawn - line_lower_threshold = 500 - - # Drop non-electric buses so they don't clutter the plot - n.buses.drop(n.buses.index[n.buses.carrier != "AC"], inplace=True) - - fossil_gas_i = n.generators[n.generators.carrier=="gas"].index - fossil_gas = n.generators_t.p.loc[:,fossil_gas_i].mul(n.snapshot_weightings.generators, axis=0).sum().groupby(n.generators.loc[fossil_gas_i,"bus"]).sum() / bus_size_factor - fossil_gas.rename(index=lambda x: x.replace(" gas", ""), inplace=True) - fossil_gas = fossil_gas.reindex(n.buses.index).fillna(0) - # make a fake MultiIndex so that area is correct for legend - fossil_gas.index = pd.MultiIndex.from_product([fossil_gas.index, ["fossil gas"]]) - - methanation_i = n.links[n.links.carrier.isin(["helmeth", "Sabatier"])].index - methanation = abs(n.links_t.p1.loc[:,methanation_i].mul(n.snapshot_weightings.generators, axis=0)).sum().groupby(n.links.loc[methanation_i,"bus1"]).sum() / bus_size_factor - methanation = methanation.groupby(methanation.index).sum().rename(index=lambda x: x.replace(" gas", "")) - # make a fake MultiIndex so that area is correct for legend - methanation.index = pd.MultiIndex.from_product([methanation.index, ["methanation"]]) - - biogas_i = n.stores[n.stores.carrier=="biogas"].index - biogas = n.stores_t.p.loc[:,biogas_i].mul(n.snapshot_weightings.generators, axis=0).sum().groupby(n.stores.loc[biogas_i,"bus"]).sum() / bus_size_factor - biogas = biogas.groupby(biogas.index).sum().rename(index=lambda x: x.replace(" biogas", "")) - # make a fake MultiIndex so that area is correct for legend - biogas.index = pd.MultiIndex.from_product([biogas.index, ["biogas"]]) - - bus_sizes = pd.concat([fossil_gas, methanation, biogas]) - bus_sizes.sort_index(inplace=True) - - to_remove = n.links.index[~n.links.carrier.str.contains("gas pipeline")] - n.links.drop(to_remove, inplace=True) - - link_widths_rem = n.links.p_nom_opt / linewidth_factor - link_widths_rem[n.links.p_nom_opt < line_lower_threshold] = 0. - - link_widths_orig = n.links.p_nom / linewidth_factor - link_widths_orig[n.links.p_nom < line_lower_threshold] = 0. - - max_usage = n.links_t.p0.abs().max(axis=0) - link_widths_used = max_usage / linewidth_factor - link_widths_used[max_usage < line_lower_threshold] = 0. - - link_color_used = n.links.carrier.map({"gas pipeline": "#f08080", - "gas pipeline new": "#c46868"}) - - n.links.bus0 = n.links.bus0.str.replace(" gas", "") - n.links.bus1 = n.links.bus1.str.replace(" gas", "") - - tech_colors = snakemake.config['plotting']['tech_colors'] - - bus_colors = { - "fossil gas": tech_colors["fossil gas"], - "methanation": tech_colors["methanation"], - "biogas": "seagreen" - } - - fig, ax = plt.subplots(figsize=(7,6), subplot_kw={"projection": ccrs.PlateCarree()}) - - n.plot( - bus_sizes=bus_sizes, - bus_colors=bus_colors, - link_colors='lightgrey', - link_widths=link_widths_orig, - branch_components=["Link"], - ax=ax, - **map_opts - ) - - n.plot( - geomap=False, - ax=ax, - bus_sizes=0., - link_colors='#e8d1d1', - link_widths=link_widths_rem, - branch_components=["Link"], - **map_opts - ) - - n.plot( - geomap=False, - ax=ax, - bus_sizes=0., - link_colors=link_color_used, - link_widths=link_widths_used, - branch_components=["Link"], - **map_opts - ) - - handles = make_legend_circles_for( - [10e6, 100e6], - scale=bus_size_factor, - facecolor='grey' - ) - labels = ["{} TWh".format(s) for s in (10, 100)] - - l2 = ax.legend( - handles, labels, - loc="upper left", - bbox_to_anchor=(-0.03, 1.01), - labelspacing=1.0, - frameon=False, - title='gas generation', - handler_map=make_handler_map_to_scale_circles_as_in(ax) - ) - - ax.add_artist(l2) - - handles = [] - labels = [] - - for s in (50, 10): - handles.append(plt.Line2D([0], [0], color="grey", linewidth=s * 1e3 / linewidth_factor)) - labels.append("{} GW".format(s)) - - l1_1 = ax.legend( - handles, labels, - loc="upper left", - bbox_to_anchor=(0.28, 1.01), - frameon=False, - labelspacing=0.8, - handletextpad=1.5, - title='gas pipeline used capacity' - ) - - ax.add_artist(l1_1) - - fig.savefig( - snakemake.output.map.replace("-costs-all","-ch4_network"), - bbox_inches="tight" - ) - - -def plot_map_without(network): - - n = network.copy() - assign_location(n) - - # Drop non-electric buses so they don't clutter the plot - n.buses.drop(n.buses.index[n.buses.carrier != "AC"], inplace=True) - - fig, ax = plt.subplots( - figsize=(7, 6), - subplot_kw={"projection": ccrs.PlateCarree()} - ) - - # PDF has minimum width, so set these to zero - line_lower_threshold = 200. - line_upper_threshold = 1e4 - linewidth_factor = 2e3 - ac_color = "gray" - dc_color = "m" - - # hack because impossible to drop buses... - if "EU gas" in n.buses.index: - eu_location = snakemake.config["plotting"].get("eu_node_location", dict(x=-5.5, y=46)) - n.buses.loc["EU gas", "x"] = eu_location["x"] - n.buses.loc["EU gas", "y"] = eu_location["y"] - - to_drop = n.links.index[(n.links.carrier != "DC") & (n.links.carrier != "B2B")] - n.links.drop(to_drop, inplace=True) - - if snakemake.wildcards["lv"] == "1.0": - line_widths = n.lines.s_nom - link_widths = n.links.p_nom - else: - line_widths = n.lines.s_nom_min - link_widths = n.links.p_nom_min - - line_widths[line_widths < line_lower_threshold] = 0. - link_widths[link_widths < line_lower_threshold] = 0. - - line_widths[line_widths > line_upper_threshold] = line_upper_threshold - link_widths[link_widths > line_upper_threshold] = line_upper_threshold - - n.plot( - bus_colors="k", - line_colors=ac_color, - link_colors=dc_color, - line_widths=line_widths / linewidth_factor, - link_widths=link_widths / linewidth_factor, - ax=ax, **map_opts - ) - - handles = [] - labels = [] - - for s in (10, 5): - handles.append(plt.Line2D([0], [0], color=ac_color, - linewidth=s * 1e3 / linewidth_factor)) - labels.append("{} GW".format(s)) - l1_1 = ax.legend(handles, labels, - loc="upper left", bbox_to_anchor=(0.05, 1.01), - frameon=False, - labelspacing=0.8, handletextpad=1.5, - title='Today\'s transmission') - ax.add_artist(l1_1) - - fig.savefig( - snakemake.output.today, - transparent=True, - bbox_inches="tight" - ) - - -def plot_series(network, carrier="AC", name="test"): - - n = network.copy() - assign_location(n) - assign_carriers(n) - - buses = n.buses.index[n.buses.carrier.str.contains(carrier)] - - supply = pd.DataFrame(index=n.snapshots) - for c in n.iterate_components(n.branch_components): - n_port = 4 if c.name=='Link' else 2 - for i in range(n_port): - supply = pd.concat((supply, - (-1) * c.pnl["p" + str(i)].loc[:, - c.df.index[c.df["bus" + str(i)].isin(buses)]].groupby(c.df.carrier, - axis=1).sum()), - axis=1) - - for c in n.iterate_components(n.one_port_components): - comps = c.df.index[c.df.bus.isin(buses)] - supply = pd.concat((supply, ((c.pnl["p"].loc[:, comps]).multiply( - c.df.loc[comps, "sign"])).groupby(c.df.carrier, axis=1).sum()), axis=1) - - supply = supply.groupby(rename_techs_tyndp, axis=1).sum() - - both = supply.columns[(supply < 0.).any() & (supply > 0.).any()] - - positive_supply = supply[both] - negative_supply = supply[both] - - positive_supply[positive_supply < 0.] = 0. - negative_supply[negative_supply > 0.] = 0. - - supply[both] = positive_supply - - suffix = " charging" - - negative_supply.columns = negative_supply.columns + suffix - - supply = pd.concat((supply, negative_supply), axis=1) - - # 14-21.2 for flaute - # 19-26.1 for flaute - - start = "2013-02-19" - stop = "2013-02-26" - - threshold = 10e3 - - to_drop = supply.columns[(abs(supply) < threshold).all()] - - if len(to_drop) != 0: - print("dropping", to_drop) - supply.drop(columns=to_drop, inplace=True) - - supply.index.name = None - - supply = supply / 1e3 - - supply.rename(columns={"electricity": "electric demand", - "heat": "heat demand"}, - inplace=True) - supply.columns = supply.columns.str.replace("residential ", "") - supply.columns = supply.columns.str.replace("services ", "") - supply.columns = supply.columns.str.replace("urban decentral ", "decentral ") - - preferred_order = pd.Index(["electric demand", - "transmission lines", - "hydroelectricity", - "hydro reservoir", - "run of river", - "pumped hydro storage", - "CHP", - "onshore wind", - "offshore wind", - "solar PV", - "solar thermal", - "building retrofitting", - "ground heat pump", - "air heat pump", - "resistive heater", - "OCGT", - "gas boiler", - "gas", - "natural gas", - "methanation", - "hydrogen storage", - "battery storage", - "hot water storage"]) - - new_columns = (preferred_order.intersection(supply.columns) - .append(supply.columns.difference(preferred_order))) - - supply = supply.groupby(supply.columns, axis=1).sum() - fig, ax = plt.subplots() - fig.set_size_inches((8, 5)) - - (supply.loc[start:stop, new_columns] - .plot(ax=ax, kind="area", stacked=True, linewidth=0., - color=[snakemake.config['plotting']['tech_colors'][i.replace(suffix, "")] - for i in new_columns])) - - handles, labels = ax.get_legend_handles_labels() - - handles.reverse() - labels.reverse() - - new_handles = [] - new_labels = [] - - for i, item in enumerate(labels): - if "charging" not in item: - new_handles.append(handles[i]) - new_labels.append(labels[i]) - - ax.legend(new_handles, new_labels, ncol=3, loc="upper left", frameon=False) - ax.set_xlim([start, stop]) - ax.set_ylim([-1300, 1900]) - ax.grid(True) - ax.set_ylabel("Power [GW]") - fig.tight_layout() - - fig.savefig("{}{}/maps/series-{}-{}-{}-{}-{}.pdf".format( - snakemake.config['results_dir'], snakemake.config['run'], - snakemake.wildcards["lv"], - carrier, start, stop, name), - transparent=True) - - -if __name__ == "__main__": - if 'snakemake' not in globals(): - from helper import mock_snakemake - snakemake = mock_snakemake( - 'plot_network', - simpl='', - clusters="45", - lv=1.0, - opts='', - sector_opts='168H-T-H-B-I-A-solar+p3-dist1', - planning_horizons="2050", - ) - - overrides = override_component_attrs(snakemake.input.overrides) - n = pypsa.Network(snakemake.input.network, override_component_attrs=overrides) - - map_opts = snakemake.config['plotting']['map'] - - plot_map(n, - components=["generators", "links", "stores", "storage_units"], - bus_size_factor=1.5e10, - transmission=False - ) - - plot_h2_map(n) - plot_ch4_map(n) - plot_map_without(n) - - #plot_series(n, carrier="AC", name=suffix) - #plot_series(n, carrier="heat", name=suffix) From adc0dd1e0c67c84a1a99145a51914e0c3a21fed0 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 11 Jul 2022 15:46:21 +0200 Subject: [PATCH 016/234] fix unit conversion for thermal energy storage (closes #247) --- scripts/prepare_sector_network.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index bcf9cf6b..49eef826 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1481,9 +1481,6 @@ def add_heat(n, costs): "for 'decentral' and 'central' separately.") tes_time_constant_days = options["tes_tau"] if name_type == "decentral" else 180. - # conversion from EUR/m^3 to EUR/MWh for 40 K diff and 1.17 kWh/m^3/K - capital_cost = costs.at[name_type + ' water tank storage', 'fixed'] / 0.00117 / 40 - n.madd("Store", nodes[name] + f" {name} water tanks", bus=nodes[name] + f" {name} water tanks", @@ -1491,7 +1488,7 @@ def add_heat(n, costs): e_nom_extendable=True, carrier=name + " water tanks", standing_loss=1 - np.exp(- 1 / 24 / tes_time_constant_days), - capital_cost=capital_cost, + capital_cost=costs.at[name_type + ' water tank storage', 'fixed'], lifetime=costs.at[name_type + ' water tank storage', 'lifetime'] ) From ec1aa1b76bc9b822d7d2b88cbcbf7da980b5424d Mon Sep 17 00:00:00 2001 From: Tom Brown Date: Mon, 18 Jul 2022 20:40:21 +0200 Subject: [PATCH 017/234] README, doc: add reference to hydrogen network paper as showcase of model --- README.md | 18 ++++++++++-------- doc/index.rst | 21 ++++++++++++--------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 5af6f3e4..b1f62d77 100644 --- a/README.md +++ b/README.md @@ -20,14 +20,16 @@ all greenhouse gas emitters except waste management and land use. **WARNING**: PyPSA-Eur-Sec is under active development and has several [limitations](https://pypsa-eur-sec.readthedocs.io/en/latest/limitations.html) which you should understand before using the model. The github repository -[issues](https://github.com/PyPSA/pypsa-eur-sec/issues) collects known -topics we are working on (please feel free to help or make suggestions). There is neither a full -documentation nor a paper yet, but we hope to have a preprint out by mid-2022. -You can find out more about the model capabilities in [a recent -presentation at EMP-E](https://nworbmot.org/energy/brown-empe.pdf) or the -following [paper in Joule with a description of the industry -sector](https://arxiv.org/abs/2109.09563). We cannot support this model if you -choose to use it. +[issues](https://github.com/PyPSA/pypsa-eur-sec/issues) collect known +topics we are working on (please feel free to help or make suggestions). +The [documentation](https://pypsa-eur-sec.readthedocs.io/) remains somewhat +patchy. +You can find showcases of the model's capabilities in the preprint +[Benefits of a Hydrogen Network in Europe](https://arxiv.org/abs/2207.05816), +a [paper in Joule with a description of the industry +sector](https://arxiv.org/abs/2109.09563), or in [a 2021 +presentation at EMP-E](https://nworbmot.org/energy/brown-empe.pdf). +We cannot support this model if you choose to use it. Please see the [documentation](https://pypsa-eur-sec.readthedocs.io/) for installation instructions and other useful information about the snakemake workflow. diff --git a/doc/index.rst b/doc/index.rst index ad43c66b..fb5f6895 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -33,17 +33,20 @@ waste management, agriculture, forestry and land use. **WARNING**: PyPSA-Eur-Sec is under active development and has several `limitations `_ which you should understand before using the model. The github repository -`issues `_ collects known -topics we are working on (please feel free to help or make suggestions). There is neither a full -documentation nor a paper yet, but we hope to have a preprint out by mid-2022. -We cannot support this model if you -choose to use it. - +`issues `_ collect known +topics we are working on (please feel free to help or make suggestions). +The `documentation `_ remains somewhat +patchy. +We cannot support this model if you choose to use it. .. note:: - More about the current model capabilities and preliminary results - can be found in `a recent presentation at EMP-E `_ - and the following `paper in Joule with a description of the industry sector `_. + You can find showcases of the model's capabilities in the + preprint `Benefits of a Hydrogen Network in Europe + `_, a `paper in Joule with a + description of the industry sector + `_, or in `a 2021 presentation + at EMP-E `_. + This diagram gives an overview of the sectors and the links between them: From cb00ba58ceb2241ff6a12ea7d623d6fb781c9c57 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 20 Jul 2022 11:35:12 +0200 Subject: [PATCH 018/234] add option to specify any config through sector opts with CF: