From d0fab469f4204617dc2ba7adc4d0a513bc34e2e2 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 12 Nov 2021 10:32:01 +0100 Subject: [PATCH 001/293] prepare: add import options --- config.default.yaml | 6 ++++ scripts/prepare_sector_network.py | 53 +++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/config.default.yaml b/config.default.yaml index 59a277531..87cd1e896 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -256,6 +256,12 @@ sector: biomass_transport: false # biomass transport between nodes conventional_generation: # generator : carrier OCGT: gas + import: + capacity_boost: 3 + options: + - lng + - pipeline + - hvdc industry: diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index fe95129b0..1e1bfb57a 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2361,6 +2361,52 @@ def remove_h2_network(n): n.stores.drop("EU H2 Store", inplace=True) +def add_import_options(n, capacity_boost=3., options=["hvdc", "pipeline", "lng"]): + + fn = snakemake.input.gas_input_nodes + import_nodes = pd.read_csv(fn, index_col=0) + import_nodes["hvdc"] = np.inf + + translate = { + "pipeline-h2": "pipeline", + "shipping-lh2": "lng", + "hvdc": "hvdc", + } + + bus_suffix = { + "pipeline": "H2", + "lng": "H2", + "hvdc": "", + } + + import_costs = pd.read_csv(snakemake.input.import_costs) + import_costs.rename(columns={"value": "marginal_cost"}, inplace=True) + import_costs["esc"] = import_costs.esc.replace(translate) + + for tech in options: + + import_costs_tech = import_costs.query("esc == @tech").set_index('importer') + + import_nodes_tech = import_nodes.loc[~import_nodes[tech].isna(), [tech]] + import_nodes_tech = import_nodes_tech.rename(columns={tech: "p_nom"}) * capacity_boost + + marginal_costs = import_nodes_tech.index.str[:2].map(import_costs_tech.marginal_cost) + import_nodes_tech["marginal_cost"] = marginal_costs + + import_nodes_tech.dropna(inplace=True) + + suffix = bus_suffix[tech] + + n.madd( + "Generator", + import_nodes_tech.index + f" {suffix} import {tech}", + bus=import_nodes_tech.index + suffix, + carrier=f"import {tech}", + marginal_cost=import_nodes_tech.marginal_cost, + p_nom=import_nodes_tech.p_nom, + ) + + def maybe_adjust_costs_and_potentials(n, opts): for o in opts: @@ -2499,6 +2545,13 @@ def limit_individual_line_extension(n, maxext): if options["co2_network"]: add_co2_network(n, costs) + if "import" in opts: + add_import_options( + n, + capacity_boost=options["imports"]["capacity_boost"], + options=options["imports"]["options"] + ) + for o in opts: m = re.match(r'^\d+h$', o, re.IGNORECASE) if m is not None: From 73fcbee67a68f22b0efa65537423e39a6bb81549 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 12 Nov 2021 10:32:40 +0100 Subject: [PATCH 002/293] solve: add energy import limit constraint --- config.default.yaml | 1 + scripts/solve_network.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/config.default.yaml b/config.default.yaml index 87cd1e896..1045f1e48 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -262,6 +262,7 @@ sector: - lng - pipeline - hvdc + limit: 1000 # TWh industry: diff --git a/scripts/solve_network.py b/scripts/solve_network.py index c784c9139..cc14142a4 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -225,10 +225,29 @@ def add_co2_sequestration_limit(n, sns): 'mu', axes=pd.Index([name]), spec=name) +def add_energy_import_limit(n, sns): + + import_gens = n.generators.loc[n.generators.carrier.str.contains("import")].index + import_opts = n.config["sector"]["import"] + + if import_gens.empty or "limit" not in import_opts.keys(): return + + weightings = n.snapshot_weightings.loc[sns] + p = get_var(n, "Generator", "p")[import_gens] + lhs = linexpr((weightings.generators, p)).sum().sum() + + rhs = import_opts["limit"] * 1e6 + + name = 'energy_import_limit' + define_constraints(n, lhs, '<=', rhs, 'GlobalConstraint', + 'mu', axes=pd.Index([name]), spec=name) + + def extra_functionality(n, snapshots): add_battery_constraints(n) add_pipe_retrofit_constraint(n) add_co2_sequestration_limit(n, snapshots) + add_energy_import_limit(n, snapshots) def solve_network(n, config, opts='', **kwargs): From e1595ec121f56990cd758a035557559391211ceb Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 12 Nov 2021 10:34:58 +0100 Subject: [PATCH 003/293] add import costs csv --- data/import-costs.csv | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 data/import-costs.csv diff --git a/data/import-costs.csv b/data/import-costs.csv new file mode 100644 index 000000000..a7a404d5c --- /dev/null +++ b/data/import-costs.csv @@ -0,0 +1,24 @@ +,importer,esc,exporter,value +0,BG,hvdc,SA,88.4941091881407 +1,BG,pipeline-h2,SA,68.10522890805974 +2,DE,shipping-lh2,AR,104.43864154323799 +3,ES,hvdc,MA,63.6945774542665 +4,ES,pipeline-h2,MA,53.32011953726621 +5,ES,shipping-lh2,EG,105.25419756386304 +6,GR,hvdc,SA,81.75210039808138 +7,GR,pipeline-h2,SA,64.18702271857515 +8,GR,shipping-lh2,EG,98.51051107249096 +9,IT,hvdc,EG,76.43589366250288 +10,IT,pipeline-h2,EG,61.07593329204597 +11,IT,shipping-lh2,EG,100.99555252518421 +2,GB,shipping-lh2,AR,104.43864154323799 +2,NL,shipping-lh2,AR,104.43864154323799 +2,SE,shipping-lh2,AR,104.43864154323799 +2,LT,shipping-lh2,AR,104.43864154323799 +2,EE,shipping-lh2,AR,104.43864154323799 +2,FI,shipping-lh2,AR,104.43864154323799 +2,LV,shipping-lh2,AR,104.43864154323799 +2,PL,shipping-lh2,AR,104.43864154323799 +2,BE,shipping-lh2,AR,104.43864154323799 +11,FR,shipping-lh2,EG,100.99555252518421 +5,PT,shipping-lh2,EG,105.25419756386304 From e4c9c499e4a132f96e7558f2134cddd87fd926ba Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 12 Nov 2021 10:51:38 +0100 Subject: [PATCH 004/293] Snakefile: add import costs file and fix syntax --- Snakefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Snakefile b/Snakefile index 734098675..9b1a7dcef 100644 --- a/Snakefile +++ b/Snakefile @@ -104,7 +104,7 @@ if config["sector"]["gas_network"]: lng="data/gas_network/scigrid-gas/data/IGGIELGN_LNGs.geojson", entry="data/gas_network/scigrid-gas/data/IGGIELGN_BorderPoints.geojson", production="data/gas_network/scigrid-gas/data/IGGIELGN_Productions.geojson", - planned_lng="data/gas_network/planned_LNGs.csv" + planned_lng="data/gas_network/planned_LNGs.csv", regions_onshore=pypsaeur("resources/regions_onshore_elec_s{simpl}_{clusters}.geojson"), output: gas_input_nodes="resources/gas_input_locations_s{simpl}_{clusters}.geojson" @@ -423,6 +423,7 @@ rule prepare_sector_network: solar_thermal_total="resources/solar_thermal_total_elec_s{simpl}_{clusters}.nc", solar_thermal_urban="resources/solar_thermal_urban_elec_s{simpl}_{clusters}.nc", solar_thermal_rural="resources/solar_thermal_rural_elec_s{simpl}_{clusters}.nc", + import_costs="data/import-costs.csv", **build_retro_cost_output, **build_biomass_transport_costs_output, **gas_infrastructure From ce545b2294314df19a68390e1d10ef4a61597711 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 12 Nov 2021 12:36:38 +0100 Subject: [PATCH 005/293] correct misc coding errors --- Snakefile | 2 +- config.default.yaml | 3 +++ scripts/prepare_sector_network.py | 26 ++++++++++++++------------ 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/Snakefile b/Snakefile index 9b1a7dcef..04e97ed9a 100644 --- a/Snakefile +++ b/Snakefile @@ -107,7 +107,7 @@ if config["sector"]["gas_network"]: planned_lng="data/gas_network/planned_LNGs.csv", regions_onshore=pypsaeur("resources/regions_onshore_elec_s{simpl}_{clusters}.geojson"), output: - gas_input_nodes="resources/gas_input_locations_s{simpl}_{clusters}.geojson" + gas_input_nodes="resources/gas_input_locations_s{simpl}_{clusters}.geojson", gas_input_nodes_simplified="resources/gas_input_locations_s{simpl}_{clusters}_simplified.csv" resources: mem_mb=2000, script: "scripts/build_gas_input_locations.py" diff --git a/config.default.yaml b/config.default.yaml index 1045f1e48..a2908cca3 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -597,3 +597,6 @@ plotting: gas-to-power/heat: '#ee8340' waste: '#e3d37d' other: '#000000' + import pipeline: '#fff7e3' + import lng: '#ded9ca' + import hvdc: '#b3ac9a' diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 1e1bfb57a..91ad15c50 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1123,7 +1123,7 @@ def add_storage_and_grids(n, costs): # remove fossil generators where there is neither # production, LNG terminal, nor entry-point beyond system scope - fn = snakemake.input.gas_input_nodes + fn = snakemake.input.gas_input_nodes_simplified gas_input_nodes = pd.read_csv(fn, index_col=0) unique = gas_input_nodes.index.unique() @@ -2363,9 +2363,9 @@ def remove_h2_network(n): def add_import_options(n, capacity_boost=3., options=["hvdc", "pipeline", "lng"]): - fn = snakemake.input.gas_input_nodes + fn = snakemake.input.gas_input_nodes_simplified import_nodes = pd.read_csv(fn, index_col=0) - import_nodes["hvdc"] = np.inf + import_nodes["hvdc"] = 1e6 translate = { "pipeline-h2": "pipeline", @@ -2374,9 +2374,9 @@ def add_import_options(n, capacity_boost=3., options=["hvdc", "pipeline", "lng"] } bus_suffix = { - "pipeline": "H2", - "lng": "H2", - "hvdc": "", + "pipeline": " H2", + "lng": " H2", + "hvdc": " DC", } import_costs = pd.read_csv(snakemake.input.import_costs) @@ -2396,14 +2396,16 @@ def add_import_options(n, capacity_boost=3., options=["hvdc", "pipeline", "lng"] import_nodes_tech.dropna(inplace=True) suffix = bus_suffix[tech] + location = import_nodes_tech.index + buses = location if tech == 'hvdc' else location + suffix n.madd( "Generator", - import_nodes_tech.index + f" {suffix} import {tech}", - bus=import_nodes_tech.index + suffix, + import_nodes_tech.index + f"{suffix} import {tech}", + bus=buses, carrier=f"import {tech}", - marginal_cost=import_nodes_tech.marginal_cost, - p_nom=import_nodes_tech.p_nom, + marginal_cost=import_nodes_tech.marginal_cost.values, + p_nom=import_nodes_tech.p_nom.values, ) @@ -2548,8 +2550,8 @@ def limit_individual_line_extension(n, maxext): if "import" in opts: add_import_options( n, - capacity_boost=options["imports"]["capacity_boost"], - options=options["imports"]["options"] + capacity_boost=options["import"]["capacity_boost"], + options=options["import"]["options"] ) for o in opts: From 6ff41ecf9da23fe65efeb10638df8cac4597a1e0 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 15 Nov 2021 10:49:35 +0100 Subject: [PATCH 006/293] add lch4 and ftfuel import options via shipping, update import costs --- config.default.yaml | 18 +++++--- data/import-costs.csv | 76 ++++++++++++++++++++++--------- scripts/prepare_sector_network.py | 37 +++++++++++---- 3 files changed, 94 insertions(+), 37 deletions(-) diff --git a/config.default.yaml b/config.default.yaml index a2908cca3..58f52a6a5 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -257,12 +257,14 @@ sector: conventional_generation: # generator : carrier OCGT: gas import: - capacity_boost: 3 + capacity_boost: 2 options: - - lng - - pipeline + - pipeline-h2 + - shipping-lh2 + - shipping-lch4 + - shipping-ftfuel - hvdc - limit: 1000 # TWh + # limit: 100 # TWh industry: @@ -597,6 +599,8 @@ plotting: gas-to-power/heat: '#ee8340' waste: '#e3d37d' other: '#000000' - import pipeline: '#fff7e3' - import lng: '#ded9ca' - import hvdc: '#b3ac9a' + import pipeline-h2: '#fff6e0' + import shipping-lh2: '#ebe1ca' + import shipping-lch4: '#d6cbb2' + import shipping-ftfuel: '#bdb093' + import hvdc: '#91856a' diff --git a/data/import-costs.csv b/data/import-costs.csv index a7a404d5c..0423d8184 100644 --- a/data/import-costs.csv +++ b/data/import-costs.csv @@ -1,24 +1,58 @@ ,importer,esc,exporter,value 0,BG,hvdc,SA,88.4941091881407 1,BG,pipeline-h2,SA,68.10522890805974 -2,DE,shipping-lh2,AR,104.43864154323799 -3,ES,hvdc,MA,63.6945774542665 -4,ES,pipeline-h2,MA,53.32011953726621 -5,ES,shipping-lh2,EG,105.25419756386304 -6,GR,hvdc,SA,81.75210039808138 -7,GR,pipeline-h2,SA,64.18702271857515 -8,GR,shipping-lh2,EG,98.51051107249096 -9,IT,hvdc,EG,76.43589366250288 -10,IT,pipeline-h2,EG,61.07593329204597 -11,IT,shipping-lh2,EG,100.99555252518421 -2,GB,shipping-lh2,AR,104.43864154323799 -2,NL,shipping-lh2,AR,104.43864154323799 -2,SE,shipping-lh2,AR,104.43864154323799 -2,LT,shipping-lh2,AR,104.43864154323799 -2,EE,shipping-lh2,AR,104.43864154323799 -2,FI,shipping-lh2,AR,104.43864154323799 -2,LV,shipping-lh2,AR,104.43864154323799 -2,PL,shipping-lh2,AR,104.43864154323799 -2,BE,shipping-lh2,AR,104.43864154323799 -11,FR,shipping-lh2,EG,100.99555252518421 -5,PT,shipping-lh2,EG,105.25419756386304 +2,DE,shipping-ftfuel,AR,182.27436184241643 +3,DE,shipping-lch4,AR,91.61639726151144 +4,DE,shipping-lh2,AR,104.43864154323799 +5,ES,hvdc,MA,63.6945774542665 +6,ES,pipeline-h2,MA,53.32011953726621 +7,ES,shipping-ftfuel,AR,182.05468623122798 +8,ES,shipping-lch4,AR,90.90298225646869 +9,ES,shipping-lh2,EG,105.25419756386304 +10,GR,hvdc,SA,81.75210039808138 +11,GR,pipeline-h2,SA,64.18702271857515 +12,GR,shipping-ftfuel,AR,182.33205836260657 +13,GR,shipping-lch4,AR,91.61589851412424 +14,GR,shipping-lh2,EG,98.51051107249096 +15,IT,hvdc,EG,76.43589366250288 +16,IT,pipeline-h2,EG,61.07593329204597 +17,IT,shipping-ftfuel,AR,182.26883556263837 +18,IT,shipping-lch4,AR,91.61639726151144 +19,IT,shipping-lh2,EG,100.99555252518421 +4,GB,shipping-lh2,AR,104.43864154323799 +4,NL,shipping-lh2,AR,104.43864154323799 +4,SE,shipping-lh2,AR,104.43864154323799 +4,LT,shipping-lh2,AR,104.43864154323799 +4,EE,shipping-lh2,AR,104.43864154323799 +4,FI,shipping-lh2,AR,104.43864154323799 +4,LV,shipping-lh2,AR,104.43864154323799 +4,PL,shipping-lh2,AR,104.43864154323799 +4,BE,shipping-lh2,AR,104.43864154323799 +2,GB,shipping-ftfuel,AR,182.27436184241643 +2,SE,shipping-ftfuel,AR,182.27436184241643 +2,IT,shipping-ftfuel,AR,182.27436184241643 +2,ES,shipping-ftfuel,AR,182.27436184241643 +2,NL,shipping-ftfuel,AR,182.27436184241643 +2,GR,shipping-ftfuel,AR,182.27436184241643 +2,LV,shipping-ftfuel,AR,182.27436184241643 +2,FI,shipping-ftfuel,AR,182.27436184241643 +2,EE,shipping-ftfuel,AR,182.27436184241643 +2,BG,shipping-ftfuel,AR,182.27436184241643 +2,BE,shipping-ftfuel,AR,182.27436184241643 +2,LT,shipping-ftfuel,AR,182.27436184241643 +2,PL,shipping-ftfuel,AR,182.27436184241643 +3,GB,shipping-lch4,AR,91.61639726151144 +3,SE,shipping-lch4,AR,91.61639726151144 +3,IT,shipping-lch4,AR,91.61639726151144 +3,ES,shipping-lch4,AR,91.61639726151144 +3,NL,shipping-lch4,AR,91.61639726151144 +3,GR,shipping-lch4,AR,91.61639726151144 +3,LV,shipping-lch4,AR,91.61639726151144 +3,FI,shipping-lch4,AR,91.61639726151144 +3,EE,shipping-lch4,AR,91.61639726151144 +3,BG,shipping-lch4,AR,91.61639726151144 +3,BE,shipping-lch4,AR,91.61639726151144 +3,LT,shipping-lch4,AR,91.61639726151144 +3,PL,shipping-lch4,AR,91.61639726151144 +19,FR,shipping-lh2,EG,100.99555252518421 +9,PT,shipping-lh2,EG,105.25419756386304 diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 1e30d7abb..c8512d745 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2350,7 +2350,11 @@ def remove_h2_network(n): n.stores.drop("EU H2 Store", inplace=True) -def add_import_options(n, capacity_boost=3., options=["hvdc", "pipeline", "lng"]): +def add_import_options( + n, + capacity_boost=3., + options=["hvdc", "pipeline-h2", "shipping-lh2", "shipping-lch4", "shipping-ftfuel"] +): fn = snakemake.input.gas_input_nodes_simplified import_nodes = pd.read_csv(fn, index_col=0) @@ -2358,28 +2362,32 @@ def add_import_options(n, capacity_boost=3., options=["hvdc", "pipeline", "lng"] translate = { "pipeline-h2": "pipeline", - "shipping-lh2": "lng", "hvdc": "hvdc", + "shipping-lh2": "lng", + "shipping-lch4": "lng", } bus_suffix = { - "pipeline": " H2", - "lng": " H2", - "hvdc": " DC", + "pipeline-h2": " H2", + "hvdc": "", + "shipping-lh2": " H2", + "shipping-lch4": " gas", } - import_costs = pd.read_csv(snakemake.input.import_costs) + import_costs = pd.read_csv(snakemake.input.import_costs, index_col=0).reset_index(drop=True) import_costs.rename(columns={"value": "marginal_cost"}, inplace=True) - import_costs["esc"] = import_costs.esc.replace(translate) + + for k, v in translate.items(): + import_nodes[k] = import_nodes[v] for tech in options: - import_costs_tech = import_costs.query("esc == @tech").set_index('importer') + import_costs_tech = import_costs.query("esc == @tech").groupby('importer').marginal_cost.min() import_nodes_tech = import_nodes.loc[~import_nodes[tech].isna(), [tech]] import_nodes_tech = import_nodes_tech.rename(columns={tech: "p_nom"}) * capacity_boost - marginal_costs = import_nodes_tech.index.str[:2].map(import_costs_tech.marginal_cost) + marginal_costs = import_nodes_tech.index.str[:2].map(import_costs_tech) import_nodes_tech["marginal_cost"] = marginal_costs import_nodes_tech.dropna(inplace=True) @@ -2397,6 +2405,17 @@ def add_import_options(n, capacity_boost=3., options=["hvdc", "pipeline", "lng"] p_nom=import_nodes_tech.p_nom.values, ) + marginal_costs = import_costs.query("esc == 'shipping-ftfuel'").marginal_cost.min() + + n.add( + "Generator", + "EU oil import shipping-ftfuel", + bus="EU oil", + carrier="import shipping-ftfuel", + marginal_cost=marginal_costs, + p_nom=1e6 + ) + def maybe_adjust_costs_and_potentials(n, opts): From 9f205aff8b2abddecb85ecaae27461676baf6c06 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 15 Nov 2021 11:02:32 +0100 Subject: [PATCH 007/293] fix options handling between regionalised and copperplated imports --- scripts/prepare_sector_network.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index c8512d745..7a5773ca8 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2380,7 +2380,9 @@ def add_import_options( for k, v in translate.items(): import_nodes[k] = import_nodes[v] - for tech in options: + regionalised_options = ["hvdc", "pipeline-h2", "shipping-lh2", "shipping-lch4"] + + for tech in set(options).intersection(regionalised_options): import_costs_tech = import_costs.query("esc == @tech").groupby('importer').marginal_cost.min() @@ -2405,16 +2407,19 @@ def add_import_options( p_nom=import_nodes_tech.p_nom.values, ) - marginal_costs = import_costs.query("esc == 'shipping-ftfuel'").marginal_cost.min() + # need special handling for copperplated Fischer-Tropsch imports + if "shipping-ftfuel" in options: - n.add( - "Generator", - "EU oil import shipping-ftfuel", - bus="EU oil", - carrier="import shipping-ftfuel", - marginal_cost=marginal_costs, - p_nom=1e6 - ) + marginal_costs = import_costs.query("esc == 'shipping-ftfuel'").marginal_cost.min() + + n.add( + "Generator", + "EU oil import shipping-ftfuel", + bus="EU oil", + carrier="import shipping-ftfuel", + marginal_cost=marginal_costs, + p_nom=1e6 + ) def maybe_adjust_costs_and_potentials(n, opts): From 2c2320b3ed8479e55b0c81b0b9be320e18bb6013 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 15 Nov 2021 11:24:52 +0100 Subject: [PATCH 008/293] plot_summary: fix issue with lh2 and h2 in carrier name --- scripts/plot_summary.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/plot_summary.py b/scripts/plot_summary.py index 8b073b178..c02268bb2 100644 --- a/scripts/plot_summary.py +++ b/scripts/plot_summary.py @@ -254,7 +254,8 @@ def plot_balances(): df = df / 1e6 #remove trailing link ports - df.index = [i[:-1] if ((i != "co2") and (i[-1:] in ["0","1","2","3"])) else i for i in df.index] + forbidden = ["co2", "import shipping-lh2", "import pipeline-h2"] + df.index = [i[:-1] if ((i not in forbidden) and (i[-1:] in ["0","1","2","3"])) else i for i in df.index] df = df.groupby(df.index.map(rename_techs)).sum() From dd99964a2da0e685d4ef294f42faf2751d5105c3 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 17 Nov 2021 09:50:34 +0100 Subject: [PATCH 009/293] update import costs from RU, KZ --- data/import-costs.csv | 113 +++++++++++++++++++++--------------------- 1 file changed, 56 insertions(+), 57 deletions(-) diff --git a/data/import-costs.csv b/data/import-costs.csv index 0423d8184..ed59e1395 100644 --- a/data/import-costs.csv +++ b/data/import-costs.csv @@ -1,58 +1,57 @@ ,importer,esc,exporter,value -0,BG,hvdc,SA,88.4941091881407 -1,BG,pipeline-h2,SA,68.10522890805974 -2,DE,shipping-ftfuel,AR,182.27436184241643 -3,DE,shipping-lch4,AR,91.61639726151144 -4,DE,shipping-lh2,AR,104.43864154323799 -5,ES,hvdc,MA,63.6945774542665 -6,ES,pipeline-h2,MA,53.32011953726621 -7,ES,shipping-ftfuel,AR,182.05468623122798 -8,ES,shipping-lch4,AR,90.90298225646869 -9,ES,shipping-lh2,EG,105.25419756386304 -10,GR,hvdc,SA,81.75210039808138 -11,GR,pipeline-h2,SA,64.18702271857515 -12,GR,shipping-ftfuel,AR,182.33205836260657 -13,GR,shipping-lch4,AR,91.61589851412424 -14,GR,shipping-lh2,EG,98.51051107249096 -15,IT,hvdc,EG,76.43589366250288 -16,IT,pipeline-h2,EG,61.07593329204597 -17,IT,shipping-ftfuel,AR,182.26883556263837 -18,IT,shipping-lch4,AR,91.61639726151144 -19,IT,shipping-lh2,EG,100.99555252518421 -4,GB,shipping-lh2,AR,104.43864154323799 -4,NL,shipping-lh2,AR,104.43864154323799 -4,SE,shipping-lh2,AR,104.43864154323799 -4,LT,shipping-lh2,AR,104.43864154323799 -4,EE,shipping-lh2,AR,104.43864154323799 -4,FI,shipping-lh2,AR,104.43864154323799 -4,LV,shipping-lh2,AR,104.43864154323799 -4,PL,shipping-lh2,AR,104.43864154323799 -4,BE,shipping-lh2,AR,104.43864154323799 -2,GB,shipping-ftfuel,AR,182.27436184241643 -2,SE,shipping-ftfuel,AR,182.27436184241643 -2,IT,shipping-ftfuel,AR,182.27436184241643 -2,ES,shipping-ftfuel,AR,182.27436184241643 -2,NL,shipping-ftfuel,AR,182.27436184241643 -2,GR,shipping-ftfuel,AR,182.27436184241643 -2,LV,shipping-ftfuel,AR,182.27436184241643 -2,FI,shipping-ftfuel,AR,182.27436184241643 -2,EE,shipping-ftfuel,AR,182.27436184241643 -2,BG,shipping-ftfuel,AR,182.27436184241643 -2,BE,shipping-ftfuel,AR,182.27436184241643 -2,LT,shipping-ftfuel,AR,182.27436184241643 -2,PL,shipping-ftfuel,AR,182.27436184241643 -3,GB,shipping-lch4,AR,91.61639726151144 -3,SE,shipping-lch4,AR,91.61639726151144 -3,IT,shipping-lch4,AR,91.61639726151144 -3,ES,shipping-lch4,AR,91.61639726151144 -3,NL,shipping-lch4,AR,91.61639726151144 -3,GR,shipping-lch4,AR,91.61639726151144 -3,LV,shipping-lch4,AR,91.61639726151144 -3,FI,shipping-lch4,AR,91.61639726151144 -3,EE,shipping-lch4,AR,91.61639726151144 -3,BG,shipping-lch4,AR,91.61639726151144 -3,BE,shipping-lch4,AR,91.61639726151144 -3,LT,shipping-lch4,AR,91.61639726151144 -3,PL,shipping-lch4,AR,91.61639726151144 -19,FR,shipping-lh2,EG,100.99555252518421 -9,PT,shipping-lh2,EG,105.25419756386304 +0,BE,shipping-lch4,AR,91.22697594467209 +1,BE,shipping-lh2,AR,104.4385881765028 +2,BG,hvdc,SA,88.4941091881407 +3,BG,pipeline-h2,SA,68.10522890805974 +4,DE,shipping-ftfuel,AR,182.27436184241643 +5,DE,shipping-lch4,AR,91.61639726151144 +6,DE,shipping-lh2,AR,104.43864154323799 +7,EE,hvdc,KZ,70.27225194958767 +8,EE,pipeline-h2,KZ,56.074027599642896 +9,EE,shipping-lch4,AR,91.65283433307607 +10,EE,shipping-lh2,AR,107.44614628704308 +11,ES,hvdc,MA,63.6945774542665 +12,ES,pipeline-h2,MA,53.32011953726621 +13,ES,shipping-ftfuel,AR,182.05468623122798 +14,ES,shipping-lch4,AR,90.90298225646869 +15,ES,shipping-lh2,EG,105.25419756386304 +16,FI,hvdc,KZ,69.33213220566181 +17,FI,pipeline-h2,KZ,55.52756242733924 +18,FI,shipping-lch4,AR,92.08939717866873 +19,FI,shipping-lh2,AR,107.44614628704308 +20,GB,shipping-lch4,AR,91.27627990339468 +21,GB,shipping-lh2,AR,104.43862902624011 +22,GR,hvdc,SA,81.75210039808138 +23,GR,pipeline-h2,SA,64.18702271857515 +24,GR,shipping-ftfuel,AR,182.33205836260657 +25,GR,shipping-lch4,AR,91.61589851412424 +26,GR,shipping-lh2,EG,98.51051107249096 +27,HU,hvdc,KZ,81.96970329821066 +28,HU,pipeline-h2,KZ,62.864638804797174 +29,IE,shipping-lch4,AR,91.22697594467209 +30,IE,shipping-lh2,AR,104.4385881765028 +31,IT,hvdc,EG,76.43589366250288 +32,IT,pipeline-h2,EG,61.07593329204597 +33,IT,shipping-ftfuel,AR,182.26883556263837 +34,IT,shipping-lch4,AR,91.61639726151144 +35,IT,shipping-lh2,EG,100.99555252518421 +36,LT,hvdc,KZ,74.7429419757618 +37,LT,pipeline-h2,KZ,58.66969888437293 +38,LT,shipping-lch4,AR,91.691576093544 +39,LT,shipping-lh2,AR,107.44614628704308 +40,LV,hvdc,KZ,71.73635012099069 +41,LV,pipeline-h2,KZ,56.92516643705386 +42,LV,shipping-lch4,AR,91.65206514343927 +43,LV,shipping-lh2,AR,107.44614628704308 +44,NL,shipping-lch4,AR,91.22838066551323 +45,NL,shipping-lh2,AR,104.4385881765028 +46,PL,hvdc,KZ,76.91804408133609 +47,PL,pipeline-h2,KZ,59.929380763381516 +48,PL,shipping-lch4,AR,91.64432367396978 +49,PL,shipping-lh2,AR,107.44614628704308 +50,RO,hvdc,KZ,80.01708323724434 +51,RO,pipeline-h2,KZ,61.73274659339972 +52,SE,shipping-lch4,AR,91.64194937436476 +53,SE,shipping-lh2,AR,107.44614628704308 +54,SK,hvdc,KZ,81.76302849367399 +55,SK,pipeline-h2,KZ,62.745447384813325 From f93111f81cfd69400254705f9088d6867df2c476 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 15 Dec 2021 11:36:21 +0100 Subject: [PATCH 010/293] update costs and change hvdc naming --- data/import-costs.csv | 124 ++++++++++++++++-------------- scripts/prepare_sector_network.py | 12 +-- 2 files changed, 74 insertions(+), 62 deletions(-) diff --git a/data/import-costs.csv b/data/import-costs.csv index ed59e1395..48d01fbf1 100644 --- a/data/import-costs.csv +++ b/data/import-costs.csv @@ -1,57 +1,69 @@ ,importer,esc,exporter,value -0,BE,shipping-lch4,AR,91.22697594467209 -1,BE,shipping-lh2,AR,104.4385881765028 -2,BG,hvdc,SA,88.4941091881407 -3,BG,pipeline-h2,SA,68.10522890805974 -4,DE,shipping-ftfuel,AR,182.27436184241643 -5,DE,shipping-lch4,AR,91.61639726151144 -6,DE,shipping-lh2,AR,104.43864154323799 -7,EE,hvdc,KZ,70.27225194958767 -8,EE,pipeline-h2,KZ,56.074027599642896 -9,EE,shipping-lch4,AR,91.65283433307607 -10,EE,shipping-lh2,AR,107.44614628704308 -11,ES,hvdc,MA,63.6945774542665 -12,ES,pipeline-h2,MA,53.32011953726621 -13,ES,shipping-ftfuel,AR,182.05468623122798 -14,ES,shipping-lch4,AR,90.90298225646869 -15,ES,shipping-lh2,EG,105.25419756386304 -16,FI,hvdc,KZ,69.33213220566181 -17,FI,pipeline-h2,KZ,55.52756242733924 -18,FI,shipping-lch4,AR,92.08939717866873 -19,FI,shipping-lh2,AR,107.44614628704308 -20,GB,shipping-lch4,AR,91.27627990339468 -21,GB,shipping-lh2,AR,104.43862902624011 -22,GR,hvdc,SA,81.75210039808138 -23,GR,pipeline-h2,SA,64.18702271857515 -24,GR,shipping-ftfuel,AR,182.33205836260657 -25,GR,shipping-lch4,AR,91.61589851412424 -26,GR,shipping-lh2,EG,98.51051107249096 -27,HU,hvdc,KZ,81.96970329821066 -28,HU,pipeline-h2,KZ,62.864638804797174 -29,IE,shipping-lch4,AR,91.22697594467209 -30,IE,shipping-lh2,AR,104.4385881765028 -31,IT,hvdc,EG,76.43589366250288 -32,IT,pipeline-h2,EG,61.07593329204597 -33,IT,shipping-ftfuel,AR,182.26883556263837 -34,IT,shipping-lch4,AR,91.61639726151144 -35,IT,shipping-lh2,EG,100.99555252518421 -36,LT,hvdc,KZ,74.7429419757618 -37,LT,pipeline-h2,KZ,58.66969888437293 -38,LT,shipping-lch4,AR,91.691576093544 -39,LT,shipping-lh2,AR,107.44614628704308 -40,LV,hvdc,KZ,71.73635012099069 -41,LV,pipeline-h2,KZ,56.92516643705386 -42,LV,shipping-lch4,AR,91.65206514343927 -43,LV,shipping-lh2,AR,107.44614628704308 -44,NL,shipping-lch4,AR,91.22838066551323 -45,NL,shipping-lh2,AR,104.4385881765028 -46,PL,hvdc,KZ,76.91804408133609 -47,PL,pipeline-h2,KZ,59.929380763381516 -48,PL,shipping-lch4,AR,91.64432367396978 -49,PL,shipping-lh2,AR,107.44614628704308 -50,RO,hvdc,KZ,80.01708323724434 -51,RO,pipeline-h2,KZ,61.73274659339972 -52,SE,shipping-lch4,AR,91.64194937436476 -53,SE,shipping-lh2,AR,107.44614628704308 -54,SK,hvdc,KZ,81.76302849367399 -55,SK,pipeline-h2,KZ,62.745447384813325 +0,BE,shipping-ftfuel,AR,182.1615272572155 +1,BE,shipping-lch4,AR,91.22697630541164 +2,BE,shipping-lh2,AR,104.43859036804487 +3,BG,hvdc-to-elec,SA,51.65706274015358 +4,BG,pipeline-h2,SA,68.10566247243335 +5,DE,hvdc-to-elec,AR,55.203771951051074 +6,DE,pipeline-h2,AR,69.88789921638937 +7,DE,shipping-ftfuel,AR,182.24375712329777 +8,DE,shipping-lch4,AR,91.61589919772385 +9,DE,shipping-lh2,AR,104.43858851873729 +10,EE,hvdc-to-elec,KZ,39.95540140644545 +11,EE,pipeline-h2,KZ,56.07506227072536 +12,EE,shipping-ftfuel,AR,182.52210726941368 +13,EE,shipping-lch4,AR,91.65234145140607 +14,EE,shipping-lh2,AR,107.44614391340409 +15,ES,hvdc-to-elec,MA,33.8156906774716 +16,ES,pipeline-h2,MA,53.32101609033417 +17,ES,shipping-ftfuel,AR,182.01243859914985 +18,ES,shipping-lch4,AR,90.90299692576285 +19,ES,shipping-lh2,AR,102.46061173774919 +20,FI,hvdc-to-elec,KZ,39.321582156206546 +21,FI,pipeline-h2,KZ,55.52681520589047 +22,FI,shipping-ftfuel,AR,182.49365807233582 +23,FI,shipping-lch4,AR,92.08969911948437 +24,FI,shipping-lh2,AR,107.44615010614157 +25,GB,shipping-ftfuel,AR,182.2339850340641 +26,GB,shipping-lch4,AR,91.27673687833187 +27,GB,shipping-lh2,AR,104.43859036804487 +28,GR,hvdc-to-elec,SA,47.127927723900086 +29,GR,pipeline-h2,SA,64.18795044273045 +30,GR,shipping-ftfuel,AR,182.26049054219877 +31,GR,shipping-lch4,AR,91.61589919772385 +32,GR,shipping-lh2,AR,98.51052118547967 +33,HU,hvdc-to-elec,KZ,46.12111396710866 +34,HU,pipeline-h2,KZ,62.86492783503022 +35,IE,shipping-ftfuel,AR,182.1158742352419 +36,IE,shipping-lch4,AR,91.22698217084326 +37,IE,shipping-lh2,AR,104.43859036804487 +38,IT,hvdc-to-elec,EG,43.748868497023665 +39,IT,pipeline-h2,EG,61.07730835733746 +40,IT,shipping-ftfuel,AR,190.04758692891002 +41,IT,shipping-lch4,AR,91.61730792754746 +42,IT,shipping-lh2,AR,100.99546124128042 +43,LT,hvdc-to-elec,KZ,42.96332128371362 +44,LT,pipeline-h2,KZ,58.66924277487358 +45,LT,shipping-ftfuel,AR,182.47542971789662 +46,LT,shipping-lch4,AR,91.69162132756114 +47,LT,shipping-lh2,AR,107.44614391340409 +48,LV,hvdc-to-elec,KZ,40.940713744949505 +49,LV,pipeline-h2,KZ,56.922952511183325 +50,LV,shipping-ftfuel,AR,197.86174537188373 +51,LV,shipping-lch4,AR,91.65313498220655 +52,LV,shipping-lh2,AR,107.44614391340409 +53,NL,shipping-ftfuel,AR,182.17524752061587 +54,NL,shipping-lch4,AR,91.22860996192286 +55,NL,shipping-lh2,AR,104.43859036804487 +56,PL,hvdc-to-elec,KZ,44.419671366131446 +57,PL,pipeline-h2,KZ,59.928424139835634 +58,PL,shipping-ftfuel,AR,182.51935857298088 +59,PL,shipping-lch4,AR,91.64465517847141 +60,PL,shipping-lh2,AR,107.44614391340409 +61,RO,hvdc-to-elec,KZ,43.47453607157155 +62,RO,pipeline-h2,KZ,61.732667054236096 +63,SE,shipping-ftfuel,AR,182.51228499588916 +64,SE,shipping-lch4,AR,91.64249825392628 +65,SE,shipping-lh2,AR,107.44614391340409 +66,SK,hvdc-to-elec,KZ,46.318087517254256 +67,SK,pipeline-h2,KZ,62.74405112407931 diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 7a5773ca8..1d75be36f 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2353,23 +2353,23 @@ def remove_h2_network(n): def add_import_options( n, capacity_boost=3., - options=["hvdc", "pipeline-h2", "shipping-lh2", "shipping-lch4", "shipping-ftfuel"] + options=["hvdc-to-elec", "pipeline-h2", "shipping-lh2", "shipping-lch4", "shipping-ftfuel"] ): fn = snakemake.input.gas_input_nodes_simplified import_nodes = pd.read_csv(fn, index_col=0) - import_nodes["hvdc"] = 1e6 + import_nodes["hvdc-to-elec"] = 1e6 translate = { "pipeline-h2": "pipeline", - "hvdc": "hvdc", + "hvdc-to-elec": "hvdc-to-elec", "shipping-lh2": "lng", "shipping-lch4": "lng", } bus_suffix = { "pipeline-h2": " H2", - "hvdc": "", + "hvdc-to-elec": "", "shipping-lh2": " H2", "shipping-lch4": " gas", } @@ -2380,7 +2380,7 @@ def add_import_options( for k, v in translate.items(): import_nodes[k] = import_nodes[v] - regionalised_options = ["hvdc", "pipeline-h2", "shipping-lh2", "shipping-lch4"] + regionalised_options = ["hvdc-to-elec", "pipeline-h2", "shipping-lh2", "shipping-lch4"] for tech in set(options).intersection(regionalised_options): @@ -2396,7 +2396,7 @@ def add_import_options( suffix = bus_suffix[tech] location = import_nodes_tech.index - buses = location if tech == 'hvdc' else location + suffix + buses = location if tech == 'hvdc-to-elec' else location + suffix n.madd( "Generator", From 609914282fcfed7f5af18967ceba9c38dbf4762a Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 13 Jan 2022 15:27:33 +0100 Subject: [PATCH 011/293] change import hvdc naming in config --- config.default.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.default.yaml b/config.default.yaml index ebe23c441..3a51e55b4 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -269,7 +269,7 @@ sector: - shipping-lh2 - shipping-lch4 - shipping-ftfuel - - hvdc + - hvdc-to-elec # limit: 100 # TWh From 188dd58d96880be12ec5869e4a4fb3a47262bc89 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 1 Aug 2022 16:09:19 +0200 Subject: [PATCH 012/293] option to limit import volume with sector_opts wc --- scripts/solve_network.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/scripts/solve_network.py b/scripts/solve_network.py index a298a0083..966d1c702 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -225,7 +225,7 @@ def add_co2_sequestration_limit(n, sns): lhs = linexpr((1, vars_final_co2_stored)).sum() limit = n.config["sector"].get("co2_sequestration_potential", 200) * 1e6 - for o in opts: + for o in n.opts: if not "seq" in o: continue limit = float(o[o.find("seq")+3:]) break @@ -243,18 +243,22 @@ def add_co2_sequestration_limit(n, sns): def add_energy_import_limit(n, sns): import_gens = n.generators.loc[n.generators.carrier.str.contains("import")].index - import_opts = n.config["sector"]["import"] - if import_gens.empty or "limit" not in import_opts.keys(): return + limit = n.config["sector"].get('import', {}).get('limit', None) + for o in n.opts: + if not o.startswith("imp"): continue + match = o.split("+")[0][3:] + if match: limit = float(match) + break + + if import_gens.empty or limit is None: return weightings = n.snapshot_weightings.loc[sns] p = get_var(n, "Generator", "p")[import_gens] lhs = linexpr((weightings.generators, p)).sum().sum() - rhs = import_opts["limit"] * 1e6 - name = 'energy_import_limit' - define_constraints(n, lhs, '<=', rhs, 'GlobalConstraint', + define_constraints(n, lhs, '<=', limit * 1e6, 'GlobalConstraint', 'mu', axes=pd.Index([name]), spec=name) @@ -315,7 +319,7 @@ def solve_network(n, config, opts='', **kwargs): if tmpdir is not None: from pathlib import Path Path(tmpdir).mkdir(parents=True, exist_ok=True) - opts = snakemake.wildcards.opts.split('-') + opts = snakemake.wildcards.sector_opts.split('-') solve_opts = snakemake.config['solving']['options'] fn = getattr(snakemake.log, 'memory', None) From 53457e8ca4d65d36c4c201215cf1a5c72e6cfa99 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 1 Aug 2022 16:09:50 +0200 Subject: [PATCH 013/293] option to limit import carriers with sector_opts wc --- scripts/prepare_sector_network.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index ee546e695..36c63c595 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2496,12 +2496,24 @@ def limit_individual_line_extension(n, maxext): if options["co2_network"]: add_co2_network(n, costs) - if "import" in opts: - add_import_options( - n, + translate = dict( + H2=["pipeline-h2", "shipping-lh2"], + AC=["hvdc-to-elec"], + CH4=["shipping-lch4"], + FT=["shipping-ftfuel"], + ) + for o in opts: + if not o.startswith("imp"): continue + subsets = o.split("+")[1:] + if len(subsets): + carriers = sum([translate[s] for s in subsets], []) + else: + carriers = options["import"]["options"] + add_import_options(n, capacity_boost=options["import"]["capacity_boost"], - options=options["import"]["options"] + options=carriers ) + break for o in opts: m = re.match(r'^\d+h$', o, re.IGNORECASE) From f660f3357a4d6d0a27d9177096fd28ca0d320da4 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Tue, 2 Aug 2022 12:23:44 +0200 Subject: [PATCH 014/293] add_energy_import_limit: fix lhs with transpose --- scripts/solve_network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/solve_network.py b/scripts/solve_network.py index 966d1c702..7eedb94d3 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -255,7 +255,7 @@ def add_energy_import_limit(n, sns): weightings = n.snapshot_weightings.loc[sns] p = get_var(n, "Generator", "p")[import_gens] - lhs = linexpr((weightings.generators, p)).sum().sum() + lhs = linexpr((weightings.generators, p.T)).sum().sum() name = 'energy_import_limit' define_constraints(n, lhs, '<=', limit * 1e6, 'GlobalConstraint', From 53aa35228a0eccf1f19a476313abfa9c1f984087 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Tue, 2 Aug 2022 18:51:00 +0200 Subject: [PATCH 015/293] limit import options --- data/import-costs.csv | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/data/import-costs.csv b/data/import-costs.csv index 48d01fbf1..4f249f557 100644 --- a/data/import-costs.csv +++ b/data/import-costs.csv @@ -4,13 +4,10 @@ 2,BE,shipping-lh2,AR,104.43859036804487 3,BG,hvdc-to-elec,SA,51.65706274015358 4,BG,pipeline-h2,SA,68.10566247243335 -5,DE,hvdc-to-elec,AR,55.203771951051074 6,DE,pipeline-h2,AR,69.88789921638937 7,DE,shipping-ftfuel,AR,182.24375712329777 8,DE,shipping-lch4,AR,91.61589919772385 9,DE,shipping-lh2,AR,104.43858851873729 -10,EE,hvdc-to-elec,KZ,39.95540140644545 -11,EE,pipeline-h2,KZ,56.07506227072536 12,EE,shipping-ftfuel,AR,182.52210726941368 13,EE,shipping-lch4,AR,91.65234145140607 14,EE,shipping-lh2,AR,107.44614391340409 @@ -19,8 +16,6 @@ 17,ES,shipping-ftfuel,AR,182.01243859914985 18,ES,shipping-lch4,AR,90.90299692576285 19,ES,shipping-lh2,AR,102.46061173774919 -20,FI,hvdc-to-elec,KZ,39.321582156206546 -21,FI,pipeline-h2,KZ,55.52681520589047 22,FI,shipping-ftfuel,AR,182.49365807233582 23,FI,shipping-lch4,AR,92.08969911948437 24,FI,shipping-lh2,AR,107.44615010614157 @@ -32,8 +27,6 @@ 30,GR,shipping-ftfuel,AR,182.26049054219877 31,GR,shipping-lch4,AR,91.61589919772385 32,GR,shipping-lh2,AR,98.51052118547967 -33,HU,hvdc-to-elec,KZ,46.12111396710866 -34,HU,pipeline-h2,KZ,62.86492783503022 35,IE,shipping-ftfuel,AR,182.1158742352419 36,IE,shipping-lch4,AR,91.22698217084326 37,IE,shipping-lh2,AR,104.43859036804487 @@ -42,21 +35,15 @@ 40,IT,shipping-ftfuel,AR,190.04758692891002 41,IT,shipping-lch4,AR,91.61730792754746 42,IT,shipping-lh2,AR,100.99546124128042 -43,LT,hvdc-to-elec,KZ,42.96332128371362 -44,LT,pipeline-h2,KZ,58.66924277487358 45,LT,shipping-ftfuel,AR,182.47542971789662 46,LT,shipping-lch4,AR,91.69162132756114 47,LT,shipping-lh2,AR,107.44614391340409 -48,LV,hvdc-to-elec,KZ,40.940713744949505 -49,LV,pipeline-h2,KZ,56.922952511183325 50,LV,shipping-ftfuel,AR,197.86174537188373 51,LV,shipping-lch4,AR,91.65313498220655 52,LV,shipping-lh2,AR,107.44614391340409 53,NL,shipping-ftfuel,AR,182.17524752061587 54,NL,shipping-lch4,AR,91.22860996192286 55,NL,shipping-lh2,AR,104.43859036804487 -56,PL,hvdc-to-elec,KZ,44.419671366131446 -57,PL,pipeline-h2,KZ,59.928424139835634 58,PL,shipping-ftfuel,AR,182.51935857298088 59,PL,shipping-lch4,AR,91.64465517847141 60,PL,shipping-lh2,AR,107.44614391340409 @@ -65,5 +52,3 @@ 63,SE,shipping-ftfuel,AR,182.51228499588916 64,SE,shipping-lch4,AR,91.64249825392628 65,SE,shipping-lh2,AR,107.44614391340409 -66,SK,hvdc-to-elec,KZ,46.318087517254256 -67,SK,pipeline-h2,KZ,62.74405112407931 From 0f1d7e80fa6ab3e584b261bf9851371c80c17d2c Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 3 Aug 2022 16:50:25 +0200 Subject: [PATCH 016/293] implement carbon management for imported hydrocarbons --- config.default.yaml | 2 +- data/import-costs.csv | 103 +++++++++++++++--------------- scripts/plot_network.py | 6 +- scripts/prepare_sector_network.py | 100 ++++++++++++++++++++++------- scripts/solve_network.py | 7 +- 5 files changed, 137 insertions(+), 81 deletions(-) diff --git a/config.default.yaml b/config.default.yaml index 89f1d1c6f..31f269163 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -622,4 +622,4 @@ plotting: import shipping-lh2: '#ebe1ca' import shipping-lch4: '#d6cbb2' import shipping-ftfuel: '#bdb093' - import hvdc: '#91856a' + import hvdc-to-elec: '#91856a' diff --git a/data/import-costs.csv b/data/import-costs.csv index 4f249f557..6e5000ef8 100644 --- a/data/import-costs.csv +++ b/data/import-costs.csv @@ -1,54 +1,51 @@ ,importer,esc,exporter,value -0,BE,shipping-ftfuel,AR,182.1615272572155 -1,BE,shipping-lch4,AR,91.22697630541164 -2,BE,shipping-lh2,AR,104.43859036804487 -3,BG,hvdc-to-elec,SA,51.65706274015358 -4,BG,pipeline-h2,SA,68.10566247243335 -6,DE,pipeline-h2,AR,69.88789921638937 -7,DE,shipping-ftfuel,AR,182.24375712329777 -8,DE,shipping-lch4,AR,91.61589919772385 -9,DE,shipping-lh2,AR,104.43858851873729 -12,EE,shipping-ftfuel,AR,182.52210726941368 -13,EE,shipping-lch4,AR,91.65234145140607 -14,EE,shipping-lh2,AR,107.44614391340409 -15,ES,hvdc-to-elec,MA,33.8156906774716 -16,ES,pipeline-h2,MA,53.32101609033417 -17,ES,shipping-ftfuel,AR,182.01243859914985 -18,ES,shipping-lch4,AR,90.90299692576285 -19,ES,shipping-lh2,AR,102.46061173774919 -22,FI,shipping-ftfuel,AR,182.49365807233582 -23,FI,shipping-lch4,AR,92.08969911948437 -24,FI,shipping-lh2,AR,107.44615010614157 -25,GB,shipping-ftfuel,AR,182.2339850340641 -26,GB,shipping-lch4,AR,91.27673687833187 -27,GB,shipping-lh2,AR,104.43859036804487 -28,GR,hvdc-to-elec,SA,47.127927723900086 -29,GR,pipeline-h2,SA,64.18795044273045 -30,GR,shipping-ftfuel,AR,182.26049054219877 -31,GR,shipping-lch4,AR,91.61589919772385 -32,GR,shipping-lh2,AR,98.51052118547967 -35,IE,shipping-ftfuel,AR,182.1158742352419 -36,IE,shipping-lch4,AR,91.22698217084326 -37,IE,shipping-lh2,AR,104.43859036804487 -38,IT,hvdc-to-elec,EG,43.748868497023665 -39,IT,pipeline-h2,EG,61.07730835733746 -40,IT,shipping-ftfuel,AR,190.04758692891002 -41,IT,shipping-lch4,AR,91.61730792754746 -42,IT,shipping-lh2,AR,100.99546124128042 -45,LT,shipping-ftfuel,AR,182.47542971789662 -46,LT,shipping-lch4,AR,91.69162132756114 -47,LT,shipping-lh2,AR,107.44614391340409 -50,LV,shipping-ftfuel,AR,197.86174537188373 -51,LV,shipping-lch4,AR,91.65313498220655 -52,LV,shipping-lh2,AR,107.44614391340409 -53,NL,shipping-ftfuel,AR,182.17524752061587 -54,NL,shipping-lch4,AR,91.22860996192286 -55,NL,shipping-lh2,AR,104.43859036804487 -58,PL,shipping-ftfuel,AR,182.51935857298088 -59,PL,shipping-lch4,AR,91.64465517847141 -60,PL,shipping-lh2,AR,107.44614391340409 -61,RO,hvdc-to-elec,KZ,43.47453607157155 -62,RO,pipeline-h2,KZ,61.732667054236096 -63,SE,shipping-ftfuel,AR,182.51228499588916 -64,SE,shipping-lch4,AR,91.64249825392628 -65,SE,shipping-lh2,AR,107.44614391340409 +0,BE,shipping-ftfuel,AR,145 +1,BE,shipping-lch4,AR,90 +2,BE,shipping-lh2,AR,105 +3,BG,hvdc-to-elec,SA,52 +4,BG,pipeline-h2,SA,68 +7,DE,shipping-ftfuel,AR,145 +8,DE,shipping-lch4,AR,90 +9,DE,shipping-lh2,AR,105 +12,EE,shipping-ftfuel,AR,145 +13,EE,shipping-lch4,AR,90 +14,EE,shipping-lh2,AR,110 +15,ES,hvdc-to-elec,MA,34 +16,ES,pipeline-h2,MA,53 +17,ES,shipping-ftfuel,AR,145 +18,ES,shipping-lch4,AR,90 +19,ES,shipping-lh2,AR,102 +22,FI,shipping-ftfuel,AR,145 +23,FI,shipping-lch4,AR,90 +24,FI,shipping-lh2,AR,110 +25,GB,shipping-ftfuel,AR,145 +26,GB,shipping-lch4,AR,90 +27,GB,shipping-lh2,AR,105 +28,GR,hvdc-to-elec,SA,47 +29,GR,pipeline-h2,SA,94 +30,GR,shipping-ftfuel,AR,145 +31,GR,shipping-lch4,AR,90 +32,GR,shipping-lh2,AR,99 +35,IE,shipping-ftfuel,AR,145 +36,IE,shipping-lch4,AR,90 +37,IE,shipping-lh2,AR,105 +38,IT,hvdc-to-elec,EG,44 +39,IT,pipeline-h2,EG,61 +40,IT,shipping-ftfuel,AR,145 +41,IT,shipping-lch4,AR,90 +42,IT,shipping-lh2,AR,101 +45,LT,shipping-ftfuel,AR,145 +46,LT,shipping-lch4,AR,90 +47,LT,shipping-lh2,AR,110 +50,LV,shipping-ftfuel,AR,145 +51,LV,shipping-lch4,AR,90 +52,LV,shipping-lh2,AR,100 +53,NL,shipping-ftfuel,AR,145 +54,NL,shipping-lch4,AR,90 +55,NL,shipping-lh2,AR,100 +58,PL,shipping-ftfuel,AR,145 +59,PL,shipping-lch4,AR,90 +60,PL,shipping-lh2,AR,110 +63,SE,shipping-ftfuel,AR,145 +64,SE,shipping-lch4,AR,90 +65,SE,shipping-lh2,AR,110 diff --git a/scripts/plot_network.py b/scripts/plot_network.py index 4a1bc6d0a..e0f2affa8 100644 --- a/scripts/plot_network.py +++ b/scripts/plot_network.py @@ -307,7 +307,7 @@ def plot_h2_map(network): ) n.plot( - geomap=False, + #geomap=False, bus_sizes=0, link_colors='#72d3d6', link_widths=link_widths_retro, @@ -441,7 +441,7 @@ def plot_ch4_map(network): ) n.plot( - geomap=False, + # geomap=False, ax=ax, bus_sizes=0., link_colors='#e8d1d1', @@ -451,7 +451,7 @@ def plot_ch4_map(network): ) n.plot( - geomap=False, + # geomap=False, ax=ax, bus_sizes=0., link_colors=link_color_used, diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 36c63c595..7b57ff204 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2289,10 +2289,10 @@ def add_import_options( capacity_boost=3., options=["hvdc-to-elec", "pipeline-h2", "shipping-lh2", "shipping-lch4", "shipping-ftfuel"] ): - + logger.info("Add import options: " + " ".join(options)) fn = snakemake.input.gas_input_nodes_simplified import_nodes = pd.read_csv(fn, index_col=0) - import_nodes["hvdc-to-elec"] = 1e6 + import_nodes["hvdc-to-elec"] = 15000 translate = { "pipeline-h2": "pipeline", @@ -2308,13 +2308,18 @@ def add_import_options( "shipping-lch4": " gas", } + co2_intensity = { + "shipping-lch4": 'gas', + "shipping-ftfuel": 'oil', + } + import_costs = pd.read_csv(snakemake.input.import_costs, index_col=0).reset_index(drop=True) import_costs.rename(columns={"value": "marginal_cost"}, inplace=True) for k, v in translate.items(): import_nodes[k] = import_nodes[v] - regionalised_options = ["hvdc-to-elec", "pipeline-h2", "shipping-lh2", "shipping-lch4"] + regionalised_options = {"hvdc-to-elec", "pipeline-h2", "shipping-lh2", "shipping-lch4"} for tech in set(options).intersection(regionalised_options): @@ -2322,44 +2327,95 @@ def add_import_options( import_nodes_tech = import_nodes.loc[~import_nodes[tech].isna(), [tech]] import_nodes_tech = import_nodes_tech.rename(columns={tech: "p_nom"}) * capacity_boost - + marginal_costs = import_nodes_tech.index.str[:2].map(import_costs_tech) import_nodes_tech["marginal_cost"] = marginal_costs - + import_nodes_tech.dropna(inplace=True) suffix = bus_suffix[tech] - location = import_nodes_tech.index - buses = location if tech == 'hvdc-to-elec' else location + suffix - - n.madd( - "Generator", - import_nodes_tech.index + f"{suffix} import {tech}", - bus=buses, - carrier=f"import {tech}", - marginal_cost=import_nodes_tech.marginal_cost.values, - p_nom=import_nodes_tech.p_nom.values, - ) + + if tech in co2_intensity.keys(): + + buses = import_nodes_tech.index + f"{suffix} import {tech}" + + n.madd("Bus", + buses + " bus", + carrier=f"import {tech}" + ) + + n.madd("Store", + buses + " store", + bus=buses + " bus", + e_nom_extendable=True, + e_nom_min=-np.inf, + e_nom_max=0, + e_min_pu=1, + e_max_pu=0, + ) + + n.madd("Link", + buses, + bus0=buses + " bus", + bus1=import_nodes_tech.index + suffix, + bus2="co2 atmosphere", + carrier=f"import {tech}", + efficiency2=-costs.at[co2_intensity[tech], 'CO2 intensity'], + marginal_cost=import_nodes_tech.marginal_cost.values, + p_nom=import_nodes_tech.p_nom.values, + ) + + else: + + location = import_nodes_tech.index + buses = location if tech == 'hvdc-to-elec' else location + suffix + + n.madd( + "Generator", + import_nodes_tech.index + f"{suffix} import {tech}", + bus=buses, + carrier=f"import {tech}", + marginal_cost=import_nodes_tech.marginal_cost.values, + p_nom=import_nodes_tech.p_nom.values, + ) # need special handling for copperplated Fischer-Tropsch imports if "shipping-ftfuel" in options: marginal_costs = import_costs.query("esc == 'shipping-ftfuel'").marginal_cost.min() - n.add( - "Generator", - "EU oil import shipping-ftfuel", - bus="EU oil", + n.add("Bus", + "EU import shipping-ftfuel bus", + carrier="import shipping-ftfuel" + ) + + n.add("Store", + "EU import shipping-ftfuel store", + bus='EU import shipping-ftfuel bus', + e_nom_extendable=True, + e_nom_min=-np.inf, + e_nom_max=0, + e_min_pu=1, + e_max_pu=0, + ) + + n.add("Link", + "EU import shipping-ftfuel", + bus0="EU import shipping-ftfuel bus", + bus1="EU oil", + bus2="co2 atmosphere", carrier="import shipping-ftfuel", + efficiency2=-costs.at["oil", 'CO2 intensity'], marginal_cost=marginal_costs, - p_nom=1e6 + p_nom=1e7, ) def maybe_adjust_costs_and_potentials(n, opts): for o in opts: - if "+" not in o: continue + flags = ["+e", "+p", "+m"] + if all(flag not in o for flag in flags): continue oo = o.split("+") carrier_list = np.hstack((n.generators.carrier.unique(), n.links.carrier.unique(), n.stores.carrier.unique(), n.storage_units.carrier.unique())) diff --git a/scripts/solve_network.py b/scripts/solve_network.py index 7eedb94d3..0bc0dd2ef 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -243,6 +243,7 @@ def add_co2_sequestration_limit(n, sns): def add_energy_import_limit(n, sns): import_gens = n.generators.loc[n.generators.carrier.str.contains("import")].index + import_links = n.links.loc[n.links.carrier.str.contains("import")].index limit = n.config["sector"].get('import', {}).get('limit', None) for o in n.opts: @@ -251,10 +252,12 @@ def add_energy_import_limit(n, sns): if match: limit = float(match) break - if import_gens.empty or limit is None: return + if (import_gens.empty and import_links.empty) or limit is None: return weightings = n.snapshot_weightings.loc[sns] - p = get_var(n, "Generator", "p")[import_gens] + p_gens = get_var(n, "Generator", "p")[import_gens] + p_links = get_var(n, "Link", "p")[import_links] + p = pd.concat([p_gens, p_links], axis=1) lhs = linexpr((weightings.generators, p.T)).sum().sum() name = 'energy_import_limit' From 353d1621fef1d5b590d895f44980863f66b333f2 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 5 Aug 2022 17:47:41 +0200 Subject: [PATCH 017/293] fix plot_summary for pypsa 0.20 --- scripts/plot_summary.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/scripts/plot_summary.py b/scripts/plot_summary.py index 7d49eab21..1d3138425 100644 --- a/scripts/plot_summary.py +++ b/scripts/plot_summary.py @@ -7,7 +7,6 @@ plt.style.use('ggplot') from prepare_sector_network import co2_emissions_year -from helper import update_config_with_sector_opts #consolidate and rename def rename_techs(label): @@ -440,8 +439,6 @@ def plot_carbon_budget_distribution(): from helper import mock_snakemake snakemake = mock_snakemake('plot_summary') - update_config_with_sector_opts(snakemake.config, snakemake.wildcards.sector_opts) - n_header = 4 plot_costs() From 1da43dcf068635fe080f2815139afeff853fab8c Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 7 Sep 2022 09:50:49 +0200 Subject: [PATCH 018/293] add reference import sites to gas input locations --- scripts/build_gas_input_locations.py | 31 +++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/scripts/build_gas_input_locations.py b/scripts/build_gas_input_locations.py index c08d92de7..c40545783 100644 --- a/scripts/build_gas_input_locations.py +++ b/scripts/build_gas_input_locations.py @@ -8,6 +8,7 @@ import pandas as pd import geopandas as gpd from shapely import wkt +from vresutils.graph import voronoi_partition_pts from cluster_gas_network import load_bus_regions @@ -59,6 +60,24 @@ def build_gas_input_locations(lng_fn, planned_lng_fn, entry_fn, prod_fn, countri return pd.concat([prod[sel], entry[sel], lng[sel]], ignore_index=True) +def assign_reference_import_sites(gas_input_locations, import_sites, europe_shape): + + europe_shape = europe_shape.squeeze().geometry.buffer(1) # 1 latlon degree + + for kind in ["lng", "pipeline"]: + + locs = import_sites.query("type == @kind") + + partition = voronoi_partition_pts(locs[["x", "y"]].values, europe_shape) + partition = gpd.GeoDataFrame(dict(name=locs.index, geometry=partition)) + partition = partition.set_crs(4326).set_index('name') + + match = gpd.sjoin(gas_input_locations.query("type == @kind"), partition, how='left') + gas_input_locations.loc[gas_input_locations["type"] == kind, "port"] = match["index_right"] + + return gas_input_locations + + if __name__ == "__main__": if 'snakemake' not in globals(): @@ -76,6 +95,9 @@ def build_gas_input_locations(lng_fn, planned_lng_fn, entry_fn, prod_fn, countri snakemake.input.regions_offshore ) + europe_shape = gpd.read_file(snakemake.input.europe_shape) + import_sites = pd.read_csv(snakemake.input.reference_import_sites, index_col=0) + # add a buffer to eastern countries because some # entry points are still in Russian or Ukrainian territory. buffer = 9000 # meters @@ -93,6 +115,8 @@ def build_gas_input_locations(lng_fn, planned_lng_fn, entry_fn, prod_fn, countri countries ) + gas_input_locations = assign_reference_import_sites(gas_input_locations, import_sites, europe_shape) + gas_input_nodes = gpd.sjoin(gas_input_locations, regions, how='left') gas_input_nodes.rename(columns={"index_right": "bus"}, inplace=True) @@ -102,4 +126,9 @@ def build_gas_input_locations(lng_fn, planned_lng_fn, entry_fn, prod_fn, countri gas_input_nodes_s = gas_input_nodes.groupby(["bus", "type"])["p_nom"].sum().unstack() gas_input_nodes_s.columns.name = "p_nom" - gas_input_nodes_s.to_csv(snakemake.output.gas_input_nodes_simplified) \ No newline at end of file + gas_input_nodes_s.to_csv(snakemake.output.gas_input_nodes_simplified) + + ports = gas_input_nodes.groupby(["bus", "type"])["port"].first().unstack().drop("production", axis=1) + ports.columns.name = "port" + + ports.to_csv(snakemake.output.ports) From 7be05fc745e82b06759c96e70fee8fbf193af448 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 7 Sep 2022 09:52:26 +0200 Subject: [PATCH 019/293] adjust to new import cost data format --- Snakefile | 9 ++++++--- scripts/prepare_sector_network.py | 16 +++++++++++++--- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/Snakefile b/Snakefile index a090ce8a5..df6692473 100644 --- a/Snakefile +++ b/Snakefile @@ -136,10 +136,13 @@ if config["sector"]["gas_network"] or config["sector"]["H2_retrofit"]: production="data/gas_network/scigrid-gas/data/IGGIELGN_Productions.geojson", planned_lng="data/gas_network/planned_LNGs.csv", regions_onshore=pypsaeur("resources/regions_onshore_elec_s{simpl}_{clusters}.geojson"), - regions_offshore=pypsaeur('resources/regions_offshore_elec_s{simpl}_{clusters}.geojson') + regions_offshore=pypsaeur('resources/regions_offshore_elec_s{simpl}_{clusters}.geojson'), + europe_shape=pypsaeur("resources/europe_shape.geojson"), + reference_import_sites="data/import-sites.csv", output: gas_input_nodes="resources/gas_input_locations_s{simpl}_{clusters}.geojson", - gas_input_nodes_simplified="resources/gas_input_locations_s{simpl}_{clusters}_simplified.csv" + gas_input_nodes_simplified="resources/gas_input_locations_s{simpl}_{clusters}_simplified.csv", + ports="resources/ports_s{simpl}_{clusters}.csv", resources: mem_mb=2000, script: "scripts/build_gas_input_locations.py" @@ -499,7 +502,7 @@ rule prepare_sector_network: solar_thermal_total="resources/solar_thermal_total_elec_s{simpl}_{clusters}.nc", solar_thermal_urban="resources/solar_thermal_urban_elec_s{simpl}_{clusters}.nc", solar_thermal_rural="resources/solar_thermal_rural_elec_s{simpl}_{clusters}.nc", - import_costs="data/import-costs.csv", + import_costs="../../../data/results.csv", # TODO: host file on zenodo or elsewhere **build_retro_cost_output, **build_biomass_transport_costs_output, **gas_infrastructure diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 7b57ff204..3bc29ef2c 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2294,6 +2294,8 @@ def add_import_options( import_nodes = pd.read_csv(fn, index_col=0) import_nodes["hvdc-to-elec"] = 15000 + ports = pd.read_csv(snakemake.input.ports, index_col=0) + translate = { "pipeline-h2": "pipeline", "hvdc-to-elec": "hvdc-to-elec", @@ -2313,11 +2315,14 @@ def add_import_options( "shipping-ftfuel": 'oil', } - import_costs = pd.read_csv(snakemake.input.import_costs, index_col=0).reset_index(drop=True) + import_costs = pd.read_csv(snakemake.input.import_costs, delimiter=";") + cols = ["esc", "exporter", "importer", "value"] + import_costs = import_costs.query("subcategory == 'Cost per MWh delivered'")[cols] import_costs.rename(columns={"value": "marginal_cost"}, inplace=True) for k, v in translate.items(): import_nodes[k] = import_nodes[v] + ports[k] = ports.get(v) regionalised_options = {"hvdc-to-elec", "pipeline-h2", "shipping-lh2", "shipping-lch4"} @@ -2325,10 +2330,15 @@ def add_import_options( import_costs_tech = import_costs.query("esc == @tech").groupby('importer').marginal_cost.min() - import_nodes_tech = import_nodes.loc[~import_nodes[tech].isna(), [tech]] + sel = ~import_nodes[tech].isna() + if tech == 'pipeline-h2': + forbidden_pipelines = ["DE", "BE", "FR", "GB"] + sel &= ~import_nodes.index.str[:2].isin(forbidden_pipelines) + import_nodes_tech = import_nodes.loc[sel, [tech]] + import_nodes_tech = import_nodes_tech.rename(columns={tech: "p_nom"}) * capacity_boost - marginal_costs = import_nodes_tech.index.str[:2].map(import_costs_tech) + marginal_costs = ports[tech].dropna().map(import_costs_tech) import_nodes_tech["marginal_cost"] = marginal_costs import_nodes_tech.dropna(inplace=True) From 9f743ff885a0b25e2fdf8565c29c1b6fe39a970b Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 7 Sep 2022 09:52:54 +0200 Subject: [PATCH 020/293] add reference import sites --- data/import-sites.csv | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 data/import-sites.csv diff --git a/data/import-sites.csv b/data/import-sites.csv new file mode 100644 index 000000000..4d71e37dc --- /dev/null +++ b/data/import-sites.csv @@ -0,0 +1,13 @@ +name,y,x,reference,type,comment +EUSW,36.39,-5.64,https://openinframap.org/,pipeline, +EUS,37.18,14.53,https://openinframap.org/,pipeline, +EUSE,41.09,25.46,https://openinframap.org/,pipeline, +EUE,49.91,22.87,https://openinframap.org/,pipeline, +EUNE,57.53,27.34,https://openinframap.org/,pipeline, +BEZEE,51.353,3.22241,https://www.gem.wiki/Zeebrugge_LNG_Terminal,lng, +ESLCG,43.4613,-7.5,https://www.gem.wiki/Mugardos_LNG_Terminal,lng,"center coordinate moved east from -8.2395" +ESVLC,39.6329,-0.2152,https://www.gem.wiki/Sagunto_LNG_Terminal,lng, +GBMLF,51.7152,-7,https://www.gem.wiki/South_Hook_LNG_Terminal,lng,"center coordinate moved west from -5.07627" +GREEV,37.96,23.4023,https://www.gem.wiki/Revithoussa_LNG_Terminal,lng, +ITVCE,45.091667,12.586111,https://www.gem.wiki/Adriatic_LNG_Terminal,lng, +PLSWI,53.909167,17,https://www.gem.wiki/%C5%9Awinouj%C5%9Bcie_Polskie_LNG_Terminal,lng,"center coordinate moved east from 14.294722" \ No newline at end of file From 0a8fcfa3897d226c62c62da3667a14cb0ab82732 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 7 Sep 2022 09:53:20 +0200 Subject: [PATCH 021/293] remove old import costs file --- data/import-costs.csv | 51 ------------------------------------------- 1 file changed, 51 deletions(-) delete mode 100644 data/import-costs.csv diff --git a/data/import-costs.csv b/data/import-costs.csv deleted file mode 100644 index 6e5000ef8..000000000 --- a/data/import-costs.csv +++ /dev/null @@ -1,51 +0,0 @@ -,importer,esc,exporter,value -0,BE,shipping-ftfuel,AR,145 -1,BE,shipping-lch4,AR,90 -2,BE,shipping-lh2,AR,105 -3,BG,hvdc-to-elec,SA,52 -4,BG,pipeline-h2,SA,68 -7,DE,shipping-ftfuel,AR,145 -8,DE,shipping-lch4,AR,90 -9,DE,shipping-lh2,AR,105 -12,EE,shipping-ftfuel,AR,145 -13,EE,shipping-lch4,AR,90 -14,EE,shipping-lh2,AR,110 -15,ES,hvdc-to-elec,MA,34 -16,ES,pipeline-h2,MA,53 -17,ES,shipping-ftfuel,AR,145 -18,ES,shipping-lch4,AR,90 -19,ES,shipping-lh2,AR,102 -22,FI,shipping-ftfuel,AR,145 -23,FI,shipping-lch4,AR,90 -24,FI,shipping-lh2,AR,110 -25,GB,shipping-ftfuel,AR,145 -26,GB,shipping-lch4,AR,90 -27,GB,shipping-lh2,AR,105 -28,GR,hvdc-to-elec,SA,47 -29,GR,pipeline-h2,SA,94 -30,GR,shipping-ftfuel,AR,145 -31,GR,shipping-lch4,AR,90 -32,GR,shipping-lh2,AR,99 -35,IE,shipping-ftfuel,AR,145 -36,IE,shipping-lch4,AR,90 -37,IE,shipping-lh2,AR,105 -38,IT,hvdc-to-elec,EG,44 -39,IT,pipeline-h2,EG,61 -40,IT,shipping-ftfuel,AR,145 -41,IT,shipping-lch4,AR,90 -42,IT,shipping-lh2,AR,101 -45,LT,shipping-ftfuel,AR,145 -46,LT,shipping-lch4,AR,90 -47,LT,shipping-lh2,AR,110 -50,LV,shipping-ftfuel,AR,145 -51,LV,shipping-lch4,AR,90 -52,LV,shipping-lh2,AR,100 -53,NL,shipping-ftfuel,AR,145 -54,NL,shipping-lch4,AR,90 -55,NL,shipping-lh2,AR,100 -58,PL,shipping-ftfuel,AR,145 -59,PL,shipping-lch4,AR,90 -60,PL,shipping-lh2,AR,110 -63,SE,shipping-ftfuel,AR,145 -64,SE,shipping-lch4,AR,90 -65,SE,shipping-lh2,AR,110 From 654fcb864d500413c21d36a39779689e05c87487 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 7 Sep 2022 10:17:34 +0200 Subject: [PATCH 022/293] add shipping-lnh3 ammonia as import option --- config.default.yaml | 2 ++ scripts/prepare_sector_network.py | 17 ++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/config.default.yaml b/config.default.yaml index c116decdb..8e8ea3627 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -270,6 +270,7 @@ sector: - pipeline-h2 - shipping-lh2 - shipping-lch4 + - shipping-lnh3 - shipping-ftfuel - hvdc-to-elec # limit: 100 # TWh @@ -630,5 +631,6 @@ plotting: import pipeline-h2: '#fff6e0' import shipping-lh2: '#ebe1ca' import shipping-lch4: '#d6cbb2' + import shipping-lnh3: '#c6c093' import shipping-ftfuel: '#bdb093' import hvdc-to-elec: '#91856a' diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 0f47829ee..3da2ae44a 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2369,7 +2369,7 @@ def remove_h2_network(n): def add_import_options( n, capacity_boost=3., - options=["hvdc-to-elec", "pipeline-h2", "shipping-lh2", "shipping-lch4", "shipping-ftfuel"] + options=["hvdc-to-elec", "pipeline-h2", "shipping-lh2", "shipping-lch4", "shipping-ftfuel", "shipping-lnh3"] ): logger.info("Add import options: " + " ".join(options)) fn = snakemake.input.gas_input_nodes_simplified @@ -2383,6 +2383,7 @@ def add_import_options( "hvdc-to-elec": "hvdc-to-elec", "shipping-lh2": "lng", "shipping-lch4": "lng", + "shipping-lnh3": "lng", } bus_suffix = { @@ -2390,6 +2391,7 @@ def add_import_options( "hvdc-to-elec": "", "shipping-lh2": " H2", "shipping-lch4": " gas", + "shipping-lnh3": " NH3", } co2_intensity = { @@ -2502,6 +2504,18 @@ def add_import_options( p_nom=1e7, ) + if "shipping-lnh3" in options and "shipping-lnh3" not in regionalised_options: + + marginal_costs = import_costs.query("esc == 'shipping-lnh3'").marginal_cost.min() + + n.add("Generator", + "EU import shipping-lnh3", + bus="EU NH3", + carrier="import shipping-lnh3", + marginal_cost=marginal_costs, + p_nom=1e7, + ) + def maybe_adjust_costs_and_potentials(n, opts): @@ -2651,6 +2665,7 @@ def limit_individual_line_extension(n, maxext): H2=["pipeline-h2", "shipping-lh2"], AC=["hvdc-to-elec"], CH4=["shipping-lch4"], + NH3=["shipping-lnh3"], FT=["shipping-ftfuel"], ) for o in opts: From a12981fada86e043abf8693ff14bac5d529e2253 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 7 Sep 2022 10:21:46 +0200 Subject: [PATCH 023/293] style: directly iterate over dictionary --- 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 3da2ae44a..5c4b3e2c6 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2429,7 +2429,7 @@ def add_import_options( suffix = bus_suffix[tech] - if tech in co2_intensity.keys(): + if tech, carrier in co2_intensity: buses = import_nodes_tech.index + f"{suffix} import {tech}" @@ -2454,7 +2454,7 @@ def add_import_options( bus1=import_nodes_tech.index + suffix, bus2="co2 atmosphere", carrier=f"import {tech}", - efficiency2=-costs.at[co2_intensity[tech], 'CO2 intensity'], + efficiency2=-costs.at[carrier, 'CO2 intensity'], marginal_cost=import_nodes_tech.marginal_cost.values, p_nom=import_nodes_tech.p_nom.values, ) From 2f501cc492594735dcbee6f02392219c2710e59e Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 8 Sep 2022 10:30:05 +0200 Subject: [PATCH 024/293] add_import_options: rename options -> import_options --- 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 5c4b3e2c6..41951ec13 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2369,9 +2369,10 @@ def remove_h2_network(n): def add_import_options( n, capacity_boost=3., - options=["hvdc-to-elec", "pipeline-h2", "shipping-lh2", "shipping-lch4", "shipping-ftfuel", "shipping-lnh3"] + import_options=["hvdc-to-elec", "pipeline-h2", "shipping-lh2", "shipping-lch4", "shipping-ftfuel", "shipping-lnh3"], + endogenous_hvdc=False, ): - logger.info("Add import options: " + " ".join(options)) + logger.info("Add import options: " + " ".join(import_options)) fn = snakemake.input.gas_input_nodes_simplified import_nodes = pd.read_csv(fn, index_col=0) import_nodes["hvdc-to-elec"] = 15000 @@ -2474,7 +2475,7 @@ def add_import_options( ) # need special handling for copperplated Fischer-Tropsch imports - if "shipping-ftfuel" in options: + if "shipping-ftfuel" in import_options: marginal_costs = import_costs.query("esc == 'shipping-ftfuel'").marginal_cost.min() @@ -2504,7 +2505,7 @@ def add_import_options( p_nom=1e7, ) - if "shipping-lnh3" in options and "shipping-lnh3" not in regionalised_options: + if "shipping-lnh3" in import_options and "shipping-lnh3" not in regionalised_options: marginal_costs = import_costs.query("esc == 'shipping-lnh3'").marginal_cost.min() @@ -2677,7 +2678,7 @@ def limit_individual_line_extension(n, maxext): carriers = options["import"]["options"] add_import_options(n, capacity_boost=options["import"]["capacity_boost"], - options=carriers + import_options=carriers ) break From a03236479fafef345a32914ae02ea587960a07d6 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 8 Sep 2022 10:30:24 +0200 Subject: [PATCH 025/293] revert handling of co2 intensity --- 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 41951ec13..e6e33ac57 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2430,7 +2430,7 @@ def add_import_options( suffix = bus_suffix[tech] - if tech, carrier in co2_intensity: + if tech in co2_intensity.keys(): buses = import_nodes_tech.index + f"{suffix} import {tech}" @@ -2455,7 +2455,7 @@ def add_import_options( bus1=import_nodes_tech.index + suffix, bus2="co2 atmosphere", carrier=f"import {tech}", - efficiency2=-costs.at[carrier, 'CO2 intensity'], + efficiency2=-costs.at[co2_intensity[tech], 'CO2 intensity'], marginal_cost=import_nodes_tech.marginal_cost.values, p_nom=import_nodes_tech.p_nom.values, ) From 68d27847f17d361cbee4fbf28a4b97872b034aa4 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 8 Sep 2022 10:31:45 +0200 Subject: [PATCH 026/293] add endogenous hvdc imports options --- Snakefile | 2 + config.default.yaml | 17 +++- scripts/prepare_sector_network.py | 150 +++++++++++++++++++++++++++++- 3 files changed, 167 insertions(+), 2 deletions(-) diff --git a/Snakefile b/Snakefile index df6692473..fc89c36ae 100644 --- a/Snakefile +++ b/Snakefile @@ -503,6 +503,8 @@ rule prepare_sector_network: solar_thermal_urban="resources/solar_thermal_urban_elec_s{simpl}_{clusters}.nc", solar_thermal_rural="resources/solar_thermal_rural_elec_s{simpl}_{clusters}.nc", import_costs="../../../data/results.csv", # TODO: host file on zenodo or elsewhere + import_p_max_pu="../../../data/hvdc-to-elec_aggregated-time-series_p-max-pu_.nc", # TODO: host file on zenodo or elsewhere + regions_onshore=pypsaeur("resources/regions_onshore_elec_s{simpl}_{clusters}.geojson"), **build_retro_cost_output, **build_biomass_transport_costs_output, **gas_infrastructure diff --git a/config.default.yaml b/config.default.yaml index 8e8ea3627..fc2004b32 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -273,7 +273,22 @@ sector: - shipping-lnh3 - shipping-ftfuel - hvdc-to-elec - # limit: 100 # TWh + endogenous_hvdc_import: + enable: false + exporters: + - CN-West + - DZ + - EG + - KZ + - LY + - MA + - SA + - TN + - TR + - UA + hvdc_losses: 3.e-5 # p.u./km + length_factor: 1.25 + distance_threshold: 0.05 # quantile industry: diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index e6e33ac57..7d66ab6bd 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2366,6 +2366,150 @@ def remove_h2_network(n): n.stores.drop("EU H2 Store", inplace=True) +def add_endogenous_hvdc_import_options(n): + + cf = snakemake.config["sector"]["import"].get("endogenous_hvdc_import", {}) + if not cf["enable"]: return + + regions = gpd.read_file(snakemake.input.regions_onshore).set_index('name') + + p_max_pu = xr.open_dataset(snakemake.input.import_p_max_pu).p_max_pu.sel(importer='EUE') + + def _coordinates(ct): + loc = geolocator.geocode(ct.split('-')[0]) + return [loc.longitude, loc.latitude] + + exporters = pd.DataFrame({ct: _coordinates(ct) for ct in cf["exporters"]}, index=['x', 'y']).T + geometry = gpd.points_from_xy(exporters.x, exporters.y) + exporters = gpd.GeoDataFrame(exporters, geometry=geometry, crs=4326) + + import_links = {} + a = regions.representative_point().to_crs(3857) + for ct in exporters.index: + b = exporters.to_crs(3857).loc[ct].geometry + d = a.distance(b) + import_links[ct] = d.where(d < d.quantile(cf["distance_threshold"])).div(1e3).dropna() # km + import_links = pd.concat(import_links) + + hvdc_cost = ( + import_links.values * cf["length_factor"] * costs.at['HVDC submarine', 'capital_cost'] + + costs.at['HVDC inverter pair', 'capital_cost'] + ) + + buses_i = exporters.index + + n.madd("Bus", buses_i, **exporters.drop('geometry', axis=1)) + + n.madd("Link", + ["import hvdc-to-elec " + ' '.join(idx).strip() for idx in import_links.index], + bus0=import_links.index.get_level_values(0), + bus1=import_links.index.get_level_values(1), + carrier="import hvdc-to-elec", + p_min_pu=0, + p_nom_extendable=True, + length=import_links.values, + capital_cost=hvdc_cost, + efficiency=1 - import_links.values * cf["hvdc_losses"], + ) + + for tech in ["solar-utility", "onwind", "offwind"]: + + p_max_pu_tech = p_max_pu.sel(technology=tech).to_pandas().dropna() + + exporters_tech = exporters.index.intersection(p_max_pu_tech.index) + + n.madd("Generator", + exporters_tech.index, + suffix=" " + tech, + bus=exporters.index, + carrier=f"external {tech}", + p_nom_extendable=True, + capital_cost=costs[tech, "fixed"], + lifetime=costs[tech, "lifetime"], + p_max_pu=p_max_pu_tech.reindex(exporters_tech.index), + ) + + # hydrogen storage + + h2_buses_i = n.madd("Bus", + buses_i, + suffix=" H2", + carrier="external H2", + location=buses_i + ) + + n.madd("Store", + h2_buses_i, + bus=h2_buses_i, + carrier='external H2', + e_nom_extendable=True, + e_cyclic=True, + capital_cost=costs.at["hydrogen storage tank incl. compressor", "fixed"] + ) + + n.madd("Link", + h2_buses_i + " Electrolysis", + bus0=buses_i, + bus1=h2_buses_i, + carrier='external H2 Electrolysis', + p_nom_extendable=True, + efficiency=costs.at["electrolysis", "efficiency"], + capital_cost=costs.at["electrolysis", "fixed"], + lifetime=costs.at["electrolysis", "lifetime"] + ) + + n.madd("Link", + h2_buses_i + " Fuel Cell", + bus0=h2_buses_i, + bus1=buses_i, + carrier='external H2 Fuel Cell', + p_nom_extendable=True, + efficiency=costs.at["fuel cell", "efficiency"], + capital_cost=costs.at["fuel cell", "fixed"] * costs.at["fuel cell", "efficiency"], + lifetime=costs.at["fuel cell", "lifetime"] + ) + + # battery storage + + b_buses_i = n.madd("Bus", + buses_i, + suffix=" battery", + carrier="external battery", + location=buses_i + ) + + n.madd("Store", + b_buses_i, + bus=b_buses_i, + carrier='external battery', + e_cyclic=True, + e_nom_extendable=True, + capital_cost=costs.at['battery storage', 'fixed'], + lifetime=costs.at['battery storage', 'lifetime'] + ) + + n.madd("Link", + b_buses_i + " charger", + bus0=buses_i, + bus1=b_buses_i, + carrier='external battery charger', + efficiency=costs.at['battery inverter', 'efficiency']**0.5, + capital_cost=costs.at['battery inverter', 'fixed'], + p_nom_extendable=True, + lifetime=costs.at['battery inverter', 'lifetime'] + ) + + n.madd("Link", + b_buses_i + " discharger", + bus0=b_buses_i, + bus1=buses_i, + carrier='external battery discharger', + efficiency=costs.at['battery inverter','efficiency']**0.5, + p_nom_extendable=True, + lifetime=costs.at['battery inverter', 'lifetime'] + ) + + def add_import_options( n, capacity_boost=3., @@ -2411,7 +2555,11 @@ def add_import_options( regionalised_options = {"hvdc-to-elec", "pipeline-h2", "shipping-lh2", "shipping-lch4"} - for tech in set(options).intersection(regionalised_options): + if endogenous_hvdc and 'hvdc-to-elec' in import_options: + import_options = [o for o in import_options if o != 'hvdc-to-elec'] + add_endogenous_hvdc_import_options(n) + + for tech in set(import_options).intersection(regionalised_options): import_costs_tech = import_costs.query("esc == @tech").groupby('importer').marginal_cost.min() From d395077b9e7c8786028c53296290401b28b60d9a Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 8 Sep 2022 10:32:15 +0200 Subject: [PATCH 027/293] import option limit is present by default --- config.default.yaml | 1 + scripts/solve_network.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/config.default.yaml b/config.default.yaml index fc2004b32..cac8c207d 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -266,6 +266,7 @@ sector: OCGT: gas import: capacity_boost: 2 + limit: false # bool or number in TWh options: - pipeline-h2 - shipping-lh2 diff --git a/scripts/solve_network.py b/scripts/solve_network.py index 0bc0dd2ef..85b9c7a96 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -245,14 +245,14 @@ def add_energy_import_limit(n, sns): import_gens = n.generators.loc[n.generators.carrier.str.contains("import")].index import_links = n.links.loc[n.links.carrier.str.contains("import")].index - limit = n.config["sector"].get('import', {}).get('limit', None) + limit = n.config["sector"].get('import', {}).get('limit', False) for o in n.opts: if not o.startswith("imp"): continue match = o.split("+")[0][3:] if match: limit = float(match) break - if (import_gens.empty and import_links.empty) or limit is None: return + if (import_gens.empty and import_links.empty) or not limit: return weightings = n.snapshot_weightings.loc[sns] p_gens = get_var(n, "Generator", "p")[import_gens] From fa29dd8342e0dc7c8d1a370e8d048dc18b3f8c60 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 8 Sep 2022 10:33:21 +0200 Subject: [PATCH 028/293] aggregate all external assets to import hvdc-to-elec --- scripts/plot_network.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/plot_network.py b/scripts/plot_network.py index a8f3cd3e5..54ffcc9f4 100644 --- a/scripts/plot_network.py +++ b/scripts/plot_network.py @@ -35,6 +35,8 @@ def rename_techs_tyndp(tech): return "offshore wind" elif "CC" in tech or "sequestration" in tech: return "CCS" + elif "external" in tech: + return "import hvdc-to-elec" else: return tech From 7eb1858eeef80982f8346e3bc7d33d86cd76784f Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 8 Sep 2022 10:36:35 +0200 Subject: [PATCH 029/293] add missing imports --- scripts/prepare_sector_network.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 7d66ab6bd..40cad1009 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -8,6 +8,7 @@ import numpy as np import xarray as xr import networkx as nx +import geopandas as gpd from itertools import product from scipy.stats import beta @@ -20,6 +21,9 @@ from networkx.algorithms import complement from pypsa.geo import haversine_pts +from geopy.geocoders import Nominatim +geolocator = Nominatim(user_agent="locate-exporting-region") + import logging logger = logging.getLogger(__name__) From 58253a05e1fc9d00cb489da2084281b17646d67b Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 8 Sep 2022 14:52:58 +0200 Subject: [PATCH 030/293] pass endogenous_hvdc_import option --- scripts/prepare_sector_network.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 40cad1009..a46d7c995 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -703,8 +703,8 @@ def add_ammonia(n, costs): carrier="Haber-Bosch", efficiency=1 / (cf_industry["MWh_elec_per_tNH3_electrolysis"] / cf_industry["MWh_NH3_per_tNH3"]), # output: MW_NH3 per MW_elec efficiency2=-cf_industry["MWh_H2_per_tNH3_electrolysis"] / cf_industry["MWh_elec_per_tNH3_electrolysis"], # input: MW_H2 per MW_elec - capital_cost=costs.at["Haber-Bosch synthesis", "fixed"], - lifetime=costs.at["Haber-Bosch synthesis", 'lifetime'] + capital_cost=costs.at["Haber-Bosch", "fixed"], + lifetime=costs.at["Haber-Bosch", 'lifetime'] ) n.madd("Link", @@ -2830,7 +2830,8 @@ def limit_individual_line_extension(n, maxext): carriers = options["import"]["options"] add_import_options(n, capacity_boost=options["import"]["capacity_boost"], - import_options=carriers + import_options=carriers, + endogenous_hvdc=options["import"]["endogenous_hvdc_import"]["enable"] ) break From 3bb3586d9cb009c2c82dbe50c9b0f08d09349a5c Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 8 Sep 2022 14:53:58 +0200 Subject: [PATCH 031/293] plot_summary: fix issue with lnh3 in carrier name --- scripts/plot_summary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/plot_summary.py b/scripts/plot_summary.py index f324f8274..6de34c2af 100644 --- a/scripts/plot_summary.py +++ b/scripts/plot_summary.py @@ -256,7 +256,7 @@ def plot_balances(): df = df / 1e6 #remove trailing link ports - forbidden = ["co2", "import shipping-lh2", "import pipeline-h2", "NH3"] + forbidden = ["co2", "import shipping-lh2", "import shipping-lnh3", "import pipeline-h2", "NH3"] df.index = [i[:-1] if ((i not in forbidden) and (i[-1:] in ["0","1","2","3"])) else i for i in df.index] df = df.groupby(df.index.map(rename_techs)).sum() From cafd14000dea6e0ded25f7939e1c2355fc8d336c Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 8 Sep 2022 14:57:00 +0200 Subject: [PATCH 032/293] plot_network: external asset aggregation takes precedence --- scripts/plot_network.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/plot_network.py b/scripts/plot_network.py index 54ffcc9f4..19e3f575a 100644 --- a/scripts/plot_network.py +++ b/scripts/plot_network.py @@ -19,6 +19,8 @@ def rename_techs_tyndp(tech): tech = rename_techs(tech) if "heat pump" in tech or "resistive heater" in tech: return "power-to-heat" + elif "external" in tech: + return "import hvdc-to-elec" elif tech in ["H2 Electrolysis", "methanation", "helmeth", "H2 liquefaction"]: return "power-to-gas" elif tech == "H2": @@ -35,8 +37,6 @@ def rename_techs_tyndp(tech): return "offshore wind" elif "CC" in tech or "sequestration" in tech: return "CCS" - elif "external" in tech: - return "import hvdc-to-elec" else: return tech From b013a1440f0349e84149f5b708efd430342a8dcc Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 8 Sep 2022 15:13:12 +0200 Subject: [PATCH 033/293] fix duplicate index accessing --- scripts/prepare_sector_network.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index a46d7c995..78197ec24 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2420,17 +2420,17 @@ def _coordinates(ct): p_max_pu_tech = p_max_pu.sel(technology=tech).to_pandas().dropna() - exporters_tech = exporters.index.intersection(p_max_pu_tech.index) + exporters_tech_i = exporters.index.intersection(p_max_pu_tech.index) n.madd("Generator", - exporters_tech.index, + exporters_tech_i, suffix=" " + tech, bus=exporters.index, carrier=f"external {tech}", p_nom_extendable=True, capital_cost=costs[tech, "fixed"], lifetime=costs[tech, "lifetime"], - p_max_pu=p_max_pu_tech.reindex(exporters_tech.index), + p_max_pu=p_max_pu_tech.reindex(exporters_tech_i), ) # hydrogen storage From 9d385346710daff4012a3cc7a73d12c31a72abbd Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 8 Sep 2022 15:17:17 +0200 Subject: [PATCH 034/293] fix costs db access --- 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 78197ec24..bb75a80e1 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2428,8 +2428,8 @@ def _coordinates(ct): bus=exporters.index, carrier=f"external {tech}", p_nom_extendable=True, - capital_cost=costs[tech, "fixed"], - lifetime=costs[tech, "lifetime"], + capital_cost=costs.at[tech, "fixed"], + lifetime=costs.at[tech, "lifetime"], p_max_pu=p_max_pu_tech.reindex(exporters_tech_i), ) From 68c73ca86c62e5035fc2db071cf3c7374af11d97 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 8 Sep 2022 15:17:36 +0200 Subject: [PATCH 035/293] use 'fixed' instead of 'capital_cost' --- 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 bb75a80e1..c4d12b39d 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2396,8 +2396,8 @@ def _coordinates(ct): import_links = pd.concat(import_links) hvdc_cost = ( - import_links.values * cf["length_factor"] * costs.at['HVDC submarine', 'capital_cost'] + - costs.at['HVDC inverter pair', 'capital_cost'] + import_links.values * cf["length_factor"] * costs.at['HVDC submarine', 'fixed'] + + costs.at['HVDC inverter pair', 'fixed'] ) buses_i = exporters.index From 9fc149ac0b0c8804a7c772b0b69820956ff998d2 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Sat, 10 Sep 2022 13:55:55 +0200 Subject: [PATCH 036/293] add colors/summary for external infrastructure, transpose p_max_pu --- config.default.yaml | 5 +++++ scripts/make_summary.py | 2 +- scripts/plot_summary.py | 5 +++++ scripts/prepare_sector_network.py | 11 ++++++----- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/config.default.yaml b/config.default.yaml index cac8c207d..662762c21 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -650,3 +650,8 @@ plotting: import shipping-lnh3: '#c6c093' import shipping-ftfuel: '#bdb093' import hvdc-to-elec: '#91856a' + external H2: '#f7cdf0' + external battery: '#dff7cb' + external offwind: '#d0e8f5' + external onwind: '#accbfc' + external solar: '#fcf1c7' \ No newline at end of file diff --git a/scripts/make_summary.py b/scripts/make_summary.py index 8d5f4e484..047c5a152 100644 --- a/scripts/make_summary.py +++ b/scripts/make_summary.py @@ -412,7 +412,7 @@ def calculate_weighted_prices(n, label, weighted_prices): elif carrier[:5] == "space": load = heat_demand_df[buses.str[:2]].rename(columns=lambda i: str(i)+suffix) else: - load = n.loads_t.p_set[buses] + load = n.loads_t.p_set[buses.intersection(n.loads_t.p_set.columns)].reindex(columns=buses).fillna(0.) for tech in link_loads[carrier]: diff --git a/scripts/plot_summary.py b/scripts/plot_summary.py index 6de34c2af..4fe5ed86c 100644 --- a/scripts/plot_summary.py +++ b/scripts/plot_summary.py @@ -38,6 +38,11 @@ def rename_techs(label): # "H2 Fuel Cell": "hydrogen storage", # "H2 pipeline": "hydrogen storage", "battery": "battery storage", + "external H2": "external H2", + "external battery": "external battery", + "external offwind": "external offwind", + "external onwind": "external onwind", + "external solar": "external solar", # "CC": "CC" } diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index c4d12b39d..4820e2e26 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -22,7 +22,7 @@ from pypsa.geo import haversine_pts from geopy.geocoders import Nominatim -geolocator = Nominatim(user_agent="locate-exporting-region") +geolocator = Nominatim(user_agent="locate-exporting-region", timeout=10) import logging logger = logging.getLogger(__name__) @@ -2372,6 +2372,7 @@ def remove_h2_network(n): def add_endogenous_hvdc_import_options(n): + logger.info("Add import options: endogenous hvdc-to-elec") cf = snakemake.config["sector"]["import"].get("endogenous_hvdc_import", {}) if not cf["enable"]: return @@ -2418,19 +2419,19 @@ def _coordinates(ct): for tech in ["solar-utility", "onwind", "offwind"]: - p_max_pu_tech = p_max_pu.sel(technology=tech).to_pandas().dropna() + p_max_pu_tech = p_max_pu.sel(technology=tech).to_pandas().dropna().T - exporters_tech_i = exporters.index.intersection(p_max_pu_tech.index) + exporters_tech_i = exporters.index.intersection(p_max_pu_tech.columns) n.madd("Generator", exporters_tech_i, suffix=" " + tech, - bus=exporters.index, + bus=exporters_tech_i, carrier=f"external {tech}", p_nom_extendable=True, capital_cost=costs.at[tech, "fixed"], lifetime=costs.at[tech, "lifetime"], - p_max_pu=p_max_pu_tech.reindex(exporters_tech_i), + p_max_pu=p_max_pu_tech.reindex(columns=exporters_tech_i), ) # hydrogen storage From 320bd8a6a612bd904288001daf37345fc7841d3d Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 16 Sep 2022 09:58:40 +0200 Subject: [PATCH 037/293] add option to define sense of energy import constraints in config --- config.default.yaml | 1 + scripts/solve_network.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/config.default.yaml b/config.default.yaml index 662762c21..07364eeec 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -267,6 +267,7 @@ sector: import: capacity_boost: 2 limit: false # bool or number in TWh + limit_sense: "==" options: - pipeline-h2 - shipping-lh2 diff --git a/scripts/solve_network.py b/scripts/solve_network.py index 85b9c7a96..18dee4e58 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -246,6 +246,7 @@ def add_energy_import_limit(n, sns): import_links = n.links.loc[n.links.carrier.str.contains("import")].index limit = n.config["sector"].get('import', {}).get('limit', False) + limit_sense = n.config["sector"].get('import', {}).get('limit_sense', '<=') for o in n.opts: if not o.startswith("imp"): continue match = o.split("+")[0][3:] @@ -261,7 +262,7 @@ def add_energy_import_limit(n, sns): lhs = linexpr((weightings.generators, p.T)).sum().sum() name = 'energy_import_limit' - define_constraints(n, lhs, '<=', limit * 1e6, 'GlobalConstraint', + define_constraints(n, lhs, limit_sense, limit * 1e6, 'GlobalConstraint', 'mu', axes=pd.Index([name]), spec=name) From 5603568efad585a4f73507ce8572cbe913205cd3 Mon Sep 17 00:00:00 2001 From: euronion <42553970+euronion@users.noreply.github.com> Date: Thu, 17 Nov 2022 12:19:56 +0100 Subject: [PATCH 038/293] include subworkflow for TRACE dependencies --- Snakefile | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Snakefile b/Snakefile index fc89c36ae..129e38c64 100644 --- a/Snakefile +++ b/Snakefile @@ -23,6 +23,13 @@ SDIR = config['summary_dir'] + '/' + config['run'] RDIR = config['results_dir'] + config['run'] CDIR = config['costs_dir'] +# Requires clone of this version of trace +# https://github.com/euronion/trace/commit/646d48b2e7a889594338bc376e0a6ecc5d18998f +# in parallel directory to /pypsa-eur-sec +subworkflow trace: + workdir: "../trace" + snakefile: "../trace/Snakefile" + configfile: "../trace/config/config.default.yaml" subworkflow pypsaeur: workdir: "../pypsa-eur" @@ -502,8 +509,8 @@ rule prepare_sector_network: solar_thermal_total="resources/solar_thermal_total_elec_s{simpl}_{clusters}.nc", solar_thermal_urban="resources/solar_thermal_urban_elec_s{simpl}_{clusters}.nc", solar_thermal_rural="resources/solar_thermal_rural_elec_s{simpl}_{clusters}.nc", - import_costs="../../../data/results.csv", # TODO: host file on zenodo or elsewhere - import_p_max_pu="../../../data/hvdc-to-elec_aggregated-time-series_p-max-pu_.nc", # TODO: host file on zenodo or elsewhere + import_costs=trace("results/results.csv"), # TODO: host file on zenodo or elsewhere + import_p_max_pu=trace("results/combined_weighted_generator_timeseries.nc"), # TODO: host file on zenodo or elsewhere regions_onshore=pypsaeur("resources/regions_onshore_elec_s{simpl}_{clusters}.geojson"), **build_retro_cost_output, **build_biomass_transport_costs_output, From 472f7906a74a341ff9dfc259e4721d4b67aeeedd Mon Sep 17 00:00:00 2001 From: euronion <42553970+euronion@users.noreply.github.com> Date: Thu, 17 Nov 2022 12:48:09 +0100 Subject: [PATCH 039/293] Add option for enabling/disabling transmission losses Requires PyPSA branch from: https://github.com/PyPSA/PyPSA/pull/462 --- config.default.yaml | 1 + scripts/solve_network.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/config.default.yaml b/config.default.yaml index 07364eeec..1e2fa36f9 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -251,6 +251,7 @@ sector: electricity_distribution_grid: true electricity_distribution_grid_cost_factor: 1.0 #multiplies cost in data/costs.csv electricity_grid_connection: true # only applies to onshore wind and utility PV + electricity_grid_transmission_losses: 3 # Enable (=3) or disable (= 0) transmission losses in electricity grid H2_network: true gas_network: false H2_retrofit: false # if set to True existing gas pipes can be retrofitted to H2 pipes diff --git a/scripts/solve_network.py b/scripts/solve_network.py index 18dee4e58..20611c447 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -288,6 +288,7 @@ def solve_network(n, config, opts='', **kwargs): if cf_solving.get('skip_iterations', False): network_lopf(n, solver_name=solver_name, solver_options=solver_options, + transmission_losses=config["sector"]["electricity_grid_transmission_losses"], extra_functionality=extra_functionality, keep_shadowprices=keep_shadowprices, **kwargs) else: @@ -295,6 +296,7 @@ def solve_network(n, config, opts='', **kwargs): track_iterations=track_iterations, min_iterations=min_iterations, max_iterations=max_iterations, + transmission_losses=config["sector"]["electricity_grid_transmission_losses"], extra_functionality=extra_functionality, keep_shadowprices=keep_shadowprices, **kwargs) From 7ae5746c8ebbdc07246404b8f7c0270afe3e5183 Mon Sep 17 00:00:00 2001 From: euronion <42553970+euronion@users.noreply.github.com> Date: Thu, 17 Nov 2022 17:00:33 +0100 Subject: [PATCH 040/293] Set electricity_grid_transmission_losses to false by default --- config.default.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config.default.yaml b/config.default.yaml index 1e2fa36f9..6f526c3ca 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -251,7 +251,7 @@ sector: electricity_distribution_grid: true electricity_distribution_grid_cost_factor: 1.0 #multiplies cost in data/costs.csv electricity_grid_connection: true # only applies to onshore wind and utility PV - electricity_grid_transmission_losses: 3 # Enable (=3) or disable (= 0) transmission losses in electricity grid + electricity_grid_transmission_losses: 0 # Enable (= value from 1 to 3) or disable (= 0) transmission losses in electricity grid, see PyPSA#462 H2_network: true gas_network: false H2_retrofit: false # if set to True existing gas pipes can be retrofitted to H2 pipes @@ -656,4 +656,4 @@ plotting: external battery: '#dff7cb' external offwind: '#d0e8f5' external onwind: '#accbfc' - external solar: '#fcf1c7' \ No newline at end of file + external solar: '#fcf1c7' From 12f4a1ac399a7bfc7be41d3a09d084385335b3dd Mon Sep 17 00:00:00 2001 From: euronion <42553970+euronion@users.noreply.github.com> Date: Fri, 25 Nov 2022 11:05:12 +0100 Subject: [PATCH 041/293] Fix: Non-compliant lines for transmission losses. --- 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 4820e2e26..5c9c6199e 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2879,4 +2879,11 @@ def limit_individual_line_extension(n, maxext): if options['electricity_grid_connection']: add_electricity_grid_connection(n, costs) + # Workaround: Remove lines with conflicting (and unrealistic) properties + # cf. https://github.com/PyPSA/pypsa-eur/issues/444 + if options['electricity_grid_transmission_losses']: + idx = n.lines.query("num_parallel == 0").index + logger.info(f"Removing {len(idx)} line(s) with properties conflicting with transmission losses functionality.") + n.mremove("Line", idx) + n.export_to_netcdf(snakemake.output[0]) From 5a4e5f34aa7022efc8eaef6f5cb1c8ae64112d4e Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Sat, 3 Dec 2022 14:36:05 +0100 Subject: [PATCH 042/293] separate bus for steel with DRI + EAF --- Snakefile | 4 +- config.default.yaml | 1 + ...build_industrial_energy_demand_per_node.py | 14 ++-- scripts/prepare_sector_network.py | 76 +++++++++++++++---- 4 files changed, 72 insertions(+), 23 deletions(-) diff --git a/Snakefile b/Snakefile index 43beaf087..088c8b8c5 100644 --- a/Snakefile +++ b/Snakefile @@ -373,7 +373,6 @@ rule build_industrial_energy_demand_per_node: input: industry_sector_ratios="resources/industry_sector_ratios.csv", industrial_production_per_node="resources/industrial_production_elec_s{simpl}_{clusters}_{planning_horizons}.csv", - industrial_energy_demand_per_node_today="resources/industrial_energy_demand_today_elec_s{simpl}_{clusters}.csv" output: industrial_energy_demand_per_node="resources/industrial_energy_demand_elec_s{simpl}_{clusters}_{planning_horizons}.csv" threads: 1 @@ -482,7 +481,10 @@ rule prepare_sector_network: busmap=pypsaeur("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", + industrial_production="resources/industrial_production_elec_s{simpl}_{clusters}_{planning_horizons}.csv", industrial_demand="resources/industrial_energy_demand_elec_s{simpl}_{clusters}_{planning_horizons}.csv", + industrial_demand_today="resources/industrial_energy_demand_today_elec_s{simpl}_{clusters}.csv", + industry_sector_ratios="resources/industry_sector_ratios.csv", heat_demand_urban="resources/heat_demand_urban_elec_s{simpl}_{clusters}.nc", heat_demand_rural="resources/heat_demand_rural_elec_s{simpl}_{clusters}.nc", heat_demand_total="resources/heat_demand_total_elec_s{simpl}_{clusters}.nc", diff --git a/config.default.yaml b/config.default.yaml index 4078ee013..0d617020d 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -266,6 +266,7 @@ sector: OCGT: gas biomass_to_liquid: false biosng: false + endogenous_steel: false diff --git a/scripts/build_industrial_energy_demand_per_node.py b/scripts/build_industrial_energy_demand_per_node.py index d665f18ec..74346403a 100644 --- a/scripts/build_industrial_energy_demand_per_node.py +++ b/scripts/build_industrial_energy_demand_per_node.py @@ -11,7 +11,7 @@ clusters=48, planning_horizons=2030, ) - + # import EU ratios df as csv fn = snakemake.input.industry_sector_ratios industry_sector_ratios = pd.read_csv(fn, index_col=0) @@ -20,13 +20,10 @@ fn = snakemake.input.industrial_production_per_node nodal_production = pd.read_csv(fn, index_col=0) - # energy demand today to get current electricity - fn = snakemake.input.industrial_energy_demand_per_node_today - nodal_today = pd.read_csv(fn, index_col=0) + # final energy consumption per node, sector and carrier + nodal_dict = {k: s * industry_sector_ratios for k, s in nodal_production.iterrows()} + nodal_df = pd.concat(nodal_dict, axis=1).T - # final energy consumption per node and industry (TWh/a) - nodal_df = nodal_production.dot(industry_sector_ratios.T) - # convert GWh to TWh and ktCO2 to MtCO2 nodal_df *= 0.001 @@ -37,8 +34,7 @@ } nodal_df.rename(columns=rename_sectors, inplace=True) - nodal_df["current electricity"] = nodal_today["electricity"] - + nodal_df.index.set_names(["node", "sector"], inplace=True) nodal_df.index.name = "TWh/a (MtCO2/a)" fn = snakemake.output.industrial_energy_demand_per_node diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 6c153f16c..724cf53d1 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2030,7 +2030,57 @@ def add_industry(n, costs): nodes = pop_layout.index # 1e6 to convert TWh to MWh - industrial_demand = pd.read_csv(snakemake.input.industrial_demand, index_col=0) * 1e6 + industrial_demand = pd.read_csv(snakemake.input.industrial_demand, index_col=[0,1]) * 1e6 + industrial_demand_today = pd.read_csv(snakemake.input.industrial_demand_today, index_col=0) * 1e6 + + industrial_production = pd.read_csv(snakemake.input.industrial_production, index_col=0) + industry_sector_ratios = pd.read_csv(snakemake.input.industry_sector_ratios, index_col=0) + + endogenous_sectors = ["DRI + Electric arc"] + sectors_b = ~industrial_demand.index.get_level_values('sector').isin(endogenous_sectors) + + if options["endogenous_steel"]: + + sector = "DRI + Electric arc" + + n.add("Bus", + "EU steel", + location="EU", + carrier="steel", + unit="t", + ) + + n.add("Load", + "EU steel", + bus="EU steel", + carrier="steel", + p_set=industrial_production[sector].sum() / 8760 + ) + + n.add("Store", + "EU steel Store", + bus="EU steel", + e_nom_extendable=True, + e_cyclic=True, + carrier="steel", + ) + + ratio = industry_sector_ratios[sector] + + n.madd("Link", + nodes, + suffix=" DRI + Electric arc" + bus0=nodes, + bus1="EU steel", + bus2=nodes + " H2", + bus3=spatial.gas.nodes, + bus4="co2 atmosphere", + efficiency=1 / ratio["elec"], + efficiency2=- ratio["hydrogen"] / ratio["elec"], + efficiency3=- ratio["methane"] / ratio["elec"], + efficiency4=ratio["process emission"] / ratio["elec"] + ) + # TODO specific consumption of heat (small), requires extension to bus5 n.madd("Bus", spatial.biomass.industry, @@ -2040,9 +2090,9 @@ def add_industry(n, costs): ) if options["biomass_transport"]: - p_set = industrial_demand.loc[spatial.biomass.locations, "solid biomass"].rename(index=lambda x: x + " solid biomass for industry") / 8760 + p_set = industrial_demand.loc[(spatial.biomass.locations, sectors_b), "solid biomass"].groupby(level='nodes').sum().rename(index=lambda x: x + " solid biomass for industry") / 8760 else: - p_set = industrial_demand["solid biomass"].sum() / 8760 + p_set = industrial_demand.loc[sectors_b, "solid biomass"].sum() / 8760 n.madd("Load", spatial.biomass.industry, @@ -2081,7 +2131,7 @@ def add_industry(n, costs): carrier="gas for industry", unit="MWh_LHV") - gas_demand = industrial_demand.loc[nodes, "methane"] / 8760. + gas_demand = industrial_demand.loc[(nodes, sectors_b), "methane"].groupby(level='node').sum() / 8760. if options["gas_network"]: spatial_gas_demand = gas_demand.rename(index=lambda x: x + " gas for industry") @@ -2126,7 +2176,7 @@ def add_industry(n, costs): suffix=" H2 for industry", bus=nodes + " H2", carrier="H2 for industry", - p_set=industrial_demand.loc[nodes, "hydrogen"] / 8760 + p_set=industrial_demand.loc[(nodes, sectors_b), "hydrogen"].groupby(level='node').sum() / 8760 ) if options["shipping_hydrogen_liquefaction"]: @@ -2255,7 +2305,7 @@ def add_industry(n, costs): ["naphtha for industry"], bus=spatial.oil.nodes, carrier="naphtha for industry", - p_set=industrial_demand.loc[nodes, "naphtha"].sum() / 8760 + p_set=industrial_demand.loc[(nodes, sectors_b), "naphtha"].sum() / 8760 ) all_aviation = ["total international aviation", "total domestic aviation"] @@ -2272,7 +2322,7 @@ def add_industry(n, costs): #except for the process emissions when naphtha is used for petrochemicals, which can be captured with other industry process emissions #tco2 per hour co2_release = ["naphtha for industry", "kerosene for aviation"] - co2 = n.loads.loc[co2_release, "p_set"].sum() * costs.at["oil", 'CO2 intensity'] - industrial_demand.loc[nodes, "process emission from feedstock"].sum() / 8760 + co2 = n.loads.loc[co2_release, "p_set"].sum() * costs.at["oil", 'CO2 intensity'] - industrial_demand.loc[(nodes, sectors_b), "process emission from feedstock"].sum() / 8760 n.add("Load", "oil emissions", @@ -2287,7 +2337,7 @@ def add_industry(n, costs): suffix=" low-temperature heat for industry", bus=[node + " urban central heat" if node + " urban central heat" in n.buses.index else node + " services urban decentral heat" for node in nodes], carrier="low-temperature heat for industry", - p_set=industrial_demand.loc[nodes, "low-temperature heat"] / 8760 + p_set=industrial_demand.loc[(nodes, sectors_b), "low-temperature heat"].groupby(level='node').sum() / 8760 ) # remove today's industrial electricity demand by scaling down total electricity demand @@ -2295,7 +2345,7 @@ def add_industry(n, costs): # TODO map onto n.bus.country loads_i = n.loads.index[(n.loads.index.str[:2] == ct) & (n.loads.carrier == "electricity")] if n.loads_t.p_set[loads_i].empty: continue - factor = 1 - industrial_demand.loc[loads_i, "current electricity"].sum() / n.loads_t.p_set[loads_i].sum().sum() + factor = 1 - industrial_demand_today.loc[loads_i, "electricity"].sum() / n.loads_t.p_set[loads_i].sum().sum() n.loads_t.p_set[loads_i] *= factor n.madd("Load", @@ -2303,7 +2353,7 @@ def add_industry(n, costs): suffix=" industry electricity", bus=nodes, carrier="industry electricity", - p_set=industrial_demand.loc[nodes, "electricity"] / 8760 + p_set=industrial_demand.loc[(nodes, sectors_b), "electricity"].groupby(level='node').sum() / 8760 ) n.add("Bus", @@ -2319,7 +2369,7 @@ def add_industry(n, costs): "process emissions", bus="process emissions", carrier="process emissions", - p_set=-industrial_demand.loc[nodes,["process emission", "process emission from feedstock"]].sum(axis=1).sum() / 8760 + p_set=-industrial_demand.loc[(nodes, sectors_b),["process emission", "process emission from feedstock"]].sum(axis=1).sum() / 8760 ) n.add("Link", @@ -2349,9 +2399,9 @@ def add_industry(n, costs): if options.get("ammonia"): if options["ammonia"] == 'regional': - p_set = industrial_demand.loc[spatial.ammonia.locations, "ammonia"].rename(index=lambda x: x + " NH3") / 8760 + p_set = industrial_demand.loc[(spatial.ammonia.locations, sectors_b), "ammonia"].groupby(level='node').sum().rename(index=lambda x: x + " NH3") / 8760 else: - p_set = industrial_demand["ammonia"].sum() / 8760 + p_set = industrial_demand.loc[sectors_b, "ammonia"].sum() / 8760 n.madd("Load", spatial.ammonia.nodes, From 59a2a72af727598dae45306caedc74638fdfa6e6 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Sat, 3 Dec 2022 14:43:06 +0100 Subject: [PATCH 043/293] additions to DRI + EAF multi-link --- 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 724cf53d1..1f500b212 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2069,7 +2069,12 @@ def add_industry(n, costs): n.madd("Link", nodes, - suffix=" DRI + Electric arc" + suffix=f" {sector}", + carrier=sector, + p_nom_extendable=True, + p_min_pu=1, + capital_cost=0, + lifetime=99, bus0=nodes, bus1="EU steel", bus2=nodes + " H2", From f63ded8a8c70a73662774d0949319afddf18936d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 26 Apr 2023 15:59:30 +0000 Subject: [PATCH 044/293] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Snakefile | 9 +- config/config.default.yaml | 34 ++--- data/import-sites.csv | 2 +- rules/build_sector.smk | 10 +- scripts/build_gas_input_locations.py | 30 ++-- scripts/make_summary.py | 6 +- scripts/plot_summary.py | 12 +- scripts/prepare_sector_network.py | 212 +++++++++++++++------------ scripts/solve_network.py | 36 +++-- 9 files changed, 206 insertions(+), 145 deletions(-) diff --git a/Snakefile b/Snakefile index 8b2d7f6a4..681bdc96b 100644 --- a/Snakefile +++ b/Snakefile @@ -50,9 +50,12 @@ wildcard_constraints: # https://github.com/euronion/trace/commit/646d48b2e7a889594338bc376e0a6ecc5d18998f # in parallel directory to /pypsa-eur-sec subworkflow trace: - workdir: "../trace" - snakefile: "../trace/Snakefile" - configfile: "../trace/config/config.default.yaml" + workdir: + "../trace" + snakefile: + "../trace/Snakefile" + configfile: + "../trace/config/config.default.yaml" include: "rules/common.smk" diff --git a/config/config.default.yaml b/config/config.default.yaml index 9b5ff2a6c..2e5a081eb 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -518,25 +518,25 @@ sector: limit: false # bool or number in TWh limit_sense: "==" options: - - pipeline-h2 - - shipping-lh2 - - shipping-lch4 - - shipping-lnh3 - - shipping-ftfuel - - hvdc-to-elec + - pipeline-h2 + - shipping-lh2 + - shipping-lch4 + - shipping-lnh3 + - shipping-ftfuel + - hvdc-to-elec endogenous_hvdc_import: enable: false exporters: - - CN-West - - DZ - - EG - - KZ - - LY - - MA - - SA - - TN - - TR - - UA + - CN-West + - DZ + - EG + - KZ + - LY + - MA + - SA + - TN + - TR + - UA hvdc_losses: 3.e-5 # p.u./km length_factor: 1.25 distance_threshold: 0.05 # quantile @@ -995,4 +995,4 @@ plotting: external battery: '#dff7cb' external offwind: '#d0e8f5' external onwind: '#accbfc' - external solar: '#fcf1c7' \ No newline at end of file + external solar: '#fcf1c7' diff --git a/data/import-sites.csv b/data/import-sites.csv index 4d71e37dc..329fc137e 100644 --- a/data/import-sites.csv +++ b/data/import-sites.csv @@ -10,4 +10,4 @@ ESVLC,39.6329,-0.2152,https://www.gem.wiki/Sagunto_LNG_Terminal,lng, GBMLF,51.7152,-7,https://www.gem.wiki/South_Hook_LNG_Terminal,lng,"center coordinate moved west from -5.07627" GREEV,37.96,23.4023,https://www.gem.wiki/Revithoussa_LNG_Terminal,lng, ITVCE,45.091667,12.586111,https://www.gem.wiki/Adriatic_LNG_Terminal,lng, -PLSWI,53.909167,17,https://www.gem.wiki/%C5%9Awinouj%C5%9Bcie_Polskie_LNG_Terminal,lng,"center coordinate moved east from 14.294722" \ No newline at end of file +PLSWI,53.909167,17,https://www.gem.wiki/%C5%9Awinouj%C5%9Bcie_Polskie_LNG_Terminal,lng,"center coordinate moved east from 14.294722" diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 662445c9a..c1eceb2e0 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -95,8 +95,7 @@ if config["sector"]["gas_network"] or config["sector"]["H2_retrofit"]: + "regions_onshore_elec_s{simpl}_{clusters}.geojson", regions_offshore=RESOURCES + "regions_offshore_elec_s{simpl}_{clusters}.geojson", - europe_shape=RESOURCES - + "europe_shape.geojson", + europe_shape=RESOURCES + "europe_shape.geojson", reference_import_sites="data/import-sites.csv", output: gas_input_nodes=RESOURCES @@ -730,10 +729,9 @@ rule prepare_sector_network: + "solar_thermal_rural_elec_s{simpl}_{clusters}.nc" if config["sector"]["solar_thermal"] else [], - import_costs=trace("results/result.csv"), # TODO: host file on zenodo or elsewhere - import_p_max_pu=trace("results/combined_weighted_generator_timeseries.nc"), # TODO: host file on zenodo or elsewhere - regions_onshore=RESOURCES - + "regions_onshore_elec_s{simpl}_{clusters}.geojson", + import_costs=trace("results/result.csv"), # TODO: host file on zenodo or elsewhere + import_p_max_pu=trace("results/combined_weighted_generator_timeseries.nc"), # TODO: host file on zenodo or elsewhere + regions_onshore=RESOURCES + "regions_onshore_elec_s{simpl}_{clusters}.geojson", output: RESULTS + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", diff --git a/scripts/build_gas_input_locations.py b/scripts/build_gas_input_locations.py index 75a04a40f..1228f9402 100644 --- a/scripts/build_gas_input_locations.py +++ b/scripts/build_gas_input_locations.py @@ -11,11 +11,10 @@ logger = logging.getLogger(__name__) -import pandas as pd import geopandas as gpd -from vresutils.graph import voronoi_partition_pts - +import pandas as pd from cluster_gas_network import load_bus_regions +from vresutils.graph import voronoi_partition_pts def read_scigrid_gas(fn): @@ -79,19 +78,21 @@ def build_gas_input_locations(lng_fn, entry_fn, prod_fn, countries): def assign_reference_import_sites(gas_input_locations, import_sites, europe_shape): - - europe_shape = europe_shape.squeeze().geometry.buffer(1) # 1 latlon degree + europe_shape = europe_shape.squeeze().geometry.buffer(1) # 1 latlon degree for kind in ["lng", "pipeline"]: - locs = import_sites.query("type == @kind") partition = voronoi_partition_pts(locs[["x", "y"]].values, europe_shape) partition = gpd.GeoDataFrame(dict(name=locs.index, geometry=partition)) - partition = partition.set_crs(4326).set_index('name') + partition = partition.set_crs(4326).set_index("name") - match = gpd.sjoin(gas_input_locations.query("type == @kind"), partition, how='left') - gas_input_locations.loc[gas_input_locations["type"] == kind, "port"] = match["index_right"] + match = gpd.sjoin( + gas_input_locations.query("type == @kind"), partition, how="left" + ) + gas_input_locations.loc[gas_input_locations["type"] == kind, "port"] = match[ + "index_right" + ] return gas_input_locations @@ -133,7 +134,9 @@ def assign_reference_import_sites(gas_input_locations, import_sites, europe_shap countries, ) - gas_input_locations = assign_reference_import_sites(gas_input_locations, import_sites, europe_shape) + gas_input_locations = assign_reference_import_sites( + gas_input_locations, import_sites, europe_shape + ) gas_input_nodes = gpd.sjoin(gas_input_locations, regions, how="left") @@ -148,7 +151,12 @@ def assign_reference_import_sites(gas_input_locations, import_sites, europe_shap gas_input_nodes_s.to_csv(snakemake.output.gas_input_nodes_simplified) - ports = gas_input_nodes.groupby(["bus", "type"])["port"].first().unstack().drop("production", axis=1) + ports = ( + gas_input_nodes.groupby(["bus", "type"])["port"] + .first() + .unstack() + .drop("production", axis=1) + ) ports.columns.name = "port" ports.to_csv(snakemake.output.ports) diff --git a/scripts/make_summary.py b/scripts/make_summary.py index bee090d82..456bd6d1a 100644 --- a/scripts/make_summary.py +++ b/scripts/make_summary.py @@ -514,7 +514,11 @@ def calculate_weighted_prices(n, label, weighted_prices): columns=lambda i: str(i) + suffix ) else: - load = n.loads_t.p_set[buses.intersection(n.loads_t.p_set.columns)].reindex(columns=buses).fillna(0.) + load = ( + n.loads_t.p_set[buses.intersection(n.loads_t.p_set.columns)] + .reindex(columns=buses) + .fillna(0.0) + ) for tech in link_loads[carrier]: names = n.links.index[n.links.index.to_series().str[-len(tech) :] == tech] diff --git a/scripts/plot_summary.py b/scripts/plot_summary.py index 00a244bfa..d48772aa9 100644 --- a/scripts/plot_summary.py +++ b/scripts/plot_summary.py @@ -282,11 +282,15 @@ def plot_balances(): df = df / 1e6 # remove trailing link ports - forbidden = ["co2", "import shipping-lh2", "import shipping-lnh3", "import pipeline-h2", "NH3"] + forbidden = [ + "co2", + "import shipping-lh2", + "import shipping-lnh3", + "import pipeline-h2", + "NH3", + ] df.index = [ - i[:-1] - if ((i not in forbidden) and (i[-1:] in ["0", "1", "2", "3"])) - else i + i[:-1] if ((i not in forbidden) and (i[-1:] in ["0", "1", "2", "3"])) else i for i in df.index ] diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 3c78bbcbe..95753e0d2 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -12,10 +12,10 @@ import re from itertools import product +import geopandas as gpd import networkx as nx import numpy as np import pandas as pd -import geopandas as gpd import pypsa import xarray as xr from _helpers import ( @@ -24,6 +24,7 @@ update_config_with_sector_opts, ) from build_energy_totals import build_co2_totals, build_eea_co2, build_eurostat_co2 +from geopy.geocoders import Nominatim from networkx.algorithms import complement from networkx.algorithms.connectivity.edge_augmentation import k_edge_augmentation from pypsa.geo import haversine_pts @@ -31,7 +32,6 @@ from scipy.stats import beta from vresutils.costdata import annuity -from geopy.geocoders import Nominatim geolocator = Nominatim(user_agent="locate-exporting-region", timeout=10) logger = logging.getLogger(__name__) @@ -2995,20 +2995,24 @@ def remove_h2_network(n): def add_endogenous_hvdc_import_options(n): - logger.info("Add import options: endogenous hvdc-to-elec") cf = snakemake.config["sector"]["import"].get("endogenous_hvdc_import", {}) - if not cf["enable"]: return + if not cf["enable"]: + return - regions = gpd.read_file(snakemake.input.regions_onshore).set_index('name') + regions = gpd.read_file(snakemake.input.regions_onshore).set_index("name") - p_max_pu = xr.open_dataset(snakemake.input.import_p_max_pu).p_max_pu.sel(importer='EUE') + p_max_pu = xr.open_dataset(snakemake.input.import_p_max_pu).p_max_pu.sel( + importer="EUE" + ) def _coordinates(ct): - loc = geolocator.geocode(ct.split('-')[0]) + loc = geolocator.geocode(ct.split("-")[0]) return [loc.longitude, loc.latitude] - exporters = pd.DataFrame({ct: _coordinates(ct) for ct in cf["exporters"]}, index=['x', 'y']).T + exporters = pd.DataFrame( + {ct: _coordinates(ct) for ct in cf["exporters"]}, index=["x", "y"] + ).T geometry = gpd.points_from_xy(exporters.x, exporters.y) exporters = gpd.GeoDataFrame(exporters, geometry=geometry, crs=4326) @@ -3017,20 +3021,23 @@ def _coordinates(ct): for ct in exporters.index: b = exporters.to_crs(3857).loc[ct].geometry d = a.distance(b) - import_links[ct] = d.where(d < d.quantile(cf["distance_threshold"])).div(1e3).dropna() # km + import_links[ct] = ( + d.where(d < d.quantile(cf["distance_threshold"])).div(1e3).dropna() + ) # km import_links = pd.concat(import_links) hvdc_cost = ( - import_links.values * cf["length_factor"] * costs.at['HVDC submarine', 'fixed'] + - costs.at['HVDC inverter pair', 'fixed'] + import_links.values * cf["length_factor"] * costs.at["HVDC submarine", "fixed"] + + costs.at["HVDC inverter pair", "fixed"] ) buses_i = exporters.index - n.madd("Bus", buses_i, **exporters.drop('geometry', axis=1)) + n.madd("Bus", buses_i, **exporters.drop("geometry", axis=1)) - n.madd("Link", - ["import hvdc-to-elec " + ' '.join(idx).strip() for idx in import_links.index], + n.madd( + "Link", + ["import hvdc-to-elec " + " ".join(idx).strip() for idx in import_links.index], bus0=import_links.index.get_level_values(0), bus1=import_links.index.get_level_values(1), carrier="import hvdc-to-elec", @@ -3042,12 +3049,12 @@ def _coordinates(ct): ) for tech in ["solar-utility", "onwind", "offwind"]: - p_max_pu_tech = p_max_pu.sel(technology=tech).to_pandas().dropna().T exporters_tech_i = exporters.index.intersection(p_max_pu_tech.columns) - n.madd("Generator", + n.madd( + "Generator", exporters_tech_i, suffix=" " + tech, bus=exporters_tech_i, @@ -3060,89 +3067,97 @@ def _coordinates(ct): # hydrogen storage - h2_buses_i = n.madd("Bus", - buses_i, - suffix=" H2", - carrier="external H2", - location=buses_i + h2_buses_i = n.madd( + "Bus", buses_i, suffix=" H2", carrier="external H2", location=buses_i ) - n.madd("Store", + n.madd( + "Store", h2_buses_i, bus=h2_buses_i, - carrier='external H2', + carrier="external H2", e_nom_extendable=True, e_cyclic=True, - capital_cost=costs.at["hydrogen storage tank incl. compressor", "fixed"] + capital_cost=costs.at["hydrogen storage tank incl. compressor", "fixed"], ) - n.madd("Link", + n.madd( + "Link", h2_buses_i + " Electrolysis", bus0=buses_i, bus1=h2_buses_i, - carrier='external H2 Electrolysis', + carrier="external H2 Electrolysis", p_nom_extendable=True, efficiency=costs.at["electrolysis", "efficiency"], capital_cost=costs.at["electrolysis", "fixed"], - lifetime=costs.at["electrolysis", "lifetime"] + lifetime=costs.at["electrolysis", "lifetime"], ) - n.madd("Link", + n.madd( + "Link", h2_buses_i + " Fuel Cell", bus0=h2_buses_i, bus1=buses_i, - carrier='external H2 Fuel Cell', + carrier="external H2 Fuel Cell", p_nom_extendable=True, efficiency=costs.at["fuel cell", "efficiency"], - capital_cost=costs.at["fuel cell", "fixed"] * costs.at["fuel cell", "efficiency"], - lifetime=costs.at["fuel cell", "lifetime"] + capital_cost=costs.at["fuel cell", "fixed"] + * costs.at["fuel cell", "efficiency"], + lifetime=costs.at["fuel cell", "lifetime"], ) # battery storage - b_buses_i = n.madd("Bus", - buses_i, - suffix=" battery", - carrier="external battery", - location=buses_i + b_buses_i = n.madd( + "Bus", buses_i, suffix=" battery", carrier="external battery", location=buses_i ) - n.madd("Store", + n.madd( + "Store", b_buses_i, bus=b_buses_i, - carrier='external battery', + carrier="external battery", e_cyclic=True, e_nom_extendable=True, - capital_cost=costs.at['battery storage', 'fixed'], - lifetime=costs.at['battery storage', 'lifetime'] + capital_cost=costs.at["battery storage", "fixed"], + lifetime=costs.at["battery storage", "lifetime"], ) - n.madd("Link", + n.madd( + "Link", b_buses_i + " charger", bus0=buses_i, bus1=b_buses_i, - carrier='external battery charger', - efficiency=costs.at['battery inverter', 'efficiency']**0.5, - capital_cost=costs.at['battery inverter', 'fixed'], + carrier="external battery charger", + efficiency=costs.at["battery inverter", "efficiency"] ** 0.5, + capital_cost=costs.at["battery inverter", "fixed"], p_nom_extendable=True, - lifetime=costs.at['battery inverter', 'lifetime'] + lifetime=costs.at["battery inverter", "lifetime"], ) - n.madd("Link", + n.madd( + "Link", b_buses_i + " discharger", bus0=b_buses_i, bus1=buses_i, - carrier='external battery discharger', - efficiency=costs.at['battery inverter','efficiency']**0.5, + carrier="external battery discharger", + efficiency=costs.at["battery inverter", "efficiency"] ** 0.5, p_nom_extendable=True, - lifetime=costs.at['battery inverter', 'lifetime'] + lifetime=costs.at["battery inverter", "lifetime"], ) def add_import_options( n, - capacity_boost=3., - import_options=["hvdc-to-elec", "pipeline-h2", "shipping-lh2", "shipping-lch4", "shipping-ftfuel", "shipping-lnh3"], + capacity_boost=3.0, + import_options=[ + "hvdc-to-elec", + "pipeline-h2", + "shipping-lh2", + "shipping-lch4", + "shipping-ftfuel", + "shipping-lnh3", + ], endogenous_hvdc=False, ): logger.info("Add import options: " + " ".join(import_options)) @@ -3169,8 +3184,8 @@ def add_import_options( } co2_intensity = { - "shipping-lch4": 'gas', - "shipping-ftfuel": 'oil', + "shipping-lch4": "gas", + "shipping-ftfuel": "oil", } import_costs = pd.read_csv(snakemake.input.import_costs, delimiter=";") @@ -3182,23 +3197,31 @@ def add_import_options( import_nodes[k] = import_nodes[v] ports[k] = ports.get(v) - regionalised_options = {"hvdc-to-elec", "pipeline-h2", "shipping-lh2", "shipping-lch4"} + regionalised_options = { + "hvdc-to-elec", + "pipeline-h2", + "shipping-lh2", + "shipping-lch4", + } - if endogenous_hvdc and 'hvdc-to-elec' in import_options: - import_options = [o for o in import_options if o != 'hvdc-to-elec'] + if endogenous_hvdc and "hvdc-to-elec" in import_options: + import_options = [o for o in import_options if o != "hvdc-to-elec"] add_endogenous_hvdc_import_options(n) for tech in set(import_options).intersection(regionalised_options): - - import_costs_tech = import_costs.query("esc == @tech").groupby('importer').marginal_cost.min() + import_costs_tech = ( + import_costs.query("esc == @tech").groupby("importer").marginal_cost.min() + ) sel = ~import_nodes[tech].isna() - if tech == 'pipeline-h2': + if tech == "pipeline-h2": forbidden_pipelines = ["DE", "BE", "FR", "GB"] sel &= ~import_nodes.index.str[:2].isin(forbidden_pipelines) import_nodes_tech = import_nodes.loc[sel, [tech]] - import_nodes_tech = import_nodes_tech.rename(columns={tech: "p_nom"}) * capacity_boost + import_nodes_tech = ( + import_nodes_tech.rename(columns={tech: "p_nom"}) * capacity_boost + ) marginal_costs = ports[tech].dropna().map(import_costs_tech) import_nodes_tech["marginal_cost"] = marginal_costs @@ -3208,15 +3231,12 @@ def add_import_options( suffix = bus_suffix[tech] if tech in co2_intensity.keys(): - buses = import_nodes_tech.index + f"{suffix} import {tech}" - n.madd("Bus", - buses + " bus", - carrier=f"import {tech}" - ) + n.madd("Bus", buses + " bus", carrier=f"import {tech}") - n.madd("Store", + n.madd( + "Store", buses + " store", bus=buses + " bus", e_nom_extendable=True, @@ -3226,21 +3246,21 @@ def add_import_options( e_max_pu=0, ) - n.madd("Link", + n.madd( + "Link", buses, bus0=buses + " bus", bus1=import_nodes_tech.index + suffix, bus2="co2 atmosphere", carrier=f"import {tech}", - efficiency2=-costs.at[co2_intensity[tech], 'CO2 intensity'], + efficiency2=-costs.at[co2_intensity[tech], "CO2 intensity"], marginal_cost=import_nodes_tech.marginal_cost.values, p_nom=import_nodes_tech.p_nom.values, ) else: - location = import_nodes_tech.index - buses = location if tech == 'hvdc-to-elec' else location + suffix + buses = location if tech == "hvdc-to-elec" else location + suffix n.madd( "Generator", @@ -3253,17 +3273,16 @@ def add_import_options( # need special handling for copperplated Fischer-Tropsch imports if "shipping-ftfuel" in import_options: + marginal_costs = import_costs.query( + "esc == 'shipping-ftfuel'" + ).marginal_cost.min() - marginal_costs = import_costs.query("esc == 'shipping-ftfuel'").marginal_cost.min() - - n.add("Bus", - "EU import shipping-ftfuel bus", - carrier="import shipping-ftfuel" - ) + n.add("Bus", "EU import shipping-ftfuel bus", carrier="import shipping-ftfuel") - n.add("Store", + n.add( + "Store", "EU import shipping-ftfuel store", - bus='EU import shipping-ftfuel bus', + bus="EU import shipping-ftfuel bus", e_nom_extendable=True, e_nom_min=-np.inf, e_nom_max=0, @@ -3271,22 +3290,28 @@ def add_import_options( e_max_pu=0, ) - n.add("Link", + n.add( + "Link", "EU import shipping-ftfuel", bus0="EU import shipping-ftfuel bus", bus1="EU oil", bus2="co2 atmosphere", carrier="import shipping-ftfuel", - efficiency2=-costs.at["oil", 'CO2 intensity'], + efficiency2=-costs.at["oil", "CO2 intensity"], marginal_cost=marginal_costs, p_nom=1e7, ) - if "shipping-lnh3" in import_options and "shipping-lnh3" not in regionalised_options: - - marginal_costs = import_costs.query("esc == 'shipping-lnh3'").marginal_cost.min() + if ( + "shipping-lnh3" in import_options + and "shipping-lnh3" not in regionalised_options + ): + marginal_costs = import_costs.query( + "esc == 'shipping-lnh3'" + ).marginal_cost.min() - n.add("Generator", + n.add( + "Generator", "EU import shipping-lnh3", bus="EU NH3", carrier="import shipping-lnh3", @@ -3298,7 +3323,8 @@ def add_import_options( def maybe_adjust_costs_and_potentials(n, opts): for o in opts: flags = ["+e", "+p", "+m"] - if all(flag not in o for flag in flags): continue + if all(flag not in o for flag in flags): + continue oo = o.split("+") carrier_list = np.hstack( ( @@ -3653,19 +3679,21 @@ def set_temporal_aggregation(n, opts, solver_name): FT=["shipping-ftfuel"], ) for o in opts: - if not o.startswith("imp"): continue + if not o.startswith("imp"): + continue subsets = o.split("+")[1:] if len(subsets): carriers = sum([translate[s] for s in subsets], []) else: carriers = options["import"]["options"] - add_import_options(n, + add_import_options( + n, capacity_boost=options["import"]["capacity_boost"], import_options=carriers, - endogenous_hvdc=options["import"]["endogenous_hvdc_import"]["enable"] + endogenous_hvdc=options["import"]["endogenous_hvdc_import"]["enable"], ) break - + if options["allam_cycle"]: add_allam(n, costs) @@ -3718,9 +3746,11 @@ def set_temporal_aggregation(n, opts, solver_name): # Workaround: Remove lines with conflicting (and unrealistic) properties # cf. https://github.com/PyPSA/pypsa-eur/issues/444 - if options['electricity_grid_transmission_losses']: + if options["electricity_grid_transmission_losses"]: idx = n.lines.query("num_parallel == 0").index - logger.info(f"Removing {len(idx)} line(s) with properties conflicting with transmission losses functionality.") + logger.info( + f"Removing {len(idx)} line(s) with properties conflicting with transmission losses functionality." + ) n.mremove("Line", idx) first_year_myopic = (snakemake.config["foresight"] == "myopic") and ( diff --git a/scripts/solve_network.py b/scripts/solve_network.py index 6588c8155..d69c648ba 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -565,19 +565,21 @@ def add_pipe_retrofit_constraint(n): def add_energy_import_limit(n, sns): - import_gens = n.generators.loc[n.generators.carrier.str.contains("import")].index import_links = n.links.loc[n.links.carrier.str.contains("import")].index - limit = n.config["sector"].get('import', {}).get('limit', False) - limit_sense = n.config["sector"].get('import', {}).get('limit_sense', '<=') + limit = n.config["sector"].get("import", {}).get("limit", False) + limit_sense = n.config["sector"].get("import", {}).get("limit_sense", "<=") for o in n.opts: - if not o.startswith("imp"): continue + if not o.startswith("imp"): + continue match = o.split("+")[0][3:] - if match: limit = float(match) + if match: + limit = float(match) break - if (import_gens.empty and import_links.empty) or not limit: return + if (import_gens.empty and import_links.empty) or not limit: + return weightings = n.snapshot_weightings.loc[sns] p_gens = get_var(n, "Generator", "p")[import_gens] @@ -585,9 +587,17 @@ def add_energy_import_limit(n, sns): p = pd.concat([p_gens, p_links], axis=1) lhs = linexpr((weightings.generators, p.T)).sum().sum() - name = 'energy_import_limit' - define_constraints(n, lhs, limit_sense, limit * 1e6, 'GlobalConstraint', - 'mu', axes=pd.Index([name]), spec=name) + name = "energy_import_limit" + define_constraints( + n, + lhs, + limit_sense, + limit * 1e6, + "GlobalConstraint", + "mu", + axes=pd.Index([name]), + spec=name, + ) def extra_functionality(n, snapshots): @@ -642,7 +652,9 @@ def solve_network(n, config, opts="", **kwargs): status, condition = n.optimize( solver_name=solver_name, extra_functionality=extra_functionality, - transmission_losses=config["sector"]["electricity_grid_transmission_losses"], + transmission_losses=config["sector"][ + "electricity_grid_transmission_losses" + ], **solver_options, **kwargs, ) @@ -653,7 +665,9 @@ def solve_network(n, config, opts="", **kwargs): min_iterations=min_iterations, max_iterations=max_iterations, extra_functionality=extra_functionality, - transmission_losses=config["sector"]["electricity_grid_transmission_losses"], + transmission_losses=config["sector"][ + "electricity_grid_transmission_losses" + ], **solver_options, **kwargs, ) From 8a472874f359b8459c1834712faf3b0e5cad9d85 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 26 Apr 2023 15:59:30 +0000 Subject: [PATCH 045/293] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Snakefile | 9 +- config/config.default.yaml | 34 ++--- data/import-sites.csv | 2 +- rules/build_sector.smk | 10 +- scripts/build_gas_input_locations.py | 30 ++-- scripts/make_summary.py | 6 +- scripts/plot_summary.py | 12 +- scripts/prepare_sector_network.py | 212 +++++++++++++++------------ scripts/solve_network.py | 39 +++-- 9 files changed, 204 insertions(+), 150 deletions(-) diff --git a/Snakefile b/Snakefile index 8b2d7f6a4..681bdc96b 100644 --- a/Snakefile +++ b/Snakefile @@ -50,9 +50,12 @@ wildcard_constraints: # https://github.com/euronion/trace/commit/646d48b2e7a889594338bc376e0a6ecc5d18998f # in parallel directory to /pypsa-eur-sec subworkflow trace: - workdir: "../trace" - snakefile: "../trace/Snakefile" - configfile: "../trace/config/config.default.yaml" + workdir: + "../trace" + snakefile: + "../trace/Snakefile" + configfile: + "../trace/config/config.default.yaml" include: "rules/common.smk" diff --git a/config/config.default.yaml b/config/config.default.yaml index 9b5ff2a6c..2e5a081eb 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -518,25 +518,25 @@ sector: limit: false # bool or number in TWh limit_sense: "==" options: - - pipeline-h2 - - shipping-lh2 - - shipping-lch4 - - shipping-lnh3 - - shipping-ftfuel - - hvdc-to-elec + - pipeline-h2 + - shipping-lh2 + - shipping-lch4 + - shipping-lnh3 + - shipping-ftfuel + - hvdc-to-elec endogenous_hvdc_import: enable: false exporters: - - CN-West - - DZ - - EG - - KZ - - LY - - MA - - SA - - TN - - TR - - UA + - CN-West + - DZ + - EG + - KZ + - LY + - MA + - SA + - TN + - TR + - UA hvdc_losses: 3.e-5 # p.u./km length_factor: 1.25 distance_threshold: 0.05 # quantile @@ -995,4 +995,4 @@ plotting: external battery: '#dff7cb' external offwind: '#d0e8f5' external onwind: '#accbfc' - external solar: '#fcf1c7' \ No newline at end of file + external solar: '#fcf1c7' diff --git a/data/import-sites.csv b/data/import-sites.csv index 4d71e37dc..329fc137e 100644 --- a/data/import-sites.csv +++ b/data/import-sites.csv @@ -10,4 +10,4 @@ ESVLC,39.6329,-0.2152,https://www.gem.wiki/Sagunto_LNG_Terminal,lng, GBMLF,51.7152,-7,https://www.gem.wiki/South_Hook_LNG_Terminal,lng,"center coordinate moved west from -5.07627" GREEV,37.96,23.4023,https://www.gem.wiki/Revithoussa_LNG_Terminal,lng, ITVCE,45.091667,12.586111,https://www.gem.wiki/Adriatic_LNG_Terminal,lng, -PLSWI,53.909167,17,https://www.gem.wiki/%C5%9Awinouj%C5%9Bcie_Polskie_LNG_Terminal,lng,"center coordinate moved east from 14.294722" \ No newline at end of file +PLSWI,53.909167,17,https://www.gem.wiki/%C5%9Awinouj%C5%9Bcie_Polskie_LNG_Terminal,lng,"center coordinate moved east from 14.294722" diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 662445c9a..c1eceb2e0 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -95,8 +95,7 @@ if config["sector"]["gas_network"] or config["sector"]["H2_retrofit"]: + "regions_onshore_elec_s{simpl}_{clusters}.geojson", regions_offshore=RESOURCES + "regions_offshore_elec_s{simpl}_{clusters}.geojson", - europe_shape=RESOURCES - + "europe_shape.geojson", + europe_shape=RESOURCES + "europe_shape.geojson", reference_import_sites="data/import-sites.csv", output: gas_input_nodes=RESOURCES @@ -730,10 +729,9 @@ rule prepare_sector_network: + "solar_thermal_rural_elec_s{simpl}_{clusters}.nc" if config["sector"]["solar_thermal"] else [], - import_costs=trace("results/result.csv"), # TODO: host file on zenodo or elsewhere - import_p_max_pu=trace("results/combined_weighted_generator_timeseries.nc"), # TODO: host file on zenodo or elsewhere - regions_onshore=RESOURCES - + "regions_onshore_elec_s{simpl}_{clusters}.geojson", + import_costs=trace("results/result.csv"), # TODO: host file on zenodo or elsewhere + import_p_max_pu=trace("results/combined_weighted_generator_timeseries.nc"), # TODO: host file on zenodo or elsewhere + regions_onshore=RESOURCES + "regions_onshore_elec_s{simpl}_{clusters}.geojson", output: RESULTS + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", diff --git a/scripts/build_gas_input_locations.py b/scripts/build_gas_input_locations.py index 75a04a40f..1228f9402 100644 --- a/scripts/build_gas_input_locations.py +++ b/scripts/build_gas_input_locations.py @@ -11,11 +11,10 @@ logger = logging.getLogger(__name__) -import pandas as pd import geopandas as gpd -from vresutils.graph import voronoi_partition_pts - +import pandas as pd from cluster_gas_network import load_bus_regions +from vresutils.graph import voronoi_partition_pts def read_scigrid_gas(fn): @@ -79,19 +78,21 @@ def build_gas_input_locations(lng_fn, entry_fn, prod_fn, countries): def assign_reference_import_sites(gas_input_locations, import_sites, europe_shape): - - europe_shape = europe_shape.squeeze().geometry.buffer(1) # 1 latlon degree + europe_shape = europe_shape.squeeze().geometry.buffer(1) # 1 latlon degree for kind in ["lng", "pipeline"]: - locs = import_sites.query("type == @kind") partition = voronoi_partition_pts(locs[["x", "y"]].values, europe_shape) partition = gpd.GeoDataFrame(dict(name=locs.index, geometry=partition)) - partition = partition.set_crs(4326).set_index('name') + partition = partition.set_crs(4326).set_index("name") - match = gpd.sjoin(gas_input_locations.query("type == @kind"), partition, how='left') - gas_input_locations.loc[gas_input_locations["type"] == kind, "port"] = match["index_right"] + match = gpd.sjoin( + gas_input_locations.query("type == @kind"), partition, how="left" + ) + gas_input_locations.loc[gas_input_locations["type"] == kind, "port"] = match[ + "index_right" + ] return gas_input_locations @@ -133,7 +134,9 @@ def assign_reference_import_sites(gas_input_locations, import_sites, europe_shap countries, ) - gas_input_locations = assign_reference_import_sites(gas_input_locations, import_sites, europe_shape) + gas_input_locations = assign_reference_import_sites( + gas_input_locations, import_sites, europe_shape + ) gas_input_nodes = gpd.sjoin(gas_input_locations, regions, how="left") @@ -148,7 +151,12 @@ def assign_reference_import_sites(gas_input_locations, import_sites, europe_shap gas_input_nodes_s.to_csv(snakemake.output.gas_input_nodes_simplified) - ports = gas_input_nodes.groupby(["bus", "type"])["port"].first().unstack().drop("production", axis=1) + ports = ( + gas_input_nodes.groupby(["bus", "type"])["port"] + .first() + .unstack() + .drop("production", axis=1) + ) ports.columns.name = "port" ports.to_csv(snakemake.output.ports) diff --git a/scripts/make_summary.py b/scripts/make_summary.py index bee090d82..456bd6d1a 100644 --- a/scripts/make_summary.py +++ b/scripts/make_summary.py @@ -514,7 +514,11 @@ def calculate_weighted_prices(n, label, weighted_prices): columns=lambda i: str(i) + suffix ) else: - load = n.loads_t.p_set[buses.intersection(n.loads_t.p_set.columns)].reindex(columns=buses).fillna(0.) + load = ( + n.loads_t.p_set[buses.intersection(n.loads_t.p_set.columns)] + .reindex(columns=buses) + .fillna(0.0) + ) for tech in link_loads[carrier]: names = n.links.index[n.links.index.to_series().str[-len(tech) :] == tech] diff --git a/scripts/plot_summary.py b/scripts/plot_summary.py index 00a244bfa..d48772aa9 100644 --- a/scripts/plot_summary.py +++ b/scripts/plot_summary.py @@ -282,11 +282,15 @@ def plot_balances(): df = df / 1e6 # remove trailing link ports - forbidden = ["co2", "import shipping-lh2", "import shipping-lnh3", "import pipeline-h2", "NH3"] + forbidden = [ + "co2", + "import shipping-lh2", + "import shipping-lnh3", + "import pipeline-h2", + "NH3", + ] df.index = [ - i[:-1] - if ((i not in forbidden) and (i[-1:] in ["0", "1", "2", "3"])) - else i + i[:-1] if ((i not in forbidden) and (i[-1:] in ["0", "1", "2", "3"])) else i for i in df.index ] diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 3c78bbcbe..95753e0d2 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -12,10 +12,10 @@ import re from itertools import product +import geopandas as gpd import networkx as nx import numpy as np import pandas as pd -import geopandas as gpd import pypsa import xarray as xr from _helpers import ( @@ -24,6 +24,7 @@ update_config_with_sector_opts, ) from build_energy_totals import build_co2_totals, build_eea_co2, build_eurostat_co2 +from geopy.geocoders import Nominatim from networkx.algorithms import complement from networkx.algorithms.connectivity.edge_augmentation import k_edge_augmentation from pypsa.geo import haversine_pts @@ -31,7 +32,6 @@ from scipy.stats import beta from vresutils.costdata import annuity -from geopy.geocoders import Nominatim geolocator = Nominatim(user_agent="locate-exporting-region", timeout=10) logger = logging.getLogger(__name__) @@ -2995,20 +2995,24 @@ def remove_h2_network(n): def add_endogenous_hvdc_import_options(n): - logger.info("Add import options: endogenous hvdc-to-elec") cf = snakemake.config["sector"]["import"].get("endogenous_hvdc_import", {}) - if not cf["enable"]: return + if not cf["enable"]: + return - regions = gpd.read_file(snakemake.input.regions_onshore).set_index('name') + regions = gpd.read_file(snakemake.input.regions_onshore).set_index("name") - p_max_pu = xr.open_dataset(snakemake.input.import_p_max_pu).p_max_pu.sel(importer='EUE') + p_max_pu = xr.open_dataset(snakemake.input.import_p_max_pu).p_max_pu.sel( + importer="EUE" + ) def _coordinates(ct): - loc = geolocator.geocode(ct.split('-')[0]) + loc = geolocator.geocode(ct.split("-")[0]) return [loc.longitude, loc.latitude] - exporters = pd.DataFrame({ct: _coordinates(ct) for ct in cf["exporters"]}, index=['x', 'y']).T + exporters = pd.DataFrame( + {ct: _coordinates(ct) for ct in cf["exporters"]}, index=["x", "y"] + ).T geometry = gpd.points_from_xy(exporters.x, exporters.y) exporters = gpd.GeoDataFrame(exporters, geometry=geometry, crs=4326) @@ -3017,20 +3021,23 @@ def _coordinates(ct): for ct in exporters.index: b = exporters.to_crs(3857).loc[ct].geometry d = a.distance(b) - import_links[ct] = d.where(d < d.quantile(cf["distance_threshold"])).div(1e3).dropna() # km + import_links[ct] = ( + d.where(d < d.quantile(cf["distance_threshold"])).div(1e3).dropna() + ) # km import_links = pd.concat(import_links) hvdc_cost = ( - import_links.values * cf["length_factor"] * costs.at['HVDC submarine', 'fixed'] + - costs.at['HVDC inverter pair', 'fixed'] + import_links.values * cf["length_factor"] * costs.at["HVDC submarine", "fixed"] + + costs.at["HVDC inverter pair", "fixed"] ) buses_i = exporters.index - n.madd("Bus", buses_i, **exporters.drop('geometry', axis=1)) + n.madd("Bus", buses_i, **exporters.drop("geometry", axis=1)) - n.madd("Link", - ["import hvdc-to-elec " + ' '.join(idx).strip() for idx in import_links.index], + n.madd( + "Link", + ["import hvdc-to-elec " + " ".join(idx).strip() for idx in import_links.index], bus0=import_links.index.get_level_values(0), bus1=import_links.index.get_level_values(1), carrier="import hvdc-to-elec", @@ -3042,12 +3049,12 @@ def _coordinates(ct): ) for tech in ["solar-utility", "onwind", "offwind"]: - p_max_pu_tech = p_max_pu.sel(technology=tech).to_pandas().dropna().T exporters_tech_i = exporters.index.intersection(p_max_pu_tech.columns) - n.madd("Generator", + n.madd( + "Generator", exporters_tech_i, suffix=" " + tech, bus=exporters_tech_i, @@ -3060,89 +3067,97 @@ def _coordinates(ct): # hydrogen storage - h2_buses_i = n.madd("Bus", - buses_i, - suffix=" H2", - carrier="external H2", - location=buses_i + h2_buses_i = n.madd( + "Bus", buses_i, suffix=" H2", carrier="external H2", location=buses_i ) - n.madd("Store", + n.madd( + "Store", h2_buses_i, bus=h2_buses_i, - carrier='external H2', + carrier="external H2", e_nom_extendable=True, e_cyclic=True, - capital_cost=costs.at["hydrogen storage tank incl. compressor", "fixed"] + capital_cost=costs.at["hydrogen storage tank incl. compressor", "fixed"], ) - n.madd("Link", + n.madd( + "Link", h2_buses_i + " Electrolysis", bus0=buses_i, bus1=h2_buses_i, - carrier='external H2 Electrolysis', + carrier="external H2 Electrolysis", p_nom_extendable=True, efficiency=costs.at["electrolysis", "efficiency"], capital_cost=costs.at["electrolysis", "fixed"], - lifetime=costs.at["electrolysis", "lifetime"] + lifetime=costs.at["electrolysis", "lifetime"], ) - n.madd("Link", + n.madd( + "Link", h2_buses_i + " Fuel Cell", bus0=h2_buses_i, bus1=buses_i, - carrier='external H2 Fuel Cell', + carrier="external H2 Fuel Cell", p_nom_extendable=True, efficiency=costs.at["fuel cell", "efficiency"], - capital_cost=costs.at["fuel cell", "fixed"] * costs.at["fuel cell", "efficiency"], - lifetime=costs.at["fuel cell", "lifetime"] + capital_cost=costs.at["fuel cell", "fixed"] + * costs.at["fuel cell", "efficiency"], + lifetime=costs.at["fuel cell", "lifetime"], ) # battery storage - b_buses_i = n.madd("Bus", - buses_i, - suffix=" battery", - carrier="external battery", - location=buses_i + b_buses_i = n.madd( + "Bus", buses_i, suffix=" battery", carrier="external battery", location=buses_i ) - n.madd("Store", + n.madd( + "Store", b_buses_i, bus=b_buses_i, - carrier='external battery', + carrier="external battery", e_cyclic=True, e_nom_extendable=True, - capital_cost=costs.at['battery storage', 'fixed'], - lifetime=costs.at['battery storage', 'lifetime'] + capital_cost=costs.at["battery storage", "fixed"], + lifetime=costs.at["battery storage", "lifetime"], ) - n.madd("Link", + n.madd( + "Link", b_buses_i + " charger", bus0=buses_i, bus1=b_buses_i, - carrier='external battery charger', - efficiency=costs.at['battery inverter', 'efficiency']**0.5, - capital_cost=costs.at['battery inverter', 'fixed'], + carrier="external battery charger", + efficiency=costs.at["battery inverter", "efficiency"] ** 0.5, + capital_cost=costs.at["battery inverter", "fixed"], p_nom_extendable=True, - lifetime=costs.at['battery inverter', 'lifetime'] + lifetime=costs.at["battery inverter", "lifetime"], ) - n.madd("Link", + n.madd( + "Link", b_buses_i + " discharger", bus0=b_buses_i, bus1=buses_i, - carrier='external battery discharger', - efficiency=costs.at['battery inverter','efficiency']**0.5, + carrier="external battery discharger", + efficiency=costs.at["battery inverter", "efficiency"] ** 0.5, p_nom_extendable=True, - lifetime=costs.at['battery inverter', 'lifetime'] + lifetime=costs.at["battery inverter", "lifetime"], ) def add_import_options( n, - capacity_boost=3., - import_options=["hvdc-to-elec", "pipeline-h2", "shipping-lh2", "shipping-lch4", "shipping-ftfuel", "shipping-lnh3"], + capacity_boost=3.0, + import_options=[ + "hvdc-to-elec", + "pipeline-h2", + "shipping-lh2", + "shipping-lch4", + "shipping-ftfuel", + "shipping-lnh3", + ], endogenous_hvdc=False, ): logger.info("Add import options: " + " ".join(import_options)) @@ -3169,8 +3184,8 @@ def add_import_options( } co2_intensity = { - "shipping-lch4": 'gas', - "shipping-ftfuel": 'oil', + "shipping-lch4": "gas", + "shipping-ftfuel": "oil", } import_costs = pd.read_csv(snakemake.input.import_costs, delimiter=";") @@ -3182,23 +3197,31 @@ def add_import_options( import_nodes[k] = import_nodes[v] ports[k] = ports.get(v) - regionalised_options = {"hvdc-to-elec", "pipeline-h2", "shipping-lh2", "shipping-lch4"} + regionalised_options = { + "hvdc-to-elec", + "pipeline-h2", + "shipping-lh2", + "shipping-lch4", + } - if endogenous_hvdc and 'hvdc-to-elec' in import_options: - import_options = [o for o in import_options if o != 'hvdc-to-elec'] + if endogenous_hvdc and "hvdc-to-elec" in import_options: + import_options = [o for o in import_options if o != "hvdc-to-elec"] add_endogenous_hvdc_import_options(n) for tech in set(import_options).intersection(regionalised_options): - - import_costs_tech = import_costs.query("esc == @tech").groupby('importer').marginal_cost.min() + import_costs_tech = ( + import_costs.query("esc == @tech").groupby("importer").marginal_cost.min() + ) sel = ~import_nodes[tech].isna() - if tech == 'pipeline-h2': + if tech == "pipeline-h2": forbidden_pipelines = ["DE", "BE", "FR", "GB"] sel &= ~import_nodes.index.str[:2].isin(forbidden_pipelines) import_nodes_tech = import_nodes.loc[sel, [tech]] - import_nodes_tech = import_nodes_tech.rename(columns={tech: "p_nom"}) * capacity_boost + import_nodes_tech = ( + import_nodes_tech.rename(columns={tech: "p_nom"}) * capacity_boost + ) marginal_costs = ports[tech].dropna().map(import_costs_tech) import_nodes_tech["marginal_cost"] = marginal_costs @@ -3208,15 +3231,12 @@ def add_import_options( suffix = bus_suffix[tech] if tech in co2_intensity.keys(): - buses = import_nodes_tech.index + f"{suffix} import {tech}" - n.madd("Bus", - buses + " bus", - carrier=f"import {tech}" - ) + n.madd("Bus", buses + " bus", carrier=f"import {tech}") - n.madd("Store", + n.madd( + "Store", buses + " store", bus=buses + " bus", e_nom_extendable=True, @@ -3226,21 +3246,21 @@ def add_import_options( e_max_pu=0, ) - n.madd("Link", + n.madd( + "Link", buses, bus0=buses + " bus", bus1=import_nodes_tech.index + suffix, bus2="co2 atmosphere", carrier=f"import {tech}", - efficiency2=-costs.at[co2_intensity[tech], 'CO2 intensity'], + efficiency2=-costs.at[co2_intensity[tech], "CO2 intensity"], marginal_cost=import_nodes_tech.marginal_cost.values, p_nom=import_nodes_tech.p_nom.values, ) else: - location = import_nodes_tech.index - buses = location if tech == 'hvdc-to-elec' else location + suffix + buses = location if tech == "hvdc-to-elec" else location + suffix n.madd( "Generator", @@ -3253,17 +3273,16 @@ def add_import_options( # need special handling for copperplated Fischer-Tropsch imports if "shipping-ftfuel" in import_options: + marginal_costs = import_costs.query( + "esc == 'shipping-ftfuel'" + ).marginal_cost.min() - marginal_costs = import_costs.query("esc == 'shipping-ftfuel'").marginal_cost.min() - - n.add("Bus", - "EU import shipping-ftfuel bus", - carrier="import shipping-ftfuel" - ) + n.add("Bus", "EU import shipping-ftfuel bus", carrier="import shipping-ftfuel") - n.add("Store", + n.add( + "Store", "EU import shipping-ftfuel store", - bus='EU import shipping-ftfuel bus', + bus="EU import shipping-ftfuel bus", e_nom_extendable=True, e_nom_min=-np.inf, e_nom_max=0, @@ -3271,22 +3290,28 @@ def add_import_options( e_max_pu=0, ) - n.add("Link", + n.add( + "Link", "EU import shipping-ftfuel", bus0="EU import shipping-ftfuel bus", bus1="EU oil", bus2="co2 atmosphere", carrier="import shipping-ftfuel", - efficiency2=-costs.at["oil", 'CO2 intensity'], + efficiency2=-costs.at["oil", "CO2 intensity"], marginal_cost=marginal_costs, p_nom=1e7, ) - if "shipping-lnh3" in import_options and "shipping-lnh3" not in regionalised_options: - - marginal_costs = import_costs.query("esc == 'shipping-lnh3'").marginal_cost.min() + if ( + "shipping-lnh3" in import_options + and "shipping-lnh3" not in regionalised_options + ): + marginal_costs = import_costs.query( + "esc == 'shipping-lnh3'" + ).marginal_cost.min() - n.add("Generator", + n.add( + "Generator", "EU import shipping-lnh3", bus="EU NH3", carrier="import shipping-lnh3", @@ -3298,7 +3323,8 @@ def add_import_options( def maybe_adjust_costs_and_potentials(n, opts): for o in opts: flags = ["+e", "+p", "+m"] - if all(flag not in o for flag in flags): continue + if all(flag not in o for flag in flags): + continue oo = o.split("+") carrier_list = np.hstack( ( @@ -3653,19 +3679,21 @@ def set_temporal_aggregation(n, opts, solver_name): FT=["shipping-ftfuel"], ) for o in opts: - if not o.startswith("imp"): continue + if not o.startswith("imp"): + continue subsets = o.split("+")[1:] if len(subsets): carriers = sum([translate[s] for s in subsets], []) else: carriers = options["import"]["options"] - add_import_options(n, + add_import_options( + n, capacity_boost=options["import"]["capacity_boost"], import_options=carriers, - endogenous_hvdc=options["import"]["endogenous_hvdc_import"]["enable"] + endogenous_hvdc=options["import"]["endogenous_hvdc_import"]["enable"], ) break - + if options["allam_cycle"]: add_allam(n, costs) @@ -3718,9 +3746,11 @@ def set_temporal_aggregation(n, opts, solver_name): # Workaround: Remove lines with conflicting (and unrealistic) properties # cf. https://github.com/PyPSA/pypsa-eur/issues/444 - if options['electricity_grid_transmission_losses']: + if options["electricity_grid_transmission_losses"]: idx = n.lines.query("num_parallel == 0").index - logger.info(f"Removing {len(idx)} line(s) with properties conflicting with transmission losses functionality.") + logger.info( + f"Removing {len(idx)} line(s) with properties conflicting with transmission losses functionality." + ) n.mremove("Line", idx) first_year_myopic = (snakemake.config["foresight"] == "myopic") and ( diff --git a/scripts/solve_network.py b/scripts/solve_network.py index 6588c8155..f59141505 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -565,29 +565,32 @@ def add_pipe_retrofit_constraint(n): def add_energy_import_limit(n, sns): - import_gens = n.generators.loc[n.generators.carrier.str.contains("import")].index import_links = n.links.loc[n.links.carrier.str.contains("import")].index - limit = n.config["sector"].get('import', {}).get('limit', False) - limit_sense = n.config["sector"].get('import', {}).get('limit_sense', '<=') + limit = n.config["sector"].get("import", {}).get("limit", False) + limit_sense = n.config["sector"].get("import", {}).get("limit_sense", "<=") for o in n.opts: - if not o.startswith("imp"): continue + if not o.startswith("imp"): + continue match = o.split("+")[0][3:] - if match: limit = float(match) + if match: + limit = float(match) break - if (import_gens.empty and import_links.empty) or not limit: return + if (import_gens.empty and import_links.empty) or not limit: + return + + weightings = n.snapshot_weightings.loc[sns, "generators"] + + p_gens = n.model["Generator-p"].loc[sns, import_gens] + p_links = n.model["Link-p"].loc[sns, import_links] + + lhs = (p_gens * weightings).sum() + (p_links * weightings).sum() - weightings = n.snapshot_weightings.loc[sns] - p_gens = get_var(n, "Generator", "p")[import_gens] - p_links = get_var(n, "Link", "p")[import_links] - p = pd.concat([p_gens, p_links], axis=1) - lhs = linexpr((weightings.generators, p.T)).sum().sum() + rhs = limit * 1e6 - name = 'energy_import_limit' - define_constraints(n, lhs, limit_sense, limit * 1e6, 'GlobalConstraint', - 'mu', axes=pd.Index([name]), spec=name) + n.model.add_constraints(lhs, limit_sense, rhs, name="energy_import_limit") def extra_functionality(n, snapshots): @@ -642,7 +645,9 @@ def solve_network(n, config, opts="", **kwargs): status, condition = n.optimize( solver_name=solver_name, extra_functionality=extra_functionality, - transmission_losses=config["sector"]["electricity_grid_transmission_losses"], + transmission_losses=config["sector"][ + "electricity_grid_transmission_losses" + ], **solver_options, **kwargs, ) @@ -653,7 +658,9 @@ def solve_network(n, config, opts="", **kwargs): min_iterations=min_iterations, max_iterations=max_iterations, extra_functionality=extra_functionality, - transmission_losses=config["sector"]["electricity_grid_transmission_losses"], + transmission_losses=config["sector"][ + "electricity_grid_transmission_losses" + ], **solver_options, **kwargs, ) From aa84617fafd79dacb3818c5f67b25d112d209887 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 12 May 2023 10:27:29 +0200 Subject: [PATCH 046/293] remove trace subworkflow --- Snakefile | 14 +------------- rules/build_sector.smk | 4 ++-- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/Snakefile b/Snakefile index 681bdc96b..80e7a53be 100644 --- a/Snakefile +++ b/Snakefile @@ -14,7 +14,7 @@ from snakemake.utils import min_version min_version("7.7") -if not exists("config/config.yaml"): +if not exists("config/config.yaml") and exists("config/config.default.yaml"): copyfile("config/config.default.yaml", "config/config.yaml") @@ -46,18 +46,6 @@ wildcard_constraints: sector_opts="[-+a-zA-Z0-9\.\s]*", -# Requires clone of this version of trace -# https://github.com/euronion/trace/commit/646d48b2e7a889594338bc376e0a6ecc5d18998f -# in parallel directory to /pypsa-eur-sec -subworkflow trace: - workdir: - "../trace" - snakefile: - "../trace/Snakefile" - configfile: - "../trace/config/config.default.yaml" - - include: "rules/common.smk" include: "rules/collect.smk" include: "rules/retrieve.smk" diff --git a/rules/build_sector.smk b/rules/build_sector.smk index c1eceb2e0..ee572a12b 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -729,8 +729,8 @@ rule prepare_sector_network: + "solar_thermal_rural_elec_s{simpl}_{clusters}.nc" if config["sector"]["solar_thermal"] else [], - import_costs=trace("results/result.csv"), # TODO: host file on zenodo or elsewhere - import_p_max_pu=trace("results/combined_weighted_generator_timeseries.nc"), # TODO: host file on zenodo or elsewhere + import_costs="data/imports/results.csv", # TODO: host file on zenodo or elsewhere + import_p_max_pu="data/imports/hvdc-to-elec_aggregated-time-series_p-max-pu_.nc", # TODO: host file on zenodo or elsewhere regions_onshore=RESOURCES + "regions_onshore_elec_s{simpl}_{clusters}.geojson", output: RESULTS From c435f39365a3cc85e1e04670fef66401fc064d3a Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 12 May 2023 10:33:53 +0200 Subject: [PATCH 047/293] update file paths --- 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 ee572a12b..370c48ea7 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -730,7 +730,7 @@ rule prepare_sector_network: if config["sector"]["solar_thermal"] else [], import_costs="data/imports/results.csv", # TODO: host file on zenodo or elsewhere - import_p_max_pu="data/imports/hvdc-to-elec_aggregated-time-series_p-max-pu_.nc", # TODO: host file on zenodo or elsewhere + import_p_max_pu="data/imports/combined_weighted_generator_timeseries.nc", # TODO: host file on zenodo or elsewhere regions_onshore=RESOURCES + "regions_onshore_elec_s{simpl}_{clusters}.geojson", output: RESULTS From 9b8e86786cf8d540b050ad678aa224a537f42db3 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 12 May 2023 14:51:47 +0200 Subject: [PATCH 048/293] from build_bus_regions import voronoi_partition_pts --- scripts/build_gas_input_locations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build_gas_input_locations.py b/scripts/build_gas_input_locations.py index 1228f9402..3c97736b3 100644 --- a/scripts/build_gas_input_locations.py +++ b/scripts/build_gas_input_locations.py @@ -14,7 +14,7 @@ import geopandas as gpd import pandas as pd from cluster_gas_network import load_bus_regions -from vresutils.graph import voronoi_partition_pts +from build_bus_regions import voronoi_partition_pts def read_scigrid_gas(fn): From 683e9e489a4e3558b50b159ff6e5574bde6928bd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 12 May 2023 12:52:14 +0000 Subject: [PATCH 049/293] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/add_electricity.py | 4 +++- scripts/build_gas_input_locations.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/add_electricity.py b/scripts/add_electricity.py index 85ab35e2f..4afa3e22c 100755 --- a/scripts/add_electricity.py +++ b/scripts/add_electricity.py @@ -418,7 +418,9 @@ def attach_conventional_generators( if f"conventional_{carrier}_{attr}" in conventional_inputs: # Values affecting generators of technology k country-specific # First map generator buses to countries; then map countries to p_max_pu - values = pd.read_csv(snakemake.input[f"conventional_{carrier}_{attr}"], index_col=0).iloc[:, 0] + values = pd.read_csv( + snakemake.input[f"conventional_{carrier}_{attr}"], index_col=0 + ).iloc[:, 0] bus_values = n.buses.country.map(values) n.generators[attr].update( n.generators.loc[idx].bus.map(bus_values).dropna() diff --git a/scripts/build_gas_input_locations.py b/scripts/build_gas_input_locations.py index 3c97736b3..17b0b2da3 100644 --- a/scripts/build_gas_input_locations.py +++ b/scripts/build_gas_input_locations.py @@ -13,8 +13,8 @@ import geopandas as gpd import pandas as pd -from cluster_gas_network import load_bus_regions from build_bus_regions import voronoi_partition_pts +from cluster_gas_network import load_bus_regions def read_scigrid_gas(fn): From 716adfd129f5fd3233b677936d1b016842a60eba Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Tue, 4 Jul 2023 13:56:52 +0200 Subject: [PATCH 050/293] add MeOH imports --- scripts/prepare_sector_network.py | 55 ++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 2b2802a54..532dd933e 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3177,8 +3177,10 @@ def add_import_options( "pipeline-h2", "shipping-lh2", "shipping-lch4", + "shipping-meoh", "shipping-ftfuel", "shipping-lnh3", + # "shipping-steel", ], endogenous_hvdc=False, ): @@ -3203,11 +3205,16 @@ def add_import_options( "shipping-lh2": " H2", "shipping-lch4": " gas", "shipping-lnh3": " NH3", + "shipping-ftfuel": " oil", + "shipping-meoh": " methanol", + # "shipping-steel": " steel", } co2_intensity = { "shipping-lch4": "gas", "shipping-ftfuel": "oil", + "shipping-meoh": "methanol", # TODO: or shipping fuel methanol + # "shipping-steel": "", TODO: is this necessary? } import_costs = pd.read_csv(snakemake.input.import_costs, delimiter=";") @@ -3219,6 +3226,10 @@ def add_import_options( import_nodes[k] = import_nodes[v] ports[k] = ports.get(v) + if endogenous_hvdc and "hvdc-to-elec" in import_options: + import_options = [o for o in import_options if o != "hvdc-to-elec"] + add_endogenous_hvdc_import_options(n) + regionalised_options = { "hvdc-to-elec", "pipeline-h2", @@ -3226,10 +3237,6 @@ def add_import_options( "shipping-lch4", } - if endogenous_hvdc and "hvdc-to-elec" in import_options: - import_options = [o for o in import_options if o != "hvdc-to-elec"] - add_endogenous_hvdc_import_options(n) - for tech in set(import_options).intersection(regionalised_options): import_costs_tech = ( import_costs.query("esc == @tech").groupby("importer").marginal_cost.min() @@ -3293,18 +3300,26 @@ def add_import_options( p_nom=import_nodes_tech.p_nom.values, ) - # need special handling for copperplated Fischer-Tropsch imports - if "shipping-ftfuel" in import_options: + # need special handling for copperplated imports + copperplated_options = { + "shipping-ftfuel", + "shipping-meoh", + # "shipping-steel", + } + + for tech in set(import_options).intersection(copperplated_options): marginal_costs = import_costs.query( - "esc == 'shipping-ftfuel'" + "esc == @tech" ).marginal_cost.min() - n.add("Bus", "EU import shipping-ftfuel bus", carrier="import shipping-ftfuel") + suffix = bus_suffix[tech] + + n.add("Bus", f"EU import {tech} bus", carrier="import {tech}") n.add( "Store", - "EU import shipping-ftfuel store", - bus="EU import shipping-ftfuel bus", + "EU import {tech} store", + bus="EU import {tech} bus", e_nom_extendable=True, e_nom_min=-np.inf, e_nom_max=0, @@ -3314,12 +3329,12 @@ def add_import_options( n.add( "Link", - "EU import shipping-ftfuel", - bus0="EU import shipping-ftfuel bus", - bus1="EU oil", + "EU import {tech}", + bus0="EU import {tech} bus", + bus1="EU" + suffix, bus2="co2 atmosphere", - carrier="import shipping-ftfuel", - efficiency2=-costs.at["oil", "CO2 intensity"], + carrier="import {tech}", + efficiency2=-costs.at[co2_intensity[tech], "CO2 intensity"], marginal_cost=marginal_costs, p_nom=1e7, ) @@ -3587,13 +3602,13 @@ def set_temporal_aggregation(n, opts, solver_name): snakemake = mock_snakemake( "prepare_sector_network", - configfiles="test/config.overnight.yaml", + configfiles="../../../config/config.yaml", simpl="", opts="", - clusters="5", + clusters="128", ll="v1.5", - sector_opts="CO2L0-24H-T-H-B-I-A-solar+p3-dist1", - planning_horizons="2030", + sector_opts="CO2L0-3H-T-H-B-I-A", + planning_horizons="2050", ) logging.basicConfig(level=snakemake.config["logging"]["level"]) @@ -3698,6 +3713,8 @@ def set_temporal_aggregation(n, opts, solver_name): CH4=["shipping-lch4"], NH3=["shipping-lnh3"], FT=["shipping-ftfuel"], + MEOH=["shipping-meoh"], + # STEEL=["shipping-steel"] ) for o in opts: if not o.startswith("imp"): From e91b008182a8cb75b2105632a8a95e80b2148da7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 4 Jul 2023 12:00:03 +0000 Subject: [PATCH 051/293] [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 626ddbc16..04b39aff0 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3217,7 +3217,7 @@ def add_import_options( co2_intensity = { "shipping-lch4": "gas", "shipping-ftfuel": "oil", - "shipping-meoh": "methanol", # TODO: or shipping fuel methanol + "shipping-meoh": "methanol", # TODO: or shipping fuel methanol # "shipping-steel": "", TODO: is this necessary? } @@ -3312,9 +3312,7 @@ def add_import_options( } for tech in set(import_options).intersection(copperplated_options): - marginal_costs = import_costs.query( - "esc == @tech" - ).marginal_cost.min() + marginal_costs = import_costs.query("esc == @tech").marginal_cost.min() suffix = bus_suffix[tech] From 8a408fce29a7ce1bdb6e436d429e1b1e886eccd1 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 31 Jul 2023 10:52:37 +0200 Subject: [PATCH 052/293] gas_input: switch production data from scigrid to gem --- rules/build_sector.smk | 5 +-- scripts/build_gas_input_locations.py | 56 +++++++++++++++++++++------- 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 2950a17fb..c750f5860 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -85,12 +85,11 @@ if config["sector"]["gas_network"] or config["sector"]["H2_retrofit"]: rule build_gas_input_locations: input: - lng=HTTP.remote( - "https://globalenergymonitor.org/wp-content/uploads/2022/09/Europe-Gas-Tracker-August-2022.xlsx", + gem=HTTP.remote( + "https://globalenergymonitor.org/wp-content/uploads/2023/07/Europe-Gas-Tracker-2023-03-v3.xlsx", keep_local=True, ), entry="data/gas_network/scigrid-gas/data/IGGIELGN_BorderPoints.geojson", - production="data/gas_network/scigrid-gas/data/IGGIELGN_Productions.geojson", regions_onshore=RESOURCES + "regions_onshore_elec_s{simpl}_{clusters}.geojson", regions_offshore=RESOURCES diff --git a/scripts/build_gas_input_locations.py b/scripts/build_gas_input_locations.py index 17b0b2da3..ecb5bb5b7 100644 --- a/scripts/build_gas_input_locations.py +++ b/scripts/build_gas_input_locations.py @@ -24,11 +24,10 @@ def read_scigrid_gas(fn): return df -def build_gem_lng_data(lng_fn): - df = pd.read_excel(lng_fn[0], sheet_name="LNG terminals - data") +def build_gem_lng_data(fn): + df = pd.read_excel(fn[0], sheet_name="LNG terminals - data") df = df.set_index("ComboID") - remove_status = ["Cancelled"] remove_country = ["Cyprus", "Turkey"] remove_terminal = ["Puerto de la Luz LNG Terminal", "Gran Canaria LNG Terminal"] @@ -43,9 +42,43 @@ def build_gem_lng_data(lng_fn): return gpd.GeoDataFrame(df, geometry=geometry, crs="EPSG:4326") -def build_gas_input_locations(lng_fn, entry_fn, prod_fn, countries): +def build_gem_prod_data(fn): + df = pd.read_excel(fn[0], sheet_name="Gas extraction - main") + df = df.set_index("GEM Unit ID") + + remove_country = ["Cyprus", "Türkiye"] + remove_fuel_type = ["oil"] + + df = df.query( + "Status != 'shut in' \ + & 'Fuel type' != 'oil' \ + & Country != @remove_country \ + & ~Latitude.isna() \ + & ~Longitude.isna()" + ).copy() + + p = pd.read_excel(fn[0], sheet_name="Gas extraction - production") + p = p.set_index("GEM Unit ID") + p = p[p["Fuel description"] == 'gas' ] + + capacities = pd.DataFrame(index=df.index) + for key in ["production", "production design capacity", "reserves"]: + cap = p.loc[p["Production/reserves"] == key, "Quantity (converted)"].groupby("GEM Unit ID").sum().reindex(df.index) + # assume capacity such that 3% of reserves can be extracted per year (25% quantile) + annualization_factor = 0.03 if key == "reserves" else 1. + capacities[key] = cap * annualization_factor + + df["mcm_per_year"] = capacities["production"] \ + .combine_first(capacities["production design capacity"]) \ + .combine_first(capacities["reserves"]) + + geometry = gpd.points_from_xy(df["Longitude"], df["Latitude"]) + return gpd.GeoDataFrame(df, geometry=geometry, crs="EPSG:4326") + + +def build_gas_input_locations(gem_fn, entry_fn, countries): # LNG terminals - lng = build_gem_lng_data(lng_fn) + lng = build_gem_lng_data(gem_fn) # Entry points from outside the model scope entry = read_scigrid_gas(entry_fn) @@ -57,16 +90,14 @@ def build_gas_input_locations(lng_fn, entry_fn, prod_fn, countries): ] # production sites inside the model scope - prod = read_scigrid_gas(prod_fn) - prod = prod.loc[ - (prod.geometry.y > 35) & (prod.geometry.x < 30) & (prod.country_code != "DE") - ] + prod = build_gem_prod_data(gem_fn) mcm_per_day_to_mw = 437.5 # MCM/day to MWh/h + mcm_per_year_to_mw = 1.199 # MCM/year to MWh/h mtpa_to_mw = 1649.224 # mtpa to MWh/h lng["p_nom"] = lng["CapacityInMtpa"] * mtpa_to_mw entry["p_nom"] = entry["max_cap_from_to_M_m3_per_d"] * mcm_per_day_to_mw - prod["p_nom"] = prod["max_supply_M_m3_per_d"] * mcm_per_day_to_mw + prod["p_nom"] = prod["mcm_per_year"] * mcm_per_year_to_mw lng["type"] = "lng" entry["type"] = "pipeline" @@ -104,7 +135,7 @@ def assign_reference_import_sites(gas_input_locations, import_sites, europe_shap snakemake = mock_snakemake( "build_gas_input_locations", simpl="", - clusters="37", + clusters="128", ) logging.basicConfig(level=snakemake.config["logging"]["level"]) @@ -128,9 +159,8 @@ def assign_reference_import_sites(gas_input_locations, import_sites, europe_shap countries = regions.index.str[:2].unique().str.replace("GB", "UK") gas_input_locations = build_gas_input_locations( - snakemake.input.lng, + snakemake.input.gem, snakemake.input.entry, - snakemake.input.production, countries, ) From 304b37fc035d0ff56970e8384939394218109769 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 31 Jul 2023 12:20:43 +0200 Subject: [PATCH 053/293] add locations, capacities and costs of existing gas storage --- rules/build_sector.smk | 1 + scripts/build_gas_input_locations.py | 24 ++++++++++++++++-------- scripts/prepare_sector_network.py | 17 ++++++++++++----- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/rules/build_sector.smk b/rules/build_sector.smk index c750f5860..6c7337991 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -90,6 +90,7 @@ if config["sector"]["gas_network"] or config["sector"]["H2_retrofit"]: keep_local=True, ), 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 diff --git a/scripts/build_gas_input_locations.py b/scripts/build_gas_input_locations.py index ecb5bb5b7..2549bacf5 100644 --- a/scripts/build_gas_input_locations.py +++ b/scripts/build_gas_input_locations.py @@ -76,7 +76,7 @@ def build_gem_prod_data(fn): return gpd.GeoDataFrame(df, geometry=geometry, crs="EPSG:4326") -def build_gas_input_locations(gem_fn, entry_fn, countries): +def build_gas_input_locations(gem_fn, entry_fn, sto_fn, countries): # LNG terminals lng = build_gem_lng_data(gem_fn) @@ -89,23 +89,30 @@ def build_gas_input_locations(gem_fn, entry_fn, countries): | (entry.from_country == "NO") # malformed datapoint # entries from NO to GB ] + sto = read_scigrid_gas(sto_fn) + remove_country = ["RU", "UA", "TR", "BY"] + sto = sto.query("country_code != @remove_country") + # production sites inside the model scope prod = build_gem_prod_data(gem_fn) mcm_per_day_to_mw = 437.5 # MCM/day to MWh/h mcm_per_year_to_mw = 1.199 # MCM/year to MWh/h mtpa_to_mw = 1649.224 # mtpa to MWh/h - lng["p_nom"] = lng["CapacityInMtpa"] * mtpa_to_mw - entry["p_nom"] = entry["max_cap_from_to_M_m3_per_d"] * mcm_per_day_to_mw - prod["p_nom"] = prod["mcm_per_year"] * mcm_per_year_to_mw + mcm_to_gwh = 11.36 # MCM to GWh + lng["capacity"] = lng["CapacityInMtpa"] * mtpa_to_mw + entry["capacity"] = entry["max_cap_from_to_M_m3_per_d"] * mcm_per_day_to_mw + prod["capacity"] = prod["mcm_per_year"] * mcm_per_year_to_mw + sto["capacity"] = sto["max_cushionGas_M_m3"] * mcm_to_gwh lng["type"] = "lng" entry["type"] = "pipeline" prod["type"] = "production" + sto["type"] = "storage" - sel = ["geometry", "p_nom", "type"] + sel = ["geometry", "capacity", "type"] - return pd.concat([prod[sel], entry[sel], lng[sel]], ignore_index=True) + return pd.concat([prod[sel], entry[sel], lng[sel], sto[sel]], ignore_index=True) def assign_reference_import_sites(gas_input_locations, import_sites, europe_shape): @@ -161,6 +168,7 @@ def assign_reference_import_sites(gas_input_locations, import_sites, europe_shap gas_input_locations = build_gas_input_locations( snakemake.input.gem, snakemake.input.entry, + snakemake.input.storage, countries, ) @@ -175,9 +183,9 @@ def assign_reference_import_sites(gas_input_locations, import_sites, europe_shap gas_input_nodes.to_file(snakemake.output.gas_input_nodes, driver="GeoJSON") gas_input_nodes_s = ( - gas_input_nodes.groupby(["bus", "type"])["p_nom"].sum().unstack() + gas_input_nodes.groupby(["bus", "type"])["capacity"].sum().unstack() ) - gas_input_nodes_s.columns.name = "p_nom" + gas_input_nodes_s.columns.name = "capacity" gas_input_nodes_s.to_csv(snakemake.output.gas_input_nodes_simplified) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 5b97a47bc..bc7e87fce 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -451,10 +451,11 @@ def add_carrier_buses(n, carrier, nodes=None): n.add("Carrier", carrier) 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 n.madd("Bus", nodes, location=location, carrier=carrier, unit=unit) - # capital cost could be corrected to e.g. 0.2 EUR/kWh * annuity and O&M n.madd( "Store", nodes + " Store", @@ -462,8 +463,7 @@ def add_carrier_buses(n, carrier, nodes=None): e_nom_extendable=True, e_cyclic=True, carrier=carrier, - capital_cost=0.2 - * costs.at[carrier, "discount rate"], # preliminary value to avoid zeros + capital_cost=capital_cost, ) n.madd( @@ -1159,7 +1159,7 @@ def add_storage_and_grids(n, costs): if options["gas_network"]: logger.info( - "Add natural gas infrastructure, incl. LNG terminals, production and entry-points." + "Add natural gas infrastructure, incl. LNG terminals, production, storage and entry-points." ) if options["H2_retrofit"]: @@ -1204,10 +1204,17 @@ def add_storage_and_grids(n, costs): remove_i = n.generators[gas_i & internal_i].index n.generators.drop(remove_i, inplace=True) - p_nom = gas_input_nodes.sum(axis=1).rename(lambda x: x + " gas") + input_types = ["lng", "pipeline", "production"] + p_nom = gas_input_nodes[input_types].sum(axis=1).rename(lambda x: x + " gas") n.generators.loc[gas_i, "p_nom_extendable"] = False n.generators.loc[gas_i, "p_nom"] = p_nom + # add existing gas storage capacity + gas_i = n.stores.carrier == "gas" + e_nom = gas_input_nodes["storage"].rename(lambda x: x + " gas Store").reindex(n.stores.index).fillna(0.) * 1e3 # MWh_LHV + e_nom.clip(upper=e_nom.quantile(0.98), inplace=True) # limit extremely large storage + n.stores.loc[gas_i, "e_nom_min"] = e_nom + # add candidates for new gas pipelines to achieve full connectivity G = nx.Graph() From 6cdfb0ba912da641034946bf36fc0dc3f8ee3b3a Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 31 Jul 2023 13:18:21 +0200 Subject: [PATCH 054/293] allow extra HVDC connections between MENA countries --- config/config.default.yaml | 7 +++++++ scripts/prepare_sector_network.py | 26 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/config/config.default.yaml b/config/config.default.yaml index 8efa3a070..96d7e4ba9 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -504,6 +504,13 @@ sector: - TN - TR - UA + extra_connections: + - MA-DZ + - DZ-LY + - DZ-TN + - TN-LY + - LY-EG + - EG-SA hvdc_losses: 3.e-5 # p.u./km length_factor: 1.25 distance_threshold: 0.05 # quantile diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index bc7e87fce..08a5ce095 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3175,6 +3175,32 @@ def _coordinates(ct): lifetime=costs.at["battery inverter", "lifetime"], ) + # add extra HVDC connections between MENA countries + + for bus0_bus1 in cf.get("extra_connections", []): + bus0, bus1 = bus0_bus1.split("-") + + a = exporters.to_crs(3857).at[bus0, "geometry"] + b = exporters.to_crs(3857).at[bus1, "geometry"] + d = a.distance(b) / 1e3 # km + + capital_cost = ( + d * cf["length_factor"] * costs.at["HVDC overhead", "fixed"] + + costs.at["HVDC inverter pair", "fixed"] + ) + + n.add( + "Link", + f"external HVDC {bus0_bus1}", + bus0=bus0, + bus1=bus1, + carrier="external HVDC", + p_min_pu=-1, + p_nom_extendable=True, + capital_cost=capital_cost, + length=d, + ) + def add_import_options( n, From df3980b4dcea3541fcaa6e4b230bca33f7c56e96 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 31 Jul 2023 13:19:13 +0200 Subject: [PATCH 055/293] adapt import options to new input data --- scripts/prepare_sector_network.py | 39 ++++++++++++++++--------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 08a5ce095..fd0edb942 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3077,7 +3077,7 @@ def _coordinates(ct): efficiency=1 - import_links.values * cf["hvdc_losses"], ) - for tech in ["solar-utility", "onwind", "offwind"]: + for tech in ["solar-utility", "onwind"]: p_max_pu_tech = p_max_pu.sel(technology=tech).to_pandas().dropna().T exporters_tech_i = exporters.index.intersection(p_max_pu_tech.columns) @@ -3107,7 +3107,7 @@ def _coordinates(ct): carrier="external H2", e_nom_extendable=True, e_cyclic=True, - capital_cost=costs.at["hydrogen storage tank incl. compressor", "fixed"], + capital_cost=costs.at["hydrogen storage tank type 1 including compressor", "fixed"], ) n.madd( @@ -3124,15 +3124,15 @@ def _coordinates(ct): n.madd( "Link", - h2_buses_i + " Fuel Cell", + h2_buses_i + " H2 Turbine", bus0=h2_buses_i, bus1=buses_i, - carrier="external H2 Fuel Cell", + carrier="external H2 Turbine", p_nom_extendable=True, - efficiency=costs.at["fuel cell", "efficiency"], - capital_cost=costs.at["fuel cell", "fixed"] - * costs.at["fuel cell", "efficiency"], - lifetime=costs.at["fuel cell", "lifetime"], + efficiency=costs.at["OCGT", "efficiency"], + capital_cost=costs.at["OCGT", "fixed"] + * costs.at["OCGT", "efficiency"], + lifetime=costs.at["OCGT", "lifetime"], ) # battery storage @@ -3210,7 +3210,7 @@ def add_import_options( "pipeline-h2", "shipping-lh2", "shipping-lch4", - "shipping-meoh", + #"shipping-meoh", "shipping-ftfuel", "shipping-lnh3", # "shipping-steel", @@ -3239,16 +3239,17 @@ def add_import_options( "shipping-lch4": " gas", "shipping-lnh3": " NH3", "shipping-ftfuel": " oil", - "shipping-meoh": " methanol", + #"shipping-meoh": " methanol", # "shipping-steel": " steel", } co2_intensity = { "shipping-lch4": "gas", "shipping-ftfuel": "oil", - "shipping-meoh": "methanol", # TODO: or shipping fuel methanol + #"shipping-meoh": "methanol", # TODO: or shipping fuel methanol # "shipping-steel": "", TODO: is this necessary? } + # TODO take from options MWh_MeOH_per_tCO2 import_costs = pd.read_csv(snakemake.input.import_costs, delimiter=";") cols = ["esc", "exporter", "importer", "value"] @@ -3336,7 +3337,7 @@ def add_import_options( # need special handling for copperplated imports copperplated_options = { "shipping-ftfuel", - "shipping-meoh", + #"shipping-meoh", # "shipping-steel", } @@ -3349,8 +3350,8 @@ def add_import_options( n.add( "Store", - "EU import {tech} store", - bus="EU import {tech} bus", + f"EU import {tech} store", + bus=f"EU import {tech} bus", e_nom_extendable=True, e_nom_min=-np.inf, e_nom_max=0, @@ -3360,11 +3361,11 @@ def add_import_options( n.add( "Link", - "EU import {tech}", - bus0="EU import {tech} bus", + f"EU import {tech}", + bus0=f"EU import {tech} bus", bus1="EU" + suffix, bus2="co2 atmosphere", - carrier="import {tech}", + carrier=f"import {tech}", efficiency2=-costs.at[co2_intensity[tech], "CO2 intensity"], marginal_cost=marginal_costs, p_nom=1e7, @@ -3743,8 +3744,8 @@ def set_temporal_aggregation(n, opts, solver_name): CH4=["shipping-lch4"], NH3=["shipping-lnh3"], FT=["shipping-ftfuel"], - MEOH=["shipping-meoh"], - # STEEL=["shipping-steel"] + # MeOH=["shipping-meoh"], + # St=["shipping-steel"] ) for o in opts: if not o.startswith("imp"): From 96865bbca3b969fc271eb4345d1aeda96ce69bb8 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 31 Jul 2023 15:29:17 +0200 Subject: [PATCH 056/293] add additional extra-long HVDC links, like Xlinks project --- config/config.default.yaml | 50 +++++++++++++++++++++++++++++++ scripts/prepare_sector_network.py | 20 +++++++++++-- 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 96d7e4ba9..e3e1ef189 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -511,6 +511,56 @@ sector: - TN-LY - LY-EG - EG-SA + xlinks: + MA: + # Plymouth + - x: -4.14 + y: 50.39 + length: 3800 + # Cork + - x: -8.52 + y: 51.92 + length: 4000 + # Brest + - x: -4.49 + y: 48.43 + length: 3200 + # Vigo + - x: -8.70 + y: 42.31 + length: 1800 + DZ: + # Barcelona + - x: 2.37 + y: 41.57 + length: 1300 + # Genoa + - x: 8.93 + y: 44.41 + length: 1800 + # Marseille + - x: 5.30 + y: 43.38 + length: 1400 + TN: + # Genoa + - x: 8.93 + y: 44.41 + length: 1500 + # Marseille + - x: 5.30 + y: 43.38 + length: 1500 + LY: + # Venice + - x: 12.39 + y: 45.56 + length: 2500 + EG: + # Venice + - x: 12.39 + y: 45.56 + length: 2500 hvdc_losses: 3.e-5 # p.u./km length_factor: 1.25 distance_threshold: 0.05 # quantile diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index fd0edb942..f85a9ef71 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -27,6 +27,7 @@ from pypsa.geo import haversine_pts from pypsa.io import import_components_from_dataframe from scipy.stats import beta +from shapely.geometry import Point geolocator = Nominatim(user_agent="locate-exporting-region", timeout=10) @@ -41,6 +42,8 @@ pd_version = parse(pd.__version__) agg_group_kwargs = dict(numeric_only=False) if pd_version >= Version("1.3") else {} +DISTANCE_CRS = 3857 + def define_spatial(nodes, options): """ @@ -3046,15 +3049,26 @@ def _coordinates(ct): exporters = gpd.GeoDataFrame(exporters, geometry=geometry, crs=4326) import_links = {} - a = regions.representative_point().to_crs(3857) + a = regions.representative_point().to_crs(DISTANCE_CRS) for ct in exporters.index: - b = exporters.to_crs(3857).loc[ct].geometry + b = exporters.to_crs(DISTANCE_CRS).loc[ct].geometry d = a.distance(b) import_links[ct] = ( d.where(d < d.quantile(cf["distance_threshold"])).div(1e3).dropna() ) # km import_links = pd.concat(import_links) + # xlinks + xlinks = {} + for bus0, links in cf["xlinks"].items(): + for link in links: + landing_point = gpd.GeoSeries([Point(link["x"], link["y"])], crs=4326).to_crs(DISTANCE_CRS) + bus1 = regions.to_crs(DISTANCE_CRS).geometry.distance(landing_point[0]).idxmin() + xlinks[(bus0, bus1)] = link["length"] + + import_links = pd.concat([import_links, pd.Series(xlinks)], axis=0) + import_links = import_links.drop_duplicates(keep='first') + hvdc_cost = ( import_links.values * cf["length_factor"] * costs.at["HVDC submarine", "fixed"] + costs.at["HVDC inverter pair", "fixed"] @@ -3346,7 +3360,7 @@ def add_import_options( suffix = bus_suffix[tech] - n.add("Bus", f"EU import {tech} bus", carrier="import {tech}") + n.add("Bus", f"EU import {tech} bus", carrier=f"import {tech}") n.add( "Store", From 9ab3be41edebf3e0df2f725bacb89269efabf48c Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 31 Jul 2023 17:09:59 +0200 Subject: [PATCH 057/293] option for losses on bidirectional links via link splitting --- config/config.default.yaml | 6 +++++- scripts/prepare_sector_network.py | 27 ++++++++++++++++++++++++++- scripts/solve_network.py | 24 ++++++++++++++++++------ 3 files changed, 49 insertions(+), 8 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index e3e1ef189..955e97006 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -466,7 +466,11 @@ sector: electricity_distribution_grid: true electricity_distribution_grid_cost_factor: 1.0 electricity_grid_connection: true - electricity_grid_transmission_losses: 0 # Enable (= value from 1 to 3) or disable (= 0) transmission losses in electricity grid + transmission_losses: + # per 1000 km + DC: 0 + H2 pipeline: 0 + gas pipeline: 0 H2_network: true gas_network: false H2_retrofit: false diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index f85a9ef71..7a44e6971 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3642,6 +3642,28 @@ def set_temporal_aggregation(n, opts, solver_name): return n +def lossy_bidirectional_links(n, carrier, losses_per_thousand_km=0.): + "Split bidirectional links into two unidirectional links to include transmission losses." + + carrier_i = n.links.query("carrier == @carrier").index + + if not losses_per_thousand_km or carrier_i.empty: + return + + logger.info(f"Specified losses for {carrier} transmission. Splitting bidirectional links.") + + carrier_i = n.links.query("carrier == @carrier").index + n.links.loc[carrier_i, "p_min_pu"] = 0 + n.links["reversed"] = False + n.links.loc[carrier_i, "efficiency"] = 1 - n.links.loc[carrier_i, "length"] * losses_per_thousand_km / 1e3 + rev_links = n.links.loc[carrier_i].copy().rename({"bus0": "bus1", "bus1": "bus0"}, axis=1) + rev_links.capital_cost = 0 + rev_links.reversed = True + rev_links.index = rev_links.index.map(lambda x: x + "-reversed") + + n.links = pd.concat([n.links, rev_links], sort=False) + + if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake @@ -3827,9 +3849,12 @@ def set_temporal_aggregation(n, opts, solver_name): if options["electricity_grid_connection"]: add_electricity_grid_connection(n, costs) + for k, v in options["transmission_losses"].items(): + lossy_bidirectional_links(n, k, v) + # Workaround: Remove lines with conflicting (and unrealistic) properties # cf. https://github.com/PyPSA/pypsa-eur/issues/444 - if options["electricity_grid_transmission_losses"]: + if snakemake.config["solving"]["options"]["transmission_losses"]: idx = n.lines.query("num_parallel == 0").index logger.info( f"Removing {len(idx)} line(s) with properties conflicting with transmission losses functionality." diff --git a/scripts/solve_network.py b/scripts/solve_network.py index 284c477a9..68ee69702 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -491,6 +491,23 @@ def add_battery_constraints(n): n.model.add_constraints(lhs == 0, name="Link-charger_ratio") +def add_lossy_bidirectional_link_constraints(n): + if not n.links.p_nom_extendable.any() or not "reversed" in n.links.columns: + return + + carriers = n.links.loc[n.links.reversed, "carrier"].unique() + + backward_i = n.links.query("carrier in @carriers and reversed and p_nom_extendable").index + forward_i = n.links.query("carrier in @carriers and ~reversed and p_nom_extendable").index + + assert len(forward_i) == len(backward_i) + + lhs = n.model["Link-p_nom"].loc[backward_i] + rhs = n.model["Link-p_nom"].loc[forward_i] + + n.model.add_constraints(lhs == rhs, name="Link-bidirectional_sync") + + def add_chp_constraints(n): electric = ( n.links.index.str.contains("urban central") @@ -619,6 +636,7 @@ def extra_functionality(n, snapshots): if "EQ" in o: add_EQ_constraints(n, o) add_battery_constraints(n) + add_lossy_bidirectional_link_constraints(n) add_pipe_retrofit_constraint(n) add_energy_import_limit(n, snapshots) @@ -649,9 +667,6 @@ def solve_network(n, config, solving, opts="", **kwargs): transmission_losses=transmission_losses, assign_all_duals=assign_all_duals, extra_functionality=extra_functionality, - transmission_losses=config["sector"][ - "electricity_grid_transmission_losses" - ], **solver_options, **kwargs, ) @@ -664,9 +679,6 @@ def solve_network(n, config, solving, opts="", **kwargs): transmission_losses=transmission_losses, assign_all_duals=assign_all_duals, extra_functionality=extra_functionality, - transmission_losses=config["sector"][ - "electricity_grid_transmission_losses" - ], **solver_options, **kwargs, ) From bf0c41f71c10cbdca6fd311d4a4f5ab8419066d3 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 2 Aug 2023 17:20:42 +0200 Subject: [PATCH 058/293] add approximate costs for import terminals --- scripts/prepare_sector_network.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 7a44e6971..0abed1ede 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3297,7 +3297,7 @@ def add_import_options( import_nodes_tech = import_nodes.loc[sel, [tech]] import_nodes_tech = ( - import_nodes_tech.rename(columns={tech: "p_nom"}) * capacity_boost + import_nodes_tech.rename(columns={tech: "p_nom"}) ) marginal_costs = ports[tech].dropna().map(import_costs_tech) @@ -3309,6 +3309,7 @@ def add_import_options( if tech in co2_intensity.keys(): buses = import_nodes_tech.index + f"{suffix} import {tech}" + capital_cost = 7018. if tech == 'shipping-lch4' else 0. # €/MW/a n.madd("Bus", buses + " bus", carrier=f"import {tech}") @@ -3332,20 +3333,27 @@ def add_import_options( carrier=f"import {tech}", efficiency2=-costs.at[co2_intensity[tech], "CO2 intensity"], marginal_cost=import_nodes_tech.marginal_cost.values, - p_nom=import_nodes_tech.p_nom.values, + p_nom_extendable=True, + capital_cost=capital_cost, + p_nom_min=import_nodes_tech.p_nom.values, + p_nom_max=import_nodes_tech.p_nom.values * capacity_boost, ) else: location = import_nodes_tech.index buses = location if tech == "hvdc-to-elec" else location + suffix + capital_cost = 1.2 * 7018. if tech == 'shipping-lh2' else 0. # €/MW/a, +20% compared to LNG + n.madd( "Generator", import_nodes_tech.index + f"{suffix} import {tech}", bus=buses, carrier=f"import {tech}", marginal_cost=import_nodes_tech.marginal_cost.values, - p_nom=import_nodes_tech.p_nom.values, + p_nom_extendable=True, + capital_cost=capital_cost, + p_nom_max=import_nodes_tech.p_nom.values * capacity_boost, ) # need special handling for copperplated imports From ad0c079ffcd0c50cbec16d60afaf00c6e4565812 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 2 Aug 2023 19:19:18 +0200 Subject: [PATCH 059/293] add option for methanol steam reforming with/without CC --- config/config.default.yaml | 1 + scripts/prepare_sector_network.py | 53 ++++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 955e97006..64836f091 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -440,6 +440,7 @@ sector: hydrogen_fuel_cell: true hydrogen_turbine: false SMR: true + methanol_reforming: true regional_co2_sequestration_potential: enable: false attribute: 'conservative estimate Mt' diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 0abed1ede..ec9886523 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -643,6 +643,54 @@ def add_allam(n, costs): ) +def add_methanol_reforming(n, costs): + + logger.info("Adding methanol steam reforming.") + + nodes = pop_layout.index + + # TODO: heat and electricity demand for process and carbon capture + # but the energy demands for carbon capture have not yet been added for other CC processes + + tech = "Methanol steam reforming" + + capital_cost = costs.at[tech, "fixed"] / costs.at[tech, "methanol-input"] + + capital_cost_cc = capital_cost + costs.at["cement capture", "fixed"] / options["MWh_MeOH_per_tCO2"] + + 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"]) / options["MWh_MeOH_per_tCO2"], + efficiency3=costs.at["cement capture", "capture_rate"] / options["MWh_MeOH_per_tCO2"], + carrier=f"{tech} CC", + lifetime=costs.at[tech, "lifetime"], + ) + + n.madd( + "Link", + nodes, + 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=1 / options["MWh_MeOH_per_tCO2"], + carrier=tech, + 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)] @@ -3263,7 +3311,7 @@ def add_import_options( #"shipping-meoh": "methanol", # TODO: or shipping fuel methanol # "shipping-steel": "", TODO: is this necessary? } - # TODO take from options MWh_MeOH_per_tCO2 + # TODO take from options["MWh_MeOH_per_tCO2"] import_costs = pd.read_csv(snakemake.input.import_costs, delimiter=";") cols = ["esc", "exporter", "importer", "value"] @@ -3810,6 +3858,9 @@ def lossy_bidirectional_links(n, carrier, losses_per_thousand_km=0.): if options["allam_cycle"]: add_allam(n, costs) + if options["methanol_reforming"]: + add_methanol_reforming(n, costs) + solver_name = snakemake.config["solving"]["solver"]["name"] n = set_temporal_aggregation(n, opts, solver_name) From 8acedfd559b14f7d65e636e77019edfa82ac269c Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 3 Aug 2023 08:39:53 +0200 Subject: [PATCH 060/293] use technology-data values for methanolisation inputs --- scripts/prepare_sector_network.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index ec9886523..a1cc2235f 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -656,7 +656,7 @@ def add_methanol_reforming(n, costs): capital_cost = costs.at[tech, "fixed"] / costs.at[tech, "methanol-input"] - capital_cost_cc = capital_cost + costs.at["cement capture", "fixed"] / options["MWh_MeOH_per_tCO2"] + capital_cost_cc = capital_cost + costs.at["cement capture", "fixed"] * costs.at["methanolisation", "carbondioxide-input"] n.madd( "Link", @@ -669,8 +669,8 @@ def add_methanol_reforming(n, costs): p_nom_extendable=True, capital_cost=capital_cost_cc, efficiency=1 / costs.at[tech, "methanol-input"], - efficiency2=(1 - costs.at["cement capture", "capture_rate"]) / options["MWh_MeOH_per_tCO2"], - efficiency3=costs.at["cement capture", "capture_rate"] / options["MWh_MeOH_per_tCO2"], + 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"], ) @@ -685,7 +685,7 @@ def add_methanol_reforming(n, costs): p_nom_extendable=True, capital_cost=capital_cost, efficiency=1 / costs.at[tech, "methanol-input"], - efficiency2=1 / options["MWh_MeOH_per_tCO2"], + efficiency2=costs.at["methanolisation", "carbondioxide-input"], carrier=tech, lifetime=costs.at[tech, "lifetime"], ) @@ -2637,11 +2637,11 @@ def add_industry(n, costs): p_nom_extendable=True, p_min_pu=options.get("min_part_load_methanolisation", 0), capital_cost=costs.at["methanolisation", "fixed"] - * options["MWh_MeOH_per_MWh_H2"], # EUR/MW_H2/a + / costs.at["methanolisation", "hydrogen-input"], # EUR/MW_H2/a lifetime=costs.at["methanolisation", "lifetime"], - efficiency=options["MWh_MeOH_per_MWh_H2"], - efficiency2=-options["MWh_MeOH_per_MWh_H2"] / options["MWh_MeOH_per_MWh_e"], - efficiency3=-options["MWh_MeOH_per_MWh_H2"] / options["MWh_MeOH_per_tCO2"], + efficiency=1 / costs.at["methanolisation", "hydrogen-input"], + efficiency2=-costs.at["methanolisation", "electricity-input"] / costs.at["methanolisation", "hydrogen-input"], + efficiency3=-costs.at["methanolisation", "carbondioxide-input"] / costs.at["methanolisation", "hydrogen-input"], ) efficiency = ( @@ -2659,7 +2659,7 @@ 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 - co2 = p_set_methanol / options["MWh_MeOH_per_tCO2"] + co2 = p_set_methanol * costs.at["methanolisation", "carbondioxide-input"] n.add( "Load", @@ -3311,7 +3311,7 @@ def add_import_options( #"shipping-meoh": "methanol", # TODO: or shipping fuel methanol # "shipping-steel": "", TODO: is this necessary? } - # TODO take from options["MWh_MeOH_per_tCO2"] + # TODO take from costs.at["methanolisation", "carbondioxide-input"] import_costs = pd.read_csv(snakemake.input.import_costs, delimiter=";") cols = ["esc", "exporter", "importer", "value"] From 86f39036112aba90f8f6365fd5c85df8a8225299 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 3 Aug 2023 09:32:57 +0200 Subject: [PATCH 061/293] add todos for methanol reforming --- scripts/prepare_sector_network.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index a1cc2235f..21add3dca 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -649,8 +649,9 @@ def add_methanol_reforming(n, costs): nodes = pop_layout.index - # TODO: heat and electricity demand for process and carbon capture + # 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 tech = "Methanol steam reforming" From 6dd504683f4d319396e7f4e6896062bf6028d134 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 3 Aug 2023 09:42:42 +0200 Subject: [PATCH 062/293] update allam cycle gas assumptions, following methanol-ldes --- config/config.default.yaml | 4 ++-- doc/configtables/sector.csv | 2 +- scripts/prepare_sector_network.py | 20 +++++++++++--------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 64836f091..995d01bf7 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -436,7 +436,7 @@ sector: coal_cc: false dac: true co2_vent: false - allam_cycle: false + allam_cycle_gas: false hydrogen_fuel_cell: true hydrogen_turbine: false SMR: true @@ -828,7 +828,7 @@ plotting: biogas to gas: '#e36311' CCGT: '#a85522' CCGT marginal: '#a85522' - allam: '#B98F76' + allam gas: '#B98F76' gas for industry co2 to atmosphere: '#692e0a' gas for industry co2 to stored: '#8a3400' gas for industry: '#853403' diff --git a/doc/configtables/sector.csv b/doc/configtables/sector.csv index d610c8626..043590f8b 100644 --- a/doc/configtables/sector.csv +++ b/doc/configtables/sector.csv @@ -75,7 +75,7 @@ helmeth,--,"{true, false}",Add option for transforming power into gas using HELM 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) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 21add3dca..e8af94b4c 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -620,7 +620,7 @@ def add_co2_network(n, costs): ) -def add_allam(n, costs): +def add_allam_cycle_gas(n, costs): logger.info("Adding Allam cycle gas power plants.") nodes = pop_layout.index @@ -628,18 +628,20 @@ 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=0.6 * 1.5e6 * 0.1, # efficiency * EUR/MW * annuity + capital_cost=0.66 * 1.832e6 * calculate_annuity(25, 0.07), # efficiency * EUR/MW * annuity marginal_cost=2, - efficiency=0.6, - efficiency2=costs.at["gas", "CO2 intensity"], - lifetime=30.0, + efficiency=0.66, + efficiency2=0.98 * costs.at["gas", "CO2 intensity"], + efficiency3=0.02 * costs.at["gas", "CO2 intensity"], + lifetime=25, ) @@ -3856,8 +3858,8 @@ def lossy_bidirectional_links(n, carrier, losses_per_thousand_km=0.): ) break - if options["allam_cycle"]: - add_allam(n, costs) + if options["allam_cycle_gas"]: + add_allam_cycle_gas(n, costs) if options["methanol_reforming"]: add_methanol_reforming(n, costs) From cf0b7ca02e37eb1b0d136a9fca9c6823aa2e4cba Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 3 Aug 2023 10:01:46 +0200 Subject: [PATCH 063/293] add FOM to allam cycle gas --- 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 e8af94b4c..e0e97c211 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -636,7 +636,7 @@ def add_allam_cycle_gas(n, costs): carrier="allam gas", p_nom_extendable=True, # TODO: add costs to technology-data - capital_cost=0.66 * 1.832e6 * calculate_annuity(25, 0.07), # efficiency * EUR/MW * annuity + capital_cost=0.66 * 1.832e6 * (calculate_annuity(25, 0.07) + 0.0385), # efficiency * EUR/MW * (annuity + FOM) marginal_cost=2, efficiency=0.66, efficiency2=0.98 * costs.at["gas", "CO2 intensity"], From 69316ce9c82667f4a1de395c5935d187101f1211 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 3 Aug 2023 10:02:32 +0200 Subject: [PATCH 064/293] add methanol to power routes; Allam, OCGT, CCGT, CCGT CC --- config/config.default.yaml | 1 + scripts/prepare_sector_network.py | 89 +++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/config/config.default.yaml b/config/config.default.yaml index 995d01bf7..31cccbce7 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -441,6 +441,7 @@ sector: hydrogen_turbine: false SMR: true methanol_reforming: true + methanol_to_power: true regional_co2_sequestration_potential: enable: false attribute: 'conservative estimate Mt' diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index e0e97c211..9dba2baf3 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -645,6 +645,92 @@ def add_allam_cycle_gas(n, costs): ) +def add_methanol_to_power(n, costs): + + # TODO: add costs to technology-data + + logger.info("Adding Allam cycle methanol power plants.") + + nodes = pop_layout.index + + 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.66 * 1.832e6 * calculate_annuity(25, 0.07), # efficiency * EUR/MW * annuity + marginal_cost=2, + efficiency=0.66, + efficiency2=0.98 * costs.at["methanolisation", "carbondioxide-input"], + efficiency3=0.02 * costs.at["methanolisation", "carbondioxide-input"], + lifetime=25, + ) + + 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, + carrier="CCGT methanol", + p_nom_extendable=True, + capital_cost=capital_cost, + marginal_cost=2, + efficiency=0.58, + lifetime=25, + ) + + logger.info("Adding methanol CCGT power plants with post-combustion carbon capture.") + + # TODO consider efficiency changes / energy inputs for CC + + 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, + ) + + logger.info("Adding methanol OCGT power plants.") + + n.madd( + "Link", + nodes, + suffix=" OCGT methanol", + bus0=spatial.methanol.nodes, + bus1=nodes, + 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, + lifetime=25, + ) + + def add_methanol_reforming(n, costs): logger.info("Adding methanol steam reforming.") @@ -3861,6 +3947,9 @@ def lossy_bidirectional_links(n, carrier, losses_per_thousand_km=0.): if options["allam_cycle_gas"]: add_allam_cycle_gas(n, costs) + if options["methanol_to_power"]: + add_methanol_to_power(n, costs) + if options["methanol_reforming"]: add_methanol_reforming(n, costs) From 6dab706e6d7baefa6731823f074c3bc51da8083f Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 3 Aug 2023 11:28:25 +0200 Subject: [PATCH 065/293] add methanol to power colors to config --- config/config.default.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/config.default.yaml b/config/config.default.yaml index 31cccbce7..d89c6c85c 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -830,6 +830,10 @@ plotting: CCGT: '#a85522' CCGT marginal: '#a85522' allam gas: '#B98F76' + allam methanol: '#B98F76' + CCGT methanol: '#B98F76' + CCGT methanol CC: '#B98F76' + OCGT methanol: '#B98F76' gas for industry co2 to atmosphere: '#692e0a' gas for industry co2 to stored: '#8a3400' gas for industry: '#853403' From aa88c6da58d24ad47bccabf25b0f99593aaeac71 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 3 Aug 2023 11:34:33 +0200 Subject: [PATCH 066/293] methanol-to-power: more control in config.yaml --- config/config.default.yaml | 6 +- scripts/prepare_sector_network.py | 152 ++++++++++++++++-------------- 2 files changed, 85 insertions(+), 73 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index d89c6c85c..d48c37b2b 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -441,7 +441,11 @@ sector: hydrogen_turbine: false SMR: true methanol_reforming: true - methanol_to_power: true + methanol_to_power: + ccgt: true + ccgt_cc: true + ocgt: true + allam: true regional_co2_sequestration_potential: enable: false attribute: 'conservative estimate Mt' diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 9dba2baf3..90121a601 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -645,90 +645,99 @@ def add_allam_cycle_gas(n, costs): ) -def add_methanol_to_power(n, costs): +def add_methanol_to_power(n, costs, types={}): # TODO: add costs to technology-data - logger.info("Adding Allam cycle methanol power plants.") - nodes = pop_layout.index - 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.66 * 1.832e6 * calculate_annuity(25, 0.07), # efficiency * EUR/MW * annuity - marginal_cost=2, - efficiency=0.66, - efficiency2=0.98 * costs.at["methanolisation", "carbondioxide-input"], - efficiency3=0.02 * costs.at["methanolisation", "carbondioxide-input"], - lifetime=25, - ) + if types["allam"]: - logger.info("Adding methanol CCGT power plants.") + logger.info("Adding Allam cycle methanol 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, - carrier="CCGT methanol", - p_nom_extendable=True, - capital_cost=capital_cost, - marginal_cost=2, - efficiency=0.58, - lifetime=25, - ) + 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.66 * 1.832e6 * calculate_annuity(25, 0.07), # efficiency * EUR/MW * annuity + marginal_cost=2, + efficiency=0.66, + efficiency2=0.98 * costs.at["methanolisation", "carbondioxide-input"], + efficiency3=0.02 * costs.at["methanolisation", "carbondioxide-input"], + lifetime=25, + ) - logger.info("Adding methanol CCGT power plants with post-combustion carbon capture.") + if types["ccgt"]: - # TODO consider efficiency changes / energy inputs for CC + logger.info("Adding methanol CCGT power plants.") - capital_cost_cc = capital_cost + costs.at["cement capture", "fixed"] * costs.at["methanolisation", "carbondioxide-input"] + # efficiency * EUR/MW * (annuity + FOM) + capital_cost = 0.58 * 916e3 * (calculate_annuity(25, 0.07) + 0.035) - 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, - ) + n.madd( + "Link", + nodes, + suffix=" CCGT methanol", + bus0=spatial.methanol.nodes, + bus1=nodes, + carrier="CCGT methanol", + p_nom_extendable=True, + capital_cost=capital_cost, + marginal_cost=2, + efficiency=0.58, + lifetime=25, + ) - logger.info("Adding methanol OCGT power plants.") + if types["ccgt_cc"]: - n.madd( - "Link", - nodes, - suffix=" OCGT methanol", - bus0=spatial.methanol.nodes, - bus1=nodes, - 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, - lifetime=25, - ) + logger.info("Adding methanol CCGT power plants with post-combustion carbon capture.") + + # TODO consider efficiency changes / energy inputs for CC + + 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, + 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, + lifetime=25, + ) def add_methanol_reforming(n, costs): @@ -3947,8 +3956,7 @@ def lossy_bidirectional_links(n, carrier, losses_per_thousand_km=0.): if options["allam_cycle_gas"]: add_allam_cycle_gas(n, costs) - if options["methanol_to_power"]: - add_methanol_to_power(n, costs) + add_methanol_to_power(n, costs, types=options["methanol_to_power"]) if options["methanol_reforming"]: add_methanol_reforming(n, costs) From 7302323b91e182bf2bab039689bf43bf2664028d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 3 Aug 2023 09:41:44 +0000 Subject: [PATCH 067/293] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/build_gas_input_locations.py | 21 +++-- scripts/prepare_sector_network.py | 122 ++++++++++++++++++--------- scripts/solve_network.py | 10 ++- 3 files changed, 102 insertions(+), 51 deletions(-) diff --git a/scripts/build_gas_input_locations.py b/scripts/build_gas_input_locations.py index 2549bacf5..fe7578513 100644 --- a/scripts/build_gas_input_locations.py +++ b/scripts/build_gas_input_locations.py @@ -48,7 +48,7 @@ def build_gem_prod_data(fn): remove_country = ["Cyprus", "Türkiye"] remove_fuel_type = ["oil"] - + df = df.query( "Status != 'shut in' \ & 'Fuel type' != 'oil' \ @@ -59,18 +59,25 @@ def build_gem_prod_data(fn): p = pd.read_excel(fn[0], sheet_name="Gas extraction - production") p = p.set_index("GEM Unit ID") - p = p[p["Fuel description"] == 'gas' ] + p = p[p["Fuel description"] == "gas"] capacities = pd.DataFrame(index=df.index) for key in ["production", "production design capacity", "reserves"]: - cap = p.loc[p["Production/reserves"] == key, "Quantity (converted)"].groupby("GEM Unit ID").sum().reindex(df.index) + cap = ( + p.loc[p["Production/reserves"] == key, "Quantity (converted)"] + .groupby("GEM Unit ID") + .sum() + .reindex(df.index) + ) # assume capacity such that 3% of reserves can be extracted per year (25% quantile) - annualization_factor = 0.03 if key == "reserves" else 1. + annualization_factor = 0.03 if key == "reserves" else 1.0 capacities[key] = cap * annualization_factor - df["mcm_per_year"] = capacities["production"] \ - .combine_first(capacities["production design capacity"]) \ + df["mcm_per_year"] = ( + capacities["production"] + .combine_first(capacities["production design capacity"]) .combine_first(capacities["reserves"]) + ) geometry = gpd.points_from_xy(df["Longitude"], df["Latitude"]) return gpd.GeoDataFrame(df, geometry=geometry, crs="EPSG:4326") @@ -89,7 +96,7 @@ def build_gas_input_locations(gem_fn, entry_fn, sto_fn, countries): | (entry.from_country == "NO") # malformed datapoint # entries from NO to GB ] - sto = read_scigrid_gas(sto_fn) + sto = read_scigrid_gas(sto_fn) remove_country = ["RU", "UA", "TR", "BY"] sto = sto.query("country_code != @remove_country") diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 996ddb3c2..c27a4a02b 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -636,7 +636,11 @@ def add_allam_cycle_gas(n, costs): carrier="allam gas", p_nom_extendable=True, # TODO: add costs to technology-data - capital_cost=0.66 * 1.832e6 * (calculate_annuity(25, 0.07) + 0.0385), # efficiency * EUR/MW * (annuity + FOM) + capital_cost=0.66 + * 1.832e6 + * ( + calculate_annuity(25, 0.07) + 0.0385 + ), # efficiency * EUR/MW * (annuity + FOM) marginal_cost=2, efficiency=0.66, efficiency2=0.98 * costs.at["gas", "CO2 intensity"], @@ -646,16 +650,13 @@ def add_allam_cycle_gas(n, costs): 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, @@ -666,7 +667,9 @@ def add_methanol_to_power(n, costs, types={}): bus3="co2 atmosphere", carrier="allam methanol", p_nom_extendable=True, - capital_cost=0.66 * 1.832e6 * calculate_annuity(25, 0.07), # efficiency * EUR/MW * annuity + capital_cost=0.66 + * 1.832e6 + * calculate_annuity(25, 0.07), # efficiency * EUR/MW * annuity marginal_cost=2, efficiency=0.66, efficiency2=0.98 * costs.at["methanolisation", "carbondioxide-input"], @@ -675,7 +678,6 @@ def add_methanol_to_power(n, costs, types={}): ) if types["ccgt"]: - logger.info("Adding methanol CCGT power plants.") # efficiency * EUR/MW * (annuity + FOM) @@ -696,12 +698,17 @@ def add_methanol_to_power(n, costs, types={}): ) if types["ccgt_cc"]: - - logger.info("Adding methanol CCGT power plants with post-combustion carbon capture.") + logger.info( + "Adding methanol CCGT power plants with post-combustion carbon capture." + ) # TODO consider efficiency changes / energy inputs for CC - capital_cost_cc = capital_cost + costs.at["cement capture", "fixed"] * costs.at["methanolisation", "carbondioxide-input"] + capital_cost_cc = ( + capital_cost + + costs.at["cement capture", "fixed"] + * costs.at["methanolisation", "carbondioxide-input"] + ) n.madd( "Link", @@ -716,13 +723,14 @@ def add_methanol_to_power(n, costs, types={}): 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"], + 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( @@ -733,7 +741,11 @@ def add_methanol_to_power(n, costs, types={}): bus1=nodes, 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=0.35 + * 458e3 + * ( + calculate_annuity(25, 0.07) + 0.035 + ), # efficiency * EUR/MW * (annuity + FOM) marginal_cost=2, efficiency=0.35, lifetime=25, @@ -741,7 +753,6 @@ def add_methanol_to_power(n, costs, types={}): def add_methanol_reforming(n, costs): - logger.info("Adding methanol steam reforming.") nodes = pop_layout.index @@ -754,7 +765,11 @@ def add_methanol_reforming(n, costs): 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"] + capital_cost_cc = ( + capital_cost + + costs.at["cement capture", "fixed"] + * costs.at["methanolisation", "carbondioxide-input"] + ) n.madd( "Link", @@ -767,8 +782,10 @@ def add_methanol_reforming(n, costs): 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"], + 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"], ) @@ -1360,8 +1377,16 @@ def add_storage_and_grids(n, costs): # add existing gas storage capacity gas_i = n.stores.carrier == "gas" - e_nom = gas_input_nodes["storage"].rename(lambda x: x + " gas Store").reindex(n.stores.index).fillna(0.) * 1e3 # MWh_LHV - e_nom.clip(upper=e_nom.quantile(0.98), inplace=True) # limit extremely large storage + e_nom = ( + gas_input_nodes["storage"] + .rename(lambda x: x + " gas Store") + .reindex(n.stores.index) + .fillna(0.0) + * 1e3 + ) # MWh_LHV + e_nom.clip( + upper=e_nom.quantile(0.98), inplace=True + ) # limit extremely large storage n.stores.loc[gas_i, "e_nom_min"] = e_nom # add candidates for new gas pipelines to achieve full connectivity @@ -2758,8 +2783,10 @@ def add_industry(n, costs): / costs.at["methanolisation", "hydrogen-input"], # EUR/MW_H2/a lifetime=costs.at["methanolisation", "lifetime"], efficiency=1 / costs.at["methanolisation", "hydrogen-input"], - efficiency2=-costs.at["methanolisation", "electricity-input"] / costs.at["methanolisation", "hydrogen-input"], - efficiency3=-costs.at["methanolisation", "carbondioxide-input"] / costs.at["methanolisation", "hydrogen-input"], + efficiency2=-costs.at["methanolisation", "electricity-input"] + / costs.at["methanolisation", "hydrogen-input"], + efficiency3=-costs.at["methanolisation", "carbondioxide-input"] + / costs.at["methanolisation", "hydrogen-input"], ) efficiency = ( @@ -3228,12 +3255,18 @@ def _coordinates(ct): xlinks = {} for bus0, links in cf["xlinks"].items(): for link in links: - landing_point = gpd.GeoSeries([Point(link["x"], link["y"])], crs=4326).to_crs(DISTANCE_CRS) - bus1 = regions.to_crs(DISTANCE_CRS).geometry.distance(landing_point[0]).idxmin() + landing_point = gpd.GeoSeries( + [Point(link["x"], link["y"])], crs=4326 + ).to_crs(DISTANCE_CRS) + bus1 = ( + regions.to_crs(DISTANCE_CRS) + .geometry.distance(landing_point[0]) + .idxmin() + ) xlinks[(bus0, bus1)] = link["length"] import_links = pd.concat([import_links, pd.Series(xlinks)], axis=0) - import_links = import_links.drop_duplicates(keep='first') + import_links = import_links.drop_duplicates(keep="first") hvdc_cost = ( import_links.values * cf["length_factor"] * costs.at["HVDC submarine", "fixed"] @@ -3287,7 +3320,9 @@ def _coordinates(ct): carrier="external H2", e_nom_extendable=True, e_cyclic=True, - capital_cost=costs.at["hydrogen storage tank type 1 including compressor", "fixed"], + capital_cost=costs.at[ + "hydrogen storage tank type 1 including compressor", "fixed" + ], ) n.madd( @@ -3310,8 +3345,7 @@ def _coordinates(ct): carrier="external H2 Turbine", p_nom_extendable=True, efficiency=costs.at["OCGT", "efficiency"], - capital_cost=costs.at["OCGT", "fixed"] - * costs.at["OCGT", "efficiency"], + capital_cost=costs.at["OCGT", "fixed"] * costs.at["OCGT", "efficiency"], lifetime=costs.at["OCGT", "lifetime"], ) @@ -3390,7 +3424,7 @@ def add_import_options( "pipeline-h2", "shipping-lh2", "shipping-lch4", - #"shipping-meoh", + # "shipping-meoh", "shipping-ftfuel", "shipping-lnh3", # "shipping-steel", @@ -3419,17 +3453,17 @@ def add_import_options( "shipping-lch4": " gas", "shipping-lnh3": " NH3", "shipping-ftfuel": " oil", - #"shipping-meoh": " methanol", + # "shipping-meoh": " methanol", # "shipping-steel": " steel", } co2_intensity = { "shipping-lch4": "gas", "shipping-ftfuel": "oil", - #"shipping-meoh": "methanol", # TODO: or shipping fuel methanol + # "shipping-meoh": "methanol", # TODO: or shipping fuel methanol # "shipping-steel": "", TODO: is this necessary? } - # TODO take from costs.at["methanolisation", "carbondioxide-input"] + # TODO take from costs.at["methanolisation", "carbondioxide-input"] import_costs = pd.read_csv(snakemake.input.import_costs, delimiter=";") cols = ["esc", "exporter", "importer", "value"] @@ -3462,9 +3496,7 @@ def add_import_options( sel &= ~import_nodes.index.str[:2].isin(forbidden_pipelines) import_nodes_tech = import_nodes.loc[sel, [tech]] - import_nodes_tech = ( - import_nodes_tech.rename(columns={tech: "p_nom"}) - ) + import_nodes_tech = import_nodes_tech.rename(columns={tech: "p_nom"}) marginal_costs = ports[tech].dropna().map(import_costs_tech) import_nodes_tech["marginal_cost"] = marginal_costs @@ -3475,7 +3507,7 @@ def add_import_options( if tech in co2_intensity.keys(): buses = import_nodes_tech.index + f"{suffix} import {tech}" - capital_cost = 7018. if tech == 'shipping-lch4' else 0. # €/MW/a + capital_cost = 7018.0 if tech == "shipping-lch4" else 0.0 # €/MW/a n.madd("Bus", buses + " bus", carrier=f"import {tech}") @@ -3509,7 +3541,9 @@ def add_import_options( location = import_nodes_tech.index buses = location if tech == "hvdc-to-elec" else location + suffix - capital_cost = 1.2 * 7018. if tech == 'shipping-lh2' else 0. # €/MW/a, +20% compared to LNG + capital_cost = ( + 1.2 * 7018.0 if tech == "shipping-lh2" else 0.0 + ) # €/MW/a, +20% compared to LNG n.madd( "Generator", @@ -3525,7 +3559,7 @@ def add_import_options( # need special handling for copperplated imports copperplated_options = { "shipping-ftfuel", - #"shipping-meoh", + # "shipping-meoh", # "shipping-steel", } @@ -3816,7 +3850,7 @@ def set_temporal_aggregation(n, opts, solver_name): return n -def lossy_bidirectional_links(n, carrier, losses_per_thousand_km=0.): +def lossy_bidirectional_links(n, carrier, losses_per_thousand_km=0.0): "Split bidirectional links into two unidirectional links to include transmission losses." carrier_i = n.links.query("carrier == @carrier").index @@ -3824,13 +3858,19 @@ def lossy_bidirectional_links(n, carrier, losses_per_thousand_km=0.): if not losses_per_thousand_km or carrier_i.empty: return - logger.info(f"Specified losses for {carrier} transmission. Splitting bidirectional links.") + logger.info( + f"Specified losses for {carrier} transmission. Splitting bidirectional links." + ) carrier_i = n.links.query("carrier == @carrier").index n.links.loc[carrier_i, "p_min_pu"] = 0 n.links["reversed"] = False - n.links.loc[carrier_i, "efficiency"] = 1 - n.links.loc[carrier_i, "length"] * losses_per_thousand_km / 1e3 - rev_links = n.links.loc[carrier_i].copy().rename({"bus0": "bus1", "bus1": "bus0"}, axis=1) + n.links.loc[carrier_i, "efficiency"] = ( + 1 - n.links.loc[carrier_i, "length"] * losses_per_thousand_km / 1e3 + ) + rev_links = ( + n.links.loc[carrier_i].copy().rename({"bus0": "bus1", "bus1": "bus0"}, axis=1) + ) rev_links.capital_cost = 0 rev_links.reversed = True rev_links.index = rev_links.index.map(lambda x: x + "-reversed") diff --git a/scripts/solve_network.py b/scripts/solve_network.py index 68ee69702..3956c2ba3 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -494,11 +494,15 @@ def add_battery_constraints(n): def add_lossy_bidirectional_link_constraints(n): if not n.links.p_nom_extendable.any() or not "reversed" in n.links.columns: return - + carriers = n.links.loc[n.links.reversed, "carrier"].unique() - backward_i = n.links.query("carrier in @carriers and reversed and p_nom_extendable").index - forward_i = n.links.query("carrier in @carriers and ~reversed and p_nom_extendable").index + backward_i = n.links.query( + "carrier in @carriers and reversed and p_nom_extendable" + ).index + forward_i = n.links.query( + "carrier in @carriers and ~reversed and p_nom_extendable" + ).index assert len(forward_i) == len(backward_i) From c1584e4ce40733795cfeba61ca3bb050b8df2f5e Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 3 Aug 2023 13:09:12 +0200 Subject: [PATCH 068/293] fix capacity synchronisation between forward and backward lossy links --- scripts/prepare_sector_network.py | 4 ++-- scripts/solve_network.py | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 996ddb3c2..a6345a7a1 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3828,14 +3828,14 @@ def lossy_bidirectional_links(n, carrier, losses_per_thousand_km=0.): carrier_i = n.links.query("carrier == @carrier").index n.links.loc[carrier_i, "p_min_pu"] = 0 - n.links["reversed"] = False n.links.loc[carrier_i, "efficiency"] = 1 - n.links.loc[carrier_i, "length"] * losses_per_thousand_km / 1e3 rev_links = n.links.loc[carrier_i].copy().rename({"bus0": "bus1", "bus1": "bus0"}, axis=1) rev_links.capital_cost = 0 - rev_links.reversed = True + rev_links["reversed"] = True rev_links.index = rev_links.index.map(lambda x: x + "-reversed") n.links = pd.concat([n.links, rev_links], sort=False) + n.links["reversed"] = n.links["reversed"].fillna(False) if __name__ == "__main__": diff --git a/scripts/solve_network.py b/scripts/solve_network.py index 68ee69702..7729c78a2 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -497,10 +497,8 @@ def add_lossy_bidirectional_link_constraints(n): carriers = n.links.loc[n.links.reversed, "carrier"].unique() - backward_i = n.links.query("carrier in @carriers and reversed and p_nom_extendable").index forward_i = n.links.query("carrier in @carriers and ~reversed and p_nom_extendable").index - - assert len(forward_i) == len(backward_i) + backward_i = forward_i + "-reversed" lhs = n.model["Link-p_nom"].loc[backward_i] rhs = n.model["Link-p_nom"].loc[forward_i] From 6ac7d86f5644e1852766811a348b4ed9d562e402 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 3 Aug 2023 13:43:30 +0000 Subject: [PATCH 069/293] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- config.default.yaml | 164 +++++++++++++++--------------- rules/build_sector.smk | 3 +- scripts/prepare_sector_network.py | 99 +++++++++++++----- 3 files changed, 155 insertions(+), 111 deletions(-) diff --git a/config.default.yaml b/config.default.yaml index 0d617020d..decf8f507 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -13,17 +13,17 @@ foresight: overnight # options are overnight, myopic, perfect (perfect is not ye scenario: simpl: # only relevant for PyPSA-Eur - - '' + - '' lv: # allowed transmission line volume expansion, can be any float >= 1.0 (today) or "opt" - - 1.0 - - 1.5 + - 1.0 + - 1.5 clusters: # number of nodes in Europe, any integer between 37 (1 node per country-zone) and several hundred - - 45 - - 50 + - 45 + - 50 opts: # only relevant for PyPSA-Eur - - '' + - '' sector_opts: # this is where the main scenario settings are - - Co2L0-3H-T-H-B-I-A-solar+p3-dist1 + - Co2L0-3H-T-H-B-I-A-solar+p3-dist1 # to really understand the options here, look in scripts/prepare_sector_network.py # Co2Lx specifies the CO2 target in x% of the 1990 values; default will give default (5%); # Co2L0p25 will give 25% CO2 emissions; Co2Lm0p05 will give 5% negative emissions @@ -41,7 +41,7 @@ scenario: # cb40ex0 distributes a carbon budget of 40 GtCO2 following an exponential # decay with initial growth rate 0 planning_horizons: # investment years for myopic and perfect; for overnight, year of cost assumptions can be different and is defined under 'costs' - - 2050 + - 2050 # for example, set to # - 2020 # - 2030 @@ -84,18 +84,18 @@ electricity: # in PyPSA-Eur-Sec pypsa_eur: Bus: - - AC + - AC Link: - - DC + - DC Generator: - - onwind - - offwind-ac - - offwind-dc - - solar - - ror + - onwind + - offwind-ac + - offwind-dc + - solar + - ror StorageUnit: - - PHS - - hydro + - PHS + - hydro Store: [] @@ -110,25 +110,25 @@ biomass: scenario: ENS_Med classes: solid biomass: - - Agricultural waste - - Fuelwood residues - - Secondary Forestry residues - woodchips - - Sawdust - - Residues from landscape care - - Municipal waste + - Agricultural waste + - Fuelwood residues + - Secondary Forestry residues - woodchips + - Sawdust + - Residues from landscape care + - Municipal waste not included: - - Sugar from sugar beet - - Rape seed - - "Sunflower, soya seed " - - Bioethanol barley, wheat, grain maize, oats, other cereals and rye - - Miscanthus, switchgrass, RCG - - Willow - - Poplar - - FuelwoodRW - - C&P_RW + - Sugar from sugar beet + - Rape seed + - "Sunflower, soya seed " + - Bioethanol barley, wheat, grain maize, oats, other cereals and rye + - Miscanthus, switchgrass, RCG + - Willow + - Poplar + - FuelwoodRW + - C&P_RW biogas: - - Manure solid, liquid - - Sludge + - Manure solid, liquid + - Sludge solar_thermal: @@ -143,10 +143,10 @@ existing_capacities: grouping_years_heat: [1980, 1985, 1990, 1995, 2000, 2005, 2010, 2015, 2019] # these should not extend 2020 threshold_capacity: 10 conventional_carriers: - - lignite - - coal - - oil - - uranium + - lignite + - coal + - oil + - uranium sector: @@ -195,7 +195,7 @@ sector: agriculture_machinery_electric_efficiency: 0.3 # electricity per use shipping_average_efficiency: 0.4 #For conversion of fuel oil to propulsion in 2011 shipping_hydrogen_liquefaction: false # whether to consider liquefaction costs for shipping H2 demands - shipping_hydrogen_share: 0 + shipping_hydrogen_share: 0 time_dep_hp_cop: true #time dependent heat pump coefficient of performance heat_pump_sink_T: 55. # Celsius, based on DTU / large area radiators; used in build_cop_profiles.py # conservatively high to cover hot water and space heating in poorly-insulated buildings @@ -211,7 +211,7 @@ sector: 2040: 0.16 2045: 0.21 2050: 0.29 - retrofitting : # co-optimises building renovation to reduce space heat demand + retrofitting: # co-optimises building renovation to reduce space heat demand retro_endogen: false # co-optimise space heat savings cost_factor: 1.0 # weight costs for building renovation interest_rate: 0.04 # for investment in building components @@ -243,7 +243,7 @@ sector: hydrogen_underground_storage: true hydrogen_underground_storage_locations: # - onshore # more than 50 km from sea - - nearshore # within 50 km of sea + - nearshore # within 50 km of sea # - offshore ammonia: false # can be false (no NH3 carrier), true (copperplated NH3), "regional" (regionalised NH3 without network) use_fischer_tropsch_waste_heat: true @@ -361,14 +361,14 @@ solving: min_iterations: 4 max_iterations: 6 keep_shadowprices: - - Bus - - Line - - Link - - Transformer - - GlobalConstraint - - Generator - - Store - - StorageUnit + - Bus + - Line + - Link + - Transformer + - GlobalConstraint + - Generator + - Store + - StorageUnit solver: name: gurobi @@ -406,47 +406,47 @@ plotting: energy_min: -20000 energy_threshold: 50 vre_techs: - - onwind - - offwind-ac - - offwind-dc - - solar - - ror + - onwind + - offwind-ac + - offwind-dc + - solar + - ror renewable_storage_techs: - - PHS - - hydro + - PHS + - hydro conv_techs: - - OCGT - - CCGT - - Nuclear - - Coal + - OCGT + - CCGT + - Nuclear + - Coal storage_techs: - - hydro+PHS - - battery - - H2 + - hydro+PHS + - battery + - H2 load_carriers: - - AC load + - AC load AC_carriers: - - AC line - - AC transformer + - AC line + - AC transformer link_carriers: - - DC line - - Converter AC-DC + - DC line + - Converter AC-DC heat_links: - - heat pump - - resistive heater - - CHP heat - - CHP electric - - gas boiler - - central heat pump - - central resistive heater - - central CHP heat - - central CHP electric - - central gas boiler + - heat pump + - resistive heater + - CHP heat + - CHP electric + - gas boiler + - central heat pump + - central resistive heater + - central CHP heat + - central CHP electric + - central gas boiler heat_generators: - - gas boiler - - central gas boiler - - solar thermal collector - - central solar thermal collector + - gas boiler + - central gas boiler + - solar thermal collector + - central solar thermal collector tech_colors: # wind onwind: "#235ebc" diff --git a/rules/build_sector.smk b/rules/build_sector.smk index aec204a97..662ab955f 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -749,7 +749,8 @@ rule prepare_sector_network: + "industrial_production_elec_s{simpl}_{clusters}_{planning_horizons}.csv", industrial_demand=RESOURCES + "industrial_energy_demand_elec_s{simpl}_{clusters}_{planning_horizons}.csv", - industrial_demand_today=RESOURCES + "industrial_energy_demand_today_elec_s{simpl}_{clusters}.csv", + industrial_demand_today=RESOURCES + + "industrial_energy_demand_today_elec_s{simpl}_{clusters}.csv", industry_sector_ratios=RESOURCES + "industry_sector_ratios.csv", heat_demand_urban=RESOURCES + "heat_demand_urban_elec_s{simpl}_{clusters}.nc", heat_demand_rural=RESOURCES + "heat_demand_rural_elec_s{simpl}_{clusters}.nc", diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 57f02fe78..ddf5aa22d 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2375,34 +2375,46 @@ def add_industry(n, costs): nyears = nhours / 8760 # 1e6 to convert TWh to MWh - industrial_demand = pd.read_csv(snakemake.input.industrial_demand, index_col=[0,1]) * 1e6 * nyears - industrial_demand_today = pd.read_csv(snakemake.input.industrial_demand_today, index_col=0) * 1e6 * nyears + industrial_demand = ( + pd.read_csv(snakemake.input.industrial_demand, index_col=[0, 1]) * 1e6 * nyears + ) + industrial_demand_today = ( + pd.read_csv(snakemake.input.industrial_demand_today, index_col=0) * 1e6 * nyears + ) - industrial_production = pd.read_csv(snakemake.input.industrial_production, index_col=0) * nyears - industry_sector_ratios = pd.read_csv(snakemake.input.industry_sector_ratios, index_col=0) + industrial_production = ( + pd.read_csv(snakemake.input.industrial_production, index_col=0) * nyears + ) + industry_sector_ratios = pd.read_csv( + snakemake.input.industry_sector_ratios, index_col=0 + ) endogenous_sectors = ["DRI + Electric arc"] - sectors_b = ~industrial_demand.index.get_level_values('sector').isin(endogenous_sectors) + sectors_b = ~industrial_demand.index.get_level_values("sector").isin( + endogenous_sectors + ) if options["endogenous_steel"]: - sector = "DRI + Electric arc" - n.add("Bus", + n.add( + "Bus", "EU steel", location="EU", carrier="steel", unit="t", ) - n.add("Load", + n.add( + "Load", "EU steel", bus="EU steel", carrier="steel", - p_set=industrial_production[sector].sum() / 8760 + p_set=industrial_production[sector].sum() / 8760, ) - n.add("Store", + n.add( + "Store", "EU steel Store", bus="EU steel", e_nom_extendable=True, @@ -2412,7 +2424,8 @@ def add_industry(n, costs): ratio = industry_sector_ratios[sector] - n.madd("Link", + n.madd( + "Link", nodes, suffix=f" {sector}", carrier=sector, @@ -2426,9 +2439,9 @@ def add_industry(n, costs): bus3=spatial.gas.nodes, bus4="co2 atmosphere", efficiency=1 / ratio["elec"], - efficiency2=- ratio["hydrogen"] / ratio["elec"], - efficiency3=- ratio["methane"] / ratio["elec"], - efficiency4=ratio["process emission"] / ratio["elec"] + efficiency2=-ratio["hydrogen"] / ratio["elec"], + efficiency3=-ratio["methane"] / ratio["elec"], + efficiency4=ratio["process emission"] / ratio["elec"], ) # TODO specific consumption of heat (small), requires extension to bus5 @@ -2442,9 +2455,12 @@ def add_industry(n, costs): if options.get("biomass_spatial", options["biomass_transport"]): p_set = ( - industrial_demand.loc[(spatial.biomass.locations, sectors_b), "solid biomass"].groupby(level='nodes').sum().rename( - index=lambda x: x + " solid biomass for industry" - ) + industrial_demand.loc[ + (spatial.biomass.locations, sectors_b), "solid biomass" + ] + .groupby(level="nodes") + .sum() + .rename(index=lambda x: x + " solid biomass for industry") / nhours ) else: @@ -2495,7 +2511,10 @@ def add_industry(n, costs): unit="MWh_LHV", ) - gas_demand = industrial_demand.loc[(nodes, sectors_b), "methane"].groupby(level='node').sum() / nhours + gas_demand = ( + industrial_demand.loc[(nodes, sectors_b), "methane"].groupby(level="node").sum() + / nhours + ) if options["gas_network"]: spatial_gas_demand = gas_demand.rename(index=lambda x: x + " gas for industry") @@ -2547,7 +2566,10 @@ def add_industry(n, costs): suffix=" H2 for industry", bus=nodes + " H2", carrier="H2 for industry", - p_set=industrial_demand.loc[(nodes, sectors_b), "hydrogen"].groupby(level='node').sum() / nhours, + p_set=industrial_demand.loc[(nodes, sectors_b), "hydrogen"] + .groupby(level="node") + .sum() + / nhours, ) shipping_hydrogen_share = get(options["shipping_hydrogen_share"], investment_year) @@ -2774,7 +2796,11 @@ def add_industry(n, costs): ) demand_factor = options.get("HVC_demand_factor", 1) - p_set = demand_factor * industrial_demand.loc[(nodes, sectors_b), "naphtha"].sum() / nhours + p_set = ( + demand_factor + * industrial_demand.loc[(nodes, sectors_b), "naphtha"].sum() + / nhours + ) if demand_factor != 1: logger.warning(f"Changing HVC demand by {demand_factor*100-100:+.2f}%.") @@ -2811,7 +2837,10 @@ def add_industry(n, costs): co2_release = ["naphtha for industry", "kerosene for aviation"] co2 = ( n.loads.loc[co2_release, "p_set"].sum() * costs.at["oil", "CO2 intensity"] - - industrial_demand.loc[(nodes, sectors_b), "process emission from feedstock"].sum() / nhours + - industrial_demand.loc[ + (nodes, sectors_b), "process emission from feedstock" + ].sum() + / nhours ) n.add( @@ -2834,7 +2863,10 @@ def add_industry(n, costs): for node in nodes ], carrier="low-temperature heat for industry", - p_set=industrial_demand.loc[(nodes, sectors_b), "low-temperature heat"].groupby(level='node').sum() / nhours + p_set=industrial_demand.loc[(nodes, sectors_b), "low-temperature heat"] + .groupby(level="node") + .sum() + / nhours, ) # remove today's industrial electricity demand by scaling down total electricity demand @@ -2859,7 +2891,10 @@ def add_industry(n, costs): suffix=" industry electricity", bus=nodes, carrier="industry electricity", - p_set=industrial_demand.loc[(nodes, sectors_b), "electricity"].groupby(level='node').sum() / nhours + p_set=industrial_demand.loc[(nodes, sectors_b), "electricity"] + .groupby(level="node") + .sum() + / nhours, ) n.madd( @@ -2874,13 +2909,16 @@ def add_industry(n, costs): if options["co2_spatial"] or options["co2network"]: p_set = ( -industrial_demand.loc[(nodes, sectors_b), sel] - .groupby(level='node').sum() + .groupby(level="node") + .sum() .sum(axis=1) .rename(index=lambda x: x + " process emissions") / nhours ) else: - p_set = -industrial_demand.loc[(nodes, sectors_b), sel].sum(axis=1).sum() / nhours + p_set = ( + -industrial_demand.loc[(nodes, sectors_b), sel].sum(axis=1).sum() / nhours + ) # this should be process emissions fossil+feedstock # then need load on atmosphere for feedstock emissions that are currently going to atmosphere via Link Fischer-Tropsch demand @@ -2919,9 +2957,14 @@ def add_industry(n, costs): ) if options.get("ammonia"): - - if options["ammonia"] == 'regional': - p_set = industrial_demand.loc[(spatial.ammonia.locations, sectors_b), "ammonia"].groupby(level='node').sum().rename(index=lambda x: x + " NH3") / nhours + if options["ammonia"] == "regional": + p_set = ( + industrial_demand.loc[(spatial.ammonia.locations, sectors_b), "ammonia"] + .groupby(level="node") + .sum() + .rename(index=lambda x: x + " NH3") + / nhours + ) else: p_set = industrial_demand.loc[sectors_b, "ammonia"].sum() / nhours From 55e9bbab4459e9b11a347317ff68ebbffe4996ca Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 3 Aug 2023 15:45:17 +0200 Subject: [PATCH 070/293] delete old config.default.yaml --- config.default.yaml | 628 -------------------------------------------- 1 file changed, 628 deletions(-) delete mode 100644 config.default.yaml diff --git a/config.default.yaml b/config.default.yaml deleted file mode 100644 index decf8f507..000000000 --- a/config.default.yaml +++ /dev/null @@ -1,628 +0,0 @@ -version: 0.6.0 - -logging_level: INFO - -retrieve_sector_databundle: true - -results_dir: results/ -summary_dir: results -costs_dir: ../technology-data/outputs/ -run: your-run-name # use this to keep track of runs with different settings -foresight: overnight # options are overnight, myopic, perfect (perfect is not yet implemented) -# if you use myopic or perfect foresight, set the investment years in "planning_horizons" below - -scenario: - simpl: # only relevant for PyPSA-Eur - - '' - lv: # allowed transmission line volume expansion, can be any float >= 1.0 (today) or "opt" - - 1.0 - - 1.5 - clusters: # number of nodes in Europe, any integer between 37 (1 node per country-zone) and several hundred - - 45 - - 50 - opts: # only relevant for PyPSA-Eur - - '' - sector_opts: # this is where the main scenario settings are - - Co2L0-3H-T-H-B-I-A-solar+p3-dist1 - # to really understand the options here, look in scripts/prepare_sector_network.py - # Co2Lx specifies the CO2 target in x% of the 1990 values; default will give default (5%); - # Co2L0p25 will give 25% CO2 emissions; Co2Lm0p05 will give 5% negative emissions - # xH is the temporal resolution; 3H is 3-hourly, i.e. one snapshot every 3 hours - # single letters are sectors: T for land transport, H for building heating, - # B for biomass supply, I for industry, shipping and aviation, - # A for agriculture, forestry and fishing - # solar+c0.5 reduces the capital cost of solar to 50\% of reference value - # solar+p3 multiplies the available installable potential by factor 3 - # seq400 sets the potential of CO2 sequestration to 400 Mt CO2 per year - # dist{n} includes distribution grids with investment cost of n times cost in data/costs.csv - # for myopic/perfect foresight cb states the carbon budget in GtCO2 (cumulative - # emissions throughout the transition path in the timeframe determined by the - # planning_horizons), be:beta decay; ex:exponential decay - # cb40ex0 distributes a carbon budget of 40 GtCO2 following an exponential - # decay with initial growth rate 0 - planning_horizons: # investment years for myopic and perfect; for overnight, year of cost assumptions can be different and is defined under 'costs' - - 2050 - # for example, set to - # - 2020 - # - 2030 - # - 2040 - # - 2050 - # for myopic foresight - -# CO2 budget as a fraction of 1990 emissions -# this is over-ridden if CO2Lx is set in sector_opts -# this is also over-ridden if cb is set in sector_opts -co2_budget: - 2020: 0.7011648746 - 2025: 0.5241935484 - 2030: 0.2970430108 - 2035: 0.1500896057 - 2040: 0.0712365591 - 2045: 0.0322580645 - 2050: 0 - -# snapshots are originally set in PyPSA-Eur/config.yaml but used again by PyPSA-Eur-Sec -snapshots: - # arguments to pd.date_range - start: "2013-01-01" - end: "2014-01-01" - closed: left # end is not inclusive - -atlite: - cutout: ../pypsa-eur/cutouts/europe-2013-era5.nc - -# this information is NOT used but needed as an argument for -# pypsa-eur/scripts/add_electricity.py/load_costs in make_summary.py -electricity: - max_hours: - battery: 6 - H2: 168 - -# regulate what components with which carriers are kept from PyPSA-Eur; -# some technologies are removed because they are implemented differently -# (e.g. battery or H2 storage) or have different year-dependent costs -# in PyPSA-Eur-Sec -pypsa_eur: - Bus: - - AC - Link: - - DC - Generator: - - onwind - - offwind-ac - - offwind-dc - - solar - - ror - StorageUnit: - - PHS - - hydro - Store: [] - - -energy: - energy_totals_year: 2011 - base_emissions_year: 1990 - eurostat_report_year: 2016 - emissions: CO2 # "CO2" or "All greenhouse gases - (CO2 equivalent)" - -biomass: - year: 2030 - scenario: ENS_Med - classes: - solid biomass: - - Agricultural waste - - Fuelwood residues - - Secondary Forestry residues - woodchips - - Sawdust - - Residues from landscape care - - Municipal waste - not included: - - Sugar from sugar beet - - Rape seed - - "Sunflower, soya seed " - - Bioethanol barley, wheat, grain maize, oats, other cereals and rye - - Miscanthus, switchgrass, RCG - - Willow - - Poplar - - FuelwoodRW - - C&P_RW - biogas: - - Manure solid, liquid - - Sludge - - -solar_thermal: - clearsky_model: simple # should be "simple" or "enhanced"? - orientation: - slope: 45. - azimuth: 180. - -# only relevant for foresight = myopic or perfect -existing_capacities: - grouping_years_power: [1980, 1985, 1990, 1995, 2000, 2005, 2010, 2015, 2020, 2025, 2030] - grouping_years_heat: [1980, 1985, 1990, 1995, 2000, 2005, 2010, 2015, 2019] # these should not extend 2020 - threshold_capacity: 10 - conventional_carriers: - - lignite - - coal - - oil - - uranium - - -sector: - district_heating: - potential: 0.6 # maximum fraction of urban demand which can be supplied by district heating - # 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 - progress: - 2020: 0.0 - 2030: 0.3 - 2040: 0.6 - 2050: 1.0 - district_heating_loss: 0.15 - bev_dsm_restriction_value: 0.75 #Set to 0 for no restriction on BEV DSM - bev_dsm_restriction_time: 7 #Time at which SOC of BEV has to be dsm_restriction_value - transport_heating_deadband_upper: 20. - transport_heating_deadband_lower: 15. - ICE_lower_degree_factor: 0.375 #in per cent increase in fuel consumption per degree above deadband - ICE_upper_degree_factor: 1.6 - EV_lower_degree_factor: 0.98 - EV_upper_degree_factor: 0.63 - bev_dsm: true #turns on EV battery - bev_availability: 0.5 #How many cars do smart charging - bev_energy: 0.05 #average battery size in MWh - bev_charge_efficiency: 0.9 #BEV (dis-)charging efficiency - bev_plug_to_wheel_efficiency: 0.2 #kWh/km from EPA https://www.fueleconomy.gov/feg/ for Tesla Model S - bev_charge_rate: 0.011 #3-phase charger with 11 kW - bev_avail_max: 0.95 - bev_avail_mean: 0.8 - v2g: true #allows feed-in to grid from EV battery - #what is not EV or FCEV is oil-fuelled ICE - land_transport_fuel_cell_share: # 1 means all FCEVs - 2020: 0 - 2030: 0.05 - 2040: 0.1 - 2050: 0.15 - land_transport_electric_share: # 1 means all EVs - 2020: 0 - 2030: 0.25 - 2040: 0.6 - 2050: 0.85 - transport_fuel_cell_efficiency: 0.5 - transport_internal_combustion_efficiency: 0.3 - agriculture_machinery_electric_share: 0 - agriculture_machinery_fuel_efficiency: 0.7 # fuel oil per use - agriculture_machinery_electric_efficiency: 0.3 # electricity per use - shipping_average_efficiency: 0.4 #For conversion of fuel oil to propulsion in 2011 - shipping_hydrogen_liquefaction: false # whether to consider liquefaction costs for shipping H2 demands - shipping_hydrogen_share: 0 - time_dep_hp_cop: true #time dependent heat pump coefficient of performance - heat_pump_sink_T: 55. # Celsius, based on DTU / large area radiators; used in build_cop_profiles.py - # conservatively high to cover hot water and space heating in poorly-insulated buildings - reduce_space_heat_exogenously: true # reduces space heat demand by a given factor (applied before losses in DH) - # this can represent e.g. building renovation, building demolition, or if - # the factor is negative: increasing floor area, increased thermal comfort, population growth - reduce_space_heat_exogenously_factor: # per unit reduction in space heat demand - # the default factors are determined by the LTS scenario from http://tool.european-calculator.eu/app/buildings/building-types-area/?levers=1ddd4444421213bdbbbddd44444ffffff11f411111221111211l212221 - 2020: 0.10 # this results in a space heat demand reduction of 10% - 2025: 0.09 # first heat demand increases compared to 2020 because of larger floor area per capita - 2030: 0.09 - 2035: 0.11 - 2040: 0.16 - 2045: 0.21 - 2050: 0.29 - retrofitting: # co-optimises building renovation to reduce space heat demand - retro_endogen: false # co-optimise space heat savings - cost_factor: 1.0 # weight costs for building renovation - interest_rate: 0.04 # for investment in building components - annualise_cost: true # annualise the investment costs - tax_weighting: false # weight costs depending on taxes in countries - construction_index: true # weight costs depending on labour/material costs per country - tes: true - tes_tau: # 180 day time constant for centralised, 3 day for decentralised - decentral: 3 - central: 180 - boilers: true - oil_boilers: false - biomass_boiler: true - chp: true - micro_chp: false - solar_thermal: true - solar_cf_correction: 0.788457 # = >>> 1/1.2683 - marginal_cost_storage: 0. #1e-4 - methanation: true - helmeth: true - coal_cc: false - dac: true - co2_vent: false - SMR: true - co2_sequestration_potential: 200 #MtCO2/a sequestration potential for Europe - co2_sequestration_cost: 10 #EUR/tCO2 for sequestration of CO2 - co2_network: false - cc_fraction: 0.9 # default fraction of CO2 captured with post-combustion capture - hydrogen_underground_storage: true - hydrogen_underground_storage_locations: - # - onshore # more than 50 km from sea - - nearshore # within 50 km of sea - # - offshore - ammonia: false # can be false (no NH3 carrier), true (copperplated NH3), "regional" (regionalised NH3 without network) - use_fischer_tropsch_waste_heat: true - use_fuel_cell_waste_heat: true - electricity_distribution_grid: true - electricity_distribution_grid_cost_factor: 1.0 #multiplies cost in data/costs.csv - electricity_grid_connection: true # only applies to onshore wind and utility PV - H2_network: true - gas_network: false - H2_retrofit: false # if set to True existing gas pipes can be retrofitted to H2 pipes - # according to hydrogen backbone strategy (April, 2020) p.15 - # https://gasforclimate2050.eu/wp-content/uploads/2020/07/2020_European-Hydrogen-Backbone_Report.pdf - # 60% of original natural gas capacity could be used in cost-optimal case as H2 capacity - H2_retrofit_capacity_per_CH4: 0.6 # ratio for H2 capacity per original CH4 capacity of retrofitted pipelines - gas_network_connectivity_upgrade: 1 # https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.connectivity.edge_augmentation.k_edge_augmentation.html#networkx.algorithms.connectivity.edge_augmentation.k_edge_augmentation - gas_distribution_grid: true - gas_distribution_grid_cost_factor: 1.0 #multiplies cost in data/costs.csv - biomass_transport: false # biomass transport between nodes - conventional_generation: # generator : carrier - OCGT: gas - biomass_to_liquid: false - biosng: false - endogenous_steel: false - - - -industry: - St_primary_fraction: # fraction of steel produced via primary route versus secondary route (scrap+EAF); today fraction is 0.6 - 2020: 0.6 - 2025: 0.55 - 2030: 0.5 - 2035: 0.45 - 2040: 0.4 - 2045: 0.35 - 2050: 0.3 - DRI_fraction: # fraction of the primary route converted to DRI + EAF - 2020: 0 - 2025: 0 - 2030: 0.05 - 2035: 0.2 - 2040: 0.4 - 2045: 0.7 - 2050: 1 - H2_DRI: 1.7 #H2 consumption in Direct Reduced Iron (DRI), MWh_H2,LHV/ton_Steel from 51kgH2/tSt in Vogl et al (2018) doi:10.1016/j.jclepro.2018.08.279 - elec_DRI: 0.322 #electricity consumption in Direct Reduced Iron (DRI) shaft, MWh/tSt HYBRIT brochure https://ssabwebsitecdn.azureedge.net/-/media/hybrit/files/hybrit_brochure.pdf - Al_primary_fraction: # fraction of aluminium produced via the primary route versus scrap; today fraction is 0.4 - 2020: 0.4 - 2025: 0.375 - 2030: 0.35 - 2035: 0.325 - 2040: 0.3 - 2045: 0.25 - 2050: 0.2 - MWh_NH3_per_tNH3: 5.166 # LHV - MWh_CH4_per_tNH3_SMR: 10.8 # 2012's demand from https://ec.europa.eu/docsroom/documents/4165/attachments/1/translations/en/renditions/pdf - MWh_elec_per_tNH3_SMR: 0.7 # same source, assuming 94-6% split methane-elec of total energy demand 11.5 MWh/tNH3 - MWh_H2_per_tNH3_electrolysis: 6.5 # from https://doi.org/10.1016/j.joule.2018.04.017, around 0.197 tH2/tHN3 (>3/17 since some H2 lost and used for energy) - MWh_elec_per_tNH3_electrolysis: 1.17 # from https://doi.org/10.1016/j.joule.2018.04.017 Table 13 (air separation and HB) - MWh_NH3_per_MWh_H2_cracker: 1.46 # https://github.com/euronion/trace/blob/44a5ff8401762edbef80eff9cfe5a47c8d3c8be4/data/efficiencies.csv - NH3_process_emissions: 24.5 # in MtCO2/a from SMR for H2 production for NH3 from UNFCCC for 2015 for EU28 - petrochemical_process_emissions: 25.5 # in MtCO2/a for petrochemical and other from UNFCCC for 2015 for EU28 - HVC_primary_fraction: 1. # fraction of today's HVC produced via primary route - HVC_mechanical_recycling_fraction: 0. # fraction of today's HVC produced via mechanical recycling - HVC_chemical_recycling_fraction: 0. # fraction of today's HVC produced via chemical recycling - HVC_production_today: 52. # MtHVC/a from DECHEMA (2017), Figure 16, page 107; includes ethylene, propylene and BTX - MWh_elec_per_tHVC_mechanical_recycling: 0.547 # from SI of https://doi.org/10.1016/j.resconrec.2020.105010, Table S5, for HDPE, PP, PS, PET. LDPE would be 0.756. - MWh_elec_per_tHVC_chemical_recycling: 6.9 # Material Economics (2019), page 125; based on pyrolysis and electric steam cracking - chlorine_production_today: 9.58 # MtCl/a from DECHEMA (2017), Table 7, page 43 - MWh_elec_per_tCl: 3.6 # DECHEMA (2017), Table 6, page 43 - MWh_H2_per_tCl: -0.9372 # DECHEMA (2017), page 43; negative since hydrogen produced in chloralkali process - methanol_production_today: 1.5 # MtMeOH/a from DECHEMA (2017), page 62 - MWh_elec_per_tMeOH: 0.167 # DECHEMA (2017), Table 14, page 65 - MWh_CH4_per_tMeOH: 10.25 # DECHEMA (2017), Table 14, page 65 - hotmaps_locate_missing: false - reference_year: 2015 - # references: - # DECHEMA (2017): https://dechema.de/dechema_media/Downloads/Positionspapiere/Technology_study_Low_carbon_energy_and_feedstock_for_the_European_chemical_industry-p-20002750.pdf - # Material Economics (2019): https://materialeconomics.com/latest-updates/industrial-transformation-2050 - -costs: - year: 2030 - lifetime: 25 #default lifetime - # From a Lion Hirth paper, also reflects average of Noothout et al 2016 - discountrate: 0.07 - # [EUR/USD] ECB: https://www.ecb.europa.eu/stats/exchange/eurofxref/html/eurofxref-graph-usd.en.html # noqa: E501 - USD2013_to_EUR2013: 0.7532 - - # Marginal and capital costs can be overwritten - # capital_cost: - # onwind: 500 - marginal_cost: - solar: 0.01 - onwind: 0.015 - offwind: 0.015 - hydro: 0. - H2: 0. - battery: 0. - - emission_prices: # only used with the option Ep (emission prices) - co2: 0. - - lines: - length_factor: 1.25 #to estimate offwind connection costs - - -solving: - #tmpdir: "path/to/tmp" - options: - formulation: kirchhoff - clip_p_max_pu: 1.e-2 - load_shedding: false - noisy_costs: true - skip_iterations: true - track_iterations: false - min_iterations: 4 - max_iterations: 6 - keep_shadowprices: - - Bus - - Line - - Link - - Transformer - - GlobalConstraint - - Generator - - Store - - StorageUnit - - solver: - name: gurobi - threads: 4 - method: 2 # barrier - crossover: 0 - BarConvTol: 1.e-6 - Seed: 123 - AggFill: 0 - PreDual: 0 - GURO_PAR_BARDENSETHRESH: 200 - #FeasibilityTol: 1.e-6 - - #name: cplex - #threads: 4 - #lpmethod: 4 # barrier - #solutiontype: 2 # non basic solution, ie no crossover - #barrier_convergetol: 1.e-5 - #feasopt_tolerance: 1.e-6 - mem: 30000 #memory in MB; 20 GB enough for 50+B+I+H2; 100 GB for 181+B+I+H2 - - -plotting: - map: - boundaries: [-11, 30, 34, 71] - color_geomap: - ocean: white - land: whitesmoke - eu_node_location: - x: -5.5 - y: 46. - costs_max: 1000 - costs_threshold: 1 - energy_max: 20000 - energy_min: -20000 - energy_threshold: 50 - vre_techs: - - onwind - - offwind-ac - - offwind-dc - - solar - - ror - renewable_storage_techs: - - PHS - - hydro - conv_techs: - - OCGT - - CCGT - - Nuclear - - Coal - storage_techs: - - hydro+PHS - - battery - - H2 - load_carriers: - - AC load - AC_carriers: - - AC line - - AC transformer - link_carriers: - - DC line - - Converter AC-DC - heat_links: - - heat pump - - resistive heater - - CHP heat - - CHP electric - - gas boiler - - central heat pump - - central resistive heater - - central CHP heat - - central CHP electric - - central gas boiler - heat_generators: - - gas boiler - - central gas boiler - - solar thermal collector - - central solar thermal collector - tech_colors: - # wind - onwind: "#235ebc" - onshore wind: "#235ebc" - offwind: "#6895dd" - offshore wind: "#6895dd" - offwind-ac: "#6895dd" - offshore wind (AC): "#6895dd" - offwind-dc: "#74c6f2" - offshore wind (DC): "#74c6f2" - # water - hydro: '#298c81' - hydro reservoir: '#298c81' - ror: '#3dbfb0' - run of river: '#3dbfb0' - hydroelectricity: '#298c81' - PHS: '#51dbcc' - wave: '#a7d4cf' - # solar - solar: "#f9d002" - solar PV: "#f9d002" - solar thermal: '#ffbf2b' - solar rooftop: '#ffea80' - # gas - OCGT: '#e0986c' - OCGT marginal: '#e0986c' - OCGT-heat: '#e0986c' - gas boiler: '#db6a25' - gas boilers: '#db6a25' - gas boiler marginal: '#db6a25' - gas: '#e05b09' - fossil gas: '#e05b09' - natural gas: '#e05b09' - CCGT: '#a85522' - CCGT marginal: '#a85522' - gas for industry co2 to atmosphere: '#692e0a' - gas for industry co2 to stored: '#8a3400' - gas for industry: '#853403' - gas for industry CC: '#692e0a' - gas pipeline: '#ebbca0' - gas pipeline new: '#a87c62' - # oil - oil: '#c9c9c9' - oil boiler: '#adadad' - agriculture machinery oil: '#949494' - shipping oil: "#808080" - land transport oil: '#afafaf' - # nuclear - Nuclear: '#ff8c00' - Nuclear marginal: '#ff8c00' - nuclear: '#ff8c00' - uranium: '#ff8c00' - # coal - Coal: '#545454' - coal: '#545454' - Coal marginal: '#545454' - solid: '#545454' - Lignite: '#826837' - lignite: '#826837' - Lignite marginal: '#826837' - # biomass - biogas: '#e3d37d' - biomass: '#baa741' - solid biomass: '#baa741' - solid biomass transport: '#baa741' - solid biomass for industry: '#7a6d26' - solid biomass for industry CC: '#47411c' - solid biomass for industry co2 from atmosphere: '#736412' - solid biomass for industry co2 to stored: '#47411c' - biomass boiler: '#8A9A5B' - biomass to liquid: '#32CD32' - BioSNG: '#123456' - # power transmission - lines: '#6c9459' - transmission lines: '#6c9459' - electricity distribution grid: '#97ad8c' - # electricity demand - Electric load: '#110d63' - electric demand: '#110d63' - electricity: '#110d63' - industry electricity: '#2d2a66' - industry new electricity: '#2d2a66' - agriculture electricity: '#494778' - # battery + EVs - battery: '#ace37f' - battery storage: '#ace37f' - home battery: '#80c944' - home battery storage: '#80c944' - BEV charger: '#baf238' - V2G: '#e5ffa8' - land transport EV: '#baf238' - Li ion: '#baf238' - # hot water storage - water tanks: '#e69487' - hot water storage: '#e69487' - hot water charging: '#e69487' - hot water discharging: '#e69487' - # heat demand - Heat load: '#cc1f1f' - heat: '#cc1f1f' - heat demand: '#cc1f1f' - rural heat: '#ff5c5c' - central heat: '#cc1f1f' - decentral heat: '#750606' - low-temperature heat for industry: '#8f2727' - process heat: '#ff0000' - agriculture heat: '#d9a5a5' - # heat supply - heat pumps: '#2fb537' - heat pump: '#2fb537' - air heat pump: '#36eb41' - ground heat pump: '#2fb537' - Ambient: '#98eb9d' - CHP: '#8a5751' - CHP CC: '#634643' - CHP heat: '#8a5751' - CHP electric: '#8a5751' - district heating: '#e8beac' - resistive heater: '#d8f9b8' - retrofitting: '#8487e8' - building retrofitting: '#8487e8' - # hydrogen - H2 for industry: "#f073da" - H2 for shipping: "#ebaee0" - H2: '#bf13a0' - hydrogen: '#bf13a0' - SMR: '#870c71' - SMR CC: '#4f1745' - H2 liquefaction: '#d647bd' - hydrogen storage: '#bf13a0' - H2 storage: '#bf13a0' - land transport fuel cell: '#6b3161' - H2 pipeline: '#f081dc' - H2 pipeline retrofitted: '#ba99b5' - H2 Fuel Cell: '#c251ae' - H2 Electrolysis: '#ff29d9' - # ammonia - NH3: '#46caf0' - ammonia: '#46caf0' - ammonia store: '#00ace0' - ammonia cracker: '#87d0e6' - Haber-Bosch: '#076987' - # syngas - Sabatier: '#9850ad' - methanation: '#c44ce6' - methane: '#c44ce6' - helmeth: '#e899ff' - # synfuels - Fischer-Tropsch: '#25c49a' - liquid: '#25c49a' - kerosene for aviation: '#a1ffe6' - naphtha for industry: '#57ebc4' - # co2 - CC: '#f29dae' - CCS: '#f29dae' - CO2 sequestration: '#f29dae' - DAC: '#ff5270' - co2 stored: '#f2385a' - co2: '#f29dae' - co2 vent: '#ffd4dc' - CO2 pipeline: '#f5627f' - # emissions - process emissions CC: '#000000' - process emissions: '#222222' - process emissions to stored: '#444444' - process emissions to atmosphere: '#888888' - oil emissions: '#aaaaaa' - shipping oil emissions: "#555555" - land transport oil emissions: '#777777' - agriculture machinery oil emissions: '#333333' - # other - shipping: '#03a2ff' - power-to-heat: '#2fb537' - power-to-gas: '#c44ce6' - power-to-H2: '#ff29d9' - power-to-liquid: '#25c49a' - gas-to-power/heat: '#ee8340' - waste: '#e3d37d' - other: '#000000' From 169c0934b798e1b8877fb4c5977add3049b76610 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 3 Aug 2023 15:47:22 +0200 Subject: [PATCH 071/293] add option for methanol imports --- config/config.default.yaml | 1 + scripts/prepare_sector_network.py | 19 +++++++++---------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index d48c37b2b..502f2fd95 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -500,6 +500,7 @@ sector: - shipping-lch4 - shipping-lnh3 - shipping-ftfuel + - shipping-meoh - hvdc-to-elec endogenous_hvdc_import: enable: false diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 14bc100b9..a4369f640 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3424,7 +3424,7 @@ def add_import_options( "pipeline-h2", "shipping-lh2", "shipping-lch4", - # "shipping-meoh", + "shipping-meoh", "shipping-ftfuel", "shipping-lnh3", # "shipping-steel", @@ -3453,17 +3453,16 @@ def add_import_options( "shipping-lch4": " gas", "shipping-lnh3": " NH3", "shipping-ftfuel": " oil", - # "shipping-meoh": " methanol", + "shipping-meoh": " methanol", # "shipping-steel": " steel", } co2_intensity = { - "shipping-lch4": "gas", - "shipping-ftfuel": "oil", - # "shipping-meoh": "methanol", # TODO: or shipping fuel methanol + "shipping-lch4": ("gas", "CO2 intensity"), + "shipping-ftfuel": ("oil", "CO2 intensity"), + "shipping-meoh": ("methanolisation", "carbondioxide-input"), # "shipping-steel": "", TODO: is this necessary? } - # TODO take from costs.at["methanolisation", "carbondioxide-input"] import_costs = pd.read_csv(snakemake.input.import_costs, delimiter=";") cols = ["esc", "exporter", "importer", "value"] @@ -3529,7 +3528,7 @@ def add_import_options( bus1=import_nodes_tech.index + suffix, bus2="co2 atmosphere", carrier=f"import {tech}", - efficiency2=-costs.at[co2_intensity[tech], "CO2 intensity"], + efficiency2=-costs.at[co2_intensity[tech][0], co2_intensity[tech][1]], marginal_cost=import_nodes_tech.marginal_cost.values, p_nom_extendable=True, capital_cost=capital_cost, @@ -3559,7 +3558,7 @@ def add_import_options( # need special handling for copperplated imports copperplated_options = { "shipping-ftfuel", - # "shipping-meoh", + "shipping-meoh", # "shipping-steel", } @@ -3588,7 +3587,7 @@ def add_import_options( bus1="EU" + suffix, bus2="co2 atmosphere", carrier=f"import {tech}", - efficiency2=-costs.at[co2_intensity[tech], "CO2 intensity"], + efficiency2=-costs.at[co2_intensity[tech][0], co2_intensity[tech][1]], marginal_cost=marginal_costs, p_nom=1e7, ) @@ -3994,7 +3993,7 @@ def lossy_bidirectional_links(n, carrier, losses_per_thousand_km=0.0): CH4=["shipping-lch4"], NH3=["shipping-lnh3"], FT=["shipping-ftfuel"], - # MeOH=["shipping-meoh"], + MeOH=["shipping-meoh"], # St=["shipping-steel"] ) for o in opts: From af3a89d4d906687cdc2c052d00e1bf7ea31207cf Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 3 Aug 2023 16:00:08 +0200 Subject: [PATCH 072/293] fix endogenous steel sector coverage --- 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 e3ae93baa..dc2bfb897 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2574,7 +2574,7 @@ def add_industry(n, costs): snakemake.input.industry_sector_ratios, index_col=0 ) - endogenous_sectors = ["DRI + Electric arc"] + endogenous_sectors = ["DRI + Electric arc"] if options["endogenous_steel"] else [] sectors_b = ~industrial_demand.index.get_level_values("sector").isin( endogenous_sectors ) From 037b92a16cde1f4bbd6ec9067aea74c35c0a7e61 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 3 Aug 2023 16:38:04 +0200 Subject: [PATCH 073/293] add heat demand for DRI + EAF --- scripts/prepare_sector_network.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index dc2bfb897..ec83704cb 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2623,12 +2623,13 @@ def add_industry(n, costs): bus2=nodes + " H2", bus3=spatial.gas.nodes, bus4="co2 atmosphere", + bus5=nodes + " uban central heat", efficiency=1 / ratio["elec"], efficiency2=-ratio["hydrogen"] / ratio["elec"], efficiency3=-ratio["methane"] / ratio["elec"], efficiency4=ratio["process emission"] / ratio["elec"], + efficiency5=-ratio["heat"] / ratio["elec"], ) - # TODO specific consumption of heat (small), requires extension to bus5 n.madd( "Bus", From 1c1ddbc2fa764ff9161bd12b5f551e1aa125cde1 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 4 Aug 2023 10:38:32 +0200 Subject: [PATCH 074/293] fix endogenous steel: convert kt to t, prevent implicit industry relocation, include lowT heat --- scripts/prepare_sector_network.py | 39 ++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index ec83704cb..0d2b0bb92 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2568,7 +2568,7 @@ def add_industry(n, costs): ) industrial_production = ( - pd.read_csv(snakemake.input.industrial_production, index_col=0) * nyears + pd.read_csv(snakemake.input.industrial_production, index_col=0) * 1e3 * nyears # kt/a -> t/a ) industry_sector_ratios = pd.read_csv( snakemake.input.industry_sector_ratios, index_col=0 @@ -2580,6 +2580,9 @@ def add_industry(n, costs): ) if options["endogenous_steel"]: + + logger.info("Adding endogenous primary steel demand in tonnes.") + sector = "DRI + Electric arc" n.add( @@ -2595,35 +2598,43 @@ def add_industry(n, costs): "EU steel", bus="EU steel", carrier="steel", - p_set=industrial_production[sector].sum() / 8760, + p_set=industrial_production[sector].sum() / nhours, ) - n.add( - "Store", - "EU steel Store", - bus="EU steel", - e_nom_extendable=True, - e_cyclic=True, - carrier="steel", - ) + # n.add( + # "Store", + # "EU steel Store", + # bus="EU steel", + # e_nom_extendable=True, + # e_cyclic=True, + # carrier="steel", + # ) ratio = industry_sector_ratios[sector] + # so that for each region supply matches consumption + p_nom = industrial_production[sector] * ratio["elec"] / nhours + + bus5 = [ + node + " urban central heat" + if node + " urban central heat" in n.buses.index + else node + " services urban decentral heat" + for node in nodes + ] + n.madd( "Link", nodes, suffix=f" {sector}", carrier=sector, - p_nom_extendable=True, + p_nom=p_nom, p_min_pu=1, - capital_cost=0, - lifetime=99, bus0=nodes, bus1="EU steel", bus2=nodes + " H2", bus3=spatial.gas.nodes, bus4="co2 atmosphere", - bus5=nodes + " uban central heat", + bus5=bus5, efficiency=1 / ratio["elec"], efficiency2=-ratio["hydrogen"] / ratio["elec"], efficiency3=-ratio["methane"] / ratio["elec"], From 58e26b0cd9242e061b9ad04fd25682b533fa5d48 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 4 Aug 2023 08:39:33 +0000 Subject: [PATCH 075/293] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/prepare_sector_network.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 0d2b0bb92..d6f4f096e 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2568,7 +2568,9 @@ def add_industry(n, costs): ) industrial_production = ( - pd.read_csv(snakemake.input.industrial_production, index_col=0) * 1e3 * nyears # kt/a -> t/a + pd.read_csv(snakemake.input.industrial_production, index_col=0) + * 1e3 + * nyears # kt/a -> t/a ) industry_sector_ratios = pd.read_csv( snakemake.input.industry_sector_ratios, index_col=0 @@ -2580,7 +2582,6 @@ def add_industry(n, costs): ) if options["endogenous_steel"]: - logger.info("Adding endogenous primary steel demand in tonnes.") sector = "DRI + Electric arc" From f44d2288bae65aa75e077a5b855c3ac9eb5fa2c1 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 4 Aug 2023 11:44:22 +0200 Subject: [PATCH 076/293] add iron ore marginal cost and DRI+EAF capital cost --- config/config.default.yaml | 2 +- scripts/prepare_sector_network.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 52ab26ead..4919b2f14 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -631,7 +631,7 @@ industry: # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#costs costs: year: 2030 - version: v0.6.0 + version: v0.6.1 rooftop_share: 0.14 # based on the potentials, assuming (0.1 kW/m2 and 10 m2/person) fill_values: FOM: 0 diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 0d2b0bb92..66b2ebd55 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2622,11 +2622,25 @@ def add_industry(n, costs): for node in nodes ] + marginal_cost = ( + costs.at["iron ore DRI-ready", "commodity"] + * costs.at["direct iron reduction furnace", "ore-input"] + * costs.at["electric arc furnace", "hbi-input"] + / ratio["elec"] + ) + + capital_cost = ( + costs.at["direct iron reduction furnace", "fixed"] + + costs.at["electric arc furnace", "fixed"] + ) / ratio["elec"] + n.madd( "Link", nodes, suffix=f" {sector}", carrier=sector, + capital_cost=capital_cost, + marginal_cost=marginal_cost, p_nom=p_nom, p_min_pu=1, bus0=nodes, From 041cfc214e5e19e5ff7a3d1eedb69bd00a32422e Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 4 Aug 2023 12:09:49 +0200 Subject: [PATCH 077/293] add steel import option --- scripts/prepare_sector_network.py | 36 +++++++++++++++++-------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index a3a99bdab..65e42ab6c 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3547,7 +3547,7 @@ def add_import_options( "shipping-meoh", "shipping-ftfuel", "shipping-lnh3", - # "shipping-steel", + "shipping-steel", ], endogenous_hvdc=False, ): @@ -3574,19 +3574,19 @@ def add_import_options( "shipping-lnh3": " NH3", "shipping-ftfuel": " oil", "shipping-meoh": " methanol", - # "shipping-steel": " steel", + "shipping-steel": " steel", } co2_intensity = { "shipping-lch4": ("gas", "CO2 intensity"), "shipping-ftfuel": ("oil", "CO2 intensity"), "shipping-meoh": ("methanolisation", "carbondioxide-input"), - # "shipping-steel": "", TODO: is this necessary? } import_costs = pd.read_csv(snakemake.input.import_costs, delimiter=";") cols = ["esc", "exporter", "importer", "value"] - import_costs = import_costs.query("subcategory == 'Cost per MWh delivered'")[cols] + fields = ['Cost per MWh delivered', "Cost per t delivered"] + import_costs = import_costs.query("subcategory in @fields")[cols] import_costs.rename(columns={"value": "marginal_cost"}, inplace=True) for k, v in translate.items(): @@ -3676,13 +3676,12 @@ def add_import_options( ) # need special handling for copperplated imports - copperplated_options = { + copperplated_carbonaceous_options = { "shipping-ftfuel", "shipping-meoh", - # "shipping-steel", } - for tech in set(import_options).intersection(copperplated_options): + for tech in set(import_options).intersection(copperplated_carbonaceous_options): marginal_costs = import_costs.query("esc == @tech").marginal_cost.min() suffix = bus_suffix[tech] @@ -3712,19 +3711,24 @@ def add_import_options( p_nom=1e7, ) - if ( - "shipping-lnh3" in import_options - and "shipping-lnh3" not in regionalised_options - ): + copperplated_carbonfree_options = { + "shipping-steel", + "shipping-lnh3", + } + + for tech in set(import_options).intersection(copperplated_carbonfree_options): + + suffix = bus_suffix[tech] + marginal_costs = import_costs.query( - "esc == 'shipping-lnh3'" + "esc == @tech" ).marginal_cost.min() n.add( "Generator", - "EU import shipping-lnh3", - bus="EU NH3", - carrier="import shipping-lnh3", + f"EU import {tech}", + bus="EU" + suffix, + carrier=f"import {tech}", marginal_cost=marginal_costs, p_nom=1e7, ) @@ -4114,7 +4118,7 @@ def lossy_bidirectional_links(n, carrier, losses_per_thousand_km=0.0): NH3=["shipping-lnh3"], FT=["shipping-ftfuel"], MeOH=["shipping-meoh"], - # St=["shipping-steel"] + St=["shipping-steel"] ) for o in opts: if not o.startswith("imp"): From 872d6f7102d24b42cecfd408cb5f54b4e0ada9bf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 4 Aug 2023 10:10:23 +0000 Subject: [PATCH 078/293] [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, 3 insertions(+), 6 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 65e42ab6c..625871ebe 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3585,7 +3585,7 @@ def add_import_options( import_costs = pd.read_csv(snakemake.input.import_costs, delimiter=";") cols = ["esc", "exporter", "importer", "value"] - fields = ['Cost per MWh delivered', "Cost per t delivered"] + fields = ["Cost per MWh delivered", "Cost per t delivered"] import_costs = import_costs.query("subcategory in @fields")[cols] import_costs.rename(columns={"value": "marginal_cost"}, inplace=True) @@ -3717,12 +3717,9 @@ def add_import_options( } for tech in set(import_options).intersection(copperplated_carbonfree_options): - suffix = bus_suffix[tech] - marginal_costs = import_costs.query( - "esc == @tech" - ).marginal_cost.min() + marginal_costs = import_costs.query("esc == @tech").marginal_cost.min() n.add( "Generator", @@ -4118,7 +4115,7 @@ def lossy_bidirectional_links(n, carrier, losses_per_thousand_km=0.0): NH3=["shipping-lnh3"], FT=["shipping-ftfuel"], MeOH=["shipping-meoh"], - St=["shipping-steel"] + St=["shipping-steel"], ) for o in opts: if not o.startswith("imp"): From 757c1a346d4527ad28a14d26dae77716eb67dcfa Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 4 Aug 2023 13:23:20 +0200 Subject: [PATCH 079/293] directly add industry MeOH consumption as MeOH load --- config/config.default.yaml | 1 + scripts/build_industry_sector_ratios.py | 5 ++--- scripts/prepare_sector_network.py | 23 +++++++++++++++++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 4919b2f14..ec42aa30d 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -623,6 +623,7 @@ industry: MWh_elec_per_tCl: 3.6 MWh_H2_per_tCl: -0.9372 methanol_production_today: 1.5 + MWh_MeOH_per_tMeOH: 5.54 MWh_elec_per_tMeOH: 0.167 MWh_CH4_per_tMeOH: 10.25 hotmaps_locate_missing: false diff --git a/scripts/build_industry_sector_ratios.py b/scripts/build_industry_sector_ratios.py index 457050026..99045bf74 100644 --- a/scripts/build_industry_sector_ratios.py +++ b/scripts/build_industry_sector_ratios.py @@ -456,8 +456,7 @@ def chemicals_industry(): sector = "Methanol" df[sector] = 0.0 - df.loc["methane", sector] = params["MWh_CH4_per_tMeOH"] - df.loc["elec", sector] = params["MWh_elec_per_tMeOH"] + df.loc["methanol", sector] = params["MWh_MeOH_per_tMeOH"] # Other chemicals @@ -602,7 +601,7 @@ def chemicals_industry(): sources = ["elec", "biomass", "methane", "hydrogen", "heat", "naphtha"] df.loc[sources, sector] = df.loc[sources, sector] * toe_to_MWh / s_out.values - return df + return df.fillna(0.) def nonmetalic_mineral_products(): diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 65e42ab6c..ee6210fa1 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3065,6 +3065,29 @@ def add_industry(n, costs): p_set=-co2, ) + # industry methanol demand + + p_set_methanol = industrial_demand.loc[(nodes, sectors_b), "methanol"].sum() / nhours + + n.madd( + "Load", + spatial.methanol.nodes, + suffix=" industry methanol", + bus=spatial.methanol.nodes, + carrier="industry methanol", + p_set=p_set_methanol, + ) + + co2 = p_set_methanol * costs.at["methanolisation", "carbondioxide-input"] + + n.add( + "Load", + "industry methanol emissions", + bus="co2 atmosphere", + carrier="industry methanol emissions", + p_set=-co2, + ) + # TODO simplify bus expression n.madd( "Load", From d2d411ea3cd8cf9931a9b095e9d76899b7803079 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 4 Aug 2023 11:24:00 +0000 Subject: [PATCH 080/293] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/build_industry_sector_ratios.py | 2 +- scripts/prepare_sector_network.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/build_industry_sector_ratios.py b/scripts/build_industry_sector_ratios.py index 99045bf74..927170a11 100644 --- a/scripts/build_industry_sector_ratios.py +++ b/scripts/build_industry_sector_ratios.py @@ -601,7 +601,7 @@ def chemicals_industry(): sources = ["elec", "biomass", "methane", "hydrogen", "heat", "naphtha"] df.loc[sources, sector] = df.loc[sources, sector] * toe_to_MWh / s_out.values - return df.fillna(0.) + return df.fillna(0.0) def nonmetalic_mineral_products(): diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 2d46e32e2..c5901c5fb 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3067,7 +3067,9 @@ def add_industry(n, costs): # industry methanol demand - p_set_methanol = industrial_demand.loc[(nodes, sectors_b), "methanol"].sum() / nhours + p_set_methanol = ( + industrial_demand.loc[(nodes, sectors_b), "methanol"].sum() / nhours + ) n.madd( "Load", From 3cf8a0f3b5270482451647f904fb65dca16f0fe7 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 4 Aug 2023 14:20:38 +0200 Subject: [PATCH 081/293] for shipping oil, use add_carrier_buses() --- scripts/prepare_sector_network.py | 34 +++++-------------------------- 1 file changed, 5 insertions(+), 29 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index c5901c5fb..7e3dfbf0f 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2784,6 +2784,8 @@ def add_industry(n, costs): / nhours, ) + # shipping + shipping_hydrogen_share = get(options["shipping_hydrogen_share"], investment_year) shipping_methanol_share = get(options["shipping_methanol_share"], investment_year) shipping_oil_share = get(options["shipping_oil_share"], investment_year) @@ -2917,6 +2919,9 @@ def add_industry(n, costs): ) if shipping_oil_share: + + add_carrier_buses(n, "oil") + p_set_oil = shipping_oil_share * p_set.sum() n.madd( @@ -2938,35 +2943,6 @@ def add_industry(n, costs): p_set=-co2, ) - 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", - [oil_bus + " Store" for oil_bus in spatial.oil.nodes], - bus=spatial.oil.nodes, - e_nom_extendable=True, - e_cyclic=True, - carrier="oil", - ) - - if "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"], - ) if options["oil_boilers"]: nodes_heat = create_nodes_for_heat_sector()[0] From 46f9209f360f45eeab967c05f8f691259009fdc4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 4 Aug 2023 12:21:00 +0000 Subject: [PATCH 082/293] [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, 2 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 7e3dfbf0f..62a6d9a34 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2919,7 +2919,6 @@ def add_industry(n, costs): ) if shipping_oil_share: - add_carrier_buses(n, "oil") p_set_oil = shipping_oil_share * p_set.sum() @@ -2943,7 +2942,6 @@ def add_industry(n, costs): p_set=-co2, ) - if options["oil_boilers"]: nodes_heat = create_nodes_for_heat_sector()[0] From 96779119c061507777cddedb36663bf299f8c263 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 4 Aug 2023 14:41:42 +0200 Subject: [PATCH 083/293] move shipping demand from industry into separate function --- config/config.default.yaml | 2 +- doc/configtables/sector-opts.csv | 1 + scripts/prepare_sector_network.py | 333 +++++++++++++++--------------- 3 files changed, 168 insertions(+), 168 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index ec42aa30d..0655f0e83 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -36,7 +36,7 @@ scenario: opts: - '' sector_opts: - - Co2L0-3H-T-H-B-I-A-solar+p3-dist1 + - Co2L0-3H-T-H-B-I-S-A-solar+p3-dist1 planning_horizons: # - 2020 # - 2030 diff --git a/doc/configtables/sector-opts.csv b/doc/configtables/sector-opts.csv index ea39c3b0d..762074eb8 100644 --- a/doc/configtables/sector-opts.csv +++ b/doc/configtables/sector-opts.csv @@ -6,6 +6,7 @@ Trigger, Description, Definition, Status ``H``,Add heating sector,,In active use ``B``,Add biomass,,In active use ``I``,Add industry sector,,In active use +``S``,Add shipping sector,,In active use ``A``,Add agriculture sector,,In active use ``dist``+``n``,Add distribution grid with investment costs of ``n`` times costs in ``data/costs_{cost_year}.csv``,,In active use ``seq``+``n``,Sets the CO2 sequestration potential to ``n`` Mt CO2 per year,,In active use diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 62a6d9a34..7bc383010 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -469,14 +469,15 @@ def add_carrier_buses(n, carrier, nodes=None): capital_cost=capital_cost, ) - n.madd( - "Generator", - nodes, - bus=nodes, - p_nom_extendable=True, - carrier=carrier, - marginal_cost=costs.at[carrier, "fuel"], - ) + if carrier in costs.index and "fuel" in costs.columns: + n.madd( + "Generator", + nodes, + bus=nodes, + p_nom_extendable=True, + carrier=carrier, + marginal_cost=costs.at[carrier, "fuel"], + ) # TODO: PyPSA-Eur merge issue @@ -2559,6 +2560,9 @@ def add_industry(n, costs): nhours = n.snapshot_weightings.generators.sum() nyears = nhours / 8760 + add_carrier_buses(n, "oil") + add_carrier_buses(n, "methanol") + # 1e6 to convert TWh to MWh industrial_demand = ( pd.read_csv(snakemake.input.industrial_demand, index_col=[0, 1]) * 1e6 * nyears @@ -2784,164 +2788,6 @@ def add_industry(n, costs): / nhours, ) - # shipping - - shipping_hydrogen_share = get(options["shipping_hydrogen_share"], investment_year) - shipping_methanol_share = get(options["shipping_methanol_share"], investment_year) - shipping_oil_share = get(options["shipping_oil_share"], investment_year) - - total_share = shipping_hydrogen_share + shipping_methanol_share + shipping_oil_share - if total_share != 1: - logger.warning( - f"Total shipping shares sum up to {total_share:.2%}, corresponding to increased or decreased demand assumptions." - ) - - domestic_navigation = pop_weighted_energy_totals.loc[ - nodes, "total domestic navigation" - ].squeeze() - international_navigation = ( - pd.read_csv(snakemake.input.shipping_demand, index_col=0).squeeze() * nyears - ) - all_navigation = domestic_navigation + international_navigation - p_set = all_navigation * 1e6 / nhours - - if shipping_hydrogen_share: - oil_efficiency = options.get( - "shipping_oil_efficiency", options.get("shipping_average_efficiency", 0.4) - ) - efficiency = oil_efficiency / costs.at["fuel cell", "efficiency"] - shipping_hydrogen_share = get( - options["shipping_hydrogen_share"], investment_year - ) - - if options["shipping_hydrogen_liquefaction"]: - n.madd( - "Bus", - nodes, - suffix=" H2 liquid", - carrier="H2 liquid", - location=nodes, - unit="MWh_LHV", - ) - - n.madd( - "Link", - nodes + " H2 liquefaction", - bus0=nodes + " H2", - bus1=nodes + " H2 liquid", - carrier="H2 liquefaction", - efficiency=costs.at["H2 liquefaction", "efficiency"], - capital_cost=costs.at["H2 liquefaction", "fixed"], - p_nom_extendable=True, - lifetime=costs.at["H2 liquefaction", "lifetime"], - ) - - shipping_bus = nodes + " H2 liquid" - else: - shipping_bus = nodes + " H2" - - efficiency = ( - options["shipping_oil_efficiency"] / costs.at["fuel cell", "efficiency"] - ) - p_set_hydrogen = shipping_hydrogen_share * p_set * efficiency - - n.madd( - "Load", - nodes, - suffix=" H2 for shipping", - bus=shipping_bus, - carrier="H2 for shipping", - p_set=p_set_hydrogen, - ) - - if shipping_methanol_share: - 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", - ) - - n.madd( - "Link", - spatial.h2.locations + " methanolisation", - bus0=spatial.h2.nodes, - bus1=spatial.methanol.nodes, - bus2=nodes, - bus3=spatial.co2.nodes, - carrier="methanolisation", - p_nom_extendable=True, - p_min_pu=options.get("min_part_load_methanolisation", 0), - capital_cost=costs.at["methanolisation", "fixed"] - / costs.at["methanolisation", "hydrogen-input"], # EUR/MW_H2/a - lifetime=costs.at["methanolisation", "lifetime"], - efficiency=1 / costs.at["methanolisation", "hydrogen-input"], - efficiency2=-costs.at["methanolisation", "electricity-input"] - / costs.at["methanolisation", "hydrogen-input"], - efficiency3=-costs.at["methanolisation", "carbondioxide-input"] - / costs.at["methanolisation", "hydrogen-input"], - ) - - efficiency = ( - options["shipping_oil_efficiency"] / options["shipping_methanol_efficiency"] - ) - p_set_methanol = shipping_methanol_share * p_set.sum() * efficiency - - n.madd( - "Load", - spatial.methanol.nodes, - suffix=" shipping methanol", - bus=spatial.methanol.nodes, - carrier="shipping methanol", - p_set=p_set_methanol, - ) - - # 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 - co2 = p_set_methanol * costs.at["methanolisation", "carbondioxide-input"] - - n.add( - "Load", - "shipping methanol emissions", - bus="co2 atmosphere", - carrier="shipping methanol emissions", - p_set=-co2, - ) - - if shipping_oil_share: - add_carrier_buses(n, "oil") - - p_set_oil = shipping_oil_share * p_set.sum() - - n.madd( - "Load", - spatial.oil.nodes, - suffix=" shipping oil", - bus=spatial.oil.nodes, - carrier="shipping oil", - p_set=p_set_oil, - ) - - co2 = p_set_oil * costs.at["oil", "CO2 intensity"] - - n.add( - "Load", - "shipping oil emissions", - bus="co2 atmosphere", - carrier="shipping oil emissions", - p_set=-co2, - ) - if options["oil_boilers"]: nodes_heat = create_nodes_for_heat_sector()[0] @@ -3047,7 +2893,7 @@ def add_industry(n, costs): n.madd( "Load", - spatial.methanol.nodes, + spatial.methanol.locations, suffix=" industry methanol", bus=spatial.methanol.nodes, carrier="industry methanol", @@ -3190,6 +3036,153 @@ def add_industry(n, costs): ) +def add_shipping(n, costs): + logger.info("Add shipping demand") + + nodes = pop_layout.index + + shipping_hydrogen_share = get(options["shipping_hydrogen_share"], investment_year) + shipping_methanol_share = get(options["shipping_methanol_share"], investment_year) + shipping_oil_share = get(options["shipping_oil_share"], investment_year) + + total_share = shipping_hydrogen_share + shipping_methanol_share + shipping_oil_share + if total_share != 1: + logger.warning( + f"Total shipping shares sum up to {total_share:.2%}, corresponding to increased or decreased demand assumptions." + ) + + domestic_navigation = pop_weighted_energy_totals.loc[ + nodes, "total domestic navigation" + ].squeeze() + international_navigation = ( + pd.read_csv(snakemake.input.shipping_demand, index_col=0).squeeze() * nyears + ) + all_navigation = domestic_navigation + international_navigation + p_set = all_navigation * 1e6 / nhours + + if shipping_hydrogen_share: + oil_efficiency = options.get( + "shipping_oil_efficiency", options.get("shipping_average_efficiency", 0.4) + ) + efficiency = oil_efficiency / costs.at["fuel cell", "efficiency"] + shipping_hydrogen_share = get( + options["shipping_hydrogen_share"], investment_year + ) + + if options["shipping_hydrogen_liquefaction"]: + n.madd( + "Bus", + nodes, + suffix=" H2 liquid", + carrier="H2 liquid", + location=nodes, + unit="MWh_LHV", + ) + + n.madd( + "Link", + nodes + " H2 liquefaction", + bus0=nodes + " H2", + bus1=nodes + " H2 liquid", + carrier="H2 liquefaction", + efficiency=costs.at["H2 liquefaction", "efficiency"], + capital_cost=costs.at["H2 liquefaction", "fixed"], + p_nom_extendable=True, + lifetime=costs.at["H2 liquefaction", "lifetime"], + ) + + shipping_bus = nodes + " H2 liquid" + else: + shipping_bus = nodes + " H2" + + efficiency = ( + options["shipping_oil_efficiency"] / costs.at["fuel cell", "efficiency"] + ) + p_set_hydrogen = shipping_hydrogen_share * p_set * efficiency + + n.madd( + "Load", + nodes, + suffix=" H2 for shipping", + bus=shipping_bus, + carrier="H2 for shipping", + p_set=p_set_hydrogen, + ) + + if shipping_methanol_share: + + add_carrier_buses(n, "methanol") + + n.madd( + "Link", + spatial.h2.locations + " methanolisation", + bus0=spatial.h2.nodes, + bus1=spatial.methanol.nodes, + bus2=nodes, + bus3=spatial.co2.nodes, + carrier="methanolisation", + p_nom_extendable=True, + p_min_pu=options.get("min_part_load_methanolisation", 0), + capital_cost=costs.at["methanolisation", "fixed"] + / costs.at["methanolisation", "hydrogen-input"], # EUR/MW_H2/a + lifetime=costs.at["methanolisation", "lifetime"], + efficiency=1 / costs.at["methanolisation", "hydrogen-input"], + efficiency2=-costs.at["methanolisation", "electricity-input"] + / costs.at["methanolisation", "hydrogen-input"], + efficiency3=-costs.at["methanolisation", "carbondioxide-input"] + / costs.at["methanolisation", "hydrogen-input"], + ) + + efficiency = ( + options["shipping_oil_efficiency"] / options["shipping_methanol_efficiency"] + ) + p_set_methanol = shipping_methanol_share * p_set.sum() * efficiency + + n.madd( + "Load", + spatial.methanol.nodes, + suffix=" shipping methanol", + bus=spatial.methanol.nodes, + carrier="shipping methanol", + p_set=p_set_methanol, + ) + + # 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 + co2 = p_set_methanol * costs.at["methanolisation", "carbondioxide-input"] + + n.add( + "Load", + "shipping methanol emissions", + bus="co2 atmosphere", + carrier="shipping methanol emissions", + p_set=-co2, + ) + + if shipping_oil_share: + add_carrier_buses(n, "oil") + + p_set_oil = shipping_oil_share * p_set.sum() + + n.madd( + "Load", + spatial.oil.nodes, + suffix=" shipping oil", + bus=spatial.oil.nodes, + carrier="shipping oil", + p_set=p_set_oil, + ) + + co2 = p_set_oil * costs.at["oil", "CO2 intensity"] + + n.add( + "Load", + "shipping oil emissions", + bus="co2 atmosphere", + carrier="shipping oil emissions", + p_set=-co2, + ) + + def add_waste_heat(n): # TODO options? @@ -3296,6 +3289,9 @@ def add_agriculture(n, costs): ) if oil_share > 0: + + add_carrier_buses(n, "oil") + n.madd( "Load", ["agriculture machinery oil"], @@ -4089,6 +4085,9 @@ def lossy_bidirectional_links(n, carrier, losses_per_thousand_km=0.0): if "I" in opts: add_industry(n, costs) + if "S" in opts: + add_shipping(n, costs) + if "I" in opts and "H" in opts: add_waste_heat(n) From 342122f758341cc887905226b72b9c4793564712 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 4 Aug 2023 12:42:06 +0000 Subject: [PATCH 084/293] [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, 2 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 7bc383010..43b07459e 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3110,7 +3110,6 @@ def add_shipping(n, costs): ) if shipping_methanol_share: - add_carrier_buses(n, "methanol") n.madd( @@ -3289,7 +3288,6 @@ def add_agriculture(n, costs): ) if oil_share > 0: - add_carrier_buses(n, "oil") n.madd( From ee05f5eb5d203fea4665432c91a5042f2bbea7f5 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 4 Aug 2023 15:05:41 +0200 Subject: [PATCH 085/293] move methanolisation to industry section from shipping --- scripts/prepare_sector_network.py | 42 ++++++++++++++++--------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 43b07459e..af32f4d7b 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2910,6 +2910,28 @@ def add_industry(n, costs): p_set=-co2, ) + # methanolisation + + n.madd( + "Link", + spatial.h2.locations + " methanolisation", + bus0=spatial.h2.nodes, + bus1=spatial.methanol.nodes, + bus2=nodes, + bus3=spatial.co2.nodes, + carrier="methanolisation", + p_nom_extendable=True, + p_min_pu=options.get("min_part_load_methanolisation", 0), + capital_cost=costs.at["methanolisation", "fixed"] + / costs.at["methanolisation", "hydrogen-input"], # EUR/MW_H2/a + lifetime=costs.at["methanolisation", "lifetime"], + efficiency=1 / costs.at["methanolisation", "hydrogen-input"], + efficiency2=-costs.at["methanolisation", "electricity-input"] + / costs.at["methanolisation", "hydrogen-input"], + efficiency3=-costs.at["methanolisation", "carbondioxide-input"] + / costs.at["methanolisation", "hydrogen-input"], + ) + # TODO simplify bus expression n.madd( "Load", @@ -3112,26 +3134,6 @@ def add_shipping(n, costs): if shipping_methanol_share: add_carrier_buses(n, "methanol") - n.madd( - "Link", - spatial.h2.locations + " methanolisation", - bus0=spatial.h2.nodes, - bus1=spatial.methanol.nodes, - bus2=nodes, - bus3=spatial.co2.nodes, - carrier="methanolisation", - p_nom_extendable=True, - p_min_pu=options.get("min_part_load_methanolisation", 0), - capital_cost=costs.at["methanolisation", "fixed"] - / costs.at["methanolisation", "hydrogen-input"], # EUR/MW_H2/a - lifetime=costs.at["methanolisation", "lifetime"], - efficiency=1 / costs.at["methanolisation", "hydrogen-input"], - efficiency2=-costs.at["methanolisation", "electricity-input"] - / costs.at["methanolisation", "hydrogen-input"], - efficiency3=-costs.at["methanolisation", "carbondioxide-input"] - / costs.at["methanolisation", "hydrogen-input"], - ) - efficiency = ( options["shipping_oil_efficiency"] / options["shipping_methanol_efficiency"] ) From 5b6d656c269ac709f2cacfad7916b1e20e6d4183 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Sat, 5 Aug 2023 10:24:14 +0200 Subject: [PATCH 086/293] endogenisation of shipping fuels first draft --- config/config.default.yaml | 10 +++ scripts/prepare_sector_network.py | 133 +++++++++++++++++++++++++++++- 2 files changed, 142 insertions(+), 1 deletion(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 0655f0e83..bd6b965b0 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -399,6 +399,16 @@ sector: 2050: 0 shipping_methanol_efficiency: 0.46 shipping_oil_efficiency: 0.40 + shipping_ammonia_efficiency: 0.44 + shipping_lng_efficiency: 0.41 + shipping_endogenous: + enable: false + fuels: + - oil + - methanol + - LH2 + - LNG + - ammonia aviation_demand_factor: 1. HVC_demand_factor: 1. time_dep_hp_cop: true diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index af32f4d7b..582804497 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3132,6 +3132,7 @@ def add_shipping(n, costs): ) if shipping_methanol_share: + add_carrier_buses(n, "methanol") efficiency = ( @@ -3184,6 +3185,132 @@ def add_shipping(n, costs): ) +def add_shipping_endogenous(n, costs): + + logger.info("Add shipping demand endogenously") + + fuels = options["shipping_endogenous"]["fuels"] + + nodes = pop_layout.index + + domestic_navigation = pop_weighted_energy_totals.loc[ + nodes, "total domestic navigation" + ].squeeze() + international_navigation = ( + pd.read_csv(snakemake.input.shipping_demand, index_col=0).squeeze() * nyears + ) + all_navigation = domestic_navigation + international_navigation + + # determine demand in MW propulsion + oil_efficiency = options["shipping_oil_efficiency"] + p_set = oil_efficiency * all_navigation * 1e6 / nhours + + n.madd( + "Bus", + nodes, + suffix=" shipping", + unit="MWh propulsion", + location=nodes, + carrier="shipping" + ) + + n.madd( + "Load", + nodes, + suffix=" shipping", + bus=nodes + " shipping", + carrier="shipping", + p_set=p_set, + ) + + if "oil" in fuels: + + add_carrier_buses(n, "oil") + + n.madd( + "Link", + nodes, + suffix=" shipping oil", + carrier="shipping oil", + bus0=spatial.oil.nodes, + bus1=nodes + " shipping", + bus2="co2 atmosphere", + efficiency=options["shipping_oil_efficiency"], + efficiency2=costs.at["oil", "CO2 intensity"], + p_min_pu=1, + p_nom_extendable=True, + ) + + if "methanol" in fuels: + + add_carrier_buses(n, "methanol") + + n.madd( + "Link", + nodes, + suffix=" shipping methanol", + carrier="shipping methanol", + bus0=spatial.methanol.nodes, + bus1=nodes + " shipping", + bus2="co2 atmosphere", + efficiency=options["shipping_methanol_efficiency"], + efficiency2=costs.at["methanolisation", "carbondioxide-input"], + p_min_pu=1, + p_nom_extendable=True, + ) + + if "LH2" in fuels: + + n.madd( + "Link", + nodes, + suffix=" shipping LH2", + bus0=nodes + " H2", + bus1=nodes + " shipping", + carrier="shipping LH2", + efficiency=costs.at["fuel cell", "efficiency"] * costs.at["H2 liquefaction", "efficiency"], + capital_cost=costs.at["H2 liquefaction", "fixed"], + lifetime=costs.at["H2 liquefaction", "lifetime"], + p_min_pu=1, + p_nom_extendable=True, + ) + + if "ammonia" in fuels: + + if not options["ammonia"]: + logger.warning("Ammonia not represented as carrier. Skipping it as shipping fuel option.") + + n.madd( + "Link", + nodes, + suffix=" shipping ammonia", + bus0=spatial.ammonia.nodes, + bus1=nodes + " shipping", + carrier="shipping ammonia", + efficiency=options["shipping_ammonia_efficiency"], + p_min_pu=1, + p_nom_extendable=True, + ) + + if "LNG" in fuels: + + n.madd( + "Link", + nodes, + suffix=" shipping LNG", + bus0=spatial.gas.nodes, + bus1=nodes + " shipping", + bus2="co2 atmosphere", + carrier="shipping LNG", + efficiency=options["shipping_lng_efficiency"], + efficiency2=costs.at["gas", "CO2 intensity"], + capital_cost=costs.at["CH4 liquefaction", "fixed"], + lifetime=costs.at["CH4 liquefaction", "lifetime"], + p_min_pu=1, + p_nom_extendable=True, + ) + + def add_waste_heat(n): # TODO options? @@ -3290,6 +3417,7 @@ def add_agriculture(n, costs): ) if oil_share > 0: + add_carrier_buses(n, "oil") n.madd( @@ -4085,9 +4213,12 @@ def lossy_bidirectional_links(n, carrier, losses_per_thousand_km=0.0): if "I" in opts: add_industry(n, costs) - if "S" in opts: + if "S" in opts and not options["shipping_endogenous"]["enable"]: add_shipping(n, costs) + if "S" in opts and options["shipping_endogenous"]["enable"]: + add_shipping_endogenous(n, costs) + if "I" in opts and "H" in opts: add_waste_heat(n) From d1964cbce73b3d6110640f0b542fefd244192ece Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 5 Aug 2023 08:24:38 +0000 Subject: [PATCH 087/293] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/prepare_sector_network.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 582804497..0050d09f2 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3132,7 +3132,6 @@ def add_shipping(n, costs): ) if shipping_methanol_share: - add_carrier_buses(n, "methanol") efficiency = ( @@ -3186,7 +3185,6 @@ def add_shipping(n, costs): def add_shipping_endogenous(n, costs): - logger.info("Add shipping demand endogenously") fuels = options["shipping_endogenous"]["fuels"] @@ -3211,7 +3209,7 @@ def add_shipping_endogenous(n, costs): suffix=" shipping", unit="MWh propulsion", location=nodes, - carrier="shipping" + carrier="shipping", ) n.madd( @@ -3224,7 +3222,6 @@ def add_shipping_endogenous(n, costs): ) if "oil" in fuels: - add_carrier_buses(n, "oil") n.madd( @@ -3242,7 +3239,6 @@ def add_shipping_endogenous(n, costs): ) if "methanol" in fuels: - add_carrier_buses(n, "methanol") n.madd( @@ -3260,7 +3256,6 @@ def add_shipping_endogenous(n, costs): ) if "LH2" in fuels: - n.madd( "Link", nodes, @@ -3268,7 +3263,8 @@ def add_shipping_endogenous(n, costs): bus0=nodes + " H2", bus1=nodes + " shipping", carrier="shipping LH2", - efficiency=costs.at["fuel cell", "efficiency"] * costs.at["H2 liquefaction", "efficiency"], + efficiency=costs.at["fuel cell", "efficiency"] + * costs.at["H2 liquefaction", "efficiency"], capital_cost=costs.at["H2 liquefaction", "fixed"], lifetime=costs.at["H2 liquefaction", "lifetime"], p_min_pu=1, @@ -3276,9 +3272,10 @@ def add_shipping_endogenous(n, costs): ) if "ammonia" in fuels: - if not options["ammonia"]: - logger.warning("Ammonia not represented as carrier. Skipping it as shipping fuel option.") + logger.warning( + "Ammonia not represented as carrier. Skipping it as shipping fuel option." + ) n.madd( "Link", @@ -3293,7 +3290,6 @@ def add_shipping_endogenous(n, costs): ) if "LNG" in fuels: - n.madd( "Link", nodes, @@ -3417,7 +3413,6 @@ def add_agriculture(n, costs): ) if oil_share > 0: - add_carrier_buses(n, "oil") n.madd( From 28f7460373c1c41e3675b800d2c88c91eec54279 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 7 Aug 2023 14:02:30 +0200 Subject: [PATCH 088/293] bump technology-data version to 0.6.2 --- 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 bd6b965b0..965021741 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -642,7 +642,7 @@ industry: # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#costs costs: year: 2030 - version: v0.6.1 + version: v0.6.2 rooftop_share: 0.14 # based on the potentials, assuming (0.1 kW/m2 and 10 m2/person) fill_values: FOM: 0 From c465562ea43b984deca636aec04b8c9b8483eac6 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 7 Aug 2023 14:07:15 +0200 Subject: [PATCH 089/293] add option to use methanolisation waste heat --- config/config.default.yaml | 1 + scripts/prepare_sector_network.py | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/config/config.default.yaml b/config/config.default.yaml index 965021741..bad53a60c 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -477,6 +477,7 @@ sector: min_part_load_fischer_tropsch: 0.9 min_part_load_methanolisation: 0.5 use_fischer_tropsch_waste_heat: true + use_methanolisation_waste_heat: true use_fuel_cell_waste_heat: true use_electrolysis_waste_heat: false electricity_distribution_grid: true diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 582804497..b0a3dba33 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3330,6 +3330,14 @@ def add_waste_heat(n): 0.95 - n.links.loc[urban_central + " Fischer-Tropsch", "efficiency"] ) + if options["use_methanolisation_waste_heat"]: + n.links.loc[urban_central + "methanolisation", "bus4"] = ( + urban_central + "urban central heat" + ) + n.links.loc[urban_central + " methanolisation", "efficiency4"] = ( + costs.at["methanolisation", "heat-output"] / costs.at["methanolisation", "hydrogen-input"] + ) + # TODO integrate usable waste heat efficiency into technology-data from DEA if options.get("use_electrolysis_waste_heat", False): n.links.loc[urban_central + " H2 Electrolysis", "bus2"] = ( From 7d90a1e2cc039b6495dee51703303c6122486008 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Aug 2023 12:11:45 +0000 Subject: [PATCH 090/293] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/prepare_sector_network.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index b57e3d4b6..9065e96a6 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3331,7 +3331,8 @@ def add_waste_heat(n): urban_central + "urban central heat" ) n.links.loc[urban_central + " methanolisation", "efficiency4"] = ( - costs.at["methanolisation", "heat-output"] / costs.at["methanolisation", "hydrogen-input"] + costs.at["methanolisation", "heat-output"] + / costs.at["methanolisation", "hydrogen-input"] ) # TODO integrate usable waste heat efficiency into technology-data from DEA From bdc7aea28e8615b814afca2c56eceb37e5423a9a Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 7 Aug 2023 14:31:19 +0200 Subject: [PATCH 091/293] link losses: exponential rather than linear model --- config/config.default.yaml | 13 ++++++++----- scripts/prepare_sector_network.py | 15 ++++++++++----- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index bad53a60c..252b00be1 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -483,11 +483,14 @@ sector: electricity_distribution_grid: true electricity_distribution_grid_cost_factor: 1.0 electricity_grid_connection: true - transmission_losses: - # per 1000 km - DC: 0 - H2 pipeline: 0 - gas pipeline: 0 + transmission_efficiency: + DC: + efficiency_static: 0.98 + efficiency_per_1000km: 0.977 + H2 pipeline: + efficiency_per_1000km: 0.979 + gas pipeline: + efficiency_per_1000km: 0.977 H2_network: true gas_network: false H2_retrofit: false diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index b57e3d4b6..3db964593 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -4096,22 +4096,27 @@ def set_temporal_aggregation(n, opts, solver_name): return n -def lossy_bidirectional_links(n, carrier, losses_per_thousand_km=0.0): +def lossy_bidirectional_links(n, carrier, efficiencies={}): "Split bidirectional links into two unidirectional links to include transmission losses." carrier_i = n.links.query("carrier == @carrier").index - if not losses_per_thousand_km or carrier_i.empty: + if not any(v != 1. for v in efficiencies.values()) or carrier_i.empty: return + efficiency_static = efficiencies.get("efficiency_static", 1) + efficiency_per_1000km = efficiencies.get("efficiency_per_1000km", 1) + logger.info( - f"Specified losses for {carrier} transmission. Splitting bidirectional links." + f"Specified losses for {carrier} transmission" + f"(static: {efficiency_static}, per 1000km: {efficiency_per_1000km})." + "Splitting bidirectional links." ) carrier_i = n.links.query("carrier == @carrier").index n.links.loc[carrier_i, "p_min_pu"] = 0 n.links.loc[carrier_i, "efficiency"] = ( - 1 - n.links.loc[carrier_i, "length"] * losses_per_thousand_km / 1e3 + efficiency_static * efficiency_per_1000km ** (n.links.loc[carrier_i, "length"] / 1e3) ) rev_links = ( n.links.loc[carrier_i].copy().rename({"bus0": "bus1", "bus1": "bus0"}, axis=1) @@ -4320,7 +4325,7 @@ def lossy_bidirectional_links(n, carrier, losses_per_thousand_km=0.0): if options["electricity_grid_connection"]: add_electricity_grid_connection(n, costs) - for k, v in options["transmission_losses"].items(): + for k, v in options["transmission_efficiency"].items(): lossy_bidirectional_links(n, k, v) # Workaround: Remove lines with conflicting (and unrealistic) properties From edd3e7bea2ec62f99bd8eeec79acaa58cf54970c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Aug 2023 12:32:08 +0000 Subject: [PATCH 092/293] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/prepare_sector_network.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 8e58cbf16..e8dd3ee3c 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -4102,7 +4102,7 @@ def lossy_bidirectional_links(n, carrier, efficiencies={}): carrier_i = n.links.query("carrier == @carrier").index - if not any(v != 1. for v in efficiencies.values()) or carrier_i.empty: + if not any(v != 1.0 for v in efficiencies.values()) or carrier_i.empty: return efficiency_static = efficiencies.get("efficiency_static", 1) @@ -4116,8 +4116,10 @@ def lossy_bidirectional_links(n, carrier, efficiencies={}): carrier_i = n.links.query("carrier == @carrier").index n.links.loc[carrier_i, "p_min_pu"] = 0 - n.links.loc[carrier_i, "efficiency"] = ( - efficiency_static * efficiency_per_1000km ** (n.links.loc[carrier_i, "length"] / 1e3) + n.links.loc[ + carrier_i, "efficiency" + ] = efficiency_static * efficiency_per_1000km ** ( + n.links.loc[carrier_i, "length"] / 1e3 ) rev_links = ( n.links.loc[carrier_i].copy().rename({"bus0": "bus1", "bus1": "bus0"}, axis=1) From b787c1cd5abb09e449ae34d1aacc84d7d54299d1 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 7 Aug 2023 17:11:23 +0200 Subject: [PATCH 093/293] biomass-to-liquid: fix unit of marginal cost --- scripts/prepare_sector_network.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index e8dd3ee3c..9fc88e0dc 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2485,7 +2485,7 @@ def add_biomass(n, costs): + costs.at["BtL", "CO2 stored"], p_nom_extendable=True, capital_cost=costs.at["BtL", "fixed"], - marginal_cost=costs.at["BtL", "efficiency"] * costs.loc["BtL", "VOM"], + marginal_cost=costs.loc["BtL", "VOM"] / costs.at["BtL", "efficiency"], ) # TODO: Update with energy penalty @@ -2506,7 +2506,8 @@ def add_biomass(n, costs): p_nom_extendable=True, capital_cost=costs.at["BtL", "fixed"] + costs.at["biomass CHP capture", "fixed"] * costs.at["BtL", "CO2 stored"], - marginal_cost=costs.at["BtL", "efficiency"] * costs.loc["BtL", "VOM"], + marginal_cost=costs.loc["BtL", "VOM"] / costs.at["BtL", "efficiency"], + ) ) # BioSNG from solid biomass From 70a09e551292725fd59a3907bac5f6ffc9a043b7 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 7 Aug 2023 17:12:31 +0200 Subject: [PATCH 094/293] add option for biomass-to-methanol route --- config/config.default.yaml | 3 ++- scripts/prepare_sector_network.py | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 252b00be1..7fb15d895 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -503,6 +503,7 @@ sector: conventional_generation: OCGT: gas biomass_to_liquid: false + biomass_to_methanol: false biosng: false endogenous_steel: false import: @@ -646,7 +647,7 @@ industry: # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#costs costs: year: 2030 - version: v0.6.2 + version: 5ce58f4 rooftop_share: 0.14 # based on the potentials, assuming (0.1 kW/m2 and 10 m2/person) fill_values: FOM: 0 diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 9fc88e0dc..6d7a4308a 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2508,6 +2508,27 @@ def add_biomass(n, costs): + costs.at["biomass CHP capture", "fixed"] * costs.at["BtL", "CO2 stored"], marginal_cost=costs.loc["BtL", "VOM"] / costs.at["BtL", "efficiency"], ) + + # biomethanol + + if options.get("biomass_to_methanol"): + + n.madd( + "Link", + spatial.biomass.nodes, + 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"], + ) ) # BioSNG from solid biomass From e7124c0e4e98d72e08ccfa2846b6cbd1d6e09f36 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 7 Aug 2023 17:13:00 +0200 Subject: [PATCH 095/293] biomass-to-methanol: add option with carbon capture --- scripts/prepare_sector_network.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 6d7a4308a..56b41beec 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2529,6 +2529,25 @@ def add_biomass(n, costs): 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"], ) + + n.madd( + "Link", + spatial.biomass.nodes, + 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"], ) # BioSNG from solid biomass From 9faa10bdec97eb7dcfb16f632dd217d742eeabf8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Aug 2023 15:13:24 +0000 Subject: [PATCH 096/293] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/prepare_sector_network.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 56b41beec..d13103a76 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2512,7 +2512,6 @@ def add_biomass(n, costs): # biomethanol if options.get("biomass_to_methanol"): - n.madd( "Link", spatial.biomass.nodes, @@ -2526,8 +2525,10 @@ def add_biomass(n, costs): 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"], + 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"], ) n.madd( @@ -2542,12 +2543,17 @@ def add_biomass(n, costs): 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"], + + 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"], + 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"], ) # BioSNG from solid biomass From 5822adb0ce97844036bab02a891cb41da5534f59 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Tue, 8 Aug 2023 17:56:22 +0200 Subject: [PATCH 097/293] add option to consider compression losses in pipelines as electricity demand --- config/config.default.yaml | 6 ++++-- scripts/prepare_sector_network.py | 9 +++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 4b613f4b5..c32671cd1 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -500,9 +500,11 @@ sector: efficiency_static: 0.98 efficiency_per_1000km: 0.977 H2 pipeline: - efficiency_per_1000km: 0.979 + efficiency_per_1000km: 1 # 0.979 + compression_per_1000km: 0.019 gas pipeline: - efficiency_per_1000km: 0.977 + efficiency_per_1000km: 1 #0.977 + compression_per_1000km: 0.01 H2_network: true gas_network: false H2_retrofit: false diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index d13103a76..7bef6bb68 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -4149,11 +4149,12 @@ def lossy_bidirectional_links(n, carrier, efficiencies={}): carrier_i = n.links.query("carrier == @carrier").index - if not any(v != 1.0 for v in efficiencies.values()) or carrier_i.empty: + if not any((v != 1.0) or (v >= 0) for v in efficiencies.values()) or carrier_i.empty: return efficiency_static = efficiencies.get("efficiency_static", 1) efficiency_per_1000km = efficiencies.get("efficiency_per_1000km", 1) + compression_per_1000km = efficiencies.get("compression_per_1000km", 0) logger.info( f"Specified losses for {carrier} transmission" @@ -4161,7 +4162,6 @@ def lossy_bidirectional_links(n, carrier, efficiencies={}): "Splitting bidirectional links." ) - carrier_i = n.links.query("carrier == @carrier").index n.links.loc[carrier_i, "p_min_pu"] = 0 n.links.loc[ carrier_i, "efficiency" @@ -4178,6 +4178,11 @@ def lossy_bidirectional_links(n, carrier, efficiencies={}): n.links = pd.concat([n.links, rev_links], sort=False) n.links["reversed"] = n.links["reversed"].fillna(False) + # do compression losses after concatenation to take electricity consumption at bus0 in either direction + carrier_i = n.links.query("carrier == @carrier").index + if compression_per_1000km > 0: + n.links.loc[carrier_i, "bus2"] = n.links.loc[carrier_i, "bus0"].map(n.buses.location) # electricity + n.links.loc[carrier_i, "efficiency2"] = - compression_per_1000km * n.links.loc[carrier_i, "length"] / 1e3 if __name__ == "__main__": if "snakemake" not in globals(): From beb755773502cebcc2b71e3d04d7c38eec866ea8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 8 Aug 2023 15:56:49 +0000 Subject: [PATCH 098/293] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/prepare_sector_network.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 7bef6bb68..0e8a8b1d5 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -4149,7 +4149,10 @@ def lossy_bidirectional_links(n, carrier, efficiencies={}): carrier_i = n.links.query("carrier == @carrier").index - if not any((v != 1.0) or (v >= 0) for v in efficiencies.values()) or carrier_i.empty: + if ( + not any((v != 1.0) or (v >= 0) for v in efficiencies.values()) + or carrier_i.empty + ): return efficiency_static = efficiencies.get("efficiency_static", 1) @@ -4181,8 +4184,13 @@ def lossy_bidirectional_links(n, carrier, efficiencies={}): # do compression losses after concatenation to take electricity consumption at bus0 in either direction carrier_i = n.links.query("carrier == @carrier").index if compression_per_1000km > 0: - n.links.loc[carrier_i, "bus2"] = n.links.loc[carrier_i, "bus0"].map(n.buses.location) # electricity - n.links.loc[carrier_i, "efficiency2"] = - compression_per_1000km * n.links.loc[carrier_i, "length"] / 1e3 + n.links.loc[carrier_i, "bus2"] = n.links.loc[carrier_i, "bus0"].map( + n.buses.location + ) # electricity + n.links.loc[carrier_i, "efficiency2"] = ( + -compression_per_1000km * n.links.loc[carrier_i, "length"] / 1e3 + ) + if __name__ == "__main__": if "snakemake" not in globals(): From ecbd32daab515e0e50ce6c5b6c5e458f7756c34a Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 9 Aug 2023 11:21:56 +0200 Subject: [PATCH 099/293] update shipping fuel efficiencies --- config/config.default.yaml | 7 ++++--- scripts/prepare_sector_network.py | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index c32671cd1..065e91885 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -411,8 +411,9 @@ sector: 2050: 0 shipping_methanol_efficiency: 0.46 shipping_oil_efficiency: 0.40 - shipping_ammonia_efficiency: 0.44 - shipping_lng_efficiency: 0.41 + shipping_ammonia_efficiency: 0.40 + shipping_lng_efficiency: 0.44 + shipping_lh2_efficiency: 0.44 shipping_endogenous: enable: false fuels: @@ -661,7 +662,7 @@ industry: # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#costs costs: year: 2030 - version: 5ce58f4 + version: 234dc24 rooftop_share: 0.14 # based on the potentials, assuming (0.1 kW/m2 and 10 m2/person) fill_values: FOM: 0 diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 0e8a8b1d5..d4ecde355 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3133,7 +3133,7 @@ def add_shipping(n, costs): oil_efficiency = options.get( "shipping_oil_efficiency", options.get("shipping_average_efficiency", 0.4) ) - efficiency = oil_efficiency / costs.at["fuel cell", "efficiency"] + efficiency = oil_efficiency / options.get("shipping_lh2_efficiency", 0.44) shipping_hydrogen_share = get( options["shipping_hydrogen_share"], investment_year ) @@ -3165,7 +3165,7 @@ def add_shipping(n, costs): shipping_bus = nodes + " H2" efficiency = ( - options["shipping_oil_efficiency"] / costs.at["fuel cell", "efficiency"] + options["shipping_oil_efficiency"] / options.get("shipping_lh2_efficiency", 0.44) ) p_set_hydrogen = shipping_hydrogen_share * p_set * efficiency @@ -3310,7 +3310,7 @@ def add_shipping_endogenous(n, costs): bus0=nodes + " H2", bus1=nodes + " shipping", carrier="shipping LH2", - efficiency=costs.at["fuel cell", "efficiency"] + efficiency=options["shipping_lh2_efficiency"] * costs.at["H2 liquefaction", "efficiency"], capital_cost=costs.at["H2 liquefaction", "fixed"], lifetime=costs.at["H2 liquefaction", "lifetime"], From 54aed5a9e21fd611692328cac296137a75e9704b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 9 Aug 2023 09:22:23 +0000 Subject: [PATCH 100/293] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- 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 d4ecde355..a87268108 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3164,8 +3164,8 @@ def add_shipping(n, costs): else: shipping_bus = nodes + " H2" - efficiency = ( - options["shipping_oil_efficiency"] / options.get("shipping_lh2_efficiency", 0.44) + efficiency = options["shipping_oil_efficiency"] / options.get( + "shipping_lh2_efficiency", 0.44 ) p_set_hydrogen = shipping_hydrogen_share * p_set * efficiency From 3faa3258f36c9414d8e946811acfff33628ba8f7 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 9 Aug 2023 12:04:29 +0200 Subject: [PATCH 101/293] add option for methanol-to-kerosene --- config/config.default.yaml | 3 ++- scripts/prepare_sector_network.py | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 065e91885..88ef63f59 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -464,6 +464,7 @@ sector: hydrogen_turbine: false SMR: true methanol_reforming: true + methanol_to_kerosene: true methanol_to_power: ccgt: true ccgt_cc: true @@ -662,7 +663,7 @@ industry: # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#costs costs: year: 2030 - version: 234dc24 + version: 9afdb4f rooftop_share: 0.14 # based on the potentials, assuming (0.1 kW/m2 and 10 m2/person) fill_values: FOM: 0 diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index d4ecde355..1b6565be3 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -753,6 +753,25 @@ def add_methanol_to_power(n, costs, types={}): ) +def add_methanol_to_kerosene(n, costs): + logger.info("Adding methanol-to-kerosene.") + + tech = "methanol-to-kerosene" + + n.madd( + "Link", + spatial.nodes, + suffix=f" {tech}", + bus0=spatial.methanol.nodes, + bus1=spatial.oil.nodes, + bus2=spatial.h2.nodes, + efficiency=costs.at[tech, "methanol-input"], + efficiency2=-costs.at[tech, "hydrogen-input"] / costs.at["methanol-input"], + p_nom_extendable=True, + p_min_pu=1, + ) + + def add_methanol_reforming(n, costs): logger.info("Adding methanol steam reforming.") @@ -4338,6 +4357,9 @@ def lossy_bidirectional_links(n, carrier, efficiencies={}): add_methanol_to_power(n, costs, types=options["methanol_to_power"]) + if options["methanol_to_kerosene"]: + add_methanol_to_kerosene(n, costs) + if options["methanol_reforming"]: add_methanol_reforming(n, costs) From cd1268e686071e327b20544266c26f801f40d4a2 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 9 Aug 2023 15:02:37 +0200 Subject: [PATCH 102/293] split HVC_production_today into ethylene, propylene and BTX --- config/config.default.yaml | 5 ++++- scripts/build_industrial_production_per_country.py | 2 +- scripts/build_industry_sector_ratios.py | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 88ef63f59..1b9c23fe3 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -647,7 +647,10 @@ industry: HVC_primary_fraction: 1. HVC_mechanical_recycling_fraction: 0. HVC_chemical_recycling_fraction: 0. - HVC_production_today: 52. + HVC_production_today: # DECHEMA report in Mt + ethylene: 21.7 + propylene: 17 + BTX: 15.7 MWh_elec_per_tHVC_mechanical_recycling: 0.547 MWh_elec_per_tHVC_chemical_recycling: 6.9 chlorine_production_today: 9.58 diff --git a/scripts/build_industrial_production_per_country.py b/scripts/build_industrial_production_per_country.py index 74cb19497..859bb8f66 100644 --- a/scripts/build_industrial_production_per_country.py +++ b/scripts/build_industrial_production_per_country.py @@ -264,7 +264,7 @@ def separate_basic_chemicals(demand, year): # assume HVC, methanol, chlorine production proportional to non-ammonia basic chemicals distribution_key = demand["Basic chemicals"] / demand["Basic chemicals"].sum() - demand["HVC"] = params["HVC_production_today"] * 1e3 * distribution_key + demand["HVC"] = sum(params["HVC_production_today"].values()) * 1e3 * distribution_key demand["Chlorine"] = params["chlorine_production_today"] * 1e3 * distribution_key demand["Methanol"] = params["methanol_production_today"] * 1e3 * distribution_key diff --git a/scripts/build_industry_sector_ratios.py b/scripts/build_industry_sector_ratios.py index 927170a11..dfbd4d645 100644 --- a/scripts/build_industry_sector_ratios.py +++ b/scripts/build_industry_sector_ratios.py @@ -383,7 +383,7 @@ def chemicals_industry(): assert s_emi.index[0] == sector # convert from MtHVC/a to ktHVC/a - s_out = params["HVC_production_today"] * 1e3 + s_out = sum(params["HVC_production_today"].values()) * 1e3 # tCO2/t material df.loc["process emission", sector] += ( From b4c2e31b2abb0873e11ec655ce9800b4cff085a3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 9 Aug 2023 13:03:20 +0000 Subject: [PATCH 103/293] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/build_industrial_production_per_country.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/build_industrial_production_per_country.py b/scripts/build_industrial_production_per_country.py index 859bb8f66..3e00e199c 100644 --- a/scripts/build_industrial_production_per_country.py +++ b/scripts/build_industrial_production_per_country.py @@ -264,7 +264,9 @@ def separate_basic_chemicals(demand, year): # assume HVC, methanol, chlorine production proportional to non-ammonia basic chemicals distribution_key = demand["Basic chemicals"] / demand["Basic chemicals"].sum() - demand["HVC"] = sum(params["HVC_production_today"].values()) * 1e3 * distribution_key + demand["HVC"] = ( + sum(params["HVC_production_today"].values()) * 1e3 * distribution_key + ) demand["Chlorine"] = params["chlorine_production_today"] * 1e3 * distribution_key demand["Methanol"] = params["methanol_production_today"] * 1e3 * distribution_key From d71cba54f4144fee5345d637be84191088fc9807 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 10 Aug 2023 12:26:24 +0200 Subject: [PATCH 104/293] improve logging for endogenous shipping --- 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 642ff28a1..90270516f 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3251,10 +3251,10 @@ def add_shipping(n, costs): def add_shipping_endogenous(n, costs): - logger.info("Add shipping demand endogenously") - fuels = options["shipping_endogenous"]["fuels"] + logger.info(f"Add shipping demand endogenously with options {fuels}") + nodes = pop_layout.index domestic_navigation = pop_weighted_energy_totals.loc[ From d6bb4d991c56dd9e727aa6eb47fff907385ee913 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 10 Aug 2023 12:27:37 +0200 Subject: [PATCH 105/293] add option to endogenise primary HVC via MTO/MTA or naphtha steam cracking (electric) --- config/config.default.yaml | 3 +- scripts/prepare_sector_network.py | 79 +++++++++++++++++++++++++++++-- 2 files changed, 78 insertions(+), 4 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 1b9c23fe3..0a9d6c60a 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -522,6 +522,7 @@ sector: biomass_to_methanol: false biosng: false endogenous_steel: false + endogenous_hvc: false import: capacity_boost: 2 limit: false # bool or number in TWh @@ -666,7 +667,7 @@ industry: # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#costs costs: year: 2030 - version: 9afdb4f + version: c338aa8 rooftop_share: 0.14 # based on the potentials, assuming (0.1 kW/m2 and 10 m2/person) fill_values: FOM: 0 diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 90270516f..f22a00030 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2646,7 +2646,11 @@ def add_industry(n, costs): snakemake.input.industry_sector_ratios, index_col=0 ) - endogenous_sectors = ["DRI + Electric arc"] if options["endogenous_steel"] else [] + endogenous_sectors = [] + if options["endogenous_steel"]: + endogenous_sectors += ["DRI + Electric arc"] + if options["endogenous_hvc"]: + endogenous_sectors += ["HVC"] sectors_b = ~industrial_demand.index.get_level_values("sector").isin( endogenous_sectors ) @@ -2896,13 +2900,82 @@ def add_industry(n, costs): ) demand_factor = options.get("HVC_demand_factor", 1) + if demand_factor != 1: + logger.warning(f"Changing HVC demand by {demand_factor*100-100:+.2f}%.") + + if options["endogenous_hvc"]: + logger.info("Adding endogenous primary high-value chemicals demand in tonnes.") + + n.add( + "Bus", + "EU HVC", + location="EU", + carrier="HVC", + unit="t", + ) + + p_set = demand_factor * industrial_production[sector].sum() / nhours + + n.add( + "Load", + "EU HVC", + bus="EU HVC", + carrier="HVC", + p_set=p_set, + ) + + n.add( + "Store", + "EU HVC Store", + bus="EU HVC", + e_nom_extendable=True, + e_cyclic=True, + carrier="HVC", + ) + + tech = "methanol-to-olefins/aromatics" + + n.madd( + "Link", + nodes, + suffix=" {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="EU HVC", + bus2=nodes, + bus3="co2 atmosphere", + efficiency=1 / costs.at[tech, "methanol-input"], + efficiency2=-costs.at[tech, "electricity-input"] / costs.at[tech, "methanol-input"], + efficiency3=costs.at[tech, "carbondioxide-output"] / costs.at[tech, "methanol-input"], + ) + + tech = "electric steam cracker" + + n.madd( + "Link", + nodes, + suffix=" {tech}", + carrier=tech, + capital_cost=costs.at[tech, "fixed"] / costs.at[tech, "naphtha-input"], + marginal_cost=costs.at[tech, "VOM"] / costs.at[tech, "naphtha-input"], + p_nom_extendable=True, + bus0=spatial.oil.nodes, + bus1="EU HVC", + bus2=nodes, + bus3="co2 atmosphere", + efficiency=1 / costs.at[tech, "naphtha-input"], + efficiency2=-costs.at[tech, "electricity-input"] / costs.at[tech, "naphtha-input"], + efficiency3=costs.at[tech, "carbondioxide-output"] / costs.at[tech, "naphtha-input"], + ) + p_set = ( demand_factor * industrial_demand.loc[(nodes, sectors_b), "naphtha"].sum() / nhours ) - if demand_factor != 1: - logger.warning(f"Changing HVC demand by {demand_factor*100-100:+.2f}%.") n.madd( "Load", From 991835a1074b69dc41a42b4cc8be9b3f4bf0afbf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 10 Aug 2023 10:28:10 +0000 Subject: [PATCH 106/293] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/prepare_sector_network.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index f22a00030..167fd5bda 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2948,8 +2948,10 @@ def add_industry(n, costs): bus2=nodes, bus3="co2 atmosphere", efficiency=1 / costs.at[tech, "methanol-input"], - efficiency2=-costs.at[tech, "electricity-input"] / costs.at[tech, "methanol-input"], - efficiency3=costs.at[tech, "carbondioxide-output"] / costs.at[tech, "methanol-input"], + efficiency2=-costs.at[tech, "electricity-input"] + / costs.at[tech, "methanol-input"], + efficiency3=costs.at[tech, "carbondioxide-output"] + / costs.at[tech, "methanol-input"], ) tech = "electric steam cracker" @@ -2967,8 +2969,10 @@ def add_industry(n, costs): bus2=nodes, bus3="co2 atmosphere", efficiency=1 / costs.at[tech, "naphtha-input"], - efficiency2=-costs.at[tech, "electricity-input"] / costs.at[tech, "naphtha-input"], - efficiency3=costs.at[tech, "carbondioxide-output"] / costs.at[tech, "naphtha-input"], + efficiency2=-costs.at[tech, "electricity-input"] + / costs.at[tech, "naphtha-input"], + efficiency3=costs.at[tech, "carbondioxide-output"] + / costs.at[tech, "naphtha-input"], ) p_set = ( From 10631cb0fddee794ecea50b02a6b8d69212293fb Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 10 Aug 2023 15:33:58 +0200 Subject: [PATCH 107/293] use country_converter to make geocoding of exporters more robust --- scripts/prepare_sector_network.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 167fd5bda..12f7baf83 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -29,6 +29,9 @@ from scipy.stats import beta from shapely.geometry import Point +import country_converter as coco +cc = coco.CountryConverter() + geolocator = Nominatim(user_agent="locate-exporting-region", timeout=10) logger = logging.getLogger(__name__) @@ -3622,7 +3625,8 @@ def add_endogenous_hvdc_import_options(n): ) def _coordinates(ct): - loc = geolocator.geocode(ct.split("-")[0]) + query = cc.convert(ct.split("-")[0], to="name") + loc = geolocator.geocode(query) return [loc.longitude, loc.latitude] exporters = pd.DataFrame( From 2673daea8c7766f95558ea727d22479c998a6095 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 10 Aug 2023 15:34:41 +0200 Subject: [PATCH 108/293] add_biomass: add methanol and/or oil carrier with BtL+BtMeoH --- scripts/prepare_sector_network.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 12f7baf83..16e19716c 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2493,6 +2493,9 @@ 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, @@ -2534,6 +2537,9 @@ def add_biomass(n, costs): # biomethanol if options.get("biomass_to_methanol"): + + add_carrier_buses(n, "methanol") + n.madd( "Link", spatial.biomass.nodes, From cb80419e8d443e6a46c936a0f1f09d0ffc056d4b Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 10 Aug 2023 15:35:01 +0200 Subject: [PATCH 109/293] add_waste_heat: add missing space to access methanolisation bus --- 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 16e19716c..06e5b7eff 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3479,7 +3479,7 @@ def add_waste_heat(n): ) if options["use_methanolisation_waste_heat"]: - n.links.loc[urban_central + "methanolisation", "bus4"] = ( + n.links.loc[urban_central + " methanolisation", "bus4"] = ( urban_central + "urban central heat" ) n.links.loc[urban_central + " methanolisation", "efficiency4"] = ( From 8455ba6be16bf3249a36bd63fb2a708e9cbac93a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 10 Aug 2023 13:35:27 +0000 Subject: [PATCH 110/293] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/prepare_sector_network.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 06e5b7eff..c84189e81 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -12,6 +12,7 @@ import re from itertools import product +import country_converter as coco import geopandas as gpd import networkx as nx import numpy as np @@ -29,7 +30,6 @@ from scipy.stats import beta from shapely.geometry import Point -import country_converter as coco cc = coco.CountryConverter() geolocator = Nominatim(user_agent="locate-exporting-region", timeout=10) @@ -2493,7 +2493,6 @@ def add_biomass(n, costs): # Solid biomass to liquid fuel if options["biomass_to_liquid"]: - add_carrier_buses(n, "oil") n.madd( @@ -2537,7 +2536,6 @@ def add_biomass(n, costs): # biomethanol if options.get("biomass_to_methanol"): - add_carrier_buses(n, "methanol") n.madd( From a3eb8f5052fd9579512d7080a7e07c8a4f36008c Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 10 Aug 2023 16:31:00 +0200 Subject: [PATCH 111/293] add_methanol_to_power: bugfix to include co2 emissions without carbon capture --- scripts/prepare_sector_network.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 06e5b7eff..251715853 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -693,11 +693,13 @@ def add_methanol_to_power(n, costs, types={}): 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, ) @@ -743,6 +745,7 @@ def add_methanol_to_power(n, costs, types={}): suffix=" OCGT methanol", bus0=spatial.methanol.nodes, bus1=nodes, + bus2="co2 atmosphere", carrier="OCGT methanol", p_nom_extendable=True, capital_cost=0.35 @@ -752,6 +755,7 @@ def add_methanol_to_power(n, costs, types={}): ), # efficiency * EUR/MW * (annuity + FOM) marginal_cost=2, efficiency=0.35, + efficiency2=costs.at["methanolisation", "carbondioxide-input"], lifetime=25, ) From b4d89e659684aa9512a3d7b1669f4750b4516095 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 10 Aug 2023 16:31:28 +0200 Subject: [PATCH 112/293] add_methanol_to_kerosene: bugfix to correctly access methanol-input --- 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 251715853..8816e886a 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -773,7 +773,7 @@ def add_methanol_to_kerosene(n, costs): bus1=spatial.oil.nodes, bus2=spatial.h2.nodes, efficiency=costs.at[tech, "methanol-input"], - efficiency2=-costs.at[tech, "hydrogen-input"] / costs.at["methanol-input"], + efficiency2=-costs.at[tech, "hydrogen-input"] / costs.at[tech, "methanol-input"], p_nom_extendable=True, p_min_pu=1, ) From 5cfbd5bdf661562d5b90f9b54a3c7e3287bddde9 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 10 Aug 2023 16:32:08 +0200 Subject: [PATCH 113/293] add rate limiter to Nominatim geocoding --- scripts/prepare_sector_network.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 8816e886a..786189602 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -10,6 +10,7 @@ import logging import os import re +import uuid from itertools import product import geopandas as gpd @@ -22,6 +23,7 @@ from add_electricity import calculate_annuity, sanitize_carriers from build_energy_totals import build_co2_totals, build_eea_co2, build_eurostat_co2 from geopy.geocoders import Nominatim +from geopy.extra.rate_limiter import RateLimiter from networkx.algorithms import complement from networkx.algorithms.connectivity.edge_augmentation import k_edge_augmentation from pypsa.geo import haversine_pts @@ -32,7 +34,8 @@ import country_converter as coco cc = coco.CountryConverter() -geolocator = Nominatim(user_agent="locate-exporting-region", timeout=10) +geolocator = Nominatim(user_agent=str(uuid.uuid4()), timeout=10) +geocode = RateLimiter(geolocator.geocode, min_delay_seconds=2) logger = logging.getLogger(__name__) @@ -3636,7 +3639,7 @@ def add_endogenous_hvdc_import_options(n): def _coordinates(ct): query = cc.convert(ct.split("-")[0], to="name") - loc = geolocator.geocode(query) + loc = geocode(dict(country=query), language='en') return [loc.longitude, loc.latitude] exporters = pd.DataFrame( From 1ee5278297fa1eef3a06eeb414ee69530436cbfa Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 10 Aug 2023 16:32:38 +0200 Subject: [PATCH 114/293] methanolisation waste heat: correctly access district heating bus --- 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 786189602..f7e89397e 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3487,7 +3487,7 @@ def add_waste_heat(n): if options["use_methanolisation_waste_heat"]: n.links.loc[urban_central + " methanolisation", "bus4"] = ( - urban_central + "urban central heat" + urban_central + " urban central heat" ) n.links.loc[urban_central + " methanolisation", "efficiency4"] = ( costs.at["methanolisation", "heat-output"] From 5a8bf57646200adbd39c54ed4fcc854389835a9b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 10 Aug 2023 14:33:27 +0000 Subject: [PATCH 115/293] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/prepare_sector_network.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index aa0825af2..4832ea263 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -23,8 +23,8 @@ from _helpers import generate_periodic_profiles, update_config_with_sector_opts from add_electricity import calculate_annuity, sanitize_carriers from build_energy_totals import build_co2_totals, build_eea_co2, build_eurostat_co2 -from geopy.geocoders import Nominatim from geopy.extra.rate_limiter import RateLimiter +from geopy.geocoders import Nominatim from networkx.algorithms import complement from networkx.algorithms.connectivity.edge_augmentation import k_edge_augmentation from pypsa.geo import haversine_pts @@ -776,7 +776,8 @@ def add_methanol_to_kerosene(n, costs): bus1=spatial.oil.nodes, bus2=spatial.h2.nodes, efficiency=costs.at[tech, "methanol-input"], - efficiency2=-costs.at[tech, "hydrogen-input"] / costs.at[tech, "methanol-input"], + efficiency2=-costs.at[tech, "hydrogen-input"] + / costs.at[tech, "methanol-input"], p_nom_extendable=True, p_min_pu=1, ) @@ -3637,7 +3638,7 @@ def add_endogenous_hvdc_import_options(n): def _coordinates(ct): query = cc.convert(ct.split("-")[0], to="name") - loc = geocode(dict(country=query), language='en') + loc = geocode(dict(country=query), language="en") return [loc.longitude, loc.latitude] exporters = pd.DataFrame( From 41ad553e81c1ca2121df3168fecc108455e0848f Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 10 Aug 2023 17:12:09 +0200 Subject: [PATCH 116/293] make MTO and steam cracker inflexible and prevent relocation --- scripts/prepare_sector_network.py | 32 +++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index aa0825af2..414507059 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2938,21 +2938,25 @@ def add_industry(n, costs): p_set=p_set, ) - n.add( - "Store", - "EU HVC Store", - bus="EU HVC", - e_nom_extendable=True, - e_cyclic=True, - carrier="HVC", - ) + # n.add( + # "Store", + # "EU HVC Store", + # bus="EU HVC", + # e_nom_extendable=True, + # e_cyclic=True, + # carrier="HVC", + # ) tech = "methanol-to-olefins/aromatics" + logger.info(f"Adding {tech}.") + + p_nom_max = demand_factor * industrial_production.loc[nodes, "HVC"] / nhours * costs.at[tech, "methanol-input"] + n.madd( "Link", nodes, - suffix=" {tech}", + 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"], @@ -2961,6 +2965,8 @@ def add_industry(n, costs): bus1="EU HVC", 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"], @@ -2970,10 +2976,14 @@ def add_industry(n, costs): tech = "electric steam cracker" + logger.info(f"Adding {tech}.") + + p_nom_max = demand_factor * industrial_production.loc[nodes, "HVC"] / nhours * costs.at[tech, "naphtha-input"] + n.madd( "Link", nodes, - suffix=" {tech}", + suffix=f" {tech}", carrier=tech, capital_cost=costs.at[tech, "fixed"] / costs.at[tech, "naphtha-input"], marginal_cost=costs.at[tech, "VOM"] / costs.at[tech, "naphtha-input"], @@ -2982,6 +2992,8 @@ def add_industry(n, costs): bus1="EU HVC", bus2=nodes, bus3="co2 atmosphere", + p_min_pu=1, + p_nom_max=p_nom_max.values, efficiency=1 / costs.at[tech, "naphtha-input"], efficiency2=-costs.at[tech, "electricity-input"] / costs.at[tech, "naphtha-input"], From 1543b80614e135f133493d07a3e01921ab4475f3 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 10 Aug 2023 17:12:30 +0200 Subject: [PATCH 117/293] endogenous_hvc: take correct sector --- 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 414507059..c0d8bd097 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2928,7 +2928,7 @@ def add_industry(n, costs): unit="t", ) - p_set = demand_factor * industrial_production[sector].sum() / nhours + p_set = demand_factor * industrial_production["HVC"].sum() / nhours n.add( "Load", From c67482fcd6e05f0ab60071be55a3e9970dd09ac6 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 10 Aug 2023 17:13:19 +0200 Subject: [PATCH 118/293] improve logging for lossy bidirectional links --- 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 c0d8bd097..5a39af13b 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -4283,8 +4283,8 @@ def lossy_bidirectional_links(n, carrier, efficiencies={}): compression_per_1000km = efficiencies.get("compression_per_1000km", 0) logger.info( - f"Specified losses for {carrier} transmission" - f"(static: {efficiency_static}, per 1000km: {efficiency_per_1000km})." + f"Specified losses for {carrier} transmission " + f"(static: {efficiency_static}, per 1000km: {efficiency_per_1000km}, compression per 1000km: {compression_per_1000km}). " "Splitting bidirectional links." ) From 4b5c09c616c7f2cf97f457fe50de362bc842e9c3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 10 Aug 2023 15:14:48 +0000 Subject: [PATCH 119/293] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/prepare_sector_network.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index f54dab931..1881ea738 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2952,7 +2952,12 @@ def add_industry(n, costs): logger.info(f"Adding {tech}.") - p_nom_max = demand_factor * industrial_production.loc[nodes, "HVC"] / nhours * costs.at[tech, "methanol-input"] + p_nom_max = ( + demand_factor + * industrial_production.loc[nodes, "HVC"] + / nhours + * costs.at[tech, "methanol-input"] + ) n.madd( "Link", @@ -2979,7 +2984,12 @@ def add_industry(n, costs): logger.info(f"Adding {tech}.") - p_nom_max = demand_factor * industrial_production.loc[nodes, "HVC"] / nhours * costs.at[tech, "naphtha-input"] + p_nom_max = ( + demand_factor + * industrial_production.loc[nodes, "HVC"] + / nhours + * costs.at[tech, "naphtha-input"] + ) n.madd( "Link", From bcfa6ceee2d010e94be55450c128526bbd79c65d Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 10 Aug 2023 20:42:41 +0200 Subject: [PATCH 120/293] mto/mta & steam cracker: correct carbon accounting --- 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 f54dab931..c78e5c61a 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2954,6 +2954,8 @@ def add_industry(n, costs): 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, @@ -2971,8 +2973,7 @@ def add_industry(n, costs): efficiency=1 / costs.at[tech, "methanol-input"], efficiency2=-costs.at[tech, "electricity-input"] / costs.at[tech, "methanol-input"], - efficiency3=costs.at[tech, "carbondioxide-output"] - / costs.at[tech, "methanol-input"], + efficiency3=co2_release, ) tech = "electric steam cracker" @@ -2981,6 +2982,8 @@ def add_industry(n, costs): p_nom_max = demand_factor * industrial_production.loc[nodes, "HVC"] / nhours * costs.at[tech, "naphtha-input"] + co2_release = costs.at[tech, "carbondioxide-output"] / costs.at[tech, "naphtha-input"] + costs.at["oil", "CO2 intensity"] + n.madd( "Link", nodes, @@ -2998,8 +3001,7 @@ def add_industry(n, costs): efficiency=1 / costs.at[tech, "naphtha-input"], efficiency2=-costs.at[tech, "electricity-input"] / costs.at[tech, "naphtha-input"], - efficiency3=costs.at[tech, "carbondioxide-output"] - / costs.at[tech, "naphtha-input"], + efficiency3=co2_release, ) p_set = ( From b161c774a2757d78c284460f688a8d3bbb286e73 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 10 Aug 2023 20:43:03 +0200 Subject: [PATCH 121/293] config: add preliminary additional tech_colors --- config/config.default.yaml | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 0a9d6c60a..472fee6b8 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -1092,9 +1092,30 @@ plotting: import shipping-lch4: '#d6cbb2' import shipping-lnh3: '#c6c093' import shipping-ftfuel: '#bdb093' - import hvdc-to-elec: '#91856a' + import shipping-meoh: '#91856a' + import shipping-steel: '#736c5d' + import hvdc-to-elec: '#47443d' external H2: '#f7cdf0' + external H2 Turbine: '#991f83' + external H2 Electrolysis: '#991f83' + external battery discharger: '#dff7cb' + external battery charger: '#dff7cb' external battery: '#dff7cb' external offwind: '#d0e8f5' external onwind: '#accbfc' external solar: '#fcf1c7' + external solar-utility: '#fcf1c7' + external HVDC: "#8a1caf" + steel: '#b1bbc9' + DRI + Electric arc: '#b1bbc9' + shipping ammonia: '#46caf0' + Methanol steam reforming CC: '#468c8b' + biomass to methanol CC: '#468c8b' + biomass to methanol: '#468c8b' + Methanol steam reforming: '#468c8b' + shipping LNG: '#e0986c' + industry methanol: '#468c8b' + industry methanol emissions: '#468c8b' + HVC: '#012345' + methanol-to-olefins/aromatics: '#012345' + electric steam cracker: '#012345' From 2c1e7e55e245a91db69bf7d05b24ce77f217cecc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 10 Aug 2023 18:44:32 +0000 Subject: [PATCH 122/293] [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, 8 insertions(+), 2 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 484ff4ce4..c13589a47 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2959,7 +2959,10 @@ def add_industry(n, costs): * costs.at[tech, "methanol-input"] ) - co2_release = costs.at[tech, "carbondioxide-output"] / costs.at[tech, "methanol-input"] + costs.at["methanolisation", "carbondioxide-input"] + co2_release = ( + costs.at[tech, "carbondioxide-output"] / costs.at[tech, "methanol-input"] + + costs.at["methanolisation", "carbondioxide-input"] + ) n.madd( "Link", @@ -2992,7 +2995,10 @@ def add_industry(n, costs): * costs.at[tech, "naphtha-input"] ) - co2_release = costs.at[tech, "carbondioxide-output"] / costs.at[tech, "naphtha-input"] + costs.at["oil", "CO2 intensity"] + co2_release = ( + costs.at[tech, "carbondioxide-output"] / costs.at[tech, "naphtha-input"] + + costs.at["oil", "CO2 intensity"] + ) n.madd( "Link", From e779b430c1210af8297e90360aaf4c0fd9d49ffe Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 10 Aug 2023 21:57:33 +0200 Subject: [PATCH 123/293] adjust plotting to new technologies --- scripts/make_summary.py | 5 ++++- scripts/plot_summary.py | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/make_summary.py b/scripts/make_summary.py index 43ecf2535..f914ea719 100644 --- a/scripts/make_summary.py +++ b/scripts/make_summary.py @@ -686,8 +686,11 @@ def to_csv(df): logging.basicConfig(level=snakemake.config["logging"]["level"]) + s = snakemake.input.networks[0] + base_dir = s[:s.find('results/')+8] + networks_dict = { - (cluster, ll, opt + sector_opt, planning_horizon): "results/" + (cluster, ll, opt + sector_opt, planning_horizon): base_dir + snakemake.params.RDIR + f"/postnetworks/elec_s{simpl}_{cluster}_l{ll}_{opt}_{sector_opt}_{planning_horizon}.nc" for simpl in snakemake.params.scenario["simpl"] diff --git a/scripts/plot_summary.py b/scripts/plot_summary.py index c257a9c38..ad844b95b 100644 --- a/scripts/plot_summary.py +++ b/scripts/plot_summary.py @@ -287,10 +287,11 @@ def plot_balances(): "import shipping-lh2", "import shipping-lnh3", "import pipeline-h2", + "import shipping-lch4", "NH3", ] df.index = [ - i[:-1] if ((i not in forbidden) and (i[-1:] in ["0", "1", "2", "3"])) else i + i[:-1] if ((i not in forbidden) and (i[-1:] in ["0", "1", "2", "3", "4"])) else i for i in df.index ] From 145a218a6f6882cf89de9def5a887779ac0a79b9 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 10 Aug 2023 21:57:58 +0200 Subject: [PATCH 124/293] add fossil generator for carrier only if fuel cost > 0 --- 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 484ff4ce4..2d00d8826 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -475,7 +475,7 @@ def add_carrier_buses(n, carrier, nodes=None): capital_cost=capital_cost, ) - if carrier in costs.index and "fuel" in costs.columns: + if carrier in costs.index and costs.at[carrier, "fuel"] > 0: n.madd( "Generator", nodes, From ebf4fdbe0da9ae2a1d81f448562631657f86406f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 10 Aug 2023 20:00:36 +0000 Subject: [PATCH 125/293] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/make_summary.py | 2 +- scripts/plot_summary.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/make_summary.py b/scripts/make_summary.py index f914ea719..54495fa88 100644 --- a/scripts/make_summary.py +++ b/scripts/make_summary.py @@ -687,7 +687,7 @@ def to_csv(df): logging.basicConfig(level=snakemake.config["logging"]["level"]) s = snakemake.input.networks[0] - base_dir = s[:s.find('results/')+8] + base_dir = s[: s.find("results/") + 8] networks_dict = { (cluster, ll, opt + sector_opt, planning_horizon): base_dir diff --git a/scripts/plot_summary.py b/scripts/plot_summary.py index ad844b95b..ce7111fd6 100644 --- a/scripts/plot_summary.py +++ b/scripts/plot_summary.py @@ -291,7 +291,9 @@ def plot_balances(): "NH3", ] df.index = [ - i[:-1] if ((i not in forbidden) and (i[-1:] in ["0", "1", "2", "3", "4"])) else i + i[:-1] + if ((i not in forbidden) and (i[-1:] in ["0", "1", "2", "3", "4"])) + else i for i in df.index ] From 692fb053885c28f138ff239b76df7d4ff7045ded Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 11 Aug 2023 09:23:01 +0200 Subject: [PATCH 126/293] add option to vary cost of imports in wildcard, e.g. AC0.8 --- scripts/prepare_sector_network.py | 42 +++++++++++++++++++------------ 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index e2c36123a..ee4038cbb 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3654,7 +3654,7 @@ def remove_h2_network(n): n.stores.drop("EU H2 Store", inplace=True) -def add_endogenous_hvdc_import_options(n): +def add_endogenous_hvdc_import_options(n, cost_factor=1.): logger.info("Add import options: endogenous hvdc-to-elec") cf = snakemake.config["sector"]["import"].get("endogenous_hvdc_import", {}) if not cf["enable"]: @@ -3722,7 +3722,7 @@ def _coordinates(ct): p_min_pu=0, p_nom_extendable=True, length=import_links.values, - capital_cost=hvdc_cost, + capital_cost=hvdc_cost * cost_factor, efficiency=1 - import_links.values * cf["hvdc_losses"], ) @@ -3738,7 +3738,7 @@ def _coordinates(ct): bus=exporters_tech_i, carrier=f"external {tech}", p_nom_extendable=True, - capital_cost=costs.at[tech, "fixed"], + capital_cost=costs.at[tech, "fixed"] * cost_factor, lifetime=costs.at[tech, "lifetime"], p_max_pu=p_max_pu_tech.reindex(columns=exporters_tech_i), ) @@ -3758,7 +3758,7 @@ def _coordinates(ct): e_cyclic=True, capital_cost=costs.at[ "hydrogen storage tank type 1 including compressor", "fixed" - ], + ] * cost_factor, ) n.madd( @@ -3769,7 +3769,7 @@ def _coordinates(ct): carrier="external H2 Electrolysis", p_nom_extendable=True, efficiency=costs.at["electrolysis", "efficiency"], - capital_cost=costs.at["electrolysis", "fixed"], + capital_cost=costs.at["electrolysis", "fixed"] * cost_factor, lifetime=costs.at["electrolysis", "lifetime"], ) @@ -3781,7 +3781,7 @@ def _coordinates(ct): carrier="external H2 Turbine", p_nom_extendable=True, efficiency=costs.at["OCGT", "efficiency"], - capital_cost=costs.at["OCGT", "fixed"] * costs.at["OCGT", "efficiency"], + capital_cost=costs.at["OCGT", "fixed"] * costs.at["OCGT", "efficiency"] * cost_factor, lifetime=costs.at["OCGT", "lifetime"], ) @@ -3798,7 +3798,7 @@ def _coordinates(ct): carrier="external battery", e_cyclic=True, e_nom_extendable=True, - capital_cost=costs.at["battery storage", "fixed"], + capital_cost=costs.at["battery storage", "fixed"] * cost_factor, lifetime=costs.at["battery storage", "lifetime"], ) @@ -3809,7 +3809,7 @@ def _coordinates(ct): bus1=b_buses_i, carrier="external battery charger", efficiency=costs.at["battery inverter", "efficiency"] ** 0.5, - capital_cost=costs.at["battery inverter", "fixed"], + capital_cost=costs.at["battery inverter", "fixed"] * cost_factor, p_nom_extendable=True, lifetime=costs.at["battery inverter", "lifetime"], ) @@ -3847,7 +3847,7 @@ def _coordinates(ct): carrier="external HVDC", p_min_pu=-1, p_nom_extendable=True, - capital_cost=capital_cost, + capital_cost=capital_cost * cost_factor, length=d, ) @@ -3867,7 +3867,10 @@ def add_import_options( ], endogenous_hvdc=False, ): - logger.info("Add import options: " + " ".join(import_options)) + if not isinstance(import_options, dict): + import_options = {k: 1. for k in import_options} + + logger.info("Add import options: " + " ".join(import_options.keys())) fn = snakemake.input.gas_input_nodes_simplified import_nodes = pd.read_csv(fn, index_col=0) import_nodes["hvdc-to-elec"] = 15000 @@ -3910,8 +3913,8 @@ def add_import_options( ports[k] = ports.get(v) if endogenous_hvdc and "hvdc-to-elec" in import_options: - import_options = [o for o in import_options if o != "hvdc-to-elec"] - add_endogenous_hvdc_import_options(n) + import_options.pop("hvdc-to-elec") + add_endogenous_hvdc_import_options(n, import_options["hvdc-to-elec"]) regionalised_options = { "hvdc-to-elec", @@ -3923,7 +3926,7 @@ def add_import_options( for tech in set(import_options).intersection(regionalised_options): import_costs_tech = ( import_costs.query("esc == @tech").groupby("importer").marginal_cost.min() - ) + ) * import_options[tech] sel = ~import_nodes[tech].isna() if tech == "pipeline-h2": @@ -3998,7 +4001,7 @@ def add_import_options( } for tech in set(import_options).intersection(copperplated_carbonaceous_options): - marginal_costs = import_costs.query("esc == @tech").marginal_cost.min() + marginal_costs = import_costs.query("esc == @tech").marginal_cost.min() * import_options[tech] suffix = bus_suffix[tech] @@ -4035,7 +4038,7 @@ def add_import_options( for tech in set(import_options).intersection(copperplated_carbonfree_options): suffix = bus_suffix[tech] - marginal_costs = import_costs.query("esc == @tech").marginal_cost.min() + marginal_costs = import_costs.query("esc == @tech").marginal_cost.min() * import_options[tech] n.add( "Generator", @@ -4464,7 +4467,14 @@ def lossy_bidirectional_links(n, carrier, efficiencies={}): continue subsets = o.split("+")[1:] if len(subsets): - carriers = sum([translate[s] for s in subsets], []) + def parse_carriers(s): + prefixes = sorted(translate.keys(), key=lambda k: len(k), reverse=True) + pattern = fr'({"|".join(prefixes)})(\d+(\.\d+)?)?' + match = re.search(pattern, s) + prefix = match.group(1) if match else None + number = float(match.group(2)) if match and match.group(2) else 1.0 + return {prefix: number} + carriers = {tk: v for s in subsets for k, v in parse_carriers(s).items() for tk in translate.get(k, [])} else: carriers = options["import"]["options"] add_import_options( From afed9d152b143e27dc4e4b15437908609305eb2e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 11 Aug 2023 07:23:32 +0000 Subject: [PATCH 127/293] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/prepare_sector_network.py | 32 +++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index ee4038cbb..58019cf57 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3654,7 +3654,7 @@ def remove_h2_network(n): n.stores.drop("EU H2 Store", inplace=True) -def add_endogenous_hvdc_import_options(n, cost_factor=1.): +def add_endogenous_hvdc_import_options(n, cost_factor=1.0): logger.info("Add import options: endogenous hvdc-to-elec") cf = snakemake.config["sector"]["import"].get("endogenous_hvdc_import", {}) if not cf["enable"]: @@ -3758,7 +3758,8 @@ def _coordinates(ct): e_cyclic=True, capital_cost=costs.at[ "hydrogen storage tank type 1 including compressor", "fixed" - ] * cost_factor, + ] + * cost_factor, ) n.madd( @@ -3781,7 +3782,9 @@ def _coordinates(ct): carrier="external H2 Turbine", p_nom_extendable=True, efficiency=costs.at["OCGT", "efficiency"], - capital_cost=costs.at["OCGT", "fixed"] * costs.at["OCGT", "efficiency"] * cost_factor, + capital_cost=costs.at["OCGT", "fixed"] + * costs.at["OCGT", "efficiency"] + * cost_factor, lifetime=costs.at["OCGT", "lifetime"], ) @@ -3868,7 +3871,7 @@ def add_import_options( endogenous_hvdc=False, ): if not isinstance(import_options, dict): - import_options = {k: 1. for k in import_options} + import_options = {k: 1.0 for k in import_options} logger.info("Add import options: " + " ".join(import_options.keys())) fn = snakemake.input.gas_input_nodes_simplified @@ -4001,7 +4004,10 @@ def add_import_options( } for tech in set(import_options).intersection(copperplated_carbonaceous_options): - marginal_costs = import_costs.query("esc == @tech").marginal_cost.min() * import_options[tech] + marginal_costs = ( + import_costs.query("esc == @tech").marginal_cost.min() + * import_options[tech] + ) suffix = bus_suffix[tech] @@ -4038,7 +4044,10 @@ def add_import_options( for tech in set(import_options).intersection(copperplated_carbonfree_options): suffix = bus_suffix[tech] - marginal_costs = import_costs.query("esc == @tech").marginal_cost.min() * import_options[tech] + marginal_costs = ( + import_costs.query("esc == @tech").marginal_cost.min() + * import_options[tech] + ) n.add( "Generator", @@ -4467,14 +4476,21 @@ def lossy_bidirectional_links(n, carrier, efficiencies={}): continue subsets = o.split("+")[1:] if len(subsets): + def parse_carriers(s): prefixes = sorted(translate.keys(), key=lambda k: len(k), reverse=True) - pattern = fr'({"|".join(prefixes)})(\d+(\.\d+)?)?' + pattern = rf'({"|".join(prefixes)})(\d+(\.\d+)?)?' match = re.search(pattern, s) prefix = match.group(1) if match else None number = float(match.group(2)) if match and match.group(2) else 1.0 return {prefix: number} - carriers = {tk: v for s in subsets for k, v in parse_carriers(s).items() for tk in translate.get(k, [])} + + carriers = { + tk: v + for s in subsets + for k, v in parse_carriers(s).items() + for tk in translate.get(k, []) + } else: carriers = options["import"]["options"] add_import_options( From 7993b0f9192a46e9688a18a90b337cd2ae8326c5 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 11 Aug 2023 09:24:20 +0200 Subject: [PATCH 128/293] use DISTANCE_CRS rather than hardcoded 3857 projection --- 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 ee4038cbb..43513d434 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3830,8 +3830,8 @@ def _coordinates(ct): for bus0_bus1 in cf.get("extra_connections", []): bus0, bus1 = bus0_bus1.split("-") - a = exporters.to_crs(3857).at[bus0, "geometry"] - b = exporters.to_crs(3857).at[bus1, "geometry"] + a = exporters.to_crs(DISTANCE_CRS).at[bus0, "geometry"] + b = exporters.to_crs(DISTANCE_CRS).at[bus1, "geometry"] d = a.distance(b) / 1e3 # km capital_cost = ( From 4edf5500ba20387b57e7c350ddf9336ee900c621 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 11 Aug 2023 12:07:03 +0200 Subject: [PATCH 129/293] retrieve.smk: add scigrid storages to files of interest --- rules/retrieve.smk | 1 + 1 file changed, 1 insertion(+) diff --git a/rules/retrieve.smk b/rules/retrieve.smk index 0b60ee2ee..37e943635 100644 --- a/rules/retrieve.smk +++ b/rules/retrieve.smk @@ -138,6 +138,7 @@ if config["enable"]["retrieve"] and ( "IGGIELGN_LNGs.geojson", "IGGIELGN_BorderPoints.geojson", "IGGIELGN_Productions.geojson", + "IGGIELGN_Storages.geojson", "IGGIELGN_PipeSegments.geojson", ] From d3f731850d3278976a8ed815dfe7354c9383437b Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 11 Aug 2023 12:07:25 +0200 Subject: [PATCH 130/293] use technology-data values for H2-DRI + EAF steel production --- scripts/prepare_sector_network.py | 41 +++++++++++++------------------ 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 977ca81ce..c5152c01e 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -674,11 +674,11 @@ def add_methanol_to_power(n, costs, types={}): bus3="co2 atmosphere", carrier="allam methanol", p_nom_extendable=True, - capital_cost=0.66 + capital_cost=0.59 * 1.832e6 * calculate_annuity(25, 0.07), # efficiency * EUR/MW * annuity marginal_cost=2, - efficiency=0.66, + efficiency=0.59, efficiency2=0.98 * costs.at["methanolisation", "carbondioxide-input"], efficiency3=0.02 * costs.at["methanolisation", "carbondioxide-input"], lifetime=25, @@ -2657,9 +2657,6 @@ def add_industry(n, costs): * 1e3 * nyears # kt/a -> t/a ) - industry_sector_ratios = pd.read_csv( - snakemake.input.industry_sector_ratios, index_col=0 - ) endogenous_sectors = [] if options["endogenous_steel"]: @@ -2700,29 +2697,31 @@ def add_industry(n, costs): # carrier="steel", # ) - ratio = industry_sector_ratios[sector] + electricity_input = ( + costs.at["direct iron reduction furnace", "electricity-input"] + * costs.at["electric arc furnace", "hbi-input"] + + costs.at["electric arc furnace", "electricity-input"] + ) - # so that for each region supply matches consumption - p_nom = industrial_production[sector] * ratio["elec"] / nhours + hydrogen_input = ( + costs.at["direct iron reduction furnace", "hydrogen-input"] + * costs.at["electric arc furnace", "hbi-input"] + ) - bus5 = [ - node + " urban central heat" - if node + " urban central heat" in n.buses.index - else node + " services urban decentral heat" - for node in nodes - ] + # so that for each region supply matches consumption + p_nom = industrial_production[sector] * electricity_input / nhours marginal_cost = ( costs.at["iron ore DRI-ready", "commodity"] * costs.at["direct iron reduction furnace", "ore-input"] * costs.at["electric arc furnace", "hbi-input"] - / ratio["elec"] + / electricity_input ) capital_cost = ( costs.at["direct iron reduction furnace", "fixed"] + costs.at["electric arc furnace", "fixed"] - ) / ratio["elec"] + ) / electricity_input n.madd( "Link", @@ -2736,14 +2735,8 @@ def add_industry(n, costs): bus0=nodes, bus1="EU steel", bus2=nodes + " H2", - bus3=spatial.gas.nodes, - bus4="co2 atmosphere", - bus5=bus5, - efficiency=1 / ratio["elec"], - efficiency2=-ratio["hydrogen"] / ratio["elec"], - efficiency3=-ratio["methane"] / ratio["elec"], - efficiency4=ratio["process emission"] / ratio["elec"], - efficiency5=-ratio["heat"] / ratio["elec"], + efficiency=1 / electricity_input, + efficiency2=-hydrogen_input / electricity_input, ) n.madd( From 8bdfb8ad83dc5e30e59257a115b219387ff3da24 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 11 Aug 2023 16:00:11 +0200 Subject: [PATCH 131/293] correctly handle dictionary config in endogenous_hvdc --- 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 c5152c01e..c2f9dc7c1 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3909,8 +3909,7 @@ def add_import_options( ports[k] = ports.get(v) if endogenous_hvdc and "hvdc-to-elec" in import_options: - import_options.pop("hvdc-to-elec") - add_endogenous_hvdc_import_options(n, import_options["hvdc-to-elec"]) + add_endogenous_hvdc_import_options(n, import_options.pop("hvdc-to-elec")) regionalised_options = { "hvdc-to-elec", From c18675f911afabff1a237f48476395778bed7fd7 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 11 Aug 2023 16:00:27 +0200 Subject: [PATCH 132/293] add matplotlibrc to input files --- matplotlibrc | 2 +- rules/postprocess.smk | 2 ++ scripts/plot_network.py | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/matplotlibrc b/matplotlibrc index f00ed5cd5..6f4dcf450 100644 --- a/matplotlibrc +++ b/matplotlibrc @@ -2,6 +2,6 @@ # # SPDX-License-Identifier: CC0-1.0 font.family: sans-serif -font.sans-serif: Ubuntu, DejaVu Sans +font.sans-serif: Roboto, Ubuntu, DejaVu Sans image.cmap: viridis figure.autolayout : True diff --git a/rules/postprocess.smk b/rules/postprocess.smk index 2618680e7..ed5a95579 100644 --- a/rules/postprocess.smk +++ b/rules/postprocess.smk @@ -16,6 +16,7 @@ rule plot_network: network=RESULTS + "postnetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", regions=RESOURCES + "regions_onshore_elec_s{simpl}_{clusters}.geojson", + rc="matplotlibrc", output: map=RESULTS + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-costs-all_{planning_horizons}.pdf", @@ -113,6 +114,7 @@ rule plot_summary: energy=RESULTS + "csvs/energy.csv", balances=RESULTS + "csvs/supply_energy.csv", eurostat=input_eurostat, + rc="matplotlibrc", output: costs=RESULTS + "graphs/costs.pdf", energy=RESULTS + "graphs/energy.pdf", diff --git a/scripts/plot_network.py b/scripts/plot_network.py index d59aae3d0..93ca41847 100644 --- a/scripts/plot_network.py +++ b/scripts/plot_network.py @@ -24,8 +24,6 @@ from plot_summary import preferred_order, rename_techs from pypsa.plot import add_legend_circles, add_legend_lines, add_legend_patches -plt.style.use(["ggplot", "matplotlibrc"]) - def rename_techs_tyndp(tech): tech = rename_techs(tech) @@ -931,6 +929,8 @@ def plot_series(network, carrier="AC", name="test"): logging.basicConfig(level=snakemake.config["logging"]["level"]) + plt.style.use(["ggplot", snakemake.input.rc]) + n = pypsa.Network(snakemake.input.network) regions = gpd.read_file(snakemake.input.regions).set_index("name") From fe083835c3ee6e7ac5095a8ab64bcdc0e8eff520 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 14 Aug 2023 18:22:47 +0200 Subject: [PATCH 133/293] update matplotlibrc --- matplotlibrc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/matplotlibrc b/matplotlibrc index 6f4dcf450..16cedd1c3 100644 --- a/matplotlibrc +++ b/matplotlibrc @@ -5,3 +5,6 @@ font.family: sans-serif font.sans-serif: Roboto, Ubuntu, DejaVu Sans image.cmap: viridis figure.autolayout : True +figure.constrained_layout.use : True +figure.dpi : 400 +legend.frameon : False \ No newline at end of file From 124bd719759d3d4edb6da032a1590eacf17e29d6 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 14 Aug 2023 18:24:59 +0200 Subject: [PATCH 134/293] plot resources: power network unclustered --- Snakefile | 1 + rules/plot.smk | 12 +++++ scripts/plot_power_network_unclustered.py | 54 +++++++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 rules/plot.smk create mode 100644 scripts/plot_power_network_unclustered.py diff --git a/Snakefile b/Snakefile index fe282a98b..85777de6c 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/plot.smk" if config["foresight"] == "overnight": diff --git a/rules/plot.smk b/rules/plot.smk new file mode 100644 index 000000000..6b5bfc045 --- /dev/null +++ b/rules/plot.smk @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2023- The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT + +rule plot_power_network_unclustered: + input: + network=RESOURCES + "networks/elec.nc", + rc="matplotlibrc", + output: + multiext(RESOURCES + "graphics/power-network-unclustered", ".png", ".pdf") + script: + "../scripts/plot_power_network_unclustered.py" diff --git a/scripts/plot_power_network_unclustered.py b/scripts/plot_power_network_unclustered.py new file mode 100644 index 000000000..de2491d98 --- /dev/null +++ b/scripts/plot_power_network_unclustered.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2023- Fabian Neumann +# +# SPDX-License-Identifier: MIT +""" +Plot unclustered electricity transmission network. +""" + +import pypsa +import matplotlib.pyplot as plt +import cartopy.crs as ccrs +from matplotlib.lines import Line2D + + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake("plot_power_network_topology") + + plt.style.use(snakemake.input.rc) + + n = pypsa.Network(snakemake.input.network) + + w = n.lines.v_nom.div(380) + c = n.lines.v_nom.map({220: "teal", 300: "orange", 380: "firebrick"}) + + fig, ax = plt.subplots(figsize=(13, 13), subplot_kw={"projection": ccrs.EqualEarth()}) + + n.plot( + ax=ax, + bus_sizes=0, + line_widths=w, + line_colors=c, + link_colors="royalblue", + link_widths=1, + ) + + handles = [ + Line2D([0], [0], color="teal", lw=2), + Line2D([0], [0], color="orange", lw=2), + Line2D([0], [0], color="firebrick", lw=2), + Line2D([0], [0], color="royalblue", lw=2), + ] + plt.legend( + handles, + ["HVAC 220 kV", "HVAC 300 kV", "HVAC 380 kV", "HVDC"], + frameon=False, + loc=[0.2, 0.85], + fontsize=14, + ) + + for fn in snakemake.output: + plt.savefig(fn) From f5281a76f4e48c8363148e43d1f3ec985d9efc88 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 14 Aug 2023 18:25:22 +0200 Subject: [PATCH 135/293] plot resources: gas network unclustered --- rules/plot.smk | 16 +++++ scripts/plot_gas_network_unclustered.py | 89 +++++++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 scripts/plot_gas_network_unclustered.py diff --git a/rules/plot.smk b/rules/plot.smk index 6b5bfc045..d8f2cc355 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -10,3 +10,19 @@ rule plot_power_network_unclustered: multiext(RESOURCES + "graphics/power-network-unclustered", ".png", ".pdf") script: "../scripts/plot_power_network_unclustered.py" + + +rule plot_gas_network_unclustered: + input: + gas_network=RESOURCES + "gas_network.csv", + gem=HTTP.remote( + "https://globalenergymonitor.org/wp-content/uploads/2023/07/Europe-Gas-Tracker-2023-03-v3.xlsx", + keep_local=True, + ), + entry="data/gas_network/scigrid-gas/data/IGGIELGN_BorderPoints.geojson", + storage="data/gas_network/scigrid-gas/data/IGGIELGN_Storages.geojson", + rc="matplotlibrc", + output: + multiext(RESOURCES + "graphics/gas-network-unclustered", ".png", ".pdf") + script: + "../scripts/plot_gas_network_unclustered.py" diff --git a/scripts/plot_gas_network_unclustered.py b/scripts/plot_gas_network_unclustered.py new file mode 100644 index 000000000..34185e20e --- /dev/null +++ b/scripts/plot_gas_network_unclustered.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2023- Fabian Neumann +# +# SPDX-License-Identifier: MIT +""" +Plot unclustered gas transmission network. +""" + +import pandas as pd +import numpy as np +from shapely import wkt +import geopandas as gpd +import matplotlib.pyplot as plt +import cartopy.crs as ccrs +import cartopy +from build_gas_input_locations import build_gas_input_locations +import matplotlib.colors as mcolors +from matplotlib.ticker import LogFormatter + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake("plot_gas_network_unclustered") + + plt.style.use(snakemake.input.rc) + + countries = snakemake.config["countries"] + + pts = build_gas_input_locations( + snakemake.input.gem, + snakemake.input.entry, + snakemake.input.storage, + countries, + ) + + sums = pts.groupby("type").capacity.sum() + + for t in ["lng", "production", "pipeline", "storage"]: + pts.loc[pts["type"] == t, "capacity"] /= sums[t] + + pts["type"] = pts["type"].replace( + dict(production="Fossil Extraction", lng="LNG Terminal", pipeline="Entrypoint", storage="Storage") + ) + + df = pd.read_csv(snakemake.input.gas_network, index_col=0) + for col in ["geometry"]: + df[col] = df[col].apply(wkt.loads) + + df = gpd.GeoDataFrame(df, geometry="geometry", crs="EPSG:4326") + + crs = ccrs.EqualEarth() + + df = df.to_crs(crs.proj4_init) + pts = pts.to_crs(crs.proj4_init) + + fig, ax = plt.subplots(figsize=(9.5, 9.5), subplot_kw={"projection": crs}) + + ax.add_feature(cartopy.feature.COASTLINE.with_scale("50m"), linewidth=0.5, zorder=2) + ax.add_feature(cartopy.feature.BORDERS.with_scale("50m"), linewidth=0.5, zorder=2) + + vmax = 100 + + df.plot( + ax=ax, + column=df["p_nom"].div(1e3).fillna(0.), + linewidths=np.log(df.p_nom.div(df.p_nom.min())).fillna(0.).div(3), + cmap="Spectral_r", + vmax=vmax, + legend=True, + legend_kwds=dict(label="Gas Pipeline Capacity [GW]", shrink=0.55, extend="max"), + norm=mcolors.LogNorm(vmin=1, vmax=vmax), + ) + + pts.plot( + ax=ax, + column="type", + markersize=pts["capacity"].fillna(0.) * 750, + alpha=0.8, + legend=True, + legend_kwds=dict(loc=[0.08, 0.86]), + zorder=6 + ) + + ax.gridlines(linestyle=":") + ax.axis('off') + + for fn in snakemake.output: + plt.savefig(fn) From 66b56cf7f4d8a58dc45e8808c0c69561b051cadd Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 14 Aug 2023 18:25:58 +0200 Subject: [PATCH 136/293] plot resources: power network clustered --- rules/plot.smk | 11 ++++ scripts/plot_power_network_clustered.py | 77 +++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 scripts/plot_power_network_clustered.py diff --git a/rules/plot.smk b/rules/plot.smk index d8f2cc355..fb37e3f24 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -26,3 +26,14 @@ rule plot_gas_network_unclustered: multiext(RESOURCES + "graphics/gas-network-unclustered", ".png", ".pdf") script: "../scripts/plot_gas_network_unclustered.py" + + +rule plot_power_network_clustered: + input: + network=RESOURCES + "networks/elec_s_{clusters}.nc", + regions_onshore=RESOURCES + "regions_onshore_elec_s_{clusters}.geojson", + rc="matplotlibrc", + output: + multiext(RESOURCES + "graphics/power-network-{clusters}", ".png", ".pdf") + script: + "../scripts/plot_power_network_clustered.py" diff --git a/scripts/plot_power_network_clustered.py b/scripts/plot_power_network_clustered.py new file mode 100644 index 000000000..50cf8d49f --- /dev/null +++ b/scripts/plot_power_network_clustered.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2023- Fabian Neumann +# +# SPDX-License-Identifier: MIT +""" +Plot unclustered electricity transmission network. +""" + +import pypsa +import geopandas as gpd +import matplotlib.pyplot as plt +import cartopy.crs as ccrs +from matplotlib.lines import Line2D +from pypsa.plot import add_legend_lines + + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake( + "plot_power_network_clustered", + clusters=128, + configfiles=["../../config/config.test.yaml"] + ) + + plt.style.use(snakemake.input.rc) + + lw_factor = 2e3 + + n = pypsa.Network(snakemake.input.network) + + regions = gpd.read_file(snakemake.input.regions_onshore).set_index('name') + + proj = ccrs.EqualEarth() + fig, ax = plt.subplots(figsize=(8,8), subplot_kw={"projection": proj}) + regions.to_crs(proj.proj4_init).plot( + ax=ax, + facecolor='none', + edgecolor='lightgray', + linewidth=0.75 + ) + n.plot( + ax=ax, + margin=0.06, + line_widths=n.lines.s_nom / lw_factor, + link_colors=n.links.p_nom.apply( + lambda x: "darkseagreen" if x > 0 else "skyblue" + ), + link_widths=2., + ) + + sizes = [10, 20] + labels = [f"HVAC ({s} GW)" for s in sizes] + scale = 1e3 / lw_factor + sizes = [s * scale for s in sizes] + + legend_kw = dict( + loc=[0.25, 0.9], + frameon=False, + labelspacing=0.5, + handletextpad=1, + fontsize=13, + ) + + add_legend_lines( + ax, sizes, labels, patch_kw=dict(color="rosybrown"), legend_kw=legend_kw + ) + + handles = [ + Line2D([0], [0], color="darkseagreen", lw=2), + Line2D([0], [0], color="skyblue", lw=2), + ] + plt.legend(handles, ["HVDC existing", "HVDC planned"], frameon=False, loc=[0., 0.9], fontsize=13) + + for fn in snakemake.output: + plt.savefig(fn) From 5f31283343d69cacd0a7ee3fc3d87011aaead021 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 14 Aug 2023 18:26:38 +0200 Subject: [PATCH 137/293] plot resources: renewable energy potential density --- rules/plot.smk | 16 ++++ .../plot_renewable_potential_unclustered.py | 88 +++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 scripts/plot_renewable_potential_unclustered.py diff --git a/rules/plot.smk b/rules/plot.smk index fb37e3f24..db6cdfad1 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -37,3 +37,19 @@ rule plot_power_network_clustered: multiext(RESOURCES + "graphics/power-network-{clusters}", ".png", ".pdf") script: "../scripts/plot_power_network_clustered.py" + + +rule plot_renewable_potential_unclustered: + input: + regions_onshore=RESOURCES + "regions_onshore.geojson", + regions_offshore=RESOURCES + "regions_offshore.geojson", + **{ + f"profile_{tech}": RESOURCES + f"profile_{tech}.nc" + for tech in config["electricity"]["renewable_carriers"] + }, + rc="matplotlibrc", + output: + wind=multiext(RESOURCES + "graphics/wind-energy-density", ".png", ".pdf"), + solar=multiext(RESOURCES + "graphics/solar-energy-density", ".png", ".pdf"), + script: + "../scripts/plot_renewable_potential_unclustered.py" diff --git a/scripts/plot_renewable_potential_unclustered.py b/scripts/plot_renewable_potential_unclustered.py new file mode 100644 index 000000000..f6163aea6 --- /dev/null +++ b/scripts/plot_renewable_potential_unclustered.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2023- Fabian Neumann +# +# SPDX-License-Identifier: MIT +""" +Plot unclustered wind and solar renewable potentials. +""" + +import pandas as pd +import xarray as xr +import geopandas as gpd +import matplotlib.pyplot as plt +import cartopy.crs as ccrs +import cartopy + + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake( + "plot_renewable_potential_unclustered", + configfiles=["../../config/config.test.yaml"] + ) + + plt.style.use(snakemake.input.rc) + + regions = pd.concat([ + gpd.read_file(snakemake.input.regions_onshore), + gpd.read_file(snakemake.input.regions_offshore)] + ) + regions = regions.dissolve("name") + + regions["Area"] = regions.to_crs(epsg=3035).area.div(1e6) + + wind = pd.Series() + for profile in ["onwind", "offwind-ac", "offwind-dc"]: + ds = xr.open_dataset(snakemake.input[f"profile_{profile}"]) + wind = pd.concat([wind, (ds.p_nom_max * ds.profile.sum("time")).to_pandas()]) + wind = wind.groupby(level=0).sum().reindex(regions.index, fill_value=0) + wind_per_skm = wind / regions.Area / 1e3 # GWh + + proj = ccrs.EqualEarth() + regions = regions.to_crs(proj.proj4_init) + fig, ax = plt.subplots(figsize=(7, 7), subplot_kw={"projection": proj}) + regions.plot( + ax=ax, + column=wind_per_skm, + cmap="Blues", + linewidths=0, + legend=True, + legend_kwds={"label": r"Wind Energy Potential [GWh/a/km$^2$]", "shrink": 0.8}, + ) + ax.add_feature(cartopy.feature.COASTLINE.with_scale("50m"), linewidth=0.5, zorder=4) + ax.add_feature(cartopy.feature.BORDERS.with_scale("50m"), linewidth=0.5, zorder=2) + ax.gridlines(linestyle=":") + ax.axis("off") + + for fn in snakemake.output["wind"]: + plt.savefig(fn) + + onregions = gpd.read_file(snakemake.input.regions_onshore).set_index("name") + onregions["Area"] = onregions.to_crs(epsg=3035).area.div(1e6) + + ds = xr.open_dataset(snakemake.input.profile_solar) + solar = (ds.p_nom_max * ds.profile.sum("time")).to_pandas() + + solar = solar.groupby(level=0).sum().reindex(onregions.index, fill_value=0) + solar_per_skm = solar / onregions.Area / 1e3 # GWh + + proj = ccrs.EqualEarth() + onregions = onregions.to_crs(proj.proj4_init) + fig, ax = plt.subplots(figsize=(7, 7), subplot_kw={"projection": proj}) + onregions.plot( + ax=ax, + column=solar_per_skm, + cmap="Reds", + linewidths=0, + legend=True, + legend_kwds={"label": r"Solar Energy Potential [GWh/a/km$^2$]", "shrink": 0.8}, + ) + ax.add_feature(cartopy.feature.COASTLINE.with_scale("50m"), linewidth=0.5, zorder=2) + ax.add_feature(cartopy.feature.BORDERS.with_scale("50m"), linewidth=0.5, zorder=2) + ax.gridlines(linestyle=":") + ax.axis("off") + + for fn in snakemake.output["solar"]: + plt.savefig(fn) From 433e65cdca8001978fd5d51b488b2fc1d656a20f Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 14 Aug 2023 18:27:02 +0200 Subject: [PATCH 138/293] plot resources: weather data maps --- rules/plot.smk | 13 +++++++ scripts/plot_weather_data_map.py | 67 ++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 scripts/plot_weather_data_map.py diff --git a/rules/plot.smk b/rules/plot.smk index db6cdfad1..770bb1a74 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -53,3 +53,16 @@ rule plot_renewable_potential_unclustered: solar=multiext(RESOURCES + "graphics/solar-energy-density", ".png", ".pdf"), script: "../scripts/plot_renewable_potential_unclustered.py" + + +rule plot_weather_data_map: + input: + cutout=f"cutouts/" + CDIR + config["atlite"]["default_cutout"] + ".nc", + rc="matplotlibrc", + output: + irradiation=multiext(RESOURCES + "graphics/weather-map-irradiation", ".png", ".pdf"), + runoff=multiext(RESOURCES + "graphics/weather-map-runoff", ".png", ".pdf"), + temperature=multiext(RESOURCES + "graphics/weather-map-temperature", ".png", ".pdf"), + wind=multiext(RESOURCES + "graphics/weather-map-wind", ".png", ".pdf"), + script: + "../scripts/plot_weather_data_map.py" diff --git a/scripts/plot_weather_data_map.py b/scripts/plot_weather_data_map.py new file mode 100644 index 000000000..09ea9f5f0 --- /dev/null +++ b/scripts/plot_weather_data_map.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2023- Fabian Neumann +# +# SPDX-License-Identifier: MIT +""" +Plot time-averaged cutout weather data on a map. +""" + +import logging +import atlite +import matplotlib.pyplot as plt +import cartopy.crs as ccrs +import cartopy +from _helpers import configure_logging + + +logger = logging.getLogger(__name__) + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake( + "plot_weather_data_map", + configfiles=["../../config/config.test.yaml"] + ) + configure_logging(snakemake) + + plt.style.use(snakemake.input.rc) + + data = dict( + irradiation=dict(label=r"Mean Direct Solar Irradiance [W/m$^2$]", cmap="Oranges"), + runoff=dict(label=r"Total Runoff [m]", cmap="Greens"), + temperature=dict(label=r"Mean Temperatures [°C]", cmap="Reds"), + wind=dict(label=r"Mean Wind Speeds [m/s]", cmap="Blues"), + ) + + cutout = atlite.Cutout(snakemake.input.cutout) + + data["irradiation"]["data"] = cutout.data.influx_direct.mean("time") + data["runoff"]["data"] = cutout.data.runoff.sum("time") + data["temperature"]["data"] = (cutout.data.temperature.mean("time") - 273.15) + data["wind"]["data"] = cutout.data.wnd100m.mean("time") + + for k, v in data.items(): + + logger.info(f"Plotting weather data map for variable '{k}'") + + fig, ax = plt.subplots( + figsize=(7, 7), subplot_kw={"projection": ccrs.EqualEarth()} + ) + + v["data"].plot( + ax=ax, + transform=ccrs.PlateCarree(), + cmap=v["cmap"], + linewidths=0, + cbar_kwargs={"label": v["label"], "shrink": 0.6}, + ) + + ax.add_feature(cartopy.feature.COASTLINE.with_scale("50m"), linewidth=0.5, zorder=2) + ax.add_feature(cartopy.feature.BORDERS.with_scale("50m"), linewidth=0.5, zorder=2) + ax.gridlines(linestyle=":") + ax.axis("off") + + for fn in snakemake.output[k]: + plt.savefig(fn) From 1514333473cff2a5408066f70fd72a8031020858 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 14 Aug 2023 18:27:22 +0200 Subject: [PATCH 139/293] plot resources: industrial sites --- rules/plot.smk | 11 +++++ scripts/plot_industrial_sites.py | 85 ++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 scripts/plot_industrial_sites.py diff --git a/rules/plot.smk b/rules/plot.smk index 770bb1a74..1eb7f6777 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -66,3 +66,14 @@ rule plot_weather_data_map: wind=multiext(RESOURCES + "graphics/weather-map-wind", ".png", ".pdf"), script: "../scripts/plot_weather_data_map.py" + + +rule plot_industrial_sites: + input: + hotmaps="data/Industrial_Database.csv", + countries=RESOURCES + "country_shapes.geojson", + rc="matplotlibrc", + output: + multiext(RESOURCES + "graphics/industrial-sites", ".png", ".pdf"), + script: + "../scripts/plot_industrial_sites.py" diff --git a/scripts/plot_industrial_sites.py b/scripts/plot_industrial_sites.py new file mode 100644 index 000000000..439c0f1e9 --- /dev/null +++ b/scripts/plot_industrial_sites.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2023- Fabian Neumann +# +# SPDX-License-Identifier: MIT +""" +Plot industrial sites. +""" + +import pandas as pd +import geopandas as gpd +import matplotlib.pyplot as plt +import cartopy.crs as ccrs +import cartopy +import country_converter as coco +cc = coco.CountryConverter() + + +def prepare_hotmaps_database(): + """ + Load hotmaps database of industrial sites. + """ + + df = pd.read_csv( + snakemake.input.hotmaps, sep=";", index_col=0 + ) + + df[["srid", "coordinates"]] = df.geom.str.split(";", expand=True) + + # remove those sites without valid locations + df.drop(df.index[df.coordinates.isna()], inplace=True) + + df["coordinates"] = gpd.GeoSeries.from_wkt(df["coordinates"]) + + gdf = gpd.GeoDataFrame(df, geometry="coordinates", crs="EPSG:4326") + + + return gdf + + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake( + "plot_industrial_sites", + configfiles=["../../config/config.test.yaml"] + ) + + plt.style.use(snakemake.input.rc) + + crs = ccrs.EqualEarth() + + countries = gpd.read_file(snakemake.input.countries).set_index('name') + + hotmaps = prepare_hotmaps_database() + hotmaps = hotmaps.cx[-12:30, 35:72] + hotmaps = hotmaps.to_crs(crs.proj4_init) + + not_represented = ["AL", "BA", "RS", "MK", "ME"] + missing_countries = countries.loc[countries.index.intersection(not_represented)] + + fig, ax = plt.subplots(figsize=(8,8), subplot_kw={"projection": crs}) + + ax.add_feature(cartopy.feature.COASTLINE.with_scale("50m"), linewidth=0.5, zorder=2) + ax.add_feature(cartopy.feature.BORDERS.with_scale("50m"), linewidth=0.5, zorder=2) + + missing_countries.to_crs(crs.proj4_init).plot( + ax=ax, + color='lightgrey', + ) + + emissions = hotmaps["Emissions_ETS_2014"].fillna(hotmaps["Emissions_EPRTR_2014"]).fillna(hotmaps["Production"]).fillna(2e4) + + hotmaps.plot( + ax=ax, + column="Subsector", + markersize=emissions / 4e4, + alpha=0.5, + legend=True, + legend_kwds=dict(title="Industry Sector (radius ~ emissions)", frameon=False, ncols=2, loc=[0,.85], title_fontproperties={'weight':'bold'}), + ) + ax.axis("off") + + for fn in snakemake.output: + plt.savefig(fn) From a4c368a37eaf87e81828b0eba6756a6d3417b32c Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 14 Aug 2023 18:27:39 +0200 Subject: [PATCH 140/293] plot resources: powerplants --- rules/plot.smk | 10 ++++++ scripts/plot_powerplants.py | 68 +++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 scripts/plot_powerplants.py diff --git a/rules/plot.smk b/rules/plot.smk index 1eb7f6777..01d7a45bb 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -77,3 +77,13 @@ rule plot_industrial_sites: multiext(RESOURCES + "graphics/industrial-sites", ".png", ".pdf"), script: "../scripts/plot_industrial_sites.py" + + +rule plot_powerplants: + input: + powerplants=RESOURCES + "powerplants.csv", + rc="matplotlibrc", + output: + multiext(RESOURCES + "graphics/powerplants", ".png", ".pdf"), + script: + "../scripts/plot_powerplants.py" diff --git a/scripts/plot_powerplants.py b/scripts/plot_powerplants.py new file mode 100644 index 000000000..0445d04cf --- /dev/null +++ b/scripts/plot_powerplants.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2023- Fabian Neumann +# +# SPDX-License-Identifier: MIT +""" +Plot power plants. +""" + +import pandas as pd +import geopandas as gpd +import matplotlib.pyplot as plt +import cartopy.crs as ccrs +import cartopy +import matplotlib.colors as mcolors + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake( + "plot_powerplants", + configfiles=["../../config/config.test.yaml"] + ) + + plt.style.use(snakemake.input.rc) + + df = pd.read_csv(snakemake.input.powerplants, index_col=0) + + df = gpd.GeoDataFrame(df, geometry=gpd.points_from_xy(df.lon, df.lat), crs="EPSG:4326") + + colors = { + "Bioenergy": "#80c944", + "Hard Coal": "black", + "Hydro": "#235ebc", + "Lignite": "#826837", + "Natural Gas": "#a85522", + "Nuclear": "#ff8c00", + "Oil": "#c9c9c9", + "Waste": "purple", + "Other": "gold" + } + + crs = ccrs.EqualEarth() + + df = df.cx[-12:30, 35:72] + df = df.to_crs(crs.proj4_init) + + fig, ax = plt.subplots(figsize=(8,8), subplot_kw={"projection": crs}) + + ax.add_feature(cartopy.feature.COASTLINE.with_scale("50m"), linewidth=0.5, zorder=2) + ax.add_feature(cartopy.feature.BORDERS.with_scale("50m"), linewidth=0.5, zorder=2) + + df.plot( + ax=ax, + column="Fueltype", + markersize=df["Capacity"] / 35, + alpha=0.75, + legend=True, + cmap=mcolors.ListedColormap( + pd.Series(df.Fueltype.unique()).sort_values().map(colors).values + ), + legend_kwds=dict(title="Technology (radius ~ capacity)", loc=[0.13, 0.82], ncols=2, title_fontproperties={'weight':'bold'}), + ) + + ax.axis("off") + + for fn in snakemake.output: + plt.savefig(fn) From 1906be9709e83f0839a1247a8ddec5918d6e577a Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 14 Aug 2023 18:28:05 +0200 Subject: [PATCH 141/293] plot resources: salt caverns unclustered --- rules/plot.smk | 10 +++++ scripts/plot_salt_caverns_unclustered.py | 55 ++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 scripts/plot_salt_caverns_unclustered.py diff --git a/rules/plot.smk b/rules/plot.smk index 01d7a45bb..61b69b967 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -87,3 +87,13 @@ rule plot_powerplants: multiext(RESOURCES + "graphics/powerplants", ".png", ".pdf"), script: "../scripts/plot_powerplants.py" + + +rule plot_salt_caverns_unclustered: + input: + caverns="data/h2_salt_caverns_GWh_per_sqkm.geojson", + rc="matplotlibrc", + output: + multiext(RESOURCES + "graphics/salt-caverns", ".png", ".pdf"), + script: + "../scripts/plot_salt_caverns_unclustered.py" diff --git a/scripts/plot_salt_caverns_unclustered.py b/scripts/plot_salt_caverns_unclustered.py new file mode 100644 index 000000000..5728b8fea --- /dev/null +++ b/scripts/plot_salt_caverns_unclustered.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2023- Fabian Neumann +# +# SPDX-License-Identifier: MIT +""" +Plot unclustered salt caverns. +""" + +import geopandas as gpd +import matplotlib.pyplot as plt +import cartopy.crs as ccrs +import cartopy + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake( + "plot_salt_caverns_unclustered", + configfiles=["../../config/config.test.yaml"] + ) + + plt.style.use(snakemake.input.rc) + + caverns = gpd.read_file(snakemake.input.caverns) + + crs = ccrs.EqualEarth() + + caverns = caverns.to_crs(crs.proj4_init) + + fig, ax = plt.subplots(figsize=(7, 7), subplot_kw={"projection": crs}) + + ax.add_feature(cartopy.feature.COASTLINE.with_scale("50m"), linewidth=0.5, zorder=2) + ax.add_feature(cartopy.feature.BORDERS.with_scale("50m"), linewidth=0.5, zorder=2) + + caverns.plot( + ax=ax, + column="storage_type", + cmap="tab10_r", + legend=True, + linewidth=0, + legend_kwds=dict( + title="Salt Caverns for\nHydrogen Storage", loc=(0.21, 0.82) + ), + ) + + + plt.xlim(-1e6, 2.6e6) + plt.ylim(4.3e6, 7.8e6) + + ax.axis("off") + ax.gridlines(linestyle=":") + + for fn in snakemake.output: + plt.savefig(fn) From 7a1c84bf6652e38d5381be40e9e8ba8421eab8c9 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 14 Aug 2023 18:28:23 +0200 Subject: [PATCH 142/293] plot resources: salt caverns clustered --- rules/plot.smk | 15 +++++ scripts/plot_salt_caverns_clustered.py | 88 ++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 scripts/plot_salt_caverns_clustered.py diff --git a/rules/plot.smk b/rules/plot.smk index 61b69b967..db043113d 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -97,3 +97,18 @@ rule plot_salt_caverns_unclustered: multiext(RESOURCES + "graphics/salt-caverns", ".png", ".pdf"), script: "../scripts/plot_salt_caverns_unclustered.py" + + +rule plot_salt_caverns_clustered: + input: + caverns=RESOURCES + "salt_cavern_potentials_s_{clusters}.csv", + regions_onshore=RESOURCES + "regions_onshore_elec_s_{clusters}.geojson", + regions_offshore=RESOURCES + "regions_offshore_elec_s_{clusters}.geojson", + rc="matplotlibrc", + output: + onshore=multiext(RESOURCES + "graphics/salt-caverns-{clusters}-onshore", ".png", ".pdf"), + nearshore=multiext(RESOURCES + "graphics/salt-caverns-{clusters}-nearshore", ".png", ".pdf"), + offshore=multiext(RESOURCES + "graphics/salt-caverns-{clusters}-offshore", ".png", ".pdf"), + script: + "../scripts/plot_salt_caverns_clustered.py" + diff --git a/scripts/plot_salt_caverns_clustered.py b/scripts/plot_salt_caverns_clustered.py new file mode 100644 index 000000000..d54718b81 --- /dev/null +++ b/scripts/plot_salt_caverns_clustered.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2023- Fabian Neumann +# +# SPDX-License-Identifier: MIT +""" +Plot unclustered salt caverns. +""" + +import pandas as pd +import geopandas as gpd +import matplotlib.pyplot as plt +import matplotlib.colors as mcolors +import cartopy.crs as ccrs +import cartopy + +def plot_salt_caverns_by_node( + cavern_nodes, + cavern_regions, + storage_type="onshore", + cmap="GnBu", + vmin=1, + vmax=3000, + label=r"H$_2$ Storage Potential [TWh]", +): + + crs = ccrs.EqualEarth() + + cavern_regions = cavern_regions.to_crs(crs.proj4_init) + + fig, ax = plt.subplots(figsize=(7, 7), subplot_kw={"projection": crs}) + + cavern_regions.plot( + ax=ax, + column=cavern_nodes[storage_type].reindex(cavern_regions.index), + cmap=cmap, + legend=True, + vmin=vmin, + vmax=vmax, + linewidths=0.5, + edgecolor='darkgray', + legend_kwds={ + "label": label, + "shrink": 0.7, + "extend": "max", + }, + norm=mcolors.LogNorm(vmin=1, vmax=vmax), + ) + + plt.title(f"{storage_type.capitalize()} Salt Cavern H$_2$ Storage Potentials") + + plt.xlim(-1.2e6, 2.6e6) + plt.ylim(4.3e6, 7.8e6) + + ax.add_feature(cartopy.feature.COASTLINE.with_scale("50m"), linewidth=0.5, zorder=2) + ax.add_feature(cartopy.feature.BORDERS.with_scale("50m"), linewidth=0.5, zorder=2) + + ax.axis("off") + + for fn in snakemake.output[storage_type]: + plt.savefig(fn) + + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake( + "plot_salt_caverns_clustered", + clusters=128, + configfiles=["../../config/config.test.yaml"] + ) + + plt.style.use(snakemake.input.rc) + + cavern_nodes = pd.read_csv(snakemake.input.caverns, index_col=0) + cavern_nodes = cavern_nodes.where(cavern_nodes > 0.5) + + cavern_regions = gpd.read_file( + snakemake.input.regions_onshore + ).set_index("name") + + cavern_offregions = gpd.read_file( + snakemake.input.regions_offshore + ).set_index("name") + + plot_salt_caverns_by_node(cavern_nodes, cavern_regions, storage_type="onshore") + plot_salt_caverns_by_node(cavern_nodes, cavern_regions, storage_type="nearshore") + plot_salt_caverns_by_node(cavern_nodes, cavern_offregions, storage_type="offshore") \ No newline at end of file From f2d24401007595c9a12c9d902539ce4106585216 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 14 Aug 2023 18:28:41 +0200 Subject: [PATCH 143/293] plot resources: biomass potentials --- rules/plot.smk | 12 +++++ scripts/plot_biomass_potentials.py | 78 ++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 scripts/plot_biomass_potentials.py diff --git a/rules/plot.smk b/rules/plot.smk index db043113d..79a4987ee 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -112,3 +112,15 @@ rule plot_salt_caverns_clustered: script: "../scripts/plot_salt_caverns_clustered.py" + +rule plot_biomass_potentials: + input: + biomass=RESOURCES + "biomass_potentials_s_{clusters}.csv", + regions_onshore=RESOURCES + "regions_onshore_elec_s_{clusters}.geojson", + rc="matplotlibrc", + output: + solid_biomass=multiext(RESOURCES + "graphics/biomass-potentials-{clusters}-solid_biomass", ".png", ".pdf"), + not_included=multiext(RESOURCES + "graphics/biomass-potentials-{clusters}-not_included", ".png", ".pdf"), + biogas=multiext(RESOURCES + "graphics/biomass-potentials-{clusters}-biogas", ".png", ".pdf"), + script: + "../scripts/plot_biomass_potentials.py" diff --git a/scripts/plot_biomass_potentials.py b/scripts/plot_biomass_potentials.py new file mode 100644 index 000000000..c138a70fc --- /dev/null +++ b/scripts/plot_biomass_potentials.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2023- Fabian Neumann +# +# SPDX-License-Identifier: MIT +""" +Plot unclustered salt caverns. +""" + +import pandas as pd +import geopandas as gpd +import matplotlib.pyplot as plt +import cartopy.crs as ccrs +import cartopy + + +def plot_biomass_potentials(bio, regions, kind): + + crs = ccrs.EqualEarth() + regions = regions.to_crs(crs.proj4_init) + + fig, ax = plt.subplots(figsize=(7, 7), subplot_kw={"projection": crs}) + + ax.add_feature(cartopy.feature.COASTLINE.with_scale("50m"), linewidth=0.5, zorder=2) + ax.add_feature(cartopy.feature.BORDERS.with_scale("50m"), linewidth=0.5, zorder=2) + + nkind = "disregarded biomass" if kind == "not included" else kind + label = f"{nkind} potentials [TWh/a]" + + regions.plot( + ax=ax, + column=bio[kind], + cmap="Greens", + legend=True, + linewidth=0.5, + edgecolor='grey', + legend_kwds={ + "label": label, + "shrink": 0.7, + "extend": "max", + }, + ) + + total = bio[kind].sum() + + ax.text(-0.8e6, 7.4e6, f"{total:.0f} TWh/a", color="#343434") + + plt.xlim(-1e6, 2.6e6) + plt.ylim(4.3e6, 7.8e6) + + ax.axis("off") + + for fn in snakemake.output[kind.replace(" ", "_")]: + plt.savefig(fn) + + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake( + "plot_biomass_potentials", + clusters=128, + configfiles=["../../config/config.test.yaml"] + ) + + plt.style.use(snakemake.input.rc) + + bio = pd.read_csv(snakemake.input.biomass, index_col=0).div(1e6) # TWh/a + + regions = gpd.read_file( + snakemake.input.regions_onshore + ).set_index("name") + + + plot_biomass_potentials(bio, regions, kind="not included") + plot_biomass_potentials(bio, regions, kind="biogas") + plot_biomass_potentials(bio, regions, kind="solid biomass") + From 14327034d5362462bdb1b780f7f58e871b7f7df6 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 14 Aug 2023 18:29:00 +0200 Subject: [PATCH 144/293] plot resources: add collection rule --- rules/collect.smk | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/rules/collect.smk b/rules/collect.smk index 74f26ccb8..d7517bc46 100644 --- a/rules/collect.smk +++ b/rules/collect.smk @@ -88,3 +88,17 @@ rule validate_elec_networks: **config["scenario"], kind=["production", "prices", "cross_border"] ), + + +rule plot_resources: + input: + RESOURCES + "graphics/power-network-unclustered.pdf", + RESOURCES + "graphics/gas-network-unclustered.pdf", + RESOURCES + "graphics/wind-energy-density.pdf", + RESOURCES + "graphics/weather-map-irradiation.pdf", + RESOURCES + "graphics/industrial-sites.pdf", + RESOURCES + "graphics/powerplants.pdf", + RESOURCES + "graphics/salt-caverns.pdf", + expand(RESOURCES + "graphics/power-network-{clusters}.pdf", **config["scenario"]), + expand(RESOURCES + "graphics/salt-caverns-{clusters}-nearshore.pdf", **config["scenario"]), + expand(RESOURCES + "graphics/biomass-potentials-{clusters}-biogas.pdf", **config["scenario"]), From ba44b8152cabba3dbe01246d64cee9e47b34e2a6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 14 Aug 2023 16:32:35 +0000 Subject: [PATCH 145/293] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- matplotlibrc | 2 +- rules/collect.smk | 14 ++++-- rules/plot.smk | 47 ++++++++++++++----- scripts/plot_biomass_potentials.py | 17 +++---- scripts/plot_gas_network_unclustered.py | 29 +++++++----- scripts/plot_industrial_sites.py | 40 +++++++++------- scripts/plot_power_network_clustered.py | 26 +++++----- scripts/plot_power_network_unclustered.py | 9 ++-- scripts/plot_powerplants.py | 26 ++++++---- .../plot_renewable_potential_unclustered.py | 21 +++++---- scripts/plot_salt_caverns_clustered.py | 26 +++++----- scripts/plot_salt_caverns_unclustered.py | 11 ++--- scripts/plot_weather_data_map.py | 24 ++++++---- 13 files changed, 169 insertions(+), 123 deletions(-) diff --git a/matplotlibrc b/matplotlibrc index 16cedd1c3..92451ceea 100644 --- a/matplotlibrc +++ b/matplotlibrc @@ -7,4 +7,4 @@ image.cmap: viridis figure.autolayout : True figure.constrained_layout.use : True figure.dpi : 400 -legend.frameon : False \ No newline at end of file +legend.frameon : False diff --git a/rules/collect.smk b/rules/collect.smk index d7517bc46..ea15439af 100644 --- a/rules/collect.smk +++ b/rules/collect.smk @@ -99,6 +99,14 @@ rule plot_resources: RESOURCES + "graphics/industrial-sites.pdf", RESOURCES + "graphics/powerplants.pdf", RESOURCES + "graphics/salt-caverns.pdf", - expand(RESOURCES + "graphics/power-network-{clusters}.pdf", **config["scenario"]), - expand(RESOURCES + "graphics/salt-caverns-{clusters}-nearshore.pdf", **config["scenario"]), - expand(RESOURCES + "graphics/biomass-potentials-{clusters}-biogas.pdf", **config["scenario"]), + expand( + RESOURCES + "graphics/power-network-{clusters}.pdf", **config["scenario"] + ), + expand( + RESOURCES + "graphics/salt-caverns-{clusters}-nearshore.pdf", + **config["scenario"] + ), + expand( + RESOURCES + "graphics/biomass-potentials-{clusters}-biogas.pdf", + **config["scenario"] + ), diff --git a/rules/plot.smk b/rules/plot.smk index 79a4987ee..b8cfdc078 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -2,12 +2,13 @@ # # SPDX-License-Identifier: MIT + rule plot_power_network_unclustered: input: network=RESOURCES + "networks/elec.nc", rc="matplotlibrc", output: - multiext(RESOURCES + "graphics/power-network-unclustered", ".png", ".pdf") + multiext(RESOURCES + "graphics/power-network-unclustered", ".png", ".pdf"), script: "../scripts/plot_power_network_unclustered.py" @@ -23,7 +24,7 @@ rule plot_gas_network_unclustered: storage="data/gas_network/scigrid-gas/data/IGGIELGN_Storages.geojson", rc="matplotlibrc", output: - multiext(RESOURCES + "graphics/gas-network-unclustered", ".png", ".pdf") + multiext(RESOURCES + "graphics/gas-network-unclustered", ".png", ".pdf"), script: "../scripts/plot_gas_network_unclustered.py" @@ -34,19 +35,19 @@ rule plot_power_network_clustered: regions_onshore=RESOURCES + "regions_onshore_elec_s_{clusters}.geojson", rc="matplotlibrc", output: - multiext(RESOURCES + "graphics/power-network-{clusters}", ".png", ".pdf") + multiext(RESOURCES + "graphics/power-network-{clusters}", ".png", ".pdf"), script: "../scripts/plot_power_network_clustered.py" rule plot_renewable_potential_unclustered: input: - regions_onshore=RESOURCES + "regions_onshore.geojson", - regions_offshore=RESOURCES + "regions_offshore.geojson", **{ f"profile_{tech}": RESOURCES + f"profile_{tech}.nc" for tech in config["electricity"]["renewable_carriers"] }, + regions_onshore=RESOURCES + "regions_onshore.geojson", + regions_offshore=RESOURCES + "regions_offshore.geojson", rc="matplotlibrc", output: wind=multiext(RESOURCES + "graphics/wind-energy-density", ".png", ".pdf"), @@ -60,9 +61,13 @@ rule plot_weather_data_map: cutout=f"cutouts/" + CDIR + config["atlite"]["default_cutout"] + ".nc", rc="matplotlibrc", output: - irradiation=multiext(RESOURCES + "graphics/weather-map-irradiation", ".png", ".pdf"), + irradiation=multiext( + RESOURCES + "graphics/weather-map-irradiation", ".png", ".pdf" + ), runoff=multiext(RESOURCES + "graphics/weather-map-runoff", ".png", ".pdf"), - temperature=multiext(RESOURCES + "graphics/weather-map-temperature", ".png", ".pdf"), + temperature=multiext( + RESOURCES + "graphics/weather-map-temperature", ".png", ".pdf" + ), wind=multiext(RESOURCES + "graphics/weather-map-wind", ".png", ".pdf"), script: "../scripts/plot_weather_data_map.py" @@ -106,9 +111,15 @@ rule plot_salt_caverns_clustered: regions_offshore=RESOURCES + "regions_offshore_elec_s_{clusters}.geojson", rc="matplotlibrc", output: - onshore=multiext(RESOURCES + "graphics/salt-caverns-{clusters}-onshore", ".png", ".pdf"), - nearshore=multiext(RESOURCES + "graphics/salt-caverns-{clusters}-nearshore", ".png", ".pdf"), - offshore=multiext(RESOURCES + "graphics/salt-caverns-{clusters}-offshore", ".png", ".pdf"), + onshore=multiext( + RESOURCES + "graphics/salt-caverns-{clusters}-onshore", ".png", ".pdf" + ), + nearshore=multiext( + RESOURCES + "graphics/salt-caverns-{clusters}-nearshore", ".png", ".pdf" + ), + offshore=multiext( + RESOURCES + "graphics/salt-caverns-{clusters}-offshore", ".png", ".pdf" + ), script: "../scripts/plot_salt_caverns_clustered.py" @@ -119,8 +130,18 @@ rule plot_biomass_potentials: regions_onshore=RESOURCES + "regions_onshore_elec_s_{clusters}.geojson", rc="matplotlibrc", output: - solid_biomass=multiext(RESOURCES + "graphics/biomass-potentials-{clusters}-solid_biomass", ".png", ".pdf"), - not_included=multiext(RESOURCES + "graphics/biomass-potentials-{clusters}-not_included", ".png", ".pdf"), - biogas=multiext(RESOURCES + "graphics/biomass-potentials-{clusters}-biogas", ".png", ".pdf"), + solid_biomass=multiext( + RESOURCES + "graphics/biomass-potentials-{clusters}-solid_biomass", + ".png", + ".pdf", + ), + not_included=multiext( + RESOURCES + "graphics/biomass-potentials-{clusters}-not_included", + ".png", + ".pdf", + ), + biogas=multiext( + RESOURCES + "graphics/biomass-potentials-{clusters}-biogas", ".png", ".pdf" + ), script: "../scripts/plot_biomass_potentials.py" diff --git a/scripts/plot_biomass_potentials.py b/scripts/plot_biomass_potentials.py index c138a70fc..0facabde5 100644 --- a/scripts/plot_biomass_potentials.py +++ b/scripts/plot_biomass_potentials.py @@ -6,15 +6,14 @@ Plot unclustered salt caverns. """ -import pandas as pd +import cartopy +import cartopy.crs as ccrs import geopandas as gpd import matplotlib.pyplot as plt -import cartopy.crs as ccrs -import cartopy +import pandas as pd def plot_biomass_potentials(bio, regions, kind): - crs = ccrs.EqualEarth() regions = regions.to_crs(crs.proj4_init) @@ -32,7 +31,7 @@ def plot_biomass_potentials(bio, regions, kind): cmap="Greens", legend=True, linewidth=0.5, - edgecolor='grey', + edgecolor="grey", legend_kwds={ "label": label, "shrink": 0.7, @@ -60,19 +59,15 @@ def plot_biomass_potentials(bio, regions, kind): snakemake = mock_snakemake( "plot_biomass_potentials", clusters=128, - configfiles=["../../config/config.test.yaml"] + configfiles=["../../config/config.test.yaml"], ) plt.style.use(snakemake.input.rc) bio = pd.read_csv(snakemake.input.biomass, index_col=0).div(1e6) # TWh/a - regions = gpd.read_file( - snakemake.input.regions_onshore - ).set_index("name") - + regions = gpd.read_file(snakemake.input.regions_onshore).set_index("name") plot_biomass_potentials(bio, regions, kind="not included") plot_biomass_potentials(bio, regions, kind="biogas") plot_biomass_potentials(bio, regions, kind="solid biomass") - diff --git a/scripts/plot_gas_network_unclustered.py b/scripts/plot_gas_network_unclustered.py index 34185e20e..c07c57a97 100644 --- a/scripts/plot_gas_network_unclustered.py +++ b/scripts/plot_gas_network_unclustered.py @@ -6,16 +6,16 @@ Plot unclustered gas transmission network. """ -import pandas as pd -import numpy as np -from shapely import wkt +import cartopy +import cartopy.crs as ccrs import geopandas as gpd +import matplotlib.colors as mcolors import matplotlib.pyplot as plt -import cartopy.crs as ccrs -import cartopy +import numpy as np +import pandas as pd from build_gas_input_locations import build_gas_input_locations -import matplotlib.colors as mcolors from matplotlib.ticker import LogFormatter +from shapely import wkt if __name__ == "__main__": if "snakemake" not in globals(): @@ -40,7 +40,12 @@ pts.loc[pts["type"] == t, "capacity"] /= sums[t] pts["type"] = pts["type"].replace( - dict(production="Fossil Extraction", lng="LNG Terminal", pipeline="Entrypoint", storage="Storage") + dict( + production="Fossil Extraction", + lng="LNG Terminal", + pipeline="Entrypoint", + storage="Storage", + ) ) df = pd.read_csv(snakemake.input.gas_network, index_col=0) @@ -63,8 +68,8 @@ df.plot( ax=ax, - column=df["p_nom"].div(1e3).fillna(0.), - linewidths=np.log(df.p_nom.div(df.p_nom.min())).fillna(0.).div(3), + column=df["p_nom"].div(1e3).fillna(0.0), + linewidths=np.log(df.p_nom.div(df.p_nom.min())).fillna(0.0).div(3), cmap="Spectral_r", vmax=vmax, legend=True, @@ -75,15 +80,15 @@ pts.plot( ax=ax, column="type", - markersize=pts["capacity"].fillna(0.) * 750, + markersize=pts["capacity"].fillna(0.0) * 750, alpha=0.8, legend=True, legend_kwds=dict(loc=[0.08, 0.86]), - zorder=6 + zorder=6, ) ax.gridlines(linestyle=":") - ax.axis('off') + ax.axis("off") for fn in snakemake.output: plt.savefig(fn) diff --git a/scripts/plot_industrial_sites.py b/scripts/plot_industrial_sites.py index 439c0f1e9..8d77a79bd 100644 --- a/scripts/plot_industrial_sites.py +++ b/scripts/plot_industrial_sites.py @@ -6,12 +6,13 @@ Plot industrial sites. """ -import pandas as pd -import geopandas as gpd -import matplotlib.pyplot as plt -import cartopy.crs as ccrs import cartopy +import cartopy.crs as ccrs import country_converter as coco +import geopandas as gpd +import matplotlib.pyplot as plt +import pandas as pd + cc = coco.CountryConverter() @@ -20,9 +21,7 @@ def prepare_hotmaps_database(): Load hotmaps database of industrial sites. """ - df = pd.read_csv( - snakemake.input.hotmaps, sep=";", index_col=0 - ) + df = pd.read_csv(snakemake.input.hotmaps, sep=";", index_col=0) df[["srid", "coordinates"]] = df.geom.str.split(";", expand=True) @@ -30,9 +29,8 @@ def prepare_hotmaps_database(): df.drop(df.index[df.coordinates.isna()], inplace=True) df["coordinates"] = gpd.GeoSeries.from_wkt(df["coordinates"]) - - gdf = gpd.GeoDataFrame(df, geometry="coordinates", crs="EPSG:4326") + gdf = gpd.GeoDataFrame(df, geometry="coordinates", crs="EPSG:4326") return gdf @@ -42,15 +40,14 @@ def prepare_hotmaps_database(): from _helpers import mock_snakemake snakemake = mock_snakemake( - "plot_industrial_sites", - configfiles=["../../config/config.test.yaml"] + "plot_industrial_sites", configfiles=["../../config/config.test.yaml"] ) plt.style.use(snakemake.input.rc) crs = ccrs.EqualEarth() - countries = gpd.read_file(snakemake.input.countries).set_index('name') + countries = gpd.read_file(snakemake.input.countries).set_index("name") hotmaps = prepare_hotmaps_database() hotmaps = hotmaps.cx[-12:30, 35:72] @@ -59,17 +56,22 @@ def prepare_hotmaps_database(): not_represented = ["AL", "BA", "RS", "MK", "ME"] missing_countries = countries.loc[countries.index.intersection(not_represented)] - fig, ax = plt.subplots(figsize=(8,8), subplot_kw={"projection": crs}) + fig, ax = plt.subplots(figsize=(8, 8), subplot_kw={"projection": crs}) ax.add_feature(cartopy.feature.COASTLINE.with_scale("50m"), linewidth=0.5, zorder=2) ax.add_feature(cartopy.feature.BORDERS.with_scale("50m"), linewidth=0.5, zorder=2) missing_countries.to_crs(crs.proj4_init).plot( ax=ax, - color='lightgrey', + color="lightgrey", ) - emissions = hotmaps["Emissions_ETS_2014"].fillna(hotmaps["Emissions_EPRTR_2014"]).fillna(hotmaps["Production"]).fillna(2e4) + emissions = ( + hotmaps["Emissions_ETS_2014"] + .fillna(hotmaps["Emissions_EPRTR_2014"]) + .fillna(hotmaps["Production"]) + .fillna(2e4) + ) hotmaps.plot( ax=ax, @@ -77,7 +79,13 @@ def prepare_hotmaps_database(): markersize=emissions / 4e4, alpha=0.5, legend=True, - legend_kwds=dict(title="Industry Sector (radius ~ emissions)", frameon=False, ncols=2, loc=[0,.85], title_fontproperties={'weight':'bold'}), + legend_kwds=dict( + title="Industry Sector (radius ~ emissions)", + frameon=False, + ncols=2, + loc=[0, 0.85], + title_fontproperties={"weight": "bold"}, + ), ) ax.axis("off") diff --git a/scripts/plot_power_network_clustered.py b/scripts/plot_power_network_clustered.py index 50cf8d49f..70e5ec854 100644 --- a/scripts/plot_power_network_clustered.py +++ b/scripts/plot_power_network_clustered.py @@ -6,14 +6,13 @@ Plot unclustered electricity transmission network. """ -import pypsa +import cartopy.crs as ccrs import geopandas as gpd import matplotlib.pyplot as plt -import cartopy.crs as ccrs +import pypsa from matplotlib.lines import Line2D from pypsa.plot import add_legend_lines - if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake @@ -21,7 +20,7 @@ snakemake = mock_snakemake( "plot_power_network_clustered", clusters=128, - configfiles=["../../config/config.test.yaml"] + configfiles=["../../config/config.test.yaml"], ) plt.style.use(snakemake.input.rc) @@ -30,15 +29,12 @@ n = pypsa.Network(snakemake.input.network) - regions = gpd.read_file(snakemake.input.regions_onshore).set_index('name') + regions = gpd.read_file(snakemake.input.regions_onshore).set_index("name") proj = ccrs.EqualEarth() - fig, ax = plt.subplots(figsize=(8,8), subplot_kw={"projection": proj}) + fig, ax = plt.subplots(figsize=(8, 8), subplot_kw={"projection": proj}) regions.to_crs(proj.proj4_init).plot( - ax=ax, - facecolor='none', - edgecolor='lightgray', - linewidth=0.75 + ax=ax, facecolor="none", edgecolor="lightgray", linewidth=0.75 ) n.plot( ax=ax, @@ -47,7 +43,7 @@ link_colors=n.links.p_nom.apply( lambda x: "darkseagreen" if x > 0 else "skyblue" ), - link_widths=2., + link_widths=2.0, ) sizes = [10, 20] @@ -71,7 +67,13 @@ Line2D([0], [0], color="darkseagreen", lw=2), Line2D([0], [0], color="skyblue", lw=2), ] - plt.legend(handles, ["HVDC existing", "HVDC planned"], frameon=False, loc=[0., 0.9], fontsize=13) + plt.legend( + handles, + ["HVDC existing", "HVDC planned"], + frameon=False, + loc=[0.0, 0.9], + fontsize=13, + ) for fn in snakemake.output: plt.savefig(fn) diff --git a/scripts/plot_power_network_unclustered.py b/scripts/plot_power_network_unclustered.py index de2491d98..2a9283474 100644 --- a/scripts/plot_power_network_unclustered.py +++ b/scripts/plot_power_network_unclustered.py @@ -6,12 +6,11 @@ Plot unclustered electricity transmission network. """ -import pypsa -import matplotlib.pyplot as plt import cartopy.crs as ccrs +import matplotlib.pyplot as plt +import pypsa from matplotlib.lines import Line2D - if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake @@ -25,7 +24,9 @@ w = n.lines.v_nom.div(380) c = n.lines.v_nom.map({220: "teal", 300: "orange", 380: "firebrick"}) - fig, ax = plt.subplots(figsize=(13, 13), subplot_kw={"projection": ccrs.EqualEarth()}) + fig, ax = plt.subplots( + figsize=(13, 13), subplot_kw={"projection": ccrs.EqualEarth()} + ) n.plot( ax=ax, diff --git a/scripts/plot_powerplants.py b/scripts/plot_powerplants.py index 0445d04cf..c3ded299e 100644 --- a/scripts/plot_powerplants.py +++ b/scripts/plot_powerplants.py @@ -6,27 +6,28 @@ Plot power plants. """ -import pandas as pd -import geopandas as gpd -import matplotlib.pyplot as plt -import cartopy.crs as ccrs import cartopy +import cartopy.crs as ccrs +import geopandas as gpd import matplotlib.colors as mcolors +import matplotlib.pyplot as plt +import pandas as pd if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake snakemake = mock_snakemake( - "plot_powerplants", - configfiles=["../../config/config.test.yaml"] + "plot_powerplants", configfiles=["../../config/config.test.yaml"] ) plt.style.use(snakemake.input.rc) df = pd.read_csv(snakemake.input.powerplants, index_col=0) - df = gpd.GeoDataFrame(df, geometry=gpd.points_from_xy(df.lon, df.lat), crs="EPSG:4326") + df = gpd.GeoDataFrame( + df, geometry=gpd.points_from_xy(df.lon, df.lat), crs="EPSG:4326" + ) colors = { "Bioenergy": "#80c944", @@ -37,7 +38,7 @@ "Nuclear": "#ff8c00", "Oil": "#c9c9c9", "Waste": "purple", - "Other": "gold" + "Other": "gold", } crs = ccrs.EqualEarth() @@ -45,7 +46,7 @@ df = df.cx[-12:30, 35:72] df = df.to_crs(crs.proj4_init) - fig, ax = plt.subplots(figsize=(8,8), subplot_kw={"projection": crs}) + fig, ax = plt.subplots(figsize=(8, 8), subplot_kw={"projection": crs}) ax.add_feature(cartopy.feature.COASTLINE.with_scale("50m"), linewidth=0.5, zorder=2) ax.add_feature(cartopy.feature.BORDERS.with_scale("50m"), linewidth=0.5, zorder=2) @@ -59,7 +60,12 @@ cmap=mcolors.ListedColormap( pd.Series(df.Fueltype.unique()).sort_values().map(colors).values ), - legend_kwds=dict(title="Technology (radius ~ capacity)", loc=[0.13, 0.82], ncols=2, title_fontproperties={'weight':'bold'}), + legend_kwds=dict( + title="Technology (radius ~ capacity)", + loc=[0.13, 0.82], + ncols=2, + title_fontproperties={"weight": "bold"}, + ), ) ax.axis("off") diff --git a/scripts/plot_renewable_potential_unclustered.py b/scripts/plot_renewable_potential_unclustered.py index f6163aea6..5e1fca422 100644 --- a/scripts/plot_renewable_potential_unclustered.py +++ b/scripts/plot_renewable_potential_unclustered.py @@ -6,13 +6,12 @@ Plot unclustered wind and solar renewable potentials. """ -import pandas as pd -import xarray as xr +import cartopy +import cartopy.crs as ccrs import geopandas as gpd import matplotlib.pyplot as plt -import cartopy.crs as ccrs -import cartopy - +import pandas as pd +import xarray as xr if __name__ == "__main__": if "snakemake" not in globals(): @@ -20,17 +19,19 @@ snakemake = mock_snakemake( "plot_renewable_potential_unclustered", - configfiles=["../../config/config.test.yaml"] + configfiles=["../../config/config.test.yaml"], ) plt.style.use(snakemake.input.rc) - regions = pd.concat([ - gpd.read_file(snakemake.input.regions_onshore), - gpd.read_file(snakemake.input.regions_offshore)] + regions = pd.concat( + [ + gpd.read_file(snakemake.input.regions_onshore), + gpd.read_file(snakemake.input.regions_offshore), + ] ) regions = regions.dissolve("name") - + regions["Area"] = regions.to_crs(epsg=3035).area.div(1e6) wind = pd.Series() diff --git a/scripts/plot_salt_caverns_clustered.py b/scripts/plot_salt_caverns_clustered.py index d54718b81..dfab4ea27 100644 --- a/scripts/plot_salt_caverns_clustered.py +++ b/scripts/plot_salt_caverns_clustered.py @@ -6,12 +6,13 @@ Plot unclustered salt caverns. """ -import pandas as pd +import cartopy +import cartopy.crs as ccrs import geopandas as gpd -import matplotlib.pyplot as plt import matplotlib.colors as mcolors -import cartopy.crs as ccrs -import cartopy +import matplotlib.pyplot as plt +import pandas as pd + def plot_salt_caverns_by_node( cavern_nodes, @@ -22,7 +23,6 @@ def plot_salt_caverns_by_node( vmax=3000, label=r"H$_2$ Storage Potential [TWh]", ): - crs = ccrs.EqualEarth() cavern_regions = cavern_regions.to_crs(crs.proj4_init) @@ -37,7 +37,7 @@ def plot_salt_caverns_by_node( vmin=vmin, vmax=vmax, linewidths=0.5, - edgecolor='darkgray', + edgecolor="darkgray", legend_kwds={ "label": label, "shrink": 0.7, @@ -67,7 +67,7 @@ def plot_salt_caverns_by_node( snakemake = mock_snakemake( "plot_salt_caverns_clustered", clusters=128, - configfiles=["../../config/config.test.yaml"] + configfiles=["../../config/config.test.yaml"], ) plt.style.use(snakemake.input.rc) @@ -75,14 +75,12 @@ def plot_salt_caverns_by_node( cavern_nodes = pd.read_csv(snakemake.input.caverns, index_col=0) cavern_nodes = cavern_nodes.where(cavern_nodes > 0.5) - cavern_regions = gpd.read_file( - snakemake.input.regions_onshore - ).set_index("name") + cavern_regions = gpd.read_file(snakemake.input.regions_onshore).set_index("name") - cavern_offregions = gpd.read_file( - snakemake.input.regions_offshore - ).set_index("name") + cavern_offregions = gpd.read_file(snakemake.input.regions_offshore).set_index( + "name" + ) plot_salt_caverns_by_node(cavern_nodes, cavern_regions, storage_type="onshore") plot_salt_caverns_by_node(cavern_nodes, cavern_regions, storage_type="nearshore") - plot_salt_caverns_by_node(cavern_nodes, cavern_offregions, storage_type="offshore") \ No newline at end of file + plot_salt_caverns_by_node(cavern_nodes, cavern_offregions, storage_type="offshore") diff --git a/scripts/plot_salt_caverns_unclustered.py b/scripts/plot_salt_caverns_unclustered.py index 5728b8fea..12fcbe53c 100644 --- a/scripts/plot_salt_caverns_unclustered.py +++ b/scripts/plot_salt_caverns_unclustered.py @@ -6,10 +6,10 @@ Plot unclustered salt caverns. """ +import cartopy +import cartopy.crs as ccrs import geopandas as gpd import matplotlib.pyplot as plt -import cartopy.crs as ccrs -import cartopy if __name__ == "__main__": if "snakemake" not in globals(): @@ -17,7 +17,7 @@ snakemake = mock_snakemake( "plot_salt_caverns_unclustered", - configfiles=["../../config/config.test.yaml"] + configfiles=["../../config/config.test.yaml"], ) plt.style.use(snakemake.input.rc) @@ -39,12 +39,9 @@ cmap="tab10_r", legend=True, linewidth=0, - legend_kwds=dict( - title="Salt Caverns for\nHydrogen Storage", loc=(0.21, 0.82) - ), + legend_kwds=dict(title="Salt Caverns for\nHydrogen Storage", loc=(0.21, 0.82)), ) - plt.xlim(-1e6, 2.6e6) plt.ylim(4.3e6, 7.8e6) diff --git a/scripts/plot_weather_data_map.py b/scripts/plot_weather_data_map.py index 09ea9f5f0..4d7015ce0 100644 --- a/scripts/plot_weather_data_map.py +++ b/scripts/plot_weather_data_map.py @@ -7,13 +7,13 @@ """ import logging + import atlite -import matplotlib.pyplot as plt -import cartopy.crs as ccrs import cartopy +import cartopy.crs as ccrs +import matplotlib.pyplot as plt from _helpers import configure_logging - logger = logging.getLogger(__name__) if __name__ == "__main__": @@ -21,15 +21,16 @@ from _helpers import mock_snakemake snakemake = mock_snakemake( - "plot_weather_data_map", - configfiles=["../../config/config.test.yaml"] + "plot_weather_data_map", configfiles=["../../config/config.test.yaml"] ) configure_logging(snakemake) plt.style.use(snakemake.input.rc) data = dict( - irradiation=dict(label=r"Mean Direct Solar Irradiance [W/m$^2$]", cmap="Oranges"), + irradiation=dict( + label=r"Mean Direct Solar Irradiance [W/m$^2$]", cmap="Oranges" + ), runoff=dict(label=r"Total Runoff [m]", cmap="Greens"), temperature=dict(label=r"Mean Temperatures [°C]", cmap="Reds"), wind=dict(label=r"Mean Wind Speeds [m/s]", cmap="Blues"), @@ -39,11 +40,10 @@ data["irradiation"]["data"] = cutout.data.influx_direct.mean("time") data["runoff"]["data"] = cutout.data.runoff.sum("time") - data["temperature"]["data"] = (cutout.data.temperature.mean("time") - 273.15) + data["temperature"]["data"] = cutout.data.temperature.mean("time") - 273.15 data["wind"]["data"] = cutout.data.wnd100m.mean("time") for k, v in data.items(): - logger.info(f"Plotting weather data map for variable '{k}'") fig, ax = plt.subplots( @@ -58,8 +58,12 @@ cbar_kwargs={"label": v["label"], "shrink": 0.6}, ) - ax.add_feature(cartopy.feature.COASTLINE.with_scale("50m"), linewidth=0.5, zorder=2) - ax.add_feature(cartopy.feature.BORDERS.with_scale("50m"), linewidth=0.5, zorder=2) + ax.add_feature( + cartopy.feature.COASTLINE.with_scale("50m"), linewidth=0.5, zorder=2 + ) + ax.add_feature( + cartopy.feature.BORDERS.with_scale("50m"), linewidth=0.5, zorder=2 + ) ax.gridlines(linestyle=":") ax.axis("off") From fb2c381365b5f8c59527d2f28c0fb21ccb8d81ac Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Tue, 15 Aug 2023 14:35:29 +0200 Subject: [PATCH 146/293] merge master --- Snakefile | 1 - rules/plot.smk | 2 + .../plot_renewable_potential_unclustered.py | 76 +++++++++++-------- 3 files changed, 47 insertions(+), 32 deletions(-) diff --git a/Snakefile b/Snakefile index 85777de6c..536fa9bca 100644 --- a/Snakefile +++ b/Snakefile @@ -33,7 +33,6 @@ BENCHMARKS = "benchmarks/" + RDIR RESOURCES = "resources/" + RDIR if not run.get("shared_resources") else "resources/" RESULTS = "results/" + RDIR - localrules: purge, diff --git a/rules/plot.smk b/rules/plot.smk index b8cfdc078..29f3f6195 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -52,6 +52,8 @@ rule plot_renewable_potential_unclustered: output: wind=multiext(RESOURCES + "graphics/wind-energy-density", ".png", ".pdf"), solar=multiext(RESOURCES + "graphics/solar-energy-density", ".png", ".pdf"), + wind_cf=multiext(RESOURCES + "graphics/wind-capacity-factor", ".png", ".pdf"), + solar_cf=multiext(RESOURCES + "graphics/solar-capacity-factor", ".png", ".pdf"), script: "../scripts/plot_renewable_potential_unclustered.py" diff --git a/scripts/plot_renewable_potential_unclustered.py b/scripts/plot_renewable_potential_unclustered.py index 5e1fca422..d7e31af67 100644 --- a/scripts/plot_renewable_potential_unclustered.py +++ b/scripts/plot_renewable_potential_unclustered.py @@ -13,6 +13,29 @@ import pandas as pd import xarray as xr +def plot_map(regions, color, cmap, label, vmin=None, vmax=None): + + proj = ccrs.EqualEarth() + regions = regions.to_crs(proj.proj4_init) + fig, ax = plt.subplots(figsize=(7, 7), subplot_kw={"projection": proj}) + regions.plot( + ax=ax, + column=color, + cmap=cmap, + linewidths=0, + legend=True, + vmin=vmin, + vmax=vmax, + legend_kwds={"label": label, "shrink": 0.8}, + ) + ax.add_feature(cartopy.feature.COASTLINE.with_scale("50m"), linewidth=0.5, zorder=4) + ax.add_feature(cartopy.feature.BORDERS.with_scale("50m"), linewidth=0.5, zorder=2) + ax.gridlines(linestyle=":") + ax.axis("off") + + return fig, ax + + if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake @@ -41,49 +64,40 @@ wind = wind.groupby(level=0).sum().reindex(regions.index, fill_value=0) wind_per_skm = wind / regions.Area / 1e3 # GWh - proj = ccrs.EqualEarth() - regions = regions.to_crs(proj.proj4_init) - fig, ax = plt.subplots(figsize=(7, 7), subplot_kw={"projection": proj}) - regions.plot( - ax=ax, - column=wind_per_skm, - cmap="Blues", - linewidths=0, - legend=True, - legend_kwds={"label": r"Wind Energy Potential [GWh/a/km$^2$]", "shrink": 0.8}, - ) - ax.add_feature(cartopy.feature.COASTLINE.with_scale("50m"), linewidth=0.5, zorder=4) - ax.add_feature(cartopy.feature.BORDERS.with_scale("50m"), linewidth=0.5, zorder=2) - ax.gridlines(linestyle=":") - ax.axis("off") + fig, ax = plot_map(regions, wind_per_skm, "Blues", r"Wind Energy Potential [GWh/a/km$^2$]") for fn in snakemake.output["wind"]: plt.savefig(fn) + cf = pd.Series() + for profile in ["onwind", "offwind-ac", "offwind-dc"]: + ds = xr.open_dataset(snakemake.input[f"profile_{profile}"]) + cf = pd.concat([cf, (ds.profile.mean("time") * 100).to_pandas()]) + cf = cf.groupby(level=0).mean().reindex(regions.index, fill_value=0.) + + fig, ax = plot_map(regions, cf, "Blues", r"Wind Capacity Factor [%]", vmin=0, vmax=60) + + for fn in snakemake.output["wind_cf"]: + plt.savefig(fn) + onregions = gpd.read_file(snakemake.input.regions_onshore).set_index("name") onregions["Area"] = onregions.to_crs(epsg=3035).area.div(1e6) ds = xr.open_dataset(snakemake.input.profile_solar) solar = (ds.p_nom_max * ds.profile.sum("time")).to_pandas() - solar = solar.groupby(level=0).sum().reindex(onregions.index, fill_value=0) + solar = solar.groupby(level=0).sum().reindex(onregions.index, fill_value=0.) solar_per_skm = solar / onregions.Area / 1e3 # GWh - proj = ccrs.EqualEarth() - onregions = onregions.to_crs(proj.proj4_init) - fig, ax = plt.subplots(figsize=(7, 7), subplot_kw={"projection": proj}) - onregions.plot( - ax=ax, - column=solar_per_skm, - cmap="Reds", - linewidths=0, - legend=True, - legend_kwds={"label": r"Solar Energy Potential [GWh/a/km$^2$]", "shrink": 0.8}, - ) - ax.add_feature(cartopy.feature.COASTLINE.with_scale("50m"), linewidth=0.5, zorder=2) - ax.add_feature(cartopy.feature.BORDERS.with_scale("50m"), linewidth=0.5, zorder=2) - ax.gridlines(linestyle=":") - ax.axis("off") + fig, ax = plot_map(onregions, solar_per_skm, "Reds", r"Solar Energy Potential [GWh/a/km$^2$]") for fn in snakemake.output["solar"]: plt.savefig(fn) + + cf = (ds.profile.mean("time") * 100).to_pandas() + cf = cf.groupby(level=0).mean().reindex(onregions.index, fill_value=0.) + + fig, ax = plot_map(onregions, cf, "Reds", r"Solar Capacity Factor [%]", vmin=6, vmax=20) + + for fn in snakemake.output["solar_cf"]: + plt.savefig(fn) From 8595f7848ba38f3ca5865fe352677c456a24528c Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Sat, 19 Aug 2023 13:02:28 +0200 Subject: [PATCH 147/293] bugfix: safe aggregation of cluster heat buses (cf. https://github.com/pandas-dev/pandas/issues/54161) --- scripts/prepare_sector_network.py | 46 +++++++++++++++---------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index c2f9dc7c1..a2cccf13a 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -4100,30 +4100,30 @@ def limit_individual_line_extension(n, maxext): aggregate_dict = { - "p_nom": "sum", - "s_nom": "sum", - "v_nom": "max", - "v_mag_pu_max": "min", - "v_mag_pu_min": "max", - "p_nom_max": "sum", - "s_nom_max": "sum", - "p_nom_min": "sum", - "s_nom_min": "sum", - "v_ang_min": "max", - "v_ang_max": "min", - "terrain_factor": "mean", - "num_parallel": "sum", - "p_set": "sum", - "e_initial": "sum", - "e_nom": "sum", - "e_nom_max": "sum", - "e_nom_min": "sum", - "state_of_charge_initial": "sum", - "state_of_charge_set": "sum", - "inflow": "sum", + "p_nom": pd.Series.sum, + "s_nom": pd.Series.sum, + "v_nom": pd.Series.max, + "v_mag_pu_max": pd.Series.min, + "v_mag_pu_min": pd.Series.max, + "p_nom_max": pd.Series.sum, + "s_nom_max": pd.Series.sum, + "p_nom_min": pd.Series.sum, + "s_nom_min": pd.Series.sum, + "v_ang_min": pd.Series.max, + "v_ang_max": pd.Series.min, + "terrain_factor": pd.Series.mean, + "num_parallel": pd.Series.sum, + "p_set": pd.Series.sum, + "e_initial": pd.Series.sum, + "e_nom": pd.Series.sum, + "e_nom_max": pd.Series.sum, + "e_nom_min": pd.Series.sum, + "state_of_charge_initial": pd.Series.sum, + "state_of_charge_set": pd.Series.sum, + "inflow": pd.Series.sum, "p_max_pu": "first", - "x": "mean", - "y": "mean", + "x": pd.Series.mean, + "y": pd.Series.mean, } From e0495634fe214216dbcbeeebba5f3a89597787ad Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Sat, 19 Aug 2023 13:03:24 +0200 Subject: [PATCH 148/293] separate configs for methanol_reforming w/wo CC --- config/config.default.yaml | 1 + scripts/prepare_sector_network.py | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/config/config.default.yaml b/config/config.default.yaml index 472fee6b8..b34eb6d1b 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -464,6 +464,7 @@ sector: hydrogen_turbine: false SMR: true methanol_reforming: true + methanol_reforming_cc: false methanol_to_kerosene: true methanol_to_power: ccgt: true diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index a2cccf13a..051400476 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -788,6 +788,30 @@ def add_methanol_reforming(n, costs): nodes = pop_layout.index + tech = "Methanol steam reforming" + + capital_cost = costs.at[tech, "fixed"] / costs.at[tech, "methanol-input"] + + n.madd( + "Link", + nodes, + 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 + # 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 @@ -4504,6 +4528,9 @@ def parse_carriers(s): if options["methanol_reforming"]: add_methanol_reforming(n, costs) + if options["methanol_reforming_cc"]: + add_methanol_reforming_cc(n, costs) + solver_name = snakemake.config["solving"]["solver"]["name"] n = set_temporal_aggregation(n, opts, solver_name) From 4f6c58be3c18f5accbf61787dff1839a3ee1693d Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Sat, 19 Aug 2023 13:04:22 +0200 Subject: [PATCH 149/293] endogenous_hvdc_imports: align calculation of efficiencies --- config/config.default.yaml | 3 ++- scripts/prepare_sector_network.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index b34eb6d1b..dd528725f 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -606,7 +606,8 @@ sector: - x: 12.39 y: 45.56 length: 2500 - hvdc_losses: 3.e-5 # p.u./km + efficiency_static: 0.98 + efficiency_per_1000km: 0.977 # p.u./km length_factor: 1.25 distance_threshold: 0.05 # quantile diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 051400476..fdfdd973c 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3730,6 +3730,8 @@ def _coordinates(ct): n.madd("Bus", buses_i, **exporters.drop("geometry", axis=1)) + efficiency = cf["efficiency_static"] * cf["efficiency_per_1000km"] ** (import_links.values / 1e3) + n.madd( "Link", ["import hvdc-to-elec " + " ".join(idx).strip() for idx in import_links.index], @@ -3740,7 +3742,7 @@ def _coordinates(ct): p_nom_extendable=True, length=import_links.values, capital_cost=hvdc_cost * cost_factor, - efficiency=1 - import_links.values * cf["hvdc_losses"], + efficiency=efficiency, ) for tech in ["solar-utility", "onwind"]: From 60c618ea8d19539ddee2c77d00e0c469c61560f9 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Sat, 19 Aug 2023 13:07:11 +0200 Subject: [PATCH 150/293] prepare_sector_network: skip unnecessary geocoding --- rules/build_sector.smk | 1 + scripts/prepare_sector_network.py | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/rules/build_sector.smk b/rules/build_sector.smk index a4b3335fd..9a8037ce5 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -785,6 +785,7 @@ rule prepare_sector_network: import_costs="data/imports/results.csv", # TODO: host file on zenodo or elsewhere import_p_max_pu="data/imports/combined_weighted_generator_timeseries.nc", # TODO: host file on zenodo or elsewhere regions_onshore=RESOURCES + "regions_onshore_elec_s{simpl}_{clusters}.geojson", + country_centroids=HTTP.remote("https://raw.githubusercontent.com/gavinr/world-countries-centroids/v1.0.0/dist/countries.csv", keep_local=True), output: RESULTS + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index fdfdd973c..6c2f89684 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3684,9 +3684,13 @@ def add_endogenous_hvdc_import_options(n, cost_factor=1.0): ) def _coordinates(ct): - query = cc.convert(ct.split("-")[0], to="name") - loc = geocode(dict(country=query), language="en") - return [loc.longitude, loc.latitude] + iso2 = ct.split("-")[0] + if iso2 in country_centroids.index: + return country_centroids.loc[iso2, ["longitude", "latitude"]].values + else: + query = cc.convert(iso2, to="name") + loc = geocode(dict(country=query), language="en") + return [loc.longitude, loc.latitude] exporters = pd.DataFrame( {ct: _coordinates(ct) for ct in cf["exporters"]}, index=["x", "y"] @@ -4405,6 +4409,8 @@ def lossy_bidirectional_links(n, carrier, efficiencies={}): pd.read_csv(snakemake.input.pop_weighted_energy_totals, index_col=0) * nyears ) + country_centroids = pd.read_csv(snakemake.input.country_centroids[0], index_col='ISO') + patch_electricity_network(n) spatial = define_spatial(pop_layout.index, options) From e0eae245eff70246b3ee5f95751aa053164f1879 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 19 Aug 2023 11:07:40 +0000 Subject: [PATCH 151/293] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Snakefile | 1 + rules/build_sector.smk | 5 +++- .../plot_renewable_potential_unclustered.py | 24 ++++++++++++------- scripts/prepare_sector_network.py | 9 +++++-- 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/Snakefile b/Snakefile index 536fa9bca..85777de6c 100644 --- a/Snakefile +++ b/Snakefile @@ -33,6 +33,7 @@ BENCHMARKS = "benchmarks/" + RDIR RESOURCES = "resources/" + RDIR if not run.get("shared_resources") else "resources/" RESULTS = "results/" + RDIR + localrules: purge, diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 9a8037ce5..028446ece 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -785,7 +785,10 @@ rule prepare_sector_network: import_costs="data/imports/results.csv", # TODO: host file on zenodo or elsewhere import_p_max_pu="data/imports/combined_weighted_generator_timeseries.nc", # TODO: host file on zenodo or elsewhere regions_onshore=RESOURCES + "regions_onshore_elec_s{simpl}_{clusters}.geojson", - country_centroids=HTTP.remote("https://raw.githubusercontent.com/gavinr/world-countries-centroids/v1.0.0/dist/countries.csv", keep_local=True), + country_centroids=HTTP.remote( + "https://raw.githubusercontent.com/gavinr/world-countries-centroids/v1.0.0/dist/countries.csv", + keep_local=True, + ), output: RESULTS + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", diff --git a/scripts/plot_renewable_potential_unclustered.py b/scripts/plot_renewable_potential_unclustered.py index d7e31af67..59ba196a1 100644 --- a/scripts/plot_renewable_potential_unclustered.py +++ b/scripts/plot_renewable_potential_unclustered.py @@ -13,8 +13,8 @@ import pandas as pd import xarray as xr -def plot_map(regions, color, cmap, label, vmin=None, vmax=None): +def plot_map(regions, color, cmap, label, vmin=None, vmax=None): proj = ccrs.EqualEarth() regions = regions.to_crs(proj.proj4_init) fig, ax = plt.subplots(figsize=(7, 7), subplot_kw={"projection": proj}) @@ -64,7 +64,9 @@ def plot_map(regions, color, cmap, label, vmin=None, vmax=None): wind = wind.groupby(level=0).sum().reindex(regions.index, fill_value=0) wind_per_skm = wind / regions.Area / 1e3 # GWh - fig, ax = plot_map(regions, wind_per_skm, "Blues", r"Wind Energy Potential [GWh/a/km$^2$]") + fig, ax = plot_map( + regions, wind_per_skm, "Blues", r"Wind Energy Potential [GWh/a/km$^2$]" + ) for fn in snakemake.output["wind"]: plt.savefig(fn) @@ -73,9 +75,11 @@ def plot_map(regions, color, cmap, label, vmin=None, vmax=None): for profile in ["onwind", "offwind-ac", "offwind-dc"]: ds = xr.open_dataset(snakemake.input[f"profile_{profile}"]) cf = pd.concat([cf, (ds.profile.mean("time") * 100).to_pandas()]) - cf = cf.groupby(level=0).mean().reindex(regions.index, fill_value=0.) + cf = cf.groupby(level=0).mean().reindex(regions.index, fill_value=0.0) - fig, ax = plot_map(regions, cf, "Blues", r"Wind Capacity Factor [%]", vmin=0, vmax=60) + fig, ax = plot_map( + regions, cf, "Blues", r"Wind Capacity Factor [%]", vmin=0, vmax=60 + ) for fn in snakemake.output["wind_cf"]: plt.savefig(fn) @@ -86,18 +90,22 @@ def plot_map(regions, color, cmap, label, vmin=None, vmax=None): ds = xr.open_dataset(snakemake.input.profile_solar) solar = (ds.p_nom_max * ds.profile.sum("time")).to_pandas() - solar = solar.groupby(level=0).sum().reindex(onregions.index, fill_value=0.) + solar = solar.groupby(level=0).sum().reindex(onregions.index, fill_value=0.0) solar_per_skm = solar / onregions.Area / 1e3 # GWh - fig, ax = plot_map(onregions, solar_per_skm, "Reds", r"Solar Energy Potential [GWh/a/km$^2$]") + fig, ax = plot_map( + onregions, solar_per_skm, "Reds", r"Solar Energy Potential [GWh/a/km$^2$]" + ) for fn in snakemake.output["solar"]: plt.savefig(fn) cf = (ds.profile.mean("time") * 100).to_pandas() - cf = cf.groupby(level=0).mean().reindex(onregions.index, fill_value=0.) + cf = cf.groupby(level=0).mean().reindex(onregions.index, fill_value=0.0) - fig, ax = plot_map(onregions, cf, "Reds", r"Solar Capacity Factor [%]", vmin=6, vmax=20) + fig, ax = plot_map( + onregions, cf, "Reds", r"Solar Capacity Factor [%]", vmin=6, vmax=20 + ) for fn in snakemake.output["solar_cf"]: plt.savefig(fn) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 6c2f89684..352932300 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -807,6 +807,7 @@ def add_methanol_reforming(n, costs): lifetime=costs.at[tech, "lifetime"], ) + def add_methanol_reforming_cc(n, costs): logger.info("Adding methanol steam reforming with carbon capture.") @@ -3734,7 +3735,9 @@ def _coordinates(ct): n.madd("Bus", buses_i, **exporters.drop("geometry", axis=1)) - efficiency = cf["efficiency_static"] * cf["efficiency_per_1000km"] ** (import_links.values / 1e3) + efficiency = cf["efficiency_static"] * cf["efficiency_per_1000km"] ** ( + import_links.values / 1e3 + ) n.madd( "Link", @@ -4409,7 +4412,9 @@ def lossy_bidirectional_links(n, carrier, efficiencies={}): pd.read_csv(snakemake.input.pop_weighted_energy_totals, index_col=0) * nyears ) - country_centroids = pd.read_csv(snakemake.input.country_centroids[0], index_col='ISO') + country_centroids = pd.read_csv( + snakemake.input.country_centroids[0], index_col="ISO" + ) patch_electricity_network(n) From d65501d3fc1b0a549f15a2b9ccd267456ed4365c Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Sat, 19 Aug 2023 13:31:01 +0200 Subject: [PATCH 152/293] methanol-to-kerosene: add carrier --- 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 352932300..11a4cc317 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -780,6 +780,7 @@ def add_methanol_to_kerosene(n, costs): / costs.at[tech, "methanol-input"], p_nom_extendable=True, p_min_pu=1, + carrier=tech, ) From b0dd5511e225c4b21c1f220e5b7acba89f05475a Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Sat, 19 Aug 2023 13:44:01 +0200 Subject: [PATCH 153/293] cluster_heat_buses: avoid error with duplicate index --- scripts/prepare_sector_network.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 11a4cc317..ed9421131 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -4213,13 +4213,9 @@ def define_clustering(attributes, aggregate_dict): pnl = c.pnl agg = define_clustering(pd.Index(pnl.keys()), aggregate_dict) for k in pnl.keys(): - pnl[k].rename( - columns=lambda x: x.replace("residential ", "").replace( - "services ", "" - ), - inplace=True, - ) - pnl[k] = pnl[k].groupby(level=0, axis=1).agg(agg[k], **agg_group_kwargs) + def renamer(s): + return s.replace("residential ", "").replace("services ", "") + pnl[k] = pnl[k].groupby(renamer, axis=1).agg(agg[k], **agg_group_kwargs) # remove unclustered assets of service/residential to_drop = c.df.index.difference(df.index) From 270a012b192d5d1e1ee5fca70b32d0a1d2ea4d22 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 19 Aug 2023 11:44:23 +0000 Subject: [PATCH 154/293] [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, 2 insertions(+) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index ed9421131..8141a7480 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -4213,8 +4213,10 @@ def define_clustering(attributes, aggregate_dict): pnl = c.pnl agg = define_clustering(pd.Index(pnl.keys()), aggregate_dict) for k in pnl.keys(): + def renamer(s): return s.replace("residential ", "").replace("services ", "") + pnl[k] = pnl[k].groupby(renamer, axis=1).agg(agg[k], **agg_group_kwargs) # remove unclustered assets of service/residential From 41fb6a386596ef2f74b0f1ab29f487dbba1bc939 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 21 Aug 2023 15:03:11 +0200 Subject: [PATCH 155/293] tsam: improve logging of segmentation clustering --- 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 ed9421131..9e99fd391 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -4279,6 +4279,7 @@ def apply_time_segmentation( sn_weightings = pd.Series( weightings, index=snapshots, name="weightings", dtype="float64" ) + logger.info("Distribution of snapshot durations: ", weightings.value_counts()) n.set_snapshots(sn_weightings.index) n.snapshot_weightings = n.snapshot_weightings.mul(sn_weightings, axis=0) From 310f09cad20123d80903c18a123d90b233514eac Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 21 Aug 2023 15:03:41 +0200 Subject: [PATCH 156/293] prepare_sector_network: remove duplicate methanol steam reforming --- scripts/prepare_sector_network.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 9e99fd391..3280e6e17 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -847,21 +847,6 @@ def add_methanol_reforming_cc(n, costs): lifetime=costs.at[tech, "lifetime"], ) - n.madd( - "Link", - nodes, - 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_dac(n, costs): heat_carriers = ["urban central heat", "services urban decentral heat"] From 7c0b6ff07035501d5a9647b6b4551989fc9507ba Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Tue, 22 Aug 2023 17:27:07 +0200 Subject: [PATCH 157/293] bugfix: cluster heat buses, pd.Series.sum can cause problems with time-dependent data --- scripts/prepare_sector_network.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 3280e6e17..fb6c4481d 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -4121,28 +4121,28 @@ def limit_individual_line_extension(n, maxext): aggregate_dict = { "p_nom": pd.Series.sum, "s_nom": pd.Series.sum, - "v_nom": pd.Series.max, - "v_mag_pu_max": pd.Series.min, - "v_mag_pu_min": pd.Series.max, + "v_nom": "max", + "v_mag_pu_max": "min", + "v_mag_pu_min": "max", "p_nom_max": pd.Series.sum, "s_nom_max": pd.Series.sum, "p_nom_min": pd.Series.sum, "s_nom_min": pd.Series.sum, - "v_ang_min": pd.Series.max, - "v_ang_max": pd.Series.min, - "terrain_factor": pd.Series.mean, - "num_parallel": pd.Series.sum, - "p_set": pd.Series.sum, - "e_initial": pd.Series.sum, + "v_ang_min": "max", + "v_ang_max": "min", + "terrain_factor": "mean", + "num_parallel": "sum", + "p_set": "sum", + "e_initial": "sum", "e_nom": pd.Series.sum, "e_nom_max": pd.Series.sum, "e_nom_min": pd.Series.sum, - "state_of_charge_initial": pd.Series.sum, - "state_of_charge_set": pd.Series.sum, - "inflow": pd.Series.sum, + "state_of_charge_initial": "sum", + "state_of_charge_set": "sum", + "inflow": "sum", "p_max_pu": "first", - "x": pd.Series.mean, - "y": pd.Series.mean, + "x": "mean", + "y": "mean", } From b8d812707c1f0aadb7bcf6d91e429a0d5ac6acf0 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Tue, 22 Aug 2023 17:28:31 +0200 Subject: [PATCH 158/293] increase memory requirements of prepare_sector_network due to segmentation clustering --- 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 028446ece..6df161545 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -794,7 +794,7 @@ rule prepare_sector_network: + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", threads: 1 resources: - mem_mb=2000, + mem_mb=8000, log: LOGS + "prepare_sector_network_elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.log", From 603e9a45c303ee953c63057ebd1f35fb3a90f481 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Tue, 22 Aug 2023 17:28:53 +0200 Subject: [PATCH 159/293] env: update dependency versions --- envs/environment.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/envs/environment.yaml b/envs/environment.yaml index c3af36bbd..a3c0e4594 100644 --- a/envs/environment.yaml +++ b/envs/environment.yaml @@ -10,7 +10,9 @@ dependencies: - python>=3.8 - pip +- pypsa>=0.25.1 - atlite>=0.2.9 +- linopy>=0.2.5 - dask # Dependencies of the workflow itself @@ -55,5 +57,4 @@ dependencies: - pip: - - tsam>=1.1.0 - - pypsa>=0.25.1 + - tsam>=2.3.0 From 2fddbff6b7d56064b2311bafabe4e38621839364 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 24 Aug 2023 08:13:25 +0000 Subject: [PATCH 160/293] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/add_existing_baseyear.py | 7 +++++-- scripts/build_industrial_distribution_key.py | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index 269705a78..088104701 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -447,8 +447,11 @@ def add_heating_capacities_installed_before_baseyear( ) # if rural heating demand for one of the nodes doesn't exist, # then columns were dropped before and heating demand share should be 0.0 - if all(f"{node} {service} rural heat" in p_set_sum.index for service in ["residential", "services"]) - else 0. + if all( + f"{node} {service} rural heat" in p_set_sum.index + for service in ["residential", "services"] + ) + else 0.0 for node in nodal_df.index ], index=nodal_df.index, diff --git a/scripts/build_industrial_distribution_key.py b/scripts/build_industrial_distribution_key.py index 63299570c..bc4a26bcf 100644 --- a/scripts/build_industrial_distribution_key.py +++ b/scripts/build_industrial_distribution_key.py @@ -13,13 +13,14 @@ import uuid from itertools import product +import country_converter as coco import geopandas as gpd import pandas as pd from packaging.version import Version, parse -import country_converter as coco cc = coco.CountryConverter() + def locate_missing_industrial_sites(df): """ Locate industrial sites without valid locations based on city and From 232ac832b9daeb78fab0c61900a9b6264e38ffeb Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 24 Aug 2023 16:51:27 +0200 Subject: [PATCH 161/293] consider grid connection costs for endogenous hvdc import --- 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 445b4efd6..7971ff04c 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3753,6 +3753,8 @@ def _coordinates(ct): exporters_tech_i = exporters.index.intersection(p_max_pu_tech.columns) + grid_connection = costs.at["electricity grid connection", "fixed"] + n.madd( "Generator", exporters_tech_i, @@ -3760,7 +3762,7 @@ def _coordinates(ct): bus=exporters_tech_i, carrier=f"external {tech}", p_nom_extendable=True, - capital_cost=costs.at[tech, "fixed"] * cost_factor, + capital_cost=(costs.at[tech, "fixed"] + grid_connection) * cost_factor, lifetime=costs.at[tech, "lifetime"], p_max_pu=p_max_pu_tech.reindex(columns=exporters_tech_i), ) From bec727f66998f7c6c8fb62b11223477974a7d0fd Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 24 Aug 2023 16:52:11 +0200 Subject: [PATCH 162/293] add future links for import data --- rules/build_sector.smk | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 6df161545..5930332f2 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -782,8 +782,10 @@ rule prepare_sector_network: + "solar_thermal_rural_elec_s{simpl}_{clusters}.nc" if config["sector"]["solar_thermal"] else [], - import_costs="data/imports/results.csv", # TODO: host file on zenodo or elsewhere - import_p_max_pu="data/imports/combined_weighted_generator_timeseries.nc", # TODO: host file on zenodo or elsewhere + # import_costs=HTTP.remote("https://tubcloud.tu-berlin.de/s/Woq9Js5MjtoaqGP/download/results.csv", keep_local=True), + import_costs="data/imports/results.csv", + # import_p_max_pu=HTTP.remote("https://tubcloud.tu-berlin.de/s/qPaoD54qHtEAo8i/download/combined_weighted_generator_timeseries.nc", keep_local=True), + import_p_max_pu="data/imports/combined_weighted_generator_timeseries.nc", regions_onshore=RESOURCES + "regions_onshore_elec_s{simpl}_{clusters}.geojson", country_centroids=HTTP.remote( "https://raw.githubusercontent.com/gavinr/world-countries-centroids/v1.0.0/dist/countries.csv", From 2580aaa148bcb3bdcb60bc4d11854ae97559becb Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 25 Aug 2023 14:57:26 +0200 Subject: [PATCH 163/293] add option to set maximum capacity of HVDC import links --- config/config.default.yaml | 1 + scripts/prepare_sector_network.py | 1 + 2 files changed, 2 insertions(+) diff --git a/config/config.default.yaml b/config/config.default.yaml index dd528725f..d81536e5d 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -610,6 +610,7 @@ sector: efficiency_per_1000km: 0.977 # p.u./km length_factor: 1.25 distance_threshold: 0.05 # quantile + p_nom_max: .inf # for HVDC links # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#industry industry: diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 7971ff04c..0a8edcc39 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3746,6 +3746,7 @@ def _coordinates(ct): length=import_links.values, capital_cost=hvdc_cost * cost_factor, efficiency=efficiency, + p_nom_max=cf["p_nom_max"], ) for tech in ["solar-utility", "onwind"]: From 590eea0bbc4f215167fae8e0e8c9290caaf61b8e Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 25 Aug 2023 14:58:02 +0200 Subject: [PATCH 164/293] consider maximum RES potential in exporting countries --- config/config.default.yaml | 1 + scripts/prepare_sector_network.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/config/config.default.yaml b/config/config.default.yaml index d81536e5d..f829f73b2 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -611,6 +611,7 @@ sector: length_factor: 1.25 distance_threshold: 0.05 # quantile p_nom_max: .inf # for HVDC links + share_of_p_nom_max_available: 1. # for wind and solar generators # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#industry industry: diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 0a8edcc39..2ad051a23 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3680,6 +3680,8 @@ def add_endogenous_hvdc_import_options(n, cost_factor=1.0): importer="EUE" ) + p_nom_max = xr.open_dataset(snakemake.input.import_p_max_pu).p_nom_max.sel(importer="EUE").to_pandas() + def _coordinates(ct): iso2 = ct.split("-")[0] if iso2 in country_centroids.index: @@ -3766,6 +3768,7 @@ def _coordinates(ct): capital_cost=(costs.at[tech, "fixed"] + grid_connection) * cost_factor, lifetime=costs.at[tech, "lifetime"], p_max_pu=p_max_pu_tech.reindex(columns=exporters_tech_i), + p_nom_max=p_nom_max[tech].reindex(index=exporters_tech_i) * cf["share_of_p_nom_max_available"], ) # hydrogen storage From 474415d1f8c41084f605e786368e1aae409eff5d Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 25 Aug 2023 14:59:08 +0200 Subject: [PATCH 165/293] add option to set maximum capacity of import links/generators --- config/config.default.yaml | 2 ++ scripts/prepare_sector_network.py | 8 +++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index f829f73b2..ca04204bd 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -536,6 +536,8 @@ sector: - shipping-ftfuel - shipping-meoh - hvdc-to-elec + p_nom_max: + pipeline-h2: .inf endogenous_hvdc_import: enable: false exporters: diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 2ad051a23..92ea71810 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3971,6 +3971,8 @@ def add_import_options( import_nodes_tech.dropna(inplace=True) + upper_p_nom_max = import_config["p_nom_max"].get(tech, np.inf) + suffix = bus_suffix[tech] if tech in co2_intensity.keys(): @@ -4001,8 +4003,8 @@ def add_import_options( marginal_cost=import_nodes_tech.marginal_cost.values, p_nom_extendable=True, capital_cost=capital_cost, - p_nom_min=import_nodes_tech.p_nom.values, - p_nom_max=import_nodes_tech.p_nom.values * capacity_boost, + p_nom_min=import_nodes_tech.p_nom.clip(upper=upper_p_nom_max).values, + p_nom_max=import_nodes_tech.p_nom.mul(capacity_boost).clip(upper=upper_p_nom_max).values, ) else: @@ -4021,7 +4023,7 @@ def add_import_options( marginal_cost=import_nodes_tech.marginal_cost.values, p_nom_extendable=True, capital_cost=capital_cost, - p_nom_max=import_nodes_tech.p_nom.values * capacity_boost, + p_nom_max=import_nodes_tech.p_nom.mul(capacity_boost).clip(upper=upper_p_nom_max).values, ) # need special handling for copperplated imports From 51e288bcc41dde274c975d1a6c5e23a3a74abaa7 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 25 Aug 2023 16:29:21 +0200 Subject: [PATCH 166/293] add option to consider waste heat from Sabatier in DH network --- config/config.default.yaml | 1 + scripts/prepare_sector_network.py | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/config/config.default.yaml b/config/config.default.yaml index ca04204bd..e26b2f67e 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -493,6 +493,7 @@ sector: min_part_load_methanolisation: 0.5 use_fischer_tropsch_waste_heat: true use_methanolisation_waste_heat: true + use_methanation_waste_heat: true use_fuel_cell_waste_heat: true use_electrolysis_waste_heat: false electricity_distribution_grid: true diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 92ea71810..532536420 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3528,6 +3528,14 @@ def add_waste_heat(n): 0.95 - n.links.loc[urban_central + " Fischer-Tropsch", "efficiency"] ) + if options["use_methanation_waste_heat"]: + n.links.loc[urban_central + " Sabatier", "bus3"] = ( + urban_central + " urban central heat" + ) + n.links.loc[urban_central + " Sabatier", "efficiency3"] = ( + 0.95 - n.links.loc[urban_central + " Sabatier", "efficiency"] + ) + if options["use_methanolisation_waste_heat"]: n.links.loc[urban_central + " methanolisation", "bus4"] = ( urban_central + " urban central heat" From afb99a5e6c46fa4ffa23432b85e142b664f57013 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 25 Aug 2023 16:31:03 +0200 Subject: [PATCH 167/293] make waste heat recovery of electrolysis the default --- 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 e26b2f67e..57172fc47 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -495,7 +495,7 @@ sector: use_methanolisation_waste_heat: true use_methanation_waste_heat: true use_fuel_cell_waste_heat: true - use_electrolysis_waste_heat: false + use_electrolysis_waste_heat: true electricity_distribution_grid: true electricity_distribution_grid_cost_factor: 1.0 electricity_grid_connection: true From bf4399aabbe4346be13f9e61b2c62b0536c7f147 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Sat, 26 Aug 2023 17:09:26 +0200 Subject: [PATCH 168/293] tech_colors for when cluster_heat_buses is activated --- config/config.default.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/config/config.default.yaml b/config/config.default.yaml index 57172fc47..ceb8caea7 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -858,8 +858,10 @@ plotting: solar thermal: '#ffbf2b' residential rural solar thermal: '#f1c069' services rural solar thermal: '#eabf61' + rural solar thermal: '#eabf61' residential urban decentral solar thermal: '#e5bc5a' services urban decentral solar thermal: '#dfb953' + urban decentral solar thermal: '#dfb953' urban central solar thermal: '#d7b24c' solar rooftop: '#ffea80' # gas @@ -873,6 +875,8 @@ plotting: residential urban decentral gas boiler: '#cb7a36' services rural gas boiler: '#c4813f' services urban decentral gas boiler: '#ba8947' + rural gas boiler: '#c4813f' + urban decentral gas boiler: '#ba8947' urban central gas boiler: '#b0904f' gas: '#e05b09' fossil gas: '#e05b09' @@ -896,7 +900,9 @@ plotting: oil boiler: '#adadad' residential rural oil boiler: '#a9a9a9' services rural oil boiler: '#a5a5a5' + rural oil boiler: '#a5a5a5' residential urban decentral oil boiler: '#a1a1a1' + urban decentral oil boiler: '#a1a1a1' urban central oil boiler: '#9d9d9d' services urban decentral oil boiler: '#999999' agriculture machinery oil: '#949494' @@ -931,6 +937,8 @@ plotting: residential urban decentral biomass boiler: '#b0b87b' services rural biomass boiler: '#c6cf98' services urban decentral biomass boiler: '#dde5b5' + rural biomass boiler: '#a1a066' + urban decentral biomass boiler: '#b0b87b' biomass to liquid: '#32CD32' BioSNG: '#123456' # power transmission @@ -962,8 +970,10 @@ plotting: water tanks: '#e69487' residential rural water tanks: '#f7b7a3' services rural water tanks: '#f3afa3' + rural water tanks: '#f3afa3' residential urban decentral water tanks: '#f2b2a3' services urban decentral water tanks: '#f1b4a4' + urban decentral water tanks: '#f1b4a4' urban central water tanks: '#e9977d' hot water storage: '#e69487' hot water charging: '#e8998b' @@ -972,12 +982,16 @@ plotting: residential urban decentral water tanks charger: '#b39995' services rural water tanks charger: '#b3abb0' services urban decentral water tanks charger: '#b3becc' + rural water tanks charger: '#b3abb0' + urban decentral water tanks charger: '#b3becc' hot water discharging: '#e99c8e' urban central water tanks discharger: '#b9816e' residential rural water tanks discharger: '#ba9685' residential urban decentral water tanks discharger: '#baac9e' services rural water tanks discharger: '#bbc2b8' services urban decentral water tanks discharger: '#bdd8d3' + rural water tanks discharger: '#bbc2b8' + urban decentral water tanks discharger: '#bdd8d3' # heat demand Heat load: '#cc1f1f' heat: '#cc1f1f' @@ -990,6 +1004,7 @@ plotting: decentral heat: '#750606' residential urban decentral heat: '#a33c3c' services urban decentral heat: '#cc1f1f' + urban decentral heat: '#cc1f1f' low-temperature heat for industry: '#8f2727' process heat: '#ff0000' agriculture heat: '#d9a5a5' @@ -999,10 +1014,12 @@ plotting: air heat pump: '#36eb41' residential urban decentral air heat pump: '#48f74f' services urban decentral air heat pump: '#5af95d' + urban decentral air heat pump: '#5af95d' urban central air heat pump: '#6cfb6b' ground heat pump: '#2fb537' residential rural ground heat pump: '#48f74f' services rural ground heat pump: '#5af95d' + rural ground heat pump: '#5af95d' Ambient: '#98eb9d' CHP: '#8a5751' urban central gas CHP: '#8d5e56' @@ -1015,7 +1032,9 @@ plotting: residential rural resistive heater: '#bef5b5' residential urban decentral resistive heater: '#b2f1a9' services rural resistive heater: '#a5ed9d' + rural resistive heater: '#a5ed9d' services urban decentral resistive heater: '#98e991' + urban decentral resistive heater: '#98e991' urban central resistive heater: '#8cdf85' retrofitting: '#8487e8' building retrofitting: '#8487e8' From 448cba5f5ca371839c80dab417612bd5dfdb2ad1 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Sat, 26 Aug 2023 17:14:09 +0200 Subject: [PATCH 169/293] plot_balance_timeseries: initial version --- rules/plot.smk | 13 ++ scripts/plot_balance_timeseries.py | 221 +++++++++++++++++++++++++++++ 2 files changed, 234 insertions(+) create mode 100644 scripts/plot_balance_timeseries.py diff --git a/rules/plot.smk b/rules/plot.smk index 29f3f6195..410020599 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -147,3 +147,16 @@ rule plot_biomass_potentials: ), script: "../scripts/plot_biomass_potentials.py" + + + +rule plot_balance_timeseries: + input: + network=RESULTS + + "postnetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + rc="matplotlibrc", + threads: 12 + output: + directory(RESULTS + "graphics/balance_timeseries/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}") + script: + "../scripts/plot_balance_timeseries.py" diff --git a/scripts/plot_balance_timeseries.py b/scripts/plot_balance_timeseries.py new file mode 100644 index 000000000..c27dfd694 --- /dev/null +++ b/scripts/plot_balance_timeseries.py @@ -0,0 +1,221 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2023- Fabian Neumann +# +# SPDX-License-Identifier: MIT +""" +Plot balance time series. +""" + +import logging +import pypsa +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt +import matplotlib.dates as mdates +from multiprocessing import Pool + +logger = logging.getLogger(__name__) + +THRESHOLD = 5 # GW + +CARRIER_GROUPS = { + "electricity": ["AC", "low voltage"], + "heat": ["urban central heat", "urban decentral heat", "rural heat", "residential urban decentral heat", "residential rural heat", "services urban decentral heat", "services rural heat"], + "hydrogen": "H2", + "oil": "oil", + "methanol": "methanol", + "ammonia": "NH3", + "biomass": ["solid biomass", "biogas"], + "CO2 atmosphere": "co2", + "CO2 stored": "co2 stored", + "methane": "gas", +} + +def plot_stacked_area_steplike(ax, df, colors={}): + + if isinstance(colors, pd.Series): + colors = colors.to_dict() + + df_cum = df.cumsum(axis=1) + + previous_series = np.zeros_like(df_cum.iloc[:, 0].values) + + for col in df_cum.columns: + ax.fill_between( + df_cum.index, + previous_series, + df_cum[col], + step='pre', + linewidth=0, + color=colors.get(col, 'grey'), + label=col, + ) + previous_series = df_cum[col].values + + +def plot_energy_balance_timeseries( + df, + time=None, + ylim=None, + resample=None, + rename={}, + preferred_order=[], + ylabel="", + colors={}, + threshold=0, + dir="", +): + + if time is not None: + df = df.loc[time] + + timespan = (df.index[-1] - df.index[0]) + long_time_frame = timespan > pd.Timedelta(weeks=5) + + techs_below_threshold = df.columns[df.abs().max() < threshold].tolist() + if techs_below_threshold: + other = {tech: "other" for tech in techs_below_threshold} + rename.update(other) + colors["other"] = 'grey' + + if rename: + df = df.groupby(df.columns.map(lambda a: rename.get(a, a)), axis=1).sum() + + if resample is not None: + # upsampling to hourly resolution required to handle overlapping block + df = df.resample("1H").ffill().resample(resample).mean() + + order = (df / df.max()).var().sort_values().index + if preferred_order: + order = preferred_order.intersection(order).append( + order.difference(preferred_order) + ) + df = df.loc[:, order] + + # fillna since plot_stacked_area_steplike cannot deal with NaNs + pos = df.where(df > 0).fillna(0.) + neg = df.where(df < 0).fillna(0.) + + fig, ax = plt.subplots(figsize=(10, 4), layout="constrained") + + plot_stacked_area_steplike(ax, pos, colors) + plot_stacked_area_steplike(ax, neg, colors) + + plt.xlim((df.index[0], df.index[-1])) + + if not long_time_frame: + # Set major ticks every Monday + ax.xaxis.set_major_locator(mdates.WeekdayLocator(byweekday=mdates.MONDAY)) + ax.xaxis.set_major_formatter(mdates.DateFormatter('%e\n%b')) + # Set minor ticks every day + ax.xaxis.set_minor_locator(mdates.DayLocator()) + ax.xaxis.set_minor_formatter(mdates.DateFormatter('%e')) + else: + # Set major ticks every first day of the month + ax.xaxis.set_major_locator(mdates.MonthLocator(bymonthday=1)) + ax.xaxis.set_major_formatter(mdates.DateFormatter('%e\n%b')) + # Set minor ticks every 15th of the month + ax.xaxis.set_minor_locator(mdates.MonthLocator(bymonthday=15)) + ax.xaxis.set_minor_formatter(mdates.DateFormatter('%e')) + + ax.tick_params(axis='x', which='minor', labelcolor='grey') + ax.grid(axis='y') + + # half the labels because pos and neg create duplicate labels + handles, labels = ax.get_legend_handles_labels() + half = int(len(handles) / 2) + fig.legend( + handles=handles[:half], + labels=labels[:half], + loc="outside right upper" + ) + + ax.axhline(0, color="grey", linewidth=0.5) + + if ylim is None: + # ensure y-axis extent is symmetric around origin in steps of 100 units + ylim = np.ceil(max(-neg.sum(axis=1).min(), pos.sum(axis=1).max()) / 100) * 100 + plt.ylim([-ylim, ylim]) + + is_kt = any(s in ylabel.lower() for s in ["co2", "steel", "hvc"]) + unit = "kt/h" if is_kt else "GW" + plt.ylabel(f"{ylabel} balance [{unit}]") + + if not long_time_frame: + # plot frequency of snapshots on top x-axis as ticks + ax2 = ax.twiny() + ax2.set_xlim(ax.get_xlim()) + ax2.set_xticks(df.index) + ax2.grid(False) + ax2.tick_params(axis='x', length=2) + ax2.xaxis.set_tick_params(labelbottom=False) + ax2.set_xticklabels([]) + + if resample is None: + resample = "native" + fn = f"ts-balance-{ylabel.replace(' ', '_')}-{resample}" + plt.savefig(dir + "/" + fn + ".pdf") + plt.savefig(dir + "/" + fn + ".png") + + + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake( + "plot_balance_timeseries", + simpl="", + clusters=100, + ll="v1.5", + opts="", + sector_opts="Co2L0-2190SEG-T-H-B-I-S-A", + planning_horizons=2050, + configfiles="../../config/config.100n-seg.yaml" + ) + + plt.style.use(["bmh", snakemake.input.rc]) + + n = pypsa.Network(snakemake.input.network) + + months = pd.date_range(freq="M", **snakemake.config["snapshots"]).format( + formatter=lambda x: x.strftime("%Y-%m") + ) + + balance = n.statistics.energy_balance(aggregate_time=False) + + n.carriers.color.update(snakemake.config["plotting"]["tech_colors"]) + colors = n.carriers.color.rename(n.carriers.nice_name) + + # wrap in function for multiprocessing + def process_group(group, carriers, balance, months, colors): + if not isinstance(carriers, list): + carriers = [carriers] + + mask = balance.index.get_level_values("bus_carrier").isin(carriers) + df = balance[mask].groupby("carrier").sum().div(1e3).T + + # native resolution for each month and carrier + for month in months: + plot_energy_balance_timeseries( + df, + time=month, + ylabel=group, + colors=colors, + threshold=THRESHOLD, + dir=snakemake.output[0] + ) + + # daily resolution for each carrier + plot_energy_balance_timeseries( + df, + resample="D", + ylabel=group, + colors=colors, + threshold=THRESHOLD, + dir=snakemake.output[0] + ) + + args = [(group, carriers, balance, months, colors) for group, carriers in CARRIER_GROUPS.items()] + with Pool(processes=snakemake.threads) as pool: + pool.starmap(process_group, args) From 635dd68a2a6a3e3dfc7013b18766b93ff7c0d1a0 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Sat, 26 Aug 2023 17:14:29 +0200 Subject: [PATCH 170/293] add missing import_config --- scripts/prepare_sector_network.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 532536420..b0d007418 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3914,6 +3914,8 @@ def add_import_options( import_nodes = pd.read_csv(fn, index_col=0) import_nodes["hvdc-to-elec"] = 15000 + import_config = snakemake.config["sector"]["import"] + ports = pd.read_csv(snakemake.input.ports, index_col=0) translate = { From 8ea7ba8b476caa1b3004f6149430946cf3301b48 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Sun, 27 Aug 2023 13:06:31 +0200 Subject: [PATCH 171/293] plot_heatmap: initial version --- rules/plot.smk | 23 ++ scripts/plot_heatmap_timeseries.py | 236 +++++++++++++++++++ scripts/plot_heatmap_timeseries_resources.py | 105 +++++++++ 3 files changed, 364 insertions(+) create mode 100644 scripts/plot_heatmap_timeseries.py create mode 100644 scripts/plot_heatmap_timeseries_resources.py diff --git a/rules/plot.smk b/rules/plot.smk index 410020599..213cbdb88 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -160,3 +160,26 @@ rule plot_balance_timeseries: directory(RESULTS + "graphics/balance_timeseries/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}") script: "../scripts/plot_balance_timeseries.py" + + +rule plot_heatmap_timeseries: + input: + network=RESULTS + + "postnetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + rc="matplotlibrc", + threads: 12 + output: + directory(RESULTS + "graphics/heatmap_timeseries/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}") + script: + "../scripts/plot_heatmap_timeseries.py" + +rule plot_heatmap_timeseries_resources: + input: + network=RESULTS + + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + rc="matplotlibrc", + threads: 12 + output: + directory(RESULTS + "graphics/heatmap_timeseries/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}") + script: + "../scripts/plot_heatmap_timeseries_resources.py" diff --git a/scripts/plot_heatmap_timeseries.py b/scripts/plot_heatmap_timeseries.py new file mode 100644 index 000000000..14c9066a7 --- /dev/null +++ b/scripts/plot_heatmap_timeseries.py @@ -0,0 +1,236 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2023- Fabian Neumann +# +# SPDX-License-Identifier: MIT +""" +Plot heatmap time series (results). +""" + +import logging +import pypsa +import pandas as pd +import seaborn as sns +import matplotlib.pyplot as plt +from multiprocessing import Pool + +logger = logging.getLogger(__name__) + +THRESHOLD_MW = 1e3 # 1 GW +THRESHOLD_MWh = 100e3 # 100 GWh + +MARGINAL_PRICES = [ + "AC", + "H2", + "NH3", + "gas", + "co2 stored", + "methanol", + "oil", + "rural heat", + "urban central heat", + "urban decentral heat" +] + +SKIP_UTILISATION_RATES = [ + 'DC', + 'H2 pipeline', + 'electricity distribution grid', + 'gas for industry', + 'gas for industry CC', + 'gas pipeline', + 'gas pipeline new', + 'process emissions', + 'process emissions CC', + 'solid biomass for industry', + 'solid biomass for industry CC', +] + + +def unstack_day_hour(s, sns): + if isinstance(sns, dict): + sns = pd.date_range(freq="h", **sns) + s_h = s.reindex(sns).ffill() + grouped = s_h.groupby(s_h.index.hour).agg(list) + index = [f"{i:02d}:00" for i in grouped.index] + columns = pd.date_range(s_h.index[0], s_h.index[-1], freq="D") + return pd.DataFrame(grouped.to_list(), index=index, columns=columns) + + +def plot_heatmap( + df, + vmin=None, + vmax=None, + center=None, + cmap="Greens", + label="", + title="", + cbar_kws={}, + tag="", + dir="", +): + _cbar_kws = dict(label=label, aspect=17, pad=0.015) + _cbar_kws.update(cbar_kws) + fig, ax = plt.subplots(figsize=(8.5, 4), constrained_layout=True) + sns.heatmap( + df, + cmap=cmap, + ax=ax, + vmin=vmin, + vmax=vmax, + center=center, + cbar_kws=_cbar_kws, + ) + plt.ylabel("hour of the day") + plt.xlabel("day of the year") + plt.title(title, fontsize='large') + + ax.grid(axis='y') + + hours = list(range(0,24)) + ax.set_yticks(hours[0::2]) + ax.set_yticklabels(df.index[0::2], rotation=0) + ax.set_yticks(hours, minor=True) + + major_ticks = [i for i, date in enumerate(df.columns) if date.day == 1] + minor_ticks = [i for i, date in enumerate(df.columns) if date.day == 15] + ax.set_xticks(major_ticks) + ax.set_xticklabels([df.columns[i].strftime('%e\n%b') for i in major_ticks], rotation=0) + ax.set_xticks(minor_ticks, minor=True) + ax.set_xticklabels([df.columns[i].strftime('%e') for i in minor_ticks], rotation=0, minor=True, color='grey') + + cb = ax.collections[0].colorbar + cb.outline.set_linewidth(0.75) + + for spine in ax.spines.values(): + spine.set_visible(True) + + title = title.lower().replace(" ", "_").replace("(", "").replace(")", "") + fn = f"ts-heatmap-{tag}-{title}" + plt.savefig(dir + "/" + fn + ".pdf") + plt.savefig(dir + "/" + fn + ".png") + plt.close() + + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake( + "plot_heatmap_timeseries", + simpl="", + clusters=100, + ll="v1.5", + opts="", + sector_opts="Co2L0-2190SEG-T-H-B-I-S-A", + planning_horizons=2050, + configfiles="../../config/config.100n-seg.yaml" + ) + + plt.style.use(["bmh", snakemake.input.rc]) + + + n = pypsa.Network(snakemake.input.network) + + dir = snakemake.output[0] + snapshots = snakemake.config["snapshots"] + carriers = n.carriers + + p_nom_opt = n.generators.groupby("carrier").p_nom_opt.sum() + data = ( + n.generators_t.p.groupby(n.generators.carrier, axis=1).sum() + / p_nom_opt * 100 # % + ) + data = data.loc[:, p_nom_opt > THRESHOLD_MW] + + def process_generator_utilisation(carrier, s): + df = unstack_day_hour(s, snapshots) + title = carriers.nice_name.get(carrier, carrier) + label = "utilisation rate [%]" + plot_heatmap( + df, + cmap="Blues", + label=label, + title=title, + vmin=0, + vmax=90, + cbar_kws=dict(extend='max'), + tag="utilisation_rate", + dir=dir, + ) + + with Pool(processes=snakemake.threads) as pool: + pool.starmap(process_generator_utilisation, data.items()) + + # marginal prices + data = n.buses_t.marginal_price.groupby(n.buses.carrier, axis=1).mean() + data = data[data.columns.intersection(MARGINAL_PRICES)].round(2) + + def process_marginal_prices(carrier, s): + df = unstack_day_hour(s, snapshots) + label = "marginal price [€/t]" if "co2" in carrier.lower() else "marginal price [€/MWh]" + plot_heatmap( + df, + cmap="Spectral_r", + label=label, + title=carrier, + cbar_kws=dict(extend='both'), + tag="marginal_price", + dir=dir, + ) + + with Pool(processes=snakemake.threads) as pool: + pool.starmap(process_marginal_prices, data.items()) + + # SOCs + e_nom_opt = n.stores.groupby("carrier").e_nom_opt.sum() + data = ( + n.stores_t.e.groupby(n.stores.carrier, axis=1).sum() + / e_nom_opt * 100 + ) + data = data.loc[:, e_nom_opt > THRESHOLD_MWh] + + def process_socs(carrier, s): + df = unstack_day_hour(s, snapshots) + label = "SOC [%]" + title = carriers.nice_name.get(carrier, carrier) + plot_heatmap( + df, + cmap="Purples", + vmin=0, + vmax=90, + label=label, + title=title, + cbar_kws=dict(extend='max'), + tag="soc", + dir=dir, + ) + + with Pool(processes=snakemake.threads) as pool: + pool.starmap(process_socs, data.items()) + + # link utilisation rates + p_nom_opt = n.links.groupby("carrier").p_nom_opt.sum() + data = ( + n.links_t.p0.groupby(n.links.carrier, axis=1).sum() + / p_nom_opt * 100 + ) + data = data[data.columns.difference(SKIP_UTILISATION_RATES)] + data = data.loc[:,p_nom_opt > THRESHOLD_MW] + + def process_link_utilisation(carrier, s): + df = unstack_day_hour(s, snapshots) + label = "utilisation rate [%]" + title = carriers.nice_name.get(carrier, carrier) + plot_heatmap( + df, + cmap="Reds", + vmin=0, + vmax=100, + label=label, + title=title, + tag="utilisation_rate", + dir=dir, + ) + + with Pool(processes=snakemake.threads) as pool: + pool.starmap(process_link_utilisation, data.items()) diff --git a/scripts/plot_heatmap_timeseries_resources.py b/scripts/plot_heatmap_timeseries_resources.py new file mode 100644 index 000000000..fada1cb36 --- /dev/null +++ b/scripts/plot_heatmap_timeseries_resources.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2023- Fabian Neumann +# +# SPDX-License-Identifier: MIT +""" +Plot heatmap time series (resources). +""" + +import logging +import pypsa +import matplotlib.pyplot as plt +from multiprocessing import Pool + +logger = logging.getLogger(__name__) + +from plot_heatmap_timeseries import unstack_day_hour, plot_heatmap + + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake( + "plot_heatmap_timeseries_resources", + simpl="", + clusters=100, + ll="v1.5", + opts="", + sector_opts="Co2L0-2190SEG-T-H-B-I-S-A", + planning_horizons=2050, + configfiles="../../config/config.100n-seg.yaml" + ) + + plt.style.use(["bmh", snakemake.input.rc]) + + n = pypsa.Network(snakemake.input.network) + + dir = snakemake.output[0] + snapshots = snakemake.config["snapshots"] + carriers = n.carriers + + # WWS capacity factors + data = n.generators_t.p_max_pu.groupby(n.generators.carrier, axis=1).mean() * 100 + + def process_capacity_factors(carrier, s): + df = unstack_day_hour(s, snapshots) + label = "capacity factor [%]" + title = carriers.nice_name.get(carrier, carrier) + plot_heatmap( + df, + cmap="Greens", + vmin=0, + vmax=100, + label=label, + title=title, + tag="capacity_factor", + dir=dir, + ) + + with Pool(processes=snakemake.threads) as pool: + pool.starmap(process_capacity_factors, data.items()) + + # heat pump COPs + data = n.links_t.efficiency.groupby(n.links.carrier, axis=1).mean() + + def process_cops(carrier, s): + df = unstack_day_hour(s, snapshots) + label = "coefficient of performance [-]" + title = carriers.nice_name.get(carrier, carrier) + plot_heatmap( + df, + cmap="Greens", + vmin=2, + vmax=4, + label=label, + title=title, + cbar_kws=dict(extend='both'), + tag="cop", + dir=dir, + ) + + with Pool(processes=snakemake.threads) as pool: + pool.starmap(process_cops, data.items()) + + # EV availability + data = n.links_t.p_max_pu.groupby(n.links.carrier, axis=1).mean() * 100 + + def process_availabilities(carrier, s): + df = unstack_day_hour(s, snapshots) + label = "availability [%]" + title = carriers.nice_name.get(carrier, carrier) + plot_heatmap( + df, + cmap="Greens", + vmin=60, + vmax=100, + label=label, + title=title, + tag="availability", + dir=dir, + ) + + with Pool(processes=snakemake.threads) as pool: + pool.starmap(process_availabilities, data.items()) + From c052246778d8b1c8222a26744292fdff6067435f Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Sun, 27 Aug 2023 17:05:54 +0200 Subject: [PATCH 172/293] plot_network: remove function plot_map_without() --- rules/postprocess.smk | 2 -- scripts/plot_network.py | 74 ----------------------------------------- 2 files changed, 76 deletions(-) diff --git a/rules/postprocess.smk b/rules/postprocess.smk index 5361c49e9..ec3387b24 100644 --- a/rules/postprocess.smk +++ b/rules/postprocess.smk @@ -20,8 +20,6 @@ rule plot_network: output: map=RESULTS + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-costs-all_{planning_horizons}.pdf", - today=RESULTS - + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-today.pdf", threads: 2 resources: mem_mb=10000, diff --git a/scripts/plot_network.py b/scripts/plot_network.py index 93ca41847..d64826aeb 100644 --- a/scripts/plot_network.py +++ b/scripts/plot_network.py @@ -679,79 +679,6 @@ def plot_ch4_map(network): ) -def plot_map_without(network): - n = network.copy() - assign_location(n) - - # Drop non-electric buses so they don't clutter the plot - n.buses.drop(n.buses.index[n.buses.carrier != "AC"], inplace=True) - - fig, ax = plt.subplots(figsize=(7, 6), subplot_kw={"projection": ccrs.EqualEarth()}) - - # PDF has minimum width, so set these to zero - line_lower_threshold = 200.0 - line_upper_threshold = 1e4 - linewidth_factor = 3e3 - ac_color = "rosybrown" - dc_color = "darkseagreen" - - # hack because impossible to drop buses... - if "EU gas" in n.buses.index: - eu_location = snakemake.params.plotting.get( - "eu_node_location", dict(x=-5.5, y=46) - ) - n.buses.loc["EU gas", "x"] = eu_location["x"] - n.buses.loc["EU gas", "y"] = eu_location["y"] - - to_drop = n.links.index[(n.links.carrier != "DC") & (n.links.carrier != "B2B")] - n.links.drop(to_drop, inplace=True) - - if snakemake.wildcards["ll"] == "v1.0": - line_widths = n.lines.s_nom - link_widths = n.links.p_nom - else: - line_widths = n.lines.s_nom_min - link_widths = n.links.p_nom_min - - line_widths = line_widths.clip(line_lower_threshold, line_upper_threshold) - link_widths = link_widths.clip(line_lower_threshold, line_upper_threshold) - - line_widths = line_widths.replace(line_lower_threshold, 0) - link_widths = link_widths.replace(line_lower_threshold, 0) - - n.plot( - bus_colors="k", - line_colors=ac_color, - link_colors=dc_color, - line_widths=line_widths / linewidth_factor, - link_widths=link_widths / linewidth_factor, - ax=ax, - **map_opts, - ) - - handles = [] - labels = [] - - for s in (10, 5): - handles.append( - plt.Line2D([0], [0], color=ac_color, linewidth=s * 1e3 / linewidth_factor) - ) - labels.append(f"{s} GW") - l1_1 = ax.legend( - handles, - labels, - loc="upper left", - bbox_to_anchor=(0.05, 1.01), - frameon=False, - labelspacing=0.8, - handletextpad=1.5, - title="Today's transmission", - ) - ax.add_artist(l1_1) - - fig.savefig(snakemake.output.today, transparent=True, bbox_inches="tight") - - def plot_series(network, carrier="AC", name="test"): n = network.copy() assign_location(n) @@ -949,7 +876,6 @@ def plot_series(network, carrier="AC", name="test"): plot_h2_map(n, regions) plot_ch4_map(n) - plot_map_without(n) # plot_series(n, carrier="AC", name=suffix) # plot_series(n, carrier="heat", name=suffix) From ead390b4d20e6f809043547110d98dde5838e4a5 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Sun, 27 Aug 2023 17:10:41 +0200 Subject: [PATCH 173/293] plot_network: remove function plot_series() This function is superseded by plot_balance_timeseries rule. --- scripts/plot_network.py | 164 ---------------------------------------- 1 file changed, 164 deletions(-) diff --git a/scripts/plot_network.py b/scripts/plot_network.py index d64826aeb..198563042 100644 --- a/scripts/plot_network.py +++ b/scripts/plot_network.py @@ -679,167 +679,6 @@ def plot_ch4_map(network): ) -def plot_series(network, carrier="AC", name="test"): - n = network.copy() - assign_location(n) - assign_carriers(n) - - buses = n.buses.index[n.buses.carrier.str.contains(carrier)] - - supply = pd.DataFrame(index=n.snapshots) - for c in n.iterate_components(n.branch_components): - n_port = 4 if c.name == "Link" else 2 - for i in range(n_port): - supply = pd.concat( - ( - supply, - (-1) - * c.pnl["p" + str(i)] - .loc[:, c.df.index[c.df["bus" + str(i)].isin(buses)]] - .groupby(c.df.carrier, axis=1) - .sum(), - ), - axis=1, - ) - - for c in n.iterate_components(n.one_port_components): - comps = c.df.index[c.df.bus.isin(buses)] - supply = pd.concat( - ( - supply, - ((c.pnl["p"].loc[:, comps]).multiply(c.df.loc[comps, "sign"])) - .groupby(c.df.carrier, axis=1) - .sum(), - ), - axis=1, - ) - - supply = supply.groupby(rename_techs_tyndp, axis=1).sum() - - both = supply.columns[(supply < 0.0).any() & (supply > 0.0).any()] - - positive_supply = supply[both] - negative_supply = supply[both] - - positive_supply[positive_supply < 0.0] = 0.0 - negative_supply[negative_supply > 0.0] = 0.0 - - supply[both] = positive_supply - - suffix = " charging" - - negative_supply.columns = negative_supply.columns + suffix - - supply = pd.concat((supply, negative_supply), axis=1) - - # 14-21.2 for flaute - # 19-26.1 for flaute - - start = "2013-02-19" - stop = "2013-02-26" - - threshold = 10e3 - - to_drop = supply.columns[(abs(supply) < threshold).all()] - - if len(to_drop) != 0: - logger.info(f"Dropping {to_drop.tolist()} from supply") - supply.drop(columns=to_drop, inplace=True) - - supply.index.name = None - - supply = supply / 1e3 - - supply.rename( - columns={"electricity": "electric demand", "heat": "heat demand"}, inplace=True - ) - supply.columns = supply.columns.str.replace("residential ", "") - supply.columns = supply.columns.str.replace("services ", "") - supply.columns = supply.columns.str.replace("urban decentral ", "decentral ") - - preferred_order = pd.Index( - [ - "electric demand", - "transmission lines", - "hydroelectricity", - "hydro reservoir", - "run of river", - "pumped hydro storage", - "CHP", - "onshore wind", - "offshore wind", - "solar PV", - "solar thermal", - "building retrofitting", - "ground heat pump", - "air heat pump", - "resistive heater", - "OCGT", - "gas boiler", - "gas", - "natural gas", - "methanation", - "hydrogen storage", - "battery storage", - "hot water storage", - ] - ) - - new_columns = preferred_order.intersection(supply.columns).append( - supply.columns.difference(preferred_order) - ) - - supply = supply.groupby(supply.columns, axis=1).sum() - fig, ax = plt.subplots() - fig.set_size_inches((8, 5)) - - ( - supply.loc[start:stop, new_columns].plot( - ax=ax, - kind="area", - stacked=True, - linewidth=0.0, - color=[ - snakemake.params.plotting["tech_colors"][i.replace(suffix, "")] - for i in new_columns - ], - ) - ) - - handles, labels = ax.get_legend_handles_labels() - - handles.reverse() - labels.reverse() - - new_handles = [] - new_labels = [] - - for i, item in enumerate(labels): - if "charging" not in item: - new_handles.append(handles[i]) - new_labels.append(labels[i]) - - ax.legend(new_handles, new_labels, ncol=3, loc="upper left", frameon=False) - ax.set_xlim([start, stop]) - ax.set_ylim([-1300, 1900]) - ax.grid(True) - ax.set_ylabel("Power [GW]") - fig.tight_layout() - - fig.savefig( - "{}/{RDIR}maps/series-{}-{}-{}-{}-{}.pdf".format( - "results", - snakemake.params.RDIR, - snakemake.wildcards["ll"], - carrier, - start, - stop, - name, - ), - transparent=True, - ) - - if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake @@ -876,6 +715,3 @@ def plot_series(network, carrier="AC", name="test"): plot_h2_map(n, regions) plot_ch4_map(n) - - # plot_series(n, carrier="AC", name=suffix) - # plot_series(n, carrier="heat", name=suffix) From 2b6f169c1433c07c04eba5ab33d64ae227cfbed0 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Sun, 27 Aug 2023 17:41:57 +0200 Subject: [PATCH 174/293] plot_network: split into separate scripts for power, hydrogen, gas --- rules/plot.smk | 76 ++++ rules/postprocess.smk | 31 -- scripts/plot_gas_network.py | 253 +++++++++++ scripts/plot_hydrogen_network.py | 273 ++++++++++++ scripts/plot_network.py | 717 ------------------------------- scripts/plot_power_network.py | 273 ++++++++++++ 6 files changed, 875 insertions(+), 748 deletions(-) create mode 100644 scripts/plot_gas_network.py create mode 100644 scripts/plot_hydrogen_network.py delete mode 100644 scripts/plot_network.py create mode 100644 scripts/plot_power_network.py diff --git a/rules/plot.smk b/rules/plot.smk index 213cbdb88..d3b818b0e 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -183,3 +183,79 @@ rule plot_heatmap_timeseries_resources: directory(RESULTS + "graphics/heatmap_timeseries/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}") script: "../scripts/plot_heatmap_timeseries_resources.py" + + +rule plot_power_network: + params: + foresight=config["foresight"], + plotting=config["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", + rc="matplotlibrc", + output: + RESULTS + + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-costs-all_{planning_horizons}.pdf", + threads: 2 + resources: + mem_mb=10000, + benchmark: + ( + BENCHMARKS + + "plot_network/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" + ) + conda: + "../envs/environment.yaml" + script: + "../scripts/plot_power_network.py" + +rule plot_hydrogen_network: + params: + foresight=config["foresight"], + plotting=config["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", + rc="matplotlibrc", + output: + RESULTS + + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-h2_network_{planning_horizons}.pdf", + threads: 2 + resources: + mem_mb=10000, + benchmark: + ( + BENCHMARKS + + "plot_hydrogen_network/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" + ) + conda: + "../envs/environment.yaml" + script: + "../scripts/plot_hydrogen_network.py" + +rule plot_gas_network: + params: + foresight=config["foresight"], + plotting=config["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", + rc="matplotlibrc", + output: + RESULTS + + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-ch4_network_{planning_horizons}.pdf", + threads: 2 + resources: + mem_mb=10000, + benchmark: + ( + BENCHMARKS + + "plot_gas_network/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" + ) + conda: + "../envs/environment.yaml" + script: + "../scripts/plot_gas_network.py" diff --git a/rules/postprocess.smk b/rules/postprocess.smk index ec3387b24..21a71ab4f 100644 --- a/rules/postprocess.smk +++ b/rules/postprocess.smk @@ -8,32 +8,6 @@ localrules: copy_conda_env, -rule plot_network: - params: - foresight=config["foresight"], - plotting=config["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", - rc="matplotlibrc", - output: - map=RESULTS - + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-costs-all_{planning_horizons}.pdf", - threads: 2 - resources: - mem_mb=10000, - benchmark: - ( - BENCHMARKS - + "plot_network/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" - ) - conda: - "../envs/environment.yaml" - script: - "../scripts/plot_network.py" - - rule copy_config: params: RDIR=RDIR, @@ -66,11 +40,6 @@ rule make_summary: costs="data/costs_{}.csv".format(config["costs"]["year"]) if config["foresight"] == "overnight" else "data/costs_{}.csv".format(config["scenario"]["planning_horizons"][0]), - plots=expand( - RESULTS - + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-costs-all_{planning_horizons}.pdf", - **config["scenario"] - ), output: nodal_costs=RESULTS + "csvs/nodal_costs.csv", nodal_capacities=RESULTS + "csvs/nodal_capacities.csv", diff --git a/scripts/plot_gas_network.py b/scripts/plot_gas_network.py new file mode 100644 index 000000000..59cb766f0 --- /dev/null +++ b/scripts/plot_gas_network.py @@ -0,0 +1,253 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2020-2023 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT +""" +Creates map of optimised hydrogen networ, storage and selected other infrastructure. +""" + +import logging + +logger = logging.getLogger(__name__) + +import cartopy.crs as ccrs +import geopandas as gpd +import matplotlib.pyplot as plt +import pandas as pd +import pypsa +from _helpers import configure_logging +from pypsa.plot import add_legend_circles, add_legend_lines, add_legend_patches + +from plot_power_network import assign_location + +def plot_ch4_map(n): + # if "gas pipeline" not in n.links.carrier.unique(): + # return + + assign_location(n) + + bus_size_factor = 8e7 + linewidth_factor = 1e4 + # MW below which not drawn + line_lower_threshold = 1e3 + + # Drop non-electric buses so they don't clutter the plot + n.buses.drop(n.buses.index[n.buses.carrier != "AC"], inplace=True) + + fossil_gas_i = n.generators[n.generators.carrier == "gas"].index + fossil_gas = ( + n.generators_t.p.loc[:, fossil_gas_i] + .mul(n.snapshot_weightings.generators, axis=0) + .sum() + .groupby(n.generators.loc[fossil_gas_i, "bus"]) + .sum() + / bus_size_factor + ) + fossil_gas.rename(index=lambda x: x.replace(" gas", ""), inplace=True) + fossil_gas = fossil_gas.reindex(n.buses.index).fillna(0) + # make a fake MultiIndex so that area is correct for legend + fossil_gas.index = pd.MultiIndex.from_product([fossil_gas.index, ["fossil gas"]]) + + methanation_i = n.links[n.links.carrier.isin(["helmeth", "Sabatier"])].index + methanation = ( + abs( + n.links_t.p1.loc[:, methanation_i].mul( + n.snapshot_weightings.generators, axis=0 + ) + ) + .sum() + .groupby(n.links.loc[methanation_i, "bus1"]) + .sum() + / bus_size_factor + ) + methanation = ( + methanation.groupby(methanation.index) + .sum() + .rename(index=lambda x: x.replace(" gas", "")) + ) + # make a fake MultiIndex so that area is correct for legend + methanation.index = pd.MultiIndex.from_product([methanation.index, ["methanation"]]) + + biogas_i = n.stores[n.stores.carrier == "biogas"].index + biogas = ( + n.stores_t.p.loc[:, biogas_i] + .mul(n.snapshot_weightings.generators, axis=0) + .sum() + .groupby(n.stores.loc[biogas_i, "bus"]) + .sum() + / bus_size_factor + ) + biogas = ( + biogas.groupby(biogas.index) + .sum() + .rename(index=lambda x: x.replace(" biogas", "")) + ) + # make a fake MultiIndex so that area is correct for legend + biogas.index = pd.MultiIndex.from_product([biogas.index, ["biogas"]]) + + bus_sizes = pd.concat([fossil_gas, methanation, biogas]) + bus_sizes.sort_index(inplace=True) + + to_remove = n.links.index[~n.links.carrier.str.contains("gas pipeline")] + n.links.drop(to_remove, inplace=True) + + link_widths_rem = n.links.p_nom_opt / linewidth_factor + link_widths_rem[n.links.p_nom_opt < line_lower_threshold] = 0.0 + + link_widths_orig = n.links.p_nom / linewidth_factor + link_widths_orig[n.links.p_nom < line_lower_threshold] = 0.0 + + max_usage = n.links_t.p0.abs().max(axis=0) + link_widths_used = max_usage / linewidth_factor + link_widths_used[max_usage < line_lower_threshold] = 0.0 + + tech_colors = snakemake.params.plotting["tech_colors"] + + pipe_colors = { + "gas pipeline": "#f08080", + "gas pipeline new": "#c46868", + "gas pipeline (in 2020)": "lightgrey", + "gas pipeline (available)": "#e8d1d1", + } + + link_color_used = n.links.carrier.map(pipe_colors) + + n.links.bus0 = n.links.bus0.str.replace(" gas", "") + n.links.bus1 = n.links.bus1.str.replace(" gas", "") + + bus_colors = { + "fossil gas": tech_colors["fossil gas"], + "methanation": tech_colors["methanation"], + "biogas": "seagreen", + } + + fig, ax = plt.subplots(figsize=(7, 6), subplot_kw={"projection": ccrs.EqualEarth()}) + + n.plot( + bus_sizes=bus_sizes, + bus_colors=bus_colors, + link_colors=pipe_colors["gas pipeline (in 2020)"], + link_widths=link_widths_orig, + branch_components=["Link"], + ax=ax, + **map_opts, + ) + + n.plot( + ax=ax, + bus_sizes=0.0, + link_colors=pipe_colors["gas pipeline (available)"], + link_widths=link_widths_rem, + branch_components=["Link"], + color_geomap=False, + boundaries=map_opts["boundaries"], + ) + + n.plot( + ax=ax, + bus_sizes=0.0, + link_colors=link_color_used, + link_widths=link_widths_used, + branch_components=["Link"], + color_geomap=False, + boundaries=map_opts["boundaries"], + ) + + sizes = [100, 10] + labels = [f"{s} TWh" for s in sizes] + sizes = [s / bus_size_factor * 1e6 for s in sizes] + + legend_kw = dict( + loc="upper left", + bbox_to_anchor=(0, 1.03), + labelspacing=0.8, + frameon=False, + handletextpad=1, + title="gas sources", + ) + + add_legend_circles( + ax, + sizes, + labels, + srid=n.srid, + patch_kw=dict(facecolor="lightgrey"), + legend_kw=legend_kw, + ) + + sizes = [50, 10] + labels = [f"{s} GW" for s in sizes] + scale = 1e3 / linewidth_factor + sizes = [s * scale for s in sizes] + + legend_kw = dict( + loc="upper left", + bbox_to_anchor=(0.25, 1.03), + frameon=False, + labelspacing=0.8, + handletextpad=1, + title="gas pipeline", + ) + + add_legend_lines( + ax, + sizes, + labels, + patch_kw=dict(color="lightgrey"), + legend_kw=legend_kw, + ) + + colors = list(pipe_colors.values()) + list(bus_colors.values()) + labels = list(pipe_colors.keys()) + list(bus_colors.keys()) + + # legend on the side + # legend_kw = dict( + # bbox_to_anchor=(1.47, 1.04), + # frameon=False, + # ) + + legend_kw = dict( + loc="upper left", + bbox_to_anchor=(0, 1.24), + ncol=2, + frameon=False, + ) + + add_legend_patches( + ax, + colors, + labels, + legend_kw=legend_kw, + ) + + fig.savefig(snakemake.output[0]) + + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake( + "plot_gas_network", + simpl="", + opts="", + clusters="5", + ll="v1.5", + sector_opts="CO2L0-1H-T-H-B-I-A-solar+p3-dist1", + planning_horizons="2030", + ) + + configure_logging(snakemake) + + plt.style.use(snakemake.input.rc) + + n = pypsa.Network(snakemake.input.network) + + regions = gpd.read_file(snakemake.input.regions).set_index("name") + + map_opts = snakemake.params.plotting["map"] + + if map_opts["boundaries"] is None: + map_opts["boundaries"] = regions.total_bounds[[0, 2, 1, 3]] + [-1, 1, -1, 1] + + plot_ch4_map(n) diff --git a/scripts/plot_hydrogen_network.py b/scripts/plot_hydrogen_network.py new file mode 100644 index 000000000..9fb5b00d6 --- /dev/null +++ b/scripts/plot_hydrogen_network.py @@ -0,0 +1,273 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2020-2023 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT +""" +Creates map of optimised hydrogen networ, storage and selected other infrastructure. +""" + +import logging + +logger = logging.getLogger(__name__) + +import cartopy.crs as ccrs +import geopandas as gpd +import matplotlib.pyplot as plt +import pandas as pd +import pypsa +from _helpers import configure_logging +from pypsa.plot import add_legend_circles, add_legend_lines, add_legend_patches + +from plot_power_network import assign_location + +def group_pipes(df, drop_direction=False): + """ + Group pipes which connect same buses and return overall capacity. + """ + if drop_direction: + positive_order = df.bus0 < df.bus1 + df_p = df[positive_order] + swap_buses = {"bus0": "bus1", "bus1": "bus0"} + df_n = df[~positive_order].rename(columns=swap_buses) + df = pd.concat([df_p, df_n]) + + # there are pipes for each investment period rename to AC buses name for plotting + df.index = df.apply( + lambda x: f"H2 pipeline {x.bus0.replace(' H2', '')} -> {x.bus1.replace(' H2', '')}", + axis=1, + ) + # group pipe lines connecting the same buses and rename them for plotting + pipe_capacity = df.groupby(level=0).agg( + {"p_nom_opt": sum, "bus0": "first", "bus1": "first"} + ) + + return pipe_capacity + + +def plot_h2_map(n, regions): + # if "H2 pipeline" not in n.links.carrier.unique(): + # return + + assign_location(n) + + h2_storage = n.stores.query("carrier == 'H2'") + regions["H2"] = h2_storage.rename( + index=h2_storage.bus.map(n.buses.location) + ).e_nom_opt.div( + 1e6 + ) # TWh + regions["H2"] = regions["H2"].where(regions["H2"] > 0.1) + + bus_size_factor = 1e5 + linewidth_factor = 7e3 + # MW below which not drawn + line_lower_threshold = 750 + + # Drop non-electric buses so they don't clutter the plot + n.buses.drop(n.buses.index[n.buses.carrier != "AC"], inplace=True) + + carriers = ["H2 Electrolysis", "H2 Fuel Cell"] + + elec = n.links[n.links.carrier.isin(carriers)].index + + bus_sizes = ( + n.links.loc[elec, "p_nom_opt"].groupby([n.links["bus0"], n.links.carrier]).sum() + / bus_size_factor + ) + + # make a fake MultiIndex so that area is correct for legend + bus_sizes.rename(index=lambda x: x.replace(" H2", ""), level=0, inplace=True) + # drop all links which are not H2 pipelines + n.links.drop( + n.links.index[~n.links.carrier.str.contains("H2 pipeline")], inplace=True + ) + + h2_new = n.links[n.links.carrier == "H2 pipeline"] + h2_retro = n.links[n.links.carrier == "H2 pipeline retrofitted"] + + if snakemake.params.foresight == "myopic": + # sum capacitiy for pipelines from different investment periods + h2_new = group_pipes(h2_new) + + if not h2_retro.empty: + h2_retro = ( + group_pipes(h2_retro, drop_direction=True) + .reindex(h2_new.index) + .fillna(0) + ) + + if not h2_retro.empty: + positive_order = h2_retro.bus0 < h2_retro.bus1 + h2_retro_p = h2_retro[positive_order] + swap_buses = {"bus0": "bus1", "bus1": "bus0"} + h2_retro_n = h2_retro[~positive_order].rename(columns=swap_buses) + h2_retro = pd.concat([h2_retro_p, h2_retro_n]) + + h2_retro["index_orig"] = h2_retro.index + h2_retro.index = h2_retro.apply( + lambda x: f"H2 pipeline {x.bus0.replace(' H2', '')} -> {x.bus1.replace(' H2', '')}", + axis=1, + ) + + retro_w_new_i = h2_retro.index.intersection(h2_new.index) + h2_retro_w_new = h2_retro.loc[retro_w_new_i] + + 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 + + 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() + + else: + h2_total = h2_new.p_nom_opt + + 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() + 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 + + retro = n.links.p_nom_opt.where( + n.links.carrier == "H2 pipeline retrofitted", other=0.0 + ) + link_widths_retro = retro / linewidth_factor + link_widths_retro[n.links.p_nom_opt < line_lower_threshold] = 0.0 + + n.links.bus0 = n.links.bus0.str.replace(" H2", "") + n.links.bus1 = n.links.bus1.str.replace(" H2", "") + + proj = ccrs.EqualEarth() + regions = regions.to_crs(proj.proj4_init) + + fig, ax = plt.subplots(figsize=(7, 6), subplot_kw={"projection": proj}) + + color_h2_pipe = "#b3f3f4" + color_retrofit = "#499a9c" + + bus_colors = {"H2 Electrolysis": "#ff29d9", "H2 Fuel Cell": "#805394"} + + n.plot( + geomap=True, + bus_sizes=bus_sizes, + bus_colors=bus_colors, + link_colors=color_h2_pipe, + link_widths=link_widths_total, + branch_components=["Link"], + ax=ax, + **map_opts, + ) + + n.plot( + geomap=True, + bus_sizes=0, + link_colors=color_retrofit, + link_widths=link_widths_retro, + branch_components=["Link"], + ax=ax, + **map_opts, + ) + + regions.plot( + ax=ax, + column="H2", + cmap="Blues", + linewidths=0, + legend=True, + vmax=6, + vmin=0, + legend_kwds={ + "label": "Hydrogen Storage [TWh]", + "shrink": 0.7, + "extend": "max", + }, + ) + + sizes = [50, 10] + labels = [f"{s} GW" for s in sizes] + sizes = [s / bus_size_factor * 1e3 for s in sizes] + + legend_kw = dict( + loc="upper left", + bbox_to_anchor=(0, 1), + labelspacing=0.8, + handletextpad=0, + frameon=False, + ) + + add_legend_circles( + ax, + sizes, + labels, + srid=n.srid, + patch_kw=dict(facecolor="lightgrey"), + legend_kw=legend_kw, + ) + + sizes = [30, 10] + labels = [f"{s} GW" for s in sizes] + scale = 1e3 / linewidth_factor + sizes = [s * scale for s in sizes] + + legend_kw = dict( + loc="upper left", + bbox_to_anchor=(0.23, 1), + frameon=False, + labelspacing=0.8, + handletextpad=1, + ) + + add_legend_lines( + ax, + sizes, + labels, + patch_kw=dict(color="lightgrey"), + legend_kw=legend_kw, + ) + + colors = [bus_colors[c] for c in carriers] + [color_h2_pipe, color_retrofit] + labels = carriers + ["H2 pipeline (total)", "H2 pipeline (repurposed)"] + + legend_kw = dict( + loc="upper left", + bbox_to_anchor=(0, 1.13), + ncol=2, + frameon=False, + ) + + add_legend_patches(ax, colors, labels, legend_kw=legend_kw) + + ax.set_facecolor("white") + + fig.savefig(snakemake.output[0]) + + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake( + "plot_hydrogen_network", + simpl="", + opts="", + clusters="5", + ll="v1.5", + sector_opts="CO2L0-1H-T-H-B-I-A-solar+p3-dist1", + planning_horizons="2030", + ) + + configure_logging(snakemake) + + plt.style.use(snakemake.input.rc) + + n = pypsa.Network(snakemake.input.network) + + regions = gpd.read_file(snakemake.input.regions).set_index("name") + + map_opts = snakemake.params.plotting["map"] + + if map_opts["boundaries"] is None: + map_opts["boundaries"] = regions.total_bounds[[0, 2, 1, 3]] + [-1, 1, -1, 1] + + plot_h2_map(n, regions) diff --git a/scripts/plot_network.py b/scripts/plot_network.py deleted file mode 100644 index 198563042..000000000 --- a/scripts/plot_network.py +++ /dev/null @@ -1,717 +0,0 @@ -# -*- coding: utf-8 -*- -# SPDX-FileCopyrightText: : 2020-2023 The PyPSA-Eur Authors -# -# SPDX-License-Identifier: MIT -""" -Creates plots for optimised network topologies, including electricity, gas and -hydrogen networks, and regional generation, storage and conversion capacities -built. - -This rule plots a map of the network with technology capacities at the -nodes. -""" - -import logging - -logger = logging.getLogger(__name__) - -import cartopy.crs as ccrs -import geopandas as gpd -import matplotlib.pyplot as plt -import pandas as pd -import pypsa -from make_summary import assign_carriers -from plot_summary import preferred_order, rename_techs -from pypsa.plot import add_legend_circles, add_legend_lines, add_legend_patches - - -def rename_techs_tyndp(tech): - tech = rename_techs(tech) - if "heat pump" in tech or "resistive heater" in tech: - return "power-to-heat" - elif "external" in tech: - return "import hvdc-to-elec" - elif tech in ["H2 Electrolysis", "methanation", "helmeth", "H2 liquefaction"]: - return "power-to-gas" - elif tech == "H2": - return "H2 storage" - elif tech in ["NH3", "Haber-Bosch", "ammonia cracker", "ammonia store"]: - return "ammonia" - elif tech in ["OCGT", "CHP", "gas boiler", "H2 Fuel Cell"]: - return "gas-to-power/heat" - # elif "solar" in tech: - # return "solar" - elif tech in ["Fischer-Tropsch", "methanolisation"]: - return "power-to-liquid" - elif "offshore wind" in tech: - return "offshore wind" - elif "CC" in tech or "sequestration" in tech: - return "CCS" - else: - return tech - - -def assign_location(n): - for c in n.iterate_components(n.one_port_components | n.branch_components): - ifind = pd.Series(c.df.index.str.find(" ", start=4), c.df.index) - for i in ifind.value_counts().index: - # these have already been assigned defaults - if i == -1: - continue - names = ifind.index[ifind == i] - c.df.loc[names, "location"] = names.str[:i] - - -def plot_map( - network, - components=["links", "stores", "storage_units", "generators"], - bus_size_factor=1.7e10, - transmission=False, - with_legend=True, -): - tech_colors = snakemake.params.plotting["tech_colors"] - - n = network.copy() - assign_location(n) - # Drop non-electric buses so they don't clutter the plot - n.buses.drop(n.buses.index[n.buses.carrier != "AC"], inplace=True) - - costs = pd.DataFrame(index=n.buses.index) - - for comp in components: - df_c = getattr(n, comp) - - if df_c.empty: - continue - - df_c["nice_group"] = df_c.carrier.map(rename_techs_tyndp) - - attr = "e_nom_opt" if comp == "stores" else "p_nom_opt" - - costs_c = ( - (df_c.capital_cost * df_c[attr]) - .groupby([df_c.location, df_c.nice_group]) - .sum() - .unstack() - .fillna(0.0) - ) - costs = pd.concat([costs, costs_c], axis=1) - - logger.debug(f"{comp}, {costs}") - - costs = costs.groupby(costs.columns, axis=1).sum() - - costs.drop(list(costs.columns[(costs == 0.0).all()]), axis=1, inplace=True) - - new_columns = preferred_order.intersection(costs.columns).append( - costs.columns.difference(preferred_order) - ) - costs = costs[new_columns] - - for item in new_columns: - if item not in tech_colors: - logger.warning(f"{item} not in config/plotting/tech_colors") - - costs = costs.stack() # .sort_index() - - # hack because impossible to drop buses... - eu_location = snakemake.params.plotting.get("eu_node_location", dict(x=-5.5, y=46)) - n.buses.loc["EU gas", "x"] = eu_location["x"] - n.buses.loc["EU gas", "y"] = eu_location["y"] - - n.links.drop( - n.links.index[(n.links.carrier != "DC") & (n.links.carrier != "B2B")], - inplace=True, - ) - - # drop non-bus - to_drop = costs.index.levels[0].symmetric_difference(n.buses.index) - if len(to_drop) != 0: - logger.info(f"Dropping non-buses {to_drop.tolist()}") - costs.drop(to_drop, level=0, inplace=True, axis=0, errors="ignore") - - # make sure they are removed from index - costs.index = pd.MultiIndex.from_tuples(costs.index.values) - - threshold = 100e6 # 100 mEUR/a - carriers = costs.groupby(level=1).sum() - carriers = carriers.where(carriers > threshold).dropna() - carriers = list(carriers.index) - - # PDF has minimum width, so set these to zero - line_lower_threshold = 500.0 - line_upper_threshold = 1e4 - linewidth_factor = 4e3 - ac_color = "rosybrown" - dc_color = "darkseagreen" - - if snakemake.wildcards["ll"] == "v1.0": - # should be zero - line_widths = n.lines.s_nom_opt - n.lines.s_nom - link_widths = n.links.p_nom_opt - n.links.p_nom - title = "added grid" - - if transmission: - line_widths = n.lines.s_nom_opt - link_widths = n.links.p_nom_opt - linewidth_factor = 2e3 - line_lower_threshold = 0.0 - title = "current grid" - else: - line_widths = n.lines.s_nom_opt - n.lines.s_nom_min - link_widths = n.links.p_nom_opt - n.links.p_nom_min - title = "added grid" - - if transmission: - line_widths = n.lines.s_nom_opt - link_widths = n.links.p_nom_opt - title = "total grid" - - line_widths = line_widths.clip(line_lower_threshold, line_upper_threshold) - link_widths = link_widths.clip(line_lower_threshold, line_upper_threshold) - - line_widths = line_widths.replace(line_lower_threshold, 0) - link_widths = link_widths.replace(line_lower_threshold, 0) - - fig, ax = plt.subplots(subplot_kw={"projection": ccrs.EqualEarth()}) - fig.set_size_inches(7, 6) - - n.plot( - bus_sizes=costs / bus_size_factor, - bus_colors=tech_colors, - line_colors=ac_color, - link_colors=dc_color, - line_widths=line_widths / linewidth_factor, - link_widths=link_widths / linewidth_factor, - ax=ax, - **map_opts, - ) - - sizes = [20, 10, 5] - labels = [f"{s} bEUR/a" for s in sizes] - sizes = [s / bus_size_factor * 1e9 for s in sizes] - - legend_kw = dict( - loc="upper left", - bbox_to_anchor=(0.01, 1.06), - labelspacing=0.8, - frameon=False, - handletextpad=0, - title="system cost", - ) - - add_legend_circles( - ax, - sizes, - labels, - srid=n.srid, - patch_kw=dict(facecolor="lightgrey"), - legend_kw=legend_kw, - ) - - sizes = [10, 5] - labels = [f"{s} GW" for s in sizes] - scale = 1e3 / linewidth_factor - sizes = [s * scale for s in sizes] - - legend_kw = dict( - loc="upper left", - bbox_to_anchor=(0.27, 1.06), - frameon=False, - labelspacing=0.8, - handletextpad=1, - title=title, - ) - - add_legend_lines( - ax, sizes, labels, patch_kw=dict(color="lightgrey"), legend_kw=legend_kw - ) - - legend_kw = dict( - bbox_to_anchor=(1.52, 1.04), - frameon=False, - ) - - if with_legend: - colors = [tech_colors[c] for c in carriers] + [ac_color, dc_color] - labels = carriers + ["HVAC line", "HVDC link"] - - add_legend_patches( - ax, - colors, - labels, - legend_kw=legend_kw, - ) - - fig.savefig(snakemake.output.map, transparent=True, bbox_inches="tight") - - -def group_pipes(df, drop_direction=False): - """ - Group pipes which connect same buses and return overall capacity. - """ - if drop_direction: - positive_order = df.bus0 < df.bus1 - df_p = df[positive_order] - swap_buses = {"bus0": "bus1", "bus1": "bus0"} - df_n = df[~positive_order].rename(columns=swap_buses) - df = pd.concat([df_p, df_n]) - - # there are pipes for each investment period rename to AC buses name for plotting - df.index = df.apply( - lambda x: f"H2 pipeline {x.bus0.replace(' H2', '')} -> {x.bus1.replace(' H2', '')}", - axis=1, - ) - # group pipe lines connecting the same buses and rename them for plotting - pipe_capacity = df.groupby(level=0).agg( - {"p_nom_opt": sum, "bus0": "first", "bus1": "first"} - ) - - return pipe_capacity - - -def plot_h2_map(network, regions): - n = network.copy() - if "H2 pipeline" not in n.links.carrier.unique(): - return - - assign_location(n) - - h2_storage = n.stores.query("carrier == 'H2'") - regions["H2"] = h2_storage.rename( - index=h2_storage.bus.map(n.buses.location) - ).e_nom_opt.div( - 1e6 - ) # TWh - regions["H2"] = regions["H2"].where(regions["H2"] > 0.1) - - bus_size_factor = 1e5 - linewidth_factor = 7e3 - # MW below which not drawn - line_lower_threshold = 750 - - # Drop non-electric buses so they don't clutter the plot - n.buses.drop(n.buses.index[n.buses.carrier != "AC"], inplace=True) - - carriers = ["H2 Electrolysis", "H2 Fuel Cell"] - - elec = n.links[n.links.carrier.isin(carriers)].index - - bus_sizes = ( - n.links.loc[elec, "p_nom_opt"].groupby([n.links["bus0"], n.links.carrier]).sum() - / bus_size_factor - ) - - # make a fake MultiIndex so that area is correct for legend - bus_sizes.rename(index=lambda x: x.replace(" H2", ""), level=0, inplace=True) - # drop all links which are not H2 pipelines - n.links.drop( - n.links.index[~n.links.carrier.str.contains("H2 pipeline")], inplace=True - ) - - h2_new = n.links[n.links.carrier == "H2 pipeline"] - h2_retro = n.links[n.links.carrier == "H2 pipeline retrofitted"] - - if snakemake.params.foresight == "myopic": - # sum capacitiy for pipelines from different investment periods - h2_new = group_pipes(h2_new) - - if not h2_retro.empty: - h2_retro = ( - group_pipes(h2_retro, drop_direction=True) - .reindex(h2_new.index) - .fillna(0) - ) - - if not h2_retro.empty: - positive_order = h2_retro.bus0 < h2_retro.bus1 - h2_retro_p = h2_retro[positive_order] - swap_buses = {"bus0": "bus1", "bus1": "bus0"} - h2_retro_n = h2_retro[~positive_order].rename(columns=swap_buses) - h2_retro = pd.concat([h2_retro_p, h2_retro_n]) - - h2_retro["index_orig"] = h2_retro.index - h2_retro.index = h2_retro.apply( - lambda x: f"H2 pipeline {x.bus0.replace(' H2', '')} -> {x.bus1.replace(' H2', '')}", - axis=1, - ) - - retro_w_new_i = h2_retro.index.intersection(h2_new.index) - h2_retro_w_new = h2_retro.loc[retro_w_new_i] - - 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 - - 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() - - else: - h2_total = h2_new.p_nom_opt - - 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() - 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 - - retro = n.links.p_nom_opt.where( - n.links.carrier == "H2 pipeline retrofitted", other=0.0 - ) - link_widths_retro = retro / linewidth_factor - link_widths_retro[n.links.p_nom_opt < line_lower_threshold] = 0.0 - - n.links.bus0 = n.links.bus0.str.replace(" H2", "") - n.links.bus1 = n.links.bus1.str.replace(" H2", "") - - proj = ccrs.EqualEarth() - regions = regions.to_crs(proj.proj4_init) - - fig, ax = plt.subplots(figsize=(7, 6), subplot_kw={"projection": proj}) - - color_h2_pipe = "#b3f3f4" - color_retrofit = "#499a9c" - - bus_colors = {"H2 Electrolysis": "#ff29d9", "H2 Fuel Cell": "#805394"} - - n.plot( - geomap=True, - bus_sizes=bus_sizes, - bus_colors=bus_colors, - link_colors=color_h2_pipe, - link_widths=link_widths_total, - branch_components=["Link"], - ax=ax, - **map_opts, - ) - - n.plot( - geomap=True, - bus_sizes=0, - link_colors=color_retrofit, - link_widths=link_widths_retro, - branch_components=["Link"], - ax=ax, - **map_opts, - ) - - regions.plot( - ax=ax, - column="H2", - cmap="Blues", - linewidths=0, - legend=True, - vmax=6, - vmin=0, - legend_kwds={ - "label": "Hydrogen Storage [TWh]", - "shrink": 0.7, - "extend": "max", - }, - ) - - sizes = [50, 10] - labels = [f"{s} GW" for s in sizes] - sizes = [s / bus_size_factor * 1e3 for s in sizes] - - legend_kw = dict( - loc="upper left", - bbox_to_anchor=(0, 1), - labelspacing=0.8, - handletextpad=0, - frameon=False, - ) - - add_legend_circles( - ax, - sizes, - labels, - srid=n.srid, - patch_kw=dict(facecolor="lightgrey"), - legend_kw=legend_kw, - ) - - sizes = [30, 10] - labels = [f"{s} GW" for s in sizes] - scale = 1e3 / linewidth_factor - sizes = [s * scale for s in sizes] - - legend_kw = dict( - loc="upper left", - bbox_to_anchor=(0.23, 1), - frameon=False, - labelspacing=0.8, - handletextpad=1, - ) - - add_legend_lines( - ax, - sizes, - labels, - patch_kw=dict(color="lightgrey"), - legend_kw=legend_kw, - ) - - colors = [bus_colors[c] for c in carriers] + [color_h2_pipe, color_retrofit] - labels = carriers + ["H2 pipeline (total)", "H2 pipeline (repurposed)"] - - legend_kw = dict( - loc="upper left", - bbox_to_anchor=(0, 1.13), - ncol=2, - frameon=False, - ) - - add_legend_patches(ax, colors, labels, legend_kw=legend_kw) - - ax.set_facecolor("white") - - fig.savefig( - snakemake.output.map.replace("-costs-all", "-h2_network"), bbox_inches="tight" - ) - - -def plot_ch4_map(network): - n = network.copy() - - if "gas pipeline" not in n.links.carrier.unique(): - return - - assign_location(n) - - bus_size_factor = 8e7 - linewidth_factor = 1e4 - # MW below which not drawn - line_lower_threshold = 1e3 - - # Drop non-electric buses so they don't clutter the plot - n.buses.drop(n.buses.index[n.buses.carrier != "AC"], inplace=True) - - fossil_gas_i = n.generators[n.generators.carrier == "gas"].index - fossil_gas = ( - n.generators_t.p.loc[:, fossil_gas_i] - .mul(n.snapshot_weightings.generators, axis=0) - .sum() - .groupby(n.generators.loc[fossil_gas_i, "bus"]) - .sum() - / bus_size_factor - ) - fossil_gas.rename(index=lambda x: x.replace(" gas", ""), inplace=True) - fossil_gas = fossil_gas.reindex(n.buses.index).fillna(0) - # make a fake MultiIndex so that area is correct for legend - fossil_gas.index = pd.MultiIndex.from_product([fossil_gas.index, ["fossil gas"]]) - - methanation_i = n.links[n.links.carrier.isin(["helmeth", "Sabatier"])].index - methanation = ( - abs( - n.links_t.p1.loc[:, methanation_i].mul( - n.snapshot_weightings.generators, axis=0 - ) - ) - .sum() - .groupby(n.links.loc[methanation_i, "bus1"]) - .sum() - / bus_size_factor - ) - methanation = ( - methanation.groupby(methanation.index) - .sum() - .rename(index=lambda x: x.replace(" gas", "")) - ) - # make a fake MultiIndex so that area is correct for legend - methanation.index = pd.MultiIndex.from_product([methanation.index, ["methanation"]]) - - biogas_i = n.stores[n.stores.carrier == "biogas"].index - biogas = ( - n.stores_t.p.loc[:, biogas_i] - .mul(n.snapshot_weightings.generators, axis=0) - .sum() - .groupby(n.stores.loc[biogas_i, "bus"]) - .sum() - / bus_size_factor - ) - biogas = ( - biogas.groupby(biogas.index) - .sum() - .rename(index=lambda x: x.replace(" biogas", "")) - ) - # make a fake MultiIndex so that area is correct for legend - biogas.index = pd.MultiIndex.from_product([biogas.index, ["biogas"]]) - - bus_sizes = pd.concat([fossil_gas, methanation, biogas]) - bus_sizes.sort_index(inplace=True) - - to_remove = n.links.index[~n.links.carrier.str.contains("gas pipeline")] - n.links.drop(to_remove, inplace=True) - - link_widths_rem = n.links.p_nom_opt / linewidth_factor - link_widths_rem[n.links.p_nom_opt < line_lower_threshold] = 0.0 - - link_widths_orig = n.links.p_nom / linewidth_factor - link_widths_orig[n.links.p_nom < line_lower_threshold] = 0.0 - - max_usage = n.links_t.p0.abs().max(axis=0) - link_widths_used = max_usage / linewidth_factor - link_widths_used[max_usage < line_lower_threshold] = 0.0 - - tech_colors = snakemake.params.plotting["tech_colors"] - - pipe_colors = { - "gas pipeline": "#f08080", - "gas pipeline new": "#c46868", - "gas pipeline (in 2020)": "lightgrey", - "gas pipeline (available)": "#e8d1d1", - } - - link_color_used = n.links.carrier.map(pipe_colors) - - n.links.bus0 = n.links.bus0.str.replace(" gas", "") - n.links.bus1 = n.links.bus1.str.replace(" gas", "") - - bus_colors = { - "fossil gas": tech_colors["fossil gas"], - "methanation": tech_colors["methanation"], - "biogas": "seagreen", - } - - fig, ax = plt.subplots(figsize=(7, 6), subplot_kw={"projection": ccrs.EqualEarth()}) - - n.plot( - bus_sizes=bus_sizes, - bus_colors=bus_colors, - link_colors=pipe_colors["gas pipeline (in 2020)"], - link_widths=link_widths_orig, - branch_components=["Link"], - ax=ax, - **map_opts, - ) - - n.plot( - ax=ax, - bus_sizes=0.0, - link_colors=pipe_colors["gas pipeline (available)"], - link_widths=link_widths_rem, - branch_components=["Link"], - color_geomap=False, - boundaries=map_opts["boundaries"], - ) - - n.plot( - ax=ax, - bus_sizes=0.0, - link_colors=link_color_used, - link_widths=link_widths_used, - branch_components=["Link"], - color_geomap=False, - boundaries=map_opts["boundaries"], - ) - - sizes = [100, 10] - labels = [f"{s} TWh" for s in sizes] - sizes = [s / bus_size_factor * 1e6 for s in sizes] - - legend_kw = dict( - loc="upper left", - bbox_to_anchor=(0, 1.03), - labelspacing=0.8, - frameon=False, - handletextpad=1, - title="gas sources", - ) - - add_legend_circles( - ax, - sizes, - labels, - srid=n.srid, - patch_kw=dict(facecolor="lightgrey"), - legend_kw=legend_kw, - ) - - sizes = [50, 10] - labels = [f"{s} GW" for s in sizes] - scale = 1e3 / linewidth_factor - sizes = [s * scale for s in sizes] - - legend_kw = dict( - loc="upper left", - bbox_to_anchor=(0.25, 1.03), - frameon=False, - labelspacing=0.8, - handletextpad=1, - title="gas pipeline", - ) - - add_legend_lines( - ax, - sizes, - labels, - patch_kw=dict(color="lightgrey"), - legend_kw=legend_kw, - ) - - colors = list(pipe_colors.values()) + list(bus_colors.values()) - labels = list(pipe_colors.keys()) + list(bus_colors.keys()) - - # legend on the side - # legend_kw = dict( - # bbox_to_anchor=(1.47, 1.04), - # frameon=False, - # ) - - legend_kw = dict( - loc="upper left", - bbox_to_anchor=(0, 1.24), - ncol=2, - frameon=False, - ) - - add_legend_patches( - ax, - colors, - labels, - legend_kw=legend_kw, - ) - - fig.savefig( - snakemake.output.map.replace("-costs-all", "-ch4_network"), bbox_inches="tight" - ) - - -if __name__ == "__main__": - if "snakemake" not in globals(): - from _helpers import mock_snakemake - - snakemake = mock_snakemake( - "plot_network", - simpl="", - opts="", - clusters="5", - ll="v1.5", - sector_opts="CO2L0-1H-T-H-B-I-A-solar+p3-dist1", - planning_horizons="2030", - ) - - logging.basicConfig(level=snakemake.config["logging"]["level"]) - - plt.style.use(["ggplot", snakemake.input.rc]) - - n = pypsa.Network(snakemake.input.network) - - regions = gpd.read_file(snakemake.input.regions).set_index("name") - - map_opts = snakemake.params.plotting["map"] - - if map_opts["boundaries"] is None: - map_opts["boundaries"] = regions.total_bounds[[0, 2, 1, 3]] + [-1, 1, -1, 1] - - plot_map( - n, - components=["generators", "links", "stores", "storage_units"], - bus_size_factor=2e10, - transmission=False, - ) - - plot_h2_map(n, regions) - plot_ch4_map(n) diff --git a/scripts/plot_power_network.py b/scripts/plot_power_network.py new file mode 100644 index 000000000..4fcf57c6d --- /dev/null +++ b/scripts/plot_power_network.py @@ -0,0 +1,273 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2020-2023 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT +""" +Creates plots for optimised power network topologies and regional generation, +storage and conversion capacities built. +""" + +import logging + +logger = logging.getLogger(__name__) + +import cartopy.crs as ccrs +import geopandas as gpd +import matplotlib.pyplot as plt +import pandas as pd +import pypsa +from plot_summary import preferred_order, rename_techs +from _helpers import configure_logging +from pypsa.plot import add_legend_circles, add_legend_lines, add_legend_patches + + +def rename_techs_tyndp(tech): + tech = rename_techs(tech) + if "heat pump" in tech or "resistive heater" in tech: + return "power-to-heat" + elif "external" in tech: + return "import hvdc-to-elec" + elif tech in ["H2 Electrolysis", "methanation", "helmeth", "H2 liquefaction"]: + return "power-to-gas" + elif tech == "H2": + return "H2 storage" + elif tech in ["NH3", "Haber-Bosch", "ammonia cracker", "ammonia store"]: + return "ammonia" + elif tech in ["OCGT", "CHP", "gas boiler", "H2 Fuel Cell"]: + return "gas-to-power/heat" + # elif "solar" in tech: + # return "solar" + elif tech in ["Fischer-Tropsch", "methanolisation"]: + return "power-to-liquid" + elif "offshore wind" in tech: + return "offshore wind" + elif "CC" in tech or "sequestration" in tech: + return "CCS" + else: + return tech + + +def assign_location(n): + for c in n.iterate_components(n.one_port_components | n.branch_components): + ifind = pd.Series(c.df.index.str.find(" ", start=4), c.df.index) + for i in ifind.value_counts().index: + # these have already been assigned defaults + if i == -1: + continue + names = ifind.index[ifind == i] + c.df.loc[names, "location"] = names.str[:i] + + +def plot_map( + network, + components=["links", "stores", "storage_units", "generators"], + bus_size_factor=2e10, + transmission=False, + with_legend=True, +): + tech_colors = snakemake.params.plotting["tech_colors"] + + n = network.copy() + assign_location(n) + # Drop non-electric buses so they don't clutter the plot + n.buses.drop(n.buses.index[n.buses.carrier != "AC"], inplace=True) + + costs = pd.DataFrame(index=n.buses.index) + + for comp in components: + df_c = getattr(n, comp) + + if df_c.empty: + continue + + df_c["nice_group"] = df_c.carrier.map(rename_techs_tyndp) + + attr = "e_nom_opt" if comp == "stores" else "p_nom_opt" + + costs_c = ( + (df_c.capital_cost * df_c[attr]) + .groupby([df_c.location, df_c.nice_group]) + .sum() + .unstack() + .fillna(0.0) + ) + costs = pd.concat([costs, costs_c], axis=1) + + logger.debug(f"{comp}, {costs}") + + costs = costs.groupby(costs.columns, axis=1).sum() + + costs.drop(list(costs.columns[(costs == 0.0).all()]), axis=1, inplace=True) + + new_columns = preferred_order.intersection(costs.columns).append( + costs.columns.difference(preferred_order) + ) + costs = costs[new_columns] + + for item in new_columns: + if item not in tech_colors: + logger.warning(f"{item} not in config/plotting/tech_colors") + + costs = costs.stack() # .sort_index() + + # hack because impossible to drop buses... + eu_location = snakemake.params.plotting.get("eu_node_location", dict(x=-5.5, y=46)) + n.buses.loc["EU gas", "x"] = eu_location["x"] + n.buses.loc["EU gas", "y"] = eu_location["y"] + + n.links.drop( + n.links.index[(n.links.carrier != "DC") & (n.links.carrier != "B2B")], + inplace=True, + ) + + # drop non-bus + to_drop = costs.index.levels[0].symmetric_difference(n.buses.index) + if len(to_drop) != 0: + logger.info(f"Dropping non-buses {to_drop.tolist()}") + costs.drop(to_drop, level=0, inplace=True, axis=0, errors="ignore") + + # make sure they are removed from index + costs.index = pd.MultiIndex.from_tuples(costs.index.values) + + threshold = 100e6 # 100 mEUR/a + carriers = costs.groupby(level=1).sum() + carriers = carriers.where(carriers > threshold).dropna() + carriers = list(carriers.index) + + # PDF has minimum width, so set these to zero + line_lower_threshold = 500.0 + line_upper_threshold = 1e4 + linewidth_factor = 4e3 + ac_color = "rosybrown" + dc_color = "darkseagreen" + + if snakemake.wildcards["ll"] == "v1.0": + # should be zero + line_widths = n.lines.s_nom_opt - n.lines.s_nom + link_widths = n.links.p_nom_opt - n.links.p_nom + title = "added grid" + + if transmission: + line_widths = n.lines.s_nom_opt + link_widths = n.links.p_nom_opt + linewidth_factor = 2e3 + line_lower_threshold = 0.0 + title = "current grid" + else: + line_widths = n.lines.s_nom_opt - n.lines.s_nom_min + link_widths = n.links.p_nom_opt - n.links.p_nom_min + title = "added grid" + + if transmission: + line_widths = n.lines.s_nom_opt + link_widths = n.links.p_nom_opt + title = "total grid" + + line_widths = line_widths.clip(line_lower_threshold, line_upper_threshold) + link_widths = link_widths.clip(line_lower_threshold, line_upper_threshold) + + line_widths = line_widths.replace(line_lower_threshold, 0) + link_widths = link_widths.replace(line_lower_threshold, 0) + + fig, ax = plt.subplots(subplot_kw={"projection": ccrs.EqualEarth()}) + fig.set_size_inches(7, 6) + + n.plot( + bus_sizes=costs / bus_size_factor, + bus_colors=tech_colors, + line_colors=ac_color, + link_colors=dc_color, + line_widths=line_widths / linewidth_factor, + link_widths=link_widths / linewidth_factor, + ax=ax, + **map_opts, + ) + + sizes = [20, 10, 5] + labels = [f"{s} bEUR/a" for s in sizes] + sizes = [s / bus_size_factor * 1e9 for s in sizes] + + legend_kw = dict( + loc="upper left", + bbox_to_anchor=(0.01, 1.06), + labelspacing=0.8, + frameon=False, + handletextpad=0, + title="system cost", + ) + + add_legend_circles( + ax, + sizes, + labels, + srid=n.srid, + patch_kw=dict(facecolor="lightgrey"), + legend_kw=legend_kw, + ) + + sizes = [10, 5] + labels = [f"{s} GW" for s in sizes] + scale = 1e3 / linewidth_factor + sizes = [s * scale for s in sizes] + + legend_kw = dict( + loc="upper left", + bbox_to_anchor=(0.27, 1.06), + frameon=False, + labelspacing=0.8, + handletextpad=1, + title=title, + ) + + add_legend_lines( + ax, sizes, labels, patch_kw=dict(color="lightgrey"), legend_kw=legend_kw + ) + + legend_kw = dict( + bbox_to_anchor=(1.52, 1.04), + frameon=False, + ) + + if with_legend: + colors = [tech_colors[c] for c in carriers] + [ac_color, dc_color] + labels = carriers + ["HVAC line", "HVDC link"] + + add_legend_patches( + ax, + colors, + labels, + legend_kw=legend_kw, + ) + + fig.savefig(snakemake.output[0]) + + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake( + "plot_power_network", + simpl="", + opts="", + clusters="5", + ll="v1.5", + sector_opts="CO2L0-1H-T-H-B-I-A-solar+p3-dist1", + planning_horizons="2030", + ) + + configure_logging(snakemake) + + plt.style.use(snakemake.input.rc) + + n = pypsa.Network(snakemake.input.network) + + regions = gpd.read_file(snakemake.input.regions).set_index("name") + + map_opts = snakemake.params.plotting["map"] + + if map_opts["boundaries"] is None: + map_opts["boundaries"] = regions.total_bounds[[0, 2, 1, 3]] + [-1, 1, -1, 1] + + plot_map(n) + From ff99719d53c4499cdd4c8d93d890df11f3478212 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Sun, 27 Aug 2023 17:47:17 +0200 Subject: [PATCH 175/293] plot_network: multiext() for network plots --- rules/plot.smk | 12 ++++++------ scripts/plot_gas_network.py | 3 ++- scripts/plot_hydrogen_network.py | 3 ++- scripts/plot_power_network.py | 3 ++- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/rules/plot.smk b/rules/plot.smk index d3b818b0e..ceda22613 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -195,8 +195,8 @@ rule plot_power_network: regions=RESOURCES + "regions_onshore_elec_s{simpl}_{clusters}.geojson", rc="matplotlibrc", output: - RESULTS - + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-costs-all_{planning_horizons}.pdf", + multiext(RESULTS + + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-costs-all_{planning_horizons}", ".png", ".pdf") threads: 2 resources: mem_mb=10000, @@ -220,8 +220,8 @@ rule plot_hydrogen_network: regions=RESOURCES + "regions_onshore_elec_s{simpl}_{clusters}.geojson", rc="matplotlibrc", output: - RESULTS - + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-h2_network_{planning_horizons}.pdf", + multiext(RESULTS + + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-h2_network_{planning_horizons}", ".png", ".pdf") threads: 2 resources: mem_mb=10000, @@ -245,8 +245,8 @@ rule plot_gas_network: regions=RESOURCES + "regions_onshore_elec_s{simpl}_{clusters}.geojson", rc="matplotlibrc", output: - RESULTS - + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-ch4_network_{planning_horizons}.pdf", + multiext(RESULTS + + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-ch4_network_{planning_horizons}", ".png", ".pdf") threads: 2 resources: mem_mb=10000, diff --git a/scripts/plot_gas_network.py b/scripts/plot_gas_network.py index 59cb766f0..d73fd3b49 100644 --- a/scripts/plot_gas_network.py +++ b/scripts/plot_gas_network.py @@ -220,7 +220,8 @@ def plot_ch4_map(n): legend_kw=legend_kw, ) - fig.savefig(snakemake.output[0]) + for fn in snakemake.output[0]: + plt.savefig(fn) if __name__ == "__main__": diff --git a/scripts/plot_hydrogen_network.py b/scripts/plot_hydrogen_network.py index 9fb5b00d6..73f167b99 100644 --- a/scripts/plot_hydrogen_network.py +++ b/scripts/plot_hydrogen_network.py @@ -240,7 +240,8 @@ def plot_h2_map(n, regions): ax.set_facecolor("white") - fig.savefig(snakemake.output[0]) + for fn in snakemake.output[0]: + plt.savefig(fn) if __name__ == "__main__": diff --git a/scripts/plot_power_network.py b/scripts/plot_power_network.py index 4fcf57c6d..6b0818421 100644 --- a/scripts/plot_power_network.py +++ b/scripts/plot_power_network.py @@ -239,7 +239,8 @@ def plot_map( legend_kw=legend_kw, ) - fig.savefig(snakemake.output[0]) + for fn in snakemake.output[0]: + plt.savefig(fn) if __name__ == "__main__": From 67b9bd46bfceff82357254dcf14efd6d5d25a05a Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Sun, 27 Aug 2023 17:50:00 +0200 Subject: [PATCH 176/293] plot_network: allocate required params --- rules/plot.smk | 2 -- 1 file changed, 2 deletions(-) diff --git a/rules/plot.smk b/rules/plot.smk index ceda22613..0f96fd542 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -187,7 +187,6 @@ rule plot_heatmap_timeseries_resources: rule plot_power_network: params: - foresight=config["foresight"], plotting=config["plotting"], input: network=RESULTS @@ -237,7 +236,6 @@ rule plot_hydrogen_network: rule plot_gas_network: params: - foresight=config["foresight"], plotting=config["plotting"], input: network=RESULTS From a39cc41f915193265c7811a43f55c1cce5402796 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 28 Aug 2023 16:24:49 +0200 Subject: [PATCH 177/293] plot_import_options: initial version --- rules/plot.smk | 12 ++ scripts/plot_import_options.py | 220 +++++++++++++++++++++++++++++++++ 2 files changed, 232 insertions(+) create mode 100644 scripts/plot_import_options.py diff --git a/rules/plot.smk b/rules/plot.smk index 0f96fd542..0b43c80d2 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -257,3 +257,15 @@ rule plot_gas_network: "../envs/environment.yaml" script: "../scripts/plot_gas_network.py" + +rule plot_import_options: + input: + network=RESULTS + + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + regions=RESOURCES + "regions_onshore_elec_s{simpl}_{clusters}.geojson", + entrypoints=RESOURCES + "gas_input_locations_s{simpl}_{clusters}_simplified.csv", + rc="matplotlibrc", + output: + multiext(RESULTS + "graphics/import_options_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", ".png", ".pdf") + script: + "../scripts/plot_import_options.py" diff --git a/scripts/plot_import_options.py b/scripts/plot_import_options.py new file mode 100644 index 000000000..7899133a5 --- /dev/null +++ b/scripts/plot_import_options.py @@ -0,0 +1,220 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2020-2023 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT +""" +Creates map of import options. +""" + +import logging + +logger = logging.getLogger(__name__) + +import cartopy.crs as ccrs +import geopandas as gpd +import matplotlib.pyplot as plt +import pandas as pd +import pypsa +from _helpers import configure_logging +from plot_power_network import assign_location +from pypsa.plot import add_legend_circles, add_legend_patches + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake( + "plot_import_options", + simpl="", + opts="", + clusters="100", + ll="v1.5", + sector_opts="Co2L0-2190SEG-T-H-B-I-S-A-imp", + planning_horizons="2050", + configfiles="../../config/config.100n-seg.yaml", + ) + + configure_logging(snakemake) + + plt.style.use(snakemake.input.rc) + + tech_colors = snakemake.config["plotting"]["tech_colors"] + tech_colors["lng"] = "tomato" + tech_colors["pipeline"] = "orchid" + + crs = ccrs.EqualEarth() + + bus_size_factor = 7.5e4 + + n = pypsa.Network(snakemake.input.network) + assign_location(n) + + regions = ( + gpd.read_file(snakemake.input.regions).set_index("name").to_crs(crs.proj4_init) + ) + + inputs = pd.read_csv(snakemake.input.entrypoints, index_col=0)[ + ["lng", "pipeline"] + ].copy() + countries = ["DE", "GB", "BE", "FR", "EE", "LV", "LT", "FI"] + pattern = "|".join(countries) + inputs.loc[inputs.index.str.contains(pattern), "pipeline"] = 0.0 + inputs = inputs.stack() + + # TODO size external nodes according to wind and solar potential + + h2_cost = n.generators.filter(regex="import (pipeline-h2|shipping-lh2)", axis=0) + regions["marginal_cost"] = h2_cost.groupby( + h2_cost.bus.map(n.buses.location) + ).marginal_cost.min() + + # patch network + n.buses.drop(n.buses.index[n.buses.carrier != "AC"], inplace=True) + if "KZ" in n.buses.index: + n.buses.loc["KZ", "x"] = 52 + n.buses.loc["KZ", "y"] = 49 + if "CN-West" in n.buses.index: + n.buses.loc["CN-West", "x"] = 79 + n.buses.loc["CN-West", "y"] = 38 + for ct in n.buses.index.intersection({"MA", "DZ", "TN", "LY", "EG", "SA"}): + n.buses.loc[ct, "y"] += 2 + + link_colors = pd.Series( + n.links.index.map( + lambda x: "olivedrab" if "import hvdc-to-elec" in x else "tan" + ), + index=n.links.index, + ) + + exporters = snakemake.config["sector"]["import"]["endogenous_hvdc_import"][ + "exporters" + ] + techs = [ + "external solar-utility", + "external onwind", + "external battery", + "external H2", + ] + mi = pd.MultiIndex.from_product([exporters, techs]) + bus_sizes_plain = pd.concat( + [pd.Series(0.3, index=mi), inputs.div(bus_size_factor)], axis=0 + ) + + fig, ax = plt.subplots(subplot_kw={"projection": crs}, figsize=(8, 12)) + + n.plot( + ax=ax, + color_geomap={"ocean": "white", "land": "#efefef"}, + bus_sizes=bus_sizes_plain, + bus_colors=tech_colors, + line_colors="tan", + line_widths=0.75, + link_widths=0.75, + link_colors=link_colors, + boundaries=[-11, 48, 26.5, 70], + margin=0, + ) + + regions.plot( + ax=ax, + column="marginal_cost", + cmap="Blues_r", + linewidths=0, + vmin=50, + vmax=100, + legend=True, + legend_kwds={ + "label": r"H$_2$ import cost [€/MWh]", + "shrink": 0.35, + "pad": 0.015, + }, + ) + + names = { + "external onwind": "onshore wind", + "external solar-utility": "solar PV", + "external battery": "battery storage", + "external H2": "hydrogen storage", + } + labels = list(names.values()) + ["HVDC import link", "internal power line"] + colors = [tech_colors[c] for c in names.keys()] + ["olivedrab", "tan"] + + legend_kw = dict( + bbox_to_anchor=(1.51, 1.03), frameon=False, title="electricity imports" + ) + + add_legend_patches( + ax, + colors, + labels, + legend_kw=legend_kw, + ) + + legend_kw = dict( + bbox_to_anchor=(1.46, 0.66), frameon=False, title="H$_2$ and CH$_4$ imports" + ) + + names = { + "lng": "LNG terminal", + "pipeline": "pipeline entry", + } + labels = list(names.values()) + colors = [tech_colors[c] for c in names.keys()] + + add_legend_patches( + ax, + colors, + labels, + legend_kw=legend_kw, + ) + + legend_kw = dict( + bbox_to_anchor=(1.42, 0.47), + frameon=False, + title="import capacity", + ) + + add_legend_circles( + ax, + [100e3 / bus_size_factor], + ["100 GW"], + patch_kw=dict(facecolor="lightgrey"), + legend_kw=legend_kw, + ) + + cost_range = ( + pd.concat( + [ + c.df.filter(like="import", axis=0) + .groupby("carrier") + .marginal_cost.describe() + for c in n.iterate_components({"Link", "Generator"}) + ] + ) + .drop("import hvdc-to-elec") + .sort_values(by="min")[["min", "max"]] + .astype(int) + .T + ) + + translate = { + "import shipping-ftfuel": "€/MWh FT", + "import shipping-meoh": "€/MWh MeOh", + "import pipeline-h2": r"€/MWh H2$_{(g)}$", + "import shipping-lh2": r"€/MWh H2$_{(l)}$", + "import shipping-lch4": r"€/MWh CH4$_{(l)}$", + "import shipping-lnh3": r"€/MWh NH3$_{(l)}$", + "import shipping-steel": r"€/t steel", + } + text = "" + for carrier, values in cost_range.items(): + if abs(values["min"] - values["max"]) < 1: + value = str(values["min"]) + else: + value = str(values["min"]) + "-" + str(values["max"]) + text += value + " " + translate[carrier] + "\n" + + ax.text(1.2, 0.0, text, transform=ax.transAxes, linespacing=1.2) + + for fn in snakemake.output: + plt.savefig(fn, bbox_inches="tight") From ae8c8268f08894ed9424d6685ed804de13e4650f Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 28 Aug 2023 16:26:44 +0200 Subject: [PATCH 178/293] prepare_sector_network: set p_nom_max for imports --- scripts/prepare_sector_network.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index b0d007418..127006885 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -4014,7 +4014,9 @@ def add_import_options( p_nom_extendable=True, capital_cost=capital_cost, p_nom_min=import_nodes_tech.p_nom.clip(upper=upper_p_nom_max).values, - p_nom_max=import_nodes_tech.p_nom.mul(capacity_boost).clip(upper=upper_p_nom_max).values, + p_nom_max=import_nodes_tech.p_nom.mul(capacity_boost) + .clip(upper=upper_p_nom_max) + .values, ) else: @@ -4033,7 +4035,9 @@ def add_import_options( marginal_cost=import_nodes_tech.marginal_cost.values, p_nom_extendable=True, capital_cost=capital_cost, - p_nom_max=import_nodes_tech.p_nom.mul(capacity_boost).clip(upper=upper_p_nom_max).values, + p_nom_max=import_nodes_tech.p_nom.mul(capacity_boost) + .clip(upper=upper_p_nom_max) + .values, ) # need special handling for copperplated imports From 69b0239e882af450f29a411276e4b6c990096fbc Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 28 Aug 2023 16:27:20 +0200 Subject: [PATCH 179/293] do not consider entrypoints from RU or BY for pipeline imports --- 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 127006885..46d96220f 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3970,7 +3970,9 @@ def add_import_options( sel = ~import_nodes[tech].isna() if tech == "pipeline-h2": - forbidden_pipelines = ["DE", "BE", "FR", "GB"] + entrypoints_internal = ["DE", "BE", "FR", "GB"] + entrypoints_via_RU_BY = ["EE", "LT", "LV", "FI"] # maybe PL Yamal + forbidden_pipelines = entrypoints_internal + entrypoints_via_RU_BY sel &= ~import_nodes.index.str[:2].isin(forbidden_pipelines) import_nodes_tech = import_nodes.loc[sel, [tech]] From 87916b5abac0df0860085c8cf6cd46449077dad6 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 28 Aug 2023 16:28:19 +0200 Subject: [PATCH 180/293] import p_nom_max formatting --- 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 46d96220f..87c5512d0 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3688,7 +3688,11 @@ def add_endogenous_hvdc_import_options(n, cost_factor=1.0): importer="EUE" ) - p_nom_max = xr.open_dataset(snakemake.input.import_p_max_pu).p_nom_max.sel(importer="EUE").to_pandas() + p_nom_max = ( + xr.open_dataset(snakemake.input.import_p_max_pu) + .p_nom_max.sel(importer="EUE") + .to_pandas() + ) def _coordinates(ct): iso2 = ct.split("-")[0] @@ -3776,7 +3780,8 @@ def _coordinates(ct): capital_cost=(costs.at[tech, "fixed"] + grid_connection) * cost_factor, lifetime=costs.at[tech, "lifetime"], p_max_pu=p_max_pu_tech.reindex(columns=exporters_tech_i), - p_nom_max=p_nom_max[tech].reindex(index=exporters_tech_i) * cf["share_of_p_nom_max_available"], + p_nom_max=p_nom_max[tech].reindex(index=exporters_tech_i) + * cf["share_of_p_nom_max_available"], ) # hydrogen storage From 9c93a779ea532299bbc25903090dc891895842a4 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 28 Aug 2023 16:28:44 +0200 Subject: [PATCH 181/293] prepare release 0.8.1 --- scripts/prepare_sector_network.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 87c5512d0..074a9a21e 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3711,6 +3711,11 @@ def _coordinates(ct): import_links = {} a = regions.representative_point().to_crs(DISTANCE_CRS) + + # Prohibit routes through Russia or Belarus + forbidden_hvdc_importers = ["FI", "LV", "LT", "EE"] + a = a.loc[~a.index.str[:2].isin(forbidden_hvdc_importers)] + for ct in exporters.index: b = exporters.to_crs(DISTANCE_CRS).loc[ct].geometry d = a.distance(b) @@ -3718,6 +3723,9 @@ def _coordinates(ct): d.where(d < d.quantile(cf["distance_threshold"])).div(1e3).dropna() ) # km import_links = pd.concat(import_links) + import_links.loc[ + import_links.index.get_level_values(0).str.contains("KZ|CN") + ] *= 1.2 # proxy for detour through Caucasus # xlinks xlinks = {} From 088d40fe3a52386d95fb3c8b985a4fb490673612 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 28 Aug 2023 16:30:02 +0200 Subject: [PATCH 182/293] plot_power_network: also assign locations to buses --- scripts/plot_power_network.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/plot_power_network.py b/scripts/plot_power_network.py index 6b0818421..c1e01913e 100644 --- a/scripts/plot_power_network.py +++ b/scripts/plot_power_network.py @@ -16,8 +16,8 @@ import matplotlib.pyplot as plt import pandas as pd import pypsa -from plot_summary import preferred_order, rename_techs from _helpers import configure_logging +from plot_summary import preferred_order, rename_techs from pypsa.plot import add_legend_circles, add_legend_lines, add_legend_patches @@ -48,7 +48,9 @@ def rename_techs_tyndp(tech): def assign_location(n): - for c in n.iterate_components(n.one_port_components | n.branch_components): + for c in n.iterate_components( + n.one_port_components | n.branch_components | {"Bus"} + ): ifind = pd.Series(c.df.index.str.find(" ", start=4), c.df.index) for i in ifind.value_counts().index: # these have already been assigned defaults From 7bfed3b12d6c5f550272b254e4ca37da3643830d Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 28 Aug 2023 16:37:31 +0200 Subject: [PATCH 183/293] remove outdated localrule definition of copy_conda_env --- rules/postprocess.smk | 1 - 1 file changed, 1 deletion(-) diff --git a/rules/postprocess.smk b/rules/postprocess.smk index 21a71ab4f..be6a834b2 100644 --- a/rules/postprocess.smk +++ b/rules/postprocess.smk @@ -5,7 +5,6 @@ localrules: copy_config, - copy_conda_env, rule copy_config: From 4a8239979c5a921a148a854ec431077d98072e93 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Tue, 29 Aug 2023 16:32:01 +0200 Subject: [PATCH 184/293] lossy_bidirectional_links: set length of reversed lines to 0 to avoid double counting in line volume limit --- 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 074a9a21e..deb03edd2 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -4386,6 +4386,7 @@ def lossy_bidirectional_links(n, carrier, efficiencies={}): n.links.loc[carrier_i].copy().rename({"bus0": "bus1", "bus1": "bus0"}, axis=1) ) rev_links.capital_cost = 0 + rev_links.length = 0 rev_links["reversed"] = True rev_links.index = rev_links.index.map(lambda x: x + "-reversed") From 327d437d027306e84a437c9f237936e3bd580afb Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 13 Sep 2023 10:49:54 +0200 Subject: [PATCH 185/293] biomass_boiler: add pelletizing cost --- 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 deb03edd2..3d0008bdb 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2517,6 +2517,7 @@ def add_biomass(n, costs): efficiency=costs.at["biomass boiler", "efficiency"], capital_cost=costs.at["biomass boiler", "efficiency"] * costs.at["biomass boiler", "fixed"], + marginal_cost=costs.at["biomass boiler", "pelletizing cost"], lifetime=costs.at["biomass boiler", "lifetime"], ) From 4ca3d0f51e9a9ec333bba4a2b867b49bb24e5177 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 13 Sep 2023 11:28:57 +0200 Subject: [PATCH 186/293] biomass upgrading: include biogas plant costs and add option to capture CO2 --- config/config.default.yaml | 1 + scripts/prepare_sector_network.py | 28 ++++++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index ceb8caea7..01a27a052 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -523,6 +523,7 @@ sector: biomass_to_liquid: false biomass_to_methanol: false biosng: false + biomass_upgrading_cc: false endogenous_steel: false endogenous_hvc: false import: diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 3d0008bdb..55fce04eb 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -114,6 +114,7 @@ def define_spatial(nodes, options): spatial.gas.biogas = ["EU biogas"] spatial.gas.industry = ["gas for industry"] spatial.gas.biogas_to_gas = ["EU biogas to gas"] + spatial.gas.biogas_to_gas_cc = ["EU biogas to gas CC"] if options.get("co2_spatial", options["co2network"]): spatial.gas.industry_cc = nodes + " gas for industry CC" else: @@ -2386,12 +2387,35 @@ def add_biomass(n, costs): bus1=spatial.gas.nodes, bus2="co2 atmosphere", carrier="biogas to gas", - capital_cost=costs.loc["biogas upgrading", "fixed"], - marginal_cost=costs.loc["biogas upgrading", "VOM"], + capital_cost=costs.at["biogas", "fixed"] + costs.at["biogas upgrading", "fixed"], + marginal_cost=costs.at["biogas upgrading", "VOM"], + efficiency=1., efficiency2=-costs.at["gas", "CO2 intensity"], p_nom_extendable=True, ) + if options["biomass_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 + + n.madd( + "Link", + spatial.gas.biogas_to_gas_cc, + bus0=spatial.gas.biogas, + bus1=spatial.gas.nodes, + bus2="co2 stored", + bus3="co2 atmosphere", + carrier="biogas to gas CC", + capital_cost=costs.at["biogas CC", "fixed"] + costs.at["biogas upgrading", "fixed"] + costs.at["biomass CHP capture", "fixed"] * costs.at["biogas CC", "CO2 stored"], + marginal_cost=costs.at["biogas CC", "VOM"] + costs.at["biogas upgrading", "VOM"], + efficiency=1., + efficiency2=costs.at["biogas CC", "CO2 stored"] * costs.at["biogas CC", "capture rate"], + efficiency3=-costs.at["gas", "CO2 intensity"] - costs.at["biogas CC", "CO2 stored"] * costs.at["biogas CC", "capture rate"], + p_nom_extendable=True, + ) + if options["biomass_transport"]: # add biomass transport transport_costs = pd.read_csv( From 9629b9a4d24a79885161519db21f237a2ba3b15f Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 13 Sep 2023 12:24:25 +0200 Subject: [PATCH 187/293] add option for electrobiofuels --- config/config.default.yaml | 2 ++ scripts/prepare_sector_network.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/config/config.default.yaml b/config/config.default.yaml index 01a27a052..15b40aeee 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -524,6 +524,7 @@ sector: biomass_to_methanol: false biosng: false biomass_upgrading_cc: false + electrobiofuels: false endogenous_steel: false endogenous_hvc: false import: @@ -883,6 +884,7 @@ plotting: fossil gas: '#e05b09' natural gas: '#e05b09' biogas to gas: '#e36311' + electrobiofuels: '#b0b87b' CCGT: '#a85522' CCGT marginal: '#a85522' allam gas: '#B98F76' diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 55fce04eb..65417df6f 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2416,6 +2416,24 @@ def add_biomass(n, costs): p_nom_extendable=True, ) + if options['electrobiofuels']: + n.madd("Link", + spatial.nodes, + suffix=" electrobiofuels", + bus0=spatial.biomass.nodes, + bus1=spatial.oil.nodes, + bus3=spatial.h2.nodes, + bus3="co2 atmosphere", + carrier="electrobiofuels", + p_nom_extendable=True, + 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']), + capital_cost=costs.at['BtL', 'fixed'] * costs.at['electrobiofuels', 'efficiency-biomass'] + costs.at['BtL', 'C stored'] * costs.at['Fischer-Tropsch', 'fixed'] * costs.at['electrobiofuels', 'efficiency-hydrogen'], + marginal_cost=costs.at['BtL', 'VOM'] * costs.at['electrobiofuels', 'efficiency-biomass'] + costs.at['BtL', 'C stored'] * costs.at['Fischer-Tropsch', 'VOM'] * costs.at['electrobiofuels', 'efficiency-hydrogen'] + ) + if options["biomass_transport"]: # add biomass transport transport_costs = pd.read_csv( From d1ccc13e2dc28ca3951804f0b0c6442b04402f75 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 13 Sep 2023 14:49:59 +0200 Subject: [PATCH 188/293] separate configs for BtL and BtMeOH w/wo CC --- config/config.default.yaml | 3 +++ scripts/prepare_sector_network.py | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 15b40aeee..0c4aa3916 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -521,7 +521,9 @@ sector: conventional_generation: OCGT: gas biomass_to_liquid: false + biomass_to_liquid_cc: false biomass_to_methanol: false + biomass_to_methanol_cc: false biosng: false biomass_upgrading_cc: false electrobiofuels: false @@ -943,6 +945,7 @@ plotting: rural biomass boiler: '#a1a066' urban decentral biomass boiler: '#b0b87b' biomass to liquid: '#32CD32' + biomass to liquid CC: '#32CDaa' BioSNG: '#123456' # power transmission lines: '#6c9459' diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 65417df6f..247334318 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2584,7 +2584,7 @@ def add_biomass(n, costs): marginal_cost=costs.loc["BtL", "VOM"] / costs.at["BtL", "efficiency"], ) - # TODO: Update with energy penalty + if options.get("biomass_to_liquid_cc"): n.madd( "Link", spatial.biomass.nodes, @@ -2593,7 +2593,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"] @@ -2629,6 +2629,7 @@ def add_biomass(n, costs): / costs.at["biomass-to-methanol", "efficiency"], ) + if options.get("biomass_to_methanol_cc"): n.madd( "Link", spatial.biomass.nodes, From 7650ccd9bcc4492c4ed3593cfe42cddd0036035c Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 13 Sep 2023 14:50:50 +0200 Subject: [PATCH 189/293] clean up format of biogas upgrading with CC and electrobiofuels --- scripts/prepare_sector_network.py | 52 +++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 247334318..9e8893d15 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2387,15 +2387,15 @@ def add_biomass(n, costs): bus1=spatial.gas.nodes, bus2="co2 atmosphere", carrier="biogas to gas", - capital_cost=costs.at["biogas", "fixed"] + costs.at["biogas upgrading", "fixed"], + capital_cost=costs.at["biogas", "fixed"] + + costs.at["biogas upgrading", "fixed"], marginal_cost=costs.at["biogas upgrading", "VOM"], - efficiency=1., + efficiency=1.0, efficiency2=-costs.at["gas", "CO2 intensity"], p_nom_extendable=True, ) - if options["biomass_upgrading_cc"]: - + if options.get("biomass_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 @@ -2408,16 +2408,24 @@ def add_biomass(n, costs): bus2="co2 stored", bus3="co2 atmosphere", carrier="biogas to gas CC", - capital_cost=costs.at["biogas CC", "fixed"] + costs.at["biogas upgrading", "fixed"] + costs.at["biomass CHP capture", "fixed"] * costs.at["biogas CC", "CO2 stored"], - marginal_cost=costs.at["biogas CC", "VOM"] + costs.at["biogas upgrading", "VOM"], - efficiency=1., - efficiency2=costs.at["biogas CC", "CO2 stored"] * costs.at["biogas CC", "capture rate"], - efficiency3=-costs.at["gas", "CO2 intensity"] - costs.at["biogas CC", "CO2 stored"] * costs.at["biogas CC", "capture rate"], + capital_cost=costs.at["biogas CC", "fixed"] + + costs.at["biogas upgrading", "fixed"] + + costs.at["biomass CHP capture", "fixed"] + * costs.at["biogas CC", "CO2 stored"], + marginal_cost=costs.at["biogas CC", "VOM"] + + costs.at["biogas upgrading", "VOM"], + efficiency=1.0, + efficiency2=costs.at["biogas CC", "CO2 stored"] + * costs.at["biogas CC", "capture rate"], + efficiency3=-costs.at["gas", "CO2 intensity"] + - costs.at["biogas CC", "CO2 stored"] + * costs.at["biogas CC", "capture rate"], p_nom_extendable=True, ) - if options['electrobiofuels']: - n.madd("Link", + if options.get("electrobiofuels"): + n.madd( + "Link", spatial.nodes, suffix=" electrobiofuels", bus0=spatial.biomass.nodes, @@ -2426,12 +2434,22 @@ def add_biomass(n, costs): bus3="co2 atmosphere", carrier="electrobiofuels", p_nom_extendable=True, - 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']), - capital_cost=costs.at['BtL', 'fixed'] * costs.at['electrobiofuels', 'efficiency-biomass'] + costs.at['BtL', 'C stored'] * costs.at['Fischer-Tropsch', 'fixed'] * costs.at['electrobiofuels', 'efficiency-hydrogen'], - marginal_cost=costs.at['BtL', 'VOM'] * costs.at['electrobiofuels', 'efficiency-biomass'] + costs.at['BtL', 'C stored'] * costs.at['Fischer-Tropsch', 'VOM'] * costs.at['electrobiofuels', 'efficiency-hydrogen'] + 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"]), + capital_cost=costs.at["BtL", "fixed"] + * costs.at["electrobiofuels", "efficiency-biomass"] + + costs.at["BtL", "C stored"] + * costs.at["Fischer-Tropsch", "fixed"] + * costs.at["electrobiofuels", "efficiency-hydrogen"], + marginal_cost=costs.at["BtL", "VOM"] + * costs.at["electrobiofuels", "efficiency-biomass"] + + costs.at["BtL", "C stored"] + * costs.at["Fischer-Tropsch", "VOM"] + * costs.at["electrobiofuels", "efficiency-hydrogen"], ) if options["biomass_transport"]: From bc9ca8fe748468f5bfc7eb91c86f87d6d805ff35 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 13 Sep 2023 15:21:00 +0200 Subject: [PATCH 190/293] bugfix: electrobiofuels bus numbering --- 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 9e8893d15..55cb21e5a 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2430,7 +2430,7 @@ def add_biomass(n, costs): suffix=" electrobiofuels", bus0=spatial.biomass.nodes, bus1=spatial.oil.nodes, - bus3=spatial.h2.nodes, + bus2=spatial.h2.nodes, bus3="co2 atmosphere", carrier="electrobiofuels", p_nom_extendable=True, From b331f600fbb9301f1946b22f13fc18c760437de1 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 13 Sep 2023 15:34:46 +0200 Subject: [PATCH 191/293] bugfix: amend biogas_to_gas_cc to spatial namespace if gas is regionally resolved --- 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 55cb21e5a..26680b6e4 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -108,6 +108,7 @@ def define_spatial(nodes, options): spatial.gas.industry = nodes + " gas for industry" spatial.gas.industry_cc = nodes + " gas for industry CC" spatial.gas.biogas_to_gas = nodes + " biogas to gas" + spatial.gas.biogas_to_gas_cc = nodes + " biogas to gas CC" else: spatial.gas.nodes = ["EU gas"] spatial.gas.locations = ["EU"] From 43db2a6e091f617c25094dc8dc97fb2c2d7af56e Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 13 Sep 2023 15:59:00 +0200 Subject: [PATCH 192/293] separate configs for bioSNG w/wo CC --- config/config.default.yaml | 2 ++ scripts/prepare_sector_network.py | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 0c4aa3916..075862c10 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -525,6 +525,7 @@ sector: biomass_to_methanol: false biomass_to_methanol_cc: false biosng: false + biosng_cc: false biomass_upgrading_cc: false electrobiofuels: false endogenous_steel: false @@ -947,6 +948,7 @@ plotting: biomass to liquid: '#32CD32' biomass to liquid CC: '#32CDaa' BioSNG: '#123456' + BioSNG CC: '#123456' # power transmission lines: '#6c9459' transmission lines: '#6c9459' diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 26680b6e4..06df61fea 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2678,7 +2678,7 @@ def add_biomass(n, costs): if options["biosng"]: n.madd( "Link", - spatial.biomass.nodes, + spatial.gas.locations, # first carrier to be spatially resolved suffix=" solid biomass to gas", bus0=spatial.biomass.nodes, bus1=spatial.gas.nodes, @@ -2693,16 +2693,16 @@ def add_biomass(n, costs): marginal_cost=costs.at["BioSNG", "efficiency"] * costs.loc["BioSNG", "VOM"], ) - # TODO: Update with energy penalty for CC + if options.get("biosng_cc"): n.madd( "Link", - spatial.biomass.nodes, + spatial.gas.locations, # first carrier to be spatially resolved suffix=" solid biomass to gas CC", bus0=spatial.biomass.nodes, 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"] From 2b312c8645649ea61f117037d888b4e2f425255a Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 13 Sep 2023 16:04:53 +0200 Subject: [PATCH 193/293] bugfix: allow meoh-to-power ccgt_cc run without ccgt --- scripts/prepare_sector_network.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 06df61fea..5407605b9 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -725,6 +725,9 @@ 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_cc = ( capital_cost + costs.at["cement capture", "fixed"] From c1e98f611be4d26490f605013eb08107f1a35590 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 14 Sep 2023 10:28:18 +0200 Subject: [PATCH 194/293] bugfix: electrobiofuels ensure oil carrier exists --- 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 5407605b9..d00a28a5b 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2428,6 +2428,7 @@ def add_biomass(n, costs): ) if options.get("electrobiofuels"): + add_carrier_buses(n, "oil") n.madd( "Link", spatial.nodes, From e505e6a95346c2b465a2406a142330790be79b39 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 14 Sep 2023 10:28:51 +0200 Subject: [PATCH 195/293] bugfix: add unit conversion from tHBI to tSteel (inconsequential) --- 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 d00a28a5b..75c5f907f 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2808,6 +2808,7 @@ def add_industry(n, costs): capital_cost = ( costs.at["direct iron reduction furnace", "fixed"] + * costs.at["electric arc furnace", "hbi-input"] + costs.at["electric arc furnace", "fixed"] ) / electricity_input From baec3aa35e979caab46cd9acbb5855a0ce3e3be6 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 14 Sep 2023 12:52:04 +0200 Subject: [PATCH 196/293] bugfix: optimize European DRI+EAF capacities capped locationally to current capacities to allow for competition with imports --- scripts/prepare_sector_network.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 75c5f907f..885e41190 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2819,7 +2819,8 @@ def add_industry(n, costs): carrier=sector, capital_cost=capital_cost, marginal_cost=marginal_cost, - p_nom=p_nom, + p_nom_max=p_nom, + p_nom_extendable=True, p_min_pu=1, bus0=nodes, bus1="EU steel", From 8fb0a91c1e57ac7a2c54d611929a9950f252c722 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 14 Sep 2023 13:26:30 +0200 Subject: [PATCH 197/293] add tech_color biogas to gas CC --- config/config.default.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/config.default.yaml b/config/config.default.yaml index 075862c10..00e0f56e8 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -887,6 +887,7 @@ plotting: fossil gas: '#e05b09' natural gas: '#e05b09' biogas to gas: '#e36311' + biogas to gas CC: '#e0986c' electrobiofuels: '#b0b87b' CCGT: '#a85522' CCGT marginal: '#a85522' From 8e20f64d47a82c5f68537c314b782e17196f0766 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 15 Sep 2023 13:59:35 +0200 Subject: [PATCH 198/293] plot_capacity_factors: first version --- rules/plot.smk | 23 ++++ scripts/plot_choropleth_capacity_factors.py | 112 ++++++++++++++++++ ...plot_choropleth_capacity_factors_sector.py | 58 +++++++++ 3 files changed, 193 insertions(+) create mode 100644 scripts/plot_choropleth_capacity_factors.py create mode 100644 scripts/plot_choropleth_capacity_factors_sector.py diff --git a/rules/plot.smk b/rules/plot.smk index 0b43c80d2..11be807c0 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -149,6 +149,29 @@ rule plot_biomass_potentials: "../scripts/plot_biomass_potentials.py" +rule plot_choropleth_capacity_factors: + input: + 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", + rc="matplotlibrc", + output: + directory(RESOURCES + "graphics/capacity-factor-s{simpl}-{clusters}"), + script: + "../scripts/plot_choropleth_capacity_factors.py" + + +rule plot_choropleth_capacity_factors_sector: + input: + cop_soil=RESOURCES + "cop_soil_total_elec_s{simpl}_{clusters}.nc", + cop_air=RESOURCES + "cop_air_total_elec_s{simpl}_{clusters}.nc", + regions_onshore=RESOURCES + "regions_onshore_elec_s{simpl}_{clusters}.geojson", + rc="matplotlibrc", + output: + directory(RESOURCES + "graphics/capacity-factor-sector-s{simpl}-{clusters}"), + script: + "../scripts/plot_choropleth_capacity_factors_sector.py" + rule plot_balance_timeseries: input: diff --git a/scripts/plot_choropleth_capacity_factors.py b/scripts/plot_choropleth_capacity_factors.py new file mode 100644 index 000000000..c5cfb0170 --- /dev/null +++ b/scripts/plot_choropleth_capacity_factors.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2023- Fabian Neumann +# +# SPDX-License-Identifier: MIT +""" +Plot average capacity factor map. +""" + +import os + +import cartopy +import cartopy.crs as ccrs +import geopandas as gpd +import matplotlib.pyplot as plt +import pypsa + + +def plot_choropleth( + df, + geodf, + carrier, + cmap="Blues", + vmin=0, + vmax=100, + label="capacity factors [%]", + dir=".", + legend_kwds=None, + title="", + **kwargs, +): + if legend_kwds is None: + legend_kwds = { + "label": label, + "shrink": 0.7, + "extend": "max", + } + + proj = ccrs.EqualEarth() + geodf = geodf.to_crs(proj.proj4_init) + + fig, ax = plt.subplots(figsize=(7, 7), subplot_kw={"projection": proj}) + + geodf.plot( + ax=ax, + column=df[carrier].reindex(geodf.index), + cmap=cmap, + linewidths=0.5, + legend=True, + vmax=vmax, + vmin=vmin, + edgecolor="lightgray", + legend_kwds=legend_kwds, + **kwargs, + ) + + ax.set_title(title) + + ax.add_feature(cartopy.feature.COASTLINE.with_scale("50m"), linewidth=0.5, zorder=2) + ax.add_feature(cartopy.feature.BORDERS.with_scale("50m"), linewidth=0.5, zorder=2) + + ax.axis("off") + + # ensure path exists, since snakemake does not create path for directory outputs + # https://github.com/snakemake/snakemake/issues/774 + if not os.path.exists(dir): + os.makedirs(dir) + + carrier_fn = carrier.replace("-", "_").replace(" ", "_") + fn = f"map-{carrier_fn}" + plt.savefig(dir + "/" + fn + ".pdf", bbox_inches="tight") + plt.savefig(dir + "/" + fn + ".png", bbox_inches="tight") + plt.close() + + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake( + "plot_choropleth_capacity_factors", + clusters=128, + configfiles=["../../config/config.test.yaml"], + ) + + plt.style.use(snakemake.input.rc) + + n = pypsa.Network(snakemake.input.network) + + df = ( + n.generators_t.p_max_pu.mean() + .groupby([n.generators.carrier, n.generators.bus]) + .first() + .unstack(0) + .mul(100) + ) + + regions_onshore = gpd.read_file(snakemake.input.regions_onshore).set_index("name") + + regions_offshore = gpd.read_file(snakemake.input.regions_offshore).set_index("name") + + plot_choropleth(df, regions_onshore, "onwind", vmax=50, dir=snakemake.output[0]) + plot_choropleth( + df, regions_onshore, "solar", "Oranges", vmax=15, dir=snakemake.output[0] + ) + plot_choropleth(df, regions_onshore, "ror", "GnBu", dir=snakemake.output[0]) + + plot_choropleth( + df, regions_offshore, "offwind-dc", vmax=50, dir=snakemake.output[0] + ) + plot_choropleth( + df, regions_offshore, "offwind-ac", vmax=50, dir=snakemake.output[0] + ) diff --git a/scripts/plot_choropleth_capacity_factors_sector.py b/scripts/plot_choropleth_capacity_factors_sector.py new file mode 100644 index 000000000..713dd81e1 --- /dev/null +++ b/scripts/plot_choropleth_capacity_factors_sector.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2023- Fabian Neumann +# +# SPDX-License-Identifier: MIT +""" +Plot average capacity factor map for selected sector-coupling technologies. +""" + +import geopandas as gpd +import matplotlib.pyplot as plt +import pandas as pd +import xarray as xr +from plot_choropleth_capacity_factors import plot_choropleth + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake( + "plot_choropleth_capacity_factors_sector", + simpl="", + clusters=128, + configfiles=["../../config/config.test.yaml"], + ) + + plt.style.use(snakemake.input.rc) + + regions_onshore = gpd.read_file(snakemake.input.regions_onshore).set_index("name") + + df = pd.DataFrame() + + ds = xr.open_dataset(snakemake.input.cop_soil) + df["ground-sourced heat pump"] = ds["soil temperature"].mean("time").to_pandas() + + ds = xr.open_dataset(snakemake.input.cop_air) + df["air-sourced heat pump"] = ds["temperature"].mean("time").to_pandas() + + plot_choropleth( + df, + regions_onshore, + "ground-sourced heat pump", + cmap="Greens", + vmax=4, + vmin=2, + label="mean COP [-]", + dir=snakemake.output[0], + ) + + plot_choropleth( + df, + regions_onshore, + "air-sourced heat pump", + cmap="Greens", + vmax=4, + vmin=2, + label="mean COP [-]", + dir=snakemake.output[0], + ) From 417bcd7d550dfa07ff29b7e6e61c81568fb38fa8 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 15 Sep 2023 14:00:07 +0200 Subject: [PATCH 199/293] plot_choropleth_prices: first version --- rules/plot.smk | 14 ++++ scripts/plot_choropleth_prices.py | 130 ++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 scripts/plot_choropleth_prices.py diff --git a/rules/plot.smk b/rules/plot.smk index 11be807c0..57e0f42db 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -173,6 +173,20 @@ rule plot_choropleth_capacity_factors_sector: "../scripts/plot_choropleth_capacity_factors_sector.py" + +rule plot_choropleth_prices: + input: + network=RESULTS + + "postnetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + regions_onshore=RESOURCES + "regions_onshore_elec_s{simpl}_{clusters}.geojson", + regions_offshore=RESOURCES + "regions_offshore_elec_s{simpl}_{clusters}.geojson", + rc="matplotlibrc", + output: + market_prices=directory(RESULTS + "graphics/market_prices-s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}"), + market_values=directory(RESULTS + "graphics/market_values-s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}"), + script: + "../scripts/plot_choropleth_prices.py" + rule plot_balance_timeseries: input: network=RESULTS diff --git a/scripts/plot_choropleth_prices.py b/scripts/plot_choropleth_prices.py new file mode 100644 index 000000000..9c505e485 --- /dev/null +++ b/scripts/plot_choropleth_prices.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2023- Fabian Neumann +# +# SPDX-License-Identifier: MIT +""" +Plot average market prices and market values on map. +""" + +import geopandas as gpd +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +import pypsa +from plot_choropleth_capacity_factors import plot_choropleth + +ROUNDER = 20 + +MARKET_VALUES = [ + "offwind-ac", + "offwind-dc", + "onwind", + "solar", + "solar rooftop", + "ror", +] + +MARKET_PRICES = [ + "AC", + "gas", + "H2", + "low voltage", + "rural heat", + "urban decentral heat", + "urban central heat", +] + + +def get_market_prices(n): + return ( + n.buses_t.marginal_price.mean() + .groupby([n.buses.location, n.buses.carrier]) + .first() + .unstack() + .drop(["", "EU"]) # drop not spatially resolved + .dropna(how="all", axis=1) + ) + + +def get_market_values(n): + mv_generators = n.statistics.market_value( + comps={"Generator"}, nice_names=False, groupby=["bus", "carrier"] + ) + + mv_links = n.statistics.market_value( + comps={"Link"}, nice_names=False, groupby=["bus0", "carrier"] + ).rename_axis(index={"bus0": "bus"}) + + mv = pd.concat([mv_generators, mv_links]).droplevel(0) + + mv.index = pd.MultiIndex.from_arrays( + [ + mv.index.get_level_values("bus").map(n.buses.location), + mv.index.get_level_values("carrier"), + ] + ) + + mv = mv.unstack().drop(["", "EU"]).dropna(how="all", axis=1) + + return mv + + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake( + "plot_choropleth_prices", + simpl="", + clusters=128, + configfiles=["../../config/config.test.yaml"], + ) + + plt.style.use(snakemake.input.rc) + + regions_onshore = gpd.read_file(snakemake.input.regions_onshore).set_index("name") + regions_offshore = gpd.read_file(snakemake.input.regions_offshore).set_index("name") + + n = pypsa.Network(snakemake.input.network) + + lmps = get_market_prices(n) + + vmins = np.floor(lmps.div(ROUNDER)).min().mul(ROUNDER) + vmaxs = np.ceil(lmps.div(ROUNDER)).max().mul(ROUNDER) + + for carrier in lmps.columns.intersection(MARKET_PRICES): + plot_choropleth( + lmps, + regions_onshore, + carrier, + cmap="Spectral_r", + vmax=vmaxs[carrier], + vmin=vmins[carrier], + label="average market price [€/MWh]", + title=n.carriers.at[carrier, "nice_name"], + dir=snakemake.output.market_prices, + ) + + mv = get_market_values(n) + + vmins = np.floor(mv.div(ROUNDER)).min().mul(ROUNDER) + vmaxs = np.ceil(mv.div(ROUNDER)).max().mul(ROUNDER) + + for carrier in mv.columns.intersection(MARKET_VALUES): + regions = ( + regions_offshore + if carrier in ["offwind-ac", "offwind-dc"] + else regions_onshore + ) + + plot_choropleth( + mv, + regions, + carrier, + cmap="Spectral_r", + vmax=vmaxs[carrier], + vmin=vmins[carrier], + label="average market value [€/MWh]", + title=n.carriers.at[carrier, "nice_name"], + dir=snakemake.output.market_values, + ) From c13834530580573adedc1c949b24946bed329b4b Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 15 Sep 2023 14:00:27 +0200 Subject: [PATCH 200/293] plot_choropleth_capacities: first version --- rules/plot.smk | 12 +++ scripts/plot_choropleth_capacities.py | 139 ++++++++++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 scripts/plot_choropleth_capacities.py diff --git a/rules/plot.smk b/rules/plot.smk index 57e0f42db..c71b17094 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -173,6 +173,18 @@ rule plot_choropleth_capacity_factors_sector: "../scripts/plot_choropleth_capacity_factors_sector.py" +rule plot_choropleth_capacities: + input: + network=RESULTS + + "postnetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + regions_onshore=RESOURCES + "regions_onshore_elec_s{simpl}_{clusters}.geojson", + regions_offshore=RESOURCES + "regions_offshore_elec_s{simpl}_{clusters}.geojson", + rc="matplotlibrc", + output: + directory(RESULTS + "graphics/p_nom_opt-s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}"), + script: + "../scripts/plot_choropleth_capacities.py" + rule plot_choropleth_prices: input: diff --git a/scripts/plot_choropleth_capacities.py b/scripts/plot_choropleth_capacities.py new file mode 100644 index 000000000..019f6c391 --- /dev/null +++ b/scripts/plot_choropleth_capacities.py @@ -0,0 +1,139 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2023- Fabian Neumann +# +# SPDX-License-Identifier: MIT +""" +Plot optimised capacities on map. +""" + +import geopandas as gpd +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +import pypsa +from plot_choropleth_capacity_factors import plot_choropleth + +ROUNDER = 10 + +IGNORE_LINKS = [ + "DC", + "H2 pipeline", + "H2 pipeline retrofitted", + "gas pipeline", + "gas pipeline new", +] + +CAPACITIES = [ + "Fischer-Tropsch", + "H2 Electrolysis", + "H2 Fuel Cell", + "Haber-Bosch", + "OCGT", + "PHS", + "SMR", + "SMR CC", + "Sabatier", + "battery charger", + "battery discharger", + "electricity distribution grid", + "home battery charger", + "home battery discharger", + "hydro", + "methanolisation", + "offwind-ac", + "offwind-dc", + "onwind", + "ror", + "gas boiler", + "air heat pump", + "ground heat pump", + "resistive heater", + "solar", + "solar rooftop", + "gas CHP", + "gas CHP CC", +] + +PREFIXES_TO_REMOVE = [ + "residential ", + "services ", + "urban ", + "rural ", + "central ", + "decentral ", + "home ", +] + + +def remove_prefixes(s): + for prefix in PREFIXES_TO_REMOVE: + s = s.replace(prefix, "", 1) + return s + + +def get_optimal_capacity(n): + p_nom_opt_oneport = n.statistics.optimal_capacity( + comps={"Generator", "StorageUnit"}, groupby=["bus", "carrier"] + ) + p_nom_opt_links = n.statistics.optimal_capacity( + comps={"Link"}, groupby=["bus0", "carrier"] + ).rename_axis(index={"bus0": "bus"}) + p_nom_opt = ( + pd.concat([p_nom_opt_oneport, p_nom_opt_links]).droplevel(0).div(1e3) + ) # GW + + p_nom_opt.index = pd.MultiIndex.from_arrays( + [ + p_nom_opt.index.get_level_values("bus").map(n.buses.location), + p_nom_opt.index.get_level_values("carrier"), + ] + ) + + p_nom_opt = p_nom_opt.unstack().drop(["", "EU"]).dropna(how="all", axis=1) + + p_nom_opt = p_nom_opt.loc[:, ~p_nom_opt.columns.isin(IGNORE_LINKS)] + + return p_nom_opt.groupby(p_nom_opt.columns.map(remove_prefixes), axis=1).sum() + + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake( + "plot_choropleth_capacities", + simpl="", + clusters=128, + configfiles=["../../config/config.test.yaml"], + ) + + plt.style.use(snakemake.input.rc) + + regions_onshore = gpd.read_file(snakemake.input.regions_onshore).set_index("name") + regions_offshore = gpd.read_file(snakemake.input.regions_offshore).set_index("name") + + n = pypsa.Network(snakemake.input.network) + + p_nom_opt = get_optimal_capacity(n) + + vmins = np.floor(p_nom_opt.div(ROUNDER)).min().mul(ROUNDER) + vmaxs = np.ceil(p_nom_opt.div(ROUNDER)).max().mul(ROUNDER) + + for carrier in p_nom_opt.columns.intersection(CAPACITIES): + regions = ( + regions_offshore + if carrier in ["offwind-ac", "offwind-dc"] + else regions_onshore + ) + + plot_choropleth( + p_nom_opt, + regions, + carrier, + cmap="YlGnBu", + vmax=vmaxs[carrier], + vmin=vmins[carrier], + label=f"optimised capacity [GW]", + title=n.carriers.nice_name.get(carrier, carrier), + dir=snakemake.output[0], + ) From d1132f72f27aa422e72a9e6630d16599a61c4cc7 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 15 Sep 2023 15:02:41 +0200 Subject: [PATCH 201/293] plot_balance_timeseries: tidy code --- scripts/plot_balance_timeseries.py | 99 +++++++++++++++++------------- 1 file changed, 57 insertions(+), 42 deletions(-) diff --git a/scripts/plot_balance_timeseries.py b/scripts/plot_balance_timeseries.py index c27dfd694..7707e2d8c 100644 --- a/scripts/plot_balance_timeseries.py +++ b/scripts/plot_balance_timeseries.py @@ -7,20 +7,30 @@ """ import logging -import pypsa +import os +from multiprocessing import Pool + +import matplotlib.dates as mdates +import matplotlib.pyplot as plt import numpy as np import pandas as pd -import matplotlib.pyplot as plt -import matplotlib.dates as mdates -from multiprocessing import Pool +import pypsa logger = logging.getLogger(__name__) -THRESHOLD = 5 # GW +THRESHOLD = 5 # GW CARRIER_GROUPS = { "electricity": ["AC", "low voltage"], - "heat": ["urban central heat", "urban decentral heat", "rural heat", "residential urban decentral heat", "residential rural heat", "services urban decentral heat", "services rural heat"], + "heat": [ + "urban central heat", + "urban decentral heat", + "rural heat", + "residential urban decentral heat", + "residential rural heat", + "services urban decentral heat", + "services rural heat", + ], "hydrogen": "H2", "oil": "oil", "methanol": "methanol", @@ -31,8 +41,8 @@ "methane": "gas", } -def plot_stacked_area_steplike(ax, df, colors={}): +def plot_stacked_area_steplike(ax, df, colors={}): if isinstance(colors, pd.Series): colors = colors.to_dict() @@ -45,9 +55,9 @@ def plot_stacked_area_steplike(ax, df, colors={}): df_cum.index, previous_series, df_cum[col], - step='pre', + step="pre", linewidth=0, - color=colors.get(col, 'grey'), + color=colors.get(col, "grey"), label=col, ) previous_series = df_cum[col].values @@ -65,19 +75,19 @@ def plot_energy_balance_timeseries( threshold=0, dir="", ): - if time is not None: df = df.loc[time] - timespan = (df.index[-1] - df.index[0]) + timespan = df.index[-1] - df.index[0] long_time_frame = timespan > pd.Timedelta(weeks=5) techs_below_threshold = df.columns[df.abs().max() < threshold].tolist() + if techs_below_threshold: other = {tech: "other" for tech in techs_below_threshold} rename.update(other) - colors["other"] = 'grey' - + colors["other"] = "grey" + if rename: df = df.groupby(df.columns.map(lambda a: rename.get(a, a)), axis=1).sum() @@ -93,8 +103,8 @@ def plot_energy_balance_timeseries( df = df.loc[:, order] # fillna since plot_stacked_area_steplike cannot deal with NaNs - pos = df.where(df > 0).fillna(0.) - neg = df.where(df < 0).fillna(0.) + pos = df.where(df > 0).fillna(0.0) + neg = df.where(df < 0).fillna(0.0) fig, ax = plt.subplots(figsize=(10, 4), layout="constrained") @@ -106,29 +116,25 @@ def plot_energy_balance_timeseries( if not long_time_frame: # Set major ticks every Monday ax.xaxis.set_major_locator(mdates.WeekdayLocator(byweekday=mdates.MONDAY)) - ax.xaxis.set_major_formatter(mdates.DateFormatter('%e\n%b')) + ax.xaxis.set_major_formatter(mdates.DateFormatter("%e\n%b")) # Set minor ticks every day ax.xaxis.set_minor_locator(mdates.DayLocator()) - ax.xaxis.set_minor_formatter(mdates.DateFormatter('%e')) + ax.xaxis.set_minor_formatter(mdates.DateFormatter("%e")) else: # Set major ticks every first day of the month ax.xaxis.set_major_locator(mdates.MonthLocator(bymonthday=1)) - ax.xaxis.set_major_formatter(mdates.DateFormatter('%e\n%b')) + ax.xaxis.set_major_formatter(mdates.DateFormatter("%e\n%b")) # Set minor ticks every 15th of the month ax.xaxis.set_minor_locator(mdates.MonthLocator(bymonthday=15)) - ax.xaxis.set_minor_formatter(mdates.DateFormatter('%e')) + ax.xaxis.set_minor_formatter(mdates.DateFormatter("%e")) - ax.tick_params(axis='x', which='minor', labelcolor='grey') - ax.grid(axis='y') + ax.tick_params(axis="x", which="minor", labelcolor="grey") + ax.grid(axis="y") # half the labels because pos and neg create duplicate labels handles, labels = ax.get_legend_handles_labels() half = int(len(handles) / 2) - fig.legend( - handles=handles[:half], - labels=labels[:half], - loc="outside right upper" - ) + fig.legend(handles=handles[:half], labels=labels[:half], loc="outside right upper") ax.axhline(0, color="grey", linewidth=0.5) @@ -147,16 +153,16 @@ def plot_energy_balance_timeseries( ax2.set_xlim(ax.get_xlim()) ax2.set_xticks(df.index) ax2.grid(False) - ax2.tick_params(axis='x', length=2) + ax2.tick_params(axis="x", length=2) ax2.xaxis.set_tick_params(labelbottom=False) ax2.set_xticklabels([]) if resample is None: - resample = "native" + resample = f"native-{time}" fn = f"ts-balance-{ylabel.replace(' ', '_')}-{resample}" plt.savefig(dir + "/" + fn + ".pdf") plt.savefig(dir + "/" + fn + ".png") - + plt.close() if __name__ == "__main__": @@ -171,9 +177,15 @@ def plot_energy_balance_timeseries( opts="", sector_opts="Co2L0-2190SEG-T-H-B-I-S-A", planning_horizons=2050, - configfiles="../../config/config.100n-seg.yaml" + configfiles="../../config/config.100n-seg.yaml", ) + # ensure path exists, since snakemake does not create path for directory outputs + # https://github.com/snakemake/snakemake/issues/774 + dir = snakemake.output[0] + if not os.path.exists(dir): + os.makedirs(dir) + plt.style.use(["bmh", snakemake.input.rc]) n = pypsa.Network(snakemake.input.network) @@ -195,6 +207,16 @@ def process_group(group, carriers, balance, months, colors): mask = balance.index.get_level_values("bus_carrier").isin(carriers) df = balance[mask].groupby("carrier").sum().div(1e3).T + # daily resolution for each carrier + plot_energy_balance_timeseries( + df, + resample="D", + ylabel=group, + colors=colors, + threshold=THRESHOLD, + dir=dir, + ) + # native resolution for each month and carrier for month in months: plot_energy_balance_timeseries( @@ -203,19 +225,12 @@ def process_group(group, carriers, balance, months, colors): ylabel=group, colors=colors, threshold=THRESHOLD, - dir=snakemake.output[0] + dir=dir, ) - # daily resolution for each carrier - plot_energy_balance_timeseries( - df, - resample="D", - ylabel=group, - colors=colors, - threshold=THRESHOLD, - dir=snakemake.output[0] - ) - - args = [(group, carriers, balance, months, colors) for group, carriers in CARRIER_GROUPS.items()] + args = [ + (group, carriers, balance, months, colors) + for group, carriers in CARRIER_GROUPS.items() + ] with Pool(processes=snakemake.threads) as pool: pool.starmap(process_group, args) From 4d208ab98106a4080aa569cf813ea0d4dfd376bb Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 15 Sep 2023 15:04:16 +0200 Subject: [PATCH 202/293] plot_choropleth*: move into subdirectories --- rules/plot.smk | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rules/plot.smk b/rules/plot.smk index c71b17094..ae792094d 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -156,7 +156,7 @@ rule plot_choropleth_capacity_factors: regions_offshore=RESOURCES + "regions_offshore_elec_s{simpl}_{clusters}.geojson", rc="matplotlibrc", output: - directory(RESOURCES + "graphics/capacity-factor-s{simpl}-{clusters}"), + directory(RESOURCES + "graphics/capacity-factor/s{simpl}-{clusters}"), script: "../scripts/plot_choropleth_capacity_factors.py" @@ -168,7 +168,7 @@ rule plot_choropleth_capacity_factors_sector: regions_onshore=RESOURCES + "regions_onshore_elec_s{simpl}_{clusters}.geojson", rc="matplotlibrc", output: - directory(RESOURCES + "graphics/capacity-factor-sector-s{simpl}-{clusters}"), + directory(RESOURCES + "graphics/capacity-factor-sector/s{simpl}-{clusters}"), script: "../scripts/plot_choropleth_capacity_factors_sector.py" @@ -181,7 +181,7 @@ rule plot_choropleth_capacities: regions_offshore=RESOURCES + "regions_offshore_elec_s{simpl}_{clusters}.geojson", rc="matplotlibrc", output: - directory(RESULTS + "graphics/p_nom_opt-s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}"), + directory(RESULTS + "graphics/p_nom_opt/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}"), script: "../scripts/plot_choropleth_capacities.py" @@ -194,8 +194,8 @@ rule plot_choropleth_prices: regions_offshore=RESOURCES + "regions_offshore_elec_s{simpl}_{clusters}.geojson", rc="matplotlibrc", output: - market_prices=directory(RESULTS + "graphics/market_prices-s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}"), - market_values=directory(RESULTS + "graphics/market_values-s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}"), + market_prices=directory(RESULTS + "graphics/market_prices/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}"), + market_values=directory(RESULTS + "graphics/market_values/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}"), script: "../scripts/plot_choropleth_prices.py" From 32c0c748726560dfb88eeabe524695afe173d1b8 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 15 Sep 2023 15:09:34 +0200 Subject: [PATCH 203/293] plot_heatmap: ensure directory exists --- scripts/plot_heatmap_timeseries.py | 88 +++++++++++--------- scripts/plot_heatmap_timeseries_resources.py | 23 +++-- 2 files changed, 64 insertions(+), 47 deletions(-) diff --git a/scripts/plot_heatmap_timeseries.py b/scripts/plot_heatmap_timeseries.py index 14c9066a7..ec11c2c4c 100644 --- a/scripts/plot_heatmap_timeseries.py +++ b/scripts/plot_heatmap_timeseries.py @@ -7,16 +7,18 @@ """ import logging -import pypsa +import os +from multiprocessing import Pool + +import matplotlib.pyplot as plt import pandas as pd +import pypsa import seaborn as sns -import matplotlib.pyplot as plt -from multiprocessing import Pool logger = logging.getLogger(__name__) -THRESHOLD_MW = 1e3 # 1 GW -THRESHOLD_MWh = 100e3 # 100 GWh +THRESHOLD_MW = 1e3 # 1 GW +THRESHOLD_MWh = 100e3 # 100 GWh MARGINAL_PRICES = [ "AC", @@ -28,21 +30,21 @@ "oil", "rural heat", "urban central heat", - "urban decentral heat" + "urban decentral heat", ] SKIP_UTILISATION_RATES = [ - 'DC', - 'H2 pipeline', - 'electricity distribution grid', - 'gas for industry', - 'gas for industry CC', - 'gas pipeline', - 'gas pipeline new', - 'process emissions', - 'process emissions CC', - 'solid biomass for industry', - 'solid biomass for industry CC', + "DC", + "H2 pipeline", + "electricity distribution grid", + "gas for industry", + "gas for industry CC", + "gas pipeline", + "gas pipeline new", + "process emissions", + "process emissions CC", + "solid biomass for industry", + "solid biomass for industry CC", ] @@ -82,11 +84,11 @@ def plot_heatmap( ) plt.ylabel("hour of the day") plt.xlabel("day of the year") - plt.title(title, fontsize='large') + plt.title(title, fontsize="large") - ax.grid(axis='y') + ax.grid(axis="y") - hours = list(range(0,24)) + hours = list(range(0, 24)) ax.set_yticks(hours[0::2]) ax.set_yticklabels(df.index[0::2], rotation=0) ax.set_yticks(hours, minor=True) @@ -94,9 +96,16 @@ def plot_heatmap( major_ticks = [i for i, date in enumerate(df.columns) if date.day == 1] minor_ticks = [i for i, date in enumerate(df.columns) if date.day == 15] ax.set_xticks(major_ticks) - ax.set_xticklabels([df.columns[i].strftime('%e\n%b') for i in major_ticks], rotation=0) + ax.set_xticklabels( + [df.columns[i].strftime("%e\n%b") for i in major_ticks], rotation=0 + ) ax.set_xticks(minor_ticks, minor=True) - ax.set_xticklabels([df.columns[i].strftime('%e') for i in minor_ticks], rotation=0, minor=True, color='grey') + ax.set_xticklabels( + [df.columns[i].strftime("%e") for i in minor_ticks], + rotation=0, + minor=True, + color="grey", + ) cb = ax.collections[0].colorbar cb.outline.set_linewidth(0.75) @@ -123,22 +132,27 @@ def plot_heatmap( opts="", sector_opts="Co2L0-2190SEG-T-H-B-I-S-A", planning_horizons=2050, - configfiles="../../config/config.100n-seg.yaml" + configfiles="../../config/config.100n-seg.yaml", ) plt.style.use(["bmh", snakemake.input.rc]) + # ensure path exists, since snakemake does not create path for directory outputs + # https://github.com/snakemake/snakemake/issues/774 + dir = snakemake.output[0] + if not os.path.exists(dir): + os.makedirs(dir) n = pypsa.Network(snakemake.input.network) - dir = snakemake.output[0] snapshots = snakemake.config["snapshots"] carriers = n.carriers p_nom_opt = n.generators.groupby("carrier").p_nom_opt.sum() data = ( n.generators_t.p.groupby(n.generators.carrier, axis=1).sum() - / p_nom_opt * 100 # % + / p_nom_opt + * 100 # % ) data = data.loc[:, p_nom_opt > THRESHOLD_MW] @@ -153,7 +167,7 @@ def process_generator_utilisation(carrier, s): title=title, vmin=0, vmax=90, - cbar_kws=dict(extend='max'), + cbar_kws=dict(extend="max"), tag="utilisation_rate", dir=dir, ) @@ -167,13 +181,17 @@ def process_generator_utilisation(carrier, s): def process_marginal_prices(carrier, s): df = unstack_day_hour(s, snapshots) - label = "marginal price [€/t]" if "co2" in carrier.lower() else "marginal price [€/MWh]" + label = ( + "marginal price [€/t]" + if "co2" in carrier.lower() + else "marginal price [€/MWh]" + ) plot_heatmap( df, cmap="Spectral_r", label=label, title=carrier, - cbar_kws=dict(extend='both'), + cbar_kws=dict(extend="both"), tag="marginal_price", dir=dir, ) @@ -183,10 +201,7 @@ def process_marginal_prices(carrier, s): # SOCs e_nom_opt = n.stores.groupby("carrier").e_nom_opt.sum() - data = ( - n.stores_t.e.groupby(n.stores.carrier, axis=1).sum() - / e_nom_opt * 100 - ) + data = n.stores_t.e.groupby(n.stores.carrier, axis=1).sum() / e_nom_opt * 100 data = data.loc[:, e_nom_opt > THRESHOLD_MWh] def process_socs(carrier, s): @@ -200,7 +215,7 @@ def process_socs(carrier, s): vmax=90, label=label, title=title, - cbar_kws=dict(extend='max'), + cbar_kws=dict(extend="max"), tag="soc", dir=dir, ) @@ -210,12 +225,9 @@ def process_socs(carrier, s): # link utilisation rates p_nom_opt = n.links.groupby("carrier").p_nom_opt.sum() - data = ( - n.links_t.p0.groupby(n.links.carrier, axis=1).sum() - / p_nom_opt * 100 - ) + data = n.links_t.p0.groupby(n.links.carrier, axis=1).sum() / p_nom_opt * 100 data = data[data.columns.difference(SKIP_UTILISATION_RATES)] - data = data.loc[:,p_nom_opt > THRESHOLD_MW] + data = data.loc[:, p_nom_opt > THRESHOLD_MW] def process_link_utilisation(carrier, s): df = unstack_day_hour(s, snapshots) diff --git a/scripts/plot_heatmap_timeseries_resources.py b/scripts/plot_heatmap_timeseries_resources.py index fada1cb36..a18ec5039 100644 --- a/scripts/plot_heatmap_timeseries_resources.py +++ b/scripts/plot_heatmap_timeseries_resources.py @@ -7,14 +7,15 @@ """ import logging -import pypsa -import matplotlib.pyplot as plt +import os from multiprocessing import Pool -logger = logging.getLogger(__name__) +import matplotlib.pyplot as plt +import pypsa -from plot_heatmap_timeseries import unstack_day_hour, plot_heatmap +logger = logging.getLogger(__name__) +from plot_heatmap_timeseries import plot_heatmap, unstack_day_hour if __name__ == "__main__": if "snakemake" not in globals(): @@ -28,14 +29,19 @@ opts="", sector_opts="Co2L0-2190SEG-T-H-B-I-S-A", planning_horizons=2050, - configfiles="../../config/config.100n-seg.yaml" + configfiles="../../config/config.100n-seg.yaml", ) plt.style.use(["bmh", snakemake.input.rc]) + # ensure path exists, since snakemake does not create path for directory outputs + # https://github.com/snakemake/snakemake/issues/774 + dir = snakemake.output[0] + if not os.path.exists(dir): + os.makedirs(dir) + n = pypsa.Network(snakemake.input.network) - dir = snakemake.output[0] snapshots = snakemake.config["snapshots"] carriers = n.carriers @@ -59,7 +65,7 @@ def process_capacity_factors(carrier, s): with Pool(processes=snakemake.threads) as pool: pool.starmap(process_capacity_factors, data.items()) - + # heat pump COPs data = n.links_t.efficiency.groupby(n.links.carrier, axis=1).mean() @@ -74,7 +80,7 @@ def process_cops(carrier, s): vmax=4, label=label, title=title, - cbar_kws=dict(extend='both'), + cbar_kws=dict(extend="both"), tag="cop", dir=dir, ) @@ -102,4 +108,3 @@ def process_availabilities(carrier, s): with Pool(processes=snakemake.threads) as pool: pool.starmap(process_availabilities, data.items()) - From 752d66fcc1cc4401a8faabc370e92ae7bb1efd09 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 15 Sep 2023 15:44:50 +0200 Subject: [PATCH 204/293] plot_choropleth_potential_used: first version --- rules/plot.smk | 14 +++++ scripts/plot_choropleth_potential_used.py | 77 +++++++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 scripts/plot_choropleth_potential_used.py diff --git a/rules/plot.smk b/rules/plot.smk index ae792094d..3fbee1f16 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -199,6 +199,20 @@ rule plot_choropleth_prices: script: "../scripts/plot_choropleth_prices.py" + +rule plot_choropleth_potential_used: + input: + network=RESULTS + + "postnetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + regions_onshore=RESOURCES + "regions_onshore_elec_s{simpl}_{clusters}.geojson", + regions_offshore=RESOURCES + "regions_offshore_elec_s{simpl}_{clusters}.geojson", + rc="matplotlibrc", + output: + directory(RESULTS + "graphics/potential_used/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}"), + script: + "../scripts/plot_choropleth_potential_used.py" + + rule plot_balance_timeseries: input: network=RESULTS diff --git a/scripts/plot_choropleth_potential_used.py b/scripts/plot_choropleth_potential_used.py new file mode 100644 index 000000000..f98ba39c9 --- /dev/null +++ b/scripts/plot_choropleth_potential_used.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2023- Fabian Neumann +# +# SPDX-License-Identifier: MIT +""" +Plot share of potential used on map. +""" + +import geopandas as gpd +import matplotlib.pyplot as plt +import pypsa +from plot_choropleth_capacity_factors import plot_choropleth + +POTENTIAL = [ + "offwind-ac", + "offwind-dc", + "onwind", + "solar", + "solar rooftop", +] + + +def get_potential_used(n): + return ( + n.generators.eval("p_nom_opt/p_nom_max*100") + .groupby([n.generators.bus.map(n.buses.location), n.generators.carrier]) + .sum() + .unstack() + .drop("EU") + ) + + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake( + "plot_choropleth_potential_used", + simpl="", + clusters=128, + configfiles=["../../config/config.test.yaml"], + ) + + plt.style.use(snakemake.input.rc) + + regions_onshore = gpd.read_file(snakemake.input.regions_onshore).set_index("name") + regions_offshore = gpd.read_file(snakemake.input.regions_offshore).set_index("name") + + n = pypsa.Network(snakemake.input.network) + + potential_used = get_potential_used(n) + + legend_kwds = { + "label": "potential used [%]", + "shrink": 0.7, + } + + for carrier in potential_used.columns.intersection(POTENTIAL): + regions = ( + regions_offshore + if carrier in ["offwind-ac", "offwind-dc"] + else regions_onshore + ) + + cmap = "Reds" if "solar" in carrier else "Blues" + + plot_choropleth( + potential_used, + regions, + carrier, + cmap=cmap, + vmax=100, + vmin=0, + legend_kwds=legend_kwds, + title=n.carriers.nice_name.get(carrier, carrier), + dir=snakemake.output[0], + ) From 160241534e8823c94dc8eddcbb455c619f195950 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 15 Sep 2023 16:44:15 +0200 Subject: [PATCH 205/293] plot_choropleth_demand: first version --- rules/plot.smk | 16 ++ scripts/plot_choropleth_demand.py | 254 ++++++++++++++++++++++++++++++ 2 files changed, 270 insertions(+) create mode 100644 scripts/plot_choropleth_demand.py diff --git a/rules/plot.smk b/rules/plot.smk index 3fbee1f16..a23b41f77 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -213,6 +213,22 @@ rule plot_choropleth_potential_used: "../scripts/plot_choropleth_potential_used.py" +rule plot_choropleth_demand: + input: + network=RESULTS + + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + industrial_demand=RESOURCES + + "industrial_energy_demand_elec_s{simpl}_{clusters}_{planning_horizons}.csv", + shipping_demand=RESOURCES + "shipping_demand_s{simpl}_{clusters}.csv", + nodal_energy_totals=RESOURCES + "pop_weighted_energy_totals_s{simpl}_{clusters}.csv", + regions_onshore=RESOURCES + "regions_onshore_elec_s{simpl}_{clusters}.geojson", + rc="matplotlibrc", + output: + directory(RESOURCES + "graphics/regional_demand/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}"), + script: + "../scripts/plot_choropleth_demand.py" + + rule plot_balance_timeseries: input: network=RESULTS diff --git a/scripts/plot_choropleth_demand.py b/scripts/plot_choropleth_demand.py new file mode 100644 index 000000000..1f47b9757 --- /dev/null +++ b/scripts/plot_choropleth_demand.py @@ -0,0 +1,254 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2023- Fabian Neumann +# +# SPDX-License-Identifier: MIT +""" +Plot choropleth regional demands. +""" + +import os + +import cartopy +import cartopy.crs as ccrs +import geopandas as gpd +import matplotlib.pyplot as plt +import pandas as pd +import pypsa +from pypsa.descriptors import get_switchable_as_dense as as_dense + +TITLES = { + "electricity": "Electricity Demand [TWh/a]", + "H2": "Hydrogen Demand [TWh/a]", + "heat": "Heat Demand [TWh/a]", + "solid biomass": "Solid Biomass Demand [TWh/a]", + "gas": "Methane Demand [TWh/a]", + "oil": "Oil Demand [TWh/a]", + "methanol": "Methanol Demand [TWh/a]", + "ammonia": "Ammonia Demand [TWh/a]", + "process emission": "Process Emissions [MtCO2/a]", + # "steel": "Steel Demand [Mt/a]", + # "HVC": "HVC Demand [Mt/a]", +} + +CMAPS = { + "electricity": "Blues", + "H2": "RdPu", + "heat": "Reds", + "solid biomass": "Greens", + "gas": "Oranges", + "oil": "Greys", + "methanol": "BuGn", + "process emission": "Greys", + "ammonia": "Purples", + # "steel": "Blues", + # "HVC": "Oranges", +} + +REGEX = { + "electricity": r"(electricity|EV)", + "H2": r"(H2|fuel cell)", + "heat": r"heat", + "solid biomass": r"biomass", + "oil": r"(oil|naphtha|kerosene)", + "gas": r"gas", + "methanol": r"methanol", + "process emission": r"emission", + "ammonia": r"ammonia", + # "steel": r"(DRI|EAF|steel)", + # "HVC": r"HVC", +} + +MAPPING = { + "H2 for industry": "H2", + "agriculture electricity": "electricity", + "agriculture heat": "heat", + "agriculture machinery oil": "oil", + "electricity": "electricity", + "gas for industry": "gas", + "industry electricity": "electricity", + "industry methanol": "methanol", + "kerosene for aviation": "oil", + "land transport EV": "EV", + "low-temperature heat for industry": "heat", + "naphtha for industry": "oil", + "urban central heat": "heat", + "residential rural heat": "heat", + "residential urban decentral heat": "heat", + "services rural heat": "heat", + "services urban decentral heat": "heat", + "solid biomass for industry": "", + "oil emissions": "emissions", + "process emissions": "emissions", + "agriculture machinery oil emissions": "emissions", + "industry methanol emissions": "emissions", +} + + +def get_oil_demand(industrial_demand, nodal_energy_totals): + oil = [ + "total international aviation", + "total domestic aviation", + "total agriculture machinery", + ] + return industrial_demand["naphtha"] + nodal_energy_totals[oil].sum(axis=1) + + +def get_biomass_demand(industrial_demand): + return industrial_demand["solid biomass"] + + +def get_methanol_demand( + industrial_demand, nodal_energy_totals, shipping_demand, config +): + efficiency = ( + config["sector"]["shipping_oil_efficiency"] + / config["sector"]["shipping_methanol_efficiency"] + ) + return ( + industrial_demand["methanol"] + + nodal_energy_totals["total domestic navigation"] + + shipping_demand * efficiency + ) + + +def get_ammonia_demand(industrial_demand, shipping_demand, config): + efficiency = ( + config["sector"]["shipping_oil_efficiency"] + / config["sector"]["shipping_ammonia_efficiency"] + ) + return industrial_demand["ammonia"] + shipping_demand * efficiency + + +def get_process_emissions(industrial_demand): + cols = ["process emission", "process emission from feedstock"] + return industrial_demand[cols].sum(axis=1) + + +def get_demand_by_region( + n, industrial_demand, nodal_energy_totals, shipping_demand, config +): + demand = as_dense(n, "Load", "p_set").div(1e6) # TWh + demand_grouped = demand.groupby( + [n.loads.carrier, n.loads.bus.map(n.buses.location)], axis=1 + ).sum() + df = (n.snapshot_weightings.generators @ demand_grouped).unstack(level=0) + df.drop(["", "EU"], inplace=True) + df.dropna(axis=1, how="all", inplace=True) + oil_demand = pd.DataFrame( + {"oil": get_oil_demand(industrial_demand, nodal_energy_totals)} + ) + + methanol_demand = pd.DataFrame( + { + "methanol": get_methanol_demand( + industrial_demand, nodal_energy_totals, shipping_demand, config + ) + } + ) + + biomass_demand = pd.DataFrame({"biomass": get_biomass_demand(industrial_demand)}) + + process_emissions_demand = pd.DataFrame( + {"process emissions": get_process_emissions(industrial_demand)} + ) + + ammonia_demand = pd.DataFrame( + {"ammonia": get_ammonia_demand(industrial_demand, shipping_demand, config)} + ) + + df = pd.concat( + [ + df, + oil_demand, + methanol_demand, + biomass_demand, + process_emissions_demand, + ammonia_demand, + ], + axis=1, + ) + + return df + + +def plot_regional_demands(df, geodf, carrier, dir="."): + series = df.filter(regex=REGEX[carrier]).sum(axis=1) + + proj = ccrs.EqualEarth() + geodf = geodf.to_crs(proj.proj4_init) + + fig, ax = plt.subplots(figsize=(7, 7), subplot_kw={"projection": proj}) + + geodf.plot( + ax=ax, + column=series, + cmap=CMAPS[carrier], + linewidths=0, + legend=True, + legend_kwds={"label": TITLES[carrier], "shrink": 0.7}, + ) + + ax.add_feature(cartopy.feature.COASTLINE.with_scale("50m"), linewidth=0.5, zorder=2) + ax.add_feature(cartopy.feature.BORDERS.with_scale("50m"), linewidth=0.5, zorder=2) + + ax.axis("off") + + carrier_fn = carrier.replace("-", "_").replace(" ", "_") + fn = f"map-{carrier_fn}" + plt.savefig(dir + "/" + fn + ".png", bbox_inches="tight") + plt.savefig(dir + "/" + fn + ".pdf", bbox_inches="tight") + plt.close() + + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake( + "plot_choropleth_demand", + simpl="", + clusters=100, + ll="vopt", + opts="", + sector_opts="Co2L0-73SN-T-H-B-I-S-A", + planning_horizons=2050, + configfiles=["../../config/config.20230825.yaml"], + ) + + plt.style.use(snakemake.input.rc) + + # ensure path exists, since snakemake does not create path for directory outputs + # https://github.com/snakemake/snakemake/issues/774 + dir = snakemake.output[0] + if not os.path.exists(dir): + os.makedirs(dir) + + n = pypsa.Network(snakemake.input.network) + + industrial_demand = pd.read_csv(snakemake.input.industrial_demand, index_col=[0, 1]) + + shipping_demand = pd.read_csv( + snakemake.input.shipping_demand, index_col=0 + ).squeeze() + + nodal_energy_totals = pd.read_csv(snakemake.input.nodal_energy_totals, index_col=0) + + options = snakemake.config["sector"] + endogenous_sectors = [] + if options["endogenous_steel"]: + endogenous_sectors += ["DRI + Electric arc"] + if options["endogenous_hvc"]: + endogenous_sectors += ["HVC"] + sectors_b = ~industrial_demand.index.get_level_values("sector").isin( + endogenous_sectors + ) + industrial_demand = industrial_demand.loc[sectors_b].groupby(level=0).sum() + + df = get_demand_by_region( + n, industrial_demand, nodal_energy_totals, shipping_demand, snakemake.config + ) + + regions = gpd.read_file(snakemake.input.regions_onshore).set_index("name") + + for carrier in TITLES.keys(): + plot_regional_demands(df, regions, carrier, dir=dir) From a0905295e0377a61cb44ae851c1b1e133779e314 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 15 Sep 2023 16:56:10 +0200 Subject: [PATCH 206/293] plot.smk: formatting --- rules/plot.smk | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/rules/plot.smk b/rules/plot.smk index a23b41f77..5bd12c463 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -108,19 +108,19 @@ rule plot_salt_caverns_unclustered: rule plot_salt_caverns_clustered: input: - caverns=RESOURCES + "salt_cavern_potentials_s_{clusters}.csv", - regions_onshore=RESOURCES + "regions_onshore_elec_s_{clusters}.geojson", - regions_offshore=RESOURCES + "regions_offshore_elec_s_{clusters}.geojson", + caverns=RESOURCES + "salt_cavern_potentials_s{simpl}_{clusters}.csv", + regions_onshore=RESOURCES + "regions_onshore_elec_s{simpl}_{clusters}.geojson", + regions_offshore=RESOURCES + "regions_offshore_elec_s{simpl}_{clusters}.geojson", rc="matplotlibrc", output: onshore=multiext( - RESOURCES + "graphics/salt-caverns-{clusters}-onshore", ".png", ".pdf" + RESOURCES + "graphics/salt-caverns-s{simpl}-{clusters}-onshore", ".png", ".pdf" ), nearshore=multiext( - RESOURCES + "graphics/salt-caverns-{clusters}-nearshore", ".png", ".pdf" + RESOURCES + "graphics/salt-caverns-s{simpl}-{clusters}-nearshore", ".png", ".pdf" ), offshore=multiext( - RESOURCES + "graphics/salt-caverns-{clusters}-offshore", ".png", ".pdf" + RESOURCES + "graphics/salt-caverns-s{simpl}-{clusters}-offshore", ".png", ".pdf" ), script: "../scripts/plot_salt_caverns_clustered.py" @@ -128,22 +128,22 @@ rule plot_salt_caverns_clustered: rule plot_biomass_potentials: input: - biomass=RESOURCES + "biomass_potentials_s_{clusters}.csv", - regions_onshore=RESOURCES + "regions_onshore_elec_s_{clusters}.geojson", + biomass=RESOURCES + "biomass_potentials_s{simpl}_{clusters}.csv", + regions_onshore=RESOURCES + "regions_onshore_elec_s{simpl}_{clusters}.geojson", rc="matplotlibrc", output: solid_biomass=multiext( - RESOURCES + "graphics/biomass-potentials-{clusters}-solid_biomass", + RESOURCES + "graphics/biomass-potentials-s{simpl}-{clusters}-solid_biomass", ".png", ".pdf", ), not_included=multiext( - RESOURCES + "graphics/biomass-potentials-{clusters}-not_included", + RESOURCES + "graphics/biomass-potentials-s{simpl}-{clusters}-not_included", ".png", ".pdf", ), biogas=multiext( - RESOURCES + "graphics/biomass-potentials-{clusters}-biogas", ".png", ".pdf" + RESOURCES + "graphics/biomass-potentials-s{simpl}-{clusters}-biogas", ".png", ".pdf" ), script: "../scripts/plot_biomass_potentials.py" From d8b284cff45012614ab5c8aee6383c3431d04795 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 21 Sep 2023 18:22:21 +0200 Subject: [PATCH 207/293] endogenous_steel: add config options for process flexibility and relocation# --- config/config.default.yaml | 2 ++ scripts/prepare_sector_network.py | 30 ++++++++++++++++++++---------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 00e0f56e8..30eadeacd 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -530,6 +530,8 @@ sector: electrobiofuels: false endogenous_steel: false endogenous_hvc: false + relocation_steel: false + flexibility_steel: false import: capacity_boost: 2 limit: false # bool or number in TWh diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 885e41190..559224141 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2760,6 +2760,15 @@ def add_industry(n, costs): sector = "DRI + Electric arc" + no_relocation = not options.get("relocation_steel", False) + no_flexibility = not options.get("flexibility_steel", False) + + s = " not" if no_relocation else " " + logger.info(f"Steel industry relocation{s} activated.") + + s = " not" if no_flexibility else " " + logger.info(f"Steel industry flexibility{s} activated.") + n.add( "Bus", "EU steel", @@ -2776,14 +2785,15 @@ def add_industry(n, costs): p_set=industrial_production[sector].sum() / nhours, ) - # n.add( - # "Store", - # "EU steel Store", - # bus="EU steel", - # e_nom_extendable=True, - # e_cyclic=True, - # carrier="steel", - # ) + if not no_flexibility: + n.add( + "Store", + "EU steel Store", + bus="EU steel", + e_nom_extendable=True, + e_cyclic=True, + carrier="steel", + ) electricity_input = ( costs.at["direct iron reduction furnace", "electricity-input"] @@ -2819,9 +2829,9 @@ def add_industry(n, costs): carrier=sector, capital_cost=capital_cost, marginal_cost=marginal_cost, - p_nom_max=p_nom, + p_nom_max=p_nom if no_relocation else np.inf, p_nom_extendable=True, - p_min_pu=1, + p_min_pu=1 if no_flexibility else 0, bus0=nodes, bus1="EU steel", bus2=nodes + " H2", From 69cc059850922dcfe23565dbb0f42dee3a7a83f7 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 21 Sep 2023 19:22:21 +0200 Subject: [PATCH 208/293] energy_import_limit: consider steel in import volume limit by using 2.1 MWh/t energy content of iron as proxy --- scripts/solve_network.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/solve_network.py b/scripts/solve_network.py index 92a7d736f..6bcd04d8c 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -608,7 +608,11 @@ def add_energy_import_limit(n, sns): p_gens = n.model["Generator-p"].loc[sns, import_gens] p_links = n.model["Link-p"].loc[sns, import_links] - lhs = (p_gens * weightings).sum() + (p_links * weightings).sum() + # using energy content of iron as proxy: 2.1 MWh/t + energy_weightings = np.where(import_gens.str.contains("steel"), 2.1, 1.0) + energy_weightings = pd.Series(energy_weightings, index=import_gens) + + lhs = (p_gens * weightings * energy_weightings).sum() + (p_links * weightings).sum() rhs = limit * 1e6 From c09961300e659cd6d969487e80ded3450c1468a1 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 12 Oct 2023 09:51:47 +0200 Subject: [PATCH 209/293] add option for haber-bosch waste heat --- config/config.default.yaml | 1 + scripts/prepare_sector_network.py | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/config/config.default.yaml b/config/config.default.yaml index 30eadeacd..41123e686 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -492,6 +492,7 @@ sector: min_part_load_fischer_tropsch: 0.9 min_part_load_methanolisation: 0.5 use_fischer_tropsch_waste_heat: true + use_haber_bosch_waste_heat: true use_methanolisation_waste_heat: true use_methanation_waste_heat: true use_fuel_cell_waste_heat: true diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 559224141..c5a4da9e8 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3593,6 +3593,8 @@ def add_waste_heat(n): logger.info("Add possibility to use industrial waste heat in district heating") + cf_industry = snakemake.params.industry + # AC buses with district heating urban_central = n.buses.index[n.buses.carrier == "urban central heat"] if not urban_central.empty: @@ -3615,6 +3617,17 @@ def add_waste_heat(n): 0.95 - n.links.loc[urban_central + " Sabatier", "efficiency"] ) + # DEA quotes 15% of total input (11% of which are high-value heat) + if options["use_haber_bosch_waste_heat"]: + n.links.loc[urban_central + " Haber-Bosch", "bus3"] = ( + urban_central + " urban central heat" + ) + total_energy_input = (cf_industry["MWh_H2_per_tNH3_electrolysis"] + cf_industry["MWh_elec_per_tNH3_electrolysis"]) / cf_industry["MWh_NH3_per_tNH3"] + electricity_input = cf_industry["MWh_elec_per_tNH3_electrolysis"] / cf_industry["MWh_NH3_per_tNH3"] + n.links.loc[urban_central + " Haber-Bosch", "efficiency3"] = ( + 0.15 * total_energy_input / electricity_input + ) + if options["use_methanolisation_waste_heat"]: n.links.loc[urban_central + " methanolisation", "bus4"] = ( urban_central + " urban central heat" From 120591aaf345b6b11b71ccaf28e51d919439ca6a Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 12 Oct 2023 13:57:48 +0200 Subject: [PATCH 210/293] reactivate co2 vent by default this is to prevent negative marginal prices for the bus co2 stored --- 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 41123e686..117806b43 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -458,7 +458,7 @@ sector: helmeth: false coal_cc: false dac: true - co2_vent: false + co2_vent: true allam_cycle_gas: false hydrogen_fuel_cell: true hydrogen_turbine: false From 2a6d8f90d2e7a3a4ae18cbb03868f1a3bd40b064 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 12 Oct 2023 17:37:15 +0200 Subject: [PATCH 211/293] plot_import_shares: initial version --- rules/plot.smk | 11 +++++ scripts/plot_import_shares.py | 85 +++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 scripts/plot_import_shares.py diff --git a/rules/plot.smk b/rules/plot.smk index 5bd12c463..e713d33e5 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -348,3 +348,14 @@ rule plot_import_options: multiext(RESULTS + "graphics/import_options_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", ".png", ".pdf") script: "../scripts/plot_import_options.py" + + +rule plot_import_shares: + input: + network=RESULTS + + "postnetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + rc="matplotlibrc", + output: + multiext(RESULTS + "graphics/import_shares/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", ".png", ".pdf") + script: + "../scripts/plot_import_shares.py" diff --git a/scripts/plot_import_shares.py b/scripts/plot_import_shares.py new file mode 100644 index 000000000..d52c4f24f --- /dev/null +++ b/scripts/plot_import_shares.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2023 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT +""" +Creates stacked bar charts of import shares per carrier. +""" + +import logging + +logger = logging.getLogger(__name__) + +import matplotlib.pyplot as plt +import pypsa +from _helpers import configure_logging + +CARRIERS = { + "AC": "electricity", + "H2": "hydrogen", + "NH3": "ammonia", + "gas": "methane", + "methanol": "methanol", + "oil": "Fischer-Tropsch", + "steel": "steel", +} + +THRESHOLD = 1 # MWh + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake( + "plot_import_shares", + simpl="", + opts="", + clusters="100", + ll="vopt", + sector_opts="Co2L0-2190SEG-T-H-B-I-S-A-imp", + planning_horizons="2050", + configfiles="../../config/config.20230926-zecm.yaml", + ) + + configure_logging(snakemake) + + plt.style.use(['bmh', snakemake.input.rc]) + + n = pypsa.Network(snakemake.input.network) + + eb = n.statistics.energy_balance().groupby(["carrier", "bus_carrier"]).sum() + supply = eb.where(eb > THRESHOLD).dropna().div(1e6) + + ie = supply.unstack(1).groupby(lambda x: "import" if "import" in x or "external" in x else "domestic").sum() + ie = ie / ie.sum() * 100 + + + fig, ax = plt.subplots(figsize=(8, 4)) + sel = list(CARRIERS.keys())[::-1] + ie[sel].rename(columns=CARRIERS).T.plot.barh( + stacked=True, + ax=ax, + color=['lightseagreen', 'coral'] + ) + + plt.ylabel("") + plt.xlabel("domestic share [%]", fontsize=11, color='lightseagreen') + plt.legend(ncol=2, bbox_to_anchor=(0.35, 1.25)) + plt.grid(axis="y") + plt.xlim(0,100) + + secax = ax.secondary_xaxis('top', functions=(lambda x: 100 - x, lambda x: 100 -x)) + secax.set_xlabel('import share [%]', fontsize=11, color='coral') + + for i in ["top", "right", "left", "bottom"]: + secax.spines[i].set_visible(False) + ax.spines[i].set_visible(False) + + def fmt(x): + return f"{x:.0f}%" if x > 1 else "" + + for container in ax.containers[:2]: + ax.bar_label(container, label_type='center', color='white', fmt=fmt) + + for fn in snakemake.output: + plt.savefig(fn, bbox_inches="tight") From dff8cfe1b8694ef55c6ad5fcc0cd7800b880f069 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 23 Oct 2023 11:27:20 +0200 Subject: [PATCH 212/293] plot_import_world_map: initial version --- rules/plot.smk | 21 ++ scripts/plot_import_world_map.py | 416 +++++++++++++++++++++++++++++++ 2 files changed, 437 insertions(+) create mode 100644 scripts/plot_import_world_map.py diff --git a/rules/plot.smk b/rules/plot.smk index e713d33e5..46a27e7fc 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -349,6 +349,27 @@ rule plot_import_options: script: "../scripts/plot_import_options.py" +rule plot_import_world_map: + input: + imports="data/imports/results.csv", + profiles="data/imports/combined_weighted_generator_timeseries.nc", + gadm_arg=HTTP.remote( + "https://geodata.ucdavis.edu/gadm/gadm4.1/gpkg/gadm41_ARG.gpkg", + keep_local=True, + static=True, + ), + copernicus_glc=HTTP.remote( + "https://zenodo.org/records/3939050/files/PROBAV_LC100_global_v3.0.1_2019-nrt_Discrete-Classification-map_EPSG-4326.tif", + keep_local=True, + static=True, + ), + rc="matplotlibrc", + output: + multiext(RESOURCES + "graphics/import_world_map", ".png", ".pdf") + script: + "../scripts/plot_import_world_map.py" + + rule plot_import_shares: input: diff --git a/scripts/plot_import_world_map.py b/scripts/plot_import_world_map.py new file mode 100644 index 000000000..9fe395065 --- /dev/null +++ b/scripts/plot_import_world_map.py @@ -0,0 +1,416 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2023- Fabian Neumann +# +# SPDX-License-Identifier: MIT +""" +Creates world map of global green fuel and material markets. +""" + +import logging +import os + +import cartopy.crs as ccrs +import country_converter as coco +import geopandas as gpd +import matplotlib.dates as mdates +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +import xarray as xr +from _helpers import configure_logging +from atlite.gis import ExclusionContainer, shape_availability +from rasterio.features import geometry_mask +from rasterio.plot import show + +logger = logging.getLogger(__name__) + +cc = coco.CountryConverter() + +AREA_CRS = "ESRI:54009" + +NICE_NAMES = { + "pipeline-h2": r"H$_2$ (pipeline)", + "shipping-lh2": "H$_2$ (ship)", + "shipping-ftfuel": "Fischer-Tropsch", + "shipping-meoh": "methanol", + "shipping-lch4": "methane", + "shipping-lnh3": "ammonia", + "shipping-steel": "steel", +} + +def rename(s): + if "solar" in s: + return "solar" + if "wind" in s: + return "wind" + if "storage" in s or "inverter" in s: + return "storage" + if "transport" in s or "shipping fuel" in s or "dry bulk" in s or "pipeline" in s: + return "transport" + if "evaporation" in s or "liquefaction" in s or "compress" in s: + return "evaporation/liquefaction" + if "direct air capture" in s or "heat pump" in s: + return "direct air capture" + if s in ["Haber-Bosch (exp)", "Fischer-Tropsch (exp)", "methanolisation (exp)", "methanation (exp)"]: + return "hydrogen conversion" + if "iron ore" in s: + return "iron ore" + if "direct iron reduction" in s: + return "direct iron reduction" + if "electric arc furnace" in s: + return "electric arc furnace" + return s.replace(" (exp)", "") + + +def get_cost_composition(df, country, escs, production): + query_str = "category == 'cost' and exporter == @country and esc in @escs" + composition = df.query(query_str).groupby(["esc", "subcategory", "importer"]).value.min() + + minimal = {} + for name, group in composition.groupby("esc"): + c = group.unstack("importer").droplevel("esc") + minimal[name] = c[c.sum().idxmin()] + composition = pd.concat(minimal, axis=1) + + composition = composition.groupby(rename).sum().div(production) + + composition = composition.where(composition > 0.01).dropna(how='all') + + sort_by = composition.sum().sort_values(ascending=True).index + selection = pd.Index(COLORS.keys()).intersection(composition.index) + composition = composition.loc[selection, sort_by].rename(columns=NICE_NAMES) + return composition + + +def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): + + excluder = ExclusionContainer(crs=AREA_CRS, res=200) + excluder.add_raster(glc_fn, codes=[20, 30, 40, 60, 100], invert=True) + excluder.add_raster(glc_fn, codes=[50], buffer=1500) + if os.path.exists(wdpa_fn): + wdpa = gpd.read_file( + wdpa_fn, + bbox=shape.to_crs(4326).geometry, + layer="WDPA_Oct2023_Public_shp-polygons", + ).to_crs(AREA_CRS) + excluder.add_geometry(wdpa.geometry, buffer=1000) + + band, transform = shape_availability(shape, excluder) + mask = ~geometry_mask( + [shape.geometry.values[0]], + transform=transform, + invert=False, + out_shape=band.shape + ) + masked_band = np.where(mask, ~band, np.nan) + + shape.plot(ax=ax, color="none", edgecolor='k', linewidth=1) + show(masked_band, transform=transform, cmap="Purples", ax=ax) + ax.set_axis_off() + + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake( + "plot_import_world_map", + simpl="", + opts="", + clusters="100", + ll="v1.5", + sector_opts="Co2L0-2190SEG-T-H-B-I-S-A-imp", + planning_horizons="2050", + configfiles="../../config/config.100n-seg.yaml", + ) + + configure_logging(snakemake) + + import_fn = snakemake.input.imports + profile_fn = snakemake.input.profiles + gadm_fn = snakemake.input.gadm_arg[0] + glc_fn = snakemake.input.copernicus_glc[0] + wdpa_fn = "/home/fneum/bwss/playgrounds/pr/pypsa-eur/resources/WDPA_Oct2023.gpkg" + + config = snakemake.config + + plt.style.use(['bmh', snakemake.input.rc]) + + tech_colors = config["plotting"]["tech_colors"] + + COLORS = { + "wind": tech_colors["onwind"], + "solar": tech_colors["solar"], + "storage": tech_colors["battery"], + "electrolysis": tech_colors["H2 Electrolysis"], + "direct air capture": tech_colors["DAC"], + "hydrogen conversion": tech_colors["Fischer-Tropsch"], + "iron ore": "#4e4f55", + "direct iron reduction": tech_colors["steel"], + "electric arc furnace": "#8795a8", + "evaporation/liquefaction": "#8487e8", + "transport": "#f7a572", + } + + # load capacity factor time series + + ds = xr.open_dataset(profile_fn) + profile = ds.sel(exporter='MA', importer='EUE').p_max_pu.to_pandas().T + profile.rename(columns={"onwind": "wind", "solar-utility": "solar"}, inplace=True) + + # download world country shapes, version clipped to Europe, and GADM in AR + + world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres')).set_index("iso_a3") + world.drop("ATA", inplace=True) + + eu_countries = cc.convert(config["countries"], src="iso2", to="iso3") + europe = world.loc[eu_countries] + + gadm = gpd.read_file(gadm_fn, layer="ADM_ADM_1").set_index("NAME_1") + shape = gadm.to_crs(AREA_CRS).loc[["Buenos Aires"]].geometry + + # load import costs + + df = pd.read_csv(import_fn, sep=";") + + import_costs = df.query("subcategory == 'Cost per MWh delivered' and esc == 'shipping-meoh'").groupby("exporter").value.min() + import_costs.index = cc.convert(import_costs.index.str.split("-").str[0], to="ISO3") + import_costs.drop("RUS", inplace=True) + + composition_arg = get_cost_composition( + df, + "AR", + ["shipping-lh2", "shipping-ftfuel", "shipping-meoh", "shipping-lch4", "shipping-lnh3"], + 500e6 + ) + + composition_sau = get_cost_composition( + df, + "SA", + ["pipeline-h2", "shipping-lh2"], + 500e6 + ) + + composition_aus = get_cost_composition( + df, + "AU", + ["shipping-steel"], + 100e6 + ) + + # add to legend + + composition_arg.loc["iron ore"] = pd.NA + composition_arg.loc["direct iron reduction"] = pd.NA + composition_arg.loc["electric arc furnace"] = pd.NA + + # create plot + + crs = ccrs.EqualEarth() + + fig, ax = plt.subplots(figsize=(14,14), subplot_kw={"projection": crs}) + + # main axis: choropleth layer + + world.to_crs(crs).plot( + column=import_costs.reindex(world.index), + linewidth=1, + edgecolor="black", + ax=ax, + cmap='Greens_r', + legend=True, + vmin=90, + vmax=140, + legend_kwds=dict(label="Cost for methanol fuel delivered [€/MWh]", orientation="horizontal", extend='max', shrink=.6, aspect=30, pad=.01), + missing_kwds=dict(color="#eee", label="not considered"), + ) + + europe.to_crs(crs).plot( + linewidth=1, + edgecolor="black", + ax=ax, + color='lavender', + ) + + for spine in ax.spines.values(): + spine.set_visible(False) + + ax.set_facecolor('none') + fig.set_facecolor('none') + + ax.text(0.93, 0.01, "Projection:\nEqual Earth", transform=ax.transAxes, fontsize=9, color="grey") + + plt.tight_layout() + + # inset: wind and solar profiles + + ax_prof = ax.inset_axes([0.36, 0.68, 0.09, 0.11]) + + week_profile = profile.loc["2013-03-01":"2013-03-07", ["solar", "wind"]] + + week_profile.plot( + ax=ax_prof, + linewidth=1, + color=['gold', "royalblue"], + ylim=(0,1), + clip_on=False, + ) + + ax_prof.legend( + title="", + loc=(0,1), + fontsize=8, + ncol=2, + columnspacing=0.8 + ) + ax_prof.set_xlabel("March 2013", fontsize=8) + ax_prof.set_ylabel("profile [p.u.]", fontsize=8) + ax_prof.tick_params(axis='both', labelsize=8) + + ax_prof.xaxis.set_major_locator(mdates.DayLocator()) + ax_prof.xaxis.set_major_formatter(mdates.DateFormatter("%d")) + xticks = week_profile.resample('D').mean().index + ax_prof.set_xticks(xticks) + ax_prof.set_xticklabels(xticks.day) + + for spine in ax_prof.spines.values(): + spine.set_visible(False) + + for label in ax_prof.get_xticklabels(): + label.set_fontsize(8) + + ax.annotate( + '', + xy=(0.45, 0.75), + xytext=(0.485, 0.72), + xycoords='axes fraction', + arrowprops=dict( + edgecolor='#555', + facecolor='#555', + linewidth=1.5, + arrowstyle='-|>', + connectionstyle="arc3,rad=0.2" + ) + ) + + # inset: Argentina e-fuel import costs + + ax_arg = ax.inset_axes([0.07, 0.08, 0.09, 0.33]) + + composition_arg.T.plot.bar(ax=ax_arg, stacked=True, color=COLORS) + + handles, labels = ax_arg.get_legend_handles_labels() + handles.reverse() + labels.reverse() + + ax_arg.legend(handles, labels, title="", ncol=1, fontsize=9, loc=(1,0)) + + ax_arg.set_title("Import costs from\nArgentina to Europe", fontsize=9) + + ax_arg.set_xlabel("") + ax_arg.set_ylim(0, 100) + ax_arg.set_ylabel("€/MWh", fontsize=10) + ax_arg.grid(axis="x") + for spine in ax_arg.spines.values(): + spine.set_visible(False) + + ax.annotate( + '', + xy=(0.25, 0.15), + xytext=(0.33, 0.2), + xycoords='axes fraction', + arrowprops=dict( + edgecolor='#555', + facecolor='#555', + linewidth=1.5, + arrowstyle='-|>', + connectionstyle="arc3,rad=-0.2" + ) + ) + + # inset: Saudi Arabia hydrogen pipeline versus ship imports + + ax_sau = ax.inset_axes([0.6625, 0.33, 0.036, 0.2]) + + composition_sau.T.plot.bar(ax=ax_sau, stacked=True, color=COLORS, legend=False) + + ax_sau.set_title(r"LH$_2$" + " ship\nvs. pipeline", fontsize=9) + + ax_sau.set_xlabel("") + ax_sau.set_ylabel("€/MWh", fontsize=10) + ax_sau.grid(axis="x") + ax_sau.set_ylim(0, 80) + ax_sau.set_yticks(range(0, 81, 20)) + for spine in ax_sau.spines.values(): + spine.set_visible(False) + + ax.annotate( + '', + xy=(0.655, 0.55), + xytext=(0.62, 0.65), + xycoords='axes fraction', + arrowprops=dict( + edgecolor='#555', + facecolor='#555', + linewidth=1.5, + arrowstyle='-|>', + connectionstyle="arc3,rad=0.2" + ) + ) + + # inset: Australia steel imports + + ax_aus = ax.inset_axes([0.75, 0.15, 0.018, 0.25]) + + composition_aus.T.plot.bar(ax=ax_aus, stacked=True, color=COLORS, legend=False) + + ax_aus.set_title("steel\nimports", fontsize=9) + + ax_aus.set_xlabel("") + ax_aus.set_ylabel("€/tonne", fontsize=10) + ax_aus.grid(axis="x") + ax_aus.set_yticks(range(0, 600, 100)) + for spine in ax_aus.spines.values(): + spine.set_visible(False) + + + ax.annotate( + '', + xy=(0.77, 0.35), + xytext=(0.815, 0.31), + xycoords='axes fraction', + arrowprops=dict( + edgecolor='#555', + facecolor='#555', + linewidth=1.5, + arrowstyle='-|>', + connectionstyle="arc3,rad=0.2" + ) + ) + + # inset: land eligibility of Buenos Aires + + ax_land = ax.inset_axes([0.315, 0.08, 0.29, 0.29]) + + shape.to_crs(crs.proj4_init).plot(ax=ax, color="none", edgecolor='k', linestyle=":", linewidth=1) + + add_land_eligibility_example(ax_land, shape, glc_fn, wdpa_fn) + + ax_land.set_title("wind exclusion\nzones (purple)", fontsize=9) + + ax.annotate( + '', + xy=(0.41, 0.22), + xytext=(0.35, 0.17), + xycoords='axes fraction', + arrowprops=dict( + edgecolor='#555', + facecolor='#555', + linewidth=1.5, + arrowstyle='-|>', + connectionstyle="arc3,rad=0.2" + ) + ) + + for fn in snakemake.output: + plt.savefig(fn, bbox_inches="tight") From 979588bda07282105330c6c1ba9a028b5774f9d7 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 23 Oct 2023 20:07:33 +0200 Subject: [PATCH 213/293] add option to specify minimum part load for methanation --- config/config.default.yaml | 5 +++-- scripts/prepare_sector_network.py | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 117806b43..ba6ee0db2 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -489,8 +489,9 @@ sector: - nearshore # within 50 km of sea # - offshore ammonia: false - min_part_load_fischer_tropsch: 0.9 - min_part_load_methanolisation: 0.5 + min_part_load_fischer_tropsch: 0.7 + min_part_load_methanolisation: 0.3 + min_part_load_methanation: 0.3 use_fischer_tropsch_waste_heat: true use_haber_bosch_waste_heat: true use_methanolisation_waste_heat: true diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index c5a4da9e8..3cb1199d5 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1582,6 +1582,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), efficiency=costs.at["methanation", "efficiency"], efficiency2=-costs.at["methanation", "efficiency"] * costs.at["gas", "CO2 intensity"], From 993e04552f5cd5aa88357c9e13397385d67a5e7e Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 25 Oct 2023 16:59:18 +0200 Subject: [PATCH 214/293] update import option colors --- 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 ba6ee0db2..b4be1471c 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -1126,14 +1126,14 @@ plotting: DC: "#8a1caf" DC-DC: "#8a1caf" DC link: "#8a1caf" - import pipeline-h2: '#fff6e0' - import shipping-lh2: '#ebe1ca' - import shipping-lch4: '#d6cbb2' - import shipping-lnh3: '#c6c093' - import shipping-ftfuel: '#bdb093' - import shipping-meoh: '#91856a' - import shipping-steel: '#736c5d' - import hvdc-to-elec: '#47443d' + import pipeline-h2: '#db8ccd' + import shipping-lh2: '#d9b8d3' + import shipping-lch4: '#f7a572' + import shipping-lnh3: '#87d0e6' + import shipping-ftfuel: '#95decb' + import shipping-meoh: '#b7dbdb' + import shipping-steel: '#94a4be' + import hvdc-to-elec: '#91856a' external H2: '#f7cdf0' external H2 Turbine: '#991f83' external H2 Electrolysis: '#991f83' From 55d09ae80f3b8856bbf6c1595aa89d554ad6f912 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 25 Oct 2023 17:00:23 +0200 Subject: [PATCH 215/293] plot_import_world_map: stylistic updates, include Namibia --- scripts/plot_import_world_map.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/scripts/plot_import_world_map.py b/scripts/plot_import_world_map.py index 9fe395065..849dd980d 100644 --- a/scripts/plot_import_world_map.py +++ b/scripts/plot_import_world_map.py @@ -171,11 +171,15 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): # load import costs - df = pd.read_csv(import_fn, sep=";") + df = pd.read_csv(import_fn, sep=";", keep_default_na=False) + + # bugfix for Namibia + df["exporter"] = df.exporter.replace("", "NA") import_costs = df.query("subcategory == 'Cost per MWh delivered' and esc == 'shipping-meoh'").groupby("exporter").value.min() - import_costs.index = cc.convert(import_costs.index.str.split("-").str[0], to="ISO3") - import_costs.drop("RUS", inplace=True) + import_costs.index = cc.convert(import_costs.index.str.split("-").str[0], src='iso2', to='iso3') + + import_costs.drop("RUS", inplace=True, errors="ignore") composition_arg = get_cost_composition( df, @@ -219,9 +223,9 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): ax=ax, cmap='Greens_r', legend=True, - vmin=90, - vmax=140, - legend_kwds=dict(label="Cost for methanol fuel delivered [€/MWh]", orientation="horizontal", extend='max', shrink=.6, aspect=30, pad=.01), + vmin=100, + vmax=150, + legend_kwds=dict(label="Cost for methanol fuel delivered to Europe [€/MWh]", orientation="horizontal", extend='max', shrink=.6, aspect=30, pad=.01), missing_kwds=dict(color="#eee", label="not considered"), ) @@ -308,7 +312,9 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): ax_arg.set_title("Import costs from\nArgentina to Europe", fontsize=9) ax_arg.set_xlabel("") - ax_arg.set_ylim(0, 100) + ax_arg.set_ylim(0, 110) + ax_arg.set_yticks(range(0, 111, 20)) + ax_arg.set_yticks(range(10, 111, 20), minor=True) ax_arg.set_ylabel("€/MWh", fontsize=10) ax_arg.grid(axis="x") for spine in ax_arg.spines.values(): @@ -339,8 +345,9 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): ax_sau.set_xlabel("") ax_sau.set_ylabel("€/MWh", fontsize=10) ax_sau.grid(axis="x") - ax_sau.set_ylim(0, 80) - ax_sau.set_yticks(range(0, 81, 20)) + ax_sau.set_ylim(0, 90) + ax_sau.set_yticks(range(0, 91, 20)) + ax_sau.set_yticks(range(10, 91, 20), minor=True) for spine in ax_sau.spines.values(): spine.set_visible(False) @@ -370,6 +377,7 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): ax_aus.set_ylabel("€/tonne", fontsize=10) ax_aus.grid(axis="x") ax_aus.set_yticks(range(0, 600, 100)) + ax_aus.set_yticks(range(50, 600, 100), minor=True) for spine in ax_aus.spines.values(): spine.set_visible(False) From 23607dcb9ce7cfc2f9b65d1f53600ca24df35dc0 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 25 Oct 2023 17:01:14 +0200 Subject: [PATCH 216/293] prepare_sector_network: hanlde 'NA' for namibia --- scripts/prepare_sector_network.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 3cb1199d5..f8eaec3a8 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -4049,7 +4049,11 @@ def add_import_options( "shipping-meoh": ("methanolisation", "carbondioxide-input"), } - import_costs = pd.read_csv(snakemake.input.import_costs, delimiter=";") + import_costs = pd.read_csv(snakemake.input.import_costs, delimiter=";", keep_default_na=False) + + # temporary bugfix for Namibia + import_costs["exporter"] = import_costs.exporter.replace("", "NA") + cols = ["esc", "exporter", "importer", "value"] fields = ["Cost per MWh delivered", "Cost per t delivered"] import_costs = import_costs.query("subcategory in @fields")[cols] From 51747b53c53723d0580d71df781b213fa99e853a Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 25 Oct 2023 17:01:59 +0200 Subject: [PATCH 217/293] add options 'nowasteheat' and 'nobiogascc' to sector_opts wildcard --- scripts/prepare_sector_network.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index f8eaec3a8..920660945 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -4582,6 +4582,18 @@ def lossy_bidirectional_links(n, carrier, efficiencies={}): if "nodistrict" in opts: options["district_heating"]["progress"] = 0.0 + if "nowasteheat" in opts: + logger.info("Disabling waste heat.") + options["use_fischer_tropsch_waste_heat"] = False + options["use_methanolisation_waste_heat"] = False + options["use_haber_bosch_waste_heat"] = False + options["use_methanation_waste_heat"] = False + options["use_fuel_cell_waste_heat"] = False + options["use_electrolysis_waste_heat"] = False + + if "nobiogascc" in opts: + options["biomass_upgrading_cc"] = False + if "T" in opts: add_land_transport(n, costs) From ce85ed1d5853115c9e1a17fe9a9a18a63faccf3c Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 25 Oct 2023 17:02:56 +0200 Subject: [PATCH 218/293] switch to Helvetica as default font --- matplotlibrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matplotlibrc b/matplotlibrc index 92451ceea..2b4f736d4 100644 --- a/matplotlibrc +++ b/matplotlibrc @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: CC0-1.0 font.family: sans-serif -font.sans-serif: Roboto, Ubuntu, DejaVu Sans +font.sans-serif: Helvetica, Roboto, Ubuntu, DejaVu Sans image.cmap: viridis figure.autolayout : True figure.constrained_layout.use : True From 8cc6764762428f5efe1b82d84c57fadffa717369 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 25 Oct 2023 17:03:27 +0200 Subject: [PATCH 219/293] plot_import_network: initial version --- rules/plot.smk | 11 ++ scripts/plot_import_network.py | 270 +++++++++++++++++++++++++++++++++ 2 files changed, 281 insertions(+) create mode 100644 scripts/plot_import_network.py diff --git a/rules/plot.smk b/rules/plot.smk index 46a27e7fc..d257e74fb 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -370,6 +370,17 @@ rule plot_import_world_map: "../scripts/plot_import_world_map.py" +rule plot_import_networks: + 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", + rc="matplotlibrc", + output: + multiext(RESULTS + "graphics/import_networks/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", ".png", ".pdf") + script: + "../scripts/plot_import_networks.py" + rule plot_import_shares: input: diff --git a/scripts/plot_import_network.py b/scripts/plot_import_network.py new file mode 100644 index 000000000..5b130685a --- /dev/null +++ b/scripts/plot_import_network.py @@ -0,0 +1,270 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2020-2023 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT +""" +Creates map of network with import options. +""" + +import logging + +logger = logging.getLogger(__name__) + +import cartopy.crs as ccrs +import geopandas as gpd +import matplotlib.pyplot as plt +import pandas as pd +import pypsa +from _helpers import configure_logging +from plot_power_network import assign_location +from pypsa.plot import add_legend_circles, add_legend_lines, add_legend_patches + +PTX_THRESHOLD = 1 # M€ + + +def import_costs(n): + kwargs = dict(comps={"Generator"}, groupby=["bus", "carrier"]) + tsc_gen = n.statistics.capex(**kwargs) + n.statistics.opex(**kwargs) + + kwargs = dict(comps={"Link"}, groupby=["bus0", "carrier"]) + tsc_link = n.statistics.capex(**kwargs) + n.statistics.opex(**kwargs) + tsc_link.rename_axis(index={"bus0": "bus"}, inplace=True) + + kwargs = dict(comps={"Store"}, groupby=["bus", "carrier"]) + tsc_sto = n.statistics.capex(**kwargs) + n.statistics.opex(**kwargs) + + # groupby needed to merge duplicate entries + tsc_imp = ( + pd.concat([tsc_gen, tsc_link, tsc_sto]) + .droplevel(0) + .groupby(["bus", "carrier"]) + .sum() + ) / 1e6 # M€ + + tsc_imp.index = pd.MultiIndex.from_arrays( + [ + tsc_imp.index.get_level_values("bus").map(n.buses.location), + tsc_imp.index.get_level_values("carrier"), + ] + ) + + locs = tsc_imp.index.get_level_values("bus") + carriers = tsc_imp.index.get_level_values("carrier") + to_skip = ["", "EU", "EU import", "EU solid", "process"] + import_components = carriers.str.contains( + "(external|import shipping|import pipeline)" + ) + tsc_imp = tsc_imp[~(locs.isna() | locs.isin(to_skip)) & import_components] + tsc_imp.index = tsc_imp.index.remove_unused_levels() + + tsc_imp = tsc_imp.where(tsc_imp > PTX_THRESHOLD).dropna() + + return tsc_imp.sort_index() + + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake( + "plot_import_networks", + simpl="", + opts="", + clusters="100", + ll="vopt", + sector_opts="Co2L0-2190SEG-T-H-B-I-S-A-nowasteheat", + planning_horizons="2050", + configfiles="../../config/config.20230926-zecm.yaml", + ) + + configure_logging(snakemake) + + plt.style.use(snakemake.input.rc) + + tech_colors = snakemake.config["plotting"]["tech_colors"] + + crs = ccrs.EqualEarth() + + bus_size_factor = 7e3 + line_width_factor = 1e4 + + n = pypsa.Network(snakemake.input.network) + assign_location(n) + if n.links.carrier.str.contains("hvdc-to-elec").any(): + exporters = snakemake.config["sector"]["import"]["endogenous_hvdc_import"][ + "exporters" + ] + n.buses.loc[exporters, "location"] = exporters + + regions = ( + gpd.read_file(snakemake.input.regions, crs=4326) + .set_index("name") + .to_crs(crs.proj4_init) + ) + + ptx = n.links.filter( + regex="(methanolisation|Haber|Fischer|Sabatier|Electrolysis)", axis=0 + ).copy() + ptx["cost"] = ptx.eval("p_nom_opt * capital_cost / 1e6") + ptx = ptx.groupby(ptx.bus0.map(n.buses.location)).cost.sum() + ptx = ptx.loc[ptx > PTX_THRESHOLD] + regions["ptx"] = ptx + + imp_costs = import_costs(n) + + # add EU import costs + + eu_sizes = {} + + carriers = ["EU import shipping-lnh3", "EU import shipping-steel"] + for carrier in carriers: + if carrier in n.generators.index: + mc = n.generators.at[carrier, "marginal_cost"] + energy = n.generators_t.p.loc[:, carrier] @ n.snapshot_weightings.generators + eu_sizes[tuple(carrier.split(" ", 1))] = mc * energy / 1e6 + + carriers = ["EU import shipping-ftfuel", "EU import shipping-meoh"] + for carrier in carriers: + if carrier in n.links.index: + mc = n.links.at[carrier, "marginal_cost"] + energy = n.links_t.p0.loc[:, carrier] @ n.snapshot_weightings.generators + eu_sizes[tuple(carrier.split(" ", 1))] = mc * energy / 1e6 + + # place EU bus in sea + if eu_sizes: + eu_sizes = pd.Series(eu_sizes) + eu_sizes.index.names = ["bus", "carrier"] + if "EU" in n.buses.index: + n.remove("Bus", "EU") + n.add("Bus", "EU", x=-9, y=47.5) + + imp_costs = pd.concat([imp_costs, eu_sizes]) + + # patch network + n.buses.drop(n.buses.index[n.buses.carrier != "AC"], inplace=True) + if "KZ" in n.buses.index: + n.buses.loc["KZ", "x"] = 52 + n.buses.loc["KZ", "y"] = 49 + if "CN-West" in n.buses.index: + n.buses.loc["CN-West", "x"] = 79 + n.buses.loc["CN-West", "y"] = 38 + for ct in n.buses.index.intersection({"MA", "DZ", "TN", "LY", "EG", "SA"}): + n.buses.loc[ct, "y"] += 2 + + link_colors = pd.Series( + n.links.index.map( + lambda x: "olivedrab" if "import hvdc-to-elec" in x else "orange" + ), + index=n.links.index, + ) + + link_widths = ( + n.links.p_nom_opt.where(n.links.p_nom_opt > 1e3) + .fillna(0.0) + .div(line_width_factor) + ) + line_widths = ( + n.lines.s_nom_opt.where(n.lines.s_nom_opt > 1e3) + .fillna(0.0) + .div(line_width_factor) + ) + + fig, ax = plt.subplots(subplot_kw={"projection": crs}, figsize=(8, 12)) + + regions.plot( + ax=ax, + column="ptx", + cmap="Blues", + linewidths=0.5, + edgecolor="lightgray", + legend=True, + vmin=0, + vmax=6000, + legend_kwds={ + "label": r"PtX investments [M€/a]", + "shrink": 0.35, + "pad": 0.015, + }, + ) + + n.plot( + ax=ax, + color_geomap={"ocean": "white", "land": "#efefef"}, + bus_sizes=imp_costs.div(bus_size_factor), + bus_colors=tech_colors, + line_colors="olive", + line_widths=line_widths, + link_widths=link_widths, + link_colors=link_colors, + ) + + n.plot( + ax=ax, + bus_sizes=0.0, + line_colors="tan", + link_colors="tan", + line_widths=n.lines.s_nom.div(line_width_factor), + link_widths=n.links.p_nom.div(line_width_factor), + boundaries=[-11, 48, 25.25, 71.5], + margin=0, + ) + + patches = imp_costs.index.get_level_values(1).unique() + labels = list(patches) + [ + "import HVDC (new)", + "internal HVDC (new)", + "internal HVAC (existing)", + "internal HVAC (reinforced)", + ] + colors = [tech_colors[c] for c in patches] + ["olivedrab", "orange", "tan", "olive"] + + legend_kw = dict( + bbox_to_anchor=(1.65, 0.6), + frameon=False, + title="technology", + alignment="left", + ) + + add_legend_patches( + ax, + colors, + labels, + legend_kw=legend_kw, + ) + + legend_kw = dict( + bbox_to_anchor=(1.52, 1.04), + frameon=False, + title="import expenditures", + alignment="left", + ) + + add_legend_circles( + ax, + [1000 / bus_size_factor, 5000 / bus_size_factor, 10000 / bus_size_factor], + ["1,000 M€/a", "5,000 M€/a", "10,000 M€/a"], + patch_kw=dict(facecolor="lightgrey"), + legend_kw=legend_kw, + ) + + legend_kw = dict( + bbox_to_anchor=(1.52, 0.83), + frameon=False, + title="transmission capacity", + alignment="left", + ) + + add_legend_lines( + ax, + [ + 10000 / line_width_factor, + 20000 / line_width_factor, + 30000 / line_width_factor, + ], + ["10 GW", "20 GW", "30 GW"], + patch_kw=dict(color="lightgrey"), + legend_kw=legend_kw, + ) + + for fn in snakemake.output: + plt.savefig(fn, bbox_inches="tight") From 19df04d5f68d6714fca22b86efa1c464dc7dd300 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 25 Oct 2023 17:04:30 +0200 Subject: [PATCH 220/293] choropleth_capacity_factors: ensure directory exists --- scripts/plot_choropleth_capacity_factors.py | 27 +++++++++------------ 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/scripts/plot_choropleth_capacity_factors.py b/scripts/plot_choropleth_capacity_factors.py index c5cfb0170..771c971a6 100644 --- a/scripts/plot_choropleth_capacity_factors.py +++ b/scripts/plot_choropleth_capacity_factors.py @@ -60,11 +60,6 @@ def plot_choropleth( ax.axis("off") - # ensure path exists, since snakemake does not create path for directory outputs - # https://github.com/snakemake/snakemake/issues/774 - if not os.path.exists(dir): - os.makedirs(dir) - carrier_fn = carrier.replace("-", "_").replace(" ", "_") fn = f"map-{carrier_fn}" plt.savefig(dir + "/" + fn + ".pdf", bbox_inches="tight") @@ -84,6 +79,12 @@ def plot_choropleth( plt.style.use(snakemake.input.rc) + # ensure path exists, since snakemake does not create path for directory outputs + # https://github.com/snakemake/snakemake/issues/774 + dir = snakemake.output[0] + if not os.path.exists(dir): + os.makedirs(dir) + n = pypsa.Network(snakemake.input.network) df = ( @@ -98,15 +99,9 @@ def plot_choropleth( regions_offshore = gpd.read_file(snakemake.input.regions_offshore).set_index("name") - plot_choropleth(df, regions_onshore, "onwind", vmax=50, dir=snakemake.output[0]) - plot_choropleth( - df, regions_onshore, "solar", "Oranges", vmax=15, dir=snakemake.output[0] - ) - plot_choropleth(df, regions_onshore, "ror", "GnBu", dir=snakemake.output[0]) + plot_choropleth(df, regions_onshore, "onwind", vmax=50, dir=dir) + plot_choropleth(df, regions_onshore, "solar", "Oranges", vmax=15, dir=dir) + plot_choropleth(df, regions_onshore, "ror", "GnBu", dir=dir) - plot_choropleth( - df, regions_offshore, "offwind-dc", vmax=50, dir=snakemake.output[0] - ) - plot_choropleth( - df, regions_offshore, "offwind-ac", vmax=50, dir=snakemake.output[0] - ) + plot_choropleth(df, regions_offshore, "offwind-dc", vmax=50, dir=dir) + plot_choropleth(df, regions_offshore, "offwind-ac", vmax=50, dir=dir) From 5d337202f79c4958f25382dcc282d21f57ae3b94 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 25 Oct 2023 17:05:19 +0200 Subject: [PATCH 221/293] plot_import_options: adjust borders --- scripts/plot_import_options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/plot_import_options.py b/scripts/plot_import_options.py index 7899133a5..a333a2ada 100644 --- a/scripts/plot_import_options.py +++ b/scripts/plot_import_options.py @@ -111,7 +111,7 @@ line_widths=0.75, link_widths=0.75, link_colors=link_colors, - boundaries=[-11, 48, 26.5, 70], + boundaries=[-11, 48, 25.25, 71.5], margin=0, ) From e63a6ca3544723181593e0b04eb7be2525704c38 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 25 Oct 2023 17:06:13 +0200 Subject: [PATCH 222/293] plot: minor formatting changes --- scripts/plot_hydrogen_network.py | 6 +++--- scripts/plot_power_network.py | 3 +-- scripts/plot_renewable_potential_unclustered.py | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/scripts/plot_hydrogen_network.py b/scripts/plot_hydrogen_network.py index 73f167b99..088c6fdea 100644 --- a/scripts/plot_hydrogen_network.py +++ b/scripts/plot_hydrogen_network.py @@ -16,9 +16,9 @@ import pandas as pd import pypsa from _helpers import configure_logging +from plot_power_network import assign_location from pypsa.plot import add_legend_circles, add_legend_lines, add_legend_patches -from plot_power_network import assign_location def group_pipes(df, drop_direction=False): """ @@ -240,8 +240,8 @@ def plot_h2_map(n, regions): ax.set_facecolor("white") - for fn in snakemake.output[0]: - plt.savefig(fn) + for fn in snakemake.output: + plt.savefig(fn, bbox_inches="tight") if __name__ == "__main__": diff --git a/scripts/plot_power_network.py b/scripts/plot_power_network.py index c1e01913e..b751dc1aa 100644 --- a/scripts/plot_power_network.py +++ b/scripts/plot_power_network.py @@ -57,7 +57,7 @@ def assign_location(n): if i == -1: continue names = ifind.index[ifind == i] - c.df.loc[names, "location"] = names.str[:i] + c.df.loc[names, "location"] = names.str[:i] def plot_map( @@ -273,4 +273,3 @@ def plot_map( map_opts["boundaries"] = regions.total_bounds[[0, 2, 1, 3]] + [-1, 1, -1, 1] plot_map(n) - diff --git a/scripts/plot_renewable_potential_unclustered.py b/scripts/plot_renewable_potential_unclustered.py index 59ba196a1..982628a2a 100644 --- a/scripts/plot_renewable_potential_unclustered.py +++ b/scripts/plot_renewable_potential_unclustered.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: MIT """ -Plot unclustered wind and solar renewable potentials. +Plot unclustered wind and solar renewable potentials and capacity factors. """ import cartopy From 8a4f2a95956d63572130e5f57138e989c428b53e Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 25 Oct 2023 17:06:52 +0200 Subject: [PATCH 223/293] plot_import_shares: add minor ticks --- scripts/plot_import_shares.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/plot_import_shares.py b/scripts/plot_import_shares.py index d52c4f24f..0c535b516 100644 --- a/scripts/plot_import_shares.py +++ b/scripts/plot_import_shares.py @@ -71,6 +71,10 @@ secax = ax.secondary_xaxis('top', functions=(lambda x: 100 - x, lambda x: 100 -x)) secax.set_xlabel('import share [%]', fontsize=11, color='coral') + ticks = range(10, 100, 20) + ax.set_xticks(ticks, minor=True) + secax.set_xticks(ticks, minor=True) + for i in ["top", "right", "left", "bottom"]: secax.spines[i].set_visible(False) ax.spines[i].set_visible(False) From 2b976ba1a2e54e5445c99f8ca55e64ba099b54b6 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 27 Oct 2023 19:48:26 +0200 Subject: [PATCH 224/293] import_links: ensure to drop potential duplicate connections --- scripts/prepare_sector_network.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 920660945..9e95de311 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3836,6 +3836,8 @@ def _coordinates(ct): import_links = pd.concat([import_links, pd.Series(xlinks)], axis=0) import_links = import_links.drop_duplicates(keep="first") + duplicated = import_links.index.duplicated(keep='first') + import_links = import_links.loc[~duplicated] hvdc_cost = ( import_links.values * cf["length_factor"] * costs.at["HVDC submarine", "fixed"] From b925c029ebb7b8e4d8c0afa04a7808fc15ded3fb Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Sun, 29 Oct 2023 11:52:38 +0100 Subject: [PATCH 225/293] add collection rules for plotting scripts --- rules/plot.smk | 52 +++++++++++++++++-- scripts/_helpers.py | 8 +++ scripts/plot_balance_timeseries.py | 7 +-- scripts/plot_choropleth_capacities.py | 17 ++++-- scripts/plot_choropleth_capacity_factors.py | 14 +++-- ...plot_choropleth_capacity_factors_sector.py | 3 ++ scripts/plot_choropleth_demand.py | 8 +-- scripts/plot_choropleth_potential_used.py | 3 ++ scripts/plot_choropleth_prices.py | 10 ++-- scripts/plot_gas_network.py | 6 +-- scripts/plot_heatmap_timeseries.py | 8 ++- scripts/plot_heatmap_timeseries_resources.py | 7 +-- scripts/plot_import_options.py | 24 ++++++--- scripts/plot_power_network.py | 4 +- 14 files changed, 118 insertions(+), 53 deletions(-) diff --git a/rules/plot.smk b/rules/plot.smk index d257e74fb..d2d659972 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -194,8 +194,7 @@ rule plot_choropleth_prices: regions_offshore=RESOURCES + "regions_offshore_elec_s{simpl}_{clusters}.geojson", rc="matplotlibrc", output: - market_prices=directory(RESULTS + "graphics/market_prices/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}"), - market_values=directory(RESULTS + "graphics/market_values/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}"), + directory(RESULTS + "graphics/prices/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}"), script: "../scripts/plot_choropleth_prices.py" @@ -224,7 +223,7 @@ rule plot_choropleth_demand: regions_onshore=RESOURCES + "regions_onshore_elec_s{simpl}_{clusters}.geojson", rc="matplotlibrc", output: - directory(RESOURCES + "graphics/regional_demand/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}"), + directory(RESULTS + "graphics/regional_demand/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}"), script: "../scripts/plot_choropleth_demand.py" @@ -259,7 +258,7 @@ rule plot_heatmap_timeseries_resources: rc="matplotlibrc", threads: 12 output: - directory(RESULTS + "graphics/heatmap_timeseries/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}") + directory(RESULTS + "graphics/heatmap_timeseries_resources/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}") script: "../scripts/plot_heatmap_timeseries_resources.py" @@ -379,7 +378,7 @@ rule plot_import_networks: output: multiext(RESULTS + "graphics/import_networks/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", ".png", ".pdf") script: - "../scripts/plot_import_networks.py" + "../scripts/plot_import_network.py" rule plot_import_shares: @@ -391,3 +390,46 @@ rule plot_import_shares: multiext(RESULTS + "graphics/import_shares/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", ".png", ".pdf") script: "../scripts/plot_import_shares.py" + + +rule plot_all_resources: + input: + rules.plot_power_network_unclustered.output, + rules.plot_gas_network_unclustered.output, + rules.plot_renewable_potential_unclustered.output, + rules.plot_weather_data_map.output, + rules.plot_industrial_sites.output, + rules.plot_powerplants.output, + rules.plot_salt_caverns_unclustered.output, + rules.plot_import_world_map.output, + expand(RESOURCES + "graphics/salt-caverns-s{simpl}-{clusters}-onshore.pdf", **config["scenario"]), + expand(RESOURCES + "graphics/power-network-{clusters}.pdf", **config["scenario"]), + expand(RESOURCES + "graphics/biomass-potentials-s{simpl}-{clusters}-biogas.pdf", **config["scenario"]), + expand(RESOURCES + "graphics/capacity-factor/s{simpl}-{clusters}", **config["scenario"]), + expand(RESOURCES + "graphics/capacity-factor-sector/s{simpl}-{clusters}", **config["scenario"]), + + +rule plot_all_results_single: + input: + RESULTS + "graphics/p_nom_opt/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", + RESULTS + "graphics/prices/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", + RESULTS + "graphics/potential_used/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", + RESULTS + "graphics/balance_timeseries/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", + RESULTS + "graphics/heatmap_timeseries/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", + RESULTS + "graphics/heatmap_timeseries_resources/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", + RESULTS + "graphics/regional_demand/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", + RESULTS + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-costs-all_{planning_horizons}.pdf", + RESULTS + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-h2_network_{planning_horizons}.pdf", + RESULTS + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-ch4_network_{planning_horizons}.pdf", + RESULTS + "graphics/import_options_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.pdf", + RESULTS + "graphics/import_networks/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.pdf", + RESULTS + "graphics/import_shares/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.pdf", + output: + RESULTS + "graphics/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.touch" + shell: + "touch {output}" + + +rule plot_all_results: + input: + expand(RESULTS + "graphics/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.touch", **config["scenario"]), diff --git a/scripts/_helpers.py b/scripts/_helpers.py index fc7bc9e0b..339c8b756 100644 --- a/scripts/_helpers.py +++ b/scripts/_helpers.py @@ -29,6 +29,14 @@ def mute_print(): yield +def ensure_output_dir_exists(snakemake): + # ensure path exists, since snakemake does not create path for directory outputs + # https://github.com/snakemake/snakemake/issues/774 + dir = snakemake.output[0] + if not os.path.exists(dir): + os.makedirs(dir) + + def configure_logging(snakemake, skip_handlers=False): """ Configure the basic behaviour for the logging module. diff --git a/scripts/plot_balance_timeseries.py b/scripts/plot_balance_timeseries.py index 7707e2d8c..e760706dc 100644 --- a/scripts/plot_balance_timeseries.py +++ b/scripts/plot_balance_timeseries.py @@ -7,7 +7,6 @@ """ import logging -import os from multiprocessing import Pool import matplotlib.dates as mdates @@ -15,6 +14,7 @@ import numpy as np import pandas as pd import pypsa +from _helpers import ensure_output_dir_exists logger = logging.getLogger(__name__) @@ -180,11 +180,8 @@ def plot_energy_balance_timeseries( configfiles="../../config/config.100n-seg.yaml", ) - # ensure path exists, since snakemake does not create path for directory outputs - # https://github.com/snakemake/snakemake/issues/774 + ensure_output_dir_exists(snakemake) dir = snakemake.output[0] - if not os.path.exists(dir): - os.makedirs(dir) plt.style.use(["bmh", snakemake.input.rc]) diff --git a/scripts/plot_choropleth_capacities.py b/scripts/plot_choropleth_capacities.py index 019f6c391..379a36a28 100644 --- a/scripts/plot_choropleth_capacities.py +++ b/scripts/plot_choropleth_capacities.py @@ -11,7 +11,9 @@ import numpy as np import pandas as pd import pypsa +from _helpers import ensure_output_dir_exists from plot_choropleth_capacity_factors import plot_choropleth +from plot_power_network import assign_location ROUNDER = 10 @@ -89,7 +91,9 @@ def get_optimal_capacity(n): ] ) - p_nom_opt = p_nom_opt.unstack().drop(["", "EU"]).dropna(how="all", axis=1) + # drop import locations and copperplated buses + to_drop = list(n.buses.index[n.buses.index.str.len() == 2]) + ["", "EU", "process"] + p_nom_opt = p_nom_opt.drop(to_drop).unstack().dropna(how="all", axis=1) p_nom_opt = p_nom_opt.loc[:, ~p_nom_opt.columns.isin(IGNORE_LINKS)] @@ -103,16 +107,23 @@ def get_optimal_capacity(n): snakemake = mock_snakemake( "plot_choropleth_capacities", simpl="", - clusters=128, - configfiles=["../../config/config.test.yaml"], + clusters=110, + ll='vopt', + opts="", + sector_opts="Co2L0-2190SEG-T-H-B-I-S-A-onwind+p0.5-imp", + planning_horizons=2050, + configfiles=["../../config/config.20231025-zecm.yaml"], ) plt.style.use(snakemake.input.rc) + ensure_output_dir_exists(snakemake) + regions_onshore = gpd.read_file(snakemake.input.regions_onshore).set_index("name") regions_offshore = gpd.read_file(snakemake.input.regions_offshore).set_index("name") n = pypsa.Network(snakemake.input.network) + assign_location(n) p_nom_opt = get_optimal_capacity(n) diff --git a/scripts/plot_choropleth_capacity_factors.py b/scripts/plot_choropleth_capacity_factors.py index 771c971a6..1ddf8a519 100644 --- a/scripts/plot_choropleth_capacity_factors.py +++ b/scripts/plot_choropleth_capacity_factors.py @@ -6,13 +6,12 @@ Plot average capacity factor map. """ -import os - import cartopy import cartopy.crs as ccrs import geopandas as gpd import matplotlib.pyplot as plt import pypsa +from _helpers import ensure_output_dir_exists def plot_choropleth( @@ -62,8 +61,10 @@ def plot_choropleth( carrier_fn = carrier.replace("-", "_").replace(" ", "_") fn = f"map-{carrier_fn}" - plt.savefig(dir + "/" + fn + ".pdf", bbox_inches="tight") - plt.savefig(dir + "/" + fn + ".png", bbox_inches="tight") + if not dir.endswith("-"): + dir += "/" + plt.savefig(dir + fn + ".pdf", bbox_inches="tight") + plt.savefig(dir + fn + ".png", bbox_inches="tight") plt.close() @@ -79,11 +80,8 @@ def plot_choropleth( plt.style.use(snakemake.input.rc) - # ensure path exists, since snakemake does not create path for directory outputs - # https://github.com/snakemake/snakemake/issues/774 + ensure_output_dir_exists(snakemake) dir = snakemake.output[0] - if not os.path.exists(dir): - os.makedirs(dir) n = pypsa.Network(snakemake.input.network) diff --git a/scripts/plot_choropleth_capacity_factors_sector.py b/scripts/plot_choropleth_capacity_factors_sector.py index 713dd81e1..f5198be0c 100644 --- a/scripts/plot_choropleth_capacity_factors_sector.py +++ b/scripts/plot_choropleth_capacity_factors_sector.py @@ -10,6 +10,7 @@ import matplotlib.pyplot as plt import pandas as pd import xarray as xr +from _helpers import ensure_output_dir_exists from plot_choropleth_capacity_factors import plot_choropleth if __name__ == "__main__": @@ -25,6 +26,8 @@ plt.style.use(snakemake.input.rc) + ensure_output_dir_exists(snakemake) + regions_onshore = gpd.read_file(snakemake.input.regions_onshore).set_index("name") df = pd.DataFrame() diff --git a/scripts/plot_choropleth_demand.py b/scripts/plot_choropleth_demand.py index 1f47b9757..5e42c8f43 100644 --- a/scripts/plot_choropleth_demand.py +++ b/scripts/plot_choropleth_demand.py @@ -6,14 +6,13 @@ Plot choropleth regional demands. """ -import os - import cartopy import cartopy.crs as ccrs import geopandas as gpd import matplotlib.pyplot as plt import pandas as pd import pypsa +from _helpers import ensure_output_dir_exists from pypsa.descriptors import get_switchable_as_dense as as_dense TITLES = { @@ -217,11 +216,8 @@ def plot_regional_demands(df, geodf, carrier, dir="."): plt.style.use(snakemake.input.rc) - # ensure path exists, since snakemake does not create path for directory outputs - # https://github.com/snakemake/snakemake/issues/774 + ensure_output_dir_exists(snakemake) dir = snakemake.output[0] - if not os.path.exists(dir): - os.makedirs(dir) n = pypsa.Network(snakemake.input.network) diff --git a/scripts/plot_choropleth_potential_used.py b/scripts/plot_choropleth_potential_used.py index f98ba39c9..1d3908b39 100644 --- a/scripts/plot_choropleth_potential_used.py +++ b/scripts/plot_choropleth_potential_used.py @@ -9,6 +9,7 @@ import geopandas as gpd import matplotlib.pyplot as plt import pypsa +from _helpers import ensure_output_dir_exists from plot_choropleth_capacity_factors import plot_choropleth POTENTIAL = [ @@ -43,6 +44,8 @@ def get_potential_used(n): plt.style.use(snakemake.input.rc) + ensure_output_dir_exists(snakemake) + regions_onshore = gpd.read_file(snakemake.input.regions_onshore).set_index("name") regions_offshore = gpd.read_file(snakemake.input.regions_offshore).set_index("name") diff --git a/scripts/plot_choropleth_prices.py b/scripts/plot_choropleth_prices.py index 9c505e485..04fdd777a 100644 --- a/scripts/plot_choropleth_prices.py +++ b/scripts/plot_choropleth_prices.py @@ -11,6 +11,7 @@ import numpy as np import pandas as pd import pypsa +from _helpers import ensure_output_dir_exists from plot_choropleth_capacity_factors import plot_choropleth ROUNDER = 20 @@ -64,7 +65,8 @@ def get_market_values(n): ] ) - mv = mv.unstack().drop(["", "EU"]).dropna(how="all", axis=1) + to_drop = list(n.buses.index[n.buses.index.str.len() == 2]) + ["", "EU", "process"] + mv = mv.drop(to_drop, errors='ignore').unstack().dropna(how="all", axis=1) return mv @@ -82,6 +84,8 @@ def get_market_values(n): plt.style.use(snakemake.input.rc) + ensure_output_dir_exists(snakemake) + regions_onshore = gpd.read_file(snakemake.input.regions_onshore).set_index("name") regions_offshore = gpd.read_file(snakemake.input.regions_offshore).set_index("name") @@ -102,7 +106,7 @@ def get_market_values(n): vmin=vmins[carrier], label="average market price [€/MWh]", title=n.carriers.at[carrier, "nice_name"], - dir=snakemake.output.market_prices, + dir=snakemake.output[0] + "/market-prices-", ) mv = get_market_values(n) @@ -126,5 +130,5 @@ def get_market_values(n): vmin=vmins[carrier], label="average market value [€/MWh]", title=n.carriers.at[carrier, "nice_name"], - dir=snakemake.output.market_values, + dir=snakemake.output[0] + "/market-values-", ) diff --git a/scripts/plot_gas_network.py b/scripts/plot_gas_network.py index d73fd3b49..8a54178cd 100644 --- a/scripts/plot_gas_network.py +++ b/scripts/plot_gas_network.py @@ -16,9 +16,9 @@ import pandas as pd import pypsa from _helpers import configure_logging +from plot_power_network import assign_location from pypsa.plot import add_legend_circles, add_legend_lines, add_legend_patches -from plot_power_network import assign_location def plot_ch4_map(n): # if "gas pipeline" not in n.links.carrier.unique(): @@ -220,8 +220,8 @@ def plot_ch4_map(n): legend_kw=legend_kw, ) - for fn in snakemake.output[0]: - plt.savefig(fn) + for fn in snakemake.output: + plt.savefig(fn, bbox_inches="tight") if __name__ == "__main__": diff --git a/scripts/plot_heatmap_timeseries.py b/scripts/plot_heatmap_timeseries.py index ec11c2c4c..945285dc5 100644 --- a/scripts/plot_heatmap_timeseries.py +++ b/scripts/plot_heatmap_timeseries.py @@ -7,13 +7,13 @@ """ import logging -import os from multiprocessing import Pool import matplotlib.pyplot as plt import pandas as pd import pypsa import seaborn as sns +from _helpers import ensure_output_dir_exists logger = logging.getLogger(__name__) @@ -137,11 +137,9 @@ def plot_heatmap( plt.style.use(["bmh", snakemake.input.rc]) - # ensure path exists, since snakemake does not create path for directory outputs - # https://github.com/snakemake/snakemake/issues/774 + ensure_output_dir_exists(snakemake) + dir = snakemake.output[0] - if not os.path.exists(dir): - os.makedirs(dir) n = pypsa.Network(snakemake.input.network) diff --git a/scripts/plot_heatmap_timeseries_resources.py b/scripts/plot_heatmap_timeseries_resources.py index a18ec5039..04868bf0a 100644 --- a/scripts/plot_heatmap_timeseries_resources.py +++ b/scripts/plot_heatmap_timeseries_resources.py @@ -7,11 +7,11 @@ """ import logging -import os from multiprocessing import Pool import matplotlib.pyplot as plt import pypsa +from _helpers import ensure_output_dir_exists logger = logging.getLogger(__name__) @@ -34,11 +34,8 @@ plt.style.use(["bmh", snakemake.input.rc]) - # ensure path exists, since snakemake does not create path for directory outputs - # https://github.com/snakemake/snakemake/issues/774 + ensure_output_dir_exists(snakemake) dir = snakemake.output[0] - if not os.path.exists(dir): - os.makedirs(dir) n = pypsa.Network(snakemake.input.network) diff --git a/scripts/plot_import_options.py b/scripts/plot_import_options.py index a333a2ada..be04c88d5 100644 --- a/scripts/plot_import_options.py +++ b/scripts/plot_import_options.py @@ -27,17 +27,25 @@ "plot_import_options", simpl="", opts="", - clusters="100", - ll="v1.5", - sector_opts="Co2L0-2190SEG-T-H-B-I-S-A-imp", + clusters="110", + ll="vopt", + sector_opts="Co2L0-2190SEG-T-H-B-I-S-A-imp+AC", planning_horizons="2050", - configfiles="../../config/config.100n-seg.yaml", + configfiles="../../config/config.20231025-zecm.yaml", ) configure_logging(snakemake) plt.style.use(snakemake.input.rc) + # dummy output if no imports considered + if "-imp" not in snakemake.wildcards.sector_opts: + import sys + fig, ax = plt.subplots() + for fn in snakemake.output: + plt.savefig(fn, bbox_inches="tight") + sys.exit(0) + tech_colors = snakemake.config["plotting"]["tech_colors"] tech_colors["lng"] = "tomato" tech_colors["pipeline"] = "orchid" @@ -200,10 +208,10 @@ translate = { "import shipping-ftfuel": "€/MWh FT", "import shipping-meoh": "€/MWh MeOh", - "import pipeline-h2": r"€/MWh H2$_{(g)}$", - "import shipping-lh2": r"€/MWh H2$_{(l)}$", - "import shipping-lch4": r"€/MWh CH4$_{(l)}$", - "import shipping-lnh3": r"€/MWh NH3$_{(l)}$", + "import pipeline-h2": r"€/MWh H$_{2 (g)}$", + "import shipping-lh2": r"€/MWh H$_{2 (l)}$", + "import shipping-lch4": r"€/MWh CH$_{4 (l)}$", + "import shipping-lnh3": r"€/MWh NH$_{3 (l)}$", "import shipping-steel": r"€/t steel", } text = "" diff --git a/scripts/plot_power_network.py b/scripts/plot_power_network.py index b751dc1aa..5c7b1abb2 100644 --- a/scripts/plot_power_network.py +++ b/scripts/plot_power_network.py @@ -241,8 +241,8 @@ def plot_map( legend_kw=legend_kw, ) - for fn in snakemake.output[0]: - plt.savefig(fn) + for fn in snakemake.output: + plt.savefig(fn, bbox_inches="tight") if __name__ == "__main__": From 8f46ba35d2019dbf5d22fa36281ef7f5fa52ac75 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 1 Nov 2023 18:16:06 +0100 Subject: [PATCH 226/293] update import options plotting (stripplot) --- rules/plot.smk | 1 + scripts/plot_import_options.py | 194 ++++++++++++++++++++++----------- scripts/plot_import_shares.py | 2 +- 3 files changed, 130 insertions(+), 67 deletions(-) diff --git a/rules/plot.smk b/rules/plot.smk index d2d659972..0319ccff9 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -342,6 +342,7 @@ rule plot_import_options: + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", regions=RESOURCES + "regions_onshore_elec_s{simpl}_{clusters}.geojson", entrypoints=RESOURCES + "gas_input_locations_s{simpl}_{clusters}_simplified.csv", + imports="data/imports/results.csv", rc="matplotlibrc", output: multiext(RESULTS + "graphics/import_options_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", ".png", ".pdf") diff --git a/scripts/plot_import_options.py b/scripts/plot_import_options.py index be04c88d5..226a2c401 100644 --- a/scripts/plot_import_options.py +++ b/scripts/plot_import_options.py @@ -11,14 +11,95 @@ logger = logging.getLogger(__name__) import cartopy.crs as ccrs +import cartopy.feature as cfeature +import country_converter as coco import geopandas as gpd import matplotlib.pyplot as plt +import numpy as np import pandas as pd import pypsa +import seaborn as sns from _helpers import configure_logging from plot_power_network import assign_location from pypsa.plot import add_legend_circles, add_legend_patches +cc = coco.CountryConverter() + +NICE_NAMES = { + "pipeline-h2": r"H$_2$ (pipeline)", + "shipping-lh2": "H$_2$ (ship)", + "shipping-lnh3": "ammonia", + "shipping-lch4": "methane", + "shipping-meoh": "methanol", + "shipping-ftfuel": "Fischer-Tropsch", + "shipping-steel": "steel", +} + +PALETTE = { + "Argentina": "#74acdf", + "Algeria": "#d21034", + "Namibia": "#003580", + "Saudi Arabia": "#006c35", + "Chile": "darkorange", + "Other": "#aaa" +} + +def create_stripplot(ic, ax): + + order = list(NICE_NAMES.values())[:-1] + minimums = ic.groupby("esc").value.min().round(1)[order].reset_index(drop=True).to_dict() + maximums = ic.groupby("esc").value.max().round(1)[order].reset_index(drop=True).to_dict() + + sns.stripplot( + data=ic, + x='esc', + y='value', + alpha=0.5, + hue='exporter', + jitter=.28, + palette=PALETTE, + ax=ax, + order=order, + size=3.5 + ) + sns.violinplot( + data=ic, + x='esc', + y='value', + linewidth=0, + saturation=0.3, + cut=0, + color='#ddd', + fill=True, + ax=ax, + order=order, + zorder=-1 + ) + + ax.set_ylim(0, 190) + ax.set_xlabel("") + ax.set_ylabel("import cost [€/MWh]") + ax.grid(False) + ax.set_yticks(np.arange(0, 200, 20)) + ax.set_yticks(np.arange(10, 200, 20), minor=True) + ax.set_xticklabels(ax.get_xticklabels(), rotation=18, ha="right") + handles, labels = ax.get_legend_handles_labels() + handles.reverse() + labels.reverse() + for x, y in minimums.items(): + ax.text(x, y - 10, str(y), ha="center", va="bottom", fontsize=9) + for x, y in maximums.items(): + ax.text(x, y + 5, str(y), ha="center", va="bottom", fontsize=9) + ax.legend( + title="", + ncol=1, + loc=(0.55, 0.05), + labelspacing=0.3, + frameon=False + ) + for spine in ax.spines.values(): + spine.set_visible(False) + if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake @@ -36,7 +117,7 @@ configure_logging(snakemake) - plt.style.use(snakemake.input.rc) + plt.style.use(["bmh", snakemake.input.rc]) # dummy output if no imports considered if "-imp" not in snakemake.wildcards.sector_opts: @@ -48,7 +129,7 @@ tech_colors = snakemake.config["plotting"]["tech_colors"] tech_colors["lng"] = "tomato" - tech_colors["pipeline"] = "orchid" + tech_colors["pipeline"] = "darksalmon" crs = ccrs.EqualEarth() @@ -89,7 +170,7 @@ link_colors = pd.Series( n.links.index.map( - lambda x: "olivedrab" if "import hvdc-to-elec" in x else "tan" + lambda x: "seagreen" if "import hvdc-to-elec" in x else "darkseagreen" ), index=n.links.index, ) @@ -105,19 +186,30 @@ ] mi = pd.MultiIndex.from_product([exporters, techs]) bus_sizes_plain = pd.concat( - [pd.Series(0.3, index=mi), inputs.div(bus_size_factor)], axis=0 + [pd.Series(0.4, index=mi), inputs.div(bus_size_factor)], axis=0 ) - fig, ax = plt.subplots(subplot_kw={"projection": crs}, figsize=(8, 12)) + df = pd.read_csv(snakemake.input.imports, sep=";", keep_default_na=False) + + df["exporter"] = df.exporter.replace("", "NA") + ic = df.query("subcategory == 'Cost per MWh delivered' and esc != 'hvdc-to-elec'") + ic["exporter"] = ic.exporter.str.split("-").str[0] + + highlighted_countries = ["DZ", "AR", "SA", "CL"] + ic["exporter"] = ic.exporter.apply(lambda x: cc.convert(names=x, to="name_short") if x in highlighted_countries else "Other") + + ic["esc"] = ic.esc.map(NICE_NAMES) + + fig, ax = plt.subplots(subplot_kw={"projection": crs}, figsize=(14, 14)) n.plot( ax=ax, color_geomap={"ocean": "white", "land": "#efefef"}, bus_sizes=bus_sizes_plain, bus_colors=tech_colors, - line_colors="tan", - line_widths=0.75, - link_widths=0.75, + line_colors="darkseagreen", + line_widths=1, + link_widths=1, link_colors=link_colors, boundaries=[-11, 48, 25.25, 71.5], margin=0, @@ -127,14 +219,16 @@ ax=ax, column="marginal_cost", cmap="Blues_r", - linewidths=0, + edgecolor='#ddd', + linewidths=0.5, vmin=50, vmax=100, legend=True, legend_kwds={ "label": r"H$_2$ import cost [€/MWh]", - "shrink": 0.35, + "shrink": 0.48, "pad": 0.015, + "aspect": 35, }, ) @@ -144,11 +238,11 @@ "external battery": "battery storage", "external H2": "hydrogen storage", } - labels = list(names.values()) + ["HVDC import link", "internal power line"] - colors = [tech_colors[c] for c in names.keys()] + ["olivedrab", "tan"] + labels = list(names.values()) + ["HVDC import link", "internal power line", "LNG terminal", "pipeline entry"] + colors = [tech_colors[c] for c in names.keys()] + ["seagreen", "darkseagreen", "tomato", "darksalmon"] legend_kw = dict( - bbox_to_anchor=(1.51, 1.03), frameon=False, title="electricity imports" + loc=(1.2, 0.85), frameon=False, title="", ncol=2 ) add_legend_patches( @@ -159,70 +253,38 @@ ) legend_kw = dict( - bbox_to_anchor=(1.46, 0.66), frameon=False, title="H$_2$ and CH$_4$ imports" + loc=(1.2, 0.72), + frameon=False, + title="existing gas import capacity", + ncol=3, + labelspacing=1.1, ) - names = { - "lng": "LNG terminal", - "pipeline": "pipeline entry", - } - labels = list(names.values()) - colors = [tech_colors[c] for c in names.keys()] - - add_legend_patches( + add_legend_circles( ax, - colors, - labels, + [10e3 / bus_size_factor, 50e3 / bus_size_factor, 100e3 / bus_size_factor], + ["10 GW", "50 GW", "100 GW"], + patch_kw=dict(facecolor="darksalmon"), legend_kw=legend_kw, ) - legend_kw = dict( - bbox_to_anchor=(1.42, 0.47), - frameon=False, - title="import capacity", + ax.add_feature( + cfeature.BORDERS.with_scale("50m"), + linewidth=.75, + color='k', ) - add_legend_circles( - ax, - [100e3 / bus_size_factor], - ["100 GW"], - patch_kw=dict(facecolor="lightgrey"), - legend_kw=legend_kw, + ax.add_feature( + cfeature.COASTLINE.with_scale("50m"), + linewidth=.75, + color='k', ) - cost_range = ( - pd.concat( - [ - c.df.filter(like="import", axis=0) - .groupby("carrier") - .marginal_cost.describe() - for c in n.iterate_components({"Link", "Generator"}) - ] - ) - .drop("import hvdc-to-elec") - .sort_values(by="min")[["min", "max"]] - .astype(int) - .T - ) + ax_lr = ax.inset_axes([1.2, 0.08, 0.45, 0.59]) - translate = { - "import shipping-ftfuel": "€/MWh FT", - "import shipping-meoh": "€/MWh MeOh", - "import pipeline-h2": r"€/MWh H$_{2 (g)}$", - "import shipping-lh2": r"€/MWh H$_{2 (l)}$", - "import shipping-lch4": r"€/MWh CH$_{4 (l)}$", - "import shipping-lnh3": r"€/MWh NH$_{3 (l)}$", - "import shipping-steel": r"€/t steel", - } - text = "" - for carrier, values in cost_range.items(): - if abs(values["min"] - values["max"]) < 1: - value = str(values["min"]) - else: - value = str(values["min"]) + "-" + str(values["max"]) - text += value + " " + translate[carrier] + "\n" - - ax.text(1.2, 0.0, text, transform=ax.transAxes, linespacing=1.2) + create_stripplot(ic, ax_lr) + + plt.tight_layout() for fn in snakemake.output: plt.savefig(fn, bbox_inches="tight") diff --git a/scripts/plot_import_shares.py b/scripts/plot_import_shares.py index 0c535b516..78d888932 100644 --- a/scripts/plot_import_shares.py +++ b/scripts/plot_import_shares.py @@ -54,7 +54,7 @@ ie = ie / ie.sum() * 100 - fig, ax = plt.subplots(figsize=(8, 4)) + fig, ax = plt.subplots(figsize=(6, 4)) sel = list(CARRIERS.keys())[::-1] ie[sel].rename(columns=CARRIERS).T.plot.barh( stacked=True, From 0c1274a348f4d5828165459cc28574c308c46a3a Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Tue, 14 Nov 2023 17:56:58 +0100 Subject: [PATCH 227/293] plot_import_shares: add import mix as subplot --- scripts/plot_import_shares.py | 73 ++++++++++++++++++++++++++++------- 1 file changed, 60 insertions(+), 13 deletions(-) diff --git a/scripts/plot_import_shares.py b/scripts/plot_import_shares.py index 78d888932..1d6f2eb4a 100644 --- a/scripts/plot_import_shares.py +++ b/scripts/plot_import_shares.py @@ -11,6 +11,7 @@ logger = logging.getLogger(__name__) import matplotlib.pyplot as plt +import pandas as pd import pypsa from _helpers import configure_logging @@ -24,6 +25,16 @@ "steel": "steel", } +COLOR_MAPPING = { + "electricity": "import hvdc-to-elec", + "hydrogen": "import pipeline-h2", + "ammonia": "import shipping-lnh3", + "methane": "import shipping-lch4", + "methanol": "import shipping-meoh", + "Fischer-Tropsch": "import shipping-ftfuel", + "steel": "import shipping-steel", +} + THRESHOLD = 1 # MWh if __name__ == "__main__": @@ -34,50 +45,80 @@ "plot_import_shares", simpl="", opts="", - clusters="100", + clusters="110", ll="vopt", - sector_opts="Co2L0-2190SEG-T-H-B-I-S-A-imp", + sector_opts="Co2L0-2190SEG-T-H-B-I-S-A-onwind+p0.5-imp", planning_horizons="2050", - configfiles="../../config/config.20230926-zecm.yaml", + configfiles="../../config/config.20231025-zecm.yaml", ) configure_logging(snakemake) plt.style.use(['bmh', snakemake.input.rc]) + tech_colors = snakemake.config["plotting"]["tech_colors"] + n = pypsa.Network(snakemake.input.network) eb = n.statistics.energy_balance().groupby(["carrier", "bus_carrier"]).sum() - supply = eb.where(eb > THRESHOLD).dropna().div(1e6) + eb = eb.unstack(1).groupby(lambda x: "import" if "import" in x or "external" in x else x).sum() + supply = eb.where(eb > THRESHOLD).dropna(how='all', axis=0).div(1e6) - ie = supply.unstack(1).groupby(lambda x: "import" if "import" in x or "external" in x else "domestic").sum() - ie = ie / ie.sum() * 100 + ie = supply.groupby(lambda x: "import" if x == "import" else "domestic").sum() + ie_rel = ie / ie.sum() * 100 + ie_sum = ie.sum().astype(int) + imp_mix = ie.loc["import"].copy() + if "steel" in imp_mix.index: + imp_mix.loc["steel"] *= 2.1 # kWh/kg + imp_mix = pd.DataFrame(imp_mix.where(imp_mix > 1e-3).dropna().rename(index=CARRIERS).sort_values(ascending=False)) + imp_mix.columns = ["import mix"] - fig, ax = plt.subplots(figsize=(6, 4)) + fig, (ax, ax_mix) = plt.subplots(2, 1, figsize=(6, 5), gridspec_kw=dict(height_ratios=[5.5, 1])) sel = list(CARRIERS.keys())[::-1] - ie[sel].rename(columns=CARRIERS).T.plot.barh( + ie_rel[sel].rename(columns=CARRIERS).T.plot.barh( stacked=True, ax=ax, color=['lightseagreen', 'coral'] ) - plt.ylabel("") - plt.xlabel("domestic share [%]", fontsize=11, color='lightseagreen') - plt.legend(ncol=2, bbox_to_anchor=(0.35, 1.25)) - plt.grid(axis="y") - plt.xlim(0,100) + ax.set_ylabel("") + ax.set_xlabel("domestic share [%]", fontsize=11, color='lightseagreen') + ax.legend(ncol=2, bbox_to_anchor=(0.35, 1.25)) + ax.grid(axis="y") + ax.set_xlim(0,100) + + imp_mix.T.plot.barh(ax=ax_mix, stacked=True, legend=True, color=[tech_colors[COLOR_MAPPING[i]]for i in imp_mix.index]) + + for i, (carrier, twh) in enumerate(ie_sum[sel].items()): + unit = "Mt" if carrier.lower().startswith("steel") else "TWh" + ax.text(119, i, f"{twh} {unit}", va='center', ha='right', color='slateblue') + ax.text(119, i + 1.5, "total\nsupply", va='center', ha='right', color='slateblue') secax = ax.secondary_xaxis('top', functions=(lambda x: 100 - x, lambda x: 100 -x)) secax.set_xlabel('import share [%]', fontsize=11, color='coral') + total_imp = imp_mix.sum().sum() + secax_mix = ax_mix.secondary_xaxis('top', functions=(lambda x: x / total_imp * 100, lambda x: x * total_imp * 100)) + + + ax_mix.text(total_imp * 1.1, -0.75, "TWh", va='center') + ax_mix.text(total_imp * 1.1, 0.75, "%", va='center') + ax_mix.legend(ncol=4, bbox_to_anchor=(1.15, -0.3), title="") + ticks = range(10, 100, 20) ax.set_xticks(ticks, minor=True) secax.set_xticks(ticks, minor=True) + secax_mix.set_xticks(ticks, minor=True) + + ax_mix.set_xlim(0, total_imp) + ax_mix.grid(axis="y") for i in ["top", "right", "left", "bottom"]: secax.spines[i].set_visible(False) ax.spines[i].set_visible(False) + ax_mix.spines[i].set_visible(False) + secax_mix.spines[i].set_visible(False) def fmt(x): return f"{x:.0f}%" if x > 1 else "" @@ -85,5 +126,11 @@ def fmt(x): for container in ax.containers[:2]: ax.bar_label(container, label_type='center', color='white', fmt=fmt) + def fmt(x): + return f"{x:.0f}" if x > 200 else "" + + for container in ax_mix.containers: + ax_mix.bar_label(container, label_type='center', color='white', fmt=fmt) + for fn in snakemake.output: plt.savefig(fn, bbox_inches="tight") From a576120d072d4b9cca99c5421a37b39eb9da7ee5 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 3 Jan 2024 08:50:46 +0100 Subject: [PATCH 228/293] add wildcard options nosteelrelocation --- scripts/prepare_sector_network.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 9e95de311..230105d93 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -4593,6 +4593,11 @@ def lossy_bidirectional_links(n, carrier, efficiencies={}): options["use_fuel_cell_waste_heat"] = False options["use_electrolysis_waste_heat"] = False + if "nosteelrelocation" in opts: + logger.info("Disabling steel industry relocation and flexibility.") + options["relocation_steel"] = False + options["flexibility_steel"] = False + if "nobiogascc" in opts: options["biomass_upgrading_cc"] = False From 85882fe235e7ab2d3a0c580f69256d98066170d7 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 3 Jan 2024 09:07:08 +0100 Subject: [PATCH 229/293] lossy_bidirectional_links: use original length for loss calculation --- scripts/prepare_sector_network.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 230105d93..676bbf9e1 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -4482,15 +4482,16 @@ def lossy_bidirectional_links(n, carrier, efficiencies={}): n.links.loc[carrier_i, "length"] / 1e3 ) rev_links = ( - n.links.loc[carrier_i].copy().rename({"bus0": "bus1", "bus1": "bus0"}, axis=1) + n.links.loc[carrier_i].copy().rename({"bus0": "bus1", "bus1": "bus0", "length": "length_original"}, axis=1) ) - rev_links.capital_cost = 0 - rev_links.length = 0 + rev_links["capital_cost"] = 0 + rev_links["length"] = 0 rev_links["reversed"] = True rev_links.index = rev_links.index.map(lambda x: x + "-reversed") n.links = pd.concat([n.links, rev_links], sort=False) n.links["reversed"] = n.links["reversed"].fillna(False) + n.links["length_original"] = n.links["length_original"].fillna(n.links.length) # do compression losses after concatenation to take electricity consumption at bus0 in either direction carrier_i = n.links.query("carrier == @carrier").index @@ -4499,7 +4500,7 @@ def lossy_bidirectional_links(n, carrier, efficiencies={}): n.buses.location ) # electricity n.links.loc[carrier_i, "efficiency2"] = ( - -compression_per_1000km * n.links.loc[carrier_i, "length"] / 1e3 + -compression_per_1000km * n.links.loc[carrier_i, "length_original"] / 1e3 ) From c3e36eac55010885b6e929b276e302cc0a4462f8 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 22 Mar 2024 12:47:27 +0100 Subject: [PATCH 230/293] various operational fixes --- Snakefile | 5 ++++ config/config.default.yaml | 1 + rules/build_electricity.smk | 2 +- rules/postprocess.smk | 2 +- rules/solve_overnight.smk | 5 +++- scripts/make_summary.py | 2 ++ scripts/prepare_sector_network.py | 8 +++--- scripts/solve_network.py | 41 +++++++++++++++++-------------- 8 files changed, 40 insertions(+), 26 deletions(-) diff --git a/Snakefile b/Snakefile index 85777de6c..96c4a23c8 100644 --- a/Snakefile +++ b/Snakefile @@ -20,6 +20,11 @@ if not exists("config/config.yaml") and exists("config/config.default.yaml"): configfile: "config/config.yaml" +# temporary for mock_snakemake +# import yaml +# from snakemake.utils import update_config +# update_config(config, yaml.safe_load(open("../../config/config.20231025-zecm.yaml"))) + COSTS = f"data/costs_{config['costs']['year']}.csv" ATLITE_NPROCESSES = config["atlite"].get("nprocesses", 4) diff --git a/config/config.default.yaml b/config/config.default.yaml index b4be1471c..bcbd1dad9 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -1089,6 +1089,7 @@ plotting: methanolisation: '#83d6d5' methanol: '#468c8b' shipping methanol: '#468c8b' + methanol-to-kerosene: '#579d9c' # co2 CC: '#f29dae' CCS: '#f29dae' diff --git a/rules/build_electricity.smk b/rules/build_electricity.smk index 383951bd8..51b1ad9f3 100644 --- a/rules/build_electricity.smk +++ b/rules/build_electricity.smk @@ -312,7 +312,7 @@ if config["lines"]["dynamic_line_rating"]["activate"]: BENCHMARKS + "build_line_rating" threads: ATLITE_NPROCESSES resources: - mem_mb=ATLITE_NPROCESSES * 1000, + mem_mb=ATLITE_NPROCESSES * 2500, conda: "../envs/environment.yaml" script: diff --git a/rules/postprocess.smk b/rules/postprocess.smk index be6a834b2..c2a781a98 100644 --- a/rules/postprocess.smk +++ b/rules/postprocess.smk @@ -57,7 +57,7 @@ rule make_summary: metrics=RESULTS + "csvs/metrics.csv", threads: 2 resources: - mem_mb=10000, + mem_mb=20000, log: LOGS + "make_summary.log", benchmark: diff --git a/rules/solve_overnight.smk b/rules/solve_overnight.smk index c77007609..b6c50c134 100644 --- a/rules/solve_overnight.smk +++ b/rules/solve_overnight.smk @@ -20,14 +20,17 @@ rule solve_sector_network: + "postnetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", shadow: "shallow" + retries: 3 log: solver=LOGS + "elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}_solver.log", python=LOGS + "elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}_python.log", + memory=LOGS + + "elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}_memory.log", threads: config["solving"]["solver"].get("threads", 4) resources: - mem_mb=config["solving"]["mem"], + mem_mb=lambda wildcards, attempt: config["solving"]["mem"] + config["solving"].get("mem_increment", 32000) * (attempt - 1), walltime=config["solving"].get("walltime", "12:00:00"), benchmark: ( diff --git a/scripts/make_summary.py b/scripts/make_summary.py index 43480fb8e..110fd74a7 100644 --- a/scripts/make_summary.py +++ b/scripts/make_summary.py @@ -670,6 +670,8 @@ def make_summaries(networks_dict): for output in outputs: df[output] = globals()["calculate_" + output](n, label, df[output]) + del n + return df diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 676bbf9e1..561d28397 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -4510,12 +4510,12 @@ def lossy_bidirectional_links(n, carrier, efficiencies={}): snakemake = mock_snakemake( "prepare_sector_network", - configfiles="../../../config/config.yaml", + configfiles="../../config/config.20231025-zecm.yaml", simpl="", opts="", - clusters="128", - ll="v1.5", - sector_opts="CO2L0-3H-T-H-B-I-A", + clusters="110", + ll="vopt", + sector_opts="Co2L0-2190SEG-T-H-B-I-S-A-imp", planning_horizons="2050", ) diff --git a/scripts/solve_network.py b/scripts/solve_network.py index 6bcd04d8c..42b1e98a7 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -38,6 +38,7 @@ logger = logging.getLogger(__name__) pypsa.pf.logger.setLevel(logging.WARNING) from pypsa.descriptors import get_switchable_as_dense as get_as_dense +from vresutils.benchmark import memory_logger def add_land_use_constraint(n, planning_horizons, config): @@ -725,24 +726,26 @@ def solve_network(n, config, solving, opts="", **kwargs): np.random.seed(solve_opts.get("seed", 123)) - n = pypsa.Network(snakemake.input.network) - - n = prepare_network( - n, - solve_opts, - config=snakemake.config, - foresight=snakemake.params.foresight, - planning_horizons=snakemake.params.planning_horizons, - co2_sequestration_potential=snakemake.params["co2_sequestration_potential"], - ) + fn = getattr(snakemake.log, "memory", None) + with memory_logger(filename=fn, interval=10.0) as mem: + n = pypsa.Network(snakemake.input.network) + + n = prepare_network( + n, + solve_opts, + config=snakemake.config, + foresight=snakemake.params.foresight, + planning_horizons=snakemake.params.planning_horizons, + co2_sequestration_potential=snakemake.params["co2_sequestration_potential"], + ) - n = solve_network( - n, - config=snakemake.config, - solving=snakemake.params.solving, - opts=opts, - log_fn=snakemake.log.solver, - ) + n = solve_network( + n, + config=snakemake.config, + solving=snakemake.params.solving, + opts=opts, + log_fn=snakemake.log.solver, + ) - n.meta = dict(snakemake.config, **dict(wildcards=dict(snakemake.wildcards))) - n.export_to_netcdf(snakemake.output[0]) + n.meta = dict(snakemake.config, **dict(wildcards=dict(snakemake.wildcards))) + n.export_to_netcdf(snakemake.output[0]) From f57b3b92882626b061c6777a6efd18a87aab2779 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Tue, 7 May 2024 11:20:32 +0200 Subject: [PATCH 231/293] add WDPA, convert to 2020EUR, plot adjustments --- rules/plot.smk | 4 +- rules/retrieve.smk | 54 +++++++++++++++++++++++ scripts/plot_import_options.py | 63 ++++++++++++++++++--------- scripts/plot_import_shares.py | 36 ++++++++-------- scripts/plot_import_world_map.py | 74 +++++++++++++++++++++----------- 5 files changed, 167 insertions(+), 64 deletions(-) diff --git a/rules/plot.smk b/rules/plot.smk index 0319ccff9..54833a63d 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -345,7 +345,8 @@ rule plot_import_options: imports="data/imports/results.csv", rc="matplotlibrc", output: - multiext(RESULTS + "graphics/import_options_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", ".png", ".pdf") + map=multiext(RESULTS + "graphics/import_options_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", ".png", ".pdf"), + distribution=multiext(RESULTS + "graphics/import_options_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-distribution", ".png", ".pdf"), script: "../scripts/plot_import_options.py" @@ -363,6 +364,7 @@ rule plot_import_world_map: keep_local=True, static=True, ), + wdpa="data/WDPA.gpkg", rc="matplotlibrc", output: multiext(RESOURCES + "graphics/import_world_map", ".png", ".pdf") diff --git a/rules/retrieve.smk b/rules/retrieve.smk index 37e943635..f2c7c7922 100644 --- a/rules/retrieve.smk +++ b/rules/retrieve.smk @@ -2,6 +2,9 @@ # # SPDX-License-Identifier: MIT +import requests +from datetime import datetime, timedelta + if config["enable"].get("retrieve", "auto") == "auto": config["enable"]["retrieve"] = has_internet_access() @@ -197,6 +200,57 @@ if config["enable"]["retrieve"]: run: move(input[0], output[0]) +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 + + def check_file_exists(url): + response = requests.head(url) + return response.status_code == 200 + + # Basic pattern where WDPA files can be found + url_pattern = ( + "https://d1gam3xoknrgr2.cloudfront.net/current/WDPA_{bYYYY}_Public_shp.zip" + ) + + # 3-letter month + 4 digit year for current/previous/next month to test + current_monthyear = datetime.now().strftime("%b%Y") + prev_monthyear = (datetime.now() - timedelta(30)).strftime("%b%Y") + next_monthyear = (datetime.now() + timedelta(30)).strftime("%b%Y") + + # Test prioritised: current month -> previous -> next + for bYYYY in [current_monthyear, prev_monthyear, next_monthyear]: + if check_file_exists(url := url_pattern.format(bYYYY=bYYYY)): + break + else: + # If None of the three URLs are working + url = False + + assert ( + url + ), f"No WDPA files found at {url_pattern} for bY='{current_monthyear}, {prev_monthyear}, or {next_monthyear}'" + + # Downloading protected area database from WDPA + # extract the main zip and then merge the contained 3 zipped shapefiles + # Website: https://www.protectedplanet.net/en/thematic-areas/wdpa + rule download_wdpa: + input: + HTTP.remote(url, keep_local=True), + params: + zip="data/WDPA_shp.zip", + folder=directory("data/WDPA"), + output: + gpkg=protected("data/WDPA.gpkg"), + run: + shell("cp {input} {params.zip}") + shell("unzip -o {params.zip} -d {params.folder}") + for i in range(3): + # vsizip is special driver for directly working with zipped shapefiles in ogr2ogr + layer_path = ( + f"/vsizip/{params.folder}/WDPA_{bYYYY}_Public_shp_{i}.zip" + ) + print(f"Adding layer {i + 1} of 3 to combined output file.") + shell("ogr2ogr -f gpkg -update -append {output.gpkg} {layer_path}") if config["enable"]["retrieve"]: diff --git a/scripts/plot_import_options.py b/scripts/plot_import_options.py index 226a2c401..9e1ee9ad4 100644 --- a/scripts/plot_import_options.py +++ b/scripts/plot_import_options.py @@ -25,6 +25,9 @@ cc = coco.CountryConverter() +# for EU: https://ec.europa.eu/eurostat/databrowser/view/prc_hicp_aind__custom_9900786/default/table?lang=en +EUR_2015_TO_2020 = 1.002 * 1.017 * 1.019 * 1.015 * 1.007 + NICE_NAMES = { "pipeline-h2": r"H$_2$ (pipeline)", "shipping-lh2": "H$_2$ (ship)", @@ -54,13 +57,13 @@ def create_stripplot(ic, ax): data=ic, x='esc', y='value', - alpha=0.5, + alpha=0.6, hue='exporter', jitter=.28, palette=PALETTE, ax=ax, order=order, - size=3.5 + size=4 ) sns.violinplot( data=ic, @@ -76,16 +79,21 @@ def create_stripplot(ic, ax): zorder=-1 ) - ax.set_ylim(0, 190) + ax.set_ylim(0, 200) ax.set_xlabel("") ax.set_ylabel("import cost [€/MWh]") ax.grid(False) - ax.set_yticks(np.arange(0, 200, 20)) - ax.set_yticks(np.arange(10, 200, 20), minor=True) + ax.set_yticks(np.arange(0, 210, 20)) + ax.set_yticks(np.arange(10, 210, 20), minor=True) ax.set_xticklabels(ax.get_xticklabels(), rotation=18, ha="right") handles, labels = ax.get_legend_handles_labels() handles.reverse() labels.reverse() + new_handles = [] + for handle in handles: + handle.set_markersize(handle.get_markersize() * 2) + new_handles.append(handle) + handles = new_handles for x, y in minimums.items(): ax.text(x, y - 10, str(y), ha="center", va="bottom", fontsize=9) for x, y in maximums.items(): @@ -128,8 +136,8 @@ def create_stripplot(ic, ax): sys.exit(0) tech_colors = snakemake.config["plotting"]["tech_colors"] - tech_colors["lng"] = "tomato" - tech_colors["pipeline"] = "darksalmon" + tech_colors["lng"] = "#e37959" + tech_colors["pipeline"] = "#86cfbc" crs = ccrs.EqualEarth() @@ -155,7 +163,7 @@ def create_stripplot(ic, ax): h2_cost = n.generators.filter(regex="import (pipeline-h2|shipping-lh2)", axis=0) regions["marginal_cost"] = h2_cost.groupby( h2_cost.bus.map(n.buses.location) - ).marginal_cost.min() + ).marginal_cost.min() * EUR_2015_TO_2020 # patch network n.buses.drop(n.buses.index[n.buses.carrier != "AC"], inplace=True) @@ -170,7 +178,7 @@ def create_stripplot(ic, ax): link_colors = pd.Series( n.links.index.map( - lambda x: "seagreen" if "import hvdc-to-elec" in x else "darkseagreen" + lambda x: "seagreen" if "import hvdc-to-elec" in x else "#b18ee6" ), index=n.links.index, ) @@ -199,15 +207,16 @@ def create_stripplot(ic, ax): ic["exporter"] = ic.exporter.apply(lambda x: cc.convert(names=x, to="name_short") if x in highlighted_countries else "Other") ic["esc"] = ic.esc.map(NICE_NAMES) + ic["value"] *= EUR_2015_TO_2020 - fig, ax = plt.subplots(subplot_kw={"projection": crs}, figsize=(14, 14)) + fig, ax = plt.subplots(subplot_kw={"projection": crs}, figsize=(10.5, 14)) n.plot( ax=ax, color_geomap={"ocean": "white", "land": "#efefef"}, bus_sizes=bus_sizes_plain, bus_colors=tech_colors, - line_colors="darkseagreen", + line_colors="#b18ee6", line_widths=1, link_widths=1, link_colors=link_colors, @@ -226,7 +235,7 @@ def create_stripplot(ic, ax): legend=True, legend_kwds={ "label": r"H$_2$ import cost [€/MWh]", - "shrink": 0.48, + "shrink": 0.53, "pad": 0.015, "aspect": 35, }, @@ -239,10 +248,16 @@ def create_stripplot(ic, ax): "external H2": "hydrogen storage", } labels = list(names.values()) + ["HVDC import link", "internal power line", "LNG terminal", "pipeline entry"] - colors = [tech_colors[c] for c in names.keys()] + ["seagreen", "darkseagreen", "tomato", "darksalmon"] + colors = [tech_colors[c] for c in names.keys()] + ["seagreen", "#b18ee6", "#e37959", "#86cfbc"] legend_kw = dict( - loc=(1.2, 0.85), frameon=False, title="", ncol=2 + loc=(0.595, 0.87), + frameon=True, + title="", + ncol=2, + framealpha=1, + borderpad=0.5, + facecolor="white", ) add_legend_patches( @@ -253,18 +268,21 @@ def create_stripplot(ic, ax): ) legend_kw = dict( - loc=(1.2, 0.72), - frameon=False, + loc=(0.623, 0.775), + frameon=True, title="existing gas import capacity", ncol=3, labelspacing=1.1, + framealpha=1, + borderpad=0.5, + facecolor="white", ) add_legend_circles( ax, [10e3 / bus_size_factor, 50e3 / bus_size_factor, 100e3 / bus_size_factor], ["10 GW", "50 GW", "100 GW"], - patch_kw=dict(facecolor="darksalmon"), + patch_kw=dict(facecolor="#86cfbc"), legend_kw=legend_kw, ) @@ -280,11 +298,14 @@ def create_stripplot(ic, ax): color='k', ) - ax_lr = ax.inset_axes([1.2, 0.08, 0.45, 0.59]) - - create_stripplot(ic, ax_lr) plt.tight_layout() - for fn in snakemake.output: + for fn in snakemake.output.map: + plt.savefig(fn, bbox_inches="tight") + + fig, ax_lr = plt.subplots(figsize=(3.9, 7.2)) + create_stripplot(ic, ax_lr) + plt.tight_layout() + for fn in snakemake.output.distribution: plt.savefig(fn, bbox_inches="tight") diff --git a/scripts/plot_import_shares.py b/scripts/plot_import_shares.py index 1d6f2eb4a..ad4c6dd07 100644 --- a/scripts/plot_import_shares.py +++ b/scripts/plot_import_shares.py @@ -16,23 +16,23 @@ from _helpers import configure_logging CARRIERS = { - "AC": "electricity", - "H2": "hydrogen", - "NH3": "ammonia", - "gas": "methane", - "methanol": "methanol", - "oil": "Fischer-Tropsch", - "steel": "steel", + "AC": "electricity (HVDC)", + "H2": "hydrogen (pipeline)", + "NH3": "ammonia (ship)", + "gas": "methane (ship)", + "methanol": "methanol (ship)", + "oil": "Fischer-Tropsch (ship)", + "steel": "steel (ship)", } COLOR_MAPPING = { - "electricity": "import hvdc-to-elec", - "hydrogen": "import pipeline-h2", - "ammonia": "import shipping-lnh3", - "methane": "import shipping-lch4", - "methanol": "import shipping-meoh", - "Fischer-Tropsch": "import shipping-ftfuel", - "steel": "import shipping-steel", + "electricity (HVDC)": "import hvdc-to-elec", + "hydrogen (pipeline)": "import pipeline-h2", + "ammonia (ship)": "import shipping-lnh3", + "methane (ship)": "import shipping-lch4", + "methanol (ship)": "import shipping-meoh", + "Fischer-Tropsch (ship)": "import shipping-ftfuel", + "steel (ship)": "import shipping-steel", } THRESHOLD = 1 # MWh @@ -76,7 +76,9 @@ fig, (ax, ax_mix) = plt.subplots(2, 1, figsize=(6, 5), gridspec_kw=dict(height_ratios=[5.5, 1])) sel = list(CARRIERS.keys())[::-1] - ie_rel[sel].rename(columns=CARRIERS).T.plot.barh( + ie_rel = ie_rel[sel].rename(columns=CARRIERS).T + ie_rel.index = ie_rel.index.str.split(" ").str[0] + ie_rel.plot.barh( stacked=True, ax=ax, color=['lightseagreen', 'coral'] @@ -84,7 +86,7 @@ ax.set_ylabel("") ax.set_xlabel("domestic share [%]", fontsize=11, color='lightseagreen') - ax.legend(ncol=2, bbox_to_anchor=(0.35, 1.25)) + ax.legend(ncol=2, bbox_to_anchor=(0.3, 1.25)) ax.grid(axis="y") ax.set_xlim(0,100) @@ -104,7 +106,7 @@ ax_mix.text(total_imp * 1.1, -0.75, "TWh", va='center') ax_mix.text(total_imp * 1.1, 0.75, "%", va='center') - ax_mix.legend(ncol=4, bbox_to_anchor=(1.15, -0.3), title="") + ax_mix.legend(ncol=3, bbox_to_anchor=(1.18, -0.3), title="") ticks = range(10, 100, 20) ax.set_xticks(ticks, minor=True) diff --git a/scripts/plot_import_world_map.py b/scripts/plot_import_world_map.py index 849dd980d..0de221df5 100644 --- a/scripts/plot_import_world_map.py +++ b/scripts/plot_import_world_map.py @@ -21,13 +21,22 @@ from atlite.gis import ExclusionContainer, shape_availability from rasterio.features import geometry_mask from rasterio.plot import show +from shapely.geometry import box + +from pypsa.plot import add_legend_patches logger = logging.getLogger(__name__) cc = coco.CountryConverter() +# for EU: https://ec.europa.eu/eurostat/databrowser/view/prc_hicp_aind__custom_9900786/default/table?lang=en +EUR_2015_TO_2020 = 1.002 * 1.017 * 1.019 * 1.015 * 1.007 + AREA_CRS = "ESRI:54009" +ARROW_COLOR = "#b3c4ad" +ARROW_COLOR = "#ccc" + NICE_NAMES = { "pipeline-h2": r"H$_2$ (pipeline)", "shipping-lh2": "H$_2$ (ship)", @@ -65,6 +74,7 @@ def rename(s): def get_cost_composition(df, country, escs, production): query_str = "category == 'cost' and exporter == @country and esc in @escs" composition = df.query(query_str).groupby(["esc", "subcategory", "importer"]).value.min() + composition *= EUR_2015_TO_2020 minimal = {} for name, group in composition.groupby("esc"): @@ -91,7 +101,7 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): wdpa = gpd.read_file( wdpa_fn, bbox=shape.to_crs(4326).geometry, - layer="WDPA_Oct2023_Public_shp-polygons", + layer="WDPA_Mar2024_Public_shp-polygons", ).to_crs(AREA_CRS) excluder.add_geometry(wdpa.geometry, buffer=1000) @@ -117,11 +127,11 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): "plot_import_world_map", simpl="", opts="", - clusters="100", - ll="v1.5", + clusters="110", + ll="vopt", sector_opts="Co2L0-2190SEG-T-H-B-I-S-A-imp", planning_horizons="2050", - configfiles="../../config/config.100n-seg.yaml", + configfiles="../../config/config.20231025-zecm.yaml", ) configure_logging(snakemake) @@ -130,7 +140,7 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): profile_fn = snakemake.input.profiles gadm_fn = snakemake.input.gadm_arg[0] glc_fn = snakemake.input.copernicus_glc[0] - wdpa_fn = "/home/fneum/bwss/playgrounds/pr/pypsa-eur/resources/WDPA_Oct2023.gpkg" + wdpa_fn = snakemake.input.wdpa config = snakemake.config @@ -166,6 +176,9 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): eu_countries = cc.convert(config["countries"], src="iso2", to="iso3") europe = world.loc[eu_countries] + bounding_box = box(-12, 33, 42, 72) + europe = gpd.clip(europe, bounding_box) + gadm = gpd.read_file(gadm_fn, layer="ADM_ADM_1").set_index("NAME_1") shape = gadm.to_crs(AREA_CRS).loc[["Buenos Aires"]].geometry @@ -177,6 +190,7 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): df["exporter"] = df.exporter.replace("", "NA") import_costs = df.query("subcategory == 'Cost per MWh delivered' and esc == 'shipping-meoh'").groupby("exporter").value.min() + import_costs *= EUR_2015_TO_2020 import_costs.index = cc.convert(import_costs.index.str.split("-").str[0], src='iso2', to='iso3') import_costs.drop("RUS", inplace=True, errors="ignore") @@ -223,7 +237,7 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): ax=ax, cmap='Greens_r', legend=True, - vmin=100, + vmin=110, vmax=150, legend_kwds=dict(label="Cost for methanol fuel delivered to Europe [€/MWh]", orientation="horizontal", extend='max', shrink=.6, aspect=30, pad=.01), missing_kwds=dict(color="#eee", label="not considered"), @@ -233,7 +247,17 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): linewidth=1, edgecolor="black", ax=ax, - color='lavender', + color='#cbc7f0', + ) + + add_legend_patches( + ax, + ["#eee", "#cbc7f0"], + ["country not considered for export", "country in European model scope"], + legend_kw = dict( + bbox_to_anchor=(1, 0), + frameon=False, + ), ) for spine in ax.spines.values(): @@ -267,7 +291,7 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): ncol=2, columnspacing=0.8 ) - ax_prof.set_xlabel("March 2013", fontsize=8) + ax_prof.set_xlabel("Day of March 2013", fontsize=8) ax_prof.set_ylabel("profile [p.u.]", fontsize=8) ax_prof.tick_params(axis='both', labelsize=8) @@ -289,8 +313,8 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): xytext=(0.485, 0.72), xycoords='axes fraction', arrowprops=dict( - edgecolor='#555', - facecolor='#555', + edgecolor=ARROW_COLOR, + facecolor=ARROW_COLOR, linewidth=1.5, arrowstyle='-|>', connectionstyle="arc3,rad=0.2" @@ -312,9 +336,9 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): ax_arg.set_title("Import costs from\nArgentina to Europe", fontsize=9) ax_arg.set_xlabel("") - ax_arg.set_ylim(0, 110) - ax_arg.set_yticks(range(0, 111, 20)) - ax_arg.set_yticks(range(10, 111, 20), minor=True) + ax_arg.set_ylim(0, 120) + ax_arg.set_yticks(range(0, 121, 20)) + ax_arg.set_yticks(range(10, 121, 20), minor=True) ax_arg.set_ylabel("€/MWh", fontsize=10) ax_arg.grid(axis="x") for spine in ax_arg.spines.values(): @@ -326,8 +350,8 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): xytext=(0.33, 0.2), xycoords='axes fraction', arrowprops=dict( - edgecolor='#555', - facecolor='#555', + edgecolor=ARROW_COLOR, + facecolor=ARROW_COLOR, linewidth=1.5, arrowstyle='-|>', connectionstyle="arc3,rad=-0.2" @@ -345,9 +369,9 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): ax_sau.set_xlabel("") ax_sau.set_ylabel("€/MWh", fontsize=10) ax_sau.grid(axis="x") - ax_sau.set_ylim(0, 90) - ax_sau.set_yticks(range(0, 91, 20)) - ax_sau.set_yticks(range(10, 91, 20), minor=True) + ax_sau.set_ylim(0, 100) + ax_sau.set_yticks(range(0, 101, 20)) + ax_sau.set_yticks(range(10, 101, 20), minor=True) for spine in ax_sau.spines.values(): spine.set_visible(False) @@ -357,8 +381,8 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): xytext=(0.62, 0.65), xycoords='axes fraction', arrowprops=dict( - edgecolor='#555', - facecolor='#555', + edgecolor=ARROW_COLOR, + facecolor=ARROW_COLOR, linewidth=1.5, arrowstyle='-|>', connectionstyle="arc3,rad=0.2" @@ -388,8 +412,8 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): xytext=(0.815, 0.31), xycoords='axes fraction', arrowprops=dict( - edgecolor='#555', - facecolor='#555', + edgecolor=ARROW_COLOR, + facecolor=ARROW_COLOR, linewidth=1.5, arrowstyle='-|>', connectionstyle="arc3,rad=0.2" @@ -400,7 +424,7 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): ax_land = ax.inset_axes([0.315, 0.08, 0.29, 0.29]) - shape.to_crs(crs.proj4_init).plot(ax=ax, color="none", edgecolor='k', linestyle=":", linewidth=1) + shape.to_crs(crs.proj4_init).plot(ax=ax, color="none", edgecolor=ARROW_COLOR, linestyle=":", linewidth=1) add_land_eligibility_example(ax_land, shape, glc_fn, wdpa_fn) @@ -412,8 +436,8 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): xytext=(0.35, 0.17), xycoords='axes fraction', arrowprops=dict( - edgecolor='#555', - facecolor='#555', + edgecolor=ARROW_COLOR, + facecolor=ARROW_COLOR, linewidth=1.5, arrowstyle='-|>', connectionstyle="arc3,rad=0.2" From 41f51c2bfbf27e63ce7c1fcc0812a894493541ee Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 14 Aug 2024 20:46:37 +0200 Subject: [PATCH 232/293] remove submodule config workaround --- Snakefile | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Snakefile b/Snakefile index 96c4a23c8..85777de6c 100644 --- a/Snakefile +++ b/Snakefile @@ -20,11 +20,6 @@ if not exists("config/config.yaml") and exists("config/config.default.yaml"): configfile: "config/config.yaml" -# temporary for mock_snakemake -# import yaml -# from snakemake.utils import update_config -# update_config(config, yaml.safe_load(open("../../config/config.20231025-zecm.yaml"))) - COSTS = f"data/costs_{config['costs']['year']}.csv" ATLITE_NPROCESSES = config["atlite"].get("nprocesses", 4) From ba09606c9654f146e565d62e6cebdb6e2e4c2509 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Sat, 24 Aug 2024 11:10:01 +0200 Subject: [PATCH 233/293] pre-commit formatting [no ci] --- config/config.default.yaml | 9 -- rules/build_sector.smk | 14 +- rules/collect.smk | 4 +- rules/plot.smk | 188 ++++++++++++++++++++------ rules/retrieve.smk | 3 + rules/solve_overnight.smk | 3 +- scripts/_helpers.py | 8 ++ scripts/plot_choropleth_capacities.py | 2 +- scripts/plot_choropleth_prices.py | 2 +- scripts/plot_import_options.py | 79 ++++++----- scripts/plot_import_shares.py | 63 +++++---- scripts/plot_import_world_map.py | 172 +++++++++++++---------- scripts/plot_summary.py | 8 +- scripts/prepare_sector_network.py | 32 +++-- 14 files changed, 382 insertions(+), 205 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 43e8fea1c..5e3d7561e 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -529,14 +529,6 @@ sector: shipping_ammonia_efficiency: 0.40 shipping_lng_efficiency: 0.44 shipping_lh2_efficiency: 0.44 - shipping_endogenous: - enable: false - fuels: - - oil - - methanol - - LH2 - - LNG - - ammonia aviation_demand_factor: 1. HVC_demand_factor: 1. time_dep_hp_cop: true @@ -659,7 +651,6 @@ sector: biomass_to_methanol_cc: false biosng: false biosng_cc: false - biomass_upgrading_cc: false electrobiofuels: false endogenous_steel: false endogenous_hvc: false diff --git a/rules/build_sector.smk b/rules/build_sector.smk index f9c159a20..3d16c2874 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -1032,12 +1032,18 @@ rule prepare_sector_network: 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"), - industrial_production=resources("industrial_production_elec_s{simpl}_{clusters}_{planning_horizons}.csv"), + industrial_production=resources( + "industrial_production_elec_s{simpl}_{clusters}_{planning_horizons}.csv" + ), industrial_demand=resources( "industrial_energy_demand_elec_s{simpl}_{clusters}_{planning_horizons}.csv" ), - industrial_demand_today=resources("industrial_energy_demand_today_elec_s{simpl}_{clusters}.csv"), - industry_sector_ratios=resources("industry_sector_ratios_{planning_horizons}.csv"), + industrial_demand_today=resources( + "industrial_energy_demand_today_elec_s{simpl}_{clusters}.csv" + ), + industry_sector_ratios=resources( + "industry_sector_ratios_{planning_horizons}.csv" + ), hourly_heat_demand_total=resources( "hourly_heat_demand_total_elec_s{simpl}_{clusters}.nc" ), @@ -1075,7 +1081,7 @@ rule prepare_sector_network: country_centroids=storage( "https://raw.githubusercontent.com/gavinr/world-countries-centroids/v1.0.0/dist/countries.csv", keep_local=True, - ) + ), output: RESULTS + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", diff --git a/rules/collect.smk b/rules/collect.smk index 2bc00fe74..9e8ce6c7d 100644 --- a/rules/collect.smk +++ b/rules/collect.smk @@ -110,9 +110,9 @@ rule plot_resources: ), expand( RESOURCES + "graphics/salt-caverns-{clusters}-nearshore.pdf", - **config["scenario"] + **config["scenario"], ), expand( RESOURCES + "graphics/biomass-potentials-{clusters}-biogas.pdf", - **config["scenario"] + **config["scenario"], ), diff --git a/rules/plot.smk b/rules/plot.smk index 54833a63d..07dd76a2f 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -114,13 +114,19 @@ rule plot_salt_caverns_clustered: rc="matplotlibrc", output: onshore=multiext( - RESOURCES + "graphics/salt-caverns-s{simpl}-{clusters}-onshore", ".png", ".pdf" + RESOURCES + "graphics/salt-caverns-s{simpl}-{clusters}-onshore", + ".png", + ".pdf", ), nearshore=multiext( - RESOURCES + "graphics/salt-caverns-s{simpl}-{clusters}-nearshore", ".png", ".pdf" + RESOURCES + "graphics/salt-caverns-s{simpl}-{clusters}-nearshore", + ".png", + ".pdf", ), offshore=multiext( - RESOURCES + "graphics/salt-caverns-s{simpl}-{clusters}-offshore", ".png", ".pdf" + RESOURCES + "graphics/salt-caverns-s{simpl}-{clusters}-offshore", + ".png", + ".pdf", ), script: "../scripts/plot_salt_caverns_clustered.py" @@ -133,7 +139,8 @@ rule plot_biomass_potentials: rc="matplotlibrc", output: solid_biomass=multiext( - RESOURCES + "graphics/biomass-potentials-s{simpl}-{clusters}-solid_biomass", + RESOURCES + + "graphics/biomass-potentials-s{simpl}-{clusters}-solid_biomass", ".png", ".pdf", ), @@ -143,7 +150,9 @@ rule plot_biomass_potentials: ".pdf", ), biogas=multiext( - RESOURCES + "graphics/biomass-potentials-s{simpl}-{clusters}-biogas", ".png", ".pdf" + RESOURCES + "graphics/biomass-potentials-s{simpl}-{clusters}-biogas", + ".png", + ".pdf", ), script: "../scripts/plot_biomass_potentials.py" @@ -181,7 +190,10 @@ rule plot_choropleth_capacities: regions_offshore=RESOURCES + "regions_offshore_elec_s{simpl}_{clusters}.geojson", rc="matplotlibrc", output: - directory(RESULTS + "graphics/p_nom_opt/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}"), + directory( + RESULTS + + "graphics/p_nom_opt/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" + ), script: "../scripts/plot_choropleth_capacities.py" @@ -194,7 +206,10 @@ rule plot_choropleth_prices: regions_offshore=RESOURCES + "regions_offshore_elec_s{simpl}_{clusters}.geojson", rc="matplotlibrc", output: - directory(RESULTS + "graphics/prices/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}"), + directory( + RESULTS + + "graphics/prices/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" + ), script: "../scripts/plot_choropleth_prices.py" @@ -207,7 +222,10 @@ rule plot_choropleth_potential_used: regions_offshore=RESOURCES + "regions_offshore_elec_s{simpl}_{clusters}.geojson", rc="matplotlibrc", output: - directory(RESULTS + "graphics/potential_used/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}"), + directory( + RESULTS + + "graphics/potential_used/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" + ), script: "../scripts/plot_choropleth_potential_used.py" @@ -219,11 +237,15 @@ rule plot_choropleth_demand: industrial_demand=RESOURCES + "industrial_energy_demand_elec_s{simpl}_{clusters}_{planning_horizons}.csv", shipping_demand=RESOURCES + "shipping_demand_s{simpl}_{clusters}.csv", - nodal_energy_totals=RESOURCES + "pop_weighted_energy_totals_s{simpl}_{clusters}.csv", + nodal_energy_totals=RESOURCES + + "pop_weighted_energy_totals_s{simpl}_{clusters}.csv", regions_onshore=RESOURCES + "regions_onshore_elec_s{simpl}_{clusters}.geojson", rc="matplotlibrc", output: - directory(RESULTS + "graphics/regional_demand/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}"), + directory( + RESULTS + + "graphics/regional_demand/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" + ), script: "../scripts/plot_choropleth_demand.py" @@ -235,7 +257,10 @@ rule plot_balance_timeseries: rc="matplotlibrc", threads: 12 output: - directory(RESULTS + "graphics/balance_timeseries/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}") + directory( + RESULTS + + "graphics/balance_timeseries/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" + ), script: "../scripts/plot_balance_timeseries.py" @@ -247,10 +272,14 @@ rule plot_heatmap_timeseries: rc="matplotlibrc", threads: 12 output: - directory(RESULTS + "graphics/heatmap_timeseries/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}") + directory( + RESULTS + + "graphics/heatmap_timeseries/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" + ), script: "../scripts/plot_heatmap_timeseries.py" + rule plot_heatmap_timeseries_resources: input: network=RESULTS @@ -258,7 +287,10 @@ rule plot_heatmap_timeseries_resources: rc="matplotlibrc", threads: 12 output: - directory(RESULTS + "graphics/heatmap_timeseries_resources/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}") + directory( + RESULTS + + "graphics/heatmap_timeseries_resources/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" + ), script: "../scripts/plot_heatmap_timeseries_resources.py" @@ -272,8 +304,12 @@ rule plot_power_network: regions=RESOURCES + "regions_onshore_elec_s{simpl}_{clusters}.geojson", rc="matplotlibrc", output: - multiext(RESULTS - + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-costs-all_{planning_horizons}", ".png", ".pdf") + multiext( + RESULTS + + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-costs-all_{planning_horizons}", + ".png", + ".pdf", + ), threads: 2 resources: mem_mb=10000, @@ -287,6 +323,7 @@ rule plot_power_network: script: "../scripts/plot_power_network.py" + rule plot_hydrogen_network: params: foresight=config["foresight"], @@ -297,8 +334,12 @@ rule plot_hydrogen_network: regions=RESOURCES + "regions_onshore_elec_s{simpl}_{clusters}.geojson", rc="matplotlibrc", output: - multiext(RESULTS - + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-h2_network_{planning_horizons}", ".png", ".pdf") + multiext( + RESULTS + + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-h2_network_{planning_horizons}", + ".png", + ".pdf", + ), threads: 2 resources: mem_mb=10000, @@ -312,6 +353,7 @@ rule plot_hydrogen_network: script: "../scripts/plot_hydrogen_network.py" + rule plot_gas_network: params: plotting=config["plotting"], @@ -321,8 +363,12 @@ rule plot_gas_network: regions=RESOURCES + "regions_onshore_elec_s{simpl}_{clusters}.geojson", rc="matplotlibrc", output: - multiext(RESULTS - + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-ch4_network_{planning_horizons}", ".png", ".pdf") + multiext( + RESULTS + + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-ch4_network_{planning_horizons}", + ".png", + ".pdf", + ), threads: 2 resources: mem_mb=10000, @@ -336,6 +382,7 @@ rule plot_gas_network: script: "../scripts/plot_gas_network.py" + rule plot_import_options: input: network=RESULTS @@ -345,11 +392,22 @@ rule plot_import_options: imports="data/imports/results.csv", rc="matplotlibrc", output: - map=multiext(RESULTS + "graphics/import_options_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", ".png", ".pdf"), - distribution=multiext(RESULTS + "graphics/import_options_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-distribution", ".png", ".pdf"), + map=multiext( + RESULTS + + "graphics/import_options_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", + ".png", + ".pdf", + ), + distribution=multiext( + RESULTS + + "graphics/import_options_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-distribution", + ".png", + ".pdf", + ), script: "../scripts/plot_import_options.py" + rule plot_import_world_map: input: imports="data/imports/results.csv", @@ -367,7 +425,7 @@ rule plot_import_world_map: wdpa="data/WDPA.gpkg", rc="matplotlibrc", output: - multiext(RESOURCES + "graphics/import_world_map", ".png", ".pdf") + multiext(RESOURCES + "graphics/import_world_map", ".png", ".pdf"), script: "../scripts/plot_import_world_map.py" @@ -379,7 +437,12 @@ rule plot_import_networks: regions=RESOURCES + "regions_onshore_elec_s{simpl}_{clusters}.geojson", rc="matplotlibrc", output: - multiext(RESULTS + "graphics/import_networks/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", ".png", ".pdf") + multiext( + RESULTS + + "graphics/import_networks/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", + ".png", + ".pdf", + ), script: "../scripts/plot_import_network.py" @@ -390,7 +453,12 @@ rule plot_import_shares: + "postnetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", rc="matplotlibrc", output: - multiext(RESULTS + "graphics/import_shares/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", ".png", ".pdf") + multiext( + RESULTS + + "graphics/import_shares/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", + ".png", + ".pdf", + ), script: "../scripts/plot_import_shares.py" @@ -405,34 +473,66 @@ rule plot_all_resources: rules.plot_powerplants.output, rules.plot_salt_caverns_unclustered.output, rules.plot_import_world_map.output, - expand(RESOURCES + "graphics/salt-caverns-s{simpl}-{clusters}-onshore.pdf", **config["scenario"]), - expand(RESOURCES + "graphics/power-network-{clusters}.pdf", **config["scenario"]), - expand(RESOURCES + "graphics/biomass-potentials-s{simpl}-{clusters}-biogas.pdf", **config["scenario"]), - expand(RESOURCES + "graphics/capacity-factor/s{simpl}-{clusters}", **config["scenario"]), - expand(RESOURCES + "graphics/capacity-factor-sector/s{simpl}-{clusters}", **config["scenario"]), + expand( + RESOURCES + "graphics/salt-caverns-s{simpl}-{clusters}-onshore.pdf", + **config["scenario"], + ), + expand( + RESOURCES + "graphics/power-network-{clusters}.pdf", **config["scenario"] + ), + expand( + RESOURCES + "graphics/biomass-potentials-s{simpl}-{clusters}-biogas.pdf", + **config["scenario"], + ), + expand( + RESOURCES + "graphics/capacity-factor/s{simpl}-{clusters}", + **config["scenario"], + ), + expand( + RESOURCES + "graphics/capacity-factor-sector/s{simpl}-{clusters}", + **config["scenario"], + ), rule plot_all_results_single: input: - RESULTS + "graphics/p_nom_opt/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", - RESULTS + "graphics/prices/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", - RESULTS + "graphics/potential_used/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", - RESULTS + "graphics/balance_timeseries/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", - RESULTS + "graphics/heatmap_timeseries/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", - RESULTS + "graphics/heatmap_timeseries_resources/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", - RESULTS + "graphics/regional_demand/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", - RESULTS + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-costs-all_{planning_horizons}.pdf", - RESULTS + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-h2_network_{planning_horizons}.pdf", - RESULTS + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-ch4_network_{planning_horizons}.pdf", - RESULTS + "graphics/import_options_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.pdf", - RESULTS + "graphics/import_networks/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.pdf", - RESULTS + "graphics/import_shares/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.pdf", + RESULTS + + "graphics/p_nom_opt/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", + RESULTS + + "graphics/prices/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", + RESULTS + + "graphics/potential_used/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", + RESULTS + + "graphics/balance_timeseries/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", + RESULTS + + "graphics/heatmap_timeseries/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", + RESULTS + + "graphics/heatmap_timeseries_resources/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", + RESULTS + + "graphics/regional_demand/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", + RESULTS + + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-costs-all_{planning_horizons}.pdf", + RESULTS + + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-h2_network_{planning_horizons}.pdf", + RESULTS + + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-ch4_network_{planning_horizons}.pdf", + RESULTS + + "graphics/import_options_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.pdf", + RESULTS + + "graphics/import_networks/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.pdf", + RESULTS + + "graphics/import_shares/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.pdf", output: - RESULTS + "graphics/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.touch" + RESULTS + + "graphics/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.touch", shell: "touch {output}" rule plot_all_results: input: - expand(RESULTS + "graphics/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.touch", **config["scenario"]), + expand( + RESULTS + + "graphics/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.touch", + **config["scenario"], + ), diff --git a/rules/retrieve.smk b/rules/retrieve.smk index 413bc7fd5..7c4557073 100644 --- a/rules/retrieve.smk +++ b/rules/retrieve.smk @@ -356,6 +356,7 @@ if config["enable"]["retrieve"]: shell("ogr2ogr -f gpkg -update -append {output.gpkg} {layer_path}") + 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 @@ -408,6 +409,8 @@ if config["enable"]["retrieve"]: print(f"Adding layer {i + 1} of 3 to combined output file.") shell("ogr2ogr -f gpkg -update -append {output.gpkg} {layer_path}") + + if config["enable"]["retrieve"]: rule retrieve_monthly_co2_prices: diff --git a/rules/solve_overnight.smk b/rules/solve_overnight.smk index e42ab1d6c..d2b6a7da9 100644 --- a/rules/solve_overnight.smk +++ b/rules/solve_overnight.smk @@ -32,7 +32,8 @@ rule solve_sector_network: + "logs/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}_python.log", threads: solver_threads resources: - mem_mb=lambda wildcards, attempt: config_provider("solving", "mem_mb") + config_provider("solving", "mem_increment", defauult=32000) * (attempt - 1), + mem_mb=lambda wildcards, attempt: config_provider("solving", "mem_mb") + + config_provider("solving", "mem_increment", defauult=32000) * (attempt - 1), runtime=config_provider("solving", "runtime", default="6h"), benchmark: ( diff --git a/scripts/_helpers.py b/scripts/_helpers.py index 878fc0abf..b7b3877b1 100644 --- a/scripts/_helpers.py +++ b/scripts/_helpers.py @@ -662,6 +662,14 @@ def update_config_from_wildcards(config, w, inplace=True): if "nodistrict" in opts: config["sector"]["district_heating"]["progress"] = 0.0 + if "nosteelrelocation" in opts: + logger.info("Disabling steel industry relocation and flexibility.") + config["sector"]["relocation_steel"] = False + config["sector"]["flexibility_steel"] = False + + if "nobiogascc" in opts: + config["sector"]["biogas_upgrading_cc"] = False + dg_enable, dg_factor = find_opt(opts, "dist") if dg_enable: config["sector"]["electricity_distribution_grid"] = True diff --git a/scripts/plot_choropleth_capacities.py b/scripts/plot_choropleth_capacities.py index 379a36a28..d2c43889c 100644 --- a/scripts/plot_choropleth_capacities.py +++ b/scripts/plot_choropleth_capacities.py @@ -108,7 +108,7 @@ def get_optimal_capacity(n): "plot_choropleth_capacities", simpl="", clusters=110, - ll='vopt', + ll="vopt", opts="", sector_opts="Co2L0-2190SEG-T-H-B-I-S-A-onwind+p0.5-imp", planning_horizons=2050, diff --git a/scripts/plot_choropleth_prices.py b/scripts/plot_choropleth_prices.py index 04fdd777a..094d4d684 100644 --- a/scripts/plot_choropleth_prices.py +++ b/scripts/plot_choropleth_prices.py @@ -66,7 +66,7 @@ def get_market_values(n): ) to_drop = list(n.buses.index[n.buses.index.str.len() == 2]) + ["", "EU", "process"] - mv = mv.drop(to_drop, errors='ignore').unstack().dropna(how="all", axis=1) + mv = mv.drop(to_drop, errors="ignore").unstack().dropna(how="all", axis=1) return mv diff --git a/scripts/plot_import_options.py b/scripts/plot_import_options.py index 9e1ee9ad4..c2823cedd 100644 --- a/scripts/plot_import_options.py +++ b/scripts/plot_import_options.py @@ -44,39 +44,44 @@ "Namibia": "#003580", "Saudi Arabia": "#006c35", "Chile": "darkorange", - "Other": "#aaa" + "Other": "#aaa", } + def create_stripplot(ic, ax): order = list(NICE_NAMES.values())[:-1] - minimums = ic.groupby("esc").value.min().round(1)[order].reset_index(drop=True).to_dict() - maximums = ic.groupby("esc").value.max().round(1)[order].reset_index(drop=True).to_dict() + minimums = ( + ic.groupby("esc").value.min().round(1)[order].reset_index(drop=True).to_dict() + ) + maximums = ( + ic.groupby("esc").value.max().round(1)[order].reset_index(drop=True).to_dict() + ) sns.stripplot( data=ic, - x='esc', - y='value', + x="esc", + y="value", alpha=0.6, - hue='exporter', - jitter=.28, + hue="exporter", + jitter=0.28, palette=PALETTE, ax=ax, order=order, - size=4 + size=4, ) sns.violinplot( data=ic, - x='esc', - y='value', + x="esc", + y="value", linewidth=0, saturation=0.3, cut=0, - color='#ddd', + color="#ddd", fill=True, ax=ax, order=order, - zorder=-1 + zorder=-1, ) ax.set_ylim(0, 200) @@ -98,16 +103,11 @@ def create_stripplot(ic, ax): ax.text(x, y - 10, str(y), ha="center", va="bottom", fontsize=9) for x, y in maximums.items(): ax.text(x, y + 5, str(y), ha="center", va="bottom", fontsize=9) - ax.legend( - title="", - ncol=1, - loc=(0.55, 0.05), - labelspacing=0.3, - frameon=False - ) + ax.legend(title="", ncol=1, loc=(0.55, 0.05), labelspacing=0.3, frameon=False) for spine in ax.spines.values(): spine.set_visible(False) + if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake @@ -130,6 +130,7 @@ def create_stripplot(ic, ax): # dummy output if no imports considered if "-imp" not in snakemake.wildcards.sector_opts: import sys + fig, ax = plt.subplots() for fn in snakemake.output: plt.savefig(fn, bbox_inches="tight") @@ -161,9 +162,10 @@ def create_stripplot(ic, ax): # TODO size external nodes according to wind and solar potential h2_cost = n.generators.filter(regex="import (pipeline-h2|shipping-lh2)", axis=0) - regions["marginal_cost"] = h2_cost.groupby( - h2_cost.bus.map(n.buses.location) - ).marginal_cost.min() * EUR_2015_TO_2020 + regions["marginal_cost"] = ( + h2_cost.groupby(h2_cost.bus.map(n.buses.location)).marginal_cost.min() + * EUR_2015_TO_2020 + ) # patch network n.buses.drop(n.buses.index[n.buses.carrier != "AC"], inplace=True) @@ -204,7 +206,13 @@ def create_stripplot(ic, ax): ic["exporter"] = ic.exporter.str.split("-").str[0] highlighted_countries = ["DZ", "AR", "SA", "CL"] - ic["exporter"] = ic.exporter.apply(lambda x: cc.convert(names=x, to="name_short") if x in highlighted_countries else "Other") + ic["exporter"] = ic.exporter.apply( + lambda x: ( + cc.convert(names=x, to="name_short") + if x in highlighted_countries + else "Other" + ) + ) ic["esc"] = ic.esc.map(NICE_NAMES) ic["value"] *= EUR_2015_TO_2020 @@ -228,7 +236,7 @@ def create_stripplot(ic, ax): ax=ax, column="marginal_cost", cmap="Blues_r", - edgecolor='#ddd', + edgecolor="#ddd", linewidths=0.5, vmin=50, vmax=100, @@ -247,8 +255,18 @@ def create_stripplot(ic, ax): "external battery": "battery storage", "external H2": "hydrogen storage", } - labels = list(names.values()) + ["HVDC import link", "internal power line", "LNG terminal", "pipeline entry"] - colors = [tech_colors[c] for c in names.keys()] + ["seagreen", "#b18ee6", "#e37959", "#86cfbc"] + labels = list(names.values()) + [ + "HVDC import link", + "internal power line", + "LNG terminal", + "pipeline entry", + ] + colors = [tech_colors[c] for c in names.keys()] + [ + "seagreen", + "#b18ee6", + "#e37959", + "#86cfbc", + ] legend_kw = dict( loc=(0.595, 0.87), @@ -288,17 +306,16 @@ def create_stripplot(ic, ax): ax.add_feature( cfeature.BORDERS.with_scale("50m"), - linewidth=.75, - color='k', + linewidth=0.75, + color="k", ) ax.add_feature( cfeature.COASTLINE.with_scale("50m"), - linewidth=.75, - color='k', + linewidth=0.75, + color="k", ) - plt.tight_layout() for fn in snakemake.output.map: diff --git a/scripts/plot_import_shares.py b/scripts/plot_import_shares.py index ad4c6dd07..c32242e1e 100644 --- a/scripts/plot_import_shares.py +++ b/scripts/plot_import_shares.py @@ -35,7 +35,7 @@ "steel (ship)": "import shipping-steel", } -THRESHOLD = 1 # MWh +THRESHOLD = 1 # MWh if __name__ == "__main__": if "snakemake" not in globals(): @@ -54,15 +54,19 @@ configure_logging(snakemake) - plt.style.use(['bmh', snakemake.input.rc]) + plt.style.use(["bmh", snakemake.input.rc]) tech_colors = snakemake.config["plotting"]["tech_colors"] n = pypsa.Network(snakemake.input.network) eb = n.statistics.energy_balance().groupby(["carrier", "bus_carrier"]).sum() - eb = eb.unstack(1).groupby(lambda x: "import" if "import" in x or "external" in x else x).sum() - supply = eb.where(eb > THRESHOLD).dropna(how='all', axis=0).div(1e6) + eb = ( + eb.unstack(1) + .groupby(lambda x: "import" if "import" in x or "external" in x else x) + .sum() + ) + supply = eb.where(eb > THRESHOLD).dropna(how="all", axis=0).div(1e6) ie = supply.groupby(lambda x: "import" if x == "import" else "domestic").sum() ie_rel = ie / ie.sum() * 100 @@ -70,42 +74,51 @@ imp_mix = ie.loc["import"].copy() if "steel" in imp_mix.index: - imp_mix.loc["steel"] *= 2.1 # kWh/kg - imp_mix = pd.DataFrame(imp_mix.where(imp_mix > 1e-3).dropna().rename(index=CARRIERS).sort_values(ascending=False)) + imp_mix.loc["steel"] *= 2.1 # kWh/kg + imp_mix = pd.DataFrame( + imp_mix.where(imp_mix > 1e-3) + .dropna() + .rename(index=CARRIERS) + .sort_values(ascending=False) + ) imp_mix.columns = ["import mix"] - fig, (ax, ax_mix) = plt.subplots(2, 1, figsize=(6, 5), gridspec_kw=dict(height_ratios=[5.5, 1])) + fig, (ax, ax_mix) = plt.subplots( + 2, 1, figsize=(6, 5), gridspec_kw=dict(height_ratios=[5.5, 1]) + ) sel = list(CARRIERS.keys())[::-1] ie_rel = ie_rel[sel].rename(columns=CARRIERS).T ie_rel.index = ie_rel.index.str.split(" ").str[0] - ie_rel.plot.barh( - stacked=True, - ax=ax, - color=['lightseagreen', 'coral'] - ) + ie_rel.plot.barh(stacked=True, ax=ax, color=["lightseagreen", "coral"]) ax.set_ylabel("") - ax.set_xlabel("domestic share [%]", fontsize=11, color='lightseagreen') + ax.set_xlabel("domestic share [%]", fontsize=11, color="lightseagreen") ax.legend(ncol=2, bbox_to_anchor=(0.3, 1.25)) ax.grid(axis="y") - ax.set_xlim(0,100) + ax.set_xlim(0, 100) - imp_mix.T.plot.barh(ax=ax_mix, stacked=True, legend=True, color=[tech_colors[COLOR_MAPPING[i]]for i in imp_mix.index]) + imp_mix.T.plot.barh( + ax=ax_mix, + stacked=True, + legend=True, + color=[tech_colors[COLOR_MAPPING[i]] for i in imp_mix.index], + ) for i, (carrier, twh) in enumerate(ie_sum[sel].items()): unit = "Mt" if carrier.lower().startswith("steel") else "TWh" - ax.text(119, i, f"{twh} {unit}", va='center', ha='right', color='slateblue') - ax.text(119, i + 1.5, "total\nsupply", va='center', ha='right', color='slateblue') + ax.text(119, i, f"{twh} {unit}", va="center", ha="right", color="slateblue") + ax.text(119, i + 1.5, "total\nsupply", va="center", ha="right", color="slateblue") - secax = ax.secondary_xaxis('top', functions=(lambda x: 100 - x, lambda x: 100 -x)) - secax.set_xlabel('import share [%]', fontsize=11, color='coral') + secax = ax.secondary_xaxis("top", functions=(lambda x: 100 - x, lambda x: 100 - x)) + secax.set_xlabel("import share [%]", fontsize=11, color="coral") total_imp = imp_mix.sum().sum() - secax_mix = ax_mix.secondary_xaxis('top', functions=(lambda x: x / total_imp * 100, lambda x: x * total_imp * 100)) - + secax_mix = ax_mix.secondary_xaxis( + "top", functions=(lambda x: x / total_imp * 100, lambda x: x * total_imp * 100) + ) - ax_mix.text(total_imp * 1.1, -0.75, "TWh", va='center') - ax_mix.text(total_imp * 1.1, 0.75, "%", va='center') + ax_mix.text(total_imp * 1.1, -0.75, "TWh", va="center") + ax_mix.text(total_imp * 1.1, 0.75, "%", va="center") ax_mix.legend(ncol=3, bbox_to_anchor=(1.18, -0.3), title="") ticks = range(10, 100, 20) @@ -126,13 +139,13 @@ def fmt(x): return f"{x:.0f}%" if x > 1 else "" for container in ax.containers[:2]: - ax.bar_label(container, label_type='center', color='white', fmt=fmt) + ax.bar_label(container, label_type="center", color="white", fmt=fmt) def fmt(x): return f"{x:.0f}" if x > 200 else "" for container in ax_mix.containers: - ax_mix.bar_label(container, label_type='center', color='white', fmt=fmt) + ax_mix.bar_label(container, label_type="center", color="white", fmt=fmt) for fn in snakemake.output: plt.savefig(fn, bbox_inches="tight") diff --git a/scripts/plot_import_world_map.py b/scripts/plot_import_world_map.py index 0de221df5..b4de1ba22 100644 --- a/scripts/plot_import_world_map.py +++ b/scripts/plot_import_world_map.py @@ -19,12 +19,11 @@ import xarray as xr from _helpers import configure_logging from atlite.gis import ExclusionContainer, shape_availability +from pypsa.plot import add_legend_patches from rasterio.features import geometry_mask from rasterio.plot import show from shapely.geometry import box -from pypsa.plot import add_legend_patches - logger = logging.getLogger(__name__) cc = coco.CountryConverter() @@ -47,6 +46,7 @@ "shipping-steel": "steel", } + def rename(s): if "solar" in s: return "solar" @@ -54,13 +54,18 @@ def rename(s): return "wind" if "storage" in s or "inverter" in s: return "storage" - if "transport" in s or "shipping fuel" in s or "dry bulk" in s or "pipeline" in s: + if "transport" in s or "shipping fuel" in s or "dry bulk" in s or "pipeline" in s: return "transport" if "evaporation" in s or "liquefaction" in s or "compress" in s: return "evaporation/liquefaction" if "direct air capture" in s or "heat pump" in s: return "direct air capture" - if s in ["Haber-Bosch (exp)", "Fischer-Tropsch (exp)", "methanolisation (exp)", "methanation (exp)"]: + if s in [ + "Haber-Bosch (exp)", + "Fischer-Tropsch (exp)", + "methanolisation (exp)", + "methanation (exp)", + ]: return "hydrogen conversion" if "iron ore" in s: return "iron ore" @@ -73,7 +78,9 @@ def rename(s): def get_cost_composition(df, country, escs, production): query_str = "category == 'cost' and exporter == @country and esc in @escs" - composition = df.query(query_str).groupby(["esc", "subcategory", "importer"]).value.min() + composition = ( + df.query(query_str).groupby(["esc", "subcategory", "importer"]).value.min() + ) composition *= EUR_2015_TO_2020 minimal = {} @@ -84,7 +91,7 @@ def get_cost_composition(df, country, escs, production): composition = composition.groupby(rename).sum().div(production) - composition = composition.where(composition > 0.01).dropna(how='all') + composition = composition.where(composition > 0.01).dropna(how="all") sort_by = composition.sum().sort_values(ascending=True).index selection = pd.Index(COLORS.keys()).intersection(composition.index) @@ -110,11 +117,11 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): [shape.geometry.values[0]], transform=transform, invert=False, - out_shape=band.shape + out_shape=band.shape, ) masked_band = np.where(mask, ~band, np.nan) - shape.plot(ax=ax, color="none", edgecolor='k', linewidth=1) + shape.plot(ax=ax, color="none", edgecolor="k", linewidth=1) show(masked_band, transform=transform, cmap="Purples", ax=ax) ax.set_axis_off() @@ -144,7 +151,7 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): config = snakemake.config - plt.style.use(['bmh', snakemake.input.rc]) + plt.style.use(["bmh", snakemake.input.rc]) tech_colors = config["plotting"]["tech_colors"] @@ -165,12 +172,14 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): # load capacity factor time series ds = xr.open_dataset(profile_fn) - profile = ds.sel(exporter='MA', importer='EUE').p_max_pu.to_pandas().T + profile = ds.sel(exporter="MA", importer="EUE").p_max_pu.to_pandas().T profile.rename(columns={"onwind": "wind", "solar-utility": "solar"}, inplace=True) # download world country shapes, version clipped to Europe, and GADM in AR - world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres')).set_index("iso_a3") + world = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres")).set_index( + "iso_a3" + ) world.drop("ATA", inplace=True) eu_countries = cc.convert(config["countries"], src="iso2", to="iso3") @@ -189,32 +198,36 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): # bugfix for Namibia df["exporter"] = df.exporter.replace("", "NA") - import_costs = df.query("subcategory == 'Cost per MWh delivered' and esc == 'shipping-meoh'").groupby("exporter").value.min() + import_costs = ( + df.query("subcategory == 'Cost per MWh delivered' and esc == 'shipping-meoh'") + .groupby("exporter") + .value.min() + ) import_costs *= EUR_2015_TO_2020 - import_costs.index = cc.convert(import_costs.index.str.split("-").str[0], src='iso2', to='iso3') + import_costs.index = cc.convert( + import_costs.index.str.split("-").str[0], src="iso2", to="iso3" + ) import_costs.drop("RUS", inplace=True, errors="ignore") composition_arg = get_cost_composition( df, "AR", - ["shipping-lh2", "shipping-ftfuel", "shipping-meoh", "shipping-lch4", "shipping-lnh3"], - 500e6 + [ + "shipping-lh2", + "shipping-ftfuel", + "shipping-meoh", + "shipping-lch4", + "shipping-lnh3", + ], + 500e6, ) composition_sau = get_cost_composition( - df, - "SA", - ["pipeline-h2", "shipping-lh2"], - 500e6 + df, "SA", ["pipeline-h2", "shipping-lh2"], 500e6 ) - composition_aus = get_cost_composition( - df, - "AU", - ["shipping-steel"], - 100e6 - ) + composition_aus = get_cost_composition(df, "AU", ["shipping-steel"], 100e6) # add to legend @@ -226,7 +239,7 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): crs = ccrs.EqualEarth() - fig, ax = plt.subplots(figsize=(14,14), subplot_kw={"projection": crs}) + fig, ax = plt.subplots(figsize=(14, 14), subplot_kw={"projection": crs}) # main axis: choropleth layer @@ -235,11 +248,18 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): linewidth=1, edgecolor="black", ax=ax, - cmap='Greens_r', + cmap="Greens_r", legend=True, vmin=110, vmax=150, - legend_kwds=dict(label="Cost for methanol fuel delivered to Europe [€/MWh]", orientation="horizontal", extend='max', shrink=.6, aspect=30, pad=.01), + legend_kwds=dict( + label="Cost for methanol fuel delivered to Europe [€/MWh]", + orientation="horizontal", + extend="max", + shrink=0.6, + aspect=30, + pad=0.01, + ), missing_kwds=dict(color="#eee", label="not considered"), ) @@ -247,14 +267,14 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): linewidth=1, edgecolor="black", ax=ax, - color='#cbc7f0', + color="#cbc7f0", ) add_legend_patches( ax, ["#eee", "#cbc7f0"], ["country not considered for export", "country in European model scope"], - legend_kw = dict( + legend_kw=dict( bbox_to_anchor=(1, 0), frameon=False, ), @@ -263,10 +283,17 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): for spine in ax.spines.values(): spine.set_visible(False) - ax.set_facecolor('none') - fig.set_facecolor('none') + ax.set_facecolor("none") + fig.set_facecolor("none") - ax.text(0.93, 0.01, "Projection:\nEqual Earth", transform=ax.transAxes, fontsize=9, color="grey") + ax.text( + 0.93, + 0.01, + "Projection:\nEqual Earth", + transform=ax.transAxes, + fontsize=9, + color="grey", + ) plt.tight_layout() @@ -279,25 +306,19 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): week_profile.plot( ax=ax_prof, linewidth=1, - color=['gold', "royalblue"], - ylim=(0,1), + color=["gold", "royalblue"], + ylim=(0, 1), clip_on=False, ) - ax_prof.legend( - title="", - loc=(0,1), - fontsize=8, - ncol=2, - columnspacing=0.8 - ) + ax_prof.legend(title="", loc=(0, 1), fontsize=8, ncol=2, columnspacing=0.8) ax_prof.set_xlabel("Day of March 2013", fontsize=8) ax_prof.set_ylabel("profile [p.u.]", fontsize=8) - ax_prof.tick_params(axis='both', labelsize=8) + ax_prof.tick_params(axis="both", labelsize=8) ax_prof.xaxis.set_major_locator(mdates.DayLocator()) ax_prof.xaxis.set_major_formatter(mdates.DateFormatter("%d")) - xticks = week_profile.resample('D').mean().index + xticks = week_profile.resample("D").mean().index ax_prof.set_xticks(xticks) ax_prof.set_xticklabels(xticks.day) @@ -308,17 +329,17 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): label.set_fontsize(8) ax.annotate( - '', + "", xy=(0.45, 0.75), xytext=(0.485, 0.72), - xycoords='axes fraction', + xycoords="axes fraction", arrowprops=dict( - edgecolor=ARROW_COLOR, - facecolor=ARROW_COLOR, - linewidth=1.5, - arrowstyle='-|>', - connectionstyle="arc3,rad=0.2" - ) + edgecolor=ARROW_COLOR, + facecolor=ARROW_COLOR, + linewidth=1.5, + arrowstyle="-|>", + connectionstyle="arc3,rad=0.2", + ), ) # inset: Argentina e-fuel import costs @@ -331,7 +352,7 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): handles.reverse() labels.reverse() - ax_arg.legend(handles, labels, title="", ncol=1, fontsize=9, loc=(1,0)) + ax_arg.legend(handles, labels, title="", ncol=1, fontsize=9, loc=(1, 0)) ax_arg.set_title("Import costs from\nArgentina to Europe", fontsize=9) @@ -345,17 +366,17 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): spine.set_visible(False) ax.annotate( - '', + "", xy=(0.25, 0.15), xytext=(0.33, 0.2), - xycoords='axes fraction', + xycoords="axes fraction", arrowprops=dict( edgecolor=ARROW_COLOR, facecolor=ARROW_COLOR, linewidth=1.5, - arrowstyle='-|>', - connectionstyle="arc3,rad=-0.2" - ) + arrowstyle="-|>", + connectionstyle="arc3,rad=-0.2", + ), ) # inset: Saudi Arabia hydrogen pipeline versus ship imports @@ -376,17 +397,17 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): spine.set_visible(False) ax.annotate( - '', + "", xy=(0.655, 0.55), xytext=(0.62, 0.65), - xycoords='axes fraction', + xycoords="axes fraction", arrowprops=dict( edgecolor=ARROW_COLOR, facecolor=ARROW_COLOR, linewidth=1.5, - arrowstyle='-|>', - connectionstyle="arc3,rad=0.2" - ) + arrowstyle="-|>", + connectionstyle="arc3,rad=0.2", + ), ) # inset: Australia steel imports @@ -405,43 +426,44 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): for spine in ax_aus.spines.values(): spine.set_visible(False) - ax.annotate( - '', + "", xy=(0.77, 0.35), xytext=(0.815, 0.31), - xycoords='axes fraction', + xycoords="axes fraction", arrowprops=dict( edgecolor=ARROW_COLOR, facecolor=ARROW_COLOR, linewidth=1.5, - arrowstyle='-|>', - connectionstyle="arc3,rad=0.2" - ) + arrowstyle="-|>", + connectionstyle="arc3,rad=0.2", + ), ) # inset: land eligibility of Buenos Aires ax_land = ax.inset_axes([0.315, 0.08, 0.29, 0.29]) - shape.to_crs(crs.proj4_init).plot(ax=ax, color="none", edgecolor=ARROW_COLOR, linestyle=":", linewidth=1) + shape.to_crs(crs.proj4_init).plot( + ax=ax, color="none", edgecolor=ARROW_COLOR, linestyle=":", linewidth=1 + ) add_land_eligibility_example(ax_land, shape, glc_fn, wdpa_fn) ax_land.set_title("wind exclusion\nzones (purple)", fontsize=9) ax.annotate( - '', + "", xy=(0.41, 0.22), xytext=(0.35, 0.17), - xycoords='axes fraction', + xycoords="axes fraction", arrowprops=dict( edgecolor=ARROW_COLOR, facecolor=ARROW_COLOR, linewidth=1.5, - arrowstyle='-|>', - connectionstyle="arc3,rad=0.2" - ) + arrowstyle="-|>", + connectionstyle="arc3,rad=0.2", + ), ) for fn in snakemake.output: diff --git a/scripts/plot_summary.py b/scripts/plot_summary.py index a374437a9..a4d8a2326 100644 --- a/scripts/plot_summary.py +++ b/scripts/plot_summary.py @@ -297,9 +297,11 @@ def plot_balances(): "H2", ] df.index = [ - i[:-1] - if ((i not in forbidden) and (i[-1:] in ["0", "1", "2", "3", "4"])) - else i + ( + i[:-1] + if ((i not in forbidden) and (i[-1:] in ["0", "1", "2", "3", "4"])) + else i + ) for i in df.index ] diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 565de056a..11cf4ef07 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -21,8 +21,6 @@ import pandas as pd import pypsa import xarray as xr -from geopy.extra.rate_limiter import RateLimiter -from geopy.geocoders import Nominatim from _helpers import ( configure_logging, set_scenario_config, @@ -36,6 +34,8 @@ build_eurostat_co2, ) from build_transport_demand import transport_degree_factor +from geopy.extra.rate_limiter import RateLimiter +from geopy.geocoders import Nominatim from networkx.algorithms import complement from networkx.algorithms.connectivity.edge_augmentation import k_edge_augmentation from prepare_network import maybe_adjust_costs_and_potentials @@ -569,7 +569,11 @@ def add_carrier_buses(n, carrier, nodes=None): ) fossils = ["coal", "gas", "oil", "lignite"] - if options.get("fossil_fuels", True) and carrier in fossils and costs.at[carrier, "fuel"] > 0: + if ( + options.get("fossil_fuels", True) + and carrier in fossils + and costs.at[carrier, "fuel"] > 0 + ): n.madd( "Generator", @@ -2944,7 +2948,6 @@ def add_biomass(n, costs): marginal_cost=costs.at["BtL", "VOM"] * costs.at["BtL", "efficiency"], ) - if options.get("biomass_to_liquid_cc"): # Assuming that acid gas removal (incl. CO2) from syngas i performed with Rectisol @@ -4066,7 +4069,10 @@ def add_industry(n, costs): ) else: p_set = ( - -industrial_demand.loc[(nodes, sectors_b), "process emission"].sum(axis=1).sum() / nhours + -industrial_demand.loc[(nodes, sectors_b), "process emission"] + .sum(axis=1) + .sum() + / nhours ) n.madd( @@ -4241,8 +4247,14 @@ def add_waste_heat(n): n.links.loc[urban_central + " Haber-Bosch", "bus3"] = ( urban_central + " urban central heat" ) - total_energy_input = (cf_industry["MWh_H2_per_tNH3_electrolysis"] + cf_industry["MWh_elec_per_tNH3_electrolysis"]) / cf_industry["MWh_NH3_per_tNH3"] - electricity_input = cf_industry["MWh_elec_per_tNH3_electrolysis"] / cf_industry["MWh_NH3_per_tNH3"] + total_energy_input = ( + cf_industry["MWh_H2_per_tNH3_electrolysis"] + + cf_industry["MWh_elec_per_tNH3_electrolysis"] + ) / cf_industry["MWh_NH3_per_tNH3"] + electricity_input = ( + cf_industry["MWh_elec_per_tNH3_electrolysis"] + / cf_industry["MWh_NH3_per_tNH3"] + ) n.links.loc[urban_central + " Haber-Bosch", "efficiency3"] = ( 0.15 * total_energy_input / electricity_input ) @@ -4463,7 +4475,7 @@ def _coordinates(ct): import_links = pd.concat([import_links, pd.Series(xlinks)], axis=0) import_links = import_links.drop_duplicates(keep="first") - duplicated = import_links.index.duplicated(keep='first') + duplicated = import_links.index.duplicated(keep="first") import_links = import_links.loc[~duplicated] hvdc_cost = ( @@ -4678,7 +4690,9 @@ def add_import_options( "shipping-meoh": ("methanolisation", "carbondioxide-input"), } - import_costs = pd.read_csv(snakemake.input.import_costs, delimiter=";", keep_default_na=False) + import_costs = pd.read_csv( + snakemake.input.import_costs, delimiter=";", keep_default_na=False + ) # temporary bugfix for Namibia import_costs["exporter"] = import_costs.exporter.replace("", "NA") From 29fd370f67df5fc24f6b55a3319d7c864ed0e2f9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 24 Aug 2024 09:13:44 +0000 Subject: [PATCH 234/293] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- rules/retrieve.smk | 2 +- scripts/prepare_sector_network.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rules/retrieve.smk b/rules/retrieve.smk index a1e4f0374..bf5fa012e 100644 --- a/rules/retrieve.smk +++ b/rules/retrieve.smk @@ -406,7 +406,7 @@ if config["enable"]["retrieve"]: layer_path = ( f"/vsizip/{params.folder}/WDPA_{bYYYY}_Public_shp_{i}.zip" ) - print(f"Adding layer {i + 1} of 3 to combined output file.") + print(f"Adding layer {i+1} of 3 to combined output file.") shell("ogr2ogr -f gpkg -update -append {output.gpkg} {layer_path}") diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 0a4423901..7a490b2a0 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -34,11 +34,11 @@ build_eurostat_co2, ) from build_transport_demand import transport_degree_factor -from geopy.extra.rate_limiter import RateLimiter -from geopy.geocoders import Nominatim from definitions.heat_sector import HeatSector from definitions.heat_system import HeatSystem from definitions.heat_system_type import HeatSystemType +from geopy.extra.rate_limiter import RateLimiter +from geopy.geocoders import Nominatim from networkx.algorithms import complement from networkx.algorithms.connectivity.edge_augmentation import k_edge_augmentation from prepare_network import maybe_adjust_costs_and_potentials From a421c33c0c39822ba054f02054a17b3bf944b49e Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 26 Aug 2024 11:39:16 +0200 Subject: [PATCH 235/293] disentangle DRI and EAF --- scripts/prepare_sector_network.py | 74 ++++++++++++++++++++++--------- 1 file changed, 53 insertions(+), 21 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 0a4423901..d37eb37d3 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3191,6 +3191,14 @@ def add_industry(n, costs): unit="t", ) + n.add( + "Bus", + "EU HBI", + location="EU", + carrier="HBI", + unit="t", + ) + n.add( "Load", "EU steel", @@ -3209,50 +3217,74 @@ def add_industry(n, costs): carrier="steel", ) - electricity_input = ( - costs.at["direct iron reduction furnace", "electricity-input"] - * costs.at["electric arc furnace", "hbi-input"] - + costs.at["electric arc furnace", "electricity-input"] - ) + n.add( + "Store", + "EU HBI Store", + bus="EU HBI", + e_nom_extendable=True, + e_cyclic=True, + carrier="HBI", + ) - hydrogen_input = ( - costs.at["direct iron reduction furnace", "hydrogen-input"] - * costs.at["electric arc furnace", "hbi-input"] - ) + electricity_input = costs.at["direct iron reduction furnace", "electricity-input"] + + hydrogen_input = costs.at["direct iron reduction furnace", "hydrogen-input"] # so that for each region supply matches consumption - p_nom = industrial_production[sector] * electricity_input / nhours + p_nom = ( + industrial_production[sector] + * costs.at["electric arc furnace", "hbi-input"] + * electricity_input + / nhours + ) marginal_cost = ( costs.at["iron ore DRI-ready", "commodity"] * costs.at["direct iron reduction furnace", "ore-input"] - * costs.at["electric arc furnace", "hbi-input"] / electricity_input ) - capital_cost = ( - costs.at["direct iron reduction furnace", "fixed"] - * costs.at["electric arc furnace", "hbi-input"] - + costs.at["electric arc furnace", "fixed"] - ) / electricity_input - n.madd( "Link", nodes, - suffix=f" {sector}", - carrier=sector, - capital_cost=capital_cost, + suffix=" DRI", + carrier="DRI", + capital_cost=costs.at["direct iron reduction furnace", "fixed"] / electricity_input, marginal_cost=marginal_cost, p_nom_max=p_nom if no_relocation else np.inf, p_nom_extendable=True, p_min_pu=1 if no_flexibility else 0, bus0=nodes, - bus1="EU steel", + bus1="EU HBI", bus2=nodes + " H2", efficiency=1 / electricity_input, efficiency2=-hydrogen_input / electricity_input, ) + electricity_input = costs.at["electric arc furnace", "electricity-input"] + + p_nom = ( + industrial_production[sector] + * electricity_input + / nhours + ) + + n.madd( + "Link", + nodes, + suffix=" EAF", + carrier="EAF", + capital_cost=costs.at["electric arc furnace", "fixed"] / electricity_input, + p_nom_max=p_nom if no_relocation else np.inf, + p_nom_extendable=True, + p_min_pu=1 if no_flexibility else 0, + bus0=nodes, + bus1="EU steel", + bus2="EU HBI", + efficiency=1 / electricity_input, + efficiency2=-costs.at["electric arc furnace", "hbi-input"] / electricity_input, + ) + n.madd( "Bus", spatial.biomass.industry, From 09ad3bff1ae5c074dd1f8351b37d3b30607ecd9f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2024 09:41:52 +0000 Subject: [PATCH 236/293] [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, 9 insertions(+), 9 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index aa394ae53..f6c6d3bf6 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3243,7 +3243,9 @@ def add_industry(n, costs): carrier="HBI", ) - electricity_input = costs.at["direct iron reduction furnace", "electricity-input"] + electricity_input = costs.at[ + "direct iron reduction furnace", "electricity-input" + ] hydrogen_input = costs.at["direct iron reduction furnace", "hydrogen-input"] @@ -3266,7 +3268,8 @@ def add_industry(n, costs): nodes, suffix=" DRI", carrier="DRI", - capital_cost=costs.at["direct iron reduction furnace", "fixed"] / electricity_input, + capital_cost=costs.at["direct iron reduction furnace", "fixed"] + / electricity_input, marginal_cost=marginal_cost, p_nom_max=p_nom if no_relocation else np.inf, p_nom_extendable=True, @@ -3280,18 +3283,14 @@ def add_industry(n, costs): electricity_input = costs.at["electric arc furnace", "electricity-input"] - p_nom = ( - industrial_production[sector] - * electricity_input - / nhours - ) + p_nom = industrial_production[sector] * electricity_input / nhours n.madd( "Link", nodes, suffix=" EAF", carrier="EAF", - capital_cost=costs.at["electric arc furnace", "fixed"] / electricity_input, + capital_cost=costs.at["electric arc furnace", "fixed"] / electricity_input, p_nom_max=p_nom if no_relocation else np.inf, p_nom_extendable=True, p_min_pu=1 if no_flexibility else 0, @@ -3299,7 +3298,8 @@ def add_industry(n, costs): bus1="EU steel", bus2="EU HBI", efficiency=1 / electricity_input, - efficiency2=-costs.at["electric arc furnace", "hbi-input"] / electricity_input, + efficiency2=-costs.at["electric arc furnace", "hbi-input"] + / electricity_input, ) n.madd( From 327c9ac800cc338d6215c5e308a0ed4899b47179 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 26 Aug 2024 11:47:23 +0200 Subject: [PATCH 237/293] remove inflation adjustment --- scripts/plot_import_options.py | 6 +++--- scripts/plot_import_world_map.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/plot_import_options.py b/scripts/plot_import_options.py index c2823cedd..eec096a1c 100644 --- a/scripts/plot_import_options.py +++ b/scripts/plot_import_options.py @@ -26,7 +26,7 @@ cc = coco.CountryConverter() # for EU: https://ec.europa.eu/eurostat/databrowser/view/prc_hicp_aind__custom_9900786/default/table?lang=en -EUR_2015_TO_2020 = 1.002 * 1.017 * 1.019 * 1.015 * 1.007 +# EUR_2015_TO_2020 = 1.002 * 1.017 * 1.019 * 1.015 * 1.007 NICE_NAMES = { "pipeline-h2": r"H$_2$ (pipeline)", @@ -164,7 +164,7 @@ def create_stripplot(ic, ax): h2_cost = n.generators.filter(regex="import (pipeline-h2|shipping-lh2)", axis=0) regions["marginal_cost"] = ( h2_cost.groupby(h2_cost.bus.map(n.buses.location)).marginal_cost.min() - * EUR_2015_TO_2020 + # * EUR_2015_TO_2020 ) # patch network @@ -215,7 +215,7 @@ def create_stripplot(ic, ax): ) ic["esc"] = ic.esc.map(NICE_NAMES) - ic["value"] *= EUR_2015_TO_2020 + # ic["value"] *= EUR_2015_TO_2020 fig, ax = plt.subplots(subplot_kw={"projection": crs}, figsize=(10.5, 14)) diff --git a/scripts/plot_import_world_map.py b/scripts/plot_import_world_map.py index b4de1ba22..a60175f9b 100644 --- a/scripts/plot_import_world_map.py +++ b/scripts/plot_import_world_map.py @@ -29,7 +29,7 @@ cc = coco.CountryConverter() # for EU: https://ec.europa.eu/eurostat/databrowser/view/prc_hicp_aind__custom_9900786/default/table?lang=en -EUR_2015_TO_2020 = 1.002 * 1.017 * 1.019 * 1.015 * 1.007 +# EUR_2015_TO_2020 = 1.002 * 1.017 * 1.019 * 1.015 * 1.007 AREA_CRS = "ESRI:54009" @@ -81,7 +81,7 @@ def get_cost_composition(df, country, escs, production): composition = ( df.query(query_str).groupby(["esc", "subcategory", "importer"]).value.min() ) - composition *= EUR_2015_TO_2020 + # composition *= EUR_2015_TO_2020 minimal = {} for name, group in composition.groupby("esc"): @@ -203,7 +203,7 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): .groupby("exporter") .value.min() ) - import_costs *= EUR_2015_TO_2020 + # import_costs *= EUR_2015_TO_2020 import_costs.index = cc.convert( import_costs.index.str.split("-").str[0], src="iso2", to="iso3" ) From 1494c4072f706fc7c61fb2192569fca37b310fd1 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 26 Aug 2024 11:49:33 +0200 Subject: [PATCH 238/293] add shipping-hbi as import option --- scripts/prepare_sector_network.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index f6c6d3bf6..838180f1e 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -4685,6 +4685,7 @@ def add_import_options( "shipping-ftfuel", "shipping-lnh3", "shipping-steel", + "shipping-hbi", ], endogenous_hvdc=False, ): @@ -4717,6 +4718,7 @@ def add_import_options( "shipping-ftfuel": " oil", "shipping-meoh": " methanol", "shipping-steel": " steel", + "shipping-hbi": " HBI", } co2_intensity = { @@ -4871,6 +4873,7 @@ def add_import_options( copperplated_carbonfree_options = { "shipping-steel", + "shipping-HBI", "shipping-lnh3", } @@ -5449,6 +5452,7 @@ def add_enhanced_geothermal(n, egs_potentials, egs_overlap, costs): FT=["shipping-ftfuel"], MeOH=["shipping-meoh"], St=["shipping-steel"], + HBI=["shipping-HBI"], ) for o in opts: if not o.startswith("imp"): From b9f1f2bff8bd5fd2904f0e6fc38699bd83aa2423 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 26 Aug 2024 11:54:34 +0200 Subject: [PATCH 239/293] near-constant pipeline-based imports via p_min_pu --- scripts/prepare_sector_network.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 838180f1e..49a72e67f 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -4819,6 +4819,9 @@ def add_import_options( 1.2 * 7018.0 if tech == "shipping-lh2" else 0.0 ) # €/MW/a, +20% compared to LNG + # pipeline imports require high minimum loading + p_min_pu = 0.9 if "pipeline" in tech else 0 + n.madd( "Generator", import_nodes_tech.index + f"{suffix} import {tech}", @@ -4827,6 +4830,7 @@ def add_import_options( marginal_cost=import_nodes_tech.marginal_cost.values, p_nom_extendable=True, capital_cost=capital_cost, + p_min_pu=p_min_pu, p_nom_max=import_nodes_tech.p_nom.mul(capacity_boost) .clip(upper=upper_p_nom_max) .values, From a7a4c092c6c5704c6eef46f50540db1252930795 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 26 Aug 2024 14:16:34 +0200 Subject: [PATCH 240/293] build_gas_input_locations: remove duplicated code --- scripts/build_gas_input_locations.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/scripts/build_gas_input_locations.py b/scripts/build_gas_input_locations.py index a7f8ce1bb..2b286c1bd 100644 --- a/scripts/build_gas_input_locations.py +++ b/scripts/build_gas_input_locations.py @@ -105,10 +105,6 @@ def build_gas_input_locations(gem_fn, entry_fn, sto_fn, countries): remove_country = ["RU", "UA", "TR", "BY"] # noqa: F841 sto = sto.query("country_code not in @remove_country").copy() - sto = read_scigrid_gas(sto_fn) - remove_country = ["RU", "UA", "TR", "BY"] - sto = sto.query("country_code != @remove_country") - # production sites inside the model scope prod = build_gem_prod_data(gem_fn) From 94faf3fcc11c4a6c41ad45a66de64610b61f85be Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 26 Aug 2024 14:17:08 +0200 Subject: [PATCH 241/293] solve_network: handle HBI in import volume limit --- scripts/solve_network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/solve_network.py b/scripts/solve_network.py index ba02f0ebd..d15133f70 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -991,7 +991,7 @@ def add_energy_import_limit(n, sns): p_links = n.model["Link-p"].loc[sns, import_links] # using energy content of iron as proxy: 2.1 MWh/t - energy_weightings = np.where(import_gens.str.contains("steel"), 2.1, 1.0) + energy_weightings = np.where(import_gens.str.contains("(steel|HBI)"), 2.1, 1.0) energy_weightings = pd.Series(energy_weightings, index=import_gens) lhs = (p_gens * weightings * energy_weightings).sum() + (p_links * weightings).sum() From 104b3b081e37b1a57b76de06dab9d833a5782b43 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 26 Aug 2024 16:05:25 +0200 Subject: [PATCH 242/293] add nuclear powerplants to be considered --- data/custom_powerplants.csv | 224 ++++++++++++++++++++++++++++++------ 1 file changed, 187 insertions(+), 37 deletions(-) diff --git a/data/custom_powerplants.csv b/data/custom_powerplants.csv index 84553a74f..72fce5180 100644 --- a/data/custom_powerplants.csv +++ b/data/custom_powerplants.csv @@ -1,37 +1,187 @@ -,Name,Fueltype,Technology,Set,Country,Capacity,Efficiency,Duration,Volume_Mm3,DamHeight_m,StorageCapacity_MWh,DateIn,DateRetrofit,DateMothball,DateOut,lat,lon,EIC,projectID -1266,Khmelnitskiy,Nuclear,,PP,UA,1901.8916595755832,,0.0,0.0,0.0,0.0,1988.0,2005.0,,,50.3023,26.6466,[nan],"{'GEO': ['GEO3842'], 'GPD': ['WRI1005111'], 'CARMA': ['CARMA22000']}" -1268,Kaniv,Hydro,Reservoir,PP,UA,452.1656050955414,,0.0,0.0,0.0,0.0,1972.0,2003.0,,,49.76653,31.47165,[nan],"{'GEO': ['GEO43017'], 'GPD': ['WRI1005122'], 'CARMA': ['CARMA21140']}" -1269,Kahovska kakhovka,Hydro,Reservoir,PP,UA,352.45222929936307,,0.0,0.0,0.0,0.0,1955.0,1956.0,,,46.77858,33.36965,[nan],"{'GEO': ['GEO43018'], 'GPD': ['WRI1005118'], 'CARMA': ['CARMA20855']}" -1347,Kharkiv,Natural Gas,Steam Turbine,CHP,UA,494.94274967602314,,0.0,0.0,0.0,0.0,1979.0,1980.0,,,49.9719,36.107,[nan],"{'GEO': ['GEO43027'], 'GPD': ['WRI1005126'], 'CARMA': ['CARMA21972']}" -1348,Kremenchuk,Hydro,Reservoir,PP,UA,617.0382165605096,,0.0,0.0,0.0,0.0,1959.0,1960.0,,,49.07759,33.2505,[nan],"{'GEO': ['GEO43019'], 'GPD': ['WRI1005121'], 'CARMA': ['CARMA23072']}" -1377,Krivorozhskaya,Hard Coal,Steam Turbine,PP,UA,2600.0164509342876,,0.0,0.0,0.0,0.0,1965.0,1992.0,,,47.5432,33.6583,[nan],"{'GEO': ['GEO42989'], 'GPD': ['WRI1005100'], 'CARMA': ['CARMA23176']}" -1407,Zmiyevskaya zmiivskaya,Hard Coal,Steam Turbine,PP,UA,2028.3816283884514,,0.0,0.0,0.0,0.0,1960.0,2005.0,,,49.5852,36.5231,[nan],"{'GEO': ['GEO42999'], 'GPD': ['WRI1005103'], 'CARMA': ['CARMA51042']}" -1408,Pridneprovskaya,Hard Coal,Steam Turbine,CHP,UA,1627.3152609570984,,0.0,0.0,0.0,0.0,1959.0,1966.0,,,48.4051,35.1131,[nan],"{'GEO': ['GEO42990'], 'GPD': ['WRI1005102'], 'CARMA': ['CARMA35874']}" -1409,Kurakhovskaya,Hard Coal,Steam Turbine,PP,UA,1371.0015824607397,,0.0,0.0,0.0,0.0,1972.0,2003.0,,,47.9944,37.24022,[nan],"{'GEO': ['GEO42994'], 'GPD': ['WRI1005104'], 'CARMA': ['CARMA23339']}" -1410,Dobrotvorsky,Hard Coal,Steam Turbine,PP,UA,553.1949895604868,,0.0,0.0,0.0,0.0,1960.0,1964.0,,,50.2133,24.375,[nan],"{'GEO': ['GEO42992'], 'GPD': ['WRI1005096'], 'CARMA': ['CARMA10971']}" -1422,Zuyevskaya,Hard Coal,Steam Turbine,PP,UA,1147.87960333801,,0.0,0.0,0.0,0.0,1982.0,2007.0,,,48.0331,38.28615,[nan],"{'GEO': ['GEO42995'], 'GPD': ['WRI1005106'], 'CARMA': ['CARMA51083']}" -1423,Zaporozhye,Nuclear,,PP,UA,5705.67497872675,,0.0,0.0,0.0,0.0,1985.0,1996.0,,,47.5119,34.5863,[nan],"{'GEO': ['GEO6207'], 'GPD': ['WRI1005114'], 'CARMA': ['CARMA50875']}" -1424,Trypilska,Hard Coal,Steam Turbine,PP,UA,1659.5849686814602,,0.0,0.0,0.0,0.0,1969.0,1972.0,,,50.1344,30.7468,[nan],"{'GEO': ['GEO43000'], 'GPD': ['WRI1005099'], 'CARMA': ['CARMA46410']}" -1425,Tashlyk,Hydro,Pumped Storage,Store,UA,285.55968954109585,,0.0,0.0,0.0,0.0,2006.0,2007.0,,,47.7968,31.1811,[nan],"{'GEO': ['GEO43025'], 'GPD': ['WRI1005117'], 'CARMA': ['CARMA44696']}" -1426,Starobeshivska,Hard Coal,Steam Turbine,PP,UA,1636.5351774497733,,0.0,0.0,0.0,0.0,1961.0,1967.0,,,47.7997,38.00612,[nan],"{'GEO': ['GEO43003'], 'GPD': ['WRI1005105'], 'CARMA': ['CARMA43083']}" -1427,South,Nuclear,,PP,UA,2852.837489363375,,0.0,0.0,0.0,0.0,1983.0,1989.0,,,47.812,31.22,[nan],"{'GEO': ['GEO5475'], 'GPD': ['WRI1005113'], 'CARMA': ['CARMA42555']}" -1428,Rovno rivne,Nuclear,,PP,UA,2695.931427448389,,0.0,0.0,0.0,0.0,1981.0,2006.0,,,51.3245,25.89744,[nan],"{'GEO': ['GEO5174'], 'GPD': ['WRI1005112'], 'CARMA': ['CARMA38114']}" -1429,Ladyzhinska,Hard Coal,Steam Turbine,PP,UA,1659.5849686814602,,0.0,0.0,0.0,0.0,1970.0,1971.0,,,48.706,29.2202,[nan],"{'GEO': ['GEO42993'], 'GPD': ['WRI1005098'], 'CARMA': ['CARMA24024']}" -1430,Kiev,Hydro,Pumped Storage,PP,UA,635.8694635681177,,0.0,0.0,0.0,0.0,1964.0,1972.0,,,50.5998,30.501,"[nan, nan]","{'GEO': ['GEO43024', 'GEO43023'], 'GPD': ['WRI1005123', 'WRI1005124'], 'CARMA': ['CARMA23516', 'CARMA23517']}" -2450,Cet chisinau,Natural Gas,,PP,MD,306.0,,0.0,0.0,0.0,0.0,,,,,47.027550000000005,28.8801,"[nan, nan]","{'GPD': ['WRI1002985', 'WRI1002984'], 'CARMA': ['CARMA8450', 'CARMA8451']}" -2460,Hydropower che costesti,Hydro,,PP,MD,16.0,,0.0,0.0,0.0,0.0,1978.0,,,,47.8381,27.2246,[nan],"{'GPD': ['WRI1002987'], 'CARMA': ['CARMA9496']}" -2465,Moldavskaya gres,Hard Coal,,PP,MD,2520.0,,0.0,0.0,0.0,0.0,,,,,46.6292,29.9407,[nan],"{'GPD': ['WRI1002989'], 'CARMA': ['CARMA28979']}" -2466,Hydropower dubasari,Hydro,,PP,MD,48.0,,0.0,0.0,0.0,0.0,,,,,47.2778,29.123,[nan],"{'GPD': ['WRI1002988'], 'CARMA': ['CARMA11384']}" -2676,Cet nord balti,Natural Gas,,PP,MD,24.0,,0.0,0.0,0.0,0.0,,,,,47.7492,27.8938,[nan],"{'GPD': ['WRI1002986'], 'CARMA': ['CARMA3071']}" -2699,Dniprodzerzhynsk,Hydro,Reservoir,PP,UA,360.3503184713376,,0.0,0.0,0.0,0.0,1963.0,1964.0,,,48.5485,34.541015,[nan],"{'GEO': ['GEO43020'], 'GPD': ['WRI1005119']}" -2707,Burshtynska tes,Hard Coal,Steam Turbine,PP,UA,2212.779958241947,,0.0,0.0,0.0,0.0,1965.0,1984.0,,,49.21038,24.66654,[nan],"{'GEO': ['GEO42991'], 'GPD': ['WRI1005097']}" -2708,Danipro dnieper,Hydro,Reservoir,PP,UA,1484.8407643312103,,0.0,0.0,0.0,0.0,1932.0,1947.0,,,47.86944,35.08611,[nan],"{'GEO': ['GEO43016'], 'GPD': ['WRI1005120']}" -2709,Dniester,Hydro,Pumped Storage,Store,UA,612.7241020616891,,0.0,0.0,0.0,0.0,2009.0,2011.0,,,48.51361,27.47333,[nan],"{'GEO': ['GEO43022'], 'GPD': ['WRI1005116', 'WRI1005115']}" -2710,Kiev,Natural Gas,Steam Turbine,CHP,UA,458.2803237740955,,0.0,0.0,0.0,0.0,1982.0,1984.0,,,50.532,30.6625,[nan],"{'GEO': ['GEO42998'], 'GPD': ['WRI1005125']}" -2712,Luganskaya,Hard Coal,Steam Turbine,PP,UA,1060.2903966575996,,0.0,0.0,0.0,0.0,1962.0,1969.0,,,48.74781,39.2624,[nan],"{'GEO': ['GEO42996'], 'GPD': ['WRI1005110']}" -2713,Slavyanskaya,Hard Coal,Steam Turbine,PP,UA,737.5933194139823,,0.0,0.0,0.0,0.0,1971.0,1971.0,,,48.872,37.76567,[nan],"{'GEO': ['GEO43002'], 'GPD': ['WRI1005109']}" -2714,Vuhlehirska uglegorskaya,Hard Coal,Steam Turbine,PP,UA,3319.1699373629203,,0.0,0.0,0.0,0.0,1972.0,1977.0,,,48.4633,38.20328,[nan],"{'GEO': ['GEO43001'], 'GPD': ['WRI1005107']}" -2715,Zaporiska,Hard Coal,Steam Turbine,PP,UA,3319.1699373629203,,0.0,0.0,0.0,0.0,1972.0,1977.0,,,47.5089,34.6253,[nan],"{'GEO': ['GEO42988'], 'GPD': ['WRI1005101']}" -3678,Mironovskaya,Hard Coal,,PP,UA,815.0,,0.0,0.0,0.0,0.0,,,,,48.3407,38.4049,[nan],"{'GPD': ['WRI1005108'], 'CARMA': ['CARMA28679']}" -3679,Kramatorskaya,Hard Coal,,PP,UA,120.0,,0.0,0.0,0.0,0.0,1974.0,,,,48.7477,37.5723,[nan],"{'GPD': ['WRI1075856'], 'CARMA': ['CARMA54560']}" -3680,Chernihiv,Hard Coal,,PP,UA,200.0,,0.0,0.0,0.0,0.0,1968.0,,,,51.455,31.2602,[nan],"{'GPD': ['WRI1075853'], 'CARMA': ['CARMA8190']}" +,Name,Fueltype,Technology,Set,Country,Capacity,Efficiency,DateIn,DateRetrofit,DateOut,lat,lon,Duration,Volume_Mm3,DamHeight_m,StorageCapacity_MWh,EIC,projectID,Status +0,Almaraz,Nuclear,Steam Turbine,PP,Spain,0.0,,2045.0,,,39.807,-5.6986,,,,,,G100000500240,operating +1,Almaraz,Nuclear,Steam Turbine,PP,Spain,0.0,,2045.0,,,39.807,-5.6986,,,,,,G100000500239,operating +2,Asco,Nuclear,Steam Turbine,PP,Spain,0.0,,2045.0,,,41.1993,0.5694,,,,,,G100000500243,operating +3,Asco,Nuclear,Steam Turbine,PP,Spain,0.0,,2045.0,,,41.1993,0.5694,,,,,,G100000500242,operating +4,Belleville,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,47.5103,2.87501,,,,,,G100000500060,operating +5,Belleville,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,47.5103,2.87501,,,,,,G100000500061,operating +6,Beznau,Nuclear,Steam Turbine,PP,Switzerland,0.0,,2045.0,,,47.552,8.2281,,,,,,G100000500505,operating +7,Beznau,Nuclear,Steam Turbine,PP,Switzerland,0.0,,2045.0,,,47.552,8.2281,,,,,,G100000500504,operating +8,Blayais,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,45.256,-0.6932,,,,,,G100000500296,operating +9,Blayais,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,45.256,-0.6932,,,,,,G100000500295,operating +10,Blayais,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,45.256,-0.6932,,,,,,G100000500294,operating +11,Blayais,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,45.256,-0.6932,,,,,,G100000500293,operating +12,Bohunice,Nuclear,Steam Turbine,PP,Slovakia,1200.0,,2045.0,,,48.4968,17.6907,,,,,,G100000501269,pre-construction +13,Bohunice,Nuclear,Steam Turbine,PP,Slovakia,0.0,,2045.0,,,48.4968,17.6907,,,,,,G100000500460,operating +14,Bohunice,Nuclear,Steam Turbine,PP,Slovakia,0.0,,2045.0,,,48.4968,17.6907,,,,,,G100000500459,operating +15,Borssele,Nuclear,Steam Turbine,PP,Netherlands,0.0,,2045.0,,2033.0,51.4312,3.7174,,,,,,G100000500496,operating +16,Borssele,Nuclear,Steam Turbine,PP,Netherlands,1650.0,,2035.0,,,51.4312,3.7174,,,,,,G100000501226,pre-construction +17,Borssele,Nuclear,Steam Turbine,PP,Netherlands,1650.0,,2035.0,,,51.4312,3.7174,,,,,,G100000501225,pre-construction +18,Bradwell,Nuclear,Steam Turbine,PP,United Kingdom,1150.0,,2030.0,,,51.7428,0.9047,,,,,,G100000500710,pre-construction +19,Bradwell,Nuclear,Steam Turbine,PP,United Kingdom,1150.0,,2045.0,,,51.7428,0.9047,,,,,,G100000500716,pre-construction +20,Bugey,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,45.7973,5.2706,,,,,,G100000500309,operating +21,Bugey,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,45.7973,5.2706,,,,,,G100000500303,operating +22,Bugey,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,45.7973,5.2706,,,,,,G100000501176,announced +23,Bugey,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,45.7973,5.2706,,,,,,G100000500310,operating +24,Bugey,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,45.7973,5.2706,,,,,,G100000500304,operating +25,Bugey,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,45.7973,5.2706,,,,,,G100000501177,announced +26,Cattenom,Nuclear,Steam Turbine,PP,France,1362.0,,1991.0,,,49.416,6.2169,,,,,,G100000500068,operating +27,Cattenom,Nuclear,Steam Turbine,PP,France,1362.0,,1992.0,,,49.416,6.2169,,,,,,G100000500069,operating +28,Cattenom,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,49.416,6.2169,,,,,,G100000500066,operating +29,Cattenom,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,49.416,6.2169,,,,,,G100000500067,operating +30,Cernavoda,Nuclear,Steam Turbine,PP,Romania,720.0,,2031.0,,,44.32401,28.0561,,,,,,G100000500674,pre-construction +31,Cernavoda,Nuclear,Steam Turbine,PP,Romania,720.0,,2032.0,,,44.3229,28.0573,,,,,,G100000500675,pre-construction +32,Cernavoda,Nuclear,Steam Turbine,PP,Romania,706.0,,1996.0,,,44.3205,28.0598,,,,,,G100000500375,operating +33,Cernavoda,Nuclear,Steam Turbine,PP,Romania,705.0,,2007.0,,,44.3218,28.0586,,,,,,G100000500377,operating +34,Chinon,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,47.2254,0.1656,,,,,,G100000500289,operating +35,Chinon,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,47.2254,0.1656,,,,,,G100000500292,operating +36,Chinon,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,47.2254,0.1656,,,,,,G100000500291,operating +37,Chinon,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,47.2254,0.1656,,,,,,G100000500290,operating +38,Chooz,Nuclear,Steam Turbine,PP,France,1560.0,,2000.0,,,50.0901,4.7872,,,,,,G100000500011,operating +39,Chooz,Nuclear,Steam Turbine,PP,France,1560.0,,2000.0,,,50.0901,4.7872,,,,,,G100000500010,operating +40,Civaux,Nuclear,Steam Turbine,PP,France,1561.0,,2002.0,,,46.4555,0.6582,,,,,,G100000500008,operating +41,Civaux,Nuclear,Steam Turbine,PP,France,1561.0,,2002.0,,,46.4555,0.6582,,,,,,G100000500009,operating +42,Cofrentes,Nuclear,Steam Turbine,PP,Spain,0.0,,2045.0,,,39.2134,-1.0509,,,,,,G100000500149,operating +43,Cruas,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,44.6325,4.7546,,,,,,G100000500279,operating +44,Cruas,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,44.6325,4.7546,,,,,,G100001017698,operating +45,Cruas,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,44.6325,4.7546,,,,,,G100000500281,operating +46,Cruas,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,44.6325,4.7546,,,,,,G100000500282,operating +47,Dabrowa Gornicza,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.3869042,19.336934,,,,,,G100001009049,announced +48,Dabrowa Gornicza,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.3869042,19.336934,,,,,,G100001009048,announced +49,Dabrowa Gornicza,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.3869042,19.336934,,,,,,G100001009047,announced +50,Dabrowa Gornicza,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.3869042,19.336934,,,,,,G100001009046,announced +51,Dampierre,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,47.7321,2.5185,,,,,,G100000500308,operating +52,Dampierre,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,47.7321,2.5185,,,,,,G100000500307,operating +53,Dampierre,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,47.7321,2.5185,,,,,,G100000500306,operating +54,Dampierre,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,47.7321,2.5185,,,,,,G100000500305,operating +55,Doel,Nuclear,Steam Turbine,PP,Belgium,0.0,,2045.0,,,51.3254,4.2597,,,,,,G100000500499,operating +56,Doel,Nuclear,Steam Turbine,PP,Belgium,0.0,,2045.0,,,51.3254,4.2597,,,,,,G100000500500,operating +57,Doel,Nuclear,Steam Turbine,PP,Belgium,0.0,,2045.0,,,51.3254,4.2597,,,,,,G100000500234,operating +58,Doicesti,Nuclear,Steam Turbine,PP,Romania,77.0,,2030.0,,,44.9967,25.3672,,,,,,G100000500876,pre-construction +59,Doicesti,Nuclear,Steam Turbine,PP,Romania,77.0,,2030.0,,,44.9967,25.3672,,,,,,G100000500875,pre-construction +60,Doicesti,Nuclear,Steam Turbine,PP,Romania,77.0,,2030.0,,,44.9967,25.3672,,,,,,G100000500874,pre-construction +61,Doicesti,Nuclear,Steam Turbine,PP,Romania,77.0,,2030.0,,,44.9967,25.3672,,,,,,G100000500872,pre-construction +62,Doicesti,Nuclear,Steam Turbine,PP,Romania,77.0,,2030.0,,,44.9967,25.3672,,,,,,G100000500873,pre-construction +63,Doicesti,Nuclear,Steam Turbine,PP,Romania,77.0,,2030.0,,,44.9967,25.3672,,,,,,G100000500871,pre-construction +64,Flamanville,Nuclear,Steam Turbine,PP,France,1650.0,,2024.0,,,49.5366,-1.8823,,,,,,G100000500007,construction +65,Flamanville,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,49.5366,-1.8823,,,,,,G100000500051,operating +66,Flamanville,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,49.5366,-1.8823,,,,,,G100000500050,operating +67,Forsmark,Nuclear,Steam Turbine,PP,Sweden,0.0,,2045.0,,,60.4028,18.1744,,,,,,G100000500246,operating +68,Forsmark,Nuclear,Steam Turbine,PP,Sweden,0.0,,2045.0,,,60.4028,18.1744,,,,,,G100000500146,operating +69,Forsmark,Nuclear,Steam Turbine,PP,Sweden,0.0,,2045.0,,,60.4028,18.1744,,,,,,G100000500128,operating +70,Goesgen,Nuclear,Steam Turbine,PP,Switzerland,0.0,,2045.0,,,47.3656,7.96801,,,,,,G100000500203,operating +71,Golfech,Nuclear,Steam Turbine,PP,France,1363.0,,1991.0,,,44.106,0.8443,,,,,,G100000500062,operating +72,Golfech,Nuclear,Steam Turbine,PP,France,1363.0,,1994.0,,,44.106,0.8443,,,,,,G100000500063,operating +73,Gravelines,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,51.0141,2.1332,,,,,,G100000500301,operating +74,Gravelines,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,51.0141,2.1332,,,,,,G100000500299,operating +75,Gravelines,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,51.0141,2.1332,,,,,,G100000501179,announced +76,Gravelines,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,51.0141,2.1332,,,,,,G100000500298,operating +77,Gravelines,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,51.0141,2.1332,,,,,,G100000501178,announced +78,Gravelines,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,51.0141,2.1332,,,,,,G100000500297,operating +79,Gravelines,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,51.0141,2.1332,,,,,,G100000500300,operating +80,Gravelines,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,51.0141,2.1332,,,,,,G100000500302,operating +81,Hartlepool,Nuclear,Steam Turbine,PP,United Kingdom,0.0,,2045.0,,,54.6341,-1.1801,,,,,,G100000501025,operating +82,Hartlepool,Nuclear,Steam Turbine,PP,United Kingdom,0.0,,2045.0,,,54.6341,-1.1801,,,,,,G100000501026,operating +83,Heysham,Nuclear,Steam Turbine,PP,United Kingdom,0.0,,2045.0,,,54.0295,-2.9149,,,,,,G100000501028,operating +84,Heysham,Nuclear,Steam Turbine,PP,United Kingdom,0.0,,2045.0,,,54.0295,-2.9149,,,,,,G100000501027,operating +85,Heysham,Nuclear,Steam Turbine,PP,United Kingdom,0.0,,2045.0,,,54.03101,-2.9112,,,,,,G100000501030,operating +86,Heysham,Nuclear,Steam Turbine,PP,United Kingdom,0.0,,2045.0,,,54.03101,-2.9112,,,,,,G100000501029,operating +87,Hinkley Point,Nuclear,Steam Turbine,PP,United Kingdom,1720.0,,2030.0,,,51.2063,-3.1423,,,,,,G100000501036,construction +88,Hinkley Point,Nuclear,Steam Turbine,PP,United Kingdom,1720.0,,2029.0,,,51.2063,-3.1423,,,,,,G100000501035,construction +89,Katowice,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.2649,19.0238,,,,,,G100000501235,announced +90,Katowice,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.2649,19.0238,,,,,,G100000501236,announced +91,Katowice,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.2649,19.0238,,,,,,G100000501244,announced +92,Katowice,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.2649,19.0238,,,,,,G100000501243,announced +93,Katowice,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.2649,19.0238,,,,,,G100000501242,announced +94,Katowice,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.2649,19.0238,,,,,,G100000501241,announced +95,Katowice,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.2649,19.0238,,,,,,G100000501237,announced +96,Katowice,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.2649,19.0238,,,,,,G100000501240,announced +97,Katowice,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.2649,19.0238,,,,,,G100000501239,announced +98,Katowice,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.2649,19.0238,,,,,,G100000501238,announced +99,Kghm Nuscale Voygr Modular,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,52.3210375,17.2786248,,,,,,G100001009044,announced +100,Kghm Nuscale Voygr Modular,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,52.3210375,17.2786248,,,,,,G100001009045,announced +101,Kghm Nuscale Voygr Modular,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,52.3210375,17.2786248,,,,,,G100001009041,announced +102,Kghm Nuscale Voygr Modular,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,52.3210375,17.2786248,,,,,,G100001009042,announced +103,Kghm Nuscale Voygr Modular,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,52.3210375,17.2786248,,,,,,G100001009043,announced +104,Kghm Nuscale Voygr Modular,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,52.3210375,17.2786248,,,,,,G100001009040,announced +111,Kozloduy,Nuclear,Steam Turbine,PP,Bulgaria,1000.0,,2033.0,,,43.7497,23.7685,,,,,,G100000500717,pre-construction +112,Kozloduy,Nuclear,Steam Turbine,PP,Bulgaria,0.0,,2045.0,,,43.7497,23.7685,,,,,,G100000500192,operating +113,Kozloduy,Nuclear,Steam Turbine,PP,Bulgaria,1000.0,,2036.0,,,43.7497,23.7685,,,,,,G100000500718,pre-construction +114,Kozloduy,Nuclear,Steam Turbine,PP,Bulgaria,1040.0,,1993.0,,,43.7497,23.7685,,,,,,G100000500191,operating +115,Krsko,Nuclear,Steam Turbine,PP,Slovenia,0.0,,2045.0,,,45.9389,15.51601,,,,,,G100000500381,announced +116,Krsko,Nuclear,Steam Turbine,PP,Slovenia,0.0,,2045.0,,2043.0,45.9389,15.51601,,,,,,G100000500380,operating +117,Leibstadt,Nuclear,Steam Turbine,PP,Switzerland,0.0,,2045.0,,,47.6012,8.1845,,,,,,G100000500087,operating +118,Loviisa,Nuclear,Steam Turbine,PP,Finland,0.0,,2045.0,,2050.0,60.3708,26.3468,,,,,,G100000500449,operating +119,Loviisa,Nuclear,Steam Turbine,PP,Finland,0.0,,2045.0,,2050.0,60.3708,26.3468,,,,,,G100000500448,operating +120,Lubiatowo Kopalino,Nuclear,Steam Turbine,PP,Poland,1250.0,,2037.0,,,54.8036,17.7881,,,,,,G100000500840,pre-construction +121,Lubiatowo Kopalino,Nuclear,Steam Turbine,PP,Poland,1250.0,,2035.0,,,54.8036,17.7881,,,,,,G100000500839,pre-construction +122,Lubiatowo Kopalino,Nuclear,Steam Turbine,PP,Poland,1250.0,,2032.0,,,54.8036,17.7881,,,,,,G100000500838,pre-construction +123,Mochovce,Nuclear,Steam Turbine,PP,Slovakia,500.0,,2000.0,,,48.2572,18.4553,,,,,,G100000500462,operating +124,Mochovce,Nuclear,Steam Turbine,PP,Slovakia,471.0,,2023.0,,,48.2572,18.4553,,,,,,G100000500490,operating +125,Mochovce,Nuclear,Steam Turbine,PP,Slovakia,500.0,,1998.0,,,48.2572,18.4553,,,,,,G100000500461,operating +126,Mochovce,Nuclear,Steam Turbine,PP,Slovakia,471.0,,2025.0,,,48.2572,18.4553,,,,,,G100000500491,construction +127,Moorside Clean Energy Hub,Nuclear,Steam Turbine,PP,United Kingdom,0.0,,2045.0,,,54.4296,-3.5109,,,,,,G100000501316,announced +128,Moorside Clean Energy Hub,Nuclear,Steam Turbine,PP,United Kingdom,0.0,,2045.0,,,54.4296,-3.5109,,,,,,G100000501317,announced +129,Nogent,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,48.5171,3.5181,,,,,,G100000500065,operating +130,Nogent,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,48.5171,3.5181,,,,,,G100000500064,operating +131,Nowa Huta,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.0656753,20.0365823,,,,,,G100001009053,announced +132,Nowa Huta,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.0656753,20.0365823,,,,,,G100001009051,announced +133,Nowa Huta,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.0656753,20.0365823,,,,,,G100001009052,announced +134,Nowa Huta,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.0656753,20.0365823,,,,,,G100001009050,announced +135,Olkiluoto,Nuclear,Steam Turbine,PP,Finland,0.0,,2045.0,,,61.2371,21.4433,,,,,,G100000500356,operating +136,Olkiluoto,Nuclear,Steam Turbine,PP,Finland,0.0,,2045.0,,,61.2371,21.4433,,,,,,G100000500355,operating +137,Olkiluoto,Nuclear,Steam Turbine,PP,Finland,1720.0,,2022.0,,,61.2356,21.4372,,,,,,G100000500006,operating +138,Oskarshamn,Nuclear,Steam Turbine,PP,Sweden,0.0,,2045.0,,,57.4166,16.67301,,,,,,G100000500041,operating +139,Ostroleka,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,53.0674009,21.5610332,,,,,,G100001009064,announced +140,Ostroleka,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,53.0674009,21.5610332,,,,,,G100001009062,announced +141,Ostroleka,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,53.0674009,21.5610332,,,,,,G100001009063,announced +142,Ostroleka,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,53.0674009,21.5610332,,,,,,G100001009065,announced +143,Paks,Nuclear,Steam Turbine,PP,Hungary,0.0,,2045.0,,,46.57763,18.8532,,,,,,G100000500455,operating +144,Paks,Nuclear,Steam Turbine,PP,Hungary,0.0,,2045.0,,,46.57763,18.8532,,,,,,G100000500456,operating +145,Paks,Nuclear,Steam Turbine,PP,Hungary,1265.0,,2032.0,,,46.57763,18.8532,,,,,,G100000500624,pre-construction +146,Paks,Nuclear,Steam Turbine,PP,Hungary,0.0,,2045.0,,,46.57763,18.8532,,,,,,G100000500457,operating +147,Paks,Nuclear,Steam Turbine,PP,Hungary,0.0,,2045.0,,,46.57763,18.8532,,,,,,G100000500454,operating +148,Paks,Nuclear,Steam Turbine,PP,Hungary,1265.0,,2032.0,,,46.57763,18.8532,,,,,,G100000500623,pre-construction +149,Paluel,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,49.8582,0.6354,,,,,,G100000500053,operating +150,Paluel,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,49.8582,0.6354,,,,,,G100000500055,operating +151,Paluel,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,49.8582,0.6354,,,,,,G100000500054,operating +152,Paluel,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,49.8582,0.6354,,,,,,G100000500052,operating +153,Patnow,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,51.1465,18.6072,,,,,,G100000501245,announced +154,Patnow,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,51.1465,18.6072,,,,,,G100001009038,announced +155,Penly,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,49.9764,1.2107,,,,,,G100000500056,operating +156,Penly,Nuclear,Steam Turbine,PP,France,1382.0,,1992.0,,,49.9764,1.2107,,,,,,G100000500057,operating +157,Penly,Nuclear,Steam Turbine,PP,France,1650.0,,2035.0,,,49.9764,1.2107,,,,,,G100000501185,pre-construction +158,Penly,Nuclear,Steam Turbine,PP,France,1650.0,,2045.0,,,49.9764,1.2107,,,,,,G100000501186,pre-construction +159,Ringhals,Nuclear,Steam Turbine,PP,Sweden,0.0,,2045.0,,,57.2574,12.1087,,,,,,G100000500186,operating +160,Ringhals,Nuclear,Steam Turbine,PP,Sweden,0.0,,2045.0,,,57.2574,12.1087,,,,,,G100000500129,operating +165,Saint Laurent,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,47.7197,1.5783,,,,,,G100000500284,operating +166,Saint Laurent,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,47.7197,1.5783,,,,,,G100000500283,operating +167,Sizewell,Nuclear,Steam Turbine,PP,United Kingdom,1600.0,,2045.0,,,52.2199,1.6203,,,,,,G100000500708,pre-construction +168,Sizewell,Nuclear,Steam Turbine,PP,United Kingdom,1600.0,,2045.0,,,52.2199,1.6203,,,,,,G100001017733,pre-construction +169,Sizewell,Nuclear,Steam Turbine,PP,United Kingdom,1250.0,,1995.0,,,52.2145,1.6206,,,,,,G100000501045,operating +170,Sizewell,Nuclear,Steam Turbine,PP,United Kingdom,1600.0,,2045.0,,,52.2199,1.6203,,,,,,G100000500709,pre-construction +171,Sizewell,Nuclear,Steam Turbine,PP,United Kingdom,1600.0,,2045.0,,,52.2199,1.6203,,,,,,G100001017734,pre-construction +175,St Alban,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,45.4043,4.7554,,,,,,G100000500059,operating +176,St Alban,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,45.4043,4.7554,,,,,,G100000500058,operating +177,Stawy Monowskie,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.024621,19.3339713,,,,,,G100001009058,announced +178,Stawy Monowskie,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.024621,19.3339713,,,,,,G100001009060,announced +179,Stawy Monowskie,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.024621,19.3339713,,,,,,G100001009061,announced +180,Stawy Monowskie,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.024621,19.3339713,,,,,,G100001009059,announced +181,Tarnobrzeg Special Economic Zone,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.5243466,21.6254433,,,,,,G100001009057,announced +182,Tarnobrzeg Special Economic Zone,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.5243466,21.6254433,,,,,,G100001009056,announced +183,Tarnobrzeg Special Economic Zone,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.5243466,21.6254433,,,,,,G100001009055,announced +184,Tarnobrzeg Special Economic Zone,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.5243466,21.6254433,,,,,,G100001009054,announced +185,Tihange,Nuclear,Steam Turbine,PP,Belgium,0.0,,2045.0,,,50.5334,5.2714,,,,,,G100000500205,operating +186,Tihange,Nuclear,Steam Turbine,PP,Belgium,0.0,,2045.0,,,50.5334,5.2714,,,,,,G100000500257,operating +187,Torness,Nuclear,Steam Turbine,PP,United Kingdom,0.0,,2045.0,,,55.9679,-2.4086,,,,,,G100000500435,operating +188,Torness,Nuclear,Steam Turbine,PP,United Kingdom,0.0,,2045.0,,,55.9679,-2.4086,,,,,,G100000500434,operating +189,Tricastin,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,44.3311,4.7311,,,,,,G100000500288,operating +190,Tricastin,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,44.3311,4.7311,,,,,,G100000500285,operating +191,Tricastin,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,44.3311,4.7311,,,,,,G100000500286,operating +192,Tricastin,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,44.3311,4.7311,,,,,,G100000500287,operating +193,Trillo,Nuclear,Steam Turbine,PP,Spain,0.0,,2045.0,,,40.7016,-2.6227,,,,,,G100000500202,operating +194,Vandellos,Nuclear,Steam Turbine,PP,Spain,0.0,,2045.0,,,40.9511,0.8662,,,,,,G100000500241,operating +195,Wloclawek,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,52.6718091,19.0537634,,,,,,G100001009068,announced +196,Wloclawek,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,52.6718091,19.0537634,,,,,,G100001009066,announced +197,Wloclawek,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,52.6718091,19.0537634,,,,,,G100001009067,announced +198,Wloclawek,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,52.6718091,19.0537634,,,,,,G100001009069,announced From 66b228a8dae513ae90c2fcc5658c1229cf0f0f2d Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 26 Aug 2024 17:27:09 +0200 Subject: [PATCH 243/293] remove duplicated download_wdpa --- rules/retrieve.smk | 55 ---------------------------------------------- 1 file changed, 55 deletions(-) diff --git a/rules/retrieve.smk b/rules/retrieve.smk index bf5fa012e..6a5b64bca 100644 --- a/rules/retrieve.smk +++ b/rules/retrieve.smk @@ -356,61 +356,6 @@ if config["enable"]["retrieve"]: shell("ogr2ogr -f gpkg -update -append {output.gpkg} {layer_path}") - -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 - - def check_file_exists(url): - response = requests.head(url) - return response.status_code == 200 - - # Basic pattern where WDPA files can be found - url_pattern = ( - "https://d1gam3xoknrgr2.cloudfront.net/current/WDPA_{bYYYY}_Public_shp.zip" - ) - - # 3-letter month + 4 digit year for current/previous/next month to test - current_monthyear = datetime.now().strftime("%b%Y") - prev_monthyear = (datetime.now() - timedelta(30)).strftime("%b%Y") - next_monthyear = (datetime.now() + timedelta(30)).strftime("%b%Y") - - # Test prioritised: current month -> previous -> next - for bYYYY in [current_monthyear, prev_monthyear, next_monthyear]: - if check_file_exists(url := url_pattern.format(bYYYY=bYYYY)): - break - else: - # If None of the three URLs are working - url = False - - assert ( - url - ), f"No WDPA files found at {url_pattern} for bY='{current_monthyear}, {prev_monthyear}, or {next_monthyear}'" - - # Downloading protected area database from WDPA - # extract the main zip and then merge the contained 3 zipped shapefiles - # Website: https://www.protectedplanet.net/en/thematic-areas/wdpa - rule download_wdpa: - input: - HTTP.remote(url, keep_local=True), - params: - zip="data/WDPA_shp.zip", - folder=directory("data/WDPA"), - output: - gpkg=protected("data/WDPA.gpkg"), - run: - shell("cp {input} {params.zip}") - shell("unzip -o {params.zip} -d {params.folder}") - for i in range(3): - # vsizip is special driver for directly working with zipped shapefiles in ogr2ogr - layer_path = ( - f"/vsizip/{params.folder}/WDPA_{bYYYY}_Public_shp_{i}.zip" - ) - print(f"Adding layer {i+1} of 3 to combined output file.") - shell("ogr2ogr -f gpkg -update -append {output.gpkg} {layer_path}") - - - if config["enable"]["retrieve"]: rule retrieve_monthly_co2_prices: From 2bef16ec7bdbb0d0740ab238de65e91bb070eb4d Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 26 Aug 2024 17:27:42 +0200 Subject: [PATCH 244/293] custom_powerplants: use iso2 country codes --- data/custom_powerplants.csv | 372 ++++++++++++++++++------------------ 1 file changed, 186 insertions(+), 186 deletions(-) diff --git a/data/custom_powerplants.csv b/data/custom_powerplants.csv index 72fce5180..14e8fba24 100644 --- a/data/custom_powerplants.csv +++ b/data/custom_powerplants.csv @@ -1,187 +1,187 @@ ,Name,Fueltype,Technology,Set,Country,Capacity,Efficiency,DateIn,DateRetrofit,DateOut,lat,lon,Duration,Volume_Mm3,DamHeight_m,StorageCapacity_MWh,EIC,projectID,Status -0,Almaraz,Nuclear,Steam Turbine,PP,Spain,0.0,,2045.0,,,39.807,-5.6986,,,,,,G100000500240,operating -1,Almaraz,Nuclear,Steam Turbine,PP,Spain,0.0,,2045.0,,,39.807,-5.6986,,,,,,G100000500239,operating -2,Asco,Nuclear,Steam Turbine,PP,Spain,0.0,,2045.0,,,41.1993,0.5694,,,,,,G100000500243,operating -3,Asco,Nuclear,Steam Turbine,PP,Spain,0.0,,2045.0,,,41.1993,0.5694,,,,,,G100000500242,operating -4,Belleville,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,47.5103,2.87501,,,,,,G100000500060,operating -5,Belleville,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,47.5103,2.87501,,,,,,G100000500061,operating -6,Beznau,Nuclear,Steam Turbine,PP,Switzerland,0.0,,2045.0,,,47.552,8.2281,,,,,,G100000500505,operating -7,Beznau,Nuclear,Steam Turbine,PP,Switzerland,0.0,,2045.0,,,47.552,8.2281,,,,,,G100000500504,operating -8,Blayais,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,45.256,-0.6932,,,,,,G100000500296,operating -9,Blayais,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,45.256,-0.6932,,,,,,G100000500295,operating -10,Blayais,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,45.256,-0.6932,,,,,,G100000500294,operating -11,Blayais,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,45.256,-0.6932,,,,,,G100000500293,operating -12,Bohunice,Nuclear,Steam Turbine,PP,Slovakia,1200.0,,2045.0,,,48.4968,17.6907,,,,,,G100000501269,pre-construction -13,Bohunice,Nuclear,Steam Turbine,PP,Slovakia,0.0,,2045.0,,,48.4968,17.6907,,,,,,G100000500460,operating -14,Bohunice,Nuclear,Steam Turbine,PP,Slovakia,0.0,,2045.0,,,48.4968,17.6907,,,,,,G100000500459,operating -15,Borssele,Nuclear,Steam Turbine,PP,Netherlands,0.0,,2045.0,,2033.0,51.4312,3.7174,,,,,,G100000500496,operating -16,Borssele,Nuclear,Steam Turbine,PP,Netherlands,1650.0,,2035.0,,,51.4312,3.7174,,,,,,G100000501226,pre-construction -17,Borssele,Nuclear,Steam Turbine,PP,Netherlands,1650.0,,2035.0,,,51.4312,3.7174,,,,,,G100000501225,pre-construction -18,Bradwell,Nuclear,Steam Turbine,PP,United Kingdom,1150.0,,2030.0,,,51.7428,0.9047,,,,,,G100000500710,pre-construction -19,Bradwell,Nuclear,Steam Turbine,PP,United Kingdom,1150.0,,2045.0,,,51.7428,0.9047,,,,,,G100000500716,pre-construction -20,Bugey,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,45.7973,5.2706,,,,,,G100000500309,operating -21,Bugey,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,45.7973,5.2706,,,,,,G100000500303,operating -22,Bugey,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,45.7973,5.2706,,,,,,G100000501176,announced -23,Bugey,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,45.7973,5.2706,,,,,,G100000500310,operating -24,Bugey,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,45.7973,5.2706,,,,,,G100000500304,operating -25,Bugey,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,45.7973,5.2706,,,,,,G100000501177,announced -26,Cattenom,Nuclear,Steam Turbine,PP,France,1362.0,,1991.0,,,49.416,6.2169,,,,,,G100000500068,operating -27,Cattenom,Nuclear,Steam Turbine,PP,France,1362.0,,1992.0,,,49.416,6.2169,,,,,,G100000500069,operating -28,Cattenom,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,49.416,6.2169,,,,,,G100000500066,operating -29,Cattenom,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,49.416,6.2169,,,,,,G100000500067,operating -30,Cernavoda,Nuclear,Steam Turbine,PP,Romania,720.0,,2031.0,,,44.32401,28.0561,,,,,,G100000500674,pre-construction -31,Cernavoda,Nuclear,Steam Turbine,PP,Romania,720.0,,2032.0,,,44.3229,28.0573,,,,,,G100000500675,pre-construction -32,Cernavoda,Nuclear,Steam Turbine,PP,Romania,706.0,,1996.0,,,44.3205,28.0598,,,,,,G100000500375,operating -33,Cernavoda,Nuclear,Steam Turbine,PP,Romania,705.0,,2007.0,,,44.3218,28.0586,,,,,,G100000500377,operating -34,Chinon,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,47.2254,0.1656,,,,,,G100000500289,operating -35,Chinon,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,47.2254,0.1656,,,,,,G100000500292,operating -36,Chinon,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,47.2254,0.1656,,,,,,G100000500291,operating -37,Chinon,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,47.2254,0.1656,,,,,,G100000500290,operating -38,Chooz,Nuclear,Steam Turbine,PP,France,1560.0,,2000.0,,,50.0901,4.7872,,,,,,G100000500011,operating -39,Chooz,Nuclear,Steam Turbine,PP,France,1560.0,,2000.0,,,50.0901,4.7872,,,,,,G100000500010,operating -40,Civaux,Nuclear,Steam Turbine,PP,France,1561.0,,2002.0,,,46.4555,0.6582,,,,,,G100000500008,operating -41,Civaux,Nuclear,Steam Turbine,PP,France,1561.0,,2002.0,,,46.4555,0.6582,,,,,,G100000500009,operating -42,Cofrentes,Nuclear,Steam Turbine,PP,Spain,0.0,,2045.0,,,39.2134,-1.0509,,,,,,G100000500149,operating -43,Cruas,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,44.6325,4.7546,,,,,,G100000500279,operating -44,Cruas,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,44.6325,4.7546,,,,,,G100001017698,operating -45,Cruas,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,44.6325,4.7546,,,,,,G100000500281,operating -46,Cruas,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,44.6325,4.7546,,,,,,G100000500282,operating -47,Dabrowa Gornicza,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.3869042,19.336934,,,,,,G100001009049,announced -48,Dabrowa Gornicza,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.3869042,19.336934,,,,,,G100001009048,announced -49,Dabrowa Gornicza,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.3869042,19.336934,,,,,,G100001009047,announced -50,Dabrowa Gornicza,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.3869042,19.336934,,,,,,G100001009046,announced -51,Dampierre,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,47.7321,2.5185,,,,,,G100000500308,operating -52,Dampierre,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,47.7321,2.5185,,,,,,G100000500307,operating -53,Dampierre,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,47.7321,2.5185,,,,,,G100000500306,operating -54,Dampierre,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,47.7321,2.5185,,,,,,G100000500305,operating -55,Doel,Nuclear,Steam Turbine,PP,Belgium,0.0,,2045.0,,,51.3254,4.2597,,,,,,G100000500499,operating -56,Doel,Nuclear,Steam Turbine,PP,Belgium,0.0,,2045.0,,,51.3254,4.2597,,,,,,G100000500500,operating -57,Doel,Nuclear,Steam Turbine,PP,Belgium,0.0,,2045.0,,,51.3254,4.2597,,,,,,G100000500234,operating -58,Doicesti,Nuclear,Steam Turbine,PP,Romania,77.0,,2030.0,,,44.9967,25.3672,,,,,,G100000500876,pre-construction -59,Doicesti,Nuclear,Steam Turbine,PP,Romania,77.0,,2030.0,,,44.9967,25.3672,,,,,,G100000500875,pre-construction -60,Doicesti,Nuclear,Steam Turbine,PP,Romania,77.0,,2030.0,,,44.9967,25.3672,,,,,,G100000500874,pre-construction -61,Doicesti,Nuclear,Steam Turbine,PP,Romania,77.0,,2030.0,,,44.9967,25.3672,,,,,,G100000500872,pre-construction -62,Doicesti,Nuclear,Steam Turbine,PP,Romania,77.0,,2030.0,,,44.9967,25.3672,,,,,,G100000500873,pre-construction -63,Doicesti,Nuclear,Steam Turbine,PP,Romania,77.0,,2030.0,,,44.9967,25.3672,,,,,,G100000500871,pre-construction -64,Flamanville,Nuclear,Steam Turbine,PP,France,1650.0,,2024.0,,,49.5366,-1.8823,,,,,,G100000500007,construction -65,Flamanville,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,49.5366,-1.8823,,,,,,G100000500051,operating -66,Flamanville,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,49.5366,-1.8823,,,,,,G100000500050,operating -67,Forsmark,Nuclear,Steam Turbine,PP,Sweden,0.0,,2045.0,,,60.4028,18.1744,,,,,,G100000500246,operating -68,Forsmark,Nuclear,Steam Turbine,PP,Sweden,0.0,,2045.0,,,60.4028,18.1744,,,,,,G100000500146,operating -69,Forsmark,Nuclear,Steam Turbine,PP,Sweden,0.0,,2045.0,,,60.4028,18.1744,,,,,,G100000500128,operating -70,Goesgen,Nuclear,Steam Turbine,PP,Switzerland,0.0,,2045.0,,,47.3656,7.96801,,,,,,G100000500203,operating -71,Golfech,Nuclear,Steam Turbine,PP,France,1363.0,,1991.0,,,44.106,0.8443,,,,,,G100000500062,operating -72,Golfech,Nuclear,Steam Turbine,PP,France,1363.0,,1994.0,,,44.106,0.8443,,,,,,G100000500063,operating -73,Gravelines,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,51.0141,2.1332,,,,,,G100000500301,operating -74,Gravelines,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,51.0141,2.1332,,,,,,G100000500299,operating -75,Gravelines,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,51.0141,2.1332,,,,,,G100000501179,announced -76,Gravelines,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,51.0141,2.1332,,,,,,G100000500298,operating -77,Gravelines,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,51.0141,2.1332,,,,,,G100000501178,announced -78,Gravelines,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,51.0141,2.1332,,,,,,G100000500297,operating -79,Gravelines,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,51.0141,2.1332,,,,,,G100000500300,operating -80,Gravelines,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,51.0141,2.1332,,,,,,G100000500302,operating -81,Hartlepool,Nuclear,Steam Turbine,PP,United Kingdom,0.0,,2045.0,,,54.6341,-1.1801,,,,,,G100000501025,operating -82,Hartlepool,Nuclear,Steam Turbine,PP,United Kingdom,0.0,,2045.0,,,54.6341,-1.1801,,,,,,G100000501026,operating -83,Heysham,Nuclear,Steam Turbine,PP,United Kingdom,0.0,,2045.0,,,54.0295,-2.9149,,,,,,G100000501028,operating -84,Heysham,Nuclear,Steam Turbine,PP,United Kingdom,0.0,,2045.0,,,54.0295,-2.9149,,,,,,G100000501027,operating -85,Heysham,Nuclear,Steam Turbine,PP,United Kingdom,0.0,,2045.0,,,54.03101,-2.9112,,,,,,G100000501030,operating -86,Heysham,Nuclear,Steam Turbine,PP,United Kingdom,0.0,,2045.0,,,54.03101,-2.9112,,,,,,G100000501029,operating -87,Hinkley Point,Nuclear,Steam Turbine,PP,United Kingdom,1720.0,,2030.0,,,51.2063,-3.1423,,,,,,G100000501036,construction -88,Hinkley Point,Nuclear,Steam Turbine,PP,United Kingdom,1720.0,,2029.0,,,51.2063,-3.1423,,,,,,G100000501035,construction -89,Katowice,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.2649,19.0238,,,,,,G100000501235,announced -90,Katowice,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.2649,19.0238,,,,,,G100000501236,announced -91,Katowice,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.2649,19.0238,,,,,,G100000501244,announced -92,Katowice,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.2649,19.0238,,,,,,G100000501243,announced -93,Katowice,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.2649,19.0238,,,,,,G100000501242,announced -94,Katowice,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.2649,19.0238,,,,,,G100000501241,announced -95,Katowice,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.2649,19.0238,,,,,,G100000501237,announced -96,Katowice,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.2649,19.0238,,,,,,G100000501240,announced -97,Katowice,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.2649,19.0238,,,,,,G100000501239,announced -98,Katowice,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.2649,19.0238,,,,,,G100000501238,announced -99,Kghm Nuscale Voygr Modular,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,52.3210375,17.2786248,,,,,,G100001009044,announced -100,Kghm Nuscale Voygr Modular,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,52.3210375,17.2786248,,,,,,G100001009045,announced -101,Kghm Nuscale Voygr Modular,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,52.3210375,17.2786248,,,,,,G100001009041,announced -102,Kghm Nuscale Voygr Modular,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,52.3210375,17.2786248,,,,,,G100001009042,announced -103,Kghm Nuscale Voygr Modular,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,52.3210375,17.2786248,,,,,,G100001009043,announced -104,Kghm Nuscale Voygr Modular,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,52.3210375,17.2786248,,,,,,G100001009040,announced -111,Kozloduy,Nuclear,Steam Turbine,PP,Bulgaria,1000.0,,2033.0,,,43.7497,23.7685,,,,,,G100000500717,pre-construction -112,Kozloduy,Nuclear,Steam Turbine,PP,Bulgaria,0.0,,2045.0,,,43.7497,23.7685,,,,,,G100000500192,operating -113,Kozloduy,Nuclear,Steam Turbine,PP,Bulgaria,1000.0,,2036.0,,,43.7497,23.7685,,,,,,G100000500718,pre-construction -114,Kozloduy,Nuclear,Steam Turbine,PP,Bulgaria,1040.0,,1993.0,,,43.7497,23.7685,,,,,,G100000500191,operating -115,Krsko,Nuclear,Steam Turbine,PP,Slovenia,0.0,,2045.0,,,45.9389,15.51601,,,,,,G100000500381,announced -116,Krsko,Nuclear,Steam Turbine,PP,Slovenia,0.0,,2045.0,,2043.0,45.9389,15.51601,,,,,,G100000500380,operating -117,Leibstadt,Nuclear,Steam Turbine,PP,Switzerland,0.0,,2045.0,,,47.6012,8.1845,,,,,,G100000500087,operating -118,Loviisa,Nuclear,Steam Turbine,PP,Finland,0.0,,2045.0,,2050.0,60.3708,26.3468,,,,,,G100000500449,operating -119,Loviisa,Nuclear,Steam Turbine,PP,Finland,0.0,,2045.0,,2050.0,60.3708,26.3468,,,,,,G100000500448,operating -120,Lubiatowo Kopalino,Nuclear,Steam Turbine,PP,Poland,1250.0,,2037.0,,,54.8036,17.7881,,,,,,G100000500840,pre-construction -121,Lubiatowo Kopalino,Nuclear,Steam Turbine,PP,Poland,1250.0,,2035.0,,,54.8036,17.7881,,,,,,G100000500839,pre-construction -122,Lubiatowo Kopalino,Nuclear,Steam Turbine,PP,Poland,1250.0,,2032.0,,,54.8036,17.7881,,,,,,G100000500838,pre-construction -123,Mochovce,Nuclear,Steam Turbine,PP,Slovakia,500.0,,2000.0,,,48.2572,18.4553,,,,,,G100000500462,operating -124,Mochovce,Nuclear,Steam Turbine,PP,Slovakia,471.0,,2023.0,,,48.2572,18.4553,,,,,,G100000500490,operating -125,Mochovce,Nuclear,Steam Turbine,PP,Slovakia,500.0,,1998.0,,,48.2572,18.4553,,,,,,G100000500461,operating -126,Mochovce,Nuclear,Steam Turbine,PP,Slovakia,471.0,,2025.0,,,48.2572,18.4553,,,,,,G100000500491,construction -127,Moorside Clean Energy Hub,Nuclear,Steam Turbine,PP,United Kingdom,0.0,,2045.0,,,54.4296,-3.5109,,,,,,G100000501316,announced -128,Moorside Clean Energy Hub,Nuclear,Steam Turbine,PP,United Kingdom,0.0,,2045.0,,,54.4296,-3.5109,,,,,,G100000501317,announced -129,Nogent,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,48.5171,3.5181,,,,,,G100000500065,operating -130,Nogent,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,48.5171,3.5181,,,,,,G100000500064,operating -131,Nowa Huta,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.0656753,20.0365823,,,,,,G100001009053,announced -132,Nowa Huta,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.0656753,20.0365823,,,,,,G100001009051,announced -133,Nowa Huta,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.0656753,20.0365823,,,,,,G100001009052,announced -134,Nowa Huta,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.0656753,20.0365823,,,,,,G100001009050,announced -135,Olkiluoto,Nuclear,Steam Turbine,PP,Finland,0.0,,2045.0,,,61.2371,21.4433,,,,,,G100000500356,operating -136,Olkiluoto,Nuclear,Steam Turbine,PP,Finland,0.0,,2045.0,,,61.2371,21.4433,,,,,,G100000500355,operating -137,Olkiluoto,Nuclear,Steam Turbine,PP,Finland,1720.0,,2022.0,,,61.2356,21.4372,,,,,,G100000500006,operating -138,Oskarshamn,Nuclear,Steam Turbine,PP,Sweden,0.0,,2045.0,,,57.4166,16.67301,,,,,,G100000500041,operating -139,Ostroleka,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,53.0674009,21.5610332,,,,,,G100001009064,announced -140,Ostroleka,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,53.0674009,21.5610332,,,,,,G100001009062,announced -141,Ostroleka,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,53.0674009,21.5610332,,,,,,G100001009063,announced -142,Ostroleka,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,53.0674009,21.5610332,,,,,,G100001009065,announced -143,Paks,Nuclear,Steam Turbine,PP,Hungary,0.0,,2045.0,,,46.57763,18.8532,,,,,,G100000500455,operating -144,Paks,Nuclear,Steam Turbine,PP,Hungary,0.0,,2045.0,,,46.57763,18.8532,,,,,,G100000500456,operating -145,Paks,Nuclear,Steam Turbine,PP,Hungary,1265.0,,2032.0,,,46.57763,18.8532,,,,,,G100000500624,pre-construction -146,Paks,Nuclear,Steam Turbine,PP,Hungary,0.0,,2045.0,,,46.57763,18.8532,,,,,,G100000500457,operating -147,Paks,Nuclear,Steam Turbine,PP,Hungary,0.0,,2045.0,,,46.57763,18.8532,,,,,,G100000500454,operating -148,Paks,Nuclear,Steam Turbine,PP,Hungary,1265.0,,2032.0,,,46.57763,18.8532,,,,,,G100000500623,pre-construction -149,Paluel,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,49.8582,0.6354,,,,,,G100000500053,operating -150,Paluel,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,49.8582,0.6354,,,,,,G100000500055,operating -151,Paluel,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,49.8582,0.6354,,,,,,G100000500054,operating -152,Paluel,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,49.8582,0.6354,,,,,,G100000500052,operating -153,Patnow,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,51.1465,18.6072,,,,,,G100000501245,announced -154,Patnow,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,51.1465,18.6072,,,,,,G100001009038,announced -155,Penly,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,49.9764,1.2107,,,,,,G100000500056,operating -156,Penly,Nuclear,Steam Turbine,PP,France,1382.0,,1992.0,,,49.9764,1.2107,,,,,,G100000500057,operating -157,Penly,Nuclear,Steam Turbine,PP,France,1650.0,,2035.0,,,49.9764,1.2107,,,,,,G100000501185,pre-construction -158,Penly,Nuclear,Steam Turbine,PP,France,1650.0,,2045.0,,,49.9764,1.2107,,,,,,G100000501186,pre-construction -159,Ringhals,Nuclear,Steam Turbine,PP,Sweden,0.0,,2045.0,,,57.2574,12.1087,,,,,,G100000500186,operating -160,Ringhals,Nuclear,Steam Turbine,PP,Sweden,0.0,,2045.0,,,57.2574,12.1087,,,,,,G100000500129,operating -165,Saint Laurent,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,47.7197,1.5783,,,,,,G100000500284,operating -166,Saint Laurent,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,47.7197,1.5783,,,,,,G100000500283,operating -167,Sizewell,Nuclear,Steam Turbine,PP,United Kingdom,1600.0,,2045.0,,,52.2199,1.6203,,,,,,G100000500708,pre-construction -168,Sizewell,Nuclear,Steam Turbine,PP,United Kingdom,1600.0,,2045.0,,,52.2199,1.6203,,,,,,G100001017733,pre-construction -169,Sizewell,Nuclear,Steam Turbine,PP,United Kingdom,1250.0,,1995.0,,,52.2145,1.6206,,,,,,G100000501045,operating -170,Sizewell,Nuclear,Steam Turbine,PP,United Kingdom,1600.0,,2045.0,,,52.2199,1.6203,,,,,,G100000500709,pre-construction -171,Sizewell,Nuclear,Steam Turbine,PP,United Kingdom,1600.0,,2045.0,,,52.2199,1.6203,,,,,,G100001017734,pre-construction -175,St Alban,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,45.4043,4.7554,,,,,,G100000500059,operating -176,St Alban,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,45.4043,4.7554,,,,,,G100000500058,operating -177,Stawy Monowskie,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.024621,19.3339713,,,,,,G100001009058,announced -178,Stawy Monowskie,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.024621,19.3339713,,,,,,G100001009060,announced -179,Stawy Monowskie,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.024621,19.3339713,,,,,,G100001009061,announced -180,Stawy Monowskie,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.024621,19.3339713,,,,,,G100001009059,announced -181,Tarnobrzeg Special Economic Zone,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.5243466,21.6254433,,,,,,G100001009057,announced -182,Tarnobrzeg Special Economic Zone,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.5243466,21.6254433,,,,,,G100001009056,announced -183,Tarnobrzeg Special Economic Zone,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.5243466,21.6254433,,,,,,G100001009055,announced -184,Tarnobrzeg Special Economic Zone,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,50.5243466,21.6254433,,,,,,G100001009054,announced -185,Tihange,Nuclear,Steam Turbine,PP,Belgium,0.0,,2045.0,,,50.5334,5.2714,,,,,,G100000500205,operating -186,Tihange,Nuclear,Steam Turbine,PP,Belgium,0.0,,2045.0,,,50.5334,5.2714,,,,,,G100000500257,operating -187,Torness,Nuclear,Steam Turbine,PP,United Kingdom,0.0,,2045.0,,,55.9679,-2.4086,,,,,,G100000500435,operating -188,Torness,Nuclear,Steam Turbine,PP,United Kingdom,0.0,,2045.0,,,55.9679,-2.4086,,,,,,G100000500434,operating -189,Tricastin,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,44.3311,4.7311,,,,,,G100000500288,operating -190,Tricastin,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,44.3311,4.7311,,,,,,G100000500285,operating -191,Tricastin,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,44.3311,4.7311,,,,,,G100000500286,operating -192,Tricastin,Nuclear,Steam Turbine,PP,France,0.0,,2045.0,,,44.3311,4.7311,,,,,,G100000500287,operating -193,Trillo,Nuclear,Steam Turbine,PP,Spain,0.0,,2045.0,,,40.7016,-2.6227,,,,,,G100000500202,operating -194,Vandellos,Nuclear,Steam Turbine,PP,Spain,0.0,,2045.0,,,40.9511,0.8662,,,,,,G100000500241,operating -195,Wloclawek,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,52.6718091,19.0537634,,,,,,G100001009068,announced -196,Wloclawek,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,52.6718091,19.0537634,,,,,,G100001009066,announced -197,Wloclawek,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,52.6718091,19.0537634,,,,,,G100001009067,announced -198,Wloclawek,Nuclear,Steam Turbine,PP,Poland,0.0,,2045.0,,,52.6718091,19.0537634,,,,,,G100001009069,announced +0,Almaraz,Nuclear,Steam Turbine,PP,ES,0.0,,2045.0,,,39.807,-5.6986,,,,,,G100000500240,operating +1,Almaraz,Nuclear,Steam Turbine,PP,ES,0.0,,2045.0,,,39.807,-5.6986,,,,,,G100000500239,operating +2,Asco,Nuclear,Steam Turbine,PP,ES,0.0,,2045.0,,,41.1993,0.5694,,,,,,G100000500243,operating +3,Asco,Nuclear,Steam Turbine,PP,ES,0.0,,2045.0,,,41.1993,0.5694,,,,,,G100000500242,operating +4,Belleville,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,47.5103,2.87501,,,,,,G100000500060,operating +5,Belleville,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,47.5103,2.87501,,,,,,G100000500061,operating +6,Beznau,Nuclear,Steam Turbine,PP,CH,0.0,,2045.0,,,47.552,8.2281,,,,,,G100000500505,operating +7,Beznau,Nuclear,Steam Turbine,PP,CH,0.0,,2045.0,,,47.552,8.2281,,,,,,G100000500504,operating +8,Blayais,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,45.256,-0.6932,,,,,,G100000500296,operating +9,Blayais,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,45.256,-0.6932,,,,,,G100000500295,operating +10,Blayais,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,45.256,-0.6932,,,,,,G100000500294,operating +11,Blayais,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,45.256,-0.6932,,,,,,G100000500293,operating +12,Bohunice,Nuclear,Steam Turbine,PP,SK,1200.0,,2045.0,,,48.4968,17.6907,,,,,,G100000501269,pre-construction +13,Bohunice,Nuclear,Steam Turbine,PP,SK,0.0,,2045.0,,,48.4968,17.6907,,,,,,G100000500460,operating +14,Bohunice,Nuclear,Steam Turbine,PP,SK,0.0,,2045.0,,,48.4968,17.6907,,,,,,G100000500459,operating +15,Borssele,Nuclear,Steam Turbine,PP,NL,0.0,,2045.0,,2033.0,51.4312,3.7174,,,,,,G100000500496,operating +16,Borssele,Nuclear,Steam Turbine,PP,NL,1650.0,,2035.0,,,51.4312,3.7174,,,,,,G100000501226,pre-construction +17,Borssele,Nuclear,Steam Turbine,PP,NL,1650.0,,2035.0,,,51.4312,3.7174,,,,,,G100000501225,pre-construction +18,Bradwell,Nuclear,Steam Turbine,PP,GB,1150.0,,2030.0,,,51.7428,0.9047,,,,,,G100000500710,pre-construction +19,Bradwell,Nuclear,Steam Turbine,PP,GB,1150.0,,2045.0,,,51.7428,0.9047,,,,,,G100000500716,pre-construction +20,Bugey,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,45.7973,5.2706,,,,,,G100000500309,operating +21,Bugey,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,45.7973,5.2706,,,,,,G100000500303,operating +22,Bugey,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,45.7973,5.2706,,,,,,G100000501176,announced +23,Bugey,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,45.7973,5.2706,,,,,,G100000500310,operating +24,Bugey,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,45.7973,5.2706,,,,,,G100000500304,operating +25,Bugey,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,45.7973,5.2706,,,,,,G100000501177,announced +26,Cattenom,Nuclear,Steam Turbine,PP,FR,1362.0,,1991.0,,,49.416,6.2169,,,,,,G100000500068,operating +27,Cattenom,Nuclear,Steam Turbine,PP,FR,1362.0,,1992.0,,,49.416,6.2169,,,,,,G100000500069,operating +28,Cattenom,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,49.416,6.2169,,,,,,G100000500066,operating +29,Cattenom,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,49.416,6.2169,,,,,,G100000500067,operating +30,Cernavoda,Nuclear,Steam Turbine,PP,RO,720.0,,2031.0,,,44.32401,28.0561,,,,,,G100000500674,pre-construction +31,Cernavoda,Nuclear,Steam Turbine,PP,RO,720.0,,2032.0,,,44.3229,28.0573,,,,,,G100000500675,pre-construction +32,Cernavoda,Nuclear,Steam Turbine,PP,RO,706.0,,1996.0,,,44.3205,28.0598,,,,,,G100000500375,operating +33,Cernavoda,Nuclear,Steam Turbine,PP,RO,705.0,,2007.0,,,44.3218,28.0586,,,,,,G100000500377,operating +34,Chinon,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,47.2254,0.1656,,,,,,G100000500289,operating +35,Chinon,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,47.2254,0.1656,,,,,,G100000500292,operating +36,Chinon,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,47.2254,0.1656,,,,,,G100000500291,operating +37,Chinon,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,47.2254,0.1656,,,,,,G100000500290,operating +38,Chooz,Nuclear,Steam Turbine,PP,FR,1560.0,,2000.0,,,50.0901,4.7872,,,,,,G100000500011,operating +39,Chooz,Nuclear,Steam Turbine,PP,FR,1560.0,,2000.0,,,50.0901,4.7872,,,,,,G100000500010,operating +40,Civaux,Nuclear,Steam Turbine,PP,FR,1561.0,,2002.0,,,46.4555,0.6582,,,,,,G100000500008,operating +41,Civaux,Nuclear,Steam Turbine,PP,FR,1561.0,,2002.0,,,46.4555,0.6582,,,,,,G100000500009,operating +42,Cofrentes,Nuclear,Steam Turbine,PP,ES,0.0,,2045.0,,,39.2134,-1.0509,,,,,,G100000500149,operating +43,Cruas,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,44.6325,4.7546,,,,,,G100000500279,operating +44,Cruas,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,44.6325,4.7546,,,,,,G100001017698,operating +45,Cruas,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,44.6325,4.7546,,,,,,G100000500281,operating +46,Cruas,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,44.6325,4.7546,,,,,,G100000500282,operating +47,Dabrowa Gornicza,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,50.3869042,19.336934,,,,,,G100001009049,announced +48,Dabrowa Gornicza,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,50.3869042,19.336934,,,,,,G100001009048,announced +49,Dabrowa Gornicza,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,50.3869042,19.336934,,,,,,G100001009047,announced +50,Dabrowa Gornicza,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,50.3869042,19.336934,,,,,,G100001009046,announced +51,Dampierre,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,47.7321,2.5185,,,,,,G100000500308,operating +52,Dampierre,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,47.7321,2.5185,,,,,,G100000500307,operating +53,Dampierre,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,47.7321,2.5185,,,,,,G100000500306,operating +54,Dampierre,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,47.7321,2.5185,,,,,,G100000500305,operating +55,Doel,Nuclear,Steam Turbine,PP,BE,0.0,,2045.0,,,51.3254,4.2597,,,,,,G100000500499,operating +56,Doel,Nuclear,Steam Turbine,PP,BE,0.0,,2045.0,,,51.3254,4.2597,,,,,,G100000500500,operating +57,Doel,Nuclear,Steam Turbine,PP,BE,0.0,,2045.0,,,51.3254,4.2597,,,,,,G100000500234,operating +58,Doicesti,Nuclear,Steam Turbine,PP,RO,77.0,,2030.0,,,44.9967,25.3672,,,,,,G100000500876,pre-construction +59,Doicesti,Nuclear,Steam Turbine,PP,RO,77.0,,2030.0,,,44.9967,25.3672,,,,,,G100000500875,pre-construction +60,Doicesti,Nuclear,Steam Turbine,PP,RO,77.0,,2030.0,,,44.9967,25.3672,,,,,,G100000500874,pre-construction +61,Doicesti,Nuclear,Steam Turbine,PP,RO,77.0,,2030.0,,,44.9967,25.3672,,,,,,G100000500872,pre-construction +62,Doicesti,Nuclear,Steam Turbine,PP,RO,77.0,,2030.0,,,44.9967,25.3672,,,,,,G100000500873,pre-construction +63,Doicesti,Nuclear,Steam Turbine,PP,RO,77.0,,2030.0,,,44.9967,25.3672,,,,,,G100000500871,pre-construction +64,Flamanville,Nuclear,Steam Turbine,PP,FR,1650.0,,2024.0,,,49.5366,-1.8823,,,,,,G100000500007,construction +65,Flamanville,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,49.5366,-1.8823,,,,,,G100000500051,operating +66,Flamanville,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,49.5366,-1.8823,,,,,,G100000500050,operating +67,Forsmark,Nuclear,Steam Turbine,PP,SE,0.0,,2045.0,,,60.4028,18.1744,,,,,,G100000500246,operating +68,Forsmark,Nuclear,Steam Turbine,PP,SE,0.0,,2045.0,,,60.4028,18.1744,,,,,,G100000500146,operating +69,Forsmark,Nuclear,Steam Turbine,PP,SE,0.0,,2045.0,,,60.4028,18.1744,,,,,,G100000500128,operating +70,Goesgen,Nuclear,Steam Turbine,PP,CH,0.0,,2045.0,,,47.3656,7.96801,,,,,,G100000500203,operating +71,Golfech,Nuclear,Steam Turbine,PP,FR,1363.0,,1991.0,,,44.106,0.8443,,,,,,G100000500062,operating +72,Golfech,Nuclear,Steam Turbine,PP,FR,1363.0,,1994.0,,,44.106,0.8443,,,,,,G100000500063,operating +73,Gravelines,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,51.0141,2.1332,,,,,,G100000500301,operating +74,Gravelines,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,51.0141,2.1332,,,,,,G100000500299,operating +75,Gravelines,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,51.0141,2.1332,,,,,,G100000501179,announced +76,Gravelines,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,51.0141,2.1332,,,,,,G100000500298,operating +77,Gravelines,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,51.0141,2.1332,,,,,,G100000501178,announced +78,Gravelines,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,51.0141,2.1332,,,,,,G100000500297,operating +79,Gravelines,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,51.0141,2.1332,,,,,,G100000500300,operating +80,Gravelines,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,51.0141,2.1332,,,,,,G100000500302,operating +81,Hartlepool,Nuclear,Steam Turbine,PP,GB,0.0,,2045.0,,,54.6341,-1.1801,,,,,,G100000501025,operating +82,Hartlepool,Nuclear,Steam Turbine,PP,GB,0.0,,2045.0,,,54.6341,-1.1801,,,,,,G100000501026,operating +83,Heysham,Nuclear,Steam Turbine,PP,GB,0.0,,2045.0,,,54.0295,-2.9149,,,,,,G100000501028,operating +84,Heysham,Nuclear,Steam Turbine,PP,GB,0.0,,2045.0,,,54.0295,-2.9149,,,,,,G100000501027,operating +85,Heysham,Nuclear,Steam Turbine,PP,GB,0.0,,2045.0,,,54.03101,-2.9112,,,,,,G100000501030,operating +86,Heysham,Nuclear,Steam Turbine,PP,GB,0.0,,2045.0,,,54.03101,-2.9112,,,,,,G100000501029,operating +87,Hinkley Point,Nuclear,Steam Turbine,PP,GB,1720.0,,2030.0,,,51.2063,-3.1423,,,,,,G100000501036,construction +88,Hinkley Point,Nuclear,Steam Turbine,PP,GB,1720.0,,2029.0,,,51.2063,-3.1423,,,,,,G100000501035,construction +89,Katowice,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,50.2649,19.0238,,,,,,G100000501235,announced +90,Katowice,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,50.2649,19.0238,,,,,,G100000501236,announced +91,Katowice,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,50.2649,19.0238,,,,,,G100000501244,announced +92,Katowice,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,50.2649,19.0238,,,,,,G100000501243,announced +93,Katowice,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,50.2649,19.0238,,,,,,G100000501242,announced +94,Katowice,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,50.2649,19.0238,,,,,,G100000501241,announced +95,Katowice,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,50.2649,19.0238,,,,,,G100000501237,announced +96,Katowice,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,50.2649,19.0238,,,,,,G100000501240,announced +97,Katowice,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,50.2649,19.0238,,,,,,G100000501239,announced +98,Katowice,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,50.2649,19.0238,,,,,,G100000501238,announced +99,Kghm Nuscale Voygr Modular,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,52.3210375,17.2786248,,,,,,G100001009044,announced +100,Kghm Nuscale Voygr Modular,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,52.3210375,17.2786248,,,,,,G100001009045,announced +101,Kghm Nuscale Voygr Modular,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,52.3210375,17.2786248,,,,,,G100001009041,announced +102,Kghm Nuscale Voygr Modular,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,52.3210375,17.2786248,,,,,,G100001009042,announced +103,Kghm Nuscale Voygr Modular,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,52.3210375,17.2786248,,,,,,G100001009043,announced +104,Kghm Nuscale Voygr Modular,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,52.3210375,17.2786248,,,,,,G100001009040,announced +111,Kozloduy,Nuclear,Steam Turbine,PP,BG,1000.0,,2033.0,,,43.7497,23.7685,,,,,,G100000500717,pre-construction +112,Kozloduy,Nuclear,Steam Turbine,PP,BG,0.0,,2045.0,,,43.7497,23.7685,,,,,,G100000500192,operating +113,Kozloduy,Nuclear,Steam Turbine,PP,BG,1000.0,,2036.0,,,43.7497,23.7685,,,,,,G100000500718,pre-construction +114,Kozloduy,Nuclear,Steam Turbine,PP,BG,1040.0,,1993.0,,,43.7497,23.7685,,,,,,G100000500191,operating +115,Krsko,Nuclear,Steam Turbine,PP,SI,0.0,,2045.0,,,45.9389,15.51601,,,,,,G100000500381,announced +116,Krsko,Nuclear,Steam Turbine,PP,SI,0.0,,2045.0,,2043.0,45.9389,15.51601,,,,,,G100000500380,operating +117,Leibstadt,Nuclear,Steam Turbine,PP,CH,0.0,,2045.0,,,47.6012,8.1845,,,,,,G100000500087,operating +118,Loviisa,Nuclear,Steam Turbine,PP,FI,0.0,,2045.0,,2050.0,60.3708,26.3468,,,,,,G100000500449,operating +119,Loviisa,Nuclear,Steam Turbine,PP,FI,0.0,,2045.0,,2050.0,60.3708,26.3468,,,,,,G100000500448,operating +120,Lubiatowo Kopalino,Nuclear,Steam Turbine,PP,PL,1250.0,,2037.0,,,54.8036,17.7881,,,,,,G100000500840,pre-construction +121,Lubiatowo Kopalino,Nuclear,Steam Turbine,PP,PL,1250.0,,2035.0,,,54.8036,17.7881,,,,,,G100000500839,pre-construction +122,Lubiatowo Kopalino,Nuclear,Steam Turbine,PP,PL,1250.0,,2032.0,,,54.8036,17.7881,,,,,,G100000500838,pre-construction +123,Mochovce,Nuclear,Steam Turbine,PP,SK,500.0,,2000.0,,,48.2572,18.4553,,,,,,G100000500462,operating +124,Mochovce,Nuclear,Steam Turbine,PP,SK,471.0,,2023.0,,,48.2572,18.4553,,,,,,G100000500490,operating +125,Mochovce,Nuclear,Steam Turbine,PP,SK,500.0,,1998.0,,,48.2572,18.4553,,,,,,G100000500461,operating +126,Mochovce,Nuclear,Steam Turbine,PP,SK,471.0,,2025.0,,,48.2572,18.4553,,,,,,G100000500491,construction +127,Moorside Clean Energy Hub,Nuclear,Steam Turbine,PP,GB,0.0,,2045.0,,,54.4296,-3.5109,,,,,,G100000501316,announced +128,Moorside Clean Energy Hub,Nuclear,Steam Turbine,PP,GB,0.0,,2045.0,,,54.4296,-3.5109,,,,,,G100000501317,announced +129,Nogent,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,48.5171,3.5181,,,,,,G100000500065,operating +130,Nogent,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,48.5171,3.5181,,,,,,G100000500064,operating +131,Nowa Huta,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,50.0656753,20.0365823,,,,,,G100001009053,announced +132,Nowa Huta,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,50.0656753,20.0365823,,,,,,G100001009051,announced +133,Nowa Huta,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,50.0656753,20.0365823,,,,,,G100001009052,announced +134,Nowa Huta,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,50.0656753,20.0365823,,,,,,G100001009050,announced +135,Olkiluoto,Nuclear,Steam Turbine,PP,FI,0.0,,2045.0,,,61.2371,21.4433,,,,,,G100000500356,operating +136,Olkiluoto,Nuclear,Steam Turbine,PP,FI,0.0,,2045.0,,,61.2371,21.4433,,,,,,G100000500355,operating +137,Olkiluoto,Nuclear,Steam Turbine,PP,FI,1720.0,,2022.0,,,61.2356,21.4372,,,,,,G100000500006,operating +138,Oskarshamn,Nuclear,Steam Turbine,PP,SE,0.0,,2045.0,,,57.4166,16.67301,,,,,,G100000500041,operating +139,Ostroleka,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,53.0674009,21.5610332,,,,,,G100001009064,announced +140,Ostroleka,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,53.0674009,21.5610332,,,,,,G100001009062,announced +141,Ostroleka,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,53.0674009,21.5610332,,,,,,G100001009063,announced +142,Ostroleka,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,53.0674009,21.5610332,,,,,,G100001009065,announced +143,Paks,Nuclear,Steam Turbine,PP,HU,0.0,,2045.0,,,46.57763,18.8532,,,,,,G100000500455,operating +144,Paks,Nuclear,Steam Turbine,PP,HU,0.0,,2045.0,,,46.57763,18.8532,,,,,,G100000500456,operating +145,Paks,Nuclear,Steam Turbine,PP,HU,1265.0,,2032.0,,,46.57763,18.8532,,,,,,G100000500624,pre-construction +146,Paks,Nuclear,Steam Turbine,PP,HU,0.0,,2045.0,,,46.57763,18.8532,,,,,,G100000500457,operating +147,Paks,Nuclear,Steam Turbine,PP,HU,0.0,,2045.0,,,46.57763,18.8532,,,,,,G100000500454,operating +148,Paks,Nuclear,Steam Turbine,PP,HU,1265.0,,2032.0,,,46.57763,18.8532,,,,,,G100000500623,pre-construction +149,Paluel,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,49.8582,0.6354,,,,,,G100000500053,operating +150,Paluel,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,49.8582,0.6354,,,,,,G100000500055,operating +151,Paluel,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,49.8582,0.6354,,,,,,G100000500054,operating +152,Paluel,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,49.8582,0.6354,,,,,,G100000500052,operating +153,Patnow,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,51.1465,18.6072,,,,,,G100000501245,announced +154,Patnow,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,51.1465,18.6072,,,,,,G100001009038,announced +155,Penly,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,49.9764,1.2107,,,,,,G100000500056,operating +156,Penly,Nuclear,Steam Turbine,PP,FR,1382.0,,1992.0,,,49.9764,1.2107,,,,,,G100000500057,operating +157,Penly,Nuclear,Steam Turbine,PP,FR,1650.0,,2035.0,,,49.9764,1.2107,,,,,,G100000501185,pre-construction +158,Penly,Nuclear,Steam Turbine,PP,FR,1650.0,,2045.0,,,49.9764,1.2107,,,,,,G100000501186,pre-construction +159,Ringhals,Nuclear,Steam Turbine,PP,SE,0.0,,2045.0,,,57.2574,12.1087,,,,,,G100000500186,operating +160,Ringhals,Nuclear,Steam Turbine,PP,SE,0.0,,2045.0,,,57.2574,12.1087,,,,,,G100000500129,operating +165,Saint Laurent,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,47.7197,1.5783,,,,,,G100000500284,operating +166,Saint Laurent,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,47.7197,1.5783,,,,,,G100000500283,operating +167,Sizewell,Nuclear,Steam Turbine,PP,GB,1600.0,,2045.0,,,52.2199,1.6203,,,,,,G100000500708,pre-construction +168,Sizewell,Nuclear,Steam Turbine,PP,GB,1600.0,,2045.0,,,52.2199,1.6203,,,,,,G100001017733,pre-construction +169,Sizewell,Nuclear,Steam Turbine,PP,GB,1250.0,,1995.0,,,52.2145,1.6206,,,,,,G100000501045,operating +170,Sizewell,Nuclear,Steam Turbine,PP,GB,1600.0,,2045.0,,,52.2199,1.6203,,,,,,G100000500709,pre-construction +171,Sizewell,Nuclear,Steam Turbine,PP,GB,1600.0,,2045.0,,,52.2199,1.6203,,,,,,G100001017734,pre-construction +175,St Alban,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,45.4043,4.7554,,,,,,G100000500059,operating +176,St Alban,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,45.4043,4.7554,,,,,,G100000500058,operating +177,Stawy Monowskie,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,50.024621,19.3339713,,,,,,G100001009058,announced +178,Stawy Monowskie,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,50.024621,19.3339713,,,,,,G100001009060,announced +179,Stawy Monowskie,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,50.024621,19.3339713,,,,,,G100001009061,announced +180,Stawy Monowskie,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,50.024621,19.3339713,,,,,,G100001009059,announced +181,Tarnobrzeg Special Economic Zone,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,50.5243466,21.6254433,,,,,,G100001009057,announced +182,Tarnobrzeg Special Economic Zone,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,50.5243466,21.6254433,,,,,,G100001009056,announced +183,Tarnobrzeg Special Economic Zone,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,50.5243466,21.6254433,,,,,,G100001009055,announced +184,Tarnobrzeg Special Economic Zone,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,50.5243466,21.6254433,,,,,,G100001009054,announced +185,Tihange,Nuclear,Steam Turbine,PP,BE,0.0,,2045.0,,,50.5334,5.2714,,,,,,G100000500205,operating +186,Tihange,Nuclear,Steam Turbine,PP,BE,0.0,,2045.0,,,50.5334,5.2714,,,,,,G100000500257,operating +187,Torness,Nuclear,Steam Turbine,PP,GB,0.0,,2045.0,,,55.9679,-2.4086,,,,,,G100000500435,operating +188,Torness,Nuclear,Steam Turbine,PP,GB,0.0,,2045.0,,,55.9679,-2.4086,,,,,,G100000500434,operating +189,Tricastin,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,44.3311,4.7311,,,,,,G100000500288,operating +190,Tricastin,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,44.3311,4.7311,,,,,,G100000500285,operating +191,Tricastin,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,44.3311,4.7311,,,,,,G100000500286,operating +192,Tricastin,Nuclear,Steam Turbine,PP,FR,0.0,,2045.0,,,44.3311,4.7311,,,,,,G100000500287,operating +193,Trillo,Nuclear,Steam Turbine,PP,ES,0.0,,2045.0,,,40.7016,-2.6227,,,,,,G100000500202,operating +194,Vandellos,Nuclear,Steam Turbine,PP,ES,0.0,,2045.0,,,40.9511,0.8662,,,,,,G100000500241,operating +195,Wloclawek,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,52.6718091,19.0537634,,,,,,G100001009068,announced +196,Wloclawek,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,52.6718091,19.0537634,,,,,,G100001009066,announced +197,Wloclawek,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,52.6718091,19.0537634,,,,,,G100001009067,announced +198,Wloclawek,Nuclear,Steam Turbine,PP,PL,0.0,,2045.0,,,52.6718091,19.0537634,,,,,,G100001009069,announced From e970226b73987548c89af743e1a1a6b7e280dc3f Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 26 Aug 2024 17:34:05 +0200 Subject: [PATCH 245/293] plot.smk: adjust to scenario management --- rules/collect.smk | 20 ++-- rules/plot.smk | 229 ++++++++++++---------------------------------- 2 files changed, 71 insertions(+), 178 deletions(-) diff --git a/rules/collect.smk b/rules/collect.smk index 9e8ce6c7d..7b27855c1 100644 --- a/rules/collect.smk +++ b/rules/collect.smk @@ -98,21 +98,21 @@ rule validate_elec_networks: rule plot_resources: input: - RESOURCES + "graphics/power-network-unclustered.pdf", - RESOURCES + "graphics/gas-network-unclustered.pdf", - RESOURCES + "graphics/wind-energy-density.pdf", - RESOURCES + "graphics/weather-map-irradiation.pdf", - RESOURCES + "graphics/industrial-sites.pdf", - RESOURCES + "graphics/powerplants.pdf", - RESOURCES + "graphics/salt-caverns.pdf", + resources("graphics/power-network-unclustered.pdf"), + resources("graphics/gas-network-unclustered.pdf"), + resources("graphics/wind-energy-density.pdf"), + resources("graphics/weather-map-irradiation.pdf"), + resources("graphics/industrial-sites.pdf"), + resources("graphics/powerplants.pdf"), + resources("graphics/salt-caverns.pdf"), expand( - RESOURCES + "graphics/power-network-{clusters}.pdf", **config["scenario"] + resources("graphics/power-network-{clusters}.pdf"), **config["scenario"] ), expand( - RESOURCES + "graphics/salt-caverns-{clusters}-nearshore.pdf", + resources("graphics/salt-caverns-{clusters}-nearshore.pdf"), **config["scenario"], ), expand( - RESOURCES + "graphics/biomass-potentials-{clusters}-biogas.pdf", + resources("graphics/biomass-potentials-{clusters}-biogas.pdf"), **config["scenario"], ), diff --git a/rules/plot.smk b/rules/plot.smk index 07dd76a2f..e26876d00 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -5,55 +5,41 @@ rule plot_power_network_unclustered: input: - network=RESOURCES + "networks/elec.nc", + network=resources("networks/elec.nc"), rc="matplotlibrc", output: - multiext(RESOURCES + "graphics/power-network-unclustered", ".png", ".pdf"), + multiext(resources("graphics/power-network-unclustered"), ".png", ".pdf"), script: "../scripts/plot_power_network_unclustered.py" rule plot_gas_network_unclustered: input: - gas_network=RESOURCES + "gas_network.csv", - gem=HTTP.remote( - "https://globalenergymonitor.org/wp-content/uploads/2023/07/Europe-Gas-Tracker-2023-03-v3.xlsx", - keep_local=True, - ), + gas_network=resources("gas_network.csv"), + 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", rc="matplotlibrc", output: - multiext(RESOURCES + "graphics/gas-network-unclustered", ".png", ".pdf"), + multiext(resources("graphics/gas-network-unclustered"), ".png", ".pdf"), script: "../scripts/plot_gas_network_unclustered.py" -rule plot_power_network_clustered: - input: - network=RESOURCES + "networks/elec_s_{clusters}.nc", - regions_onshore=RESOURCES + "regions_onshore_elec_s_{clusters}.geojson", - rc="matplotlibrc", - output: - multiext(RESOURCES + "graphics/power-network-{clusters}", ".png", ".pdf"), - script: - "../scripts/plot_power_network_clustered.py" - - rule plot_renewable_potential_unclustered: input: **{ - f"profile_{tech}": RESOURCES + f"profile_{tech}.nc" + f"profile_{tech}": resources(f"profile_{tech}.nc") for tech in config["electricity"]["renewable_carriers"] }, - regions_onshore=RESOURCES + "regions_onshore.geojson", - regions_offshore=RESOURCES + "regions_offshore.geojson", + regions_onshore=resources("regions_onshore.geojson"), + regions_offshore=resources("regions_offshore.geojson"), rc="matplotlibrc", output: - wind=multiext(RESOURCES + "graphics/wind-energy-density", ".png", ".pdf"), - solar=multiext(RESOURCES + "graphics/solar-energy-density", ".png", ".pdf"), - wind_cf=multiext(RESOURCES + "graphics/wind-capacity-factor", ".png", ".pdf"), - solar_cf=multiext(RESOURCES + "graphics/solar-capacity-factor", ".png", ".pdf"), + wind=multiext(resources("graphics/wind-energy-density"), ".png", ".pdf"), + solar=multiext(resources("graphics/solar-energy-density"), ".png", ".pdf"), + wind_cf=multiext(resources("graphics/wind-capacity-factor"), ".png", ".pdf"), + solar_cf=multiext(resources("graphics/solar-capacity-factor"), ".png", ".pdf"), script: "../scripts/plot_renewable_potential_unclustered.py" @@ -64,13 +50,13 @@ rule plot_weather_data_map: rc="matplotlibrc", output: irradiation=multiext( - RESOURCES + "graphics/weather-map-irradiation", ".png", ".pdf" + resources("graphics/weather-map-irradiation"), ".png", ".pdf" ), - runoff=multiext(RESOURCES + "graphics/weather-map-runoff", ".png", ".pdf"), + runoff=multiext(resources("graphics/weather-map-runoff"), ".png", ".pdf"), temperature=multiext( - RESOURCES + "graphics/weather-map-temperature", ".png", ".pdf" + resources("graphics/weather-map-temperature"), ".png", ".pdf" ), - wind=multiext(RESOURCES + "graphics/weather-map-wind", ".png", ".pdf"), + wind=multiext(resources("graphics/weather-map-wind"), ".png", ".pdf"), script: "../scripts/plot_weather_data_map.py" @@ -78,20 +64,20 @@ rule plot_weather_data_map: rule plot_industrial_sites: input: hotmaps="data/Industrial_Database.csv", - countries=RESOURCES + "country_shapes.geojson", + countries=resources("country_shapes.geojson"), rc="matplotlibrc", output: - multiext(RESOURCES + "graphics/industrial-sites", ".png", ".pdf"), + multiext(resources("graphics/industrial-sites"), ".png", ".pdf"), script: "../scripts/plot_industrial_sites.py" rule plot_powerplants: input: - powerplants=RESOURCES + "powerplants.csv", + powerplants=resources("powerplants.csv"), rc="matplotlibrc", output: - multiext(RESOURCES + "graphics/powerplants", ".png", ".pdf"), + multiext(resources("graphics/powerplants"), ".png", ".pdf"), script: "../scripts/plot_powerplants.py" @@ -101,30 +87,30 @@ rule plot_salt_caverns_unclustered: caverns="data/h2_salt_caverns_GWh_per_sqkm.geojson", rc="matplotlibrc", output: - multiext(RESOURCES + "graphics/salt-caverns", ".png", ".pdf"), + multiext(resources("graphics/salt-caverns"), ".png", ".pdf"), script: "../scripts/plot_salt_caverns_unclustered.py" rule plot_salt_caverns_clustered: input: - caverns=RESOURCES + "salt_cavern_potentials_s{simpl}_{clusters}.csv", - regions_onshore=RESOURCES + "regions_onshore_elec_s{simpl}_{clusters}.geojson", - regions_offshore=RESOURCES + "regions_offshore_elec_s{simpl}_{clusters}.geojson", + caverns=resources("salt_cavern_potentials_s{simpl}_{clusters}.csv"), + regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), + regions_offshore=resources("regions_offshore_elec_s{simpl}_{clusters}.geojson"), rc="matplotlibrc", output: onshore=multiext( - RESOURCES + "graphics/salt-caverns-s{simpl}-{clusters}-onshore", + resources("graphics/salt-caverns-s{simpl}-{clusters}-onshore"), ".png", ".pdf", ), nearshore=multiext( - RESOURCES + "graphics/salt-caverns-s{simpl}-{clusters}-nearshore", + resources("graphics/salt-caverns-s{simpl}-{clusters}-nearshore"), ".png", ".pdf", ), offshore=multiext( - RESOURCES + "graphics/salt-caverns-s{simpl}-{clusters}-offshore", + resources("graphics/salt-caverns-s{simpl}-{clusters}-offshore"), ".png", ".pdf", ), @@ -134,23 +120,22 @@ rule plot_salt_caverns_clustered: rule plot_biomass_potentials: input: - biomass=RESOURCES + "biomass_potentials_s{simpl}_{clusters}.csv", - regions_onshore=RESOURCES + "regions_onshore_elec_s{simpl}_{clusters}.geojson", + biomass=resources("biomass_potentials_s{simpl}_{clusters}.csv"), + regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), rc="matplotlibrc", output: solid_biomass=multiext( - RESOURCES - + "graphics/biomass-potentials-s{simpl}-{clusters}-solid_biomass", + resources("graphics/biomass-potentials-s{simpl}-{clusters}-solid_biomass"), ".png", ".pdf", ), not_included=multiext( - RESOURCES + "graphics/biomass-potentials-s{simpl}-{clusters}-not_included", + resources("graphics/biomass-potentials-s{simpl}-{clusters}-not_included"), ".png", ".pdf", ), biogas=multiext( - RESOURCES + "graphics/biomass-potentials-s{simpl}-{clusters}-biogas", + resources("graphics/biomass-potentials-s{simpl}-{clusters}-biogas"), ".png", ".pdf", ), @@ -160,24 +145,24 @@ rule plot_biomass_potentials: rule plot_choropleth_capacity_factors: input: - 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", + 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"), rc="matplotlibrc", output: - directory(RESOURCES + "graphics/capacity-factor/s{simpl}-{clusters}"), + directory(resources("graphics/capacity-factor/s{simpl}-{clusters}")), script: "../scripts/plot_choropleth_capacity_factors.py" rule plot_choropleth_capacity_factors_sector: input: - cop_soil=RESOURCES + "cop_soil_total_elec_s{simpl}_{clusters}.nc", - cop_air=RESOURCES + "cop_air_total_elec_s{simpl}_{clusters}.nc", - regions_onshore=RESOURCES + "regions_onshore_elec_s{simpl}_{clusters}.geojson", + cop_soil=resources("cop_soil_total_elec_s{simpl}_{clusters}.nc"), + cop_air=resources("cop_air_total_elec_s{simpl}_{clusters}.nc"), + regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), rc="matplotlibrc", output: - directory(RESOURCES + "graphics/capacity-factor-sector/s{simpl}-{clusters}"), + directory(resources("graphics/capacity-factor-sector/s{simpl}-{clusters}")), script: "../scripts/plot_choropleth_capacity_factors_sector.py" @@ -186,8 +171,8 @@ rule plot_choropleth_capacities: input: network=RESULTS + "postnetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", - 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_elec_s{simpl}_{clusters}.geojson"), + regions_offshore=resources("regions_offshore_elec_s{simpl}_{clusters}.geojson"), rc="matplotlibrc", output: directory( @@ -202,8 +187,8 @@ rule plot_choropleth_prices: input: network=RESULTS + "postnetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", - 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_elec_s{simpl}_{clusters}.geojson"), + regions_offshore=resources("regions_offshore_elec_s{simpl}_{clusters}.geojson"), rc="matplotlibrc", output: directory( @@ -218,8 +203,8 @@ rule plot_choropleth_potential_used: input: network=RESULTS + "postnetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", - 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_elec_s{simpl}_{clusters}.geojson"), + regions_offshore=resources("regions_offshore_elec_s{simpl}_{clusters}.geojson"), rc="matplotlibrc", output: directory( @@ -234,12 +219,10 @@ rule plot_choropleth_demand: input: network=RESULTS + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", - industrial_demand=RESOURCES - + "industrial_energy_demand_elec_s{simpl}_{clusters}_{planning_horizons}.csv", - shipping_demand=RESOURCES + "shipping_demand_s{simpl}_{clusters}.csv", - nodal_energy_totals=RESOURCES - + "pop_weighted_energy_totals_s{simpl}_{clusters}.csv", - regions_onshore=RESOURCES + "regions_onshore_elec_s{simpl}_{clusters}.geojson", + industrial_demand=resources("industrial_energy_demand_elec_s{simpl}_{clusters}_{planning_horizons}.csv"), + shipping_demand=resources("shipping_demand_s{simpl}_{clusters}.csv"), + nodal_energy_totals=resources("pop_weighted_energy_totals_s{simpl}_{clusters}.csv"), + regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), rc="matplotlibrc", output: directory( @@ -295,100 +278,12 @@ rule plot_heatmap_timeseries_resources: "../scripts/plot_heatmap_timeseries_resources.py" -rule plot_power_network: - params: - plotting=config["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", - rc="matplotlibrc", - output: - multiext( - RESULTS - + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-costs-all_{planning_horizons}", - ".png", - ".pdf", - ), - threads: 2 - resources: - mem_mb=10000, - benchmark: - ( - BENCHMARKS - + "plot_network/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" - ) - conda: - "../envs/environment.yaml" - script: - "../scripts/plot_power_network.py" - - -rule plot_hydrogen_network: - params: - foresight=config["foresight"], - plotting=config["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", - rc="matplotlibrc", - output: - multiext( - RESULTS - + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-h2_network_{planning_horizons}", - ".png", - ".pdf", - ), - threads: 2 - resources: - mem_mb=10000, - benchmark: - ( - BENCHMARKS - + "plot_hydrogen_network/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" - ) - conda: - "../envs/environment.yaml" - script: - "../scripts/plot_hydrogen_network.py" - - -rule plot_gas_network: - params: - plotting=config["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", - rc="matplotlibrc", - output: - multiext( - RESULTS - + "maps/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}-ch4_network_{planning_horizons}", - ".png", - ".pdf", - ), - threads: 2 - resources: - mem_mb=10000, - benchmark: - ( - BENCHMARKS - + "plot_gas_network/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" - ) - conda: - "../envs/environment.yaml" - script: - "../scripts/plot_gas_network.py" - - rule plot_import_options: input: network=RESULTS + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", - regions=RESOURCES + "regions_onshore_elec_s{simpl}_{clusters}.geojson", - entrypoints=RESOURCES + "gas_input_locations_s{simpl}_{clusters}_simplified.csv", + regions=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), + entrypoints=resources("gas_input_locations_s{simpl}_{clusters}_simplified.csv"), imports="data/imports/results.csv", rc="matplotlibrc", output: @@ -412,20 +307,18 @@ rule plot_import_world_map: input: imports="data/imports/results.csv", profiles="data/imports/combined_weighted_generator_timeseries.nc", - gadm_arg=HTTP.remote( + gadm_arg=storage( "https://geodata.ucdavis.edu/gadm/gadm4.1/gpkg/gadm41_ARG.gpkg", keep_local=True, - static=True, ), - copernicus_glc=HTTP.remote( + copernicus_glc=storage( "https://zenodo.org/records/3939050/files/PROBAV_LC100_global_v3.0.1_2019-nrt_Discrete-Classification-map_EPSG-4326.tif", keep_local=True, - static=True, ), wdpa="data/WDPA.gpkg", rc="matplotlibrc", output: - multiext(RESOURCES + "graphics/import_world_map", ".png", ".pdf"), + multiext(resources("graphics/import_world_map"), ".png", ".pdf"), script: "../scripts/plot_import_world_map.py" @@ -434,7 +327,7 @@ rule plot_import_networks: 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", + regions=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), rc="matplotlibrc", output: multiext( @@ -474,22 +367,22 @@ rule plot_all_resources: rules.plot_salt_caverns_unclustered.output, rules.plot_import_world_map.output, expand( - RESOURCES + "graphics/salt-caverns-s{simpl}-{clusters}-onshore.pdf", + resources("graphics/salt-caverns-s{simpl}-{clusters}-onshore.pdf"), **config["scenario"], ), expand( - RESOURCES + "graphics/power-network-{clusters}.pdf", **config["scenario"] + resources("graphics/power-network-{clusters}.pdf"), **config["scenario"] ), expand( - RESOURCES + "graphics/biomass-potentials-s{simpl}-{clusters}-biogas.pdf", + resources("graphics/biomass-potentials-s{simpl}-{clusters}-biogas.pdf"), **config["scenario"], ), expand( - RESOURCES + "graphics/capacity-factor/s{simpl}-{clusters}", + resources("graphics/capacity-factor/s{simpl}-{clusters}"), **config["scenario"], ), expand( - RESOURCES + "graphics/capacity-factor-sector/s{simpl}-{clusters}", + resources("graphics/capacity-factor-sector/s{simpl}-{clusters}"), **config["scenario"], ), From a78e536b452518be98bff1b1aad455521af59b37 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 26 Aug 2024 17:34:31 +0200 Subject: [PATCH 246/293] config.default.yaml: remove duplicate entry --- config/config.default.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 2e2724ce8..ae42957c2 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -898,7 +898,6 @@ industry: MWh_elec_per_tCl: 3.6 MWh_H2_per_tCl: -0.9372 methanol_production_today: 1.5 - MWh_MeOH_per_tMeOH: 5.54 MWh_elec_per_tMeOH: 0.167 MWh_CH4_per_tMeOH: 10.25 MWh_MeOH_per_tMeOH: 5.528 From bed73c71395a190720fbaf050f1c187ec947c82a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2024 15:37:59 +0000 Subject: [PATCH 247/293] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- rules/collect.smk | 4 +--- rules/plot.smk | 12 +++++++----- rules/retrieve.smk | 1 + 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/rules/collect.smk b/rules/collect.smk index 7b27855c1..29f437a7d 100644 --- a/rules/collect.smk +++ b/rules/collect.smk @@ -105,9 +105,7 @@ rule plot_resources: resources("graphics/industrial-sites.pdf"), resources("graphics/powerplants.pdf"), resources("graphics/salt-caverns.pdf"), - expand( - resources("graphics/power-network-{clusters}.pdf"), **config["scenario"] - ), + expand(resources("graphics/power-network-{clusters}.pdf"), **config["scenario"]), expand( resources("graphics/salt-caverns-{clusters}-nearshore.pdf"), **config["scenario"], diff --git a/rules/plot.smk b/rules/plot.smk index e26876d00..764f44235 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -219,9 +219,13 @@ rule plot_choropleth_demand: input: network=RESULTS + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", - industrial_demand=resources("industrial_energy_demand_elec_s{simpl}_{clusters}_{planning_horizons}.csv"), + industrial_demand=resources( + "industrial_energy_demand_elec_s{simpl}_{clusters}_{planning_horizons}.csv" + ), shipping_demand=resources("shipping_demand_s{simpl}_{clusters}.csv"), - nodal_energy_totals=resources("pop_weighted_energy_totals_s{simpl}_{clusters}.csv"), + nodal_energy_totals=resources( + "pop_weighted_energy_totals_s{simpl}_{clusters}.csv" + ), regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), rc="matplotlibrc", output: @@ -370,9 +374,7 @@ rule plot_all_resources: resources("graphics/salt-caverns-s{simpl}-{clusters}-onshore.pdf"), **config["scenario"], ), - expand( - resources("graphics/power-network-{clusters}.pdf"), **config["scenario"] - ), + expand(resources("graphics/power-network-{clusters}.pdf"), **config["scenario"]), expand( resources("graphics/biomass-potentials-s{simpl}-{clusters}-biogas.pdf"), **config["scenario"], diff --git a/rules/retrieve.smk b/rules/retrieve.smk index 288a445f2..df1f7379f 100644 --- a/rules/retrieve.smk +++ b/rules/retrieve.smk @@ -373,6 +373,7 @@ if config["enable"]["retrieve"]: shell("ogr2ogr -f gpkg -update -append {output.gpkg} {layer_path}") + if config["enable"]["retrieve"]: rule retrieve_monthly_co2_prices: From 3d5f3aaa7f545bdb0780e55ef809df098bf1830e Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 16 Sep 2024 10:21:35 +0200 Subject: [PATCH 248/293] resolve overlooked merge conflicts --- config/config.default.yaml | 3 --- rules/build_sector.smk | 3 --- 2 files changed, 6 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index a7164af5d..69170923b 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -1255,11 +1255,8 @@ plotting: rural biomass boiler: '#a1a066' urban decentral biomass boiler: '#b0b87b' biomass to liquid: '#32CD32' -<<<<<<< HEAD biomass to liquid CC: '#32CDaa' -======= unsustainable solid biomass: '#998622' ->>>>>>> master unsustainable bioliquids: '#32CD32' electrobiofuels: 'red' BioSNG: '#123456' diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 2797366c7..c34f9c38c 100755 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -1100,9 +1100,6 @@ 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" ), From c705f2dbb2ab13f31607892a5a26234323565538 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 16 Sep 2024 14:09:22 +0200 Subject: [PATCH 249/293] resolve overlooked merge conflicts --- config/config.default.yaml | 39 +++----- doc/configtables/sector-opts.csv | 1 - envs/environment.yaml | 1 - rules/build_sector.smk | 17 ++-- rules/plot.smk | 149 +++++++++++++++--------------- scripts/_helpers.py | 7 +- scripts/make_summary.py | 5 +- scripts/prepare_sector_network.py | 142 +--------------------------- 8 files changed, 102 insertions(+), 259 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 9f5ff0cdd..3d8e3591c 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -614,19 +614,6 @@ sector: hydrogen_turbine: false SMR: true SMR_cc: true - 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 regional_oil_demand: false regional_coal_demand: false regional_co2_sequestration_potential: @@ -659,6 +646,19 @@ sector: # - onshore # more than 50 km from sea - nearshore # within 50 km of sea # - offshore + 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 @@ -698,8 +698,6 @@ sector: OCGT: gas biomass_to_liquid: false biomass_to_liquid_cc: false - biomass_to_methanol: false - biomass_to_methanol_cc: false biosng: false bioH2: false biosng_cc: false @@ -708,6 +706,8 @@ sector: endogenous_hvc: false relocation_steel: false flexibility_steel: false + relocation_ammonia: false + flexibility_ammonia: false import: capacity_boost: 2 limit: false # bool or number in TWh @@ -1199,10 +1199,6 @@ plotting: CCGT: '#a85522' CCGT marginal: '#a85522' allam gas: '#B98F76' - allam methanol: '#B98F76' - CCGT methanol: '#B98F76' - CCGT methanol CC: '#B98F76' - OCGT methanol: '#B98F76' gas for industry co2 to atmosphere: '#692e0a' gas for industry co2 to stored: '#8a3400' gas for industry: '#853403' @@ -1418,7 +1414,6 @@ plotting: methanol: '#FF7B00' methanol transport: '#FF7B00' shipping methanol: '#468c8b' - methanol-to-kerosene: '#579d9c' industry methanol: '#468c8b' # co2 CC: '#f29dae' @@ -1483,15 +1478,11 @@ plotting: steel: '#b1bbc9' DRI + Electric arc: '#b1bbc9' shipping ammonia: '#46caf0' - Methanol steam reforming CC: '#468c8b' biomass to methanol CC: '#468c8b' biomass to methanol: '#468c8b' - Methanol steam reforming: '#468c8b' shipping LNG: '#e0986c' - industry methanol: '#468c8b' industry methanol emissions: '#468c8b' HVC: '#012345' - methanol-to-olefins/aromatics: '#012345' electric steam cracker: '#012345' load: "#dd2e23" waste CHP: '#e3d37d' diff --git a/doc/configtables/sector-opts.csv b/doc/configtables/sector-opts.csv index 66cd00bf5..afe631fab 100644 --- a/doc/configtables/sector-opts.csv +++ b/doc/configtables/sector-opts.csv @@ -6,7 +6,6 @@ Trigger, Description, Definition, Status ``H``,Add heating sector,,In active use ``B``,Add biomass,,In active use ``I``,Add industry sector,,In active use -``S``,Add shipping sector,,In active use ``A``,Add agriculture sector,,In active use ``dist``+``n``,Add distribution grid with investment costs of ``n`` times costs in ``resources/costs_{cost_year}.csv``,,In active use ``seq``+``n``,Sets the CO2 sequestration potential to ``n`` Mt CO2 per year,,In active use diff --git a/envs/environment.yaml b/envs/environment.yaml index 03add0ceb..bb6822e95 100644 --- a/envs/environment.yaml +++ b/envs/environment.yaml @@ -10,7 +10,6 @@ dependencies: - python>=3.8 - pip -- pypsa>=0.25.1 - atlite>=0.2.9 - pypsa>=0.30.2 - linopy diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 51e41aab4..1a57d318a 100755 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -105,7 +105,7 @@ rule build_gas_input_locations: gas_input_nodes_simplified=resources( "gas_input_locations_s_{clusters}_simplified.csv" ), - ports=resources("ports_s{simpl}_{clusters}.csv"), + ports=resources("ports_s_{clusters}.csv"), resources: mem_mb=2000, log: @@ -1083,7 +1083,7 @@ rule prepare_sector_network: "industrial_energy_demand_base_s_{clusters}_{planning_horizons}.csv" ), industrial_demand_today=resources( - "industrial_energy_demand_today_elec_s{simpl}_{clusters}.csv" + "industrial_energy_demand_today_base_s_{clusters}.csv" ), industry_sector_ratios=resources( "industry_sector_ratios_{planning_horizons}.csv" @@ -1121,15 +1121,14 @@ rule prepare_sector_network: if config_provider("sector", "enhanced_geothermal", "enable")(w) else [] ), - # import_costs=storage("https://tubcloud.tu-berlin.de/s/Woq9Js5MjtoaqGP/download/results.csv", keep_local=True), import_costs="data/imports/results.csv", - # import_p_max_pu=storage("https://tubcloud.tu-berlin.de/s/qPaoD54qHtEAo8i/download/combined_weighted_generator_timeseries.nc", keep_local=True), import_p_max_pu="data/imports/combined_weighted_generator_timeseries.nc", - regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), - country_centroids=storage( - "https://raw.githubusercontent.com/gavinr/world-countries-centroids/v1.0.0/dist/countries.csv", - keep_local=True, - ), + regions_onshore=resources("regions_onshore_base_s_{clusters}.geojson"), + country_shapes="data/naturalearth/ne_10m_admin_0_countries_deu.shp", # TODO FN: instead of country_centroids use .representative_point() + # country_centroids=storage( + # "https://raw.githubusercontent.com/gavinr/world-countries-centroids/v1.0.0/dist/countries.csv", + # keep_local=True, + # ), output: RESULTS + "prenetworks/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", diff --git a/rules/plot.smk b/rules/plot.smk index 764f44235..c11d1c059 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -94,23 +94,23 @@ rule plot_salt_caverns_unclustered: rule plot_salt_caverns_clustered: input: - caverns=resources("salt_cavern_potentials_s{simpl}_{clusters}.csv"), - regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), - regions_offshore=resources("regions_offshore_elec_s{simpl}_{clusters}.geojson"), + caverns=resources("salt_cavern_potentials_s_{clusters}.csv"), + regions_onshore=resources("regions_onshore_base_s_{clusters}.geojson"), + regions_offshore=resources("regions_offshore_base_s_{clusters}.geojson"), rc="matplotlibrc", output: onshore=multiext( - resources("graphics/salt-caverns-s{simpl}-{clusters}-onshore"), + resources("graphics/salt-caverns-s-{clusters}-onshore"), ".png", ".pdf", ), nearshore=multiext( - resources("graphics/salt-caverns-s{simpl}-{clusters}-nearshore"), + resources("graphics/salt-caverns-s-{clusters}-nearshore"), ".png", ".pdf", ), offshore=multiext( - resources("graphics/salt-caverns-s{simpl}-{clusters}-offshore"), + resources("graphics/salt-caverns-s-{clusters}-offshore"), ".png", ".pdf", ), @@ -120,22 +120,22 @@ rule plot_salt_caverns_clustered: rule plot_biomass_potentials: input: - biomass=resources("biomass_potentials_s{simpl}_{clusters}.csv"), - regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), + biomass=resources("biomass_potentials_s_{clusters}.csv"), + regions_onshore=resources("regions_onshore_base_s_{clusters}.geojson"), rc="matplotlibrc", output: solid_biomass=multiext( - resources("graphics/biomass-potentials-s{simpl}-{clusters}-solid_biomass"), + resources("graphics/biomass-potentials-s-{clusters}-solid_biomass"), ".png", ".pdf", ), not_included=multiext( - resources("graphics/biomass-potentials-s{simpl}-{clusters}-not_included"), + resources("graphics/biomass-potentials-s-{clusters}-not_included"), ".png", ".pdf", ), biogas=multiext( - resources("graphics/biomass-potentials-s{simpl}-{clusters}-biogas"), + resources("graphics/biomass-potentials-s-{clusters}-biogas"), ".png", ".pdf", ), @@ -145,24 +145,24 @@ rule plot_biomass_potentials: rule plot_choropleth_capacity_factors: input: - 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"), + 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"), rc="matplotlibrc", output: - directory(resources("graphics/capacity-factor/s{simpl}-{clusters}")), + directory(resources("graphics/capacity-factor/s-{clusters}")), script: "../scripts/plot_choropleth_capacity_factors.py" rule plot_choropleth_capacity_factors_sector: input: - cop_soil=resources("cop_soil_total_elec_s{simpl}_{clusters}.nc"), - cop_air=resources("cop_air_total_elec_s{simpl}_{clusters}.nc"), - regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), + cop_soil=resources("cop_soil_total_base_s_{clusters}.nc"), + cop_air=resources("cop_air_total_base_s_{clusters}.nc"), + regions_onshore=resources("regions_onshore_base_s_{clusters}.geojson"), rc="matplotlibrc", output: - directory(resources("graphics/capacity-factor-sector/s{simpl}-{clusters}")), + directory(resources("graphics/capacity-factor-sector/s-{clusters}")), script: "../scripts/plot_choropleth_capacity_factors_sector.py" @@ -170,14 +170,14 @@ rule plot_choropleth_capacity_factors_sector: rule plot_choropleth_capacities: input: network=RESULTS - + "postnetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", - regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), - regions_offshore=resources("regions_offshore_elec_s{simpl}_{clusters}.geojson"), + + "postnetworks/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + regions_onshore=resources("regions_onshore_base_s_{clusters}.geojson"), + regions_offshore=resources("regions_offshore_base_s_{clusters}.geojson"), rc="matplotlibrc", output: directory( RESULTS - + "graphics/p_nom_opt/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" + + "graphics/p_nom_opt/s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" ), script: "../scripts/plot_choropleth_capacities.py" @@ -186,14 +186,14 @@ rule plot_choropleth_capacities: rule plot_choropleth_prices: input: network=RESULTS - + "postnetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", - regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), - regions_offshore=resources("regions_offshore_elec_s{simpl}_{clusters}.geojson"), + + "postnetworks/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + regions_onshore=resources("regions_onshore_base_s_{clusters}.geojson"), + regions_offshore=resources("regions_offshore_base_s_{clusters}.geojson"), rc="matplotlibrc", output: directory( RESULTS - + "graphics/prices/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" + + "graphics/prices/s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" ), script: "../scripts/plot_choropleth_prices.py" @@ -202,14 +202,14 @@ rule plot_choropleth_prices: rule plot_choropleth_potential_used: input: network=RESULTS - + "postnetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", - regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), - regions_offshore=resources("regions_offshore_elec_s{simpl}_{clusters}.geojson"), + + "postnetworks/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + regions_onshore=resources("regions_onshore_base_s_{clusters}.geojson"), + regions_offshore=resources("regions_offshore_base_s_{clusters}.geojson"), rc="matplotlibrc", output: directory( RESULTS - + "graphics/potential_used/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" + + "graphics/potential_used/s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" ), script: "../scripts/plot_choropleth_potential_used.py" @@ -218,20 +218,20 @@ rule plot_choropleth_potential_used: rule plot_choropleth_demand: 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", industrial_demand=resources( - "industrial_energy_demand_elec_s{simpl}_{clusters}_{planning_horizons}.csv" + "industrial_energy_demand_base_s_{clusters}_{planning_horizons}.csv" ), - shipping_demand=resources("shipping_demand_s{simpl}_{clusters}.csv"), + shipping_demand=resources("shipping_demand_s_{clusters}.csv"), nodal_energy_totals=resources( - "pop_weighted_energy_totals_s{simpl}_{clusters}.csv" + "pop_weighted_energy_totals_s_{clusters}.csv" ), - regions_onshore=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), + regions_onshore=resources("regions_onshore_base_s_{clusters}.geojson"), rc="matplotlibrc", output: directory( RESULTS - + "graphics/regional_demand/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" + + "graphics/regional_demand/s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" ), script: "../scripts/plot_choropleth_demand.py" @@ -240,13 +240,13 @@ rule plot_choropleth_demand: rule plot_balance_timeseries: input: 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", rc="matplotlibrc", threads: 12 output: directory( RESULTS - + "graphics/balance_timeseries/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" + + "graphics/balance_timeseries/s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" ), script: "../scripts/plot_balance_timeseries.py" @@ -255,13 +255,13 @@ rule plot_balance_timeseries: rule plot_heatmap_timeseries: input: 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", rc="matplotlibrc", threads: 12 output: directory( RESULTS - + "graphics/heatmap_timeseries/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" + + "graphics/heatmap_timeseries/s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" ), script: "../scripts/plot_heatmap_timeseries.py" @@ -270,13 +270,13 @@ rule plot_heatmap_timeseries: rule plot_heatmap_timeseries_resources: 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", rc="matplotlibrc", threads: 12 output: directory( RESULTS - + "graphics/heatmap_timeseries_resources/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" + + "graphics/heatmap_timeseries_resources/s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}" ), script: "../scripts/plot_heatmap_timeseries_resources.py" @@ -285,21 +285,21 @@ rule plot_heatmap_timeseries_resources: rule plot_import_options: input: network=RESULTS - + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", - regions=resources("regions_onshore_elec_s{simpl}_{clusters}.geojson"), - entrypoints=resources("gas_input_locations_s{simpl}_{clusters}_simplified.csv"), + + "prenetworks/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + regions=resources("regions_onshore_base_s_{clusters}.geojson"), + entrypoints=resources("gas_input_locations_s_{clusters}_simplified.csv"), imports="data/imports/results.csv", rc="matplotlibrc", output: map=multiext( RESULTS - + "graphics/import_options_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", + + "graphics/import_options_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", ".png", ".pdf", ), distribution=multiext( RESULTS - + "graphics/import_options_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-distribution", + + "graphics/import_options_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}-distribution", ".png", ".pdf", ), @@ -315,10 +315,7 @@ rule plot_import_world_map: "https://geodata.ucdavis.edu/gadm/gadm4.1/gpkg/gadm41_ARG.gpkg", keep_local=True, ), - copernicus_glc=storage( - "https://zenodo.org/records/3939050/files/PROBAV_LC100_global_v3.0.1_2019-nrt_Discrete-Classification-map_EPSG-4326.tif", - keep_local=True, - ), + copernicus_glc="data/PROBAV_LC100_global_v3.0.1_2019-nrt_Discrete-Classification-map_EPSG-4326.tif", wdpa="data/WDPA.gpkg", rc="matplotlibrc", output: @@ -330,13 +327,13 @@ rule plot_import_world_map: rule plot_import_networks: 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"), rc="matplotlibrc", output: multiext( RESULTS - + "graphics/import_networks/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", + + "graphics/import_networks/s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", ".png", ".pdf", ), @@ -347,12 +344,12 @@ rule plot_import_networks: rule plot_import_shares: input: 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", rc="matplotlibrc", output: multiext( RESULTS - + "graphics/import_shares/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", + + "graphics/import_shares/s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", ".png", ".pdf", ), @@ -371,20 +368,20 @@ rule plot_all_resources: rules.plot_salt_caverns_unclustered.output, rules.plot_import_world_map.output, expand( - resources("graphics/salt-caverns-s{simpl}-{clusters}-onshore.pdf"), + resources("graphics/salt-caverns-s-{clusters}-onshore.pdf"), **config["scenario"], ), expand(resources("graphics/power-network-{clusters}.pdf"), **config["scenario"]), expand( - resources("graphics/biomass-potentials-s{simpl}-{clusters}-biogas.pdf"), + resources("graphics/biomass-potentials-s-{clusters}-biogas.pdf"), **config["scenario"], ), expand( - resources("graphics/capacity-factor/s{simpl}-{clusters}"), + resources("graphics/capacity-factor/s-{clusters}"), **config["scenario"], ), expand( - resources("graphics/capacity-factor-sector/s{simpl}-{clusters}"), + resources("graphics/capacity-factor-sector/s-{clusters}"), **config["scenario"], ), @@ -392,34 +389,34 @@ rule plot_all_resources: rule plot_all_results_single: input: RESULTS - + "graphics/p_nom_opt/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", + + "graphics/p_nom_opt/s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", RESULTS - + "graphics/prices/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", + + "graphics/prices/s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", RESULTS - + "graphics/potential_used/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", + + "graphics/potential_used/s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", RESULTS - + "graphics/balance_timeseries/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", + + "graphics/balance_timeseries/s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", RESULTS - + "graphics/heatmap_timeseries/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", + + "graphics/heatmap_timeseries/s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", RESULTS - + "graphics/heatmap_timeseries_resources/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", + + "graphics/heatmap_timeseries_resources/s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", RESULTS - + "graphics/regional_demand/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", + + "graphics/regional_demand/s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}", 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", 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", 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", RESULTS - + "graphics/import_options_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.pdf", + + "graphics/import_options_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.pdf", RESULTS - + "graphics/import_networks/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.pdf", + + "graphics/import_networks/s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.pdf", RESULTS - + "graphics/import_shares/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.pdf", + + "graphics/import_shares/s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.pdf", output: RESULTS - + "graphics/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.touch", + + "graphics/s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.touch", shell: "touch {output}" @@ -428,6 +425,6 @@ rule plot_all_results: input: expand( RESULTS - + "graphics/s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.touch", + + "graphics/s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.touch", **config["scenario"], ), diff --git a/scripts/_helpers.py b/scripts/_helpers.py index ac3aee1f0..072af6640 100644 --- a/scripts/_helpers.py +++ b/scripts/_helpers.py @@ -695,13 +695,12 @@ def update_config_from_wildcards(config, w, inplace=True): if "nodistrict" in opts: config["sector"]["district_heating"]["progress"] = 0.0 - if "nosteelrelocation" in opts: + if "norelocation" in opts: logger.info("Disabling steel industry relocation and flexibility.") config["sector"]["relocation_steel"] = False config["sector"]["flexibility_steel"] = False - - if "nobiogascc" in opts: - config["sector"]["biogas_upgrading_cc"] = False + config["sector"]["relocation_ammonia"] = False + config["sector"]["flexibility_ammonia"] = False dg_enable, dg_factor = find_opt(opts, "dist") if dg_enable: diff --git a/scripts/make_summary.py b/scripts/make_summary.py index 573b13719..1a19ba39d 100644 --- a/scripts/make_summary.py +++ b/scripts/make_summary.py @@ -760,11 +760,8 @@ def to_csv(df): configure_logging(snakemake) set_scenario_config(snakemake) - s = snakemake.input.networks[0] - base_dir = s[: s.find("results/") + 8] - networks_dict = { - (cluster, ll, opt + sector_opt, planning_horizon): base_dir + (cluster, ll, opt + sector_opt, planning_horizon): "results/" + snakemake.params.RDIR + f"/postnetworks/base_s_{cluster}_l{ll}_{opt}_{sector_opt}_{planning_horizon}.nc" for cluster in snakemake.params.scenario["clusters"] diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index ec4cf8a36..0f9b97cb3 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3086,7 +3086,6 @@ 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, @@ -3104,7 +3103,7 @@ def add_biomass(n, costs): marginal_cost=costs.at["BtL", "VOM"] * costs.at["BtL", "efficiency"], ) - if options.get("biomass_to_liquid_cc"): + 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 @@ -3128,56 +3127,6 @@ def add_biomass(n, costs): marginal_cost=costs.at["BtL", "VOM"] * costs.at["BtL", "efficiency"], ) - # biomethanol - - if options.get("biomass_to_methanol"): - add_carrier_buses(n, "methanol") - - n.madd( - "Link", - spatial.biomass.nodes, - 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"], - ) - - if options.get("biomass_to_methanol_cc"): - n.madd( - "Link", - spatial.biomass.nodes, - 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"], - ) - # 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 @@ -3234,7 +3183,7 @@ def add_biomass(n, costs): marginal_cost=costs.at["BioSNG", "VOM"] * costs.at["BioSNG", "efficiency"], ) - if options.get("biosng_cc"): + 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( @@ -4374,40 +4323,6 @@ def add_waste_heat(n): / costs.at["methanolisation", "hydrogen-input"] ) * options["use_methanolisation_waste_heat"] - if options["use_methanation_waste_heat"]: - n.links.loc[urban_central + " Sabatier", "bus3"] = ( - urban_central + " urban central heat" - ) - n.links.loc[urban_central + " Sabatier", "efficiency3"] = ( - 0.95 - n.links.loc[urban_central + " Sabatier", "efficiency"] - ) - - # DEA quotes 15% of total input (11% of which are high-value heat) - if options["use_haber_bosch_waste_heat"]: - n.links.loc[urban_central + " Haber-Bosch", "bus3"] = ( - urban_central + " urban central heat" - ) - total_energy_input = ( - cf_industry["MWh_H2_per_tNH3_electrolysis"] - + cf_industry["MWh_elec_per_tNH3_electrolysis"] - ) / cf_industry["MWh_NH3_per_tNH3"] - electricity_input = ( - cf_industry["MWh_elec_per_tNH3_electrolysis"] - / cf_industry["MWh_NH3_per_tNH3"] - ) - n.links.loc[urban_central + " Haber-Bosch", "efficiency3"] = ( - 0.15 * total_energy_input / electricity_input - ) - - if options["use_methanolisation_waste_heat"]: - n.links.loc[urban_central + " methanolisation", "bus4"] = ( - urban_central + " urban central heat" - ) - n.links.loc[urban_central + " methanolisation", "efficiency4"] = ( - costs.at["methanolisation", "heat-output"] - / costs.at["methanolisation", "hydrogen-input"] - ) - # TODO integrate usable waste heat efficiency into technology-data from DEA if ( options["use_electrolysis_waste_heat"] @@ -5004,47 +4919,6 @@ def add_import_options( ) -def maybe_adjust_costs_and_potentials(n, opts): - for o in opts: - flags = ["+e", "+p", "+m"] - if all(flag not in o for flag in flags): - continue - oo = o.split("+") - carrier_list = np.hstack( - ( - n.generators.carrier.unique(), - n.links.carrier.unique(), - n.stores.carrier.unique(), - n.storage_units.carrier.unique(), - ) - ) - suptechs = map(lambda c: c.split("-", 2)[0], carrier_list) - if oo[0].startswith(tuple(suptechs)): - carrier = oo[0] - attr_lookup = {"p": "p_nom_max", "e": "e_nom_max", "c": "capital_cost"} - attr = attr_lookup[oo[1][0]] - factor = float(oo[1][1:]) - # 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 - else: - if attr == "p_nom_max": - comps = {"Generator", "Link", "StorageUnit"} - elif attr == "e_nom_max": - comps = {"Store"} - else: - comps = {"Generator", "Link", "StorageUnit", "Store"} - for c in n.iterate_components(comps): - if carrier == "solar": - sel = c.df.carrier.str.contains( - carrier - ) & ~c.df.carrier.str.contains("solar rooftop") - else: - sel = c.df.carrier.str.contains(carrier) - c.df.loc[sel, attr] *= factor - logger.info(f"changing {attr} for {carrier} by factor {factor}") - - def limit_individual_line_extension(n, maxext): logger.info(f"Limiting new HVAC and HVDC extensions to {maxext} MW") n.lines["s_nom_max"] = n.lines["s_nom"] + maxext @@ -5218,7 +5092,6 @@ def lossy_bidirectional_links(n, carrier, efficiencies={}): f"(static: {efficiency_static}, per 1000km: {efficiency_per_1000km}, compression per 1000km: {compression_per_1000km}). " "Splitting bidirectional links." ) - logger.info("Distribution of snapshot durations: ", weightings.value_counts()) n.links.loc[carrier_i, "p_min_pu"] = 0 n.links.loc[carrier_i, "efficiency"] = ( @@ -5607,17 +5480,6 @@ def parse_carriers(s): if options["allam_cycle_gas"]: add_allam_cycle_gas(n, costs) - add_methanol_to_power(n, costs, types=options["methanol_to_power"]) - - if options["methanol_to_kerosene"]: - add_methanol_to_kerosene(n, costs) - - if options["methanol_reforming"]: - add_methanol_reforming(n, costs) - - if options["methanol_reforming_cc"]: - add_methanol_reforming_cc(n, costs) - n = set_temporal_aggregation( n, snakemake.params.time_resolution, snakemake.input.snapshot_weightings ) From c9b48c7b2587897775317cad559fa6f096f41c8a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 16 Sep 2024 12:11:42 +0000 Subject: [PATCH 250/293] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- rules/build_sector.smk | 2 +- rules/plot.smk | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 1a57d318a..e70a78b13 100755 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -1124,7 +1124,7 @@ rule prepare_sector_network: import_costs="data/imports/results.csv", import_p_max_pu="data/imports/combined_weighted_generator_timeseries.nc", regions_onshore=resources("regions_onshore_base_s_{clusters}.geojson"), - country_shapes="data/naturalearth/ne_10m_admin_0_countries_deu.shp", # TODO FN: instead of country_centroids use .representative_point() + country_shapes="data/naturalearth/ne_10m_admin_0_countries_deu.shp", # TODO FN: instead of country_centroids use .representative_point() # country_centroids=storage( # "https://raw.githubusercontent.com/gavinr/world-countries-centroids/v1.0.0/dist/countries.csv", # keep_local=True, diff --git a/rules/plot.smk b/rules/plot.smk index c11d1c059..6b556c1fa 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -223,9 +223,7 @@ rule plot_choropleth_demand: "industrial_energy_demand_base_s_{clusters}_{planning_horizons}.csv" ), shipping_demand=resources("shipping_demand_s_{clusters}.csv"), - nodal_energy_totals=resources( - "pop_weighted_energy_totals_s_{clusters}.csv" - ), + nodal_energy_totals=resources("pop_weighted_energy_totals_s_{clusters}.csv"), regions_onshore=resources("regions_onshore_base_s_{clusters}.geojson"), rc="matplotlibrc", output: From fc2a5b55e4b778a7ed9552bab90468a955aa283a Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 16 Sep 2024 14:26:14 +0200 Subject: [PATCH 251/293] remove endogenous_hvc option; replaced on master with simplified option --- config/config.default.yaml | 8 +-- scripts/plot_choropleth_demand.py | 2 - scripts/prepare_sector_network.py | 104 ------------------------------ 3 files changed, 2 insertions(+), 112 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 3d8e3591c..fb0e07a02 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -565,9 +565,6 @@ sector: 2050: 0 shipping_methanol_efficiency: 0.46 shipping_oil_efficiency: 0.40 - shipping_ammonia_efficiency: 0.40 - shipping_lng_efficiency: 0.44 - shipping_lh2_efficiency: 0.44 aviation_demand_factor: 1. HVC_demand_factor: 1. time_dep_hp_cop: true @@ -698,12 +695,11 @@ sector: OCGT: gas biomass_to_liquid: false biomass_to_liquid_cc: false + electrobiofuels: false biosng: false - bioH2: false biosng_cc: false - electrobiofuels: false + bioH2: false endogenous_steel: false - endogenous_hvc: false relocation_steel: false flexibility_steel: false relocation_ammonia: false diff --git a/scripts/plot_choropleth_demand.py b/scripts/plot_choropleth_demand.py index 5e42c8f43..9bf9aae64 100644 --- a/scripts/plot_choropleth_demand.py +++ b/scripts/plot_choropleth_demand.py @@ -233,8 +233,6 @@ def plot_regional_demands(df, geodf, carrier, dir="."): endogenous_sectors = [] if options["endogenous_steel"]: endogenous_sectors += ["DRI + Electric arc"] - if options["endogenous_hvc"]: - endogenous_sectors += ["HVC"] sectors_b = ~industrial_demand.index.get_level_values("sector").isin( endogenous_sectors ) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 0f9b97cb3..53e33b8fb 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3273,8 +3273,6 @@ def add_industry(n, costs): endogenous_sectors = [] if options["endogenous_steel"]: endogenous_sectors += ["DRI + Electric arc"] - if options["endogenous_hvc"]: - endogenous_sectors += ["HVC"] sectors_b = ~industrial_demand.index.get_level_values("sector").isin( endogenous_sectors ) @@ -3774,108 +3772,6 @@ def add_industry(n, costs): lifetime=costs.at["Fischer-Tropsch", "lifetime"], ) - if options["endogenous_hvc"]: - logger.info("Adding endogenous primary high-value chemicals demand in tonnes.") - - n.add( - "Bus", - "EU HVC", - location="EU", - carrier="HVC", - unit="t", - ) - - p_set = demand_factor * industrial_production["HVC"].sum() / nhours - - n.add( - "Load", - "EU HVC", - bus="EU HVC", - carrier="HVC", - p_set=p_set, - ) - - # n.add( - # "Store", - # "EU HVC Store", - # bus="EU HVC", - # e_nom_extendable=True, - # e_cyclic=True, - # carrier="HVC", - # ) - - tech = "methanol-to-olefins/aromatics" - - logger.info(f"Adding {tech}.") - - 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="EU HVC", - 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, - ) - - tech = "electric steam cracker" - - logger.info(f"Adding {tech}.") - - p_nom_max = ( - demand_factor - * industrial_production.loc[nodes, "HVC"] - / nhours - * costs.at[tech, "naphtha-input"] - ) - - co2_release = ( - costs.at[tech, "carbondioxide-output"] / costs.at[tech, "naphtha-input"] - + costs.at["oil", "CO2 intensity"] - ) - - n.madd( - "Link", - nodes, - suffix=f" {tech}", - carrier=tech, - capital_cost=costs.at[tech, "fixed"] / costs.at[tech, "naphtha-input"], - marginal_cost=costs.at[tech, "VOM"] / costs.at[tech, "naphtha-input"], - p_nom_extendable=True, - bus0=spatial.oil.nodes, - bus1="EU HVC", - bus2=nodes, - bus3="co2 atmosphere", - p_min_pu=1, - p_nom_max=p_nom_max.values, - efficiency=1 / costs.at[tech, "naphtha-input"], - efficiency2=-costs.at[tech, "electricity-input"] - / costs.at[tech, "naphtha-input"], - efficiency3=co2_release, - ) - # naphtha demand_factor = options.get("HVC_demand_factor", 1) if demand_factor != 1: From 18efa555d1eed16177d980d3006cf54c7d30a709 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 16 Sep 2024 14:55:08 +0200 Subject: [PATCH 252/293] adjust to industry_demand format --- scripts/prepare_sector_network.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 53e33b8fb..c7664477b 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -533,11 +533,7 @@ def add_carrier_buses(n, carrier, nodes=None): ) fossils = ["coal", "gas", "oil", "lignite"] - if ( - options.get("fossil_fuels", True) - and carrier in fossils - and costs.at[carrier, "fuel"] > 0 - ): + if options.get("fossil_fuels", True) and carrier in fossils: suffix = "" @@ -3536,7 +3532,7 @@ def add_industry(n, costs): ) p_set_methanol = ( - industrial_demand["methanol"].rename(lambda x: x + " industry methanol") + industrial_demand.loc[(nodes, sectors_b), "methanol"].rename(lambda x: x + " industry methanol") / nhours ) @@ -3807,8 +3803,8 @@ def add_industry(n, costs): # some CO2 from naphtha are process emissions from steam cracker # rest of CO2 released to atmosphere either in waste-to-energy or decay process_co2_per_naphtha = ( - industrial_demand.loc[nodes, "process emission from feedstock"].sum() - / industrial_demand.loc[nodes, "naphtha"].sum() + industrial_demand.loc[(nodes, sectors_b), "process emission from feedstock"].sum() + / industrial_demand.loc[(nodes, sectors_b), "naphtha"].sum() ) emitted_co2_per_naphtha = costs.at["oil", "CO2 intensity"] - process_co2_per_naphtha From 02395104a8ab608782c9d0a39eb57b476d888ebd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 16 Sep 2024 12:57:31 +0000 Subject: [PATCH 253/293] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/prepare_sector_network.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index c7664477b..e534da4ed 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3532,7 +3532,9 @@ def add_industry(n, costs): ) p_set_methanol = ( - industrial_demand.loc[(nodes, sectors_b), "methanol"].rename(lambda x: x + " industry methanol") + industrial_demand.loc[(nodes, sectors_b), "methanol"].rename( + lambda x: x + " industry methanol" + ) / nhours ) @@ -3803,7 +3805,9 @@ def add_industry(n, costs): # some CO2 from naphtha are process emissions from steam cracker # rest of CO2 released to atmosphere either in waste-to-energy or decay process_co2_per_naphtha = ( - industrial_demand.loc[(nodes, sectors_b), "process emission from feedstock"].sum() + industrial_demand.loc[ + (nodes, sectors_b), "process emission from feedstock" + ].sum() / industrial_demand.loc[(nodes, sectors_b), "naphtha"].sum() ) emitted_co2_per_naphtha = costs.at["oil", "CO2 intensity"] - process_co2_per_naphtha From 8eae4fc240dddf8049124eb98da5cfeaf90121a2 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 16 Sep 2024 15:40:09 +0200 Subject: [PATCH 254/293] update data/import-sites.csv --- data/import-sites.csv | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/data/import-sites.csv b/data/import-sites.csv index 329fc137e..9ecb8b713 100644 --- a/data/import-sites.csv +++ b/data/import-sites.csv @@ -1,13 +1,12 @@ -name,y,x,reference,type,comment -EUSW,36.39,-5.64,https://openinframap.org/,pipeline, -EUS,37.18,14.53,https://openinframap.org/,pipeline, -EUSE,41.09,25.46,https://openinframap.org/,pipeline, -EUE,49.91,22.87,https://openinframap.org/,pipeline, -EUNE,57.53,27.34,https://openinframap.org/,pipeline, -BEZEE,51.353,3.22241,https://www.gem.wiki/Zeebrugge_LNG_Terminal,lng, -ESLCG,43.4613,-7.5,https://www.gem.wiki/Mugardos_LNG_Terminal,lng,"center coordinate moved east from -8.2395" -ESVLC,39.6329,-0.2152,https://www.gem.wiki/Sagunto_LNG_Terminal,lng, -GBMLF,51.7152,-7,https://www.gem.wiki/South_Hook_LNG_Terminal,lng,"center coordinate moved west from -5.07627" -GREEV,37.96,23.4023,https://www.gem.wiki/Revithoussa_LNG_Terminal,lng, -ITVCE,45.091667,12.586111,https://www.gem.wiki/Adriatic_LNG_Terminal,lng, -PLSWI,53.909167,17,https://www.gem.wiki/%C5%9Awinouj%C5%9Bcie_Polskie_LNG_Terminal,lng,"center coordinate moved east from 14.294722" +name,y,x,comment,type +EUSW,36.39,-5.64,https://openinframap.org/,pipeline +EUS,37.18,14.53,https://openinframap.org/,pipeline +EUSE,41.09,25.46,https://openinframap.org/,pipeline +EUE,48.54,22.14,https://openinframap.org/,pipeline +T0462,37.9600,23.4023,Revithoussa LNG terminal Greece,lng +T0466,45.0917,12.5861,Adriatic LNG terminal Italy,lng +T0492,51.9711,4.0689,Gate LNG terminal Netherlands,lng +T0498,53.9092,14.2947,Swinoujscie LNG terminal Poland,lng +T0500,37.9419,-8.8435,Sines LNG terminal Portugal,lng +T0522,41.3394,2.1583,Barcelona LNG terminal Spain,lng +T0557,51.7152,-5.0763,South Hook LNG terminal UK,lng From b29710bf5714353918e6060415adcd5df90c31d6 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 16 Sep 2024 16:18:38 +0200 Subject: [PATCH 255/293] build_gas_input_locations: fewer border extensions, replace voronoi_partition_pts --- scripts/base_network.py | 4 ++-- scripts/build_gas_input_locations.py | 13 +++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/scripts/base_network.py b/scripts/base_network.py index a6a54617b..33205e5a7 100644 --- a/scripts/base_network.py +++ b/scripts/base_network.py @@ -859,7 +859,7 @@ def _get_linetype_by_voltage(v_nom, d_linetypes): return line_type_min -def voronoi(points, outline, crs=4326): +def voronoi(points, outline, crs=4326, dissolve_by="Bus"): """ Create Voronoi polygons from a set of points within an outline. """ @@ -878,7 +878,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").reindex(points.index).squeeze() + return joined.dissolve(by=dissolve_by).reindex(points.index).squeeze() def build_bus_shapes(n, country_shapes, offshore_shapes, countries): diff --git a/scripts/build_gas_input_locations.py b/scripts/build_gas_input_locations.py index 87fcac1e7..b51552d93 100644 --- a/scripts/build_gas_input_locations.py +++ b/scripts/build_gas_input_locations.py @@ -14,6 +14,7 @@ import pandas as pd from _helpers import configure_logging, set_scenario_config from cluster_gas_network import load_bus_regions +from base_network import voronoi logger = logging.getLogger(__name__) @@ -139,17 +140,17 @@ def assign_reference_import_sites(gas_input_locations, import_sites, europe_shap europe_shape = europe_shape.squeeze().geometry.buffer(1) # 1 latlon degree for kind in ["lng", "pipeline"]: - locs = import_sites.query("type == @kind") + import_locs = import_sites.query("type == @kind") - partition = voronoi_partition_pts(locs[["x", "y"]].values, europe_shape) - partition = gpd.GeoDataFrame(dict(name=locs.index, geometry=partition)) + partition = voronoi(import_locs, europe_shape, dissolve_by="name") + partition = gpd.GeoDataFrame(dict(name=import_locs.index, geometry=partition)) partition = partition.set_crs(4326).set_index("name") match = gpd.sjoin( gas_input_locations.query("type == @kind"), partition, how="left" ) gas_input_locations.loc[gas_input_locations["type"] == kind, "port"] = match[ - "index_right" + "name" ] return gas_input_locations @@ -175,9 +176,9 @@ def assign_reference_import_sites(gas_input_locations, import_sites, europe_shap import_sites = pd.read_csv(snakemake.input.reference_import_sites, index_col=0) # add a buffer to eastern countries because some - # entry points are still in Russian or Ukrainian territory. + # entry points are still in Ukrainian territory. buffer = 9000 # meters - eastern_countries = ["FI", "EE", "LT", "LV", "PL", "SK", "HU", "RO"] + eastern_countries = ["SK", "HU", "RO"] add_buffer_b = regions.index.str[:2].isin(eastern_countries) regions.loc[add_buffer_b] = ( regions[add_buffer_b].to_crs(3035).buffer(buffer).to_crs(4326) From 1c083c4b6eff290675bad73d4f9e3dcc4bf21c32 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 16 Sep 2024 14:21:55 +0000 Subject: [PATCH 256/293] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/build_gas_input_locations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build_gas_input_locations.py b/scripts/build_gas_input_locations.py index b51552d93..666810155 100644 --- a/scripts/build_gas_input_locations.py +++ b/scripts/build_gas_input_locations.py @@ -13,8 +13,8 @@ import geopandas as gpd import pandas as pd from _helpers import configure_logging, set_scenario_config -from cluster_gas_network import load_bus_regions from base_network import voronoi +from cluster_gas_network import load_bus_regions logger = logging.getLogger(__name__) From 3bd422a2e14d80bc92e72fc4a7a1a22efcfa97a0 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 16 Sep 2024 18:31:17 +0200 Subject: [PATCH 257/293] split industrial_energy_demand_per_node by sector --- scripts/build_industrial_energy_demand_per_node.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/scripts/build_industrial_energy_demand_per_node.py b/scripts/build_industrial_energy_demand_per_node.py index f9927147f..cb4c70dbd 100644 --- a/scripts/build_industrial_energy_demand_per_node.py +++ b/scripts/build_industrial_energy_demand_per_node.py @@ -70,11 +70,7 @@ nodal_production_stacked.index.names = [None, None] # final energy consumption per node and industry (TWh/a) - nodal_df = ( - (nodal_sector_ratios.multiply(nodal_production_stacked)) - .T.groupby(level=0) - .sum() - ) + nodal_df = nodal_sector_ratios.multiply(nodal_production_stacked).T rename_sectors = { "elec": "electricity", From 9aa09c0cdfca97d632a3ae385f4e869345aa6ce1 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 16 Sep 2024 18:31:46 +0200 Subject: [PATCH 258/293] industrial production per country, adjust to dictionary HVC_production_today --- scripts/build_industrial_production_per_country.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build_industrial_production_per_country.py b/scripts/build_industrial_production_per_country.py index b7f98d760..bc5f2eb80 100644 --- a/scripts/build_industrial_production_per_country.py +++ b/scripts/build_industrial_production_per_country.py @@ -317,7 +317,7 @@ def separate_basic_chemicals(demand, year): / params["basic_chemicals_without_NH3_production_today"] / 1e3 ) - demand["HVC"] = params["HVC_production_today"] * 1e3 * distribution_key + demand["HVC"] = sum(params["HVC_production_today"].values()) * 1e3 * distribution_key demand["Chlorine"] = params["chlorine_production_today"] * 1e3 * distribution_key demand["Methanol"] = params["methanol_production_today"] * 1e3 * distribution_key From a0a581b83d384cc79994ad0f61c5ee68a098d638 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 16 Sep 2024 18:33:08 +0200 Subject: [PATCH 259/293] simplify centroid calculation for exporting countryies --- rules/build_sector.smk | 6 +----- scripts/prepare_sector_network.py | 35 +++++++------------------------ 2 files changed, 9 insertions(+), 32 deletions(-) diff --git a/rules/build_sector.smk b/rules/build_sector.smk index e70a78b13..98764a20d 100755 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -1124,11 +1124,7 @@ rule prepare_sector_network: import_costs="data/imports/results.csv", import_p_max_pu="data/imports/combined_weighted_generator_timeseries.nc", regions_onshore=resources("regions_onshore_base_s_{clusters}.geojson"), - country_shapes="data/naturalearth/ne_10m_admin_0_countries_deu.shp", # TODO FN: instead of country_centroids use .representative_point() - # country_centroids=storage( - # "https://raw.githubusercontent.com/gavinr/world-countries-centroids/v1.0.0/dist/countries.csv", - # keep_local=True, - # ), + country_shapes="data/naturalearth/ne_10m_admin_0_countries_deu.shp", output: RESULTS + "prenetworks/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 48e7d8f06..463c37051 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -38,8 +38,6 @@ from definitions.heat_sector import HeatSector from definitions.heat_system import HeatSystem from definitions.heat_system_type import HeatSystemType -from geopy.extra.rate_limiter import RateLimiter -from geopy.geocoders import Nominatim from networkx.algorithms import complement from networkx.algorithms.connectivity.edge_augmentation import k_edge_augmentation from prepare_network import maybe_adjust_costs_and_potentials @@ -48,11 +46,6 @@ from scipy.stats import beta from shapely.geometry import Point -cc = coco.CountryConverter() - -geolocator = Nominatim(user_agent=str(uuid.uuid4()), timeout=10) -geocode = RateLimiter(geolocator.geocode, min_delay_seconds=2) - spatial = SimpleNamespace() logger = logging.getLogger(__name__) @@ -4322,20 +4315,10 @@ def add_endogenous_hvdc_import_options(n, cost_factor=1.0): .to_pandas() ) - def _coordinates(ct): - iso2 = ct.split("-")[0] - if iso2 in country_centroids.index: - return country_centroids.loc[iso2, ["longitude", "latitude"]].values - else: - query = cc.convert(iso2, to="name") - loc = geocode(dict(country=query), language="en") - return [loc.longitude, loc.latitude] - - exporters = pd.DataFrame( - {ct: _coordinates(ct) for ct in cf["exporters"]}, index=["x", "y"] - ).T - geometry = gpd.points_from_xy(exporters.x, exporters.y) - exporters = gpd.GeoDataFrame(exporters, geometry=geometry, crs=4326) + exporters_iso2 = [e.split("-")[0] for e in cf["exporters"]] # noqa + exporters = country_shapes.query( + "ISO_A2 in @exporters_iso2" + ).set_index("ISO_A2").representative_point() import_links = {} a = regions.representative_point().to_crs(DISTANCE_CRS) @@ -4345,14 +4328,14 @@ def _coordinates(ct): a = a.loc[~a.index.str[:2].isin(forbidden_hvdc_importers)] for ct in exporters.index: - b = exporters.to_crs(DISTANCE_CRS).loc[ct].geometry + b = exporters.to_crs(DISTANCE_CRS)[ct] d = a.distance(b) import_links[ct] = ( d.where(d < d.quantile(cf["distance_threshold"])).div(1e3).dropna() ) # km import_links = pd.concat(import_links) import_links.loc[ - import_links.index.get_level_values(0).str.contains("KZ|CN") + import_links.index.get_level_values(0).str.contains("KZ|CN|MN|UZ") ] *= 1.2 # proxy for detour through Caucasus # xlinks @@ -4381,7 +4364,7 @@ def _coordinates(ct): buses_i = exporters.index - n.madd("Bus", buses_i, **exporters.drop("geometry", axis=1)) + n.madd("Bus", buses_i, x=exporters.x, y=exporters.y) efficiency = cf["efficiency_static"] * cf["efficiency_per_1000km"] ** ( import_links.values / 1e3 @@ -5210,9 +5193,7 @@ def add_enhanced_geothermal(n, egs_potentials, egs_overlap, costs): ) pop_weighted_energy_totals.update(pop_weighted_heat_totals) - country_centroids = pd.read_csv( - snakemake.input.country_centroids[0], index_col="ISO" - ) + country_shapes = gpd.read_file(snakemake.input.country_shapes) landfall_lengths = { tech: settings["landfall_length"] From aab1dd365e2dcbaf5806783f74eb64810f83132f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 16 Sep 2024 16:34:01 +0000 Subject: [PATCH 260/293] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/build_industrial_production_per_country.py | 4 +++- scripts/prepare_sector_network.py | 10 ++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/scripts/build_industrial_production_per_country.py b/scripts/build_industrial_production_per_country.py index bc5f2eb80..d7a2a0071 100644 --- a/scripts/build_industrial_production_per_country.py +++ b/scripts/build_industrial_production_per_country.py @@ -317,7 +317,9 @@ def separate_basic_chemicals(demand, year): / params["basic_chemicals_without_NH3_production_today"] / 1e3 ) - demand["HVC"] = sum(params["HVC_production_today"].values()) * 1e3 * distribution_key + demand["HVC"] = ( + sum(params["HVC_production_today"].values()) * 1e3 * distribution_key + ) demand["Chlorine"] = params["chlorine_production_today"] * 1e3 * distribution_key demand["Methanol"] = params["methanol_production_today"] * 1e3 * distribution_key diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 463c37051..eace963a8 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -4315,10 +4315,12 @@ def add_endogenous_hvdc_import_options(n, cost_factor=1.0): .to_pandas() ) - exporters_iso2 = [e.split("-")[0] for e in cf["exporters"]] # noqa - exporters = country_shapes.query( - "ISO_A2 in @exporters_iso2" - ).set_index("ISO_A2").representative_point() + exporters_iso2 = [e.split("-")[0] for e in cf["exporters"]] # noqa + exporters = ( + country_shapes.query("ISO_A2 in @exporters_iso2") + .set_index("ISO_A2") + .representative_point() + ) import_links = {} a = regions.representative_point().to_crs(DISTANCE_CRS) From 269496db2d2d0f182c4f8e151d57e7a43fcdd393 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Tue, 17 Sep 2024 15:47:52 +0200 Subject: [PATCH 261/293] add option noPtXflex which sets min_part_load to 1 for all PtX processes --- config/config.default.yaml | 2 ++ scripts/_helpers.py | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/config/config.default.yaml b/config/config.default.yaml index 5afe1eaba..30c1d1944 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -659,6 +659,8 @@ sector: min_part_load_fischer_tropsch: 0.5 min_part_load_methanolisation: 0.3 min_part_load_methanation: 0.3 + min_part_load_electrolysis: 0 + min_part_load_haber_bosch: 0 use_fischer_tropsch_waste_heat: 0.25 use_haber_bosch_waste_heat: 0.25 use_methanolisation_waste_heat: 0.25 diff --git a/scripts/_helpers.py b/scripts/_helpers.py index 072af6640..c87828448 100644 --- a/scripts/_helpers.py +++ b/scripts/_helpers.py @@ -702,6 +702,14 @@ def update_config_from_wildcards(config, w, inplace=True): config["sector"]["relocation_ammonia"] = False config["sector"]["flexibility_ammonia"] = False + if "noPtXflex" in opts: + logger.info("Disabling power-to-x flexibility.") + config["sector"]["min_part_load_fischer_tropsch"] = 1 + config["sector"]["min_part_load_methanolisation"] = 1 + config["sector"]["min_part_load_methanation"] = 1 + config["sector"]["min_part_load_electrolysis"] = 1 + config["sector"]["min_part_load_haber_bosch"] = 1 + dg_enable, dg_factor = find_opt(opts, "dist") if dg_enable: config["sector"]["electricity_distribution_grid"] = True From 4c97add0fd4ff32e58c024b62bb5655ba1b1edb0 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Tue, 17 Sep 2024 15:50:18 +0200 Subject: [PATCH 262/293] minor fixes, ammonia relocation, revise steel relocation --- config/config.default.yaml | 6 ++-- scripts/prepare_sector_network.py | 50 +++++++++++++++---------------- 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 30c1d1944..537ac63d5 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -702,9 +702,7 @@ sector: bioH2: false endogenous_steel: false relocation_steel: false - flexibility_steel: false relocation_ammonia: false - flexibility_ammonia: false import: capacity_boost: 2 limit: false # bool or number in TWh @@ -1334,6 +1332,7 @@ plotting: heat pumps: '#2fb537' heat pump: '#2fb537' air heat pump: '#36eb41' + rural air heat pump: '#36eb41' residential urban decentral air heat pump: '#48f74f' services urban decentral air heat pump: '#5af95d' urban decentral air heat pump: '#5af95d' @@ -1474,6 +1473,9 @@ plotting: external HVDC: "#8a1caf" steel: '#b1bbc9' DRI + Electric arc: '#b1bbc9' + DRI: '#b1bbc9' + EAF: '#b1bba1' + HBI: '#cd7a56' shipping ammonia: '#46caf0' biomass to methanol CC: '#468c8b' biomass to methanol: '#468c8b' diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 463c37051..ca10cc0f9 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -10,11 +10,9 @@ import logging import os import re -import uuid from itertools import product from types import SimpleNamespace -import country_converter as coco import geopandas as gpd import networkx as nx import numpy as np @@ -1161,6 +1159,13 @@ def add_ammonia(n, costs): nodes = pop_layout.index + p_nom = n.loads.loc[spatial.ammonia.nodes, "p_set"] + + no_relocation = not options["relocation_ammonia"] + + s = " not" if no_relocation else "" + logger.info(f"Ammonia industry relocation{s} activated.") + n.add("Carrier", "NH3") n.madd( @@ -1174,7 +1179,10 @@ def add_ammonia(n, costs): bus0=nodes, bus1=spatial.ammonia.nodes, bus2=nodes + " H2", - p_nom_extendable=True, + p_nom=p_nom if no_relocation else 0, + p_nom_extendable=False if no_relocation else True, + p_max_pu=1 if no_relocation else 0, # so that no imports can substitute + p_min_pu=options["min_part_load_haber_bosch"], carrier="Haber-Bosch", efficiency=1 / costs.at["Haber-Bosch", "electricity-input"], efficiency2=-costs.at["Haber-Bosch", "hydrogen-input"] @@ -1396,6 +1404,7 @@ def add_storage_and_grids(n, costs): efficiency=costs.at["electrolysis", "efficiency"], capital_cost=costs.at["electrolysis", "fixed"], lifetime=costs.at["electrolysis", "lifetime"], + p_min_pu=options["min_part_load_electrolysis"], ) if options["hydrogen_fuel_cell"]: @@ -3190,9 +3199,6 @@ def add_industry(n, costs): nhours = n.snapshot_weightings.generators.sum() nyears = nhours / 8760 - add_carrier_buses(n, "oil") - add_carrier_buses(n, "methanol") - # 1e6 to convert TWh to MWh industrial_demand = ( pd.read_csv(snakemake.input.industrial_demand, index_col=[0, 1]) * 1e6 * nyears @@ -3219,15 +3225,11 @@ def add_industry(n, costs): sector = "DRI + Electric arc" - no_relocation = not options.get("relocation_steel", False) - no_flexibility = not options.get("flexibility_steel", False) + no_relocation = not options["relocation_steel"] - s = " not" if no_relocation else " " + s = " not" if no_relocation else "" logger.info(f"Steel industry relocation{s} activated.") - s = " not" if no_flexibility else " " - logger.info(f"Steel industry flexibility{s} activated.") - n.add( "Bus", "EU steel", @@ -3252,7 +3254,7 @@ def add_industry(n, costs): p_set=industrial_production[sector].sum() / nhours, ) - if not no_flexibility: + if not no_relocation: n.add( "Store", "EU steel Store", @@ -3299,9 +3301,9 @@ def add_industry(n, costs): capital_cost=costs.at["direct iron reduction furnace", "fixed"] / electricity_input, marginal_cost=marginal_cost, - p_nom_max=p_nom if no_relocation else np.inf, - p_nom_extendable=True, - p_min_pu=1 if no_flexibility else 0, + p_nom=p_nom if no_relocation else 0, + p_nom_extendable=False if no_relocation else True, + p_min_pu=1 if no_relocation else 0, # so that no imports can substitute bus0=nodes, bus1="EU HBI", bus2=nodes + " H2", @@ -3319,9 +3321,9 @@ def add_industry(n, costs): suffix=" EAF", carrier="EAF", capital_cost=costs.at["electric arc furnace", "fixed"] / electricity_input, - p_nom_max=p_nom if no_relocation else np.inf, - p_nom_extendable=True, - p_min_pu=1 if no_flexibility else 0, + p_nom=p_nom if no_relocation else 0, + p_nom_extendable=False if no_relocation else True, + p_min_pu=1 if no_relocation else 0, # so that no imports can substitute bus0=nodes, bus1="EU steel", bus2="EU HBI", @@ -3986,14 +3988,12 @@ def add_industry(n, costs): -industrial_demand.loc[(nodes, sectors_b), "process emission"] .groupby(level="node") .sum() - .sum(axis=1) .rename(index=lambda x: x + " process emissions") / nhours ) else: p_set = ( -industrial_demand.loc[(nodes, sectors_b), "process emission"] - .sum(axis=1) .sum() / nhours ) @@ -5232,15 +5232,15 @@ def add_enhanced_geothermal(n, egs_potentials, egs_overlap, costs): if options["biomass"]: add_biomass(n, costs) - if options["ammonia"]: - add_ammonia(n, costs) - if options["methanol"]: add_methanol(n, costs) if options["industry"]: add_industry(n, costs) + if options["ammonia"]: + add_ammonia(n, costs) + if options["heating"]: add_waste_heat(n) @@ -5269,7 +5269,7 @@ def add_enhanced_geothermal(n, egs_potentials, egs_overlap, costs): St=["shipping-steel"], HBI=["shipping-HBI"], ) - for o in opts: + for o in snakemake.wildcards.sector_opts.split("-"): if not o.startswith("imp"): continue subsets = o.split("+")[1:] From 610191e5d570b500198f55b8316b60c978a63fce Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 17 Sep 2024 13:53:27 +0000 Subject: [PATCH 263/293] [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, 4 insertions(+), 5 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 5345527a1..53d135313 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1181,7 +1181,7 @@ def add_ammonia(n, costs): bus2=nodes + " H2", p_nom=p_nom if no_relocation else 0, p_nom_extendable=False if no_relocation else True, - p_max_pu=1 if no_relocation else 0, # so that no imports can substitute + p_max_pu=1 if no_relocation else 0, # so that no imports can substitute p_min_pu=options["min_part_load_haber_bosch"], carrier="Haber-Bosch", efficiency=1 / costs.at["Haber-Bosch", "electricity-input"], @@ -3303,7 +3303,7 @@ def add_industry(n, costs): marginal_cost=marginal_cost, p_nom=p_nom if no_relocation else 0, p_nom_extendable=False if no_relocation else True, - p_min_pu=1 if no_relocation else 0, # so that no imports can substitute + p_min_pu=1 if no_relocation else 0, # so that no imports can substitute bus0=nodes, bus1="EU HBI", bus2=nodes + " H2", @@ -3323,7 +3323,7 @@ def add_industry(n, costs): capital_cost=costs.at["electric arc furnace", "fixed"] / electricity_input, p_nom=p_nom if no_relocation else 0, p_nom_extendable=False if no_relocation else True, - p_min_pu=1 if no_relocation else 0, # so that no imports can substitute + p_min_pu=1 if no_relocation else 0, # so that no imports can substitute bus0=nodes, bus1="EU steel", bus2="EU HBI", @@ -3993,8 +3993,7 @@ def add_industry(n, costs): ) else: p_set = ( - -industrial_demand.loc[(nodes, sectors_b), "process emission"] - .sum() + -industrial_demand.loc[(nodes, sectors_b), "process emission"].sum() / nhours ) From 6e1484f02a45d79f0b2151f8709c7e66605fd638 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Tue, 17 Sep 2024 17:15:27 +0200 Subject: [PATCH 264/293] min_part_load_shipping_imports --- config/config.default.yaml | 1 + scripts/_helpers.py | 4 ++++ scripts/prepare_sector_network.py | 3 +++ 3 files changed, 8 insertions(+) diff --git a/config/config.default.yaml b/config/config.default.yaml index 537ac63d5..0c784c387 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -717,6 +717,7 @@ sector: - hvdc-to-elec p_nom_max: pipeline-h2: .inf + min_part_load_shipping_imports: 0 endogenous_hvdc_import: enable: false exporters: diff --git a/scripts/_helpers.py b/scripts/_helpers.py index c87828448..f699aa73e 100644 --- a/scripts/_helpers.py +++ b/scripts/_helpers.py @@ -710,6 +710,10 @@ def update_config_from_wildcards(config, w, inplace=True): config["sector"]["min_part_load_electrolysis"] = 1 config["sector"]["min_part_load_haber_bosch"] = 1 + if "noshipflex" in opts: + logger.info("Disabling shipping import flexibility.") + config["sector"]["import"]["min_part_load_shipping_imports"] = 1 + dg_enable, dg_factor = find_opt(opts, "dist") if dg_enable: config["sector"]["electricity_distribution_grid"] = True diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 5345527a1..8ebfc8ff5 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -4655,6 +4655,7 @@ def add_import_options( p_nom_max=import_nodes_tech.p_nom.mul(capacity_boost) .clip(upper=upper_p_nom_max) .values, + p_min_pu=import_config["min_part_load_shipping_imports"], ) else: @@ -4719,6 +4720,7 @@ def add_import_options( efficiency2=-costs.at[co2_intensity[tech][0], co2_intensity[tech][1]], marginal_cost=marginal_costs, p_nom=1e7, + p_min_pu=import_config["min_part_load_shipping_imports"], ) copperplated_carbonfree_options = { @@ -4742,6 +4744,7 @@ def add_import_options( carrier=f"import {tech}", marginal_cost=marginal_costs, p_nom=1e7, + p_min_pu=import_config["min_part_load_shipping_imports"], ) From cdf9455a57613987357d93ca721b34bda67b8287 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 18 Sep 2024 17:43:56 +0200 Subject: [PATCH 265/293] major: revise carbonaceous import options implementation --- config/config.default.yaml | 2 + rules/build_sector.smk | 2 +- scripts/prepare_sector_network.py | 321 +++++++++++++++--------------- scripts/solve_network.py | 15 +- 4 files changed, 166 insertions(+), 174 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 0c784c387..67c825ea5 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -718,6 +718,8 @@ sector: p_nom_max: pipeline-h2: .inf min_part_load_shipping_imports: 0 + min_part_load_pipeline_imports: 0.9 + exporter_energy_limit: 500.e+6 endogenous_hvdc_import: enable: false exporters: diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 98764a20d..a361fff7e 100755 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -1121,7 +1121,7 @@ rule prepare_sector_network: if config_provider("sector", "enhanced_geothermal", "enable")(w) else [] ), - import_costs="data/imports/results.csv", + import_costs="data/imports/results.parquet", import_p_max_pu="data/imports/combined_weighted_generator_timeseries.nc", regions_onshore=resources("regions_onshore_base_s_{clusters}.geojson"), country_shapes="data/naturalearth/ne_10m_admin_0_countries_deu.shp", diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 04c36af0c..74005b6a6 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -4542,7 +4542,8 @@ def add_import_options( import_nodes = pd.read_csv(fn, index_col=0) import_nodes["hvdc-to-elec"] = 15000 - import_config = snakemake.config["sector"]["import"] + import_config = snakemake.params["sector"]["import"] + cost_year = snakemake.params["costs"]["year"] # noqa: F841 ports = pd.read_csv(snakemake.input.ports, index_col=0) @@ -4572,15 +4573,17 @@ def add_import_options( "shipping-meoh": ("methanolisation", "carbondioxide-input"), } - import_costs = pd.read_csv( - snakemake.input.import_costs, delimiter=";", keep_default_na=False - ) + terminal_capital_cost = { + "shipping-lch4": 7018, # €/MW/a + "shipping-lh2": 7018 * 1.2, #+20% compared to LNG + } - # temporary bugfix for Namibia - import_costs["exporter"] = import_costs.exporter.replace("", "NA") + import_costs = pd.read_parquet( + snakemake.input.import_costs + ).reset_index().query("year == @cost_year and scenario == 'default'") cols = ["esc", "exporter", "importer", "value"] - fields = ["Cost per MWh delivered", "Cost per t delivered"] + fields = ["Cost per MWh delivered", "Cost per t delivered"] # noqa: F841 import_costs = import_costs.query("subcategory in @fields")[cols] import_costs.rename(columns={"value": "marginal_cost"}, inplace=True) @@ -4591,6 +4594,16 @@ def add_import_options( if endogenous_hvdc and "hvdc-to-elec" in import_options: add_endogenous_hvdc_import_options(n, import_options.pop("hvdc-to-elec")) + export_buses = import_costs.query("esc in @import_options").exporter.unique() + " export" + n.madd("Bus", export_buses, carrier="export") + n.madd( + "Store", + export_buses + " budget", + bus=export_buses, + e_nom=import_config["exporter_energy_limit"], + e_nom_initial=import_config["exporter_energy_limit"], + ) + regionalised_options = { "hvdc-to-elec", "pipeline-h2", @@ -4599,151 +4612,128 @@ def add_import_options( } for tech in set(import_options).intersection(regionalised_options): - import_costs_tech = ( - import_costs.query("esc == @tech").groupby("importer").marginal_cost.min() - ) * import_options[tech] - - sel = ~import_nodes[tech].isna() - if tech == "pipeline-h2": - entrypoints_internal = ["DE", "BE", "FR", "GB"] - entrypoints_via_RU_BY = ["EE", "LT", "LV", "FI"] # maybe PL Yamal - forbidden_pipelines = entrypoints_internal + entrypoints_via_RU_BY - sel &= ~import_nodes.index.str[:2].isin(forbidden_pipelines) - import_nodes_tech = import_nodes.loc[sel, [tech]] - import_nodes_tech = import_nodes_tech.rename(columns={tech: "p_nom"}) + import_nodes_tech = import_nodes[tech].dropna() + forbidden_importers = [] + if "pipeline" in tech: + forbidden_importers.extend(["DE", "BE", "FR", "GB"]) # internal entrypoints + forbidden_importers.extend(["EE", "LT", "LV", "FI"]) # entrypoints via RU_BY + sel = ~import_nodes_tech.index.str.contains("|".join(forbidden_importers)) + import_nodes_tech = import_nodes_tech.loc[sel] - marginal_costs = ports[tech].dropna().map(import_costs_tech) - import_nodes_tech["marginal_cost"] = marginal_costs + groupers = ["exporter", "importer"] + df = ( + import_costs.query("esc == @tech") + .groupby(groupers) + .marginal_cost.min() + .mul(import_options[tech]) + .reset_index() + ) - import_nodes_tech.dropna(inplace=True) + bus_ports = ports[tech].dropna() - upper_p_nom_max = import_config["p_nom_max"].get(tech, np.inf) + df["importer"] = df["importer"].map(bus_ports.groupby(bus_ports).groups) + df = df.explode("importer").query("importer in @import_nodes_tech.index") + df["p_nom"] = df["importer"].map(import_nodes_tech) suffix = bus_suffix[tech] - if tech in co2_intensity.keys(): - buses = import_nodes_tech.index + f"{suffix} import {tech}" - capital_cost = 7018.0 if tech == "shipping-lch4" else 0.0 # €/MW/a - - n.madd("Bus", buses + " bus", carrier=f"import {tech}") - - n.madd( - "Store", - buses + " store", - bus=buses + " bus", - e_nom_extendable=True, - e_nom_min=-np.inf, - e_nom_max=0, - e_min_pu=1, - e_max_pu=0, - ) - - n.madd( - "Link", - buses, - bus0=buses + " bus", - bus1=import_nodes_tech.index + suffix, - bus2="co2 atmosphere", - carrier=f"import {tech}", - efficiency2=-costs.at[co2_intensity[tech][0], co2_intensity[tech][1]], - marginal_cost=import_nodes_tech.marginal_cost.values, - p_nom_extendable=True, - capital_cost=capital_cost, - p_nom_min=import_nodes_tech.p_nom.clip(upper=upper_p_nom_max).values, - p_nom_max=import_nodes_tech.p_nom.mul(capacity_boost) - .clip(upper=upper_p_nom_max) - .values, - p_min_pu=import_config["min_part_load_shipping_imports"], - ) + import_buses = df.importer.unique() + " import " + tech + domestic_buses = df.importer.unique() + suffix + # pipeline imports require high minimum loading + if "pipeline" in tech: + p_min_pu = import_config["min_part_load_pipeline_imports"] + elif "shipping" in tech: + p_min_pu = import_config["min_part_load_shipping_imports"] else: - location = import_nodes_tech.index - buses = location if tech == "hvdc-to-elec" else location + suffix - - capital_cost = ( - 1.2 * 7018.0 if tech == "shipping-lh2" else 0.0 - ) # €/MW/a, +20% compared to LNG + p_min_pu = 0 - # pipeline imports require high minimum loading - p_min_pu = 0.9 if "pipeline" in tech else 0 - - n.madd( - "Generator", - import_nodes_tech.index + f"{suffix} import {tech}", - bus=buses, - carrier=f"import {tech}", - marginal_cost=import_nodes_tech.marginal_cost.values, - p_nom_extendable=True, - capital_cost=capital_cost, - p_min_pu=p_min_pu, - p_nom_max=import_nodes_tech.p_nom.mul(capacity_boost) - .clip(upper=upper_p_nom_max) - .values, - ) - - # need special handling for copperplated imports - copperplated_carbonaceous_options = { - "shipping-ftfuel", - "shipping-meoh", - } - - for tech in set(import_options).intersection(copperplated_carbonaceous_options): - marginal_costs = ( - import_costs.query("esc == @tech").marginal_cost.min() - * import_options[tech] + upper_p_nom_max = import_config["p_nom_max"].get(tech, np.inf) + import_nodes_p_nom = import_nodes_tech.loc[df.importer.unique()] + p_nom_max = import_nodes_p_nom.mul(capacity_boost).clip(upper=upper_p_nom_max).values + p_nom_min = ( + import_nodes_p_nom.clip(upper=upper_p_nom_max).values + if tech == "shipping-lch4" + else 0 ) - suffix = bus_suffix[tech] + bus2 = "co2 atmosphere" if tech in co2_intensity else "" + efficiency2 = ( + -costs.at[co2_intensity[tech][0], co2_intensity[tech][1]] + if tech in co2_intensity + else np.nan + ) - n.add("Bus", f"EU import {tech} bus", carrier=f"import {tech}") + n.madd("Bus", import_buses, carrier="import " + tech) - n.add( - "Store", - f"EU import {tech} store", - bus=f"EU import {tech} bus", - e_nom_extendable=True, - e_nom_min=-np.inf, - e_nom_max=0, - e_min_pu=1, - e_max_pu=0, + n.madd( + "Link", + pd.Index(df.exporter + " " + df.importer + " import " + tech), + bus0=df.exporter.values + " export", + bus1=df.importer.values + " import " + tech, + carrier="import " + tech, + marginal_cost=df.marginal_cost.values, + p_nom=import_config["exporter_energy_limit"] / 1e3, + # in one hour at most 0.1% of total annual energy ) - n.add( + n.madd( "Link", - f"EU import {tech}", - bus0=f"EU import {tech} bus", - bus1="EU" + suffix, - bus2="co2 atmosphere", - carrier=f"import {tech}", - efficiency2=-costs.at[co2_intensity[tech][0], co2_intensity[tech][1]], - marginal_cost=marginal_costs, - p_nom=1e7, - p_min_pu=import_config["min_part_load_shipping_imports"], + pd.Index(df.importer.unique() + " import infrastructure " + tech), + bus0=import_buses, + bus1=domestic_buses, + carrier="import infrastructure " + tech, + capital_cost=terminal_capital_cost.get(tech, 0), + p_nom_extendable=True, + p_nom_max=p_nom_max, + p_nom_min=p_nom_min, + p_min_pu=p_min_pu, + bus2=bus2, + efficiency2=efficiency2, ) - copperplated_carbonfree_options = { + # need special handling for copperplated imports + copperplated_options = { + # "shipping-ftfuel", + "shipping-meoh", "shipping-steel", - "shipping-HBI", + "shipping-hbi", "shipping-lnh3", } - for tech in set(import_options).intersection(copperplated_carbonfree_options): - suffix = bus_suffix[tech] - + for tech in set(import_options).intersection(copperplated_options): marginal_costs = ( - import_costs.query("esc == @tech").marginal_cost.min() - * import_options[tech] + import_costs.query("esc == @tech") + .groupby("exporter").marginal_cost.min() + .mul(import_options[tech]) ) - n.add( - "Generator", - f"EU import {tech}", - bus="EU" + suffix, - carrier=f"import {tech}", - marginal_cost=marginal_costs, - p_nom=1e7, + bus2 = "co2 atmosphere" if tech in co2_intensity else "" + efficiency2 = ( + -costs.at[co2_intensity[tech][0], co2_intensity[tech][1]] + if tech in co2_intensity + else np.nan + ) + + # using energy content of iron as proxy: 2.1 MWh/t + unit_to_mwh = 2.1 if tech in ["shipping-steel", "shipping-hbi"] else 1. + + suffix = bus_suffix[tech] + + n.madd( + "Link", + marginal_costs.index + " import " + tech, + bus0=marginal_costs.index + " export", + bus1="EU" + suffix, + carrier="import " + tech, + p_nom=import_config["exporter_energy_limit"] / 1e3, + # in one hour at most 0.1% of total annual energy + marginal_cost=marginal_costs.values / unit_to_mwh, + efficiency=1 / unit_to_mwh, p_min_pu=import_config["min_part_load_shipping_imports"], + bus2=bus2, + efficiency2=efficiency2, ) @@ -5263,45 +5253,48 @@ def add_enhanced_geothermal(n, egs_potentials, egs_overlap, costs): if options["co2network"]: add_co2_network(n, costs) - translate = dict( - H2=["pipeline-h2", "shipping-lh2"], - AC=["hvdc-to-elec"], - CH4=["shipping-lch4"], - NH3=["shipping-lnh3"], - FT=["shipping-ftfuel"], - MeOH=["shipping-meoh"], - St=["shipping-steel"], - HBI=["shipping-HBI"], - ) - for o in snakemake.wildcards.sector_opts.split("-"): - if not o.startswith("imp"): - continue - subsets = o.split("+")[1:] - if len(subsets): - - def parse_carriers(s): - prefixes = sorted(translate.keys(), key=lambda k: len(k), reverse=True) - pattern = rf'({"|".join(prefixes)})(\d+(\.\d+)?)?' - match = re.search(pattern, s) - prefix = match.group(1) if match else None - number = float(match.group(2)) if match and match.group(2) else 1.0 - return {prefix: number} - - carriers = { - tk: v - for s in subsets - for k, v in parse_carriers(s).items() - for tk in translate.get(k, []) - } - else: - carriers = options["import"]["options"] - add_import_options( - n, - capacity_boost=options["import"]["capacity_boost"], - import_options=carriers, - endogenous_hvdc=options["import"]["endogenous_hvdc_import"]["enable"], - ) - break + def wildcard_to_import_options(sector_opts): + + translate = dict( + H2=["pipeline-h2", "shipping-lh2"], + AC=["hvdc-to-elec"], + CH4=["shipping-lch4"], + NH3=["shipping-lnh3"], + FT=["shipping-ftfuel"], + MeOH=["shipping-meoh"], + St=["shipping-steel"], + HBI=["shipping-hbi"], + ) + + def parse_carriers(s): + prefixes = sorted(translate.keys(), key=len, reverse=True) + pattern = rf'({"|".join(prefixes)})(\d+(\.\d+)?)?' + match = re.search(pattern, s) + prefix = match.group(1) if match else None + number = float(match.group(2)) if match and match.group(2) else 1.0 + return {prefix: number} + + for o in sector_opts.split("-"): + if o.startswith("imp"): + subsets = o.split("+")[1:] + if len(subsets): + return { + tk: v + for s in subsets + for k, v in parse_carriers(s).items() + for tk in translate.get(k, []) + } + else: + return options["import"]["options"] + + import_options = wildcard_to_import_options(snakemake.wildcards.sector_opts) + + add_import_options( + n, + capacity_boost=options["import"]["capacity_boost"], + import_options=import_options, + endogenous_hvdc=options["import"]["endogenous_hvdc_import"]["enable"], + ) if options["allam_cycle_gas"]: add_allam_gas(n, costs) diff --git a/scripts/solve_network.py b/scripts/solve_network.py index 26bf5686d..66275f907 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -890,8 +890,10 @@ def add_pipe_retrofit_constraint(n): def add_energy_import_limit(n, sns): - import_gens = n.generators.loc[n.generators.carrier.str.contains("import")].index - import_links = n.links.loc[n.links.carrier.str.contains("import")].index + import_links = n.links.loc[ + n.links.carrier.str.contains("import") + & n.links.carrier.str.contains("import infrastructure") + ].index limit = n.config["sector"].get("import", {}).get("limit", False) limit_sense = n.config["sector"].get("import", {}).get("limit_sense", "<=") @@ -903,19 +905,14 @@ def add_energy_import_limit(n, sns): limit = float(match) break - if (import_gens.empty and import_links.empty) or not limit: + if import_links.empty or not limit: return weightings = n.snapshot_weightings.loc[sns, "generators"] - p_gens = n.model["Generator-p"].loc[sns, import_gens] p_links = n.model["Link-p"].loc[sns, import_links] - # using energy content of iron as proxy: 2.1 MWh/t - energy_weightings = np.where(import_gens.str.contains("(steel|HBI)"), 2.1, 1.0) - energy_weightings = pd.Series(energy_weightings, index=import_gens) - - lhs = (p_gens * weightings * energy_weightings).sum() + (p_links * weightings).sum() + lhs = (p_links * weightings).sum() rhs = limit * 1e6 From 9fde68973e4c8ebc8c9cb2a66d21028373ee23ba Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 18 Sep 2024 15:44:28 +0000 Subject: [PATCH 266/293] [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, 22 insertions(+), 13 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 5a114a4ef..48575b0d6 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -4543,7 +4543,7 @@ def add_import_options( import_nodes["hvdc-to-elec"] = 15000 import_config = snakemake.params["sector"]["import"] - cost_year = snakemake.params["costs"]["year"] # noqa: F841 + cost_year = snakemake.params["costs"]["year"] # noqa: F841 ports = pd.read_csv(snakemake.input.ports, index_col=0) @@ -4574,16 +4574,18 @@ def add_import_options( } terminal_capital_cost = { - "shipping-lch4": 7018, # €/MW/a - "shipping-lh2": 7018 * 1.2, #+20% compared to LNG + "shipping-lch4": 7018, # €/MW/a + "shipping-lh2": 7018 * 1.2, # +20% compared to LNG } - import_costs = pd.read_parquet( - snakemake.input.import_costs - ).reset_index().query("year == @cost_year and scenario == 'default'") + import_costs = ( + pd.read_parquet(snakemake.input.import_costs) + .reset_index() + .query("year == @cost_year and scenario == 'default'") + ) cols = ["esc", "exporter", "importer", "value"] - fields = ["Cost per MWh delivered", "Cost per t delivered"] # noqa: F841 + fields = ["Cost per MWh delivered", "Cost per t delivered"] # noqa: F841 import_costs = import_costs.query("subcategory in @fields")[cols] import_costs.rename(columns={"value": "marginal_cost"}, inplace=True) @@ -4594,7 +4596,9 @@ def add_import_options( if endogenous_hvdc and "hvdc-to-elec" in import_options: add_endogenous_hvdc_import_options(n, import_options.pop("hvdc-to-elec")) - export_buses = import_costs.query("esc in @import_options").exporter.unique() + " export" + export_buses = ( + import_costs.query("esc in @import_options").exporter.unique() + " export" + ) n.madd("Bus", export_buses, carrier="export") n.madd( "Store", @@ -4616,8 +4620,10 @@ def add_import_options( import_nodes_tech = import_nodes[tech].dropna() forbidden_importers = [] if "pipeline" in tech: - forbidden_importers.extend(["DE", "BE", "FR", "GB"]) # internal entrypoints - forbidden_importers.extend(["EE", "LT", "LV", "FI"]) # entrypoints via RU_BY + forbidden_importers.extend(["DE", "BE", "FR", "GB"]) # internal entrypoints + forbidden_importers.extend( + ["EE", "LT", "LV", "FI"] + ) # entrypoints via RU_BY sel = ~import_nodes_tech.index.str.contains("|".join(forbidden_importers)) import_nodes_tech = import_nodes_tech.loc[sel] @@ -4651,7 +4657,9 @@ def add_import_options( upper_p_nom_max = import_config["p_nom_max"].get(tech, np.inf) import_nodes_p_nom = import_nodes_tech.loc[df.importer.unique()] - p_nom_max = import_nodes_p_nom.mul(capacity_boost).clip(upper=upper_p_nom_max).values + p_nom_max = ( + import_nodes_p_nom.mul(capacity_boost).clip(upper=upper_p_nom_max).values + ) p_nom_min = ( import_nodes_p_nom.clip(upper=upper_p_nom_max).values if tech == "shipping-lch4" @@ -4705,7 +4713,8 @@ def add_import_options( for tech in set(import_options).intersection(copperplated_options): marginal_costs = ( import_costs.query("esc == @tech") - .groupby("exporter").marginal_cost.min() + .groupby("exporter") + .marginal_cost.min() .mul(import_options[tech]) ) @@ -4717,7 +4726,7 @@ def add_import_options( ) # using energy content of iron as proxy: 2.1 MWh/t - unit_to_mwh = 2.1 if tech in ["shipping-steel", "shipping-hbi"] else 1. + unit_to_mwh = 2.1 if tech in ["shipping-steel", "shipping-hbi"] else 1.0 suffix = bus_suffix[tech] From d859495ad892604829bc1e0aa4162f17cef9d415 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 18 Sep 2024 17:46:19 +0200 Subject: [PATCH 267/293] config: add hbi and steel imports to default --- config/config.default.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/config.default.yaml b/config/config.default.yaml index 67c825ea5..fb4f172a4 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -714,6 +714,8 @@ sector: - shipping-lnh3 - shipping-ftfuel - shipping-meoh + - shipping-hbi + - shipping-steel - hvdc-to-elec p_nom_max: pipeline-h2: .inf From 133aad665ce2f1488db4f7997ac79656a5bc5209 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 18 Sep 2024 18:15:42 +0200 Subject: [PATCH 268/293] add filter for allowed exporters --- config/config.default.yaml | 98 +++++++++++++++++++++++++++++++ scripts/prepare_sector_network.py | 3 +- 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index fb4f172a4..2255e4a16 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -717,6 +717,70 @@ sector: - shipping-hbi - shipping-steel - hvdc-to-elec + exporters: + - AR-South + - AR-North + - AU-West + - AU-East + - BR-Northeast + - BR-Southeast + - CA-East + - CL + - CN-Northeast + - CN-Southeast + - CN-West + - DZ + - EG + - KZ + - LY + - MA + - EH + - NA + - OM + - SA + - TN + - TR + - UA + - US-Northeast + - US-Southeast + - US-Northwest + - US-South + - US-Southwest + - US-Alaska + - MX + - TM + - UZ + - MN + - PK + - VN + - MM + - TH + - ZA + - KE + - TZ + - NG + - AO + - ET + - MZ + - ER + - MR + - SN + - UY + - CO + - PE + - IN-Northwest + - IN-South + - IN-East + - SD + - SY + - YE + - IR + - IQ + - AF + - SO + - MG + - BO + - VE p_nom_max: pipeline-h2: .inf min_part_load_shipping_imports: 0 @@ -760,6 +824,40 @@ sector: - x: -8.70 y: 42.31 length: 1800 + MR: + # Plymouth + - x: -4.14 + y: 50.39 + length: 5300 + # Cork + - x: -8.52 + y: 51.92 + length: 5500 + # Brest + - x: -4.49 + y: 48.43 + length: 4700 + # Vigo + - x: -8.70 + y: 42.31 + length: 3300 + EH: + # Plymouth + - x: -4.14 + y: 50.39 + length: 4800 + # Cork + - x: -8.52 + y: 51.92 + length: 5000 + # Brest + - x: -4.49 + y: 48.43 + length: 4200 + # Vigo + - x: -8.70 + y: 42.31 + length: 2800 DZ: # Barcelona - x: 2.37 diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 48575b0d6..73b36e20c 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -4544,6 +4544,7 @@ def add_import_options( import_config = snakemake.params["sector"]["import"] cost_year = snakemake.params["costs"]["year"] # noqa: F841 + exporters = import_config["exporters"] # noqa: F841 ports = pd.read_csv(snakemake.input.ports, index_col=0) @@ -4581,7 +4582,7 @@ def add_import_options( import_costs = ( pd.read_parquet(snakemake.input.import_costs) .reset_index() - .query("year == @cost_year and scenario == 'default'") + .query("year == @cost_year and scenario == 'default' and exporter in @exporters") ) cols = ["esc", "exporter", "importer", "value"] From 6ba2b18049344a620419b9553a403cc56dd35e16 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 18 Sep 2024 16:17:56 +0000 Subject: [PATCH 269/293] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- 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 73b36e20c..cd2c98488 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -4582,7 +4582,9 @@ def add_import_options( import_costs = ( pd.read_parquet(snakemake.input.import_costs) .reset_index() - .query("year == @cost_year and scenario == 'default' and exporter in @exporters") + .query( + "year == @cost_year and scenario == 'default' and exporter in @exporters" + ) ) cols = ["esc", "exporter", "importer", "value"] From 89bf39ca39a6a1cc6e6e1621848ebfe9b5f01abe Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 19 Sep 2024 16:44:39 +0200 Subject: [PATCH 270/293] time_aggregation: tidy logger --- scripts/time_aggregation.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/time_aggregation.py b/scripts/time_aggregation.py index 51ab79545..d057bb733 100644 --- a/scripts/time_aggregation.py +++ b/scripts/time_aggregation.py @@ -144,8 +144,7 @@ weightings, axis=0 ) - logger.info( - f"Distribution of snapshot durations:\n{snapshot_weightings.objective.value_counts()}" - ) + sns_distribution = snapshot_weightings.objective.value_counts().sort_index() + logger.info(f"Distribution of snapshot durations:\n{sns_distribution}") snapshot_weightings.to_csv(snakemake.output.snapshot_weightings) From 6f148bea967896178ba8aa2cffbde16497b3ee90 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 19 Sep 2024 16:45:04 +0200 Subject: [PATCH 271/293] solve_overnight: fix resource trigger by attempt --- rules/solve_overnight.smk | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rules/solve_overnight.smk b/rules/solve_overnight.smk index 622ca065f..310bd2ae1 100644 --- a/rules/solve_overnight.smk +++ b/rules/solve_overnight.smk @@ -32,8 +32,8 @@ rule solve_sector_network: + "logs/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}_python.log", threads: solver_threads resources: - mem_mb=lambda wildcards, attempt: config_provider("solving", "mem_mb") - + config_provider("solving", "mem_increment", defauult=32000) * (attempt - 1), + mem_mb=lambda w, attempt: config_provider("solving", "mem_mb")(w) + + config_provider("solving", "mem_increment", default=32000)(w) * (attempt - 1), runtime=config_provider("solving", "runtime", default="6h"), benchmark: ( From 1f5543b8ca24ce2420adf2203de25b8f0a072de7 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 19 Sep 2024 16:46:00 +0200 Subject: [PATCH 272/293] prepare_sector: adjust endogenous_hvdc_import --- scripts/prepare_sector_network.py | 78 ++++++++++++++++--------------- 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index cd2c98488..ee4a5a920 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -10,6 +10,7 @@ import logging import os import re +import warnings from itertools import product from types import SimpleNamespace @@ -4304,22 +4305,15 @@ def add_endogenous_hvdc_import_options(n, cost_factor=1.0): regions = gpd.read_file(snakemake.input.regions_onshore).set_index("name") - p_max_pu = xr.open_dataset(snakemake.input.import_p_max_pu).p_max_pu.sel( - importer="EUE" - ) + p_max_pu = xr.open_dataset(snakemake.input.import_p_max_pu).p_max_pu + p_max_pu = p_max_pu.isel(importer=p_max_pu.notnull().argmax('importer')) - p_nom_max = ( - xr.open_dataset(snakemake.input.import_p_max_pu) - .p_nom_max.sel(importer="EUE") - .to_pandas() - ) + p_nom_max = xr.open_dataset(snakemake.input.import_p_max_pu).p_nom_max + p_nom_max = p_nom_max.isel(importer=p_nom_max.notnull().argmax('importer')).to_pandas() exporters_iso2 = [e.split("-")[0] for e in cf["exporters"]] # noqa - exporters = ( - country_shapes.query("ISO_A2 in @exporters_iso2") - .set_index("ISO_A2") - .representative_point() - ) + exporters = country_shapes.set_index("ISO_A2").loc[exporters_iso2].representative_point() + exporters.index = cf["exporters"] import_links = {} a = regions.representative_point().to_crs(DISTANCE_CRS) @@ -4337,21 +4331,23 @@ def add_endogenous_hvdc_import_options(n, cost_factor=1.0): import_links = pd.concat(import_links) import_links.loc[ import_links.index.get_level_values(0).str.contains("KZ|CN|MN|UZ") - ] *= 1.2 # proxy for detour through Caucasus - - # xlinks - xlinks = {} - for bus0, links in cf["xlinks"].items(): - for link in links: - landing_point = gpd.GeoSeries( - [Point(link["x"], link["y"])], crs=4326 - ).to_crs(DISTANCE_CRS) - bus1 = ( - regions.to_crs(DISTANCE_CRS) - .geometry.distance(landing_point[0]) - .idxmin() - ) - xlinks[(bus0, bus1)] = link["length"] + ] *= 1.2 # proxy for detour through Caucasus in addition to crow-fly distance factor + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + # xlinks + xlinks = {} + for bus0, links in cf["xlinks"].items(): + for link in links: + landing_point = gpd.GeoSeries( + [Point(link["x"], link["y"])], crs=4326 + ).to_crs(DISTANCE_CRS) + bus1 = ( + regions.to_crs(DISTANCE_CRS) + .geometry.distance(landing_point[0]) + .idxmin() + ) + xlinks[(bus0, bus1)] = link["length"] import_links = pd.concat([import_links, pd.Series(xlinks)], axis=0) import_links = import_links.drop_duplicates(keep="first") @@ -4377,12 +4373,13 @@ def add_endogenous_hvdc_import_options(n, cost_factor=1.0): bus0=import_links.index.get_level_values(0), bus1=import_links.index.get_level_values(1), carrier="import hvdc-to-elec", - p_min_pu=0, p_nom_extendable=True, length=import_links.values, capital_cost=hvdc_cost * cost_factor, efficiency=efficiency, p_nom_max=cf["p_nom_max"], + bus2=import_links.index.get_level_values(0) + " export", + efficiency2=-1, ) for tech in ["solar-utility", "onwind"]: @@ -4397,12 +4394,12 @@ def add_endogenous_hvdc_import_options(n, cost_factor=1.0): exporters_tech_i, suffix=" " + tech, bus=exporters_tech_i, - carrier=f"external {tech}", + carrier="external " + tech, p_nom_extendable=True, capital_cost=(costs.at[tech, "fixed"] + grid_connection) * cost_factor, lifetime=costs.at[tech, "lifetime"], - p_max_pu=p_max_pu_tech.reindex(columns=exporters_tech_i), - p_nom_max=p_nom_max[tech].reindex(index=exporters_tech_i) + p_max_pu=p_max_pu_tech.reindex(columns=exporters_tech_i).values, + p_nom_max=p_nom_max[tech].reindex(index=exporters_tech_i).values * cf["share_of_p_nom_max_available"], ) @@ -4534,7 +4531,9 @@ def add_import_options( ], endogenous_hvdc=False, ): - if not isinstance(import_options, dict): + if not import_options: + return + elif not isinstance(import_options, dict): import_options = {k: 1.0 for k in import_options} logger.info("Add import options: " + " ".join(import_options.keys())) @@ -4596,9 +4595,6 @@ def add_import_options( import_nodes[k] = import_nodes[v] ports[k] = ports.get(v) - if endogenous_hvdc and "hvdc-to-elec" in import_options: - add_endogenous_hvdc_import_options(n, import_options.pop("hvdc-to-elec")) - export_buses = ( import_costs.query("esc in @import_options").exporter.unique() + " export" ) @@ -4611,6 +4607,10 @@ def add_import_options( e_nom_initial=import_config["exporter_energy_limit"], ) + if endogenous_hvdc and "hvdc-to-elec" in import_options: + cost_factor = import_options.pop("hvdc-to-elec") + add_endogenous_hvdc_import_options(n, cost_factor) + regionalised_options = { "hvdc-to-elec", "pipeline-h2", @@ -5300,12 +5300,14 @@ def parse_carriers(s): return options["import"]["options"] import_options = wildcard_to_import_options(snakemake.wildcards.sector_opts) + capacity_boost = options["import"]["capacity_boost"] + endogenous_hvdc = options["import"]["endogenous_hvdc_import"]["enable"] add_import_options( n, - capacity_boost=options["import"]["capacity_boost"], + capacity_boost=capacity_boost, import_options=import_options, - endogenous_hvdc=options["import"]["endogenous_hvdc_import"]["enable"], + endogenous_hvdc=endogenous_hvdc, ) if options["allam_cycle_gas"]: From 411a24c5dd9b98645c06a199ec9bc4a4810b279d Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 19 Sep 2024 16:46:27 +0200 Subject: [PATCH 273/293] solve_network: fix energy import limit --- scripts/solve_network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/solve_network.py b/scripts/solve_network.py index 66275f907..32ccf917d 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -897,7 +897,7 @@ def add_energy_import_limit(n, sns): limit = n.config["sector"].get("import", {}).get("limit", False) limit_sense = n.config["sector"].get("import", {}).get("limit_sense", "<=") - for o in n.opts: + for o in n.meta["wildcards"]["sector_opts"]: if not o.startswith("imp"): continue match = o.split("+")[0][3:] From 079e41366cd8c02d9e9ae82f3c66345f35e2ef7d Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 19 Sep 2024 16:46:46 +0200 Subject: [PATCH 274/293] solve_network: fail on warning status --- scripts/solve_network.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/solve_network.py b/scripts/solve_network.py index 32ccf917d..9ad3d01fd 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -1069,6 +1069,8 @@ def solve_network(n, config, params, solving, **kwargs): logger.warning( f"Solving status '{status}' with termination condition '{condition}'" ) + if "warning" in status: + raise RuntimeError("Solving status 'warning'. Discarding solution.") if "infeasible" in condition: labels = n.model.compute_infeasibilities() logger.info(f"Labels:\n{labels}") From 30749d43f456836c1ab6d9292db9741b1ba411a4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 19 Sep 2024 14:47:08 +0000 Subject: [PATCH 275/293] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/prepare_sector_network.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index ee4a5a920..725f174cb 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -4306,13 +4306,17 @@ def add_endogenous_hvdc_import_options(n, cost_factor=1.0): regions = gpd.read_file(snakemake.input.regions_onshore).set_index("name") p_max_pu = xr.open_dataset(snakemake.input.import_p_max_pu).p_max_pu - p_max_pu = p_max_pu.isel(importer=p_max_pu.notnull().argmax('importer')) + p_max_pu = p_max_pu.isel(importer=p_max_pu.notnull().argmax("importer")) p_nom_max = xr.open_dataset(snakemake.input.import_p_max_pu).p_nom_max - p_nom_max = p_nom_max.isel(importer=p_nom_max.notnull().argmax('importer')).to_pandas() + p_nom_max = p_nom_max.isel( + importer=p_nom_max.notnull().argmax("importer") + ).to_pandas() exporters_iso2 = [e.split("-")[0] for e in cf["exporters"]] # noqa - exporters = country_shapes.set_index("ISO_A2").loc[exporters_iso2].representative_point() + exporters = ( + country_shapes.set_index("ISO_A2").loc[exporters_iso2].representative_point() + ) exporters.index = cf["exporters"] import_links = {} @@ -4331,7 +4335,9 @@ def add_endogenous_hvdc_import_options(n, cost_factor=1.0): import_links = pd.concat(import_links) import_links.loc[ import_links.index.get_level_values(0).str.contains("KZ|CN|MN|UZ") - ] *= 1.2 # proxy for detour through Caucasus in addition to crow-fly distance factor + ] *= ( + 1.2 # proxy for detour through Caucasus in addition to crow-fly distance factor + ) with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=DeprecationWarning) From 41c03894eedefea613b1473515b3b40e5bc6ef90 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 19 Sep 2024 16:55:33 +0200 Subject: [PATCH 276/293] solve_network: drop double memory logger --- scripts/solve_network.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/scripts/solve_network.py b/scripts/solve_network.py index 9ad3d01fd..0f642649a 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -1102,18 +1102,16 @@ def solve_network(n, config, params, solving, **kwargs): np.random.seed(solve_opts.get("seed", 123)) - fn = getattr(snakemake.log, "memory", None) - with memory_logger(filename=fn, interval=10.0) as mem: - n = pypsa.Network(snakemake.input.network) - - n = prepare_network( - n, - solve_opts, - config=snakemake.config, - foresight=snakemake.params.foresight, - planning_horizons=snakemake.params.planning_horizons, - co2_sequestration_potential=snakemake.params["co2_sequestration_potential"], - ) + n = pypsa.Network(snakemake.input.network) + + n = prepare_network( + n, + solve_opts, + config=snakemake.config, + foresight=snakemake.params.foresight, + planning_horizons=snakemake.params.planning_horizons, + co2_sequestration_potential=snakemake.params["co2_sequestration_potential"], + ) with memory_logger( filename=getattr(snakemake.log, "memory", None), interval=30.0 From 95dafdab921e831ca76b41aa96a3fe8e8c98c0ff Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 20 Sep 2024 09:46:28 +0200 Subject: [PATCH 277/293] prepare_sector: fix add_ammonia by using industrial demand distribution and p_min_pu for no_relocation --- scripts/prepare_sector_network.py | 35 +++++++++++++++---------------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 725f174cb..6ad49e49e 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1159,8 +1159,9 @@ def add_ammonia(n, costs): logger.info("Adding ammonia carrier with synthesis, cracking and storage") nodes = pop_layout.index + nhours = n.snapshot_weightings.generators.sum() - p_nom = n.loads.loc[spatial.ammonia.nodes, "p_set"] + p_nom = industrial_demand["ammonia"].groupby(level="node").sum().div(nhours) no_relocation = not options["relocation_ammonia"] @@ -1182,8 +1183,7 @@ def add_ammonia(n, costs): bus2=nodes + " H2", p_nom=p_nom if no_relocation else 0, p_nom_extendable=False if no_relocation else True, - p_max_pu=1 if no_relocation else 0, # so that no imports can substitute - p_min_pu=options["min_part_load_haber_bosch"], + p_min_pu=1 if no_relocation else options["min_part_load_haber_bosch"], carrier="Haber-Bosch", efficiency=1 / costs.at["Haber-Bosch", "electricity-input"], efficiency2=-costs.at["Haber-Bosch", "hydrogen-input"] @@ -3198,21 +3198,6 @@ def add_industry(n, costs): nodes = pop_layout.index nhours = n.snapshot_weightings.generators.sum() - nyears = nhours / 8760 - - # 1e6 to convert TWh to MWh - industrial_demand = ( - pd.read_csv(snakemake.input.industrial_demand, index_col=[0, 1]) * 1e6 * nyears - ) - industrial_demand_today = ( - pd.read_csv(snakemake.input.industrial_demand_today, index_col=0) * 1e6 * nyears - ) - - industrial_production = ( - pd.read_csv(snakemake.input.industrial_production, index_col=0) - * 1e3 - * nyears # kt/a -> t/a - ) endogenous_sectors = [] if options["endogenous_steel"]: @@ -5191,6 +5176,20 @@ def add_enhanced_geothermal(n, egs_potentials, egs_overlap, costs): nhours = n.snapshot_weightings.generators.sum() nyears = nhours / 8760 + # 1e6 to convert TWh to MWh + industrial_demand = ( + pd.read_csv(snakemake.input.industrial_demand, index_col=[0, 1]) * 1e6 * nyears + ) + industrial_demand_today = ( + pd.read_csv(snakemake.input.industrial_demand_today, index_col=0) * 1e6 * nyears + ) + + industrial_production = ( + pd.read_csv(snakemake.input.industrial_production, index_col=0) + * 1e3 + * nyears # kt/a -> t/a + ) + costs = prepare_costs( snakemake.input.costs, snakemake.params.costs, From 9a26d244406151ca397460bf69c140d036a16cc2 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 20 Sep 2024 10:05:20 +0200 Subject: [PATCH 278/293] prepare_sector: do not force production HB, DRI, EAF --- scripts/prepare_sector_network.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 6ad49e49e..c23bde5f4 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1183,7 +1183,7 @@ def add_ammonia(n, costs): bus2=nodes + " H2", p_nom=p_nom if no_relocation else 0, p_nom_extendable=False if no_relocation else True, - p_min_pu=1 if no_relocation else options["min_part_load_haber_bosch"], + p_min_pu=options["min_part_load_haber_bosch"], carrier="Haber-Bosch", efficiency=1 / costs.at["Haber-Bosch", "electricity-input"], efficiency2=-costs.at["Haber-Bosch", "hydrogen-input"] @@ -3289,7 +3289,6 @@ def add_industry(n, costs): marginal_cost=marginal_cost, p_nom=p_nom if no_relocation else 0, p_nom_extendable=False if no_relocation else True, - p_min_pu=1 if no_relocation else 0, # so that no imports can substitute bus0=nodes, bus1="EU HBI", bus2=nodes + " H2", @@ -3309,7 +3308,6 @@ def add_industry(n, costs): capital_cost=costs.at["electric arc furnace", "fixed"] / electricity_input, p_nom=p_nom if no_relocation else 0, p_nom_extendable=False if no_relocation else True, - p_min_pu=1 if no_relocation else 0, # so that no imports can substitute bus0=nodes, bus1="EU steel", bus2="EU HBI", From 76e3885caf42ca172c5d6e9f5594d7e2bf15c177 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 20 Sep 2024 10:05:38 +0200 Subject: [PATCH 279/293] prepare_sector: correct unit of HB capacities --- scripts/prepare_sector_network.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index c23bde5f4..b362aab49 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1161,7 +1161,10 @@ def add_ammonia(n, costs): nodes = pop_layout.index nhours = n.snapshot_weightings.generators.sum() - p_nom = industrial_demand["ammonia"].groupby(level="node").sum().div(nhours) + p_nom = ( + industrial_demand["ammonia"].groupby(level="node").sum().div(nhours) + / costs.at["Haber-Bosch", "electricity-input"] + ) no_relocation = not options["relocation_ammonia"] From 5872aa335a206bf39c981fbebdd5a398e9af99b9 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 20 Sep 2024 14:47:31 +0200 Subject: [PATCH 280/293] heat vent with noPtXflex --- scripts/_helpers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/_helpers.py b/scripts/_helpers.py index f699aa73e..3792b8839 100644 --- a/scripts/_helpers.py +++ b/scripts/_helpers.py @@ -709,6 +709,7 @@ def update_config_from_wildcards(config, w, inplace=True): config["sector"]["min_part_load_methanation"] = 1 config["sector"]["min_part_load_electrolysis"] = 1 config["sector"]["min_part_load_haber_bosch"] = 1 + config["sector"]["central_heat_vent"] = True if "noshipflex" in opts: logger.info("Disabling shipping import flexibility.") From 20d35f187f96ac1e101e0a1b31784f849c4f8633 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 20 Sep 2024 14:47:57 +0200 Subject: [PATCH 281/293] export budget: correctly set e_initial --- 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 b362aab49..9019a03fc 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -4596,7 +4596,7 @@ def add_import_options( export_buses + " budget", bus=export_buses, e_nom=import_config["exporter_energy_limit"], - e_nom_initial=import_config["exporter_energy_limit"], + e_initial=import_config["exporter_energy_limit"], ) if endogenous_hvdc and "hvdc-to-elec" in import_options: From 7de31312f95f772aa9e5d4c1b2a6f619b7cd29fc Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 20 Sep 2024 14:48:12 +0200 Subject: [PATCH 282/293] ensure shipping-ftfuel can be added --- 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 9019a03fc..d8497dd6b 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -4698,7 +4698,7 @@ def add_import_options( # need special handling for copperplated imports copperplated_options = { - # "shipping-ftfuel", + "shipping-ftfuel", "shipping-meoh", "shipping-steel", "shipping-hbi", From 61a961dbe2d6247ef927c573f2f24cd1fe653312 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 20 Sep 2024 15:23:43 +0200 Subject: [PATCH 283/293] allow shipping-lch4 imports without gas_network --- scripts/prepare_sector_network.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index d8497dd6b..c2cc5e932 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -4640,7 +4640,10 @@ def add_import_options( suffix = bus_suffix[tech] import_buses = df.importer.unique() + " import " + tech - domestic_buses = df.importer.unique() + suffix + if tech == "shipping-lch4" and len(spatial.gas.nodes) == 1: + domestic_buses = "EU gas" + else: + domestic_buses = df.importer.unique() + suffix # pipeline imports require high minimum loading if "pipeline" in tech: From 0dd49f7e2b4735e71e10d5b8348eac6b3a340fc5 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 20 Sep 2024 15:24:09 +0200 Subject: [PATCH 284/293] add 'allwasteheat' sensitivity option --- scripts/_helpers.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/_helpers.py b/scripts/_helpers.py index 3792b8839..1189efebb 100644 --- a/scripts/_helpers.py +++ b/scripts/_helpers.py @@ -692,6 +692,14 @@ def update_config_from_wildcards(config, w, inplace=True): config["sector"]["use_fuel_cell_waste_heat"] = False config["sector"]["use_electrolysis_waste_heat"] = False + if "allwasteheat" in opts: + config["sector"]["use_fischer_tropsch_waste_heat"] = True + config["sector"]["use_methanolisation_waste_heat"] = True + config["sector"]["use_haber_bosch_waste_heat"] = True + config["sector"]["use_methanation_waste_heat"] = True + config["sector"]["use_fuel_cell_waste_heat"] = True + config["sector"]["use_electrolysis_waste_heat"] = True + if "nodistrict" in opts: config["sector"]["district_heating"]["progress"] = 0.0 From 16c79d7ea019a37d4b8751e4d1cb8b3662c77824 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 20 Sep 2024 15:24:37 +0200 Subject: [PATCH 285/293] solve_network: fix energy import limit --- scripts/solve_network.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/solve_network.py b/scripts/solve_network.py index 0f642649a..d8a2839fe 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -892,12 +892,12 @@ def add_pipe_retrofit_constraint(n): def add_energy_import_limit(n, sns): import_links = n.links.loc[ n.links.carrier.str.contains("import") - & n.links.carrier.str.contains("import infrastructure") + & ~n.links.carrier.str.contains("import infrastructure") ].index limit = n.config["sector"].get("import", {}).get("limit", False) limit_sense = n.config["sector"].get("import", {}).get("limit_sense", "<=") - for o in n.meta["wildcards"]["sector_opts"]: + for o in n.meta["wildcards"]["sector_opts"].split("-"): if not o.startswith("imp"): continue match = o.split("+")[0][3:] From a8f2426b78f2338bdf9a97f27206850edfea8342 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 20 Sep 2024 15:25:34 +0200 Subject: [PATCH 286/293] hvdc-to-elec only take *efficiency from export budget --- 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 c2cc5e932..f5f2c5342 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -4371,7 +4371,7 @@ def add_endogenous_hvdc_import_options(n, cost_factor=1.0): efficiency=efficiency, p_nom_max=cf["p_nom_max"], bus2=import_links.index.get_level_values(0) + " export", - efficiency2=-1, + efficiency2=-efficiency, ) for tech in ["solar-utility", "onwind"]: From eb571a6fde0e7aa26ed2678d3f1ac748a1dbe93c Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Sun, 22 Sep 2024 10:40:16 +0200 Subject: [PATCH 287/293] bugfix: account for kerosene emissions in methanol-to-kerosene link --- scripts/prepare_sector_network.py | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index f5f2c5342..bcf9162d8 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -937,25 +937,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( @@ -967,12 +952,13 @@ 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 a91854822b5e71a4f3e07d677c543c8c83710738 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 22 Sep 2024 08:40:42 +0000 Subject: [PATCH 288/293] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- 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 bcf9162d8..308a0ab2d 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -956,8 +956,7 @@ def add_methanol_to_kerosene(n, costs): 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"], + efficiency3=costs.at["oil", "CO2 intensity"] / costs.at[tech, "methanol-input"], p_nom_extendable=True, ) From 5b8d9b69523b8b13ec88b5fa12e6e6bbcec5eaaa Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 23 Sep 2024 14:04:34 +0200 Subject: [PATCH 289/293] MtK: fix methanol input efficiency --- 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 bcf9162d8..dfc5fbaee 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -953,7 +953,7 @@ def add_methanol_to_kerosene(n, costs): 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"] From 708582605d2706fb74af346375b19efcd578205a Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 23 Sep 2024 14:04:59 +0200 Subject: [PATCH 290/293] MtK: add VOM --- scripts/add_electricity.py | 6 +++++- scripts/prepare_sector_network.py | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) 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 dfc5fbaee..53e38bbde 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -949,6 +949,7 @@ 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, From 60d6df54a531e5890109203ef16aecbf16b54c38 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 23 Sep 2024 16:27:57 +0200 Subject: [PATCH 291/293] .sync-send: add data dir --- .sync-send | 1 + 1 file changed, 1 insertion(+) diff --git a/.sync-send b/.sync-send index 483c7a999..62633d2e2 100644 --- a/.sync-send +++ b/.sync-send @@ -9,3 +9,4 @@ config/test envs matplotlibrc Snakefile +data \ No newline at end of file From 87a2fcf3d704ed24225eb07c6c32788a4ce14d5e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 24 Sep 2024 00:36:45 +0000 Subject: [PATCH 292/293] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .sync-send | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.sync-send b/.sync-send index 62633d2e2..c4618ed02 100644 --- a/.sync-send +++ b/.sync-send @@ -9,4 +9,4 @@ config/test envs matplotlibrc Snakefile -data \ No newline at end of file +data From 80627ced59de222dbb9a58ed5382f8046a758950 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 14 Nov 2024 21:21:59 +0100 Subject: [PATCH 293/293] final for revision 1 --- config/config.20240826-z1.yaml | 667 +++++++++++++++++++ config/config.default.yaml | 123 +++- matplotlibrc | 1 + rules/build_sector.smk | 8 +- rules/plot.smk | 106 ++- scripts/plot_backup_power_map.py | 153 +++++ scripts/plot_balance_timeseries.py | 17 +- scripts/plot_dh_share.py | 63 ++ scripts/plot_fixed_demand_totals.py | 120 ++++ scripts/plot_gas_network_unclustered.py | 10 +- scripts/plot_heatmap_timeseries.py | 8 +- scripts/plot_heatmap_timeseries_resources.py | 8 +- scripts/plot_import_options.py | 111 +-- scripts/plot_import_sankey.py | 171 +++++ scripts/plot_import_shares.py | 34 +- scripts/plot_import_supply_curve.py | 117 ++++ scripts/plot_import_world_map.py | 181 +++-- scripts/plot_import_world_map_hydrogen.py | 182 +++++ scripts/plot_industrial_sites.py | 4 +- scripts/plot_power_network_unclustered.py | 39 +- scripts/plot_salt_caverns_clustered.py | 4 +- scripts/plot_salt_caverns_unclustered.py | 2 +- scripts/plot_summary.py | 2 +- 23 files changed, 1930 insertions(+), 201 deletions(-) create mode 100644 config/config.20240826-z1.yaml create mode 100644 scripts/plot_backup_power_map.py create mode 100644 scripts/plot_dh_share.py create mode 100644 scripts/plot_fixed_demand_totals.py create mode 100644 scripts/plot_import_sankey.py create mode 100644 scripts/plot_import_supply_curve.py create mode 100644 scripts/plot_import_world_map_hydrogen.py diff --git a/config/config.20240826-z1.yaml b/config/config.20240826-z1.yaml new file mode 100644 index 000000000..33e50bbaa --- /dev/null +++ b/config/config.20240826-z1.yaml @@ -0,0 +1,667 @@ +# SPDX-FileCopyrightText: : 2017-2023 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: CC0-1.0 + +run: + name: "20240826-z1" + shared_resources: + policy: false + disable_progressbar: true + +remote: + ssh: "z1" + path: "SCRATCH/projects/import-benefits/pypsa-eur" + +scenario: + simpl: + - '' + ll: + - vopt + clusters: + - 115 + opts: + - '' + # test sets + # sector_opts: + # - "" + # - imp + # - norelocation + sector_opts: + - "" + - imp + # subsets of carriers + - imp+AC + - imp+H2 + - imp+CH4 + - imp+NH3 + - imp+FT + - imp+MeOH + - imp+HBI+St # products + - imp+H2+CH4+NH3+FT+MeOH # molecules + - imp+H2+CH4+NH3+FT+MeOH+HBI+St # molecules + products + - imp+CH4+NH3+FT+MeOH # hydrogen derivatives + - imp+CH4+NH3+FT+MeOH+HBI+St # hydrogen derivatives + products + - imp+MeOH+FT # liquid carbonaceous fuels + - imp+CH4+FT+MeOH # carbonaceous fuels + # subsets of carriers (+20%) + - imp+AC1.2 + - imp+H21.2 + - imp+CH41.2 + - imp+NH31.2 + - imp+FT1.2 + - imp+MeOH1.2 + - imp+HBI1.2+St1.2 # products + - imp+H21.2+CH41.2+NH31.2+FT1.2+MeOH1.2 # molecules + - imp+H21.2+CH41.2+NH31.2+FT1.2+MeOH1.2+HBI1.2+St1.2 # molecules + products + - imp+CH41.2+NH31.2+FT1.2+MeOH1.2 # hydrogen derivatives + - imp+CH41.2+NH31.2+FT1.2+MeOH1.2+HBI1.2+St1.2 # hydrogen derivatives + products + - imp+MeOH1.2+FT1.2 # liquid carbonaceous fuels + - imp+CH41.2+FT1.2+MeOH1.2 # carbonaceous fuels + # subsets of carriers (+10%) + - imp+AC1.1 + - imp+H21.1 + - imp+CH41.1 + - imp+NH31.1 + - imp+FT1.1 + - imp+MeOH1.1 + - imp+HBI1.1+St1.1 # products + - imp+H21.1+CH41.1+NH31.1+FT1.1+MeOH1.1 # molecules + - imp+H21.1+CH41.1+NH31.1+FT1.1+MeOH1.1+HBI1.1+St1.1 # molecules + products + - imp+CH41.1+NH31.1+FT1.1+MeOH1.1 # hydrogen derivatives + - imp+CH41.1+NH31.1+FT1.1+MeOH1.1+HBI1.1+St1.1 # hydrogen derivatives + products + - imp+MeOH1.1+FT1.1 # liquid carbonaceous fuels + - imp+CH41.1+FT1.1+MeOH1.1 # carbonaceous fuels + # subsets of carriers (-10%) + - imp+AC0.9 + - imp+H20.9 + - imp+CH40.9 + - imp+NH30.9 + - imp+FT0.9 + - imp+MeOH0.9 + - imp+HBI0.9+St0.9 # products + - imp+H20.9+CH40.9+NH30.9+FT0.9+MeOH0.9 # molecules + - imp+H20.9+CH40.9+NH30.9+FT0.9+MeOH0.9+HBI0.9+St0.9 # molecules + products + - imp+CH40.9+NH30.9+FT0.9+MeOH0.9 # hydrogen derivatives + - imp+CH40.9+NH30.9+FT0.9+MeOH0.9+HBI0.9+St0.9 # hydrogen derivatives + products + - imp+MeOH0.9+FT0.9 # liquid carbonaceous fuels + - imp+CH40.9+FT0.9+MeOH0.9 # carbonaceous fuels + # subsets of carriers (-20%) + - imp+AC0.8 + - imp+H20.8 + - imp+CH40.8 + - imp+NH30.8 + - imp+FT0.8 + - imp+MeOH0.8 + - imp+HBI0.8+St0.8 # products + - imp+H20.8+CH40.8+NH30.8+FT0.8+MeOH0.8 # molecules + - imp+H20.8+CH40.8+NH30.8+FT0.8+MeOH0.8+HBI0.8+St0.8 # molecules + products + - imp+CH40.8+NH30.8+FT0.8+MeOH0.8 # hydrogen derivatives + - imp+CH40.8+NH30.8+FT0.8+MeOH0.8+HBI0.8+St0.8 # hydrogen derivatives + products + - imp+MeOH0.8+FT0.8 # liquid carbonaceous fuels + - imp+CH40.8+FT0.8+MeOH0.8 # carbonaceous fuels + # lower/higher import costs of molecules + - imp+AC+H22.0+CH42.0+NH32.0+FT2.0+MeOH2.0+HBI2.0+St2.0 + - imp+AC+H21.5+CH41.5+NH31.5+FT1.5+MeOH1.5+HBI1.5+St1.5 + - imp+AC+H21.3+CH41.3+NH31.3+FT1.3+MeOH1.3+HBI1.3+St1.3 + - imp+AC+H21.2+CH41.2+NH31.2+FT1.2+MeOH1.2+HBI1.2+St1.2 + - imp+AC+H21.1+CH41.1+NH31.1+FT1.1+MeOH1.1+HBI1.1+St1.1 + - imp+AC+H20.9+CH40.9+NH30.9+FT0.9+MeOH0.9+HBI0.9+St0.9 + - imp+AC+H20.8+CH40.8+NH30.8+FT0.8+MeOH0.8+HBI0.8+St0.8 + - imp+AC+H20.7+CH40.7+NH30.7+FT0.7+MeOH0.7+HBI0.7+St0.7 + - imp+AC+H20.6+CH40.6+NH30.6+FT0.6+MeOH0.6+HBI0.6+St0.6 + - imp+AC+H20.5+CH40.5+NH30.5+FT0.5+MeOH0.5+HBI0.5+St0.5 + # higher/lower import costs of everything + - imp+AC2.0+H22.0+CH42.0+NH32.0+FT2.0+MeOH2.0+HBI2.0+St2.0 + - imp+AC1.5+H21.5+CH41.5+NH31.5+FT1.5+MeOH1.5+HBI1.5+St1.5 + - imp+AC1.3+H21.3+CH41.3+NH31.3+FT1.3+MeOH1.3+HBI1.3+St1.3 + - imp+AC1.2+H21.2+CH41.2+NH31.2+FT1.2+MeOH1.2+HBI1.2+St1.2 + - imp+AC1.1+H21.1+CH41.1+NH31.1+FT1.1+MeOH1.1+HBI1.1+St1.1 + - imp+AC0.9+H20.9+CH40.9+NH30.9+FT0.9+MeOH0.9+HBI0.9+St0.9 + - imp+AC0.8+H20.8+CH40.8+NH30.8+FT0.8+MeOH0.8+HBI0.8+St0.8 + - imp+AC0.7+H20.7+CH40.7+NH30.7+FT0.7+MeOH0.7+HBI0.7+St0.7 + - imp+AC0.6+H20.6+CH40.6+NH30.6+FT0.6+MeOH0.6+HBI0.6+St0.6 + - imp+AC0.5+H20.5+CH40.5+NH30.5+FT0.5+MeOH0.5+HBI0.5+St0.5 + # lower import costs of electricity + - imp+AC0.9+H2+CH4+NH3+FT+MeOH+HBI+St + - imp+AC0.8+H2+CH4+NH3+FT+MeOH+HBI+St + - imp+AC0.7+H2+CH4+NH3+FT+MeOH+HBI+St + # lower/higher import costs of carbonaceous fuels + - imp+AC+H2+CH42.0+NH3+FT2.0+MeOH2.0+HBI+St + - imp+AC+H2+CH41.5+NH3+FT1.5+MeOH1.5+HBI+St + - imp+AC+H2+CH41.3+NH3+FT1.3+MeOH1.3+HBI+St + - imp+AC+H2+CH41.2+NH3+FT1.2+MeOH1.2+HBI+St + - imp+AC+H2+CH41.1+NH3+FT1.1+MeOH1.1+HBI+St + - imp+AC+H2+CH40.9+NH3+FT0.9+MeOH0.9+HBI+St + - imp+AC+H2+CH40.8+NH3+FT0.8+MeOH0.8+HBI+St + - imp+AC+H2+CH40.7+NH3+FT0.7+MeOH0.7+HBI+St + - imp+AC+H2+CH40.6+NH3+FT0.6+MeOH0.6+HBI+St + - imp+AC+H2+CH40.5+NH3+FT0.5+MeOH0.5+HBI+St + # import volume sensitivity (all) + - imp10000 + - imp9000 + - imp8000 + - imp7000 + - imp6000 + - imp5000 + - imp4000 + - imp3000 + - imp2000 + - imp1500 + - imp1000 + - imp500 + # import volume sensitivity (AC) + - imp7000+AC + - imp6000+AC + - imp5000+AC + - imp4000+AC + - imp3000+AC + - imp2000+AC + - imp1000+AC + # - imp500+AC + # import volume sensitivity (H2) + - imp7000+H2 + - imp6000+H2 + - imp5000+H2 + - imp4000+H2 + - imp3000+H2 + - imp2000+H2 + - imp1000+H2 + - imp500+H2 + # import volume sensitivity (CH4) + - imp5000+CH4 + - imp4000+CH4 + - imp3000+CH4 + - imp2000+CH4 + - imp1000+CH4 + - imp500+CH4 + # import volume sensitivity (FT+MeOH) + - imp1500+FT+MeOH + - imp1000+FT+MeOH + - imp500+FT+MeOH + # import volume sensitivity (NH3) + - imp1500+NH3 + - imp1000+NH3 + - imp500+NH3 + # import volume sensitivity (chemicals & products) + - imp7000+H2+CH4+NH3+FT+MeOH+HBI+St + - imp6000+H2+CH4+NH3+FT+MeOH+HBI+St + - imp5000+H2+CH4+NH3+FT+MeOH+HBI+St + - imp4000+H2+CH4+NH3+FT+MeOH+HBI+St + - imp3000+H2+CH4+NH3+FT+MeOH+HBI+St + - imp2000+H2+CH4+NH3+FT+MeOH+HBI+St + - imp1000+H2+CH4+NH3+FT+MeOH+HBI+St + - imp500+H2+CH4+NH3+FT+MeOH+HBI+St + # import volume sensitivity (50%, cheap chemicals) + - imp10000+AC+H21.5+CH41.5+NH31.5+FT1.5+MeOH1.5+HBI1.5+St1.5 + - imp9000+AC+H21.5+CH41.5+NH31.5+FT1.5+MeOH1.5+HBI1.5+St1.5 + - imp8000+AC+H21.5+CH41.5+NH31.5+FT1.5+MeOH1.5+HBI1.5+St1.5 + - imp7000+AC+H21.5+CH41.5+NH31.5+FT1.5+MeOH1.5+HBI1.5+St1.5 + - imp6000+AC+H21.5+CH41.5+NH31.5+FT1.5+MeOH1.5+HBI1.5+St1.5 + - imp5000+AC+H21.5+CH41.5+NH31.5+FT1.5+MeOH1.5+HBI1.5+St1.5 + - imp4000+AC+H21.5+CH41.5+NH31.5+FT1.5+MeOH1.5+HBI1.5+St1.5 + - imp3000+AC+H21.5+CH41.5+NH31.5+FT1.5+MeOH1.5+HBI1.5+St1.5 + - imp2000+AC+H21.5+CH41.5+NH31.5+FT1.5+MeOH1.5+HBI1.5+St1.5 + - imp1500+AC+H21.5+CH41.5+NH31.5+FT1.5+MeOH1.5+HBI1.5+St1.5 + - imp1000+AC+H21.5+CH41.5+NH31.5+FT1.5+MeOH1.5+HBI1.5+St1.5 + - imp500+AC+H21.5+CH41.5+NH31.5+FT1.5+MeOH1.5+HBI1.5+St1.5 + # import volume sensitivity (130%, expensive chemicals) + - imp10000+AC+H21.3+CH41.3+NH31.3+FT1.3+MeOH1.3+HBI1.3+St1.3 + - imp9000+AC+H21.3+CH41.3+NH31.3+FT1.3+MeOH1.3+HBI1.3+St1.3 + - imp8000+AC+H21.3+CH41.3+NH31.3+FT1.3+MeOH1.3+HBI1.3+St1.3 + - imp7000+AC+H21.3+CH41.3+NH31.3+FT1.3+MeOH1.3+HBI1.3+St1.3 + - imp6000+AC+H21.3+CH41.3+NH31.3+FT1.3+MeOH1.3+HBI1.3+St1.3 + - imp5000+AC+H21.3+CH41.3+NH31.3+FT1.3+MeOH1.3+HBI1.3+St1.3 + - imp4000+AC+H21.3+CH41.3+NH31.3+FT1.3+MeOH1.3+HBI1.3+St1.3 + - imp3000+AC+H21.3+CH41.3+NH31.3+FT1.3+MeOH1.3+HBI1.3+St1.3 + - imp2000+AC+H21.3+CH41.3+NH31.3+FT1.3+MeOH1.3+HBI1.3+St1.3 + - imp1500+AC+H21.3+CH41.3+NH31.3+FT1.3+MeOH1.3+HBI1.3+St1.3 + - imp1000+AC+H21.3+CH41.3+NH31.3+FT1.3+MeOH1.3+HBI1.3+St1.3 + - imp500+AC+H21.3+CH41.3+NH31.3+FT1.3+MeOH1.3+HBI1.3+St1.3 + # import volume sensitivity (120%, expensive chemicals) + - imp10000+AC+H21.2+CH41.2+NH31.2+FT1.2+MeOH1.2+HBI1.2+St1.2 + - imp9000+AC+H21.2+CH41.2+NH31.2+FT1.2+MeOH1.2+HBI1.2+St1.2 + - imp8000+AC+H21.2+CH41.2+NH31.2+FT1.2+MeOH1.2+HBI1.2+St1.2 + - imp7000+AC+H21.2+CH41.2+NH31.2+FT1.2+MeOH1.2+HBI1.2+St1.2 + - imp6000+AC+H21.2+CH41.2+NH31.2+FT1.2+MeOH1.2+HBI1.2+St1.2 + - imp5000+AC+H21.2+CH41.2+NH31.2+FT1.2+MeOH1.2+HBI1.2+St1.2 + - imp4000+AC+H21.2+CH41.2+NH31.2+FT1.2+MeOH1.2+HBI1.2+St1.2 + - imp3000+AC+H21.2+CH41.2+NH31.2+FT1.2+MeOH1.2+HBI1.2+St1.2 + - imp2000+AC+H21.2+CH41.2+NH31.2+FT1.2+MeOH1.2+HBI1.2+St1.2 + - imp1500+AC+H21.2+CH41.2+NH31.2+FT1.2+MeOH1.2+HBI1.2+St1.2 + - imp1000+AC+H21.2+CH41.2+NH31.2+FT1.2+MeOH1.2+HBI1.2+St1.2 + - imp500+AC+H21.2+CH41.2+NH31.2+FT1.2+MeOH1.2+HBI1.2+St1.2 + # import volume sensitivity (110%, expensive chemicals) + - imp10000+AC+H21.1+CH41.1+NH31.1+FT1.1+MeOH1.1+HBI1.1+St1.1 + - imp9000+AC+H21.1+CH41.1+NH31.1+FT1.1+MeOH1.1+HBI1.1+St1.1 + - imp8000+AC+H21.1+CH41.1+NH31.1+FT1.1+MeOH1.1+HBI1.1+St1.1 + - imp7000+AC+H21.1+CH41.1+NH31.1+FT1.1+MeOH1.1+HBI1.1+St1.1 + - imp6000+AC+H21.1+CH41.1+NH31.1+FT1.1+MeOH1.1+HBI1.1+St1.1 + - imp5000+AC+H21.1+CH41.1+NH31.1+FT1.1+MeOH1.1+HBI1.1+St1.1 + - imp4000+AC+H21.1+CH41.1+NH31.1+FT1.1+MeOH1.1+HBI1.1+St1.1 + - imp3000+AC+H21.1+CH41.1+NH31.1+FT1.1+MeOH1.1+HBI1.1+St1.1 + - imp2000+AC+H21.1+CH41.1+NH31.1+FT1.1+MeOH1.1+HBI1.1+St1.1 + - imp1500+AC+H21.1+CH41.1+NH31.1+FT1.1+MeOH1.1+HBI1.1+St1.1 + - imp1000+AC+H21.1+CH41.1+NH31.1+FT1.1+MeOH1.1+HBI1.1+St1.1 + - imp500+AC+H21.1+CH41.1+NH31.1+FT1.1+MeOH1.1+HBI1.1+St1.1 + # import volume sensitivity (90%, cheap chemicals) + - imp10000+AC+H20.9+CH40.9+NH30.9+FT0.9+MeOH0.9+HBI0.9+St0.9 + - imp9000+AC+H20.9+CH40.9+NH30.9+FT0.9+MeOH0.9+HBI0.9+St0.9 + - imp8000+AC+H20.9+CH40.9+NH30.9+FT0.9+MeOH0.9+HBI0.9+St0.9 + - imp7000+AC+H20.9+CH40.9+NH30.9+FT0.9+MeOH0.9+HBI0.9+St0.9 + - imp6000+AC+H20.9+CH40.9+NH30.9+FT0.9+MeOH0.9+HBI0.9+St0.9 + - imp5000+AC+H20.9+CH40.9+NH30.9+FT0.9+MeOH0.9+HBI0.9+St0.9 + - imp4000+AC+H20.9+CH40.9+NH30.9+FT0.9+MeOH0.9+HBI0.9+St0.9 + - imp3000+AC+H20.9+CH40.9+NH30.9+FT0.9+MeOH0.9+HBI0.9+St0.9 + - imp2000+AC+H20.9+CH40.9+NH30.9+FT0.9+MeOH0.9+HBI0.9+St0.9 + - imp1500+AC+H20.9+CH40.9+NH30.9+FT0.9+MeOH0.9+HBI0.9+St0.9 + - imp1000+AC+H20.9+CH40.9+NH30.9+FT0.9+MeOH0.9+HBI0.9+St0.9 + - imp500+AC+H20.9+CH40.9+NH30.9+FT0.9+MeOH0.9+HBI0.9+St0.9 + # import volume sensitivity (80%, cheap chemicals) + - imp10000+AC+H20.8+CH40.8+NH30.8+FT0.8+MeOH0.8+HBI0.8+St0.8 + - imp9000+AC+H20.8+CH40.8+NH30.8+FT0.8+MeOH0.8+HBI0.8+St0.8 + - imp8000+AC+H20.8+CH40.8+NH30.8+FT0.8+MeOH0.8+HBI0.8+St0.8 + - imp7000+AC+H20.8+CH40.8+NH30.8+FT0.8+MeOH0.8+HBI0.8+St0.8 + - imp6000+AC+H20.8+CH40.8+NH30.8+FT0.8+MeOH0.8+HBI0.8+St0.8 + - imp5000+AC+H20.8+CH40.8+NH30.8+FT0.8+MeOH0.8+HBI0.8+St0.8 + - imp4000+AC+H20.8+CH40.8+NH30.8+FT0.8+MeOH0.8+HBI0.8+St0.8 + - imp3000+AC+H20.8+CH40.8+NH30.8+FT0.8+MeOH0.8+HBI0.8+St0.8 + - imp2000+AC+H20.8+CH40.8+NH30.8+FT0.8+MeOH0.8+HBI0.8+St0.8 + - imp1500+AC+H20.8+CH40.8+NH30.8+FT0.8+MeOH0.8+HBI0.8+St0.8 + - imp1000+AC+H20.8+CH40.8+NH30.8+FT0.8+MeOH0.8+HBI0.8+St0.8 + - imp500+AC+H20.8+CH40.8+NH30.8+FT0.8+MeOH0.8+HBI0.8+St0.8 + # import volume sensitivity (70%, cheap chemicals) + - imp10000+AC+H20.7+CH40.7+NH30.7+FT0.7+MeOH0.7+HBI0.7+St0.7 + - imp9000+AC+H20.7+CH40.7+NH30.7+FT0.7+MeOH0.7+HBI0.7+St0.7 + - imp8000+AC+H20.7+CH40.7+NH30.7+FT0.7+MeOH0.7+HBI0.7+St0.7 + - imp7000+AC+H20.7+CH40.7+NH30.7+FT0.7+MeOH0.7+HBI0.7+St0.7 + - imp6000+AC+H20.7+CH40.7+NH30.7+FT0.7+MeOH0.7+HBI0.7+St0.7 + - imp5000+AC+H20.7+CH40.7+NH30.7+FT0.7+MeOH0.7+HBI0.7+St0.7 + - imp4000+AC+H20.7+CH40.7+NH30.7+FT0.7+MeOH0.7+HBI0.7+St0.7 + - imp3000+AC+H20.7+CH40.7+NH30.7+FT0.7+MeOH0.7+HBI0.7+St0.7 + - imp2000+AC+H20.7+CH40.7+NH30.7+FT0.7+MeOH0.7+HBI0.7+St0.7 + - imp1500+AC+H20.7+CH40.7+NH30.7+FT0.7+MeOH0.7+HBI0.7+St0.7 + - imp1000+AC+H20.7+CH40.7+NH30.7+FT0.7+MeOH0.7+HBI0.7+St0.7 + - imp500+AC+H20.7+CH40.7+NH30.7+FT0.7+MeOH0.7+HBI0.7+St0.7 + # import volume sensitivity (50%, cheap chemicals) + - imp10000+AC+H20.5+CH40.5+NH30.5+FT0.5+MeOH0.5+HBI0.5+St0.5 + - imp9000+AC+H20.5+CH40.5+NH30.5+FT0.5+MeOH0.5+HBI0.5+St0.5 + - imp8000+AC+H20.5+CH40.5+NH30.5+FT0.5+MeOH0.5+HBI0.5+St0.5 + - imp7000+AC+H20.5+CH40.5+NH30.5+FT0.5+MeOH0.5+HBI0.5+St0.5 + - imp6000+AC+H20.5+CH40.5+NH30.5+FT0.5+MeOH0.5+HBI0.5+St0.5 + - imp5000+AC+H20.5+CH40.5+NH30.5+FT0.5+MeOH0.5+HBI0.5+St0.5 + - imp4000+AC+H20.5+CH40.5+NH30.5+FT0.5+MeOH0.5+HBI0.5+St0.5 + - imp3000+AC+H20.5+CH40.5+NH30.5+FT0.5+MeOH0.5+HBI0.5+St0.5 + - imp2000+AC+H20.5+CH40.5+NH30.5+FT0.5+MeOH0.5+HBI0.5+St0.5 + - imp1500+AC+H20.5+CH40.5+NH30.5+FT0.5+MeOH0.5+HBI0.5+St0.5 + - imp1000+AC+H20.5+CH40.5+NH30.5+FT0.5+MeOH0.5+HBI0.5+St0.5 + - imp500+AC+H20.5+CH40.5+NH30.5+FT0.5+MeOH0.5+HBI0.5+St0.5 + # additional scenario: no hydrogen network + - noH2network + - noH2network-imp + - noH2network-imp+AC+CH4+NH3+FT+MeOH+HBI+St # no hydrogen imports + # additional scenario: no wasteheat + - nowasteheat + # - nowasteheat-imp + # additional scenario: all wasteheat + - allwasteheat + - allwasteheat-imp + # additional scenario: all waste heat, no relocation + - allwasteheat-norelocation-imp + - allwasteheat-norelocation + # no steel+ammonia industry relocation + - norelocation + - norelocation-imp + # no power-to-X flexibility + - noPtXflex + - noPtXflex-imp + # no shipping import flexibility + # - noshipflex-imp + # onwind expansion to 3 MW/km2 + - onwind+p2 + - onwind+p2-imp + # unlimited land usage for utility-scale techs (10x) + - onwind+p10-solar+p10 + - onwind+p10-solar+p10-imp + planning_horizons: + - 2050 + +enable: + retrieve: true + +atlite: + nprocesses: 12 + +clustering: + focus_weights: + RO: 0.025 # tuned for 115 regions + GR: 0.015 # tuned for 115 regions + PT: 0.018 # tuned for 115 regions + BG: 0.015 # tuned for 115 regions + CZ: 0.015 # tuned for 115 regions + HU: 0.015 # tuned for 115 regions + IE: 0.012 # tuned for 115 regions + NL: 0.015 # tuned for 115 regions + PL: 0.06 # tuned for 115 regions + FR: 0.2 # tuned for 115 regions + DE: 0.14 # tuned for 115 regions + CH: 0.01 # tuned for 115 regions + RS: 0.015 # tuned for 115 regions + BE: 0.015 # tuned for 115 regions + temporal: + resolution_sector: 2190SEG + simplify_network: + remove_stubs_across_borders: false + +electricity: + base_network: osm-prebuilt + extendable_carriers: + Generator: + - solar + - onwind + - offwind-ac + - offwind-dc + - offwind-float + - nuclear + conventional_carriers: + - nuclear + renewable_carriers: + - solar + - onwind + - offwind-ac + - offwind-dc + - offwind-float + - hydro + + powerplants_filter: Fueltype == 'Hydro' # only take hydro from powerplantmatching + custom_powerplants: true # take all from data/custom_powerplants.csv + + estimate_renewable_capacities: + enable: false + +conventional: + nuclear: + p_max_pu: "data/nuclear_p_max_pu.csv" + p_min_pu: "data/nuclear_p_max_pu.csv" + +renewable: + solar: + capacity_per_sqkm: 5.1 + onwind: + capacity_per_sqkm: 1.5 + hydro: + flatten_dispatch: true + offwind-ac: + landfall_length: 30 + offwind-dc: + landfall_length: 35 + offwind-floating: + landfall_length: 40 + +lines: + max_extension: 15000 + dynamic_line_rating: + activate: true + max_voltage_difference: 45. + max_line_rating: 1.3 + +links: + max_extension: 15000 + +transmission_projects: + status: + - under_construction + - in_permitting + - confirmed + - planned_not_yet_permitted + +biomass: + year: 2050 + +adjustments: + electricity: false + sector: + factor: + Link: + electricity distribution grid: + capital_cost: 2 + +sector: + land_transport_fuel_cell_share: 0. + land_transport_electric_share: 1. + land_transport_ice_share: 0. + endogenous_steel: true + relocation_steel: true + relocation_ammonia: true + solar_thermal: true + ammonia: true + gas_network: false + H2_retrofit: true + biomass_boiler: true + hydrogen_turbine: true + biosng: false + biosng_cc: false + electrobiofuels: true + biomass_to_liquid: false + biomass_to_liquid_cc: false + biogas_upgrading_cc: true + methanol: + methanol_reforming: false + methanol_reforming_cc: false + methanol_to_kerosene: true + methanol_to_power: + ccgt: false + ccgt_cc: false + ocgt: true + allam: false + biomass_to_methanol: false + biomass_to_methanol_cc: false + municipal_solid_waste: false + use_fuel_cell_waste_heat: 1 + import: + capacity_boost: 2.5 + limit: false # bool or number in TWh + limit_sense: "==" + options: + - pipeline-h2 + - shipping-lh2 + - shipping-lch4 + - shipping-lnh3 + - shipping-ftfuel + - shipping-meoh + - shipping-steel + - shipping-hbi + - hvdc-to-elec + exporters: + - AR-South + - AR-North + - AU-West + - AU-East + - BR-Northeast + - BR-Southeast + - CA-East + - CL + - CN-Northeast + - CN-Southeast + - CN-West + - DZ + - EG + - KZ + - LY + - MA + - EH + - NA + - OM + - SA + - TN + - TR + - UA + - US-Northeast + - US-Southeast + - US-Northwest + - US-South + - US-Southwest + - US-Alaska + - MX + - TM + - UZ + - MN + - PK + - TH + - ZA + - KE + - TZ + - NG + - AO + - ET + - MZ + - ER + - MR + - SN + - UY + - CO + - PE + - IN-Northwest + - IN-South + - IN-East + - MG + - BO + p_nom_max: + pipeline-h2: 50700 # EHB 3 parallel 48" pipelines + min_part_load_shipping_imports: 0 + min_part_load_pipeline_imports: 0.9 + endogenous_hvdc_import: + enable: true + exporters: + - CN-West + - OM + - EH + - MA + - DZ + - TN + - LY + - EG + - TR + - SA + - UA + - KZ + - UZ + - TM + - MN + - MR + extra_connections: [] + xlinks: + MA: + # Plymouth + - x: -4.14 + y: 50.39 + length: 3800 + # Cork + - x: -8.52 + y: 51.92 + length: 4000 + # Brest + - x: -4.49 + y: 48.43 + length: 3200 + # Vigo + - x: -8.70 + y: 42.31 + length: 1800 + MR: + # Plymouth + - x: -4.14 + y: 50.39 + length: 5300 + # Cork + - x: -8.52 + y: 51.92 + length: 5500 + # Brest + - x: -4.49 + y: 48.43 + length: 4700 + # Vigo + - x: -8.70 + y: 42.31 + length: 3300 + EH: + # Plymouth + - x: -4.14 + y: 50.39 + length: 4800 + # Cork + - x: -8.52 + y: 51.92 + length: 5000 + # Brest + - x: -4.49 + y: 48.43 + length: 4200 + # Vigo + - x: -8.70 + y: 42.31 + length: 2800 + DZ: + # Barcelona + - x: 2.37 + y: 41.57 + length: 1300 + # Genoa + - x: 8.93 + y: 44.41 + length: 1800 + # Marseille + - x: 5.30 + y: 43.38 + length: 1400 + TN: + # Genoa + - x: 8.93 + y: 44.41 + length: 1500 + # Marseille + - x: 5.30 + y: 43.38 + length: 1500 + LY: + # Venice + - x: 12.39 + y: 45.56 + length: 2500 + EG: + # Venice + - x: 12.39 + y: 45.56 + length: 2500 + efficiency_static: 0.98 + efficiency_per_1000km: 0.977 # p.u./km + length_factor: 1.25 + distance_threshold: 0.04 # quantile + p_nom_max: 25000 # MW + share_of_p_nom_max_available: 1 + +industry: + HVC_primary_fraction: 0.45 + HVC_mechanical_recycling_fraction: 0.3 + HVC_chemical_recycling_fraction: 0.15 + HVC_environment_sequestration_fraction: 0.2 + waste_to_energy: false + waste_to_energy_cc: false + +costs: + year: 2040 + version: 40675bf + +solving: + options: + skip_iterations: true + transmission_losses: 2 + linearized_unit_commitment: false + + solver_options: + gurobi-default: + threads: 4 + BarConvTol: 5.e-5 + + mem_mb: 150000 + runtime: 96h diff --git a/config/config.default.yaml b/config/config.default.yaml index 2255e4a16..29446b83a 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -677,6 +677,9 @@ sector: H2 pipeline: efficiency_per_1000km: 1 # 0.982 compression_per_1000km: 0.018 + # H2 pipeline retrofitted: + # efficiency_per_1000km: 1 # 0.982 + # compression_per_1000km: 0.018 gas pipeline: efficiency_per_1000km: 1 #0.977 compression_per_1000km: 0.01 @@ -1222,22 +1225,56 @@ plotting: energy_threshold: 50. nice_names: - OCGT: "Open-Cycle Gas" - CCGT: "Combined-Cycle Gas" - offwind-ac: "Offshore Wind (AC)" - offwind-dc: "Offshore Wind (DC)" - offwind-float: "Offshore Wind (Floating)" - onwind: "Onshore Wind" - solar: "Solar" - PHS: "Pumped Hydro Storage" - hydro: "Reservoir & Dam" - battery: "Battery Storage" + H2 Fuel Cell: "hydrogen fuel cell" + H2 turbine: "hydrogen turbine" + OCGT: "open-cycle gas turbine" + CCGT: "closed-cycle gas turbine" + offwind-ac: "offshore wind (AC)" + offwind-dc: "offshore wind (DC)" + offwind-float: "offshore wind (floating)" + onwind: "onshore wind" + solar: "utility-scale solar" + solar rooftop: "rooftop solar" + nuclear: "nuclear" + PHS: "pumped-hydro storage" + hydro: "reservoir & dam" + battery: "battery storage" + battery inverter: "battery storage" + SMR: "steam methane reforming" + SMR CC: "steam methane reforming (CC)" + DRI: "direct reduced iron" + EAF: "electric arc furnace" H2: "Hydrogen Storage" + H2 Electrolysis: "electrolysis" + H2 for industry: "chlor-alkali electrolysis" + H2 pipeline: "hydrogen pipeline" lines: "Transmission Lines" - ror: "Run of River" - load: "Load Shedding" - ac: "AC" - dc: "DC" + ror: "run of river" + load: "load shedding" + AC: "HVAC line" + DC: "HVDC link" + import pipeline-h2: "hydrogen import (pipeline)" + import infrastructure pipeline-h2: "hydrogen import (pipeline)" + import shipping-lh2: "hydrogen import (ship)" + import shipping-lch4: "methane import (ship)" + import shipping-lnh3: "ammonia import (ship)" + import shipping-ftfuel: "Fischer-Tropsch import (ship)" + import shipping-meoh: "methanol import (ship)" + import shipping-steel: "steel import (ship)" + import shipping-hbi: "HBI import (ship)" + import hvdc-to-elec: "electricity import (HVDC)" + pipeline-h2: "hydrogen import (pipeline)" + infrastructure pipeline-h2: "hydrogen import (pipeline)" + shipping-lh2: "hydrogen import (ship)" + shipping-lch4: "methane import (ship)" + shipping-lnh3: "ammonia import (ship)" + shipping-ftfuel: "Fischer-Tropsch import (ship)" + shipping-meoh: "methanol import (ship)" + shipping-steel: "steel import (ship)" + shipping-hbi: "HBI import (ship)" + hvdc-to-elec: "electricity import (HVDC)" + external onwind: "onshore wind (external)" + external solar-utility: "solar photovoltaics (external)" tech_colors: # wind @@ -1301,6 +1338,7 @@ plotting: gas for industry co2 to stored: '#8a3400' gas for industry: '#853403' gas for industry CC: '#692e0a' + pipeline: '#ebbca0' gas pipeline: '#ebbca0' gas pipeline new: '#a87c62' # oil @@ -1357,7 +1395,7 @@ plotting: biomass to liquid CC: '#32CDaa' unsustainable solid biomass: '#998622' unsustainable bioliquids: '#32CD32' - electrobiofuels: 'red' + electrobiofuels: '#7a6d26' BioSNG: '#123456' BioSNG CC: '#123456' solid biomass to hydrogen: '#654321' @@ -1377,7 +1415,7 @@ plotting: battery: '#ace37f' battery storage: '#ace37f' battery charger: '#88a75b' - battery discharger: '#5d4e29' + battery discharger: '#ace37f' home battery: '#80c944' home battery storage: '#80c944' home battery charger: '#5e8032' @@ -1465,7 +1503,7 @@ plotting: retrofitting: '#8487e8' building retrofitting: '#8487e8' # hydrogen - H2 for industry: "#f073da" + H2 for industry: "#FFA07A" H2 for shipping: "#ebaee0" H2: '#bf13a0' hydrogen: '#bf13a0' @@ -1479,9 +1517,9 @@ plotting: land transport fuel cell: '#6b3161' H2 pipeline: '#f081dc' H2 pipeline retrofitted: '#ba99b5' - H2 Fuel Cell: '#c251ae' - H2 fuel cell: '#c251ae' - H2 turbine: '#991f83' + H2 Fuel Cell: '#a6a8ed' + H2 fuel cell: '#a6a8ed' + H2 turbine: '#d5d6f5' H2 Electrolysis: '#ff29d9' H2 electrolysis: '#ff29d9' # ammonia @@ -1489,7 +1527,7 @@ plotting: ammonia: '#46caf0' ammonia store: '#00ace0' ammonia cracker: '#87d0e6' - Haber-Bosch: '#076987' + Haber-Bosch: '#c1cf3a' # syngas Sabatier: '#9850ad' methanation: '#c44ce6' @@ -1503,7 +1541,7 @@ plotting: methanol-to-olefins/aromatics: '#FFA07A' Methanol steam reforming: '#FFBF00' Methanol steam reforming CC: '#A2EA8A' - methanolisation: '#00FFBF' + methanolisation: '#a1ffe6' biomass-to-methanol: '#EAD28A' biomass-to-methanol CC: '#EADBAD' allam methanol: '#B98F76' @@ -1517,6 +1555,7 @@ plotting: # co2 CC: '#f29dae' CCS: '#f29dae' + carbon capture and storage: '#f29dae' CO2 sequestration: '#f29dae' DAC: '#ff5270' co2 stored: '#f2385a' @@ -1542,27 +1581,41 @@ plotting: power-to-liquid: '#25c49a' gas-to-power/heat: '#ee8340' waste: '#e3d37d' - other: '#000000' + other: '#777' + Other: '#777' geothermal: '#ba91b1' geothermal heat: '#ba91b1' geothermal district heat: '#d19D00' geothermal organic rankine cycle: '#ffbf00' - AC: "#70af1d" - AC-AC: "#70af1d" - AC line: "#70af1d" - links: "#8a1caf" - HVDC links: "#8a1caf" - DC: "#8a1caf" - DC-DC: "#8a1caf" - DC link: "#8a1caf" + AC: "rosybrown" + AC-AC: "rosybrown" + AC line: "rosybrown" + links: "darkseagreen" + HVDC links: "darkseagreen" + DC: "darkseagreen" + DC-DC: "darkseagreen" + DC link: "darkseagreen" import pipeline-h2: '#db8ccd' - import shipping-lh2: '#d9b8d3' + import infrastructure pipeline-h2: '#db8ccd' + import shipping-lh2: '#e0c5dc' import shipping-lch4: '#f7a572' - import shipping-lnh3: '#87d0e6' - import shipping-ftfuel: '#95decb' - import shipping-meoh: '#b7dbdb' + import shipping-lnh3: '#e2ed74' + import shipping-ftfuel: '#93eda2' + import shipping-meoh: '#87d0e6' import shipping-steel: '#94a4be' + import shipping-hbi: '#d3dceb' import hvdc-to-elec: '#91856a' + electricity imports (HVDC): '#91856a' + pipeline-h2: '#db8ccd' + infrastructure pipeline-h2: '#db8ccd' + shipping-lh2: '#e0c5dc' + shipping-lch4: '#f7a572' + shipping-lnh3: '#e2ed74' + shipping-ftfuel: '#93eda2' + shipping-meoh: '#87d0e6' + shipping-steel: '#94a4be' + shipping-hbi: '#d3dceb' + hvdc-to-elec: '#91856a' external H2: '#f7cdf0' external H2 Turbine: '#991f83' external H2 Electrolysis: '#991f83' diff --git a/matplotlibrc b/matplotlibrc index e028d405a..abbb203e4 100644 --- a/matplotlibrc +++ b/matplotlibrc @@ -8,3 +8,4 @@ figure.autolayout : True figure.constrained_layout.use : True figure.dpi : 400 legend.frameon : False +mathtext.default: regular \ No newline at end of file diff --git a/rules/build_sector.smk b/rules/build_sector.smk index e141e469f..1c0d08dd1 100755 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -945,15 +945,15 @@ rule time_aggregation: ), output: snapshot_weightings=resources( - "snapshot_weightings_base_s_{clusters}_elec_l{ll}_{opts}_{sector_opts}.csv" + "snapshot_weightings_base_s_{clusters}_elec_l{ll}_{opts}.csv" ), threads: 1 resources: mem_mb=5000, log: - logs("time_aggregation_base_s_{clusters}_elec_l{ll}_{opts}_{sector_opts}.log"), + logs("time_aggregation_base_s_{clusters}_elec_l{ll}_{opts}.log"), benchmark: - benchmarks("time_aggregation_base_s_{clusters}_elec_l{ll}_{opts}_{sector_opts}") + benchmarks("time_aggregation_base_s_{clusters}_elec_l{ll}_{opts}") conda: "../envs/environment.yaml" script: @@ -1025,7 +1025,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}_{sector_opts}.csv" + "snapshot_weightings_base_s_{clusters}_elec_l{ll}_{opts}.csv" ), retro_cost=lambda w: ( resources("retro_cost_base_s_{clusters}.csv") diff --git a/rules/plot.smk b/rules/plot.smk index 6b556c1fa..50c3c91ed 100644 --- a/rules/plot.smk +++ b/rules/plot.smk @@ -5,7 +5,7 @@ rule plot_power_network_unclustered: input: - network=resources("networks/elec.nc"), + network=resources("networks/base.nc"), rc="matplotlibrc", output: multiext(resources("graphics/power-network-unclustered"), ".png", ".pdf"), @@ -182,6 +182,19 @@ rule plot_choropleth_capacities: script: "../scripts/plot_choropleth_capacities.py" +rule plot_dh_share: + input: + regions_onshore=resources("regions_onshore_base_s_{clusters}.geojson"), + dh_share=resources("district_heat_share_base_s_{clusters}_{planning_horizons}.csv"), + rc="matplotlibrc", + output: + multiext( + resources("graphics/dh-share-s-{clusters}-{planning_horizons}"), + ".png", + ".pdf", + ), + script: + "../scripts/plot_dh_share.py" rule plot_choropleth_prices: input: @@ -235,6 +248,21 @@ rule plot_choropleth_demand: "../scripts/plot_choropleth_demand.py" +rule plot_backup_power_map: + params: + plotting=config_provider("plotting"), + input: + network=RESULTS + + "postnetworks/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + regions=resources("regions_onshore_base_s_{clusters}.geojson"), + rc="matplotlibrc", + output: + RESULTS + + "graphics/backup_map/s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.pdf", + script: + "../scripts/plot_backup_power_map.py" + + rule plot_balance_timeseries: input: network=RESULTS @@ -286,7 +314,7 @@ rule plot_import_options: + "prenetworks/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", regions=resources("regions_onshore_base_s_{clusters}.geojson"), entrypoints=resources("gas_input_locations_s_{clusters}_simplified.csv"), - imports="data/imports/results.csv", + imports="data/imports/results.parquet", rc="matplotlibrc", output: map=multiext( @@ -305,15 +333,38 @@ rule plot_import_options: "../scripts/plot_import_options.py" +rule retrieve_gadm_argentina: + input: + storage("https://geodata.ucdavis.edu/gadm/gadm4.1/gpkg/gadm41_ARG.gpkg"), + output: + "data/gadm/gadm41_ARG.gpkg", + run: + move(input[0], output[0]) + + +rule retrieve_naturalearth_lowres_countries: + input: + storage( + "https://naciscdn.org/naturalearth/110m/cultural/ne_110m_admin_0_countries.zip" + ), + params: + zip="data/naturalearth/ne_110m_admin_0_countries.zip", + output: + countries="data/naturalearth/ne_110m_admin_0_countries.shp", + run: + move(input[0], params["zip"]) + output_folder = Path(output["countries"]).parent + unpack_archive(params["zip"], output_folder) + os.remove(params["zip"]) + + rule plot_import_world_map: input: - imports="data/imports/results.csv", + imports="data/imports/results.parquet", profiles="data/imports/combined_weighted_generator_timeseries.nc", - gadm_arg=storage( - "https://geodata.ucdavis.edu/gadm/gadm4.1/gpkg/gadm41_ARG.gpkg", - keep_local=True, - ), - copernicus_glc="data/PROBAV_LC100_global_v3.0.1_2019-nrt_Discrete-Classification-map_EPSG-4326.tif", + gadm_arg="data/gadm/gadm41_ARG.gpkg", + countries="data/naturalearth/ne_110m_admin_0_countries.shp", + copernicus_glc="data/Copernicus_LC100_global_v3.0.1_2019-nrt_Discrete-Classification-map_EPSG-4326.tif", wdpa="data/WDPA.gpkg", rc="matplotlibrc", output: @@ -322,6 +373,17 @@ rule plot_import_world_map: "../scripts/plot_import_world_map.py" +rule plot_import_world_map_hydrogen: + input: + imports="data/imports/results.parquet", + countries="data/naturalearth/ne_110m_admin_0_countries.shp", + rc="matplotlibrc", + output: + multiext(resources("graphics/import_world_map_hydrogen"), ".png", ".pdf"), + script: + "../scripts/plot_import_world_map_hydrogen.py" + + rule plot_import_networks: input: network=RESULTS @@ -355,6 +417,34 @@ rule plot_import_shares: "../scripts/plot_import_shares.py" +rule plot_import_sankey: + input: + network=RESULTS + + "postnetworks/base_s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", + output: + RESULTS + + "graphics/import_sankey/s_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.pdf", + script: + "../scripts/plot_import_sankey.py" + + +rule plot_import_supply_curve: + input: + imports="data/imports/results.parquet", + rc="matplotlibrc", + output: + hvdc_to_elec=multiext(resources("graphics/import_supply_curve_hvdc-to-elec"), ".png", ".pdf"), + pipeline_h2=multiext(resources("graphics/import_supply_curve_pipeline-h2"), ".png", ".pdf"), + shipping_lh2=multiext(resources("graphics/import_supply_curve_shipping-lh2"), ".png", ".pdf"), + shipping_lnh3=multiext(resources("graphics/import_supply_curve_shipping-lnh3"), ".png", ".pdf"), + shipping_lch4=multiext(resources("graphics/import_supply_curve_shipping-lch4"), ".png", ".pdf"), + shipping_meoh=multiext(resources("graphics/import_supply_curve_shipping-meoh"), ".png", ".pdf"), + shipping_ftfuel=multiext(resources("graphics/import_supply_curve_shipping-ftfuel"), ".png", ".pdf"), + shipping_hbi=multiext(resources("graphics/import_supply_curve_shipping-hbi"), ".png", ".pdf"), + shipping_steel=multiext(resources("graphics/import_supply_curve_shipping-steel"), ".png", ".pdf"), + script: + "../scripts/plot_import_supply_curve.py" + rule plot_all_resources: input: rules.plot_power_network_unclustered.output, diff --git a/scripts/plot_backup_power_map.py b/scripts/plot_backup_power_map.py new file mode 100644 index 000000000..68ea2c6cc --- /dev/null +++ b/scripts/plot_backup_power_map.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2023-2024 PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT +""" +Plot map of backup power capacities. +""" + +import geopandas as gpd +import matplotlib.pyplot as plt +import pypsa +from _helpers import set_scenario_config +import cartopy.crs as ccrs +from pypsa.plot import add_legend_lines, add_legend_circles, add_legend_patches + +BACKUP_TYPES = { + 'OCGT': "#e05b09", + 'OCGT methanol': "#a1ffe6", + 'H2 turbine': "#d5d6f5", + 'H2 Fuel Cell': "#a6a8ed", + 'urban central gas CHP': "#f7b7a3", + 'urban central gas CHP CC': "#e69487", + 'urban central solid biomass CHP': "#d5ca8d", + 'urban central solid biomass CHP CC': "#baa741", + 'battery discharger': "#ace37f", + 'home battery discharger': "#80c944", +} + +NICE_NAMES = { + 'OCGT': "gas turbine", + 'OCGT methanol': "methanol turbine", + 'H2 turbine': "hydrogen turbine", + 'H2 Fuel Cell': "hydrogen CHP", + 'urban central gas CHP': "gas CHP", + 'urban central gas CHP CC': "gas CHP CC", + 'urban central solid biomass CHP': "biomass CHP", + 'urban central solid biomass CHP CC': "biomass CHP CC", + 'battery discharger': "utility-scale battery", + 'home battery discharger': "home battery", +} + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake( + "plot_backup_power_map", + opts="", + clusters="115", + ll="vopt", + sector_opts="imp", + planning_horizons="2050", + configfiles="config/config.20240826-z1.yaml", + ) + set_scenario_config(snakemake) + + plt.style.use(snakemake.input.rc) + + lw_factor = 6e3 + bs_factor = 3e4 + + n = pypsa.Network(snakemake.input.network) + + bus_sizes = n.links.query("carrier in @BACKUP_TYPES.keys()").groupby([n.links.bus1.map(n.buses.location), n.links.carrier]).apply(lambda x: (x.p_nom_opt * x.efficiency).sum()) + + n.mremove("Bus", n.buses.query("carrier != 'AC'").index) + n.buses = n.buses.loc[n.buses.index.str.len() != 2] + n.buses = n.buses.loc[~n.buses.index.str.contains("-")] + + prices = ((n.snapshot_weightings.generators @ n.buses_t.marginal_price) / n.snapshot_weightings.generators.sum())[n.buses.index] + + regions = gpd.read_file(snakemake.input.regions).set_index("name") + + proj = ccrs.EqualEarth() + + fig, ax = plt.subplots(figsize=(8, 8), subplot_kw={"projection": proj}) + regions.to_crs(proj.proj4_init).plot( + ax=ax, column=prices, cmap='Purples', + legend=True, + vmin=30, + vmax=90, + legend_kwds={ + "label": r"time-averaged electricity price [€/MWh]", + "shrink": 0.5, + "pad": 0.02, + "orientation": "vertical", + "extend": "both", + }, + ) + n.plot( + ax=ax, + margin=0.06, + line_widths=n.lines.s_nom_opt / lw_factor, + link_widths=n.links.p_nom_opt / lw_factor, + line_colors='skyblue', + link_colors='plum', + geomap='50m', + bus_sizes=bus_sizes / bs_factor, + bus_colors=BACKUP_TYPES, + ) + + sizes = [10, 20] + labels = [f"{s} GW" for s in sizes] + scale = 1e3 / lw_factor + sizes = [s * scale for s in sizes] + + legend_kw = dict( + loc=[0.625, 1.05], + frameon=False, + labelspacing=2, + handletextpad=1, + fontsize=11, + ) + + add_legend_lines( + ax, + sizes, + labels, + patch_kw=dict(color="plum", linestyle=":", gapcolor="skyblue"), + legend_kw=legend_kw, + ) + + legend_kw = dict( + loc=[0, 1], + frameon=False, + labelspacing=0.5, + handletextpad=1, + fontsize=11, + ncol=2, + ) + + add_legend_patches( + ax, list(BACKUP_TYPES.values()), list(NICE_NAMES.values()), legend_kw=legend_kw + ) + + sizes = [10, 30] + labels = [f"{s} GW" for s in sizes] + scale = 1e3 / bs_factor + sizes = [s * scale for s in sizes] + + legend_kw = dict( + loc=[0.82, 1.05], + frameon=False, + labelspacing=2, + handletextpad=1, + fontsize=11, + ) + + add_legend_circles( + ax, sizes, labels, patch_kw=dict(facecolor="lightgrey"), legend_kw=legend_kw + ) + + plt.savefig(snakemake.output[0], bbox_inches="tight") diff --git a/scripts/plot_balance_timeseries.py b/scripts/plot_balance_timeseries.py index e760706dc..827bd586a 100644 --- a/scripts/plot_balance_timeseries.py +++ b/scripts/plot_balance_timeseries.py @@ -106,7 +106,7 @@ def plot_energy_balance_timeseries( pos = df.where(df > 0).fillna(0.0) neg = df.where(df < 0).fillna(0.0) - fig, ax = plt.subplots(figsize=(10, 4), layout="constrained") + fig, ax = plt.subplots(figsize=(10, 4.5), layout="constrained") plot_stacked_area_steplike(ax, pos, colors) plot_stacked_area_steplike(ax, neg, colors) @@ -134,7 +134,7 @@ def plot_energy_balance_timeseries( # half the labels because pos and neg create duplicate labels handles, labels = ax.get_legend_handles_labels() half = int(len(handles) / 2) - fig.legend(handles=handles[:half], labels=labels[:half], loc="outside right upper") + fig.legend(handles=handles[:half], labels=labels[:half], loc="outside right upper", fontsize=6) ax.axhline(0, color="grey", linewidth=0.5) @@ -172,12 +172,12 @@ def plot_energy_balance_timeseries( snakemake = mock_snakemake( "plot_balance_timeseries", simpl="", - clusters=100, - ll="v1.5", + clusters=115, + ll="vopt", opts="", - sector_opts="Co2L0-2190SEG-T-H-B-I-S-A", + sector_opts="imp", planning_horizons=2050, - configfiles="../../config/config.100n-seg.yaml", + configfiles="config/config.20240826-z1.yaml", ) ensure_output_dir_exists(snakemake) @@ -191,10 +191,11 @@ def plot_energy_balance_timeseries( formatter=lambda x: x.strftime("%Y-%m") ) - balance = n.statistics.energy_balance(aggregate_time=False) + balance = n.statistics.energy_balance(aggregate_time=False, nice_names=False) n.carriers.color.update(snakemake.config["plotting"]["tech_colors"]) - colors = n.carriers.color.rename(n.carriers.nice_name) + colors = n.carriers.color#.rename(n.carriers.nice_name) + colors = colors.replace("", "grey") # wrap in function for multiprocessing def process_group(group, carriers, balance, months, colors): diff --git a/scripts/plot_dh_share.py b/scripts/plot_dh_share.py new file mode 100644 index 000000000..ea7bdb00a --- /dev/null +++ b/scripts/plot_dh_share.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2023- Fabian Neumann +# +# SPDX-License-Identifier: MIT +""" +Plot district heating shares. +""" + +import cartopy +import cartopy.crs as ccrs +import geopandas as gpd +import matplotlib.pyplot as plt +import pandas as pd + + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake( + "plot_dh_share", + clusters=115, + planning_horizons=2050, + configfiles=["config/config.20240826-z1.yaml"], + ) + + plt.style.use(snakemake.input.rc) + + dh_share = pd.read_csv(snakemake.input.dh_share, index_col=0)["district fraction of node"].mul(100) # % + + regions = gpd.read_file(snakemake.input.regions_onshore).set_index("name") + + crs = ccrs.EqualEarth() + + regions = regions.to_crs(crs.proj4_init) + + fig, ax = plt.subplots(figsize=(7, 7), subplot_kw={"projection": crs}) + + ax.add_feature(cartopy.feature.COASTLINE.with_scale("50m"), linewidth=0.5, zorder=2) + ax.add_feature(cartopy.feature.BORDERS.with_scale("50m"), linewidth=0.5, zorder=2) + + regions.plot( + ax=ax, + column=dh_share.reindex(regions.index), + cmap="Reds", + legend=True, + linewidth=0.5, + vmax=60, + edgecolor="grey", + legend_kwds={ + "label": "district heating share of residential/services heat demand [%]", + "shrink": 0.7, + "extend": "max", + }, + ) + + plt.xlim(-1e6, 2.6e6) + plt.ylim(4.3e6, 7.8e6) + + ax.axis("off") + + for fn in snakemake.output: + plt.savefig(fn) \ No newline at end of file diff --git a/scripts/plot_fixed_demand_totals.py b/scripts/plot_fixed_demand_totals.py new file mode 100644 index 000000000..51370022a --- /dev/null +++ b/scripts/plot_fixed_demand_totals.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2023- Fabian Neumann +# +# SPDX-License-Identifier: MIT +""" +Plot fixed demand totals. +""" + +import matplotlib.pyplot as plt +import pypsa +from pypsa.descriptors import get_switchable_as_dense as as_dense + +COLUMNS = { + 'EV battery': "electricity", + 'H2': "hydrogen", + 'NH3': "ammonia", + 'agriculture machinery oil': "oil", + 'gas for industry': "methane", + 'industry methanol': "methanol", + 'kerosene for aviation': "oil", + 'low voltage': "electricity", + 'naphtha for industry': "oil", + 'rural heat': "heat", + 'shipping methanol': "methanol", + 'solid biomass for industry': "biomass", + 'steel': "steel", + 'urban central heat': "heat", + 'urban decentral heat': "heat", + '': "electricity", +} + +INDEX = { + 'H2 for industry': "industry", + 'NH3': "fertilizers", + 'agriculture electricity': "agriculture", + 'agriculture heat': "agriculture", + 'agriculture machinery oil': "agriculture", + 'electricity': "residential/services", + 'gas for industry': "industry", + 'industry electricity': "industry", + 'industry methanol': "industry", + 'kerosene for aviation': "aviation", + 'land transport EV': "road transport", + 'low-temperature heat for industry': "industry", + 'naphtha for industry': "feedstock", + 'rural heat': "rural heat", + 'shipping methanol': "shipping", + 'solid biomass for industry': "industry", + 'steel': "industry", + 'urban central heat': "urban heat", + 'urban decentral heat': "urban heat", +} + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake( + "plot_fixed_demand_totals", + simpl="", + clusters=115, + ll="vopt", + opts="", + sector_opts="", + planning_horizons=2050, + configfiles="config/config.20240826-z1.yaml", + ) + + plt.style.use(["bmh", snakemake.input.rc]) + + n = pypsa.Network(snakemake.input.network) + + w = n.snapshot_weightings.generators + loads = w @ as_dense(n, "Load", "p_set") + + stack = ( + loads.groupby([n.loads.carrier, n.loads.bus.map(n.buses.carrier)]) + .sum() + .div(1e6) + .unstack() + ) + + stack = stack.drop("process emissions", axis=0).drop("process emissions", axis=1) + + stack = stack.groupby(INDEX).sum().T.groupby(COLUMNS).sum() + + stack.loc["steel"] *= 2.1 + + stack = stack.loc[stack.sum(axis=1).sort_values().index] + + fig, ax = plt.subplots(figsize=(8, 2.8)) + + stack.plot.barh(ax=ax, stacked=True, width=0.8) + + ax.set_xlabel("Final energy and non-energy demand [TWh/a]") + ax.set_ylabel("") + + ax.grid(False) + + ax.legend(loc='center left', bbox_to_anchor=(1, 0.5), title="Used for...") + + + def fmt(x): + return f"{x:.0f}" if x > 200 else "" + + for c in ax.containers: + ax.bar_label(c, label_type="center", color='white', fmt=fmt) + + plt.tight_layout() + + ax.set_xlim(-50, 5000) + ax.set_xticks(range(0, 5001, 500)) + ax.set_xticks(range(0, 5001, 250), minor=True) + + + for i, (idx, total) in enumerate(stack.sum(axis=1).items()): + ax.text(total + 50, i, f"{total:.0f}", va='center') + + for fn in snakemake.output: + plt.savefig(fn, bbox_inches="tight") diff --git a/scripts/plot_gas_network_unclustered.py b/scripts/plot_gas_network_unclustered.py index c07c57a97..08a3ac99f 100644 --- a/scripts/plot_gas_network_unclustered.py +++ b/scripts/plot_gas_network_unclustered.py @@ -14,14 +14,16 @@ import numpy as np import pandas as pd from build_gas_input_locations import build_gas_input_locations -from matplotlib.ticker import LogFormatter from shapely import wkt if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake - snakemake = mock_snakemake("plot_gas_network_unclustered") + snakemake = mock_snakemake( + "plot_gas_network_unclustered", + configfiles="config/config.20240826-z1.yaml", + ) plt.style.use(snakemake.input.rc) @@ -68,8 +70,8 @@ df.plot( ax=ax, - column=df["p_nom"].div(1e3).fillna(0.0), - linewidths=np.log(df.p_nom.div(df.p_nom.min())).fillna(0.0).div(3), + column=df["p_nom_diameter"].div(1e3).fillna(0.0), + linewidths=np.log(df.p_nom_diameter.div(df.p_nom_diameter.min())).fillna(0.0).div(3), cmap="Spectral_r", vmax=vmax, legend=True, diff --git a/scripts/plot_heatmap_timeseries.py b/scripts/plot_heatmap_timeseries.py index 945285dc5..3a444446c 100644 --- a/scripts/plot_heatmap_timeseries.py +++ b/scripts/plot_heatmap_timeseries.py @@ -127,12 +127,12 @@ def plot_heatmap( snakemake = mock_snakemake( "plot_heatmap_timeseries", simpl="", - clusters=100, - ll="v1.5", + clusters=115, + ll="vopt", opts="", - sector_opts="Co2L0-2190SEG-T-H-B-I-S-A", + sector_opts="imp", planning_horizons=2050, - configfiles="../../config/config.100n-seg.yaml", + configfiles="config/config.20240826-z1.yaml", ) plt.style.use(["bmh", snakemake.input.rc]) diff --git a/scripts/plot_heatmap_timeseries_resources.py b/scripts/plot_heatmap_timeseries_resources.py index 04868bf0a..58b8f280e 100644 --- a/scripts/plot_heatmap_timeseries_resources.py +++ b/scripts/plot_heatmap_timeseries_resources.py @@ -24,12 +24,12 @@ snakemake = mock_snakemake( "plot_heatmap_timeseries_resources", simpl="", - clusters=100, - ll="v1.5", + clusters=115, + ll="vopt", opts="", - sector_opts="Co2L0-2190SEG-T-H-B-I-S-A", + sector_opts="", planning_horizons=2050, - configfiles="../../config/config.100n-seg.yaml", + configfiles="config/config.20240826-z1.yaml", ) plt.style.use(["bmh", snakemake.input.rc]) diff --git a/scripts/plot_import_options.py b/scripts/plot_import_options.py index eec096a1c..b702cf5a5 100644 --- a/scripts/plot_import_options.py +++ b/scripts/plot_import_options.py @@ -25,32 +25,32 @@ cc = coco.CountryConverter() -# for EU: https://ec.europa.eu/eurostat/databrowser/view/prc_hicp_aind__custom_9900786/default/table?lang=en -# EUR_2015_TO_2020 = 1.002 * 1.017 * 1.019 * 1.015 * 1.007 NICE_NAMES = { "pipeline-h2": r"H$_2$ (pipeline)", "shipping-lh2": "H$_2$ (ship)", "shipping-lnh3": "ammonia", - "shipping-lch4": "methane", "shipping-meoh": "methanol", + "shipping-lch4": "methane", "shipping-ftfuel": "Fischer-Tropsch", + "shipping-hbi": "HBI", "shipping-steel": "steel", } PALETTE = { - "Argentina": "#74acdf", - "Algeria": "#d21034", - "Namibia": "#003580", + "Namibia": "#74acdf", + "Western Sahara": "#d21034", + "Australia": "#003580", "Saudi Arabia": "#006c35", "Chile": "darkorange", - "Other": "#aaa", + "Algeria": "#6accb8", + "Other": "#bbb", } def create_stripplot(ic, ax): - order = list(NICE_NAMES.values())[:-1] + order = pd.Index(NICE_NAMES.values())[:-1].intersection(ic.esc.unique()) minimums = ( ic.groupby("esc").value.min().round(1)[order].reset_index(drop=True).to_dict() ) @@ -59,11 +59,23 @@ def create_stripplot(ic, ax): ) sns.stripplot( - data=ic, + data=ic.query("exporter_base == 'Other'"), + x="esc", + y="value", + alpha=0.3, + hue="exporter_base", + jitter=0.28, + palette=PALETTE, + ax=ax, + order=order, + size=2, + ) + sns.stripplot( + data=ic.query("exporter_base != 'Other'"), x="esc", y="value", - alpha=0.6, - hue="exporter", + alpha=0.5, + hue="exporter_base", jitter=0.28, palette=PALETTE, ax=ax, @@ -103,7 +115,7 @@ def create_stripplot(ic, ax): ax.text(x, y - 10, str(y), ha="center", va="bottom", fontsize=9) for x, y in maximums.items(): ax.text(x, y + 5, str(y), ha="center", va="bottom", fontsize=9) - ax.legend(title="", ncol=1, loc=(0.55, 0.05), labelspacing=0.3, frameon=False) + ax.legend(title="", ncol=1, loc=(0.45, 0.05), labelspacing=0.3, frameon=False) for spine in ax.spines.values(): spine.set_visible(False) @@ -116,11 +128,11 @@ def create_stripplot(ic, ax): "plot_import_options", simpl="", opts="", - clusters="110", + clusters="115", ll="vopt", - sector_opts="Co2L0-2190SEG-T-H-B-I-S-A-imp+AC", + sector_opts="imp", planning_horizons="2050", - configfiles="../../config/config.20231025-zecm.yaml", + configfiles="config/config.20240826-z1.yaml", ) configure_logging(snakemake) @@ -128,7 +140,7 @@ def create_stripplot(ic, ax): plt.style.use(["bmh", snakemake.input.rc]) # dummy output if no imports considered - if "-imp" not in snakemake.wildcards.sector_opts: + if "imp" not in snakemake.wildcards.sector_opts: import sys fig, ax = plt.subplots() @@ -138,11 +150,11 @@ def create_stripplot(ic, ax): tech_colors = snakemake.config["plotting"]["tech_colors"] tech_colors["lng"] = "#e37959" - tech_colors["pipeline"] = "#86cfbc" + tech_colors["pipeline"] = "#6f84d6" crs = ccrs.EqualEarth() - bus_size_factor = 7.5e4 + bus_size_factor = 6e4 n = pypsa.Network(snakemake.input.network) assign_location(n) @@ -159,12 +171,9 @@ def create_stripplot(ic, ax): inputs.loc[inputs.index.str.contains(pattern), "pipeline"] = 0.0 inputs = inputs.stack() - # TODO size external nodes according to wind and solar potential - - h2_cost = n.generators.filter(regex="import (pipeline-h2|shipping-lh2)", axis=0) + h2_cost = n.links.filter(regex="import (pipeline-h2|shipping-lh2)", axis=0) regions["marginal_cost"] = ( - h2_cost.groupby(h2_cost.bus.map(n.buses.location)).marginal_cost.min() - # * EUR_2015_TO_2020 + h2_cost.groupby(h2_cost.bus1.str.split(" ").str[:2].str.join(" ")).marginal_cost.min() ) # patch network @@ -175,12 +184,28 @@ def create_stripplot(ic, ax): if "CN-West" in n.buses.index: n.buses.loc["CN-West", "x"] = 79 n.buses.loc["CN-West", "y"] = 38 - for ct in n.buses.index.intersection({"MA", "DZ", "TN", "LY", "EG", "SA"}): + for ct in n.buses.index.intersection({"DZ", "LY", "EG", "SA"}): n.buses.loc[ct, "y"] += 2 + n.buses.loc["MA", "x"] -= 2.4 + n.buses.loc["MA", "x"] -= 1 + to_remove = ["MR", "EH", "OM", "UZ", "CN-West", "TM", "MN"] + n.mremove("Link", n.links.query("bus0 in @to_remove").index) + # auxiliary buses for xlinks + n.add("Bus", "Atlantic", x=-10, y=45) + n.add("Bus", "Kaukasus", x=49, y=40.5) + n.links.loc[(n.links.bus0 == "MA") & ~(n.links.bus1.str.startswith("ES")) & ~(n.links.bus1.str.startswith("PT")), "bus0"] = "Atlantic" + n.links.loc[n.links.bus0 == "KZ", "bus0"] = "Kaukasus" + n.madd( + "Link", + ["Kausasus", "Atlantic"], + suffix=" import hvdc-to-elec auxiliary", + bus0=["KZ", "MA"], + bus1=["Kaukasus", "Atlantic"] + ) link_colors = pd.Series( n.links.index.map( - lambda x: "seagreen" if "import hvdc-to-elec" in x else "#b18ee6" + lambda x: "#d257d9" if "import hvdc-to-elec" in x else "#ab97c9" ), index=n.links.index, ) @@ -199,14 +224,13 @@ def create_stripplot(ic, ax): [pd.Series(0.4, index=mi), inputs.div(bus_size_factor)], axis=0 ) - df = pd.read_csv(snakemake.input.imports, sep=";", keep_default_na=False) + df = pd.read_parquet(snakemake.input.imports).reset_index().query("scenario == 'default' and year == 2040") - df["exporter"] = df.exporter.replace("", "NA") - ic = df.query("subcategory == 'Cost per MWh delivered' and esc != 'hvdc-to-elec'") - ic["exporter"] = ic.exporter.str.split("-").str[0] + ic = df.query("subcategory == 'Cost per MWh delivered' and esc != 'hvdc-to-elec'").copy() + ic["exporter_base"] = ic.exporter.str.split("-").str[0] - highlighted_countries = ["DZ", "AR", "SA", "CL"] - ic["exporter"] = ic.exporter.apply( + highlighted_countries = ["AU", "NA", "SA", "EH", "CL", "DZ"] + ic["exporter_base"] = ic.exporter_base.apply( lambda x: ( cc.convert(names=x, to="name_short") if x in highlighted_countries @@ -215,7 +239,6 @@ def create_stripplot(ic, ax): ) ic["esc"] = ic.esc.map(NICE_NAMES) - # ic["value"] *= EUR_2015_TO_2020 fig, ax = plt.subplots(subplot_kw={"projection": crs}, figsize=(10.5, 14)) @@ -224,7 +247,7 @@ def create_stripplot(ic, ax): color_geomap={"ocean": "white", "land": "#efefef"}, bus_sizes=bus_sizes_plain, bus_colors=tech_colors, - line_colors="#b18ee6", + line_colors="#ab97c9", line_widths=1, link_widths=1, link_colors=link_colors, @@ -235,18 +258,20 @@ def create_stripplot(ic, ax): regions.plot( ax=ax, column="marginal_cost", - cmap="Blues_r", - edgecolor="#ddd", + cmap="viridis_r", + edgecolor="#ccc", linewidths=0.5, - vmin=50, - vmax=100, + vmin=70, + vmax=110, legend=True, legend_kwds={ "label": r"H$_2$ import cost [€/MWh]", "shrink": 0.53, "pad": 0.015, "aspect": 35, + "extend": "both", }, + missing_kwds=dict(color="#ddd", hatch="..", label="no imports"), ) names = { @@ -262,10 +287,10 @@ def create_stripplot(ic, ax): "pipeline entry", ] colors = [tech_colors[c] for c in names.keys()] + [ - "seagreen", - "#b18ee6", + "#d257d9", + "#ab97c9", "#e37959", - "#86cfbc", + "#6f84d6", ] legend_kw = dict( @@ -286,13 +311,13 @@ def create_stripplot(ic, ax): ) legend_kw = dict( - loc=(0.623, 0.775), + loc=(0.6, 0.76), frameon=True, title="existing gas import capacity", ncol=3, labelspacing=1.1, framealpha=1, - borderpad=0.5, + borderpad=1, facecolor="white", ) @@ -300,7 +325,7 @@ def create_stripplot(ic, ax): ax, [10e3 / bus_size_factor, 50e3 / bus_size_factor, 100e3 / bus_size_factor], ["10 GW", "50 GW", "100 GW"], - patch_kw=dict(facecolor="#86cfbc"), + patch_kw=dict(facecolor="#6f84d6"), legend_kw=legend_kw, ) diff --git a/scripts/plot_import_sankey.py b/scripts/plot_import_sankey.py new file mode 100644 index 000000000..86c7fd485 --- /dev/null +++ b/scripts/plot_import_sankey.py @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2023 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT +""" +Creates Sankey charts of import flows. +""" + +import logging + +import country_converter as coco +import numpy as np +import pandas as pd +import plotly.graph_objects as go +import pypsa +from _helpers import configure_logging +from plotly.io import write_image +from plotly.subplots import make_subplots +import plotly.io as pio + +pio.kaleido.scope.mathjax = None + +logger = logging.getLogger(__name__) + +cc = coco.CountryConverter() + +TECH_COLORS = { + "import pipeline-h2": "#db8ccd", + "import shipping-lh2": "#e0c5dc", + "import shipping-lnh3": "#e2ed74", + "import shipping-lch4": "#f7a572", + "import shipping-ftfuel": "#93eda2", + "import shipping-meoh": "#87d0e6", + "import shipping-hbi": "#d3dceb", + "import shipping-steel": "#94a4be", + "import hvdc-to-elec": "#91856a", +} + +NICE_NAMES = { + "import hvdc-to-elec": "electricity", + "import pipeline-h2": "hydrogen (pipeline)", + "import shipping-lh2": "hydrogen (ship)", + "import shipping-ftfuel": "Fischer-Tropsch", + "import shipping-meoh": "methanol", + "import shipping-lch4": "methane (ship)", + "import shipping-lnh3": "ammonia", + "import shipping-steel": "steel", + "import shipping-hbi": "HBI", +} + + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + + snakemake = mock_snakemake( + "plot_import_sankey", + opts="", + clusters="115", + ll="vopt", + sector_opts="imp+AC+H20.9+CH40.9+NH30.9+FT0.9+MeOH0.9+HBI0.9+St0.9", + planning_horizons="2050", + configfiles="config/config.20240826-z1.yaml", + ) + + configure_logging(snakemake) + + n = pypsa.Network(snakemake.input.network) + df = ( + n.snapshot_weightings.generators @ n.links_t.p0.T.groupby( + [n.links.carrier, n.links.bus0.str.replace(" export", ""), n.links.bus1.str[:2]] + ).sum().T.filter(like="import").div(1e6) + ) + + df = df.loc[~df.index.get_level_values("carrier").str.contains("infrastructure")] + df.name = "value" + df = df.reset_index(level="carrier").reset_index() + df.rename(columns=dict(bus0="source", bus1="target", carrier="label"), inplace=True) + df = df.loc[df["value"] > 1] + + df["source"] = cc.convert(df.source.str.split("-").str[0], to="short_name") + df["target"] = cc.convert( + df.target.str.split("-").str[0], to="short_name", not_found="Anywhere in Europe" + ) + country_aggregations = { + "Tunisia": "Maghreb", + "Algeria": "Maghreb", + "Western Sahara": "Maghreb", + "Morocco": "Maghreb", + "Chile": "South America", + "Argentina": "South America", + "Libya": "Mashreq", + "Egypt": "Mashreq", + } + df["source"] = df["source"].replace(country_aggregations) + + + aggregate = df.groupby("target").value.sum() < 10 + aggregate = aggregate[aggregate].index.tolist() + df["target"] = df.target.map(lambda x: "Other" if x in aggregate else x) + + import_volumes = df.groupby("target").value.sum().round(1) + export_volumes = df.groupby("source").value.sum().round(1) + + df["source"] = df["source"].map( + lambda x: f"{x} ({export_volumes[x]} TWh)" + ) + df["target"] = df["target"].map( + lambda x: f"{x} ({import_volumes[x]} TWh)" + ) + + labels = np.unique(df[["source", "target"]]) + nodes = pd.Series(range(len(labels)), index=labels) + + link_colors = df["label"].map(TECH_COLORS) + + fig = make_subplots(specs=[[{"secondary_y": True}]]) + + fig.add_trace( + go.Sankey( + arrangement="snap", + valuesuffix=" TWh", + valueformat=".1f", + node=dict( + pad=4, + thickness=10, + label=labels, + color="#bbb", + line=dict(color="black", width=0.7), + ), + link=dict( + source=df.source.map(nodes), + target=df.target.map(nodes), + value=df.value, + label=df.label, + color=link_colors, + ), + ) + ) + + for label, color in TECH_COLORS.items(): + fig.add_trace( + go.Scatter( + x=[None], + y=[None], + mode="markers", + marker=dict(size=10, color=color), + name=NICE_NAMES[label], + legendgroup=NICE_NAMES[label], + showlegend=True, + ), + secondary_y=False, + ) + + axis_kwargs = dict(showgrid=False, zeroline=False, showticklabels=False) + fig.update_layout( + height=500, + width=400, + margin=dict(l=20, r=20, t=20, b=20), + font=dict(family="Helvetica, Arial", color="black"), + paper_bgcolor="rgba(255,255,255,0.1)", + plot_bgcolor="rgba(255,255,255,0.1)", + legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1), + xaxis=axis_kwargs, + yaxis=axis_kwargs, + xaxis2=axis_kwargs, + yaxis2=axis_kwargs, + ) + + write_image(fig, snakemake.output[0]) diff --git a/scripts/plot_import_shares.py b/scripts/plot_import_shares.py index c32242e1e..caa6b6800 100644 --- a/scripts/plot_import_shares.py +++ b/scripts/plot_import_shares.py @@ -22,6 +22,7 @@ "gas": "methane (ship)", "methanol": "methanol (ship)", "oil": "Fischer-Tropsch (ship)", + "HBI": "HBI (ship)", "steel": "steel (ship)", } @@ -33,6 +34,7 @@ "methanol (ship)": "import shipping-meoh", "Fischer-Tropsch (ship)": "import shipping-ftfuel", "steel (ship)": "import shipping-steel", + "HBI (ship)": "import shipping-hbi", } THRESHOLD = 1 # MWh @@ -43,13 +45,12 @@ snakemake = mock_snakemake( "plot_import_shares", - simpl="", opts="", - clusters="110", + clusters="115", ll="vopt", - sector_opts="Co2L0-2190SEG-T-H-B-I-S-A-onwind+p0.5-imp", + sector_opts="imp", planning_horizons="2050", - configfiles="../../config/config.20231025-zecm.yaml", + configfiles="config/config.20240826-z1.yaml", ) configure_logging(snakemake) @@ -60,7 +61,7 @@ n = pypsa.Network(snakemake.input.network) - eb = n.statistics.energy_balance().groupby(["carrier", "bus_carrier"]).sum() + eb = n.statistics.energy_balance(nice_names=False).groupby(["carrier", "bus_carrier"]).sum() eb = ( eb.unstack(1) .groupby(lambda x: "import" if "import" in x or "external" in x else x) @@ -74,7 +75,7 @@ imp_mix = ie.loc["import"].copy() if "steel" in imp_mix.index: - imp_mix.loc["steel"] *= 2.1 # kWh/kg + imp_mix.loc[["steel", "HBI"]] *= 2.1 # kWh/kg imp_mix = pd.DataFrame( imp_mix.where(imp_mix > 1e-3) .dropna() @@ -88,12 +89,14 @@ ) sel = list(CARRIERS.keys())[::-1] ie_rel = ie_rel[sel].rename(columns=CARRIERS).T + if ie["HBI"].sum() < 0.5: + ie_rel.loc["HBI (ship)"] = 0. ie_rel.index = ie_rel.index.str.split(" ").str[0] - ie_rel.plot.barh(stacked=True, ax=ax, color=["lightseagreen", "coral"]) + ie_rel.plot.barh(stacked=True, ax=ax, width=0.8, color=["lightseagreen", "coral"]) ax.set_ylabel("") ax.set_xlabel("domestic share [%]", fontsize=11, color="lightseagreen") - ax.legend(ncol=2, bbox_to_anchor=(0.3, 1.25)) + ax.legend(ncol=2, bbox_to_anchor=(0.55, 1.35)) ax.grid(axis="y") ax.set_xlim(0, 100) @@ -101,11 +104,15 @@ ax=ax_mix, stacked=True, legend=True, + width=0.9, color=[tech_colors[COLOR_MAPPING[i]] for i in imp_mix.index], ) for i, (carrier, twh) in enumerate(ie_sum[sel].items()): - unit = "Mt" if carrier.lower().startswith("steel") else "TWh" + if carrier.lower().startswith("steel") or carrier.lower().startswith("hbi"): + unit = "Mt" + else: + unit = "TWh" ax.text(119, i, f"{twh} {unit}", va="center", ha="right", color="slateblue") ax.text(119, i + 1.5, "total\nsupply", va="center", ha="right", color="slateblue") @@ -117,9 +124,10 @@ "top", functions=(lambda x: x / total_imp * 100, lambda x: x * total_imp * 100) ) - ax_mix.text(total_imp * 1.1, -0.75, "TWh", va="center") - ax_mix.text(total_imp * 1.1, 0.75, "%", va="center") - ax_mix.legend(ncol=3, bbox_to_anchor=(1.18, -0.3), title="") + ax_mix.text(total_imp * 1.03, 0, str(total_imp.astype(int)) + " TWh", va="center", ha="left", color="slateblue") + ax_mix.text(total_imp * 1.1, -1.05, "TWh", va="center") + ax_mix.text(total_imp * 1.1, 1.05, "%", va="center") + ax_mix.legend(ncol=3, bbox_to_anchor=(1.2, -0.4), title="") ticks = range(10, 100, 20) ax.set_xticks(ticks, minor=True) @@ -142,7 +150,7 @@ def fmt(x): ax.bar_label(container, label_type="center", color="white", fmt=fmt) def fmt(x): - return f"{x:.0f}" if x > 200 else "" + return f"{x:.0f}" if x > 80 else "" for container in ax_mix.containers: ax_mix.bar_label(container, label_type="center", color="white", fmt=fmt) diff --git a/scripts/plot_import_supply_curve.py b/scripts/plot_import_supply_curve.py new file mode 100644 index 000000000..762b11b62 --- /dev/null +++ b/scripts/plot_import_supply_curve.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2023 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT +""" +Creates stacked bar charts of import shares per carrier. +""" + +import logging + +logger = logging.getLogger(__name__) + +import matplotlib.pyplot as plt +import pandas as pd +from _helpers import configure_logging + +from plot_import_world_map import rename + +TECH_MAPPING = { + "hvdc-to-elec": "electricity (HVDC)", + "pipeline-h2": "hydrogen (pipeline)", + "shipping-lh2": "hydrogen (ship)", + "shipping-lnh3": "ammonia (ship)", + "shipping-lch4": "methane (ship)", + "shipping-meoh": "methanol (ship)", + "shipping-ftfuel": "Fischer-Tropsch (ship)", + "shipping-hbi": "HBI (ship)", + "shipping-steel": "steel (ship)", +} + +import country_converter as coco +cc = coco.CountryConverter() + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake( + "plot_import_supply_curve", + configfiles="config/config.20240826-z1.yaml", + ) + + configure_logging(snakemake) + + plt.style.use(["bmh", snakemake.input.rc]) + + tech_colors = snakemake.config["plotting"]["tech_colors"] + + COLORS = { + "wind": tech_colors["onwind"], + "solar": tech_colors["solar"], + "battery": tech_colors["battery"], + "electrolysis": tech_colors["H2 Electrolysis"], + "fuel storage": '#ffd4dc', + "hydrogen conversion": tech_colors["Fischer-Tropsch"], + "direct air capture": tech_colors["DAC"], + "iron ore": "#4e4f55", + "direct iron reduction": tech_colors["steel"], + "electric arc furnace": "#8795a8", + "evaporation/liquefaction": "#8487e8", + "transport": "#e0ae75", + } + + for esc, esc_nice_name in TECH_MAPPING.items(): + + print(esc_nice_name) + + production = 100e6 if esc in ["shipping-steel", "shipping-hbi"] else 500e6 + + df = pd.read_parquet(snakemake.input.imports).reset_index().query("scenario == 'default' and year == 2040 and category == 'cost' and esc == @esc") + + df.drop(["scenario", "year", "wacc", "esc", "category"], axis=1, inplace=True) + + df["subcategory"] = df["subcategory"].apply(rename) + + df = df.groupby(["exporter", "importer", "subcategory"]).value.sum().div(production).unstack("importer").min(axis=1).unstack("subcategory") + + cols = pd.Index(COLORS.keys()).intersection(df.columns) + df = df.loc[df.sum(axis=1).sort_values(ascending=False).index, cols].reindex(COLORS.keys(), axis=1) + + split_index = df.index.str.split("-") + suffix = split_index.str.get(1).fillna("").map(lambda x: f" ({x})" if x else x) + prefix = pd.Index(cc.convert(split_index.str[0], to="short_name")).map(lambda x: "USA" if x == "United States" else x) + df.index = prefix + suffix + + fig, ax = plt.subplots(figsize=(5, 11)) + + df.plot.barh(ax=ax, stacked=True, color=COLORS, width=0.85) + + for i, (idx, row) in enumerate(df.iterrows()): + sum_value = row.sum() + ax.text(sum_value + 5, i, f"{sum_value:.1f}", va='center', ha='left', fontsize=8) + + handles, labels = ax.get_legend_handles_labels() + handles.reverse() + labels.reverse() + ax.legend(handles, labels, title="", ncol=2, loc=(-0.55, 1.05)) + limit = 800 if esc in ["shipping-steel", "shipping-hbi"] else 200 + ax.set_xlim(0, limit + int(limit / 8)) + ax.set_xticks(range(0, limit + 1, int(limit / 4))) + ax.set_xticks(range(int(limit / 8), limit + 1, int(limit / 8)), minor=True) + ax.set_xlabel(esc_nice_name + (" [€/t]" if esc in ["shipping-steel", "shipping-hbi"] else " [€/MWh]"), fontsize=10) + ax.set_ylabel("") + ax.grid(False, axis="both") + + # Mirror the xticks on the top side of the axis + ax2 = ax.twiny() + ax2.set_xlim(ax.get_xlim()) + ax2.set_xticks(ax.get_xticks()) + ax2.set_xticks(ax.get_xticks(minor=True), minor=True) + ax2.set_xlabel(ax.get_xlabel(), fontsize=10) + ax2.grid(False, axis="both") + + plt.tight_layout() + + for fn in snakemake.output[esc.replace("-", "_")]: + plt.savefig(fn, bbox_inches="tight") diff --git a/scripts/plot_import_world_map.py b/scripts/plot_import_world_map.py index a60175f9b..22e8f4539 100644 --- a/scripts/plot_import_world_map.py +++ b/scripts/plot_import_world_map.py @@ -19,22 +19,37 @@ import xarray as xr from _helpers import configure_logging from atlite.gis import ExclusionContainer, shape_availability -from pypsa.plot import add_legend_patches +from matplotlib.patches import Patch from rasterio.features import geometry_mask from rasterio.plot import show from shapely.geometry import box + +def add_legend_patches(ax, colors, labels, hatches, patch_kw=None, legend_kw=None): + colors = np.atleast_1d(colors) + labels = np.atleast_1d(labels) + if patch_kw is None: + patch_kw = {} + if legend_kw is None: + legend_kw = {} + + if len(colors) != len(labels): + msg = "Colors and labels must have the same length." + raise ValueError(msg) + + handles = [Patch(facecolor=c, hatch=h, **patch_kw) for c, h in zip(colors, hatches)] + + legend = ax.legend(handles, labels, **legend_kw) + + ax.get_figure().add_artist(legend) + logger = logging.getLogger(__name__) cc = coco.CountryConverter() -# for EU: https://ec.europa.eu/eurostat/databrowser/view/prc_hicp_aind__custom_9900786/default/table?lang=en -# EUR_2015_TO_2020 = 1.002 * 1.017 * 1.019 * 1.015 * 1.007 - AREA_CRS = "ESRI:54009" -ARROW_COLOR = "#b3c4ad" -ARROW_COLOR = "#ccc" +ARROW_COLOR = "#A0A0A0" NICE_NAMES = { "pipeline-h2": r"H$_2$ (pipeline)", @@ -44,17 +59,39 @@ "shipping-lch4": "methane", "shipping-lnh3": "ammonia", "shipping-steel": "steel", + "shipping-hbi": "HBI", } +def reduce_resolution(gdf, tolerance=0.01, min_area=0.1): + def simplify_and_filter(geom): + simplified = geom.simplify(tolerance) + if simplified.geom_type == 'MultiPolygon': + return gpd.GeoSeries([poly for poly in simplified.geoms if poly.area > min_area]).union_all() + return simplified if simplified.area > min_area else None + + # Create a copy of the input GeoDataFrame + gdf_reduced = gdf.copy() + + # Apply the simplification and filtering + gdf_reduced['geometry'] = gdf_reduced['geometry'].apply(simplify_and_filter) + + # Remove any rows with null geometries + gdf_reduced = gdf_reduced.dropna(subset=['geometry']) + + return gdf_reduced + + def rename(s): if "solar" in s: return "solar" if "wind" in s: return "wind" - if "storage" in s or "inverter" in s: - return "storage" - if "transport" in s or "shipping fuel" in s or "dry bulk" in s or "pipeline" in s: + if "inverter" in s or "battery" in s: + return "battery" + if "storage" in s or "Buffer" in s or "storing" in s: + return "fuel storage" + if "transport" in s or "shipping fuel" in s or "dry bulk" in s or "pipeline" in s or "ship" in s or "HVDC" in s: return "transport" if "evaporation" in s or "liquefaction" in s or "compress" in s: return "evaporation/liquefaction" @@ -81,7 +118,6 @@ def get_cost_composition(df, country, escs, production): composition = ( df.query(query_str).groupby(["esc", "subcategory", "importer"]).value.min() ) - # composition *= EUR_2015_TO_2020 minimal = {} for name, group in composition.groupby("esc"): @@ -132,22 +168,22 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): snakemake = mock_snakemake( "plot_import_world_map", - simpl="", opts="", - clusters="110", + clusters=115, ll="vopt", - sector_opts="Co2L0-2190SEG-T-H-B-I-S-A-imp", + sector_opts="imp", planning_horizons="2050", - configfiles="../../config/config.20231025-zecm.yaml", + configfiles="config/config.20240826-z1.yaml", ) configure_logging(snakemake) import_fn = snakemake.input.imports profile_fn = snakemake.input.profiles - gadm_fn = snakemake.input.gadm_arg[0] - glc_fn = snakemake.input.copernicus_glc[0] + gadm_fn = snakemake.input.gadm_arg + glc_fn = snakemake.input.copernicus_glc wdpa_fn = snakemake.input.wdpa + countries_fn = snakemake.input.countries config = snakemake.config @@ -158,29 +194,29 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): COLORS = { "wind": tech_colors["onwind"], "solar": tech_colors["solar"], - "storage": tech_colors["battery"], + "battery": tech_colors["battery"], "electrolysis": tech_colors["H2 Electrolysis"], - "direct air capture": tech_colors["DAC"], + "fuel storage": '#ffd4dc', "hydrogen conversion": tech_colors["Fischer-Tropsch"], + "direct air capture": tech_colors["DAC"], "iron ore": "#4e4f55", "direct iron reduction": tech_colors["steel"], "electric arc furnace": "#8795a8", "evaporation/liquefaction": "#8487e8", - "transport": "#f7a572", + "transport": "#e0ae75", } # load capacity factor time series ds = xr.open_dataset(profile_fn) - profile = ds.sel(exporter="MA", importer="EUE").p_max_pu.to_pandas().T + profile = ds.sel(exporter="MA", importer="EUSW").p_max_pu.to_pandas().T profile.rename(columns={"onwind": "wind", "solar-utility": "solar"}, inplace=True) # download world country shapes, version clipped to Europe, and GADM in AR - world = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres")).set_index( - "iso_a3" - ) + world = gpd.read_file(countries_fn).set_index("ADM0_A3_DE") world.drop("ATA", inplace=True) + world.rename(index={"KOS": "XKX"}, inplace=True) eu_countries = cc.convert(config["countries"], src="iso2", to="iso3") europe = world.loc[eu_countries] @@ -193,26 +229,33 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): # load import costs - df = pd.read_csv(import_fn, sep=";", keep_default_na=False) - - # bugfix for Namibia - df["exporter"] = df.exporter.replace("", "NA") + df = ( + pd.read_parquet(import_fn) + .reset_index() + .query("scenario == 'default' and year == 2040") + ) import_costs = ( df.query("subcategory == 'Cost per MWh delivered' and esc == 'shipping-meoh'") .groupby("exporter") .value.min() ) - # import_costs *= EUR_2015_TO_2020 - import_costs.index = cc.convert( - import_costs.index.str.split("-").str[0], src="iso2", to="iso3" - ) - import_costs.drop("RUS", inplace=True, errors="ignore") + import_regions = pd.concat( + { + idx: gpd.read_file(f"../data/imports/regions/{idx}.gpkg") + .set_index("index") + .loc["onshore"] + for idx in df.exporter.unique() + } + ) + import_regions.index = import_regions.index.get_level_values(0) + import_regions = gpd.GeoDataFrame(geometry=import_regions, crs=4326) + import_regions = reduce_resolution(import_regions, tolerance=0.1, min_area=0.1) composition_arg = get_cost_composition( df, - "AR", + "AR-South", [ "shipping-lh2", "shipping-ftfuel", @@ -227,7 +270,9 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): df, "SA", ["pipeline-h2", "shipping-lh2"], 500e6 ) - composition_aus = get_cost_composition(df, "AU", ["shipping-steel"], 100e6) + composition_aus = get_cost_composition( + df, "AU-West", ["shipping-hbi", "shipping-steel"], 100e6 + ) # add to legend @@ -244,14 +289,28 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): # main axis: choropleth layer world.to_crs(crs).plot( - column=import_costs.reindex(world.index), linewidth=1, edgecolor="black", ax=ax, - cmap="Greens_r", + color="#eee", + ) + + europe.to_crs(crs).plot( + linewidth=1, + edgecolor="black", + ax=ax, + color="#cbc7f0", + ) + + import_regions.to_crs(crs).plot( + column=import_costs.reindex(import_regions.index), + linewidth=1, + edgecolor="black", + ax=ax, + cmap="viridis_r", legend=True, - vmin=110, - vmax=150, + vmin=120, + vmax=170, legend_kwds=dict( label="Cost for methanol fuel delivered to Europe [€/MWh]", orientation="horizontal", @@ -260,24 +319,19 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): aspect=30, pad=0.01, ), - missing_kwds=dict(color="#eee", label="not considered"), - ) - - europe.to_crs(crs).plot( - linewidth=1, - edgecolor="black", - ax=ax, - color="#cbc7f0", + missing_kwds=dict(color="#ccc", hatch=".."), ) add_legend_patches( ax, - ["#eee", "#cbc7f0"], - ["country not considered for export", "country in European model scope"], + ["#eee", "#ccc", "#cbc7f0"], + ["region not considered for export", "region with excluded ship exports", "region in European model scope"], + ["", "..", ""], legend_kw=dict( bbox_to_anchor=(1, 0), frameon=False, ), + patch_kw=dict(alpha=0.99) ) for spine in ax.spines.values(): @@ -354,12 +408,12 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): ax_arg.legend(handles, labels, title="", ncol=1, fontsize=9, loc=(1, 0)) - ax_arg.set_title("Import costs from\nArgentina to Europe", fontsize=9) + ax_arg.set_title("Import costs from\nSouthern Argentina\nto Europe by ship", fontsize=9) ax_arg.set_xlabel("") - ax_arg.set_ylim(0, 120) - ax_arg.set_yticks(range(0, 121, 20)) - ax_arg.set_yticks(range(10, 121, 20), minor=True) + ax_arg.set_ylim(0, 140) + ax_arg.set_yticks(range(0, 141, 20)) + ax_arg.set_yticks(range(10, 141, 20), minor=True) ax_arg.set_ylabel("€/MWh", fontsize=10) ax_arg.grid(axis="x") for spine in ax_arg.spines.values(): @@ -367,8 +421,8 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): ax.annotate( "", - xy=(0.25, 0.15), - xytext=(0.33, 0.2), + xy=(0.25, 0.1), + xytext=(0.33, 0.15), xycoords="axes fraction", arrowprops=dict( edgecolor=ARROW_COLOR, @@ -390,9 +444,9 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): ax_sau.set_xlabel("") ax_sau.set_ylabel("€/MWh", fontsize=10) ax_sau.grid(axis="x") - ax_sau.set_ylim(0, 100) - ax_sau.set_yticks(range(0, 101, 20)) - ax_sau.set_yticks(range(10, 101, 20), minor=True) + ax_sau.set_ylim(0, 110) + ax_sau.set_yticks(range(0, 120, 20)) + ax_sau.set_yticks(range(10, 120, 20), minor=True) for spine in ax_sau.spines.values(): spine.set_visible(False) @@ -412,7 +466,7 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): # inset: Australia steel imports - ax_aus = ax.inset_axes([0.75, 0.15, 0.018, 0.25]) + ax_aus = ax.inset_axes([0.74, 0.15, 0.036, 0.25]) composition_aus.T.plot.bar(ax=ax_aus, stacked=True, color=COLORS, legend=False) @@ -421,15 +475,16 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): ax_aus.set_xlabel("") ax_aus.set_ylabel("€/tonne", fontsize=10) ax_aus.grid(axis="x") - ax_aus.set_yticks(range(0, 600, 100)) - ax_aus.set_yticks(range(50, 600, 100), minor=True) + ax_aus.set_yticks(range(0, 601, 100)) + ax_aus.set_yticks(range(50, 601, 100), minor=True) + ax_aus.set_ylim(0, 600) for spine in ax_aus.spines.values(): spine.set_visible(False) ax.annotate( "", - xy=(0.77, 0.35), - xytext=(0.815, 0.31), + xy=(0.775, 0.35), + xytext=(0.82, 0.315), xycoords="axes fraction", arrowprops=dict( edgecolor=ARROW_COLOR, @@ -445,7 +500,7 @@ def add_land_eligibility_example(ax, shape, glc_fn, wdpa_fn): ax_land = ax.inset_axes([0.315, 0.08, 0.29, 0.29]) shape.to_crs(crs.proj4_init).plot( - ax=ax, color="none", edgecolor=ARROW_COLOR, linestyle=":", linewidth=1 + ax=ax, color="none", edgecolor="k", linestyle=":", linewidth=1 ) add_land_eligibility_example(ax_land, shape, glc_fn, wdpa_fn) diff --git a/scripts/plot_import_world_map_hydrogen.py b/scripts/plot_import_world_map_hydrogen.py new file mode 100644 index 000000000..059859aa4 --- /dev/null +++ b/scripts/plot_import_world_map_hydrogen.py @@ -0,0 +1,182 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2023-2024 Fabian Neumann +# +# SPDX-License-Identifier: MIT +""" +Creates world map of global green hydrogen export costs. +""" + +import logging + +import cartopy.crs as ccrs +import country_converter as coco +import geopandas as gpd +import matplotlib.pyplot as plt +import pandas as pd +from _helpers import configure_logging +from shapely.geometry import box + + +from plot_import_world_map import add_legend_patches, reduce_resolution + +logger = logging.getLogger(__name__) + +cc = coco.CountryConverter() + +AREA_CRS = "ESRI:54009" + + +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + + snakemake = mock_snakemake( + "plot_import_world_map_hydrogen", + opts="", + clusters=115, + ll="vopt", + sector_opts="imp", + planning_horizons="2050", + configfiles="config/config.20240826-z1.yaml", + ) + + configure_logging(snakemake) + + import_fn = snakemake.input.imports + countries_fn = snakemake.input.countries + + config = snakemake.config + + plt.style.use(["bmh", snakemake.input.rc]) + + # download world country shapes, version clipped to Europe, and GADM in AR + + world = gpd.read_file(countries_fn).set_index("ADM0_A3_DE") + world.drop("ATA", inplace=True) + world.rename(index={"KOS": "XKX"}, inplace=True) + + eu_countries = cc.convert(config["countries"], src="iso2", to="iso3") + europe = world.loc[eu_countries] + + bounding_box = box(-12, 33, 42, 72) + europe = gpd.clip(europe, bounding_box) + + # load import costs + + df = ( + pd.read_parquet(import_fn) + .reset_index() + .query("scenario == 'default' and year == 2040") + ) + + h2_vectors = ["shipping-lh2", "pipeline-h2"] + import_costs = ( + df.query("subcategory == 'Cost per MWh delivered' and esc in @h2_vectors") + .groupby(["exporter", "esc"]) + .value.min() + .unstack() + ) + + import_vectors = import_costs.idxmin(axis=1) + import_costs = import_costs.min(axis=1) + + import_regions = pd.concat( + { + idx: gpd.read_file(f"../data/imports/regions/{idx}.gpkg") + .set_index("index") + .loc["onshore"] + for idx in df.exporter.unique() + } + ) + import_regions.index = import_regions.index.get_level_values(0) + import_regions = gpd.GeoDataFrame(geometry=import_regions, crs=4326) + import_regions = reduce_resolution(import_regions, tolerance=0.1, min_area=0.1) + + # create plot + + crs = ccrs.EqualEarth() + + fig, ax = plt.subplots(figsize=(14, 14), subplot_kw={"projection": crs}) + + # main axis: choropleth layer + + world.to_crs(crs).plot( + linewidth=1, + edgecolor="black", + ax=ax, + color="#eee", + ) + + europe.to_crs(crs).plot( + linewidth=1, + edgecolor="black", + ax=ax, + color="#cbc7f0", + ) + + pipe_i = import_vectors.loc[import_vectors == "pipeline-h2"].index + ship_i = import_vectors.loc[import_vectors != "pipeline-h2"].index + + import_regions.loc[pipe_i].to_crs(crs).plot( + column=import_costs.reindex(import_regions.loc[pipe_i].index), + hatch="..", + linewidth=1, + edgecolor="black", + ax=ax, + cmap="viridis_r", + vmin=75, + vmax=145, + ) + + import_regions.loc[ship_i].to_crs(crs).plot( + column=import_costs.reindex(import_regions.loc[ship_i].index), + linewidth=1, + edgecolor="black", + ax=ax, + cmap="viridis_r", + legend=True, + vmin=75, + vmax=145, + legend_kwds=dict( + label="Cost of hydrogen delivered to Europe [€/MWh]", + orientation="horizontal", + extend="both", + shrink=0.6, + aspect=30, + pad=0.01, + ), + ) + + add_legend_patches( + ax, + ["#eee", "#28b594", "#cbc7f0"], + ["region not considered for export", "hydrogen export by pipeline", "region in European model scope"], + ["", "..", ""], + legend_kw=dict( + bbox_to_anchor=(1, 0), + frameon=False, + ), + patch_kw=dict(alpha=0.99), + ) + + import_regions.representative_point().to_crs(crs).annotate() + + for spine in ax.spines.values(): + spine.set_visible(False) + + ax.set_facecolor("none") + fig.set_facecolor("none") + + ax.text( + 0.93, + 0.01, + "Projection:\nEqual Earth", + transform=ax.transAxes, + fontsize=9, + color="grey", + ) + + plt.tight_layout() + + for fn in snakemake.output: + plt.savefig(fn, bbox_inches="tight") diff --git a/scripts/plot_industrial_sites.py b/scripts/plot_industrial_sites.py index 8d77a79bd..6040c913b 100644 --- a/scripts/plot_industrial_sites.py +++ b/scripts/plot_industrial_sites.py @@ -40,7 +40,7 @@ def prepare_hotmaps_database(): from _helpers import mock_snakemake snakemake = mock_snakemake( - "plot_industrial_sites", configfiles=["../../config/config.test.yaml"] + "plot_industrial_sites", configfiles=["config/config.20240826-z1.yaml"] ) plt.style.use(snakemake.input.rc) @@ -53,7 +53,7 @@ def prepare_hotmaps_database(): hotmaps = hotmaps.cx[-12:30, 35:72] hotmaps = hotmaps.to_crs(crs.proj4_init) - not_represented = ["AL", "BA", "RS", "MK", "ME"] + not_represented = ["AL", "BA", "RS", "MK", "ME", "XK"] missing_countries = countries.loc[countries.index.intersection(not_represented)] fig, ax = plt.subplots(figsize=(8, 8), subplot_kw={"projection": crs}) diff --git a/scripts/plot_power_network_unclustered.py b/scripts/plot_power_network_unclustered.py index 2a9283474..fa06a862e 100644 --- a/scripts/plot_power_network_unclustered.py +++ b/scripts/plot_power_network_unclustered.py @@ -10,19 +10,28 @@ import matplotlib.pyplot as plt import pypsa from matplotlib.lines import Line2D +import seaborn as sns if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake - snakemake = mock_snakemake("plot_power_network_topology") + snakemake = mock_snakemake( + "plot_power_network_unclustered", + configfiles="config/config.20240826-z1.yaml", + ) plt.style.use(snakemake.input.rc) n = pypsa.Network(snakemake.input.network) - w = n.lines.v_nom.div(380) - c = n.lines.v_nom.map({220: "teal", 300: "orange", 380: "firebrick"}) + voltages = n.lines.v_nom.unique() + cmap = sns.color_palette("rainbow", as_cmap=True) + norm = plt.Normalize(voltages.min(), voltages.max()) + color_mapping_dict = {v: cmap(norm(v)) for v in voltages} + + w = n.lines.v_nom.div(380 / 1.5) + c = n.lines.v_nom.map(color_mapping_dict) fig, ax = plt.subplots( figsize=(13, 13), subplot_kw={"projection": ccrs.EqualEarth()} @@ -34,18 +43,30 @@ line_widths=w, line_colors=c, link_colors="royalblue", - link_widths=1, + link_widths=2, ) handles = [ - Line2D([0], [0], color="teal", lw=2), - Line2D([0], [0], color="orange", lw=2), - Line2D([0], [0], color="firebrick", lw=2), - Line2D([0], [0], color="royalblue", lw=2), + Line2D([0], [0], color=color_mapping_dict[220], lw=2), + Line2D([0], [0], color=color_mapping_dict[275], lw=2), + Line2D([0], [0], color=color_mapping_dict[300], lw=2), + Line2D([0], [0], color=color_mapping_dict[330], lw=2), + Line2D([0], [0], color=color_mapping_dict[380], lw=2), + Line2D([0], [0], color=color_mapping_dict[400], lw=2), + Line2D([0], [0], color="royalblue", lw=2), # HVDC remains royalblue ] + plt.legend( handles, - ["HVAC 220 kV", "HVAC 300 kV", "HVAC 380 kV", "HVDC"], + [ + "HVAC 220 kV", + "HVAC 275 kV", + "HVAC 300 kV", + "HVAC 330 kV", + "HVAC 380 kV", + "HVAC 400 kV", + "HVDC", + ], frameon=False, loc=[0.2, 0.85], fontsize=14, diff --git a/scripts/plot_salt_caverns_clustered.py b/scripts/plot_salt_caverns_clustered.py index dfab4ea27..50620d1a1 100644 --- a/scripts/plot_salt_caverns_clustered.py +++ b/scripts/plot_salt_caverns_clustered.py @@ -66,8 +66,8 @@ def plot_salt_caverns_by_node( snakemake = mock_snakemake( "plot_salt_caverns_clustered", - clusters=128, - configfiles=["../../config/config.test.yaml"], + clusters=115, + configfiles="config/config.20240826-z1.yaml", ) plt.style.use(snakemake.input.rc) diff --git a/scripts/plot_salt_caverns_unclustered.py b/scripts/plot_salt_caverns_unclustered.py index 12fcbe53c..b45eb21cc 100644 --- a/scripts/plot_salt_caverns_unclustered.py +++ b/scripts/plot_salt_caverns_unclustered.py @@ -17,7 +17,7 @@ snakemake = mock_snakemake( "plot_salt_caverns_unclustered", - configfiles=["../../config/config.test.yaml"], + configfiles="config/config.20240826-z1.yaml", ) plt.style.use(snakemake.input.rc) diff --git a/scripts/plot_summary.py b/scripts/plot_summary.py index a4d8a2326..473adb7fa 100644 --- a/scripts/plot_summary.py +++ b/scripts/plot_summary.py @@ -46,7 +46,7 @@ def rename_techs(label): # "H2 Electrolysis": "hydrogen storage", # "H2 Fuel Cell": "hydrogen storage", # "H2 pipeline": "hydrogen storage", - "battery": "battery storage", + # "battery": "battery storage", "external H2": "external H2", "external battery": "external battery", "external offwind": "external offwind",