From b29e1c196eb71896559f7235bdaa6864ff616cb7 Mon Sep 17 00:00:00 2001 From: millingermarkus Date: Thu, 30 May 2024 14:28:34 +0200 Subject: [PATCH 001/344] Added msw incineration --- scripts/prepare_sector_network.py | 97 +++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 9f53e3170..a0b36286e 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -61,13 +61,18 @@ def define_spatial(nodes, options): spatial.biomass.locations = nodes spatial.biomass.industry = nodes + " solid biomass for industry" spatial.biomass.industry_cc = nodes + " solid biomass for industry CC" + spatial.msw.nodes = nodes + " municipal solid waste" + spatial.msw.locations = nodes else: spatial.biomass.nodes = ["EU solid biomass"] spatial.biomass.locations = ["EU"] spatial.biomass.industry = ["solid biomass for industry"] spatial.biomass.industry_cc = ["solid biomass for industry CC"] + spatial.msw.nodes = ["EU municipal solid waste"] + spatial.msw.locations = ["EU"] spatial.biomass.df = pd.DataFrame(vars(spatial.biomass), index=nodes) + spatial.msw.df = pd.DataFrame(vars(spatial.msw), index=nodes) # co2 @@ -2161,11 +2166,16 @@ def add_biomass(n, costs): solid_biomass_potentials_spatial = biomass_potentials["solid biomass"].rename( index=lambda x: x + " solid biomass" ) + msw_biomass_potentials_spatial = biomass_potentials["municipal solid waste"].rename( + index=lambda x: x + " municipal solid waste" + ) else: solid_biomass_potentials_spatial = biomass_potentials["solid biomass"].sum() + msw_biomass_potentials_spatial = biomass_potentials["municipal solid waste"].sum() n.add("Carrier", "biogas") n.add("Carrier", "solid biomass") + n.add("Carrier", "municipal solid waste") n.madd( "Bus", @@ -2183,6 +2193,11 @@ def add_biomass(n, costs): unit="MWh_LHV", ) + n.madd("Bus", + spatial.msw.nodes, + location=spatial.msw.locations, + carrier="municipal solid waste") + n.madd( "Store", spatial.gas.biogas, @@ -2203,6 +2218,15 @@ def add_biomass(n, costs): e_initial=solid_biomass_potentials_spatial, ) + n.madd("Store", + spatial.msw.nodes, + bus=spatial.msw.nodes, + carrier="municipal solid waste", + e_nom=msw_biomass_potentials_spatial, + marginal_cost=0,#costs.at["municipal solid waste", "fuel"], + e_initial=msw_biomass_potentials_spatial, + ) + n.madd( "Link", spatial.gas.biogas_to_gas, @@ -2274,6 +2298,18 @@ def add_biomass(n, costs): carrier="solid biomass transport", ) + n.madd( + "Link", + biomass_transport.index, + bus0=biomass_transport.bus0 + " municipal solid waste", + bus1=biomass_transport.bus1 + " municipal solid waste", + p_nom_extendable=False, + p_nom=5e4, + length=biomass_transport.length.values, + marginal_cost=biomass_transport.costs * biomass_transport.length.values, + carrier="municipal solid waste transport", + ) + elif options["biomass_spatial"]: # add artificial biomass generators at nodes which include transport costs transport_costs = pd.read_csv( @@ -2303,6 +2339,25 @@ def add_biomass(n, costs): type="operational_limit", ) + #Add municipal solid waste + n.madd( + "Generator", + spatial.msw.nodes, + bus=spatial.msw.nodes, + carrier="municipal solid waste", + p_nom=10000, + marginal_cost=0#costs.at["municipal solid waste", "fuel"] + + bus_transport_costs * average_distance, + ) + n.add( + "GlobalConstraint", + "msw limit", + carrier_attribute="municipal solid waste", + sense="<=", + constant=biomass_potentials["municipal solid waste"].sum(), + type="operational_limit", + ) + # AC buses with district heating urban_central = n.buses.index[n.buses.carrier == "urban central heat"] if not urban_central.empty and options["chp"]: @@ -2359,6 +2414,48 @@ def add_biomass(n, costs): lifetime=costs.at[key, "lifetime"], ) + if options['waste_chp']: + print('Adding waste CHPs') + n.madd("Link", + urban_central + " waste CHP", + bus0=urban_central + " municipal solid waste", + bus1=urban_central, + bus4=urban_central + " urban central heat", + bus3="co2 atmosphere", + carrier="urban central waste incineration", + p_nom_extendable=True, + # p_nom=biomass_potential['municipal solid waste'] / 8760, + capital_cost=costs.at['waste CHP', 'fixed'] * costs.at['waste CHP', 'efficiency'], + marginal_cost=costs.at['waste CHP', 'VOM'], + efficiency=costs.at['waste CHP', 'efficiency'], + efficiency4=costs.at['waste CHP', 'efficiency-heat'], + efficiency3=costs.at['solid biomass', 'CO2 intensity']-costs.at['solid biomass', 'CO2 intensity'], + lifetime=costs.at['waste CHP', 'lifetime']) + + if beccs: + n.madd("Link", + urban_central + " waste CHP CC", + bus0=urban_central + " municipal solid waste", + bus1=urban_central, + bus4=urban_central + " urban central heat", + bus3="co2 atmosphere", + bus2="co2 stored", + carrier="urban central waste incineration CC", + p_nom_extendable=True, + # p_nom=costs.at['waste CHP CC', 'efficiency'] * biomass_potential['municipal solid waste'] / 8760, + capital_cost=costs.at['waste CHP CC', 'fixed'] * costs.at['waste CHP CC', 'efficiency'] + + costs.at['biomass CHP capture', 'fixed'] * costs.at['solid biomass', 'CO2 intensity'], + marginal_cost=costs.at['waste CHP CC', 'VOM'], + efficiency=costs.at['waste CHP CC', 'efficiency'], + efficiency4=costs.at['waste CHP CC', 'efficiency-heat'], + #Assuming same CO2 intensity as solid biomass + efficiency3=costs.at['solid biomass', 'CO2 intensity'] * (1 - options["cc_fraction"])-costs.at['solid biomass', 'CO2 intensity'], + efficiency2=costs.at['solid biomass', 'CO2 intensity'] * options["cc_fraction"], + c_b=costs.at['waste CHP CC', 'c_b'], + c_v=costs.at['waste CHP CC', 'c_v'], + lifetime=costs.at['waste CHP CC', 'lifetime']) + + if options["biomass_boiler"]: # TODO: Add surcharge for pellets nodes = pop_layout.index From c7ce47dffdfb0a31625d602e7ab1c2517e4cd6d3 Mon Sep 17 00:00:00 2001 From: millingermarkus Date: Wed, 3 Jul 2024 09:22:09 +0200 Subject: [PATCH 002/344] Energy penalty for solid biomass CHP --- scripts/prepare_sector_network.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index dfa06cac5..6735fd7ed 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2417,28 +2417,22 @@ def add_biomass(n, costs): bus4=spatial.co2.df.loc[urban_central, "nodes"].values, carrier="urban central solid biomass CHP CC", p_nom_extendable=True, - capital_cost=costs.at[key, "fixed"] * costs.at[key, "efficiency"] + capital_cost=costs.at[key + " CC", "fixed"] * costs.at[key + " CC", "efficiency"] + costs.at["biomass CHP capture", "fixed"] * costs.at["solid biomass", "CO2 intensity"], - marginal_cost=costs.at[key, "VOM"], - efficiency=costs.at[key, "efficiency"] + marginal_cost=costs.at[key + " CC", "VOM"], + efficiency=costs.at[key + " CC", "efficiency"] - costs.at["solid biomass", "CO2 intensity"] * ( costs.at["biomass CHP capture", "electricity-input"] + costs.at["biomass CHP capture", "compression-electricity-input"] ), - efficiency2=costs.at[key, "efficiency-heat"] - + costs.at["solid biomass", "CO2 intensity"] - * ( - costs.at["biomass CHP capture", "heat-output"] - + costs.at["biomass CHP capture", "compression-heat-output"] - - costs.at["biomass CHP capture", "heat-input"] - ), + efficiency2=costs.at[key + " CC", "efficiency-heat"], efficiency3=-costs.at["solid biomass", "CO2 intensity"] * costs.at["biomass CHP capture", "capture_rate"], efficiency4=costs.at["solid biomass", "CO2 intensity"] * costs.at["biomass CHP capture", "capture_rate"], - lifetime=costs.at[key, "lifetime"], + lifetime=costs.at[key + " CC", "lifetime"], ) if options["biomass_boiler"]: From b5ff1e8f9c935d89cebd87da9f2692f6bb3d3e2b Mon Sep 17 00:00:00 2001 From: millingermarkus Date: Wed, 3 Jul 2024 09:38:08 +0200 Subject: [PATCH 003/344] Added note on carbon capture for BioSNG and BtL --- scripts/prepare_sector_network.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 6735fd7ed..18ed90c4f 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2478,7 +2478,8 @@ def add_biomass(n, costs): marginal_cost=costs.at["BtL", "efficiency"] * costs.at["BtL", "VOM"], ) - # TODO: Update with energy penalty + #Assuming that acid gas removal (incl. CO2) from syngas i performed with Rectisol + #process (Methanol) and that electricity demand for this is included in the base process n.madd( "Link", spatial.biomass.nodes, @@ -2518,7 +2519,8 @@ def add_biomass(n, costs): marginal_cost=costs.at["BioSNG", "efficiency"] * costs.at["BioSNG", "VOM"], ) - # TODO: Update with energy penalty for CC + # Assuming that acid gas removal (incl. CO2) from syngas i performed with Rectisol + # process (Methanol) and that electricity demand for this is included in the base process n.madd( "Link", spatial.biomass.nodes, From 76e15d6862636d7c134f28e7ca28d0dab4e9c678 Mon Sep 17 00:00:00 2001 From: millingermarkus Date: Wed, 3 Jul 2024 09:42:15 +0200 Subject: [PATCH 004/344] Carbon capture capital cost for waste CHP --- scripts/prepare_sector_network.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 18ed90c4f..461a6db6c 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3100,7 +3100,9 @@ def add_industry(n, costs): carrier="waste CHP CC", p_nom_extendable=True, capital_cost=costs.at["waste CHP CC", "fixed"] - * costs.at["waste CHP CC", "efficiency"], + * costs.at["waste CHP CC", "efficiency"] + + costs.at['biomass CHP capture', 'fixed'] + * costs.at['oil', 'CO2 intensity'], marginal_cost=costs.at["waste CHP CC", "VOM"], efficiency=costs.at["waste CHP CC", "efficiency"], efficiency2=costs.at["waste CHP CC", "efficiency-heat"], From 53328e89f2a6df6d2ba269141645488fe069aaad Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 3 Jul 2024 08:07:29 +0000 Subject: [PATCH 005/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/prepare_sector_network.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 461a6db6c..18d0d1a38 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2417,7 +2417,8 @@ def add_biomass(n, costs): bus4=spatial.co2.df.loc[urban_central, "nodes"].values, carrier="urban central solid biomass CHP CC", p_nom_extendable=True, - capital_cost=costs.at[key + " CC", "fixed"] * costs.at[key + " CC", "efficiency"] + capital_cost=costs.at[key + " CC", "fixed"] + * costs.at[key + " CC", "efficiency"] + costs.at["biomass CHP capture", "fixed"] * costs.at["solid biomass", "CO2 intensity"], marginal_cost=costs.at[key + " CC", "VOM"], @@ -2478,8 +2479,8 @@ def add_biomass(n, costs): marginal_cost=costs.at["BtL", "efficiency"] * costs.at["BtL", "VOM"], ) - #Assuming that acid gas removal (incl. CO2) from syngas i performed with Rectisol - #process (Methanol) and that electricity demand for this is included in the base process + # Assuming that acid gas removal (incl. CO2) from syngas i performed with Rectisol + # process (Methanol) and that electricity demand for this is included in the base process n.madd( "Link", spatial.biomass.nodes, @@ -3101,8 +3102,8 @@ def add_industry(n, costs): p_nom_extendable=True, capital_cost=costs.at["waste CHP CC", "fixed"] * costs.at["waste CHP CC", "efficiency"] - + costs.at['biomass CHP capture', 'fixed'] - * costs.at['oil', 'CO2 intensity'], + + costs.at["biomass CHP capture", "fixed"] + * costs.at["oil", "CO2 intensity"], marginal_cost=costs.at["waste CHP CC", "VOM"], efficiency=costs.at["waste CHP CC", "efficiency"], efficiency2=costs.at["waste CHP CC", "efficiency-heat"], From 84ddfea91ee97064df58eaa51b56bf95337e8b80 Mon Sep 17 00:00:00 2001 From: millingermarkus Date: Wed, 3 Jul 2024 12:38:16 +0200 Subject: [PATCH 006/344] Added electrobiofuels --- config/config.default.yaml | 2 ++ scripts/prepare_sector_network.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/config/config.default.yaml b/config/config.default.yaml index ee61d366a..60eff164b 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -606,6 +606,7 @@ sector: conventional_generation: OCGT: gas biomass_to_liquid: false + electrobiofuels: false biosng: false limit_max_growth: enable: false @@ -1030,6 +1031,7 @@ plotting: services rural biomass boiler: '#c6cf98' services urban decentral biomass boiler: '#dde5b5' biomass to liquid: '#32CD32' + electrobiofuels: 'red' BioSNG: '#123456' # power transmission lines: '#6c9459' diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index dfa06cac5..1a1f97c01 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2505,6 +2505,36 @@ def add_biomass(n, costs): marginal_cost=costs.at["BtL", "efficiency"] * costs.at["BtL", "VOM"], ) + #Electrobiofuels (BtL with hydrogen addition to make more use of biogenic carbon). + #Combination of efuels and biomass to liquid, both based on Fischer-Tropsch. + #Experimental version - use with caution + if options['electrobiofuels'] and options.get("biomass_spatial", options["biomass_transport"]): + efuel_scale_factor = costs.at['BtL', 'C stored'] + n.madd( + "Link", + spatial.biomass.nodes, + suffix=" electrobiofuels", + bus0=spatial.biomass.nodes, + bus1=spatial.oil.nodes, + bus2=spatial.h2.nodes, + bus3="co2 atmosphere", + carrier="electrobiofuels", + lifetime=costs.at['electrobiofuels', 'lifetime'], + efficiency=costs.at['electrobiofuels', 'efficiency-biomass'], + efficiency2=-costs.at['electrobiofuels', 'efficiency-hydrogen'], + efficiency3=-costs.at['solid biomass', 'CO2 intensity'] + costs.at['BtL', 'CO2 stored'] + * (1 - costs.at['Fischer-Tropsch', 'capture rate']), + p_nom_extendable=True, + capital_cost=costs.at['BtL', 'fixed'] + * costs.at['BtL', 'efficiency'] / costs.at['electrobiofuels', 'efficiency-biomass'] + + efuel_scale_factor * costs.at['Fischer-Tropsch', 'fixed'] + * costs.at['Fischer-Tropsch', 'efficiency'] / costs.at['electrobiofuels', 'efficiency-hydrogen'], + marginal_cost=costs.at['BtL', 'VOM'] + * costs.at['BtL', 'efficiency'] / costs.at['electrobiofuels', 'efficiency-biomass'] + + efuel_scale_factor * costs.at['Fischer-Tropsch', 'VOM'] + * costs.at['Fischer-Tropsch', 'efficiency'] / costs.at['electrobiofuels', 'efficiency-hydrogen'] + ) + # BioSNG from solid biomass if options["biosng"]: n.madd( From b934653b65581b17ae462487f5cdcbed08c97b1d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 3 Jul 2024 10:42:04 +0000 Subject: [PATCH 007/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/prepare_sector_network.py | 45 ++++++++++++++++++------------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 1a1f97c01..50d8e134a 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2505,11 +2505,13 @@ def add_biomass(n, costs): marginal_cost=costs.at["BtL", "efficiency"] * costs.at["BtL", "VOM"], ) - #Electrobiofuels (BtL with hydrogen addition to make more use of biogenic carbon). - #Combination of efuels and biomass to liquid, both based on Fischer-Tropsch. - #Experimental version - use with caution - if options['electrobiofuels'] and options.get("biomass_spatial", options["biomass_transport"]): - efuel_scale_factor = costs.at['BtL', 'C stored'] + # Electrobiofuels (BtL with hydrogen addition to make more use of biogenic carbon). + # Combination of efuels and biomass to liquid, both based on Fischer-Tropsch. + # Experimental version - use with caution + if options["electrobiofuels"] and options.get( + "biomass_spatial", options["biomass_transport"] + ): + efuel_scale_factor = costs.at["BtL", "C stored"] n.madd( "Link", spatial.biomass.nodes, @@ -2519,20 +2521,27 @@ def add_biomass(n, costs): bus2=spatial.h2.nodes, bus3="co2 atmosphere", carrier="electrobiofuels", - lifetime=costs.at['electrobiofuels', 'lifetime'], - efficiency=costs.at['electrobiofuels', 'efficiency-biomass'], - efficiency2=-costs.at['electrobiofuels', 'efficiency-hydrogen'], - efficiency3=-costs.at['solid biomass', 'CO2 intensity'] + costs.at['BtL', 'CO2 stored'] - * (1 - costs.at['Fischer-Tropsch', 'capture rate']), + lifetime=costs.at["electrobiofuels", "lifetime"], + efficiency=costs.at["electrobiofuels", "efficiency-biomass"], + efficiency2=-costs.at["electrobiofuels", "efficiency-hydrogen"], + efficiency3=-costs.at["solid biomass", "CO2 intensity"] + + costs.at["BtL", "CO2 stored"] + * (1 - costs.at["Fischer-Tropsch", "capture rate"]), p_nom_extendable=True, - capital_cost=costs.at['BtL', 'fixed'] - * costs.at['BtL', 'efficiency'] / costs.at['electrobiofuels', 'efficiency-biomass'] - + efuel_scale_factor * costs.at['Fischer-Tropsch', 'fixed'] - * costs.at['Fischer-Tropsch', 'efficiency'] / costs.at['electrobiofuels', 'efficiency-hydrogen'], - marginal_cost=costs.at['BtL', 'VOM'] - * costs.at['BtL', 'efficiency'] / costs.at['electrobiofuels', 'efficiency-biomass'] - + efuel_scale_factor * costs.at['Fischer-Tropsch', 'VOM'] - * costs.at['Fischer-Tropsch', 'efficiency'] / costs.at['electrobiofuels', 'efficiency-hydrogen'] + capital_cost=costs.at["BtL", "fixed"] + * costs.at["BtL", "efficiency"] + / costs.at["electrobiofuels", "efficiency-biomass"] + + efuel_scale_factor + * costs.at["Fischer-Tropsch", "fixed"] + * costs.at["Fischer-Tropsch", "efficiency"] + / costs.at["electrobiofuels", "efficiency-hydrogen"], + marginal_cost=costs.at["BtL", "VOM"] + * costs.at["BtL", "efficiency"] + / costs.at["electrobiofuels", "efficiency-biomass"] + + efuel_scale_factor + * costs.at["Fischer-Tropsch", "VOM"] + * costs.at["Fischer-Tropsch", "efficiency"] + / costs.at["electrobiofuels", "efficiency-hydrogen"], ) # BioSNG from solid biomass From 270ce82b9f22dce831dfd87c03e5068eb07e0e07 Mon Sep 17 00:00:00 2001 From: Markus Millinger <50738187+millingermarkus@users.noreply.github.com> Date: Thu, 4 Jul 2024 09:30:22 +0200 Subject: [PATCH 008/344] Cost correction --- scripts/prepare_sector_network.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 50d8e134a..b0155a99a 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2530,18 +2530,14 @@ def add_biomass(n, costs): p_nom_extendable=True, capital_cost=costs.at["BtL", "fixed"] * costs.at["BtL", "efficiency"] - / costs.at["electrobiofuels", "efficiency-biomass"] + efuel_scale_factor * costs.at["Fischer-Tropsch", "fixed"] - * costs.at["Fischer-Tropsch", "efficiency"] - / costs.at["electrobiofuels", "efficiency-hydrogen"], + * costs.at["Fischer-Tropsch", "efficiency"], marginal_cost=costs.at["BtL", "VOM"] * costs.at["BtL", "efficiency"] - / costs.at["electrobiofuels", "efficiency-biomass"] + efuel_scale_factor * costs.at["Fischer-Tropsch", "VOM"] - * costs.at["Fischer-Tropsch", "efficiency"] - / costs.at["electrobiofuels", "efficiency-hydrogen"], + * costs.at["Fischer-Tropsch", "efficiency"], ) # BioSNG from solid biomass From 192a8eb14bb2fb03d81d04e4a10e7695fac03615 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 4 Jul 2024 07:30:50 +0000 Subject: [PATCH 009/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/prepare_sector_network.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index b0155a99a..8132f343a 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2528,13 +2528,11 @@ def add_biomass(n, costs): + costs.at["BtL", "CO2 stored"] * (1 - costs.at["Fischer-Tropsch", "capture rate"]), p_nom_extendable=True, - capital_cost=costs.at["BtL", "fixed"] - * costs.at["BtL", "efficiency"] + capital_cost=costs.at["BtL", "fixed"] * costs.at["BtL", "efficiency"] + efuel_scale_factor * costs.at["Fischer-Tropsch", "fixed"] * costs.at["Fischer-Tropsch", "efficiency"], - marginal_cost=costs.at["BtL", "VOM"] - * costs.at["BtL", "efficiency"] + marginal_cost=costs.at["BtL", "VOM"] * costs.at["BtL", "efficiency"] + efuel_scale_factor * costs.at["Fischer-Tropsch", "VOM"] * costs.at["Fischer-Tropsch", "efficiency"], From 821eb07e986696c4b9b4bd1071e3804fa948c143 Mon Sep 17 00:00:00 2001 From: Markus Millinger <50738187+millingermarkus@users.noreply.github.com> Date: Thu, 4 Jul 2024 09:59:45 +0200 Subject: [PATCH 010/344] Correct costs of BtL and BioSNG --- scripts/prepare_sector_network.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 18d0d1a38..6862df97d 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2475,8 +2475,8 @@ def add_biomass(n, costs): efficiency2=-costs.at["solid biomass", "CO2 intensity"] + costs.at["BtL", "CO2 stored"], p_nom_extendable=True, - capital_cost=costs.at["BtL", "fixed"], - marginal_cost=costs.at["BtL", "efficiency"] * costs.at["BtL", "VOM"], + capital_cost=costs.at["BtL", "fixed"] * costs.at["BtL", "efficiency"], + marginal_cost=costs.at["BtL", "VOM"] * costs.at["BtL", "efficiency"], ) # Assuming that acid gas removal (incl. CO2) from syngas i performed with Rectisol @@ -2496,9 +2496,9 @@ def add_biomass(n, costs): + costs.at["BtL", "CO2 stored"] * (1 - costs.at["BtL", "capture rate"]), efficiency3=costs.at["BtL", "CO2 stored"] * costs.at["BtL", "capture rate"], p_nom_extendable=True, - capital_cost=costs.at["BtL", "fixed"] + capital_cost=costs.at["BtL", "fixed"] * costs.at["BtL", "efficiency"] + costs.at["biomass CHP capture", "fixed"] * costs.at["BtL", "CO2 stored"], - marginal_cost=costs.at["BtL", "efficiency"] * costs.at["BtL", "VOM"], + marginal_cost=costs.at["BtL", "VOM"] * costs.at["BtL", "efficiency"], ) # BioSNG from solid biomass @@ -2516,8 +2516,8 @@ def add_biomass(n, costs): efficiency3=-costs.at["solid biomass", "CO2 intensity"] + costs.at["BioSNG", "CO2 stored"], p_nom_extendable=True, - capital_cost=costs.at["BioSNG", "fixed"], - marginal_cost=costs.at["BioSNG", "efficiency"] * costs.at["BioSNG", "VOM"], + capital_cost=costs.at["BioSNG", "fixed"] * costs.at["BioSNG", "efficiency"], + marginal_cost=costs.at["BioSNG", "VOM"] * costs.at["BioSNG", "efficiency"], ) # Assuming that acid gas removal (incl. CO2) from syngas i performed with Rectisol @@ -2539,10 +2539,10 @@ def add_biomass(n, costs): + costs.at["BioSNG", "CO2 stored"] * (1 - costs.at["BioSNG", "capture rate"]), p_nom_extendable=True, - capital_cost=costs.at["BioSNG", "fixed"] + capital_cost=costs.at["BioSNG", "fixed"] * costs.at["BioSNG", "efficiency"] + costs.at["biomass CHP capture", "fixed"] * costs.at["BioSNG", "CO2 stored"], - marginal_cost=costs.at["BioSNG", "efficiency"] * costs.at["BioSNG", "VOM"], + marginal_cost=costs.at["BioSNG", "VOM"] * costs.at["BioSNG", "efficiency"], ) From c71fa78bfa4fe7471817416387837cf66623209c Mon Sep 17 00:00:00 2001 From: millingermarkus Date: Thu, 4 Jul 2024 10:18:19 +0200 Subject: [PATCH 011/344] Added biomass imports to prepare sector network --- config/config.default.yaml | 6 ++++ scripts/prepare_sector_network.py | 46 +++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/config/config.default.yaml b/config/config.default.yaml index ee61d366a..a618ed95b 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -628,6 +628,11 @@ sector: max_boost: 0.25 var_cf: true sustainability_factor: 0.0025 + solid_biomass_import: + enable: false + price: 54 #EUR/MWh + max_amount: 5 #EJ + upstream_emissions_factor: .1 #share of solid biomass CO2 emissions at full combustion # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#industry industry: @@ -1017,6 +1022,7 @@ plotting: biogas: '#e3d37d' biomass: '#baa741' solid biomass: '#baa741' + solid biomass import: '#d5ca8d' solid biomass transport: '#baa741' solid biomass for industry: '#7a6d26' solid biomass for industry CC: '#47411c' diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index dfa06cac5..8332858a7 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2285,6 +2285,52 @@ def add_biomass(n, costs): e_initial=solid_biomass_potentials_spatial, ) + if options["solid_biomass_import"].get("enable", False): + biomass_import_price = options["solid_biomass_import"]["price"] + biomass_import_max_amount = round(options["solid_biomass_import"]["max_amount"] + * 1e9 / 3.6,0) #EJ --> MWh + biomass_import_upstream_emissions = round(options["solid_biomass_import"]["upstream_emissions_factor"] + * costs.at['solid biomass', 'CO2 intensity'],4) + + print("Adding biomass import with cost", biomass_import_price, + "EUR/MWh, a limit of", options["solid_biomass_import"]["max_amount"], + "EJ, and embedded emissions of", + options["solid_biomass_import"]["upstream_emissions_factor"] * 100, '%') + + n.add("Carrier", "solid biomass import") + + n.madd( + "Bus", + ["EU solid biomass import"], + location="EU", + carrier="solid biomass import" + ) + + n.madd( + "Store", + ["solid biomass import"], + bus=["EU solid biomass import"], + carrier="solid biomass import", + e_nom=biomass_import_max_amount, + marginal_cost=biomass_import_price, + e_initial=biomass_import_max_amount + ) + + n.madd( + "Link", + spatial.biomass.nodes, + suffix=" solid biomass import", + bus0=["EU solid biomass import"], + bus1=spatial.biomass.nodes, + bus2="co2 atmosphere", + carrier="solid biomass import", + efficiency=1., + efficiency2=biomass_import_upstream_emissions, + p_nom_extendable=True + ) + + + n.madd( "Link", spatial.gas.biogas_to_gas, From ee6d8b1e42788ad9bad8ef8f89c1303e9ceaf851 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 4 Jul 2024 08:22:21 +0000 Subject: [PATCH 012/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/prepare_sector_network.py | 35 ++++++++++++++++++------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 8332858a7..d7ca791c8 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2287,15 +2287,24 @@ def add_biomass(n, costs): if options["solid_biomass_import"].get("enable", False): biomass_import_price = options["solid_biomass_import"]["price"] - biomass_import_max_amount = round(options["solid_biomass_import"]["max_amount"] - * 1e9 / 3.6,0) #EJ --> MWh - biomass_import_upstream_emissions = round(options["solid_biomass_import"]["upstream_emissions_factor"] - * costs.at['solid biomass', 'CO2 intensity'],4) + biomass_import_max_amount = round( + options["solid_biomass_import"]["max_amount"] * 1e9 / 3.6, 0 + ) # EJ --> MWh + biomass_import_upstream_emissions = round( + options["solid_biomass_import"]["upstream_emissions_factor"] + * costs.at["solid biomass", "CO2 intensity"], + 4, + ) - print("Adding biomass import with cost", biomass_import_price, - "EUR/MWh, a limit of", options["solid_biomass_import"]["max_amount"], - "EJ, and embedded emissions of", - options["solid_biomass_import"]["upstream_emissions_factor"] * 100, '%') + print( + "Adding biomass import with cost", + biomass_import_price, + "EUR/MWh, a limit of", + options["solid_biomass_import"]["max_amount"], + "EJ, and embedded emissions of", + options["solid_biomass_import"]["upstream_emissions_factor"] * 100, + "%", + ) n.add("Carrier", "solid biomass import") @@ -2303,7 +2312,7 @@ def add_biomass(n, costs): "Bus", ["EU solid biomass import"], location="EU", - carrier="solid biomass import" + carrier="solid biomass import", ) n.madd( @@ -2313,7 +2322,7 @@ def add_biomass(n, costs): carrier="solid biomass import", e_nom=biomass_import_max_amount, marginal_cost=biomass_import_price, - e_initial=biomass_import_max_amount + e_initial=biomass_import_max_amount, ) n.madd( @@ -2324,13 +2333,11 @@ def add_biomass(n, costs): bus1=spatial.biomass.nodes, bus2="co2 atmosphere", carrier="solid biomass import", - efficiency=1., + efficiency=1.0, efficiency2=biomass_import_upstream_emissions, - p_nom_extendable=True + p_nom_extendable=True, ) - - n.madd( "Link", spatial.gas.biogas_to_gas, From 5c7bb2bf51304855d932da91b00c0a3ef7dbbfcf Mon Sep 17 00:00:00 2001 From: millingermarkus Date: Thu, 4 Jul 2024 11:16:00 +0200 Subject: [PATCH 013/344] Add option to exclude fossil fuels --- config/config.default.yaml | 1 + scripts/prepare_sector_network.py | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index ee61d366a..fb89878fc 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -407,6 +407,7 @@ sector: biomass: true industry: true agriculture: true + fossil_fuels: true district_heating: potential: 0.6 progress: diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index dfa06cac5..afef5c8fa 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -542,11 +542,19 @@ def add_carrier_buses(n, carrier, nodes=None): capital_cost=capital_cost, ) + fossils = ['coal', 'gas', 'oil', 'lignite'] + if not options.get('fossil_fuels',True) and carrier in fossils: + print('Not adding fossil ', carrier) + extendable = False + else: + print('Adding fossil ', carrier) + extendable = True + n.madd( "Generator", nodes, bus=nodes, - p_nom_extendable=True, + p_nom_extendable=extendable, carrier=carrier, marginal_cost=costs.at[carrier, "fuel"], ) @@ -2894,12 +2902,17 @@ def add_industry(n, costs): carrier="oil", ) + if not options.get('fossil_fuels', True): + extendable = False + else: + extendable = True + if "oil" not in n.generators.carrier.unique(): n.madd( "Generator", spatial.oil.nodes, bus=spatial.oil.nodes, - p_nom_extendable=True, + p_nom_extendable=extendable, carrier="oil", marginal_cost=costs.at["oil", "fuel"], ) From 5ca27837f37654e04c519081f40658215bec2c84 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 4 Jul 2024 09:20:58 +0000 Subject: [PATCH 014/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/prepare_sector_network.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index afef5c8fa..e4fc32997 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -542,12 +542,12 @@ def add_carrier_buses(n, carrier, nodes=None): capital_cost=capital_cost, ) - fossils = ['coal', 'gas', 'oil', 'lignite'] - if not options.get('fossil_fuels',True) and carrier in fossils: - print('Not adding fossil ', carrier) + fossils = ["coal", "gas", "oil", "lignite"] + if not options.get("fossil_fuels", True) and carrier in fossils: + print("Not adding fossil ", carrier) extendable = False else: - print('Adding fossil ', carrier) + print("Adding fossil ", carrier) extendable = True n.madd( @@ -2902,7 +2902,7 @@ def add_industry(n, costs): carrier="oil", ) - if not options.get('fossil_fuels', True): + if not options.get("fossil_fuels", True): extendable = False else: extendable = True From c81d208da21fb15f1ba3da80b30dc0ee2cc9628d Mon Sep 17 00:00:00 2001 From: millingermarkus Date: Thu, 4 Jul 2024 11:59:32 +0200 Subject: [PATCH 015/344] Remove redundant Waste CHP addition --- scripts/prepare_sector_network.py | 42 ------------------------------- 1 file changed, 42 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index adeb83197..57d58fbb7 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2496,48 +2496,6 @@ def add_biomass(n, costs): lifetime=costs.at[key, "lifetime"], ) - if options['waste_chp']: - print('Adding waste CHPs') - n.madd("Link", - urban_central + " waste CHP", - bus0=urban_central + " municipal solid waste", - bus1=urban_central, - bus4=urban_central + " urban central heat", - bus3="co2 atmosphere", - carrier="urban central waste incineration", - p_nom_extendable=True, - # p_nom=biomass_potential['municipal solid waste'] / 8760, - capital_cost=costs.at['waste CHP', 'fixed'] * costs.at['waste CHP', 'efficiency'], - marginal_cost=costs.at['waste CHP', 'VOM'], - efficiency=costs.at['waste CHP', 'efficiency'], - efficiency4=costs.at['waste CHP', 'efficiency-heat'], - efficiency3=costs.at['solid biomass', 'CO2 intensity']-costs.at['solid biomass', 'CO2 intensity'], - lifetime=costs.at['waste CHP', 'lifetime']) - - if beccs: - n.madd("Link", - urban_central + " waste CHP CC", - bus0=urban_central + " municipal solid waste", - bus1=urban_central, - bus4=urban_central + " urban central heat", - bus3="co2 atmosphere", - bus2="co2 stored", - carrier="urban central waste incineration CC", - p_nom_extendable=True, - # p_nom=costs.at['waste CHP CC', 'efficiency'] * biomass_potential['municipal solid waste'] / 8760, - capital_cost=costs.at['waste CHP CC', 'fixed'] * costs.at['waste CHP CC', 'efficiency'] - + costs.at['biomass CHP capture', 'fixed'] * costs.at['solid biomass', 'CO2 intensity'], - marginal_cost=costs.at['waste CHP CC', 'VOM'], - efficiency=costs.at['waste CHP CC', 'efficiency'], - efficiency4=costs.at['waste CHP CC', 'efficiency-heat'], - #Assuming same CO2 intensity as solid biomass - efficiency3=costs.at['solid biomass', 'CO2 intensity'] * (1 - options["cc_fraction"])-costs.at['solid biomass', 'CO2 intensity'], - efficiency2=costs.at['solid biomass', 'CO2 intensity'] * options["cc_fraction"], - c_b=costs.at['waste CHP CC', 'c_b'], - c_v=costs.at['waste CHP CC', 'c_v'], - lifetime=costs.at['waste CHP CC', 'lifetime']) - - if options["biomass_boiler"]: # TODO: Add surcharge for pellets nodes = pop_layout.index From fbacb76e9c37f5191c49dd12de690483021f1e05 Mon Sep 17 00:00:00 2001 From: millingermarkus Date: Thu, 4 Jul 2024 13:34:05 +0200 Subject: [PATCH 016/344] Minor edits to MSW --- config/config.default.yaml | 4 +++- scripts/prepare_sector_network.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index ee61d366a..af38830f5 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -365,7 +365,6 @@ biomass: - Secondary Forestry residues - woodchips - Sawdust - Residues from landscape care - - Municipal waste not included: - Sugar from sugar beet - Rape seed @@ -379,6 +378,8 @@ biomass: biogas: - Manure solid, liquid - Sludge + municipal solid waste: + - Municipal waste # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#solar-thermal solar_thermal: @@ -1017,6 +1018,7 @@ plotting: biogas: '#e3d37d' biomass: '#baa741' solid biomass: '#baa741' + municipal solid waste: '#91ba41' solid biomass transport: '#baa741' solid biomass for industry: '#7a6d26' solid biomass for industry CC: '#47411c' diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 57d58fbb7..297574a29 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -56,6 +56,7 @@ def define_spatial(nodes, options): # biomass spatial.biomass = SimpleNamespace() + spatial.msw = SimpleNamespace() if options.get("biomass_spatial", options["biomass_transport"]): spatial.biomass.nodes = nodes + " solid biomass" @@ -3110,6 +3111,17 @@ def add_industry(n, costs): efficiency3=process_co2_per_naphtha, ) + if options.get("biomass",True): + n.madd( + "Link", + spatial.msw.locations, + bus0=spatial.msw.nodes, + bus1=non_sequestered_hvc_locations, + carrier="municipal solid waste", + p_nom_extendable=True, + efficiency=1., + ) + n.madd( "Link", spatial.oil.demand_locations, From e42b36e83db6fe7c9b6d38ece0c5554c6e09510d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 4 Jul 2024 11:40:19 +0000 Subject: [PATCH 017/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/prepare_sector_network.py | 33 ++++++++++++++++++------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 297574a29..66bb346ef 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2249,12 +2249,14 @@ def add_biomass(n, costs): solid_biomass_potentials_spatial = biomass_potentials["solid biomass"].rename( index=lambda x: x + " solid biomass" ) - msw_biomass_potentials_spatial = biomass_potentials["municipal solid waste"].rename( - index=lambda x: x + " municipal solid waste" - ) + msw_biomass_potentials_spatial = biomass_potentials[ + "municipal solid waste" + ].rename(index=lambda x: x + " municipal solid waste") else: solid_biomass_potentials_spatial = biomass_potentials["solid biomass"].sum() - msw_biomass_potentials_spatial = biomass_potentials["municipal solid waste"].sum() + msw_biomass_potentials_spatial = biomass_potentials[ + "municipal solid waste" + ].sum() n.add("Carrier", "biogas") n.add("Carrier", "solid biomass") @@ -2276,10 +2278,12 @@ def add_biomass(n, costs): unit="MWh_LHV", ) - n.madd("Bus", - spatial.msw.nodes, - location=spatial.msw.locations, - carrier="municipal solid waste") + n.madd( + "Bus", + spatial.msw.nodes, + location=spatial.msw.locations, + carrier="municipal solid waste", + ) n.madd( "Store", @@ -2301,12 +2305,13 @@ def add_biomass(n, costs): e_initial=solid_biomass_potentials_spatial, ) - n.madd("Store", + n.madd( + "Store", spatial.msw.nodes, bus=spatial.msw.nodes, carrier="municipal solid waste", e_nom=msw_biomass_potentials_spatial, - marginal_cost=0,#costs.at["municipal solid waste", "fuel"], + marginal_cost=0, # costs.at["municipal solid waste", "fuel"], e_initial=msw_biomass_potentials_spatial, ) @@ -2422,14 +2427,14 @@ def add_biomass(n, costs): type="operational_limit", ) - #Add municipal solid waste + # Add municipal solid waste n.madd( "Generator", spatial.msw.nodes, bus=spatial.msw.nodes, carrier="municipal solid waste", p_nom=10000, - marginal_cost=0#costs.at["municipal solid waste", "fuel"] + marginal_cost=0 # costs.at["municipal solid waste", "fuel"] + bus_transport_costs * average_distance, ) n.add( @@ -3111,7 +3116,7 @@ def add_industry(n, costs): efficiency3=process_co2_per_naphtha, ) - if options.get("biomass",True): + if options.get("biomass", True): n.madd( "Link", spatial.msw.locations, @@ -3119,7 +3124,7 @@ def add_industry(n, costs): bus1=non_sequestered_hvc_locations, carrier="municipal solid waste", p_nom_extendable=True, - efficiency=1., + efficiency=1.0, ) n.madd( From ecccc1429fdc1c6c4dda81c886bb60170ed3e113 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Thu, 18 Jul 2024 15:39:27 +0200 Subject: [PATCH 018/344] add rule to retrieve JRC IDEES 2021 --- rules/retrieve.smk | 9 ++++++ scripts/retrieve_jrc_idees.py | 55 +++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 scripts/retrieve_jrc_idees.py diff --git a/rules/retrieve.smk b/rules/retrieve.smk index 10ad9684a..5b0871cab 100644 --- a/rules/retrieve.smk +++ b/rules/retrieve.smk @@ -54,6 +54,15 @@ if config["enable"]["retrieve"] and config["enable"].get("retrieve_databundle", script: "../scripts/retrieve_eurostat_data.py" + rule retrieve_jrc_idees: + output: + directory("data/bundle/jrc-idees-2021"), + log: + "logs/retrieve_jrc_idees.log", + retries: 2 + script: + "../scripts/retrieve_jrc_idees.py" + rule retrieve_eurostat_household_data: output: "data/eurostat/eurostat-household_energy_balances-february_2024.csv", diff --git a/scripts/retrieve_jrc_idees.py b/scripts/retrieve_jrc_idees.py new file mode 100644 index 000000000..094843113 --- /dev/null +++ b/scripts/retrieve_jrc_idees.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2024- The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT +""" +Retrieve and extract JRC IDEES 2021 data. +""" + +import os +import requests +from pathlib import Path +from bs4 import BeautifulSoup +from _helpers import configure_logging, progress_retrieve, set_scenario_config +import zipfile +import logging + + +logger = logging.getLogger(__name__) + +# Define the base URL +url_jrc = "https://jeodpp.jrc.ec.europa.eu/ftp/jrc-opendata/JRC-IDEES/JRC-IDEES-2021_v1/" + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + snakemake = mock_snakemake("retrieve_jrc_idees") + rootpath = ".." + else: + rootpath = "." + + configure_logging(snakemake) + set_scenario_config(snakemake) + + disable_progress = snakemake.config["run"].get("disable_progressbar", False) + + # create a local directory to save the zip files + local_dir = snakemake.output[0] + if not os.path.exists(local_dir): + os.makedirs(local_dir) + + # get the list of zip files from the JRC URL + response = requests.get(url_jrc) + soup = BeautifulSoup(response.text, 'html.parser') + zip_files = [link.get('href') for link in soup.find_all('a') + if link.get('href').endswith('.zip')] + + logger.info(f"Downloading {len(zip_files)} .zip files for JRC IDEES from '{url_jrc}'.") + + # download and unpack each zip file + for zip_file in zip_files: + logger.info(f"Downloading and unpacking {zip_file}") + zip_url = url_jrc + zip_file + to_fn = local_dir+"/" + zip_file[:-4] + progress_retrieve(zip_url, to_fn, disable=disable_progress) + From 957176a20d66161d54c62bdea1a842e3637a6be5 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Thu, 18 Jul 2024 15:39:52 +0200 Subject: [PATCH 019/344] adjustment to build_energy_totals --- rules/build_sector.smk | 2 +- scripts/build_energy_totals.py | 125 +++++++++++++++++++-------------- 2 files changed, 73 insertions(+), 54 deletions(-) diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 6614b163a..866f69625 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -290,7 +290,7 @@ rule build_energy_totals: co2="data/bundle/eea/UNFCCC_v23.csv", swiss="data/switzerland-new_format-all_years.csv", swiss_transport="data/gr-e-11.03.02.01.01-cc.csv", - idees="data/bundle/jrc-idees-2015", + idees="data/bundle/jrc-idees-2021", district_heat_share="data/district_heat_share.csv", eurostat="data/eurostat/Balances-April2023", eurostat_households="data/eurostat/eurostat-household_energy_balances-february_2024.csv", diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index a476ec657..1dbc93e66 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -110,7 +110,8 @@ def reverse(dictionary: dict) -> dict: idees_rename = {"GR": "EL", "GB": "UK"} eu28 = cc.EU28as("ISO2").ISO2.tolist() - +# TODO GB kicked out JRC-IDEES 2021 +eu27 = cc.EU27as("ISO2").ISO2.tolist() eu28_eea = eu28.copy() eu28_eea.remove("GB") eu28_eea.append("UK") @@ -329,9 +330,9 @@ def idees_per_country(ct: str, base_dir: str) -> pd.DataFrame: """ ct_idees = idees_rename.get(ct, ct) - fn_residential = f"{base_dir}/JRC-IDEES-2015_Residential_{ct_idees}.xlsx" - fn_tertiary = f"{base_dir}/JRC-IDEES-2015_Tertiary_{ct_idees}.xlsx" - fn_transport = f"{base_dir}/JRC-IDEES-2015_Transport_{ct_idees}.xlsx" + fn_residential = f"{base_dir}/JRC-IDEES-2021_Residential_{ct_idees}.xlsx" + fn_tertiary = f"{base_dir}/JRC-IDEES-2021_Tertiary_{ct_idees}.xlsx" + fn_transport = f"{base_dir}/JRC-IDEES-2021_Transport_{ct_idees}.xlsx" ct_totals = {} @@ -357,14 +358,16 @@ def idees_per_country(ct: str, base_dir: str) -> pd.DataFrame: row = "Energy consumption by fuel - Eurostat structure (ktoe)" ct_totals["total residential"] = df.loc[row] - assert df.index[47] == "Electricity" - ct_totals["electricity residential"] = df.iloc[47] + assert df.index[40] == "Electricity" + ct_totals["electricity residential"] = df.iloc[40] - assert df.index[46] == "Derived heat" - ct_totals["derived heat residential"] = df.iloc[46] + # TODO derived heat changed to distributed heat and numbers changed as well! + # this needs to be checked + assert df.index[39] == "Distributed heat" + ct_totals["derived heat residential"] = df.iloc[39] - assert df.index[50] == "Thermal uses" - ct_totals["thermal uses residential"] = df.iloc[50] + assert df.index[43] == "Thermal uses" + ct_totals["thermal uses residential"] = df.iloc[43] # services @@ -390,14 +393,15 @@ def idees_per_country(ct: str, base_dir: str) -> pd.DataFrame: row = "Energy consumption by fuel - Eurostat structure (ktoe)" ct_totals["total services"] = df.loc[row] - assert df.index[50] == "Electricity" - ct_totals["electricity services"] = df.iloc[50] + assert df.index[43] == "Electricity" + ct_totals["electricity services"] = df.iloc[43] - assert df.index[49] == "Derived heat" - ct_totals["derived heat services"] = df.iloc[49] + # TODO check derived heat changed to distributed heat + assert df.index[42] == "Distributed heat" + ct_totals["derived heat services"] = df.iloc[42] - assert df.index[53] == "Thermal uses" - ct_totals["thermal uses services"] = df.iloc[53] + assert df.index[46] == "Thermal uses" + ct_totals["thermal uses services"] = df.iloc[46] # agriculture, forestry and fishing @@ -410,7 +414,7 @@ def idees_per_country(ct: str, base_dir: str) -> pd.DataFrame: "Lighting", "Ventilation", "Specific electricity uses", - "Pumping devices (electric)", + "Pumping devices (electricity)", ] ct_totals["total agriculture electricity"] = df.loc[rows].sum() @@ -419,8 +423,8 @@ def idees_per_country(ct: str, base_dir: str) -> pd.DataFrame: rows = [ "Motor drives", - "Farming machine drives (diesel oil incl. biofuels)", - "Pumping devices (diesel oil incl. biofuels)", + "Farming machine drives (diesel oil and liquid biofuels)", + "Pumping devices (diesel oil and liquid biofuels)", ] ct_totals["total agriculture machinery"] = df.loc[rows].sum() @@ -435,7 +439,7 @@ def idees_per_country(ct: str, base_dir: str) -> pd.DataFrame: ct_totals["electricity road"] = df.loc["Electricity"] - ct_totals["total two-wheel"] = df.loc["Powered 2-wheelers (Gasoline)"] + ct_totals["total two-wheel"] = df.loc["Powered two-wheelers (Gasoline)"] assert df.index[19] == "Passenger cars" ct_totals["total passenger cars"] = df.iloc[19] @@ -449,58 +453,64 @@ def idees_per_country(ct: str, base_dir: str) -> pd.DataFrame: assert df.index[39] == "Battery electric vehicles" ct_totals["electricity other road passenger"] = df.iloc[39] - assert df.index[41] == "Light duty vehicles" + assert df.index[41] == "Light commercial vehicles" ct_totals["total light duty road freight"] = df.iloc[41] assert df.index[49] == "Battery electric vehicles" ct_totals["electricity light duty road freight"] = df.iloc[49] - row = "Heavy duty vehicles (Diesel oil incl. biofuels)" + row = "Heavy goods vehicles (Diesel oil incl. biofuels)" ct_totals["total heavy duty road freight"] = df.loc[row] assert df.index[61] == "Passenger cars" ct_totals["passenger car efficiency"] = df.iloc[61] + df = pd.read_excel(fn_transport, "TrRail_ene", index_col=0) - ct_totals["total rail"] = df.loc["by fuel (EUROSTAT DATA)"] + ct_totals["total rail"] = df.loc["by fuel"] ct_totals["electricity rail"] = df.loc["Electricity"] - assert df.index[15] == "Passenger transport" - ct_totals["total rail passenger"] = df.iloc[15] + assert df.index[9] == "Passenger transport" + ct_totals["total rail passenger"] = df.iloc[9] - assert df.index[16] == "Metro and tram, urban light rail" - assert df.index[19] == "Electric" - assert df.index[20] == "High speed passenger trains" - ct_totals["electricity rail passenger"] = df.iloc[[16, 19, 20]].sum() + assert df.index[10] == "Metro and tram, urban light rail" + assert df.index[13] == "Electric" + assert df.index[14] == "High speed passenger trains" + ct_totals["electricity rail passenger"] = df.iloc[[10, 13, 14]].sum() - assert df.index[21] == "Freight transport" - ct_totals["total rail freight"] = df.iloc[21] + assert df.index[15] == "Freight transport" + ct_totals["total rail freight"] = df.iloc[15] - assert df.index[23] == "Electric" - ct_totals["electricity rail freight"] = df.iloc[23] + assert df.index[17] == "Electric" + ct_totals["electricity rail freight"] = df.iloc[17] df = pd.read_excel(fn_transport, "TrAvia_ene", index_col=0) - assert df.index[6] == "Passenger transport" - ct_totals["total aviation passenger"] = df.iloc[6] + assert df.index[4] == "Passenger transport" + ct_totals["total aviation passenger"] = df.iloc[4] - assert df.index[10] == "Freight transport" - ct_totals["total aviation freight"] = df.iloc[10] + assert df.index[8] == "Freight transport" + ct_totals["total aviation freight"] = df.iloc[8] - assert df.index[7] == "Domestic" - ct_totals["total domestic aviation passenger"] = df.iloc[7] + assert df.index[2] == "Domestic" + ct_totals["total domestic aviation passenger"] = df.iloc[2] - assert df.index[8] == "International - Intra-EU" - assert df.index[9] == "International - Extra-EU" - ct_totals["total international aviation passenger"] = df.iloc[[8, 9]].sum() + # TODO added Ukraine to intra EU flights + assert df.index[6] == "International - Intra-EEAwUK" + assert df.index[7] == "International - Extra-EEAwUK" + ct_totals["total international aviation passenger"] = df.iloc[[6, 7]].sum() + + # TODO freight changed from "Domestic and International - Intra-EU" -> split + # domestic and international (intra-EU and outside EU) + assert df.index[9] == "Domestic" + ct_totals["total domestic aviation freight"] = df.iloc[9] - assert df.index[11] == "Domestic and International - Intra-EU" - ct_totals["total domestic aviation freight"] = df.iloc[11] - assert df.index[12] == "International - Extra-EU" - ct_totals["total international aviation freight"] = df.iloc[12] + assert df.index[10] == "International - Intra-EEAwUK" + assert df.index[11] == "International - Extra-EEAwUK" + ct_totals["total international aviation freight"] = df.iloc[[10, 11]].sum() ct_totals["total domestic aviation"] = ( ct_totals["total domestic aviation freight"] @@ -515,7 +525,7 @@ def idees_per_country(ct: str, base_dir: str) -> pd.DataFrame: df = pd.read_excel(fn_transport, "TrNavi_ene", index_col=0) # coastal and inland - ct_totals["total domestic navigation"] = df.loc["by fuel (EUROSTAT DATA)"] + ct_totals["total domestic navigation"] = df.loc["Energy consumption (ktoe)"] df = pd.read_excel(fn_transport, "TrRoad_act", index_col=0) @@ -567,10 +577,15 @@ def build_idees(countries: List[str]) -> pd.DataFrame: names=["country", "year"], ) + # clean up dataframe + years = np.arange(2000, 2022) + totals = totals[totals.index.get_level_values(1).isin(years)] + # efficiency kgoe/100km -> ktoe/100km so that after conversion TWh/100km totals.loc[:, "passenger car efficiency"] /= 1e6 # convert ktoe to TWh exclude = totals.columns.str.fullmatch("passenger cars") + totals = totals.copy() totals.loc[:, ~exclude] *= 11.63 / 1e3 return totals @@ -664,7 +679,8 @@ def build_energy_totals( for use in uses: fuel_use = df[f"electricity {sector} {use}"] - fuel = df[f"electricity {sector}"] + fuel = (df[f"electricity {sector}"] + .replace(0, np.nan).infer_objects(copy=False)) avg = fuel_use.div(fuel).mean() logger.debug( f"{sector}: average fraction of electricity for {use} is {avg:.3f}" @@ -680,6 +696,7 @@ def build_energy_totals( df[f"total {sector} {use}"] - df[f"electricity {sector} {use}"] ) nonelectric = df[f"total {sector}"] - df[f"electricity {sector}"] + nonelectric = nonelectric.copy().replace(0, np.nan) avg = nonelectric_use.div(nonelectric).mean() logger.debug( f"{sector}: average fraction of non-electric for {use} is {avg:.3f}" @@ -716,6 +733,7 @@ def build_energy_totals( nonelectric = ( no_norway[f"total {sector}"] - no_norway[f"electricity {sector}"] ) + nonelectric = nonelectric.copy().replace(0, np.nan) fraction = nonelectric_use.div(nonelectric).mean() df.loc["NO", f"total {sector} {use}"] = ( total_heating * fraction @@ -793,7 +811,8 @@ def build_energy_totals( mean_BA = df.loc["BA"].loc[2014:2021, "total residential"].mean() mean_RS = df.loc["RS"].loc[2014:2021, "total residential"].mean() ratio = mean_BA / mean_RS - df.loc["BA"] = df.loc["BA"].replace(0.0, np.nan).values + df.loc["BA"] = (df.loc["BA"].replace(0.0, np.nan) + .infer_objects(copy=False).values) df.loc["BA"] = df.loc["BA"].combine_first(ratio * df.loc["RS"]).values return df @@ -1375,7 +1394,7 @@ def update_residential_from_eurostat(energy: pd.DataFrame) -> pd.DataFrame: "Updated energy balances for residential using disaggregate final energy consumption data in Households from Eurostat" ) - +#%% if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake @@ -1391,7 +1410,7 @@ def update_residential_from_eurostat(energy: pd.DataFrame) -> pd.DataFrame: population = nuts3["pop"].groupby(nuts3.country).sum() countries = snakemake.params.countries - idees_countries = pd.Index(countries).intersection(eu28) + idees_countries = pd.Index(countries).intersection(eu27) input_eurostat = snakemake.input.eurostat eurostat = build_eurostat( @@ -1405,8 +1424,8 @@ def update_residential_from_eurostat(energy: pd.DataFrame) -> pd.DataFrame: energy = build_energy_totals(countries, eurostat, swiss, idees) - # Data from IDEES only exists from 2000-2015. - logger.info("Extrapolate IDEES data based on eurostat for years 2015-2021.") + # Data from IDEES only exists from 2000-2021. + logger.info("Extrapolate IDEES data based on eurostat for years 2021-x.") energy = rescale_idees_from_eurostat(idees_countries, energy, eurostat) update_residential_from_eurostat(energy) From 8cc23945b5ec9cb3fa4107e14b2679ae9d5b65d5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 18 Jul 2024 13:43:38 +0000 Subject: [PATCH 020/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/build_energy_totals.py | 19 +++++++------- scripts/retrieve_jrc_idees.py | 45 ++++++++++++++++++++-------------- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index 1dbc93e66..a6eb70d16 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -465,7 +465,6 @@ def idees_per_country(ct: str, base_dir: str) -> pd.DataFrame: assert df.index[61] == "Passenger cars" ct_totals["passenger car efficiency"] = df.iloc[61] - df = pd.read_excel(fn_transport, "TrRail_ene", index_col=0) ct_totals["total rail"] = df.loc["by fuel"] @@ -501,13 +500,12 @@ def idees_per_country(ct: str, base_dir: str) -> pd.DataFrame: assert df.index[6] == "International - Intra-EEAwUK" assert df.index[7] == "International - Extra-EEAwUK" ct_totals["total international aviation passenger"] = df.iloc[[6, 7]].sum() - + # TODO freight changed from "Domestic and International - Intra-EU" -> split # domestic and international (intra-EU and outside EU) assert df.index[9] == "Domestic" ct_totals["total domestic aviation freight"] = df.iloc[9] - assert df.index[10] == "International - Intra-EEAwUK" assert df.index[11] == "International - Extra-EEAwUK" ct_totals["total international aviation freight"] = df.iloc[[10, 11]].sum() @@ -580,7 +578,7 @@ def build_idees(countries: List[str]) -> pd.DataFrame: # clean up dataframe years = np.arange(2000, 2022) totals = totals[totals.index.get_level_values(1).isin(years)] - + # efficiency kgoe/100km -> ktoe/100km so that after conversion TWh/100km totals.loc[:, "passenger car efficiency"] /= 1e6 # convert ktoe to TWh @@ -679,8 +677,9 @@ def build_energy_totals( for use in uses: fuel_use = df[f"electricity {sector} {use}"] - fuel = (df[f"electricity {sector}"] - .replace(0, np.nan).infer_objects(copy=False)) + fuel = ( + df[f"electricity {sector}"].replace(0, np.nan).infer_objects(copy=False) + ) avg = fuel_use.div(fuel).mean() logger.debug( f"{sector}: average fraction of electricity for {use} is {avg:.3f}" @@ -811,8 +810,9 @@ def build_energy_totals( mean_BA = df.loc["BA"].loc[2014:2021, "total residential"].mean() mean_RS = df.loc["RS"].loc[2014:2021, "total residential"].mean() ratio = mean_BA / mean_RS - df.loc["BA"] = (df.loc["BA"].replace(0.0, np.nan) - .infer_objects(copy=False).values) + df.loc["BA"] = ( + df.loc["BA"].replace(0.0, np.nan).infer_objects(copy=False).values + ) df.loc["BA"] = df.loc["BA"].combine_first(ratio * df.loc["RS"]).values return df @@ -1394,7 +1394,8 @@ def update_residential_from_eurostat(energy: pd.DataFrame) -> pd.DataFrame: "Updated energy balances for residential using disaggregate final energy consumption data in Households from Eurostat" ) -#%% + +# %% if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake diff --git a/scripts/retrieve_jrc_idees.py b/scripts/retrieve_jrc_idees.py index 094843113..e163a163c 100644 --- a/scripts/retrieve_jrc_idees.py +++ b/scripts/retrieve_jrc_idees.py @@ -6,50 +6,57 @@ Retrieve and extract JRC IDEES 2021 data. """ +import logging import os -import requests -from pathlib import Path -from bs4 import BeautifulSoup -from _helpers import configure_logging, progress_retrieve, set_scenario_config import zipfile -import logging +from pathlib import Path +import requests +from _helpers import configure_logging, progress_retrieve, set_scenario_config +from bs4 import BeautifulSoup logger = logging.getLogger(__name__) # Define the base URL -url_jrc = "https://jeodpp.jrc.ec.europa.eu/ftp/jrc-opendata/JRC-IDEES/JRC-IDEES-2021_v1/" - +url_jrc = ( + "https://jeodpp.jrc.ec.europa.eu/ftp/jrc-opendata/JRC-IDEES/JRC-IDEES-2021_v1/" +) + if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake + snakemake = mock_snakemake("retrieve_jrc_idees") rootpath = ".." else: rootpath = "." - + configure_logging(snakemake) set_scenario_config(snakemake) - + disable_progress = snakemake.config["run"].get("disable_progressbar", False) - + # create a local directory to save the zip files local_dir = snakemake.output[0] if not os.path.exists(local_dir): os.makedirs(local_dir) - + # get the list of zip files from the JRC URL response = requests.get(url_jrc) - soup = BeautifulSoup(response.text, 'html.parser') - zip_files = [link.get('href') for link in soup.find_all('a') - if link.get('href').endswith('.zip')] - - logger.info(f"Downloading {len(zip_files)} .zip files for JRC IDEES from '{url_jrc}'.") - + soup = BeautifulSoup(response.text, "html.parser") + zip_files = [ + link.get("href") + for link in soup.find_all("a") + if link.get("href").endswith(".zip") + ] + + logger.info( + f"Downloading {len(zip_files)} .zip files for JRC IDEES from '{url_jrc}'." + ) + # download and unpack each zip file for zip_file in zip_files: logger.info(f"Downloading and unpacking {zip_file}") zip_url = url_jrc + zip_file - to_fn = local_dir+"/" + zip_file[:-4] + to_fn = local_dir + "/" + zip_file[:-4] progress_retrieve(zip_url, to_fn, disable=disable_progress) - From 5f009590f5e9ecbbf41c030b77b94e608946072d Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Thu, 18 Jul 2024 15:48:45 +0200 Subject: [PATCH 021/344] remove todos --- scripts/build_energy_totals.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index a6eb70d16..b3f6ddb73 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -110,7 +110,6 @@ def reverse(dictionary: dict) -> dict: idees_rename = {"GR": "EL", "GB": "UK"} eu28 = cc.EU28as("ISO2").ISO2.tolist() -# TODO GB kicked out JRC-IDEES 2021 eu27 = cc.EU27as("ISO2").ISO2.tolist() eu28_eea = eu28.copy() eu28_eea.remove("GB") From 4c46c57fecf002c0027b977b2fd6f15d0df5dca4 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Fri, 19 Jul 2024 10:20:19 +0200 Subject: [PATCH 022/344] avoid double download of JRC idees --- scripts/build_energy_totals.py | 10 ++++++--- scripts/retrieve_jrc_idees.py | 41 +++++++++++++--------------------- 2 files changed, 22 insertions(+), 29 deletions(-) diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index b3f6ddb73..a93562b2b 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -329,9 +329,9 @@ def idees_per_country(ct: str, base_dir: str) -> pd.DataFrame: """ ct_idees = idees_rename.get(ct, ct) - fn_residential = f"{base_dir}/JRC-IDEES-2021_Residential_{ct_idees}.xlsx" - fn_tertiary = f"{base_dir}/JRC-IDEES-2021_Tertiary_{ct_idees}.xlsx" - fn_transport = f"{base_dir}/JRC-IDEES-2021_Transport_{ct_idees}.xlsx" + fn_residential = f"{base_dir}/{ct_idees}/JRC-IDEES-2021_Residential_{ct_idees}.xlsx" + fn_tertiary = f"{base_dir}/{ct_idees}/JRC-IDEES-2021_Tertiary_{ct_idees}.xlsx" + fn_transport = f"{base_dir}/{ct_idees}/JRC-IDEES-2021_Transport_{ct_idees}.xlsx" ct_totals = {} @@ -1103,6 +1103,10 @@ def build_transport_data( transport_data = pd.concat([transport_data, swiss_cars]).sort_index() transport_data.rename(columns={"passenger cars": "number cars"}, inplace=True) + + # clean up dataframe + years = np.arange(2000, 2022) + transport_data = transport_data[transport_data.index.get_level_values(1).isin(years)] missing = transport_data.index[transport_data["number cars"].isna()] if not missing.empty: diff --git a/scripts/retrieve_jrc_idees.py b/scripts/retrieve_jrc_idees.py index e163a163c..6c61ee19f 100644 --- a/scripts/retrieve_jrc_idees.py +++ b/scripts/retrieve_jrc_idees.py @@ -10,22 +10,19 @@ import os import zipfile from pathlib import Path - -import requests from _helpers import configure_logging, progress_retrieve, set_scenario_config -from bs4 import BeautifulSoup + logger = logging.getLogger(__name__) # Define the base URL url_jrc = ( - "https://jeodpp.jrc.ec.europa.eu/ftp/jrc-opendata/JRC-IDEES/JRC-IDEES-2021_v1/" + "https://jeodpp.jrc.ec.europa.eu/ftp/jrc-opendata/JRC-IDEES/JRC-IDEES-2021_v1/JRC-IDEES-2021.zip" ) if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake - snakemake = mock_snakemake("retrieve_jrc_idees") rootpath = ".." else: @@ -33,30 +30,22 @@ configure_logging(snakemake) set_scenario_config(snakemake) - disable_progress = snakemake.config["run"].get("disable_progressbar", False) - # create a local directory to save the zip files - local_dir = snakemake.output[0] - if not os.path.exists(local_dir): - os.makedirs(local_dir) - - # get the list of zip files from the JRC URL - response = requests.get(url_jrc) - soup = BeautifulSoup(response.text, "html.parser") - zip_files = [ - link.get("href") - for link in soup.find_all("a") - if link.get("href").endswith(".zip") - ] + to_fn = snakemake.output[0] + to_fn_zp = to_fn + ".zip" + # download .zip file logger.info( - f"Downloading {len(zip_files)} .zip files for JRC IDEES from '{url_jrc}'." + f"Downloading JRC IDEES from {url_jrc}." ) + progress_retrieve(url_jrc, to_fn_zp, disable=disable_progress) + + # extract + logger.info("Extracting JRC IDEES data.") + with zipfile.ZipFile(to_fn_zp, "r") as zip_ref: + zip_ref.extractall(to_fn) + + logger.info(f"JRC IDEES data available in '{to_fn}'.") - # download and unpack each zip file - for zip_file in zip_files: - logger.info(f"Downloading and unpacking {zip_file}") - zip_url = url_jrc + zip_file - to_fn = local_dir + "/" + zip_file[:-4] - progress_retrieve(zip_url, to_fn, disable=disable_progress) + \ No newline at end of file From 086d46cdb4aa0a8943da99057d57f6068615ae4b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 19 Jul 2024 08:21:07 +0000 Subject: [PATCH 023/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/build_energy_totals.py | 6 ++++-- scripts/retrieve_jrc_idees.py | 15 +++++---------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index a93562b2b..72c2b5bde 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -1103,10 +1103,12 @@ def build_transport_data( transport_data = pd.concat([transport_data, swiss_cars]).sort_index() transport_data.rename(columns={"passenger cars": "number cars"}, inplace=True) - + # clean up dataframe years = np.arange(2000, 2022) - transport_data = transport_data[transport_data.index.get_level_values(1).isin(years)] + transport_data = transport_data[ + transport_data.index.get_level_values(1).isin(years) + ] missing = transport_data.index[transport_data["number cars"].isna()] if not missing.empty: diff --git a/scripts/retrieve_jrc_idees.py b/scripts/retrieve_jrc_idees.py index 6c61ee19f..0e23b82c6 100644 --- a/scripts/retrieve_jrc_idees.py +++ b/scripts/retrieve_jrc_idees.py @@ -10,19 +10,18 @@ import os import zipfile from pathlib import Path -from _helpers import configure_logging, progress_retrieve, set_scenario_config +from _helpers import configure_logging, progress_retrieve, set_scenario_config logger = logging.getLogger(__name__) # Define the base URL -url_jrc = ( - "https://jeodpp.jrc.ec.europa.eu/ftp/jrc-opendata/JRC-IDEES/JRC-IDEES-2021_v1/JRC-IDEES-2021.zip" -) +url_jrc = "https://jeodpp.jrc.ec.europa.eu/ftp/jrc-opendata/JRC-IDEES/JRC-IDEES-2021_v1/JRC-IDEES-2021.zip" if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake + snakemake = mock_snakemake("retrieve_jrc_idees") rootpath = ".." else: @@ -36,16 +35,12 @@ to_fn_zp = to_fn + ".zip" # download .zip file - logger.info( - f"Downloading JRC IDEES from {url_jrc}." - ) + logger.info(f"Downloading JRC IDEES from {url_jrc}.") progress_retrieve(url_jrc, to_fn_zp, disable=disable_progress) - + # extract logger.info("Extracting JRC IDEES data.") with zipfile.ZipFile(to_fn_zp, "r") as zip_ref: zip_ref.extractall(to_fn) logger.info(f"JRC IDEES data available in '{to_fn}'.") - - \ No newline at end of file From 8fe5921e44266dc0b4ececd1e3bbc93a229e65ca Mon Sep 17 00:00:00 2001 From: Amos Schledorn <60692940+amosschle@users.noreply.github.com> Date: Fri, 19 Jul 2024 12:30:26 +0200 Subject: [PATCH 024/344] Fix negative district heating progress (#1168) * impose minimum district heating progress of 0 * update release_notes * update configtables --- doc/configtables/sector.csv | 2 +- doc/release_notes.rst | 2 ++ scripts/build_district_heat_share.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/configtables/sector.csv b/doc/configtables/sector.csv index 059c42339..5045cecdc 100644 --- a/doc/configtables/sector.csv +++ b/doc/configtables/sector.csv @@ -5,7 +5,7 @@ biomass,--,"{true, false}",Flag to include biomass sector. industry,--,"{true, false}",Flag to include industry sector. agriculture,--,"{true, false}",Flag to include agriculture sector. district_heating,--,,`prepare_sector_network.py `_ --- potential,--,float,maximum fraction of urban demand which can be supplied by district heating +-- potential,--,float,maximum fraction of urban demand which can be supplied by district heating. Ignored where below current fraction. -- progress,--,Dictionary with planning horizons as keys., Increase of today's district heating demand to potential maximum district heating share. Progress = 0 means today's district heating share. Progress = 1 means maximum fraction of urban demand is supplied by district heating -- district_heating_loss,--,float,Share increase in district heat demand in urban central due to heat losses cluster_heat_buses,--,"{true, false}",Cluster residential and service heat buses in `prepare_sector_network.py `_ to one to save memory. diff --git a/doc/release_notes.rst b/doc/release_notes.rst index cf0f2c76b..fc8815cd6 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -31,6 +31,8 @@ Upcoming Release * Bugfix: Correctly read in threshold capacity below which to remove components from previous planning horizons in :mod:`add_brownfield`. +* Bugfix: Impose minimum value of zero for district heating progress between current and future market share in :mod:`build_district_heat_share`. + PyPSA-Eur 0.11.0 (25th May 2024) ===================================== diff --git a/scripts/build_district_heat_share.py b/scripts/build_district_heat_share.py index d62d2ab0f..7e8497d69 100644 --- a/scripts/build_district_heat_share.py +++ b/scripts/build_district_heat_share.py @@ -86,7 +86,7 @@ urban_fraction = pd.concat([urban_fraction, dist_fraction_node], axis=1).max(axis=1) # difference of max potential and today's share of district heating - diff = (urban_fraction * central_fraction) - dist_fraction_node + diff = ((urban_fraction * central_fraction) - dist_fraction_node).clip(lower=0) progress = get( snakemake.config["sector"]["district_heating"]["progress"], investment_year ) From 104fa7311935482620bc12e680515ae4adb6c12c Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Fri, 19 Jul 2024 14:36:46 +0200 Subject: [PATCH 025/344] remove rescale to Eurostat --- scripts/build_energy_totals.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index 72c2b5bde..a53189e07 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -1147,8 +1147,8 @@ def rescale_idees_from_eurostat( idees_countries: List[str], energy: pd.DataFrame, eurostat: pd.DataFrame ) -> pd.DataFrame: """ - Takes JRC IDEES data from 2015 and rescales it by the ratio of the Eurostat - data and the 2015 Eurostat data. + Takes JRC IDEES data from 2021 and rescales it by the ratio of the Eurostat + data and the 2021 Eurostat data. Missing data: ['passenger car efficiency', 'passenger cars'] Parameters @@ -1178,7 +1178,7 @@ def rescale_idees_from_eurostat( main_cols = ["Total all products", "Electricity"] # read in the eurostat data for 2015 - eurostat_2015 = eurostat.xs(2015, level="year")[main_cols] + eurostat_2015 = eurostat.xs(2021, level="year")[main_cols] # calculate the ratio of the two data sets ratio = eurostat[main_cols] / eurostat_2015 ratio = ratio.droplevel([2, 5]) @@ -1430,10 +1430,6 @@ def update_residential_from_eurostat(energy: pd.DataFrame) -> pd.DataFrame: energy = build_energy_totals(countries, eurostat, swiss, idees) - # Data from IDEES only exists from 2000-2021. - logger.info("Extrapolate IDEES data based on eurostat for years 2021-x.") - energy = rescale_idees_from_eurostat(idees_countries, energy, eurostat) - update_residential_from_eurostat(energy) energy.to_csv(snakemake.output.energy_name) From dea48c965a0dd36f05ca61c2b632ed71dfc179d2 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Fri, 19 Jul 2024 14:41:36 +0200 Subject: [PATCH 026/344] correct indexing of DH share --- data/district_heat_share.csv | 2 +- scripts/build_energy_totals.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/data/district_heat_share.csv b/data/district_heat_share.csv index 5afd65c86..07d4f51dd 100644 --- a/data/district_heat_share.csv +++ b/data/district_heat_share.csv @@ -22,7 +22,7 @@ RS,25,5821 SI,8.86,1739 ES,0.251589260787732,1273 SE,50.4, -UK,2, +GB,2, BY,70, EE,52,5406 KO,3,207 diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index a53189e07..349a2c825 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -860,8 +860,10 @@ def build_district_heat_share(countries: List[str], idees: pd.DataFrame) -> pd.S .squeeze() ) # make conservative assumption and take minimum from both data sets + new_index = pd.MultiIndex.from_product([dh_share.index, + district_heat_share.index.get_level_values(1).unique()]) district_heat_share = pd.concat( - [district_heat_share, dh_share.reindex_like(district_heat_share)], axis=1 + [district_heat_share, dh_share.reindex(new_index, level=0)], axis=1 ).min(axis=1) district_heat_share.name = "district heat share" From 028d0d6e7aeb9d4ef8c50313299e8b0c0cab095c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 19 Jul 2024 13:15:35 +0000 Subject: [PATCH 027/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/build_energy_totals.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index 349a2c825..70ed4bec6 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -860,8 +860,9 @@ def build_district_heat_share(countries: List[str], idees: pd.DataFrame) -> pd.S .squeeze() ) # make conservative assumption and take minimum from both data sets - new_index = pd.MultiIndex.from_product([dh_share.index, - district_heat_share.index.get_level_values(1).unique()]) + new_index = pd.MultiIndex.from_product( + [dh_share.index, district_heat_share.index.get_level_values(1).unique()] + ) district_heat_share = pd.concat( [district_heat_share, dh_share.reindex(new_index, level=0)], axis=1 ).min(axis=1) From 167b056defe1473dcc83048c9408b0848ec0a03b Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Fri, 19 Jul 2024 15:22:50 +0200 Subject: [PATCH 028/344] remove obsolete rural/urban COPs and rename cop_..._total to cop_..._individual_heating --- rules/build_sector.smk | 20 ++++---------------- scripts/build_cop_profiles.py | 11 +++++------ scripts/prepare_sector_network.py | 4 ++-- 3 files changed, 11 insertions(+), 24 deletions(-) diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 6614b163a..9986bcebf 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -220,18 +220,10 @@ rule build_cop_profiles: heat_pump_sink_T=config_provider("sector", "heat_pump_sink_T"), input: temp_soil_total=resources("temp_soil_total_elec_s{simpl}_{clusters}.nc"), - temp_soil_rural=resources("temp_soil_rural_elec_s{simpl}_{clusters}.nc"), - temp_soil_urban=resources("temp_soil_urban_elec_s{simpl}_{clusters}.nc"), temp_air_total=resources("temp_air_total_elec_s{simpl}_{clusters}.nc"), - temp_air_rural=resources("temp_air_rural_elec_s{simpl}_{clusters}.nc"), - temp_air_urban=resources("temp_air_urban_elec_s{simpl}_{clusters}.nc"), output: - cop_soil_total=resources("cop_soil_total_elec_s{simpl}_{clusters}.nc"), - cop_soil_rural=resources("cop_soil_rural_elec_s{simpl}_{clusters}.nc"), - cop_soil_urban=resources("cop_soil_urban_elec_s{simpl}_{clusters}.nc"), - cop_air_total=resources("cop_air_total_elec_s{simpl}_{clusters}.nc"), - cop_air_rural=resources("cop_air_rural_elec_s{simpl}_{clusters}.nc"), - cop_air_urban=resources("cop_air_urban_elec_s{simpl}_{clusters}.nc"), + cop_soil_individual_heating=resources("cop_soil_individual_heating_elec_s{simpl}_{clusters}.nc"), + cop_air_individual_heating=resources("cop_air_individual_heating_elec_s{simpl}_{clusters}.nc"), resources: mem_mb=20000, log: @@ -1029,12 +1021,8 @@ rule prepare_sector_network: temp_air_total=resources("temp_air_total_elec_s{simpl}_{clusters}.nc"), temp_air_rural=resources("temp_air_rural_elec_s{simpl}_{clusters}.nc"), temp_air_urban=resources("temp_air_urban_elec_s{simpl}_{clusters}.nc"), - cop_soil_total=resources("cop_soil_total_elec_s{simpl}_{clusters}.nc"), - cop_soil_rural=resources("cop_soil_rural_elec_s{simpl}_{clusters}.nc"), - cop_soil_urban=resources("cop_soil_urban_elec_s{simpl}_{clusters}.nc"), - cop_air_total=resources("cop_air_total_elec_s{simpl}_{clusters}.nc"), - cop_air_rural=resources("cop_air_rural_elec_s{simpl}_{clusters}.nc"), - cop_air_urban=resources("cop_air_urban_elec_s{simpl}_{clusters}.nc"), + cop_soil_individual_heating=resources("cop_soil_individual_heating_elec_s{simpl}_{clusters}.nc"), + cop_air_individual_heating=resources("cop_air_individual_heating_elec_s{simpl}_{clusters}.nc"), solar_thermal_total=lambda w: ( resources("solar_thermal_total_elec_s{simpl}_{clusters}.nc") if config_provider("sector", "solar_thermal")(w) diff --git a/scripts/build_cop_profiles.py b/scripts/build_cop_profiles.py index 2a47198b2..104737b85 100644 --- a/scripts/build_cop_profiles.py +++ b/scripts/build_cop_profiles.py @@ -67,12 +67,11 @@ def coefficient_of_performance(delta_T, source="air"): set_scenario_config(snakemake) - for area in ["total", "urban", "rural"]: - for source in ["air", "soil"]: - source_T = xr.open_dataarray(snakemake.input[f"temp_{source}_{area}"]) + for source in ["air", "soil"]: + source_T = xr.open_dataarray(snakemake.input[f"temp_{source}_total"]) - delta_T = snakemake.params.heat_pump_sink_T - source_T + delta_T = snakemake.params.heat_pump_sink_T - source_T - cop = coefficient_of_performance(delta_T, source) + cop = coefficient_of_performance(delta_T, source) - cop.to_netcdf(snakemake.output[f"cop_{source}_{area}"]) + cop.to_netcdf(snakemake.output[f"cop_{source}_individual_heating"]) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 53b811aa9..1967bf443 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1824,10 +1824,10 @@ def add_heat(n, costs): ] cop = { - "air": xr.open_dataarray(snakemake.input.cop_air_total) + "air": xr.open_dataarray(snakemake.input.cop_air_individual_heating) .to_pandas() .reindex(index=n.snapshots), - "ground": xr.open_dataarray(snakemake.input.cop_soil_total) + "ground": xr.open_dataarray(snakemake.input.cop_soil_individual_heating) .to_pandas() .reindex(index=n.snapshots), } From f3c898f43dd6a5a878bfc26c47344a2af7c1bc6f Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Fri, 19 Jul 2024 15:39:16 +0200 Subject: [PATCH 029/344] build district heating heat pump COPs using approximation from Jensen et al. and values from Pieper et al. --- config/config.default.yaml | 11 +- rules/build_sector.smk | 10 +- scripts/build_cop_profiles.py | 316 +++++++++++++++++++++++++++++++++- 3 files changed, 331 insertions(+), 6 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 0af347341..c9ee439b0 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -418,6 +418,15 @@ sector: 2045: 0.8 2050: 1.0 district_heating_loss: 0.15 + # check these numbers! + forward_temperature: 90 #C + return_temperature: 60 #C + heat_source_cooling: 6 #K + heat_pump_cop_approximation: + refrigerant: ammonia + heat_exchanger_pinch_point_temperature_difference: 5 #K + isentropic_compressor_efficiency: 0.8 + heat_loss: 0.0 cluster_heat_buses: true heat_demand_cutout: default bev_dsm_restriction_value: 0.75 @@ -500,7 +509,7 @@ sector: aviation_demand_factor: 1. HVC_demand_factor: 1. time_dep_hp_cop: true - heat_pump_sink_T: 55. + heat_pump_sink_T_individual_heating: 55. reduce_space_heat_exogenously: true reduce_space_heat_exogenously_factor: 2020: 0.10 # this results in a space heat demand reduction of 10% diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 9986bcebf..41c857b34 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -217,13 +217,19 @@ rule build_temperature_profiles: rule build_cop_profiles: params: - heat_pump_sink_T=config_provider("sector", "heat_pump_sink_T"), + heat_pump_sink_T_individual_heating=config_provider("sector", "heat_pump_sink_T_individual_heating"), + forward_temperature_district_heating=config_provider("sector", "district_heating", "forward_temperature"), + return_temperature_district_heating=config_provider("sector", "district_heating", "return_temperature"), + heat_source_cooling_district_heating=config_provider("sector", "district_heating", "heat_source_cooling"), + heat_pump_cop_approximation=config_provider("sector", "district_heating", "heat_pump_cop_approximation"), input: temp_soil_total=resources("temp_soil_total_elec_s{simpl}_{clusters}.nc"), temp_air_total=resources("temp_air_total_elec_s{simpl}_{clusters}.nc"), output: cop_soil_individual_heating=resources("cop_soil_individual_heating_elec_s{simpl}_{clusters}.nc"), cop_air_individual_heating=resources("cop_air_individual_heating_elec_s{simpl}_{clusters}.nc"), + cop_air_district_heating=resources("cop_air_district_heating_elec_s{simpl}_{clusters}.nc"), + cop_soil_district_heating=resources("cop_soil_district_heating_elec_s{simpl}_{clusters}.nc"), resources: mem_mb=20000, log: @@ -1023,6 +1029,8 @@ rule prepare_sector_network: temp_air_urban=resources("temp_air_urban_elec_s{simpl}_{clusters}.nc"), cop_soil_individual_heating=resources("cop_soil_individual_heating_elec_s{simpl}_{clusters}.nc"), cop_air_individual_heating=resources("cop_air_individual_heating_elec_s{simpl}_{clusters}.nc"), + cop_air_district_heating=resources("cop_air_district_heating_elec_s{simpl}_{clusters}.nc"), + cop_soil_district_heating=resources("cop_soil_district_heating_elec_s{simpl}_{clusters}.nc"), solar_thermal_total=lambda w: ( resources("solar_thermal_total_elec_s{simpl}_{clusters}.nc") if config_provider("sector", "solar_thermal")(w) diff --git a/scripts/build_cop_profiles.py b/scripts/build_cop_profiles.py index 104737b85..6d7050baf 100644 --- a/scripts/build_cop_profiles.py +++ b/scripts/build_cop_profiles.py @@ -42,11 +42,14 @@ [1] Staffell et al., Energy & Environmental Science 11 (2012): A review of domestic heat pumps, https://doi.org/10.1039/C2EE22653G. """ +from typing import Union +from enum import Enum import xarray as xr +import numpy as np from _helpers import set_scenario_config -def coefficient_of_performance(delta_T, source="air"): +def coefficient_of_performance_individual_heating(delta_T, source="air"): if source == "air": return 6.81 - 0.121 * delta_T + 0.000630 * delta_T**2 elif source == "soil": @@ -55,6 +58,301 @@ def coefficient_of_performance(delta_T, source="air"): raise NotImplementedError("'source' must be one of ['air', 'soil']") +def celsius_to_kelvin(t_celsius: Union[float, xr.DataArray, np.array]) -> Union[float, xr.DataArray, np.array]: + if (np.asarray(t_celsius) > 200).any(): + raise ValueError("t_celsius > 200. Are you sure you are using the right units?") + return t_celsius + 273.15 + + +def logarithmic_mean(t_hot: Union[float, xr.DataArray, np.ndarray], t_cold: Union[float, xr.DataArray, np.ndarray]) -> Union[float, xr.DataArray, np.ndarray]: + if (np.asarray(t_hot <= t_cold)).any(): + raise ValueError("t_hot must be greater than t_cold") + return (t_hot - t_cold) / np.log(t_hot / t_cold) + +class CopDistrictHeating: + + def __init__( + self, + forward_temperature_celsius: Union[xr.DataArray, np.array], + source_inlet_temperature_celsius: Union[xr.DataArray, np.array], + return_temperature_celsius: Union[xr.DataArray, np.array], + source_outlet_temperature_celsius: Union[xr.DataArray, np.array], + delta_t_pinch_point: float = 5, + isentropic_compressor_efficiency: float = 0.8, + heat_loss: float = 0.0, + ) -> None: + """ + Initialize the COPProfileBuilder object. + + Parameters: + ---------- + forward_temperature_celsius : Union[xr.DataArray, np.array] + The forward temperature in Celsius. + return_temperature_celsius : Union[xr.DataArray, np.array] + The return temperature in Celsius. + source_inlet_temperature_celsius : Union[xr.DataArray, np.array] + The source inlet temperature in Celsius. + source_outlet_temperature_celsius : Union[xr.DataArray, np.array] + The source outlet temperature in Celsius. + delta_t_pinch_point : float, optional + The pinch point temperature difference, by default 5. + isentropic_compressor_efficiency : float, optional + The isentropic compressor efficiency, by default 0.8. + heat_loss : float, optional + The heat loss, by default 0.0. + """ + self.t_source_in = celsius_to_kelvin(source_inlet_temperature_celsius) + self.t_sink_out = celsius_to_kelvin(forward_temperature_celsius) + + self.t_sink_in = celsius_to_kelvin(return_temperature_celsius) + self.t_source_out = celsius_to_kelvin(source_outlet_temperature_celsius) + + self.isentropic_efficiency_compressor = isentropic_compressor_efficiency + self.heat_loss = heat_loss + self.delta_t_pinch = delta_t_pinch_point + + def cop(self) -> Union[xr.DataArray, np.array]: + """ + Calculate the coefficient of performance (COP) for the system. + + Returns: + Union[xr.DataArray, np.array]: The calculated COP values. + """ + return ( + self.ideal_lorenz_cop + * ( + ( + 1 + + (self.delta_t_refrigerant_sink + self.delta_t_pinch) + / self.t_sink_mean + ) + / ( + 1 + + ( + self.delta_t_refrigerant_sink + + self.delta_t_refrigerant_source + + 2 * self.delta_t_pinch + ) + / self.delta_t_lift + ) + ) + * self.isentropic_efficiency_compressor + * (1 - self.ratio_evaporation_compression_work) + + 1 + - self.isentropic_efficiency_compressor + - self.heat_loss + ) + + @property + def t_sink_mean(self) -> Union[xr.DataArray, np.array]: + """ + Calculate the logarithmic mean temperature difference between the cold and hot sinks. + + Returns + ------- + Union[xr.DataArray, np.array] + The mean temperature difference. + """ + return logarithmic_mean(t_cold=self.t_sink_in, t_hot=self.t_sink_out) + + @property + def t_source_mean(self) -> Union[xr.DataArray, np.array]: + """ + Calculate the logarithmic mean temperature of the heat source. + + Returns + ------- + Union[xr.DataArray, np.array] + The mean temperature of the heat source. + """ + return logarithmic_mean(t_hot=self.t_source_in, t_cold=self.t_source_out) + + @property + def delta_t_lift(self) -> Union[xr.DataArray, np.array]: + """ + Calculate the temperature lift as the difference between the logarithmic sink and source temperatures. + + Returns + ------- + Union[xr.DataArray, np.array] + The temperature difference between the sink and source. + """ + return self.t_sink_mean - self.t_source_mean + + @property + def ideal_lorenz_cop(self) -> Union[xr.DataArray, np.array]: + """ + Ideal Lorenz coefficient of performance (COP). + + The ideal Lorenz COP is calculated as the ratio of the mean sink temperature + to the lift temperature difference. + + Returns + ------- + np.array + The ideal Lorenz COP. + + """ + return self.t_sink_mean / self.delta_t_lift + + @property + def delta_t_refrigerant_source(self) -> Union[xr.DataArray, np.array]: + """ + Calculate the temperature difference between the refrigerant source inlet and outlet. + + Returns + ------- + Union[xr.DataArray, np.array] + The temperature difference between the refrigerant source inlet and outlet. + """ + return self._approximate_delta_t_refrigerant_source( + delta_t_source=self.t_source_in - self.t_source_out + ) + + @property + def delta_t_refrigerant_sink(self) -> Union[xr.DataArray, np.array]: + """ + Temperature difference between the refrigerant and the sink based on approximation. + + Returns + ------- + Union[xr.DataArray, np.array] + The temperature difference between the refrigerant and the sink. + """ + return self._approximate_delta_t_refrigerant_sink() + + @property + def ratio_evaporation_compression_work(self) -> Union[xr.DataArray, np.array]: + """ + Calculate the ratio of evaporation to compression work based on approximation. + + Returns + ------- + Union[xr.DataArray, np.array] + The calculated ratio of evaporation to compression work. + """ + return self._ratio_evaporation_compression_work_approximation() + + @property + def delta_t_sink(self) -> Union[xr.DataArray, np.array]: + """ + Calculate the temperature difference at the sink. + + Returns + ------- + Union[xr.DataArray, np.array] + The temperature difference at the sink. + """ + return self.t_sink_out - self.t_sink_in + + def _approximate_delta_t_refrigerant_source( + self, delta_t_source: Union[xr.DataArray, np.array] + ) -> Union[xr.DataArray, np.array]: + """ + Approximates the temperature difference between the refrigerant and the source. + + Parameters + ---------- + delta_t_source : Union[xr.DataArray, np.array] + The temperature difference for the refrigerant source. + + Returns + ------- + Union[xr.DataArray, np.array] + The approximate temperature difference for the refrigerant source. + """ + return delta_t_source / 2 + + def _approximate_delta_t_refrigerant_sink( + self, + refrigerant: str = "ammonia", + a: float = {"ammonia": 0.2, "isobutane": -0.0011}, + b: float = {"ammonia": 0.2, "isobutane": 0.3}, + c: float = {"ammonia": 0.016, "isobutane": 2.4}, + ) -> Union[xr.DataArray, np.array]: + """ + Approximates the temperature difference at the refrigerant sink. + + Parameters: + ---------- + refrigerant : str, optional + The refrigerant used in the system. Either 'isobutane' or 'ammonia. Default is 'ammonia'. + a : float, optional + Coefficient for the temperature difference between the sink and source, default is 0.2. + b : float, optional + Coefficient for the temperature difference at the sink, default is 0.2. + c : float, optional + Constant term, default is 0.016. + + Returns: + ------- + Union[xr.DataArray, np.array] + The approximate temperature difference at the refrigerant sink. + + Notes: + ------ + This function assumes ammonia as the refrigerant. + + The approximate temperature difference at the refrigerant sink is calculated using the following formula: + a * (t_sink_out - t_source_out + 2 * delta_t_pinch) + b * delta_t_sink + c + + """ + if refrigerant not in a.keys(): + raise ValueError( + f"Invalid refrigerant '{refrigerant}'. Must be one of {a.keys()}" + ) + return ( + a[refrigerant] + * (self.t_sink_out - self.t_source_out + 2 * self.delta_t_pinch) + + b[refrigerant] * self.delta_t_sink + + c[refrigerant] + ) + + def _ratio_evaporation_compression_work_approximation( + self, + refrigerant: str = "ammonia", + a: float = {"ammonia": 0.0014, "isobutane": 0.0035}, + b: float = {"ammonia": -0.0015, "isobutane": -0.0033}, + c: float = {"ammonia": 0.039, "isobutane": 0.053}, + ) -> Union[xr.DataArray, np.array]: + """ + Calculate the ratio of evaporation to compression work approximation. + + Parameters: + ---------- + refrigerant : str, optional + The refrigerant used in the system. Either 'isobutane' or 'ammonia. Default is 'ammonia'. + a : float, optional + Coefficient 'a' in the approximation equation. Default is 0.0014. + b : float, optional + Coefficient 'b' in the approximation equation. Default is -0.0015. + c : float, optional + Coefficient 'c' in the approximation equation. Default is 0.039. + + Returns: + ------- + Union[xr.DataArray, np.array] + The calculated ratio of evaporation to compression work. + + Notes: + ------ + This function assumes ammonia as the refrigerant. + + The approximation equation used is: + ratio = a * (t_sink_out - t_source_out + 2 * delta_t_pinch) + b * delta_t_sink + c + """ + if refrigerant not in a.keys(): + raise ValueError( + f"Invalid refrigerant '{refrigerant}'. Must be one of {a.keys()}" + ) + return ( + a[refrigerant] + * (self.t_sink_out - self.t_source_out + 2 * self.delta_t_pinch) + + b[refrigerant] * self.delta_t_sink + + c[refrigerant] + ) + + if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake @@ -70,8 +368,18 @@ def coefficient_of_performance(delta_T, source="air"): for source in ["air", "soil"]: source_T = xr.open_dataarray(snakemake.input[f"temp_{source}_total"]) - delta_T = snakemake.params.heat_pump_sink_T - source_T + delta_T = snakemake.params.heat_pump_sink_T_individual_heating - source_T + + cop_individual_heating = coefficient_of_performance_individual_heating(delta_T, source) + cop_individual_heating.to_netcdf(snakemake.output[f"cop_{source}_individual_heating"]) - cop = coefficient_of_performance(delta_T, source) + cop_district_heating = CopDistrictHeating( + forward_temperature_celsius=snakemake.params.forward_temperature_district_heating, + return_temperature_celsius=snakemake.params.return_temperature_district_heating, + source_inlet_temperature_celsius=source_T, + source_outlet_temperature_celsius=source_T - snakemake.params.heat_source_cooling_district_heating, + ).cop() - cop.to_netcdf(snakemake.output[f"cop_{source}_individual_heating"]) + cop_district_heating.to_netcdf(snakemake.output[f"cop_{source}_district_heating"]) + + From 39e853e2b363427a52b3dc056c335a1365ebd1a7 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Fri, 19 Jul 2024 15:34:44 +0200 Subject: [PATCH 030/344] make sure not to divide by zero --- scripts/build_energy_totals.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index 70ed4bec6..4e353c11b 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -847,7 +847,7 @@ def build_district_heat_share(countries: List[str], idees: pd.DataFrame) -> pd.S ) total_heat = idees[["thermal uses residential", "thermal uses services"]].sum( axis=1 - ) + ).replace(0, np.nan) district_heat_share = district_heat / total_heat @@ -866,6 +866,8 @@ def build_district_heat_share(countries: List[str], idees: pd.DataFrame) -> pd.S district_heat_share = pd.concat( [district_heat_share, dh_share.reindex(new_index, level=0)], axis=1 ).min(axis=1) + + district_heat_share = district_heat_share.reindex(countries, level=0) district_heat_share.name = "district heat share" From 0e1e50900635af5626e1412fa1c314045696ee4a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 19 Jul 2024 13:43:14 +0000 Subject: [PATCH 031/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/build_energy_totals.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index 4e353c11b..9491cc57c 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -845,9 +845,11 @@ def build_district_heat_share(countries: List[str], idees: pd.DataFrame) -> pd.S district_heat = idees[["derived heat residential", "derived heat services"]].sum( axis=1 ) - total_heat = idees[["thermal uses residential", "thermal uses services"]].sum( - axis=1 - ).replace(0, np.nan) + total_heat = ( + idees[["thermal uses residential", "thermal uses services"]] + .sum(axis=1) + .replace(0, np.nan) + ) district_heat_share = district_heat / total_heat @@ -866,7 +868,7 @@ def build_district_heat_share(countries: List[str], idees: pd.DataFrame) -> pd.S district_heat_share = pd.concat( [district_heat_share, dh_share.reindex(new_index, level=0)], axis=1 ).min(axis=1) - + district_heat_share = district_heat_share.reindex(countries, level=0) district_heat_share.name = "district heat share" From 052394fa88dc550b781f21fd020219f69b0e081d Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Fri, 19 Jul 2024 15:57:53 +0200 Subject: [PATCH 032/344] change naming from individual/district heating to denctral/central heating --- rules/build_sector.smk | 26 +++++++++++++------------- scripts/build_cop_profiles.py | 4 ++-- scripts/prepare_sector_network.py | 4 ++-- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 41c857b34..995eb9e2a 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -217,19 +217,19 @@ rule build_temperature_profiles: rule build_cop_profiles: params: - heat_pump_sink_T_individual_heating=config_provider("sector", "heat_pump_sink_T_individual_heating"), - forward_temperature_district_heating=config_provider("sector", "district_heating", "forward_temperature"), - return_temperature_district_heating=config_provider("sector", "district_heating", "return_temperature"), - heat_source_cooling_district_heating=config_provider("sector", "district_heating", "heat_source_cooling"), - heat_pump_cop_approximation=config_provider("sector", "district_heating", "heat_pump_cop_approximation"), + heat_pump_sink_T_decentral_heating=config_provider("sector", "heat_pump_sink_T_individual_heating"), + forward_temperature_central_heating=config_provider("sector", "district_heating", "forward_temperature"), + return_temperature_central_heating=config_provider("sector", "district_heating", "return_temperature"), + heat_source_cooling_central_heating=config_provider("sector", "district_heating", "heat_source_cooling"), + heat_pump_cop_approximation_central_heating=config_provider("sector", "district_heating", "heat_pump_cop_approximation"), input: temp_soil_total=resources("temp_soil_total_elec_s{simpl}_{clusters}.nc"), temp_air_total=resources("temp_air_total_elec_s{simpl}_{clusters}.nc"), output: - cop_soil_individual_heating=resources("cop_soil_individual_heating_elec_s{simpl}_{clusters}.nc"), - cop_air_individual_heating=resources("cop_air_individual_heating_elec_s{simpl}_{clusters}.nc"), - cop_air_district_heating=resources("cop_air_district_heating_elec_s{simpl}_{clusters}.nc"), - cop_soil_district_heating=resources("cop_soil_district_heating_elec_s{simpl}_{clusters}.nc"), + cop_air_decentral_heating=resources("cop_air_decentral_elec_s{simpl}_{clusters}.nc"), + cop_soil_decentral_heating=resources("cop_soil_decentral_elec_s{simpl}_{clusters}.nc"), + cop_air_central_heating=resources("cop_air_central_heating_elec_s{simpl}_{clusters}.nc"), + cop_soil_central_heating=resources("cop_soil_central_heating_elec_s{simpl}_{clusters}.nc"), resources: mem_mb=20000, log: @@ -1027,10 +1027,10 @@ rule prepare_sector_network: temp_air_total=resources("temp_air_total_elec_s{simpl}_{clusters}.nc"), temp_air_rural=resources("temp_air_rural_elec_s{simpl}_{clusters}.nc"), temp_air_urban=resources("temp_air_urban_elec_s{simpl}_{clusters}.nc"), - cop_soil_individual_heating=resources("cop_soil_individual_heating_elec_s{simpl}_{clusters}.nc"), - cop_air_individual_heating=resources("cop_air_individual_heating_elec_s{simpl}_{clusters}.nc"), - cop_air_district_heating=resources("cop_air_district_heating_elec_s{simpl}_{clusters}.nc"), - cop_soil_district_heating=resources("cop_soil_district_heating_elec_s{simpl}_{clusters}.nc"), + cop_soil_decentral_heating=resources("cop_soil_decentral_heating_elec_s{simpl}_{clusters}.nc"), + cop_air_decentral_heating=resources("cop_air_decentral_heating_elec_s{simpl}_{clusters}.nc"), + cop_air_central_heating=resources("cop_air_central_heating_elec_s{simpl}_{clusters}.nc"), + cop_soil_central_heating=resources("cop_soil_central_heating_elec_s{simpl}_{clusters}.nc"), solar_thermal_total=lambda w: ( resources("solar_thermal_total_elec_s{simpl}_{clusters}.nc") if config_provider("sector", "solar_thermal")(w) diff --git a/scripts/build_cop_profiles.py b/scripts/build_cop_profiles.py index 6d7050baf..15c796509 100644 --- a/scripts/build_cop_profiles.py +++ b/scripts/build_cop_profiles.py @@ -371,7 +371,7 @@ def _ratio_evaporation_compression_work_approximation( delta_T = snakemake.params.heat_pump_sink_T_individual_heating - source_T cop_individual_heating = coefficient_of_performance_individual_heating(delta_T, source) - cop_individual_heating.to_netcdf(snakemake.output[f"cop_{source}_individual_heating"]) + cop_individual_heating.to_netcdf(snakemake.output[f"cop_{source}_decentral_heating"]) cop_district_heating = CopDistrictHeating( forward_temperature_celsius=snakemake.params.forward_temperature_district_heating, @@ -380,6 +380,6 @@ def _ratio_evaporation_compression_work_approximation( source_outlet_temperature_celsius=source_T - snakemake.params.heat_source_cooling_district_heating, ).cop() - cop_district_heating.to_netcdf(snakemake.output[f"cop_{source}_district_heating"]) + cop_district_heating.to_netcdf(snakemake.output[f"cop_{source}_central_heating"]) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 1967bf443..4cec75304 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1824,10 +1824,10 @@ def add_heat(n, costs): ] cop = { - "air": xr.open_dataarray(snakemake.input.cop_air_individual_heating) + "air": xr.open_dataarray(snakemake.input.cop_air_decentral_heating) .to_pandas() .reindex(index=n.snapshots), - "ground": xr.open_dataarray(snakemake.input.cop_soil_individual_heating) + "ground": xr.open_dataarray(snakemake.input.cop_soil_decentral_heating) .to_pandas() .reindex(index=n.snapshots), } From 71efc8f12a76dddcbaa04cdd685f81fe0ed7205f Mon Sep 17 00:00:00 2001 From: Fabian Hofmann Date: Fri, 19 Jul 2024 17:49:41 +0200 Subject: [PATCH 033/344] draft bot for automated fixed env yaml (#1049) * draft bot for automated fixed env yaml * Update .github/workflows/update-fixed-env.yaml Co-authored-by: Fabian Neumann --------- Co-authored-by: Fabian Neumann --- .github/workflows/update-fixed-env.yaml | 39 +++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/update-fixed-env.yaml diff --git a/.github/workflows/update-fixed-env.yaml b/.github/workflows/update-fixed-env.yaml new file mode 100644 index 000000000..f66890aa1 --- /dev/null +++ b/.github/workflows/update-fixed-env.yaml @@ -0,0 +1,39 @@ +name: Fixed Environment YAML Monitor + +on: + push: + branches: + - master + paths: + - 'env/environment.yaml' + +jobs: + update_environment_fixed: + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup micromamba + uses: mamba-org/setup-micromamba@v1 + with: + micromamba-version: latest + environment-file: envs/environment.yaml + log-level: debug + init-shell: bash + cache-environment: true + cache-downloads: true + + - name: Update environment.fixed.yaml + run: | + mamba env export --file envs/environment.fixed.yaml --no-builds + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + branch: update-environment-fixed + title: Update fixed environment + body: Automatically generated PR to update environment.fixed.yaml + labels: automated From 0c7e7cb46b5806879ebd480647b61af2de2824ea Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Fri, 19 Jul 2024 18:54:19 +0200 Subject: [PATCH 034/344] fix naming, udpate docs --- rules/build_sector.smk | 12 ++++++------ scripts/build_cop_profiles.py | 22 +++++++++------------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 995eb9e2a..8b4608600 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -218,16 +218,16 @@ rule build_temperature_profiles: rule build_cop_profiles: params: heat_pump_sink_T_decentral_heating=config_provider("sector", "heat_pump_sink_T_individual_heating"), - forward_temperature_central_heating=config_provider("sector", "district_heating", "forward_temperature"), - return_temperature_central_heating=config_provider("sector", "district_heating", "return_temperature"), - heat_source_cooling_central_heating=config_provider("sector", "district_heating", "heat_source_cooling"), - heat_pump_cop_approximation_central_heating=config_provider("sector", "district_heating", "heat_pump_cop_approximation"), + forward_temperature_district_heating=config_provider("sector", "district_heating", "forward_temperature"), + return_temperature_district_heating=config_provider("sector", "district_heating", "return_temperature"), + heat_source_cooling_district_heating=config_provider("sector", "district_heating", "heat_source_cooling"), + heat_pump_cop_approximation_district_heating=config_provider("sector", "district_heating", "heat_pump_cop_approximation"), input: temp_soil_total=resources("temp_soil_total_elec_s{simpl}_{clusters}.nc"), temp_air_total=resources("temp_air_total_elec_s{simpl}_{clusters}.nc"), output: - cop_air_decentral_heating=resources("cop_air_decentral_elec_s{simpl}_{clusters}.nc"), - cop_soil_decentral_heating=resources("cop_soil_decentral_elec_s{simpl}_{clusters}.nc"), + cop_air_decentral_heating=resources("cop_air_decentral_heating_elec_s{simpl}_{clusters}.nc"), + cop_soil_decentral_heating=resources("cop_soil_decentral_heating_elec_s{simpl}_{clusters}.nc"), cop_air_central_heating=resources("cop_air_central_heating_elec_s{simpl}_{clusters}.nc"), cop_soil_central_heating=resources("cop_soil_central_heating_elec_s{simpl}_{clusters}.nc"), resources: diff --git a/scripts/build_cop_profiles.py b/scripts/build_cop_profiles.py index 15c796509..77caa1a6b 100644 --- a/scripts/build_cop_profiles.py +++ b/scripts/build_cop_profiles.py @@ -6,8 +6,8 @@ Build coefficient of performance (COP) time series for air- or ground-sourced heat pumps. -The COP is approximated as a quatratic function of the temperature difference between source and -sink, based on Staffell et al. 2012. +For individual (decentral) heat pumps, the COP is approximated as a quatratic function of the temperature difference between source and sink, based on Staffell et al. 2012. +For district (central) heating, the COP is approximated based on Jensen et al. 2018 and parameters from Pieper et al. 2020. This rule is executed in ``build_sector.smk``. @@ -21,25 +21,21 @@ Inputs: ------- - ``resources//temp_soil_total_elec_s_.nc``: Soil temperature (total) time series. -- ``resources//temp_soil_rural_elec_s_.nc``: Soil temperature (rural) time series. -- ``resources//temp_soil_urban_elec_s_.nc``: Soil temperature (urban) time series. - ``resources//temp_air_total_elec_s_.nc``: Ambient air temperature (total) time series. -- ``resources//temp_air_rural_elec_s_.nc``: Ambient air temperature (rural) time series. -- ``resources//temp_air_urban_elec_s_.nc``: Ambient air temperature (urban) time series. Outputs: -------- -- ``resources/cop_soil_total_elec_s_.nc``: COP (ground-sourced) time series (total). -- ``resources/cop_soil_rural_elec_s_.nc``: COP (ground-sourced) time series (rural). -- ``resources/cop_soil_urban_elec_s_.nc``: COP (ground-sourced) time series (urban). -- ``resources/cop_air_total_elec_s_.nc``: COP (air-sourced) time series (total). -- ``resources/cop_air_rural_elec_s_.nc``: COP (air-sourced) time series (rural). -- ``resources/cop_air_urban_elec_s_.nc``: COP (air-sourced) time series (urban). +- ``resources/cop_air_decentral_heating_elec_s_.nc``: COP (air-sourced) time series (decentral heating). +- ``resources/cop_soil_decentral_heating_elec_s_.nc``: COP (ground-sourced) time series (decentral heating). +- ``resources/cop_air_central_heating_elec_s_.nc``: COP (air-sourced) time series (central heating). +- ``resources/cop_soil_central_heating_elec_s_.nc``: COP (ground-sourced) time series (central heating). References ---------- [1] Staffell et al., Energy & Environmental Science 11 (2012): A review of domestic heat pumps, https://doi.org/10.1039/C2EE22653G. +[2] Jensen et al., Proceedings of the13th IIR-Gustav Lorentzen Conference on Natural Refrigerants (2018): Heat pump COP, part 2: Generalized COP estimation of heat pump processes, https://doi.org/10.18462/iir.gl.2018.1386 +[3] Pieper et al., Energy 205 (2020): Comparison of COP estimation methods for large-scale heat pumps used in energy planning, https://doi.org/10.1016/j.energy.2020.117994 """ from typing import Union @@ -368,7 +364,7 @@ def _ratio_evaporation_compression_work_approximation( for source in ["air", "soil"]: source_T = xr.open_dataarray(snakemake.input[f"temp_{source}_total"]) - delta_T = snakemake.params.heat_pump_sink_T_individual_heating - source_T + delta_T = snakemake.params.heat_pump_sink_T_decentral_heating - source_T cop_individual_heating = coefficient_of_performance_individual_heating(delta_T, source) cop_individual_heating.to_netcdf(snakemake.output[f"cop_{source}_decentral_heating"]) From 560373a864e1695806a8556a0e726cfd4de4a13a Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Fri, 19 Jul 2024 18:54:30 +0200 Subject: [PATCH 035/344] add DH cops to network --- scripts/prepare_sector_network.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 4cec75304..66b5085da 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1824,10 +1824,16 @@ def add_heat(n, costs): ] cop = { - "air": xr.open_dataarray(snakemake.input.cop_air_decentral_heating) + "air decentral": xr.open_dataarray(snakemake.input.cop_air_decentral_heating) .to_pandas() .reindex(index=n.snapshots), - "ground": xr.open_dataarray(snakemake.input.cop_soil_decentral_heating) + "ground decentral": xr.open_dataarray(snakemake.input.cop_soil_decentral_heating) + .to_pandas() + .reindex(index=n.snapshots), + "air central": xr.open_dataarray(snakemake.input.cop_air_central_heating) + .to_pandas() + .reindex(index=n.snapshots), + "ground central": xr.open_dataarray(snakemake.input.cop_soil_central_heating) .to_pandas() .reindex(index=n.snapshots), } @@ -1922,7 +1928,7 @@ def add_heat(n, costs): for heat_pump_type in heat_pump_types: costs_name = f"{name_type} {heat_pump_type}-sourced heat pump" efficiency = ( - cop[heat_pump_type][nodes] + cop[f"{heat_pump_type} {name_type}"][nodes] if options["time_dep_hp_cop"] else costs.at[costs_name, "efficiency"] ) From ba55971c23036bc53ada676ceb64eb83c12ac6e8 Mon Sep 17 00:00:00 2001 From: Iegor Riepin Date: Fri, 19 Jul 2024 19:23:35 +0200 Subject: [PATCH 036/344] Compatibility of data processing for Ukraine (#1146) * data: retrieve gdp and pop raw data for UA,MD * Updated retrieval code. * add_electricity script for UA and MD using updated, endogenised GDP and PPP data. * Outsourced building of gdp and ppp. Only called when UA and/or MD are in country list. * Accepted suggestion by @fneum in retrieve rule. * Cleaned determine_availability_matrix_MD_UA.py, removed redundant code * Updated build_gdp_pop_non_nuts3 script to use the mean to distribute GDP p.c. Added release notes. Bug fixes. * Bug fixes --> 'ppp' to 'pop' * Bug fix: only distribute load to buses with substation. * Updated to download GDP and population raster via PyPSA-Eur zenodo databundle. Removal of dedicated retrieve function. Updated release notes. * Updated release notes. * correct input file paths * update zenodo url for databundle release v0.3.0 * doc: update release_notes * minor adjustments: benchmark, increase mem_mb, remove plots, handling of optional inputs --------- Co-authored-by: bobbyxng Co-authored-by: Bobby Xiong <36541459+bobbyxng@users.noreply.github.com> Co-authored-by: Fabian Neumann --- data/GDP_PPP_30arcsec_v3_mapped_default.csv | 151 ----------------- doc/release_notes.rst | 6 + rules/build_electricity.smk | 33 +++- rules/retrieve.smk | 4 +- scripts/add_electricity.py | 26 +-- scripts/build_gdp_pop_non_nuts3.py | 152 ++++++++++++++++++ .../determine_availability_matrix_MD_UA.py | 6 +- scripts/retrieve_databundle.py | 2 +- 8 files changed, 214 insertions(+), 166 deletions(-) delete mode 100644 data/GDP_PPP_30arcsec_v3_mapped_default.csv create mode 100644 scripts/build_gdp_pop_non_nuts3.py diff --git a/data/GDP_PPP_30arcsec_v3_mapped_default.csv b/data/GDP_PPP_30arcsec_v3_mapped_default.csv deleted file mode 100644 index f0e640b36..000000000 --- a/data/GDP_PPP_30arcsec_v3_mapped_default.csv +++ /dev/null @@ -1,151 +0,0 @@ -name,GDP_PPP,country -3140,632728.0438507323,MD -3139,806541.9318093687,MD -3142,1392454.6690911907,MD -3152,897871.2903553953,MD -3246,645554.8588933202,MD -7049,1150156.4449477682,MD -1924,162285.16792916053,UA -1970,751970.6071848695,UA -2974,368873.75840156944,UA -2977,294847.85539198935,UA -2979,197988.13680768458,UA -2980,301371.2491126519,UA -3031,56925.21878805953,UA -3032,139395.18279351242,UA -3033,145377.8061037629,UA -3035,52282.83655208812,UA -3036,497950.25890516065,UA -3037,1183293.1987702171,UA -3038,255005.98207636533,UA -3039,224711.50098325178,UA -3040,342959.943226467,UA -3044,69119.31486955672,UA -3045,246273.65986119965,UA -3047,146742.08407299497,UA -3049,107265.7028733467,UA -3050,1126147.985259493,UA -3051,69833.56303043803,UA -3052,67230.88206577855,UA -3053,27019.224685201345,UA -3054,260571.47337292184,UA -3055,88760.94152915622,UA -3056,101368.26196568517,UA -3058,55752.92329667119,UA -3059,89024.37880630122,UA -3062,358411.291265149,UA -3064,75081.64142862396,UA -3065,158101.42949135564,UA -3066,83763.89576442329,UA -3068,173474.51218344545,UA -3069,60327.01572375589,UA -3070,18073.687271955278,UA -3071,249069.43314695224,UA -3072,220707.35700825177,UA -3073,61342.30137462664,UA -3074,254235.98867635374,UA -3077,769558.9832370486,UA -3078,132674.2315809836,UA -3079,1388517.1478032232,UA -3080,1861003.8718246964,UA -3082,140123.73854745473,UA -3083,834887.5595419679,UA -3084,1910795.5590558557,UA -3086,93828.36549170096,UA -3088,347197.65113392205,UA -3089,3754718.141734592,UA -3090,521912.69768585655,UA -3093,232818.05269714879,UA -3095,435376.20361377904,UA -3099,345596.5288937008,UA -3100,175689.10947424968,UA -3105,538438.9311459162,UA -3107,88096.86032871014,UA -3108,79847.68447063807,UA -3109,348504.73449373,UA -3144,71657.0165675802,UA -3146,80342.05037424155,UA -3158,74465.12922576343,UA -3164,3102112.2672631275,UA -3165,65215.04081671433,UA -3166,413924.2225725632,UA -3167,135060.0056434935,UA -3168,54980.442979330146,UA -3170,29584.879122227037,UA -3171,142780.68163047134,UA -3172,40436.63814695243,UA -3173,1253342.1790126422,UA -3174,173842.03139155387,UA -3176,65699.76352408895,UA -3177,143591.75419817626,UA -3178,56434.04525832523,UA -3179,389996.1670051216,UA -3180,138452.84503524794,UA -3181,67402.59500436619,UA -3184,51204.293695376415,UA -3185,46867.82356528432,UA -3186,103892.35612417295,UA -3187,193668.91476930346,UA -3189,54584.176457692694,UA -3190,219077.64942830536,UA -3197,88516.52699983507,UA -3198,298166.8272673622,UA -3199,61334.952541812374,UA -3229,175692.61136747137,UA -3230,106722.62773321665,UA -3236,61542.06264321315,UA -3241,83752.90489164277,UA -4301,48419.52825967164,UA -4305,147759.74280349456,UA -4306,53156.905740992224,UA -4315,218025.78516351627,UA -4317,155240.40554731718,UA -4318,1342144.2459407183,UA -4319,91669.1449633853,UA -4321,85852.49282415409,UA -4347,67938.7698430624,UA -4357,20064.979012172935,UA -4360,47840.51245168512,UA -4361,55580.924388032574,UA -4362,165753.82588729708,UA -4363,46390.2448142152,UA -4365,96265.47592938849,UA -4366,272003.25510057947,UA -4367,80878.50229245829,UA -4370,330072.35444044066,UA -4371,7707066.181975477,UA -4373,2019766.7891575783,UA -4374,985354.331818515,UA -4377,230805.08833664874,UA -4382,125670.67125287943,UA -4383,46914.065511740075,UA -4384,48020.804310510954,UA -4385,55612.34707641123,UA -4387,74558.3475791577,UA -4388,245243.33449409154,UA -4389,95696.56767732685,UA -4391,251085.7523045193,UA -4401,66375.82996856027,UA -4403,111954.41038437477,UA -4405,46911.68560148837,UA -4408,150782.51691456966,UA -4409,112776.7399582134,UA -4410,153076.56860965435,UA -4412,192629.31238456024,UA -4413,181295.3120834606,UA -4414,995694.9413199169,UA -4416,157640.7868989174,UA -4418,77580.20674809469,UA -4420,122320.99275223716,UA -4424,184891.10924920067,UA -4425,84486.75974340564,UA -4431,50485.84380961137,UA -4435,231040.45446464577,UA -4436,81222.18707585508,UA -4438,114819.76472988473,UA -4439,76839.1052178896,UA -4440,135337.0313562152,UA -4441,49159.485269198034,UA -7031,42001.73757065917,UA -7059,159790.48382874,UA -7063,39599.10564971086,UA diff --git a/doc/release_notes.rst b/doc/release_notes.rst index fc8815cd6..2cf0fe709 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -31,6 +31,12 @@ Upcoming Release * Bugfix: Correctly read in threshold capacity below which to remove components from previous planning horizons in :mod:`add_brownfield`. +* For countries not contained in the NUTS3-specific datasets (i.e. MD and UA), the mapping of GDP per capita and population per bus region used to spatially distribute electricity demand is now endogenised in a new rule :mod:`build_gdp_ppp_non_nuts3`. https://github.com/PyPSA/pypsa-eur/pull/1146 + +* The databundle has been updated to release v0.3.0, which includes raw GDP and population data for countries outside the NUTS system (UA, MD). https://github.com/PyPSA/pypsa-eur/pull/1146 + +* Updated filtering in :mod:`determine_availability_matrix_MD_UA.py` to improve speed. https://github.com/PyPSA/pypsa-eur/pull/1146 + * Bugfix: Impose minimum value of zero for district heating progress between current and future market share in :mod:`build_district_heat_share`. PyPSA-Eur 0.11.0 (25th May 2024) diff --git a/rules/build_electricity.smk b/rules/build_electricity.smk index eff0d9e09..a9ab71ef2 100644 --- a/rules/build_electricity.smk +++ b/rules/build_electricity.smk @@ -375,6 +375,37 @@ def input_conventional(w): } +# Optional input when having Ukraine (UA) or Moldova (MD) in the countries list +def input_gdp_pop_non_nuts3(w): + countries = set(config_provider("countries")(w)) + if {"UA", "MD"}.intersection(countries): + return {"gdp_pop_non_nuts3": resources("gdp_pop_non_nuts3.geojson")} + return {} + + +rule build_gdp_pop_non_nuts3: + params: + countries=config_provider("countries"), + input: + base_network=resources("networks/base.nc"), + regions=resources("regions_onshore.geojson"), + gdp_non_nuts3="data/bundle/GDP_per_capita_PPP_1990_2015_v2.nc", + pop_non_nuts3="data/bundle/ppp_2013_1km_Aggregated.tif", + output: + resources("gdp_pop_non_nuts3.geojson"), + log: + logs("build_gdp_pop_non_nuts3.log"), + benchmark: + benchmarks("build_gdp_pop_non_nuts3") + threads: 1 + resources: + mem_mb=8000, + conda: + "../envs/environment.yaml" + script: + "../scripts/build_gdp_pop_non_nuts3.py" + + rule add_electricity: params: length_factor=config_provider("lines", "length_factor"), @@ -390,6 +421,7 @@ rule add_electricity: input: unpack(input_profile_tech), unpack(input_conventional), + unpack(input_gdp_pop_non_nuts3), base_network=resources("networks/base.nc"), line_rating=lambda w: ( resources("networks/line_rating.nc") @@ -411,7 +443,6 @@ rule add_electricity: ), load=resources("electricity_demand.csv"), nuts3_shapes=resources("nuts3_shapes.geojson"), - ua_md_gdp="data/GDP_PPP_30arcsec_v3_mapped_default.csv", output: resources("networks/elec.nc"), log: diff --git a/rules/retrieve.smk b/rules/retrieve.smk index 10ad9684a..71b005ac3 100644 --- a/rules/retrieve.smk +++ b/rules/retrieve.smk @@ -29,6 +29,8 @@ if config["enable"]["retrieve"] and config["enable"].get("retrieve_databundle", "h2_salt_caverns_GWh_per_sqkm.geojson", "natura/natura.tiff", "gebco/GEBCO_2014_2D.nc", + "GDP_per_capita_PPP_1990_2015_v2.nc", + "ppp_2013_1km_Aggregated.tif", ] rule retrieve_databundle: @@ -163,7 +165,7 @@ if config["enable"]["retrieve"]: rule retrieve_ship_raster: input: storage( - "https://zenodo.org/records/10973944/files/shipdensity_global.zip", + "https://zenodo.org/records/12760663/files/shipdensity_global.zip", keep_local=True, ), output: diff --git a/scripts/add_electricity.py b/scripts/add_electricity.py index f90d6c851..17e030b42 100755 --- a/scripts/add_electricity.py +++ b/scripts/add_electricity.py @@ -294,19 +294,19 @@ def shapes_to_shapes(orig, dest): return transfer -def attach_load(n, regions, load, nuts3_shapes, ua_md_gdp, countries, scaling=1.0): +def attach_load( + n, regions, load, nuts3_shapes, gdp_pop_non_nuts3, countries, scaling=1.0 +): substation_lv_i = n.buses.index[n.buses["substation_lv"]] - regions = gpd.read_file(regions).set_index("name").reindex(substation_lv_i) + gdf_regions = gpd.read_file(regions).set_index("name").reindex(substation_lv_i) opsd_load = pd.read_csv(load, index_col=0, parse_dates=True).filter(items=countries) - ua_md_gdp = pd.read_csv(ua_md_gdp, dtype={"name": "str"}).set_index("name") - logger.info(f"Load data scaled by factor {scaling}.") opsd_load *= scaling nuts3 = gpd.read_file(nuts3_shapes).set_index("index") - def upsample(cntry, group): + def upsample(cntry, group, gdp_pop_non_nuts3): load = opsd_load[cntry] if len(group) == 1: @@ -325,7 +325,15 @@ def upsample(cntry, group): factors = normed(0.6 * normed(gdp_n) + 0.4 * normed(pop_n)) if cntry in ["UA", "MD"]: # overwrite factor because nuts3 provides no data for UA+MD - factors = normed(ua_md_gdp.loc[group.index, "GDP_PPP"].squeeze()) + gdp_pop_non_nuts3 = gpd.read_file(gdp_pop_non_nuts3).set_index("Bus") + gdp_pop_non_nuts3 = gdp_pop_non_nuts3.loc[ + (gdp_pop_non_nuts3.country == cntry) + & (gdp_pop_non_nuts3.index.isin(substation_lv_i)) + ] + factors = normed( + 0.6 * normed(gdp_pop_non_nuts3["gdp"]) + + 0.4 * normed(gdp_pop_non_nuts3["pop"]) + ) return pd.DataFrame( factors.values * load.values[:, np.newaxis], index=load.index, @@ -334,8 +342,8 @@ def upsample(cntry, group): load = pd.concat( [ - upsample(cntry, group) - for cntry, group in regions.geometry.groupby(regions.country) + upsample(cntry, group, gdp_pop_non_nuts3) + for cntry, group in gdf_regions.geometry.groupby(gdf_regions.country) ], axis=1, ) @@ -821,7 +829,7 @@ def attach_line_rating( snakemake.input.regions, snakemake.input.load, snakemake.input.nuts3_shapes, - snakemake.input.ua_md_gdp, + snakemake.input.get("gdp_pop_non_nuts3"), params.countries, params.scaling_factor, ) diff --git a/scripts/build_gdp_pop_non_nuts3.py b/scripts/build_gdp_pop_non_nuts3.py new file mode 100644 index 000000000..fad73dfea --- /dev/null +++ b/scripts/build_gdp_pop_non_nuts3.py @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2017-2024 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT +""" +Maps the per-capita GDP and population values to non-NUTS3 regions. The script +takes as input the country code, a GeoDataFrame containing the regions, and the +file paths to the datasets containing the GDP and POP values for non-NUTS3 +countries. +""" + +import logging + +import geopandas as gpd +import numpy as np +import pandas as pd +import pypsa +import rasterio +import xarray as xr +from _helpers import configure_logging, set_scenario_config +from rasterio.mask import mask +from shapely.geometry import box + +logger = logging.getLogger(__name__) + + +def calc_gdp_pop(country, regions, gdp_non_nuts3, pop_non_nuts3): + """ + Calculate the GDP p.c. and population values for non NUTS3 regions. + + Parameters: + country (str): The two-letter country code of the non-NUTS3 region. + regions (GeoDataFrame): A GeoDataFrame containing the regions. + gdp_non_nuts3 (str): The file path to the dataset containing the GDP p.c values + for non NUTS3 countries (e.g. MD, UA) + pop_non_nuts3 (str): The file path to the dataset containing the POP values + for non NUTS3 countries (e.g. MD, UA) + + Returns: + tuple: A tuple containing two GeoDataFrames: + - gdp: A GeoDataFrame with the mean GDP p.c. values mapped to each bus. + - pop: A GeoDataFrame with the summed POP values mapped to each bus. + """ + regions = ( + regions.rename(columns={"name": "Bus"}) + .drop(columns=["x", "y"]) + .set_index("Bus") + ) + regions = regions[regions.country == country] + # Create a bounding box for UA, MD from region shape, including a buffer of 10000 metres + bounding_box = ( + gpd.GeoDataFrame(geometry=[box(*regions.total_bounds)], crs=regions.crs) + .to_crs(epsg=3857) + .buffer(10000) + .to_crs(regions.crs) + ) + + # GDP Mapping + logger.info(f"Mapping mean GDP p.c. to non-NUTS3 region: {country}") + with xr.open_dataset(gdp_non_nuts3) as src_gdp: + src_gdp = src_gdp.where( + (src_gdp.longitude >= bounding_box.bounds.minx.min()) + & (src_gdp.longitude <= bounding_box.bounds.maxx.max()) + & (src_gdp.latitude >= bounding_box.bounds.miny.min()) + & (src_gdp.latitude <= bounding_box.bounds.maxy.max()), + drop=True, + ) + gdp = src_gdp.to_dataframe().reset_index() + gdp = gdp.rename(columns={"GDP_per_capita_PPP": "gdp"}) + gdp = gdp[gdp.time == gdp.time.max()] + gdp_raster = gpd.GeoDataFrame( + gdp, + geometry=gpd.points_from_xy(gdp.longitude, gdp.latitude), + crs="EPSG:4326", + ) + gdp_mapped = gpd.sjoin(gdp_raster, regions, predicate="within") + gdp = ( + gdp_mapped.copy() + .groupby(["Bus", "country"]) + .agg({"gdp": "mean"}) + .reset_index(level=["country"]) + ) + + # Population Mapping + logger.info(f"Mapping summed population to non-NUTS3 region: {country}") + with rasterio.open(pop_non_nuts3) as src_pop: + # Mask the raster with the bounding box + out_image, out_transform = mask(src_pop, bounding_box, crop=True) + out_meta = src_pop.meta.copy() + out_meta.update( + { + "driver": "GTiff", + "height": out_image.shape[1], + "width": out_image.shape[2], + "transform": out_transform, + } + ) + masked_data = out_image[0] # Use the first band (rest is empty) + row_indices, col_indices = np.where(masked_data != src_pop.nodata) + values = masked_data[row_indices, col_indices] + + # Affine transformation from pixel coordinates to geo coordinates + x_coords, y_coords = rasterio.transform.xy(out_transform, row_indices, col_indices) + pop_raster = pd.DataFrame({"x": x_coords, "y": y_coords, "pop": values}) + pop_raster = gpd.GeoDataFrame( + pop_raster, + geometry=gpd.points_from_xy(pop_raster.x, pop_raster.y), + crs=src_pop.crs, + ) + pop_mapped = gpd.sjoin(pop_raster, regions, predicate="within") + pop = ( + pop_mapped.groupby(["Bus", "country"]) + .agg({"pop": "sum"}) + .reset_index() + .set_index("Bus") + ) + gdp_pop = regions.join(gdp.drop(columns="country"), on="Bus").join( + pop.drop(columns="country"), on="Bus" + ) + gdp_pop.fillna(0, inplace=True) + + return gdp_pop + + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake("build_gdp_pop_non_nuts3") + configure_logging(snakemake) + set_scenario_config(snakemake) + + n = pypsa.Network(snakemake.input.base_network) + regions = gpd.read_file(snakemake.input.regions) + + gdp_non_nuts3 = snakemake.input.gdp_non_nuts3 + pop_non_nuts3 = snakemake.input.pop_non_nuts3 + + subset = {"MD", "UA"}.intersection(snakemake.params.countries) + + gdp_pop = pd.concat( + [ + calc_gdp_pop(country, regions, gdp_non_nuts3, pop_non_nuts3) + for country in subset + ], + axis=0, + ) + + logger.info( + f"Exporting GDP and POP values for non-NUTS3 regions {snakemake.output}" + ) + gdp_pop.reset_index().to_file(snakemake.output, driver="GeoJSON") diff --git a/scripts/determine_availability_matrix_MD_UA.py b/scripts/determine_availability_matrix_MD_UA.py index 678ef025d..2ed11d3c0 100644 --- a/scripts/determine_availability_matrix_MD_UA.py +++ b/scripts/determine_availability_matrix_MD_UA.py @@ -48,7 +48,9 @@ def get_wdpa_layer_name(wdpa_fn, layer_substring): regions = ( gpd.read_file(snakemake.input.regions).set_index("name").rename_axis("bus") ) - buses = regions.index + # Limit to "UA" and "MD" regions + buses = regions.loc[regions["country"].isin(["UA", "MD"])].index.values + regions = regions.loc[buses] excluder = atlite.ExclusionContainer(crs=3035, res=100) @@ -152,8 +154,6 @@ def get_wdpa_layer_name(wdpa_fn, layer_substring): plt.axis("off") plt.savefig(snakemake.output.availability_map, bbox_inches="tight", dpi=500) - # Limit results only to buses for UA and MD - buses = regions.loc[regions["country"].isin(["UA", "MD"])].index.values availability = availability.sel(bus=buses) # Save and plot for verification diff --git a/scripts/retrieve_databundle.py b/scripts/retrieve_databundle.py index e2736f634..63b71dd5e 100644 --- a/scripts/retrieve_databundle.py +++ b/scripts/retrieve_databundle.py @@ -48,7 +48,7 @@ configure_logging(snakemake) set_scenario_config(snakemake) - url = "https://zenodo.org/records/10973944/files/bundle.tar.xz" + url = "https://zenodo.org/records/12760663/files/bundle.tar.xz" tarball_fn = Path(f"{rootpath}/bundle.tar.xz") to_fn = Path(rootpath) / Path(snakemake.output[0]).parent.parent From b49895a0411533a47803e176ac149e6ea7bba9c4 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 19 Jul 2024 19:43:11 +0200 Subject: [PATCH 037/344] determine_availability_matrix_MD_UA: enable parallelism & remove plots (#1170) * determine_availability_matrix_MD_UA: enable parallelism through temp files and remove plots * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- doc/release_notes.rst | 8 +++-- rules/build_electricity.smk | 1 - scripts/build_gdp_pop_non_nuts3.py | 9 ++--- .../determine_availability_matrix_MD_UA.py | 35 ++++++++++++------- 4 files changed, 32 insertions(+), 21 deletions(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 2cf0fe709..eb29ce4b1 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -31,14 +31,16 @@ Upcoming Release * Bugfix: Correctly read in threshold capacity below which to remove components from previous planning horizons in :mod:`add_brownfield`. -* For countries not contained in the NUTS3-specific datasets (i.e. MD and UA), the mapping of GDP per capita and population per bus region used to spatially distribute electricity demand is now endogenised in a new rule :mod:`build_gdp_ppp_non_nuts3`. https://github.com/PyPSA/pypsa-eur/pull/1146 +* For countries not contained in the NUTS3-specific datasets (i.e. MD and UA), the mapping of GDP per capita and population per bus region used to spatially distribute electricity demand is now endogenised in a new rule :mod:`build_gdp_ppp_non_nuts3`. https://github.com/PyPSA/pypsa-eur/pull/1146 -* The databundle has been updated to release v0.3.0, which includes raw GDP and population data for countries outside the NUTS system (UA, MD). https://github.com/PyPSA/pypsa-eur/pull/1146 +* The databundle has been updated to release v0.3.0, which includes raw GDP and population data for countries outside the NUTS system (UA, MD). https://github.com/PyPSA/pypsa-eur/pull/1146 -* Updated filtering in :mod:`determine_availability_matrix_MD_UA.py` to improve speed. https://github.com/PyPSA/pypsa-eur/pull/1146 +* Updated filtering in :mod:`determine_availability_matrix_MD_UA.py` to improve speed. https://github.com/PyPSA/pypsa-eur/pull/1146 * Bugfix: Impose minimum value of zero for district heating progress between current and future market share in :mod:`build_district_heat_share`. +* Enable parallelism in :mod:`determine_availability_matrix_MD_UA.py` and remove plots. This requires the use of temporary files. + PyPSA-Eur 0.11.0 (25th May 2024) ===================================== diff --git a/rules/build_electricity.smk b/rules/build_electricity.smk index a9ab71ef2..18ff8230b 100644 --- a/rules/build_electricity.smk +++ b/rules/build_electricity.smk @@ -202,7 +202,6 @@ rule determine_availability_matrix_MD_UA: + ".nc", output: availability_matrix=resources("availability_matrix_MD-UA_{technology}.nc"), - availability_map=resources("availability_matrix_MD-UA_{technology}.png"), log: logs("determine_availability_matrix_MD_UA_{technology}.log"), threads: config["atlite"].get("nprocesses", 4) diff --git a/scripts/build_gdp_pop_non_nuts3.py b/scripts/build_gdp_pop_non_nuts3.py index fad73dfea..d475aec92 100644 --- a/scripts/build_gdp_pop_non_nuts3.py +++ b/scripts/build_gdp_pop_non_nuts3.py @@ -3,10 +3,11 @@ # # SPDX-License-Identifier: MIT """ -Maps the per-capita GDP and population values to non-NUTS3 regions. The script -takes as input the country code, a GeoDataFrame containing the regions, and the -file paths to the datasets containing the GDP and POP values for non-NUTS3 -countries. +Maps the per-capita GDP and population values to non-NUTS3 regions. + +The script takes as input the country code, a GeoDataFrame containing +the regions, and the file paths to the datasets containing the GDP and +POP values for non-NUTS3 countries. """ import logging diff --git a/scripts/determine_availability_matrix_MD_UA.py b/scripts/determine_availability_matrix_MD_UA.py index 2ed11d3c0..0e7962ab4 100644 --- a/scripts/determine_availability_matrix_MD_UA.py +++ b/scripts/determine_availability_matrix_MD_UA.py @@ -8,16 +8,15 @@ import functools import logging +import os import time +from tempfile import NamedTemporaryFile import atlite import fiona import geopandas as gpd -import matplotlib.pyplot as plt import numpy as np from _helpers import configure_logging, set_scenario_config -from atlite.gis import shape_availability -from rasterio.plot import show logger = logging.getLogger(__name__) @@ -40,7 +39,7 @@ def get_wdpa_layer_name(wdpa_fn, layer_substring): configure_logging(snakemake) set_scenario_config(snakemake) - nprocesses = None # snakemake.config["atlite"].get("nprocesses") + nprocesses = int(snakemake.threads) noprogress = not snakemake.config["atlite"].get("show_progress", True) config = snakemake.config["renewable"][snakemake.wildcards.technology] @@ -95,8 +94,15 @@ def get_wdpa_layer_name(wdpa_fn, layer_substring): bbox=regions.geometry, layer=layer, ).to_crs(3035) + + # temporary file needed for parallelization + with NamedTemporaryFile(suffix=".geojson", delete=False) as f: + plg_tmp_fn = f.name if not wdpa.empty: - excluder.add_geometry(wdpa.geometry) + wdpa[["geometry"]].to_file(plg_tmp_fn) + while not os.path.exists(plg_tmp_fn): + time.sleep(1) + excluder.add_geometry(plg_tmp_fn) layer = get_wdpa_layer_name(wdpa_fn, "points") wdpa_pts = gpd.read_file( @@ -109,8 +115,15 @@ def get_wdpa_layer_name(wdpa_fn, layer_substring): wdpa_pts = wdpa_pts.set_geometry( wdpa_pts["geometry"].buffer(wdpa_pts["buffer_radius"]) ) + + # temporary file needed for parallelization + with NamedTemporaryFile(suffix=".geojson", delete=False) as f: + pts_tmp_fn = f.name if not wdpa_pts.empty: - excluder.add_geometry(wdpa_pts.geometry) + wdpa_pts[["geometry"]].to_file(pts_tmp_fn) + while not os.path.exists(pts_tmp_fn): + time.sleep(1) + excluder.add_geometry(pts_tmp_fn) if "max_depth" in config: # lambda not supported for atlite + multiprocessing @@ -146,13 +159,9 @@ def get_wdpa_layer_name(wdpa_fn, layer_substring): else: availability = cutout.availabilitymatrix(regions, excluder, **kwargs) - regions_geometry = regions.to_crs(3035).geometry - band, transform = shape_availability(regions_geometry, excluder) - fig, ax = plt.subplots(figsize=(4, 8)) - gpd.GeoSeries(regions_geometry.union_all()).plot(ax=ax, color="none") - show(band, transform=transform, cmap="Greens", ax=ax) - plt.axis("off") - plt.savefig(snakemake.output.availability_map, bbox_inches="tight", dpi=500) + for fn in [pts_tmp_fn, plg_tmp_fn]: + if os.path.exists(fn): + os.remove(fn) availability = availability.sel(bus=buses) From 745df4d2c1b1908c924cce36995c732880c7004e Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 22 Jul 2024 15:22:32 +0200 Subject: [PATCH 038/344] fix typo --- rules/common.smk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rules/common.smk b/rules/common.smk index 2b8495e1c..a7131334c 100644 --- a/rules/common.smk +++ b/rules/common.smk @@ -55,7 +55,7 @@ def dynamic_getter(wildcards, keys, default): scenario_name = wildcards.run if scenario_name not in scenarios: raise ValueError( - f"Scenario {scenario_name} not found in file {config['run']['scenario']['file']}." + f"Scenario {scenario_name} not found in file {config['run']['scenarios']['file']}." ) config_with_scenario = scenario_config(scenario_name) config_with_wildcards = update_config_from_wildcards( From 5361facbb7a3f480442bbea6e5792085b28ddeec Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Jul 2024 13:39:04 +0000 Subject: [PATCH 039/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- rules/build_sector.smk | 52 +++++++++++++++++++++-------- scripts/build_cop_profiles.py | 54 ++++++++++++++++++++----------- scripts/prepare_sector_network.py | 4 ++- 3 files changed, 77 insertions(+), 33 deletions(-) diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 8b4608600..510af7c04 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -217,19 +217,37 @@ rule build_temperature_profiles: rule build_cop_profiles: params: - heat_pump_sink_T_decentral_heating=config_provider("sector", "heat_pump_sink_T_individual_heating"), - forward_temperature_district_heating=config_provider("sector", "district_heating", "forward_temperature"), - return_temperature_district_heating=config_provider("sector", "district_heating", "return_temperature"), - heat_source_cooling_district_heating=config_provider("sector", "district_heating", "heat_source_cooling"), - heat_pump_cop_approximation_district_heating=config_provider("sector", "district_heating", "heat_pump_cop_approximation"), + heat_pump_sink_T_decentral_heating=config_provider( + "sector", "heat_pump_sink_T_individual_heating" + ), + forward_temperature_district_heating=config_provider( + "sector", "district_heating", "forward_temperature" + ), + return_temperature_district_heating=config_provider( + "sector", "district_heating", "return_temperature" + ), + heat_source_cooling_district_heating=config_provider( + "sector", "district_heating", "heat_source_cooling" + ), + heat_pump_cop_approximation_district_heating=config_provider( + "sector", "district_heating", "heat_pump_cop_approximation" + ), input: temp_soil_total=resources("temp_soil_total_elec_s{simpl}_{clusters}.nc"), temp_air_total=resources("temp_air_total_elec_s{simpl}_{clusters}.nc"), output: - cop_air_decentral_heating=resources("cop_air_decentral_heating_elec_s{simpl}_{clusters}.nc"), - cop_soil_decentral_heating=resources("cop_soil_decentral_heating_elec_s{simpl}_{clusters}.nc"), - cop_air_central_heating=resources("cop_air_central_heating_elec_s{simpl}_{clusters}.nc"), - cop_soil_central_heating=resources("cop_soil_central_heating_elec_s{simpl}_{clusters}.nc"), + cop_air_decentral_heating=resources( + "cop_air_decentral_heating_elec_s{simpl}_{clusters}.nc" + ), + cop_soil_decentral_heating=resources( + "cop_soil_decentral_heating_elec_s{simpl}_{clusters}.nc" + ), + cop_air_central_heating=resources( + "cop_air_central_heating_elec_s{simpl}_{clusters}.nc" + ), + cop_soil_central_heating=resources( + "cop_soil_central_heating_elec_s{simpl}_{clusters}.nc" + ), resources: mem_mb=20000, log: @@ -1027,10 +1045,18 @@ rule prepare_sector_network: temp_air_total=resources("temp_air_total_elec_s{simpl}_{clusters}.nc"), temp_air_rural=resources("temp_air_rural_elec_s{simpl}_{clusters}.nc"), temp_air_urban=resources("temp_air_urban_elec_s{simpl}_{clusters}.nc"), - cop_soil_decentral_heating=resources("cop_soil_decentral_heating_elec_s{simpl}_{clusters}.nc"), - cop_air_decentral_heating=resources("cop_air_decentral_heating_elec_s{simpl}_{clusters}.nc"), - cop_air_central_heating=resources("cop_air_central_heating_elec_s{simpl}_{clusters}.nc"), - cop_soil_central_heating=resources("cop_soil_central_heating_elec_s{simpl}_{clusters}.nc"), + cop_soil_decentral_heating=resources( + "cop_soil_decentral_heating_elec_s{simpl}_{clusters}.nc" + ), + cop_air_decentral_heating=resources( + "cop_air_decentral_heating_elec_s{simpl}_{clusters}.nc" + ), + cop_air_central_heating=resources( + "cop_air_central_heating_elec_s{simpl}_{clusters}.nc" + ), + cop_soil_central_heating=resources( + "cop_soil_central_heating_elec_s{simpl}_{clusters}.nc" + ), solar_thermal_total=lambda w: ( resources("solar_thermal_total_elec_s{simpl}_{clusters}.nc") if config_provider("sector", "solar_thermal")(w) diff --git a/scripts/build_cop_profiles.py b/scripts/build_cop_profiles.py index 77caa1a6b..89eeb9b68 100644 --- a/scripts/build_cop_profiles.py +++ b/scripts/build_cop_profiles.py @@ -38,10 +38,11 @@ [3] Pieper et al., Energy 205 (2020): Comparison of COP estimation methods for large-scale heat pumps used in energy planning, https://doi.org/10.1016/j.energy.2020.117994 """ -from typing import Union from enum import Enum -import xarray as xr +from typing import Union + import numpy as np +import xarray as xr from _helpers import set_scenario_config @@ -54,17 +55,23 @@ def coefficient_of_performance_individual_heating(delta_T, source="air"): raise NotImplementedError("'source' must be one of ['air', 'soil']") -def celsius_to_kelvin(t_celsius: Union[float, xr.DataArray, np.array]) -> Union[float, xr.DataArray, np.array]: +def celsius_to_kelvin( + t_celsius: Union[float, xr.DataArray, np.array] +) -> Union[float, xr.DataArray, np.array]: if (np.asarray(t_celsius) > 200).any(): raise ValueError("t_celsius > 200. Are you sure you are using the right units?") return t_celsius + 273.15 -def logarithmic_mean(t_hot: Union[float, xr.DataArray, np.ndarray], t_cold: Union[float, xr.DataArray, np.ndarray]) -> Union[float, xr.DataArray, np.ndarray]: +def logarithmic_mean( + t_hot: Union[float, xr.DataArray, np.ndarray], + t_cold: Union[float, xr.DataArray, np.ndarray], +) -> Union[float, xr.DataArray, np.ndarray]: if (np.asarray(t_hot <= t_cold)).any(): raise ValueError("t_hot must be greater than t_cold") return (t_hot - t_cold) / np.log(t_hot / t_cold) + class CopDistrictHeating: def __init__( @@ -142,7 +149,8 @@ def cop(self) -> Union[xr.DataArray, np.array]: @property def t_sink_mean(self) -> Union[xr.DataArray, np.array]: """ - Calculate the logarithmic mean temperature difference between the cold and hot sinks. + Calculate the logarithmic mean temperature difference between the cold + and hot sinks. Returns ------- @@ -166,7 +174,8 @@ def t_source_mean(self) -> Union[xr.DataArray, np.array]: @property def delta_t_lift(self) -> Union[xr.DataArray, np.array]: """ - Calculate the temperature lift as the difference between the logarithmic sink and source temperatures. + Calculate the temperature lift as the difference between the + logarithmic sink and source temperatures. Returns ------- @@ -187,14 +196,14 @@ def ideal_lorenz_cop(self) -> Union[xr.DataArray, np.array]: ------- np.array The ideal Lorenz COP. - """ return self.t_sink_mean / self.delta_t_lift @property def delta_t_refrigerant_source(self) -> Union[xr.DataArray, np.array]: """ - Calculate the temperature difference between the refrigerant source inlet and outlet. + Calculate the temperature difference between the refrigerant source + inlet and outlet. Returns ------- @@ -208,7 +217,8 @@ def delta_t_refrigerant_source(self) -> Union[xr.DataArray, np.array]: @property def delta_t_refrigerant_sink(self) -> Union[xr.DataArray, np.array]: """ - Temperature difference between the refrigerant and the sink based on approximation. + Temperature difference between the refrigerant and the sink based on + approximation. Returns ------- @@ -220,7 +230,8 @@ def delta_t_refrigerant_sink(self) -> Union[xr.DataArray, np.array]: @property def ratio_evaporation_compression_work(self) -> Union[xr.DataArray, np.array]: """ - Calculate the ratio of evaporation to compression work based on approximation. + Calculate the ratio of evaporation to compression work based on + approximation. Returns ------- @@ -228,7 +239,7 @@ def ratio_evaporation_compression_work(self) -> Union[xr.DataArray, np.array]: The calculated ratio of evaporation to compression work. """ return self._ratio_evaporation_compression_work_approximation() - + @property def delta_t_sink(self) -> Union[xr.DataArray, np.array]: """ @@ -245,7 +256,8 @@ def _approximate_delta_t_refrigerant_source( self, delta_t_source: Union[xr.DataArray, np.array] ) -> Union[xr.DataArray, np.array]: """ - Approximates the temperature difference between the refrigerant and the source. + Approximates the temperature difference between the refrigerant and the + source. Parameters ---------- @@ -291,7 +303,6 @@ def _approximate_delta_t_refrigerant_sink( The approximate temperature difference at the refrigerant sink is calculated using the following formula: a * (t_sink_out - t_source_out + 2 * delta_t_pinch) + b * delta_t_sink + c - """ if refrigerant not in a.keys(): raise ValueError( @@ -366,16 +377,21 @@ def _ratio_evaporation_compression_work_approximation( delta_T = snakemake.params.heat_pump_sink_T_decentral_heating - source_T - cop_individual_heating = coefficient_of_performance_individual_heating(delta_T, source) - cop_individual_heating.to_netcdf(snakemake.output[f"cop_{source}_decentral_heating"]) + cop_individual_heating = coefficient_of_performance_individual_heating( + delta_T, source + ) + cop_individual_heating.to_netcdf( + snakemake.output[f"cop_{source}_decentral_heating"] + ) cop_district_heating = CopDistrictHeating( forward_temperature_celsius=snakemake.params.forward_temperature_district_heating, return_temperature_celsius=snakemake.params.return_temperature_district_heating, source_inlet_temperature_celsius=source_T, - source_outlet_temperature_celsius=source_T - snakemake.params.heat_source_cooling_district_heating, + source_outlet_temperature_celsius=source_T + - snakemake.params.heat_source_cooling_district_heating, ).cop() - cop_district_heating.to_netcdf(snakemake.output[f"cop_{source}_central_heating"]) - - + cop_district_heating.to_netcdf( + snakemake.output[f"cop_{source}_central_heating"] + ) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 66b5085da..1916c62ec 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1827,7 +1827,9 @@ def add_heat(n, costs): "air decentral": xr.open_dataarray(snakemake.input.cop_air_decentral_heating) .to_pandas() .reindex(index=n.snapshots), - "ground decentral": xr.open_dataarray(snakemake.input.cop_soil_decentral_heating) + "ground decentral": xr.open_dataarray( + snakemake.input.cop_soil_decentral_heating + ) .to_pandas() .reindex(index=n.snapshots), "air central": xr.open_dataarray(snakemake.input.cop_air_central_heating) From 877ca3eb449cf61db376433883956ca74b34e7e2 Mon Sep 17 00:00:00 2001 From: lisazeyen <35347358+lisazeyen@users.noreply.github.com> Date: Mon, 22 Jul 2024 16:44:26 +0200 Subject: [PATCH 040/344] change sign sequestration store marginal cost (#1174) --- scripts/prepare_sector_network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 53b811aa9..b86f9557a 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -688,7 +688,7 @@ def add_co2_tracking(n, costs, options): e_nom_extendable=True, e_nom_max=e_nom_max, capital_cost=options["co2_sequestration_cost"], - marginal_cost=0.1, + marginal_cost=-0.1, bus=sequestration_buses, lifetime=options["co2_sequestration_lifetime"], carrier="co2 sequestered", From b8b92ace64f7f99d6e0697385f85647eb65e2946 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 22 Jul 2024 17:35:50 +0200 Subject: [PATCH 041/344] common.smk: make solver_threads also look for capitalised 'Threads' --- rules/common.smk | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rules/common.smk b/rules/common.smk index a7131334c..ef518beb9 100644 --- a/rules/common.smk +++ b/rules/common.smk @@ -81,7 +81,8 @@ def config_provider(*keys, default=None): def solver_threads(w): solver_options = config_provider("solving", "solver_options")(w) option_set = config_provider("solving", "solver", "options")(w) - threads = solver_options[option_set].get("threads", 4) + solver_option_set = solver_options[option_set] + threads = solver_option_set.get("threads") or solver_option_set.get("Threads") or 4 return threads From 53bbc28908faf26ea93ff58fa4c7e95078e4abed Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Mon, 22 Jul 2024 21:15:40 +0200 Subject: [PATCH 042/344] add eurostat data for GB agriculture --- scripts/build_energy_totals.py | 42 ++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index 9491cc57c..d4afe7c8f 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -55,6 +55,21 @@ idx = pd.IndexSlice + +# from JRC-2021 methodology p.58 +agriculture_idees_eurostat_mapping = { + "Solids": ["C0000X0350-0370", "P1000", "S2000"], + "LPG": ["O4630"], + "Diesel oil and liquid biofuels": ["O4671XR5220B", "R5210P", "R5210B", "R5220P", "R5220B", "R5230P", "R5230B", "R5290"], + "Fuel oil and other liquids": ["O4680", "O4100_TOT_4200-4500XBIO", "O4652XR5210B", "O4651", "O4653", "O4661XR5230B", "O4669", "O4640", "O4691", "O4692", "O4695", "O4694", "O4693", "O4699"], + "Natural gas and biogas": ["G3000", "C0350-0370", "R5300"], + "Biomass and waste": ["R5110-5150_W6000RI", "R5160", "W6210", "W6100_6220"], + "Solar and geothermal": ["RA200", "RA410"], + "Ambient heat": ["RA600"], + "Distributed heat": ["H8000"], + "Electricity": ["E7000"] +} + def cartesian(s1: pd.Series, s2: pd.Series) -> pd.DataFrame: """ Compute the Cartesian product of two pandas Series. @@ -648,6 +663,33 @@ def build_energy_totals( df = pd.concat([df.drop("CH", errors="ignore"), swiss]).sort_index() # get values for missing countries based on Eurostat EnergyBalances + + # agriculture + + to_fill = df.index[ + df["total agriculture"].isna() + & df.index.get_level_values("country").isin(eurostat_countries) + ] + c = to_fill.get_level_values("country") + y = to_fill.get_level_values("year") + + # take total final energy consumption from Eurostat + eurostat_sector = 'Agriculture & forestry' + slicer = idx[c, y, :, :, eurostat_sector] + + fill_values = eurostat.loc[slicer]["Total all products"].groupby(level=[0,1]).sum() + # fill missing years for some countries by mean over the other years + means = fill_values.groupby(level='country').transform('mean') + fill_values = fill_values.where(fill_values != 0, means) + + # split into end uses by average EU data from IDEES + uses = ["electricity", "heat", "machinery"] + + for use in uses: + avg = (idees["total agriculture electricity"] + /idees["total agriculture"]).mean() + df.loc[to_fill, f"total agriculture {use}"] = df.loc[to_fill, "total agriculture"] * avg + # divide cooking/space/water according to averages in EU28 uses = ["space", "cooking", "water"] From b957f88645bf6aa31261e58c697d4da525c39334 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Mon, 22 Jul 2024 21:40:47 +0200 Subject: [PATCH 043/344] fix bugs and clean up --- scripts/build_energy_totals.py | 54 +++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index d4afe7c8f..55da77e3d 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -55,21 +55,6 @@ idx = pd.IndexSlice - -# from JRC-2021 methodology p.58 -agriculture_idees_eurostat_mapping = { - "Solids": ["C0000X0350-0370", "P1000", "S2000"], - "LPG": ["O4630"], - "Diesel oil and liquid biofuels": ["O4671XR5220B", "R5210P", "R5210B", "R5220P", "R5220B", "R5230P", "R5230B", "R5290"], - "Fuel oil and other liquids": ["O4680", "O4100_TOT_4200-4500XBIO", "O4652XR5210B", "O4651", "O4653", "O4661XR5230B", "O4669", "O4640", "O4691", "O4692", "O4695", "O4694", "O4693", "O4699"], - "Natural gas and biogas": ["G3000", "C0350-0370", "R5300"], - "Biomass and waste": ["R5110-5150_W6000RI", "R5160", "W6210", "W6100_6220"], - "Solar and geothermal": ["RA200", "RA410"], - "Ambient heat": ["RA600"], - "Distributed heat": ["H8000"], - "Electricity": ["E7000"] -} - def cartesian(s1: pd.Series, s2: pd.Series) -> pd.DataFrame: """ Compute the Cartesian product of two pandas Series. @@ -603,6 +588,31 @@ def build_idees(countries: List[str]) -> pd.DataFrame: return totals +def fill_missing_years(fill_values: pd.Series) -> pd.Series: + """ + Fill missing years for some countries by mean over the other years. + + Parameters + ---------- + fill_values : pd.Series + A pandas Series with a MultiIndex (levels: country and year) representing + energy values, where some values may be zero and need to be filled. + + Returns + ------- + pd.Series + A pandas Series with zero values replaced by the mean value of the corresponding + country. + + Notes + ----- + - The function groups the data by the 'country' level and computes the mean for each group. + - Zero values in the original Series are replaced by the mean value of their respective country group. + """ + means = fill_values.groupby(level='country').transform('mean') + return fill_values.where(fill_values != 0, means) + + def build_energy_totals( countries: List[str], eurostat: pd.DataFrame, @@ -656,6 +666,8 @@ def build_energy_totals( slicer = idx[in_eurostat, :, :, "Bunkers", :] fill_values = eurostat.loc[slicer, "Total all products"].groupby(level=[0, 1]).sum() + # fill missing years for some countries by mean over the other years + fill_values = fill_missing_years(fill_values) df.loc[in_eurostat, "total international navigation"] = fill_values # add swiss energy data @@ -679,8 +691,8 @@ def build_energy_totals( fill_values = eurostat.loc[slicer]["Total all products"].groupby(level=[0,1]).sum() # fill missing years for some countries by mean over the other years - means = fill_values.groupby(level='country').transform('mean') - fill_values = fill_values.where(fill_values != 0, means) + fill_values = fill_missing_years(fill_values) + df.loc[to_fill, "total agriculture"] = fill_values # split into end uses by average EU data from IDEES uses = ["electricity", "heat", "machinery"] @@ -711,6 +723,8 @@ def build_energy_totals( fill_values = ( eurostat.loc[slicer, eurostat_fuels[fuel]].groupby(level=[0, 1]).sum() ) + # fill missing years for some countries by mean over the other years + fill_values = fill_missing_years(fill_values) df.loc[to_fill, f"{fuel} {sector}"] = fill_values for sector in ["residential", "services"]: @@ -786,16 +800,22 @@ def build_energy_totals( slicer = idx[c, y, :, :, "Domestic aviation"] fill_values = eurostat.loc[slicer, "Total all products"].groupby(level=[0, 1]).sum() + # fill missing years for some countries by mean over the other years + fill_values = fill_missing_years(fill_values) df.loc[to_fill, "total domestic aviation"] = fill_values slicer = idx[c, y, :, :, "International aviation"] fill_values = eurostat.loc[slicer, "Total all products"].groupby(level=[0, 1]).sum() + # fill missing years for some countries by mean over the other years + fill_values = fill_missing_years(fill_values) df.loc[to_fill, "total international aviation"] = fill_values # missing domestic navigation slicer = idx[c, y, :, :, "Domestic Navigation"] fill_values = eurostat.loc[slicer, "Total all products"].groupby(level=[0, 1]).sum() + # fill missing years for some countries by mean over the other years + fill_values = fill_missing_years(fill_values) df.loc[to_fill, "total domestic navigation"] = fill_values # split road traffic for non-IDEES From fb26aaf38dc85e37570162060212ef22425b6c6c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Jul 2024 19:42:52 +0000 Subject: [PATCH 044/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/build_energy_totals.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index 55da77e3d..6404cd931 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -609,7 +609,7 @@ def fill_missing_years(fill_values: pd.Series) -> pd.Series: - The function groups the data by the 'country' level and computes the mean for each group. - Zero values in the original Series are replaced by the mean value of their respective country group. """ - means = fill_values.groupby(level='country').transform('mean') + means = fill_values.groupby(level="country").transform("mean") return fill_values.where(fill_values != 0, means) @@ -675,33 +675,36 @@ def build_energy_totals( df = pd.concat([df.drop("CH", errors="ignore"), swiss]).sort_index() # get values for missing countries based on Eurostat EnergyBalances - + # agriculture - + to_fill = df.index[ df["total agriculture"].isna() & df.index.get_level_values("country").isin(eurostat_countries) ] c = to_fill.get_level_values("country") y = to_fill.get_level_values("year") - + # take total final energy consumption from Eurostat - eurostat_sector = 'Agriculture & forestry' + eurostat_sector = "Agriculture & forestry" slicer = idx[c, y, :, :, eurostat_sector] - - fill_values = eurostat.loc[slicer]["Total all products"].groupby(level=[0,1]).sum() + + fill_values = eurostat.loc[slicer]["Total all products"].groupby(level=[0, 1]).sum() # fill missing years for some countries by mean over the other years fill_values = fill_missing_years(fill_values) df.loc[to_fill, "total agriculture"] = fill_values - + # split into end uses by average EU data from IDEES uses = ["electricity", "heat", "machinery"] - + for use in uses: - avg = (idees["total agriculture electricity"] - /idees["total agriculture"]).mean() - df.loc[to_fill, f"total agriculture {use}"] = df.loc[to_fill, "total agriculture"] * avg - + avg = ( + idees["total agriculture electricity"] / idees["total agriculture"] + ).mean() + df.loc[to_fill, f"total agriculture {use}"] = ( + df.loc[to_fill, "total agriculture"] * avg + ) + # divide cooking/space/water according to averages in EU28 uses = ["space", "cooking", "water"] From f11f8d86134aaa38b5bb696e79a7ad4b9fb6ebef Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 22 Jul 2024 21:51:37 +0200 Subject: [PATCH 045/344] solver settings: add copt-gpu setings for pdlp solver --- config/config.default.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/config/config.default.yaml b/config/config.default.yaml index 0af347341..d1d13065c 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -879,6 +879,13 @@ solving: Threads: 8 LpMethod: 2 Crossover: 0 + RelGap: 1.e-6 + Dualize: 0 + copt-gpu: + LpMethod: 6 + GPUMode: 1 + PDLPTol: 1.e-5 + Crossover: 0 cbc-default: {} # Used in CI glpk-default: {} # Used in CI From 3c62a3b4cb21e8a86eb0478a8fd30baf41d09266 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Mon, 22 Jul 2024 21:52:54 +0200 Subject: [PATCH 046/344] add release notes --- doc/release_notes.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index eb29ce4b1..fcfd8083f 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -10,6 +10,9 @@ Release Notes Upcoming Release ================ +* Upadte JRC-IDEES-2015 to `JRC-IDEES-2021 +`__. + * Changed default assumptions about waste heat usage from PtX and fuel cells in district heating. The default value for the link efficiency scaling factor was changed from 100% to 25%. It can be set to other values in the configuration ``sector: use_TECHNOLOGY_waste_heat``. From 7451349fed6f2814d0b26627b6982552f8738da4 Mon Sep 17 00:00:00 2001 From: lisazeyen <35347358+lisazeyen@users.noreply.github.com> Date: Tue, 23 Jul 2024 15:10:05 +0200 Subject: [PATCH 047/344] take eurostat year for transport demand --- scripts/build_transport_demand.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build_transport_demand.py b/scripts/build_transport_demand.py index 4a29667af..074dc260b 100644 --- a/scripts/build_transport_demand.py +++ b/scripts/build_transport_demand.py @@ -26,7 +26,7 @@ def build_nodal_transport_data(fn, pop_layout, year): # get numbers of car and fuel efficiency per country transport_data = pd.read_csv(fn, index_col=[0, 1]) - transport_data = transport_data.xs(min(2015, year), level="year") + transport_data = transport_data.xs(year, level="year") # break number of cars down to nodal level based on population density nodal_transport_data = transport_data.loc[pop_layout.ct].fillna(0.0) From d142d5a50b0a64bfbbc2e00a396abcaeecc5ee60 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 24 Jul 2024 10:54:15 +0200 Subject: [PATCH 048/344] aggregate curtailment into single curtailment generator per bus (#1177) * add curtailment generator mode * add documentation --- config/config.default.yaml | 5 +++-- doc/configtables/solving.csv | 1 + doc/release_notes.rst | 6 ++++++ scripts/solve_network.py | 16 ++++++++++++++++ 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index d1d13065c..ab5bb0438 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -786,6 +786,7 @@ solving: options: clip_p_max_pu: 1.e-2 load_shedding: false + curtailment_mode: false noisy_costs: true skip_iterations: true rolling_horizon: false @@ -830,7 +831,7 @@ solving: solver_options: highs-default: # refer to https://ergo-code.github.io/HiGHS/dev/options/definitions/ - threads: 4 + threads: 1 solver: "ipm" run_crossover: "off" small_matrix_value: 1e-6 @@ -841,7 +842,7 @@ solving: parallel: "on" random_seed: 123 gurobi-default: - threads: 4 + threads: 8 method: 2 # barrier crossover: 0 BarConvTol: 1.e-6 diff --git a/doc/configtables/solving.csv b/doc/configtables/solving.csv index 4cfb90657..d2e22c28c 100644 --- a/doc/configtables/solving.csv +++ b/doc/configtables/solving.csv @@ -2,6 +2,7 @@ options,,, -- clip_p_max_pu,p.u.,float,To avoid too small values in the renewables` per-unit availability time series values below this threshold are set to zero. -- load_shedding,bool/float,"{'true','false', float}","Add generators with very high marginal cost to simulate load shedding and avoid problem infeasibilities. If load shedding is a float, it denotes the marginal cost in EUR/kWh." +-- curtailment_mode,bool/float,"{'true','false'}","Fixes the dispatch profiles of generators with time-varying p_max_pu by setting ``p_min_pu = p_max_pu`` and adds an auxiliary curtailment generator (with negative sign to absorb excess power) at every AC bus. This can speed up the solving process as the curtailment decision is aggregated into a single generator per region. Defaults to ``false``." -- noisy_costs,bool,"{'true','false'}","Add random noise to marginal cost of generators by :math:`\mathcal{U}(0.009,0,011)` and capital cost of lines and links by :math:`\mathcal{U}(0.09,0,11)`." -- skip_iterations,bool,"{'true','false'}","Skip iterating, do not update impedances of branches. Defaults to true." -- rolling_horizon,bool,"{'true','false'}","Switch for rule :mod:`solve_operations_network` whether to optimize the network in a rolling horizon manner, where the snapshot range is split into slices of size `horizon` which are solved consecutively. This setting has currently no effect on sector-coupled networks." diff --git a/doc/release_notes.rst b/doc/release_notes.rst index eb29ce4b1..c38888a0a 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -41,6 +41,12 @@ Upcoming Release * Enable parallelism in :mod:`determine_availability_matrix_MD_UA.py` and remove plots. This requires the use of temporary files. +* Added option ``solving: curtailment_mode``` which fixes the dispatch profiles + of generators with time-varying p_max_pu by setting ``p_min_pu = p_max_pu`` + and adds an auxiliary curtailment generator with negative sign (to absorb + excess power) at every AC bus. This can speed up the solving process as the + curtailment decision is aggregated into a single generator per region. + PyPSA-Eur 0.11.0 (25th May 2024) ===================================== diff --git a/scripts/solve_network.py b/scripts/solve_network.py index 42ffe6be9..a2391ea2b 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -471,6 +471,22 @@ def prepare_network( p_nom=1e9, # kW ) + if solve_opts.get("curtailment_mode"): + n.add("Carrier", "curtailment", color="#fedfed", nice_name="Curtailment") + n.generators_t.p_min_pu = n.generators_t.p_max_pu + buses_i = n.buses.query("carrier == 'AC'").index + n.madd( + "Generator", + buses_i, + suffix=" curtailment", + bus=buses_i, + p_min_pu=-1, + p_max_pu=0, + marginal_cost=-0.1, + carrier="curtailment", + p_nom=1e6, + ) + if solve_opts.get("noisy_costs"): for t in n.iterate_components(): # if 'capital_cost' in t.df: From 8b1b1144d59d024add0ea2746d919a500bae3ba8 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 24 Jul 2024 11:01:00 +0200 Subject: [PATCH 049/344] cutouts: update zenodo repository version (#1176) --- config/config.default.yaml | 34 ++++++++++++--------------------- doc/configtables/atlite.csv | 2 +- doc/configtables/hydro.csv | 2 +- doc/configtables/lines.csv | 2 +- doc/configtables/offwind-ac.csv | 2 +- doc/configtables/offwind-dc.csv | 2 +- doc/configtables/onwind.csv | 2 +- doc/configtables/solar.csv | 2 +- doc/preparation.rst | 2 +- doc/release_notes.rst | 6 ++++++ rules/retrieve.smk | 2 +- scripts/build_cutout.py | 2 +- 12 files changed, 28 insertions(+), 32 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index ab5bb0438..4641837e4 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -134,35 +134,25 @@ electricity: # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#atlite atlite: - default_cutout: europe-2013-era5 + default_cutout: europe-2013-sarah3-era5 nprocesses: 4 show_progress: false cutouts: # use 'base' to determine geographical bounds and time span from config # base: # module: era5 - europe-2013-era5: - module: era5 # in priority order + europe-2013-sarah3-era5: + module: [sarah, era5] # in priority order x: [-12., 42.] - y: [33., 72] + y: [33., 72.] dx: 0.3 dy: 0.3 time: ['2013', '2013'] - europe-2013-sarah: - module: [sarah, era5] # in priority order - x: [-12., 42.] - y: [33., 65] - dx: 0.2 - dy: 0.2 - time: ['2013', '2013'] - sarah_interpolate: false - sarah_dir: - features: [influx, temperature] # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#renewable renewable: onwind: - cutout: europe-2013-era5 + cutout: europe-2013-sarah3-era5 resource: method: wind turbine: Vestas_V112_3MW @@ -181,7 +171,7 @@ renewable: excluder_resolution: 100 clip_p_max_pu: 1.e-2 offwind-ac: - cutout: europe-2013-era5 + cutout: europe-2013-sarah3-era5 resource: method: wind turbine: NREL_ReferenceTurbine_2020ATB_5.5MW @@ -197,7 +187,7 @@ renewable: excluder_resolution: 200 clip_p_max_pu: 1.e-2 offwind-dc: - cutout: europe-2013-era5 + cutout: europe-2013-sarah3-era5 resource: method: wind turbine: NREL_ReferenceTurbine_2020ATB_5.5MW @@ -213,7 +203,7 @@ renewable: excluder_resolution: 200 clip_p_max_pu: 1.e-2 offwind-float: - cutout: europe-2013-era5 + cutout: europe-2013-sarah3-era5 resource: method: wind turbine: NREL_ReferenceTurbine_5MW_offshore @@ -231,7 +221,7 @@ renewable: max_depth: 1000 clip_p_max_pu: 1.e-2 solar: - cutout: europe-2013-sarah + cutout: europe-2013-sarah3-era5 resource: method: pv panel: CSi @@ -246,7 +236,7 @@ renewable: excluder_resolution: 100 clip_p_max_pu: 1.e-2 solar-hsat: - cutout: europe-2013-sarah + cutout: europe-2013-sarah3-era5 resource: method: pv panel: CSi @@ -261,7 +251,7 @@ renewable: excluder_resolution: 100 clip_p_max_pu: 1.e-2 hydro: - cutout: europe-2013-era5 + cutout: europe-2013-sarah3-era5 carriers: [ror, PHS, hydro] PHS_max_hours: 6 hydro_max_hours: "energy_capacity_totals_by_country" # one of energy_capacity_totals_by_country, estimate_by_large_installations or a float @@ -295,7 +285,7 @@ lines: under_construction: 'keep' # 'zero': set capacity to zero, 'remove': remove, 'keep': with full capacity dynamic_line_rating: activate: false - cutout: europe-2013-era5 + cutout: europe-2013-sarah3-era5 correction_factor: 0.95 max_voltage_difference: false max_line_rating: false diff --git a/doc/configtables/atlite.csv b/doc/configtables/atlite.csv index 0b01005d9..5e0d2bd00 100644 --- a/doc/configtables/atlite.csv +++ b/doc/configtables/atlite.csv @@ -3,7 +3,7 @@ default_cutout,--,str,"Defines a default cutout." nprocesses,--,int,"Number of parallel processes in cutout preparation" show_progress,bool,true/false,"Whether progressbar for atlite conversion processes should be shown. False saves time." cutouts,,, --- {name},--,"Convention is to name cutouts like ``--`` (e.g. ``europe-2013-era5``).","Name of the cutout netcdf file. The user may specify multiple cutouts under configuration ``atlite: cutouts:``. Reference is used in configuration ``renewable: {technology}: cutout:``. The cutout ``base`` may be used to automatically calculate temporal and spatial bounds of the network." +-- {name},--,"Convention is to name cutouts like ``--`` (e.g. ``europe-2013-sarah3-era5``).","Name of the cutout netcdf file. The user may specify multiple cutouts under configuration ``atlite: cutouts:``. Reference is used in configuration ``renewable: {technology}: cutout:``. The cutout ``base`` may be used to automatically calculate temporal and spatial bounds of the network." -- -- module,--,"Subset of {'era5','sarah'}","Source of the reanalysis weather dataset (e.g. `ERA5 `_ or `SARAH-2 `_)" -- -- x,°,"Float interval within [-180, 180]","Range of longitudes to download weather data for. If not defined, it defaults to the spatial bounds of all bus shapes." -- -- y,°,"Float interval within [-90, 90]","Range of latitudes to download weather data for. If not defined, it defaults to the spatial bounds of all bus shapes." diff --git a/doc/configtables/hydro.csv b/doc/configtables/hydro.csv index 790029d1d..8903aa0fa 100644 --- a/doc/configtables/hydro.csv +++ b/doc/configtables/hydro.csv @@ -1,5 +1,5 @@ ,Unit,Values,Description -cutout,--,Must be 'europe-2013-era5',Specifies the directory where the relevant weather data ist stored. +cutout,--,Must be 'europe-2013-sarah3-era5',Specifies the directory where the relevant weather data ist stored. carriers,--,"Any subset of {'ror', 'PHS', 'hydro'}","Specifies the types of hydro power plants to build per-unit availability time series for. 'ror' stands for run-of-river plants, 'PHS' represents pumped-hydro storage, and 'hydro' stands for hydroelectric dams." PHS_max_hours,h,float,Maximum state of charge capacity of the pumped-hydro storage (PHS) in terms of hours at full output capacity ``p_nom``. Cf. `PyPSA documentation `_. hydro_max_hours,h,"Any of {float, 'energy_capacity_totals_by_country', 'estimate_by_large_installations'}",Maximum state of charge capacity of the pumped-hydro storage (PHS) in terms of hours at full output capacity ``p_nom`` or heuristically determined. Cf. `PyPSA documentation `_. diff --git a/doc/configtables/lines.csv b/doc/configtables/lines.csv index 3707d4a66..79fa2e16d 100644 --- a/doc/configtables/lines.csv +++ b/doc/configtables/lines.csv @@ -8,7 +8,7 @@ under_construction,--,"One of {'zero': set capacity to zero, 'remove': remove co reconnect_crimea,--,"true or false","Whether to reconnect Crimea to the Ukrainian grid" dynamic_line_rating,,, -- activate,bool,"true or false","Whether to take dynamic line rating into account" --- cutout,--,"Should be a folder listed in the configuration ``atlite: cutouts:`` (e.g. 'europe-2013-era5') or reference an existing folder in the directory ``cutouts``. Source module must be ERA5.","Specifies the directory where the relevant weather data ist stored." +-- cutout,--,"Should be a folder listed in the configuration ``atlite: cutouts:`` (e.g. 'europe-2013-sarah3-era5') or reference an existing folder in the directory ``cutouts``. Source module must be ERA5.","Specifies the directory where the relevant weather data ist stored." -- correction_factor,--,"float","Factor to compensate for overestimation of wind speeds in hourly averaged wind data" -- max_voltage_difference,deg,"float","Maximum voltage angle difference in degrees or 'false' to disable" -- max_line_rating,--,"float","Maximum line rating relative to nominal capacity without DLR, e.g. 1.3 or 'false' to disable" diff --git a/doc/configtables/offwind-ac.csv b/doc/configtables/offwind-ac.csv index b2533f04c..9ba2fa7e4 100644 --- a/doc/configtables/offwind-ac.csv +++ b/doc/configtables/offwind-ac.csv @@ -1,5 +1,5 @@ ,Unit,Values,Description -cutout,--,"Should be a folder listed in the configuration ``atlite: cutouts:`` (e.g. 'europe-2013-era5') or reference an existing folder in the directory ``cutouts``. Source module must be ERA5.","Specifies the directory where the relevant weather data ist stored." +cutout,--,"Should be a folder listed in the configuration ``atlite: cutouts:`` (e.g. 'europe-2013-sarah3-era5') or reference an existing folder in the directory ``cutouts``. Source module must be ERA5.","Specifies the directory where the relevant weather data ist stored." resource,,, -- method,--,"Must be 'wind'","A superordinate technology type." -- turbine,--,"One of turbine types included in `atlite `_. Can be a string or a dictionary with years as keys which denote the year another turbine model becomes available.","Specifies the turbine type and its characteristic power curve." diff --git a/doc/configtables/offwind-dc.csv b/doc/configtables/offwind-dc.csv index 7c5375434..e55d8944e 100644 --- a/doc/configtables/offwind-dc.csv +++ b/doc/configtables/offwind-dc.csv @@ -1,5 +1,5 @@ ,Unit,Values,Description -cutout,--,"Should be a folder listed in the configuration ``atlite: cutouts:`` (e.g. 'europe-2013-era5') or reference an existing folder in the directory ``cutouts``. Source module must be ERA5.","Specifies the directory where the relevant weather data ist stored." +cutout,--,"Should be a folder listed in the configuration ``atlite: cutouts:`` (e.g. 'europe-2013-sarah3-era5') or reference an existing folder in the directory ``cutouts``. Source module must be ERA5.","Specifies the directory where the relevant weather data ist stored." resource,,, -- method,--,"Must be 'wind'","A superordinate technology type." -- turbine,--,"One of turbine types included in `atlite `_. Can be a string or a dictionary with years as keys which denote the year another turbine model becomes available.","Specifies the turbine type and its characteristic power curve." diff --git a/doc/configtables/onwind.csv b/doc/configtables/onwind.csv index 3b09214b8..a801d83cd 100644 --- a/doc/configtables/onwind.csv +++ b/doc/configtables/onwind.csv @@ -1,5 +1,5 @@ ,Unit,Values,Description -cutout,--,"Should be a folder listed in the configuration ``atlite: cutouts:`` (e.g. 'europe-2013-era5') or reference an existing folder in the directory ``cutouts``. Source module must be ERA5.","Specifies the directory where the relevant weather data ist stored." +cutout,--,"Should be a folder listed in the configuration ``atlite: cutouts:`` (e.g. 'europe-2013-sarah3-era5') or reference an existing folder in the directory ``cutouts``. Source module must be ERA5.","Specifies the directory where the relevant weather data ist stored." resource,,, -- method,--,"Must be 'wind'","A superordinate technology type." -- turbine,--,"One of turbine types included in `atlite `_. Can be a string or a dictionary with years as keys which denote the year another turbine model becomes available.","Specifies the turbine type and its characteristic power curve." diff --git a/doc/configtables/solar.csv b/doc/configtables/solar.csv index 18587694c..21d7c2e41 100644 --- a/doc/configtables/solar.csv +++ b/doc/configtables/solar.csv @@ -1,5 +1,5 @@ ,Unit,Values,Description -cutout,--,"Should be a folder listed in the configuration ``atlite: cutouts:`` (e.g. 'europe-2013-era5') or reference an existing folder in the directory ``cutouts``. Source module can be ERA5 or SARAH-2.","Specifies the directory where the relevant weather data ist stored that is specified at ``atlite/cutouts`` configuration. Both ``sarah`` and ``era5`` work." +cutout,--,"Should be a folder listed in the configuration ``atlite: cutouts:`` (e.g. 'europe-2013-sarah3-era5') or reference an existing folder in the directory ``cutouts``. Source module can be ERA5 or SARAH-2.","Specifies the directory where the relevant weather data ist stored that is specified at ``atlite/cutouts`` configuration. Both ``sarah`` and ``era5`` work." resource,,, -- method,--,"Must be 'pv'","A superordinate technology type." -- panel,--,"One of {'Csi', 'CdTe', 'KANENA'} as defined in `atlite `_ . Can be a string or a dictionary with years as keys which denote the year another turbine model becomes available.","Specifies the solar panel technology and its characteristic attributes." diff --git a/doc/preparation.rst b/doc/preparation.rst index 06e8b19b1..669f33927 100644 --- a/doc/preparation.rst +++ b/doc/preparation.rst @@ -16,7 +16,7 @@ using the ``retrieve*`` rules (:ref:`data`). Having downloaded the necessary data, - :mod:`build_shapes` generates GeoJSON files with shapes of the countries, exclusive economic zones and `NUTS3 `__ areas. -- :mod:`build_cutout` prepares smaller weather data portions from `ERA5 `__ for cutout ``europe-2013-era5`` and SARAH for cutout ``europe-2013-sarah``. +- :mod:`build_cutout` prepares smaller weather data portions from `ERA5 `__ for cutout ``europe-2013-sarah3-era5`` and SARAH for cutout ``europe-2013-sarah``. With these and the externally extracted ENTSO-E online map topology (``data/entsoegridkit``), it can build a base PyPSA network with the following rules: diff --git a/doc/release_notes.rst b/doc/release_notes.rst index c38888a0a..b6c0db546 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -41,6 +41,12 @@ Upcoming Release * Enable parallelism in :mod:`determine_availability_matrix_MD_UA.py` and remove plots. This requires the use of temporary files. +* Updated pre-built `weather data cutouts + `__. These are now merged cutouts with + solar irradiation from the new SARAH-3 dataset while taking all other + variables from ERA5. Cutouts are now available for multiple years (2010, 2013, + 2019, and 2023). + * Added option ``solving: curtailment_mode``` which fixes the dispatch profiles of generators with time-varying p_max_pu by setting ``p_min_pu = p_max_pu`` and adds an auxiliary curtailment generator with negative sign (to absorb diff --git a/rules/retrieve.smk b/rules/retrieve.smk index 71b005ac3..18b0ddd22 100644 --- a/rules/retrieve.smk +++ b/rules/retrieve.smk @@ -71,7 +71,7 @@ if config["enable"]["retrieve"] and config["enable"].get("retrieve_cutout", True rule retrieve_cutout: input: storage( - "https://zenodo.org/records/6382570/files/{cutout}.nc", + "https://zenodo.org/records/12791128/files/{cutout}.nc", ), output: protected("cutouts/" + CDIR + "{cutout}.nc"), diff --git a/scripts/build_cutout.py b/scripts/build_cutout.py index 1edb18cef..e8d6207c4 100644 --- a/scripts/build_cutout.py +++ b/scripts/build_cutout.py @@ -103,7 +103,7 @@ if "snakemake" not in globals(): from _helpers import mock_snakemake - snakemake = mock_snakemake("build_cutout", cutout="europe-2013-era5") + snakemake = mock_snakemake("build_cutout", cutout="europe-2013-sarah3-era5") configure_logging(snakemake) set_scenario_config(snakemake) From eab315291e502365db01e3058bd7487af2a0c48f Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 24 Jul 2024 13:19:57 +0200 Subject: [PATCH 050/344] remove {scope} wildcard (#1171) * remove {scope} wildcard * do not create removed files --- doc/release_notes.rst | 2 + doc/wildcards.rst | 7 ---- rules/build_sector.smk | 60 ++++++++------------------- scripts/build_cop_profiles.py | 19 +++------ scripts/build_hourly_heat_demand.py | 4 +- scripts/build_temperature_profiles.py | 6 +-- 6 files changed, 29 insertions(+), 69 deletions(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index b6c0db546..0aa97e5a6 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -39,6 +39,8 @@ Upcoming Release * Bugfix: Impose minimum value of zero for district heating progress between current and future market share in :mod:`build_district_heat_share`. +* The ``{scope}`` wildcard was removed, since its outputs were not used. + * Enable parallelism in :mod:`determine_availability_matrix_MD_UA.py` and remove plots. This requires the use of temporary files. * Updated pre-built `weather data cutouts diff --git a/doc/wildcards.rst b/doc/wildcards.rst index f8e60e203..9ddb7b0a8 100644 --- a/doc/wildcards.rst +++ b/doc/wildcards.rst @@ -142,13 +142,6 @@ The ``{sector_opts}`` wildcard is only used for sector-coupling studies. :widths: 10,20,10,10 :file: configtables/sector-opts.csv -.. _scope: - -The ``{scope}`` wildcard -======================== - -Takes values ``residential``, ``urban``, ``total``. - .. _planning_horizons: The ``{planning_horizons}`` wildcard diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 6614b163a..139ced1f9 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -151,18 +151,18 @@ rule build_daily_heat_demand: snapshots=config_provider("snapshots"), drop_leap_day=config_provider("enable", "drop_leap_day"), input: - pop_layout=resources("pop_layout_{scope}.nc"), + pop_layout=resources("pop_layout_total.nc"), regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), cutout=heat_demand_cutout, output: - heat_demand=resources("daily_heat_demand_{scope}_elec_s{simpl}_{clusters}.nc"), + heat_demand=resources("daily_heat_demand_total_elec_s{simpl}_{clusters}.nc"), resources: mem_mb=20000, threads: 8 log: - logs("build_daily_heat_demand_{scope}_{simpl}_{clusters}.loc"), + logs("build_daily_heat_demand_total_{simpl}_{clusters}.loc"), benchmark: - benchmarks("build_daily_heat_demand/{scope}_s{simpl}_{clusters}") + benchmarks("build_daily_heat_demand/total_s{simpl}_{clusters}") conda: "../envs/environment.yaml" script: @@ -175,16 +175,16 @@ rule build_hourly_heat_demand: drop_leap_day=config_provider("enable", "drop_leap_day"), input: heat_profile="data/heat_load_profile_BDEW.csv", - heat_demand=resources("daily_heat_demand_{scope}_elec_s{simpl}_{clusters}.nc"), + heat_demand=resources("daily_heat_demand_total_elec_s{simpl}_{clusters}.nc"), output: - heat_demand=resources("hourly_heat_demand_{scope}_elec_s{simpl}_{clusters}.nc"), + heat_demand=resources("hourly_heat_demand_total_elec_s{simpl}_{clusters}.nc"), resources: mem_mb=2000, threads: 8 log: - logs("build_hourly_heat_demand_{scope}_{simpl}_{clusters}.loc"), + logs("build_hourly_heat_demand_total_{simpl}_{clusters}.loc"), benchmark: - benchmarks("build_hourly_heat_demand/{scope}_s{simpl}_{clusters}") + benchmarks("build_hourly_heat_demand/total_s{simpl}_{clusters}") conda: "../envs/environment.yaml" script: @@ -196,19 +196,19 @@ rule build_temperature_profiles: snapshots=config_provider("snapshots"), drop_leap_day=config_provider("enable", "drop_leap_day"), input: - pop_layout=resources("pop_layout_{scope}.nc"), + pop_layout=resources("pop_layout_total.nc"), regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), cutout=heat_demand_cutout, output: - temp_soil=resources("temp_soil_{scope}_elec_s{simpl}_{clusters}.nc"), - temp_air=resources("temp_air_{scope}_elec_s{simpl}_{clusters}.nc"), + temp_soil=resources("temp_soil_total_elec_s{simpl}_{clusters}.nc"), + temp_air=resources("temp_air_total_elec_s{simpl}_{clusters}.nc"), resources: mem_mb=20000, threads: 8 log: - logs("build_temperature_profiles_{scope}_{simpl}_{clusters}.log"), + logs("build_temperature_profiles_total_{simpl}_{clusters}.log"), benchmark: - benchmarks("build_temperature_profiles/{scope}_s{simpl}_{clusters}") + benchmarks("build_temperature_profiles/total_s{simpl}_{clusters}") conda: "../envs/environment.yaml" script: @@ -220,18 +220,10 @@ rule build_cop_profiles: heat_pump_sink_T=config_provider("sector", "heat_pump_sink_T"), input: temp_soil_total=resources("temp_soil_total_elec_s{simpl}_{clusters}.nc"), - temp_soil_rural=resources("temp_soil_rural_elec_s{simpl}_{clusters}.nc"), - temp_soil_urban=resources("temp_soil_urban_elec_s{simpl}_{clusters}.nc"), temp_air_total=resources("temp_air_total_elec_s{simpl}_{clusters}.nc"), - temp_air_rural=resources("temp_air_rural_elec_s{simpl}_{clusters}.nc"), - temp_air_urban=resources("temp_air_urban_elec_s{simpl}_{clusters}.nc"), output: cop_soil_total=resources("cop_soil_total_elec_s{simpl}_{clusters}.nc"), - cop_soil_rural=resources("cop_soil_rural_elec_s{simpl}_{clusters}.nc"), - cop_soil_urban=resources("cop_soil_urban_elec_s{simpl}_{clusters}.nc"), cop_air_total=resources("cop_air_total_elec_s{simpl}_{clusters}.nc"), - cop_air_rural=resources("cop_air_rural_elec_s{simpl}_{clusters}.nc"), - cop_air_urban=resources("cop_air_urban_elec_s{simpl}_{clusters}.nc"), resources: mem_mb=20000, log: @@ -263,18 +255,18 @@ rule build_solar_thermal_profiles: drop_leap_day=config_provider("enable", "drop_leap_day"), solar_thermal=config_provider("solar_thermal"), input: - pop_layout=resources("pop_layout_{scope}.nc"), + pop_layout=resources("pop_layout_total.nc"), regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), cutout=solar_thermal_cutout, output: - solar_thermal=resources("solar_thermal_{scope}_elec_s{simpl}_{clusters}.nc"), + solar_thermal=resources("solar_thermal_total_elec_s{simpl}_{clusters}.nc"), resources: mem_mb=20000, threads: 16 log: - logs("build_solar_thermal_profiles_{scope}_s{simpl}_{clusters}.log"), + logs("build_solar_thermal_profiles_total_s{simpl}_{clusters}.log"), benchmark: - benchmarks("build_solar_thermal_profiles/{scope}_s{simpl}_{clusters}") + benchmarks("build_solar_thermal_profiles/total_s{simpl}_{clusters}") conda: "../envs/environment.yaml" script: @@ -1024,32 +1016,14 @@ rule prepare_sector_network: "district_heat_share_elec_s{simpl}_{clusters}_{planning_horizons}.csv" ), temp_soil_total=resources("temp_soil_total_elec_s{simpl}_{clusters}.nc"), - temp_soil_rural=resources("temp_soil_rural_elec_s{simpl}_{clusters}.nc"), - temp_soil_urban=resources("temp_soil_urban_elec_s{simpl}_{clusters}.nc"), temp_air_total=resources("temp_air_total_elec_s{simpl}_{clusters}.nc"), - temp_air_rural=resources("temp_air_rural_elec_s{simpl}_{clusters}.nc"), - temp_air_urban=resources("temp_air_urban_elec_s{simpl}_{clusters}.nc"), cop_soil_total=resources("cop_soil_total_elec_s{simpl}_{clusters}.nc"), - cop_soil_rural=resources("cop_soil_rural_elec_s{simpl}_{clusters}.nc"), - cop_soil_urban=resources("cop_soil_urban_elec_s{simpl}_{clusters}.nc"), cop_air_total=resources("cop_air_total_elec_s{simpl}_{clusters}.nc"), - cop_air_rural=resources("cop_air_rural_elec_s{simpl}_{clusters}.nc"), - cop_air_urban=resources("cop_air_urban_elec_s{simpl}_{clusters}.nc"), solar_thermal_total=lambda w: ( resources("solar_thermal_total_elec_s{simpl}_{clusters}.nc") if config_provider("sector", "solar_thermal")(w) else [] ), - solar_thermal_urban=lambda w: ( - resources("solar_thermal_urban_elec_s{simpl}_{clusters}.nc") - if config_provider("sector", "solar_thermal")(w) - else [] - ), - solar_thermal_rural=lambda w: ( - resources("solar_thermal_rural_elec_s{simpl}_{clusters}.nc") - if config_provider("sector", "solar_thermal")(w) - else [] - ), egs_potentials=lambda w: ( resources("egs_potentials_s{simpl}_{clusters}.csv") if config_provider("sector", "enhanced_geothermal", "enable")(w) diff --git a/scripts/build_cop_profiles.py b/scripts/build_cop_profiles.py index 2a47198b2..a6d999470 100644 --- a/scripts/build_cop_profiles.py +++ b/scripts/build_cop_profiles.py @@ -21,20 +21,12 @@ Inputs: ------- - ``resources//temp_soil_total_elec_s_.nc``: Soil temperature (total) time series. -- ``resources//temp_soil_rural_elec_s_.nc``: Soil temperature (rural) time series. -- ``resources//temp_soil_urban_elec_s_.nc``: Soil temperature (urban) time series. - ``resources//temp_air_total_elec_s_.nc``: Ambient air temperature (total) time series. -- ``resources//temp_air_rural_elec_s_.nc``: Ambient air temperature (rural) time series. -- ``resources//temp_air_urban_elec_s_.nc``: Ambient air temperature (urban) time series. Outputs: -------- - ``resources/cop_soil_total_elec_s_.nc``: COP (ground-sourced) time series (total). -- ``resources/cop_soil_rural_elec_s_.nc``: COP (ground-sourced) time series (rural). -- ``resources/cop_soil_urban_elec_s_.nc``: COP (ground-sourced) time series (urban). - ``resources/cop_air_total_elec_s_.nc``: COP (air-sourced) time series (total). -- ``resources/cop_air_rural_elec_s_.nc``: COP (air-sourced) time series (rural). -- ``resources/cop_air_urban_elec_s_.nc``: COP (air-sourced) time series (urban). References @@ -67,12 +59,11 @@ def coefficient_of_performance(delta_T, source="air"): set_scenario_config(snakemake) - for area in ["total", "urban", "rural"]: - for source in ["air", "soil"]: - source_T = xr.open_dataarray(snakemake.input[f"temp_{source}_{area}"]) + for source in ["air", "soil"]: + source_T = xr.open_dataarray(snakemake.input[f"temp_{source}_total"]) - delta_T = snakemake.params.heat_pump_sink_T - source_T + delta_T = snakemake.params.heat_pump_sink_T - source_T - cop = coefficient_of_performance(delta_T, source) + cop = coefficient_of_performance(delta_T, source) - cop.to_netcdf(snakemake.output[f"cop_{source}_{area}"]) + cop.to_netcdf(snakemake.output[f"cop_{source}_total"]) diff --git a/scripts/build_hourly_heat_demand.py b/scripts/build_hourly_heat_demand.py index c28860f3c..8573a1986 100644 --- a/scripts/build_hourly_heat_demand.py +++ b/scripts/build_hourly_heat_demand.py @@ -22,12 +22,12 @@ ------ - ``data/heat_load_profile_BDEW.csv``: Intraday heat profile for water and space heating demand for the residential and services sectors for weekends and weekdays. -- ``resources/daily_heat_demand__elec_s_.nc``: Daily heat demand per cluster. +- ``resources/daily_heat_demand_total_elec_s_.nc``: Daily heat demand per cluster. Outputs ------- -- ``resources/hourly_heat_demand__elec_s_.nc``: +- ``resources/hourly_heat_demand_total_elec_s_.nc``: """ from itertools import product diff --git a/scripts/build_temperature_profiles.py b/scripts/build_temperature_profiles.py index 493bd08f7..8e07ee87c 100644 --- a/scripts/build_temperature_profiles.py +++ b/scripts/build_temperature_profiles.py @@ -25,15 +25,15 @@ Inputs ------ -- ``resources//pop_layout_.nc``: +- ``resources//pop_layout_total.nc``: - ``resources//regions_onshore_elec_s_.geojson``: - ``cutout``: Weather data cutout, as specified in config Outputs ------- -- ``resources/temp_soil__elec_s_.nc``: -- ``resources/temp_air__elec_s_.nc` +- ``resources/temp_soil_total_elec_s_.nc``: +- ``resources/temp_air_total_elec_s_.nc` """ import atlite From 0e6a7377ea48241701b9e91518ed649084f49bc6 Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Wed, 24 Jul 2024 15:03:14 +0200 Subject: [PATCH 051/344] update configtables --- doc/configtables/sector.csv | 70 +++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/doc/configtables/sector.csv b/doc/configtables/sector.csv index 5045cecdc..bb74035ee 100644 --- a/doc/configtables/sector.csv +++ b/doc/configtables/sector.csv @@ -5,9 +5,17 @@ biomass,--,"{true, false}",Flag to include biomass sector. industry,--,"{true, false}",Flag to include industry sector. agriculture,--,"{true, false}",Flag to include agriculture sector. district_heating,--,,`prepare_sector_network.py `_ --- potential,--,float,maximum fraction of urban demand which can be supplied by district heating. Ignored where below current fraction. --- progress,--,Dictionary with planning horizons as keys., Increase of today's district heating demand to potential maximum district heating share. Progress = 0 means today's district heating share. Progress = 1 means maximum fraction of urban demand is supplied by district heating --- district_heating_loss,--,float,Share increase in district heat demand in urban central due to heat losses +#NAME?,--,float,maximum fraction of urban demand which can be supplied by district heating. Ignored where below current fraction. +#NAME?,--,Dictionary with planning horizons as keys., Increase of today's district heating demand to potential maximum district heating share. Progress = 0 means today's district heating share. Progress = 1 means maximum fraction of urban demand is supplied by district heating +#NAME?,--,float,Share increase in district heat demand in urban central due to heat losses +#NAME?,°C,float,Forward temperature in district heating +#NAME?,°C,float,Return temperature in district heating. Must be lower than forward temperature +#NAME?,K,float,Cooling of heat source for heat pumps +#NAME?,,, +#NAME?,--,"{ammonia, isobutane}",Heat pump refrigerant assumed for COP approximation +#NAME?,K,float,Heat pump pinch point temperature difference in heat exchangers assumed for approximation. +#NAME?,--,float,Isentropic efficiency of heat pump compressor assumed for approximation. Must be between 0 and 1. +#NAME?,--,float,Heat pump heat loss assumed for approximation. Must be between 0 and 1. cluster_heat_buses,--,"{true, false}",Cluster residential and service heat buses in `prepare_sector_network.py `_ to one to save memory. ,,, bev_dsm_restriction _value,--,float,Adds a lower state of charge (SOC) limit for battery electric vehicles (BEV) to manage its own energy demand (DSM). Located in `build_transport_demand.py `_. Set to 0 for no restriction on BEV DSM @@ -57,21 +65,21 @@ heat_pump_sink_T,°C,float,The temperature heat sink used in heat pumps based on reduce_space_heat _exogenously,--,"{true, false}",Influence on space heating demand by a certain factor (applied before losses in district heating). reduce_space_heat _exogenously_factor,--,Dictionary with planning horizons as keys.,"A positive factor can mean renovation or demolition of a building. If the factor is negative, it can mean an increase in floor area, increased thermal comfort, population growth. The default factors are determined by the `Eurocalc Homes and buildings decarbonization scenario `_" retrofitting,,, --- retro_endogen,--,"{true, false}",Add retrofitting as an endogenous system which co-optimise space heat savings. --- cost_factor,--,float,Weight costs for building renovation --- interest_rate,--,float,The interest rate for investment in building components --- annualise_cost,--,"{true, false}",Annualise the investment costs of retrofitting --- tax_weighting,--,"{true, false}",Weight the costs of retrofitting depending on taxes in countries --- construction_index,--,"{true, false}",Weight the costs of retrofitting depending on labour/material costs per country +#NAME?,--,"{true, false}",Add retrofitting as an endogenous system which co-optimise space heat savings. +#NAME?,--,float,Weight costs for building renovation +#NAME?,--,float,The interest rate for investment in building components +#NAME?,--,"{true, false}",Annualise the investment costs of retrofitting +#NAME?,--,"{true, false}",Weight the costs of retrofitting depending on taxes in countries +#NAME?,--,"{true, false}",Weight the costs of retrofitting depending on labour/material costs per country tes,--,"{true, false}",Add option for storing thermal energy in large water pits associated with district heating systems and individual thermal energy storage (TES) tes_tau,,,The time constant used to calculate the decay of thermal energy in thermal energy storage (TES): 1- :math:`e^{-1/24τ}`. --- decentral,days,float,The time constant in decentralized thermal energy storage (TES) --- central,days,float,The time constant in centralized thermal energy storage (TES) +#NAME?,days,float,The time constant in decentralized thermal energy storage (TES) +#NAME?,days,float,The time constant in centralized thermal energy storage (TES) boilers,--,"{true, false}",Add option for transforming gas into heat using gas boilers resistive_heaters,--,"{true, false}",Add option for transforming electricity into heat using resistive heaters (independently from gas boilers) oil_boilers,--,"{true, false}",Add option for transforming oil into heat using boilers biomass_boiler,--,"{true, false}",Add option for transforming biomass into heat using boilers -overdimension_individual_heating,--,"float",Add option for overdimensioning individual heating systems by a certain factor. This allows them to cover heat demand peaks e.g. 10% higher than those in the data with a setting of 1.1. +overdimension_individual_heating,--,float,Add option for overdimensioning individual heating systems by a certain factor. This allows them to cover heat demand peaks e.g. 10% higher than those in the data with a setting of 1.1. chp,--,"{true, false}",Add option for using Combined Heat and Power (CHP) micro_chp,--,"{true, false}",Add option for using Combined Heat and Power (CHP) for decentral areas. solar_thermal,--,"{true, false}",Add option for using solar thermal to generate heat. @@ -89,12 +97,12 @@ SMR CC,--,"{true, false}",Add option for transforming natural gas into hydrogen regional_methanol_demand,--,"{true, false}",Spatially resolve methanol demand. Set to true if regional CO2 constraints needed. regional_oil_demand,--,"{true, false}",Spatially resolve oil demand. Set to true if regional CO2 constraints needed. regional_co2 _sequestration_potential,,, --- enable,--,"{true, false}",Add option for regionally-resolved geological carbon dioxide sequestration potentials based on `CO2StoP `_. --- attribute,--,string or list,Name (or list of names) of the attribute(s) for the sequestration potential --- include_onshore,--,"{true, false}",Add options for including onshore sequestration potentials --- min_size,Gt ,float,Any sites with lower potential than this value will be excluded --- max_size,Gt ,float,The maximum sequestration potential for any one site. --- years_of_storage,years,float,The years until potential exhausted at optimised annual rate +#NAME?,--,"{true, false}",Add option for regionally-resolved geological carbon dioxide sequestration potentials based on `CO2StoP `_. +#NAME?,--,string or list,Name (or list of names) of the attribute(s) for the sequestration potential +#NAME?,--,"{true, false}",Add options for including onshore sequestration potentials +#NAME?,Gt ,float,Any sites with lower potential than this value will be excluded +#NAME?,Gt ,float,The maximum sequestration potential for any one site. +#NAME?,years,float,The years until potential exhausted at optimised annual rate co2_sequestration_potential,MtCO2/a,float,The potential of sequestering CO2 in Europe per year co2_sequestration_cost,currency/tCO2,float,The cost of sequestering a ton of CO2 co2_sequestration_lifetime,years,int,The lifetime of a CO2 sequestration site @@ -121,9 +129,9 @@ electricity_distribution _grid_cost_factor,,,Multiplies the investment cost of t electricity_grid _connection,--,"{true, false}",Add the cost of electricity grid connection for onshore wind and solar transmission_efficiency,,,Section to specify transmission losses or compression energy demands of bidirectional links. Splits them into two capacity-linked unidirectional links. -- {carrier},--,str,The carrier of the link. --- -- efficiency_static,p.u.,float,Length-independent transmission efficiency. --- -- efficiency_per_1000km,p.u. per 1000 km,float,Length-dependent transmission efficiency ($\eta^{\text{length}}$) --- -- compression_per_1000km,p.u. per 1000 km,float,Length-dependent electricity demand for compression ($\eta \cdot \text{length}$) implemented as multi-link to local electricity bus. +#NAME?,p.u.,float,Length-independent transmission efficiency. +#NAME?,p.u. per 1000 km,float,Length-dependent transmission efficiency ($\eta^{\text{length}}$) +#NAME?,p.u. per 1000 km,float,Length-dependent electricity demand for compression ($\eta \cdot \text{length}$) implemented as multi-link to local electricity bus. H2_network,--,"{true, false}",Add option for new hydrogen pipelines gas_network,--,"{true, false}","Add existing natural gas infrastructure, incl. LNG terminals, production and entry-points. The existing gas network is added with a lossless transport model. A length-weighted `k-edge augmentation algorithm `_ can be run to add new candidate gas pipelines such that all regions of the model can be connected to the gas network. When activated, all the gas demands are regionally disaggregated as well." H2_retrofit,--,"{true, false}",Add option for retrofiting existing pipelines to transport hydrogen. @@ -139,17 +147,17 @@ conventional_generation,,,Add a more detailed description of conventional carrie biomass_to_liquid,--,"{true, false}",Add option for transforming solid biomass into liquid fuel with the same properties as oil biosng,--,"{true, false}",Add option for transforming solid biomass into synthesis gas with the same properties as natural gas limit_max_growth,,, --- enable,--,"{true, false}",Add option to limit the maximum growth of a carrier --- factor,p.u.,float,The maximum growth factor of a carrier (e.g. 1.3 allows 30% larger than max historic growth) --- max_growth,,, +#NAME?,--,"{true, false}",Add option to limit the maximum growth of a carrier +#NAME?,p.u.,float,The maximum growth factor of a carrier (e.g. 1.3 allows 30% larger than max historic growth) +#NAME?,,, -- -- {carrier},GW,float,The historic maximum growth of a carrier --- max_relative_growth,,, +#NAME?,,, -- -- {carrier},p.u.,float,The historic maximum relative growth of a carrier ,,, enhanced_geothermal,,, --- enable,--,"{true, false}",Add option to include Enhanced Geothermal Systems --- flexible,--,"{true, false}",Add option for flexible operation (see Ricks et al. 2024) --- max_hours,--,int,The maximum hours the reservoir can be charged under flexible operation --- max_boost,--,float,The maximum boost in power output under flexible operation --- var_cf,--,"{true, false}",Add option for variable capacity factor (see Ricks et al. 2024) --- sustainability_factor,--,float,Share of sourced heat that is replenished by the earth's core (see details in `build_egs_potentials.py `_) +#NAME?,--,"{true, false}",Add option to include Enhanced Geothermal Systems +#NAME?,--,"{true, false}",Add option for flexible operation (see Ricks et al. 2024) +#NAME?,--,int,The maximum hours the reservoir can be charged under flexible operation +#NAME?,--,float,The maximum boost in power output under flexible operation +#NAME?,--,"{true, false}",Add option for variable capacity factor (see Ricks et al. 2024) +#NAME?,--,float,Share of sourced heat that is replenished by the earth's core (see details in `build_egs_potentials.py `_) \ No newline at end of file From 3fac27ff27317246bd613652802477d4652bbc95 Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Wed, 24 Jul 2024 15:03:44 +0200 Subject: [PATCH 052/344] use module structure --- rules/build_sector.smk | 10 +- .../build_cop_profiles/BaseCopApproximator.py | 61 ++++++ .../CentralHeatingCopApproximator.py} | 184 ++++-------------- .../DecentralHeatingCopApproximator.py | 79 ++++++++ scripts/build_cop_profiles/__main__.py | 42 ++++ 5 files changed, 228 insertions(+), 148 deletions(-) create mode 100644 scripts/build_cop_profiles/BaseCopApproximator.py rename scripts/{build_cop_profiles.py => build_cop_profiles/CentralHeatingCopApproximator.py} (58%) create mode 100644 scripts/build_cop_profiles/DecentralHeatingCopApproximator.py create mode 100644 scripts/build_cop_profiles/__main__.py diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 510af7c04..278bff48e 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -220,16 +220,16 @@ rule build_cop_profiles: heat_pump_sink_T_decentral_heating=config_provider( "sector", "heat_pump_sink_T_individual_heating" ), - forward_temperature_district_heating=config_provider( + forward_temperature_central_heating=config_provider( "sector", "district_heating", "forward_temperature" ), - return_temperature_district_heating=config_provider( + return_temperature_central_heating=config_provider( "sector", "district_heating", "return_temperature" ), - heat_source_cooling_district_heating=config_provider( + heat_source_cooling_central_heating=config_provider( "sector", "district_heating", "heat_source_cooling" ), - heat_pump_cop_approximation_district_heating=config_provider( + heat_pump_cop_approximation_central_heating=config_provider( "sector", "district_heating", "heat_pump_cop_approximation" ), input: @@ -257,7 +257,7 @@ rule build_cop_profiles: conda: "../envs/environment.yaml" script: - "../scripts/build_cop_profiles.py" + "../scripts/build_cop_profiles/__main__.py" def solar_thermal_cutout(wildcards): diff --git a/scripts/build_cop_profiles/BaseCopApproximator.py b/scripts/build_cop_profiles/BaseCopApproximator.py new file mode 100644 index 000000000..87343d368 --- /dev/null +++ b/scripts/build_cop_profiles/BaseCopApproximator.py @@ -0,0 +1,61 @@ + +from abc import ABC, abstractmethod +from typing import Union +import xarray as xr +import numpy as np + +class BaseCopApproximator(ABC): + """ + Abstract class for approximating the coefficient of performance (COP) of a heat pump.""" + def __init__( + self, + forward_temperature_celsius: Union[xr.DataArray, np.array], + source_inlet_temperature_celsius: Union[xr.DataArray, np.array], + ): + """ + Initialize CopApproximator. + + Parameters: + ---------- + forward_temperature_celsius : Union[xr.DataArray, np.array] + The forward temperature in Celsius. + return_temperature_celsius : Union[xr.DataArray, np.array] + The return temperature in Celsius. + """ + pass + + @abstractmethod + def approximate_cop(self) -> Union[xr.DataArray, np.array]: + """Approximate heat pump coefficient of performance (COP). + + Returns: + ------- + Union[xr.DataArray, np.array] + The calculated COP values. + """ + pass + + def celsius_to_kelvin(t_celsius: Union[float, xr.DataArray, np.array]) -> Union[float, xr.DataArray, np.array]: + if (np.asarray(t_celsius) > 200).any(): + raise ValueError("t_celsius > 200. Are you sure you are using the right units?") + return t_celsius + 273.15 + + + def logarithmic_mean(t_hot: Union[float, xr.DataArray, np.ndarray], t_cold: Union[float, xr.DataArray, np.ndarray]) -> Union[float, xr.DataArray, np.ndarray]: + if (np.asarray(t_hot <= t_cold)).any(): + raise ValueError("t_hot must be greater than t_cold") + return (t_hot - t_cold) / np.log(t_hot / t_cold) + + @staticmethod + def celsius_to_kelvin(t_celsius: Union[float, xr.DataArray, np.array]) -> Union[float, xr.DataArray, np.array]: + if (np.asarray(t_celsius) > 200).any(): + raise ValueError("t_celsius > 200. Are you sure you are using the right units?") + return t_celsius + 273.15 + + @staticmethod + def logarithmic_mean(t_hot: Union[float, xr.DataArray, np.ndarray], t_cold: Union[float, xr.DataArray, np.ndarray]) -> Union[float, xr.DataArray, np.ndarray]: + if (np.asarray(t_hot <= t_cold)).any(): + raise ValueError("t_hot must be greater than t_cold") + return (t_hot - t_cold) / np.log(t_hot / t_cold) + + diff --git a/scripts/build_cop_profiles.py b/scripts/build_cop_profiles/CentralHeatingCopApproximator.py similarity index 58% rename from scripts/build_cop_profiles.py rename to scripts/build_cop_profiles/CentralHeatingCopApproximator.py index 89eeb9b68..9b4454264 100644 --- a/scripts/build_cop_profiles.py +++ b/scripts/build_cop_profiles/CentralHeatingCopApproximator.py @@ -1,78 +1,17 @@ -# -*- coding: utf-8 -*- -# SPDX-FileCopyrightText: : 2020-2024 The PyPSA-Eur Authors -# -# SPDX-License-Identifier: MIT -""" -Build coefficient of performance (COP) time series for air- or ground-sourced -heat pumps. -For individual (decentral) heat pumps, the COP is approximated as a quatratic function of the temperature difference between source and sink, based on Staffell et al. 2012. -For district (central) heating, the COP is approximated based on Jensen et al. 2018 and parameters from Pieper et al. 2020. - -This rule is executed in ``build_sector.smk``. - -Relevant Settings ------------------ - -.. code:: yaml - heat_pump_sink_T: - - -Inputs: -------- -- ``resources//temp_soil_total_elec_s_.nc``: Soil temperature (total) time series. -- ``resources//temp_air_total_elec_s_.nc``: Ambient air temperature (total) time series. - -Outputs: --------- -- ``resources/cop_air_decentral_heating_elec_s_.nc``: COP (air-sourced) time series (decentral heating). -- ``resources/cop_soil_decentral_heating_elec_s_.nc``: COP (ground-sourced) time series (decentral heating). -- ``resources/cop_air_central_heating_elec_s_.nc``: COP (air-sourced) time series (central heating). -- ``resources/cop_soil_central_heating_elec_s_.nc``: COP (ground-sourced) time series (central heating). - - -References ----------- -[1] Staffell et al., Energy & Environmental Science 11 (2012): A review of domestic heat pumps, https://doi.org/10.1039/C2EE22653G. -[2] Jensen et al., Proceedings of the13th IIR-Gustav Lorentzen Conference on Natural Refrigerants (2018): Heat pump COP, part 2: Generalized COP estimation of heat pump processes, https://doi.org/10.18462/iir.gl.2018.1386 -[3] Pieper et al., Energy 205 (2020): Comparison of COP estimation methods for large-scale heat pumps used in energy planning, https://doi.org/10.1016/j.energy.2020.117994 -""" - -from enum import Enum from typing import Union - -import numpy as np import xarray as xr -from _helpers import set_scenario_config - - -def coefficient_of_performance_individual_heating(delta_T, source="air"): - if source == "air": - return 6.81 - 0.121 * delta_T + 0.000630 * delta_T**2 - elif source == "soil": - return 8.77 - 0.150 * delta_T + 0.000734 * delta_T**2 - else: - raise NotImplementedError("'source' must be one of ['air', 'soil']") - - -def celsius_to_kelvin( - t_celsius: Union[float, xr.DataArray, np.array] -) -> Union[float, xr.DataArray, np.array]: - if (np.asarray(t_celsius) > 200).any(): - raise ValueError("t_celsius > 200. Are you sure you are using the right units?") - return t_celsius + 273.15 - - -def logarithmic_mean( - t_hot: Union[float, xr.DataArray, np.ndarray], - t_cold: Union[float, xr.DataArray, np.ndarray], -) -> Union[float, xr.DataArray, np.ndarray]: - if (np.asarray(t_hot <= t_cold)).any(): - raise ValueError("t_hot must be greater than t_cold") - return (t_hot - t_cold) / np.log(t_hot / t_cold) +import numpy as np +from BaseCopApproximator import BaseCopApproximator -class CopDistrictHeating: +class CentralHeatingCopApproximator(BaseCopApproximator): + """ + Approximate the coefficient of performance (COP) for a heat pump in a central heating system (district heating). + + Uses an approximation method proposed by Jensen et al. (2018) and default parameters from Pieper et al. (2020). + The method is based on a thermodynamic heat pump model with some hard-to-know parameters being approximated. + """ def __init__( self, @@ -85,7 +24,6 @@ def __init__( heat_loss: float = 0.0, ) -> None: """ - Initialize the COPProfileBuilder object. Parameters: ---------- @@ -104,17 +42,17 @@ def __init__( heat_loss : float, optional The heat loss, by default 0.0. """ - self.t_source_in = celsius_to_kelvin(source_inlet_temperature_celsius) - self.t_sink_out = celsius_to_kelvin(forward_temperature_celsius) + self.t_source_in_kelvin = BaseCopApproximator.celsius_to_kelvin(source_inlet_temperature_celsius) + self.t_sink_out_kelvin = BaseCopApproximator.celsius_to_kelvin(forward_temperature_celsius) - self.t_sink_in = celsius_to_kelvin(return_temperature_celsius) - self.t_source_out = celsius_to_kelvin(source_outlet_temperature_celsius) + self.t_sink_in_kelvin = BaseCopApproximator.celsius_to_kelvin(return_temperature_celsius) + self.t_source_out = BaseCopApproximator.celsius_to_kelvin(source_outlet_temperature_celsius) - self.isentropic_efficiency_compressor = isentropic_compressor_efficiency + self.isentropic_efficiency_compressor_kelvin = isentropic_compressor_efficiency self.heat_loss = heat_loss self.delta_t_pinch = delta_t_pinch_point - def cop(self) -> Union[xr.DataArray, np.array]: + def approximate_cop(self) -> Union[xr.DataArray, np.array]: """ Calculate the coefficient of performance (COP) for the system. @@ -127,7 +65,7 @@ def cop(self) -> Union[xr.DataArray, np.array]: ( 1 + (self.delta_t_refrigerant_sink + self.delta_t_pinch) - / self.t_sink_mean + / self.t_sink_mean_kelvin ) / ( 1 @@ -139,28 +77,27 @@ def cop(self) -> Union[xr.DataArray, np.array]: / self.delta_t_lift ) ) - * self.isentropic_efficiency_compressor + * self.isentropic_efficiency_compressor_kelvin * (1 - self.ratio_evaporation_compression_work) + 1 - - self.isentropic_efficiency_compressor + - self.isentropic_efficiency_compressor_kelvin - self.heat_loss ) @property - def t_sink_mean(self) -> Union[xr.DataArray, np.array]: + def t_sink_mean_kelvin(self) -> Union[xr.DataArray, np.array]: """ - Calculate the logarithmic mean temperature difference between the cold - and hot sinks. + Calculate the logarithmic mean temperature difference between the cold and hot sinks. Returns ------- Union[xr.DataArray, np.array] The mean temperature difference. """ - return logarithmic_mean(t_cold=self.t_sink_in, t_hot=self.t_sink_out) + return BaseCopApproximator.logarithmic_mean(t_cold=self.t_sink_in_kelvin, t_hot=self.t_sink_out_kelvin) @property - def t_source_mean(self) -> Union[xr.DataArray, np.array]: + def t_source_mean_kelvin(self) -> Union[xr.DataArray, np.array]: """ Calculate the logarithmic mean temperature of the heat source. @@ -169,20 +106,19 @@ def t_source_mean(self) -> Union[xr.DataArray, np.array]: Union[xr.DataArray, np.array] The mean temperature of the heat source. """ - return logarithmic_mean(t_hot=self.t_source_in, t_cold=self.t_source_out) + return BaseCopApproximator.logarithmic_mean(t_hot=self.t_source_in_kelvin, t_cold=self.t_source_out) @property def delta_t_lift(self) -> Union[xr.DataArray, np.array]: """ - Calculate the temperature lift as the difference between the - logarithmic sink and source temperatures. + Calculate the temperature lift as the difference between the logarithmic sink and source temperatures. Returns ------- Union[xr.DataArray, np.array] The temperature difference between the sink and source. """ - return self.t_sink_mean - self.t_source_mean + return self.t_sink_mean_kelvin - self.t_source_mean_kelvin @property def ideal_lorenz_cop(self) -> Union[xr.DataArray, np.array]: @@ -196,14 +132,14 @@ def ideal_lorenz_cop(self) -> Union[xr.DataArray, np.array]: ------- np.array The ideal Lorenz COP. + """ - return self.t_sink_mean / self.delta_t_lift + return self.t_sink_mean_kelvin / self.delta_t_lift @property def delta_t_refrigerant_source(self) -> Union[xr.DataArray, np.array]: """ - Calculate the temperature difference between the refrigerant source - inlet and outlet. + Calculate the temperature difference between the refrigerant source inlet and outlet. Returns ------- @@ -211,14 +147,13 @@ def delta_t_refrigerant_source(self) -> Union[xr.DataArray, np.array]: The temperature difference between the refrigerant source inlet and outlet. """ return self._approximate_delta_t_refrigerant_source( - delta_t_source=self.t_source_in - self.t_source_out + delta_t_source=self.t_source_in_kelvin - self.t_source_out ) @property def delta_t_refrigerant_sink(self) -> Union[xr.DataArray, np.array]: """ - Temperature difference between the refrigerant and the sink based on - approximation. + Temperature difference between the refrigerant and the sink based on approximation. Returns ------- @@ -230,8 +165,7 @@ def delta_t_refrigerant_sink(self) -> Union[xr.DataArray, np.array]: @property def ratio_evaporation_compression_work(self) -> Union[xr.DataArray, np.array]: """ - Calculate the ratio of evaporation to compression work based on - approximation. + Calculate the ratio of evaporation to compression work based on approximation. Returns ------- @@ -239,7 +173,7 @@ def ratio_evaporation_compression_work(self) -> Union[xr.DataArray, np.array]: The calculated ratio of evaporation to compression work. """ return self._ratio_evaporation_compression_work_approximation() - + @property def delta_t_sink(self) -> Union[xr.DataArray, np.array]: """ @@ -250,14 +184,13 @@ def delta_t_sink(self) -> Union[xr.DataArray, np.array]: Union[xr.DataArray, np.array] The temperature difference at the sink. """ - return self.t_sink_out - self.t_sink_in + return self.t_sink_out_kelvin - self.t_sink_in_kelvin def _approximate_delta_t_refrigerant_source( self, delta_t_source: Union[xr.DataArray, np.array] ) -> Union[xr.DataArray, np.array]: """ - Approximates the temperature difference between the refrigerant and the - source. + Approximates the temperature difference between the refrigerant and the source. Parameters ---------- @@ -267,7 +200,7 @@ def _approximate_delta_t_refrigerant_source( Returns ------- Union[xr.DataArray, np.array] - The approximate temperature difference for the refrigerant source. + The approximate temperature difference between the refrigerant and heat source. """ return delta_t_source / 2 @@ -279,7 +212,7 @@ def _approximate_delta_t_refrigerant_sink( c: float = {"ammonia": 0.016, "isobutane": 2.4}, ) -> Union[xr.DataArray, np.array]: """ - Approximates the temperature difference at the refrigerant sink. + Approximates the temperature difference between the refrigerant and heat sink. Parameters: ---------- @@ -295,7 +228,7 @@ def _approximate_delta_t_refrigerant_sink( Returns: ------- Union[xr.DataArray, np.array] - The approximate temperature difference at the refrigerant sink. + The approximate temperature difference between the refrigerant and heat sink. Notes: ------ @@ -303,6 +236,7 @@ def _approximate_delta_t_refrigerant_sink( The approximate temperature difference at the refrigerant sink is calculated using the following formula: a * (t_sink_out - t_source_out + 2 * delta_t_pinch) + b * delta_t_sink + c + """ if refrigerant not in a.keys(): raise ValueError( @@ -310,7 +244,7 @@ def _approximate_delta_t_refrigerant_sink( ) return ( a[refrigerant] - * (self.t_sink_out - self.t_source_out + 2 * self.delta_t_pinch) + * (self.t_sink_out_kelvin - self.t_source_out + 2 * self.delta_t_pinch) + b[refrigerant] * self.delta_t_sink + c[refrigerant] ) @@ -339,7 +273,7 @@ def _ratio_evaporation_compression_work_approximation( Returns: ------- Union[xr.DataArray, np.array] - The calculated ratio of evaporation to compression work. + The approximated ratio of evaporation to compression work. Notes: ------ @@ -354,44 +288,8 @@ def _ratio_evaporation_compression_work_approximation( ) return ( a[refrigerant] - * (self.t_sink_out - self.t_source_out + 2 * self.delta_t_pinch) + * (self.t_sink_out_kelvin - self.t_source_out + 2 * self.delta_t_pinch) + b[refrigerant] * self.delta_t_sink + c[refrigerant] ) - -if __name__ == "__main__": - if "snakemake" not in globals(): - from _helpers import mock_snakemake - - snakemake = mock_snakemake( - "build_cop_profiles", - simpl="", - clusters=48, - ) - - set_scenario_config(snakemake) - - for source in ["air", "soil"]: - source_T = xr.open_dataarray(snakemake.input[f"temp_{source}_total"]) - - delta_T = snakemake.params.heat_pump_sink_T_decentral_heating - source_T - - cop_individual_heating = coefficient_of_performance_individual_heating( - delta_T, source - ) - cop_individual_heating.to_netcdf( - snakemake.output[f"cop_{source}_decentral_heating"] - ) - - cop_district_heating = CopDistrictHeating( - forward_temperature_celsius=snakemake.params.forward_temperature_district_heating, - return_temperature_celsius=snakemake.params.return_temperature_district_heating, - source_inlet_temperature_celsius=source_T, - source_outlet_temperature_celsius=source_T - - snakemake.params.heat_source_cooling_district_heating, - ).cop() - - cop_district_heating.to_netcdf( - snakemake.output[f"cop_{source}_central_heating"] - ) diff --git a/scripts/build_cop_profiles/DecentralHeatingCopApproximator.py b/scripts/build_cop_profiles/DecentralHeatingCopApproximator.py new file mode 100644 index 000000000..03ca63fea --- /dev/null +++ b/scripts/build_cop_profiles/DecentralHeatingCopApproximator.py @@ -0,0 +1,79 @@ + + + +from typing import Union +import xarray as xr +import numpy as np + +from BaseCopApproximator import BaseCopApproximator + +class DecentralHeatingCopApproximator(BaseCopApproximator): + """ + Approximate the coefficient of performance (COP) for a heat pump in a decentral heating system (individual/household heating). + + Uses a quadratic regression on the temperature difference between the source and sink based on empirical data proposed by Staffell et al. 2012 . + + References + ---------- + [1] Staffell et al., Energy & Environmental Science 11 (2012): A review of domestic heat pumps, https://doi.org/10.1039/C2EE22653G. + """ + + def __init__( + self, + forward_temperature_celsius: Union[xr.DataArray, np.array], + source_inlet_temperature_celsius: Union[xr.DataArray, np.array], + source_type: str + ): + """ + Initialize the COPProfileBuilder object. + + Parameters: + ---------- + forward_temperature_celsius : Union[xr.DataArray, np.array] + The forward temperature in Celsius. + return_temperature_celsius : Union[xr.DataArray, np.array] + The return temperature in Celsius. + source: str + The source of the heat pump. Must be either 'air' or 'soil' + """ + + self.delta_t = forward_temperature_celsius - source_inlet_temperature_celsius + if source_type not in ["air", "soil"]: + raise ValueError("'source' must be one of ['air', 'soil']") + else: + self.source_type = source_type + + def approximate_cop(self) -> Union[xr.DataArray, np.array]: + """ + Compute output of quadratic regression for air-/ground-source heat pumps. + + Calls the appropriate method depending on `source`.""" + if self.source_type == "air": + return self._approximate_cop_air_source() + elif self.source_type == "soil": + return self._approximate_cop_ground_source() + + def _approximate_cop_air_source(self) -> Union[xr.DataArray, np.array]: + """ + Evaluate quadratic regression for an air-sourced heat pump. + + COP = 6.81 - 0.121 * delta_T + 0.000630 * delta_T^2 + + Returns + ------- + Union[xr.DataArray, np.array] + The calculated COP values.""" + return 6.81 - 0.121 * self.delta_t + 0.000630 * self.delta_t**2 + + def _approximate_cop_ground_source(self) -> Union[xr.DataArray, np.array]: + """ + Evaluate quadratic regression for a ground-sourced heat pump. + + COP = 8.77 - 0.150 * delta_T + 0.000734 * delta_T^2 + + Returns + ------- + Union[xr.DataArray, np.array] + The calculated COP values.""" + return 8.77 - 0.150 * self.delta_t + 0.000734 * self.delta_t**2 + \ No newline at end of file diff --git a/scripts/build_cop_profiles/__main__.py b/scripts/build_cop_profiles/__main__.py new file mode 100644 index 000000000..2568476fc --- /dev/null +++ b/scripts/build_cop_profiles/__main__.py @@ -0,0 +1,42 @@ + + +import xarray as xr +import numpy as np +from CentralHeatingCopApproximator import CentralHeatingCopApproximator +from DecentralHeatingCopApproximator import DecentralHeatingCopApproximator +from _helpers import set_scenario_config + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake( + "build_cop_profiles", + simpl="", + clusters=48, + ) + + set_scenario_config(snakemake) + + for source_type in ["air", "soil"]: + # source inlet temperature (air/soil) is based on weather data + source_inlet_temperature_celsius = xr.open_dataarray(snakemake.input[f"temp_{source_type}_total"]) + + # Approximate COP for decentral (individual) heating + cop_individual_heating = DecentralHeatingCopApproximator( + forward_temperature_celsius=snakemake.params.heat_pump_sink_T_decentral_heating, + source_inlet_temperature_celsius=source_inlet_temperature_celsius, + source_type=source_type + ).approximate_cop() + cop_individual_heating.to_netcdf(snakemake.output[f"cop_{source_type}_decentral_heating"]) + + # Approximate COP for central (district) heating + cop_central_heating = CentralHeatingCopApproximator( + forward_temperature_celsius=snakemake.params.forward_temperature_central_heating, + return_temperature_celsius=snakemake.params.return_temperature_central_heating, + source_inlet_temperature_celsius=source_inlet_temperature_celsius, + source_outlet_temperature_celsius=source_inlet_temperature_celsius - snakemake.params.heat_source_cooling_central_heating, + ).approximate_cop() + cop_central_heating.to_netcdf(snakemake.output[f"cop_{source_type}_central_heating"]) + + From a9f4c6375dc5cd10fcdc3a71912cc7b5288d7516 Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Wed, 24 Jul 2024 15:04:14 +0200 Subject: [PATCH 053/344] change default DH return temperature to 50C --- config/config.default.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 7e2f7077a..cace1f4c4 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -420,7 +420,7 @@ sector: district_heating_loss: 0.15 # check these numbers! forward_temperature: 90 #C - return_temperature: 60 #C + return_temperature: 50 #C heat_source_cooling: 6 #K heat_pump_cop_approximation: refrigerant: ammonia From 0470f119cf34cc0b3d1cc17559a0f719f6b0e1ee Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 24 Jul 2024 15:31:46 +0200 Subject: [PATCH 054/344] base_network: use GeoSeries.voronoi_polygons instead of custom solution (#1172) * base_network: use GeoSeries.voronoi_polygons instead of custom solution * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * base_network: move voronoi calculations into separate function * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * base_network: fix typo in function voronoi() * base_network: refine voronoi function * add release note [no ci] --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- doc/release_notes.rst | 3 ++ scripts/base_network.py | 80 ++++++++++++----------------------------- 2 files changed, 26 insertions(+), 57 deletions(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 0aa97e5a6..fa0473fc3 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -55,6 +55,9 @@ Upcoming Release excess power) at every AC bus. This can speed up the solving process as the curtailment decision is aggregated into a single generator per region. +* In :mod:`base_network`, replace own voronoi polygon calculation function with + Geopandas `gdf.voronoi_polygons` method. + PyPSA-Eur 0.11.0 (25th May 2024) ===================================== diff --git a/scripts/base_network.py b/scripts/base_network.py index f87d666b1..118e7dba3 100644 --- a/scripts/base_network.py +++ b/scripts/base_network.py @@ -72,6 +72,7 @@ """ import logging +import warnings from itertools import product import geopandas as gpd @@ -85,9 +86,9 @@ import yaml from _helpers import REGION_COLS, configure_logging, get_snapshots, set_scenario_config from packaging.version import Version, parse -from scipy import spatial from scipy.sparse import csgraph -from shapely.geometry import LineString, Point, Polygon +from scipy.spatial import KDTree +from shapely.geometry import LineString, Point PD_GE_2_2 = parse(pd.__version__) >= Version("2.2") @@ -118,7 +119,7 @@ def _find_closest_links(links, new_links, distance_upper_bound=1.5): querycoords = np.vstack( [new_links[["x1", "y1", "x2", "y2"]], new_links[["x2", "y2", "x1", "y1"]]] ) - tree = spatial.KDTree(treecoords) + tree = KDTree(treecoords) dist, ind = tree.query(querycoords, distance_upper_bound=distance_upper_bound) found_b = ind < len(links) found_i = np.arange(len(new_links) * 2)[found_b] % len(new_links) @@ -273,7 +274,7 @@ def _add_links_from_tyndp(buses, links, links_tyndp, europe_shape): return buses, links tree_buses = buses.query("carrier=='AC'") - tree = spatial.KDTree(tree_buses[["x", "y"]]) + tree = KDTree(tree_buses[["x", "y"]]) _, ind0 = tree.query(links_tyndp[["x1", "y1"]]) ind0_b = ind0 < len(tree_buses) links_tyndp.loc[ind0_b, "bus0"] = tree_buses.index[ind0[ind0_b]] @@ -788,59 +789,26 @@ def base_network( return n -def voronoi_partition_pts(points, outline): +def voronoi(points, outline, crs=4326): """ - Compute the polygons of a voronoi partition of `points` within the polygon - `outline`. Taken from - https://github.com/FRESNA/vresutils/blob/master/vresutils/graph.py. - - Attributes - ---------- - points : Nx2 - ndarray[dtype=float] - outline : Polygon - Returns - ------- - polygons : N - ndarray[dtype=Polygon|MultiPolygon] + Create Voronoi polygons from a set of points within an outline. """ - points = np.asarray(points) - - if len(points) == 1: - polygons = [outline] - else: - xmin, ymin = np.amin(points, axis=0) - xmax, ymax = np.amax(points, axis=0) - xspan = xmax - xmin - yspan = ymax - ymin - - # to avoid any network positions outside all Voronoi cells, append - # the corners of a rectangle framing these points - vor = spatial.Voronoi( - np.vstack( - ( - points, - [ - [xmin - 3.0 * xspan, ymin - 3.0 * yspan], - [xmin - 3.0 * xspan, ymax + 3.0 * yspan], - [xmax + 3.0 * xspan, ymin - 3.0 * yspan], - [xmax + 3.0 * xspan, ymax + 3.0 * yspan], - ], - ) - ) - ) - - polygons = [] - for i in range(len(points)): - poly = Polygon(vor.vertices[vor.regions[vor.point_region[i]]]) - - if not poly.is_valid: - poly = poly.buffer(0) - - with np.errstate(invalid="ignore"): - poly = poly.intersection(outline) + pts = gpd.GeoSeries( + gpd.points_from_xy(points.x, points.y), + index=points.index, + crs=crs, + ) + voronoi = pts.voronoi_polygons(extend_to=outline).clip(outline) - polygons.append(poly) + # can be removed with shapely 2.1 where order is preserved + # https://github.com/shapely/shapely/issues/2020 + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=UserWarning) + pts = gpd.GeoDataFrame(geometry=pts) + voronoi = gpd.GeoDataFrame(geometry=voronoi) + joined = gpd.sjoin_nearest(pts, voronoi, how="right") - return polygons + return joined.dissolve(by="Bus").squeeze() def build_bus_shapes(n, country_shapes, offshore_shapes, countries): @@ -870,9 +838,7 @@ def build_bus_shapes(n, country_shapes, offshore_shapes, countries): "name": onshore_locs.index, "x": onshore_locs["x"], "y": onshore_locs["y"], - "geometry": voronoi_partition_pts( - onshore_locs.values, onshore_shape - ), + "geometry": voronoi(onshore_locs, onshore_shape), "country": country, }, crs=n.crs, @@ -888,7 +854,7 @@ def build_bus_shapes(n, country_shapes, offshore_shapes, countries): "name": offshore_locs.index, "x": offshore_locs["x"], "y": offshore_locs["y"], - "geometry": voronoi_partition_pts(offshore_locs.values, offshore_shape), + "geometry": voronoi(offshore_locs, offshore_shape), "country": country, }, crs=n.crs, From eb2911cb9300cfaeba3fa5e579f7f6513481af60 Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Wed, 24 Jul 2024 15:31:48 +0200 Subject: [PATCH 055/344] rename __main__ to run --- scripts/build_cop_profiles/{__main__.py => run.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename scripts/build_cop_profiles/{__main__.py => run.py} (100%) diff --git a/scripts/build_cop_profiles/__main__.py b/scripts/build_cop_profiles/run.py similarity index 100% rename from scripts/build_cop_profiles/__main__.py rename to scripts/build_cop_profiles/run.py From a4847a10ce9fb3246e3cff1d2225c3911971910f Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Wed, 24 Jul 2024 15:40:21 +0200 Subject: [PATCH 056/344] update build_sector.smk --- rules/build_sector.smk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 278bff48e..662773f7b 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -257,7 +257,7 @@ rule build_cop_profiles: conda: "../envs/environment.yaml" script: - "../scripts/build_cop_profiles/__main__.py" + "../scripts/build_cop_profiles/run.py" def solar_thermal_cutout(wildcards): From 8d6feb6a66860b861aec2183b221e30aff185d3e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 24 Jul 2024 15:04:58 +0000 Subject: [PATCH 057/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- doc/configtables/sector.csv | 2 +- .../build_cop_profiles/BaseCopApproximator.py | 76 ++++++++++++------- .../CentralHeatingCopApproximator.py | 67 ++++++++++------ .../DecentralHeatingCopApproximator.py | 43 ++++++----- scripts/build_cop_profiles/run.py | 26 ++++--- 5 files changed, 132 insertions(+), 82 deletions(-) diff --git a/doc/configtables/sector.csv b/doc/configtables/sector.csv index bb74035ee..de2873edd 100644 --- a/doc/configtables/sector.csv +++ b/doc/configtables/sector.csv @@ -160,4 +160,4 @@ enhanced_geothermal,,, #NAME?,--,int,The maximum hours the reservoir can be charged under flexible operation #NAME?,--,float,The maximum boost in power output under flexible operation #NAME?,--,"{true, false}",Add option for variable capacity factor (see Ricks et al. 2024) -#NAME?,--,float,Share of sourced heat that is replenished by the earth's core (see details in `build_egs_potentials.py `_) \ No newline at end of file +#NAME?,--,float,Share of sourced heat that is replenished by the earth's core (see details in `build_egs_potentials.py `_) diff --git a/scripts/build_cop_profiles/BaseCopApproximator.py b/scripts/build_cop_profiles/BaseCopApproximator.py index 87343d368..247ff0fea 100644 --- a/scripts/build_cop_profiles/BaseCopApproximator.py +++ b/scripts/build_cop_profiles/BaseCopApproximator.py @@ -1,32 +1,39 @@ +# -*- coding: utf-8 -*- from abc import ABC, abstractmethod from typing import Union -import xarray as xr + import numpy as np +import xarray as xr + class BaseCopApproximator(ABC): """ - Abstract class for approximating the coefficient of performance (COP) of a heat pump.""" + Abstract class for approximating the coefficient of performance (COP) of a + heat pump. + """ + def __init__( - self, - forward_temperature_celsius: Union[xr.DataArray, np.array], - source_inlet_temperature_celsius: Union[xr.DataArray, np.array], - ): - """ - Initialize CopApproximator. - - Parameters: - ---------- - forward_temperature_celsius : Union[xr.DataArray, np.array] - The forward temperature in Celsius. - return_temperature_celsius : Union[xr.DataArray, np.array] - The return temperature in Celsius. - """ - pass - + self, + forward_temperature_celsius: Union[xr.DataArray, np.array], + source_inlet_temperature_celsius: Union[xr.DataArray, np.array], + ): + """ + Initialize CopApproximator. + + Parameters: + ---------- + forward_temperature_celsius : Union[xr.DataArray, np.array] + The forward temperature in Celsius. + return_temperature_celsius : Union[xr.DataArray, np.array] + The return temperature in Celsius. + """ + pass + @abstractmethod def approximate_cop(self) -> Union[xr.DataArray, np.array]: - """Approximate heat pump coefficient of performance (COP). + """ + Approximate heat pump coefficient of performance (COP). Returns: ------- @@ -34,28 +41,39 @@ def approximate_cop(self) -> Union[xr.DataArray, np.array]: The calculated COP values. """ pass - - def celsius_to_kelvin(t_celsius: Union[float, xr.DataArray, np.array]) -> Union[float, xr.DataArray, np.array]: + + def celsius_to_kelvin( + t_celsius: Union[float, xr.DataArray, np.array] + ) -> Union[float, xr.DataArray, np.array]: if (np.asarray(t_celsius) > 200).any(): - raise ValueError("t_celsius > 200. Are you sure you are using the right units?") + raise ValueError( + "t_celsius > 200. Are you sure you are using the right units?" + ) return t_celsius + 273.15 - - def logarithmic_mean(t_hot: Union[float, xr.DataArray, np.ndarray], t_cold: Union[float, xr.DataArray, np.ndarray]) -> Union[float, xr.DataArray, np.ndarray]: + def logarithmic_mean( + t_hot: Union[float, xr.DataArray, np.ndarray], + t_cold: Union[float, xr.DataArray, np.ndarray], + ) -> Union[float, xr.DataArray, np.ndarray]: if (np.asarray(t_hot <= t_cold)).any(): raise ValueError("t_hot must be greater than t_cold") return (t_hot - t_cold) / np.log(t_hot / t_cold) @staticmethod - def celsius_to_kelvin(t_celsius: Union[float, xr.DataArray, np.array]) -> Union[float, xr.DataArray, np.array]: + def celsius_to_kelvin( + t_celsius: Union[float, xr.DataArray, np.array] + ) -> Union[float, xr.DataArray, np.array]: if (np.asarray(t_celsius) > 200).any(): - raise ValueError("t_celsius > 200. Are you sure you are using the right units?") + raise ValueError( + "t_celsius > 200. Are you sure you are using the right units?" + ) return t_celsius + 273.15 @staticmethod - def logarithmic_mean(t_hot: Union[float, xr.DataArray, np.ndarray], t_cold: Union[float, xr.DataArray, np.ndarray]) -> Union[float, xr.DataArray, np.ndarray]: + def logarithmic_mean( + t_hot: Union[float, xr.DataArray, np.ndarray], + t_cold: Union[float, xr.DataArray, np.ndarray], + ) -> Union[float, xr.DataArray, np.ndarray]: if (np.asarray(t_hot <= t_cold)).any(): raise ValueError("t_hot must be greater than t_cold") return (t_hot - t_cold) / np.log(t_hot / t_cold) - - diff --git a/scripts/build_cop_profiles/CentralHeatingCopApproximator.py b/scripts/build_cop_profiles/CentralHeatingCopApproximator.py index 9b4454264..f2b8d80c1 100644 --- a/scripts/build_cop_profiles/CentralHeatingCopApproximator.py +++ b/scripts/build_cop_profiles/CentralHeatingCopApproximator.py @@ -1,16 +1,21 @@ +# -*- coding: utf-8 -*- from typing import Union -import xarray as xr -import numpy as np +import numpy as np +import xarray as xr from BaseCopApproximator import BaseCopApproximator + class CentralHeatingCopApproximator(BaseCopApproximator): """ - Approximate the coefficient of performance (COP) for a heat pump in a central heating system (district heating). - - Uses an approximation method proposed by Jensen et al. (2018) and default parameters from Pieper et al. (2020). - The method is based on a thermodynamic heat pump model with some hard-to-know parameters being approximated. + Approximate the coefficient of performance (COP) for a heat pump in a + central heating system (district heating). + + Uses an approximation method proposed by Jensen et al. (2018) and + default parameters from Pieper et al. (2020). The method is based on + a thermodynamic heat pump model with some hard-to-know parameters + being approximated. """ def __init__( @@ -42,11 +47,19 @@ def __init__( heat_loss : float, optional The heat loss, by default 0.0. """ - self.t_source_in_kelvin = BaseCopApproximator.celsius_to_kelvin(source_inlet_temperature_celsius) - self.t_sink_out_kelvin = BaseCopApproximator.celsius_to_kelvin(forward_temperature_celsius) + self.t_source_in_kelvin = BaseCopApproximator.celsius_to_kelvin( + source_inlet_temperature_celsius + ) + self.t_sink_out_kelvin = BaseCopApproximator.celsius_to_kelvin( + forward_temperature_celsius + ) - self.t_sink_in_kelvin = BaseCopApproximator.celsius_to_kelvin(return_temperature_celsius) - self.t_source_out = BaseCopApproximator.celsius_to_kelvin(source_outlet_temperature_celsius) + self.t_sink_in_kelvin = BaseCopApproximator.celsius_to_kelvin( + return_temperature_celsius + ) + self.t_source_out = BaseCopApproximator.celsius_to_kelvin( + source_outlet_temperature_celsius + ) self.isentropic_efficiency_compressor_kelvin = isentropic_compressor_efficiency self.heat_loss = heat_loss @@ -87,14 +100,17 @@ def approximate_cop(self) -> Union[xr.DataArray, np.array]: @property def t_sink_mean_kelvin(self) -> Union[xr.DataArray, np.array]: """ - Calculate the logarithmic mean temperature difference between the cold and hot sinks. + Calculate the logarithmic mean temperature difference between the cold + and hot sinks. Returns ------- Union[xr.DataArray, np.array] The mean temperature difference. """ - return BaseCopApproximator.logarithmic_mean(t_cold=self.t_sink_in_kelvin, t_hot=self.t_sink_out_kelvin) + return BaseCopApproximator.logarithmic_mean( + t_cold=self.t_sink_in_kelvin, t_hot=self.t_sink_out_kelvin + ) @property def t_source_mean_kelvin(self) -> Union[xr.DataArray, np.array]: @@ -106,12 +122,15 @@ def t_source_mean_kelvin(self) -> Union[xr.DataArray, np.array]: Union[xr.DataArray, np.array] The mean temperature of the heat source. """ - return BaseCopApproximator.logarithmic_mean(t_hot=self.t_source_in_kelvin, t_cold=self.t_source_out) + return BaseCopApproximator.logarithmic_mean( + t_hot=self.t_source_in_kelvin, t_cold=self.t_source_out + ) @property def delta_t_lift(self) -> Union[xr.DataArray, np.array]: """ - Calculate the temperature lift as the difference between the logarithmic sink and source temperatures. + Calculate the temperature lift as the difference between the + logarithmic sink and source temperatures. Returns ------- @@ -132,14 +151,14 @@ def ideal_lorenz_cop(self) -> Union[xr.DataArray, np.array]: ------- np.array The ideal Lorenz COP. - """ return self.t_sink_mean_kelvin / self.delta_t_lift @property def delta_t_refrigerant_source(self) -> Union[xr.DataArray, np.array]: """ - Calculate the temperature difference between the refrigerant source inlet and outlet. + Calculate the temperature difference between the refrigerant source + inlet and outlet. Returns ------- @@ -153,7 +172,8 @@ def delta_t_refrigerant_source(self) -> Union[xr.DataArray, np.array]: @property def delta_t_refrigerant_sink(self) -> Union[xr.DataArray, np.array]: """ - Temperature difference between the refrigerant and the sink based on approximation. + Temperature difference between the refrigerant and the sink based on + approximation. Returns ------- @@ -165,7 +185,8 @@ def delta_t_refrigerant_sink(self) -> Union[xr.DataArray, np.array]: @property def ratio_evaporation_compression_work(self) -> Union[xr.DataArray, np.array]: """ - Calculate the ratio of evaporation to compression work based on approximation. + Calculate the ratio of evaporation to compression work based on + approximation. Returns ------- @@ -173,7 +194,7 @@ def ratio_evaporation_compression_work(self) -> Union[xr.DataArray, np.array]: The calculated ratio of evaporation to compression work. """ return self._ratio_evaporation_compression_work_approximation() - + @property def delta_t_sink(self) -> Union[xr.DataArray, np.array]: """ @@ -190,7 +211,8 @@ def _approximate_delta_t_refrigerant_source( self, delta_t_source: Union[xr.DataArray, np.array] ) -> Union[xr.DataArray, np.array]: """ - Approximates the temperature difference between the refrigerant and the source. + Approximates the temperature difference between the refrigerant and the + source. Parameters ---------- @@ -212,7 +234,8 @@ def _approximate_delta_t_refrigerant_sink( c: float = {"ammonia": 0.016, "isobutane": 2.4}, ) -> Union[xr.DataArray, np.array]: """ - Approximates the temperature difference between the refrigerant and heat sink. + Approximates the temperature difference between the refrigerant and + heat sink. Parameters: ---------- @@ -236,7 +259,6 @@ def _approximate_delta_t_refrigerant_sink( The approximate temperature difference at the refrigerant sink is calculated using the following formula: a * (t_sink_out - t_source_out + 2 * delta_t_pinch) + b * delta_t_sink + c - """ if refrigerant not in a.keys(): raise ValueError( @@ -292,4 +314,3 @@ def _ratio_evaporation_compression_work_approximation( + b[refrigerant] * self.delta_t_sink + c[refrigerant] ) - diff --git a/scripts/build_cop_profiles/DecentralHeatingCopApproximator.py b/scripts/build_cop_profiles/DecentralHeatingCopApproximator.py index 03ca63fea..b49be54c0 100644 --- a/scripts/build_cop_profiles/DecentralHeatingCopApproximator.py +++ b/scripts/build_cop_profiles/DecentralHeatingCopApproximator.py @@ -1,16 +1,18 @@ - +# -*- coding: utf-8 -*- from typing import Union -import xarray as xr -import numpy as np +import numpy as np +import xarray as xr from BaseCopApproximator import BaseCopApproximator + class DecentralHeatingCopApproximator(BaseCopApproximator): """ - Approximate the coefficient of performance (COP) for a heat pump in a decentral heating system (individual/household heating). - + Approximate the coefficient of performance (COP) for a heat pump in a + decentral heating system (individual/household heating). + Uses a quadratic regression on the temperature difference between the source and sink based on empirical data proposed by Staffell et al. 2012 . References @@ -22,7 +24,7 @@ def __init__( self, forward_temperature_celsius: Union[xr.DataArray, np.array], source_inlet_temperature_celsius: Union[xr.DataArray, np.array], - source_type: str + source_type: str, ): """ Initialize the COPProfileBuilder object. @@ -36,18 +38,20 @@ def __init__( source: str The source of the heat pump. Must be either 'air' or 'soil' """ - + self.delta_t = forward_temperature_celsius - source_inlet_temperature_celsius if source_type not in ["air", "soil"]: raise ValueError("'source' must be one of ['air', 'soil']") else: self.source_type = source_type - + def approximate_cop(self) -> Union[xr.DataArray, np.array]: """ - Compute output of quadratic regression for air-/ground-source heat pumps. - - Calls the appropriate method depending on `source`.""" + Compute output of quadratic regression for air-/ground-source heat + pumps. + + Calls the appropriate method depending on `source`. + """ if self.source_type == "air": return self._approximate_cop_air_source() elif self.source_type == "soil": @@ -56,24 +60,25 @@ def approximate_cop(self) -> Union[xr.DataArray, np.array]: def _approximate_cop_air_source(self) -> Union[xr.DataArray, np.array]: """ Evaluate quadratic regression for an air-sourced heat pump. - + COP = 6.81 - 0.121 * delta_T + 0.000630 * delta_T^2 - + Returns ------- Union[xr.DataArray, np.array] - The calculated COP values.""" + The calculated COP values. + """ return 6.81 - 0.121 * self.delta_t + 0.000630 * self.delta_t**2 - + def _approximate_cop_ground_source(self) -> Union[xr.DataArray, np.array]: """ Evaluate quadratic regression for a ground-sourced heat pump. - + COP = 8.77 - 0.150 * delta_T + 0.000734 * delta_T^2 - + Returns ------- Union[xr.DataArray, np.array] - The calculated COP values.""" + The calculated COP values. + """ return 8.77 - 0.150 * self.delta_t + 0.000734 * self.delta_t**2 - \ No newline at end of file diff --git a/scripts/build_cop_profiles/run.py b/scripts/build_cop_profiles/run.py index 2568476fc..c25b80a3f 100644 --- a/scripts/build_cop_profiles/run.py +++ b/scripts/build_cop_profiles/run.py @@ -1,10 +1,11 @@ +# -*- coding: utf-8 -*- -import xarray as xr import numpy as np +import xarray as xr +from _helpers import set_scenario_config from CentralHeatingCopApproximator import CentralHeatingCopApproximator from DecentralHeatingCopApproximator import DecentralHeatingCopApproximator -from _helpers import set_scenario_config if __name__ == "__main__": if "snakemake" not in globals(): @@ -19,24 +20,29 @@ set_scenario_config(snakemake) for source_type in ["air", "soil"]: - # source inlet temperature (air/soil) is based on weather data - source_inlet_temperature_celsius = xr.open_dataarray(snakemake.input[f"temp_{source_type}_total"]) + # source inlet temperature (air/soil) is based on weather data + source_inlet_temperature_celsius = xr.open_dataarray( + snakemake.input[f"temp_{source_type}_total"] + ) # Approximate COP for decentral (individual) heating cop_individual_heating = DecentralHeatingCopApproximator( forward_temperature_celsius=snakemake.params.heat_pump_sink_T_decentral_heating, source_inlet_temperature_celsius=source_inlet_temperature_celsius, - source_type=source_type + source_type=source_type, ).approximate_cop() - cop_individual_heating.to_netcdf(snakemake.output[f"cop_{source_type}_decentral_heating"]) + cop_individual_heating.to_netcdf( + snakemake.output[f"cop_{source_type}_decentral_heating"] + ) # Approximate COP for central (district) heating cop_central_heating = CentralHeatingCopApproximator( forward_temperature_celsius=snakemake.params.forward_temperature_central_heating, return_temperature_celsius=snakemake.params.return_temperature_central_heating, source_inlet_temperature_celsius=source_inlet_temperature_celsius, - source_outlet_temperature_celsius=source_inlet_temperature_celsius - snakemake.params.heat_source_cooling_central_heating, + source_outlet_temperature_celsius=source_inlet_temperature_celsius + - snakemake.params.heat_source_cooling_central_heating, ).approximate_cop() - cop_central_heating.to_netcdf(snakemake.output[f"cop_{source_type}_central_heating"]) - - + cop_central_heating.to_netcdf( + snakemake.output[f"cop_{source_type}_central_heating"] + ) From 0c2f9b5daa197c425b1fca4c3df901fb1b407259 Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Wed, 24 Jul 2024 17:05:04 +0200 Subject: [PATCH 058/344] remove rural/urban temperature --- rules/build_sector.smk | 2 -- 1 file changed, 2 deletions(-) diff --git a/rules/build_sector.smk b/rules/build_sector.smk index a5ecbd6de..790ebc7ce 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -1041,8 +1041,6 @@ rule prepare_sector_network: ), temp_soil_total=resources("temp_soil_total_elec_s{simpl}_{clusters}.nc"), temp_air_total=resources("temp_air_total_elec_s{simpl}_{clusters}.nc"), - temp_air_rural=resources("temp_air_rural_elec_s{simpl}_{clusters}.nc"), - temp_air_urban=resources("temp_air_urban_elec_s{simpl}_{clusters}.nc"), cop_soil_decentral_heating=resources( "cop_soil_decentral_heating_elec_s{simpl}_{clusters}.nc" ), From 368971f2a6c9e47d3685f7adc8bcfcc236d2cab7 Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Wed, 24 Jul 2024 17:19:42 +0200 Subject: [PATCH 059/344] remove cop_total input from prepare_sector_network --- rules/build_sector.smk | 2 -- 1 file changed, 2 deletions(-) diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 790ebc7ce..5f8d58176 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -1053,8 +1053,6 @@ rule prepare_sector_network: cop_soil_central_heating=resources( "cop_soil_central_heating_elec_s{simpl}_{clusters}.nc" ), - cop_soil_total=resources("cop_soil_total_elec_s{simpl}_{clusters}.nc"), - cop_air_total=resources("cop_air_total_elec_s{simpl}_{clusters}.nc"), solar_thermal_total=lambda w: ( resources("solar_thermal_total_elec_s{simpl}_{clusters}.nc") if config_provider("sector", "solar_thermal")(w) From 9b7831974fdefa5b89ab54bf2a537d192129bdc7 Mon Sep 17 00:00:00 2001 From: toniseibold Date: Mon, 15 Jul 2024 13:12:04 +0200 Subject: [PATCH 060/344] excluding emissions from sector ratios development [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci just changing the fillna function makes the whole PR cleaner --- scripts/build_industry_sector_ratios_intermediate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/build_industry_sector_ratios_intermediate.py b/scripts/build_industry_sector_ratios_intermediate.py index 5fe042abe..ebbabdb2e 100644 --- a/scripts/build_industry_sector_ratios_intermediate.py +++ b/scripts/build_industry_sector_ratios_intermediate.py @@ -129,11 +129,12 @@ def build_industry_sector_ratios_intermediate(): ] today_sector_ratios_ct.loc[:, ~missing_mask] = today_sector_ratios_ct.loc[ :, ~missing_mask - ].fillna(0) + ].fillna(future_sector_ratios) intermediate_sector_ratios[ct] = ( today_sector_ratios_ct * (1 - fraction_future) + future_sector_ratios * fraction_future ) + intermediate_sector_ratios = pd.concat(intermediate_sector_ratios, axis=1) intermediate_sector_ratios.to_csv(snakemake.output.industry_sector_ratios) From b80bb7bb7a05c3024b4b4d123be3cdffe6ffcf4b Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Thu, 25 Jul 2024 14:33:14 +0200 Subject: [PATCH 061/344] update base year COPs --- scripts/add_existing_baseyear.py | 83 ++++++++++++++++++-------------- 1 file changed, 48 insertions(+), 35 deletions(-) diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index d53336756..3d4debea9 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -419,11 +419,11 @@ def add_heating_capacities_installed_before_baseyear( n, baseyear, grouping_years, - ashp_cop, - gshp_cop, - time_dep_hp_cop, + cop: dict, + time_dep_hp_cop: bool, costs, default_lifetime, + existing_heating: pd.DataFrame ): """ Parameters @@ -435,13 +435,13 @@ def add_heating_capacities_installed_before_baseyear( currently assumed heating capacities split between residential and services proportional to heating load in both 50% capacities in rural buses 50% in urban buses + cop: dict + Dictionary with time-dependent coefficients of perforamnce (COPs) for air and ground heat pumps as values and keys "air decentral", "ground decentral", "air central", "ground central" + time_dep_hp_cop: bool + If True, time-dependent (dynamic) COPs are used for heat pumps """ logger.debug(f"Adding heating capacities installed before {baseyear}") - existing_heating = pd.read_csv( - snakemake.input.existing_heating_distribution, header=[0, 1], index_col=0 - ) - for name in existing_heating.columns.get_level_values(0).unique(): name_type = "central" if name == "urban central" else "decentral" @@ -457,12 +457,11 @@ def add_heating_capacities_installed_before_baseyear( # Add heat pumps costs_name = f"decentral {heat_pump_type}-sourced heat pump" - cop = {"air": ashp_cop, "ground": gshp_cop} - - if time_dep_hp_cop: - efficiency = cop[heat_pump_type][nodes] - else: - efficiency = costs.at[costs_name, "efficiency"] + efficiency = ( + cop[f"{heat_pump_type} {name_type}"][nodes] + if options["time_dep_hp_cop"] + else costs.at[costs_name, "efficiency"] + ) too_large_grouping_years = [gy for gy in grouping_years if gy >= int(baseyear)] if too_large_grouping_years: @@ -639,29 +638,43 @@ def add_heating_capacities_installed_before_baseyear( ) if options["heating"]: - time_dep_hp_cop = options["time_dep_hp_cop"] - ashp_cop = ( - xr.open_dataarray(snakemake.input.cop_air_total) - .to_pandas() - .reindex(index=n.snapshots) - ) - gshp_cop = ( - xr.open_dataarray(snakemake.input.cop_soil_total) - .to_pandas() - .reindex(index=n.snapshots) - ) - default_lifetime = snakemake.params.existing_capacities[ - "default_heating_lifetime" - ] + add_heating_capacities_installed_before_baseyear( - n, - baseyear, - grouping_years_heat, - ashp_cop, - gshp_cop, - time_dep_hp_cop, - costs, - default_lifetime, + n=n, + baseyear=baseyear, + grouping_years=grouping_years_heat, + cop={ + "air decentral": xr.open_dataarray( + snakemake.input.cop_air_decentral_heating + ) + .to_pandas() + .reindex(index=n.snapshots), + "ground decentral": xr.open_dataarray( + snakemake.input.cop_soil_decentral_heating + ) + .to_pandas() + .reindex(index=n.snapshots), + "air central": xr.open_dataarray( + snakemake.input.cop_air_central_heating + ) + .to_pandas() + .reindex(index=n.snapshots), + "ground central": xr.open_dataarray( + snakemake.input.cop_soil_central_heating + ) + .to_pandas() + .reindex(index=n.snapshots), + }, + time_dep_hp_cop=options["time_dep_hp_cop"], + costs=costs, + default_lifetime=snakemake.params.existing_capacities[ + "default_heating_lifetime" + ], + existing_heating=pd.read_csv( + snakemake.input.existing_heating_distribution, + header=[0, 1], + index_col=0, + ), ) if options.get("cluster_heat_buses", False): From 744aaf71decd17eb9f5c985103966c0c6afd247f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 25 Jul 2024 12:33:37 +0000 Subject: [PATCH 062/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/add_existing_baseyear.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index 3d4debea9..cc82295da 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -423,7 +423,7 @@ def add_heating_capacities_installed_before_baseyear( time_dep_hp_cop: bool, costs, default_lifetime, - existing_heating: pd.DataFrame + existing_heating: pd.DataFrame, ): """ Parameters @@ -458,10 +458,10 @@ def add_heating_capacities_installed_before_baseyear( costs_name = f"decentral {heat_pump_type}-sourced heat pump" efficiency = ( - cop[f"{heat_pump_type} {name_type}"][nodes] - if options["time_dep_hp_cop"] - else costs.at[costs_name, "efficiency"] - ) + cop[f"{heat_pump_type} {name_type}"][nodes] + if options["time_dep_hp_cop"] + else costs.at[costs_name, "efficiency"] + ) too_large_grouping_years = [gy for gy in grouping_years if gy >= int(baseyear)] if too_large_grouping_years: From d297f84ea7e3ff9f19d4a2da627c0d3f25192d34 Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Thu, 25 Jul 2024 14:50:27 +0200 Subject: [PATCH 063/344] update inputs for add_existing_baseyear --- rules/solve_myopic.smk | 14 ++++++++++++-- rules/solve_perfect.smk | 14 ++++++++++++-- scripts/add_existing_baseyear.py | 4 ++-- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/rules/solve_myopic.smk b/rules/solve_myopic.smk index 21fb7169b..8d3230ada 100644 --- a/rules/solve_myopic.smk +++ b/rules/solve_myopic.smk @@ -21,8 +21,18 @@ rule add_existing_baseyear: config_provider("scenario", "planning_horizons", 0)(w) ) ), - cop_soil_total=resources("cop_soil_total_elec_s{simpl}_{clusters}.nc"), - cop_air_total=resources("cop_air_total_elec_s{simpl}_{clusters}.nc"), + cop_soil_decentral_heating=resources( + "cop_soil_decentral_heating_elec_s{simpl}_{clusters}.nc" + ), + cop_air_decentral_heating=resources( + "cop_air_decentral_heating_elec_s{simpl}_{clusters}.nc" + ), + cop_air_central_heating=resources( + "cop_air_central_heating_elec_s{simpl}_{clusters}.nc" + ), + cop_soil_central_heating=resources( + "cop_soil_central_heating_elec_s{simpl}_{clusters}.nc" + ), existing_heating_distribution=resources( "existing_heating_distribution_elec_s{simpl}_{clusters}_{planning_horizons}.csv" ), diff --git a/rules/solve_perfect.smk b/rules/solve_perfect.smk index 51cb39207..d2ea1a16a 100644 --- a/rules/solve_perfect.smk +++ b/rules/solve_perfect.smk @@ -19,8 +19,18 @@ rule add_existing_baseyear: config_provider("scenario", "planning_horizons", 0)(w) ) ), - cop_soil_total=resources("cop_soil_total_elec_s{simpl}_{clusters}.nc"), - cop_air_total=resources("cop_air_total_elec_s{simpl}_{clusters}.nc"), + cop_soil_decentral_heating=resources( + "cop_soil_decentral_heating_elec_s{simpl}_{clusters}.nc" + ), + cop_air_decentral_heating=resources( + "cop_air_decentral_heating_elec_s{simpl}_{clusters}.nc" + ), + cop_air_central_heating=resources( + "cop_air_central_heating_elec_s{simpl}_{clusters}.nc" + ), + cop_soil_central_heating=resources( + "cop_soil_central_heating_elec_s{simpl}_{clusters}.nc" + ), existing_heating_distribution=resources( "existing_heating_distribution_elec_s{simpl}_{clusters}_{planning_horizons}.csv" ), diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index cc82295da..9770e6ce5 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -436,7 +436,7 @@ def add_heating_capacities_installed_before_baseyear( services proportional to heating load in both 50% capacities in rural buses 50% in urban buses cop: dict - Dictionary with time-dependent coefficients of perforamnce (COPs) for air and ground heat pumps as values and keys "air decentral", "ground decentral", "air central", "ground central" + Dictionary with time-dependent coefficients of performance (COPs) for air and ground heat pumps as values and keys "air decentral", "ground decentral", "air central", "ground central" time_dep_hp_cop: bool If True, time-dependent (dynamic) COPs are used for heat pumps """ @@ -459,7 +459,7 @@ def add_heating_capacities_installed_before_baseyear( efficiency = ( cop[f"{heat_pump_type} {name_type}"][nodes] - if options["time_dep_hp_cop"] + if time_dep_hp_cop else costs.at[costs_name, "efficiency"] ) From da201e1b9ac49f7987193d066065c84b7c0d1d45 Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Thu, 25 Jul 2024 15:08:42 +0200 Subject: [PATCH 064/344] udpate add_brownfield COPs --- rules/solve_myopic.smk | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/rules/solve_myopic.smk b/rules/solve_myopic.smk index 8d3230ada..bf952d4d9 100644 --- a/rules/solve_myopic.smk +++ b/rules/solve_myopic.smk @@ -87,8 +87,18 @@ rule add_brownfield: + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", network_p=solved_previous_horizon, #solved network at previous time step costs=resources("costs_{planning_horizons}.csv"), - cop_soil_total=resources("cop_soil_total_elec_s{simpl}_{clusters}.nc"), - cop_air_total=resources("cop_air_total_elec_s{simpl}_{clusters}.nc"), + cop_soil_decentral_heating=resources( + "cop_soil_decentral_heating_elec_s{simpl}_{clusters}.nc" + ), + cop_air_decentral_heating=resources( + "cop_air_decentral_heating_elec_s{simpl}_{clusters}.nc" + ), + cop_air_central_heating=resources( + "cop_air_central_heating_elec_s{simpl}_{clusters}.nc" + ), + cop_soil_central_heating=resources( + "cop_soil_central_heating_elec_s{simpl}_{clusters}.nc" + ), output: RESULTS + "prenetworks-brownfield/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", From 8875ab6c38c4cc90341ec3058f0667ba831b2135 Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Thu, 25 Jul 2024 15:10:45 +0200 Subject: [PATCH 065/344] add licensing info --- scripts/build_cop_profiles/BaseCopApproximator.py | 3 +++ scripts/build_cop_profiles/CentralHeatingCopApproximator.py | 4 ++++ scripts/build_cop_profiles/DecentralHeatingCopApproximator.py | 4 ++++ scripts/build_cop_profiles/run.py | 4 +++- 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/scripts/build_cop_profiles/BaseCopApproximator.py b/scripts/build_cop_profiles/BaseCopApproximator.py index 247ff0fea..ad537a74b 100644 --- a/scripts/build_cop_profiles/BaseCopApproximator.py +++ b/scripts/build_cop_profiles/BaseCopApproximator.py @@ -1,4 +1,7 @@ # -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2020-2024 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT from abc import ABC, abstractmethod from typing import Union diff --git a/scripts/build_cop_profiles/CentralHeatingCopApproximator.py b/scripts/build_cop_profiles/CentralHeatingCopApproximator.py index f2b8d80c1..a29dab592 100644 --- a/scripts/build_cop_profiles/CentralHeatingCopApproximator.py +++ b/scripts/build_cop_profiles/CentralHeatingCopApproximator.py @@ -1,4 +1,8 @@ # -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2020-2024 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT + from typing import Union diff --git a/scripts/build_cop_profiles/DecentralHeatingCopApproximator.py b/scripts/build_cop_profiles/DecentralHeatingCopApproximator.py index b49be54c0..dfcbdfb34 100644 --- a/scripts/build_cop_profiles/DecentralHeatingCopApproximator.py +++ b/scripts/build_cop_profiles/DecentralHeatingCopApproximator.py @@ -1,4 +1,8 @@ # -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2020-2024 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT + from typing import Union diff --git a/scripts/build_cop_profiles/run.py b/scripts/build_cop_profiles/run.py index c25b80a3f..12d012bb3 100644 --- a/scripts/build_cop_profiles/run.py +++ b/scripts/build_cop_profiles/run.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- - +# SPDX-FileCopyrightText: : 2020-2024 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT import numpy as np import xarray as xr From 64d0f291d59d3b7a5a1b1b4f14fdd817ae36c754 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:11:05 +0000 Subject: [PATCH 066/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/build_cop_profiles/DecentralHeatingCopApproximator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/build_cop_profiles/DecentralHeatingCopApproximator.py b/scripts/build_cop_profiles/DecentralHeatingCopApproximator.py index dfcbdfb34..d8526c396 100644 --- a/scripts/build_cop_profiles/DecentralHeatingCopApproximator.py +++ b/scripts/build_cop_profiles/DecentralHeatingCopApproximator.py @@ -4,7 +4,6 @@ # SPDX-License-Identifier: MIT - from typing import Union import numpy as np From 3c5b11e4ea583f12eca3d5edfb6c13d28262df68 Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Thu, 25 Jul 2024 15:28:31 +0200 Subject: [PATCH 067/344] update release notes --- doc/release_notes.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index fa0473fc3..8b60381b8 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -10,6 +10,8 @@ Release Notes Upcoming Release ================ +* Changed heat pump COP approximation for central heating to be based on Jensen et al. 2018 and a default forward temperature of 90C. This is more realistic for district heating than the previously used approximation method. + * Changed default assumptions about waste heat usage from PtX and fuel cells in district heating. The default value for the link efficiency scaling factor was changed from 100% to 25%. It can be set to other values in the configuration ``sector: use_TECHNOLOGY_waste_heat``. From f096592b7f61ea807b650b89e3f8eca3dec8957c Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Fri, 26 Jul 2024 19:45:45 +0200 Subject: [PATCH 068/344] add option to make central heating forward/return temperatures country-specific --- config/config.default.yaml | 14 ++++++-- scripts/_helpers.py | 4 +++ scripts/build_cop_profiles/run.py | 60 +++++++++++++++++++++++++++---- 3 files changed, 70 insertions(+), 8 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index a606cd25c..16825d591 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -409,8 +409,18 @@ sector: 2050: 1.0 district_heating_loss: 0.15 # check these numbers! - forward_temperature: 90 #C - return_temperature: 50 #C + forward_temperature: + generic: 90 + DK: 70 + SE: 70 + NO: 70 + FI: 70 + return_temperature: + generic: 50 + DK: 40 + SE: 40 + NO: 40 + FI: 40 heat_source_cooling: 6 #K heat_pump_cop_approximation: refrigerant: ammonia diff --git a/scripts/_helpers.py b/scripts/_helpers.py index a3b77c1c0..040ba1255 100644 --- a/scripts/_helpers.py +++ b/scripts/_helpers.py @@ -785,3 +785,7 @@ def get_snapshots(snapshots, drop_leap_day=False, freq="h", **kwargs): time = time[~((time.month == 2) & (time.day == 29))] return time + + +def get_country_from_node_name(node_name: str) -> str: + return node_name[:2] \ No newline at end of file diff --git a/scripts/build_cop_profiles/run.py b/scripts/build_cop_profiles/run.py index 12d012bb3..0c40a94dc 100644 --- a/scripts/build_cop_profiles/run.py +++ b/scripts/build_cop_profiles/run.py @@ -5,10 +5,43 @@ import numpy as np import xarray as xr -from _helpers import set_scenario_config +from _helpers import set_scenario_config, get_country_from_node_name from CentralHeatingCopApproximator import CentralHeatingCopApproximator from DecentralHeatingCopApproximator import DecentralHeatingCopApproximator + +def map_temperature_dict_to_onshore_regions( + temperature_dict: dict, onshore_regions: xr.DataArray +) -> xr.DataArray: + """ + Map dictionary of temperatures to onshore regions. + + Parameters: + ---------- + temperature_dict : dictionary + Dictionary with temperatures as values and country keys as keys. One key must be named "generic" + onshore_regions : xr.DataArray + Names of onshore regions + + Returns: + ------- + xr.DataArray + The dictionary values mapped to onshore regions with onshore regions as coordinates. + """ + return xr.DataArray( + [ + ( + temperature_dict[get_country_from_node_name(node_name)] + if get_country_from_node_name(node_name) in temperature_dict.keys() + else temperature_dict["generic"] + ) + for node_name in onshore_regions["name"].values + ], + dims=["name"], + coords={"name": onshore_regions["name"]}, + ) + + if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake @@ -23,12 +56,12 @@ for source_type in ["air", "soil"]: # source inlet temperature (air/soil) is based on weather data - source_inlet_temperature_celsius = xr.open_dataarray( + source_inlet_temperature_celsius: xr.DataArray = xr.open_dataarray( snakemake.input[f"temp_{source_type}_total"] ) # Approximate COP for decentral (individual) heating - cop_individual_heating = DecentralHeatingCopApproximator( + cop_individual_heating: xr.DataArray = DecentralHeatingCopApproximator( forward_temperature_celsius=snakemake.params.heat_pump_sink_T_decentral_heating, source_inlet_temperature_celsius=source_inlet_temperature_celsius, source_type=source_type, @@ -37,10 +70,25 @@ snakemake.output[f"cop_{source_type}_decentral_heating"] ) + # map forward and return temperatures specified on country-level to onshore regions + onshore_regions: xr.DataArray = source_inlet_temperature_celsius["name"] + forward_temperature_central_heating: xr.DataArray = ( + map_temperature_dict_to_onshore_regions( + temperature_dict=snakemake.params.forward_temperature_central_heating, + onshore_regions=onshore_regions, + ) + ) + return_temperature_central_heating: xr.DataArray = ( + map_temperature_dict_to_onshore_regions( + temperature_dict=snakemake.params.return_temperature_central_heating, + onshore_regions=onshore_regions, + ) + ) + # Approximate COP for central (district) heating - cop_central_heating = CentralHeatingCopApproximator( - forward_temperature_celsius=snakemake.params.forward_temperature_central_heating, - return_temperature_celsius=snakemake.params.return_temperature_central_heating, + cop_central_heating: xr.DataArray = CentralHeatingCopApproximator( + forward_temperature_celsius=forward_temperature_central_heating, + return_temperature_celsius=return_temperature_central_heating, source_inlet_temperature_celsius=source_inlet_temperature_celsius, source_outlet_temperature_celsius=source_inlet_temperature_celsius - snakemake.params.heat_source_cooling_central_heating, From 08c30d8b2ecd2be73168a87e61fdd32f07405d24 Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Fri, 26 Jul 2024 20:08:06 +0200 Subject: [PATCH 069/344] ensure correct dimensions in forward/return temperature arayx and change naming from "generic" to "default" --- config/config.default.yaml | 5 ++--- scripts/build_cop_profiles/run.py | 26 ++++++++++++++++---------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 16825d591..527f68c4c 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -408,15 +408,14 @@ sector: 2045: 0.8 2050: 1.0 district_heating_loss: 0.15 - # check these numbers! forward_temperature: - generic: 90 + default: 90 DK: 70 SE: 70 NO: 70 FI: 70 return_temperature: - generic: 50 + default: 50 DK: 40 SE: 40 NO: 40 diff --git a/scripts/build_cop_profiles/run.py b/scripts/build_cop_profiles/run.py index 0c40a94dc..d2976b6ea 100644 --- a/scripts/build_cop_profiles/run.py +++ b/scripts/build_cop_profiles/run.py @@ -11,7 +11,7 @@ def map_temperature_dict_to_onshore_regions( - temperature_dict: dict, onshore_regions: xr.DataArray + temperature_dict: dict, onshore_regions: xr.DataArray, snapshots: xr.DataArray ) -> xr.DataArray: """ Map dictionary of temperatures to onshore regions. @@ -19,7 +19,7 @@ def map_temperature_dict_to_onshore_regions( Parameters: ---------- temperature_dict : dictionary - Dictionary with temperatures as values and country keys as keys. One key must be named "generic" + Dictionary with temperatures as values and country keys as keys. One key must be named "default" onshore_regions : xr.DataArray Names of onshore regions @@ -30,15 +30,19 @@ def map_temperature_dict_to_onshore_regions( """ return xr.DataArray( [ - ( - temperature_dict[get_country_from_node_name(node_name)] - if get_country_from_node_name(node_name) in temperature_dict.keys() - else temperature_dict["generic"] - ) - for node_name in onshore_regions["name"].values + [ + ( + temperature_dict[get_country_from_node_name(node_name)] + if get_country_from_node_name(node_name) in temperature_dict.keys() + else temperature_dict["default"] + ) + for node_name in onshore_regions["name"].values + ] + # pass both nodes and snapshots as dimensions to preserve correct data structure + for _ in snapshots["time"].values ], - dims=["name"], - coords={"name": onshore_regions["name"]}, + dims=["time", "name"], + coords={"time": snapshots["time"], "name": onshore_regions["name"]}, ) @@ -76,12 +80,14 @@ def map_temperature_dict_to_onshore_regions( map_temperature_dict_to_onshore_regions( temperature_dict=snakemake.params.forward_temperature_central_heating, onshore_regions=onshore_regions, + snapshots=source_inlet_temperature_celsius["time"] ) ) return_temperature_central_heating: xr.DataArray = ( map_temperature_dict_to_onshore_regions( temperature_dict=snakemake.params.return_temperature_central_heating, onshore_regions=onshore_regions, + snapshots=source_inlet_temperature_celsius["time"] ) ) From 9d485a2185ce4a39a27325e070573b2859c1647e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 26 Jul 2024 18:12:18 +0000 Subject: [PATCH 070/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- config/config.default.yaml | 2 +- scripts/_helpers.py | 2 +- scripts/build_cop_profiles/run.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 527f68c4c..92a24534b 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -414,7 +414,7 @@ sector: SE: 70 NO: 70 FI: 70 - return_temperature: + return_temperature: default: 50 DK: 40 SE: 40 diff --git a/scripts/_helpers.py b/scripts/_helpers.py index 040ba1255..1c890ea58 100644 --- a/scripts/_helpers.py +++ b/scripts/_helpers.py @@ -788,4 +788,4 @@ def get_snapshots(snapshots, drop_leap_day=False, freq="h", **kwargs): def get_country_from_node_name(node_name: str) -> str: - return node_name[:2] \ No newline at end of file + return node_name[:2] diff --git a/scripts/build_cop_profiles/run.py b/scripts/build_cop_profiles/run.py index d2976b6ea..ef6253dd5 100644 --- a/scripts/build_cop_profiles/run.py +++ b/scripts/build_cop_profiles/run.py @@ -5,7 +5,7 @@ import numpy as np import xarray as xr -from _helpers import set_scenario_config, get_country_from_node_name +from _helpers import get_country_from_node_name, set_scenario_config from CentralHeatingCopApproximator import CentralHeatingCopApproximator from DecentralHeatingCopApproximator import DecentralHeatingCopApproximator @@ -80,14 +80,14 @@ def map_temperature_dict_to_onshore_regions( map_temperature_dict_to_onshore_regions( temperature_dict=snakemake.params.forward_temperature_central_heating, onshore_regions=onshore_regions, - snapshots=source_inlet_temperature_celsius["time"] + snapshots=source_inlet_temperature_celsius["time"], ) ) return_temperature_central_heating: xr.DataArray = ( map_temperature_dict_to_onshore_regions( temperature_dict=snakemake.params.return_temperature_central_heating, onshore_regions=onshore_regions, - snapshots=source_inlet_temperature_celsius["time"] + snapshots=source_inlet_temperature_celsius["time"], ) ) From 23ea16dc1401f8ac006bf658e6c5ddaec14cea9c Mon Sep 17 00:00:00 2001 From: Toni Seibold <153275395+toniseibold@users.noreply.github.com> Date: Mon, 29 Jul 2024 09:47:08 +0200 Subject: [PATCH 071/344] Lifetime of Gas Pipelines (#1162) * inf lifetime of gas pipelines avoids adding new links each planning horizon * p_nom_extendable for existing gas pipelines is set false if retrofitting to H2 is not allowed * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- scripts/add_brownfield.py | 8 -------- scripts/prepare_sector_network.py | 6 ++++-- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/scripts/add_brownfield.py b/scripts/add_brownfield.py index 672f9e62e..72dd1c88e 100644 --- a/scripts/add_brownfield.py +++ b/scripts/add_brownfield.py @@ -89,10 +89,6 @@ def add_brownfield(n, n_p, year): # deal with gas network pipe_carrier = ["gas pipeline"] if snakemake.params.H2_retrofit: - # drop capacities of previous year to avoid duplicating - to_drop = n.links.carrier.isin(pipe_carrier) & (n.links.build_year != year) - n.mremove("Link", n.links.loc[to_drop].index) - # subtract the already retrofitted from today's gas grid capacity h2_retrofitted_fixed_i = n.links[ (n.links.carrier == "H2 pipeline retrofitted") @@ -115,10 +111,6 @@ def add_brownfield(n, n_p, year): index=pipe_capacity.index ).fillna(0) n.links.loc[gas_pipes_i, "p_nom"] = remaining_capacity - else: - new_pipes = n.links.carrier.isin(pipe_carrier) & (n.links.build_year == year) - n.links.loc[new_pipes, "p_nom"] = 0.0 - n.links.loc[new_pipes, "p_nom_min"] = 0.0 def disable_grid_expansion_if_limit_hit(n): diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index b86f9557a..970a1a0a8 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1238,12 +1238,14 @@ def add_storage_and_grids(n, costs): gas_pipes["p_nom_min"] = 0.0 # 0.1 EUR/MWkm/a to prefer decommissioning to address degeneracy gas_pipes["capital_cost"] = 0.1 * gas_pipes.length + gas_pipes["p_nom_extendable"] = True else: gas_pipes["p_nom_max"] = np.inf gas_pipes["p_nom_min"] = gas_pipes.p_nom gas_pipes["capital_cost"] = ( gas_pipes.length * costs.at["CH4 (g) pipeline", "fixed"] ) + gas_pipes["p_nom_extendable"] = False n.madd( "Link", @@ -1252,14 +1254,14 @@ def add_storage_and_grids(n, costs): bus1=gas_pipes.bus1 + " gas", p_min_pu=gas_pipes.p_min_pu, p_nom=gas_pipes.p_nom, - p_nom_extendable=True, + p_nom_extendable=gas_pipes.p_nom_extendable, p_nom_max=gas_pipes.p_nom_max, p_nom_min=gas_pipes.p_nom_min, length=gas_pipes.length, capital_cost=gas_pipes.capital_cost, tags=gas_pipes.name, carrier="gas pipeline", - lifetime=costs.at["CH4 (g) pipeline", "lifetime"], + lifetime=np.inf, ) # remove fossil generators where there is neither From f5fe0d062c136c86c92717f9fc311c76786972de Mon Sep 17 00:00:00 2001 From: Micha Date: Mon, 29 Jul 2024 09:50:02 +0200 Subject: [PATCH 072/344] Rename ev battery master (#1116) * rename EV battery * add release note * adjust tech colors * fix for battery renaming in plot_summary --------- Co-authored-by: Fabian Neumann --- config/config.default.yaml | 2 +- doc/release_notes.rst | 2 ++ scripts/prepare_perfect_foresight.py | 2 +- scripts/prepare_sector_network.py | 8 ++++---- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 4641837e4..f1ab5f318 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -1054,7 +1054,7 @@ plotting: V2G: '#e5ffa8' land transport EV: '#baf238' land transport demand: '#38baf2' - Li ion: '#baf238' + EV battery: '#baf238' # hot water storage water tanks: '#e69487' residential rural water tanks: '#f7b7a3' diff --git a/doc/release_notes.rst b/doc/release_notes.rst index fa0473fc3..383287a64 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -10,6 +10,8 @@ Release Notes Upcoming Release ================ +* Renamed the carrier of batteries in BEVs from `battery storage` to `EV battery` and the corresponding bus carrier from `Li ion` to `EV battery`. This is to avoid confusion with stationary battery storage. + * Changed default assumptions about waste heat usage from PtX and fuel cells in district heating. The default value for the link efficiency scaling factor was changed from 100% to 25%. It can be set to other values in the configuration ``sector: use_TECHNOLOGY_waste_heat``. diff --git a/scripts/prepare_perfect_foresight.py b/scripts/prepare_perfect_foresight.py index c5e4caf90..efc957005 100644 --- a/scripts/prepare_perfect_foresight.py +++ b/scripts/prepare_perfect_foresight.py @@ -250,7 +250,7 @@ def adjust_stores(n): n.stores.loc[cyclic_i, "e_cyclic_per_period"] = True n.stores.loc[cyclic_i, "e_cyclic"] = False # non cyclic store assumptions - non_cyclic_store = ["co2", "co2 stored", "solid biomass", "biogas", "Li ion"] + non_cyclic_store = ["co2", "co2 stored", "solid biomass", "biogas", "EV battery"] co2_i = n.stores[n.stores.carrier.isin(non_cyclic_store)].index n.stores.loc[co2_i, "e_cyclic_per_period"] = False n.stores.loc[co2_i, "e_cyclic"] = False diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 970a1a0a8..eaaf207da 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1543,14 +1543,14 @@ def add_EVs( temperature, ): - n.add("Carrier", "Li ion") + n.add("Carrier", "EV battery") n.madd( "Bus", spatial.nodes, suffix=" EV battery", location=spatial.nodes, - carrier="Li ion", + carrier="EV battery", unit="MWh_el", ) @@ -1623,9 +1623,9 @@ def add_EVs( n.madd( "Store", spatial.nodes, - suffix=" battery storage", + suffix=" EV battery", bus=spatial.nodes + " EV battery", - carrier="battery storage", + carrier="EV battery", e_cyclic=True, e_nom=e_nom, e_max_pu=1, From 6c14d2072636302a33e7afcc71e20909ca304573 Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Mon, 29 Jul 2024 10:26:47 +0200 Subject: [PATCH 073/344] checkout sector configtable from master --- doc/configtables/sector.csv | 70 ++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 39 deletions(-) diff --git a/doc/configtables/sector.csv b/doc/configtables/sector.csv index de2873edd..5045cecdc 100644 --- a/doc/configtables/sector.csv +++ b/doc/configtables/sector.csv @@ -5,17 +5,9 @@ biomass,--,"{true, false}",Flag to include biomass sector. industry,--,"{true, false}",Flag to include industry sector. agriculture,--,"{true, false}",Flag to include agriculture sector. district_heating,--,,`prepare_sector_network.py `_ -#NAME?,--,float,maximum fraction of urban demand which can be supplied by district heating. Ignored where below current fraction. -#NAME?,--,Dictionary with planning horizons as keys., Increase of today's district heating demand to potential maximum district heating share. Progress = 0 means today's district heating share. Progress = 1 means maximum fraction of urban demand is supplied by district heating -#NAME?,--,float,Share increase in district heat demand in urban central due to heat losses -#NAME?,°C,float,Forward temperature in district heating -#NAME?,°C,float,Return temperature in district heating. Must be lower than forward temperature -#NAME?,K,float,Cooling of heat source for heat pumps -#NAME?,,, -#NAME?,--,"{ammonia, isobutane}",Heat pump refrigerant assumed for COP approximation -#NAME?,K,float,Heat pump pinch point temperature difference in heat exchangers assumed for approximation. -#NAME?,--,float,Isentropic efficiency of heat pump compressor assumed for approximation. Must be between 0 and 1. -#NAME?,--,float,Heat pump heat loss assumed for approximation. Must be between 0 and 1. +-- potential,--,float,maximum fraction of urban demand which can be supplied by district heating. Ignored where below current fraction. +-- progress,--,Dictionary with planning horizons as keys., Increase of today's district heating demand to potential maximum district heating share. Progress = 0 means today's district heating share. Progress = 1 means maximum fraction of urban demand is supplied by district heating +-- district_heating_loss,--,float,Share increase in district heat demand in urban central due to heat losses cluster_heat_buses,--,"{true, false}",Cluster residential and service heat buses in `prepare_sector_network.py `_ to one to save memory. ,,, bev_dsm_restriction _value,--,float,Adds a lower state of charge (SOC) limit for battery electric vehicles (BEV) to manage its own energy demand (DSM). Located in `build_transport_demand.py `_. Set to 0 for no restriction on BEV DSM @@ -65,21 +57,21 @@ heat_pump_sink_T,°C,float,The temperature heat sink used in heat pumps based on reduce_space_heat _exogenously,--,"{true, false}",Influence on space heating demand by a certain factor (applied before losses in district heating). reduce_space_heat _exogenously_factor,--,Dictionary with planning horizons as keys.,"A positive factor can mean renovation or demolition of a building. If the factor is negative, it can mean an increase in floor area, increased thermal comfort, population growth. The default factors are determined by the `Eurocalc Homes and buildings decarbonization scenario `_" retrofitting,,, -#NAME?,--,"{true, false}",Add retrofitting as an endogenous system which co-optimise space heat savings. -#NAME?,--,float,Weight costs for building renovation -#NAME?,--,float,The interest rate for investment in building components -#NAME?,--,"{true, false}",Annualise the investment costs of retrofitting -#NAME?,--,"{true, false}",Weight the costs of retrofitting depending on taxes in countries -#NAME?,--,"{true, false}",Weight the costs of retrofitting depending on labour/material costs per country +-- retro_endogen,--,"{true, false}",Add retrofitting as an endogenous system which co-optimise space heat savings. +-- cost_factor,--,float,Weight costs for building renovation +-- interest_rate,--,float,The interest rate for investment in building components +-- annualise_cost,--,"{true, false}",Annualise the investment costs of retrofitting +-- tax_weighting,--,"{true, false}",Weight the costs of retrofitting depending on taxes in countries +-- construction_index,--,"{true, false}",Weight the costs of retrofitting depending on labour/material costs per country tes,--,"{true, false}",Add option for storing thermal energy in large water pits associated with district heating systems and individual thermal energy storage (TES) tes_tau,,,The time constant used to calculate the decay of thermal energy in thermal energy storage (TES): 1- :math:`e^{-1/24τ}`. -#NAME?,days,float,The time constant in decentralized thermal energy storage (TES) -#NAME?,days,float,The time constant in centralized thermal energy storage (TES) +-- decentral,days,float,The time constant in decentralized thermal energy storage (TES) +-- central,days,float,The time constant in centralized thermal energy storage (TES) boilers,--,"{true, false}",Add option for transforming gas into heat using gas boilers resistive_heaters,--,"{true, false}",Add option for transforming electricity into heat using resistive heaters (independently from gas boilers) oil_boilers,--,"{true, false}",Add option for transforming oil into heat using boilers biomass_boiler,--,"{true, false}",Add option for transforming biomass into heat using boilers -overdimension_individual_heating,--,float,Add option for overdimensioning individual heating systems by a certain factor. This allows them to cover heat demand peaks e.g. 10% higher than those in the data with a setting of 1.1. +overdimension_individual_heating,--,"float",Add option for overdimensioning individual heating systems by a certain factor. This allows them to cover heat demand peaks e.g. 10% higher than those in the data with a setting of 1.1. chp,--,"{true, false}",Add option for using Combined Heat and Power (CHP) micro_chp,--,"{true, false}",Add option for using Combined Heat and Power (CHP) for decentral areas. solar_thermal,--,"{true, false}",Add option for using solar thermal to generate heat. @@ -97,12 +89,12 @@ SMR CC,--,"{true, false}",Add option for transforming natural gas into hydrogen regional_methanol_demand,--,"{true, false}",Spatially resolve methanol demand. Set to true if regional CO2 constraints needed. regional_oil_demand,--,"{true, false}",Spatially resolve oil demand. Set to true if regional CO2 constraints needed. regional_co2 _sequestration_potential,,, -#NAME?,--,"{true, false}",Add option for regionally-resolved geological carbon dioxide sequestration potentials based on `CO2StoP `_. -#NAME?,--,string or list,Name (or list of names) of the attribute(s) for the sequestration potential -#NAME?,--,"{true, false}",Add options for including onshore sequestration potentials -#NAME?,Gt ,float,Any sites with lower potential than this value will be excluded -#NAME?,Gt ,float,The maximum sequestration potential for any one site. -#NAME?,years,float,The years until potential exhausted at optimised annual rate +-- enable,--,"{true, false}",Add option for regionally-resolved geological carbon dioxide sequestration potentials based on `CO2StoP `_. +-- attribute,--,string or list,Name (or list of names) of the attribute(s) for the sequestration potential +-- include_onshore,--,"{true, false}",Add options for including onshore sequestration potentials +-- min_size,Gt ,float,Any sites with lower potential than this value will be excluded +-- max_size,Gt ,float,The maximum sequestration potential for any one site. +-- years_of_storage,years,float,The years until potential exhausted at optimised annual rate co2_sequestration_potential,MtCO2/a,float,The potential of sequestering CO2 in Europe per year co2_sequestration_cost,currency/tCO2,float,The cost of sequestering a ton of CO2 co2_sequestration_lifetime,years,int,The lifetime of a CO2 sequestration site @@ -129,9 +121,9 @@ electricity_distribution _grid_cost_factor,,,Multiplies the investment cost of t electricity_grid _connection,--,"{true, false}",Add the cost of electricity grid connection for onshore wind and solar transmission_efficiency,,,Section to specify transmission losses or compression energy demands of bidirectional links. Splits them into two capacity-linked unidirectional links. -- {carrier},--,str,The carrier of the link. -#NAME?,p.u.,float,Length-independent transmission efficiency. -#NAME?,p.u. per 1000 km,float,Length-dependent transmission efficiency ($\eta^{\text{length}}$) -#NAME?,p.u. per 1000 km,float,Length-dependent electricity demand for compression ($\eta \cdot \text{length}$) implemented as multi-link to local electricity bus. +-- -- efficiency_static,p.u.,float,Length-independent transmission efficiency. +-- -- efficiency_per_1000km,p.u. per 1000 km,float,Length-dependent transmission efficiency ($\eta^{\text{length}}$) +-- -- compression_per_1000km,p.u. per 1000 km,float,Length-dependent electricity demand for compression ($\eta \cdot \text{length}$) implemented as multi-link to local electricity bus. H2_network,--,"{true, false}",Add option for new hydrogen pipelines gas_network,--,"{true, false}","Add existing natural gas infrastructure, incl. LNG terminals, production and entry-points. The existing gas network is added with a lossless transport model. A length-weighted `k-edge augmentation algorithm `_ can be run to add new candidate gas pipelines such that all regions of the model can be connected to the gas network. When activated, all the gas demands are regionally disaggregated as well." H2_retrofit,--,"{true, false}",Add option for retrofiting existing pipelines to transport hydrogen. @@ -147,17 +139,17 @@ conventional_generation,,,Add a more detailed description of conventional carrie biomass_to_liquid,--,"{true, false}",Add option for transforming solid biomass into liquid fuel with the same properties as oil biosng,--,"{true, false}",Add option for transforming solid biomass into synthesis gas with the same properties as natural gas limit_max_growth,,, -#NAME?,--,"{true, false}",Add option to limit the maximum growth of a carrier -#NAME?,p.u.,float,The maximum growth factor of a carrier (e.g. 1.3 allows 30% larger than max historic growth) -#NAME?,,, +-- enable,--,"{true, false}",Add option to limit the maximum growth of a carrier +-- factor,p.u.,float,The maximum growth factor of a carrier (e.g. 1.3 allows 30% larger than max historic growth) +-- max_growth,,, -- -- {carrier},GW,float,The historic maximum growth of a carrier -#NAME?,,, +-- max_relative_growth,,, -- -- {carrier},p.u.,float,The historic maximum relative growth of a carrier ,,, enhanced_geothermal,,, -#NAME?,--,"{true, false}",Add option to include Enhanced Geothermal Systems -#NAME?,--,"{true, false}",Add option for flexible operation (see Ricks et al. 2024) -#NAME?,--,int,The maximum hours the reservoir can be charged under flexible operation -#NAME?,--,float,The maximum boost in power output under flexible operation -#NAME?,--,"{true, false}",Add option for variable capacity factor (see Ricks et al. 2024) -#NAME?,--,float,Share of sourced heat that is replenished by the earth's core (see details in `build_egs_potentials.py `_) +-- enable,--,"{true, false}",Add option to include Enhanced Geothermal Systems +-- flexible,--,"{true, false}",Add option for flexible operation (see Ricks et al. 2024) +-- max_hours,--,int,The maximum hours the reservoir can be charged under flexible operation +-- max_boost,--,float,The maximum boost in power output under flexible operation +-- var_cf,--,"{true, false}",Add option for variable capacity factor (see Ricks et al. 2024) +-- sustainability_factor,--,float,Share of sourced heat that is replenished by the earth's core (see details in `build_egs_potentials.py `_) From 9183846c6eee1b1df85459eb1189655e4d485faf Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Mon, 29 Jul 2024 10:30:23 +0200 Subject: [PATCH 074/344] update sector configtable --- doc/configtables/sector.csv | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/configtables/sector.csv b/doc/configtables/sector.csv index 5045cecdc..17dae5e06 100644 --- a/doc/configtables/sector.csv +++ b/doc/configtables/sector.csv @@ -6,6 +6,14 @@ industry,--,"{true, false}",Flag to include industry sector. agriculture,--,"{true, false}",Flag to include agriculture sector. district_heating,--,,`prepare_sector_network.py `_ -- potential,--,float,maximum fraction of urban demand which can be supplied by district heating. Ignored where below current fraction. +-- forward_temperature,°C,float,Forward temperature in district heating +-- return_temperature,°C,float,Return temperature in district heating. Must be lower than forward temperature +-- heat_source_cooling,K,float,Cooling of heat source for heat pumps +-- heat_pump_cop_approximation,,, +-- refrigerant,--,"{ammonia, isobutane}",Heat pump refrigerant assumed for COP approximation +-- heat_exchanger_pinch_point_temperature_difference,K,float,Heat pump pinch point temperature difference in heat exchangers assumed for approximation. +-- isentropic_compressor_efficiency,--,float,Isentropic efficiency of heat pump compressor assumed for approximation. Must be between 0 and 1. +-- heat_loss,--,float,Heat pump heat loss assumed for approximation. Must be between 0 and 1. -- progress,--,Dictionary with planning horizons as keys., Increase of today's district heating demand to potential maximum district heating share. Progress = 0 means today's district heating share. Progress = 1 means maximum fraction of urban demand is supplied by district heating -- district_heating_loss,--,float,Share increase in district heat demand in urban central due to heat losses cluster_heat_buses,--,"{true, false}",Cluster residential and service heat buses in `prepare_sector_network.py `_ to one to save memory. From 160cb011ccd4a63762d4798585803fd1d440ea5f Mon Sep 17 00:00:00 2001 From: Amos Schledorn <60692940+amosschle@users.noreply.github.com> Date: Mon, 29 Jul 2024 11:30:44 +0200 Subject: [PATCH 075/344] Update doc/configtables/sector.csv Co-authored-by: Fabian Neumann --- doc/configtables/sector.csv | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/configtables/sector.csv b/doc/configtables/sector.csv index 17dae5e06..e14da5573 100644 --- a/doc/configtables/sector.csv +++ b/doc/configtables/sector.csv @@ -10,10 +10,10 @@ district_heating,--,,`prepare_sector_network.py `_ to one to save memory. From 69b01e670fce9b43e2b2b7dd99a3b0bd2d965d2e Mon Sep 17 00:00:00 2001 From: Amos Schledorn <60692940+amosschle@users.noreply.github.com> Date: Mon, 29 Jul 2024 11:33:28 +0200 Subject: [PATCH 076/344] Update config/config.default.yaml Co-authored-by: Fabian Neumann --- config/config.default.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index fbaa965be..5827bffb7 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -408,7 +408,6 @@ sector: 2045: 0.8 2050: 1.0 district_heating_loss: 0.15 - # check these numbers! forward_temperature: 90 #C return_temperature: 50 #C heat_source_cooling: 6 #K From 1d3e39979a79a27ed738d718373074b3506d2ac9 Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Mon, 29 Jul 2024 11:36:46 +0200 Subject: [PATCH 077/344] remove duplicate staticmethods --- .../build_cop_profiles/BaseCopApproximator.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/scripts/build_cop_profiles/BaseCopApproximator.py b/scripts/build_cop_profiles/BaseCopApproximator.py index ad537a74b..891182847 100644 --- a/scripts/build_cop_profiles/BaseCopApproximator.py +++ b/scripts/build_cop_profiles/BaseCopApproximator.py @@ -45,23 +45,6 @@ def approximate_cop(self) -> Union[xr.DataArray, np.array]: """ pass - def celsius_to_kelvin( - t_celsius: Union[float, xr.DataArray, np.array] - ) -> Union[float, xr.DataArray, np.array]: - if (np.asarray(t_celsius) > 200).any(): - raise ValueError( - "t_celsius > 200. Are you sure you are using the right units?" - ) - return t_celsius + 273.15 - - def logarithmic_mean( - t_hot: Union[float, xr.DataArray, np.ndarray], - t_cold: Union[float, xr.DataArray, np.ndarray], - ) -> Union[float, xr.DataArray, np.ndarray]: - if (np.asarray(t_hot <= t_cold)).any(): - raise ValueError("t_hot must be greater than t_cold") - return (t_hot - t_cold) / np.log(t_hot / t_cold) - @staticmethod def celsius_to_kelvin( t_celsius: Union[float, xr.DataArray, np.array] From 9073e49085d08af194aa75a273f77c9ef018425f Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Mon, 29 Jul 2024 11:43:53 +0200 Subject: [PATCH 078/344] add link to paper in release notes --- doc/release_notes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index ad204b53d..32eb73344 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -10,7 +10,7 @@ Release Notes Upcoming Release ================ -* Changed heat pump COP approximation for central heating to be based on Jensen et al. 2018 and a default forward temperature of 90C. This is more realistic for district heating than the previously used approximation method. +* Changed heat pump COP approximation for central heating to be based on `Jensen et al. (2018) `__ and a default forward temperature of 90C. This is more realistic for district heating than the previously used approximation method. * Renamed the carrier of batteries in BEVs from `battery storage` to `EV battery` and the corresponding bus carrier from `Li ion` to `EV battery`. This is to avoid confusion with stationary battery storage. From 936b4903039873ceb2e7caeb1b61873096738427 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 29 Jul 2024 11:51:20 +0200 Subject: [PATCH 079/344] address groupby(axis=...) deprecation (#1182) --- scripts/build_retro_cost.py | 2 +- scripts/plot_validation_electricity_production.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/build_retro_cost.py b/scripts/build_retro_cost.py index 52f545e9b..44f4a7384 100755 --- a/scripts/build_retro_cost.py +++ b/scripts/build_retro_cost.py @@ -890,7 +890,7 @@ def calculate_gain_utilisation_factor(heat_transfer_perm2, Q_ht, Q_gain): Calculates gain utilisation factor nu. """ # time constant of the building tau [h] = c_m [Wh/(m^2K)] * 1 /(H_tr_e+H_tb*H_ve) [m^2 K /W] - tau = c_m / heat_transfer_perm2.T.groupby(axis=1).sum().T + tau = c_m / heat_transfer_perm2.groupby().sum() alpha = alpha_H_0 + (tau / tau_H_0) # heat balance ratio gamma = (1 / Q_ht).mul(Q_gain.sum(axis=1), axis=0) diff --git a/scripts/plot_validation_electricity_production.py b/scripts/plot_validation_electricity_production.py index 5a68cfa54..f842bea30 100644 --- a/scripts/plot_validation_electricity_production.py +++ b/scripts/plot_validation_electricity_production.py @@ -70,7 +70,7 @@ optimized = optimized[["Generator", "StorageUnit"]].droplevel(0, axis=1) optimized = optimized.rename(columns=n.buses.country, level=0) optimized = optimized.rename(columns=carrier_groups, level=1) - optimized = optimized.groupby(axis=1, level=[0, 1]).sum() + optimized = optimized.T.groupby(level=[0, 1]).sum().T data = pd.concat([historic, optimized], keys=["Historic", "Optimized"], axis=1) data.columns.names = ["Kind", "Country", "Carrier"] From e9e0a0da2064a242b0d731dc2218de0e6cbca190 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 29 Jul 2024 11:56:14 +0200 Subject: [PATCH 080/344] address fillna(method='{b|f}fill') deprecation (#1181) * address fillne(method='{b|f}fill') deprecation * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- scripts/add_electricity.py | 2 +- scripts/make_summary_perfect.py | 2 +- scripts/prepare_network.py | 4 +--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/scripts/add_electricity.py b/scripts/add_electricity.py index 17e030b42..495109533 100755 --- a/scripts/add_electricity.py +++ b/scripts/add_electricity.py @@ -852,7 +852,7 @@ def attach_line_rating( fuel_price = pd.read_csv( snakemake.input.fuel_price, index_col=0, header=0, parse_dates=True ) - fuel_price = fuel_price.reindex(n.snapshots).fillna(method="ffill") + fuel_price = fuel_price.reindex(n.snapshots).ffill() else: fuel_price = None diff --git a/scripts/make_summary_perfect.py b/scripts/make_summary_perfect.py index 76bd4ad0a..8e56e5d4d 100644 --- a/scripts/make_summary_perfect.py +++ b/scripts/make_summary_perfect.py @@ -631,7 +631,7 @@ def calculate_co2_emissions(n, label, df): weightings = n.snapshot_weightings.generators.mul( n.investment_period_weightings["years"] .reindex(n.snapshots) - .fillna(method="bfill") + .bfill() .fillna(1.0), axis=0, ) diff --git a/scripts/prepare_network.py b/scripts/prepare_network.py index 00cb00bfe..382e633da 100755 --- a/scripts/prepare_network.py +++ b/scripts/prepare_network.py @@ -137,9 +137,7 @@ def add_emission_prices(n, emission_prices={"co2": 0.0}, exclude_co2=False): def add_dynamic_emission_prices(n): co2_price = pd.read_csv(snakemake.input.co2_price, index_col=0, parse_dates=True) co2_price = co2_price[~co2_price.index.duplicated()] - co2_price = ( - co2_price.reindex(n.snapshots).fillna(method="ffill").fillna(method="bfill") - ) + co2_price = co2_price.reindex(n.snapshots).ffill().bfill() emissions = ( n.generators.carrier.map(n.carriers.co2_emissions) / n.generators.efficiency From c99cd72727b790cc37cf65046c3211edf7c2e5fc Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Mon, 29 Jul 2024 13:36:06 +0200 Subject: [PATCH 081/344] include Tonis Feedback --- scripts/build_energy_totals.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index 6404cd931..44bb470fa 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -360,10 +360,8 @@ def idees_per_country(ct: str, base_dir: str) -> pd.DataFrame: assert df.index[40] == "Electricity" ct_totals["electricity residential"] = df.iloc[40] - # TODO derived heat changed to distributed heat and numbers changed as well! - # this needs to be checked assert df.index[39] == "Distributed heat" - ct_totals["derived heat residential"] = df.iloc[39] + ct_totals["distributed heat residential"] = df.iloc[39] assert df.index[43] == "Thermal uses" ct_totals["thermal uses residential"] = df.iloc[43] @@ -395,9 +393,8 @@ def idees_per_country(ct: str, base_dir: str) -> pd.DataFrame: assert df.index[43] == "Electricity" ct_totals["electricity services"] = df.iloc[43] - # TODO check derived heat changed to distributed heat assert df.index[42] == "Distributed heat" - ct_totals["derived heat services"] = df.iloc[42] + ct_totals["distributed heat services"] = df.iloc[42] assert df.index[46] == "Thermal uses" ct_totals["thermal uses services"] = df.iloc[46] @@ -900,14 +897,14 @@ def build_district_heat_share(countries: List[str], idees: pd.DataFrame) -> pd.S Notes ----- - - The function calculates the district heating share as the sum of residential and services derived heat, divided by the sum of residential and services thermal uses. + - The function calculates the district heating share as the sum of residential and services distributed heat, divided by the sum of residential and services thermal uses. - The district heating share is then reindexed to match the provided list of countries. - Missing district heating shares are filled from `data/district_heat_share.csv`. - The function makes a conservative assumption and takes the minimum district heating share from both the IDEES data and `data/district_heat_share.csv`. """ # district heating share - district_heat = idees[["derived heat residential", "derived heat services"]].sum( + district_heat = idees[["distributed heat residential", "distributed heat services"]].sum( axis=1 ) total_heat = ( @@ -1265,7 +1262,7 @@ def rescale_idees_from_eurostat( "total residential water", "total residential cooking", "total residential", - "derived heat residential", + "distributed heat residential", "thermal uses residential", ], "elec": [ @@ -1281,7 +1278,7 @@ def rescale_idees_from_eurostat( "total services water", "total services cooking", "total services", - "derived heat services", + "distributed heat services", "thermal uses services", ], "elec": [ From 0edf566e3fd0bc9f1dbb0cc6db29a94362efa3e3 Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Mon, 29 Jul 2024 14:36:30 +0200 Subject: [PATCH 082/344] attempt to handle heat_pump_sources dynamically --- config/config.default.yaml | 6 ++++++ rules/build_sector.smk | 28 ++++++++++++++++------------ 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 5827bffb7..f68ff9607 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -416,6 +416,12 @@ sector: heat_exchanger_pinch_point_temperature_difference: 5 #K isentropic_compressor_efficiency: 0.8 heat_loss: 0.0 + heat_pump_sources: + central_heating: + - air + decentral_heating: + - air + - ground cluster_heat_buses: true heat_demand_cutout: default bev_dsm_restriction_value: 0.75 diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 5f8d58176..d6f959f3f 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -232,22 +232,26 @@ rule build_cop_profiles: heat_pump_cop_approximation_central_heating=config_provider( "sector", "district_heating", "heat_pump_cop_approximation" ), + heat_pump_sources=config_provider("sector", "heat_pump_sources"), input: temp_soil_total=resources("temp_soil_total_elec_s{simpl}_{clusters}.nc"), temp_air_total=resources("temp_air_total_elec_s{simpl}_{clusters}.nc"), output: - cop_air_decentral_heating=resources( - "cop_air_decentral_heating_elec_s{simpl}_{clusters}.nc" - ), - cop_soil_decentral_heating=resources( - "cop_soil_decentral_heating_elec_s{simpl}_{clusters}.nc" - ), - cop_air_central_heating=resources( - "cop_air_central_heating_elec_s{simpl}_{clusters}.nc" - ), - cop_soil_central_heating=resources( - "cop_soil_central_heating_elec_s{simpl}_{clusters}.nc" - ), + **{f"cop_{source}_{sink}": resources( + "cop_" + source + "_" + {sink} + "_" + "elec_s{simpl}_{clusters}.nc" + ) for sink, source in config_provider("sector", "heat_pump_sources").items()}, + # cop_air_decentral_heating=resources( + # "cop_air_decentral_heating_elec_s{simpl}_{clusters}.nc" + # ), + # cop_soil_decentral_heating=resources( + # "cop_soil_decentral_heating_elec_s{simpl}_{clusters}.nc" + # ), + # cop_air_central_heating=resources( + # "cop_air_central_heating_elec_s{simpl}_{clusters}.nc" + # ), + # cop_soil_central_heating=resources( + # "cop_soil_central_heating_elec_s{simpl}_{clusters}.nc" + # ), resources: mem_mb=20000, log: From 51f42b4a36c046534fc50363b77e9d0546d3bbd8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 29 Jul 2024 12:36:58 +0000 Subject: [PATCH 083/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- config/config.default.yaml | 6 +++--- rules/build_sector.smk | 9 ++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index f68ff9607..5e204a956 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -418,10 +418,10 @@ sector: heat_loss: 0.0 heat_pump_sources: central_heating: - - air + - air decentral_heating: - - air - - ground + - air + - ground cluster_heat_buses: true heat_demand_cutout: default bev_dsm_restriction_value: 0.75 diff --git a/rules/build_sector.smk b/rules/build_sector.smk index d6f959f3f..dbba8805c 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -237,9 +237,12 @@ rule build_cop_profiles: temp_soil_total=resources("temp_soil_total_elec_s{simpl}_{clusters}.nc"), temp_air_total=resources("temp_air_total_elec_s{simpl}_{clusters}.nc"), output: - **{f"cop_{source}_{sink}": resources( - "cop_" + source + "_" + {sink} + "_" + "elec_s{simpl}_{clusters}.nc" - ) for sink, source in config_provider("sector", "heat_pump_sources").items()}, + **{ + f"cop_{source}_{sink}": resources( + "cop_" + source + "_" + {sink} + "_" + "elec_s{simpl}_{clusters}.nc" + ) + for sink, source in config_provider("sector", "heat_pump_sources").items() + }, # cop_air_decentral_heating=resources( # "cop_air_decentral_heating_elec_s{simpl}_{clusters}.nc" # ), From d1427175638cd926dfbc6bbb6fc9357670b94663 Mon Sep 17 00:00:00 2001 From: Philipp Glaum Date: Mon, 29 Jul 2024 14:49:57 +0200 Subject: [PATCH 084/344] add methanol techs for master to branch --- config/config.default.yaml | 31 +- rules/build_sector.smk | 3 + scripts/prepare_sector_network.py | 459 ++++++++++++++++++++++++++++-- 3 files changed, 461 insertions(+), 32 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 0af347341..a31abd3d8 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -541,7 +541,6 @@ sector: hydrogen_turbine: false SMR: true SMR_cc: true - regional_methanol_demand: false regional_oil_demand: false regional_coal_demand: false regional_co2_sequestration_potential: @@ -567,6 +566,21 @@ sector: # - onshore # more than 50 km from sea - nearshore # within 50 km of sea # - offshore + methanol: false # if industry is modelled, methanol is still added even if false + methanol_spatial: false # if true demand is also regional even if regional demand is set to false, since methanol is regionally resolved + regional_methanol_demand: false + methanol_transport: false + methanol_reforming: false + methanol_reforming_cc: false + methanol_to_kerosene: false + methanol_to_olefins: false + methanol_to_power: + ccgt: false + ccgt_cc: false + ocgt: false + allam: false + biomass_to_methanol: false + biomass_to_methanol_cc: false ammonia: false min_part_load_fischer_tropsch: 0.5 min_part_load_methanolisation: 0.3 @@ -1157,8 +1171,19 @@ plotting: liquid: '#25c49a' kerosene for aviation: '#a1ffe6' naphtha for industry: '#57ebc4' - methanolisation: '#83d6d5' - methanol: '#468c8b' + methanol-to-kerosene: '#C98468' + methanol-to-olefins/aromatics: '#FFA07A' + Methanol steam reforming: '#FFBF00' + Methanol steam reforming CC: '#A2EA8A' + methanolisation: '#00FFBF' + biomass-to-methanol: #EAD28A + biomass-to-methanol CC: #EADBAD + allam methanol: '#B98F76' + CCGT methanol: '#B98F76' + CCGT methanol CC: '#B98F76' + OCGT methanol: '#B98F76' + methanol: '#FF7B00' + methanol transport: '#FF7B00' shipping methanol: '#468c8b' industry methanol: '#468c8b' # co2 diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 6614b163a..276f61e50 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -1020,6 +1020,9 @@ rule prepare_sector_network: hourly_heat_demand_total=resources( "hourly_heat_demand_total_elec_s{simpl}_{clusters}.nc" ), + industrial_production=resources( + "industrial_production_elec_s{simpl}_{clusters}_{planning_horizons}.csv" + ), district_heat_share=resources( "district_heat_share_elec_s{simpl}_{clusters}_{planning_horizons}.csv" ), diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 8cfa62a42..2288354de 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -141,17 +141,24 @@ def define_spatial(nodes, options): spatial.methanol = SimpleNamespace() - spatial.methanol.nodes = ["EU methanol"] - spatial.methanol.locations = ["EU"] - - if options["regional_methanol_demand"]: + if options.get("methanol_spatial", False): + spatial.methanol.nodes = nodes + " methanol" + spatial.methanol.locations = nodes spatial.methanol.demand_locations = nodes spatial.methanol.industry = nodes + " industry methanol" spatial.methanol.shipping = nodes + " shipping methanol" else: - spatial.methanol.demand_locations = ["EU"] - spatial.methanol.shipping = ["EU shipping methanol"] - spatial.methanol.industry = ["EU industry methanol"] + spatial.methanol.nodes = ["EU methanol"] + spatial.methanol.locations = ["EU"] + + if options["regional_methanol_demand"]: + spatial.methanol.demand_locations = nodes + spatial.methanol.industry = nodes + " industry methanol" + spatial.methanol.shipping = nodes + " shipping methanol" + else: + spatial.methanol.demand_locations = ["EU"] + spatial.methanol.shipping = ["EU shipping methanol"] + spatial.methanol.industry = ["EU industry methanol"] # oil spatial.oil = SimpleNamespace() @@ -762,6 +769,331 @@ def add_allam(n, costs): ) +def add_biomass_to_methanol(n, costs): + + if len(spatial.biomass.nodes) <= 1 and len(spatial.methanol.nodes) > 1: + link_names = nodes + " " + spatial.biomass.nodes + else: + link_names = spatial.biomass.nodes + + n.madd( + "Link", + link_names, + suffix=" biomass-to-methanol", + bus0=spatial.biomass.nodes, + bus1=spatial.methanol.nodes, + bus2="co2 atmosphere", + carrier="biomass-to-methanol", + lifetime=costs.at["biomass-to-methanol", "lifetime"], + efficiency=costs.at["biomass-to-methanol", "efficiency"], + efficiency2=-costs.at["solid biomass", "CO2 intensity"] + + costs.at["biomass-to-methanol", "CO2 stored"], + p_nom_extendable=True, + capital_cost=costs.at["biomass-to-methanol", "fixed"] + / costs.at["biomass-to-methanol", "efficiency"], + marginal_cost=costs.loc["biomass-to-methanol", "VOM"] + / costs.at["biomass-to-methanol", "efficiency"], + ) + + +def add_biomass_to_methanol_cc(n, costs): + + if len(spatial.biomass.nodes) <= 1 and len(spatial.methanol.nodes) > 1: + link_names = nodes + " " + spatial.biomass.nodes + else: + link_names = spatial.biomass.nodes + + n.madd( + "Link", + link_names, + suffix=" biomass-to-methanol CC", + bus0=spatial.biomass.nodes, + bus1=spatial.methanol.nodes, + bus2="co2 atmosphere", + bus3=spatial.co2.nodes, + carrier="biomass-to-methanol CC", + lifetime=costs.at["biomass-to-methanol", "lifetime"], + efficiency=costs.at["biomass-to-methanol", "efficiency"], + efficiency2=-costs.at["solid biomass", "CO2 intensity"] + + costs.at["biomass-to-methanol", "CO2 stored"] + * (1 - costs.at["biomass-to-methanol", "capture rate"]), + efficiency3=costs.at["biomass-to-methanol", "CO2 stored"] + * costs.at["biomass-to-methanol", "capture rate"], + p_nom_extendable=True, + capital_cost=costs.at["biomass-to-methanol", "fixed"] + / costs.at["biomass-to-methanol", "efficiency"] + + costs.at["biomass CHP capture", "fixed"] + * costs.at["biomass-to-methanol", "CO2 stored"], + marginal_cost=costs.loc["biomass-to-methanol", "VOM"] + / costs.at["biomass-to-methanol", "efficiency"], + ) + + +def add_methanol_to_power(n, costs, types={}): + # TODO: add costs to technology-data + + nodes = pop_layout.index + + if types["allam"]: + logger.info("Adding Allam cycle methanol power plants.") + + n.madd( + "Link", + nodes, + suffix=" allam methanol", + bus0=spatial.methanol.nodes, + bus1=nodes, + bus2=spatial.co2.df.loc[nodes, "nodes"].values, + bus3="co2 atmosphere", + carrier="allam methanol", + p_nom_extendable=True, + capital_cost=0.59 + * 1.832e6 + * calculate_annuity(25, 0.07), # efficiency * EUR/MW * annuity + marginal_cost=2, + efficiency=0.59, + efficiency2=0.98 * costs.at["methanolisation", "carbondioxide-input"], + efficiency3=0.02 * costs.at["methanolisation", "carbondioxide-input"], + lifetime=25, + ) + + if types["ccgt"]: + logger.info("Adding methanol CCGT power plants.") + + # efficiency * EUR/MW * (annuity + FOM) + capital_cost = 0.58 * 916e3 * (calculate_annuity(25, 0.07) + 0.035) + + n.madd( + "Link", + nodes, + suffix=" CCGT methanol", + bus0=spatial.methanol.nodes, + bus1=nodes, + bus2="co2 atmosphere", + carrier="CCGT methanol", + p_nom_extendable=True, + capital_cost=capital_cost, + marginal_cost=2, + efficiency=0.58, + efficiency2=costs.at["methanolisation", "carbondioxide-input"], + lifetime=25, + ) + + if types["ccgt_cc"]: + logger.info( + "Adding methanol CCGT power plants with post-combustion carbon capture." + ) + + # TODO consider efficiency changes / energy inputs for CC + + # efficiency * EUR/MW * (annuity + FOM) + capital_cost = 0.58 * 916e3 * (calculate_annuity(25, 0.07) + 0.035) + + capital_cost_cc = ( + capital_cost + + costs.at["cement capture", "fixed"] + * costs.at["methanolisation", "carbondioxide-input"] + ) + + n.madd( + "Link", + nodes, + suffix=" CCGT methanol CC", + bus0=spatial.methanol.nodes, + bus1=nodes, + bus2=spatial.co2.df.loc[nodes, "nodes"].values, + bus3="co2 atmosphere", + carrier="CCGT methanol CC", + p_nom_extendable=True, + capital_cost=capital_cost_cc, + marginal_cost=2, + efficiency=0.58, + efficiency2=costs.at["cement capture", "capture_rate"] + * costs.at["methanolisation", "carbondioxide-input"], + efficiency3=(1 - costs.at["cement capture", "capture_rate"]) + * costs.at["methanolisation", "carbondioxide-input"], + lifetime=25, + ) + + if types["ocgt"]: + logger.info("Adding methanol OCGT power plants.") + + n.madd( + "Link", + nodes, + suffix=" OCGT methanol", + bus0=spatial.methanol.nodes, + bus1=nodes, + bus2="co2 atmosphere", + carrier="OCGT methanol", + p_nom_extendable=True, + capital_cost=0.35 + * 458e3 + * ( + calculate_annuity(25, 0.07) + 0.035 + ), # efficiency * EUR/MW * (annuity + FOM) + marginal_cost=2, + efficiency=0.35, + efficiency2=costs.at["methanolisation", "carbondioxide-input"], + lifetime=25, + ) + + +def add_methanol_to_olefins(n, costs): + nodes = pop_layout.index + nhours = n.snapshot_weightings.generators.sum() + nyears = nhours / 8760 + + tech = "methanol-to-olefins/aromatics" + + logger.info(f"Adding {tech}.") + + demand_factor = options["HVC_demand_factor"] + + industrial_production = ( + pd.read_csv(snakemake.input.industrial_production, index_col=0) + * 1e3 + * nyears # kt/a -> t/a + ) + + p_nom_max = ( + demand_factor + * industrial_production.loc[nodes, "HVC"] + / nhours + * costs.at[tech, "methanol-input"] + ) + + co2_release = ( + costs.at[tech, "carbondioxide-output"] / costs.at[tech, "methanol-input"] + + costs.at["methanolisation", "carbondioxide-input"] + ) + + n.madd( + "Link", + spatial.methanol.locations, + suffix=f" {tech}", + carrier=tech, + capital_cost=costs.at[tech, "fixed"] / costs.at[tech, "methanol-input"], + marginal_cost=costs.at[tech, "VOM"] / costs.at[tech, "methanol-input"], + p_nom_extendable=True, + bus0=spatial.methanol.nodes, + bus1=spatial.oil.naphtha, + bus2=nodes, + bus3="co2 atmosphere", + p_min_pu=1, + p_nom_max=p_nom_max.values, + efficiency=1 / costs.at[tech, "methanol-input"], + efficiency2=-costs.at[tech, "electricity-input"] + / costs.at[tech, "methanol-input"], + efficiency3=co2_release, + ) + + +def add_methanol_to_kerosene(n, costs): + nodes = pop_layout.index + nhours = n.snapshot_weightings.generators.sum() + nyears = nhours / 8760 + + demand_factor = options["aviation_demand_factor"] + + tech = "methanol-to-kerosene" + + logger.info(f"Adding {tech}.") + + all_aviation = ["total international aviation", "total domestic aviation"] + + p_nom_max = ( + demand_factor + * pop_weighted_energy_totals.loc[nodes, all_aviation].sum(axis=1) + * 1e6 + / nhours + * costs.at[tech, "methanol-input"] + ) + + # cost data available at https://www.concawe.eu/wp-content/uploads/Rpt_22-17.pdf table 94 + + n.madd( + "Link", + spatial.methanol.locations, + suffix=f" {tech}", + carrier=tech, + # capital_cost= , + bus0=spatial.methanol.nodes, + bus1=spatial.oil.kerosene, + bus2=spatial.h2.nodes, + efficiency=costs.at[tech, "methanol-input"], + efficiency2=-costs.at[tech, "hydrogen-input"] + / costs.at[tech, "methanol-input"], + p_nom_extendable=True, + p_min_pu=1, + p_nom_max=p_nom_max.values, + ) + + +def add_methanol_reforming(n, costs): + logger.info("Adding methanol steam reforming.") + + nodes = pop_layout.index + + tech = "Methanol steam reforming" + + capital_cost = costs.at[tech, "fixed"] / costs.at[tech, "methanol-input"] + + n.madd( + "Link", + spatial.methanol.locations, + suffix=f" {tech}", + bus0=spatial.methanol.nodes, + bus1=spatial.h2.nodes, + bus2="co2 atmosphere", + p_nom_extendable=True, + capital_cost=capital_cost, + efficiency=1 / costs.at[tech, "methanol-input"], + efficiency2=costs.at["methanolisation", "carbondioxide-input"], + carrier=tech, + lifetime=costs.at[tech, "lifetime"], + ) + + +def add_methanol_reforming_cc(n, costs): + logger.info("Adding methanol steam reforming with carbon capture.") + + nodes = pop_layout.index + + tech = "Methanol steam reforming" + + # TODO: heat release and electricity demand for process and carbon capture + # but the energy demands for carbon capture have not yet been added for other CC processes + # 10.1016/j.rser.2020.110171: 0.129 kWh_e/kWh_H2, -0.09 kWh_heat/kWh_H2 + + capital_cost = costs.at[tech, "fixed"] / costs.at[tech, "methanol-input"] + + capital_cost_cc = ( + capital_cost + + costs.at["cement capture", "fixed"] + * costs.at["methanolisation", "carbondioxide-input"] + ) + + n.madd( + "Link", + nodes, + suffix=f" {tech} CC", + bus0=spatial.methanol.nodes, + bus1=spatial.h2.nodes, + bus2="co2 atmosphere", + bus3=spatial.co2.nodes, + p_nom_extendable=True, + capital_cost=capital_cost_cc, + efficiency=1 / costs.at[tech, "methanol-input"], + efficiency2=(1 - costs.at["cement capture", "capture_rate"]) + * costs.at["methanolisation", "carbondioxide-input"], + efficiency3=costs.at["cement capture", "capture_rate"] + * costs.at["methanolisation", "carbondioxide-input"], + carrier=f"{tech} CC", + lifetime=costs.at[tech, "lifetime"], + ) + + def add_dac(n, costs): heat_carriers = ["urban central heat", "services urban decentral heat"] heat_buses = n.buses.index[n.buses.carrier.isin(heat_carriers)] @@ -2226,6 +2558,64 @@ def add_heat(n, costs): ) +def add_methanol(n, costs): + logger.info("Add methanol") + + n.add("Carrier", "methanol") + + n.madd( + "Bus", + spatial.methanol.nodes, + location=spatial.methanol.locations, + carrier="methanol", + unit="MWh_LHV", + ) + + n.madd( + "Store", + spatial.methanol.nodes, + suffix=" Store", + bus=spatial.methanol.nodes, + e_nom_extendable=True, + e_cyclic=True, + carrier="methanol", + capital_cost=0.02, + ) + + if options["methanol_transport"]: + methanol_transport = create_network_topology( + n, "methanol transport ", bidirectional=True + ) + n.madd( + "Link", + methanol_transport.index, + bus0=methanol_transport.bus0 + " methanol", + bus1=methanol_transport.bus1 + " methanol", + p_nom_extendable=False, + p_nom=5e4, + length=methanol_transport.length.values, + marginal_cost=0.027 + * methanol_transport.length.values, # assuming 0.15€/ton-km and 0.183t/1000MWhMeOH + carrier="methanol transport", + ) + + if "biomass" in n.buses.carrier.unique(): + if options["biomass_to_methanol"]: + add_biomass_to_methanol(n, costs) + + if options["biomass_to_methanol"]: + add_biomass_to_methanol_cc(n, costs) + + if options["methanol_to_power"]: + add_methanol_to_power(n, costs, types=options["methanol_to_power"]) + + if options["methanol_reforming"]: + add_methanol_reforming(n, costs) + + if options["methanol_reforming_cc"]: + add_methanol_reforming_cc(n, costs) + + def add_biomass(n, costs): logger.info("Add biomass") @@ -2685,25 +3075,26 @@ def add_industry(n, costs): ) # methanol for industry + # add methanol nodes if not already added + if "methanol" not in n.buses.carrier.unique(): + n.madd( + "Bus", + spatial.methanol.nodes, + carrier="methanol", + location=spatial.methanol.locations, + unit="MWh_LHV", + ) - n.madd( - "Bus", - spatial.methanol.nodes, - carrier="methanol", - location=spatial.methanol.locations, - unit="MWh_LHV", - ) - - n.madd( - "Store", - spatial.methanol.nodes, - suffix=" Store", - bus=spatial.methanol.nodes, - e_nom_extendable=True, - e_cyclic=True, - carrier="methanol", - capital_cost=0.02, - ) + n.madd( + "Store", + spatial.methanol.nodes, + suffix=" Store", + bus=spatial.methanol.nodes, + e_nom_extendable=True, + e_cyclic=True, + carrier="methanol", + capital_cost=0.02, + ) n.madd( "Bus", @@ -2718,7 +3109,7 @@ def add_industry(n, costs): / nhours ) - if not options["regional_methanol_demand"]: + if not options["regional_methanol_demand"] or not options["methanol_spatial"]: p_set_methanol = p_set_methanol.sum() n.madd( @@ -2840,7 +3231,7 @@ def add_industry(n, costs): * efficiency ) - if not options["regional_methanol_demand"]: + if not options["regional_methanol_demand"] or not options["methanol_spatial"]: p_set_methanol_shipping = p_set_methanol_shipping.sum() n.madd( @@ -3129,6 +3520,9 @@ def add_industry(n, costs): efficiency3=process_co2_per_naphtha, ) + if options["methanol_to_olefins"]: + add_methanol_to_olefins(n, costs) + # aviation demand_factor = options.get("aviation_demand_factor", 1) if demand_factor != 1: @@ -3173,6 +3567,9 @@ def add_industry(n, costs): efficiency2=costs.at["oil", "CO2 intensity"], ) + if options["methanol_to_kerosene"]: + add_methanol_to_kerosene(n, costs) + # TODO simplify bus expression n.madd( "Load", @@ -3947,9 +4344,10 @@ def add_enhanced_geothermal(n, egs_potentials, egs_overlap, costs): simpl="", opts="", clusters="37", - ll="v1.0", - sector_opts="730H-T-H-B-I-A-dist1", + ll="vopt", + sector_opts="", planning_horizons="2050", + run="enable_all", ) configure_logging(snakemake) @@ -4012,6 +4410,9 @@ def add_enhanced_geothermal(n, egs_potentials, egs_overlap, costs): if options["ammonia"]: add_ammonia(n, costs) + if options["methanol"]: + add_methanol(n, costs) + if options["industry"]: add_industry(n, costs) From 201889424c48c7a0c9a6d05d9e23f739c4377651 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Mon, 29 Jul 2024 15:39:01 +0200 Subject: [PATCH 085/344] update industry to new JRC-idees --- config/config.default.yaml | 2 +- rules/build_sector.smk | 4 +- ...ustrial_energy_demand_per_country_today.py | 4 +- ...build_industrial_production_per_country.py | 72 ++++++++++--------- 4 files changed, 44 insertions(+), 38 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index d1d13065c..373f4a6c5 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -716,7 +716,7 @@ industry: MWh_CH4_per_tMeOH: 10.25 MWh_MeOH_per_tMeOH: 5.528 hotmaps_locate_missing: false - reference_year: 2015 + reference_year: 2021 # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#costs diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 866f69625..cd259d39c 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -509,7 +509,7 @@ rule build_industrial_production_per_country: countries=config_provider("countries"), input: ammonia_production=resources("ammonia_production.csv"), - jrc="data/bundle/jrc-idees-2015", + jrc="data/bundle/jrc-idees-2021", eurostat="data/eurostat/Balances-April2023", output: industrial_production_per_country=resources( @@ -656,7 +656,7 @@ rule build_industrial_energy_demand_per_country_today: countries=config_provider("countries"), industry=config_provider("industry"), input: - jrc="data/bundle/jrc-idees-2015", + jrc="data/bundle/jrc-idees-2021", industrial_production_per_country=resources( "industrial_production_per_country.csv" ), diff --git a/scripts/build_industrial_energy_demand_per_country_today.py b/scripts/build_industrial_energy_demand_per_country_today.py index b77ba8d65..114485e78 100644 --- a/scripts/build_industrial_energy_demand_per_country_today.py +++ b/scripts/build_industrial_energy_demand_per_country_today.py @@ -8,7 +8,7 @@ Inputs ------- -- ``data/bundle/jrc-idees-2015`` +- ``data/bundle/jrc-idees-2021`` - ``industrial_production_per_country.csv`` Outputs @@ -120,7 +120,7 @@ def industrial_energy_demand_per_country(country, year, jrc_dir): jrc_country = jrc_names.get(country, country) - fn = f"{jrc_dir}/JRC-IDEES-2015_EnergyBalance_{jrc_country}.xlsx" + fn = f"{jrc_dir}/{jrc_country}/JRC-IDEES-2021_EnergyBalance_{jrc_country}.xlsx" sheets = list(sector_sheets.values()) df_dict = pd.read_excel(fn, sheet_name=sheets, index_col=0) diff --git a/scripts/build_industrial_production_per_country.py b/scripts/build_industrial_production_per_country.py index ec86a78d5..1bb75271d 100644 --- a/scripts/build_industrial_production_per_country.py +++ b/scripts/build_industrial_production_per_country.py @@ -16,7 +16,7 @@ Inputs ------- - ``resources/ammonia_production.csv`` -- ``data/bundle-sector/jrc-idees-2015`` +- ``data/bundle-sector/jrc-idees-2021`` - ``data/eurostat`` Outputs @@ -50,11 +50,11 @@ - Aluminium - primary production - Aluminium - secondary production - Other non-ferrous metals -- Transport Equipment -- Machinery Equipment +- Transport equipment +- Machinery equipment - Textiles and leather - Wood and wood products -- Other Industrial Sectors +- Other industrial sectors - Ammonia - HVC - Chlorine @@ -79,25 +79,25 @@ sub_sheet_name_dict = { "Iron and steel": "ISI", - "Chemicals Industry": "CHI", + "Chemical industry": "CHI", "Non-metallic mineral products": "NMM", "Pulp, paper and printing": "PPA", "Food, beverages and tobacco": "FBT", "Non Ferrous Metals": "NFM", - "Transport Equipment": "TRE", - "Machinery Equipment": "MAE", + "Transport equipment": "TRE", + "Machinery equipment": "MAE", "Textiles and leather": "TEL", "Wood and wood products": "WWP", - "Other Industrial Sectors": "OIS", + "Other industrial sectors": "OIS", } -eu28 = cc.EU28as("ISO2").ISO2.values +eu27 = cc.EU27as("ISO2").ISO2.values jrc_names = {"GR": "EL", "GB": "UK"} sect2sub = { "Iron and steel": ["Electric arc", "Integrated steelworks"], - "Chemicals Industry": [ + "Chemical industry": [ "Basic chemicals", "Other chemicals", "Pharmaceutical products etc.", @@ -119,11 +119,11 @@ "Aluminium - secondary production", "Other non-ferrous metals", ], - "Transport Equipment": ["Transport Equipment"], - "Machinery Equipment": ["Machinery Equipment"], + "Transport equipment": ["Transport equipment"], + "Machinery equipment": ["Machinery equipment"], "Textiles and leather": ["Textiles and leather"], "Wood and wood products": ["Wood and wood products"], - "Other Industrial Sectors": ["Other Industrial Sectors"], + "Other industrial sectors": ["Other industrial sectors"], } sub2sect = {v: k for k, vv in sect2sub.items() for v in vv} @@ -145,27 +145,29 @@ "Aluminium - primary production": "Aluminium - primary production", "Aluminium - secondary production": "Aluminium - secondary production", "Other non-ferrous metals": "Other non-ferrous metals (kt lead eq.)", - "Transport Equipment": "Physical output (index)", - "Machinery Equipment": "Physical output (index)", + "Transport equipment": "Physical output (index)", + "Machinery equipment": "Physical output (index)", "Textiles and leather": "Physical output (index)", "Wood and wood products": "Physical output (index)", - "Other Industrial Sectors": "Physical output (index)", + "Other industrial sectors": "Physical output (index)", } eb_sectors = { "Iron & steel": "Iron and steel", - "Chemical & petrochemical": "Chemicals Industry", + "Chemical & petrochemical": "Chemical industry", "Non-ferrous metals": "Non-metallic mineral products", "Paper, pulp & printing": "Pulp, paper and printing", "Food, beverages & tobacco": "Food, beverages and tobacco", "Non-metallic minerals": "Non Ferrous Metals", - "Transport equipment": "Transport Equipment", - "Machinery": "Machinery Equipment", + "Transport equipment": "Transport equipment", + "Machinery": "Machinery equipment", "Textile & leather": "Textiles and leather", "Wood & wood products": "Wood and wood products", - "Not elsewhere specified (industry)": "Other Industrial Sectors", + "Not elsewhere specified (industry)": "Other industrial sectors", } + + # TODO: this should go in a csv in `data` # Annual energy consumption in Switzerland by sector in 2015 (in TJ) # From: Energieverbrauch in der Industrie und im Dienstleistungssektor, Der Bundesrat @@ -173,16 +175,16 @@ e_switzerland = pd.Series( { "Iron and steel": 7889.0, - "Chemicals Industry": 26871.0, + "Chemical industry": 26871.0, "Non-metallic mineral products": 15513.0 + 3820.0, "Pulp, paper and printing": 12004.0, "Food, beverages and tobacco": 17728.0, "Non Ferrous Metals": 3037.0, - "Transport Equipment": 14993.0, - "Machinery Equipment": 4724.0, + "Transport equipment": 14993.0, + "Machinery equipment": 4724.0, "Textiles and leather": 1742.0, "Wood and wood products": 0.0, - "Other Industrial Sectors": 10825.0, + "Other industrial sectors": 10825.0, "current electricity": 53760.0, } ) @@ -199,8 +201,12 @@ def get_energy_ratio(country, eurostat_dir, jrc_dir, year): if country == "CH": e_country = e_switzerland * tj_to_ktoe else: + ct_eurostat = country.replace("GB","UK") + if ct_eurostat == "UK": + year=2019 + logger.info("Assume Eurostat data for GB from 2019.") # estimate physical output, energy consumption in the sector and country - fn = f"{eurostat_dir}/{country}-Energy-balance-sheets-April-2023-edition.xlsb" + fn = f"{eurostat_dir}/{ct_eurostat}-Energy-balance-sheets-April-2023-edition.xlsb" df = pd.read_excel( fn, sheet_name=str(min(2021, year)), @@ -210,19 +216,19 @@ def get_energy_ratio(country, eurostat_dir, jrc_dir, year): ) e_country = df.loc[eb_sectors.keys(), "Total"].rename(eb_sectors) - fn = f"{jrc_dir}/JRC-IDEES-2015_Industry_EU28.xlsx" + fn = f"{jrc_dir}/EU27/JRC-IDEES-2021_Industry_EU27.xlsx" with mute_print(): df = pd.read_excel(fn, sheet_name="Ind_Summary", index_col=0, header=0).squeeze( "columns" ) - assert df.index[48] == "by sector" + assert df.index[49] == "by sector" year_i = df.columns.get_loc(year) - e_eu28 = df.iloc[49:76, year_i] - e_eu28.index = e_eu28.index.str.lstrip() + e_eu27 = df.iloc[50:77, year_i] + e_eu27.index = e_eu27.index.str.lstrip() - e_ratio = e_country / e_eu28 + e_ratio = e_country / e_eu27 return pd.Series({k: e_ratio[v] for k, v in sub2sect.items()}) @@ -230,7 +236,7 @@ def get_energy_ratio(country, eurostat_dir, jrc_dir, year): def industry_production_per_country(country, year, eurostat_dir, jrc_dir): def get_sector_data(sector, country): jrc_country = jrc_names.get(country, country) - fn = f"{jrc_dir}/JRC-IDEES-2015_Industry_{jrc_country}.xlsx" + fn = f"{jrc_dir}/{jrc_country}/JRC-IDEES-2021_Industry_{jrc_country}.xlsx" sheet = sub_sheet_name_dict[sector] with mute_print(): df = pd.read_excel(fn, sheet_name=sheet, index_col=0, header=0).squeeze( @@ -245,10 +251,10 @@ def get_sector_data(sector, country): return df - ct = "EU28" if country not in eu28 else country + ct = "EU27" if country not in eu27 else country demand = pd.concat([get_sector_data(s, ct) for s in sect2sub]) - if country not in eu28: + if country not in eu27: demand *= get_energy_ratio(country, eurostat_dir, jrc_dir, year) demand.name = country From fb2ab4c50bf715cbfb3e0962738c97e7782fa1aa Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Mon, 29 Jul 2024 15:44:30 +0200 Subject: [PATCH 086/344] update ammonia data --- rules/build_sector.smk | 2 +- scripts/build_ammonia_production.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/rules/build_sector.smk b/rules/build_sector.smk index cd259d39c..227f64da0 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -437,7 +437,7 @@ rule build_salt_cavern_potentials: rule build_ammonia_production: input: - usgs="data/bundle/myb1-2017-nitro.xls", + usgs="data/bundle/myb1-2021-nitro-ert.xlsx", output: ammonia_production=resources("ammonia_production.csv"), threads: 1 diff --git a/scripts/build_ammonia_production.py b/scripts/build_ammonia_production.py index 37692f2f1..83dfde20f 100644 --- a/scripts/build_ammonia_production.py +++ b/scripts/build_ammonia_production.py @@ -18,7 +18,8 @@ Description ------- -This functions takes data from the `Minerals Yearbook `_ (June 2024) published by the US Geological Survey (USGS) and the National Minerals Information Center and extracts the annual ammonia production per country in ktonN/a. The data is converted to ktonNH3/a. +This functions takes data from the `Minerals Yearbook `_ + (July 2024) published by the US Geological Survey (USGS) and the National Minerals Information Center and extracts the annual ammonia production per country in ktonN/a. The data is converted to ktonNH3/a. """ import country_converter as coco @@ -48,9 +49,9 @@ ammonia.index = cc.convert(ammonia.index, to="iso2") - years = [str(i) for i in range(2013, 2018)] + years = [str(i) for i in range(2017, 2022)] - ammonia = ammonia[years] + ammonia = ammonia.rename(columns=lambda x: str(x))[years] # convert from ktonN to ktonNH3 ammonia *= 17 / 14 From 314f6c6f0feeccaac9ed6ea1fba4bff28b2e1c00 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Mon, 29 Jul 2024 16:00:13 +0200 Subject: [PATCH 087/344] retrieve ammonia demand data --- rules/retrieve.smk | 9 +++++++ scripts/retrieve_ammonia_demand.py | 42 ++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 scripts/retrieve_ammonia_demand.py diff --git a/rules/retrieve.smk b/rules/retrieve.smk index 24e387c52..7bf7c6809 100644 --- a/rules/retrieve.smk +++ b/rules/retrieve.smk @@ -65,6 +65,15 @@ if config["enable"]["retrieve"] and config["enable"].get("retrieve_databundle", script: "../scripts/retrieve_jrc_idees.py" + rule retrieve_ammonia_demand: + output: + "data/bundle/myb1-2021-nitro-ert.xlsx", + log: + "logs/retrieve_ammonia_demand.log", + retries: 2 + script: + "../scripts/retrieve_ammonia_demand.py" + rule retrieve_eurostat_household_data: output: "data/eurostat/eurostat-household_energy_balances-february_2024.csv", diff --git a/scripts/retrieve_ammonia_demand.py b/scripts/retrieve_ammonia_demand.py new file mode 100644 index 000000000..cdfa582b6 --- /dev/null +++ b/scripts/retrieve_ammonia_demand.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2024- The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT +""" +Retrieve ammonia demand from https://www.usgs.gov/centers/national-minerals-information-center/nitrogen-statistics-and-information. +""" + +import logging +import os +import zipfile +from pathlib import Path + +from _helpers import configure_logging, progress_retrieve, set_scenario_config + +logger = logging.getLogger(__name__) + +# Define the base URL +url = "https://d9-wret.s3.us-west-2.amazonaws.com/assets/palladium/production/s3fs-public/media/files/myb1-2022-nitro-ert.xlsx" + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake("retrieve_ammonia_demand") + rootpath = ".." + else: + rootpath = "." + + configure_logging(snakemake) + set_scenario_config(snakemake) + disable_progress = snakemake.config["run"].get("disable_progressbar", False) + + to_fn = snakemake.output[0] + + + # download .zip file + logger.info(f"Downloading Ammonia demand from {url}.") + progress_retrieve(url, to_fn, disable=disable_progress) + + + logger.info(f"Ammonia demand data available in '{to_fn}'.") From aa820fa0464480ec5e11c1782d2ce1791ad9f370 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Mon, 29 Jul 2024 16:00:37 +0200 Subject: [PATCH 088/344] change industry reference year to 2019 --- config/config.default.yaml | 2 +- scripts/build_industrial_production_per_country.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 373f4a6c5..521013b0c 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -716,7 +716,7 @@ industry: MWh_CH4_per_tMeOH: 10.25 MWh_MeOH_per_tMeOH: 5.528 hotmaps_locate_missing: false - reference_year: 2021 + reference_year: 2019 # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#costs diff --git a/scripts/build_industrial_production_per_country.py b/scripts/build_industrial_production_per_country.py index 1bb75271d..332627dd3 100644 --- a/scripts/build_industrial_production_per_country.py +++ b/scripts/build_industrial_production_per_country.py @@ -293,6 +293,7 @@ def separate_basic_chemicals(demand, year): """ Separate basic chemicals into ammonia, chlorine, methanol and HVC. """ + # ammonia data from 2017-2021 ammonia = pd.read_csv(snakemake.input.ammonia_production, index_col=0) there = ammonia.index.intersection(demand.index) @@ -301,8 +302,11 @@ def separate_basic_chemicals(demand, year): logger.info(f"Following countries have no ammonia demand: {missing.tolist()}") demand["Ammonia"] = 0.0 - - demand.loc[there, "Ammonia"] = ammonia.loc[there, str(year)] + + year_to_use = min(max(year, 2017), 2021) + if year_to_use != year: + logger.info(f"Using data from {year_to_use} for ammonia production.") + demand.loc[there, "Ammonia"] = ammonia.loc[there, str(year_to_use)] demand["Basic chemicals"] -= demand["Ammonia"] From 2f3fde26d0cc832818bcd85a51952591e8efb302 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Mon, 29 Jul 2024 16:09:38 +0200 Subject: [PATCH 089/344] update ammonia demand data to 2022 --- rules/build_sector.smk | 2 +- rules/retrieve.smk | 2 +- scripts/build_ammonia_production.py | 4 ++-- scripts/build_industrial_production_per_country.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 227f64da0..0ced9c058 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -437,7 +437,7 @@ rule build_salt_cavern_potentials: rule build_ammonia_production: input: - usgs="data/bundle/myb1-2021-nitro-ert.xlsx", + usgs="data/bundle/myb1-2022-nitro-ert.xlsx", output: ammonia_production=resources("ammonia_production.csv"), threads: 1 diff --git a/rules/retrieve.smk b/rules/retrieve.smk index 7bf7c6809..120c2e1bd 100644 --- a/rules/retrieve.smk +++ b/rules/retrieve.smk @@ -67,7 +67,7 @@ if config["enable"]["retrieve"] and config["enable"].get("retrieve_databundle", rule retrieve_ammonia_demand: output: - "data/bundle/myb1-2021-nitro-ert.xlsx", + "data/bundle/myb1-2022-nitro-ert.xlsx", log: "logs/retrieve_ammonia_demand.log", retries: 2 diff --git a/scripts/build_ammonia_production.py b/scripts/build_ammonia_production.py index 83dfde20f..e0e2de5e4 100644 --- a/scripts/build_ammonia_production.py +++ b/scripts/build_ammonia_production.py @@ -43,13 +43,13 @@ skiprows=5, header=0, index_col=0, - skipfooter=19, + skipfooter=7, na_values=["--"], ) ammonia.index = cc.convert(ammonia.index, to="iso2") - years = [str(i) for i in range(2017, 2022)] + years = [str(i) for i in range(2018, 2023)] ammonia = ammonia.rename(columns=lambda x: str(x))[years] diff --git a/scripts/build_industrial_production_per_country.py b/scripts/build_industrial_production_per_country.py index 332627dd3..90bf54824 100644 --- a/scripts/build_industrial_production_per_country.py +++ b/scripts/build_industrial_production_per_country.py @@ -303,7 +303,7 @@ def separate_basic_chemicals(demand, year): demand["Ammonia"] = 0.0 - year_to_use = min(max(year, 2017), 2021) + year_to_use = min(max(year, 2018), 2022) if year_to_use != year: logger.info(f"Using data from {year_to_use} for ammonia production.") demand.loc[there, "Ammonia"] = ammonia.loc[there, str(year_to_use)] From b0e00249aa941b884336b35347c62c8df878ae1e Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Mon, 29 Jul 2024 17:37:06 +0200 Subject: [PATCH 090/344] adjust mapping energy demand per ct --- ...ustrial_energy_demand_per_country_today.py | 95 ++++++++++--------- 1 file changed, 49 insertions(+), 46 deletions(-) diff --git a/scripts/build_industrial_energy_demand_per_country_today.py b/scripts/build_industrial_energy_demand_per_country_today.py index 114485e78..983b255e7 100644 --- a/scripts/build_industrial_energy_demand_per_country_today.py +++ b/scripts/build_industrial_energy_demand_per_country_today.py @@ -36,7 +36,7 @@ - Integrated steelworks - Machinery Equipment - Methanol -- Other Industrial Sectors +- Other industrial sectors - Other chemicals - Other non-ferrous metals - Paper production @@ -74,46 +74,49 @@ # name in JRC-IDEES Energy Balances sector_sheets = { - "Integrated steelworks": "cisb", - "Electric arc": "cise", - "Alumina production": "cnfa", - "Aluminium - primary production": "cnfp", - "Aluminium - secondary production": "cnfs", - "Other non-ferrous metals": "cnfo", - "Basic chemicals": "cbch", - "Other chemicals": "coch", - "Pharmaceutical products etc.": "cpha", - "Basic chemicals feedstock": "cpch", - "Cement": "ccem", - "Ceramics & other NMM": "ccer", - "Glass production": "cgla", - "Pulp production": "cpul", - "Paper production": "cpap", - "Printing and media reproduction": "cprp", - "Food, beverages and tobacco": "cfbt", - "Transport Equipment": "ctre", - "Machinery Equipment": "cmae", - "Textiles and leather": "ctel", - "Wood and wood products": "cwwp", - "Mining and quarrying": "cmiq", - "Construction": "ccon", - "Non-specified": "cnsi", + "Integrated steelworks": "FC_IND_IS_BF_E", + "Electric arc": "FC_IND_IS_EA_E", + "Alumina production": "FC_IND_NFM_AM_E", + "Aluminium - primary production": "FC_IND_NFM_PA_E", + "Aluminium - secondary production": "FC_IND_NFM_SA_E", + "Other non-ferrous metals": "FC_IND_NFM_OM_E", + "Basic chemicals": "FC_IND_CPC_BC_E", + "Other chemicals": "FC_IND_CPC_OC_E", + "Pharmaceutical products etc.": "FC_IND_CPC_PH_E", + "Basic chemicals feedstock": "FC_IND_CPC_BC_E", + "Cement": "FC_IND_NMM_CM_E", + "Ceramics & other NMM": "FC_IND_NMM_CR_E", + "Glass production": "FC_IND_NMM_GL_E", + "Pulp production": "FC_IND_PPP_PU_E", + "Paper production": "FC_IND_PPP_PA_E", + "Printing and media reproduction": "FC_IND_PPP_PR_E", + "Food, beverages and tobacco": "FC_IND_FBT_E", + "Transport equipment": "FC_IND_TE_E", + "Machinery equipment": "FC_IND_MAC_E", + "Textiles and leather": "FC_IND_TL_E", + "Wood and wood products": "FC_IND_WP_E", + "Mining and quarrying": "FC_IND_MQ_E", + "Construction": "FC_IND_CON_E", + "Non-specified": "FC_IND_NSP_E", } fuels = { - "All Products": "all", - "Solid Fuels": "solid", - "Total petroleum products (without biofuels)": "liquid", - "Gases": "gas", + "Total": "all", + "Solid fossil fuels": "solid", + "Peat and peat products": "solid", + "Oil shale and oil sands": "solid", + "Oil and petroleum products": "liquid", + "Manufactured gases": "gas", + "Natural gas": "gas", "Nuclear heat": "heat", - "Derived heat": "heat", - "Biomass and Renewable wastes": "biomass", - "Wastes (non-renewable)": "waste", + "Heat": "heat", + "Renewables and biofuels": "biomass", + "Non-renewable waste": "waste", "Electricity": "electricity", } -eu28 = cc.EU28as("ISO2").ISO2.tolist() +eu27 = cc.EU27as("ISO2").ISO2.tolist() jrc_names = {"GR": "EL", "GB": "UK"} @@ -139,7 +142,7 @@ def get_subsector_data(sheet): ) sel = ["Mining and quarrying", "Construction", "Non-specified"] - df["Other Industrial Sectors"] = df[sel].sum(axis=1) + df["Other industrial sectors"] = df[sel].sum(axis=1) df["Basic chemicals"] += df["Basic chemicals feedstock"] df.drop(columns=sel + ["Basic chemicals feedstock"], index="all", inplace=True) @@ -189,20 +192,20 @@ def separate_basic_chemicals(demand, production): return demand -def add_non_eu28_industrial_energy_demand(countries, demand, production): - non_eu28 = countries.difference(eu28) - if non_eu28.empty: +def add_non_eu27_industrial_energy_demand(countries, demand, production): + non_eu27 = countries.difference(eu27) + if non_eu27.empty: return demand - eu28_production = production.loc[countries.intersection(eu28)].sum() - eu28_energy = demand.groupby(level=1).sum() - eu28_averages = eu28_energy / eu28_production + eu27_production = production.loc[countries.intersection(eu27)].sum() + eu27_energy = demand.groupby(level=1).sum() + eu27_averages = eu27_energy / eu27_production - demand_non_eu28 = pd.concat( - {k: v * eu28_averages for k, v in production.loc[non_eu28].iterrows()} + demand_non_eu27 = pd.concat( + {k: v * eu27_averages for k, v in production.loc[non_eu27].iterrows()} ) - return pd.concat([demand, demand_non_eu28]) + return pd.concat([demand, demand_non_eu27]) def industrial_energy_demand(countries, year): @@ -232,10 +235,10 @@ def industrial_energy_demand(countries, year): set_scenario_config(snakemake) params = snakemake.params.industry - year = params.get("reference_year", 2015) + year = params.get("reference_year", 2019) countries = pd.Index(snakemake.params.countries) - demand = industrial_energy_demand(countries.intersection(eu28), year) + demand = industrial_energy_demand(countries.intersection(eu27), year) # output in MtMaterial/a production = ( @@ -245,7 +248,7 @@ def industrial_energy_demand(countries, year): demand = separate_basic_chemicals(demand, production) - demand = add_non_eu28_industrial_energy_demand(countries, demand, production) + demand = add_non_eu27_industrial_energy_demand(countries, demand, production) # for format compatibility demand = demand.stack(future_stack=True).unstack(level=[0, 2]) From 29479c50d0150bf768d02cca7a242b867b5b564d Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Mon, 29 Jul 2024 18:29:27 +0200 Subject: [PATCH 091/344] pass heat source/system type to prepare_sector_network and add_existing_baseyear --- config/config.default.yaml | 4 +- rules/build_sector.smk | 37 ++-- rules/solve_myopic.smk | 14 +- scripts/add_existing_baseyear.py | 72 ++++---- .../DecentralHeatingCopApproximator.py | 8 +- scripts/build_cop_profiles/run.py | 74 +++++--- scripts/prepare_sector_network.py | 163 ++++++++---------- 7 files changed, 176 insertions(+), 196 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index f68ff9607..9f5954010 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -417,9 +417,9 @@ sector: isentropic_compressor_efficiency: 0.8 heat_loss: 0.0 heat_pump_sources: - central_heating: + central: - air - decentral_heating: + decentral: - air - ground cluster_heat_buses: true diff --git a/rules/build_sector.smk b/rules/build_sector.smk index d6f959f3f..9b2bfde65 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -214,6 +214,13 @@ rule build_temperature_profiles: script: "../scripts/build_temperature_profiles.py" +# def output_cop(wildcards): +# return { +# f"cop_{source}_{sink}": resources( +# "cop_" + source + "_" + sink + "_" + "elec_s{simpl}_{clusters}.nc" +# ) +# for sink, source in config["sector"]["heat_pump_sources"].items() +# } rule build_cop_profiles: params: @@ -237,21 +244,7 @@ rule build_cop_profiles: temp_soil_total=resources("temp_soil_total_elec_s{simpl}_{clusters}.nc"), temp_air_total=resources("temp_air_total_elec_s{simpl}_{clusters}.nc"), output: - **{f"cop_{source}_{sink}": resources( - "cop_" + source + "_" + {sink} + "_" + "elec_s{simpl}_{clusters}.nc" - ) for sink, source in config_provider("sector", "heat_pump_sources").items()}, - # cop_air_decentral_heating=resources( - # "cop_air_decentral_heating_elec_s{simpl}_{clusters}.nc" - # ), - # cop_soil_decentral_heating=resources( - # "cop_soil_decentral_heating_elec_s{simpl}_{clusters}.nc" - # ), - # cop_air_central_heating=resources( - # "cop_air_central_heating_elec_s{simpl}_{clusters}.nc" - # ), - # cop_soil_central_heating=resources( - # "cop_soil_central_heating_elec_s{simpl}_{clusters}.nc" - # ), + cop_profiles=resources("cop_profiles_elec_s{simpl}_{clusters}.nc"), resources: mem_mb=20000, log: @@ -969,6 +962,7 @@ rule prepare_sector_network: adjustments=config_provider("adjustments", "sector"), emissions_scope=config_provider("energy", "emissions"), RDIR=RDIR, + heat_pump_sources=config_provider("sector", "heat_pump_sources"), input: unpack(input_profile_offwind), **rules.cluster_gas_network.output, @@ -1045,18 +1039,7 @@ rule prepare_sector_network: ), temp_soil_total=resources("temp_soil_total_elec_s{simpl}_{clusters}.nc"), temp_air_total=resources("temp_air_total_elec_s{simpl}_{clusters}.nc"), - cop_soil_decentral_heating=resources( - "cop_soil_decentral_heating_elec_s{simpl}_{clusters}.nc" - ), - cop_air_decentral_heating=resources( - "cop_air_decentral_heating_elec_s{simpl}_{clusters}.nc" - ), - cop_air_central_heating=resources( - "cop_air_central_heating_elec_s{simpl}_{clusters}.nc" - ), - cop_soil_central_heating=resources( - "cop_soil_central_heating_elec_s{simpl}_{clusters}.nc" - ), + cop_profiles=resources("cop_profiles_elec_s{simpl}_{clusters}.nc"), solar_thermal_total=lambda w: ( resources("solar_thermal_total_elec_s{simpl}_{clusters}.nc") if config_provider("sector", "solar_thermal")(w) diff --git a/rules/solve_myopic.smk b/rules/solve_myopic.smk index bf952d4d9..09f25b241 100644 --- a/rules/solve_myopic.smk +++ b/rules/solve_myopic.smk @@ -9,6 +9,7 @@ rule add_existing_baseyear: sector=config_provider("sector"), existing_capacities=config_provider("existing_capacities"), costs=config_provider("costs"), + heat_pump_sources=config_provider("sector", "heat_pump_sources"), input: network=RESULTS + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", @@ -21,18 +22,7 @@ rule add_existing_baseyear: config_provider("scenario", "planning_horizons", 0)(w) ) ), - cop_soil_decentral_heating=resources( - "cop_soil_decentral_heating_elec_s{simpl}_{clusters}.nc" - ), - cop_air_decentral_heating=resources( - "cop_air_decentral_heating_elec_s{simpl}_{clusters}.nc" - ), - cop_air_central_heating=resources( - "cop_air_central_heating_elec_s{simpl}_{clusters}.nc" - ), - cop_soil_central_heating=resources( - "cop_soil_central_heating_elec_s{simpl}_{clusters}.nc" - ), + cop_profiles=resources("cop_profiles_elec_s{simpl}_{clusters}.nc"), existing_heating_distribution=resources( "existing_heating_distribution_elec_s{simpl}_{clusters}_{planning_horizons}.csv" ), diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index 9770e6ce5..00b5eeb7e 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -442,27 +442,27 @@ def add_heating_capacities_installed_before_baseyear( """ logger.debug(f"Adding heating capacities installed before {baseyear}") - for name in existing_heating.columns.get_level_values(0).unique(): - name_type = "central" if name == "urban central" else "decentral" + for heat_system in existing_heating.columns.get_level_values(0).unique(): + system_type = "central" if heat_system == "urban central" else "decentral" - nodes = pd.Index(n.buses.location[n.buses.index.str.contains(f"{name} heat")]) + nodes = pd.Index(n.buses.location[n.buses.index.str.contains(f"{heat_system} heat")]) - if (name_type != "central") and options["electricity_distribution_grid"]: + if (system_type != "central") and options["electricity_distribution_grid"]: nodes_elec = nodes + " low voltage" else: nodes_elec = nodes - heat_pump_type = "air" if "urban" in name else "ground" - # Add heat pumps - costs_name = f"decentral {heat_pump_type}-sourced heat pump" + heat_source = snakemake.params.heat_pump_sources[system_type] + costs_name = f"{system_type} {heat_source}-sourced heat pump" efficiency = ( - cop[f"{heat_pump_type} {name_type}"][nodes] - if time_dep_hp_cop + cop.sel(heat_system=system_type, heat_source=heat_source, name=nodes).to_pandas().reindex(index=n.snapshots) + if options["time_dep_hp_cop"] else costs.at[costs_name, "efficiency"] ) + too_large_grouping_years = [gy for gy in grouping_years if gy >= int(baseyear)] if too_large_grouping_years: logger.warning( @@ -491,14 +491,14 @@ def add_heating_capacities_installed_before_baseyear( n.madd( "Link", nodes, - suffix=f" {name} {heat_pump_type} heat pump-{grouping_year}", + suffix=f" {heat_system} {heat_source} heat pump-{grouping_year}", bus0=nodes_elec, - bus1=nodes + " " + name + " heat", - carrier=f"{name} {heat_pump_type} heat pump", + bus1=nodes + " " + heat_system + " heat", + carrier=f"{heat_system} {heat_source} heat pump", efficiency=efficiency, capital_cost=costs.at[costs_name, "efficiency"] * costs.at[costs_name, "fixed"], - p_nom=existing_heating.loc[nodes, (name, f"{heat_pump_type} heat pump")] + p_nom=existing_heating.loc[nodes, (heat_system, f"{heat_source} heat pump")] * ratio / costs.at[costs_name, "efficiency"], build_year=int(grouping_year), @@ -509,66 +509,66 @@ def add_heating_capacities_installed_before_baseyear( n.madd( "Link", nodes, - suffix=f" {name} resistive heater-{grouping_year}", + suffix=f" {heat_system} resistive heater-{grouping_year}", bus0=nodes_elec, - bus1=nodes + " " + name + " heat", - carrier=name + " resistive heater", - efficiency=costs.at[f"{name_type} resistive heater", "efficiency"], + bus1=nodes + " " + heat_system + " heat", + carrier=heat_system + " resistive heater", + efficiency=costs.at[f"{system_type} resistive heater", "efficiency"], capital_cost=( - costs.at[f"{name_type} resistive heater", "efficiency"] - * costs.at[f"{name_type} resistive heater", "fixed"] + costs.at[f"{system_type} resistive heater", "efficiency"] + * costs.at[f"{system_type} resistive heater", "fixed"] ), p_nom=( - existing_heating.loc[nodes, (name, "resistive heater")] + existing_heating.loc[nodes, (heat_system, "resistive heater")] * ratio - / costs.at[f"{name_type} resistive heater", "efficiency"] + / costs.at[f"{system_type} resistive heater", "efficiency"] ), build_year=int(grouping_year), - lifetime=costs.at[f"{name_type} resistive heater", "lifetime"], + lifetime=costs.at[f"{system_type} resistive heater", "lifetime"], ) n.madd( "Link", nodes, - suffix=f" {name} gas boiler-{grouping_year}", + suffix=f" {heat_system} gas boiler-{grouping_year}", bus0="EU gas" if "EU gas" in spatial.gas.nodes else nodes + " gas", - bus1=nodes + " " + name + " heat", + bus1=nodes + " " + heat_system + " heat", bus2="co2 atmosphere", - carrier=name + " gas boiler", - efficiency=costs.at[f"{name_type} gas boiler", "efficiency"], + carrier=heat_system + " gas boiler", + efficiency=costs.at[f"{system_type} gas boiler", "efficiency"], efficiency2=costs.at["gas", "CO2 intensity"], capital_cost=( - costs.at[f"{name_type} gas boiler", "efficiency"] - * costs.at[f"{name_type} gas boiler", "fixed"] + costs.at[f"{system_type} gas boiler", "efficiency"] + * costs.at[f"{system_type} gas boiler", "fixed"] ), p_nom=( - existing_heating.loc[nodes, (name, "gas boiler")] + existing_heating.loc[nodes, (heat_system, "gas boiler")] * ratio - / costs.at[f"{name_type} gas boiler", "efficiency"] + / costs.at[f"{system_type} gas boiler", "efficiency"] ), build_year=int(grouping_year), - lifetime=costs.at[f"{name_type} gas boiler", "lifetime"], + lifetime=costs.at[f"{system_type} gas boiler", "lifetime"], ) n.madd( "Link", nodes, - suffix=f" {name} oil boiler-{grouping_year}", + suffix=f" {heat_system} oil boiler-{grouping_year}", bus0=spatial.oil.nodes, - bus1=nodes + " " + name + " heat", + bus1=nodes + " " + heat_system + " heat", bus2="co2 atmosphere", - carrier=name + " oil boiler", + carrier=heat_system + " oil boiler", efficiency=costs.at["decentral oil boiler", "efficiency"], efficiency2=costs.at["oil", "CO2 intensity"], capital_cost=costs.at["decentral oil boiler", "efficiency"] * costs.at["decentral oil boiler", "fixed"], p_nom=( - existing_heating.loc[nodes, (name, "oil boiler")] + existing_heating.loc[nodes, (heat_system, "oil boiler")] * ratio / costs.at["decentral oil boiler", "efficiency"] ), build_year=int(grouping_year), - lifetime=costs.at[f"{name_type} gas boiler", "lifetime"], + lifetime=costs.at[f"{system_type} gas boiler", "lifetime"], ) # delete links with p_nom=nan corresponding to extra nodes in country diff --git a/scripts/build_cop_profiles/DecentralHeatingCopApproximator.py b/scripts/build_cop_profiles/DecentralHeatingCopApproximator.py index d8526c396..11be7407f 100644 --- a/scripts/build_cop_profiles/DecentralHeatingCopApproximator.py +++ b/scripts/build_cop_profiles/DecentralHeatingCopApproximator.py @@ -39,12 +39,12 @@ def __init__( return_temperature_celsius : Union[xr.DataArray, np.array] The return temperature in Celsius. source: str - The source of the heat pump. Must be either 'air' or 'soil' + The source of the heat pump. Must be either 'air' or 'ground' """ self.delta_t = forward_temperature_celsius - source_inlet_temperature_celsius - if source_type not in ["air", "soil"]: - raise ValueError("'source' must be one of ['air', 'soil']") + if source_type not in ["air", "ground"]: + raise ValueError("'source' must be one of ['air', 'ground']") else: self.source_type = source_type @@ -57,7 +57,7 @@ def approximate_cop(self) -> Union[xr.DataArray, np.array]: """ if self.source_type == "air": return self._approximate_cop_air_source() - elif self.source_type == "soil": + elif self.source_type == "ground": return self._approximate_cop_ground_source() def _approximate_cop_air_source(self) -> Union[xr.DataArray, np.array]: diff --git a/scripts/build_cop_profiles/run.py b/scripts/build_cop_profiles/run.py index 12d012bb3..5178483a8 100644 --- a/scripts/build_cop_profiles/run.py +++ b/scripts/build_cop_profiles/run.py @@ -4,11 +4,39 @@ # SPDX-License-Identifier: MIT import numpy as np +import pandas as pd import xarray as xr from _helpers import set_scenario_config from CentralHeatingCopApproximator import CentralHeatingCopApproximator from DecentralHeatingCopApproximator import DecentralHeatingCopApproximator + +def get_cop( + heat_system_type: str, + heat_source: str, + source_inlet_temperature_celsius: xr.DataArray, +) -> xr.DataArray: + if heat_system_type == "decentral": + return DecentralHeatingCopApproximator( + forward_temperature_celsius=snakemake.params.heat_pump_sink_T_decentral_heating, + source_inlet_temperature_celsius=source_inlet_temperature_celsius, + source_type=heat_source, + ).approximate_cop() + + elif heat_system_type == "central": + return CentralHeatingCopApproximator( + forward_temperature_celsius=snakemake.params.forward_temperature_central_heating, + return_temperature_celsius=snakemake.params.return_temperature_central_heating, + source_inlet_temperature_celsius=source_inlet_temperature_celsius, + source_outlet_temperature_celsius=source_inlet_temperature_celsius + - snakemake.params.heat_source_cooling_central_heating, + ).approximate_cop() + else: + raise ValueError( + f"Invalid heat system type '{heat_system_type}'. Must be one of ['decentral', 'central']" + ) + + if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake @@ -21,30 +49,28 @@ set_scenario_config(snakemake) - for source_type in ["air", "soil"]: - # source inlet temperature (air/soil) is based on weather data - source_inlet_temperature_celsius = xr.open_dataarray( - snakemake.input[f"temp_{source_type}_total"] + cop_all_system_types = [] + for heat_system_type, heat_sources in snakemake.params.heat_pump_sources.items(): + cop_this_system_type = [] + for heat_source in heat_sources: + source_inlet_temperature_celsius = xr.open_dataarray( + snakemake.input[f"temp_{heat_source.replace('ground', 'soil')}_total"] + ) + cop_da = get_cop( + heat_system_type=heat_system_type, + heat_source=heat_source, + source_inlet_temperature_celsius=source_inlet_temperature_celsius, + ) + cop_this_system_type.append(cop_da) + cop_all_system_types.append( + xr.concat( + cop_this_system_type, dim=pd.Index(heat_sources, name="heat_source") + ) ) - # Approximate COP for decentral (individual) heating - cop_individual_heating = DecentralHeatingCopApproximator( - forward_temperature_celsius=snakemake.params.heat_pump_sink_T_decentral_heating, - source_inlet_temperature_celsius=source_inlet_temperature_celsius, - source_type=source_type, - ).approximate_cop() - cop_individual_heating.to_netcdf( - snakemake.output[f"cop_{source_type}_decentral_heating"] - ) + cop_dataarray = xr.concat( + cop_all_system_types, + dim=pd.Index(snakemake.params.heat_pump_sources.keys(), name="heat_system"), + ) - # Approximate COP for central (district) heating - cop_central_heating = CentralHeatingCopApproximator( - forward_temperature_celsius=snakemake.params.forward_temperature_central_heating, - return_temperature_celsius=snakemake.params.return_temperature_central_heating, - source_inlet_temperature_celsius=source_inlet_temperature_celsius, - source_outlet_temperature_celsius=source_inlet_temperature_celsius - - snakemake.params.heat_source_cooling_central_heating, - ).approximate_cop() - cop_central_heating.to_netcdf( - snakemake.output[f"cop_{source_type}_central_heating"] - ) + cop_dataarray.to_netcdf(snakemake.output.cop_profiles) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index d5b892adf..e59984d3b 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1825,23 +1825,6 @@ def add_heat(n, costs): "urban central", ] - cop = { - "air decentral": xr.open_dataarray(snakemake.input.cop_air_decentral_heating) - .to_pandas() - .reindex(index=n.snapshots), - "ground decentral": xr.open_dataarray( - snakemake.input.cop_soil_decentral_heating - ) - .to_pandas() - .reindex(index=n.snapshots), - "air central": xr.open_dataarray(snakemake.input.cop_air_central_heating) - .to_pandas() - .reindex(index=n.snapshots), - "ground central": xr.open_dataarray(snakemake.input.cop_soil_central_heating) - .to_pandas() - .reindex(index=n.snapshots), - } - if options["solar_thermal"]: solar_thermal = ( xr.open_dataarray(snakemake.input.solar_thermal_total) @@ -1851,31 +1834,32 @@ def add_heat(n, costs): # 1e3 converts from W/m^2 to MW/(1000m^2) = kW/m^2 solar_thermal = options["solar_cf_correction"] * solar_thermal / 1e3 - for name in heat_systems: - name_type = "central" if name == "urban central" else "decentral" + cop = xr.open_dataarray(snakemake.input.cop_profiles) + for heat_system in heat_systems: + system_type = "central" if heat_system == "urban central" else "decentral" - if name == "urban central": + if heat_system == "urban central": nodes = dist_fraction.index[dist_fraction > 0] else: nodes = pop_layout.index - n.add("Carrier", name + " heat") + n.add("Carrier", heat_system + " heat") n.madd( "Bus", - nodes + f" {name} heat", + nodes + f" {heat_system} heat", location=nodes, - carrier=name + " heat", + carrier=heat_system + " heat", unit="MWh_th", ) - if name == "urban central" and options.get("central_heat_vent"): + if heat_system == "urban central" and options.get("central_heat_vent"): n.madd( "Generator", - nodes + f" {name} heat vent", - bus=nodes + f" {name} heat", + nodes + f" {heat_system} heat vent", + bus=nodes + f" {heat_system} heat", location=nodes, - carrier=name + " heat vent", + carrier=heat_system + " heat vent", p_nom_extendable=True, p_max_pu=0, p_min_pu=-1, @@ -1886,18 +1870,18 @@ def add_heat(n, costs): for sector in sectors: # heat demand weighting - if "rural" in name: + if "rural" in heat_system: factor = 1 - urban_fraction[nodes] - elif "urban central" in name: + elif "urban central" in heat_system: factor = dist_fraction[nodes] - elif "urban decentral" in name: + elif "urban decentral" in heat_system: factor = urban_fraction[nodes] - dist_fraction[nodes] else: raise NotImplementedError( - f" {name} not in " f"heat systems: {heat_systems}" + f" {heat_system} not in " f"heat systems: {heat_systems}" ) - if sector in name: + if sector in heat_system: heat_load = ( heat_demand[[sector + " water", sector + " space"]] .T.groupby(level=1) @@ -1906,7 +1890,7 @@ def add_heat(n, costs): .multiply(factor) ) - if name == "urban central": + if heat_system == "urban central": heat_load = ( heat_demand.T.groupby(level=1) .sum() @@ -1919,20 +1903,17 @@ def add_heat(n, costs): n.madd( "Load", nodes, - suffix=f" {name} heat", - bus=nodes + f" {name} heat", - carrier=name + " heat", + suffix=f" {heat_system} heat", + bus=nodes + f" {heat_system} heat", + carrier=heat_system + " heat", p_set=heat_load, ) ## Add heat pumps - - heat_pump_types = ["air"] if "urban" in name else ["ground", "air"] - - for heat_pump_type in heat_pump_types: - costs_name = f"{name_type} {heat_pump_type}-sourced heat pump" + for heat_source in snakemake.params.heat_pump_sources[system_type]: + costs_name = f"{system_type} {heat_source}-sourced heat pump" efficiency = ( - cop[f"{heat_pump_type} {name_type}"][nodes] + cop.sel(heat_system=system_type, heat_source=heat_source, name=nodes).to_pandas().reindex(index=n.snapshots) if options["time_dep_hp_cop"] else costs.at[costs_name, "efficiency"] ) @@ -1940,10 +1921,10 @@ def add_heat(n, costs): n.madd( "Link", nodes, - suffix=f" {name} {heat_pump_type} heat pump", + suffix=f" {heat_system} {heat_source} heat pump", bus0=nodes, - bus1=nodes + f" {name} heat", - carrier=f"{name} {heat_pump_type} heat pump", + bus1=nodes + f" {heat_system} heat", + carrier=f"{heat_system} {heat_source} heat pump", efficiency=efficiency, capital_cost=costs.at[costs_name, "efficiency"] * costs.at[costs_name, "fixed"] @@ -1953,59 +1934,59 @@ def add_heat(n, costs): ) if options["tes"]: - n.add("Carrier", name + " water tanks") + n.add("Carrier", heat_system + " water tanks") n.madd( "Bus", - nodes + f" {name} water tanks", + nodes + f" {heat_system} water tanks", location=nodes, - carrier=name + " water tanks", + carrier=heat_system + " water tanks", unit="MWh_th", ) n.madd( "Link", - nodes + f" {name} water tanks charger", - bus0=nodes + f" {name} heat", - bus1=nodes + f" {name} water tanks", + nodes + f" {heat_system} water tanks charger", + bus0=nodes + f" {heat_system} heat", + bus1=nodes + f" {heat_system} water tanks", efficiency=costs.at["water tank charger", "efficiency"], - carrier=name + " water tanks charger", + carrier=heat_system + " water tanks charger", p_nom_extendable=True, ) n.madd( "Link", - nodes + f" {name} water tanks discharger", - bus0=nodes + f" {name} water tanks", - bus1=nodes + f" {name} heat", - carrier=name + " water tanks discharger", + nodes + f" {heat_system} water tanks discharger", + bus0=nodes + f" {heat_system} water tanks", + bus1=nodes + f" {heat_system} heat", + carrier=heat_system + " water tanks discharger", efficiency=costs.at["water tank discharger", "efficiency"], p_nom_extendable=True, ) - tes_time_constant_days = options["tes_tau"][name_type] + tes_time_constant_days = options["tes_tau"][system_type] n.madd( "Store", - nodes + f" {name} water tanks", - bus=nodes + f" {name} water tanks", + nodes + f" {heat_system} water tanks", + bus=nodes + f" {heat_system} water tanks", e_cyclic=True, e_nom_extendable=True, - carrier=name + " water tanks", + carrier=heat_system + " water tanks", standing_loss=1 - np.exp(-1 / 24 / tes_time_constant_days), - capital_cost=costs.at[name_type + " water tank storage", "fixed"], - lifetime=costs.at[name_type + " water tank storage", "lifetime"], + capital_cost=costs.at[system_type + " water tank storage", "fixed"], + lifetime=costs.at[system_type + " water tank storage", "lifetime"], ) if options["resistive_heaters"]: - key = f"{name_type} resistive heater" + key = f"{system_type} resistive heater" n.madd( "Link", - nodes + f" {name} resistive heater", + nodes + f" {heat_system} resistive heater", bus0=nodes, - bus1=nodes + f" {name} heat", - carrier=name + " resistive heater", + bus1=nodes + f" {heat_system} heat", + carrier=heat_system + " resistive heater", efficiency=costs.at[key, "efficiency"], capital_cost=costs.at[key, "efficiency"] * costs.at[key, "fixed"] @@ -2015,16 +1996,16 @@ def add_heat(n, costs): ) if options["boilers"]: - key = f"{name_type} gas boiler" + key = f"{system_type} gas boiler" n.madd( "Link", - nodes + f" {name} gas boiler", + nodes + f" {heat_system} gas boiler", p_nom_extendable=True, bus0=spatial.gas.df.loc[nodes, "nodes"].values, - bus1=nodes + f" {name} heat", + bus1=nodes + f" {heat_system} heat", bus2="co2 atmosphere", - carrier=name + " gas boiler", + carrier=heat_system + " gas boiler", efficiency=costs.at[key, "efficiency"], efficiency2=costs.at["gas", "CO2 intensity"], capital_cost=costs.at[key, "efficiency"] @@ -2034,22 +2015,22 @@ def add_heat(n, costs): ) if options["solar_thermal"]: - n.add("Carrier", name + " solar thermal") + n.add("Carrier", heat_system + " solar thermal") n.madd( "Generator", nodes, - suffix=f" {name} solar thermal collector", - bus=nodes + f" {name} heat", - carrier=name + " solar thermal", + suffix=f" {heat_system} solar thermal collector", + bus=nodes + f" {heat_system} heat", + carrier=heat_system + " solar thermal", p_nom_extendable=True, - capital_cost=costs.at[name_type + " solar thermal", "fixed"] + capital_cost=costs.at[system_type + " solar thermal", "fixed"] * overdim_factor, p_max_pu=solar_thermal[nodes], - lifetime=costs.at[name_type + " solar thermal", "lifetime"], + lifetime=costs.at[system_type + " solar thermal", "lifetime"], ) - if options["chp"] and name == "urban central": + if options["chp"] and heat_system == "urban central": # add gas CHP; biomass CHP is added in biomass section n.madd( "Link", @@ -2106,16 +2087,16 @@ def add_heat(n, costs): lifetime=costs.at["central gas CHP", "lifetime"], ) - if options["chp"] and options["micro_chp"] and name != "urban central": + if options["chp"] and options["micro_chp"] and heat_system != "urban central": n.madd( "Link", - nodes + f" {name} micro gas CHP", + nodes + f" {heat_system} micro gas CHP", p_nom_extendable=True, bus0=spatial.gas.df.loc[nodes, "nodes"].values, bus1=nodes, - bus2=nodes + f" {name} heat", + bus2=nodes + f" {heat_system} heat", bus3="co2 atmosphere", - carrier=name + " micro gas CHP", + carrier=heat_system + " micro gas CHP", efficiency=costs.at["micro CHP", "efficiency"], efficiency2=costs.at["micro CHP", "efficiency-heat"], efficiency3=costs.at["gas", "CO2 intensity"], @@ -2150,27 +2131,27 @@ def add_heat(n, costs): heat_demand["services space"] + heat_demand["residential space"] ) / heat_demand.T.groupby(level=[1]).sum().T - for name in n.loads[ + for heat_system in n.loads[ n.loads.carrier.isin([x + " heat" for x in heat_systems]) ].index: - node = n.buses.loc[name, "location"] + node = n.buses.loc[heat_system, "location"] ct = pop_layout.loc[node, "ct"] # weighting 'f' depending on the size of the population at the node - if "urban central" in name: + if "urban central" in heat_system: f = dist_fraction[node] - elif "urban decentral" in name: + elif "urban decentral" in heat_system: f = urban_fraction[node] - dist_fraction[node] else: f = 1 - urban_fraction[node] if f == 0: continue # get sector name ("residential"/"services"/or both "tot" for urban central) - if "urban central" in name: + if "urban central" in heat_system: sec = "tot" - if "residential" in name: + if "residential" in heat_system: sec = "residential" - if "services" in name: + if "services" in heat_system: sec = "services" # get floor aread at node and region (urban/rural) in m^2 @@ -2178,7 +2159,7 @@ def add_heat(n, costs): pop_layout.loc[node].fraction * floor_area.loc[ct, "value"] * 10**6 ).loc[sec] * f # total heat demand at node [MWh] - demand = n.loads_t.p_set[name] + demand = n.loads_t.p_set[heat_system] # space heat demand at node [MWh] space_heat_demand = demand * w_space[sec][node] @@ -2219,12 +2200,12 @@ def add_heat(n, costs): # add for each retrofitting strength a generator with heat generation profile following the profile of the heat demand for strength in strengths: - node_name = " ".join(name.split(" ")[2::]) + node_name = " ".join(heat_system.split(" ")[2::]) n.madd( "Generator", [node], suffix=" retrofitting " + strength + " " + node_name, - bus=name, + bus=heat_system, carrier="retrofitting", p_nom_extendable=True, p_nom_max=dE_diff[strength] From 1fe54513cd8f33961263d21a693dd4f8cf0c595f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 29 Jul 2024 16:33:49 +0000 Subject: [PATCH 092/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- config/config.default.yaml | 6 +++--- rules/build_sector.smk | 2 ++ scripts/add_existing_baseyear.py | 13 +++++++++---- scripts/prepare_sector_network.py | 4 +++- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 9f5954010..794b2baea 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -418,10 +418,10 @@ sector: heat_loss: 0.0 heat_pump_sources: central: - - air + - air decentral: - - air - - ground + - air + - ground cluster_heat_buses: true heat_demand_cutout: default bev_dsm_restriction_value: 0.75 diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 9b2bfde65..b7d916ea4 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -214,6 +214,7 @@ rule build_temperature_profiles: script: "../scripts/build_temperature_profiles.py" + # def output_cop(wildcards): # return { # f"cop_{source}_{sink}": resources( @@ -222,6 +223,7 @@ rule build_temperature_profiles: # for sink, source in config["sector"]["heat_pump_sources"].items() # } + rule build_cop_profiles: params: heat_pump_sink_T_decentral_heating=config_provider( diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index 00b5eeb7e..610523e99 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -445,7 +445,9 @@ def add_heating_capacities_installed_before_baseyear( for heat_system in existing_heating.columns.get_level_values(0).unique(): system_type = "central" if heat_system == "urban central" else "decentral" - nodes = pd.Index(n.buses.location[n.buses.index.str.contains(f"{heat_system} heat")]) + nodes = pd.Index( + n.buses.location[n.buses.index.str.contains(f"{heat_system} heat")] + ) if (system_type != "central") and options["electricity_distribution_grid"]: nodes_elec = nodes + " low voltage" @@ -457,12 +459,13 @@ def add_heating_capacities_installed_before_baseyear( costs_name = f"{system_type} {heat_source}-sourced heat pump" efficiency = ( - cop.sel(heat_system=system_type, heat_source=heat_source, name=nodes).to_pandas().reindex(index=n.snapshots) + cop.sel(heat_system=system_type, heat_source=heat_source, name=nodes) + .to_pandas() + .reindex(index=n.snapshots) if options["time_dep_hp_cop"] else costs.at[costs_name, "efficiency"] ) - too_large_grouping_years = [gy for gy in grouping_years if gy >= int(baseyear)] if too_large_grouping_years: logger.warning( @@ -498,7 +501,9 @@ def add_heating_capacities_installed_before_baseyear( efficiency=efficiency, capital_cost=costs.at[costs_name, "efficiency"] * costs.at[costs_name, "fixed"], - p_nom=existing_heating.loc[nodes, (heat_system, f"{heat_source} heat pump")] + p_nom=existing_heating.loc[ + nodes, (heat_system, f"{heat_source} heat pump") + ] * ratio / costs.at[costs_name, "efficiency"], build_year=int(grouping_year), diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index e59984d3b..f71dd5f58 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1913,7 +1913,9 @@ def add_heat(n, costs): for heat_source in snakemake.params.heat_pump_sources[system_type]: costs_name = f"{system_type} {heat_source}-sourced heat pump" efficiency = ( - cop.sel(heat_system=system_type, heat_source=heat_source, name=nodes).to_pandas().reindex(index=n.snapshots) + cop.sel(heat_system=system_type, heat_source=heat_source, name=nodes) + .to_pandas() + .reindex(index=n.snapshots) if options["time_dep_hp_cop"] else costs.at[costs_name, "efficiency"] ) From b92f0d0c3086e03ff4ae88733e3ca46bc0af206c Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Tue, 30 Jul 2024 15:26:28 +0200 Subject: [PATCH 093/344] update industry sector ratios to new JRC data --- rules/build_sector.smk | 2 +- scripts/build_industry_sector_ratios.py | 442 +++++++++++++----------- 2 files changed, 235 insertions(+), 209 deletions(-) diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 0ced9c058..0722bffae 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -459,7 +459,7 @@ rule build_industry_sector_ratios: ammonia=config_provider("sector", "ammonia", default=False), input: ammonia_production=resources("ammonia_production.csv"), - idees="data/bundle/jrc-idees-2015", + idees="data/bundle/jrc-idees-2021", output: industry_sector_ratios=resources("industry_sector_ratios.csv"), threads: 1 diff --git a/scripts/build_industry_sector_ratios.py b/scripts/build_industry_sector_ratios.py index b7a62d91b..0f8bec24c 100644 --- a/scripts/build_industry_sector_ratios.py +++ b/scripts/build_industry_sector_ratios.py @@ -17,7 +17,7 @@ Inputs ------- - ``resources/ammonia_production.csv`` -- ``data/bundle-sector/jrc-idees-2015`` +- ``data/bundle-sector/jrc-idees-2021`` Outputs ------- @@ -50,40 +50,17 @@ import pandas as pd from _helpers import mute_print, set_scenario_config +import country_converter as coco + + +cc = coco.CountryConverter() # GWh/ktoe OR MWh/toe toe_to_MWh = 11.630 -eu28 = [ - "FR", - "DE", - "GB", - "IT", - "ES", - "PL", - "SE", - "NL", - "BE", - "FI", - "DK", - "PT", - "RO", - "AT", - "BG", - "EE", - "GR", - "LV", - "CZ", - "HU", - "IE", - "SK", - "LT", - "HR", - "LU", - "SI", - "CY", - "MT", -] +eu27 = cc.EU27as("ISO2").ISO2.tolist() + + sheet_names = { "Iron and steel": "ISI", @@ -92,11 +69,11 @@ "Pulp, paper and printing": "PPA", "Food, beverages and tobacco": "FBT", "Non Ferrous Metals": "NFM", - "Transport Equipment": "TRE", - "Machinery Equipment": "MAE", + "Transport equipment": "TRE", + "Machinery equipment": "MAE", "Textiles and leather": "TEL", "Wood and wood products": "WWP", - "Other Industrial Sectors": "OIS", + "Other industrial sectors": "OIS", } @@ -116,7 +93,7 @@ ] -def load_idees_data(sector, country="EU28"): +def load_idees_data(sector, country="EU27"): suffixes = {"out": "", "fec": "_fec", "ued": "_ued", "emi": "_emi"} sheets = {k: sheet_names[sector] + v for k, v in suffixes.items()} @@ -125,7 +102,7 @@ def usecols(x): with mute_print(): idees = pd.read_excel( - f"{snakemake.input.idees}/JRC-IDEES-2015_Industry_{country}.xlsx", + f"{snakemake.input.idees}/{country}/JRC-IDEES-2021_Industry_{country}.xlsx", sheet_name=list(sheets.values()), index_col=0, header=0, @@ -134,15 +111,25 @@ def usecols(x): for k, v in sheets.items(): idees[k] = idees.pop(v).squeeze() + idees[k] = idees[k][year] return idees def iron_and_steel(): - # There are two different approaches to produce iron and steel: - # i.e., integrated steelworks and electric arc. - # Electric arc approach has higher efficiency and relies more on electricity. - # We assume that integrated steelworks will be replaced by electric arc entirely. + """ + This function calculates the energy consumption and emissions for different + approaches to producing iron and steel. The two primary approaches are + integrated steelworks and electric arc furnaces (EAF). The function assumes + that integrated steelworks will be replaced entirely by electric arc + furnaces due to their higher efficiency and greater reliance on electricity. + + Returns: + pd.DataFrame: A DataFrame containing the energy consumption (in MWh/t material) + and process emissions (in tCO2/t material) for different steel + production approaches, including electric arc, DRI + electric arc, + and integrated steelworks. + """ sector = "Iron and steel" idees = load_idees_data(sector) @@ -155,51 +142,51 @@ def iron_and_steel(): df[sector] = 0.0 - s_fec = idees["fec"][51:57] + s_fec = idees["fec"][52:68] assert s_fec.index[0] == sector sel = ["Lighting", "Air compressors", "Motor drives", "Fans and pumps"] - df.at["elec", sector] += s_fec[sel].sum() + df.at["elec", sector] += s_fec.loc[sel].sum() - df.at["heat", sector] += s_fec["Low enthalpy heat"] + df.at["heat", sector] += s_fec.loc["Low-enthalpy heat"] subsector = "Steel: Smelters" - s_fec = idees["fec"][61:67] - s_ued = idees["ued"][61:67] + s_fec = idees["fec"][63:68] + s_ued = idees["ued"][63:68] assert s_fec.index[0] == subsector assert s_ued.index[0] == subsector # efficiency changes due to transforming all the smelters into methane - key = "Natural gas (incl. biogas)" - eff_met = s_ued[key] / s_fec[key] + key = "Natural gas and biogas" + eff_met = s_ued.loc[key] / s_fec.loc[key] df.at["methane", sector] += s_ued[subsector] / eff_met subsector = "Steel: Electric arc" - s_fec = idees["fec"][67:68] + s_fec = idees["fec"][69:70] assert s_fec.index[0] == subsector df.at["elec", sector] += s_fec[subsector] - subsector = "Steel: Furnaces, Refining and Rolling" - s_fec = idees["fec"][68:75] - s_ued = idees["ued"][68:75] + subsector = "Steel: Furnaces, refining and rolling" + s_fec = idees["fec"][70:77] + s_ued = idees["ued"][70:77] assert s_fec.index[0] == subsector assert s_ued.index[0] == subsector - key = "Steel: Furnaces, Refining and Rolling - Electric" + key = "Steel: Furnaces, refining and rolling - Electric" eff = s_ued[key] / s_fec[key] # assume fully electrified, other processes scaled by used energy df.at["elec", sector] += s_ued[subsector] / eff - subsector = "Steel: Products finishing" - s_fec = idees["fec"][75:92] - s_ued = idees["ued"][75:92] + subsector = "Steel: Product finishing" + s_fec = idees["fec"][77:95] + s_ued = idees["ued"][77:95] assert s_fec.index[0] == subsector assert s_ued.index[0] == subsector - key = "Steel: Products finishing - Electric" + key = "Steel: Product finishing - Electric" eff = s_ued[key] / s_fec[key] # assume fully electrified @@ -207,7 +194,7 @@ def iron_and_steel(): # Process emissions (per physical output) - s_emi = idees["emi"][51:93] + s_emi = idees["emi"][52:95] assert s_emi.index[0] == sector s_out = idees["out"][7:8] @@ -242,63 +229,63 @@ def iron_and_steel(): df[sector] = 0.0 - s_fec = idees["fec"][3:9] + s_fec = idees["fec"][3:50] assert s_fec.index[0] == sector sel = ["Lighting", "Air compressors", "Motor drives", "Fans and pumps"] df.loc["elec", sector] += s_fec[sel].sum() - df.loc["heat", sector] += s_fec["Low enthalpy heat"] + df.loc["heat", sector] += s_fec["Low-enthalpy heat"] - subsector = "Steel: Sinter/Pellet making" + subsector = 'Steel: Sinter/Pellet-making' - s_fec = idees["fec"][13:19] - s_ued = idees["ued"][13:19] + s_fec = idees["fec"][14:20] + s_ued = idees["ued"][14:20] assert s_fec.index[0] == subsector assert s_ued.index[0] == subsector df.loc["elec", sector] += s_fec["Electricity"] - sel = ["Natural gas (incl. biogas)", "Residual fuel oil"] + sel = ["Natural gas and biogas", "Fuel oil"] df.loc["methane", sector] += s_fec[sel].sum() df.loc["coal", sector] += s_fec["Solids"] - subsector = "Steel: Blast /Basic oxygen furnace" + subsector = 'Steel: Blast /Basic oxygen furnace' - s_fec = idees["fec"][19:25] - s_ued = idees["ued"][19:25] + s_fec = idees["fec"][20:26] + s_ued = idees["ued"][20:26] assert s_fec.index[0] == subsector assert s_ued.index[0] == subsector - sel = ["Natural gas (incl. biogas)", "Residual fuel oil"] + sel = ["Natural gas and biogas", "Fuel oil"] df.loc["methane", sector] += s_fec[sel].sum() df.loc["coal", sector] += s_fec["Solids"] df.loc["coke", sector] = s_fec["Coke"] - subsector = "Steel: Furnaces, Refining and Rolling" + subsector = "Steel: Furnaces, refining and rolling" - s_fec = idees["fec"][25:32] - s_ued = idees["ued"][25:32] + s_fec = idees["fec"][26:33] + s_ued = idees["ued"][26:33] assert s_fec.index[0] == subsector assert s_ued.index[0] == subsector - key = "Steel: Furnaces, Refining and Rolling - Electric" + key = 'Steel: Furnaces, refining and rolling - Electric' eff = s_ued[key] / s_fec[key] # assume fully electrified, other processes scaled by used energy df.loc["elec", sector] += s_ued[subsector] / eff - subsector = "Steel: Products finishing" + subsector = "Steel: Product finishing" - s_fec = idees["fec"][32:49] - s_ued = idees["ued"][32:49] + s_fec = idees["fec"][33:50] + s_ued = idees["ued"][33:50] assert s_fec.index[0] == subsector assert s_ued.index[0] == subsector - key = "Steel: Products finishing - Electric" + key = 'Steel: Product finishing - Electric' eff = s_ued[key] / s_fec[key] # assume fully electrified @@ -306,7 +293,7 @@ def iron_and_steel(): # Process emissions (per physical output) - s_emi = idees["emi"][3:50] + s_emi = idees["emi"][3:51] assert s_emi.index[0] == sector s_out = idees["out"][6:7] @@ -323,6 +310,17 @@ def iron_and_steel(): def chemicals_industry(): + """ + This function calculates the energy consumption and emissions for the chemicals industry, + focusing on various subsectors such as basic chemicals, steam processing, furnaces, + and process cooling. The function also accounts for specific processes in ammonia, + chlorine, methanol production, and other chemicals. + + Returns: + pd.DataFrame: A DataFrame containing the energy consumption (in MWh/t material) + and process emissions (in tCO2/t material) for various subsectors + within the chemicals industry. + """ sector = "Chemicals Industry" idees = load_idees_data(sector) @@ -340,15 +338,15 @@ def chemicals_industry(): sel = ["Lighting", "Air compressors", "Motor drives", "Fans and pumps"] df.loc["elec", sector] += s_fec[sel].sum() - df.loc["heat", sector] += s_fec["Low enthalpy heat"] + df.loc["heat", sector] += s_fec["Low-enthalpy heat"] subsector = "Chemicals: Feedstock (energy used as raw material)" - # There are Solids, Refinery gas, LPG, Diesel oil, Residual fuel oil, + # There are Solids, Refinery gas, LPG, Diesel oil, Fuel oil, # Other liquids, Naphtha, Natural gas for feedstock. # Naphta represents 47%, methane 17%. LPG (18%) solids, refinery gas, - # diesel oil, residual fuel oils and other liquids are assimilated to Naphtha + # diesel oil, Fuel oils and other liquids are assimilated to Naphtha - s_fec = idees["fec"][13:22] + s_fec = idees["fec"][14:23] assert s_fec.index[0] == subsector df.loc["naphtha", sector] += s_fec["Naphtha"] @@ -362,7 +360,7 @@ def chemicals_industry(): "Refinery gas", "LPG", "Diesel oil", - "Residual fuel oil", + "Fuel oil", "Other liquids", ] df.loc["naphtha", sector] += s_fec[sel].sum() @@ -372,21 +370,21 @@ def chemicals_industry(): # converted to methane, since we need >1000 C temperatures here. # The current efficiency of methane is assumed in the conversion. - s_fec = idees["fec"][22:33] - s_ued = idees["ued"][22:33] + s_fec = idees["fec"][23:34] + s_ued = idees["ued"][23:34] assert s_fec.index[0] == subsector assert s_ued.index[0] == subsector # efficiency of natural gas - eff_ch4 = s_ued["Natural gas (incl. biogas)"] / s_fec["Natural gas (incl. biogas)"] + eff_ch4 = s_ued["Natural gas and biogas"] / s_fec["Natural gas and biogas"] # replace all fec by methane df.loc["methane", sector] += s_ued[subsector] / eff_ch4 subsector = "Chemicals: Furnaces" - s_fec = idees["fec"][33:41] - s_ued = idees["ued"][33:41] + s_fec = idees["fec"][34:42] + s_ued = idees["ued"][34:42] assert s_fec.index[0] == subsector assert s_ued.index[0] == subsector @@ -399,8 +397,8 @@ def chemicals_industry(): subsector = "Chemicals: Process cooling" - s_fec = idees["fec"][41:55] - s_ued = idees["ued"][41:55] + s_fec = idees["fec"][42:56] + s_ued = idees["ued"][42:56] assert s_fec.index[0] == subsector assert s_ued.index[0] == subsector @@ -412,17 +410,17 @@ def chemicals_industry(): subsector = "Chemicals: Generic electric process" - s_fec = idees["fec"][55:56] + s_fec = idees["fec"][56:57] assert s_fec.index[0] == subsector df.loc["elec", sector] += s_fec[subsector] # Process emissions - # Correct everything by subtracting 2015's ammonia demand and + # Correct everything by subtracting 2019's ammonia demand and # putting in ammonia demand for H2 and electricity separately - s_emi = idees["emi"][3:57] + s_emi = idees["emi"][3:58] assert s_emi.index[0] == sector # convert from MtHVC/a to ktHVC/a @@ -447,7 +445,7 @@ def chemicals_industry(): # subtract ammonia energy demand (in ktNH3/a) ammonia = pd.read_csv(snakemake.input.ammonia_production, index_col=0) - ammonia_total = ammonia.loc[ammonia.index.intersection(eu28), str(year)].sum() + ammonia_total = ammonia.loc[ammonia.index.intersection(eu27), str(year)].sum() df.loc["methane", sector] -= ammonia_total * params["MWh_CH4_per_tNH3_SMR"] df.loc["elec", sector] -= ammonia_total * params["MWh_elec_per_tNH3_SMR"] @@ -507,22 +505,22 @@ def chemicals_industry(): df[sector] = 0.0 - s_fec = idees["fec"][58:64] + s_fec = idees["fec"][59:65] assert s_fec.index[0] == sector sel = ["Lighting", "Air compressors", "Motor drives", "Fans and pumps"] df.loc["elec", sector] += s_fec[sel].sum() - df.loc["heat", sector] += s_fec["Low enthalpy heat"] + df.loc["heat", sector] += s_fec["Low-enthalpy heat"] - subsector = "Chemicals: High enthalpy heat processing" + subsector = 'Chemicals: High-enthalpy heat processing' - s_fec = idees["fec"][68:81] - s_ued = idees["ued"][68:81] + s_fec = idees["fec"][70:83] + s_ued = idees["ued"][70:83] assert s_fec.index[0] == subsector assert s_ued.index[0] == subsector - key = "High enthalpy heat processing - Electric (microwave)" + key = 'High-enthalpy heat processing - Electric (microwave)' eff_elec = s_ued[key] / s_fec[key] # assume fully electrified @@ -530,8 +528,8 @@ def chemicals_industry(): subsector = "Chemicals: Furnaces" - s_fec = idees["fec"][81:89] - s_ued = idees["ued"][81:89] + s_fec = idees["fec"][83:92] + s_ued = idees["ued"][83:92] assert s_fec.index[0] == subsector assert s_ued.index[0] == subsector @@ -543,8 +541,8 @@ def chemicals_industry(): subsector = "Chemicals: Process cooling" - s_fec = idees["fec"][89:103] - s_ued = idees["ued"][89:103] + s_fec = idees["fec"][91:105] + s_ued = idees["ued"][91:105] assert s_fec.index[0] == subsector assert s_ued.index[0] == subsector @@ -556,14 +554,14 @@ def chemicals_industry(): subsector = "Chemicals: Generic electric process" - s_fec = idees["fec"][103:104] + s_fec = idees["fec"][105:106] assert s_fec.index[0] == subsector df.loc["elec", sector] += s_fec[subsector] # Process emissions - s_emi = idees["emi"][58:105] + s_emi = idees["emi"][59:107] s_out = idees["out"][9:10] assert s_emi.index[0] == sector assert sector in str(s_out.index) @@ -581,22 +579,22 @@ def chemicals_industry(): df[sector] = 0.0 - s_fec = idees["fec"][106:112] + s_fec = idees["fec"][108:114] assert s_fec.index[0] == sector sel = ["Lighting", "Air compressors", "Motor drives", "Fans and pumps"] df.loc["elec", sector] += s_fec[sel].sum() - df.loc["heat", sector] += s_fec["Low enthalpy heat"] + df.loc["heat", sector] += s_fec["Low-enthalpy heat"] - subsector = "Chemicals: High enthalpy heat processing" + subsector = 'Chemicals: High-enthalpy heat processing' - s_fec = idees["fec"][116:129] - s_ued = idees["ued"][116:129] + s_fec = idees["fec"][119:132] + s_ued = idees["ued"][119:132] assert s_fec.index[0] == subsector assert s_ued.index[0] == subsector - key = "High enthalpy heat processing - Electric (microwave)" + key = 'High-enthalpy heat processing - Electric (microwave)' eff_elec = s_ued[key] / s_fec[key] # assume fully electrified @@ -604,8 +602,8 @@ def chemicals_industry(): subsector = "Chemicals: Furnaces" - s_fec = idees["fec"][129:137] - s_ued = idees["ued"][129:137] + s_fec = idees["fec"][132:140] + s_ued = idees["ued"][132:140] assert s_fec.index[0] == subsector assert s_ued.index[0] == subsector @@ -617,8 +615,8 @@ def chemicals_industry(): subsector = "Chemicals: Process cooling" - s_fec = idees["fec"][137:151] - s_ued = idees["ued"][137:151] + s_fec = idees["fec"][140:154] + s_ued = idees["ued"][140:154] assert s_fec.index[0] == subsector assert s_ued.index[0] == subsector @@ -630,7 +628,7 @@ def chemicals_industry(): subsector = "Chemicals: Generic electric process" - s_fec = idees["fec"][151:152] + s_fec = idees["fec"][154:155] s_out = idees["out"][10:11] assert s_fec.index[0] == subsector assert sector in str(s_out.index) @@ -648,8 +646,16 @@ def chemicals_industry(): def nonmetalic_mineral_products(): - # This includes cement, ceramic and glass production. - # This includes process emissions related to the fabrication of clinker. + """ + This function calculates the energy consumption and emissions for the non-metallic mineral + products industry, focusing on three main sectors: cement, ceramics, and glass production. + It takes into account the specific processes and their associated energy types and emissions. + + Returns: + pd.DataFrame: A DataFrame containing the energy consumption (in MWh/t material) + and process emissions (in tCO2/t material) for the cement, ceramics, + and glass production sectors. + """ sector = "Non-metallic mineral products" idees = load_idees_data(sector) @@ -681,27 +687,27 @@ def nonmetalic_mineral_products(): sel = ["Lighting", "Air compressors", "Motor drives", "Fans and pumps"] df.loc["elec", sector] += s_fec[sel].sum() - df.loc["heat", sector] += s_fec["Low enthalpy heat"] + df.loc["heat", sector] += s_fec["Low-enthalpy heat"] # pre-processing: keep existing elec and biomass, rest to methane df.loc["elec", sector] += s_fec["Cement: Grinding, milling of raw material"] - df.loc["biomass", sector] += s_fec["Biomass"] + df.loc["biomass", sector] += s_fec['Biomass and waste'] df.loc["methane", sector] += ( - s_fec["Cement: Pre-heating and pre-calcination"] - s_fec["Biomass"] + s_fec["Cement: Pre-heating and pre-calcination"] - s_fec['Biomass and waste'] ) subsector = "Cement: Clinker production (kilns)" - s_fec = idees["fec"][34:43] - s_ued = idees["ued"][34:43] + s_fec = idees["fec"][23:32] + s_ued = idees["ued"][23:32] assert s_fec.index[0] == subsector assert s_ued.index[0] == subsector - df.loc["biomass", sector] += s_fec["Biomass"] + df.loc["biomass", sector] += s_fec['Biomass and waste'] df.loc["methane", sector] += ( - s_fec["Cement: Clinker production (kilns)"] - s_fec["Biomass"] + s_fec["Cement: Clinker production (kilns)"] - s_fec['Biomass and waste'] ) - df.loc["elec", sector] += s_fec["Cement: Grinding, packaging"] + df.loc["elec", sector] += s_fec['Cement: Grinding, packaging and precasting'] # Process emissions @@ -709,7 +715,7 @@ def nonmetalic_mineral_products(): # Calcium carbonate -> lime + CO2 # CaCO3 -> CaO + CO2 - s_emi = idees["emi"][3:44] + s_emi = idees["emi"][3:45] assert s_emi.index[0] == sector s_out = idees["out"][7:8] @@ -737,19 +743,21 @@ def nonmetalic_mineral_products(): df[sector] = 0.0 - s_fec = idees["fec"][45:94] - s_ued = idees["ued"][45:94] + s_fec = idees["fec"][46:95] + s_ued = idees["ued"][46:95] assert s_fec.index[0] == sector assert s_ued.index[0] == sector sel = ["Lighting", "Air compressors", "Motor drives", "Fans and pumps"] df.loc["elec", sector] += s_fec[sel].sum() - df.loc["heat", sector] += s_fec["Low enthalpy heat"] + df.loc["heat", sector] += s_fec["Low-enthalpy heat"] # Efficiency changes due to electrification key = "Ceramics: Microwave drying and sintering" - eff_elec = s_ued[key] / s_fec[key] + # the values are zero in new JRC-data -> assume here value from JRC-2015 + # eff_elec = s_ued[key] / s_fec[key] + eff_elec = 11.6/26 sel = [ "Ceramics: Mixing of raw material", @@ -767,7 +775,7 @@ def nonmetalic_mineral_products(): df.loc["elec", sector] += s_ued["Ceramics: Product finishing"] / eff_elec - s_emi = idees["emi"][45:94] + s_emi = idees["emi"][46:96] assert s_emi.index[0] == sector s_out = idees["out"][8:9] @@ -795,15 +803,15 @@ def nonmetalic_mineral_products(): df[sector] = 0.0 - s_fec = idees["fec"][95:123] - s_ued = idees["ued"][95:123] + s_fec = idees["fec"][97:126] + s_ued = idees["ued"][97:126] assert s_fec.index[0] == sector assert s_ued.index[0] == sector sel = ["Lighting", "Air compressors", "Motor drives", "Fans and pumps"] df.loc["elec", sector] += s_fec[sel].sum() - df.loc["heat", sector] += s_fec["Low enthalpy heat"] + df.loc["heat", sector] += s_fec["Low-enthalpy heat"] # Efficiency changes due to electrification key = "Glass: Electric melting tank" @@ -817,7 +825,7 @@ def nonmetalic_mineral_products(): sel = ["Glass: Forming", "Glass: Annealing", "Glass: Finishing processes"] df.loc["elec", sector] += s_ued[sel].sum() / eff_elec - s_emi = idees["emi"][95:124] + s_emi = idees["emi"][97:127] assert s_emi.index[0] == sector s_out = idees["out"][9:10] @@ -834,8 +842,16 @@ def nonmetalic_mineral_products(): def pulp_paper_printing(): - # Pulp, paper and printing can be completely electrified. - # There are no process emissions associated to this sector. + """ + Models the energy consumption for the pulp, paper, and printing sector, + assuming complete electrification of all processes. This sector does not have + any process emissions associated with it. + + Returns: + pd.DataFrame: A DataFrame containing the energy consumption (in MWh/t material) + for the pulp, paper, and printing sector. + + """ sector = "Pulp, paper and printing" idees = load_idees_data(sector) @@ -857,15 +873,15 @@ def pulp_paper_printing(): df[sector] = 0.0 - s_fec = idees["fec"][3:28] - s_ued = idees["ued"][3:28] + s_fec = idees["fec"][3:29] + s_ued = idees["ued"][3:29] assert s_fec.index[0] == sector assert s_ued.index[0] == sector sel = ["Lighting", "Air compressors", "Motor drives", "Fans and pumps"] df.loc["elec", sector] += s_fec[sel].sum() - df.loc["heat", sector] += s_fec["Low enthalpy heat"] + df.loc["heat", sector] += s_fec["Low-enthalpy heat"] # Industry-specific sel = [ @@ -876,7 +892,7 @@ def pulp_paper_printing(): df.loc["elec", sector] += s_fec[sel].sum() # Efficiency changes due to biomass - eff_bio = s_ued["Biomass"] / s_fec["Biomass"] + eff_bio = s_ued['Biomass and waste'] / s_fec['Biomass and waste'] df.loc["biomass", sector] += s_ued["Pulp: Pulping thermal"] / eff_bio s_out = idees["out"][8:9] @@ -906,15 +922,15 @@ def pulp_paper_printing(): df[sector] = 0.0 - s_fec = idees["fec"][29:78] - s_ued = idees["ued"][29:78] + s_fec = idees["fec"][30:80] + s_ued = idees["ued"][30:80] assert s_fec.index[0] == sector assert s_ued.index[0] == sector sel = ["Lighting", "Air compressors", "Motor drives", "Fans and pumps"] df.loc["elec", sector] += s_fec[sel].sum() - df.loc["heat", sector] += s_fec["Low enthalpy heat"] + df.loc["heat", sector] += s_fec["Low-enthalpy heat"] # Industry-specific df.loc["elec", sector] += s_fec["Paper: Stock preparation"] @@ -925,22 +941,22 @@ def pulp_paper_printing(): # add electricity from process that is already electrified df.loc["elec", sector] += s_fec["Paper: Product finishing - Electricity"] - s_fec = idees["fec"][53:64] - s_ued = idees["ued"][53:64] + s_fec = idees["fec"][55:66] + s_ued = idees["ued"][55:66] assert s_fec.index[0] == "Paper: Paper machine - Steam use" assert s_ued.index[0] == "Paper: Paper machine - Steam use" # Efficiency changes due to biomass - eff_bio = s_ued["Biomass"] / s_fec["Biomass"] + eff_bio = s_ued['Biomass and waste'] / s_fec['Biomass and waste'] df.loc["biomass", sector] += s_ued["Paper: Paper machine - Steam use"] / eff_bio - s_fec = idees["fec"][66:77] - s_ued = idees["ued"][66:77] + s_fec = idees["fec"][68:79] + s_ued = idees["ued"][68:79] assert s_fec.index[0] == "Paper: Product finishing - Steam use" assert s_ued.index[0] == "Paper: Product finishing - Steam use" # Efficiency changes due to biomass - eff_bio = s_ued["Biomass"] / s_fec["Biomass"] + eff_bio = s_ued['Biomass and waste'] / s_fec['Biomass and waste'] df.loc["biomass", sector] += s_ued["Paper: Product finishing - Steam use"] / eff_bio s_out = idees["out"][9:10] @@ -959,8 +975,8 @@ def pulp_paper_printing(): df[sector] = 0.0 - s_fec = idees["fec"][79:90] - s_ued = idees["ued"][79:90] + s_fec = idees["fec"][81:93] + s_ued = idees["ued"][81:93] assert s_fec.index[0] == sector assert s_ued.index[0] == sector @@ -968,8 +984,8 @@ def pulp_paper_printing(): df.loc["elec", sector] += s_fec[sel].sum() df.loc["elec", sector] += s_ued[sel].sum() - df.loc["heat", sector] += s_fec["Low enthalpy heat"] - df.loc["heat", sector] += s_ued["Low enthalpy heat"] + df.loc["heat", sector] += s_fec["Low-enthalpy heat"] + df.loc["heat", sector] += s_ued["Low-enthalpy heat"] # Industry-specific df.loc["elec", sector] += s_fec["Printing and publishing"] @@ -986,8 +1002,16 @@ def pulp_paper_printing(): def food_beverages_tobacco(): - # Food, beverages and tobaco can be completely electrified. - # There are no process emissions associated to this sector. + """ + Calculates the energy consumption for the food, beverages, and tobacco sector, + assuming complete electrification of all processes. This sector does not have + any process emissions associated with it. + + + Returns: + pd.DataFrame: A DataFrame containing the energy consumption (in MWh/t material) + for the food, beverages, and tobacco sector. + """ sector = "Food, beverages and tobacco" idees = load_idees_data(sector) @@ -996,15 +1020,15 @@ def food_beverages_tobacco(): df[sector] = 0.0 - s_fec = idees["fec"][3:78] - s_ued = idees["ued"][3:78] + s_fec = idees["fec"][3:79] + s_ued = idees["ued"][3:79] assert s_fec.index[0] == sector assert s_ued.index[0] == sector sel = ["Lighting", "Air compressors", "Motor drives", "Fans and pumps"] df.loc["elec", sector] += s_fec[sel].sum() - df.loc["heat", sector] += s_fec["Low enthalpy heat"] + df.loc["heat", sector] += s_fec["Low-enthalpy heat"] # Efficiency changes due to electrification @@ -1052,7 +1076,7 @@ def non_ferrous_metals(): # Alumina - # High enthalpy heat is converted to methane. + # High-enthalpy heat is converted to methane. # Process heat at T>500C is required here. # Refining is electrified. # There are no process emissions associated to Alumina manufacturing. @@ -1069,24 +1093,24 @@ def non_ferrous_metals(): sel = ["Lighting", "Air compressors", "Motor drives", "Fans and pumps"] df.loc["elec", sector] += s_fec[sel].sum() - df.loc["heat", sector] += s_fec["Low enthalpy heat"] + df.loc["heat", sector] += s_fec["Low-enthalpy heat"] # High-enthalpy heat is transformed into methane - s_fec = idees["fec"][13:24] - s_ued = idees["ued"][13:24] - assert s_fec.index[0] == "Alumina production: High enthalpy heat" - assert s_ued.index[0] == "Alumina production: High enthalpy heat" + s_fec = idees["fec"][14:25] + s_ued = idees["ued"][14:25] + assert s_fec.index[0] == 'Alumina production: High-enthalpy heat' + assert s_ued.index[0] == 'Alumina production: High-enthalpy heat' - eff_met = s_ued["Natural gas (incl. biogas)"] / s_fec["Natural gas (incl. biogas)"] + eff_met = s_ued["Natural gas and biogas"] / s_fec["Natural gas and biogas"] df.loc["methane", sector] += ( - s_fec["Alumina production: High enthalpy heat"] / eff_met + s_fec['Alumina production: High-enthalpy heat'] / eff_met ) # Efficiency changes due to electrification - s_fec = idees["fec"][24:30] - s_ued = idees["ued"][24:30] + s_fec = idees["fec"][25:31] + s_ued = idees["ued"][25:31] assert s_fec.index[0] == "Alumina production: Refining" assert s_ued.index[0] == "Alumina production: Refining" @@ -1111,15 +1135,15 @@ def non_ferrous_metals(): df[sector] = 0.0 - s_fec = idees["fec"][31:66] - s_ued = idees["ued"][31:66] + s_fec = idees["fec"][32:68] + s_ued = idees["ued"][32:68] assert s_fec.index[0] == sector assert s_ued.index[0] == sector sel = ["Lighting", "Air compressors", "Motor drives", "Fans and pumps"] df.loc["elec", sector] += s_fec[sel].sum() - df.loc["heat", sector] += s_fec["Low enthalpy heat"] + df.loc["heat", sector] += s_fec["Low-enthalpy heat"] # Add aluminium electrolysis (smelting df.loc["elec", sector] += s_fec["Aluminium electrolysis (smelting)"] @@ -1135,7 +1159,7 @@ def non_ferrous_metals(): eff_elec = s_ued[key] / s_fec[key] df.loc["elec", sector] += s_ued["Aluminium finishing"] / eff_elec - s_emi = idees["emi"][31:67] + s_emi = idees["emi"][32:69] assert s_emi.index[0] == sector s_out = idees["out"][11:12] @@ -1160,15 +1184,15 @@ def non_ferrous_metals(): df[sector] = 0.0 - s_fec = idees["fec"][68:109] - s_ued = idees["ued"][68:109] + s_fec = idees["fec"][70:112] + s_ued = idees["ued"][70:112] assert s_fec.index[0] == sector assert s_ued.index[0] == sector sel = ["Lighting", "Air compressors", "Motor drives", "Fans and pumps"] df.loc["elec", sector] += s_fec[sel].sum() - df.loc["heat", sector] += s_fec["Low enthalpy heat"] + df.loc["heat", sector] += s_fec["Low-enthalpy heat"] # Efficiency changes due to electrification key = "Secondary aluminium - Electric" @@ -1181,7 +1205,7 @@ def non_ferrous_metals(): key = "Aluminium processing (metallurgy e.g. cast house, reheating)" df.loc["elec", sector] += s_ued[key] / eff_elec - key = "Aluminium finishing - Electric" + key = 'Aluminium finishing - Electric' eff_elec = s_ued[key] / s_fec[key] df.loc["elec", sector] += s_ued["Aluminium finishing"] / eff_elec @@ -1200,15 +1224,15 @@ def non_ferrous_metals(): df[sector] = 0.0 - s_fec = idees["fec"][110:152] - s_ued = idees["ued"][110:152] + s_fec = idees["fec"][113:156] + s_ued = idees["ued"][113:156] assert s_fec.index[0] == sector assert s_ued.index[0] == sector sel = ["Lighting", "Air compressors", "Motor drives", "Fans and pumps"] df.loc["elec", sector] += s_fec[sel].sum() - df.loc["heat", sector] += s_fec["Low enthalpy heat"] + df.loc["heat", sector] += s_fec["Low-enthalpy heat"] # Efficiency changes due to electrification key = "Metal production - Electric" @@ -1224,7 +1248,7 @@ def non_ferrous_metals(): eff_elec = s_ued[key] / s_fec[key] df.loc["elec", sector] += s_ued["Metal finishing"] / eff_elec - s_emi = idees["emi"][110:153] + s_emi = idees["emi"][113:157] assert s_emi.index[0] == sector s_out = idees["out"][13:14] @@ -1247,22 +1271,22 @@ def non_ferrous_metals(): def transport_equipment(): - sector = "Transport Equipment" + sector = "Transport equipment" idees = load_idees_data(sector) df = pd.DataFrame(index=index) df[sector] = 0.0 - s_fec = idees["fec"][3:45] - s_ued = idees["ued"][3:45] + s_fec = idees["fec"][3:46] + s_ued = idees["ued"][3:46] assert s_fec.index[0] == sector assert s_ued.index[0] == sector sel = ["Lighting", "Air compressors", "Motor drives", "Fans and pumps"] df.loc["elec", sector] += s_fec[sel].sum() - df.loc["heat", sector] += s_fec["Low enthalpy heat"] + df.loc["heat", sector] += s_fec["Low-enthalpy heat"] # Efficiency changes due to electrification key = "Trans. Eq.: Electric Foundries" @@ -1281,7 +1305,7 @@ def transport_equipment(): df.loc["elec", sector] += s_fec["Trans. Eq.: Product finishing"] # Steam processing is supplied with biomass - eff_biomass = s_ued["Biomass"] / s_fec["Biomass"] + eff_biomass = s_ued['Biomass and waste'] / s_fec['Biomass and waste'] df.loc["biomass", sector] += s_ued["Trans. Eq.: Steam processing"] / eff_biomass s_out = idees["out"][3:4] @@ -1297,7 +1321,7 @@ def transport_equipment(): def machinery_equipment(): - sector = "Machinery Equipment" + sector = "Machinery equipment" idees = load_idees_data(sector) @@ -1305,15 +1329,15 @@ def machinery_equipment(): df[sector] = 0.0 - s_fec = idees["fec"][3:45] - s_ued = idees["ued"][3:45] + s_fec = idees["fec"][3:46] + s_ued = idees["ued"][3:46] assert s_fec.index[0] == sector assert s_ued.index[0] == sector sel = ["Lighting", "Air compressors", "Motor drives", "Fans and pumps"] df.loc["elec", sector] += s_fec[sel].sum() - df.loc["heat", sector] += s_fec["Low enthalpy heat"] + df.loc["heat", sector] += s_fec["Low-enthalpy heat"] # Efficiency changes due to electrification key = "Mach. Eq.: Electric Foundries" @@ -1333,7 +1357,7 @@ def machinery_equipment(): df.loc["elec", sector] += s_fec["Mach. Eq.: Product finishing"] # Steam processing is supplied with biomass - eff_biomass = s_ued["Biomass"] / s_fec["Biomass"] + eff_biomass = s_ued['Biomass and waste'] / s_fec['Biomass and waste'] df.loc["biomass", sector] += s_ued["Mach. Eq.: Steam processing"] / eff_biomass s_out = idees["out"][3:4] @@ -1357,26 +1381,28 @@ def textiles_and_leather(): df[sector] = 0.0 - s_fec = idees["fec"][3:57] - s_ued = idees["ued"][3:57] + s_fec = idees["fec"][3:58] + s_ued = idees["ued"][3:58] assert s_fec.index[0] == sector assert s_ued.index[0] == sector sel = ["Lighting", "Air compressors", "Motor drives", "Fans and pumps"] df.loc["elec", sector] += s_fec[sel].sum() - df.loc["heat", sector] += s_fec["Low enthalpy heat"] + df.loc["heat", sector] += s_fec["Low-enthalpy heat"] # Efficiency changes due to electrification key = "Textiles: Electric drying" - eff_elec = s_ued[key] / s_fec[key] + # in new JRC data zero assume old data + # eff_elec = s_ued[key] / s_fec[key] + eff_elec = 73.7 / 146.6 df.loc["elec", sector] += s_ued["Textiles: Drying"] / eff_elec df.loc["elec", sector] += s_fec["Textiles: Electric general machinery"] df.loc["elec", sector] += s_fec["Textiles: Finishing Electric"] # Steam processing is supplied with biomass - eff_biomass = s_ued[15:26]["Biomass"] / s_fec[15:26]["Biomass"] + eff_biomass = s_ued[15:26]['Biomass and waste'] / s_fec[15:26]['Biomass and waste'] df.loc["biomass", sector] += ( s_ued["Textiles: Pretreatment with steam"] / eff_biomass ) @@ -1405,15 +1431,15 @@ def wood_and_wood_products(): df[sector] = 0.0 - s_fec = idees["fec"][3:46] - s_ued = idees["ued"][3:46] + s_fec = idees["fec"][3:47] + s_ued = idees["ued"][3:47] assert s_fec.index[0] == sector assert s_ued.index[0] == sector sel = ["Lighting", "Air compressors", "Motor drives", "Fans and pumps"] df.loc["elec", sector] += s_fec[sel].sum() - df.loc["heat", sector] += s_fec["Low enthalpy heat"] + df.loc["heat", sector] += s_fec["Low-enthalpy heat"] # Efficiency changes due to electrification key = "Wood: Electric drying" @@ -1424,7 +1450,7 @@ def wood_and_wood_products(): df.loc["elec", sector] += s_fec["Wood: Finishing Electric"] # Steam processing is supplied with biomass - eff_biomass = s_ued[15:25]["Biomass"] / s_fec[15:25]["Biomass"] + eff_biomass = s_ued[15:25]['Biomass and waste'] / s_fec[15:25]['Biomass and waste'] df.loc["biomass", sector] += ( s_ued["Wood: Specific processes with steam"] / eff_biomass ) @@ -1442,23 +1468,23 @@ def wood_and_wood_products(): def other_industrial_sectors(): - sector = "Other Industrial Sectors" - + + sector = "Other industrial sectors" idees = load_idees_data(sector) df = pd.DataFrame(index=index) df[sector] = 0.0 - s_fec = idees["fec"][3:67] - s_ued = idees["ued"][3:67] + s_fec = idees["fec"][3:68] + s_ued = idees["ued"][3:68] assert s_fec.index[0] == sector assert s_ued.index[0] == sector sel = ["Lighting", "Air compressors", "Motor drives", "Fans and pumps"] df.loc["elec", sector] += s_fec[sel].sum() - df.loc["heat", sector] += s_fec["Low enthalpy heat"] + df.loc["heat", sector] += s_fec["Low-enthalpy heat"] # Efficiency changes due to electrification key = "Other Industrial sectors: Electric processing" @@ -1484,7 +1510,7 @@ def other_industrial_sectors(): df.loc["elec", sector] += s_fec[key] # Steam processing is supplied with biomass - eff_biomass = s_ued[15:25]["Biomass"] / s_fec[15:25]["Biomass"] + eff_biomass = s_ued[15:25]['Biomass and waste'] / s_fec[15:25]['Biomass and waste'] df.loc["biomass", sector] += ( s_ued["Other Industrial sectors: Steam processing"] / eff_biomass ) @@ -1508,10 +1534,10 @@ def other_industrial_sectors(): snakemake = mock_snakemake("build_industry_sector_ratios") set_scenario_config(snakemake) - # TODO make params option - year = 2015 params = snakemake.params.industry + + year = params["reference_year"] df = pd.concat( [ From c52737db7290b60be4f752d64039d931f4284150 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Tue, 30 Jul 2024 15:28:08 +0200 Subject: [PATCH 094/344] small fixes in docstrings --- scripts/build_industrial_energy_demand_per_country_today.py | 4 ++-- scripts/build_industry_sector_ratios_intermediate.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/build_industrial_energy_demand_per_country_today.py b/scripts/build_industrial_energy_demand_per_country_today.py index 983b255e7..8f63795eb 100644 --- a/scripts/build_industrial_energy_demand_per_country_today.py +++ b/scripts/build_industrial_energy_demand_per_country_today.py @@ -34,7 +34,7 @@ - Glass production - HVC - Integrated steelworks -- Machinery Equipment +- Machinery equipment - Methanol - Other industrial sectors - Other chemicals @@ -44,7 +44,7 @@ - Printing and media reproduction - Pulp production - Textiles and leather -- Transport Equipment +- Transport equipment - Wood and wood products the output file contains the energy demand in TWh/a for the following carriers diff --git a/scripts/build_industry_sector_ratios_intermediate.py b/scripts/build_industry_sector_ratios_intermediate.py index 5fe042abe..1f6dfcd6f 100644 --- a/scripts/build_industry_sector_ratios_intermediate.py +++ b/scripts/build_industry_sector_ratios_intermediate.py @@ -56,8 +56,8 @@ - Aluminium - primary production - Aluminium - secondary production - Other non-ferrous metals -- Transport Equipment -- Machinery Equipment +- Transport equipment +- Machinery equipment - Textiles and leather - Wood and wood products - Other Industrial Sectors From 5e73dae3a7b5a5d39c36d47f6f7220b417f2ca46 Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Tue, 30 Jul 2024 18:21:10 +0200 Subject: [PATCH 095/344] add heat_pump_sources to config and docs --- config/config.default.yaml | 6 +++--- doc/configtables/sector.csv | 10 +++++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 794b2baea..9f5954010 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -418,10 +418,10 @@ sector: heat_loss: 0.0 heat_pump_sources: central: - - air + - air decentral: - - air - - ground + - air + - ground cluster_heat_buses: true heat_demand_cutout: default bev_dsm_restriction_value: 0.75 diff --git a/doc/configtables/sector.csv b/doc/configtables/sector.csv index e14da5573..17435a28b 100644 --- a/doc/configtables/sector.csv +++ b/doc/configtables/sector.csv @@ -6,6 +6,9 @@ industry,--,"{true, false}",Flag to include industry sector. agriculture,--,"{true, false}",Flag to include agriculture sector. district_heating,--,,`prepare_sector_network.py `_ -- potential,--,float,maximum fraction of urban demand which can be supplied by district heating. Ignored where below current fraction. +-- progress,--,Dictionary with planning horizons as keys., Increase of today's district heating demand to potential maximum district heating share. Progress = 0 means today's district heating share. Progress = 1 means maximum fraction of urban demand is supplied by district heating +-- district_heating_loss,--,float,Share increase in district heat demand in urban central due to heat losses +cluster_heat_buses,--,"{true, false}",Cluster residential and service heat buses in `prepare_sector_network.py `_ to one to save memory. -- forward_temperature,°C,float,Forward temperature in district heating -- return_temperature,°C,float,Return temperature in district heating. Must be lower than forward temperature -- heat_source_cooling,K,float,Cooling of heat source for heat pumps @@ -14,9 +17,10 @@ district_heating,--,,`prepare_sector_network.py `_ to one to save memory. +-- heat_pump_sources,,, +-- -- central,--,"Any subset of {ground, air}", Heat pump sources for central heating heat pumps +-- -- decentral,--,"Any subset of {ground, air}", Heat pump sources for decentral heating heat pumps + ,,, bev_dsm_restriction _value,--,float,Adds a lower state of charge (SOC) limit for battery electric vehicles (BEV) to manage its own energy demand (DSM). Located in `build_transport_demand.py `_. Set to 0 for no restriction on BEV DSM bev_dsm_restriction _time,--,float,Time at which SOC of BEV has to be dsm_restriction_value From a2c0311ae7c6ece7003a4cdeeeb3566fbfa856dc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 30 Jul 2024 16:23:58 +0000 Subject: [PATCH 096/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- config/config.default.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 9f5954010..794b2baea 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -418,10 +418,10 @@ sector: heat_loss: 0.0 heat_pump_sources: central: - - air + - air decentral: - - air - - ground + - air + - ground cluster_heat_buses: true heat_demand_cutout: default bev_dsm_restriction_value: 0.75 From 4c1ec3559dc91ecb1dea4d32d2e593c530a549d2 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 31 Jul 2024 11:53:20 +0200 Subject: [PATCH 097/344] some small adjustments to run as single node model (#1183) * some small adjustments to run as single node model * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- scripts/cluster_gas_network.py | 3 +++ scripts/prepare_sector_network.py | 12 ++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/scripts/cluster_gas_network.py b/scripts/cluster_gas_network.py index 6d4e6c48a..b95c45807 100755 --- a/scripts/cluster_gas_network.py +++ b/scripts/cluster_gas_network.py @@ -58,6 +58,9 @@ def build_clustered_gas_network(df, bus_regions, length_factor=1.25): # drop pipes within the same region df = df.loc[df.bus1 != df.bus0] + if df.empty: + return df + # recalculate lengths as center to center * length factor df["length"] = df.apply( lambda p: length_factor diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index eaaf207da..5b28c1b48 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2775,10 +2775,11 @@ def add_industry(n, costs): ) domestic_navigation = pop_weighted_energy_totals.loc[ - nodes, "total domestic navigation" + nodes, ["total domestic navigation"] ].squeeze() international_navigation = ( - pd.read_csv(snakemake.input.shipping_demand, index_col=0).squeeze() * nyears + pd.read_csv(snakemake.input.shipping_demand, index_col=0).squeeze(axis=1) + * nyears ) all_navigation = domestic_navigation + international_navigation p_set = all_navigation * 1e6 / nhours @@ -3946,12 +3947,11 @@ def add_enhanced_geothermal(n, egs_potentials, egs_overlap, costs): snakemake = mock_snakemake( "prepare_sector_network", - # configfiles="test/config.overnight.yaml", simpl="", opts="", - clusters="37", - ll="v1.0", - sector_opts="730H-T-H-B-I-A-dist1", + clusters="1", + ll="vopt", + sector_opts="", planning_horizons="2050", ) From d48500fc3b49eb93f3440071b33069c8de334ada Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Wed, 31 Jul 2024 16:06:16 +0200 Subject: [PATCH 098/344] use ffill and bfill --- scripts/build_energy_totals.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index 44bb470fa..f4905927f 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -587,7 +587,8 @@ def build_idees(countries: List[str]) -> pd.DataFrame: def fill_missing_years(fill_values: pd.Series) -> pd.Series: """ - Fill missing years for some countries by mean over the other years. + Fill missing years for some countries by first using forward fill (ffill) + and then backward fill (bfill). Parameters ---------- @@ -598,16 +599,23 @@ def fill_missing_years(fill_values: pd.Series) -> pd.Series: Returns ------- pd.Series - A pandas Series with zero values replaced by the mean value of the corresponding - country. + A pandas Series with zero values replaced by the forward-filled and + backward-filled values of the corresponding country. Notes ----- - - The function groups the data by the 'country' level and computes the mean for each group. - - Zero values in the original Series are replaced by the mean value of their respective country group. + - The function groups the data by the 'country' level and performs forward fill + and backward fill to fill zero values. + - Zero values in the original Series are replaced by the ffilled and bfilled + value of their respective country group. """ - means = fill_values.groupby(level="country").transform("mean") - return fill_values.where(fill_values != 0, means) + # Replace zero values with NaN for correct filling + fill_values = fill_values.replace(0, pd.NA) + + # Forward fill and then backward fill within each country group + fill_values = fill_values.groupby(level="country").ffill().bfill() + + return fill_values def build_energy_totals( @@ -724,6 +732,7 @@ def build_energy_totals( eurostat.loc[slicer, eurostat_fuels[fuel]].groupby(level=[0, 1]).sum() ) # fill missing years for some countries by mean over the other years + breakpoint() fill_values = fill_missing_years(fill_values) df.loc[to_fill, f"{fuel} {sector}"] = fill_values From e37824336edc318e0b4803dd28106c3e13e49804 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 31 Jul 2024 14:10:09 +0000 Subject: [PATCH 099/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/build_energy_totals.py | 14 +-- ...build_industrial_production_per_country.py | 7 +- scripts/build_industry_sector_ratios.py | 96 +++++++++---------- scripts/retrieve_ammonia_demand.py | 2 - 4 files changed, 57 insertions(+), 62 deletions(-) diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index f4905927f..18c136662 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -587,7 +587,7 @@ def build_idees(countries: List[str]) -> pd.DataFrame: def fill_missing_years(fill_values: pd.Series) -> pd.Series: """ - Fill missing years for some countries by first using forward fill (ffill) + Fill missing years for some countries by first using forward fill (ffill) and then backward fill (bfill). Parameters @@ -599,14 +599,14 @@ def fill_missing_years(fill_values: pd.Series) -> pd.Series: Returns ------- pd.Series - A pandas Series with zero values replaced by the forward-filled and + A pandas Series with zero values replaced by the forward-filled and backward-filled values of the corresponding country. Notes ----- - - The function groups the data by the 'country' level and performs forward fill + - The function groups the data by the 'country' level and performs forward fill and backward fill to fill zero values. - - Zero values in the original Series are replaced by the ffilled and bfilled + - Zero values in the original Series are replaced by the ffilled and bfilled value of their respective country group. """ # Replace zero values with NaN for correct filling @@ -913,9 +913,9 @@ def build_district_heat_share(countries: List[str], idees: pd.DataFrame) -> pd.S """ # district heating share - district_heat = idees[["distributed heat residential", "distributed heat services"]].sum( - axis=1 - ) + district_heat = idees[ + ["distributed heat residential", "distributed heat services"] + ].sum(axis=1) total_heat = ( idees[["thermal uses residential", "thermal uses services"]] .sum(axis=1) diff --git a/scripts/build_industrial_production_per_country.py b/scripts/build_industrial_production_per_country.py index 90bf54824..9c5433036 100644 --- a/scripts/build_industrial_production_per_country.py +++ b/scripts/build_industrial_production_per_country.py @@ -167,7 +167,6 @@ } - # TODO: this should go in a csv in `data` # Annual energy consumption in Switzerland by sector in 2015 (in TJ) # From: Energieverbrauch in der Industrie und im Dienstleistungssektor, Der Bundesrat @@ -201,9 +200,9 @@ def get_energy_ratio(country, eurostat_dir, jrc_dir, year): if country == "CH": e_country = e_switzerland * tj_to_ktoe else: - ct_eurostat = country.replace("GB","UK") + ct_eurostat = country.replace("GB", "UK") if ct_eurostat == "UK": - year=2019 + year = 2019 logger.info("Assume Eurostat data for GB from 2019.") # estimate physical output, energy consumption in the sector and country fn = f"{eurostat_dir}/{ct_eurostat}-Energy-balance-sheets-April-2023-edition.xlsb" @@ -302,7 +301,7 @@ def separate_basic_chemicals(demand, year): logger.info(f"Following countries have no ammonia demand: {missing.tolist()}") demand["Ammonia"] = 0.0 - + year_to_use = min(max(year, 2018), 2022) if year_to_use != year: logger.info(f"Using data from {year_to_use} for ammonia production.") diff --git a/scripts/build_industry_sector_ratios.py b/scripts/build_industry_sector_ratios.py index 0f8bec24c..760dca765 100644 --- a/scripts/build_industry_sector_ratios.py +++ b/scripts/build_industry_sector_ratios.py @@ -48,10 +48,9 @@ The unit of the specific energy consumption is MWh/t material and tCO2/t material for process emissions. """ +import country_converter as coco import pandas as pd from _helpers import mute_print, set_scenario_config -import country_converter as coco - cc = coco.CountryConverter() @@ -61,7 +60,6 @@ eu27 = cc.EU27as("ISO2").ISO2.tolist() - sheet_names = { "Iron and steel": "ISI", "Chemicals Industry": "CHI", @@ -122,7 +120,8 @@ def iron_and_steel(): approaches to producing iron and steel. The two primary approaches are integrated steelworks and electric arc furnaces (EAF). The function assumes that integrated steelworks will be replaced entirely by electric arc - furnaces due to their higher efficiency and greater reliance on electricity. + furnaces due to their higher efficiency and greater reliance on + electricity. Returns: pd.DataFrame: A DataFrame containing the energy consumption (in MWh/t material) @@ -237,7 +236,7 @@ def iron_and_steel(): df.loc["heat", sector] += s_fec["Low-enthalpy heat"] - subsector = 'Steel: Sinter/Pellet-making' + subsector = "Steel: Sinter/Pellet-making" s_fec = idees["fec"][14:20] s_ued = idees["ued"][14:20] @@ -251,7 +250,7 @@ def iron_and_steel(): df.loc["coal", sector] += s_fec["Solids"] - subsector = 'Steel: Blast /Basic oxygen furnace' + subsector = "Steel: Blast /Basic oxygen furnace" s_fec = idees["fec"][20:26] s_ued = idees["ued"][20:26] @@ -272,7 +271,7 @@ def iron_and_steel(): assert s_fec.index[0] == subsector assert s_ued.index[0] == subsector - key = 'Steel: Furnaces, refining and rolling - Electric' + key = "Steel: Furnaces, refining and rolling - Electric" eff = s_ued[key] / s_fec[key] # assume fully electrified, other processes scaled by used energy @@ -285,7 +284,7 @@ def iron_and_steel(): assert s_fec.index[0] == subsector assert s_ued.index[0] == subsector - key = 'Steel: Product finishing - Electric' + key = "Steel: Product finishing - Electric" eff = s_ued[key] / s_fec[key] # assume fully electrified @@ -311,10 +310,11 @@ def iron_and_steel(): def chemicals_industry(): """ - This function calculates the energy consumption and emissions for the chemicals industry, - focusing on various subsectors such as basic chemicals, steam processing, furnaces, - and process cooling. The function also accounts for specific processes in ammonia, - chlorine, methanol production, and other chemicals. + This function calculates the energy consumption and emissions for the + chemicals industry, focusing on various subsectors such as basic chemicals, + steam processing, furnaces, and process cooling. The function also accounts + for specific processes in ammonia, chlorine, methanol production, and other + chemicals. Returns: pd.DataFrame: A DataFrame containing the energy consumption (in MWh/t material) @@ -513,14 +513,14 @@ def chemicals_industry(): df.loc["heat", sector] += s_fec["Low-enthalpy heat"] - subsector = 'Chemicals: High-enthalpy heat processing' + subsector = "Chemicals: High-enthalpy heat processing" s_fec = idees["fec"][70:83] s_ued = idees["ued"][70:83] assert s_fec.index[0] == subsector assert s_ued.index[0] == subsector - key = 'High-enthalpy heat processing - Electric (microwave)' + key = "High-enthalpy heat processing - Electric (microwave)" eff_elec = s_ued[key] / s_fec[key] # assume fully electrified @@ -587,14 +587,14 @@ def chemicals_industry(): df.loc["heat", sector] += s_fec["Low-enthalpy heat"] - subsector = 'Chemicals: High-enthalpy heat processing' + subsector = "Chemicals: High-enthalpy heat processing" s_fec = idees["fec"][119:132] s_ued = idees["ued"][119:132] assert s_fec.index[0] == subsector assert s_ued.index[0] == subsector - key = 'High-enthalpy heat processing - Electric (microwave)' + key = "High-enthalpy heat processing - Electric (microwave)" eff_elec = s_ued[key] / s_fec[key] # assume fully electrified @@ -647,9 +647,10 @@ def chemicals_industry(): def nonmetalic_mineral_products(): """ - This function calculates the energy consumption and emissions for the non-metallic mineral - products industry, focusing on three main sectors: cement, ceramics, and glass production. - It takes into account the specific processes and their associated energy types and emissions. + This function calculates the energy consumption and emissions for the non- + metallic mineral products industry, focusing on three main sectors: cement, + ceramics, and glass production. It takes into account the specific + processes and their associated energy types and emissions. Returns: pd.DataFrame: A DataFrame containing the energy consumption (in MWh/t material) @@ -691,9 +692,9 @@ def nonmetalic_mineral_products(): # pre-processing: keep existing elec and biomass, rest to methane df.loc["elec", sector] += s_fec["Cement: Grinding, milling of raw material"] - df.loc["biomass", sector] += s_fec['Biomass and waste'] + df.loc["biomass", sector] += s_fec["Biomass and waste"] df.loc["methane", sector] += ( - s_fec["Cement: Pre-heating and pre-calcination"] - s_fec['Biomass and waste'] + s_fec["Cement: Pre-heating and pre-calcination"] - s_fec["Biomass and waste"] ) subsector = "Cement: Clinker production (kilns)" @@ -703,11 +704,11 @@ def nonmetalic_mineral_products(): assert s_fec.index[0] == subsector assert s_ued.index[0] == subsector - df.loc["biomass", sector] += s_fec['Biomass and waste'] + df.loc["biomass", sector] += s_fec["Biomass and waste"] df.loc["methane", sector] += ( - s_fec["Cement: Clinker production (kilns)"] - s_fec['Biomass and waste'] + s_fec["Cement: Clinker production (kilns)"] - s_fec["Biomass and waste"] ) - df.loc["elec", sector] += s_fec['Cement: Grinding, packaging and precasting'] + df.loc["elec", sector] += s_fec["Cement: Grinding, packaging and precasting"] # Process emissions @@ -757,7 +758,7 @@ def nonmetalic_mineral_products(): key = "Ceramics: Microwave drying and sintering" # the values are zero in new JRC-data -> assume here value from JRC-2015 # eff_elec = s_ued[key] / s_fec[key] - eff_elec = 11.6/26 + eff_elec = 11.6 / 26 sel = [ "Ceramics: Mixing of raw material", @@ -844,13 +845,12 @@ def nonmetalic_mineral_products(): def pulp_paper_printing(): """ Models the energy consumption for the pulp, paper, and printing sector, - assuming complete electrification of all processes. This sector does not have - any process emissions associated with it. - + assuming complete electrification of all processes. This sector does not + have any process emissions associated with it. + Returns: pd.DataFrame: A DataFrame containing the energy consumption (in MWh/t material) for the pulp, paper, and printing sector. - """ sector = "Pulp, paper and printing" @@ -892,7 +892,7 @@ def pulp_paper_printing(): df.loc["elec", sector] += s_fec[sel].sum() # Efficiency changes due to biomass - eff_bio = s_ued['Biomass and waste'] / s_fec['Biomass and waste'] + eff_bio = s_ued["Biomass and waste"] / s_fec["Biomass and waste"] df.loc["biomass", sector] += s_ued["Pulp: Pulping thermal"] / eff_bio s_out = idees["out"][8:9] @@ -947,7 +947,7 @@ def pulp_paper_printing(): assert s_ued.index[0] == "Paper: Paper machine - Steam use" # Efficiency changes due to biomass - eff_bio = s_ued['Biomass and waste'] / s_fec['Biomass and waste'] + eff_bio = s_ued["Biomass and waste"] / s_fec["Biomass and waste"] df.loc["biomass", sector] += s_ued["Paper: Paper machine - Steam use"] / eff_bio s_fec = idees["fec"][68:79] @@ -956,7 +956,7 @@ def pulp_paper_printing(): assert s_ued.index[0] == "Paper: Product finishing - Steam use" # Efficiency changes due to biomass - eff_bio = s_ued['Biomass and waste'] / s_fec['Biomass and waste'] + eff_bio = s_ued["Biomass and waste"] / s_fec["Biomass and waste"] df.loc["biomass", sector] += s_ued["Paper: Product finishing - Steam use"] / eff_bio s_out = idees["out"][9:10] @@ -1003,13 +1003,12 @@ def pulp_paper_printing(): def food_beverages_tobacco(): """ - Calculates the energy consumption for the food, beverages, and tobacco sector, - assuming complete electrification of all processes. This sector does not have - any process emissions associated with it. + Calculates the energy consumption for the food, beverages, and tobacco + sector, assuming complete electrification of all processes. This sector + does not have any process emissions associated with it. - Returns: - pd.DataFrame: A DataFrame containing the energy consumption (in MWh/t material) + pd.DataFrame: A DataFrame containing the energy consumption (in MWh/t material) for the food, beverages, and tobacco sector. """ @@ -1099,12 +1098,12 @@ def non_ferrous_metals(): s_fec = idees["fec"][14:25] s_ued = idees["ued"][14:25] - assert s_fec.index[0] == 'Alumina production: High-enthalpy heat' - assert s_ued.index[0] == 'Alumina production: High-enthalpy heat' + assert s_fec.index[0] == "Alumina production: High-enthalpy heat" + assert s_ued.index[0] == "Alumina production: High-enthalpy heat" eff_met = s_ued["Natural gas and biogas"] / s_fec["Natural gas and biogas"] df.loc["methane", sector] += ( - s_fec['Alumina production: High-enthalpy heat'] / eff_met + s_fec["Alumina production: High-enthalpy heat"] / eff_met ) # Efficiency changes due to electrification @@ -1205,7 +1204,7 @@ def non_ferrous_metals(): key = "Aluminium processing (metallurgy e.g. cast house, reheating)" df.loc["elec", sector] += s_ued[key] / eff_elec - key = 'Aluminium finishing - Electric' + key = "Aluminium finishing - Electric" eff_elec = s_ued[key] / s_fec[key] df.loc["elec", sector] += s_ued["Aluminium finishing"] / eff_elec @@ -1305,7 +1304,7 @@ def transport_equipment(): df.loc["elec", sector] += s_fec["Trans. Eq.: Product finishing"] # Steam processing is supplied with biomass - eff_biomass = s_ued['Biomass and waste'] / s_fec['Biomass and waste'] + eff_biomass = s_ued["Biomass and waste"] / s_fec["Biomass and waste"] df.loc["biomass", sector] += s_ued["Trans. Eq.: Steam processing"] / eff_biomass s_out = idees["out"][3:4] @@ -1357,7 +1356,7 @@ def machinery_equipment(): df.loc["elec", sector] += s_fec["Mach. Eq.: Product finishing"] # Steam processing is supplied with biomass - eff_biomass = s_ued['Biomass and waste'] / s_fec['Biomass and waste'] + eff_biomass = s_ued["Biomass and waste"] / s_fec["Biomass and waste"] df.loc["biomass", sector] += s_ued["Mach. Eq.: Steam processing"] / eff_biomass s_out = idees["out"][3:4] @@ -1402,7 +1401,7 @@ def textiles_and_leather(): df.loc["elec", sector] += s_fec["Textiles: Finishing Electric"] # Steam processing is supplied with biomass - eff_biomass = s_ued[15:26]['Biomass and waste'] / s_fec[15:26]['Biomass and waste'] + eff_biomass = s_ued[15:26]["Biomass and waste"] / s_fec[15:26]["Biomass and waste"] df.loc["biomass", sector] += ( s_ued["Textiles: Pretreatment with steam"] / eff_biomass ) @@ -1450,7 +1449,7 @@ def wood_and_wood_products(): df.loc["elec", sector] += s_fec["Wood: Finishing Electric"] # Steam processing is supplied with biomass - eff_biomass = s_ued[15:25]['Biomass and waste'] / s_fec[15:25]['Biomass and waste'] + eff_biomass = s_ued[15:25]["Biomass and waste"] / s_fec[15:25]["Biomass and waste"] df.loc["biomass", sector] += ( s_ued["Wood: Specific processes with steam"] / eff_biomass ) @@ -1468,7 +1467,7 @@ def wood_and_wood_products(): def other_industrial_sectors(): - + sector = "Other industrial sectors" idees = load_idees_data(sector) @@ -1510,7 +1509,7 @@ def other_industrial_sectors(): df.loc["elec", sector] += s_fec[key] # Steam processing is supplied with biomass - eff_biomass = s_ued[15:25]['Biomass and waste'] / s_fec[15:25]['Biomass and waste'] + eff_biomass = s_ued[15:25]["Biomass and waste"] / s_fec[15:25]["Biomass and waste"] df.loc["biomass", sector] += ( s_ued["Other Industrial sectors: Steam processing"] / eff_biomass ) @@ -1534,9 +1533,8 @@ def other_industrial_sectors(): snakemake = mock_snakemake("build_industry_sector_ratios") set_scenario_config(snakemake) - params = snakemake.params.industry - + year = params["reference_year"] df = pd.concat( diff --git a/scripts/retrieve_ammonia_demand.py b/scripts/retrieve_ammonia_demand.py index cdfa582b6..56e326bb5 100644 --- a/scripts/retrieve_ammonia_demand.py +++ b/scripts/retrieve_ammonia_demand.py @@ -33,10 +33,8 @@ to_fn = snakemake.output[0] - # download .zip file logger.info(f"Downloading Ammonia demand from {url}.") progress_retrieve(url, to_fn, disable=disable_progress) - logger.info(f"Ammonia demand data available in '{to_fn}'.") From 4499fa4e73eed5a148171831cd03a3ace8e048d2 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Wed, 31 Jul 2024 16:14:12 +0200 Subject: [PATCH 100/344] fix typo --- doc/release_notes.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 9c1ba489e..16f48235d 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -10,11 +10,11 @@ Release Notes Upcoming Release ================ -* Upadte JRC-IDEES-2015 to `JRC-IDEES-2021 +* Update JRC-IDEES-2015 to `JRC-IDEES-2021 `__. -* Updata Ammonia production from USGS to 2022 `data +* Update Ammonia production from USGS to 2022 `data `__. From 4362300dced326c92523aeb5581d798c720aeb76 Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Wed, 31 Jul 2024 16:54:46 +0200 Subject: [PATCH 101/344] update configtables and docs --- config/config.default.yaml | 6 +++--- doc/configtables/sector.csv | 10 +++------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 794b2baea..9f5954010 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -418,10 +418,10 @@ sector: heat_loss: 0.0 heat_pump_sources: central: - - air + - air decentral: - - air - - ground + - air + - ground cluster_heat_buses: true heat_demand_cutout: default bev_dsm_restriction_value: 0.75 diff --git a/doc/configtables/sector.csv b/doc/configtables/sector.csv index 17435a28b..e14da5573 100644 --- a/doc/configtables/sector.csv +++ b/doc/configtables/sector.csv @@ -6,9 +6,6 @@ industry,--,"{true, false}",Flag to include industry sector. agriculture,--,"{true, false}",Flag to include agriculture sector. district_heating,--,,`prepare_sector_network.py `_ -- potential,--,float,maximum fraction of urban demand which can be supplied by district heating. Ignored where below current fraction. --- progress,--,Dictionary with planning horizons as keys., Increase of today's district heating demand to potential maximum district heating share. Progress = 0 means today's district heating share. Progress = 1 means maximum fraction of urban demand is supplied by district heating --- district_heating_loss,--,float,Share increase in district heat demand in urban central due to heat losses -cluster_heat_buses,--,"{true, false}",Cluster residential and service heat buses in `prepare_sector_network.py `_ to one to save memory. -- forward_temperature,°C,float,Forward temperature in district heating -- return_temperature,°C,float,Return temperature in district heating. Must be lower than forward temperature -- heat_source_cooling,K,float,Cooling of heat source for heat pumps @@ -17,10 +14,9 @@ cluster_heat_buses,--,"{true, false}",Cluster residential and service heat buses -- -- heat_exchanger_pinch_point_temperature_difference,K,float,Heat pump pinch point temperature difference in heat exchangers assumed for approximation. -- -- isentropic_compressor_efficiency,--,float,Isentropic efficiency of heat pump compressor assumed for approximation. Must be between 0 and 1. -- -- heat_loss,--,float,Heat pump heat loss assumed for approximation. Must be between 0 and 1. --- heat_pump_sources,,, --- -- central,--,"Any subset of {ground, air}", Heat pump sources for central heating heat pumps --- -- decentral,--,"Any subset of {ground, air}", Heat pump sources for decentral heating heat pumps - +-- progress,--,Dictionary with planning horizons as keys., Increase of today's district heating demand to potential maximum district heating share. Progress = 0 means today's district heating share. Progress = 1 means maximum fraction of urban demand is supplied by district heating +-- district_heating_loss,--,float,Share increase in district heat demand in urban central due to heat losses +cluster_heat_buses,--,"{true, false}",Cluster residential and service heat buses in `prepare_sector_network.py `_ to one to save memory. ,,, bev_dsm_restriction _value,--,float,Adds a lower state of charge (SOC) limit for battery electric vehicles (BEV) to manage its own energy demand (DSM). Located in `build_transport_demand.py `_. Set to 0 for no restriction on BEV DSM bev_dsm_restriction _time,--,float,Time at which SOC of BEV has to be dsm_restriction_value From 4cfca27e4f623b4c8bf69793c9ecb4c0a81a02ec Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 31 Jul 2024 14:55:09 +0000 Subject: [PATCH 102/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- config/config.default.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 9f5954010..794b2baea 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -418,10 +418,10 @@ sector: heat_loss: 0.0 heat_pump_sources: central: - - air + - air decentral: - - air - - ground + - air + - ground cluster_heat_buses: true heat_demand_cutout: default bev_dsm_restriction_value: 0.75 From caa260fddb51ef6619c990e379d0ea3de2bd9406 Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Wed, 31 Jul 2024 17:01:27 +0200 Subject: [PATCH 103/344] update class docs --- .../build_cop_profiles/BaseCopApproximator.py | 50 ++++++++++++- .../CentralHeatingCopApproximator.py | 73 ++++++++++++++++++- .../DecentralHeatingCopApproximator.py | 44 ++++++++--- 3 files changed, 153 insertions(+), 14 deletions(-) diff --git a/scripts/build_cop_profiles/BaseCopApproximator.py b/scripts/build_cop_profiles/BaseCopApproximator.py index 891182847..10019b3d3 100644 --- a/scripts/build_cop_profiles/BaseCopApproximator.py +++ b/scripts/build_cop_profiles/BaseCopApproximator.py @@ -14,6 +14,24 @@ class BaseCopApproximator(ABC): """ Abstract class for approximating the coefficient of performance (COP) of a heat pump. + + Attributes: + ---------- + forward_temperature_celsius : Union[xr.DataArray, np.array] + The forward temperature in Celsius. + source_inlet_temperature_celsius : Union[xr.DataArray, np.array] + The source inlet temperature in Celsius. + + Methods: + ------- + __init__(self, forward_temperature_celsius, source_inlet_temperature_celsius) + Initialize CopApproximator. + approximate_cop(self) + Approximate heat pump coefficient of performance (COP). + celsius_to_kelvin(t_celsius) + Convert temperature from Celsius to Kelvin. + logarithmic_mean(t_hot, t_cold) + Calculate the logarithmic mean temperature difference. """ def __init__( @@ -28,8 +46,8 @@ def __init__( ---------- forward_temperature_celsius : Union[xr.DataArray, np.array] The forward temperature in Celsius. - return_temperature_celsius : Union[xr.DataArray, np.array] - The return temperature in Celsius. + source_inlet_temperature_celsius : Union[xr.DataArray, np.array] + The source inlet temperature in Celsius. """ pass @@ -49,6 +67,19 @@ def approximate_cop(self) -> Union[xr.DataArray, np.array]: def celsius_to_kelvin( t_celsius: Union[float, xr.DataArray, np.array] ) -> Union[float, xr.DataArray, np.array]: + """ + Convert temperature from Celsius to Kelvin. + + Parameters: + ---------- + t_celsius : Union[float, xr.DataArray, np.array] + Temperature in Celsius. + + Returns: + ------- + Union[float, xr.DataArray, np.array] + Temperature in Kelvin. + """ if (np.asarray(t_celsius) > 200).any(): raise ValueError( "t_celsius > 200. Are you sure you are using the right units?" @@ -60,6 +91,21 @@ def logarithmic_mean( t_hot: Union[float, xr.DataArray, np.ndarray], t_cold: Union[float, xr.DataArray, np.ndarray], ) -> Union[float, xr.DataArray, np.ndarray]: + """ + Calculate the logarithmic mean temperature difference. + + Parameters: + ---------- + t_hot : Union[float, xr.DataArray, np.ndarray] + Hot temperature. + t_cold : Union[float, xr.DataArray, np.ndarray] + Cold temperature. + + Returns: + ------- + Union[float, xr.DataArray, np.ndarray] + Logarithmic mean temperature difference. + """ if (np.asarray(t_hot <= t_cold)).any(): raise ValueError("t_hot must be greater than t_cold") return (t_hot - t_cold) / np.log(t_hot / t_cold) diff --git a/scripts/build_cop_profiles/CentralHeatingCopApproximator.py b/scripts/build_cop_profiles/CentralHeatingCopApproximator.py index a29dab592..7bf15b300 100644 --- a/scripts/build_cop_profiles/CentralHeatingCopApproximator.py +++ b/scripts/build_cop_profiles/CentralHeatingCopApproximator.py @@ -20,8 +20,77 @@ class CentralHeatingCopApproximator(BaseCopApproximator): default parameters from Pieper et al. (2020). The method is based on a thermodynamic heat pump model with some hard-to-know parameters being approximated. - """ + Attributes: + ---------- + forward_temperature_celsius : Union[xr.DataArray, np.array] + The forward temperature in Celsius. + return_temperature_celsius : Union[xr.DataArray, np.array] + The return temperature in Celsius. + source_inlet_temperature_celsius : Union[xr.DataArray, np.array] + The source inlet temperature in Celsius. + source_outlet_temperature_celsius : Union[xr.DataArray, np.array] + The source outlet temperature in Celsius. + delta_t_pinch_point : float, optional + The pinch point temperature difference, by default 5. + isentropic_compressor_efficiency : float, optional + The isentropic compressor efficiency, by default 0.8. + heat_loss : float, optional + The heat loss, by default 0.0. + + Methods: + ------- + __init__( + forward_temperature_celsius: Union[xr.DataArray, np.array], + source_inlet_temperature_celsius: Union[xr.DataArray, np.array], + return_temperature_celsius: Union[xr.DataArray, np.array], + source_outlet_temperature_celsius: Union[xr.DataArray, np.array], + delta_t_pinch_point: float = 5, + isentropic_compressor_efficiency: float = 0.8, + heat_loss: float = 0.0, + ) -> None: + Initializes the CentralHeatingCopApproximator object. + + approximate_cop(self) -> Union[xr.DataArray, np.array]: + Calculate the coefficient of performance (COP) for the system. + + _approximate_delta_t_refrigerant_source( + self, delta_t_source: Union[xr.DataArray, np.array] + ) -> Union[xr.DataArray, np.array]: + Approximates the temperature difference between the refrigerant and the source. + + _approximate_delta_t_refrigerant_sink( + self, + refrigerant: str = "ammonia", + a: float = {"ammonia": 0.2, "isobutane": -0.0011}, + b: float = {"ammonia": 0.2, "isobutane": 0.3}, + c: float = {"ammonia": 0.016, "isobutane": 2.4}, + ) -> Union[xr.DataArray, np.array]: + Approximates the temperature difference between the refrigerant and heat sink. + + _ratio_evaporation_compression_work_approximation( + self, + refrigerant: str = "ammonia", + a: float = {"ammonia": 0.0014, "isobutane": 0.0035}, + ) -> Union[xr.DataArray, np.array]: + Calculate the ratio of evaporation to compression work based on approximation. + + _approximate_delta_t_refrigerant_sink( + self, + refrigerant: str = "ammonia", + a: float = {"ammonia": 0.2, "isobutane": -0.0011}, + b: float = {"ammonia": 0.2, "isobutane": 0.3}, + c: float = {"ammonia": 0.016, "isobutane": 2.4}, + ) -> Union[xr.DataArray, np.array]: + Approximates the temperature difference between the refrigerant and heat sink. + + _ratio_evaporation_compression_work_approximation( + self, + refrigerant: str = "ammonia", + a: float = {"ammonia": 0.0014, "isobutane": 0.0035}, + ) -> Union[xr.DataArray, np.array]: + Calculate the ratio of evaporation to compression work based on approximation. + """ def __init__( self, forward_temperature_celsius: Union[xr.DataArray, np.array], @@ -33,6 +102,7 @@ def __init__( heat_loss: float = 0.0, ) -> None: """ + Initializes the CentralHeatingCopApproximator object. Parameters: ---------- @@ -74,6 +144,7 @@ def approximate_cop(self) -> Union[xr.DataArray, np.array]: Calculate the coefficient of performance (COP) for the system. Returns: + -------- Union[xr.DataArray, np.array]: The calculated COP values. """ return ( diff --git a/scripts/build_cop_profiles/DecentralHeatingCopApproximator.py b/scripts/build_cop_profiles/DecentralHeatingCopApproximator.py index 11be7407f..d84b67956 100644 --- a/scripts/build_cop_profiles/DecentralHeatingCopApproximator.py +++ b/scripts/build_cop_profiles/DecentralHeatingCopApproximator.py @@ -16,7 +16,27 @@ class DecentralHeatingCopApproximator(BaseCopApproximator): Approximate the coefficient of performance (COP) for a heat pump in a decentral heating system (individual/household heating). - Uses a quadratic regression on the temperature difference between the source and sink based on empirical data proposed by Staffell et al. 2012 . + Uses a quadratic regression on the temperature difference between the source and sink based on empirical data proposed by Staffell et al. 2012. + + Attributes + ---------- + forward_temperature_celsius : Union[xr.DataArray, np.array] + The forward temperature in Celsius. + source_inlet_temperature_celsius : Union[xr.DataArray, np.array] + The source inlet temperature in Celsius. + source_type : str + The source of the heat pump. Must be either 'air' or 'ground'. + + Methods + ------- + __init__(forward_temperature_celsius, source_inlet_temperature_celsius, source_type) + Initialize the DecentralHeatingCopApproximator object. + approximate_cop() + Compute the COP values using quadratic regression for air-/ground-source heat pumps. + _approximate_cop_air_source() + Evaluate quadratic regression for an air-sourced heat pump. + _approximate_cop_ground_source() + Evaluate quadratic regression for a ground-sourced heat pump. References ---------- @@ -30,30 +50,32 @@ def __init__( source_type: str, ): """ - Initialize the COPProfileBuilder object. + Initialize the DecentralHeatingCopApproximator object. - Parameters: + Parameters ---------- forward_temperature_celsius : Union[xr.DataArray, np.array] The forward temperature in Celsius. - return_temperature_celsius : Union[xr.DataArray, np.array] - The return temperature in Celsius. - source: str - The source of the heat pump. Must be either 'air' or 'ground' + source_inlet_temperature_celsius : Union[xr.DataArray, np.array] + The source inlet temperature in Celsius. + source_type : str + The source of the heat pump. Must be either 'air' or 'ground'. """ self.delta_t = forward_temperature_celsius - source_inlet_temperature_celsius if source_type not in ["air", "ground"]: - raise ValueError("'source' must be one of ['air', 'ground']") + raise ValueError("'source_type' must be one of ['air', 'ground']") else: self.source_type = source_type def approximate_cop(self) -> Union[xr.DataArray, np.array]: """ - Compute output of quadratic regression for air-/ground-source heat - pumps. + Compute the COP values using quadratic regression for air-/ground-source heat pumps. - Calls the appropriate method depending on `source`. + Returns + ------- + Union[xr.DataArray, np.array] + The calculated COP values. """ if self.source_type == "air": return self._approximate_cop_air_source() From d273de86001d29bd8996d218eb5cc01f569f1a26 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 31 Jul 2024 15:01:48 +0000 Subject: [PATCH 104/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/build_cop_profiles/CentralHeatingCopApproximator.py | 1 + scripts/build_cop_profiles/DecentralHeatingCopApproximator.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/build_cop_profiles/CentralHeatingCopApproximator.py b/scripts/build_cop_profiles/CentralHeatingCopApproximator.py index 7bf15b300..08dd6a1a8 100644 --- a/scripts/build_cop_profiles/CentralHeatingCopApproximator.py +++ b/scripts/build_cop_profiles/CentralHeatingCopApproximator.py @@ -91,6 +91,7 @@ class CentralHeatingCopApproximator(BaseCopApproximator): ) -> Union[xr.DataArray, np.array]: Calculate the ratio of evaporation to compression work based on approximation. """ + def __init__( self, forward_temperature_celsius: Union[xr.DataArray, np.array], diff --git a/scripts/build_cop_profiles/DecentralHeatingCopApproximator.py b/scripts/build_cop_profiles/DecentralHeatingCopApproximator.py index d84b67956..e5f58af41 100644 --- a/scripts/build_cop_profiles/DecentralHeatingCopApproximator.py +++ b/scripts/build_cop_profiles/DecentralHeatingCopApproximator.py @@ -70,7 +70,8 @@ def __init__( def approximate_cop(self) -> Union[xr.DataArray, np.array]: """ - Compute the COP values using quadratic regression for air-/ground-source heat pumps. + Compute the COP values using quadratic regression for air-/ground- + source heat pumps. Returns ------- From 214efeb0569a9a8c72bff32d8226a79891f1f466 Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Wed, 31 Jul 2024 17:30:24 +0200 Subject: [PATCH 105/344] make test.sh executable --- test.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 test.sh diff --git a/test.sh b/test.sh old mode 100644 new mode 100755 From c9557a5988df701c522e30ced7525028f1ca5f9c Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Wed, 31 Jul 2024 18:50:41 +0200 Subject: [PATCH 106/344] remove breakpoint --- scripts/build_energy_totals.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index 18c136662..2afd7bb99 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -732,7 +732,6 @@ def build_energy_totals( eurostat.loc[slicer, eurostat_fuels[fuel]].groupby(level=[0, 1]).sum() ) # fill missing years for some countries by mean over the other years - breakpoint() fill_values = fill_missing_years(fill_values) df.loc[to_fill, f"{fuel} {sector}"] = fill_values From 5cea20456fa03fe1b355f50c3a4a50de42690867 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Wed, 31 Jul 2024 18:59:57 +0200 Subject: [PATCH 107/344] correct hyperlink in release notes --- doc/release_notes.rst | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 16f48235d..f26ec91c7 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -10,13 +10,9 @@ Release Notes Upcoming Release ================ -* Update JRC-IDEES-2015 to `JRC-IDEES-2021 -`__. - - -* Update Ammonia production from USGS to 2022 `data -`__. +* Update JRC-IDEES-2015 to `JRC-IDEES-2021 `__. +* Update Ammonia production from USGS to 2022 `data `__. * Renamed the carrier of batteries in BEVs from `battery storage` to `EV battery` and the corresponding bus carrier from `Li ion` to `EV battery`. This is to avoid confusion with stationary battery storage. From b0150bd65d4fb7afd107a1edd9bebf5757f0feba Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Thu, 1 Aug 2024 11:59:26 +0200 Subject: [PATCH 108/344] refactor prepare_sector_network.add_heat using Enums --- config/config.default.yaml | 9 +++ rules/build_sector.smk | 1 + scripts/prepare_sector_network.py | 104 ++++++++++++------------------ 3 files changed, 53 insertions(+), 61 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 794b2baea..bfa99d90f 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -422,6 +422,15 @@ sector: decentral: - air - ground + heat_systems: + rural: + - residential rural + - services rural + urban decentral: + - residential urban decentral + - services urban decentral + urban central: + - urban central cluster_heat_buses: true heat_demand_cutout: default bev_dsm_restriction_value: 0.75 diff --git a/rules/build_sector.smk b/rules/build_sector.smk index b7d916ea4..34477f5b4 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -965,6 +965,7 @@ rule prepare_sector_network: emissions_scope=config_provider("energy", "emissions"), RDIR=RDIR, heat_pump_sources=config_provider("sector", "heat_pump_sources"), + heat_systems=config_provider("sector", "heat_systems") input: unpack(input_profile_offwind), **rules.cluster_gas_network.output, diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index f71dd5f58..52989c2d7 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -22,6 +22,7 @@ set_scenario_config, update_config_from_wildcards, ) +from _entities import HeatSystem, HeatSector from add_electricity import calculate_annuity, sanitize_carriers, sanitize_locations from build_energy_totals import ( build_co2_totals, @@ -1817,14 +1818,6 @@ def add_heat(n, costs): for sector in sectors: heat_demand[sector + " space"] = (1 - dE) * heat_demand[sector + " space"] - heat_systems = [ - "residential rural", - "services rural", - "residential urban decentral", - "services urban decentral", - "urban central", - ] - if options["solar_thermal"]: solar_thermal = ( xr.open_dataarray(snakemake.input.solar_thermal_total) @@ -1835,31 +1828,31 @@ def add_heat(n, costs): solar_thermal = options["solar_cf_correction"] * solar_thermal / 1e3 cop = xr.open_dataarray(snakemake.input.cop_profiles) - for heat_system in heat_systems: - system_type = "central" if heat_system == "urban central" else "decentral" + for heat_system in HeatSystem: #this loops through all heat systems defined in _entities.HeatSystem + # system_type = "central" if heat_system == "urban central" else "decentral" - if heat_system == "urban central": + if heat_system == HeatSystem.URBAN_CENTRAL: nodes = dist_fraction.index[dist_fraction > 0] else: nodes = pop_layout.index - n.add("Carrier", heat_system + " heat") + n.add("Carrier", f"{heat_system} heat") n.madd( "Bus", nodes + f" {heat_system} heat", location=nodes, - carrier=heat_system + " heat", + carrier=f"{heat_system} heat", unit="MWh_th", ) - if heat_system == "urban central" and options.get("central_heat_vent"): + if heat_system == HeatSystem.URBAN_CENTRAL and options.get("central_heat_vent"): n.madd( "Generator", nodes + f" {heat_system} heat vent", bus=nodes + f" {heat_system} heat", location=nodes, - carrier=heat_system + " heat vent", + carrier=f"{heat_system} heat vent", p_nom_extendable=True, p_max_pu=0, p_min_pu=-1, @@ -1867,30 +1860,18 @@ def add_heat(n, costs): ) ## Add heat load + factor = heat_system.heat_demand_weighting(urban_fraction=urban_fraction[nodes], dist_fraction=dist_fraction[nodes]) + if not heat_system == HeatSystem.URBAN_CENTRAL: + heat_load = ( + heat_demand[[heat_system.sector.value + " water", heat_system.sector.value + " space"]] + .T.groupby(level=1) + .sum() + .T[nodes] + .multiply(factor) + ) - for sector in sectors: - # heat demand weighting - if "rural" in heat_system: - factor = 1 - urban_fraction[nodes] - elif "urban central" in heat_system: - factor = dist_fraction[nodes] - elif "urban decentral" in heat_system: - factor = urban_fraction[nodes] - dist_fraction[nodes] - else: - raise NotImplementedError( - f" {heat_system} not in " f"heat systems: {heat_systems}" - ) - - if sector in heat_system: - heat_load = ( - heat_demand[[sector + " water", sector + " space"]] - .T.groupby(level=1) - .sum() - .T[nodes] - .multiply(factor) - ) - if heat_system == "urban central": + if heat_system == HeatSystem.URBAN_CENTRAL: heat_load = ( heat_demand.T.groupby(level=1) .sum() @@ -1905,15 +1886,15 @@ def add_heat(n, costs): nodes, suffix=f" {heat_system} heat", bus=nodes + f" {heat_system} heat", - carrier=heat_system + " heat", + carrier=heat_system.value + " heat", p_set=heat_load, ) ## Add heat pumps - for heat_source in snakemake.params.heat_pump_sources[system_type]: - costs_name = f"{system_type} {heat_source}-sourced heat pump" + for heat_source in snakemake.params.heat_pump_sources[heat_system.system_type]: + costs_name = heat_system.heat_pump_costs_name(heat_source) efficiency = ( - cop.sel(heat_system=system_type, heat_source=heat_source, name=nodes) + cop.sel(heat_system=heat_system.system_type, heat_source=heat_source, name=nodes) .to_pandas() .reindex(index=n.snapshots) if options["time_dep_hp_cop"] @@ -1936,13 +1917,13 @@ def add_heat(n, costs): ) if options["tes"]: - n.add("Carrier", heat_system + " water tanks") + n.add("Carrier", heat_system.value + " water tanks") n.madd( "Bus", nodes + f" {heat_system} water tanks", location=nodes, - carrier=heat_system + " water tanks", + carrier=heat_system.value + " water tanks", unit="MWh_th", ) @@ -1952,7 +1933,7 @@ def add_heat(n, costs): bus0=nodes + f" {heat_system} heat", bus1=nodes + f" {heat_system} water tanks", efficiency=costs.at["water tank charger", "efficiency"], - carrier=heat_system + " water tanks charger", + carrier=heat_system.value + " water tanks charger", p_nom_extendable=True, ) @@ -1961,12 +1942,12 @@ def add_heat(n, costs): nodes + f" {heat_system} water tanks discharger", bus0=nodes + f" {heat_system} water tanks", bus1=nodes + f" {heat_system} heat", - carrier=heat_system + " water tanks discharger", + carrier=heat_system.value + " water tanks discharger", efficiency=costs.at["water tank discharger", "efficiency"], p_nom_extendable=True, ) - tes_time_constant_days = options["tes_tau"][system_type] + tes_time_constant_days = options["tes_tau"][heat_system.system_type] n.madd( "Store", @@ -1974,21 +1955,21 @@ def add_heat(n, costs): bus=nodes + f" {heat_system} water tanks", e_cyclic=True, e_nom_extendable=True, - carrier=heat_system + " water tanks", + carrier=heat_system.value + " water tanks", standing_loss=1 - np.exp(-1 / 24 / tes_time_constant_days), - capital_cost=costs.at[system_type + " water tank storage", "fixed"], - lifetime=costs.at[system_type + " water tank storage", "lifetime"], + capital_cost=costs.at[heat_system.system_type + " water tank storage", "fixed"], + lifetime=costs.at[heat_system.system_type + " water tank storage", "lifetime"], ) if options["resistive_heaters"]: - key = f"{system_type} resistive heater" + key = f"{heat_system.system_type} resistive heater" n.madd( "Link", nodes + f" {heat_system} resistive heater", bus0=nodes, bus1=nodes + f" {heat_system} heat", - carrier=heat_system + " resistive heater", + carrier=heat_system.value + " resistive heater", efficiency=costs.at[key, "efficiency"], capital_cost=costs.at[key, "efficiency"] * costs.at[key, "fixed"] @@ -1998,7 +1979,7 @@ def add_heat(n, costs): ) if options["boilers"]: - key = f"{system_type} gas boiler" + key = f"{heat_system.system_type} gas boiler" n.madd( "Link", @@ -2007,7 +1988,7 @@ def add_heat(n, costs): bus0=spatial.gas.df.loc[nodes, "nodes"].values, bus1=nodes + f" {heat_system} heat", bus2="co2 atmosphere", - carrier=heat_system + " gas boiler", + carrier=heat_system.value + " gas boiler", efficiency=costs.at[key, "efficiency"], efficiency2=costs.at["gas", "CO2 intensity"], capital_cost=costs.at[key, "efficiency"] @@ -2017,19 +1998,19 @@ def add_heat(n, costs): ) if options["solar_thermal"]: - n.add("Carrier", heat_system + " solar thermal") + n.add("Carrier", heat_system.value + " solar thermal") n.madd( "Generator", nodes, suffix=f" {heat_system} solar thermal collector", bus=nodes + f" {heat_system} heat", - carrier=heat_system + " solar thermal", + carrier=heat_system.value + " solar thermal", p_nom_extendable=True, - capital_cost=costs.at[system_type + " solar thermal", "fixed"] + capital_cost=costs.at[heat_system.system_type + " solar thermal", "fixed"] * overdim_factor, p_max_pu=solar_thermal[nodes], - lifetime=costs.at[system_type + " solar thermal", "lifetime"], + lifetime=costs.at[heat_system.system_type + " solar thermal", "lifetime"], ) if options["chp"] and heat_system == "urban central": @@ -2125,16 +2106,16 @@ def add_heat(n, costs): # share of space heat demand 'w_space' of total heat demand w_space = {} - for sector in sectors: - w_space[sector] = heat_demand[sector + " space"] / ( - heat_demand[sector + " space"] + heat_demand[sector + " water"] + for sector in HeatSector: + w_space[sector.value] = heat_demand[sector.value + " space"] / ( + heat_demand[sector.value + " space"] + heat_demand[sector.value + " water"] ) w_space["tot"] = ( heat_demand["services space"] + heat_demand["residential space"] ) / heat_demand.T.groupby(level=[1]).sum().T for heat_system in n.loads[ - n.loads.carrier.isin([x + " heat" for x in heat_systems]) + n.loads.carrier.isin([x.value + " heat" for x in HeatSystem]) ].index: node = n.buses.loc[heat_system, "location"] ct = pop_layout.loc[node, "ct"] @@ -2148,6 +2129,7 @@ def add_heat(n, costs): f = 1 - urban_fraction[node] if f == 0: continue + # get sector name ("residential"/"services"/or both "tot" for urban central) if "urban central" in heat_system: sec = "tot" From b2d346f02cdeb3f16f15c258a98cf864c33f7a2a Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Thu, 1 Aug 2024 17:47:21 +0200 Subject: [PATCH 109/344] some simplifications --- scripts/prepare_sector_network.py | 34 +++++++++++-------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 35de1fe72..cdd12866a 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -543,21 +543,16 @@ def add_carrier_buses(n, carrier, nodes=None): ) fossils = ["coal", "gas", "oil", "lignite"] - if not options.get("fossil_fuels", True) and carrier in fossils: - print("Not adding fossil ", carrier) - extendable = False - else: - print("Adding fossil ", carrier) - extendable = True - - n.madd( - "Generator", - nodes, - bus=nodes, - p_nom_extendable=extendable, - carrier=carrier, - marginal_cost=costs.at[carrier, "fuel"], - ) + if options.get("fossil_fuels", True) and carrier in fossils: + + n.madd( + "Generator", + nodes, + bus=nodes, + p_nom_extendable=True, + carrier=carrier, + marginal_cost=costs.at[carrier, "fuel"], + ) # TODO: PyPSA-Eur merge issue @@ -2906,17 +2901,12 @@ def add_industry(n, costs): carrier="oil", ) - if not options.get("fossil_fuels", True): - extendable = False - else: - extendable = True - - if "oil" not in n.generators.carrier.unique(): + if options.get("fossil_fuels", True) and "oil" not in n.generators.carrier.unique(): n.madd( "Generator", spatial.oil.nodes, bus=spatial.oil.nodes, - p_nom_extendable=extendable, + p_nom_extendable=True, carrier="oil", marginal_cost=costs.at["oil", "fuel"], ) From 3eb8b163049b595a91ef5e5036f7f239e68c8145 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Thu, 1 Aug 2024 17:47:41 +0200 Subject: [PATCH 110/344] add description for new config setting --- doc/configtables/sector.csv | 311 ++++++++++++++++++------------------ 1 file changed, 156 insertions(+), 155 deletions(-) diff --git a/doc/configtables/sector.csv b/doc/configtables/sector.csv index 5045cecdc..89c0e55cc 100644 --- a/doc/configtables/sector.csv +++ b/doc/configtables/sector.csv @@ -1,155 +1,156 @@ -,Unit,Values,Description -transport,--,"{true, false}",Flag to include transport sector. -heating,--,"{true, false}",Flag to include heating sector. -biomass,--,"{true, false}",Flag to include biomass sector. -industry,--,"{true, false}",Flag to include industry sector. -agriculture,--,"{true, false}",Flag to include agriculture sector. -district_heating,--,,`prepare_sector_network.py `_ --- potential,--,float,maximum fraction of urban demand which can be supplied by district heating. Ignored where below current fraction. --- progress,--,Dictionary with planning horizons as keys., Increase of today's district heating demand to potential maximum district heating share. Progress = 0 means today's district heating share. Progress = 1 means maximum fraction of urban demand is supplied by district heating --- district_heating_loss,--,float,Share increase in district heat demand in urban central due to heat losses -cluster_heat_buses,--,"{true, false}",Cluster residential and service heat buses in `prepare_sector_network.py `_ to one to save memory. -,,, -bev_dsm_restriction _value,--,float,Adds a lower state of charge (SOC) limit for battery electric vehicles (BEV) to manage its own energy demand (DSM). Located in `build_transport_demand.py `_. Set to 0 for no restriction on BEV DSM -bev_dsm_restriction _time,--,float,Time at which SOC of BEV has to be dsm_restriction_value -transport_heating _deadband_upper,°C,float,"The maximum temperature in the vehicle. At higher temperatures, the energy required for cooling in the vehicle increases." -transport_heating _deadband_lower,°C,float,"The minimum temperature in the vehicle. At lower temperatures, the energy required for heating in the vehicle increases." -,,, -ICE_lower_degree_factor,--,float,Share increase in energy demand in internal combustion engine (ICE) for each degree difference between the cold environment and the minimum temperature. -ICE_upper_degree_factor,--,float,Share increase in energy demand in internal combustion engine (ICE) for each degree difference between the hot environment and the maximum temperature. -EV_lower_degree_factor,--,float,Share increase in energy demand in electric vehicles (EV) for each degree difference between the cold environment and the minimum temperature. -EV_upper_degree_factor,--,float,Share increase in energy demand in electric vehicles (EV) for each degree difference between the hot environment and the maximum temperature. -bev_dsm,--,"{true, false}",Add the option for battery electric vehicles (BEV) to participate in demand-side management (DSM) -,,, -bev_availability,--,float,The share for battery electric vehicles (BEV) that are able to do demand side management (DSM) -bev_energy,--,float,The average size of battery electric vehicles (BEV) in MWh -bev_charge_efficiency,--,float,Battery electric vehicles (BEV) charge and discharge efficiency -bev_charge_rate,MWh,float,The power consumption for one electric vehicle (EV) in MWh. Value derived from 3-phase charger with 11 kW. -bev_avail_max,--,float,The maximum share plugged-in availability for passenger electric vehicles. -bev_avail_mean,--,float,The average share plugged-in availability for passenger electric vehicles. -v2g,--,"{true, false}",Allows feed-in to grid from EV battery -land_transport_fuel_cell _share,--,Dictionary with planning horizons as keys.,The share of vehicles that uses fuel cells in a given year -land_transport_electric _share,--,Dictionary with planning horizons as keys.,The share of vehicles that uses electric vehicles (EV) in a given year -land_transport_ice _share,--,Dictionary with planning horizons as keys.,The share of vehicles that uses internal combustion engines (ICE) in a given year. What is not EV or FCEV is oil-fuelled ICE. -transport_electric_efficiency,MWh/100km,float,The conversion efficiencies of electric vehicles in transport -transport_fuel_cell_efficiency,MWh/100km,float,The H2 conversion efficiencies of fuel cells in transport -transport_ice_efficiency,MWh/100km,float,The oil conversion efficiencies of internal combustion engine (ICE) in transport -agriculture_machinery _electric_share,--,float,The share for agricultural machinery that uses electricity -agriculture_machinery _oil_share,--,float,The share for agricultural machinery that uses oil -agriculture_machinery _fuel_efficiency,--,float,The efficiency of electric-powered machinery in the conversion of electricity to meet agricultural needs. -agriculture_machinery _electric_efficiency,--,float,The efficiency of oil-powered machinery in the conversion of oil to meet agricultural needs. -Mwh_MeOH_per_MWh_H2,LHV,float,"The energy amount of the produced methanol per energy amount of hydrogen. From `DECHEMA (2017) `_, page 64." -MWh_MeOH_per_tCO2,LHV,float,"The energy amount of the produced methanol per ton of CO2. From `DECHEMA (2017) `_, page 66." -MWh_MeOH_per_MWh_e,LHV,float,"The energy amount of the produced methanol per energy amount of electricity. From `DECHEMA (2017) `_, page 64." -shipping_hydrogen _liquefaction,--,"{true, false}",Whether to include liquefaction costs for hydrogen demand in shipping. -,,, -shipping_hydrogen_share,--,Dictionary with planning horizons as keys.,The share of ships powered by hydrogen in a given year -shipping_methanol_share,--,Dictionary with planning horizons as keys.,The share of ships powered by methanol in a given year -shipping_oil_share,--,Dictionary with planning horizons as keys.,The share of ships powered by oil in a given year -shipping_methanol _efficiency,--,float,The efficiency of methanol-powered ships in the conversion of methanol to meet shipping needs (propulsion). The efficiency increase from oil can be 10-15% higher according to the `IEA `_ -,,, -shipping_oil_efficiency,--,float,The efficiency of oil-powered ships in the conversion of oil to meet shipping needs (propulsion). Base value derived from 2011 -aviation_demand_factor,--,float,The proportion of demand for aviation compared to today's consumption -HVC_demand_factor,--,float,The proportion of demand for high-value chemicals compared to today's consumption -,,, -time_dep_hp_cop,--,"{true, false}",Consider the time dependent coefficient of performance (COP) of the heat pump -heat_pump_sink_T,°C,float,The temperature heat sink used in heat pumps based on DTU / large area radiators. The value is conservatively high to cover hot water and space heating in poorly-insulated buildings -reduce_space_heat _exogenously,--,"{true, false}",Influence on space heating demand by a certain factor (applied before losses in district heating). -reduce_space_heat _exogenously_factor,--,Dictionary with planning horizons as keys.,"A positive factor can mean renovation or demolition of a building. If the factor is negative, it can mean an increase in floor area, increased thermal comfort, population growth. The default factors are determined by the `Eurocalc Homes and buildings decarbonization scenario `_" -retrofitting,,, --- retro_endogen,--,"{true, false}",Add retrofitting as an endogenous system which co-optimise space heat savings. --- cost_factor,--,float,Weight costs for building renovation --- interest_rate,--,float,The interest rate for investment in building components --- annualise_cost,--,"{true, false}",Annualise the investment costs of retrofitting --- tax_weighting,--,"{true, false}",Weight the costs of retrofitting depending on taxes in countries --- construction_index,--,"{true, false}",Weight the costs of retrofitting depending on labour/material costs per country -tes,--,"{true, false}",Add option for storing thermal energy in large water pits associated with district heating systems and individual thermal energy storage (TES) -tes_tau,,,The time constant used to calculate the decay of thermal energy in thermal energy storage (TES): 1- :math:`e^{-1/24τ}`. --- decentral,days,float,The time constant in decentralized thermal energy storage (TES) --- central,days,float,The time constant in centralized thermal energy storage (TES) -boilers,--,"{true, false}",Add option for transforming gas into heat using gas boilers -resistive_heaters,--,"{true, false}",Add option for transforming electricity into heat using resistive heaters (independently from gas boilers) -oil_boilers,--,"{true, false}",Add option for transforming oil into heat using boilers -biomass_boiler,--,"{true, false}",Add option for transforming biomass into heat using boilers -overdimension_individual_heating,--,"float",Add option for overdimensioning individual heating systems by a certain factor. This allows them to cover heat demand peaks e.g. 10% higher than those in the data with a setting of 1.1. -chp,--,"{true, false}",Add option for using Combined Heat and Power (CHP) -micro_chp,--,"{true, false}",Add option for using Combined Heat and Power (CHP) for decentral areas. -solar_thermal,--,"{true, false}",Add option for using solar thermal to generate heat. -solar_cf_correction,--,float,The correction factor for the value provided by the solar thermal profile calculations -marginal_cost_storage,currency/MWh ,float,The marginal cost of discharging batteries in distributed grids -methanation,--,"{true, false}",Add option for transforming hydrogen and CO2 into methane using methanation. -coal_cc,--,"{true, false}",Add option for coal CHPs with carbon capture -dac,--,"{true, false}",Add option for Direct Air Capture (DAC) -co2_vent,--,"{true, false}",Add option for vent out CO2 from storages to the atmosphere. -allam_cycle,--,"{true, false}",Add option to include `Allam cycle gas power plants `_ -hydrogen_fuel_cell,--,"{true, false}",Add option to include hydrogen fuel cell for re-electrification. Assuming OCGT technology costs -hydrogen_turbine,--,"{true, false}",Add option to include hydrogen turbine for re-electrification. Assuming OCGT technology costs -SMR,--,"{true, false}",Add option for transforming natural gas into hydrogen and CO2 using Steam Methane Reforming (SMR) -SMR CC,--,"{true, false}",Add option for transforming natural gas into hydrogen and CO2 using Steam Methane Reforming (SMR) and Carbon Capture (CC) -regional_methanol_demand,--,"{true, false}",Spatially resolve methanol demand. Set to true if regional CO2 constraints needed. -regional_oil_demand,--,"{true, false}",Spatially resolve oil demand. Set to true if regional CO2 constraints needed. -regional_co2 _sequestration_potential,,, --- enable,--,"{true, false}",Add option for regionally-resolved geological carbon dioxide sequestration potentials based on `CO2StoP `_. --- attribute,--,string or list,Name (or list of names) of the attribute(s) for the sequestration potential --- include_onshore,--,"{true, false}",Add options for including onshore sequestration potentials --- min_size,Gt ,float,Any sites with lower potential than this value will be excluded --- max_size,Gt ,float,The maximum sequestration potential for any one site. --- years_of_storage,years,float,The years until potential exhausted at optimised annual rate -co2_sequestration_potential,MtCO2/a,float,The potential of sequestering CO2 in Europe per year -co2_sequestration_cost,currency/tCO2,float,The cost of sequestering a ton of CO2 -co2_sequestration_lifetime,years,int,The lifetime of a CO2 sequestration site -co2_spatial,--,"{true, false}","Add option to spatially resolve carrier representing stored carbon dioxide. This allows for more detailed modelling of CCUTS, e.g. regarding the capturing of industrial process emissions, usage as feedstock for electrofuels, transport of carbon dioxide, and geological sequestration sites." -,,, -co2network,--,"{true, false}",Add option for planning a new carbon dioxide transmission network -co2_network_cost_factor,p.u.,float,The cost factor for the capital cost of the carbon dioxide transmission network -,,, -cc_fraction,--,float,The default fraction of CO2 captured with post-combustion capture -hydrogen_underground _storage,--,"{true, false}",Add options for storing hydrogen underground. Storage potential depends regionally. -hydrogen_underground _storage_locations,,"{onshore, nearshore, offshore}","The location where hydrogen underground storage can be located. Onshore, nearshore, offshore means it must be located more than 50 km away from the sea, within 50 km of the sea, or within the sea itself respectively." -,,, -ammonia,--,"{true, false, regional}","Add ammonia as a carrrier. It can be either true (copperplated NH3), false (no NH3 carrier) or ""regional"" (regionalised NH3 without network)" -min_part_load_fischer _tropsch,per unit of p_nom ,float,The minimum unit dispatch (``p_min_pu``) for the Fischer-Tropsch process -min_part_load _methanolisation,per unit of p_nom ,float,The minimum unit dispatch (``p_min_pu``) for the methanolisation process -,,, -use_fischer_tropsch _waste_heat,--,"{true, false}",Add option for using waste heat of Fischer Tropsch in district heating networks -use_fuel_cell_waste_heat,--,"{true, false}",Add option for using waste heat of fuel cells in district heating networks -use_electrolysis_waste _heat,--,"{true, false}",Add option for using waste heat of electrolysis in district heating networks -electricity_transmission _grid,--,"{true, false}",Switch for enabling/disabling the electricity transmission grid. -electricity_distribution _grid,--,"{true, false}",Add a simplified representation of the exchange capacity between transmission and distribution grid level through a link. -electricity_distribution _grid_cost_factor,,,Multiplies the investment cost of the electricity distribution grid -,,, -electricity_grid _connection,--,"{true, false}",Add the cost of electricity grid connection for onshore wind and solar -transmission_efficiency,,,Section to specify transmission losses or compression energy demands of bidirectional links. Splits them into two capacity-linked unidirectional links. --- {carrier},--,str,The carrier of the link. --- -- efficiency_static,p.u.,float,Length-independent transmission efficiency. --- -- efficiency_per_1000km,p.u. per 1000 km,float,Length-dependent transmission efficiency ($\eta^{\text{length}}$) --- -- compression_per_1000km,p.u. per 1000 km,float,Length-dependent electricity demand for compression ($\eta \cdot \text{length}$) implemented as multi-link to local electricity bus. -H2_network,--,"{true, false}",Add option for new hydrogen pipelines -gas_network,--,"{true, false}","Add existing natural gas infrastructure, incl. LNG terminals, production and entry-points. The existing gas network is added with a lossless transport model. A length-weighted `k-edge augmentation algorithm `_ can be run to add new candidate gas pipelines such that all regions of the model can be connected to the gas network. When activated, all the gas demands are regionally disaggregated as well." -H2_retrofit,--,"{true, false}",Add option for retrofiting existing pipelines to transport hydrogen. -H2_retrofit_capacity _per_CH4,--,float,"The ratio for H2 capacity per original CH4 capacity of retrofitted pipelines. The `European Hydrogen Backbone (April, 2020) p.15 `_ 60% of original natural gas capacity could be used in cost-optimal case as H2 capacity." -gas_network_connectivity _upgrade ,--,float,The number of desired edge connectivity (k) in the length-weighted `k-edge augmentation algorithm `_ used for the gas network -gas_distribution_grid,--,"{true, false}",Add a gas distribution grid -gas_distribution_grid _cost_factor,,,Multiplier for the investment cost of the gas distribution grid -,,, -biomass_spatial,--,"{true, false}",Add option for resolving biomass demand regionally -biomass_transport,--,"{true, false}",Add option for transporting solid biomass between nodes -biogas_upgrading_cc,--,"{true, false}",Add option to capture CO2 from biomass upgrading -conventional_generation,,,Add a more detailed description of conventional carriers. Any power generation requires the consumption of fuel from nodes representing that fuel. -biomass_to_liquid,--,"{true, false}",Add option for transforming solid biomass into liquid fuel with the same properties as oil -biosng,--,"{true, false}",Add option for transforming solid biomass into synthesis gas with the same properties as natural gas -limit_max_growth,,, --- enable,--,"{true, false}",Add option to limit the maximum growth of a carrier --- factor,p.u.,float,The maximum growth factor of a carrier (e.g. 1.3 allows 30% larger than max historic growth) --- max_growth,,, --- -- {carrier},GW,float,The historic maximum growth of a carrier --- max_relative_growth,,, --- -- {carrier},p.u.,float,The historic maximum relative growth of a carrier -,,, -enhanced_geothermal,,, --- enable,--,"{true, false}",Add option to include Enhanced Geothermal Systems --- flexible,--,"{true, false}",Add option for flexible operation (see Ricks et al. 2024) --- max_hours,--,int,The maximum hours the reservoir can be charged under flexible operation --- max_boost,--,float,The maximum boost in power output under flexible operation --- var_cf,--,"{true, false}",Add option for variable capacity factor (see Ricks et al. 2024) --- sustainability_factor,--,float,Share of sourced heat that is replenished by the earth's core (see details in `build_egs_potentials.py `_) +,Unit,Values,Description +transport,--,"{true, false}",Flag to include transport sector. +heating,--,"{true, false}",Flag to include heating sector. +biomass,--,"{true, false}",Flag to include biomass sector. +industry,--,"{true, false}",Flag to include industry sector. +agriculture,--,"{true, false}",Flag to include agriculture sector. +fossil_fuels,--,"{true, false}","Flag to include imports of fossil fuels ( [""coal"", ""gas"", ""oil"", ""lignite""])" +district_heating,--,,`prepare_sector_network.py `_ +-- potential,--,float,maximum fraction of urban demand which can be supplied by district heating. Ignored where below current fraction. +-- progress,--,Dictionary with planning horizons as keys., Increase of today's district heating demand to potential maximum district heating share. Progress = 0 means today's district heating share. Progress = 1 means maximum fraction of urban demand is supplied by district heating +-- district_heating_loss,--,float,Share increase in district heat demand in urban central due to heat losses +cluster_heat_buses,--,"{true, false}",Cluster residential and service heat buses in `prepare_sector_network.py `_ to one to save memory. +,,, +bev_dsm_restriction _value,--,float,Adds a lower state of charge (SOC) limit for battery electric vehicles (BEV) to manage its own energy demand (DSM). Located in `build_transport_demand.py `_. Set to 0 for no restriction on BEV DSM +bev_dsm_restriction _time,--,float,Time at which SOC of BEV has to be dsm_restriction_value +transport_heating _deadband_upper,°C,float,"The maximum temperature in the vehicle. At higher temperatures, the energy required for cooling in the vehicle increases." +transport_heating _deadband_lower,°C,float,"The minimum temperature in the vehicle. At lower temperatures, the energy required for heating in the vehicle increases." +,,, +ICE_lower_degree_factor,--,float,Share increase in energy demand in internal combustion engine (ICE) for each degree difference between the cold environment and the minimum temperature. +ICE_upper_degree_factor,--,float,Share increase in energy demand in internal combustion engine (ICE) for each degree difference between the hot environment and the maximum temperature. +EV_lower_degree_factor,--,float,Share increase in energy demand in electric vehicles (EV) for each degree difference between the cold environment and the minimum temperature. +EV_upper_degree_factor,--,float,Share increase in energy demand in electric vehicles (EV) for each degree difference between the hot environment and the maximum temperature. +bev_dsm,--,"{true, false}",Add the option for battery electric vehicles (BEV) to participate in demand-side management (DSM) +,,, +bev_availability,--,float,The share for battery electric vehicles (BEV) that are able to do demand side management (DSM) +bev_energy,--,float,The average size of battery electric vehicles (BEV) in MWh +bev_charge_efficiency,--,float,Battery electric vehicles (BEV) charge and discharge efficiency +bev_charge_rate,MWh,float,The power consumption for one electric vehicle (EV) in MWh. Value derived from 3-phase charger with 11 kW. +bev_avail_max,--,float,The maximum share plugged-in availability for passenger electric vehicles. +bev_avail_mean,--,float,The average share plugged-in availability for passenger electric vehicles. +v2g,--,"{true, false}",Allows feed-in to grid from EV battery +land_transport_fuel_cell _share,--,Dictionary with planning horizons as keys.,The share of vehicles that uses fuel cells in a given year +land_transport_electric _share,--,Dictionary with planning horizons as keys.,The share of vehicles that uses electric vehicles (EV) in a given year +land_transport_ice _share,--,Dictionary with planning horizons as keys.,The share of vehicles that uses internal combustion engines (ICE) in a given year. What is not EV or FCEV is oil-fuelled ICE. +transport_electric_efficiency,MWh/100km,float,The conversion efficiencies of electric vehicles in transport +transport_fuel_cell_efficiency,MWh/100km,float,The H2 conversion efficiencies of fuel cells in transport +transport_ice_efficiency,MWh/100km,float,The oil conversion efficiencies of internal combustion engine (ICE) in transport +agriculture_machinery _electric_share,--,float,The share for agricultural machinery that uses electricity +agriculture_machinery _oil_share,--,float,The share for agricultural machinery that uses oil +agriculture_machinery _fuel_efficiency,--,float,The efficiency of electric-powered machinery in the conversion of electricity to meet agricultural needs. +agriculture_machinery _electric_efficiency,--,float,The efficiency of oil-powered machinery in the conversion of oil to meet agricultural needs. +Mwh_MeOH_per_MWh_H2,LHV,float,"The energy amount of the produced methanol per energy amount of hydrogen. From `DECHEMA (2017) `_, page 64." +MWh_MeOH_per_tCO2,LHV,float,"The energy amount of the produced methanol per ton of CO2. From `DECHEMA (2017) `_, page 66." +MWh_MeOH_per_MWh_e,LHV,float,"The energy amount of the produced methanol per energy amount of electricity. From `DECHEMA (2017) `_, page 64." +shipping_hydrogen _liquefaction,--,"{true, false}",Whether to include liquefaction costs for hydrogen demand in shipping. +,,, +shipping_hydrogen_share,--,Dictionary with planning horizons as keys.,The share of ships powered by hydrogen in a given year +shipping_methanol_share,--,Dictionary with planning horizons as keys.,The share of ships powered by methanol in a given year +shipping_oil_share,--,Dictionary with planning horizons as keys.,The share of ships powered by oil in a given year +shipping_methanol _efficiency,--,float,The efficiency of methanol-powered ships in the conversion of methanol to meet shipping needs (propulsion). The efficiency increase from oil can be 10-15% higher according to the `IEA `_ +,,, +shipping_oil_efficiency,--,float,The efficiency of oil-powered ships in the conversion of oil to meet shipping needs (propulsion). Base value derived from 2011 +aviation_demand_factor,--,float,The proportion of demand for aviation compared to today's consumption +HVC_demand_factor,--,float,The proportion of demand for high-value chemicals compared to today's consumption +,,, +time_dep_hp_cop,--,"{true, false}",Consider the time dependent coefficient of performance (COP) of the heat pump +heat_pump_sink_T,°C,float,The temperature heat sink used in heat pumps based on DTU / large area radiators. The value is conservatively high to cover hot water and space heating in poorly-insulated buildings +reduce_space_heat _exogenously,--,"{true, false}",Influence on space heating demand by a certain factor (applied before losses in district heating). +reduce_space_heat _exogenously_factor,--,Dictionary with planning horizons as keys.,"A positive factor can mean renovation or demolition of a building. If the factor is negative, it can mean an increase in floor area, increased thermal comfort, population growth. The default factors are determined by the `Eurocalc Homes and buildings decarbonization scenario `_" +retrofitting,,, +-- retro_endogen,--,"{true, false}",Add retrofitting as an endogenous system which co-optimise space heat savings. +-- cost_factor,--,float,Weight costs for building renovation +-- interest_rate,--,float,The interest rate for investment in building components +-- annualise_cost,--,"{true, false}",Annualise the investment costs of retrofitting +-- tax_weighting,--,"{true, false}",Weight the costs of retrofitting depending on taxes in countries +-- construction_index,--,"{true, false}",Weight the costs of retrofitting depending on labour/material costs per country +tes,--,"{true, false}",Add option for storing thermal energy in large water pits associated with district heating systems and individual thermal energy storage (TES) +tes_tau,,,The time constant used to calculate the decay of thermal energy in thermal energy storage (TES): 1- :math:`e^{-1/24τ}`. +-- decentral,days,float,The time constant in decentralized thermal energy storage (TES) +-- central,days,float,The time constant in centralized thermal energy storage (TES) +boilers,--,"{true, false}",Add option for transforming gas into heat using gas boilers +resistive_heaters,--,"{true, false}",Add option for transforming electricity into heat using resistive heaters (independently from gas boilers) +oil_boilers,--,"{true, false}",Add option for transforming oil into heat using boilers +biomass_boiler,--,"{true, false}",Add option for transforming biomass into heat using boilers +overdimension_individual_heating,--,float,Add option for overdimensioning individual heating systems by a certain factor. This allows them to cover heat demand peaks e.g. 10% higher than those in the data with a setting of 1.1. +chp,--,"{true, false}",Add option for using Combined Heat and Power (CHP) +micro_chp,--,"{true, false}",Add option for using Combined Heat and Power (CHP) for decentral areas. +solar_thermal,--,"{true, false}",Add option for using solar thermal to generate heat. +solar_cf_correction,--,float,The correction factor for the value provided by the solar thermal profile calculations +marginal_cost_storage,currency/MWh ,float,The marginal cost of discharging batteries in distributed grids +methanation,--,"{true, false}",Add option for transforming hydrogen and CO2 into methane using methanation. +coal_cc,--,"{true, false}",Add option for coal CHPs with carbon capture +dac,--,"{true, false}",Add option for Direct Air Capture (DAC) +co2_vent,--,"{true, false}",Add option for vent out CO2 from storages to the atmosphere. +allam_cycle,--,"{true, false}",Add option to include `Allam cycle gas power plants `_ +hydrogen_fuel_cell,--,"{true, false}",Add option to include hydrogen fuel cell for re-electrification. Assuming OCGT technology costs +hydrogen_turbine,--,"{true, false}",Add option to include hydrogen turbine for re-electrification. Assuming OCGT technology costs +SMR,--,"{true, false}",Add option for transforming natural gas into hydrogen and CO2 using Steam Methane Reforming (SMR) +SMR CC,--,"{true, false}",Add option for transforming natural gas into hydrogen and CO2 using Steam Methane Reforming (SMR) and Carbon Capture (CC) +regional_methanol_demand,--,"{true, false}",Spatially resolve methanol demand. Set to true if regional CO2 constraints needed. +regional_oil_demand,--,"{true, false}",Spatially resolve oil demand. Set to true if regional CO2 constraints needed. +regional_co2 _sequestration_potential,,, +-- enable,--,"{true, false}",Add option for regionally-resolved geological carbon dioxide sequestration potentials based on `CO2StoP `_. +-- attribute,--,string or list,Name (or list of names) of the attribute(s) for the sequestration potential +-- include_onshore,--,"{true, false}",Add options for including onshore sequestration potentials +-- min_size,Gt ,float,Any sites with lower potential than this value will be excluded +-- max_size,Gt ,float,The maximum sequestration potential for any one site. +-- years_of_storage,years,float,The years until potential exhausted at optimised annual rate +co2_sequestration_potential,MtCO2/a,float,The potential of sequestering CO2 in Europe per year +co2_sequestration_cost,currency/tCO2,float,The cost of sequestering a ton of CO2 +co2_sequestration_lifetime,years,int,The lifetime of a CO2 sequestration site +co2_spatial,--,"{true, false}","Add option to spatially resolve carrier representing stored carbon dioxide. This allows for more detailed modelling of CCUTS, e.g. regarding the capturing of industrial process emissions, usage as feedstock for electrofuels, transport of carbon dioxide, and geological sequestration sites." +,,, +co2network,--,"{true, false}",Add option for planning a new carbon dioxide transmission network +co2_network_cost_factor,p.u.,float,The cost factor for the capital cost of the carbon dioxide transmission network +,,, +cc_fraction,--,float,The default fraction of CO2 captured with post-combustion capture +hydrogen_underground _storage,--,"{true, false}",Add options for storing hydrogen underground. Storage potential depends regionally. +hydrogen_underground _storage_locations,,"{onshore, nearshore, offshore}","The location where hydrogen underground storage can be located. Onshore, nearshore, offshore means it must be located more than 50 km away from the sea, within 50 km of the sea, or within the sea itself respectively." +,,, +ammonia,--,"{true, false, regional}","Add ammonia as a carrrier. It can be either true (copperplated NH3), false (no NH3 carrier) or ""regional"" (regionalised NH3 without network)" +min_part_load_fischer _tropsch,per unit of p_nom ,float,The minimum unit dispatch (``p_min_pu``) for the Fischer-Tropsch process +min_part_load _methanolisation,per unit of p_nom ,float,The minimum unit dispatch (``p_min_pu``) for the methanolisation process +,,, +use_fischer_tropsch _waste_heat,--,"{true, false}",Add option for using waste heat of Fischer Tropsch in district heating networks +use_fuel_cell_waste_heat,--,"{true, false}",Add option for using waste heat of fuel cells in district heating networks +use_electrolysis_waste _heat,--,"{true, false}",Add option for using waste heat of electrolysis in district heating networks +electricity_transmission _grid,--,"{true, false}",Switch for enabling/disabling the electricity transmission grid. +electricity_distribution _grid,--,"{true, false}",Add a simplified representation of the exchange capacity between transmission and distribution grid level through a link. +electricity_distribution _grid_cost_factor,,,Multiplies the investment cost of the electricity distribution grid +,,, +electricity_grid _connection,--,"{true, false}",Add the cost of electricity grid connection for onshore wind and solar +transmission_efficiency,,,Section to specify transmission losses or compression energy demands of bidirectional links. Splits them into two capacity-linked unidirectional links. +-- {carrier},--,str,The carrier of the link. +-- -- efficiency_static,p.u.,float,Length-independent transmission efficiency. +-- -- efficiency_per_1000km,p.u. per 1000 km,float,Length-dependent transmission efficiency ($\eta^{\text{length}}$) +-- -- compression_per_1000km,p.u. per 1000 km,float,Length-dependent electricity demand for compression ($\eta \cdot \text{length}$) implemented as multi-link to local electricity bus. +H2_network,--,"{true, false}",Add option for new hydrogen pipelines +gas_network,--,"{true, false}","Add existing natural gas infrastructure, incl. LNG terminals, production and entry-points. The existing gas network is added with a lossless transport model. A length-weighted `k-edge augmentation algorithm `_ can be run to add new candidate gas pipelines such that all regions of the model can be connected to the gas network. When activated, all the gas demands are regionally disaggregated as well." +H2_retrofit,--,"{true, false}",Add option for retrofiting existing pipelines to transport hydrogen. +H2_retrofit_capacity _per_CH4,--,float,"The ratio for H2 capacity per original CH4 capacity of retrofitted pipelines. The `European Hydrogen Backbone (April, 2020) p.15 `_ 60% of original natural gas capacity could be used in cost-optimal case as H2 capacity." +gas_network_connectivity _upgrade ,--,float,The number of desired edge connectivity (k) in the length-weighted `k-edge augmentation algorithm `_ used for the gas network +gas_distribution_grid,--,"{true, false}",Add a gas distribution grid +gas_distribution_grid _cost_factor,,,Multiplier for the investment cost of the gas distribution grid +,,, +biomass_spatial,--,"{true, false}",Add option for resolving biomass demand regionally +biomass_transport,--,"{true, false}",Add option for transporting solid biomass between nodes +biogas_upgrading_cc,--,"{true, false}",Add option to capture CO2 from biomass upgrading +conventional_generation,,,Add a more detailed description of conventional carriers. Any power generation requires the consumption of fuel from nodes representing that fuel. +biomass_to_liquid,--,"{true, false}",Add option for transforming solid biomass into liquid fuel with the same properties as oil +biosng,--,"{true, false}",Add option for transforming solid biomass into synthesis gas with the same properties as natural gas +limit_max_growth,,, +-- enable,--,"{true, false}",Add option to limit the maximum growth of a carrier +-- factor,p.u.,float,The maximum growth factor of a carrier (e.g. 1.3 allows 30% larger than max historic growth) +-- max_growth,,, +-- -- {carrier},GW,float,The historic maximum growth of a carrier +-- max_relative_growth,,, +-- -- {carrier},p.u.,float,The historic maximum relative growth of a carrier +,,, +enhanced_geothermal,,, +-- enable,--,"{true, false}",Add option to include Enhanced Geothermal Systems +-- flexible,--,"{true, false}",Add option for flexible operation (see Ricks et al. 2024) +-- max_hours,--,int,The maximum hours the reservoir can be charged under flexible operation +-- max_boost,--,float,The maximum boost in power output under flexible operation +-- var_cf,--,"{true, false}",Add option for variable capacity factor (see Ricks et al. 2024) +-- sustainability_factor,--,float,Share of sourced heat that is replenished by the earth's core (see details in `build_egs_potentials.py `_) From 9a6505d889fac25483c70ccad3ec24d4eb66c474 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Thu, 1 Aug 2024 17:50:22 +0200 Subject: [PATCH 111/344] add release notes --- doc/release_notes.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 383287a64..1908d7391 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -10,6 +10,8 @@ Release Notes Upcoming Release ================ +* Add flag ``sector: fossil_fuels`` in config to remove the option of importing fossil fuels + * Renamed the carrier of batteries in BEVs from `battery storage` to `EV battery` and the corresponding bus carrier from `Li ion` to `EV battery`. This is to avoid confusion with stationary battery storage. * Changed default assumptions about waste heat usage from PtX and fuel cells in district heating. From de3f679f33a3af4e4d03f6dba851d74beb8e2123 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 1 Aug 2024 15:54:22 +0000 Subject: [PATCH 112/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/prepare_sector_network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index cdd12866a..520a20903 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -544,7 +544,7 @@ def add_carrier_buses(n, carrier, nodes=None): fossils = ["coal", "gas", "oil", "lignite"] if options.get("fossil_fuels", True) and carrier in fossils: - + n.madd( "Generator", nodes, From 311c82d65e5ff3e4562dc49fe744087958592f91 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 2 Aug 2024 09:53:34 +0200 Subject: [PATCH 113/344] naturalearth: automatically download and remove from data bundle --- rules/build_electricity.smk | 2 +- rules/retrieve.smk | 21 ++++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/rules/build_electricity.smk b/rules/build_electricity.smk index 18ff8230b..10e5dfc0c 100644 --- a/rules/build_electricity.smk +++ b/rules/build_electricity.smk @@ -106,7 +106,7 @@ rule build_shapes: params: countries=config_provider("countries"), input: - naturalearth=ancient("data/bundle/naturalearth/ne_10m_admin_0_countries.shp"), + naturalearth=ancient("data/naturalearth/ne_10m_admin_0_countries_deu.shp"), eez=ancient("data/bundle/eez/World_EEZ_v8_2014.shp"), nuts3=ancient("data/bundle/NUTS_2013_60M_SH/data/NUTS_RG_60M_2013.shp"), nuts3pop=ancient("data/bundle/nama_10r_3popgdp.tsv.gz"), diff --git a/rules/retrieve.smk b/rules/retrieve.smk index 18b0ddd22..436d93c45 100644 --- a/rules/retrieve.smk +++ b/rules/retrieve.smk @@ -4,6 +4,7 @@ import requests from datetime import datetime, timedelta +from shutil import move, unpack_archive if config["enable"].get("retrieve", "auto") == "auto": config["enable"]["retrieve"] = has_internet_access() @@ -16,7 +17,6 @@ if config["enable"]["retrieve"] and config["enable"].get("retrieve_databundle", datafiles = [ "je-e-21.03.02.xls", "eez/World_EEZ_v8_2014.shp", - "naturalearth/ne_10m_admin_0_countries.shp", "NUTS_2013_60M_SH/data/NUTS_RG_60M_2013.shp", "nama_10r_3popgdp.tsv.gz", "nama_10r_3gdp.tsv.gz", @@ -211,6 +211,25 @@ if config["enable"]["retrieve"]: move(input[0], output[0]) +if config["enable"]["retrieve"]: + + # Download directly from naciscdn.org which is a redirect from naturalearth.com + # (https://www.naturalearthdata.com/downloads/10m-cultural-vectors/10m-admin-0-countries/) + # Use point-of-view (POV) variant of Germany so that Crimea is included. + rule retrieve_naturalearth_countries: + input: + storage("https://naciscdn.org/naturalearth/10m/cultural/ne_10m_admin_0_countries_deu.zip") + params: + zip="data/naturalearth/ne_10m_admin_0_countries_deu.zip", + output: + countries="data/naturalearth/ne_10m_admin_0_countries_deu.shp" + run: + move(input[0], params["zip"]) + output_folder = Path(output["countries"]).parent + unpack_archive(params["zip"], output_folder) + os.remove(params["zip"]) + + if config["enable"]["retrieve"]: # Some logic to find the correct file URL # Sometimes files are released delayed or ahead of schedule, check which file is currently available From 89906cfdb461e4e0640819566d46e70a7fbd33b8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 2 Aug 2024 07:54:58 +0000 Subject: [PATCH 114/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- rules/retrieve.smk | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rules/retrieve.smk b/rules/retrieve.smk index 436d93c45..524121e23 100644 --- a/rules/retrieve.smk +++ b/rules/retrieve.smk @@ -218,11 +218,13 @@ if config["enable"]["retrieve"]: # Use point-of-view (POV) variant of Germany so that Crimea is included. rule retrieve_naturalearth_countries: input: - storage("https://naciscdn.org/naturalearth/10m/cultural/ne_10m_admin_0_countries_deu.zip") + storage( + "https://naciscdn.org/naturalearth/10m/cultural/ne_10m_admin_0_countries_deu.zip" + ), params: zip="data/naturalearth/ne_10m_admin_0_countries_deu.zip", output: - countries="data/naturalearth/ne_10m_admin_0_countries_deu.shp" + countries="data/naturalearth/ne_10m_admin_0_countries_deu.shp", run: move(input[0], params["zip"]) output_folder = Path(output["countries"]).parent From baa8a827f559b392b57adafbad59fe6cd2c7fb31 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 2 Aug 2024 11:42:30 +0200 Subject: [PATCH 115/344] change FRESNA references to PyPSA --- README.md | 4 ++-- doc/foresight.rst | 2 +- doc/preparation.rst | 2 +- scripts/build_powerplants.py | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index d5a72b775..5c918c9a8 100644 --- a/README.md +++ b/README.md @@ -65,10 +65,10 @@ The dataset consists of: (alternating current lines at and above 220kV voltage level and all high voltage direct current lines) and 3803 substations. - The open power plant database - [powerplantmatching](https://github.com/FRESNA/powerplantmatching). + [powerplantmatching](https://github.com/PyPSA/powerplantmatching). - Electrical demand time series from the [OPSD project](https://open-power-system-data.org/). -- Renewable time series based on ERA5 and SARAH, assembled using the [atlite tool](https://github.com/FRESNA/atlite). +- Renewable time series based on ERA5 and SARAH, assembled using the [atlite tool](https://github.com/PyPSA/atlite). - Geographical potentials for wind and solar generators based on land use (CORINE) and excluding nature reserves (Natura2000) are computed with the [atlite library](https://github.com/PyPSA/atlite). A sector-coupled extension adds demand diff --git a/doc/foresight.rst b/doc/foresight.rst index 400f67cee..43a93ead1 100644 --- a/doc/foresight.rst +++ b/doc/foresight.rst @@ -242,7 +242,7 @@ Rule overview file `__ generated by pypsa-eur which, in turn, is based on the `powerplantmatching - `__ database. + `__ database. Existing wind and solar capacities are retrieved from `IRENA annual statistics `__ and distributed among the diff --git a/doc/preparation.rst b/doc/preparation.rst index 669f33927..4585f4dba 100644 --- a/doc/preparation.rst +++ b/doc/preparation.rst @@ -25,7 +25,7 @@ With these and the externally extracted ENTSO-E online map topology Then the process continues by calculating conventional power plant capacities, potentials, and per-unit availability time series for variable renewable energy carriers and hydro power plants with the following rules: -- :mod:`build_powerplants` for today's thermal power plant capacities using `powerplantmatching `__ allocating these to the closest substation for each powerplant, +- :mod:`build_powerplants` for today's thermal power plant capacities using `powerplantmatching `__ allocating these to the closest substation for each powerplant, - :mod:`build_ship_raster` for building shipping traffic density, - :mod:`build_renewable_profiles` for the hourly capacity factors and installation potentials constrained by land-use in each substation's Voronoi cell for PV, onshore and offshore wind, and - :mod:`build_hydro_profile` for the hourly per-unit hydro power availability time series. diff --git a/scripts/build_powerplants.py b/scripts/build_powerplants.py index 4e2bb88f3..bde2bd38c 100755 --- a/scripts/build_powerplants.py +++ b/scripts/build_powerplants.py @@ -6,7 +6,7 @@ # coding: utf-8 """ Retrieves conventional powerplant capacities and locations from -`powerplantmatching `_, assigns +`powerplantmatching `_, assigns these to buses and creates a ``.csv`` file. It is possible to amend the powerplant database with custom entries provided in ``data/custom_powerplants.csv``. @@ -30,17 +30,17 @@ ------ - ``networks/base.nc``: confer :ref:`base`. -- ``data/custom_powerplants.csv``: custom powerplants in the same format as `powerplantmatching `_ provides +- ``data/custom_powerplants.csv``: custom powerplants in the same format as `powerplantmatching `_ provides Outputs ------- -- ``resource/powerplants.csv``: A list of conventional power plants (i.e. neither wind nor solar) with fields for name, fuel type, technology, country, capacity in MW, duration, commissioning year, retrofit year, latitude, longitude, and dam information as documented in the `powerplantmatching README `_; additionally it includes information on the closest substation/bus in ``networks/base.nc``. +- ``resource/powerplants.csv``: A list of conventional power plants (i.e. neither wind nor solar) with fields for name, fuel type, technology, country, capacity in MW, duration, commissioning year, retrofit year, latitude, longitude, and dam information as documented in the `powerplantmatching README `_; additionally it includes information on the closest substation/bus in ``networks/base.nc``. .. image:: img/powerplantmatching.png :scale: 30 % - **Source:** `powerplantmatching on GitHub `_ + **Source:** `powerplantmatching on GitHub `_ Description ----------- From 8bfca9ce696d930b823e4df784c665b8cab23aff Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Fri, 2 Aug 2024 14:05:42 +0200 Subject: [PATCH 116/344] enable electrobiofuels also without biomass spatially resolved --- scripts/prepare_sector_network.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 7ecdb8363..81acb4180 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -200,7 +200,7 @@ def define_spatial(nodes, options): spatial.geothermal_heat = SimpleNamespace() spatial.geothermal_heat.nodes = ["EU enhanced geothermal systems"] spatial.geothermal_heat.locations = ["EU"] - + return spatial @@ -2514,13 +2514,14 @@ def add_biomass(n, costs): # Electrobiofuels (BtL with hydrogen addition to make more use of biogenic carbon). # Combination of efuels and biomass to liquid, both based on Fischer-Tropsch. # Experimental version - use with caution - if options["electrobiofuels"] and options.get( - "biomass_spatial", options["biomass_transport"] - ): + if options["electrobiofuels"]: + efuel_scale_factor = costs.at["BtL", "C stored"] + name = (pd.Index(spatial.biomass.nodes) + " " + + pd.Index(spatial.h2.nodes.str.replace(" H2", ""))) n.madd( "Link", - spatial.biomass.nodes, + name, suffix=" electrobiofuels", bus0=spatial.biomass.nodes, bus1=spatial.oil.nodes, From c7a186863e6ca2ed336cc0fc19bf194914910a1b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 2 Aug 2024 12:07:16 +0000 Subject: [PATCH 117/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/prepare_sector_network.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 81acb4180..37d1299ce 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -200,7 +200,7 @@ def define_spatial(nodes, options): spatial.geothermal_heat = SimpleNamespace() spatial.geothermal_heat.nodes = ["EU enhanced geothermal systems"] spatial.geothermal_heat.locations = ["EU"] - + return spatial @@ -2517,8 +2517,11 @@ def add_biomass(n, costs): if options["electrobiofuels"]: efuel_scale_factor = costs.at["BtL", "C stored"] - name = (pd.Index(spatial.biomass.nodes) + " " - + pd.Index(spatial.h2.nodes.str.replace(" H2", ""))) + name = ( + pd.Index(spatial.biomass.nodes) + + " " + + pd.Index(spatial.h2.nodes.str.replace(" H2", "")) + ) n.madd( "Link", name, From 9c35a6716cf7763ed3a5f387ed414e39d86c5050 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Fri, 2 Aug 2024 14:11:26 +0200 Subject: [PATCH 118/344] add documentation for new config option --- doc/configtables/sector.csv | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/configtables/sector.csv b/doc/configtables/sector.csv index 89c0e55cc..f6e521163 100644 --- a/doc/configtables/sector.csv +++ b/doc/configtables/sector.csv @@ -138,6 +138,7 @@ biomass_transport,--,"{true, false}",Add option for transporting solid biomass b biogas_upgrading_cc,--,"{true, false}",Add option to capture CO2 from biomass upgrading conventional_generation,,,Add a more detailed description of conventional carriers. Any power generation requires the consumption of fuel from nodes representing that fuel. biomass_to_liquid,--,"{true, false}",Add option for transforming solid biomass into liquid fuel with the same properties as oil +electrobiofuels,--,"{true, false}","Add option for transforming solid biomass and hydrogen into liquid fuel to make more use of biogenic carbon, as a combination of BtL and Fischer-Tropsch" biosng,--,"{true, false}",Add option for transforming solid biomass into synthesis gas with the same properties as natural gas limit_max_growth,,, -- enable,--,"{true, false}",Add option to limit the maximum growth of a carrier From 39c502f29a9b82b073c2b9f5d65bd36837c793cc Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Fri, 2 Aug 2024 14:13:21 +0200 Subject: [PATCH 119/344] add release note --- doc/release_notes.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 1908d7391..4293541be 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -10,6 +10,8 @@ Release Notes Upcoming Release ================ +* Add option to produce electrobiofuels from solid biomass and hydrogen, as a combination of BtL and Fischer-Tropsch to make more use of the biogenic carbon + * Add flag ``sector: fossil_fuels`` in config to remove the option of importing fossil fuels * Renamed the carrier of batteries in BEVs from `battery storage` to `EV battery` and the corresponding bus carrier from `Li ion` to `EV battery`. This is to avoid confusion with stationary battery storage. From eb81cfb7148e7603eda54ebd90017687bd4d2fdb Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Fri, 2 Aug 2024 15:13:47 +0200 Subject: [PATCH 120/344] change units from EJ->TWh and clean up --- config/config.default.yaml | 2 +- scripts/prepare_sector_network.py | 29 +++++++++++------------------ 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index a618ed95b..f549752f9 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -631,7 +631,7 @@ sector: solid_biomass_import: enable: false price: 54 #EUR/MWh - max_amount: 5 #EJ + max_amount: 1390 # TWh upstream_emissions_factor: .1 #share of solid biomass CO2 emissions at full combustion # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#industry diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index d7ca791c8..53af58583 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2287,24 +2287,17 @@ def add_biomass(n, costs): if options["solid_biomass_import"].get("enable", False): biomass_import_price = options["solid_biomass_import"]["price"] - biomass_import_max_amount = round( - options["solid_biomass_import"]["max_amount"] * 1e9 / 3.6, 0 - ) # EJ --> MWh - biomass_import_upstream_emissions = round( - options["solid_biomass_import"]["upstream_emissions_factor"] - * costs.at["solid biomass", "CO2 intensity"], - 4, - ) - - print( - "Adding biomass import with cost", - biomass_import_price, - "EUR/MWh, a limit of", - options["solid_biomass_import"]["max_amount"], - "EJ, and embedded emissions of", - options["solid_biomass_import"]["upstream_emissions_factor"] * 100, - "%", + # convert TWh in MWh + biomass_import_max_amount = options["solid_biomass_import"]["max_amount"] * 1e6 + biomass_import_upstream_emissions = options["solid_biomass_import"]["upstream_emissions_factor"] + + logger.info( + "Adding biomass import with cost %.2f EUR/MWh, a limit of %.2f TWh, and embedded emissions of %.2f%%", + biomass_import_price, + options["solid_biomass_import"]["max_amount"], + biomass_import_upstream_emissions * 100 ) + n.add("Carrier", "solid biomass import") @@ -2334,7 +2327,7 @@ def add_biomass(n, costs): bus2="co2 atmosphere", carrier="solid biomass import", efficiency=1.0, - efficiency2=biomass_import_upstream_emissions, + efficiency2=biomass_import_upstream_emissions* costs.at["solid biomass", "CO2 intensity"], p_nom_extendable=True, ) From 4d9ba02e187aeae5d06db742ebbadce3dbe03987 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Fri, 2 Aug 2024 15:22:39 +0200 Subject: [PATCH 121/344] add doc for new config --- doc/configtables/sector.csv | 315 ++++++++++++++++++------------------ 1 file changed, 160 insertions(+), 155 deletions(-) diff --git a/doc/configtables/sector.csv b/doc/configtables/sector.csv index 059c42339..21d56422f 100644 --- a/doc/configtables/sector.csv +++ b/doc/configtables/sector.csv @@ -1,155 +1,160 @@ -,Unit,Values,Description -transport,--,"{true, false}",Flag to include transport sector. -heating,--,"{true, false}",Flag to include heating sector. -biomass,--,"{true, false}",Flag to include biomass sector. -industry,--,"{true, false}",Flag to include industry sector. -agriculture,--,"{true, false}",Flag to include agriculture sector. -district_heating,--,,`prepare_sector_network.py `_ --- potential,--,float,maximum fraction of urban demand which can be supplied by district heating --- progress,--,Dictionary with planning horizons as keys., Increase of today's district heating demand to potential maximum district heating share. Progress = 0 means today's district heating share. Progress = 1 means maximum fraction of urban demand is supplied by district heating --- district_heating_loss,--,float,Share increase in district heat demand in urban central due to heat losses -cluster_heat_buses,--,"{true, false}",Cluster residential and service heat buses in `prepare_sector_network.py `_ to one to save memory. -,,, -bev_dsm_restriction _value,--,float,Adds a lower state of charge (SOC) limit for battery electric vehicles (BEV) to manage its own energy demand (DSM). Located in `build_transport_demand.py `_. Set to 0 for no restriction on BEV DSM -bev_dsm_restriction _time,--,float,Time at which SOC of BEV has to be dsm_restriction_value -transport_heating _deadband_upper,°C,float,"The maximum temperature in the vehicle. At higher temperatures, the energy required for cooling in the vehicle increases." -transport_heating _deadband_lower,°C,float,"The minimum temperature in the vehicle. At lower temperatures, the energy required for heating in the vehicle increases." -,,, -ICE_lower_degree_factor,--,float,Share increase in energy demand in internal combustion engine (ICE) for each degree difference between the cold environment and the minimum temperature. -ICE_upper_degree_factor,--,float,Share increase in energy demand in internal combustion engine (ICE) for each degree difference between the hot environment and the maximum temperature. -EV_lower_degree_factor,--,float,Share increase in energy demand in electric vehicles (EV) for each degree difference between the cold environment and the minimum temperature. -EV_upper_degree_factor,--,float,Share increase in energy demand in electric vehicles (EV) for each degree difference between the hot environment and the maximum temperature. -bev_dsm,--,"{true, false}",Add the option for battery electric vehicles (BEV) to participate in demand-side management (DSM) -,,, -bev_availability,--,float,The share for battery electric vehicles (BEV) that are able to do demand side management (DSM) -bev_energy,--,float,The average size of battery electric vehicles (BEV) in MWh -bev_charge_efficiency,--,float,Battery electric vehicles (BEV) charge and discharge efficiency -bev_charge_rate,MWh,float,The power consumption for one electric vehicle (EV) in MWh. Value derived from 3-phase charger with 11 kW. -bev_avail_max,--,float,The maximum share plugged-in availability for passenger electric vehicles. -bev_avail_mean,--,float,The average share plugged-in availability for passenger electric vehicles. -v2g,--,"{true, false}",Allows feed-in to grid from EV battery -land_transport_fuel_cell _share,--,Dictionary with planning horizons as keys.,The share of vehicles that uses fuel cells in a given year -land_transport_electric _share,--,Dictionary with planning horizons as keys.,The share of vehicles that uses electric vehicles (EV) in a given year -land_transport_ice _share,--,Dictionary with planning horizons as keys.,The share of vehicles that uses internal combustion engines (ICE) in a given year. What is not EV or FCEV is oil-fuelled ICE. -transport_electric_efficiency,MWh/100km,float,The conversion efficiencies of electric vehicles in transport -transport_fuel_cell_efficiency,MWh/100km,float,The H2 conversion efficiencies of fuel cells in transport -transport_ice_efficiency,MWh/100km,float,The oil conversion efficiencies of internal combustion engine (ICE) in transport -agriculture_machinery _electric_share,--,float,The share for agricultural machinery that uses electricity -agriculture_machinery _oil_share,--,float,The share for agricultural machinery that uses oil -agriculture_machinery _fuel_efficiency,--,float,The efficiency of electric-powered machinery in the conversion of electricity to meet agricultural needs. -agriculture_machinery _electric_efficiency,--,float,The efficiency of oil-powered machinery in the conversion of oil to meet agricultural needs. -Mwh_MeOH_per_MWh_H2,LHV,float,"The energy amount of the produced methanol per energy amount of hydrogen. From `DECHEMA (2017) `_, page 64." -MWh_MeOH_per_tCO2,LHV,float,"The energy amount of the produced methanol per ton of CO2. From `DECHEMA (2017) `_, page 66." -MWh_MeOH_per_MWh_e,LHV,float,"The energy amount of the produced methanol per energy amount of electricity. From `DECHEMA (2017) `_, page 64." -shipping_hydrogen _liquefaction,--,"{true, false}",Whether to include liquefaction costs for hydrogen demand in shipping. -,,, -shipping_hydrogen_share,--,Dictionary with planning horizons as keys.,The share of ships powered by hydrogen in a given year -shipping_methanol_share,--,Dictionary with planning horizons as keys.,The share of ships powered by methanol in a given year -shipping_oil_share,--,Dictionary with planning horizons as keys.,The share of ships powered by oil in a given year -shipping_methanol _efficiency,--,float,The efficiency of methanol-powered ships in the conversion of methanol to meet shipping needs (propulsion). The efficiency increase from oil can be 10-15% higher according to the `IEA `_ -,,, -shipping_oil_efficiency,--,float,The efficiency of oil-powered ships in the conversion of oil to meet shipping needs (propulsion). Base value derived from 2011 -aviation_demand_factor,--,float,The proportion of demand for aviation compared to today's consumption -HVC_demand_factor,--,float,The proportion of demand for high-value chemicals compared to today's consumption -,,, -time_dep_hp_cop,--,"{true, false}",Consider the time dependent coefficient of performance (COP) of the heat pump -heat_pump_sink_T,°C,float,The temperature heat sink used in heat pumps based on DTU / large area radiators. The value is conservatively high to cover hot water and space heating in poorly-insulated buildings -reduce_space_heat _exogenously,--,"{true, false}",Influence on space heating demand by a certain factor (applied before losses in district heating). -reduce_space_heat _exogenously_factor,--,Dictionary with planning horizons as keys.,"A positive factor can mean renovation or demolition of a building. If the factor is negative, it can mean an increase in floor area, increased thermal comfort, population growth. The default factors are determined by the `Eurocalc Homes and buildings decarbonization scenario `_" -retrofitting,,, --- retro_endogen,--,"{true, false}",Add retrofitting as an endogenous system which co-optimise space heat savings. --- cost_factor,--,float,Weight costs for building renovation --- interest_rate,--,float,The interest rate for investment in building components --- annualise_cost,--,"{true, false}",Annualise the investment costs of retrofitting --- tax_weighting,--,"{true, false}",Weight the costs of retrofitting depending on taxes in countries --- construction_index,--,"{true, false}",Weight the costs of retrofitting depending on labour/material costs per country -tes,--,"{true, false}",Add option for storing thermal energy in large water pits associated with district heating systems and individual thermal energy storage (TES) -tes_tau,,,The time constant used to calculate the decay of thermal energy in thermal energy storage (TES): 1- :math:`e^{-1/24τ}`. --- decentral,days,float,The time constant in decentralized thermal energy storage (TES) --- central,days,float,The time constant in centralized thermal energy storage (TES) -boilers,--,"{true, false}",Add option for transforming gas into heat using gas boilers -resistive_heaters,--,"{true, false}",Add option for transforming electricity into heat using resistive heaters (independently from gas boilers) -oil_boilers,--,"{true, false}",Add option for transforming oil into heat using boilers -biomass_boiler,--,"{true, false}",Add option for transforming biomass into heat using boilers -overdimension_individual_heating,--,"float",Add option for overdimensioning individual heating systems by a certain factor. This allows them to cover heat demand peaks e.g. 10% higher than those in the data with a setting of 1.1. -chp,--,"{true, false}",Add option for using Combined Heat and Power (CHP) -micro_chp,--,"{true, false}",Add option for using Combined Heat and Power (CHP) for decentral areas. -solar_thermal,--,"{true, false}",Add option for using solar thermal to generate heat. -solar_cf_correction,--,float,The correction factor for the value provided by the solar thermal profile calculations -marginal_cost_storage,currency/MWh ,float,The marginal cost of discharging batteries in distributed grids -methanation,--,"{true, false}",Add option for transforming hydrogen and CO2 into methane using methanation. -coal_cc,--,"{true, false}",Add option for coal CHPs with carbon capture -dac,--,"{true, false}",Add option for Direct Air Capture (DAC) -co2_vent,--,"{true, false}",Add option for vent out CO2 from storages to the atmosphere. -allam_cycle,--,"{true, false}",Add option to include `Allam cycle gas power plants `_ -hydrogen_fuel_cell,--,"{true, false}",Add option to include hydrogen fuel cell for re-electrification. Assuming OCGT technology costs -hydrogen_turbine,--,"{true, false}",Add option to include hydrogen turbine for re-electrification. Assuming OCGT technology costs -SMR,--,"{true, false}",Add option for transforming natural gas into hydrogen and CO2 using Steam Methane Reforming (SMR) -SMR CC,--,"{true, false}",Add option for transforming natural gas into hydrogen and CO2 using Steam Methane Reforming (SMR) and Carbon Capture (CC) -regional_methanol_demand,--,"{true, false}",Spatially resolve methanol demand. Set to true if regional CO2 constraints needed. -regional_oil_demand,--,"{true, false}",Spatially resolve oil demand. Set to true if regional CO2 constraints needed. -regional_co2 _sequestration_potential,,, --- enable,--,"{true, false}",Add option for regionally-resolved geological carbon dioxide sequestration potentials based on `CO2StoP `_. --- attribute,--,string or list,Name (or list of names) of the attribute(s) for the sequestration potential --- include_onshore,--,"{true, false}",Add options for including onshore sequestration potentials --- min_size,Gt ,float,Any sites with lower potential than this value will be excluded --- max_size,Gt ,float,The maximum sequestration potential for any one site. --- years_of_storage,years,float,The years until potential exhausted at optimised annual rate -co2_sequestration_potential,MtCO2/a,float,The potential of sequestering CO2 in Europe per year -co2_sequestration_cost,currency/tCO2,float,The cost of sequestering a ton of CO2 -co2_sequestration_lifetime,years,int,The lifetime of a CO2 sequestration site -co2_spatial,--,"{true, false}","Add option to spatially resolve carrier representing stored carbon dioxide. This allows for more detailed modelling of CCUTS, e.g. regarding the capturing of industrial process emissions, usage as feedstock for electrofuels, transport of carbon dioxide, and geological sequestration sites." -,,, -co2network,--,"{true, false}",Add option for planning a new carbon dioxide transmission network -co2_network_cost_factor,p.u.,float,The cost factor for the capital cost of the carbon dioxide transmission network -,,, -cc_fraction,--,float,The default fraction of CO2 captured with post-combustion capture -hydrogen_underground _storage,--,"{true, false}",Add options for storing hydrogen underground. Storage potential depends regionally. -hydrogen_underground _storage_locations,,"{onshore, nearshore, offshore}","The location where hydrogen underground storage can be located. Onshore, nearshore, offshore means it must be located more than 50 km away from the sea, within 50 km of the sea, or within the sea itself respectively." -,,, -ammonia,--,"{true, false, regional}","Add ammonia as a carrrier. It can be either true (copperplated NH3), false (no NH3 carrier) or ""regional"" (regionalised NH3 without network)" -min_part_load_fischer _tropsch,per unit of p_nom ,float,The minimum unit dispatch (``p_min_pu``) for the Fischer-Tropsch process -min_part_load _methanolisation,per unit of p_nom ,float,The minimum unit dispatch (``p_min_pu``) for the methanolisation process -,,, -use_fischer_tropsch _waste_heat,--,"{true, false}",Add option for using waste heat of Fischer Tropsch in district heating networks -use_fuel_cell_waste_heat,--,"{true, false}",Add option for using waste heat of fuel cells in district heating networks -use_electrolysis_waste _heat,--,"{true, false}",Add option for using waste heat of electrolysis in district heating networks -electricity_transmission _grid,--,"{true, false}",Switch for enabling/disabling the electricity transmission grid. -electricity_distribution _grid,--,"{true, false}",Add a simplified representation of the exchange capacity between transmission and distribution grid level through a link. -electricity_distribution _grid_cost_factor,,,Multiplies the investment cost of the electricity distribution grid -,,, -electricity_grid _connection,--,"{true, false}",Add the cost of electricity grid connection for onshore wind and solar -transmission_efficiency,,,Section to specify transmission losses or compression energy demands of bidirectional links. Splits them into two capacity-linked unidirectional links. --- {carrier},--,str,The carrier of the link. --- -- efficiency_static,p.u.,float,Length-independent transmission efficiency. --- -- efficiency_per_1000km,p.u. per 1000 km,float,Length-dependent transmission efficiency ($\eta^{\text{length}}$) --- -- compression_per_1000km,p.u. per 1000 km,float,Length-dependent electricity demand for compression ($\eta \cdot \text{length}$) implemented as multi-link to local electricity bus. -H2_network,--,"{true, false}",Add option for new hydrogen pipelines -gas_network,--,"{true, false}","Add existing natural gas infrastructure, incl. LNG terminals, production and entry-points. The existing gas network is added with a lossless transport model. A length-weighted `k-edge augmentation algorithm `_ can be run to add new candidate gas pipelines such that all regions of the model can be connected to the gas network. When activated, all the gas demands are regionally disaggregated as well." -H2_retrofit,--,"{true, false}",Add option for retrofiting existing pipelines to transport hydrogen. -H2_retrofit_capacity _per_CH4,--,float,"The ratio for H2 capacity per original CH4 capacity of retrofitted pipelines. The `European Hydrogen Backbone (April, 2020) p.15 `_ 60% of original natural gas capacity could be used in cost-optimal case as H2 capacity." -gas_network_connectivity _upgrade ,--,float,The number of desired edge connectivity (k) in the length-weighted `k-edge augmentation algorithm `_ used for the gas network -gas_distribution_grid,--,"{true, false}",Add a gas distribution grid -gas_distribution_grid _cost_factor,,,Multiplier for the investment cost of the gas distribution grid -,,, -biomass_spatial,--,"{true, false}",Add option for resolving biomass demand regionally -biomass_transport,--,"{true, false}",Add option for transporting solid biomass between nodes -biogas_upgrading_cc,--,"{true, false}",Add option to capture CO2 from biomass upgrading -conventional_generation,,,Add a more detailed description of conventional carriers. Any power generation requires the consumption of fuel from nodes representing that fuel. -biomass_to_liquid,--,"{true, false}",Add option for transforming solid biomass into liquid fuel with the same properties as oil -biosng,--,"{true, false}",Add option for transforming solid biomass into synthesis gas with the same properties as natural gas -limit_max_growth,,, --- enable,--,"{true, false}",Add option to limit the maximum growth of a carrier --- factor,p.u.,float,The maximum growth factor of a carrier (e.g. 1.3 allows 30% larger than max historic growth) --- max_growth,,, --- -- {carrier},GW,float,The historic maximum growth of a carrier --- max_relative_growth,,, --- -- {carrier},p.u.,float,The historic maximum relative growth of a carrier -,,, -enhanced_geothermal,,, --- enable,--,"{true, false}",Add option to include Enhanced Geothermal Systems --- flexible,--,"{true, false}",Add option for flexible operation (see Ricks et al. 2024) --- max_hours,--,int,The maximum hours the reservoir can be charged under flexible operation --- max_boost,--,float,The maximum boost in power output under flexible operation --- var_cf,--,"{true, false}",Add option for variable capacity factor (see Ricks et al. 2024) --- sustainability_factor,--,float,Share of sourced heat that is replenished by the earth's core (see details in `build_egs_potentials.py `_) +,Unit,Values,Description +transport,--,"{true, false}",Flag to include transport sector. +heating,--,"{true, false}",Flag to include heating sector. +biomass,--,"{true, false}",Flag to include biomass sector. +industry,--,"{true, false}",Flag to include industry sector. +agriculture,--,"{true, false}",Flag to include agriculture sector. +district_heating,--,,`prepare_sector_network.py `_ +-- potential,--,float,maximum fraction of urban demand which can be supplied by district heating +-- progress,--,Dictionary with planning horizons as keys., Increase of today's district heating demand to potential maximum district heating share. Progress = 0 means today's district heating share. Progress = 1 means maximum fraction of urban demand is supplied by district heating +-- district_heating_loss,--,float,Share increase in district heat demand in urban central due to heat losses +cluster_heat_buses,--,"{true, false}",Cluster residential and service heat buses in `prepare_sector_network.py `_ to one to save memory. +,,, +bev_dsm_restriction _value,--,float,Adds a lower state of charge (SOC) limit for battery electric vehicles (BEV) to manage its own energy demand (DSM). Located in `build_transport_demand.py `_. Set to 0 for no restriction on BEV DSM +bev_dsm_restriction _time,--,float,Time at which SOC of BEV has to be dsm_restriction_value +transport_heating _deadband_upper,°C,float,"The maximum temperature in the vehicle. At higher temperatures, the energy required for cooling in the vehicle increases." +transport_heating _deadband_lower,°C,float,"The minimum temperature in the vehicle. At lower temperatures, the energy required for heating in the vehicle increases." +,,, +ICE_lower_degree_factor,--,float,Share increase in energy demand in internal combustion engine (ICE) for each degree difference between the cold environment and the minimum temperature. +ICE_upper_degree_factor,--,float,Share increase in energy demand in internal combustion engine (ICE) for each degree difference between the hot environment and the maximum temperature. +EV_lower_degree_factor,--,float,Share increase in energy demand in electric vehicles (EV) for each degree difference between the cold environment and the minimum temperature. +EV_upper_degree_factor,--,float,Share increase in energy demand in electric vehicles (EV) for each degree difference between the hot environment and the maximum temperature. +bev_dsm,--,"{true, false}",Add the option for battery electric vehicles (BEV) to participate in demand-side management (DSM) +,,, +bev_availability,--,float,The share for battery electric vehicles (BEV) that are able to do demand side management (DSM) +bev_energy,--,float,The average size of battery electric vehicles (BEV) in MWh +bev_charge_efficiency,--,float,Battery electric vehicles (BEV) charge and discharge efficiency +bev_charge_rate,MWh,float,The power consumption for one electric vehicle (EV) in MWh. Value derived from 3-phase charger with 11 kW. +bev_avail_max,--,float,The maximum share plugged-in availability for passenger electric vehicles. +bev_avail_mean,--,float,The average share plugged-in availability for passenger electric vehicles. +v2g,--,"{true, false}",Allows feed-in to grid from EV battery +land_transport_fuel_cell _share,--,Dictionary with planning horizons as keys.,The share of vehicles that uses fuel cells in a given year +land_transport_electric _share,--,Dictionary with planning horizons as keys.,The share of vehicles that uses electric vehicles (EV) in a given year +land_transport_ice _share,--,Dictionary with planning horizons as keys.,The share of vehicles that uses internal combustion engines (ICE) in a given year. What is not EV or FCEV is oil-fuelled ICE. +transport_electric_efficiency,MWh/100km,float,The conversion efficiencies of electric vehicles in transport +transport_fuel_cell_efficiency,MWh/100km,float,The H2 conversion efficiencies of fuel cells in transport +transport_ice_efficiency,MWh/100km,float,The oil conversion efficiencies of internal combustion engine (ICE) in transport +agriculture_machinery _electric_share,--,float,The share for agricultural machinery that uses electricity +agriculture_machinery _oil_share,--,float,The share for agricultural machinery that uses oil +agriculture_machinery _fuel_efficiency,--,float,The efficiency of electric-powered machinery in the conversion of electricity to meet agricultural needs. +agriculture_machinery _electric_efficiency,--,float,The efficiency of oil-powered machinery in the conversion of oil to meet agricultural needs. +Mwh_MeOH_per_MWh_H2,LHV,float,"The energy amount of the produced methanol per energy amount of hydrogen. From `DECHEMA (2017) `_, page 64." +MWh_MeOH_per_tCO2,LHV,float,"The energy amount of the produced methanol per ton of CO2. From `DECHEMA (2017) `_, page 66." +MWh_MeOH_per_MWh_e,LHV,float,"The energy amount of the produced methanol per energy amount of electricity. From `DECHEMA (2017) `_, page 64." +shipping_hydrogen _liquefaction,--,"{true, false}",Whether to include liquefaction costs for hydrogen demand in shipping. +,,, +shipping_hydrogen_share,--,Dictionary with planning horizons as keys.,The share of ships powered by hydrogen in a given year +shipping_methanol_share,--,Dictionary with planning horizons as keys.,The share of ships powered by methanol in a given year +shipping_oil_share,--,Dictionary with planning horizons as keys.,The share of ships powered by oil in a given year +shipping_methanol _efficiency,--,float,The efficiency of methanol-powered ships in the conversion of methanol to meet shipping needs (propulsion). The efficiency increase from oil can be 10-15% higher according to the `IEA `_ +,,, +shipping_oil_efficiency,--,float,The efficiency of oil-powered ships in the conversion of oil to meet shipping needs (propulsion). Base value derived from 2011 +aviation_demand_factor,--,float,The proportion of demand for aviation compared to today's consumption +HVC_demand_factor,--,float,The proportion of demand for high-value chemicals compared to today's consumption +,,, +time_dep_hp_cop,--,"{true, false}",Consider the time dependent coefficient of performance (COP) of the heat pump +heat_pump_sink_T,°C,float,The temperature heat sink used in heat pumps based on DTU / large area radiators. The value is conservatively high to cover hot water and space heating in poorly-insulated buildings +reduce_space_heat _exogenously,--,"{true, false}",Influence on space heating demand by a certain factor (applied before losses in district heating). +reduce_space_heat _exogenously_factor,--,Dictionary with planning horizons as keys.,"A positive factor can mean renovation or demolition of a building. If the factor is negative, it can mean an increase in floor area, increased thermal comfort, population growth. The default factors are determined by the `Eurocalc Homes and buildings decarbonization scenario `_" +retrofitting,,, +-- retro_endogen,--,"{true, false}",Add retrofitting as an endogenous system which co-optimise space heat savings. +-- cost_factor,--,float,Weight costs for building renovation +-- interest_rate,--,float,The interest rate for investment in building components +-- annualise_cost,--,"{true, false}",Annualise the investment costs of retrofitting +-- tax_weighting,--,"{true, false}",Weight the costs of retrofitting depending on taxes in countries +-- construction_index,--,"{true, false}",Weight the costs of retrofitting depending on labour/material costs per country +tes,--,"{true, false}",Add option for storing thermal energy in large water pits associated with district heating systems and individual thermal energy storage (TES) +tes_tau,,,The time constant used to calculate the decay of thermal energy in thermal energy storage (TES): 1- :math:`e^{-1/24τ}`. +-- decentral,days,float,The time constant in decentralized thermal energy storage (TES) +-- central,days,float,The time constant in centralized thermal energy storage (TES) +boilers,--,"{true, false}",Add option for transforming gas into heat using gas boilers +resistive_heaters,--,"{true, false}",Add option for transforming electricity into heat using resistive heaters (independently from gas boilers) +oil_boilers,--,"{true, false}",Add option for transforming oil into heat using boilers +biomass_boiler,--,"{true, false}",Add option for transforming biomass into heat using boilers +overdimension_individual_heating,--,float,Add option for overdimensioning individual heating systems by a certain factor. This allows them to cover heat demand peaks e.g. 10% higher than those in the data with a setting of 1.1. +chp,--,"{true, false}",Add option for using Combined Heat and Power (CHP) +micro_chp,--,"{true, false}",Add option for using Combined Heat and Power (CHP) for decentral areas. +solar_thermal,--,"{true, false}",Add option for using solar thermal to generate heat. +solar_cf_correction,--,float,The correction factor for the value provided by the solar thermal profile calculations +marginal_cost_storage,currency/MWh ,float,The marginal cost of discharging batteries in distributed grids +methanation,--,"{true, false}",Add option for transforming hydrogen and CO2 into methane using methanation. +coal_cc,--,"{true, false}",Add option for coal CHPs with carbon capture +dac,--,"{true, false}",Add option for Direct Air Capture (DAC) +co2_vent,--,"{true, false}",Add option for vent out CO2 from storages to the atmosphere. +allam_cycle,--,"{true, false}",Add option to include `Allam cycle gas power plants `_ +hydrogen_fuel_cell,--,"{true, false}",Add option to include hydrogen fuel cell for re-electrification. Assuming OCGT technology costs +hydrogen_turbine,--,"{true, false}",Add option to include hydrogen turbine for re-electrification. Assuming OCGT technology costs +SMR,--,"{true, false}",Add option for transforming natural gas into hydrogen and CO2 using Steam Methane Reforming (SMR) +SMR CC,--,"{true, false}",Add option for transforming natural gas into hydrogen and CO2 using Steam Methane Reforming (SMR) and Carbon Capture (CC) +regional_methanol_demand,--,"{true, false}",Spatially resolve methanol demand. Set to true if regional CO2 constraints needed. +regional_oil_demand,--,"{true, false}",Spatially resolve oil demand. Set to true if regional CO2 constraints needed. +regional_co2 _sequestration_potential,,, +-- enable,--,"{true, false}",Add option for regionally-resolved geological carbon dioxide sequestration potentials based on `CO2StoP `_. +-- attribute,--,string or list,Name (or list of names) of the attribute(s) for the sequestration potential +-- include_onshore,--,"{true, false}",Add options for including onshore sequestration potentials +-- min_size,Gt ,float,Any sites with lower potential than this value will be excluded +-- max_size,Gt ,float,The maximum sequestration potential for any one site. +-- years_of_storage,years,float,The years until potential exhausted at optimised annual rate +co2_sequestration_potential,MtCO2/a,float,The potential of sequestering CO2 in Europe per year +co2_sequestration_cost,currency/tCO2,float,The cost of sequestering a ton of CO2 +co2_sequestration_lifetime,years,int,The lifetime of a CO2 sequestration site +co2_spatial,--,"{true, false}","Add option to spatially resolve carrier representing stored carbon dioxide. This allows for more detailed modelling of CCUTS, e.g. regarding the capturing of industrial process emissions, usage as feedstock for electrofuels, transport of carbon dioxide, and geological sequestration sites." +,,, +co2network,--,"{true, false}",Add option for planning a new carbon dioxide transmission network +co2_network_cost_factor,p.u.,float,The cost factor for the capital cost of the carbon dioxide transmission network +,,, +cc_fraction,--,float,The default fraction of CO2 captured with post-combustion capture +hydrogen_underground _storage,--,"{true, false}",Add options for storing hydrogen underground. Storage potential depends regionally. +hydrogen_underground _storage_locations,,"{onshore, nearshore, offshore}","The location where hydrogen underground storage can be located. Onshore, nearshore, offshore means it must be located more than 50 km away from the sea, within 50 km of the sea, or within the sea itself respectively." +,,, +ammonia,--,"{true, false, regional}","Add ammonia as a carrrier. It can be either true (copperplated NH3), false (no NH3 carrier) or ""regional"" (regionalised NH3 without network)" +min_part_load_fischer _tropsch,per unit of p_nom ,float,The minimum unit dispatch (``p_min_pu``) for the Fischer-Tropsch process +min_part_load _methanolisation,per unit of p_nom ,float,The minimum unit dispatch (``p_min_pu``) for the methanolisation process +,,, +use_fischer_tropsch _waste_heat,--,"{true, false}",Add option for using waste heat of Fischer Tropsch in district heating networks +use_fuel_cell_waste_heat,--,"{true, false}",Add option for using waste heat of fuel cells in district heating networks +use_electrolysis_waste _heat,--,"{true, false}",Add option for using waste heat of electrolysis in district heating networks +electricity_transmission _grid,--,"{true, false}",Switch for enabling/disabling the electricity transmission grid. +electricity_distribution _grid,--,"{true, false}",Add a simplified representation of the exchange capacity between transmission and distribution grid level through a link. +electricity_distribution _grid_cost_factor,,,Multiplies the investment cost of the electricity distribution grid +,,, +electricity_grid _connection,--,"{true, false}",Add the cost of electricity grid connection for onshore wind and solar +transmission_efficiency,,,Section to specify transmission losses or compression energy demands of bidirectional links. Splits them into two capacity-linked unidirectional links. +-- {carrier},--,str,The carrier of the link. +-- -- efficiency_static,p.u.,float,Length-independent transmission efficiency. +-- -- efficiency_per_1000km,p.u. per 1000 km,float,Length-dependent transmission efficiency ($\eta^{\text{length}}$) +-- -- compression_per_1000km,p.u. per 1000 km,float,Length-dependent electricity demand for compression ($\eta \cdot \text{length}$) implemented as multi-link to local electricity bus. +H2_network,--,"{true, false}",Add option for new hydrogen pipelines +gas_network,--,"{true, false}","Add existing natural gas infrastructure, incl. LNG terminals, production and entry-points. The existing gas network is added with a lossless transport model. A length-weighted `k-edge augmentation algorithm `_ can be run to add new candidate gas pipelines such that all regions of the model can be connected to the gas network. When activated, all the gas demands are regionally disaggregated as well." +H2_retrofit,--,"{true, false}",Add option for retrofiting existing pipelines to transport hydrogen. +H2_retrofit_capacity _per_CH4,--,float,"The ratio for H2 capacity per original CH4 capacity of retrofitted pipelines. The `European Hydrogen Backbone (April, 2020) p.15 `_ 60% of original natural gas capacity could be used in cost-optimal case as H2 capacity." +gas_network_connectivity _upgrade ,--,float,The number of desired edge connectivity (k) in the length-weighted `k-edge augmentation algorithm `_ used for the gas network +gas_distribution_grid,--,"{true, false}",Add a gas distribution grid +gas_distribution_grid _cost_factor,,,Multiplier for the investment cost of the gas distribution grid +,,, +biomass_spatial,--,"{true, false}",Add option for resolving biomass demand regionally +biomass_transport,--,"{true, false}",Add option for transporting solid biomass between nodes +biogas_upgrading_cc,--,"{true, false}",Add option to capture CO2 from biomass upgrading +conventional_generation,,,Add a more detailed description of conventional carriers. Any power generation requires the consumption of fuel from nodes representing that fuel. +biomass_to_liquid,--,"{true, false}",Add option for transforming solid biomass into liquid fuel with the same properties as oil +biosng,--,"{true, false}",Add option for transforming solid biomass into synthesis gas with the same properties as natural gas +limit_max_growth,,, +-- enable,--,"{true, false}",Add option to limit the maximum growth of a carrier +-- factor,p.u.,float,The maximum growth factor of a carrier (e.g. 1.3 allows 30% larger than max historic growth) +-- max_growth,,, +-- -- {carrier},GW,float,The historic maximum growth of a carrier +-- max_relative_growth,,, +-- -- {carrier},p.u.,float,The historic maximum relative growth of a carrier +,,, +enhanced_geothermal,,, +-- enable,--,"{true, false}",Add option to include Enhanced Geothermal Systems +-- flexible,--,"{true, false}",Add option for flexible operation (see Ricks et al. 2024) +-- max_hours,--,int,The maximum hours the reservoir can be charged under flexible operation +-- max_boost,--,float,The maximum boost in power output under flexible operation +-- var_cf,--,"{true, false}",Add option for variable capacity factor (see Ricks et al. 2024) +-- sustainability_factor,--,float,Share of sourced heat that is replenished by the earth's core (see details in `build_egs_potentials.py `_) +solid_biomass_import,,, +-- enable,--,"{true, false}",Add option to include solid biomass imports +-- price,currency/MWh,float,Price for importing solid biomass +-- max_amount,Twh,float,Maximum solid biomass import potential +-- upstream_emissions_factor,p.u.,float,Upstream emissions of solid biomass imports From 440f3e4a7f6cea53585d41f129dc4b9d08e68901 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Fri, 2 Aug 2024 15:26:32 +0200 Subject: [PATCH 122/344] add release notes --- doc/release_notes.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 4293541be..d2453bb11 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -10,7 +10,9 @@ Release Notes Upcoming Release ================ -* Add option to produce electrobiofuels from solid biomass and hydrogen, as a combination of BtL and Fischer-Tropsch to make more use of the biogenic carbon +* Add option to import solid biomass + +* Add option to produce electrobiofuels (flag ``electrobiofuels`) from solid biomass and hydrogen, as a combination of BtL and Fischer-Tropsch to make more use of the biogenic carbon * Add flag ``sector: fossil_fuels`` in config to remove the option of importing fossil fuels From e28c66e43c03476470e7d425c80536993b632aa0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 2 Aug 2024 13:27:40 +0000 Subject: [PATCH 123/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/prepare_sector_network.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index a3de3f20d..090555251 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2295,15 +2295,16 @@ def add_biomass(n, costs): biomass_import_price = options["solid_biomass_import"]["price"] # convert TWh in MWh biomass_import_max_amount = options["solid_biomass_import"]["max_amount"] * 1e6 - biomass_import_upstream_emissions = options["solid_biomass_import"]["upstream_emissions_factor"] - + biomass_import_upstream_emissions = options["solid_biomass_import"][ + "upstream_emissions_factor" + ] + logger.info( - "Adding biomass import with cost %.2f EUR/MWh, a limit of %.2f TWh, and embedded emissions of %.2f%%", - biomass_import_price, - options["solid_biomass_import"]["max_amount"], - biomass_import_upstream_emissions * 100 + "Adding biomass import with cost %.2f EUR/MWh, a limit of %.2f TWh, and embedded emissions of %.2f%%", + biomass_import_price, + options["solid_biomass_import"]["max_amount"], + biomass_import_upstream_emissions * 100, ) - n.add("Carrier", "solid biomass import") @@ -2333,7 +2334,8 @@ def add_biomass(n, costs): bus2="co2 atmosphere", carrier="solid biomass import", efficiency=1.0, - efficiency2=biomass_import_upstream_emissions* costs.at["solid biomass", "CO2 intensity"], + efficiency2=biomass_import_upstream_emissions + * costs.at["solid biomass", "CO2 intensity"], p_nom_extendable=True, ) From 4a6dd2fe33125086926f6653b55c308b8efb8f0c Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Fri, 2 Aug 2024 15:56:37 +0200 Subject: [PATCH 124/344] fix naming bugs --- config/config.default.yaml | 15 +++-------- scripts/build_cop_profiles/run.py | 12 ++++----- scripts/prepare_sector_network.py | 43 +++++++++++++++---------------- 3 files changed, 31 insertions(+), 39 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index bfa99d90f..ebb8eb4f1 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -417,20 +417,13 @@ sector: isentropic_compressor_efficiency: 0.8 heat_loss: 0.0 heat_pump_sources: - central: + urban central: - air - decentral: + urban decentral: - air - - ground - heat_systems: rural: - - residential rural - - services rural - urban decentral: - - residential urban decentral - - services urban decentral - urban central: - - urban central + - air + - ground cluster_heat_buses: true heat_demand_cutout: default bev_dsm_restriction_value: 0.75 diff --git a/scripts/build_cop_profiles/run.py b/scripts/build_cop_profiles/run.py index 5178483a8..583d097aa 100644 --- a/scripts/build_cop_profiles/run.py +++ b/scripts/build_cop_profiles/run.py @@ -12,18 +12,18 @@ def get_cop( - heat_system_type: str, + heat_system_category: str, heat_source: str, source_inlet_temperature_celsius: xr.DataArray, ) -> xr.DataArray: - if heat_system_type == "decentral": + if heat_system_category in ["urban decentral", "rural"]: return DecentralHeatingCopApproximator( forward_temperature_celsius=snakemake.params.heat_pump_sink_T_decentral_heating, source_inlet_temperature_celsius=source_inlet_temperature_celsius, source_type=heat_source, ).approximate_cop() - elif heat_system_type == "central": + elif heat_system_category == "urban central": return CentralHeatingCopApproximator( forward_temperature_celsius=snakemake.params.forward_temperature_central_heating, return_temperature_celsius=snakemake.params.return_temperature_central_heating, @@ -33,7 +33,7 @@ def get_cop( ).approximate_cop() else: raise ValueError( - f"Invalid heat system type '{heat_system_type}'. Must be one of ['decentral', 'central']" + f"Invalid heat system type '{heat_system_category}'. Must be one of ['urban decentral', 'urban central', 'rural]" ) @@ -50,14 +50,14 @@ def get_cop( set_scenario_config(snakemake) cop_all_system_types = [] - for heat_system_type, heat_sources in snakemake.params.heat_pump_sources.items(): + for heat_system_category, heat_sources in snakemake.params.heat_pump_sources.items(): cop_this_system_type = [] for heat_source in heat_sources: source_inlet_temperature_celsius = xr.open_dataarray( snakemake.input[f"temp_{heat_source.replace('ground', 'soil')}_total"] ) cop_da = get_cop( - heat_system_type=heat_system_type, + heat_system_category=heat_system_category, heat_source=heat_source, source_inlet_temperature_celsius=source_inlet_temperature_celsius, ) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 52989c2d7..989f4dc26 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1840,9 +1840,9 @@ def add_heat(n, costs): n.madd( "Bus", - nodes + f" {heat_system} heat", + nodes + f" {heat_system.value} heat", location=nodes, - carrier=f"{heat_system} heat", + carrier=f"{heat_system.value} heat", unit="MWh_th", ) @@ -1891,10 +1891,10 @@ def add_heat(n, costs): ) ## Add heat pumps - for heat_source in snakemake.params.heat_pump_sources[heat_system.system_type]: + for heat_source in snakemake.params.heat_pump_sources[heat_system.system_category]: costs_name = heat_system.heat_pump_costs_name(heat_source) efficiency = ( - cop.sel(heat_system=heat_system.system_type, heat_source=heat_source, name=nodes) + cop.sel(heat_system=heat_system.system_category, heat_source=heat_source, name=nodes) .to_pandas() .reindex(index=n.snapshots) if options["time_dep_hp_cop"] @@ -2013,7 +2013,7 @@ def add_heat(n, costs): lifetime=costs.at[heat_system.system_type + " solar thermal", "lifetime"], ) - if options["chp"] and heat_system == "urban central": + if options["chp"] and heat_system == HeatSystem.URBAN_CENTRAL: # add gas CHP; biomass CHP is added in biomass section n.madd( "Link", @@ -2070,7 +2070,7 @@ def add_heat(n, costs): lifetime=costs.at["central gas CHP", "lifetime"], ) - if options["chp"] and options["micro_chp"] and heat_system != "urban central": + if options["chp"] and options["micro_chp"] and heat_system.value != "urban central": n.madd( "Link", nodes + f" {heat_system} micro gas CHP", @@ -2079,7 +2079,7 @@ def add_heat(n, costs): bus1=nodes, bus2=nodes + f" {heat_system} heat", bus3="co2 atmosphere", - carrier=heat_system + " micro gas CHP", + carrier=heat_system.value + " micro gas CHP", efficiency=costs.at["micro CHP", "efficiency"], efficiency2=costs.at["micro CHP", "efficiency-heat"], efficiency3=costs.at["gas", "CO2 intensity"], @@ -2106,36 +2106,35 @@ def add_heat(n, costs): # share of space heat demand 'w_space' of total heat demand w_space = {} - for sector in HeatSector: - w_space[sector.value] = heat_demand[sector.value + " space"] / ( - heat_demand[sector.value + " space"] + heat_demand[sector.value + " water"] + for sector in sectors: + w_space[sector] = heat_demand[sector + " space"] / ( + heat_demand[sector + " space"] + heat_demand[sector + " water"] ) w_space["tot"] = ( heat_demand["services space"] + heat_demand["residential space"] ) / heat_demand.T.groupby(level=[1]).sum().T - for heat_system in n.loads[ - n.loads.carrier.isin([x.value + " heat" for x in HeatSystem]) + for name in n.loads[ + n.loads.carrier.isin([x + " heat" for x in heat_systems]) ].index: - node = n.buses.loc[heat_system, "location"] + node = n.buses.loc[name, "location"] ct = pop_layout.loc[node, "ct"] # weighting 'f' depending on the size of the population at the node - if "urban central" in heat_system: + if "urban central" in name: f = dist_fraction[node] - elif "urban decentral" in heat_system: + elif "urban decentral" in name: f = urban_fraction[node] - dist_fraction[node] else: f = 1 - urban_fraction[node] if f == 0: continue - # get sector name ("residential"/"services"/or both "tot" for urban central) - if "urban central" in heat_system: + if "urban central" in name: sec = "tot" - if "residential" in heat_system: + if "residential" in name: sec = "residential" - if "services" in heat_system: + if "services" in name: sec = "services" # get floor aread at node and region (urban/rural) in m^2 @@ -2143,7 +2142,7 @@ def add_heat(n, costs): pop_layout.loc[node].fraction * floor_area.loc[ct, "value"] * 10**6 ).loc[sec] * f # total heat demand at node [MWh] - demand = n.loads_t.p_set[heat_system] + demand = n.loads_t.p_set[name] # space heat demand at node [MWh] space_heat_demand = demand * w_space[sec][node] @@ -2184,12 +2183,12 @@ def add_heat(n, costs): # add for each retrofitting strength a generator with heat generation profile following the profile of the heat demand for strength in strengths: - node_name = " ".join(heat_system.split(" ")[2::]) + node_name = " ".join(name.split(" ")[2::]) n.madd( "Generator", [node], suffix=" retrofitting " + strength + " " + node_name, - bus=heat_system, + bus=name, carrier="retrofitting", p_nom_extendable=True, p_nom_max=dE_diff[strength] From 318e05c9fb663c467f59f7610679aca0db40cbf6 Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Fri, 2 Aug 2024 16:29:54 +0200 Subject: [PATCH 125/344] refactor naming --- scripts/build_cop_profiles/run.py | 42 +++++++++++++++++++++---------- scripts/prepare_sector_network.py | 41 +++++++++++++++--------------- 2 files changed, 49 insertions(+), 34 deletions(-) diff --git a/scripts/build_cop_profiles/run.py b/scripts/build_cop_profiles/run.py index 583d097aa..7aa5b1bf9 100644 --- a/scripts/build_cop_profiles/run.py +++ b/scripts/build_cop_profiles/run.py @@ -7,23 +7,35 @@ import pandas as pd import xarray as xr from _helpers import set_scenario_config +import sys; sys.path.append("..") +from scripts._entities import HeatSystemType from CentralHeatingCopApproximator import CentralHeatingCopApproximator from DecentralHeatingCopApproximator import DecentralHeatingCopApproximator def get_cop( - heat_system_category: str, + heat_system_type: str, heat_source: str, source_inlet_temperature_celsius: xr.DataArray, ) -> xr.DataArray: - if heat_system_category in ["urban decentral", "rural"]: - return DecentralHeatingCopApproximator( - forward_temperature_celsius=snakemake.params.heat_pump_sink_T_decentral_heating, - source_inlet_temperature_celsius=source_inlet_temperature_celsius, - source_type=heat_source, - ).approximate_cop() + """ + Calculate the coefficient of performance (COP) for a heating system. + + Parameters + ---------- + heat_system_type : str + The type of heating system. + heat_source : str + The heat source used in the heating system. + source_inlet_temperature_celsius : xr.DataArray + The inlet temperature of the heat source in Celsius. - elif heat_system_category == "urban central": + Returns + ------- + xr.DataArray + The calculated coefficient of performance (COP) for the heating system. + """ + if HeatSystemType(heat_system_type).is_central: return CentralHeatingCopApproximator( forward_temperature_celsius=snakemake.params.forward_temperature_central_heating, return_temperature_celsius=snakemake.params.return_temperature_central_heating, @@ -31,10 +43,14 @@ def get_cop( source_outlet_temperature_celsius=source_inlet_temperature_celsius - snakemake.params.heat_source_cooling_central_heating, ).approximate_cop() + else: - raise ValueError( - f"Invalid heat system type '{heat_system_category}'. Must be one of ['urban decentral', 'urban central', 'rural]" - ) + return DecentralHeatingCopApproximator( + forward_temperature_celsius=snakemake.params.heat_pump_sink_T_decentral_heating, + source_inlet_temperature_celsius=source_inlet_temperature_celsius, + source_type=heat_source, + ).approximate_cop() + if __name__ == "__main__": @@ -50,14 +66,14 @@ def get_cop( set_scenario_config(snakemake) cop_all_system_types = [] - for heat_system_category, heat_sources in snakemake.params.heat_pump_sources.items(): + for heat_system_type, heat_sources in snakemake.params.heat_pump_sources.items(): cop_this_system_type = [] for heat_source in heat_sources: source_inlet_temperature_celsius = xr.open_dataarray( snakemake.input[f"temp_{heat_source.replace('ground', 'soil')}_total"] ) cop_da = get_cop( - heat_system_category=heat_system_category, + heat_system_type=heat_system_type, heat_source=heat_source, source_inlet_temperature_celsius=source_inlet_temperature_celsius, ) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 989f4dc26..c15d81675 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1829,7 +1829,6 @@ def add_heat(n, costs): cop = xr.open_dataarray(snakemake.input.cop_profiles) for heat_system in HeatSystem: #this loops through all heat systems defined in _entities.HeatSystem - # system_type = "central" if heat_system == "urban central" else "decentral" if heat_system == HeatSystem.URBAN_CENTRAL: nodes = dist_fraction.index[dist_fraction > 0] @@ -1886,15 +1885,15 @@ def add_heat(n, costs): nodes, suffix=f" {heat_system} heat", bus=nodes + f" {heat_system} heat", - carrier=heat_system.value + " heat", + carrier=f"{heat_system} heat", p_set=heat_load, ) ## Add heat pumps - for heat_source in snakemake.params.heat_pump_sources[heat_system.system_category]: + for heat_source in snakemake.params.heat_pump_sources[heat_system.system_type.value]: costs_name = heat_system.heat_pump_costs_name(heat_source) efficiency = ( - cop.sel(heat_system=heat_system.system_category, heat_source=heat_source, name=nodes) + cop.sel(heat_system=heat_system.system_type.value, heat_source=heat_source, name=nodes) .to_pandas() .reindex(index=n.snapshots) if options["time_dep_hp_cop"] @@ -1917,13 +1916,13 @@ def add_heat(n, costs): ) if options["tes"]: - n.add("Carrier", heat_system.value + " water tanks") + n.add("Carrier", f"{heat_system} water tanks") n.madd( "Bus", nodes + f" {heat_system} water tanks", location=nodes, - carrier=heat_system.value + " water tanks", + carrier=f"{heat_system} water tanks", unit="MWh_th", ) @@ -1933,7 +1932,7 @@ def add_heat(n, costs): bus0=nodes + f" {heat_system} heat", bus1=nodes + f" {heat_system} water tanks", efficiency=costs.at["water tank charger", "efficiency"], - carrier=heat_system.value + " water tanks charger", + carrier=f"{heat_system} water tanks charger", p_nom_extendable=True, ) @@ -1942,12 +1941,12 @@ def add_heat(n, costs): nodes + f" {heat_system} water tanks discharger", bus0=nodes + f" {heat_system} water tanks", bus1=nodes + f" {heat_system} heat", - carrier=heat_system.value + " water tanks discharger", + carrier=f"{heat_system} water tanks discharger", efficiency=costs.at["water tank discharger", "efficiency"], p_nom_extendable=True, ) - tes_time_constant_days = options["tes_tau"][heat_system.system_type] + tes_time_constant_days = options["tes_tau"][heat_system.central_or_decentral] n.madd( "Store", @@ -1955,21 +1954,21 @@ def add_heat(n, costs): bus=nodes + f" {heat_system} water tanks", e_cyclic=True, e_nom_extendable=True, - carrier=heat_system.value + " water tanks", + carrier=f"{heat_system} water tanks", standing_loss=1 - np.exp(-1 / 24 / tes_time_constant_days), - capital_cost=costs.at[heat_system.system_type + " water tank storage", "fixed"], - lifetime=costs.at[heat_system.system_type + " water tank storage", "lifetime"], + capital_cost=costs.at[heat_system.central_or_decentral + " water tank storage", "fixed"], + lifetime=costs.at[heat_system.central_or_decentral + " water tank storage", "lifetime"], ) if options["resistive_heaters"]: - key = f"{heat_system.system_type} resistive heater" + key = f"{heat_system.central_or_decentral} resistive heater" n.madd( "Link", nodes + f" {heat_system} resistive heater", bus0=nodes, bus1=nodes + f" {heat_system} heat", - carrier=heat_system.value + " resistive heater", + carrier=f"{heat_system} resistive heater", efficiency=costs.at[key, "efficiency"], capital_cost=costs.at[key, "efficiency"] * costs.at[key, "fixed"] @@ -1979,7 +1978,7 @@ def add_heat(n, costs): ) if options["boilers"]: - key = f"{heat_system.system_type} gas boiler" + key = f"{heat_system.central_or_decentral} gas boiler" n.madd( "Link", @@ -1988,7 +1987,7 @@ def add_heat(n, costs): bus0=spatial.gas.df.loc[nodes, "nodes"].values, bus1=nodes + f" {heat_system} heat", bus2="co2 atmosphere", - carrier=heat_system.value + " gas boiler", + carrier=f"{heat_system} gas boiler", efficiency=costs.at[key, "efficiency"], efficiency2=costs.at["gas", "CO2 intensity"], capital_cost=costs.at[key, "efficiency"] @@ -1998,19 +1997,19 @@ def add_heat(n, costs): ) if options["solar_thermal"]: - n.add("Carrier", heat_system.value + " solar thermal") + n.add("Carrier", f"{heat_system} solar thermal") n.madd( "Generator", nodes, suffix=f" {heat_system} solar thermal collector", bus=nodes + f" {heat_system} heat", - carrier=heat_system.value + " solar thermal", + carrier=f"{heat_system} solar thermal", p_nom_extendable=True, - capital_cost=costs.at[heat_system.system_type + " solar thermal", "fixed"] + capital_cost=costs.at[heat_system.central_or_decentral + " solar thermal", "fixed"] * overdim_factor, p_max_pu=solar_thermal[nodes], - lifetime=costs.at[heat_system.system_type + " solar thermal", "lifetime"], + lifetime=costs.at[heat_system.central_or_decentral + " solar thermal", "lifetime"], ) if options["chp"] and heat_system == HeatSystem.URBAN_CENTRAL: @@ -2115,7 +2114,7 @@ def add_heat(n, costs): ) / heat_demand.T.groupby(level=[1]).sum().T for name in n.loads[ - n.loads.carrier.isin([x + " heat" for x in heat_systems]) + n.loads.carrier.isin([x + " heat" for x in HeatSystem]) ].index: node = n.buses.loc[name, "location"] ct = pop_layout.loc[node, "ct"] From 69c6a4dca418d01e6f74c7a95ed64ad2ea3afb38 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Fri, 2 Aug 2024 16:45:57 +0200 Subject: [PATCH 126/344] set e_max_pu to force municipal waste to be used --- scripts/prepare_sector_network.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 87170287c..142ecca58 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2358,7 +2358,8 @@ def add_biomass(n, costs): * costs.at["solid biomass", "CO2 intensity"], p_nom_extendable=True, ) - + + e_max_pu = pd.Series([1] * (len(snapshots) - 1) + [0], index=n.snapshots) n.madd( "Store", spatial.msw.nodes, @@ -2366,6 +2367,7 @@ def add_biomass(n, costs): carrier="municipal solid waste", e_nom=msw_biomass_potentials_spatial, marginal_cost=0, # costs.at["municipal solid waste", "fuel"], + e_max_pu=e_max_pu, e_initial=msw_biomass_potentials_spatial, ) From 3149bfd926d2aad2741b3c4cbee629dbcdd4d635 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 2 Aug 2024 14:47:20 +0000 Subject: [PATCH 127/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/prepare_sector_network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 142ecca58..4e736176a 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2358,7 +2358,7 @@ def add_biomass(n, costs): * costs.at["solid biomass", "CO2 intensity"], p_nom_extendable=True, ) - + e_max_pu = pd.Series([1] * (len(snapshots) - 1) + [0], index=n.snapshots) n.madd( "Store", From b62db804351166ae548d727af95d8ba621b10b7f Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Fri, 2 Aug 2024 16:59:42 +0200 Subject: [PATCH 128/344] fix bug with e_max_pu --- 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 4e736176a..c7080f630 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2358,8 +2358,8 @@ def add_biomass(n, costs): * costs.at["solid biomass", "CO2 intensity"], p_nom_extendable=True, ) - - e_max_pu = pd.Series([1] * (len(snapshots) - 1) + [0], index=n.snapshots) + + e_max_pu = pd.Series([1] * (len(n.snapshots) - 1) + [0], index=n.snapshots) n.madd( "Store", spatial.msw.nodes, From f58322c3e52b890858c00c01053afacafe95f870 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 2 Aug 2024 15:02:07 +0000 Subject: [PATCH 129/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/prepare_sector_network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index c7080f630..8fdbac6ed 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2358,7 +2358,7 @@ def add_biomass(n, costs): * costs.at["solid biomass", "CO2 intensity"], p_nom_extendable=True, ) - + e_max_pu = pd.Series([1] * (len(n.snapshots) - 1) + [0], index=n.snapshots) n.madd( "Store", From 76b377b2d149c6637fa9637ff99335ce89af0ada Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Fri, 2 Aug 2024 17:02:16 +0200 Subject: [PATCH 130/344] udpate docs --- doc/configtables/sector.csv | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/doc/configtables/sector.csv b/doc/configtables/sector.csv index e14da5573..a075dd3d7 100644 --- a/doc/configtables/sector.csv +++ b/doc/configtables/sector.csv @@ -6,6 +6,8 @@ industry,--,"{true, false}",Flag to include industry sector. agriculture,--,"{true, false}",Flag to include agriculture sector. district_heating,--,,`prepare_sector_network.py `_ -- potential,--,float,maximum fraction of urban demand which can be supplied by district heating. Ignored where below current fraction. +-- progress,--,Dictionary with planning horizons as keys., Increase of today's district heating demand to potential maximum district heating share. Progress = 0 means today's district heating share. Progress = 1 means maximum fraction of urban demand is supplied by district heating +-- district_heating_loss,--,float,Share increase in district heat demand in urban central due to heat losses -- forward_temperature,°C,float,Forward temperature in district heating -- return_temperature,°C,float,Return temperature in district heating. Must be lower than forward temperature -- heat_source_cooling,K,float,Cooling of heat source for heat pumps @@ -14,8 +16,10 @@ district_heating,--,,`prepare_sector_network.py `_ to one to save memory. ,,, bev_dsm_restriction _value,--,float,Adds a lower state of charge (SOC) limit for battery electric vehicles (BEV) to manage its own energy demand (DSM). Located in `build_transport_demand.py `_. Set to 0 for no restriction on BEV DSM From bbf64a2fde2825f0a954526b1b99cc53ce1f7b60 Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Fri, 2 Aug 2024 17:03:34 +0200 Subject: [PATCH 131/344] Refactor module structure --- scripts/build_cop_profiles/run.py | 2 +- scripts/enums/HeatSector.py | 25 ++++ scripts/enums/HeatSystem.py | 218 ++++++++++++++++++++++++++++++ scripts/enums/HeatSystemType.py | 35 +++++ scripts/prepare_sector_network.py | 4 +- 5 files changed, 282 insertions(+), 2 deletions(-) create mode 100644 scripts/enums/HeatSector.py create mode 100644 scripts/enums/HeatSystem.py create mode 100644 scripts/enums/HeatSystemType.py diff --git a/scripts/build_cop_profiles/run.py b/scripts/build_cop_profiles/run.py index 7aa5b1bf9..776ed289a 100644 --- a/scripts/build_cop_profiles/run.py +++ b/scripts/build_cop_profiles/run.py @@ -8,7 +8,7 @@ import xarray as xr from _helpers import set_scenario_config import sys; sys.path.append("..") -from scripts._entities import HeatSystemType +from scripts.enums.HeatSystemType import HeatSystemType from CentralHeatingCopApproximator import CentralHeatingCopApproximator from DecentralHeatingCopApproximator import DecentralHeatingCopApproximator diff --git a/scripts/enums/HeatSector.py b/scripts/enums/HeatSector.py new file mode 100644 index 000000000..008a06933 --- /dev/null +++ b/scripts/enums/HeatSector.py @@ -0,0 +1,25 @@ +# SPDX-License-Identifier: MIT + +from enum import Enum + +class HeatSector(Enum): + """ + Enumeration class representing different heat sectors. + + Attributes: + RESIDENTIAL (str): Represents the residential heat sector. + SERVICES (str): Represents the services heat sector. + """ + + RESIDENTIAL = "residential" + SERVICES = "services" + + def __str__(self) -> str: + """ + Returns the string representation of the heat sector. + + Returns: + str: The string representation of the heat sector. + """ + return self.value + diff --git a/scripts/enums/HeatSystem.py b/scripts/enums/HeatSystem.py new file mode 100644 index 000000000..042793a6c --- /dev/null +++ b/scripts/enums/HeatSystem.py @@ -0,0 +1,218 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2020-2024 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT + +from enum import Enum + +from .HeatSystemType import HeatSystemType +from .HeatSector import HeatSector + +class HeatSystem(Enum): + """ + Enumeration representing different heat systems. + + Attributes + ---------- + RESIDENTIAL_RURAL : str + Heat system for residential areas in rural locations. + SERVICES_RURAL : str + Heat system for service areas in rural locations. + RESIDENTIAL_URBAN_DECENTRAL : str + Heat system for residential areas in urban decentralized locations. + SERVICES_URBAN_DECENTRAL : str + Heat system for service areas in urban decentralized locations. + URBAN_CENTRAL : str + Heat system for urban central areas. + + Methods + ------- + __str__() + Returns the string representation of the heat system. + central_or_decentral() + Returns whether the heat system is central or decentralized. + system_type() + Returns the type of the heat system. + sector() + Returns the sector of the heat system. + rural() + Returns whether the heat system is for rural areas. + urban_decentral() + Returns whether the heat system is for urban decentralized areas. + urban() + Returns whether the heat system is for urban areas. + heat_demand_weighting(urban_fraction=None, dist_fraction=None) + Calculates the heat demand weighting based on urban fraction and distribution fraction. + heat_pump_costs_name(heat_source) + Generates the name for the heat pump costs based on the heat source. + """ + + RESIDENTIAL_RURAL = "residential rural" + SERVICES_RURAL = "services rural" + RESIDENTIAL_URBAN_DECENTRAL = "residential urban decentral" + SERVICES_URBAN_DECENTRAL = "services urban decentral" + URBAN_CENTRAL = "urban central" + + def __init__(self, *args): + super().__init__(*args) + + def __str__(self) -> str: + """ + Returns the string representation of the heat system. + + Returns + ------- + str + The string representation of the heat system. + """ + return self.value + + @property + def central_or_decentral(self) -> str: + """ + Returns whether the heat system is central or decentralized. + + Returns + ------- + str + "central" if the heat system is central, "decentral" otherwise. + """ + if self == HeatSystem.URBAN_CENTRAL: + return "central" + else: + return "decentral" + + @property + def system_type(self) -> HeatSystemType: + """ + Returns the type of the heat system. + + Returns + ------- + str + The type of the heat system. + + Raises + ------ + RuntimeError + If the heat system is invalid. + """ + if self == HeatSystem.URBAN_CENTRAL: + return HeatSystemType.URBAN_CENTRAL + elif self == HeatSystem.RESIDENTIAL_URBAN_DECENTRAL or self == HeatSystem.SERVICES_URBAN_DECENTRAL: + return HeatSystemType.URBAN_DECENTRAL + elif self == HeatSystem.RESIDENTIAL_RURAL or self == HeatSystem.SERVICES_RURAL: + return HeatSystemType.RURAL + else: + raise RuntimeError(f"Invalid heat system: {self}") + + @property + def sector(self) -> HeatSector: + """ + Returns the sector of the heat system. + + Returns + ------- + HeatSector + The sector of the heat system. + """ + if ( + self == HeatSystem.RESIDENTIAL_RURAL + or self == HeatSystem.RESIDENTIAL_URBAN_DECENTRAL + ): + return HeatSector.RESIDENTIAL + elif ( + self == HeatSystem.SERVICES_RURAL + or self == HeatSystem.SERVICES_URBAN_DECENTRAL + ): + return HeatSector.SERVICES + else: + 'tot' + + @property + def is_rural(self) -> bool: + """ + Returns whether the heat system is for rural areas. + + Returns + ------- + bool + True if the heat system is for rural areas, False otherwise. + """ + if self == HeatSystem.RESIDENTIAL_RURAL or self == HeatSystem.SERVICES_RURAL: + return True + else: + return False + + @property + def is_urban_decentral(self) -> bool: + """ + Returns whether the heat system is for urban decentralized areas. + + Returns + ------- + bool + True if the heat system is for urban decentralized areas, False otherwise. + """ + if self == HeatSystem.RESIDENTIAL_URBAN_DECENTRAL or self == HeatSystem.SERVICES_URBAN_DECENTRAL: + return True + else: + return False + + @property + def is_urban(self) -> bool: + """ + Returns whether the heat system is for urban areas. + + Returns + ------- + bool True if the heat system is for urban areas, False otherwise. """ + return not self.is_rural + + def heat_demand_weighting(self, urban_fraction=None, dist_fraction=None) -> float: + """ + Calculates the heat demand weighting based on urban fraction and distribution fraction. + + Parameters + ---------- + urban_fraction : float, optional + The fraction of urban heat demand. + dist_fraction : float, optional + The fraction of distributed heat demand. + + Returns + ------- + float + The heat demand weighting. + + Raises + ------ + RuntimeError + If the heat system is invalid. + """ + if "rural" in self.value: + return 1 - urban_fraction + elif "urban central" in self.value: + return dist_fraction + elif "urban decentral" in self.value: + return urban_fraction - dist_fraction + else: + raise RuntimeError(f"Invalid heat system: {self}") + + def heat_pump_costs_name(self, heat_source: str) -> str: + """ + Generates the name for the heat pump costs based on the heat source. + + Parameters + ---------- + heat_source : str + The heat source. + + Returns + ------- + str + The name for the heat pump costs. + """ + return f"{self.central_or_decentral} {heat_source}-sourced heat pump" + + diff --git a/scripts/enums/HeatSystemType.py b/scripts/enums/HeatSystemType.py new file mode 100644 index 000000000..6847965da --- /dev/null +++ b/scripts/enums/HeatSystemType.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2020-2024 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT + +from enum import Enum + +class HeatSystemType(Enum): + """ + Enumeration representing different types of heat systems. + """ + + URBAN_CENTRAL = "urban central" + URBAN_DECENTRAL = "urban decentral" + RURAL = "rural" + + def __str__(self) -> str: + """ + Returns the string representation of the heat system type. + + Returns: + str: The string representation of the heat system type. + """ + return self.value + + @property + def is_central(self) -> bool: + """ + Returns whether the heat system type is central. + + Returns: + bool: True if the heat system type is central, False otherwise. + """ + return self == HeatSystemType.URBAN_CENTRAL + diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index c15d81675..99598f45f 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -22,7 +22,9 @@ set_scenario_config, update_config_from_wildcards, ) -from _entities import HeatSystem, HeatSector +from scripts.enums.HeatSystem import HeatSystem +from scripts.enums.HeatSystemType import HeatSystemType +from scripts.enums.HeatSector import HeatSector from add_electricity import calculate_annuity, sanitize_carriers, sanitize_locations from build_energy_totals import ( build_co2_totals, From 6b924ecfb0088e1100df0edadedbebcd98d917a5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 2 Aug 2024 15:15:09 +0000 Subject: [PATCH 132/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- rules/build_sector.smk | 2 +- scripts/build_cop_profiles/run.py | 9 +++-- scripts/enums/HeatSector.py | 3 +- scripts/enums/HeatSystem.py | 35 +++++++++++------- scripts/enums/HeatSystemType.py | 4 +- scripts/prepare_sector_network.py | 61 +++++++++++++++++++++++-------- 6 files changed, 78 insertions(+), 36 deletions(-) diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 34477f5b4..5916aa02b 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -965,7 +965,7 @@ rule prepare_sector_network: emissions_scope=config_provider("energy", "emissions"), RDIR=RDIR, heat_pump_sources=config_provider("sector", "heat_pump_sources"), - heat_systems=config_provider("sector", "heat_systems") + heat_systems=config_provider("sector", "heat_systems"), input: unpack(input_profile_offwind), **rules.cluster_gas_network.output, diff --git a/scripts/build_cop_profiles/run.py b/scripts/build_cop_profiles/run.py index 776ed289a..6a4a00618 100644 --- a/scripts/build_cop_profiles/run.py +++ b/scripts/build_cop_profiles/run.py @@ -7,11 +7,15 @@ import pandas as pd import xarray as xr from _helpers import set_scenario_config -import sys; sys.path.append("..") -from scripts.enums.HeatSystemType import HeatSystemType from CentralHeatingCopApproximator import CentralHeatingCopApproximator from DecentralHeatingCopApproximator import DecentralHeatingCopApproximator +from scripts.enums.HeatSystemType import HeatSystemType + +import sys + +sys.path.append("..") + def get_cop( heat_system_type: str, @@ -52,7 +56,6 @@ def get_cop( ).approximate_cop() - if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake diff --git a/scripts/enums/HeatSector.py b/scripts/enums/HeatSector.py index 008a06933..4c763fec1 100644 --- a/scripts/enums/HeatSector.py +++ b/scripts/enums/HeatSector.py @@ -1,7 +1,9 @@ +# -*- coding: utf-8 -*- # SPDX-License-Identifier: MIT from enum import Enum + class HeatSector(Enum): """ Enumeration class representing different heat sectors. @@ -22,4 +24,3 @@ def __str__(self) -> str: str: The string representation of the heat sector. """ return self.value - diff --git a/scripts/enums/HeatSystem.py b/scripts/enums/HeatSystem.py index 042793a6c..608da92fa 100644 --- a/scripts/enums/HeatSystem.py +++ b/scripts/enums/HeatSystem.py @@ -5,8 +5,9 @@ from enum import Enum -from .HeatSystemType import HeatSystemType -from .HeatSector import HeatSector +from scripts.enums.HeatSector import HeatSector +from scripts.enums.HeatSystemType import HeatSystemType + class HeatSystem(Enum): """ @@ -55,7 +56,7 @@ class HeatSystem(Enum): def __init__(self, *args): super().__init__(*args) - + def __str__(self) -> str: """ Returns the string representation of the heat system. @@ -81,7 +82,7 @@ def central_or_decentral(self) -> str: return "central" else: return "decentral" - + @property def system_type(self) -> HeatSystemType: """ @@ -99,7 +100,10 @@ def system_type(self) -> HeatSystemType: """ if self == HeatSystem.URBAN_CENTRAL: return HeatSystemType.URBAN_CENTRAL - elif self == HeatSystem.RESIDENTIAL_URBAN_DECENTRAL or self == HeatSystem.SERVICES_URBAN_DECENTRAL: + elif ( + self == HeatSystem.RESIDENTIAL_URBAN_DECENTRAL + or self == HeatSystem.SERVICES_URBAN_DECENTRAL + ): return HeatSystemType.URBAN_DECENTRAL elif self == HeatSystem.RESIDENTIAL_RURAL or self == HeatSystem.SERVICES_RURAL: return HeatSystemType.RURAL @@ -127,7 +131,7 @@ def sector(self) -> HeatSector: ): return HeatSector.SERVICES else: - 'tot' + "tot" @property def is_rural(self) -> bool: @@ -143,7 +147,7 @@ def is_rural(self) -> bool: return True else: return False - + @property def is_urban_decentral(self) -> bool: """ @@ -154,7 +158,10 @@ def is_urban_decentral(self) -> bool: bool True if the heat system is for urban decentralized areas, False otherwise. """ - if self == HeatSystem.RESIDENTIAL_URBAN_DECENTRAL or self == HeatSystem.SERVICES_URBAN_DECENTRAL: + if ( + self == HeatSystem.RESIDENTIAL_URBAN_DECENTRAL + or self == HeatSystem.SERVICES_URBAN_DECENTRAL + ): return True else: return False @@ -166,12 +173,14 @@ def is_urban(self) -> bool: Returns ------- - bool True if the heat system is for urban areas, False otherwise. """ + bool True if the heat system is for urban areas, False otherwise. + """ return not self.is_rural - + def heat_demand_weighting(self, urban_fraction=None, dist_fraction=None) -> float: """ - Calculates the heat demand weighting based on urban fraction and distribution fraction. + Calculates the heat demand weighting based on urban fraction and + distribution fraction. Parameters ---------- @@ -198,7 +207,7 @@ def heat_demand_weighting(self, urban_fraction=None, dist_fraction=None) -> floa return urban_fraction - dist_fraction else: raise RuntimeError(f"Invalid heat system: {self}") - + def heat_pump_costs_name(self, heat_source: str) -> str: """ Generates the name for the heat pump costs based on the heat source. @@ -214,5 +223,3 @@ def heat_pump_costs_name(self, heat_source: str) -> str: The name for the heat pump costs. """ return f"{self.central_or_decentral} {heat_source}-sourced heat pump" - - diff --git a/scripts/enums/HeatSystemType.py b/scripts/enums/HeatSystemType.py index 6847965da..5bd1bee32 100644 --- a/scripts/enums/HeatSystemType.py +++ b/scripts/enums/HeatSystemType.py @@ -5,6 +5,7 @@ from enum import Enum + class HeatSystemType(Enum): """ Enumeration representing different types of heat systems. @@ -22,7 +23,7 @@ def __str__(self) -> str: str: The string representation of the heat system type. """ return self.value - + @property def is_central(self) -> bool: """ @@ -32,4 +33,3 @@ def is_central(self) -> bool: bool: True if the heat system type is central, False otherwise. """ return self == HeatSystemType.URBAN_CENTRAL - diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index cd3c2beb9..88748dcc6 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -22,9 +22,6 @@ set_scenario_config, update_config_from_wildcards, ) -from scripts.enums.HeatSystem import HeatSystem -from scripts.enums.HeatSystemType import HeatSystemType -from scripts.enums.HeatSector import HeatSector from add_electricity import calculate_annuity, sanitize_carriers, sanitize_locations from build_energy_totals import ( build_co2_totals, @@ -40,6 +37,10 @@ from pypsa.io import import_components_from_dataframe from scipy.stats import beta +from scripts.enums.HeatSector import HeatSector +from scripts.enums.HeatSystem import HeatSystem +from scripts.enums.HeatSystemType import HeatSystemType + spatial = SimpleNamespace() logger = logging.getLogger(__name__) @@ -1833,7 +1834,11 @@ def add_heat(n, costs): solar_thermal = options["solar_cf_correction"] * solar_thermal / 1e3 cop = xr.open_dataarray(snakemake.input.cop_profiles) - for heat_system in HeatSystem: #this loops through all heat systems defined in _entities.HeatSystem + for ( + heat_system + ) in ( + HeatSystem + ): # this loops through all heat systems defined in _entities.HeatSystem if heat_system == HeatSystem.URBAN_CENTRAL: nodes = dist_fraction.index[dist_fraction > 0] @@ -1864,17 +1869,23 @@ def add_heat(n, costs): ) ## Add heat load - factor = heat_system.heat_demand_weighting(urban_fraction=urban_fraction[nodes], dist_fraction=dist_fraction[nodes]) + factor = heat_system.heat_demand_weighting( + urban_fraction=urban_fraction[nodes], dist_fraction=dist_fraction[nodes] + ) if not heat_system == HeatSystem.URBAN_CENTRAL: heat_load = ( - heat_demand[[heat_system.sector.value + " water", heat_system.sector.value + " space"]] + heat_demand[ + [ + heat_system.sector.value + " water", + heat_system.sector.value + " space", + ] + ] .T.groupby(level=1) .sum() .T[nodes] .multiply(factor) ) - if heat_system == HeatSystem.URBAN_CENTRAL: heat_load = ( heat_demand.T.groupby(level=1) @@ -1895,10 +1906,16 @@ def add_heat(n, costs): ) ## Add heat pumps - for heat_source in snakemake.params.heat_pump_sources[heat_system.system_type.value]: + for heat_source in snakemake.params.heat_pump_sources[ + heat_system.system_type.value + ]: costs_name = heat_system.heat_pump_costs_name(heat_source) efficiency = ( - cop.sel(heat_system=heat_system.system_type.value, heat_source=heat_source, name=nodes) + cop.sel( + heat_system=heat_system.system_type.value, + heat_source=heat_source, + name=nodes, + ) .to_pandas() .reindex(index=n.snapshots) if options["time_dep_hp_cop"] @@ -1951,7 +1968,9 @@ def add_heat(n, costs): p_nom_extendable=True, ) - tes_time_constant_days = options["tes_tau"][heat_system.central_or_decentral] + tes_time_constant_days = options["tes_tau"][ + heat_system.central_or_decentral + ] n.madd( "Store", @@ -1961,8 +1980,12 @@ def add_heat(n, costs): e_nom_extendable=True, carrier=f"{heat_system} water tanks", standing_loss=1 - np.exp(-1 / 24 / tes_time_constant_days), - capital_cost=costs.at[heat_system.central_or_decentral + " water tank storage", "fixed"], - lifetime=costs.at[heat_system.central_or_decentral + " water tank storage", "lifetime"], + capital_cost=costs.at[ + heat_system.central_or_decentral + " water tank storage", "fixed" + ], + lifetime=costs.at[ + heat_system.central_or_decentral + " water tank storage", "lifetime" + ], ) if options["resistive_heaters"]: @@ -2011,10 +2034,14 @@ def add_heat(n, costs): bus=nodes + f" {heat_system} heat", carrier=f"{heat_system} solar thermal", p_nom_extendable=True, - capital_cost=costs.at[heat_system.central_or_decentral + " solar thermal", "fixed"] + capital_cost=costs.at[ + heat_system.central_or_decentral + " solar thermal", "fixed" + ] * overdim_factor, p_max_pu=solar_thermal[nodes], - lifetime=costs.at[heat_system.central_or_decentral + " solar thermal", "lifetime"], + lifetime=costs.at[ + heat_system.central_or_decentral + " solar thermal", "lifetime" + ], ) if options["chp"] and heat_system == HeatSystem.URBAN_CENTRAL: @@ -2074,7 +2101,11 @@ def add_heat(n, costs): lifetime=costs.at["central gas CHP", "lifetime"], ) - if options["chp"] and options["micro_chp"] and heat_system.value != "urban central": + if ( + options["chp"] + and options["micro_chp"] + and heat_system.value != "urban central" + ): n.madd( "Link", nodes + f" {heat_system} micro gas CHP", From a990a898ef3535000204fbc1508f88b69b3c2896 Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Fri, 2 Aug 2024 17:27:16 +0200 Subject: [PATCH 133/344] update naming in add_existing_baseyear --- scripts/add_existing_baseyear.py | 43 +++++++++++++++++--------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index 610523e99..6edc7b7bc 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -23,6 +23,9 @@ ) from add_electricity import sanitize_carriers from prepare_sector_network import cluster_heat_buses, define_spatial, prepare_costs +from scripts.enums.HeatSystem import HeatSystem +from scripts.enums.HeatSystemType import HeatSystemType +from scripts.enums.HeatSector import HeatSector logger = logging.getLogger(__name__) cc = coco.CountryConverter() @@ -443,23 +446,23 @@ def add_heating_capacities_installed_before_baseyear( logger.debug(f"Adding heating capacities installed before {baseyear}") for heat_system in existing_heating.columns.get_level_values(0).unique(): - system_type = "central" if heat_system == "urban central" else "decentral" + heat_system = HeatSystem(heat_system) nodes = pd.Index( n.buses.location[n.buses.index.str.contains(f"{heat_system} heat")] ) - if (system_type != "central") and options["electricity_distribution_grid"]: + if (not heat_system.is_central) and options["electricity_distribution_grid"]: nodes_elec = nodes + " low voltage" else: nodes_elec = nodes # Add heat pumps - heat_source = snakemake.params.heat_pump_sources[system_type] - costs_name = f"{system_type} {heat_source}-sourced heat pump" + heat_source = snakemake.params.heat_pump_sources[heat_system.system_type.value] + costs_name = f"{heat_system.system_type} {heat_source}-sourced heat pump" efficiency = ( - cop.sel(heat_system=system_type, heat_source=heat_source, name=nodes) + cop.sel(heat_system=heat_system.system_type.value, heat_source=heat_source, name=nodes) .to_pandas() .reindex(index=n.snapshots) if options["time_dep_hp_cop"] @@ -496,13 +499,13 @@ def add_heating_capacities_installed_before_baseyear( nodes, suffix=f" {heat_system} {heat_source} heat pump-{grouping_year}", bus0=nodes_elec, - bus1=nodes + " " + heat_system + " heat", + bus1=nodes + " " + heat_system.value + " heat", carrier=f"{heat_system} {heat_source} heat pump", efficiency=efficiency, capital_cost=costs.at[costs_name, "efficiency"] * costs.at[costs_name, "fixed"], p_nom=existing_heating.loc[ - nodes, (heat_system, f"{heat_source} heat pump") + nodes, (heat_system.value, f"{heat_source} heat pump") ] * ratio / costs.at[costs_name, "efficiency"], @@ -516,20 +519,20 @@ def add_heating_capacities_installed_before_baseyear( nodes, suffix=f" {heat_system} resistive heater-{grouping_year}", bus0=nodes_elec, - bus1=nodes + " " + heat_system + " heat", + bus1=nodes + " " + heat_system.value + " heat", carrier=heat_system + " resistive heater", - efficiency=costs.at[f"{system_type} resistive heater", "efficiency"], + efficiency=costs.at[f"{heat_system.system_type} resistive heater", "efficiency"], capital_cost=( - costs.at[f"{system_type} resistive heater", "efficiency"] - * costs.at[f"{system_type} resistive heater", "fixed"] + costs.at[f"{heat_system.system_type} resistive heater", "efficiency"] + * costs.at[f"{heat_system.system_type} resistive heater", "fixed"] ), p_nom=( - existing_heating.loc[nodes, (heat_system, "resistive heater")] + existing_heating.loc[nodes, (heat_system.value, "resistive heater")] * ratio - / costs.at[f"{system_type} resistive heater", "efficiency"] + / costs.at[f"{heat_system.system_type} resistive heater", "efficiency"] ), build_year=int(grouping_year), - lifetime=costs.at[f"{system_type} resistive heater", "lifetime"], + lifetime=costs.at[f"{heat_system.system_type} resistive heater", "lifetime"], ) n.madd( @@ -540,19 +543,19 @@ def add_heating_capacities_installed_before_baseyear( bus1=nodes + " " + heat_system + " heat", bus2="co2 atmosphere", carrier=heat_system + " gas boiler", - efficiency=costs.at[f"{system_type} gas boiler", "efficiency"], + efficiency=costs.at[f"{heat_system.system_type} gas boiler", "efficiency"], efficiency2=costs.at["gas", "CO2 intensity"], capital_cost=( - costs.at[f"{system_type} gas boiler", "efficiency"] - * costs.at[f"{system_type} gas boiler", "fixed"] + costs.at[f"{heat_system.system_type} gas boiler", "efficiency"] + * costs.at[f"{heat_system.system_type} gas boiler", "fixed"] ), p_nom=( existing_heating.loc[nodes, (heat_system, "gas boiler")] * ratio - / costs.at[f"{system_type} gas boiler", "efficiency"] + / costs.at[f"{heat_system.system_type} gas boiler", "efficiency"] ), build_year=int(grouping_year), - lifetime=costs.at[f"{system_type} gas boiler", "lifetime"], + lifetime=costs.at[f"{heat_system.system_type} gas boiler", "lifetime"], ) n.madd( @@ -573,7 +576,7 @@ def add_heating_capacities_installed_before_baseyear( / costs.at["decentral oil boiler", "efficiency"] ), build_year=int(grouping_year), - lifetime=costs.at[f"{system_type} gas boiler", "lifetime"], + lifetime=costs.at[f"{heat_system.system_type} gas boiler", "lifetime"], ) # delete links with p_nom=nan corresponding to extra nodes in country From 0259e066e4a5688d002e47ec9b34bc9e45ae437d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 2 Aug 2024 15:28:09 +0000 Subject: [PATCH 134/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/add_existing_baseyear.py | 29 ++++++++++++++++++++++------- scripts/build_cop_profiles/run.py | 4 ++-- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index 6edc7b7bc..bf5a8dc35 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -23,9 +23,10 @@ ) from add_electricity import sanitize_carriers from prepare_sector_network import cluster_heat_buses, define_spatial, prepare_costs + +from scripts.enums.HeatSector import HeatSector from scripts.enums.HeatSystem import HeatSystem from scripts.enums.HeatSystemType import HeatSystemType -from scripts.enums.HeatSector import HeatSector logger = logging.getLogger(__name__) cc = coco.CountryConverter() @@ -462,7 +463,11 @@ def add_heating_capacities_installed_before_baseyear( costs_name = f"{heat_system.system_type} {heat_source}-sourced heat pump" efficiency = ( - cop.sel(heat_system=heat_system.system_type.value, heat_source=heat_source, name=nodes) + cop.sel( + heat_system=heat_system.system_type.value, + heat_source=heat_source, + name=nodes, + ) .to_pandas() .reindex(index=n.snapshots) if options["time_dep_hp_cop"] @@ -521,18 +526,26 @@ def add_heating_capacities_installed_before_baseyear( bus0=nodes_elec, bus1=nodes + " " + heat_system.value + " heat", carrier=heat_system + " resistive heater", - efficiency=costs.at[f"{heat_system.system_type} resistive heater", "efficiency"], + efficiency=costs.at[ + f"{heat_system.system_type} resistive heater", "efficiency" + ], capital_cost=( - costs.at[f"{heat_system.system_type} resistive heater", "efficiency"] + costs.at[ + f"{heat_system.system_type} resistive heater", "efficiency" + ] * costs.at[f"{heat_system.system_type} resistive heater", "fixed"] ), p_nom=( existing_heating.loc[nodes, (heat_system.value, "resistive heater")] * ratio - / costs.at[f"{heat_system.system_type} resistive heater", "efficiency"] + / costs.at[ + f"{heat_system.system_type} resistive heater", "efficiency" + ] ), build_year=int(grouping_year), - lifetime=costs.at[f"{heat_system.system_type} resistive heater", "lifetime"], + lifetime=costs.at[ + f"{heat_system.system_type} resistive heater", "lifetime" + ], ) n.madd( @@ -543,7 +556,9 @@ def add_heating_capacities_installed_before_baseyear( bus1=nodes + " " + heat_system + " heat", bus2="co2 atmosphere", carrier=heat_system + " gas boiler", - efficiency=costs.at[f"{heat_system.system_type} gas boiler", "efficiency"], + efficiency=costs.at[ + f"{heat_system.system_type} gas boiler", "efficiency" + ], efficiency2=costs.at["gas", "CO2 intensity"], capital_cost=( costs.at[f"{heat_system.system_type} gas boiler", "efficiency"] diff --git a/scripts/build_cop_profiles/run.py b/scripts/build_cop_profiles/run.py index 6a4a00618..7e405a424 100644 --- a/scripts/build_cop_profiles/run.py +++ b/scripts/build_cop_profiles/run.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: MIT +import sys + import numpy as np import pandas as pd import xarray as xr @@ -12,8 +14,6 @@ from scripts.enums.HeatSystemType import HeatSystemType -import sys - sys.path.append("..") From ddf0da2687a541c9147c9293744b706352cc69fd Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Sun, 4 Aug 2024 16:01:51 +0200 Subject: [PATCH 135/344] base_network: bugfix - bring voronoi cells in correct order --- scripts/base_network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/base_network.py b/scripts/base_network.py index 118e7dba3..8172b332a 100644 --- a/scripts/base_network.py +++ b/scripts/base_network.py @@ -808,7 +808,7 @@ def voronoi(points, outline, crs=4326): voronoi = gpd.GeoDataFrame(geometry=voronoi) joined = gpd.sjoin_nearest(pts, voronoi, how="right") - return joined.dissolve(by="Bus").squeeze() + return joined.dissolve(by="Bus").reindex(points.index).squeeze() def build_bus_shapes(n, country_shapes, offshore_shapes, countries): From acb0a8d7af0c452c44d9823692a583ba281b74fd Mon Sep 17 00:00:00 2001 From: Philipp Glaum Date: Mon, 5 Aug 2024 10:08:04 +0200 Subject: [PATCH 136/344] get methanol to kerosene cost form technology data --- scripts/prepare_sector_network.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 245d6bc4d..898496d0b 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1011,15 +1011,14 @@ def add_methanol_to_kerosene(n, costs): * costs.at[tech, "methanol-input"] ) - # cost data available at https://www.concawe.eu/wp-content/uploads/Rpt_22-17.pdf table 94 + capital_cost = costs.at[tech, "fixed"] / costs.at[tech, "methanol-input"] n.madd( "Link", spatial.methanol.locations, suffix=f" {tech}", carrier=tech, - # capital_cost= , - bus0=spatial.methanol.nodes, + capital_cost=capital_cost, bus1=spatial.oil.kerosene, bus2=spatial.h2.nodes, efficiency=costs.at[tech, "methanol-input"], From a56834e51ebe7bc0ae6805143369396406993b9c Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Mon, 5 Aug 2024 10:32:22 +0200 Subject: [PATCH 137/344] add switch for msw waste --- config/config.default.yaml | 2 + scripts/prepare_sector_network.py | 122 +++++++++++++++++------------- 2 files changed, 71 insertions(+), 53 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 6b906ee89..32c073b48 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -600,6 +600,7 @@ sector: biomass_to_liquid: false electrobiofuels: false biosng: false + municipal_solid_waste: false limit_max_growth: enable: false # allowing 30% larger than max historic growth @@ -627,6 +628,7 @@ sector: max_amount: 1390 # TWh upstream_emissions_factor: .1 #share of solid biomass CO2 emissions at full combustion + # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#industry industry: St_primary_fraction: diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 8fdbac6ed..fdded30a3 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2266,8 +2266,38 @@ def add_biomass(n, costs): n.add("Carrier", "biogas") n.add("Carrier", "solid biomass") - n.add("Carrier", "municipal solid waste") - + + + if (options["municipal_solid_waste"] and not options["industry"] + and cf_industry["waste_to_energy"] or cf_industry["waste_to_energy_cc"]): + logger.warning("Flag municipal_solid_waste can be only used with industry " + "sector waste to energy." + "Setting municipal_solid_waste=False.") + options["municipal_solid_waste"] = False + + if options["municipal_solid_waste"]: + + n.add("Carrier", "municipal solid waste") + + n.madd( + "Bus", + spatial.msw.nodes, + location=spatial.msw.locations, + carrier="municipal solid waste", + ) + + e_max_pu = pd.Series([1] * (len(n.snapshots) - 1) + [0], index=n.snapshots) + n.madd( + "Store", + spatial.msw.nodes, + bus=spatial.msw.nodes, + carrier="municipal solid waste", + e_nom=msw_biomass_potentials_spatial, + marginal_cost=0, # costs.at["municipal solid waste", "fuel"], + e_max_pu=e_max_pu, + e_initial=msw_biomass_potentials_spatial, + ) + n.madd( "Bus", spatial.gas.biogas, @@ -2284,12 +2314,6 @@ def add_biomass(n, costs): unit="MWh_LHV", ) - n.madd( - "Bus", - spatial.msw.nodes, - location=spatial.msw.locations, - carrier="municipal solid waste", - ) n.madd( "Store", @@ -2358,18 +2382,8 @@ def add_biomass(n, costs): * costs.at["solid biomass", "CO2 intensity"], p_nom_extendable=True, ) - - e_max_pu = pd.Series([1] * (len(n.snapshots) - 1) + [0], index=n.snapshots) - n.madd( - "Store", - spatial.msw.nodes, - bus=spatial.msw.nodes, - carrier="municipal solid waste", - e_nom=msw_biomass_potentials_spatial, - marginal_cost=0, # costs.at["municipal solid waste", "fuel"], - e_max_pu=e_max_pu, - e_initial=msw_biomass_potentials_spatial, - ) + + n.madd( "Link", @@ -2441,18 +2455,19 @@ def add_biomass(n, costs): marginal_cost=biomass_transport.costs * biomass_transport.length.values, carrier="solid biomass transport", ) - - n.madd( - "Link", - biomass_transport.index, - bus0=biomass_transport.bus0 + " municipal solid waste", - bus1=biomass_transport.bus1 + " municipal solid waste", - p_nom_extendable=False, - p_nom=5e4, - length=biomass_transport.length.values, - marginal_cost=biomass_transport.costs * biomass_transport.length.values, - carrier="municipal solid waste transport", - ) + + if options["municipal_solid_waste"]: + n.madd( + "Link", + biomass_transport.index, + bus0=biomass_transport.bus0 + " municipal solid waste", + bus1=biomass_transport.bus1 + " municipal solid waste", + p_nom_extendable=False, + p_nom=5e4, + length=biomass_transport.length.values, + marginal_cost=biomass_transport.costs * biomass_transport.length.values, + carrier="municipal solid waste transport", + ) elif options["biomass_spatial"]: # add artificial biomass generators at nodes which include transport costs @@ -2482,25 +2497,26 @@ def add_biomass(n, costs): constant=biomass_potentials["solid biomass"].sum(), type="operational_limit", ) - - # Add municipal solid waste - n.madd( - "Generator", - spatial.msw.nodes, - bus=spatial.msw.nodes, - carrier="municipal solid waste", - p_nom=10000, - marginal_cost=0 # costs.at["municipal solid waste", "fuel"] - + bus_transport_costs * average_distance, - ) - n.add( - "GlobalConstraint", - "msw limit", - carrier_attribute="municipal solid waste", - sense="<=", - constant=biomass_potentials["municipal solid waste"].sum(), - type="operational_limit", - ) + + if options["municipal_solid_waste"]: + # Add municipal solid waste + n.madd( + "Generator", + spatial.msw.nodes, + bus=spatial.msw.nodes, + carrier="municipal solid waste", + p_nom=10000, + marginal_cost=0 # costs.at["municipal solid waste", "fuel"] + + bus_transport_costs * average_distance, + ) + n.add( + "GlobalConstraint", + "msw limit", + carrier_attribute="municipal solid waste", + sense="<=", + constant=biomass_potentials["municipal solid waste"].sum(), + type="operational_limit", + ) # AC buses with district heating urban_central = n.buses.index[n.buses.carrier == "urban central heat"] @@ -3207,7 +3223,7 @@ def add_industry(n, costs): efficiency3=process_co2_per_naphtha, ) - if options.get("biomass", True): + if options.get("biomass", True) and options["municipal_solid_waste"]: n.madd( "Link", spatial.msw.locations, @@ -4110,7 +4126,7 @@ def add_enhanced_geothermal(n, egs_potentials, egs_overlap, costs): "prepare_sector_network", simpl="", opts="", - clusters="1", + clusters="37", ll="vopt", sector_opts="", planning_horizons="2050", From 69f51ec7d6e30ccc63d900aa61952c355fba1480 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Mon, 5 Aug 2024 10:45:57 +0200 Subject: [PATCH 138/344] add release notes and doc for new config settings --- doc/configtables/sector.csv | 323 ++++++++++++++++++------------------ doc/release_notes.rst | 4 +- 2 files changed, 165 insertions(+), 162 deletions(-) diff --git a/doc/configtables/sector.csv b/doc/configtables/sector.csv index c96aa629c..9cf5a5041 100644 --- a/doc/configtables/sector.csv +++ b/doc/configtables/sector.csv @@ -1,161 +1,162 @@ -,Unit,Values,Description -transport,--,"{true, false}",Flag to include transport sector. -heating,--,"{true, false}",Flag to include heating sector. -biomass,--,"{true, false}",Flag to include biomass sector. -industry,--,"{true, false}",Flag to include industry sector. -agriculture,--,"{true, false}",Flag to include agriculture sector. -fossil_fuels,--,"{true, false}","Flag to include imports of fossil fuels ( [""coal"", ""gas"", ""oil"", ""lignite""])" -district_heating,--,,`prepare_sector_network.py `_ --- potential,--,float,maximum fraction of urban demand which can be supplied by district heating --- progress,--,Dictionary with planning horizons as keys., Increase of today's district heating demand to potential maximum district heating share. Progress = 0 means today's district heating share. Progress = 1 means maximum fraction of urban demand is supplied by district heating --- district_heating_loss,--,float,Share increase in district heat demand in urban central due to heat losses -cluster_heat_buses,--,"{true, false}",Cluster residential and service heat buses in `prepare_sector_network.py `_ to one to save memory. -,,, -bev_dsm_restriction _value,--,float,Adds a lower state of charge (SOC) limit for battery electric vehicles (BEV) to manage its own energy demand (DSM). Located in `build_transport_demand.py `_. Set to 0 for no restriction on BEV DSM -bev_dsm_restriction _time,--,float,Time at which SOC of BEV has to be dsm_restriction_value -transport_heating _deadband_upper,°C,float,"The maximum temperature in the vehicle. At higher temperatures, the energy required for cooling in the vehicle increases." -transport_heating _deadband_lower,°C,float,"The minimum temperature in the vehicle. At lower temperatures, the energy required for heating in the vehicle increases." -,,, -ICE_lower_degree_factor,--,float,Share increase in energy demand in internal combustion engine (ICE) for each degree difference between the cold environment and the minimum temperature. -ICE_upper_degree_factor,--,float,Share increase in energy demand in internal combustion engine (ICE) for each degree difference between the hot environment and the maximum temperature. -EV_lower_degree_factor,--,float,Share increase in energy demand in electric vehicles (EV) for each degree difference between the cold environment and the minimum temperature. -EV_upper_degree_factor,--,float,Share increase in energy demand in electric vehicles (EV) for each degree difference between the hot environment and the maximum temperature. -bev_dsm,--,"{true, false}",Add the option for battery electric vehicles (BEV) to participate in demand-side management (DSM) -,,, -bev_availability,--,float,The share for battery electric vehicles (BEV) that are able to do demand side management (DSM) -bev_energy,--,float,The average size of battery electric vehicles (BEV) in MWh -bev_charge_efficiency,--,float,Battery electric vehicles (BEV) charge and discharge efficiency -bev_charge_rate,MWh,float,The power consumption for one electric vehicle (EV) in MWh. Value derived from 3-phase charger with 11 kW. -bev_avail_max,--,float,The maximum share plugged-in availability for passenger electric vehicles. -bev_avail_mean,--,float,The average share plugged-in availability for passenger electric vehicles. -v2g,--,"{true, false}",Allows feed-in to grid from EV battery -land_transport_fuel_cell _share,--,Dictionary with planning horizons as keys.,The share of vehicles that uses fuel cells in a given year -land_transport_electric _share,--,Dictionary with planning horizons as keys.,The share of vehicles that uses electric vehicles (EV) in a given year -land_transport_ice _share,--,Dictionary with planning horizons as keys.,The share of vehicles that uses internal combustion engines (ICE) in a given year. What is not EV or FCEV is oil-fuelled ICE. -transport_electric_efficiency,MWh/100km,float,The conversion efficiencies of electric vehicles in transport -transport_fuel_cell_efficiency,MWh/100km,float,The H2 conversion efficiencies of fuel cells in transport -transport_ice_efficiency,MWh/100km,float,The oil conversion efficiencies of internal combustion engine (ICE) in transport -agriculture_machinery _electric_share,--,float,The share for agricultural machinery that uses electricity -agriculture_machinery _oil_share,--,float,The share for agricultural machinery that uses oil -agriculture_machinery _fuel_efficiency,--,float,The efficiency of electric-powered machinery in the conversion of electricity to meet agricultural needs. -agriculture_machinery _electric_efficiency,--,float,The efficiency of oil-powered machinery in the conversion of oil to meet agricultural needs. -Mwh_MeOH_per_MWh_H2,LHV,float,"The energy amount of the produced methanol per energy amount of hydrogen. From `DECHEMA (2017) `_, page 64." -MWh_MeOH_per_tCO2,LHV,float,"The energy amount of the produced methanol per ton of CO2. From `DECHEMA (2017) `_, page 66." -MWh_MeOH_per_MWh_e,LHV,float,"The energy amount of the produced methanol per energy amount of electricity. From `DECHEMA (2017) `_, page 64." -shipping_hydrogen _liquefaction,--,"{true, false}",Whether to include liquefaction costs for hydrogen demand in shipping. -,,, -shipping_hydrogen_share,--,Dictionary with planning horizons as keys.,The share of ships powered by hydrogen in a given year -shipping_methanol_share,--,Dictionary with planning horizons as keys.,The share of ships powered by methanol in a given year -shipping_oil_share,--,Dictionary with planning horizons as keys.,The share of ships powered by oil in a given year -shipping_methanol _efficiency,--,float,The efficiency of methanol-powered ships in the conversion of methanol to meet shipping needs (propulsion). The efficiency increase from oil can be 10-15% higher according to the `IEA `_ -,,, -shipping_oil_efficiency,--,float,The efficiency of oil-powered ships in the conversion of oil to meet shipping needs (propulsion). Base value derived from 2011 -aviation_demand_factor,--,float,The proportion of demand for aviation compared to today's consumption -HVC_demand_factor,--,float,The proportion of demand for high-value chemicals compared to today's consumption -,,, -time_dep_hp_cop,--,"{true, false}",Consider the time dependent coefficient of performance (COP) of the heat pump -heat_pump_sink_T,°C,float,The temperature heat sink used in heat pumps based on DTU / large area radiators. The value is conservatively high to cover hot water and space heating in poorly-insulated buildings -reduce_space_heat _exogenously,--,"{true, false}",Influence on space heating demand by a certain factor (applied before losses in district heating). -reduce_space_heat _exogenously_factor,--,Dictionary with planning horizons as keys.,"A positive factor can mean renovation or demolition of a building. If the factor is negative, it can mean an increase in floor area, increased thermal comfort, population growth. The default factors are determined by the `Eurocalc Homes and buildings decarbonization scenario `_" -retrofitting,,, --- retro_endogen,--,"{true, false}",Add retrofitting as an endogenous system which co-optimise space heat savings. --- cost_factor,--,float,Weight costs for building renovation --- interest_rate,--,float,The interest rate for investment in building components --- annualise_cost,--,"{true, false}",Annualise the investment costs of retrofitting --- tax_weighting,--,"{true, false}",Weight the costs of retrofitting depending on taxes in countries --- construction_index,--,"{true, false}",Weight the costs of retrofitting depending on labour/material costs per country -tes,--,"{true, false}",Add option for storing thermal energy in large water pits associated with district heating systems and individual thermal energy storage (TES) -tes_tau,,,The time constant used to calculate the decay of thermal energy in thermal energy storage (TES): 1- :math:`e^{-1/24τ}`. --- decentral,days,float,The time constant in decentralized thermal energy storage (TES) --- central,days,float,The time constant in centralized thermal energy storage (TES) -boilers,--,"{true, false}",Add option for transforming gas into heat using gas boilers -resistive_heaters,--,"{true, false}",Add option for transforming electricity into heat using resistive heaters (independently from gas boilers) -oil_boilers,--,"{true, false}",Add option for transforming oil into heat using boilers -biomass_boiler,--,"{true, false}",Add option for transforming biomass into heat using boilers -overdimension_individual_heating,--,"float",Add option for overdimensioning individual heating systems by a certain factor. This allows them to cover heat demand peaks e.g. 10% higher than those in the data with a setting of 1.1. -chp,--,"{true, false}",Add option for using Combined Heat and Power (CHP) -micro_chp,--,"{true, false}",Add option for using Combined Heat and Power (CHP) for decentral areas. -solar_thermal,--,"{true, false}",Add option for using solar thermal to generate heat. -solar_cf_correction,--,float,The correction factor for the value provided by the solar thermal profile calculations -marginal_cost_storage,currency/MWh ,float,The marginal cost of discharging batteries in distributed grids -methanation,--,"{true, false}",Add option for transforming hydrogen and CO2 into methane using methanation. -coal_cc,--,"{true, false}",Add option for coal CHPs with carbon capture -dac,--,"{true, false}",Add option for Direct Air Capture (DAC) -co2_vent,--,"{true, false}",Add option for vent out CO2 from storages to the atmosphere. -allam_cycle,--,"{true, false}",Add option to include `Allam cycle gas power plants `_ -hydrogen_fuel_cell,--,"{true, false}",Add option to include hydrogen fuel cell for re-electrification. Assuming OCGT technology costs -hydrogen_turbine,--,"{true, false}",Add option to include hydrogen turbine for re-electrification. Assuming OCGT technology costs -SMR,--,"{true, false}",Add option for transforming natural gas into hydrogen and CO2 using Steam Methane Reforming (SMR) -SMR CC,--,"{true, false}",Add option for transforming natural gas into hydrogen and CO2 using Steam Methane Reforming (SMR) and Carbon Capture (CC) -regional_methanol_demand,--,"{true, false}",Spatially resolve methanol demand. Set to true if regional CO2 constraints needed. -regional_oil_demand,--,"{true, false}",Spatially resolve oil demand. Set to true if regional CO2 constraints needed. -regional_co2 _sequestration_potential,,, --- enable,--,"{true, false}",Add option for regionally-resolved geological carbon dioxide sequestration potentials based on `CO2StoP `_. --- attribute,--,string or list,Name (or list of names) of the attribute(s) for the sequestration potential --- include_onshore,--,"{true, false}",Add options for including onshore sequestration potentials --- min_size,Gt ,float,Any sites with lower potential than this value will be excluded --- max_size,Gt ,float,The maximum sequestration potential for any one site. --- years_of_storage,years,float,The years until potential exhausted at optimised annual rate -co2_sequestration_potential,MtCO2/a,float,The potential of sequestering CO2 in Europe per year -co2_sequestration_cost,currency/tCO2,float,The cost of sequestering a ton of CO2 -co2_sequestration_lifetime,years,int,The lifetime of a CO2 sequestration site -co2_spatial,--,"{true, false}","Add option to spatially resolve carrier representing stored carbon dioxide. This allows for more detailed modelling of CCUTS, e.g. regarding the capturing of industrial process emissions, usage as feedstock for electrofuels, transport of carbon dioxide, and geological sequestration sites." -,,, -co2network,--,"{true, false}",Add option for planning a new carbon dioxide transmission network -co2_network_cost_factor,p.u.,float,The cost factor for the capital cost of the carbon dioxide transmission network -,,, -cc_fraction,--,float,The default fraction of CO2 captured with post-combustion capture -hydrogen_underground _storage,--,"{true, false}",Add options for storing hydrogen underground. Storage potential depends regionally. -hydrogen_underground _storage_locations,,"{onshore, nearshore, offshore}","The location where hydrogen underground storage can be located. Onshore, nearshore, offshore means it must be located more than 50 km away from the sea, within 50 km of the sea, or within the sea itself respectively." -,,, -ammonia,--,"{true, false, regional}","Add ammonia as a carrrier. It can be either true (copperplated NH3), false (no NH3 carrier) or ""regional"" (regionalised NH3 without network)" -min_part_load_fischer _tropsch,per unit of p_nom ,float,The minimum unit dispatch (``p_min_pu``) for the Fischer-Tropsch process -min_part_load _methanolisation,per unit of p_nom ,float,The minimum unit dispatch (``p_min_pu``) for the methanolisation process -,,, -use_fischer_tropsch _waste_heat,--,"{true, false}",Add option for using waste heat of Fischer Tropsch in district heating networks -use_fuel_cell_waste_heat,--,"{true, false}",Add option for using waste heat of fuel cells in district heating networks -use_electrolysis_waste _heat,--,"{true, false}",Add option for using waste heat of electrolysis in district heating networks -electricity_transmission _grid,--,"{true, false}",Switch for enabling/disabling the electricity transmission grid. -electricity_distribution _grid,--,"{true, false}",Add a simplified representation of the exchange capacity between transmission and distribution grid level through a link. -electricity_distribution _grid_cost_factor,,,Multiplies the investment cost of the electricity distribution grid -,,, -electricity_grid _connection,--,"{true, false}",Add the cost of electricity grid connection for onshore wind and solar -transmission_efficiency,,,Section to specify transmission losses or compression energy demands of bidirectional links. Splits them into two capacity-linked unidirectional links. --- {carrier},--,str,The carrier of the link. --- -- efficiency_static,p.u.,float,Length-independent transmission efficiency. --- -- efficiency_per_1000km,p.u. per 1000 km,float,Length-dependent transmission efficiency ($\eta^{\text{length}}$) --- -- compression_per_1000km,p.u. per 1000 km,float,Length-dependent electricity demand for compression ($\eta \cdot \text{length}$) implemented as multi-link to local electricity bus. -H2_network,--,"{true, false}",Add option for new hydrogen pipelines -gas_network,--,"{true, false}","Add existing natural gas infrastructure, incl. LNG terminals, production and entry-points. The existing gas network is added with a lossless transport model. A length-weighted `k-edge augmentation algorithm `_ can be run to add new candidate gas pipelines such that all regions of the model can be connected to the gas network. When activated, all the gas demands are regionally disaggregated as well." -H2_retrofit,--,"{true, false}",Add option for retrofiting existing pipelines to transport hydrogen. -H2_retrofit_capacity _per_CH4,--,float,"The ratio for H2 capacity per original CH4 capacity of retrofitted pipelines. The `European Hydrogen Backbone (April, 2020) p.15 `_ 60% of original natural gas capacity could be used in cost-optimal case as H2 capacity." -gas_network_connectivity _upgrade ,--,float,The number of desired edge connectivity (k) in the length-weighted `k-edge augmentation algorithm `_ used for the gas network -gas_distribution_grid,--,"{true, false}",Add a gas distribution grid -gas_distribution_grid _cost_factor,,,Multiplier for the investment cost of the gas distribution grid -,,, -biomass_spatial,--,"{true, false}",Add option for resolving biomass demand regionally -biomass_transport,--,"{true, false}",Add option for transporting solid biomass between nodes -biogas_upgrading_cc,--,"{true, false}",Add option to capture CO2 from biomass upgrading -conventional_generation,,,Add a more detailed description of conventional carriers. Any power generation requires the consumption of fuel from nodes representing that fuel. -biomass_to_liquid,--,"{true, false}",Add option for transforming solid biomass into liquid fuel with the same properties as oil -biosng,--,"{true, false}",Add option for transforming solid biomass into synthesis gas with the same properties as natural gas -limit_max_growth,,, --- enable,--,"{true, false}",Add option to limit the maximum growth of a carrier --- factor,p.u.,float,The maximum growth factor of a carrier (e.g. 1.3 allows 30% larger than max historic growth) --- max_growth,,, --- -- {carrier},GW,float,The historic maximum growth of a carrier --- max_relative_growth,,, --- -- {carrier},p.u.,float,The historic maximum relative growth of a carrier -,,, -enhanced_geothermal,,, --- enable,--,"{true, false}",Add option to include Enhanced Geothermal Systems --- flexible,--,"{true, false}",Add option for flexible operation (see Ricks et al. 2024) --- max_hours,--,int,The maximum hours the reservoir can be charged under flexible operation --- max_boost,--,float,The maximum boost in power output under flexible operation --- var_cf,--,"{true, false}",Add option for variable capacity factor (see Ricks et al. 2024) --- sustainability_factor,--,float,Share of sourced heat that is replenished by the earth's core (see details in `build_egs_potentials.py `_) -solid_biomass_import,,, --- enable,--,"{true, false}",Add option to include solid biomass imports --- price,currency/MWh,float,Price for importing solid biomass --- max_amount,Twh,float,Maximum solid biomass import potential --- upstream_emissions_factor,p.u.,float,Upstream emissions of solid biomass imports +,Unit,Values,Description +transport,--,"{true, false}",Flag to include transport sector. +heating,--,"{true, false}",Flag to include heating sector. +biomass,--,"{true, false}",Flag to include biomass sector. +industry,--,"{true, false}",Flag to include industry sector. +agriculture,--,"{true, false}",Flag to include agriculture sector. +fossil_fuels,--,"{true, false}","Flag to include imports of fossil fuels ( [""coal"", ""gas"", ""oil"", ""lignite""])" +district_heating,--,,`prepare_sector_network.py `_ +-- potential,--,float,maximum fraction of urban demand which can be supplied by district heating +-- progress,--,Dictionary with planning horizons as keys., Increase of today's district heating demand to potential maximum district heating share. Progress = 0 means today's district heating share. Progress = 1 means maximum fraction of urban demand is supplied by district heating +-- district_heating_loss,--,float,Share increase in district heat demand in urban central due to heat losses +cluster_heat_buses,--,"{true, false}",Cluster residential and service heat buses in `prepare_sector_network.py `_ to one to save memory. +,,, +bev_dsm_restriction _value,--,float,Adds a lower state of charge (SOC) limit for battery electric vehicles (BEV) to manage its own energy demand (DSM). Located in `build_transport_demand.py `_. Set to 0 for no restriction on BEV DSM +bev_dsm_restriction _time,--,float,Time at which SOC of BEV has to be dsm_restriction_value +transport_heating _deadband_upper,°C,float,"The maximum temperature in the vehicle. At higher temperatures, the energy required for cooling in the vehicle increases." +transport_heating _deadband_lower,°C,float,"The minimum temperature in the vehicle. At lower temperatures, the energy required for heating in the vehicle increases." +,,, +ICE_lower_degree_factor,--,float,Share increase in energy demand in internal combustion engine (ICE) for each degree difference between the cold environment and the minimum temperature. +ICE_upper_degree_factor,--,float,Share increase in energy demand in internal combustion engine (ICE) for each degree difference between the hot environment and the maximum temperature. +EV_lower_degree_factor,--,float,Share increase in energy demand in electric vehicles (EV) for each degree difference between the cold environment and the minimum temperature. +EV_upper_degree_factor,--,float,Share increase in energy demand in electric vehicles (EV) for each degree difference between the hot environment and the maximum temperature. +bev_dsm,--,"{true, false}",Add the option for battery electric vehicles (BEV) to participate in demand-side management (DSM) +,,, +bev_availability,--,float,The share for battery electric vehicles (BEV) that are able to do demand side management (DSM) +bev_energy,--,float,The average size of battery electric vehicles (BEV) in MWh +bev_charge_efficiency,--,float,Battery electric vehicles (BEV) charge and discharge efficiency +bev_charge_rate,MWh,float,The power consumption for one electric vehicle (EV) in MWh. Value derived from 3-phase charger with 11 kW. +bev_avail_max,--,float,The maximum share plugged-in availability for passenger electric vehicles. +bev_avail_mean,--,float,The average share plugged-in availability for passenger electric vehicles. +v2g,--,"{true, false}",Allows feed-in to grid from EV battery +land_transport_fuel_cell _share,--,Dictionary with planning horizons as keys.,The share of vehicles that uses fuel cells in a given year +land_transport_electric _share,--,Dictionary with planning horizons as keys.,The share of vehicles that uses electric vehicles (EV) in a given year +land_transport_ice _share,--,Dictionary with planning horizons as keys.,The share of vehicles that uses internal combustion engines (ICE) in a given year. What is not EV or FCEV is oil-fuelled ICE. +transport_electric_efficiency,MWh/100km,float,The conversion efficiencies of electric vehicles in transport +transport_fuel_cell_efficiency,MWh/100km,float,The H2 conversion efficiencies of fuel cells in transport +transport_ice_efficiency,MWh/100km,float,The oil conversion efficiencies of internal combustion engine (ICE) in transport +agriculture_machinery _electric_share,--,float,The share for agricultural machinery that uses electricity +agriculture_machinery _oil_share,--,float,The share for agricultural machinery that uses oil +agriculture_machinery _fuel_efficiency,--,float,The efficiency of electric-powered machinery in the conversion of electricity to meet agricultural needs. +agriculture_machinery _electric_efficiency,--,float,The efficiency of oil-powered machinery in the conversion of oil to meet agricultural needs. +Mwh_MeOH_per_MWh_H2,LHV,float,"The energy amount of the produced methanol per energy amount of hydrogen. From `DECHEMA (2017) `_, page 64." +MWh_MeOH_per_tCO2,LHV,float,"The energy amount of the produced methanol per ton of CO2. From `DECHEMA (2017) `_, page 66." +MWh_MeOH_per_MWh_e,LHV,float,"The energy amount of the produced methanol per energy amount of electricity. From `DECHEMA (2017) `_, page 64." +shipping_hydrogen _liquefaction,--,"{true, false}",Whether to include liquefaction costs for hydrogen demand in shipping. +,,, +shipping_hydrogen_share,--,Dictionary with planning horizons as keys.,The share of ships powered by hydrogen in a given year +shipping_methanol_share,--,Dictionary with planning horizons as keys.,The share of ships powered by methanol in a given year +shipping_oil_share,--,Dictionary with planning horizons as keys.,The share of ships powered by oil in a given year +shipping_methanol _efficiency,--,float,The efficiency of methanol-powered ships in the conversion of methanol to meet shipping needs (propulsion). The efficiency increase from oil can be 10-15% higher according to the `IEA `_ +,,, +shipping_oil_efficiency,--,float,The efficiency of oil-powered ships in the conversion of oil to meet shipping needs (propulsion). Base value derived from 2011 +aviation_demand_factor,--,float,The proportion of demand for aviation compared to today's consumption +HVC_demand_factor,--,float,The proportion of demand for high-value chemicals compared to today's consumption +,,, +time_dep_hp_cop,--,"{true, false}",Consider the time dependent coefficient of performance (COP) of the heat pump +heat_pump_sink_T,°C,float,The temperature heat sink used in heat pumps based on DTU / large area radiators. The value is conservatively high to cover hot water and space heating in poorly-insulated buildings +reduce_space_heat _exogenously,--,"{true, false}",Influence on space heating demand by a certain factor (applied before losses in district heating). +reduce_space_heat _exogenously_factor,--,Dictionary with planning horizons as keys.,"A positive factor can mean renovation or demolition of a building. If the factor is negative, it can mean an increase in floor area, increased thermal comfort, population growth. The default factors are determined by the `Eurocalc Homes and buildings decarbonization scenario `_" +retrofitting,,, +-- retro_endogen,--,"{true, false}",Add retrofitting as an endogenous system which co-optimise space heat savings. +-- cost_factor,--,float,Weight costs for building renovation +-- interest_rate,--,float,The interest rate for investment in building components +-- annualise_cost,--,"{true, false}",Annualise the investment costs of retrofitting +-- tax_weighting,--,"{true, false}",Weight the costs of retrofitting depending on taxes in countries +-- construction_index,--,"{true, false}",Weight the costs of retrofitting depending on labour/material costs per country +tes,--,"{true, false}",Add option for storing thermal energy in large water pits associated with district heating systems and individual thermal energy storage (TES) +tes_tau,,,The time constant used to calculate the decay of thermal energy in thermal energy storage (TES): 1- :math:`e^{-1/24τ}`. +-- decentral,days,float,The time constant in decentralized thermal energy storage (TES) +-- central,days,float,The time constant in centralized thermal energy storage (TES) +boilers,--,"{true, false}",Add option for transforming gas into heat using gas boilers +resistive_heaters,--,"{true, false}",Add option for transforming electricity into heat using resistive heaters (independently from gas boilers) +oil_boilers,--,"{true, false}",Add option for transforming oil into heat using boilers +biomass_boiler,--,"{true, false}",Add option for transforming biomass into heat using boilers +overdimension_individual_heating,--,float,Add option for overdimensioning individual heating systems by a certain factor. This allows them to cover heat demand peaks e.g. 10% higher than those in the data with a setting of 1.1. +chp,--,"{true, false}",Add option for using Combined Heat and Power (CHP) +micro_chp,--,"{true, false}",Add option for using Combined Heat and Power (CHP) for decentral areas. +solar_thermal,--,"{true, false}",Add option for using solar thermal to generate heat. +solar_cf_correction,--,float,The correction factor for the value provided by the solar thermal profile calculations +marginal_cost_storage,currency/MWh ,float,The marginal cost of discharging batteries in distributed grids +methanation,--,"{true, false}",Add option for transforming hydrogen and CO2 into methane using methanation. +coal_cc,--,"{true, false}",Add option for coal CHPs with carbon capture +dac,--,"{true, false}",Add option for Direct Air Capture (DAC) +co2_vent,--,"{true, false}",Add option for vent out CO2 from storages to the atmosphere. +allam_cycle,--,"{true, false}",Add option to include `Allam cycle gas power plants `_ +hydrogen_fuel_cell,--,"{true, false}",Add option to include hydrogen fuel cell for re-electrification. Assuming OCGT technology costs +hydrogen_turbine,--,"{true, false}",Add option to include hydrogen turbine for re-electrification. Assuming OCGT technology costs +SMR,--,"{true, false}",Add option for transforming natural gas into hydrogen and CO2 using Steam Methane Reforming (SMR) +SMR CC,--,"{true, false}",Add option for transforming natural gas into hydrogen and CO2 using Steam Methane Reforming (SMR) and Carbon Capture (CC) +regional_methanol_demand,--,"{true, false}",Spatially resolve methanol demand. Set to true if regional CO2 constraints needed. +regional_oil_demand,--,"{true, false}",Spatially resolve oil demand. Set to true if regional CO2 constraints needed. +regional_co2 _sequestration_potential,,, +-- enable,--,"{true, false}",Add option for regionally-resolved geological carbon dioxide sequestration potentials based on `CO2StoP `_. +-- attribute,--,string or list,Name (or list of names) of the attribute(s) for the sequestration potential +-- include_onshore,--,"{true, false}",Add options for including onshore sequestration potentials +-- min_size,Gt ,float,Any sites with lower potential than this value will be excluded +-- max_size,Gt ,float,The maximum sequestration potential for any one site. +-- years_of_storage,years,float,The years until potential exhausted at optimised annual rate +co2_sequestration_potential,MtCO2/a,float,The potential of sequestering CO2 in Europe per year +co2_sequestration_cost,currency/tCO2,float,The cost of sequestering a ton of CO2 +co2_sequestration_lifetime,years,int,The lifetime of a CO2 sequestration site +co2_spatial,--,"{true, false}","Add option to spatially resolve carrier representing stored carbon dioxide. This allows for more detailed modelling of CCUTS, e.g. regarding the capturing of industrial process emissions, usage as feedstock for electrofuels, transport of carbon dioxide, and geological sequestration sites." +,,, +co2network,--,"{true, false}",Add option for planning a new carbon dioxide transmission network +co2_network_cost_factor,p.u.,float,The cost factor for the capital cost of the carbon dioxide transmission network +,,, +cc_fraction,--,float,The default fraction of CO2 captured with post-combustion capture +hydrogen_underground _storage,--,"{true, false}",Add options for storing hydrogen underground. Storage potential depends regionally. +hydrogen_underground _storage_locations,,"{onshore, nearshore, offshore}","The location where hydrogen underground storage can be located. Onshore, nearshore, offshore means it must be located more than 50 km away from the sea, within 50 km of the sea, or within the sea itself respectively." +,,, +ammonia,--,"{true, false, regional}","Add ammonia as a carrrier. It can be either true (copperplated NH3), false (no NH3 carrier) or ""regional"" (regionalised NH3 without network)" +min_part_load_fischer _tropsch,per unit of p_nom ,float,The minimum unit dispatch (``p_min_pu``) for the Fischer-Tropsch process +min_part_load _methanolisation,per unit of p_nom ,float,The minimum unit dispatch (``p_min_pu``) for the methanolisation process +,,, +use_fischer_tropsch _waste_heat,--,"{true, false}",Add option for using waste heat of Fischer Tropsch in district heating networks +use_fuel_cell_waste_heat,--,"{true, false}",Add option for using waste heat of fuel cells in district heating networks +use_electrolysis_waste _heat,--,"{true, false}",Add option for using waste heat of electrolysis in district heating networks +electricity_transmission _grid,--,"{true, false}",Switch for enabling/disabling the electricity transmission grid. +electricity_distribution _grid,--,"{true, false}",Add a simplified representation of the exchange capacity between transmission and distribution grid level through a link. +electricity_distribution _grid_cost_factor,,,Multiplies the investment cost of the electricity distribution grid +,,, +electricity_grid _connection,--,"{true, false}",Add the cost of electricity grid connection for onshore wind and solar +transmission_efficiency,,,Section to specify transmission losses or compression energy demands of bidirectional links. Splits them into two capacity-linked unidirectional links. +-- {carrier},--,str,The carrier of the link. +-- -- efficiency_static,p.u.,float,Length-independent transmission efficiency. +-- -- efficiency_per_1000km,p.u. per 1000 km,float,Length-dependent transmission efficiency ($\eta^{\text{length}}$) +-- -- compression_per_1000km,p.u. per 1000 km,float,Length-dependent electricity demand for compression ($\eta \cdot \text{length}$) implemented as multi-link to local electricity bus. +H2_network,--,"{true, false}",Add option for new hydrogen pipelines +gas_network,--,"{true, false}","Add existing natural gas infrastructure, incl. LNG terminals, production and entry-points. The existing gas network is added with a lossless transport model. A length-weighted `k-edge augmentation algorithm `_ can be run to add new candidate gas pipelines such that all regions of the model can be connected to the gas network. When activated, all the gas demands are regionally disaggregated as well." +H2_retrofit,--,"{true, false}",Add option for retrofiting existing pipelines to transport hydrogen. +H2_retrofit_capacity _per_CH4,--,float,"The ratio for H2 capacity per original CH4 capacity of retrofitted pipelines. The `European Hydrogen Backbone (April, 2020) p.15 `_ 60% of original natural gas capacity could be used in cost-optimal case as H2 capacity." +gas_network_connectivity _upgrade ,--,float,The number of desired edge connectivity (k) in the length-weighted `k-edge augmentation algorithm `_ used for the gas network +gas_distribution_grid,--,"{true, false}",Add a gas distribution grid +gas_distribution_grid _cost_factor,,,Multiplier for the investment cost of the gas distribution grid +,,, +biomass_spatial,--,"{true, false}",Add option for resolving biomass demand regionally +biomass_transport,--,"{true, false}",Add option for transporting solid biomass between nodes +biogas_upgrading_cc,--,"{true, false}",Add option to capture CO2 from biomass upgrading +conventional_generation,,,Add a more detailed description of conventional carriers. Any power generation requires the consumption of fuel from nodes representing that fuel. +biomass_to_liquid,--,"{true, false}",Add option for transforming solid biomass into liquid fuel with the same properties as oil +biosng,--,"{true, false}",Add option for transforming solid biomass into synthesis gas with the same properties as natural gas +municipal_solid_waste,--,"{true, false}",Add option for municipal solid waste +limit_max_growth,,, +-- enable,--,"{true, false}",Add option to limit the maximum growth of a carrier +-- factor,p.u.,float,The maximum growth factor of a carrier (e.g. 1.3 allows 30% larger than max historic growth) +-- max_growth,,, +-- -- {carrier},GW,float,The historic maximum growth of a carrier +-- max_relative_growth,,, +-- -- {carrier},p.u.,float,The historic maximum relative growth of a carrier +,,, +enhanced_geothermal,,, +-- enable,--,"{true, false}",Add option to include Enhanced Geothermal Systems +-- flexible,--,"{true, false}",Add option for flexible operation (see Ricks et al. 2024) +-- max_hours,--,int,The maximum hours the reservoir can be charged under flexible operation +-- max_boost,--,float,The maximum boost in power output under flexible operation +-- var_cf,--,"{true, false}",Add option for variable capacity factor (see Ricks et al. 2024) +-- sustainability_factor,--,float,Share of sourced heat that is replenished by the earth's core (see details in `build_egs_potentials.py `_) +solid_biomass_import,,, +-- enable,--,"{true, false}",Add option to include solid biomass imports +-- price,currency/MWh,float,Price for importing solid biomass +-- max_amount,Twh,float,Maximum solid biomass import potential +-- upstream_emissions_factor,p.u.,float,Upstream emissions of solid biomass imports diff --git a/doc/release_notes.rst b/doc/release_notes.rst index d2453bb11..355e516a8 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -10,9 +10,11 @@ Release Notes Upcoming Release ================ +* split solid biomass potentials into solid biomass and municipal solid waste. Add option to use municipal solid waste. This option is only activated in combination with the flag ``waste_to_energy`` + * Add option to import solid biomass -* Add option to produce electrobiofuels (flag ``electrobiofuels`) from solid biomass and hydrogen, as a combination of BtL and Fischer-Tropsch to make more use of the biogenic carbon +* Add option to produce electrobiofuels (flag ``electrobiofuels``) from solid biomass and hydrogen, as a combination of BtL and Fischer-Tropsch to make more use of the biogenic carbon * Add flag ``sector: fossil_fuels`` in config to remove the option of importing fossil fuels From e0b5fe9048f6dd597fdb52b5108405f6509b9dde Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2024 08:46:28 +0000 Subject: [PATCH 139/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/prepare_sector_network.py | 36 ++++++++++++++++--------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index fdded30a3..081ada586 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2266,26 +2266,31 @@ def add_biomass(n, costs): n.add("Carrier", "biogas") n.add("Carrier", "solid biomass") - - - if (options["municipal_solid_waste"] and not options["industry"] - and cf_industry["waste_to_energy"] or cf_industry["waste_to_energy_cc"]): - logger.warning("Flag municipal_solid_waste can be only used with industry " - "sector waste to energy." - "Setting municipal_solid_waste=False.") + + if ( + options["municipal_solid_waste"] + and not options["industry"] + and cf_industry["waste_to_energy"] + or cf_industry["waste_to_energy_cc"] + ): + logger.warning( + "Flag municipal_solid_waste can be only used with industry " + "sector waste to energy." + "Setting municipal_solid_waste=False." + ) options["municipal_solid_waste"] = False - + if options["municipal_solid_waste"]: - + n.add("Carrier", "municipal solid waste") - + n.madd( "Bus", spatial.msw.nodes, location=spatial.msw.locations, carrier="municipal solid waste", ) - + e_max_pu = pd.Series([1] * (len(n.snapshots) - 1) + [0], index=n.snapshots) n.madd( "Store", @@ -2297,7 +2302,7 @@ def add_biomass(n, costs): e_max_pu=e_max_pu, e_initial=msw_biomass_potentials_spatial, ) - + n.madd( "Bus", spatial.gas.biogas, @@ -2314,7 +2319,6 @@ def add_biomass(n, costs): unit="MWh_LHV", ) - n.madd( "Store", spatial.gas.biogas, @@ -2382,8 +2386,6 @@ def add_biomass(n, costs): * costs.at["solid biomass", "CO2 intensity"], p_nom_extendable=True, ) - - n.madd( "Link", @@ -2455,7 +2457,7 @@ def add_biomass(n, costs): marginal_cost=biomass_transport.costs * biomass_transport.length.values, carrier="solid biomass transport", ) - + if options["municipal_solid_waste"]: n.madd( "Link", @@ -2497,7 +2499,7 @@ def add_biomass(n, costs): constant=biomass_potentials["solid biomass"].sum(), type="operational_limit", ) - + if options["municipal_solid_waste"]: # Add municipal solid waste n.madd( From 813e54555a75191ab546aaefe86747325bd56ac8 Mon Sep 17 00:00:00 2001 From: Bobby Xiong <36541459+bobbyxng@users.noreply.github.com> Date: Mon, 5 Aug 2024 11:18:02 +0200 Subject: [PATCH 140/344] Corrected Moyle Interconnector capacity in links_p_nom.csv to 500 MW (#1199) --- data/links_p_nom.csv | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/links_p_nom.csv b/data/links_p_nom.csv index bd7a4c959..56a99e52d 100644 --- a/data/links_p_nom.csv +++ b/data/links_p_nom.csv @@ -5,7 +5,7 @@ Cross-Channel,France - Echingen 50°41′48″N 1°38′21″E / 50.69667 Volgograd-Donbass,Russia - Volzhskaya 48°49′34″N 44°40′20″E / 48.82611°N 44.67222°E,Ukraine - Mikhailovskaya 48°39′13″N 38°33′56″E / 48.65361°N 38.56556°E,475(0/475),400,750.0,1965,Merc/Thyr,Shut down in 2014,[1],44.672222222222224,48.82611111111111,38.565555555555555,48.65361111111111 Konti-Skan 1,Denmark - Vester Hassing 57°3′46″N 10°5′24″E / 57.06278°N 10.09000°E,Sweden - Stenkullen 57°48′15″N 12°19′13″E / 57.80417°N 12.32028°E,176(87/89),250,250.0,1965,Merc,Replaced in August 2006 by modern converters using thyristors,[1],10.09,57.062777777777775,12.320277777777777,57.80416666666667 SACOI 1a,Italy - Suvereto 43°3′10″N 10°41′42″E / 43.05278°N 10.69500°E ( before 1992: Italy - San Dalmazio 43°15′43″N 10°55′05″E / 43.26194°N 10.91806°E),"France- Lucciana 42°31′40″N 9°26′59″E / 42.52778°N 9.44972°E",483(365/118),200,200.0,1965,Merc,"Replaced in 1986 by Thyr- multiterminal scheme",[1],10.695,43.05277777777778,9.449722222222222,42.52777777777778 -SACOI 1b,"France- Lucciana 42°31′40″N 9°26′59″E / 42.52778°N 9.44972°E", "Codrongianos- Italy 40°39′7″N 8°42′48″E / 40.65194°N 8.71333°E",483(365/118),200,200.0,1965,Merc,"Replaced in 1986 by Thyr- multiterminal scheme",[1],9.449722222222222,42.52777777777778,8.679351,40.65765 +SACOI 1b,"France- Lucciana 42°31′40″N 9°26′59″E / 42.52778°N 9.44972°E","Codrongianos- Italy 40°39′7″N 8°42′48″E / 40.65194°N 8.71333°E",483(365/118),200,200.0,1965,Merc,"Replaced in 1986 by Thyr- multiterminal scheme",[1],9.449722222222222,42.52777777777778,8.679351,40.65765 Kingsnorth,UK - Kingsnorth 51°25′11″N 0°35′46″E / 51.41972°N 0.59611°E,UK - London-Beddington 51°22′23″N 0°7′38″W / 51.37306°N 0.12722°W,85(85/0),266,320.0,1975,Merc,Bipolar scheme Supplier: English Electric Shut down in 1987,[33],0.5961111111111111,51.41972222222222,-0.1272222222222222,51.37305555555555 Skagerrak 1 + 2,Denmark - Tjele 56°28′44″N 9°34′1″E / 56.47889°N 9.56694°E,Norway - Kristiansand 58°15′36″N 7°53′55″E / 58.26000°N 7.89861°E,230(130/100),250,500.0,1977,Thyr,Supplier: STK(Nexans) Control system upgrade by ABB in 2007,[34][35][36],9.566944444444445,56.47888888888889,7.898611111111111,58.26 Gotland 2,Sweden - Västervik 57°43′41″N 16°38′51″E / 57.72806°N 16.64750°E,Sweden - Yigne 57°35′13″N 18°11′44″E / 57.58694°N 18.19556°E,99.5(92.9/6.6),150,130.0,1983,Thyr,Supplier: ABB,,16.6475,57.72805555555556,18.195555555555554,57.58694444444444 @@ -23,7 +23,7 @@ Visby-Nas,Sweden - Nas 57°05′58″N 18°14′27″E / 57.09944°N 18.24 SwePol,Poland - Wierzbięcin 54°30′8″N 16°53′28″E / 54.50222°N 16.89111°E,Sweden - Stärnö 56°9′11″N 14°50′29″E / 56.15306°N 14.84139°E,245(245/0),450,600.0,2000,Thyr,Supplier: ABB,[38],16.891111111111112,54.50222222222222,14.841388888888888,56.153055555555554 Tjæreborg,Denmark - Tjæreborg/Enge 55°26′52″N 8°35′34″E / 55.44778°N 8.59278°E,Denmark - Tjæreborg/Substation 55°28′07″N 8°33′36″E / 55.46861°N 8.56000°E,4.3(4.3/0),9,7.0,2000,IGBT,Interconnection to wind power generating stations,,8.592777777777778,55.44777777777778,8.56,55.46861111111111 Italy-Greece,Greece - Arachthos 39°11′00″N 20°57′48″E / 39.18333°N 20.96333°E,Italy - Galatina 40°9′53″N 18°7′49″E / 40.16472°N 18.13028°E,310(200/110),400,500.0,2001,Thyr,,,20.963333333333335,39.18333333333333,18.130277777777778,40.164722222222224 -Moyle,UK - Auchencrosh 55°04′10″N 4°58′50″W / 55.06944°N 4.98056°W,UK - N. Ireland- Ballycronan More 54°50′34″N 5°46′11″W / 54.84278°N 5.76972°W,63.5(63.5/0),250,2501.0,2001,Thyr,"Supplier: Siemens- Nexans",[39],-4.980555555555556,55.06944444444444,-5.769722222222223,54.842777777777776 +Moyle,UK - Auchencrosh 55°04′10″N 4°58′50″W / 55.06944°N 4.98056°W,UK - N. Ireland- Ballycronan More 54°50′34″N 5°46′11″W / 54.84278°N 5.76972°W,63.5(63.5/0),250,500.0,2001,Thyr,"Supplier: Siemens- Nexans",[39],-4.980555555555556,55.06944444444444,-5.769722222222223,54.842777777777776 HVDC Troll,Norway - Kollsnes 60°33′01″N 4°50′26″E / 60.55028°N 4.84056°E,Norway - Offshore platform Troll A 60°40′00″N 3°40′00″E / 60.66667°N 3.66667°E,70(70/0),60,80.0,2004,IGBT,Power supply for offshore gas compressor Supplier: ABB,[40],4.8405555555555555,60.55027777777778,3.6666666666666665,60.666666666666664 Estlink,Finland - Espoo 60°12′14″N 24°33′06″E / 60.20389°N 24.55167°E,Estonia - Harku 59°23′5″N 24°33′37″E / 59.38472°N 24.56028°E,105(105/0),150,350.0,2006,IGBT,Supplier: ABB,[40],24.551666666666666,60.20388888888889,24.560277777777777,59.38472222222222 NorNed,Netherlands - Eemshaven 53°26′4″N 6°51′57″E / 53.43444°N 6.86583°E,Norway - Feda 58°16′58″N 6°51′55″E / 58.28278°N 6.86528°E,580(580/0),450,700.0,2008,Thyr,"Supplier: ABB- Nexans",[40],6.865833333333334,53.434444444444445,6.865277777777778,58.28277777777778 From 5362c376966bdb7b996040ae6850f9154c634e27 Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Mon, 5 Aug 2024 11:27:52 +0200 Subject: [PATCH 141/344] update myopic mode to heat system declarations --- rules/solve_myopic.smk | 14 +-- scripts/add_existing_baseyear.py | 172 +++++++++++++----------------- scripts/enums/HeatSystem.py | 40 ++++++- scripts/prepare_sector_network.py | 5 +- 4 files changed, 118 insertions(+), 113 deletions(-) diff --git a/rules/solve_myopic.smk b/rules/solve_myopic.smk index 09f25b241..8d7fa2841 100644 --- a/rules/solve_myopic.smk +++ b/rules/solve_myopic.smk @@ -69,6 +69,7 @@ rule add_brownfield: snapshots=config_provider("snapshots"), drop_leap_day=config_provider("enable", "drop_leap_day"), carriers=config_provider("electricity", "renewable_carriers"), + heat_pump_sources=config_provider("sector", "heat_pump_sources"), input: unpack(input_profile_tech_brownfield), simplify_busmap=resources("busmap_elec_s{simpl}.csv"), @@ -77,18 +78,7 @@ rule add_brownfield: + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", network_p=solved_previous_horizon, #solved network at previous time step costs=resources("costs_{planning_horizons}.csv"), - cop_soil_decentral_heating=resources( - "cop_soil_decentral_heating_elec_s{simpl}_{clusters}.nc" - ), - cop_air_decentral_heating=resources( - "cop_air_decentral_heating_elec_s{simpl}_{clusters}.nc" - ), - cop_air_central_heating=resources( - "cop_air_central_heating_elec_s{simpl}_{clusters}.nc" - ), - cop_soil_central_heating=resources( - "cop_soil_central_heating_elec_s{simpl}_{clusters}.nc" - ), + cop_profiles=resources("cop_profiles_elec_s{simpl}_{clusters}.nc"), output: RESULTS + "prenetworks-brownfield/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index 6edc7b7bc..1c6209893 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -438,8 +438,8 @@ def add_heating_capacities_installed_before_baseyear( currently assumed heating capacities split between residential and services proportional to heating load in both 50% capacities in rural buses 50% in urban buses - cop: dict - Dictionary with time-dependent coefficients of performance (COPs) for air and ground heat pumps as values and keys "air decentral", "ground decentral", "air central", "ground central" + cop: xr.DataArray + DataArray with time-dependent coefficients of performance (COPs) heat pumps. Coordinates are heat sources (see config), heat system types (see :file:`scripts/enums/HeatSystemType.py`), nodes and snapshots. time_dep_hp_cop: bool If True, time-dependent (dynamic) COPs are used for heat pumps """ @@ -452,66 +452,65 @@ def add_heating_capacities_installed_before_baseyear( n.buses.location[n.buses.index.str.contains(f"{heat_system} heat")] ) - if (not heat_system.is_central) and options["electricity_distribution_grid"]: + if (not heat_system == HeatSystem.URBAN_CENTRAL) and options["electricity_distribution_grid"]: nodes_elec = nodes + " low voltage" else: nodes_elec = nodes - # Add heat pumps - heat_source = snakemake.params.heat_pump_sources[heat_system.system_type.value] - costs_name = f"{heat_system.system_type} {heat_source}-sourced heat pump" - - efficiency = ( - cop.sel(heat_system=heat_system.system_type.value, heat_source=heat_source, name=nodes) - .to_pandas() - .reindex(index=n.snapshots) - if options["time_dep_hp_cop"] - else costs.at[costs_name, "efficiency"] - ) - - too_large_grouping_years = [gy for gy in grouping_years if gy >= int(baseyear)] - if too_large_grouping_years: - logger.warning( - f"Grouping years >= baseyear are ignored. Dropping {too_large_grouping_years}." + too_large_grouping_years = [gy for gy in grouping_years if gy >= int(baseyear)] + if too_large_grouping_years: + logger.warning( + f"Grouping years >= baseyear are ignored. Dropping {too_large_grouping_years}." + ) + valid_grouping_years = pd.Series( + [ + int(grouping_year) + for grouping_year in grouping_years + if int(grouping_year) + default_lifetime > int(baseyear) + and int(grouping_year) < int(baseyear) + ] ) - valid_grouping_years = pd.Series( - [ - int(grouping_year) - for grouping_year in grouping_years - if int(grouping_year) + default_lifetime > int(baseyear) - and int(grouping_year) < int(baseyear) - ] - ) - assert valid_grouping_years.is_monotonic_increasing + assert valid_grouping_years.is_monotonic_increasing - # get number of years of each interval - _years = valid_grouping_years.diff() - # Fill NA from .diff() with value for the first interval - _years[0] = valid_grouping_years[0] - baseyear + default_lifetime - # Installation is assumed to be linear for the past - ratios = _years / _years.sum() + # get number of years of each interval + _years = valid_grouping_years.diff() + # Fill NA from .diff() with value for the first interval + _years[0] = valid_grouping_years[0] - baseyear + default_lifetime + # Installation is assumed to be linear for the past + ratios = _years / _years.sum() for ratio, grouping_year in zip(ratios, valid_grouping_years): + # Add heat pumps + for heat_source in snakemake.params.heat_pump_sources[heat_system.system_type.value]: + costs_name = heat_system.heat_pump_costs_name(heat_source) + + efficiency = ( + cop.sel(heat_system=heat_system.system_type.value, heat_source=heat_source, name=nodes) + .to_pandas() + .reindex(index=n.snapshots) + if options["time_dep_hp_cop"] + else costs.at[costs_name, "efficiency"] + ) - n.madd( - "Link", - nodes, - suffix=f" {heat_system} {heat_source} heat pump-{grouping_year}", - bus0=nodes_elec, - bus1=nodes + " " + heat_system.value + " heat", - carrier=f"{heat_system} {heat_source} heat pump", - efficiency=efficiency, - capital_cost=costs.at[costs_name, "efficiency"] - * costs.at[costs_name, "fixed"], - p_nom=existing_heating.loc[ - nodes, (heat_system.value, f"{heat_source} heat pump") - ] - * ratio - / costs.at[costs_name, "efficiency"], - build_year=int(grouping_year), - lifetime=costs.at[costs_name, "lifetime"], - ) + n.madd( + "Link", + nodes, + suffix=f" {heat_system} {heat_source} heat pump-{grouping_year}", + bus0=nodes_elec, + bus1=nodes + " " + heat_system.value + " heat", + carrier=f"{heat_system} {heat_source} heat pump", + efficiency=efficiency, + capital_cost=costs.at[costs_name, "efficiency"] + * costs.at[costs_name, "fixed"], + p_nom=existing_heating.loc[ + nodes, (heat_system.value, f"{heat_source} heat pump") + ] + * ratio + / costs.at[costs_name, "efficiency"], + build_year=int(grouping_year), + lifetime=costs.at[costs_name, "lifetime"], + ) # add resistive heater, gas boilers and oil boilers n.madd( @@ -520,42 +519,42 @@ def add_heating_capacities_installed_before_baseyear( suffix=f" {heat_system} resistive heater-{grouping_year}", bus0=nodes_elec, bus1=nodes + " " + heat_system.value + " heat", - carrier=heat_system + " resistive heater", - efficiency=costs.at[f"{heat_system.system_type} resistive heater", "efficiency"], + carrier=heat_system.value + " resistive heater", + efficiency=costs.at[heat_system.resistive_heater_costs_name, "efficiency"], capital_cost=( - costs.at[f"{heat_system.system_type} resistive heater", "efficiency"] - * costs.at[f"{heat_system.system_type} resistive heater", "fixed"] + costs.at[heat_system.resistive_heater_costs_name, "efficiency"] + * costs.at[heat_system.resistive_heater_costs_name, "fixed"] ), p_nom=( existing_heating.loc[nodes, (heat_system.value, "resistive heater")] * ratio - / costs.at[f"{heat_system.system_type} resistive heater", "efficiency"] + / costs.at[heat_system.resistive_heater_costs_name, "efficiency"] ), build_year=int(grouping_year), - lifetime=costs.at[f"{heat_system.system_type} resistive heater", "lifetime"], + lifetime=costs.at[heat_system.resistive_heater_costs_name, "lifetime"], ) n.madd( "Link", nodes, - suffix=f" {heat_system} gas boiler-{grouping_year}", + suffix=f"{heat_system} gas boiler-{grouping_year}", bus0="EU gas" if "EU gas" in spatial.gas.nodes else nodes + " gas", - bus1=nodes + " " + heat_system + " heat", + bus1=f"{nodes} {heat_system} heat", bus2="co2 atmosphere", - carrier=heat_system + " gas boiler", - efficiency=costs.at[f"{heat_system.system_type} gas boiler", "efficiency"], + carrier=heat_system.value + " gas boiler", + efficiency=costs.at[heat_system.gas_boiler_costs_name, "efficiency"], efficiency2=costs.at["gas", "CO2 intensity"], capital_cost=( - costs.at[f"{heat_system.system_type} gas boiler", "efficiency"] - * costs.at[f"{heat_system.system_type} gas boiler", "fixed"] + costs.at[heat_system.gas_boiler_costs_name, "efficiency"] + * costs.at[heat_system.gas_boiler_costs_name, "fixed"] ), p_nom=( - existing_heating.loc[nodes, (heat_system, "gas boiler")] + existing_heating.loc[nodes, (heat_system.value, "gas boiler")] * ratio - / costs.at[f"{heat_system.system_type} gas boiler", "efficiency"] + / costs.at[heat_system.gas_boiler_costs_name, "efficiency"] ), build_year=int(grouping_year), - lifetime=costs.at[f"{heat_system.system_type} gas boiler", "lifetime"], + lifetime=costs.at[heat_system.gas_boiler_costs_name, "lifetime"], ) n.madd( @@ -563,20 +562,20 @@ def add_heating_capacities_installed_before_baseyear( nodes, suffix=f" {heat_system} oil boiler-{grouping_year}", bus0=spatial.oil.nodes, - bus1=nodes + " " + heat_system + " heat", + bus1=f"{nodes} {heat_system} heat", bus2="co2 atmosphere", - carrier=heat_system + " oil boiler", - efficiency=costs.at["decentral oil boiler", "efficiency"], + carrier=heat_system.value + " oil boiler", + efficiency=costs.at[heat_system.oil_boiler_costs_name, "efficiency"], efficiency2=costs.at["oil", "CO2 intensity"], - capital_cost=costs.at["decentral oil boiler", "efficiency"] - * costs.at["decentral oil boiler", "fixed"], + capital_cost=costs.at[heat_system.oil_boiler_costs_name, "efficiency"] + * costs.at[heat_system.oil_boiler_costs_name, "fixed"], p_nom=( - existing_heating.loc[nodes, (heat_system, "oil boiler")] + existing_heating.loc[nodes, (heat_system.value, "oil boiler")] * ratio - / costs.at["decentral oil boiler", "efficiency"] + / costs.at[heat_system.oil_boiler_costs_name, "efficiency"] ), build_year=int(grouping_year), - lifetime=costs.at[f"{heat_system.system_type} gas boiler", "lifetime"], + lifetime=costs.at[f"{heat_system.central_or_decentral} gas boiler", "lifetime"], ) # delete links with p_nom=nan corresponding to extra nodes in country @@ -651,28 +650,7 @@ def add_heating_capacities_installed_before_baseyear( n=n, baseyear=baseyear, grouping_years=grouping_years_heat, - cop={ - "air decentral": xr.open_dataarray( - snakemake.input.cop_air_decentral_heating - ) - .to_pandas() - .reindex(index=n.snapshots), - "ground decentral": xr.open_dataarray( - snakemake.input.cop_soil_decentral_heating - ) - .to_pandas() - .reindex(index=n.snapshots), - "air central": xr.open_dataarray( - snakemake.input.cop_air_central_heating - ) - .to_pandas() - .reindex(index=n.snapshots), - "ground central": xr.open_dataarray( - snakemake.input.cop_soil_central_heating - ) - .to_pandas() - .reindex(index=n.snapshots), - }, + cop=xr.open_dataarray(snakemake.input.cop_profiles), time_dep_hp_cop=options["time_dep_hp_cop"], costs=costs, default_lifetime=snakemake.params.existing_capacities[ diff --git a/scripts/enums/HeatSystem.py b/scripts/enums/HeatSystem.py index 608da92fa..4b1c11af4 100644 --- a/scripts/enums/HeatSystem.py +++ b/scripts/enums/HeatSystem.py @@ -210,7 +210,7 @@ def heat_demand_weighting(self, urban_fraction=None, dist_fraction=None) -> floa def heat_pump_costs_name(self, heat_source: str) -> str: """ - Generates the name for the heat pump costs based on the heat source. + Generates the name for the heat pump costs based on the heat source and system. Parameters ---------- @@ -223,3 +223,41 @@ def heat_pump_costs_name(self, heat_source: str) -> str: The name for the heat pump costs. """ return f"{self.central_or_decentral} {heat_source}-sourced heat pump" + + @property + def resistive_heater_costs_name(self) -> str: + """ + Generates the name for the resistive heater costs based on the heat system. + + Returns + ------- + str + The name for the heater costs. + """ + return f"{self.central_or_decentral} resistive heater" + + @property + def gas_boiler_costs_name(self) -> str: + """ + Generates the name for the gas boiler costs based on the heat system. + + Returns + ------- + str + The name for the gas boiler costs. + """ + return f"{self.central_or_decentral} gas boiler" + + @property + def oil_boiler_costs_name(self) -> str: + """ + Generates the name for the oil boiler costs based on the heat system. + + Returns + ------- + str + The name for the oil boiler costs. + """ + return "decentral oil boiler" + + diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 88748dcc6..9ea69dfb4 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1802,7 +1802,7 @@ def build_heat_demand(n): return heat_demand -def add_heat(n, costs): +def add_heat(n, costs, cop): logger.info("Add heat sector") sectors = ["residential", "services"] @@ -1833,7 +1833,6 @@ def add_heat(n, costs): # 1e3 converts from W/m^2 to MW/(1000m^2) = kW/m^2 solar_thermal = options["solar_cf_correction"] * solar_thermal / 1e3 - cop = xr.open_dataarray(snakemake.input.cop_profiles) for ( heat_system ) in ( @@ -4098,7 +4097,7 @@ def add_enhanced_geothermal(n, egs_potentials, egs_overlap, costs): add_land_transport(n, costs) if options["heating"]: - add_heat(n, costs) + add_heat(n=n, costs=costs, cop=xr.open_dataarray(snakemake.input.cop_profiles)) if options["biomass"]: add_biomass(n, costs) From 390085aaad9088942701470ef0a0bd3e46f0fdd2 Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Mon, 5 Aug 2024 11:35:34 +0200 Subject: [PATCH 142/344] update add_existing_baseyear --- scripts/add_existing_baseyear.py | 53 -------------------------------- 1 file changed, 53 deletions(-) diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index abdd0a8d3..593dfa266 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -458,7 +458,6 @@ def add_heating_capacities_installed_before_baseyear( else: nodes_elec = nodes -<<<<<<< HEAD too_large_grouping_years = [gy for gy in grouping_years if gy >= int(baseyear)] if too_large_grouping_years: logger.warning( @@ -471,28 +470,6 @@ def add_heating_capacities_installed_before_baseyear( if int(grouping_year) + default_lifetime > int(baseyear) and int(grouping_year) < int(baseyear) ] -======= - # Add heat pumps - heat_source = snakemake.params.heat_pump_sources[heat_system.system_type.value] - costs_name = f"{heat_system.system_type} {heat_source}-sourced heat pump" - - efficiency = ( - cop.sel( - heat_system=heat_system.system_type.value, - heat_source=heat_source, - name=nodes, - ) - .to_pandas() - .reindex(index=n.snapshots) - if options["time_dep_hp_cop"] - else costs.at[costs_name, "efficiency"] - ) - - too_large_grouping_years = [gy for gy in grouping_years if gy >= int(baseyear)] - if too_large_grouping_years: - logger.warning( - f"Grouping years >= baseyear are ignored. Dropping {too_large_grouping_years}." ->>>>>>> 0259e066e4a5688d002e47ec9b34bc9e45ae437d ) assert valid_grouping_years.is_monotonic_increasing @@ -543,42 +520,19 @@ def add_heating_capacities_installed_before_baseyear( suffix=f" {heat_system} resistive heater-{grouping_year}", bus0=nodes_elec, bus1=nodes + " " + heat_system.value + " heat", -<<<<<<< HEAD carrier=heat_system.value + " resistive heater", efficiency=costs.at[heat_system.resistive_heater_costs_name, "efficiency"], capital_cost=( costs.at[heat_system.resistive_heater_costs_name, "efficiency"] * costs.at[heat_system.resistive_heater_costs_name, "fixed"] -======= - carrier=heat_system + " resistive heater", - efficiency=costs.at[ - f"{heat_system.system_type} resistive heater", "efficiency" - ], - capital_cost=( - costs.at[ - f"{heat_system.system_type} resistive heater", "efficiency" - ] - * costs.at[f"{heat_system.system_type} resistive heater", "fixed"] ->>>>>>> 0259e066e4a5688d002e47ec9b34bc9e45ae437d ), p_nom=( existing_heating.loc[nodes, (heat_system.value, "resistive heater")] * ratio -<<<<<<< HEAD / costs.at[heat_system.resistive_heater_costs_name, "efficiency"] ), build_year=int(grouping_year), lifetime=costs.at[heat_system.resistive_heater_costs_name, "lifetime"], -======= - / costs.at[ - f"{heat_system.system_type} resistive heater", "efficiency" - ] - ), - build_year=int(grouping_year), - lifetime=costs.at[ - f"{heat_system.system_type} resistive heater", "lifetime" - ], ->>>>>>> 0259e066e4a5688d002e47ec9b34bc9e45ae437d ) n.madd( @@ -588,15 +542,8 @@ def add_heating_capacities_installed_before_baseyear( bus0="EU gas" if "EU gas" in spatial.gas.nodes else nodes + " gas", bus1=f"{nodes} {heat_system} heat", bus2="co2 atmosphere", -<<<<<<< HEAD carrier=heat_system.value + " gas boiler", efficiency=costs.at[heat_system.gas_boiler_costs_name, "efficiency"], -======= - carrier=heat_system + " gas boiler", - efficiency=costs.at[ - f"{heat_system.system_type} gas boiler", "efficiency" - ], ->>>>>>> 0259e066e4a5688d002e47ec9b34bc9e45ae437d efficiency2=costs.at["gas", "CO2 intensity"], capital_cost=( costs.at[heat_system.gas_boiler_costs_name, "efficiency"] From 61bb225a34fadc2eda0eb35c81b33efd86bda296 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2024 09:42:42 +0000 Subject: [PATCH 143/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/add_existing_baseyear.py | 26 ++++++++++++++++++++------ scripts/enums/HeatSystem.py | 8 ++++---- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index 593dfa266..15c57f32b 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -453,12 +453,16 @@ def add_heating_capacities_installed_before_baseyear( n.buses.location[n.buses.index.str.contains(f"{heat_system} heat")] ) - if (not heat_system == HeatSystem.URBAN_CENTRAL) and options["electricity_distribution_grid"]: + if (not heat_system == HeatSystem.URBAN_CENTRAL) and options[ + "electricity_distribution_grid" + ]: nodes_elec = nodes + " low voltage" else: nodes_elec = nodes - too_large_grouping_years = [gy for gy in grouping_years if gy >= int(baseyear)] + too_large_grouping_years = [ + gy for gy in grouping_years if gy >= int(baseyear) + ] if too_large_grouping_years: logger.warning( f"Grouping years >= baseyear are ignored. Dropping {too_large_grouping_years}." @@ -483,11 +487,17 @@ def add_heating_capacities_installed_before_baseyear( for ratio, grouping_year in zip(ratios, valid_grouping_years): # Add heat pumps - for heat_source in snakemake.params.heat_pump_sources[heat_system.system_type.value]: + for heat_source in snakemake.params.heat_pump_sources[ + heat_system.system_type.value + ]: costs_name = heat_system.heat_pump_costs_name(heat_source) efficiency = ( - cop.sel(heat_system=heat_system.system_type.value, heat_source=heat_source, name=nodes) + cop.sel( + heat_system=heat_system.system_type.value, + heat_source=heat_source, + name=nodes, + ) .to_pandas() .reindex(index=n.snapshots) if options["time_dep_hp_cop"] @@ -521,7 +531,9 @@ def add_heating_capacities_installed_before_baseyear( bus0=nodes_elec, bus1=nodes + " " + heat_system.value + " heat", carrier=heat_system.value + " resistive heater", - efficiency=costs.at[heat_system.resistive_heater_costs_name, "efficiency"], + efficiency=costs.at[ + heat_system.resistive_heater_costs_name, "efficiency" + ], capital_cost=( costs.at[heat_system.resistive_heater_costs_name, "efficiency"] * costs.at[heat_system.resistive_heater_costs_name, "fixed"] @@ -576,7 +588,9 @@ def add_heating_capacities_installed_before_baseyear( / costs.at[heat_system.oil_boiler_costs_name, "efficiency"] ), build_year=int(grouping_year), - lifetime=costs.at[f"{heat_system.central_or_decentral} gas boiler", "lifetime"], + lifetime=costs.at[ + f"{heat_system.central_or_decentral} gas boiler", "lifetime" + ], ) # delete links with p_nom=nan corresponding to extra nodes in country diff --git a/scripts/enums/HeatSystem.py b/scripts/enums/HeatSystem.py index 4b1c11af4..75cd3344e 100644 --- a/scripts/enums/HeatSystem.py +++ b/scripts/enums/HeatSystem.py @@ -210,7 +210,8 @@ def heat_demand_weighting(self, urban_fraction=None, dist_fraction=None) -> floa def heat_pump_costs_name(self, heat_source: str) -> str: """ - Generates the name for the heat pump costs based on the heat source and system. + Generates the name for the heat pump costs based on the heat source and + system. Parameters ---------- @@ -227,7 +228,8 @@ def heat_pump_costs_name(self, heat_source: str) -> str: @property def resistive_heater_costs_name(self) -> str: """ - Generates the name for the resistive heater costs based on the heat system. + Generates the name for the resistive heater costs based on the heat + system. Returns ------- @@ -259,5 +261,3 @@ def oil_boiler_costs_name(self) -> str: The name for the oil boiler costs. """ return "decentral oil boiler" - - From f593983ea04bb7e44c6fdb2699c67d8505ca52bf Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Mon, 5 Aug 2024 11:45:12 +0200 Subject: [PATCH 144/344] update copyright notic in heatSector --- scripts/enums/HeatSector.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/enums/HeatSector.py b/scripts/enums/HeatSector.py index 4c763fec1..03bcaffdb 100644 --- a/scripts/enums/HeatSector.py +++ b/scripts/enums/HeatSector.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2020-2024 The PyPSA-Eur Authors +# # SPDX-License-Identifier: MIT from enum import Enum From 975716250ad3c133b105b3d0b34822478fdea8b2 Mon Sep 17 00:00:00 2001 From: Philipp Glaum Date: Mon, 5 Aug 2024 12:00:15 +0200 Subject: [PATCH 145/344] add bus0 for methanol-to-kerosene process --- scripts/prepare_sector_network.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 989bc4f95..581074f2c 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1022,6 +1022,7 @@ def add_methanol_to_kerosene(n, costs): suffix=f" {tech}", carrier=tech, capital_cost=capital_cost, + bus0=spatial.methanol.nodes, bus1=spatial.oil.kerosene, bus2=spatial.h2.nodes, efficiency=costs.at[tech, "methanol-input"], From 268ff9378375e8618130833274a772e27041e2b4 Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Mon, 5 Aug 2024 12:07:24 +0200 Subject: [PATCH 146/344] fix boiler buses for existing heating --- scripts/add_existing_baseyear.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index 15c57f32b..d0f85a1a2 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -552,7 +552,7 @@ def add_heating_capacities_installed_before_baseyear( nodes, suffix=f"{heat_system} gas boiler-{grouping_year}", bus0="EU gas" if "EU gas" in spatial.gas.nodes else nodes + " gas", - bus1=f"{nodes} {heat_system} heat", + bus1=nodes + " " + heat_system.value + " heat", bus2="co2 atmosphere", carrier=heat_system.value + " gas boiler", efficiency=costs.at[heat_system.gas_boiler_costs_name, "efficiency"], @@ -575,7 +575,7 @@ def add_heating_capacities_installed_before_baseyear( nodes, suffix=f" {heat_system} oil boiler-{grouping_year}", bus0=spatial.oil.nodes, - bus1=f"{nodes} {heat_system} heat", + bus1=nodes + " " + heat_system.value + " heat", bus2="co2 atmosphere", carrier=heat_system.value + " oil boiler", efficiency=costs.at[heat_system.oil_boiler_costs_name, "efficiency"], From ea7fc92546ce05e9fbc4e36f2562b47e3a107859 Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Mon, 5 Aug 2024 12:19:43 +0200 Subject: [PATCH 147/344] update solve_perfect.add_existing_baseyear input/params --- rules/solve_perfect.smk | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rules/solve_perfect.smk b/rules/solve_perfect.smk index d2ea1a16a..057f7ee2a 100644 --- a/rules/solve_perfect.smk +++ b/rules/solve_perfect.smk @@ -7,6 +7,7 @@ rule add_existing_baseyear: sector=config_provider("sector"), existing_capacities=config_provider("existing_capacities"), costs=config_provider("costs"), + heat_pump_sources=config_provider("sector", "heat_pump_sources"), input: network=RESULTS + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", @@ -19,6 +20,7 @@ rule add_existing_baseyear: config_provider("scenario", "planning_horizons", 0)(w) ) ), + cop_profiles=resources("cop_profiles_elec_s{simpl}_{clusters}.nc"), cop_soil_decentral_heating=resources( "cop_soil_decentral_heating_elec_s{simpl}_{clusters}.nc" ), From 93d60a2927bf7c5cf9872d8fd123319503f8d8c2 Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Mon, 5 Aug 2024 12:26:26 +0200 Subject: [PATCH 148/344] remove obsolote cop inputs from solve_perfect.add_existing_baseyear --- rules/solve_perfect.smk | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/rules/solve_perfect.smk b/rules/solve_perfect.smk index 057f7ee2a..a06c6dfa1 100644 --- a/rules/solve_perfect.smk +++ b/rules/solve_perfect.smk @@ -21,18 +21,6 @@ rule add_existing_baseyear: ) ), cop_profiles=resources("cop_profiles_elec_s{simpl}_{clusters}.nc"), - cop_soil_decentral_heating=resources( - "cop_soil_decentral_heating_elec_s{simpl}_{clusters}.nc" - ), - cop_air_decentral_heating=resources( - "cop_air_decentral_heating_elec_s{simpl}_{clusters}.nc" - ), - cop_air_central_heating=resources( - "cop_air_central_heating_elec_s{simpl}_{clusters}.nc" - ), - cop_soil_central_heating=resources( - "cop_soil_central_heating_elec_s{simpl}_{clusters}.nc" - ), existing_heating_distribution=resources( "existing_heating_distribution_elec_s{simpl}_{clusters}_{planning_horizons}.csv" ), From b298d58d61e4e5002a97fe4382ae86d11c2ae60a Mon Sep 17 00:00:00 2001 From: Philipp Glaum Date: Mon, 5 Aug 2024 13:27:43 +0200 Subject: [PATCH 149/344] fix missing biomass-to-methanol due to wrong if statement --- scripts/prepare_sector_network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 581074f2c..5d61821cf 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2605,7 +2605,7 @@ def add_methanol(n, costs): carrier="methanol transport", ) - if "biomass" in n.buses.carrier.unique(): + if n.buses.carrier.str.contains("biomass").any(): if options["biomass_to_methanol"]: add_biomass_to_methanol(n, costs) From 280e8d6d01237a79a1befd4c1df19db6bfa440c4 Mon Sep 17 00:00:00 2001 From: Philipp Glaum Date: Mon, 5 Aug 2024 13:40:05 +0200 Subject: [PATCH 150/344] use correct colorcode for biomass-to-methanol --- config/config.default.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 10d166146..6ba8039e6 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -1183,8 +1183,8 @@ plotting: Methanol steam reforming: '#FFBF00' Methanol steam reforming CC: '#A2EA8A' methanolisation: '#00FFBF' - biomass-to-methanol: #EAD28A - biomass-to-methanol CC: #EADBAD + biomass-to-methanol: '#EAD28A' + biomass-to-methanol CC: '#EADBAD' allam methanol: '#B98F76' CCGT methanol: '#B98F76' CCGT methanol CC: '#B98F76' From 4fe8eeeffab8191ff2fb69f9f5a4320c4bd3ab2d Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Mon, 5 Aug 2024 15:07:46 +0200 Subject: [PATCH 151/344] rename "enums" to "definitions" and write modules in snake case --- scripts/definitions/heat_sector.py | 28 +++ scripts/definitions/heat_system.py | 263 ++++++++++++++++++++++++ scripts/definitions/heat_system_type.py | 35 ++++ 3 files changed, 326 insertions(+) create mode 100644 scripts/definitions/heat_sector.py create mode 100644 scripts/definitions/heat_system.py create mode 100644 scripts/definitions/heat_system_type.py diff --git a/scripts/definitions/heat_sector.py b/scripts/definitions/heat_sector.py new file mode 100644 index 000000000..03bcaffdb --- /dev/null +++ b/scripts/definitions/heat_sector.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2020-2024 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT + +from enum import Enum + + +class HeatSector(Enum): + """ + Enumeration class representing different heat sectors. + + Attributes: + RESIDENTIAL (str): Represents the residential heat sector. + SERVICES (str): Represents the services heat sector. + """ + + RESIDENTIAL = "residential" + SERVICES = "services" + + def __str__(self) -> str: + """ + Returns the string representation of the heat sector. + + Returns: + str: The string representation of the heat sector. + """ + return self.value diff --git a/scripts/definitions/heat_system.py b/scripts/definitions/heat_system.py new file mode 100644 index 000000000..2f305644c --- /dev/null +++ b/scripts/definitions/heat_system.py @@ -0,0 +1,263 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2020-2024 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT + +from enum import Enum + +from scripts.definitions.heat_sector import HeatSector +from scripts.definitions.heat_system_type import HeatSystemType + + +class HeatSystem(Enum): + """ + Enumeration representing different heat systems. + + Attributes + ---------- + RESIDENTIAL_RURAL : str + Heat system for residential areas in rural locations. + SERVICES_RURAL : str + Heat system for service areas in rural locations. + RESIDENTIAL_URBAN_DECENTRAL : str + Heat system for residential areas in urban decentralized locations. + SERVICES_URBAN_DECENTRAL : str + Heat system for service areas in urban decentralized locations. + URBAN_CENTRAL : str + Heat system for urban central areas. + + Methods + ------- + __str__() + Returns the string representation of the heat system. + central_or_decentral() + Returns whether the heat system is central or decentralized. + system_type() + Returns the type of the heat system. + sector() + Returns the sector of the heat system. + rural() + Returns whether the heat system is for rural areas. + urban_decentral() + Returns whether the heat system is for urban decentralized areas. + urban() + Returns whether the heat system is for urban areas. + heat_demand_weighting(urban_fraction=None, dist_fraction=None) + Calculates the heat demand weighting based on urban fraction and distribution fraction. + heat_pump_costs_name(heat_source) + Generates the name for the heat pump costs based on the heat source. + """ + + RESIDENTIAL_RURAL = "residential rural" + SERVICES_RURAL = "services rural" + RESIDENTIAL_URBAN_DECENTRAL = "residential urban decentral" + SERVICES_URBAN_DECENTRAL = "services urban decentral" + URBAN_CENTRAL = "urban central" + + def __init__(self, *args): + super().__init__(*args) + + def __str__(self) -> str: + """ + Returns the string representation of the heat system. + + Returns + ------- + str + The string representation of the heat system. + """ + return self.value + + @property + def central_or_decentral(self) -> str: + """ + Returns whether the heat system is central or decentralized. + + Returns + ------- + str + "central" if the heat system is central, "decentral" otherwise. + """ + if self == HeatSystem.URBAN_CENTRAL: + return "central" + else: + return "decentral" + + @property + def system_type(self) -> HeatSystemType: + """ + Returns the type of the heat system. + + Returns + ------- + str + The type of the heat system. + + Raises + ------ + RuntimeError + If the heat system is invalid. + """ + if self == HeatSystem.URBAN_CENTRAL: + return HeatSystemType.URBAN_CENTRAL + elif ( + self == HeatSystem.RESIDENTIAL_URBAN_DECENTRAL + or self == HeatSystem.SERVICES_URBAN_DECENTRAL + ): + return HeatSystemType.URBAN_DECENTRAL + elif self == HeatSystem.RESIDENTIAL_RURAL or self == HeatSystem.SERVICES_RURAL: + return HeatSystemType.RURAL + else: + raise RuntimeError(f"Invalid heat system: {self}") + + @property + def sector(self) -> HeatSector: + """ + Returns the sector of the heat system. + + Returns + ------- + HeatSector + The sector of the heat system. + """ + if ( + self == HeatSystem.RESIDENTIAL_RURAL + or self == HeatSystem.RESIDENTIAL_URBAN_DECENTRAL + ): + return HeatSector.RESIDENTIAL + elif ( + self == HeatSystem.SERVICES_RURAL + or self == HeatSystem.SERVICES_URBAN_DECENTRAL + ): + return HeatSector.SERVICES + else: + "tot" + + @property + def is_rural(self) -> bool: + """ + Returns whether the heat system is for rural areas. + + Returns + ------- + bool + True if the heat system is for rural areas, False otherwise. + """ + if self == HeatSystem.RESIDENTIAL_RURAL or self == HeatSystem.SERVICES_RURAL: + return True + else: + return False + + @property + def is_urban_decentral(self) -> bool: + """ + Returns whether the heat system is for urban decentralized areas. + + Returns + ------- + bool + True if the heat system is for urban decentralized areas, False otherwise. + """ + if ( + self == HeatSystem.RESIDENTIAL_URBAN_DECENTRAL + or self == HeatSystem.SERVICES_URBAN_DECENTRAL + ): + return True + else: + return False + + @property + def is_urban(self) -> bool: + """ + Returns whether the heat system is for urban areas. + + Returns + ------- + bool True if the heat system is for urban areas, False otherwise. + """ + return not self.is_rural + + def heat_demand_weighting(self, urban_fraction=None, dist_fraction=None) -> float: + """ + Calculates the heat demand weighting based on urban fraction and + distribution fraction. + + Parameters + ---------- + urban_fraction : float, optional + The fraction of urban heat demand. + dist_fraction : float, optional + The fraction of distributed heat demand. + + Returns + ------- + float + The heat demand weighting. + + Raises + ------ + RuntimeError + If the heat system is invalid. + """ + if "rural" in self.value: + return 1 - urban_fraction + elif "urban central" in self.value: + return dist_fraction + elif "urban decentral" in self.value: + return urban_fraction - dist_fraction + else: + raise RuntimeError(f"Invalid heat system: {self}") + + def heat_pump_costs_name(self, heat_source: str) -> str: + """ + Generates the name for the heat pump costs based on the heat source and + system. + + Parameters + ---------- + heat_source : str + The heat source. + + Returns + ------- + str + The name for the heat pump costs. + """ + return f"{self.central_or_decentral} {heat_source}-sourced heat pump" + + @property + def resistive_heater_costs_name(self) -> str: + """ + Generates the name for the resistive heater costs based on the heat + system. + + Returns + ------- + str + The name for the heater costs. + """ + return f"{self.central_or_decentral} resistive heater" + + @property + def gas_boiler_costs_name(self) -> str: + """ + Generates the name for the gas boiler costs based on the heat system. + + Returns + ------- + str + The name for the gas boiler costs. + """ + return f"{self.central_or_decentral} gas boiler" + + @property + def oil_boiler_costs_name(self) -> str: + """ + Generates the name for the oil boiler costs based on the heat system. + + Returns + ------- + str + The name for the oil boiler costs. + """ + return "decentral oil boiler" diff --git a/scripts/definitions/heat_system_type.py b/scripts/definitions/heat_system_type.py new file mode 100644 index 000000000..5bd1bee32 --- /dev/null +++ b/scripts/definitions/heat_system_type.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2020-2024 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT + +from enum import Enum + + +class HeatSystemType(Enum): + """ + Enumeration representing different types of heat systems. + """ + + URBAN_CENTRAL = "urban central" + URBAN_DECENTRAL = "urban decentral" + RURAL = "rural" + + def __str__(self) -> str: + """ + Returns the string representation of the heat system type. + + Returns: + str: The string representation of the heat system type. + """ + return self.value + + @property + def is_central(self) -> bool: + """ + Returns whether the heat system type is central. + + Returns: + bool: True if the heat system type is central, False otherwise. + """ + return self == HeatSystemType.URBAN_CENTRAL From 2e35bef480614a09ff25e7154cbb8f1028b6e1c5 Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Mon, 5 Aug 2024 15:08:05 +0200 Subject: [PATCH 152/344] rename "enums" to "definitions" and write modules in snake case --- scripts/add_existing_baseyear.py | 6 +- scripts/build_cop_profiles/run.py | 2 +- scripts/enums/HeatSector.py | 28 ---- scripts/enums/HeatSystem.py | 263 ------------------------------ scripts/enums/HeatSystemType.py | 35 ---- scripts/prepare_sector_network.py | 6 +- 6 files changed, 7 insertions(+), 333 deletions(-) delete mode 100644 scripts/enums/HeatSector.py delete mode 100644 scripts/enums/HeatSystem.py delete mode 100644 scripts/enums/HeatSystemType.py diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index d0f85a1a2..ff1a3e46a 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -24,9 +24,9 @@ from add_electricity import sanitize_carriers from prepare_sector_network import cluster_heat_buses, define_spatial, prepare_costs -from scripts.enums.HeatSector import HeatSector -from scripts.enums.HeatSystem import HeatSystem -from scripts.enums.HeatSystemType import HeatSystemType +from scripts.definitions.heat_sector import HeatSector +from scripts.definitions.heat_system import HeatSystem +from scripts.definitions.heat_system_type import HeatSystemType logger = logging.getLogger(__name__) cc = coco.CountryConverter() diff --git a/scripts/build_cop_profiles/run.py b/scripts/build_cop_profiles/run.py index 7e405a424..4d57db310 100644 --- a/scripts/build_cop_profiles/run.py +++ b/scripts/build_cop_profiles/run.py @@ -12,7 +12,7 @@ from CentralHeatingCopApproximator import CentralHeatingCopApproximator from DecentralHeatingCopApproximator import DecentralHeatingCopApproximator -from scripts.enums.HeatSystemType import HeatSystemType +from scripts.definitions.heat_system_type import HeatSystemType sys.path.append("..") diff --git a/scripts/enums/HeatSector.py b/scripts/enums/HeatSector.py deleted file mode 100644 index 03bcaffdb..000000000 --- a/scripts/enums/HeatSector.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- -# SPDX-FileCopyrightText: : 2020-2024 The PyPSA-Eur Authors -# -# SPDX-License-Identifier: MIT - -from enum import Enum - - -class HeatSector(Enum): - """ - Enumeration class representing different heat sectors. - - Attributes: - RESIDENTIAL (str): Represents the residential heat sector. - SERVICES (str): Represents the services heat sector. - """ - - RESIDENTIAL = "residential" - SERVICES = "services" - - def __str__(self) -> str: - """ - Returns the string representation of the heat sector. - - Returns: - str: The string representation of the heat sector. - """ - return self.value diff --git a/scripts/enums/HeatSystem.py b/scripts/enums/HeatSystem.py deleted file mode 100644 index 75cd3344e..000000000 --- a/scripts/enums/HeatSystem.py +++ /dev/null @@ -1,263 +0,0 @@ -# -*- coding: utf-8 -*- -# SPDX-FileCopyrightText: : 2020-2024 The PyPSA-Eur Authors -# -# SPDX-License-Identifier: MIT - -from enum import Enum - -from scripts.enums.HeatSector import HeatSector -from scripts.enums.HeatSystemType import HeatSystemType - - -class HeatSystem(Enum): - """ - Enumeration representing different heat systems. - - Attributes - ---------- - RESIDENTIAL_RURAL : str - Heat system for residential areas in rural locations. - SERVICES_RURAL : str - Heat system for service areas in rural locations. - RESIDENTIAL_URBAN_DECENTRAL : str - Heat system for residential areas in urban decentralized locations. - SERVICES_URBAN_DECENTRAL : str - Heat system for service areas in urban decentralized locations. - URBAN_CENTRAL : str - Heat system for urban central areas. - - Methods - ------- - __str__() - Returns the string representation of the heat system. - central_or_decentral() - Returns whether the heat system is central or decentralized. - system_type() - Returns the type of the heat system. - sector() - Returns the sector of the heat system. - rural() - Returns whether the heat system is for rural areas. - urban_decentral() - Returns whether the heat system is for urban decentralized areas. - urban() - Returns whether the heat system is for urban areas. - heat_demand_weighting(urban_fraction=None, dist_fraction=None) - Calculates the heat demand weighting based on urban fraction and distribution fraction. - heat_pump_costs_name(heat_source) - Generates the name for the heat pump costs based on the heat source. - """ - - RESIDENTIAL_RURAL = "residential rural" - SERVICES_RURAL = "services rural" - RESIDENTIAL_URBAN_DECENTRAL = "residential urban decentral" - SERVICES_URBAN_DECENTRAL = "services urban decentral" - URBAN_CENTRAL = "urban central" - - def __init__(self, *args): - super().__init__(*args) - - def __str__(self) -> str: - """ - Returns the string representation of the heat system. - - Returns - ------- - str - The string representation of the heat system. - """ - return self.value - - @property - def central_or_decentral(self) -> str: - """ - Returns whether the heat system is central or decentralized. - - Returns - ------- - str - "central" if the heat system is central, "decentral" otherwise. - """ - if self == HeatSystem.URBAN_CENTRAL: - return "central" - else: - return "decentral" - - @property - def system_type(self) -> HeatSystemType: - """ - Returns the type of the heat system. - - Returns - ------- - str - The type of the heat system. - - Raises - ------ - RuntimeError - If the heat system is invalid. - """ - if self == HeatSystem.URBAN_CENTRAL: - return HeatSystemType.URBAN_CENTRAL - elif ( - self == HeatSystem.RESIDENTIAL_URBAN_DECENTRAL - or self == HeatSystem.SERVICES_URBAN_DECENTRAL - ): - return HeatSystemType.URBAN_DECENTRAL - elif self == HeatSystem.RESIDENTIAL_RURAL or self == HeatSystem.SERVICES_RURAL: - return HeatSystemType.RURAL - else: - raise RuntimeError(f"Invalid heat system: {self}") - - @property - def sector(self) -> HeatSector: - """ - Returns the sector of the heat system. - - Returns - ------- - HeatSector - The sector of the heat system. - """ - if ( - self == HeatSystem.RESIDENTIAL_RURAL - or self == HeatSystem.RESIDENTIAL_URBAN_DECENTRAL - ): - return HeatSector.RESIDENTIAL - elif ( - self == HeatSystem.SERVICES_RURAL - or self == HeatSystem.SERVICES_URBAN_DECENTRAL - ): - return HeatSector.SERVICES - else: - "tot" - - @property - def is_rural(self) -> bool: - """ - Returns whether the heat system is for rural areas. - - Returns - ------- - bool - True if the heat system is for rural areas, False otherwise. - """ - if self == HeatSystem.RESIDENTIAL_RURAL or self == HeatSystem.SERVICES_RURAL: - return True - else: - return False - - @property - def is_urban_decentral(self) -> bool: - """ - Returns whether the heat system is for urban decentralized areas. - - Returns - ------- - bool - True if the heat system is for urban decentralized areas, False otherwise. - """ - if ( - self == HeatSystem.RESIDENTIAL_URBAN_DECENTRAL - or self == HeatSystem.SERVICES_URBAN_DECENTRAL - ): - return True - else: - return False - - @property - def is_urban(self) -> bool: - """ - Returns whether the heat system is for urban areas. - - Returns - ------- - bool True if the heat system is for urban areas, False otherwise. - """ - return not self.is_rural - - def heat_demand_weighting(self, urban_fraction=None, dist_fraction=None) -> float: - """ - Calculates the heat demand weighting based on urban fraction and - distribution fraction. - - Parameters - ---------- - urban_fraction : float, optional - The fraction of urban heat demand. - dist_fraction : float, optional - The fraction of distributed heat demand. - - Returns - ------- - float - The heat demand weighting. - - Raises - ------ - RuntimeError - If the heat system is invalid. - """ - if "rural" in self.value: - return 1 - urban_fraction - elif "urban central" in self.value: - return dist_fraction - elif "urban decentral" in self.value: - return urban_fraction - dist_fraction - else: - raise RuntimeError(f"Invalid heat system: {self}") - - def heat_pump_costs_name(self, heat_source: str) -> str: - """ - Generates the name for the heat pump costs based on the heat source and - system. - - Parameters - ---------- - heat_source : str - The heat source. - - Returns - ------- - str - The name for the heat pump costs. - """ - return f"{self.central_or_decentral} {heat_source}-sourced heat pump" - - @property - def resistive_heater_costs_name(self) -> str: - """ - Generates the name for the resistive heater costs based on the heat - system. - - Returns - ------- - str - The name for the heater costs. - """ - return f"{self.central_or_decentral} resistive heater" - - @property - def gas_boiler_costs_name(self) -> str: - """ - Generates the name for the gas boiler costs based on the heat system. - - Returns - ------- - str - The name for the gas boiler costs. - """ - return f"{self.central_or_decentral} gas boiler" - - @property - def oil_boiler_costs_name(self) -> str: - """ - Generates the name for the oil boiler costs based on the heat system. - - Returns - ------- - str - The name for the oil boiler costs. - """ - return "decentral oil boiler" diff --git a/scripts/enums/HeatSystemType.py b/scripts/enums/HeatSystemType.py deleted file mode 100644 index 5bd1bee32..000000000 --- a/scripts/enums/HeatSystemType.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: utf-8 -*- -# SPDX-FileCopyrightText: : 2020-2024 The PyPSA-Eur Authors -# -# SPDX-License-Identifier: MIT - -from enum import Enum - - -class HeatSystemType(Enum): - """ - Enumeration representing different types of heat systems. - """ - - URBAN_CENTRAL = "urban central" - URBAN_DECENTRAL = "urban decentral" - RURAL = "rural" - - def __str__(self) -> str: - """ - Returns the string representation of the heat system type. - - Returns: - str: The string representation of the heat system type. - """ - return self.value - - @property - def is_central(self) -> bool: - """ - Returns whether the heat system type is central. - - Returns: - bool: True if the heat system type is central, False otherwise. - """ - return self == HeatSystemType.URBAN_CENTRAL diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 83b76bb97..d9d0f6e32 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -37,9 +37,9 @@ from pypsa.io import import_components_from_dataframe from scipy.stats import beta -from scripts.enums.HeatSector import HeatSector -from scripts.enums.HeatSystem import HeatSystem -from scripts.enums.HeatSystemType import HeatSystemType +from scripts.definitions.heat_sector import HeatSector +from scripts.definitions.heat_system import HeatSystem +from scripts.definitions.heat_system_type import HeatSystemType spatial = SimpleNamespace() logger = logging.getLogger(__name__) From 0e8fb80bc42adb6bb098c08645da3472efb1081f Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Mon, 5 Aug 2024 16:11:18 +0200 Subject: [PATCH 153/344] clean up, improve docs --- scripts/add_existing_baseyear.py | 12 +++---- scripts/definitions/heat_system.py | 4 +++ scripts/prepare_sector_network.py | 55 +++++++++++++++++------------- 3 files changed, 41 insertions(+), 30 deletions(-) diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index ff1a3e46a..f67c38d9f 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -420,13 +420,13 @@ def add_power_capacities_installed_before_baseyear(n, grouping_years, costs, bas def add_heating_capacities_installed_before_baseyear( - n, - baseyear, - grouping_years, + n: pypsa.Network, + baseyear: int, + grouping_years: list, cop: dict, time_dep_hp_cop: bool, - costs, - default_lifetime, + costs: pd.DataFrame, + default_lifetime: int, existing_heating: pd.DataFrame, ): """ @@ -500,7 +500,7 @@ def add_heating_capacities_installed_before_baseyear( ) .to_pandas() .reindex(index=n.snapshots) - if options["time_dep_hp_cop"] + if time_dep_hp_cop else costs.at[costs_name, "efficiency"] ) diff --git a/scripts/definitions/heat_system.py b/scripts/definitions/heat_system.py index 2f305644c..b907b0fef 100644 --- a/scripts/definitions/heat_system.py +++ b/scripts/definitions/heat_system.py @@ -212,6 +212,7 @@ def heat_pump_costs_name(self, heat_source: str) -> str: """ Generates the name for the heat pump costs based on the heat source and system. + Used to retrieve data from `technology-data `. Parameters ---------- @@ -230,6 +231,7 @@ def resistive_heater_costs_name(self) -> str: """ Generates the name for the resistive heater costs based on the heat system. + Used to retrieve data from `technology-data `. Returns ------- @@ -242,6 +244,7 @@ def resistive_heater_costs_name(self) -> str: def gas_boiler_costs_name(self) -> str: """ Generates the name for the gas boiler costs based on the heat system. + Used to retrieve data from `technology-data `. Returns ------- @@ -254,6 +257,7 @@ def gas_boiler_costs_name(self) -> str: def oil_boiler_costs_name(self) -> str: """ Generates the name for the oil boiler costs based on the heat system. + Used to retrieve data from `technology-data `. Returns ------- diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index d9d0f6e32..585b3f258 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1780,7 +1780,7 @@ def build_heat_demand(n): .unstack(level=1) ) - sectors = ["residential", "services"] + sectors = [sector.value for sector in HeatSector] uses = ["water", "space"] heat_demand = {} @@ -1808,10 +1808,21 @@ def build_heat_demand(n): return heat_demand -def add_heat(n, costs, cop): +def add_heat(n: pypsa.Network, costs: pd.DataFrame, cop: xr.DataArray): + """ + Add heat sector to the network. + + Parameters: + n (pypsa.Network): The PyPSA network object. + costs (pd.DataFrame): DataFrame containing cost information. + cop (xr.DataArray): DataArray containing coefficient of performance (COP) values. + + Returns: + None + """ logger.info("Add heat sector") - sectors = ["residential", "services"] + sectors = [sector.value for sector in HeatSector] heat_demand = build_heat_demand(n) @@ -3113,27 +3124,23 @@ def add_industry(n, costs): if options["oil_boilers"]: nodes = pop_layout.index - for name in [ - "residential rural", - "services rural", - "residential urban decentral", - "services urban decentral", - ]: - n.madd( - "Link", - nodes + f" {name} oil boiler", - p_nom_extendable=True, - bus0=spatial.oil.nodes, - bus1=nodes + f" {name} heat", - bus2="co2 atmosphere", - carrier=f"{name} oil boiler", - efficiency=costs.at["decentral oil boiler", "efficiency"], - efficiency2=costs.at["oil", "CO2 intensity"], - capital_cost=costs.at["decentral oil boiler", "efficiency"] - * costs.at["decentral oil boiler", "fixed"] - * options["overdimension_individual_heating"], - lifetime=costs.at["decentral oil boiler", "lifetime"], - ) + for heat_system in HeatSystem: + if not heat_system == HeatSystem.URBAN_CENTRAL: + n.madd( + "Link", + nodes + f" {heat_system} oil boiler", + p_nom_extendable=True, + bus0=spatial.oil.nodes, + bus1=nodes + f" {heat_system} heat", + bus2="co2 atmosphere", + carrier=f"{heat_system} oil boiler", + efficiency=costs.at["decentral oil boiler", "efficiency"], + efficiency2=costs.at["oil", "CO2 intensity"], + capital_cost=costs.at["decentral oil boiler", "efficiency"] + * costs.at["decentral oil boiler", "fixed"] + * options["overdimension_individual_heating"], + lifetime=costs.at["decentral oil boiler", "lifetime"], + ) n.madd( "Link", From 0ec367ed7a771782495002437c2d1bae8da66179 Mon Sep 17 00:00:00 2001 From: lisazeyen <35347358+lisazeyen@users.noreply.github.com> Date: Tue, 6 Aug 2024 17:37:36 +0200 Subject: [PATCH 154/344] save idees directly in data not in bundle Co-authored-by: Fabian Neumann --- rules/retrieve.smk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rules/retrieve.smk b/rules/retrieve.smk index 720fa9149..8d0615db4 100644 --- a/rules/retrieve.smk +++ b/rules/retrieve.smk @@ -58,7 +58,7 @@ if config["enable"]["retrieve"] and config["enable"].get("retrieve_databundle", rule retrieve_jrc_idees: output: - directory("data/bundle/jrc-idees-2021"), + directory("data/jrc-idees-2021"), log: "logs/retrieve_jrc_idees.log", retries: 2 From 2c7e30bb1774b804efe88cb04c7f4ecad6f47469 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Tue, 6 Aug 2024 17:46:23 +0200 Subject: [PATCH 155/344] remove ammonia demand script --- rules/build_sector.smk | 2 +- rules/retrieve.smk | 9 ------- scripts/retrieve_ammonia_demand.py | 40 ------------------------------ 3 files changed, 1 insertion(+), 50 deletions(-) delete mode 100644 scripts/retrieve_ammonia_demand.py diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 62655b7b9..8e9cb4de1 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -429,7 +429,7 @@ rule build_salt_cavern_potentials: rule build_ammonia_production: input: - usgs="data/bundle/myb1-2022-nitro-ert.xlsx", + usgs=storage("https://d9-wret.s3.us-west-2.amazonaws.com/assets/palladium/production/s3fs-public/media/files/myb1-2022-nitro-ert.xlsx"), output: ammonia_production=resources("ammonia_production.csv"), threads: 1 diff --git a/rules/retrieve.smk b/rules/retrieve.smk index 8d0615db4..b5950b92d 100644 --- a/rules/retrieve.smk +++ b/rules/retrieve.smk @@ -23,7 +23,6 @@ if config["enable"]["retrieve"] and config["enable"].get("retrieve_databundle", "corine/g250_clc06_V18_5.tif", "eea/UNFCCC_v23.csv", "nuts/NUTS_RG_10M_2013_4326_LEVL_2.geojson", - "myb1-2017-nitro.xls", "emobility/KFZ__count", "emobility/Pkw__count", "h2_salt_caverns_GWh_per_sqkm.geojson", @@ -65,14 +64,6 @@ if config["enable"]["retrieve"] and config["enable"].get("retrieve_databundle", script: "../scripts/retrieve_jrc_idees.py" - rule retrieve_ammonia_demand: - output: - "data/bundle/myb1-2022-nitro-ert.xlsx", - log: - "logs/retrieve_ammonia_demand.log", - retries: 2 - script: - "../scripts/retrieve_ammonia_demand.py" rule retrieve_eurostat_household_data: output: diff --git a/scripts/retrieve_ammonia_demand.py b/scripts/retrieve_ammonia_demand.py deleted file mode 100644 index 56e326bb5..000000000 --- a/scripts/retrieve_ammonia_demand.py +++ /dev/null @@ -1,40 +0,0 @@ -# -*- coding: utf-8 -*- -# SPDX-FileCopyrightText: : 2024- The PyPSA-Eur Authors -# -# SPDX-License-Identifier: MIT -""" -Retrieve ammonia demand from https://www.usgs.gov/centers/national-minerals-information-center/nitrogen-statistics-and-information. -""" - -import logging -import os -import zipfile -from pathlib import Path - -from _helpers import configure_logging, progress_retrieve, set_scenario_config - -logger = logging.getLogger(__name__) - -# Define the base URL -url = "https://d9-wret.s3.us-west-2.amazonaws.com/assets/palladium/production/s3fs-public/media/files/myb1-2022-nitro-ert.xlsx" - -if __name__ == "__main__": - if "snakemake" not in globals(): - from _helpers import mock_snakemake - - snakemake = mock_snakemake("retrieve_ammonia_demand") - rootpath = ".." - else: - rootpath = "." - - configure_logging(snakemake) - set_scenario_config(snakemake) - disable_progress = snakemake.config["run"].get("disable_progressbar", False) - - to_fn = snakemake.output[0] - - # download .zip file - logger.info(f"Downloading Ammonia demand from {url}.") - progress_retrieve(url, to_fn, disable=disable_progress) - - logger.info(f"Ammonia demand data available in '{to_fn}'.") From 5725e8bf22818fc771d2c4ec054a91aa6894c544 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Tue, 6 Aug 2024 17:47:35 +0200 Subject: [PATCH 156/344] remove comment about uk flights --- scripts/build_energy_totals.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index 2afd7bb99..a0dc07fdc 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -492,7 +492,6 @@ def idees_per_country(ct: str, base_dir: str) -> pd.DataFrame: assert df.index[2] == "Domestic" ct_totals["total domestic aviation passenger"] = df.iloc[2] - # TODO added Ukraine to intra EU flights assert df.index[6] == "International - Intra-EEAwUK" assert df.index[7] == "International - Extra-EEAwUK" ct_totals["total international aviation passenger"] = df.iloc[[6, 7]].sum() From a9d991f547caba1cf76d96a6d2dd05a839e104d0 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Tue, 6 Aug 2024 17:51:10 +0200 Subject: [PATCH 157/344] fix release notes --- doc/release_notes.rst | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index f26ec91c7..201038141 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -9,10 +9,15 @@ Release Notes Upcoming Release ================ +* Update JRC-IDEES-2015 to `JRC-IDEES-2021 `__. The reference year is changed from 2015 to 2019. -* Update JRC-IDEES-2015 to `JRC-IDEES-2021 `__. +* split solid biomass potentials into solid biomass and municipal solid waste. Add option to use municipal solid waste. This option is only activated in combination with the flag ``waste_to_energy`` -* Update Ammonia production from USGS to 2022 `data `__. +* Add option to import solid biomass + +* Add option to produce electrobiofuels (flag ``electrobiofuels``) from solid biomass and hydrogen, as a combination of BtL and Fischer-Tropsch to make more use of the biogenic carbon + +* Add flag ``sector: fossil_fuels`` in config to remove the option of importing fossil fuels * Renamed the carrier of batteries in BEVs from `battery storage` to `EV battery` and the corresponding bus carrier from `Li ion` to `EV battery`. This is to avoid confusion with stationary battery storage. @@ -45,8 +50,25 @@ Upcoming Release * Bugfix: Impose minimum value of zero for district heating progress between current and future market share in :mod:`build_district_heat_share`. +* The ``{scope}`` wildcard was removed, since its outputs were not used. + * Enable parallelism in :mod:`determine_availability_matrix_MD_UA.py` and remove plots. This requires the use of temporary files. +* Updated pre-built `weather data cutouts + `__. These are now merged cutouts with + solar irradiation from the new SARAH-3 dataset while taking all other + variables from ERA5. Cutouts are now available for multiple years (2010, 2013, + 2019, and 2023). + +* Added option ``solving: curtailment_mode``` which fixes the dispatch profiles + of generators with time-varying p_max_pu by setting ``p_min_pu = p_max_pu`` + and adds an auxiliary curtailment generator with negative sign (to absorb + excess power) at every AC bus. This can speed up the solving process as the + curtailment decision is aggregated into a single generator per region. + +* In :mod:`base_network`, replace own voronoi polygon calculation function with + Geopandas `gdf.voronoi_polygons` method. + PyPSA-Eur 0.11.0 (25th May 2024) ===================================== From 3c528b7b0ac29ba85159fe9a80e51ce2c5f7310a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 6 Aug 2024 15:51:56 +0000 Subject: [PATCH 158/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- rules/build_sector.smk | 4 +++- rules/retrieve.smk | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 8e9cb4de1..b7ce92651 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -429,7 +429,9 @@ rule build_salt_cavern_potentials: rule build_ammonia_production: input: - usgs=storage("https://d9-wret.s3.us-west-2.amazonaws.com/assets/palladium/production/s3fs-public/media/files/myb1-2022-nitro-ert.xlsx"), + usgs=storage( + "https://d9-wret.s3.us-west-2.amazonaws.com/assets/palladium/production/s3fs-public/media/files/myb1-2022-nitro-ert.xlsx" + ), output: ammonia_production=resources("ammonia_production.csv"), threads: 1 diff --git a/rules/retrieve.smk b/rules/retrieve.smk index b5950b92d..1dde3da38 100644 --- a/rules/retrieve.smk +++ b/rules/retrieve.smk @@ -64,7 +64,6 @@ if config["enable"]["retrieve"] and config["enable"].get("retrieve_databundle", script: "../scripts/retrieve_jrc_idees.py" - rule retrieve_eurostat_household_data: output: "data/eurostat/eurostat-household_energy_balances-february_2024.csv", From 24f05f51d2af1f4da59c3c636b3917b20d6b0e8f Mon Sep 17 00:00:00 2001 From: lisazeyen <35347358+lisazeyen@users.noreply.github.com> Date: Wed, 7 Aug 2024 09:56:26 +0200 Subject: [PATCH 159/344] fix intersphinx mapping --- doc/conf.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 5c4b3b89c..a166dd70e 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -341,4 +341,6 @@ # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {"https://docs.python.org/": None} +intersphinx_mapping = { + 'https://docs.python.org/': ('https://docs.python.org/3', None), +} From 682d53d1b5618915945a782f42364ff7117f33ce Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Wed, 7 Aug 2024 10:07:39 +0200 Subject: [PATCH 160/344] remove /bundle --- rules/build_sector.smk | 8 ++++---- scripts/build_energy_totals.py | 4 +--- .../build_industrial_energy_demand_per_country_today.py | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/rules/build_sector.smk b/rules/build_sector.smk index b7ce92651..c89e3cd69 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -282,7 +282,7 @@ rule build_energy_totals: co2="data/bundle/eea/UNFCCC_v23.csv", swiss="data/switzerland-new_format-all_years.csv", swiss_transport="data/gr-e-11.03.02.01.01-cc.csv", - idees="data/bundle/jrc-idees-2021", + idees="data/jrc-idees-2021", district_heat_share="data/district_heat_share.csv", eurostat="data/eurostat/Balances-April2023", eurostat_households="data/eurostat/eurostat-household_energy_balances-february_2024.csv", @@ -453,7 +453,7 @@ rule build_industry_sector_ratios: ammonia=config_provider("sector", "ammonia", default=False), input: ammonia_production=resources("ammonia_production.csv"), - idees="data/bundle/jrc-idees-2021", + idees="data/jrc-idees-2021", output: industry_sector_ratios=resources("industry_sector_ratios.csv"), threads: 1 @@ -503,7 +503,7 @@ rule build_industrial_production_per_country: countries=config_provider("countries"), input: ammonia_production=resources("ammonia_production.csv"), - jrc="data/bundle/jrc-idees-2021", + jrc="data/jrc-idees-2021", eurostat="data/eurostat/Balances-April2023", output: industrial_production_per_country=resources( @@ -650,7 +650,7 @@ rule build_industrial_energy_demand_per_country_today: countries=config_provider("countries"), industry=config_provider("industry"), input: - jrc="data/bundle/jrc-idees-2021", + jrc="data/jrc-idees-2021", industrial_production_per_country=resources( "industrial_production_per_country.csv" ), diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index a0dc07fdc..229eb1b1f 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -25,7 +25,7 @@ - `data/bundle/eea_UNFCCC_v23.csv`: CO2 emissions data from EEA. - `data/switzerland-new_format-all_years.csv`: Swiss energy data. - `data/gr-e-11.03.02.01.01-cc.csv`: Swiss transport data -- `data/bundle/jrc-idees`: JRC IDEES data. +- `data/jrc-idees`: JRC IDEES data. - `data/district_heat_share.csv`: District heating shares. - `data/eurostat/Balances-April2023`: Eurostat energy balances. - `data/eurostat/eurostat-household_energy_balances-february_2024.csv`: Eurostat household energy balances. @@ -496,8 +496,6 @@ def idees_per_country(ct: str, base_dir: str) -> pd.DataFrame: assert df.index[7] == "International - Extra-EEAwUK" ct_totals["total international aviation passenger"] = df.iloc[[6, 7]].sum() - # TODO freight changed from "Domestic and International - Intra-EU" -> split - # domestic and international (intra-EU and outside EU) assert df.index[9] == "Domestic" ct_totals["total domestic aviation freight"] = df.iloc[9] diff --git a/scripts/build_industrial_energy_demand_per_country_today.py b/scripts/build_industrial_energy_demand_per_country_today.py index 8f63795eb..fa82a6603 100644 --- a/scripts/build_industrial_energy_demand_per_country_today.py +++ b/scripts/build_industrial_energy_demand_per_country_today.py @@ -8,7 +8,7 @@ Inputs ------- -- ``data/bundle/jrc-idees-2021`` +- ``data/jrc-idees-2021`` - ``industrial_production_per_country.csv`` Outputs From 8794f530504cc2a2c1734402e818c88e16508687 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Wed, 7 Aug 2024 10:11:07 +0200 Subject: [PATCH 161/344] rename eurostat_2015 --- scripts/build_energy_totals.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index 229eb1b1f..9fd8a1f1b 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -1252,9 +1252,9 @@ def rescale_idees_from_eurostat( main_cols = ["Total all products", "Electricity"] # read in the eurostat data for 2015 - eurostat_2015 = eurostat.xs(2021, level="year")[main_cols] + eurostat_2021 = eurostat.xs(2021, level="year")[main_cols] # calculate the ratio of the two data sets - ratio = eurostat[main_cols] / eurostat_2015 + ratio = eurostat[main_cols] / eurostat_2021 ratio = ratio.droplevel([2, 5]) cols_rename = {"Total all products": "total", "Electricity": "ele"} index_rename = {v: k for k, v in idees_rename.items()} From 59574211a453f18f7d0c63868273612ac1ccfc69 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 7 Aug 2024 10:15:35 +0200 Subject: [PATCH 162/344] retrieve.smk: consistently use conda directive --- rules/retrieve.smk | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rules/retrieve.smk b/rules/retrieve.smk index 18b0ddd22..dcfbd1af2 100644 --- a/rules/retrieve.smk +++ b/rules/retrieve.smk @@ -53,6 +53,8 @@ if config["enable"]["retrieve"] and config["enable"].get("retrieve_databundle", log: "logs/retrieve_eurostat_data.log", retries: 2 + conda: + "../envs/retrieve.yaml" script: "../scripts/retrieve_eurostat_data.py" @@ -62,6 +64,8 @@ if config["enable"]["retrieve"] and config["enable"].get("retrieve_databundle", log: "logs/retrieve_eurostat_household_data.log", retries: 2 + conda: + "../envs/retrieve.yaml" script: "../scripts/retrieve_eurostat_household_data.py" From 9c32932eee710bb2cb2d549bd4ce6f9a690d5664 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 7 Aug 2024 08:18:38 +0000 Subject: [PATCH 163/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- doc/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index a166dd70e..efce867ed 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -342,5 +342,5 @@ # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { - 'https://docs.python.org/': ('https://docs.python.org/3', None), + "https://docs.python.org/": ("https://docs.python.org/3", None), } From 6b3fc183866277bc856fd55b452c476bfdaeff36 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Wed, 7 Aug 2024 10:33:47 +0200 Subject: [PATCH 164/344] include review fneum build_industrial_production_per_country --- scripts/build_industrial_production_per_country.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/scripts/build_industrial_production_per_country.py b/scripts/build_industrial_production_per_country.py index 9c5433036..4a9c43ede 100644 --- a/scripts/build_industrial_production_per_country.py +++ b/scripts/build_industrial_production_per_country.py @@ -202,13 +202,12 @@ def get_energy_ratio(country, eurostat_dir, jrc_dir, year): else: ct_eurostat = country.replace("GB", "UK") if ct_eurostat == "UK": - year = 2019 logger.info("Assume Eurostat data for GB from 2019.") # estimate physical output, energy consumption in the sector and country fn = f"{eurostat_dir}/{ct_eurostat}-Energy-balance-sheets-April-2023-edition.xlsb" df = pd.read_excel( fn, - sheet_name=str(min(2021, year)), + sheet_name=str(min(2019, year)), index_col=2, header=0, skiprows=4, @@ -292,7 +291,7 @@ def separate_basic_chemicals(demand, year): """ Separate basic chemicals into ammonia, chlorine, methanol and HVC. """ - # ammonia data from 2017-2021 + # ammonia data from 2018-2022 ammonia = pd.read_csv(snakemake.input.ammonia_production, index_col=0) there = ammonia.index.intersection(demand.index) @@ -304,7 +303,7 @@ def separate_basic_chemicals(demand, year): year_to_use = min(max(year, 2018), 2022) if year_to_use != year: - logger.info(f"Using data from {year_to_use} for ammonia production.") + logger.info(f"Year {year} outside data range. Using data from {year_to_use} for ammonia production.") demand.loc[there, "Ammonia"] = ammonia.loc[there, str(year_to_use)] demand["Basic chemicals"] -= demand["Ammonia"] From 7848e08bc77be21b58db1b569e973e55874a0b4a Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Wed, 7 Aug 2024 10:40:23 +0200 Subject: [PATCH 165/344] create previous idees split for aviation --- scripts/build_energy_totals.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index 9fd8a1f1b..0a4074382 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -497,11 +497,12 @@ def idees_per_country(ct: str, base_dir: str) -> pd.DataFrame: ct_totals["total international aviation passenger"] = df.iloc[[6, 7]].sum() assert df.index[9] == "Domestic" - ct_totals["total domestic aviation freight"] = df.iloc[9] - assert df.index[10] == "International - Intra-EEAwUK" + ct_totals["total domestic aviation freight"] = df.iloc[[9,10]].sum() + + assert df.index[11] == "International - Extra-EEAwUK" - ct_totals["total international aviation freight"] = df.iloc[[10, 11]].sum() + ct_totals["total international aviation freight"] = df.iloc[11].sum() ct_totals["total domestic aviation"] = ( ct_totals["total domestic aviation freight"] From 7e75aab29537226dc348b27c31e26dac23ab23ed Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Wed, 7 Aug 2024 10:47:22 +0200 Subject: [PATCH 166/344] directly read in nan values --- scripts/build_energy_totals.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index 0a4074382..e508bd54b 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -170,6 +170,7 @@ def eurostat_per_country(input_eurostat: str, country: str) -> pd.DataFrame: sheet_name=None, skiprows=4, index_col=list(range(4)), + na_values=":" ) sheet.pop("Cover") return pd.concat(sheet) @@ -502,7 +503,7 @@ def idees_per_country(ct: str, base_dir: str) -> pd.DataFrame: assert df.index[11] == "International - Extra-EEAwUK" - ct_totals["total international aviation freight"] = df.iloc[11].sum() + ct_totals["total international aviation freight"] = df.iloc[11] ct_totals["total domestic aviation"] = ( ct_totals["total domestic aviation freight"] @@ -607,8 +608,6 @@ def fill_missing_years(fill_values: pd.Series) -> pd.Series: - Zero values in the original Series are replaced by the ffilled and bfilled value of their respective country group. """ - # Replace zero values with NaN for correct filling - fill_values = fill_values.replace(0, pd.NA) # Forward fill and then backward fill within each country group fill_values = fill_values.groupby(level="country").ffill().bfill() From 5bf36c218c3bd25f04d5fb01e699e503b5523b7f Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Wed, 7 Aug 2024 11:49:33 +0200 Subject: [PATCH 167/344] add ch data as csv --- ...ch_industrial_production_per_subsector.csv | 16 +++++++ rules/build_sector.smk | 1 + ...build_industrial_production_per_country.py | 43 ++++++++----------- 3 files changed, 36 insertions(+), 24 deletions(-) create mode 100644 data/ch_industrial_production_per_subsector.csv diff --git a/data/ch_industrial_production_per_subsector.csv b/data/ch_industrial_production_per_subsector.csv new file mode 100644 index 000000000..acb7ed81f --- /dev/null +++ b/data/ch_industrial_production_per_subsector.csv @@ -0,0 +1,16 @@ +sector,2014,2015,2016,2017,2018,2019,2020,2021,2022,2023 +Nahrung,19035,17728,16486,16782,16261,17062,17132,17335,15625,15713 +Textil / Leder,1845,1742,1450,1497,1433,1488,1331,1496,1371,1112 +Papier / Druck,12407,12257,11100,11086,9530,9436,8482,9186,8850,7657 +Chemie / Pharma,27253,27050,27501,26953,26506,26055,24889,25595,25028,23240 +Zement / Beton,15513,13307,13576,13429,13263,13221,12612,12843,12919,11400 +Andere NE-Mineralien,4029,3820,3884,4254,3546,3577,3475,3602,3603,3178 +Metall / Eisen,7841,7889,7638,8049,8053,7188,7051,6872,6782,5531 +NE-Metall,3386,3037,2833,2813,2743,2931,2719,2992,2896,2653 +Metall / Geräte,14652,14993,14272,14689,14300,15037,13946,15042,13862,12578 +Maschinen,4561,4724,4861,4957,4365,4364,4061,4290,3698,3545 +Andere Industrien,10750,10825,10225,9833,9689,9545,8696,9371,8823,8157 +current electricity,55142,53760,51302,52173,51604,51389,48933,51730,50926,46969 +,,,,,,,,,, +,,,,,,,,,, +"source: https://pubdb.bfe.admin.ch/de/publication/download/11817, accessed August 2024",,,,,,,,,, diff --git a/rules/build_sector.smk b/rules/build_sector.smk index c89e3cd69..4227422c8 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -502,6 +502,7 @@ rule build_industrial_production_per_country: industry=config_provider("industry"), countries=config_provider("countries"), input: + ch_industrial_production="data/ch_industrial_production_per_subsector.csv", ammonia_production=resources("ammonia_production.csv"), jrc="data/jrc-idees-2021", eurostat="data/eurostat/Balances-April2023", diff --git a/scripts/build_industrial_production_per_country.py b/scripts/build_industrial_production_per_country.py index 4a9c43ede..5b691be60 100644 --- a/scripts/build_industrial_production_per_country.py +++ b/scripts/build_industrial_production_per_country.py @@ -31,7 +31,7 @@ This dataset provides detailed information about the consumption of energy for various processes. If the country is not part of the EU28, the energy consumption in the industrial sectors is taken from the `Eurostat ` dataset. The industrial production is calculated for the year specified in the config["industry"]["reference_year"]. -The ammonia production is provided by the rule `build_ammonia_production `. Since Switzerland is not part of the EU28 nor reported by eurostat, the energy consumption in the industrial sectors is taken from the `BFE dataset. +The ammonia production is provided by the rule `build_ammonia_production `. Since Switzerland is not part of the EU28 nor reported by eurostat, the energy consumption in the industrial sectors is taken from the `BFE dataset. After the industrial production is calculated, the basic chemicals are separated into ammonia, chlorine, methanol and HVC. The production of these chemicals is assumed to be proportional to the production of basic chemicals without ammonia. The following subcategories [kton/a] are considered: @@ -167,27 +167,19 @@ } -# TODO: this should go in a csv in `data` -# Annual energy consumption in Switzerland by sector in 2015 (in TJ) -# From: Energieverbrauch in der Industrie und im Dienstleistungssektor, Der Bundesrat -# http://www.bfe.admin.ch/themen/00526/00541/00543/index.html?lang=de&dossier_id=00775 -e_switzerland = pd.Series( - { - "Iron and steel": 7889.0, - "Chemical industry": 26871.0, - "Non-metallic mineral products": 15513.0 + 3820.0, - "Pulp, paper and printing": 12004.0, - "Food, beverages and tobacco": 17728.0, - "Non Ferrous Metals": 3037.0, - "Transport equipment": 14993.0, - "Machinery equipment": 4724.0, - "Textiles and leather": 1742.0, - "Wood and wood products": 0.0, - "Other industrial sectors": 10825.0, - "current electricity": 53760.0, +ch_mapping = { + "Nahrung": "Food, beverages and tobacco", + "Textil / Leder": "Textiles and leather", + "Papier / Druck": "Pulp, paper and printing", + "Chemie / Pharma": "Chemical industry", + "Zement / Beton": "Non-metallic mineral products", + "Andere NE-Mineralien": "Other non-ferrous metals", + "Metall / Eisen": "Iron and steel", + "NE-Metall": "Non Ferrous Metals", + "Metall / Geräte" : "Transport equipment", + "Maschinen": "Machinery equipment", + "Andere Industrien": "Other industrial sectors", } -) - def find_physical_output(df): start = np.where(df.index.str.contains("Physical output", na=""))[0][0] @@ -198,11 +190,14 @@ def find_physical_output(df): def get_energy_ratio(country, eurostat_dir, jrc_dir, year): if country == "CH": - e_country = e_switzerland * tj_to_ktoe + # data ranges between 2014-2023 + e_country = pd.read_csv(snakemake.input.ch_industrial_production, + index_col=0).dropna() + e_country = e_country.rename(index=ch_mapping).groupby(level=0).sum() + e_country = e_country[str(min(2019, year))] + e_country *= tj_to_ktoe else: ct_eurostat = country.replace("GB", "UK") - if ct_eurostat == "UK": - logger.info("Assume Eurostat data for GB from 2019.") # estimate physical output, energy consumption in the sector and country fn = f"{eurostat_dir}/{ct_eurostat}-Energy-balance-sheets-April-2023-edition.xlsb" df = pd.read_excel( From 6a51a1d94ed636bb5167ced7e75fb6945ad2d18f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 7 Aug 2024 09:59:06 +0000 Subject: [PATCH 168/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- doc/conf.py | 2 +- scripts/build_energy_totals.py | 5 ++--- ...build_industrial_production_per_country.py | 20 +++++++++++-------- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index a166dd70e..efce867ed 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -342,5 +342,5 @@ # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { - 'https://docs.python.org/': ('https://docs.python.org/3', None), + "https://docs.python.org/": ("https://docs.python.org/3", None), } diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index e508bd54b..d8c2f5212 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -170,7 +170,7 @@ def eurostat_per_country(input_eurostat: str, country: str) -> pd.DataFrame: sheet_name=None, skiprows=4, index_col=list(range(4)), - na_values=":" + na_values=":", ) sheet.pop("Cover") return pd.concat(sheet) @@ -499,9 +499,8 @@ def idees_per_country(ct: str, base_dir: str) -> pd.DataFrame: assert df.index[9] == "Domestic" assert df.index[10] == "International - Intra-EEAwUK" - ct_totals["total domestic aviation freight"] = df.iloc[[9,10]].sum() + ct_totals["total domestic aviation freight"] = df.iloc[[9, 10]].sum() - assert df.index[11] == "International - Extra-EEAwUK" ct_totals["total international aviation freight"] = df.iloc[11] diff --git a/scripts/build_industrial_production_per_country.py b/scripts/build_industrial_production_per_country.py index 5b691be60..ccb4feca6 100644 --- a/scripts/build_industrial_production_per_country.py +++ b/scripts/build_industrial_production_per_country.py @@ -172,14 +172,15 @@ "Textil / Leder": "Textiles and leather", "Papier / Druck": "Pulp, paper and printing", "Chemie / Pharma": "Chemical industry", - "Zement / Beton": "Non-metallic mineral products", + "Zement / Beton": "Non-metallic mineral products", "Andere NE-Mineralien": "Other non-ferrous metals", "Metall / Eisen": "Iron and steel", "NE-Metall": "Non Ferrous Metals", - "Metall / Geräte" : "Transport equipment", + "Metall / Geräte": "Transport equipment", "Maschinen": "Machinery equipment", - "Andere Industrien": "Other industrial sectors", - } + "Andere Industrien": "Other industrial sectors", +} + def find_physical_output(df): start = np.where(df.index.str.contains("Physical output", na=""))[0][0] @@ -191,11 +192,12 @@ def find_physical_output(df): def get_energy_ratio(country, eurostat_dir, jrc_dir, year): if country == "CH": # data ranges between 2014-2023 - e_country = pd.read_csv(snakemake.input.ch_industrial_production, - index_col=0).dropna() + e_country = pd.read_csv( + snakemake.input.ch_industrial_production, index_col=0 + ).dropna() e_country = e_country.rename(index=ch_mapping).groupby(level=0).sum() e_country = e_country[str(min(2019, year))] - e_country *= tj_to_ktoe + e_country *= tj_to_ktoe else: ct_eurostat = country.replace("GB", "UK") # estimate physical output, energy consumption in the sector and country @@ -298,7 +300,9 @@ def separate_basic_chemicals(demand, year): year_to_use = min(max(year, 2018), 2022) if year_to_use != year: - logger.info(f"Year {year} outside data range. Using data from {year_to_use} for ammonia production.") + logger.info( + f"Year {year} outside data range. Using data from {year_to_use} for ammonia production." + ) demand.loc[there, "Ammonia"] = ammonia.loc[there, str(year_to_use)] demand["Basic chemicals"] -= demand["Ammonia"] From 80603fda6eef226ca48de1b037150bf9faa33495 Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Wed, 7 Aug 2024 13:33:59 +0200 Subject: [PATCH 169/344] update config and script --- config/config.default.yaml | 15 +++++---------- rules/build_sector.smk | 2 ++ scripts/build_cop_profiles/run.py | 30 +++++++++++++++++------------- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index f7ee67162..126107718 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -410,18 +410,13 @@ sector: 2045: 0.8 2050: 1.0 district_heating_loss: 0.15 + # check these numbers! forward_temperature: - default: 90 - DK: 70 - SE: 70 - NO: 70 - FI: 70 + DE: 90 #C + DK: 60 #C return_temperature: - default: 50 - DK: 40 - SE: 40 - NO: 40 - FI: 40 + DE: 50 #C + DK: 40 #C heat_source_cooling: 6 #K heat_pump_cop_approximation: refrigerant: ammonia diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 5916aa02b..c7796babc 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -242,9 +242,11 @@ rule build_cop_profiles: "sector", "district_heating", "heat_pump_cop_approximation" ), heat_pump_sources=config_provider("sector", "heat_pump_sources"), + snapshots=config_provider("snapshots"), input: temp_soil_total=resources("temp_soil_total_elec_s{simpl}_{clusters}.nc"), temp_air_total=resources("temp_air_total_elec_s{simpl}_{clusters}.nc"), + regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), output: cop_profiles=resources("cop_profiles_elec_s{simpl}_{clusters}.nc"), resources: diff --git a/scripts/build_cop_profiles/run.py b/scripts/build_cop_profiles/run.py index 44bca6ebf..69e2b5199 100644 --- a/scripts/build_cop_profiles/run.py +++ b/scripts/build_cop_profiles/run.py @@ -8,6 +8,7 @@ import numpy as np import pandas as pd import xarray as xr +import geopandas as gpd from _helpers import get_country_from_node_name, set_scenario_config from CentralHeatingCopApproximator import CentralHeatingCopApproximator from DecentralHeatingCopApproximator import DecentralHeatingCopApproximator @@ -17,8 +18,8 @@ def map_temperature_dict_to_onshore_regions( supply_temperature_by_country: dict, - onshore_regions: xr.DataArray, - snapshots: xr.DataArray, + regions_onshore: pd.Index, + snapshots: pd.DatetimeIndex, ) -> xr.DataArray: """ Map dictionary of temperatures to onshore regions. @@ -27,8 +28,10 @@ def map_temperature_dict_to_onshore_regions( ---------- supply_temperature_by_country : dictionary Dictionary with temperatures as values and country keys as keys. One key must be named "default" - onshore_regions : xr.DataArray + regions_onshore : pd.Index Names of onshore regions + snapshots : pd.DatetimeIndex + Time stamps Returns: ------- @@ -44,13 +47,13 @@ def map_temperature_dict_to_onshore_regions( in supply_temperature_by_country.keys() else supply_temperature_by_country["default"] ) - for node_name in onshore_regions["name"].values + for node_name in regions_onshore.values ] # pass both nodes and snapshots as dimensions to preserve correct data structure - for _ in snapshots["time"].values + for _ in snapshots ], dims=["time", "name"], - coords={"time": snapshots["time"], "name": onshore_regions["name"]}, + coords={"time": snapshots, "name": regions_onshore}, ) @@ -108,19 +111,20 @@ def get_cop( set_scenario_config(snakemake) # map forward and return temperatures specified on country-level to onshore regions - onshore_regions: xr.DataArray = source_inlet_temperature_celsius["name"] + regions_onshore = gpd.read_file(snakemake.input.regions_onshore)["name"] + snapshots = pd.date_range(freq="h", **snakemake.params.snapshots) forward_temperature_central_heating_by_node_and_time: xr.DataArray = ( map_temperature_dict_to_onshore_regions( - temperature_dict=snakemake.params.forward_temperature_central_heating, - onshore_regions=onshore_regions, - snapshots=source_inlet_temperature_celsius["time"], + supply_temperature_by_country=snakemake.params.forward_temperature_central_heating, + regions_onshore=regions_onshore, + snapshots=snapshots, ) ) return_temperature_central_heating_by_node_and_time: xr.DataArray = ( map_temperature_dict_to_onshore_regions( - temperature_dict=snakemake.params.return_temperature_central_heating, - onshore_regions=onshore_regions, - snapshots=source_inlet_temperature_celsius["time"], + supply_temperature_by_country=snakemake.params.return_temperature_central_heating, + regions_onshore=regions_onshore, + snapshots=snapshots, ) ) cop_all_system_types = [] From bab392f37896e249a8b45a8cecb9cd2de335b19a Mon Sep 17 00:00:00 2001 From: lisazeyen <35347358+lisazeyen@users.noreply.github.com> Date: Wed, 7 Aug 2024 14:14:19 +0200 Subject: [PATCH 170/344] remove not needed function --- rules/build_sector.smk | 9 --------- 1 file changed, 9 deletions(-) diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 5916aa02b..d1a29e832 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -215,15 +215,6 @@ rule build_temperature_profiles: "../scripts/build_temperature_profiles.py" -# def output_cop(wildcards): -# return { -# f"cop_{source}_{sink}": resources( -# "cop_" + source + "_" + sink + "_" + "elec_s{simpl}_{clusters}.nc" -# ) -# for sink, source in config["sector"]["heat_pump_sources"].items() -# } - - rule build_cop_profiles: params: heat_pump_sink_T_decentral_heating=config_provider( From 7f76bf8e84a7b0a962cf1c4c8bb186d4ae0103da Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 7 Aug 2024 12:37:44 +0000 Subject: [PATCH 171/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/build_cop_profiles/run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build_cop_profiles/run.py b/scripts/build_cop_profiles/run.py index 69e2b5199..d21b863d9 100644 --- a/scripts/build_cop_profiles/run.py +++ b/scripts/build_cop_profiles/run.py @@ -5,10 +5,10 @@ import sys +import geopandas as gpd import numpy as np import pandas as pd import xarray as xr -import geopandas as gpd from _helpers import get_country_from_node_name, set_scenario_config from CentralHeatingCopApproximator import CentralHeatingCopApproximator from DecentralHeatingCopApproximator import DecentralHeatingCopApproximator From ab6415bdc33253796cf4c3b9831a2144bc012ffc Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Wed, 7 Aug 2024 14:54:28 +0200 Subject: [PATCH 172/344] re-add default supply temperature --- config/config.default.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/config.default.yaml b/config/config.default.yaml index 126107718..aead3e3d6 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -412,9 +412,11 @@ sector: district_heating_loss: 0.15 # check these numbers! forward_temperature: + Default: 90 DE: 90 #C DK: 60 #C return_temperature: + Default: 50 DE: 50 #C DK: 40 #C heat_source_cooling: 6 #K From c907d59253e90b4aa8c54baaa54f4a44b67f8b4f Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 7 Aug 2024 15:20:08 +0200 Subject: [PATCH 173/344] remove unused rule `prepare_links_p_nom` (#1203) * remove rule * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- config/config.default.yaml | 1 - doc/configtables/enable.csv | 1 - doc/preparation.rst | 5 -- doc/release_notes.rst | 2 + rules/build_electricity.smk | 15 ------ scripts/prepare_links_p_nom.py | 95 ---------------------------------- 6 files changed, 2 insertions(+), 117 deletions(-) delete mode 100644 scripts/prepare_links_p_nom.py diff --git a/config/config.default.yaml b/config/config.default.yaml index 5229c385a..99655d726 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -67,7 +67,6 @@ snapshots: # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#enable enable: retrieve: auto - prepare_links_p_nom: false retrieve_databundle: true retrieve_cost_data: true build_cutout: false diff --git a/doc/configtables/enable.csv b/doc/configtables/enable.csv index c74d0eff2..0268319eb 100644 --- a/doc/configtables/enable.csv +++ b/doc/configtables/enable.csv @@ -1,6 +1,5 @@ ,Unit,Values,Description enable,str or bool,"{auto, true, false}","Switch to include (true) or exclude (false) the retrieve_* rules of snakemake into the workflow; 'auto' sets true|false based on availability of an internet connection to prevent issues with snakemake failing due to lack of internet connection." -prepare_links_p_nom,bool,"{true, false}","Switch to retrieve current HVDC projects from `Wikipedia `_" retrieve_databundle,bool,"{true, false}","Switch to retrieve databundle from zenodo via the rule :mod:`retrieve_databundle` or whether to keep a custom databundle located in the corresponding folder." retrieve_cost_data,bool,"{true, false}","Switch to retrieve technology cost data from `technology-data repository `_." build_cutout,bool,"{true, false}","Switch to enable the building of cutouts via the rule :mod:`build_cutout`." diff --git a/doc/preparation.rst b/doc/preparation.rst index 4585f4dba..83f9781c2 100644 --- a/doc/preparation.rst +++ b/doc/preparation.rst @@ -41,11 +41,6 @@ Rule ``build_cutout`` .. automodule:: build_cutout -Rule ``prepare_links_p_nom`` -=============================== - -.. automodule:: prepare_links_p_nom - .. _base: Rule ``base_network`` diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 9ce418b21..528d94df4 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -10,6 +10,8 @@ Release Notes Upcoming Release ================ +* The rule ``prepare_links_p_nom`` was removed since it was outdated and not used. + * Changed heat pump COP approximation for central heating to be based on `Jensen et al. (2018) `__ and a default forward temperature of 90C. This is more realistic for district heating than the previously used approximation method. * split solid biomass potentials into solid biomass and municipal solid waste. Add option to use municipal solid waste. This option is only activated in combination with the flag ``waste_to_energy`` diff --git a/rules/build_electricity.smk b/rules/build_electricity.smk index 10e5dfc0c..64bd85a1d 100644 --- a/rules/build_electricity.smk +++ b/rules/build_electricity.smk @@ -2,21 +2,6 @@ # # SPDX-License-Identifier: MIT -if config["enable"].get("prepare_links_p_nom", False): - - rule prepare_links_p_nom: - output: - "data/links_p_nom.csv", - log: - logs("prepare_links_p_nom.log"), - threads: 1 - resources: - mem_mb=1500, - conda: - "../envs/environment.yaml" - script: - "../scripts/prepare_links_p_nom.py" - rule build_electricity_demand: params: diff --git a/scripts/prepare_links_p_nom.py b/scripts/prepare_links_p_nom.py deleted file mode 100644 index 7c1ed2117..000000000 --- a/scripts/prepare_links_p_nom.py +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# SPDX-FileCopyrightText: : 2017-2024 The PyPSA-Eur Authors -# -# SPDX-License-Identifier: MIT -""" -Extracts capacities of HVDC links from `Wikipedia. - -`_. - -Relevant Settings ------------------ - -.. code:: yaml - - enable: - prepare_links_p_nom: - -.. seealso:: - Documentation of the configuration file ``config/config.yaml`` at - :ref:`toplevel_cf` - -Inputs ------- - -*None* - -Outputs -------- - -- ``data/links_p_nom.csv``: A plain download of https://en.wikipedia.org/wiki/List_of_HVDC_projects#Europe plus extracted coordinates. - -Description ------------ - -*None* -""" - -import logging - -import pandas as pd -from _helpers import configure_logging, set_scenario_config - -logger = logging.getLogger(__name__) - - -def multiply(s): - return s.str[0].astype(float) * s.str[1].astype(float) - - -def extract_coordinates(s): - regex = ( - r"(\d{1,2})°(\d{1,2})′(\d{1,2})″(N|S) " r"(\d{1,2})°(\d{1,2})′(\d{1,2})″(E|W)" - ) - e = s.str.extract(regex, expand=True) - lat = ( - e[0].astype(float) + (e[1].astype(float) + e[2].astype(float) / 60.0) / 60.0 - ) * e[3].map({"N": +1.0, "S": -1.0}) - lon = ( - e[4].astype(float) + (e[5].astype(float) + e[6].astype(float) / 60.0) / 60.0 - ) * e[7].map({"E": +1.0, "W": -1.0}) - return lon, lat - - -if __name__ == "__main__": - if "snakemake" not in globals(): - from _helpers import mock_snakemake # rule must be enabled in config - - snakemake = mock_snakemake("prepare_links_p_nom", simpl="") - configure_logging(snakemake) - set_scenario_config(snakemake) - - links_p_nom = pd.read_html( - "https://en.wikipedia.org/wiki/List_of_HVDC_projects", header=0, match="SwePol" - )[0] - - mw = "Power (MW)" - m_b = links_p_nom[mw].str.contains("x").fillna(False) - - links_p_nom.loc[m_b, mw] = links_p_nom.loc[m_b, mw].str.split("x").pipe(multiply) - links_p_nom[mw] = ( - links_p_nom[mw].str.extract("[-/]?([\d.]+)", expand=False).astype(float) - ) - - links_p_nom["x1"], links_p_nom["y1"] = extract_coordinates( - links_p_nom["Converterstation 1"] - ) - links_p_nom["x2"], links_p_nom["y2"] = extract_coordinates( - links_p_nom["Converterstation 2"] - ) - - links_p_nom.dropna(subset=["x1", "y1", "x2", "y2"]).to_csv( - snakemake.output[0], index=False - ) From fb41016c605407b9af393c8c18425c6a1c5cfc2a Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 7 Aug 2024 15:28:55 +0200 Subject: [PATCH 174/344] EEZ: Update EEZ to v12, auto-download and remove from databundle (#1188) * eez: update to version 12, autodownload, remove pycountry * eez: do not simplify as it distorts topology * remove missed merge conflicts --- doc/conf.py | 1 - doc/requirements.txt | 1 - envs/environment.yaml | 1 - rules/build_electricity.smk | 2 +- rules/retrieve.smk | 36 +++++++++++++++++++++++++++++++++++- scripts/build_shapes.py | 33 +++++++++------------------------ 6 files changed, 45 insertions(+), 29 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index efce867ed..f0d1ca377 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -53,7 +53,6 @@ autodoc_mock_imports = [ "atlite", "snakemake", - "pycountry", "rioxarray", "country_converter", "tabula", diff --git a/doc/requirements.txt b/doc/requirements.txt index a1cd0a5c4..dca414fcb 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -17,7 +17,6 @@ tabula-py # cartopy scikit-learn -pycountry pyyaml seaborn memory_profiler diff --git a/envs/environment.yaml b/envs/environment.yaml index febd6ea2e..c8d8a6336 100644 --- a/envs/environment.yaml +++ b/envs/environment.yaml @@ -18,7 +18,6 @@ dependencies: # Dependencies of the workflow itself - xlrd - openpyxl!=3.1.1 -- pycountry - seaborn - snakemake-minimal>=8.14 - memory_profiler diff --git a/rules/build_electricity.smk b/rules/build_electricity.smk index 64bd85a1d..34472f27d 100644 --- a/rules/build_electricity.smk +++ b/rules/build_electricity.smk @@ -92,7 +92,7 @@ rule build_shapes: countries=config_provider("countries"), input: naturalearth=ancient("data/naturalearth/ne_10m_admin_0_countries_deu.shp"), - eez=ancient("data/bundle/eez/World_EEZ_v8_2014.shp"), + eez=ancient("data/eez/World_EEZ_v12_20231025_gpkg/eez_v12.gpkg"), nuts3=ancient("data/bundle/NUTS_2013_60M_SH/data/NUTS_RG_60M_2013.shp"), nuts3pop=ancient("data/bundle/nama_10r_3popgdp.tsv.gz"), nuts3gdp=ancient("data/bundle/nama_10r_3gdp.tsv.gz"), diff --git a/rules/retrieve.smk b/rules/retrieve.smk index aa4452156..ffb44baef 100644 --- a/rules/retrieve.smk +++ b/rules/retrieve.smk @@ -16,7 +16,6 @@ if config["enable"]["retrieve"] is False: if config["enable"]["retrieve"] and config["enable"].get("retrieve_databundle", True): datafiles = [ "je-e-21.03.02.xls", - "eez/World_EEZ_v8_2014.shp", "NUTS_2013_60M_SH/data/NUTS_RG_60M_2013.shp", "nama_10r_3popgdp.tsv.gz", "nama_10r_3gdp.tsv.gz", @@ -215,6 +214,41 @@ if config["enable"]["retrieve"]: move(input[0], output[0]) +if config["enable"]["retrieve"]: + + rule retrieve_eez: + params: + zip="data/eez/World_EEZ_v12_20231025_gpkg.zip", + output: + gpkg="data/eez/World_EEZ_v12_20231025_gpkg/eez_v12.gpkg", + run: + import os + import requests + from uuid import uuid4 + + name = str(uuid4())[:8] + org = str(uuid4())[:8] + + response = requests.post( + "https://www.marineregions.org/download_file.php", + params={"name": "World_EEZ_v12_20231025_gpkg.zip"}, + data={ + "name": name, + "organisation": org, + "email": f"{name}@{org}.org", + "country": "Germany", + "user_category": "academia", + "purpose_category": "Research", + "agree": "1", + }, + ) + + with open(params["zip"], "wb") as f: + f.write(response.content) + output_folder = Path(params["zip"]).parent + unpack_archive(params["zip"], output_folder) + os.remove(params["zip"]) + if config["enable"]["retrieve"]: # Download directly from naciscdn.org which is a redirect from naturalearth.com diff --git a/scripts/build_shapes.py b/scripts/build_shapes.py index 93a738582..c2fe7ce63 100644 --- a/scripts/build_shapes.py +++ b/scripts/build_shapes.py @@ -26,7 +26,7 @@ .. image:: img/countries.png :scale: 33 % -- ``data/bundle/eez/World_EEZ_v8_2014.shp``: World `exclusive economic zones `_ (EEZ) +- ``data/eez/World_EEZ_v12_20231025_gpkg/eez_v12.gpkg ``: World `exclusive economic zones `_ (EEZ) .. image:: img/eez.png :scale: 33 % @@ -76,19 +76,13 @@ import geopandas as gpd import numpy as np import pandas as pd -import pycountry as pyc +import country_converter as coco from _helpers import configure_logging, set_scenario_config from shapely.geometry import MultiPolygon, Polygon logger = logging.getLogger(__name__) - -def _get_country(target, **keys): - assert len(keys) == 1 - try: - return getattr(pyc.countries.get(**keys), target) - except (KeyError, AttributeError): - return np.nan +cc = coco.CountryConverter() def _simplify_polys(polys, minarea=0.1, tolerance=None, filterremote=True): @@ -135,22 +129,15 @@ def countries(naturalearth, country_list): return s -def eez(country_shapes, eez, country_list): +def eez(eez, country_list): df = gpd.read_file(eez) - df = df.loc[ - df["ISO_3digit"].isin( - [_get_country("alpha_3", alpha_2=c) for c in country_list] - ) - ] - df["name"] = df["ISO_3digit"].map(lambda c: _get_country("alpha_2", alpha_3=c)) + iso3_list = cc.convert(country_list, src="ISO2", to="ISO3") + df = df.query("ISO_TER1 in @iso3_list and POL_TYPE == '200NM'").copy() + df["name"] = cc.convert(df.ISO_TER1, src="ISO3", to="ISO2") s = df.set_index("name").geometry.map( lambda s: _simplify_polys(s, filterremote=False) ) - s = gpd.GeoSeries( - {k: v for k, v in s.items() if v.distance(country_shapes[k]) < 1e-3}, - crs=df.crs, - ) - s = s.to_frame("geometry") + s = s.to_frame("geometry").set_crs(df.crs) s.index.name = "name" return s @@ -262,9 +249,7 @@ def nuts3(country_shapes, nuts3, nuts3pop, nuts3gdp, ch_cantons, ch_popgdp): country_shapes = countries(snakemake.input.naturalearth, snakemake.params.countries) country_shapes.reset_index().to_file(snakemake.output.country_shapes) - offshore_shapes = eez( - country_shapes, snakemake.input.eez, snakemake.params.countries - ) + offshore_shapes = eez(snakemake.input.eez, snakemake.params.countries) offshore_shapes.reset_index().to_file(snakemake.output.offshore_shapes) europe_shape = gpd.GeoDataFrame( From f8d0efbe992413aac522851939dd7445f6084595 Mon Sep 17 00:00:00 2001 From: cpschau <124347782+cpschau@users.noreply.github.com> Date: Wed, 7 Aug 2024 17:52:00 +0200 Subject: [PATCH 175/344] Addition of unsustainable biomass potentials (#1139) * add columns to potential df defined by difference to eurostat * add network components * add unsustainable bioliquids * replaced stores by generators, still infeasible * remove municipal waste * remove separate treatment of waste from biomass potential calculation * phase out unsustainble biomass potentials * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * phase-out unsustainable bioliquids * remove waste_incineration from build_sector rule * multiple potential calculation for different planning horizons * raised costs of unsustainable solid biomass * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * stores instead of generators * change snakemake inputs * add phas-eout to config * add techcolor for unsustainable bioliquids * add config parameter to disable inclusion of unsustainable bioenergy potentials * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * add biomass to params * remove call of snakemake object in define_spatial * Quick resolve of review part 1 (config parameters, if-clause-reduction, bioliquid spatial, fix bioliquid link capacity * Quick resolve of review part 2 (config table, helper function, fixed build_eurostat, removed dir-change, forced unsustainable usage, reverted overnight distinction in Snakefile) * Cast of planning_horizon parameter to int type after test run * added JRC fuel costs for solid and liquid biofuels, BtL VOM * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * clean-up after master merge * adressed review (increase threads for build_eurostat, fixed e_max_pu of Stores, changed version of technology-data); added release note --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: lisazeyen <35347358+lisazeyen@users.noreply.github.com> --- config/config.default.yaml | 20 ++++- doc/configtables/biomass.csv | 2 + doc/release_notes.rst | 4 + rules/build_sector.smk | 6 +- rules/retrieve.smk | 2 + scripts/build_biomass_potentials.py | 135 +++++++++++++++++++++++++++- scripts/build_shapes.py | 2 +- scripts/prepare_sector_network.py | 100 +++++++++++++++++++++ 8 files changed, 265 insertions(+), 6 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 99655d726..2749ecb31 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -369,6 +369,23 @@ biomass: - Sludge municipal solid waste: - Municipal waste + share_unsustainable_use_retained: + 2020: 1 + 2025: 0.66 + 2030: 0.33 + 2035: 0 + 2040: 0 + 2045: 0 + 2050: 0 + share_sustainable_potential_available: + 2020: 0 + 2025: 0.33 + 2030: 0.66 + 2035: 1 + 2040: 1 + 2045: 1 + 2050: 1 + # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#solar-thermal solar_thermal: @@ -737,7 +754,7 @@ industry: # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#costs costs: year: 2030 - version: v0.9.0 + version: v0.9.1 social_discountrate: 0.02 fill_values: FOM: 0 @@ -1055,6 +1072,7 @@ plotting: services rural biomass boiler: '#c6cf98' services urban decentral biomass boiler: '#dde5b5' biomass to liquid: '#32CD32' + unsustainable bioliquids: '#32CD32' electrobiofuels: 'red' BioSNG: '#123456' # power transmission diff --git a/doc/configtables/biomass.csv b/doc/configtables/biomass.csv index f5b4841f2..865d247e2 100644 --- a/doc/configtables/biomass.csv +++ b/doc/configtables/biomass.csv @@ -5,3 +5,5 @@ classes ,,, -- solid biomass,--,Array of biomass comodity,The comodity that are included as solid biomass -- not included,--,Array of biomass comodity,The comodity that are not included as a biomass potential -- biogas,--,Array of biomass comodity,The comodity that are included as biogas +share_unsustainable_use_retained,--,Dictionary with planning horizons as keys., Share of unsustainable biomass use retained using primary production of Eurostat data as reference +share_sustainable_potential_available,--,Dictionary with planning horizons as keys., Share determines phase-in of ENSPRESO biomass potentials diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 528d94df4..7404e2ef4 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -10,6 +10,10 @@ Release Notes Upcoming Release ================ +* Added unsustainable biomass potentials for solid, gaseous, and liquid biomass. The potentials can be phased-out and/or + substituted by the phase-in of sustainable biomass types using the config parameters + ``biomass: share_unsustainable_use_retained`` and ``biomass: share_sustainable_potential_available``. + * The rule ``prepare_links_p_nom`` was removed since it was outdated and not used. * Changed heat pump COP approximation for central heating to be based on `Jensen et al. (2018) `__ and a default forward temperature of 90C. This is more realistic for district heating than the previously used approximation method. diff --git a/rules/build_sector.smk b/rules/build_sector.smk index d1a29e832..eb5c74338 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -345,7 +345,8 @@ rule build_biomass_potentials: "https://zenodo.org/records/10356004/files/ENSPRESO_BIOMASS.xlsx", keep_local=True, ), - nuts2="data/bundle/nuts/NUTS_RG_10M_2013_4326_LEVL_2.geojson", # https://gisco-services.ec.europa.eu/distribution/v2/nuts/download/#nuts21 + eurostat="data/eurostat/Balances-April2023", + nuts2="data/bundle/nuts/NUTS_RG_10M_2013_4326_LEVL_2.geojson", regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), nuts3_population=ancient("data/bundle/nama_10r_3popgdp.tsv.gz"), swiss_cantons=ancient("data/ch_cantons.csv"), @@ -358,7 +359,7 @@ rule build_biomass_potentials: biomass_potentials=resources( "biomass_potentials_s{simpl}_{clusters}_{planning_horizons}.csv" ), - threads: 1 + threads: 8 resources: mem_mb=1000, log: @@ -954,6 +955,7 @@ rule prepare_sector_network: countries=config_provider("countries"), adjustments=config_provider("adjustments", "sector"), emissions_scope=config_provider("energy", "emissions"), + biomass=config_provider("biomass"), RDIR=RDIR, heat_pump_sources=config_provider("sector", "heat_pump_sources"), heat_systems=config_provider("sector", "heat_systems"), diff --git a/rules/retrieve.smk b/rules/retrieve.smk index ffb44baef..86c6b9982 100644 --- a/rules/retrieve.smk +++ b/rules/retrieve.smk @@ -249,6 +249,8 @@ if config["enable"]["retrieve"]: unpack_archive(params["zip"], output_folder) os.remove(params["zip"]) + + if config["enable"]["retrieve"]: # Download directly from naciscdn.org which is a redirect from naturalearth.com diff --git a/scripts/build_biomass_potentials.py b/scripts/build_biomass_potentials.py index 883734ebc..0a2692e82 100644 --- a/scripts/build_biomass_potentials.py +++ b/scripts/build_biomass_potentials.py @@ -13,11 +13,51 @@ import numpy as np import pandas as pd from _helpers import configure_logging, set_scenario_config +from build_energy_totals import build_eurostat logger = logging.getLogger(__name__) AVAILABLE_BIOMASS_YEARS = [2010, 2020, 2030, 2040, 2050] +def _calc_unsustainable_potential(df, df_unsustainable, share_unsus, resource_type): + """ + Calculate the unsustainable biomass potential for a given resource type or + regex. + + Parameters + ---------- + df : pd.DataFrame + The dataframe with sustainable biomass potentials. + df_unsustainable : pd.DataFrame + The dataframe with unsustainable biomass potentials. + share_unsus : float + The share of unsustainable biomass potential retained. + resource_type : str or regex + The resource type to calculate the unsustainable potential for. + + Returns + ------- + pd.Series + The unsustainable biomass potential for the given resource type or regex. + """ + + if "|" in resource_type: + resource_potential = df_unsustainable.filter(regex=resource_type).sum(axis=1) + else: + resource_potential = df_unsustainable[resource_type] + + return ( + df.apply( + lambda c: c.sum() + / df.loc[df.index.str[:2] == c.name[:2]].sum().sum() + * resource_potential.loc[c.name[:2]], + axis=1, + ) + .mul(share_unsus) + .clip(lower=0) + ) + + def build_nuts_population_data(year=2013): pop = pd.read_csv( snakemake.input.nuts3_population, @@ -211,15 +251,104 @@ def convert_nuts2_to_regions(bio_nuts2, regions): return bio_regions +def add_unsustainable_potentials(df): + """ + Add unsustainable biomass potentials to the given dataframe. The difference + between the data of JRC and Eurostat is assumed to be unsustainable + biomass. + + Parameters + ---------- + df : pd.DataFrame + The dataframe with sustainable biomass potentials. + unsustainable_biomass : str + Path to the file with unsustainable biomass potentials. + + Returns + ------- + pd.DataFrame + The dataframe with added unsustainable biomass potentials. + """ + if "GB" in snakemake.config["countries"]: + latest_year = 2019 + else: + latest_year = 2021 + idees_rename = {"GR": "EL", "GB": "UK"} + df_unsustainable = ( + build_eurostat( + countries=snakemake.config["countries"], + input_eurostat=snakemake.input.eurostat, + nprocesses=int(snakemake.threads), + ) + .xs( + max(min(latest_year, int(snakemake.wildcards.planning_horizons)), 1990), + level=1, + ) + .xs("Primary production", level=2) + .droplevel([1, 2, 3]) + ) + + df_unsustainable.index = df_unsustainable.index.str.strip() + df_unsustainable = df_unsustainable.rename( + {v: k for k, v in idees_rename.items()}, axis=0 + ) + + bio_carriers = [ + "Primary solid biofuels", + "Biogases", + "Renewable municipal waste", + "Pure biogasoline", + "Blended biogasoline", + "Pure biodiesels", + "Blended biodiesels", + "Pure bio jet kerosene", + "Blended bio jet kerosene", + "Other liquid biofuels", + ] + + df_unsustainable = df_unsustainable[bio_carriers] + + # Phase out unsustainable biomass potentials linearly from 2020 to 2035 while phasing in sustainable potentials + share_unsus = params.get("share_unsustainable_use_retained").get(investment_year) + + df_wo_ch = df.drop(df.filter(regex="CH\d", axis=0).index) + + # Calculate unsustainable solid biomass + df_wo_ch["unsustainable solid biomass"] = _calc_unsustainable_potential( + df_wo_ch, df_unsustainable, share_unsus, "Primary solid biofuels" + ) + + # Calculate unsustainable biogas + df_wo_ch["unsustainable biogas"] = _calc_unsustainable_potential( + df_wo_ch, df_unsustainable, share_unsus, "Biogases" + ) + + # Calculate unsustainable bioliquids + df_wo_ch["unsustainable bioliquids"] = _calc_unsustainable_potential( + df_wo_ch, + df_unsustainable, + share_unsus, + resource_type="gasoline|diesel|kerosene|liquid", + ) + + share_sus = params.get("share_sustainable_potential_available").get(investment_year) + df *= share_sus + + df = df.join(df_wo_ch.filter(like="unsustainable")).fillna(0) + + return df + + if __name__ == "__main__": if "snakemake" not in globals(): + from _helpers import mock_snakemake snakemake = mock_snakemake( "build_biomass_potentials", simpl="", - clusters="5", - planning_horizons=2050, + clusters="37", + planning_horizons=2020, ) configure_logging(snakemake) @@ -269,6 +398,8 @@ def convert_nuts2_to_regions(bio_nuts2, regions): grouper = {v: k for k, vv in params["classes"].items() for v in vv} df = df.T.groupby(grouper).sum().T + df = add_unsustainable_potentials(df) + df *= 1e6 # TWh/a to MWh/a df.index.name = "MWh/a" diff --git a/scripts/build_shapes.py b/scripts/build_shapes.py index c2fe7ce63..411d56a4d 100644 --- a/scripts/build_shapes.py +++ b/scripts/build_shapes.py @@ -73,10 +73,10 @@ from itertools import takewhile from operator import attrgetter +import country_converter as coco import geopandas as gpd import numpy as np import pandas as pd -import country_converter as coco from _helpers import configure_logging, set_scenario_config from shapely.geometry import MultiPolygon, Polygon diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 585b3f258..c73373eee 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -64,6 +64,7 @@ def define_spatial(nodes, options): if options.get("biomass_spatial", options["biomass_transport"]): spatial.biomass.nodes = nodes + " solid biomass" + spatial.biomass.bioliquids = nodes + " bioliquids" spatial.biomass.locations = nodes spatial.biomass.industry = nodes + " solid biomass for industry" spatial.biomass.industry_cc = nodes + " solid biomass for industry CC" @@ -71,6 +72,7 @@ def define_spatial(nodes, options): spatial.msw.locations = nodes else: spatial.biomass.nodes = ["EU solid biomass"] + spatial.biomass.bioliquids = ["EU unsustainable bioliquids"] spatial.biomass.locations = ["EU"] spatial.biomass.industry = ["solid biomass for industry"] spatial.biomass.industry_cc = ["solid biomass for industry CC"] @@ -2262,8 +2264,14 @@ def add_biomass(n, costs): biogas_potentials_spatial = biomass_potentials["biogas"].rename( index=lambda x: x + " biogas" ) + unsustainable_biogas_potentials_spatial = biomass_potentials[ + "unsustainable biogas" + ].rename(index=lambda x: x + " biogas") else: biogas_potentials_spatial = biomass_potentials["biogas"].sum() + unsustainable_biogas_potentials_spatial = biomass_potentials[ + "unsustainable biogas" + ].sum() if options.get("biomass_spatial", options["biomass_transport"]): solid_biomass_potentials_spatial = biomass_potentials["solid biomass"].rename( @@ -2272,11 +2280,27 @@ def add_biomass(n, costs): msw_biomass_potentials_spatial = biomass_potentials[ "municipal solid waste" ].rename(index=lambda x: x + " municipal solid waste") + unsustainable_solid_biomass_potentials_spatial = biomass_potentials[ + "unsustainable solid biomass" + ].rename(index=lambda x: x + " solid biomass") + else: solid_biomass_potentials_spatial = biomass_potentials["solid biomass"].sum() msw_biomass_potentials_spatial = biomass_potentials[ "municipal solid waste" ].sum() + unsustainable_solid_biomass_potentials_spatial = biomass_potentials[ + "unsustainable solid biomass" + ].sum() + + if options["regional_oil_demand"]: + unsustainable_liquid_biofuel_potentials_spatial = biomass_potentials[ + "unsustainable bioliquids" + ].rename(index=lambda x: x + " bioliquids") + else: + unsustainable_liquid_biofuel_potentials_spatial = biomass_potentials[ + "unsustainable bioliquids" + ].sum() n.add("Carrier", "biogas") n.add("Carrier", "solid biomass") @@ -2401,6 +2425,81 @@ def add_biomass(n, costs): p_nom_extendable=True, ) + if biomass_potentials.filter(like="unsustainable").sum().sum() > 0: + + # Create timeseries to force usage of unsustainable potentials + e_max_pu = pd.DataFrame(1, index=n.snapshots, columns=spatial.gas.biogas) + e_max_pu.iloc[-1] = 0 + + n.madd( + "Store", + spatial.gas.biogas, + suffix=" unsustainable", + bus=spatial.gas.biogas, + carrier="unsustainable biogas", + e_nom=unsustainable_biogas_potentials_spatial, + marginal_cost=costs.at["biogas", "fuel"], + e_initial=unsustainable_biogas_potentials_spatial, + e_nom_extendable=False, + e_max_pu=e_max_pu, + ) + + e_max_pu = pd.DataFrame(1, index=n.snapshots, columns=spatial.biomass.nodes) + e_max_pu.iloc[-1] = 0 + + n.madd( + "Store", + spatial.biomass.nodes, + suffix=" unsustainable", + bus=spatial.biomass.nodes, + carrier="unsustainable solid biomass", + e_nom=unsustainable_solid_biomass_potentials_spatial, + marginal_cost=costs.at["fuelwood", "fuel"], + e_initial=unsustainable_solid_biomass_potentials_spatial, + e_nom_extendable=False, + e_max_pu=e_max_pu, + ) + + n.madd( + "Bus", + spatial.biomass.bioliquids, + location=spatial.biomass.locations, + carrier="unsustainable bioliquids", + unit="MWh_LHV", + ) + + e_max_pu = pd.DataFrame( + 1, index=n.snapshots, columns=spatial.biomass.bioliquids + ) + e_max_pu.iloc[-1] = 0 + + n.madd( + "Store", + spatial.biomass.bioliquids, + suffix=" unsustainable", + bus=spatial.biomass.bioliquids, + carrier="unsustainable bioliquids", + e_nom=unsustainable_liquid_biofuel_potentials_spatial, + marginal_cost=costs.at["biodiesel crops", "fuel"], + e_initial=unsustainable_liquid_biofuel_potentials_spatial, + e_nom_extendable=False, + e_max_pu=e_max_pu, + ) + + n.madd( + "Link", + spatial.biomass.bioliquids, + bus0=spatial.biomass.bioliquids, + bus1=spatial.oil.nodes, + bus2="co2 atmosphere", + carrier="unsustainable bioliquids", + efficiency=1, + efficiency2=-costs.at["solid biomass", "CO2 intensity"] + + costs.at["BtL", "CO2 stored"], + p_nom=unsustainable_liquid_biofuel_potentials_spatial, + marginal_cost=costs.at["BtL", "VOM"], + ) + n.madd( "Link", spatial.gas.biogas_to_gas, @@ -4132,6 +4231,7 @@ def add_enhanced_geothermal(n, egs_potentials, egs_overlap, costs): # %% if __name__ == "__main__": if "snakemake" not in globals(): + from _helpers import mock_snakemake snakemake = mock_snakemake( From 69c6ea0e1229a52364c04001222a39eccd3b52a0 Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Wed, 7 Aug 2024 17:55:29 +0200 Subject: [PATCH 176/344] use relative imports of scripts.definitions where possible --- scripts/add_existing_baseyear.py | 6 +++--- scripts/prepare_sector_network.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index f67c38d9f..942b13989 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -24,9 +24,9 @@ from add_electricity import sanitize_carriers from prepare_sector_network import cluster_heat_buses, define_spatial, prepare_costs -from scripts.definitions.heat_sector import HeatSector -from scripts.definitions.heat_system import HeatSystem -from scripts.definitions.heat_system_type import HeatSystemType +from definitions.heat_sector import HeatSector +from definitions.heat_system import HeatSystem +from definitions.heat_system_type import HeatSystemType logger = logging.getLogger(__name__) cc = coco.CountryConverter() diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 585b3f258..7c6f7c619 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -37,9 +37,9 @@ from pypsa.io import import_components_from_dataframe from scipy.stats import beta -from scripts.definitions.heat_sector import HeatSector -from scripts.definitions.heat_system import HeatSystem -from scripts.definitions.heat_system_type import HeatSystemType +from definitions.heat_sector import HeatSector +from definitions.heat_system import HeatSystem +from definitions.heat_system_type import HeatSystemType spatial = SimpleNamespace() logger = logging.getLogger(__name__) From cfed67f794ea68f0200bde8023c9e1ae4defe9f0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 8 Aug 2024 14:46:06 +0000 Subject: [PATCH 177/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/add_existing_baseyear.py | 3 +-- scripts/prepare_sector_network.py | 7 +++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index 942b13989..212ae8af5 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -22,11 +22,10 @@ update_config_from_wildcards, ) from add_electricity import sanitize_carriers -from prepare_sector_network import cluster_heat_buses, define_spatial, prepare_costs - from definitions.heat_sector import HeatSector from definitions.heat_system import HeatSystem from definitions.heat_system_type import HeatSystemType +from prepare_sector_network import cluster_heat_buses, define_spatial, prepare_costs logger = logging.getLogger(__name__) cc = coco.CountryConverter() diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 7c6f7c619..191294b6a 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -30,6 +30,9 @@ build_eurostat_co2, ) from build_transport_demand import transport_degree_factor +from definitions.heat_sector import HeatSector +from definitions.heat_system import HeatSystem +from definitions.heat_system_type import HeatSystemType from networkx.algorithms import complement from networkx.algorithms.connectivity.edge_augmentation import k_edge_augmentation from prepare_network import maybe_adjust_costs_and_potentials @@ -37,10 +40,6 @@ from pypsa.io import import_components_from_dataframe from scipy.stats import beta -from definitions.heat_sector import HeatSector -from definitions.heat_system import HeatSystem -from definitions.heat_system_type import HeatSystemType - spatial = SimpleNamespace() logger = logging.getLogger(__name__) From 130ee49dbd132c07713c165b4b43a91676fe47c5 Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Fri, 26 Jul 2024 19:45:45 +0200 Subject: [PATCH 178/344] add option to make central heating forward/return temperatures country-specific --- scripts/build_cop_profiles/run.py | 37 +++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/scripts/build_cop_profiles/run.py b/scripts/build_cop_profiles/run.py index d21b863d9..1b2c0a35d 100644 --- a/scripts/build_cop_profiles/run.py +++ b/scripts/build_cop_profiles/run.py @@ -3,6 +3,43 @@ # # SPDX-License-Identifier: MIT +""" +Approximate heat pump coefficient-of-performance (COP) profiles for different heat sources and systems. + +For central heating, this is based on Jensen et al. (2018) (c.f. `CentralHeatingCopApproximator `_) and for decentral heating, the approximation is based on Staffell et al. (2012) (c.f. `DecentralHeatingCopApproximator `_). + +Relevant Settings +----------------- + +.. code:: yaml + sector: + heat_pump_sink_T_decentral_heating: + district_heating: + forward_temperature: + return_temperature: + heat_source_cooling: + heat_pump_cop_approximation: + refrigerant: + heat_exchanger_pinch_point_temperature_difference + isentropic_compressor_efficiency: + heat_loss: + heat_pump_sources: + urban central: + urban decentral: + rural: + snapshots: + +Inputs +------ +- `resources//regions_onshore.geojson`: Onshore regions +- `resources//temp_soil_total`: Ground temperature +- `resources//temp_air_total`: Air temperature + +Outputs +------- +- `resources//cop_profiles.nc`: Heat pump coefficient-of-performance (COP) profiles +""" + import sys import geopandas as gpd From a914366391b03e6c10a7056e955eccbf21d0520e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 8 Aug 2024 15:13:31 +0000 Subject: [PATCH 179/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/build_cop_profiles/run.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/build_cop_profiles/run.py b/scripts/build_cop_profiles/run.py index 1b2c0a35d..23229e987 100644 --- a/scripts/build_cop_profiles/run.py +++ b/scripts/build_cop_profiles/run.py @@ -2,9 +2,9 @@ # SPDX-FileCopyrightText: : 2020-2024 The PyPSA-Eur Authors # # SPDX-License-Identifier: MIT - """ -Approximate heat pump coefficient-of-performance (COP) profiles for different heat sources and systems. +Approximate heat pump coefficient-of-performance (COP) profiles for different +heat sources and systems. For central heating, this is based on Jensen et al. (2018) (c.f. `CentralHeatingCopApproximator `_) and for decentral heating, the approximation is based on Staffell et al. (2012) (c.f. `DecentralHeatingCopApproximator `_). From 023cc7529e4ff70d81c64f0027e2553d673f24ec Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Thu, 8 Aug 2024 17:15:01 +0200 Subject: [PATCH 180/344] set lower supply temperatures for Scandinavia --- config/config.default.yaml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index aead3e3d6..adb9ffa1b 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -412,13 +412,17 @@ sector: district_heating_loss: 0.15 # check these numbers! forward_temperature: - Default: 90 - DE: 90 #C - DK: 60 #C + default: 90 + DK: 70 + SE: 70 + NO: 70 + FI: 70 return_temperature: - Default: 50 - DE: 50 #C - DK: 40 #C + default: 50 + DK: 40 + SE: 40 + NO: 40 + FI: 40 heat_source_cooling: 6 #K heat_pump_cop_approximation: refrigerant: ammonia From 5ee5a43be30dd953685f4f78c291bce3a5d92edc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 8 Aug 2024 15:16:03 +0000 Subject: [PATCH 181/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- config/config.default.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index adb9ffa1b..650069381 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -413,10 +413,10 @@ sector: # check these numbers! forward_temperature: default: 90 - DK: 70 - SE: 70 - NO: 70 - FI: 70 + DK: 70 + SE: 70 + NO: 70 + FI: 70 return_temperature: default: 50 DK: 40 From 9a0930419e98a010dba814d2201c4ad639e1d94f Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Thu, 8 Aug 2024 17:17:33 +0200 Subject: [PATCH 182/344] update configtables --- doc/configtables/sector.csv | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/configtables/sector.csv b/doc/configtables/sector.csv index e0deb8ca9..9b8ed932f 100644 --- a/doc/configtables/sector.csv +++ b/doc/configtables/sector.csv @@ -9,8 +9,8 @@ district_heating,--,,`prepare_sector_network.py Date: Thu, 8 Aug 2024 17:18:48 +0200 Subject: [PATCH 183/344] update release notes --- doc/release_notes.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 9ce418b21..7a815ebc6 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -10,6 +10,8 @@ Release Notes Upcoming Release ================ +* Added option to use country-specific district heating forward and return temperatures. Defaults to lower temperatures in Scandinavia. + * Changed heat pump COP approximation for central heating to be based on `Jensen et al. (2018) `__ and a default forward temperature of 90C. This is more realistic for district heating than the previously used approximation method. * split solid biomass potentials into solid biomass and municipal solid waste. Add option to use municipal solid waste. This option is only activated in combination with the flag ``waste_to_energy`` From 70db93fd32c208e632e66ff588dd5f72d5149867 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Fri, 9 Aug 2024 13:14:26 +0200 Subject: [PATCH 184/344] correct sheet name chemical feedstocks --- scripts/build_industrial_energy_demand_per_country_today.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build_industrial_energy_demand_per_country_today.py b/scripts/build_industrial_energy_demand_per_country_today.py index fa82a6603..c120dafe5 100644 --- a/scripts/build_industrial_energy_demand_per_country_today.py +++ b/scripts/build_industrial_energy_demand_per_country_today.py @@ -83,7 +83,7 @@ "Basic chemicals": "FC_IND_CPC_BC_E", "Other chemicals": "FC_IND_CPC_OC_E", "Pharmaceutical products etc.": "FC_IND_CPC_PH_E", - "Basic chemicals feedstock": "FC_IND_CPC_BC_E", + "Basic chemicals feedstock": "FC_IND_CPC_NE", "Cement": "FC_IND_NMM_CM_E", "Ceramics & other NMM": "FC_IND_NMM_CR_E", "Glass production": "FC_IND_NMM_GL_E", From c5256c1f370711a2f2fd21b29020581e3bb7e194 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Fri, 9 Aug 2024 14:14:02 +0200 Subject: [PATCH 185/344] add coke oven transformation output --- rules/build_sector.smk | 2 ++ scripts/build_energy_totals.py | 29 +++++++++++++++++++ ...ustrial_energy_demand_per_country_today.py | 18 +++++++++++- 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 1304e7755..faad2bb17 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -301,6 +301,7 @@ rule build_energy_totals: eurostat="data/eurostat/Balances-April2023", eurostat_households="data/eurostat/eurostat-household_energy_balances-february_2024.csv", output: + transformation_output_coke=resources("transformation_output_coke.csv"), energy_name=resources("energy_totals.csv"), co2_name=resources("co2_totals.csv"), transport_name=resources("transport_data.csv"), @@ -666,6 +667,7 @@ rule build_industrial_energy_demand_per_country_today: countries=config_provider("countries"), industry=config_provider("industry"), input: + transformation_output_coke=resources("transformation_output_coke.csv"), jrc="data/jrc-idees-2021", industrial_production_per_country=resources( "industrial_production_per_country.csv" diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index d8c2f5212..f7d754b83 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -1473,6 +1473,31 @@ def update_residential_from_eurostat(energy: pd.DataFrame) -> pd.DataFrame: ) +def build_transformation_output_coke(eurostat, fn): + """ + Extracts and builds the transformation output data for coke ovens from the + Eurostat dataset. + + This function specifically filters the Eurostat data to extract + transformation output related to coke ovens. + Since the transformation output for coke ovens + is not included in the final energy consumption of the iron and steel sector, + it needs to be processed and added separately. The filtered data is saved + as a CSV file. + + Parameters: + eurostat (pd.DataFrame): A pandas DataFrame containing Eurostat data with + a multi-level index + fn (str): The file path where the resulting CSV file should be saved. + + Output: + The resulting transformation output data for coke ovens is saved as a CSV + file at the path specified in fn. + """ + slicer = pd.IndexSlice[:,:,:, "Coke ovens", "Other sources", :] + df = eurostat.loc[slicer, :].droplevel(level=[2,3,4,5]) + df.to_csv(fn) + # %% if __name__ == "__main__": if "snakemake" not in globals(): @@ -1498,6 +1523,10 @@ def update_residential_from_eurostat(energy: pd.DataFrame) -> pd.DataFrame: nprocesses=snakemake.threads, disable_progressbar=snakemake.config["run"].get("disable_progressbar", False), ) + + build_transformation_output_coke(eurostat, + snakemake.output.transformation_output_coke) + swiss = build_swiss() idees = build_idees(idees_countries) diff --git a/scripts/build_industrial_energy_demand_per_country_today.py b/scripts/build_industrial_energy_demand_per_country_today.py index c120dafe5..316d075f8 100644 --- a/scripts/build_industrial_energy_demand_per_country_today.py +++ b/scripts/build_industrial_energy_demand_per_country_today.py @@ -227,6 +227,18 @@ def industrial_energy_demand(countries, year): return pd.concat(demand_l, keys=countries) +def add_coke_ovens(demand, fn, year): + df = pd.read_csv(fn, index_col=[0,1]).xs(year, level=1) + df = df.rename(columns={'Total all products':'Total'})[fuels.keys()] + df = df.rename(columns=fuels).T.groupby(level=0).sum().T + df["other"] = df["all"] - df.loc[:,df.columns != "all"].sum(axis=1) + df = df.T.reindex_like(demand.xs("Integrated steelworks", axis=1, level=1)).fillna(0) + sel = demand.columns.get_level_values(1)=="Integrated steelworks" + demand.loc[:,sel] += 0.75 * df.values + + return demand + + if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake @@ -252,7 +264,11 @@ def industrial_energy_demand(countries, year): # for format compatibility demand = demand.stack(future_stack=True).unstack(level=[0, 2]) - + + # add energy consumption of coke ovens + demand = add_coke_ovens(demand, snakemake.input.transformation_output_coke, + year) + # style and annotation demand.index.name = "TWh/a" demand.sort_index(axis=1, inplace=True) From 9b8139abbea7cdfd06625575b392ab71d382a9d1 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Fri, 9 Aug 2024 14:24:06 +0200 Subject: [PATCH 186/344] add docstring --- ...ustrial_energy_demand_per_country_today.py | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/scripts/build_industrial_energy_demand_per_country_today.py b/scripts/build_industrial_energy_demand_per_country_today.py index 316d075f8..947aad207 100644 --- a/scripts/build_industrial_energy_demand_per_country_today.py +++ b/scripts/build_industrial_energy_demand_per_country_today.py @@ -227,14 +227,42 @@ def industrial_energy_demand(countries, year): return pd.concat(demand_l, keys=countries) -def add_coke_ovens(demand, fn, year): +def add_coke_ovens(demand, fn, year, factor=0.75): + """ + Adds the energy consumption of coke ovens to the energy demand for + integrated steelworks. + + This function reads the energy consumption data for coke ovens from a + CSV file, processes it to match the structure of the `demand` DataFrame, + and then adds a specified share of this energy consumption to the energy + demand for integrated steelworks. + + The `factor` parameter controls what proportion of the coke ovens' energy + consumption should be attributed to the iron and steel production. + The default value of 75% is based on https://doi.org/10.1016/j.erss.2022.102565 + + Parameters: + demand (pd.DataFrame): A pandas DataFrame containing energy demand data + with a multi-level column index where one of the + levels corresponds to "Integrated steelworks". + fn (str): The file path to the CSV file containing the coke ovens energy + consumption data. + year (int): The year for which the coke ovens data should be selected. + factor (float, optional): The proportion of coke ovens energy consumption to add to the + integrated steelworks demand. Defaults to 0.75. + + Returns: + pd.DataFrame: The updated `demand` DataFrame with the coke ovens energy + consumption added to the integrated steelworks energy demand. + """ + df = pd.read_csv(fn, index_col=[0,1]).xs(year, level=1) df = df.rename(columns={'Total all products':'Total'})[fuels.keys()] df = df.rename(columns=fuels).T.groupby(level=0).sum().T df["other"] = df["all"] - df.loc[:,df.columns != "all"].sum(axis=1) df = df.T.reindex_like(demand.xs("Integrated steelworks", axis=1, level=1)).fillna(0) sel = demand.columns.get_level_values(1)=="Integrated steelworks" - demand.loc[:,sel] += 0.75 * df.values + demand.loc[:,sel] += factor * df.values return demand From 156eccefb72739acc7435d8decd2c5f10af0850e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 9 Aug 2024 12:24:36 +0000 Subject: [PATCH 187/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/build_energy_totals.py | 22 +++++++------ ...ustrial_energy_demand_per_country_today.py | 33 ++++++++++--------- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index f7d754b83..48004ea29 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -1480,24 +1480,25 @@ def build_transformation_output_coke(eurostat, fn): This function specifically filters the Eurostat data to extract transformation output related to coke ovens. - Since the transformation output for coke ovens + Since the transformation output for coke ovens is not included in the final energy consumption of the iron and steel sector, it needs to be processed and added separately. The filtered data is saved as a CSV file. Parameters: eurostat (pd.DataFrame): A pandas DataFrame containing Eurostat data with - a multi-level index + a multi-level index fn (str): The file path where the resulting CSV file should be saved. - + Output: The resulting transformation output data for coke ovens is saved as a CSV file at the path specified in fn. """ - slicer = pd.IndexSlice[:,:,:, "Coke ovens", "Other sources", :] - df = eurostat.loc[slicer, :].droplevel(level=[2,3,4,5]) + slicer = pd.IndexSlice[:, :, :, "Coke ovens", "Other sources", :] + df = eurostat.loc[slicer, :].droplevel(level=[2, 3, 4, 5]) df.to_csv(fn) - + + # %% if __name__ == "__main__": if "snakemake" not in globals(): @@ -1523,10 +1524,11 @@ def build_transformation_output_coke(eurostat, fn): nprocesses=snakemake.threads, disable_progressbar=snakemake.config["run"].get("disable_progressbar", False), ) - - build_transformation_output_coke(eurostat, - snakemake.output.transformation_output_coke) - + + build_transformation_output_coke( + eurostat, snakemake.output.transformation_output_coke + ) + swiss = build_swiss() idees = build_idees(idees_countries) diff --git a/scripts/build_industrial_energy_demand_per_country_today.py b/scripts/build_industrial_energy_demand_per_country_today.py index 947aad207..ef559efa2 100644 --- a/scripts/build_industrial_energy_demand_per_country_today.py +++ b/scripts/build_industrial_energy_demand_per_country_today.py @@ -231,16 +231,16 @@ def add_coke_ovens(demand, fn, year, factor=0.75): """ Adds the energy consumption of coke ovens to the energy demand for integrated steelworks. - + This function reads the energy consumption data for coke ovens from a CSV file, processes it to match the structure of the `demand` DataFrame, and then adds a specified share of this energy consumption to the energy demand for integrated steelworks. - + The `factor` parameter controls what proportion of the coke ovens' energy consumption should be attributed to the iron and steel production. The default value of 75% is based on https://doi.org/10.1016/j.erss.2022.102565 - + Parameters: demand (pd.DataFrame): A pandas DataFrame containing energy demand data with a multi-level column index where one of the @@ -248,22 +248,24 @@ def add_coke_ovens(demand, fn, year, factor=0.75): fn (str): The file path to the CSV file containing the coke ovens energy consumption data. year (int): The year for which the coke ovens data should be selected. - factor (float, optional): The proportion of coke ovens energy consumption to add to the + factor (float, optional): The proportion of coke ovens energy consumption to add to the integrated steelworks demand. Defaults to 0.75. - + Returns: pd.DataFrame: The updated `demand` DataFrame with the coke ovens energy consumption added to the integrated steelworks energy demand. """ - df = pd.read_csv(fn, index_col=[0,1]).xs(year, level=1) - df = df.rename(columns={'Total all products':'Total'})[fuels.keys()] + df = pd.read_csv(fn, index_col=[0, 1]).xs(year, level=1) + df = df.rename(columns={"Total all products": "Total"})[fuels.keys()] df = df.rename(columns=fuels).T.groupby(level=0).sum().T - df["other"] = df["all"] - df.loc[:,df.columns != "all"].sum(axis=1) - df = df.T.reindex_like(demand.xs("Integrated steelworks", axis=1, level=1)).fillna(0) - sel = demand.columns.get_level_values(1)=="Integrated steelworks" - demand.loc[:,sel] += factor * df.values - + df["other"] = df["all"] - df.loc[:, df.columns != "all"].sum(axis=1) + df = df.T.reindex_like(demand.xs("Integrated steelworks", axis=1, level=1)).fillna( + 0 + ) + sel = demand.columns.get_level_values(1) == "Integrated steelworks" + demand.loc[:, sel] += factor * df.values + return demand @@ -292,11 +294,10 @@ def add_coke_ovens(demand, fn, year, factor=0.75): # for format compatibility demand = demand.stack(future_stack=True).unstack(level=[0, 2]) - + # add energy consumption of coke ovens - demand = add_coke_ovens(demand, snakemake.input.transformation_output_coke, - year) - + demand = add_coke_ovens(demand, snakemake.input.transformation_output_coke, year) + # style and annotation demand.index.name = "TWh/a" demand.sort_index(axis=1, inplace=True) From 5c577b62906bbbf163013d62c18197cc34e43f79 Mon Sep 17 00:00:00 2001 From: Philipp Glaum Date: Mon, 12 Aug 2024 10:38:01 +0200 Subject: [PATCH 188/344] remove methanol transport because it has no impact --- config/config.default.yaml | 4 +--- scripts/prepare_sector_network.py | 38 ++++++------------------------- 2 files changed, 8 insertions(+), 34 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 83371c379..f4a70c244 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -590,10 +590,8 @@ sector: # - onshore # more than 50 km from sea - nearshore # within 50 km of sea # - offshore - methanol: false # if industry is modelled, methanol is still added even if false - methanol_spatial: false # if true demand is also regional even if regional demand is set to false, since methanol is regionally resolved regional_methanol_demand: false - methanol_transport: false + methanol: false methanol_reforming: false methanol_reforming_cc: false methanol_to_kerosene: false diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index bb850c3a3..eb16ac946 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -153,24 +153,17 @@ def define_spatial(nodes, options): spatial.methanol = SimpleNamespace() - if options.get("methanol_spatial", False): - spatial.methanol.nodes = nodes + " methanol" - spatial.methanol.locations = nodes + spatial.methanol.nodes = ["EU methanol"] + spatial.methanol.locations = ["EU"] + + if options["regional_methanol_demand"]: spatial.methanol.demand_locations = nodes spatial.methanol.industry = nodes + " industry methanol" spatial.methanol.shipping = nodes + " shipping methanol" else: - spatial.methanol.nodes = ["EU methanol"] - spatial.methanol.locations = ["EU"] - - if options["regional_methanol_demand"]: - spatial.methanol.demand_locations = nodes - spatial.methanol.industry = nodes + " industry methanol" - spatial.methanol.shipping = nodes + " shipping methanol" - else: - spatial.methanol.demand_locations = ["EU"] - spatial.methanol.shipping = ["EU shipping methanol"] - spatial.methanol.industry = ["EU industry methanol"] + spatial.methanol.demand_locations = ["EU"] + spatial.methanol.shipping = ["EU shipping methanol"] + spatial.methanol.industry = ["EU industry methanol"] # oil spatial.oil = SimpleNamespace() @@ -2610,23 +2603,6 @@ def add_methanol(n, costs): capital_cost=0.02, ) - if options["methanol_transport"]: - methanol_transport = create_network_topology( - n, "methanol transport ", bidirectional=True - ) - n.madd( - "Link", - methanol_transport.index, - bus0=methanol_transport.bus0 + " methanol", - bus1=methanol_transport.bus1 + " methanol", - p_nom_extendable=False, - p_nom=5e4, - length=methanol_transport.length.values, - marginal_cost=0.027 - * methanol_transport.length.values, # assuming 0.15€/ton-km and 0.183t/1000MWhMeOH - carrier="methanol transport", - ) - if n.buses.carrier.str.contains("biomass").any(): if options["biomass_to_methanol"]: add_biomass_to_methanol(n, costs) From d14ced2549f8f0669fdba420906250da661c751d Mon Sep 17 00:00:00 2001 From: Philipp Glaum Date: Mon, 12 Aug 2024 10:43:13 +0200 Subject: [PATCH 189/344] cleanup --- 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 eb16ac946..ca3634a84 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3367,7 +3367,7 @@ def add_industry(n, costs): / nhours ) - if not options["regional_methanol_demand"] or not options["methanol_spatial"]: + if not options["regional_methanol_demand"]: p_set_methanol = p_set_methanol.sum() n.madd( @@ -3490,7 +3490,7 @@ def add_industry(n, costs): * efficiency ) - if not options["regional_methanol_demand"] or not options["methanol_spatial"]: + if not options["regional_methanol_demand"]: p_set_methanol_shipping = p_set_methanol_shipping.sum() n.madd( From 560929a2a8dbf3b0d7bdfc9a43d020b6a1193dd3 Mon Sep 17 00:00:00 2001 From: Philipp Glaum Date: Mon, 12 Aug 2024 10:55:32 +0200 Subject: [PATCH 190/344] add release note --- doc/release_notes.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 7404e2ef4..9d059e7a4 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -9,6 +9,7 @@ Release Notes Upcoming Release ================ +* Add technology options for methanol, like electricity production from methanol, biomass to methanol, methanol to kerosene, ... * Added unsustainable biomass potentials for solid, gaseous, and liquid biomass. The potentials can be phased-out and/or substituted by the phase-in of sustainable biomass types using the config parameters From aa91c7b2461e85e9c6ce9df826af3688b69f44a9 Mon Sep 17 00:00:00 2001 From: Philipp Glaum Date: Mon, 12 Aug 2024 11:11:05 +0200 Subject: [PATCH 191/344] add documentation for configuration --- config/config.default.yaml | 2 +- doc/configtables/sector.csv | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index f4a70c244..8f9e6d7ad 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -590,8 +590,8 @@ sector: # - onshore # more than 50 km from sea - nearshore # within 50 km of sea # - offshore - regional_methanol_demand: false methanol: false + regional_methanol_demand: false methanol_reforming: false methanol_reforming_cc: false methanol_to_kerosene: false diff --git a/doc/configtables/sector.csv b/doc/configtables/sector.csv index e0deb8ca9..b9dd22f08 100644 --- a/doc/configtables/sector.csv +++ b/doc/configtables/sector.csv @@ -99,7 +99,6 @@ hydrogen_fuel_cell,--,"{true, false}",Add option to include hydrogen fuel cell f hydrogen_turbine,--,"{true, false}",Add option to include hydrogen turbine for re-electrification. Assuming OCGT technology costs SMR,--,"{true, false}",Add option for transforming natural gas into hydrogen and CO2 using Steam Methane Reforming (SMR) SMR CC,--,"{true, false}",Add option for transforming natural gas into hydrogen and CO2 using Steam Methane Reforming (SMR) and Carbon Capture (CC) -regional_methanol_demand,--,"{true, false}",Spatially resolve methanol demand. Set to true if regional CO2 constraints needed. regional_oil_demand,--,"{true, false}",Spatially resolve oil demand. Set to true if regional CO2 constraints needed. regional_co2 _sequestration_potential,,, -- enable,--,"{true, false}",Add option for regionally-resolved geological carbon dioxide sequestration potentials based on `CO2StoP `_. @@ -120,6 +119,18 @@ cc_fraction,--,float,The default fraction of CO2 captured with post-combustion c hydrogen_underground _storage,--,"{true, false}",Add options for storing hydrogen underground. Storage potential depends regionally. hydrogen_underground _storage_locations,,"{onshore, nearshore, offshore}","The location where hydrogen underground storage can be located. Onshore, nearshore, offshore means it must be located more than 50 km away from the sea, within 50 km of the sea, or within the sea itself respectively." ,,, +methanol, --,"{true, false}", Add methanol as carrrier and add enables methnol technologies +regional_methanol_demand,--,"{true, false}",Spatially resolve methanol demand. Set to true if regional CO2 constraints needed. +methanol_reforming,--,"{true, false}", Add methanol reforming +methanol_reforming_cc,--,"{true, false}", Add methanol reforming with carbon capture +methanol_to_kerosene,--,"{true, false}", Add methanol to kerosene +methanol_to_olefins,--,"{true, false}", Add methanol to olefins +methanol_to_power,--,--, Add different methanol to power technologies +-- ccgt,--,"{true, false}", Add combined cycle gas turbine (CCGT) technology using methanol +-- ccgt_cc,--,"{true, false}", Add combined cycle gas turbine (CCGT) technology using methanol with carbon capture +-- ocgt,--,"{true, false}", Add open cycle gas turbine (OCGT) technology using methanol +-- allam,--,"{true, false}", Add Allam cycle gas power plants using methanol +,,, ammonia,--,"{true, false, regional}","Add ammonia as a carrrier. It can be either true (copperplated NH3), false (no NH3 carrier) or ""regional"" (regionalised NH3 without network)" min_part_load_fischer _tropsch,per unit of p_nom ,float,The minimum unit dispatch (``p_min_pu``) for the Fischer-Tropsch process min_part_load _methanolisation,per unit of p_nom ,float,The minimum unit dispatch (``p_min_pu``) for the methanolisation process From a76cb299641bad65046c1ad56cf37236fa25cacd Mon Sep 17 00:00:00 2001 From: Philipp Glaum Date: Mon, 12 Aug 2024 11:26:18 +0200 Subject: [PATCH 192/344] fix spelling mistake --- doc/configtables/sector.csv | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/configtables/sector.csv b/doc/configtables/sector.csv index b9dd22f08..c50bf6091 100644 --- a/doc/configtables/sector.csv +++ b/doc/configtables/sector.csv @@ -119,16 +119,16 @@ cc_fraction,--,float,The default fraction of CO2 captured with post-combustion c hydrogen_underground _storage,--,"{true, false}",Add options for storing hydrogen underground. Storage potential depends regionally. hydrogen_underground _storage_locations,,"{onshore, nearshore, offshore}","The location where hydrogen underground storage can be located. Onshore, nearshore, offshore means it must be located more than 50 km away from the sea, within 50 km of the sea, or within the sea itself respectively." ,,, -methanol, --,"{true, false}", Add methanol as carrrier and add enables methnol technologies +methanol, --,"{true, false}", Add methanol as carrrier and add enabled methnol technologies regional_methanol_demand,--,"{true, false}",Spatially resolve methanol demand. Set to true if regional CO2 constraints needed. methanol_reforming,--,"{true, false}", Add methanol reforming methanol_reforming_cc,--,"{true, false}", Add methanol reforming with carbon capture methanol_to_kerosene,--,"{true, false}", Add methanol to kerosene methanol_to_olefins,--,"{true, false}", Add methanol to olefins methanol_to_power,--,--, Add different methanol to power technologies --- ccgt,--,"{true, false}", Add combined cycle gas turbine (CCGT) technology using methanol --- ccgt_cc,--,"{true, false}", Add combined cycle gas turbine (CCGT) technology using methanol with carbon capture --- ocgt,--,"{true, false}", Add open cycle gas turbine (OCGT) technology using methanol +-- ccgt,--,"{true, false}", Add combined cycle gas turbine (CCGT) using methanol +-- ccgt_cc,--,"{true, false}", Add combined cycle gas turbine (CCGT) with carbon capture using methanol +-- ocgt,--,"{true, false}", Add open cycle gas turbine (OCGT) using methanol -- allam,--,"{true, false}", Add Allam cycle gas power plants using methanol ,,, ammonia,--,"{true, false, regional}","Add ammonia as a carrrier. It can be either true (copperplated NH3), false (no NH3 carrier) or ""regional"" (regionalised NH3 without network)" From 6686e6e196e08b110c377a37cb735f4f138274b3 Mon Sep 17 00:00:00 2001 From: lisazeyen <35347358+lisazeyen@users.noreply.github.com> Date: Mon, 12 Aug 2024 15:27:24 +0200 Subject: [PATCH 193/344] Update doc/release_notes.rst Co-authored-by: Fabian Neumann --- doc/release_notes.rst | 8 -------- 1 file changed, 8 deletions(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index c4823f5f9..5d95d4da8 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -11,14 +11,6 @@ Upcoming Release ================ * Update JRC-IDEES-2015 to `JRC-IDEES-2021 `__. The reference year is changed from 2015 to 2019. -* split solid biomass potentials into solid biomass and municipal solid waste. Add option to use municipal solid waste. This option is only activated in combination with the flag ``waste_to_energy`` - -* Add option to import solid biomass - -* Add option to produce electrobiofuels (flag ``electrobiofuels``) from solid biomass and hydrogen, as a combination of BtL and Fischer-Tropsch to make more use of the biogenic carbon - -* Add flag ``sector: fossil_fuels`` in config to remove the option of importing fossil fuels - * Added unsustainable biomass potentials for solid, gaseous, and liquid biomass. The potentials can be phased-out and/or substituted by the phase-in of sustainable biomass types using the config parameters ``biomass: share_unsustainable_use_retained`` and ``biomass: share_sustainable_potential_available``. From c79a9faa2c172c6f2f024b7cf351594f451c1dc7 Mon Sep 17 00:00:00 2001 From: Bobby Xiong <36541459+bobbyxng@users.noreply.github.com> Date: Mon, 12 Aug 2024 16:56:10 +0200 Subject: [PATCH 194/344] Updated under_construction status of links that are commissioned by now (#1205) * Updated links 14802, 14801, 12931, 14803, 14848, 14804: under_construction=False * Updated links 14802, 14801, 14803, 14848, 14804: under_construction=False * Updated link 8394 and parameter_corrections: Continuation of North-Sea-Link. --- data/entsoegridkit/links.csv | 12 ++++++------ data/parameter_corrections.yaml | 1 + 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/data/entsoegridkit/links.csv b/data/entsoegridkit/links.csv index 00a488ddf..abcaf0cc1 100644 --- a/data/entsoegridkit/links.csv +++ b/data/entsoegridkit/links.csv @@ -6,7 +6,7 @@ link_id,bus0,bus1,length,underground,under_construction,tags,geometry 5587,1377,2382,76847.0139826037,f,f,'"MW"=>"None", "TSO"=>"None", "oid"=>"32533", "ext1"=>"None", "EIC_2"=>"None", "EIC_3"=>"None", "EIC_4"=>"None", "text_"=>"200", "symbol"=>"DC-Line", "country"=>"IT", "t9_code"=>"0", "visible"=>"1", "EIC_code"=>"None", "tie_line"=>"0", "oneCircuit"=>"0", "CreatedDate"=>"None", "DeletedDate"=>"None", "ModifiedDate"=>"None", "Internalcomments"=>"None", "visible_on_printed"=>"1"','LINESTRING(8.67675371049727 40.6777653795244,9.03900099999999 40.979898,9.22164899999999 41.133159,9.19977299501706 41.2082924934473)' 5640,1422,1638,234733.218840324,f,f,'"MW"=>"None", "TSO"=>"None", "oid"=>"32590", "ext1"=>"None", "EIC_2"=>"None", "EIC_3"=>"None", "EIC_4"=>"None", "text_"=>"Rómulo", "symbol"=>"DC-Line", "country"=>"ES", "t9_code"=>"0", "visible"=>"1", "EIC_code"=>"None", "tie_line"=>"0", "oneCircuit"=>"0", "CreatedDate"=>"None", "DeletedDate"=>"None", "ModifiedDate"=>"None", "Internalcomments"=>"None", "visible_on_printed"=>"1"','LINESTRING(2.48932993486183 39.561252379133,1.13159199999999 39.610978,0 39.710356,-0.234388957535875 39.7314420592468)' 13589,2262,7428,316517.539537871,f,f,,'LINESTRING(9.17009350125146 41.2967653544603,9.38095099999999 41.331451,9.858856 41.352072,10.70755 41.479776,11.25 41.448903,12.100067 41.432431,12.380219 41.426253,12.418671 41.401536,12.704315 41.347948,12.805939 41.368564,12.9016442293009 41.3921592955445)' -14802,2258,7029,391819.608605717,f,t,,'LINESTRING(14.0986517070226 42.4133438660838,14.412689 42.431566,15.115814 42.363618,16.269379 42.067646,16.875 42.126747,16.962891 42.135913,18.531189 42.271212,18.7271798293119 42.3522936900005)' +14802,2258,7029,391819.608605717,f,f,,'LINESTRING(14.0986517070226 42.4133438660838,14.412689 42.431566,15.115814 42.363618,16.269379 42.067646,16.875 42.126747,16.962891 42.135913,18.531189 42.271212,18.7271798293119 42.3522936900005)' 14668,2333,3671,146536.932669904,f,t,,'LINESTRING(6.04271995139229 45.4637174756646,6.16607700000001 45.327048,6.351471 45.183973,6.54922499999999 45.148148,6.62338299999999 45.101638,6.642609 45.089036,6.70440700000001 45.05121,6.980438 45.089036,7.00653099999999 45.092914,7.21939099999999 45.094853,7.223511 45.089036,7.378693 44.871443,7.32136143270145 44.8385424366672)' 14808,2379,2383,103628.671904731,f,f,,'LINESTRING(9.37725891362686 42.7057449479108,9.79980499999999 42.799431,10.5931379465185 42.9693952059839)' 5575,2379,2380,24868.4258834249,f,f,'"MW"=>"None", "TSO"=>"None", "oid"=>"32521", "ext1"=>"None", "EIC_2"=>"None", "EIC_3"=>"None", "EIC_4"=>"None", "text_"=>" ", "symbol"=>"DC-Line", "country"=>"FR", "t9_code"=>"0", "visible"=>"1", "EIC_code"=>"None", "tie_line"=>"0", "oneCircuit"=>"0", "CreatedDate"=>"None", "DeletedDate"=>"None", "ModifiedDate"=>"None", "Internalcomments"=>"None", "visible_on_printed"=>"1"','LINESTRING(9.37679000208623 42.7053229039427,9.357605 42.552069,9.45054814341409 42.5389781005166)' @@ -15,7 +15,7 @@ link_id,bus0,bus1,length,underground,under_construction,tags,geometry 5583,2382,7428,11623.019620339,f,f,'"MW"=>"None", "TSO"=>"None", "oid"=>"32529", "ext1"=>"None", "EIC_2"=>"None", "EIC_3"=>"None", "EIC_4"=>"None", "text_"=>" ", "symbol"=>"DC-Line", "country"=>"IT", "t9_code"=>"FR-IT-01", "visible"=>"1", "EIC_code"=>"None", "tie_line"=>"1", "oneCircuit"=>"0", "CreatedDate"=>"None", "DeletedDate"=>"None", "ModifiedDate"=>"1.555323123e+12", "Internalcomments"=>"None", "visible_on_printed"=>"1"','LINESTRING(9.17008474107272 41.2967639130447,9.168091 41.303603,9.18319700000001 41.250968,9.1995514318356 41.2089447559651)' 14825,2476,2585,45367.7245799963,f,f,,'LINESTRING(2.98259070757654 42.2776059846425,2.90313700000001 42.397094,2.867432 42.467032,2.77404800000001 42.655172)' 8745,3611,8302,9361.61122972312,f,f,'"MW"=>"None", "TSO"=>"None", "oid"=>"120591", "ext1"=>"None", "EIC_2"=>"None", "EIC_3"=>"None", "EIC_4"=>"None", "text_"=>"None", "symbol"=>"DC-Line", "country"=>"CH", "t9_code"=>"None", "visible"=>"1", "EIC_code"=>"None", "tie_line"=>"0", "oneCircuit"=>"1", "CreatedDate"=>"1.556535027e+12", "DeletedDate"=>"None", "ModifiedDate"=>"None", "Internalcomments"=>"None", "visible_on_printed"=>"1"','LINESTRING(7.95410166666667 47.5542867377085,7.928009 47.555214,7.937622 47.526475,7.96895162362761 47.4961125343931)' -14801,4709,4781,50206.4589537583,f,t,,'LINESTRING(6.43068069229957 50.8136946409214,6.020508 50.766865,5.925751 50.755572,5.73118285928413 50.7304278585398)' +14801,4709,4781,50206.4589537583,f,f,,'LINESTRING(6.43068069229957 50.8136946409214,6.020508 50.766865,5.925751 50.755572,5.73118285928413 50.7304278585398)' 14814,4972,5062,232745.802729813,f,f,,'LINESTRING(4.04528166772434 51.9611233898246,2.41561900000001 51.702353,0.794192405058928 51.4189824547604)' 5558,4975,7427,45665.1050240866,f,t,'"MW"=>"None", "TSO"=>"None", "oid"=>"32502", "ext1"=>"None", "EIC_2"=>"None", "EIC_3"=>"None", "EIC_4"=>"None", "text_"=>" ", "symbol"=>"DC-Line", "country"=>"UK", "t9_code"=>" BE-UK-01", "visible"=>"1", "EIC_code"=>"None", "tie_line"=>"1", "oneCircuit"=>"0", "CreatedDate"=>"None", "DeletedDate"=>"None", "ModifiedDate"=>"1.555407949e+12", "Internalcomments"=>"None", "visible_on_printed"=>"1"','LINESTRING(1.92947399999999 51.251601,1.27623412238205 51.2327009391635)' 14826,4977,4983,52725.5506558225,f,f,,'LINESTRING(1.75051314494826 50.9186901861196,1.43508900000001 50.970535,1.02353536683349 51.0370060560335)' @@ -33,16 +33,16 @@ link_id,bus0,bus1,length,underground,under_construction,tags,geometry 5571,5743,7074,89346.6337548304,f,f,'"MW"=>"None", "TSO"=>"None", "oid"=>"32517", "ext1"=>"None", "EIC_2"=>"None", "EIC_3"=>"None", "EIC_4"=>"None", "text_"=>"HelWin1", "symbol"=>"DC-Line", "country"=>"DE", "t9_code"=>"0", "visible"=>"1", "EIC_code"=>"None", "tie_line"=>"0", "oneCircuit"=>"0", "CreatedDate"=>"None", "DeletedDate"=>"None", "ModifiedDate"=>"1.545224101e+12", "Internalcomments"=>"None", "visible_on_printed"=>"1"','LINESTRING(8.12610708224912 54.310749538123,8.238373 54.256401,9.32699442549698 53.9319562532009)' 5567,5744,5787,139209.866527364,f,f,'"MW"=>"None", "TSO"=>"None", "oid"=>"32512", "ext1"=>"None", "EIC_2"=>"None", "EIC_3"=>"None", "EIC_4"=>"None", "text_"=>"DolWin1", "symbol"=>"DC-Line", "country"=>"DE", "t9_code"=>"0", "visible"=>"1", "EIC_code"=>"None", "tie_line"=>"0", "oneCircuit"=>"0", "CreatedDate"=>"None", "DeletedDate"=>"None", "ModifiedDate"=>"1.545224147e+12", "Internalcomments"=>"None", "visible_on_printed"=>"1"','LINESTRING(6.84493115764205 53.880869,6.909027 53.880869,7.116394 53.835512,7.36358600000001 53.396432,7.32101399999999 53.112163,7.33612100000001 52.893992,7.16075117704058 52.8485079587114)' 5570,5745,8272,99066.5793764307,f,f,'"MW"=>"None", "TSO"=>"None", "oid"=>"32515", "ext1"=>"None", "EIC_2"=>"None", "EIC_3"=>"None", "EIC_4"=>"None", "text_"=>"DolWin3", "symbol"=>"DC-Line", "country"=>"DE", "t9_code"=>"0", "visible"=>"1", "EIC_code"=>"None", "tie_line"=>"0", "oneCircuit"=>"0", "CreatedDate"=>"None", "DeletedDate"=>"None", "ModifiedDate"=>"1.545224133e+12", "Internalcomments"=>"None", "visible_on_printed"=>"1"','LINESTRING(6.84423599483409 53.8134043878533,6.71127300000001 53.693454,6.65634200000001 53.59821,6.73461900000001 53.55581,7.112274 53.45126,7.05596900000001 53.340713,7.237244 53.26932,7.223511 53.18135,7.223511 53.1805270078955)' -14803,5751,5803,280301.445474794,f,t,,'LINESTRING(6.75668661933496 53.437616158174,6.838989 53.664171,6.96258499999999 53.785238,7.34298700000001 53.882488,7.80029300000001 54.517096,8.20678699999999 55.297102,8.86005375885099 55.4336013425692)' +14803,5751,5803,280301.445474794,f,f,,'LINESTRING(6.75668661933496 53.437616158174,6.838989 53.664171,6.96258499999999 53.785238,7.34298700000001 53.882488,7.80029300000001 54.517096,8.20678699999999 55.297102,8.86005375885099 55.4336013425692)' 14821,5749,6363,575352.425009444,f,f,,'LINESTRING(6.83036734046461 53.4374933986115,6.253967 53.645452,6.33636499999999 55.776573,6.34597800000001 56.029855,6.34597800000001 56.030622,6.43661500000001 58.130121,6.90176957000565 58.2653404287817)' 5568,5768,5787,131420.09609615,f,f,'"MW"=>"None", "TSO"=>"None", "oid"=>"32513", "ext1"=>"None", "EIC_2"=>"None", "EIC_3"=>"None", "EIC_4"=>"None", "text_"=>"DolWin2", "symbol"=>"DC-Line", "country"=>"DE", "t9_code"=>"0", "visible"=>"1", "EIC_code"=>"None", "tie_line"=>"0", "oneCircuit"=>"0", "CreatedDate"=>"None", "DeletedDate"=>"None", "ModifiedDate"=>"1.545224159e+12", "Internalcomments"=>"None", "visible_on_printed"=>"1"','LINESTRING(7.11083415172816 53.9630966319811,7.07107499999999 53.80795,7.301788 53.39807,7.267456 53.110514,7.29354899999999 52.907246,7.16070024970726 52.8485606886388)' 12932,5770,5773,6905.52230262641,f,t,,'LINESTRING(7.15460523215685 53.4027398808691,7.24823000000001 53.375956)' -14848,5858,6358,574884.998052791,f,t,,'LINESTRING(6.81690675921544 58.6338502746805,6.63024900000001 58.249559,6.78268399999999 57.579197,7.17544599999999 56.532986,7.17407200000001 56.5345,7.46521000000001 55.776573,7.46521000000001 55.776573,7.64099100000001 55.312736,8.458099 54.316523,9.394684 53.934262)' +14848,5858,6358,574884.998052791,f,f,,'LINESTRING(6.81690675921544 58.6338502746805,6.63024900000001 58.249559,6.78268399999999 57.579197,7.17544599999999 56.532986,7.17407200000001 56.5345,7.46521000000001 55.776573,7.46521000000001 55.776573,7.64099100000001 55.312736,8.458099 54.316523,9.394684 53.934262)' 5581,5893,6072,59184.4227659405,f,f,'"MW"=>"None", "TSO"=>"None", "oid"=>"32527", "ext1"=>"None", "EIC_2"=>"None", "EIC_3"=>"None", "EIC_4"=>"None", "text_"=>" ", "symbol"=>"DC-Line", "country"=>"UK", "t9_code"=>"222.1.2", "visible"=>"1", "EIC_code"=>"None", "tie_line"=>"1", "oneCircuit"=>"0", "CreatedDate"=>"None", "DeletedDate"=>"None", "ModifiedDate"=>"None", "Internalcomments"=>"None", "visible_on_printed"=>"1"','LINESTRING(-4.94702447012386 55.0727948492206,-5.137482 55.042188,-5.62500000000001 54.890036,-5.631866 54.887667,-5.7332134509551 54.813550429852)' 5580,5893,6072,58741.4601812995,f,f,'"MW"=>"None", "TSO"=>"None", "oid"=>"32526", "ext1"=>"None", "EIC_2"=>"None", "EIC_3"=>"None", "EIC_4"=>"None", "text_"=>" ", "symbol"=>"DC-Line", "country"=>"UK", "t9_code"=>"222.1.1", "visible"=>"1", "EIC_code"=>"None", "tie_line"=>"1", "oneCircuit"=>"0", "CreatedDate"=>"None", "DeletedDate"=>"None", "ModifiedDate"=>"None", "Internalcomments"=>"None", "visible_on_printed"=>"1"','LINESTRING(-4.94689333475508 55.0726735779237,-5.045471 55.009914,-5.59616099999999 54.840245,-5.62500000000001 54.834709,-5.73306677066227 54.8134313531551)' 8009,5897,5936,363085.503577327,f,f,'"MW"=>"None", "TSO"=>"None", "oid"=>"70191", "ext1"=>"None", "EIC_2"=>"None", "EIC_3"=>"None", "EIC_4"=>"None", "text_"=>"Western HVDC link", "symbol"=>"DC-Line", "country"=>"UK", "t9_code"=>"None", "visible"=>"1", "EIC_code"=>"None", "tie_line"=>"0", "oneCircuit"=>"0", "CreatedDate"=>"1.514994622e+12", "DeletedDate"=>"None", "ModifiedDate"=>"1.51499467e+12", "Internalcomments"=>"None", "visible_on_printed"=>"1"','LINESTRING(-3.18595885129092 53.213699479605,-3.158569 53.308724,-3.40988200000001 53.511735,-4.081421 53.803084,-5.158081 54.013418,-5.28442399999999 54.866334,-5.177307 55.345546,-4.88616899999999 55.586883,-4.8806877889882 55.7044245716822)' 14815,5937,6086,242400.41935291,f,f,,'LINESTRING(-3.12293971810515 53.2087645354697,-3.13934300000001 53.266034,-3.368683 53.377594,-5.18280000000001 53.495399,-5.62500000000001 53.519084,-5.62500000000001 53.519084,-6.101532 53.503568,-6.61057668606004 53.483977180569)' -14804,5949,6684,695432.776022422,f,t,,'LINESTRING(6.64773945778347 59.5995729910866,6.483307 59.539192,6.374817 59.538495,6.24847399999999 59.510636,6.196289 59.448566,5.898285 59.321981,5.64697299999999 59.234284,5.62500000000001 59.223042,4.81338500000001 58.813742,2.03384400000001 57.374679,0 56.170023,-0.650940000000012 55.776573,-1.55838055228731 55.2221613174321)' +14804,5949,6684,695432.776022422,f,f,,'LINESTRING(6.64773945778347 59.5995729910866,6.483307 59.539192,6.374817 59.538495,6.24847399999999 59.510636,6.196289 59.448566,5.898285 59.321981,5.64697299999999 59.234284,5.62500000000001 59.223042,4.81338500000001 58.813742,2.03384400000001 57.374679,0 56.170023,-0.650940000000012 55.776573,-1.55838055228731 55.2221613174321)' 5635,6300,6348,93313.2906756649,f,f,'"MW"=>"None", "TSO"=>"None", "oid"=>"32585", "ext1"=>"None", "EIC_2"=>"None", "EIC_3"=>"None", "EIC_4"=>"None", "text_"=>"150", "symbol"=>"DC-Line", "country"=>"SE", "t9_code"=>"0", "visible"=>"1", "EIC_code"=>"None", "tie_line"=>"0", "oneCircuit"=>"0", "CreatedDate"=>"None", "DeletedDate"=>"None", "ModifiedDate"=>"None", "Internalcomments"=>"None", "visible_on_printed"=>"1"','LINESTRING(18.2272491895352 57.5711315582343,17.274628 57.645401,16.875 57.674052,16.6818074486274 57.692364166947)' 14819,6311,6416,122337.134741418,f,f,,'LINESTRING(10.2163282994747 57.1311139024238,10.567474 57.20771,10.737762 57.192832,10.972595 57.230016,11.25 57.33171,11.532898 57.436081,11.867981 57.556366,12.0227165657676 57.561507168045)' 14809,6311,6416,122935.90852816,f,f,,'LINESTRING(10.2163571716117 57.1310010356663,10.366974 57.123569,10.578461 57.16678,10.740509 57.15263,11.001434 57.197296,11.174469 57.255281,11.25 57.282754,11.56723 57.399104,12.0227887239052 57.5613889668514)' @@ -58,6 +58,6 @@ link_id,bus0,bus1,length,underground,under_construction,tags,geometry 14818,6586,6618,257364.279393886,f,f,,'LINESTRING(21.3559064590049 61.0800030227353,21.303864 61.005076,20.946808 60.801394,18.153534 60.501202,18.007965 60.483615,17.171631 60.503906,17.0593630437863 60.5503864910584)' 14817,6589,6618,197128.229552834,f,f,,'LINESTRING(21.3557421230034 61.0800501553429,20.902863 60.846249,18.224945 60.556604,18.0193872312079 60.533018071939)' 14812,6620,6623,140169.735736189,f,f,,'LINESTRING(22.3045576957813 60.4368452717433,21.404114 60.329667,19.8472351583549 60.129935739173)' -8394,6684,6696,21158.5735245602,f,t,'"MW"=>"None", "TSO"=>"None", "oid"=>"89791", "ext1"=>"None", "EIC_2"=>"None", "EIC_3"=>"None", "EIC_4"=>"None", "text_"=>"None", "symbol"=>"DC-Line", "country"=>"NO", "t9_code"=>"None", "visible"=>"1", "EIC_code"=>"None", "tie_line"=>"0", "oneCircuit"=>"0", "CreatedDate"=>"1.518010133e+12", "DeletedDate"=>"None", "ModifiedDate"=>"None", "Internalcomments"=>"None", "visible_on_printed"=>"1"','LINESTRING(6.64851407057135 59.5996162767494,6.99238592942864 59.5246589234811)' +8394,6684,6696,21158.5735245602,f,f,'"MW"=>"None", "TSO"=>"None", "oid"=>"89791", "ext1"=>"None", "EIC_2"=>"None", "EIC_3"=>"None", "EIC_4"=>"None", "text_"=>"None", "symbol"=>"DC-Line", "country"=>"NO", "t9_code"=>"None", "visible"=>"1", "EIC_code"=>"None", "tie_line"=>"0", "oneCircuit"=>"0", "CreatedDate"=>"1.518010133e+12", "DeletedDate"=>"None", "ModifiedDate"=>"None", "Internalcomments"=>"None", "visible_on_printed"=>"1"','LINESTRING(6.64851407057135 59.5996162767494,6.99238592942864 59.5246589234811)' 5569,5787,8272,38561.1931761179,f,t,'"MW"=>"None", "TSO"=>"None", "oid"=>"32514", "ext1"=>"None", "EIC_2"=>"None", "EIC_3"=>"None", "EIC_4"=>"None", "text_"=>"DolWin 3", "symbol"=>"DC-Line", "country"=>"DE", "t9_code"=>"0", "visible"=>"1", "EIC_code"=>"None", "tie_line"=>"0", "oneCircuit"=>"0", "CreatedDate"=>"None", "DeletedDate"=>"None", "ModifiedDate"=>"1.489072219e+12", "Internalcomments"=>"None", "visible_on_printed"=>"1"','LINESTRING(7.223511 53.1805270078955,7.223511 53.179704,7.21527100000001 53.121229,7.24273699999999 52.932086,7.16056753068224 52.8486333236236)' 14813,7053,7430,192856.020480538,f,f,,'LINESTRING(10.8823542109264 53.948125809387,11.25 54.061,11.657867 54.186548,12.208557 54.386955,12.236023 54.402946,12.43515 54.541003,12.602692 54.684153,12.745514 54.844199,12.744141 54.842618,12.87735 54.979978,12.947388 55.077581,12.9299984288384 55.0630403498842)' diff --git a/data/parameter_corrections.yaml b/data/parameter_corrections.yaml index df15738af..3d19bed8d 100644 --- a/data/parameter_corrections.yaml +++ b/data/parameter_corrections.yaml @@ -15,6 +15,7 @@ Link: "115000": 1200 # Caithness Moray HVDC index: "14804": 1400 # North-Sea link (NSN Link) + "8394": 1400 # North-Sea Link (NSN Link) continuation "14822": 700 # NO-DK Skagerrak 4 "14827": 440 # NO-DK Skagerrak 3 "14810": 500 # NO-DK Skagerrak 1-2 From d58a848f97f52916730f794c35131d67c6671012 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 13 Aug 2024 16:46:34 +0200 Subject: [PATCH 195/344] [pre-commit.ci] pre-commit autoupdate (#1208) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black-pre-commit-mirror: 24.4.2 → 24.8.0](https://github.com/psf/black-pre-commit-mirror/compare/24.4.2...24.8.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 72eabf9e6..8b9bf2e00 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -51,7 +51,7 @@ repos: # Formatting with "black" coding style - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.4.2 + rev: 24.8.0 hooks: # Format Python files - id: black From 7542d57820e84970f90ff2407c13238200156763 Mon Sep 17 00:00:00 2001 From: AmosSchledorn Date: Tue, 13 Aug 2024 17:07:20 +0200 Subject: [PATCH 196/344] style: move get_country_from_node_name function from _helpers tu run --- scripts/_helpers.py | 3 --- scripts/build_cop_profiles/run.py | 4 +++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/scripts/_helpers.py b/scripts/_helpers.py index 1c890ea58..d97144ba4 100644 --- a/scripts/_helpers.py +++ b/scripts/_helpers.py @@ -786,6 +786,3 @@ def get_snapshots(snapshots, drop_leap_day=False, freq="h", **kwargs): return time - -def get_country_from_node_name(node_name: str) -> str: - return node_name[:2] diff --git a/scripts/build_cop_profiles/run.py b/scripts/build_cop_profiles/run.py index 23229e987..401fdc18e 100644 --- a/scripts/build_cop_profiles/run.py +++ b/scripts/build_cop_profiles/run.py @@ -46,7 +46,7 @@ import numpy as np import pandas as pd import xarray as xr -from _helpers import get_country_from_node_name, set_scenario_config +from _helpers import set_scenario_config from CentralHeatingCopApproximator import CentralHeatingCopApproximator from DecentralHeatingCopApproximator import DecentralHeatingCopApproximator @@ -134,6 +134,8 @@ def get_cop( source_type=heat_source, ).approximate_cop() +def get_country_from_node_name(node_name: str) -> str: + return node_name[:2] if __name__ == "__main__": if "snakemake" not in globals(): From 92435678723b01482d544f0606ff5c26ced8063c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 13 Aug 2024 15:08:47 +0000 Subject: [PATCH 197/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/_helpers.py | 1 - scripts/build_cop_profiles/run.py | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/_helpers.py b/scripts/_helpers.py index d97144ba4..a3b77c1c0 100644 --- a/scripts/_helpers.py +++ b/scripts/_helpers.py @@ -785,4 +785,3 @@ def get_snapshots(snapshots, drop_leap_day=False, freq="h", **kwargs): time = time[~((time.month == 2) & (time.day == 29))] return time - diff --git a/scripts/build_cop_profiles/run.py b/scripts/build_cop_profiles/run.py index 401fdc18e..b4ec3e43f 100644 --- a/scripts/build_cop_profiles/run.py +++ b/scripts/build_cop_profiles/run.py @@ -134,9 +134,11 @@ def get_cop( source_type=heat_source, ).approximate_cop() + def get_country_from_node_name(node_name: str) -> str: return node_name[:2] + if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake From 6ca8e80095c9304cb5c88804956ac486f584d83d Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Tue, 13 Aug 2024 18:13:32 +0200 Subject: [PATCH 198/344] add option to use atlite to smooth wind turbine power curves (#1209) --- config/config.default.yaml | 5 +++++ doc/configtables/offwind-ac.csv | 1 + doc/configtables/offwind-dc.csv | 1 + doc/configtables/onwind.csv | 1 + doc/release_notes.rst | 3 +++ 5 files changed, 11 insertions(+) diff --git a/config/config.default.yaml b/config/config.default.yaml index 306624d07..52512136d 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -155,6 +155,7 @@ renewable: resource: method: wind turbine: Vestas_V112_3MW + smooth: true add_cutout_windspeed: true capacity_per_sqkm: 3 # correction_factor: 0.93 @@ -174,6 +175,7 @@ renewable: resource: method: wind turbine: NREL_ReferenceTurbine_2020ATB_5.5MW + smooth: true add_cutout_windspeed: true capacity_per_sqkm: 2 correction_factor: 0.8855 @@ -190,6 +192,7 @@ renewable: resource: method: wind turbine: NREL_ReferenceTurbine_2020ATB_5.5MW + smooth: true add_cutout_windspeed: true capacity_per_sqkm: 2 correction_factor: 0.8855 @@ -206,6 +209,8 @@ renewable: resource: method: wind turbine: NREL_ReferenceTurbine_5MW_offshore + smooth: true + add_cutout_windspeed: true # ScholzPhd Tab 4.3.1: 10MW/km^2 capacity_per_sqkm: 2 correction_factor: 0.8855 diff --git a/doc/configtables/offwind-ac.csv b/doc/configtables/offwind-ac.csv index 9ba2fa7e4..c9c7c8435 100644 --- a/doc/configtables/offwind-ac.csv +++ b/doc/configtables/offwind-ac.csv @@ -3,6 +3,7 @@ cutout,--,"Should be a folder listed in the configuration ``atlite: cutouts:`` ( resource,,, -- method,--,"Must be 'wind'","A superordinate technology type." -- turbine,--,"One of turbine types included in `atlite `_. Can be a string or a dictionary with years as keys which denote the year another turbine model becomes available.","Specifies the turbine type and its characteristic power curve." +-- smooth,--,"{True, False}","Switch to apply a gaussian kernel density smoothing to the power curve." capacity_per_sqkm,:math:`MW/km^2`,float,"Allowable density of wind turbine placement." correction_factor,--,float,"Correction factor for capacity factor time series." excluder_resolution,m,float,"Resolution on which to perform geographical elibility analysis." diff --git a/doc/configtables/offwind-dc.csv b/doc/configtables/offwind-dc.csv index e55d8944e..86bf0cdcf 100644 --- a/doc/configtables/offwind-dc.csv +++ b/doc/configtables/offwind-dc.csv @@ -3,6 +3,7 @@ cutout,--,"Should be a folder listed in the configuration ``atlite: cutouts:`` ( resource,,, -- method,--,"Must be 'wind'","A superordinate technology type." -- turbine,--,"One of turbine types included in `atlite `_. Can be a string or a dictionary with years as keys which denote the year another turbine model becomes available.","Specifies the turbine type and its characteristic power curve." +-- smooth,--,"{True, False}","Switch to apply a gaussian kernel density smoothing to the power curve." capacity_per_sqkm,:math:`MW/km^2`,float,"Allowable density of wind turbine placement." correction_factor,--,float,"Correction factor for capacity factor time series." excluder_resolution,m,float,"Resolution on which to perform geographical elibility analysis." diff --git a/doc/configtables/onwind.csv b/doc/configtables/onwind.csv index a801d83cd..676494f4e 100644 --- a/doc/configtables/onwind.csv +++ b/doc/configtables/onwind.csv @@ -3,6 +3,7 @@ cutout,--,"Should be a folder listed in the configuration ``atlite: cutouts:`` ( resource,,, -- method,--,"Must be 'wind'","A superordinate technology type." -- turbine,--,"One of turbine types included in `atlite `_. Can be a string or a dictionary with years as keys which denote the year another turbine model becomes available.","Specifies the turbine type and its characteristic power curve." +-- smooth,--,"{True, False}","Switch to apply a gaussian kernel density smoothing to the power curve." capacity_per_sqkm,:math:`MW/km^2`,float,"Allowable density of wind turbine placement." corine,,, -- grid_codes,--,"Any subset of the `CORINE Land Cover code list `_","Specifies areas according to CORINE Land Cover codes which are generally eligible for wind turbine placement." diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 5d95d4da8..274231c6c 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -9,6 +9,9 @@ Release Notes Upcoming Release ================ + +* Add option to apply a gaussian kernel density smoothing to wind turbine power curves. + * Update JRC-IDEES-2015 to `JRC-IDEES-2021 `__. The reference year is changed from 2015 to 2019. * Added unsustainable biomass potentials for solid, gaseous, and liquid biomass. The potentials can be phased-out and/or From f263862455248291366dd615fa41dd90646e3246 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Tue, 13 Aug 2024 21:03:07 +0200 Subject: [PATCH 199/344] use lower resolution EEZ shapes to reduce excessive RAM use (#1210) --- rules/build_electricity.smk | 2 +- rules/retrieve.smk | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/rules/build_electricity.smk b/rules/build_electricity.smk index 34472f27d..97e1e16aa 100644 --- a/rules/build_electricity.smk +++ b/rules/build_electricity.smk @@ -92,7 +92,7 @@ rule build_shapes: countries=config_provider("countries"), input: naturalearth=ancient("data/naturalearth/ne_10m_admin_0_countries_deu.shp"), - eez=ancient("data/eez/World_EEZ_v12_20231025_gpkg/eez_v12.gpkg"), + eez=ancient("data/eez/World_EEZ_v12_20231025_LR/eez_v12_lowres.gpkg"), nuts3=ancient("data/bundle/NUTS_2013_60M_SH/data/NUTS_RG_60M_2013.shp"), nuts3pop=ancient("data/bundle/nama_10r_3popgdp.tsv.gz"), nuts3gdp=ancient("data/bundle/nama_10r_3gdp.tsv.gz"), diff --git a/rules/retrieve.smk b/rules/retrieve.smk index 3a46a076e..b2382f51f 100644 --- a/rules/retrieve.smk +++ b/rules/retrieve.smk @@ -226,9 +226,9 @@ if config["enable"]["retrieve"]: rule retrieve_eez: params: - zip="data/eez/World_EEZ_v12_20231025_gpkg.zip", + zip="data/eez/World_EEZ_v12_20231025_LR.zip", output: - gpkg="data/eez/World_EEZ_v12_20231025_gpkg/eez_v12.gpkg", + gpkg="data/eez/World_EEZ_v12_20231025_LR/eez_v12_lowres.gpkg", run: import os import requests @@ -239,7 +239,7 @@ if config["enable"]["retrieve"]: response = requests.post( "https://www.marineregions.org/download_file.php", - params={"name": "World_EEZ_v12_20231025_gpkg.zip"}, + params={"name": "World_EEZ_v12_20231025_LR.zip"}, data={ "name": name, "organisation": org, From 3ef6d519d715efb2aec863f9c09ee2ec605c711b Mon Sep 17 00:00:00 2001 From: Bobby Xiong <36541459+bobbyxng@users.noreply.github.com> Date: Wed, 14 Aug 2024 10:24:38 +0200 Subject: [PATCH 200/344] Fix simplify_network.py to handle more complex topologies (#1211) * Fixed simplify_network.py to handle more complex topologies, i.e. if two links are connected to nearby AC buses separated by an AC line. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- scripts/simplify_network.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/scripts/simplify_network.py b/scripts/simplify_network.py index 558e4cf28..2b759a732 100644 --- a/scripts/simplify_network.py +++ b/scripts/simplify_network.py @@ -298,13 +298,24 @@ def simplify_links( _, labels = connected_components(adjacency_matrix, directed=False) labels = pd.Series(labels, n.buses.index) - G = n.graph() + # Only span graph over the DC link components + G = n.graph(branch_components=["Link"]) def split_links(nodes): nodes = frozenset(nodes) seen = set() - supernodes = {m for m in nodes if len(G.adj[m]) > 2 or (set(G.adj[m]) - nodes)} + + # Supernodes are endpoints of links, identified by having lass then two neighbours or being an AC Bus + # An example for the latter is if two different links are connected to the same AC bus. + supernodes = { + m + for m in nodes + if ( + (len(G.adj[m]) < 2 or (set(G.adj[m]) - nodes)) + or (n.buses.loc[m, "carrier"] == "AC") + ) + } for u in supernodes: for m, ls in G.adj[u].items(): From 1cf85b0be889683935ec3898b5eac494aff5dd1b Mon Sep 17 00:00:00 2001 From: Bobby Xiong <36541459+bobbyxng@users.noreply.github.com> Date: Wed, 14 Aug 2024 14:22:03 +0200 Subject: [PATCH 201/344] Fix for Corsica in simplify_network: Include local substation (#1215) * Fixed simplify_network.py to handle more complex topologies, i.e. if two links are connected to nearby AC buses separated by an AC line. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Added fix for Corsica node: If region containing Corsica is modelled, include substation on Corsica as supernode (end point). * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- scripts/simplify_network.py | 45 +++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/scripts/simplify_network.py b/scripts/simplify_network.py index 2b759a732..c54e3418b 100644 --- a/scripts/simplify_network.py +++ b/scripts/simplify_network.py @@ -306,6 +306,14 @@ def split_links(nodes): seen = set() + # Corsica substation + node_corsica = find_closest_bus( + n, + x=9.44802, + y=42.52842, + tol=2000, # Tolerance needed to only return the bus if the region is actually modelled + ) + # Supernodes are endpoints of links, identified by having lass then two neighbours or being an AC Bus # An example for the latter is if two different links are connected to the same AC bus. supernodes = { @@ -314,6 +322,7 @@ def split_links(nodes): if ( (len(G.adj[m]) < 2 or (set(G.adj[m]) - nodes)) or (n.buses.loc[m, "carrier"] == "AC") + or (m == node_corsica) ) } @@ -530,6 +539,42 @@ def cluster( return clustering.network, clustering.busmap +def find_closest_bus(n, x, y, tol=2000): + """ + Find the index of the closest bus to the given coordinates within a specified tolerance. + Parameters: + n (pypsa.Network): The network object. + x (float): The x-coordinate (longitude) of the target location. + y (float): The y-coordinate (latitude) of the target location. + tol (float): The distance tolerance in meters. Default is 2000 meters. + + Returns: + int: The index of the closest bus to the target location within the tolerance. + Returns None if no bus is within the tolerance. + """ + # Conversion factors + meters_per_degree_lat = 111139 # Meters per degree of latitude + meters_per_degree_lon = 111139 * np.cos( + np.radians(y) + ) # Meters per degree of longitude at the given latitude + + x0 = np.array(n.buses.x) + y0 = np.array(n.buses.y) + + # Calculate distances in meters + dist = np.sqrt( + ((x - x0) * meters_per_degree_lon) ** 2 + + ((y - y0) * meters_per_degree_lat) ** 2 + ) + + # Find the closest bus within the tolerance + min_dist = dist.min() + if min_dist <= tol: + return n.buses.index[dist.argmin()] + else: + return None + + if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake From f72afe7d102a2c605a9d8b7e00ebc3cd41ce284c Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 14 Aug 2024 16:27:06 +0200 Subject: [PATCH 202/344] bugfix: make sure to include Ukraine offshore shapes with new EEZ files --- scripts/build_shapes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/build_shapes.py b/scripts/build_shapes.py index 411d56a4d..4de5370a7 100644 --- a/scripts/build_shapes.py +++ b/scripts/build_shapes.py @@ -132,7 +132,8 @@ def countries(naturalearth, country_list): def eez(eez, country_list): df = gpd.read_file(eez) iso3_list = cc.convert(country_list, src="ISO2", to="ISO3") - df = df.query("ISO_TER1 in @iso3_list and POL_TYPE == '200NM'").copy() + pol_type = ["200NM", "Overlapping claim"] + df = df.query("ISO_TER1 in @iso3_list and POL_TYPE in @pol_type").copy() df["name"] = cc.convert(df.ISO_TER1, src="ISO3", to="ISO2") s = df.set_index("name").geometry.map( lambda s: _simplify_polys(s, filterremote=False) From a9f67b313f63df1b2d550fc04bac634416116752 Mon Sep 17 00:00:00 2001 From: Philipp Glaum <95913147+p-glaum@users.noreply.github.com> Date: Thu, 15 Aug 2024 11:42:21 +0200 Subject: [PATCH 203/344] handle new and upgraded TYNDP&NEP lines/links in base network (OSM compatible) (#1085) * add general implementation to add tyndp and nep. TODO: Remove duplicate? * pre_osm_merge * clean and update base_network.py update default config settings * clean and update base_network.py update default config settings * base_network.py:remove adding of transmission projects add_transmission_project.py: add new script for creating lines and link csv from transmission projects add_electricity.py: add new projects from created csv files * cluster_network.py: do not allow to group lines with different build years together-> requires pypsa update simplify_network.py: fix bug of simplify links * remove legacies of removing transmission projects from base_network * restructure folders:new folder for tranmission project csv add_transmission_projects: improve logging, cleanup * update lines csvs and add default line type for upgraded and new lines * remove duplicate lines which were already in gridkit * allow to connect ac lines only to ac buses * add manual links csv (adjusted links_tyndp.csv) and update default config * add realease note * remove links_tyndp.csv file, references in build_elec.smk and function in base_network.py to add them * add configuration options for transmission projects to documentation and add template folder for transmission projects * update pypsa version in environments * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * integrate Fabian's review comments 2 * add electricity:add import pathlib and duplicate printing out of adding line projects * update NEP line csv * address Fabian's comments * build_transmission_projects: address Fabian's final comments simplify_network: use modus to get line type which appears most often * build_transmission_project: change from .geometry to ["geometry"] * build_transmission_projects: remove redundanty line * build_transmission_projects: remove buffer for europe shape because of higher resolution default config: fix wrong key for skip argument in transmission_projects * update configtables and default config * update manual links csv and delete undetected duplicate links in tyndp2020 * final adjustments --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Fabian Neumann --- config/config.default.yaml | 23 +- data/links_tyndp.csv | 41 -- .../manual/new_links.csv | 21 + data/transmission_projects/nep/new_lines.csv | 16 + data/transmission_projects/nep/new_links.csv | 12 + .../template/new_lines.csv | 5 + .../template/new_links.csv | 5 + .../template/upgraded_lines.csv | 5 + .../template/upgraded_links.csv | 5 + .../tyndp2020/new_lines.csv | 119 ++++ .../tyndp2020/new_links.csv | 49 ++ .../tyndp2020/upgraded_lines.csv | 27 + .../tyndp2020/upgraded_links.csv | 8 + doc/configtables/links.csv | 1 - doc/configtables/transmission_projects.csv | 9 + doc/configuration.rst | 17 + doc/release_notes.rst | 1 + envs/environment.fixed.yaml | 2 +- envs/environment.yaml | 2 +- rules/build_electricity.smk | 48 +- scripts/add_electricity.py | 23 + scripts/base_network.py | 112 ---- scripts/build_transmission_projects.py | 564 ++++++++++++++++++ scripts/cluster_network.py | 1 + scripts/simplify_network.py | 5 +- 25 files changed, 958 insertions(+), 163 deletions(-) delete mode 100644 data/links_tyndp.csv create mode 100644 data/transmission_projects/manual/new_links.csv create mode 100644 data/transmission_projects/nep/new_lines.csv create mode 100644 data/transmission_projects/nep/new_links.csv create mode 100644 data/transmission_projects/template/new_lines.csv create mode 100644 data/transmission_projects/template/new_links.csv create mode 100644 data/transmission_projects/template/upgraded_lines.csv create mode 100644 data/transmission_projects/template/upgraded_links.csv create mode 100644 data/transmission_projects/tyndp2020/new_lines.csv create mode 100644 data/transmission_projects/tyndp2020/new_links.csv create mode 100644 data/transmission_projects/tyndp2020/upgraded_lines.csv create mode 100644 data/transmission_projects/tyndp2020/upgraded_links.csv create mode 100644 doc/configtables/transmission_projects.csv create mode 100644 scripts/build_transmission_projects.py diff --git a/config/config.default.yaml b/config/config.default.yaml index 52512136d..14221dac6 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -286,7 +286,7 @@ lines: max_extension: 20000 #MW length_factor: 1.25 reconnect_crimea: true - under_construction: 'keep' # 'zero': set capacity to zero, 'remove': remove, 'keep': with full capacity + under_construction: 'keep' # 'zero': set capacity to zero, 'remove': remove, 'keep': with full capacity for lines in grid extract dynamic_line_rating: activate: false cutout: europe-2013-sarah3-era5 @@ -299,8 +299,25 @@ links: p_max_pu: 1.0 p_nom_max: .inf max_extension: 30000 #MW - include_tyndp: true - under_construction: 'zero' # 'zero': set capacity to zero, 'remove': remove, 'keep': with full capacity + under_construction: 'keep' # 'zero': set capacity to zero, 'remove': remove, 'keep': with full capacity for lines in grid extract + +# docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#transmission_projects +transmission_projects: + enable: true + include: + tyndp2020: true + nep: true + manual: true + skip: + - upgraded_lines + - upgraded_links + status: + - under_construction + - in_permitting + - confirmed + #- planned_not_yet_permitted + #- under_consideration + new_link_capacity: zero #keep or zero # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#transformers transformers: diff --git a/data/links_tyndp.csv b/data/links_tyndp.csv deleted file mode 100644 index 43030be57..000000000 --- a/data/links_tyndp.csv +++ /dev/null @@ -1,41 +0,0 @@ -Name,Converterstation 1,Converterstation 2,Length (given) (km),Length (distance*1.2) (km),Power (MW),status,replaces,Ref,x1,y1,x2,y2 -Biscay Gulf,Gatica (ES),Cubnezais (FR),370,,2200,in permitting,,https://tyndp.entsoe.eu/tyndp2018/projects/projects/16,-2.867,43.367,-0.408943,45.074191 -Italy-France,Piossasco (IT),Grand Ile (FR),190,,1000,under construction,,https://tyndp.entsoe.eu/tyndp2018/projects/projects/21,7.468,44.9898,6.045,45.472 -IFA2,Tourbe (FR),Chilling (GB),,247.2,1000,under construction,,https://tyndp.entsoe.eu/tyndp2018/projects/projects/25,-0.172042,49.083593,-1.277269,50.839338 -Italy-Montenegro,Villanova (IT),Latsva (MT),445,,1200,under construction,Link.14539,https://tyndp.entsoe.eu/tyndp2018/projects/projects/28,14.125,42.3947222222222,18.7947222222222,42.3175 -NordLink,Tonstad (NO),Wilster (DE),514,,1400,under construction,,https://tyndp.entsoe.eu/tyndp2018/projects/projects/37,6.716948,58.662631,9.373979,53.922479 -COBRA cable,Endrup (DK),Eemshaven (NL),325,,700,under construction,,https://tyndp.entsoe.eu/tyndp2018/projects/projects/71,8.718392,55.523115,6.835494,53.438589 -Thames Estuary Cluster (NEMO-Link),Richborough (GB),Gezelle (BE),140,,1000,under construction,,https://tyndp.entsoe.eu/tyndp2018/projects/projects/74,1.324854,51.295891,3.23043,51.24902 -Anglo-Scottish -1,Hunterston (UK),Deeside (UK),422,,2400,built,,https://tyndp.entsoe.eu/tyndp2018/projects/projects/77,-4.898329,55.723331,-3.032972,53.199735 -ALEGrO,Lixhe (BE),Oberzier (DE),100,,1000,built,,https://tyndp.entsoe.eu/tyndp2018/projects/projects/92,5.67933,50.7567965,6.474704,50.867532 -North Sea Link,Kvilldal (NO),Blythe (GB),720,,1400,under construction,,https://tyndp.entsoe.eu/tyndp2018/projects/projects/110,6.637527,59.515096,-1.510277,55.126957 -HVDC SuedOstLink,Wolmirstedt (DE),Isar (DE),,557,2000,in permitting,,https://tyndp.entsoe.eu/tyndp2018/projects/projects/130,11.629014,52.252137,12.091596,48.080837 -HVDC Line A-North,Emden East (DE),Osterath (DE),,284,2000,in permitting,,https://tyndp.entsoe.eu/tyndp2018/projects/projects/132,7.206009,53.359403,6.619451,51.272935 -France-Alderney-Britain,Exeter (UK),Menuel (FR),220,,1400,in permitting,,https://tyndp.entsoe.eu/tyndp2018/projects/projects/153,-3.533899,50.718412,-1.469216,49.509594 -Viking DKW-GB,Bicker Fen (GB),Revsing (DK),,807,1400,in permitting,,https://tyndp.entsoe.eu/tyndp2018/projects/projects/167,-0.203587,52.93979,9.178363,55.509166 -ElecLink,Sellindge (UK),Mandarins (FR),,72,1000,under construction,,https://tyndp.entsoe.eu/tyndp2018/projects/projects/172,0.975555555555556,51.1058333333333,1.78472222222222,50.9030555555556 -Greenconnector,Verderio (IT),Sils i.D. (CH),150,,1000,in permitting,,https://tyndp.entsoe.eu/tyndp2018/projects/projects/174,9.439781,45.668539,9.76569,46.432156 -Hansa PowerBridge I,Hurva (SE),Guestrow (DE),,283,700,in permitting,,https://tyndp.entsoe.eu/tyndp2018/projects/projects/176,13.6022222222222,55.8330555555556,12.189538,53.803155 -NorthConnect,Simadalen (NO),Peterhead (UK),650,,1400,in permitting,,https://tyndp.entsoe.eu/tyndp2018/projects/projects/190,7.16027,60.500527,-1.784066,57.508123 -HVDC SuedLink,Wilster (DE),Großgartach (DE),,637,4000,in permitting,,https://tyndp.entsoe.eu/tyndp2018/projects/projects/235,9.373979,53.922479,9.117193,49.145157 -AQUIND Interconnector,Lovedean (GB),Barnabos (FR),254,,2000,in permitting,,https://tyndp.entsoe.eu/tyndp2018/projects/projects/247,-1.020512,50.908244,0.991736,49.656631 -HVDC Ultranet,Osterath (DE),Philippsburg (DE),,314,600,in permitting,,https://tyndp.entsoe.eu/tyndp2018/projects/projects/254,6.619451,51.272935,8.458036,49.235253 -Gridlink,Kingsnorth (UK),Warande (FR),160,,1400,in permitting,,https://tyndp.entsoe.eu/tyndp2018/projects/projects/285,0.596111111111111,51.41972,2.376776,51.034368 -NeuConnect,Grain (UK),Fedderwarden (DE),680,,1400,in permitting,,https://tyndp.entsoe.eu/tyndp2018/projects/projects/309,0.716666666666667,51.44,8.046524,53.562763 -NordBalt,Klaipeda (LT),Nybro (SE),450,,700,built,,https://en.wikipedia.org/wiki/NordBalt,21.256667,55.681667,15.854167,56.767778 -Estlink 1,Harku (EE),Espoo (FI),105,,350,built,,https://en.wikipedia.org/wiki/Estlink,24.560278,59.384722,24.551667,60.203889 -Greenlink,Waterford (IE),Pembroke (UK),,180,500,under construction,,https://tyndp2022-project-platform.azurewebsites.net/projectsheets/transmission/286,-6.987,52.260,-4.986,51.686 -Celtic Interconnector,Aghada (IE),La Martyre (FR),,572,700,under consideration,,https://tyndp2022-project-platform.azurewebsites.net/projectsheets/transmission/107,-8.16642,51.91413,-4.184,48.459 -GiLA,Bordeaux (FR),Nantes (FR),,312,640,under consideration,,https://eepublicdownloads.blob.core.windows.net/public-cdn-container/tyndp-documents/TYNDP2024/240220_TYNDP2024_project_portfolio.xlsx,-1.209,46.901,-0.576,44.960 -HG North Tyrrhenian Corridor,Milan (IT),Viterbo (IT),,500,2000,in permitting,,https://eepublicdownloads.blob.core.windows.net/public-cdn-container/tyndp-documents/TYNDP2024/240220_TYNDP2024_project_portfolio.xlsx,9.409,45.553,12.015,42.244 -HG Adriatic Corridor,Ferrara (IT),Foggia (IT),,582,2000,in permitting,,https://eepublicdownloads.blob.core.windows.net/public-cdn-container/tyndp-documents/TYNDP2024/240220_TYNDP2024_project_portfolio.xlsx,11.661,44.855,15.550,41.513 -SAPEI 2,Fioumesanto (IT),Montalto (IT),,390,1000,in permitting,,https://eepublicdownloads.blob.core.windows.net/public-cdn-container/tyndp-documents/TYNDP2024/240220_TYNDP2024_project_portfolio.xlsx,8.283,40.790,11.602,42.331 -HG Ionian-Tyrrhenian Corridor,Rossano (IT),Latina (IT),,496,2000,in permitting,,https://eepublicdownloads.blob.core.windows.net/public-cdn-container/tyndp-documents/TYNDP2024/240220_TYNDP2024_project_portfolio.xlsx,16.629,39.568,12.779,41.430 -HG Ionian-Tyrrhenian Corridor 2,Rossano (IT),Catania (IT),,330,2000,in permitting,,https://eepublicdownloads.blob.core.windows.net/public-cdn-container/tyndp-documents/TYNDP2024/240220_TYNDP2024_project_portfolio.xlsx,16.629,39.568,15.049,37.408 -Germany-UK Hybrid Interconnector,Fetteresso (UK),Emden (DE),800,,2000,under consideration,,https://eepublicdownloads.blob.core.windows.net/public-cdn-container/tyndp-documents/TYNDP2024/240220_TYNDP2024_project_portfolio.xlsx,-2.383,56.991,7.207,53.376 -NU-Link Interconnector,Hornsea (UK),Moerdijk (NL),,460,1200,under consideration,,https://eepublicdownloads.blob.core.windows.net/public-cdn-container/tyndp-documents/TYNDP2024/240220_TYNDP2024_project_portfolio.xlsx,-0.261,53.655,4.586,51.661 -APOLLO-LINK,La Farga (ES),La Spezia (IT),,725,2091,under consideration,,https://eepublicdownloads.blob.core.windows.net/public-cdn-container/tyndp-documents/TYNDP2024/240220_TYNDP2024_project_portfolio.xlsx,2.883,42.062,9.884,44.107 -Baltic WindConnector (BWC),Lubmin (DE),Lihula (EE),,960,2000,under consideration,,https://eepublicdownloads.blob.core.windows.net/public-cdn-container/tyndp-documents/TYNDP2024/240220_TYNDP2024_project_portfolio.xlsx,13.686,54.139,23.818,58.675 -High-Voltage Direct Current Interconnector Project Romania-Hungary,Constanta (RO),Albertirsa (HU),,930,2500,under consideration,,https://eepublicdownloads.blob.core.windows.net/public-cdn-container/tyndp-documents/TYNDP2024/240220_TYNDP2024_project_portfolio.xlsx,28.588,44.201,19.584,47.224 -Rhine-Main-Link,Ovelgönne (DE),Marxheim (DE),,433,4000,in permitting,,https://eepublicdownloads.blob.core.windows.net/public-cdn-container/tyndp-documents/TYNDP2024/240220_TYNDP2024_project_portfolio.xlsx,8.379,53.315,8.435,50.078 -Green Aegean Interconnector,Arachthos (GR),Ottenhofen (DE),,600,3000,under consideration,,https://eepublicdownloads.blob.core.windows.net/public-cdn-container/tyndp-documents/TYNDP2024/240220_TYNDP2024_project_portfolio.xlsx,20.967,39.185,11.868,48.207 diff --git a/data/transmission_projects/manual/new_links.csv b/data/transmission_projects/manual/new_links.csv new file mode 100644 index 000000000..901f95931 --- /dev/null +++ b/data/transmission_projects/manual/new_links.csv @@ -0,0 +1,21 @@ +,bus0,bus1,length,p_nom,project_status,tags,x0,y0,x1,y1 +TYNDP2018_154,Exeter (UK),Menuel (FR),220,1400,in_permitting,"{name:France-Alderney-Britain, url:https://tyndp.entsoe.eu/tyndp2018/projects/projects/153}",-3.533899,50.718412,-1.469216,49.509594 +TYNDP2018_25,Tourbe (FR),Chilling (GB),247.2,1000,under_construction,"{name:IFA2, url:https://tyndp.entsoe.eu/tyndp2018/projects/projects/25]",-0.172042,49.083593,-1.277269,50.839338 +TYNDP2018_285,Kingsnorth (UK),Warande (FR),160,1400,in_permitting,"{name:Gridlink, url:https://tyndp.entsoe.eu/tyndp2018/projects/projects/285}",0.596111111111111,51.41972,2.376776,51.034368 +TYNDP2018_190,Simadalen (NO),Peterhead (UK),650,1400,in_permitting,"{name:NorthConnect, url:https://tyndp.entsoe.eu/tyndp2018/projects/projects/190}",7.16027,60.500527,-1.784066,57.508123 +TYNDP2018_16,Gatica (ES),Cubnezais (FR),370,2200,in_permitting,"{name:Biscay Gulf, url:https://tyndp.entsoe.eu/tyndp2018/projects/projects/16}",-2.867,43.367,-0.408943,45.074191 +TYNDP2018_21,Piossasco (IT),Grand Ile (FR),190,1000,under_construction,"{name:Italy-France, url:https://tyndp.entsoe.eu/tyndp2018/projects/projects/21}",7.468,44.9898,6.045,45.472 +TYNDP2022_286,Waterford (IE),Pembroke (UK),180,500,under_construction,"{name:Greenlink,url:{name:TYNDP2022_286,url:https://tyndp2022-project-platform.azurewebsites.net/projectsheets/transmission/286}}",-6.987,52.26,-4.986,51.686 +TYNDP2024_1134,Bordeaux (FR),Nantes (FR),312,640,under_consideration,"{name:GiLA,url:{name:TYNDP2024_1134,url:https://eepublicdownloads.blob.core.windows.net/public-cdn-container/tyndp-documents/TYNDP2024/240220_TYNDP2024_project_portfolio.xlsx}}",-1.209,46.901,-0.576,44.96 +TYNDP2024_1157,Milan (IT),Viterbo (IT),500,2000,in_permitting,"{name:HG North Tyrrhenian Corridor,url:{name:TYNDP2024_1157,url:https://eepublicdownloads.blob.core.windows.net/public-cdn-container/tyndp-documents/TYNDP2024/240220_TYNDP2024_project_portfolio.xlsx}}",9.409,45.553,12.015,42.244 +TYNDP2024_1166,Ferrara (IT),Foggia (IT),582,2000,in_permitting,"{name:HG Adriatic Corridor,url:{name:TYNDP2024_1166,url:https://eepublicdownloads.blob.core.windows.net/public-cdn-container/tyndp-documents/TYNDP2024/240220_TYNDP2024_project_portfolio.xlsx}}",11.661,44.855,15.55,41.513 +TYNDP2024_1169,Fioumesanto (IT),Montalto (IT),390,1000,in_permitting,"{name:SAPEI 2,url:{name:TYNDP2024_1169,url:https://eepublicdownloads.blob.core.windows.net/public-cdn-container/tyndp-documents/TYNDP2024/240220_TYNDP2024_project_portfolio.xlsx}}",8.283,40.79,11.602,42.331 +TYNDP2024_1168,Rossano (IT),Latina (IT),496,2000,in_permitting,"{name:HG Ionian-Tyrrhenian Corridor,url:{name:TYNDP2024_1168,url:https://eepublicdownloads.blob.core.windows.net/public-cdn-container/tyndp-documents/TYNDP2024/240220_TYNDP2024_project_portfolio.xlsx}}",16.629,39.568,12.779,41.43 +TYNDP2024_1168_1,Rossano (IT),Catania (IT),330,2000,in_permitting,"{name:HG Ionian-Tyrrhenian Corridor 2,url:{name:TYNDP2024_1168_1,url:https://eepublicdownloads.blob.core.windows.net/public-cdn-container/tyndp-documents/TYNDP2024/240220_TYNDP2024_project_portfolio.xlsx}}",16.629,39.568,15.049,37.408 +TYNDP2024_1192,Fetteresso (UK),Emden (DE),800,2000,under_consideration,"{name:Germany-UK Hybrid Interconnector,url:{name:TYNDP2024_1192,url:https://eepublicdownloads.blob.core.windows.net/public-cdn-container/tyndp-documents/TYNDP2024/240220_TYNDP2024_project_portfolio.xlsx}}",-2.383,56.991,7.207,53.376 +TYNDP2024_1197,Hornsea (UK),Moerdijk (NL),460,1200,under_consideration,"{name:NU-Link Interconnector,url:{name:TYNDP2024_1197,url:https://eepublicdownloads.blob.core.windows.net/public-cdn-container/tyndp-documents/TYNDP2024/240220_TYNDP2024_project_portfolio.xlsx}}",-0.261,53.655,4.586,51.661 +TYNDP2024_1210,La Farga (ES),La Spezia (IT),725,2091,under_consideration,"{name:APOLLO-LINK,url:{name:TYNDP2024_1210,url:https://eepublicdownloads.blob.core.windows.net/public-cdn-container/tyndp-documents/TYNDP2024/240220_TYNDP2024_project_portfolio.xlsx}}",2.883,42.062,9.884,44.107 +TYNDP2024_1211,Lubmin (DE),Sindi (EE),960,2000,under_consideration,"{name:Baltic WindConnector (BWC),url:{name:TYNDP2024_1211,url:https://eepublicdownloads.blob.core.windows.net/public-cdn-container/tyndp-documents/TYNDP2024/240220_TYNDP2024_project_portfolio.xlsx}}",13.686,54.139,24.671903,58.429161 +TYNDP2024_1216,Constanta (RO),Albertirsa (HU),930,2500,under_consideration,"{name:High-Voltage Direct Current Interconnector Project Romania-Hungary,url:{name:TYNDP2024_1216,url:https://eepublicdownloads.blob.core.windows.net/public-cdn-container/tyndp-documents/TYNDP2024/240220_TYNDP2024_project_portfolio.xlsx}}",28.588,44.201,19.584,47.224 +TYNDP2024_1229,Ovelgönne (DE),Marxheim (DE),433,4000,in_permitting,"{name:Rhine-Main-Link,url:{name:TYNDP2024_1229,url:https://eepublicdownloads.blob.core.windows.net/public-cdn-container/tyndp-documents/TYNDP2024/240220_TYNDP2024_project_portfolio.xlsx}}",8.379,53.315,8.435,50.078 +TYNDP2024_1231,Arachthos (GR),Ottenhofen (DE),600,3000,under_consideration,"{name:Green Aegean Interconnector,url:{name:TYNDP2024_1231,url:https://eepublicdownloads.blob.core.windows.net/public-cdn-container/tyndp-documents/TYNDP2024/240220_TYNDP2024_project_portfolio.xlsx}}",20.967,39.185,11.868,48.207 diff --git a/data/transmission_projects/nep/new_lines.csv b/data/transmission_projects/nep/new_lines.csv new file mode 100644 index 000000000..19c50116a --- /dev/null +++ b/data/transmission_projects/nep/new_lines.csv @@ -0,0 +1,16 @@ +,bus0,bus1,length,v_nom,num_parallel,project_status,build_year,tags,x0,y0,x1,y1,type +P43,Dipperz/Hessen,Bergrheinfeld//Bayern,85,380.0,2.0,confirmed,2031,"{""url"":""https://data.netzausbau.de/2037-2023/NEP/NEP_2037_2045_Bestaetigung.pdf"", ""status"":""confirmed"", ""origin"":""NEP"",""version"":2024,""location0"":""Dipperz/Hessen"",""location1"":""Bergrheinfeld//Bayern""",9.807198712581199,50.54115855,10.1810033,50.0082136,Al/St 490/64 4-bundle 380.0 +P71,Kiel/Schleswig-Holstein,Göhl/Schleswig-Holstein,85,380.0,2.0,confirmed,2037,"{""url"":""https://data.netzausbau.de/2037-2023/NEP/NEP_2037_2045_Bestaetigung.pdf"", ""status"":""confirmed"", ""origin"":""NEP"",""version"":2024,""location0"":""Kiel/Schleswig-Holstein"",""location1"":""Göhl/Schleswig-Holstein""",10.135555,54.3227085,10.9399634,54.2861191,Al/St 490/64 4-bundle 380.0 +P84,Hamburg,Sahms/Schleswig-Holstein,35,380.0,2.0,confirmed,2031,"{""url"":""https://data.netzausbau.de/2037-2023/NEP/NEP_2037_2045_Bestaetigung.pdf"", ""status"":""confirmed"", ""origin"":""NEP"",""version"":2024,""location0"":""Hamburg"",""location1"":""Sahms/Schleswig-Holstein""",10.000654,53.550341,10.5331297,53.5252973,Al/St 490/64 4-bundle 380.0 +P227,Lübeck/Schleswig-Holstein,Sahms/Schleswig-Holstein,52,380.0,2.0,confirmed,2030,"{""url"":""https://data.netzausbau.de/2037-2023/NEP/NEP_2037_2045_Bestaetigung.pdf"", ""status"":""confirmed"", ""origin"":""NEP"",""version"":2024,""location0"":""Lübeck/Schleswig-Holstein"",""location1"":""Sahms/Schleswig-Holstein""",10.684738,53.866444,10.5331297,53.5252973,Al/St 490/64 4-bundle 380.0 +P402,Westerkappeln/Nordrhein-Westfalen,Gersteinwerk/Nordrhein-Westfalen,89,380.0,2.0,confirmed,2033,"{""url"":""https://data.netzausbau.de/2037-2023/NEP/NEP_2037_2045_Bestaetigung.pdf"", ""status"":""confirmed"", ""origin"":""NEP"",""version"":2024,""location0"":""Westerkappeln/Nordrhein-Westfalen"",""location1"":""Gersteinwerk/Nordrhein-Westfalen""",7.8772237,52.3141716,7.722207887811252,51.6749132,Al/St 490/64 4-bundle 380.0 +P470,Emden/Niedersachsen,Dörpen/West /Niedersachsen,66,380.0,2.0,confirmed,2037,"{""url"":""https://data.netzausbau.de/2037-2023/NEP/NEP_2037_2045_Bestaetigung.pdf"", ""status"":""confirmed"", ""origin"":""NEP"",""version"":2024,""location0"":""Emden/Niedersachsen"",""location1"":""Dörpen/West /Niedersachsen""",7.2058304,53.3670541,7.256622642835093,52.9807372,Al/St 490/64 4-bundle 380.0 +P476,Hochwöhrden/Schleswig-Holstein,Mehlbek/Schleswig-Holstein,39,380.0,2.0,confirmed,2032,"{""url"":""https://data.netzausbau.de/2037-2023/NEP/NEP_2037_2045_Bestaetigung.pdf"", ""status"":""confirmed"", ""origin"":""NEP"",""version"":2024,""location0"":""Hochwöhrden/Schleswig-Holstein"",""location1"":""Mehlbek/Schleswig-Holstein""",9.0323968,54.158602,9.4346536,54.0045912,Al/St 490/64 4-bundle 380.0 +P478,Pöschendorf/Schleswig-Holstein,Alfstedt/Niedersachsen,37,380.0,2.0,confirmed,2037,"{""url"":""https://data.netzausbau.de/2037-2023/NEP/NEP_2037_2045_Bestaetigung.pdf"", ""status"":""confirmed"", ""origin"":""NEP"",""version"":2024,""location0"":""Pöschendorf/Schleswig-Holstein"",""location1"":""Alfstedt/Niedersachsen""",9.4944931,54.0409041,9.0666915,53.5499782,Al/St 490/64 4-bundle 380.0 +P485,Herlasgrün/Sachsen,Marktleuthen/Bayern,69,380.0,2.0,confirmed,2037,"{""url"":""https://data.netzausbau.de/2037-2023/NEP/NEP_2037_2045_Bestaetigung.pdf"", ""status"":""confirmed"", ""origin"":""NEP"",""version"":2024,""location0"":""Herlasgrün/Sachsen"",""location1"":""Marktleuthen/Bayern""",12.2299537,50.5680975,11.9945062,50.1279651,Al/St 490/64 4-bundle 380.0 +P490,Petersgmünd /Bayern,Goldshöfe /Baden-Württemberg,104,380.0,2.0,confirmed,2037,"{""url"":""https://data.netzausbau.de/2037-2023/NEP/NEP_2037_2045_Bestaetigung.pdf"", ""status"":""confirmed"", ""origin"":""NEP"",""version"":2024,""location0"":""Petersgmünd /Bayern"",""location1"":""Goldshöfe /Baden-Württemberg""",11.0252473,49.185644,10.1279814,48.8940723,Al/St 490/64 4-bundle 380.0 +P540,Eisfeld/Thüringen,Grafenrheinfeld /Bayern,126,380.0,2.0,confirmed,2037,"{""url"":""https://data.netzausbau.de/2037-2023/NEP/NEP_2037_2045_Bestaetigung.pdf"", ""status"":""confirmed"", ""origin"":""NEP"",""version"":2024,""location0"":""Eisfeld/Thüringen"",""location1"":""Grafenrheinfeld /Bayern""",10.908994,50.4269984,10.1966808,50.0055317,Al/St 490/64 4-bundle 380.0 +P625,Streumen/Sachsen,Schmölln/Sachsen,92,380.0,2.0,confirmed,2035,"{""url"":""https://data.netzausbau.de/2037-2023/NEP/NEP_2037_2045_Bestaetigung.pdf"", ""status"":""confirmed"", ""origin"":""NEP"",""version"":2024,""location0"":""Streumen/Sachsen"",""location1"":""Schmölln/Sachsen""",13.4054041,51.3582767,14.2347005,51.1244043,Al/St 490/64 4-bundle 380.0 +P627,Schossin/Mecklenburg-Vorpommern,Perleberg/Brandenburg,89,380.0,2.0,confirmed,2037,"{""url"":""https://data.netzausbau.de/2037-2023/NEP/NEP_2037_2045_Bestaetigung.pdf"", ""status"":""confirmed"", ""origin"":""NEP"",""version"":2024,""location0"":""Schossin/Mecklenburg-Vorpommern"",""location1"":""Perleberg/Brandenburg""",11.2823593,53.5284043,11.8627933,53.0762716,Al/St 490/64 4-bundle 380.0 +P635,Grabowhöfe/Mecklenburg-Vorpommern,Marke/Sachsen-Anhalt,280,380.0,2.0,confirmed,2037,"{""url"":""https://data.netzausbau.de/2037-2023/NEP/NEP_2037_2045_Bestaetigung.pdf"", ""status"":""confirmed"", ""origin"":""NEP"",""version"":2024,""location0"":""Grabowhöfe/Mecklenburg-Vorpommern"",""location1"":""Marke/Sachsen-Anhalt""",12.594171,53.5683057,12.252458735300388,51.7301547,Al/St 490/64 4-bundle 380.0 +P636,Delitzsch/Sachsen,Eula/Sachsen,58,380.0,2.0,confirmed,2037,"{""url"":""https://data.netzausbau.de/2037-2023/NEP/NEP_2037_2045_Bestaetigung.pdf"", ""status"":""confirmed"", ""origin"":""NEP"",""version"":2024,""location0"":""Delitzsch/Sachsen"",""location1"":""Eula/Sachsen""",12.342857,51.5255661,12.5130329,51.1496022,Al/St 490/64 4-bundle 380.0 diff --git a/data/transmission_projects/nep/new_links.csv b/data/transmission_projects/nep/new_links.csv new file mode 100644 index 000000000..d237893f1 --- /dev/null +++ b/data/transmission_projects/nep/new_links.csv @@ -0,0 +1,12 @@ +,bus0,bus1,p_nom,length,project_status,build_year,underground,tags,x0,y0,x1,y1 +DC21,Wilhelmshaven/Niedersachsen,Lippetal/Nordrhein-Westfalen,2000.0,270,confirmed,2032,True,"{""url"":""https://data.netzausbau.de/2037-2023/NEP/NEP_2037_2045_Bestaetigung.pdf"", ""status"":""confirmed"", ""origin"":""NEP"",""version"":2024,""location0"":""Wilhelmshaven/Niedersachsen"",""location1"":""Lippetal/Nordrhein-Westfalen""",8.106301,53.5278793,8.0787633,51.6516157 +DC25,Heide/Schleswig-Holstein,Polsum/Nordrhein-Westfalen,2000.0,440,confirmed,2032,True,"{""url"":""https://data.netzausbau.de/2037-2023/NEP/NEP_2037_2045_Bestaetigung.pdf"", ""status"":""confirmed"", ""origin"":""NEP"",""version"":2024,""location0"":""Heide/Schleswig-Holstein"",""location1"":""Polsum/Nordrhein-Westfalen""",9.0929015,54.1948456,7.0528322,51.6264919 +DC31,Hemmingstedt/Schleswig-Holstein,Stralendorf/Mecklenburg-Vorpommern,2000.0,212,confirmed,2032,True,"{""url"":""https://data.netzausbau.de/2037-2023/NEP/NEP_2037_2045_Bestaetigung.pdf"", ""status"":""confirmed"", ""origin"":""NEP"",""version"":2024,""location0"":""Hemmingstedt/Schleswig-Holstein"",""location1"":""Stralendorf/Mecklenburg-Vorpommern""",9.08263,54.1506435,11.3010418,53.572637 +DC32,Pöschendorf/Schleswig-Holstein,Stralendorf/Mecklenburg-Vorpommern,2000.0,170,confirmed,2034,True,"{""url"":""https://data.netzausbau.de/2037-2023/NEP/NEP_2037_2045_Bestaetigung.pdf"", ""status"":""confirmed"", ""origin"":""NEP"",""version"":2024,""location0"":""Pöschendorf/Schleswig-Holstein"",""location1"":""Stralendorf/Mecklenburg-Vorpommern""",9.4944931,54.0409041,11.3010418,53.572637 +DC34,Ovelgönne/Niedersachsen,Bürstadt/Hessen,2000.0,523,confirmed,2033,True,"{""url"":""https://data.netzausbau.de/2037-2023/NEP/NEP_2037_2045_Bestaetigung.pdf"", ""status"":""confirmed"", ""origin"":""NEP"",""version"":2024,""location0"":""Ovelgönne/Niedersachsen"",""location1"":""Bürstadt/Hessen""",8.4209977,53.3427799,8.4747076,49.6531823 +DC35,Ovelgönne/Niedersachsen,Marxheim/Taunus,2000.0,461,confirmed,2035,True,"{""url"":""https://data.netzausbau.de/2037-2023/NEP/NEP_2037_2045_Bestaetigung.pdf"", ""status"":""confirmed"", ""origin"":""NEP"",""version"":2024,""location0"":""Ovelgönne/Niedersachsen"",""location1"":""Marxheim/Taunus""",8.4209977,53.3427799,8.4320722,50.0724011 +DC40,Nüttermoor/Niedersachsen,Streumen/Sachsen,2000.0,594,confirmed,2037,True,"{""url"":""https://data.netzausbau.de/2037-2023/NEP/NEP_2037_2045_Bestaetigung.pdf"", ""status"":""confirmed"", ""origin"":""NEP"",""version"":2024,""location0"":""Nüttermoor/Niedersachsen"",""location1"":""Streumen/Sachsen""",7.4360347,53.2625382,13.4054041,51.3582767 +DC40plus,Dörpen/West,Klostermansfeld/Sachsen-Anhalt,2000.0,426,confirmed,2037,True,"{""url"":""https://data.netzausbau.de/2037-2023/NEP/NEP_2037_2045_Bestaetigung.pdf"", ""status"":""confirmed"", ""origin"":""NEP"",""version"":2024,""location0"":""Dörpen/West"",""location1"":""Klostermansfeld/Sachsen-Anhalt""",7.256622642835093,52.9807372,11.4933395,51.5841525 +DC41,Alfstedt/Niedersachsen,Hüffenhardt/Baden-Württemberg,2000.0,607,confirmed,2037,True,"{""url"":""https://data.netzausbau.de/2037-2023/NEP/NEP_2037_2045_Bestaetigung.pdf"", ""status"":""confirmed"", ""origin"":""NEP"",""version"":2024,""location0"":""Alfstedt/Niedersachsen"",""location1"":""Hüffenhardt/Baden-Württemberg""",9.0666915,53.5499782,9.0827453,49.2908862 +DC42,Sahms/Schleswig-Holstein,Böblingen/Baden-Württemberg,2000.0,737,confirmed,2037,True,"{""url"":""https://data.netzausbau.de/2037-2023/NEP/NEP_2037_2045_Bestaetigung.pdf"", ""status"":""confirmed"", ""origin"":""NEP"",""version"":2024,""location0"":""Sahms/Schleswig-Holstein"",""location1"":""Böblingen/Baden-Württemberg""",10.5331297,53.5252973,9.0113444,48.684969 +DC42plus,Sahms/Schleswig-Holstein,Trennfeld/Bayern,2000.0,546,confirmed,2037,True,"{""url"":""https://data.netzausbau.de/2037-2023/NEP/NEP_2037_2045_Bestaetigung.pdf"", ""status"":""confirmed"", ""origin"":""NEP"",""version"":2024,""location0"":""Sahms/Schleswig-Holstein"",""location1"":""Trennfeld/Bayern""",10.5331297,53.5252973,9.6151453,49.7950676 diff --git a/data/transmission_projects/template/new_lines.csv b/data/transmission_projects/template/new_lines.csv new file mode 100644 index 000000000..2f75428c3 --- /dev/null +++ b/data/transmission_projects/template/new_lines.csv @@ -0,0 +1,5 @@ +,project_status,length,build_year,underground,v_nom,tags,x0,y0,x1,y1,num_parallel +New_line_1,in_permitting,100,2025,False,380,"{link to source, name of project, further custom information}",-7.793621146853301,41.52102394053344,-8.38943499999999,40.95916,2 +New_line_2,under_construction,100,2023,False,380,"{link to source, name of project, further custom information}",,,,, +New_line_3,in_planning,100,2040,False,380,"{link to source, name of project, further custom information}",,,,, +New_line_4,under_consideration,100,2050,true,380,"{link to source, name of project, further custom information}",,,,, diff --git a/data/transmission_projects/template/new_links.csv b/data/transmission_projects/template/new_links.csv new file mode 100644 index 000000000..ff2d652eb --- /dev/null +++ b/data/transmission_projects/template/new_links.csv @@ -0,0 +1,5 @@ +,project_status,length,build_year,underground,p_nom,tags,x0,y0,x1,y1 +New_link_1,in_permitting,100,2025,False,1000,"{link to source, name of project, further custom information}",-7.793621,41.52145,-8.38944,40.95915 +New_link_2,under_construction,100,2023,False,600,"{link to source, name of project, further custom information}",-7.793621,41.52145,-8.38944,40.95915 +New_link_3,in_planning,100,2040,False,2000,"{link to source, name of project, further custom information}",-7.793621,41.52145,-8.38944,40.95915 +New_link_4,under_consideration,100,2050,true,600,"{link to source, name of project, further custom information}",-7.793621,41.52145,-8.38944,40.95915 diff --git a/data/transmission_projects/template/upgraded_lines.csv b/data/transmission_projects/template/upgraded_lines.csv new file mode 100644 index 000000000..86192a3f7 --- /dev/null +++ b/data/transmission_projects/template/upgraded_lines.csv @@ -0,0 +1,5 @@ +,project_status,length,build_year,underground,v_nom,tags,x0,y0,x1,y1,num_parallel +Upgraded_line_1,in_permitting,100,2025,False,380,"{link to source, name of project, further custom information}",-7.793621146853301,41.52102394053344,-8.38943499999999,40.95916,2 +Upgraded_line_2,under_construction,100,2023,False,380,"{link to source, name of project, further custom information}",,,,, +Upgraded_line_3,in_planning,100,2040,False,380,"{link to source, name of project, further custom information}",,,,, +Upgraded_line_4,under_consideration,100,2050,true,380,"{link to source, name of project, further custom information}",,,,, diff --git a/data/transmission_projects/template/upgraded_links.csv b/data/transmission_projects/template/upgraded_links.csv new file mode 100644 index 000000000..65270cc37 --- /dev/null +++ b/data/transmission_projects/template/upgraded_links.csv @@ -0,0 +1,5 @@ +,project_status,length,build_year,underground,p_nom,tags,x0,y0,x1,y1 +Upgraded_link_1,in_permitting,100,2025,False,1000,"{link to source, name of project, further custom information}",-7.793621,41.52145,-8.38944,40.95915 +Upgraded_link_2,under_construction,100,2023,False,600,"{link to source, name of project, further custom information}",-7.793621,41.52145,-8.38944,40.95915 +Upgraded_link_3,in_planning,100,2040,False,2000,"{link to source, name of project, further custom information}",-7.793621,41.52145,-8.38944,40.95915 +Upgraded_link_4,under_consideration,100,2050,true,600,"{link to source, name of project, further custom information}",-7.793621,41.52145,-8.38944,40.95915 diff --git a/data/transmission_projects/tyndp2020/new_lines.csv b/data/transmission_projects/tyndp2020/new_lines.csv new file mode 100644 index 000000000..247296550 --- /dev/null +++ b/data/transmission_projects/tyndp2020/new_lines.csv @@ -0,0 +1,119 @@ +,project_status,length,build_year,underground,v_nom,tags,x0,y0,x1,y1,num_parallel,type +TYNDP2020_0,in_permitting,131.0,2022,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/1"", ""tyndp2020_proj_id""=>""1"", ""tyndp2020_invest_id""=>""4"", ""tyndp_status""=>""in_permitting""",-7.793621146853301,41.52102394053344,-8.38943499999999,40.95916,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_1,in_permitting,30.0,2022,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/4"", ""tyndp2020_proj_id""=>""4"", ""tyndp2020_invest_id""=>""18"", ""tyndp_status""=>""in_permitting""",-8.273869552394737,42.46748640601422,-7.94998199999999,42.383908,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_2,in_permitting,140.21,2022,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/4"", ""tyndp2020_proj_id""=>""4"", ""tyndp2020_invest_id""=>""496"", ""tyndp_status""=>""in_permitting""",-7.94998199999999,42.383908,-8.582626164089064,41.764651842353146,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_3,in_permitting,140.21,2022,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/4"", ""tyndp2020_proj_id""=>""4"", ""tyndp2020_invest_id""=>""496"", ""tyndp_status""=>""in_permitting""",-8.582626164089064,41.764651842353146,-8.67645299999999,41.303603,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_4,under_construction,23.0,2022,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/23"", ""tyndp2020_proj_id""=>""23"", ""tyndp2020_invest_id""=>""60"", ""tyndp_status""=>""under_construction""",3.08990499999999,50.50906,3.429108,50.773813,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_6,in_permitting,80.0,2023,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/33"", ""tyndp2020_proj_id""=>""33"", ""tyndp2020_invest_id""=>""90"", ""tyndp_status""=>""in_permitting""",11.126404,43.872158,11.413422,44.580687,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_8,in_permitting,120.5,2026,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/35"", ""tyndp2020_proj_id""=>""35"", ""tyndp2020_invest_id""=>""313"", ""tyndp_status""=>""in_permitting""",14.3524,49.1584,15.687103,49.531448,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_9,in_permitting,117.0,2028,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/35"", ""tyndp2020_proj_id""=>""35"", ""tyndp2020_invest_id""=>""315"", ""tyndp_status""=>""in_permitting""",14.3524,49.1584,13.282471,49.60804,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_11,under_construction,110.0,2020,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/39"", ""tyndp2020_proj_id""=>""39"", ""tyndp2020_invest_id""=>""144"", ""tyndp_status""=>""under_construction""",9.75860599999998,54.306108,9.29580699999999,55.075223,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_12,planned_not_yet_permitting,157.0,2027,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/47"", ""tyndp2020_proj_id""=>""47"", ""tyndp2020_invest_id""=>""689"", ""tyndp_status""=>""planned_not_yet_permitting""",10.06485,48.282279,10.897064,47.236355,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_13,under_construction,110.0,2020,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/48"", ""tyndp2020_proj_id""=>""48"", ""tyndp2020_invest_id""=>""1500"", ""tyndp_status""=>""under_construction""",17.539673,47.882276,17.786865,47.653363,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_14,under_construction,110.0,2020,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/48"", ""tyndp2020_proj_id""=>""48"", ""tyndp2020_invest_id""=>""1500"", ""tyndp_status""=>""under_construction""",17.539673,47.882276,18.540802,48.236565,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_15,under_construction,48.0,2020,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/48"", ""tyndp2020_proj_id""=>""48"", ""tyndp2020_invest_id""=>""1501"", ""tyndp_status""=>""under_construction""",19.958038,48.380882,20.55954,48.263084,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_16,in_permitting,60.0,2024,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/78"", ""tyndp2020_proj_id""=>""78"", ""tyndp2020_invest_id""=>""458"", ""tyndp_status""=>""in_permitting""",-3.21075400000001,51.135416,-2.633972,51.533523,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_17,in_permitting,138.0,2023,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/81"", ""tyndp2020_proj_id""=>""81"", ""tyndp2020_invest_id""=>""462"", ""tyndp_status""=>""in_permitting""",-6.61376999999999,53.441446,-6.65496799999999,54.509921,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_21,planned_not_yet_permitting,61.0,2023,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/85"", ""tyndp2020_proj_id""=>""85"", ""tyndp2020_invest_id""=>""779"", ""tyndp_status""=>""planned_not_yet_permitting""",-8.046112,38.05458,-8.1897,37.6672,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_22,planned_not_yet_permitting,61.0,2023,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/85"", ""tyndp2020_proj_id""=>""85"", ""tyndp2020_invest_id""=>""779"", ""tyndp_status""=>""planned_not_yet_permitting""",-8.1897,37.6672,-7.73025499999999,37.397437,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_23,planned_not_yet_permitting,75.0,2023,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/85"", ""tyndp2020_proj_id""=>""85"", ""tyndp2020_invest_id""=>""1670"", ""tyndp_status""=>""planned_not_yet_permitting""",-7.55310100000001,38.17991,-7.89230299999999,38.808681,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_24,under_construction,75.0,2022,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/103"", ""tyndp2020_proj_id""=>""103"", ""tyndp2020_invest_id""=>""1488"", ""tyndp_status""=>""under_construction""",4.9617,52.333661,5.763702,52.619725,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_25,in_permitting,200.0,2025,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/111"", ""tyndp2020_proj_id""=>""111"", ""tyndp2020_invest_id""=>""396"", ""tyndp_status""=>""in_permitting""",24.612122,65.877531,20.232697,66.70191,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_26,in_permitting,156.0,2024,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/111"", ""tyndp2020_proj_id""=>""111"", ""tyndp2020_invest_id""=>""1710"", ""tyndp_status""=>""in_permitting""",24.612122,65.877531,26.114502,64.778807,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_27,under_construction,108.0,2023,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/123"", ""tyndp2020_proj_id""=>""123"", ""tyndp2020_invest_id""=>""373"", ""tyndp_status""=>""under_construction""",21.625214,53.051946,21.40686,52.213497,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_28,in_permitting,160.0,2026,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/124"", ""tyndp2020_proj_id""=>""124"", ""tyndp2020_invest_id""=>""733"", ""tyndp_status""=>""in_permitting""",16.659393,57.43682,14.749146,56.326437,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_29,in_permitting,178.0,2024,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/127"", ""tyndp2020_proj_id""=>""127"", ""tyndp2020_invest_id""=>""86"", ""tyndp_status""=>""in_permitting""",15.636292,41.359288,14.028168,42.409263,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_30,under_construction,30.0,2021,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/127"", ""tyndp2020_proj_id""=>""127"", ""tyndp2020_invest_id""=>""96"", ""tyndp_status""=>""under_construction""",15.555267,41.165216,15.3286,40.9749,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_31,under_construction,159.0,2023,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/138"", ""tyndp2020_proj_id""=>""138"", ""tyndp2020_invest_id""=>""273"", ""tyndp_status""=>""under_construction""",28.02475,44.323848,26.7166,45.0762,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_32,in_permitting,140.0,2024,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/138"", ""tyndp2020_proj_id""=>""138"", ""tyndp2020_invest_id""=>""275"", ""tyndp_status""=>""in_permitting""",27.960205,45.447607,26.872559,46.265341,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_33,under_construction,86.0,2021,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/138"", ""tyndp2020_proj_id""=>""138"", ""tyndp2020_invest_id""=>""800"", ""tyndp_status""=>""under_construction""",27.610016,43.326177,27.358704,42.576344,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_34,in_permitting,151.0,2023,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/142"", ""tyndp2020_proj_id""=>""142"", ""tyndp2020_invest_id""=>""256"", ""tyndp_status""=>""in_permitting""",26.003265,42.200038,23.083964803314,40.8967964275543,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_35,under_construction,13.0,2021,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/142"", ""tyndp2020_proj_id""=>""142"", ""tyndp2020_invest_id""=>""258"", ""tyndp_status""=>""under_construction""",26.003265,42.200038,25.804138,41.964596,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_36,under_construction,150.0,2021,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/142"", ""tyndp2020_proj_id""=>""142"", ""tyndp2020_invest_id""=>""262"", ""tyndp_status""=>""under_construction""",26.003265,42.200038,27.358704,42.576344,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_37,under_construction,131.0,2024,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/144"", ""tyndp2020_proj_id""=>""144"", ""tyndp2020_invest_id""=>""238"", ""tyndp_status""=>""under_construction""",21.88777,45.30413,20.706482,44.86755,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_38,under_construction,116.0,2024,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/144"", ""tyndp2020_proj_id""=>""144"", ""tyndp2020_invest_id""=>""269"", ""tyndp_status""=>""under_construction""",22.578278,44.679395,21.88777,45.30413,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_39,in_permitting,274.0,2025,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/144"", ""tyndp2020_proj_id""=>""144"", ""tyndp2020_invest_id""=>""270"", ""tyndp_status""=>""in_permitting""",21.2815,45.7426,21.2815,45.7426,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_43,planned_not_yet_permitting,57.11,2025,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/170"", ""tyndp2020_proj_id""=>""170"", ""tyndp2020_invest_id""=>""1661"", ""tyndp_status""=>""planned_not_yet_permitting""",16.11557,54.132674,16.6857237480894,54.0342010066989,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_44,planned_not_yet_permitting,119.0,2025,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/170"", ""tyndp2020_proj_id""=>""170"", ""tyndp2020_invest_id""=>""1662"", ""tyndp_status""=>""planned_not_yet_permitting""",16.858521,53.16571,16.6857237480894,54.0342010066989,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_45,planned_not_yet_permitting,288.5,2025,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/170"", ""tyndp2020_proj_id""=>""170"", ""tyndp2020_invest_id""=>""1664"", ""tyndp_status""=>""planned_not_yet_permitting""",14.831543,53.330873,18.104095,54.714309,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_46,planned_not_yet_permitting,77.0,2024,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/170"", ""tyndp2020_proj_id""=>""170"", ""tyndp2020_invest_id""=>""1665"", ""tyndp_status""=>""planned_not_yet_permitting""",18.104095,54.714309,18.684998,54.266026,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_47,in_permitting,92.0,2023,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/183"", ""tyndp2020_proj_id""=>""183"", ""tyndp2020_invest_id""=>""1018"", ""tyndp_status""=>""in_permitting""",8.86459400000001,55.074436,8.66683999999999,55.481966,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_48,under_construction,60.0,2022,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/186"", ""tyndp2020_proj_id""=>""186"", ""tyndp2020_invest_id""=>""886"", ""tyndp_status""=>""under_construction""",16.399841,48.323387,16.820068,48.943249,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_49,in_permitting,60.0,2028,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/187"", ""tyndp2020_proj_id""=>""187"", ""tyndp2020_invest_id""=>""997"", ""tyndp_status""=>""in_permitting""",13.080597,48.256684,13.080597,48.256684,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_50,under_construction,300.0,2022,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/197"", ""tyndp2020_proj_id""=>""197"", ""tyndp2020_invest_id""=>""742"", ""tyndp_status""=>""under_construction""",26.114502,64.778807,25.180664,62.262171,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_51,in_permitting,83.0,2023,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/200"", ""tyndp2020_proj_id""=>""200"", ""tyndp2020_invest_id""=>""308"", ""tyndp_status""=>""in_permitting""",13.191833,50.341955,12.675274,50.160683,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_52,under_construction,87.0,2021,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/200"", ""tyndp2020_proj_id""=>""200"", ""tyndp2020_invest_id""=>""309"", ""tyndp_status""=>""under_construction""",12.675274,50.160683,13.282471,49.60804,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_53,under_construction,60.0,2023,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/207"", ""tyndp2020_proj_id""=>""207"", ""tyndp2020_invest_id""=>""939"", ""tyndp_status""=>""under_construction""",8.09143099999999,53.313647,7.23587000000001,53.374317,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_54,under_construction,30.0,2020,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/207"", ""tyndp2020_proj_id""=>""207"", ""tyndp2020_invest_id""=>""1684"", ""tyndp_status""=>""under_construction""",8.09143099999999,53.313647,8.00079299999999,53.55581,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_56,under_construction,182.0,2023,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/208"", ""tyndp2020_proj_id""=>""208"", ""tyndp2020_invest_id""=>""156"", ""tyndp_status""=>""under_construction""",6.615143,51.630805,7.38830599999999,53.076703,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_57,in_permitting,45.2,2026,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/227"", ""tyndp2020_proj_id""=>""227"", ""tyndp2020_invest_id""=>""627"", ""tyndp_status""=>""in_permitting""",19.500315,43.953021,19.388123,43.723475,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_58,in_permitting,100.0,2026,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/227"", ""tyndp2020_proj_id""=>""227"", ""tyndp2020_invest_id""=>""630"", ""tyndp_status""=>""in_permitting""",19.500315,43.953021,19.315338,43.347152,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_59,under_construction,151.0,2019,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/227"", ""tyndp2020_proj_id""=>""227"", ""tyndp2020_invest_id""=>""1526"", ""tyndp_status""=>""under_construction""",18.772888,42.315909,19.315338,43.347152,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_60,under_construction,,2020,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/227"", ""tyndp2020_proj_id""=>""227"", ""tyndp2020_invest_id""=>""1527"", ""tyndp_status""=>""under_construction""",20.928955,44.056999,20.739984,43.680615,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_61,planned_not_yet_permitting,32.0,2025,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/228"", ""tyndp2020_proj_id""=>""228"", ""tyndp2020_invest_id""=>""1231"", ""tyndp_status""=>""planned_not_yet_permitting""",7.50366199999999,47.891485,7.71240200000001,48.103763,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_62,under_consideration,40.0,2035,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/229"", ""tyndp2020_proj_id""=>""229"", ""tyndp2020_invest_id""=>""1270"", ""tyndp_status""=>""under_consideration""",15.038505,52.76751,15.5,51.93333,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_63,under_consideration,40.0,2035,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/229"", ""tyndp2020_proj_id""=>""229"", ""tyndp2020_invest_id""=>""1271"", ""tyndp_status""=>""under_consideration""",15.5,51.93333,16.729431,52.32359,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_64,under_consideration,90.0,2035,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/229"", ""tyndp2020_proj_id""=>""229"", ""tyndp2020_invest_id""=>""1673"", ""tyndp_status""=>""under_consideration""",15.5,51.93333,16.056519,51.449728,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_65,under_consideration,80.0,2035,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/229"", ""tyndp2020_proj_id""=>""229"", ""tyndp2020_invest_id""=>""1674"", ""tyndp_status""=>""under_consideration""",15.5,51.93333,14.615936,52.125058,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_66,in_permitting,100.0,2024,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/230"", ""tyndp2020_proj_id""=>""230"", ""tyndp2020_invest_id""=>""355"", ""tyndp_status""=>""in_permitting""",15.086975,51.069017,16.365509,50.816348,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_67,under_consideration,11.0,2030,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/231"", ""tyndp2020_proj_id""=>""231"", ""tyndp2020_invest_id""=>""1282"", ""tyndp_status""=>""under_consideration""",8.16970799999999,47.523693,8.205414,47.649662,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_68,under_consideration,91.0,2033,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/241"", ""tyndp2020_proj_id""=>""241"", ""tyndp2020_invest_id""=>""1276"", ""tyndp_status""=>""under_consideration""",18.420959,45.289022,18.538055,44.455349,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_69,under_consideration,46.2,2033,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/241"", ""tyndp2020_proj_id""=>""241"", ""tyndp2020_invest_id""=>""1277"", ""tyndp_status""=>""under_consideration""",18.420959,45.289022,18.436344,44.904884,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_70,under_consideration,25.0,2033,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/241"", ""tyndp2020_proj_id""=>""241"", ""tyndp2020_invest_id""=>""1279"", ""tyndp_status""=>""under_consideration""",18.420959,45.289022,18.420959,45.289022,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_71,under_consideration,47.8,2032,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/241"", ""tyndp2020_proj_id""=>""241"", ""tyndp2020_invest_id""=>""1530"", ""tyndp_status""=>""under_consideration""",18.436344,44.904884,18.538055,44.455349,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_72,under_consideration,70.0,2035,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/243"", ""tyndp2020_proj_id""=>""243"", ""tyndp2020_invest_id""=>""1269"", ""tyndp_status""=>""under_consideration""",18.663025,45.454351,19.175262,45.75411,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_73,planned_not_yet_permitting,65.0,2028,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/244"", ""tyndp2020_proj_id""=>""244"", ""tyndp2020_invest_id""=>""1245"", ""tyndp_status""=>""planned_not_yet_permitting""",6.25122100000001,49.182601,7.028503,49.340336,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_74,under_construction,27.0,2021,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/245"", ""tyndp2020_proj_id""=>""245"", ""tyndp2020_invest_id""=>""1246"", ""tyndp_status""=>""under_construction""",6.936493,53.145947,7.389679,53.097323,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_76,planned_not_yet_permitting,90.0,2029,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/252"", ""tyndp2020_proj_id""=>""252"", ""tyndp2020_invest_id""=>""1050"", ""tyndp_status""=>""planned_not_yet_permitting""",5.71701000000001,51.094036,5.35583499999999,50.557942,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_77,in_permitting,90.0,2024,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/252"", ""tyndp2020_proj_id""=>""252"", ""tyndp2020_invest_id""=>""1456"", ""tyndp_status""=>""in_permitting""",4.603271,51.209464,5.71701000000001,51.094036,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_78,planned_not_yet_permitting,60.0,2033,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/252"", ""tyndp2020_proj_id""=>""252"", ""tyndp2020_invest_id""=>""1515"", ""tyndp_status""=>""planned_not_yet_permitting""",5.35583499999999,50.557942,4.35470599999999,50.529143,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_79,planned_not_yet_permitting,47.0,2035,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/252"", ""tyndp2020_proj_id""=>""252"", ""tyndp2020_invest_id""=>""1516"", ""tyndp_status""=>""planned_not_yet_permitting""",4.22012299999999,50.864911,4.35470599999999,50.529143,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_80,planned_not_yet_permitting,16.0,2025,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/252"", ""tyndp2020_proj_id""=>""252"", ""tyndp2020_invest_id""=>""1517"", ""tyndp_status""=>""planned_not_yet_permitting""",4.177551,51.162122,4.22012299999999,50.864911,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_81,planned_not_yet_permitting,36.0,2030,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/252"", ""tyndp2020_proj_id""=>""252"", ""tyndp2020_invest_id""=>""1676"", ""tyndp_status""=>""planned_not_yet_permitting""",4.177551,51.162122,4.603271,51.209464,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_82,under_construction,125.0,2022,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/258"", ""tyndp2020_proj_id""=>""258"", ""tyndp2020_invest_id""=>""667"", ""tyndp_status""=>""under_construction""",9.24087500000001,53.906765,8.86459400000001,55.074436,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_83,under_consideration,120.0,2030,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/259"", ""tyndp2020_proj_id""=>""259"", ""tyndp2020_invest_id""=>""1205"", ""tyndp_status""=>""under_consideration""",21.566162,47.659838,22.026215,47.044861,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_84,under_consideration,1.0,2035,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/263"", ""tyndp2020_proj_id""=>""263"", ""tyndp2020_invest_id""=>""1258"", ""tyndp_status""=>""under_consideration""",9.556022,47.302744,8.298798,47.148634,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_85,under_consideration,1.0,2035,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/263"", ""tyndp2020_proj_id""=>""263"", ""tyndp2020_invest_id""=>""1258"", ""tyndp_status""=>""under_consideration""",9.556022,47.302744,9.253235,46.824496,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_86,under_consideration,,2035,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/263"", ""tyndp2020_proj_id""=>""263"", ""tyndp2020_invest_id""=>""1583"", ""tyndp_status""=>""under_consideration""",9.556022,47.302744,9.80228979023356,47.6377345779987,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_87,under_consideration,,2035,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/263"", ""tyndp2020_proj_id""=>""263"", ""tyndp2020_invest_id""=>""1583"", ""tyndp_status""=>""under_consideration""",9.556022,47.302744,9.790192,47.138359,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_88,in_permitting,64.0,2029,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/264"", ""tyndp2020_proj_id""=>""264"", ""tyndp2020_invest_id""=>""1259"", ""tyndp_status""=>""in_permitting""",8.16970799999999,47.523693,8.298798,47.148634,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_89,in_permitting,45.4,2027,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/264"", ""tyndp2020_proj_id""=>""264"", ""tyndp2020_invest_id""=>""1287"", ""tyndp_status""=>""in_permitting""",7.19192499999999,47.364874,7.22213700000001,47.007416,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_90,in_permitting,87.1,2035,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/264"", ""tyndp2020_proj_id""=>""264"", ""tyndp2020_invest_id""=>""1288"", ""tyndp_status""=>""in_permitting""",8.298798,47.148634,8.32488999999999,46.532414,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_92,in_permitting,124.0,2029,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/266"", ""tyndp2020_proj_id""=>""266"", ""tyndp2020_invest_id""=>""1286"", ""tyndp_status""=>""in_permitting""",7.555847,46.336499,8.795929,46.401882,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_93,in_permitting,106.6,2027,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/266"", ""tyndp2020_proj_id""=>""266"", ""tyndp2020_invest_id""=>""1733"", ""tyndp_status""=>""in_permitting""",7.64236499999999,47.079475,7.555847,46.336499,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_94,under_construction,30.0,2022,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/266"", ""tyndp2020_proj_id""=>""266"", ""tyndp2020_invest_id""=>""1734"", ""tyndp_status""=>""under_construction""",7.165833,46.200745,7.555847,46.336499,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_95,planned_not_yet_permitting,110.0,2030,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/270"", ""tyndp2020_proj_id""=>""270"", ""tyndp2020_invest_id""=>""1212"", ""tyndp_status""=>""planned_not_yet_permitting""",-1.14818,42.10892,-1.671123,41.952398,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_96,planned_not_yet_permitting,80.0,2029,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/276"", ""tyndp2020_proj_id""=>""276"", ""tyndp2020_invest_id""=>""1208"", ""tyndp_status""=>""planned_not_yet_permitting""",-0.812988000000009,43.979969,-0.575409000000004,43.345155,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_97,under_construction,1.0,2020,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/297"", ""tyndp2020_proj_id""=>""297"", ""tyndp2020_invest_id""=>""445"", ""tyndp_status""=>""under_construction""",4.307132,51.301299,4.307132,51.301299,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_98,under_construction,128.0,2025,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/312"", ""tyndp2020_proj_id""=>""312"", ""tyndp2020_invest_id""=>""1472"", ""tyndp_status""=>""under_construction""",13.080597,48.256684,12.803192,47.244746,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_99,in_permitting,90.0,2024,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/313"", ""tyndp2020_proj_id""=>""313"", ""tyndp2020_invest_id""=>""1473"", ""tyndp_status""=>""in_permitting""",12.172852,48.638354,13.080597,48.256684,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_100,under_construction,80.0,2021,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/320"", ""tyndp2020_proj_id""=>""320"", ""tyndp2020_invest_id""=>""1558"", ""tyndp_status""=>""under_construction""",15.724182,46.386728,16.689606,46.633408,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_101,in_permitting,85.0,2025,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/322"", ""tyndp2020_proj_id""=>""322"", ""tyndp2020_invest_id""=>""1476"", ""tyndp_status""=>""in_permitting""",10.06485,48.282279,9.73251299999999,47.623752,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_104,planned_not_yet_permitting,40.0,2026,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/328"", ""tyndp2020_proj_id""=>""328"", ""tyndp2020_invest_id""=>""1630"", ""tyndp_status""=>""planned_not_yet_permitting""",6.590884,49.789617,6.140853,49.688695,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_105,under_consideration,78.0,2035,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/330"", ""tyndp2020_proj_id""=>""330"", ""tyndp2020_invest_id""=>""1498"", ""tyndp_status""=>""under_consideration""",17.440796,49.236431,18.287416,49.038186,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_107,planned_not_yet_permitting,100.0,2028,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/340"", ""tyndp2020_proj_id""=>""340"", ""tyndp2020_invest_id""=>""1519"", ""tyndp_status""=>""planned_not_yet_permitting""",3.429108,50.773813,4.35470599999999,50.529143,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_108,under_consideration,60.0,2030,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/341"", ""tyndp2020_proj_id""=>""341"", ""tyndp2020_invest_id""=>""1538"", ""tyndp_status""=>""under_consideration""",20.9729,44.874362,20.378265,44.819838,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_109,planned_not_yet_permitting,180.0,2030,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/343"", ""tyndp2020_proj_id""=>""343"", ""tyndp2020_invest_id""=>""1532"", ""tyndp_status""=>""planned_not_yet_permitting""",17.201843,44.772087,15.26017,44.886816,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_110,planned_not_yet_permitting,68.0,2030,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/343"", ""tyndp2020_proj_id""=>""343"", ""tyndp2020_invest_id""=>""1533"", ""tyndp_status""=>""planned_not_yet_permitting""",15.26017,44.886816,14.558258,45.323186,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_111,under_consideration,165.0,2040,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/344"", ""tyndp2020_proj_id""=>""344"", ""tyndp2020_invest_id""=>""1541"", ""tyndp_status""=>""under_consideration""",6.14410399999999,52.52207,5.671692,51.908696,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_113,under_construction,161.0,2020,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/350"", ""tyndp2020_proj_id""=>""350"", ""tyndp2020_invest_id""=>""1622"", ""tyndp_status""=>""under_construction""",20.725963,41.239159,19.966278,41.16108,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_115,under_consideration,38.8,2025,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/379"", ""tyndp2020_proj_id""=>""379"", ""tyndp2020_invest_id""=>""1593"", ""tyndp_status""=>""under_consideration""",-2.880286,43.349776,-3.05282599999999,43.265206,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_116,under_consideration,69.12,2025,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/379"", ""tyndp2020_proj_id""=>""379"", ""tyndp2020_invest_id""=>""1595"", ""tyndp_status""=>""under_consideration""",-2.880286,43.349776,-2.24395799999999,43.047816,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_117,under_consideration,57.0,2025,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/379"", ""tyndp2020_proj_id""=>""379"", ""tyndp2020_invest_id""=>""1597"", ""tyndp_status""=>""under_consideration""",-2.880286,43.349776,-2.20687899999999,43.098977,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_120,under_construction,14.0,2021,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/1043"", ""tyndp2020_proj_id""=>""1043"", ""tyndp2020_invest_id""=>""1685"", ""tyndp_status""=>""under_construction""",9.48532099999999,53.629168,9.48532099999999,53.629168,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_121,under_construction,60.0,2024,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/1043"", ""tyndp2020_proj_id""=>""1043"", ""tyndp2020_invest_id""=>""1686"", ""tyndp_status""=>""under_construction""",10.299683,52.286643,9.988131,51.954234,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_122,under_construction,45.0,2024,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/1043"", ""tyndp2020_proj_id""=>""1043"", ""tyndp2020_invest_id""=>""1686"", ""tyndp_status""=>""under_construction""",9.988131,51.954234,9.852428,51.649318,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_123,under_construction,105.0,2024,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/1043"", ""tyndp2020_proj_id""=>""1043"", ""tyndp2020_invest_id""=>""1686"", ""tyndp_status""=>""under_construction""",9.852428,51.649318,9.621277,50.85971,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_124,in_permitting,290.0,2028,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/1046"", ""tyndp2020_proj_id""=>""1046"", ""tyndp2020_invest_id""=>""1688"", ""tyndp_status""=>""in_permitting""",26.842346,64.556701,27.766571,62.262171,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_125,in_permitting,100.0,2029,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/1046"", ""tyndp2020_proj_id""=>""1046"", ""tyndp2020_invest_id""=>""1689"", ""tyndp_status""=>""in_permitting""",25.692902,65.279688,26.842346,64.556701,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_126,planned_not_yet_permitting,175.0,2030,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/1046"", ""tyndp2020_proj_id""=>""1046"", ""tyndp2020_invest_id""=>""1697"", ""tyndp_status""=>""planned_not_yet_permitting""",25.180664,62.262171,24.915619,60.90707,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_127,under_consideration,31.0,2035,True,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/1047"", ""tyndp2020_proj_id""=>""1047"", ""tyndp2020_invest_id""=>""1691"", ""tyndp_status""=>""under_consideration""",7.23587000000001,53.374317,6.86370800000001,53.418536,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_128,planned_not_yet_permitting,170.0,2030,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/1052"", ""tyndp2020_proj_id""=>""1052"", ""tyndp2020_invest_id""=>""1713"", ""tyndp_status""=>""planned_not_yet_permitting""",12.693329,46.798179,14.5912685597917,46.536256583087,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_129,planned_not_yet_permitting,105.0,2027,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/1054"", ""tyndp2020_proj_id""=>""1054"", ""tyndp2020_invest_id""=>""1715"", ""tyndp_status""=>""planned_not_yet_permitting""",10.897064,47.236355,11.964111,47.292271,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_130,under_consideration,90.0,2035,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/1056"", ""tyndp2020_proj_id""=>""1056"", ""tyndp2020_invest_id""=>""1723"", ""tyndp_status""=>""under_consideration""",16.78425,43.503952,17.436701,43.052743,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_132,in_permitting,50.0,2027,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/1059"", ""tyndp2020_proj_id""=>""1059"", ""tyndp2020_invest_id""=>""645"", ""tyndp_status""=>""in_permitting""",15.996094,39.990799,16.020813,39.780047,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_133,in_permitting,,2030,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/1059"", ""tyndp2020_proj_id""=>""1059"", ""tyndp2020_invest_id""=>""1727"", ""tyndp_status""=>""in_permitting""",15.03067,40.66814,15.058136,40.941527,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_134,in_permitting,,2030,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/1059"", ""tyndp2020_proj_id""=>""1059"", ""tyndp2020_invest_id""=>""1727"", ""tyndp_status""=>""in_permitting""",15.058136,40.941527,14.957886,41.143501,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_135,under_construction,40.0,2022,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/1063"", ""tyndp2020_proj_id""=>""1063"", ""tyndp2020_invest_id""=>""1731"", ""tyndp_status""=>""under_construction""",4.218787,51.424034,4.218787,51.424034,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_136,under_consideration,50.6,2036,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/1074"", ""tyndp2020_proj_id""=>""1074"", ""tyndp2020_invest_id""=>""1742"", ""tyndp_status""=>""under_consideration""",19.720459,46.020807,20.170898,46.29856,2,Al/St 240/40 4-bundle 380.0 +TYNDP2020_137,under_consideration,80.0,2036,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/1074"", ""tyndp2020_proj_id""=>""1074"", ""tyndp2020_invest_id""=>""1743"", ""tyndp_status""=>""under_consideration""",19.175262,45.75411,19.756165,45.527517,2,Al/St 240/40 4-bundle 380.0 diff --git a/data/transmission_projects/tyndp2020/new_links.csv b/data/transmission_projects/tyndp2020/new_links.csv new file mode 100644 index 000000000..902cc9b35 --- /dev/null +++ b/data/transmission_projects/tyndp2020/new_links.csv @@ -0,0 +1,49 @@ +,project_status,length,build_year,underground,p_nom,tags,x0,y0,x1,y1 +TYNDP2020_0,under_consideration,330.0,2033,True,600.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/234"", ""tyndp2020_proj_id""=>""234"", ""tyndp2020_invest_id""=>""1236"", ""tyndp_status""=>""under_consideration""",12.492828,55.609384,16.11557,54.132674 +TYNDP2020_1,in_permitting,700.0,2026,True,2000.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/235"", ""tyndp2020_proj_id""=>""235"", ""tyndp2020_invest_id""=>""664"", ""tyndp_status""=>""in_permitting""",9.24087500000001,53.906765,9.12211621382025,49.0939516665424 +TYNDP2020_2,in_permitting,558.0,2026,True,2000.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/235"", ""tyndp2020_proj_id""=>""235"", ""tyndp2020_invest_id""=>""664"", ""tyndp_status""=>""in_permitting""",9.327393,53.931837,10.081329,49.950336 +TYNDP2020_3,under_consideration,200.0,2035,True,800.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/239"", ""tyndp2020_proj_id""=>""239"", ""tyndp2020_invest_id""=>""1241"", ""tyndp_status""=>""under_consideration""",17.166138,63.184727,21.901245,63.03068 +TYNDP2020_4,planned_not_yet_permitting,267.0,2030,False,2000.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/1034"", ""tyndp2020_proj_id""=>""1034"", ""tyndp2020_invest_id""=>""1692"", ""tyndp_status""=>""planned_not_yet_permitting""",8.00079299999999,53.55581,8.06808499999999,51.653815 +TYNDP2020_5,planned_not_yet_permitting,407.0,2030,False,2000.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/1034"", ""tyndp2020_proj_id""=>""1034"", ""tyndp2020_invest_id""=>""1693"", ""tyndp_status""=>""planned_not_yet_permitting""",9.13101199999999,54.20101,7.06420899999999,51.66063 +TYNDP2020_6,under_construction,445.0,2026,True,600.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/28"", ""tyndp2020_proj_id""=>""28"", ""tyndp2020_invest_id""=>""1503"", ""tyndp_status""=>""under_construction""",14.028168,42.409263,18.772888,42.315909 +TYNDP2020_7,in_permitting,240.0,2023,True,2000.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/247"", ""tyndp2020_proj_id""=>""247"", ""tyndp2020_invest_id""=>""1381"", ""tyndp_status""=>""in_permitting""",-1.05331400000001,50.900435,1.11373900000001,49.712937 +TYNDP2020_8,under_consideration,133.0,2027,True,700.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/1040"", ""tyndp2020_proj_id""=>""1040"", ""tyndp2020_invest_id""=>""1677"", ""tyndp_status""=>""under_consideration""",-5.80902099999999,54.726206,-4.880676,55.704677 +TYNDP2020_10,in_permitting,340.0,2024,False,2000.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/254"", ""tyndp2020_proj_id""=>""254"", ""tyndp2020_invest_id""=>""660"", ""tyndp_status""=>""in_permitting""",6.58493,51.245584,8.478699,49.283036 +TYNDP2020_11,under_consideration,230.0,2025,True,1400.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/1049"", ""tyndp2020_proj_id""=>""1049"", ""tyndp2020_invest_id""=>""1706"", ""tyndp_status""=>""under_consideration""",0.763549999999995,51.403489,3.18054200000001,51.330612 +TYNDP2020_12,under_consideration,610.0,2028,True,1400.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/1050"", ""tyndp2020_proj_id""=>""1050"", ""tyndp2020_invest_id""=>""1707"", ""tyndp_status""=>""under_consideration""",0.482024999999987,51.519853,7.23587000000001,53.374317 +TYNDP2020_13,under_consideration,640.0,2028,True,1400.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/1051"", ""tyndp2020_proj_id""=>""1051"", ""tyndp2020_invest_id""=>""1708"", ""tyndp_status""=>""under_consideration""",1.30737299999999,52.593038,9.22851599999999,55.481188 +TYNDP2020_14,under_consideration,300.0,2030,True,1400.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/267"", ""tyndp2020_proj_id""=>""267"", ""tyndp2020_invest_id""=>""1262"", ""tyndp_status""=>""under_consideration""",13.580475,55.86067,12.204437,53.760078 +TYNDP2020_15,planned_not_yet_permitting,230.0,2030,False,1500.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/270"", ""tyndp2020_proj_id""=>""270"", ""tyndp2020_invest_id""=>""1211"", ""tyndp_status""=>""planned_not_yet_permitting""",-1.671123,41.952398,-0.575409000000004,43.345155 +TYNDP2020_16,under_consideration,200.0,2040,False,1000.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/1058"", ""tyndp2020_proj_id""=>""1058"", ""tyndp2020_invest_id""=>""1726"", ""tyndp_status""=>""under_consideration""",9.43585963014029,48.6383575221442,8.298798,47.148634 +TYNDP2020_17,in_permitting,500.0,2026,True,700.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/107"", ""tyndp2020_proj_id""=>""107"", ""tyndp2020_invest_id""=>""810"", ""tyndp_status""=>""in_permitting""",-8.36471600000001,51.892597,-4.237976,48.518424 +TYNDP2020_19,planned_not_yet_permitting,225.0,2029,False,2000.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/276"", ""tyndp2020_proj_id""=>""276"", ""tyndp2020_invest_id""=>""1206"", ""tyndp_status""=>""planned_not_yet_permitting""",-1.645402,42.818157,-0.812988000000009,43.979969 +TYNDP2020_20,under_consideration,195.0,2036,True,500.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/1068"", ""tyndp2020_proj_id""=>""1068"", ""tyndp2020_invest_id""=>""1740"", ""tyndp_status""=>""under_consideration""",18.227692,57.571097,21.100616,56.489036 +TYNDP2020_21,planned_not_yet_permitting,156.0,2022,True,1500.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/285"", ""tyndp2020_proj_id""=>""285"", ""tyndp2020_invest_id""=>""1383"", ""tyndp_status""=>""planned_not_yet_permitting""",0.575409000000004,51.430896,2.16293300000001,50.923813 +TYNDP2020_22,in_permitting,540.0,2025,True,2000.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/130"", ""tyndp2020_proj_id""=>""130"", ""tyndp2020_invest_id""=>""665"", ""tyndp_status""=>""in_permitting""",11.637268,52.27404,12.292328,48.659222 +TYNDP2020_23,in_permitting,300.0,2025,False,2000.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/132"", ""tyndp2020_proj_id""=>""132"", ""tyndp2020_invest_id""=>""661"", ""tyndp_status""=>""in_permitting""",7.23587000000001,53.374317,6.58493,51.245584 +TYNDP2020_24,under_consideration,110.0,2036,False,2000.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/1077"", ""tyndp2020_proj_id""=>""1077"", ""tyndp2020_invest_id""=>""1747"", ""tyndp_status""=>""under_consideration""",22.9422,40.662931,22.111359,41.50035 +TYNDP2020_25,under_consideration,170.0,2036,False,2000.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/1077"", ""tyndp2020_proj_id""=>""1077"", ""tyndp2020_invest_id""=>""1748"", ""tyndp_status""=>""under_consideration""",22.111359,41.50035,23.03833,42.549034 +TYNDP2020_26,under_consideration,180.0,2036,False,2000.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/1081"", ""tyndp2020_proj_id""=>""1081"", ""tyndp2020_invest_id""=>""1749"", ""tyndp_status""=>""under_consideration""",21.137695,39.279042,19.966278,41.16108 +TYNDP2020_27,under_consideration,665.0,2026,True,1800.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/296"", ""tyndp2020_proj_id""=>""296"", ""tyndp2020_invest_id""=>""1437"", ""tyndp_status""=>""under_consideration""",-1.972046,43.32318,-1.847076,47.30531 +TYNDP2020_28,under_consideration,665.0,2026,True,1800.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/296"", ""tyndp2020_proj_id""=>""296"", ""tyndp2020_invest_id""=>""1437"", ""tyndp_status""=>""under_consideration""",-1.847076,47.30531,-3.967438,50.489842 +TYNDP2020_29,in_permitting,100.0,2024,True,400.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/299"", ""tyndp2020_proj_id""=>""299"", ""tyndp2020_invest_id""=>""1458"", ""tyndp_status""=>""in_permitting""",8.63662699999999,40.644178,9.45098899999999,42.538916 +TYNDP2020_30,in_permitting,50.0,2024,False,400.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/299"", ""tyndp2020_proj_id""=>""299"", ""tyndp2020_invest_id""=>""1458"", ""tyndp_status""=>""in_permitting""",9.45098899999999,42.538916,10.671844,43.014689 +TYNDP2020_31,in_permitting,170.0,2028,True,1000.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/150"", ""tyndp2020_proj_id""=>""150"", ""tyndp2020_invest_id""=>""616"", ""tyndp_status""=>""in_permitting""",12.516174,45.825928,14.615936,46.084662 +TYNDP2020_32,in_permitting,700.0,2024,True,1400.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/309"", ""tyndp2020_proj_id""=>""309"", ""tyndp2020_invest_id""=>""1628"", ""tyndp_status""=>""in_permitting""",0.763549999999995,51.403489,8.00079299999999,53.55581 +TYNDP2020_33,in_permitting,400.0,2026,True,750.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/170"", ""tyndp2020_proj_id""=>""170"", ""tyndp2020_invest_id""=>""1034"", ""tyndp_status""=>""in_permitting""",21.295624,56.032924,18.104095,54.714309 +TYNDP2020_34,under_consideration,328.0,2035,True,2000.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/335"", ""tyndp2020_proj_id""=>""335"", ""tyndp2020_invest_id""=>""1505"", ""tyndp_status""=>""under_consideration""",8.66683999999999,55.481966,3.590555,55.343106 +TYNDP2020_35,under_consideration,360.0,2035,True,2000.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/335"", ""tyndp2020_proj_id""=>""335"", ""tyndp2020_invest_id""=>""1506"", ""tyndp_status""=>""under_consideration""",4.05120799999999,51.942572,3.590555,55.343106 +TYNDP2020_36,under_construction,69.0,2021,True,1000.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/172"", ""tyndp2020_proj_id""=>""172"", ""tyndp2020_invest_id""=>""1487"", ""tyndp_status""=>""under_construction""",0.95855700000001,51.059523,1.833344,50.902167 +TYNDP2020_37,in_permitting,165.0,2024,True,1000.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/174"", ""tyndp2020_proj_id""=>""174"", ""tyndp2020_invest_id""=>""1014"", ""tyndp_status""=>""in_permitting""",9.449615,45.620761,9.253235,46.824496 +TYNDP2020_38,in_permitting,300.0,2026,True,700.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/176"", ""tyndp2020_proj_id""=>""176"", ""tyndp2020_invest_id""=>""995"", ""tyndp_status""=>""in_permitting""",13.580475,55.86067,12.204437,53.760078 +TYNDP2020_39,under_consideration,170.0,2030,True,600.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/179"", ""tyndp2020_proj_id""=>""179"", ""tyndp2020_invest_id""=>""1016"", ""tyndp_status""=>""under_consideration""",11.969604,55.468735,12.271729,54.068253 +TYNDP2020_40,under_consideration,290.0,2035,True,2000.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/335"", ""tyndp2020_proj_id""=>""335"", ""tyndp2020_invest_id""=>""1507"", ""tyndp_status""=>""under_consideration""",6.86370800000001,53.418536,3.590555,55.343106 +TYNDP2020_41,under_consideration,390.0,2035,True,2000.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/335"", ""tyndp2020_proj_id""=>""335"", ""tyndp2020_invest_id""=>""1508"", ""tyndp_status""=>""under_consideration""",9.24087500000001,53.906765,3.590555,55.343106 +TYNDP2020_42,under_consideration,400.0,2035,True,2000.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/335"", ""tyndp2020_proj_id""=>""335"", ""tyndp2020_invest_id""=>""1509"", ""tyndp_status""=>""under_consideration""",8.418274,53.182996,3.590555,55.343106 +TYNDP2020_43,under_consideration,490.0,2035,True,2000.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/335"", ""tyndp2020_proj_id""=>""335"", ""tyndp2020_invest_id""=>""1511"", ""tyndp_status""=>""under_consideration""",10.458984,53.435719,3.590555,55.343106 +TYNDP2020_44,planned_not_yet_permitting,400.0,2030,True,1000.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/338"", ""tyndp2020_proj_id""=>""338"", ""tyndp2020_invest_id""=>""1521"", ""tyndp_status""=>""planned_not_yet_permitting""",14.028168,42.409263,12.811432,43.915702 +TYNDP2020_45,planned_not_yet_permitting,400.0,2025,True,1000.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/339"", ""tyndp2020_proj_id""=>""339"", ""tyndp2020_invest_id""=>""1557"", ""tyndp_status""=>""planned_not_yet_permitting""",9.18319700000001,39.374649,13.304443,38.159396 +TYNDP2020_46,planned_not_yet_permitting,500.0,2025,True,1000.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/339"", ""tyndp2020_proj_id""=>""339"", ""tyndp2020_invest_id""=>""1557"", ""tyndp_status""=>""planned_not_yet_permitting""",9.18319700000001,39.374649,14.639282,40.688969 +TYNDP2020_47,under_consideration,150.0,2035,False,1000.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/225"", ""tyndp2020_proj_id""=>""225"", ""tyndp2020_invest_id""=>""1107"", ""tyndp_status""=>""under_consideration""",5.35583499999999,50.557942,6.568451,50.403266 +TYNDP2020_48,planned_not_yet_permitting,211.0,2025,False,750.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/349"", ""tyndp2020_proj_id""=>""349"", ""tyndp2020_invest_id""=>""1638"", ""tyndp_status""=>""planned_not_yet_permitting""",-3.474151,53.250084,-6.602175,53.348929 +TYNDP2020_49,planned_not_yet_permitting,255.0,2025,False,750.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/349"", ""tyndp2020_proj_id""=>""349"", ""tyndp2020_invest_id""=>""1640"", ""tyndp_status""=>""planned_not_yet_permitting""",-6.602175,53.348929,-9.578832,54.120411 diff --git a/data/transmission_projects/tyndp2020/upgraded_lines.csv b/data/transmission_projects/tyndp2020/upgraded_lines.csv new file mode 100644 index 000000000..ecf07b6aa --- /dev/null +++ b/data/transmission_projects/tyndp2020/upgraded_lines.csv @@ -0,0 +1,27 @@ +,project_status,length,build_year,underground,v_nom,tags,x0,y0,x1,y1,num_parallel,type +6031,in_permitting,32.0,2024,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/103"", ""tyndp2020_proj_id""=>""103"", ""tyndp2020_invest_id""=>""1490"", ""tyndp_status""=>""in_permitting""",5.763702,52.619725,6.14410399999999,52.52207,2,Al/St 240/40 4-bundle 380.0 +5123,in_permitting,35.0,2023,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/103"", ""tyndp2020_proj_id""=>""103"", ""tyndp2020_invest_id""=>""1539"", ""tyndp_status""=>""in_permitting""",4.592285,51.916321,4.83398400000001,51.704055,2,Al/St 240/40 4-bundle 380.0 +8435,planned_not_yet_permitting,41.5,2023,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/170"", ""tyndp2020_proj_id""=>""170"", ""tyndp2020_invest_id""=>""1663"", ""tyndp_status""=>""planned_not_yet_permitting""",14.440155,53.16571,14.831543,53.330873,2,Al/St 240/40 4-bundle 380.0 +12697,in_permitting,70.0,2021,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/230"", ""tyndp2020_proj_id""=>""230"", ""tyndp2020_invest_id""=>""353"", ""tyndp_status""=>""in_permitting""",14.440155,53.16571,15.038505,52.76751,2,Al/St 240/40 4-bundle 380.0 +14734,under_construction,49.0,2022,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/264"", ""tyndp2020_proj_id""=>""264"", ""tyndp2020_invest_id""=>""1284"", ""tyndp_status""=>""under_construction""",10.362854,46.809459,9.79568500000001,46.561693,2,Al/St 240/40 4-bundle 380.0 +6348,under_construction,17.0,2020,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/297"", ""tyndp2020_proj_id""=>""297"", ""tyndp2020_invest_id""=>""445"", ""tyndp_status""=>""under_construction""",4.26544200000001,51.358062,4.307132,51.301299,2,Al/St 240/40 4-bundle 380.0 +6347,in_permitting,4.0,2025,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/297"", ""tyndp2020_proj_id""=>""297"", ""tyndp2020_invest_id""=>""604"", ""tyndp_status""=>""in_permitting""",4.307132,51.301299,4.27917500000001,51.25504,2,Al/St 240/40 4-bundle 380.0 +10813,in_permitting,10.0,2025,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/322"", ""tyndp2020_proj_id""=>""322"", ""tyndp2020_invest_id""=>""1476"", ""tyndp_status""=>""in_permitting""",9.73251299999999,47.623752,9.80228979023356,47.6377345779987,2,Al/St 240/40 4-bundle 380.0 +14499,under_consideration,2.0,2030,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/341"", ""tyndp2020_proj_id""=>""341"", ""tyndp2020_invest_id""=>""1536"", ""tyndp_status""=>""under_consideration""",22.578278,44.679395,22.542572,44.612956,2,Al/St 240/40 4-bundle 380.0 +9296,planned_not_yet_permitting,203.0,2030,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/343"", ""tyndp2020_proj_id""=>""343"", ""tyndp2020_invest_id""=>""1534"", ""tyndp_status""=>""planned_not_yet_permitting""",15.26017,44.886816,16.500092,43.603267,2,Al/St 240/40 4-bundle 380.0 +6044,in_permitting,80.0,2029,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/346"", ""tyndp2020_proj_id""=>""346"", ""tyndp2020_invest_id""=>""1544"", ""tyndp_status""=>""in_permitting""",4.218787,51.424034,5.061511,51.608344,2,Al/St 240/40 4-bundle 380.0 +6087,under_construction,40.0,2023,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/348"", ""tyndp2020_proj_id""=>""348"", ""tyndp2020_invest_id""=>""1546"", ""tyndp_status""=>""under_construction""",6.86370800000001,53.418536,6.474759,53.213488,2,Al/St 240/40 4-bundle 380.0 +14697,under_consideration,35.512,2036,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/376"", ""tyndp2020_proj_id""=>""376"", ""tyndp2020_invest_id""=>""1559"", ""tyndp_status""=>""under_consideration""",21.474152,40.839788,21.475525,41.045181,2,Al/St 240/40 4-bundle 380.0 +14758,under_consideration,12.0,2030,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/377"", ""tyndp2020_proj_id""=>""377"", ""tyndp2020_invest_id""=>""1561"", ""tyndp_status""=>""under_consideration""",5.71701000000001,51.094036,5.89691199999999,51.140586,2,Al/St 240/40 4-bundle 380.0 +8621,in_permitting,131.0,2022,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/1"", ""tyndp2020_proj_id""=>""1"", ""tyndp2020_invest_id""=>""4"", ""tyndp_status""=>""in_permitting""",-7.72338899999999,41.614416,-7.793621146853301,41.52102394053344,2,Al/St 240/40 4-bundle 380.0 +6036,in_permitting,50.0,2025,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/103"", ""tyndp2020_proj_id""=>""103"", ""tyndp2020_invest_id""=>""1540"", ""tyndp_status""=>""in_permitting""",5.46295200000001,51.422333,5.89691199999999,51.140586,2,Al/St 240/40 4-bundle 380.0 +3715,in_permitting,94.0,2022,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/142"", ""tyndp2020_proj_id""=>""142"", ""tyndp2020_invest_id""=>""257"", ""tyndp_status""=>""in_permitting""",26.003265,42.200038,24.483032,42.09007,2,Al/St 240/40 4-bundle 380.0 +8260,in_permitting,74.0,2025,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/144"", ""tyndp2020_proj_id""=>""144"", ""tyndp2020_invest_id""=>""270"", ""tyndp_status""=>""in_permitting""",21.88777,45.30413,21.2815,45.7426,2,Al/St 240/40 4-bundle 380.0 +8367,in_permitting,274.0,2025,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/144"", ""tyndp2020_proj_id""=>""144"", ""tyndp2020_invest_id""=>""270"", ""tyndp_status""=>""in_permitting""",21.2815,45.7426,21.409607,46.176978,2,Al/St 240/40 4-bundle 380.0 +1693,in_permitting,,2026,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/227"", ""tyndp2020_proj_id""=>""227"", ""tyndp2020_invest_id""=>""630"", ""tyndp_status""=>""in_permitting""",20.187378,44.664746,19.500315,43.953021,2,Al/St 240/40 4-bundle 380.0 +12696,in_permitting,147.0,2022,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/230"", ""tyndp2020_proj_id""=>""230"", ""tyndp2020_invest_id""=>""1232"", ""tyndp_status""=>""in_permitting""",15.038505,52.76751,16.729431,52.32359,2,Al/St 240/40 4-bundle 380.0 +3966,planned_not_yet_permitting,70.0,2029,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/276"", ""tyndp2020_proj_id""=>""276"", ""tyndp2020_invest_id""=>""1207"", ""tyndp_status""=>""planned_not_yet_permitting""",-0.812988000000009,43.979969,-0.547943,44.563077,2,Al/St 240/40 4-bundle 380.0 +14277,in_permitting,15.0,2025,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/297"", ""tyndp2020_proj_id""=>""297"", ""tyndp2020_invest_id""=>""604"", ""tyndp_status""=>""in_permitting""",4.27917500000001,51.25504,4.177551,51.162122,2,Al/St 240/40 4-bundle 380.0 +14594,under_consideration,70.0,2034,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/325"", ""tyndp2020_proj_id""=>""325"", ""tyndp2020_invest_id""=>""1483"", ""tyndp_status""=>""under_consideration""",14.5912685597917,46.536256583087,15.119934,46.263443,2,Al/St 240/40 4-bundle 380.0 +12655,under_construction,161.0,2020,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/350"", ""tyndp2020_proj_id""=>""350"", ""tyndp2020_invest_id""=>""1622"", ""tyndp_status""=>""under_construction""",21.475525,41.045181,20.725963,41.239159,2,Al/St 240/40 4-bundle 380.0 +6355,under_consideration,90.0,2036,False,380,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/1074"", ""tyndp2020_proj_id""=>""1074"", ""tyndp2020_invest_id""=>""1744"", ""tyndp_status""=>""under_consideration""",19.756165,45.527517,19.609222,45.021127,2,Al/St 240/40 4-bundle 380.0 diff --git a/data/transmission_projects/tyndp2020/upgraded_links.csv b/data/transmission_projects/tyndp2020/upgraded_links.csv new file mode 100644 index 000000000..5fe56694d --- /dev/null +++ b/data/transmission_projects/tyndp2020/upgraded_links.csv @@ -0,0 +1,8 @@ +,project_status,length,build_year,underground,p_nom,tags,x0,y0,x1,y1 +T0,in_permitting,370.0,2027,True,2000.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/16"", ""tyndp2020_proj_id""=>""16"", ""tyndp2020_invest_id""=>""38"", ""tyndp_status""=>""in_permitting""",-2.880286,43.349776,-0.341949000000001,45.030833 +14801,under_construction,90.0,2020,True,1000.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/92"", ""tyndp2020_proj_id""=>""92"", ""tyndp2020_invest_id""=>""146"", ""tyndp_status""=>""under_construction""",6.46545399999999,50.831096,5.688171,50.729502 +T17,in_permitting,665.0,2024,True,1400.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/190"", ""tyndp2020_proj_id""=>""190"", ""tyndp2020_invest_id""=>""1382"", ""tyndp_status""=>""in_permitting""",7.23331357825616,60.5160767478361,-1.815491,57.484833 +T25,planned_not_yet_permitting,195.0,2023,True,500.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/286"", ""tyndp2020_proj_id""=>""286"", ""tyndp2020_invest_id""=>""1385"", ""tyndp_status""=>""planned_not_yet_permitting""",-6.96670499999999,52.27404,-4.850464,51.728729 +T12,in_permitting,220.0,2025,True,1400.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/153"", ""tyndp2020_proj_id""=>""153"", ""tyndp2020_invest_id""=>""987"", ""tyndp_status""=>""in_permitting""",-1.47628799999999,49.460984,-3.357697,50.739062 +T13,under_construction,770.0,2023,True,1400.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/167"", ""tyndp2020_proj_id""=>""167"", ""tyndp2020_invest_id""=>""998"", ""tyndp_status""=>""under_construction""",9.22851599999999,55.481188,-0.208961173685554,52.9600676434916 +5635,under_consideration,125.0,2036,True,700.0,"""url""=>""https://tyndp2020-project-platform.azurewebsites.net/projectsheets/transmission/1068"", ""tyndp2020_proj_id""=>""1068"", ""tyndp2020_invest_id""=>""1741"", ""tyndp_status""=>""under_consideration""",16.659393,57.43682,18.227692,57.571097 diff --git a/doc/configtables/links.csv b/doc/configtables/links.csv index c1ffb427c..9dedc36ab 100644 --- a/doc/configtables/links.csv +++ b/doc/configtables/links.csv @@ -2,5 +2,4 @@ p_max_pu,--,"Value in [0.,1.]","Correction factor for link capacities ``p_nom``." p_nom_max,MW,"float","Global upper limit for the maximum capacity of each extendable DC link." max_extension,MW,"float","Upper limit for the extended capacity of each extendable DC link." -include_tyndp,bool,"{'true', 'false'}","Specifies whether to add HVDC link projects from the `TYNDP 2018 `_ which are at least in permitting." under_construction,--,"One of {'zero': set capacity to zero, 'remove': remove completely, 'keep': keep with full capacity}","Specifies how to handle lines which are currently under construction." diff --git a/doc/configtables/transmission_projects.csv b/doc/configtables/transmission_projects.csv new file mode 100644 index 000000000..acba48426 --- /dev/null +++ b/doc/configtables/transmission_projects.csv @@ -0,0 +1,9 @@ +,Unit,Values,Description +enable,bool,"{true,false}",Whether to integrate this transmission projects or not. +include,--,,"Name of the transmission projects. They should be unique and have to be provided in the `data/transmission_projects` folder." +-- tyndp2020,bool,"{true,false}",Whether to integrate the TYNDP 2020 dataset. +-- nep,bool,"{true,false}",Whether to integrate the German network development plan dataset. +-- manual,bool,"{true,false}",Whether to integrate the manually added transmission projects. They are taken from the previously existing links_tyndp.csv file. +skip,list,,"Type of lines to skip from all transmission projects. Possible values are: ``upgraded_lines``, ``upgraded_links``, ``new_lines``, ``new_links``." +status,list or dict,,"Status to include into the model as list or as dict with name of project and status to include. Possible values for status are ``under_construction``, ``in_permitting``, ``confirmed``, ``planned_not_yet_permitted``, ``under_consideration``." +new_link_capacity,--,"{zero,keep}",Whether to set the new link capacity to the provided capacity or set it to zero. diff --git a/doc/configuration.rst b/doc/configuration.rst index 8695d4bb4..e140261ab 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -379,6 +379,23 @@ overwrite the existing values. .. _transformers_cf: +``transmission projects`` +======================= + +Allows to define additional transmission projects that will be added to the base network, e.g., from the TYNDP 2020 dataset. The projects are read in from the CSV files in the subfolder of ``data/transmission_projects/``. New transmission projects, e.g. from TYNDP 2024, can be added in a new subfolder of transmission projects, e.g. ``data/transmission_projects/tyndp2024`` while extending the list of ``transmission_projects`` in the ``config.yaml`` by ``tyndp2024``. The CSV files in the project folder should have the same columns as the CSV files in the template folder ``data/transmission_projects/template``. + +.. literalinclude:: ../config/config.default.yaml + :language: yaml + :start-at: transmission_projects: + :end-before: # docs + +.. csv-table:: + :header-rows: 1 + :widths: 22,7,22,33 + :file: configtables/transmission_projects.csv + +.. _transformers_cf: + ``transformers`` ================ diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 274231c6c..ac8c25597 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -9,6 +9,7 @@ Release Notes Upcoming Release ================ +* More modular and flexible handling of transmission projects. One can now add new transmission projects in a subfolder of `data/transmission projects` similar to the files in the template folder. After adding the new files and updating the config section `transmission_projects:`, transmission projects will be included if they are not duplicates of existing lines or other projects. * Add option to apply a gaussian kernel density smoothing to wind turbine power curves. diff --git a/envs/environment.fixed.yaml b/envs/environment.fixed.yaml index 67fff66f2..917deab78 100644 --- a/envs/environment.fixed.yaml +++ b/envs/environment.fixed.yaml @@ -335,7 +335,7 @@ dependencies: - pyomo=6.6.1 - pyparsing=3.1.2 - pyproj=3.6.1 -- pypsa=0.28.0 +- pypsa=0.29.0 - pyqt=5.15.9 - pyqt5-sip=12.12.2 - pyscipopt=5.0.1 diff --git a/envs/environment.yaml b/envs/environment.yaml index c8d8a6336..c87de77a4 100644 --- a/envs/environment.yaml +++ b/envs/environment.yaml @@ -11,7 +11,7 @@ dependencies: - pip - atlite>=0.2.9 -- pypsa>=0.28 +- pypsa>=0.29 - linopy - dask diff --git a/rules/build_electricity.smk b/rules/build_electricity.smk index 97e1e16aa..f35050917 100644 --- a/rules/build_electricity.smk +++ b/rules/build_electricity.smk @@ -56,7 +56,6 @@ rule base_network: snapshots=config_provider("snapshots"), drop_leap_day=config_provider("enable", "drop_leap_day"), lines=config_provider("lines"), - links=config_provider("links"), transformers=config_provider("transformers"), input: eg_buses="data/entsoegridkit/buses.csv", @@ -66,7 +65,6 @@ rule base_network: eg_transformers="data/entsoegridkit/transformers.csv", parameter_corrections="data/parameter_corrections.yaml", links_p_nom="data/links_p_nom.csv", - links_tyndp="data/links_tyndp.csv", country_shapes=resources("country_shapes.geojson"), offshore_shapes=resources("offshore_shapes.geojson"), europe_shape=resources("europe_shape.geojson"), @@ -342,6 +340,40 @@ rule build_line_rating: "../scripts/build_line_rating.py" +rule build_transmission_projects: + params: + transmission_projects=config_provider("transmission_projects"), + line_factor=config_provider("lines", "length_factor"), + input: + base_network=resources("networks/base.nc"), + offshore_shapes=resources("offshore_shapes.geojson"), + europe_shape=resources("europe_shape.geojson"), + transmission_projects=lambda w: [ + "data/transmission_projects/" + name + for name, include in config_provider("transmission_projects", "include")( + w + ).items() + if include + ], + output: + new_lines=resources("transmission_projects/new_lines.csv"), + new_links=resources("transmission_projects/new_links.csv"), + adjust_lines=resources("transmission_projects/adjust_lines.csv"), + adjust_links=resources("transmission_projects/adjust_links.csv"), + new_buses=resources("transmission_projects/new_buses.csv"), + log: + logs("build_transmission_projects.log"), + benchmark: + benchmarks("build_transmission_projects") + resources: + mem_mb=2000, + threads: 1 + conda: + "../envs/environment.yaml" + script: + "../scripts/build_transmission_projects.py" + + def input_profile_tech(w): return { f"profile_{tech}": resources(f"profile_{tech}.nc") @@ -402,6 +434,7 @@ rule add_electricity: costs=config_provider("costs"), foresight=config_provider("foresight"), drop_leap_day=config_provider("enable", "drop_leap_day"), + transmission_projects=config_provider("transmission_projects"), input: unpack(input_profile_tech), unpack(input_conventional), @@ -412,6 +445,17 @@ rule add_electricity: if config_provider("lines", "dynamic_line_rating", "activate")(w) else resources("networks/base.nc") ), + transmission_projects=lambda w: ( + [ + resources("transmission_projects/new_buses.csv"), + resources("transmission_projects/new_lines.csv"), + resources("transmission_projects/new_links.csv"), + resources("transmission_projects/adjust_lines.csv"), + resources("transmission_projects/adjust_links.csv"), + ] + if config_provider("transmission_projects", "enable")(w) + else [] + ), tech_costs=lambda w: resources( f"costs_{config_provider('costs', 'year')(w)}.csv" ), diff --git a/scripts/add_electricity.py b/scripts/add_electricity.py index 495109533..076eb84ed 100755 --- a/scripts/add_electricity.py +++ b/scripts/add_electricity.py @@ -84,6 +84,7 @@ import logging from itertools import product +from pathlib import Path from typing import Dict, List import geopandas as gpd @@ -799,6 +800,25 @@ def attach_line_rating( n.lines_t.s_max_pu *= s_max_pu +def add_transmission_projects(n, transmission_projects): + logger.info(f"Adding transmission projects to network.") + for path in transmission_projects: + path = Path(path) + df = pd.read_csv(path, index_col=0, dtype={"bus0": str, "bus1": str}) + if df.empty: + continue + if "new_buses" in path.name: + n.madd("Bus", df.index, **df) + elif "new_lines" in path.name: + n.madd("Line", df.index, **df) + elif "new_links" in path.name: + n.madd("Link", df.index, **df) + elif "adjust_lines": + n.lines.update(df) + elif "adjust_links": + n.links.update(df) + + if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake @@ -811,6 +831,9 @@ def attach_line_rating( n = pypsa.Network(snakemake.input.base_network) + if params["transmission_projects"]["enable"]: + add_transmission_projects(n, snakemake.input.transmission_projects) + time = get_snapshots(snakemake.params.snapshots, snakemake.params.drop_leap_day) n.set_snapshots(time) diff --git a/scripts/base_network.py b/scripts/base_network.py index 8172b332a..78cf6984f 100644 --- a/scripts/base_network.py +++ b/scripts/base_network.py @@ -25,7 +25,6 @@ links: p_max_pu: under_construction: - include_tyndp: transformers: x: @@ -43,7 +42,6 @@ - ``data/entsoegridkit``: Extract from the geographical vector data of the online `ENTSO-E Interactive Map `_ by the `GridKit `_ toolkit dating back to March 2022. - ``data/parameter_corrections.yaml``: Corrections for ``data/entsoegridkit`` - ``data/links_p_nom.csv``: confer :ref:`links` -- ``data/links_tyndp.csv``: List of projects in the `TYNDP 2018 `_ that are at least *in permitting* with fields for start- and endpoint (names and coordinates), length, capacity, construction status, and project reference ID. - ``resources/country_shapes.geojson``: confer :ref:`shapes` - ``resources/offshore_shapes.geojson``: confer :ref:`shapes` - ``resources/europe_shape.geojson``: confer :ref:`shapes` @@ -222,112 +220,6 @@ def _load_links_from_eg(buses, eg_links): return links -def _add_links_from_tyndp(buses, links, links_tyndp, europe_shape): - links_tyndp = pd.read_csv(links_tyndp) - - # remove all links from list which lie outside all of the desired countries - europe_shape = gpd.read_file(europe_shape).loc[0, "geometry"] - europe_shape_prepped = shapely.prepared.prep(europe_shape) - x1y1_in_europe_b = links_tyndp[["x1", "y1"]].apply( - lambda p: europe_shape_prepped.contains(Point(p)), axis=1 - ) - x2y2_in_europe_b = links_tyndp[["x2", "y2"]].apply( - lambda p: europe_shape_prepped.contains(Point(p)), axis=1 - ) - is_within_covered_countries_b = x1y1_in_europe_b & x2y2_in_europe_b - - if not is_within_covered_countries_b.all(): - logger.info( - "TYNDP links outside of the covered area (skipping): " - + ", ".join(links_tyndp.loc[~is_within_covered_countries_b, "Name"]) - ) - - links_tyndp = links_tyndp.loc[is_within_covered_countries_b] - if links_tyndp.empty: - return buses, links - - has_replaces_b = links_tyndp.replaces.notnull() - oids = dict(Bus=_get_oid(buses), Link=_get_oid(links)) - keep_b = dict( - Bus=pd.Series(True, index=buses.index), Link=pd.Series(True, index=links.index) - ) - for reps in links_tyndp.loc[has_replaces_b, "replaces"]: - for comps in reps.split(":"): - oids_to_remove = comps.split(".") - c = oids_to_remove.pop(0) - keep_b[c] &= ~oids[c].isin(oids_to_remove) - buses = buses.loc[keep_b["Bus"]] - links = links.loc[keep_b["Link"]] - - links_tyndp["j"] = _find_closest_links( - links, links_tyndp, distance_upper_bound=0.20 - ) - # Corresponds approximately to 20km tolerances - - if links_tyndp["j"].notnull().any(): - logger.info( - "TYNDP links already in the dataset (skipping): " - + ", ".join(links_tyndp.loc[links_tyndp["j"].notnull(), "Name"]) - ) - links_tyndp = links_tyndp.loc[links_tyndp["j"].isnull()] - if links_tyndp.empty: - return buses, links - - tree_buses = buses.query("carrier=='AC'") - tree = KDTree(tree_buses[["x", "y"]]) - _, ind0 = tree.query(links_tyndp[["x1", "y1"]]) - ind0_b = ind0 < len(tree_buses) - links_tyndp.loc[ind0_b, "bus0"] = tree_buses.index[ind0[ind0_b]] - - _, ind1 = tree.query(links_tyndp[["x2", "y2"]]) - ind1_b = ind1 < len(tree_buses) - links_tyndp.loc[ind1_b, "bus1"] = tree_buses.index[ind1[ind1_b]] - - links_tyndp_located_b = ( - links_tyndp["bus0"].notnull() & links_tyndp["bus1"].notnull() - ) - if not links_tyndp_located_b.all(): - logger.warning( - "Did not find connected buses for TYNDP links (skipping): " - + ", ".join(links_tyndp.loc[~links_tyndp_located_b, "Name"]) - ) - links_tyndp = links_tyndp.loc[links_tyndp_located_b] - - logger.info("Adding the following TYNDP links: " + ", ".join(links_tyndp["Name"])) - - links_tyndp = links_tyndp[["bus0", "bus1"]].assign( - carrier="DC", - p_nom=links_tyndp["Power (MW)"], - length=links_tyndp["Length (given) (km)"].fillna( - links_tyndp["Length (distance*1.2) (km)"] - ), - under_construction=True, - underground=False, - geometry=( - links_tyndp[["x1", "y1", "x2", "y2"]].apply( - lambda s: str(LineString([[s.x1, s.y1], [s.x2, s.y2]])), axis=1 - ) - ), - tags=( - '"name"=>"' - + links_tyndp["Name"] - + '", ' - + '"ref"=>"' - + links_tyndp["Ref"] - + '", ' - + '"status"=>"' - + links_tyndp["status"] - + '"' - ), - ) - - links_tyndp.index = "T" + links_tyndp.index.astype(str) - - links = pd.concat([links, links_tyndp], sort=True) - - return buses, links - - def _load_lines_from_eg(buses, eg_lines): lines = ( pd.read_csv( @@ -731,7 +623,6 @@ def base_network( eg_lines, eg_links, links_p_nom, - links_tyndp, europe_shape, country_shapes, offshore_shapes, @@ -741,8 +632,6 @@ def base_network( buses = _load_buses_from_eg(eg_buses, europe_shape, config["electricity"]) links = _load_links_from_eg(buses, eg_links) - if config["links"].get("include_tyndp"): - buses, links = _add_links_from_tyndp(buses, links, links_tyndp, europe_shape) converters = _load_converters_from_eg(buses, eg_converters) @@ -911,7 +800,6 @@ def append_bus_shapes(n, shapes, type): snakemake.input.eg_lines, snakemake.input.eg_links, snakemake.input.links_p_nom, - snakemake.input.links_tyndp, snakemake.input.europe_shape, snakemake.input.country_shapes, snakemake.input.offshore_shapes, diff --git a/scripts/build_transmission_projects.py b/scripts/build_transmission_projects.py new file mode 100644 index 000000000..ba185d17e --- /dev/null +++ b/scripts/build_transmission_projects.py @@ -0,0 +1,564 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2017-2024 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT + +# coding: utf-8 +""" +Gets the transmission projects defined in the config file, concatenates and +deduplicates them. Projects are later included in :mod:`add_electricity.py`. + +Relevant Settings +----------------- + +.. code:: yaml + +transmission_projects: + include: + #tyndp: true # For later, when other TYNDP projects are combined with new version + nep: true + status: + - confirmed + - in_permitting + - under_construction + #- under_consideration + link_under_construction: zero + +.. seealso:: + Documentation of the configuration file ``config/config.yaml`` at + :ref:`transmission_projects` +Inputs +------ + +- ``networks/base_network.nc``: Base network topology for the electricity grid. This is processed in :mod:`base_network.py`. +- ``data/transmission_projects/"project_name"/``: Takes the transmission projects from the subfolder of data/transmission_projects. The subfolder name is the project name. +- ``offshore_shapes.geojson``: Shapefile containing the offshore regions. Used to determine if a new bus should be added for a new line or link. +- ``europe_shape.geojson``: Shapefile containing the shape of Europe. Used to determine if a project is within the considered countries. + +Outputs +------- + +- ``transmission_projects/new_lines.csv``: New project lines to be added to the network. This includes new lines and upgraded lines. +- ``transmission_projects/new_links.csv``: New project links to be added to the network. This includes new links and upgraded links. +- ``transmission_projects/adjust_lines.csv``: For lines which are upgraded, the decommissioning year of the existing line is adjusted to the build year of the upgraded line. +- ``transmission_projects/adjust_links.csv``: For links which are upgraded, the decommissioning year of the existing link is adjusted to the build year of the upgraded link. +- ``transmission_projects/new_buses.csv``: For some links, we have to add new buses (e.g. North Sea Wind Power Hub). +""" +import logging +import os +from pathlib import Path + +import geopandas as gpd +import numpy as np +import pandas as pd +import pypsa +import shapely +from _helpers import configure_logging, set_scenario_config +from pypsa.descriptors import nominal_attrs +from scipy import spatial +from shapely.geometry import LineString, Point + +logger = logging.getLogger(__name__) + + +def add_new_buses(n, new_ports): + # Add new buses for the ports which do not have an existing bus close by. If there are multiple ports at the same location, only one bus is added. + duplicated = new_ports.duplicated(subset=["x", "y"], keep="first") + to_add = new_ports[~duplicated] + added_buses = n.madd( + "Bus", + names=to_add.index, + suffix=" bus", + x=to_add.x, + y=to_add.y, + v_nom=380, + under_construction=True, + symbol="substation", + substation_off=True, + substation_lv=False, + carrier="AC", + ) + new_buses = n.buses.loc[added_buses].copy().dropna(axis=1, how="all") + new_ports.loc[to_add.index, "neighbor"] = added_buses + new_ports["neighbor"] = new_ports.groupby(["x", "y"])["neighbor"].transform("first") + return new_ports, new_buses + + +def find_country_for_bus(bus, shapes): + """ + Find the country of a bus based on its coordinates and the provided + shapefile. + + Shapefile must contain a column "country" with the country names. + """ + point = Point(bus.x, bus.y) + country = shapes.loc[shapes.contains(point), "country"] + return country.values[0] + + +def connect_new_lines( + lines, + n, + new_buses_df, + offshore_shapes=None, + distance_upper_bound=np.inf, + bus_carrier="AC", +): + """ + Find the closest existing bus to the port of each line. + + If closest bus is further away than distance_upper_bound and is + inside an offshore region, a new bus is created. and the line is + connected to it. + """ + bus_carrier = np.atleast_1d(bus_carrier) + buses = n.buses.query("carrier in @bus_carrier").copy() + bus_tree = spatial.KDTree(buses[["x", "y"]]) + + for port in [0, 1]: + lines_port = lines["geometry"].apply( + lambda x: pd.Series( + get_bus_coords_from_port(x, port=port), index=["x", "y"] + ) + ) + distances, indices = bus_tree.query(lines_port) + # Series of lines with closest bus in the existing network and whether they match the distance criterion + lines_port["neighbor"] = buses.iloc[indices].index + lines_port["match_distance"] = distances < distance_upper_bound + # For buses which are not close to any existing bus, only add a new bus if the line is going offshore (e.g. North Sea Wind Power Hub) + if not lines_port.match_distance.all() and offshore_shapes.union_all(): + potential_new_buses = lines_port[~lines_port.match_distance] + is_offshore = potential_new_buses.apply( + lambda x: offshore_shapes.union_all().contains(Point(x.x, x.y)), axis=1 + ) + new_buses = potential_new_buses[is_offshore] + if not new_buses.empty: + new_port, new_buses = add_new_buses(n, new_buses) + new_buses["country"] = new_buses.apply( + lambda bus: find_country_for_bus(bus, offshore_shapes), axis=1 + ) + lines_port.loc[new_port.index, "match_distance"] = True + lines_port.loc[new_port.index, "neighbor"] = new_port.neighbor + new_buses_df = pd.concat([new_buses_df, new_buses]) + + if not lines_port.match_distance.all(): + logging.warning( + "Could not find bus close enough to connect the the following lines:\n" + + str(lines_port[~lines_port.match_distance].index.to_list()) + + "\n Lines will be ignored." + ) + lines.drop(lines_port[~lines_port.match_distance].index, inplace=True) + lines_port = lines_port[lines_port.match_distance] + + lines.loc[lines_port.index, f"bus{port}"] = lines_port["neighbor"] + + lines = lines.assign(under_construction=True) + + return lines, new_buses_df + + +def get_branch_coords_from_geometry(linestring, reversed=False): + """ + Reduces a linestring to its start and end points. Used to simplify the + linestring which can have more than two points. + + Parameters: + linestring: Shapely linestring + reversed (bool, optional): If True, returns the end and start points instead of the start and end points. + Defaults to False. + + Returns: + numpy.ndarray: Flattened array of start and end coordinates. + """ + coords = np.asarray(linestring.coords) + ind = [0, -1] if not reversed else [-1, 0] + start_end_coords = coords[ind] + return start_end_coords.flatten() + + +def get_branch_coords_from_buses(line): + """ + Gets line string for branch component in an pypsa network. + + Parameters: + linestring: shapely linestring + reversed (bool, optional): If True, returns the end and start points instead of the start and end points. + Defaults to False. + + Returns: + numpy.ndarray: Flattened array of start and end coordinates. + """ + start_coords = n.buses.loc[line.bus0, ["x", "y"]].values + end_coords = n.buses.loc[line.bus1, ["x", "y"]].values + return np.array([start_coords, end_coords]).flatten() + + +def get_bus_coords_from_port(linestring, port=0): + """ + Extracts the coordinates of a specified port from a given linestring. + + Parameters: + linestring: The shapely linestring. + port (int): The index of the port to extract coordinates from. Default is 0. + + Returns: + tuple: The coordinates of the specified port as a tuple (x, y). + """ + coords = np.asarray(linestring.coords) + ind = [0, -1] + coords = coords[ind] + coords = coords[port] + return coords + + +def find_closest_lines(lines, new_lines, distance_upper_bound=0.1, type="new"): + """ + Find the closest lines in the existing set of lines to a set of new lines. + + Parameters: + lines (pandas.DataFrame): DataFrame of the existing lines. + new_lines (pandas.DataFrame): DataFrame with column geometry containing the new lines. + distance_upper_bound (float, optional): Maximum distance to consider a line as a match. Defaults to 0.1 which corresponds to approximately 15 km. + + Returns: + pandas.Series: Series containing with index the new lines and values providing closest existing line. + """ + + # get coordinates of start and end points of all lines, for new lines we need to check both directions + treelines = lines.apply(get_branch_coords_from_buses, axis=1) + querylines = pd.concat( + [ + new_lines["geometry"].apply(get_branch_coords_from_geometry), + new_lines["geometry"].apply(get_branch_coords_from_geometry, reversed=True), + ] + ) + treelines = np.vstack(treelines) + querylines = np.vstack(querylines) + tree = spatial.KDTree(treelines) + dist, ind = tree.query(querylines, distance_upper_bound=distance_upper_bound) + found_b = ind < len(lines) + # since the new lines are checked in both directions, we need to find the correct index of the new line + found_i = np.arange(len(querylines))[found_b] % len(new_lines) + # create a DataFrame with the distances, new line and its closest existing line + line_map = pd.DataFrame( + dict(D=dist[found_b], existing_line=lines.index[ind[found_b] % len(lines)]), + index=new_lines.index[found_i].rename("new_lines"), + ) + if type == "new": + if len(found_i) != 0: + logger.warning( + "Found new lines similar to existing lines:\n" + + str(line_map["existing_line"].to_dict()) + + "\n Lines are assumed to be duplicated and will be ignored." + ) + elif type == "upgraded": + if len(found_i) < len(new_lines): + not_found = new_lines.index.difference(line_map.index) + logger.warning( + "Could not find upgraded lines close enough to existing lines:\n" + + str(not_found.to_list()) + + "\n Lines will be ignored." + ) + # only keep the closer line of the new line pair (since lines are checked in both directions) + line_map = line_map.sort_values(by="D")[ + lambda ds: ~ds.index.duplicated(keep="first") + ].sort_index()["existing_line"] + return line_map + + +def adjust_decommissioning(upgraded_lines, line_map): + """ + Adjust the decommissioning year of the existing lines to the built year of + the upgraded lines. + """ + to_update = pd.DataFrame(index=line_map) + to_update["build_year"] = ( + 1990 # dummy build_year to make existing lines decommissioned when upgraded lines are built + ) + to_update["lifetime"] = ( + upgraded_lines.rename(line_map)["build_year"] - 1990 + ) # set lifetime to the difference between build year of upgraded line and existing line + return to_update + + +def get_upgraded_lines(branch_component, n, upgraded_lines, line_map): + """ + Get upgraded lines by merging information of existing line and upgraded + line. + """ + # get first the information of the existing lines which will be upgraded + lines_to_add = n.df(branch_component).loc[line_map].copy() + # get columns of upgraded lines which are not in existing lines + new_columns = upgraded_lines.columns.difference(lines_to_add.columns) + # rename upgraded lines to match existing lines + upgraded_lines = upgraded_lines.rename(line_map) + # set the same index names to be able to merge + upgraded_lines.index.name = lines_to_add.index.name + # merge upgraded lines with existing lines + lines_to_add.update(upgraded_lines) + # add column which was added in upgraded lines + lines_to_add = pd.concat([lines_to_add, upgraded_lines[new_columns]], axis=1) + # only consider columns of original upgraded lines and bus0 and bus1 + lines_to_add = lines_to_add.loc[:, ["bus0", "bus1", *upgraded_lines.columns]] + # set capacity of upgraded lines to capacity of existing lines + lines_to_add[nominal_attrs[branch_component]] = n.df(branch_component).loc[ + line_map, nominal_attrs[branch_component] + ] + # change index of new lines to avoid duplicates + lines_to_add.index = lines_to_add.index.astype(str) + "_upgraded" + return lines_to_add + + +def get_project_files(path, skip=[]): + path = Path(path) + lines = {} + files = [ + p + for p in path.iterdir() + if p.is_file() + and p.suffix == ".csv" + and not any(substring in p.name for substring in skip) + ] + if not files: + logger.warning(f"No projects found for {path.parent.name}") + return lines + for file in files: + df = pd.read_csv(file, index_col=0) + df["geometry"] = df.apply( + lambda x: LineString([[x.x0, x.y0], [x.x1, x.y1]]), axis=1 + ) + df.drop(columns=["x0", "y0", "x1", "y1"], inplace=True) + lines[file.stem] = df + return lines + + +def remove_projects_outside_countries(lines, europe_shape): + """ + Remove projects which are not in the considered countries. + """ + europe_shape_prepped = shapely.prepared.prep(europe_shape) + is_within_covered_countries = lines["geometry"].apply( + lambda x: europe_shape_prepped.contains(x) + ) + + if not is_within_covered_countries.all(): + logger.warning( + "Project lines outside of the covered area (skipping): " + + ", ".join(str(i) for i in lines.loc[~is_within_covered_countries].index) + ) + + lines = lines.loc[is_within_covered_countries] + return lines + + +def is_similar(ds1, ds2, percentage=10): + """ + Check if values in series ds2 are within a specified percentage of series + ds1. + + Returns: + - A boolean series where True indicates ds2 values are within the percentage range of ds2. + """ + lower_bound = ds1 * (1 - percentage / 100) + upper_bound = ds1 * (1 + percentage / 100) + return np.logical_and(ds2 >= lower_bound, ds2 <= upper_bound) + + +def set_underwater_fraction(new_links, offshore_shapes): + new_links_gds = gpd.GeoSeries(new_links["geometry"]) + new_links["underwater_fraction"] = ( + new_links_gds.intersection(offshore_shapes.union_all()).length + / new_links_gds.length + ).round(2) + + +def add_projects( + n, + new_lines_df, + new_links_df, + adjust_lines_df, + adjust_links_df, + new_buses_df, + europe_shape, + offshore_shapes, + path, + plan, + status=["confirmed", "under construction"], + skip=[], +): + lines_dict = get_project_files(path, skip=skip) + for key, lines in lines_dict.items(): + logging.info(f"Processing {key.replace('_', ' ')} projects from {plan}.") + lines = remove_projects_outside_countries(lines, europe_shape) + if isinstance(status, dict): + status = status[plan] + lines = lines.loc[lines.project_status.isin(status)] + if lines.empty: + continue + if key == "new_lines": + new_lines, new_buses_df = connect_new_lines( + lines, n, new_buses_df, bus_carrier="AC" + ) + duplicate_lines = find_closest_lines( + n.lines, new_lines, distance_upper_bound=0.10, type="new" + ) + # TODO: think about using build_year instead of v_nom + # ignore duplicates where v_nom is not within a tolerance of 10% + to_ignore = is_similar( + new_lines.loc[duplicate_lines.index, "v_nom"], + duplicate_lines.map(n.lines["v_nom"]), + ) + duplicate_lines = duplicate_lines[~to_ignore] + new_lines = new_lines.drop(duplicate_lines.index, errors="ignore") + new_lines_df = pd.concat([new_lines_df, new_lines]) + # add new lines to network to be able to find added duplicates + n.madd("Line", new_lines.index, **new_lines) + elif key == "new_links": + new_links, new_buses_df = connect_new_lines( + lines, + n, + new_buses_df, + offshore_shapes=offshore_shapes, + distance_upper_bound=0.4, + bus_carrier=["AC", "DC"], + ) + duplicate_links = find_closest_lines( + n.links, new_links, distance_upper_bound=0.10, type="new" + ) + # TODO: think about using build_year instead of p_nom + # ignore duplicates where p_nom is not within a tolerance of 10% + to_ignore = is_similar( + new_links.loc[duplicate_links.index, "p_nom"], + duplicate_links.map(n.links["p_nom"]), + ) + duplicate_links = duplicate_links[~to_ignore] + new_links = new_links.drop(duplicate_links.index, errors="ignore") + set_underwater_fraction(new_links, offshore_shapes) + new_links_df = pd.concat([new_links_df, new_links]) + # add new links to network to be able to find added duplicates + n.madd("Link", new_links.index, **new_links) + elif key == "upgraded_lines": + line_map = find_closest_lines( + n.lines, lines, distance_upper_bound=0.30, type="upgraded" + ) + upgraded_lines = lines.loc[line_map.index] + lines_to_adjust = adjust_decommissioning(upgraded_lines, line_map) + adjust_lines_df = pd.concat([adjust_lines_df, lines_to_adjust]) + upgraded_lines = get_upgraded_lines("Line", n, upgraded_lines, line_map) + new_lines_df = pd.concat([new_lines_df, upgraded_lines]) + elif key == "upgraded_links": + line_map = find_closest_lines( + n.links.query("carrier=='DC'"), + lines, + distance_upper_bound=0.30, + type="upgraded", + ) + upgraded_links = lines.loc[line_map.index] + links_to_adjust = adjust_decommissioning(upgraded_links, line_map) + adjust_links_df = pd.concat([adjust_links_df, links_to_adjust]) + upgraded_links = get_upgraded_lines("Link", n, upgraded_links, line_map) + new_links_df = pd.concat([new_links_df, upgraded_links]) + set_underwater_fraction(new_links_df, offshore_shapes) + else: + logger.warning(f"Unknown project type {key}") + continue + return new_lines_df, new_links_df, adjust_lines_df, adjust_links_df, new_buses_df + + +def fill_length_from_geometry(line, line_factor=1.2): + if not pd.isna(line.length): + return line.length + length = gpd.GeoSeries(line["geometry"], crs=4326).to_crs(3035).length.values[0] + length = length / 1000 * line_factor + return round(length, 1) + + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake("build_transmission_projects", run="all") + configure_logging(snakemake) + set_scenario_config(snakemake) + + line_factor = snakemake.params.line_factor + + n = pypsa.Network(snakemake.input.base_network) + + new_lines_df = pd.DataFrame() + new_links_df = pd.DataFrame() + adjust_lines_df = pd.DataFrame() + adjust_links_df = pd.DataFrame() + new_buses_df = pd.DataFrame() + + europe_shape = gpd.read_file(snakemake.input.europe_shape).loc[0, "geometry"] + offshore_shapes = gpd.read_file(snakemake.input.offshore_shapes).rename( + {"name": "country"}, axis=1 + ) + + transmission_projects = snakemake.params.transmission_projects + projects = [ + project + for project, include in transmission_projects["include"].items() + if include + ] + paths = snakemake.input.transmission_projects + for project in projects: + path = list(filter(lambda path: project in path, paths))[0] + new_lines_df, new_links_df, adjust_lines_df, adjust_links_df, new_buses_df = ( + add_projects( + n, + new_lines_df, + new_links_df, + adjust_lines_df, + adjust_links_df, + new_buses_df, + europe_shape, + offshore_shapes, + path=path, + plan=project, + status=transmission_projects["status"], + skip=transmission_projects["skip"], + ) + ) + if not new_lines_df.empty: + line_type = "Al/St 240/40 4-bundle 380.0" + # Add new line type for new lines + new_lines_df["type"] = new_lines_df["type"].fillna(line_type) + new_lines_df["num_parallel"] = new_lines_df["num_parallel"].fillna(2) + if "underground" in new_lines_df.columns: + new_lines_df["underground"] = ( + new_lines_df["underground"].astype("bool").fillna(False) + ) + # Add carrier types of lines + new_lines_df["carrier"] = "AC" + # Fill empty length values with length calculated from geometry + new_lines_df["length"] = new_lines_df.apply( + fill_length_from_geometry, args=(line_factor,), axis=1 + ) + # get s_nom from line type + new_lines_df["s_nom"] = ( + np.sqrt(3) + * n.line_types.loc[new_lines_df["type"], "i_nom"].values + * new_lines_df["v_nom"] + * new_lines_df["num_parallel"] + ).round(2) + if not new_links_df.empty: + # Add carrier types of lines and links + new_links_df["carrier"] = "DC" + # Fill empty length values with length calculated from geometry + new_links_df["length"] = new_links_df.apply( + fill_length_from_geometry, args=(line_factor,), axis=1 + ) + # Whether to keep existing link capacity or set to zero + not_upgraded = ~new_links_df.index.str.contains("upgraded") + if transmission_projects["new_link_capacity"] == "keep": + new_links_df.loc[not_upgraded, "p_nom"] = new_links_df["p_nom"].fillna(0) + elif transmission_projects["new_link_capacity"] == "zero": + new_links_df.loc[not_upgraded, "p_nom"] = 0 + # export csv files for new buses, lines, links and adjusted lines and links + new_lines_df.to_csv(snakemake.output.new_lines) + new_links_df.to_csv(snakemake.output.new_links) + adjust_lines_df.to_csv(snakemake.output.adjust_lines) + adjust_links_df.to_csv(snakemake.output.adjust_links) + new_buses_df.to_csv(snakemake.output.new_buses) diff --git a/scripts/cluster_network.py b/scripts/cluster_network.py index da7bd1788..79184167e 100644 --- a/scripts/cluster_network.py +++ b/scripts/cluster_network.py @@ -410,6 +410,7 @@ def clustering_for_n_clusters( generator_strategies=generator_strategies, one_port_strategies=one_port_strategies, scale_link_capital_costs=False, + custom_line_groupers=["build_year"], ) if not n.links.empty: diff --git a/scripts/simplify_network.py b/scripts/simplify_network.py index c54e3418b..9ab0cb233 100644 --- a/scripts/simplify_network.py +++ b/scripts/simplify_network.py @@ -124,7 +124,7 @@ def simplify_network_to_380(n): n.buses["v_nom"] = 380.0 - (linetype_380,) = n.lines.loc[n.lines.v_nom == 380.0, "type"].unique() + linetype_380 = n.lines["type"].mode()[0] n.lines["type"] = linetype_380 n.lines["v_nom"] = 380 n.lines["i_nom"] = n.line_types.i_nom[linetype_380] @@ -579,7 +579,7 @@ def find_closest_bus(n, x, y, tol=2000): if "snakemake" not in globals(): from _helpers import mock_snakemake - snakemake = mock_snakemake("simplify_network", simpl="") + snakemake = mock_snakemake("simplify_network", simpl="", run="all") configure_logging(snakemake) set_scenario_config(snakemake) @@ -655,6 +655,7 @@ def find_closest_bus(n, x, y, tol=2000): "substation_off", "geometry", "underground", + "project_status", ] n.buses.drop(remove, axis=1, inplace=True, errors="ignore") n.lines.drop(remove, axis=1, errors="ignore", inplace=True) From 55370a57fb61f5985dfaf05b322a00153df5e44f Mon Sep 17 00:00:00 2001 From: Fabian Hofmann Date: Thu, 15 Aug 2024 14:51:51 +0200 Subject: [PATCH 204/344] retrieve: remove cutout protection (#1220) --- rules/retrieve.smk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rules/retrieve.smk b/rules/retrieve.smk index b2382f51f..de84b2fae 100644 --- a/rules/retrieve.smk +++ b/rules/retrieve.smk @@ -85,7 +85,7 @@ if config["enable"]["retrieve"] and config["enable"].get("retrieve_cutout", True "https://zenodo.org/records/12791128/files/{cutout}.nc", ), output: - protected("cutouts/" + CDIR + "{cutout}.nc"), + "cutouts/" + CDIR + "{cutout}.nc", log: "logs/" + CDIR + "retrieve_cutout_{cutout}.log", resources: From 0221372b499e9685986ba593fb799c156f144c44 Mon Sep 17 00:00:00 2001 From: cpschau <124347782+cpschau@users.noreply.github.com> Date: Thu, 15 Aug 2024 14:52:33 +0200 Subject: [PATCH 205/344] modified environments to include tabula-py update and fixed PDF econding (#1219) --- doc/release_notes.rst | 2 ++ envs/environment.fixed.yaml | 3 ++- envs/environment.yaml | 3 ++- scripts/build_biomass_transport_costs.py | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index ac8c25597..892558b19 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -9,6 +9,8 @@ Release Notes Upcoming Release ================ +* Fixed PDF encoding in ``build_biomass_transport_costs`` with update of tabula-py and jpype1 + * More modular and flexible handling of transmission projects. One can now add new transmission projects in a subfolder of `data/transmission projects` similar to the files in the template folder. After adding the new files and updating the config section `transmission_projects:`, transmission projects will be included if they are not duplicates of existing lines or other projects. * Add option to apply a gaussian kernel density smoothing to wind turbine power curves. diff --git a/envs/environment.fixed.yaml b/envs/environment.fixed.yaml index 917deab78..31dc75274 100644 --- a/envs/environment.fixed.yaml +++ b/envs/environment.fixed.yaml @@ -391,7 +391,7 @@ dependencies: - stack_data=0.6.2 - statsmodels=0.14.2 - stopit=1.1.2 -- tabula-py=2.7.0 +- jpype1=1.5.0 - tabulate=0.9.0 - tbb=2021.11.0 - tblib=3.0.0 @@ -466,3 +466,4 @@ dependencies: - snakemake-executor-plugin-slurm-jobstep==0.2.1 - snakemake-storage-plugin-http==0.2.3 - tsam==2.3.1 + - tabula-py=2.9.3 diff --git a/envs/environment.yaml b/envs/environment.yaml index c87de77a4..9115ccd97 100644 --- a/envs/environment.yaml +++ b/envs/environment.yaml @@ -43,7 +43,7 @@ dependencies: - geopy - tqdm - pytz -- tabula-py +- jpype1 - pyxlsb - graphviz - pre-commit @@ -63,3 +63,4 @@ dependencies: - snakemake-executor-plugin-slurm - snakemake-executor-plugin-cluster-generic - highspy + - tabula-py diff --git a/scripts/build_biomass_transport_costs.py b/scripts/build_biomass_transport_costs.py index 085a0f004..d56509026 100644 --- a/scripts/build_biomass_transport_costs.py +++ b/scripts/build_biomass_transport_costs.py @@ -24,7 +24,7 @@ ENERGY_CONTENT = 4.8 # unit MWh/t (wood pellets) system = platform.system() -encoding = "cp1252" if system == "Windows" else None +encoding = "cp1252" if system == "Windows" else "utf-8" def get_countries(): From 55cca6a0f93308d7500e36dd6c98b672d4501d06 Mon Sep 17 00:00:00 2001 From: Bobby Xiong <36541459+bobbyxng@users.noreply.github.com> Date: Thu, 15 Aug 2024 19:45:49 +0200 Subject: [PATCH 206/344] Bug fix: Carrier type of added supernodes in simplify_network need to be set to "AC" (#1221) * Fixed simplify_network.py to handle more complex topologies, i.e. if two links are connected to nearby AC buses separated by an AC line. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Added fix for Corsica node: If region containing Corsica is modelled, include substation on Corsica as supernode (end point). * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Bug fix: Carrier type of all supernodes corrected to 'AC' * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Bug fix: Carrier type of all supernodes corrected to 'AC' * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Bug fix: Carrier type of all supernodes corrected to 'AC' --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- scripts/simplify_network.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/scripts/simplify_network.py b/scripts/simplify_network.py index 9ab0cb233..66a5d1190 100644 --- a/scripts/simplify_network.py +++ b/scripts/simplify_network.py @@ -301,19 +301,11 @@ def simplify_links( # Only span graph over the DC link components G = n.graph(branch_components=["Link"]) - def split_links(nodes): + def split_links(nodes, added_supernodes=None): nodes = frozenset(nodes) seen = set() - # Corsica substation - node_corsica = find_closest_bus( - n, - x=9.44802, - y=42.52842, - tol=2000, # Tolerance needed to only return the bus if the region is actually modelled - ) - # Supernodes are endpoints of links, identified by having lass then two neighbours or being an AC Bus # An example for the latter is if two different links are connected to the same AC bus. supernodes = { @@ -322,7 +314,7 @@ def split_links(nodes): if ( (len(G.adj[m]) < 2 or (set(G.adj[m]) - nodes)) or (n.buses.loc[m, "carrier"] == "AC") - or (m == node_corsica) + or (m in added_supernodes) ) } @@ -359,6 +351,17 @@ def split_links(nodes): 0.0, index=n.buses.index, columns=list(connection_costs_per_link) ) + node_corsica = find_closest_bus( + n, + x=9.44802, + y=42.52842, + tol=2000, # Tolerance needed to only return the bus if the region is actually modelled + ) + + added_supernodes = [] + if node_corsica is not None: + added_supernodes.append(node_corsica) + for lbl in labels.value_counts().loc[lambda s: s > 2].index: for b, buses, links in split_links(labels.index[labels == lbl]): if len(buses) <= 2: @@ -421,6 +424,9 @@ def split_links(nodes): logger.debug("Collecting all components using the busmap") + # Change carrier type of all added super_nodes to "AC" + n.buses.loc[added_supernodes, "carrier"] = "AC" + _aggregate_and_move_components( n, busmap, From 8480d62d33bef82aee9f8300d56c1c2427451bc8 Mon Sep 17 00:00:00 2001 From: Philipp Glaum <95913147+p-glaum@users.noreply.github.com> Date: Fri, 16 Aug 2024 13:16:52 +0200 Subject: [PATCH 207/344] fix bug in simplyfy network (#1222) --- scripts/simplify_network.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/simplify_network.py b/scripts/simplify_network.py index 66a5d1190..9db3435bf 100644 --- a/scripts/simplify_network.py +++ b/scripts/simplify_network.py @@ -301,7 +301,7 @@ def simplify_links( # Only span graph over the DC link components G = n.graph(branch_components=["Link"]) - def split_links(nodes, added_supernodes=None): + def split_links(nodes, added_supernodes): nodes = frozenset(nodes) seen = set() @@ -363,7 +363,9 @@ def split_links(nodes, added_supernodes=None): added_supernodes.append(node_corsica) for lbl in labels.value_counts().loc[lambda s: s > 2].index: - for b, buses, links in split_links(labels.index[labels == lbl]): + for b, buses, links in split_links( + labels.index[labels == lbl], added_supernodes + ): if len(buses) <= 2: continue From ebbb75491bbcd79876fc75d4312b9f4efb2f94ff Mon Sep 17 00:00:00 2001 From: Markus Millinger <50738187+millingermarkus@users.noreply.github.com> Date: Fri, 16 Aug 2024 13:18:10 +0200 Subject: [PATCH 208/344] Solid biomass to hydrogen process (#1213) * Added solid biomass to hydrogen, bioH2, process * Add doc for new config * add release notes * enable bioH2 also without biomass spatially resolved * Update doc/release_notes.rst Co-authored-by: Fabian Neumann * Added overnight cost --------- Co-authored-by: Fabian Neumann --- config/config.default.yaml | 2 ++ doc/configtables/sector.csv | 1 + doc/release_notes.rst | 3 +++ scripts/prepare_sector_network.py | 23 +++++++++++++++++++++++ 4 files changed, 29 insertions(+) diff --git a/config/config.default.yaml b/config/config.default.yaml index 14221dac6..654745dc9 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -654,6 +654,7 @@ sector: biomass_to_liquid: false electrobiofuels: false biosng: false + bioH2: false municipal_solid_waste: false limit_max_growth: enable: false @@ -1097,6 +1098,7 @@ plotting: unsustainable bioliquids: '#32CD32' electrobiofuels: 'red' BioSNG: '#123456' + solid biomass to hydrogen: '#654321' # power transmission lines: '#6c9459' transmission lines: '#6c9459' diff --git a/doc/configtables/sector.csv b/doc/configtables/sector.csv index e0deb8ca9..573805bb3 100644 --- a/doc/configtables/sector.csv +++ b/doc/configtables/sector.csv @@ -151,6 +151,7 @@ biogas_upgrading_cc,--,"{true, false}",Add option to capture CO2 from biomass up conventional_generation,,,Add a more detailed description of conventional carriers. Any power generation requires the consumption of fuel from nodes representing that fuel. biomass_to_liquid,--,"{true, false}",Add option for transforming solid biomass into liquid fuel with the same properties as oil biosng,--,"{true, false}",Add option for transforming solid biomass into synthesis gas with the same properties as natural gas +bioH2,--,"{true, false}",Add option for transforming solid biomass into hydrogen with carbon capture municipal_solid_waste,--,"{true, false}",Add option for municipal solid waste limit_max_growth,,, -- enable,--,"{true, false}",Add option to limit the maximum growth of a carrier diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 892558b19..6c9a289c8 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -9,6 +9,9 @@ Release Notes Upcoming Release ================ + +* Add option to produce hydrogen from solid biomass (flag ``solid biomass to hydrogen``), combined with carbon capture + * Fixed PDF encoding in ``build_biomass_transport_costs`` with update of tabula-py and jpype1 * More modular and flexible handling of transmission projects. One can now add new transmission projects in a subfolder of `data/transmission projects` similar to the files in the template folder. After adding the new files and updating the config section `transmission_projects:`, transmission projects will be included if they are not duplicates of existing lines or other projects. diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index c73373eee..4f3e3d2b3 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2830,6 +2830,29 @@ def add_biomass(n, costs): marginal_cost=costs.at["BioSNG", "VOM"] * costs.at["BioSNG", "efficiency"], ) + if options["bioH2"]: + name = (pd.Index(spatial.biomass.nodes) + " " + + pd.Index(spatial.h2.nodes.str.replace(" H2", ""))) + n.madd( + "Link", + name, + suffix=" solid biomass to hydrogen CC", + bus0=spatial.biomass.nodes, + bus1=spatial.h2.nodes, + bus2=spatial.co2.nodes, + bus3="co2 atmosphere", + carrier="solid biomass to hydrogen", + efficiency=costs.at['solid biomass to hydrogen', 'efficiency'], + efficiency2=costs.at['solid biomass', 'CO2 intensity'] * options["cc_fraction"], + efficiency3=-costs.at['solid biomass', 'CO2 intensity'] * options["cc_fraction"], + p_nom_extendable=True, + capital_cost=costs.at['solid biomass to hydrogen', 'fixed'] * costs.at['solid biomass to hydrogen', 'efficiency'] + + costs.at['biomass CHP capture', 'fixed'] * costs.at['solid biomass', 'CO2 intensity'], + overnight_cost=costs.at['solid biomass to hydrogen', 'investment'] * costs.at['solid biomass to hydrogen', 'efficiency'] + + costs.at['biomass CHP capture', 'investment'] * costs.at['solid biomass', 'CO2 intensity'], + marginal_cost=0., + ) + def add_industry(n, costs): logger.info("Add industrial demand") From eb16d74a47df49dc719e904634c35c50ebf12afb Mon Sep 17 00:00:00 2001 From: lisazeyen <35347358+lisazeyen@users.noreply.github.com> Date: Fri, 16 Aug 2024 13:30:01 +0200 Subject: [PATCH 209/344] increase min cluster to 38 because of corsica link --- config/config.default.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 654745dc9..cecd34e52 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -42,7 +42,7 @@ scenario: ll: - vopt clusters: - - 37 + - 38 - 128 - 256 opts: From 18759053f01476f6ab2cf624d13ce3d20b3b03c0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 16 Aug 2024 11:42:18 +0000 Subject: [PATCH 210/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/prepare_sector_network.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 238630671..71b504391 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2830,8 +2830,11 @@ def add_biomass(n, costs): ) if options["bioH2"]: - name = (pd.Index(spatial.biomass.nodes) + " " - + pd.Index(spatial.h2.nodes.str.replace(" H2", ""))) + name = ( + pd.Index(spatial.biomass.nodes) + + " " + + pd.Index(spatial.h2.nodes.str.replace(" H2", "")) + ) n.madd( "Link", name, @@ -2841,16 +2844,22 @@ def add_biomass(n, costs): bus2=spatial.co2.nodes, bus3="co2 atmosphere", carrier="solid biomass to hydrogen", - efficiency=costs.at['solid biomass to hydrogen', 'efficiency'], - efficiency2=costs.at['solid biomass', 'CO2 intensity'] * options["cc_fraction"], - efficiency3=-costs.at['solid biomass', 'CO2 intensity'] * options["cc_fraction"], + efficiency=costs.at["solid biomass to hydrogen", "efficiency"], + efficiency2=costs.at["solid biomass", "CO2 intensity"] + * options["cc_fraction"], + efficiency3=-costs.at["solid biomass", "CO2 intensity"] + * options["cc_fraction"], p_nom_extendable=True, - capital_cost=costs.at['solid biomass to hydrogen', 'fixed'] * costs.at['solid biomass to hydrogen', 'efficiency'] - + costs.at['biomass CHP capture', 'fixed'] * costs.at['solid biomass', 'CO2 intensity'], - overnight_cost=costs.at['solid biomass to hydrogen', 'investment'] * costs.at['solid biomass to hydrogen', 'efficiency'] - + costs.at['biomass CHP capture', 'investment'] * costs.at['solid biomass', 'CO2 intensity'], - marginal_cost=0., - ) + capital_cost=costs.at["solid biomass to hydrogen", "fixed"] + * costs.at["solid biomass to hydrogen", "efficiency"] + + costs.at["biomass CHP capture", "fixed"] + * costs.at["solid biomass", "CO2 intensity"], + overnight_cost=costs.at["solid biomass to hydrogen", "investment"] + * costs.at["solid biomass to hydrogen", "efficiency"] + + costs.at["biomass CHP capture", "investment"] + * costs.at["solid biomass", "CO2 intensity"], + marginal_cost=0.0, + ) def add_industry(n, costs): From ad100c494b4c562b79f65b75322d2eb34984d7aa Mon Sep 17 00:00:00 2001 From: lisazeyen <35347358+lisazeyen@users.noreply.github.com> Date: Fri, 16 Aug 2024 13:57:02 +0200 Subject: [PATCH 211/344] increase CH transport data until 2022 --- scripts/build_energy_totals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index 48004ea29..a4f22220e 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -1167,7 +1167,7 @@ def build_transport_data( if "CH" in countries: fn = snakemake.input.swiss_transport - swiss_cars = pd.read_csv(fn, index_col=0).loc[2000:2015, ["passenger cars"]] + swiss_cars = pd.read_csv(fn, index_col=0).loc[2000:2023, ["passenger cars"]] swiss_cars.index = pd.MultiIndex.from_product( [["CH"], swiss_cars.index], names=["country", "year"] From 885a881e7824f40b109faedfbf88b46dff9f462b Mon Sep 17 00:00:00 2001 From: lisazeyen <35347358+lisazeyen@users.noreply.github.com> Date: Fri, 16 Aug 2024 14:39:04 +0200 Subject: [PATCH 212/344] bug fix for reference year<2018 for ammonia --- scripts/build_industry_sector_ratios.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build_industry_sector_ratios.py b/scripts/build_industry_sector_ratios.py index 760dca765..530ac910f 100644 --- a/scripts/build_industry_sector_ratios.py +++ b/scripts/build_industry_sector_ratios.py @@ -445,7 +445,7 @@ def chemicals_industry(): # subtract ammonia energy demand (in ktNH3/a) ammonia = pd.read_csv(snakemake.input.ammonia_production, index_col=0) - ammonia_total = ammonia.loc[ammonia.index.intersection(eu27), str(year)].sum() + ammonia_total = ammonia.loc[ammonia.index.intersection(eu27), str(max(2018, year))].sum() df.loc["methane", sector] -= ammonia_total * params["MWh_CH4_per_tNH3_SMR"] df.loc["elec", sector] -= ammonia_total * params["MWh_elec_per_tNH3_SMR"] From ca47becc873431da30f4770d1cf3e0e330ad50f1 Mon Sep 17 00:00:00 2001 From: Fabian Date: Mon, 19 Aug 2024 13:33:28 +0200 Subject: [PATCH 213/344] build_transport_data: select swiss years according to data selection use index.unique instead of index.levels where needed --- scripts/base_network.py | 2 +- scripts/build_biomass_potentials.py | 2 +- scripts/build_energy_totals.py | 18 ++++++++---------- ..._industrial_energy_demand_per_node_today.py | 2 +- scripts/build_retro_cost.py | 2 +- scripts/make_summary_perfect.py | 12 ++++++------ 6 files changed, 18 insertions(+), 20 deletions(-) diff --git a/scripts/base_network.py b/scripts/base_network.py index 78cf6984f..2b0fee63b 100644 --- a/scripts/base_network.py +++ b/scripts/base_network.py @@ -95,7 +95,7 @@ def _get_oid(df): if "tags" in df.columns: - return df.tags.str.extract('"oid"=>"(\d+)"', expand=False) + return df.tags.str.extract(r'"oid"=>"(\d+)"', expand=False) else: return pd.Series(np.nan, df.index) diff --git a/scripts/build_biomass_potentials.py b/scripts/build_biomass_potentials.py index 0a2692e82..b69fcfbe9 100644 --- a/scripts/build_biomass_potentials.py +++ b/scripts/build_biomass_potentials.py @@ -311,7 +311,7 @@ def add_unsustainable_potentials(df): # Phase out unsustainable biomass potentials linearly from 2020 to 2035 while phasing in sustainable potentials share_unsus = params.get("share_unsustainable_use_retained").get(investment_year) - df_wo_ch = df.drop(df.filter(regex="CH\d", axis=0).index) + df_wo_ch = df.drop(df.filter(regex=r"CH\d", axis=0).index) # Calculate unsustainable solid biomass df_wo_ch["unsustainable solid biomass"] = _calc_unsustainable_potential( diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index a4f22220e..5aefb26b6 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -242,7 +242,7 @@ def build_eurostat( temp.index = pd.MultiIndex.from_frame( temp.index.to_frame().fillna("International aviation") ) - df = pd.concat([temp, df.loc[~int_avia]]) + df = pd.concat([temp, df.loc[~int_avia]]).sort_index() # Fill in missing data on "Domestic aviation" for each country. for country in countries: @@ -651,8 +651,8 @@ def build_energy_totals( """ eurostat_fuels = {"electricity": "Electricity", "total": "Total all products"} - eurostat_countries = eurostat.index.levels[0] - eurostat_years = eurostat.index.levels[1] + eurostat_countries = eurostat.index.unique(0) + eurostat_years = eurostat.index.unique(1) to_drop = ["passenger cars", "passenger car efficiency"] new_index = pd.MultiIndex.from_product( @@ -1153,13 +1153,14 @@ def build_transport_data( ---------- - Swiss transport data: `BFS `_ """ + years = np.arange(2000, 2022) # first collect number of cars transport_data = pd.DataFrame(idees["passenger cars"]) countries_without_ch = set(countries) - {"CH"} new_index = pd.MultiIndex.from_product( - [countries_without_ch, transport_data.index.levels[1]], + [countries_without_ch, transport_data.index.unique(1)], names=["country", "year"], ) @@ -1167,7 +1168,7 @@ def build_transport_data( if "CH" in countries: fn = snakemake.input.swiss_transport - swiss_cars = pd.read_csv(fn, index_col=0).loc[2000:2023, ["passenger cars"]] + swiss_cars = pd.read_csv(fn, index_col=0).loc[years, ["passenger cars"]] swiss_cars.index = pd.MultiIndex.from_product( [["CH"], swiss_cars.index], names=["country", "year"] @@ -1175,10 +1176,8 @@ def build_transport_data( transport_data = pd.concat([transport_data, swiss_cars]).sort_index() - transport_data.rename(columns={"passenger cars": "number cars"}, inplace=True) - + transport_data = transport_data.rename(columns={"passenger cars": "number cars"}) # clean up dataframe - years = np.arange(2000, 2022) transport_data = transport_data[ transport_data.index.get_level_values(1).isin(years) ] @@ -1188,11 +1187,10 @@ def build_transport_data( logger.info( f"Missing data on cars from:\n{list(missing)}\nFilling gaps with averaged data." ) - cars_pp = transport_data["number cars"] / population fill_values = { - year: cars_pp.mean() * population for year in transport_data.index.levels[1] + year: cars_pp.mean() * population for year in transport_data.index.unique(1) } fill_values = pd.DataFrame(fill_values).stack() fill_values = pd.DataFrame(fill_values, columns=["number cars"]) diff --git a/scripts/build_industrial_energy_demand_per_node_today.py b/scripts/build_industrial_energy_demand_per_node_today.py index 7a1ee7ac5..aa9e9dffa 100644 --- a/scripts/build_industrial_energy_demand_per_node_today.py +++ b/scripts/build_industrial_energy_demand_per_node_today.py @@ -66,7 +66,7 @@ def build_nodal_industrial_energy_demand(): ) countries = keys.country.unique() - sectors = industrial_demand.columns.levels[1] + sectors = industrial_demand.columns.unique(1) for country, sector in product(countries, sectors): buses = keys.index[keys.country == country] diff --git a/scripts/build_retro_cost.py b/scripts/build_retro_cost.py index 44f4a7384..c6dd31cf9 100755 --- a/scripts/build_retro_cost.py +++ b/scripts/build_retro_cost.py @@ -254,7 +254,7 @@ def prepare_building_stock_data(): index = pd.MultiIndex.from_product([[ct], averaged_data.index.to_list()]) averaged_data.index = index averaged_data["estimated"] = 1 - if ct not in area_tot.index.levels[0]: + if ct not in area_tot.index.unique(0): area_tot = pd.concat([area_tot, averaged_data], sort=True) else: area_tot.loc[averaged_data.index] = averaged_data diff --git a/scripts/make_summary_perfect.py b/scripts/make_summary_perfect.py index 8e56e5d4d..59aa6e741 100644 --- a/scripts/make_summary_perfect.py +++ b/scripts/make_summary_perfect.py @@ -40,9 +40,9 @@ def calculate_costs(n, label, costs): investments = n.investment_periods cols = pd.MultiIndex.from_product( [ - costs.columns.levels[0], - costs.columns.levels[1], - costs.columns.levels[2], + costs.columns.unique(0), + costs.columns.unique(1), + costs.columns.unique(2), investments, ], names=costs.columns.names[:3] + ["year"], @@ -339,9 +339,9 @@ def calculate_supply_energy(n, label, supply_energy): investments = n.investment_periods cols = pd.MultiIndex.from_product( [ - supply_energy.columns.levels[0], - supply_energy.columns.levels[1], - supply_energy.columns.levels[2], + supply_energy.columns.unique(0), + supply_energy.columns.unique(1), + supply_energy.columns.unique(2), investments, ], names=supply_energy.columns.names[:3] + ["year"], From 54a2d24517c140fdd9026d4b1e882865f1302d2e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Aug 2024 11:39:34 +0000 Subject: [PATCH 214/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/build_industry_sector_ratios.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/build_industry_sector_ratios.py b/scripts/build_industry_sector_ratios.py index 530ac910f..c2438f915 100644 --- a/scripts/build_industry_sector_ratios.py +++ b/scripts/build_industry_sector_ratios.py @@ -445,7 +445,9 @@ def chemicals_industry(): # subtract ammonia energy demand (in ktNH3/a) ammonia = pd.read_csv(snakemake.input.ammonia_production, index_col=0) - ammonia_total = ammonia.loc[ammonia.index.intersection(eu27), str(max(2018, year))].sum() + ammonia_total = ammonia.loc[ + ammonia.index.intersection(eu27), str(max(2018, year)) + ].sum() df.loc["methane", sector] -= ammonia_total * params["MWh_CH4_per_tNH3_SMR"] df.loc["elec", sector] -= ammonia_total * params["MWh_elec_per_tNH3_SMR"] From 50c2fee6ce9eb320f31196354579295c2d794e8d Mon Sep 17 00:00:00 2001 From: lisazeyen <35347358+lisazeyen@users.noreply.github.com> Date: Tue, 20 Aug 2024 10:14:59 +0200 Subject: [PATCH 215/344] avoid infinity in sector rations --- scripts/build_industry_sector_ratios_intermediate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/build_industry_sector_ratios_intermediate.py b/scripts/build_industry_sector_ratios_intermediate.py index 42e8a7dc9..d4d46c399 100644 --- a/scripts/build_industry_sector_ratios_intermediate.py +++ b/scripts/build_industry_sector_ratios_intermediate.py @@ -80,6 +80,7 @@ """ import pandas as pd +import numpy as np from prepare_sector_network import get @@ -104,7 +105,7 @@ def build_industry_sector_ratios_intermediate(): snakemake.input.industry_sector_ratios, index_col=0 ) - today_sector_ratios = demand.div(production, axis=1) + today_sector_ratios = demand.div(production, axis=1).replace([np.inf, -np.inf], 0) today_sector_ratios.dropna(how="all", axis=1, inplace=True) From 8b0474e41cb625442a119e0fd01b651e9524cf0e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 20 Aug 2024 08:15:58 +0000 Subject: [PATCH 216/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/build_industry_sector_ratios_intermediate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build_industry_sector_ratios_intermediate.py b/scripts/build_industry_sector_ratios_intermediate.py index d4d46c399..0fbaa567f 100644 --- a/scripts/build_industry_sector_ratios_intermediate.py +++ b/scripts/build_industry_sector_ratios_intermediate.py @@ -79,8 +79,8 @@ Unit of the output file is MWh/t. """ -import pandas as pd import numpy as np +import pandas as pd from prepare_sector_network import get From f4819d0b4c605ed59363263835ed88f36e144d6d Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Tue, 20 Aug 2024 13:51:33 +0200 Subject: [PATCH 217/344] co2 sequestration depending on period --- scripts/solve_network.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/scripts/solve_network.py b/scripts/solve_network.py index a2391ea2b..5f644bc9b 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -43,6 +43,8 @@ set_scenario_config, update_config_from_wildcards, ) + +from prepare_sector_network import get from pypsa.descriptors import get_activity_mask from pypsa.descriptors import get_switchable_as_dense as get_as_dense @@ -287,15 +289,19 @@ def add_solar_potential_constraints(n, config): n.model.add_constraints(lhs <= rhs, name="solar_potential") -def add_co2_sequestration_limit(n, limit=200): +def add_co2_sequestration_limit(n, limit_dict): """ Add a global constraint on the amount of Mt CO2 that can be sequestered. """ if not n.investment_periods.empty: periods = n.investment_periods - names = pd.Index([f"co2_sequestration_limit-{period}" for period in periods]) + limit = pd.Series({f"co2_sequestration_limit-{period}": + limit_dict.get(period, 200) for period in periods}) + names = limit.index else: + limit = get(limit_dict, + int(snakemake.wildcards.planning_horizons)) periods = [np.nan] names = pd.Index(["co2_sequestration_limit"]) @@ -515,8 +521,8 @@ def prepare_network( n = add_max_growth(n) if n.stores.carrier.eq("co2 sequestered").any(): - limit = co2_sequestration_potential - add_co2_sequestration_limit(n, limit=limit) + limit_dict = co2_sequestration_potential + add_co2_sequestration_limit(n, limit_dict=limit_dict) return n @@ -1116,19 +1122,19 @@ def solve_network(n, config, solving, **kwargs): return n - +#%% if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake snakemake = mock_snakemake( - "solve_sector_network", - configfiles="../config/test/config.perfect.yaml", + "solve_sector_network_myopic", + # configfiles="../config/test/config.perfect.yaml", simpl="", opts="", - clusters="37", - ll="v1.0", - sector_opts="CO2L0-1H-T-H-B-I-A-dist1", + clusters="38", + ll="vopt", + sector_opts="", planning_horizons="2030", ) configure_logging(snakemake) @@ -1140,7 +1146,7 @@ def solve_network(n, config, solving, **kwargs): np.random.seed(solve_opts.get("seed", 123)) n = pypsa.Network(snakemake.input.network) - + n = prepare_network( n, solve_opts, From dcc84dfb9f623376dfeafe518fdf8dd43770f031 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Tue, 20 Aug 2024 14:02:12 +0200 Subject: [PATCH 218/344] update config --- config/config.default.yaml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 3d1292e3c..74e672e29 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -612,7 +612,14 @@ sector: min_size: 3 max_size: 25 years_of_storage: 25 - co2_sequestration_potential: 200 + co2_sequestration_potential: + 2020: 0 + 2025: 0 + 2030: 50 + 2035: 100 + 2040: 200 + 2045: 200 + 2050: 200 co2_sequestration_cost: 10 co2_sequestration_lifetime: 50 co2_spatial: false From 2682c313ba5d691cb1c232502b58e1fe4faca11c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 20 Aug 2024 12:04:44 +0000 Subject: [PATCH 219/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/solve_network.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/scripts/solve_network.py b/scripts/solve_network.py index 5f644bc9b..b7d5665b8 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -43,7 +43,6 @@ set_scenario_config, update_config_from_wildcards, ) - from prepare_sector_network import get from pypsa.descriptors import get_activity_mask from pypsa.descriptors import get_switchable_as_dense as get_as_dense @@ -296,12 +295,15 @@ def add_co2_sequestration_limit(n, limit_dict): if not n.investment_periods.empty: periods = n.investment_periods - limit = pd.Series({f"co2_sequestration_limit-{period}": - limit_dict.get(period, 200) for period in periods}) + limit = pd.Series( + { + f"co2_sequestration_limit-{period}": limit_dict.get(period, 200) + for period in periods + } + ) names = limit.index else: - limit = get(limit_dict, - int(snakemake.wildcards.planning_horizons)) + limit = get(limit_dict, int(snakemake.wildcards.planning_horizons)) periods = [np.nan] names = pd.Index(["co2_sequestration_limit"]) @@ -1122,7 +1124,8 @@ def solve_network(n, config, solving, **kwargs): return n -#%% + +# %% if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake @@ -1146,7 +1149,7 @@ def solve_network(n, config, solving, **kwargs): np.random.seed(solve_opts.get("seed", 123)) n = pypsa.Network(snakemake.input.network) - + n = prepare_network( n, solve_opts, From 5fb89068fda761567710fe89a3c41321d02c92e5 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Tue, 20 Aug 2024 15:05:30 +0200 Subject: [PATCH 220/344] right intend for co2 seq potential --- config/config.default.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 74e672e29..9d565b633 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -612,14 +612,14 @@ sector: min_size: 3 max_size: 25 years_of_storage: 25 - co2_sequestration_potential: - 2020: 0 - 2025: 0 - 2030: 50 - 2035: 100 - 2040: 200 - 2045: 200 - 2050: 200 + co2_sequestration_potential: + 2020: 0 + 2025: 0 + 2030: 50 + 2035: 100 + 2040: 200 + 2045: 200 + 2050: 200 co2_sequestration_cost: 10 co2_sequestration_lifetime: 50 co2_spatial: false From 7490647084f35127d43e253b7a6586fd1ccb2ea5 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Tue, 20 Aug 2024 15:05:45 +0200 Subject: [PATCH 221/344] update release notes and doc --- doc/configtables/sector.csv | 350 ++++++++++++++++++------------------ doc/release_notes.rst | 1 + scripts/solve_network.py | 10 +- 3 files changed, 181 insertions(+), 180 deletions(-) diff --git a/doc/configtables/sector.csv b/doc/configtables/sector.csv index 88f56abe2..88442a9ee 100644 --- a/doc/configtables/sector.csv +++ b/doc/configtables/sector.csv @@ -1,175 +1,175 @@ -,Unit,Values,Description -transport,--,"{true, false}",Flag to include transport sector. -heating,--,"{true, false}",Flag to include heating sector. -biomass,--,"{true, false}",Flag to include biomass sector. -industry,--,"{true, false}",Flag to include industry sector. -agriculture,--,"{true, false}",Flag to include agriculture sector. -fossil_fuels,--,"{true, false}","Flag to include imports of fossil fuels ( [""coal"", ""gas"", ""oil"", ""lignite""])" -district_heating,--,,`prepare_sector_network.py `_ --- potential,--,float,maximum fraction of urban demand which can be supplied by district heating --- progress,--,Dictionary with planning horizons as keys., Increase of today's district heating demand to potential maximum district heating share. Progress = 0 means today's district heating share. Progress = 1 means maximum fraction of urban demand is supplied by district heating --- district_heating_loss,--,float,Share increase in district heat demand in urban central due to heat losses --- forward_temperature,°C,Dictionary with country codes as keys. One key must be 'default'.,Forward temperature in district heating --- return_temperature,°C,Dictionary with country codes as keys. One key must be 'default'.,Return temperature in district heating. Must be lower than forward temperature --- heat_source_cooling,K,float,Cooling of heat source for heat pumps --- heat_pump_cop_approximation,,, --- -- refrigerant,--,"{ammonia, isobutane}",Heat pump refrigerant assumed for COP approximation --- -- heat_exchanger_pinch_point_temperature_difference,K,float,Heat pump pinch point temperature difference in heat exchangers assumed for approximation. --- -- isentropic_compressor_efficiency,--,float,Isentropic efficiency of heat pump compressor assumed for approximation. Must be between 0 and 1. --- -- heat_loss,--,float,Heat pump heat loss assumed for approximation. Must be between 0 and 1. --- heat_pump_sources,--,, --- -- urban central,--,List of heat sources for heat pumps in urban central heating, --- -- urban decentral,--,List of heat sources for heat pumps in urban decentral heating, --- -- rural,--,List of heat sources for heat pumps in rural heating, -cluster_heat_buses,--,"{true, false}",Cluster residential and service heat buses in `prepare_sector_network.py `_ to one to save memory. -,,, -bev_dsm_restriction _value,--,float,Adds a lower state of charge (SOC) limit for battery electric vehicles (BEV) to manage its own energy demand (DSM). Located in `build_transport_demand.py `_. Set to 0 for no restriction on BEV DSM -bev_dsm_restriction _time,--,float,Time at which SOC of BEV has to be dsm_restriction_value -transport_heating _deadband_upper,°C,float,"The maximum temperature in the vehicle. At higher temperatures, the energy required for cooling in the vehicle increases." -transport_heating _deadband_lower,°C,float,"The minimum temperature in the vehicle. At lower temperatures, the energy required for heating in the vehicle increases." -,,, -ICE_lower_degree_factor,--,float,Share increase in energy demand in internal combustion engine (ICE) for each degree difference between the cold environment and the minimum temperature. -ICE_upper_degree_factor,--,float,Share increase in energy demand in internal combustion engine (ICE) for each degree difference between the hot environment and the maximum temperature. -EV_lower_degree_factor,--,float,Share increase in energy demand in electric vehicles (EV) for each degree difference between the cold environment and the minimum temperature. -EV_upper_degree_factor,--,float,Share increase in energy demand in electric vehicles (EV) for each degree difference between the hot environment and the maximum temperature. -bev_dsm,--,"{true, false}",Add the option for battery electric vehicles (BEV) to participate in demand-side management (DSM) -,,, -bev_availability,--,float,The share for battery electric vehicles (BEV) that are able to do demand side management (DSM) -bev_energy,--,float,The average size of battery electric vehicles (BEV) in MWh -bev_charge_efficiency,--,float,Battery electric vehicles (BEV) charge and discharge efficiency -bev_charge_rate,MWh,float,The power consumption for one electric vehicle (EV) in MWh. Value derived from 3-phase charger with 11 kW. -bev_avail_max,--,float,The maximum share plugged-in availability for passenger electric vehicles. -bev_avail_mean,--,float,The average share plugged-in availability for passenger electric vehicles. -v2g,--,"{true, false}",Allows feed-in to grid from EV battery -land_transport_fuel_cell _share,--,Dictionary with planning horizons as keys.,The share of vehicles that uses fuel cells in a given year -land_transport_electric _share,--,Dictionary with planning horizons as keys.,The share of vehicles that uses electric vehicles (EV) in a given year -land_transport_ice _share,--,Dictionary with planning horizons as keys.,The share of vehicles that uses internal combustion engines (ICE) in a given year. What is not EV or FCEV is oil-fuelled ICE. -transport_electric_efficiency,MWh/100km,float,The conversion efficiencies of electric vehicles in transport -transport_fuel_cell_efficiency,MWh/100km,float,The H2 conversion efficiencies of fuel cells in transport -transport_ice_efficiency,MWh/100km,float,The oil conversion efficiencies of internal combustion engine (ICE) in transport -agriculture_machinery _electric_share,--,float,The share for agricultural machinery that uses electricity -agriculture_machinery _oil_share,--,float,The share for agricultural machinery that uses oil -agriculture_machinery _fuel_efficiency,--,float,The efficiency of electric-powered machinery in the conversion of electricity to meet agricultural needs. -agriculture_machinery _electric_efficiency,--,float,The efficiency of oil-powered machinery in the conversion of oil to meet agricultural needs. -Mwh_MeOH_per_MWh_H2,LHV,float,"The energy amount of the produced methanol per energy amount of hydrogen. From `DECHEMA (2017) `_, page 64." -MWh_MeOH_per_tCO2,LHV,float,"The energy amount of the produced methanol per ton of CO2. From `DECHEMA (2017) `_, page 66." -MWh_MeOH_per_MWh_e,LHV,float,"The energy amount of the produced methanol per energy amount of electricity. From `DECHEMA (2017) `_, page 64." -shipping_hydrogen _liquefaction,--,"{true, false}",Whether to include liquefaction costs for hydrogen demand in shipping. -,,, -shipping_hydrogen_share,--,Dictionary with planning horizons as keys.,The share of ships powered by hydrogen in a given year -shipping_methanol_share,--,Dictionary with planning horizons as keys.,The share of ships powered by methanol in a given year -shipping_oil_share,--,Dictionary with planning horizons as keys.,The share of ships powered by oil in a given year -shipping_methanol _efficiency,--,float,The efficiency of methanol-powered ships in the conversion of methanol to meet shipping needs (propulsion). The efficiency increase from oil can be 10-15% higher according to the `IEA `_ -,,, -shipping_oil_efficiency,--,float,The efficiency of oil-powered ships in the conversion of oil to meet shipping needs (propulsion). Base value derived from 2011 -aviation_demand_factor,--,float,The proportion of demand for aviation compared to today's consumption -HVC_demand_factor,--,float,The proportion of demand for high-value chemicals compared to today's consumption -,,, -time_dep_hp_cop,--,"{true, false}",Consider the time dependent coefficient of performance (COP) of the heat pump -heat_pump_sink_T,°C,float,The temperature heat sink used in heat pumps based on DTU / large area radiators. The value is conservatively high to cover hot water and space heating in poorly-insulated buildings -reduce_space_heat _exogenously,--,"{true, false}",Influence on space heating demand by a certain factor (applied before losses in district heating). -reduce_space_heat _exogenously_factor,--,Dictionary with planning horizons as keys.,"A positive factor can mean renovation or demolition of a building. If the factor is negative, it can mean an increase in floor area, increased thermal comfort, population growth. The default factors are determined by the `Eurocalc Homes and buildings decarbonization scenario `_" -retrofitting,,, --- retro_endogen,--,"{true, false}",Add retrofitting as an endogenous system which co-optimise space heat savings. --- cost_factor,--,float,Weight costs for building renovation --- interest_rate,--,float,The interest rate for investment in building components --- annualise_cost,--,"{true, false}",Annualise the investment costs of retrofitting --- tax_weighting,--,"{true, false}",Weight the costs of retrofitting depending on taxes in countries --- construction_index,--,"{true, false}",Weight the costs of retrofitting depending on labour/material costs per country -tes,--,"{true, false}",Add option for storing thermal energy in large water pits associated with district heating systems and individual thermal energy storage (TES) -tes_tau,,,The time constant used to calculate the decay of thermal energy in thermal energy storage (TES): 1- :math:`e^{-1/24τ}`. --- decentral,days,float,The time constant in decentralized thermal energy storage (TES) --- central,days,float,The time constant in centralized thermal energy storage (TES) -boilers,--,"{true, false}",Add option for transforming gas into heat using gas boilers -resistive_heaters,--,"{true, false}",Add option for transforming electricity into heat using resistive heaters (independently from gas boilers) -oil_boilers,--,"{true, false}",Add option for transforming oil into heat using boilers -biomass_boiler,--,"{true, false}",Add option for transforming biomass into heat using boilers -overdimension_individual_heating,--,float,Add option for overdimensioning individual heating systems by a certain factor. This allows them to cover heat demand peaks e.g. 10% higher than those in the data with a setting of 1.1. -chp,--,"{true, false}",Add option for using Combined Heat and Power (CHP) -micro_chp,--,"{true, false}",Add option for using Combined Heat and Power (CHP) for decentral areas. -solar_thermal,--,"{true, false}",Add option for using solar thermal to generate heat. -solar_cf_correction,--,float,The correction factor for the value provided by the solar thermal profile calculations -marginal_cost_storage,currency/MWh ,float,The marginal cost of discharging batteries in distributed grids -methanation,--,"{true, false}",Add option for transforming hydrogen and CO2 into methane using methanation. -coal_cc,--,"{true, false}",Add option for coal CHPs with carbon capture -dac,--,"{true, false}",Add option for Direct Air Capture (DAC) -co2_vent,--,"{true, false}",Add option for vent out CO2 from storages to the atmosphere. -allam_cycle,--,"{true, false}",Add option to include `Allam cycle gas power plants `_ -hydrogen_fuel_cell,--,"{true, false}",Add option to include hydrogen fuel cell for re-electrification. Assuming OCGT technology costs -hydrogen_turbine,--,"{true, false}",Add option to include hydrogen turbine for re-electrification. Assuming OCGT technology costs -SMR,--,"{true, false}",Add option for transforming natural gas into hydrogen and CO2 using Steam Methane Reforming (SMR) -SMR CC,--,"{true, false}",Add option for transforming natural gas into hydrogen and CO2 using Steam Methane Reforming (SMR) and Carbon Capture (CC) -regional_methanol_demand,--,"{true, false}",Spatially resolve methanol demand. Set to true if regional CO2 constraints needed. -regional_oil_demand,--,"{true, false}",Spatially resolve oil demand. Set to true if regional CO2 constraints needed. -regional_co2 _sequestration_potential,,, --- enable,--,"{true, false}",Add option for regionally-resolved geological carbon dioxide sequestration potentials based on `CO2StoP `_. --- attribute,--,string or list,Name (or list of names) of the attribute(s) for the sequestration potential --- include_onshore,--,"{true, false}",Add options for including onshore sequestration potentials --- min_size,Gt ,float,Any sites with lower potential than this value will be excluded --- max_size,Gt ,float,The maximum sequestration potential for any one site. --- years_of_storage,years,float,The years until potential exhausted at optimised annual rate -co2_sequestration_potential,MtCO2/a,float,The potential of sequestering CO2 in Europe per year -co2_sequestration_cost,currency/tCO2,float,The cost of sequestering a ton of CO2 -co2_sequestration_lifetime,years,int,The lifetime of a CO2 sequestration site -co2_spatial,--,"{true, false}","Add option to spatially resolve carrier representing stored carbon dioxide. This allows for more detailed modelling of CCUTS, e.g. regarding the capturing of industrial process emissions, usage as feedstock for electrofuels, transport of carbon dioxide, and geological sequestration sites." -,,, -co2network,--,"{true, false}",Add option for planning a new carbon dioxide transmission network -co2_network_cost_factor,p.u.,float,The cost factor for the capital cost of the carbon dioxide transmission network -,,, -cc_fraction,--,float,The default fraction of CO2 captured with post-combustion capture -hydrogen_underground _storage,--,"{true, false}",Add options for storing hydrogen underground. Storage potential depends regionally. -hydrogen_underground _storage_locations,,"{onshore, nearshore, offshore}","The location where hydrogen underground storage can be located. Onshore, nearshore, offshore means it must be located more than 50 km away from the sea, within 50 km of the sea, or within the sea itself respectively." -,,, -ammonia,--,"{true, false, regional}","Add ammonia as a carrrier. It can be either true (copperplated NH3), false (no NH3 carrier) or ""regional"" (regionalised NH3 without network)" -min_part_load_fischer _tropsch,per unit of p_nom ,float,The minimum unit dispatch (``p_min_pu``) for the Fischer-Tropsch process -min_part_load _methanolisation,per unit of p_nom ,float,The minimum unit dispatch (``p_min_pu``) for the methanolisation process -,,, -use_fischer_tropsch _waste_heat,--,"{true, false}",Add option for using waste heat of Fischer Tropsch in district heating networks -use_fuel_cell_waste_heat,--,"{true, false}",Add option for using waste heat of fuel cells in district heating networks -use_electrolysis_waste _heat,--,"{true, false}",Add option for using waste heat of electrolysis in district heating networks -electricity_transmission _grid,--,"{true, false}",Switch for enabling/disabling the electricity transmission grid. -electricity_distribution _grid,--,"{true, false}",Add a simplified representation of the exchange capacity between transmission and distribution grid level through a link. -electricity_distribution _grid_cost_factor,,,Multiplies the investment cost of the electricity distribution grid -,,, -electricity_grid _connection,--,"{true, false}",Add the cost of electricity grid connection for onshore wind and solar -transmission_efficiency,,,Section to specify transmission losses or compression energy demands of bidirectional links. Splits them into two capacity-linked unidirectional links. --- {carrier},--,str,The carrier of the link. --- -- efficiency_static,p.u.,float,Length-independent transmission efficiency. --- -- efficiency_per_1000km,p.u. per 1000 km,float,Length-dependent transmission efficiency ($\eta^{\text{length}}$) --- -- compression_per_1000km,p.u. per 1000 km,float,Length-dependent electricity demand for compression ($\eta \cdot \text{length}$) implemented as multi-link to local electricity bus. -H2_network,--,"{true, false}",Add option for new hydrogen pipelines -gas_network,--,"{true, false}","Add existing natural gas infrastructure, incl. LNG terminals, production and entry-points. The existing gas network is added with a lossless transport model. A length-weighted `k-edge augmentation algorithm `_ can be run to add new candidate gas pipelines such that all regions of the model can be connected to the gas network. When activated, all the gas demands are regionally disaggregated as well." -H2_retrofit,--,"{true, false}",Add option for retrofiting existing pipelines to transport hydrogen. -H2_retrofit_capacity _per_CH4,--,float,"The ratio for H2 capacity per original CH4 capacity of retrofitted pipelines. The `European Hydrogen Backbone (April, 2020) p.15 `_ 60% of original natural gas capacity could be used in cost-optimal case as H2 capacity." -gas_network_connectivity _upgrade ,--,float,The number of desired edge connectivity (k) in the length-weighted `k-edge augmentation algorithm `_ used for the gas network -gas_distribution_grid,--,"{true, false}",Add a gas distribution grid -gas_distribution_grid _cost_factor,,,Multiplier for the investment cost of the gas distribution grid -,,, -biomass_spatial,--,"{true, false}",Add option for resolving biomass demand regionally -biomass_transport,--,"{true, false}",Add option for transporting solid biomass between nodes -biogas_upgrading_cc,--,"{true, false}",Add option to capture CO2 from biomass upgrading -conventional_generation,,,Add a more detailed description of conventional carriers. Any power generation requires the consumption of fuel from nodes representing that fuel. -biomass_to_liquid,--,"{true, false}",Add option for transforming solid biomass into liquid fuel with the same properties as oil -biosng,--,"{true, false}",Add option for transforming solid biomass into synthesis gas with the same properties as natural gas -bioH2,--,"{true, false}",Add option for transforming solid biomass into hydrogen with carbon capture -municipal_solid_waste,--,"{true, false}",Add option for municipal solid waste -limit_max_growth,,, --- enable,--,"{true, false}",Add option to limit the maximum growth of a carrier --- factor,p.u.,float,The maximum growth factor of a carrier (e.g. 1.3 allows 30% larger than max historic growth) --- max_growth,,, --- -- {carrier},GW,float,The historic maximum growth of a carrier --- max_relative_growth,,, --- -- {carrier},p.u.,float,The historic maximum relative growth of a carrier -,,, -enhanced_geothermal,,, --- enable,--,"{true, false}",Add option to include Enhanced Geothermal Systems --- flexible,--,"{true, false}",Add option for flexible operation (see Ricks et al. 2024) --- max_hours,--,int,The maximum hours the reservoir can be charged under flexible operation --- max_boost,--,float,The maximum boost in power output under flexible operation --- var_cf,--,"{true, false}",Add option for variable capacity factor (see Ricks et al. 2024) --- sustainability_factor,--,float,Share of sourced heat that is replenished by the earth's core (see details in `build_egs_potentials.py `_) -solid_biomass_import,,, --- enable,--,"{true, false}",Add option to include solid biomass imports --- price,currency/MWh,float,Price for importing solid biomass --- max_amount,Twh,float,Maximum solid biomass import potential --- upstream_emissions_factor,p.u.,float,Upstream emissions of solid biomass imports +,Unit,Values,Description +transport,--,"{true, false}",Flag to include transport sector. +heating,--,"{true, false}",Flag to include heating sector. +biomass,--,"{true, false}",Flag to include biomass sector. +industry,--,"{true, false}",Flag to include industry sector. +agriculture,--,"{true, false}",Flag to include agriculture sector. +fossil_fuels,--,"{true, false}","Flag to include imports of fossil fuels ( [""coal"", ""gas"", ""oil"", ""lignite""])" +district_heating,--,,`prepare_sector_network.py `_ +-- potential,--,float,maximum fraction of urban demand which can be supplied by district heating +-- progress,--,Dictionary with planning horizons as keys., Increase of today's district heating demand to potential maximum district heating share. Progress = 0 means today's district heating share. Progress = 1 means maximum fraction of urban demand is supplied by district heating +-- district_heating_loss,--,float,Share increase in district heat demand in urban central due to heat losses +-- forward_temperature,°C,Dictionary with country codes as keys. One key must be 'default'.,Forward temperature in district heating +-- return_temperature,°C,Dictionary with country codes as keys. One key must be 'default'.,Return temperature in district heating. Must be lower than forward temperature +-- heat_source_cooling,K,float,Cooling of heat source for heat pumps +-- heat_pump_cop_approximation,,, +-- -- refrigerant,--,"{ammonia, isobutane}",Heat pump refrigerant assumed for COP approximation +-- -- heat_exchanger_pinch_point_temperature_difference,K,float,Heat pump pinch point temperature difference in heat exchangers assumed for approximation. +-- -- isentropic_compressor_efficiency,--,float,Isentropic efficiency of heat pump compressor assumed for approximation. Must be between 0 and 1. +-- -- heat_loss,--,float,Heat pump heat loss assumed for approximation. Must be between 0 and 1. +-- heat_pump_sources,--,, +-- -- urban central,--,List of heat sources for heat pumps in urban central heating, +-- -- urban decentral,--,List of heat sources for heat pumps in urban decentral heating, +-- -- rural,--,List of heat sources for heat pumps in rural heating, +cluster_heat_buses,--,"{true, false}",Cluster residential and service heat buses in `prepare_sector_network.py `_ to one to save memory. +,,, +bev_dsm_restriction _value,--,float,Adds a lower state of charge (SOC) limit for battery electric vehicles (BEV) to manage its own energy demand (DSM). Located in `build_transport_demand.py `_. Set to 0 for no restriction on BEV DSM +bev_dsm_restriction _time,--,float,Time at which SOC of BEV has to be dsm_restriction_value +transport_heating _deadband_upper,°C,float,"The maximum temperature in the vehicle. At higher temperatures, the energy required for cooling in the vehicle increases." +transport_heating _deadband_lower,°C,float,"The minimum temperature in the vehicle. At lower temperatures, the energy required for heating in the vehicle increases." +,,, +ICE_lower_degree_factor,--,float,Share increase in energy demand in internal combustion engine (ICE) for each degree difference between the cold environment and the minimum temperature. +ICE_upper_degree_factor,--,float,Share increase in energy demand in internal combustion engine (ICE) for each degree difference between the hot environment and the maximum temperature. +EV_lower_degree_factor,--,float,Share increase in energy demand in electric vehicles (EV) for each degree difference between the cold environment and the minimum temperature. +EV_upper_degree_factor,--,float,Share increase in energy demand in electric vehicles (EV) for each degree difference between the hot environment and the maximum temperature. +bev_dsm,--,"{true, false}",Add the option for battery electric vehicles (BEV) to participate in demand-side management (DSM) +,,, +bev_availability,--,float,The share for battery electric vehicles (BEV) that are able to do demand side management (DSM) +bev_energy,--,float,The average size of battery electric vehicles (BEV) in MWh +bev_charge_efficiency,--,float,Battery electric vehicles (BEV) charge and discharge efficiency +bev_charge_rate,MWh,float,The power consumption for one electric vehicle (EV) in MWh. Value derived from 3-phase charger with 11 kW. +bev_avail_max,--,float,The maximum share plugged-in availability for passenger electric vehicles. +bev_avail_mean,--,float,The average share plugged-in availability for passenger electric vehicles. +v2g,--,"{true, false}",Allows feed-in to grid from EV battery +land_transport_fuel_cell _share,--,Dictionary with planning horizons as keys.,The share of vehicles that uses fuel cells in a given year +land_transport_electric _share,--,Dictionary with planning horizons as keys.,The share of vehicles that uses electric vehicles (EV) in a given year +land_transport_ice _share,--,Dictionary with planning horizons as keys.,The share of vehicles that uses internal combustion engines (ICE) in a given year. What is not EV or FCEV is oil-fuelled ICE. +transport_electric_efficiency,MWh/100km,float,The conversion efficiencies of electric vehicles in transport +transport_fuel_cell_efficiency,MWh/100km,float,The H2 conversion efficiencies of fuel cells in transport +transport_ice_efficiency,MWh/100km,float,The oil conversion efficiencies of internal combustion engine (ICE) in transport +agriculture_machinery _electric_share,--,float,The share for agricultural machinery that uses electricity +agriculture_machinery _oil_share,--,float,The share for agricultural machinery that uses oil +agriculture_machinery _fuel_efficiency,--,float,The efficiency of electric-powered machinery in the conversion of electricity to meet agricultural needs. +agriculture_machinery _electric_efficiency,--,float,The efficiency of oil-powered machinery in the conversion of oil to meet agricultural needs. +Mwh_MeOH_per_MWh_H2,LHV,float,"The energy amount of the produced methanol per energy amount of hydrogen. From `DECHEMA (2017) `_, page 64." +MWh_MeOH_per_tCO2,LHV,float,"The energy amount of the produced methanol per ton of CO2. From `DECHEMA (2017) `_, page 66." +MWh_MeOH_per_MWh_e,LHV,float,"The energy amount of the produced methanol per energy amount of electricity. From `DECHEMA (2017) `_, page 64." +shipping_hydrogen _liquefaction,--,"{true, false}",Whether to include liquefaction costs for hydrogen demand in shipping. +,,, +shipping_hydrogen_share,--,Dictionary with planning horizons as keys.,The share of ships powered by hydrogen in a given year +shipping_methanol_share,--,Dictionary with planning horizons as keys.,The share of ships powered by methanol in a given year +shipping_oil_share,--,Dictionary with planning horizons as keys.,The share of ships powered by oil in a given year +shipping_methanol _efficiency,--,float,The efficiency of methanol-powered ships in the conversion of methanol to meet shipping needs (propulsion). The efficiency increase from oil can be 10-15% higher according to the `IEA `_ +,,, +shipping_oil_efficiency,--,float,The efficiency of oil-powered ships in the conversion of oil to meet shipping needs (propulsion). Base value derived from 2011 +aviation_demand_factor,--,float,The proportion of demand for aviation compared to today's consumption +HVC_demand_factor,--,float,The proportion of demand for high-value chemicals compared to today's consumption +,,, +time_dep_hp_cop,--,"{true, false}",Consider the time dependent coefficient of performance (COP) of the heat pump +heat_pump_sink_T,°C,float,The temperature heat sink used in heat pumps based on DTU / large area radiators. The value is conservatively high to cover hot water and space heating in poorly-insulated buildings +reduce_space_heat _exogenously,--,"{true, false}",Influence on space heating demand by a certain factor (applied before losses in district heating). +reduce_space_heat _exogenously_factor,--,Dictionary with planning horizons as keys.,"A positive factor can mean renovation or demolition of a building. If the factor is negative, it can mean an increase in floor area, increased thermal comfort, population growth. The default factors are determined by the `Eurocalc Homes and buildings decarbonization scenario `_" +retrofitting,,, +-- retro_endogen,--,"{true, false}",Add retrofitting as an endogenous system which co-optimise space heat savings. +-- cost_factor,--,float,Weight costs for building renovation +-- interest_rate,--,float,The interest rate for investment in building components +-- annualise_cost,--,"{true, false}",Annualise the investment costs of retrofitting +-- tax_weighting,--,"{true, false}",Weight the costs of retrofitting depending on taxes in countries +-- construction_index,--,"{true, false}",Weight the costs of retrofitting depending on labour/material costs per country +tes,--,"{true, false}",Add option for storing thermal energy in large water pits associated with district heating systems and individual thermal energy storage (TES) +tes_tau,,,The time constant used to calculate the decay of thermal energy in thermal energy storage (TES): 1- :math:`e^{-1/24τ}`. +-- decentral,days,float,The time constant in decentralized thermal energy storage (TES) +-- central,days,float,The time constant in centralized thermal energy storage (TES) +boilers,--,"{true, false}",Add option for transforming gas into heat using gas boilers +resistive_heaters,--,"{true, false}",Add option for transforming electricity into heat using resistive heaters (independently from gas boilers) +oil_boilers,--,"{true, false}",Add option for transforming oil into heat using boilers +biomass_boiler,--,"{true, false}",Add option for transforming biomass into heat using boilers +overdimension_individual_heating,--,float,Add option for overdimensioning individual heating systems by a certain factor. This allows them to cover heat demand peaks e.g. 10% higher than those in the data with a setting of 1.1. +chp,--,"{true, false}",Add option for using Combined Heat and Power (CHP) +micro_chp,--,"{true, false}",Add option for using Combined Heat and Power (CHP) for decentral areas. +solar_thermal,--,"{true, false}",Add option for using solar thermal to generate heat. +solar_cf_correction,--,float,The correction factor for the value provided by the solar thermal profile calculations +marginal_cost_storage,currency/MWh ,float,The marginal cost of discharging batteries in distributed grids +methanation,--,"{true, false}",Add option for transforming hydrogen and CO2 into methane using methanation. +coal_cc,--,"{true, false}",Add option for coal CHPs with carbon capture +dac,--,"{true, false}",Add option for Direct Air Capture (DAC) +co2_vent,--,"{true, false}",Add option for vent out CO2 from storages to the atmosphere. +allam_cycle,--,"{true, false}",Add option to include `Allam cycle gas power plants `_ +hydrogen_fuel_cell,--,"{true, false}",Add option to include hydrogen fuel cell for re-electrification. Assuming OCGT technology costs +hydrogen_turbine,--,"{true, false}",Add option to include hydrogen turbine for re-electrification. Assuming OCGT technology costs +SMR,--,"{true, false}",Add option for transforming natural gas into hydrogen and CO2 using Steam Methane Reforming (SMR) +SMR CC,--,"{true, false}",Add option for transforming natural gas into hydrogen and CO2 using Steam Methane Reforming (SMR) and Carbon Capture (CC) +regional_methanol_demand,--,"{true, false}",Spatially resolve methanol demand. Set to true if regional CO2 constraints needed. +regional_oil_demand,--,"{true, false}",Spatially resolve oil demand. Set to true if regional CO2 constraints needed. +regional_co2 _sequestration_potential,,, +-- enable,--,"{true, false}",Add option for regionally-resolved geological carbon dioxide sequestration potentials based on `CO2StoP `_. +-- attribute,--,string or list,Name (or list of names) of the attribute(s) for the sequestration potential +-- include_onshore,--,"{true, false}",Add options for including onshore sequestration potentials +-- min_size,Gt ,float,Any sites with lower potential than this value will be excluded +-- max_size,Gt ,float,The maximum sequestration potential for any one site. +-- years_of_storage,years,float,The years until potential exhausted at optimised annual rate +co2_sequestration_potential,--,Dictionary with planning horizons as keys.,The potential of sequestering CO2 in Europe per year and investment period +co2_sequestration_cost,currency/tCO2,float,The cost of sequestering a ton of CO2 +co2_sequestration_lifetime,years,int,The lifetime of a CO2 sequestration site +co2_spatial,--,"{true, false}","Add option to spatially resolve carrier representing stored carbon dioxide. This allows for more detailed modelling of CCUTS, e.g. regarding the capturing of industrial process emissions, usage as feedstock for electrofuels, transport of carbon dioxide, and geological sequestration sites." +,,, +co2network,--,"{true, false}",Add option for planning a new carbon dioxide transmission network +co2_network_cost_factor,p.u.,float,The cost factor for the capital cost of the carbon dioxide transmission network +,,, +cc_fraction,--,float,The default fraction of CO2 captured with post-combustion capture +hydrogen_underground _storage,--,"{true, false}",Add options for storing hydrogen underground. Storage potential depends regionally. +hydrogen_underground _storage_locations,,"{onshore, nearshore, offshore}","The location where hydrogen underground storage can be located. Onshore, nearshore, offshore means it must be located more than 50 km away from the sea, within 50 km of the sea, or within the sea itself respectively." +,,, +ammonia,--,"{true, false, regional}","Add ammonia as a carrrier. It can be either true (copperplated NH3), false (no NH3 carrier) or ""regional"" (regionalised NH3 without network)" +min_part_load_fischer _tropsch,per unit of p_nom ,float,The minimum unit dispatch (``p_min_pu``) for the Fischer-Tropsch process +min_part_load _methanolisation,per unit of p_nom ,float,The minimum unit dispatch (``p_min_pu``) for the methanolisation process +,,, +use_fischer_tropsch _waste_heat,--,"{true, false}",Add option for using waste heat of Fischer Tropsch in district heating networks +use_fuel_cell_waste_heat,--,"{true, false}",Add option for using waste heat of fuel cells in district heating networks +use_electrolysis_waste _heat,--,"{true, false}",Add option for using waste heat of electrolysis in district heating networks +electricity_transmission _grid,--,"{true, false}",Switch for enabling/disabling the electricity transmission grid. +electricity_distribution _grid,--,"{true, false}",Add a simplified representation of the exchange capacity between transmission and distribution grid level through a link. +electricity_distribution _grid_cost_factor,,,Multiplies the investment cost of the electricity distribution grid +,,, +electricity_grid _connection,--,"{true, false}",Add the cost of electricity grid connection for onshore wind and solar +transmission_efficiency,,,Section to specify transmission losses or compression energy demands of bidirectional links. Splits them into two capacity-linked unidirectional links. +-- {carrier},--,str,The carrier of the link. +-- -- efficiency_static,p.u.,float,Length-independent transmission efficiency. +-- -- efficiency_per_1000km,p.u. per 1000 km,float,Length-dependent transmission efficiency ($\eta^{\text{length}}$) +-- -- compression_per_1000km,p.u. per 1000 km,float,Length-dependent electricity demand for compression ($\eta \cdot \text{length}$) implemented as multi-link to local electricity bus. +H2_network,--,"{true, false}",Add option for new hydrogen pipelines +gas_network,--,"{true, false}","Add existing natural gas infrastructure, incl. LNG terminals, production and entry-points. The existing gas network is added with a lossless transport model. A length-weighted `k-edge augmentation algorithm `_ can be run to add new candidate gas pipelines such that all regions of the model can be connected to the gas network. When activated, all the gas demands are regionally disaggregated as well." +H2_retrofit,--,"{true, false}",Add option for retrofiting existing pipelines to transport hydrogen. +H2_retrofit_capacity _per_CH4,--,float,"The ratio for H2 capacity per original CH4 capacity of retrofitted pipelines. The `European Hydrogen Backbone (April, 2020) p.15 `_ 60% of original natural gas capacity could be used in cost-optimal case as H2 capacity." +gas_network_connectivity _upgrade ,--,float,The number of desired edge connectivity (k) in the length-weighted `k-edge augmentation algorithm `_ used for the gas network +gas_distribution_grid,--,"{true, false}",Add a gas distribution grid +gas_distribution_grid _cost_factor,,,Multiplier for the investment cost of the gas distribution grid +,,, +biomass_spatial,--,"{true, false}",Add option for resolving biomass demand regionally +biomass_transport,--,"{true, false}",Add option for transporting solid biomass between nodes +biogas_upgrading_cc,--,"{true, false}",Add option to capture CO2 from biomass upgrading +conventional_generation,,,Add a more detailed description of conventional carriers. Any power generation requires the consumption of fuel from nodes representing that fuel. +biomass_to_liquid,--,"{true, false}",Add option for transforming solid biomass into liquid fuel with the same properties as oil +biosng,--,"{true, false}",Add option for transforming solid biomass into synthesis gas with the same properties as natural gas +bioH2,--,"{true, false}",Add option for transforming solid biomass into hydrogen with carbon capture +municipal_solid_waste,--,"{true, false}",Add option for municipal solid waste +limit_max_growth,,, +-- enable,--,"{true, false}",Add option to limit the maximum growth of a carrier +-- factor,p.u.,float,The maximum growth factor of a carrier (e.g. 1.3 allows 30% larger than max historic growth) +-- max_growth,,, +-- -- {carrier},GW,float,The historic maximum growth of a carrier +-- max_relative_growth,,, +-- -- {carrier},p.u.,float,The historic maximum relative growth of a carrier +,,, +enhanced_geothermal,,, +-- enable,--,"{true, false}",Add option to include Enhanced Geothermal Systems +-- flexible,--,"{true, false}",Add option for flexible operation (see Ricks et al. 2024) +-- max_hours,--,int,The maximum hours the reservoir can be charged under flexible operation +-- max_boost,--,float,The maximum boost in power output under flexible operation +-- var_cf,--,"{true, false}",Add option for variable capacity factor (see Ricks et al. 2024) +-- sustainability_factor,--,float,Share of sourced heat that is replenished by the earth's core (see details in `build_egs_potentials.py `_) +solid_biomass_import,,, +-- enable,--,"{true, false}",Add option to include solid biomass imports +-- price,currency/MWh,float,Price for importing solid biomass +-- max_amount,Twh,float,Maximum solid biomass import potential +-- upstream_emissions_factor,p.u.,float,Upstream emissions of solid biomass imports diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 6fb6c51c1..3ee042840 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -9,6 +9,7 @@ Release Notes Upcoming Release ================ +* Add investment period dependent CO2 sequestration potentials * Add option to produce hydrogen from solid biomass (flag ``solid biomass to hydrogen``), combined with carbon capture diff --git a/scripts/solve_network.py b/scripts/solve_network.py index b7d5665b8..99f7b5751 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -1131,14 +1131,14 @@ def solve_network(n, config, solving, **kwargs): from _helpers import mock_snakemake snakemake = mock_snakemake( - "solve_sector_network_myopic", - # configfiles="../config/test/config.perfect.yaml", + "solve_sector_network_perfect", + configfiles="../config/test/config.perfect.yaml", simpl="", opts="", - clusters="38", - ll="vopt", + clusters="5", + ll="v1.0", sector_opts="", - planning_horizons="2030", + # planning_horizons="2030", ) configure_logging(snakemake) set_scenario_config(snakemake) From 0c36de9bf8b72b474ccde63fb65fa0e97e18a08e Mon Sep 17 00:00:00 2001 From: Bobby Xiong <36541459+bobbyxng@users.noreply.github.com> Date: Thu, 22 Aug 2024 15:01:20 +0200 Subject: [PATCH 222/344] Introducing OpenStreetMap high-voltage grid to PyPSA-Eur (#1079) * Implemented which uses the overpass API to download power features for individual countries. * Extended rule by input. * Bug fixes and improvements to clean_osm_data.py. Added in retrieve_osm_data.py. * Updated clean_osm_data and retrieve_osm_data to create clean substations. * Finished clean_osm_data function. * Added check whether line is a circle. If so, drop it. * Extended build_electricity.smk by build_osm_network.py * Added build_osm_network * Working osm-network-fast * Bug fixes. * Finalised and cleaned including docstrings. * Added try catch to retrieve_osm_data. Allows for parallelisation of downloads. * Updated cleaning process. * Set maximum number of threads for retrieving to 4, wrt. fair usage policy and potential request errors. * Intermediate update on clean_osm_data.py. Added docstrings. * Bug fix. * Bug fix. * Bug fixes in data types out of clean_osm_data * Significant improvements to retrieve_osm_data, clean_osm_data. Cleaned code. Speed improvements * Cleaned config. * Fixes. * Bug fixes. * Updated default config * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Removed overpass from required packages. Not needed anymore. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Added links_relations (route = power, frequency = 0) to retrieval. This will change how HVDC links are extracted in the near future. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Work-in-progress clean_osm_data * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Added clean links output to clean_osm_data. Script uses OSM relations to retrieve clean HVDC links. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * New code for integrating HVDC links. Using relations. Base network implementation functioning. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * removed manual line dropping. * Updated clean script * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * reverted Snakefile to default: sync settings * added prebuilt functionality. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Updated build_electricity.smk to work with scenario management. * removed commented-out code. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * removed commented-out code. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fixed bug in pdf export by substituting pdf export with svg. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Bug-fix Snakefile * dropped not needed columns from build_osm_network. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Updated build_shapes, config.default and clean_osm_data. * pre-commit changes. * test * Added initial prepare_osm_network_release.py script * Finalised prepare_osm_network_release script to build clean and stable OSM base_network input files. * Added new rules/development.smk * Updated clean_osm_data to add substation_centroid to linestrings * Updated clean_osm_data to add substation_centroid to linestrings * Updated clean_osm_data to add substation_centroid to linestrings * Updated clean_osm_data to add substation_centroid to linestrings * Added osm-prebuilt functionality and zenodo sandbox repository. * Updated clean_osm_data to geopandas v.1.01 * Made base_network and build_osm_network function more robust for empty links. * Made base_network and build_osm_network function more robust for empty links. * Bug fix in base_network. Voltage level null is now kept (relevant e.g. for Corsica) * Merge with hcanges in upstream PR 1146. Fixing UA and MD. * Updated Zenodo and fixed prepare_osm_network_release * Updated osm network release. * Updated prepare osm network release. * Updated MD, UA scripts. * Cleaned determine_availability_matrix_MD_UA.py, removed redundant code * Bug fixes. * Bug fixes for UA MD scripts. * Rename of build script. * Bug fix: only distribute load to buses with substation. * Updated zenodo sandbox repository. * Updated config.default * Cleaned config.default.yaml: Related settings grouped together and redundant voltage settings aggregated. * Cleaned config.default.yaml: Related settings grouped together and redundant voltage settings aggregated. Added release notes. * Updated Zenodo repositories for OSM-prebuilt to offcial publication. * Updated configtables * Updated links.csv: Under_construction lines to in commission. * Updated link 8394 and parameter_corrections: Continuation of North-Sea-Link. * Major update: fix simplify_network, fix Corsica, updated build_osm_network to include lines overpassing nodes. * remove config backup * Bug fix: Carrier type of all supernodes corrected to 'AC' * Bug fix: Carrier type of all supernodes corrected to 'AC' * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Updated rules and base_network for compatibility with TYNDP projects. * Updated Zenodo repository and prebuilt network to include 150 kV HVDC connections. * Removed outdated config backup. * Implemented all comments from PR #1079. Cleaned up OSM implementation. * Bug fix: Added all voltages, 200 kV-750 kV, to default config. * Cleaning and bugfixes. * Updated Zenodo repository to https://zenodo.org/records/13358976. Added converter voltages, 'underground' property for DC lines/cables, and included Konti-Skan HVDC (DK-SE). Added compatibility with https://github.com/PyPSA/pypsa-eur/pull/1079 and https://github.com/PyPSA/pypsa-eur/pull/1085 * Apply suggestions from code review * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * simplify_network: handle complicated transformer topologies * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * syntax fix --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Fabian Neumann --- Snakefile | 1 + config/config.default.yaml | 7 +- doc/configtables/electricity.csv | 3 +- doc/release_notes.rst | 2 + envs/environment.yaml | 1 + rules/build_electricity.smk | 97 +- rules/development.smk | 26 + rules/retrieve.smk | 82 ++ scripts/base_network.py | 305 +++- scripts/build_osm_network.py | 863 +++++++++++ scripts/clean_osm_data.py | 1807 ++++++++++++++++++++++++ scripts/prepare_osm_network_release.py | 146 ++ scripts/retrieve_osm_data.py | 152 ++ scripts/simplify_network.py | 9 +- 14 files changed, 3430 insertions(+), 71 deletions(-) create mode 100644 rules/development.smk create mode 100644 scripts/build_osm_network.py create mode 100644 scripts/clean_osm_data.py create mode 100644 scripts/prepare_osm_network_release.py create mode 100644 scripts/retrieve_osm_data.py diff --git a/Snakefile b/Snakefile index be2545ada..eb99437bf 100644 --- a/Snakefile +++ b/Snakefile @@ -54,6 +54,7 @@ include: "rules/build_sector.smk" include: "rules/solve_electricity.smk" include: "rules/postprocess.smk" include: "rules/validate.smk" +include: "rules/development.smk" if config["foresight"] == "overnight": diff --git a/config/config.default.yaml b/config/config.default.yaml index 9d565b633..86689ea72 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -42,7 +42,7 @@ scenario: ll: - vopt clusters: - - 38 + - 41 - 128 - 256 opts: @@ -74,7 +74,6 @@ enable: custom_busmap: false drop_leap_day: true - # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#co2-budget co2_budget: 2020: 0.701 @@ -87,7 +86,8 @@ co2_budget: # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#electricity electricity: - voltages: [220., 300., 380., 500., 750.] + voltages: [200., 220., 300., 380., 500., 750.] + base_network: entsoegridkit gaslimit_enable: false gaslimit: false co2limit_enable: false @@ -276,6 +276,7 @@ conventional: # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#lines lines: types: + 200.: "Al/St 240/40 2-bundle 200.0" 220.: "Al/St 240/40 2-bundle 220.0" 300.: "Al/St 240/40 3-bundle 300.0" 380.: "Al/St 240/40 4-bundle 380.0" diff --git a/doc/configtables/electricity.csv b/doc/configtables/electricity.csv index ee733660c..9bad7bfc6 100644 --- a/doc/configtables/electricity.csv +++ b/doc/configtables/electricity.csv @@ -1,5 +1,6 @@ ,Unit,Values,Description -voltages,kV,"Any subset of {220., 300., 380.}",Voltage levels to consider +voltages,kV,"Any subset of {200., 220., 300., 380., 500., 750.}",Voltage levels to consider +base_network, --, "Any value in {'entsoegridkit', 'osm-prebuilt', 'osm-raw}", "Specify the underlying base network, i.e. GridKit (based on ENTSO-E web map extract, OpenStreetMap (OSM) prebuilt or raw (built from raw OSM data), takes longer." gaslimit_enable,bool,true or false,Add an overall absolute gas limit configured in ``electricity: gaslimit``. gaslimit,MWhth,float or false,Global gas usage limit co2limit_enable,bool,true or false,Add an overall absolute carbon-dioxide emissions limit configured in ``electricity: co2limit`` in :mod:`prepare_network`. **Warning:** This option should currently only be used with electricity-only networks, not for sector-coupled networks.. diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 3ee042840..b1345e0c7 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -74,6 +74,8 @@ Upcoming Release * Enable parallelism in :mod:`determine_availability_matrix_MD_UA.py` and remove plots. This requires the use of temporary files. +* Added new major feature to create the base_network from OpenStreetMap (OSM) data (PR https://github.com/PyPSA/pypsa-eur/pull/1079). Note that a heuristics based cleaning process is used for lines and links where electrical parameters are incomplete, missing, or ambiguous. Through ``electricity["base_network"]``, the base network can be set to "entsoegridkit" (original default setting, deprecated soon), "osm-prebuilt" (which downloads the latest prebuilt snapshot based on OSM data from Zenodo), or "osm-raw" which retrieves (once) and cleans the raw OSM data and subsequently builds the network. Note that this process may take a few minutes. + * Updated pre-built `weather data cutouts `__. These are now merged cutouts with solar irradiation from the new SARAH-3 dataset while taking all other diff --git a/envs/environment.yaml b/envs/environment.yaml index 9115ccd97..98cf858d6 100644 --- a/envs/environment.yaml +++ b/envs/environment.yaml @@ -47,6 +47,7 @@ dependencies: - pyxlsb - graphviz - pre-commit +- geojson # Keep in conda environment when calling ipython - ipython diff --git a/rules/build_electricity.smk b/rules/build_electricity.smk index f35050917..4446ef765 100644 --- a/rules/build_electricity.smk +++ b/rules/build_electricity.smk @@ -50,6 +50,19 @@ rule build_powerplants: "../scripts/build_powerplants.py" +def input_base_network(w): + base_network = config_provider("electricity", "base_network")(w) + components = {"buses", "lines", "links", "converters", "transformers"} + if base_network == "osm-raw": + inputs = {c: resources(f"osm-raw/build/{c}.csv") for c in components} + else: + inputs = {c: f"data/{base_network}/{c}.csv" for c in components} + if base_network == "entsoegridkit": + inputs["parameter_corrections"] = "data/parameter_corrections.yaml" + inputs["links_p_nom"] = "data/links_p_nom.csv" + return inputs + + rule base_network: params: countries=config_provider("countries"), @@ -58,13 +71,7 @@ rule base_network: lines=config_provider("lines"), transformers=config_provider("transformers"), input: - eg_buses="data/entsoegridkit/buses.csv", - eg_lines="data/entsoegridkit/lines.csv", - eg_links="data/entsoegridkit/links.csv", - eg_converters="data/entsoegridkit/converters.csv", - eg_transformers="data/entsoegridkit/transformers.csv", - parameter_corrections="data/parameter_corrections.yaml", - links_p_nom="data/links_p_nom.csv", + unpack(input_base_network), country_shapes=resources("country_shapes.geojson"), offshore_shapes=resources("offshore_shapes.geojson"), europe_shape=resources("europe_shape.geojson"), @@ -629,3 +636,79 @@ rule prepare_network: "../envs/environment.yaml" script: "../scripts/prepare_network.py" + + +if config["electricity"]["base_network"] == "osm-raw": + + rule clean_osm_data: + input: + cables_way=expand( + "data/osm-raw/{country}/cables_way.json", + country=config_provider("countries"), + ), + lines_way=expand( + "data/osm-raw/{country}/lines_way.json", + country=config_provider("countries"), + ), + links_relation=expand( + "data/osm-raw/{country}/links_relation.json", + country=config_provider("countries"), + ), + substations_way=expand( + "data/osm-raw/{country}/substations_way.json", + country=config_provider("countries"), + ), + substations_relation=expand( + "data/osm-raw/{country}/substations_relation.json", + country=config_provider("countries"), + ), + offshore_shapes=resources("offshore_shapes.geojson"), + country_shapes=resources("country_shapes.geojson"), + output: + substations=resources("osm-raw/clean/substations.geojson"), + substations_polygon=resources("osm-raw/clean/substations_polygon.geojson"), + lines=resources("osm-raw/clean/lines.geojson"), + links=resources("osm-raw/clean/links.geojson"), + log: + logs("clean_osm_data.log"), + benchmark: + benchmarks("clean_osm_data") + threads: 1 + resources: + mem_mb=4000, + conda: + "../envs/environment.yaml" + script: + "../scripts/clean_osm_data.py" + + +if config["electricity"]["base_network"] == "osm-raw": + + rule build_osm_network: + input: + substations=resources("osm-raw/clean/substations.geojson"), + lines=resources("osm-raw/clean/lines.geojson"), + links=resources("osm-raw/clean/links.geojson"), + country_shapes=resources("country_shapes.geojson"), + output: + lines=resources("osm-raw/build/lines.csv"), + links=resources("osm-raw/build/links.csv"), + converters=resources("osm-raw/build/converters.csv"), + transformers=resources("osm-raw/build/transformers.csv"), + substations=resources("osm-raw/build/buses.csv"), + lines_geojson=resources("osm-raw/build/geojson/lines.geojson"), + links_geojson=resources("osm-raw/build/geojson/links.geojson"), + converters_geojson=resources("osm-raw/build/geojson/converters.geojson"), + transformers_geojson=resources("osm-raw/build/geojson/transformers.geojson"), + substations_geojson=resources("osm-raw/build/geojson/buses.geojson"), + log: + logs("build_osm_network.log"), + benchmark: + benchmarks("build_osm_network") + threads: 1 + resources: + mem_mb=4000, + conda: + "../envs/environment.yaml" + script: + "../scripts/build_osm_network.py" diff --git a/rules/development.smk b/rules/development.smk new file mode 100644 index 000000000..465490258 --- /dev/null +++ b/rules/development.smk @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: : 2023-2024 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT + +if config["electricity"]["base_network"] == "osm-raw": + + rule prepare_osm_network_release: + input: + base_network=resources("networks/base.nc"), + output: + buses=resources("osm-raw/release/buses.csv"), + converters=resources("osm-raw/release/converters.csv"), + lines=resources("osm-raw/release/lines.csv"), + links=resources("osm-raw/release/links.csv"), + transformers=resources("osm-raw/release/transformers.csv"), + log: + logs("prepare_osm_network_release.log"), + benchmark: + benchmarks("prepare_osm_network_release") + threads: 1 + resources: + mem_mb=1000, + conda: + "../envs/environment.yaml" + script: + "../scripts/prepare_osm_network_release.py" diff --git a/rules/retrieve.smk b/rules/retrieve.smk index de84b2fae..c30696ccf 100644 --- a/rules/retrieve.smk +++ b/rules/retrieve.smk @@ -390,3 +390,85 @@ if config["enable"]["retrieve"]: "../envs/retrieve.yaml" script: "../scripts/retrieve_monthly_fuel_prices.py" + + +if config["enable"]["retrieve"] and ( + config["electricity"]["base_network"] == "osm-prebuilt" +): + + rule retrieve_osm_prebuilt: + input: + buses=storage("https://zenodo.org/records/13358976/files/buses.csv"), + converters=storage( + "https://zenodo.org/records/13358976/files/converters.csv" + ), + lines=storage("https://zenodo.org/records/13358976/files/lines.csv"), + links=storage("https://zenodo.org/records/13358976/files/links.csv"), + transformers=storage( + "https://zenodo.org/records/13358976/files/transformers.csv" + ), + output: + buses="data/osm-prebuilt/buses.csv", + converters="data/osm-prebuilt/converters.csv", + lines="data/osm-prebuilt/lines.csv", + links="data/osm-prebuilt/links.csv", + transformers="data/osm-prebuilt/transformers.csv", + log: + "logs/retrieve_osm_prebuilt.log", + threads: 1 + resources: + mem_mb=500, + retries: 2 + run: + for key in input.keys(): + move(input[key], output[key]) + validate_checksum(output[key], input[key]) + + + +if config["enable"]["retrieve"] and ( + config["electricity"]["base_network"] == "osm-raw" +): + + rule retrieve_osm_data: + output: + cables_way="data/osm-raw/{country}/cables_way.json", + lines_way="data/osm-raw/{country}/lines_way.json", + links_relation="data/osm-raw/{country}/links_relation.json", + substations_way="data/osm-raw/{country}/substations_way.json", + substations_relation="data/osm-raw/{country}/substations_relation.json", + log: + "logs/retrieve_osm_data_{country}.log", + threads: 1 + conda: + "../envs/retrieve.yaml" + script: + "../scripts/retrieve_osm_data.py" + + +if config["enable"]["retrieve"] and ( + config["electricity"]["base_network"] == "osm-raw" +): + + rule retrieve_osm_data_all: + input: + expand( + "data/osm-raw/{country}/cables_way.json", + country=config_provider("countries"), + ), + expand( + "data/osm-raw/{country}/lines_way.json", + country=config_provider("countries"), + ), + expand( + "data/osm-raw/{country}/links_relation.json", + country=config_provider("countries"), + ), + expand( + "data/osm-raw/{country}/substations_way.json", + country=config_provider("countries"), + ), + expand( + "data/osm-raw/{country}/substations_relation.json", + country=config_provider("countries"), + ), diff --git a/scripts/base_network.py b/scripts/base_network.py index 2b0fee63b..afb66387e 100644 --- a/scripts/base_network.py +++ b/scripts/base_network.py @@ -5,7 +5,11 @@ # coding: utf-8 """ -Creates the network topology from an `ENTSO-E map extract `_ (March 2022) as a PyPSA network. +Creates the network topology from a `ENTSO-E map extract. +`_ (March 2022) +or `OpenStreetMap data `_ (Aug 2024) +as a PyPSA +network. Relevant Settings ----------------- @@ -131,45 +135,50 @@ def _find_closest_links(links, new_links, distance_upper_bound=1.5): ) -def _load_buses_from_eg(eg_buses, europe_shape, config_elec): +def _load_buses(buses, europe_shape, config): buses = ( pd.read_csv( - eg_buses, + buses, quotechar="'", true_values=["t"], false_values=["f"], dtype=dict(bus_id="str"), ) .set_index("bus_id") - .drop(["station_id"], axis=1) .rename(columns=dict(voltage="v_nom")) ) + if "station_id" in buses.columns: + buses.drop("station_id", axis=1, inplace=True) + buses["carrier"] = buses.pop("dc").map({True: "DC", False: "AC"}) buses["under_construction"] = buses.under_construction.where( lambda s: s.notnull(), False ).astype(bool) - - # remove all buses outside of all countries including exclusive economic zones (offshore) europe_shape = gpd.read_file(europe_shape).loc[0, "geometry"] europe_shape_prepped = shapely.prepared.prep(europe_shape) buses_in_europe_b = buses[["x", "y"]].apply( lambda p: europe_shape_prepped.contains(Point(p)), axis=1 ) + v_nom_min = min(config["electricity"]["voltages"]) + v_nom_max = max(config["electricity"]["voltages"]) + buses_with_v_nom_to_keep_b = ( - buses.v_nom.isin(config_elec["voltages"]) | buses.v_nom.isnull() - ) - logger.info( - f'Removing buses with voltages {pd.Index(buses.v_nom.unique()).dropna().difference(config_elec["voltages"])}' + (v_nom_min <= buses.v_nom) & (buses.v_nom <= v_nom_max) + | (buses.v_nom.isnull()) + | ( + buses.carrier == "DC" + ) # Keeping all DC buses from the input dataset independent of voltage (e.g. 150 kV connections) ) + logger.info(f"Removing buses outside of range AC {v_nom_min} - {v_nom_max} V") return pd.DataFrame(buses.loc[buses_in_europe_b & buses_with_v_nom_to_keep_b]) -def _load_transformers_from_eg(buses, eg_transformers): +def _load_transformers(buses, transformers): transformers = pd.read_csv( - eg_transformers, + transformers, quotechar="'", true_values=["t"], false_values=["f"], @@ -181,9 +190,9 @@ def _load_transformers_from_eg(buses, eg_transformers): return transformers -def _load_converters_from_eg(buses, eg_converters): +def _load_converters_from_eg(buses, converters): converters = pd.read_csv( - eg_converters, + converters, quotechar="'", true_values=["t"], false_values=["f"], @@ -197,9 +206,25 @@ def _load_converters_from_eg(buses, eg_converters): return converters -def _load_links_from_eg(buses, eg_links): +def _load_converters_from_osm(buses, converters): + converters = pd.read_csv( + converters, + quotechar="'", + true_values=["t"], + false_values=["f"], + dtype=dict(converter_id="str", bus0="str", bus1="str"), + ).set_index("converter_id") + + converters = _remove_dangling_branches(converters, buses) + + converters["carrier"] = "" + + return converters + + +def _load_links_from_eg(buses, links): links = pd.read_csv( - eg_links, + links, quotechar="'", true_values=["t"], false_values=["f"], @@ -208,7 +233,7 @@ def _load_links_from_eg(buses, eg_links): links["length"] /= 1e3 - # Skagerrak Link is connected to 132kV bus which is removed in _load_buses_from_eg. + # Skagerrak Link is connected to 132kV bus which is removed in _load_buses. # Connect to neighboring 380kV bus links.loc[links.bus1 == "6396", "bus1"] = "6398" @@ -220,10 +245,35 @@ def _load_links_from_eg(buses, eg_links): return links -def _load_lines_from_eg(buses, eg_lines): +def _load_links_from_osm(buses, links): + links = pd.read_csv( + links, + quotechar="'", + true_values=["t"], + false_values=["f"], + dtype=dict( + link_id="str", + bus0="str", + bus1="str", + voltage="int", + p_nom="float", + ), + ).set_index("link_id") + + links["length"] /= 1e3 + + links = _remove_dangling_branches(links, buses) + + # Add DC line parameters + links["carrier"] = "DC" + + return links + + +def _load_lines(buses, lines): lines = ( pd.read_csv( - eg_lines, + lines, quotechar="'", true_values=["t"], false_values=["f"], @@ -240,6 +290,7 @@ def _load_lines_from_eg(buses, eg_lines): ) lines["length"] /= 1e3 + lines["carrier"] = "AC" lines = _remove_dangling_branches(lines, buses) @@ -290,7 +341,7 @@ def _reconnect_crimea(lines): return pd.concat([lines, lines_to_crimea]) -def _set_electrical_parameters_lines(lines, config): +def _set_electrical_parameters_lines_eg(lines, config): v_noms = config["electricity"]["voltages"] linetypes = config["lines"]["types"] @@ -302,16 +353,36 @@ def _set_electrical_parameters_lines(lines, config): return lines +def _set_electrical_parameters_lines_osm(lines, config): + if lines.empty: + lines["type"] = [] + return lines + + v_noms = config["electricity"]["voltages"] + linetypes = _get_linetypes_config(config["lines"]["types"], v_noms) + + lines["carrier"] = "AC" + lines["dc"] = False + + lines.loc[:, "type"] = lines.v_nom.apply( + lambda x: _get_linetype_by_voltage(x, linetypes) + ) + + lines["s_max_pu"] = config["lines"]["s_max_pu"] + + return lines + + def _set_lines_s_nom_from_linetypes(n): n.lines["s_nom"] = ( np.sqrt(3) * n.lines["type"].map(n.line_types.i_nom) * n.lines["v_nom"] - * n.lines.num_parallel + * n.lines["num_parallel"] ) -def _set_electrical_parameters_links(links, config, links_p_nom): +def _set_electrical_parameters_links_eg(links, config, links_p_nom): if links.empty: return links @@ -343,12 +414,27 @@ def _set_electrical_parameters_links(links, config, links_p_nom): return links +def _set_electrical_parameters_links_osm(links, config): + if links.empty: + return links + + p_max_pu = config["links"].get("p_max_pu", 1.0) + links["p_max_pu"] = p_max_pu + links["p_min_pu"] = -p_max_pu + links["carrier"] = "DC" + links["dc"] = True + + return links + + def _set_electrical_parameters_converters(converters, config): p_max_pu = config["links"].get("p_max_pu", 1.0) converters["p_max_pu"] = p_max_pu converters["p_min_pu"] = -p_max_pu - converters["p_nom"] = 2000 + # if column "p_nom" does not exist, set to 2000 + if "p_nom" not in converters: + converters["p_nom"] = 2000 # Converters are combined with links converters["under_construction"] = False @@ -463,7 +549,7 @@ def prefer_voltage(x, which): buses["substation_lv"] = ( lv_b & onshore_b & (~buses["under_construction"]) & has_connections_b ) - buses["substation_off"] = ((hv_b & offshore_b) | (hv_b & onshore_b)) & ( + buses["substation_off"] = (offshore_b | (hv_b & onshore_b)) & ( ~buses["under_construction"] ) @@ -617,11 +703,11 @@ def _set_shapes(n, country_shapes, offshore_shapes): def base_network( - eg_buses, - eg_converters, - eg_transformers, - eg_lines, - eg_links, + buses, + converters, + transformers, + lines, + links, links_p_nom, europe_shape, country_shapes, @@ -629,29 +715,59 @@ def base_network( parameter_corrections, config, ): - buses = _load_buses_from_eg(eg_buses, europe_shape, config["electricity"]) - links = _load_links_from_eg(buses, eg_links) + base_network = config["electricity"].get("base_network") + assert base_network in { + "entsoegridkit", + "osm-raw", + "osm-prebuilt", + }, f"base_network must be either 'entsoegridkit', 'osm-raw' or 'osm-prebuilt', but got '{base_network}'" + if base_network == "entsoegridkit": + warnings.warn( + "The 'entsoegridkit' base network is deprecated and will be removed in future versions. Please use 'osm-raw' or 'osm-prebuilt' instead.", + DeprecationWarning, + ) + + logger.info(f"Creating base network using {base_network}.") + + buses = _load_buses(buses, europe_shape, config) + transformers = _load_transformers(buses, transformers) + lines = _load_lines(buses, lines) - converters = _load_converters_from_eg(buses, eg_converters) + if base_network == "entsoegridkit": + links = _load_links_from_eg(buses, links) + converters = _load_converters_from_eg(buses, converters) - lines = _load_lines_from_eg(buses, eg_lines) - transformers = _load_transformers_from_eg(buses, eg_transformers) + # Optionally reconnect Crimea + if (config["lines"].get("reconnect_crimea", True)) & ( + "UA" in config["countries"] + ): + lines = _reconnect_crimea(lines) - if config["lines"].get("reconnect_crimea", True) and "UA" in config["countries"]: - lines = _reconnect_crimea(lines) + # Set electrical parameters of lines and links + lines = _set_electrical_parameters_lines_eg(lines, config) + links = _set_electrical_parameters_links_eg(links, config, links_p_nom) + elif base_network in {"osm-prebuilt", "osm-raw"}: + links = _load_links_from_osm(buses, links) + converters = _load_converters_from_osm(buses, converters) + + # Set electrical parameters of lines and links + lines = _set_electrical_parameters_lines_osm(lines, config) + links = _set_electrical_parameters_links_osm(links, config) + else: + raise ValueError( + "base_network must be either 'entsoegridkit', 'osm-raw', or 'osm-prebuilt'" + ) - lines = _set_electrical_parameters_lines(lines, config) + # Set electrical parameters of transformers and converters transformers = _set_electrical_parameters_transformers(transformers, config) - links = _set_electrical_parameters_links(links, config, links_p_nom) converters = _set_electrical_parameters_converters(converters, config) n = pypsa.Network() - n.name = "PyPSA-Eur" + n.name = f"PyPSA-Eur ({base_network})" time = get_snapshots(snakemake.params.snapshots, snakemake.params.drop_leap_day) n.set_snapshots(time) - n.madd("Carrier", ["AC", "DC"]) n.import_components_from_dataframe(buses, "Bus") n.import_components_from_dataframe(lines, "Line") @@ -660,8 +776,8 @@ def base_network( n.import_components_from_dataframe(converters, "Link") _set_lines_s_nom_from_linetypes(n) - - _apply_parameter_corrections(n, parameter_corrections) + if config["electricity"].get("base_network") == "entsoegridkit": + _apply_parameter_corrections(n, parameter_corrections) n = _remove_unconnected_components(n) @@ -675,9 +791,64 @@ def base_network( _set_shapes(n, country_shapes, offshore_shapes) + # Add carriers if they are present in buses.carriers + carriers_in_buses = set(n.buses.carrier.dropna().unique()) + carriers = carriers_in_buses.intersection({"AC", "DC"}) + + if carriers: + n.madd("Carrier", carriers) + return n +def _get_linetypes_config(line_types, voltages): + """ + Return the dictionary of linetypes for selected voltages. The dictionary is + a subset of the dictionary line_types, whose keys match the selected + voltages. + + Parameters + ---------- + line_types : dict + Dictionary of linetypes: keys are nominal voltages and values are linetypes. + voltages : list + List of selected voltages. + + Returns + ------- + Dictionary of linetypes for selected voltages. + """ + # get voltages value that are not available in the line types + vnoms_diff = set(voltages).symmetric_difference(set(line_types.keys())) + if vnoms_diff: + logger.warning( + f"Voltages {vnoms_diff} not in the {line_types} or {voltages} list." + ) + return {k: v for k, v in line_types.items() if k in voltages} + + +def _get_linetype_by_voltage(v_nom, d_linetypes): + """ + Return the linetype of a specific line based on its voltage v_nom. + + Parameters + ---------- + v_nom : float + The voltage of the line. + d_linetypes : dict + Dictionary of linetypes: keys are nominal voltages and values are linetypes. + + Returns + ------- + The linetype of the line whose nominal voltage is closest to the line voltage. + """ + v_nom_min, line_type_min = min( + d_linetypes.items(), + key=lambda x: abs(x[0] - v_nom), + ) + return line_type_min + + def voronoi(points, outline, crs=4326): """ Create Voronoi polygons from a set of points within an outline. @@ -793,25 +964,47 @@ def append_bus_shapes(n, shapes, type): configure_logging(snakemake) set_scenario_config(snakemake) + countries = snakemake.params.countries + + buses = snakemake.input.buses + converters = snakemake.input.converters + transformers = snakemake.input.transformers + lines = snakemake.input.lines + links = snakemake.input.links + europe_shape = snakemake.input.europe_shape + country_shapes = snakemake.input.country_shapes + offshore_shapes = snakemake.input.offshore_shapes + config = snakemake.config + + if "links_p_nom" in snakemake.input.keys(): + links_p_nom = snakemake.input.links_p_nom + else: + links_p_nom = None + + if "parameter_corrections" in snakemake.input.keys(): + parameter_corrections = snakemake.input.parameter_corrections + else: + parameter_corrections = None + n = base_network( - snakemake.input.eg_buses, - snakemake.input.eg_converters, - snakemake.input.eg_transformers, - snakemake.input.eg_lines, - snakemake.input.eg_links, - snakemake.input.links_p_nom, - snakemake.input.europe_shape, - snakemake.input.country_shapes, - snakemake.input.offshore_shapes, - snakemake.input.parameter_corrections, - snakemake.config, + buses, + converters, + transformers, + lines, + links, + links_p_nom, + europe_shape, + country_shapes, + offshore_shapes, + parameter_corrections, + config, ) onshore_regions, offshore_regions, shapes, offshore_shapes = build_bus_shapes( n, - snakemake.input.country_shapes, - snakemake.input.offshore_shapes, - snakemake.params.countries, + country_shapes, + offshore_shapes, + countries, ) shapes.to_file(snakemake.output.regions_onshore) diff --git a/scripts/build_osm_network.py b/scripts/build_osm_network.py new file mode 100644 index 000000000..83461a98d --- /dev/null +++ b/scripts/build_osm_network.py @@ -0,0 +1,863 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2020-2024 The PyPSA-Eur and PyPSA-Earth Authors +# +# SPDX-License-Identifier: MIT + +import logging +import os +import string + +import geopandas as gpd +import numpy as np +import pandas as pd +from _helpers import configure_logging, set_scenario_config +from shapely.geometry import LineString, Point +from shapely.ops import linemerge, nearest_points, split +from tqdm import tqdm + +logger = logging.getLogger(__name__) + + +GEO_CRS = "EPSG:4326" +DISTANCE_CRS = "EPSG:3035" +BUS_TOL = ( + 5000 # unit: meters, default 5000 - Buses within this distance are grouped together +) +LINES_COLUMNS = [ + "bus0", + "bus1", + "voltage", + "circuits", + "length", + "underground", + "under_construction", + "geometry", +] +LINKS_COLUMNS = [ + "bus0", + "bus1", + "voltage", + "p_nom", + "length", + "underground", + "under_construction", + "geometry", +] +TRANSFORMERS_COLUMNS = [ + "bus0", + "bus1", + "voltage_bus0", + "voltage_bus1", + "country", + "geometry", +] + + +def line_endings_to_bus_conversion(lines): + """ + Converts line endings to bus connections. + + This function takes a df of lines and converts the line endings to bus + connections. It performs the necessary operations to ensure that the line + endings are properly connected to the buses in the network. + + Parameters: + lines (DataFrame) + + Returns: + lines (DataFrame) + """ + # Assign to every line a start and end point + + lines["bounds"] = lines["geometry"].boundary # create start and end point + + lines["bus_0_coors"] = lines["bounds"].map(lambda p: p.geoms[0]) + lines["bus_1_coors"] = lines["bounds"].map(lambda p: p.geoms[-1]) + + # splits into coordinates + lines["bus0_lon"] = lines["bus_0_coors"].x + lines["bus0_lat"] = lines["bus_0_coors"].y + lines["bus1_lon"] = lines["bus_1_coors"].x + lines["bus1_lat"] = lines["bus_1_coors"].y + + return lines + + +# tol in m +def set_substations_ids(buses, distance_crs, tol=5000): + """ + Function to set substations ids to buses, accounting for location + tolerance. + + The algorithm is as follows: + + 1. initialize all substation ids to -1 + 2. if the current substation has been already visited [substation_id < 0], then skip the calculation + 3. otherwise: + 1. identify the substations within the specified tolerance (tol) + 2. when all the substations in tolerance have substation_id < 0, then specify a new substation_id + 3. otherwise, if one of the substation in tolerance has a substation_id >= 0, then set that substation_id to all the others; + in case of multiple substations with substation_ids >= 0, the first value is picked for all + """ + + buses["station_id"] = -1 + + # create temporary series to execute distance calculations using m as reference distances + temp_bus_geom = buses.geometry.to_crs(distance_crs) + + # set tqdm options for substation ids + tqdm_kwargs_substation_ids = dict( + ascii=False, + unit=" buses", + total=buses.shape[0], + desc="Set substation ids ", + ) + + station_id = 0 + for i, row in tqdm(buses.iterrows(), **tqdm_kwargs_substation_ids): + if buses.loc[i, "station_id"] >= 0: + continue + + # get substations within tolerance + close_nodes = np.flatnonzero( + temp_bus_geom.distance(temp_bus_geom.loc[i]) <= tol + ) + + if len(close_nodes) == 1: + # if only one substation is in tolerance, then the substation is the current one iì + # Note that the node cannot be with substation_id >= 0, given the preliminary check + # at the beginning of the for loop + buses.loc[buses.index[i], "station_id"] = station_id + # update station id + station_id += 1 + else: + # several substations in tolerance + # get their ids + subset_substation_ids = buses.loc[buses.index[close_nodes], "station_id"] + # check if all substation_ids are negative (<0) + all_neg = subset_substation_ids.max() < 0 + # check if at least a substation_id is negative (<0) + some_neg = subset_substation_ids.min() < 0 + + if all_neg: + # when all substation_ids are negative, then this is a new substation id + # set the current station_id and increment the counter + buses.loc[buses.index[close_nodes], "station_id"] = station_id + station_id += 1 + elif some_neg: + # otherwise, when at least a substation_id is non-negative, then pick the first value + # and set it to all the other substations within tolerance + sub_id = -1 + for substation_id in subset_substation_ids: + if substation_id >= 0: + sub_id = substation_id + break + buses.loc[buses.index[close_nodes], "station_id"] = sub_id + + +def set_lines_ids(lines, buses, distance_crs): + """ + Function to set line buses ids to the closest bus in the list. + """ + # set tqdm options for set lines ids + tqdm_kwargs_line_ids = dict( + ascii=False, + unit=" lines", + total=lines.shape[0], + desc="Set line/link bus ids ", + ) + + # initialization + lines["bus0"] = -1 + lines["bus1"] = -1 + + busesepsg = buses.to_crs(distance_crs) + linesepsg = lines.to_crs(distance_crs) + + for i, row in tqdm(linesepsg.iterrows(), **tqdm_kwargs_line_ids): + # select buses having the voltage level of the current line + buses_sel = busesepsg[ + (buses["voltage"] == row["voltage"]) & (buses["dc"] == row["dc"]) + ] + + # find the closest node of the bus0 of the line + bus0_id = buses_sel.geometry.distance(row.geometry.boundary.geoms[0]).idxmin() + lines.loc[i, "bus0"] = buses.loc[bus0_id, "bus_id"] + + # check if the line starts exactly in the node, otherwise modify the linestring + distance_bus0 = busesepsg.geometry.loc[bus0_id].distance( + row.geometry.boundary.geoms[0] + ) + + if distance_bus0 > 0: + # the line does not start in the node, thus modify the linestring + line_start_point = lines.geometry.loc[i].boundary.geoms[0] + new_segment = LineString([buses.geometry.loc[bus0_id], line_start_point]) + modified_line = linemerge([new_segment, lines.geometry.loc[i]]) + lines.loc[i, "geometry"] = modified_line + + # find the closest node of the bus1 of the line + bus1_id = buses_sel.geometry.distance(row.geometry.boundary.geoms[1]).idxmin() + lines.loc[i, "bus1"] = buses.loc[bus1_id, "bus_id"] + + # check if the line ends exactly in the node, otherwise modify the linestring + distance_bus1 = busesepsg.geometry.loc[bus1_id].distance( + row.geometry.boundary.geoms[1] + ) + + if distance_bus1 > 0: + # the line does not start in the node, thus modify the linestring + line_end_point = lines.geometry.loc[i].boundary.geoms[1] + new_segment = LineString([line_end_point, buses.geometry.loc[bus1_id]]) + modified_line = linemerge([lines.geometry.loc[i], new_segment]) + lines.loc[i, "geometry"] = modified_line + + return lines, buses + + +def merge_stations_same_station_id( + buses, delta_lon=0.001, delta_lat=0.001, precision=4 +): + """ + Function to merge buses with same voltage and station_id This function + iterates over all substation ids and creates a bus_id for every substation + and voltage level. + + Therefore, a substation with multiple voltage levels is represented + with different buses, one per voltage level + """ + # initialize list of cleaned buses + buses_clean = [] + + # initialize the number of buses + n_buses = 0 + + for g_name, g_value in buses.groupby(by="station_id"): + # average location of the buses having the same station_id + station_point_x = np.round(g_value.geometry.x.mean(), precision) + station_point_y = np.round(g_value.geometry.y.mean(), precision) + # is_dclink_boundary_point = any(g_value["is_dclink_boundary_point"]) + + # loop for every voltage level in the bus + # The location of the buses is averaged; in the case of multiple voltage levels for the same station_id, + # each bus corresponding to a voltage level and each polatity is located at a distance regulated by delta_lon/delta_lat + v_it = 0 + for v_name, bus_row in g_value.groupby(by=["voltage", "dc"]): + lon_bus = np.round(station_point_x + v_it * delta_lon, precision) + lat_bus = np.round(station_point_y + v_it * delta_lat, precision) + + bus_data = [ + n_buses, # "bus_id" + g_name, # "station_id" + v_name[0], # "voltage" + bus_row["dc"].all(), # "dc" + "|".join(bus_row["symbol"].unique()), # "symbol" + bus_row["under_construction"].any(), # "under_construction" + "|".join(bus_row["tag_substation"].unique()), # "tag_substation" + bus_row["tag_area"].sum(), # "tag_area" + lon_bus, # "lon" + lat_bus, # "lat" + bus_row["country"].iloc[0], # "country" + Point(lon_bus, lat_bus), # "geometry" + ] + + # add the bus + buses_clean.append(bus_data) + + # increase counters + v_it += 1 + n_buses += 1 + + # names of the columns + buses_clean_columns = [ + "bus_id", + "station_id", + "voltage", + "dc", + "symbol", + "under_construction", + "tag_substation", + "tag_area", + "x", + "y", + "country", + # "is_dclink_boundary_point", + "geometry", + ] + + gdf_buses_clean = gpd.GeoDataFrame( + buses_clean, columns=buses_clean_columns + ).set_crs(crs=buses.crs, inplace=True) + + return gdf_buses_clean + + +def get_ac_frequency(df, fr_col="tag_frequency"): + """ + # Function to define a default frequency value. + + Attempts to find the most usual non-zero frequency across the + dataframe; 50 Hz is assumed as a back-up value + """ + + # Initialize a default frequency value + ac_freq_default = 50 + + grid_freq_levels = df[fr_col].value_counts(sort=True, dropna=True) + if not grid_freq_levels.empty: + # AC lines frequency shouldn't be 0Hz + ac_freq_levels = grid_freq_levels.loc[ + grid_freq_levels.index.get_level_values(0) != "0" + ] + ac_freq_default = ac_freq_levels.index.get_level_values(0)[0] + + return ac_freq_default + + +def get_transformers(buses, lines): + """ + Function to create fake transformer lines that connect buses of the same + station_id at different voltage. + """ + + ac_freq = get_ac_frequency(lines) + df_transformers = [] + + # Transformers should be added between AC buses only + buses_ac = buses[buses["dc"] != True] + + for g_name, g_value in buses_ac.sort_values("voltage", ascending=True).groupby( + by="station_id" + ): + # note: by construction there cannot be more that two buses with the same station_id and same voltage + n_voltages = len(g_value) + + if n_voltages > 1: + for id in range(n_voltages - 1): + # when g_value has more than one node, it means that there are multiple voltages for the same bus + transformer_geometry = LineString( + [g_value.geometry.iloc[id], g_value.geometry.iloc[id + 1]] + ) + + transformer_data = [ + f"transf_{g_name}_{id}", # "line_id" + g_value["bus_id"].iloc[id], # "bus0" + g_value["bus_id"].iloc[id + 1], # "bus1" + g_value.voltage.iloc[id], # "voltage_bus0" + g_value.voltage.iloc[id + 1], # "voltage_bus0" + g_value.country.iloc[id], # "country" + transformer_geometry, # "geometry" + ] + + df_transformers.append(transformer_data) + + # name of the columns + transformers_columns = [ + "transformer_id", + "bus0", + "bus1", + "voltage_bus0", + "voltage_bus1", + "country", + "geometry", + ] + + df_transformers = gpd.GeoDataFrame(df_transformers, columns=transformers_columns) + if not df_transformers.empty: + init_index = 0 if lines.empty else lines.index[-1] + 1 + df_transformers.set_index(init_index + df_transformers.index, inplace=True) + # update line endings + df_transformers = line_endings_to_bus_conversion(df_transformers) + df_transformers.drop(columns=["bounds", "bus_0_coors", "bus_1_coors"], inplace=True) + + gdf_transformers = gpd.GeoDataFrame(df_transformers) + gdf_transformers.crs = GEO_CRS + + return gdf_transformers + + +def _find_closest_bus(row, buses, distance_crs, tol=5000): + """ + Find the closest bus to a given bus based on geographical distance and + country. + + Parameters: + - row: The bus_id of the bus to find the closest bus for. + - buses: A GeoDataFrame containing information about all the buses. + - distance_crs: The coordinate reference system to use for distance calculations. + - tol: The tolerance distance within which a bus is considered closest (default: 5000). + Returns: + - closest_bus_id: The bus_id of the closest bus, or None if no bus is found within the distance and same country. + """ + gdf_buses = buses.copy() + gdf_buses = gdf_buses.to_crs(distance_crs) + # Get the geometry of the bus with bus_id = link_bus_id + bus = gdf_buses[gdf_buses["bus_id"] == row] + bus_geom = bus.geometry.values[0] + + gdf_buses_filtered = gdf_buses[gdf_buses["dc"] == False] + + # Find the closest point in the filtered buses + nearest_geom = nearest_points(bus_geom, gdf_buses_filtered.union_all())[1] + + # Get the bus_id of the closest bus + closest_bus = gdf_buses_filtered.loc[gdf_buses["geometry"] == nearest_geom] + + # check if closest_bus_id is within the distance + within_distance = ( + closest_bus.to_crs(distance_crs).distance(bus.to_crs(distance_crs), align=False) + ).values[0] <= tol + + in_same_country = closest_bus.country.values[0] == bus.country.values[0] + + if within_distance and in_same_country: + closest_bus_id = closest_bus.bus_id.values[0] + else: + closest_bus_id = None + + return closest_bus_id + + +def _get_converters(buses, links, distance_crs): + """ + Get the converters for the given buses and links. Connecting link endings + to closest AC bus. + + Parameters: + - buses (pandas.DataFrame): DataFrame containing information about buses. + - links (pandas.DataFrame): DataFrame containing information about links. + Returns: + - gdf_converters (geopandas.GeoDataFrame): GeoDataFrame containing information about converters. + """ + converters = [] + for idx, row in links.iterrows(): + for conv in range(2): + link_end = row[f"bus{conv}"] + # HVDC Gotland is connected to 130 kV grid, closest HVAC bus is further away + + closest_bus = _find_closest_bus(link_end, buses, distance_crs, tol=40000) + + if closest_bus is None: + continue + + converter_id = f"converter/{row['link_id']}_{conv}" + converter_geometry = LineString( + [ + buses[buses["bus_id"] == link_end].geometry.values[0], + buses[buses["bus_id"] == closest_bus].geometry.values[0], + ] + ) + + logger.info( + f"Added converter #{conv+1}/2 for link {row['link_id']}:{converter_id}." + ) + + converter_data = [ + converter_id, # "line_id" + link_end, # "bus0" + closest_bus, # "bus1" + row["voltage"], # "voltage" + row["p_nom"], # "p_nom" + False, # "underground" + False, # "under_construction" + buses[buses["bus_id"] == closest_bus].country.values[0], # "country" + converter_geometry, # "geometry" + ] + + # Create the converter + converters.append(converter_data) + + conv_columns = [ + "converter_id", + "bus0", + "bus1", + "voltage", + "p_nom", + "underground", + "under_construction", + "country", + "geometry", + ] + + gdf_converters = gpd.GeoDataFrame( + converters, columns=conv_columns, crs=GEO_CRS + ).reset_index() + + return gdf_converters + + +def connect_stations_same_station_id(lines, buses): + """ + Function to create fake links between substations with the same + substation_id. + """ + ac_freq = get_ac_frequency(lines) + station_id_list = buses.station_id.unique() + + add_lines = [] + from shapely.geometry import LineString + + for s_id in station_id_list: + buses_station_id = buses[buses.station_id == s_id] + + if len(buses_station_id) > 1: + for b_it in range(1, len(buses_station_id)): + line_geometry = LineString( + [ + buses_station_id.geometry.iloc[0], + buses_station_id.geometry.iloc[b_it], + ] + ) + line_bounds = line_geometry.bounds + + line_data = [ + f"link{buses_station_id}_{b_it}", # "line_id" + buses_station_id.index[0], # "bus0" + buses_station_id.index[b_it], # "bus1" + 400000, # "voltage" + 1, # "circuits" + 0.0, # "length" + False, # "underground" + False, # "under_construction" + "transmission", # "tag_type" + ac_freq, # "tag_frequency" + buses_station_id.country.iloc[0], # "country" + line_geometry, # "geometry" + line_bounds, # "bounds" + buses_station_id.geometry.iloc[0], # "bus_0_coors" + buses_station_id.geometry.iloc[b_it], # "bus_1_coors" + buses_station_id.lon.iloc[0], # "bus0_lon" + buses_station_id.lat.iloc[0], # "bus0_lat" + buses_station_id.lon.iloc[b_it], # "bus1_lon" + buses_station_id.lat.iloc[b_it], # "bus1_lat" + ] + + add_lines.append(line_data) + + # name of the columns + add_lines_columns = [ + "line_id", + "bus0", + "bus1", + "voltage", + "circuits", + "length", + "underground", + "under_construction", + "tag_type", + "tag_frequency", + "country", + "geometry", + "bounds", + "bus_0_coors", + "bus_1_coors", + "bus0_lon", + "bus0_lat", + "bus1_lon", + "bus1_lat", + ] + + df_add_lines = gpd.GeoDataFrame(pd.concat(add_lines), columns=add_lines_columns) + lines = pd.concat([lines, df_add_lines], ignore_index=True) + + return lines + + +def set_lv_substations(buses): + """ + Function to set what nodes are lv, thereby setting substation_lv The + current methodology is to set lv nodes to buses where multiple voltage + level are found, hence when the station_id is duplicated. + """ + # initialize column substation_lv to true + buses["substation_lv"] = True + + # For each station number with multiple buses make lowest voltage `substation_lv = TRUE` + bus_with_stations_duplicates = buses[ + buses.station_id.duplicated(keep=False) + ].sort_values(by=["station_id", "voltage"]) + lv_bus_at_station_duplicates = ( + buses[buses.station_id.duplicated(keep=False)] + .sort_values(by=["station_id", "voltage"]) + .drop_duplicates(subset=["station_id"]) + ) + # Set all buses with station duplicates "False" + buses.loc[bus_with_stations_duplicates.index, "substation_lv"] = False + # Set lv_buses with station duplicates "True" + buses.loc[lv_bus_at_station_duplicates.index, "substation_lv"] = True + + return buses + + +def merge_stations_lines_by_station_id_and_voltage( + lines, links, buses, distance_crs, tol=5000 +): + """ + Function to merge close stations and adapt the line datasets to adhere to + the merged dataset. + """ + + logger.info(" - Setting substation ids with tolerance of %.2f m" % (tol)) + + # bus types (AC != DC) + buses_ac = buses[buses["dc"] == False].reset_index() + buses_dc = buses[buses["dc"] == True].reset_index() + + # set_substations_ids(buses, distance_crs, tol=tol) + set_substations_ids(buses_ac, distance_crs, tol=tol) + set_substations_ids(buses_dc, distance_crs, tol=tol) + + logger.info(" - Merging substations with the same id") + + # merge buses with same station id and voltage + if not buses.empty: + buses_ac = merge_stations_same_station_id(buses_ac) + buses_dc = merge_stations_same_station_id(buses_dc) + buses_dc["bus_id"] = buses_ac["bus_id"].max() + buses_dc["bus_id"] + 1 + buses = pd.concat([buses_ac, buses_dc], ignore_index=True) + set_substations_ids(buses, distance_crs, tol=tol) + + logger.info(" - Specifying the bus ids of the line endings") + + # set the bus ids to the line dataset + lines, buses = set_lines_ids(lines, buses, distance_crs) + links, buses = set_lines_ids(links, buses, distance_crs) + + # drop lines starting and ending in the same node + lines.drop(lines[lines["bus0"] == lines["bus1"]].index, inplace=True) + links.drop(links[links["bus0"] == links["bus1"]].index, inplace=True) + # update line endings + lines = line_endings_to_bus_conversion(lines) + links = line_endings_to_bus_conversion(links) + + # set substation_lv + set_lv_substations(buses) + + # reset index + lines.reset_index(drop=True, inplace=True) + links.reset_index(drop=True, inplace=True) + + return lines, links, buses + + +def _split_linestring_by_point(linestring, points): + """ + Function to split a linestring geometry by multiple inner points. + + Parameters + ---------- + lstring : LineString + Linestring of the line to be split + points : list + List of points to split the linestring + + Return + ------ + list_lines : list + List of linestring to split the line + """ + + list_linestrings = [linestring] + + for p in points: + # execute split to all lines and store results + temp_list = [split(l, p) for l in list_linestrings] + # nest all geometries + list_linestrings = [lstring for tval in temp_list for lstring in tval.geoms] + + return list_linestrings + + +def fix_overpassing_lines(lines, buses, distance_crs, tol=1): + """ + Fix overpassing lines by splitting them at nodes within a given tolerance, + to include the buses being overpassed. + + Parameters: + - lines (GeoDataFrame): The lines to be fixed. + - buses (GeoDataFrame): The buses representing nodes. + - distance_crs (str): The coordinate reference system (CRS) for distance calculations. + - tol (float): The tolerance distance in meters for determining if a bus is within a line. + Returns: + - lines (GeoDataFrame): The fixed lines. + - buses (GeoDataFrame): The buses representing nodes. + """ + + lines_to_add = [] # list of lines to be added + lines_to_split = [] # list of lines that have been split + + lines_epsgmod = lines.to_crs(distance_crs) + buses_epsgmod = buses.to_crs(distance_crs) + + # set tqdm options for substation ids + tqdm_kwargs_substation_ids = dict( + ascii=False, + unit=" lines", + total=lines.shape[0], + desc="Verify lines overpassing nodes ", + ) + + for l in tqdm(lines.index, **tqdm_kwargs_substation_ids): + # bus indices being within tolerance from the line + bus_in_tol_epsg = buses_epsgmod[ + buses_epsgmod.geometry.distance(lines_epsgmod.geometry.loc[l]) <= tol + ] + + # Get boundary points + endpoint0 = lines_epsgmod.geometry.loc[l].boundary.geoms[0] + endpoint1 = lines_epsgmod.geometry.loc[l].boundary.geoms[1] + + # Calculate distances + dist_to_ep0 = bus_in_tol_epsg.geometry.distance(endpoint0) + dist_to_ep1 = bus_in_tol_epsg.geometry.distance(endpoint1) + + # exclude endings of the lines + bus_in_tol_epsg = bus_in_tol_epsg[(dist_to_ep0 > tol) | (dist_to_ep1 > tol)] + + if not bus_in_tol_epsg.empty: + # add index of line to split + lines_to_split.append(l) + + buses_locs = buses.geometry.loc[bus_in_tol_epsg.index] + + # get new line geometries + new_geometries = _split_linestring_by_point(lines.geometry[l], buses_locs) + n_geoms = len(new_geometries) + + # create temporary copies of the line + df_append = gpd.GeoDataFrame([lines.loc[l]] * n_geoms) + # update geometries + df_append["geometry"] = new_geometries + # update name of the line if there are multiple line segments + df_append["line_id"] = [ + str(df_append["line_id"].iloc[0]) + + (f"-{letter}" if n_geoms > 1 else "") + for letter in string.ascii_lowercase[:n_geoms] + ] + + lines_to_add.append(df_append) + + if not lines_to_add: + return lines, buses + + df_to_add = gpd.GeoDataFrame(pd.concat(lines_to_add, ignore_index=True)) + df_to_add.set_crs(lines.crs, inplace=True) + df_to_add.set_index(lines.index[-1] + df_to_add.index, inplace=True) + + # update length + df_to_add["length"] = df_to_add.to_crs(distance_crs).geometry.length + + # update line endings + df_to_add = line_endings_to_bus_conversion(df_to_add) + + # remove original lines + lines.drop(lines_to_split, inplace=True) + + lines = df_to_add if lines.empty else pd.concat([lines, df_to_add]) + + lines = gpd.GeoDataFrame(lines.reset_index(drop=True), crs=lines.crs) + + return lines, buses + + +def build_network(inputs, outputs): + + logger.info("Reading input data.") + buses = gpd.read_file(inputs["substations"]) + lines = gpd.read_file(inputs["lines"]) + links = gpd.read_file(inputs["links"]) + + lines = line_endings_to_bus_conversion(lines) + links = line_endings_to_bus_conversion(links) + + logger.info( + "Fixing lines overpassing nodes: Connecting nodes and splittling lines." + ) + lines, buses = fix_overpassing_lines(lines, buses, DISTANCE_CRS, tol=1) + + # Merge buses with same voltage and within tolerance + logger.info(f"Aggregating close substations with a tolerance of {BUS_TOL} m") + + lines, links, buses = merge_stations_lines_by_station_id_and_voltage( + lines, links, buses, DISTANCE_CRS, BUS_TOL + ) + + # Recalculate lengths of lines + utm = lines.estimate_utm_crs(datum_name="WGS 84") + lines["length"] = lines.to_crs(utm).length + links["length"] = links.to_crs(utm).length + + transformers = get_transformers(buses, lines) + converters = _get_converters(buses, links, DISTANCE_CRS) + + logger.info("Saving outputs") + + ### Convert output to pypsa-eur friendly format + # Rename "substation" in buses["symbol"] to "Substation" + buses["symbol"] = buses["symbol"].replace({"substation": "Substation"}) + + # Drop unnecessary index column and set respective element ids as index + lines.set_index("line_id", inplace=True) + if not links.empty: + links.set_index("link_id", inplace=True) + converters.set_index("converter_id", inplace=True) + transformers.set_index("transformer_id", inplace=True) + buses.set_index("bus_id", inplace=True) + + # Convert voltages from V to kV + lines["voltage"] = lines["voltage"] / 1000 + if not links.empty: + links["voltage"] = links["voltage"] / 1000 + if not converters.empty: + converters["voltage"] = converters["voltage"] / 1000 + transformers["voltage_bus0"], transformers["voltage_bus1"] = ( + transformers["voltage_bus0"] / 1000, + transformers["voltage_bus1"] / 1000, + ) + buses["voltage"] = buses["voltage"] / 1000 + + # Convert 'true' and 'false' to 't' and 'f' + lines = lines.replace({True: "t", False: "f"}) + links = links.replace({True: "t", False: "f"}) + converters = converters.replace({True: "t", False: "f"}) + buses = buses.replace({True: "t", False: "f"}) + + # Change column orders + lines = lines[LINES_COLUMNS] + if not links.empty: + links = links[LINKS_COLUMNS] + else: + links = pd.DataFrame(columns=["link_id"] + LINKS_COLUMNS) + links.set_index("link_id", inplace=True) + transformers = transformers[TRANSFORMERS_COLUMNS] + + # Export to csv for base_network + buses.to_csv(outputs["substations"], quotechar="'") + lines.to_csv(outputs["lines"], quotechar="'") + links.to_csv(outputs["links"], quotechar="'") + converters.to_csv(outputs["converters"], quotechar="'") + transformers.to_csv(outputs["transformers"], quotechar="'") + + # Export to GeoJSON for quick validations + buses.to_file(outputs["substations_geojson"]) + lines.to_file(outputs["lines_geojson"]) + links.to_file(outputs["links_geojson"]) + converters.to_file(outputs["converters_geojson"]) + transformers.to_file(outputs["transformers_geojson"]) + + return None + + +if __name__ == "__main__": + # Detect running outside of snakemake and mock snakemake for testing + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake("build_osm_network") + + configure_logging(snakemake) + set_scenario_config(snakemake) + + countries = snakemake.config["countries"] + + build_network(snakemake.input, snakemake.output) diff --git a/scripts/clean_osm_data.py b/scripts/clean_osm_data.py new file mode 100644 index 000000000..8669d0af5 --- /dev/null +++ b/scripts/clean_osm_data.py @@ -0,0 +1,1807 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2020-2024 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT +""" +This script is used to clean OpenStreetMap (OSM) data for creating a PyPSA-Eur +ready network. + +The script performs various cleaning operations on the OSM data, including: +- Cleaning voltage, circuits, cables, wires, and frequency columns +- Splitting semicolon-separated cells into new rows +- Distributing values to circuits based on the number of splits +- Adding line endings to substations based on line data +""" + +import json +import logging +import os +import re + +import geopandas as gpd +import numpy as np +import pandas as pd +from _helpers import configure_logging, set_scenario_config +from shapely.geometry import LineString, MultiLineString, Point, Polygon +from shapely.ops import linemerge, unary_union + +logger = logging.getLogger(__name__) + + +def _create_linestring(row): + """ + Create a LineString object from the given row. + + Args: + row (dict): A dictionary containing the row data. + + Returns: + LineString: A LineString object representing the geometry. + """ + coords = [(coord["lon"], coord["lat"]) for coord in row["geometry"]] + return LineString(coords) + + +def _create_polygon(row): + """ + Create a Shapely Polygon from a list of coordinate dictionaries. + + Parameters: + coords (list): List of dictionaries with 'lat' and 'lon' keys + representing coordinates. + + Returns: + shapely.geometry.Polygon: The constructed polygon object. + """ + # Extract coordinates as tuples + point_coords = [(coord["lon"], coord["lat"]) for coord in row["geometry"]] + + # Ensure closure by repeating the first coordinate as the last coordinate + if point_coords[0] != point_coords[-1]: + point_coords.append(point_coords[0]) + + # Create Polygon object + polygon = Polygon(point_coords) + + return polygon + + +def _find_closest_polygon(gdf, point): + """ + Find the closest polygon in a GeoDataFrame to a given point. + + Parameters: + gdf (GeoDataFrame): A GeoDataFrame containing polygons. + point (Point): A Point object representing the target point. + + Returns: + int: The index of the closest polygon in the GeoDataFrame. + """ + # Compute the distance to each polygon + gdf["distance"] = gdf["geometry"].apply(lambda geom: point.distance(geom)) + + # Find the index of the closest polygon + closest_idx = gdf["distance"].idxmin() + + # Get the closest polygon's row + closest_polygon = gdf.loc[closest_idx] + + return closest_idx + + +def _clean_voltage(column): + """ + Function to clean the raw voltage column: manual fixing and drop nan values + + Args: + - column: pandas Series, the column to be cleaned + + Returns: + - column: pandas Series, the cleaned column + """ + logger.info("Cleaning voltages.") + column = column.copy() + + column = ( + column.astype(str) + .str.lower() + .str.replace("400/220/110 kV'", "400000;220000;110000") + .str.replace("400/220/110/20_kv", "400000;220000;110000;20000") + .str.replace("2x25000", "25000;25000") + .str.replace("é", ";") + ) + + column = ( + column.astype(str) + .str.lower() + .str.replace("(temp 150000)", "") + .str.replace("low", "1000") + .str.replace("minor", "1000") + .str.replace("medium", "33000") + .str.replace("med", "33000") + .str.replace("m", "33000") + .str.replace("high", "150000") + .str.replace("23000-109000", "109000") + .str.replace("380000>220000", "380000;220000") + .str.replace(":", ";") + .str.replace("<", ";") + .str.replace(",", ";") + .str.replace("kv", "000") + .str.replace("kva", "000") + .str.replace("/", ";") + .str.replace("nan", "") + .str.replace("", "") + ) + + # Remove all remaining non-numeric characters except for semicolons + column = column.apply(lambda x: re.sub(r"[^0-9;]", "", str(x))) + + column.dropna(inplace=True) + return column + + +def _clean_circuits(column): + """ + Function to clean the raw circuits column: manual fixing and drop nan + values + + Args: + - column: pandas Series, the column to be cleaned + + Returns: + - column: pandas Series, the cleaned column + """ + logger.info("Cleaning circuits.") + column = column.copy() + column = ( + column.astype(str) + .str.replace("partial", "") + .str.replace("1operator=RTE operator:wikidata=Q2178795", "") + .str.lower() + .str.replace("1,5", "3") + .str.replace("1/3", "1") + .str.replace("", "") + .str.replace("nan", "") + ) + + # Remove all remaining non-numeric characters except for semicolons + column = column.apply(lambda x: re.sub(r"[^0-9;]", "", x)) + + column.dropna(inplace=True) + return column.astype(str) + + +def _clean_cables(column): + """ + Function to clean the raw cables column: manual fixing and drop nan values + + Args: + - column: pandas Series, the column to be cleaned + + Returns: + - column: pandas Series, the cleaned column + """ + logger.info("Cleaning cables.") + column = column.copy() + column = ( + column.astype(str) + .str.lower() + .str.replace("1/3", "1") + .str.replace("3x2;2", "3") + .str.replace("", "") + .str.replace("nan", "") + ) + + # Remove all remaining non-numeric characters except for semicolons + column = column.apply(lambda x: re.sub(r"[^0-9;]", "", x)) + + column.dropna(inplace=True) + return column.astype(str) + + +def _clean_wires(column): + """ + Function to clean the raw wires column: manual fixing and drop nan values + + Args: + - column: pandas Series, the column to be cleaned + + Returns: + - column: pandas Series, the cleaned column + """ + logger.info("Cleaning wires.") + column = column.copy() + column = ( + column.astype(str) + .str.lower() + .str.replace("?", "") + .str.replace("trzyprzewodowe", "3") + .str.replace("pojedyńcze", "1") + .str.replace("single", "1") + .str.replace("double", "2") + .str.replace("triple", "3") + .str.replace("quad", "4") + .str.replace("fivefold", "5") + .str.replace("yes", "3") + .str.replace("1/3", "1") + .str.replace("3x2;2", "3") + .str.replace("_", "") + .str.replace("", "") + .str.replace("nan", "") + ) + + # Remove all remaining non-numeric characters except for semicolons + column = column.apply(lambda x: re.sub(r"[^0-9;]", "", x)) + + column.dropna(inplace=True) + return column.astype(str) + + +def _check_voltage(voltage, list_voltages): + """ + Check if the given voltage is present in the list of allowed voltages. + + Parameters: + voltage (str): The voltage to check. + list_voltages (list): A list of allowed voltages. + + Returns: + bool: True if the voltage is present in the list of allowed voltages, + False otherwise. + """ + voltages = voltage.split(";") + for v in voltages: + if v in list_voltages: + return True + return False + + +def _clean_frequency(column): + """ + Function to clean the raw frequency column: manual fixing and drop nan + values + + Args: + - column: pandas Series, the column to be cleaned + + Returns: + - column: pandas Series, the cleaned column + """ + logger.info("Cleaning frequencies.") + column = column.copy() + column = ( + column.astype(str) + .str.lower() + .str.replace("16.67", "16.7") + .str.replace("16,7", "16.7") + .str.replace("?", "") + .str.replace("hz", "") + .str.replace(" ", "") + .str.replace("", "") + .str.replace("nan", "") + ) + + # Remove all remaining non-numeric characters except for semicolons + column = column.apply(lambda x: re.sub(r"[^0-9;.]", "", x)) + + column.dropna(inplace=True) + return column.astype(str) + + +def _clean_rating(column): + """ + Function to clean and sum the rating columns: + + Args: + - column: pandas Series, the column to be cleaned + + Returns: + - column: pandas Series, the cleaned column + """ + logger.info("Cleaning ratings.") + column = column.copy() + column = column.astype(str).str.replace("MW", "") + + # Remove all remaining non-numeric characters except for semicolons + column = column.apply(lambda x: re.sub(r"[^0-9;]", "", x)) + + # Sum up all ratings if there are multiple entries + column = column.str.split(";").apply(lambda x: sum([int(i) for i in x])) + + column.dropna(inplace=True) + return column.astype(str) + + +def _split_cells(df, cols=["voltage"]): + """ + Split semicolon separated cells i.e. [66000;220000] and create new + identical rows. + + Parameters + ---------- + df : dataframe + Dataframe under analysis + cols : list + List of target columns over which to perform the analysis + + Example + ------- + Original data: + row 1: '66000;220000', '50' + + After applying split_cells(): + row 1, '66000', '50', 2 + row 2, '220000', '50', 2 + """ + if df.empty: + return df + + # Create a dictionary to store the suffix count for each original ID + suffix_counts = {} + # Create a dictionary to store the number of splits associated with each + # original ID + num_splits = {} + + # Split cells and create new rows + x = df.assign(**{col: df[col].str.split(";") for col in cols}) + x = x.explode(cols, ignore_index=True) + + # Count the number of splits associated with each original ID + num_splits = x.groupby("id").size().to_dict() + + # Update the 'split_elements' column + x["split_elements"] = x["id"].map(num_splits) + + # Function to generate the new ID with suffix and update the number of + # splits + def generate_new_id(row): + original_id = row["id"] + if row["split_elements"] == 1: + return original_id + else: + suffix_counts[original_id] = suffix_counts.get(original_id, 0) + 1 + return f"{original_id}-{suffix_counts[original_id]}" + + # Update the ID column with the new IDs + x["id"] = x.apply(generate_new_id, axis=1) + + return x + + +def _distribute_to_circuits(row): + """ + Distributes the number of circuits or cables to individual circuits based + on the given row data. + + Parameters: + - row: A dictionary representing a row of data containing information about + circuits and cables. + + Returns: + - single_circuit: The number of circuits to be assigned to each individual + circuit. + """ + if row["circuits"] != "": + circuits = int(row["circuits"]) + else: + cables = int(row["cables"]) + circuits = cables / 3 + + single_circuit = int(max(1, np.floor_divide(circuits, row["split_elements"]))) + single_circuit = str(single_circuit) + + return single_circuit + + +def _add_line_endings_to_substations( + df_substations, + gdf_lines, + path_country_shapes, + path_offshore_shapes, + prefix, +): + """ + Add line endings to substations. + + This function takes two pandas DataFrames, `substations` and `lines`, and + adds line endings to the substations based on the information from the + lines DataFrame. + + Parameters: + - substations (pandas DataFrame): DataFrame containing information about + substations. + - lines (pandas DataFrame): DataFrame containing information about lines. + + Returns: + - buses (pandas DataFrame): DataFrame containing the updated information + about substations with line endings. + """ + if gdf_lines.empty: + return df_substations + + logger.info("Adding line endings to substations") + # extract columns from df_substations + bus_s = pd.DataFrame(columns=df_substations.columns) + bus_e = pd.DataFrame(columns=df_substations.columns) + + # TODO pypsa-eur: fix country code to contain single country code + # Read information from gdf_lines + bus_s[["voltage", "country"]] = gdf_lines[["voltage", "country"]] + bus_s.loc[:, "geometry"] = gdf_lines.geometry.boundary.map( + lambda p: p.geoms[0] if len(p.geoms) >= 2 else None + ) + bus_s.loc[:, "lon"] = bus_s["geometry"].map(lambda p: p.x if p != None else None) + bus_s.loc[:, "lat"] = bus_s["geometry"].map(lambda p: p.y if p != None else None) + bus_s.loc[:, "dc"] = gdf_lines["dc"] + + bus_e[["voltage", "country"]] = gdf_lines[["voltage", "country"]] + bus_e.loc[:, "geometry"] = gdf_lines.geometry.boundary.map( + lambda p: p.geoms[1] if len(p.geoms) >= 2 else None + ) + bus_e.loc[:, "lon"] = bus_e["geometry"].map(lambda p: p.x if p != None else None) + bus_e.loc[:, "lat"] = bus_e["geometry"].map(lambda p: p.y if p != None else None) + bus_e.loc[:, "dc"] = gdf_lines["dc"] + + bus_all = pd.concat([bus_s, bus_e], ignore_index=True) + + # Group gdf_substations by voltage and and geometry (dropping duplicates) + bus_all = bus_all.groupby(["voltage", "lon", "lat", "dc"]).first().reset_index() + bus_all = bus_all[df_substations.columns] + bus_all.loc[:, "bus_id"] = bus_all.apply( + lambda row: f"{prefix}/{row.name + 1}", axis=1 + ) + + # Initialize default values + bus_all["station_id"] = None + # Assuming substations completed for installed lines + bus_all["under_construction"] = False + bus_all["tag_area"] = None + bus_all["symbol"] = "substation" + # TODO: this tag may be improved, maybe depending on voltage levels + bus_all["tag_substation"] = "transmission" + bus_all["tag_source"] = prefix + + buses = pd.concat([df_substations, bus_all], ignore_index=True) + buses.set_index("bus_id", inplace=True) + + # Fix country codes + # TODO pypsa-eur: Temporary solution as long as the shapes have a low, + # incomplete resolution (cf. 2500 meters for buffering) + bool_multiple_countries = buses["country"].str.contains(";") + gdf_offshore = gpd.read_file(path_offshore_shapes).set_index("name")["geometry"] + gdf_offshore = gpd.GeoDataFrame( + gdf_offshore, geometry=gdf_offshore, crs=gdf_offshore.crs + ) + gdf_countries = gpd.read_file(path_country_shapes).set_index("name")["geometry"] + # reproject to enable buffer + gdf_countries = gpd.GeoDataFrame(geometry=gdf_countries, crs=gdf_countries.crs) + gdf_union = gdf_countries.merge( + gdf_offshore, how="outer", left_index=True, right_index=True + ) + gdf_union["geometry"] = gdf_union.apply( + lambda row: gpd.GeoSeries([row["geometry_x"], row["geometry_y"]]).union_all(), + axis=1, + ) + gdf_union = gpd.GeoDataFrame(geometry=gdf_union["geometry"], crs=crs) + gdf_buses_tofix = gpd.GeoDataFrame( + buses[bool_multiple_countries], geometry="geometry", crs=crs + ) + joined = gpd.sjoin( + gdf_buses_tofix, gdf_union.reset_index(), how="left", predicate="within" + ) + + # For all remaining rows where the country/index_right column is NaN, find + # find the closest polygon index + joined.loc[joined["name"].isna(), "name"] = joined.loc[ + joined["name"].isna(), "geometry" + ].apply(lambda x: _find_closest_polygon(gdf_union, x)) + + joined.reset_index(inplace=True) + joined = joined.drop_duplicates(subset="bus_id") + joined.set_index("bus_id", inplace=True) + + buses.loc[bool_multiple_countries, "country"] = joined.loc[ + bool_multiple_countries, "name" + ] + + return buses.reset_index() + + +def _import_lines_and_cables(path_lines): + """ + Import lines and cables from the given input paths. + + Parameters: + - path_lines (dict): A dictionary containing the input paths for lines and + cables data. + + Returns: + - df_lines (DataFrame): A DataFrame containing the imported lines and + cables data. + """ + columns = [ + "id", + "bounds", + "nodes", + "geometry", + "country", + "power", + "cables", + "circuits", + "frequency", + "voltage", + "wires", + ] + df_lines = pd.DataFrame(columns=columns) + + logger.info("Importing lines and cables") + for key in path_lines: + logger.info(f"Processing {key}...") + for idx, ip in enumerate(path_lines[key]): + if ( + os.path.exists(ip) and os.path.getsize(ip) > 400 + ): # unpopulated OSM json is about 51 bytes + country = os.path.basename(os.path.dirname(path_lines[key][idx])) + + logger.info( + f" - Importing {key} {str(idx+1).zfill(2)}/{str(len(path_lines[key])).zfill(2)}: {ip}" + ) + with open(ip, "r") as f: + data = json.load(f) + + df = pd.DataFrame(data["elements"]) + df["id"] = df["id"].astype(str) + df["country"] = country + + col_tags = [ + "power", + "cables", + "circuits", + "frequency", + "voltage", + "wires", + ] + + tags = pd.json_normalize(df["tags"]).map( + lambda x: str(x) if pd.notnull(x) else x + ) + + for ct in col_tags: + if ct not in tags.columns: + tags[ct] = pd.NA + + tags = tags.loc[:, col_tags] + + df = pd.concat([df, tags], axis="columns") + df.drop(columns=["type", "tags"], inplace=True) + + df_lines = pd.concat([df_lines, df], axis="rows") + + else: + logger.info( + f" - Skipping {key} {str(idx+1).zfill(2)}/{str(len(path_lines[key])).zfill(2)} (empty): {ip}" + ) + continue + logger.info("---") + + return df_lines + + +def _import_links(path_links): + """ + Import links from the given input paths. + + Parameters: + - path_links (dict): A dictionary containing the input paths for links. + + Returns: + - df_links (DataFrame): A DataFrame containing the imported links data. + """ + columns = [ + "id", + "bounds", + "nodes", + "geometry", + "country", + "circuits", + "frequency", + "rating", + "voltage", + ] + df_links = pd.DataFrame(columns=columns) + + logger.info("Importing links") + for key in path_links: + logger.info(f"Processing {key}...") + for idx, ip in enumerate(path_links[key]): + if ( + os.path.exists(ip) and os.path.getsize(ip) > 400 + ): # unpopulated OSM json is about 51 bytes + country = os.path.basename(os.path.dirname(path_links[key][idx])) + + logger.info( + f" - Importing {key} {str(idx+1).zfill(2)}/{str(len(path_links[key])).zfill(2)}: {ip}" + ) + with open(ip, "r") as f: + data = json.load(f) + + df = pd.DataFrame(data["elements"]) + df["id"] = df["id"].astype(str) + df["id"] = df["id"].apply(lambda x: (f"relation/{x}")) + df["country"] = country + + col_tags = [ + "circuits", + "frequency", + "rating", + "voltage", + ] + + tags = pd.json_normalize(df["tags"]).map( + lambda x: str(x) if pd.notnull(x) else x + ) + + for ct in col_tags: + if ct not in tags.columns: + tags[ct] = pd.NA + + tags = tags.loc[:, col_tags] + + df = pd.concat([df, tags], axis="columns") + df.drop(columns=["type", "tags"], inplace=True) + + df_links = pd.concat([df_links, df], axis="rows") + + else: + logger.info( + f" - Skipping {key} {str(idx+1).zfill(2)}/{str(len(path_links[key])).zfill(2)} (empty): {ip}" + ) + continue + logger.info("---") + logger.info("Dropping lines without rating.") + len_before = len(df_links) + df_links = df_links.dropna(subset=["rating"]) + len_after = len(df_links) + logger.info( + f"Dropped {len_before-len_after} elements without rating. " + + f"Imported {len_after} elements." + ) + + return df_links + + +def _create_single_link(row): + """ + Create a single link from multiple rows within a OSM link relation. + + Parameters: + - row: A row of OSM data containing information about the link. + + Returns: + - single_link: A single LineString representing the link. + + This function takes a row of OSM data and extracts the relevant information + to create a single link. It filters out elements (substations, electrodes) + with invalid roles and finds the longest link based on its endpoints. + If the longest link is a MultiLineString, it extracts the longest + linestring from it. The resulting single link is returned. + """ + valid_roles = ["line", "cable"] + df = pd.json_normalize(row["members"]) + df = df[df["role"].isin(valid_roles)] + df.loc[:, "geometry"] = df.apply(_create_linestring, axis=1) + df.loc[:, "length"] = df["geometry"].apply(lambda x: x.length) + + list_endpoints = [] + for idx, row in df.iterrows(): + tuple = sorted([row["geometry"].coords[0], row["geometry"].coords[-1]]) + # round tuple to 3 decimals + tuple = ( + round(tuple[0][0], 3), + round(tuple[0][1], 3), + round(tuple[1][0], 3), + round(tuple[1][1], 3), + ) + list_endpoints.append(tuple) + + df.loc[:, "endpoints"] = list_endpoints + df_longest = df.loc[df.groupby("endpoints")["length"].idxmin()] + + single_link = linemerge(df_longest["geometry"].values.tolist()) + + # If the longest component is a MultiLineString, extract the longest linestring from it + if isinstance(single_link, MultiLineString): + # Find connected components + components = list(single_link.geoms) + + # Find the longest connected linestring + single_link = max(components, key=lambda x: x.length) + + return single_link + + +def _drop_duplicate_lines(df_lines): + """ + Drop duplicate lines from the given dataframe. Duplicates are usually lines + cross-border lines or slightly outside the country border of focus. + + Parameters: + - df_lines (pandas.DataFrame): The dataframe containing lines data. + + Returns: + - df_lines (pandas.DataFrame): The dataframe with duplicate lines removed + and cleaned data. + + This function drops duplicate lines from the given dataframe based on the + 'id' column. It groups the duplicate rows by 'id' and aggregates the + 'country' column to a string split by semicolon, as they appear in multiple + country datasets. One example of the duplicates is kept, accordingly. + Finally, the updated dataframe without multiple duplicates is returned. + """ + logger.info("Dropping duplicate lines.") + duplicate_rows = df_lines[df_lines.duplicated(subset=["id"], keep=False)].copy() + + # Group rows by id and aggregate the country column to a string split by semicolon + grouped_duplicates = ( + duplicate_rows.groupby("id")["country"].agg(lambda x: ";".join(x)).reset_index() + ) + duplicate_rows.drop_duplicates(subset="id", inplace=True) + duplicate_rows.drop(columns=["country"], inplace=True) + duplicate_rows = duplicate_rows.join( + grouped_duplicates.set_index("id"), on="id", how="left" + ) + + len_before = len(df_lines) + # Drop duplicates and update the df_lines dataframe with the cleaned data + df_lines = df_lines[~df_lines["id"].isin(duplicate_rows["id"])] + df_lines = pd.concat([df_lines, duplicate_rows], axis="rows") + len_after = len(df_lines) + + logger.info( + f"Dropped {len_before - len_after} duplicate elements. " + + f"Keeping {len_after} elements." + ) + + return df_lines + + +def _filter_by_voltage(df, min_voltage=200000): + """ + Filter rows in the DataFrame based on the voltage in V. + + Parameters: + - df (pandas.DataFrame): The DataFrame containing the substations or lines data. + - min_voltage (int, optional): The minimum voltage value to filter the + rows. Defaults to 200000 [unit: V]. + + Returns: + - filtered df (pandas.DataFrame): The filtered DataFrame containing + the lines or substations above min_voltage. + - list_voltages (list): A list of unique voltage values above min_voltage. + The type of the list elements is string. + """ + if df.empty: + return df, [] + + logger.info( + f"Filtering dataframe by voltage. Only keeping rows above and including {min_voltage} V." + ) + list_voltages = df["voltage"].str.split(";").explode().unique().astype(str) + # Keep numeric strings + list_voltages = list_voltages[np.vectorize(str.isnumeric)(list_voltages)] + list_voltages = list_voltages.astype(int) + list_voltages = list_voltages[list_voltages >= int(min_voltage)] + list_voltages = list_voltages.astype(str) + + bool_voltages = df["voltage"].apply(_check_voltage, list_voltages=list_voltages) + len_before = len(df) + df = df[bool_voltages] + len_after = len(df) + logger.info( + f"Dropped {len_before - len_after} elements with voltage below {min_voltage}. " + + f"Keeping {len_after} elements." + ) + + return df, list_voltages + + +def _clean_substations(df_substations, list_voltages): + """ + Clean the substation data by performing the following steps: + - Split cells in the dataframe. + - Filter substation data based on specified voltages. + - Update the frequency values based on the split count. + - Split cells in the 'frequency' column. + - Set remaining invalid frequency values that are not in ['0', '50'] + to '50'. + + Parameters: + - df_substations (pandas.DataFrame): The input dataframe containing + substation data. + - list_voltages (list): A list of voltages above min_voltage to filter the + substation data. + + Returns: + - df_substations (pandas.DataFrame): The cleaned substation dataframe. + """ + df_substations = df_substations.copy() + + df_substations = _split_cells(df_substations) + + bool_voltages = df_substations["voltage"].apply( + _check_voltage, list_voltages=list_voltages + ) + df_substations = df_substations[bool_voltages] + df_substations.loc[:, "split_count"] = df_substations["id"].apply( + lambda x: x.split("-")[1] if "-" in x else "0" + ) + df_substations.loc[:, "split_count"] = df_substations["split_count"].astype(int) + + bool_split = df_substations["split_elements"] > 1 + bool_frequency_len = ( + df_substations["frequency"].apply(lambda x: len(x.split(";"))) + == df_substations["split_elements"] + ) + + op_freq = lambda row: row["frequency"].split(";")[row["split_count"] - 1] + + df_substations.loc[bool_frequency_len & bool_split, "frequency"] = ( + df_substations.loc[bool_frequency_len & bool_split,].apply(op_freq, axis=1) + ) + + df_substations = _split_cells(df_substations, cols=["frequency"]) + bool_invalid_frequency = df_substations["frequency"].apply( + lambda x: x not in ["50", "0"] + ) + df_substations.loc[bool_invalid_frequency, "frequency"] = "50" + + return df_substations + + +def _clean_lines(df_lines, list_voltages): + """ + Cleans and processes the `df_lines` DataFrame heuristically based on the + information available per respective line and cable. Further checks to + ensure data consistency and completeness. + + Parameters + ---------- + df_lines : pandas.DataFrame + The input DataFrame containing line information with columns such as + 'voltage', 'circuits', 'frequency', 'cables', 'split_elements', 'id', + etc. + list_voltages : list + A list of unique voltage values above a certain threshold. (type: str) + + Returns + ------- + df_lines : pandas.DataFrame + The cleaned DataFrame with updated columns 'circuits', 'frequency', and + 'cleaned' to reflect the applied transformations. + + Description + ----------- + This function performs the following operations: + + - Initializes a 'cleaned' column with False, step-wise updates to True + following the respective cleaning step. + - Splits the voltage cells in the DataFrame at semicolons using a helper + function `_split_cells`. + - Filters the DataFrame to only include rows with valid voltages. + - Sets circuits of remaining lines without any applicable heuristic equal + to 1. + + The function ensures that the resulting DataFrame has consistent and + complete information for further processing or analysis while maintaining + the data of the original OSM data set wherever possible. + """ + logger.info("Cleaning lines and determining circuits.") + # Initiate boolean with False, only set to true if all cleaning steps are + # passed + df_lines = df_lines.copy() + df_lines["cleaned"] = False + + df_lines["voltage_original"] = df_lines["voltage"] + df_lines["circuits_original"] = df_lines["circuits"] + + df_lines = _split_cells(df_lines) + bool_voltages = df_lines["voltage"].apply( + _check_voltage, list_voltages=list_voltages + ) + df_lines = df_lines[bool_voltages] + + bool_ac = df_lines["frequency"] != "0" + bool_dc = ~bool_ac + valid_frequency = ["50", "0"] + bool_invalid_frequency = df_lines["frequency"].apply( + lambda x: x not in valid_frequency + ) + + bool_noinfo = (df_lines["cables"] == "") & (df_lines["circuits"] == "") + # Fill in all values where cables info and circuits does not exist. Assuming 1 circuit + df_lines.loc[bool_noinfo, "circuits"] = "1" + df_lines.loc[bool_noinfo & bool_invalid_frequency, "frequency"] = "50" + df_lines.loc[bool_noinfo, "cleaned"] = True + + # Fill in all values where cables info exists and split_elements == 1 + bool_cables_ac = ( + (df_lines["cables"] != "") + & (df_lines["split_elements"] == 1) + & (df_lines["cables"] != "0") + & (df_lines["cables"].apply(lambda x: len(x.split(";")) == 1)) + & (df_lines["circuits"] == "") + & (df_lines["cleaned"] == False) + & bool_ac + ) + + df_lines.loc[bool_cables_ac, "circuits"] = df_lines.loc[ + bool_cables_ac, "cables" + ].apply(lambda x: str(int(max(1, np.floor_divide(int(x), 3))))) + + df_lines.loc[bool_cables_ac, "frequency"] = "50" + df_lines.loc[bool_cables_ac, "cleaned"] = True + + bool_cables_dc = ( + (df_lines["cables"] != "") + & (df_lines["split_elements"] == 1) + & (df_lines["cables"] != "0") + & (df_lines["cables"].apply(lambda x: len(x.split(";")) == 1)) + & (df_lines["circuits"] == "") + & (df_lines["cleaned"] == False) + & bool_dc + ) + + df_lines.loc[bool_cables_dc, "circuits"] = df_lines.loc[ + bool_cables_dc, "cables" + ].apply(lambda x: str(int(max(1, np.floor_divide(int(x), 2))))) + + df_lines.loc[bool_cables_dc, "frequency"] = "0" + df_lines.loc[bool_cables_dc, "cleaned"] = True + + # Fill in all values where circuits info exists and split_elements == 1 + bool_lines = ( + (df_lines["circuits"] != "") + & (df_lines["split_elements"] == 1) + & (df_lines["circuits"] != "0") + & (df_lines["circuits"].apply(lambda x: len(x.split(";")) == 1)) + & (df_lines["cleaned"] == False) + ) + + df_lines.loc[bool_lines & bool_ac, "frequency"] = "50" + df_lines.loc[bool_lines & bool_dc, "frequency"] = "0" + df_lines.loc[bool_lines, "cleaned"] = True + + # Clean those values where number of voltages split by semicolon is larger + # than no cables or no circuits + bool_cables = ( + (df_lines["voltage_original"].apply(lambda x: len(x.split(";")) > 1)) + & (df_lines["cables"].apply(lambda x: len(x.split(";")) == 1)) + & (df_lines["circuits"].apply(lambda x: len(x.split(";")) == 1)) + & (df_lines["cleaned"] == False) + ) + + df_lines.loc[bool_cables, "circuits"] = df_lines[bool_cables].apply( + _distribute_to_circuits, axis=1 + ) + df_lines.loc[bool_cables & bool_ac, "frequency"] = "50" + df_lines.loc[bool_cables & bool_dc, "frequency"] = "0" + df_lines.loc[bool_cables, "cleaned"] = True + + # Clean those values where multiple circuit values are present, divided by + # semicolon + has_multiple_circuits = df_lines["circuits"].apply(lambda x: len(x.split(";")) > 1) + circuits_match_split_elements = df_lines.apply( + lambda row: len(row["circuits"].split(";")) == row["split_elements"], + axis=1, + ) + is_not_cleaned = df_lines["cleaned"] == False + bool_cables = has_multiple_circuits & circuits_match_split_elements & is_not_cleaned + + df_lines.loc[bool_cables, "circuits"] = df_lines.loc[bool_cables].apply( + lambda row: str(row["circuits"].split(";")[int(row["id"].split("-")[-1]) - 1]), + axis=1, + ) + + df_lines.loc[bool_cables & bool_ac, "frequency"] = "50" + df_lines.loc[bool_cables & bool_dc, "frequency"] = "0" + df_lines.loc[bool_cables, "cleaned"] = True + + # Clean those values where multiple cables values are present, divided by + # semicolon + has_multiple_cables = df_lines["cables"].apply(lambda x: len(x.split(";")) > 1) + cables_match_split_elements = df_lines.apply( + lambda row: len(row["cables"].split(";")) == row["split_elements"], + axis=1, + ) + is_not_cleaned = df_lines["cleaned"] == False + bool_cables = has_multiple_cables & cables_match_split_elements & is_not_cleaned + + df_lines.loc[bool_cables, "circuits"] = df_lines.loc[bool_cables].apply( + lambda row: str( + max( + 1, + np.floor_divide( + int(row["cables"].split(";")[int(row["id"].split("-")[-1]) - 1]), 3 + ), + ) + ), + axis=1, + ) + + df_lines.loc[bool_cables & bool_ac, "frequency"] = "50" + df_lines.loc[bool_cables & bool_dc, "frequency"] = "0" + df_lines.loc[bool_cables, "cleaned"] = True + + # All remaining lines to circuits == 1 + bool_leftover = df_lines["cleaned"] == False + if sum(bool_leftover) > 0: + str_id = "; ".join(str(id) for id in df_lines.loc[bool_leftover, "id"]) + logger.info(f"Setting circuits of remaining {sum(bool_leftover)} lines to 1...") + logger.info(f"Lines affected: {str_id}") + + df_lines.loc[bool_leftover, "circuits"] = "1" + df_lines.loc[bool_leftover & bool_ac, "frequency"] = "50" + df_lines.loc[bool_leftover & bool_dc, "frequency"] = "0" + df_lines.loc[bool_leftover, "cleaned"] = True + + return df_lines + + +def _create_substations_geometry(df_substations): + """ + Creates geometries. + + Parameters: + df_substations (DataFrame): The input DataFrame containing the substations + data. + + Returns: + df_substations (DataFrame): A new DataFrame with the + polygons ["polygon"] of the substations geometries. + """ + logger.info("Creating substations geometry.") + df_substations = df_substations.copy() + + # Create centroids from geometries and keep the original polygons + df_substations.loc[:, "polygon"] = df_substations["geometry"] + + return df_substations + + +def _create_substations_centroid(df_substations): + """ + Creates centroids from geometries and keeps the original polygons. + + Parameters: + df_substations (DataFrame): The input DataFrame containing the substations + data. + + Returns: + df_substations (DataFrame): A new DataFrame with the centroids ["geometry"] + and polygons ["polygon"] of the substations geometries. + """ + logger.info("Creating substations geometry.") + df_substations = df_substations.copy() + + df_substations.loc[:, "geometry"] = df_substations["polygon"].apply( + lambda x: x.centroid + ) + + df_substations.loc[:, "lon"] = df_substations["geometry"].apply(lambda x: x.x) + df_substations.loc[:, "lat"] = df_substations["geometry"].apply(lambda x: x.y) + + return df_substations + + +def _create_lines_geometry(df_lines): + """ + Create line geometry for the given DataFrame of lines. + + Parameters: + - df_lines (pandas.DataFrame): DataFrame containing lines data. + + Returns: + - df_lines (pandas.DataFrame): DataFrame with transformed 'geometry' + column (type: shapely LineString). + + Notes: + - This function transforms 'geometry' column in the input DataFrame by + applying the '_create_linestring' function to each row. + - It then drops rows where the geometry has equal start and end points, + as these are usually not lines but outlines of areas. + """ + logger.info("Creating lines geometry.") + df_lines = df_lines.copy() + df_lines.loc[:, "geometry"] = df_lines.apply(_create_linestring, axis=1) + + bool_circle = df_lines["geometry"].apply(lambda x: x.coords[0] == x.coords[-1]) + df_lines = df_lines[~bool_circle] + + return df_lines + + +def _add_bus_centroid_to_line(linestring, point): + """ + Adds the centroid of a substation to a linestring by extending the + linestring with a new segment. + + Parameters: + linestring (LineString): The original linestring to extend. + point (Point): The centroid of the bus. + + Returns: + merged (LineString): The extended linestring with the new segment. + """ + start = linestring.coords[0] + end = linestring.coords[-1] + + dist_to_start = point.distance(Point(start)) + dist_to_end = point.distance(Point(end)) + + if dist_to_start < dist_to_end: + new_segment = LineString([point.coords[0], start]) + else: + new_segment = LineString([point.coords[0], end]) + + merged = linemerge([linestring, new_segment]) + + return merged + + +def _finalise_substations(df_substations): + """ + Finalises the substations column types. + + Args: + df_substations (pandas.DataFrame): The input DataFrame + containing substations data. + + Returns: + df_substations (pandas.DataFrame(): The DataFrame with finalised column + types and transformed data. + """ + logger.info("Finalising substations column types.") + df_substations = df_substations.copy() + # rename columns + df_substations.rename( + columns={ + "id": "bus_id", + "power": "symbol", + "substation": "tag_substation", + }, + inplace=True, + ) + + # Initiate new columns for subsequent build_osm_network step + df_substations.loc[:, "symbol"] = "substation" + df_substations.loc[:, "tag_substation"] = "transmission" + df_substations.loc[:, "dc"] = False + df_substations.loc[df_substations["frequency"] == "0", "dc"] = True + df_substations.loc[:, "under_construction"] = False + df_substations.loc[:, "station_id"] = None + df_substations.loc[:, "tag_area"] = None + df_substations.loc[:, "tag_source"] = df_substations["bus_id"] + + # Only included needed columns + df_substations = df_substations[ + [ + "bus_id", + "symbol", + "tag_substation", + "voltage", + "lon", + "lat", + "dc", + "under_construction", + "station_id", + "tag_area", + "country", + "geometry", + "polygon", + "tag_source", + ] + ] + + # Substation data types + df_substations["voltage"] = df_substations["voltage"].astype(int) + + return df_substations + + +def _finalise_lines(df_lines): + """ + Finalises the lines column types. + + Args: + df_lines (pandas.DataFrame): The input DataFrame containing lines data. + + Returns: + df_lines (pandas.DataFrame(): The DataFrame with finalised column types + and transformed data. + """ + logger.info("Finalising lines column types.") + df_lines = df_lines.copy() + # Rename columns + df_lines.rename( + columns={ + "id": "line_id", + "power": "tag_type", + "frequency": "tag_frequency", + }, + inplace=True, + ) + + # Initiate new columns for subsequent build_osm_network step + df_lines.loc[:, "bus0"] = None + df_lines.loc[:, "bus1"] = None + df_lines.loc[:, "length"] = None + df_lines.loc[:, "underground"] = False + df_lines.loc[df_lines["tag_type"] == "line", "underground"] = False + df_lines.loc[df_lines["tag_type"] == "cable", "underground"] = True + df_lines.loc[:, "under_construction"] = False + df_lines.loc[:, "dc"] = False + df_lines.loc[df_lines["tag_frequency"] == "50", "dc"] = False + df_lines.loc[df_lines["tag_frequency"] == "0", "dc"] = True + + # Only include needed columns + df_lines = df_lines[ + [ + "line_id", + "circuits", + "tag_type", + "voltage", + "tag_frequency", + "bus0", + "bus1", + "length", + "underground", + "under_construction", + "dc", + "country", + "geometry", + ] + ] + + df_lines["circuits"] = df_lines["circuits"].astype(int) + df_lines["voltage"] = df_lines["voltage"].astype(int) + df_lines["tag_frequency"] = df_lines["tag_frequency"].astype(int) + + return df_lines + + +def _finalise_links(df_links): + """ + Finalises the links column types. + + Args: + df_links (pandas.DataFrame): The input DataFrame containing links data. + + Returns: + df_links (pandas.DataFrame(): The DataFrame with finalised column types + and transformed data. + """ + logger.info("Finalising links column types.") + df_links = df_links.copy() + # Rename columns + df_links.rename( + columns={ + "id": "link_id", + "rating": "p_nom", + }, + inplace=True, + ) + + # Initiate new columns for subsequent build_osm_network step + df_links["bus0"] = None + df_links["bus1"] = None + df_links["length"] = None + df_links["underground"] = True + df_links["under_construction"] = False + df_links["dc"] = True + + # Only include needed columns + df_links = df_links[ + [ + "link_id", + "voltage", + "p_nom", + "bus0", + "bus1", + "length", + "underground", + "under_construction", + "dc", + "country", + "geometry", + ] + ] + + df_links["p_nom"] = df_links["p_nom"].astype(int) + df_links["voltage"] = df_links["voltage"].astype(int) + + return df_links + + +def _import_substations(path_substations): + """ + Import substations from the given input paths. This function imports both + substations from OSM ways as well as relations that contain nested + information on the substations shape and electrical parameters. Ways and + relations are subsequently concatenated to form a single DataFrame + containing unique bus ids. + + Args: + path_substations (dict): A dictionary containing input paths for + substations. + + Returns: + pd.DataFrame: A DataFrame containing the imported substations data. + """ + cols_substations_way = [ + "id", + "geometry", + "country", + "power", + "substation", + "voltage", + "frequency", + ] + cols_substations_relation = [ + "id", + "country", + "power", + "substation", + "voltage", + "frequency", + ] + df_substations_way = pd.DataFrame(columns=cols_substations_way) + df_substations_relation = pd.DataFrame(columns=cols_substations_relation) + + logger.info("Importing substations") + for key in path_substations: + logger.info(f"Processing {key}...") + for idx, ip in enumerate(path_substations[key]): + if ( + os.path.exists(ip) and os.path.getsize(ip) > 400 + ): # unpopulated OSM json is about 51 bytes + country = os.path.basename(os.path.dirname(path_substations[key][idx])) + logger.info( + f" - Importing {key} {str(idx+1).zfill(2)}/{str(len(path_substations[key])).zfill(2)}: {ip}" + ) + with open(ip, "r") as f: + data = json.load(f) + + df = pd.DataFrame(data["elements"]) + df["id"] = df["id"].astype(str) + # new string that adds "way/" to id + df["id"] = df["id"].apply( + lambda x: ( + f"way/{x}" if key == "substations_way" else f"relation/{x}" + ) + ) + df["country"] = country + + col_tags = ["power", "substation", "voltage", "frequency"] + + tags = pd.json_normalize(df["tags"]).map( + lambda x: str(x) if pd.notnull(x) else x + ) + + for ct in col_tags: + if ct not in tags.columns: + tags[ct] = pd.NA + + tags = tags.loc[:, col_tags] + + df = pd.concat([df, tags], axis="columns") + + if key == "substations_way": + df.drop(columns=["type", "tags", "bounds", "nodes"], inplace=True) + df_substations_way = pd.concat( + [df_substations_way, df], axis="rows" + ) + elif key == "substations_relation": + df.drop(columns=["type", "tags", "bounds"], inplace=True) + df_substations_relation = pd.concat( + [df_substations_relation, df], axis="rows" + ) + + else: + logger.info( + f" - Skipping {key} {str(idx+1).zfill(2)}/{str(len(path_substations[key])).zfill(2)} (empty): {ip}" + ) + continue + logger.info("---") + + df_substations_way.drop_duplicates(subset="id", keep="first", inplace=True) + df_substations_relation.drop_duplicates(subset="id", keep="first", inplace=True) + + df_substations_way["geometry"] = df_substations_way.apply(_create_polygon, axis=1) + + # Normalise the members column of df_substations_relation + cols_members = ["id", "type", "ref", "role", "geometry"] + df_substations_relation_members = pd.DataFrame(columns=cols_members) + + for index, row in df_substations_relation.iterrows(): + col_members = ["type", "ref", "role", "geometry"] + df = pd.json_normalize(row["members"]) + + for cm in col_members: + if cm not in df.columns: + df[cm] = pd.NA + + df = df.loc[:, col_members] + df["id"] = str(row["id"]) + df["ref"] = df["ref"].astype(str) + df = df[df["type"] != "node"] + df = df.dropna(subset=["geometry"]) + df = df[~df["role"].isin(["", "incoming_line", "substation", "inner"])] + df_substations_relation_members = pd.concat( + [df_substations_relation_members, df], axis="rows" + ) + + df_substations_relation_members.reset_index(inplace=True) + df_substations_relation_members["linestring"] = ( + df_substations_relation_members.apply(_create_linestring, axis=1) + ) + df_substations_relation_members_grouped = ( + df_substations_relation_members.groupby("id")["linestring"] + .apply(lambda x: linemerge(x.tolist())) + .reset_index() + ) + df_substations_relation_members_grouped["geometry"] = ( + df_substations_relation_members_grouped["linestring"].apply( + lambda x: x.convex_hull + ) + ) + + df_substations_relation = ( + df_substations_relation.join( + df_substations_relation_members_grouped.set_index("id"), on="id", how="left" + ) + .drop(columns=["members", "linestring"]) + .dropna(subset=["geometry"]) + ) + + # reorder columns and concatenate + df_substations_relation = df_substations_relation[cols_substations_way] + df_substations = pd.concat( + [df_substations_way, df_substations_relation], axis="rows" + ) + + return df_substations + + +def _remove_lines_within_substations(gdf_lines, gdf_substations_polygon): + """ + Removes lines that are within substation polygons from the given + GeoDataFrame of lines. These are not needed to create network (e.g. bus + bars, switchgear, etc.) + + Parameters: + - gdf_lines (GeoDataFrame): A GeoDataFrame containing lines with 'line_id' + and 'geometry' columns. + - gdf_substations_polygon (GeoDataFrame): A GeoDataFrame containing + substation polygons. + + Returns: + GeoDataFrame: A new GeoDataFrame without lines within substation polygons. + """ + logger.info("Identifying and removing lines within substation polygons...") + gdf = gpd.sjoin( + gdf_lines[["line_id", "geometry"]], + gdf_substations_polygon, + how="inner", + predicate="within", + )["line_id"] + + logger.info( + f"Removed {len(gdf)} lines within substations of original {len(gdf_lines)} lines." + ) + gdf_lines = gdf_lines[~gdf_lines["line_id"].isin(gdf)] + + return gdf_lines + + +def _merge_touching_polygons(df): + """ + Merge touching polygons in a GeoDataFrame. + + Parameters: + - df: pandas.DataFrame or geopandas.GeoDataFrame + The input DataFrame containing the polygons to be merged. + + Returns: + - gdf: geopandas.GeoDataFrame + The GeoDataFrame with merged polygons. + """ + + gdf = gpd.GeoDataFrame(df, geometry="polygon", crs=crs) + combined_polygons = unary_union(gdf.geometry) + if combined_polygons.geom_type == "MultiPolygon": + gdf_combined = gpd.GeoDataFrame( + geometry=[poly for poly in combined_polygons.geoms], crs=crs + ) + else: + gdf_combined = gpd.GeoDataFrame(geometry=[combined_polygons], crs=crs) + + gdf.reset_index(drop=True, inplace=True) + + for i, combined_geom in gdf_combined.iterrows(): + mask = gdf.intersects(combined_geom.geometry) + gdf.loc[mask, "polygon_merged"] = combined_geom.geometry + + gdf.drop(columns=["polygon"], inplace=True) + gdf.rename(columns={"polygon_merged": "polygon"}, inplace=True) + + return gdf + + +def _add_endpoints_to_line(linestring, polygon_dict): + """ + Adds endpoints to a line by removing any overlapping areas with polygons. + + Parameters: + linestring (LineString): The original line to add endpoints to. + polygon_dict (dict): A dictionary of polygons, where the keys are bus IDs and the values are the corresponding polygons. + + Returns: + LineString: The modified line with added endpoints. + """ + if not polygon_dict: + return linestring + polygon_centroids = { + bus_id: polygon.centroid for bus_id, polygon in polygon_dict.items() + } + polygon_unary = polygons = unary_union(list(polygon_dict.values())) + + # difference with polygon + linestring_new = linestring.difference(polygon_unary) + + if type(linestring_new) == MultiLineString: + # keep the longest line in the multilinestring + linestring_new = max(linestring_new.geoms, key=lambda x: x.length) + + for p in polygon_centroids: + linestring_new = _add_bus_centroid_to_line(linestring_new, polygon_centroids[p]) + + return linestring_new + + +def _get_polygons_at_endpoints(linestring, polygon_dict): + """ + Get the polygons that contain the endpoints of a given linestring. + + Parameters: + linestring (LineString): The linestring for which to find the polygons at the endpoints. + polygon_dict (dict): A dictionary containing polygons as values, with bus_ids as keys. + + Returns: + dict: A dictionary containing bus_ids as keys and polygons as values, where the polygons contain the endpoints of the linestring. + """ + # Get the endpoints of the linestring + start_point = Point(linestring.coords[0]) + end_point = Point(linestring.coords[-1]) + + # Initialize dictionary to store bus_ids as keys and polygons as values + bus_id_polygon_dict = {} + + for bus_id, polygon in polygon_dict.items(): + if polygon.contains(start_point) or polygon.contains(end_point): + bus_id_polygon_dict[bus_id] = polygon + + return bus_id_polygon_dict + + +def _extend_lines_to_substations(gdf_lines, gdf_substations_polygon): + """ + Extends the lines in the given GeoDataFrame `gdf_lines` to the centroid of + the nearest substations represented by the polygons in the + `gdf_substations_polygon` GeoDataFrame. + + Parameters: + gdf_lines (GeoDataFrame): A GeoDataFrame containing the lines to be extended. + gdf_substations_polygon (GeoDataFrame): A GeoDataFrame containing the polygons representing substations. + + Returns: + GeoDataFrame: A new GeoDataFrame with the lines extended to the substations. + """ + gdf = gpd.sjoin( + gdf_lines, + gdf_substations_polygon.drop_duplicates(subset="polygon", inplace=False), + how="left", + lsuffix="line", + rsuffix="bus", + predicate="intersects", + ).drop(columns="index_bus") + + # Group by 'line_id' and create a dictionary mapping 'bus_id' to 'geometry_bus', excluding the grouping columns + gdf = ( + gdf.groupby("line_id") + .apply( + lambda x: x[["bus_id", "geometry_bus"]] + .dropna() + .set_index("bus_id")["geometry_bus"] + .to_dict(), + include_groups=False, + ) + .reset_index() + ) + gdf.columns = ["line_id", "bus_dict"] + + gdf["intersects_bus"] = gdf.apply(lambda row: len(row["bus_dict"]) > 0, axis=1) + + gdf.loc[:, "line_geometry"] = gdf.join( + gdf_lines.set_index("line_id")["geometry"], on="line_id" + )["geometry"] + + # Polygons at the endpoints of the linestring + gdf["bus_endpoints"] = gdf.apply( + lambda row: _get_polygons_at_endpoints(row["line_geometry"], row["bus_dict"]), + axis=1, + ) + + gdf.loc[:, "line_geometry_new"] = gdf.apply( + lambda row: _add_endpoints_to_line(row["line_geometry"], row["bus_endpoints"]), + axis=1, + ) + + gdf.set_index("line_id", inplace=True) + gdf_lines.set_index("line_id", inplace=True) + + gdf_lines.loc[:, "geometry"] = gdf["line_geometry_new"] + + return gdf_lines + + +# Function to bridge gaps between all lines + + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake("clean_osm_data") + + configure_logging(snakemake) + set_scenario_config(snakemake) + + # Parameters + crs = "EPSG:4326" # Correct crs for OSM data + min_voltage_ac = 200000 # [unit: V] Minimum voltage value to filter AC lines. + min_voltage_dc = 150000 # [unit: V] Minimum voltage value to filter DC links. + + logger.info("---") + logger.info("SUBSTATIONS") + # Input + path_substations = { + "substations_way": snakemake.input.substations_way, + "substations_relation": snakemake.input.substations_relation, + } + + # Cleaning process + df_substations = _import_substations(path_substations) + df_substations["voltage"] = _clean_voltage(df_substations["voltage"]) + df_substations, list_voltages = _filter_by_voltage( + df_substations, min_voltage=min_voltage_ac + ) + df_substations["frequency"] = _clean_frequency(df_substations["frequency"]) + df_substations = _clean_substations(df_substations, list_voltages) + df_substations = _create_substations_geometry(df_substations) + + # Merge touching polygons + df_substations = _merge_touching_polygons(df_substations) + df_substations = _create_substations_centroid(df_substations) + df_substations = _finalise_substations(df_substations) + + # Create polygon GeoDataFrame to remove lines within substations + gdf_substations_polygon = gpd.GeoDataFrame( + df_substations[["bus_id", "polygon", "voltage"]], + geometry="polygon", + crs=crs, + ) + + gdf_substations_polygon["geometry"] = gdf_substations_polygon.polygon.copy() + + logger.info("---") + logger.info("LINES AND CABLES") + path_lines = { + "lines": snakemake.input.lines_way, + "cables": snakemake.input.cables_way, + } + + # Cleaning process + df_lines = _import_lines_and_cables(path_lines) + df_lines = _drop_duplicate_lines(df_lines) + df_lines.loc[:, "voltage"] = _clean_voltage(df_lines["voltage"]) + df_lines, list_voltages = _filter_by_voltage(df_lines, min_voltage=min_voltage_ac) + df_lines.loc[:, "circuits"] = _clean_circuits(df_lines["circuits"]) + df_lines.loc[:, "cables"] = _clean_cables(df_lines["cables"]) + df_lines.loc[:, "frequency"] = _clean_frequency(df_lines["frequency"]) + df_lines.loc[:, "wires"] = _clean_wires(df_lines["wires"]) + df_lines = _clean_lines(df_lines, list_voltages) + + # Drop DC lines, will be added through relations later + len_before = len(df_lines) + df_lines = df_lines[df_lines["frequency"] == "50"] + len_after = len(df_lines) + logger.info( + f"Dropped {len_before - len_after} DC lines. Keeping {len_after} AC lines." + ) + + df_lines = _create_lines_geometry(df_lines) + df_lines = _finalise_lines(df_lines) + + # Create GeoDataFrame + gdf_lines = gpd.GeoDataFrame(df_lines, geometry="geometry", crs=crs) + gdf_lines = _remove_lines_within_substations(gdf_lines, gdf_substations_polygon) + gdf_lines = _extend_lines_to_substations(gdf_lines, gdf_substations_polygon) + + logger.info("---") + logger.info("HVDC LINKS") + path_links = { + "links": snakemake.input.links_relation, + } + + df_links = _import_links(path_links) + + df_links = _drop_duplicate_lines(df_links) + df_links.loc[:, "voltage"] = _clean_voltage(df_links["voltage"]) + df_links, list_voltages = _filter_by_voltage(df_links, min_voltage=min_voltage_dc) + # Keep only highest voltage of split string + df_links.loc[:, "voltage"] = df_links["voltage"].apply( + lambda x: str(max(map(int, x.split(";")))) + ) + df_links.loc[:, "frequency"] = _clean_frequency(df_links["frequency"]) + df_links.loc[:, "rating"] = _clean_rating(df_links["rating"]) + + df_links.loc[:, "geometry"] = df_links.apply(_create_single_link, axis=1) + df_links = _finalise_links(df_links) + gdf_links = gpd.GeoDataFrame(df_links, geometry="geometry", crs=crs).set_index( + "link_id" + ) + + # Add line endings to substations + path_country_shapes = snakemake.input.country_shapes + path_offshore_shapes = snakemake.input.offshore_shapes + + df_substations = _add_line_endings_to_substations( + df_substations, + gdf_lines, + path_country_shapes, + path_offshore_shapes, + prefix="line-end", + ) + + df_substations = _add_line_endings_to_substations( + df_substations, + gdf_links, + path_country_shapes, + path_offshore_shapes, + prefix="link-end", + ) + + # Drop polygons and create GDF + gdf_substations = gpd.GeoDataFrame( + df_substations.drop(columns=["polygon"]), geometry="geometry", crs=crs + ) + + output_substations_polygon = snakemake.output["substations_polygon"] + output_substations = snakemake.output["substations"] + output_lines = snakemake.output["lines"] + output_links = snakemake.output["links"] + + logger.info( + f"Exporting clean substations with polygon shapes to {output_substations_polygon}" + ) + gdf_substations_polygon.drop(columns=["geometry"]).to_file( + output_substations_polygon, driver="GeoJSON" + ) + logger.info(f"Exporting clean substations to {output_substations}") + gdf_substations.to_file(output_substations, driver="GeoJSON") + logger.info(f"Exporting clean lines to {output_lines}") + gdf_lines.to_file(output_lines, driver="GeoJSON") + logger.info(f"Exporting clean links to {output_links}") + gdf_links.to_file(output_links, driver="GeoJSON") + + logger.info("Cleaning OSM data completed.") diff --git a/scripts/prepare_osm_network_release.py b/scripts/prepare_osm_network_release.py new file mode 100644 index 000000000..ac6b25354 --- /dev/null +++ b/scripts/prepare_osm_network_release.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2020-2024 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT + +import logging +import os + +import pandas as pd +import pypsa +from _helpers import configure_logging, set_scenario_config + +logger = logging.getLogger(__name__) + + +BUSES_COLUMNS = [ + "bus_id", + "voltage", + "dc", + "symbol", + "under_construction", + "x", + "y", + "country", + "geometry", +] +LINES_COLUMNS = [ + "line_id", + "bus0", + "bus1", + "voltage", + "circuits", + "length", + "underground", + "under_construction", + "geometry", +] +LINKS_COLUMNS = [ + "link_id", + "bus0", + "bus1", + "voltage", + "p_nom", + "length", + "underground", + "under_construction", + "geometry", +] +TRANSFORMERS_COLUMNS = [ + "transformer_id", + "bus0", + "bus1", + "voltage_bus0", + "voltage_bus1", + "geometry", +] +CONVERTERS_COLUMNS = [ + "converter_id", + "bus0", + "bus1", + "voltage", + "geometry", +] + + +def export_clean_csv(df, columns, output_file): + """ + Export a cleaned DataFrame to a CSV file. + + Args: + df (pandas.DataFrame): The DataFrame to be exported. + columns (list): A list of column names to include in the exported CSV file. + output_file (str): The path to the output CSV file. + + Returns: + None + """ + rename_dict = { + "Bus": "bus_id", + "Line": "line_id", + "Link": "link_id", + "Transformer": "transformer_id", + "v_nom": "voltage", + "num_parallel": "circuits", + } + + if "converter_id" in columns: + rename_dict["Link"] = "converter_id" + + df.reset_index().rename(columns=rename_dict).loc[:, columns].replace( + {True: "t", False: "f"} + ).to_csv(output_file, index=False, quotechar="'") + + return None + + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake("prepare_osm_network_release") + + configure_logging(snakemake) + set_scenario_config(snakemake) + + network = pypsa.Network(snakemake.input.base_network) + + network.buses["dc"] = network.buses.pop("carrier").map({"DC": "t", "AC": "f"}) + network.lines.length = network.lines.length * 1e3 + network.links.length = network.links.length * 1e3 + + # Export to clean csv for release + logger.info(f"Exporting {len(network.buses)} buses to %s", snakemake.output.buses) + export_clean_csv(network.buses, BUSES_COLUMNS, snakemake.output.buses) + + logger.info( + f"Exporting {len(network.transformers)} transformers to %s", + snakemake.output.transformers, + ) + export_clean_csv( + network.transformers, TRANSFORMERS_COLUMNS, snakemake.output.transformers + ) + + logger.info(f"Exporting {len(network.lines)} lines to %s", snakemake.output.lines) + export_clean_csv(network.lines, LINES_COLUMNS, snakemake.output.lines) + + # Boolean that specifies if link element is a converter + is_converter = network.links.index.str.startswith("conv") == True + + logger.info( + f"Exporting {len(network.links[~is_converter])} links to %s", + snakemake.output.links, + ) + export_clean_csv( + network.links[~is_converter], LINKS_COLUMNS, snakemake.output.links + ) + + logger.info( + f"Exporting {len(network.links[is_converter])} converters to %s", + snakemake.output.converters, + ) + export_clean_csv( + network.links[is_converter], CONVERTERS_COLUMNS, snakemake.output.converters + ) + + logger.info("Export of OSM network for release complete.") diff --git a/scripts/retrieve_osm_data.py b/scripts/retrieve_osm_data.py new file mode 100644 index 000000000..745533cff --- /dev/null +++ b/scripts/retrieve_osm_data.py @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2020-2024 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT +""" +Retrieve OSM data for the specified country using the overpass API and save it +to the specified output files. + +Note that overpass requests are based on a fair +use policy. `retrieve_osm_data` is meant to be used in a way that respects this +policy by fetching the needed data once, only. +""" + +import json +import logging +import os +import time + +import requests +from _helpers import ( # set_scenario_config,; update_config_from_wildcards,; update_config_from_wildcards, + configure_logging, + set_scenario_config, +) + +logger = logging.getLogger(__name__) + + +def retrieve_osm_data( + country, + output, + features=[ + "cables_way", + "lines_way", + "links_relation", + "substations_way", + "substations_relation", + ], +): + """ + Retrieve OSM data for the specified country and save it to the specified + output files. + + Parameters + ---------- + country : str + The country code for which the OSM data should be retrieved. + output : dict + A dictionary mapping feature names to the corresponding output file + paths. Saving the OSM data to .json files. + features : list, optional + A list of OSM features to retrieve. The default is [ + "cables_way", + "lines_way", + "substations_way", + "substations_relation", + ]. + """ + # Overpass API endpoint URL + overpass_url = "https://overpass-api.de/api/interpreter" + + features_dict = { + "cables_way": 'way["power"="cable"]', + "lines_way": 'way["power"="line"]', + "links_relation": 'relation["route"="power"]["frequency"="0"]', + "substations_way": 'way["power"="substation"]', + "substations_relation": 'relation["power"="substation"]', + } + + wait_time = 5 + + for f in features: + if f not in features_dict: + logger.info( + f"Invalid feature: {f}. Supported features: {list(features_dict.keys())}" + ) + raise ValueError( + f"Invalid feature: {f}. Supported features: {list(features_dict.keys())}" + ) + + retries = 3 + for attempt in range(retries): + logger.info( + f" - Fetching OSM data for feature '{f}' in {country} (Attempt {attempt+1})..." + ) + + # Build the overpass query + op_area = f'area["ISO3166-1"="{country}"]' + op_query = f""" + [out:json]; + {op_area}->.searchArea; + ( + {features_dict[f]}(area.searchArea); + ); + out body geom; + """ + try: + # Send the request + response = requests.post(overpass_url, data=op_query) + response.raise_for_status() # Raise HTTPError for bad responses + data = response.json() + + filepath = output[f] + parentfolder = os.path.dirname(filepath) + if not os.path.exists(parentfolder): + os.makedirs(parentfolder) + + with open(filepath, mode="w") as f: + json.dump(response.json(), f, indent=2) + logger.info(" - Done.") + break # Exit the retry loop on success + except (json.JSONDecodeError, requests.exceptions.RequestException) as e: + logger.error(f"Error for feature '{f}' in country {country}: {e}") + logger.debug( + f"Response text: {response.text if response else 'No response'}" + ) + if attempt < retries - 1: + wait_time += 15 + logger.info(f"Waiting {wait_time} seconds before retrying...") + time.sleep(wait_time) + else: + logger.error( + f"Failed to retrieve data for feature '{f}' in country {country} after {retries} attempts." + ) + except Exception as e: + # For now, catch any other exceptions and log them. Treat this + # the same as a RequestException and try to run again two times. + logger.error( + f"Unexpected error for feature '{f}' in country {country}: {e}" + ) + if attempt < retries - 1: + wait_time += 10 + logger.info(f"Waiting {wait_time} seconds before retrying...") + time.sleep(wait_time) + else: + logger.error( + f"Failed to retrieve data for feature '{f}' in country {country} after {retries} attempts." + ) + + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake("retrieve_osm_data", country="BE") + configure_logging(snakemake) + set_scenario_config(snakemake) + + # Retrieve the OSM data + country = snakemake.wildcards.country + output = snakemake.output + + retrieve_osm_data(country, output) diff --git a/scripts/simplify_network.py b/scripts/simplify_network.py index 9db3435bf..741fd324c 100644 --- a/scripts/simplify_network.py +++ b/scripts/simplify_network.py @@ -108,7 +108,7 @@ logger = logging.getLogger(__name__) -def simplify_network_to_380(n): +def simplify_network_to_380(n, linetype_380): """ Fix all lines to a voltage level of 380 kV and remove all transformers. @@ -132,8 +132,8 @@ def simplify_network_to_380(n): trafo_map = pd.Series(n.transformers.bus1.values, n.transformers.bus0.values) trafo_map = trafo_map[~trafo_map.index.duplicated(keep="first")] - several_trafo_b = trafo_map.isin(trafo_map.index) - trafo_map[several_trafo_b] = trafo_map[several_trafo_b].map(trafo_map) + while (several_trafo_b := trafo_map.isin(trafo_map.index)).any(): + trafo_map[several_trafo_b] = trafo_map[several_trafo_b].map(trafo_map) missing_buses_i = n.buses.index.difference(trafo_map.index) missing = pd.Series(missing_buses_i, missing_buses_i) trafo_map = pd.concat([trafo_map, missing]) @@ -600,7 +600,8 @@ def find_closest_bus(n, x, y, tol=2000): # remove integer outputs for compatibility with PyPSA v0.26.0 n.generators.drop("n_mod", axis=1, inplace=True, errors="ignore") - n, trafo_map = simplify_network_to_380(n) + linetype_380 = snakemake.config["lines"]["types"][380] + n, trafo_map = simplify_network_to_380(n, linetype_380) technology_costs = load_costs( snakemake.input.tech_costs, From 6c9d23ced245b099b2dda38edb3c3cb300694731 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 22 Aug 2024 15:24:22 +0200 Subject: [PATCH 223/344] config: set correct minimum number of clusters --- config/config.default.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 86689ea72..0fd6e9869 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -42,7 +42,7 @@ scenario: ll: - vopt clusters: - - 41 + - 38 - 128 - 256 opts: From 73d13536083218d0ff2bf1c5782c4aef922dd217 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 23 Aug 2024 09:17:21 +0200 Subject: [PATCH 224/344] cluster_network: fix if no clustering required --- scripts/cluster_network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/cluster_network.py b/scripts/cluster_network.py index 79184167e..9e1db6a77 100644 --- a/scripts/cluster_network.py +++ b/scripts/cluster_network.py @@ -512,7 +512,7 @@ def plot_busmap_for_n_clusters(n, n_clusters, solver_name="scip", fn=None): busmap = n.buses.index.to_series() linemap = n.lines.index.to_series() clustering = pypsa.clustering.spatial.Clustering( - n, busmap, linemap, linemap, pd.Series(dtype="O") + n, busmap, linemap ) else: Nyears = n.snapshot_weightings.objective.sum() / 8760 From ef0bbd5f379bae377ae30e8a4fb191503d4bec20 Mon Sep 17 00:00:00 2001 From: Bobby Xiong <36541459+bobbyxng@users.noreply.github.com> Date: Fri, 23 Aug 2024 17:35:46 +0200 Subject: [PATCH 225/344] Custom busmap (#1231) * Added functionality to set custom_busmaps for different base_networks. * Updated configtables and location for busmaps. * Added release nots. * removed run parameter in mock_snakemake (only used for debugging). * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: Fabian Neumann Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- doc/configtables/enable.csv | 2 +- doc/release_notes.rst | 2 ++ rules/build_electricity.smk | 15 ++++++++++----- scripts/cluster_network.py | 6 ++---- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/doc/configtables/enable.csv b/doc/configtables/enable.csv index 0268319eb..5359131de 100644 --- a/doc/configtables/enable.csv +++ b/doc/configtables/enable.csv @@ -4,5 +4,5 @@ retrieve_databundle,bool,"{true, false}","Switch to retrieve databundle from zen retrieve_cost_data,bool,"{true, false}","Switch to retrieve technology cost data from `technology-data repository `_." build_cutout,bool,"{true, false}","Switch to enable the building of cutouts via the rule :mod:`build_cutout`." retrieve_cutout,bool,"{true, false}","Switch to enable the retrieval of cutouts from zenodo with :mod:`retrieve_cutout`." -custom_busmap,bool,"{true, false}","Switch to enable the use of custom busmaps in rule :mod:`cluster_network`. If activated the rule looks for provided busmaps at ``data/custom_busmap_elec_s{simpl}_{clusters}.csv`` which should have the same format as ``resources/busmap_elec_s{simpl}_{clusters}.csv``, i.e. the index should contain the buses of ``networks/elec_s{simpl}.nc``." +custom_busmap,bool,"{true, false}","Switch to enable the use of custom busmaps in rule :mod:`cluster_network`. If activated the rule looks for provided busmaps at ``data/busmaps/elec_s{simpl}_{clusters}_{base_network}.csv`` which should have the same format as ``resources/busmap_elec_s{simpl}_{clusters}.csv``, i.e. the index should contain the buses of ``networks/elec_s{simpl}.nc``. {base_network} is the name of the selected base_network in electricity, e.g. ``gridkit``, ``osm-prebuilt``, or ``osm-raw``." drop_leap_day,bool,"{true, false}","Switch to drop February 29 from all time-dependent data in leap years" diff --git a/doc/release_notes.rst b/doc/release_notes.rst index b1345e0c7..4df38b019 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -91,6 +91,8 @@ Upcoming Release * In :mod:`base_network`, replace own voronoi polygon calculation function with Geopandas `gdf.voronoi_polygons` method. +* Move custom busmaps to ```data/busmaps/elec_s{simpl}_{clusters}_{base_network}.csv``` (if enabled). This allows for different busmaps depending on the base network and scenario. + PyPSA-Eur 0.11.0 (25th May 2024) ===================================== diff --git a/rules/build_electricity.smk b/rules/build_electricity.smk index 4446ef765..1f568e99c 100644 --- a/rules/build_electricity.smk +++ b/rules/build_electricity.smk @@ -530,6 +530,15 @@ rule simplify_network: "../scripts/simplify_network.py" +# Optional input when using custom busmaps - Needs to be tailored to selected base_network +def input_cluster_network(w): + if config_provider("enable", "custom_busmap", default=False)(w): + base_network = config_provider("electricity", "base_network")(w) + custom_busmap = f"data/busmaps/elec_s{w.simpl}_{w.clusters}_{base_network}.csv" + return {"custom_busmap": custom_busmap} + return {"custom_busmap": []} + + rule cluster_network: params: cluster_network=config_provider("clustering", "cluster_network"), @@ -546,15 +555,11 @@ rule cluster_network: length_factor=config_provider("lines", "length_factor"), costs=config_provider("costs"), input: + unpack(input_cluster_network), network=resources("networks/elec_s{simpl}.nc"), regions_onshore=resources("regions_onshore_elec_s{simpl}.geojson"), regions_offshore=resources("regions_offshore_elec_s{simpl}.geojson"), busmap=ancient(resources("busmap_elec_s{simpl}.csv")), - custom_busmap=lambda w: ( - "data/custom_busmap_elec_s{simpl}_{clusters}.csv" - if config_provider("enable", "custom_busmap", default=False)(w) - else [] - ), tech_costs=lambda w: resources( f"costs_{config_provider('costs', 'year')(w)}.csv" ), diff --git a/scripts/cluster_network.py b/scripts/cluster_network.py index 9e1db6a77..b63747c24 100644 --- a/scripts/cluster_network.py +++ b/scripts/cluster_network.py @@ -36,7 +36,7 @@ - ``resources/regions_offshore_elec_s{simpl}.geojson``: confer :ref:`simplify` - ``resources/busmap_elec_s{simpl}.csv``: confer :ref:`simplify` - ``networks/elec_s{simpl}.nc``: confer :ref:`simplify` -- ``data/custom_busmap_elec_s{simpl}_{clusters}.csv``: optional input +- ``data/custom_busmap_elec_s{simpl}_{clusters}_{base_network}.csv``: optional input Outputs ------- @@ -511,9 +511,7 @@ def plot_busmap_for_n_clusters(n, n_clusters, solver_name="scip", fn=None): # Fast-path if no clustering is necessary busmap = n.buses.index.to_series() linemap = n.lines.index.to_series() - clustering = pypsa.clustering.spatial.Clustering( - n, busmap, linemap - ) + clustering = pypsa.clustering.spatial.Clustering(n, busmap, linemap) else: Nyears = n.snapshot_weightings.objective.sum() / 8760 From a357ba11e99091cf6fb1c75ac67b8114c3f5669e Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 26 Aug 2024 11:21:15 +0200 Subject: [PATCH 226/344] add capital cost for liquid carbonaceous fuel stores (closes #489) (#1234) * add capital cost for liquid carbonaceous fuel stores (closes #489) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- scripts/prepare_sector_network.py | 70 ++++++++++--------------------- 1 file changed, 21 insertions(+), 49 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 71b504391..b6644d6dc 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -539,7 +539,24 @@ def add_carrier_buses(n, carrier, nodes=None): unit = "MWh_LHV" if carrier == "gas" else "MWh_th" # preliminary value for non-gas carriers to avoid zeros - capital_cost = costs.at["gas storage", "fixed"] if carrier == "gas" else 0.02 + if carrier == "gas": + capital_cost = costs.at["gas storage", "fixed"] + elif carrier == "oil": + # based on https://www.engineeringtoolbox.com/fuels-higher-calorific-values-d_169.html + mwh_per_m3 = 44.9 * 724 * 0.278 * 1e-3 # MJ/kg * kg/m3 * kWh/MJ * MWh/kWh + capital_cost = ( + costs.at["General liquid hydrocarbon storage (product)", "fixed"] + / mwh_per_m3 + ) + elif carrier == "methanol": + # based on https://www.engineeringtoolbox.com/fossil-fuels-energy-content-d_1298.html + mwh_per_m3 = 5.54 * 791 * 1e-3 # kWh/kg * kg/m3 * MWh/kWh + capital_cost = ( + costs.at["General liquid hydrocarbon storage (product)", "fixed"] + / mwh_per_m3 + ) + else: + capital_cost = 0.1 n.madd("Bus", nodes, location=location, carrier=carrier, unit=unit) @@ -2999,24 +3016,7 @@ def add_industry(n, costs): # methanol for industry - n.madd( - "Bus", - spatial.methanol.nodes, - carrier="methanol", - location=spatial.methanol.locations, - unit="MWh_LHV", - ) - - n.madd( - "Store", - spatial.methanol.nodes, - suffix=" Store", - bus=spatial.methanol.nodes, - e_nom_extendable=True, - e_cyclic=True, - carrier="methanol", - capital_cost=0.02, - ) + add_carrier_buses(n, "methanol") n.madd( "Bus", @@ -3187,38 +3187,10 @@ def add_industry(n, costs): ], # CO2 intensity methanol based on stoichiometric calculation with 22.7 GJ/t methanol (32 g/mol), CO2 (44 g/mol), 277.78 MWh/TJ = 0.218 t/MWh ) - if "oil" not in n.buses.carrier.unique(): - n.madd( - "Bus", - spatial.oil.nodes, - location=spatial.oil.locations, - carrier="oil", - unit="MWh_LHV", - ) - - if "oil" not in n.stores.carrier.unique(): - # could correct to e.g. 0.001 EUR/kWh * annuity and O&M - n.madd( - "Store", - spatial.oil.nodes, - suffix=" Store", - bus=spatial.oil.nodes, - e_nom_extendable=True, - e_cyclic=True, - carrier="oil", - ) + if shipping_oil_share: - if options.get("fossil_fuels", True) and "oil" not in n.generators.carrier.unique(): - n.madd( - "Generator", - spatial.oil.nodes, - bus=spatial.oil.nodes, - p_nom_extendable=True, - carrier="oil", - marginal_cost=costs.at["oil", "fuel"], - ) + add_carrier_buses(n, "oil") - if shipping_oil_share: p_set_oil = shipping_oil_share * p_set.rename(lambda x: x + " shipping oil") if not options["regional_oil_demand"]: From b44f544d77cb032814be0f60597bd1ddaeff6976 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 26 Aug 2024 13:56:10 +0200 Subject: [PATCH 227/344] simplify_network: do not use mode for linetype inferral --- scripts/simplify_network.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/simplify_network.py b/scripts/simplify_network.py index 741fd324c..124618cb1 100644 --- a/scripts/simplify_network.py +++ b/scripts/simplify_network.py @@ -124,7 +124,6 @@ def simplify_network_to_380(n, linetype_380): n.buses["v_nom"] = 380.0 - linetype_380 = n.lines["type"].mode()[0] n.lines["type"] = linetype_380 n.lines["v_nom"] = 380 n.lines["i_nom"] = n.line_types.i_nom[linetype_380] From cab85cbb617c4267adc7626168bc65e7a99f1065 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 26 Aug 2024 15:04:15 +0200 Subject: [PATCH 228/344] update GEM Europe gas tracker to May 2024 version (#1235) --- doc/release_notes.rst | 3 +++ rules/build_sector.smk | 5 +---- rules/retrieve.smk | 17 +++++++++++++++++ scripts/build_gas_input_locations.py | 23 +++++++++++++++-------- 4 files changed, 36 insertions(+), 12 deletions(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 4df38b019..c9359f717 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -9,6 +9,9 @@ Release Notes Upcoming Release ================ + +* Update GEM Europe Gas Tracker to May 2024 version. + * Add investment period dependent CO2 sequestration potentials * Add option to produce hydrogen from solid biomass (flag ``solid biomass to hydrogen``), combined with carbon capture diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 598df96e3..c1e4d573f 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -93,10 +93,7 @@ rule build_gas_network: rule build_gas_input_locations: input: - gem=storage( - "https://globalenergymonitor.org/wp-content/uploads/2023/07/Europe-Gas-Tracker-2023-03-v3.xlsx", - keep_local=True, - ), + gem="data/gem/Europe-Gas-Tracker-2024-05.xlsx", entry="data/gas_network/scigrid-gas/data/IGGIELGN_BorderPoints.geojson", storage="data/gas_network/scigrid-gas/data/IGGIELGN_Storages.geojson", regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), diff --git a/rules/retrieve.smk b/rules/retrieve.smk index c30696ccf..df1f7379f 100644 --- a/rules/retrieve.smk +++ b/rules/retrieve.smk @@ -280,6 +280,23 @@ if config["enable"]["retrieve"]: os.remove(params["zip"]) +if config["enable"]["retrieve"]: + + rule retrieve_gem_europe_gas_tracker: + output: + "data/gem/Europe-Gas-Tracker-2024-05.xlsx", + run: + import requests + + response = requests.get( + "https://globalenergymonitor.org/wp-content/uploads/2024/05/Europe-Gas-Tracker-2024-05.xlsx", + headers={"User-Agent": "Mozilla/5.0"}, + ) + with open(output[0], "wb") as f: + f.write(response.content) + + + if config["enable"]["retrieve"]: # Some logic to find the correct file URL # Sometimes files are released delayed or ahead of schedule, check which file is currently available diff --git a/scripts/build_gas_input_locations.py b/scripts/build_gas_input_locations.py index ca43db3c1..0cd114758 100644 --- a/scripts/build_gas_input_locations.py +++ b/scripts/build_gas_input_locations.py @@ -36,15 +36,20 @@ def build_gem_lng_data(fn): "Gran Canaria LNG Terminal", ] + status_list = ["Operating", "Construction"] # noqa: F841 + df = df.query( - "Status != 'Cancelled' \ + "Status in @status_list \ + & FacilityType == 'Import' \ & Country != @remove_country \ & TerminalName != @remove_terminal \ - & CapacityInMtpa != '--'" + & CapacityInMtpa != '--' \ + & CapacityInMtpa != 0" ) geometry = gpd.points_from_xy(df["Longitude"], df["Latitude"]) - return gpd.GeoDataFrame(df, geometry=geometry, crs="EPSG:4326") + gdf = gpd.GeoDataFrame(df, geometry=geometry, crs="EPSG:4326") + return gdf def build_gem_prod_data(fn): @@ -54,8 +59,10 @@ def build_gem_prod_data(fn): remove_country = ["Cyprus", "Türkiye"] # noqa: F841 remove_fuel_type = ["oil"] # noqa: F841 + status_list = ["operating", "in development"] # noqa: F841 + df = df.query( - "Status != 'shut in' \ + "Status in @status_list \ & 'Fuel type' != 'oil' \ & Country != @remove_country \ & ~Latitude.isna() \ @@ -64,7 +71,7 @@ def build_gem_prod_data(fn): p = pd.read_excel(fn, sheet_name="Gas extraction - production") p = p.set_index("GEM Unit ID") - p = p[p["Fuel description"] == "gas"] + p = p[p["Fuel description"].str.contains("gas")] capacities = pd.DataFrame(index=df.index) for key in ["production", "production design capacity", "reserves"]: @@ -85,7 +92,8 @@ def build_gem_prod_data(fn): ) geometry = gpd.points_from_xy(df["Longitude"], df["Latitude"]) - return gpd.GeoDataFrame(df, geometry=geometry, crs="EPSG:4326") + gdf = gpd.GeoDataFrame(df, geometry=geometry, crs="EPSG:4326") + return gdf def build_gas_input_locations(gem_fn, entry_fn, sto_fn, countries): @@ -134,8 +142,7 @@ def build_gas_input_locations(gem_fn, entry_fn, sto_fn, countries): snakemake = mock_snakemake( "build_gas_input_locations", simpl="", - clusters="5", - configfiles="config/test/config.overnight.yaml", + clusters="128", ) configure_logging(snakemake) From 25f6c014e72e491df399977992aaf74ae0215dcf Mon Sep 17 00:00:00 2001 From: Philipp Glaum Date: Mon, 26 Aug 2024 15:15:02 +0200 Subject: [PATCH 229/344] adress review comments --- config/config.default.yaml | 26 ++++----- doc/configtables/sector.csv | 26 ++++----- scripts/prepare_sector_network.py | 96 ++++++++++--------------------- 3 files changed, 57 insertions(+), 91 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index f3f4f2384..eeff81912 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -631,19 +631,19 @@ sector: # - onshore # more than 50 km from sea - nearshore # within 50 km of sea # - offshore - methanol: false - regional_methanol_demand: false - methanol_reforming: false - methanol_reforming_cc: false - methanol_to_kerosene: false - methanol_to_olefins: false - methanol_to_power: - ccgt: false - ccgt_cc: false - ocgt: false - allam: false - biomass_to_methanol: false - biomass_to_methanol_cc: false + methanol: + regional_methanol_demand: false + methanol_reforming: false + methanol_reforming_cc: false + methanol_to_kerosene: false + methanol_to_olefins: false + methanol_to_power: + ccgt: false + ccgt_cc: false + ocgt: false + allam: false + biomass_to_methanol: false + biomass_to_methanol_cc: false ammonia: false min_part_load_fischer_tropsch: 0.5 min_part_load_methanolisation: 0.3 diff --git a/doc/configtables/sector.csv b/doc/configtables/sector.csv index c554e5e9a..916bce09f 100644 --- a/doc/configtables/sector.csv +++ b/doc/configtables/sector.csv @@ -89,7 +89,7 @@ chp,--,"{true, false}",Add option for using Combined Heat and Power (CHP) micro_chp,--,"{true, false}",Add option for using Combined Heat and Power (CHP) for decentral areas. solar_thermal,--,"{true, false}",Add option for using solar thermal to generate heat. solar_cf_correction,--,float,The correction factor for the value provided by the solar thermal profile calculations -marginal_cost_storage,currency/MWh ,float,The marginal cost of discharging batteries in distributed grids +marginal_cost_storage,"currency/MWh ",float,The marginal cost of discharging batteries in distributed grids methanation,--,"{true, false}",Add option for transforming hydrogen and CO2 into methane using methanation. coal_cc,--,"{true, false}",Add option for coal CHPs with carbon capture dac,--,"{true, false}",Add option for Direct Air Capture (DAC) @@ -119,17 +119,17 @@ cc_fraction,--,float,The default fraction of CO2 captured with post-combustion c hydrogen_underground _storage,--,"{true, false}",Add options for storing hydrogen underground. Storage potential depends regionally. hydrogen_underground _storage_locations,,"{onshore, nearshore, offshore}","The location where hydrogen underground storage can be located. Onshore, nearshore, offshore means it must be located more than 50 km away from the sea, within 50 km of the sea, or within the sea itself respectively." ,,, -methanol, --,"{true, false}", Add methanol as carrrier and add enabled methnol technologies -regional_methanol_demand,--,"{true, false}",Spatially resolve methanol demand. Set to true if regional CO2 constraints needed. -methanol_reforming,--,"{true, false}", Add methanol reforming -methanol_reforming_cc,--,"{true, false}", Add methanol reforming with carbon capture -methanol_to_kerosene,--,"{true, false}", Add methanol to kerosene -methanol_to_olefins,--,"{true, false}", Add methanol to olefins -methanol_to_power,--,--, Add different methanol to power technologies --- ccgt,--,"{true, false}", Add combined cycle gas turbine (CCGT) using methanol --- ccgt_cc,--,"{true, false}", Add combined cycle gas turbine (CCGT) with carbon capture using methanol --- ocgt,--,"{true, false}", Add open cycle gas turbine (OCGT) using methanol --- allam,--,"{true, false}", Add Allam cycle gas power plants using methanol +methanol,--,--,Add methanol as carrrier and add enabled methnol technologies +-- regional_methanol_demand,--,"{true, false}",Spatially resolve methanol demand. Set to true if regional CO2 constraints needed. +-- methanol_reforming,--,"{true, false}"," Add methanol reforming" +-- methanol_reforming_cc,--,"{true, false}"," Add methanol reforming with carbon capture" +-- methanol_to_kerosene,--,"{true, false}"," Add methanol to kerosene" +-- methanol_to_olefins,--,"{true, false}"," Add methanol to olefins" +-- methanol_to_power,--,--," Add different methanol to power technologies" +-- -- ccgt,--,"{true, false}"," Add combined cycle gas turbine (CCGT) using methanol" +-- -- ccgt_cc,--,"{true, false}"," Add combined cycle gas turbine (CCGT) with carbon capture using methanol" +-- -- ocgt,--,"{true, false}"," Add open cycle gas turbine (OCGT) using methanol" +-- -- allam,--,"{true, false}"," Add Allam cycle gas power plants using methanol" ,,, ammonia,--,"{true, false, regional}","Add ammonia as a carrrier. It can be either true (copperplated NH3), false (no NH3 carrier) or ""regional"" (regionalised NH3 without network)" min_part_load_fischer _tropsch,per unit of p_nom ,float,The minimum unit dispatch (``p_min_pu``) for the Fischer-Tropsch process @@ -152,7 +152,7 @@ H2_network,--,"{true, false}",Add option for new hydrogen pipelines gas_network,--,"{true, false}","Add existing natural gas infrastructure, incl. LNG terminals, production and entry-points. The existing gas network is added with a lossless transport model. A length-weighted `k-edge augmentation algorithm `_ can be run to add new candidate gas pipelines such that all regions of the model can be connected to the gas network. When activated, all the gas demands are regionally disaggregated as well." H2_retrofit,--,"{true, false}",Add option for retrofiting existing pipelines to transport hydrogen. H2_retrofit_capacity _per_CH4,--,float,"The ratio for H2 capacity per original CH4 capacity of retrofitted pipelines. The `European Hydrogen Backbone (April, 2020) p.15 `_ 60% of original natural gas capacity could be used in cost-optimal case as H2 capacity." -gas_network_connectivity _upgrade ,--,float,The number of desired edge connectivity (k) in the length-weighted `k-edge augmentation algorithm `_ used for the gas network +"gas_network_connectivity _upgrade ",--,float,The number of desired edge connectivity (k) in the length-weighted `k-edge augmentation algorithm `_ used for the gas network gas_distribution_grid,--,"{true, false}",Add a gas distribution grid gas_distribution_grid _cost_factor,,,Multiplier for the investment cost of the gas distribution grid ,,, diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 4bbced7e4..27a5e2e79 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -155,7 +155,7 @@ def define_spatial(nodes, options): spatial.methanol.nodes = ["EU methanol"] spatial.methanol.locations = ["EU"] - if options["regional_methanol_demand"]: + if options["methanol"]["regional_methanol_demand"]: spatial.methanol.demand_locations = nodes spatial.methanol.industry = nodes + " industry methanol" spatial.methanol.shipping = nodes + " shipping methanol" @@ -779,14 +779,9 @@ def add_allam(n, costs): def add_biomass_to_methanol(n, costs): - if len(spatial.biomass.nodes) <= 1 and len(spatial.methanol.nodes) > 1: - link_names = nodes + " " + spatial.biomass.nodes - else: - link_names = spatial.biomass.nodes - n.madd( "Link", - link_names, + spatial.biomass.nodes, suffix=" biomass-to-methanol", bus0=spatial.biomass.nodes, bus1=spatial.methanol.nodes, @@ -806,14 +801,9 @@ def add_biomass_to_methanol(n, costs): def add_biomass_to_methanol_cc(n, costs): - if len(spatial.biomass.nodes) <= 1 and len(spatial.methanol.nodes) > 1: - link_names = nodes + " " + spatial.biomass.nodes - else: - link_names = spatial.biomass.nodes - n.madd( "Link", - link_names, + spatial.biomass.nodes, suffix=" biomass-to-methanol CC", bus0=spatial.biomass.nodes, bus1=spatial.methanol.nodes, @@ -869,7 +859,7 @@ def add_methanol_to_power(n, costs, types={}): logger.info("Adding methanol CCGT power plants.") # efficiency * EUR/MW * (annuity + FOM) - capital_cost = 0.58 * 916e3 * (calculate_annuity(25, 0.07) + 0.035) + capital_cost = costs.at["CCGT", "efficiency"] * costs.at["CCGT", "fixed"] n.madd( "Link", @@ -882,9 +872,9 @@ def add_methanol_to_power(n, costs, types={}): p_nom_extendable=True, capital_cost=capital_cost, marginal_cost=2, - efficiency=0.58, + efficiency=costs.at["CCGT", "efficiency"], efficiency2=costs.at["methanolisation", "carbondioxide-input"], - lifetime=25, + lifetime=costs.at["CCGT", "lifetime"], ) if types["ccgt_cc"]: @@ -895,7 +885,7 @@ def add_methanol_to_power(n, costs, types={}): # TODO consider efficiency changes / energy inputs for CC # efficiency * EUR/MW * (annuity + FOM) - capital_cost = 0.58 * 916e3 * (calculate_annuity(25, 0.07) + 0.035) + capital_cost = costs.at["CCGT", "efficiency"] * costs.at["CCGT", "fixed"] capital_cost_cc = ( capital_cost @@ -915,12 +905,12 @@ def add_methanol_to_power(n, costs, types={}): p_nom_extendable=True, capital_cost=capital_cost_cc, marginal_cost=2, - efficiency=0.58, + efficiency=costs.at["CCGT", "efficiency"], efficiency2=costs.at["cement capture", "capture_rate"] * costs.at["methanolisation", "carbondioxide-input"], efficiency3=(1 - costs.at["cement capture", "capture_rate"]) * costs.at["methanolisation", "carbondioxide-input"], - lifetime=25, + lifetime=costs.at["CCGT", "lifetime"], ) if types["ocgt"]: @@ -935,15 +925,11 @@ def add_methanol_to_power(n, costs, types={}): bus2="co2 atmosphere", carrier="OCGT methanol", p_nom_extendable=True, - capital_cost=0.35 - * 458e3 - * ( - calculate_annuity(25, 0.07) + 0.035 - ), # efficiency * EUR/MW * (annuity + FOM) + capital_cost=costs.at["OCGT", "fixed"] * costs.at["OCGT", "efficiency"], marginal_cost=2, - efficiency=0.35, + efficiency=costs.at["OCGT", "efficiency"], efficiency2=costs.at["methanolisation", "carbondioxide-input"], - lifetime=25, + lifetime=costs.at["OCGT", "lifetime"], ) @@ -978,7 +964,7 @@ def add_methanol_to_olefins(n, costs): n.madd( "Link", - spatial.methanol.locations, + nodes, suffix=f" {tech}", carrier=tech, capital_cost=costs.at[tech, "fixed"] / costs.at[tech, "methanol-input"], @@ -1000,7 +986,6 @@ def add_methanol_to_olefins(n, costs): def add_methanol_to_kerosene(n, costs): nodes = pop_layout.index nhours = n.snapshot_weightings.generators.sum() - nyears = nhours / 8760 demand_factor = options["aviation_demand_factor"] @@ -1022,7 +1007,7 @@ def add_methanol_to_kerosene(n, costs): n.madd( "Link", - spatial.methanol.locations, + spatial.h2.locations, suffix=f" {tech}", carrier=tech, capital_cost=capital_cost, @@ -1041,15 +1026,13 @@ def add_methanol_to_kerosene(n, costs): def add_methanol_reforming(n, costs): logger.info("Adding methanol steam reforming.") - nodes = pop_layout.index - tech = "Methanol steam reforming" capital_cost = costs.at[tech, "fixed"] / costs.at[tech, "methanol-input"] n.madd( "Link", - spatial.methanol.locations, + spatial.h2.locations, suffix=f" {tech}", bus0=spatial.methanol.nodes, bus1=spatial.h2.nodes, @@ -1066,8 +1049,6 @@ def add_methanol_reforming(n, costs): def add_methanol_reforming_cc(n, costs): logger.info("Adding methanol steam reforming with carbon capture.") - nodes = pop_layout.index - tech = "Methanol steam reforming" # TODO: heat release and electricity demand for process and carbon capture @@ -1084,7 +1065,7 @@ def add_methanol_reforming_cc(n, costs): n.madd( "Link", - nodes, + spatial.h2.locations, suffix=f" {tech} CC", bus0=spatial.methanol.nodes, bus1=spatial.h2.nodes, @@ -2579,43 +2560,28 @@ def add_heat(n: pypsa.Network, costs: pd.DataFrame, cop: xr.DataArray): def add_methanol(n, costs): - logger.info("Add methanol") - n.add("Carrier", "methanol") - - n.madd( - "Bus", - spatial.methanol.nodes, - location=spatial.methanol.locations, - carrier="methanol", - unit="MWh_LHV", - ) + methanol_options = options["methanol"] + if not any(methanol_options.values()): + return - n.madd( - "Store", - spatial.methanol.nodes, - suffix=" Store", - bus=spatial.methanol.nodes, - e_nom_extendable=True, - e_cyclic=True, - carrier="methanol", - capital_cost=0.02, - ) + logger.info("Add methanol") + add_carrier_buses(n, "methanol") if n.buses.carrier.str.contains("biomass").any(): - if options["biomass_to_methanol"]: + if methanol_options["biomass_to_methanol"]: add_biomass_to_methanol(n, costs) - if options["biomass_to_methanol"]: + if methanol_options["biomass_to_methanol"]: add_biomass_to_methanol_cc(n, costs) - if options["methanol_to_power"]: - add_methanol_to_power(n, costs, types=options["methanol_to_power"]) + if methanol_options["methanol_to_power"]: + add_methanol_to_power(n, costs, types=methanol_options["methanol_to_power"]) - if options["methanol_reforming"]: + if methanol_options["methanol_reforming"]: add_methanol_reforming(n, costs) - if options["methanol_reforming_cc"]: + if methanol_options["methanol_reforming_cc"]: add_methanol_reforming_cc(n, costs) @@ -3398,7 +3364,7 @@ def add_industry(n, costs): / nhours ) - if not options["regional_methanol_demand"]: + if not options["methanol"]["regional_methanol_demand"]: p_set_methanol = p_set_methanol.sum() n.madd( @@ -3521,7 +3487,7 @@ def add_industry(n, costs): * efficiency ) - if not options["regional_methanol_demand"]: + if not options["methanol"]["regional_methanol_demand"]: p_set_methanol_shipping = p_set_methanol_shipping.sum() n.madd( @@ -3819,7 +3785,7 @@ def add_industry(n, costs): efficiency3=process_co2_per_naphtha, ) - if options["methanol_to_olefins"]: + if options["methanol"]["methanol_to_olefins"]: add_methanol_to_olefins(n, costs) # aviation @@ -3866,7 +3832,7 @@ def add_industry(n, costs): efficiency2=costs.at["oil", "CO2 intensity"], ) - if options["methanol_to_kerosene"]: + if options["methanol"]["methanol_to_kerosene"]: add_methanol_to_kerosene(n, costs) # TODO simplify bus expression From 20d5eb7cb735dc1ee541a516299a4004c903991c Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 26 Aug 2024 15:18:39 +0200 Subject: [PATCH 230/344] update nuclear EAF based on 2021-2023 IAEA data (#1236) --- data/nuclear_p_max_pu.csv | 31 ++++++++++++++++--------------- doc/release_notes.rst | 4 ++++ 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/data/nuclear_p_max_pu.csv b/data/nuclear_p_max_pu.csv index 0fdb5e5b4..5a8d1d131 100644 --- a/data/nuclear_p_max_pu.csv +++ b/data/nuclear_p_max_pu.csv @@ -1,16 +1,17 @@ country,factor -BE,0.796 -BG,0.894 -CZ,0.827 -FI,0.936 -FR,0.71 -DE,0.871 -HU,0.913 -NL,0.868 -RO,0.909 -SK,0.9 -SI,0.913 -ES,0.897 -SE,0.851 -CH,0.87 -GB,0.656 +BE,0.883 +BG,0.876 +CZ,0.839 +FI,0.924 +FR,0.616 +DE,0.926 +HU,0.891 +NL,0.901 +RO,0.906 +SK,0.908 +SI,0.884 +ES,0.883 +SE,0.817 +CH,0.834 +GB,0.684 +UA,0.701 diff --git a/doc/release_notes.rst b/doc/release_notes.rst index c9359f717..48c10ea69 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -10,6 +10,10 @@ Release Notes Upcoming Release ================ +* Updated country-specific Energy Availability Factors (EAFs) for nuclear power + plants based on `IAEA 2021-2023 reported country averages + `__. + * Update GEM Europe Gas Tracker to May 2024 version. * Add investment period dependent CO2 sequestration potentials From 66051e04d4b09bc80de75fb13ad71355ab83a9ae Mon Sep 17 00:00:00 2001 From: Tom Brown Date: Mon, 26 Aug 2024 16:51:57 +0200 Subject: [PATCH 231/344] build_electricity: raise memory for build_tranmission_projects (#1237) 2 GB RAM was not enough, to avoid out-of-memory problems raised to 4 GB. --- rules/build_electricity.smk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rules/build_electricity.smk b/rules/build_electricity.smk index 1f568e99c..6568c8ca8 100644 --- a/rules/build_electricity.smk +++ b/rules/build_electricity.smk @@ -373,7 +373,7 @@ rule build_transmission_projects: benchmark: benchmarks("build_transmission_projects") resources: - mem_mb=2000, + mem_mb=4000, threads: 1 conda: "../envs/environment.yaml" From bbff4297f97f6593f462da1cbc8a1ea737353dc9 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 26 Aug 2024 17:16:06 +0200 Subject: [PATCH 232/344] temporarily disable n.shapes until memory issues resolved (#1238) --- scripts/base_network.py | 4 ++-- scripts/cluster_network.py | 4 ++-- scripts/simplify_network.py | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/base_network.py b/scripts/base_network.py index afb66387e..0f661c284 100644 --- a/scripts/base_network.py +++ b/scripts/base_network.py @@ -1008,12 +1008,12 @@ def append_bus_shapes(n, shapes, type): ) shapes.to_file(snakemake.output.regions_onshore) - append_bus_shapes(n, shapes, "onshore") + # append_bus_shapes(n, shapes, "onshore") if offshore_regions: shapes = pd.concat(offshore_regions, ignore_index=True) shapes.to_file(snakemake.output.regions_offshore) - append_bus_shapes(n, shapes, "offshore") + # append_bus_shapes(n, shapes, "offshore") else: offshore_shapes.to_frame().to_file(snakemake.output.regions_offshore) diff --git a/scripts/cluster_network.py b/scripts/cluster_network.py index b63747c24..4460b0adb 100644 --- a/scripts/cluster_network.py +++ b/scripts/cluster_network.py @@ -557,12 +557,12 @@ def plot_busmap_for_n_clusters(n, n_clusters, solver_name="scip", fn=None): ): # also available: linemap_positive, linemap_negative getattr(clustering, attr).to_csv(snakemake.output[attr]) - nc.shapes = n.shapes.copy() + # nc.shapes = n.shapes.copy() for which in ["regions_onshore", "regions_offshore"]: regions = gpd.read_file(snakemake.input[which]) clustered_regions = cluster_regions((clustering.busmap,), regions) clustered_regions.to_file(snakemake.output[which]) - append_bus_shapes(nc, clustered_regions, type=which.split("_")[1]) + # append_bus_shapes(nc, clustered_regions, type=which.split("_")[1]) nc.meta = dict(snakemake.config, **dict(wildcards=dict(snakemake.wildcards))) nc.export_to_netcdf(snakemake.output.network) diff --git a/scripts/simplify_network.py b/scripts/simplify_network.py index 124618cb1..e95891203 100644 --- a/scripts/simplify_network.py +++ b/scripts/simplify_network.py @@ -669,7 +669,7 @@ def find_closest_bus(n, x, y, tol=2000): n.lines.drop(remove, axis=1, errors="ignore", inplace=True) if snakemake.wildcards.simpl: - shapes = n.shapes + # shapes = n.shapes n, cluster_map = cluster( n, int(snakemake.wildcards.simpl), @@ -679,7 +679,7 @@ def find_closest_bus(n, x, y, tol=2000): params.simplify_network["feature"], params.aggregation_strategies, ) - n.shapes = shapes + # n.shapes = shapes busmaps.append(cluster_map) update_p_nom_max(n) @@ -691,7 +691,7 @@ def find_closest_bus(n, x, y, tol=2000): regions = gpd.read_file(snakemake.input[which]) clustered_regions = cluster_regions(busmaps, regions) clustered_regions.to_file(snakemake.output[which]) - append_bus_shapes(n, clustered_regions, type=which.split("_")[1]) + # append_bus_shapes(n, clustered_regions, type=which.split("_")[1]) n.meta = dict(snakemake.config, **dict(wildcards=dict(snakemake.wildcards))) n.export_to_netcdf(snakemake.output.network) From 3f94f89a84abb0f5ff2e3090dfe2c3dd41948ac6 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 26 Aug 2024 17:38:30 +0200 Subject: [PATCH 233/344] build_powerplants: can assign to all buses not just substations (#1239) --- scripts/build_powerplants.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/build_powerplants.py b/scripts/build_powerplants.py index bde2bd38c..47cf2e141 100755 --- a/scripts/build_powerplants.py +++ b/scripts/build_powerplants.py @@ -206,12 +206,11 @@ def replace_natural_gas_fueltype(df): # Add "everywhere powerplants" to all bus locations ppl = add_everywhere_powerplants( - ppl, n.buses.query("substation_lv"), snakemake.params.everywhere_powerplants + ppl, n.buses, snakemake.params.everywhere_powerplants ) - substations = n.buses.query("substation_lv") ppl = ppl.dropna(subset=["lat", "lon"]) - ppl = map_country_bus(ppl, substations) + ppl = map_country_bus(ppl, n.buses) bus_null_b = ppl["bus"].isnull() if bus_null_b.any(): From 9c0ab3e0c0eb3e1c147d528e9454c0cb1173f9f6 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 28 Aug 2024 14:41:54 +0200 Subject: [PATCH 234/344] amend codespell blacklist --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8b9bf2e00..7f7453028 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,7 +33,7 @@ repos: rev: v2.3.0 hooks: - id: codespell - args: ['--ignore-regex="(\b[A-Z]+\b)"', '--ignore-words-list=fom,appartment,bage,ore,setis,tabacco,berfore,vor'] # Ignore capital case words, e.g. country codes + args: ['--ignore-regex="(\b[A-Z]+\b)"', '--ignore-words-list=fom,appartment,bage,ore,setis,tabacco,berfore,vor,pris'] # Ignore capital case words, e.g. country codes types_or: [python, rst, markdown] files: ^(scripts|doc)/ From 550bda4b440f71ec9f165a0e0e1ad2f74f50d4d7 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 29 Aug 2024 11:49:37 +0200 Subject: [PATCH 235/344] eurostat_co2: fix update to 2023 energy balances --- scripts/build_energy_totals.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index 5aefb26b6..fe4b9e703 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -1065,9 +1065,9 @@ def build_eurostat_co2(eurostat: pd.DataFrame, year: int = 1990) -> pd.Series: specific_emissions = pd.Series(index=eurostat.columns, dtype=float) # emissions in tCO2_equiv per MWh_th - specific_emissions["Solid fuels"] = 0.36 # Approximates coal - specific_emissions["Oil (total)"] = 0.285 # Average of distillate and residue - specific_emissions["Gas"] = 0.2 # For natural gas + specific_emissions["Solid fossil fuels"] = 0.36 # Approximates coal + specific_emissions["Oil and petroleum products"] = 0.285 # Average of distillate and residue + specific_emissions["Natural gas"] = 0.2 # For natural gas return eurostat_year.multiply(specific_emissions).sum(axis=1) @@ -1099,7 +1099,7 @@ def build_co2_totals( co2 = eea_co2.reindex(countries) - for ct in pd.Index(countries).intersection(["BA", "RS", "AL", "ME", "MK"]): + for ct in pd.Index(countries).intersection(["BA", "RS", "AL", "ME", "MK", "UA", "MD"]): mappings = { "electricity": (ct, "+", "Electricity & heat generation", np.nan), "residential non-elec": (ct, "+", "+", "Residential"), From 0c296aa28ffca80032dece42dd5608d0e3838f2f Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 29 Aug 2024 12:04:27 +0200 Subject: [PATCH 236/344] change base_network default to 'osm-prebuilt' --- config/config.default.yaml | 2 +- doc/release_notes.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 0fd6e9869..22a5ac2bf 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -87,7 +87,7 @@ co2_budget: # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#electricity electricity: voltages: [200., 220., 300., 380., 500., 750.] - base_network: entsoegridkit + base_network: osm-prebuilt gaslimit_enable: false gaslimit: false co2limit_enable: false diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 48c10ea69..caf73292e 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -81,7 +81,7 @@ Upcoming Release * Enable parallelism in :mod:`determine_availability_matrix_MD_UA.py` and remove plots. This requires the use of temporary files. -* Added new major feature to create the base_network from OpenStreetMap (OSM) data (PR https://github.com/PyPSA/pypsa-eur/pull/1079). Note that a heuristics based cleaning process is used for lines and links where electrical parameters are incomplete, missing, or ambiguous. Through ``electricity["base_network"]``, the base network can be set to "entsoegridkit" (original default setting, deprecated soon), "osm-prebuilt" (which downloads the latest prebuilt snapshot based on OSM data from Zenodo), or "osm-raw" which retrieves (once) and cleans the raw OSM data and subsequently builds the network. Note that this process may take a few minutes. +* Added new major feature to create the base_network from OpenStreetMap (OSM) data (PR https://github.com/PyPSA/pypsa-eur/pull/1079). Note that a heuristics based cleaning process is used for lines and links where electrical parameters are incomplete, missing, or ambiguous. Through ``electricity["base_network"]``, the base network can be set to "entsoegridkit" (now deprecated), "osm-prebuilt" (default, downloads the latest prebuilt snapshot based on OSM data from Zenodo), or "osm-raw" which retrieves (once) and cleans the raw OSM data and subsequently builds the network. Note that this process may take a few minutes. * Updated pre-built `weather data cutouts `__. These are now merged cutouts with From 7665de61d3f163005392a8f12130439274e61803 Mon Sep 17 00:00:00 2001 From: Philipp Glaum Date: Thu, 29 Aug 2024 15:34:44 +0200 Subject: [PATCH 237/344] prepare_sector_network: convert e_max_pu timeseries for msw stores to pd.DataFrame. --- scripts/prepare_sector_network.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index b6644d6dc..cc4c7f3a8 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2345,7 +2345,12 @@ def add_biomass(n, costs): carrier="municipal solid waste", ) - e_max_pu = pd.Series([1] * (len(n.snapshots) - 1) + [0], index=n.snapshots) + e_max_pu = np.array( + len(spatial.msw.nodes) * [[1] * (len(n.snapshots) - 1) + [0]] + ).T + e_max_pu = pd.DataFrame( + e_max_pu, index=n.snapshots, columns=spatial.msw.nodes + ).astype(float) n.madd( "Store", spatial.msw.nodes, From bca4f95a070e7a1623c6d88c6517894236804cd9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 29 Aug 2024 13:38:39 +0000 Subject: [PATCH 238/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/build_energy_totals.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index fe4b9e703..24ba86bbd 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -1066,7 +1066,9 @@ def build_eurostat_co2(eurostat: pd.DataFrame, year: int = 1990) -> pd.Series: # emissions in tCO2_equiv per MWh_th specific_emissions["Solid fossil fuels"] = 0.36 # Approximates coal - specific_emissions["Oil and petroleum products"] = 0.285 # Average of distillate and residue + specific_emissions["Oil and petroleum products"] = ( + 0.285 # Average of distillate and residue + ) specific_emissions["Natural gas"] = 0.2 # For natural gas return eurostat_year.multiply(specific_emissions).sum(axis=1) @@ -1099,7 +1101,9 @@ def build_co2_totals( co2 = eea_co2.reindex(countries) - for ct in pd.Index(countries).intersection(["BA", "RS", "AL", "ME", "MK", "UA", "MD"]): + for ct in pd.Index(countries).intersection( + ["BA", "RS", "AL", "ME", "MK", "UA", "MD"] + ): mappings = { "electricity": (ct, "+", "Electricity & heat generation", np.nan), "residential non-elec": (ct, "+", "+", "Residential"), From 1b2ec575bc95a9213188839283f3a74a512df220 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 30 Aug 2024 10:26:34 +0200 Subject: [PATCH 239/344] retrieve urban population fraction from World Bank API (#1248) * retrieve urban population fraction from World Bank API * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- data/urban_percent.csv | 30 ---------------------------- doc/configtables/licenses-sector.csv | 1 - doc/release_notes.rst | 6 ++++++ rules/build_sector.smk | 2 +- rules/retrieve.smk | 24 ++++++++++++++++++++++ scripts/build_population_layouts.py | 20 +++++++++---------- 6 files changed, 40 insertions(+), 43 deletions(-) delete mode 100644 data/urban_percent.csv diff --git a/data/urban_percent.csv b/data/urban_percent.csv deleted file mode 100644 index d57e07281..000000000 --- a/data/urban_percent.csv +++ /dev/null @@ -1,30 +0,0 @@ -AT,66 -BA,40 -BE,98 -BG,74 -CH,74 -CZ,73 -DE,75 -DK,88 -EE,68 -ES,80 -FI,84 -FR,80 -GB,83 -GR,78 -HR,59 -HU,71 -IE,63 -IT,69 -LT,67 -LU,90 -LV,67 -NL,90 -NO,80 -PL,61 -PT,63 -RO,55 -RS,56 -SE,86 -SI,50 -SK,54 diff --git a/doc/configtables/licenses-sector.csv b/doc/configtables/licenses-sector.csv index a44e0a5d5..8c721260d 100644 --- a/doc/configtables/licenses-sector.csv +++ b/doc/configtables/licenses-sector.csv @@ -1,6 +1,5 @@ description,file/folder,licence,source JRC IDEES database,jrc-idees-2015/,CC BY 4.0,https://ec.europa.eu/jrc/en/potencia/jrc-idees -urban/rural fraction,urban_percent.csv,unknown,unknown JRC ENSPRESO biomass potentials,remote,CC BY 4.0,https://data.jrc.ec.europa.eu/dataset/74ed5a04-7d74-4807-9eab-b94774309d9f EEA emission statistics,eea/UNFCCC_v23.csv,EEA standard re-use policy,https://www.eea.europa.eu/data-and-maps/data/national-emissions-reported-to-the-unfccc-and-to-the-eu-greenhouse-gas-monitoring-mechanism-16 Eurostat Energy Balances,eurostat-energy_balances-*/,Eurostat,https://ec.europa.eu/eurostat/web/energy/data/energy-balances diff --git a/doc/release_notes.rst b/doc/release_notes.rst index caf73292e..d5fbe7819 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -1,3 +1,4 @@ + .. SPDX-FileCopyrightText: 2019-2024 The PyPSA-Eur Authors @@ -10,6 +11,11 @@ Release Notes Upcoming Release ================ +* Retrieve share of urban population from `World Bank API + `__. The data + originates from the United Nations Population Division. Previously, a file + ``data/urban_percent.csv`` with an undocumented source was used. + * Updated country-specific Energy Availability Factors (EAFs) for nuclear power plants based on `IAEA 2021-2023 reported country averages `__. diff --git a/rules/build_sector.smk b/rules/build_sector.smk index c1e4d573f..f95731b78 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -6,7 +6,7 @@ rule build_population_layouts: input: nuts3_shapes=resources("nuts3_shapes.geojson"), - urban_percent="data/urban_percent.csv", + urban_percent="data/worldbank/API_SP.URB.TOTL.IN.ZS_DS2_en_csv_v2_3403768.csv", cutout=lambda w: "cutouts/" + CDIR + config_provider("atlite", "default_cutout")(w) diff --git a/rules/retrieve.smk b/rules/retrieve.smk index df1f7379f..5c4af68e0 100644 --- a/rules/retrieve.smk +++ b/rules/retrieve.smk @@ -259,6 +259,30 @@ if config["enable"]["retrieve"]: +if config["enable"]["retrieve"]: + + rule retrieve_worldbank_urban_population: + params: + zip="data/worldbank/API_SP.URB.TOTL.IN.ZS_DS2_en_csv_v2_3403768.zip", + output: + gpkg="data/worldbank/API_SP.URB.TOTL.IN.ZS_DS2_en_csv_v2_3403768.csv", + run: + import os + import requests + + response = requests.get( + "https://api.worldbank.org/v2/en/indicator/SP.URB.TOTL.IN.ZS?downloadformat=csv", + params={"name": "API_SP.URB.TOTL.IN.ZS_DS2_en_csv_v2_3403768.zip"}, + ) + + with open(params["zip"], "wb") as f: + f.write(response.content) + output_folder = Path(params["zip"]).parent + unpack_archive(params["zip"], output_folder) + os.remove(params["zip"]) + + + if config["enable"]["retrieve"]: # Download directly from naciscdn.org which is a redirect from naturalearth.com diff --git a/scripts/build_population_layouts.py b/scripts/build_population_layouts.py index ca664ed0a..4cabdc7cd 100644 --- a/scripts/build_population_layouts.py +++ b/scripts/build_population_layouts.py @@ -9,6 +9,7 @@ import logging import atlite +import country_converter as coco import geopandas as gpd import numpy as np import pandas as pd @@ -17,6 +18,10 @@ logger = logging.getLogger(__name__) +cc = coco.CountryConverter() + +coco.logging.getLogger().setLevel(coco.logging.CRITICAL) + if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake @@ -45,20 +50,13 @@ countries = np.sort(nuts3.country.unique()) + urban_fraction = pd.read_csv(snakemake.input.urban_percent, skiprows=4) + iso3 = urban_fraction["Country Code"] + urban_fraction["iso2"] = cc.convert(names=iso3, src="ISO3", to="ISO2") urban_fraction = ( - pd.read_csv( - snakemake.input.urban_percent, header=None, index_col=0, names=["fraction"] - ).squeeze() - / 100.0 + urban_fraction.query("iso2 in @countries").set_index("iso2")["2019"].div(100) ) - # fill missing Balkans values - missing = ["AL", "ME", "MK"] - reference = ["RS", "BA"] - average = urban_fraction[reference].mean() - fill_values = pd.Series({ct: average for ct in missing}) - urban_fraction = pd.concat([urban_fraction, fill_values]) - # population in each grid cell pop_cells = pd.Series(I.dot(nuts3["pop"])) From b207b4ebe8c3b08a5d8b84deb225138612fac3c6 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 30 Aug 2024 10:26:48 +0200 Subject: [PATCH 240/344] remove unused geth2015 hydro capacities (#1246) --- data/geth2015_hydro_capacities.csv | 32 ------------------------------ rules/build_electricity.smk | 1 - scripts/add_electricity.py | 1 - 3 files changed, 34 deletions(-) delete mode 100644 data/geth2015_hydro_capacities.csv diff --git a/data/geth2015_hydro_capacities.csv b/data/geth2015_hydro_capacities.csv deleted file mode 100644 index ddb91baec..000000000 --- a/data/geth2015_hydro_capacities.csv +++ /dev/null @@ -1,32 +0,0 @@ -# Table 25 from F. Geth et al., An overview of large-scale stationary electricity storage plants in Europe (2015) 1212–1227 -country,n,p_nom_discharge,p_nom_charge,e_stor -AT,19,4.051,3.246,132.41 -BE,3,1.301,1.196,5.71 -BG,3,1.399,0.93,11.13 -HR,3,0.281,0.246,2.34 -CY,0,-,-,- -CZ,3,1.119,1.145,5.72 -DK,0,-,-,- -EE,0,-,-,- -FI ,0,-,-,- -FR,10,5.512,4.317,83.37 -DE,34,6.805,6.417,39.12 -GR,2,0.735,-,4.97 -HU,0,-,-,- -IE,1,0.292,-,1.8 -IT,25,7.833,7.64,68.27 -LV,0,-,-,- -LT,1,0.9,0.88,10.8 -LU,1,1.296,1.05,4.92 -MT,0,-,-,- -NL,0,-,-,- -PL,6,1.757,1.647,7.96 -PT,7,1.279,-,40.77 -RO,5,0.285,0.2,10.2 -SK,4,1.016,0.79,3.63 -SI,1,0.185,0.18,0.5 -ES,26,6.358,5.859,70 -SE,2,0.091,-,72.12 -GB,4,2.788,2.65,26.7 -NO,8,1.273,0.892,399.39 -CH,20,2.291,1.512,311.48 diff --git a/rules/build_electricity.smk b/rules/build_electricity.smk index 6568c8ca8..8c3ce32d6 100644 --- a/rules/build_electricity.smk +++ b/rules/build_electricity.smk @@ -469,7 +469,6 @@ rule add_electricity: regions=resources("regions_onshore.geojson"), powerplants=resources("powerplants.csv"), hydro_capacities=ancient("data/hydro_capacities.csv"), - geth_hydro_capacities="data/geth2015_hydro_capacities.csv", unit_commitment="data/unit_commitment.csv", fuel_price=lambda w: ( resources("monthly_fuel_price.csv") diff --git a/scripts/add_electricity.py b/scripts/add_electricity.py index 076eb84ed..6728ea214 100755 --- a/scripts/add_electricity.py +++ b/scripts/add_electricity.py @@ -51,7 +51,6 @@ .. image:: img/hydrocapacities.png :scale: 34 % -- ``data/geth2015_hydro_capacities.csv``: alternative to capacities above; not currently used! - ``resources/electricity_demand.csv`` Hourly per-country electricity demand profiles. - ``resources/regions_onshore.geojson``: confer :ref:`busregions` - ``resources/nuts3_shapes.geojson``: confer :ref:`shapes` From 2cf695185898e6b0adc71bc90dd8e273bb4bfd95 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 30 Aug 2024 12:02:39 +0200 Subject: [PATCH 241/344] build_ammonia_production: forward fill missing values --- scripts/build_ammonia_production.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/build_ammonia_production.py b/scripts/build_ammonia_production.py index e0e2de5e4..a82d13719 100644 --- a/scripts/build_ammonia_production.py +++ b/scripts/build_ammonia_production.py @@ -56,6 +56,8 @@ # convert from ktonN to ktonNH3 ammonia *= 17 / 14 + ammonia.ffill(axis=1, inplace=True) + ammonia.index.name = "ktonNH3/a" ammonia.to_csv(snakemake.output.ammonia_production) From bc09f1df77c097a929d2273ff3b96f9e0103579a Mon Sep 17 00:00:00 2001 From: Philipp Glaum <95913147+p-glaum@users.noreply.github.com> Date: Fri, 30 Aug 2024 12:06:11 +0200 Subject: [PATCH 242/344] prepare_sector_network.py: add oil bus whenever industry is added (#1247) * prepare_sector_network.py: add oil bus whenever industry is added * prepare_sector_network: Move added oil and methnaol buses to top of "add_industry", and add oil buses where used in "add_biomass" --- scripts/prepare_sector_network.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index cc4c7f3a8..87aa90fa8 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2447,7 +2447,7 @@ def add_biomass(n, costs): ) if biomass_potentials.filter(like="unsustainable").sum().sum() > 0: - + add_carrier_buses(n, "oil") # Create timeseries to force usage of unsustainable potentials e_max_pu = pd.DataFrame(1, index=n.snapshots, columns=spatial.gas.biogas) e_max_pu.iloc[-1] = 0 @@ -2731,6 +2731,7 @@ def add_biomass(n, costs): # Solid biomass to liquid fuel if options["biomass_to_liquid"]: + add_carrier_buses(n, "oil") n.madd( "Link", spatial.biomass.nodes, @@ -2774,7 +2775,7 @@ def add_biomass(n, costs): # Combination of efuels and biomass to liquid, both based on Fischer-Tropsch. # Experimental version - use with caution if options["electrobiofuels"]: - + add_carrier_buses(n, "oil") efuel_scale_factor = costs.at["BtL", "C stored"] name = ( pd.Index(spatial.biomass.nodes) @@ -2886,6 +2887,10 @@ def add_biomass(n, costs): def add_industry(n, costs): logger.info("Add industrial demand") + # add oil buses for shipping, aviation and naptha for industry + add_carrier_buses(n, "oil") + # add methanol buses for industry + add_carrier_buses(n, "methanol") nodes = pop_layout.index nhours = n.snapshot_weightings.generators.sum() @@ -3021,8 +3026,6 @@ def add_industry(n, costs): # methanol for industry - add_carrier_buses(n, "methanol") - n.madd( "Bus", spatial.methanol.industry, @@ -3194,8 +3197,6 @@ def add_industry(n, costs): if shipping_oil_share: - add_carrier_buses(n, "oil") - p_set_oil = shipping_oil_share * p_set.rename(lambda x: x + " shipping oil") if not options["regional_oil_demand"]: From e815c36b5c325a203a88afbf2fa0a4c87ee7115f Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 30 Aug 2024 12:13:15 +0200 Subject: [PATCH 243/344] revert ffill of ammonia production --- scripts/build_ammonia_production.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/build_ammonia_production.py b/scripts/build_ammonia_production.py index a82d13719..e0e2de5e4 100644 --- a/scripts/build_ammonia_production.py +++ b/scripts/build_ammonia_production.py @@ -56,8 +56,6 @@ # convert from ktonN to ktonNH3 ammonia *= 17 / 14 - ammonia.ffill(axis=1, inplace=True) - ammonia.index.name = "ktonNH3/a" ammonia.to_csv(snakemake.output.ammonia_production) From 3a1b57515addbcdc94b1b9f837d37543c86d5616 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 30 Aug 2024 14:53:54 +0200 Subject: [PATCH 244/344] industry: steel GEM, ammonia, refinery, cement plants non-EU (#1241) * industry distribution: split EAF + integrated using GEM GSPT, add ammonia plants * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * add release note [no ci] * add cement plant data for countries not in Hotmaps * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * add refineries data for countries not in Hotmaps --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- data/ammonia_plants.csv | 37 +++ data/cement-plants-noneu.csv | 26 ++ data/refineries-noneu.csv | 15 ++ doc/release_notes.rst | 25 ++ rules/build_sector.smk | 6 +- rules/retrieve.smk | 17 ++ scripts/build_industrial_distribution_key.py | 232 +++++++++++++++++- ...industrial_energy_demand_per_node_today.py | 8 +- .../build_industrial_production_per_node.py | 8 +- 9 files changed, 360 insertions(+), 14 deletions(-) create mode 100644 data/ammonia_plants.csv create mode 100644 data/cement-plants-noneu.csv create mode 100644 data/refineries-noneu.csv diff --git a/data/ammonia_plants.csv b/data/ammonia_plants.csv new file mode 100644 index 000000000..1317ceb95 --- /dev/null +++ b/data/ammonia_plants.csv @@ -0,0 +1,37 @@ +"Plant","Ammonia [kt/a]","Urea [kt/a]","AN [kt/a]","UAN [kt/a]","CAN [kt/a]","Nitrogen fertilisers [kt/a]","MAP [kt/a]","Country","Latitude","Longitude","Date","Source","Comment" +"Yara Sluiskil","1700","1300",,,,,,"Netherlands","51.27186","3.84896","13-01-2023","https://www.icis.com/explore/resources/news/2023/01/18/10846094/insight-poor-demand-high-costs-stifle-europe-industry-despite-falling-gas-prices/", +"OCI Geleen","550",,,,,,,"Netherlands","50.99738","5.78497","13-01-2023","https://www.icis.com/explore/resources/news/2023/01/18/10846094/insight-poor-demand-high-costs-stifle-europe-industry-despite-falling-gas-prices/", +"CF Fertilisers Billingham","590",,,,,,,"United Kingdom","53.28149","-2.79744","13-01-2023","https://www.icis.com/explore/resources/news/2023/01/18/10846094/insight-poor-demand-high-costs-stifle-europe-industry-despite-falling-gas-prices/; https://www.cfindustries.com/what-we-do/ammonia-productiont", +"Yara Tertre","420",,,,,,,"Belgium","50.47842","3.80285","13-01-2023","https://www.icis.com/explore/resources/news/2023/01/18/10846094/insight-poor-demand-high-costs-stifle-europe-industry-despite-falling-gas-prices/", +"BASF Antwerp","650",,,,,,,"Belgium","51.35583","4.27811","13-01-2023","https://www.icis.com/explore/resources/news/2023/01/18/10846094/insight-poor-demand-high-costs-stifle-europe-industry-despite-falling-gas-prices/", +"Yara Gonfreville","400",,,,,,,"France","49.47982","0.19587","13-01-2023","https://www.icis.com/explore/resources/news/2023/01/18/10846094/insight-poor-demand-high-costs-stifle-europe-industry-despite-falling-gas-prices/", +"Borealis Rouen",,,,,,,,"France","49.41916","1.02584","13-01-2023","https://www.icis.com/explore/resources/news/2023/01/18/10846094/insight-poor-demand-high-costs-stifle-europe-industry-despite-falling-gas-prices/", +"BASF Ludwigshafen","880",,,,,,,"Germany","49.51171","8.42009","13-01-2023","https://www.icis.com/explore/resources/news/2023/01/18/10846094/insight-poor-demand-high-costs-stifle-europe-industry-despite-falling-gas-prices/", +"SKW","1300",,,,,,,"Germany","51.87746","12.5858","13-01-2023","https://www.icis.com/explore/resources/news/2023/01/18/10846094/insight-poor-demand-high-costs-stifle-europe-industry-despite-falling-gas-prices/", +"Yara Brunsbuettel","750",,,,,,,"Germany","53.91152","9.20982","13-01-2023","https://www.icis.com/explore/resources/news/2023/01/18/10846094/insight-poor-demand-high-costs-stifle-europe-industry-despite-falling-gas-prices/", +"Yara Porsgrunn","500",,,,,,,"Norway","59.12444","9.61922","13-01-2023","https://www.icis.com/explore/resources/news/2023/01/18/10846094/insight-poor-demand-high-costs-stifle-europe-industry-despite-falling-gas-prices/", +"Fertiberia Puertollano","400","250",,"200",,,,"Spain","38.67276","-4.0608","13-01-2023","https://www.icis.com/explore/resources/news/2023/01/18/10846094/insight-poor-demand-high-costs-stifle-europe-industry-despite-falling-gas-prices/; https://www.icis.com/explore/resources/news/2021/10/25/10698264/spain-s-fertiberia-urea-ammonia-plants-in-palos-to-remain-shut-on-gas-prices/", +"Fertiberia Huelva","200","130",,,,,,"Spain","37.22572","-6.94742","13-01-2023","https://www.icis.com/explore/resources/news/2023/01/18/10846094/insight-poor-demand-high-costs-stifle-europe-industry-despite-falling-gas-prices/; https://www.icis.com/explore/resources/news/2021/10/25/10698264/spain-s-fertiberia-urea-ammonia-plants-in-palos-to-remain-shut-on-gas-prices/", +"Yara Ferrara","600","600",,,,,,"Italy","44.86206","11.57922","13-01-2023","https://www.icis.com/explore/resources/news/2023/01/18/10846094/insight-poor-demand-high-costs-stifle-europe-industry-despite-falling-gas-prices/", +"Petrokemija",,"500",,,,,,"Croatia","45.46977","16.78944","13-01-2023","https://www.icis.com/explore/resources/news/2023/01/18/10846094/insight-poor-demand-high-costs-stifle-europe-industry-despite-falling-gas-prices/", +"Neochim","450",,"710",,,,,"Bulgaria","42.20514","25.61547","13-01-2023","https://www.icis.com/explore/resources/news/2023/01/18/10846094/insight-poor-demand-high-costs-stifle-europe-industry-despite-falling-gas-prices/", +"Agopolychim",,,"400","800",,,"300","Bulgaria","43.20652","27.65983","13-01-2023","https://www.icis.com/explore/resources/news/2023/01/18/10846094/insight-poor-demand-high-costs-stifle-europe-industry-despite-falling-gas-prices/; https://ammoniaenergy.org/articles/agropolychim-investing-in-ammonia-distribution-from-bulgaria/","no own ammonia plant" +"Azomures","1600",,,,,,,"Romania","46.76187","24.42361","13-01-2023","https://www.icis.com/explore/resources/news/2023/01/18/10846094/insight-poor-demand-high-costs-stifle-europe-industry-despite-falling-gas-prices/","Total for ammonia, AN, NPK, UAN, Urea" +"Nitrogenmuvek","497",,,,"1300",,,"Hungary","47.16691","18.12844","13-01-2023","https://www.icis.com/explore/resources/news/2023/01/18/10846094/insight-poor-demand-high-costs-stifle-europe-industry-despite-falling-gas-prices/", +"DUSLO","530",,,,,,,"Slovakia","48.18705","17.9301","13-01-2023","https://www.icis.com/explore/resources/news/2023/01/18/10846094/insight-poor-demand-high-costs-stifle-europe-industry-despite-falling-gas-prices/", +"Grupa Azoty","524","375",,,,,,"Poland","51.45179","21.98174","13-01-2023","https://www.icis.com/explore/resources/news/2023/01/18/10846094/insight-poor-demand-high-costs-stifle-europe-industry-despite-falling-gas-prices/", +"Anwil",,,,,,"965",,"Poland","52.70438","18.96606","13-01-2023","https://www.icis.com/explore/resources/news/2023/01/18/10846094/insight-poor-demand-high-costs-stifle-europe-industry-despite-falling-gas-prices/", +"Achema","1100","785","651","1300","540",,,"Lithuania","55.081","24.32377","13-01-2023","https://www.icis.com/explore/resources/news/2023/01/18/10846094/insight-poor-demand-high-costs-stifle-europe-industry-despite-falling-gas-prices/", +"Lifosa",,,,,,"3241",,"Lithuania","55.27567","24.01652","13-01-2023","https://www.icis.com/explore/resources/news/2023/01/18/10846094/insight-poor-demand-high-costs-stifle-europe-industry-despite-falling-gas-prices/", +"Unknown (Linz)",,,,,,,,"Austria","48.28749","14.32369","15-08-2024",,"most likely location" +"Unknown (Kothla-Järve)",,,,,,,,"Estonia","59.39984","27.25401","15-08-2024",,"most likely location" +"Unknown (Oulu)",,,,,,,,"Finland","65.0033","25.43731","15-08-2024",,"most likely location" +"Unknown (Ptolemaida)",,,,,,,,"Greece","40.50701","21.71414","15-08-2024",,"most likely location" +"Unknown (Sabac)",,,,,,,,"Serbia","44.76298","19.68426","15-08-2024",,"most likely location" +"Unknown (Lugano)",,,,,,,,"Switzerland","46.02709","8.9641","15-08-2024",,"most likely location" +"Rivneazot","420",,"540","300","470 ",,,"Ukraine","50.70502","26.1630","28-08-2024","https://interfax.com/newsroom/top-stories/100959/; https://interfax.com/newsroom/top-stories/103386/; https://gmk.center/en/news/ostchem-plants-produced-520-6-thousand-tons-of-fertilizers-in-q1/; http://www.ostchem.com/en/o-kompanii/proizvodstvo/rovno", +"Stirol (Horlivka)","1470","940 ","693",,,,,"Ukraine","48.29697","38.10412","28-08-2024","http://ostchem.com/en/o-kompanii/proizvodstvo/stirol", +"Severodonetsk Azot","1020","390","550",,,,,"Ukraine","48.94185","38.47399","28-08-2024",, +"Odessa Port Plant ","1160","942",,,,,,"Ukraine","46.49460","30.73222","28-08-2024","https://www.spfu.gov.ua/userfiles/files/OPP_Teaser_Eng.pdf", +"Cherkazy Azot","963",,"970","500",,,,"Ukraine","49.38241","32.05719","28-08-2024","https://gmk.center/en/news/ostchem-plants-produced-520-6-thousand-tons-of-fertilizers-in-q1/https://gmk.center/en/news/ostchem-plants-produced-520-6-thousand-tons-of-fertilizers-in-q1/; http://ostchem.com/en/o-kompanii/proizvodstvo/azot", +"DneproAZOT","660",,,,,,,"Ukraine","48.49004","34.66667","28-08-2024","https://www.alfalaval.com/media/stories/fertilizers/increased-plant-capacity-and-reduced-maintenance-at-fertilizer-plant/", diff --git a/data/cement-plants-noneu.csv b/data/cement-plants-noneu.csv new file mode 100644 index 000000000..27c046c03 --- /dev/null +++ b/data/cement-plants-noneu.csv @@ -0,0 +1,26 @@ +"Company","Site","Cement [kt/a]","Country","Latitude","Longitude","Date","Source","Comment" +"Titan Cementarnica USJE AD","Skopje",910,"MK",41.96816,21.45604,"28-08-2024","https://www.usgs.gov/centers/national-minerals-information-center/international-minerals-statistics-and-information", +"Colacem Albania Sh.p.k","Balldre plant, Lezhe",500,"AL",41.83689,19.63339,"28-08-2024","https://www.usgs.gov/centers/national-minerals-information-center/international-minerals-statistics-and-information", +"Fushe Kruje Cement Factory, Sh.p.k.","Elbasan cement plant",300,"AL",41.12184,20.04347,"28-08-2024","https://www.usgs.gov/centers/national-minerals-information-center/international-minerals-statistics-and-information", +"Fushe Kruje Cement Factory, Sh.p.k.","Fushe Kruje plant",1330,"AL",40.46403,19.49948,"28-08-2024","https://www.usgs.gov/centers/national-minerals-information-center/international-minerals-statistics-and-information", +"Antea Cement Sh.A","Boka e Kuqe, Burizane",1500,"AL",41.54838,19.72574,"28-08-2024","https://www.usgs.gov/centers/national-minerals-information-center/international-minerals-statistics-and-information", +"Beocinska Fabrika Cementa","Beocin",2000,"RS",45.20952,19.71043,"28-08-2024","https://www.usgs.gov/centers/national-minerals-information-center/international-minerals-statistics-and-information", +"Holcim (Srbija) a.d.","Novi Popovac",1400,"RS",43.90633,21.50378,"28-08-2024","https://www.usgs.gov/centers/national-minerals-information-center/international-minerals-statistics-and-information", +"Titan Cementara Kosjerić Ltd.","Kosjeric",750,"RS",44.0125,19.88831,"28-08-2024","https://www.usgs.gov/centers/national-minerals-information-center/international-minerals-statistics-and-information", +"Sharrcem Sh.p.k.","Hani i Elezit,",850,"XK",42.14704,21.29863,"28-08-2024","https://www.usgs.gov/centers/national-minerals-information-center/international-minerals-statistics-and-information", +"Fabrika Cementa Lukavac d.d.","Lukavac",800,"BA",44.52596,18.52811,"28-08-2024","https://www.usgs.gov/centers/national-minerals-information-center/international-minerals-statistics-and-information", +"Tvornica Cementa Kakanj d.d. ","Kakanj",400,"BA",44.11922,18.10897,"28-08-2024","https://www.usgs.gov/centers/national-minerals-information-center/international-minerals-statistics-and-information", +"Lafarge Ciment Moldova SA","Rezina",1400,"MD",47.79094,28.9553,"28-08-2024","https://www.usgs.gov/centers/national-minerals-information-center/international-minerals-statistics-and-information", +"ZAO Rybnitsa Cement Comple","Rybnitsa",1100,"MD",47.78115,29.02078,"28-08-2024","https://www.usgs.gov/centers/national-minerals-information-center/international-minerals-statistics-and-information", +"ChAO Nikolayevtsemen","Mykolayiv",900,"UA",46.94555,32.06641,"28-08-2024","https://www.usgs.gov/centers/national-minerals-information-center/international-minerals-statistics-and-information; https://cemark.ua/en/zavodi/prat-mikolajivcement ", +"OAO KyivCement ","Kyiv",175,"UA",50.38525,30.55205,"28-08-2024","https://www.usgs.gov/centers/national-minerals-information-center/international-minerals-statistics-and-information https://www.dyckerhoff.com.ua/en/dyckerhoff-cement-ukraine ", +"OAO YuGCement ","Mykolayiv",1250,"UA",46.98512,31.9317,"28-08-2024","https://www.usgs.gov/centers/national-minerals-information-center/international-minerals-statistics-and-information, https://www.dyckerhoff.com.ua/en/yug-cemen", +"OOO Cement","Odessa",550,"UA",46.50818,30.67422,"28-08-2024","https://www.usgs.gov/centers/national-minerals-information-center/international-minerals-statistics-and-information https://se.ua/en/projects/llc-cement-odessa-cement-plant/ ", +"Overin Ltd","Amvrosievka",800,"UA",47.83453,38.46961,"28-08-2024","https://www.usgs.gov/centers/national-minerals-information-center/international-minerals-statistics-and-information","assumed equal split" +"Overin Ltd","Kamenskoye",800,"UA",48.51783,34.63382,"28-08-2024","https://www.usgs.gov/centers/national-minerals-information-center/international-minerals-statistics-and-information","assumed equal split" +"Overin Ltd","Kriviy Rih",800,"UA",47.87454,33.43586,"28-08-2024","https://www.usgs.gov/centers/national-minerals-information-center/international-minerals-statistics-and-information","assumed equal split" +"PAO Eurocement Ukraine","Balakleya, Kharkivs'ka",1000,"UA",49.49314,36.74926,"28-08-2024","https://www.usgs.gov/centers/national-minerals-information-center/international-minerals-statistics-and-information https://www.globalcement.com/news/item/13733-update-on-ukraine-february-2022 ", +"PAO Ivano-Frankovsktsement","Ivano-Frankivsk ",3600,"UA",48.97709,24.71227,"28-08-2024","https://www.usgs.gov/centers/national-minerals-information-center/international-minerals-statistics-and-information", +"PAO Kramatorkiy Tsementnyi Zavod PUSHKA ","Kramatorsk, Donets'ka",138,"UA",48.72802,37.54362,"28-08-2024","https://www.usgs.gov/centers/national-minerals-information-center/international-minerals-statistics-and-information https://www.cemnet.com/News/story/144668/pushka-ups-cement-production-56-6p-per-cent-to138-400t-in-q1.html ", +"PAO Podolsk Cement","Khmel'nyts'ka Oblast' ",2050,"UA",48.7461,26.64664,"28-08-2024","https://www.usgs.gov/centers/national-minerals-information-center/international-minerals-statistics-and-information https://cemark.ua/en/zavodi/at-podilskiy-cement ", +"PAO Volyn'Cement","Volyn'ska Oblast'",2000,"UA",50.54731,26.25774,"28-08-2024","https://www.usgs.gov/centers/national-minerals-information-center/international-minerals-statistics-and-information https://www.dyckerhoff.com.ua/volyn-cement ", diff --git a/data/refineries-noneu.csv b/data/refineries-noneu.csv new file mode 100644 index 000000000..e60127971 --- /dev/null +++ b/data/refineries-noneu.csv @@ -0,0 +1,15 @@ +"Site","Capacity [bbl/day]","Country","Latitude","Longitude","Date","Source","Comment" +"Modrica",1,"BA",44.95978,18.31294,"29-08-2024","https://modricaoil.com/Content/Read/onama?lang=en-US","the only one" +"OKTA, Skopje",1,"MK",42.0022,21.65532,"29-08-2024","https://en.wikipedia.org/wiki/OKTA","the only one" +"Ballsh",20000,"AL",40.60468,19.75231,"29-08-2024","https://www.trade.gov/country-commercial-guides/albania-oil-and-gas", +"Fier",10000,"AL",40.70095,19.54698,"29-08-2024","https://www.trade.gov/country-commercial-guides/albania-oil-and-gas", +"Elbasan",5000,"AL",41.08621,20.00762,"29-08-2024","https://www.trade.gov/country-commercial-guides/albania-oil-and-gas", +"Pančevo",96000,"RS",44.83051,20.68127,"29-08-2024","https://en.wikipedia.org/wiki/Naftna_Industrija_Srbije", +"Novi Sad",52000,"RS",45.27559,19.86733,"29-08-2024","https://en.wikipedia.org/wiki/Naftna_Industrija_Srbije", +"Halychyna Refinery (Pryvat), Drohobych",40000,"UA",49.34,23.4851,"29-08-2024","https://en.wikipedia.org/wiki/List_of_oil_refineries", +"Kherson Refinery (Alliance)",36000,"UA",46.65946,32.57479,"29-08-2024","https://en.wikipedia.org/wiki/List_of_oil_refineries", +"Kremenchuk Refinery (Ukrtatnafta)",368500,"UA",49.15299,33.4327,"29-08-2024","https://en.wikipedia.org/wiki/List_of_oil_refineries", +"LINOS Refinery, Lysychansk Oil Refinery (TNK-BP)",320000,"UA",48.84876,38.29979,"29-08-2024","https://en.wikipedia.org/wiki/List_of_oil_refineries", +"Lviv Oil Research & Refinery",18000,"UA",49.80662,24.04376,"29-08-2024","https://en.wikipedia.org/wiki/List_of_oil_refineries","capacity estimated based on number of employees" +"Naftokhimik Prykarpattya (Pryvat) Nadvirna",39000,"UA",48.62873,24.59924,"29-08-2024","https://en.wikipedia.org/wiki/List_of_oil_refineries", +"Odesa Refinery (LUKOIL)",70000,"UA",46.51189,30.68805,"29-08-2024","https://en.wikipedia.org/wiki/List_of_oil_refineries", diff --git a/doc/release_notes.rst b/doc/release_notes.rst index d5fbe7819..d7a6e3442 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -11,6 +11,31 @@ Release Notes Upcoming Release ================ +* Added data on the locations and capacities of ammonia plants in Europe. + This data is used as a spatial distribution key for the ammonia demand. + The data manually collected with sources noted in ``data/ammonia_plants.csv``. + +* Added data on the locations and capacities of cement plants in Europe that are + not included in the Hotmaps industrial database. The data sourced from the + `USGS 2019 Minerals Yearbooks + `__ + of specific countries is used as a spatial distribution key for the cement + demand. The data is stored in ``data/cement-plants-noneu.csv``. + +* Added data on the locations and capacities of refineries in Europe that are + not included in the Hotmaps industrial database. The data is mostly sourced + from the `Wikipedia list of oil refineries + `__. The data is stored + in ``data/refineries-noneu.csv``. + +* Included data from the `Global Steel Plant Tracker + `__ + provided by Global Energy Monitor. The data includes among other attributes + the locations, ages, operating status, relining dates, manufacturing process + and capacities of steel plants in Europe. This data is used as a spatial + distribution key for the steel production, which is now separated by process + type (EAF, DRI + EAF, integrated). + * Retrieve share of urban population from `World Bank API `__. The data originates from the United Nations Population Division. Previously, a file diff --git a/rules/build_sector.smk b/rules/build_sector.smk index f95731b78..23555dbc6 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -575,10 +575,14 @@ rule build_industrial_distribution_key: input: regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), clustered_pop_layout=resources("pop_layout_elec_s{simpl}_{clusters}.csv"), - hotmaps_industrial_database=storage( + hotmaps=storage( "https://gitlab.com/hotmaps/industrial_sites/industrial_sites_Industrial_Database/-/raw/master/data/Industrial_Database.csv", keep_local=True, ), + gem_gspt="data/gem/Global-Steel-Plant-Tracker-April-2024-Standard-Copy-V1.xlsx", + ammonia="data/ammonia_plants.csv", + cement_supplement="data/cement-plants-noneu.csv", + refineries_supplement="data/refineries-noneu.csv", output: industrial_distribution_key=resources( "industrial_distribution_key_elec_s{simpl}_{clusters}.csv" diff --git a/rules/retrieve.smk b/rules/retrieve.smk index 5c4af68e0..c0cd5b6c2 100644 --- a/rules/retrieve.smk +++ b/rules/retrieve.smk @@ -321,6 +321,23 @@ if config["enable"]["retrieve"]: +if config["enable"]["retrieve"]: + + rule retrieve_gem_steel_plant_tracker: + output: + "data/gem/Global-Steel-Plant-Tracker-April-2024-Standard-Copy-V1.xlsx", + run: + import requests + + response = requests.get( + "https://globalenergymonitor.org/wp-content/uploads/2024/04/Global-Steel-Plant-Tracker-April-2024-Standard-Copy-V1.xlsx", + headers={"User-Agent": "Mozilla/5.0"}, + ) + with open(output[0], "wb") as f: + f.write(response.content) + + + if config["enable"]["retrieve"]: # Some logic to find the correct file URL # Sometimes files are released delayed or ahead of schedule, check which file is currently available diff --git a/scripts/build_industrial_distribution_key.py b/scripts/build_industrial_distribution_key.py index e66547289..76a0e6d63 100644 --- a/scripts/build_industrial_distribution_key.py +++ b/scripts/build_industrial_distribution_key.py @@ -100,7 +100,7 @@ def prepare_hotmaps_database(regions): """ Load hotmaps database of industrial sites and map onto bus regions. """ - df = pd.read_csv(snakemake.input.hotmaps_industrial_database, sep=";", index_col=0) + df = pd.read_csv(snakemake.input.hotmaps, sep=";", index_col=0) df[["srid", "coordinates"]] = df.geom.str.split(";", expand=True) @@ -133,7 +133,97 @@ def prepare_hotmaps_database(regions): return gdf -def build_nodal_distribution_key(hotmaps, regions, countries): +def prepare_gem_database(regions): + """ + Load GEM database of steel plants and map onto bus regions. + """ + + df = pd.read_excel( + snakemake.input.gem_gspt, + sheet_name="Steel Plants", + na_values=["N/A", "unknown", ">0"], + ).query("Region == 'Europe'") + + df["Retired Date"] = pd.to_numeric( + df["Retired Date"].combine_first(df["Idled Date"]), errors="coerce" + ) + df["Start date"] = pd.to_numeric( + df["Start date"].str.split("-").str[0], errors="coerce" + ) + + latlon = ( + df["Coordinates"] + .str.split(", ", expand=True) + .rename(columns={0: "lat", 1: "lon"}) + ) + geometry = gpd.points_from_xy(latlon["lon"], latlon["lat"]) + gdf = gpd.GeoDataFrame(df, geometry=geometry, crs="EPSG:4326") + + gdf = gpd.sjoin(gdf, regions, how="inner", predicate="within") + + gdf.rename(columns={"name": "bus"}, inplace=True) + gdf["country"] = gdf.bus.str[:2] + + return gdf + + +def prepare_ammonia_database(regions): + """ + Load ammonia database of plants and map onto bus regions. + """ + df = pd.read_csv(snakemake.input.ammonia, index_col=0) + + geometry = gpd.points_from_xy(df.Longitude, df.Latitude) + gdf = gpd.GeoDataFrame(df, geometry=geometry, crs="EPSG:4326") + + gdf = gpd.sjoin(gdf, regions, how="inner", predicate="within") + + gdf.rename(columns={"name": "bus"}, inplace=True) + gdf["country"] = gdf.bus.str[:2] + + return gdf + + +def prepare_cement_supplement(regions): + """ + Load supplementary cement plants from non-EU-(NO-CH) and map onto bus + regions. + """ + + df = pd.read_csv(snakemake.input.cement_supplement, index_col=0) + + geometry = gpd.points_from_xy(df.Longitude, df.Latitude) + gdf = gpd.GeoDataFrame(df, geometry=geometry, crs="EPSG:4326") + + gdf = gpd.sjoin(gdf, regions, how="inner", predicate="within") + + gdf.rename(columns={"name": "bus"}, inplace=True) + gdf["country"] = gdf.bus.str[:2] + + return gdf + + +def prepare_refineries_supplement(regions): + """ + Load supplementary refineries from non-EU-(NO-CH) and map onto bus regions. + """ + + df = pd.read_csv(snakemake.input.refineries_supplement, index_col=0) + + geometry = gpd.points_from_xy(df.Longitude, df.Latitude) + gdf = gpd.GeoDataFrame(df, geometry=geometry, crs="EPSG:4326") + + gdf = gpd.sjoin(gdf, regions, how="inner", predicate="within") + + gdf.rename(columns={"name": "bus"}, inplace=True) + gdf["country"] = gdf.bus.str[:2] + + return gdf + + +def build_nodal_distribution_key( + hotmaps, gem, ammonia, cement, refineries, regions, countries +): """ Build nodal distribution keys for each sector. """ @@ -158,15 +248,137 @@ def build_nodal_distribution_key(hotmaps, regions, countries): if emissions.sum() == 0: key = pd.Series(1 / len(facilities), facilities.index) else: - # BEWARE: this is a strong assumption - emissions = emissions.fillna(emissions.mean()) + # assume 20% quantile for missing values + emissions = emissions.fillna(emissions.quantile(0.2)) key = emissions / emissions.sum() key = key.groupby(facilities.bus).sum().reindex(regions_ct, fill_value=0.0) + elif sector == "Cement" and country in cement.country.unique(): + facilities = cement.query("country == @country") + production = facilities["Cement [kt/a]"] + if production.sum() == 0: + key = pd.Series(1 / len(facilities), facilities.index) + else: + key = production / production.sum() + key = key.groupby(facilities.bus).sum().reindex(regions_ct, fill_value=0.0) + elif sector == "Refineries" and country in refineries.country.unique(): + facilities = refineries.query("country == @country") + production = facilities["Capacity [bbl/day]"] + if production.sum() == 0: + key = pd.Series(1 / len(facilities), facilities.index) + else: + key = production / production.sum() + key = key.groupby(facilities.bus).sum().reindex(regions_ct, fill_value=0.0) else: key = keys.loc[regions_ct, "population"] keys.loc[regions_ct, sector] = key + # add specific steel subsectors + steel_processes = ["EAF", "DRI + EAF", "Integrated steelworks"] + for process, country in product(steel_processes, countries): + regions_ct = regions.index[regions.index.str.contains(country)] + + facilities = gem.query("country == @country") + + if process == "EAF": + status_list = [ + "construction", + "operating", + "operating pre-retirement", + "retired", + ] + capacities = facilities.loc[ + facilities["Capacity operating status"].isin(status_list) + & ( + facilities["Retired Date"].isna() + | facilities["Retired Date"].gt(2025) + ), + "Nominal EAF steel capacity (ttpa)", + ].dropna() + elif process == "DRI + EAF": + status_list = [ + "construction", + "operating", + "operating pre-retirement", + "retired", + "announced", + ] + sel = [ + "Nominal BOF steel capacity (ttpa)", + "Nominal OHF steel capacity (ttpa)", + "Nominal iron capacity (ttpa)", + ] + status_filter = facilities["Capacity operating status"].isin(status_list) + retirement_filter = facilities["Retired Date"].isna() | facilities[ + "Retired Date" + ].gt(2030) + start_filter = ( + facilities["Start date"].isna() + & ~facilities["Capacity operating status"].eq("announced") + ) | facilities["Start date"].le(2030) + capacities = ( + facilities.loc[status_filter & retirement_filter & start_filter, sel] + .sum(axis=1) + .dropna() + ) + elif process == "Integrated steelworks": + status_list = [ + "construction", + "operating", + "operating pre-retirement", + "retired", + ] + sel = [ + "Nominal BOF steel capacity (ttpa)", + "Nominal OHF steel capacity (ttpa)", + ] + capacities = ( + facilities.loc[ + facilities["Capacity operating status"].isin(status_list) + & ( + facilities["Retired Date"].isna() + | facilities["Retired Date"].gt(2025) + ), + sel, + ] + .sum(axis=1) + .dropna() + ) + else: + raise ValueError(f"Unknown process {process}") + + if not capacities.empty: + if capacities.sum() == 0: + key = pd.Series(1 / len(capacities), capacities.index) + else: + key = capacities / capacities.sum() + buses = facilities.loc[capacities.index, "bus"] + key = key.groupby(buses).sum().reindex(regions_ct, fill_value=0.0) + else: + key = keys.loc[regions_ct, "population"] + + keys.loc[regions_ct, process] = key + + # add ammonia + for country in countries: + regions_ct = regions.index[regions.index.str.contains(country)] + + facilities = ammonia.query("country == @country") + + if not facilities.empty: + production = facilities["Ammonia [kt/a]"] + if production.sum() == 0: + key = pd.Series(1 / len(facilities), facilities.index) + else: + # assume 50% of the minimum production for missing values + production = production.fillna(0.5 * facilities["Ammonia [kt/a]"].min()) + key = production / production.sum() + key = key.groupby(facilities.bus).sum().reindex(regions_ct, fill_value=0.0) + else: + key = 0.0 + + keys.loc[regions_ct, "Ammonia"] = key + return keys @@ -188,6 +400,16 @@ def build_nodal_distribution_key(hotmaps, regions, countries): hotmaps = prepare_hotmaps_database(regions) - keys = build_nodal_distribution_key(hotmaps, regions, countries) + gem = prepare_gem_database(regions) + + ammonia = prepare_ammonia_database(regions) + + cement = prepare_cement_supplement(regions) + + refineries = prepare_refineries_supplement(regions) + + keys = build_nodal_distribution_key( + hotmaps, gem, ammonia, cement, refineries, regions, countries + ) keys.to_csv(snakemake.output.industrial_distribution_key) diff --git a/scripts/build_industrial_energy_demand_per_node_today.py b/scripts/build_industrial_energy_demand_per_node_today.py index aa9e9dffa..fd7c281c8 100644 --- a/scripts/build_industrial_energy_demand_per_node_today.py +++ b/scripts/build_industrial_energy_demand_per_node_today.py @@ -33,10 +33,10 @@ # map JRC/our sectors to hotmaps sector, where mapping exist sector_mapping = { - "Electric arc": "Iron and steel", - "Integrated steelworks": "Iron and steel", - "DRI + Electric arc": "Iron and steel", - "Ammonia": "Chemical industry", + "Electric arc": "EAF", + "Integrated steelworks": "Integrated steelworks", + "DRI + Electric arc": "DRI + EAF", + "Ammonia": "Ammonia", "Basic chemicals (without ammonia)": "Chemical industry", "Other chemicals": "Chemical industry", "Pharmaceutical products etc.": "Chemical industry", diff --git a/scripts/build_industrial_production_per_node.py b/scripts/build_industrial_production_per_node.py index d3edfa45b..862345e57 100644 --- a/scripts/build_industrial_production_per_node.py +++ b/scripts/build_industrial_production_per_node.py @@ -32,10 +32,10 @@ # map JRC/our sectors to hotmaps sector, where mapping exist sector_mapping = { - "Electric arc": "Iron and steel", - "Integrated steelworks": "Iron and steel", - "DRI + Electric arc": "Iron and steel", - "Ammonia": "Chemical industry", + "Electric arc": "EAF", + "Integrated steelworks": "Integrated steelworks", + "DRI + Electric arc": "DRI + EAF", + "Ammonia": "Ammonia", "HVC": "Chemical industry", "HVC (mechanical recycling)": "Chemical industry", "HVC (chemical recycling)": "Chemical industry", From 4f8d583d38227b3581a61398e6a53f4bdc329371 Mon Sep 17 00:00:00 2001 From: Philipp Glaum <95913147+p-glaum@users.noreply.github.com> Date: Fri, 30 Aug 2024 15:34:06 +0200 Subject: [PATCH 245/344] prepare_sector_network: fix municipal waste transport links (#1250) --- scripts/prepare_sector_network.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 87aa90fa8..0e9665389 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2595,13 +2595,15 @@ def add_biomass(n, costs): if options["municipal_solid_waste"]: n.madd( "Link", - biomass_transport.index, - bus0=biomass_transport.bus0 + " municipal solid waste", - bus1=biomass_transport.bus1 + " municipal solid waste", + biomass_transport.index + " municipal solid waste", + bus0=biomass_transport.bus0.values + " municipal solid waste", + bus1=biomass_transport.bus1.values + " municipal solid waste", p_nom_extendable=False, p_nom=5e4, length=biomass_transport.length.values, - marginal_cost=biomass_transport.costs * biomass_transport.length.values, + marginal_cost=( + biomass_transport.costs * biomass_transport.length + ).values, carrier="municipal solid waste transport", ) From 88d28de3a18f598f973426f2384b52481b004c34 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 30 Aug 2024 15:36:03 +0200 Subject: [PATCH 246/344] resolve Kosovo (XK) as separate country (#1249) * resolve Kosovo (XK) as separate country * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fillna * add missing key in data/existing_infrastructure/existing_heating_raw.csv --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- config/config.default.yaml | 4 ++-- config/examples/config.entsoe-all.yaml | 4 ++-- data/district_heat_share.csv | 2 +- data/era5-annual-HDD-per-country.csv | 1 + data/era5-annual-runoff-per-country.csv | 1 + .../existing_heating_raw.csv | 1 + doc/configtables/countries.csv | 2 +- doc/release_notes.rst | 2 ++ scripts/_helpers.py | 3 ++- scripts/build_biomass_potentials.py | 14 +++++------- scripts/build_electricity_demand.py | 8 +++---- scripts/build_energy_totals.py | 6 ++--- scripts/build_hydro_profile.py | 10 +++++---- ...build_industrial_production_per_country.py | 2 ++ scripts/build_population_layouts.py | 5 +++-- scripts/build_retro_cost.py | 2 ++ scripts/build_shapes.py | 22 ++++++++++--------- scripts/prepare_sector_network.py | 2 +- 18 files changed, 52 insertions(+), 39 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 22a5ac2bf..dfd57a72f 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -42,7 +42,7 @@ scenario: ll: - vopt clusters: - - 38 + - 39 - 128 - 256 opts: @@ -56,7 +56,7 @@ scenario: - 2050 # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#countries -countries: ['AL', 'AT', 'BA', 'BE', 'BG', 'CH', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI', 'FR', 'GB', 'GR', 'HR', 'HU', 'IE', 'IT', 'LT', 'LU', 'LV', 'ME', 'MK', 'NL', 'NO', 'PL', 'PT', 'RO', 'RS', 'SE', 'SI', 'SK'] +countries: ['AL', 'AT', 'BA', 'BE', 'BG', 'CH', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI', 'FR', 'GB', 'GR', 'HR', 'HU', 'IE', 'IT', 'LT', 'LU', 'LV', 'ME', 'MK', 'NL', 'NO', 'PL', 'PT', 'RO', 'RS', 'SE', 'SI', 'SK', 'XK'] # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#snapshots snapshots: diff --git a/config/examples/config.entsoe-all.yaml b/config/examples/config.entsoe-all.yaml index 4e7edd036..9f52094d6 100644 --- a/config/examples/config.entsoe-all.yaml +++ b/config/examples/config.entsoe-all.yaml @@ -15,7 +15,7 @@ scenario: ll: - vopt clusters: - - 39 + - 41 - 128 - 256 opts: @@ -26,7 +26,7 @@ scenario: - '' # TODO add Turkey (TR) -countries: ['AL', 'AT', 'BA', 'BE', 'BG', 'CH', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI', 'FR', 'GB', 'GR', 'HR', 'HU', 'IE', 'IT', 'LT', 'LU', 'LV', 'ME', 'MD', 'MK', 'NL', 'NO', 'PL', 'PT', 'RO', 'RS', 'SE', 'SI', 'SK', 'UA'] +countries: ['AL', 'AT', 'BA', 'BE', 'BG', 'CH', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI', 'FR', 'GB', 'GR', 'HR', 'HU', 'IE', 'IT', 'LT', 'LU', 'LV', 'ME', 'MD', 'MK', 'NL', 'NO', 'PL', 'PT', 'RO', 'RS', 'SE', 'SI', 'SK', 'UA', 'XK'] electricity: custom_powerplants: true diff --git a/data/district_heat_share.csv b/data/district_heat_share.csv index 07d4f51dd..ad47fb3b5 100644 --- a/data/district_heat_share.csv +++ b/data/district_heat_share.csv @@ -25,7 +25,7 @@ SE,50.4, GB,2, BY,70, EE,52,5406 -KO,3,207 +XK,3,207 RO,23,9962 SK,54,15000 NL,4,9800 diff --git a/data/era5-annual-HDD-per-country.csv b/data/era5-annual-HDD-per-country.csv index 5f6ad45db..4bb02806c 100644 --- a/data/era5-annual-HDD-per-country.csv +++ b/data/era5-annual-HDD-per-country.csv @@ -29,6 +29,7 @@ PL,1615,1584,1275,1356,1364,1378,1517,1290,1230,1296,1249,1438,1294,1478,1446,15 PT,114,114,102,113,86,110,95,79,94,96,107,103,99,109,80,131,106,93,93,97,78,106,104,115,103,98,103,97,112,110,115,116,106,109,109,109,99,139,107,102,89,90,95,104,99,108,85,89,75,87,104,96,104,85,66,88,62,82,96,90,90,78,87,97,103,90,91,91,84,96,80,101,100,81,79,87,79,96,81,72,78,98,75 RO,931,939,801,813,874,851,839,815,799,780,736,811,883,948,822,968,810,789,826,731,744,826,890,867,873,706,791,776,858,792,787,798,838,782,752,855,772,836,780,880,801,811,763,807,917,815,882,827,710,700,846,801,848,701,806,869,842,805,740,710,760,721,844,754,799,780,684,695,695,757,793,781,709,662,681,721,720,690,624,648,740,973,611 RS,292,305,246,274,285,276,265,263,261,239,222,252,273,300,249,313,248,249,254,231,226,270,283,271,274,233,253,243,267,254,251,246,272,242,243,265,233,265,241,273,251,255,244,253,275,256,268,254,231,220,275,243,263,223,250,270,259,250,237,217,234,217,262,238,265,247,217,214,222,229,251,244,214,192,222,223,232,211,197,212,234,306,192 +XK,292,305,246,274,285,276,265,263,261,239,222,252,273,300,249,313,248,249,254,231,226,270,283,271,274,233,253,243,267,254,251,246,272,242,243,265,233,265,241,273,251,255,244,253,275,256,268,254,231,220,275,243,263,223,250,270,259,250,237,217,234,217,262,238,265,247,217,214,222,229,251,244,214,192,222,223,232,211,197,212,234,306,192 SK,221,219,199,207,209,201,212,202,189,189,178,210,195,217,208,228,192,193,185,185,178,208,213,211,219,179,187,192,203,200,192,188,195,178,178,197,188,204,193,215,191,190,180,193,212,198,207,187,172,173,202,186,191,171,186,204,193,184,177,162,184,174,190,184,191,182,163,161,170,188,175,179,174,142,161,170,176,157,152,158,180,231,150 SI,81,77,66,74,72,71,72,69,61,65,61,72,67,73,69,78,65,66,62,61,59,75,75,73,73,62,65,67,71,69,67,68,71,62,61,69,62,71,68,74,67,65,65,69,73,71,71,63,59,58,70,61,63,55,61,71,63,62,62,54,59,55,65,63,67,61,54,57,57,65,58,59,59,46,55,57,58,53,52,53,61,77,51 SE,4509,4537,3713,3939,4134,4059,4374,3918,3633,4015,3891,4219,3560,3919,4426,4488,3950,4223,3662,3988,3814,4451,4260,4021,4358,4613,3929,4280,4255,4254,4043,3806,3975,3634,3625,4238,4132,4314,4246,4287,4301,3913,3840,3819,4588,4139,4376,3931,3476,3446,3785,3695,3893,3991,3916,4073,3757,3950,3781,3446,3898,3778,3755,3769,3632,3561,3606,3590,3806,4397,3474,3935,3675,3452,3421,3635,3693,3705,3689,3247,3807,5084,3769 diff --git a/data/era5-annual-runoff-per-country.csv b/data/era5-annual-runoff-per-country.csv index 862ff743a..f160f0434 100644 --- a/data/era5-annual-runoff-per-country.csv +++ b/data/era5-annual-runoff-per-country.csv @@ -29,6 +29,7 @@ PL,19040.141287526196,13600.003649707409,4232.940272903036,6263.550300160282,117 PT,17389.910347609384,7956.13238827081,8339.728858321132,2075.556471824434,3081.9400195534536,5428.965709165979,13519.093138173732,8266.183051247071,2305.611210847845,4751.414112923152,8372.129135066036,5969.31083523099,2397.6586063764958,3916.9962424030286,9654.699566273128,9603.363148222781,4163.162870337118,9179.465624121007,12131.854156488425,23406.646477036124,10635.077955993413,11029.404714084018,16475.917880350513,12087.55769685171,9804.492049893286,26178.184180984652,5669.820254013018,7559.528320921835,15405.930172416956,11163.387795923936,7088.025310785357,10191.330496756716,7128.069257230898,9335.631563569985,5236.13128329125,4572.251664471752,32605.577182903042,149323.86523586328,34294.81803755196,6061.11721308257,5836.654497517657,4843.7020037722705,7814.782411245904,9395.759000250582,14057.157961109715,8192.288977642056,7655.760207241933,9203.645337709624,7637.7450966202805,7971.812432879156,7421.37678917759,2935.2871895485287,6329.939163930326,8820.796560748415,8944.109217212255,14719.27363438586,11050.779819799696,8379.75073967695,5590.525909815882,10485.131539563748,20476.028147427794,7715.162648805314,11939.81827695821,5116.2596877306005,3047.0903835389504,9273.489011971804,5135.313604526994,3331.418010762715,6338.048693933388,12105.616460835923,7097.468638389797,3348.213951094401,11773.920694867731,13465.586888738457,4379.602415677836,11394.463621230614,3104.8789319036796,7216.150544702967,6653.765864361187,5720.580737490315,6685.493238874142,8486.079863634486,8329.192253601708 RO,46545.50823036229,35517.718755954746,12007.97895282227,19448.903832688356,20440.973051620913,15132.394486734484,21523.767710605425,32075.68337603852,20312.088233477134,14653.654680967496,19346.344374357534,19295.50358149612,27702.617554877004,24644.67023787785,52797.882559506776,42262.7162454671,34689.01143849651,42583.74114324521,28973.147767939397,36882.64868179707,29063.471368254675,39575.626278602576,31898.858414072816,36283.98261870408,42207.50410750205,41714.957670180156,43270.313090823794,40474.54716426176,53565.91349208634,74865.02611452111,48190.11154819992,57257.26135564704,46523.80416861758,44300.83791323246,54901.1701096736,47361.27151969814,49156.09266449866,56461.30009557872,56062.45724028393,58506.511677323855,48499.732173446595,45901.64074770707,33171.12855766741,38310.49272614435,39377.41586450999,28638.803870927226,29424.553978129992,39429.14347226504,39300.73151290347,27525.04968129815,47095.77682827757,30948.90232192865,32939.14262154816,30155.99148400138,35067.04977301419,35592.25066656211,45790.67404045973,42596.77718301701,39223.35618808953,25767.93476452563,30963.38609617001,30034.908201595936,23323.05819720473,28517.276888551194,41560.85308154171,43999.15897330077,21427.19650317167,25084.131072037453,18450.764512377853,38182.39266803458,20422.517191285966,14090.243196787245,21998.55223596247,30809.187151501046,22071.90474198888,31906.454754357772,20763.073093214778,25620.747974742615,19248.368008063957,23289.011422937554,26669.846131314298,27207.74766143675,29386.08741874402 RS,12898.821957973714,11760.375115749226,5762.823681912337,10868.645457667955,6851.647476795101,6489.091511565737,8016.427124526303,11096.059611708006,8784.092785314235,6844.044739629848,5968.673897869134,7450.873145908507,8457.218084424714,10466.134979346216,18700.932768467817,18783.264457747777,9647.157790116991,14650.396388121844,12360.14810912279,11640.673528541236,8882.446980807292,14762.619600006037,18399.930168952076,13646.806400326906,14489.119148714197,14104.804002759012,15462.34970923992,10894.228431882033,15095.088711819322,17853.281396922546,12916.46431873845,14207.379133135786,14418.834370720482,12752.5957180177,15544.588217112372,15820.904121022999,13534.055257847373,15215.146639151637,12996.626380040323,13029.919161637963,10196.668003968522,10284.440766030819,7022.693160251435,8401.249663323453,8435.803252725058,10265.915943082338,8623.546341166757,7782.429841191159,9309.259412625106,4849.32865233776,9480.40891035497,8237.330612702019,6688.9341497267815,7113.796951355988,8945.392918392941,11391.406447517316,8770.78174795334,7797.074404297926,11205.567652241964,7490.638744688466,6890.626375097134,8721.152535625733,6986.899718851523,10014.515393386338,11863.685949406314,12576.12226693591,6930.903954576986,6933.68818306879,8436.351253425808,11680.836931112251,5673.257881932308,4512.930178739657,6094.8735580904795,11616.485592765608,8423.704814584986,9582.260510301834,6280.960312130538,10196.683253538626,5869.428152205924,6637.126459584064,8223.324863109141,9163.49600405833,11744.973584261521 +XK,12898.821957973714,11760.375115749226,5762.823681912337,10868.645457667955,6851.647476795101,6489.091511565737,8016.427124526303,11096.059611708006,8784.092785314235,6844.044739629848,5968.673897869134,7450.873145908507,8457.218084424714,10466.134979346216,18700.932768467817,18783.264457747777,9647.157790116991,14650.396388121844,12360.14810912279,11640.673528541236,8882.446980807292,14762.619600006037,18399.930168952076,13646.806400326906,14489.119148714197,14104.804002759012,15462.34970923992,10894.228431882033,15095.088711819322,17853.281396922546,12916.46431873845,14207.379133135786,14418.834370720482,12752.5957180177,15544.588217112372,15820.904121022999,13534.055257847373,15215.146639151637,12996.626380040323,13029.919161637963,10196.668003968522,10284.440766030819,7022.693160251435,8401.249663323453,8435.803252725058,10265.915943082338,8623.546341166757,7782.429841191159,9309.259412625106,4849.32865233776,9480.40891035497,8237.330612702019,6688.9341497267815,7113.796951355988,8945.392918392941,11391.406447517316,8770.78174795334,7797.074404297926,11205.567652241964,7490.638744688466,6890.626375097134,8721.152535625733,6986.899718851523,10014.515393386338,11863.685949406314,12576.12226693591,6930.903954576986,6933.68818306879,8436.351253425808,11680.836931112251,5673.257881932308,4512.930178739657,6094.8735580904795,11616.485592765608,8423.704814584986,9582.260510301834,6280.960312130538,10196.683253538626,5869.428152205924,6637.126459584064,8223.324863109141,9163.49600405833,11744.973584261521 SK,10526.703178382486,6581.841291454129,2779.6437301689157,6043.293021386589,8068.711762338472,4716.572547513843,4263.788112757618,7436.446538154413,5733.7942336519345,5091.94458281141,6885.47211985414,6956.591212284293,7116.928783228905,4573.269245439668,8169.130903254093,6799.092645889461,7190.199390340817,8819.311158887818,6679.080394471699,8681.76846617681,6714.688996801623,8780.135433744465,9674.052829336924,7510.89080672348,12315.672504913706,11551.288938768861,10658.095118642217,7760.467099195778,7334.701885838525,11743.053621084162,9101.455526104724,11252.254293590808,7089.2297957961755,11692.575497480175,11870.620362608648,8320.411050776946,12664.612697391287,10840.053026355938,9544.145158945727,8285.851845920657,6554.955003499832,6014.385824757616,5119.229272436076,5566.197178944401,8283.751441606055,4576.739002618681,5529.727054286627,4884.285979430572,7030.767074231632,6380.185010419688,6092.06334940043,4789.6919845964085,3988.728835990378,6100.445127127096,6174.336930810738,5839.492530664778,6739.897695678873,6073.547495791424,6239.663814901599,5985.626769812626,5946.910319392802,5350.4706690989715,2897.742723724252,3812.447551889599,5110.734824948982,5466.7571969447445,3822.1224333717755,4189.745228198809,3608.9397299109723,8925.718578619328,5520.297117401385,2338.023634704523,3634.014208205775,5079.021357670912,3789.3939110245783,3599.858750509022,4222.707040620564,3096.1701009538024,2158.8092391216674,4542.203594136759,4220.210061470592,2961.7796555712353,3705.0055429796116 SI,8932.252857660058,6769.672897729603,3971.443731895907,6727.0196008878875,5996.060296331706,4552.96378417224,7787.375795346372,9548.071392837826,4890.204133819321,7094.028640574649,11752.798116380785,9378.602450871665,7169.30972105447,8221.267552376507,10568.142173580174,7705.119699907013,8559.440940655051,9496.2682906945,10641.418485889739,13644.09405840664,8756.865265119286,10877.994032024846,13095.473005855994,10408.222250010325,17110.767148034232,13257.44359305617,10163.420442559087,10743.809603872312,11213.570708968457,12419.028940460184,8518.188189494229,14975.496301661744,10217.449212514946,10521.007791805137,10759.209828867228,9699.803414833696,14530.647692616923,14530.715084460895,12439.143395173185,11990.046930204517,9001.647305603035,10111.423546501868,6824.172499114327,10267.968760960974,10233.709492652128,9562.175707725224,11231.941186997657,8178.692540046128,8568.526412713521,8103.883097000008,9639.895318751553,9157.43571368136,6814.50158997294,8089.913064615838,8170.645053083449,10765.044719853075,6713.170108103339,8936.8362631332,9542.147586182518,8964.110093884974,8653.114721452737,8353.75428643612,4558.589088593759,9432.405903987477,6927.92152111369,7408.760392367348,5834.871486466167,9336.912851251047,8839.494837985663,10766.693939731143,5073.937522450624,6475.598951785893,9482.419717110955,13414.771212079502,5906.5584945059145,9942.151265372298,7696.092255607837,9521.835825354157,8476.124990692872,6361.010018809483,7504.908031281684,6349.444363482254,12494.86342976942 SE,97729.33612275522,104893.13349064675,152164.30605691054,147567.81837152236,172633.569885874,142454.06079467706,109264.12507808255,145966.80578917114,158733.59492028368,161670.6171803483,134822.12256461562,136958.6213484757,175679.6273876132,133962.36617697845,126787.36233921513,130861.49290945902,151133.2982760823,140247.52694114187,130508.52271890236,137421.63579450184,164132.0529341604,142467.57286064525,135886.87305352974,163762.3962091317,161128.38161511483,148196.28633931337,167892.87677861055,132017.696130452,105953.99289036485,119407.22923207263,147139.6039546916,151309.55114963037,164893.61323375857,137608.18237925606,149284.78771947857,108749.15747050248,131859.531763871,117290.48017785426,131751.97960784068,111117.27409251402,110551.615114603,81550.1135393761,114677.940027603,102356.46697333359,128977.80283219383,94483.98267503879,114937.96457142044,93256.91327642264,96751.29026217168,91074.34139788701,87931.96567208324,96560.83822983531,116746.94389793588,64626.25905820224,75156.91767736348,51489.37512699482,74729.26978432719,134673.01513808029,110440.65771030058,149033.9827088587,143576.83069821307,93040.02354987273,64075.160200006496,102493.08906248632,106664.82884715112,90351.43771324471,94801.81222609026,92445.93458012305,88919.20770998915,108111.72872590217,127440.80298482065,124444.06618883039,98961.91573013023,87913.90602563386,115956.48217115598,95526.84265190935,103459.08985036932,86058.90377748368,99040.9103276256,110493.22773914765,94182.7249029169,169752.66741900297,99896.09236620825 diff --git a/data/existing_infrastructure/existing_heating_raw.csv b/data/existing_infrastructure/existing_heating_raw.csv index 18774460c..5e2581c93 100644 --- a/data/existing_infrastructure/existing_heating_raw.csv +++ b/data/existing_infrastructure/existing_heating_raw.csv @@ -28,4 +28,5 @@ United Kingdom,160.49,1.26,7.39,13.81,0.81,0.21 Norway,,,,,2.91,0.334 Switzerland,,,,,1,0.849 Serbia,,,,,, +Kosovo,,,,,, Bosnia Herzegovina,,,,,, diff --git a/doc/configtables/countries.csv b/doc/configtables/countries.csv index 6a386416c..41ddd8e91 100644 --- a/doc/configtables/countries.csv +++ b/doc/configtables/countries.csv @@ -1,2 +1,2 @@ ,Unit,Values,Description -countries,--,"Subset of {'AL', 'AT', 'BA', 'BE', 'BG', 'CH', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI', 'FR', 'GB', 'GR', 'HR', 'HU', 'IE', 'IT', 'LT', 'LU', 'LV', 'ME', 'MK', 'NL', 'NO', 'PL', 'PT', 'RO', 'RS', 'SE', 'SI', 'SK'}","European countries defined by their `Two-letter country codes (ISO 3166-1) `_ which should be included in the energy system model." +countries,--,"Subset of {'AL', 'AT', 'BA', 'BE', 'BG', 'CH', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI', 'FR', 'GB', 'GR', 'HR', 'HU', 'IE', 'IT', 'LT', 'LU', 'LV', 'ME', 'MK', 'NL', 'NO', 'PL', 'PT', 'RO', 'RS', 'SE', 'SI', 'SK', 'XK'}","European countries defined by their `Two-letter country codes (ISO 3166-1) `_ which should be included in the energy system model." diff --git a/doc/release_notes.rst b/doc/release_notes.rst index d7a6e3442..8c4603572 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -11,6 +11,8 @@ Release Notes Upcoming Release ================ +* Represent Kosovo (XK) as separate country. + * Added data on the locations and capacities of ammonia plants in Europe. This data is used as a spatial distribution key for the ammonia demand. The data manually collected with sources noted in ``data/ammonia_plants.csv``. diff --git a/scripts/_helpers.py b/scripts/_helpers.py index a3b77c1c0..f79d92583 100644 --- a/scripts/_helpers.py +++ b/scripts/_helpers.py @@ -507,7 +507,8 @@ def generate_periodic_profiles(dt_index, nodes, weekly_profile, localize=None): week_df = pd.DataFrame(index=dt_index, columns=nodes) for node in nodes: - timezone = pytz.timezone(pytz.country_timezones[node[:2]][0]) + ct = node[:2] if node[:2] != "XK" else "RS" + timezone = pytz.timezone(pytz.country_timezones[ct][0]) tz_dt_index = dt_index.tz_convert(timezone) week_df[node] = [24 * dt.weekday() + dt.hour for dt in tz_dt_index] week_df[node] = week_df[node].map(weekly_profile) diff --git a/scripts/build_biomass_potentials.py b/scripts/build_biomass_potentials.py index b69fcfbe9..4c7752e4b 100644 --- a/scripts/build_biomass_potentials.py +++ b/scripts/build_biomass_potentials.py @@ -88,9 +88,11 @@ def build_nuts_population_data(year=2013): pop = pd.concat([pop, pd.concat(swiss)]).to_frame("total") # add missing manually - pop["AL"] = 2893 - pop["BA"] = 3871 - pop["RS"] = 7210 + pop["AL"] = 2778 + pop["BA"] = 3234 + pop["RS"] = 6664 + pop["ME"] = 617 + pop["XK"] = 1587 pop["ct"] = pop.index.str[:2] @@ -149,10 +151,6 @@ def enspreso_biomass_potentials(year=2020, scenario="ENS_Low"): bio = dff.groupby(["NUTS2", "commodity"]).potential.sum().unstack() - # currently Serbia and Kosovo not split, so aggregate - bio.loc["RS"] += bio.loc["XK"] - bio.drop("XK", inplace=True) - return bio @@ -199,7 +197,7 @@ def build_nuts2_shapes(): ) countries = gpd.read_file(snakemake.input.country_shapes).set_index("name") - missing_iso2 = countries.index.intersection(["AL", "RS", "BA"]) + missing_iso2 = countries.index.intersection(["AL", "RS", "XK", "BA"]) missing = countries.loc[missing_iso2] nuts2.rename(index={"ME00": "ME", "MK00": "MK"}, inplace=True) diff --git a/scripts/build_electricity_demand.py b/scripts/build_electricity_demand.py index 622fdafa2..d3334ec85 100755 --- a/scripts/build_electricity_demand.py +++ b/scripts/build_electricity_demand.py @@ -192,9 +192,9 @@ def manual_adjustment(load, fn_load, countries): if "ME" in load: load["BA"] = load.HR * (11.0 / 16.2) - if ("KV" not in load or load.KV.isnull().values.all()) and "KV" in countries: + if "XK" not in load and "XK" in countries: if "RS" in load: - load["KV"] = load["RS"] * (4.8 / 27.0) + load["XK"] = load["RS"] * (4.8 / 27.0) copy_timeslice(load, "GR", "2015-08-11 21:00", "2015-08-15 20:00", Delta(weeks=1)) copy_timeslice(load, "AT", "2018-12-31 22:00", "2019-01-01 22:00", Delta(days=2)) @@ -311,8 +311,8 @@ def manual_adjustment(load, fn_load, countries): logger.info("Supplement missing data with synthetic data.") fn = snakemake.input.synthetic synthetic_load = pd.read_csv(fn, index_col=0, parse_dates=True) - # "UA" does not appear in synthetic load data - countries = list(set(countries) - set(["UA", "MD"])) + # UA, MD, XK do not appear in synthetic load data + countries = list(set(countries) - set(["UA", "MD", "XK"])) synthetic_load = synthetic_load.loc[snapshots, countries] load = load.combine_first(synthetic_load) diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index 24ba86bbd..2802d8b3b 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -1102,7 +1102,7 @@ def build_co2_totals( co2 = eea_co2.reindex(countries) for ct in pd.Index(countries).intersection( - ["BA", "RS", "AL", "ME", "MK", "UA", "MD"] + ["BA", "RS", "XK", "AL", "ME", "MK", "UA", "MD"] ): mappings = { "electricity": (ct, "+", "Electricity & heat generation", np.nan), @@ -1455,10 +1455,10 @@ def update_residential_from_eurostat(energy: pd.DataFrame) -> pd.DataFrame: for nrg_name, (code, siec) in nrg_type.items(): # Select energy balance type, rename columns and countries to match IDEES data, - # convert TJ to TWh, and drop XK data already since included in RS data + # convert TJ to TWh col_to_rename = {"geo": "country", "TIME_PERIOD": "year", "OBS_VALUE": nrg_name} idx_to_rename = {v: k for k, v in idees_rename.items()} - drop_geo = ["EU27_2020", "EA20", "XK"] + drop_geo = ["EU27_2020", "EA20"] nrg_data = eurostat_households.query( "nrg_bal == @code and siec == @siec and geo not in @drop_geo and OBS_VALUE > 0" ).copy() diff --git a/scripts/build_hydro_profile.py b/scripts/build_hydro_profile.py index 6a0315c73..2d0d2e521 100644 --- a/scripts/build_hydro_profile.py +++ b/scripts/build_hydro_profile.py @@ -82,7 +82,7 @@ def get_eia_annual_hydro_generation(fn, countries, capacities=False): countries=["Czechia", "Slovakia"], start=1980, end=1992 ), "Former Serbia and Montenegro": dict( - countries=["Serbia", "Montenegro"], start=1992, end=2005 + countries=["Serbia", "Montenegro", "Kosovo"], start=1992, end=2005 ), "Former Yugoslavia": dict( countries=[ @@ -90,6 +90,7 @@ def get_eia_annual_hydro_generation(fn, countries, capacities=False): "Croatia", "Bosnia and Herzegovina", "Serbia", + "Kosovo", "Montenegro", "North Macedonia", ], @@ -111,9 +112,8 @@ def get_eia_annual_hydro_generation(fn, countries, capacities=False): ) df.loc["Germany"] = df.filter(like="Germany", axis=0).sum() - df.loc["Serbia"] += df.loc["Kosovo"].fillna(0.0) df = df.loc[~df.index.str.contains("Former")] - df.drop(["Europe", "Germany, West", "Germany, East", "Kosovo"], inplace=True) + df.drop(["Europe", "Germany, West", "Germany, East"], inplace=True) df.index = cc.convert(df.index, to="iso2") df.index.name = "countries" @@ -122,6 +122,8 @@ def get_eia_annual_hydro_generation(fn, countries, capacities=False): factor = 1e3 if capacities else 1e6 df = df.T[countries] * factor + df.ffill(axis=0, inplace=True) + return df @@ -129,7 +131,7 @@ def correct_eia_stats_by_capacity(eia_stats, fn, countries, baseyear=2019): cap = get_eia_annual_hydro_generation(fn, countries, capacities=True) ratio = cap / cap.loc[baseyear] eia_stats_corrected = eia_stats / ratio - to_keep = ["AL", "AT", "CH", "DE", "GB", "NL", "RS", "RO", "SK"] + to_keep = ["AL", "AT", "CH", "DE", "GB", "NL", "RS", "XK", "RO", "SK"] to_correct = eia_stats_corrected.columns.difference(to_keep) eia_stats.loc[:, to_correct] = eia_stats_corrected.loc[:, to_correct] diff --git a/scripts/build_industrial_production_per_country.py b/scripts/build_industrial_production_per_country.py index ccb4feca6..b7f98d760 100644 --- a/scripts/build_industrial_production_per_country.py +++ b/scripts/build_industrial_production_per_country.py @@ -345,5 +345,7 @@ def separate_basic_chemicals(demand, year): separate_basic_chemicals(demand, year) + demand.fillna(0.0, inplace=True) + fn = snakemake.output.industrial_production_per_country demand.to_csv(fn, float_format="%.2f") diff --git a/scripts/build_population_layouts.py b/scripts/build_population_layouts.py index 4cabdc7cd..b9af8d850 100644 --- a/scripts/build_population_layouts.py +++ b/scripts/build_population_layouts.py @@ -20,8 +20,6 @@ cc = coco.CountryConverter() -coco.logging.getLogger().setLevel(coco.logging.CRITICAL) - if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake @@ -32,6 +30,7 @@ configure_logging(snakemake) set_scenario_config(snakemake) + coco.logging.getLogger().setLevel(coco.logging.CRITICAL) cutout = atlite.Cutout(snakemake.input.cutout) @@ -56,6 +55,8 @@ urban_fraction = ( urban_fraction.query("iso2 in @countries").set_index("iso2")["2019"].div(100) ) + if "XK" in countries: + urban_fraction["XK"] = urban_fraction["RS"] # population in each grid cell pop_cells = pd.Series(I.dot(nuts3["pop"])) diff --git a/scripts/build_retro_cost.py b/scripts/build_retro_cost.py index c6dd31cf9..42144c1aa 100755 --- a/scripts/build_retro_cost.py +++ b/scripts/build_retro_cost.py @@ -198,6 +198,7 @@ def prepare_building_stock_data(): "Iceland": "IS", "Montenegro": "ME", "Serbia": "RS", + "Kosovo": "XK", "Albania": "AL", "United Kingdom": "GB", "Bosnia and Herzegovina": "BA", @@ -1073,6 +1074,7 @@ def sample_dE_costs_area( "AL": ["BG", "RO", "GR"], "BA": ["HR"], "RS": ["BG", "RO", "HR", "HU"], + "KV": ["RS"], "MK": ["BG", "GR"], "ME": ["BA", "AL", "RS", "HR"], "CH": ["SE", "DE"], diff --git a/scripts/build_shapes.py b/scripts/build_shapes.py index 4de5370a7..2370f2aef 100644 --- a/scripts/build_shapes.py +++ b/scripts/build_shapes.py @@ -106,8 +106,6 @@ def _simplify_polys(polys, minarea=0.1, tolerance=None, filterremote=True): def countries(naturalearth, country_list): - if "RS" in country_list: - country_list.append("KV") df = gpd.read_file(naturalearth) @@ -116,15 +114,12 @@ def countries(naturalearth, country_list): df[x].where(lambda s: s != "-99") for x in ("ISO_A2", "WB_A2", "ADM0_A3") ) df["name"] = reduce(lambda x, y: x.fillna(y), fieldnames, next(fieldnames)).str[:2] + df.replace({"name": {"KV": "XK"}}, inplace=True) df = df.loc[ df.name.isin(country_list) & ((df["scalerank"] == 0) | (df["scalerank"] == 5)) ] s = df.set_index("name")["geometry"].map(_simplify_polys).set_crs(df.crs) - if "RS" in country_list: - s["RS"] = s["RS"].union(s.pop("KV")) - # cleanup shape union - s["RS"] = Polygon(s["RS"].exterior.coords) return s @@ -195,7 +190,9 @@ def nuts3(country_shapes, nuts3, nuts3pop, nuts3gdp, ch_cantons, ch_popgdp): df = df.join(pd.DataFrame(dict(pop=pop, gdp=gdp))) - df["country"] = df.index.to_series().str[:2].replace(dict(UK="GB", EL="GR")) + df["country"] = ( + df.index.to_series().str[:2].replace(dict(UK="GB", EL="GR", KV="XK")) + ) excludenuts = pd.Index( ( @@ -217,13 +214,18 @@ def nuts3(country_shapes, nuts3, nuts3pop, nuts3gdp, ch_cantons, ch_popgdp): "FR9", ) ) - excludecountry = pd.Index(("MT", "TR", "LI", "IS", "CY", "KV")) + excludecountry = pd.Index(("MT", "TR", "LI", "IS", "CY")) df = df.loc[df.index.difference(excludenuts)] df = df.loc[~df.country.isin(excludecountry)] manual = gpd.GeoDataFrame( - [["BA1", "BA", 3871.0], ["RS1", "RS", 7210.0], ["AL1", "AL", 2893.0]], + [ + ["BA1", "BA", 3234.0], + ["RS1", "RS", 6664.0], + ["AL1", "AL", 2778.0], + ["XK1", "XK", 1587.0], + ], columns=["NUTS_ID", "country", "pop"], geometry=gpd.GeoSeries(), crs=df.crs, @@ -234,7 +236,7 @@ def nuts3(country_shapes, nuts3, nuts3pop, nuts3gdp, ch_cantons, ch_popgdp): df = pd.concat([df, manual], sort=False) - df.loc["ME000", "pop"] = 650.0 + df.loc["ME000", "pop"] = 617.0 return df diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 0e9665389..3e3e117c3 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -281,7 +281,7 @@ def co2_emissions_year( eurostat = build_eurostat(input_eurostat, countries) - # this only affects the estimation of CO2 emissions for BA, RS, AL, ME, MK + # this only affects the estimation of CO2 emissions for BA, RS, AL, ME, MK, XK eurostat_co2 = build_eurostat_co2(eurostat, year) co2_totals = build_co2_totals(countries, eea_co2, eurostat_co2) From d2f8162d7b8be83766fe41c88aea2ed50a7042c7 Mon Sep 17 00:00:00 2001 From: Philipp Glaum <95913147+p-glaum@users.noreply.github.com> Date: Fri, 30 Aug 2024 15:56:21 +0200 Subject: [PATCH 247/344] prepare_sector_network: correct if statement (#1252) --- scripts/prepare_sector_network.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 3e3e117c3..9d099e073 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2324,8 +2324,7 @@ def add_biomass(n, costs): if ( options["municipal_solid_waste"] and not options["industry"] - and cf_industry["waste_to_energy"] - or cf_industry["waste_to_energy_cc"] + and (cf_industry["waste_to_energy"] or cf_industry["waste_to_energy_cc"]) ): logger.warning( "Flag municipal_solid_waste can be only used with industry " From bf2d82a384f77575f7ebef943cf72edf5122b0b7 Mon Sep 17 00:00:00 2001 From: Amos Schledorn <60692940+amos-schledorn@users.noreply.github.com> Date: Fri, 30 Aug 2024 16:01:46 +0200 Subject: [PATCH 248/344] Dynamic central heating temperatures (#1206) * feat: Add rule to build central heating temperature profiles, adjust build_cop_profiles accordingly * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * style: make new config settings prettier * docs: add file headers * docs: update configtables and module docstring * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * docs: update release notes * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * chore: use smooth ambient temperature through rolling window; remove default vals in class * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update doc/configtables/sector.csv --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Amos Schledorn Co-authored-by: Fabian Neumann --- config/config.default.yaml | 33 +-- doc/configtables/sector.csv | 9 +- doc/release_notes.rst | 2 + rules/build_sector.smk | 73 +++++- ...entral_heating_temperature_approximator.py | 218 ++++++++++++++++++ .../run.py | 147 ++++++++++++ scripts/build_cop_profiles/run.py | 62 +---- 7 files changed, 468 insertions(+), 76 deletions(-) create mode 100644 scripts/build_central_heating_temperature_profiles/central_heating_temperature_approximator.py create mode 100644 scripts/build_central_heating_temperature_profiles/run.py diff --git a/config/config.default.yaml b/config/config.default.yaml index dfd57a72f..2026c11fa 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -449,19 +449,26 @@ sector: 2045: 0.8 2050: 1.0 district_heating_loss: 0.15 - # check these numbers! - forward_temperature: - default: 90 - DK: 70 - SE: 70 - NO: 70 - FI: 70 - return_temperature: - default: 50 - DK: 40 - SE: 40 - NO: 40 - FI: 40 + supply_temperature_approximation: + max_forward_temperature: + default: 90 + DK: 70 + SE: 70 + NO: 70 + min_forward_temperature: + default: 68 + DK: 54 + SE: 54 + NO: 54 + return_temperature: + default: 50 + DK: 40 + SE: 40 + NO: 40 + FI: 40 + lower_threshold_ambient_temperature: 0 + upper_threshold_ambient_temperature: 10 + rolling_window_ambient_temperature: 72 heat_source_cooling: 6 #K heat_pump_cop_approximation: refrigerant: ammonia diff --git a/doc/configtables/sector.csv b/doc/configtables/sector.csv index 88442a9ee..044c8dc40 100644 --- a/doc/configtables/sector.csv +++ b/doc/configtables/sector.csv @@ -9,8 +9,13 @@ district_heating,--,,`prepare_sector_network.py `__. The reference year is changed from 2015 to 2019. +* Made central heating supply temperatures dynamic based on an adaptation of a reference curve from Pieper et al. (2019) (https://www.sciencedirect.com/science/article/pii/S0360544219305857?via%3Dihub). + * Added option to use country-specific district heating forward and return temperatures. Defaults to lower temperatures in Scandinavia. * Added unsustainable biomass potentials for solid, gaseous, and liquid biomass. The potentials can be phased-out and/or diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 23555dbc6..9f94dbbd6 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -212,17 +212,72 @@ rule build_temperature_profiles: "../scripts/build_temperature_profiles.py" +rule build_central_heating_temperature_profiles: + params: + max_forward_temperature_central_heating=config_provider( + "sector", + "district_heating", + "supply_temperature_approximation", + "max_forward_temperature", + ), + min_forward_temperature_central_heating=config_provider( + "sector", + "district_heating", + "supply_temperature_approximation", + "min_forward_temperature", + ), + return_temperature_central_heating=config_provider( + "sector", + "district_heating", + "supply_temperature_approximation", + "return_temperature", + ), + snapshots=config_provider("snapshots"), + lower_threshold_ambient_temperature=config_provider( + "sector", + "district_heating", + "supply_temperature_approximation", + "lower_threshold_ambient_temperature", + ), + upper_threshold_ambient_temperature=config_provider( + "sector", + "district_heating", + "supply_temperature_approximation", + "upper_threshold_ambient_temperature", + ), + rolling_window_ambient_temperature=config_provider( + "sector", + "district_heating", + "supply_temperature_approximation", + "rolling_window_ambient_temperature", + ), + input: + temp_air_total=resources("temp_air_total_elec_s{simpl}_{clusters}.nc"), + regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), + output: + central_heating_forward_temperature_profiles=resources( + "central_heating_forward_temperature_profiles_elec_s{simpl}_{clusters}.nc" + ), + central_heating_return_temperature_profiles=resources( + "central_heating_return_temperature_profiles_elec_s{simpl}_{clusters}.nc" + ), + resources: + mem_mb=20000, + log: + logs("build_central_heating_temperature_profiles_s{simpl}_{clusters}.log"), + benchmark: + benchmarks("build_central_heating_temperature_profiles/s{simpl}_{clusters}") + conda: + "../envs/environment.yaml" + script: + "../scripts/build_central_heating_temperature_profiles/run.py" + + rule build_cop_profiles: params: heat_pump_sink_T_decentral_heating=config_provider( "sector", "heat_pump_sink_T_individual_heating" ), - forward_temperature_central_heating=config_provider( - "sector", "district_heating", "forward_temperature" - ), - return_temperature_central_heating=config_provider( - "sector", "district_heating", "return_temperature" - ), heat_source_cooling_central_heating=config_provider( "sector", "district_heating", "heat_source_cooling" ), @@ -232,6 +287,12 @@ rule build_cop_profiles: heat_pump_sources=config_provider("sector", "heat_pump_sources"), snapshots=config_provider("snapshots"), input: + central_heating_forward_temperature_profiles=resources( + "central_heating_forward_temperature_profiles_elec_s{simpl}_{clusters}.nc" + ), + central_heating_return_temperature_profiles=resources( + "central_heating_return_temperature_profiles_elec_s{simpl}_{clusters}.nc" + ), temp_soil_total=resources("temp_soil_total_elec_s{simpl}_{clusters}.nc"), temp_air_total=resources("temp_air_total_elec_s{simpl}_{clusters}.nc"), regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), diff --git a/scripts/build_central_heating_temperature_profiles/central_heating_temperature_approximator.py b/scripts/build_central_heating_temperature_profiles/central_heating_temperature_approximator.py new file mode 100644 index 000000000..5b467824d --- /dev/null +++ b/scripts/build_central_heating_temperature_profiles/central_heating_temperature_approximator.py @@ -0,0 +1,218 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2020-2024 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT + +import pandas as pd +import xarray as xr + + +class CentralHeatingTemperatureApproximator: + """ + A class to approximate central heating temperatures based on ambient + temperature. + + Attributes + ---------- + ambient_temperature : xr.DataArray + The ambient temperature data. + max_forward_temperature : xr.DataArray + The maximum forward temperature. + min_forward_temperature : xr.DataArray + The minimum forward temperature. + fixed_return_temperature : xr.DataArray + The fixed return temperature. + lower_threshold_ambient_temperature : float + Forward temperature is `max_forward_temperature` for ambient temperatures lower-or-equal this threshold. + upper_threshold_ambient_temperature : float + Forward temperature is `min_forward_temperature` for ambient temperatures higher-or-equal this threshold. + """ + + def __init__( + self, + ambient_temperature: xr.DataArray, + max_forward_temperature: float, + min_forward_temperature: float, + fixed_return_temperature: float, + lower_threshold_ambient_temperature: float, + upper_threshold_ambient_temperature: float, + rolling_window_ambient_temperature: int, + ) -> None: + """ + Initialize the CentralHeatingTemperatureApproximator. + + Parameters + ---------- + ambient_temperature : xr.DataArray + The ambient temperature data. + max_forward_temperature : xr.DataArray + The maximum forward temperature. + min_forward_temperature : xr.DataArray + The minimum forward temperature. + fixed_return_temperature : xr.DataArray + The fixed return temperature. + lower_threshold_ambient_temperature : float + Forward temperature is `max_forward_temperature` for ambient temperatures lower-or-equal this threshold. + upper_threshold_ambient_temperature : float + Forward temperature is `min_forward_temperature` for ambient temperatures higher-or-equal this threshold. + rolling_window_ambient_temperature : int + Rolling window size for averaging ambient temperature. + """ + self._ambient_temperature = ambient_temperature + self.max_forward_temperature = max_forward_temperature + self.min_forward_temperature = min_forward_temperature + self.fixed_return_temperature = fixed_return_temperature + self.lower_threshold_ambient_temperature = lower_threshold_ambient_temperature + self.upper_threshold_ambient_temperature = upper_threshold_ambient_temperature + self.rolling_window_ambient_temperature = rolling_window_ambient_temperature + + def ambient_temperature_rolling_mean(self) -> xr.DataArray: + """ + Property to get ambient temperature. + + Returns + ------- + xr.DataArray + Rolling mean of ambient temperature input. + """ + # bfill to avoid NAs in the beginning + return ( + self._ambient_temperature.rolling( + time=self.rolling_window_ambient_temperature + ) + .mean(skip_na=True) + .bfill(dim="time") + ) + + @property + def forward_temperature(self) -> xr.DataArray: + """ + Property to get dynamic forward temperature. + + Returns + ------- + xr.DataArray + Dynamic forward temperatures + """ + return self._approximate_forward_temperature() + + @property + def return_temperature(self) -> float: + """ + Property to get return temperature. + + Returns + ------- + float + Return temperature. + """ + return self._approximate_return_temperature() + + def _approximate_forward_temperature(self) -> xr.DataArray: + """ + Approximate dynamic forward temperature based on reference curve. Adapted from [Pieper et al. (2019)](https://doi.org/10.1016/j.energy.2019.03.165). + + Returns + ------- + xr.DataArray + Dynamic forward temperatures. + """ + + forward_temperature = xr.where( + self.ambient_temperature_rolling_mean() + <= self.lower_threshold_ambient_temperature, + self.max_forward_temperature, + xr.where( + self.ambient_temperature_rolling_mean() + >= self.upper_threshold_ambient_temperature, + self.min_forward_temperature, + self.min_forward_temperature + + (self.max_forward_temperature - self.min_forward_temperature) + * ( + self.upper_threshold_ambient_temperature + - self.ambient_temperature_rolling_mean() + ) + / ( + self.upper_threshold_ambient_temperature + - self.lower_threshold_ambient_temperature + ), + ), + ) + return forward_temperature + + def _approximate_return_temperature(self) -> float: + """ + Approximate return temperature. + + Returns + ------- + float + Return temperature. + """ + return self.fixed_return_temperature + + @property + def forward_temperature(self) -> xr.DataArray: + """ + Property to get dynamic forward temperature. + + Returns + ------- + xr.DataArray + Dynamic forward temperatures. + """ + return self._approximate_forward_temperature() + + @property + def return_temperature(self) -> float: + """ + Property to get return temperature. + + Returns + ------- + float + Return temperature. + """ + return self._approximate_return_temperature() + + def _approximate_forward_temperature(self) -> xr.DataArray: + """ + Approximate dynamic forward temperature. + + Returns + ------- + xr.DataArray + Dynamic forward temperatures. + """ + forward_temperature = xr.where( + self.ambient_temperature_rolling_mean() + <= self.lower_threshold_ambient_temperature, + self.max_forward_temperature, + xr.where( + self.ambient_temperature_rolling_mean() + >= self.upper_threshold_ambient_temperature, + self.min_forward_temperature, + self.min_forward_temperature + + (self.max_forward_temperature - self.min_forward_temperature) + * ( + self.upper_threshold_ambient_temperature + - self.ambient_temperature_rolling_mean() + ) + / ( + self.upper_threshold_ambient_temperature + - self.lower_threshold_ambient_temperature + ), + ), + ) + return forward_temperature + + def _approximate_return_temperature(self) -> float: + """ + Approximate return temperature. + + Returns + ------- + float + Return temperature. + """ + return self.fixed_return_temperature diff --git a/scripts/build_central_heating_temperature_profiles/run.py b/scripts/build_central_heating_temperature_profiles/run.py new file mode 100644 index 000000000..115293e4c --- /dev/null +++ b/scripts/build_central_heating_temperature_profiles/run.py @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2020-2024 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT +""" +Approximate district heating forward and return temperature profiles based on +ambient temperature. The method is based on a reference curve from Pieper et +al. 2019, where for ambient temperatures below 0C, the highest possible forward +temperature is assumed and vice versa for temperatures above 10C. Between these +threshold levels, forward temperatures are linearly interpolated. + +By default, temperature levels are increased for non-Scandinavian countries. +The default ratios between min. and max. forward temperatures is based on AGFW-Hauptbericht 2022. + +Relevant Settings +----------------- + +.. code:: yaml + sector: + district_heating: + max_forward_temperature: + min_forward_temperature: + return_temperature: +Inputs +------ +- `resources//temp_air_total`: Air temperature + +Outputs +------- +- `resources//central_heating_temperature_profiles.nc`: + +References +---------- +- Pieper, et al. (2019): "Assessment of a combination of three heat sources for heat pumps to supply district heating" (https://doi.org/10.1016/j.energy.2019.03.165). +- AGFW (2022): "Hauptbericht 2022" (https://www.agfw.de/zahlen-und-statistiken/agfw-hauptbericht) +""" + +import sys + +import geopandas as gpd +import numpy as np +import pandas as pd +import xarray as xr +from _helpers import set_scenario_config +from central_heating_temperature_approximator import ( + CentralHeatingTemperatureApproximator, +) + + +def get_country_from_node_name(node_name: str) -> str: + return node_name[:2] + + +def map_temperature_dict_to_onshore_regions( + supply_temperature_by_country: dict, + regions_onshore: pd.Index, + snapshots: pd.DatetimeIndex, +) -> xr.DataArray: + """ + Map dictionary of temperatures to onshore regions. + + Parameters: + ---------- + supply_temperature_by_country : dictionary + Dictionary with temperatures as values and country keys as keys. One key must be named "default" + regions_onshore : pd.Index + Names of onshore regions + snapshots : pd.DatetimeIndex + Time stamps + + Returns: + ------- + xr.DataArray + The dictionary values mapped to onshore regions with onshore regions as coordinates. + """ + return xr.DataArray( + [ + [ + ( + supply_temperature_by_country[get_country_from_node_name(node_name)] + if get_country_from_node_name(node_name) + in supply_temperature_by_country.keys() + else supply_temperature_by_country["default"] + ) + for node_name in regions_onshore.values + ] + # pass both nodes and snapshots as dimensions to preserve correct data structure + for _ in snapshots + ], + dims=["time", "name"], + coords={"time": snapshots, "name": regions_onshore}, + ) + + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake( + "build_cop_profiles", + simpl="", + clusters=48, + ) + + set_scenario_config(snakemake) + + # map forward and return temperatures specified on country-level to onshore regions + regions_onshore = gpd.read_file(snakemake.input.regions_onshore)["name"] + snapshots = pd.date_range(freq="h", **snakemake.params.snapshots) + max_forward_temperature_central_heating_by_node_and_time: xr.DataArray = ( + map_temperature_dict_to_onshore_regions( + supply_temperature_by_country=snakemake.params.max_forward_temperature_central_heating, + regions_onshore=regions_onshore, + snapshots=snapshots, + ) + ) + min_forward_temperature_central_heating_by_node_and_time: xr.DataArray = ( + map_temperature_dict_to_onshore_regions( + supply_temperature_by_country=snakemake.params.min_forward_temperature_central_heating, + regions_onshore=regions_onshore, + snapshots=snapshots, + ) + ) + return_temperature_central_heating_by_node_and_time: xr.DataArray = ( + map_temperature_dict_to_onshore_regions( + supply_temperature_by_country=snakemake.params.return_temperature_central_heating, + regions_onshore=regions_onshore, + snapshots=snapshots, + ) + ) + + central_heating_temperature_approximator = CentralHeatingTemperatureApproximator( + ambient_temperature=xr.open_dataarray(snakemake.input.temp_air_total), + max_forward_temperature=max_forward_temperature_central_heating_by_node_and_time, + min_forward_temperature=min_forward_temperature_central_heating_by_node_and_time, + fixed_return_temperature=return_temperature_central_heating_by_node_and_time, + lower_threshold_ambient_temperature=snakemake.params.lower_threshold_ambient_temperature, + upper_threshold_ambient_temperature=snakemake.params.upper_threshold_ambient_temperature, + rolling_window_ambient_temperature=snakemake.params.rolling_window_ambient_temperature, + ) + + central_heating_temperature_approximator.forward_temperature.to_netcdf( + snakemake.output.central_heating_forward_temperature_profiles + ) + central_heating_temperature_approximator.return_temperature.to_netcdf( + snakemake.output.central_heating_return_temperature_profiles + ) diff --git a/scripts/build_cop_profiles/run.py b/scripts/build_cop_profiles/run.py index b4ec3e43f..d1faf1b12 100644 --- a/scripts/build_cop_profiles/run.py +++ b/scripts/build_cop_profiles/run.py @@ -53,47 +53,6 @@ from scripts.definitions.heat_system_type import HeatSystemType -def map_temperature_dict_to_onshore_regions( - supply_temperature_by_country: dict, - regions_onshore: pd.Index, - snapshots: pd.DatetimeIndex, -) -> xr.DataArray: - """ - Map dictionary of temperatures to onshore regions. - - Parameters: - ---------- - supply_temperature_by_country : dictionary - Dictionary with temperatures as values and country keys as keys. One key must be named "default" - regions_onshore : pd.Index - Names of onshore regions - snapshots : pd.DatetimeIndex - Time stamps - - Returns: - ------- - xr.DataArray - The dictionary values mapped to onshore regions with onshore regions as coordinates. - """ - return xr.DataArray( - [ - [ - ( - supply_temperature_by_country[get_country_from_node_name(node_name)] - if get_country_from_node_name(node_name) - in supply_temperature_by_country.keys() - else supply_temperature_by_country["default"] - ) - for node_name in regions_onshore.values - ] - # pass both nodes and snapshots as dimensions to preserve correct data structure - for _ in snapshots - ], - dims=["time", "name"], - coords={"time": snapshots, "name": regions_onshore}, - ) - - def get_cop( heat_system_type: str, heat_source: str, @@ -154,20 +113,13 @@ def get_country_from_node_name(node_name: str) -> str: # map forward and return temperatures specified on country-level to onshore regions regions_onshore = gpd.read_file(snakemake.input.regions_onshore)["name"] snapshots = pd.date_range(freq="h", **snakemake.params.snapshots) - forward_temperature_central_heating_by_node_and_time: xr.DataArray = ( - map_temperature_dict_to_onshore_regions( - supply_temperature_by_country=snakemake.params.forward_temperature_central_heating, - regions_onshore=regions_onshore, - snapshots=snapshots, - ) + central_heating_forward_temperature: xr.DataArray = xr.open_dataarray( + snakemake.input.central_heating_forward_temperature_profiles ) - return_temperature_central_heating_by_node_and_time: xr.DataArray = ( - map_temperature_dict_to_onshore_regions( - supply_temperature_by_country=snakemake.params.return_temperature_central_heating, - regions_onshore=regions_onshore, - snapshots=snapshots, - ) + central_heating_return_temperature: xr.DataArray = xr.open_dataarray( + snakemake.input.central_heating_return_temperature_profiles ) + cop_all_system_types = [] for heat_system_type, heat_sources in snakemake.params.heat_pump_sources.items(): cop_this_system_type = [] @@ -179,8 +131,8 @@ def get_country_from_node_name(node_name: str) -> str: heat_system_type=heat_system_type, heat_source=heat_source, source_inlet_temperature_celsius=source_inlet_temperature_celsius, - forward_temperature_by_node_and_time=forward_temperature_central_heating_by_node_and_time, - return_temperature_by_node_and_time=return_temperature_central_heating_by_node_and_time, + forward_temperature_by_node_and_time=central_heating_forward_temperature, + return_temperature_by_node_and_time=central_heating_return_temperature, ) cop_this_system_type.append(cop_da) cop_all_system_types.append( From 0df54bc37002f5f434ff6566e98571be3d767696 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 30 Aug 2024 16:15:34 +0200 Subject: [PATCH 249/344] Squashed commit of the following: commit a2b82195b872bae11a9e247c53756ca3ae512362 Author: Fabian Neumann Date: Fri Aug 30 16:13:36 2024 +0200 minor adjustments commit 4f9eb04fec4534abbef10fedd4d9b5c34f064670 Merge: 7b525a51 bf2d82a3 Author: Fabian Neumann Date: Fri Aug 30 16:08:58 2024 +0200 Merge branch 'master' into master commit 7b525a515a36a4ebb248280546583ac843f6b277 Merge: d9033374 a357ba11 Author: danielelerede-oet Date: Mon Aug 26 13:21:06 2024 +0200 Merge branch 'master' into master commit d9033374e4bf0bb70df0828743b9aab69c0cbb27 Author: danielelerede-oet Date: Mon Aug 26 10:34:27 2024 +0200 Update scripts/solve_network.py Co-authored-by: Fabian Neumann commit 98e6c5b0f152ca7e064b3c80be26f50040628388 Author: danielelerede-oet Date: Mon Aug 26 10:34:21 2024 +0200 Update scripts/solve_network.py Co-authored-by: Fabian Neumann commit be19a2ba2401a2815c09e74d2add24461b3fe2aa Author: danielelerede-oet Date: Mon Aug 26 10:34:15 2024 +0200 Update scripts/solve_operations_network.py Co-authored-by: Fabian Neumann commit 8093eaa399cfea62d1ef93ec4ed5ed46bf5dcc6d Author: danielelerede-oet Date: Mon Aug 26 10:34:08 2024 +0200 Update scripts/solve_network.py Co-authored-by: Fabian Neumann commit 746d1761accbe44d3a851430b94844fe3ab43d72 Author: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed Aug 21 15:29:12 2024 +0000 [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci commit f46fdee7f7ceeb9040ee73123fa4a3bd1aa14a56 Author: Daniele Lerede Date: Wed Aug 21 17:11:32 2024 +0200 fix solve_operations_network --- doc/release_notes.rst | 2 ++ rules/solve_electricity.smk | 7 +++++++ scripts/solve_network.py | 8 +++++--- scripts/solve_operations_network.py | 8 +++++++- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 786eb602a..e912da2d8 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -11,6 +11,8 @@ Release Notes Upcoming Release ================ +* Bugfix for passing function arguments in rule :mod:`solve_operations_network`. + * Represent Kosovo (XK) as separate country. * Added data on the locations and capacities of ammonia plants in Europe. diff --git a/rules/solve_electricity.smk b/rules/solve_electricity.smk index 389687a0f..e2dbe42ef 100644 --- a/rules/solve_electricity.smk +++ b/rules/solve_electricity.smk @@ -41,6 +41,13 @@ rule solve_network: rule solve_operations_network: params: options=config_provider("solving", "options"), + solving=config_provider("solving"), + foresight=config_provider("foresight"), + planning_horizons=config_provider("scenario", "planning_horizons"), + co2_sequestration_potential=config_provider( + "sector", "co2_sequestration_potential", default=200 + ), + custom_extra_functionality=input_custom_extra_functionality, input: network=RESULTS + "networks/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc", output: diff --git a/scripts/solve_network.py b/scripts/solve_network.py index 99f7b5751..c70d4d3c8 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -1055,8 +1055,8 @@ def extra_functionality(n, snapshots): if config["sector"]["enhanced_geothermal"]["enable"]: add_flexible_egs_constraint(n) - if snakemake.params.custom_extra_functionality: - source_path = snakemake.params.custom_extra_functionality + if n.params.custom_extra_functionality: + source_path = n.params.custom_extra_functionality assert os.path.exists(source_path), f"{source_path} does not exist" sys.path.append(os.path.dirname(source_path)) module_name = os.path.splitext(os.path.basename(source_path))[0] @@ -1065,7 +1065,7 @@ def extra_functionality(n, snapshots): custom_extra_functionality(n, snapshots, snakemake) -def solve_network(n, config, solving, **kwargs): +def solve_network(n, config, params, solving, **kwargs): set_of_options = solving["solver"]["options"] cf_solving = solving["options"] @@ -1093,6 +1093,7 @@ def solve_network(n, config, solving, **kwargs): # add to network for extra_functionality n.config = config + n.params = params if rolling_horizon and snakemake.rule == "solve_operations_network": kwargs["horizon"] = cf_solving.get("horizon", 365) @@ -1165,6 +1166,7 @@ def solve_network(n, config, solving, **kwargs): n = solve_network( n, config=snakemake.config, + params=snakemake.params, solving=snakemake.params.solving, log_fn=snakemake.log.solver, ) diff --git a/scripts/solve_operations_network.py b/scripts/solve_operations_network.py index bd4ce7aaa..4336c3a7e 100644 --- a/scripts/solve_operations_network.py +++ b/scripts/solve_operations_network.py @@ -49,7 +49,13 @@ n.optimize.fix_optimal_capacities() n = prepare_network(n, solve_opts, config=snakemake.config) - n = solve_network(n, config=snakemake.config, log_fn=snakemake.log.solver) + n = solve_network( + n, + config=snakemake.config, + params=snakemake.params, + solving=snakemake.params.solving, + log_fn=snakemake.log.solver, + ) n.meta = dict(snakemake.config, **dict(wildcards=dict(snakemake.wildcards))) n.export_to_netcdf(snakemake.output[0]) From 8d78fe83be4e044865505cdb0f7dd7b1c1a57b08 Mon Sep 17 00:00:00 2001 From: Lukas Trippe Date: Fri, 30 Aug 2024 16:42:35 +0200 Subject: [PATCH 250/344] ci: code scanning with CodeQL (#1251) * ci: code scanning with CodeQL * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 92 ++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..fd64e164b --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,92 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: ["master"] + pull_request: + branches: ["master"] + schedule: + - cron: '23 18 * * 5' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: python + build-mode: none + # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" From cae63a3578de797406370c5d6eb73e85f0f75b1b Mon Sep 17 00:00:00 2001 From: Philipp Glaum Date: Fri, 30 Aug 2024 16:47:46 +0200 Subject: [PATCH 251/344] prepare_sector_network: get tech data for allam cycle from data base --- config/config.default.yaml | 2 +- scripts/prepare_sector_network.py | 20 +++++++++----------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 42b069d6b..4060729d4 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -815,7 +815,7 @@ industry: # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#costs costs: year: 2030 - version: v0.9.1 + version: v0.9.2 social_discountrate: 0.02 fill_values: FOM: 0 diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 5f3936457..2bb63e893 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -786,11 +786,11 @@ def add_allam(n, costs): carrier="allam", p_nom_extendable=True, # TODO: add costs to technology-data - capital_cost=0.6 * 1.5e6 * 0.1, # efficiency * EUR/MW * annuity - marginal_cost=2, - efficiency=0.6, + capital_cost=costs.at["allam", "fixed"] * costs.at["allam", "efficiency"], + marginal_cost=costs.at["allam", "VOM"] * costs.at["allam", "efficiency"], + efficiency=costs.at["allam", "efficiency"], efficiency2=costs.at["gas", "CO2 intensity"], - lifetime=30.0, + lifetime=costs.at["allam", "lifetime"], ) @@ -862,11 +862,9 @@ def add_methanol_to_power(n, costs, types={}): bus3="co2 atmosphere", carrier="allam methanol", p_nom_extendable=True, - capital_cost=0.59 - * 1.832e6 - * calculate_annuity(25, 0.07), # efficiency * EUR/MW * annuity - marginal_cost=2, - efficiency=0.59, + capital_cost=costs.at["allam", "fixed"] * costs.at["allam", "efficiency"], + marginal_cost=costs.at["allam", "VOM"] * costs.at["allam", "efficiency"], + efficiency=costs.at["allam", "efficiency"], efficiency2=0.98 * costs.at["methanolisation", "carbondioxide-input"], efficiency3=0.02 * costs.at["methanolisation", "carbondioxide-input"], lifetime=25, @@ -921,7 +919,7 @@ def add_methanol_to_power(n, costs, types={}): carrier="CCGT methanol CC", p_nom_extendable=True, capital_cost=capital_cost_cc, - marginal_cost=2, + marginal_cost=costs.at["CCGT", "VOM"] * costs.at["CCGT", "VOM"], efficiency=costs.at["CCGT", "efficiency"], efficiency2=costs.at["cement capture", "capture_rate"] * costs.at["methanolisation", "carbondioxide-input"], @@ -943,7 +941,7 @@ def add_methanol_to_power(n, costs, types={}): carrier="OCGT methanol", p_nom_extendable=True, capital_cost=costs.at["OCGT", "fixed"] * costs.at["OCGT", "efficiency"], - marginal_cost=2, + marginal_cost=costs.at["OCGT", "VOM"] * costs.at["OCGT", "efficiency"], efficiency=costs.at["OCGT", "efficiency"], efficiency2=costs.at["methanolisation", "carbondioxide-input"], lifetime=costs.at["OCGT", "lifetime"], From 51c5e39e2d51dafd25c51072e09d4fa47ce6c8c1 Mon Sep 17 00:00:00 2001 From: Philipp Glaum Date: Fri, 30 Aug 2024 16:54:55 +0200 Subject: [PATCH 252/344] cleanup --- scripts/prepare_sector_network.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 2bb63e893..e1c6fd7c6 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3356,26 +3356,6 @@ def add_industry(n, costs): ) # methanol for industry - # add methanol nodes if not already added - if "methanol" not in n.buses.carrier.unique(): - n.madd( - "Bus", - spatial.methanol.nodes, - carrier="methanol", - location=spatial.methanol.locations, - unit="MWh_LHV", - ) - - n.madd( - "Store", - spatial.methanol.nodes, - suffix=" Store", - bus=spatial.methanol.nodes, - e_nom_extendable=True, - e_cyclic=True, - carrier="methanol", - capital_cost=0.02, - ) n.madd( "Bus", From 3a1ee934da1d1d70b9dcb53bacbb69cf4d1f9e9b Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 30 Aug 2024 18:06:26 +0200 Subject: [PATCH 253/344] prepare release v0.12.0 --- config/config.default.yaml | 8 +- doc/configtables/atlite.csv | 2 +- doc/configtables/solar.csv | 2 +- doc/img/intro-workflow.png | Bin 228916 -> 124775 bytes doc/preparation.rst | 21 ++ doc/release_notes.rst | 268 +++++++++++++++----- doc/retrieve.rst | 34 ++- doc/sector.rst | 10 + doc/tutorial.rst | 150 ++++++----- doc/tutorial_sector.rst | 481 ++++++++++++++++++------------------ envs/environment.fixed.yaml | 473 +---------------------------------- scripts/build_cutout.py | 4 +- 12 files changed, 606 insertions(+), 847 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 2026c11fa..7ed7eb333 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -155,7 +155,7 @@ renewable: resource: method: wind turbine: Vestas_V112_3MW - smooth: true + smooth: false add_cutout_windspeed: true capacity_per_sqkm: 3 # correction_factor: 0.93 @@ -175,7 +175,7 @@ renewable: resource: method: wind turbine: NREL_ReferenceTurbine_2020ATB_5.5MW - smooth: true + smooth: false add_cutout_windspeed: true capacity_per_sqkm: 2 correction_factor: 0.8855 @@ -192,7 +192,7 @@ renewable: resource: method: wind turbine: NREL_ReferenceTurbine_2020ATB_5.5MW - smooth: true + smooth: false add_cutout_windspeed: true capacity_per_sqkm: 2 correction_factor: 0.8855 @@ -209,7 +209,7 @@ renewable: resource: method: wind turbine: NREL_ReferenceTurbine_5MW_offshore - smooth: true + smooth: false add_cutout_windspeed: true # ScholzPhd Tab 4.3.1: 10MW/km^2 capacity_per_sqkm: 2 diff --git a/doc/configtables/atlite.csv b/doc/configtables/atlite.csv index 5e0d2bd00..10965eedd 100644 --- a/doc/configtables/atlite.csv +++ b/doc/configtables/atlite.csv @@ -4,7 +4,7 @@ nprocesses,--,int,"Number of parallel processes in cutout preparation" show_progress,bool,true/false,"Whether progressbar for atlite conversion processes should be shown. False saves time." cutouts,,, -- {name},--,"Convention is to name cutouts like ``--`` (e.g. ``europe-2013-sarah3-era5``).","Name of the cutout netcdf file. The user may specify multiple cutouts under configuration ``atlite: cutouts:``. Reference is used in configuration ``renewable: {technology}: cutout:``. The cutout ``base`` may be used to automatically calculate temporal and spatial bounds of the network." --- -- module,--,"Subset of {'era5','sarah'}","Source of the reanalysis weather dataset (e.g. `ERA5 `_ or `SARAH-2 `_)" +-- -- module,--,"Subset of {'era5','sarah'}","Source of the reanalysis weather dataset (e.g. `ERA5 `_ or `SARAH-3 `_)" -- -- x,°,"Float interval within [-180, 180]","Range of longitudes to download weather data for. If not defined, it defaults to the spatial bounds of all bus shapes." -- -- y,°,"Float interval within [-90, 90]","Range of latitudes to download weather data for. If not defined, it defaults to the spatial bounds of all bus shapes." -- -- dx,°,"Larger than 0.25","Grid resolution for longitude" diff --git a/doc/configtables/solar.csv b/doc/configtables/solar.csv index 21d7c2e41..933c18460 100644 --- a/doc/configtables/solar.csv +++ b/doc/configtables/solar.csv @@ -1,5 +1,5 @@ ,Unit,Values,Description -cutout,--,"Should be a folder listed in the configuration ``atlite: cutouts:`` (e.g. 'europe-2013-sarah3-era5') or reference an existing folder in the directory ``cutouts``. Source module can be ERA5 or SARAH-2.","Specifies the directory where the relevant weather data ist stored that is specified at ``atlite/cutouts`` configuration. Both ``sarah`` and ``era5`` work." +cutout,--,"Should be a folder listed in the configuration ``atlite: cutouts:`` (e.g. 'europe-2013-sarah3-era5') or reference an existing folder in the directory ``cutouts``. Source module can be ERA5 and/or SARAH-3.","Specifies the directory where the relevant weather data ist stored that is specified at ``atlite/cutouts`` configuration. Both ``sarah`` and ``era5`` work." resource,,, -- method,--,"Must be 'pv'","A superordinate technology type." -- panel,--,"One of {'Csi', 'CdTe', 'KANENA'} as defined in `atlite `_ . Can be a string or a dictionary with years as keys which denote the year another turbine model becomes available.","Specifies the solar panel technology and its characteristic attributes." diff --git a/doc/img/intro-workflow.png b/doc/img/intro-workflow.png index 4009fbcd98650357e72299d0273884306464eb0d..87cec61f99f19fbfaeb220346b08e0130bb3172d 100644 GIT binary patch literal 124775 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 z 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. The + reference year was changed from 2015 to 2019. + (https://github.com/PyPSA/pypsa-eur/pull/1167) + +* Updated pre-built `weather data cutouts + `__. These are now merged cutouts with + solar irradiation from the new SARAH-3 dataset while taking all other + variables from ERA5. Cutouts are now available for multiple years (2010, 2013, + 2019, and 2023). The overall download size was cut in half. + (https://github.com/PyPSA/pypsa-eur/pull/1176) + +* Included data from the `Global Steel Plant Tracker + `__ + provided by Global Energy Monitor. The data includes among other attributes + the locations, ages, operating status, relining dates, manufacturing process + and capacities of steel plants in Europe. This data is used as a spatial + distribution key for the steel production, which is now separated by process + type (EAF, DRI + EAF, integrated). + (https://github.com/PyPSA/pypsa-eur/pull/1241) + +* Added data on the locations and capacities of ammonia plants in Europe. This + data is used as a spatial distribution key for the ammonia demand. The data + manually collected with sources noted in ``data/ammonia_plants.csv``. + (https://github.com/PyPSA/pypsa-eur/pull/1241) * Added data on the locations and capacities of cement plants in Europe that are not included in the Hotmaps industrial database. The data sourced from the @@ -25,117 +57,221 @@ Upcoming Release `__ of specific countries is used as a spatial distribution key for the cement demand. The data is stored in ``data/cement-plants-noneu.csv``. + (https://github.com/PyPSA/pypsa-eur/pull/1241) * Added data on the locations and capacities of refineries in Europe that are not included in the Hotmaps industrial database. The data is mostly sourced from the `Wikipedia list of oil refineries `__. The data is stored in ``data/refineries-noneu.csv``. - -* Included data from the `Global Steel Plant Tracker - `__ - provided by Global Energy Monitor. The data includes among other attributes - the locations, ages, operating status, relining dates, manufacturing process - and capacities of steel plants in Europe. This data is used as a spatial - distribution key for the steel production, which is now separated by process - type (EAF, DRI + EAF, integrated). + (https://github.com/PyPSA/pypsa-eur/pull/1241) * Retrieve share of urban population from `World Bank API `__. The data originates from the United Nations Population Division. Previously, a file ``data/urban_percent.csv`` with an undocumented source was used. + (https://github.com/PyPSA/pypsa-eur/pull/1248) + +* Updated Global Energy Monitor's Europe Gas Tracker to May 2024 version. + (https://github.com/PyPSA/pypsa-eur/pull/1235) * Updated country-specific Energy Availability Factors (EAFs) for nuclear power plants based on `IAEA 2021-2023 reported country averages `__. + (https://github.com/PyPSA/pypsa-eur/pull/1236) + +* Updated technology-data to v0.9.2, with added methanol and biomass + assumptions. + +* Updated EEZ shapes to v12. This data is now automatically retrieved and was + removed from the data bundle. (https://github.com/PyPSA/pypsa-eur/pull/1188, + https://github.com/PyPSA/pypsa-eur/pull/1210) -* Update GEM Europe Gas Tracker to May 2024 version. +* The country shapes from Naturalearth are now automatically retrieved and are + removed from the data bundle. (https://github.com/PyPSA/pypsa-eur/pull/1190) -* Add investment period dependent CO2 sequestration potentials +**New Features** + +* Improved biomass representation: -* Add option to produce hydrogen from solid biomass (flag ``solid biomass to hydrogen``), combined with carbon capture + * Added unsustainable biomass potentials for solid, gaseous, and liquid biomass + based on current consumption levels from Eurostat energy balances. The + potentials can be phased-out and/or substituted by the phase-in of sustainable + biomass types using the config parameters ``biomass: + share_unsustainable_use_retained`` and ``biomass: + share_sustainable_potential_available``. + (https://github.com/PyPSA/pypsa-eur/pull/1139) + + * Added energy penalty for BECC applications. + (https://github.com/PyPSA/pypsa-eur/pull/1130) -* Fixed PDF encoding in ``build_biomass_transport_costs`` with update of tabula-py and jpype1 + * Added option to enable the import of solid biomass. + (https://github.com/PyPSA/pypsa-eur/pull/1194) -* More modular and flexible handling of transmission projects. One can now add new transmission projects in a subfolder of `data/transmission projects` similar to the files in the template folder. After adding the new files and updating the config section `transmission_projects:`, transmission projects will be included if they are not duplicates of existing lines or other projects. + * Added option to produce electrobiofuels from solid biomass and hydrogen. This + process combined BtL and Fischer-Tropsch to efficiently use the available + biogenic carbon. (https://github.com/PyPSA/pypsa-eur/pull/1193) -* Add option to apply a gaussian kernel density smoothing to wind turbine power curves. + * Added option to split municipal solid waste from solid biomass. + (https://github.com/PyPSA/pypsa-eur/pull/1195, + https://github.com/PyPSA/pypsa-eur/pull/1134) -* Update JRC-IDEES-2015 to `JRC-IDEES-2021 `__. The reference year is changed from 2015 to 2019. + * Added option to produce hydrogen from solid biomass with or without carbon + capture. (https://github.com/PyPSA/pypsa-eur/pull/1213) -* Made central heating supply temperatures dynamic based on an adaptation of a reference curve from Pieper et al. (2019) (https://www.sciencedirect.com/science/article/pii/S0360544219305857?via%3Dihub). +* Improved district heating representation: -* Added option to use country-specific district heating forward and return temperatures. Defaults to lower temperatures in Scandinavia. + * Added option to use country-specific district heating forward and return + temperatures. Defaults to lower temperatures in Scandinavia. + (https://github.com/PyPSA/pypsa-eur/pull/1180) -* Added unsustainable biomass potentials for solid, gaseous, and liquid biomass. The potentials can be phased-out and/or - substituted by the phase-in of sustainable biomass types using the config parameters - ``biomass: share_unsustainable_use_retained`` and ``biomass: share_sustainable_potential_available``. + * Made central heating supply temperatures dynamic based on an adaptation of a + reference curve from Pieper et al. (2019) + (https://www.sciencedirect.com/science/article/pii/S0360544219305857?via%3Dihub). + (https://github.com/PyPSA/pypsa-eur/pull/1206/) -* The rule ``prepare_links_p_nom`` was removed since it was outdated and not used. + * Changed heat pump COP approximation for central heating to be based on + `Jensen et al. (2018) + `__ + and a default forward temperature of 90C. This is more realistic for + district heating than the previously used approximation method. + (https://github.com/PyPSA/pypsa-eur/pull/1176) -* Changed heat pump COP approximation for central heating to be based on `Jensen et al. (2018) `__ and a default forward temperature of 90C. This is more realistic for district heating than the previously used approximation method. + * Added option for various power-to-X processes to specify their share of waste + heat that can be used in district heating. The default was changed from 100% + to 25%. (https://github.com/PyPSA/pypsa-eur/pull/1141) -* split solid biomass potentials into solid biomass and municipal solid waste. Add option to use municipal solid waste. This option is only activated in combination with the flag ``waste_to_energy`` +* Added Enhanced Geothermal Systems for generation of electricity and district heat. + Cost and available capacity assumptions based on `Aghahosseini et al. (2020) + `__. + See configuration ``sector: enhanced_geothermal`` for details; by default switched off. -* Add option to import solid biomass +* Represent Kosovo (XK) as separate country. + (https://github.com/PyPSA/pypsa-eur/pull/1249) -* Add option to produce electrobiofuels (flag ``electrobiofuels``) from solid biomass and hydrogen, as a combination of BtL and Fischer-Tropsch to make more use of the biogenic carbon +* Add option to specify carbon sequestration potentials per investment period. + (https://github.com/PyPSA/pypsa-eur/pull/1228) -* Add flag ``sector: fossil_fuels`` in config to remove the option of importing fossil fuels +* Add option to completely eliminate the use of fossil fuels. + (https://github.com/PyPSA/pypsa-eur/pull/1187) -* Renamed the carrier of batteries in BEVs from `battery storage` to `EV battery` and the corresponding bus carrier from `Li ion` to `EV battery`. This is to avoid confusion with stationary battery storage. +* Added more modular and flexible handling of planned transmission reinforcement + projects (e.g. TYNDP). See configuration settings ``transmission_projects:``. + (https://github.com/PyPSA/pypsa-eur/pull/1085) -* Changed default assumptions about waste heat usage from PtX and fuel cells in district heating. - The default value for the link efficiency scaling factor was changed from 100% to 25%. - It can be set to other values in the configuration ``sector: use_TECHNOLOGY_waste_heat``. +* Added option to smooth wind turbine power curves with a Gaussian kernel density. + (https://github.com/PyPSA/pypsa-eur/pull/1209). -* In simplifying polygons in :mod:`build_shapes` default to no tolerance. +* Added option ``solving: curtailment_mode``` which fixes the dispatch profiles + of generators with time-varying p_max_pu by setting ``p_min_pu = p_max_pu`` + and adds an auxiliary curtailment generator with negative sign (to absorb + excess power) at every AC bus. This can speed up the solving process as the + curtailment decision is aggregated into a single generator per region. + (https://github.com/PyPSA/pypsa-eur/pull/1177) -* Set non-zero capital_cost for methanol stores to avoid unrealistic storage sizes +* Added capital costs to all liquid carbonaceous fuel stores. + (https://github.com/PyPSA/pypsa-eur/pull/1234) -* Set p_nom = p_nom_min for generators with baseyear == grouping_year in add_existing_baseyear. This has no effect on the optimization but helps n.statistics to correctly report already installed capacities. +**Breaking Changes** -* Reverted outdated hotfix for doubled renewable capacity in myopic optimization. +* Due to memory issues, the feature ``n.shapes`` is temporarily disabled. + (https://github.com/PyPSA/pypsa-eur/pull/1238) -* Added Enhanced Geothermal Systems for generation of electricity and district heat. - Cost and available capacity assumptions based on `Aghahosseini et al. (2020) - `__. - See configuration ``sector: enhanced_geothermal`` for details; by default switched off. +* Renamed the carrier of batteries in BEVs from `battery storage` to `EV + battery` and the corresponding bus carrier from `Li ion` to `EV battery`. This + is to avoid confusion with stationary battery storage. + (https://github.com/PyPSA/pypsa-eur/pull/1116) -* Partially revert https://github.com/PyPSA/pypsa-eur/pull/967 to return to old grouping year logic (which was mostly correct) +**Changes** -* Bugfix: Correctly read in threshold capacity below which to remove components from previous planning horizons in :mod:`add_brownfield`. +* Powerplants can now be assigned to all buses, not just substations. + (https://github.com/PyPSA/pypsa-eur/pull/1239) -* For countries not contained in the NUTS3-specific datasets (i.e. MD and UA), the mapping of GDP per capita and population per bus region used to spatially distribute electricity demand is now endogenised in a new rule :mod:`build_gdp_ppp_non_nuts3`. https://github.com/PyPSA/pypsa-eur/pull/1146 +* Avoid adding existing gas pipelines repeatedly for different planning + horizons. + (https://github.com/PyPSA/pypsa-eur/pull/1162https://github.com/PyPSA/pypsa-eur/pull/1162) -* The databundle has been updated to release v0.3.0, which includes raw GDP and population data for countries outside the NUTS system (UA, MD). https://github.com/PyPSA/pypsa-eur/pull/1146 +* Move custom busmaps to + ``data/busmaps/elec_s{simpl}_{clusters}_{base_network}.csv``. This allows for + different busmaps depending on the base network. + (https://github.com/PyPSA/pypsa-eur/pull/1231) -* Updated filtering in :mod:`determine_availability_matrix_MD_UA.py` to improve speed. https://github.com/PyPSA/pypsa-eur/pull/1146 +* For countries not contained in the NUTS3-specific datasets (i.e. MD and UA), + the mapping of GDP per capita and population per bus region used to spatially + distribute electricity demand is now endogenised in a new rule + :mod:`build_gdp_ppp_non_nuts3`. The databundle has been updated accordingly. + (https://github.com/PyPSA/pypsa-eur/pull/1146) -* Bugfix: Impose minimum value of zero for district heating progress between current and future market share in :mod:`build_district_heat_share`. +* Enable parallelism in :mod:`determine_availability_matrix_MD_UA.py` and remove + plots. This requires the use of temporary files. + (https://github.com/PyPSA/pypsa-eur/pull/1170) + +* In :mod:`base_network`, replace own voronoi polygon calculation function with + Geopandas `gdf.voronoi_polygons` method. + (https://github.com/PyPSA/pypsa-eur/pull/1172) + +* In simplifying polygons in :mod:`build_shapes` default to no tolerance. + (https://github.com/PyPSA/pypsa-eur/pull/1137) + +* Updated filtering in :mod:`determine_availability_matrix_MD_UA.py` to improve + speed. (https://github.com/PyPSA/pypsa-eur/pull/1146) + +* Removed unused data files and rules. + (https://github.com/PyPSA/pypsa-eur/pull/1246, + https://github.com/PyPSA/pypsa-eur/pull/1203) * The ``{scope}`` wildcard was removed, since its outputs were not used. + (https://github.com/PyPSA/pypsa-eur/pull/1171) -* Enable parallelism in :mod:`determine_availability_matrix_MD_UA.py` and remove plots. This requires the use of temporary files. +* Unify how the oil bus is added. -* Added new major feature to create the base_network from OpenStreetMap (OSM) data (PR https://github.com/PyPSA/pypsa-eur/pull/1079). Note that a heuristics based cleaning process is used for lines and links where electrical parameters are incomplete, missing, or ambiguous. Through ``electricity["base_network"]``, the base network can be set to "entsoegridkit" (now deprecated), "osm-prebuilt" (default, downloads the latest prebuilt snapshot based on OSM data from Zenodo), or "osm-raw" which retrieves (once) and cleans the raw OSM data and subsequently builds the network. Note that this process may take a few minutes. +* Set ``p_nom = p_nom_min`` for generators with ``baseyear == grouping_year`` in + :mod:`add_existing_baseyear`. This has no effect on the optimization but helps + to correctly report already installed capacities using ``n.statistics()``. -* Updated pre-built `weather data cutouts - `__. These are now merged cutouts with - solar irradiation from the new SARAH-3 dataset while taking all other - variables from ERA5. Cutouts are now available for multiple years (2010, 2013, - 2019, and 2023). +* Cutouts are no longer marked as ``protected()``. + (https://github.com/PyPSA/pypsa-eur/pull/1220) -* Added option ``solving: curtailment_mode``` which fixes the dispatch profiles - of generators with time-varying p_max_pu by setting ``p_min_pu = p_max_pu`` - and adds an auxiliary curtailment generator with negative sign (to absorb - excess power) at every AC bus. This can speed up the solving process as the - curtailment decision is aggregated into a single generator per region. +**Bugfixes and Compatibility** -* In :mod:`base_network`, replace own voronoi polygon calculation function with - Geopandas `gdf.voronoi_polygons` method. +* Bugfix in :mod:`simplify_network` for spatially resolving Corsica. + (https://github.com/PyPSA/pypsa-eur/pull/1215) + +* Bugfix for running without spatial resolution. + (https://github.com/PyPSA/pypsa-eur/pull/1183) + +* Bugfix: Impose minimum value of zero for district heating progress between + current and future market share in :mod:`build_district_heat_share`. + (https://github.com/PyPSA/pypsa-eur/pull/1168) + +* Bugfix: Correctly read in threshold capacity below which to remove components + from previous planning horizons in :mod:`add_brownfield`. + +* Bugfix for passing function arguments in rule :mod:`solve_operations_network`. + +* Bugfix avoiding infinity values in the intermediate industry sector ratios. + (https://github.com/PyPSA/pypsa-eur/pull/1227) + +* Bugfix: Add floating wind to cost update function in + :mod:`prepare_sector_network`. (https://github.com/PyPSA/pypsa-eur/pull/1106) + +* Fixed PDF encoding in ``build_biomass_transport_costs``. + (https://github.com/PyPSA/pypsa-eur/pull/1219) + +* Dropped ``pycountry`` dependency in favour of ``country_converter``. + (https://github.com/PyPSA/pypsa-eur/pull/1188) + +* Use temporary mirror for broken link to Eurostat energy balances (April 2023). + (https://github.com/PyPSA/pypsa-eur/pull/1147) + +* Compatibility with geopandas 1.0+. + (https://github.com/PyPSA/pypsa-eur/pull/1136) + +* Compatibility with snakemake 8.14+. + (https://github.com/PyPSA/pypsa-eur/pull/1112) + +* Address various deprecations. -* Move custom busmaps to ```data/busmaps/elec_s{simpl}_{clusters}_{base_network}.csv``` (if enabled). This allows for different busmaps depending on the base network and scenario. PyPSA-Eur 0.11.0 (25th May 2024) ===================================== diff --git a/doc/retrieve.rst b/doc/retrieve.rst index 6b339355a..a00a8c799 100644 --- a/doc/retrieve.rst +++ b/doc/retrieve.rst @@ -19,13 +19,43 @@ Rule ``retrieve_databundle`` .. automodule:: retrieve_databundle +Rule ``retrieve_eurostat_data`` +=============================== + +.. automodule:: retrieve_eurostat_data + + +Rule ``retrieve_jrc_idees`` +=============================== + +.. automodule:: retrieve_jrc_idees + + + +Rule ``retrieve_eurostat_household_data`` +========================================= + +.. automodule:: retrieve_eurostat_household_data + + +Rule ``retrieve_gas_infrastructure_data`` +========================================= + +.. automodule:: retrieve_gas_infrastructure_data + + +Rule ``retrieve_osm_data`` +========================================= + +.. automodule:: retrieve_osm_data + Rule ``retrieve_cutout`` ============================ .. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.6382570.svg :target: https://doi.org/10.5281/zenodo.6382570 -Cutouts are spatio-temporal subsets of the European weather data from the `ECMWF ERA5 `__ reanalysis dataset and the `CMSAF SARAH-2 `__ solar surface radiation dataset for the year 2013. +Cutouts are spatio-temporal subsets of the European weather data from the `ECMWF ERA5 `__ reanalysis dataset and the `CMSAF SARAH-3 `__ solar surface radiation dataset for the year 2013, 2019 or 2023. They have been prepared by and are for use with the `atlite `__ tool. You can either generate them yourself using the ``build_cutouts`` rule or retrieve them directly from `zenodo `__ through the rule ``retrieve_cutout``. The :ref:`tutorial` uses a smaller cutout than required for the full model (30 MB), which is also automatically downloaded. @@ -47,7 +77,7 @@ The :ref:`tutorial` uses a smaller cutout than required for the full model (30 M **Outputs** -- ``cutouts/{cutout}``: weather data from either the `ERA5 `__ reanalysis weather dataset or `SARAH-2 `__ satellite-based historic weather data. +- ``cutouts/{cutout}``: weather data from either the `ERA5 `__ reanalysis weather dataset and/or `SARAH-3 `__ satellite-based historic weather data. .. seealso:: For details see :mod:`build_cutout` and read the `atlite documentation `__. diff --git a/doc/sector.rst b/doc/sector.rst index e186ccf75..24f3c9e9d 100644 --- a/doc/sector.rst +++ b/doc/sector.rst @@ -43,6 +43,11 @@ Rule ``build_biomass_potentials`` .. automodule:: build_biomass_potentials +Rule ``build_egs_potentials`` +============================================================================== + +.. automodule:: build_egs_potentials + Rule ``build_biomass_transport_costs`` ============================================================================== @@ -58,6 +63,11 @@ Rule ``build_cop_profiles`` .. automodule:: build_cop_profiles +Rule ``build_central_heating_temperature_profiles`` +============================================================================== + +.. automodule:: build_central_heating_temperature_profiles + Rule ``build_energy_totals`` ============================================================================== diff --git a/doc/tutorial.rst b/doc/tutorial.rst index 4a07132be..73b4df8b8 100644 --- a/doc/tutorial.rst +++ b/doc/tutorial.rst @@ -80,7 +80,7 @@ adapt the required range of coordinates to the selection of countries. We can also decide which weather data source should be used to calculate potentials and capacity factor time-series for each carrier. For example, we may -want to use the ERA-5 dataset for solar and not the default SARAH-2 dataset. +want to use the ERA-5 dataset for solar and not the default SARAH-3 dataset. .. literalinclude:: ../config/test/config.electricity.yaml :language: yaml @@ -132,89 +132,99 @@ This triggers a workflow of multiple preceding jobs that depend on each rule's i graph[bgcolor=white, margin=0]; node[shape=box, style=rounded, fontname=sans, fontsize=10, penwidth=2]; edge[penwidth=2, color=grey]; - 0[label = "solve_network", color = "0.21 0.6 0.85", style="rounded"]; - 1[label = "prepare_network\nll: copt\nopts: ", color = "0.51 0.6 0.85", style="rounded"]; - 2[label = "add_extra_components", color = "0.43 0.6 0.85", style="rounded"]; - 3[label = "cluster_network\nclusters: 6", color = "0.17 0.6 0.85", style="rounded"]; - 4[label = "simplify_network\nsimpl: ", color = "0.49 0.6 0.85", style="rounded"]; - 5[label = "add_electricity", color = "0.26 0.6 0.85", style="rounded"]; - 6[label = "build_renewable_profiles\ntechnology: solar", color = "0.02 0.6 0.85", style="rounded"]; - 7[label = "base_network", color = "0.35 0.6 0.85", style="rounded"]; - 8[label = "build_shapes", color = "0.62 0.6 0.85", style="rounded"]; - 9[label = "retrieve_databundle", color = "0.24 0.6 0.85", style="rounded"]; - 10[label = "retrieve_cutout\ncutout: be-03-2013-era5", color = "0.36 0.6 0.85", style="rounded"]; - 11[label = "build_renewable_profiles\ntechnology: solar-hsat", color = "0.02 0.6 0.85", style="rounded"]; - 12[label = "build_renewable_profiles\ntechnology: onwind", color = "0.02 0.6 0.85", style="rounded"]; - 13[label = "build_renewable_profiles\ntechnology: offwind-ac", color = "0.02 0.6 0.85", style="rounded"]; - 14[label = "build_ship_raster", color = "0.08 0.6 0.85", style="rounded"]; - 15[label = "retrieve_ship_raster", color = "0.28 0.6 0.85", style="rounded"]; - 16[label = "build_renewable_profiles\ntechnology: offwind-dc", color = "0.02 0.6 0.85", style="rounded"]; - 17[label = "build_renewable_profiles\ntechnology: offwind-float", color = "0.02 0.6 0.85", style="rounded"]; - 18[label = "build_line_rating", color = "0.07 0.6 0.85", style="rounded"]; - 19[label = "retrieve_cost_data\nyear: 2030", color = "0.47 0.6 0.85", style="rounded"]; - 20[label = "build_powerplants", color = "0.11 0.6 0.85", style="rounded"]; - 21[label = "build_electricity_demand", color = "0.05 0.6 0.85", style="rounded"]; - 22[label = "retrieve_electricity_demand", color = "0.58 0.6 0.85", style="rounded"]; - 23[label = "retrieve_synthetic_electricity_demand", color = "0.11 0.6 0.85", style="rounded"]; + 0[label = "solve_network", color = "0.16 0.6 0.85", style="rounded"]; + 1[label = "prepare_network\nll: copt\nopts: ", color = "0.40 0.6 0.85", style="rounded"]; + 2[label = "add_extra_components", color = "0.03 0.6 0.85", style="rounded"]; + 3[label = "cluster_network\nclusters: 6", color = "0.26 0.6 0.85", style="rounded"]; + 4[label = "simplify_network\nsimpl: ", color = "0.17 0.6 0.85", style="rounded"]; + 5[label = "add_electricity", color = "0.39 0.6 0.85", style="rounded"]; + 6[label = "build_renewable_profiles\ntechnology: solar", color = "0.13 0.6 0.85", style="rounded"]; + 7[label = "base_network", color = "0.01 0.6 0.85", style="rounded"]; + 8[label = "retrieve_osm_prebuilt", color = "0.27 0.6 0.85", style="rounded"]; + 9[label = "build_shapes", color = "0.18 0.6 0.85", style="rounded"]; + 10[label = "retrieve_naturalearth_countries", color = "0.41 0.6 0.85", style="rounded"]; + 11[label = "retrieve_eez", color = "0.14 0.6 0.85", style="rounded"]; + 12[label = "retrieve_databundle", color = "0.38 0.6 0.85", style="rounded"]; + 13[label = "retrieve_cutout\ncutout: be-03-2013-era5", color = "0.51 0.6 0.85", style="rounded"]; + 14[label = "build_renewable_profiles\ntechnology: solar-hsat", color = "0.13 0.6 0.85", style="rounded"]; + 15[label = "build_renewable_profiles\ntechnology: onwind", color = "0.13 0.6 0.85", style="rounded"]; + 16[label = "build_renewable_profiles\ntechnology: offwind-ac", color = "0.13 0.6 0.85", style="rounded"]; + 17[label = "build_ship_raster", color = "0.16 0.6 0.85", style="rounded"]; + 18[label = "retrieve_ship_raster", color = "0.53 0.6 0.85", style="rounded"]; + 19[label = "build_renewable_profiles\ntechnology: offwind-dc", color = "0.13 0.6 0.85", style="rounded"]; + 20[label = "build_renewable_profiles\ntechnology: offwind-float", color = "0.13 0.6 0.85", style="rounded"]; + 21[label = "build_line_rating", color = "0.46 0.6 0.85", style="rounded"]; + 22[label = "build_transmission_projects", color = "0.29 0.6 0.85", style="rounded"]; + 23[label = "retrieve_cost_data\nyear: 2030", color = "0.11 0.6 0.85", style="rounded"]; + 24[label = "build_powerplants", color = "0.18 0.6 0.85", style="rounded"]; + 25[label = "build_electricity_demand", color = "0.30 0.6 0.85", style="rounded"]; + 26[label = "retrieve_electricity_demand", color = "0.13 0.6 0.85", style="rounded"]; + 27[label = "retrieve_synthetic_electricity_demand", color = "0.43 0.6 0.85", style="rounded"]; 1 -> 0 2 -> 1 - 19 -> 1 + 23 -> 1 3 -> 2 - 19 -> 2 + 23 -> 2 4 -> 3 - 19 -> 3 + 23 -> 3 5 -> 4 - 19 -> 4 + 23 -> 4 7 -> 4 6 -> 5 - 11 -> 5 - 12 -> 5 - 13 -> 5 + 14 -> 5 + 15 -> 5 16 -> 5 - 17 -> 5 - 7 -> 5 - 18 -> 5 19 -> 5 20 -> 5 + 7 -> 5 21 -> 5 - 8 -> 5 + 22 -> 5 + 23 -> 5 + 24 -> 5 + 25 -> 5 + 9 -> 5 7 -> 6 + 12 -> 6 9 -> 6 - 8 -> 6 - 10 -> 6 + 13 -> 6 8 -> 7 - 9 -> 8 - 7 -> 11 - 9 -> 11 - 8 -> 11 - 10 -> 11 - 7 -> 12 - 9 -> 12 - 8 -> 12 - 10 -> 12 - 7 -> 13 - 9 -> 13 - 14 -> 13 - 8 -> 13 - 10 -> 13 - 15 -> 14 - 10 -> 14 + 9 -> 7 + 10 -> 9 + 11 -> 9 + 12 -> 9 + 7 -> 14 + 12 -> 14 + 9 -> 14 + 13 -> 14 + 7 -> 15 + 12 -> 15 + 9 -> 15 + 13 -> 15 7 -> 16 + 12 -> 16 + 17 -> 16 9 -> 16 - 14 -> 16 - 8 -> 16 - 10 -> 16 - 7 -> 17 - 9 -> 17 - 14 -> 17 - 8 -> 17 - 10 -> 17 - 7 -> 18 - 10 -> 18 + 13 -> 16 + 18 -> 17 + 13 -> 17 + 7 -> 19 + 12 -> 19 + 17 -> 19 + 9 -> 19 + 13 -> 19 7 -> 20 - 22 -> 21 - 23 -> 21 - } + 12 -> 20 + 17 -> 20 + 9 -> 20 + 13 -> 20 + 7 -> 21 + 13 -> 21 + 7 -> 22 + 9 -> 22 + 7 -> 24 + 26 -> 25 + 27 -> 25 + } | @@ -235,17 +245,21 @@ In the terminal, this will show up as a list of jobs to be run: build_renewable_profiles 6 build_shapes 1 build_ship_raster 1 + build_transmission_projects 1 cluster_network 1 prepare_network 1 retrieve_cost_data 1 retrieve_cutout 1 retrieve_databundle 1 + retrieve_eez 1 retrieve_electricity_demand 1 + retrieve_naturalearth_countries 1 + retrieve_osm_prebuilt 1 retrieve_ship_raster 1 retrieve_synthetic_electricity_demand 1 simplify_network 1 solve_network 1 - total 24 + total 28 ``snakemake`` then runs these jobs in the correct order. diff --git a/doc/tutorial_sector.rst b/doc/tutorial_sector.rst index 450f1e696..a369356f2 100644 --- a/doc/tutorial_sector.rst +++ b/doc/tutorial_sector.rst @@ -74,6 +74,7 @@ which were already included in the electricity-only tutorial: base_network 1 build_ammonia_production 1 build_biomass_potentials 1 + build_central_heating_temperature_profiles 1 build_clustered_population_layouts 1 build_cop_profiles 1 build_daily_heat_demand 1 @@ -102,8 +103,9 @@ which were already included in the electricity-only tutorial: build_ship_raster 1 build_shipping_demand 1 build_simplified_population_layouts 1 - build_solar_thermal_profiles 3 - build_temperature_profiles 3 + build_solar_thermal_profiles 1 + build_temperature_profiles 1 + build_transmission_projects 1 build_transport_demand 1 cluster_gas_network 1 cluster_network 1 @@ -118,16 +120,23 @@ which were already included in the electricity-only tutorial: retrieve_cost_data 1 retrieve_cutout 1 retrieve_databundle 1 + retrieve_eez 1 retrieve_electricity_demand 1 retrieve_eurostat_data 1 retrieve_eurostat_household_data 1 retrieve_gas_infrastructure_data 1 + retrieve_gem_europe_gas_tracker 1 + retrieve_gem_steel_plant_tracker 1 + retrieve_jrc_idees 1 + retrieve_naturalearth_countries 1 + retrieve_osm_prebuilt 1 retrieve_ship_raster 1 retrieve_synthetic_electricity_demand 1 + retrieve_worldbank_urban_population 1 simplify_network 1 solve_sector_network 1 time_aggregation 1 - total 69 + total 74 This covers the retrieval of additional raw data from online resources and preprocessing data about the transport, industry, and heating sectors as well as @@ -146,264 +155,266 @@ successfully. graph[bgcolor=white, margin=0]; node[shape=box, style=rounded, fontname=sans, fontsize=10, penwidth=2]; edge[penwidth=2, color=grey]; - 0[label = "all", color = "0.28 0.6 0.85", style="rounded"]; - 1[label = "plot_summary", color = "0.60 0.6 0.85", style="rounded"]; + 0[label = "all", color = "0.22 0.6 0.85", style="rounded"]; + 1[label = "plot_summary", color = "0.11 0.6 0.85", style="rounded"]; 2[label = "make_summary", color = "0.30 0.6 0.85", style="rounded"]; - 3[label = "solve_sector_network", color = "0.36 0.6 0.85", style="rounded"]; - 4[label = "prepare_sector_network\nsector_opts: ", color = "0.22 0.6 0.85", style="rounded"]; - 5[label = "build_renewable_profiles\ntechnology: offwind-ac", color = "0.20 0.6 0.85", style="rounded"]; - 6[label = "base_network", color = "0.00 0.6 0.85", style="rounded"]; - 7[label = "build_shapes", color = "0.25 0.6 0.85", style="rounded"]; - 8[label = "retrieve_databundle", color = "0.06 0.6 0.85", style="rounded"]; - 9[label = "build_ship_raster", color = "0.06 0.6 0.85", style="rounded"]; - 10[label = "retrieve_ship_raster", color = "0.27 0.6 0.85", style="rounded"]; - 11[label = "retrieve_cutout\ncutout: be-03-2013-era5", color = "0.26 0.6 0.85", style="rounded"]; - 12[label = "build_renewable_profiles\ntechnology: offwind-dc", color = "0.20 0.6 0.85", style="rounded"]; - 13[label = "build_renewable_profiles\ntechnology: offwind-float", color = "0.20 0.6 0.85", style="rounded"]; - 14[label = "cluster_gas_network", color = "0.37 0.6 0.85", style="rounded"]; - 15[label = "build_gas_network", color = "0.44 0.6 0.85", style="rounded"]; - 16[label = "retrieve_gas_infrastructure_data", color = "0.43 0.6 0.85", style="rounded"]; - 17[label = "cluster_network\nclusters: 5", color = "0.08 0.6 0.85", style="rounded"]; - 18[label = "simplify_network\nsimpl: ", color = "0.01 0.6 0.85", style="rounded"]; - 19[label = "add_electricity", color = "0.53 0.6 0.85", style="rounded"]; - 20[label = "build_renewable_profiles\ntechnology: solar", color = "0.20 0.6 0.85", style="rounded"]; - 21[label = "build_renewable_profiles\ntechnology: solar-hsat", color = "0.20 0.6 0.85", style="rounded"]; - 22[label = "build_renewable_profiles\ntechnology: onwind", color = "0.20 0.6 0.85", style="rounded"]; - 23[label = "retrieve_cost_data\nyear: 2030", color = "0.11 0.6 0.85", style="rounded"]; - 24[label = "build_powerplants", color = "0.62 0.6 0.85", style="rounded"]; - 25[label = "build_electricity_demand", color = "0.66 0.6 0.85", style="rounded"]; - 26[label = "retrieve_electricity_demand", color = "0.20 0.6 0.85", style="rounded"]; - 27[label = "retrieve_synthetic_electricity_demand", color = "0.52 0.6 0.85", style="rounded"]; - 28[label = "build_gas_input_locations", color = "0.21 0.6 0.85", style="rounded"]; - 29[label = "time_aggregation", color = "0.58 0.6 0.85", style="rounded"]; - 30[label = "prepare_network\nll: v1.5\nopts: ", color = "0.61 0.6 0.85", style="rounded"]; - 31[label = "add_extra_components", color = "0.59 0.6 0.85", style="rounded"]; - 32[label = "build_hourly_heat_demand", color = "0.48 0.6 0.85", style="rounded"]; - 33[label = "build_daily_heat_demand\nscope: total", color = "0.12 0.6 0.85", style="rounded"]; - 34[label = "build_population_layouts", color = "0.62 0.6 0.85", style="rounded"]; - 35[label = "build_solar_thermal_profiles\nscope: total", color = "0.23 0.6 0.85", style="rounded"]; - 36[label = "retrieve_eurostat_data", color = "0.45 0.6 0.85", style="rounded"]; - 37[label = "build_population_weighted_energy_totals\nkind: energy", color = "0.22 0.6 0.85", style="rounded"]; - 38[label = "build_energy_totals", color = "0.65 0.6 0.85", style="rounded"]; - 39[label = "retrieve_eurostat_household_data", color = "0.36 0.6 0.85", style="rounded"]; - 40[label = "build_clustered_population_layouts", color = "0.02 0.6 0.85", style="rounded"]; - 41[label = "build_population_weighted_energy_totals\nkind: heat", color = "0.22 0.6 0.85", style="rounded"]; - 42[label = "build_heat_totals", color = "0.53 0.6 0.85", style="rounded"]; - 43[label = "build_shipping_demand", color = "0.17 0.6 0.85", style="rounded"]; - 44[label = "build_transport_demand", color = "0.49 0.6 0.85", style="rounded"]; - 45[label = "build_temperature_profiles\nscope: total", color = "0.32 0.6 0.85", style="rounded"]; - 46[label = "build_biomass_potentials\nplanning_horizons: 2030", color = "0.34 0.6 0.85", style="rounded"]; - 47[label = "build_salt_cavern_potentials", color = "0.55 0.6 0.85", style="rounded"]; - 48[label = "build_simplified_population_layouts", color = "0.46 0.6 0.85", style="rounded"]; - 49[label = "build_industrial_energy_demand_per_node", color = "0.14 0.6 0.85", style="rounded"]; - 50[label = "build_industry_sector_ratios_intermediate\nplanning_horizons: 2030", color = "0.27 0.6 0.85", style="rounded"]; - 51[label = "build_industry_sector_ratios", color = "0.11 0.6 0.85", style="rounded"]; - 52[label = "build_ammonia_production", color = "0.25 0.6 0.85", style="rounded"]; - 53[label = "build_industrial_energy_demand_per_country_today", color = "0.44 0.6 0.85", style="rounded"]; - 54[label = "build_industrial_production_per_country", color = "0.18 0.6 0.85", style="rounded"]; - 55[label = "build_industrial_production_per_node", color = "0.41 0.6 0.85", style="rounded"]; - 56[label = "build_industrial_distribution_key", color = "0.04 0.6 0.85", style="rounded"]; - 57[label = "build_industrial_production_per_country_tomorrow\nplanning_horizons: 2030", color = "0.09 0.6 0.85", style="rounded"]; - 58[label = "build_industrial_energy_demand_per_node_today", color = "0.46 0.6 0.85", style="rounded"]; - 59[label = "build_district_heat_share\nplanning_horizons: 2030", color = "0.39 0.6 0.85", style="rounded"]; - 60[label = "build_temperature_profiles\nscope: rural", color = "0.32 0.6 0.85", style="rounded"]; - 61[label = "build_temperature_profiles\nscope: urban", color = "0.32 0.6 0.85", style="rounded"]; - 62[label = "build_cop_profiles", color = "0.55 0.6 0.85", style="rounded"]; - 63[label = "build_solar_thermal_profiles\nscope: urban", color = "0.23 0.6 0.85", style="rounded"]; - 64[label = "build_solar_thermal_profiles\nscope: rural", color = "0.23 0.6 0.85", style="rounded"]; - 65[label = "plot_power_network_clustered", color = "0.41 0.6 0.85", style="rounded"]; - 66[label = "plot_power_network", color = "0.40 0.6 0.85", style="rounded"]; - 67[label = "plot_hydrogen_network", color = "0.42 0.6 0.85", style="rounded"]; - 68[label = "plot_gas_network", color = "0.32 0.6 0.85", style="rounded"]; + 3[label = "solve_sector_network", color = "0.42 0.6 0.85", style="rounded"]; + 4[label = "prepare_sector_network\nsector_opts: ", color = "0.45 0.6 0.85", style="rounded"]; + 5[label = "build_renewable_profiles\ntechnology: offwind-ac", color = "0.44 0.6 0.85", style="rounded"]; + 6[label = "base_network", color = "0.26 0.6 0.85", style="rounded"]; + 7[label = "retrieve_osm_prebuilt", color = "0.01 0.6 0.85", style="rounded"]; + 8[label = "build_shapes", color = "0.50 0.6 0.85", style="rounded"]; + 9[label = "retrieve_naturalearth_countries", color = "0.09 0.6 0.85", style="rounded"]; + 10[label = "retrieve_eez", color = "0.52 0.6 0.85", style="rounded"]; + 11[label = "retrieve_databundle", color = "0.00 0.6 0.85", style="rounded"]; + 12[label = "build_ship_raster", color = "0.29 0.6 0.85", style="rounded"]; + 13[label = "retrieve_ship_raster", color = "0.13 0.6 0.85", style="rounded"]; + 14[label = "retrieve_cutout\ncutout: be-03-2013-era5", color = "0.06 0.6 0.85", style="rounded"]; + 15[label = "build_renewable_profiles\ntechnology: offwind-dc", color = "0.44 0.6 0.85", style="rounded"]; + 16[label = "build_renewable_profiles\ntechnology: offwind-float", color = "0.44 0.6 0.85", style="rounded"]; + 17[label = "cluster_gas_network", color = "0.48 0.6 0.85", style="rounded"]; + 18[label = "build_gas_network", color = "0.59 0.6 0.85", style="rounded"]; + 19[label = "retrieve_gas_infrastructure_data", color = "0.14 0.6 0.85", style="rounded"]; + 20[label = "cluster_network\nclusters: 5", color = "0.08 0.6 0.85", style="rounded"]; + 21[label = "simplify_network\nsimpl: ", color = "0.25 0.6 0.85", style="rounded"]; + 22[label = "add_electricity", color = "0.46 0.6 0.85", style="rounded"]; + 23[label = "build_renewable_profiles\ntechnology: solar", color = "0.44 0.6 0.85", style="rounded"]; + 24[label = "build_renewable_profiles\ntechnology: solar-hsat", color = "0.44 0.6 0.85", style="rounded"]; + 25[label = "build_renewable_profiles\ntechnology: onwind", color = "0.44 0.6 0.85", style="rounded"]; + 26[label = "build_transmission_projects", color = "0.63 0.6 0.85", style="rounded"]; + 27[label = "retrieve_cost_data\nyear: 2030", color = "0.05 0.6 0.85", style="rounded"]; + 28[label = "build_powerplants", color = "0.43 0.6 0.85", style="rounded"]; + 29[label = "build_electricity_demand", color = "0.39 0.6 0.85", style="rounded"]; + 30[label = "retrieve_electricity_demand", color = "0.62 0.6 0.85", style="rounded"]; + 31[label = "retrieve_synthetic_electricity_demand", color = "0.31 0.6 0.85", style="rounded"]; + 32[label = "build_gas_input_locations", color = "0.45 0.6 0.85", style="rounded"]; + 33[label = "retrieve_gem_europe_gas_tracker", color = "0.33 0.6 0.85", style="rounded"]; + 34[label = "time_aggregation", color = "0.60 0.6 0.85", style="rounded"]; + 35[label = "prepare_network\nll: v1.5\nopts: ", color = "0.23 0.6 0.85", style="rounded"]; + 36[label = "add_extra_components", color = "0.36 0.6 0.85", style="rounded"]; + 37[label = "build_hourly_heat_demand", color = "0.15 0.6 0.85", style="rounded"]; + 38[label = "build_daily_heat_demand", color = "0.57 0.6 0.85", style="rounded"]; + 39[label = "build_population_layouts", color = "0.47 0.6 0.85", style="rounded"]; + 40[label = "retrieve_worldbank_urban_population", color = "0.19 0.6 0.85", style="rounded"]; + 41[label = "build_solar_thermal_profiles", color = "0.11 0.6 0.85", style="rounded"]; + 42[label = "retrieve_eurostat_data", color = "0.04 0.6 0.85", style="rounded"]; + 43[label = "build_population_weighted_energy_totals\nkind: energy", color = "0.04 0.6 0.85", style="rounded"]; + 44[label = "build_energy_totals", color = "0.30 0.6 0.85", style="rounded"]; + 45[label = "retrieve_jrc_idees", color = "0.02 0.6 0.85", style="rounded"]; + 46[label = "retrieve_eurostat_household_data", color = "0.49 0.6 0.85", style="rounded"]; + 47[label = "build_clustered_population_layouts", color = "0.19 0.6 0.85", style="rounded"]; + 48[label = "build_population_weighted_energy_totals\nkind: heat", color = "0.04 0.6 0.85", style="rounded"]; + 49[label = "build_heat_totals", color = "0.08 0.6 0.85", style="rounded"]; + 50[label = "build_shipping_demand", color = "0.52 0.6 0.85", style="rounded"]; + 51[label = "build_transport_demand", color = "0.16 0.6 0.85", style="rounded"]; + 52[label = "build_temperature_profiles", color = "0.58 0.6 0.85", style="rounded"]; + 53[label = "build_biomass_potentials\nplanning_horizons: 2030", color = "0.55 0.6 0.85", style="rounded"]; + 54[label = "build_salt_cavern_potentials", color = "0.28 0.6 0.85", style="rounded"]; + 55[label = "build_simplified_population_layouts", color = "0.14 0.6 0.85", style="rounded"]; + 56[label = "build_industrial_energy_demand_per_node", color = "0.24 0.6 0.85", style="rounded"]; + 57[label = "build_industry_sector_ratios_intermediate\nplanning_horizons: 2030", color = "0.60 0.6 0.85", style="rounded"]; + 58[label = "build_industry_sector_ratios", color = "0.26 0.6 0.85", style="rounded"]; + 59[label = "build_ammonia_production", color = "0.16 0.6 0.85", style="rounded"]; + 60[label = "build_industrial_energy_demand_per_country_today", color = "0.18 0.6 0.85", style="rounded"]; + 61[label = "build_industrial_production_per_country", color = "0.61 0.6 0.85", style="rounded"]; + 62[label = "build_industrial_production_per_node", color = "0.65 0.6 0.85", style="rounded"]; + 63[label = "build_industrial_distribution_key", color = "0.31 0.6 0.85", style="rounded"]; + 64[label = "retrieve_gem_steel_plant_tracker", color = "0.27 0.6 0.85", style="rounded"]; + 65[label = "build_industrial_production_per_country_tomorrow\nplanning_horizons: 2030", color = "0.09 0.6 0.85", style="rounded"]; + 66[label = "build_industrial_energy_demand_per_node_today", color = "0.40 0.6 0.85", style="rounded"]; + 67[label = "build_district_heat_share\nplanning_horizons: 2030", color = "0.07 0.6 0.85", style="rounded"]; + 68[label = "build_cop_profiles", color = "0.38 0.6 0.85", style="rounded"]; + 69[label = "build_central_heating_temperature_profiles", color = "0.55 0.6 0.85", style="rounded"]; + 70[label = "plot_power_network_clustered", color = "0.20 0.6 0.85", style="rounded"]; + 71[label = "plot_power_network", color = "0.53 0.6 0.85", style="rounded"]; + 72[label = "plot_hydrogen_network", color = "0.64 0.6 0.85", style="rounded"]; + 73[label = "plot_gas_network", color = "0.28 0.6 0.85", style="rounded"]; 1 -> 0 2 -> 1 - 36 -> 1 - 8 -> 1 + 42 -> 1 + 11 -> 1 3 -> 2 - 23 -> 2 - 65 -> 2 - 66 -> 2 - 67 -> 2 - 68 -> 2 + 27 -> 2 + 70 -> 2 + 71 -> 2 + 72 -> 2 + 73 -> 2 4 -> 3 5 -> 4 - 12 -> 4 - 13 -> 4 - 14 -> 4 - 28 -> 4 - 29 -> 4 - 30 -> 4 - 36 -> 4 - 37 -> 4 - 41 -> 4 - 43 -> 4 - 44 -> 4 - 38 -> 4 - 8 -> 4 - 46 -> 4 - 23 -> 4 - 47 -> 4 - 18 -> 4 + 15 -> 4 + 16 -> 4 17 -> 4 - 40 -> 4 - 48 -> 4 - 49 -> 4 32 -> 4 - 59 -> 4 - 45 -> 4 - 60 -> 4 - 61 -> 4 - 62 -> 4 + 34 -> 4 35 -> 4 - 63 -> 4 - 64 -> 4 + 42 -> 4 + 43 -> 4 + 48 -> 4 + 50 -> 4 + 51 -> 4 + 44 -> 4 + 11 -> 4 + 53 -> 4 + 27 -> 4 + 54 -> 4 + 21 -> 4 + 20 -> 4 + 47 -> 4 + 55 -> 4 + 56 -> 4 + 37 -> 4 + 67 -> 4 + 52 -> 4 + 68 -> 4 + 41 -> 4 6 -> 5 - 8 -> 5 - 9 -> 5 - 7 -> 5 11 -> 5 + 12 -> 5 + 8 -> 5 + 14 -> 5 7 -> 6 - 8 -> 7 - 10 -> 9 - 11 -> 9 - 6 -> 12 - 8 -> 12 - 9 -> 12 - 7 -> 12 - 11 -> 12 - 6 -> 13 - 8 -> 13 - 9 -> 13 - 7 -> 13 - 11 -> 13 - 15 -> 14 - 17 -> 14 - 16 -> 15 + 8 -> 6 + 9 -> 8 + 10 -> 8 + 11 -> 8 + 13 -> 12 + 14 -> 12 + 6 -> 15 + 11 -> 15 + 12 -> 15 + 8 -> 15 + 14 -> 15 + 6 -> 16 + 11 -> 16 + 12 -> 16 + 8 -> 16 + 14 -> 16 18 -> 17 - 23 -> 17 + 20 -> 17 19 -> 18 - 23 -> 18 - 6 -> 18 - 20 -> 19 - 21 -> 19 - 22 -> 19 - 5 -> 19 - 12 -> 19 - 13 -> 19 - 6 -> 19 - 23 -> 19 - 24 -> 19 - 25 -> 19 - 7 -> 19 - 6 -> 20 - 8 -> 20 - 7 -> 20 - 11 -> 20 + 21 -> 20 + 27 -> 20 + 22 -> 21 + 27 -> 21 6 -> 21 - 8 -> 21 - 7 -> 21 - 11 -> 21 + 23 -> 22 + 24 -> 22 + 25 -> 22 + 5 -> 22 + 15 -> 22 + 16 -> 22 6 -> 22 + 26 -> 22 + 27 -> 22 + 28 -> 22 + 29 -> 22 8 -> 22 - 7 -> 22 - 11 -> 22 + 6 -> 23 + 11 -> 23 + 8 -> 23 + 14 -> 23 6 -> 24 - 26 -> 25 - 27 -> 25 - 16 -> 28 - 17 -> 28 + 11 -> 24 + 8 -> 24 + 14 -> 24 + 6 -> 25 + 11 -> 25 + 8 -> 25 + 14 -> 25 + 6 -> 26 + 8 -> 26 + 6 -> 28 30 -> 29 - 32 -> 29 - 35 -> 29 - 31 -> 30 - 23 -> 30 - 17 -> 31 - 23 -> 31 + 31 -> 29 33 -> 32 - 34 -> 33 - 17 -> 33 - 11 -> 33 - 7 -> 34 - 11 -> 34 - 34 -> 35 - 17 -> 35 - 11 -> 35 + 19 -> 32 + 20 -> 32 + 35 -> 34 + 37 -> 34 + 41 -> 34 + 36 -> 35 + 27 -> 35 + 20 -> 36 + 27 -> 36 38 -> 37 - 40 -> 37 - 7 -> 38 - 8 -> 38 - 36 -> 38 39 -> 38 - 34 -> 40 - 17 -> 40 - 11 -> 40 - 42 -> 41 - 40 -> 41 - 38 -> 42 - 7 -> 43 - 17 -> 43 - 38 -> 43 - 40 -> 44 - 37 -> 44 - 38 -> 44 + 20 -> 38 + 14 -> 38 + 8 -> 39 + 40 -> 39 + 14 -> 39 + 39 -> 41 + 20 -> 41 + 14 -> 41 + 44 -> 43 + 47 -> 43 8 -> 44 + 11 -> 44 45 -> 44 - 34 -> 45 - 17 -> 45 - 11 -> 45 - 8 -> 46 - 17 -> 46 - 7 -> 46 - 8 -> 47 - 17 -> 47 - 34 -> 48 - 18 -> 48 - 11 -> 48 - 50 -> 49 - 55 -> 49 - 58 -> 49 - 51 -> 50 - 53 -> 50 - 54 -> 50 + 42 -> 44 + 46 -> 44 + 39 -> 47 + 20 -> 47 + 14 -> 47 + 49 -> 48 + 47 -> 48 + 44 -> 49 + 8 -> 50 + 20 -> 50 + 44 -> 50 + 47 -> 51 + 43 -> 51 + 44 -> 51 + 11 -> 51 52 -> 51 - 8 -> 51 - 8 -> 52 + 39 -> 52 + 20 -> 52 + 14 -> 52 + 42 -> 53 + 11 -> 53 + 20 -> 53 8 -> 53 - 54 -> 53 - 52 -> 54 - 8 -> 54 - 36 -> 54 - 56 -> 55 - 57 -> 55 - 17 -> 56 - 40 -> 56 - 54 -> 57 - 56 -> 58 - 53 -> 58 - 38 -> 59 - 40 -> 59 - 34 -> 60 - 17 -> 60 - 11 -> 60 - 34 -> 61 - 17 -> 61 - 11 -> 61 - 45 -> 62 - 60 -> 62 - 61 -> 62 - 34 -> 63 - 17 -> 63 - 11 -> 63 - 34 -> 64 - 17 -> 64 - 11 -> 64 - 17 -> 65 - 3 -> 66 - 17 -> 66 - 3 -> 67 - 17 -> 67 - 3 -> 68 - 17 -> 68 + 11 -> 54 + 20 -> 54 + 39 -> 55 + 21 -> 55 + 14 -> 55 + 57 -> 56 + 62 -> 56 + 66 -> 56 + 58 -> 57 + 60 -> 57 + 61 -> 57 + 59 -> 58 + 45 -> 58 + 44 -> 60 + 45 -> 60 + 61 -> 60 + 59 -> 61 + 45 -> 61 + 42 -> 61 + 63 -> 62 + 65 -> 62 + 20 -> 63 + 47 -> 63 + 64 -> 63 + 61 -> 65 + 63 -> 66 + 60 -> 66 + 44 -> 67 + 47 -> 67 + 69 -> 68 + 52 -> 68 + 20 -> 68 + 52 -> 69 + 20 -> 69 + 20 -> 70 + 3 -> 71 + 20 -> 71 + 3 -> 72 + 20 -> 72 + 3 -> 73 + 20 -> 73 } | diff --git a/envs/environment.fixed.yaml b/envs/environment.fixed.yaml index 31dc75274..d625a5b16 100644 --- a/envs/environment.fixed.yaml +++ b/envs/environment.fixed.yaml @@ -1,469 +1,6 @@ -# SPDX-FileCopyrightText: : 2017-2024 The PyPSA-Eur Authors -# -# SPDX-License-Identifier: CC0-1.0 - -name: pypsa-eur +name: pypsa-eur-20240812 channels: -- conda-forge -- bioconda -- gurobi -- defaults -dependencies: -- _libgcc_mutex=0.1 -- _openmp_mutex=4.5 -- affine=2.4.0 -- alsa-lib=1.2.11 -- ampl-mp=3.1.0 -- amply=0.1.6 -- appdirs=1.4.4 -- argparse-dataclass=2.0.0 -- asttokens=2.4.1 -- atk-1.0=2.38.0 -- atlite=0.2.12 -- attr=2.5.1 -- attrs=23.2.0 -- aws-c-auth=0.7.22 -- aws-c-cal=0.6.14 -- aws-c-common=0.9.19 -- aws-c-compression=0.2.18 -- aws-c-event-stream=0.4.2 -- aws-c-http=0.8.1 -- aws-c-io=0.14.8 -- aws-c-mqtt=0.10.4 -- aws-c-s3=0.5.9 -- aws-c-sdkutils=0.1.16 -- aws-checksums=0.1.18 -- aws-crt-cpp=0.26.9 -- aws-sdk-cpp=1.11.329 -- azure-core-cpp=1.11.1 -- azure-identity-cpp=1.6.0 -- azure-storage-blobs-cpp=12.10.0 -- azure-storage-common-cpp=12.5.0 -- beautifulsoup4=4.12.3 -- blosc=1.21.5 -- bokeh=3.4.1 -- bottleneck=1.3.8 -- branca=0.7.2 -- brotli=1.1.0 -- brotli-bin=1.1.0 -- brotli-python=1.1.0 -- bzip2=1.0.8 -- c-ares=1.28.1 -- c-blosc2=2.14.4 -- ca-certificates=2024.2.2 -- cads-api-client=1.0.3 -- cairo=1.18.0 -- cartopy=0.23.0 -- cdsapi=0.7.0 -- certifi=2024.2.2 -- cffi=1.16.0 -- cfgv=3.3.1 -- cfitsio=4.4.0 -- cftime=1.6.3 -- charset-normalizer=3.3.2 -- click=8.1.7 -- click-plugins=1.1.1 -- cligj=0.7.2 -- cloudpickle=3.0.0 -- coin-or-cbc=2.10.10 -- coin-or-cgl=0.60.7 -- coin-or-clp=1.17.8 -- coin-or-osi=0.108.10 -- coin-or-utils=2.11.11 -- coincbc=2.10.10 -- colorama=0.4.6 -- conda-inject=1.3.1 -- configargparse=1.7 -- connection_pool=0.0.3 -- contourpy=1.2.1 -- country_converter=1.2 -- cppad=20240000.4 -- cycler=0.12.1 -- cytoolz=0.12.3 -- dask=2024.5.1 -- dask-core=2024.5.1 -- dask-expr=1.1.1 -- datrie=0.8.2 -- dbus=1.13.6 -- decorator=5.1.1 -- deprecation=2.1.0 -- descartes=1.1.0 -- distlib=0.3.8 -- distributed=2024.5.1 -- distro=1.9.0 -- docutils=0.21.2 -- dpath=2.1.6 -- entsoe-py=0.6.7 -- et_xmlfile=1.1.0 -- exceptiongroup=1.2.0 -- executing=2.0.1 -- expat=2.6.2 -- filelock=3.14.0 -- fiona=1.9.6 -- fmt=10.2.1 -- folium=0.16.0 -- font-ttf-dejavu-sans-mono=2.37 -- font-ttf-inconsolata=3.000 -- font-ttf-source-code-pro=2.038 -- font-ttf-ubuntu=0.83 -- fontconfig=2.14.2 -- fonts-conda-ecosystem=1 -- fonts-conda-forge=1 -- fonttools=4.52.1 -- freetype=2.12.1 -- freexl=2.0.0 -- fribidi=1.0.10 -- fsspec=2024.5.0 -- gdal=3.8.5 -- gdk-pixbuf=2.42.12 -- geographiclib=2.0 -- geojson-rewind=1.1.0 -- geopandas=0.14.4 -- geopandas-base=0.14.4 -- geopy=2.4.1 -- geos=3.12.1 -- geotiff=1.7.3 -- gettext=0.22.5 -- gettext-tools=0.22.5 -- gflags=2.2.2 -- giflib=5.2.2 -- gitdb=4.0.11 -- gitpython=3.1.43 -- glib=2.80.2 -- glib-tools=2.80.2 -- glog=0.7.0 -- glpk=5.0 -- gmp=6.3.0 -- graphite2=1.3.13 -- graphviz=11.0.0 -- gst-plugins-base=1.24.3 -- gstreamer=1.24.3 -- gtk2=2.24.33 -- gts=0.7.6 -- gurobi=11.0.2 -- harfbuzz=8.5.0 -- hdf4=4.2.15 -- hdf5=1.14.3 -- humanfriendly=10.0 -- icu=73.2 -- identify=2.5.36 -- idna=3.7 -- immutables=0.20 -- importlib-metadata=7.1.0 -- importlib_metadata=7.1.0 -- importlib_resources=6.4.0 -- iniconfig=2.0.0 -- ipopt=3.14.16 -- ipython=8.24.0 -- jedi=0.19.1 -- jinja2=3.1.4 -- joblib=1.4.2 -- json-c=0.17 -- jsonschema=4.22.0 -- jsonschema-specifications=2023.12.1 -- jupyter_core=5.7.2 -- kealib=1.5.3 -- keyutils=1.6.1 -- kiwisolver=1.4.5 -- krb5=1.21.2 -- lame=3.100 -- lcms2=2.16 -- ld_impl_linux-64=2.40 -- lerc=4.0.0 -- libabseil=20240116.2 -- libaec=1.1.3 -- libarchive=3.7.4 -- libarrow=16.1.0 -- libarrow-acero=16.1.0 -- libarrow-dataset=16.1.0 -- libarrow-substrait=16.1.0 -- libasprintf=0.22.5 -- libasprintf-devel=0.22.5 -- libblas=3.9.0 -- libboost-headers=1.85.0 -- libbrotlicommon=1.1.0 -- libbrotlidec=1.1.0 -- libbrotlienc=1.1.0 -- libcap=2.69 -- libcblas=3.9.0 -- libclang-cpp15=15.0.7 -- libclang13=18.1.5 -- libcrc32c=1.1.2 -- libcups=2.3.3 -- libcurl=8.8.0 -- libdeflate=1.20 -- libedit=3.1.20191231 -- libev=4.33 -- libevent=2.1.12 -- libexpat=2.6.2 -- libffi=3.4.2 -- libflac=1.4.3 -- libgcc-ng=13.2.0 -- libgcrypt=1.10.3 -- libgd=2.3.3 -- libgdal=3.8.5 -- libgettextpo=0.22.5 -- libgettextpo-devel=0.22.5 -- libgfortran-ng=13.2.0 -- libgfortran5=13.2.0 -- libglib=2.80.2 -- libgomp=13.2.0 -- libgoogle-cloud=2.24.0 -- libgoogle-cloud-storage=2.24.0 -- libgpg-error=1.49 -- libgrpc=1.62.2 -- libhwloc=2.9.3 -- libiconv=1.17 -- libjpeg-turbo=3.0.0 -- libkml=1.3.0 -- liblapack=3.9.0 -- liblapacke=3.9.0 -- libllvm15=15.0.7 -- libllvm18=18.1.6 -- libnetcdf=4.9.2 -- libnghttp2=1.58.0 -- libnsl=2.0.1 -- libogg=1.3.4 -- libopenblas=0.3.27 -- libopus=1.3.1 -- libparquet=16.1.0 -- libpng=1.6.43 -- libpq=16.3 -- libprotobuf=4.25.3 -- libre2-11=2023.09.01 -- librsvg=2.58.0 -- librttopo=1.1.0 -- libscotch=7.0.4 -- libsndfile=1.2.2 -- libspatialindex=1.9.3 -- libspatialite=5.1.0 -- libspral=2024.01.18 -- libsqlite=3.45.3 -- libssh2=1.11.0 -- libstdcxx-ng=13.2.0 -- libsystemd0=255 -- libthrift=0.19.0 -- libtiff=4.6.0 -- libutf8proc=2.8.0 -- libuuid=2.38.1 -- libvorbis=1.3.7 -- libwebp=1.4.0 -- libwebp-base=1.4.0 -- libxcb=1.15 -- libxcrypt=4.4.36 -- libxkbcommon=1.7.0 -- libxml2=2.12.7 -- libxslt=1.1.39 -- libzip=1.10.1 -- libzlib=1.2.13 -- linopy=0.3.9 -- locket=1.0.0 -- lxml=5.2.2 -- lz4=4.3.3 -- lz4-c=1.9.4 -- lzo=2.10 -- mapclassify=2.6.1 -- markupsafe=2.1.5 -- matplotlib=3.8.4 -- matplotlib-base=3.8.4 -- matplotlib-inline=0.1.7 -- memory_profiler=0.61.0 -- metis=5.1.0 -- minizip=4.0.5 -- mpfr=4.2.1 -- mpg123=1.32.6 -- msgpack-python=1.0.8 -- multiurl=0.3.1 -- mumps-include=5.7.1 -- mumps-seq=5.7.1 -- munkres=1.1.4 -- mysql-common=8.3.0 -- mysql-libs=8.3.0 -- nbformat=5.10.4 -- ncurses=6.5 -- netcdf4=1.6.5 -- networkx=3.3 -- nodeenv=1.8.0 -- nomkl=1.0 -- nspr=4.35 -- nss=3.100 -- numexpr=2.9.0 -- numpy=1.26.4 -- openjdk=22.0.1 -- openjpeg=2.5.2 -- openpyxl=3.1.2 -- openssl=3.3.0 -- orc=2.0.1 -- packaging=24.0 -- pandas=2.2.2 -- pango=1.52.2 -- parso=0.8.4 -- partd=1.4.2 -- patsy=0.5.6 -- pcre2=10.43 -- pexpect=4.9.0 -- pickleshare=0.7.5 -- pillow=10.3.0 -- pip=24.0 -- pixman=0.43.2 -- pkgutil-resolve-name=1.3.10 -- plac=1.4.3 -- platformdirs=4.2.2 -- pluggy=1.5.0 -- ply=3.11 -- poppler=24.04.0 -- poppler-data=0.4.12 -- postgresql=16.3 -- powerplantmatching=0.5.15 -- pre-commit=3.7.1 -- progressbar2=4.4.2 -- proj=9.4.0 -- prompt-toolkit=3.0.42 -- psutil=5.9.8 -- pthread-stubs=0.4 -- ptyprocess=0.7.0 -- pulp=2.8.0 -- pulseaudio-client=17.0 -- pure_eval=0.2.2 -- py-cpuinfo=9.0.0 -- pyarrow=16.1.0 -- pyarrow-core=16.1.0 -- pyarrow-hotfix=0.6 -- pycountry=22.3.5 -- pycparser=2.22 -- pygments=2.18.0 -- pyomo=6.6.1 -- pyparsing=3.1.2 -- pyproj=3.6.1 -- pypsa=0.29.0 -- pyqt=5.15.9 -- pyqt5-sip=12.12.2 -- pyscipopt=5.0.1 -- pyshp=2.3.1 -- pysocks=1.7.1 -- pytables=3.9.2 -- pytest=8.2.1 -- python=3.11.9 -- python-dateutil=2.9.0 -- python-fastjsonschema=2.19.1 -- python-tzdata=2024.1 -- python-utils=3.8.2 -- python_abi=3.11 -- pytz=2024.1 -- pyxlsb=1.0.10 -- pyyaml=6.0.1 -- qt-main=5.15.8 -- rasterio=1.3.10 -- re2=2023.09.01 -- readline=8.2 -- referencing=0.35.1 -- requests=2.32.2 -- reretry=0.11.8 -- rioxarray=0.15.5 -- rpds-py=0.18.1 -- rtree=1.2.0 -- s2n=1.4.15 -- scikit-learn=1.5.0 -- scip=9.0.1 -- scipy=1.13.1 -- scotch=7.0.4 -- seaborn=0.13.2 -- seaborn-base=0.13.2 -- setuptools=70.0.0 -- setuptools-scm=8.1.0 -- setuptools_scm=8.1.0 -- shapely=2.0.4 -- sip=6.7.12 -- six=1.16.0 -- smart_open=7.0.4 -- smmap=5.0.0 -- snakemake-interface-common=1.17.2 -- snakemake-interface-executor-plugins=9.1.1 -- snakemake-interface-report-plugins=1.0.0 -- snakemake-interface-storage-plugins=3.2.2 -- snakemake-minimal=8.11.6 -- snappy=1.2.0 -- snuggs=1.4.7 -- sortedcontainers=2.4.0 -- soupsieve=2.5 -- spdlog=1.13.0 -- sqlite=3.45.3 -- stack_data=0.6.2 -- statsmodels=0.14.2 -- stopit=1.1.2 -- jpype1=1.5.0 -- tabulate=0.9.0 -- tbb=2021.11.0 -- tblib=3.0.0 -- threadpoolctl=3.5.0 -- throttler=1.2.2 -- tiledb=2.23.0 -- tk=8.6.13 -- toml=0.10.2 -- tomli=2.0.1 -- toolz=0.12.1 -- toposort=1.10 -- tornado=6.4 -- tqdm=4.66.4 -- traitlets=5.14.3 -- typing-extensions=4.11.0 -- typing_extensions=4.11.0 -- tzcode=2024a -- tzdata=2024a -- ukkonen=1.0.1 -- unidecode=1.3.8 -- unixodbc=2.3.12 -- uriparser=0.9.8 -- urllib3=2.2.1 -- validators=0.28.2 -- virtualenv=20.26.2 -- wcwidth=0.2.13 -- wheel=0.43.0 -- wrapt=1.16.0 -- xarray=2024.5.0 -- xcb-util=0.4.0 -- xcb-util-image=0.4.0 -- xcb-util-keysyms=0.4.0 -- xcb-util-renderutil=0.3.9 -- xcb-util-wm=0.4.1 -- xerces-c=3.2.5 -- xkeyboard-config=2.41 -- xlrd=2.0.1 -- xorg-fixesproto=5.0 -- xorg-inputproto=2.3.2 -- xorg-kbproto=1.0.7 -- xorg-libice=1.1.1 -- xorg-libsm=1.2.4 -- xorg-libx11=1.8.9 -- xorg-libxau=1.0.11 -- xorg-libxdmcp=1.1.3 -- xorg-libxext=1.3.4 -- xorg-libxfixes=5.0.3 -- xorg-libxi=1.7.10 -- xorg-libxrender=0.9.11 -- xorg-libxt=1.3.0 -- xorg-libxtst=1.2.3 -- xorg-recordproto=1.14.2 -- xorg-renderproto=0.11.1 -- xorg-xextproto=7.3.0 -- xorg-xf86vidmodeproto=2.3.1 -- xorg-xproto=7.0.31 -- xyzservices=2024.4.0 -- xz=5.2.6 -- yaml=0.2.5 -- yte=1.5.4 -- zict=3.0.0 -- zipp=3.17.0 -- zlib=1.2.13 -- zlib-ng=2.0.7 -- zstd=1.5.6 -- pip: - - highspy==1.5.3 - - oauthlib==3.2.2 - - requests-oauthlib==1.3.1 - - snakemake-executor-plugin-cluster-generic==1.0.9 - - snakemake-executor-plugin-slurm==0.5.1 - - snakemake-executor-plugin-slurm-jobstep==0.2.1 - - snakemake-storage-plugin-http==0.2.3 - - tsam==2.3.1 - - tabula-py=2.9.3 + - http://conda.anaconda.org/gurobi + - conda-forge + - defaults +prefix: /home/fneum/miniconda3/envs/pypsa-eur-20240812 diff --git a/scripts/build_cutout.py b/scripts/build_cutout.py index e8d6207c4..015eb66a8 100644 --- a/scripts/build_cutout.py +++ b/scripts/build_cutout.py @@ -37,7 +37,7 @@ ------- - ``cutouts/{cutout}``: weather data from either the `ERA5 `_ - reanalysis weather dataset or `SARAH-2 `_ + reanalysis weather dataset or `SARAH-3 `_ satellite-based historic weather data with the following structure: **ERA5 cutout:** @@ -80,7 +80,7 @@ .. image:: img/era5.png :scale: 40 % -A **SARAH-2 cutout** can be used to amend the fields ``temperature``, ``influx_toa``, ``influx_direct``, ``albedo``, +A **SARAH-3 cutout** can be used to amend the fields ``temperature``, ``influx_toa``, ``influx_direct``, ``albedo``, ``influx_diffuse`` of ERA5 using satellite-based radiation observations. .. image:: img/sarah.png From 56f2b581bfceaf4be67bf55ea6ba76aa4dd68bc3 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 30 Aug 2024 18:06:41 +0200 Subject: [PATCH 254/344] Add emissions and fuel consumption from fuel refinieries (#1253) * Add emissions and fuel consumption from fuel refinieries * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- config/config.default.yaml | 3 ++ doc/configtables/industry.csv | 1 + doc/release_notes.rst | 3 ++ scripts/prepare_sector_network.py | 49 ++++++++++++++++++++++++++----- 4 files changed, 48 insertions(+), 8 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 2026c11fa..4290760e8 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -798,6 +798,7 @@ industry: MWh_MeOH_per_tMeOH: 5.528 hotmaps_locate_missing: false reference_year: 2019 + oil_refining_emissions: 0.013 # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#costs @@ -1078,6 +1079,8 @@ plotting: gas pipeline new: '#a87c62' # oil oil: '#c9c9c9' + oil primary: '#d2d2d2' + oil refining: '#e6e6e6' imported oil: '#a3a3a3' oil boiler: '#adadad' residential rural oil boiler: '#a9a9a9' diff --git a/doc/configtables/industry.csv b/doc/configtables/industry.csv index 4187e1183..fd80e8049 100644 --- a/doc/configtables/industry.csv +++ b/doc/configtables/industry.csv @@ -35,3 +35,4 @@ MWh_CH4_per_tMeOH,MWhCH4/tMeOH,float,"The energy amount of methane needed to pro MWh_MeOH_per_tMeOH,LHV,float,"The energy amount per ton of methanol. From `DECHEMA (2017) `_, page 74." hotmaps_locate_missing,--,"{true,false}",Locate industrial sites without valid locations based on city and countries. reference_year,year,YYYY,The year used as the baseline for industrial energy demand and production. Data extracted from `JRC-IDEES 2015 `_ +oil_refining_emissions,tCO2/MWh,float,"The emissions from oil fuel processing (e.g. oil in petrochemical refinieries). The default value of 0.013 tCO2/MWh is based on DE statistics for 2019; the EU value is very similar." diff --git a/doc/release_notes.rst b/doc/release_notes.rst index e912da2d8..0508252f2 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -11,6 +11,9 @@ Release Notes Upcoming Release ================ +* Added option to specify emissions fuel processing (e.g. oil in petrochemical + refinieries) with setting ``industry: oil_refining_emissions:``. + * Bugfix for passing function arguments in rule :mod:`solve_operations_network`. * Represent Kosovo (XK) as separate country. diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 9d099e073..5f579c5d9 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -573,14 +573,47 @@ def add_carrier_buses(n, carrier, nodes=None): fossils = ["coal", "gas", "oil", "lignite"] if options.get("fossil_fuels", True) and carrier in fossils: - n.madd( - "Generator", - nodes, - bus=nodes, - p_nom_extendable=True, - carrier=carrier, - marginal_cost=costs.at[carrier, "fuel"], - ) + suffix = "" + + if carrier == "oil" and cf_industry["oil_refining_emissions"] > 0: + + n.madd( + "Bus", + nodes + " primary", + location=location, + carrier=carrier + " primary", + unit=unit, + ) + + n.madd( + "Link", + nodes + " refining", + bus0=nodes + " primary", + bus1=nodes, + bus2="co2 atmosphere", + location=location, + carrier=carrier + " refining", + p_nom=1e6, + efficiency=1 + - ( + cf_industry["oil_refining_emissions"] + / costs.at[carrier, "CO2 intensity"] + ), + efficiency2=cf_industry["oil_refining_emissions"], + ) + + suffix = " primary" + + else: + + n.madd( + "Generator", + nodes + suffix, + bus=nodes + suffix, + p_nom_extendable=True, + carrier=carrier + suffix, + marginal_cost=costs.at[carrier, "fuel"], + ) # TODO: PyPSA-Eur merge issue From 0a1d58fa5590a9f975d403075907b015c9376425 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 30 Aug 2024 18:12:49 +0200 Subject: [PATCH 255/344] update version number, amend authors --- CITATION.cff | 16 +++++++++++++++- config/config.default.yaml | 2 +- doc/conf.py | 4 ++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index c921a773e..2caf4226f 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -6,7 +6,7 @@ cff-version: 1.1.0 message: "If you use this package, please cite it in the following way." title: "PyPSA-Eur: An open sector-coupled optimisation model of the European energy system" repository: https://github.com/pypsa/pypsa-eur -version: 0.11.0 +version: 0.12.0 license: MIT authors: - family-names: Brown @@ -36,3 +36,17 @@ authors: - family-names: Hörsch given-names: Jonas orcid: https://orcid.org/0000-0001-9438-767X + - family-names: Schledorn + given-names: Amos + - family-names: Schauß + given-names: Caspar + - family-names: van Greevenbroek + given-names: Koen + - family-names: Millinger + given-names: Markus + - family-names: Glaum + given-names: Philipp + - family-names: Xiong + given-names: Bobby + - family-names: Seibold + given-names: Toni diff --git a/config/config.default.yaml b/config/config.default.yaml index 00e5838eb..5e736e2e3 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -3,7 +3,7 @@ # SPDX-License-Identifier: CC0-1.0 # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#top-level-configuration -version: 0.11.0 +version: 0.12.0 tutorial: false logging: diff --git a/doc/conf.py b/doc/conf.py index f0d1ca377..bb929a467 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -92,9 +92,9 @@ # built documents. # # The short X.Y version. -version = "0.11" +version = "0.12" # The full version, including alpha/beta/rc tags. -release = "0.11.0" +release = "0.12.0" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From d2e75629625f7d90d5ddc2701eb30a45e055f65b Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 30 Aug 2024 18:16:52 +0200 Subject: [PATCH 256/344] fix version number in release notes --- doc/release_notes.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 4241ba09e..dd39fcbca 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -11,8 +11,8 @@ Release Notes .. Upcoming Release .. ================ -PyPSA-Eur 0.11.0 (25th May 2024) -================================ +PyPSA-Eur 0.12.0 (30th August 2024) +=================================== **Data Updates and Extensions** From 0bcb21559704accdc5900f536c4899489c132676 Mon Sep 17 00:00:00 2001 From: Tom Brown Date: Mon, 2 Sep 2024 19:32:13 +0200 Subject: [PATCH 257/344] use JRC-IDEES thermal energy service instead of FE for buildings This change consists of: - reading the final energy (FE) to thermal energy service (TES) efficiency for each each country, seperately for gas and oil (this efficiency represents e.g. the losses in older non-condensing boilers) - using TES instead of FE for the n.loads, so that it is pure energy service instead of including losses in legacy equipment - using the stored efficiencies for baseyear equipment in add_existing_baseyear In the baseyear (e.g. 2020) this should have no effect on FE, since the reduction to TES is conpensated by the lower efficiencies of existing equipment. In the future (e.g. 2050) this leads to a reduction in heating demand, since new equipment is more efficient than existing. --- rules/build_sector.smk | 2 + rules/solve_myopic.smk | 1 + scripts/add_existing_baseyear.py | 25 +++++++- scripts/build_energy_totals.py | 101 +++++++++++++++++++++++++++++- scripts/prepare_sector_network.py | 11 +++- 5 files changed, 135 insertions(+), 5 deletions(-) diff --git a/rules/build_sector.smk b/rules/build_sector.smk index f95731b78..ec7fcd0c3 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -305,6 +305,7 @@ rule build_energy_totals: co2_name=resources("co2_totals.csv"), transport_name=resources("transport_data.csv"), district_heat_share=resources("district_heat_share.csv"), + heating_efficiencies=resources("heating_efficiencies.csv"), threads: 16 resources: mem_mb=10000, @@ -1037,6 +1038,7 @@ rule prepare_sector_network: district_heat_share=resources( "district_heat_share_elec_s{simpl}_{clusters}_{planning_horizons}.csv" ), + heating_efficiencies=resources("heating_efficiencies.csv"), temp_soil_total=resources("temp_soil_total_elec_s{simpl}_{clusters}.nc"), temp_air_total=resources("temp_air_total_elec_s{simpl}_{clusters}.nc"), cop_profiles=resources("cop_profiles_elec_s{simpl}_{clusters}.nc"), diff --git a/rules/solve_myopic.smk b/rules/solve_myopic.smk index 8d7fa2841..f868a9973 100644 --- a/rules/solve_myopic.smk +++ b/rules/solve_myopic.smk @@ -26,6 +26,7 @@ rule add_existing_baseyear: existing_heating_distribution=resources( "existing_heating_distribution_elec_s{simpl}_{clusters}_{planning_horizons}.csv" ), + heating_efficiencies=resources("heating_efficiencies.csv"), output: RESULTS + "prenetworks-brownfield/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index 212ae8af5..7e9dd0826 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -546,6 +546,14 @@ def add_heating_capacities_installed_before_baseyear( lifetime=costs.at[heat_system.resistive_heater_costs_name, "lifetime"], ) + if "residential" in heat_system.value: + efficiency = nodes.str[:2].map(heating_efficiencies["gas residential space efficiency"]) + elif "services" in heat_system.value: + efficiency = nodes.str[:2].map(heating_efficiencies["gas services space efficiency"]) + else: + #default used for urban central, since no info on district heating boilers + efficiency = costs.at[heat_system.gas_boiler_costs_name, "efficiency"] + n.madd( "Link", nodes, @@ -554,7 +562,7 @@ def add_heating_capacities_installed_before_baseyear( bus1=nodes + " " + heat_system.value + " heat", bus2="co2 atmosphere", carrier=heat_system.value + " gas boiler", - efficiency=costs.at[heat_system.gas_boiler_costs_name, "efficiency"], + efficiency=efficiency, efficiency2=costs.at["gas", "CO2 intensity"], capital_cost=( costs.at[heat_system.gas_boiler_costs_name, "efficiency"] @@ -569,6 +577,14 @@ def add_heating_capacities_installed_before_baseyear( lifetime=costs.at[heat_system.gas_boiler_costs_name, "lifetime"], ) + if "residential" in heat_system.value: + efficiency = nodes.str[:2].map(heating_efficiencies["oil residential space efficiency"]) + elif "services" in heat_system.value: + efficiency = nodes.str[:2].map(heating_efficiencies["oil services space efficiency"]) + else: + #default used for urban central, since no info on district heating boilers + efficiency = costs.at[heat_system.oil_boiler_costs_name, "efficiency"] + n.madd( "Link", nodes, @@ -577,7 +593,7 @@ def add_heating_capacities_installed_before_baseyear( bus1=nodes + " " + heat_system.value + " heat", bus2="co2 atmosphere", carrier=heat_system.value + " oil boiler", - efficiency=costs.at[heat_system.oil_boiler_costs_name, "efficiency"], + efficiency=efficiency, efficiency2=costs.at["oil", "CO2 intensity"], capital_cost=costs.at[heat_system.oil_boiler_costs_name, "efficiency"] * costs.at[heat_system.oil_boiler_costs_name, "fixed"], @@ -660,6 +676,11 @@ def add_heating_capacities_installed_before_baseyear( if options["heating"]: + #one could use baseyear here instead (but dangerous if no data) + heating_efficiencies = ( + pd.read_csv(snakemake.input.heating_efficiencies, index_col=[0,1]) + ).swaplevel().loc[int(snakemake.config["energy"]["energy_totals_year"])] + add_heating_capacities_installed_before_baseyear( n=n, baseyear=baseyear, diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index 24ba86bbd..73c3d1a04 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -367,6 +367,24 @@ def idees_per_country(ct: str, base_dir: str) -> pd.DataFrame: assert df.index[43] == "Thermal uses" ct_totals["thermal uses residential"] = df.iloc[43] + df = pd.read_excel(fn_residential, "RES_hh_eff", index_col=0) + + ct_totals["total residential space efficiency"] = df.loc["Space heating"] + + assert df.index[5] == "Diesel oil" + ct_totals["oil residential space efficiency"] = df.iloc[5] + + assert df.index[6] == "Natural gas" + ct_totals["gas residential space efficiency"] = df.iloc[6] + + ct_totals["total residential water efficiency"] = df.loc["Water heating"] + + assert df.index[18] == "Diesel oil" + ct_totals["oil residential water efficiency"] = df.iloc[18] + + assert df.index[19] == "Natural gas" + ct_totals["gas residential water efficiency"] = df.iloc[19] + # services df = pd.read_excel(fn_tertiary, "SER_hh_fec", index_col=0) @@ -400,6 +418,24 @@ def idees_per_country(ct: str, base_dir: str) -> pd.DataFrame: assert df.index[46] == "Thermal uses" ct_totals["thermal uses services"] = df.iloc[46] + df = pd.read_excel(fn_tertiary, "SER_hh_eff", index_col=0) + + ct_totals["total services space efficiency"] = df.loc["Space heating"] + + assert df.index[5] == "Diesel oil" + ct_totals["oil services space efficiency"] = df.iloc[5] + + assert df.index[7] == "Conventional gas heaters" + ct_totals["gas services space efficiency"] = df.iloc[7] + + ct_totals["total services water efficiency"] = df.loc["Hot water"] + + assert df.index[20] == "Diesel oil" + ct_totals["oil services water efficiency"] = df.iloc[20] + + assert df.index[21] == "Natural gas" + ct_totals["gas services water efficiency"] = df.iloc[21] + # agriculture, forestry and fishing start = "Detailed split of energy consumption (ktoe)" @@ -576,7 +612,9 @@ def build_idees(countries: List[str]) -> pd.DataFrame: # efficiency kgoe/100km -> ktoe/100km so that after conversion TWh/100km totals.loc[:, "passenger car efficiency"] /= 1e6 # convert ktoe to TWh - exclude = totals.columns.str.fullmatch("passenger cars") + exclude = totals.columns.str.fullmatch("passenger cars") \ + ^ totals.columns.str.fullmatch(".*space efficiency") \ + ^ totals.columns.str.fullmatch(".*water efficiency") totals = totals.copy() totals.loc[:, ~exclude] *= 11.63 / 1e3 @@ -654,11 +692,16 @@ def build_energy_totals( eurostat_countries = eurostat.index.unique(0) eurostat_years = eurostat.index.unique(1) - to_drop = ["passenger cars", "passenger car efficiency"] new_index = pd.MultiIndex.from_product( [countries, eurostat_years], names=["country", "year"] ) + to_drop = idees.columns[idees.columns.str.contains("space efficiency") + ^ idees.columns.str.contains("water efficiency")] + to_drop = to_drop.append(pd.Index( + ["passenger cars", "passenger car efficiency"] + )) + df = idees.reindex(new_index).drop(to_drop, axis=1) in_eurostat = df.index.levels[0].intersection(eurostat_countries) @@ -1500,6 +1543,57 @@ def build_transformation_output_coke(eurostat, fn): df = eurostat.loc[slicer, :].droplevel(level=[2, 3, 4, 5]) df.to_csv(fn) +def build_heating_efficiencies( + countries: List[str], idees: pd.DataFrame +) -> pd.DataFrame: + """ + Build heating efficiencies for a set of countries based on IDEES data. + + Parameters + ---------- + countries : List[str] + List of country codes. + idees : pd.DataFrame + DataFrame with IDEES data. + + Returns + ------- + pd.DataFrame + DataFrame with heating efficiencies. + + + Notes + ----- + - It fills missing data with average data. + """ + + years = np.arange(2000, 2022) + + cols = idees.columns[idees.columns.str.contains("space efficiency") + ^ idees.columns.str.contains("water efficiency")] + + logger.info(cols) + + heating_efficiencies = pd.DataFrame(idees[cols]) + + new_index = pd.MultiIndex.from_product( + [countries, heating_efficiencies.index.unique(1)], + names=["country", "year"], + ) + + heating_efficiencies = heating_efficiencies.reindex(index=new_index) + + for col in cols: + unstacked = heating_efficiencies[col].unstack() + + fillvalue = unstacked.mean() + + for ct in unstacked.index: + mask = unstacked.loc[ct].isna() + unstacked.loc[ct, mask] = fillvalue[mask] + heating_efficiencies[col] = unstacked.stack() + + return heating_efficiencies # %% if __name__ == "__main__": @@ -1556,3 +1650,6 @@ def build_transformation_output_coke(eurostat, fn): transport = build_transport_data(countries, population, idees) transport.to_csv(snakemake.output.transport_name) + + heating_efficiencies = build_heating_efficiencies(countries, idees) + heating_efficiencies.to_csv(snakemake.output.heating_efficiencies) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 87aa90fa8..f303c10c1 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1806,9 +1806,14 @@ def build_heat_demand(n): for sector, use in product(sectors, uses): name = f"{sector} {use}" + #efficiency for final energy to thermal energy service + eff = pop_weighted_energy_totals.index.str[:2].map( + heating_efficiencies[f"total {sector} {use} efficiency"] + ) + heat_demand[name] = ( heat_demand_shape[name] / heat_demand_shape[name].sum() - ).multiply(pop_weighted_energy_totals[f"total {sector} {use}"]) * 1e6 + ).multiply(pop_weighted_energy_totals[f"total {sector} {use}"]*eff) * 1e6 electric_heat_supply[name] = ( heat_demand_shape[name] / heat_demand_shape[name].sum() ).multiply(pop_weighted_energy_totals[f"electricity {sector} {use}"]) * 1e6 @@ -4282,6 +4287,10 @@ def add_enhanced_geothermal(n, egs_potentials, egs_overlap, costs): ) pop_weighted_energy_totals.update(pop_weighted_heat_totals) + heating_efficiencies = ( + pd.read_csv(snakemake.input.heating_efficiencies, index_col=[0,1]) + ).swaplevel().loc[int(snakemake.config["energy"]["energy_totals_year"])] + patch_electricity_network(n) spatial = define_spatial(pop_layout.index, options) From ff943d87ada4584e7dc3fe22fa8edb8f7d63c6bb Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Tue, 3 Sep 2024 13:34:27 +0200 Subject: [PATCH 258/344] add missing snakemake input --- rules/solve_perfect.smk | 1 + 1 file changed, 1 insertion(+) diff --git a/rules/solve_perfect.smk b/rules/solve_perfect.smk index a06c6dfa1..4db4e31af 100644 --- a/rules/solve_perfect.smk +++ b/rules/solve_perfect.smk @@ -25,6 +25,7 @@ rule add_existing_baseyear: "existing_heating_distribution_elec_s{simpl}_{clusters}_{planning_horizons}.csv" ), existing_heating="data/existing_infrastructure/existing_heating_raw.csv", + heating_efficiencies=resources("heating_efficiencies.csv"), output: RESULTS + "prenetworks-brownfield/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", From 30bcddb66afb77eba43c842567164aa8688b627c Mon Sep 17 00:00:00 2001 From: Philipp Glaum Date: Tue, 3 Sep 2024 14:10:20 +0200 Subject: [PATCH 259/344] prepare_sector_networks: account correctely for msw co2 environment.yaml: add license comment --- envs/environment.fixed.yaml | 10 +++++++--- scripts/prepare_sector_network.py | 13 +++++++------ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/envs/environment.fixed.yaml b/envs/environment.fixed.yaml index d625a5b16..ac2d7eeea 100644 --- a/envs/environment.fixed.yaml +++ b/envs/environment.fixed.yaml @@ -1,6 +1,10 @@ +# SPDX-FileCopyrightText: : 2017-2024 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: CC0-1.0 + name: pypsa-eur-20240812 channels: - - http://conda.anaconda.org/gurobi - - conda-forge - - defaults +- http://conda.anaconda.org/gurobi +- conda-forge +- defaults prefix: /home/fneum/miniconda3/envs/pypsa-eur-20240812 diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 5f579c5d9..89175b1f9 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2377,12 +2377,9 @@ def add_biomass(n, costs): carrier="municipal solid waste", ) - e_max_pu = np.array( - len(spatial.msw.nodes) * [[1] * (len(n.snapshots) - 1) + [0]] - ).T - e_max_pu = pd.DataFrame( - e_max_pu, index=n.snapshots, columns=spatial.msw.nodes - ).astype(float) + e_max_pu = pd.DataFrame(1, index=n.snapshots, columns=spatial.msw.nodes) + e_max_pu.iloc[-1] = 0 + n.madd( "Store", spatial.msw.nodes, @@ -3383,9 +3380,13 @@ def add_industry(n, costs): spatial.msw.locations, bus0=spatial.msw.nodes, bus1=non_sequestered_hvc_locations, + bus2="co2 atmosphere", carrier="municipal solid waste", p_nom_extendable=True, efficiency=1.0, + efficiency2=-costs.at[ + "oil", "CO2 intensity" + ], # because msw is co2 neutral and will be burned in waste CHP or decomposed as oil ) n.madd( From 5e5944df57f1d2b412b7e7eb8d77b5004b97bea6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 3 Sep 2024 12:12:20 +0000 Subject: [PATCH 260/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- doc/release_notes.rst | 4 ++-- doc/tutorial.rst | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index dd39fcbca..2a009fe56 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -100,7 +100,7 @@ PyPSA-Eur 0.12.0 (30th August 2024) share_unsustainable_use_retained`` and ``biomass: share_sustainable_potential_available``. (https://github.com/PyPSA/pypsa-eur/pull/1139) - + * Added energy penalty for BECC applications. (https://github.com/PyPSA/pypsa-eur/pull/1130) @@ -258,7 +258,7 @@ PyPSA-Eur 0.12.0 (30th August 2024) :mod:`prepare_sector_network`. (https://github.com/PyPSA/pypsa-eur/pull/1106) * Fixed PDF encoding in ``build_biomass_transport_costs``. - (https://github.com/PyPSA/pypsa-eur/pull/1219) + (https://github.com/PyPSA/pypsa-eur/pull/1219) * Dropped ``pycountry`` dependency in favour of ``country_converter``. (https://github.com/PyPSA/pypsa-eur/pull/1188) diff --git a/doc/tutorial.rst b/doc/tutorial.rst index 73b4df8b8..f514491e2 100644 --- a/doc/tutorial.rst +++ b/doc/tutorial.rst @@ -224,7 +224,7 @@ This triggers a workflow of multiple preceding jobs that depend on each rule's i 7 -> 24 26 -> 25 27 -> 25 - } + } | From 7b98afc85aafd56245ce669873d0c9822c6762c0 Mon Sep 17 00:00:00 2001 From: Philipp Glaum Date: Tue, 3 Sep 2024 16:02:28 +0200 Subject: [PATCH 261/344] fix if condition to add msw --- scripts/prepare_sector_network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 89175b1f9..a83e2d124 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2357,7 +2357,7 @@ def add_biomass(n, costs): if ( options["municipal_solid_waste"] and not options["industry"] - and (cf_industry["waste_to_energy"] or cf_industry["waste_to_energy_cc"]) + and not (cf_industry["waste_to_energy"] or cf_industry["waste_to_energy_cc"]) ): logger.warning( "Flag municipal_solid_waste can be only used with industry " From 34f5d2eb68f4dd35dcb77a7e813e3af91685b406 Mon Sep 17 00:00:00 2001 From: Michael Lindner Date: Tue, 3 Sep 2024 16:04:00 +0200 Subject: [PATCH 262/344] bugfix missing oil generator: remove else clause --- scripts/prepare_sector_network.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 5f579c5d9..6c0fc6c3d 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -604,16 +604,14 @@ def add_carrier_buses(n, carrier, nodes=None): suffix = " primary" - else: - - n.madd( - "Generator", - nodes + suffix, - bus=nodes + suffix, - p_nom_extendable=True, - carrier=carrier + suffix, - marginal_cost=costs.at[carrier, "fuel"], - ) + n.madd( + "Generator", + nodes + suffix, + bus=nodes + suffix, + p_nom_extendable=True, + carrier=carrier + suffix, + marginal_cost=costs.at[carrier, "fuel"], + ) # TODO: PyPSA-Eur merge issue From 0b30aef50be43fc8c7c678334b647fcf6fa70062 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 3 Sep 2024 14:38:25 +0000 Subject: [PATCH 263/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- doc/release_notes.rst | 4 ++-- doc/tutorial.rst | 2 +- envs/environment.fixed.yaml | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index dd39fcbca..2a009fe56 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -100,7 +100,7 @@ PyPSA-Eur 0.12.0 (30th August 2024) share_unsustainable_use_retained`` and ``biomass: share_sustainable_potential_available``. (https://github.com/PyPSA/pypsa-eur/pull/1139) - + * Added energy penalty for BECC applications. (https://github.com/PyPSA/pypsa-eur/pull/1130) @@ -258,7 +258,7 @@ PyPSA-Eur 0.12.0 (30th August 2024) :mod:`prepare_sector_network`. (https://github.com/PyPSA/pypsa-eur/pull/1106) * Fixed PDF encoding in ``build_biomass_transport_costs``. - (https://github.com/PyPSA/pypsa-eur/pull/1219) + (https://github.com/PyPSA/pypsa-eur/pull/1219) * Dropped ``pycountry`` dependency in favour of ``country_converter``. (https://github.com/PyPSA/pypsa-eur/pull/1188) diff --git a/doc/tutorial.rst b/doc/tutorial.rst index 73b4df8b8..f514491e2 100644 --- a/doc/tutorial.rst +++ b/doc/tutorial.rst @@ -224,7 +224,7 @@ This triggers a workflow of multiple preceding jobs that depend on each rule's i 7 -> 24 26 -> 25 27 -> 25 - } + } | diff --git a/envs/environment.fixed.yaml b/envs/environment.fixed.yaml index d625a5b16..df58a5ae9 100644 --- a/envs/environment.fixed.yaml +++ b/envs/environment.fixed.yaml @@ -1,6 +1,6 @@ name: pypsa-eur-20240812 channels: - - http://conda.anaconda.org/gurobi - - conda-forge - - defaults +- http://conda.anaconda.org/gurobi +- conda-forge +- defaults prefix: /home/fneum/miniconda3/envs/pypsa-eur-20240812 From f100504a1d25d1c49c90f2f987bf1541701f7b77 Mon Sep 17 00:00:00 2001 From: Michael Lindner Date: Tue, 3 Sep 2024 16:40:21 +0200 Subject: [PATCH 264/344] add release-note --- doc/release_notes.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 2a009fe56..325462adc 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -11,6 +11,8 @@ Release Notes .. Upcoming Release .. ================ +* bugfix: The oil generator was incorrectly dropped when the config `oil_refining_emissions` was greater than zero. This was the default behaviour in 0.12.0. + PyPSA-Eur 0.12.0 (30th August 2024) =================================== From 9f0e46860c3e92d99d6527dedb8e2954b0b20cff Mon Sep 17 00:00:00 2001 From: Micha Date: Sun, 8 Sep 2024 11:46:09 +0200 Subject: [PATCH 265/344] bugfix: determine bus_carrier locally instead of overwriting n.stores (#1262) * bugfix: determine bus_carrier locally instead of overwriting n.stores * add release_note * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- doc/release_notes.rst | 2 ++ scripts/solve_network.py | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 325462adc..f8460e14d 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -11,6 +11,8 @@ Release Notes .. Upcoming Release .. ================ +* bugfix: The carrier of stores was silently overwritten by their bus_carrier as a side effect when building the co2 constraints + * bugfix: The oil generator was incorrectly dropped when the config `oil_refining_emissions` was greater than zero. This was the default behaviour in 0.12.0. PyPSA-Eur 0.12.0 (30th August 2024) diff --git a/scripts/solve_network.py b/scripts/solve_network.py index c70d4d3c8..bdc10dd18 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -330,8 +330,8 @@ def add_carbon_constraint(n, snapshots): continue # stores - n.stores["carrier"] = n.stores.bus.map(n.buses.carrier) - stores = n.stores.query("carrier in @emissions.index and not e_cyclic") + bus_carrier = n.stores.bus.map(n.buses.carrier) + stores = n.stores[bus_carrier.isin(emissions.index) & ~n.stores.e_cyclic] if not stores.empty: last = n.snapshot_weightings.reset_index().groupby("period").last() last_i = last.set_index([last.index, last.timestep]).index @@ -356,8 +356,8 @@ def add_carbon_budget_constraint(n, snapshots): continue # stores - n.stores["carrier"] = n.stores.bus.map(n.buses.carrier) - stores = n.stores.query("carrier in @emissions.index and not e_cyclic") + bus_carrier = n.stores.bus.map(n.buses.carrier) + stores = n.stores[bus_carrier.isin(emissions.index) & ~n.stores.e_cyclic] if not stores.empty: last = n.snapshot_weightings.reset_index().groupby("period").last() last_i = last.set_index([last.index, last.timestep]).index @@ -1000,8 +1000,8 @@ def add_co2_atmosphere_constraint(n, snapshots): continue # stores - n.stores["carrier"] = n.stores.bus.map(n.buses.carrier) - stores = n.stores.query("carrier in @emissions.index and not e_cyclic") + bus_carrier = n.stores.bus.map(n.buses.carrier) + stores = n.stores[bus_carrier.isin(emissions.index) & ~n.stores.e_cyclic] if not stores.empty: last_i = snapshots[-1] lhs = n.model["Store-e"].loc[last_i, stores.index] From d02984c5a91b26281251e3826e33ed14bbad18ae Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Sun, 8 Sep 2024 11:48:13 +0200 Subject: [PATCH 266/344] add color for unsustainable solid biomass --- config/config.default.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/config.default.yaml b/config/config.default.yaml index 5e736e2e3..5438b2495 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -1109,6 +1109,7 @@ plotting: biogas: '#e3d37d' biomass: '#baa741' solid biomass: '#baa741' + unsustainable solid biomass: '#bab000' municipal solid waste: '#91ba41' solid biomass import: '#d5ca8d' solid biomass transport: '#baa741' From 4a3e7924d2b3c9d5a432eb2e382fa8c0ed9c1261 Mon Sep 17 00:00:00 2001 From: Amos Schledorn <60692940+amos-schledorn@users.noreply.github.com> Date: Sun, 8 Sep 2024 11:50:25 +0200 Subject: [PATCH 267/344] Make overdimensioning factor for heating systems specific to central/decentral heating (#1259) * chore: make overdim_factor heat-system-specific * doc: update configtables * doc: update release notes * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix: read overdimensioning factor for industrial oil boilers from options * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: Amos Schledorn Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Fabian Neumann --- config/config.default.yaml | 4 +++- doc/configtables/sector.csv | 4 +++- doc/release_notes.rst | 2 ++ scripts/prepare_sector_network.py | 13 +++++++++---- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 5438b2495..54701daf5 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -590,7 +590,9 @@ sector: resistive_heaters: true oil_boilers: false biomass_boiler: true - overdimension_individual_heating: 1.1 #to cover demand peaks bigger than data + overdimension_heat_generators: + decentral: 1.1 #to cover demand peaks bigger than data + central: 1.0 chp: true micro_chp: false solar_thermal: true diff --git a/doc/configtables/sector.csv b/doc/configtables/sector.csv index 044c8dc40..7de2a173d 100644 --- a/doc/configtables/sector.csv +++ b/doc/configtables/sector.csv @@ -89,7 +89,9 @@ boilers,--,"{true, false}",Add option for transforming gas into heat using gas b resistive_heaters,--,"{true, false}",Add option for transforming electricity into heat using resistive heaters (independently from gas boilers) oil_boilers,--,"{true, false}",Add option for transforming oil into heat using boilers biomass_boiler,--,"{true, false}",Add option for transforming biomass into heat using boilers -overdimension_individual_heating,--,float,Add option for overdimensioning individual heating systems by a certain factor. This allows them to cover heat demand peaks e.g. 10% higher than those in the data with a setting of 1.1. +overdimension_heat_generators,,,Add option for overdimensioning heating systems by a certain factor. This allows them to cover heat demand peaks e.g. 10% higher than those in the data with a setting of 1.1. +-- decentral,--,float,The factor for overdimensioning (increasing CAPEX) decentral heating systems +-- central,--,float,The factor for overdimensioning (increasing CAPEX) central heating systems chp,--,"{true, false}",Add option for using Combined Heat and Power (CHP) micro_chp,--,"{true, false}",Add option for using Combined Heat and Power (CHP) for decentral areas. solar_thermal,--,"{true, false}",Add option for using solar thermal to generate heat. diff --git a/doc/release_notes.rst b/doc/release_notes.rst index f8460e14d..32c9eed70 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -11,6 +11,8 @@ Release Notes .. Upcoming Release .. ================ +* Made the overdimensioning factor for heating systems specific for central/decentral heating, defaults to no overdimensionining for central heating and no changes to decentral heating compared to previous version. + * bugfix: The carrier of stores was silently overwritten by their bus_carrier as a side effect when building the co2 constraints * bugfix: The oil generator was incorrectly dropped when the config `oil_refining_emissions` was greater than zero. This was the default behaviour in 0.12.0. diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index fcf41d88f..9242fbf3f 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1875,8 +1875,6 @@ def add_heat(n: pypsa.Network, costs: pd.DataFrame, cop: xr.DataArray): heat_demand = build_heat_demand(n) - overdim_factor = options["overdimension_individual_heating"] - district_heat_info = pd.read_csv(snakemake.input.district_heat_share, index_col=0) dist_fraction = district_heat_info["district fraction of node"] urban_fraction = district_heat_info["urban fraction"] @@ -1905,6 +1903,9 @@ def add_heat(n: pypsa.Network, costs: pd.DataFrame, cop: xr.DataArray): HeatSystem ): # this loops through all heat systems defined in _entities.HeatSystem + overdim_factor = options["overdimension_heat_generators"][ + heat_system.central_or_decentral + ] if heat_system == HeatSystem.URBAN_CENTRAL: nodes = dist_fraction.index[dist_fraction > 0] else: @@ -2753,7 +2754,9 @@ def add_biomass(n, costs): efficiency=costs.at["biomass boiler", "efficiency"], capital_cost=costs.at["biomass boiler", "efficiency"] * costs.at["biomass boiler", "fixed"] - * options["overdimension_individual_heating"], + * options["overdimension_heat_generators"][ + HeatSystem(name).central_or_decentral + ], marginal_cost=costs.at["biomass boiler", "pelletizing cost"], lifetime=costs.at["biomass boiler", "lifetime"], ) @@ -3275,7 +3278,9 @@ def add_industry(n, costs): efficiency2=costs.at["oil", "CO2 intensity"], capital_cost=costs.at["decentral oil boiler", "efficiency"] * costs.at["decentral oil boiler", "fixed"] - * options["overdimension_individual_heating"], + * options["overdimension_heat_generators"][ + heat_system.central_or_decentral + ], lifetime=costs.at["decentral oil boiler", "lifetime"], ) From a7bb47886597a2fbe21dc722546f987487e78dc2 Mon Sep 17 00:00:00 2001 From: Philipp Glaum <95913147+p-glaum@users.noreply.github.com> Date: Sun, 8 Sep 2024 11:51:52 +0200 Subject: [PATCH 268/344] update nep links to include "Startnetz" ("start network") links (#1263) --- data/transmission_projects/nep/new_links.csv | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/data/transmission_projects/nep/new_links.csv b/data/transmission_projects/nep/new_links.csv index d237893f1..7a551f36c 100644 --- a/data/transmission_projects/nep/new_links.csv +++ b/data/transmission_projects/nep/new_links.csv @@ -10,3 +10,9 @@ DC40plus,Dörpen/West,Klostermansfeld/Sachsen-Anhalt,2000.0,426,confirmed,2037,T DC41,Alfstedt/Niedersachsen,Hüffenhardt/Baden-Württemberg,2000.0,607,confirmed,2037,True,"{""url"":""https://data.netzausbau.de/2037-2023/NEP/NEP_2037_2045_Bestaetigung.pdf"", ""status"":""confirmed"", ""origin"":""NEP"",""version"":2024,""location0"":""Alfstedt/Niedersachsen"",""location1"":""Hüffenhardt/Baden-Württemberg""",9.0666915,53.5499782,9.0827453,49.2908862 DC42,Sahms/Schleswig-Holstein,Böblingen/Baden-Württemberg,2000.0,737,confirmed,2037,True,"{""url"":""https://data.netzausbau.de/2037-2023/NEP/NEP_2037_2045_Bestaetigung.pdf"", ""status"":""confirmed"", ""origin"":""NEP"",""version"":2024,""location0"":""Sahms/Schleswig-Holstein"",""location1"":""Böblingen/Baden-Württemberg""",10.5331297,53.5252973,9.0113444,48.684969 DC42plus,Sahms/Schleswig-Holstein,Trennfeld/Bayern,2000.0,546,confirmed,2037,True,"{""url"":""https://data.netzausbau.de/2037-2023/NEP/NEP_2037_2045_Bestaetigung.pdf"", ""status"":""confirmed"", ""origin"":""NEP"",""version"":2024,""location0"":""Sahms/Schleswig-Holstein"",""location1"":""Trennfeld/Bayern""",10.5331297,53.5252973,9.6151453,49.7950676 +DC1,Emden Ost,Osterath,2000,297,in_permitting,2027,True,"{url:https://www.netzentwicklungsplan.de/sites/default/files/2024-04/NEP_2037_2045_V2023_Anhang_2E_Aktualisierung_April_2024_%28komprimiert%29.pdf,TYNDP 2022:132,""location0"":""Emden Ost"",""location1"":""Osterath""",7.2492328,53.3655941,6.6207151,51.2690922 +DC2,Osterath,Philippsburg,2000,299,in_permitting,2026,False,"{url:https://www.netzentwicklungsplan.de/sites/default/files/2024-04/NEP_2037_2045_V2023_Anhang_2E_Aktualisierung_April_2024_%28komprimiert%29.pdf,TYNDP 2022: 254,""location0"":""Osterath"",""location1"":""Philippsburg""",6.6207151,51.2690922,7.5514555,48.9953996 +DC3,Brunsbüttel,Großgartach,2000,694,in_permitting,2028,True,"{url:https://www.netzentwicklungsplan.de/sites/default/files/2024-04/NEP_2037_2045_V2023_Anhang_2E_Aktualisierung_April_2024_%28komprimiert%29.pdf,TYNDP 2022: 235,""location0"":""Brunsbüttel"",""location1"":""Großgartach""",9.1395423,53.8972549,9.1258701,49.1425406 +DC4,Wilster ,Bergrheinfeld,2000,540,in_permitting,2028,True,"{url:https://www.netzentwicklungsplan.de/sites/default/files/2024-04/NEP_2037_2045_V2023_Anhang_2E_Aktualisierung_April_2024_%28komprimiert%29.pdf,TYNDP 2022: 235,""location0"":""Wilster "",""location1"":""Bergrheinfeld""",9.3741366,53.9233519,10.1810033,50.0082136 +DC5,Wolmirstedt,Isar/Bayern,2000,539,in_permitting,2027,True,"{url:https://www.netzentwicklungsplan.de/sites/default/files/2024-04/NEP_2037_2045_V2023_Anhang_2E_Aktualisierung_April_2024_%28komprimiert%29.pdf,TYNDP 2022: 130, name:SuedOstLink,""location0"":""Wolmirstedt"",""location1"":""Isar/Bayern""",11.6267388,52.2484924,11.5745421,47.6691308 +DC20,Klein Rogahn,Isar/Bayern,2000,759,in_permitting,2030,False,"{url:https://www.netzentwicklungsplan.de/sites/default/files/2024-04/NEP_2037_2045_V2023_Anhang_2E_Aktualisierung_April_2024_%28komprimiert%29.pdf, name:SuedOstLink+,""location0"":""Klein Rogahn"",""location1"":""Isar/Bayern""",11.3482229,53.6044461,11.5745421,47.6691308 From 53f84f7eea8013b233c9e842e1d9f2b012810fb1 Mon Sep 17 00:00:00 2001 From: cpschau <124347782+cpschau@users.noreply.github.com> Date: Sun, 8 Sep 2024 11:59:37 +0200 Subject: [PATCH 269/344] Follow-Up: Unsustainable Biomass (#1254) * add oil bus; extend global biomass limit; set unsustainable potentials to zero for overnight * keep sustainable potentials for switzerland * remove potential distinction for overnight; additional global constraint for unsustainable solid biomass * add unsustainable generator suffix * added unsustainable solid biomass bus; changed global constraints to equality; removed forced emptying of unsustainable solid biomass store * restored equality constraint for sustainable solid biomass; restored forced emptying of unsustainable store * remove directory change for debugger * added tech_color for unsustainable solid biomass * modified spatial namespaces and removed suffixes --- config/config.default.yaml | 1 + rules/build_sector.smk | 0 scripts/build_biomass_potentials.py | 6 +-- scripts/prepare_sector_network.py | 57 +++++++++++++++++++++++++---- 4 files changed, 54 insertions(+), 10 deletions(-) mode change 100644 => 100755 rules/build_sector.smk mode change 100644 => 100755 scripts/build_biomass_potentials.py mode change 100644 => 100755 scripts/prepare_sector_network.py diff --git a/config/config.default.yaml b/config/config.default.yaml index 54701daf5..1a7f9e57a 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -1127,6 +1127,7 @@ plotting: services rural biomass boiler: '#c6cf98' services urban decentral biomass boiler: '#dde5b5' biomass to liquid: '#32CD32' + unsustainable solid biomass: '#998622' unsustainable bioliquids: '#32CD32' electrobiofuels: 'red' BioSNG: '#123456' diff --git a/rules/build_sector.smk b/rules/build_sector.smk old mode 100644 new mode 100755 diff --git a/scripts/build_biomass_potentials.py b/scripts/build_biomass_potentials.py old mode 100644 new mode 100755 index 4c7752e4b..a3c51292c --- a/scripts/build_biomass_potentials.py +++ b/scripts/build_biomass_potentials.py @@ -330,7 +330,7 @@ def add_unsustainable_potentials(df): ) share_sus = params.get("share_sustainable_potential_available").get(investment_year) - df *= share_sus + df.loc[df_wo_ch.index] *= share_sus df = df.join(df_wo_ch.filter(like="unsustainable")).fillna(0) @@ -345,8 +345,8 @@ def add_unsustainable_potentials(df): snakemake = mock_snakemake( "build_biomass_potentials", simpl="", - clusters="37", - planning_horizons=2020, + clusters="38", + planning_horizons=2050, ) configure_logging(snakemake) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py old mode 100644 new mode 100755 index 9242fbf3f..8748b64fa --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -63,7 +63,8 @@ def define_spatial(nodes, options): if options.get("biomass_spatial", options["biomass_transport"]): spatial.biomass.nodes = nodes + " solid biomass" - spatial.biomass.bioliquids = nodes + " bioliquids" + spatial.biomass.nodes_unsustainable = nodes + " unsustainable solid biomass" + spatial.biomass.bioliquids = nodes + " unsustainable bioliquids" spatial.biomass.locations = nodes spatial.biomass.industry = nodes + " solid biomass for industry" spatial.biomass.industry_cc = nodes + " solid biomass for industry CC" @@ -71,6 +72,7 @@ def define_spatial(nodes, options): spatial.msw.locations = nodes else: spatial.biomass.nodes = ["EU solid biomass"] + spatial.biomass.nodes_unsustainable = ["EU unsustainable solid biomass"] spatial.biomass.bioliquids = ["EU unsustainable bioliquids"] spatial.biomass.locations = ["EU"] spatial.biomass.industry = ["solid biomass for industry"] @@ -2493,14 +2495,21 @@ def add_biomass(n, costs): e_max_pu=e_max_pu, ) + n.madd( + "Bus", + spatial.biomass.nodes_unsustainable, + location=spatial.biomass.locations, + carrier="unsustainable solid biomass", + unit="MWh_LHV", + ) + e_max_pu = pd.DataFrame(1, index=n.snapshots, columns=spatial.biomass.nodes) e_max_pu.iloc[-1] = 0 n.madd( "Store", - spatial.biomass.nodes, - suffix=" unsustainable", - bus=spatial.biomass.nodes, + spatial.biomass.nodes_unsustainable, + bus=spatial.biomass.nodes_unsustainable, carrier="unsustainable solid biomass", e_nom=unsustainable_solid_biomass_potentials_spatial, marginal_cost=costs.at["fuelwood", "fuel"], @@ -2509,6 +2518,16 @@ def add_biomass(n, costs): e_max_pu=e_max_pu, ) + n.madd( + "Link", + spatial.biomass.nodes_unsustainable, + bus0=spatial.biomass.nodes_unsustainable, + bus1=spatial.biomass.nodes, + carrier="unsustainable solid biomass", + efficiency=1, + p_nom=unsustainable_solid_biomass_potentials_spatial, + ) + n.madd( "Bus", spatial.biomass.bioliquids, @@ -2525,7 +2544,6 @@ def add_biomass(n, costs): n.madd( "Store", spatial.biomass.bioliquids, - suffix=" unsustainable", bus=spatial.biomass.bioliquids, carrier="unsustainable bioliquids", e_nom=unsustainable_liquid_biofuel_potentials_spatial, @@ -2535,6 +2553,8 @@ def add_biomass(n, costs): e_max_pu=e_max_pu, ) + add_carrier_buses(n, "oil") + n.madd( "Link", spatial.biomass.bioliquids, @@ -2663,6 +2683,29 @@ def add_biomass(n, costs): constant=biomass_potentials["solid biomass"].sum(), type="operational_limit", ) + if biomass_potentials["unsustainable solid biomass"].sum() > 0: + n.madd( + "Generator", + spatial.biomass.nodes_unsustainable, + bus=spatial.biomass.nodes_unsustainable, + carrier="unsustainable solid biomass", + p_nom=10000, + marginal_cost=costs.at["fuelwood", "fuel"] + + bus_transport_costs * average_distance, + ) + # Set last snapshot of e_max_pu for unsustainable solid biomass to 1 to make operational limit work + unsus_stores_idx = n.stores.loc[ + n.stores.carrier == "unsustainable solid biomass" + ].index + n.stores_t.e_max_pu.loc[n.snapshots[-1], unsus_stores_idx] = 1 + n.add( + "GlobalConstraint", + "unsustainable biomass limit", + carrier_attribute="unsustainable solid biomass", + sense="==", + constant=biomass_potentials["unsustainable solid biomass"].sum(), + type="operational_limit", + ) if options["municipal_solid_waste"]: # Add municipal solid waste @@ -4285,10 +4328,10 @@ def add_enhanced_geothermal(n, egs_potentials, egs_overlap, costs): "prepare_sector_network", simpl="", opts="", - clusters="37", + clusters="38", ll="vopt", sector_opts="", - planning_horizons="2050", + planning_horizons="2030", ) configure_logging(snakemake) From 2eaa316a6bd876a94a5cc7026bea7179932f1641 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Sun, 8 Sep 2024 12:00:19 +0200 Subject: [PATCH 270/344] remove duplicated color for unsustainable solid biomass --- config/config.default.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 1a7f9e57a..d9ed959e5 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -1111,7 +1111,6 @@ plotting: biogas: '#e3d37d' biomass: '#baa741' solid biomass: '#baa741' - unsustainable solid biomass: '#bab000' municipal solid waste: '#91ba41' solid biomass import: '#d5ca8d' solid biomass transport: '#baa741' From a69373b96cb71ce5a3eda5283c19e664401d947c Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Sun, 8 Sep 2024 13:00:15 +0200 Subject: [PATCH 271/344] mirror global energy monitor datasets (temporary) (#1265) --- rules/retrieve.smk | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/rules/retrieve.smk b/rules/retrieve.smk index c0cd5b6c2..67b91b998 100644 --- a/rules/retrieve.smk +++ b/rules/retrieve.smk @@ -312,10 +312,9 @@ if config["enable"]["retrieve"]: run: import requests - response = requests.get( - "https://globalenergymonitor.org/wp-content/uploads/2024/05/Europe-Gas-Tracker-2024-05.xlsx", - headers={"User-Agent": "Mozilla/5.0"}, - ) + # mirror of https://globalenergymonitor.org/wp-content/uploads/2024/05/Europe-Gas-Tracker-2024-05.xlsx + url = "https://tubcloud.tu-berlin.de/s/LMBJQCsN6Ez5cN2/download/Europe-Gas-Tracker-2024-05.xlsx" + response = requests.get(url) with open(output[0], "wb") as f: f.write(response.content) @@ -329,10 +328,9 @@ if config["enable"]["retrieve"]: run: import requests - response = requests.get( - "https://globalenergymonitor.org/wp-content/uploads/2024/04/Global-Steel-Plant-Tracker-April-2024-Standard-Copy-V1.xlsx", - headers={"User-Agent": "Mozilla/5.0"}, - ) + # mirror or https://globalenergymonitor.org/wp-content/uploads/2024/04/Global-Steel-Plant-Tracker-April-2024-Standard-Copy-V1.xlsx + url = "https://tubcloud.tu-berlin.de/s/Aqebo3rrQZWKGsG/download/Global-Steel-Plant-Tracker-April-2024-Standard-Copy-V1.xlsx" + response = requests.get(url) with open(output[0], "wb") as f: f.write(response.content) From 0423366db60eacf050539f5baf74ff09294e2cf4 Mon Sep 17 00:00:00 2001 From: Fabian Hofmann Date: Mon, 9 Sep 2024 14:14:16 +0200 Subject: [PATCH 272/344] prepare_sector_network: fix e_max_pu in unsustainable biomass stores (#1266) --- scripts/prepare_sector_network.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 8748b64fa..7f7448740 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2503,7 +2503,9 @@ def add_biomass(n, costs): unit="MWh_LHV", ) - e_max_pu = pd.DataFrame(1, index=n.snapshots, columns=spatial.biomass.nodes) + e_max_pu = pd.DataFrame( + 1, index=n.snapshots, columns=spatial.biomass.nodes_unsustainable + ) e_max_pu.iloc[-1] = 0 n.madd( @@ -2694,9 +2696,12 @@ def add_biomass(n, costs): + bus_transport_costs * average_distance, ) # Set last snapshot of e_max_pu for unsustainable solid biomass to 1 to make operational limit work - unsus_stores_idx = n.stores.loc[ - n.stores.carrier == "unsustainable solid biomass" - ].index + unsus_stores_idx = n.stores.query( + "carrier == 'unsustainable solid biomass'" + ).index + unsus_stores_idx = unsus_stores_idx.intersection( + n.stores_t.e_max_pu.columns + ) n.stores_t.e_max_pu.loc[n.snapshots[-1], unsus_stores_idx] = 1 n.add( "GlobalConstraint", From 5cf706fe4d043dc0e3ae22796216a811a9355878 Mon Sep 17 00:00:00 2001 From: Amos Schledorn <60692940+amos-schledorn@users.noreply.github.com> Date: Mon, 9 Sep 2024 16:38:39 +0200 Subject: [PATCH 273/344] Update central heating temperatures based on Euroheat data and AGFW-Hauptbericht (#1264) * chore: update config.default Using Euroheat market outlook 2024 and AGFW-Hauptbericht * feat: extrapolate missing values in central_heating_temperature_profile.run * update release notes * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix: map_temperature_dict_to_onshore regions and correct use of extrapolation function * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix: extrapolation_ration in build_central_heating_temperature_profiles.run * style: remove obsolete time index in min/max fwd, return temperatures * feat: throw exception if max_fwd_temp < min_fwd_temp or min_fwd_temp < return_temp * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: Amos Schledorn Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- config/config.default.yaml | 23 ++--- doc/release_notes.rst | 2 +- ...entral_heating_temperature_approximator.py | 9 ++ .../run.py | 95 ++++++++++++++----- 4 files changed, 90 insertions(+), 39 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index d9ed959e5..63cdcabff 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -451,21 +451,18 @@ sector: district_heating_loss: 0.15 supply_temperature_approximation: max_forward_temperature: - default: 90 - DK: 70 - SE: 70 - NO: 70 + FR: 110 + DK: 75 + DE: 109 + CZ: 130 + FI: 115 + PL: 130 + SE: 102 + IT: 90 min_forward_temperature: - default: 68 - DK: 54 - SE: 54 - NO: 54 + DE: 82 return_temperature: - default: 50 - DK: 40 - SE: 40 - NO: 40 - FI: 40 + DE: 58 lower_threshold_ambient_temperature: 0 upper_threshold_ambient_temperature: 10 rolling_window_ambient_temperature: 72 diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 32c9eed70..97db4bafc 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -9,8 +9,8 @@ Release Notes ########################################## .. Upcoming Release -.. ================ +* Updated district heating supply temperatures based on `Euroheat's DHC Market Outlook 2024`__ and `AGFW-Hauptbericht 2022 `__. `min_forward_temperature` and `return_temperature` (not given by Euroheat) are extrapolated based on German values. * Made the overdimensioning factor for heating systems specific for central/decentral heating, defaults to no overdimensionining for central heating and no changes to decentral heating compared to previous version. * bugfix: The carrier of stores was silently overwritten by their bus_carrier as a side effect when building the co2 constraints diff --git a/scripts/build_central_heating_temperature_profiles/central_heating_temperature_approximator.py b/scripts/build_central_heating_temperature_profiles/central_heating_temperature_approximator.py index 5b467824d..67f2c0196 100644 --- a/scripts/build_central_heating_temperature_profiles/central_heating_temperature_approximator.py +++ b/scripts/build_central_heating_temperature_profiles/central_heating_temperature_approximator.py @@ -58,6 +58,15 @@ def __init__( rolling_window_ambient_temperature : int Rolling window size for averaging ambient temperature. """ + + if any(max_forward_temperature < min_forward_temperature): + raise ValueError( + "max_forward_temperature must be greater than min_forward_temperature" + ) + if any(min_forward_temperature < fixed_return_temperature): + raise ValueError( + "min_forward_temperature must be greater than fixed_return_temperature" + ) self._ambient_temperature = ambient_temperature self.max_forward_temperature = max_forward_temperature self.min_forward_temperature = min_forward_temperature diff --git a/scripts/build_central_heating_temperature_profiles/run.py b/scripts/build_central_heating_temperature_profiles/run.py index 115293e4c..feb4ab4f5 100644 --- a/scripts/build_central_heating_temperature_profiles/run.py +++ b/scripts/build_central_heating_temperature_profiles/run.py @@ -9,8 +9,8 @@ temperature is assumed and vice versa for temperatures above 10C. Between these threshold levels, forward temperatures are linearly interpolated. -By default, temperature levels are increased for non-Scandinavian countries. -The default ratios between min. and max. forward temperatures is based on AGFW-Hauptbericht 2022. +By default, `max_forward_temperature` from Euroheat DHC Market Outlook 2024 is used; `min_forward_temperature` and `return_temperature` for Germany is used from AGFW-Hauptbericht 2022. +`min_forward_temperature` and `return_temperature` for other countries are extrapolated based on the ratio between `max_forward_temperature` and `min_forward_temperature` and `return_temperature` for those countries not missing (by default only Germany). Relevant Settings ----------------- @@ -47,26 +47,68 @@ ) +def extrapolate_missing_supply_temperatures_by_country( + extrapolate_from: dict, extrapolate_to: dict +) -> xr.DataArray: + """ + Extrapolates missing supply temperatures by country. + + Parameters: + extrapolate_from (dict): A dictionary containing supply temperatures to extrapolate from. Should contain all countries. + extrapolate_to (dict): A dictionary containing supply temperatures to extrapolate to. Where `country` is present, average ratio between `extrapolate_to[country]` and `extrapolate_from[country]` is applied to all countries for which `country` is not present in `extrapolate_from.keys()` to infer ratio for extrapolation. + + Returns: + xr.DataArray: A DataArray containing the extrapolated supply temperatures. + """ + + if not all([key in extrapolate_from.keys() for key in extrapolate_to.keys()]): + raise ValueError( + "Not all countries in extrapolate_to are present in extrapolate_from." + ) + # average ratio between extrapolate_from and extrapolate_to for those countries that are in both dictionaries + extrapolation_ratio = np.mean( + [extrapolate_to[key] / extrapolate_from[key] for key in extrapolate_to.keys()] + ) + + # apply extrapolation ratio to all keys missing in extrapolate_to + return { + key: ( + extrapolate_to[key] + if key in extrapolate_to.keys() + else extrapolate_from[key] * extrapolation_ratio + ) + for key in extrapolate_from.keys() + } + + def get_country_from_node_name(node_name: str) -> str: + """ + Extracts the country code from a given node name. + + Parameters: + node_name (str): The name of the node. + + Returns: + str: The country code extracted from the node name. + """ return node_name[:2] def map_temperature_dict_to_onshore_regions( supply_temperature_by_country: dict, regions_onshore: pd.Index, - snapshots: pd.DatetimeIndex, ) -> xr.DataArray: """ Map dictionary of temperatures to onshore regions. + Missing values are replaced by the mean of all values. + Parameters: ---------- supply_temperature_by_country : dictionary - Dictionary with temperatures as values and country keys as keys. One key must be named "default" + Dictionary with temperatures as values and country keys as keys. regions_onshore : pd.Index Names of onshore regions - snapshots : pd.DatetimeIndex - Time stamps Returns: ------- @@ -75,20 +117,16 @@ def map_temperature_dict_to_onshore_regions( """ return xr.DataArray( [ - [ - ( - supply_temperature_by_country[get_country_from_node_name(node_name)] - if get_country_from_node_name(node_name) - in supply_temperature_by_country.keys() - else supply_temperature_by_country["default"] - ) - for node_name in regions_onshore.values - ] - # pass both nodes and snapshots as dimensions to preserve correct data structure - for _ in snapshots + ( + supply_temperature_by_country[get_country_from_node_name(node_name)] + if get_country_from_node_name(node_name) + in supply_temperature_by_country.keys() + else np.mean(list(supply_temperature_by_country.values())) + ) + for node_name in regions_onshore.values ], - dims=["time", "name"], - coords={"time": snapshots, "name": regions_onshore}, + dims=["name"], + coords={"name": regions_onshore}, ) @@ -104,28 +142,35 @@ def map_temperature_dict_to_onshore_regions( set_scenario_config(snakemake) + max_forward_temperature = snakemake.params.max_forward_temperature_central_heating + min_forward_temperature = extrapolate_missing_supply_temperatures_by_country( + extrapolate_from=max_forward_temperature, + extrapolate_to=snakemake.params.min_forward_temperature_central_heating, + ) + return_temperature = extrapolate_missing_supply_temperatures_by_country( + extrapolate_from=max_forward_temperature, + extrapolate_to=snakemake.params.return_temperature_central_heating, + ) + # map forward and return temperatures specified on country-level to onshore regions regions_onshore = gpd.read_file(snakemake.input.regions_onshore)["name"] snapshots = pd.date_range(freq="h", **snakemake.params.snapshots) max_forward_temperature_central_heating_by_node_and_time: xr.DataArray = ( map_temperature_dict_to_onshore_regions( - supply_temperature_by_country=snakemake.params.max_forward_temperature_central_heating, + supply_temperature_by_country=max_forward_temperature, regions_onshore=regions_onshore, - snapshots=snapshots, ) ) min_forward_temperature_central_heating_by_node_and_time: xr.DataArray = ( map_temperature_dict_to_onshore_regions( - supply_temperature_by_country=snakemake.params.min_forward_temperature_central_heating, + supply_temperature_by_country=min_forward_temperature, regions_onshore=regions_onshore, - snapshots=snapshots, ) ) return_temperature_central_heating_by_node_and_time: xr.DataArray = ( map_temperature_dict_to_onshore_regions( - supply_temperature_by_country=snakemake.params.return_temperature_central_heating, + supply_temperature_by_country=return_temperature, regions_onshore=regions_onshore, - snapshots=snapshots, ) ) From 93ec44de77d09f328f71d57545f680bd708f1370 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Mon, 9 Sep 2024 18:10:59 +0200 Subject: [PATCH 274/344] use heat_system.sector --- scripts/add_existing_baseyear.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index 7e9dd0826..34114a660 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -546,9 +546,9 @@ def add_heating_capacities_installed_before_baseyear( lifetime=costs.at[heat_system.resistive_heater_costs_name, "lifetime"], ) - if "residential" in heat_system.value: + if "residential"==heat_system.sector.value: efficiency = nodes.str[:2].map(heating_efficiencies["gas residential space efficiency"]) - elif "services" in heat_system.value: + elif "services"==heat_system.sector.value: efficiency = nodes.str[:2].map(heating_efficiencies["gas services space efficiency"]) else: #default used for urban central, since no info on district heating boilers @@ -577,9 +577,9 @@ def add_heating_capacities_installed_before_baseyear( lifetime=costs.at[heat_system.gas_boiler_costs_name, "lifetime"], ) - if "residential" in heat_system.value: + if "residential"==heat_system.sector.value: efficiency = nodes.str[:2].map(heating_efficiencies["oil residential space efficiency"]) - elif "services" in heat_system.value: + elif "services"==heat_system.sector.value: efficiency = nodes.str[:2].map(heating_efficiencies["oil services space efficiency"]) else: #default used for urban central, since no info on district heating boilers @@ -639,11 +639,11 @@ def add_heating_capacities_installed_before_baseyear( "add_existing_baseyear", configfiles="config/config.yaml", simpl="", - clusters="20", - ll="v1.5", + clusters="38", + ll="vopt", opts="", - sector_opts="none", - planning_horizons=2030, + sector_opts="", + planning_horizons=2020, ) configure_logging(snakemake) @@ -676,7 +676,7 @@ def add_heating_capacities_installed_before_baseyear( if options["heating"]: - #one could use baseyear here instead (but dangerous if no data) + # one could use baseyear here instead (but dangerous if no data) heating_efficiencies = ( pd.read_csv(snakemake.input.heating_efficiencies, index_col=[0,1]) ).swaplevel().loc[int(snakemake.config["energy"]["energy_totals_year"])] From 59984d06003f120de9efba68e0639586749a6db9 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Mon, 9 Sep 2024 18:19:18 +0200 Subject: [PATCH 275/344] add function for efficiency --- scripts/add_existing_baseyear.py | 58 ++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index 34114a660..10277ad5b 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -418,6 +418,46 @@ def add_power_capacities_installed_before_baseyear(n, grouping_years, costs, bas ] +def get_efficiency(heat_system, carrier, nodes, heating_efficiencies, costs): + """ + Computes the heating system efficiency based on the sector and carrier type. + + Parameters: + ----------- + heat_system : object + carrier : str + The type of fuel or energy carrier (e.g., 'gas', 'oil'). + nodes : pandas.Series + A pandas Series containing node information used to match the heating efficiency data. + heating_efficiencies : dict + A dictionary containing efficiency values for different carriers and sectors. + costs : pandas.DataFrame + A DataFrame containing boiler cost and efficiency data for different heating systems. + + Returns: + -------- + efficiency : pandas.Series or float + A pandas Series mapping the efficiencies based on nodes for residential and services sectors, or a single + efficiency value for other heating systems (e.g., urban central). + + Notes: + ------ + - For residential and services sectors, efficiency is mapped based on the nodes. + - For other sectors, the default boiler efficiency is retrieved from the `costs` database. + """ + + if heat_system.sector.value == "residential": + key = f"{carrier} residential space efficiency" + efficiency = nodes.str[:2].map(heating_efficiencies[key]) + elif heat_system.sector.value == "services": + key = f"{carrier} services space efficiency" + efficiency = nodes.str[:2].map(heating_efficiencies[key]) + else: + boiler_costs_name = getattr(heat_system, f"{carrier}_boiler_costs_name") + efficiency = costs.at[boiler_costs_name, "efficiency"] + + return efficiency + def add_heating_capacities_installed_before_baseyear( n: pypsa.Network, baseyear: int, @@ -546,13 +586,8 @@ def add_heating_capacities_installed_before_baseyear( lifetime=costs.at[heat_system.resistive_heater_costs_name, "lifetime"], ) - if "residential"==heat_system.sector.value: - efficiency = nodes.str[:2].map(heating_efficiencies["gas residential space efficiency"]) - elif "services"==heat_system.sector.value: - efficiency = nodes.str[:2].map(heating_efficiencies["gas services space efficiency"]) - else: - #default used for urban central, since no info on district heating boilers - efficiency = costs.at[heat_system.gas_boiler_costs_name, "efficiency"] + efficiency = get_efficiency(heat_system, "gas", nodes, + heating_efficiencies, costs) n.madd( "Link", @@ -577,13 +612,8 @@ def add_heating_capacities_installed_before_baseyear( lifetime=costs.at[heat_system.gas_boiler_costs_name, "lifetime"], ) - if "residential"==heat_system.sector.value: - efficiency = nodes.str[:2].map(heating_efficiencies["oil residential space efficiency"]) - elif "services"==heat_system.sector.value: - efficiency = nodes.str[:2].map(heating_efficiencies["oil services space efficiency"]) - else: - #default used for urban central, since no info on district heating boilers - efficiency = costs.at[heat_system.oil_boiler_costs_name, "efficiency"] + efficiency = get_efficiency(heat_system, "oil", nodes, + heating_efficiencies, costs) n.madd( "Link", From 861054961ac4e126b9762a031249c457458e2af5 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Mon, 9 Sep 2024 18:20:30 +0200 Subject: [PATCH 276/344] simplify expression --- scripts/add_existing_baseyear.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index 10277ad5b..c34de3627 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -708,8 +708,8 @@ def add_heating_capacities_installed_before_baseyear( # one could use baseyear here instead (but dangerous if no data) heating_efficiencies = ( - pd.read_csv(snakemake.input.heating_efficiencies, index_col=[0,1]) - ).swaplevel().loc[int(snakemake.config["energy"]["energy_totals_year"])] + pd.read_csv(snakemake.input.heating_efficiencies, index_col=[1,0]) + ).loc[int(snakemake.config["energy"]["energy_totals_year"])] add_heating_capacities_installed_before_baseyear( n=n, From 5465d8cc75455b3a220d5e7526853a968b173be2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 16:28:07 +0000 Subject: [PATCH 277/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/add_existing_baseyear.py | 18 +++++++++++------- scripts/build_energy_totals.py | 24 +++++++++++++++--------- scripts/prepare_sector_network.py | 12 +++++++----- 3 files changed, 33 insertions(+), 21 deletions(-) diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index c34de3627..1635fab79 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -420,7 +420,8 @@ def add_power_capacities_installed_before_baseyear(n, grouping_years, costs, bas def get_efficiency(heat_system, carrier, nodes, heating_efficiencies, costs): """ - Computes the heating system efficiency based on the sector and carrier type. + Computes the heating system efficiency based on the sector and carrier + type. Parameters: ----------- @@ -445,7 +446,7 @@ def get_efficiency(heat_system, carrier, nodes, heating_efficiencies, costs): - For residential and services sectors, efficiency is mapped based on the nodes. - For other sectors, the default boiler efficiency is retrieved from the `costs` database. """ - + if heat_system.sector.value == "residential": key = f"{carrier} residential space efficiency" efficiency = nodes.str[:2].map(heating_efficiencies[key]) @@ -458,6 +459,7 @@ def get_efficiency(heat_system, carrier, nodes, heating_efficiencies, costs): return efficiency + def add_heating_capacities_installed_before_baseyear( n: pypsa.Network, baseyear: int, @@ -586,8 +588,9 @@ def add_heating_capacities_installed_before_baseyear( lifetime=costs.at[heat_system.resistive_heater_costs_name, "lifetime"], ) - efficiency = get_efficiency(heat_system, "gas", nodes, - heating_efficiencies, costs) + efficiency = get_efficiency( + heat_system, "gas", nodes, heating_efficiencies, costs + ) n.madd( "Link", @@ -612,8 +615,9 @@ def add_heating_capacities_installed_before_baseyear( lifetime=costs.at[heat_system.gas_boiler_costs_name, "lifetime"], ) - efficiency = get_efficiency(heat_system, "oil", nodes, - heating_efficiencies, costs) + efficiency = get_efficiency( + heat_system, "oil", nodes, heating_efficiencies, costs + ) n.madd( "Link", @@ -708,7 +712,7 @@ def add_heating_capacities_installed_before_baseyear( # one could use baseyear here instead (but dangerous if no data) heating_efficiencies = ( - pd.read_csv(snakemake.input.heating_efficiencies, index_col=[1,0]) + pd.read_csv(snakemake.input.heating_efficiencies, index_col=[1, 0]) ).loc[int(snakemake.config["energy"]["energy_totals_year"])] add_heating_capacities_installed_before_baseyear( diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index 73c3d1a04..a4064af65 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -612,9 +612,11 @@ def build_idees(countries: List[str]) -> pd.DataFrame: # efficiency kgoe/100km -> ktoe/100km so that after conversion TWh/100km totals.loc[:, "passenger car efficiency"] /= 1e6 # convert ktoe to TWh - exclude = totals.columns.str.fullmatch("passenger cars") \ - ^ totals.columns.str.fullmatch(".*space efficiency") \ + exclude = ( + totals.columns.str.fullmatch("passenger cars") + ^ totals.columns.str.fullmatch(".*space efficiency") ^ totals.columns.str.fullmatch(".*water efficiency") + ) totals = totals.copy() totals.loc[:, ~exclude] *= 11.63 / 1e3 @@ -696,11 +698,11 @@ def build_energy_totals( [countries, eurostat_years], names=["country", "year"] ) - to_drop = idees.columns[idees.columns.str.contains("space efficiency") - ^ idees.columns.str.contains("water efficiency")] - to_drop = to_drop.append(pd.Index( - ["passenger cars", "passenger car efficiency"] - )) + to_drop = idees.columns[ + idees.columns.str.contains("space efficiency") + ^ idees.columns.str.contains("water efficiency") + ] + to_drop = to_drop.append(pd.Index(["passenger cars", "passenger car efficiency"])) df = idees.reindex(new_index).drop(to_drop, axis=1) @@ -1543,6 +1545,7 @@ def build_transformation_output_coke(eurostat, fn): df = eurostat.loc[slicer, :].droplevel(level=[2, 3, 4, 5]) df.to_csv(fn) + def build_heating_efficiencies( countries: List[str], idees: pd.DataFrame ) -> pd.DataFrame: @@ -1569,8 +1572,10 @@ def build_heating_efficiencies( years = np.arange(2000, 2022) - cols = idees.columns[idees.columns.str.contains("space efficiency") - ^ idees.columns.str.contains("water efficiency")] + cols = idees.columns[ + idees.columns.str.contains("space efficiency") + ^ idees.columns.str.contains("water efficiency") + ] logger.info(cols) @@ -1595,6 +1600,7 @@ def build_heating_efficiencies( return heating_efficiencies + # %% if __name__ == "__main__": if "snakemake" not in globals(): diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index f303c10c1..097f9bb56 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1806,14 +1806,14 @@ def build_heat_demand(n): for sector, use in product(sectors, uses): name = f"{sector} {use}" - #efficiency for final energy to thermal energy service + # efficiency for final energy to thermal energy service eff = pop_weighted_energy_totals.index.str[:2].map( heating_efficiencies[f"total {sector} {use} efficiency"] - ) + ) heat_demand[name] = ( heat_demand_shape[name] / heat_demand_shape[name].sum() - ).multiply(pop_weighted_energy_totals[f"total {sector} {use}"]*eff) * 1e6 + ).multiply(pop_weighted_energy_totals[f"total {sector} {use}"] * eff) * 1e6 electric_heat_supply[name] = ( heat_demand_shape[name] / heat_demand_shape[name].sum() ).multiply(pop_weighted_energy_totals[f"electricity {sector} {use}"]) * 1e6 @@ -4288,8 +4288,10 @@ def add_enhanced_geothermal(n, egs_potentials, egs_overlap, costs): pop_weighted_energy_totals.update(pop_weighted_heat_totals) heating_efficiencies = ( - pd.read_csv(snakemake.input.heating_efficiencies, index_col=[0,1]) - ).swaplevel().loc[int(snakemake.config["energy"]["energy_totals_year"])] + (pd.read_csv(snakemake.input.heating_efficiencies, index_col=[0, 1])) + .swaplevel() + .loc[int(snakemake.config["energy"]["energy_totals_year"])] + ) patch_electricity_network(n) From 9aa9a59bb03cdaac1d45fde0b23813589e483ed1 Mon Sep 17 00:00:00 2001 From: Toni Seibold <153275395+toniseibold@users.noreply.github.com> Date: Mon, 9 Sep 2024 19:35:15 +0200 Subject: [PATCH 278/344] removing scripts. (#1267) --- scripts/definitions/heat_system.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/definitions/heat_system.py b/scripts/definitions/heat_system.py index b907b0fef..2806f6bf4 100644 --- a/scripts/definitions/heat_system.py +++ b/scripts/definitions/heat_system.py @@ -5,8 +5,8 @@ from enum import Enum -from scripts.definitions.heat_sector import HeatSector -from scripts.definitions.heat_system_type import HeatSystemType +from definitions.heat_sector import HeatSector +from definitions.heat_system_type import HeatSystemType class HeatSystem(Enum): From 5f45a1be8404836f1f1445b78c996d3046715758 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Tue, 10 Sep 2024 12:32:42 +0200 Subject: [PATCH 279/344] build_biomass_transport_costs: do not rename XK to KO --- scripts/build_biomass_transport_costs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build_biomass_transport_costs.py b/scripts/build_biomass_transport_costs.py index d56509026..db3695426 100644 --- a/scripts/build_biomass_transport_costs.py +++ b/scripts/build_biomass_transport_costs.py @@ -79,7 +79,7 @@ def build_biomass_transport_costs(): transport_costs.name = "EUR/km/MWh" # rename country names - to_rename = {"UK": "GB", "XK": "KO", "EL": "GR"} + to_rename = {"UK": "GB", "EL": "GR"} transport_costs.rename(to_rename, inplace=True) # add missing Norway with data from Sweden From 879c896bea45ca6908a96ddb7d6cdb8111220e77 Mon Sep 17 00:00:00 2001 From: "daniel.rdt" Date: Tue, 10 Sep 2024 13:51:34 +0200 Subject: [PATCH 280/344] fix bug for plotting of hydrogen network with myopic foresight. --- scripts/plot_hydrogen_network.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/plot_hydrogen_network.py b/scripts/plot_hydrogen_network.py index b4585fb2c..d7e42ff87 100644 --- a/scripts/plot_hydrogen_network.py +++ b/scripts/plot_hydrogen_network.py @@ -115,7 +115,7 @@ def plot_h2_map(n, regions): retro_wo_new_i = h2_retro.index.difference(h2_new.index) h2_retro_wo_new = h2_retro.loc[retro_wo_new_i] - h2_retro_wo_new.index = h2_retro_wo_new.index_orig + h2_retro_wo_new.index = h2_retro_wo_new.index_orig.apply(lambda x: x.split('-2')[0]) to_concat = [h2_new, h2_retro_w_new, h2_retro_wo_new] h2_total = pd.concat(to_concat).p_nom_opt.groupby(level=0).sum() @@ -126,7 +126,10 @@ def plot_h2_map(n, regions): link_widths_total = h2_total / linewidth_factor n.links.rename(index=lambda x: x.split("-2")[0], inplace=True) - n.links = n.links.groupby(level=0).first() + # group links by summing up p_nom values and taking the first value of the rest of the columns + other_cols = dict.fromkeys(n.links.columns.drop(["p_nom_opt", "p_nom"]), "first") + n.links = n.links.groupby(level=0).agg({"p_nom_opt": "sum", "p_nom": "sum", **other_cols}) + link_widths_total = link_widths_total.reindex(n.links.index).fillna(0.0) link_widths_total[n.links.p_nom_opt < line_lower_threshold] = 0.0 From e89027e9466c70ca47600fb543cd592d73848dec Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 10 Sep 2024 12:04:16 +0000 Subject: [PATCH 281/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/plot_hydrogen_network.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/scripts/plot_hydrogen_network.py b/scripts/plot_hydrogen_network.py index d7e42ff87..6d666acda 100644 --- a/scripts/plot_hydrogen_network.py +++ b/scripts/plot_hydrogen_network.py @@ -115,7 +115,9 @@ def plot_h2_map(n, regions): retro_wo_new_i = h2_retro.index.difference(h2_new.index) h2_retro_wo_new = h2_retro.loc[retro_wo_new_i] - h2_retro_wo_new.index = h2_retro_wo_new.index_orig.apply(lambda x: x.split('-2')[0]) + h2_retro_wo_new.index = h2_retro_wo_new.index_orig.apply( + lambda x: x.split("-2")[0] + ) to_concat = [h2_new, h2_retro_w_new, h2_retro_wo_new] h2_total = pd.concat(to_concat).p_nom_opt.groupby(level=0).sum() @@ -128,8 +130,10 @@ def plot_h2_map(n, regions): n.links.rename(index=lambda x: x.split("-2")[0], inplace=True) # group links by summing up p_nom values and taking the first value of the rest of the columns other_cols = dict.fromkeys(n.links.columns.drop(["p_nom_opt", "p_nom"]), "first") - n.links = n.links.groupby(level=0).agg({"p_nom_opt": "sum", "p_nom": "sum", **other_cols}) - + n.links = n.links.groupby(level=0).agg( + {"p_nom_opt": "sum", "p_nom": "sum", **other_cols} + ) + link_widths_total = link_widths_total.reindex(n.links.index).fillna(0.0) link_widths_total[n.links.p_nom_opt < line_lower_threshold] = 0.0 From 8a0b6ba9114b020ad9677ea5218366f28f647d8f Mon Sep 17 00:00:00 2001 From: cpschau <124347782+cpschau@users.noreply.github.com> Date: Tue, 10 Sep 2024 16:21:14 +0200 Subject: [PATCH 282/344] Hot fix: Rename reindex transport costs for unsustainable biomass (#1271) * reindex transport costs for unsus biomass * change index of potential dataframe for unsustainable solid potentials --- scripts/prepare_sector_network.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 7f7448740..6ef86cc44 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2332,7 +2332,7 @@ def add_biomass(n, costs): ].rename(index=lambda x: x + " municipal solid waste") unsustainable_solid_biomass_potentials_spatial = biomass_potentials[ "unsustainable solid biomass" - ].rename(index=lambda x: x + " solid biomass") + ].rename(index=lambda x: x + " unsustainable solid biomass") else: solid_biomass_potentials_spatial = biomass_potentials["solid biomass"].sum() @@ -2693,7 +2693,12 @@ def add_biomass(n, costs): carrier="unsustainable solid biomass", p_nom=10000, marginal_cost=costs.at["fuelwood", "fuel"] - + bus_transport_costs * average_distance, + + bus_transport_costs.rename( + dict( + zip(spatial.biomass.nodes, spatial.biomass.nodes_unsustainable) + ) + ) + * average_distance, ) # Set last snapshot of e_max_pu for unsustainable solid biomass to 1 to make operational limit work unsus_stores_idx = n.stores.query( From ca9556597ed7b1ca1e4287caace4f8c192c4f53d Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Tue, 10 Sep 2024 20:40:31 +0200 Subject: [PATCH 283/344] limit powerplantmatching version to below v0.6 due to upcoming breaking changes --- envs/environment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envs/environment.yaml b/envs/environment.yaml index 98cf858d6..e40d07d8a 100644 --- a/envs/environment.yaml +++ b/envs/environment.yaml @@ -24,7 +24,7 @@ dependencies: - yaml - pytables - lxml -- powerplantmatching>=0.5.15 +- powerplantmatching>=0.5.15,<0.6 - numpy - pandas>=2.1 - geopandas>=1 From ac606c1f67db517b13f8e6fb2b7735266ad0b899 Mon Sep 17 00:00:00 2001 From: Bobby Xiong <36541459+bobbyxng@users.noreply.github.com> Date: Tue, 10 Sep 2024 20:51:28 +0200 Subject: [PATCH 284/344] Build biomass transport costs from pre-extracted .csv instead of pdf (#1272) * Removed tabula dependency for building biomass transport costs. Instead use pre-extracted .csvs. Updated rule and script accordingly. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Updated script to optionally allow running the script with tabula-py dependency to recreate csv, if needed. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- data/biomass_transport_costs_supplychain1.csv | 41 +++++++++++ data/biomass_transport_costs_supplychain2.csv | 41 +++++++++++ envs/environment.yaml | 1 - rules/build_sector.smk | 6 +- scripts/build_biomass_transport_costs.py | 68 +++++++++++++------ 5 files changed, 130 insertions(+), 27 deletions(-) create mode 100644 data/biomass_transport_costs_supplychain1.csv create mode 100644 data/biomass_transport_costs_supplychain2.csv diff --git a/data/biomass_transport_costs_supplychain1.csv b/data/biomass_transport_costs_supplychain1.csv new file mode 100644 index 000000000..b5f5a0e3b --- /dev/null +++ b/data/biomass_transport_costs_supplychain1.csv @@ -0,0 +1,41 @@ +country,Loading,Loading,Loading,Transport to plant,Transport to plant,Unloading,TOTAL,TOTAL,TOTAL +country,Time,Machinery costs,Waiting time truck driver,60 average speed km/h,Trailer,Waiting time truck driver,Fix,per km,per km/ton +country,h,EUR,EUR.1,EUR/km,EUR.2,EUR.3,EUR.4,EUR/km.1,EUR/km/ton +BE,0.08,3.56,1.31,1.45,6.42,1.31,12.61,0.02,0.47 +BG,0.08,2.06,0.56,1.06,2.61,0.56,5.79,0.02,0.22 +CZ,0.08,2.34,0.62,1.1,4.08,0.62,7.65,0.02,0.29 +DK,0.08,5.14,2.54,1.85,7.91,2.54,18.13,0.03,0.68 +DE,0.08,3.41,1.31,1.37,5.99,1.31,12.01,0.02,0.45 +EE,0.08,2.26,0.5,1.1,4.14,0.5,7.38,0.02,0.28 +IE,0.08,3.29,1.03,1.4,6.29,1.03,11.64,0.02,0.44 +EL,0.08,2.83,0.85,1.25,5.17,0.85,9.7,0.02,0.37 +ES,0.08,3.19,1.22,1.32,5.28,1.22,10.91,0.02,0.41 +FR,0.08,3.52,1.35,1.38,6.49,1.35,12.71,0.02,0.48 +IT,0.08,3.59,1.26,1.56,5.82,1.26,11.94,0.03,0.45 +CY,0.08,2.82,0.85,1.25,5.09,0.85,9.6,0.02,0.36 +LV,0.08,2.11,0.43,1.05,3.87,0.43,6.84,0.02,0.26 +LT,0.08,1.99,0.39,1.02,3.49,0.39,6.25,0.02,0.24 +LU,0.08,3.38,1.21,1.31,6.97,1.21,12.78,0.02,0.48 +HU,0.08,2.12,0.42,1.12,3.33,0.42,6.28,0.02,0.24 +MT,0.08,2.44,0.62,1.16,4.32,0.62,7.99,0.02,0.3 +NL,0.08,3.83,1.57,1.51,6.37,1.57,13.33,0.03,0.5 +AT,0.08,3.33,1.18,1.35,6.37,1.18,12.06,0.02,0.45 +PL,0.08,2.27,0.68,1.08,3.35,0.68,6.98,0.02,0.27 +PT,0.08,2.3,0.37,1.16,4.67,0.37,7.71,0.02,0.29 +RO,0.08,1.88,0.4,0.99,2.8,0.4,5.49,0.02,0.21 +SI,0.08,2.47,0.58,1.18,4.65,0.58,8.29,0.02,0.32 +SK,0.08,2.24,0.48,1.12,3.93,0.48,7.13,0.02,0.27 +FI,0.08,3.59,1.2,1.48,7.0,1.2,12.98,0.02,0.49 +SE,0.08,3.84,1.26,1.57,7.71,1.26,14.07,0.03,0.53 +UK,0.08,3.65,1.21,1.56,6.56,1.21,12.64,0.03,0.48 +HR,0.08,2.12,0.46,1.05,3.79,0.46,6.84,0.02,0.26 +AL,0.08,1.67,0.19,0.96,2.55,0.19,4.61,0.02,0.18 +BA,0.08,1.66,0.23,0.91,2.81,0.23,4.92,0.02,0.19 +MK,0.08,1.5,0.22,0.84,2.32,0.22,4.27,0.01,0.17 +ME,0.08,1.71,0.24,0.94,2.83,0.24,5.02,0.02,0.2 +RS,0.08,1.69,0.22,0.96,2.64,0.22,4.77,0.02,0.19 +XK,0.08,1.62,0.21,0.89,2.81,0.21,4.85,0.01,0.19 +UA,0.08,1.25,0.13,0.65,2.8,0.13,4.32,0.01,0.16 +TR,0.08,2.35,0.52,1.24,3.44,0.52,6.82,0.02,0.26 +MD,0.08,1.33,0.09,0.74,2.8,0.09,4.31,0.01,0.17 +CH,0.08,3.79,1.18,1.47,8.9,1.18,15.06,0.02,0.56 diff --git a/data/biomass_transport_costs_supplychain2.csv b/data/biomass_transport_costs_supplychain2.csv new file mode 100644 index 000000000..530ff7d94 --- /dev/null +++ b/data/biomass_transport_costs_supplychain2.csv @@ -0,0 +1,41 @@ +country,Loading,Loading,Loading,Transport to plant,Transport to plant,Unloading,TOTAL,TOTAL,TOTAL +country,Time,Machinery costs,Waiting time truck driver,60 average speed km/h,Trailer,Waiting time truck driver,Fix,per km,per km/ton +country,h,EUR,EUR.1,EUR/km,EUR.2,EUR.3,EUR.4,EUR/km.1,EUR/km/ton +BE,0.05,3.06,0.88,1.49,16.57,0.88,21.39,0.02,0.88 +BG,0.05,1.77,0.37,1.07,6.74,0.37,9.26,0.02,0.39 +CZ,0.05,2.05,0.41,1.13,10.52,0.41,13.4,0.02,0.55 +DK,0.05,4.24,1.7,1.89,20.4,1.7,28.04,0.03,1.15 +DE,0.05,2.91,0.87,1.41,15.45,0.87,20.11,0.02,0.83 +EE,0.05,2.01,0.33,1.13,10.67,0.33,13.34,0.02,0.55 +IE,0.05,2.87,0.69,1.44,16.23,0.69,20.48,0.02,0.84 +EL,0.05,2.47,0.57,1.28,13.34,0.57,16.95,0.02,0.7 +ES,0.05,2.71,0.82,1.35,13.63,0.82,17.98,0.02,0.74 +FR,0.05,3.02,0.91,1.42,16.74,0.91,21.56,0.02,0.89 +IT,0.05,3.07,0.85,1.59,15.0,0.85,19.77,0.03,0.82 +CY,0.05,2.46,0.57,1.28,13.12,0.57,16.71,0.02,0.69 +LV,0.05,1.89,0.29,1.08,9.98,0.29,12.44,0.02,0.52 +LT,0.05,1.78,0.26,1.04,9.01,0.26,11.3,0.02,0.47 +LU,0.05,2.94,0.81,1.35,17.98,0.81,22.54,0.02,0.92 +HU,0.05,1.88,0.28,1.14,8.59,0.28,11.03,0.02,0.46 +MT,0.05,2.15,0.41,1.19,11.15,0.41,14.12,0.02,0.58 +NL,0.05,3.23,1.05,1.55,16.42,1.05,21.76,0.03,0.9 +AT,0.05,2.88,0.79,1.38,16.42,0.79,20.88,0.02,0.86 +PL,0.05,1.96,0.46,1.1,8.64,0.46,11.51,0.02,0.48 +PT,0.05,2.09,0.25,1.18,12.04,0.25,14.63,0.02,0.6 +RO,0.05,1.66,0.27,1.01,7.23,0.27,9.43,0.02,0.39 +SI,0.05,2.2,0.39,1.2,12.0,0.39,14.98,0.02,0.62 +SK,0.05,1.99,0.32,1.15,10.13,0.32,12.77,0.02,0.53 +FI,0.05,3.12,0.8,1.52,18.05,0.8,22.78,0.03,0.94 +SE,0.05,3.35,0.85,1.62,19.89,0.85,24.93,0.03,1.02 +UK,0.05,3.16,0.81,1.6,16.93,0.81,21.71,0.03,0.9 +HR,0.05,1.89,0.31,1.07,9.77,0.31,12.28,0.02,0.51 +AL,0.05,1.5,0.13,0.98,6.59,0.13,8.35,0.02,0.35 +BA,0.05,1.5,0.15,0.93,7.25,0.15,9.05,0.02,0.38 +MK,0.05,1.35,0.15,0.85,5.98,0.15,7.62,0.01,0.32 +ME,0.05,1.54,0.16,0.96,7.31,0.16,9.17,0.02,0.38 +RS,0.05,1.52,0.15,0.97,6.8,0.15,8.62,0.02,0.36 +XK,0.05,1.46,0.14,0.91,7.25,0.14,8.99,0.02,0.37 +UA,0.05,1.16,0.09,0.66,7.23,0.09,8.57,0.01,0.35 +TR,0.05,2.07,0.35,1.26,8.88,0.35,11.63,0.02,0.49 +MD,0.05,1.24,0.06,0.75,7.23,0.06,8.59,0.01,0.36 +CH,0.05,3.37,0.79,1.52,22.97,0.79,27.92,0.03,1.14 diff --git a/envs/environment.yaml b/envs/environment.yaml index e40d07d8a..0b56c1efc 100644 --- a/envs/environment.yaml +++ b/envs/environment.yaml @@ -64,4 +64,3 @@ dependencies: - snakemake-executor-plugin-slurm - snakemake-executor-plugin-cluster-generic - highspy - - tabula-py diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 9f94dbbd6..c719c338d 100755 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -435,10 +435,8 @@ rule build_biomass_potentials: rule build_biomass_transport_costs: input: - transport_cost_data=storage( - "https://publications.jrc.ec.europa.eu/repository/bitstream/JRC98626/biomass potentials in europe_web rev.pdf", - keep_local=True, - ), + sc1="data/biomass_transport_costs_supplychain1.csv", + sc2="data/biomass_transport_costs_supplychain2.csv", output: biomass_transport_costs=resources("biomass_transport_costs.csv"), threads: 1 diff --git a/scripts/build_biomass_transport_costs.py b/scripts/build_biomass_transport_costs.py index db3695426..d18362306 100644 --- a/scripts/build_biomass_transport_costs.py +++ b/scripts/build_biomass_transport_costs.py @@ -16,33 +16,51 @@ @author: bw0928 """ - -import platform - import pandas as pd -import tabula as tbl ENERGY_CONTENT = 4.8 # unit MWh/t (wood pellets) -system = platform.system() -encoding = "cp1252" if system == "Windows" else "utf-8" -def get_countries(): - pandas_options = dict( +def get_cost_per_tkm(pdf, datapage, countrypage): + """ + Extracts the cost tables from the JRC report PDF. + + https://publications.jrc.ec.europa.eu/repository/bitstream/JRC98626/biomass%20potentials%20in%20europe_web%20rev.pdf + - pdf (str): The filepath of the PDF file containing the data. + - datapage (int): The page number of the data table in the PDF. + - countrypage (int): The page number of the table containing the country indices in the PDF. + + Returns: + - pandas.DataFrame: The data table with the cost per tkm for biomass transport, indexed by country. + + Raises: + - ImportError: If tabula-py and platform are not installed. + """ + try: + import platform + + import tabula as tbl + except: + ImportError("Please install tabula-py and platform") + + system = platform.system() + encoding = "cp1252" if system == "Windows" else "utf-8" + + # Obtain countries: + pandas_options_country = dict( skiprows=range(6), header=None, index_col=0, encoding=encoding ) - return tbl.read_pdf( - str(snakemake.input.transport_cost_data), - pages="145", + countries = tbl.read_pdf( + pdf, + pages=countrypage, multiple_tables=False, - pandas_options=pandas_options, + pandas_options=pandas_options_country, encoding=encoding, )[0].index - -def get_cost_per_tkm(page, countries): - pandas_options = dict( + # Obtain data tables + pandas_options_data = dict( skiprows=range(6), header=0, sep=" |,", @@ -52,10 +70,10 @@ def get_cost_per_tkm(page, countries): ) sc = tbl.read_pdf( - str(snakemake.input.transport_cost_data), - pages=page, + pdf, + pages=datapage, multiple_tables=False, - pandas_options=pandas_options, + pandas_options=pandas_options_data, encoding=encoding, )[0] sc.index = countries @@ -65,10 +83,16 @@ def get_cost_per_tkm(page, countries): def build_biomass_transport_costs(): - countries = get_countries() - - sc1 = get_cost_per_tkm(146, countries) - sc2 = get_cost_per_tkm(147, countries) + # Optional build from JRC report pdf, requires tabula and java dependencies. + # Update `pdf` path to the JRC report if needed. + # sc1 = get_cost_per_tkm(pdf = "report.pdf", datapage=146, countrypage=145) + # sc2 = get_cost_per_tkm(pdf = "report.pdf", datapage=147, countrypage=145) + + # Use extracted csv from JRC report + # https://publications.jrc.ec.europa.eu/repository/bitstream/JRC98626/biomass%20potentials%20in%20europe_web%20rev.pdf + # Pages 146 (144) for supply chain 1 and 147 (145) for supply chain 2 + sc1 = pd.read_csv(snakemake.input.sc1, index_col=0, skiprows=2) + sc2 = pd.read_csv(snakemake.input.sc2, index_col=0, skiprows=2) # take mean of both supply chains to_concat = [sc1["EUR/km/ton"], sc2["EUR/km/ton"]] From 8c57a8037952dbf0bec0d6731cdd8f7de2c1d90a Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Tue, 10 Sep 2024 21:07:13 +0200 Subject: [PATCH 285/344] smk: use storage() only in combination with retrieve rules (#1274) * smk: use storage() only in combination with retrieve rules * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- doc/release_notes.rst | 4 +++ rules/build_sector.smk | 19 +++----------- rules/retrieve.smk | 59 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 15 deletions(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 97db4bafc..e75d33440 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -17,6 +17,10 @@ Release Notes * bugfix: The oil generator was incorrectly dropped when the config `oil_refining_emissions` was greater than zero. This was the default behaviour in 0.12.0. +* Uses of Snakemake's ``storage()`` function are integrated into retrieval + rules. This simplifies the use of ``mock_snakemake`` and places downloaded + data more transparently into the ``data`` directory. + PyPSA-Eur 0.12.0 (30th August 2024) =================================== diff --git a/rules/build_sector.smk b/rules/build_sector.smk index c719c338d..1c80cd80d 100755 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -402,10 +402,7 @@ rule build_biomass_potentials: params: biomass=config_provider("biomass"), input: - enspreso_biomass=storage( - "https://zenodo.org/records/10356004/files/ENSPRESO_BIOMASS.xlsx", - keep_local=True, - ), + enspreso_biomass="data/ENSPRESO_BIOMASS.xlsx", eurostat="data/eurostat/Balances-April2023", nuts2="data/bundle/nuts/NUTS_RG_10M_2013_4326_LEVL_2.geojson", regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), @@ -458,10 +455,7 @@ rule build_sequestration_potentials: "sector", "regional_co2_sequestration_potential" ), input: - sequestration_potential=storage( - "https://raw.githubusercontent.com/ericzhou571/Co2Storage/main/resources/complete_map_2020_unit_Mt.geojson", - keep_local=True, - ), + sequestration_potential="data/omplete_map_2020_unit_Mt.geojson", regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), regions_offshore=resources("regions_offshore_elec_s{simpl}_{clusters}.geojson"), output: @@ -503,9 +497,7 @@ rule build_salt_cavern_potentials: rule build_ammonia_production: input: - usgs=storage( - "https://d9-wret.s3.us-west-2.amazonaws.com/assets/palladium/production/s3fs-public/media/files/myb1-2022-nitro-ert.xlsx" - ), + usgs="data/myb1-2022-nitro-ert.xlsx", output: ammonia_production=resources("ammonia_production.csv"), threads: 1 @@ -634,10 +626,7 @@ rule build_industrial_distribution_key: input: regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), clustered_pop_layout=resources("pop_layout_elec_s{simpl}_{clusters}.csv"), - hotmaps=storage( - "https://gitlab.com/hotmaps/industrial_sites/industrial_sites_Industrial_Database/-/raw/master/data/Industrial_Database.csv", - keep_local=True, - ), + hotmaps="data/Industrial_Database.csv", gem_gspt="data/gem/Global-Steel-Plant-Tracker-April-2024-Standard-Copy-V1.xlsx", ammonia="data/ammonia_plants.csv", cement_supplement="data/cement-plants-noneu.csv", diff --git a/rules/retrieve.smk b/rules/retrieve.smk index 67b91b998..844618e03 100644 --- a/rules/retrieve.smk +++ b/rules/retrieve.smk @@ -191,6 +191,65 @@ if config["enable"]["retrieve"]: validate_checksum(output[0], input[0]) +if config["enable"]["retrieve"]: + + rule retrieve_jrc_enspreso_biomass: + input: + storage( + "https://zenodo.org/records/10356004/files/ENSPRESO_BIOMASS.xlsx", + keep_local=True, + ), + output: + "data/ENSPRESO_BIOMASS.xlsx", + retries: 1 + run: + move(input[0], output[0]) + + +if config["enable"]["retrieve"]: + + rule retrieve_hotmaps_industrial_sites: + input: + storage( + "https://gitlab.com/hotmaps/industrial_sites/industrial_sites_Industrial_Database/-/raw/master/data/Industrial_Database.csv", + keep_local=True, + ), + output: + "data/Industrial_Database.csv", + retries: 1 + run: + move(input[0], output[0]) + + +if config["enable"]["retrieve"]: + + rule retrieve_usgs_ammonia_production: + input: + storage( + "https://d9-wret.s3.us-west-2.amazonaws.com/assets/palladium/production/s3fs-public/media/files/myb1-2022-nitro-ert.xlsx" + ), + output: + "data/myb1-2022-nitro-ert.xlsx", + retries: 1 + run: + move(input[0], output[0]) + + +if config["enable"]["retrieve"]: + + rule retrieve_geological_co2_storage_potential: + input: + storage( + "https://raw.githubusercontent.com/ericzhou571/Co2Storage/main/resources/complete_map_2020_unit_Mt.geojson", + keep_local=True, + ), + output: + "data/complete_map_2020_unit_Mt.geojson", + retries: 1 + run: + move(input[0], output[0]) + + if config["enable"]["retrieve"]: # Downloading Copernicus Global Land Cover for land cover and land use: From f118d15a4bf9a886e5c90735c78b5249191666af Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Tue, 10 Sep 2024 21:54:24 +0200 Subject: [PATCH 286/344] fix typo --- rules/build_sector.smk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 1c80cd80d..ccb56aaaa 100755 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -455,7 +455,7 @@ rule build_sequestration_potentials: "sector", "regional_co2_sequestration_potential" ), input: - sequestration_potential="data/omplete_map_2020_unit_Mt.geojson", + sequestration_potential="data/complete_map_2020_unit_Mt.geojson", regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), regions_offshore=resources("regions_offshore_elec_s{simpl}_{clusters}.geojson"), output: From 6cb68b191a738e45e6ff05640ea6704d6409e291 Mon Sep 17 00:00:00 2001 From: Philipp Glaum Date: Wed, 11 Sep 2024 09:32:42 +0200 Subject: [PATCH 287/344] address Lisa's comments --- scripts/prepare_sector_network.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index e310c8b19..452fbf1b3 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -919,7 +919,7 @@ def add_methanol_to_power(n, costs, types={}): carrier="CCGT methanol", p_nom_extendable=True, capital_cost=capital_cost, - marginal_cost=2, + marginal_cost=costs.at["CCGT", "VOM"], efficiency=costs.at["CCGT", "efficiency"], efficiency2=costs.at["methanolisation", "carbondioxide-input"], lifetime=costs.at["CCGT", "lifetime"], @@ -952,7 +952,7 @@ def add_methanol_to_power(n, costs, types={}): carrier="CCGT methanol CC", p_nom_extendable=True, capital_cost=capital_cost_cc, - marginal_cost=costs.at["CCGT", "VOM"] * costs.at["CCGT", "VOM"], + marginal_cost=costs.at["CCGT", "VOM"], efficiency=costs.at["CCGT", "efficiency"], efficiency2=costs.at["cement capture", "capture_rate"] * costs.at["methanolisation", "carbondioxide-input"], @@ -982,7 +982,7 @@ def add_methanol_to_power(n, costs, types={}): def add_methanol_to_olefins(n, costs): - nodes = pop_layout.index + nodes = spatial.nodes nhours = n.snapshot_weightings.generators.sum() nyears = nhours / 8760 @@ -2617,7 +2617,7 @@ def add_methanol(n, costs): logger.info("Add methanol") add_carrier_buses(n, "methanol") - if n.buses.carrier.str.contains("biomass").any(): + if options["biomass"]: if methanol_options["biomass_to_methanol"]: add_biomass_to_methanol(n, costs) From b90f83a659105ee49316027be4dc6c1d09e27c17 Mon Sep 17 00:00:00 2001 From: lisazeyen <35347358+lisazeyen@users.noreply.github.com> Date: Wed, 11 Sep 2024 09:35:31 +0200 Subject: [PATCH 288/344] simplify drop expression Co-authored-by: Fabian Neumann --- scripts/build_energy_totals.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index 3d37a8ee3..4f0962620 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -698,11 +698,9 @@ def build_energy_totals( [countries, eurostat_years], names=["country", "year"] ) - to_drop = idees.columns[ - idees.columns.str.contains("space efficiency") - ^ idees.columns.str.contains("water efficiency") - ] - to_drop = to_drop.append(pd.Index(["passenger cars", "passenger car efficiency"])) + efficiency_keywords = ["space efficiency", "water efficiency"] + to_drop = idees.columns[~idees.columns.str.contains('|'.join(efficiency_keywords))] + to_drop = to_drop.append(["passenger cars", "passenger car efficiency"]) df = idees.reindex(new_index).drop(to_drop, axis=1) From 163f8344817d43344d69e7b02a64f9fdcb1a13fc Mon Sep 17 00:00:00 2001 From: lisazeyen <35347358+lisazeyen@users.noreply.github.com> Date: Wed, 11 Sep 2024 09:35:50 +0200 Subject: [PATCH 289/344] remove snakemake.config Co-authored-by: Fabian Neumann --- scripts/add_existing_baseyear.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index 1635fab79..3757d4ba8 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -711,9 +711,9 @@ def add_heating_capacities_installed_before_baseyear( if options["heating"]: # one could use baseyear here instead (but dangerous if no data) - heating_efficiencies = ( - pd.read_csv(snakemake.input.heating_efficiencies, index_col=[1, 0]) - ).loc[int(snakemake.config["energy"]["energy_totals_year"])] + fn = snakemake.input.heating_efficiencies + year = int(snakemake.params["energy_totals_year"]) + heating_efficiencies = pd.read_csv(fn, index_col=[1, 0]).loc[year] add_heating_capacities_installed_before_baseyear( n=n, From 649e48e1a9805388ed7f92f8b97f5d8fd2c907fe Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 11 Sep 2024 07:35:58 +0000 Subject: [PATCH 290/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/build_energy_totals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index 4f0962620..72bc01b5b 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -699,7 +699,7 @@ def build_energy_totals( ) efficiency_keywords = ["space efficiency", "water efficiency"] - to_drop = idees.columns[~idees.columns.str.contains('|'.join(efficiency_keywords))] + to_drop = idees.columns[~idees.columns.str.contains("|".join(efficiency_keywords))] to_drop = to_drop.append(["passenger cars", "passenger car efficiency"]) df = idees.reindex(new_index).drop(to_drop, axis=1) From 0ac6a6a1f2c07a5ee0ff8696e29de5e615fbbd35 Mon Sep 17 00:00:00 2001 From: lisazeyen <35347358+lisazeyen@users.noreply.github.com> Date: Wed, 11 Sep 2024 09:38:07 +0200 Subject: [PATCH 291/344] simplify exclude statement Co-authored-by: Fabian Neumann --- scripts/build_energy_totals.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index 72bc01b5b..5fe405a67 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -612,11 +612,8 @@ def build_idees(countries: List[str]) -> pd.DataFrame: # efficiency kgoe/100km -> ktoe/100km so that after conversion TWh/100km totals.loc[:, "passenger car efficiency"] /= 1e6 # convert ktoe to TWh - exclude = ( - totals.columns.str.fullmatch("passenger cars") - ^ totals.columns.str.fullmatch(".*space efficiency") - ^ totals.columns.str.fullmatch(".*water efficiency") - ) + patterns = ["passenger cars", ".*space efficiency", ".*water efficiency"] + exclude = totals.columns.str.fullmatch("|".join(patterns)) totals = totals.copy() totals.loc[:, ~exclude] *= 11.63 / 1e3 From 82a669635b7b12ad65d8dd9c3d91ad1ea42745e5 Mon Sep 17 00:00:00 2001 From: lisazeyen <35347358+lisazeyen@users.noreply.github.com> Date: Wed, 11 Sep 2024 09:38:21 +0200 Subject: [PATCH 292/344] removing logging Co-authored-by: Fabian Neumann --- scripts/build_energy_totals.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index 5fe405a67..ce5bf7b05 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -1572,7 +1572,6 @@ def build_heating_efficiencies( ^ idees.columns.str.contains("water efficiency") ] - logger.info(cols) heating_efficiencies = pd.DataFrame(idees[cols]) From 0bdf90280bc65a805b20013e08d4b75c7b0eea6b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 11 Sep 2024 07:38:40 +0000 Subject: [PATCH 293/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/build_energy_totals.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index ce5bf7b05..b09ba080c 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -1572,7 +1572,6 @@ def build_heating_efficiencies( ^ idees.columns.str.contains("water efficiency") ] - heating_efficiencies = pd.DataFrame(idees[cols]) new_index = pd.MultiIndex.from_product( From c0172117004c4d7bf2d6eb195387219af5dcdd61 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Wed, 11 Sep 2024 10:16:09 +0200 Subject: [PATCH 294/344] use snakemake.params --- rules/build_sector.smk | 1 + scripts/prepare_sector_network.py | 8 +++----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/rules/build_sector.smk b/rules/build_sector.smk index db7084a9b..cb3db79cf 100755 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -1027,6 +1027,7 @@ rule prepare_sector_network: RDIR=RDIR, heat_pump_sources=config_provider("sector", "heat_pump_sources"), heat_systems=config_provider("sector", "heat_systems"), + energy_totals_year=config_provider("energy", "energy_totals_year"), input: unpack(input_profile_offwind), **rules.cluster_gas_network.output, diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 4aac58016..4ddcac327 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -4378,11 +4378,9 @@ def add_enhanced_geothermal(n, egs_potentials, egs_overlap, costs): ) pop_weighted_energy_totals.update(pop_weighted_heat_totals) - heating_efficiencies = ( - (pd.read_csv(snakemake.input.heating_efficiencies, index_col=[0, 1])) - .swaplevel() - .loc[int(snakemake.config["energy"]["energy_totals_year"])] - ) + fn = snakemake.input.heating_efficiencies + year = int(snakemake.params["energy_totals_year"]) + heating_efficiencies = pd.read_csv(fn, index_col=[1, 0]).loc[year] patch_electricity_network(n) From 4315b3a18ac98f50f16610de0ae8656900b96a6a Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Wed, 11 Sep 2024 14:09:23 +0200 Subject: [PATCH 295/344] fix dropping bug --- scripts/build_energy_totals.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index b09ba080c..6afa6b334 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -696,8 +696,8 @@ def build_energy_totals( ) efficiency_keywords = ["space efficiency", "water efficiency"] - to_drop = idees.columns[~idees.columns.str.contains("|".join(efficiency_keywords))] - to_drop = to_drop.append(["passenger cars", "passenger car efficiency"]) + to_drop = idees.columns[idees.columns.str.contains("|".join(efficiency_keywords))] + to_drop = to_drop.append(pd.Index(["passenger cars", "passenger car efficiency"])) df = idees.reindex(new_index).drop(to_drop, axis=1) From 6a1438cffdf819a15c8191c1d5fcc7277cb64419 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Wed, 11 Sep 2024 15:06:17 +0200 Subject: [PATCH 296/344] fix heating_system.sector bug --- scripts/add_existing_baseyear.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index 3757d4ba8..53d8e3907 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -446,16 +446,18 @@ def get_efficiency(heat_system, carrier, nodes, heating_efficiencies, costs): - For residential and services sectors, efficiency is mapped based on the nodes. - For other sectors, the default boiler efficiency is retrieved from the `costs` database. """ - - if heat_system.sector.value == "residential": + + if heat_system.value == "urban central": + boiler_costs_name = getattr(heat_system, f"{carrier}_boiler_costs_name") + efficiency = costs.at[boiler_costs_name, "efficiency"] + elif heat_system.sector.value == "residential": key = f"{carrier} residential space efficiency" efficiency = nodes.str[:2].map(heating_efficiencies[key]) elif heat_system.sector.value == "services": key = f"{carrier} services space efficiency" efficiency = nodes.str[:2].map(heating_efficiencies[key]) else: - boiler_costs_name = getattr(heat_system, f"{carrier}_boiler_costs_name") - efficiency = costs.at[boiler_costs_name, "efficiency"] + logger.warning(f"{heat_system} not defined.") return efficiency @@ -671,13 +673,13 @@ def add_heating_capacities_installed_before_baseyear( snakemake = mock_snakemake( "add_existing_baseyear", - configfiles="config/config.yaml", + configfiles="config/test/config.myopic.yaml", simpl="", - clusters="38", - ll="vopt", + clusters="5", + ll="v1.5", opts="", sector_opts="", - planning_horizons=2020, + planning_horizons=2030, ) configure_logging(snakemake) From b7bcf3d30204b9cc4c4518c283bc4542de4d9cca Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Wed, 11 Sep 2024 15:06:32 +0200 Subject: [PATCH 297/344] add energy totals year to params --- rules/solve_myopic.smk | 1 + rules/solve_perfect.smk | 1 + 2 files changed, 2 insertions(+) diff --git a/rules/solve_myopic.smk b/rules/solve_myopic.smk index f868a9973..6340787ad 100644 --- a/rules/solve_myopic.smk +++ b/rules/solve_myopic.smk @@ -10,6 +10,7 @@ rule add_existing_baseyear: existing_capacities=config_provider("existing_capacities"), costs=config_provider("costs"), heat_pump_sources=config_provider("sector", "heat_pump_sources"), + energy_totals_year=config_provider("energy", "energy_totals_year"), input: network=RESULTS + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", diff --git a/rules/solve_perfect.smk b/rules/solve_perfect.smk index 4db4e31af..f1ec9966e 100644 --- a/rules/solve_perfect.smk +++ b/rules/solve_perfect.smk @@ -8,6 +8,7 @@ rule add_existing_baseyear: existing_capacities=config_provider("existing_capacities"), costs=config_provider("costs"), heat_pump_sources=config_provider("sector", "heat_pump_sources"), + energy_totals_year=config_provider("energy", "energy_totals_year"), input: network=RESULTS + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", From a57e39b3e716a787785b088d647c9cbec2c27ce1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 11 Sep 2024 13:09:51 +0000 Subject: [PATCH 298/344] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/add_existing_baseyear.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index 53d8e3907..84d20b726 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -446,7 +446,7 @@ def get_efficiency(heat_system, carrier, nodes, heating_efficiencies, costs): - For residential and services sectors, efficiency is mapped based on the nodes. - For other sectors, the default boiler efficiency is retrieved from the `costs` database. """ - + if heat_system.value == "urban central": boiler_costs_name = getattr(heat_system, f"{carrier}_boiler_costs_name") efficiency = costs.at[boiler_costs_name, "efficiency"] From d9ab4940804c180d70442e5281012c53eaa441b8 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Wed, 11 Sep 2024 15:15:22 +0200 Subject: [PATCH 299/344] add release note --- doc/release_notes.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index e75d33440..7340fe316 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -10,6 +10,8 @@ Release Notes .. Upcoming Release +* Change the heating demand from final energy which includes losses in legacy equipment to thermal energy service based on JRC-IDEES. Efficiencies of existing heating capacities are lowered according to the conversion of final energy to thermal energy service. For overnight scenarios or future planning horizon this change leads to a reduction in heat supply. + * Updated district heating supply temperatures based on `Euroheat's DHC Market Outlook 2024`__ and `AGFW-Hauptbericht 2022 `__. `min_forward_temperature` and `return_temperature` (not given by Euroheat) are extrapolated based on German values. * Made the overdimensioning factor for heating systems specific for central/decentral heating, defaults to no overdimensionining for central heating and no changes to decentral heating compared to previous version. From 4524aca5d9a22f5e81d9408a55f090227478ed15 Mon Sep 17 00:00:00 2001 From: Lukas Trippe Date: Wed, 11 Sep 2024 16:31:19 +0200 Subject: [PATCH 300/344] fix: connection check failing (#1280) --- rules/common.smk | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/rules/common.smk b/rules/common.smk index ef518beb9..203fb9c06 100644 --- a/rules/common.smk +++ b/rules/common.smk @@ -115,20 +115,26 @@ def input_custom_extra_functionality(w): return [] -# Check if the workflow has access to the internet by trying to access the HEAD of specified url -def has_internet_access(url="www.zenodo.org") -> bool: - import http.client as http_client +def has_internet_access(url: str = "https://www.zenodo.org", timeout: int = 3) -> bool: + """ + Checks if internet connection is available by sending a HEAD request + to a reliable server like Google. + + Parameters: + - url (str): The URL to check for internet connection. Default is Google. + - timeout (int | float): The maximum time (in seconds) the request should wait. - # based on answer and comments from - # https://stackoverflow.com/a/29854274/11318472 - conn = http_client.HTTPConnection(url, timeout=5) # need access to zenodo anyway + Returns: + - bool: True if the internet is available, otherwise False. + """ try: - conn.request("HEAD", "/") - return True - except: + # Send a HEAD request to avoid fetching full response + response = requests.head(url, timeout=timeout, allow_redirects=True) + return response.status_code == 200 + except requests.ConnectionError: # (e.g., no internet, DNS issues) + return False + except requests.Timeout: # (e.g., slow or no network) return False - finally: - conn.close() def solved_previous_horizon(w): From e6d4dd67dcde507447dfb38e3206a80bab6e646f Mon Sep 17 00:00:00 2001 From: Lukas Trippe Date: Wed, 11 Sep 2024 16:49:31 +0200 Subject: [PATCH 301/344] fix: add validator config (#1273) --- config/test/config.validator.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 config/test/config.validator.yaml diff --git a/config/test/config.validator.yaml b/config/test/config.validator.yaml new file mode 100644 index 000000000..a7440deee --- /dev/null +++ b/config/test/config.validator.yaml @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: : 2017-2024 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: CC0-1.0 + +logging: + level: INFO + format: '%(asctime)s - %(levelname)s - %(name)s - %(message)s' + +run: + prefix: validation + +clustering: + temporal: + resolution_elec: 365H + resolution_sector: 365H From 65054ae903e4ac93f3d07d752483ed2a8821194c Mon Sep 17 00:00:00 2001 From: cpschau <124347782+cpschau@users.noreply.github.com> Date: Wed, 11 Sep 2024 17:20:00 +0200 Subject: [PATCH 302/344] Simplify integration of unsustainable solid biomass (#1275) * remove extra bus and link; remove redundant addition of oil carrier_bus * remove redundant addition of oil carrier buses * changed PyPSA requirement in envs/environment.yaml --- envs/environment.yaml | 2 +- scripts/prepare_sector_network.py | 23 ++--------------------- 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/envs/environment.yaml b/envs/environment.yaml index 0b56c1efc..bb6822e95 100644 --- a/envs/environment.yaml +++ b/envs/environment.yaml @@ -11,7 +11,7 @@ dependencies: - pip - atlite>=0.2.9 -- pypsa>=0.29 +- pypsa>=0.30.2 - linopy - dask diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 4a9c1be6b..58df65681 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2812,7 +2812,6 @@ def add_biomass(n, costs): ) if biomass_potentials.filter(like="unsustainable").sum().sum() > 0: - add_carrier_buses(n, "oil") # Create timeseries to force usage of unsustainable potentials e_max_pu = pd.DataFrame(1, index=n.snapshots, columns=spatial.gas.biogas) e_max_pu.iloc[-1] = 0 @@ -2830,14 +2829,6 @@ def add_biomass(n, costs): e_max_pu=e_max_pu, ) - n.madd( - "Bus", - spatial.biomass.nodes_unsustainable, - location=spatial.biomass.locations, - carrier="unsustainable solid biomass", - unit="MWh_LHV", - ) - e_max_pu = pd.DataFrame( 1, index=n.snapshots, columns=spatial.biomass.nodes_unsustainable ) @@ -2846,7 +2837,7 @@ def add_biomass(n, costs): n.madd( "Store", spatial.biomass.nodes_unsustainable, - bus=spatial.biomass.nodes_unsustainable, + bus=spatial.biomass.nodes, carrier="unsustainable solid biomass", e_nom=unsustainable_solid_biomass_potentials_spatial, marginal_cost=costs.at["fuelwood", "fuel"], @@ -2855,16 +2846,6 @@ def add_biomass(n, costs): e_max_pu=e_max_pu, ) - n.madd( - "Link", - spatial.biomass.nodes_unsustainable, - bus0=spatial.biomass.nodes_unsustainable, - bus1=spatial.biomass.nodes, - carrier="unsustainable solid biomass", - efficiency=1, - p_nom=unsustainable_solid_biomass_potentials_spatial, - ) - n.madd( "Bus", spatial.biomass.bioliquids, @@ -3024,7 +3005,7 @@ def add_biomass(n, costs): n.madd( "Generator", spatial.biomass.nodes_unsustainable, - bus=spatial.biomass.nodes_unsustainable, + bus=spatial.biomass.nodes, carrier="unsustainable solid biomass", p_nom=10000, marginal_cost=costs.at["fuelwood", "fuel"] From 5316655c55b0198d90d333ba4ed253e28487c104 Mon Sep 17 00:00:00 2001 From: Lukas Trippe Date: Wed, 11 Sep 2024 18:32:39 +0200 Subject: [PATCH 303/344] add dependabot (#1282) --- .github/dependabot.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..f89e903c7 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +# dependabot +# Ref: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file +# ------------------------------------------------------------------------------ +version: 2 +updates: +- package-ecosystem: github-actions + directory: / + schedule: + interval: daily + groups: + # open a single pull-request for all GitHub actions updates + github-actions: + patterns: + - '*' From 643baf780e6b0a665bac658ab5351889776b521c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Sep 2024 06:44:21 +0200 Subject: [PATCH 304/344] Bump the github-actions group with 4 updates (#1283) Bumps the github-actions group with 4 updates: [actions/checkout](https://github.com/actions/checkout), [actions/cache](https://github.com/actions/cache), [actions/upload-artifact](https://github.com/actions/upload-artifact) and [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request). Updates `actions/checkout` from 3 to 4 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) Updates `actions/cache` from 3 to 4 - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v3...v4) Updates `actions/upload-artifact` from 4.3.0 to 4.4.0 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4.3.0...v4.4.0) Updates `peter-evans/create-pull-request` from 6 to 7 - [Release notes](https://github.com/peter-evans/create-pull-request/releases) - [Commits](https://github.com/peter-evans/create-pull-request/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: peter-evans/create-pull-request dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 6 +++--- .github/workflows/update-fixed-env.yaml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bad6039f9..6664ad902 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -47,7 +47,7 @@ jobs: shell: bash -l {0} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup secrets run: | @@ -73,7 +73,7 @@ jobs: echo "WEEK=$(date +'%Y%U')" >> $GITHUB_ENV - name: Cache data and cutouts folders - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | data @@ -84,7 +84,7 @@ jobs: run: ./test.sh - name: Upload artifacts - uses: actions/upload-artifact@v4.3.0 + uses: actions/upload-artifact@v4.4.0 with: name: resources-results path: | diff --git a/.github/workflows/update-fixed-env.yaml b/.github/workflows/update-fixed-env.yaml index f66890aa1..3bb0991b2 100644 --- a/.github/workflows/update-fixed-env.yaml +++ b/.github/workflows/update-fixed-env.yaml @@ -30,7 +30,7 @@ jobs: mamba env export --file envs/environment.fixed.yaml --no-builds - name: Create Pull Request - uses: peter-evans/create-pull-request@v6 + uses: peter-evans/create-pull-request@v7 with: token: ${{ secrets.GITHUB_TOKEN }} branch: update-environment-fixed From e8e0833e3ab568623baf31a79d5d491dd4b04d12 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 12 Sep 2024 09:43:55 +0200 Subject: [PATCH 305/344] Document nearly all data sources in `doc/data_sources.rst` (#1284) * initial data-sources page * continue with data sources * continue with data sources * finalize data sources documentation * correct DIW link --- .github/pull_request_template.md | 7 +- README.md | 2 +- data/heat_load_profile.csv | 25 -- doc/configtables/licenses-sector.csv | 24 -- doc/configtables/licenses.csv | 12 - doc/data_sources.rst | 579 +++++++++++++++++++++++++++ doc/index.rst | 1 + doc/licenses.rst | 22 +- doc/release_notes.rst | 2 + 9 files changed, 588 insertions(+), 86 deletions(-) delete mode 100644 data/heat_load_profile.csv delete mode 100644 doc/configtables/licenses-sector.csv delete mode 100644 doc/configtables/licenses.csv create mode 100644 doc/data_sources.rst diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index f70a2cde3..8c340b7b7 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -5,9 +5,10 @@ Closes # (if applicable). ## Checklist -- [ ] I tested my contribution locally and it seems to work fine. +- [ ] I tested my contribution locally and it works as intended. - [ ] Code and workflow changes are sufficiently documented. - [ ] Changed dependencies are added to `envs/environment.yaml`. -- [ ] Changes in configuration options are added in all of `config.default.yaml`. -- [ ] Changes in configuration options are also documented in `doc/configtables/*.csv`. +- [ ] Changes in configuration options are added in `config/config.default.yaml`. +- [ ] Changes in configuration options are documented in `doc/configtables/*.csv`. +- [ ] Sources of newly added data are documented in `doc/data_sources.rst`. - [ ] A release note `doc/release_notes.rst` is added. diff --git a/README.md b/README.md index 5c918c9a8..25e8800e7 100644 --- a/README.md +++ b/README.md @@ -105,4 +105,4 @@ We strongly welcome anyone interested in contributing to this project. If you ha The code in PyPSA-Eur is released as free software under the [MIT License](https://opensource.org/licenses/MIT), see [`doc/licenses.rst`](doc/licenses.rst). However, different licenses and terms of use may apply to the various -input data. +input data, see [`doc/data_sources.rst`](doc/data_sources.rst). diff --git a/data/heat_load_profile.csv b/data/heat_load_profile.csv deleted file mode 100644 index f1e13ea40..000000000 --- a/data/heat_load_profile.csv +++ /dev/null @@ -1,25 +0,0 @@ -hour,residential space weekday,residential space weekend,residential water weekday,residential water weekend,services space weekday,services space weekend,services water weekday,services water weekend -0,0.9181438689,0.9421512708,1,1,0.9181438689,0.9421512708,1,1 -1,0.9172359071,0.9400891069,1,1,0.9172359071,0.9400891069,1,1 -2,0.9269464481,0.9461062015,1,1,0.9269464481,0.9461062015,1,1 -3,0.9415047932,0.9535084941,1,1,0.9415047932,0.9535084941,1,1 -4,0.9656299507,0.9651094993,1,1,0.9656299507,0.9651094993,1,1 -5,1.0221166443,0.9834676747,1,1,1.0221166443,0.9834676747,1,1 -6,1.1553090493,1.0124171051,1,1,1.1553090493,1.0124171051,1,1 -7,1.2093411031,1.0446615927,1,1,1.2093411031,1.0446615927,1,1 -8,1.1470295942,1.088203419,1,1,1.1470295942,1.088203419,1,1 -9,1.0877191341,1.1110334576,1,1,1.0877191341,1.1110334576,1,1 -10,1.0418327372,1.0926752822,1,1,1.0418327372,1.0926752822,1,1 -11,1.0062977133,1.055488209,1,1,1.0062977133,1.055488209,1,1 -12,0.9837030359,1.0251266112,1,1,0.9837030359,1.0251266112,1,1 -13,0.9667570278,0.9990015154,1,1,0.9667570278,0.9990015154,1,1 -14,0.9548320932,0.9782897278,1,1,0.9548320932,0.9782897278,1,1 -15,0.9509232061,0.9698167237,1,1,0.9509232061,0.9698167237,1,1 -16,0.9636973319,0.974288587,1,1,0.9636973319,0.974288587,1,1 -17,0.9799372563,0.9886456216,1,1,0.9799372563,0.9886456216,1,1 -18,1.0046501848,1.0084159643,1,1,1.0046501848,1.0084159643,1,1 -19,1.0079452419,1.0171243296,1,1,1.0079452419,1.0171243296,1,1 -20,0.9860566481,0.9994722379,1,1,0.9860566481,0.9994722379,1,1 -21,0.9705228074,0.982761591,1,1,0.9705228074,0.982761591,1,1 -22,0.9586485819,0.9698167237,1,1,0.9586485819,0.9698167237,1,1 -23,0.9335023778,0.9515079292,1,1,0.9335023778,0.9515079292,1,1 diff --git a/doc/configtables/licenses-sector.csv b/doc/configtables/licenses-sector.csv deleted file mode 100644 index 8c721260d..000000000 --- a/doc/configtables/licenses-sector.csv +++ /dev/null @@ -1,24 +0,0 @@ -description,file/folder,licence,source -JRC IDEES database,jrc-idees-2015/,CC BY 4.0,https://ec.europa.eu/jrc/en/potencia/jrc-idees -JRC ENSPRESO biomass potentials,remote,CC BY 4.0,https://data.jrc.ec.europa.eu/dataset/74ed5a04-7d74-4807-9eab-b94774309d9f -EEA emission statistics,eea/UNFCCC_v23.csv,EEA standard re-use policy,https://www.eea.europa.eu/data-and-maps/data/national-emissions-reported-to-the-unfccc-and-to-the-eu-greenhouse-gas-monitoring-mechanism-16 -Eurostat Energy Balances,eurostat-energy_balances-*/,Eurostat,https://ec.europa.eu/eurostat/web/energy/data/energy-balances -Swiss energy statistics from Swiss Federal Office of Energy,switzerland-sfoe/,unknown,http://www.bfe.admin.ch/themen/00526/00541/00542/02167/index.html?dossier_id=02169 -BASt emobility statistics,emobility/,unknown,http://www.bast.de/DE/Verkehrstechnik/Fachthemen/v2-verkehrszaehlung/Stundenwerte.html?nn=626916 -BDEW heating profile,heat_load_profile_BDEW.csv,unknown,https://github.com/oemof/demandlib -co2 budgets,co2_budget.csv,CC BY 4.0,https://arxiv.org/abs/2004.11009 -existing heating potentials,existing_infrastructure/existing_heating_raw.csv,unknown,https://energy.ec.europa.eu/publications/mapping-and-analyses-current-and-future-2020-2030-heatingcooling-fuel-deployment-fossilrenewables-1_en -IRENA existing VRE capacities,existing_infrastructure/{solar|onwind|offwind}_capcity_IRENA.csv,unknown,https://www.irena.org/Statistics/Download-Data -USGS ammonia production,myb1-2017-nitro.xls,unknown,https://www.usgs.gov/centers/nmic/nitrogen-statistics-and-information -hydrogen salt cavern potentials,h2_salt_caverns_GWh_per_sqkm.geojson,CC BY 4.0,https://doi.org/10.1016/j.ijhydene.2019.12.161 https://doi.org/10.20944/preprints201910.0187.v1 -international port trade volumes,attributed_ports.json,CC BY 4.0,https://datacatalog.worldbank.org/search/dataset/0038118/Global---International-Ports -hotmaps industrial site database,Industrial_Database.csv,CC BY 4.0,https://gitlab.com/hotmaps/industrial_sites/industrial_sites_Industrial_Database -Hotmaps building stock data,data_building_stock.csv,CC BY 4.0,https://gitlab.com/hotmaps/building-stock -U-values Poland,u_values_poland.csv,unknown,https://data.europa.eu/euodp/de/data/dataset/building-stock-observatory -Floor area missing in hotmaps building stock data,floor_area_missing.csv,unknown,https://data.europa.eu/euodp/de/data/dataset/building-stock-observatory -Comparative level investment,comparative_level_investment.csv,Eurostat,https://ec.europa.eu/eurostat/statistics-explained/index.php?title=Comparative_price_levels_for_investment -Electricity taxes,electricity_taxes_eu.csv,Eurostat,https://appsso.eurostat.ec.europa.eu/nui/show.do?dataset=nrg_pc_204&lang=en -Building topologies and corresponding standard values,tabula-calculator-calcsetbuilding.csv,unknown,https://episcope.eu/fileadmin/tabula/public/calc/tabula-calculator.xlsx -Retrofitting thermal envelope costs for Germany,retro_cost_germany.csv,unknown,https://www.iwu.de/forschung/handlungslogiken/kosten-energierelevanter-bau-und-anlagenteile-bei-modernisierung/ -District heating most countries,jrc-idees-2015/,CC BY 4.0,https://ec.europa.eu/jrc/en/potencia/jrc-idees,, -District heating missing countries,district_heat_share.csv,unknown,https://www.euroheat.org/knowledge-hub/country-profiles,, diff --git a/doc/configtables/licenses.csv b/doc/configtables/licenses.csv deleted file mode 100644 index d1fa4aa89..000000000 --- a/doc/configtables/licenses.csv +++ /dev/null @@ -1,12 +0,0 @@ -"Files","BY","NC","SA","Mark Changes",Detail -"corine/*","x",,,"x",https://land.copernicus.eu/pan-european/corine-land-cover/clc-2012?tab=metadata -"eez/*","x","x","x",,http://www.marineregions.org/disclaimer.php -"natura/*","x",,,,https://www.eea.europa.eu/data-and-maps/data/natura-10#tab-metadata -"naturalearth/*",,,,,http://www.naturalearthdata.com/about/terms-of-use/ -"NUTS_2013 _60M_SH/*","x","x",,"x",https://ec.europa.eu/eurostat/web/gisco/geodata/reference-data/administrative-units-statistical-units -"cantons.csv","x",,"x",,https://en.wikipedia.org/wiki/Data_codes_for_Switzerland -"gebco/GEBCO_2014_2D.nc","x",,,,https://www.gebco.net/data_and_products/gridded_bathymetry_data/documents/gebco_2014_historic.pdf -"hydro_capacities.csv","x",,,, -"je-e-21.03.02.xls","x","x",,,https://www.bfs.admin.ch/bfs/en/home/fso/swiss-federal-statistical-office/terms-of-use.html -"nama_10r_3 gdp.tsv.gz","x",,,"x",https://ec.europa.eu/eurostat/about/policies/copyright -"nama_10r_3 popgdp.tsv.gz","x",,,"x",https://ec.europa.eu/eurostat/about/policies/copyright diff --git a/doc/data_sources.rst b/doc/data_sources.rst new file mode 100644 index 000000000..ecd72458d --- /dev/null +++ b/doc/data_sources.rst @@ -0,0 +1,579 @@ +.. + SPDX-FileCopyrightText: 2024 The PyPSA-Eur Authors + + SPDX-License-Identifier: CC-BY-4.0 + +########################################## +Data Sources +########################################## + +PyPSA-Eur is combiled from a variety of data sources. The following table provides an +overview of the data sources used in PyPSA-Eur. Different licenses apply to the +data sources. + +Zenodo data bundle +======================= + +Data in this section is downloaded and extracted from the Zenodo data bundle +(https://zenodo.org/records/12760663). Files included in the data bundle are too +large to be placed directly in the repository, have been reduced in spatial +scope to reduce file size, or are not provided through stable URLs elsewhere. + +``data/bundle/je-e-21.03.02.xls`` + +- **Source:** Swiss Federal Statistics Office +- **Link:** https://www.bfs.admin.ch/bfs/en/home/news/whats-new.assetdetail.7786557.html#context-sidebar +- **License:** `custom (OPEN BY ASK) `__ +- **Description:** Population and GDP data for Swiss Cantons. + +``data/bundle/NUTS_2013_60M_SH`` + +- **Source:** GISCO +- **Link:** https://gisco-services.ec.europa.eu/distribution/v2/nuts/download/ +- **License:** `custom `__ +- **Description:** Europe's NUTS administrative regions. + +``data/bundle/nama_10r_3popgdp.tsv.gz`` + +- **Source:** Eurostat +- **Link:** https://ec.europa.eu/eurostat/databrowser/view/NAMA_10R_3POPGDP/default/table?lang=en +- **License:** `custom `__ +- **Description:** Average annual population to calculate regional GDP data (thousand persons) by NUTS 3 regions. + +``data/bundle/nama_10r_3gdp.tsv.gz`` + +- **Source:** Eurostat +- **Link:** https://ec.europa.eu/eurostat/databrowser/view/nama_10r_3gdp/default/table?lang=en +- **License:** `custom `__ +- **Description:** Gross domestic product (GDP) at current market prices by NUTS 3 regions. + +``data/bundle/corine`` + +- **Source:** European Environment Agency (EEA) +- **Link:** https://sdi.eea.europa.eu/catalogue/copernicus/api/records/a84ae124-c5c5-4577-8e10-511bfe55cc0d +- **License:** `custom `__ +- **Description:** CORINE Land Cover (CLC) database. + +``data/bundle/eea`` + +- **Source:** European Environment Agency (EEA) +- **Link:** https://www.eea.europa.eu/en/datahub/datahubitem-view/3b7fe76c-524a-439a-bfd2-a6e4046302a2 +- **License:** CC-BY 4.0 (`reference `__) +- **Description:** Total GHG emissions and removals in the EU. + +``data/bundle/nuts`` + +- **Source:** GISCO +- **Link:** https://gisco-services.ec.europa.eu/distribution/v2/nuts/download/ +- **License:** `custom `__ +- **Description:** Europe's NUTS administrative regions. + +``data/bundle/emobility`` + +- **Source:** German Federal Highway Research Institute (BASt) +- **Link:** https://www.bast.de/DE/Verkehrstechnik/Fachthemen/v2-verkehrszaehlung/zaehl_node.html +- **License:** CC-BY 4.0 (`reference `__) +- **Description:** Contains data from permanent automatic counting stations on highways and federal roads in Germany. + +``data/bundle/h2_salt_caverns_GWh_per_sqkm.geojson`` + +- **Source:** Dilara Gulcin Caglayan, Nikolaus Weber, Heidi U. Heinrichs, Jochen + Linßen, Martin Robinius, Peter A. Kukla, Detlef Stolten, Technical potential + of salt caverns for hydrogen storage in Europe, International Journal of + Hydrogen Energy, Volume 45, Issue 11, 2020, Pages 6793-6805. +- **Link:** https://doi.org/10.1016/j.ijhydene.2019.12.161 +- **License:** CC-BY 4.0 +- **Description:** Contains geological hydrogen storage potentials for Europe. + +``data/bundle/natura`` + +- **Source:** European Environment Agency (EEA) +- **Link:** https://www.eea.europa.eu/en/datahub/datahubitem-view/6fc8ad2d-195d-40f4-bdec-576e7d1268e4 +- **License:** CC-BY 4.0 (`reference `__) +- **Description:** Natura 2000 natural protection areas. + +``data/bundle/gebco`` + +- **Source:** GEBCO +- **Link:** https://www.gebco.net/data_and_products/gridded_bathymetry_data/version_20141103/ +- **License:** CC0 (`reference `__) +- **Description:** Bathymetric dataset (2014 version). + +``data/bundle/GDP_per_capita_PPP_1990_2015_v2.nc`` + +- **Source:** Kummu, M., Taka, M. & Guillaume, J. Gridded global datasets for + Gross Domestic Product and Human Development Index over 1990-2015. Sci Data 5, + 180004 (2018). https://doi.org/10.1038/sdata.2018.4 +- **Link:** https://datadryad.org/stash/dataset/doi:10.5061/dryad.dk1j0 +- **License:** CC0 (`reference `__) +- **Description:** Gridded GDP data. + +``data/bundle/ppp_2013_1km_Aggregated.tif`` + +- **Source:** WorldPop (www.worldpop.org - School of Geography and Environmental + Science, University of Southampton; Department of Geography and Geosciences, + University of Louisville; Departement de Geographie, Universite de Namur) and + Center for International Earth Science Information Network (CIESIN), Columbia + University (2018). Global High Resolution Population Denominators Project - + Funded by The Bill and Melinda Gates Foundation (OPP1134076). + https://dx.doi.org/10.5258/SOTON/WP00647 +- **Link:** https://hub.worldpop.org/doi/10.5258/SOTON/WP00647 +- **License:** CC-BY 4.0 (`reference `__) +- **Description:** Gridded population data. + + +Specific retrieval rules +======================== + +Data in this section is retrieved and extracted in rules specified in ``rules/retrieve.smk``. + +``data/ENSPRESO_BIOMASS.xlsx`` + +- **Source:** European Commission Joint Research Centre (JRC) +- **Link:** https://data.jrc.ec.europa.eu/dataset/74ed5a04-7d74-4807-9eab-b94774309d9f +- **License:** CC-BY 4.0 +- **Description:** Contains biomass potentials for Europe. + +``data/complete_map_2020_unit_Mt.geojson`` + +- **Source:** SETIS +- **Link:** https://setis.ec.europa.eu/european-co2-storage-database_en, processed with https://github.com/ericzhou571/Co2Storage +- **License:** `various `__ +- **Description:** European CO2 storage database CO2StoP. + +``data/myb1-2022-nitro-ert.xlsx`` + +- **Source:** United States Geological Survey (USGS) +- **Link:** https://www.usgs.gov/centers/national-minerals-information-center/nitrogen-statistics-and-information +- **License:** CC0 (`reference `__) +- **Description:** Statistics and information on the worldwide supply of, demand for, and flow of the mineral commodity nitrogen. + +``data/Industrial_Database.csv`` + +- **Source:** Simon Pezzutto, Stefano Zambotti, Silvia Croce, Pietro Zambelli, + Giulia Garegnani, Chiara Scaramuzzino, Ramón Pascual Pascuas, Alyona + Zubaryeva, Franziska Haas, Dagmar Exner (EURAC), Andreas Mueller (e-think), + Michael Hartner (TUW), Tobias Fleiter, Anna-Lena Klingler, Matthias Kuehnbach, + Pia Manz, Simon Marwitz, Matthias Rehfeldt, Jan Steinbach, Eftim Popovski + (Fraunhofer ISI) Reviewed by Lukas Kranzl, Sara Fritz (TUW) + Hotmaps Project, D2.3 WP2 Report - Open Data Set for the EU28, 2018 + https://www.hotmaps-project.eu +- **Link:** https://gitlab.com/hotmaps/industrial_sites/industrial_sites_Industrial_Database +- **License:** CC-BY 4.0 (`reference `__) +- **Description:** Contains georeferenced industrial sites of energy-intensive + industry sectors, together with GHG-emissions, production capacity, fuel + demand and excess heat potentials calculated from emission and production + data. + +``data/eurostat/Balances-April2023`` + +- **Source:** Eurostat +- **Link:** https://ec.europa.eu/eurostat/documents/38154/4956218/Balances-April2023.zip +- **License:** CC-BY 4.0 (`reference `__) +- **Description:** Contains energy balances for Europe. + +``data/eurostat/eurostat-household_energy_balances-february_2024.csv`` + +- **Source:** Eurostat +- **Link:** https://ec.europa.eu/eurostat/databrowser-backend/api/extraction/1.0/LIVE/false/sdmx/csv/nrg_d_hhq__custom_11480365?startPeriod=2013&endPeriod=2022&i&compressed=true +- **License:** CC-BY 4.0 (`reference `__) +- **Description:** Contains household energy balances for Europe. + +``data/jrc-idees-2021`` + +- **Source:** Rózsai, M., Jaxa-Rozen, M., Salvucci, R., Sikora, P., Tattini, J. + and Neuwahl, F., JRC-IDEES-2021: the Integrated Database of the European + Energy System - Data update and technical documentation, Publications Office + of the European Union, Luxembourg, 2024, doi:10.2760/614599, JRC137809. +- **Link:** https://jeodpp.jrc.ec.europa.eu/ftp/jrc-opendata/JRC-IDEES/JRC-IDEES-2021_v1 +- **License:** CC-BY 4.0 (`reference `__) +- **Description:** Contains more granular energy balances for Europe. + +``data/gas_network`` + +- **Source:** Jan Diettrich, Adam Pluta, & Wided Medjroubi. (2021). SciGRID_gas + IGGIELGN (1.1.2) [Data set]. Zenodo. https://doi.org/10.5281/zenodo.4767098 +- **Link:** https://zenodo.org/records/4767098 +- **License:** CC-BY 4.0 (`reference `__) +- **Description:** Contains gas infrastructure data. + +``data/electricity_demand_raw.csv`` + +- **Source:** Open Power System Data (OPSD) from ENTSO-E Transparency +- **Link:** + https://data.open-power-system-data.org/time_series/2019-06-05/time_series_60min_singleindex.csv + and https://data.open-power-system-data.org/time_series/2020-10-06/time_series_60min_singleindex.csv +- **License:** unknown +- **Description:** Contains country-level electricity demand time series. + +``data/load_synthetic_raw.csv`` + +- **Source:** Frysztacki, M., van der Most, L., & Neumann, F. (2024). + Interannual Electricity Demand Calculator [Data set]. Zenodo. + https://doi.org/10.5281/zenodo.10820928 +- **Link:** https://zenodo.org/records/10820928 +- **License:** CC-BY 4.0 +- **Description:** Contains synthetic country-level electricity demand time series. + +``data/shipdensity_global.zip`` + +- **Source:** World Bank +- **Link:** https://datacatalog.worldbank.org/search/dataset/0037580/Global-Shipping-Traffic-Density +- **License:** CC-BY 4.0 (`reference `__) +- **Description:** Global shipping traffic density. + +``data/Copernicus_LC100_global_v3.0.1_2019-nrt_Discrete-Classification-map_EPSG-4326.tif`` + +- **Source:** Marcel Buchhorn, Bruno Smets, Luc Bertels, Bert De Roo, Myroslava + Lesiv, Nandin-Erdene Tsendbazar, Martin Herold, & Steffen Fritz. (2020). + Copernicus Global Land Service: Land Cover 100m: collection 3: epoch 2019: + Globe (V3.0.1) [Data set]. Zenodo. https://doi.org/10.5281/zenodo.3939050 +- **Link:** https://zenodo.org/records/3939050 +- **License:** CC-BY 4.0 (`reference `__) +- **Description:** Contains rastered land cover and land use data. + +``data/LUISA_basemap_020321_50m.tif`` + +- **Source:** European Commission Joint Research Centre (JRC) +- **Link:** https://jeodpp.jrc.ec.europa.eu/ftp/jrc-opendata/LUISA/EUROPE/Basemaps/LandUse/2018/LATEST/ +- **License:** CC-BY 4.0 (`reference `__) +- **Description:** Contains rastered land cover and land use data. + +``data/eez`` + +- **Source:** Marine Regions +- **Link:** https://www.marineregions.org/download_file.php +- **License:** CC-BY-NC-SA +- **Description:** Contains offshore exclusive economic zones. + +``data/worldbank`` + +- **Source:** World Bank +- **Link:** https://data.worldbank.org/indicator/SP.URB.TOTL.IN.ZS +- **License:** CC-BY 4.0 +- **Description:** Contains share of urban population by country. + +``data/naturalearth`` + +- **Source:** Natural Earth +- **Link:** https://www.naturalearthdata.com/downloads/10m-cultural-vectors/ +- **License:** CC0 (`reference `__) +- **Description:** Country shapes, using point-of-view (POV) variant of Germany so that Crimea is included. + +``data/gem/Europe-Gas-Tracker-2024-05.xlsx`` + +- **Source:** Global Energy Monitor +- **Link:** https://globalenergymonitor.org/projects/global-steel-plant-tracker/ +- **License:** CC-BY 4.0 (`reference `__) +- **Description:** Covers methane gas pipelines, LNG terminals, oil and gas-fired power plants, and methane gas extraction sites. + +``data/gem/Global-Steel-Plant-Tracker-April-2024-Standard-Copy-V1.xlsx`` + +- **Source:** Global Energy Monitor +- **Link:** https://globalenergymonitor.org/projects/global-steel-plant-tracker/ +- **License:** CC-BY 4.0 (`reference `__) +- **Description:** The Global Steel Plant Tracker (GSPT) provides information on + global crude iron and steel production plants, and includes every plant + currently operating with a capacity of five hundred thousand tonnes per year + (ttpa) or more of crude iron or steel. + +``data/WDPA.gpkg`` + +- **Source:** UNEP-WCMC and IUCN (2024), Protected Planet: The World Database on + Protected Areas (WDPA) [Online], September 2024, Cambridge, UK: UNEP-WCMC and + IUCN. Available at: www.protectedplanet.net. +- **Link:** https://www.protectedplanet.net/en/thematic-areas/wdpa +- **License:** `custom `__ +- **Description:** Contains global protected areas. + +``data/WDPA_WDOECM_marine.gpkg`` + +- **Source:** UNEP-WCMC and IUCN (2024), Protected Planet: The World Database on + Protected Areas (WDPA) and World Database on Other Effective Area-based + Conservation Measures (WD-OECM) [Online], September 2024, Cambridge, UK: + UNEP-WCMC and IUCN. Available at: www.protectedplanet.net. +- **Link:** https://www.protectedplanet.net/en/thematic-areas/marine-protected-areas +- **License:** `custom `__ +- **Description:** Contains global protected marine areas. + +``data/osm-prebuilt`` + +- **Source:** OpenStreetMap; Xiong, B., Neumann, F., & Brown, T. (2024). + Prebuilt Electricity Network for PyPSA-Eur based on OpenStreetMap Data (0.3) + [Data set]. Zenodo. https://doi.org/10.5281/zenodo.13358976 +- **Link:** https://zenodo.org/records/13358976 +- **License:** ODbL (`reference `) +- **Description:** Pre-built data of high-voltage transmission grid in Europe from OpenStreetMap. + +``data/osm-raw`` + +- **Source:** OpenStreetMap via Overpass API +- **Link:** https://overpass-api.de/api/interpreter +- **License:** ODbL +- **Description:** Data of high-voltage transmission grid in Europe from OpenStreetMap. + +``cutouts`` + +- **Source:** `ERA5 + `__ + and `SARAH-3 `__ +- **Link:** https://zenodo.org/records/12791128 +- **License:** CC-BY 4.0 +- **Description:** Contains weather data cutouts for Europe to read in with ``atlite``. + +``resources/costs_{year}.csv`` + +- **Source:** various, mostly compiled from Danish Energy Agency (DEA) + `Technology Catalogues + `__. +- **Link:** https://github.com/PyPSA/technology-data +- **License:** GPL-3.0 +- **Description:** Contains technology data for different years such as costs, efficiencies, and lifetimes. + +``resources/powerplants.csv`` + +- **Source:** F. Gotzens, H. Heinrichs, J. Hörsch, and F. Hofmann, Performing + energy modelling exercises in a transparent way - The issue of data quality in + power plant databases, Energy Strategy Reviews, vol. 23, pp. 1-12, Jan. 2019. + https://doi.org/10.1016/j.esr.2018.11.004 +- **Link:** https://github.com/PyPSA/powerplantmatching +- **License:** GPL-3.0 +- **Description:** Contains matched dataset of powerplants in Europe. + + +Repository +========== + +Data in this section is included in the PyPSA-Eur repository in the ``data`` folder. + +``data/entsoegridkit`` + +- **Source:** ENTSO-E +- **Link:** https://www.entsoe.eu/data/map/, extracted with https://github.com/PyPSA/GridKit/tree/master/entsoe +- **License:** unknown +- **Description:** Data of high-voltage transmission grid in Europe from ENTSO-E. + +``data/existing_infrastructure`` + +- **Source:** European Commission DG ENER; Mapping and analyses of the current and future (2020 - 2030) heating/cooling fuel deployment +- **Link:** https://energy.ec.europa.eu/publications/mapping-and-analyses-current-and-future-2020-2030-heatingcooling-fuel-deployment-fossilrenewables-1_en +- **License:** CC-BY 4.0 (`reference `__) +- **Description:** Contains country-level data on existing heating infrastructure, i.e. gas, oil, coal boilers, resistive heaters, air- and ground-sourced heat pumps. + +``data/retro/comparative_level_investment.csv`` + +- **Source:** Eurostat +- **Link:** https://ec.europa.eu/eurostat/statistics-explained/index.php?title=Comparative_price_levels_for_investment +- **License:** `custom `__ +- **Description:** Contains data on comparative price levels for investment in Europe. + +``data/retro/data_building_stock.csv`` + +- **Source:** Simon Pezzutto, Stefano Zambotti, Silvia Croce, Pietro Zambelli, + Giulia Garegnani, Chiara Scaramuzzino, Ramón Pascual Pascuas, Alyona + Zubaryeva, Franziska Haas, Dagmar Exner (EURAC), Andreas Müller (e-think), + Michael Hartner (TUW), Tobias Fleiter, Anna-Lena Klingler, Matthias Kühnbach, + Pia Manz, Simon Marwitz, Matthias Rehfeldt, Jan Steinbach, Eftim Popovski + (Fraunhofer ISI) Reviewed by Lukas Kranzl, Sara Fritz (TUW) Hotmaps Project, + D2.3 WP2 Report - Open Data Set for the EU28, 2018 www.hotmaps-project.eu +- **Link:** https://gitlab.com/hotmaps/building-stock +- **License:** CC-BY 4.0 +- **Description:** Contains data on European building stock. + +``data/retro/electricity_taxes_eu.csv`` + +- **Source:** Eurostat +- **Link:** https://ec.europa.eu/eurostat/databrowser/view/NRG_PC_204/default/table?lang=en +- **License:** `custom `__ +- **Description:** Electricity prices for household consumers. + +``data/retro/{floor_area_missing,u_values_poland}.csv`` + +- **Source:** EU Building Stock Observatory +- **Link:** https://data.europa.eu/euodp/de/data/dataset/building-stock-observatory +- **License:** `custom `__ +- **Description:** The EU Building Stock Observatory monitors the energy + performance of buildings across Europe. It assesses improvements in the energy + efficiency of buildings and the impact of this on the actual energy + consumption of the buildings sector overall. + +``data/retro/retro_cost_germany.csv`` + +- **Source:** Institut Wohnen und Umwelt (IWU) +- **Link:** https://www.iwu.de/forschung/handlungslogiken/kosten-energierelevanter-bau-und-anlagenteile-bei-modernisierung/ +- **License:** unknown +- **Description:** Contains thermal envelop costs for retrofitting buildings in + Germany. + +``data/retro/window_assumptions.csv`` + +- **Source:** ifeu, Fraunhofer IEE and Consentec (2018): Building sector + Efficiency: A crucial Component of the Energy Transition. A study commissioned + by Agora Energiewende. +- **Link:** https://www.agora-energiewende.de/en/publications/building-sector-efficiency-a-crucial-component-of-the-energy-transition/ +- **License:** unknown +- **Description:** Contains data on physical parameters of double- and triple-glazed windows. + +``data/transmission_projects/nep`` + +- **Source:** German Federal Network Agency (Bundesnetzagentur, BNetzA) +- **Link:** https://data.netzausbau.de/2037-2023/NEP/NEP_2037_2045_Bestaetigung.pdf +- **License:** unknown +- **Description:** Contains transmission projects in Europe from German network development plan (Netzentwicklungsplan). + +``data/transmission_projects/tyndp2020`` + +- **Source:** ENTSO-E +- **Link:** https://tyndp2020-project-platform.azurewebsites.net/projectsheets +- **License:** unknown +- **Description:** Contains transmission projects in Europe from ENTSO-E Ten Year Network Development Plan (TYNDP). + +``data/ammonia_plants.csv`` + +- **Source:** manually collected, mostly from ICIS +- **Link:** https://www.icis.com/explore/resources/news/2023/01/18/10846094/insight-poor-demand-high-costs-stifle-europe-industry-despite-falling-gas-prices/ +- **License:** CC-BY 4.0 (for compiled dataset) +- **Description:** Locations and production capacities of ammonia plants in Europe. + +``data/attributed_ports.json`` + +- **Source:** World Bank +- **Link:** https://datacatalog.worldbank.org/search/dataset/0038118/Global---International-Ports +- **License:** CC-BY 4.0 (`reference `__) +- **Description:** International ports with attributes describing name, port functions, total capacity and location. + +``data/cement_plants-noneu.csv`` + +- **Source:** manually collected, mostly from USGS +- **Link:** https://www.usgs.gov/centers/national-minerals-information-center/international-minerals-statistics-and-information +- **License:** CC0 (`reference `__) +- **Description:** Contains energy balances for Europe. + +``data/ch_cantons.csv`` + +- **Source:** Wikipedia +- **Link:** https://en.wikipedia.org/wiki/Data_codes_for_Switzerland +- **License:** CC-BY-SA 4.0 +- **Description:** Contains NUTS codes for regions in Switzerland. + +``data/ch_industrial_production_per_subsector.csv`` + +- **Source:** Swiss Federal Office of Energy (SFOE) +- **Link:** https://pubdb.bfe.admin.ch/de/publication/download/11817 +- **License:** `custom `__ +- **Description:** Contains energy consumption in industry and the service sector in Switzerland. + +``data/district_heat_share.csv`` + +- **Source:** Euroheat & Power +- **Link:** https://www.euroheat.org/knowledge-hub/country-profiles +- **License:** unknown +- **Description:** Contains district heating shares for European countries. + +``data/egs_costs.json`` + +- **Source:** Arman Aghahosseini, Christian Breyer, From hot rock to useful + energy: A global estimate of enhanced geothermal systems potential, Applied + Energy, Volume 279, 2020, 115769. +- **Link:** https://doi.org/10.1016/j.apenergy.2020.115769 +- **License:** unknown +- **Description:** Contains rastered potentials and capital costs for enhanced geothermal electricity generation in Europe. + +``data/eia_hydro_annual_capacity.csv`` + +- **Source:** Energy Information Agency (EIA) +- **Link:** https://www.eia.gov/international/data/world/electricity/electricity-generation +- **License:** CC0 (`reference `__) +- **Description:** Contains country-level hydro-electric capacity for Europe by year. + +``data/eia_hydro_annual_generation.csv`` + +- **Source:** Energy Information Agency (EIA) +- **Link:** https://www.eia.gov/international/data/world/electricity/electricity-generation +- **License:** CC0 (`reference `__) +- **Description:** Contains country-level hydro-electric generato for Europe by year. + +``data/era5-annual-HDD-per-country.csv`` + +- **Source:** Neumann, Fabian +- **Link:** https://gist.github.com/fneum/d99e24e19da423038fd55fe3a4ddf875 +- **License:** CC-BY 4.0 +- **Description:** Contains country-level annual sum of heating degree days in + Europe. Used for rescaling heat demand in weather years not covered by energy + balance statistics. + +``data/era5-annual-runoff-per-country.csv`` + +- **Source:** Neumann, Fabian +- **Link:** https://gist.github.com/fneum/d99e24e19da423038fd55fe3a4ddf875 +- **License:** CC-BY 4.0 +- **Description:** Contains country-level annual sum of runoff in Europe. Used + for rescaling hydro-electricity availability in weather years not covered by + EIA hydro-generation statistics. + +``data/gr-e-11.03.02.01.01-cc.csv`` + +- **Source:** Swiss Federal Statistics Office +- **Link:** https://www.bfs.admin.ch/asset/de/30305426 +- **License:** `custom (OPEN BY ASK) `__ +- **Description:** Stock of road motor vehicles in Switzerland. + +``data/heat_load_profile_BDEW.csv`` + +- **Source:** oemof/demandlib +- **Link:** https://github.com/oemof/demandlib +- **License:** unknown +- **Description:** Contains standard heat load profiles based on data from BDEW (German Association of Energy and Water Industries). + +``data/hydro_capacities.csv`` + +.. warning:: + The provenance of the data is unclear. We will improve this in the future. + +``data/links_p_nom.csv`` + +- **Source:** Wikipedia +- **Link:** https://en.wikipedia.org/wiki/List_of_HVDC_projects +- **License:** CC-BY-SA 4.0 +- **Description:** Contains list of HVDC transmission line projects. + +``data/nuclear_p_max_pu.csv`` + +- **Source:** International Atomic Energy Agency (IAEA) +- **Link:** https://pris.iaea.org/PRIS/WorldStatistics/ThreeYrsEnergyAvailabilityFactor.aspx +- **License:** `custom `__ +- **Description:** Country-level nuclear power plant availability factors. + +``data/refineries-noneu.csv`` + +- **Source:** manually collected, mostly from Energy Information Agency (EIA) +- **Link:** https://www.eia.gov/petroleum/refinerycapacity/table3.pdf +- **License:** CC0 (`reference `__) +- **Description:** Contains locations and capacities of oil refineries in Europe. + +``data/switzerland-new_format-all_years.csv`` + +- **Source:** Swiss Federal Office of Energy (SFOE) +- **Link:** https://www.bfe.admin.ch/bfe/de/home/versorgung/statistik-und-geodaten/energiestatistiken/energieverbrauch-nach-verwendungszweck.html/ +- **License:** `custom `__ +- **Description:** Contains energy consumption by sector / application for Switzerland. + +``data/unit_commitment.csv`` + +- **Source:** `DIW + `__, + `Agora Energiewende + `__, + `Schill et al. (2017) + `__, + `Martin (2022) `__ +- **Link:** https://github.com/lisazeyen/hourly_vs_annually/blob/b67ca9222711372d8ab6cd58f9ebe7bc637939bf/scripts/solve_network.py#L554 +- **License:** CC-BY 4.0 +- **Description:** Contains energy balances for Europe. + +``data/biomass_transport_costs_supply_chain{1,2}.csv`` + +- **Source:** European Commission Joint Research Centre (JRC) +- **Link:** https://publications.jrc.ec.europa.eu/repository/handle/JRC98626 +- **License:** CC-BY 4.0 (`reference `__) +- **Description:** Contains transport costs for different types of biomass. diff --git a/doc/index.rst b/doc/index.rst index e23744e19..961f1482d 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -264,6 +264,7 @@ The PyPSA-Eur workflow is continuously tested for Linux, macOS and Windows (WSL release_notes licenses + data_sources validation limitations contributing diff --git a/doc/licenses.rst b/doc/licenses.rst index 5ab65ca9d..27ee12d2c 100644 --- a/doc/licenses.rst +++ b/doc/licenses.rst @@ -18,24 +18,4 @@ PyPSA-Eur is released under multiple licenses: See the individual files and the `dep5 <.reuse/dep5>`__ file for license details. Additionally, different licenses and terms of use also apply to the various -input data for both electricity-only and sector-coupled modelling exercises, -which are summarised below. - -Electricity Systems Databundle -============================== - -.. note:: - More details are included in `the description of the - data bundles on zenodo `__. - -* BY: Attribute Source -* NC: Non-Commercial Use Only -* SA: Share Alike - -.. csv-table:: - :header-rows: 1 - :file: configtables/licenses.csv - -.. csv-table:: - :header-rows: 1 - :file: configtables/licenses-sector.csv +input data. diff --git a/doc/release_notes.rst b/doc/release_notes.rst index a3411e87f..accbdd23b 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -27,6 +27,8 @@ Release Notes rules. This simplifies the use of ``mock_snakemake`` and places downloaded data more transparently into the ``data`` directory. +* The sources of nearly all data files are now listed in the documentation. + PyPSA-Eur 0.12.0 (30th August 2024) =================================== From 1b97a16bfaac6b586e9e6ea06959c883014baa42 Mon Sep 17 00:00:00 2001 From: lisazeyen <35347358+lisazeyen@users.noreply.github.com> Date: Thu, 12 Sep 2024 16:02:10 +0200 Subject: [PATCH 306/344] add option to vary parameter (#1244) * add option to vary parameter * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * remove logger.info * adjust maybe_adjust_costs_and_potentials * update configtables * revert removed cost_factor * reset build_energy_totals to master * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * add release notes * Revert "revert removed cost_factor" This reverts commit b7154f046954bd6de34c2910f3f9f52b44d5f233. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Fabian Neumann --- config/config.default.yaml | 8 +++-- doc/configtables/adjustments.csv | 22 ++++++++----- doc/release_notes.rst | 1 + scripts/_helpers.py | 32 ++++++++++++++++++ scripts/prepare_network.py | 55 +++++++++++++++++++++---------- scripts/prepare_sector_network.py | 47 ++++---------------------- 6 files changed, 96 insertions(+), 69 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 246a15f47..cb4787da8 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -662,7 +662,6 @@ sector: use_electrolysis_waste_heat: 0.25 electricity_transmission_grid: true electricity_distribution_grid: true - electricity_distribution_grid_cost_factor: 1.0 electricity_grid_connection: true transmission_efficiency: DC: @@ -871,7 +870,12 @@ clustering: # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#adjustments adjustments: electricity: false - sector: false + sector: + factor: + Link: + electricity distribution grid: + capital_cost: 1.0 + absolute: false # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#solving solving: diff --git a/doc/configtables/adjustments.csv b/doc/configtables/adjustments.csv index 526173527..c1eb2b902 100644 --- a/doc/configtables/adjustments.csv +++ b/doc/configtables/adjustments.csv @@ -1,8 +1,14 @@ -,Unit,Values,Description -adjustments,,, --- electricity,bool or dict,,"Parameter adjustments for capital cost, marginal cost, and maximum capacities of carriers. Applied in :mod:`prepare_network.`" --- -- {attr},,,"Attribute can be ``e_nom_opt``, ``p_nom_opt``, ``marginal_cost`` or ``capital_cost``" --- -- -- {carrier},float,per-unit,"Any carrier of the network to which parameter adjustment factor should be applied." --- sector,bool or dict,,"Parameter adjustments for capital cost, marginal cost, and maximum capacities of carriers. Applied in :mod:`prepare_sector_network.`" --- -- {attr},,,"Attribute can be ``e_nom_opt``, ``p_nom_opt``, ``marginal_cost`` or ``capital_cost``" --- -- -- {carrier},float,per-unit,"Any carrier of the network to which parameter adjustment factor should be applied." +,Unit,Values,Description +adjustments,,, +-- electricity,bool or dict,,Parameter adjustments applied in :mod:`prepare_network.` +-- -- factor,,,Multiply original value with given factor +-- -- absolute,,,Set attribute to absolute value +-- -- -- {component},,,PyPSA component in :mod:`prepare_network.` +-- -- -- -- {carrier},,,Any carrier of the network to which parameter adjustment factor should be applied. +-- -- -- -- -- {attr},float,per-unit,Attribute to which parameter adjustment factor should be applied. +-- sector,bool or dict,,Parameter adjustments applied in :mod:`prepare_sector_network.` +-- -- factor,,,Multiply original value with given factor +-- -- absolute,,,Set attribute to absolute value +-- -- -- {component},,,PyPSA component in :mod:`prepare_network.` +-- -- -- -- {carrier},,,Any carrier of the network to which parameter adjustment factor should be applied. +-- -- -- -- -- {attr},Float or dict,per-unit,Attribute to which parameter adjustment factor should be applied. Can be also a dictionary with planning horizons as keys. diff --git a/doc/release_notes.rst b/doc/release_notes.rst index accbdd23b..db1393805 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -10,6 +10,7 @@ Release Notes .. Upcoming Release +* Add function ``modify_attribute`` which allows to adjust every attribute of every PyPSA component either by a multiplication with a factor or setting an absolute value. These adjustments can also depend on the planning horizons and are set in the config under ``adjustments``. The function ``maybe_adjust_costs_and_potentials`` is removed. * Add technology options for methanol, like electricity production from methanol, biomass to methanol, methanol to kerosene, ... diff --git a/scripts/_helpers.py b/scripts/_helpers.py index f79d92583..3f6eac405 100644 --- a/scripts/_helpers.py +++ b/scripts/_helpers.py @@ -282,6 +282,38 @@ def aggregate_p(n): ) +def get(item, investment_year=None): + """ + Check whether item depends on investment year. + """ + if not isinstance(item, dict): + return item + elif investment_year in item.keys(): + return item[investment_year] + else: + logger.warning( + f"Investment key {investment_year} not found in dictionary {item}." + ) + keys = sorted(item.keys()) + if investment_year < keys[0]: + logger.warning(f"Lower than minimum key. Taking minimum key {keys[0]}") + return item[keys[0]] + elif investment_year > keys[-1]: + logger.warning(f"Higher than maximum key. Taking maximum key {keys[0]}") + return item[keys[-1]] + else: + logger.warning( + "Interpolate linearly between the next lower and next higher year." + ) + lower_key = max(k for k in keys if k < investment_year) + higher_key = min(k for k in keys if k > investment_year) + lower = item[lower_key] + higher = item[higher_key] + return lower + (higher - lower) * (investment_year - lower_key) / ( + higher_key - lower_key + ) + + def aggregate_e_nom(n): return pd.concat( [ diff --git a/scripts/prepare_network.py b/scripts/prepare_network.py index 382e633da..05f407304 100755 --- a/scripts/prepare_network.py +++ b/scripts/prepare_network.py @@ -64,6 +64,7 @@ import pypsa from _helpers import ( configure_logging, + get, set_scenario_config, update_config_from_wildcards, ) @@ -75,26 +76,43 @@ logger = logging.getLogger(__name__) -def maybe_adjust_costs_and_potentials(n, adjustments): - if not adjustments: +def modify_attribute(n, adjustments, investment_year, modification="factor"): + if not adjustments[modification]: return - - for attr, carrier_factor in adjustments.items(): - for carrier, factor in carrier_factor.items(): - # beware if factor is 0 and p_nom_max is np.inf, 0*np.inf is nan - if carrier == "AC": # lines do not have carrier - n.lines[attr] *= factor + change_dict = adjustments[modification] + for c in change_dict.keys(): + if c not in n.components.keys(): + logger.warning(f"{c} needs to be a PyPSA Component") + continue + for carrier in change_dict[c].keys(): + ind_i = n.df(c)[n.df(c).carrier == carrier].index + if ind_i.empty: continue - comps = { - "p_nom_max": {"Generator", "Link", "StorageUnit"}, - "e_nom_max": {"Store"}, - "capital_cost": {"Generator", "Link", "StorageUnit", "Store"}, - "marginal_cost": {"Generator", "Link", "StorageUnit", "Store"}, - } - for c in n.iterate_components(comps[attr]): - sel = c.df.index[c.df.carrier == carrier] - c.df.loc[sel, attr] *= factor - logger.info(f"changing {attr} for {carrier} by factor {factor}") + for parameter in change_dict[c][carrier].keys(): + if parameter not in n.df(c).columns: + logger.warning(f"Attribute {parameter} needs to be in {c} columns.") + continue + if investment_year: + factor = get(change_dict[c][carrier][parameter], investment_year) + else: + factor = change_dict[c][carrier][parameter] + if modification == "factor": + logger.info(f"Modify {parameter} of {carrier} by factor {factor} ") + n.df(c).loc[ind_i, parameter] *= factor + elif modification == "absolute": + logger.info(f"Set {parameter} of {carrier} to {factor} ") + n.df(c).loc[ind_i, parameter] = factor + else: + logger.warning( + f"{modification} needs to be either 'absolute' or 'factor'." + ) + + +def maybe_adjust_costs_and_potentials(n, adjustments, investment_year=None): + if not adjustments: + return + for modification in adjustments.keys(): + modify_attribute(n, adjustments, investment_year, modification) def add_co2limit(n, co2limit, Nyears=1.0): @@ -300,6 +318,7 @@ def set_line_nom_max( n.links["p_nom_max"] = n.links.p_nom_max.clip(upper=p_nom_max_set) +# %% if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 58df65681..4c7c059a7 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -19,6 +19,7 @@ import xarray as xr from _helpers import ( configure_logging, + get, set_scenario_config, update_config_from_wildcards, ) @@ -241,38 +242,6 @@ def determine_emission_sectors(options): return sectors -def get(item, investment_year=None): - """ - Check whether item depends on investment year. - """ - if not isinstance(item, dict): - return item - elif investment_year in item.keys(): - return item[investment_year] - else: - logger.warning( - f"Investment key {investment_year} not found in dictionary {item}." - ) - keys = sorted(item.keys()) - if investment_year < keys[0]: - logger.warning(f"Lower than minimum key. Taking minimum key {keys[0]}") - return item[keys[0]] - elif investment_year > keys[-1]: - logger.warning(f"Higher than maximum key. Taking maximum key {keys[0]}") - return item[keys[-1]] - else: - logger.warning( - "Interpolate linearly between the next lower and next higher year." - ) - lower_key = max(k for k in keys if k < investment_year) - higher_key = min(k for k in keys if k > investment_year) - lower = item[lower_key] - higher = item[higher_key] - return lower + (higher - lower) * (investment_year - lower_key) / ( - higher_key - lower_key - ) - - def co2_emissions_year( countries, input_eurostat, options, emissions_scope, input_co2, year ): @@ -1313,12 +1282,6 @@ def insert_electricity_distribution_grid(n, costs): # TODO pop_layout? # TODO options? - cost_factor = options["electricity_distribution_grid_cost_factor"] - - logger.info( - f"Inserting electricity distribution grid with investment cost factor of {cost_factor:.2f}" - ) - nodes = pop_layout.index n.madd( @@ -1339,7 +1302,7 @@ def insert_electricity_distribution_grid(n, costs): carrier="electricity distribution grid", efficiency=1, lifetime=costs.at["electricity distribution grid", "lifetime"], - capital_cost=costs.at["electricity distribution grid", "fixed"] * cost_factor, + capital_cost=costs.at["electricity distribution grid", "fixed"], ) # deduct distribution losses from electricity demand as these are included in total load @@ -4794,8 +4757,6 @@ def add_enhanced_geothermal(n, egs_potentials, egs_overlap, costs): n, snakemake.input["egs_potentials"], snakemake.input["egs_overlap"], costs ) - maybe_adjust_costs_and_potentials(n, snakemake.params["adjustments"]) - if options["gas_distribution_grid"]: insert_gas_distribution_costs(n, costs) @@ -4821,6 +4782,10 @@ def add_enhanced_geothermal(n, egs_potentials, egs_overlap, costs): if options.get("cluster_heat_buses", False) and not first_year_myopic: cluster_heat_buses(n) + maybe_adjust_costs_and_potentials( + n, snakemake.params["adjustments"], investment_year + ) + n.meta = dict(snakemake.config, **dict(wildcards=dict(snakemake.wildcards))) sanitize_carriers(n, snakemake.config) From 16b0f35cd85f84b1721d017d25cbb51e395e9409 Mon Sep 17 00:00:00 2001 From: Bobby Xiong <36541459+bobbyxng@users.noreply.github.com> Date: Thu, 12 Sep 2024 17:53:25 +0200 Subject: [PATCH 307/344] Bug fixes in add_existing_baseyear: Defaults and missing empty space (#1289) * Bug fixes in add_existing baseyear. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: lisazeyen <35347358+lisazeyen@users.noreply.github.com> --- scripts/add_existing_baseyear.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index 84d20b726..7eff0e2e0 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -597,7 +597,7 @@ def add_heating_capacities_installed_before_baseyear( n.madd( "Link", nodes, - suffix=f"{heat_system} gas boiler-{grouping_year}", + suffix=f" {heat_system} gas boiler-{grouping_year}", bus0="EU gas" if "EU gas" in spatial.gas.nodes else nodes + " gas", bus1=nodes + " " + heat_system.value + " heat", bus2="co2 atmosphere", @@ -666,6 +666,22 @@ def add_heating_capacities_installed_before_baseyear( ) +def set_defaults(n): + """ + Set default values for missing values in the network. + + Parameters: + n (pypsa.Network): The network object. + Returns: + None + """ + if "Link" in n.components: + if "reversed" in n.links.columns: + # Replace NA values with default value False + n.links.loc[n.links.reversed.isna(), "reversed"] = False + n.links.reversed = n.links.reversed.astype(bool) + + # %% if __name__ == "__main__": if "snakemake" not in globals(): @@ -734,6 +750,9 @@ def add_heating_capacities_installed_before_baseyear( ), ) + # Set defaults for missing missing values + set_defaults(n) + if options.get("cluster_heat_buses", False): cluster_heat_buses(n) From e35f65151805c786593c41c2cd8f1142c1f6f449 Mon Sep 17 00:00:00 2001 From: cpschau <124347782+cpschau@users.noreply.github.com> Date: Thu, 12 Sep 2024 19:20:07 +0200 Subject: [PATCH 308/344] Update NUTS3 shapes (#1286) * increase nuts3 shape resolution * retrieve and use shapes with EPSG:4326 * co-retrieve nuts2 shapes; add retries * added release note * changed doc/data_sources --- doc/data_sources.rst | 14 +++++++------- doc/release_notes.rst | 2 ++ rules/build_electricity.smk | 2 +- rules/build_sector.smk | 2 +- rules/retrieve.smk | 29 +++++++++++++++++++++++++++-- scripts/build_biomass_potentials.py | 4 ++-- scripts/build_shapes.py | 1 - 7 files changed, 40 insertions(+), 14 deletions(-) mode change 100644 => 100755 rules/build_electricity.smk mode change 100644 => 100755 rules/retrieve.smk mode change 100644 => 100755 scripts/build_shapes.py diff --git a/doc/data_sources.rst b/doc/data_sources.rst index ecd72458d..ac4eb379c 100644 --- a/doc/data_sources.rst +++ b/doc/data_sources.rst @@ -61,13 +61,6 @@ scope to reduce file size, or are not provided through stable URLs elsewhere. - **License:** CC-BY 4.0 (`reference `__) - **Description:** Total GHG emissions and removals in the EU. -``data/bundle/nuts`` - -- **Source:** GISCO -- **Link:** https://gisco-services.ec.europa.eu/distribution/v2/nuts/download/ -- **License:** `custom `__ -- **Description:** Europe's NUTS administrative regions. - ``data/bundle/emobility`` - **Source:** German Federal Highway Research Institute (BASt) @@ -127,6 +120,13 @@ Specific retrieval rules Data in this section is retrieved and extracted in rules specified in ``rules/retrieve.smk``. +``data/nuts`` + +- **Source:** GISCO +- **Link:** https://gisco-services.ec.europa.eu/distribution/v2/nuts/download/ +- **License:** `custom `__ +- **Description:** Europe's NUTS administrative regions. + ``data/ENSPRESO_BIOMASS.xlsx`` - **Source:** European Commission Joint Research Centre (JRC) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index db1393805..ce3c2cdb1 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -18,6 +18,8 @@ Release Notes * Updated district heating supply temperatures based on `Euroheat's DHC Market Outlook 2024`__ and `AGFW-Hauptbericht 2022 `__. `min_forward_temperature` and `return_temperature` (not given by Euroheat) are extrapolated based on German values. +* Increased the resolution of NUTS3 and NUTS2 shapes from 1:60M to 1:3M, with data now directly retrieved from GISCO + * Made the overdimensioning factor for heating systems specific for central/decentral heating, defaults to no overdimensionining for central heating and no changes to decentral heating compared to previous version. * bugfix: The carrier of stores was silently overwritten by their bus_carrier as a side effect when building the co2 constraints diff --git a/rules/build_electricity.smk b/rules/build_electricity.smk old mode 100644 new mode 100755 index 8c3ce32d6..3df4422e1 --- a/rules/build_electricity.smk +++ b/rules/build_electricity.smk @@ -98,7 +98,7 @@ rule build_shapes: input: naturalearth=ancient("data/naturalearth/ne_10m_admin_0_countries_deu.shp"), eez=ancient("data/eez/World_EEZ_v12_20231025_LR/eez_v12_lowres.gpkg"), - nuts3=ancient("data/bundle/NUTS_2013_60M_SH/data/NUTS_RG_60M_2013.shp"), + nuts3=ancient("data/nuts/NUTS_RG_03M_2013_4326_LEVL_3.geojson"), nuts3pop=ancient("data/bundle/nama_10r_3popgdp.tsv.gz"), nuts3gdp=ancient("data/bundle/nama_10r_3gdp.tsv.gz"), ch_cantons=ancient("data/ch_cantons.csv"), diff --git a/rules/build_sector.smk b/rules/build_sector.smk index b3cfc4afa..ccd4243e7 100755 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -405,7 +405,7 @@ rule build_biomass_potentials: input: enspreso_biomass="data/ENSPRESO_BIOMASS.xlsx", eurostat="data/eurostat/Balances-April2023", - nuts2="data/bundle/nuts/NUTS_RG_10M_2013_4326_LEVL_2.geojson", + nuts2="data/nuts/NUTS_RG_03M_2013_4326_LEVL_2.geojson", regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), nuts3_population=ancient("data/bundle/nama_10r_3popgdp.tsv.gz"), swiss_cantons=ancient("data/ch_cantons.csv"), diff --git a/rules/retrieve.smk b/rules/retrieve.smk old mode 100644 new mode 100755 index 844618e03..c842e61fc --- a/rules/retrieve.smk +++ b/rules/retrieve.smk @@ -5,6 +5,7 @@ import requests from datetime import datetime, timedelta from shutil import move, unpack_archive +from zipfile import ZipFile if config["enable"].get("retrieve", "auto") == "auto": config["enable"]["retrieve"] = has_internet_access() @@ -16,12 +17,10 @@ if config["enable"]["retrieve"] is False: if config["enable"]["retrieve"] and config["enable"].get("retrieve_databundle", True): datafiles = [ "je-e-21.03.02.xls", - "NUTS_2013_60M_SH/data/NUTS_RG_60M_2013.shp", "nama_10r_3popgdp.tsv.gz", "nama_10r_3gdp.tsv.gz", "corine/g250_clc06_V18_5.tif", "eea/UNFCCC_v23.csv", - "nuts/NUTS_RG_10M_2013_4326_LEVL_2.geojson", "emobility/KFZ__count", "emobility/Pkw__count", "h2_salt_caverns_GWh_per_sqkm.geojson", @@ -77,6 +76,32 @@ if config["enable"]["retrieve"] and config["enable"].get("retrieve_databundle", "../scripts/retrieve_eurostat_household_data.py" +if config["enable"]["retrieve"]: + + rule retrieve_nuts_shapes: + input: + shapes=storage( + "https://gisco-services.ec.europa.eu/distribution/v2/nuts/download/ref-nuts-2013-03m.geojson.zip" + ), + output: + shapes_level_3="data/nuts/NUTS_RG_03M_2013_4326_LEVL_3.geojson", + shapes_level_2="data/nuts/NUTS_RG_03M_2013_4326_LEVL_2.geojson", + params: + zip_file="data/nuts/ref-nuts-2013-03m.geojson.zip", + run: + os.rename(input.shapes, params.zip_file) + with ZipFile(params.zip_file, "r") as zip_ref: + for level in ["LEVL_3", "LEVL_2"]: + filename = f"NUTS_RG_03M_2013_4326_{level}.geojson" + zip_ref.extract(filename, Path(output.shapes_level_3).parent) + extracted_file = Path(output.shapes_level_3).parent / filename + extracted_file.rename( + getattr(output, f"shapes_level_{level[-1]}") + ) + os.remove(params.zip_file) + + + if config["enable"]["retrieve"] and config["enable"].get("retrieve_cutout", True): rule retrieve_cutout: diff --git a/scripts/build_biomass_potentials.py b/scripts/build_biomass_potentials.py index a3c51292c..bb56ebedf 100755 --- a/scripts/build_biomass_potentials.py +++ b/scripts/build_biomass_potentials.py @@ -193,7 +193,7 @@ def build_nuts2_shapes(): - consistently name ME, MK """ nuts2 = gpd.GeoDataFrame( - gpd.read_file(snakemake.input.nuts2).set_index("id").geometry + gpd.read_file(snakemake.input.nuts2).set_index("NUTS_ID").geometry ) countries = gpd.read_file(snakemake.input.country_shapes).set_index("name") @@ -345,7 +345,7 @@ def add_unsustainable_potentials(df): snakemake = mock_snakemake( "build_biomass_potentials", simpl="", - clusters="38", + clusters="39", planning_horizons=2050, ) diff --git a/scripts/build_shapes.py b/scripts/build_shapes.py old mode 100644 new mode 100755 index 2370f2aef..29eb91478 --- a/scripts/build_shapes.py +++ b/scripts/build_shapes.py @@ -150,7 +150,6 @@ def country_cover(country_shapes, eez_shapes=None): def nuts3(country_shapes, nuts3, nuts3pop, nuts3gdp, ch_cantons, ch_popgdp): df = gpd.read_file(nuts3) - df = df.loc[df["STAT_LEVL_"] == 3] df["geometry"] = df["geometry"].map(_simplify_polys) df = df.rename(columns={"NUTS_ID": "id"})[["id", "geometry"]].set_index("id") From 3e2286633a65bf1174bbd096f19f0dd635618bbd Mon Sep 17 00:00:00 2001 From: Lukas Trippe Date: Fri, 13 Sep 2024 10:52:16 +0200 Subject: [PATCH 309/344] fix: env update trigger from #1049 (#1281) * Update update-fixed-env.yaml * Update update-fixed-env.yaml * Update update-fixed-env.yaml * handle reuse * fix * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update update-fixed-env.yaml --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .github/workflows/update-fixed-env.yaml | 32 ++++++++++++------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/update-fixed-env.yaml b/.github/workflows/update-fixed-env.yaml index 3bb0991b2..ec63a9448 100644 --- a/.github/workflows/update-fixed-env.yaml +++ b/.github/workflows/update-fixed-env.yaml @@ -1,39 +1,39 @@ -name: Fixed Environment YAML Monitor +name: Fixed-Version Environment Checker on: push: branches: - master paths: - - 'env/environment.yaml' + - 'envs/environment.yaml' jobs: - update_environment_fixed: + update-environment-fixed: + name: Update environment.fixed.yaml runs-on: ubuntu-latest steps: - - name: Checkout Repository - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Setup micromamba - uses: mamba-org/setup-micromamba@v1 + - name: Setup conda + uses: conda-incubator/setup-miniconda@v3 with: - micromamba-version: latest + activate-environment: pypsa-eur environment-file: envs/environment.yaml - log-level: debug - init-shell: bash - cache-environment: true - cache-downloads: true - name: Update environment.fixed.yaml run: | - mamba env export --file envs/environment.fixed.yaml --no-builds + conda env export --name pypsa-eur --no-builds > envs/environment.fixed.yaml + + - name: Add SPDX header + run: | + SPDX_HEADER="# SPDX-FileCopyrightText: : 2017-2024 The PyPSA-Eur Authors\n# SPDX-License-Identifier: CC0-1.0\n" + echo -e "$SPDX_HEADER" | cat - envs/environment.fixed.yaml > temp && mv temp envs/environment.fixed.yaml - name: Create Pull Request uses: peter-evans/create-pull-request@v7 with: token: ${{ secrets.GITHUB_TOKEN }} branch: update-environment-fixed - title: Update fixed environment - body: Automatically generated PR to update environment.fixed.yaml - labels: automated + title: "[github-actions.ci] Update fixed environment" + body: Automatically generated PR to update environment.fixed.yaml, since environment.yaml was updated. From dbe7f48d6e394c485439aff050cc2fdb018b32f5 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 13 Sep 2024 11:09:27 +0200 Subject: [PATCH 310/344] update data bundle version (with reduced files) (#1291) * update data bundle version (with reduced files) * update data bundle to v0.4.1 with corrected permissions --- rules/retrieve.smk | 3 +-- scripts/retrieve_databundle.py | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/rules/retrieve.smk b/rules/retrieve.smk index c842e61fc..e13aa32ff 100755 --- a/rules/retrieve.smk +++ b/rules/retrieve.smk @@ -33,7 +33,6 @@ if config["enable"]["retrieve"] and config["enable"].get("retrieve_databundle", rule retrieve_databundle: output: expand("data/bundle/{file}", file=datafiles), - directory("data/bundle/jrc-idees-2015"), log: "logs/retrieve_databundle.log", resources: @@ -201,7 +200,7 @@ if config["enable"]["retrieve"]: rule retrieve_ship_raster: input: storage( - "https://zenodo.org/records/12760663/files/shipdensity_global.zip", + "https://zenodo.org/records/13757228/files/shipdensity_global.zip", keep_local=True, ), output: diff --git a/scripts/retrieve_databundle.py b/scripts/retrieve_databundle.py index 63b71dd5e..95856f385 100644 --- a/scripts/retrieve_databundle.py +++ b/scripts/retrieve_databundle.py @@ -4,8 +4,8 @@ # # SPDX-License-Identifier: MIT """ -.. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.3517935.svg - :target: https://doi.org/10.5281/zenodo.3517935 +.. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.3517934.svg + :target: https://doi.org/10.5281/zenodo.3517934 The data bundle contains common GIS datasets like NUTS3 shapes, EEZ shapes, CORINE Landcover, Natura 2000 and also electricity specific summary statistics @@ -13,7 +13,7 @@ data on NUTS3 levels and energy balances. This rule downloads the data bundle from `zenodo -`_ and extracts it in the ``data`` +`_ and extracts it in the ``data`` sub-directory, such that all files of the bundle are stored in the ``data/bundle`` subdirectory. @@ -48,7 +48,7 @@ configure_logging(snakemake) set_scenario_config(snakemake) - url = "https://zenodo.org/records/12760663/files/bundle.tar.xz" + url = "https://zenodo.org/records/13757228/files/bundle.tar.xz" tarball_fn = Path(f"{rootpath}/bundle.tar.xz") to_fn = Path(rootpath) / Path(snakemake.output[0]).parent.parent From 42f2c37ebf3cc7de2aed25a9a703fd11006851de Mon Sep 17 00:00:00 2001 From: Bobby Xiong <36541459+bobbyxng@users.noreply.github.com> Date: Fri, 13 Sep 2024 14:55:52 +0200 Subject: [PATCH 311/344] Updated osm-prebuilt network to v0.4 and added version control to config (#1293) * Updated base_network script and rules. * Updated osm-prebuilt to version 0.4 and added version control. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- config/config.default.yaml | 1 + doc/configtables/electricity.csv | 1 + doc/data_sources.rst | 8 +++---- doc/release_notes.rst | 4 ++++ rules/build_electricity.smk | 8 +++++-- rules/retrieve.smk | 36 ++++++++++++++++++++++---------- scripts/base_network.py | 14 +++++++++++-- 7 files changed, 53 insertions(+), 19 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index cb4787da8..4a32d8f20 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -88,6 +88,7 @@ co2_budget: electricity: voltages: [200., 220., 300., 380., 500., 750.] base_network: osm-prebuilt + osm-prebuilt-version: 0.4 gaslimit_enable: false gaslimit: false co2limit_enable: false diff --git a/doc/configtables/electricity.csv b/doc/configtables/electricity.csv index 9bad7bfc6..f1676187e 100644 --- a/doc/configtables/electricity.csv +++ b/doc/configtables/electricity.csv @@ -1,6 +1,7 @@ ,Unit,Values,Description voltages,kV,"Any subset of {200., 220., 300., 380., 500., 750.}",Voltage levels to consider base_network, --, "Any value in {'entsoegridkit', 'osm-prebuilt', 'osm-raw}", "Specify the underlying base network, i.e. GridKit (based on ENTSO-E web map extract, OpenStreetMap (OSM) prebuilt or raw (built from raw OSM data), takes longer." +osm-prebuilt-version, --, "float, any value in range 0.1-0.4", "Choose the version of the prebuilt OSM network. Defaults to latest Zenodo release." gaslimit_enable,bool,true or false,Add an overall absolute gas limit configured in ``electricity: gaslimit``. gaslimit,MWhth,float or false,Global gas usage limit co2limit_enable,bool,true or false,Add an overall absolute carbon-dioxide emissions limit configured in ``electricity: co2limit`` in :mod:`prepare_network`. **Warning:** This option should currently only be used with electricity-only networks, not for sector-coupled networks.. diff --git a/doc/data_sources.rst b/doc/data_sources.rst index ac4eb379c..128fe0063 100644 --- a/doc/data_sources.rst +++ b/doc/data_sources.rst @@ -299,10 +299,10 @@ Data in this section is retrieved and extracted in rules specified in ``rules/re ``data/osm-prebuilt`` - **Source:** OpenStreetMap; Xiong, B., Neumann, F., & Brown, T. (2024). - Prebuilt Electricity Network for PyPSA-Eur based on OpenStreetMap Data (0.3) - [Data set]. Zenodo. https://doi.org/10.5281/zenodo.13358976 -- **Link:** https://zenodo.org/records/13358976 -- **License:** ODbL (`reference `) + Prebuilt Electricity Network for PyPSA-Eur based on OpenStreetMap Data (0.4) + [Data set]. Zenodo. https://doi.org/10.5281/zenodo.13759222 +- **Link:** https://zenodo.org/records/13759222 +- **License:** ODbL (`reference `) - **Description:** Pre-built data of high-voltage transmission grid in Europe from OpenStreetMap. ``data/osm-raw`` diff --git a/doc/release_notes.rst b/doc/release_notes.rst index ce3c2cdb1..7aefcedd1 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -32,6 +32,10 @@ Release Notes * The sources of nearly all data files are now listed in the documentation. +* Updated osm-prebuilt network to version 0.4: https://doi.org/10.5281/zenodo.13759222 : Added Kosovo (XK) as dedicated region. Fixed major 330 kV line in Moldova (MD) (https://www.openstreetmap.org/way/33360284). + +* Add version control to osm-prebuilt: `config["electricity"]["osm-prebuilt-version"]`. Defaults to latest Zenodo release, i.e. v0.4, Config is only considered when selecting `osm-prebuilt` as `base_network`. + PyPSA-Eur 0.12.0 (30th August 2024) =================================== diff --git a/rules/build_electricity.smk b/rules/build_electricity.smk index 3df4422e1..cbe8035aa 100755 --- a/rules/build_electricity.smk +++ b/rules/build_electricity.smk @@ -52,12 +52,16 @@ rule build_powerplants: def input_base_network(w): base_network = config_provider("electricity", "base_network")(w) + osm_prebuilt_version = config_provider("electricity", "osm-prebuilt-version")(w) components = {"buses", "lines", "links", "converters", "transformers"} if base_network == "osm-raw": inputs = {c: resources(f"osm-raw/build/{c}.csv") for c in components} - else: + elif base_network == "osm-prebuilt": + inputs = { + c: f"data/{base_network}/{osm_prebuilt_version}/{c}.csv" for c in components + } + elif base_network == "entsoegridkit": inputs = {c: f"data/{base_network}/{c}.csv" for c in components} - if base_network == "entsoegridkit": inputs["parameter_corrections"] = "data/parameter_corrections.yaml" inputs["links_p_nom"] = "data/links_p_nom.csv" return inputs diff --git a/rules/retrieve.smk b/rules/retrieve.smk index e13aa32ff..69ea0c4b1 100755 --- a/rules/retrieve.smk +++ b/rules/retrieve.smk @@ -534,24 +534,38 @@ if config["enable"]["retrieve"]: if config["enable"]["retrieve"] and ( config["electricity"]["base_network"] == "osm-prebuilt" ): - + # Dictionary of prebuilt versions, e.g. 0.3 : "13358976" + osm_prebuilt_version = { + 0.1: "12799202", + 0.2: "13342577", + 0.3: "13358976", + 0.4: "13759222", + } + + # update rule to use the correct version rule retrieve_osm_prebuilt: input: - buses=storage("https://zenodo.org/records/13358976/files/buses.csv"), + buses=storage( + f"https://zenodo.org/records/{osm_prebuilt_version[config['electricity']['osm-prebuilt-version']]}/files/buses.csv" + ), converters=storage( - "https://zenodo.org/records/13358976/files/converters.csv" + f"https://zenodo.org/records/{osm_prebuilt_version[config['electricity']['osm-prebuilt-version']]}/files/converters.csv" + ), + lines=storage( + f"https://zenodo.org/records/{osm_prebuilt_version[config['electricity']['osm-prebuilt-version']]}/files/lines.csv" + ), + links=storage( + f"https://zenodo.org/records/{osm_prebuilt_version[config['electricity']['osm-prebuilt-version']]}/files/links.csv" ), - lines=storage("https://zenodo.org/records/13358976/files/lines.csv"), - links=storage("https://zenodo.org/records/13358976/files/links.csv"), transformers=storage( - "https://zenodo.org/records/13358976/files/transformers.csv" + f"https://zenodo.org/records/{osm_prebuilt_version[config['electricity']['osm-prebuilt-version']]}/files/transformers.csv" ), output: - buses="data/osm-prebuilt/buses.csv", - converters="data/osm-prebuilt/converters.csv", - lines="data/osm-prebuilt/lines.csv", - links="data/osm-prebuilt/links.csv", - transformers="data/osm-prebuilt/transformers.csv", + buses=f"data/osm-prebuilt/{config['electricity']['osm-prebuilt-version']}/buses.csv", + converters=f"data/osm-prebuilt/{config['electricity']['osm-prebuilt-version']}/converters.csv", + lines=f"data/osm-prebuilt/{config['electricity']['osm-prebuilt-version']}/lines.csv", + links=f"data/osm-prebuilt/{config['electricity']['osm-prebuilt-version']}/links.csv", + transformers=f"data/osm-prebuilt/{config['electricity']['osm-prebuilt-version']}/transformers.csv", log: "logs/retrieve_osm_prebuilt.log", threads: 1 diff --git a/scripts/base_network.py b/scripts/base_network.py index 0f661c284..a6a54617b 100644 --- a/scripts/base_network.py +++ b/scripts/base_network.py @@ -717,6 +717,7 @@ def base_network( ): base_network = config["electricity"].get("base_network") + osm_prebuilt_version = config["electricity"].get("osm-prebuilt-version") assert base_network in { "entsoegridkit", "osm-raw", @@ -728,7 +729,12 @@ def base_network( DeprecationWarning, ) - logger.info(f"Creating base network using {base_network}.") + logger_str = ( + f"Creating base network using {base_network}" + + (f" v{osm_prebuilt_version}" if base_network == "osm-prebuilt" else "") + + "." + ) + logger.info(logger_str) buses = _load_buses(buses, europe_shape, config) transformers = _load_transformers(buses, transformers) @@ -764,7 +770,11 @@ def base_network( converters = _set_electrical_parameters_converters(converters, config) n = pypsa.Network() - n.name = f"PyPSA-Eur ({base_network})" + n.name = ( + f"PyPSA-Eur ({base_network}" + + (f" v{osm_prebuilt_version}" if base_network == "osm-prebuilt" else "") + + ")" + ) time = get_snapshots(snakemake.params.snapshots, snakemake.params.drop_leap_day) n.set_snapshots(time) From 40351fbf9b02a43888cb90170c441c4d5f8e919a Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 13 Sep 2024 15:14:39 +0200 Subject: [PATCH 312/344] prepare release v0.13.0 (#1292) * prepare release v0.13.0 * adjust release note * amend release note * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- CITATION.cff | 2 +- config/config.default.yaml | 2 +- doc/conf.py | 4 +- doc/release_notes.rst | 103 ++++++++++++++++++++++++++++++++----- 4 files changed, 95 insertions(+), 16 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index 2caf4226f..ffc89b3df 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -6,7 +6,7 @@ cff-version: 1.1.0 message: "If you use this package, please cite it in the following way." title: "PyPSA-Eur: An open sector-coupled optimisation model of the European energy system" repository: https://github.com/pypsa/pypsa-eur -version: 0.12.0 +version: 0.13.0 license: MIT authors: - family-names: Brown diff --git a/config/config.default.yaml b/config/config.default.yaml index 4a32d8f20..063f768e0 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -3,7 +3,7 @@ # SPDX-License-Identifier: CC0-1.0 # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#top-level-configuration -version: 0.12.0 +version: 0.13.0 tutorial: false logging: diff --git a/doc/conf.py b/doc/conf.py index bb929a467..7165b86cf 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -92,9 +92,9 @@ # built documents. # # The short X.Y version. -version = "0.12" +version = "0.13" # The full version, including alpha/beta/rc tags. -release = "0.12.0" +release = "0.13.0" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 7aefcedd1..0e3bb751e 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -10,31 +10,110 @@ Release Notes .. Upcoming Release -* Add function ``modify_attribute`` which allows to adjust every attribute of every PyPSA component either by a multiplication with a factor or setting an absolute value. These adjustments can also depend on the planning horizons and are set in the config under ``adjustments``. The function ``maybe_adjust_costs_and_potentials`` is removed. +PyPSA-Eur 0.13.0 (13th September 2024) +====================================== -* Add technology options for methanol, like electricity production from methanol, biomass to methanol, methanol to kerosene, ... +**Features** -* Change the heating demand from final energy which includes losses in legacy equipment to thermal energy service based on JRC-IDEES. Efficiencies of existing heating capacities are lowered according to the conversion of final energy to thermal energy service. For overnight scenarios or future planning horizon this change leads to a reduction in heat supply. +* Add new methanol-based technologies: methanol-to-power, methanol reforming, + methanol-to-kerosene, methanol-to-olefins/aromatics, biomass-to-methanol with + and without carbon capture. (https://github.com/PyPSA/pypsa-eur/pull/1207) -* Updated district heating supply temperatures based on `Euroheat's DHC Market Outlook 2024`__ and `AGFW-Hauptbericht 2022 `__. `min_forward_temperature` and `return_temperature` (not given by Euroheat) are extrapolated based on German values. +* Add function ``modify_attribute`` to :mod:`prepare_sector_network` which allows to adjust any attribute of any + PyPSA component either by a multiplication with a factor or setting an + absolute value. These adjustments can also depend on the planning horizons and + are set in the config under ``adjustments``. + (https://github.com/PyPSA/pypsa-eur/pull/1244) -* Increased the resolution of NUTS3 and NUTS2 shapes from 1:60M to 1:3M, with data now directly retrieved from GISCO +* Add version control to osm-prebuilt: + ``config["electricity"]["osm-prebuilt-version"]``. Defaults to latest Zenodo + release, i.e. v0.4, Config is only considered when selecting ``osm-prebuilt`` + as ``base_network``. (https://github.com/PyPSA/pypsa-eur/pull/1293) -* Made the overdimensioning factor for heating systems specific for central/decentral heating, defaults to no overdimensionining for central heating and no changes to decentral heating compared to previous version. - -* bugfix: The carrier of stores was silently overwritten by their bus_carrier as a side effect when building the co2 constraints +**Changes** -* bugfix: The oil generator was incorrectly dropped when the config `oil_refining_emissions` was greater than zero. This was the default behaviour in 0.12.0. +* Use JRC-IDEES thermal energy service instead of final energy demand for + buildings heating demand. Final energy includes losses in legacy equipment. + Efficiencies of existing heating capacities are lowered according to the + conversion of final energy to thermal energy service. For overnight scenarios + or future planning horizons this change leads to a reduction in heat supply + and, therefore, system cost. (https://github.com/PyPSA/pypsa-eur/pull/1255) + +* Updated district heating supply temperatures based on `Euroheat's DHC Market + Outlook + 2024`__ + and `AGFW-Hauptbericht 2022 + `__. + ``min_forward_temperature`` and ``return_temperature`` (not given by Euroheat) are + extrapolated based on German values. (https://github.com/PyPSA/pypsa-eur/pull/1264) + +* Refined implementation of unsustainable biomass. + (https://github.com/PyPSA/pypsa-eur/pull/1275, + https://github.com/PyPSA/pypsa-eur/pull/1271, + https://github.com/PyPSA/pypsa-eur/pull/1254, + https://github.com/PyPSA/pypsa-eur/pull/1266) + +* Biomass transport costs are now stored in the ``data`` folder. Extraction from + PDF file is skipped. (https://github.com/PyPSA/pypsa-eur/pull/1272) + +* Increased the resolution of NUTS3 and NUTS2 shapes from 1:60M to 1:3M. The + shapefiles are now directly retrieved with the ``retrieve_nuts_shapes`` rule. + (https://github.com/PyPSA/pypsa-eur/pull/1286) * Uses of Snakemake's ``storage()`` function are integrated into retrieval rules. This simplifies the use of ``mock_snakemake`` and places downloaded data more transparently into the ``data`` directory. + (https://github.com/PyPSA/pypsa-eur/pull/1274) -* The sources of nearly all data files are now listed in the documentation. +* Updated data bundle to remove files which are now directly downloaded in the + rules. This reduces the size of the data bundle. + (https://github.com/PyPSA/pypsa-eur/pull/1291) + +* Update NEP transmission projects to include `Startnetz`. + (https://github.com/PyPSA/pypsa-eur/pull/1263) + +* Auto-update ``envs/environment.fixed.yaml``. + (https://github.com/PyPSA/pypsa-eur/pull/1281) + +**Bugfixes and Compatibility** + +* Updated osm-prebuilt network to version 0.4 + (https://doi.org/10.5281/zenodo.13759222). Added Kosovo (XK) as dedicated + region. Fixed major 330 kV line in Moldova (MD) + (https://www.openstreetmap.org/way/33360284). + (https://github.com/PyPSA/pypsa-eur/pull/1293) + +* Made the overdimensioning factor for heating systems specific for + central/decentral heating, defaults to no overdimensionining for central + heating and no changes to decentral heating compared to previous version. + (https://github.com/PyPSA/pypsa-eur/pull/1259) -* Updated osm-prebuilt network to version 0.4: https://doi.org/10.5281/zenodo.13759222 : Added Kosovo (XK) as dedicated region. Fixed major 330 kV line in Moldova (MD) (https://www.openstreetmap.org/way/33360284). +* The carrier of stores was previously silently overwritten by their bus' + carrier when building global emission constraints. + (https://github.com/PyPSA/pypsa-eur/pull/1262) -* Add version control to osm-prebuilt: `config["electricity"]["osm-prebuilt-version"]`. Defaults to latest Zenodo release, i.e. v0.4, Config is only considered when selecting `osm-prebuilt` as `base_network`. +* The fossil oil generator was incorrectly dropped when ``sector: + oil_refining_emissions`` was greater than zero. (https://github.com/PyPSA/pypsa-eur/pull/1257) + +* Correctly account for the CO2 emissions of municipal solid waste. + (https://github.com/PyPSA/pypsa-eur/pull/1256) + +* Added a missing space in the component name of retrofitted gas boilers. + (https://github.com/PyPSA/pypsa-eur/pull/1289) + +* Global Energy Monitor datasets are temporarily mirrored on alternative + servers. (https://github.com/PyPSA/pypsa-eur/pull/1265) + +* Fixed plotting of hydrogen networks with myopic pathway optimisation. + (https://github.com/PyPSA/pypsa-eur/pull/1270) + +* Fixed internet connection check. + (https://github.com/PyPSA/pypsa-eur/pull/1280) + +**Documentation** + +* The sources of nearly all data files are now listed in the documentation. + (https://github.com/PyPSA/pypsa-eur/pull/1284) PyPSA-Eur 0.12.0 (30th August 2024) =================================== From 013b705ee471014f066e9715273dd0503f48804e Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 13 Sep 2024 15:37:01 +0200 Subject: [PATCH 313/344] Clustering: build renewable profiles and add all assets after clustering (#1201) * Cluster first: build renewable profiles and add all assets after clustering * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * correction: pass landfall_lengths through functions * assign landfall_lenghts correctly * remove parameter add_land_use_constraint * fix network_dict * calculate distance to shoreline, remove underwater_fraction * adjust simplification parameter to exclude Crete from offshore wind connections * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * remove unused geth2015 hydro capacities * removing remaining traces of {simpl} wildcard * add release notes and update workflow graphics * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: lisazeyen --- .gitignore | 2 + Snakefile | 1 - config/config.default.yaml | 22 +- config/examples/config.entsoe-all.yaml | 2 - config/examples/config.perfect.yaml | 2 - doc/configtables/clustering.csv | 8 +- doc/configtables/electricity.csv | 2 +- doc/configtables/enable.csv | 2 +- doc/configtables/links.csv | 1 + doc/configtables/load.csv | 3 + doc/configtables/offwind-ac.csv | 1 + doc/configtables/offwind-dc.csv | 1 + doc/configtables/scenario.csv | 1 - doc/configuration.rst | 2 +- doc/img/intro-workflow.png | Bin 124775 -> 445946 bytes doc/introduction.rst | 4 +- doc/preparation.rst | 19 +- doc/release_notes.rst | 59 +- doc/simplification.rst | 11 +- doc/tutorial.rst | 221 +-- doc/tutorial_sector.rst | 1287 +++++++++-------- doc/wildcards.rst | 14 - rules/build_electricity.smk | 322 +++-- rules/build_sector.smk | 320 ++-- rules/collect.smk | 27 +- rules/common.smk | 6 +- rules/postprocess.smk | 64 +- rules/solve_electricity.smk | 22 +- rules/solve_myopic.smk | 50 +- rules/solve_overnight.smk | 14 +- rules/solve_perfect.smk | 53 +- rules/validate.smk | 18 +- scripts/add_brownfield.py | 14 - scripts/add_electricity.py | 566 +++++--- scripts/add_existing_baseyear.py | 86 +- scripts/add_extra_components.py | 253 ---- scripts/add_transmission_projects_and_dlr.py | 106 ++ scripts/build_biomass_potentials.py | 1 - .../run.py | 1 - scripts/build_clustered_population_layouts.py | 6 +- scripts/build_cop_profiles/run.py | 1 - scripts/build_daily_heat_demand.py | 5 +- scripts/build_district_heat_share.py | 1 - scripts/build_egs_potentials.py | 1 - scripts/build_electricity_demand_base.py | 127 ++ .../build_existing_heating_distribution.py | 5 +- scripts/build_gas_input_locations.py | 1 - scripts/build_gdp_pop_non_nuts3.py | 6 +- scripts/build_hac_features.py | 47 + scripts/build_hourly_heat_demand.py | 5 +- scripts/build_industrial_distribution_key.py | 7 +- ...build_industrial_energy_demand_per_node.py | 7 +- ...industrial_energy_demand_per_node_today.py | 7 +- .../build_industrial_production_per_node.py | 10 +- scripts/build_line_rating.py | 55 +- ...build_population_weighted_energy_totals.py | 1 - scripts/build_powerplants.py | 4 +- scripts/build_renewable_profiles.py | 273 +--- scripts/build_retro_cost.py | 1 - scripts/build_salt_cavern_potentials.py | 4 +- scripts/build_sequestration_potentials.py | 4 +- scripts/build_shipping_demand.py | 6 +- scripts/build_solar_thermal_profiles.py | 10 +- scripts/build_temperature_profiles.py | 7 +- scripts/build_transport_demand.py | 6 +- scripts/cluster_gas_network.py | 2 +- scripts/cluster_network.py | 427 ++---- scripts/determine_availability_matrix.py | 194 +++ .../determine_availability_matrix_MD_UA.py | 14 +- scripts/make_summary.py | 3 +- scripts/make_summary_perfect.py | 3 +- scripts/plot_gas_network.py | 1 - scripts/plot_hydrogen_network.py | 1 - scripts/plot_power_network.py | 1 - scripts/plot_power_network_perfect.py | 1 - scripts/plot_statistics.py | 1 - scripts/plot_validation_cross_border_flows.py | 1 - scripts/plot_validation_electricity_prices.py | 1 - .../plot_validation_electricity_production.py | 1 - scripts/prepare_network.py | 9 +- scripts/prepare_perfect_foresight.py | 1 - scripts/prepare_sector_network.py | 77 +- scripts/simplify_network.py | 384 +---- scripts/solve_network.py | 106 +- scripts/solve_operations_network.py | 1 - scripts/time_aggregation.py | 9 +- test.sh | 2 +- 87 files changed, 2649 insertions(+), 2786 deletions(-) delete mode 100644 scripts/add_extra_components.py create mode 100644 scripts/add_transmission_projects_and_dlr.py create mode 100644 scripts/build_electricity_demand_base.py create mode 100644 scripts/build_hac_features.py create mode 100644 scripts/determine_availability_matrix.py diff --git a/.gitignore b/.gitignore index 21062dd3d..27a2fb182 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,5 @@ d1gam3xoknrgr2.cloudfront.net/ *.ipynb merger-todos.md + +*.html diff --git a/Snakefile b/Snakefile index eb99437bf..90a44c9d9 100644 --- a/Snakefile +++ b/Snakefile @@ -39,7 +39,6 @@ localrules: wildcard_constraints: - simpl="[a-zA-Z0-9]*", clusters="[0-9]+(m|c)?|all", ll=r"(v|c)([0-9\.]+|opt)", opts=r"[-+a-zA-Z0-9\.]*", diff --git a/config/config.default.yaml b/config/config.default.yaml index 063f768e0..a98a0af7c 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -37,8 +37,6 @@ foresight: overnight # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#scenario # Wildcard docs in https://pypsa-eur.readthedocs.io/en/latest/wildcards.html scenario: - simpl: - - '' ll: - vopt clusters: @@ -188,6 +186,7 @@ renewable: max_shore_distance: 30000 excluder_resolution: 200 clip_p_max_pu: 1.e-2 + landfall_length: 10 offwind-dc: cutout: europe-2013-sarah3-era5 resource: @@ -205,6 +204,7 @@ renewable: min_shore_distance: 30000 excluder_resolution: 200 clip_p_max_pu: 1.e-2 + landfall_length: 10 offwind-float: cutout: europe-2013-sarah3-era5 resource: @@ -225,6 +225,7 @@ renewable: min_depth: 60 max_depth: 1000 clip_p_max_pu: 1.e-2 + landfall_length: 10 solar: cutout: europe-2013-sarah3-era5 resource: @@ -301,6 +302,7 @@ links: p_max_pu: 1.0 p_nom_max: .inf max_extension: 30000 #MW + length_factor: 1.25 under_construction: 'keep' # 'zero': set capacity to zero, 'remove': remove, 'keep': with full capacity for lines in grid extract # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#transmission_projects @@ -335,6 +337,9 @@ load: scaling_factor: 1.0 fixed_year: false # false or year (e.g. 2013) supplement_synthetic: true + distribution_key: + gdp: 0.6 + population: 0.4 # docs # TODO: PyPSA-Eur merge issue in prepare_sector_network.py @@ -849,16 +854,15 @@ clustering: focus_weights: false simplify_network: to_substations: false - algorithm: kmeans # choose from: [hac, kmeans] - feature: solar+onwind-time - exclude_carriers: [] remove_stubs: true - remove_stubs_across_borders: true + remove_stubs_across_borders: false cluster_network: algorithm: kmeans - feature: solar+onwind-time - exclude_carriers: [] - consider_efficiency_classes: false + hac_features: + - wnd100m + - influx_direct + exclude_carriers: [] + consider_efficiency_classes: false aggregation_strategies: generators: committable: any diff --git a/config/examples/config.entsoe-all.yaml b/config/examples/config.entsoe-all.yaml index 9f52094d6..3c275ae7a 100644 --- a/config/examples/config.entsoe-all.yaml +++ b/config/examples/config.entsoe-all.yaml @@ -10,8 +10,6 @@ run: shared_cutouts: true scenario: - simpl: - - '' ll: - vopt clusters: diff --git a/config/examples/config.perfect.yaml b/config/examples/config.perfect.yaml index 7bfdbdd2e..811ecbb82 100644 --- a/config/examples/config.perfect.yaml +++ b/config/examples/config.perfect.yaml @@ -10,8 +10,6 @@ foresight: perfect # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#scenario # Wildcard docs in https://pypsa-eur.readthedocs.io/en/latest/wildcards.html scenario: - simpl: - - '' ll: - v1.0 clusters: diff --git a/doc/configtables/clustering.csv b/doc/configtables/clustering.csv index 65411738d..42e457932 100644 --- a/doc/configtables/clustering.csv +++ b/doc/configtables/clustering.csv @@ -2,16 +2,14 @@ focus_weights,,,Optionally specify the focus weights for the clustering of countries. For instance: `DE: 0.8` will distribute 80% of all nodes to Germany and 20% to the rest of the countries. simplify_network,,, -- to_substations,bool,"{'true','false'}","Aggregates all nodes without power injection (positive or negative, i.e. demand or generation) to electrically closest ones" --- algorithm,str,"One of {‘kmeans’, ‘hac’, ‘modularity‘}", --- feature,str,"Str in the format ‘carrier1+carrier2+...+carrierN-X’, where CarrierI can be from {‘solar’, ‘onwind’, ‘offwind’, ‘ror’} and X is one of {‘cap’, ‘time’}.", -- exclude_carriers,list,"List of Str like [ 'solar', 'onwind'] or empy list []","List of carriers which will not be aggregated. If empty, all carriers will be aggregated." -- remove stubs,bool,"{'true','false'}",Controls whether radial parts of the network should be recursively aggregated. Defaults to true. -- remove_stubs_across_borders,bool,"{'true','false'}",Controls whether radial parts of the network should be recursively aggregated across borders. Defaults to true. cluster_network,,, -- algorithm,str,"One of {‘kmeans’, ‘hac’}", --- feature,str,"Str in the format ‘carrier1+carrier2+...+carrierN-X’, where CarrierI can be from {‘solar’, ‘onwind’, ‘offwind’, ‘ror’} and X is one of {‘cap’, ‘time’}.", --- exclude_carriers,list,"List of Str like [ 'solar', 'onwind'] or empy list []","List of carriers which will not be aggregated. If empty, all carriers will be aggregated." --- consider_efficiency_classes,bool,"{'true','false'}","Aggregated each carriers into the top 10-quantile (high), the bottom 90-quantile (low), and everything in between (medium)." +-- hac_features,list,"List of meteorological variables contained in the weather data cutout that should be considered for hierarchical clustering.", +exclude_carriers,list,"List of Str like [ 'solar', 'onwind'] or empy list []","List of carriers which will not be aggregated. If empty, all carriers will be aggregated." +consider_efficiency_classes,bool,"{'true','false'}","Aggregated each carriers into the top 10-quantile (high), the bottom 90-quantile (low), and everything in between (medium)." aggregation_strategies,,, -- generators,,, -- -- {key},str,"{key} can be any of the component of the generator (str). It’s value can be any that can be converted to pandas.Series using getattr(). For example one of {min, max, sum}.","Aggregates the component according to the given strategy. For example, if sum, then all values within each cluster are summed to represent the new generator." diff --git a/doc/configtables/electricity.csv b/doc/configtables/electricity.csv index f1676187e..5fe20967f 100644 --- a/doc/configtables/electricity.csv +++ b/doc/configtables/electricity.csv @@ -27,7 +27,7 @@ custom_powerplants,--,"use `pandas.query `_." build_cutout,bool,"{true, false}","Switch to enable the building of cutouts via the rule :mod:`build_cutout`." retrieve_cutout,bool,"{true, false}","Switch to enable the retrieval of cutouts from zenodo with :mod:`retrieve_cutout`." -custom_busmap,bool,"{true, false}","Switch to enable the use of custom busmaps in rule :mod:`cluster_network`. If activated the rule looks for provided busmaps at ``data/busmaps/elec_s{simpl}_{clusters}_{base_network}.csv`` which should have the same format as ``resources/busmap_elec_s{simpl}_{clusters}.csv``, i.e. the index should contain the buses of ``networks/elec_s{simpl}.nc``. {base_network} is the name of the selected base_network in electricity, e.g. ``gridkit``, ``osm-prebuilt``, or ``osm-raw``." +custom_busmap,bool,"{true, false}","Switch to enable the use of custom busmaps in rule :mod:`cluster_network`. If activated the rule looks for provided busmaps at ``data/busmaps/base_s_{clusters}_{base_network}.csv`` which should have the same format as ``resources/busmap_base_s_{clusters}.csv``, i.e. the index should contain the buses of ``networks/base_s.nc``. {base_network} is the name of the selected base_network in electricity, e.g. ``gridkit``, ``osm-prebuilt``, or ``osm-raw``." drop_leap_day,bool,"{true, false}","Switch to drop February 29 from all time-dependent data in leap years" diff --git a/doc/configtables/links.csv b/doc/configtables/links.csv index 9dedc36ab..56342c7db 100644 --- a/doc/configtables/links.csv +++ b/doc/configtables/links.csv @@ -2,4 +2,5 @@ p_max_pu,--,"Value in [0.,1.]","Correction factor for link capacities ``p_nom``." p_nom_max,MW,"float","Global upper limit for the maximum capacity of each extendable DC link." max_extension,MW,"float","Upper limit for the extended capacity of each extendable DC link." +length_factor,--,float,"Correction factor to account for the fact that buses are *not* connected by links through air-line distance." under_construction,--,"One of {'zero': set capacity to zero, 'remove': remove completely, 'keep': keep with full capacity}","Specifies how to handle lines which are currently under construction." diff --git a/doc/configtables/load.csv b/doc/configtables/load.csv index 9ebfea323..293772fa6 100644 --- a/doc/configtables/load.csv +++ b/doc/configtables/load.csv @@ -5,3 +5,6 @@ manual_adjustments,bool,"{true, false}","Whether to adjust the load data manuall scaling_factor,--,float,"Global correction factor for the load time series." fixed_year,--,Year or False,"To specify a fixed year for the load time series that deviates from the snapshots' year" supplement_synthetic,bool,"{true, false}","Whether to supplement missing data for selected time period should be supplemented by synthetic data from https://zenodo.org/records/10820928." +distribution_key,--,--,"Distribution key for spatially disaggregating the per-country electricity demand data." +-- gdp,float,"[0, 1]","Weighting factor for the GDP data in the distribution key." +-- population,float,"[0, 1]","Weighting factor for the population data in the distribution key." diff --git a/doc/configtables/offwind-ac.csv b/doc/configtables/offwind-ac.csv index c9c7c8435..7dc8a4435 100644 --- a/doc/configtables/offwind-ac.csv +++ b/doc/configtables/offwind-ac.csv @@ -15,3 +15,4 @@ max_depth,m,float,"Maximum sea water depth at which wind turbines can be build. min_shore_distance,m,float,"Minimum distance to the shore below which wind turbines cannot be build. Such areas close to the shore are excluded in the process of calculating the AC-connected offshore wind potential." max_shore_distance,m,float,"Maximum distance to the shore above which wind turbines cannot be build. Such areas close to the shore are excluded in the process of calculating the AC-connected offshore wind potential." clip_p_max_pu,p.u.,float,"To avoid too small values in the renewables` per-unit availability time series values below this threshold are set to zero." +landfall_length,km,float,"Fixed length of the cable connection that is onshorelandfall in km. If 'centroid', the length is calculated as the distance to centroid of the onshore bus." diff --git a/doc/configtables/offwind-dc.csv b/doc/configtables/offwind-dc.csv index 86bf0cdcf..c78e06245 100644 --- a/doc/configtables/offwind-dc.csv +++ b/doc/configtables/offwind-dc.csv @@ -15,3 +15,4 @@ max_depth,m,float,"Maximum sea water depth at which wind turbines can be build. min_shore_distance,m,float,"Minimum distance to the shore below which wind turbines cannot be build." max_shore_distance,m,float,"Maximum distance to the shore above which wind turbines cannot be build." clip_p_max_pu,p.u.,float,"To avoid too small values in the renewables` per-unit availability time series values below this threshold are set to zero." +landfall_length,km,float,"Fixed length of the cable connection that is onshorelandfall in km. If 'centroid', the length is calculated as the distance to centroid of the onshore bus." diff --git a/doc/configtables/scenario.csv b/doc/configtables/scenario.csv index d456be801..a4e662d87 100644 --- a/doc/configtables/scenario.csv +++ b/doc/configtables/scenario.csv @@ -1,5 +1,4 @@ ,Unit,Values,Description -simpl,--,cf. :ref:`simpl`,"List of ``{simpl}`` wildcards to run." clusters,--,cf. :ref:`clusters`,"List of ``{clusters}`` wildcards to run." ll,--,cf. :ref:`ll`,"List of ``{ll}`` wildcards to run." opts,--,cf. :ref:`opts`,"List of ``{opts}`` wildcards to run." diff --git a/doc/configuration.rst b/doc/configuration.rst index e140261ab..38adf1c48 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -87,7 +87,7 @@ facilitate running multiple scenarios through a single command For each wildcard, a **list of values** is provided. The rule ``solve_all_elec_networks`` will trigger the rules for creating -``results/networks/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc`` for **all +``results/networks/base_s_{clusters}_elec_l{ll}_{opts}.nc`` for **all combinations** of the provided wildcard values as defined by Python's `itertools.product(...) `__ function diff --git a/doc/img/intro-workflow.png b/doc/img/intro-workflow.png index 87cec61f99f19fbfaeb220346b08e0130bb3172d..e75e75e864d585f6540c60f9f5a0c4931b2e1855 100644 GIT binary patch literal 445946 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 $Xm1wl@7vNwZ|jQFMm3M;5TRTJp;k-i%OXqC^S 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 z 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

P?PuclKkk#}Gpp;ZX5v=wuCyeCOq+BS8gc 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 QJtz-(y85}Sb4q9e0Os8m$^ZZW diff --git a/doc/introduction.rst b/doc/introduction.rst index d0f7386de..0d0bd4131 100644 --- a/doc/introduction.rst +++ b/doc/introduction.rst @@ -35,7 +35,7 @@ For instance, an invocation to .. code:: bash - .../pypsa-eur % snakemake -call results/networks/elec_s_128_ec_lvopt_.nc + .../pypsa-eur % snakemake -call results/networks/base_s_128_elec_lvopt_.nc follows this dependency graph @@ -50,7 +50,7 @@ preceding rules which another rule takes as input data. .. note:: The dependency graph was generated using - ``snakemake --dag results/networks/elec_s_128_ec_lvopt_.nc -F | sed -n "/digraph/,/}/p" | dot -Tpng -o doc/img/intro-workflow.png`` + ``snakemake --dag results/networks/base_s_128_elec_lvopt_.nc -F | sed -n "/digraph/,/}/p" | dot -Tpng -o doc/img/intro-workflow.png`` For the use of ``snakemake``, it makes sense to familiarize yourself quickly with the `basic tutorial diff --git a/doc/preparation.rst b/doc/preparation.rst index 8de4af42b..ebae97d1f 100644 --- a/doc/preparation.rst +++ b/doc/preparation.rst @@ -27,11 +27,12 @@ Then the process continues by calculating conventional power plant capacities, p - :mod:`build_powerplants` for today's thermal power plant capacities using `powerplantmatching `__ allocating these to the closest substation for each powerplant, - :mod:`build_ship_raster` for building shipping traffic density, +- :mod:`determine_availability_matrix` for the land eligibility analysis of each cutout grid cell for PV, onshore and offshore wind, - :mod:`build_renewable_profiles` for the hourly capacity factors and installation potentials constrained by land-use in each substation's Voronoi cell for PV, onshore and offshore wind, and - :mod:`build_hydro_profile` for the hourly per-unit hydro power availability time series. The central rule :mod:`add_electricity` then ties all the different data inputs -together into a detailed PyPSA network stored in ``networks/elec.nc``. +together into a detailed PyPSA network stored in ``networks/base_s_{clusters}_elec.nc``. .. _cutout: @@ -115,6 +116,15 @@ Rule ``determine_availability_matrix_MD_UA`` .. automodule:: determine_availability_matrix_MD_UA + +.. _renewableprofiles: + +Rule ``determine_availability_matrix`` +====================================== + +.. automodule:: determine_availability_matrix + + .. _renewableprofiles: Rule ``build_renewable_profiles`` @@ -129,10 +139,3 @@ Rule ``build_hydro_profile`` =============================== .. automodule:: build_hydro_profile - -.. _electricity: - -Rule ``add_electricity`` -============================= - -.. automodule:: add_electricity diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 0e3bb751e..501bb5d8e 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -8,7 +8,8 @@ Release Notes ########################################## -.. Upcoming Release +Upcoming Release +================ PyPSA-Eur 0.13.0 (13th September 2024) ====================================== @@ -115,6 +116,62 @@ PyPSA-Eur 0.13.0 (13th September 2024) * The sources of nearly all data files are now listed in the documentation. (https://github.com/PyPSA/pypsa-eur/pull/1284) +* Rearranged workflow to cluster the electricity network before calculating + renewable profiles and adding further electricity system components. + + - Moved rules ``simplify_network`` and ``cluster_network`` before + ``add_electricity`` and ``build_renewable_profiles``. + + - Split rule ``build_renewable_profiles`` into two separate rules, + ``determine_availability_matrix`` for land eligibility analysis and + ``build_renewable_profiles``, which now only computes the profiles and total + potentials from the pre-computed availability matrix. + + - Removed variables ``weight``, ``underwater_fraction``, and ``potential`` from the + output of ``build_renewable_profiles`` as it is no longer needed. + + - HAC-clustering is now based on wind speeds and irradiation time series + rather than capacity factors of wind and solar power plants. + + - Added new rule ``build_hac_features`` that aggregates cutout weather data to + base regions in preparation for ``cluster_network``. + + - Removed ``{simpl}`` wildcard and all associated code of the ``m`` suffix of + the ``{cluster}`` wildcard. This means that the option to pre-cluster the + network in ``simplify_network`` was removed. It will be superseded by + clustering renewable profiles and potentials within clustered regions by + resource classes soon. + + - Added new rule ``add_transmission_projects_and_dlr`` which adds the outputs + from ``build_line_rating`` and ``build_transmission_projects`` to the output + of ``base_network``. + + - The rule ``add_extra_components`` was integrated into ``add_electricity`` + + - Added new rule ``build_electricity_demand_base`` to determine the load + distribution of the substations in the base network (which was previously + done in ``add_electricity``). This time series is used as weights for + kmeans-clustering in ``cluster_network`` and is later added to the network in + ``add_electricity`` in aggregated form. + + - The weights of the kmeans clustering algorithm are now exclusively based on + the load distribution. Previously, they also included the distribution of + thermal capacity. + + - Since the networks no longer start with the whole electricity system added + pre-clustering, the files have been renamed from ``elec...nc`` to + ``base...nc`` to identify them as derivatives of ``base.nc``. + + - The scripts ``simplify_network.py`` and ``cluster_network.py`` were + simplified to become less nested and profited from the removed need to deal + with cost data. + + - New configuration options to calculate connection costs of offshore wind + plants. Offshore connection costs are now calculated based on the underwater + distance to the shoreline plus a configurable ``landfall_length`` which + defaults to 10 km. Previously the distance to the region's centroid was + used, which is not practical when the regions are already aggregated. + PyPSA-Eur 0.12.0 (30th August 2024) =================================== diff --git a/doc/simplification.rst b/doc/simplification.rst index cb0484617..f313e7cb8 100644 --- a/doc/simplification.rst +++ b/doc/simplification.rst @@ -9,7 +9,7 @@ Simplifying Electricity Networks ########################################## -The simplification ``snakemake`` rules prepare **approximations** of the full model, for which it is computationally viable to co-optimize generation, storage and transmission capacities. +The simplification ``snakemake`` rules prepare **approximations** of the network model, for which it is computationally viable to co-optimize generation, storage and transmission capacities. - :mod:`simplify_network` transforms the transmission grid to a 380 kV only equivalent network, while - :mod:`cluster_network` uses a `k-means `__ based clustering technique to partition the network into a given number of zones and then reduce the network to a representation with one bus per zone. @@ -18,7 +18,7 @@ The simplification and clustering steps are described in detail in the paper - Jonas Hörsch and Tom Brown. `The role of spatial scale in joint optimisations of generation and transmission for European highly renewable scenarios `__), *14th International Conference on the European Energy Market*, 2017. `arXiv:1705.07617 `__, `doi:10.1109/EEM.2017.7982024 `__. -After simplification and clustering of the network, additional components may be appended in the rule :mod:`add_extra_components` and the network is prepared for solving in :mod:`prepare_network`. +After simplification and clustering of the network, further electricity network components may be appended in the rule :mod:`add_electricity` and the network is prepared for solving in :mod:`prepare_network`. .. _simplify: @@ -34,13 +34,12 @@ Rule ``cluster_network`` .. automodule:: cluster_network -.. _extra_components: +.. _electricity: -Rule ``add_extra_components`` +Rule ``add_electricity`` ============================= -.. automodule:: add_extra_components - +.. automodule:: add_electricity .. _prepare: diff --git a/doc/tutorial.rst b/doc/tutorial.rst index f514491e2..41daa3728 100644 --- a/doc/tutorial.rst +++ b/doc/tutorial.rst @@ -32,7 +32,7 @@ configuration, execute .. code:: bash :class: full-width - snakemake -call results/test-elec/networks/elec_s_6_ec_lcopt_.nc --configfile config/test/config.electricity.yaml + snakemake -call results/test-elec/networks/base_s_6_elec_lcopt_.nc --configfile config/test/config.electricity.yaml This configuration is set to download a reduced cutout via the rule :mod:`retrieve_cutout`. For more information on the data dependencies of PyPSA-Eur, continue reading :ref:`data`. @@ -114,9 +114,9 @@ clustered down to 6 buses and every 24 hours aggregated to one snapshot. The com .. code:: bash - snakemake -call results/test-elec/networks/elec_s_6_ec_lcopt_.nc --configfile config/test/config.electricity.yaml + snakemake -call results/test-elec/networks/base_s_6_elec_lcopt_.nc --configfile config/test/config.electricity.yaml -orders ``snakemake`` to run the rule :mod:`solve_network` that produces the solved network and stores it in ``results/networks`` with the name ``elec_s_6_ec_lcopt_.nc``: +orders ``snakemake`` to run the rule :mod:`solve_network` that produces the solved network and stores it in ``results/networks`` with the name ``base_s_6_elec_lcopt_.nc``: .. literalinclude:: ../rules/solve_electricity.smk :start-at: rule solve_network: @@ -132,98 +132,129 @@ This triggers a workflow of multiple preceding jobs that depend on each rule's i graph[bgcolor=white, margin=0]; node[shape=box, style=rounded, fontname=sans, fontsize=10, penwidth=2]; edge[penwidth=2, color=grey]; - 0[label = "solve_network", color = "0.16 0.6 0.85", style="rounded"]; - 1[label = "prepare_network\nll: copt\nopts: ", color = "0.40 0.6 0.85", style="rounded"]; - 2[label = "add_extra_components", color = "0.03 0.6 0.85", style="rounded"]; - 3[label = "cluster_network\nclusters: 6", color = "0.26 0.6 0.85", style="rounded"]; - 4[label = "simplify_network\nsimpl: ", color = "0.17 0.6 0.85", style="rounded"]; - 5[label = "add_electricity", color = "0.39 0.6 0.85", style="rounded"]; - 6[label = "build_renewable_profiles\ntechnology: solar", color = "0.13 0.6 0.85", style="rounded"]; - 7[label = "base_network", color = "0.01 0.6 0.85", style="rounded"]; - 8[label = "retrieve_osm_prebuilt", color = "0.27 0.6 0.85", style="rounded"]; - 9[label = "build_shapes", color = "0.18 0.6 0.85", style="rounded"]; - 10[label = "retrieve_naturalearth_countries", color = "0.41 0.6 0.85", style="rounded"]; - 11[label = "retrieve_eez", color = "0.14 0.6 0.85", style="rounded"]; - 12[label = "retrieve_databundle", color = "0.38 0.6 0.85", style="rounded"]; - 13[label = "retrieve_cutout\ncutout: be-03-2013-era5", color = "0.51 0.6 0.85", style="rounded"]; - 14[label = "build_renewable_profiles\ntechnology: solar-hsat", color = "0.13 0.6 0.85", style="rounded"]; - 15[label = "build_renewable_profiles\ntechnology: onwind", color = "0.13 0.6 0.85", style="rounded"]; - 16[label = "build_renewable_profiles\ntechnology: offwind-ac", color = "0.13 0.6 0.85", style="rounded"]; - 17[label = "build_ship_raster", color = "0.16 0.6 0.85", style="rounded"]; - 18[label = "retrieve_ship_raster", color = "0.53 0.6 0.85", style="rounded"]; - 19[label = "build_renewable_profiles\ntechnology: offwind-dc", color = "0.13 0.6 0.85", style="rounded"]; - 20[label = "build_renewable_profiles\ntechnology: offwind-float", color = "0.13 0.6 0.85", style="rounded"]; - 21[label = "build_line_rating", color = "0.46 0.6 0.85", style="rounded"]; - 22[label = "build_transmission_projects", color = "0.29 0.6 0.85", style="rounded"]; - 23[label = "retrieve_cost_data\nyear: 2030", color = "0.11 0.6 0.85", style="rounded"]; - 24[label = "build_powerplants", color = "0.18 0.6 0.85", style="rounded"]; - 25[label = "build_electricity_demand", color = "0.30 0.6 0.85", style="rounded"]; - 26[label = "retrieve_electricity_demand", color = "0.13 0.6 0.85", style="rounded"]; - 27[label = "retrieve_synthetic_electricity_demand", color = "0.43 0.6 0.85", style="rounded"]; + 0[label = "solve_network", color = "0.19 0.6 0.85", style="rounded"]; + 1[label = "prepare_network\nll: copt\nopts: ", color = "0.24 0.6 0.85", style="rounded"]; + 2[label = "add_electricity", color = "0.35 0.6 0.85", style="rounded"]; + 3[label = "build_renewable_profiles", color = "0.15 0.6 0.85", style="rounded"]; + 4[label = "determine_availability_matrix\ntechnology: solar", color = "0.39 0.6 0.85", style="rounded"]; + 5[label = "retrieve_databundle", color = "0.65 0.6 0.85", style="rounded"]; + 6[label = "build_shapes", color = "0.45 0.6 0.85", style="rounded"]; + 7[label = "retrieve_naturalearth_countries", color = "0.03 0.6 0.85", style="rounded"]; + 8[label = "retrieve_eez", color = "0.17 0.6 0.85", style="rounded"]; + 9[label = "cluster_network\nclusters: 6", color = "0.38 0.6 0.85", style="rounded"]; + 10[label = "simplify_network", color = "0.14 0.6 0.85", style="rounded"]; + 11[label = "add_transmission_projects_and_dlr", color = "0.61 0.6 0.85", style="rounded"]; + 12[label = "base_network", color = "0.36 0.6 0.85", style="rounded"]; + 13[label = "retrieve_osm_prebuilt", color = "0.22 0.6 0.85", style="rounded"]; + 14[label = "build_line_rating", color = "0.50 0.6 0.85", style="rounded"]; + 15[label = "retrieve_cutout\ncutout: be-03-2013-era5", color = "0.02 0.6 0.85", style="rounded"]; + 16[label = "build_transmission_projects", color = "0.08 0.6 0.85", style="rounded"]; + 17[label = "build_electricity_demand_base", color = "0.11 0.6 0.85", style="rounded"]; + 18[label = "build_electricity_demand", color = "0.60 0.6 0.85", style="rounded"]; + 19[label = "retrieve_electricity_demand", color = "0.60 0.6 0.85", style="rounded"]; + 20[label = "retrieve_synthetic_electricity_demand", color = "0.32 0.6 0.85", style="rounded"]; + 21[label = "build_renewable_profiles", color = "0.15 0.6 0.85", style="rounded"]; + 22[label = "determine_availability_matrix\ntechnology: solar-hsat", color = "0.39 0.6 0.85", style="rounded"]; + 23[label = "build_renewable_profiles", color = "0.15 0.6 0.85", style="rounded"]; + 24[label = "determine_availability_matrix\ntechnology: onwind", color = "0.39 0.6 0.85", style="rounded"]; + 25[label = "build_renewable_profiles", color = "0.15 0.6 0.85", style="rounded"]; + 26[label = "determine_availability_matrix\ntechnology: offwind-ac", color = "0.39 0.6 0.85", style="rounded"]; + 27[label = "build_ship_raster", color = "0.12 0.6 0.85", style="rounded"]; + 28[label = "retrieve_ship_raster", color = "0.44 0.6 0.85", style="rounded"]; + 29[label = "build_renewable_profiles", color = "0.15 0.6 0.85", style="rounded"]; + 30[label = "determine_availability_matrix\ntechnology: offwind-dc", color = "0.39 0.6 0.85", style="rounded"]; + 31[label = "build_renewable_profiles", color = "0.15 0.6 0.85", style="rounded"]; + 32[label = "determine_availability_matrix\ntechnology: offwind-float", color = "0.39 0.6 0.85", style="rounded"]; + 33[label = "retrieve_cost_data\nyear: 2030", color = "0.01 0.6 0.85", style="rounded"]; + 34[label = "build_powerplants", color = "0.52 0.6 0.85", style="rounded"]; 1 -> 0 2 -> 1 - 23 -> 1 + 33 -> 1 3 -> 2 + 21 -> 2 23 -> 2 + 25 -> 2 + 29 -> 2 + 31 -> 2 + 9 -> 2 + 33 -> 2 + 34 -> 2 + 17 -> 2 4 -> 3 - 23 -> 3 + 6 -> 3 + 9 -> 3 + 15 -> 3 5 -> 4 - 23 -> 4 - 7 -> 4 - 6 -> 5 - 14 -> 5 - 15 -> 5 - 16 -> 5 - 19 -> 5 - 20 -> 5 - 7 -> 5 - 21 -> 5 - 22 -> 5 - 23 -> 5 - 24 -> 5 - 25 -> 5 - 9 -> 5 + 6 -> 4 + 9 -> 4 + 15 -> 4 7 -> 6 - 12 -> 6 - 9 -> 6 - 13 -> 6 - 8 -> 7 - 9 -> 7 + 8 -> 6 + 5 -> 6 10 -> 9 - 11 -> 9 - 12 -> 9 - 7 -> 14 + 17 -> 9 + 11 -> 10 + 12 -> 10 + 12 -> 11 + 14 -> 11 + 16 -> 11 + 13 -> 12 + 6 -> 12 12 -> 14 - 9 -> 14 - 13 -> 14 - 7 -> 15 - 12 -> 15 - 9 -> 15 - 13 -> 15 - 7 -> 16 + 15 -> 14 12 -> 16 - 17 -> 16 - 9 -> 16 - 13 -> 16 + 6 -> 16 + 10 -> 17 + 6 -> 17 18 -> 17 - 13 -> 17 - 7 -> 19 - 12 -> 19 - 17 -> 19 - 9 -> 19 - 13 -> 19 - 7 -> 20 - 12 -> 20 - 17 -> 20 - 9 -> 20 - 13 -> 20 - 7 -> 21 - 13 -> 21 - 7 -> 22 + 19 -> 18 + 20 -> 18 + 22 -> 21 + 6 -> 21 + 9 -> 21 + 15 -> 21 + 5 -> 22 + 6 -> 22 9 -> 22 - 7 -> 24 + 15 -> 22 + 24 -> 23 + 6 -> 23 + 9 -> 23 + 15 -> 23 + 5 -> 24 + 6 -> 24 + 9 -> 24 + 15 -> 24 26 -> 25 - 27 -> 25 + 6 -> 25 + 9 -> 25 + 15 -> 25 + 5 -> 26 + 27 -> 26 + 6 -> 26 + 9 -> 26 + 15 -> 26 + 28 -> 27 + 15 -> 27 + 30 -> 29 + 6 -> 29 + 9 -> 29 + 15 -> 29 + 5 -> 30 + 27 -> 30 + 6 -> 30 + 9 -> 30 + 15 -> 30 + 32 -> 31 + 6 -> 31 + 9 -> 31 + 15 -> 31 + 5 -> 32 + 27 -> 32 + 6 -> 32 + 9 -> 32 + 15 -> 32 + 9 -> 34 } | @@ -237,9 +268,10 @@ In the terminal, this will show up as a list of jobs to be run: job count ------------------------------------- ------- add_electricity 1 - add_extra_components 1 + add_transmission_projects_and_dlr 1 base_network 1 build_electricity_demand 1 + build_electricity_demand_base 1 build_line_rating 1 build_powerplants 1 build_renewable_profiles 6 @@ -247,6 +279,7 @@ In the terminal, this will show up as a list of jobs to be run: build_ship_raster 1 build_transmission_projects 1 cluster_network 1 + determine_availability_matrix 6 prepare_network 1 retrieve_cost_data 1 retrieve_cutout 1 @@ -259,7 +292,7 @@ In the terminal, this will show up as a list of jobs to be run: retrieve_synthetic_electricity_demand 1 simplify_network 1 solve_network 1 - total 28 + total 35 ``snakemake`` then runs these jobs in the correct order. @@ -269,13 +302,12 @@ A job (here ``simplify_network``) will display its attributes and normally some .. code:: bash rule simplify_network: - input: resources/test/networks/elec.nc, resources/test/costs_2030.csv, resources/test/regions_onshore.geojson, resources/test/regions_offshore.geojson - output: resources/test/networks/elec_s.nc, resources/test/regions_onshore_elec_s.geojson, resources/test/regions_offshore_elec_s.geojson, resources/test/busmap_elec_s.csv - log: logs/test/simplify_network/elec_s.log - jobid: 4 - benchmark: benchmarks/test/simplify_network/elec_s + input: resources/test/networks/base_extended.nc, resources/test/regions_onshore.geojson, resources/test/regions_offshore.geojson + output: resources/test/networks/base_s.nc, resources/test/regions_onshore_base_s.geojson, resources/test/regions_offshore_base_s.geojson, resources/test/busmap_base_s.csv + log: logs/test/simplify_network.log + jobid: 10 + benchmark: benchmarks/test/simplify_network_b reason: Forced execution - wildcards: simpl= resources: tmpdir=, mem_mb=12000, mem_mib=11445 Once the whole worktree is finished, it should state so in the terminal. @@ -291,10 +323,9 @@ You can produce any output file occurring in the ``Snakefile`` by running For example, you can explore the evolution of the PyPSA networks by running #. ``snakemake resources/networks/base.nc -call --configfile config/test/config.electricity.yaml`` -#. ``snakemake resources/networks/elec.nc -call --configfile config/test/config.electricity.yaml`` -#. ``snakemake resources/networks/elec_s.nc -call --configfile config/test/config.electricity.yaml`` -#. ``snakemake resources/networks/elec_s_6.nc -call --configfile config/test/config.electricity.yaml`` -#. ``snakemake resources/networks/elec_s_6_ec_lcopt_.nc -call --configfile config/test/config.electricity.yaml`` +#. ``snakemake resources/networks/base_s.nc -call --configfile config/test/config.electricity.yaml`` +#. ``snakemake resources/networks/base_s_6.nc -call --configfile config/test/config.electricity.yaml`` +#. ``snakemake resources/networks/base_s_6_elec_lcopt_.nc -call --configfile config/test/config.electricity.yaml`` To run all combinations of wildcard values provided in the ``config/config.yaml`` under ``scenario:``, you can use the collection rule ``solve_elec_networks``. @@ -332,6 +363,6 @@ Jupyter Notebooks). import pypsa - n = pypsa.Network("results/networks/elec_s_6_ec_lcopt_.nc") + n = pypsa.Network("results/networks/base_s_6_elec_lcopt_.nc") For inspiration, read the `examples section in the PyPSA documentation `__. diff --git a/doc/tutorial_sector.rst b/doc/tutorial_sector.rst index a369356f2..3455f8f03 100644 --- a/doc/tutorial_sector.rst +++ b/doc/tutorial_sector.rst @@ -69,7 +69,7 @@ which were already included in the electricity-only tutorial: job count ------------------------------------------------ ------- add_electricity 1 - add_extra_components 1 + add_transmission_projects_and_dlr 1 all 1 base_network 1 build_ammonia_production 1 @@ -80,6 +80,7 @@ which were already included in the electricity-only tutorial: build_daily_heat_demand 1 build_district_heat_share 1 build_electricity_demand 1 + build_electricity_demand_base 1 build_energy_totals 1 build_gas_input_locations 1 build_gas_network 1 @@ -102,13 +103,13 @@ which were already included in the electricity-only tutorial: build_shapes 1 build_ship_raster 1 build_shipping_demand 1 - build_simplified_population_layouts 1 build_solar_thermal_profiles 1 build_temperature_profiles 1 build_transmission_projects 1 build_transport_demand 1 cluster_gas_network 1 cluster_network 1 + determine_availability_matrix 6 make_summary 1 plot_gas_network 1 plot_hydrogen_network 1 @@ -127,16 +128,19 @@ which were already included in the electricity-only tutorial: retrieve_gas_infrastructure_data 1 retrieve_gem_europe_gas_tracker 1 retrieve_gem_steel_plant_tracker 1 + retrieve_hotmaps_industrial_sites 1 + retrieve_jrc_enspreso_biomass 1 retrieve_jrc_idees 1 retrieve_naturalearth_countries 1 retrieve_osm_prebuilt 1 retrieve_ship_raster 1 retrieve_synthetic_electricity_demand 1 + retrieve_usgs_ammonia_production 1 retrieve_worldbank_urban_population 1 simplify_network 1 solve_sector_network 1 time_aggregation 1 - total 74 + total 83 This covers the retrieval of additional raw data from online resources and preprocessing data about the transport, industry, and heating sectors as well as @@ -155,266 +159,299 @@ successfully. graph[bgcolor=white, margin=0]; node[shape=box, style=rounded, fontname=sans, fontsize=10, penwidth=2]; edge[penwidth=2, color=grey]; - 0[label = "all", color = "0.22 0.6 0.85", style="rounded"]; - 1[label = "plot_summary", color = "0.11 0.6 0.85", style="rounded"]; - 2[label = "make_summary", color = "0.30 0.6 0.85", style="rounded"]; - 3[label = "solve_sector_network", color = "0.42 0.6 0.85", style="rounded"]; + 0[label = "all", color = "0.41 0.6 0.85", style="rounded"]; + 1[label = "plot_summary", color = "0.60 0.6 0.85", style="rounded"]; + 2[label = "make_summary", color = "0.62 0.6 0.85", style="rounded"]; + 3[label = "solve_sector_network", color = "0.62 0.6 0.85", style="rounded"]; 4[label = "prepare_sector_network\nsector_opts: ", color = "0.45 0.6 0.85", style="rounded"]; - 5[label = "build_renewable_profiles\ntechnology: offwind-ac", color = "0.44 0.6 0.85", style="rounded"]; - 6[label = "base_network", color = "0.26 0.6 0.85", style="rounded"]; - 7[label = "retrieve_osm_prebuilt", color = "0.01 0.6 0.85", style="rounded"]; - 8[label = "build_shapes", color = "0.50 0.6 0.85", style="rounded"]; - 9[label = "retrieve_naturalearth_countries", color = "0.09 0.6 0.85", style="rounded"]; - 10[label = "retrieve_eez", color = "0.52 0.6 0.85", style="rounded"]; - 11[label = "retrieve_databundle", color = "0.00 0.6 0.85", style="rounded"]; - 12[label = "build_ship_raster", color = "0.29 0.6 0.85", style="rounded"]; - 13[label = "retrieve_ship_raster", color = "0.13 0.6 0.85", style="rounded"]; - 14[label = "retrieve_cutout\ncutout: be-03-2013-era5", color = "0.06 0.6 0.85", style="rounded"]; - 15[label = "build_renewable_profiles\ntechnology: offwind-dc", color = "0.44 0.6 0.85", style="rounded"]; - 16[label = "build_renewable_profiles\ntechnology: offwind-float", color = "0.44 0.6 0.85", style="rounded"]; - 17[label = "cluster_gas_network", color = "0.48 0.6 0.85", style="rounded"]; - 18[label = "build_gas_network", color = "0.59 0.6 0.85", style="rounded"]; - 19[label = "retrieve_gas_infrastructure_data", color = "0.14 0.6 0.85", style="rounded"]; - 20[label = "cluster_network\nclusters: 5", color = "0.08 0.6 0.85", style="rounded"]; - 21[label = "simplify_network\nsimpl: ", color = "0.25 0.6 0.85", style="rounded"]; - 22[label = "add_electricity", color = "0.46 0.6 0.85", style="rounded"]; - 23[label = "build_renewable_profiles\ntechnology: solar", color = "0.44 0.6 0.85", style="rounded"]; - 24[label = "build_renewable_profiles\ntechnology: solar-hsat", color = "0.44 0.6 0.85", style="rounded"]; - 25[label = "build_renewable_profiles\ntechnology: onwind", color = "0.44 0.6 0.85", style="rounded"]; - 26[label = "build_transmission_projects", color = "0.63 0.6 0.85", style="rounded"]; - 27[label = "retrieve_cost_data\nyear: 2030", color = "0.05 0.6 0.85", style="rounded"]; - 28[label = "build_powerplants", color = "0.43 0.6 0.85", style="rounded"]; - 29[label = "build_electricity_demand", color = "0.39 0.6 0.85", style="rounded"]; - 30[label = "retrieve_electricity_demand", color = "0.62 0.6 0.85", style="rounded"]; - 31[label = "retrieve_synthetic_electricity_demand", color = "0.31 0.6 0.85", style="rounded"]; - 32[label = "build_gas_input_locations", color = "0.45 0.6 0.85", style="rounded"]; - 33[label = "retrieve_gem_europe_gas_tracker", color = "0.33 0.6 0.85", style="rounded"]; - 34[label = "time_aggregation", color = "0.60 0.6 0.85", style="rounded"]; - 35[label = "prepare_network\nll: v1.5\nopts: ", color = "0.23 0.6 0.85", style="rounded"]; - 36[label = "add_extra_components", color = "0.36 0.6 0.85", style="rounded"]; - 37[label = "build_hourly_heat_demand", color = "0.15 0.6 0.85", style="rounded"]; - 38[label = "build_daily_heat_demand", color = "0.57 0.6 0.85", style="rounded"]; - 39[label = "build_population_layouts", color = "0.47 0.6 0.85", style="rounded"]; - 40[label = "retrieve_worldbank_urban_population", color = "0.19 0.6 0.85", style="rounded"]; - 41[label = "build_solar_thermal_profiles", color = "0.11 0.6 0.85", style="rounded"]; - 42[label = "retrieve_eurostat_data", color = "0.04 0.6 0.85", style="rounded"]; - 43[label = "build_population_weighted_energy_totals\nkind: energy", color = "0.04 0.6 0.85", style="rounded"]; - 44[label = "build_energy_totals", color = "0.30 0.6 0.85", style="rounded"]; - 45[label = "retrieve_jrc_idees", color = "0.02 0.6 0.85", style="rounded"]; - 46[label = "retrieve_eurostat_household_data", color = "0.49 0.6 0.85", style="rounded"]; - 47[label = "build_clustered_population_layouts", color = "0.19 0.6 0.85", style="rounded"]; - 48[label = "build_population_weighted_energy_totals\nkind: heat", color = "0.04 0.6 0.85", style="rounded"]; - 49[label = "build_heat_totals", color = "0.08 0.6 0.85", style="rounded"]; - 50[label = "build_shipping_demand", color = "0.52 0.6 0.85", style="rounded"]; - 51[label = "build_transport_demand", color = "0.16 0.6 0.85", style="rounded"]; - 52[label = "build_temperature_profiles", color = "0.58 0.6 0.85", style="rounded"]; - 53[label = "build_biomass_potentials\nplanning_horizons: 2030", color = "0.55 0.6 0.85", style="rounded"]; - 54[label = "build_salt_cavern_potentials", color = "0.28 0.6 0.85", style="rounded"]; - 55[label = "build_simplified_population_layouts", color = "0.14 0.6 0.85", style="rounded"]; - 56[label = "build_industrial_energy_demand_per_node", color = "0.24 0.6 0.85", style="rounded"]; - 57[label = "build_industry_sector_ratios_intermediate\nplanning_horizons: 2030", color = "0.60 0.6 0.85", style="rounded"]; - 58[label = "build_industry_sector_ratios", color = "0.26 0.6 0.85", style="rounded"]; - 59[label = "build_ammonia_production", color = "0.16 0.6 0.85", style="rounded"]; - 60[label = "build_industrial_energy_demand_per_country_today", color = "0.18 0.6 0.85", style="rounded"]; - 61[label = "build_industrial_production_per_country", color = "0.61 0.6 0.85", style="rounded"]; - 62[label = "build_industrial_production_per_node", color = "0.65 0.6 0.85", style="rounded"]; - 63[label = "build_industrial_distribution_key", color = "0.31 0.6 0.85", style="rounded"]; - 64[label = "retrieve_gem_steel_plant_tracker", color = "0.27 0.6 0.85", style="rounded"]; - 65[label = "build_industrial_production_per_country_tomorrow\nplanning_horizons: 2030", color = "0.09 0.6 0.85", style="rounded"]; - 66[label = "build_industrial_energy_demand_per_node_today", color = "0.40 0.6 0.85", style="rounded"]; - 67[label = "build_district_heat_share\nplanning_horizons: 2030", color = "0.07 0.6 0.85", style="rounded"]; - 68[label = "build_cop_profiles", color = "0.38 0.6 0.85", style="rounded"]; - 69[label = "build_central_heating_temperature_profiles", color = "0.55 0.6 0.85", style="rounded"]; - 70[label = "plot_power_network_clustered", color = "0.20 0.6 0.85", style="rounded"]; - 71[label = "plot_power_network", color = "0.53 0.6 0.85", style="rounded"]; - 72[label = "plot_hydrogen_network", color = "0.64 0.6 0.85", style="rounded"]; - 73[label = "plot_gas_network", color = "0.28 0.6 0.85", style="rounded"]; + 5[label = "build_renewable_profiles", color = "0.20 0.6 0.85", style="rounded"]; + 6[label = "determine_availability_matrix\ntechnology: offwind-ac", color = "0.24 0.6 0.85", style="rounded"]; + 7[label = "retrieve_databundle", color = "0.58 0.6 0.85", style="rounded"]; + 8[label = "build_ship_raster", color = "0.51 0.6 0.85", style="rounded"]; + 9[label = "retrieve_ship_raster", color = "0.03 0.6 0.85", style="rounded"]; + 10[label = "retrieve_cutout\ncutout: be-03-2013-era5", color = "0.32 0.6 0.85", style="rounded"]; + 11[label = "build_shapes", color = "0.11 0.6 0.85", style="rounded"]; + 12[label = "retrieve_naturalearth_countries", color = "0.63 0.6 0.85", style="rounded"]; + 13[label = "retrieve_eez", color = "0.00 0.6 0.85", style="rounded"]; + 14[label = "cluster_network\nclusters: 5", color = "0.64 0.6 0.85", style="rounded"]; + 15[label = "simplify_network", color = "0.21 0.6 0.85", style="rounded"]; + 16[label = "add_transmission_projects_and_dlr", color = "0.17 0.6 0.85", style="rounded"]; + 17[label = "base_network", color = "0.53 0.6 0.85", style="rounded"]; + 18[label = "retrieve_osm_prebuilt", color = "0.21 0.6 0.85", style="rounded"]; + 19[label = "build_transmission_projects", color = "0.02 0.6 0.85", style="rounded"]; + 20[label = "build_electricity_demand_base", color = "0.44 0.6 0.85", style="rounded"]; + 21[label = "build_electricity_demand", color = "0.16 0.6 0.85", style="rounded"]; + 22[label = "retrieve_electricity_demand", color = "0.06 0.6 0.85", style="rounded"]; + 23[label = "retrieve_synthetic_electricity_demand", color = "0.09 0.6 0.85", style="rounded"]; + 24[label = "build_renewable_profiles", color = "0.20 0.6 0.85", style="rounded"]; + 25[label = "determine_availability_matrix\ntechnology: offwind-dc", color = "0.24 0.6 0.85", style="rounded"]; + 26[label = "build_renewable_profiles", color = "0.20 0.6 0.85", style="rounded"]; + 27[label = "determine_availability_matrix\ntechnology: offwind-float", color = "0.24 0.6 0.85", style="rounded"]; + 28[label = "cluster_gas_network", color = "0.39 0.6 0.85", style="rounded"]; + 29[label = "build_gas_network", color = "0.29 0.6 0.85", style="rounded"]; + 30[label = "retrieve_gas_infrastructure_data", color = "0.25 0.6 0.85", style="rounded"]; + 31[label = "build_gas_input_locations", color = "0.58 0.6 0.85", style="rounded"]; + 32[label = "retrieve_gem_europe_gas_tracker", color = "0.05 0.6 0.85", style="rounded"]; + 33[label = "time_aggregation", color = "0.66 0.6 0.85", style="rounded"]; + 34[label = "prepare_network\nll: v1.5\nopts: ", color = "0.55 0.6 0.85", style="rounded"]; + 35[label = "add_electricity", color = "0.36 0.6 0.85", style="rounded"]; + 36[label = "build_renewable_profiles", color = "0.20 0.6 0.85", style="rounded"]; + 37[label = "determine_availability_matrix\ntechnology: solar", color = "0.24 0.6 0.85", style="rounded"]; + 38[label = "build_renewable_profiles", color = "0.20 0.6 0.85", style="rounded"]; + 39[label = "determine_availability_matrix\ntechnology: solar-hsat", color = "0.24 0.6 0.85", style="rounded"]; + 40[label = "build_renewable_profiles", color = "0.20 0.6 0.85", style="rounded"]; + 41[label = "determine_availability_matrix\ntechnology: onwind", color = "0.24 0.6 0.85", style="rounded"]; + 42[label = "retrieve_cost_data\nyear: 2030", color = "0.55 0.6 0.85", style="rounded"]; + 43[label = "build_powerplants", color = "0.18 0.6 0.85", style="rounded"]; + 44[label = "build_hourly_heat_demand", color = "0.29 0.6 0.85", style="rounded"]; + 45[label = "build_daily_heat_demand", color = "0.40 0.6 0.85", style="rounded"]; + 46[label = "build_population_layouts", color = "0.27 0.6 0.85", style="rounded"]; + 47[label = "retrieve_worldbank_urban_population", color = "0.30 0.6 0.85", style="rounded"]; + 48[label = "build_solar_thermal_profiles", color = "0.27 0.6 0.85", style="rounded"]; + 49[label = "retrieve_eurostat_data", color = "0.13 0.6 0.85", style="rounded"]; + 50[label = "build_population_weighted_energy_totals\nkind: energy", color = "0.24 0.6 0.85", style="rounded"]; + 51[label = "build_energy_totals", color = "0.26 0.6 0.85", style="rounded"]; + 52[label = "retrieve_jrc_idees", color = "0.48 0.6 0.85", style="rounded"]; + 53[label = "retrieve_eurostat_household_data", color = "0.12 0.6 0.85", style="rounded"]; + 54[label = "build_clustered_population_layouts", color = "0.35 0.6 0.85", style="rounded"]; + 55[label = "build_population_weighted_energy_totals\nkind: heat", color = "0.24 0.6 0.85", style="rounded"]; + 56[label = "build_heat_totals", color = "0.01 0.6 0.85", style="rounded"]; + 57[label = "build_shipping_demand", color = "0.60 0.6 0.85", style="rounded"]; + 58[label = "build_transport_demand", color = "0.50 0.6 0.85", style="rounded"]; + 59[label = "build_temperature_profiles", color = "0.54 0.6 0.85", style="rounded"]; + 60[label = "build_biomass_potentials\nplanning_horizons: 2030", color = "0.45 0.6 0.85", style="rounded"]; + 61[label = "retrieve_jrc_enspreso_biomass", color = "0.07 0.6 0.85", style="rounded"]; + 62[label = "build_salt_cavern_potentials", color = "0.18 0.6 0.85", style="rounded"]; + 63[label = "build_industrial_energy_demand_per_node", color = "0.65 0.6 0.85", style="rounded"]; + 64[label = "build_industry_sector_ratios_intermediate\nplanning_horizons: 2030", color = "0.64 0.6 0.85", style="rounded"]; + 65[label = "build_industry_sector_ratios", color = "0.51 0.6 0.85", style="rounded"]; + 66[label = "build_ammonia_production", color = "0.15 0.6 0.85", style="rounded"]; + 67[label = "retrieve_usgs_ammonia_production", color = "0.38 0.6 0.85", style="rounded"]; + 68[label = "build_industrial_energy_demand_per_country_today", color = "0.65 0.6 0.85", style="rounded"]; + 69[label = "build_industrial_production_per_country", color = "0.11 0.6 0.85", style="rounded"]; + 70[label = "build_industrial_production_per_node", color = "0.07 0.6 0.85", style="rounded"]; + 71[label = "build_industrial_distribution_key", color = "0.48 0.6 0.85", style="rounded"]; + 72[label = "retrieve_hotmaps_industrial_sites", color = "0.20 0.6 0.85", style="rounded"]; + 73[label = "retrieve_gem_steel_plant_tracker", color = "0.10 0.6 0.85", style="rounded"]; + 74[label = "build_industrial_production_per_country_tomorrow\nplanning_horizons: 2030", color = "0.34 0.6 0.85", style="rounded"]; + 75[label = "build_industrial_energy_demand_per_node_today", color = "0.28 0.6 0.85", style="rounded"]; + 76[label = "build_district_heat_share\nplanning_horizons: 2030", color = "0.57 0.6 0.85", style="rounded"]; + 77[label = "build_cop_profiles", color = "0.02 0.6 0.85", style="rounded"]; + 78[label = "build_central_heating_temperature_profiles", color = "0.15 0.6 0.85", style="rounded"]; + 79[label = "plot_power_network_clustered", color = "0.43 0.6 0.85", style="rounded"]; + 80[label = "plot_power_network", color = "0.05 0.6 0.85", style="rounded"]; + 81[label = "plot_hydrogen_network", color = "0.52 0.6 0.85", style="rounded"]; + 82[label = "plot_gas_network", color = "0.46 0.6 0.85", style="rounded"]; 1 -> 0 2 -> 1 - 42 -> 1 - 11 -> 1 + 49 -> 1 + 7 -> 1 3 -> 2 - 27 -> 2 - 70 -> 2 - 71 -> 2 - 72 -> 2 - 73 -> 2 + 42 -> 2 + 79 -> 2 + 80 -> 2 + 81 -> 2 + 82 -> 2 4 -> 3 5 -> 4 - 15 -> 4 - 16 -> 4 - 17 -> 4 - 32 -> 4 + 24 -> 4 + 26 -> 4 + 28 -> 4 + 31 -> 4 + 33 -> 4 34 -> 4 - 35 -> 4 - 42 -> 4 - 43 -> 4 - 48 -> 4 + 49 -> 4 50 -> 4 + 55 -> 4 + 57 -> 4 + 58 -> 4 51 -> 4 - 44 -> 4 - 11 -> 4 - 53 -> 4 - 27 -> 4 + 7 -> 4 + 60 -> 4 + 42 -> 4 + 62 -> 4 + 15 -> 4 + 14 -> 4 54 -> 4 - 21 -> 4 - 20 -> 4 - 47 -> 4 - 55 -> 4 - 56 -> 4 - 37 -> 4 - 67 -> 4 - 52 -> 4 - 68 -> 4 - 41 -> 4 + 63 -> 4 + 44 -> 4 + 70 -> 4 + 76 -> 4 + 59 -> 4 + 77 -> 4 + 48 -> 4 6 -> 5 11 -> 5 - 12 -> 5 - 8 -> 5 14 -> 5 + 10 -> 5 7 -> 6 8 -> 6 + 11 -> 6 + 14 -> 6 + 10 -> 6 9 -> 8 10 -> 8 - 11 -> 8 - 13 -> 12 - 14 -> 12 - 6 -> 15 - 11 -> 15 - 12 -> 15 - 8 -> 15 - 14 -> 15 - 6 -> 16 - 11 -> 16 - 12 -> 16 - 8 -> 16 - 14 -> 16 + 12 -> 11 + 13 -> 11 + 7 -> 11 + 15 -> 14 + 20 -> 14 + 16 -> 15 + 17 -> 15 + 17 -> 16 + 19 -> 16 18 -> 17 - 20 -> 17 - 19 -> 18 + 11 -> 17 + 17 -> 19 + 11 -> 19 + 15 -> 20 + 11 -> 20 21 -> 20 - 27 -> 20 22 -> 21 - 27 -> 21 - 6 -> 21 - 23 -> 22 - 24 -> 22 - 25 -> 22 - 5 -> 22 - 15 -> 22 - 16 -> 22 - 6 -> 22 - 26 -> 22 - 27 -> 22 - 28 -> 22 - 29 -> 22 - 8 -> 22 - 6 -> 23 - 11 -> 23 - 8 -> 23 - 14 -> 23 - 6 -> 24 + 23 -> 21 + 25 -> 24 11 -> 24 - 8 -> 24 14 -> 24 - 6 -> 25 - 11 -> 25 + 10 -> 24 + 7 -> 25 8 -> 25 + 11 -> 25 14 -> 25 - 6 -> 26 - 8 -> 26 - 6 -> 28 + 10 -> 25 + 27 -> 26 + 11 -> 26 + 14 -> 26 + 10 -> 26 + 7 -> 27 + 8 -> 27 + 11 -> 27 + 14 -> 27 + 10 -> 27 + 29 -> 28 + 14 -> 28 30 -> 29 - 31 -> 29 - 33 -> 32 - 19 -> 32 - 20 -> 32 + 32 -> 31 + 30 -> 31 + 14 -> 31 + 34 -> 33 + 44 -> 33 + 48 -> 33 35 -> 34 - 37 -> 34 - 41 -> 34 + 42 -> 34 36 -> 35 - 27 -> 35 - 20 -> 36 - 27 -> 36 - 38 -> 37 + 38 -> 35 + 40 -> 35 + 5 -> 35 + 24 -> 35 + 26 -> 35 + 14 -> 35 + 42 -> 35 + 43 -> 35 + 20 -> 35 + 37 -> 36 + 11 -> 36 + 14 -> 36 + 10 -> 36 + 7 -> 37 + 11 -> 37 + 14 -> 37 + 10 -> 37 39 -> 38 - 20 -> 38 + 11 -> 38 14 -> 38 - 8 -> 39 - 40 -> 39 + 10 -> 38 + 7 -> 39 + 11 -> 39 14 -> 39 - 39 -> 41 - 20 -> 41 + 10 -> 39 + 41 -> 40 + 11 -> 40 + 14 -> 40 + 10 -> 40 + 7 -> 41 + 11 -> 41 14 -> 41 - 44 -> 43 - 47 -> 43 - 8 -> 44 - 11 -> 44 + 10 -> 41 + 14 -> 43 45 -> 44 - 42 -> 44 - 46 -> 44 - 39 -> 47 - 20 -> 47 - 14 -> 47 - 49 -> 48 - 47 -> 48 - 44 -> 49 - 8 -> 50 - 20 -> 50 - 44 -> 50 - 47 -> 51 - 43 -> 51 - 44 -> 51 + 46 -> 45 + 14 -> 45 + 10 -> 45 + 11 -> 46 + 47 -> 46 + 10 -> 46 + 46 -> 48 + 14 -> 48 + 10 -> 48 + 51 -> 50 + 54 -> 50 11 -> 51 + 7 -> 51 52 -> 51 - 39 -> 52 - 20 -> 52 - 14 -> 52 - 42 -> 53 - 11 -> 53 - 20 -> 53 - 8 -> 53 - 11 -> 54 - 20 -> 54 - 39 -> 55 - 21 -> 55 - 14 -> 55 - 57 -> 56 - 62 -> 56 - 66 -> 56 - 58 -> 57 - 60 -> 57 - 61 -> 57 + 49 -> 51 + 53 -> 51 + 46 -> 54 + 14 -> 54 + 10 -> 54 + 56 -> 55 + 54 -> 55 + 51 -> 56 + 11 -> 57 + 14 -> 57 + 51 -> 57 + 54 -> 58 + 50 -> 58 + 51 -> 58 + 7 -> 58 59 -> 58 - 45 -> 58 - 44 -> 60 - 45 -> 60 + 46 -> 59 + 14 -> 59 + 10 -> 59 61 -> 60 - 59 -> 61 - 45 -> 61 - 42 -> 61 - 63 -> 62 - 65 -> 62 - 20 -> 63 - 47 -> 63 + 49 -> 60 + 7 -> 60 + 14 -> 60 + 11 -> 60 + 7 -> 62 + 14 -> 62 64 -> 63 - 61 -> 65 - 63 -> 66 - 60 -> 66 - 44 -> 67 - 47 -> 67 - 69 -> 68 + 70 -> 63 + 75 -> 63 + 65 -> 64 + 68 -> 64 + 69 -> 64 + 66 -> 65 + 52 -> 65 + 67 -> 66 + 51 -> 68 52 -> 68 - 20 -> 68 + 69 -> 68 + 66 -> 69 52 -> 69 - 20 -> 69 - 20 -> 70 - 3 -> 71 - 20 -> 71 - 3 -> 72 - 20 -> 72 - 3 -> 73 - 20 -> 73 + 49 -> 69 + 71 -> 70 + 74 -> 70 + 14 -> 71 + 54 -> 71 + 72 -> 71 + 73 -> 71 + 69 -> 74 + 71 -> 75 + 68 -> 75 + 51 -> 76 + 54 -> 76 + 78 -> 77 + 59 -> 77 + 14 -> 77 + 59 -> 78 + 14 -> 78 + 14 -> 79 + 3 -> 80 + 14 -> 80 + 3 -> 81 + 14 -> 81 + 3 -> 82 + 14 -> 82 } | @@ -474,430 +511,462 @@ workflow: graph[bgcolor=white, margin=0]; node[shape=box, style=rounded, fontname=sans, fontsize=10, penwidth=2]; edge[penwidth=2, color=grey]; - 0[label = "all", color = "0.65 0.6 0.85", style="rounded"]; + 0[label = "all", color = "0.58 0.6 0.85", style="rounded"]; 1[label = "plot_summary", color = "0.14 0.6 0.85", style="rounded"]; - 2[label = "make_summary", color = "0.38 0.6 0.85", style="rounded"]; - 3[label = "solve_sector_network_myopic", color = "0.19 0.6 0.85", style="rounded"]; - 4[label = "add_existing_baseyear", color = "0.32 0.6 0.85", style="rounded"]; - 5[label = "prepare_sector_network\nsector_opts: ", color = "0.36 0.6 0.85", style="rounded"]; - 6[label = "build_renewable_profiles\ntechnology: offwind-ac", color = "0.57 0.6 0.85", style="rounded"]; - 7[label = "base_network", color = "0.24 0.6 0.85", style="rounded"]; - 8[label = "build_shapes", color = "0.51 0.6 0.85", style="rounded"]; - 9[label = "retrieve_databundle", color = "0.31 0.6 0.85", style="rounded"]; - 10[label = "build_ship_raster", color = "0.23 0.6 0.85", style="rounded"]; - 11[label = "retrieve_ship_raster", color = "0.58 0.6 0.85", style="rounded"]; - 12[label = "retrieve_cutout\ncutout: be-03-2013-era5", color = "0.21 0.6 0.85", style="rounded"]; - 13[label = "build_renewable_profiles\ntechnology: offwind-dc", color = "0.57 0.6 0.85", style="rounded"]; - 14[label = "build_renewable_profiles\ntechnology: offwind-float", color = "0.57 0.6 0.85", style="rounded"]; - 15[label = "cluster_gas_network", color = "0.06 0.6 0.85", style="rounded"]; - 16[label = "build_gas_network", color = "0.29 0.6 0.85", style="rounded"]; - 17[label = "retrieve_gas_infrastructure_data", color = "0.18 0.6 0.85", style="rounded"]; - 18[label = "cluster_network\nclusters: 5", color = "0.59 0.6 0.85", style="rounded"]; - 19[label = "simplify_network\nsimpl: ", color = "0.41 0.6 0.85", style="rounded"]; - 20[label = "add_electricity", color = "0.25 0.6 0.85", style="rounded"]; - 21[label = "build_renewable_profiles\ntechnology: solar", color = "0.57 0.6 0.85", style="rounded"]; - 22[label = "build_renewable_profiles\ntechnology: solar-hsat", color = "0.57 0.6 0.85", style="rounded"]; - 23[label = "build_renewable_profiles\ntechnology: onwind", color = "0.57 0.6 0.85", style="rounded"]; - 24[label = "retrieve_cost_data\nyear: 2030", color = "0.41 0.6 0.85", style="rounded"]; - 25[label = "build_powerplants", color = "0.36 0.6 0.85", style="rounded"]; - 26[label = "build_electricity_demand", color = "0.12 0.6 0.85", style="rounded"]; - 27[label = "retrieve_electricity_demand", color = "0.23 0.6 0.85", style="rounded"]; - 28[label = "retrieve_synthetic_electricity_demand", color = "0.35 0.6 0.85", style="rounded"]; - 29[label = "build_gas_input_locations", color = "0.10 0.6 0.85", style="rounded"]; - 30[label = "time_aggregation", color = "0.60 0.6 0.85", style="rounded"]; - 31[label = "prepare_network\nll: v1.5\nopts: ", color = "0.37 0.6 0.85", style="rounded"]; - 32[label = "add_extra_components", color = "0.10 0.6 0.85", style="rounded"]; - 33[label = "build_hourly_heat_demand", color = "0.11 0.6 0.85", style="rounded"]; - 34[label = "build_daily_heat_demand\nscope: total", color = "0.39 0.6 0.85", style="rounded"]; - 35[label = "build_population_layouts", color = "0.40 0.6 0.85", style="rounded"]; - 36[label = "build_solar_thermal_profiles\nscope: total", color = "0.20 0.6 0.85", style="rounded"]; - 37[label = "retrieve_eurostat_data", color = "0.45 0.6 0.85", style="rounded"]; - 38[label = "build_population_weighted_energy_totals\nkind: energy", color = "0.60 0.6 0.85", style="rounded"]; - 39[label = "build_energy_totals", color = "0.48 0.6 0.85", style="rounded"]; - 40[label = "retrieve_eurostat_household_data", color = "0.08 0.6 0.85", style="rounded"]; - 41[label = "build_clustered_population_layouts", color = "0.07 0.6 0.85", style="rounded"]; - 42[label = "build_population_weighted_energy_totals\nkind: heat", color = "0.60 0.6 0.85", style="rounded"]; - 43[label = "build_heat_totals", color = "0.52 0.6 0.85", style="rounded"]; - 44[label = "build_shipping_demand", color = "0.50 0.6 0.85", style="rounded"]; - 45[label = "build_transport_demand", color = "0.49 0.6 0.85", style="rounded"]; - 46[label = "build_temperature_profiles\nscope: total", color = "0.63 0.6 0.85", style="rounded"]; - 47[label = "build_biomass_potentials\nplanning_horizons: 2030", color = "0.56 0.6 0.85", style="rounded"]; - 48[label = "build_salt_cavern_potentials", color = "0.43 0.6 0.85", style="rounded"]; - 49[label = "build_simplified_population_layouts", color = "0.58 0.6 0.85", style="rounded"]; - 50[label = "build_industrial_energy_demand_per_node", color = "0.27 0.6 0.85", style="rounded"]; - 51[label = "build_industry_sector_ratios_intermediate\nplanning_horizons: 2030", color = "0.26 0.6 0.85", style="rounded"]; - 52[label = "build_industry_sector_ratios", color = "0.46 0.6 0.85", style="rounded"]; - 53[label = "build_ammonia_production", color = "0.17 0.6 0.85", style="rounded"]; - 54[label = "build_industrial_energy_demand_per_country_today", color = "0.02 0.6 0.85", style="rounded"]; - 55[label = "build_industrial_production_per_country", color = "0.30 0.6 0.85", style="rounded"]; - 56[label = "build_industrial_production_per_node", color = "0.30 0.6 0.85", style="rounded"]; - 57[label = "build_industrial_distribution_key", color = "0.05 0.6 0.85", style="rounded"]; - 58[label = "build_industrial_production_per_country_tomorrow\nplanning_horizons: 2030", color = "0.01 0.6 0.85", style="rounded"]; - 59[label = "build_industrial_energy_demand_per_node_today", color = "0.14 0.6 0.85", style="rounded"]; - 60[label = "build_district_heat_share\nplanning_horizons: 2030", color = "0.56 0.6 0.85", style="rounded"]; - 61[label = "build_temperature_profiles\nscope: rural", color = "0.63 0.6 0.85", style="rounded"]; - 62[label = "build_temperature_profiles\nscope: urban", color = "0.63 0.6 0.85", style="rounded"]; - 63[label = "build_cop_profiles", color = "0.64 0.6 0.85", style="rounded"]; - 64[label = "build_solar_thermal_profiles\nscope: urban", color = "0.20 0.6 0.85", style="rounded"]; - 65[label = "build_solar_thermal_profiles\nscope: rural", color = "0.20 0.6 0.85", style="rounded"]; - 66[label = "build_existing_heating_distribution", color = "0.21 0.6 0.85", style="rounded"]; - 67[label = "solve_sector_network_myopic", color = "0.19 0.6 0.85", style="rounded"]; - 68[label = "add_brownfield", color = "0.27 0.6 0.85", style="rounded"]; - 69[label = "prepare_sector_network\nsector_opts: ", color = "0.36 0.6 0.85", style="rounded"]; - 70[label = "build_biomass_potentials\nplanning_horizons: 2040", color = "0.56 0.6 0.85", style="rounded"]; - 71[label = "retrieve_cost_data\nyear: 2040", color = "0.41 0.6 0.85", style="rounded"]; - 72[label = "build_industrial_energy_demand_per_node", color = "0.27 0.6 0.85", style="rounded"]; - 73[label = "build_industry_sector_ratios_intermediate\nplanning_horizons: 2040", color = "0.26 0.6 0.85", style="rounded"]; - 74[label = "build_industrial_production_per_node", color = "0.30 0.6 0.85", style="rounded"]; - 75[label = "build_industrial_production_per_country_tomorrow\nplanning_horizons: 2040", color = "0.01 0.6 0.85", style="rounded"]; - 76[label = "build_district_heat_share\nplanning_horizons: 2040", color = "0.56 0.6 0.85", style="rounded"]; - 77[label = "solve_sector_network_myopic", color = "0.19 0.6 0.85", style="rounded"]; - 78[label = "add_brownfield", color = "0.27 0.6 0.85", style="rounded"]; - 79[label = "prepare_sector_network\nsector_opts: ", color = "0.36 0.6 0.85", style="rounded"]; - 80[label = "build_biomass_potentials\nplanning_horizons: 2050", color = "0.56 0.6 0.85", style="rounded"]; - 81[label = "retrieve_cost_data\nyear: 2050", color = "0.41 0.6 0.85", style="rounded"]; - 82[label = "build_industrial_energy_demand_per_node", color = "0.27 0.6 0.85", style="rounded"]; - 83[label = "build_industry_sector_ratios_intermediate\nplanning_horizons: 2050", color = "0.26 0.6 0.85", style="rounded"]; - 84[label = "build_industrial_production_per_node", color = "0.30 0.6 0.85", style="rounded"]; - 85[label = "build_industrial_production_per_country_tomorrow\nplanning_horizons: 2050", color = "0.01 0.6 0.85", style="rounded"]; - 86[label = "build_district_heat_share\nplanning_horizons: 2050", color = "0.56 0.6 0.85", style="rounded"]; - 87[label = "plot_power_network_clustered", color = "0.03 0.6 0.85", style="rounded"]; - 88[label = "plot_power_network", color = "0.16 0.6 0.85", style="rounded"]; - 89[label = "plot_power_network", color = "0.16 0.6 0.85", style="rounded"]; - 90[label = "plot_power_network", color = "0.16 0.6 0.85", style="rounded"]; - 91[label = "plot_hydrogen_network", color = "0.54 0.6 0.85", style="rounded"]; - 92[label = "plot_hydrogen_network", color = "0.54 0.6 0.85", style="rounded"]; - 93[label = "plot_hydrogen_network", color = "0.54 0.6 0.85", style="rounded"]; + 2[label = "make_summary", color = "0.16 0.6 0.85", style="rounded"]; + 3[label = "solve_sector_network_myopic", color = "0.04 0.6 0.85", style="rounded"]; + 4[label = "add_existing_baseyear", color = "0.29 0.6 0.85", style="rounded"]; + 5[label = "prepare_sector_network\nsector_opts: ", color = "0.01 0.6 0.85", style="rounded"]; + 6[label = "build_renewable_profiles", color = "0.29 0.6 0.85", style="rounded"]; + 7[label = "determine_availability_matrix\ntechnology: offwind-ac", color = "0.48 0.6 0.85", style="rounded"]; + 8[label = "retrieve_databundle", color = "0.25 0.6 0.85", style="rounded"]; + 9[label = "build_ship_raster", color = "0.35 0.6 0.85", style="rounded"]; + 10[label = "retrieve_ship_raster", color = "0.36 0.6 0.85", style="rounded"]; + 11[label = "retrieve_cutout\ncutout: be-03-2013-era5", color = "0.37 0.6 0.85", style="rounded"]; + 12[label = "build_shapes", color = "0.64 0.6 0.85", style="rounded"]; + 13[label = "retrieve_naturalearth_countries", color = "0.39 0.6 0.85", style="rounded"]; + 14[label = "retrieve_eez", color = "0.43 0.6 0.85", style="rounded"]; + 15[label = "cluster_network\nclusters: 5", color = "0.36 0.6 0.85", style="rounded"]; + 16[label = "simplify_network", color = "0.13 0.6 0.85", style="rounded"]; + 17[label = "add_transmission_projects_and_dlr", color = "0.05 0.6 0.85", style="rounded"]; + 18[label = "base_network", color = "0.34 0.6 0.85", style="rounded"]; + 19[label = "retrieve_osm_prebuilt", color = "0.39 0.6 0.85", style="rounded"]; + 20[label = "build_transmission_projects", color = "0.17 0.6 0.85", style="rounded"]; + 21[label = "build_electricity_demand_base", color = "0.41 0.6 0.85", style="rounded"]; + 22[label = "build_electricity_demand", color = "0.26 0.6 0.85", style="rounded"]; + 23[label = "retrieve_electricity_demand", color = "0.32 0.6 0.85", style="rounded"]; + 24[label = "retrieve_synthetic_electricity_demand", color = "0.60 0.6 0.85", style="rounded"]; + 25[label = "build_renewable_profiles", color = "0.29 0.6 0.85", style="rounded"]; + 26[label = "determine_availability_matrix\ntechnology: offwind-dc", color = "0.48 0.6 0.85", style="rounded"]; + 27[label = "build_renewable_profiles", color = "0.29 0.6 0.85", style="rounded"]; + 28[label = "determine_availability_matrix\ntechnology: offwind-float", color = "0.48 0.6 0.85", style="rounded"]; + 29[label = "cluster_gas_network", color = "0.50 0.6 0.85", style="rounded"]; + 30[label = "build_gas_network", color = "0.12 0.6 0.85", style="rounded"]; + 31[label = "retrieve_gas_infrastructure_data", color = "0.09 0.6 0.85", style="rounded"]; + 32[label = "build_gas_input_locations", color = "0.06 0.6 0.85", style="rounded"]; + 33[label = "retrieve_gem_europe_gas_tracker", color = "0.11 0.6 0.85", style="rounded"]; + 34[label = "time_aggregation", color = "0.64 0.6 0.85", style="rounded"]; + 35[label = "prepare_network\nll: v1.5\nopts: ", color = "0.25 0.6 0.85", style="rounded"]; + 36[label = "add_electricity", color = "0.30 0.6 0.85", style="rounded"]; + 37[label = "build_renewable_profiles", color = "0.29 0.6 0.85", style="rounded"]; + 38[label = "determine_availability_matrix\ntechnology: solar", color = "0.48 0.6 0.85", style="rounded"]; + 39[label = "build_renewable_profiles", color = "0.29 0.6 0.85", style="rounded"]; + 40[label = "determine_availability_matrix\ntechnology: solar-hsat", color = "0.48 0.6 0.85", style="rounded"]; + 41[label = "build_renewable_profiles", color = "0.29 0.6 0.85", style="rounded"]; + 42[label = "determine_availability_matrix\ntechnology: onwind", color = "0.48 0.6 0.85", style="rounded"]; + 43[label = "retrieve_cost_data\nyear: 2030", color = "0.61 0.6 0.85", style="rounded"]; + 44[label = "build_powerplants", color = "0.51 0.6 0.85", style="rounded"]; + 45[label = "build_hourly_heat_demand", color = "0.07 0.6 0.85", style="rounded"]; + 46[label = "build_daily_heat_demand", color = "0.12 0.6 0.85", style="rounded"]; + 47[label = "build_population_layouts", color = "0.40 0.6 0.85", style="rounded"]; + 48[label = "retrieve_worldbank_urban_population", color = "0.65 0.6 0.85", style="rounded"]; + 49[label = "build_solar_thermal_profiles", color = "0.40 0.6 0.85", style="rounded"]; + 50[label = "retrieve_eurostat_data", color = "0.45 0.6 0.85", style="rounded"]; + 51[label = "build_population_weighted_energy_totals\nkind: energy", color = "0.02 0.6 0.85", style="rounded"]; + 52[label = "build_energy_totals", color = "0.23 0.6 0.85", style="rounded"]; + 53[label = "retrieve_jrc_idees", color = "0.35 0.6 0.85", style="rounded"]; + 54[label = "retrieve_eurostat_household_data", color = "0.19 0.6 0.85", style="rounded"]; + 55[label = "build_clustered_population_layouts", color = "0.24 0.6 0.85", style="rounded"]; + 56[label = "build_population_weighted_energy_totals\nkind: heat", color = "0.02 0.6 0.85", style="rounded"]; + 57[label = "build_heat_totals", color = "0.66 0.6 0.85", style="rounded"]; + 58[label = "build_shipping_demand", color = "0.59 0.6 0.85", style="rounded"]; + 59[label = "build_transport_demand", color = "0.19 0.6 0.85", style="rounded"]; + 60[label = "build_temperature_profiles", color = "0.27 0.6 0.85", style="rounded"]; + 61[label = "build_biomass_potentials\nplanning_horizons: 2030", color = "0.08 0.6 0.85", style="rounded"]; + 62[label = "retrieve_jrc_enspreso_biomass", color = "0.18 0.6 0.85", style="rounded"]; + 63[label = "build_salt_cavern_potentials", color = "0.57 0.6 0.85", style="rounded"]; + 64[label = "build_industrial_energy_demand_per_node", color = "0.13 0.6 0.85", style="rounded"]; + 65[label = "build_industry_sector_ratios_intermediate\nplanning_horizons: 2030", color = "0.05 0.6 0.85", style="rounded"]; + 66[label = "build_industry_sector_ratios", color = "0.28 0.6 0.85", style="rounded"]; + 67[label = "build_ammonia_production", color = "0.22 0.6 0.85", style="rounded"]; + 68[label = "retrieve_usgs_ammonia_production", color = "0.49 0.6 0.85", style="rounded"]; + 69[label = "build_industrial_energy_demand_per_country_today", color = "0.20 0.6 0.85", style="rounded"]; + 70[label = "build_industrial_production_per_country", color = "0.18 0.6 0.85", style="rounded"]; + 71[label = "build_industrial_production_per_node", color = "0.32 0.6 0.85", style="rounded"]; + 72[label = "build_industrial_distribution_key", color = "0.55 0.6 0.85", style="rounded"]; + 73[label = "retrieve_hotmaps_industrial_sites", color = "0.16 0.6 0.85", style="rounded"]; + 74[label = "retrieve_gem_steel_plant_tracker", color = "0.47 0.6 0.85", style="rounded"]; + 75[label = "build_industrial_production_per_country_tomorrow\nplanning_horizons: 2030", color = "0.21 0.6 0.85", style="rounded"]; + 76[label = "build_industrial_energy_demand_per_node_today", color = "0.00 0.6 0.85", style="rounded"]; + 77[label = "build_district_heat_share\nplanning_horizons: 2030", color = "0.08 0.6 0.85", style="rounded"]; + 78[label = "build_cop_profiles", color = "0.44 0.6 0.85", style="rounded"]; + 79[label = "build_central_heating_temperature_profiles", color = "0.42 0.6 0.85", style="rounded"]; + 80[label = "build_existing_heating_distribution", color = "0.42 0.6 0.85", style="rounded"]; + 81[label = "solve_sector_network_myopic", color = "0.04 0.6 0.85", style="rounded"]; + 82[label = "add_brownfield", color = "0.37 0.6 0.85", style="rounded"]; + 83[label = "prepare_sector_network\nsector_opts: ", color = "0.01 0.6 0.85", style="rounded"]; + 84[label = "build_biomass_potentials\nplanning_horizons: 2040", color = "0.08 0.6 0.85", style="rounded"]; + 85[label = "retrieve_cost_data\nyear: 2040", color = "0.61 0.6 0.85", style="rounded"]; + 86[label = "build_industrial_energy_demand_per_node", color = "0.13 0.6 0.85", style="rounded"]; + 87[label = "build_industry_sector_ratios_intermediate\nplanning_horizons: 2040", color = "0.05 0.6 0.85", style="rounded"]; + 88[label = "build_industrial_production_per_node", color = "0.32 0.6 0.85", style="rounded"]; + 89[label = "build_industrial_production_per_country_tomorrow\nplanning_horizons: 2040", color = "0.21 0.6 0.85", style="rounded"]; + 90[label = "build_district_heat_share\nplanning_horizons: 2040", color = "0.08 0.6 0.85", style="rounded"]; + 91[label = "solve_sector_network_myopic", color = "0.04 0.6 0.85", style="rounded"]; + 92[label = "add_brownfield", color = "0.37 0.6 0.85", style="rounded"]; + 93[label = "prepare_sector_network\nsector_opts: ", color = "0.01 0.6 0.85", style="rounded"]; + 94[label = "build_biomass_potentials\nplanning_horizons: 2050", color = "0.08 0.6 0.85", style="rounded"]; + 95[label = "retrieve_cost_data\nyear: 2050", color = "0.61 0.6 0.85", style="rounded"]; + 96[label = "build_industrial_energy_demand_per_node", color = "0.13 0.6 0.85", style="rounded"]; + 97[label = "build_industry_sector_ratios_intermediate\nplanning_horizons: 2050", color = "0.05 0.6 0.85", style="rounded"]; + 98[label = "build_industrial_production_per_node", color = "0.32 0.6 0.85", style="rounded"]; + 99[label = "build_industrial_production_per_country_tomorrow\nplanning_horizons: 2050", color = "0.21 0.6 0.85", style="rounded"]; + 100[label = "build_district_heat_share\nplanning_horizons: 2050", color = "0.08 0.6 0.85", style="rounded"]; + 101[label = "plot_power_network_clustered", color = "0.27 0.6 0.85", style="rounded"]; + 102[label = "plot_power_network", color = "0.54 0.6 0.85", style="rounded"]; + 103[label = "plot_power_network", color = "0.54 0.6 0.85", style="rounded"]; + 104[label = "plot_power_network", color = "0.54 0.6 0.85", style="rounded"]; + 105[label = "plot_hydrogen_network", color = "0.02 0.6 0.85", style="rounded"]; + 106[label = "plot_hydrogen_network", color = "0.02 0.6 0.85", style="rounded"]; + 107[label = "plot_hydrogen_network", color = "0.02 0.6 0.85", style="rounded"]; 1 -> 0 2 -> 1 - 37 -> 1 - 9 -> 1 + 50 -> 1 + 8 -> 1 3 -> 2 - 67 -> 2 - 77 -> 2 - 24 -> 2 - 87 -> 2 - 88 -> 2 - 89 -> 2 - 90 -> 2 + 81 -> 2 91 -> 2 - 92 -> 2 - 93 -> 2 + 43 -> 2 + 101 -> 2 + 102 -> 2 + 103 -> 2 + 104 -> 2 + 105 -> 2 + 106 -> 2 + 107 -> 2 4 -> 3 - 24 -> 3 + 43 -> 3 5 -> 4 - 25 -> 4 - 19 -> 4 - 18 -> 4 - 41 -> 4 - 24 -> 4 - 63 -> 4 - 66 -> 4 + 44 -> 4 + 16 -> 4 + 15 -> 4 + 55 -> 4 + 43 -> 4 + 78 -> 4 + 80 -> 4 + 52 -> 4 6 -> 5 - 13 -> 5 - 14 -> 5 - 15 -> 5 + 25 -> 5 + 27 -> 5 29 -> 5 - 30 -> 5 - 31 -> 5 - 37 -> 5 - 38 -> 5 - 42 -> 5 - 44 -> 5 - 45 -> 5 - 39 -> 5 - 9 -> 5 - 47 -> 5 - 24 -> 5 - 48 -> 5 - 19 -> 5 - 18 -> 5 - 41 -> 5 - 49 -> 5 + 32 -> 5 + 34 -> 5 + 35 -> 5 50 -> 5 - 33 -> 5 - 60 -> 5 - 46 -> 5 + 51 -> 5 + 56 -> 5 + 58 -> 5 + 59 -> 5 + 52 -> 5 + 8 -> 5 61 -> 5 - 62 -> 5 + 43 -> 5 63 -> 5 - 36 -> 5 + 16 -> 5 + 15 -> 5 + 55 -> 5 64 -> 5 - 65 -> 5 + 45 -> 5 + 71 -> 5 + 77 -> 5 + 60 -> 5 + 78 -> 5 + 49 -> 5 7 -> 6 - 9 -> 6 - 10 -> 6 - 8 -> 6 12 -> 6 + 15 -> 6 + 11 -> 6 8 -> 7 - 9 -> 8 - 11 -> 10 - 12 -> 10 - 7 -> 13 - 9 -> 13 - 10 -> 13 - 8 -> 13 - 12 -> 13 - 7 -> 14 - 9 -> 14 - 10 -> 14 - 8 -> 14 - 12 -> 14 + 9 -> 7 + 12 -> 7 + 15 -> 7 + 11 -> 7 + 10 -> 9 + 11 -> 9 + 13 -> 12 + 14 -> 12 + 8 -> 12 16 -> 15 - 18 -> 15 + 21 -> 15 17 -> 16 + 18 -> 16 + 18 -> 17 + 20 -> 17 19 -> 18 - 24 -> 18 - 20 -> 19 - 24 -> 19 - 7 -> 19 - 21 -> 20 - 22 -> 20 - 23 -> 20 - 6 -> 20 - 13 -> 20 - 14 -> 20 - 7 -> 20 - 24 -> 20 - 25 -> 20 - 26 -> 20 - 8 -> 20 - 7 -> 21 - 9 -> 21 - 8 -> 21 + 12 -> 18 + 18 -> 20 + 12 -> 20 + 16 -> 21 12 -> 21 - 7 -> 22 - 9 -> 22 - 8 -> 22 - 12 -> 22 - 7 -> 23 - 9 -> 23 - 8 -> 23 - 12 -> 23 - 7 -> 25 - 27 -> 26 - 28 -> 26 - 17 -> 29 - 18 -> 29 + 22 -> 21 + 23 -> 22 + 24 -> 22 + 26 -> 25 + 12 -> 25 + 15 -> 25 + 11 -> 25 + 8 -> 26 + 9 -> 26 + 12 -> 26 + 15 -> 26 + 11 -> 26 + 28 -> 27 + 12 -> 27 + 15 -> 27 + 11 -> 27 + 8 -> 28 + 9 -> 28 + 12 -> 28 + 15 -> 28 + 11 -> 28 + 30 -> 29 + 15 -> 29 31 -> 30 - 33 -> 30 - 36 -> 30 - 32 -> 31 - 24 -> 31 - 18 -> 32 - 24 -> 32 - 34 -> 33 + 33 -> 32 + 31 -> 32 + 15 -> 32 35 -> 34 - 18 -> 34 - 12 -> 34 - 8 -> 35 - 12 -> 35 - 35 -> 36 - 18 -> 36 - 12 -> 36 - 39 -> 38 - 41 -> 38 - 8 -> 39 - 9 -> 39 - 37 -> 39 + 45 -> 34 + 49 -> 34 + 36 -> 35 + 43 -> 35 + 37 -> 36 + 39 -> 36 + 41 -> 36 + 6 -> 36 + 25 -> 36 + 27 -> 36 + 15 -> 36 + 43 -> 36 + 44 -> 36 + 21 -> 36 + 38 -> 37 + 12 -> 37 + 15 -> 37 + 11 -> 37 + 8 -> 38 + 12 -> 38 + 15 -> 38 + 11 -> 38 40 -> 39 - 35 -> 41 - 18 -> 41 + 12 -> 39 + 15 -> 39 + 11 -> 39 + 8 -> 40 + 12 -> 40 + 15 -> 40 + 11 -> 40 + 42 -> 41 12 -> 41 - 43 -> 42 - 41 -> 42 - 39 -> 43 - 8 -> 44 - 18 -> 44 - 39 -> 44 - 41 -> 45 - 38 -> 45 - 39 -> 45 - 9 -> 45 + 15 -> 41 + 11 -> 41 + 8 -> 42 + 12 -> 42 + 15 -> 42 + 11 -> 42 + 15 -> 44 46 -> 45 - 35 -> 46 - 18 -> 46 - 12 -> 46 - 9 -> 47 - 18 -> 47 - 8 -> 47 - 9 -> 48 - 18 -> 48 - 35 -> 49 - 19 -> 49 - 12 -> 49 - 51 -> 50 - 56 -> 50 - 59 -> 50 + 47 -> 46 + 15 -> 46 + 11 -> 46 + 12 -> 47 + 48 -> 47 + 11 -> 47 + 47 -> 49 + 15 -> 49 + 11 -> 49 52 -> 51 - 54 -> 51 55 -> 51 + 12 -> 52 + 8 -> 52 53 -> 52 - 9 -> 52 - 9 -> 53 - 9 -> 54 - 55 -> 54 - 53 -> 55 - 9 -> 55 - 37 -> 55 + 50 -> 52 + 54 -> 52 + 47 -> 55 + 15 -> 55 + 11 -> 55 57 -> 56 - 58 -> 56 - 18 -> 57 - 41 -> 57 - 55 -> 58 - 57 -> 59 - 54 -> 59 - 39 -> 60 - 41 -> 60 - 35 -> 61 - 18 -> 61 + 55 -> 56 + 52 -> 57 + 12 -> 58 + 15 -> 58 + 52 -> 58 + 55 -> 59 + 51 -> 59 + 52 -> 59 + 8 -> 59 + 60 -> 59 + 47 -> 60 + 15 -> 60 + 11 -> 60 + 62 -> 61 + 50 -> 61 + 8 -> 61 + 15 -> 61 12 -> 61 - 35 -> 62 - 18 -> 62 - 12 -> 62 - 46 -> 63 - 61 -> 63 - 62 -> 63 - 35 -> 64 - 18 -> 64 - 12 -> 64 - 35 -> 65 - 18 -> 65 - 12 -> 65 - 41 -> 66 - 38 -> 66 - 60 -> 66 + 8 -> 63 + 15 -> 63 + 65 -> 64 + 71 -> 64 + 76 -> 64 + 66 -> 65 + 69 -> 65 + 70 -> 65 + 67 -> 66 + 53 -> 66 68 -> 67 - 71 -> 67 - 21 -> 68 - 22 -> 68 - 23 -> 68 - 6 -> 68 - 13 -> 68 - 14 -> 68 - 19 -> 68 - 18 -> 68 - 69 -> 68 - 3 -> 68 - 71 -> 68 - 63 -> 68 - 6 -> 69 - 13 -> 69 - 14 -> 69 - 15 -> 69 - 29 -> 69 - 30 -> 69 - 31 -> 69 - 37 -> 69 - 38 -> 69 - 42 -> 69 - 44 -> 69 - 45 -> 69 - 39 -> 69 - 9 -> 69 + 52 -> 69 + 53 -> 69 70 -> 69 - 71 -> 69 - 48 -> 69 - 19 -> 69 - 18 -> 69 - 41 -> 69 - 49 -> 69 - 72 -> 69 - 33 -> 69 - 76 -> 69 - 46 -> 69 - 61 -> 69 - 62 -> 69 - 63 -> 69 - 36 -> 69 - 64 -> 69 - 65 -> 69 - 9 -> 70 - 18 -> 70 - 8 -> 70 + 67 -> 70 + 53 -> 70 + 50 -> 70 + 72 -> 71 + 75 -> 71 + 15 -> 72 + 55 -> 72 73 -> 72 74 -> 72 - 59 -> 72 - 52 -> 73 - 54 -> 73 - 55 -> 73 - 57 -> 74 - 75 -> 74 - 55 -> 75 - 39 -> 76 - 41 -> 76 - 78 -> 77 - 81 -> 77 - 21 -> 78 - 22 -> 78 - 23 -> 78 - 6 -> 78 - 13 -> 78 - 14 -> 78 - 19 -> 78 - 18 -> 78 + 70 -> 75 + 72 -> 76 + 69 -> 76 + 52 -> 77 + 55 -> 77 79 -> 78 - 67 -> 78 - 81 -> 78 - 63 -> 78 - 6 -> 79 - 13 -> 79 - 14 -> 79 + 60 -> 78 + 15 -> 78 + 60 -> 79 15 -> 79 - 29 -> 79 - 30 -> 79 - 31 -> 79 - 37 -> 79 - 38 -> 79 - 42 -> 79 - 44 -> 79 - 45 -> 79 - 39 -> 79 - 9 -> 79 - 80 -> 79 - 81 -> 79 - 48 -> 79 - 19 -> 79 - 18 -> 79 - 41 -> 79 - 49 -> 79 - 82 -> 79 - 33 -> 79 - 86 -> 79 - 46 -> 79 - 61 -> 79 - 62 -> 79 - 63 -> 79 - 36 -> 79 - 64 -> 79 - 65 -> 79 - 9 -> 80 - 18 -> 80 - 8 -> 80 + 55 -> 80 + 51 -> 80 + 77 -> 80 + 82 -> 81 + 85 -> 81 + 37 -> 82 + 39 -> 82 + 41 -> 82 + 6 -> 82 + 25 -> 82 + 27 -> 82 + 16 -> 82 + 15 -> 82 83 -> 82 - 84 -> 82 - 59 -> 82 + 3 -> 82 + 85 -> 82 + 78 -> 82 + 6 -> 83 + 25 -> 83 + 27 -> 83 + 29 -> 83 + 32 -> 83 + 34 -> 83 + 35 -> 83 + 50 -> 83 + 51 -> 83 + 56 -> 83 + 58 -> 83 + 59 -> 83 52 -> 83 - 54 -> 83 + 8 -> 83 + 84 -> 83 + 85 -> 83 + 63 -> 83 + 16 -> 83 + 15 -> 83 55 -> 83 - 57 -> 84 - 85 -> 84 - 55 -> 85 - 39 -> 86 - 41 -> 86 - 18 -> 87 - 3 -> 88 - 18 -> 88 - 67 -> 89 - 18 -> 89 - 77 -> 90 - 18 -> 90 - 3 -> 91 - 18 -> 91 - 67 -> 92 - 18 -> 92 - 77 -> 93 - 18 -> 93 + 86 -> 83 + 45 -> 83 + 88 -> 83 + 90 -> 83 + 60 -> 83 + 78 -> 83 + 49 -> 83 + 62 -> 84 + 50 -> 84 + 8 -> 84 + 15 -> 84 + 12 -> 84 + 87 -> 86 + 88 -> 86 + 76 -> 86 + 66 -> 87 + 69 -> 87 + 70 -> 87 + 72 -> 88 + 89 -> 88 + 70 -> 89 + 52 -> 90 + 55 -> 90 + 92 -> 91 + 95 -> 91 + 37 -> 92 + 39 -> 92 + 41 -> 92 + 6 -> 92 + 25 -> 92 + 27 -> 92 + 16 -> 92 + 15 -> 92 + 93 -> 92 + 81 -> 92 + 95 -> 92 + 78 -> 92 + 6 -> 93 + 25 -> 93 + 27 -> 93 + 29 -> 93 + 32 -> 93 + 34 -> 93 + 35 -> 93 + 50 -> 93 + 51 -> 93 + 56 -> 93 + 58 -> 93 + 59 -> 93 + 52 -> 93 + 8 -> 93 + 94 -> 93 + 95 -> 93 + 63 -> 93 + 16 -> 93 + 15 -> 93 + 55 -> 93 + 96 -> 93 + 45 -> 93 + 98 -> 93 + 100 -> 93 + 60 -> 93 + 78 -> 93 + 49 -> 93 + 62 -> 94 + 50 -> 94 + 8 -> 94 + 15 -> 94 + 12 -> 94 + 97 -> 96 + 98 -> 96 + 76 -> 96 + 66 -> 97 + 69 -> 97 + 70 -> 97 + 72 -> 98 + 99 -> 98 + 70 -> 99 + 52 -> 100 + 55 -> 100 + 15 -> 101 + 3 -> 102 + 15 -> 102 + 81 -> 103 + 15 -> 103 + 91 -> 104 + 15 -> 104 + 3 -> 105 + 15 -> 105 + 81 -> 106 + 15 -> 106 + 91 -> 107 + 15 -> 107 } | diff --git a/doc/wildcards.rst b/doc/wildcards.rst index 9ddb7b0a8..389b35d70 100644 --- a/doc/wildcards.rst +++ b/doc/wildcards.rst @@ -38,15 +38,6 @@ series and potentials using the rule :mod:`build_renewable_profiles`. It can take the values ``onwind``, ``offwind-ac``, ``offwind-dc``, ``offwind-float``, and ``solar`` but **not** ``hydro`` (since hydroelectric plant profiles are created by a different rule)`` -.. _simpl: - -The ``{simpl}`` wildcard -======================== - -The ``{simpl}`` wildcard specifies number of buses a detailed -network model should be pre-clustered to in the rule -:mod:`simplify_network` (before :mod:`cluster_network`). - .. _clusters: The ``{clusters}`` wildcard @@ -57,11 +48,6 @@ network model should be reduced to in the rule :mod:`cluster_network`. The number of clusters must be lower than the total number of nodes and higher than the number of countries. However, a country counts twice if it has two asynchronous subnetworks (e.g. Denmark or Italy). - -If an `m` is placed behind the number of clusters (e.g. ``100m``), -generators are only moved to the clustered buses but not aggregated -by carrier; i.e. the clustered bus may have more than one e.g. wind generator. - .. _ll: The ``{ll}`` wildcard diff --git a/rules/build_electricity.smk b/rules/build_electricity.smk index cbe8035aa..be97270aa 100755 --- a/rules/build_electricity.smk +++ b/rules/build_electricity.smk @@ -35,12 +35,14 @@ rule build_powerplants: everywhere_powerplants=config_provider("electricity", "everywhere_powerplants"), countries=config_provider("countries"), input: - base_network=resources("networks/base.nc"), + network=resources("networks/base_s_{clusters}.nc"), custom_powerplants="data/custom_powerplants.csv", output: - resources("powerplants.csv"), + resources("powerplants_s_{clusters}.csv"), log: - logs("build_powerplants.log"), + logs("build_powerplants_s_{clusters}.log"), + benchmark: + benchmarks("build_powerplants_s_{clusters}") threads: 1 resources: mem_mb=7000, @@ -169,6 +171,8 @@ rule build_ship_raster: rule determine_availability_matrix_MD_UA: + params: + renewable=config_provider("renewable"), input: copernicus="data/Copernicus_LC100_global_v3.0.1_2019-nrt_Discrete-Classification-map_EPSG-4326.tif", wdpa="data/WDPA.gpkg", @@ -186,18 +190,20 @@ rule determine_availability_matrix_MD_UA: country_shapes=resources("country_shapes.geojson"), offshore_shapes=resources("offshore_shapes.geojson"), regions=lambda w: ( - resources("regions_onshore.geojson") + resources("regions_onshore_base_s_{clusters}.geojson") if w.technology in ("onwind", "solar", "solar-hsat") - else resources("regions_offshore.geojson") + else resources("regions_offshore_base_s_{clusters}.geojson") ), cutout=lambda w: "cutouts/" + CDIR + config_provider("renewable", w.technology, "cutout")(w) + ".nc", output: - availability_matrix=resources("availability_matrix_MD-UA_{technology}.nc"), + availability_matrix=resources( + "availability_matrix_MD-UA_{clusters}_{technology}.nc" + ), log: - logs("determine_availability_matrix_MD_UA_{technology}.log"), + logs("determine_availability_matrix_MD_UA_{clusters}_{technology}.log"), threads: config["atlite"].get("nprocesses", 4) resources: mem_mb=config["atlite"].get("nprocesses", 4) * 5000, @@ -213,20 +219,17 @@ def input_ua_md_availability_matrix(w): if {"UA", "MD"}.intersection(countries): return { "availability_matrix_MD_UA": resources( - "availability_matrix_MD-UA_{technology}.nc" + "availability_matrix_MD-UA_{clusters}_{technology}.nc" ) } return {} -rule build_renewable_profiles: +rule determine_availability_matrix: params: - snapshots=config_provider("snapshots"), - drop_leap_day=config_provider("enable", "drop_leap_day"), renewable=config_provider("renewable"), input: unpack(input_ua_md_availability_matrix), - base_network=resources("networks/base.nc"), corine=ancient("data/bundle/corine/g250_clc06_V18_5.tif"), natura=lambda w: ( "data/bundle/natura/natura.tiff" @@ -256,20 +259,48 @@ rule build_renewable_profiles: country_shapes=resources("country_shapes.geojson"), offshore_shapes=resources("offshore_shapes.geojson"), regions=lambda w: ( - resources("regions_onshore.geojson") + resources("regions_onshore_base_s_{clusters}.geojson") if w.technology in ("onwind", "solar", "solar-hsat") - else resources("regions_offshore.geojson") + else resources("regions_offshore_base_s_{clusters}.geojson") ), cutout=lambda w: "cutouts/" + CDIR + config_provider("renewable", w.technology, "cutout")(w) + ".nc", output: - profile=resources("profile_{technology}.nc"), + resources("availability_matrix_{clusters}_{technology}.nc"), log: - logs("build_renewable_profile_{technology}.log"), + logs("determine_availability_matrix_{clusters}_{technology}.log"), benchmark: - benchmarks("build_renewable_profiles_{technology}") + benchmarks("determine_availability_matrix_{clusters}_{technology}") + threads: config["atlite"].get("nprocesses", 4) + resources: + mem_mb=config["atlite"].get("nprocesses", 4) * 5000, + conda: + "../envs/environment.yaml" + script: + "../scripts/determine_availability_matrix.py" + + +rule build_renewable_profiles: + params: + snapshots=config_provider("snapshots"), + drop_leap_day=config_provider("enable", "drop_leap_day"), + renewable=config_provider("renewable"), + input: + availability_matrix=resources("availability_matrix_{clusters}_{technology}.nc"), + offshore_shapes=resources("offshore_shapes.geojson"), + regions=resources("regions_onshore_base_s_{clusters}.geojson"), + cutout=lambda w: "cutouts/" + + CDIR + + config_provider("renewable", w.technology, "cutout")(w) + + ".nc", + output: + profile=resources("profile_{clusters}_{technology}.nc"), + log: + logs("build_renewable_profile_{clusters}_{technology}.log"), + benchmark: + benchmarks("build_renewable_profiles_{clusters}_{technology}") threads: config["atlite"].get("nprocesses", 4) resources: mem_mb=config["atlite"].get("nprocesses", 4) * 5000, @@ -337,7 +368,7 @@ rule build_line_rating: + config_provider("lines", "dynamic_line_rating", "cutout")(w) + ".nc", output: - output=resources("networks/line_rating.nc"), + output=resources("dlr.nc"), log: logs("build_line_rating.log"), benchmark: @@ -385,6 +416,44 @@ rule build_transmission_projects: "../scripts/build_transmission_projects.py" +rule add_transmission_projects_and_dlr: + params: + transmission_projects=config_provider("transmission_projects"), + dlr=config_provider("lines", "dynamic_line_rating"), + s_max_pu=config_provider("lines", "s_max_pu"), + input: + network=resources("networks/base.nc"), + dlr=lambda w: ( + resources("dlr.nc") + if config_provider("lines", "dynamic_line_rating", "activate")(w) + else [] + ), + transmission_projects=lambda w: ( + [ + resources("transmission_projects/new_buses.csv"), + resources("transmission_projects/new_lines.csv"), + resources("transmission_projects/new_links.csv"), + resources("transmission_projects/adjust_lines.csv"), + resources("transmission_projects/adjust_links.csv"), + ] + if config_provider("transmission_projects", "enable")(w) + else [] + ), + output: + network=resources("networks/base_extended.nc"), + log: + logs("add_transmission_projects_and_dlr.log"), + benchmark: + benchmarks("add_transmission_projects_and_dlr") + threads: 1 + resources: + mem_mb=4000, + conda: + "../envs/environment.yaml" + script: + "../scripts/add_transmission_projects_and_dlr.py" + + def input_profile_tech(w): return { f"profile_{tech}": resources(f"profile_{tech}.nc") @@ -414,8 +483,8 @@ rule build_gdp_pop_non_nuts3: params: countries=config_provider("countries"), input: - base_network=resources("networks/base.nc"), - regions=resources("regions_onshore.geojson"), + base_network=resources("networks/base_s.nc"), + regions=resources("regions_onshore_base_s.geojson"), gdp_non_nuts3="data/bundle/GDP_per_capita_PPP_1990_2015_v2.nc", pop_non_nuts3="data/bundle/ppp_2013_1km_Aggregated.tif", output: @@ -433,97 +502,76 @@ rule build_gdp_pop_non_nuts3: "../scripts/build_gdp_pop_non_nuts3.py" -rule add_electricity: +rule build_electricity_demand_base: params: - length_factor=config_provider("lines", "length_factor"), - scaling_factor=config_provider("load", "scaling_factor"), - countries=config_provider("countries"), - snapshots=config_provider("snapshots"), - renewable=config_provider("renewable"), - electricity=config_provider("electricity"), - conventional=config_provider("conventional"), - costs=config_provider("costs"), - foresight=config_provider("foresight"), - drop_leap_day=config_provider("enable", "drop_leap_day"), - transmission_projects=config_provider("transmission_projects"), + distribution_key=config_provider("load", "distribution_key"), input: - unpack(input_profile_tech), - unpack(input_conventional), unpack(input_gdp_pop_non_nuts3), - base_network=resources("networks/base.nc"), - line_rating=lambda w: ( - resources("networks/line_rating.nc") - if config_provider("lines", "dynamic_line_rating", "activate")(w) - else resources("networks/base.nc") - ), - transmission_projects=lambda w: ( - [ - resources("transmission_projects/new_buses.csv"), - resources("transmission_projects/new_lines.csv"), - resources("transmission_projects/new_links.csv"), - resources("transmission_projects/adjust_lines.csv"), - resources("transmission_projects/adjust_links.csv"), - ] - if config_provider("transmission_projects", "enable")(w) - else [] - ), - tech_costs=lambda w: resources( - f"costs_{config_provider('costs', 'year')(w)}.csv" - ), - regions=resources("regions_onshore.geojson"), - powerplants=resources("powerplants.csv"), - hydro_capacities=ancient("data/hydro_capacities.csv"), - unit_commitment="data/unit_commitment.csv", - fuel_price=lambda w: ( - resources("monthly_fuel_price.csv") - if config_provider("conventional", "dynamic_fuel_price")(w) - else [] - ), + base_network=resources("networks/base_s.nc"), + regions=resources("regions_onshore_base_s.geojson"), + nuts3=resources("nuts3_shapes.geojson"), load=resources("electricity_demand.csv"), - nuts3_shapes=resources("nuts3_shapes.geojson"), output: - resources("networks/elec.nc"), + resources("electricity_demand_base_s.nc"), log: - logs("add_electricity.log"), + logs("build_electricity_demand_base_s.log"), benchmark: - benchmarks("add_electricity") - threads: 1 + benchmarks("build_electricity_demand_base_s") + resources: + mem_mb=5000, + conda: + "../envs/environment.yaml" + script: + "../scripts/build_electricity_demand_base.py" + + +rule build_hac_features: + params: + snapshots=config_provider("snapshots"), + drop_leap_day=config_provider("enable", "drop_leap_day"), + features=config_provider("clustering", "cluster_network", "hac_features"), + input: + cutout=lambda w: "cutouts/" + + CDIR + + config_provider("atlite", "default_cutout")(w) + + ".nc", + regions=resources("regions_onshore_base_s.geojson"), + output: + resources("hac_features.nc"), + log: + logs("build_hac_features.log"), + benchmark: + benchmarks("build_hac_features") + threads: config["atlite"].get("nprocesses", 4) resources: mem_mb=10000, conda: "../envs/environment.yaml" script: - "../scripts/add_electricity.py" + "../scripts/build_hac_features.py" rule simplify_network: params: simplify_network=config_provider("clustering", "simplify_network"), + cluster_network=config_provider("clustering", "cluster_network"), aggregation_strategies=config_provider( "clustering", "aggregation_strategies", default={} ), - focus_weights=config_provider("clustering", "focus_weights", default=None), - renewable_carriers=config_provider("electricity", "renewable_carriers"), - max_hours=config_provider("electricity", "max_hours"), - length_factor=config_provider("lines", "length_factor"), p_max_pu=config_provider("links", "p_max_pu", default=1.0), - costs=config_provider("costs"), input: - network=resources("networks/elec.nc"), - tech_costs=lambda w: resources( - f"costs_{config_provider('costs', 'year')(w)}.csv" - ), + network=resources("networks/base_extended.nc"), regions_onshore=resources("regions_onshore.geojson"), regions_offshore=resources("regions_offshore.geojson"), output: - network=resources("networks/elec_s{simpl}.nc"), - regions_onshore=resources("regions_onshore_elec_s{simpl}.geojson"), - regions_offshore=resources("regions_offshore_elec_s{simpl}.geojson"), - busmap=resources("busmap_elec_s{simpl}.csv"), + network=resources("networks/base_s.nc"), + regions_onshore=resources("regions_onshore_base_s.geojson"), + regions_offshore=resources("regions_offshore_base_s.geojson"), + busmap=resources("busmap_base_s.csv"), log: - logs("simplify_network/elec_s{simpl}.log"), + logs("simplify_network.log"), benchmark: - benchmarks("simplify_network/elec_s{simpl}") + benchmarks("simplify_network_b") threads: 1 resources: mem_mb=12000, @@ -537,7 +585,7 @@ rule simplify_network: def input_cluster_network(w): if config_provider("enable", "custom_busmap", default=False)(w): base_network = config_provider("electricity", "base_network")(w) - custom_busmap = f"data/busmaps/elec_s{w.simpl}_{w.clusters}_{base_network}.csv" + custom_busmap = f"data/busmaps/base_s_{w.clusters}_{base_network}.csv" return {"custom_busmap": custom_busmap} return {"custom_busmap": []} @@ -556,26 +604,29 @@ rule cluster_network: ), max_hours=config_provider("electricity", "max_hours"), length_factor=config_provider("lines", "length_factor"), - costs=config_provider("costs"), input: unpack(input_cluster_network), - network=resources("networks/elec_s{simpl}.nc"), - regions_onshore=resources("regions_onshore_elec_s{simpl}.geojson"), - regions_offshore=resources("regions_offshore_elec_s{simpl}.geojson"), - busmap=ancient(resources("busmap_elec_s{simpl}.csv")), - tech_costs=lambda w: resources( - f"costs_{config_provider('costs', 'year')(w)}.csv" + network=resources("networks/base_s.nc"), + regions_onshore=resources("regions_onshore_base_s.geojson"), + regions_offshore=resources("regions_offshore_base_s.geojson"), + busmap=ancient(resources("busmap_base_s.csv")), + hac_features=lambda w: ( + resources("hac_features.nc") + if config_provider("clustering", "cluster_network", "algorithm")(w) + == "hac" + else [] ), + load=resources("electricity_demand_base_s.nc"), output: - network=resources("networks/elec_s{simpl}_{clusters}.nc"), - regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), - regions_offshore=resources("regions_offshore_elec_s{simpl}_{clusters}.geojson"), - busmap=resources("busmap_elec_s{simpl}_{clusters}.csv"), - linemap=resources("linemap_elec_s{simpl}_{clusters}.csv"), + network=resources("networks/base_s_{clusters}.nc"), + regions_onshore=resources("regions_onshore_base_s_{clusters}.geojson"), + regions_offshore=resources("regions_offshore_base_s_{clusters}.geojson"), + busmap=resources("busmap_base_s_{clusters}.csv"), + linemap=resources("linemap_base_s_{clusters}.csv"), log: - logs("cluster_network/elec_s{simpl}_{clusters}.log"), + logs("cluster_network_base_s_{clusters}.log"), benchmark: - benchmarks("cluster_network/elec_s{simpl}_{clusters}") + benchmarks("cluster_network_base_s_{clusters}") threads: 1 resources: mem_mb=10000, @@ -585,29 +636,76 @@ rule cluster_network: "../scripts/cluster_network.py" -rule add_extra_components: +def input_profile_tech(w): + return { + f"profile_{tech}": resources( + "profile_{clusters}_" + tech + ".nc" + if tech != "hydro" + else f"profile_{tech}.nc" + ) + for tech in config_provider("electricity", "renewable_carriers")(w) + } + + +def input_conventional(w): + return { + f"conventional_{carrier}_{attr}": fn + for carrier, d in config_provider("conventional", default={None: {}})(w).items() + if carrier in config_provider("electricity", "conventional_carriers")(w) + for attr, fn in d.items() + if str(fn).startswith("data/") + } + + +rule add_electricity: params: - extendable_carriers=config_provider("electricity", "extendable_carriers"), - max_hours=config_provider("electricity", "max_hours"), + line_length_factor=config_provider("lines", "length_factor"), + link_length_factor=config_provider("links", "length_factor"), + scaling_factor=config_provider("load", "scaling_factor"), + countries=config_provider("countries"), + snapshots=config_provider("snapshots"), + renewable=config_provider("renewable"), + electricity=config_provider("electricity"), + conventional=config_provider("conventional"), costs=config_provider("costs"), + foresight=config_provider("foresight"), + drop_leap_day=config_provider("enable", "drop_leap_day"), + consider_efficiency_classes=config_provider( + "clustering", "consider_efficiency_classes" + ), + aggregation_strategies=config_provider("clustering", "aggregation_strategies"), + exclude_carriers=config_provider("clustering", "exclude_carriers"), input: - network=resources("networks/elec_s{simpl}_{clusters}.nc"), + unpack(input_profile_tech), + unpack(input_conventional), + base_network=resources("networks/base_s_{clusters}.nc"), tech_costs=lambda w: resources( f"costs_{config_provider('costs', 'year')(w)}.csv" ), + regions=resources("regions_onshore_base_s_{clusters}.geojson"), + powerplants=resources("powerplants_s_{clusters}.csv"), + hydro_capacities=ancient("data/hydro_capacities.csv"), + unit_commitment="data/unit_commitment.csv", + fuel_price=lambda w: ( + resources("monthly_fuel_price.csv") + if config_provider("conventional", "dynamic_fuel_price")(w) + else [] + ), + load=resources("electricity_demand_base_s.nc"), + busmap=resources("busmap_base_s_{clusters}.csv"), output: - resources("networks/elec_s{simpl}_{clusters}_ec.nc"), + resources("networks/base_s_{clusters}_elec.nc"), log: - logs("add_extra_components/elec_s{simpl}_{clusters}.log"), + logs("add_electricity_{clusters}.log"), benchmark: - benchmarks("add_extra_components/elec_s{simpl}_{clusters}_ec") + benchmarks("add_electricity_{clusters}") threads: 1 resources: - mem_mb=4000, + mem_mb=10000, conda: "../envs/environment.yaml" script: - "../scripts/add_extra_components.py" + "../scripts/add_electricity.py" rule prepare_network: @@ -626,17 +724,17 @@ rule prepare_network: autarky=config_provider("electricity", "autarky", default={}), drop_leap_day=config_provider("enable", "drop_leap_day"), input: - resources("networks/elec_s{simpl}_{clusters}_ec.nc"), + resources("networks/base_s_{clusters}_elec.nc"), tech_costs=lambda w: resources( f"costs_{config_provider('costs', 'year')(w)}.csv" ), co2_price=lambda w: resources("co2_price.csv") if "Ept" in w.opts else [], output: - resources("networks/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc"), + resources("networks/base_s_{clusters}_elec_l{ll}_{opts}.nc"), log: - logs("prepare_network/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.log"), + logs("prepare_network_base_s_{clusters}_elec_l{ll}_{opts}.log"), benchmark: - (benchmarks("prepare_network/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}")) + benchmarks("prepare_network_base_s_{clusters}_elec_l{ll}_{opts}") threads: 1 resources: mem_mb=4000, diff --git a/rules/build_sector.smk b/rules/build_sector.smk index ccd4243e7..bd19fef34 100755 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -33,19 +33,19 @@ rule build_clustered_population_layouts: pop_layout_total=resources("pop_layout_total.nc"), pop_layout_urban=resources("pop_layout_urban.nc"), pop_layout_rural=resources("pop_layout_rural.nc"), - regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), + regions_onshore=resources("regions_onshore_base_s_{clusters}.geojson"), cutout=lambda w: "cutouts/" + CDIR + config_provider("atlite", "default_cutout")(w) + ".nc", output: - clustered_pop_layout=resources("pop_layout_elec_s{simpl}_{clusters}.csv"), + clustered_pop_layout=resources("pop_layout_base_s_{clusters}.csv"), log: - logs("build_clustered_population_layouts_{simpl}_{clusters}.log"), + logs("build_clustered_population_layouts_s_{clusters}.log"), resources: mem_mb=10000, benchmark: - benchmarks("build_clustered_population_layouts/s{simpl}_{clusters}") + benchmarks("build_clustered_population_layouts/s_{clusters}") conda: "../envs/environment.yaml" script: @@ -57,19 +57,19 @@ rule build_simplified_population_layouts: pop_layout_total=resources("pop_layout_total.nc"), pop_layout_urban=resources("pop_layout_urban.nc"), pop_layout_rural=resources("pop_layout_rural.nc"), - regions_onshore=resources("regions_onshore_elec_s{simpl}.geojson"), + regions_onshore=resources("regions_onshore_base_s.geojson"), cutout=lambda w: "cutouts/" + CDIR + config_provider("atlite", "default_cutout")(w) + ".nc", output: - clustered_pop_layout=resources("pop_layout_elec_s{simpl}.csv"), + clustered_pop_layout=resources("pop_layout_base_s.csv"), resources: mem_mb=10000, log: - logs("build_simplified_population_layouts_{simpl}"), + logs("build_simplified_population_layouts_s"), benchmark: - benchmarks("build_simplified_population_layouts/s{simpl}") + benchmarks("build_simplified_population_layouts/s") conda: "../envs/environment.yaml" script: @@ -96,17 +96,17 @@ rule build_gas_input_locations: gem="data/gem/Europe-Gas-Tracker-2024-05.xlsx", entry="data/gas_network/scigrid-gas/data/IGGIELGN_BorderPoints.geojson", storage="data/gas_network/scigrid-gas/data/IGGIELGN_Storages.geojson", - regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), - regions_offshore=resources("regions_offshore_elec_s{simpl}_{clusters}.geojson"), + regions_onshore=resources("regions_onshore_base_s_{clusters}.geojson"), + regions_offshore=resources("regions_offshore_base_s_{clusters}.geojson"), output: - gas_input_nodes=resources("gas_input_locations_s{simpl}_{clusters}.geojson"), + gas_input_nodes=resources("gas_input_locations_s_{clusters}.geojson"), gas_input_nodes_simplified=resources( - "gas_input_locations_s{simpl}_{clusters}_simplified.csv" + "gas_input_locations_s_{clusters}_simplified.csv" ), resources: mem_mb=2000, log: - logs("build_gas_input_locations_s{simpl}_{clusters}.log"), + logs("build_gas_input_locations_s_{clusters}.log"), conda: "../envs/environment.yaml" script: @@ -116,14 +116,14 @@ rule build_gas_input_locations: rule cluster_gas_network: input: cleaned_gas_network=resources("gas_network.csv"), - regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), - regions_offshore=resources("regions_offshore_elec_s{simpl}_{clusters}.geojson"), + regions_onshore=resources("regions_onshore_base_s_{clusters}.geojson"), + regions_offshore=resources("regions_offshore_base_s_{clusters}.geojson"), output: - clustered_gas_network=resources("gas_network_elec_s{simpl}_{clusters}.csv"), + clustered_gas_network=resources("gas_network_base_s_{clusters}.csv"), resources: mem_mb=4000, log: - logs("cluster_gas_network_s{simpl}_{clusters}.log"), + logs("cluster_gas_network_{clusters}.log"), conda: "../envs/environment.yaml" script: @@ -149,17 +149,17 @@ rule build_daily_heat_demand: drop_leap_day=config_provider("enable", "drop_leap_day"), input: pop_layout=resources("pop_layout_total.nc"), - regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), + regions_onshore=resources("regions_onshore_base_s_{clusters}.geojson"), cutout=heat_demand_cutout, output: - heat_demand=resources("daily_heat_demand_total_elec_s{simpl}_{clusters}.nc"), + heat_demand=resources("daily_heat_demand_total_base_s_{clusters}.nc"), resources: mem_mb=20000, threads: 8 log: - logs("build_daily_heat_demand_total_{simpl}_{clusters}.loc"), + logs("build_daily_heat_demand_total_s_{clusters}.loc"), benchmark: - benchmarks("build_daily_heat_demand/total_s{simpl}_{clusters}") + benchmarks("build_daily_heat_demand/total_s_{clusters}") conda: "../envs/environment.yaml" script: @@ -172,16 +172,16 @@ rule build_hourly_heat_demand: drop_leap_day=config_provider("enable", "drop_leap_day"), input: heat_profile="data/heat_load_profile_BDEW.csv", - heat_demand=resources("daily_heat_demand_total_elec_s{simpl}_{clusters}.nc"), + heat_demand=resources("daily_heat_demand_total_base_s_{clusters}.nc"), output: - heat_demand=resources("hourly_heat_demand_total_elec_s{simpl}_{clusters}.nc"), + heat_demand=resources("hourly_heat_demand_total_base_s_{clusters}.nc"), resources: mem_mb=2000, threads: 8 log: - logs("build_hourly_heat_demand_total_{simpl}_{clusters}.loc"), + logs("build_hourly_heat_demand_total_s_{clusters}.loc"), benchmark: - benchmarks("build_hourly_heat_demand/total_s{simpl}_{clusters}") + benchmarks("build_hourly_heat_demand/total_s_{clusters}") conda: "../envs/environment.yaml" script: @@ -194,18 +194,18 @@ rule build_temperature_profiles: drop_leap_day=config_provider("enable", "drop_leap_day"), input: pop_layout=resources("pop_layout_total.nc"), - regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), + regions_onshore=resources("regions_onshore_base_s_{clusters}.geojson"), cutout=heat_demand_cutout, output: - temp_soil=resources("temp_soil_total_elec_s{simpl}_{clusters}.nc"), - temp_air=resources("temp_air_total_elec_s{simpl}_{clusters}.nc"), + temp_soil=resources("temp_soil_total_base_s_{clusters}.nc"), + temp_air=resources("temp_air_total_base_s_{clusters}.nc"), resources: mem_mb=20000, threads: 8 log: - logs("build_temperature_profiles_total_{simpl}_{clusters}.log"), + logs("build_temperature_profiles_total_s_{clusters}.log"), benchmark: - benchmarks("build_temperature_profiles/total_s{simpl}_{clusters}") + benchmarks("build_temperature_profiles/total_{clusters}") conda: "../envs/environment.yaml" script: @@ -252,21 +252,21 @@ rule build_central_heating_temperature_profiles: "rolling_window_ambient_temperature", ), input: - temp_air_total=resources("temp_air_total_elec_s{simpl}_{clusters}.nc"), - regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), + temp_air_total=resources("temp_air_total_base_s_{clusters}.nc"), + regions_onshore=resources("regions_onshore_base_s_{clusters}.geojson"), output: central_heating_forward_temperature_profiles=resources( - "central_heating_forward_temperature_profiles_elec_s{simpl}_{clusters}.nc" + "central_heating_forward_temperature_profiles_base_s_{clusters}.nc" ), central_heating_return_temperature_profiles=resources( - "central_heating_return_temperature_profiles_elec_s{simpl}_{clusters}.nc" + "central_heating_return_temperature_profiles_base_s_{clusters}.nc" ), resources: mem_mb=20000, log: - logs("build_central_heating_temperature_profiles_s{simpl}_{clusters}.log"), + logs("build_central_heating_temperature_profiles_s_{clusters}.log"), benchmark: - benchmarks("build_central_heating_temperature_profiles/s{simpl}_{clusters}") + benchmarks("build_central_heating_temperature_profiles/s_{clusters}") conda: "../envs/environment.yaml" script: @@ -288,22 +288,22 @@ rule build_cop_profiles: snapshots=config_provider("snapshots"), input: central_heating_forward_temperature_profiles=resources( - "central_heating_forward_temperature_profiles_elec_s{simpl}_{clusters}.nc" + "central_heating_forward_temperature_profiles_base_s_{clusters}.nc" ), central_heating_return_temperature_profiles=resources( - "central_heating_return_temperature_profiles_elec_s{simpl}_{clusters}.nc" + "central_heating_return_temperature_profiles_base_s_{clusters}.nc" ), - temp_soil_total=resources("temp_soil_total_elec_s{simpl}_{clusters}.nc"), - temp_air_total=resources("temp_air_total_elec_s{simpl}_{clusters}.nc"), - regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), + temp_soil_total=resources("temp_soil_total_base_s_{clusters}.nc"), + temp_air_total=resources("temp_air_total_base_s_{clusters}.nc"), + regions_onshore=resources("regions_onshore_base_s_{clusters}.geojson"), output: - cop_profiles=resources("cop_profiles_elec_s{simpl}_{clusters}.nc"), + cop_profiles=resources("cop_profiles_base_s_{clusters}.nc"), resources: mem_mb=20000, log: - logs("build_cop_profiles_s{simpl}_{clusters}.log"), + logs("build_cop_profiles_s_{clusters}.log"), benchmark: - benchmarks("build_cop_profiles/s{simpl}_{clusters}") + benchmarks("build_cop_profiles/s_{clusters}") conda: "../envs/environment.yaml" script: @@ -330,17 +330,17 @@ rule build_solar_thermal_profiles: solar_thermal=config_provider("solar_thermal"), input: pop_layout=resources("pop_layout_total.nc"), - regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), + regions_onshore=resources("regions_onshore_base_s_{clusters}.geojson"), cutout=solar_thermal_cutout, output: - solar_thermal=resources("solar_thermal_total_elec_s{simpl}_{clusters}.nc"), + solar_thermal=resources("solar_thermal_total_base_s_{clusters}.nc"), resources: mem_mb=20000, threads: 16 log: - logs("build_solar_thermal_profiles_total_s{simpl}_{clusters}.log"), + logs("build_solar_thermal_profiles_total_s_{clusters}.log"), benchmark: - benchmarks("build_solar_thermal_profiles/total_s{simpl}_{clusters}") + benchmarks("build_solar_thermal_profiles/total_{clusters}") conda: "../envs/environment.yaml" script: @@ -406,25 +406,25 @@ rule build_biomass_potentials: enspreso_biomass="data/ENSPRESO_BIOMASS.xlsx", eurostat="data/eurostat/Balances-April2023", nuts2="data/nuts/NUTS_RG_03M_2013_4326_LEVL_2.geojson", - regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), + regions_onshore=resources("regions_onshore_base_s_{clusters}.geojson"), nuts3_population=ancient("data/bundle/nama_10r_3popgdp.tsv.gz"), swiss_cantons=ancient("data/ch_cantons.csv"), swiss_population=ancient("data/bundle/je-e-21.03.02.xls"), country_shapes=resources("country_shapes.geojson"), output: biomass_potentials_all=resources( - "biomass_potentials_all_s{simpl}_{clusters}_{planning_horizons}.csv" + "biomass_potentials_all_{clusters}_{planning_horizons}.csv" ), biomass_potentials=resources( - "biomass_potentials_s{simpl}_{clusters}_{planning_horizons}.csv" + "biomass_potentials_s_{clusters}_{planning_horizons}.csv" ), threads: 8 resources: mem_mb=1000, log: - logs("build_biomass_potentials_s{simpl}_{clusters}_{planning_horizons}.log"), + logs("build_biomass_potentials_s_{clusters}_{planning_horizons}.log"), benchmark: - benchmarks("build_biomass_potentials_s{simpl}_{clusters}_{planning_horizons}") + benchmarks("build_biomass_potentials_s_{clusters}_{planning_horizons}") conda: "../envs/environment.yaml" script: @@ -457,19 +457,19 @@ rule build_sequestration_potentials: ), input: sequestration_potential="data/complete_map_2020_unit_Mt.geojson", - regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), - regions_offshore=resources("regions_offshore_elec_s{simpl}_{clusters}.geojson"), + regions_onshore=resources("regions_onshore_base_s_{clusters}.geojson"), + regions_offshore=resources("regions_offshore_base_s_{clusters}.geojson"), output: sequestration_potential=resources( - "co2_sequestration_potential_elec_s{simpl}_{clusters}.csv" + "co2_sequestration_potential_base_s_{clusters}.csv" ), threads: 1 resources: mem_mb=4000, log: - logs("build_sequestration_potentials_s{simpl}_{clusters}.log"), + logs("build_sequestration_potentials_{clusters}.log"), benchmark: - benchmarks("build_sequestration_potentials_s{simpl}_{clusters}") + benchmarks("build_sequestration_potentials_{clusters}") conda: "../envs/environment.yaml" script: @@ -479,17 +479,17 @@ rule build_sequestration_potentials: rule build_salt_cavern_potentials: input: salt_caverns="data/bundle/h2_salt_caverns_GWh_per_sqkm.geojson", - regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), - regions_offshore=resources("regions_offshore_elec_s{simpl}_{clusters}.geojson"), + regions_onshore=resources("regions_onshore_base_s_{clusters}.geojson"), + regions_offshore=resources("regions_offshore_base_s_{clusters}.geojson"), output: - h2_cavern_potential=resources("salt_cavern_potentials_s{simpl}_{clusters}.csv"), + h2_cavern_potential=resources("salt_cavern_potentials_s_{clusters}.csv"), threads: 1 resources: mem_mb=2000, log: - logs("build_salt_cavern_potentials_s{simpl}_{clusters}.log"), + logs("build_salt_cavern_potentials_s_{clusters}.log"), benchmark: - benchmarks("build_salt_cavern_potentials_s{simpl}_{clusters}") + benchmarks("build_salt_cavern_potentials_s_{clusters}") conda: "../envs/environment.yaml" script: @@ -625,8 +625,8 @@ rule build_industrial_distribution_key: ), countries=config_provider("countries"), input: - regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), - clustered_pop_layout=resources("pop_layout_elec_s{simpl}_{clusters}.csv"), + regions_onshore=resources("regions_onshore_base_s_{clusters}.geojson"), + clustered_pop_layout=resources("pop_layout_base_s_{clusters}.csv"), hotmaps="data/Industrial_Database.csv", gem_gspt="data/gem/Global-Steel-Plant-Tracker-April-2024-Standard-Copy-V1.xlsx", ammonia="data/ammonia_plants.csv", @@ -634,15 +634,15 @@ rule build_industrial_distribution_key: refineries_supplement="data/refineries-noneu.csv", output: industrial_distribution_key=resources( - "industrial_distribution_key_elec_s{simpl}_{clusters}.csv" + "industrial_distribution_key_base_s_{clusters}.csv" ), threads: 1 resources: mem_mb=1000, log: - logs("build_industrial_distribution_key_s{simpl}_{clusters}.log"), + logs("build_industrial_distribution_key_{clusters}.log"), benchmark: - benchmarks("build_industrial_distribution_key/s{simpl}_{clusters}") + benchmarks("build_industrial_distribution_key/s_{clusters}") conda: "../envs/environment.yaml" script: @@ -652,26 +652,24 @@ rule build_industrial_distribution_key: rule build_industrial_production_per_node: input: industrial_distribution_key=resources( - "industrial_distribution_key_elec_s{simpl}_{clusters}.csv" + "industrial_distribution_key_base_s_{clusters}.csv" ), industrial_production_per_country_tomorrow=resources( "industrial_production_per_country_tomorrow_{planning_horizons}.csv" ), output: industrial_production_per_node=resources( - "industrial_production_elec_s{simpl}_{clusters}_{planning_horizons}.csv" + "industrial_production_base_s_{clusters}_{planning_horizons}.csv" ), threads: 1 resources: mem_mb=1000, log: - logs( - "build_industrial_production_per_node_s{simpl}_{clusters}_{planning_horizons}.log" - ), + logs("build_industrial_production_per_node_{clusters}_{planning_horizons}.log"), benchmark: ( benchmarks( - "build_industrial_production_per_node/s{simpl}_{clusters}_{planning_horizons}" + "build_industrial_production_per_node/s_{clusters}_{planning_horizons}" ) ) conda: @@ -686,26 +684,26 @@ rule build_industrial_energy_demand_per_node: "industry_sector_ratios_{planning_horizons}.csv" ), industrial_production_per_node=resources( - "industrial_production_elec_s{simpl}_{clusters}_{planning_horizons}.csv" + "industrial_production_base_s_{clusters}_{planning_horizons}.csv" ), industrial_energy_demand_per_node_today=resources( - "industrial_energy_demand_today_elec_s{simpl}_{clusters}.csv" + "industrial_energy_demand_today_base_s_{clusters}.csv" ), output: industrial_energy_demand_per_node=resources( - "industrial_energy_demand_elec_s{simpl}_{clusters}_{planning_horizons}.csv" + "industrial_energy_demand_base_s_{clusters}_{planning_horizons}.csv" ), threads: 1 resources: mem_mb=1000, log: logs( - "build_industrial_energy_demand_per_node_s{simpl}_{clusters}_{planning_horizons}.log" + "build_industrial_energy_demand_per_node_{clusters}_{planning_horizons}.log" ), benchmark: ( benchmarks( - "build_industrial_energy_demand_per_node/s{simpl}_{clusters}_{planning_horizons}" + "build_industrial_energy_demand_per_node/s_{clusters}_{planning_horizons}" ) ) conda: @@ -744,22 +742,22 @@ rule build_industrial_energy_demand_per_country_today: rule build_industrial_energy_demand_per_node_today: input: industrial_distribution_key=resources( - "industrial_distribution_key_elec_s{simpl}_{clusters}.csv" + "industrial_distribution_key_base_s_{clusters}.csv" ), industrial_energy_demand_per_country_today=resources( "industrial_energy_demand_per_country_today.csv" ), output: industrial_energy_demand_per_node_today=resources( - "industrial_energy_demand_today_elec_s{simpl}_{clusters}.csv" + "industrial_energy_demand_today_base_s_{clusters}.csv" ), threads: 1 resources: mem_mb=1000, log: - logs("build_industrial_energy_demand_per_node_today_s{simpl}_{clusters}.log"), + logs("build_industrial_energy_demand_per_node_today_{clusters}.log"), benchmark: - benchmarks("build_industrial_energy_demand_per_node_today/s{simpl}_{clusters}") + benchmarks("build_industrial_energy_demand_per_node_today/s_{clusters}") conda: "../envs/environment.yaml" script: @@ -773,23 +771,23 @@ rule build_retro_cost: input: building_stock="data/retro/data_building_stock.csv", data_tabula="data/bundle/retro/tabula-calculator-calcsetbuilding.csv", - air_temperature=resources("temp_air_total_elec_s{simpl}_{clusters}.nc"), + air_temperature=resources("temp_air_total_base_s_{clusters}.nc"), u_values_PL="data/retro/u_values_poland.csv", tax_w="data/retro/electricity_taxes_eu.csv", construction_index="data/retro/comparative_level_investment.csv", floor_area_missing="data/retro/floor_area_missing.csv", - clustered_pop_layout=resources("pop_layout_elec_s{simpl}_{clusters}.csv"), + clustered_pop_layout=resources("pop_layout_base_s_{clusters}.csv"), cost_germany="data/retro/retro_cost_germany.csv", window_assumptions="data/retro/window_assumptions.csv", output: - retro_cost=resources("retro_cost_elec_s{simpl}_{clusters}.csv"), - floor_area=resources("floor_area_elec_s{simpl}_{clusters}.csv"), + retro_cost=resources("retro_cost_base_s_{clusters}.csv"), + floor_area=resources("floor_area_base_s_{clusters}.csv"), resources: mem_mb=1000, log: - logs("build_retro_cost_s{simpl}_{clusters}.log"), + logs("build_retro_cost_{clusters}.log"), benchmark: - benchmarks("build_retro_cost/s{simpl}_{clusters}") + benchmarks("build_retro_cost/s_{clusters}") conda: "../envs/environment.yaml" script: @@ -801,14 +799,14 @@ rule build_population_weighted_energy_totals: snapshots=config_provider("snapshots"), input: energy_totals=resources("{kind}_totals.csv"), - clustered_pop_layout=resources("pop_layout_elec_s{simpl}_{clusters}.csv"), + clustered_pop_layout=resources("pop_layout_base_s_{clusters}.csv"), output: - resources("pop_weighted_{kind}_totals_s{simpl}_{clusters}.csv"), + resources("pop_weighted_{kind}_totals_s_{clusters}.csv"), threads: 1 resources: mem_mb=2000, log: - logs("build_population_weighted_{kind}_totals_s{simpl}_{clusters}.log"), + logs("build_population_weighted_{kind}_totals_{clusters}.log"), conda: "../envs/environment.yaml" script: @@ -819,17 +817,17 @@ rule build_shipping_demand: input: ports="data/attributed_ports.json", scope=resources("europe_shape.geojson"), - regions=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), + regions=resources("regions_onshore_base_s_{clusters}.geojson"), demand=resources("energy_totals.csv"), params: energy_totals_year=config_provider("energy", "energy_totals_year"), output: - resources("shipping_demand_s{simpl}_{clusters}.csv"), + resources("shipping_demand_s_{clusters}.csv"), threads: 1 resources: mem_mb=2000, log: - logs("build_shipping_demand_s{simpl}_{clusters}.log"), + logs("build_shipping_demand_s_{clusters}.log"), conda: "../envs/environment.yaml" script: @@ -843,24 +841,24 @@ rule build_transport_demand: sector=config_provider("sector"), energy_totals_year=config_provider("energy", "energy_totals_year"), input: - clustered_pop_layout=resources("pop_layout_elec_s{simpl}_{clusters}.csv"), + clustered_pop_layout=resources("pop_layout_base_s_{clusters}.csv"), pop_weighted_energy_totals=resources( - "pop_weighted_energy_totals_s{simpl}_{clusters}.csv" + "pop_weighted_energy_totals_s_{clusters}.csv" ), transport_data=resources("transport_data.csv"), traffic_data_KFZ="data/bundle/emobility/KFZ__count", traffic_data_Pkw="data/bundle/emobility/Pkw__count", - temp_air_total=resources("temp_air_total_elec_s{simpl}_{clusters}.nc"), + temp_air_total=resources("temp_air_total_base_s_{clusters}.nc"), output: - transport_demand=resources("transport_demand_s{simpl}_{clusters}.csv"), - transport_data=resources("transport_data_s{simpl}_{clusters}.csv"), - avail_profile=resources("avail_profile_s{simpl}_{clusters}.csv"), - dsm_profile=resources("dsm_profile_s{simpl}_{clusters}.csv"), + transport_demand=resources("transport_demand_s_{clusters}.csv"), + transport_data=resources("transport_data_s_{clusters}.csv"), + avail_profile=resources("avail_profile_s_{clusters}.csv"), + dsm_profile=resources("dsm_profile_s_{clusters}.csv"), threads: 1 resources: mem_mb=2000, log: - logs("build_transport_demand_s{simpl}_{clusters}.log"), + logs("build_transport_demand_s_{clusters}.log"), conda: "../envs/environment.yaml" script: @@ -873,16 +871,16 @@ rule build_district_heat_share: energy_totals_year=config_provider("energy", "energy_totals_year"), input: district_heat_share=resources("district_heat_share.csv"), - clustered_pop_layout=resources("pop_layout_elec_s{simpl}_{clusters}.csv"), + clustered_pop_layout=resources("pop_layout_base_s_{clusters}.csv"), output: district_heat_share=resources( - "district_heat_share_elec_s{simpl}_{clusters}_{planning_horizons}.csv" + "district_heat_share_base_s_{clusters}_{planning_horizons}.csv" ), threads: 1 resources: mem_mb=1000, log: - logs("build_district_heat_share_s{simpl}_{clusters}_{planning_horizons}.log"), + logs("build_district_heat_share_{clusters}_{planning_horizons}.log"), conda: "../envs/environment.yaml" script: @@ -896,27 +894,27 @@ rule build_existing_heating_distribution: existing_capacities=config_provider("existing_capacities"), input: existing_heating="data/existing_infrastructure/existing_heating_raw.csv", - clustered_pop_layout=resources("pop_layout_elec_s{simpl}_{clusters}.csv"), + clustered_pop_layout=resources("pop_layout_base_s_{clusters}.csv"), clustered_pop_energy_layout=resources( - "pop_weighted_energy_totals_s{simpl}_{clusters}.csv" + "pop_weighted_energy_totals_s_{clusters}.csv" ), district_heat_share=resources( - "district_heat_share_elec_s{simpl}_{clusters}_{planning_horizons}.csv" + "district_heat_share_base_s_{clusters}_{planning_horizons}.csv" ), output: existing_heating_distribution=resources( - "existing_heating_distribution_elec_s{simpl}_{clusters}_{planning_horizons}.csv" + "existing_heating_distribution_base_s_{clusters}_{planning_horizons}.csv" ), threads: 1 resources: mem_mb=2000, log: logs( - "build_existing_heating_distribution_elec_s{simpl}_{clusters}_{planning_horizons}.log" + "build_existing_heating_distribution_base_s_{clusters}_{planning_horizons}.log" ), benchmark: benchmarks( - "build_existing_heating_distribution/elec_s{simpl}_{clusters}_{planning_horizons}" + "build_existing_heating_distribution/base_s_{clusters}_{planning_horizons}" ) conda: "../envs/environment.yaml" @@ -930,28 +928,28 @@ rule time_aggregation: drop_leap_day=config_provider("enable", "drop_leap_day"), solver_name=config_provider("solving", "solver", "name"), input: - network=resources("networks/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc"), + network=resources("networks/base_s_{clusters}_elec_l{ll}_{opts}.nc"), hourly_heat_demand_total=lambda w: ( - resources("hourly_heat_demand_total_elec_s{simpl}_{clusters}.nc") + resources("hourly_heat_demand_total_base_s_{clusters}.nc") if config_provider("sector", "heating")(w) else [] ), solar_thermal_total=lambda w: ( - resources("solar_thermal_total_elec_s{simpl}_{clusters}.nc") + resources("solar_thermal_total_base_s_{clusters}.nc") if config_provider("sector", "solar_thermal")(w) else [] ), output: snapshot_weightings=resources( - "snapshot_weightings_elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.csv" + "snapshot_weightings_base_s_{clusters}_elec_l{ll}_{opts}.csv" ), threads: 1 resources: mem_mb=5000, log: - logs("time_aggregation_elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.log"), + logs("time_aggregation_base_s_{clusters}_elec_l{ll}_{opts}.log"), benchmark: - benchmarks("time_aggregation_elec_s{simpl}_{clusters}_ec_l{ll}_{opts}") + benchmarks("time_aggregation_base_s_{clusters}_elec_l{ll}_{opts}") conda: "../envs/environment.yaml" script: @@ -960,7 +958,7 @@ rule time_aggregation: def input_profile_offwind(w): return { - f"profile_{tech}": resources(f"profile_{tech}.nc") + f"profile_{tech}": resources("profile_{clusters}_" + tech + ".nc") for tech in ["offwind-ac", "offwind-dc", "offwind-float"] if (tech in config_provider("electricity", "renewable_carriers")(w)) } @@ -973,21 +971,21 @@ rule build_egs_potentials: costs=config_provider("costs"), input: egs_cost="data/egs_costs.json", - regions=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), + regions=resources("regions_onshore_base_s_{clusters}.geojson"), air_temperature=( - resources("temp_air_total_elec_s{simpl}_{clusters}.nc") + resources("temp_air_total_base_s_{clusters}.nc") if config_provider("sector", "enhanced_geothermal", "var_cf") else [] ), output: - egs_potentials=resources("egs_potentials_s{simpl}_{clusters}.csv"), - egs_overlap=resources("egs_overlap_s{simpl}_{clusters}.csv"), - egs_capacity_factors=resources("egs_capacity_factors_s{simpl}_{clusters}.csv"), + egs_potentials=resources("egs_potentials_{clusters}.csv"), + egs_overlap=resources("egs_overlap_{clusters}.csv"), + egs_capacity_factors=resources("egs_capacity_factors_{clusters}.csv"), threads: 1 resources: mem_mb=2000, log: - logs("build_egs_potentials_s{simpl}_{clusters}.log"), + logs("build_egs_potentials_{clusters}.log"), conda: "../envs/environment.yaml" script: @@ -1005,6 +1003,7 @@ rule prepare_sector_network: costs=config_provider("costs"), sector=config_provider("sector"), industry=config_provider("industry"), + renewable=config_provider("renewable"), lines=config_provider("lines"), pypsa_eur=config_provider("pypsa_eur"), length_factor=config_provider("lines", "length_factor"), @@ -1022,15 +1021,15 @@ rule prepare_sector_network: **rules.cluster_gas_network.output, **rules.build_gas_input_locations.output, snapshot_weightings=resources( - "snapshot_weightings_elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.csv" + "snapshot_weightings_base_s_{clusters}_elec_l{ll}_{opts}.csv" ), retro_cost=lambda w: ( - resources("retro_cost_elec_s{simpl}_{clusters}.csv") + resources("retro_cost_base_s_{clusters}.csv") if config_provider("sector", "retrofitting", "retro_endogen")(w) else [] ), floor_area=lambda w: ( - resources("floor_area_elec_s{simpl}_{clusters}.csv") + resources("floor_area_base_s_{clusters}.csv") if config_provider("sector", "retrofitting", "retro_endogen")(w) else [] ), @@ -1041,96 +1040,91 @@ rule prepare_sector_network: else [] ), sequestration_potential=lambda w: ( - resources("co2_sequestration_potential_elec_s{simpl}_{clusters}.csv") + resources("co2_sequestration_potential_base_s_{clusters}.csv") if config_provider( "sector", "regional_co2_sequestration_potential", "enable" )(w) else [] ), - network=resources("networks/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc"), + network=resources("networks/base_s_{clusters}_elec_l{ll}_{opts}.nc"), eurostat="data/eurostat/Balances-April2023", pop_weighted_energy_totals=resources( - "pop_weighted_energy_totals_s{simpl}_{clusters}.csv" - ), - pop_weighted_heat_totals=resources( - "pop_weighted_heat_totals_s{simpl}_{clusters}.csv" - ), - shipping_demand=resources("shipping_demand_s{simpl}_{clusters}.csv"), - transport_demand=resources("transport_demand_s{simpl}_{clusters}.csv"), - transport_data=resources("transport_data_s{simpl}_{clusters}.csv"), - avail_profile=resources("avail_profile_s{simpl}_{clusters}.csv"), - dsm_profile=resources("dsm_profile_s{simpl}_{clusters}.csv"), + "pop_weighted_energy_totals_s_{clusters}.csv" + ), + pop_weighted_heat_totals=resources("pop_weighted_heat_totals_s_{clusters}.csv"), + shipping_demand=resources("shipping_demand_s_{clusters}.csv"), + transport_demand=resources("transport_demand_s_{clusters}.csv"), + transport_data=resources("transport_data_s_{clusters}.csv"), + avail_profile=resources("avail_profile_s_{clusters}.csv"), + dsm_profile=resources("dsm_profile_s_{clusters}.csv"), co2_totals_name=resources("co2_totals.csv"), co2="data/bundle/eea/UNFCCC_v23.csv", biomass_potentials=lambda w: ( resources( - "biomass_potentials_s{simpl}_{clusters}_" + "biomass_potentials_s_{clusters}_" + "{}.csv".format(config_provider("biomass", "year")(w)) ) if config_provider("foresight")(w) == "overnight" - else resources( - "biomass_potentials_s{simpl}_{clusters}_{planning_horizons}.csv" - ) + else resources("biomass_potentials_s_{clusters}_{planning_horizons}.csv") ), costs=lambda w: ( resources("costs_{}.csv".format(config_provider("costs", "year")(w))) if config_provider("foresight")(w) == "overnight" else resources("costs_{planning_horizons}.csv") ), - h2_cavern=resources("salt_cavern_potentials_s{simpl}_{clusters}.csv"), - busmap_s=resources("busmap_elec_s{simpl}.csv"), - busmap=resources("busmap_elec_s{simpl}_{clusters}.csv"), - clustered_pop_layout=resources("pop_layout_elec_s{simpl}_{clusters}.csv"), - simplified_pop_layout=resources("pop_layout_elec_s{simpl}.csv"), + h2_cavern=resources("salt_cavern_potentials_s_{clusters}.csv"), + busmap_s=resources("busmap_base_s.csv"), + busmap=resources("busmap_base_s_{clusters}.csv"), + clustered_pop_layout=resources("pop_layout_base_s_{clusters}.csv"), industrial_demand=resources( - "industrial_energy_demand_elec_s{simpl}_{clusters}_{planning_horizons}.csv" + "industrial_energy_demand_base_s_{clusters}_{planning_horizons}.csv" ), hourly_heat_demand_total=resources( - "hourly_heat_demand_total_elec_s{simpl}_{clusters}.nc" + "hourly_heat_demand_total_base_s_{clusters}.nc" ), industrial_production=resources( - "industrial_production_elec_s{simpl}_{clusters}_{planning_horizons}.csv" + "industrial_production_base_s_{clusters}_{planning_horizons}.csv" ), district_heat_share=resources( - "district_heat_share_elec_s{simpl}_{clusters}_{planning_horizons}.csv" + "district_heat_share_base_s_{clusters}_{planning_horizons}.csv" ), heating_efficiencies=resources("heating_efficiencies.csv"), - temp_soil_total=resources("temp_soil_total_elec_s{simpl}_{clusters}.nc"), - temp_air_total=resources("temp_air_total_elec_s{simpl}_{clusters}.nc"), - cop_profiles=resources("cop_profiles_elec_s{simpl}_{clusters}.nc"), + temp_soil_total=resources("temp_soil_total_base_s_{clusters}.nc"), + temp_air_total=resources("temp_air_total_base_s_{clusters}.nc"), + cop_profiles=resources("cop_profiles_base_s_{clusters}.nc"), solar_thermal_total=lambda w: ( - resources("solar_thermal_total_elec_s{simpl}_{clusters}.nc") + resources("solar_thermal_total_base_s_{clusters}.nc") if config_provider("sector", "solar_thermal")(w) else [] ), egs_potentials=lambda w: ( - resources("egs_potentials_s{simpl}_{clusters}.csv") + resources("egs_potentials_{clusters}.csv") if config_provider("sector", "enhanced_geothermal", "enable")(w) else [] ), egs_overlap=lambda w: ( - resources("egs_overlap_s{simpl}_{clusters}.csv") + resources("egs_overlap_{clusters}.csv") if config_provider("sector", "enhanced_geothermal", "enable")(w) else [] ), egs_capacity_factors=lambda w: ( - resources("egs_capacity_factors_s{simpl}_{clusters}.csv") + resources("egs_capacity_factors_{clusters}.csv") if config_provider("sector", "enhanced_geothermal", "enable")(w) else [] ), output: RESULTS - + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + + "prenetworks/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", threads: 1 resources: mem_mb=2000, log: RESULTS - + "logs/prepare_sector_network_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.log", + + "logs/prepare_sector_network_base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.log", benchmark: ( RESULTS - + "benchmarks/prepare_sector_network/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" + + "benchmarks/prepare_sector_network/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" ) conda: "../envs/environment.yaml" diff --git a/rules/collect.smk b/rules/collect.smk index 214b8102a..ca58e17c3 100644 --- a/rules/collect.smk +++ b/rules/collect.smk @@ -6,7 +6,6 @@ localrules: all, cluster_networks, - extra_components_networks, prepare_elec_networks, prepare_sector_networks, solve_elec_networks, @@ -16,16 +15,7 @@ localrules: rule cluster_networks: input: expand( - resources("networks/elec_s{simpl}_{clusters}.nc"), - **config["scenario"], - run=config["run"]["name"], - ), - - -rule extra_components_networks: - input: - expand( - resources("networks/elec_s{simpl}_{clusters}_ec.nc"), + resources("networks/base_s_{clusters}.nc"), **config["scenario"], run=config["run"]["name"], ), @@ -34,7 +24,7 @@ rule extra_components_networks: rule prepare_elec_networks: input: expand( - resources("networks/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc"), + resources("networks/base_s_{clusters}_elec_l{ll}_{opts}.nc"), **config["scenario"], run=config["run"]["name"], ), @@ -44,7 +34,7 @@ rule prepare_sector_networks: input: expand( RESULTS - + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + + "prenetworks/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", **config["scenario"], run=config["run"]["name"], ), @@ -53,7 +43,7 @@ rule prepare_sector_networks: rule solve_elec_networks: input: expand( - RESULTS + "networks/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc", + RESULTS + "networks/base_s_{clusters}_elec_l{ll}_{opts}.nc", **config["scenario"], run=config["run"]["name"], ), @@ -63,7 +53,7 @@ rule solve_sector_networks: input: expand( RESULTS - + "postnetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + + "postnetworks/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", **config["scenario"], run=config["run"]["name"], ), @@ -73,7 +63,7 @@ rule solve_sector_networks_perfect: input: expand( RESULTS - + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-costs-all_{planning_horizons}.pdf", + + "maps/base_s_{clusters}_l{ll}_{opts}_{sector_opts}-costs-all_{planning_horizons}.pdf", **config["scenario"], run=config["run"]["name"], ), @@ -82,14 +72,13 @@ rule solve_sector_networks_perfect: rule validate_elec_networks: input: expand( - RESULTS - + "figures/.statistics_plots_elec_s{simpl}_{clusters}_ec_l{ll}_{opts}", + RESULTS + "figures/.statistics_plots_base_s_{clusters}_elec_l{ll}_{opts}", **config["scenario"], run=config["run"]["name"], ), expand( RESULTS - + "figures/.validation_{kind}_plots_elec_s{simpl}_{clusters}_ec_l{ll}_{opts}", + + "figures/.validation_{kind}_plots_base_s_{clusters}_elec_l{ll}_{opts}", **config["scenario"], run=config["run"]["name"], kind=["production", "prices", "cross_border"], diff --git a/rules/common.smk b/rules/common.smk index 203fb9c06..332103f26 100644 --- a/rules/common.smk +++ b/rules/common.smk @@ -98,9 +98,7 @@ def memory(w): if m is not None: factor *= int(m.group(1)) / 8760 break - if w.clusters.endswith("m") or w.clusters.endswith("c"): - return int(factor * (55000 + 600 * int(w.clusters[:-1]))) - elif w.clusters == "all": + if w.clusters == "all": return int(factor * (18000 + 180 * 4000)) else: return int(factor * (10000 + 195 * int(w.clusters))) @@ -144,7 +142,7 @@ def solved_previous_horizon(w): return ( RESULTS - + "postnetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_" + + "postnetworks/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_" + planning_horizon_p + ".nc" ) diff --git a/rules/postprocess.smk b/rules/postprocess.smk index edeff1ef4..24a7ac34c 100644 --- a/rules/postprocess.smk +++ b/rules/postprocess.smk @@ -9,17 +9,15 @@ if config["foresight"] != "perfect": params: plotting=config_provider("plotting"), input: - network=resources("networks/elec_s{simpl}_{clusters}.nc"), - regions_onshore=resources( - "regions_onshore_elec_s{simpl}_{clusters}.geojson" - ), + network=resources("networks/base_s_{clusters}.nc"), + regions_onshore=resources("regions_onshore_base_s_{clusters}.geojson"), output: - map=resources("maps/power-network-s{simpl}-{clusters}.pdf"), + map=resources("maps/power-network-s-{clusters}.pdf"), threads: 1 resources: mem_mb=4000, benchmark: - benchmarks("plot_power_network_clustered/elec_s{simpl}_{clusters}") + benchmarks("plot_power_network_clustered/base_s_{clusters}") conda: "../envs/environment.yaml" script: @@ -30,21 +28,21 @@ if config["foresight"] != "perfect": plotting=config_provider("plotting"), input: network=RESULTS - + "postnetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", - regions=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), + + "postnetworks/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + regions=resources("regions_onshore_base_s_{clusters}.geojson"), output: map=RESULTS - + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-costs-all_{planning_horizons}.pdf", + + "maps/base_s_{clusters}_l{ll}_{opts}_{sector_opts}-costs-all_{planning_horizons}.pdf", threads: 2 resources: mem_mb=10000, log: RESULTS - + "logs/plot_power_network/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.log", + + "logs/plot_power_network/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.log", benchmark: ( RESULTS - + "benchmarks/plot_power_network/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" + + "benchmarks/plot_power_network/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" ) conda: "../envs/environment.yaml" @@ -57,21 +55,21 @@ if config["foresight"] != "perfect": foresight=config_provider("foresight"), input: network=RESULTS - + "postnetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", - regions=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), + + "postnetworks/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + regions=resources("regions_onshore_base_s_{clusters}.geojson"), output: map=RESULTS - + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-h2_network_{planning_horizons}.pdf", + + "maps/base_s_{clusters}_l{ll}_{opts}_{sector_opts}-h2_network_{planning_horizons}.pdf", threads: 2 resources: mem_mb=10000, log: RESULTS - + "logs/plot_hydrogen_network/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.log", + + "logs/plot_hydrogen_network/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.log", benchmark: ( RESULTS - + "benchmarks/plot_hydrogen_network/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" + + "benchmarks/plot_hydrogen_network/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" ) conda: "../envs/environment.yaml" @@ -83,21 +81,21 @@ if config["foresight"] != "perfect": plotting=config_provider("plotting"), input: network=RESULTS - + "postnetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", - regions=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), + + "postnetworks/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + regions=resources("regions_onshore_base_s_{clusters}.geojson"), output: map=RESULTS - + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-ch4_network_{planning_horizons}.pdf", + + "maps/base_s_{clusters}_l{ll}_{opts}_{sector_opts}-ch4_network_{planning_horizons}.pdf", threads: 2 resources: mem_mb=10000, log: RESULTS - + "logs/plot_gas_network/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.log", + + "logs/plot_gas_network/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.log", benchmark: ( RESULTS - + "benchmarks/plot_gas_network/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" + + "benchmarks/plot_gas_network/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" ) conda: "../envs/environment.yaml" @@ -110,7 +108,7 @@ if config["foresight"] == "perfect": def output_map_year(w): return { f"map_{year}": RESULTS - + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-costs-all_" + + "maps/base_s_{clusters}_l{ll}_{opts}_{sector_opts}-costs-all_" + f"{year}.pdf" for year in config_provider("scenario", "planning_horizons")(w) } @@ -120,8 +118,8 @@ if config["foresight"] == "perfect": plotting=config_provider("plotting"), input: network=RESULTS - + "postnetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_brownfield_all_years.nc", - regions=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), + + "postnetworks/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_brownfield_all_years.nc", + regions=resources("regions_onshore_base_s_{clusters}.geojson"), output: unpack(output_map_year), threads: 2 @@ -144,7 +142,7 @@ rule make_summary: input: networks=expand( RESULTS - + "postnetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + + "postnetworks/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", **config["scenario"], allow_missing=True, ), @@ -158,20 +156,20 @@ rule make_summary: ) ), ac_plot=expand( - resources("maps/power-network-s{simpl}-{clusters}.pdf"), + resources("maps/power-network-s-{clusters}.pdf"), **config["scenario"], allow_missing=True, ), costs_plot=expand( RESULTS - + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-costs-all_{planning_horizons}.pdf", + + "maps/base_s_{clusters}_l{ll}_{opts}_{sector_opts}-costs-all_{planning_horizons}.pdf", **config["scenario"], allow_missing=True, ), h2_plot=lambda w: expand( ( RESULTS - + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-h2_network_{planning_horizons}.pdf" + + "maps/base_s_{clusters}_l{ll}_{opts}_{sector_opts}-h2_network_{planning_horizons}.pdf" if config_provider("sector", "H2_network")(w) else [] ), @@ -181,7 +179,7 @@ rule make_summary: ch4_plot=lambda w: expand( ( RESULTS - + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-ch4_network_{planning_horizons}.pdf" + + "maps/base_s_{clusters}_l{ll}_{opts}_{sector_opts}-ch4_network_{planning_horizons}.pdf" if config_provider("sector", "gas_network")(w) else [] ), @@ -260,19 +258,19 @@ STATISTICS_BARPLOTS = [ ] -rule plot_elec_statistics: +rule plot_base_statistics: params: plotting=config_provider("plotting"), barplots=STATISTICS_BARPLOTS, input: - network=RESULTS + "networks/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc", + network=RESULTS + "networks/base_s_{clusters}_elec_l{ll}_{opts}.nc", output: **{ f"{plot}_bar": RESULTS - + f"figures/statistics_{plot}_bar_elec_s{{simpl}}_{{clusters}}_ec_l{{ll}}_{{opts}}.pdf" + + f"figures/statistics_{plot}_bar_base_s_{{clusters}}_elec_l{{ll}}_{{opts}}.pdf" for plot in STATISTICS_BARPLOTS }, barplots_touch=RESULTS - + "figures/.statistics_plots_elec_s{simpl}_{clusters}_ec_l{ll}_{opts}", + + "figures/.statistics_plots_base_s_{clusters}_elec_l{ll}_{opts}", script: "../scripts/plot_statistics.py" diff --git a/rules/solve_electricity.smk b/rules/solve_electricity.smk index e2dbe42ef..204286a30 100644 --- a/rules/solve_electricity.smk +++ b/rules/solve_electricity.smk @@ -13,19 +13,19 @@ rule solve_network: ), custom_extra_functionality=input_custom_extra_functionality, input: - network=resources("networks/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc"), + network=resources("networks/base_s_{clusters}_elec_l{ll}_{opts}.nc"), output: - network=RESULTS + "networks/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc", - config=RESULTS + "configs/config.elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.yaml", + network=RESULTS + "networks/base_s_{clusters}_elec_l{ll}_{opts}.nc", + config=RESULTS + "configs/config.base_s_{clusters}_elec_l{ll}_{opts}.yaml", log: solver=normpath( RESULTS - + "logs/solve_network/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}_solver.log" + + "logs/solve_network/base_s_{clusters}_elec_l{ll}_{opts}_solver.log" ), python=RESULTS - + "logs/solve_network/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}_python.log", + + "logs/solve_network/base_s_{clusters}_elec_l{ll}_{opts}_python.log", benchmark: - (RESULTS + "benchmarks/solve_network/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}") + (RESULTS + "benchmarks/solve_network/base_s_{clusters}_elec_l{ll}_{opts}") threads: solver_threads resources: mem_mb=memory, @@ -49,20 +49,20 @@ rule solve_operations_network: ), custom_extra_functionality=input_custom_extra_functionality, input: - network=RESULTS + "networks/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc", + network=RESULTS + "networks/base_s_{clusters}_elec_l{ll}_{opts}.nc", output: - network=RESULTS + "networks/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}_op.nc", + network=RESULTS + "networks/base_s_{clusters}_elec_l{ll}_{opts}_op.nc", log: solver=normpath( RESULTS - + "logs/solve_operations_network/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}_op_solver.log" + + "logs/solve_operations_network/base_s_{clusters}_elec_l{ll}_{opts}_op_solver.log" ), python=RESULTS - + "logs/solve_operations_network/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}_op_python.log", + + "logs/solve_operations_network/base_s_{clusters}_elec_l{ll}_{opts}_op_python.log", benchmark: ( RESULTS - + "benchmarks/solve_operations_network/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}" + + "benchmarks/solve_operations_network/base_s_{clusters}_elec_l{ll}_{opts}" ) threads: 4 resources: diff --git a/rules/solve_myopic.smk b/rules/solve_myopic.smk index 6340787ad..f17959f9a 100644 --- a/rules/solve_myopic.smk +++ b/rules/solve_myopic.smk @@ -13,24 +13,24 @@ rule add_existing_baseyear: energy_totals_year=config_provider("energy", "energy_totals_year"), input: network=RESULTS - + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", - powerplants=resources("powerplants.csv"), - busmap_s=resources("busmap_elec_s{simpl}.csv"), - busmap=resources("busmap_elec_s{simpl}_{clusters}.csv"), - clustered_pop_layout=resources("pop_layout_elec_s{simpl}_{clusters}.csv"), + + "prenetworks/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + powerplants=resources("powerplants_s_{clusters}.csv"), + busmap_s=resources("busmap_base_s.csv"), + busmap=resources("busmap_base_s_{clusters}.csv"), + clustered_pop_layout=resources("pop_layout_base_s_{clusters}.csv"), costs=lambda w: resources( "costs_{}.csv".format( config_provider("scenario", "planning_horizons", 0)(w) ) ), - cop_profiles=resources("cop_profiles_elec_s{simpl}_{clusters}.nc"), + cop_profiles=resources("cop_profiles_base_s_{clusters}.nc"), existing_heating_distribution=resources( - "existing_heating_distribution_elec_s{simpl}_{clusters}_{planning_horizons}.csv" + "existing_heating_distribution_base_s_{clusters}_{planning_horizons}.csv" ), heating_efficiencies=resources("heating_efficiencies.csv"), output: RESULTS - + "prenetworks-brownfield/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + + "prenetworks-brownfield/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", wildcard_constraints: # TODO: The first planning_horizon needs to be aligned across scenarios # snakemake does not support passing functions to wildcard_constraints @@ -41,11 +41,11 @@ rule add_existing_baseyear: mem_mb=2000, log: RESULTS - + "logs/add_existing_baseyear_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.log", + + "logs/add_existing_baseyear_base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.log", benchmark: ( RESULTS - + "benchmarks/add_existing_baseyear/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" + + "benchmarks/add_existing_baseyear/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" ) conda: "../envs/environment.yaml" @@ -55,7 +55,7 @@ rule add_existing_baseyear: def input_profile_tech_brownfield(w): return { - f"profile_{tech}": resources(f"profile_{tech}.nc") + f"profile_{tech}": resources("profile_{clusters}_" + tech + ".nc") for tech in config_provider("electricity", "renewable_carriers")(w) if tech != "hydro" } @@ -74,26 +74,26 @@ rule add_brownfield: heat_pump_sources=config_provider("sector", "heat_pump_sources"), input: unpack(input_profile_tech_brownfield), - simplify_busmap=resources("busmap_elec_s{simpl}.csv"), - cluster_busmap=resources("busmap_elec_s{simpl}_{clusters}.csv"), + simplify_busmap=resources("busmap_base_s.csv"), + cluster_busmap=resources("busmap_base_s_{clusters}.csv"), network=RESULTS - + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + + "prenetworks/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", network_p=solved_previous_horizon, #solved network at previous time step costs=resources("costs_{planning_horizons}.csv"), - cop_profiles=resources("cop_profiles_elec_s{simpl}_{clusters}.nc"), + cop_profiles=resources("cop_profiles_base_s_{clusters}.nc"), output: RESULTS - + "prenetworks-brownfield/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + + "prenetworks-brownfield/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", threads: 4 resources: mem_mb=10000, log: RESULTS - + "logs/add_brownfield_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.log", + + "logs/add_brownfield_base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.log", benchmark: ( RESULTS - + "benchmarks/add_brownfield/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" + + "benchmarks/add_brownfield/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" ) conda: "../envs/environment.yaml" @@ -115,22 +115,22 @@ rule solve_sector_network_myopic: custom_extra_functionality=input_custom_extra_functionality, input: network=RESULTS - + "prenetworks-brownfield/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + + "prenetworks-brownfield/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", costs=resources("costs_{planning_horizons}.csv"), output: network=RESULTS - + "postnetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + + "postnetworks/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", config=RESULTS - + "configs/config.elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.yaml", + + "configs/config.base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.yaml", shadow: "shallow" log: solver=RESULTS - + "logs/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}_solver.log", + + "logs/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}_solver.log", memory=RESULTS - + "logs/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}_memory.log", + + "logs/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}_memory.log", python=RESULTS - + "logs/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}_python.log", + + "logs/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}_python.log", threads: solver_threads resources: mem_mb=config_provider("solving", "mem_mb"), @@ -138,7 +138,7 @@ rule solve_sector_network_myopic: benchmark: ( RESULTS - + "benchmarks/solve_sector_network/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" + + "benchmarks/solve_sector_network/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" ) conda: "../envs/environment.yaml" diff --git a/rules/solve_overnight.smk b/rules/solve_overnight.smk index 26dee7a6f..e8217a5ad 100644 --- a/rules/solve_overnight.smk +++ b/rules/solve_overnight.smk @@ -14,21 +14,21 @@ rule solve_sector_network: custom_extra_functionality=input_custom_extra_functionality, input: network=RESULTS - + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + + "prenetworks/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", output: network=RESULTS - + "postnetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + + "postnetworks/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", config=RESULTS - + "configs/config.elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.yaml", + + "configs/config.base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.yaml", shadow: "shallow" log: solver=RESULTS - + "logs/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}_solver.log", + + "logs/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}_solver.log", memory=RESULTS - + "logs/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}_memory.log", + + "logs/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}_memory.log", python=RESULTS - + "logs/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}_python.log", + + "logs/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}_python.log", threads: solver_threads resources: mem_mb=config_provider("solving", "mem_mb"), @@ -36,7 +36,7 @@ rule solve_sector_network: benchmark: ( RESULTS - + "benchmarks/solve_sector_network/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" + + "benchmarks/solve_sector_network/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" ) conda: "../envs/environment.yaml" diff --git a/rules/solve_perfect.smk b/rules/solve_perfect.smk index f1ec9966e..6ec9aeb48 100644 --- a/rules/solve_perfect.smk +++ b/rules/solve_perfect.smk @@ -11,25 +11,25 @@ rule add_existing_baseyear: energy_totals_year=config_provider("energy", "energy_totals_year"), input: network=RESULTS - + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", - powerplants=resources("powerplants.csv"), - busmap_s=resources("busmap_elec_s{simpl}.csv"), - busmap=resources("busmap_elec_s{simpl}_{clusters}.csv"), - clustered_pop_layout=resources("pop_layout_elec_s{simpl}_{clusters}.csv"), + + "prenetworks/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + powerplants=resources("powerplants_s_{clusters}.csv"), + busmap_s=resources("busmap_base_s.csv"), + busmap=resources("busmap_base_s_{clusters}.csv"), + clustered_pop_layout=resources("pop_layout_base_s_{clusters}.csv"), costs=lambda w: resources( "costs_{}.csv".format( config_provider("scenario", "planning_horizons", 0)(w) ) ), - cop_profiles=resources("cop_profiles_elec_s{simpl}_{clusters}.nc"), + cop_profiles=resources("cop_profiles_base_s_{clusters}.nc"), existing_heating_distribution=resources( - "existing_heating_distribution_elec_s{simpl}_{clusters}_{planning_horizons}.csv" + "existing_heating_distribution_base_s_{clusters}_{planning_horizons}.csv" ), existing_heating="data/existing_infrastructure/existing_heating_raw.csv", heating_efficiencies=resources("heating_efficiencies.csv"), output: RESULTS - + "prenetworks-brownfield/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + + "prenetworks-brownfield/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", wildcard_constraints: planning_horizons=config["scenario"]["planning_horizons"][0], #only applies to baseyear threads: 1 @@ -38,11 +38,11 @@ rule add_existing_baseyear: runtime=config_provider("solving", "runtime", default="24h"), log: logs( - "add_existing_baseyear_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.log" + "add_existing_baseyear_base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.log" ), benchmark: benchmarks( - "add_existing_baseyear/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" + "add_existing_baseyear/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" ) conda: "../envs/environment.yaml" @@ -53,7 +53,7 @@ rule add_existing_baseyear: def input_network_year(w): return { f"network_{year}": RESULTS - + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}" + + "prenetworks/base_s_{clusters}_l{ll}_{opts}_{sector_opts}" + f"_{year}.nc" for year in config_provider("scenario", "planning_horizons")(w)[1:] } @@ -68,25 +68,21 @@ rule prepare_perfect_foresight: brownfield_network=lambda w: ( RESULTS + "prenetworks-brownfield/" - + "elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_" + + "base_s_{clusters}_l{ll}_{opts}_{sector_opts}_" + "{}.nc".format( str(config_provider("scenario", "planning_horizons", 0)(w)) ) ), output: RESULTS - + "prenetworks-brownfield/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_brownfield_all_years.nc", + + "prenetworks-brownfield/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_brownfield_all_years.nc", threads: 2 resources: mem_mb=10000, log: - logs( - "prepare_perfect_foresight{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}.log" - ), + logs("prepare_perfect_foresight_{clusters}_l{ll}_{opts}_{sector_opts}.log"), benchmark: - benchmarks( - "prepare_perfect_foresight{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}" - ) + benchmarks("prepare_perfect_foresight_{clusters}_l{ll}_{opts}_{sector_opts}") conda: "../envs/environment.yaml" script: @@ -105,13 +101,13 @@ rule solve_sector_network_perfect: custom_extra_functionality=input_custom_extra_functionality, input: network=RESULTS - + "prenetworks-brownfield/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_brownfield_all_years.nc", + + "prenetworks-brownfield/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_brownfield_all_years.nc", costs=resources("costs_2030.csv"), output: network=RESULTS - + "postnetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_brownfield_all_years.nc", + + "postnetworks/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_brownfield_all_years.nc", config=RESULTS - + "configs/config.elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_brownfield_all_years.yaml", + + "configs/config.base_s_{clusters}_l{ll}_{opts}_{sector_opts}_brownfield_all_years.yaml", threads: solver_threads resources: mem_mb=config_provider("solving", "mem"), @@ -119,15 +115,15 @@ rule solve_sector_network_perfect: "shallow" log: solver=RESULTS - + "logs/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_brownfield_all_years_solver.log", + + "logs/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_brownfield_all_years_solver.log", python=RESULTS - + "logs/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_brownfield_all_years_python.log", + + "logs/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_brownfield_all_years_python.log", memory=RESULTS - + "logs/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_brownfield_all_years_memory.log", + + "logs/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_brownfield_all_years_memory.log", benchmark: ( RESULTS - + "benchmarks/solve_sector_network/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_brownfield_all_years}" + + "benchmarks/solve_sector_network/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_brownfield_all_years}" ) conda: "../envs/environment.yaml" @@ -137,9 +133,8 @@ rule solve_sector_network_perfect: def input_networks_make_summary_perfect(w): return { - f"networks_{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}": RESULTS - + f"postnetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_brownfield_all_years.nc" - for simpl in config_provider("scenario", "simpl")(w) + f"networks_s_{clusters}_l{ll}_{opts}_{sector_opts}": RESULTS + + f"postnetworks/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_brownfield_all_years.nc" for clusters in config_provider("scenario", "clusters")(w) for opts in config_provider("scenario", "opts")(w) for sector_opts in config_provider("scenario", "sector_opts")(w) diff --git a/rules/validate.smk b/rules/validate.smk index 91fe6e911..7bbefc410 100644 --- a/rules/validate.smk +++ b/rules/validate.smk @@ -69,16 +69,16 @@ rule build_electricity_prices: rule plot_validation_electricity_production: input: - network=RESULTS + "networks/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc", + network=RESULTS + "networks/base_s_{clusters}_elec_l{ll}_{opts}.nc", electricity_production=resources("historical_electricity_production.csv"), output: **{ plot: RESULTS - + f"figures/validation_{plot}_elec_s{{simpl}}_{{clusters}}_ec_l{{ll}}_{{opts}}.pdf" + + f"figures/validation_{plot}_base_s_{{clusters}}_elec_l{{ll}}_{{opts}}.pdf" for plot in PRODUCTION_PLOTS }, plots_touch=RESULTS - + "figures/.validation_production_plots_elec_s{simpl}_{clusters}_ec_l{ll}_{opts}", + + "figures/.validation_production_plots_base_s_{clusters}_elec_l{ll}_{opts}", script: "../scripts/plot_validation_electricity_production.py" @@ -87,31 +87,31 @@ rule plot_validation_cross_border_flows: params: countries=config_provider("countries"), input: - network=RESULTS + "networks/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc", + network=RESULTS + "networks/base_s_{clusters}_elec_l{ll}_{opts}.nc", cross_border_flows=resources("historical_cross_border_flows.csv"), output: **{ plot: RESULTS - + f"figures/validation_{plot}_elec_s{{simpl}}_{{clusters}}_ec_l{{ll}}_{{opts}}.pdf" + + f"figures/validation_{plot}_base_s_{{clusters}}_elec_l{{ll}}_{{opts}}.pdf" for plot in CROSS_BORDER_PLOTS }, plots_touch=RESULTS - + "figures/.validation_cross_border_plots_elec_s{simpl}_{clusters}_ec_l{ll}_{opts}", + + "figures/.validation_cross_border_plots_base_s_{clusters}_elec_l{ll}_{opts}", script: "../scripts/plot_validation_cross_border_flows.py" rule plot_validation_electricity_prices: input: - network=RESULTS + "networks/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc", + network=RESULTS + "networks/base_s_{clusters}_elec_l{ll}_{opts}.nc", electricity_prices=resources("historical_electricity_prices.csv"), output: **{ plot: RESULTS - + f"figures/validation_{plot}_elec_s{{simpl}}_{{clusters}}_ec_l{{ll}}_{{opts}}.pdf" + + f"figures/validation_{plot}_base_s_{{clusters}}_elec_l{{ll}}_{{opts}}.pdf" for plot in PRICES_PLOTS }, plots_touch=RESULTS - + "figures/.validation_prices_plots_elec_s{simpl}_{clusters}_ec_l{ll}_{opts}", + + "figures/.validation_prices_plots_base_s_{clusters}_elec_l{ll}_{opts}", script: "../scripts/plot_validation_electricity_prices.py" diff --git a/scripts/add_brownfield.py b/scripts/add_brownfield.py index 72dd1c88e..65e55f968 100644 --- a/scripts/add_brownfield.py +++ b/scripts/add_brownfield.py @@ -170,14 +170,6 @@ def adjust_renewable_profiles(n, input_profiles, params, year): using the latest year below or equal to the selected year. """ - # spatial clustering - cluster_busmap = pd.read_csv(snakemake.input.cluster_busmap, index_col=0).squeeze() - simplify_busmap = pd.read_csv( - snakemake.input.simplify_busmap, index_col=0 - ).squeeze() - clustermaps = simplify_busmap.map(cluster_busmap) - clustermaps.index = clustermaps.index.astype(str) - # temporal clustering dr = get_snapshots(params["snapshots"], params["drop_leap_day"]) snapshotmaps = ( @@ -202,11 +194,6 @@ def adjust_renewable_profiles(n, input_profiles, params, year): .transpose("time", "bus") .to_pandas() ) - - # spatial clustering - weight = ds["weight"].sel(year=closest_year).to_pandas() - weight = weight.groupby(clustermaps).transform(normed_or_uniform) - p_max_pu = (p_max_pu * weight).T.groupby(clustermaps).sum().T p_max_pu.columns = p_max_pu.columns + f" {carrier}" # temporal_clustering @@ -222,7 +209,6 @@ def adjust_renewable_profiles(n, input_profiles, params, year): snakemake = mock_snakemake( "add_brownfield", - simpl="", clusters="37", opts="", ll="v1.0", diff --git a/scripts/add_electricity.py b/scripts/add_electricity.py index 6728ea214..82c78a28a 100755 --- a/scripts/add_electricity.py +++ b/scripts/add_electricity.py @@ -3,7 +3,8 @@ # # SPDX-License-Identifier: MIT """ -Adds electrical generators and existing hydro storage units to a base network. +Adds existing electrical generators, hydro-electric plants as well as +greenfield and battery and hydrogen storage to the clustered network. Relevant Settings ----------------- @@ -11,19 +12,11 @@ .. code:: yaml costs: - year: - version: - dicountrate: - emission_prices: + year: version: dicountrate: emission_prices: electricity: - max_hours: - marginal_cost: - capital_cost: - conventional_carriers: - co2limit: - extendable_carriers: - estimate_renewable_capacities: + max_hours: marginal_cost: capital_cost: conventional_carriers: co2limit: + extendable_carriers: estimate_renewable_capacities: load: @@ -31,13 +24,14 @@ renewable: hydro: - carriers: - hydro_max_hours: - hydro_capital_cost: + carriers: hydro_max_hours: hydro_capital_cost: lines: length_factor: + links: + length_factor: + .. seealso:: Documentation of the configuration file ``config/config.yaml`` at :ref:`costs_cf`, :ref:`electricity_cf`, :ref:`load_cf`, :ref:`renewable_cf`, :ref:`lines_cf` @@ -45,23 +39,31 @@ Inputs ------ -- ``resources/costs.csv``: The database of cost assumptions for all included technologies for specific years from various sources; e.g. discount rate, lifetime, investment (CAPEX), fixed operation and maintenance (FOM), variable operation and maintenance (VOM), fuel costs, efficiency, carbon-dioxide intensity. -- ``data/hydro_capacities.csv``: Hydropower plant store/discharge power capacities, energy storage capacity, and average hourly inflow by country. +- ``resources/costs.csv``: The database of cost assumptions for all included + technologies for specific years from various sources; e.g. discount rate, + lifetime, investment (CAPEX), fixed operation and maintenance (FOM), variable + operation and maintenance (VOM), fuel costs, efficiency, carbon-dioxide + intensity. +- ``data/hydro_capacities.csv``: Hydropower plant store/discharge power + capacities, energy storage capacity, and average hourly inflow by country. .. image:: img/hydrocapacities.png :scale: 34 % -- ``resources/electricity_demand.csv`` Hourly per-country electricity demand profiles. -- ``resources/regions_onshore.geojson``: confer :ref:`busregions` +- ``resources/electricity_demand_base_s.nc`` Hourly nodal electricity demand + profiles. +- ``resources/regions_onshore_base_s_{clusters}.geojson``: confer + :ref:`busregions` - ``resources/nuts3_shapes.geojson``: confer :ref:`shapes` -- ``resources/powerplants.csv``: confer :ref:`powerplants` -- ``resources/profile_{}.nc``: all technologies in ``config["renewables"].keys()``, confer :ref:`renewableprofiles`. -- ``networks/base.nc``: confer :ref:`base` +- ``resources/powerplants_s_{clusters}.csv``: confer :ref:`powerplants` +- ``resources/profile_{clusters}_{}.nc``: all technologies in + ``config["renewables"].keys()``, confer :ref:`renewableprofiles`. +- ``networks/base_s_{clusters}.nc`` Outputs ------- -- ``networks/elec.nc``: +- ``networks/base_s_{clusters}_elec.nc``: .. image:: img/elec.png :scale: 33 % @@ -69,29 +71,53 @@ Description ----------- -The rule :mod:`add_electricity` ties all the different data inputs from the preceding rules together into a detailed PyPSA network that is stored in ``networks/elec.nc``. It includes: +The rule :mod:`add_electricity` ties all the different data inputs from the +preceding rules together into a detailed PyPSA network that is stored in +``networks/base_s_{clusters}_elec.nc``. It includes: -- today's transmission topology and transfer capacities (optionally including lines which are under construction according to the config settings ``lines: under_construction`` and ``links: under_construction``), -- today's thermal and hydro power generation capacities (for the technologies listed in the config setting ``electricity: conventional_carriers``), and -- today's load time-series (upsampled in a top-down approach according to population and gross domestic product) +- today's transmission topology and transfer capacities (optionally including + lines which are under construction according to the config settings ``lines: + under_construction`` and ``links: under_construction``), +- today's thermal and hydro power generation capacities (for the technologies + listed in the config setting ``electricity: conventional_carriers``), and +- today's load time-series (upsampled in a top-down approach according to + population and gross domestic product) It further adds extendable ``generators`` with **zero** capacity for -- photovoltaic, onshore and AC- as well as DC-connected offshore wind installations with today's locational, hourly wind and solar capacity factors (but **no** current capacities), -- additional open- and combined-cycle gas turbines (if ``OCGT`` and/or ``CCGT`` is listed in the config setting ``electricity: extendable_carriers``) +- photovoltaic, onshore and AC- as well as DC-connected offshore wind + installations with today's locational, hourly wind and solar capacity factors + (but **no** current capacities), +- additional open- and combined-cycle gas turbines (if ``OCGT`` and/or ``CCGT`` + is listed in the config setting ``electricity: extendable_carriers``) + +Furthermore, it attaches additional extendable components to the clustered +network with **zero** initial capacity: + +- ``StorageUnits`` of carrier 'H2' and/or 'battery'. If this option is chosen, + every bus is given an extendable ``StorageUnit`` of the corresponding carrier. + The energy and power capacities are linked through a parameter that specifies + the energy capacity as maximum hours at full dispatch power and is configured + in ``electricity: max_hours:``. This linkage leads to one investment variable + per storage unit. The default ``max_hours`` lead to long-term hydrogen and + short-term battery storage units. + +- ``Stores`` of carrier 'H2' and/or 'battery' in combination with ``Links``. If + this option is chosen, the script adds extra buses with corresponding carrier + where energy ``Stores`` are attached and which are connected to the + corresponding power buses via two links, one each for charging and + discharging. This leads to three investment variables for the energy capacity, + charging and discharging capacity of the storage unit. """ import logging -from itertools import product from pathlib import Path from typing import Dict, List -import geopandas as gpd import numpy as np import pandas as pd import powerplantmatching as pm import pypsa -import scipy.sparse as sparse import xarray as xr from _helpers import ( configure_logging, @@ -100,7 +126,7 @@ update_p_nom_max, ) from powerplantmatching.export import map_country_bus -from shapely.prepared import prep +from pypsa.clustering.spatial import DEFAULT_ONE_PORT_STRATEGIES, normed_or_uniform idx = pd.IndexSlice @@ -263,7 +289,20 @@ def costs_for_storage(store, link1, link2=None, max_hours=1.0): return costs -def load_powerplants(ppl_fn): +def load_and_aggregate_powerplants( + ppl_fn: str, + costs: pd.DataFrame, + consider_efficiency_classes: bool = False, + aggregation_strategies: dict = None, + exclude_carriers: list = None, +) -> pd.DataFrame: + + if not aggregation_strategies: + aggregation_strategies = {} + + if not exclude_carriers: + exclude_carriers = [] + carrier_dict = { "ocgt": "OCGT", "ccgt": "CCGT", @@ -271,94 +310,120 @@ def load_powerplants(ppl_fn): "ccgt, thermal": "CCGT", "hard coal": "coal", } - return ( + tech_dict = { + "Run-Of-River": "ror", + "Reservoir": "hydro", + "Pumped Storage": "PHS", + } + ppl = ( pd.read_csv(ppl_fn, index_col=0, dtype={"bus": "str"}) .powerplant.to_pypsa_names() .rename(columns=str.lower) - .replace({"carrier": carrier_dict}) + .replace({"carrier": carrier_dict, "technology": tech_dict}) ) + # Replace carriers "natural gas" and "hydro" with the respective technology; + # OCGT or CCGT and hydro, PHS, or ror) + ppl["carrier"] = ppl.carrier.where( + ~ppl.carrier.isin(["hydro", "natural gas"]), ppl.technology + ) -def shapes_to_shapes(orig, dest): - """ - Adopted from vresutils.transfer.Shapes2Shapes() - """ - orig_prepped = list(map(prep, orig)) - transfer = sparse.lil_matrix((len(dest), len(orig)), dtype=float) - - for i, j in product(range(len(dest)), range(len(orig))): - if orig_prepped[j].intersects(dest.iloc[i]): - area = orig.iloc[j].intersection(dest.iloc[i]).area - transfer[i, j] = area / dest.iloc[i].area - - return transfer + cost_columns = [ + "VOM", + "FOM", + "efficiency", + "capital_cost", + "marginal_cost", + "fuel", + "lifetime", + ] + ppl = ppl.join(costs[cost_columns], on="carrier", rsuffix="_r") + ppl["efficiency"] = ppl.efficiency.combine_first(ppl.efficiency_r) + ppl["lifetime"] = (ppl.dateout - ppl.datein).fillna(np.inf) + ppl["build_year"] = ppl.datein.fillna(0).astype(int) + ppl["marginal_cost"] = ( + ppl.carrier.map(costs.VOM) + ppl.carrier.map(costs.fuel) / ppl.efficiency + ) -def attach_load( - n, regions, load, nuts3_shapes, gdp_pop_non_nuts3, countries, scaling=1.0 -): - substation_lv_i = n.buses.index[n.buses["substation_lv"]] - gdf_regions = gpd.read_file(regions).set_index("name").reindex(substation_lv_i) - opsd_load = pd.read_csv(load, index_col=0, parse_dates=True).filter(items=countries) + strategies = { + **DEFAULT_ONE_PORT_STRATEGIES, + **{"country": "first"}, + **aggregation_strategies.get("generators", {}), + } + strategies = {k: v for k, v in strategies.items() if k in ppl.columns} + + to_aggregate = ~ppl.carrier.isin(exclude_carriers) + df = ppl[to_aggregate].copy() + + if consider_efficiency_classes: + for c in df.carrier.unique(): + df_c = df.query("carrier == @c") + low = df_c.efficiency.quantile(0.10) + high = df_c.efficiency.quantile(0.90) + if low < high: + labels = ["low", "medium", "high"] + suffix = pd.cut( + df_c.efficiency, bins=[0, low, high, 1], labels=labels + ).astype(str) + df.update({"carrier": df_c.carrier + " " + suffix + " efficiency"}) + + grouper = ["bus", "carrier"] + weights = df.groupby(grouper).p_nom.transform(normed_or_uniform) + + for k, v in strategies.items(): + if v == "capacity_weighted_average": + df[k] = df[k] * weights + strategies[k] = pd.Series.sum + + aggregated = df.groupby(grouper, as_index=False).agg(strategies) + aggregated.index = aggregated.bus + " " + aggregated.carrier + aggregated.build_year = aggregated.build_year.astype(int) + + disaggregated = ppl[~to_aggregate][aggregated.columns].copy() + disaggregated.index = ( + disaggregated.bus + + " " + + disaggregated.carrier + + " " + + disaggregated.index.astype(str) + ) - logger.info(f"Load data scaled by factor {scaling}.") - opsd_load *= scaling + return pd.concat([aggregated, disaggregated]) - nuts3 = gpd.read_file(nuts3_shapes).set_index("index") - def upsample(cntry, group, gdp_pop_non_nuts3): - load = opsd_load[cntry] +def attach_load( + n: pypsa.Network, + load_fn: str, + busmap_fn: str, + scaling: float = 1.0, +) -> None: - if len(group) == 1: - return pd.DataFrame({group.index[0]: load}) - nuts3_cntry = nuts3.loc[nuts3.country == cntry] - transfer = shapes_to_shapes(group, nuts3_cntry.geometry).T.tocsr() - gdp_n = pd.Series( - transfer.dot(nuts3_cntry["gdp"].fillna(1.0).values), index=group.index - ) - pop_n = pd.Series( - transfer.dot(nuts3_cntry["pop"].fillna(1.0).values), index=group.index - ) + load = ( + xr.open_dataarray(load_fn).to_dataframe().squeeze(axis=1).unstack(level="time") + ) - # relative factors 0.6 and 0.4 have been determined from a linear - # regression on the country to continent load data - factors = normed(0.6 * normed(gdp_n) + 0.4 * normed(pop_n)) - if cntry in ["UA", "MD"]: - # overwrite factor because nuts3 provides no data for UA+MD - gdp_pop_non_nuts3 = gpd.read_file(gdp_pop_non_nuts3).set_index("Bus") - gdp_pop_non_nuts3 = gdp_pop_non_nuts3.loc[ - (gdp_pop_non_nuts3.country == cntry) - & (gdp_pop_non_nuts3.index.isin(substation_lv_i)) - ] - factors = normed( - 0.6 * normed(gdp_pop_non_nuts3["gdp"]) - + 0.4 * normed(gdp_pop_non_nuts3["pop"]) - ) - return pd.DataFrame( - factors.values * load.values[:, np.newaxis], - index=load.index, - columns=factors.index, - ) + # apply clustering busmap + busmap = pd.read_csv(busmap_fn, dtype=str).set_index("Bus").squeeze() + load = load.groupby(busmap).sum().T - load = pd.concat( - [ - upsample(cntry, group, gdp_pop_non_nuts3) - for cntry, group in gdf_regions.geometry.groupby(gdf_regions.country) - ], - axis=1, - ) + logger.info(f"Load data scaled by factor {scaling}.") + load *= scaling - n.madd( - "Load", substation_lv_i, bus=substation_lv_i, p_set=load - ) # carrier="electricity" + n.madd("Load", load.columns, bus=load.columns, p_set=load) # carrier="electricity" -def update_transmission_costs(n, costs, length_factor=1.0): - # TODO: line length factor of lines is applied to lines and links. - # Separate the function to distinguish. +def set_transmission_costs( + n: pypsa.Network, + costs: pd.DataFrame, + line_length_factor: float = 1.0, + link_length_factor: float = 1.0, +) -> None: n.lines["capital_cost"] = ( - n.lines["length"] * length_factor * costs.at["HVAC overhead", "capital_cost"] + n.lines["length"] + * line_length_factor + * costs.at["HVAC overhead", "capital_cost"] ) if n.links.empty: @@ -373,7 +438,7 @@ def update_transmission_costs(n, costs, length_factor=1.0): costs = ( n.links.loc[dc_b, "length"] - * length_factor + * link_length_factor * ( (1.0 - n.links.loc[dc_b, "underwater_fraction"]) * costs.at["HVDC overhead", "capital_cost"] @@ -386,13 +451,25 @@ def update_transmission_costs(n, costs, length_factor=1.0): def attach_wind_and_solar( - n, costs, input_profiles, carriers, extendable_carriers, line_length_factor=1 -): + n: pypsa.Network, + costs: pd.DataFrame, + input_profiles: str, + carriers: list | set, + extendable_carriers: list | set, + line_length_factor: float = 1.0, + landfall_lengths: dict = None, +) -> None: add_missing_carriers(n, carriers) + + if landfall_lengths is None: + landfall_lengths = {} + for car in carriers: if car == "hydro": continue + landfall_length = landfall_lengths.get(car, 0.0) + with xr.open_dataset(getattr(input_profiles, "profile_" + car)) as ds: if ds.indexes["bus"].empty: continue @@ -403,17 +480,15 @@ def attach_wind_and_solar( supcar = car.split("-", 2)[0] if supcar == "offwind": - underwater_fraction = ds["underwater_fraction"].to_pandas() - connection_cost = ( - line_length_factor - * ds["average_distance"].to_pandas() - * ( - underwater_fraction - * costs.at[car + "-connection-submarine", "capital_cost"] - + (1.0 - underwater_fraction) - * costs.at[car + "-connection-underground", "capital_cost"] - ) + distance = ds["average_distance"].to_pandas() + submarine_cost = costs.at[car + "-connection-submarine", "capital_cost"] + underground_cost = costs.at[ + car + "-connection-underground", "capital_cost" + ] + connection_cost = line_length_factor * ( + distance * submarine_cost + landfall_length * underground_cost ) + capital_cost = ( costs.at["offwind", "capital_cost"] + costs.at[car + "-station", "capital_cost"] @@ -435,7 +510,6 @@ def attach_wind_and_solar( carrier=car, p_nom_extendable=car in extendable_carriers["Generator"], p_nom_max=ds["p_nom_max"].to_pandas(), - weight=ds["weight"].to_pandas(), marginal_cost=costs.at[supcar, "marginal_cost"], capital_cost=capital_cost, efficiency=costs.at[supcar, "efficiency"], @@ -457,19 +531,7 @@ def attach_conventional_generators( ): carriers = list(set(conventional_carriers) | set(extendable_carriers["Generator"])) - # Replace carrier "natural gas" with the respective technology (OCGT or - # CCGT) to align with PyPSA names of "carriers" and avoid filtering "natural - # gas" powerplants in ppl.query("carrier in @carriers") - ppl.loc[ppl["carrier"] == "natural gas", "carrier"] = ppl.loc[ - ppl["carrier"] == "natural gas", "technology" - ] - - ppl = ( - ppl.query("carrier in @carriers") - .join(costs, on="carrier", rsuffix="_r") - .rename(index=lambda s: f"C{str(s)}") - ) - ppl["efficiency"] = ppl.efficiency.fillna(ppl.efficiency_r) + ppl = ppl.query("carrier in @carriers") # reduce carriers to those in power plant dataset carriers = list(set(carriers) & set(ppl.carrier.unique())) @@ -496,13 +558,11 @@ def attach_conventional_generators( fuel_price.columns = ppl.index marginal_cost = fuel_price.div(ppl.efficiency).add(ppl.carrier.map(costs.VOM)) else: - marginal_cost = ( - ppl.carrier.map(costs.VOM) + ppl.carrier.map(costs.fuel) / ppl.efficiency - ) + marginal_cost = ppl.marginal_cost # Define generators using modified ppl DataFrame caps = ppl.groupby("carrier").p_nom.sum().div(1e3).round(2) - logger.info(f"Adding {len(ppl)} generators with capacities [GW] \n{caps}") + logger.info(f"Adding {len(ppl)} generators with capacities [GW]pp \n{caps}") n.madd( "Generator", @@ -515,8 +575,8 @@ def attach_conventional_generators( efficiency=ppl.efficiency, marginal_cost=marginal_cost, capital_cost=ppl.capital_cost, - build_year=ppl.datein.fillna(0).astype(int), - lifetime=(ppl.dateout - ppl.datein).fillna(np.inf), + build_year=ppl.build_year, + lifetime=ppl.lifetime, **committable_attrs, ) @@ -546,14 +606,9 @@ def attach_hydro(n, costs, ppl, profile_hydro, hydro_capacities, carriers, **par add_missing_carriers(n, carriers) add_co2_emissions(n, costs, carriers) - ppl = ( - ppl.query('carrier == "hydro"') - .reset_index(drop=True) - .rename(index=lambda s: f"{str(s)} hydro") - ) - ror = ppl.query('technology == "Run-Of-River"') - phs = ppl.query('technology == "Pumped Storage"') - hydro = ppl.query('technology == "Reservoir"') + ror = ppl.query('carrier == "ror"') + phs = ppl.query('carrier == "PHS"') + hydro = ppl.query('carrier == "hydro"') country = ppl["bus"].map(n.buses.country).rename("country") @@ -618,7 +673,7 @@ def attach_hydro(n, costs, ppl, profile_hydro, hydro_capacities, carriers, **par if "hydro" in carriers and not hydro.empty: hydro_max_hours = params.get("hydro_max_hours") - assert hydro_max_hours is not None, "No path for hydro capacities given." + assert hydro_capacities is not None, "No path for hydro capacities given." hydro_stats = pd.read_csv( hydro_capacities, comment="#", na_values="-", index_col=0 @@ -626,7 +681,13 @@ def attach_hydro(n, costs, ppl, profile_hydro, hydro_capacities, carriers, **par e_target = hydro_stats["E_store[TWh]"].clip(lower=0.2) * 1e6 e_installed = hydro.eval("p_nom * max_hours").groupby(hydro.country).sum() e_missing = e_target - e_installed - missing_mh_i = hydro.query("max_hours.isnull()").index + missing_mh_i = hydro.query("max_hours.isnull() or max_hours == 0").index + # some countries may have missing storage capacity but only one plant + # which needs to be scaled to the target storage capacity + missing_mh_single_i = hydro.index[ + ~hydro.country.duplicated() & hydro.country.isin(e_missing.dropna().index) + ] + missing_mh_i = missing_mh_i.union(missing_mh_single_i) if hydro_max_hours == "energy_capacity_totals_by_country": # watch out some p_nom values like IE's are totally underrepresented @@ -649,7 +710,8 @@ def attach_hydro(n, costs, ppl, profile_hydro, hydro_capacities, carriers, **par f'Assuming max_hours=6 for hydro reservoirs in the countries: {", ".join(missing_countries)}' ) hydro_max_hours = hydro.max_hours.where( - hydro.max_hours > 0, hydro.country.map(max_hours_country) + (hydro.max_hours > 0) & ~hydro.index.isin(missing_mh_single_i), + hydro.country.map(max_hours_country), ).fillna(6) if params.get("flatten_dispatch", False): @@ -775,64 +837,144 @@ def estimate_renewable_capacities( ) -def attach_line_rating( - n, rating, s_max_pu, correction_factor, max_voltage_difference, max_line_rating -): - # TODO: Only considers overhead lines - n.lines_t.s_max_pu = (rating / n.lines.s_nom[rating.columns]) * correction_factor - if max_voltage_difference: - x_pu = ( - n.lines.type.map(n.line_types["x_per_length"]) - * n.lines.length - / (n.lines.v_nom**2) +def attach_storageunits(n, costs, extendable_carriers, max_hours): + carriers = extendable_carriers["StorageUnit"] + + n.madd("Carrier", carriers) + + buses_i = n.buses.index + + lookup_store = {"H2": "electrolysis", "battery": "battery inverter"} + lookup_dispatch = {"H2": "fuel cell", "battery": "battery inverter"} + + for carrier in carriers: + roundtrip_correction = 0.5 if carrier == "battery" else 1 + + n.madd( + "StorageUnit", + buses_i, + " " + carrier, + bus=buses_i, + carrier=carrier, + p_nom_extendable=True, + capital_cost=costs.at[carrier, "capital_cost"], + marginal_cost=costs.at[carrier, "marginal_cost"], + efficiency_store=costs.at[lookup_store[carrier], "efficiency"] + ** roundtrip_correction, + efficiency_dispatch=costs.at[lookup_dispatch[carrier], "efficiency"] + ** roundtrip_correction, + max_hours=max_hours[carrier], + cyclic_state_of_charge=True, + ) + + +def attach_stores(n, costs, extendable_carriers): + carriers = extendable_carriers["Store"] + + n.madd("Carrier", carriers) + + buses_i = n.buses.index + + if "H2" in carriers: + h2_buses_i = n.madd("Bus", buses_i + " H2", carrier="H2", location=buses_i) + + n.madd( + "Store", + h2_buses_i, + bus=h2_buses_i, + carrier="H2", + e_nom_extendable=True, + e_cyclic=True, + capital_cost=costs.at["hydrogen storage underground", "capital_cost"], ) - # need to clip here as cap values might be below 1 - # -> would mean the line cannot be operated at actual given pessimistic ampacity - s_max_pu_cap = ( - np.deg2rad(max_voltage_difference) / (x_pu * n.lines.s_nom) - ).clip(lower=1) - n.lines_t.s_max_pu = n.lines_t.s_max_pu.clip( - lower=1, upper=s_max_pu_cap, axis=1 + + n.madd( + "Link", + h2_buses_i + " Electrolysis", + bus0=buses_i, + bus1=h2_buses_i, + carrier="H2 electrolysis", + p_nom_extendable=True, + efficiency=costs.at["electrolysis", "efficiency"], + capital_cost=costs.at["electrolysis", "capital_cost"], + marginal_cost=costs.at["electrolysis", "marginal_cost"], ) - if max_line_rating: - n.lines_t.s_max_pu = n.lines_t.s_max_pu.clip(upper=max_line_rating) - n.lines_t.s_max_pu *= s_max_pu + n.madd( + "Link", + h2_buses_i + " Fuel Cell", + bus0=h2_buses_i, + bus1=buses_i, + carrier="H2 fuel cell", + p_nom_extendable=True, + efficiency=costs.at["fuel cell", "efficiency"], + # NB: fixed cost is per MWel + capital_cost=costs.at["fuel cell", "capital_cost"] + * costs.at["fuel cell", "efficiency"], + marginal_cost=costs.at["fuel cell", "marginal_cost"], + ) -def add_transmission_projects(n, transmission_projects): - logger.info(f"Adding transmission projects to network.") - for path in transmission_projects: - path = Path(path) - df = pd.read_csv(path, index_col=0, dtype={"bus0": str, "bus1": str}) - if df.empty: - continue - if "new_buses" in path.name: - n.madd("Bus", df.index, **df) - elif "new_lines" in path.name: - n.madd("Line", df.index, **df) - elif "new_links" in path.name: - n.madd("Link", df.index, **df) - elif "adjust_lines": - n.lines.update(df) - elif "adjust_links": - n.links.update(df) + if "battery" in carriers: + b_buses_i = n.madd( + "Bus", buses_i + " battery", carrier="battery", location=buses_i + ) + + n.madd( + "Store", + b_buses_i, + bus=b_buses_i, + carrier="battery", + e_cyclic=True, + e_nom_extendable=True, + capital_cost=costs.at["battery storage", "capital_cost"], + marginal_cost=costs.at["battery", "marginal_cost"], + ) + + n.madd("Carrier", ["battery charger", "battery discharger"]) + + n.madd( + "Link", + b_buses_i + " charger", + bus0=buses_i, + bus1=b_buses_i, + carrier="battery charger", + # the efficiencies are "round trip efficiencies" + efficiency=costs.at["battery inverter", "efficiency"] ** 0.5, + capital_cost=costs.at["battery inverter", "capital_cost"], + p_nom_extendable=True, + marginal_cost=costs.at["battery inverter", "marginal_cost"], + ) + + n.madd( + "Link", + b_buses_i + " discharger", + bus0=b_buses_i, + bus1=buses_i, + carrier="battery discharger", + efficiency=costs.at["battery inverter", "efficiency"] ** 0.5, + p_nom_extendable=True, + marginal_cost=costs.at["battery inverter", "marginal_cost"], + ) if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake - snakemake = mock_snakemake("add_electricity") + snakemake = mock_snakemake("add_electricity", clusters=100) configure_logging(snakemake) set_scenario_config(snakemake) params = snakemake.params + max_hours = params.electricity["max_hours"] + landfall_lengths = { + tech: settings["landfall_length"] + for tech, settings in params.renewable.items() + if "landfall_length" in settings.keys() + } n = pypsa.Network(snakemake.input.base_network) - if params["transmission_projects"]["enable"]: - add_transmission_projects(n, snakemake.input.transmission_projects) - time = get_snapshots(snakemake.params.snapshots, snakemake.params.drop_leap_day) n.set_snapshots(time) @@ -841,22 +983,31 @@ def add_transmission_projects(n, transmission_projects): costs = load_costs( snakemake.input.tech_costs, params.costs, - params.electricity["max_hours"], + max_hours, Nyears, ) - ppl = load_powerplants(snakemake.input.powerplants) + + ppl = load_and_aggregate_powerplants( + snakemake.input.powerplants, + costs, + params.consider_efficiency_classes, + params.aggregation_strategies, + params.exclude_carriers, + ) attach_load( n, - snakemake.input.regions, snakemake.input.load, - snakemake.input.nuts3_shapes, - snakemake.input.get("gdp_pop_non_nuts3"), - params.countries, + snakemake.input.busmap, params.scaling_factor, ) - update_transmission_costs(n, costs, params.length_factor) + set_transmission_costs( + n, + costs, + params.line_length_factor, + params.link_length_factor, + ) renewable_carriers = set(params.electricity["renewable_carriers"]) extendable_carriers = params.electricity["extendable_carriers"] @@ -896,7 +1047,8 @@ def add_transmission_projects(n, transmission_projects): snakemake.input, renewable_carriers, extendable_carriers, - params.length_factor, + params.line_length_factor, + landfall_lengths, ) if "hydro" in renewable_carriers: @@ -933,24 +1085,12 @@ def add_transmission_projects(n, transmission_projects): update_p_nom_max(n) - line_rating_config = snakemake.config["lines"]["dynamic_line_rating"] - if line_rating_config["activate"]: - rating = xr.open_dataarray(snakemake.input.line_rating).to_pandas().transpose() - s_max_pu = snakemake.config["lines"]["s_max_pu"] - correction_factor = line_rating_config["correction_factor"] - max_voltage_difference = line_rating_config["max_voltage_difference"] - max_line_rating = line_rating_config["max_line_rating"] - - attach_line_rating( - n, - rating, - s_max_pu, - correction_factor, - max_voltage_difference, - max_line_rating, - ) + attach_storageunits(n, costs, extendable_carriers, max_hours) + attach_stores(n, costs, extendable_carriers) sanitize_carriers(n, snakemake.config) + if "location" in n.buses: + sanitize_locations(n) - n.meta = snakemake.config + 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 7eff0e2e0..2cd160907 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -120,7 +120,7 @@ def add_existing_renewables(df_agg, costs): df_agg.at[name, "DateOut"] = ( year + costs.at[cost_key, "lifetime"] - 1 ) - df_agg.at[name, "cluster_bus"] = node + df_agg.at[name, "bus"] = node def add_power_capacities_installed_before_baseyear(n, grouping_years, costs, baseyear): @@ -135,7 +135,8 @@ def add_power_capacities_installed_before_baseyear(n, grouping_years, costs, bas baseyear : int """ logger.debug( - f"Adding power capacities installed before {baseyear} from powerplants.csv" + f"Adding power capacities installed before {baseyear} from" + " powerplants_s_{clusters}.csv" ) df_agg = pd.read_csv(snakemake.input.powerplants, index_col=0) @@ -184,19 +185,6 @@ def add_power_capacities_installed_before_baseyear(n, grouping_years, costs, bas ) df_agg.loc[biomass_i, "DateOut"] = df_agg.loc[biomass_i, "DateOut"].fillna(dateout) - # assign clustered bus - busmap_s = pd.read_csv(snakemake.input.busmap_s, index_col=0).squeeze() - busmap = pd.read_csv(snakemake.input.busmap, index_col=0).squeeze() - - inv_busmap = {} - for k, v in busmap.items(): - inv_busmap[v] = inv_busmap.get(v, []) + [k] - - clustermaps = busmap_s.map(busmap) - clustermaps.index = clustermaps.index.astype(int) - - df_agg["cluster_bus"] = df_agg.bus.map(clustermaps) - # include renewables in df_agg add_existing_renewables(df_agg, costs) @@ -225,14 +213,14 @@ def add_power_capacities_installed_before_baseyear(n, grouping_years, costs, bas df = df_agg.pivot_table( index=["grouping_year", "Fueltype"], - columns="cluster_bus", + columns="bus", values="Capacity", aggfunc="sum", ) lifetime = df_agg.pivot_table( index=["grouping_year", "Fueltype"], - columns="cluster_bus", + columns="bus", values="lifetime", aggfunc="mean", # currently taken mean for clustering lifetimes ) @@ -280,54 +268,23 @@ def add_power_capacities_installed_before_baseyear(n, grouping_years, costs, bas ] = capacity.loc[already_build.str.replace(name_suffix, "")].values new_capacity = capacity.loc[new_build.str.replace(name_suffix, "")] - if "m" in snakemake.wildcards.clusters: - for ind in new_capacity.index: - # existing capacities are split evenly among regions in every country - inv_ind = list(inv_busmap[ind]) - - # for offshore the splitting only includes coastal regions - inv_ind = [ - i for i in inv_ind if (i + name_suffix_by) in n.generators.index - ] - - p_max_pu = n.generators_t.p_max_pu[ - [i + name_suffix_by for i in inv_ind] - ] - p_max_pu.columns = [i + name_suffix for i in inv_ind] - - n.madd( - "Generator", - [i + name_suffix for i in inv_ind], - bus=ind, - carrier=generator, - p_nom=new_capacity[ind] - / len(inv_ind), # split among regions in a country - marginal_cost=marginal_cost, - capital_cost=capital_cost, - efficiency=costs.at[cost_key, "efficiency"], - p_max_pu=p_max_pu, - build_year=grouping_year, - lifetime=costs.at[cost_key, "lifetime"], - ) - - else: - p_max_pu = n.generators_t.p_max_pu[capacity.index + name_suffix_by] + p_max_pu = n.generators_t.p_max_pu[capacity.index + name_suffix_by] - if not new_build.empty: - n.madd( - "Generator", - new_capacity.index, - suffix=name_suffix, - bus=new_capacity.index, - carrier=generator, - p_nom=new_capacity, - marginal_cost=marginal_cost, - capital_cost=capital_cost, - efficiency=costs.at[cost_key, "efficiency"], - p_max_pu=p_max_pu.rename(columns=n.generators.bus), - build_year=grouping_year, - lifetime=costs.at[cost_key, "lifetime"], - ) + if not new_build.empty: + n.madd( + "Generator", + new_capacity.index, + suffix=name_suffix, + bus=new_capacity.index, + carrier=generator, + p_nom=new_capacity, + marginal_cost=marginal_cost, + capital_cost=capital_cost, + efficiency=costs.at[cost_key, "efficiency"], + p_max_pu=p_max_pu.rename(columns=n.generators.bus), + build_year=grouping_year, + lifetime=costs.at[cost_key, "lifetime"], + ) else: bus0 = vars(spatial)[carrier[generator]].nodes @@ -690,7 +647,6 @@ def set_defaults(n): snakemake = mock_snakemake( "add_existing_baseyear", configfiles="config/test/config.myopic.yaml", - simpl="", clusters="5", ll="v1.5", opts="", diff --git a/scripts/add_extra_components.py b/scripts/add_extra_components.py deleted file mode 100644 index 90e7eaec9..000000000 --- a/scripts/add_extra_components.py +++ /dev/null @@ -1,253 +0,0 @@ -# -*- coding: utf-8 -*- -# SPDX-FileCopyrightText: : 2017-2024 The PyPSA-Eur Authors -# -# SPDX-License-Identifier: MIT - -# coding: utf-8 -""" -Adds extra extendable components to the clustered and simplified network. - -Relevant Settings ------------------ - -.. code:: yaml - - costs: - year: - version: - dicountrate: - emission_prices: - - electricity: - max_hours: - marginal_cost: - capital_cost: - extendable_carriers: - StorageUnit: - Store: - -.. seealso:: - Documentation of the configuration file ``config/config.yaml`` at :ref:`costs_cf`, - :ref:`electricity_cf` - -Inputs ------- - -- ``resources/costs.csv``: The database of cost assumptions for all included technologies for specific years from various sources; e.g. discount rate, lifetime, investment (CAPEX), fixed operation and maintenance (FOM), variable operation and maintenance (VOM), fuel costs, efficiency, carbon-dioxide intensity. - -Outputs -------- - -- ``networks/elec_s{simpl}_{clusters}_ec.nc``: - - -Description ------------ - -The rule :mod:`add_extra_components` attaches additional extendable components to the clustered and simplified network. These can be configured in the ``config/config.yaml`` at ``electricity: extendable_carriers:``. It processes ``networks/elec_s{simpl}_{clusters}.nc`` to build ``networks/elec_s{simpl}_{clusters}_ec.nc``, which in contrast to the former (depending on the configuration) contain with **zero** initial capacity - -- ``StorageUnits`` of carrier 'H2' and/or 'battery'. If this option is chosen, every bus is given an extendable ``StorageUnit`` of the corresponding carrier. The energy and power capacities are linked through a parameter that specifies the energy capacity as maximum hours at full dispatch power and is configured in ``electricity: max_hours:``. This linkage leads to one investment variable per storage unit. The default ``max_hours`` lead to long-term hydrogen and short-term battery storage units. - -- ``Stores`` of carrier 'H2' and/or 'battery' in combination with ``Links``. If this option is chosen, the script adds extra buses with corresponding carrier where energy ``Stores`` are attached and which are connected to the corresponding power buses via two links, one each for charging and discharging. This leads to three investment variables for the energy capacity, charging and discharging capacity of the storage unit. -""" -import logging - -import numpy as np -import pandas as pd -import pypsa -from _helpers import configure_logging, set_scenario_config -from add_electricity import load_costs, sanitize_carriers, sanitize_locations - -idx = pd.IndexSlice - -logger = logging.getLogger(__name__) - - -def attach_storageunits(n, costs, extendable_carriers, max_hours): - carriers = extendable_carriers["StorageUnit"] - - n.madd("Carrier", carriers) - - buses_i = n.buses.index - - lookup_store = {"H2": "electrolysis", "battery": "battery inverter"} - lookup_dispatch = {"H2": "fuel cell", "battery": "battery inverter"} - - for carrier in carriers: - roundtrip_correction = 0.5 if carrier == "battery" else 1 - - n.madd( - "StorageUnit", - buses_i, - " " + carrier, - bus=buses_i, - carrier=carrier, - p_nom_extendable=True, - capital_cost=costs.at[carrier, "capital_cost"], - marginal_cost=costs.at[carrier, "marginal_cost"], - efficiency_store=costs.at[lookup_store[carrier], "efficiency"] - ** roundtrip_correction, - efficiency_dispatch=costs.at[lookup_dispatch[carrier], "efficiency"] - ** roundtrip_correction, - max_hours=max_hours[carrier], - cyclic_state_of_charge=True, - ) - - -def attach_stores(n, costs, extendable_carriers): - carriers = extendable_carriers["Store"] - - n.madd("Carrier", carriers) - - buses_i = n.buses.index - - if "H2" in carriers: - h2_buses_i = n.madd("Bus", buses_i + " H2", carrier="H2", location=buses_i) - - n.madd( - "Store", - h2_buses_i, - bus=h2_buses_i, - carrier="H2", - e_nom_extendable=True, - e_cyclic=True, - capital_cost=costs.at["hydrogen storage underground", "capital_cost"], - ) - - n.madd( - "Link", - h2_buses_i + " Electrolysis", - bus0=buses_i, - bus1=h2_buses_i, - carrier="H2 electrolysis", - p_nom_extendable=True, - efficiency=costs.at["electrolysis", "efficiency"], - capital_cost=costs.at["electrolysis", "capital_cost"], - marginal_cost=costs.at["electrolysis", "marginal_cost"], - ) - - n.madd( - "Link", - h2_buses_i + " Fuel Cell", - bus0=h2_buses_i, - bus1=buses_i, - carrier="H2 fuel cell", - p_nom_extendable=True, - efficiency=costs.at["fuel cell", "efficiency"], - # NB: fixed cost is per MWel - capital_cost=costs.at["fuel cell", "capital_cost"] - * costs.at["fuel cell", "efficiency"], - marginal_cost=costs.at["fuel cell", "marginal_cost"], - ) - - if "battery" in carriers: - b_buses_i = n.madd( - "Bus", buses_i + " battery", carrier="battery", location=buses_i - ) - - n.madd( - "Store", - b_buses_i, - bus=b_buses_i, - carrier="battery", - e_cyclic=True, - e_nom_extendable=True, - capital_cost=costs.at["battery storage", "capital_cost"], - marginal_cost=costs.at["battery", "marginal_cost"], - ) - - n.madd("Carrier", ["battery charger", "battery discharger"]) - - n.madd( - "Link", - b_buses_i + " charger", - bus0=buses_i, - bus1=b_buses_i, - carrier="battery charger", - # the efficiencies are "round trip efficiencies" - efficiency=costs.at["battery inverter", "efficiency"] ** 0.5, - capital_cost=costs.at["battery inverter", "capital_cost"], - p_nom_extendable=True, - marginal_cost=costs.at["battery inverter", "marginal_cost"], - ) - - n.madd( - "Link", - b_buses_i + " discharger", - bus0=b_buses_i, - bus1=buses_i, - carrier="battery discharger", - efficiency=costs.at["battery inverter", "efficiency"] ** 0.5, - p_nom_extendable=True, - marginal_cost=costs.at["battery inverter", "marginal_cost"], - ) - - -def attach_hydrogen_pipelines(n, costs, extendable_carriers): - as_stores = extendable_carriers.get("Store", []) - - if "H2 pipeline" not in extendable_carriers.get("Link", []): - return - - assert "H2" in as_stores, ( - "Attaching hydrogen pipelines requires hydrogen " - "storage to be modelled as Store-Link-Bus combination. See " - "`config.yaml` at `electricity: extendable_carriers: Store:`." - ) - - # determine bus pairs - attrs = ["bus0", "bus1", "length"] - candidates = pd.concat( - [n.lines[attrs], n.links.query('carrier=="DC"')[attrs]] - ).reset_index(drop=True) - - # remove bus pair duplicates regardless of order of bus0 and bus1 - h2_links = candidates[ - ~pd.DataFrame(np.sort(candidates[["bus0", "bus1"]])).duplicated() - ] - h2_links.index = h2_links.apply(lambda c: f"H2 pipeline {c.bus0}-{c.bus1}", axis=1) - - # add pipelines - n.add("Carrier", "H2 pipeline") - - n.madd( - "Link", - h2_links.index, - bus0=h2_links.bus0.values + " H2", - bus1=h2_links.bus1.values + " H2", - p_min_pu=-1, - p_nom_extendable=True, - length=h2_links.length.values, - capital_cost=costs.at["H2 pipeline", "capital_cost"] * h2_links.length, - efficiency=costs.at["H2 pipeline", "efficiency"], - carrier="H2 pipeline", - ) - - -if __name__ == "__main__": - if "snakemake" not in globals(): - from _helpers import mock_snakemake - - snakemake = mock_snakemake("add_extra_components", simpl="", clusters=5) - configure_logging(snakemake) - set_scenario_config(snakemake) - - n = pypsa.Network(snakemake.input.network) - extendable_carriers = snakemake.params.extendable_carriers - max_hours = snakemake.params.max_hours - - Nyears = n.snapshot_weightings.objective.sum() / 8760.0 - costs = load_costs( - snakemake.input.tech_costs, snakemake.params.costs, max_hours, Nyears - ) - - attach_storageunits(n, costs, extendable_carriers, max_hours) - attach_stores(n, costs, extendable_carriers) - attach_hydrogen_pipelines(n, costs, extendable_carriers) - - sanitize_carriers(n, snakemake.config) - if "location" in n.buses: - sanitize_locations(n) - - n.meta = dict(snakemake.config, **dict(wildcards=dict(snakemake.wildcards))) - n.export_to_netcdf(snakemake.output[0]) diff --git a/scripts/add_transmission_projects_and_dlr.py b/scripts/add_transmission_projects_and_dlr.py new file mode 100644 index 000000000..0dad16bbe --- /dev/null +++ b/scripts/add_transmission_projects_and_dlr.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2017-2024 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT +""" +Add transmission projects and DLR to the network. +""" + +import logging +from pathlib import Path + +import numpy as np +import pandas as pd +import pypsa +import xarray as xr +from _helpers import configure_logging, set_scenario_config + +logger = logging.getLogger(__name__) + + +def attach_transmission_projects( + n: pypsa.Network, transmission_projects: list[str] +) -> None: + logger.info("Adding transmission projects to network.") + for path in transmission_projects: + path = Path(path) + df = pd.read_csv(path, index_col=0, dtype={"bus0": str, "bus1": str}) + if df.empty: + continue + if "new_buses" in path.name: + n.madd("Bus", df.index, **df) + elif "new_lines" in path.name: + n.madd("Line", df.index, **df) + elif "new_links" in path.name: + n.madd("Link", df.index, **df) + elif "adjust_lines" in path.name: + n.lines.update(df) + elif "adjust_links" in path.name: + n.links.update(df) + + +def attach_line_rating( + n: pypsa.Network, + rating: pd.DataFrame, + s_max_pu: float, + correction_factor: float, + max_voltage_difference: float | bool, + max_line_rating: float | bool, +) -> None: + logger.info("Attaching dynamic line rating to network.") + # TODO: Only considers overhead lines + n.lines_t.s_max_pu = (rating / n.lines.s_nom[rating.columns]) * correction_factor + if max_voltage_difference: + x_pu = ( + n.lines.type.map(n.line_types["x_per_length"]) + * n.lines.length + / (n.lines.v_nom**2) + ) + # need to clip here as cap values might be below 1 + # -> would mean the line cannot be operated at actual given pessimistic ampacity + s_max_pu_cap = ( + np.deg2rad(max_voltage_difference) / (x_pu * n.lines.s_nom) + ).clip(lower=1) + n.lines_t.s_max_pu = n.lines_t.s_max_pu.clip( + lower=1, upper=s_max_pu_cap, axis=1 + ) + if max_line_rating: + n.lines_t.s_max_pu = n.lines_t.s_max_pu.clip(upper=max_line_rating) + n.lines_t.s_max_pu *= s_max_pu + + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake("add_transmission_projects_and_dlr") + configure_logging(snakemake) + set_scenario_config(snakemake) + + params = snakemake.params + + n = pypsa.Network(snakemake.input.network) + + if params["transmission_projects"]["enable"]: + + attach_transmission_projects(n, snakemake.input.transmission_projects) + + if params["dlr"]["activate"]: + + rating = xr.open_dataarray(snakemake.input.dlr).to_pandas().transpose() + + s_max_pu = params["s_max_pu"] + correction_factor = params["dlr"]["correction_factor"] + max_voltage_difference = params["dlr"]["max_voltage_difference"] + max_line_rating = params["dlr"]["max_line_rating"] + + attach_line_rating( + n, + rating, + s_max_pu, + correction_factor, + max_voltage_difference, + max_line_rating, + ) + + n.export_to_netcdf(snakemake.output[0]) diff --git a/scripts/build_biomass_potentials.py b/scripts/build_biomass_potentials.py index bb56ebedf..32402eb25 100755 --- a/scripts/build_biomass_potentials.py +++ b/scripts/build_biomass_potentials.py @@ -344,7 +344,6 @@ def add_unsustainable_potentials(df): snakemake = mock_snakemake( "build_biomass_potentials", - simpl="", clusters="39", planning_horizons=2050, ) diff --git a/scripts/build_central_heating_temperature_profiles/run.py b/scripts/build_central_heating_temperature_profiles/run.py index feb4ab4f5..7a43556be 100644 --- a/scripts/build_central_heating_temperature_profiles/run.py +++ b/scripts/build_central_heating_temperature_profiles/run.py @@ -136,7 +136,6 @@ def map_temperature_dict_to_onshore_regions( snakemake = mock_snakemake( "build_cop_profiles", - simpl="", clusters=48, ) diff --git a/scripts/build_clustered_population_layouts.py b/scripts/build_clustered_population_layouts.py index 2d9c6acbe..013059529 100644 --- a/scripts/build_clustered_population_layouts.py +++ b/scripts/build_clustered_population_layouts.py @@ -17,11 +17,7 @@ if "snakemake" not in globals(): from _helpers import mock_snakemake - snakemake = mock_snakemake( - "build_clustered_population_layouts", - simpl="", - clusters=48, - ) + snakemake = mock_snakemake("build_clustered_population_layouts", clusters=48) set_scenario_config(snakemake) diff --git a/scripts/build_cop_profiles/run.py b/scripts/build_cop_profiles/run.py index d1faf1b12..b93f7df69 100644 --- a/scripts/build_cop_profiles/run.py +++ b/scripts/build_cop_profiles/run.py @@ -104,7 +104,6 @@ def get_country_from_node_name(node_name: str) -> str: snakemake = mock_snakemake( "build_cop_profiles", - simpl="", clusters=48, ) diff --git a/scripts/build_daily_heat_demand.py b/scripts/build_daily_heat_demand.py index 97d1368e2..6bcb6bd8a 100644 --- a/scripts/build_daily_heat_demand.py +++ b/scripts/build_daily_heat_demand.py @@ -27,13 +27,13 @@ ------ - ``resources//pop_layout_.nc``: Population layout (spatial population distribution). -- ``resources//regions_onshore_elec_s_.geojson``: Onshore region shapes. +- ``resources//regions_onshore_base_s_.geojson``: Onshore region shapes. - ``cutout``: Weather data cutout, as specified in config Outputs ------- -- ``resources/daily_heat_demand__elec_s_.nc``: +- ``resources/daily_heat_demand__base_s_.nc``: Relevant settings ----------------- @@ -58,7 +58,6 @@ snakemake = mock_snakemake( "build_daily_heat_demands", scope="total", - simpl="", clusters=48, ) set_scenario_config(snakemake) diff --git a/scripts/build_district_heat_share.py b/scripts/build_district_heat_share.py index 7e8497d69..6d0bdc324 100644 --- a/scripts/build_district_heat_share.py +++ b/scripts/build_district_heat_share.py @@ -44,7 +44,6 @@ snakemake = mock_snakemake( "build_district_heat_share", - simpl="", clusters=60, planning_horizons="2050", ) diff --git a/scripts/build_egs_potentials.py b/scripts/build_egs_potentials.py index 65ebab3ae..d80238695 100644 --- a/scripts/build_egs_potentials.py +++ b/scripts/build_egs_potentials.py @@ -201,7 +201,6 @@ def get_capacity_factors(network_regions_file, air_temperatures_file): snakemake = mock_snakemake( "build_egs_potentials", - simpl="", clusters=37, ) diff --git a/scripts/build_electricity_demand_base.py b/scripts/build_electricity_demand_base.py new file mode 100644 index 000000000..f48cc1a48 --- /dev/null +++ b/scripts/build_electricity_demand_base.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2017-2024 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT +""" +Builds the electricity demand for base regions based on population and GDP. +""" + +import logging +from itertools import product + +import geopandas as gpd +import numpy as np +import pandas as pd +import pypsa +import scipy.sparse as sparse +import xarray as xr +from _helpers import configure_logging, set_scenario_config +from shapely.prepared import prep + +logger = logging.getLogger(__name__) + + +def normed(s: pd.Series) -> pd.Series: + return s / s.sum() + + +def shapes_to_shapes(orig: gpd.GeoSeries, dest: gpd.GeoSeries) -> sparse.lil_matrix: + """ + Adopted from vresutils.transfer.Shapes2Shapes() + """ + orig_prepped = list(map(prep, orig)) + transfer = sparse.lil_matrix((len(dest), len(orig)), dtype=float) + + for i, j in product(range(len(dest)), range(len(orig))): + if orig_prepped[j].intersects(dest.iloc[i]): + area = orig.iloc[j].intersection(dest.iloc[i]).area + transfer[i, j] = area / dest.iloc[i].area + + return transfer + + +def upsample_load( + n: pypsa.Network, + regions_fn: str, + load_fn: str, + nuts3_fn: str, + gdp_pop_non_nuts3_fn: str, + distribution_key: dict[str, float], +) -> pd.DataFrame: + substation_lv_i = n.buses.index[n.buses["substation_lv"]] + gdf_regions = gpd.read_file(regions_fn).set_index("name").reindex(substation_lv_i) + load = pd.read_csv(load_fn, index_col=0, parse_dates=True) + + nuts3 = gpd.read_file(nuts3_fn).set_index("index") + + gdp_weight = distribution_key.get("gdp", 0.6) + pop_weight = distribution_key.get("pop", 0.4) + + data_arrays = [] + + for cntry, group in gdf_regions.geometry.groupby(gdf_regions.country): + + load_ct = load[cntry] + + if cntry in ["UA", "MD"]: + # separate handling because nuts3 provides no data for UA+MD + gdp_pop_non_nuts3 = gpd.read_file(gdp_pop_non_nuts3_fn).set_index("Bus") + gdp_pop_non_nuts3 = gdp_pop_non_nuts3.loc[ + (gdp_pop_non_nuts3.country == cntry) + & (gdp_pop_non_nuts3.index.isin(substation_lv_i)) + ] + factors = normed( + gdp_weight * normed(gdp_pop_non_nuts3["gdp"]) + + pop_weight * normed(gdp_pop_non_nuts3["pop"]) + ) + + elif len(group) == 1: + factors = pd.Series(1.0, index=group.index) + + else: + nuts3_cntry = nuts3.loc[nuts3.country == cntry] + transfer = shapes_to_shapes(group, nuts3_cntry.geometry).T.tocsr() + gdp_n = pd.Series( + transfer.dot(nuts3_cntry["gdp"].fillna(1.0).values), index=group.index + ) + pop_n = pd.Series( + transfer.dot(nuts3_cntry["pop"].fillna(1.0).values), index=group.index + ) + + factors = normed(gdp_weight * normed(gdp_n) + pop_weight * normed(pop_n)) + + data_arrays.append( + xr.DataArray( + factors.values * load_ct.values[:, np.newaxis], + dims=["time", "bus"], + coords={"time": load_ct.index.values, "bus": factors.index.values}, + ) + ) + + return xr.concat(data_arrays, dim="bus") + + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake("build_electricity_demand_base") + configure_logging(snakemake) + set_scenario_config(snakemake) + + params = snakemake.params + + n = pypsa.Network(snakemake.input.base_network) + + load = upsample_load( + n, + regions_fn=snakemake.input.regions, + load_fn=snakemake.input.load, + nuts3_fn=snakemake.input.nuts3, + gdp_pop_non_nuts3_fn=snakemake.input.get("gdp_pop_non_nuts3"), + distribution_key=params.distribution_key, + ) + + load.name = "electricity demand (MW)" + comp = dict(zlib=True, complevel=9, least_significant_digit=5) + load.to_netcdf(snakemake.output[0], encoding={load.name: comp}) diff --git a/scripts/build_existing_heating_distribution.py b/scripts/build_existing_heating_distribution.py index d37fcfee4..2cc43bafb 100644 --- a/scripts/build_existing_heating_distribution.py +++ b/scripts/build_existing_heating_distribution.py @@ -14,11 +14,11 @@ - Existing heating generators: `data/existing_heating_raw.csv` per country - Population layout: `resources/{run_name}/pop_layout_s_.csv`. Output of `scripts/build_clustered_population_layout.py` - Population layout with energy demands: `resources//pop_weighted_energy_totals_s_.csv` -- District heating share: `resources//district_heat_share_elec_s__.csv` +- District heating share: `resources//district_heat_share_base_s__.csv` Outputs: -------- -- Existing heat generation capacities distributed to nodes: `resources/{run_name}/existing_heating_distribution_elec_s{simpl}_{clusters}_{planning_horizons}.csv` +- Existing heat generation capacities distributed to nodes: `resources/{run_name}/existing_heating_distribution_base_s_{clusters}_{planning_horizons}.csv` Relevant settings: ------------------ @@ -154,7 +154,6 @@ def build_existing_heating(): snakemake = mock_snakemake( "build_existing_heating_distribution", - simpl="", clusters=48, planning_horizons=2050, ) diff --git a/scripts/build_gas_input_locations.py b/scripts/build_gas_input_locations.py index 0cd114758..ed36d338d 100644 --- a/scripts/build_gas_input_locations.py +++ b/scripts/build_gas_input_locations.py @@ -141,7 +141,6 @@ def build_gas_input_locations(gem_fn, entry_fn, sto_fn, countries): snakemake = mock_snakemake( "build_gas_input_locations", - simpl="", clusters="128", ) diff --git a/scripts/build_gdp_pop_non_nuts3.py b/scripts/build_gdp_pop_non_nuts3.py index d475aec92..2af2517df 100644 --- a/scripts/build_gdp_pop_non_nuts3.py +++ b/scripts/build_gdp_pop_non_nuts3.py @@ -42,11 +42,7 @@ def calc_gdp_pop(country, regions, gdp_non_nuts3, pop_non_nuts3): - gdp: A GeoDataFrame with the mean GDP p.c. values mapped to each bus. - pop: A GeoDataFrame with the summed POP values mapped to each bus. """ - regions = ( - regions.rename(columns={"name": "Bus"}) - .drop(columns=["x", "y"]) - .set_index("Bus") - ) + regions = regions.rename(columns={"name": "Bus"}).set_index("Bus") regions = regions[regions.country == country] # Create a bounding box for UA, MD from region shape, including a buffer of 10000 metres bounding_box = ( diff --git a/scripts/build_hac_features.py b/scripts/build_hac_features.py new file mode 100644 index 000000000..08d5e1201 --- /dev/null +++ b/scripts/build_hac_features.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2024 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT +""" +Aggregate all rastered cutout data to base regions Voronoi cells. +""" + +import logging + +import atlite +import geopandas as gpd +from _helpers import get_snapshots, set_scenario_config +from atlite.aggregate import aggregate_matrix +from dask.distributed import Client + +logger = logging.getLogger(__name__) + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake("build_hac_features") + set_scenario_config(snakemake) + + params = snakemake.params + nprocesses = int(snakemake.threads) + + if nprocesses > 1: + client = Client(n_workers=nprocesses, threads_per_worker=1) + else: + client = None + + time = get_snapshots(params.snapshots, params.drop_leap_day) + + cutout = atlite.Cutout(snakemake.input.cutout).sel(time=time) + + regions = gpd.read_file(snakemake.input.regions).set_index("name") + I = cutout.indicatormatrix(regions) # noqa: E741 + + ds = cutout.data[params.features].map( + aggregate_matrix, matrix=I, index=regions.index + ) + + ds = ds.load(scheduler=client) + + ds.to_netcdf(snakemake.output[0]) diff --git a/scripts/build_hourly_heat_demand.py b/scripts/build_hourly_heat_demand.py index 8573a1986..9bb1f77ff 100644 --- a/scripts/build_hourly_heat_demand.py +++ b/scripts/build_hourly_heat_demand.py @@ -22,12 +22,12 @@ ------ - ``data/heat_load_profile_BDEW.csv``: Intraday heat profile for water and space heating demand for the residential and services sectors for weekends and weekdays. -- ``resources/daily_heat_demand_total_elec_s_.nc``: Daily heat demand per cluster. +- ``resources/daily_heat_demand_total_base_s_.nc``: Daily heat demand per cluster. Outputs ------- -- ``resources/hourly_heat_demand_total_elec_s_.nc``: +- ``resources/hourly_heat_demand_total_base_s_.nc``: """ from itertools import product @@ -43,7 +43,6 @@ snakemake = mock_snakemake( "build_hourly_heat_demand", scope="total", - simpl="", clusters=5, ) set_scenario_config(snakemake) diff --git a/scripts/build_industrial_distribution_key.py b/scripts/build_industrial_distribution_key.py index 76a0e6d63..8fe707a41 100644 --- a/scripts/build_industrial_distribution_key.py +++ b/scripts/build_industrial_distribution_key.py @@ -8,13 +8,13 @@ Inputs ------- -- ``resources/regions_onshore_elec_s{simpl}_{clusters}.geojson`` -- ``resources/pop_layout_elec_s{simpl}_{clusters}.csv`` +- ``resources/regions_onshore_base_s_{clusters}.geojson`` +- ``resources/pop_layout_base_s_{clusters}.csv`` Outputs ------- -- ``resources/industrial_distribution_key_elec_s{simpl}_{clusters}.csv`` +- ``resources/industrial_distribution_key_base_s_{clusters}.csv`` Description ------- @@ -388,7 +388,6 @@ def build_nodal_distribution_key( snakemake = mock_snakemake( "build_industrial_distribution_key", - simpl="", clusters=128, ) configure_logging(snakemake) diff --git a/scripts/build_industrial_energy_demand_per_node.py b/scripts/build_industrial_energy_demand_per_node.py index eb022daed..db9cac6b4 100644 --- a/scripts/build_industrial_energy_demand_per_node.py +++ b/scripts/build_industrial_energy_demand_per_node.py @@ -8,14 +8,14 @@ Inputs ------ -- ``resources/industrial_energy_demand_today_elec_s{simpl}_{clusters}.csv`` +- ``resources/industrial_energy_demand_today_base_s_{clusters}.csv`` - ``resources/industry_sector_ratios_{planning_horizons}.csv`` -- ``resources/industrial_production_elec_s{simpl}_{clusters}_{planning_horizons}.csv`` +- ``resources/industrial_production_base_s_{clusters}_{planning_horizons}.csv`` Outputs ------- -- ``resources/industrial_energy_demand_elec_s{simpl}_{clusters}_{planning_horizons}.csv`` +- ``resources/industrial_energy_demand_base_s_{clusters}_{planning_horizons}.csv`` Description ------- @@ -45,7 +45,6 @@ snakemake = mock_snakemake( "build_industrial_energy_demand_per_node", - simpl="", clusters=48, planning_horizons=2030, ) diff --git a/scripts/build_industrial_energy_demand_per_node_today.py b/scripts/build_industrial_energy_demand_per_node_today.py index fd7c281c8..0675ce079 100644 --- a/scripts/build_industrial_energy_demand_per_node_today.py +++ b/scripts/build_industrial_energy_demand_per_node_today.py @@ -8,19 +8,19 @@ Inputs ------- -- ``resources/industrial_distribution_key_elec_s{simpl}_{clusters}.csv`` +- ``resources/industrial_distribution_key_base_s_{clusters}.csv`` - ``resources/industrial_energy_demand_per_country_today.csv`` Outputs ------- -- ``resources/industrial_energy_demand_per_node_today_elec_s{simpl}_{clusters}.csv`` +- ``resources/industrial_energy_demand_per_node_today_base_s_{clusters}.csv`` Description ------- This rule maps the industrial energy demand per country `industrial_energy_demand_per_country_today.csv` to each bus region. -The energy demand per country is multiplied by the mapping value from the file ``industrial_distribution_key_elec_s{simpl}_{clusters}.csv`` between 0 and 1 to get the industrial energy demand per bus. +The energy demand per country is multiplied by the mapping value from the file ``industrial_distribution_key_base_s_{clusters}.csv`` between 0 and 1 to get the industrial energy demand per bus. The unit of the energy demand is TWh/a. """ @@ -92,7 +92,6 @@ def build_nodal_industrial_energy_demand(): snakemake = mock_snakemake( "build_industrial_energy_demand_per_node_today", - simpl="", clusters=48, ) set_scenario_config(snakemake) diff --git a/scripts/build_industrial_production_per_node.py b/scripts/build_industrial_production_per_node.py index 862345e57..cdc6e1a20 100644 --- a/scripts/build_industrial_production_per_node.py +++ b/scripts/build_industrial_production_per_node.py @@ -8,13 +8,13 @@ Inputs ------- -- ``resources/industrial_distribution_key_elec_s{simpl}_{clusters}.csv`` +- ``resources/industrial_distribution_key_base_s_{clusters}.csv`` - ``resources/industrial_production_per_country_tomorrow_{planning_horizons}.csv`` Outputs ------- -- ``resources/industrial_production_per_node_elec_s{simpl}_{clusters}_{planning_horizons}.csv`` +- ``resources/industrial_production_per_node_base_s_{clusters}_{planning_horizons}.csv`` Description ------- @@ -87,11 +87,7 @@ def build_nodal_industrial_production(): if "snakemake" not in globals(): from _helpers import mock_snakemake - snakemake = mock_snakemake( - "build_industrial_production_per_node", - simpl="", - clusters=48, - ) + snakemake = mock_snakemake("build_industrial_production_per_node", clusters=48) set_scenario_config(snakemake) build_nodal_industrial_production() diff --git a/scripts/build_line_rating.py b/scripts/build_line_rating.py index f4b01fe03..86e98d62b 100755 --- a/scripts/build_line_rating.py +++ b/scripts/build_line_rating.py @@ -5,7 +5,7 @@ # coding: utf-8 """ -Adds dynamic line rating timeseries to the base network. +Calculates dynamic line rating time series from base network. Relevant Settings ----------------- @@ -14,11 +14,12 @@ lines: cutout: - line_rating: + dynamic_line_rating: .. seealso:: Documentation of the configuration file ``config.yaml` + Inputs ------ @@ -28,7 +29,7 @@ Outputs ------- -- ``resources/line_rating.nc`` +- ``resources/dlr.nc`` Description @@ -50,6 +51,7 @@ the maximal possible capacity factor "s_max_pu" for each transmission line at each time step is calculated. """ +import logging import re import atlite @@ -58,11 +60,14 @@ import pypsa import xarray as xr from _helpers import configure_logging, get_snapshots, set_scenario_config +from dask.distributed import Client from shapely.geometry import LineString as Line from shapely.geometry import Point +logger = logging.getLogger(__name__) + -def calculate_resistance(T, R_ref, T_ref=293, alpha=0.00403): +def calculate_resistance(T, R_ref, T_ref: float | int = 293, alpha: float = 0.00403): """ Calculates the resistance at other temperatures than the reference temperature. @@ -84,7 +89,12 @@ def calculate_resistance(T, R_ref, T_ref=293, alpha=0.00403): return R_ref * (1 + alpha * (T - T_ref)) -def calculate_line_rating(n, cutout): +def calculate_line_rating( + n: pypsa.Network, + cutout: atlite.Cutout, + show_progress: bool = True, + dask_kwargs: dict = None, +) -> xr.DataArray: """ Calculates the maximal allowed power flow in each line for each time step considering the maximal temperature. @@ -97,6 +107,10 @@ def calculate_line_rating(n, cutout): ------- xarray DataArray object with maximal power. """ + if dask_kwargs is None: + dask_kwargs = {} + + logger.info("Calculating dynamic line rating.") relevant_lines = n.lines[~n.lines["underground"]].copy() buses = relevant_lines[["bus0", "bus1"]].values x = n.buses.x @@ -120,7 +134,16 @@ def calculate_line_rating(n, cutout): relevant_lines["n_bundle"] = relevant_lines["n_bundle"].fillna(1) R *= relevant_lines["n_bundle"] R = calculate_resistance(T=353, R_ref=R) - Imax = cutout.line_rating(shapes, R, D=0.0218, Ts=353, epsilon=0.8, alpha=0.8) + Imax = cutout.line_rating( + shapes, + R, + D=0.0218, + Ts=353, + epsilon=0.8, + alpha=0.8, + show_progress=show_progress, + dask_kwargs=dask_kwargs, + ) line_factor = relevant_lines.eval("v_nom * n_bundle * num_parallel") / 1e3 # in mW return xr.DataArray( data=np.sqrt(3) * Imax * line_factor.values.reshape(-1, 1), @@ -134,21 +157,23 @@ def calculate_line_rating(n, cutout): if "snakemake" not in globals(): from _helpers import mock_snakemake - snakemake = mock_snakemake( - "build_line_rating", - network="elec", - simpl="", - clusters="5", - ll="v1.0", - opts="Co2L-4H", - ) + snakemake = mock_snakemake("build_line_rating") configure_logging(snakemake) set_scenario_config(snakemake) + nprocesses = int(snakemake.threads) + show_progress = not snakemake.config["run"].get("disable_progressbar", True) + show_progress = show_progress and snakemake.config["atlite"]["show_progress"] + if nprocesses > 1: + client = Client(n_workers=nprocesses, threads_per_worker=1) + else: + client = None + dask_kwargs = {"scheduler": client} + n = pypsa.Network(snakemake.input.base_network) time = get_snapshots(snakemake.params.snapshots, snakemake.params.drop_leap_day) cutout = atlite.Cutout(snakemake.input.cutout).sel(time=time) - da = calculate_line_rating(n, cutout) + da = calculate_line_rating(n, cutout, show_progress, dask_kwargs) da.to_netcdf(snakemake.output[0]) diff --git a/scripts/build_population_weighted_energy_totals.py b/scripts/build_population_weighted_energy_totals.py index 60af66aac..74c2573bf 100644 --- a/scripts/build_population_weighted_energy_totals.py +++ b/scripts/build_population_weighted_energy_totals.py @@ -16,7 +16,6 @@ snakemake = mock_snakemake( "build_population_weighted_energy_totals", kind="heat", - simpl="", clusters=60, ) set_scenario_config(snakemake) diff --git a/scripts/build_powerplants.py b/scripts/build_powerplants.py index 47cf2e141..94d03585b 100755 --- a/scripts/build_powerplants.py +++ b/scripts/build_powerplants.py @@ -35,7 +35,7 @@ Outputs ------- -- ``resource/powerplants.csv``: A list of conventional power plants (i.e. neither wind nor solar) with fields for name, fuel type, technology, country, capacity in MW, duration, commissioning year, retrofit year, latitude, longitude, and dam information as documented in the `powerplantmatching README `_; additionally it includes information on the closest substation/bus in ``networks/base.nc``. +- ``resource/powerplants_s_{clusters}.csv``: A list of conventional power plants (i.e. neither wind nor solar) with fields for name, fuel type, technology, country, capacity in MW, duration, commissioning year, retrofit year, latitude, longitude, and dam information as documented in the `powerplantmatching README `_; additionally it includes information on the closest substation/bus in ``networks/base_s_{clusters}.nc``. .. image:: img/powerplantmatching.png :scale: 30 % @@ -171,7 +171,7 @@ def replace_natural_gas_fueltype(df): configure_logging(snakemake) set_scenario_config(snakemake) - n = pypsa.Network(snakemake.input.base_network) + n = pypsa.Network(snakemake.input.network) countries = snakemake.params.countries ppl = ( diff --git a/scripts/build_renewable_profiles.py b/scripts/build_renewable_profiles.py index 57568f246..ab6a4748d 100644 --- a/scripts/build_renewable_profiles.py +++ b/scripts/build_renewable_profiles.py @@ -5,12 +5,11 @@ # # SPDX-License-Identifier: MIT """ -Calculates for each network node the (i) installable capacity (based on land- -use), (ii) the available generation time series (based on weather data), and -(iii) the average distance from the node for onshore wind, AC-connected -offshore wind, DC-connected offshore wind and solar PV generators. In addition -for offshore wind it calculates the fraction of the grid connection which is -under water. +Calculates for each clustered region the (i) installable capacity (based on +land-use from :mod:`determine_availability_matrix`), (ii) the available +generation time series (based on weather data), and (iii) the average distance +from the node for onshore wind, AC-connected offshore wind, DC-connected +offshore wind and solar PV generators. .. note:: Hydroelectric profiles are built in script :mod:`build_hydro_profiles`. @@ -26,9 +25,8 @@ renewable: {technology}: - cutout: corine: luisa: grid_codes: distance: natura: max_depth: min_depth: - max_shore_distance: min_shore_distance: capacity_per_sqkm: - correction_factor: min_p_max_pu: clip_p_max_pu: resource: + cutout: capacity_per_sqkm: correction_factor: min_p_max_pu: + clip_p_max_pu: resource: .. seealso:: Documentation of the configuration file ``config/config.yaml`` at @@ -37,40 +35,14 @@ Inputs ------ -- ``data/bundle/corine/g250_clc06_V18_5.tif``: `CORINE Land Cover (CLC) - `_ inventory on `44 - classes `_ of - land use (e.g. forests, arable land, industrial, urban areas) at 100m - resolution. - - .. image:: img/corine.png - :scale: 33 % - -- ``data/LUISA_basemap_020321_50m.tif``: `LUISA Base Map - `_ land - coverage dataset at 50m resolution similar to CORINE. For codes in relation to - CORINE land cover, see `Annex 1 of the technical documentation - `_. - -- ``data/bundle/gebco/GEBCO_2014_2D.nc``: A `bathymetric - `_ data set with a global terrain - model for ocean and land at 15 arc-second intervals by the `General - Bathymetric Chart of the Oceans (GEBCO) - `_. - - .. image:: img/gebco_2019_grid_image.jpg - :scale: 50 % - - **Source:** `GEBCO - `_ - -- ``resources/natura.tiff``: confer :ref:`natura` +- ``resources/availability_matrix_{clusters}_{technology}.nc``: see :mod:`determine_availability_matrix` - ``resources/offshore_shapes.geojson``: confer :ref:`shapes` -- ``resources/regions_onshore.geojson``: (if not offshore wind), confer +- ``resources/regions_onshore_base_s_{clusters}.geojson``: (if not offshore + wind), confer :ref:`busregions` +- ``resources/regions_offshore_base_s_{clusters}.geojson``: (if offshore wind), :ref:`busregions` -- ``resources/regions_offshore.geojson``: (if offshore wind), :ref:`busregions` - ``"cutouts/" + params["renewable"][{technology}]['cutout']``: :ref:`cutout` -- ``networks/base.nc``: :ref:`base` +- ``networks/_base_s_{clusters}.nc``: :ref:`base` Outputs ------- @@ -80,21 +52,13 @@ =================== ========== ========================================================= Field Dimensions Description =================== ========== ========================================================= - profile bus, time the per unit hourly availability factors for each node - ------------------- ---------- --------------------------------------------------------- - weight bus sum of the layout weighting for each node - ------------------- ---------- --------------------------------------------------------- - p_nom_max bus maximal installable capacity at the node (in MW) + profile bus, time the per unit hourly availability factors for each bus ------------------- ---------- --------------------------------------------------------- - potential y, x layout of generator units at cutout grid cells inside the - Voronoi cell (maximal installable capacity at each grid - cell multiplied by capacity factor) + p_nom_max bus maximal installable capacity at the bus (in MW) ------------------- ---------- --------------------------------------------------------- - average_distance bus average distance of units in the Voronoi cell to the - grid node (in km) - ------------------- ---------- --------------------------------------------------------- - underwater_fraction bus fraction of the average connection distance which is - under water (only for offshore) + average_distance bus average distance of units in the region to the + grid bus for onshore technologies and to the shoreline + for offshore technologies (in km) =================== ========== ========================================================= - **profile** @@ -109,50 +73,28 @@ :scale: 33 % :align: center - - **potential** - - .. image:: img/potential_heatmap.png - :scale: 33 % - :align: center - - **average_distance** .. image:: img/distance_hist.png :scale: 33 % :align: center - - **underwater_fraction** - - .. image:: img/underwater_hist.png - :scale: 33 % - :align: center - Description ----------- This script functions at two main spatial resolutions: the resolution of the -network nodes and their `Voronoi cells -`_, and the resolution of the -cutout grid cells for the weather data. Typically the weather data grid is finer -than the network nodes, so we have to work out the distribution of generators -across the grid cells within each Voronoi cell. This is done by taking account -of a combination of the available land at each grid cell and the capacity factor -there. - -First the script computes how much of the technology can be installed at each -cutout grid cell and each node using the `atlite -`_ library. This uses the CORINE land use data, -LUISA land use data, Natura2000 nature reserves, GEBCO bathymetry data, and -shipping lanes. - -.. image:: img/eligibility.png - :scale: 50 % - :align: center - -To compute the layout of generators in each node's Voronoi cell, the installable -potential in each grid cell is multiplied with the capacity factor at each grid -cell. This is done since we assume more generators are installed at cells with a -higher capacity factor. +clustered network regions, and the resolution of the cutout grid cells for the +weather data. Typically the weather data grid is finer than the network regions, +so we have to work out the distribution of generators across the grid cells +within each region. This is done by taking account of a combination of the +available land at each grid cell (computed in +:mod:`determine_availability_matrix`) and the capacity factor there. + +Based on the availability matrix, the script first computes how much of the +technology can be installed at each cutout grid cell. To compute the layout of +generators in each clustered region, the installable potential in each grid cell +is multiplied with the capacity factor at each grid cell. This is done since we +assume more generators are installed at cells with a higher capacity factor. .. image:: img/offwinddc-gridcell.png :scale: 50 % @@ -174,23 +116,17 @@ the weather data cutout from ``atlite``. The maximal installable potential for the node (`p_nom_max`) is computed by -adding up the installable potentials of the individual grid cells. If the model -comes close to this limit, then the time series may slightly overestimate -production since it is assumed the geographical distribution is proportional to -capacity factor. +adding up the installable potentials of the individual grid cells. """ -import functools import logging import time import atlite import geopandas as gpd -import numpy as np import xarray as xr from _helpers import configure_logging, get_snapshots, set_scenario_config +from build_shapes import _simplify_polys from dask.distributed import Client -from pypsa.geo import haversine -from shapely.geometry import LineString logger = logging.getLogger(__name__) @@ -199,15 +135,19 @@ if "snakemake" not in globals(): from _helpers import mock_snakemake - snakemake = mock_snakemake("build_renewable_profiles", technology="offwind-dc") + snakemake = mock_snakemake( + "build_renewable_profiles", clusters=38, technology="offwind-ac" + ) configure_logging(snakemake) set_scenario_config(snakemake) nprocesses = int(snakemake.threads) noprogress = snakemake.config["run"].get("disable_progressbar", True) noprogress = noprogress or not snakemake.config["atlite"]["show_progress"] - params = snakemake.params.renewable[snakemake.wildcards.technology] + technology = snakemake.wildcards.technology + params = snakemake.params.renewable[technology] resource = params["resource"] # pv panel params / wind turbine params + resource["show_progress"] = not noprogress tech = next(t for t in ["panel", "turbine"] if t in resource) models = resource[tech] @@ -229,6 +169,9 @@ sns = get_snapshots(snakemake.params.snapshots, snakemake.params.drop_leap_day) cutout = atlite.Cutout(snakemake.input.cutout).sel(time=sns) + + availability = xr.open_dataarray(snakemake.input.availability_matrix) + regions = gpd.read_file(snakemake.input.regions) assert not regions.empty, ( f"List of regions in {snakemake.input.regions} is empty, please " @@ -236,186 +179,96 @@ ) # do not pull up, set_index does not work if geo dataframe is empty regions = regions.set_index("name").rename_axis("bus") - buses = regions.index - - res = params.get("excluder_resolution", 100) - excluder = atlite.ExclusionContainer(crs=3035, res=res) - - if params["natura"]: - excluder.add_raster(snakemake.input.natura, nodata=0, allow_no_overlap=True) - - for dataset in ["corine", "luisa"]: - kwargs = {"nodata": 0} if dataset == "luisa" else {} - settings = params.get(dataset, {}) - if not settings: - continue - if dataset == "luisa" and res > 50: - logger.info( - "LUISA data is available at 50m resolution, " - f"but coarser {res}m resolution is used." - ) - if isinstance(settings, list): - settings = {"grid_codes": settings} - if "grid_codes" in settings: - codes = settings["grid_codes"] - excluder.add_raster( - snakemake.input[dataset], codes=codes, invert=True, crs=3035, **kwargs - ) - if settings.get("distance", 0.0) > 0.0: - codes = settings["distance_grid_codes"] - buffer = settings["distance"] - excluder.add_raster( - snakemake.input[dataset], codes=codes, buffer=buffer, crs=3035, **kwargs - ) - - if params.get("ship_threshold"): - shipping_threshold = ( - params["ship_threshold"] * 8760 * 6 - ) # approximation because 6 years of data which is hourly collected - func = functools.partial(np.less, shipping_threshold) - excluder.add_raster( - snakemake.input.ship_density, codes=func, crs=4326, allow_no_overlap=True - ) - - if params.get("max_depth"): - # lambda not supported for atlite + multiprocessing - # use named function np.greater with partially frozen argument instead - # and exclude areas where: -max_depth > grid cell depth - func = functools.partial(np.greater, -params["max_depth"]) - excluder.add_raster(snakemake.input.gebco, codes=func, crs=4326, nodata=-1000) - - if params.get("min_depth"): - func = functools.partial(np.greater, -params["min_depth"]) - excluder.add_raster( - snakemake.input.gebco, codes=func, crs=4326, nodata=-1000, invert=True - ) - - if "min_shore_distance" in params: - buffer = params["min_shore_distance"] - excluder.add_geometry(snakemake.input.country_shapes, buffer=buffer) - - if "max_shore_distance" in params: - buffer = params["max_shore_distance"] - excluder.add_geometry( - snakemake.input.country_shapes, buffer=buffer, invert=True - ) - - logger.info("Calculate landuse availability...") - start = time.time() - - kwargs = dict(nprocesses=nprocesses, disable_progressbar=noprogress) - availability = cutout.availabilitymatrix(regions, excluder, **kwargs) - - duration = time.time() - start - logger.info(f"Completed landuse availability calculation ({duration:2.2f}s)") - - # For Moldova and Ukraine: Overwrite parts not covered by Corine with - # externally determined available areas - if "availability_matrix_MD_UA" in snakemake.input.keys(): - availability_MDUA = xr.open_dataarray( - snakemake.input["availability_matrix_MD_UA"] + if snakemake.wildcards.technology.startswith("offwind"): + # for offshore regions, the shortest distance to the shoreline is used + offshore_regions = availability.coords["bus"].values + regions = regions.loc[offshore_regions] + regions = regions.map(lambda g: _simplify_polys(g, minarea=1)).set_crs( + regions.crs ) - availability.loc[availability_MDUA.coords] = availability_MDUA + else: + # for onshore regions, the representative point of the region is used + regions = regions.representative_point() + regions = regions.geometry.to_crs(3035) + buses = regions.index area = cutout.grid.to_crs(3035).area / 1e6 area = xr.DataArray( area.values.reshape(cutout.shape), [cutout.coords["y"], cutout.coords["x"]] ) - potential = capacity_per_sqkm * availability.sum("bus") * area func = getattr(cutout, resource.pop("method")) if client is not None: resource["dask_kwargs"] = {"scheduler": client} - logger.info("Calculate average capacity factor...") + logger.info(f"Calculate average capacity factor for technology {technology}...") start = time.time() capacity_factor = correction_factor * func(capacity_factor=True, **resource) layout = capacity_factor * area * capacity_per_sqkm duration = time.time() - start - logger.info(f"Completed average capacity factor calculation ({duration:2.2f}s)") + logger.info( + f"Completed average capacity factor calculation for technology {technology} ({duration:2.2f}s)" + ) profiles = [] - capacities = [] for year, model in models.items(): logger.info( - f"Calculate weighted capacity factor time series for model {model}..." + f"Calculate weighted capacity factor time series for model {model} for technology {technology}..." ) start = time.time() resource[tech] = model - profile, capacity = func( + profile = func( matrix=availability.stack(spatial=["y", "x"]), layout=layout, index=buses, per_unit=True, - return_capacity=True, + return_capacity=False, **resource, ) dim = {"year": [year]} profile = profile.expand_dims(dim) - capacity = capacity.expand_dims(dim) profiles.append(profile.rename("profile")) - capacities.append(capacity.rename("weight")) duration = time.time() - start logger.info( - f"Completed weighted capacity factor time series calculation for model {model} ({duration:2.2f}s)" + f"Completed weighted capacity factor time series calculation for model {model} for technology {technology} ({duration:2.2f}s)" ) profiles = xr.merge(profiles) - capacities = xr.merge(capacities) - logger.info("Calculating maximal capacity per bus") + logger.info(f"Calculating maximal capacity per bus for technology {technology}") p_nom_max = capacity_per_sqkm * availability @ area - logger.info("Calculate average distances.") + logger.info(f"Calculate average distances for technology {technology}.") layoutmatrix = (layout * availability).stack(spatial=["y", "x"]) - coords = cutout.grid[["x", "y"]] - bus_coords = regions[["x", "y"]] + coords = cutout.grid.representative_point().to_crs(3035) average_distance = [] - centre_of_mass = [] for bus in buses: row = layoutmatrix.sel(bus=bus).data nz_b = row != 0 row = row[nz_b] co = coords[nz_b] - distances = haversine(bus_coords.loc[bus], co) + distances = co.distance(regions[bus]).div(1e3) # km average_distance.append((distances * (row / row.sum())).sum()) - centre_of_mass.append(co.values.T @ (row / row.sum())) average_distance = xr.DataArray(average_distance, [buses]) - centre_of_mass = xr.DataArray(centre_of_mass, [buses, ("spatial", ["x", "y"])]) ds = xr.merge( [ correction_factor * profiles, - capacities, p_nom_max.rename("p_nom_max"), - potential.rename("potential"), average_distance.rename("average_distance"), ] ) - - if snakemake.wildcards.technology.startswith("offwind"): - logger.info("Calculate underwater fraction of connections.") - offshore_shape = gpd.read_file(snakemake.input["offshore_shapes"]).union_all() - underwater_fraction = [] - for bus in buses: - p = centre_of_mass.sel(bus=bus).data - line = LineString([p, regions.loc[bus, ["x", "y"]]]) - frac = line.intersection(offshore_shape).length / line.length - underwater_fraction.append(frac) - - ds["underwater_fraction"] = xr.DataArray(underwater_fraction, [buses]) - # select only buses with some capacity and minimal capacity factor mean_profile = ds["profile"].mean("time") if "year" in ds.indexes: diff --git a/scripts/build_retro_cost.py b/scripts/build_retro_cost.py index 42144c1aa..b25b6a648 100755 --- a/scripts/build_retro_cost.py +++ b/scripts/build_retro_cost.py @@ -1050,7 +1050,6 @@ def sample_dE_costs_area( snakemake = mock_snakemake( "build_retro_cost", - simpl="", clusters=48, ll="v1.0", sector_opts="Co2L0-168H-T-H-B-I-solar3-dist1", diff --git a/scripts/build_salt_cavern_potentials.py b/scripts/build_salt_cavern_potentials.py index f2c2ce8f9..2923da16b 100644 --- a/scripts/build_salt_cavern_potentials.py +++ b/scripts/build_salt_cavern_potentials.py @@ -74,9 +74,7 @@ def salt_cavern_potential_by_region(caverns, regions): if "snakemake" not in globals(): from _helpers import mock_snakemake - snakemake = mock_snakemake( - "build_salt_cavern_potentials", simpl="", clusters="37" - ) + snakemake = mock_snakemake("build_salt_cavern_potentials", clusters="37") set_scenario_config(snakemake) diff --git a/scripts/build_sequestration_potentials.py b/scripts/build_sequestration_potentials.py index 0d70448d6..a153a282c 100644 --- a/scripts/build_sequestration_potentials.py +++ b/scripts/build_sequestration_potentials.py @@ -38,9 +38,7 @@ def allocate_sequestration_potential( if "snakemake" not in globals(): from _helpers import mock_snakemake - snakemake = mock_snakemake( - "build_sequestration_potentials", simpl="", clusters="128" - ) + snakemake = mock_snakemake("build_sequestration_potentials", clusters="128") set_scenario_config(snakemake) diff --git a/scripts/build_shipping_demand.py b/scripts/build_shipping_demand.py index d8c960ae0..649e53c7b 100644 --- a/scripts/build_shipping_demand.py +++ b/scripts/build_shipping_demand.py @@ -17,11 +17,7 @@ if "snakemake" not in globals(): from _helpers import mock_snakemake - snakemake = mock_snakemake( - "build_shipping_demand", - simpl="", - clusters=48, - ) + snakemake = mock_snakemake("build_shipping_demand", clusters=48) set_scenario_config(snakemake) scope = gpd.read_file(snakemake.input.scope).geometry[0] diff --git a/scripts/build_solar_thermal_profiles.py b/scripts/build_solar_thermal_profiles.py index 9de04f451..4f13b5fe1 100644 --- a/scripts/build_solar_thermal_profiles.py +++ b/scripts/build_solar_thermal_profiles.py @@ -26,13 +26,13 @@ ------ - ``resources/.nc``: -- ``resources/_.geojson``: +- ``resources/_.geojson``: - ``cutout``: Weather data cutout, as specified in config Outputs ------- -- ``resources/solar_thermal__elec_s_.nc``: +- ``resources/solar_thermal__base_s_.nc``: """ import atlite @@ -46,11 +46,7 @@ if "snakemake" not in globals(): from _helpers import mock_snakemake - snakemake = mock_snakemake( - "build_solar_thermal_profiles", - simpl="", - clusters=48, - ) + snakemake = mock_snakemake("build_solar_thermal_profiles", clusters=48) set_scenario_config(snakemake) nprocesses = int(snakemake.threads) diff --git a/scripts/build_temperature_profiles.py b/scripts/build_temperature_profiles.py index 8e07ee87c..34cd39d84 100644 --- a/scripts/build_temperature_profiles.py +++ b/scripts/build_temperature_profiles.py @@ -26,14 +26,14 @@ ------ - ``resources//pop_layout_total.nc``: -- ``resources//regions_onshore_elec_s_.geojson``: +- ``resources//regions_onshore_base_s_.geojson``: - ``cutout``: Weather data cutout, as specified in config Outputs ------- -- ``resources/temp_soil_total_elec_s_.nc``: -- ``resources/temp_air_total_elec_s_.nc` +- ``resources/temp_soil_total_base_s_.nc``: +- ``resources/temp_air_total_base_s_.nc` """ import atlite @@ -49,7 +49,6 @@ snakemake = mock_snakemake( "build_temperature_profiles", - simpl="", clusters=48, ) set_scenario_config(snakemake) diff --git a/scripts/build_transport_demand.py b/scripts/build_transport_demand.py index 074dc260b..f2ae0c21c 100644 --- a/scripts/build_transport_demand.py +++ b/scripts/build_transport_demand.py @@ -167,11 +167,7 @@ def bev_dsm_profile(snapshots, nodes, options): if "snakemake" not in globals(): from _helpers import mock_snakemake - snakemake = mock_snakemake( - "build_transport_demand", - simpl="", - clusters=128, - ) + snakemake = mock_snakemake("build_transport_demand", clusters=128) configure_logging(snakemake) set_scenario_config(snakemake) diff --git a/scripts/cluster_gas_network.py b/scripts/cluster_gas_network.py index b95c45807..559983964 100755 --- a/scripts/cluster_gas_network.py +++ b/scripts/cluster_gas_network.py @@ -108,7 +108,7 @@ def aggregate_parallel_pipes(df): if "snakemake" not in globals(): from _helpers import mock_snakemake - snakemake = mock_snakemake("cluster_gas_network", simpl="", clusters="37") + snakemake = mock_snakemake("cluster_gas_network", clusters="37") configure_logging(snakemake) set_scenario_config(snakemake) diff --git a/scripts/cluster_network.py b/scripts/cluster_network.py index 4460b0adb..cff506946 100644 --- a/scripts/cluster_network.py +++ b/scripts/cluster_network.py @@ -6,7 +6,7 @@ # coding: utf-8 """ Creates networks clustered to ``{cluster}`` number of zones with aggregated -buses, generators and transmission corridors. +buses and transmission corridors. Relevant Settings ----------------- @@ -32,30 +32,30 @@ Inputs ------ -- ``resources/regions_onshore_elec_s{simpl}.geojson``: confer :ref:`simplify` -- ``resources/regions_offshore_elec_s{simpl}.geojson``: confer :ref:`simplify` -- ``resources/busmap_elec_s{simpl}.csv``: confer :ref:`simplify` -- ``networks/elec_s{simpl}.nc``: confer :ref:`simplify` -- ``data/custom_busmap_elec_s{simpl}_{clusters}_{base_network}.csv``: optional input +- ``resources/regions_onshore_base.geojson``: confer :ref:`simplify` +- ``resources/regions_offshore_base.geojson``: confer :ref:`simplify` +- ``resources/busmap_base_s.csv``: confer :ref:`simplify` +- ``networks/base.nc``: confer :ref:`simplify` +- ``data/custom_busmap_base_s_{clusters}_{base_network}.csv``: optional input Outputs ------- -- ``resources/regions_onshore_elec_s{simpl}_{clusters}.geojson``: +- ``resources/regions_onshore_base_s_{clusters}.geojson``: - .. image:: img/regions_onshore_elec_s_X.png + .. image:: img/regions_onshore_base_s_X.png :scale: 33 % -- ``resources/regions_offshore_elec_s{simpl}_{clusters}.geojson``: +- ``resources/regions_offshore_base_s_{clusters}.geojson``: - .. image:: img/regions_offshore_elec_s_X.png + .. image:: img/regions_offshore_base_s_X.png :scale: 33 % -- ``resources/busmap_elec_s{simpl}_{clusters}.csv``: Mapping of buses from ``networks/elec_s{simpl}.nc`` to ``networks/elec_s{simpl}_{clusters}.nc``; -- ``resources/linemap_elec_s{simpl}_{clusters}.csv``: Mapping of lines from ``networks/elec_s{simpl}.nc`` to ``networks/elec_s{simpl}_{clusters}.nc``; -- ``networks/elec_s{simpl}_{clusters}.nc``: +- ``resources/busmap_base_s_{clusters}.csv``: Mapping of buses from ``networks/base.nc`` to ``networks/base_s_{clusters}.nc``; +- ``resources/linemap_base_s_{clusters}.csv``: Mapping of lines from ``networks/base.nc`` to ``networks/base_s_{clusters}.nc``; +- ``networks/base_s_{clusters}.nc``: - .. image:: img/elec_s_X.png + .. image:: img/base_s_X.png :scale: 40 % Description @@ -63,60 +63,33 @@ .. note:: - **Why is clustering used both in** ``simplify_network`` **and** ``cluster_network`` **?** - - Consider for example a network ``networks/elec_s100_50.nc`` in which - ``simplify_network`` clusters the network to 100 buses and in a second - step ``cluster_network``` reduces it down to 50 buses. - - In preliminary tests, it turns out, that the principal effect of - changing spatial resolution is actually only partially due to the - transmission network. It is more important to differentiate between - wind generators with higher capacity factors from those with lower - capacity factors, i.e. to have a higher spatial resolution in the - renewable generation than in the number of buses. - - The two-step clustering allows to study this effect by looking at - networks like ``networks/elec_s100_50m.nc``. Note the additional - ``m`` in the ``{cluster}`` wildcard. So in the example network - there are still up to 100 different wind generators. - - In combination these two features allow you to study the spatial - resolution of the transmission network separately from the - spatial resolution of renewable generators. - **Is it possible to run the model without the** ``simplify_network`` **rule?** No, the network clustering methods in the PyPSA module `pypsa.clustering.spatial `_ do not work reliably with multiple voltage levels and transformers. -.. tip:: - The rule :mod:`cluster_networks` runs - for all ``scenario`` s in the configuration file - the rule :mod:`cluster_network`. - Exemplary unsolved network clustered to 512 nodes: -.. image:: img/elec_s_512.png +.. image:: img/base_s_512.png :scale: 40 % :align: center Exemplary unsolved network clustered to 256 nodes: -.. image:: img/elec_s_256.png +.. image:: img/base_s_256.png :scale: 40 % :align: center Exemplary unsolved network clustered to 128 nodes: -.. image:: img/elec_s_128.png +.. image:: img/base_s_128.png :scale: 40 % :align: center Exemplary unsolved network clustered to 37 nodes: -.. image:: img/elec_s_37.png +.. image:: img/base_s_37.png :scale: 40 % :align: center """ @@ -127,13 +100,11 @@ import geopandas as gpd import linopy -import matplotlib.pyplot as plt import numpy as np import pandas as pd import pypsa -import seaborn as sns -from _helpers import configure_logging, set_scenario_config, update_p_nom_max -from add_electricity import load_costs +import xarray as xr +from _helpers import configure_logging, set_scenario_config from base_network import append_bus_shapes from packaging.version import Version, parse from pypsa.clustering.spatial import ( @@ -142,6 +113,7 @@ busmap_by_kmeans, get_clustering_from_busmap, ) +from scipy.sparse.csgraph import connected_components PD_GE_2_2 = parse(pd.__version__) >= Version("2.2") @@ -154,79 +126,61 @@ def normed(x): return (x / x.sum()).fillna(0.0) -def weighting_for_country(n, x): - conv_carriers = {"OCGT", "CCGT", "PHS", "hydro"} - gen = n.generators.loc[n.generators.carrier.isin(conv_carriers)].groupby( - "bus" - ).p_nom.sum().reindex(n.buses.index, fill_value=0.0) + n.storage_units.loc[ - n.storage_units.carrier.isin(conv_carriers) - ].groupby( - "bus" - ).p_nom.sum().reindex( - n.buses.index, fill_value=0.0 - ) - load = n.loads_t.p_set.mean().groupby(n.loads.bus).sum() +def weighting_for_country(df: pd.DataFrame, weights: pd.Series) -> pd.Series: + w = normed(weights.reindex(df.index, fill_value=0)) + return (w * (100 / w.max())).clip(lower=1).astype(int) - b_i = x.index - g = normed(gen.reindex(b_i, fill_value=0)) - l = normed(load.reindex(b_i, fill_value=0)) - w = g + l - return (w * (100.0 / w.max())).clip(lower=1.0).astype(int) +def get_feature_data_for_hac(fn: str) -> pd.DataFrame: + ds = xr.open_dataset(fn) + feature_data = ( + pd.concat([ds[var].to_pandas() for var in ds.data_vars], axis=0).fillna(0.0).T + ) + feature_data.columns = feature_data.columns.astype(str) + return feature_data -def get_feature_for_hac(n, buses_i=None, feature=None): - if buses_i is None: - buses_i = n.buses.index +def fix_country_assignment_for_hac(n: pypsa.Network) -> None: - if feature is None: - feature = "solar+onwind-time" + # overwrite country of nodes that are disconnected from their country-topology + for country in n.buses.country.unique(): + m = n[n.buses.country == country].copy() - carriers = feature.split("-")[0].split("+") - if "offwind" in carriers: - carriers.remove("offwind") - carriers = np.append( - carriers, n.generators.carrier.filter(like="offwind").unique() - ) + _, labels = connected_components(m.adjacency_matrix(), directed=False) - if feature.split("-")[1] == "cap": - feature_data = pd.DataFrame(index=buses_i, columns=carriers) - for carrier in carriers: - gen_i = n.generators.query("carrier == @carrier").index - attach = ( - n.generators_t.p_max_pu[gen_i] - .mean() - .rename(index=n.generators.loc[gen_i].bus) - ) - feature_data[carrier] = attach - - if feature.split("-")[1] == "time": - feature_data = pd.DataFrame(columns=buses_i) - for carrier in carriers: - gen_i = n.generators.query("carrier == @carrier").index - attach = n.generators_t.p_max_pu[gen_i].rename( - columns=n.generators.loc[gen_i].bus - ) - feature_data = pd.concat([feature_data, attach], axis=0)[buses_i] + component = pd.Series(labels, index=m.buses.index) + component_sizes = component.value_counts() - feature_data = feature_data.T - # timestamp raises error in sklearn >= v1.2: - feature_data.columns = feature_data.columns.astype(str) + if len(component_sizes) > 1: + disconnected_bus = component[component == component_sizes.index[-1]].index[ + 0 + ] - feature_data = feature_data.fillna(0) + neighbor_bus = n.lines.query( + "bus0 == @disconnected_bus or bus1 == @disconnected_bus" + ).iloc[0][["bus0", "bus1"]] + new_country = list(set(n.buses.loc[neighbor_bus].country) - {country})[0] - return feature_data + logger.info( + f"overwriting country `{country}` of bus `{disconnected_bus}` " + f"to new country `{new_country}`, because it is disconnected " + "from its initial inter-country transmission grid." + ) + n.buses.at[disconnected_bus, "country"] = new_country -def distribute_clusters(n, n_clusters, focus_weights=None, solver_name="scip"): +def distribute_n_clusters_to_countries( + n: pypsa.Network, + n_clusters: int, + cluster_weights: pd.Series, + focus_weights: dict | None = None, + solver_name: str = "scip", +) -> pd.Series: """ Determine the number of clusters per country. """ L = ( - n.loads_t.p_set.mean() - .groupby(n.loads.bus) - .sum() - .groupby([n.buses.country, n.buses.sub_network]) + cluster_weights.groupby([n.buses.country, n.buses.sub_network]) .sum() .pipe(normed) ) @@ -277,92 +231,50 @@ def distribute_clusters(n, n_clusters, focus_weights=None, solver_name="scip"): def busmap_for_n_clusters( - n, - n_clusters, - solver_name, - focus_weights=None, - algorithm="kmeans", - feature=None, + n: pypsa.Network, + n_clusters_c: pd.Series, + cluster_weights: pd.Series, + algorithm: str = "kmeans", + features: pd.DataFrame | None = None, **algorithm_kwds, -): +) -> pd.Series: + if algorithm == "hac" and features is None: + raise ValueError("For HAC clustering, features must be provided.") + if algorithm == "kmeans": algorithm_kwds.setdefault("n_init", 1000) algorithm_kwds.setdefault("max_iter", 30000) algorithm_kwds.setdefault("tol", 1e-6) algorithm_kwds.setdefault("random_state", 0) - def fix_country_assignment_for_hac(n): - from scipy.sparse import csgraph - - # overwrite country of nodes that are disconnected from their country-topology - for country in n.buses.country.unique(): - m = n[n.buses.country == country].copy() - - _, labels = csgraph.connected_components( - m.adjacency_matrix(), directed=False - ) - - component = pd.Series(labels, index=m.buses.index) - component_sizes = component.value_counts() - - if len(component_sizes) > 1: - disconnected_bus = component[ - component == component_sizes.index[-1] - ].index[0] - - neighbor_bus = n.lines.query( - "bus0 == @disconnected_bus or bus1 == @disconnected_bus" - ).iloc[0][["bus0", "bus1"]] - new_country = list(set(n.buses.loc[neighbor_bus].country) - {country})[ - 0 - ] - - logger.info( - f"overwriting country `{country}` of bus `{disconnected_bus}` " - f"to new country `{new_country}`, because it is disconnected " - "from its initial inter-country transmission grid." - ) - n.buses.at[disconnected_bus, "country"] = new_country - return n - - if algorithm == "hac": - feature = get_feature_for_hac(n, buses_i=n.buses.index, feature=feature) - n = fix_country_assignment_for_hac(n) - - if (algorithm != "hac") and (feature is not None): - logger.warning( - f"Keyword argument feature is only valid for algorithm `hac`. " - f"Given feature `{feature}` will be ignored." - ) - - n.determine_network_topology() - - n_clusters = distribute_clusters( - n, n_clusters, focus_weights=focus_weights, solver_name=solver_name - ) - def busmap_for_country(x): prefix = x.name[0] + x.name[1] + " " - logger.debug(f"Determining busmap for country {prefix[:-1]}") + logger.debug( + f"Determining busmap for country {prefix[:-1]} " + f"from {len(x)} buses to {n_clusters_c[x.name]}." + ) if len(x) == 1: return pd.Series(prefix + "0", index=x.index) - weight = weighting_for_country(n, x) + weight = weighting_for_country(x, cluster_weights) if algorithm == "kmeans": return prefix + busmap_by_kmeans( - n, weight, n_clusters[x.name], buses_i=x.index, **algorithm_kwds + n, weight, n_clusters_c[x.name], buses_i=x.index, **algorithm_kwds ) elif algorithm == "hac": return prefix + busmap_by_hac( - n, n_clusters[x.name], buses_i=x.index, feature=feature.loc[x.index] + n, + n_clusters_c[x.name], + buses_i=x.index, + feature=features.reindex(x.index, fill_value=0.0), ) elif algorithm == "modularity": return prefix + busmap_by_greedy_modularity( - n, n_clusters[x.name], buses_i=x.index + n, n_clusters_c[x.name], buses_i=x.index ) else: raise ValueError( - f"`algorithm` must be one of 'kmeans' or 'hac'. Is {algorithm}." + f"`algorithm` must be one of 'kmeans' or 'hac' or 'modularity'. Is {algorithm}." ) compat_kws = dict(include_groups=False) if PD_GE_2_2 else {} @@ -376,93 +288,61 @@ def busmap_for_country(x): def clustering_for_n_clusters( - n, - n_clusters, - custom_busmap=False, - aggregate_carriers=None, - line_length_factor=1.25, - aggregation_strategies=dict(), - solver_name="scip", - algorithm="hac", - feature=None, - extended_link_costs=0, - focus_weights=None, -): - if not isinstance(custom_busmap, pd.Series): - busmap = busmap_for_n_clusters( - n, n_clusters, solver_name, focus_weights, algorithm, feature - ) - else: - busmap = custom_busmap + n: pypsa.Network, + busmap: pd.Series, + line_length_factor: float = 1.25, + aggregation_strategies: dict | None = None, +) -> pypsa.clustering.spatial.Clustering: + + if aggregation_strategies is None: + aggregation_strategies = dict() line_strategies = aggregation_strategies.get("lines", dict()) - generator_strategies = aggregation_strategies.get("generators", dict()) - one_port_strategies = aggregation_strategies.get("one_ports", dict()) + + bus_strategies = aggregation_strategies.get("buses", dict()) + bus_strategies.setdefault("substation_lv", lambda x: bool(x.sum())) + bus_strategies.setdefault("substation_off", lambda x: bool(x.sum())) clustering = get_clustering_from_busmap( n, busmap, - aggregate_generators_weighted=True, - aggregate_generators_carriers=aggregate_carriers, - aggregate_one_ports=["Load", "StorageUnit"], line_length_factor=line_length_factor, + bus_strategies=bus_strategies, line_strategies=line_strategies, - generator_strategies=generator_strategies, - one_port_strategies=one_port_strategies, - scale_link_capital_costs=False, custom_line_groupers=["build_year"], ) - if not n.links.empty: - nc = clustering.network - nc.links["underwater_fraction"] = ( - n.links.eval("underwater_fraction * length").div(nc.links.length).dropna() - ) - nc.links["capital_cost"] = nc.links["capital_cost"].add( - (nc.links.length - n.links.length) - .clip(lower=0) - .mul(extended_link_costs) - .dropna(), - fill_value=0, - ) - return clustering -def cluster_regions(busmaps, regions): +def cluster_regions( + busmaps: tuple | list, regions: gpd.GeoDataFrame, with_country: bool = False +) -> gpd.GeoDataFrame: """ Cluster regions based on busmaps and save the results to a file and to the network. Parameters: - busmaps (list): A list of busmaps used for clustering. - - which (str): The type of regions to cluster. + - regions (gpd.GeoDataFrame): The regions to cluster. + - with_country (bool): Whether to keep country column. Returns: None """ busmap = reduce(lambda x, y: x.map(y), busmaps[1:], busmaps[0]) - regions = regions.reindex(columns=["name", "geometry"]).set_index("name") + columns = ["name", "country", "geometry"] if with_country else ["name", "geometry"] + regions = regions.reindex(columns=columns).set_index("name") regions_c = regions.dissolve(busmap) regions_c.index.name = "name" return regions_c.reset_index() -def plot_busmap_for_n_clusters(n, n_clusters, solver_name="scip", fn=None): - busmap = busmap_for_n_clusters(n, n_clusters, solver_name) - cs = busmap.unique() - cr = sns.color_palette("hls", len(cs)) - n.plot(bus_colors=busmap.map(dict(zip(cs, cr)))) - if fn is not None: - plt.savefig(fn, bbox_inches="tight") - del cs, cr - - if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake - snakemake = mock_snakemake("cluster_network", simpl="", clusters="40") + snakemake = mock_snakemake("cluster_network", clusters=60) configure_logging(snakemake) set_scenario_config(snakemake) @@ -470,43 +350,20 @@ def plot_busmap_for_n_clusters(n, n_clusters, solver_name="scip", fn=None): solver_name = snakemake.config["solving"]["solver"]["name"] n = pypsa.Network(snakemake.input.network) + buses_prev, lines_prev, links_prev = len(n.buses), len(n.lines), len(n.links) - # remove integer outputs for compatibility with PyPSA v0.26.0 - n.generators.drop("n_mod", axis=1, inplace=True, errors="ignore") - - exclude_carriers = params.cluster_network["exclude_carriers"] - aggregate_carriers = set(n.generators.carrier) - set(exclude_carriers) - conventional_carriers = set(params.conventional_carriers) - if snakemake.wildcards.clusters.endswith("m"): - n_clusters = int(snakemake.wildcards.clusters[:-1]) - aggregate_carriers = conventional_carriers & aggregate_carriers - elif snakemake.wildcards.clusters.endswith("c"): - n_clusters = int(snakemake.wildcards.clusters[:-1]) - aggregate_carriers = aggregate_carriers - conventional_carriers - elif snakemake.wildcards.clusters == "all": + load = ( + xr.open_dataarray(snakemake.input.load) + .mean(dim="time") + .to_pandas() + .reindex(n.buses.index, fill_value=0.0) + ) + + if snakemake.wildcards.clusters == "all": n_clusters = len(n.buses) else: n_clusters = int(snakemake.wildcards.clusters) - if params.cluster_network.get("consider_efficiency_classes", False): - carriers = [] - for c in aggregate_carriers: - gens = n.generators.query("carrier == @c") - low = gens.efficiency.quantile(0.10) - high = gens.efficiency.quantile(0.90) - if low >= high: - carriers += [c] - else: - labels = ["low", "medium", "high"] - suffix = pd.cut( - gens.efficiency, bins=[0, low, high, 1], labels=labels - ).astype(str) - carriers += [f"{c} {label} efficiency" for label in labels] - n.generators.update( - {"carrier": gens.carrier + " " + suffix + " efficiency"} - ) - aggregate_carriers = carriers - if n_clusters == len(n.buses): # Fast-path if no clustering is necessary busmap = n.buses.index.to_series() @@ -515,13 +372,6 @@ def plot_busmap_for_n_clusters(n, n_clusters, solver_name="scip", fn=None): else: Nyears = n.snapshot_weightings.objective.sum() / 8760 - hvac_overhead_cost = load_costs( - snakemake.input.tech_costs, - params.costs, - params.max_hours, - Nyears, - ).at["HVAC overhead", "capital_cost"] - custom_busmap = params.custom_busmap if custom_busmap: custom_busmap = pd.read_csv( @@ -529,32 +379,42 @@ def plot_busmap_for_n_clusters(n, n_clusters, solver_name="scip", fn=None): ).squeeze() custom_busmap.index = custom_busmap.index.astype(str) logger.info(f"Imported custom busmap from {snakemake.input.custom_busmap}") + busmap = custom_busmap + else: + algorithm = params.cluster_network["algorithm"] + features = None + if algorithm == "hac": + features = get_feature_data_for_hac(snakemake.input.hac_features) + fix_country_assignment_for_hac(n) + + n.determine_network_topology() + + n_clusters_c = distribute_n_clusters_to_countries( + n, + n_clusters, + load, + focus_weights=params.focus_weights, + solver_name=solver_name, + ) + + busmap = busmap_for_n_clusters( + n, + n_clusters_c, + cluster_weights=load, + algorithm=algorithm, + features=features, + ) clustering = clustering_for_n_clusters( n, - n_clusters, - custom_busmap, - aggregate_carriers, - params.length_factor, - params.aggregation_strategies, - solver_name, - params.cluster_network["algorithm"], - params.cluster_network["feature"], - hvac_overhead_cost, - params.focus_weights, + busmap, + line_length_factor=params.length_factor, + aggregation_strategies=params.aggregation_strategies, ) nc = clustering.network - update_p_nom_max(nc) - - if params.cluster_network.get("consider_efficiency_classes"): - labels = [f" {label} efficiency" for label in ["low", "medium", "high"]] - nc.generators["carrier"] = nc.generators.carrier.replace(labels, "", regex=True) - for attr in ( - "busmap", - "linemap", - ): # also available: linemap_positive, linemap_negative + for attr in ["busmap", "linemap"]: getattr(clustering, attr).to_csv(snakemake.output[attr]) # nc.shapes = n.shapes.copy() @@ -566,3 +426,10 @@ def plot_busmap_for_n_clusters(n, n_clusters, solver_name="scip", fn=None): nc.meta = dict(snakemake.config, **dict(wildcards=dict(snakemake.wildcards))) nc.export_to_netcdf(snakemake.output.network) + + logger.info( + f"Clustered network:\n" + f"Buses: {buses_prev} to {len(nc.buses)}\n" + f"Lines: {lines_prev} to {len(nc.lines)}\n" + f"Links: {links_prev} to {len(nc.links)}" + ) diff --git a/scripts/determine_availability_matrix.py b/scripts/determine_availability_matrix.py new file mode 100644 index 000000000..8006740e3 --- /dev/null +++ b/scripts/determine_availability_matrix.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# SPDX-FileCopyrightText: : 2017-2024 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT +""" +The script performs a land eligibility analysis of what share of land is +availability for developing the selected technology at each cutout grid cell. +The script uses the `atlite `_ library and +several GIS datasets like the CORINE land use data, LUISA land use data, +Natura2000 nature reserves, GEBCO bathymetry data, and shipping lanes. + +Relevant settings +----------------- + +.. code:: yaml + + atlite: + nprocesses: + + renewable: + {technology}: + cutout: corine: luisa: grid_codes: distance: natura: max_depth: + min_depth: max_shore_distance: min_shore_distance: resource: + +.. seealso:: + Documentation of the configuration file ``config/config.yaml`` at + :ref:`atlite_cf`, :ref:`renewable_cf` + +Inputs +------ + +- ``data/bundle/corine/g250_clc06_V18_5.tif``: `CORINE Land Cover (CLC) + `_ inventory on `44 + classes `_ of + land use (e.g. forests, arable land, industrial, urban areas) at 100m + resolution. + + .. image:: img/corine.png + :scale: 33 % + +- ``data/LUISA_basemap_020321_50m.tif``: `LUISA Base Map + `_ land + coverage dataset at 50m resolution similar to CORINE. For codes in relation to + CORINE land cover, see `Annex 1 of the technical documentation + `_. + +- ``data/bundle/gebco/GEBCO_2014_2D.nc``: A `bathymetric + `_ data set with a global terrain + model for ocean and land at 15 arc-second intervals by the `General + Bathymetric Chart of the Oceans (GEBCO) + `_. + + .. image:: img/gebco_2019_grid_image.jpg + :scale: 50 % + + **Source:** `GEBCO + `_ + +- ``resources/natura.tiff``: confer :ref:`natura` +- ``resources/offshore_shapes.geojson``: confer :ref:`shapes` +- ``resources/regions_onshore_base_s_{clusters}.geojson``: (if not offshore + wind), confer :ref:`busregions` +- ``resources/regions_offshore_base_s_{clusters}.geojson``: (if offshore wind), + :ref:`busregions` +- ``"cutouts/" + params["renewable"][{technology}]['cutout']``: :ref:`cutout` +- ``networks/_base_s_{clusters}.nc``: :ref:`base` + +Outputs +------- + +- ``resources/availability_matrix_{clusters_{technology}.nc`` +""" +import functools +import logging +import time + +import atlite +import geopandas as gpd +import numpy as np +import xarray as xr +from _helpers import configure_logging, set_scenario_config + +logger = logging.getLogger(__name__) + + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake( + "build_renewable_profiles", clusters=100, technology="onwind" + ) + configure_logging(snakemake) + set_scenario_config(snakemake) + + nprocesses = int(snakemake.threads) + noprogress = snakemake.config["run"].get("disable_progressbar", True) + noprogress = noprogress or not snakemake.config["atlite"]["show_progress"] + technology = snakemake.wildcards.technology + params = snakemake.params.renewable[technology] + + cutout = atlite.Cutout(snakemake.input.cutout) + regions = gpd.read_file(snakemake.input.regions) + assert not regions.empty, ( + f"List of regions in {snakemake.input.regions} is empty, please " + "disable the corresponding renewable technology" + ) + # do not pull up, set_index does not work if geo dataframe is empty + regions = regions.set_index("name").rename_axis("bus") + + res = params.get("excluder_resolution", 100) + excluder = atlite.ExclusionContainer(crs=3035, res=res) + + if params["natura"]: + excluder.add_raster(snakemake.input.natura, nodata=0, allow_no_overlap=True) + + for dataset in ["corine", "luisa"]: + kwargs = {"nodata": 0} if dataset == "luisa" else {} + settings = params.get(dataset, {}) + if not settings: + continue + if dataset == "luisa" and res > 50: + logger.info( + "LUISA data is available at 50m resolution, " + f"but coarser {res}m resolution is used." + ) + if isinstance(settings, list): + settings = {"grid_codes": settings} + if "grid_codes" in settings: + codes = settings["grid_codes"] + excluder.add_raster( + snakemake.input[dataset], codes=codes, invert=True, crs=3035, **kwargs + ) + if settings.get("distance", 0.0) > 0.0: + codes = settings["distance_grid_codes"] + buffer = settings["distance"] + excluder.add_raster( + snakemake.input[dataset], codes=codes, buffer=buffer, crs=3035, **kwargs + ) + + if params.get("ship_threshold"): + shipping_threshold = ( + params["ship_threshold"] * 8760 * 6 + ) # approximation because 6 years of data which is hourly collected + func = functools.partial(np.less, shipping_threshold) + excluder.add_raster( + snakemake.input.ship_density, codes=func, crs=4326, allow_no_overlap=True + ) + + if params.get("max_depth"): + # lambda not supported for atlite + multiprocessing + # use named function np.greater with partially frozen argument instead + # and exclude areas where: -max_depth > grid cell depth + func = functools.partial(np.greater, -params["max_depth"]) + excluder.add_raster(snakemake.input.gebco, codes=func, crs=4326, nodata=-1000) + + if params.get("min_depth"): + func = functools.partial(np.greater, -params["min_depth"]) + excluder.add_raster( + snakemake.input.gebco, codes=func, crs=4326, nodata=-1000, invert=True + ) + + if "min_shore_distance" in params: + buffer = params["min_shore_distance"] + excluder.add_geometry(snakemake.input.country_shapes, buffer=buffer) + + if "max_shore_distance" in params: + buffer = params["max_shore_distance"] + excluder.add_geometry( + snakemake.input.country_shapes, buffer=buffer, invert=True + ) + + logger.info(f"Calculate landuse availability for {technology}...") + start = time.time() + + kwargs = dict(nprocesses=nprocesses, disable_progressbar=noprogress) + availability = cutout.availabilitymatrix(regions, excluder, **kwargs) + + duration = time.time() - start + logger.info( + f"Completed landuse availability calculation for {technology} ({duration:2.2f}s)" + ) + + # For Moldova and Ukraine: Overwrite parts not covered by Corine with + # externally determined available areas + if "availability_matrix_MD_UA" in snakemake.input.keys(): + availability_MDUA = xr.open_dataarray( + snakemake.input["availability_matrix_MD_UA"] + ) + availability.loc[availability_MDUA.coords] = availability_MDUA + + availability.to_netcdf(snakemake.output[0]) diff --git a/scripts/determine_availability_matrix_MD_UA.py b/scripts/determine_availability_matrix_MD_UA.py index 0e7962ab4..424f19373 100644 --- a/scripts/determine_availability_matrix_MD_UA.py +++ b/scripts/determine_availability_matrix_MD_UA.py @@ -34,21 +34,21 @@ def get_wdpa_layer_name(wdpa_fn, layer_substring): from _helpers import mock_snakemake snakemake = mock_snakemake( - "determine_availability_matrix_MD_UA", technology="solar" + "determine_availability_matrix_MD_UA", clusters=100, technology="solar" ) configure_logging(snakemake) set_scenario_config(snakemake) nprocesses = int(snakemake.threads) noprogress = not snakemake.config["atlite"].get("show_progress", True) - config = snakemake.config["renewable"][snakemake.wildcards.technology] + config = snakemake.params["renewable"][snakemake.wildcards.technology] cutout = atlite.Cutout(snakemake.input.cutout) regions = ( gpd.read_file(snakemake.input.regions).set_index("name").rename_axis("bus") ) # Limit to "UA" and "MD" regions - buses = regions.loc[regions["country"].isin(["UA", "MD"])].index.values + buses = regions.filter(regex="(UA|MD)", axis=0).index.values regions = regions.loc[buses] excluder = atlite.ExclusionContainer(crs=3035, res=100) @@ -125,24 +125,24 @@ def get_wdpa_layer_name(wdpa_fn, layer_substring): time.sleep(1) excluder.add_geometry(pts_tmp_fn) - if "max_depth" in config: + if config.get("max_depth"): # lambda not supported for atlite + multiprocessing # use named function np.greater with partially frozen argument instead # and exclude areas where: -max_depth > grid cell depth func = functools.partial(np.greater, -config["max_depth"]) excluder.add_raster(snakemake.input.gebco, codes=func, crs=4236, nodata=-1000) - if "min_shore_distance" in config: + if config.get("min_shore_distance"): buffer = config["min_shore_distance"] excluder.add_geometry(snakemake.input.country_shapes, buffer=buffer) - if "max_shore_distance" in config: + if config.get("max_shore_distance"): buffer = config["max_shore_distance"] excluder.add_geometry( snakemake.input.country_shapes, buffer=buffer, invert=True ) - if "ship_threshold" in config: + if config.get("ship_threshold"): shipping_threshold = config["ship_threshold"] * 8760 * 6 func = functools.partial(np.less, shipping_threshold) excluder.add_raster( diff --git a/scripts/make_summary.py b/scripts/make_summary.py index 5746697be..1f92b91aa 100644 --- a/scripts/make_summary.py +++ b/scripts/make_summary.py @@ -761,8 +761,7 @@ def to_csv(df): networks_dict = { (cluster, ll, opt + sector_opt, planning_horizon): "results/" + snakemake.params.RDIR - + f"/postnetworks/elec_s{simpl}_{cluster}_l{ll}_{opt}_{sector_opt}_{planning_horizon}.nc" - for simpl in snakemake.params.scenario["simpl"] + + f"/postnetworks/base_s_{cluster}_l{ll}_{opt}_{sector_opt}_{planning_horizon}.nc" for cluster in snakemake.params.scenario["clusters"] for opt in snakemake.params.scenario["opts"] for sector_opt in snakemake.params.scenario["sector_opts"] diff --git a/scripts/make_summary_perfect.py b/scripts/make_summary_perfect.py index 59aa6e741..842eb000c 100644 --- a/scripts/make_summary_perfect.py +++ b/scripts/make_summary_perfect.py @@ -732,8 +732,7 @@ def to_csv(df): networks_dict = { (clusters, lv, opts + sector_opts): "results/" + run - + f"postnetworks/elec_s{simpl}_{clusters}_l{lv}_{opts}_{sector_opts}_brownfield_all_years.nc" - for simpl in snakemake.config["scenario"]["simpl"] + + f"postnetworks/base_s_{clusters}_l{lv}_{opts}_{sector_opts}_brownfield_all_years.nc" for clusters in snakemake.config["scenario"]["clusters"] for opts in snakemake.config["scenario"]["opts"] for sector_opts in snakemake.config["scenario"]["sector_opts"] diff --git a/scripts/plot_gas_network.py b/scripts/plot_gas_network.py index 26186d510..7f3fe86cd 100644 --- a/scripts/plot_gas_network.py +++ b/scripts/plot_gas_network.py @@ -229,7 +229,6 @@ def plot_ch4_map(n): snakemake = mock_snakemake( "plot_gas_network", - simpl="", opts="", clusters="37", ll="v1.0", diff --git a/scripts/plot_hydrogen_network.py b/scripts/plot_hydrogen_network.py index 6d666acda..57850116b 100644 --- a/scripts/plot_hydrogen_network.py +++ b/scripts/plot_hydrogen_network.py @@ -256,7 +256,6 @@ def plot_h2_map(n, regions): snakemake = mock_snakemake( "plot_hydrogen_network", - simpl="", opts="", clusters="37", ll="v1.0", diff --git a/scripts/plot_power_network.py b/scripts/plot_power_network.py index 6db53bcca..d358229b6 100644 --- a/scripts/plot_power_network.py +++ b/scripts/plot_power_network.py @@ -249,7 +249,6 @@ def plot_map( snakemake = mock_snakemake( "plot_power_network", - simpl="", opts="", clusters="37", ll="v1.0", diff --git a/scripts/plot_power_network_perfect.py b/scripts/plot_power_network_perfect.py index f7506a00c..7d2252be8 100644 --- a/scripts/plot_power_network_perfect.py +++ b/scripts/plot_power_network_perfect.py @@ -176,7 +176,6 @@ def plot_map_perfect( snakemake = mock_snakemake( "plot_power_network_perfect", - simpl="", opts="", clusters="37", ll="v1.0", diff --git a/scripts/plot_statistics.py b/scripts/plot_statistics.py index 738fa618b..cc98a5364 100644 --- a/scripts/plot_statistics.py +++ b/scripts/plot_statistics.py @@ -18,7 +18,6 @@ snakemake = mock_snakemake( "plot_elec_statistics", - simpl="", opts="Ept-12h", clusters="37", ll="v1.0", diff --git a/scripts/plot_validation_cross_border_flows.py b/scripts/plot_validation_cross_border_flows.py index 8de7d8a18..9107726b4 100644 --- a/scripts/plot_validation_cross_border_flows.py +++ b/scripts/plot_validation_cross_border_flows.py @@ -181,7 +181,6 @@ def cross_border_bar(countries, data): snakemake = mock_snakemake( "plot_electricity_prices", - simpl="", opts="Ept-12h", clusters="37", ll="v1.0", diff --git a/scripts/plot_validation_electricity_prices.py b/scripts/plot_validation_electricity_prices.py index 9efd6c469..b210086dc 100644 --- a/scripts/plot_validation_electricity_prices.py +++ b/scripts/plot_validation_electricity_prices.py @@ -18,7 +18,6 @@ snakemake = mock_snakemake( "plot_electricity_prices", - simpl="", opts="Ept-12h", clusters="37", ll="v1.0", diff --git a/scripts/plot_validation_electricity_production.py b/scripts/plot_validation_electricity_production.py index f842bea30..6336b7a74 100644 --- a/scripts/plot_validation_electricity_production.py +++ b/scripts/plot_validation_electricity_production.py @@ -29,7 +29,6 @@ snakemake = mock_snakemake( "plot_validation_electricity_production", - simpl="", opts="Ept", clusters="37c", ll="v1.0", diff --git a/scripts/prepare_network.py b/scripts/prepare_network.py index 05f407304..bf36c0898 100755 --- a/scripts/prepare_network.py +++ b/scripts/prepare_network.py @@ -41,12 +41,12 @@ ------ - ``resources/costs.csv``: The database of cost assumptions for all included technologies for specific years from various sources; e.g. discount rate, lifetime, investment (CAPEX), fixed operation and maintenance (FOM), variable operation and maintenance (VOM), fuel costs, efficiency, carbon-dioxide intensity. -- ``networks/elec_s{simpl}_{clusters}.nc``: confer :ref:`cluster` +- ``networks/base_s_{clusters}.nc``: confer :ref:`cluster` Outputs ------- -- ``networks/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc``: Complete PyPSA network that will be handed to the ``solve_network`` rule. +- ``networks/base_s_{clusters}_elec_l{ll}_{opts}.nc``: Complete PyPSA network that will be handed to the ``solve_network`` rule. Description ----------- @@ -68,7 +68,7 @@ set_scenario_config, update_config_from_wildcards, ) -from add_electricity import load_costs, update_transmission_costs +from add_electricity import load_costs, set_transmission_costs from pypsa.descriptors import expand_series idx = pd.IndexSlice @@ -191,7 +191,7 @@ def set_transmission_limit(n, ll_type, factor, costs, Nyears=1): + n.links.loc[links_dc_b, "p_nom"] @ n.links.loc[links_dc_b, col] ) - update_transmission_costs(n, costs) + set_transmission_costs(n, costs) if factor == "opt" or float(factor) > 1.0: n.lines["s_nom_min"] = lines_s_nom @@ -325,7 +325,6 @@ def set_line_nom_max( snakemake = mock_snakemake( "prepare_network", - simpl="", clusters="37", ll="v1.0", opts="Co2L-4H", diff --git a/scripts/prepare_perfect_foresight.py b/scripts/prepare_perfect_foresight.py index efc957005..f35019138 100644 --- a/scripts/prepare_perfect_foresight.py +++ b/scripts/prepare_perfect_foresight.py @@ -493,7 +493,6 @@ def apply_time_segmentation_perfect( snakemake = mock_snakemake( "prepare_perfect_foresight", - simpl="", opts="", clusters="37", ll="v1.5", diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 4c7c059a7..0821a4f0b 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -407,12 +407,20 @@ def make_index(c): return topo -# TODO merge issue with PyPSA-Eur -def update_wind_solar_costs(n, costs): +def update_wind_solar_costs( + n: pypsa.Network, + costs: pd.DataFrame, + line_length_factor: int | float = 1, + landfall_lengths: dict = None, +) -> None: """ Update costs for wind and solar generators added with pypsa-eur to those cost in the planning year. """ + + if landfall_lengths is None: + landfall_lengths = {} + # NB: solar costs are also manipulated for rooftop # when distribution grid is inserted n.generators.loc[n.generators.carrier == "solar", "capital_cost"] = costs.at[ @@ -424,22 +432,9 @@ def update_wind_solar_costs(n, costs): ] # for offshore wind, need to calculated connection costs - - # assign clustered bus - # map initial network -> simplified network - busmap_s = pd.read_csv(snakemake.input.busmap_s, index_col=0).squeeze() - busmap_s.index = busmap_s.index.astype(str) - busmap_s = busmap_s.astype(str) - # map simplified network -> clustered network - busmap = pd.read_csv(snakemake.input.busmap, index_col=0).squeeze() - busmap.index = busmap.index.astype(str) - busmap = busmap.astype(str) - # map initial network -> clustered network - clustermaps = busmap_s.map(busmap) - - # code adapted from pypsa-eur/scripts/add_electricity.py for connection in ["dc", "ac", "float"]: tech = "offwind-" + connection + landfall_length = landfall_lengths.get(tech, 0.0) if tech not in n.generators.carrier.values: continue profile = snakemake.input["profile_offwind-" + connection] @@ -449,30 +444,12 @@ def update_wind_solar_costs(n, costs): if "year" in ds.indexes: ds = ds.sel(year=ds.year.min(), drop=True) - underwater_fraction = ds["underwater_fraction"].to_pandas() - connection_cost = ( - snakemake.params.length_factor - * ds["average_distance"].to_pandas() - * ( - underwater_fraction - * costs.at[tech + "-connection-submarine", "fixed"] - + (1.0 - underwater_fraction) - * costs.at[tech + "-connection-underground", "fixed"] - ) - ) - - # convert to aggregated clusters with weighting - weight = ds["weight"].to_pandas() - - # e.g. clusters == 37m means that VRE generators are left - # at clustering of simplified network, but that they are - # connected to 37-node network - genmap = ( - busmap_s if snakemake.wildcards.clusters[-1:] == "m" else clustermaps + distance = ds["average_distance"].to_pandas() + submarine_cost = costs.at[tech + "-connection-submarine", "fixed"] + underground_cost = costs.at[tech + "-connection-underground", "fixed"] + connection_cost = line_length_factor * ( + distance * submarine_cost + landfall_length * underground_cost ) - connection_cost = (connection_cost * weight).groupby( - genmap - ).sum() / weight.groupby(genmap).sum() capital_cost = ( costs.at["offwind", "fixed"] @@ -613,10 +590,10 @@ def remove_non_electric_buses(n): n.buses = n.buses[n.buses.carrier.isin(["AC", "DC"])] -def patch_electricity_network(n): +def patch_electricity_network(n, costs, landfall_lengths): remove_elec_base_techs(n) remove_non_electric_buses(n) - update_wind_solar_costs(n, costs) + update_wind_solar_costs(n, costs, landfall_lengths=landfall_lengths) n.loads["carrier"] = "electricity" n.buses["location"] = n.buses.index n.buses["unit"] = "MWh_el" @@ -1340,13 +1317,7 @@ def insert_electricity_distribution_grid(n, costs): # set existing solar to cost of utility cost rather the 50-50 rooftop-utility solar = n.generators.index[n.generators.carrier == "solar"] n.generators.loc[solar, "capital_cost"] = costs.at["solar-utility", "fixed"] - if snakemake.wildcards.clusters[-1:] == "m": - simplified_pop_layout = pd.read_csv( - snakemake.input.simplified_pop_layout, index_col=0 - ) - pop_solar = simplified_pop_layout.total.rename(index=lambda x: x + " solar") - else: - pop_solar = pop_layout.total.rename(index=lambda x: x + " solar") + pop_solar = pop_layout.total.rename(index=lambda x: x + " solar") # add max solar rooftop potential assuming 0.1 kW/m2 and 20 m2/person, # i.e. 2 kW/person (population data is in thousands of people) so we get MW @@ -4621,7 +4592,6 @@ def add_enhanced_geothermal(n, egs_potentials, egs_overlap, costs): snakemake = mock_snakemake( "prepare_sector_network", - simpl="", opts="", clusters="38", ll="vopt", @@ -4658,12 +4628,17 @@ def add_enhanced_geothermal(n, egs_potentials, egs_overlap, costs): ) pop_weighted_energy_totals.update(pop_weighted_heat_totals) + landfall_lengths = { + tech: settings["landfall_length"] + for tech, settings in snakemake.params.renewable.items() + if "landfall_length" in settings.keys() + } + patch_electricity_network(n, costs, landfall_lengths) + fn = snakemake.input.heating_efficiencies year = int(snakemake.params["energy_totals_year"]) heating_efficiencies = pd.read_csv(fn, index_col=[1, 0]).loc[year] - patch_electricity_network(n) - spatial = define_spatial(pop_layout.index, options) if snakemake.params.foresight in ["myopic", "perfect"]: diff --git a/scripts/simplify_network.py b/scripts/simplify_network.py index e95891203..343091505 100644 --- a/scripts/simplify_network.py +++ b/scripts/simplify_network.py @@ -19,96 +19,74 @@ cluster_network: aggregation_strategies: - costs: - year: - version: - fill_values: - marginal_cost: - capital_cost: - - electricity: - max_hours: - - lines: - length_factor: - links: p_max_pu: - solving: - solver: - name: .. seealso:: Documentation of the configuration file ``config/config.yaml`` at - :ref:`costs_cf`, :ref:`electricity_cf`, :ref:`renewable_cf`, - :ref:`lines_cf`, :ref:`links_cf`, :ref:`solving_cf` + :ref:`electricity_cf`, :ref:`renewable_cf`, + :ref:`lines_cf`, :ref:`links_cf` Inputs ------ -- ``resources/costs.csv``: The database of cost assumptions for all included technologies for specific years from various sources; e.g. discount rate, lifetime, investment (CAPEX), fixed operation and maintenance (FOM), variable operation and maintenance (VOM), fuel costs, efficiency, carbon-dioxide intensity. - ``resources/regions_onshore.geojson``: confer :ref:`busregions` - ``resources/regions_offshore.geojson``: confer :ref:`busregions` -- ``networks/elec.nc``: confer :ref:`electricity` +- ``networks/base.nc`` Outputs ------- -- ``resources/regions_onshore_elec_s{simpl}.geojson``: +- ``resources/regions_onshore_base.geojson``: - .. image:: img/regions_onshore_elec_s.png + .. image:: img/regions_onshore_base_s.png :scale: 33 % -- ``resources/regions_offshore_elec_s{simpl}.geojson``: +- ``resources/regions_offshore_base.geojson``: - .. image:: img/regions_offshore_elec_s .png + .. image:: img/regions_offshore_base_s .png :scale: 33 % -- ``resources/busmap_elec_s{simpl}.csv``: Mapping of buses from ``networks/elec.nc`` to ``networks/elec_s{simpl}.nc``; -- ``networks/elec_s{simpl}.nc``: +- ``resources/busmap_base_s.csv``: Mapping of buses from ``networks/base.nc`` to ``networks/base_s.nc``; +- ``networks/base.nc``: - .. image:: img/elec_s.png + .. image:: img/base_s.png :scale: 33 % Description ----------- -The rule :mod:`simplify_network` does up to four things: +The rule :mod:`simplify_network` does up to three things: 1. Create an equivalent transmission network in which all voltage levels are mapped to the 380 kV level by the function ``simplify_network(...)``. -2. DC only sub-networks that are connected at only two buses to the AC network are reduced to a single representative link in the function ``simplify_links(...)``. The components attached to buses in between are moved to the nearest endpoint. The grid connection cost of offshore wind generators are added to the capital costs of the generator. +2. DC only sub-networks that are connected at only two buses to the AC network are reduced to a single representative link in the function ``simplify_links(...)``. -3. Stub lines and links, i.e. dead-ends of the network, are sequentially removed from the network in the function ``remove_stubs(...)``. Components are moved along. - -4. Optionally, if an integer were provided for the wildcard ``{simpl}`` (e.g. ``networks/elec_s500.nc``), the network is clustered to this number of clusters with the routines from the ``cluster_network`` rule with the function ``cluster_network.cluster(...)``. This step is usually skipped! +3. Stub lines and links, i.e. dead-ends of the network, are sequentially removed from the network in the function ``remove_stubs(...)``. """ import logging from functools import reduce +from typing import Tuple import geopandas as gpd import numpy as np import pandas as pd import pypsa import scipy as sp -from _helpers import configure_logging, set_scenario_config, update_p_nom_max -from add_electricity import load_costs +from _helpers import configure_logging, set_scenario_config from base_network import append_bus_shapes -from cluster_network import cluster_regions, clustering_for_n_clusters -from pypsa.clustering.spatial import ( - aggregateoneport, - busmap_by_stubs, - get_clustering_from_busmap, -) -from pypsa.io import import_components_from_dataframe, import_series_from_dataframe +from cluster_network import cluster_regions +from pypsa.clustering.spatial import busmap_by_stubs, get_clustering_from_busmap from scipy.sparse.csgraph import connected_components, dijkstra logger = logging.getLogger(__name__) -def simplify_network_to_380(n, linetype_380): +def simplify_network_to_380( + n: pypsa.Network, linetype_380: str +) -> Tuple[pypsa.Network, pd.Series]: """ Fix all lines to a voltage level of 380 kV and remove all transformers. @@ -149,123 +127,7 @@ def simplify_network_to_380(n, linetype_380): return n, trafo_map -def _prepare_connection_costs_per_link(n, costs, renewable_carriers, length_factor): - if n.links.empty: - return {} - - return { - tech: ( - n.links.length - * length_factor - * ( - n.links.underwater_fraction - * costs.at[tech + "-connection-submarine", "capital_cost"] - + (1.0 - n.links.underwater_fraction) - * costs.at[tech + "-connection-underground", "capital_cost"] - ) - ) - for tech in renewable_carriers - if tech.startswith("offwind") - } - - -def _compute_connection_costs_to_bus( - n, - busmap, - costs, - renewable_carriers, - length_factor, - connection_costs_per_link=None, - buses=None, -): - if connection_costs_per_link is None: - connection_costs_per_link = _prepare_connection_costs_per_link( - n, costs, renewable_carriers, length_factor - ) - - if buses is None: - buses = busmap.index[busmap.index != busmap.values] - - connection_costs_to_bus = pd.DataFrame(index=buses) - - for tech in connection_costs_per_link: - adj = n.adjacency_matrix( - weights=pd.concat( - dict( - Link=connection_costs_per_link[tech].reindex(n.links.index), - Line=pd.Series(0.0, n.lines.index), - ) - ) - ) - - costs_between_buses = dijkstra( - adj, directed=False, indices=n.buses.index.get_indexer(buses) - ) - connection_costs_to_bus[tech] = costs_between_buses[ - np.arange(len(buses)), n.buses.index.get_indexer(busmap.loc[buses]) - ] - - return connection_costs_to_bus - - -def _adjust_capital_costs_using_connection_costs(n, connection_costs_to_bus): - connection_costs = {} - for tech in connection_costs_to_bus: - tech_b = n.generators.carrier == tech - costs = ( - n.generators.loc[tech_b, "bus"] - .map(connection_costs_to_bus[tech]) - .loc[lambda s: s > 0] - ) - if not costs.empty: - n.generators.loc[costs.index, "capital_cost"] += costs - logger.info( - "Displacing {} generator(s) and adding connection costs to capital_costs: {} ".format( - tech, - ", ".join( - "{:.0f} Eur/MW/a for `{}`".format(d, b) - for b, d in costs.items() - ), - ) - ) - connection_costs[tech] = costs - - -def _aggregate_and_move_components( - n, - busmap, - connection_costs_to_bus, - aggregate_one_ports={"Load", "StorageUnit"}, - aggregation_strategies=dict(), - exclude_carriers=None, -): - def replace_components(n, c, df, pnl): - n.mremove(c, n.df(c).index) - - import_components_from_dataframe(n, df, c) - for attr, df in pnl.items(): - if not df.empty: - import_series_from_dataframe(n, df, c, attr) - - _adjust_capital_costs_using_connection_costs(n, connection_costs_to_bus) - - generator_strategies = aggregation_strategies["generators"] - - carriers = set(n.generators.carrier) - set(exclude_carriers) - generators, generators_pnl = aggregateoneport( - n, - busmap, - "Generator", - carriers=carriers, - custom_strategies=generator_strategies, - ) - - replace_components(n, "Generator", generators, generators_pnl) - - for one_port in aggregate_one_ports: - df, pnl = aggregateoneport(n, busmap, component=one_port) - replace_components(n, one_port, df, pnl) - +def _remove_clustered_buses_and_branches(n: pypsa.Network, busmap: pd.Series) -> None: buses_to_del = n.buses.index.difference(busmap) n.mremove("Bus", buses_to_del) for c in n.branch_components: @@ -274,14 +136,8 @@ def replace_components(n, c, df, pnl): def simplify_links( - n, - costs, - renewables, - length_factor, - p_max_pu, - exclude_carriers, - aggregation_strategies=dict(), -): + n: pypsa.Network, p_max_pu: int | float +) -> Tuple[pypsa.Network, pd.Series]: ## Complex multi-node links are folded into end-points logger.info("Simplifying connected link components") @@ -343,13 +199,6 @@ def split_links(nodes, added_supernodes): busmap = n.buses.index.to_series() - connection_costs_per_link = _prepare_connection_costs_per_link( - n, costs, renewables, length_factor - ) - connection_costs_to_bus = pd.DataFrame( - 0.0, index=n.buses.index, columns=list(connection_costs_per_link) - ) - node_corsica = find_closest_bus( n, x=9.44802, @@ -375,15 +224,6 @@ def split_links(nodes, added_supernodes): n.buses.loc[b, ["x", "y"]], n.buses.loc[buses[1:-1], ["x", "y"]] ) busmap.loc[buses] = b[np.r_[0, m.argmin(axis=0), 1]] - connection_costs_to_bus.loc[buses] += _compute_connection_costs_to_bus( - n, - busmap, - costs, - renewables, - length_factor, - connection_costs_per_link, - buses, - ) all_links = [i for _, i in sum(links, [])] @@ -421,61 +261,41 @@ def split_links(nodes, added_supernodes): params.setdefault(attr, default) n.links.loc[name] = pd.Series(params) - # n.add("Link", **params) + # n.add("Link", name, **params) logger.debug("Collecting all components using the busmap") + _remove_clustered_buses_and_branches(n, busmap) + # Change carrier type of all added super_nodes to "AC" n.buses.loc[added_supernodes, "carrier"] = "AC" - _aggregate_and_move_components( - n, - busmap, - connection_costs_to_bus, - aggregation_strategies=aggregation_strategies, - exclude_carriers=exclude_carriers, - ) return n, busmap def remove_stubs( - n, - costs, - renewable_carriers, - length_factor, - simplify_network, - aggregation_strategies=dict(), -): + n: pypsa.Network, simplify_network: dict +) -> Tuple[pypsa.Network, pd.Series]: logger.info("Removing stubs") across_borders = simplify_network["remove_stubs_across_borders"] matching_attrs = [] if across_borders else ["country"] busmap = busmap_by_stubs(n, matching_attrs) - connection_costs_to_bus = _compute_connection_costs_to_bus( - n, busmap, costs, renewable_carriers, length_factor - ) - - _aggregate_and_move_components( - n, - busmap, - connection_costs_to_bus, - aggregation_strategies=aggregation_strategies, - exclude_carriers=simplify_network["exclude_carriers"], - ) + _remove_clustered_buses_and_branches(n, busmap) return n, busmap -def aggregate_to_substations(n, aggregation_strategies=dict(), buses_i=None): +def aggregate_to_substations( + n: pypsa.Network, + buses_i: pd.Index | list, + aggregation_strategies: dict | None = None, +) -> Tuple[pypsa.Network, pd.Series]: # can be used to aggregate a selection of buses to electrically closest neighbors - # if no buses are given, nodes that are no substations or without offshore connection are aggregated - - if buses_i is None: - logger.info( - "Aggregating buses that are no substations or have no valid offshore connection" - ) - buses_i = list(set(n.buses.index) - set(n.generators.bus) - set(n.loads.bus)) + logger.info("Aggregating buses to substations") + if aggregation_strategies is None: + aggregation_strategies = dict() weight = pd.concat( { @@ -503,49 +323,21 @@ def aggregate_to_substations(n, aggregation_strategies=dict(), buses_i=None): busmap.loc[buses_i] = dist.idxmin(1) line_strategies = aggregation_strategies.get("lines", dict()) - generator_strategies = aggregation_strategies.get("generators", dict()) - one_port_strategies = aggregation_strategies.get("one_ports", dict()) + + bus_strategies = aggregation_strategies.get("buses", dict()) + bus_strategies.setdefault("substation_lv", lambda x: bool(x.sum())) + bus_strategies.setdefault("substation_off", lambda x: bool(x.sum())) clustering = get_clustering_from_busmap( n, busmap, - aggregate_generators_weighted=True, - aggregate_generators_carriers=None, - aggregate_one_ports=["Load", "StorageUnit"], line_length_factor=1.0, + bus_strategies=bus_strategies, line_strategies=line_strategies, - generator_strategies=generator_strategies, - one_port_strategies=one_port_strategies, - scale_link_capital_costs=False, ) return clustering.network, busmap -def cluster( - n, - n_clusters, - focus_weights, - solver_name, - algorithm="hac", - feature=None, - aggregation_strategies=dict(), -): - logger.info(f"Clustering to {n_clusters} buses") - - clustering = clustering_for_n_clusters( - n, - n_clusters, - custom_busmap=False, - aggregation_strategies=aggregation_strategies, - solver_name=solver_name, - algorithm=algorithm, - feature=feature, - focus_weights=focus_weights, - ) - - return clustering.network, clustering.busmap - - def find_closest_bus(n, x, y, tol=2000): """ Find the index of the closest bus to the given coordinates within a specified tolerance. @@ -586,71 +378,28 @@ def find_closest_bus(n, x, y, tol=2000): if "snakemake" not in globals(): from _helpers import mock_snakemake - snakemake = mock_snakemake("simplify_network", simpl="", run="all") + snakemake = mock_snakemake("simplify_network") configure_logging(snakemake) set_scenario_config(snakemake) params = snakemake.params - solver_name = snakemake.config["solving"]["solver"]["name"] n = pypsa.Network(snakemake.input.network) Nyears = n.snapshot_weightings.objective.sum() / 8760 - - # remove integer outputs for compatibility with PyPSA v0.26.0 - n.generators.drop("n_mod", axis=1, inplace=True, errors="ignore") + buses_prev, lines_prev, links_prev = len(n.buses), len(n.lines), len(n.links) linetype_380 = snakemake.config["lines"]["types"][380] n, trafo_map = simplify_network_to_380(n, linetype_380) - technology_costs = load_costs( - snakemake.input.tech_costs, - params.costs, - params.max_hours, - Nyears, - ) - - n, simplify_links_map = simplify_links( - n, - technology_costs, - params.renewable_carriers, - params.length_factor, - params.p_max_pu, - params.simplify_network["exclude_carriers"], - params.aggregation_strategies, - ) + n, simplify_links_map = simplify_links(n, params.p_max_pu) busmaps = [trafo_map, simplify_links_map] if params.simplify_network["remove_stubs"]: - n, stub_map = remove_stubs( - n, - technology_costs, - params.renewable_carriers, - params.length_factor, - params.simplify_network, - aggregation_strategies=params.aggregation_strategies, - ) + n, stub_map = remove_stubs(n, params.simplify_network) busmaps.append(stub_map) - if params.simplify_network["to_substations"]: - n, substation_map = aggregate_to_substations(n, params.aggregation_strategies) - busmaps.append(substation_map) - - # treatment of outliers (nodes without a profile for considered carrier): - # all nodes that have no profile of the given carrier are being aggregated to closest neighbor - if params.simplify_network["algorithm"] == "hac": - carriers = params.simplify_network["feature"].split("-")[0].split("+") - for carrier in carriers: - buses_i = list( - set(n.buses.index) - set(n.generators.query("carrier == @carrier").bus) - ) - logger.info( - f"clustering preparation (hac): aggregating {len(buses_i)} buses of type {carrier}." - ) - n, busmap_hac = aggregate_to_substations( - n, params.aggregation_strategies, buses_i - ) - busmaps.append(busmap_hac) + substations_i = n.buses.query("substation_lv or substation_off").index # some entries in n.buses are not updated in previous functions, therefore can be wrong. as they are not needed # and are lost when clustering (for example with the simpl wildcard), we remove them for consistency: @@ -659,8 +408,6 @@ def find_closest_bus(n, x, y, tol=2000): "tags", "under_construction", "onshore_bus", - "substation_lv", - "substation_off", "geometry", "underground", "project_status", @@ -668,30 +415,39 @@ def find_closest_bus(n, x, y, tol=2000): n.buses.drop(remove, axis=1, inplace=True, errors="ignore") n.lines.drop(remove, axis=1, errors="ignore", inplace=True) - if snakemake.wildcards.simpl: - # shapes = n.shapes - n, cluster_map = cluster( - n, - int(snakemake.wildcards.simpl), - params.focus_weights, - solver_name, - params.simplify_network["algorithm"], - params.simplify_network["feature"], - params.aggregation_strategies, + if params.simplify_network["to_substations"]: + n, substation_map = aggregate_to_substations( + n, substations_i, params.aggregation_strategies ) - # n.shapes = shapes - busmaps.append(cluster_map) + busmaps.append(substation_map) - update_p_nom_max(n) + # all buses without shapes need to be clustered to their closest neighbor for HAC + if params.cluster_network["algorithm"] == "hac": + buses_i = list(n.buses.index.difference(n.shapes.idx)) + logger.info( + "Preparing for HAC-Clustering. " + f"Aggregating {len(buses_i)} buses without Voronoi shapes to closest neighbor." + ) + n, busmap_hac = aggregate_to_substations( + n, buses_i, params.aggregation_strategies + ) + busmaps.append(busmap_hac) busmap_s = reduce(lambda x, y: x.map(y), busmaps[1:], busmaps[0]) busmap_s.to_csv(snakemake.output.busmap) for which in ["regions_onshore", "regions_offshore"]: regions = gpd.read_file(snakemake.input[which]) - clustered_regions = cluster_regions(busmaps, regions) + clustered_regions = cluster_regions(busmaps, regions, with_country=True) clustered_regions.to_file(snakemake.output[which]) # append_bus_shapes(n, clustered_regions, type=which.split("_")[1]) n.meta = dict(snakemake.config, **dict(wildcards=dict(snakemake.wildcards))) n.export_to_netcdf(snakemake.output.network) + + logger.info( + f"Simplified network:\n" + f"Buses: {buses_prev} to {len(n.buses)}\n" + f"Lines: {lines_prev} to {len(n.lines)}\n" + f"Links: {links_prev} to {len(n.links)}" + ) diff --git a/scripts/solve_network.py b/scripts/solve_network.py index bdc10dd18..0cbc0ec11 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -51,13 +51,6 @@ pypsa.pf.logger.setLevel(logging.WARNING) -def add_land_use_constraint(n, planning_horizons, config): - if "m" in snakemake.wildcards.clusters: - _add_land_use_constraint_m(n, planning_horizons, config) - else: - _add_land_use_constraint(n) - - def add_land_use_constraint_perfect(n): """ Add global constraints for tech capacity limit. @@ -121,7 +114,7 @@ def check_p_min_p_max(p_nom_max): return n -def _add_land_use_constraint(n): +def add_land_use_constraint(n): # warning: this will miss existing offwind which is not classed AC-DC and has carrier 'offwind' for carrier in [ @@ -159,58 +152,6 @@ def _add_land_use_constraint(n): n.generators["p_nom_max"] = n.generators["p_nom_max"].clip(lower=0) -def _add_land_use_constraint_m(n, planning_horizons, config): - # if generators clustering is lower than network clustering, land_use accounting is at generators clusters - - grouping_years = config["existing_capacities"]["grouping_years_power"] - current_horizon = snakemake.wildcards.planning_horizons - - for carrier in [ - "solar", - "solar rooftop", - "solar-hsat", - "onwind", - "offwind-ac", - "offwind-dc", - ]: - - existing = n.generators.loc[n.generators.carrier == carrier, "p_nom"] - ind = list( - {i.split(sep=" ")[0] + " " + i.split(sep=" ")[1] for i in existing.index} - ) - - previous_years = [ - str(y) - for y in set(planning_horizons + grouping_years) - if y < int(snakemake.wildcards.planning_horizons) - ] - - for p_year in previous_years: - ind2 = [ - i for i in ind if i + " " + carrier + "-" + p_year in existing.index - ] - sel_current = [i + " " + carrier + "-" + current_horizon for i in ind2] - sel_p_year = [i + " " + carrier + "-" + p_year for i in ind2] - n.generators.loc[sel_current, "p_nom_max"] -= existing.loc[ - sel_p_year - ].rename(lambda x: x[:-4] + current_horizon) - - # check if existing capacities are larger than technical potential - existing_large = n.generators[ - n.generators["p_nom_min"] > n.generators["p_nom_max"] - ].index - if len(existing_large): - logger.warning( - f"Existing capacities larger than technical potential for {existing_large},\ - adjust technical potential to existing capacities" - ) - n.generators.loc[existing_large, "p_nom_max"] = n.generators.loc[ - existing_large, "p_nom_min" - ] - - n.generators["p_nom_max"] = n.generators["p_nom_max"].clip(lower=0) - - def add_solar_potential_constraints(n, config): """ Add constraint to make sure the sum capacity of all solar technologies (fixed, tracking, ets. ) is below the region potential. @@ -246,37 +187,17 @@ def add_solar_potential_constraints(n, config): lambda x: (x * factor) if carrier in x.name else x, axis=1 ) - if "m" in snakemake.wildcards.clusters: - location = pd.Series( - [" ".join(i.split(" ")[:2]) for i in n.generators.index], - index=n.generators.index, - ) - ggrouper = pd.Series( - n.generators.loc[solar].index.rename("bus").map(location), - index=n.generators.loc[solar].index, - ).to_xarray() - rhs = ( - n.generators.loc[solar_today, "p_nom_max"] - .groupby(n.generators.loc[solar_today].index.rename("bus").map(location)) - .sum() - - n.generators.loc[solar_hsat, "p_nom_opt"] - .groupby(n.generators.loc[solar_hsat].index.rename("bus").map(location)) - .sum() - * land_use_factors["solar-hsat"] - ).clip(lower=0) - - else: - location = pd.Series(n.buses.index, index=n.buses.index) - ggrouper = n.generators.loc[solar].bus - rhs = ( - n.generators.loc[solar_today, "p_nom_max"] - .groupby(n.generators.loc[solar_today].bus.map(location)) - .sum() - - n.generators.loc[solar_hsat, "p_nom_opt"] - .groupby(n.generators.loc[solar_hsat].bus.map(location)) - .sum() - * land_use_factors["solar-hsat"] - ).clip(lower=0) + location = pd.Series(n.buses.index, index=n.buses.index) + ggrouper = n.generators.loc[solar].bus + rhs = ( + n.generators.loc[solar_today, "p_nom_max"] + .groupby(n.generators.loc[solar_today].bus.map(location)) + .sum() + - n.generators.loc[solar_hsat, "p_nom_opt"] + .groupby(n.generators.loc[solar_hsat].bus.map(location)) + .sum() + * land_use_factors["solar-hsat"] + ).clip(lower=0) lhs = ( (n.model["Generator-p_nom"].rename(rename).loc[solar] * land_use.squeeze()) @@ -515,7 +436,7 @@ def prepare_network( n.snapshot_weightings[:] = 8760.0 / nhours if foresight == "myopic": - add_land_use_constraint(n, planning_horizons, config) + add_land_use_constraint(n) if foresight == "perfect": n = add_land_use_constraint_perfect(n) @@ -1134,7 +1055,6 @@ def solve_network(n, config, params, solving, **kwargs): snakemake = mock_snakemake( "solve_sector_network_perfect", configfiles="../config/test/config.perfect.yaml", - simpl="", opts="", clusters="5", ll="v1.0", diff --git a/scripts/solve_operations_network.py b/scripts/solve_operations_network.py index 4336c3a7e..28e0c1b79 100644 --- a/scripts/solve_operations_network.py +++ b/scripts/solve_operations_network.py @@ -29,7 +29,6 @@ snakemake = mock_snakemake( "solve_operations_network", configfiles="test/config.electricity.yaml", - simpl="", opts="", clusters="5", ll="v1.5", diff --git a/scripts/time_aggregation.py b/scripts/time_aggregation.py index 7ed691126..51ab79545 100644 --- a/scripts/time_aggregation.py +++ b/scripts/time_aggregation.py @@ -20,17 +20,17 @@ Inputs ------ -- ``networks/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc``: the network whose +- ``networks/base_s_{clusters}_elec_l{ll}_{opts}.nc``: the network whose snapshots are to be aggregated -- ``resources/hourly_heat_demand_total_elec_s{simpl}_{clusters}.nc``: the total +- ``resources/hourly_heat_demand_total_base_s_{clusters}.nc``: the total hourly heat demand -- ``resources/solar_thermal_total_elec_s{simpl}_{clusters}.nc``: the total +- ``resources/solar_thermal_total_base_s_{clusters}.nc``: the total hourly solar thermal generation Outputs ------- -- ``snapshot_weightings_elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.csv`` +- ``snapshot_weightings_base_s_{clusters}_elec_l{ll}_{opts}.csv`` Description ----------- @@ -63,7 +63,6 @@ snakemake = mock_snakemake( "time_aggregation", configfiles="test/config.overnight.yaml", - simpl="", opts="", clusters="37", ll="v1.0", diff --git a/test.sh b/test.sh index a3dfb65f6..64566aa2f 100755 --- a/test.sh +++ b/test.sh @@ -7,7 +7,7 @@ set -x && \ snakemake -call solve_elec_networks --configfile config/test/config.electricity.yaml --rerun-triggers=mtime && \ snakemake -call all --configfile config/test/config.overnight.yaml --rerun-triggers=mtime && \ snakemake -call all --configfile config/test/config.myopic.yaml --rerun-triggers=mtime && \ -snakemake -call all --configfile config/test/config.perfect.yaml --rerun-triggers=mtime && \ +snakemake -call make_summary_perfect --configfile config/test/config.perfect.yaml --rerun-triggers=mtime && \ snakemake -call all --configfile config/test/config.scenarios.yaml --rerun-triggers=mtime -n && \ set +x From 0a18ddbb65b205f109a7a98909db86ac977f4024 Mon Sep 17 00:00:00 2001 From: Lukas Trippe Date: Fri, 13 Sep 2024 15:56:29 +0200 Subject: [PATCH 314/344] add validator report (#1295) * ci: add validator * disable dev mode * update version --- .github/workflows/validate.yaml | 46 +++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/workflows/validate.yaml diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml new file mode 100644 index 000000000..9303834b2 --- /dev/null +++ b/.github/workflows/validate.yaml @@ -0,0 +1,46 @@ +name: Validator Bot + +on: + pull_request: + branches: + - master + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + run-validation: + name: Run validation + if: github.event.pull_request.head.repo.full_name == github.repository + runs-on: self-hosted + steps: + - uses: lkstrp/pypsa-validator@v0.2.1 + with: + step: run-self-hosted-validation + env_file: envs/environment.yaml + snakemake_config: config/test/config.validator.yaml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + create-report: + name: Create report + if: github.event.pull_request.head.repo.full_name == github.repository + needs: run-validation + runs-on: ubuntu-latest + steps: + - uses: lkstrp/pypsa-validator@v0.2.1 + with: + step: create-comment + snakemake_config: config/test/config.validator.yaml + # The path starting from prefix in config + # For plot results///.png pass + # /.png + plots: > + " + graphs/energy.svg + graphs/costs.svg + graphs/balances-energy.svg + " + validator_key: ${{ secrets.VALIDATOR_KEY }} From d809cf374abb2687ea32f30663565229305b2780 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 13 Sep 2024 17:39:51 +0200 Subject: [PATCH 315/344] move cluster-first release notes to upcoming release --- doc/release_notes.rst | 113 +++++++++++++++++++++--------------------- 1 file changed, 57 insertions(+), 56 deletions(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 501bb5d8e..8859c41dd 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -11,6 +11,63 @@ Release Notes Upcoming Release ================ + +* Rearranged workflow to cluster the electricity network before calculating + renewable profiles and adding further electricity system components. + + - Moved rules ``simplify_network`` and ``cluster_network`` before + ``add_electricity`` and ``build_renewable_profiles``. + + - Split rule ``build_renewable_profiles`` into two separate rules, + ``determine_availability_matrix`` for land eligibility analysis and + ``build_renewable_profiles``, which now only computes the profiles and total + potentials from the pre-computed availability matrix. + + - Removed variables ``weight``, ``underwater_fraction``, and ``potential`` from the + output of ``build_renewable_profiles`` as it is no longer needed. + + - HAC-clustering is now based on wind speeds and irradiation time series + rather than capacity factors of wind and solar power plants. + + - Added new rule ``build_hac_features`` that aggregates cutout weather data to + base regions in preparation for ``cluster_network``. + + - Removed ``{simpl}`` wildcard and all associated code of the ``m`` suffix of + the ``{cluster}`` wildcard. This means that the option to pre-cluster the + network in ``simplify_network`` was removed. It will be superseded by + clustering renewable profiles and potentials within clustered regions by + resource classes soon. + + - Added new rule ``add_transmission_projects_and_dlr`` which adds the outputs + from ``build_line_rating`` and ``build_transmission_projects`` to the output + of ``base_network``. + + - The rule ``add_extra_components`` was integrated into ``add_electricity`` + + - Added new rule ``build_electricity_demand_base`` to determine the load + distribution of the substations in the base network (which was previously + done in ``add_electricity``). This time series is used as weights for + kmeans-clustering in ``cluster_network`` and is later added to the network in + ``add_electricity`` in aggregated form. + + - The weights of the kmeans clustering algorithm are now exclusively based on + the load distribution. Previously, they also included the distribution of + thermal capacity. + + - Since the networks no longer start with the whole electricity system added + pre-clustering, the files have been renamed from ``elec...nc`` to + ``base...nc`` to identify them as derivatives of ``base.nc``. + + - The scripts ``simplify_network.py`` and ``cluster_network.py`` were + simplified to become less nested and profited from the removed need to deal + with cost data. + + - New configuration options to calculate connection costs of offshore wind + plants. Offshore connection costs are now calculated based on the underwater + distance to the shoreline plus a configurable ``landfall_length`` which + defaults to 10 km. Previously the distance to the region's centroid was + used, which is not practical when the regions are already aggregated. + PyPSA-Eur 0.13.0 (13th September 2024) ====================================== @@ -116,62 +173,6 @@ PyPSA-Eur 0.13.0 (13th September 2024) * The sources of nearly all data files are now listed in the documentation. (https://github.com/PyPSA/pypsa-eur/pull/1284) -* Rearranged workflow to cluster the electricity network before calculating - renewable profiles and adding further electricity system components. - - - Moved rules ``simplify_network`` and ``cluster_network`` before - ``add_electricity`` and ``build_renewable_profiles``. - - - Split rule ``build_renewable_profiles`` into two separate rules, - ``determine_availability_matrix`` for land eligibility analysis and - ``build_renewable_profiles``, which now only computes the profiles and total - potentials from the pre-computed availability matrix. - - - Removed variables ``weight``, ``underwater_fraction``, and ``potential`` from the - output of ``build_renewable_profiles`` as it is no longer needed. - - - HAC-clustering is now based on wind speeds and irradiation time series - rather than capacity factors of wind and solar power plants. - - - Added new rule ``build_hac_features`` that aggregates cutout weather data to - base regions in preparation for ``cluster_network``. - - - Removed ``{simpl}`` wildcard and all associated code of the ``m`` suffix of - the ``{cluster}`` wildcard. This means that the option to pre-cluster the - network in ``simplify_network`` was removed. It will be superseded by - clustering renewable profiles and potentials within clustered regions by - resource classes soon. - - - Added new rule ``add_transmission_projects_and_dlr`` which adds the outputs - from ``build_line_rating`` and ``build_transmission_projects`` to the output - of ``base_network``. - - - The rule ``add_extra_components`` was integrated into ``add_electricity`` - - - Added new rule ``build_electricity_demand_base`` to determine the load - distribution of the substations in the base network (which was previously - done in ``add_electricity``). This time series is used as weights for - kmeans-clustering in ``cluster_network`` and is later added to the network in - ``add_electricity`` in aggregated form. - - - The weights of the kmeans clustering algorithm are now exclusively based on - the load distribution. Previously, they also included the distribution of - thermal capacity. - - - Since the networks no longer start with the whole electricity system added - pre-clustering, the files have been renamed from ``elec...nc`` to - ``base...nc`` to identify them as derivatives of ``base.nc``. - - - The scripts ``simplify_network.py`` and ``cluster_network.py`` were - simplified to become less nested and profited from the removed need to deal - with cost data. - - - New configuration options to calculate connection costs of offshore wind - plants. Offshore connection costs are now calculated based on the underwater - distance to the shoreline plus a configurable ``landfall_length`` which - defaults to 10 km. Previously the distance to the region's centroid was - used, which is not practical when the regions are already aggregated. - PyPSA-Eur 0.12.0 (30th August 2024) =================================== From 5fdbe93f67b5c86fb29e5036145293b461a239cf Mon Sep 17 00:00:00 2001 From: Lukas Trippe Date: Mon, 16 Sep 2024 13:14:41 +0200 Subject: [PATCH 316/344] test: fix and update test workflows (#1296) * test: fix and update test workflows * resolve conflict * bring back test.sh --- .github/workflows/ci.yaml | 95 ------------------------------ .github/workflows/test.yaml | 112 ++++++++++++++++++++++++++++++++++++ README.md | 2 +- doc/index.rst | 2 +- envs/environment.yaml | 7 +-- test.sh | 0 6 files changed, 117 insertions(+), 101 deletions(-) delete mode 100644 .github/workflows/ci.yaml create mode 100644 .github/workflows/test.yaml mode change 100755 => 100644 test.sh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml deleted file mode 100644 index 6664ad902..000000000 --- a/.github/workflows/ci.yaml +++ /dev/null @@ -1,95 +0,0 @@ -# SPDX-FileCopyrightText: : 2021-2024 The PyPSA-Eur Authors -# -# SPDX-License-Identifier: CC0-1.0 - -name: CI - -# Caching method based on and described by: -# epassaro (2021): https://dev.to/epassaro/caching-anaconda-environments-in-github-actions-5hde -# and code in GitHub repo: https://github.com/epassaro/cache-conda-envs - -on: - push: - branches: - - master - pull_request: - branches: - - master - schedule: - - cron: "0 5 * * TUE" - -env: - DATA_CACHE_NUMBER: 2 - -jobs: - build: - - strategy: - fail-fast: false - max-parallel: 3 - matrix: - os: - - ubuntu-latest - - macos-latest - - windows-latest - inhouse: - - stable - - master - exclude: - - os: macos-latest - inhouse: master - - os: windows-latest - inhouse: master - runs-on: ${{ matrix.os }} - - defaults: - run: - shell: bash -l {0} - - steps: - - uses: actions/checkout@v4 - - - name: Setup secrets - run: | - echo -ne "url: ${CDSAPI_URL}\nkey: ${CDSAPI_TOKEN}\n" > ~/.cdsapirc - - - name: Setup micromamba - uses: mamba-org/setup-micromamba@v1 - with: - micromamba-version: latest - environment-file: envs/environment.yaml - log-level: debug - init-shell: bash - cache-environment: true - cache-downloads: true - - - name: Install inhouse packages - run: | - pip install git+https://github.com/PyPSA/atlite.git@master git+https://github.com/PyPSA/powerplantmatching.git@master git+https://github.com/PyPSA/linopy.git@master - if: ${{ matrix.inhouse }} == 'master' - - - name: Set cache dates - run: | - echo "WEEK=$(date +'%Y%U')" >> $GITHUB_ENV - - - name: Cache data and cutouts folders - uses: actions/cache@v4 - with: - path: | - data - cutouts - key: data-cutouts-${{ env.WEEK }}-${{ env.DATA_CACHE_NUMBER }} - - - name: Test snakemake workflow - run: ./test.sh - - - name: Upload artifacts - uses: actions/upload-artifact@v4.4.0 - with: - name: resources-results - path: | - resources - results - if-no-files-found: warn - retention-days: 1 - if: matrix.os == 'ubuntu' && matrix.inhouse == 'stable' diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 000000000..391e0b6aa --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,112 @@ +# SPDX-FileCopyrightText: : 2021-2024 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: CC0-1.0 + +name: Test workflows + +on: + push: + branches: + - master + pull_request: + branches: + - master + schedule: + - cron: "0 5 * * TUE" + +# Cancel any in-progress runs when a new run is triggered +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + run: + name: Run + strategy: + fail-fast: false + matrix: + os: + - macos + - windows + - ubuntu + inhouse: + - stable-inhouse-deps + - dev-inhouse-deps + exclude: + - os: macos + inhouse: dev-inhouse-deps + - os: windows + inhouse: dev-inhouse-deps + runs-on: ${{ matrix.os }}-latest + + defaults: + run: + shell: bash -l {0} + + steps: + - uses: actions/checkout@v4 + + - name: Setup secrets + run: | + echo -ne "url: ${CDSAPI_URL}\nkey: ${CDSAPI_TOKEN}\n" > ~/.cdsapirc + + - name: Set cache dates + run: | + echo "week=$(date +'%Y%U')" >> $GITHUB_ENV # data and cutouts + echo "today=$(/bin/date -u '+%Y%m%d')" >> $GITHUB_ENV # env + + - name: Cache data and cutouts folders + uses: actions/cache@v4 + with: + path: | + data + cutouts + key: data-cutouts-${{ env.week }} + + - name: Setup Conda + uses: conda-incubator/setup-miniconda@v3 + with: + activate-environment: pypsa-eur + + - name: Cache Conda env + uses: actions/cache@v3 + with: + path: ${{ env.CONDA }}/envs + key: conda-${{ runner.os }}--${{ runner.arch }}--${{ env.today }}-${{ hashFiles('envs/environment.yaml') }} + id: cache-env + + - name: Update environment + if: steps.cache-env.outputs.cache-hit != 'true' && matrix.os != 'macos' + run: conda env update -n pypsa-eur -f envs/environment.yaml + + # Temporary fix for MacOS, since highspy with pypsa can not be resolved + - name: Update environment (macos specific) + if: steps.cache-env.outputs.cache-hit != 'true' && matrix.os == 'macos' + run: | + sed -i '' '/- pypsa/d' envs/environment.yaml # Remove pypsa from environment list + conda env update -n pypsa-eur -f envs/environment.yaml + pip install highspy + conda install -c conda-forge pypsa + + - name: Install inhouse packages from master + if: matrix.inhouse == 'dev-inhouse-deps' + run: | + python -m pip install uv + uv pip install git+https://github.com/PyPSA/pypsa.git@master + uv pip install git+https://github.com/PyPSA/atlite.git@master + uv pip install git+https://github.com/PyPSA/powerplantmatching.git@master + uv pip install git+https://github.com/PyPSA/linopy.git@master + + - name: Run snakemake test workflows + run: | + chmod +x test.sh + ./test.sh + + - name: Upload artifacts + if: matrix.os == 'ubuntu' && matrix.inhouse == 'stable-inhouse-deps' + uses: actions/upload-artifact@v4.4.0 + with: + name: resources-results + path: | + results + retention-days: 7 diff --git a/README.md b/README.md index 25e8800e7..31a10d9bf 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ SPDX-License-Identifier: CC-BY-4.0 --> ![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/pypsa/pypsa-eur?include_prereleases) -[![Build Status](https://github.com/pypsa/pypsa-eur/actions/workflows/ci.yaml/badge.svg)](https://github.com/PyPSA/pypsa-eur/actions) +[![Test workflows](https://github.com/pypsa/pypsa-eur/actions/workflows/test.yaml/badge.svg)](https://github.com/pypsa/pypsa-eur/actions/workflows/test.yaml) [![Documentation](https://readthedocs.org/projects/pypsa-eur/badge/?version=latest)](https://pypsa-eur.readthedocs.io/en/latest/?badge=latest) ![Size](https://img.shields.io/github/repo-size/pypsa/pypsa-eur) [![Zenodo PyPSA-Eur](https://zenodo.org/badge/DOI/10.5281/zenodo.3520874.svg)](https://doi.org/10.5281/zenodo.3520874) diff --git a/doc/index.rst b/doc/index.rst index 961f1482d..a07c1e8ca 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -10,7 +10,7 @@ PyPSA-Eur: A Sector-Coupled Open Optimisation Model of the European Energy Syste .. image:: https://img.shields.io/github/v/release/pypsa/pypsa-eur?include_prereleases :alt: GitHub release (latest by date including pre-releases) -.. image:: https://github.com/pypsa/pypsa-eur/actions/workflows/ci.yaml/badge.svg +.. image:: https://github.com/pypsa/pypsa-eur/actions/workflows/test.yaml/badge.svg :target: https://github.com/PyPSA/pypsa-eur/actions .. image:: https://readthedocs.org/projects/pypsa-eur/badge/?version=latest diff --git a/envs/environment.yaml b/envs/environment.yaml index bb6822e95..9415a27f6 100644 --- a/envs/environment.yaml +++ b/envs/environment.yaml @@ -15,7 +15,7 @@ dependencies: - linopy - dask - # Dependencies of the workflow itself +# Dependencies of the workflow itself - xlrd - openpyxl!=3.1.1 - seaborn @@ -49,15 +49,14 @@ dependencies: - pre-commit - geojson - # Keep in conda environment when calling ipython +# Keep in conda environment when calling ipython - ipython - # GIS dependencies: +# GIS dependencies: - cartopy - descartes - rasterio!=1.2.10 - - pip: - tsam>=2.3.1 - snakemake-storage-plugin-http diff --git a/test.sh b/test.sh old mode 100755 new mode 100644 From f9bddc914e8b3e56bfec04372dec3ac836dae913 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 16 Sep 2024 14:07:44 +0200 Subject: [PATCH 317/344] [github-actions.ci] Update fixed environment (#1297) * [create-pull-request] automated change * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: lkstrp <62255395+lkstrp@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- envs/environment.fixed.yaml | 460 +++++++++++++++++++++++++++++++++++- 1 file changed, 456 insertions(+), 4 deletions(-) diff --git a/envs/environment.fixed.yaml b/envs/environment.fixed.yaml index ac2d7eeea..aaa0039c7 100644 --- a/envs/environment.fixed.yaml +++ b/envs/environment.fixed.yaml @@ -1,10 +1,462 @@ # SPDX-FileCopyrightText: : 2017-2024 The PyPSA-Eur Authors -# # SPDX-License-Identifier: CC0-1.0 -name: pypsa-eur-20240812 +name: pypsa-eur channels: -- http://conda.anaconda.org/gurobi - conda-forge +- bioconda - defaults -prefix: /home/fneum/miniconda3/envs/pypsa-eur-20240812 +dependencies: +- _libgcc_mutex=0.1 +- _openmp_mutex=4.5 +- affine=2.4.0 +- alsa-lib=1.2.12 +- ampl-mp=3.1.0 +- amply=0.1.6 +- appdirs=1.4.4 +- argparse-dataclass=2.0.0 +- asttokens=2.4.1 +- atk-1.0=2.38.0 +- atlite=0.2.14 +- attrs=24.2.0 +- aws-c-auth=0.7.30 +- aws-c-cal=0.7.4 +- aws-c-common=0.9.28 +- aws-c-compression=0.2.19 +- aws-c-event-stream=0.4.3 +- aws-c-http=0.8.9 +- aws-c-io=0.14.18 +- aws-c-mqtt=0.10.5 +- aws-c-s3=0.6.5 +- aws-c-sdkutils=0.1.19 +- aws-checksums=0.1.18 +- aws-crt-cpp=0.28.2 +- aws-sdk-cpp=1.11.379 +- azure-core-cpp=1.13.0 +- azure-identity-cpp=1.8.0 +- azure-storage-blobs-cpp=12.12.0 +- azure-storage-common-cpp=12.7.0 +- azure-storage-files-datalake-cpp=12.11.0 +- beautifulsoup4=4.12.3 +- blosc=1.21.6 +- bokeh=3.5.2 +- bottleneck=1.4.0 +- branca=0.7.2 +- brotli=1.1.0 +- brotli-bin=1.1.0 +- brotli-python=1.1.0 +- bzip2=1.0.8 +- c-ares=1.33.1 +- c-blosc2=2.15.1 +- ca-certificates=2024.8.30 +- cads-api-client=1.3.2 +- cairo=1.18.0 +- cartopy=0.23.0 +- cdsapi=0.7.3 +- certifi=2024.8.30 +- cffi=1.17.1 +- cfgv=3.3.1 +- cfitsio=4.4.1 +- cftime=1.6.4 +- charset-normalizer=3.3.2 +- click=8.1.7 +- click-plugins=1.1.1 +- cligj=0.7.2 +- cloudpickle=3.0.0 +- coin-or-cbc=2.10.12 +- coin-or-cgl=0.60.9 +- coin-or-clp=1.17.10 +- coin-or-osi=0.108.11 +- coin-or-utils=2.11.12 +- coincbc=2.10.12 +- colorama=0.4.6 +- conda-inject=1.3.2 +- configargparse=1.7 +- connection_pool=0.0.3 +- contourpy=1.3.0 +- country_converter=1.2 +- cppad=20240000.5 +- cycler=0.12.1 +- cytoolz=0.12.3 +- dask=2024.9.0 +- dask-core=2024.9.0 +- dask-expr=1.1.14 +- datrie=0.8.2 +- dbus=1.13.6 +- decorator=5.1.1 +- deprecation=2.1.0 +- descartes=1.1.0 +- distlib=0.3.8 +- distributed=2024.9.0 +- docutils=0.21.2 +- double-conversion=3.3.0 +- dpath=2.2.0 +- entsoe-py=0.6.8 +- et_xmlfile=1.1.0 +- exceptiongroup=1.2.2 +- executing=2.1.0 +- expat=2.6.3 +- filelock=3.16.0 +- fiona=1.10.0 +- fmt=11.0.2 +- folium=0.17.0 +- font-ttf-dejavu-sans-mono=2.37 +- font-ttf-inconsolata=3.000 +- font-ttf-source-code-pro=2.038 +- font-ttf-ubuntu=0.83 +- fontconfig=2.14.2 +- fonts-conda-ecosystem=1 +- fonts-conda-forge=1 +- fonttools=4.53.1 +- freetype=2.12.1 +- freexl=2.0.0 +- fribidi=1.0.10 +- fsspec=2024.9.0 +- gdal=3.9.2 +- gdk-pixbuf=2.42.12 +- geographiclib=2.0 +- geojson=3.1.0 +- geojson-rewind=1.1.0 +- geopandas=1.0.1 +- geopandas-base=1.0.1 +- geopy=2.4.1 +- geos=3.12.2 +- geotiff=1.7.3 +- gflags=2.2.2 +- giflib=5.2.2 +- gitdb=4.0.11 +- gitpython=3.1.43 +- glog=0.7.1 +- glpk=5.0 +- gmp=6.3.0 +- graphite2=1.3.13 +- graphviz=12.0.0 +- gtk2=2.24.33 +- gts=0.7.6 +- h2=4.1.0 +- harfbuzz=9.0.0 +- hdf4=4.2.15 +- hdf5=1.14.3 +- highspy=1.7.2 +- hpack=4.0.0 +- humanfriendly=10.0 +- hyperframe=6.0.1 +- icu=75.1 +- identify=2.6.1 +- idna=3.10 +- immutables=0.20 +- importlib-metadata=8.5.0 +- importlib_metadata=8.5.0 +- importlib_resources=6.4.5 +- iniconfig=2.0.0 +- ipopt=3.14.16 +- ipython=8.27.0 +- jedi=0.19.1 +- jinja2=3.1.4 +- joblib=1.4.2 +- jpype1=1.5.0 +- json-c=0.17 +- jsonschema=4.23.0 +- jsonschema-specifications=2023.12.1 +- jupyter_core=5.7.2 +- kealib=1.5.3 +- keyutils=1.6.1 +- kiwisolver=1.4.7 +- krb5=1.21.3 +- lcms2=2.16 +- ld_impl_linux-64=2.40 +- lerc=4.0.0 +- libabseil=20240116.2 +- libaec=1.1.3 +- libarchive=3.7.4 +- libarrow=17.0.0 +- libarrow-acero=17.0.0 +- libarrow-dataset=17.0.0 +- libarrow-substrait=17.0.0 +- libblas=3.9.0 +- libbrotlicommon=1.1.0 +- libbrotlidec=1.1.0 +- libbrotlienc=1.1.0 +- libcblas=3.9.0 +- libclang-cpp18.1=18.1.8 +- libclang13=18.1.8 +- libcrc32c=1.1.2 +- libcups=2.3.3 +- libcurl=8.10.0 +- libdeflate=1.21 +- libdrm=2.4.123 +- libedit=3.1.20191231 +- libegl=1.7.0 +- libev=4.33 +- libevent=2.1.12 +- libexpat=2.6.3 +- libffi=3.4.2 +- libgcc=14.1.0 +- libgcc-ng=14.1.0 +- libgd=2.3.3 +- libgdal=3.9.2 +- libgdal-core=3.9.2 +- libgdal-fits=3.9.2 +- libgdal-grib=3.9.2 +- libgdal-hdf4=3.9.2 +- libgdal-hdf5=3.9.2 +- libgdal-jp2openjpeg=3.9.2 +- libgdal-kea=3.9.2 +- libgdal-netcdf=3.9.2 +- libgdal-pdf=3.9.2 +- libgdal-pg=3.9.2 +- libgdal-postgisraster=3.9.2 +- libgdal-tiledb=3.9.2 +- libgdal-xls=3.9.2 +- libgfortran=14.1.0 +- libgfortran-ng=14.1.0 +- libgfortran5=14.1.0 +- libgl=1.7.0 +- libglib=2.80.3 +- libglvnd=1.7.0 +- libglx=1.7.0 +- libgomp=14.1.0 +- libgoogle-cloud=2.29.0 +- libgoogle-cloud-storage=2.29.0 +- libgrpc=1.62.2 +- libhwloc=2.11.1 +- libiconv=1.17 +- libjpeg-turbo=3.0.0 +- libkml=1.3.0 +- liblapack=3.9.0 +- liblapacke=3.9.0 +- libllvm18=18.1.8 +- libnetcdf=4.9.2 +- libnghttp2=1.58.0 +- libnsl=2.0.1 +- libopenblas=0.3.27 +- libparquet=17.0.0 +- libpciaccess=0.18 +- libpng=1.6.44 +- libpq=16.4 +- libprotobuf=4.25.3 +- libre2-11=2023.09.01 +- librsvg=2.58.4 +- librttopo=1.1.0 +- libscotch=7.0.4 +- libspatialite=5.1.0 +- libspral=2024.05.08 +- libsqlite=3.46.1 +- libssh2=1.11.0 +- libstdcxx=14.1.0 +- libstdcxx-ng=14.1.0 +- libthrift=0.20.0 +- libtiff=4.6.0 +- libutf8proc=2.8.0 +- libuuid=2.38.1 +- libwebp-base=1.4.0 +- libxcb=1.16 +- libxcrypt=4.4.36 +- libxkbcommon=1.7.0 +- libxml2=2.12.7 +- libxslt=1.1.39 +- libzip=1.10.1 +- libzlib=1.3.1 +- linopy=0.3.14 +- locket=1.0.0 +- lxml=5.3.0 +- lz4=4.3.3 +- lz4-c=1.9.4 +- lzo=2.10 +- mapclassify=2.8.0 +- markupsafe=2.1.5 +- matplotlib=3.9.2 +- matplotlib-base=3.9.2 +- matplotlib-inline=0.1.7 +- memory_profiler=0.61.0 +- metis=5.1.0 +- minizip=4.0.7 +- mpfr=4.2.1 +- msgpack-python=1.1.0 +- multiurl=0.3.1 +- mumps-include=5.7.3 +- mumps-seq=5.7.3 +- munkres=1.1.4 +- mysql-common=9.0.1 +- mysql-libs=9.0.1 +- nbformat=5.10.4 +- ncurses=6.5 +- netcdf4=1.7.1 +- networkx=3.3 +- nodeenv=1.9.1 +- nomkl=1.0 +- nspr=4.35 +- nss=3.104 +- numexpr=2.10.0 +- numpy=1.26.4 +- openjpeg=2.5.2 +- openpyxl=3.1.5 +- openssl=3.3.2 +- orc=2.0.2 +- packaging=24.1 +- pandas=2.2.2 +- pango=1.54.0 +- parso=0.8.4 +- partd=1.4.2 +- patsy=0.5.6 +- pcre2=10.44 +- pexpect=4.9.0 +- pickleshare=0.7.5 +- pillow=10.4.0 +- pip=24.2 +- pixman=0.43.2 +- pkgutil-resolve-name=1.3.10 +- plac=1.4.3 +- platformdirs=4.3.3 +- pluggy=1.5.0 +- polars=1.7.1 +- poppler=24.08.0 +- poppler-data=0.4.12 +- postgresql=16.4 +- powerplantmatching=0.5.17 +- pre-commit=3.8.0 +- progressbar2=4.5.0 +- proj=9.4.1 +- prompt-toolkit=3.0.47 +- psutil=6.0.0 +- pthread-stubs=0.4 +- ptyprocess=0.7.0 +- pulp=2.8.0 +- pure_eval=0.2.3 +- py-cpuinfo=9.0.0 +- pyarrow=17.0.0 +- pyarrow-core=17.0.0 +- pyarrow-hotfix=0.6 +- pycountry=24.6.1 +- pycparser=2.22 +- pygments=2.18.0 +- pyogrio=0.9.0 +- pyparsing=3.1.4 +- pyproj=3.6.1 +- pypsa=0.30.2 +- pyscipopt=5.1.1 +- pyshp=2.3.1 +- pyside6=6.7.2 +- pysocks=1.7.1 +- pytables=3.10.1 +- pytest=8.3.3 +- python=3.12.5 +- python-dateutil=2.9.0 +- python-fastjsonschema=2.20.0 +- python-tzdata=2024.1 +- python-utils=3.8.2 +- python_abi=3.12 +- pytz=2024.2 +- pyxlsb=1.0.10 +- pyyaml=6.0.2 +- qhull=2020.2 +- qt6-main=6.7.2 +- rasterio=1.3.11 +- re2=2023.09.01 +- readline=8.2 +- referencing=0.35.1 +- requests=2.32.3 +- reretry=0.11.8 +- rioxarray=0.17.0 +- rpds-py=0.20.0 +- s2n=1.5.2 +- scikit-learn=1.5.2 +- scip=9.1.0 +- scipy=1.14.1 +- seaborn=0.13.2 +- seaborn-base=0.13.2 +- setuptools=73.0.1 +- shapely=2.0.6 +- six=1.16.0 +- smart_open=7.0.4 +- smmap=5.0.0 +- snakemake-interface-common=1.17.3 +- snakemake-interface-executor-plugins=9.2.0 +- snakemake-interface-report-plugins=1.0.0 +- snakemake-interface-storage-plugins=3.3.0 +- snakemake-minimal=8.20.3 +- snappy=1.2.1 +- snuggs=1.4.7 +- sortedcontainers=2.4.0 +- soupsieve=2.5 +- spdlog=1.14.1 +- sqlite=3.46.1 +- stack_data=0.6.2 +- statsmodels=0.14.2 +- tabulate=0.9.0 +- tbb=2021.13.0 +- tblib=3.0.0 +- threadpoolctl=3.5.0 +- throttler=1.2.2 +- tiledb=2.26.0 +- tk=8.6.13 +- tomli=2.0.1 +- toolz=0.12.1 +- toposort=1.10 +- tornado=6.4.1 +- tqdm=4.66.5 +- traitlets=5.14.3 +- typing-extensions=4.12.2 +- typing_extensions=4.12.2 +- tzcode=2024b +- tzdata=2024a +- ukkonen=1.0.1 +- unidecode=1.3.8 +- unixodbc=2.3.12 +- uriparser=0.9.8 +- urllib3=2.2.2 +- validators=0.34.0 +- virtualenv=20.26.4 +- wayland=1.23.1 +- wcwidth=0.2.13 +- wheel=0.44.0 +- wrapt=1.16.0 +- xarray=2024.9.0 +- xcb-util=0.4.1 +- xcb-util-cursor=0.1.5 +- xcb-util-image=0.4.0 +- xcb-util-keysyms=0.4.1 +- xcb-util-renderutil=0.3.10 +- xcb-util-wm=0.4.2 +- xerces-c=3.2.5 +- xkeyboard-config=2.42 +- xlrd=2.0.1 +- xorg-fixesproto=5.0 +- xorg-inputproto=2.3.2 +- xorg-kbproto=1.0.7 +- xorg-libice=1.1.1 +- xorg-libsm=1.2.4 +- xorg-libx11=1.8.9 +- xorg-libxau=1.0.11 +- xorg-libxdmcp=1.1.3 +- xorg-libxext=1.3.4 +- xorg-libxfixes=5.0.3 +- xorg-libxi=1.7.10 +- xorg-libxrender=0.9.11 +- xorg-libxtst=1.2.5 +- xorg-libxxf86vm=1.1.5 +- xorg-recordproto=1.14.2 +- xorg-renderproto=0.11.1 +- xorg-xextproto=7.3.0 +- xorg-xproto=7.0.31 +- xyzservices=2024.9.0 +- xz=5.2.6 +- yaml=0.2.5 +- yte=1.5.4 +- zict=3.0.0 +- zipp=3.20.2 +- zlib=1.3.1 +- zlib-ng=2.2.1 +- zstandard=0.23.0 +- zstd=1.5.6 +- pip: + - oauthlib==3.2.2 + - ply==3.11 + - pyomo==6.8.0 + - requests-oauthlib==1.3.1 + - snakemake-executor-plugin-cluster-generic==1.0.9 + - snakemake-executor-plugin-slurm==0.10.2 + - snakemake-executor-plugin-slurm-jobstep==0.2.1 + - snakemake-storage-plugin-http==0.2.3 + - tsam==2.3.4 +prefix: /usr/share/miniconda/envs/pypsa-eur From df71b1a64c0e684cb7b98ba0fad09aa1ad7ceda5 Mon Sep 17 00:00:00 2001 From: Philipp Glaum <95913147+p-glaum@users.noreply.github.com> Date: Mon, 16 Sep 2024 15:02:42 +0200 Subject: [PATCH 318/344] remove MtO as long as there is no HVC bus (#1299) --- config/config.default.yaml | 1 - doc/configtables/sector.csv | 1 - scripts/prepare_sector_network.py | 53 ------------------------------- 3 files changed, 55 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index a98a0af7c..91e4af8da 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -648,7 +648,6 @@ sector: methanol_reforming: false methanol_reforming_cc: false methanol_to_kerosene: false - methanol_to_olefins: false methanol_to_power: ccgt: false ccgt_cc: false diff --git a/doc/configtables/sector.csv b/doc/configtables/sector.csv index 000927ced..08c903b82 100644 --- a/doc/configtables/sector.csv +++ b/doc/configtables/sector.csv @@ -131,7 +131,6 @@ methanol,--,--,Add methanol as carrrier and add enabled methnol technologies -- methanol_reforming,--,"{true, false}"," Add methanol reforming" -- methanol_reforming_cc,--,"{true, false}"," Add methanol reforming with carbon capture" -- methanol_to_kerosene,--,"{true, false}"," Add methanol to kerosene" --- methanol_to_olefins,--,"{true, false}"," Add methanol to olefins" -- methanol_to_power,--,--," Add different methanol to power technologies" -- -- ccgt,--,"{true, false}"," Add combined cycle gas turbine (CCGT) using methanol" -- -- ccgt_cc,--,"{true, false}"," Add combined cycle gas turbine (CCGT) with carbon capture using methanol" diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 0821a4f0b..f088b97cf 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -927,56 +927,6 @@ def add_methanol_to_power(n, costs, types={}): ) -def add_methanol_to_olefins(n, costs): - nodes = spatial.nodes - nhours = n.snapshot_weightings.generators.sum() - nyears = nhours / 8760 - - tech = "methanol-to-olefins/aromatics" - - logger.info(f"Adding {tech}.") - - demand_factor = options["HVC_demand_factor"] - - industrial_production = ( - pd.read_csv(snakemake.input.industrial_production, index_col=0) - * 1e3 - * nyears # kt/a -> t/a - ) - - p_nom_max = ( - demand_factor - * industrial_production.loc[nodes, "HVC"] - / nhours - * costs.at[tech, "methanol-input"] - ) - - co2_release = ( - costs.at[tech, "carbondioxide-output"] / costs.at[tech, "methanol-input"] - + costs.at["methanolisation", "carbondioxide-input"] - ) - - n.madd( - "Link", - nodes, - suffix=f" {tech}", - carrier=tech, - capital_cost=costs.at[tech, "fixed"] / costs.at[tech, "methanol-input"], - marginal_cost=costs.at[tech, "VOM"] / costs.at[tech, "methanol-input"], - p_nom_extendable=True, - bus0=spatial.methanol.nodes, - bus1=spatial.oil.naphtha, - bus2=nodes, - bus3="co2 atmosphere", - p_min_pu=1, - p_nom_max=p_nom_max.values, - efficiency=1 / costs.at[tech, "methanol-input"], - efficiency2=-costs.at[tech, "electricity-input"] - / costs.at[tech, "methanol-input"], - efficiency3=co2_release, - ) - - def add_methanol_to_kerosene(n, costs): nodes = pop_layout.index nhours = n.snapshot_weightings.generators.sum() @@ -3771,9 +3721,6 @@ def add_industry(n, costs): efficiency3=process_co2_per_naphtha, ) - if options["methanol"]["methanol_to_olefins"]: - add_methanol_to_olefins(n, costs) - # aviation demand_factor = options.get("aviation_demand_factor", 1) if demand_factor != 1: From d8f8a828f900364ce509052ad320f5bfeba085a1 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 16 Sep 2024 15:04:24 +0200 Subject: [PATCH 319/344] =?UTF-8?q?options:=20biosng=5Fcc,=20biomass=5Fto?= =?UTF-8?q?=5Fliquid=5Fcc,=2098%=20capture=20rate=20Allam=20gas,=E2=80=A6?= =?UTF-8?q?=20(#1298)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * options: biosng_cc, biomass_to_liquid_cc, 98% capture rate Allam gas, avoid option.get * fix duplicated bus port --- config/config.default.yaml | 2 + doc/configtables/sector.csv | 2 + doc/release_notes.rst | 5 ++ scripts/prepare_sector_network.py | 76 ++++++++++++++++--------------- 4 files changed, 48 insertions(+), 37 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 91e4af8da..d613aef91 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -693,8 +693,10 @@ sector: conventional_generation: OCGT: gas biomass_to_liquid: false + biomass_to_liquid_cc: false electrobiofuels: false biosng: false + biosng_cc: false bioH2: false municipal_solid_waste: false limit_max_growth: diff --git a/doc/configtables/sector.csv b/doc/configtables/sector.csv index 08c903b82..64f0a6c0d 100644 --- a/doc/configtables/sector.csv +++ b/doc/configtables/sector.csv @@ -167,7 +167,9 @@ biomass_transport,--,"{true, false}",Add option for transporting solid biomass b biogas_upgrading_cc,--,"{true, false}",Add option to capture CO2 from biomass upgrading conventional_generation,,,Add a more detailed description of conventional carriers. Any power generation requires the consumption of fuel from nodes representing that fuel. biomass_to_liquid,--,"{true, false}",Add option for transforming solid biomass into liquid fuel with the same properties as oil +biomass_to_liquid_cc,--,"{true, false}",Add option for transforming solid biomass into liquid fuel with the same properties as oil with carbon capture biosng,--,"{true, false}",Add option for transforming solid biomass into synthesis gas with the same properties as natural gas +biosng_cc,--,"{true, false}",Add option for transforming solid biomass into synthesis gas with the same properties as natural gas with carbon capture bioH2,--,"{true, false}",Add option for transforming solid biomass into hydrogen with carbon capture municipal_solid_waste,--,"{true, false}",Add option for municipal solid waste limit_max_growth,,, diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 8859c41dd..dca98d379 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -68,6 +68,11 @@ Upcoming Release defaults to 10 km. Previously the distance to the region's centroid was used, which is not practical when the regions are already aggregated. +* Added options ``biosng_cc`` and ``biomass_to_liquid_cc`` to separate the base + technology from the option to capture carbon from it. + +* Added 98% imperfect capture rate of Allam cycle gas turbine. + PyPSA-Eur 0.13.0 (13th September 2024) ====================================== diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index f088b97cf..89b4f6e1b 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -132,9 +132,9 @@ def define_spatial(nodes, options): # ammonia - if options.get("ammonia"): + if options["ammonia"]: spatial.ammonia = SimpleNamespace() - if options.get("ammonia") == "regional": + if options["ammonia"] == "regional": spatial.ammonia.nodes = nodes + " NH3" spatial.ammonia.locations = nodes else: @@ -519,7 +519,7 @@ def add_carrier_buses(n, carrier, nodes=None): ) fossils = ["coal", "gas", "oil", "lignite"] - if options.get("fossil_fuels", True) and carrier in fossils: + if options["fossil_fuels"] and carrier in fossils: suffix = "" @@ -750,7 +750,7 @@ def add_co2_network(n, costs): ) -def add_allam(n, costs): +def add_allam_gas(n, costs): logger.info("Adding Allam cycle gas power plants.") nodes = pop_layout.index @@ -758,17 +758,18 @@ def add_allam(n, costs): n.madd( "Link", nodes, - suffix=" allam", + suffix=" allam gas", bus0=spatial.gas.df.loc[nodes, "nodes"].values, bus1=nodes, bus2=spatial.co2.df.loc[nodes, "nodes"].values, - carrier="allam", + bus3="co2 atmosphere", + carrier="allam gas", p_nom_extendable=True, - # TODO: add costs to technology-data capital_cost=costs.at["allam", "fixed"] * costs.at["allam", "efficiency"], marginal_cost=costs.at["allam", "VOM"] * costs.at["allam", "efficiency"], efficiency=costs.at["allam", "efficiency"], - efficiency2=costs.at["gas", "CO2 intensity"], + efficiency2=0.98 * costs.at["gas", "CO2 intensity"], + efficiency3=0.02 * costs.at["gas", "CO2 intensity"], lifetime=costs.at["allam", "lifetime"], ) @@ -823,8 +824,10 @@ def add_biomass_to_methanol_cc(n, costs): ) -def add_methanol_to_power(n, costs, types={}): - # TODO: add costs to technology-data +def add_methanol_to_power(n, costs, types=None): + + if types is None: + types = {} nodes = pop_layout.index @@ -1121,8 +1124,7 @@ def add_generation(n, costs): nodes = pop_layout.index - fallback = {"OCGT": "gas"} - conventionals = options.get("conventional_generation", fallback) + conventionals = options["conventional_generation"] for generator, carrier in conventionals.items(): carrier_nodes = vars(spatial)[carrier].nodes @@ -1564,7 +1566,7 @@ def add_storage_and_grids(n, costs): complement_edges["length"] = complement_edges.apply(haversine, axis=1) # apply k_edge_augmentation weighted by length of complement edges - k_edge = options.get("gas_network_connectivity_upgrade", 3) + k_edge = options["gas_network_connectivity_upgrade"] if augmentation := list( k_edge_augmentation(G, k_edge, avail=complement_edges.values) ): @@ -1612,7 +1614,7 @@ def add_storage_and_grids(n, costs): lifetime=costs.at["H2 (g) pipeline repurposed", "lifetime"], ) - if options.get("H2_network", True): + if options["H2_network"]: logger.info("Add options for new hydrogen pipelines.") h2_pipes = create_network_topology( @@ -1682,7 +1684,7 @@ def add_storage_and_grids(n, costs): bus2=spatial.co2.nodes, p_nom_extendable=True, carrier="Sabatier", - p_min_pu=options.get("min_part_load_methanation", 0), + p_min_pu=options["min_part_load_methanation"], efficiency=costs.at["methanation", "efficiency"], efficiency2=-costs.at["methanation", "efficiency"] * costs.at["gas", "CO2 intensity"], @@ -1691,7 +1693,7 @@ def add_storage_and_grids(n, costs): lifetime=costs.at["methanation", "lifetime"], ) - if options.get("coal_cc"): + if options["coal_cc"]: n.madd( "Link", spatial.nodes, @@ -1835,7 +1837,7 @@ def add_EVs( p_set=profile, ) - p_nom = number_cars * options.get("bev_charge_rate", 0.011) * electric_share + p_nom = number_cars * options["bev_charge_rate"] * electric_share n.madd( "Link", @@ -1847,7 +1849,7 @@ def add_EVs( carrier="BEV charger", p_max_pu=avail_profile[spatial.nodes], lifetime=1, - efficiency=options.get("bev_charge_efficiency", 0.9), + efficiency=options["bev_charge_efficiency"], ) if options["v2g"]: @@ -1861,13 +1863,13 @@ def add_EVs( carrier="V2G", p_max_pu=avail_profile[spatial.nodes], lifetime=1, - efficiency=options.get("bev_charge_efficiency", 0.9), + efficiency=options["bev_charge_efficiency"], ) if options["bev_dsm"]: e_nom = ( number_cars - * options.get("bev_energy", 0.05) + * options["bev_energy"] * options["bev_availability"] * electric_share ) @@ -2116,7 +2118,7 @@ def add_heat(n: pypsa.Network, costs: pd.DataFrame, cop: xr.DataArray): unit="MWh_th", ) - if heat_system == HeatSystem.URBAN_CENTRAL and options.get("central_heat_vent"): + if heat_system == HeatSystem.URBAN_CENTRAL and options["central_heat_vent"]: n.madd( "Generator", nodes + f" {heat_system} heat vent", @@ -2786,7 +2788,7 @@ def add_biomass(n, costs): p_nom_extendable=True, ) - if options.get("biogas_upgrading_cc"): + if options["biogas_upgrading_cc"]: # Assuming for costs that the CO2 from upgrading is pure, such as in amine scrubbing. I.e., with and without CC is # equivalent. Adding biomass CHP capture because biogas is often small-scale and decentral so further # from e.g. CO2 grid or buyers. This is a proxy for the added cost for e.g. a raw biogas pipeline to a central upgrading facility @@ -3034,6 +3036,8 @@ def add_biomass(n, costs): marginal_cost=costs.at["BtL", "VOM"] * costs.at["BtL", "efficiency"], ) + # Solid biomass to liquid fuel with carbon capture + if options["biomass_to_liquid_cc"]: # Assuming that acid gas removal (incl. CO2) from syngas i performed with Rectisol # process (Methanol) and that electricity demand for this is included in the base process n.madd( @@ -3044,7 +3048,7 @@ def add_biomass(n, costs): bus1=spatial.oil.nodes, bus2="co2 atmosphere", bus3=spatial.co2.nodes, - carrier="biomass to liquid", + carrier="biomass to liquid CC", lifetime=costs.at["BtL", "lifetime"], efficiency=costs.at["BtL", "efficiency"], efficiency2=-costs.at["solid biomass", "CO2 intensity"] @@ -3112,6 +3116,8 @@ def add_biomass(n, costs): marginal_cost=costs.at["BioSNG", "VOM"] * costs.at["BioSNG", "efficiency"], ) + # BioSNG from solid biomass with carbon capture + if options["biosng_cc"]: # Assuming that acid gas removal (incl. CO2) from syngas i performed with Rectisol # process (Methanol) and that electricity demand for this is included in the base process n.madd( @@ -3122,7 +3128,7 @@ def add_biomass(n, costs): bus1=spatial.gas.nodes, bus2=spatial.co2.nodes, bus3="co2 atmosphere", - carrier="BioSNG", + carrier="BioSNG CC", lifetime=costs.at["BioSNG", "lifetime"], efficiency=costs.at["BioSNG", "efficiency"], efficiency2=costs.at["BioSNG", "CO2 stored"] @@ -3162,10 +3168,6 @@ def add_biomass(n, costs): * costs.at["solid biomass to hydrogen", "efficiency"] + costs.at["biomass CHP capture", "fixed"] * costs.at["solid biomass", "CO2 intensity"], - overnight_cost=costs.at["solid biomass to hydrogen", "investment"] - * costs.at["solid biomass to hydrogen", "efficiency"] - + costs.at["biomass CHP capture", "investment"] - * costs.at["solid biomass", "CO2 intensity"], marginal_cost=0.0, ) @@ -3356,7 +3358,7 @@ def add_industry(n, costs): bus3=spatial.co2.nodes, carrier="methanolisation", p_nom_extendable=True, - p_min_pu=options.get("min_part_load_methanolisation", 0), + p_min_pu=options["min_part_load_methanolisation"], capital_cost=costs.at["methanolisation", "fixed"] * options["MWh_MeOH_per_MWh_H2"], # EUR/MW_H2/a marginal_cost=options["MWh_MeOH_per_MWh_H2"] @@ -3552,12 +3554,12 @@ def add_industry(n, costs): efficiency2=-costs.at["oil", "CO2 intensity"] * costs.at["Fischer-Tropsch", "efficiency"], p_nom_extendable=True, - p_min_pu=options.get("min_part_load_fischer_tropsch", 0), + p_min_pu=options["min_part_load_fischer_tropsch"], lifetime=costs.at["Fischer-Tropsch", "lifetime"], ) # naphtha - demand_factor = options.get("HVC_demand_factor", 1) + demand_factor = options["HVC_demand_factor"] if demand_factor != 1: logger.warning(f"Changing HVC demand by {demand_factor*100-100:+.2f}%.") @@ -3630,7 +3632,7 @@ def add_industry(n, costs): efficiency3=process_co2_per_naphtha, ) - if options.get("biomass", True) and options["municipal_solid_waste"]: + if options["biomass"] and options["municipal_solid_waste"]: n.madd( "Link", spatial.msw.locations, @@ -3722,7 +3724,7 @@ def add_industry(n, costs): ) # aviation - demand_factor = options.get("aviation_demand_factor", 1) + demand_factor = options["aviation_demand_factor"] if demand_factor != 1: logger.warning(f"Changing aviation demand by {demand_factor*100-100:+.2f}%.") @@ -3862,7 +3864,7 @@ def add_industry(n, costs): lifetime=costs.at["cement capture", "lifetime"], ) - if options.get("ammonia"): + if options["ammonia"]: if options["ammonia"] == "regional": p_set = ( industrial_demand.loc[spatial.ammonia.locations, "ammonia"].rename( @@ -4639,8 +4641,8 @@ def add_enhanced_geothermal(n, egs_potentials, egs_overlap, costs): if options["co2network"]: add_co2_network(n, costs) - if options["allam_cycle"]: - add_allam(n, costs) + if options["allam_cycle_gas"]: + add_allam_gas(n, costs) n = set_temporal_aggregation( n, snakemake.params.time_resolution, snakemake.input.snapshot_weightings @@ -4701,7 +4703,7 @@ def add_enhanced_geothermal(n, egs_potentials, egs_overlap, costs): snakemake.params.planning_horizons[0] == investment_year ) - if options.get("cluster_heat_buses", False) and not first_year_myopic: + if options["cluster_heat_buses"] and not first_year_myopic: cluster_heat_buses(n) maybe_adjust_costs_and_potentials( From 90e13650756c3aa035300496cc5d47659c2dfb08 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 16 Sep 2024 15:21:07 +0200 Subject: [PATCH 320/344] follow-up to #1298 --- config/config.default.yaml | 2 +- doc/configtables/sector.csv | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index d613aef91..e67a3d830 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -606,7 +606,7 @@ sector: dac: true co2_vent: false central_heat_vent: false - allam_cycle: false + allam_cycle_gas: false hydrogen_fuel_cell: true hydrogen_turbine: false SMR: true diff --git a/doc/configtables/sector.csv b/doc/configtables/sector.csv index 64f0a6c0d..80f8c0550 100644 --- a/doc/configtables/sector.csv +++ b/doc/configtables/sector.csv @@ -101,7 +101,7 @@ methanation,--,"{true, false}",Add option for transforming hydrogen and CO2 into coal_cc,--,"{true, false}",Add option for coal CHPs with carbon capture dac,--,"{true, false}",Add option for Direct Air Capture (DAC) co2_vent,--,"{true, false}",Add option for vent out CO2 from storages to the atmosphere. -allam_cycle,--,"{true, false}",Add option to include `Allam cycle gas power plants `_ +allam_cycle_gas,--,"{true, false}",Add option to include `Allam cycle gas power plants `_ hydrogen_fuel_cell,--,"{true, false}",Add option to include hydrogen fuel cell for re-electrification. Assuming OCGT technology costs hydrogen_turbine,--,"{true, false}",Add option to include hydrogen turbine for re-electrification. Assuming OCGT technology costs SMR,--,"{true, false}",Add option for transforming natural gas into hydrogen and CO2 using Steam Methane Reforming (SMR) From ae0d1d58742e22fb2e4390eeaf49a638cd0e322f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Sep 2024 22:11:14 +0200 Subject: [PATCH 321/344] Bump actions/cache from 3 to 4 in the github-actions group (#1300) Bumps the github-actions group with 1 update: [actions/cache](https://github.com/actions/cache). Updates `actions/cache` from 3 to 4 - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 391e0b6aa..00599de6a 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -69,7 +69,7 @@ jobs: activate-environment: pypsa-eur - name: Cache Conda env - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ env.CONDA }}/envs key: conda-${{ runner.os }}--${{ runner.arch }}--${{ env.today }}-${{ hashFiles('envs/environment.yaml') }} From 08548dc738b11d1febf705b7fe7e37c87e71692d Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Tue, 17 Sep 2024 07:39:23 +0200 Subject: [PATCH 322/344] remove unnecessary [-4:] from snakemake.wildcards.planning --- scripts/build_district_heat_share.py | 2 +- scripts/build_industry_sector_ratios_intermediate.py | 2 +- scripts/prepare_sector_network.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/build_district_heat_share.py b/scripts/build_district_heat_share.py index 6d0bdc324..e84819189 100644 --- a/scripts/build_district_heat_share.py +++ b/scripts/build_district_heat_share.py @@ -50,7 +50,7 @@ configure_logging(snakemake) set_scenario_config(snakemake) - investment_year = int(snakemake.wildcards.planning_horizons[-4:]) + investment_year = int(snakemake.wildcards.planning_horizons) pop_layout = pd.read_csv(snakemake.input.clustered_pop_layout, index_col=0) diff --git a/scripts/build_industry_sector_ratios_intermediate.py b/scripts/build_industry_sector_ratios_intermediate.py index 0fbaa567f..e47ad0b7f 100644 --- a/scripts/build_industry_sector_ratios_intermediate.py +++ b/scripts/build_industry_sector_ratios_intermediate.py @@ -150,7 +150,7 @@ def build_industry_sector_ratios_intermediate(): planning_horizons="2030", ) - year = int(snakemake.wildcards.planning_horizons[-4:]) + year = int(snakemake.wildcards.planning_horizons) params = snakemake.params.industry diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 89b4f6e1b..fcbacd385 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -4555,7 +4555,7 @@ def add_enhanced_geothermal(n, egs_potentials, egs_overlap, costs): options = snakemake.params.sector cf_industry = snakemake.params.industry - investment_year = int(snakemake.wildcards.planning_horizons[-4:]) + investment_year = int(snakemake.wildcards.planning_horizons) n = pypsa.Network(snakemake.input.network) From 9420c9081e1a200729359222b2ff235dd5ee0421 Mon Sep 17 00:00:00 2001 From: Lukas Trippe Date: Tue, 17 Sep 2024 14:00:40 +0200 Subject: [PATCH 323/344] ci: show heat balances in validator (#1304) --- .github/workflows/validate.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index 9303834b2..eb657c33d 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -42,5 +42,8 @@ jobs: graphs/energy.svg graphs/costs.svg graphs/balances-energy.svg + graphs/balances-urban_central_heat.svg + graphs/balances-urban_decentral_heat.svg + graphs/balances-rural_heat.svg " validator_key: ${{ secrets.VALIDATOR_KEY }} From bb8363a650bc830351b321384d42a4e856941e10 Mon Sep 17 00:00:00 2001 From: Koen van Greevenbroek <74298901+koen-vg@users.noreply.github.com> Date: Wed, 18 Sep 2024 16:28:03 +0200 Subject: [PATCH 324/344] Add {sector_opts} wildcard to snapshot_weightings output (#1307) This restores the functionality of time-aggregation wildcards for sector-coupled networks. --- rules/build_sector.smk | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rules/build_sector.smk b/rules/build_sector.smk index bd19fef34..4ba1f45b4 100755 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -941,15 +941,15 @@ rule time_aggregation: ), output: snapshot_weightings=resources( - "snapshot_weightings_base_s_{clusters}_elec_l{ll}_{opts}.csv" + "snapshot_weightings_base_s_{clusters}_elec_l{ll}_{opts}_{sector_opts}.csv" ), threads: 1 resources: mem_mb=5000, log: - logs("time_aggregation_base_s_{clusters}_elec_l{ll}_{opts}.log"), + logs("time_aggregation_base_s_{clusters}_elec_l{ll}_{opts}_{sector_opts}.log"), benchmark: - benchmarks("time_aggregation_base_s_{clusters}_elec_l{ll}_{opts}") + benchmarks("time_aggregation_base_s_{clusters}_elec_l{ll}_{opts}_{sector_opts}") conda: "../envs/environment.yaml" script: @@ -1021,7 +1021,7 @@ rule prepare_sector_network: **rules.cluster_gas_network.output, **rules.build_gas_input_locations.output, snapshot_weightings=resources( - "snapshot_weightings_base_s_{clusters}_elec_l{ll}_{opts}.csv" + "snapshot_weightings_base_s_{clusters}_elec_l{ll}_{opts}_{sector_opts}.csv" ), retro_cost=lambda w: ( resources("retro_cost_base_s_{clusters}.csv") From 38f2dc7a75218ed9c5add44c1fcc00fc42ade45e Mon Sep 17 00:00:00 2001 From: Koen van Greevenbroek <74298901+koen-vg@users.noreply.github.com> Date: Wed, 18 Sep 2024 16:29:05 +0200 Subject: [PATCH 325/344] Only add buses in specified countries (#1308) * Only add buses in specified countries This is necessary for the new OSM-based base network, where loaded buses already have a "country" attribute. * Add release note --- doc/release_notes.rst | 2 ++ scripts/base_network.py | 16 +++++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index dca98d379..ef265859f 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -73,6 +73,8 @@ Upcoming Release * Added 98% imperfect capture rate of Allam cycle gas turbine. +* Resolved a problem where excluding certain countries from `countries` configuration led to clustering errors. + PyPSA-Eur 0.13.0 (13th September 2024) ====================================== diff --git a/scripts/base_network.py b/scripts/base_network.py index a6a54617b..801ce0620 100644 --- a/scripts/base_network.py +++ b/scripts/base_network.py @@ -135,7 +135,7 @@ def _find_closest_links(links, new_links, distance_upper_bound=1.5): ) -def _load_buses(buses, europe_shape, config): +def _load_buses(buses, europe_shape, countries, config): buses = ( pd.read_csv( buses, @@ -161,6 +161,12 @@ def _load_buses(buses, europe_shape, config): lambda p: europe_shape_prepped.contains(Point(p)), axis=1 ) + buses_in_countries_b = ( + buses.country.isin(countries) + if "country" in buses + else pd.Series(True, buses.index) + ) + v_nom_min = min(config["electricity"]["voltages"]) v_nom_max = max(config["electricity"]["voltages"]) @@ -173,7 +179,9 @@ def _load_buses(buses, europe_shape, config): ) logger.info(f"Removing buses outside of range AC {v_nom_min} - {v_nom_max} V") - return pd.DataFrame(buses.loc[buses_in_europe_b & buses_with_v_nom_to_keep_b]) + return pd.DataFrame( + buses.loc[buses_in_europe_b & buses_in_countries_b & buses_with_v_nom_to_keep_b] + ) def _load_transformers(buses, transformers): @@ -712,6 +720,7 @@ def base_network( europe_shape, country_shapes, offshore_shapes, + countries, parameter_corrections, config, ): @@ -736,7 +745,7 @@ def base_network( ) logger.info(logger_str) - buses = _load_buses(buses, europe_shape, config) + buses = _load_buses(buses, europe_shape, countries, config) transformers = _load_transformers(buses, transformers) lines = _load_lines(buses, lines) @@ -1006,6 +1015,7 @@ def append_bus_shapes(n, shapes, type): europe_shape, country_shapes, offshore_shapes, + countries, parameter_corrections, config, ) From 81da6859ee691a7a80cd9309a23eaa740fb661c0 Mon Sep 17 00:00:00 2001 From: Koen van Greevenbroek <74298901+koen-vg@users.noreply.github.com> Date: Fri, 20 Sep 2024 08:54:19 +0200 Subject: [PATCH 326/344] Fix industrial demand for ammonia when endogenously modelled (#1312) * Fix industrial demand for ammonia when endogenously modelled In calculating industrial energy demand today, demand for ammonia must be resolved into just "ammonia" when endogenously modelled, or into electricity and hydrogen if not. * Add release note --- doc/release_notes.rst | 2 + rules/build_sector.smk | 1 + ...ustrial_energy_demand_per_country_today.py | 38 +++++++++++-------- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index ef265859f..18741d613 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -75,6 +75,8 @@ Upcoming Release * Resolved a problem where excluding certain countries from `countries` configuration led to clustering errors. +* Bugfix: demand for ammonia was double-counted at current/near-term planning horizons when ``sector['ammonia']`` was set to ``True``. + PyPSA-Eur 0.13.0 (13th September 2024) ====================================== diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 4ba1f45b4..8de040ff1 100755 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -716,6 +716,7 @@ rule build_industrial_energy_demand_per_country_today: params: countries=config_provider("countries"), industry=config_provider("industry"), + ammonia=config_provider("sector", "ammonia", default=False), input: transformation_output_coke=resources("transformation_output_coke.csv"), jrc="data/jrc-idees-2021", diff --git a/scripts/build_industrial_energy_demand_per_country_today.py b/scripts/build_industrial_energy_demand_per_country_today.py index ef559efa2..5ff91351c 100644 --- a/scripts/build_industrial_energy_demand_per_country_today.py +++ b/scripts/build_industrial_energy_demand_per_country_today.py @@ -133,6 +133,9 @@ def get_subsector_data(sheet): df["hydrogen"] = 0.0 + if snakemake.params.ammonia: + df["ammonia"] = 0.0 + df["other"] = df["all"] - df.loc[df.index != "all"].sum() return df @@ -153,14 +156,6 @@ def get_subsector_data(sheet): def separate_basic_chemicals(demand, production): - - ammonia = pd.DataFrame( - { - "hydrogen": production["Ammonia"] * params["MWh_H2_per_tNH3_electrolysis"], - "electricity": production["Ammonia"] - * params["MWh_elec_per_tNH3_electrolysis"], - } - ).T chlorine = pd.DataFrame( { "hydrogen": production["Chlorine"] * params["MWh_H2_per_tCl"], @@ -174,16 +169,29 @@ def separate_basic_chemicals(demand, production): } ).T - demand["Ammonia"] = ammonia.unstack().reindex(index=demand.index, fill_value=0.0) demand["Chlorine"] = chlorine.unstack().reindex(index=demand.index, fill_value=0.0) demand["Methanol"] = methanol.unstack().reindex(index=demand.index, fill_value=0.0) - demand["HVC"] = ( - demand["Basic chemicals"] - - demand["Ammonia"] - - demand["Methanol"] - - demand["Chlorine"] - ) + demand["HVC"] = demand["Basic chemicals"] - demand["Methanol"] - demand["Chlorine"] + + # Deal with ammonia separately, depending on whether it is modelled endogenously. + ammonia_exo = pd.DataFrame( + { + "hydrogen": production["Ammonia"] * params["MWh_H2_per_tNH3_electrolysis"], + "electricity": production["Ammonia"] + * params["MWh_elec_per_tNH3_electrolysis"], + } + ).T + + if snakemake.params.ammonia: + ammonia = pd.DataFrame( + {"ammonia": production["Ammonia"] * params["MWh_NH3_per_tNH3"]} + ).T + else: + ammonia = ammonia_exo + + demand["Ammonia"] = ammonia.unstack().reindex(index=demand.index, fill_value=0.0) + demand["HVC"] -= ammonia_exo.unstack().reindex(index=demand.index, fill_value=0.0) demand.drop(columns="Basic chemicals", inplace=True) From 9e43aa5b8883f501f16ff3579e7622233ba4f9cf Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 20 Sep 2024 09:01:27 +0200 Subject: [PATCH 327/344] follow-up to #1312: avoid use of snakemake in industrial_energy_demand_per_country --- ...build_industrial_energy_demand_per_country_today.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/scripts/build_industrial_energy_demand_per_country_today.py b/scripts/build_industrial_energy_demand_per_country_today.py index 5ff91351c..f011e61f6 100644 --- a/scripts/build_industrial_energy_demand_per_country_today.py +++ b/scripts/build_industrial_energy_demand_per_country_today.py @@ -121,7 +121,7 @@ jrc_names = {"GR": "EL", "GB": "UK"} -def industrial_energy_demand_per_country(country, year, jrc_dir): +def industrial_energy_demand_per_country(country, year, jrc_dir, endogenous_ammonia): jrc_country = jrc_names.get(country, country) fn = f"{jrc_dir}/{jrc_country}/JRC-IDEES-2021_EnergyBalance_{jrc_country}.xlsx" @@ -133,7 +133,8 @@ def get_subsector_data(sheet): df["hydrogen"] = 0.0 - if snakemake.params.ammonia: + # ammonia is handled separately + if endogenous_ammonia: df["ammonia"] = 0.0 df["other"] = df["all"] - df.loc[df.index != "all"].sum() @@ -220,7 +221,10 @@ def industrial_energy_demand(countries, year): nprocesses = snakemake.threads disable_progress = snakemake.config["run"].get("disable_progressbar", False) func = partial( - industrial_energy_demand_per_country, year=year, jrc_dir=snakemake.input.jrc + industrial_energy_demand_per_country, + year=year, + jrc_dir=snakemake.input.jrc, + endogenous_ammonia=snakemake.params.ammonia, ) tqdm_kwargs = dict( ascii=False, From a6ac2b486556c38e1e0c5922a7c84a3408826255 Mon Sep 17 00:00:00 2001 From: Lukas Trippe Date: Fri, 20 Sep 2024 12:32:27 +0200 Subject: [PATCH 328/344] fix: make `test.sh` executable (#1314) --- .github/workflows/test.yaml | 1 - test.sh | 0 2 files changed, 1 deletion(-) mode change 100644 => 100755 test.sh diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 00599de6a..3435f6fee 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -99,7 +99,6 @@ jobs: - name: Run snakemake test workflows run: | - chmod +x test.sh ./test.sh - name: Upload artifacts diff --git a/test.sh b/test.sh old mode 100644 new mode 100755 From 6387c46c2e859fcd853d3bbe697ba9978c35b1e1 Mon Sep 17 00:00:00 2001 From: Lukas Trippe Date: Mon, 23 Sep 2024 10:13:20 +0200 Subject: [PATCH 329/344] test: use makefile (#1315) * test: use makefile * more make commands --- .github/workflows/test.yaml | 2 +- .github/workflows/update-fixed-env.yaml | 2 +- Makefile | 59 +++++++++++++++++++++++++ envs/environment.fixed.yaml | 2 +- test.sh | 13 ------ 5 files changed, 62 insertions(+), 16 deletions(-) create mode 100755 Makefile delete mode 100755 test.sh diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 3435f6fee..5ac25db80 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -99,7 +99,7 @@ jobs: - name: Run snakemake test workflows run: | - ./test.sh + make test - name: Upload artifacts if: matrix.os == 'ubuntu' && matrix.inhouse == 'stable-inhouse-deps' diff --git a/.github/workflows/update-fixed-env.yaml b/.github/workflows/update-fixed-env.yaml index ec63a9448..425b246ff 100644 --- a/.github/workflows/update-fixed-env.yaml +++ b/.github/workflows/update-fixed-env.yaml @@ -23,7 +23,7 @@ jobs: - name: Update environment.fixed.yaml run: | - conda env export --name pypsa-eur --no-builds > envs/environment.fixed.yaml + conda env export --name pypsa-eur-fixed --no-builds > envs/environment.fixed.yaml - name: Add SPDX header run: | diff --git a/Makefile b/Makefile new file mode 100755 index 000000000..8f2d39010 --- /dev/null +++ b/Makefile @@ -0,0 +1,59 @@ +# SPDX-FileCopyrightText: : 2021-2024 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: CC0-1.0 + +.PHONY: _conda_check install install-fixed test clean-tests reset + +# Helper: Check if conda or mamba is installed and set CONDA_OR_MAMBA variable +_conda_check: + @# Check if conda or mamba is installed and set CONDA_OR_MAMBA variable + @if command -v conda &> /dev/null; then \ + echo "Conda detected, using Conda..."; \ + $(eval CONDA_OR_MAMBA := conda) \ + elif command -v mamba &> /dev/null; then \ + echo "Conda not found, but Mamba detected. Using Mamba..."; \ + $(eval CONDA_OR_MAMBA := mamba) \ + else \ + echo "Neither Conda nor Mamba is installed. Please install one of them and retry."; \ + exit 1; \ + fi + +# Install the environment +install: _conda_check + @$(CONDA_OR_MAMBA) env create -f envs/environment.yaml + @$(CONDA_OR_MAMBA) run -n pypsa-eur pre-commit install + +# Install fixed environment +install-fixed: _conda_check + @$(CONDA_OR_MAMBA) env create -f envs/environment.fixed.yaml + @$(CONDA_OR_MAMBA) run -n pypsa-eur pre-commit install + +# Run default tests +test: + set -e + snakemake -call solve_elec_networks --configfile config/test/config.electricity.yaml --rerun-triggers=mtime + snakemake -call all --configfile config/test/config.overnight.yaml --rerun-triggers=mtime + snakemake -call all --configfile config/test/config.myopic.yaml --rerun-triggers=mtime + snakemake -call make_summary_perfect --configfile config/test/config.perfect.yaml --rerun-triggers=mtime + snakemake -call all --configfile config/test/config.scenarios.yaml --rerun-triggers=mtime -n + echo "All tests completed successfully." + +# Cleans all output files from tests +clean-tests: + snakemake -call solve_elec_networks --configfile config/test/config.electricity.yaml --rerun-triggers=mtime --delete-all-output + snakemake -call all --configfile config/test/config.overnight.yaml --rerun-triggers=mtime --delete-all-output + snakemake -call all --configfile config/test/config.myopic.yaml --rerun-triggers=mtime --delete-all-output + snakemake -call make_summary_perfect --configfile config/test/config.perfect.yaml --rerun-triggers=mtime --delete-all-output + snakemake -call all --configfile config/test/config.scenarios.yaml --rerun-triggers=mtime -n --delete-all-output + +# Removes all created files except for large cutout files (similar to fresh clone) +reset: + @echo "Do you really wanna continue? This will remove logs, resources, benchmarks, results, and .snakemake directories (y/n): " && \ + read ans && [ $${ans} = y ] && ( \ + rm -r ./logs || true; \ + rm -r ./resources || true; \ + rm -r ./benchmarks || true; \ + rm -r ./results || true; \ + rm -r ./.snakemake || true; \ + echo "Reset completed." \ + ) || echo "Reset cancelled." diff --git a/envs/environment.fixed.yaml b/envs/environment.fixed.yaml index aaa0039c7..e45224c2a 100644 --- a/envs/environment.fixed.yaml +++ b/envs/environment.fixed.yaml @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: : 2017-2024 The PyPSA-Eur Authors # SPDX-License-Identifier: CC0-1.0 -name: pypsa-eur +name: pypsa-eur-fixed channels: - conda-forge - bioconda diff --git a/test.sh b/test.sh deleted file mode 100755 index 64566aa2f..000000000 --- a/test.sh +++ /dev/null @@ -1,13 +0,0 @@ -# SPDX-FileCopyrightText: : 2021-2024 The PyPSA-Eur Authors -# -# SPDX-License-Identifier: CC0-1.0 - -set -x && \ - -snakemake -call solve_elec_networks --configfile config/test/config.electricity.yaml --rerun-triggers=mtime && \ -snakemake -call all --configfile config/test/config.overnight.yaml --rerun-triggers=mtime && \ -snakemake -call all --configfile config/test/config.myopic.yaml --rerun-triggers=mtime && \ -snakemake -call make_summary_perfect --configfile config/test/config.perfect.yaml --rerun-triggers=mtime && \ -snakemake -call all --configfile config/test/config.scenarios.yaml --rerun-triggers=mtime -n && \ - -set +x From e2be4d730ea00e413a3c349d4ee72179b1043e0b Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 23 Sep 2024 11:52:54 +0200 Subject: [PATCH 330/344] bugfix: account for kerosene emissions in methanol-to-kerosene link (#1317) * bugfix: account for kerosene emissions in methanol-to-kerosene link * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- scripts/prepare_sector_network.py | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index fcbacd385..8716de45c 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -931,25 +931,10 @@ def add_methanol_to_power(n, costs, types=None): def add_methanol_to_kerosene(n, costs): - nodes = pop_layout.index - nhours = n.snapshot_weightings.generators.sum() - - demand_factor = options["aviation_demand_factor"] - tech = "methanol-to-kerosene" logger.info(f"Adding {tech}.") - all_aviation = ["total international aviation", "total domestic aviation"] - - p_nom_max = ( - demand_factor - * pop_weighted_energy_totals.loc[nodes, all_aviation].sum(axis=1) - * 1e6 - / nhours - * costs.at[tech, "methanol-input"] - ) - capital_cost = costs.at[tech, "fixed"] / costs.at[tech, "methanol-input"] n.madd( @@ -961,12 +946,12 @@ def add_methanol_to_kerosene(n, costs): bus0=spatial.methanol.nodes, bus1=spatial.oil.kerosene, bus2=spatial.h2.nodes, + bus3="co2 atmosphere", efficiency=costs.at[tech, "methanol-input"], efficiency2=-costs.at[tech, "hydrogen-input"] / costs.at[tech, "methanol-input"], + efficiency3=costs.at["oil", "CO2 intensity"] / costs.at[tech, "methanol-input"], p_nom_extendable=True, - p_min_pu=1, - p_nom_max=p_nom_max.values, ) From 1a64345067e4122643f2d8ea9cf7e77793498e3d Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 23 Sep 2024 12:01:27 +0200 Subject: [PATCH 331/344] fix retrieve_worldbank_urban_population (#1318) --- rules/retrieve.smk | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/rules/retrieve.smk b/rules/retrieve.smk index 69ea0c4b1..9d2d3a5b8 100755 --- a/rules/retrieve.smk +++ b/rules/retrieve.smk @@ -346,23 +346,28 @@ if config["enable"]["retrieve"]: rule retrieve_worldbank_urban_population: params: - zip="data/worldbank/API_SP.URB.TOTL.IN.ZS_DS2_en_csv_v2_3403768.zip", + zip="data/worldbank/API_SP.URB.TOTL.IN.ZS_DS2_en_csv_v2.zip", output: - gpkg="data/worldbank/API_SP.URB.TOTL.IN.ZS_DS2_en_csv_v2_3403768.csv", + gpkg="data/worldbank/API_SP.URB.TOTL.IN.ZS_DS2_en_csv_v2.csv", run: import os import requests response = requests.get( "https://api.worldbank.org/v2/en/indicator/SP.URB.TOTL.IN.ZS?downloadformat=csv", - params={"name": "API_SP.URB.TOTL.IN.ZS_DS2_en_csv_v2_3403768.zip"}, ) with open(params["zip"], "wb") as f: f.write(response.content) output_folder = Path(params["zip"]).parent unpack_archive(params["zip"], output_folder) - os.remove(params["zip"]) + + for f in os.listdir(output_folder): + if f.startswith( + "API_SP.URB.TOTL.IN.ZS_DS2_en_csv_v2_" + ) and f.endswith(".csv"): + os.rename(os.path.join(output_folder, f), output.gpkg) + break From 0694cc60b85e05c0c230621dd0c853cf6ee71ced Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 23 Sep 2024 15:52:32 +0200 Subject: [PATCH 332/344] follow-up to #1318 --- rules/build_sector.smk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 8de040ff1..f5e395643 100755 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -6,7 +6,7 @@ rule build_population_layouts: input: nuts3_shapes=resources("nuts3_shapes.geojson"), - urban_percent="data/worldbank/API_SP.URB.TOTL.IN.ZS_DS2_en_csv_v2_3403768.csv", + urban_percent="data/worldbank/API_SP.URB.TOTL.IN.ZS_DS2_en_csv_v2.csv", cutout=lambda w: "cutouts/" + CDIR + config_provider("atlite", "default_cutout")(w) From 343aba1db943598d9c37d95bf7efbfa630b19458 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 23 Sep 2024 15:52:57 +0200 Subject: [PATCH 333/344] Methanol-to-kerosene: correct efficiency and add VOM (#1320) * MtK: add VOM * MtK: fix methanol input efficiency --- scripts/add_electricity.py | 6 +++++- scripts/prepare_sector_network.py | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/add_electricity.py b/scripts/add_electricity.py index 82c78a28a..1bf690ba3 100755 --- a/scripts/add_electricity.py +++ b/scripts/add_electricity.py @@ -231,10 +231,14 @@ def load_costs(tech_costs, config, max_hours, Nyears=1.0): # set all asset costs and other parameters costs = pd.read_csv(tech_costs, index_col=[0, 1]).sort_index() - # correct units to MW + # correct units from kW to MW costs.loc[costs.unit.str.contains("/kW"), "value"] *= 1e3 costs.unit = costs.unit.str.replace("/kW", "/MW") + # correct units from GW to MW + costs.loc[costs.unit.str.contains("/GW"), "value"] /= 1e3 + costs.unit = costs.unit.str.replace("/GW", "/MW") + fill_values = config["fill_values"] costs = costs.value.unstack().fillna(fill_values) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 8716de45c..66fffcec5 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -943,11 +943,12 @@ def add_methanol_to_kerosene(n, costs): suffix=f" {tech}", carrier=tech, capital_cost=capital_cost, + marginal_cost=costs.at[tech, "VOM"], bus0=spatial.methanol.nodes, bus1=spatial.oil.kerosene, bus2=spatial.h2.nodes, bus3="co2 atmosphere", - efficiency=costs.at[tech, "methanol-input"], + efficiency=1 / costs.at[tech, "methanol-input"], efficiency2=-costs.at[tech, "hydrogen-input"] / costs.at[tech, "methanol-input"], efficiency3=costs.at["oil", "CO2 intensity"] / costs.at[tech, "methanol-input"], From 8ed765429443fdb71cadb36a1573497764369c32 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 23 Sep 2024 15:53:11 +0200 Subject: [PATCH 334/344] myopic: ensure all links with capital costs have lifetime (#1319) * myopic: ensure all links with capital costs have lifetime * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- scripts/prepare_sector_network.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 66fffcec5..dc04f5bd3 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -953,6 +953,7 @@ def add_methanol_to_kerosene(n, costs): / costs.at[tech, "methanol-input"], efficiency3=costs.at["oil", "CO2 intensity"] / costs.at[tech, "methanol-input"], p_nom_extendable=True, + lifetime=costs.at[tech, "lifetime"], ) @@ -2772,6 +2773,7 @@ def add_biomass(n, costs): efficiency=costs.at["biogas", "efficiency"], efficiency2=-costs.at["gas", "CO2 intensity"], p_nom_extendable=True, + lifetime=costs.at["biogas", "lifetime"], ) if options["biogas_upgrading_cc"]: @@ -2799,6 +2801,7 @@ def add_biomass(n, costs): - costs.at["biogas CC", "CO2 stored"] * costs.at["biogas CC", "capture rate"], p_nom_extendable=True, + lifetime=costs.at["biogas CC", "lifetime"], ) if options["biomass_transport"]: @@ -3155,6 +3158,7 @@ def add_biomass(n, costs): + costs.at["biomass CHP capture", "fixed"] * costs.at["solid biomass", "CO2 intensity"], marginal_cost=0.0, + lifetime=25, # TODO: add value to technology-data ) @@ -4466,6 +4470,7 @@ def add_enhanced_geothermal(n, egs_potentials, egs_overlap, costs): p_nom_max=p_nom_max.set_axis(well_name) / efficiency_orc, capital_cost=capital_cost.set_axis(well_name) * efficiency_orc, efficiency=bus_eta, + lifetime=costs.at["geothermal", "lifetime"], ) # adding Organic Rankine Cycle as a single link @@ -4478,6 +4483,7 @@ def add_enhanced_geothermal(n, egs_potentials, egs_overlap, costs): carrier="geothermal organic rankine cycle", capital_cost=orc_capital_cost * efficiency_orc, efficiency=efficiency_orc, + lifetime=costs.at["organic rankine cycle", "lifetime"], ) if as_chp and bus + " urban central heat" in n.buses.index: @@ -4493,6 +4499,7 @@ def add_enhanced_geothermal(n, egs_potentials, egs_overlap, costs): / 100.0, efficiency=efficiency_dh, p_nom_extendable=True, + lifetime=costs.at["geothermal", "lifetime"], ) elif as_chp and not bus + " urban central heat" in n.buses.index: n.links.at[bus + " geothermal organic rankine cycle", "efficiency"] = ( From 0d1c36ba6e538d9cb0819509e2d4afb9b3991872 Mon Sep 17 00:00:00 2001 From: Uzair Aftab <48220549+Uzaaft@users.noreply.github.com> Date: Tue, 24 Sep 2024 08:42:01 +0200 Subject: [PATCH 335/344] fix: move pypsa dependency to pip (#1322) --- envs/environment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envs/environment.yaml b/envs/environment.yaml index 9415a27f6..9fd7aa00a 100644 --- a/envs/environment.yaml +++ b/envs/environment.yaml @@ -11,7 +11,6 @@ dependencies: - pip - atlite>=0.2.9 -- pypsa>=0.30.2 - linopy - dask @@ -63,3 +62,4 @@ dependencies: - snakemake-executor-plugin-slurm - snakemake-executor-plugin-cluster-generic - highspy + - pypsa>=0.30.2 From e65ce91a73df87e8ee064ad78df33909ee797fa2 Mon Sep 17 00:00:00 2001 From: Lukas Trippe Date: Tue, 24 Sep 2024 10:26:03 +0200 Subject: [PATCH 336/344] fix: failing macos pypsa installation (#1325) --- .github/workflows/test.yaml | 11 +---------- envs/environment.yaml | 3 ++- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 5ac25db80..323405636 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -76,18 +76,9 @@ jobs: id: cache-env - name: Update environment - if: steps.cache-env.outputs.cache-hit != 'true' && matrix.os != 'macos' + if: steps.cache-env.outputs.cache-hit != 'true' run: conda env update -n pypsa-eur -f envs/environment.yaml - # Temporary fix for MacOS, since highspy with pypsa can not be resolved - - name: Update environment (macos specific) - if: steps.cache-env.outputs.cache-hit != 'true' && matrix.os == 'macos' - run: | - sed -i '' '/- pypsa/d' envs/environment.yaml # Remove pypsa from environment list - conda env update -n pypsa-eur -f envs/environment.yaml - pip install highspy - conda install -c conda-forge pypsa - - name: Install inhouse packages from master if: matrix.inhouse == 'dev-inhouse-deps' run: | diff --git a/envs/environment.yaml b/envs/environment.yaml index 9fd7aa00a..8ee9806a9 100644 --- a/envs/environment.yaml +++ b/envs/environment.yaml @@ -10,8 +10,10 @@ dependencies: - python>=3.8 - pip +- pypsa>=0.30.2 - atlite>=0.2.9 - linopy + - dask # Dependencies of the workflow itself @@ -62,4 +64,3 @@ dependencies: - snakemake-executor-plugin-slurm - snakemake-executor-plugin-cluster-generic - highspy - - pypsa>=0.30.2 From 51f8c2935ae3f80b15ad0ab71708ad006ad49da9 Mon Sep 17 00:00:00 2001 From: Toni Seibold <153275395+toniseibold@users.noreply.github.com> Date: Tue, 24 Sep 2024 22:25:15 +0200 Subject: [PATCH 337/344] Adding config for post discretization bugfix (#1309) * Adding config for post discretization bugfix --- config/config.default.yaml | 1 + doc/configtables/solving.csv | 1 + envs/environment.yaml | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index e67a3d830..1cb19dbbf 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -915,6 +915,7 @@ solving: DC: 0.3 H2 pipeline: 0.3 gas pipeline: 0.3 + fractional_last_unit_size: false agg_p_nom_limits: agg_offwind: false diff --git a/doc/configtables/solving.csv b/doc/configtables/solving.csv index d2e22c28c..ec1080d23 100644 --- a/doc/configtables/solving.csv +++ b/doc/configtables/solving.csv @@ -23,6 +23,7 @@ options,,, -- -- -- {carrier},,, -- -- link_threshold,,float,The threshold relative to the discrete link unit size beyond which to round up to the next unit by carrier (given in dictionary style). -- -- -- {carrier},,, +-- -- fractional_last_unit_size,bool,"{'true','false'}",When true, links and lines can be built up to p_nom_max. When false, they can only be built up to a multiple of the unit size. agg_p_nom_limits,,,Configure per carrier generator nominal capacity constraints for individual countries if ``'CCL'`` is in ``{opts}`` wildcard. -- agg_offwind,bool,"{'true','false'}",Aggregate together all the types of offwind when writing the constraint. Default is false. -- include_existing,bool,"{'true','false'}",Take existing capacities into account when writing the constraint. Default is false. diff --git a/envs/environment.yaml b/envs/environment.yaml index 8ee9806a9..9352d56ba 100644 --- a/envs/environment.yaml +++ b/envs/environment.yaml @@ -10,7 +10,7 @@ dependencies: - python>=3.8 - pip -- pypsa>=0.30.2 +- pypsa>=0.30.3 - atlite>=0.2.9 - linopy From c5ebb66f7248b6770d43d7f18b188d5cfa737534 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Sep 2024 08:24:19 +0200 Subject: [PATCH 338/344] Bump lkstrp/pypsa-validator in the github-actions group (#1329) Bumps the github-actions group with 1 update: [lkstrp/pypsa-validator](https://github.com/lkstrp/pypsa-validator). Updates `lkstrp/pypsa-validator` from 0.2.1 to 0.2.2 - [Release notes](https://github.com/lkstrp/pypsa-validator/releases) - [Commits](https://github.com/lkstrp/pypsa-validator/compare/v0.2.1...v0.2.2) --- updated-dependencies: - dependency-name: lkstrp/pypsa-validator dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/validate.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index eb657c33d..d9802d2f2 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -16,7 +16,7 @@ jobs: if: github.event.pull_request.head.repo.full_name == github.repository runs-on: self-hosted steps: - - uses: lkstrp/pypsa-validator@v0.2.1 + - uses: lkstrp/pypsa-validator@v0.2.2 with: step: run-self-hosted-validation env_file: envs/environment.yaml @@ -30,7 +30,7 @@ jobs: needs: run-validation runs-on: ubuntu-latest steps: - - uses: lkstrp/pypsa-validator@v0.2.1 + - uses: lkstrp/pypsa-validator@v0.2.2 with: step: create-comment snakemake_config: config/test/config.validator.yaml From 79cea531464aebaf4690eda768a3bc0f7c544b0e Mon Sep 17 00:00:00 2001 From: Toni Seibold <153275395+toniseibold@users.noreply.github.com> Date: Wed, 25 Sep 2024 13:23:50 +0200 Subject: [PATCH 339/344] Reduce retrofit potential in myopic optimization (#1258) * reduce potential of gas network that can be retrofitted to H2 when gas grid is not resolved in myopic optimization * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * adding comment --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- scripts/add_brownfield.py | 40 ++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/scripts/add_brownfield.py b/scripts/add_brownfield.py index 65e55f968..4ccb65f97 100644 --- a/scripts/add_brownfield.py +++ b/scripts/add_brownfield.py @@ -87,30 +87,48 @@ def add_brownfield(n, n_p, year): n.import_series_from_dataframe(c.pnl[tattr], c.name, tattr) # deal with gas network - pipe_carrier = ["gas pipeline"] if snakemake.params.H2_retrofit: - # subtract the already retrofitted from today's gas grid capacity + # subtract the already retrofitted from the maximum capacity h2_retrofitted_fixed_i = n.links[ (n.links.carrier == "H2 pipeline retrofitted") & (n.links.build_year != year) ].index - gas_pipes_i = n.links[n.links.carrier.isin(pipe_carrier)].index - CH4_per_H2 = 1 / snakemake.params.H2_retrofit_capacity_per_CH4 - fr = "H2 pipeline retrofitted" - to = "gas pipeline" - # today's pipe capacity - pipe_capacity = n.links.loc[gas_pipes_i, "p_nom"] + h2_retrofitted = n.links[ + (n.links.carrier == "H2 pipeline retrofitted") + & (n.links.build_year == year) + ].index + + # pipe capacity always set in prepare_sector_network to todays gas grid capacity * H2_per_CH4 + # and is therefore constant up to this point + pipe_capacity = n.links.loc[h2_retrofitted, "p_nom_max"] # already retrofitted capacity from gas -> H2 already_retrofitted = ( n.links.loc[h2_retrofitted_fixed_i, "p_nom"] - .rename(lambda x: x.split("-2")[0].replace(fr, to) + f"-{year}") + .rename(lambda x: x.split("-2")[0] + f"-{year}") .groupby(level=0) .sum() ) - remaining_capacity = pipe_capacity - CH4_per_H2 * already_retrofitted.reindex( + remaining_capacity = pipe_capacity - already_retrofitted.reindex( index=pipe_capacity.index ).fillna(0) - n.links.loc[gas_pipes_i, "p_nom"] = remaining_capacity + n.links.loc[h2_retrofitted, "p_nom_max"] = remaining_capacity + + # reduce gas network capacity + gas_pipes_i = n.links[n.links.carrier == "gas pipeline"].index + if not gas_pipes_i.empty: + # subtract the already retrofitted from today's gas grid capacity + pipe_capacity = n.links.loc[gas_pipes_i, "p_nom"] + fr = "H2 pipeline retrofitted" + to = "gas pipeline" + CH4_per_H2 = 1 / snakemake.params.H2_retrofit_capacity_per_CH4 + already_retrofitted.index = already_retrofitted.index.str.replace(fr, to) + remaining_capacity = ( + pipe_capacity + - CH4_per_H2 + * already_retrofitted.reindex(index=pipe_capacity.index).fillna(0) + ) + n.links.loc[gas_pipes_i, "p_nom"] = remaining_capacity + n.links.loc[gas_pipes_i, "p_nom_max"] = remaining_capacity def disable_grid_expansion_if_limit_hit(n): From 24e0ddd5a6883b5df466229b56bd4429946291d6 Mon Sep 17 00:00:00 2001 From: Lukas Trippe Date: Wed, 25 Sep 2024 15:45:32 +0200 Subject: [PATCH 340/344] prepare for new `n.add` (#1316) * prepare for new `n.add` * Update scripts/prepare_sector_network.py Co-authored-by: Fabian Neumann --------- Co-authored-by: Fabian Neumann --- scripts/prepare_sector_network.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index dc04f5bd3..f65e2d7b3 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1269,7 +1269,7 @@ def insert_electricity_distribution_grid(n, costs): bus=n.generators.loc[solar, "bus"] + " low voltage", carrier="solar rooftop", p_nom_extendable=True, - p_nom_max=potential, + p_nom_max=potential.loc[solar], marginal_cost=n.generators.loc[solar, "marginal_cost"], capital_cost=costs.at["solar-rooftop", "fixed"], efficiency=n.generators.loc[solar, "efficiency"], @@ -1821,7 +1821,7 @@ def add_EVs( suffix=" land transport EV", bus=spatial.nodes + " EV battery", carrier="land transport EV", - p_set=profile, + p_set=profile.loc[n.snapshots], ) p_nom = number_cars * options["bev_charge_rate"] * electric_share @@ -1834,7 +1834,7 @@ def add_EVs( bus1=spatial.nodes + " EV battery", p_nom=p_nom, carrier="BEV charger", - p_max_pu=avail_profile[spatial.nodes], + p_max_pu=avail_profile.loc[n.snapshots, spatial.nodes], lifetime=1, efficiency=options["bev_charge_efficiency"], ) @@ -1848,7 +1848,7 @@ def add_EVs( bus0=spatial.nodes + " EV battery", p_nom=p_nom, carrier="V2G", - p_max_pu=avail_profile[spatial.nodes], + p_max_pu=avail_profile.loc[n.snapshots, spatial.nodes], lifetime=1, efficiency=options["bev_charge_efficiency"], ) @@ -1870,7 +1870,7 @@ def add_EVs( e_cyclic=True, e_nom=e_nom, e_max_pu=1, - e_min_pu=dsm_profile[spatial.nodes], + e_min_pu=dsm_profile.loc[n.snapshots, spatial.nodes], ) @@ -2152,7 +2152,7 @@ def add_heat(n: pypsa.Network, costs: pd.DataFrame, cop: xr.DataArray): suffix=f" {heat_system} heat", bus=nodes + f" {heat_system} heat", carrier=f"{heat_system} heat", - p_set=heat_load, + p_set=heat_load.loc[n.snapshots], ) ## Add heat pumps From d4bad07cf461c1b60700a3213f5d650b2da64c06 Mon Sep 17 00:00:00 2001 From: Philipp Glaum <95913147+p-glaum@users.noreply.github.com> Date: Wed, 25 Sep 2024 16:58:35 +0200 Subject: [PATCH 341/344] build_transmission_projects: set s_max_pu to the value according to config (#1323) --- rules/build_electricity.smk | 1 + scripts/build_transmission_projects.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/rules/build_electricity.smk b/rules/build_electricity.smk index be97270aa..10d253f2a 100755 --- a/rules/build_electricity.smk +++ b/rules/build_electricity.smk @@ -386,6 +386,7 @@ rule build_transmission_projects: params: transmission_projects=config_provider("transmission_projects"), line_factor=config_provider("lines", "length_factor"), + s_max_pu=config_provider("lines", "s_max_pu"), input: base_network=resources("networks/base.nc"), offshore_shapes=resources("offshore_shapes.geojson"), diff --git a/scripts/build_transmission_projects.py b/scripts/build_transmission_projects.py index ba185d17e..8dbc044bb 100644 --- a/scripts/build_transmission_projects.py +++ b/scripts/build_transmission_projects.py @@ -482,6 +482,7 @@ def fill_length_from_geometry(line, line_factor=1.2): set_scenario_config(snakemake) line_factor = snakemake.params.line_factor + s_max_pu = snakemake.params.s_max_pu n = pypsa.Network(snakemake.input.base_network) @@ -543,6 +544,8 @@ def fill_length_from_geometry(line, line_factor=1.2): * new_lines_df["v_nom"] * new_lines_df["num_parallel"] ).round(2) + # set s_max_pu + new_lines_df["s_max_pu"] = s_max_pu if not new_links_df.empty: # Add carrier types of lines and links new_links_df["carrier"] = "DC" From 991916370f810a5588b2788ac9a301221e125de3 Mon Sep 17 00:00:00 2001 From: Lukas Trippe Date: Fri, 27 Sep 2024 13:07:02 +0200 Subject: [PATCH 342/344] ci: better inhouse checks (#1331) * ci: better inhouse checks * fix: no uv --- .github/workflows/test.yaml | 124 +++++++++++++++++++++------- config/test/config.electricity.yaml | 4 +- config/test/config.myopic.yaml | 4 +- config/test/config.overnight.yaml | 4 +- config/test/config.perfect.yaml | 4 +- config/test/config.scenarios.yaml | 4 +- 6 files changed, 104 insertions(+), 40 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 323405636..f780e4d55 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -19,9 +19,12 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + ENV_FILE: envs/environment.yaml + jobs: - run: - name: Run + run-tests: + name: OS strategy: fail-fast: false matrix: @@ -29,14 +32,6 @@ jobs: - macos - windows - ubuntu - inhouse: - - stable-inhouse-deps - - dev-inhouse-deps - exclude: - - os: macos - inhouse: dev-inhouse-deps - - os: windows - inhouse: dev-inhouse-deps runs-on: ${{ matrix.os }}-latest defaults: @@ -46,57 +41,126 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Setup secrets + - name: Setup secrets & cache dates run: | echo -ne "url: ${CDSAPI_URL}\nkey: ${CDSAPI_TOKEN}\n" > ~/.cdsapirc + echo "week=$(date +'%Y%U')" >> $GITHUB_ENV # data and cutouts + echo "today=$(/bin/date -u '+%Y%m%d')" >> $GITHUB_ENV # env + + - uses: actions/cache@v4 + with: + path: | + data + cutouts + key: data-cutouts-${{ env.week }} + + - uses: conda-incubator/setup-miniconda@v3 + with: + activate-environment: pypsa-eur + + - name: Cache Conda env + uses: actions/cache@v4 + with: + path: ${{ env.CONDA }}/envs + key: conda-${{ runner.os }}-${{ runner.arch }}-${{ env.today }}-${hashFiles('${{ env.ENV_FILE }}')} + id: cache-env + + - name: Update environment + if: steps.cache-env.outputs.cache-hit != 'true' + run: conda env update -n pypsa-eur -f ${{ env.ENV_FILE }} - - name: Set cache dates + - name: Run snakemake test workflows run: | + conda activate pypsa-eur + make test + + - name: Upload artifacts + uses: actions/upload-artifact@v3.2.1-node20 + with: + name: logs-${{ matrix.inhouse }} + path: | + logs + .snakemake/log + retention-days: 3 + + run-tests-on-dev-deps: + name: Inhouse + strategy: + fail-fast: false + matrix: + inhouse: + - pypsa + - atlite + - powerplantmatching + - linopy + runs-on: ubuntu-latest + + defaults: + run: + shell: bash -l {0} + + steps: + - uses: actions/checkout@v4 + + # Only run checks if package is not pinned + - name: Check if inhouse package is pinned + run: | + grep_line=$(grep -- '- ${{ matrix.inhouse }}' ${{ env.ENV_FILE }}) + if [[ $grep_line == *"<"* || $grep_line == *"=="* ]]; then + echo "pinned=true" >> $GITHUB_ENV + else + echo "pinned=false" >> $GITHUB_ENV + fi + + - name: Setup secrets & cache dates + if: env.pinned == 'false' + run: | + echo -ne "url: ${CDSAPI_URL}\nkey: ${CDSAPI_TOKEN}\n" > ~/.cdsapirc echo "week=$(date +'%Y%U')" >> $GITHUB_ENV # data and cutouts echo "today=$(/bin/date -u '+%Y%m%d')" >> $GITHUB_ENV # env - - name: Cache data and cutouts folders - uses: actions/cache@v4 + - uses: actions/cache@v4 + if: env.pinned == 'false' with: path: | data cutouts key: data-cutouts-${{ env.week }} - - name: Setup Conda - uses: conda-incubator/setup-miniconda@v3 + - uses: conda-incubator/setup-miniconda@v3 + if: env.pinned == 'false' with: activate-environment: pypsa-eur - name: Cache Conda env + if: env.pinned == 'false' uses: actions/cache@v4 with: path: ${{ env.CONDA }}/envs - key: conda-${{ runner.os }}--${{ runner.arch }}--${{ env.today }}-${{ hashFiles('envs/environment.yaml') }} + key: conda-${{ runner.os }}-${{ runner.arch }}-${{ matrix.inhouse }}-${{ env.today }}-${hashFiles('${{ env.ENV_FILE }}')} id: cache-env - name: Update environment - if: steps.cache-env.outputs.cache-hit != 'true' - run: conda env update -n pypsa-eur -f envs/environment.yaml + if: env.pinned == 'false' && steps.cache-env.outputs.cache-hit != 'true' + run: conda env update -n pypsa-eur -f ${{ env.ENV_FILE }} - name: Install inhouse packages from master - if: matrix.inhouse == 'dev-inhouse-deps' + if: env.pinned == 'false' run: | - python -m pip install uv - uv pip install git+https://github.com/PyPSA/pypsa.git@master - uv pip install git+https://github.com/PyPSA/atlite.git@master - uv pip install git+https://github.com/PyPSA/powerplantmatching.git@master - uv pip install git+https://github.com/PyPSA/linopy.git@master + python -m pip install git+https://github.com/PyPSA/${{ matrix.inhouse }}.git@master - name: Run snakemake test workflows + if: env.pinned == 'false' run: | + conda activate pypsa-eur make test - name: Upload artifacts - if: matrix.os == 'ubuntu' && matrix.inhouse == 'stable-inhouse-deps' - uses: actions/upload-artifact@v4.4.0 + if: env.pinned == 'false' + uses: actions/upload-artifact@v4 with: - name: resources-results + name: logs-${{ matrix.inhouse }} path: | - results - retention-days: 7 + logs + .snakemake/log + retention-days: 3 diff --git a/config/test/config.electricity.yaml b/config/test/config.electricity.yaml index 38fa31abf..147d735f3 100644 --- a/config/test/config.electricity.yaml +++ b/config/test/config.electricity.yaml @@ -79,8 +79,8 @@ lines: solving: solver: - name: glpk - options: "glpk-default" + name: highs + options: highs-default plotting: diff --git a/config/test/config.myopic.yaml b/config/test/config.myopic.yaml index 0ede9aa77..ba4c77795 100644 --- a/config/test/config.myopic.yaml +++ b/config/test/config.myopic.yaml @@ -86,8 +86,8 @@ industry: solving: solver: - name: glpk - options: glpk-default + name: highs + options: highs-default mem: 4000 plotting: diff --git a/config/test/config.overnight.yaml b/config/test/config.overnight.yaml index 92379ae27..e14ff86ba 100644 --- a/config/test/config.overnight.yaml +++ b/config/test/config.overnight.yaml @@ -80,8 +80,8 @@ industry: solving: solver: - name: glpk - options: glpk-default + name: highs + options: highs-default mem: 4000 plotting: diff --git a/config/test/config.perfect.yaml b/config/test/config.perfect.yaml index 781b3fd49..742b30124 100644 --- a/config/test/config.perfect.yaml +++ b/config/test/config.perfect.yaml @@ -85,8 +85,8 @@ industry: solving: solver: - name: glpk - options: glpk-default + name: highs + options: highs-default mem: 4000 plotting: diff --git a/config/test/config.scenarios.yaml b/config/test/config.scenarios.yaml index a9a826fdc..d238703db 100644 --- a/config/test/config.scenarios.yaml +++ b/config/test/config.scenarios.yaml @@ -57,5 +57,5 @@ renewable: solving: solver: - name: glpk - options: "glpk-default" + name: highs + options: highs-default From 0bf72cac6f1ea0a9b22185a779fa965b2ccb5e2b Mon Sep 17 00:00:00 2001 From: Lukas Trippe Date: Fri, 27 Sep 2024 13:58:41 +0200 Subject: [PATCH 343/344] build: pin `rasterio<1.4` (#1334) * build: pin `rasterio<1.4` * fix fixed updater * run tests daily --- .github/workflows/test.yaml | 2 +- .github/workflows/update-fixed-env.yaml | 4 ++-- envs/environment.yaml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index f780e4d55..4308276fb 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -12,7 +12,7 @@ on: branches: - master schedule: - - cron: "0 5 * * TUE" + - cron: "0 5 * * *" # Cancel any in-progress runs when a new run is triggered concurrency: diff --git a/.github/workflows/update-fixed-env.yaml b/.github/workflows/update-fixed-env.yaml index 425b246ff..22c061908 100644 --- a/.github/workflows/update-fixed-env.yaml +++ b/.github/workflows/update-fixed-env.yaml @@ -18,12 +18,12 @@ jobs: - name: Setup conda uses: conda-incubator/setup-miniconda@v3 with: - activate-environment: pypsa-eur + activate-environment: ${{ github.event.repository.name }} environment-file: envs/environment.yaml - name: Update environment.fixed.yaml run: | - conda env export --name pypsa-eur-fixed --no-builds > envs/environment.fixed.yaml + conda env export --name ${{ github.event.repository.name }} --no-builds | sed 's/^name: ${{ github.event.repository.name }}$/name: ${{ github.event.repository.name }}-fixed/' > envs/environment.fixed.yaml - name: Add SPDX header run: | diff --git a/envs/environment.yaml b/envs/environment.yaml index 9352d56ba..67ad6d09f 100644 --- a/envs/environment.yaml +++ b/envs/environment.yaml @@ -56,7 +56,7 @@ dependencies: # GIS dependencies: - cartopy - descartes -- rasterio!=1.2.10 +- rasterio<1.4 - pip: - tsam>=2.3.1 From f183736b098feec15da83dda724c8efefbd0c363 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 27 Sep 2024 14:02:25 +0200 Subject: [PATCH 344/344] [github-actions.ci] Update fixed environment (#1335) * [create-pull-request] automated change * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: lkstrp <62255395+lkstrp@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- envs/environment.fixed.yaml | 94 ++++++++++++++++++------------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/envs/environment.fixed.yaml b/envs/environment.fixed.yaml index e45224c2a..42d9bf73a 100644 --- a/envs/environment.fixed.yaml +++ b/envs/environment.fixed.yaml @@ -19,19 +19,19 @@ dependencies: - atk-1.0=2.38.0 - atlite=0.2.14 - attrs=24.2.0 -- aws-c-auth=0.7.30 +- aws-c-auth=0.7.31 - aws-c-cal=0.7.4 - aws-c-common=0.9.28 - aws-c-compression=0.2.19 - aws-c-event-stream=0.4.3 -- aws-c-http=0.8.9 +- aws-c-http=0.8.10 - aws-c-io=0.14.18 -- aws-c-mqtt=0.10.5 -- aws-c-s3=0.6.5 +- aws-c-mqtt=0.10.6 +- aws-c-s3=0.6.6 - aws-c-sdkutils=0.1.19 -- aws-checksums=0.1.18 -- aws-crt-cpp=0.28.2 -- aws-sdk-cpp=1.11.379 +- aws-checksums=0.1.20 +- aws-crt-cpp=0.28.3 +- aws-sdk-cpp=1.11.407 - azure-core-cpp=1.13.0 - azure-identity-cpp=1.8.0 - azure-storage-blobs-cpp=12.12.0 @@ -49,7 +49,7 @@ dependencies: - c-ares=1.33.1 - c-blosc2=2.15.1 - ca-certificates=2024.8.30 -- cads-api-client=1.3.2 +- cads-api-client=1.4.2 - cairo=1.18.0 - cartopy=0.23.0 - cdsapi=0.7.3 @@ -96,8 +96,8 @@ dependencies: - exceptiongroup=1.2.2 - executing=2.1.0 - expat=2.6.3 -- filelock=3.16.0 -- fiona=1.10.0 +- filelock=3.16.1 +- fiona=1.10.1 - fmt=11.0.2 - folium=0.17.0 - font-ttf-dejavu-sans-mono=2.37 @@ -107,7 +107,7 @@ dependencies: - fontconfig=2.14.2 - fonts-conda-ecosystem=1 - fonts-conda-forge=1 -- fonttools=4.53.1 +- fonttools=4.54.1 - freetype=2.12.1 - freexl=2.0.0 - fribidi=1.0.10 @@ -120,7 +120,7 @@ dependencies: - geopandas=1.0.1 - geopandas-base=1.0.1 - geopy=2.4.1 -- geos=3.12.2 +- geos=3.13.0 - geotiff=1.7.3 - gflags=2.2.2 - giflib=5.2.2 @@ -137,7 +137,6 @@ dependencies: - harfbuzz=9.0.0 - hdf4=4.2.15 - hdf5=1.14.3 -- highspy=1.7.2 - hpack=4.0.0 - humanfriendly=10.0 - hyperframe=6.0.1 @@ -155,7 +154,7 @@ dependencies: - jinja2=3.1.4 - joblib=1.4.2 - jpype1=1.5.0 -- json-c=0.17 +- json-c=0.18 - jsonschema=4.23.0 - jsonschema-specifications=2023.12.1 - jupyter_core=5.7.2 @@ -164,7 +163,7 @@ dependencies: - kiwisolver=1.4.7 - krb5=1.21.3 - lcms2=2.16 -- ld_impl_linux-64=2.40 +- ld_impl_linux-64=2.43 - lerc=4.0.0 - libabseil=20240116.2 - libaec=1.1.3 @@ -178,11 +177,11 @@ dependencies: - libbrotlidec=1.1.0 - libbrotlienc=1.1.0 - libcblas=3.9.0 -- libclang-cpp18.1=18.1.8 -- libclang13=18.1.8 +- libclang-cpp19.1=19.1.0 +- libclang13=19.1.0 - libcrc32c=1.1.2 - libcups=2.3.3 -- libcurl=8.10.0 +- libcurl=8.10.1 - libdeflate=1.21 - libdrm=2.4.123 - libedit=3.1.20191231 @@ -212,7 +211,7 @@ dependencies: - libgfortran-ng=14.1.0 - libgfortran5=14.1.0 - libgl=1.7.0 -- libglib=2.80.3 +- libglib=2.82.1 - libglvnd=1.7.0 - libglx=1.7.0 - libgomp=14.1.0 @@ -225,11 +224,12 @@ dependencies: - libkml=1.3.0 - liblapack=3.9.0 - liblapacke=3.9.0 -- libllvm18=18.1.8 +- libllvm19=19.1.0 - libnetcdf=4.9.2 - libnghttp2=1.58.0 - libnsl=2.0.1 - libopenblas=0.3.27 +- libopengl=1.7.0 - libparquet=17.0.0 - libpciaccess=0.18 - libpng=1.6.44 @@ -246,16 +246,16 @@ dependencies: - libstdcxx=14.1.0 - libstdcxx-ng=14.1.0 - libthrift=0.20.0 -- libtiff=4.6.0 +- libtiff=4.7.0 - libutf8proc=2.8.0 - libuuid=2.38.1 - libwebp-base=1.4.0 -- libxcb=1.16 +- libxcb=1.17.0 - libxcrypt=4.4.36 - libxkbcommon=1.7.0 - libxml2=2.12.7 - libxslt=1.1.39 -- libzip=1.10.1 +- libzip=1.11.1 - libzlib=1.3.1 - linopy=0.3.14 - locket=1.0.0 @@ -263,7 +263,7 @@ dependencies: - lz4=4.3.3 - lz4-c=1.9.4 - lzo=2.10 -- mapclassify=2.8.0 +- mapclassify=2.8.1 - markupsafe=2.1.5 - matplotlib=3.9.2 - matplotlib-base=3.9.2 @@ -286,7 +286,7 @@ dependencies: - nodeenv=1.9.1 - nomkl=1.0 - nspr=4.35 -- nss=3.104 +- nss=3.105 - numexpr=2.10.0 - numpy=1.26.4 - openjpeg=2.5.2 @@ -294,7 +294,7 @@ dependencies: - openssl=3.3.2 - orc=2.0.2 - packaging=24.1 -- pandas=2.2.2 +- pandas=2.2.3 - pango=1.54.0 - parso=0.8.4 - partd=1.4.2 @@ -307,17 +307,17 @@ dependencies: - pixman=0.43.2 - pkgutil-resolve-name=1.3.10 - plac=1.4.3 -- platformdirs=4.3.3 +- platformdirs=4.3.6 - pluggy=1.5.0 -- polars=1.7.1 +- polars=1.8.2 - poppler=24.08.0 - poppler-data=0.4.12 - postgresql=16.4 -- powerplantmatching=0.5.17 +- powerplantmatching=0.5.19 - pre-commit=3.8.0 - progressbar2=4.5.0 -- proj=9.4.1 -- prompt-toolkit=3.0.47 +- proj=9.5.0 +- prompt-toolkit=3.0.48 - psutil=6.0.0 - pthread-stubs=0.4 - ptyprocess=0.7.0 @@ -333,20 +333,20 @@ dependencies: - pyogrio=0.9.0 - pyparsing=3.1.4 - pyproj=3.6.1 -- pypsa=0.30.2 +- pypsa=0.30.3 - pyscipopt=5.1.1 - pyshp=2.3.1 - pyside6=6.7.2 - pysocks=1.7.1 - pytables=3.10.1 - pytest=8.3.3 -- python=3.12.5 +- python=3.12.6 - python-dateutil=2.9.0 - python-fastjsonschema=2.20.0 -- python-tzdata=2024.1 +- python-tzdata=2024.2 - python-utils=3.8.2 - python_abi=3.12 -- pytz=2024.2 +- pytz=2024.1 - pyxlsb=1.0.10 - pyyaml=6.0.2 - qhull=2020.2 @@ -359,13 +359,13 @@ dependencies: - reretry=0.11.8 - rioxarray=0.17.0 - rpds-py=0.20.0 -- s2n=1.5.2 +- s2n=1.5.3 - scikit-learn=1.5.2 - scip=9.1.0 - scipy=1.14.1 - seaborn=0.13.2 - seaborn-base=0.13.2 -- setuptools=73.0.1 +- setuptools=75.1.0 - shapely=2.0.6 - six=1.16.0 - smart_open=7.0.4 @@ -374,7 +374,7 @@ dependencies: - snakemake-interface-executor-plugins=9.2.0 - snakemake-interface-report-plugins=1.0.0 - snakemake-interface-storage-plugins=3.3.0 -- snakemake-minimal=8.20.3 +- snakemake-minimal=8.20.5 - snappy=1.2.1 - snuggs=1.4.7 - sortedcontainers=2.4.0 @@ -382,13 +382,13 @@ dependencies: - spdlog=1.14.1 - sqlite=3.46.1 - stack_data=0.6.2 -- statsmodels=0.14.2 +- statsmodels=0.14.3 - tabulate=0.9.0 - tbb=2021.13.0 - tblib=3.0.0 - threadpoolctl=3.5.0 - throttler=1.2.2 -- tiledb=2.26.0 +- tiledb=2.26.1 - tk=8.6.13 - tomli=2.0.1 - toolz=0.12.1 @@ -404,9 +404,9 @@ dependencies: - unidecode=1.3.8 - unixodbc=2.3.12 - uriparser=0.9.8 -- urllib3=2.2.2 +- urllib3=2.2.3 - validators=0.34.0 -- virtualenv=20.26.4 +- virtualenv=20.26.5 - wayland=1.23.1 - wcwidth=0.2.13 - wheel=0.44.0 @@ -423,10 +423,9 @@ dependencies: - xlrd=2.0.1 - xorg-fixesproto=5.0 - xorg-inputproto=2.3.2 -- xorg-kbproto=1.0.7 - xorg-libice=1.1.1 - xorg-libsm=1.2.4 -- xorg-libx11=1.8.9 +- xorg-libx11=1.8.10 - xorg-libxau=1.0.11 - xorg-libxdmcp=1.1.3 - xorg-libxext=1.3.4 @@ -438,7 +437,7 @@ dependencies: - xorg-recordproto=1.14.2 - xorg-renderproto=0.11.1 - xorg-xextproto=7.3.0 -- xorg-xproto=7.0.31 +- xorg-xorgproto=2024.1 - xyzservices=2024.9.0 - xz=5.2.6 - yaml=0.2.5 @@ -446,10 +445,11 @@ dependencies: - zict=3.0.0 - zipp=3.20.2 - zlib=1.3.1 -- zlib-ng=2.2.1 +- zlib-ng=2.2.2 - zstandard=0.23.0 - zstd=1.5.6 - pip: + - highspy==1.7.2 - oauthlib==3.2.2 - ply==3.11 - pyomo==6.8.0 @@ -458,5 +458,5 @@ dependencies: - snakemake-executor-plugin-slurm==0.10.2 - snakemake-executor-plugin-slurm-jobstep==0.2.1 - snakemake-storage-plugin-http==0.2.3 - - tsam==2.3.4 + - tsam==2.3.5 prefix: /usr/share/miniconda/envs/pypsa-eur

P?PuclKkk#}Gpp;ZX5v=wuCyeCOq+BS8gc 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 QJtz-(y85}Sb4q9e0Os8m$^ZZW literal 228916 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