From e7b971e2a5b82a7b5440c46527855fcf7930db08 Mon Sep 17 00:00:00 2001 From: Michael Lindner Date: Mon, 7 Jul 2025 14:00:58 +0200 Subject: [PATCH 01/26] simplify indexing --- scripts/pypsa-de/modify_prenetwork.py | 40 ++++++++++++--------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/scripts/pypsa-de/modify_prenetwork.py b/scripts/pypsa-de/modify_prenetwork.py index 9ead691e4..1b3d77efe 100644 --- a/scripts/pypsa-de/modify_prenetwork.py +++ b/scripts/pypsa-de/modify_prenetwork.py @@ -835,42 +835,38 @@ def aladin_mobility_demand(n): simulation_period_correction_factor = n.snapshot_weightings.objective.sum() / 8760 # oil demand - oil_demand = aladin_demand.Liquids * simulation_period_correction_factor - oil_index = n.loads[ - (n.loads.carrier == "land transport oil") & (n.loads.index.str[:2] == "DE") - ].index - oil_demand.index = [f"{i} land transport oil" for i in oil_demand.index] + oil_demand = pd.Series( + aladin_demand.Liquids * simulation_period_correction_factor, + index=aladin_demand.index + " land transport oil", + ) - profile = n.loads_t.p_set.loc[:, oil_index] + profile = n.loads_t.p_set.loc[:, oil_demand.index] profile /= profile.sum() - n.loads_t.p_set.loc[:, oil_index] = (oil_demand * profile).div( + n.loads_t.p_set.loc[:, oil_demand.index] = (oil_demand * profile).div( n.snapshot_weightings.objective, axis=0 ) # hydrogen demand - h2_demand = aladin_demand.Hydrogen * simulation_period_correction_factor - h2_index = n.loads[ - (n.loads.carrier == "land transport fuel cell") - & (n.loads.index.str[:2] == "DE") - ].index - h2_demand.index = [f"{i} land transport fuel cell" for i in h2_demand.index] + h2_demand = pd.Series( + aladin_demand.Hydrogen * simulation_period_correction_factor, + index=aladin_demand.index + " land transport fuel cell", + ) - profile = n.loads_t.p_set.loc[:, h2_index] + profile = n.loads_t.p_set.loc[:, h2_demand.index] profile /= profile.sum() - n.loads_t.p_set.loc[:, h2_index] = (h2_demand * profile).div( + n.loads_t.p_set.loc[:, h2_demand.index] = (h2_demand * profile).div( n.snapshot_weightings.objective, axis=0 ) # electricity demand - ev_demand = aladin_demand.Electricity * simulation_period_correction_factor - ev_index = n.loads[ - (n.loads.carrier == "land transport EV") & (n.loads.index.str[:2] == "DE") - ].index - ev_demand.index = [f"{i} land transport EV" for i in ev_demand.index] + ev_demand = pd.Series( + aladin_demand.Electricity * simulation_period_correction_factor, + index=aladin_demand.index + " land transport EV", + ) - profile = n.loads_t.p_set.loc[:, ev_index] + profile = n.loads_t.p_set.loc[:, ev_demand.index] profile /= profile.sum() - n.loads_t.p_set.loc[:, ev_index] = (ev_demand * profile).div( + n.loads_t.p_set.loc[:, ev_demand.index] = (ev_demand * profile).div( n.snapshot_weightings.objective, axis=0 ) From 76d967d12270875b54be9290ea08d4391e606b1c Mon Sep 17 00:00:00 2001 From: Michael Lindner Date: Mon, 7 Jul 2025 14:23:21 +0200 Subject: [PATCH 02/26] determine charger capacities like in prepare_sector_network; remove land_transport_electric_share --- Snakefile | 7 +++--- scripts/pypsa-de/modify_prenetwork.py | 36 ++++++++++++++------------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/Snakefile b/Snakefile index 59fc95e0e..bfa089ea7 100644 --- a/Snakefile +++ b/Snakefile @@ -488,9 +488,6 @@ rule modify_prenetwork: must_run=config_provider("must_run"), clustering=config_provider("clustering", "temporal", "resolution_sector"), H2_plants=config_provider("electricity", "H2_plants_DE"), - land_transport_electric_share=config_provider( - "sector", "land_transport_electric_share" - ), onshore_nep_force=config_provider("onshore_nep_force"), offshore_nep_force=config_provider("offshore_nep_force"), shipping_methanol_efficiency=config_provider( @@ -500,6 +497,9 @@ rule modify_prenetwork: shipping_methanol_share=config_provider("sector", "shipping_methanol_share"), mwh_meoh_per_tco2=config_provider("sector", "MWh_MeOH_per_tCO2"), scale_capacity=config_provider("scale_capacity"), + bev_charge_rate=config_provider("sector", "bev_charge_rate"), + bev_energy=config_provider("sector", "bev_energy"), + bev_dsm_availability=config_provider("sector", "bev_dsm_availability"), input: costs_modifications="ariadne-data/costs_{planning_horizons}-modifications.csv", network=resources( @@ -514,7 +514,6 @@ rule modify_prenetwork: aladin_demand=resources( "mobility_demand_aladin_{clusters}_{planning_horizons}.csv" ), - transport_data=resources("transport_data_s_{clusters}.csv"), biomass_potentials=resources( "biomass_potentials_s_{clusters}_{planning_horizons}.csv" ), diff --git a/scripts/pypsa-de/modify_prenetwork.py b/scripts/pypsa-de/modify_prenetwork.py index 1b3d77efe..422196171 100644 --- a/scripts/pypsa-de/modify_prenetwork.py +++ b/scripts/pypsa-de/modify_prenetwork.py @@ -871,37 +871,39 @@ def aladin_mobility_demand(n): ) # adjust BEV charger and V2G capacities - number_cars = pd.read_csv(snakemake.input.transport_data, index_col=0)[ - "number cars" - ].filter(like="DE") - - factor = ( - aladin_demand.number_of_cars - * 1e6 - / ( - number_cars - * snakemake.params.land_transport_electric_share[ - int(snakemake.wildcards.planning_horizons) - ] - ) - ) BEV_charger_i = n.links[ (n.links.carrier == "BEV charger") & (n.links.bus0.str.startswith("DE")) ].index - n.links.loc[BEV_charger_i].p_nom *= pd.Series(factor.values, index=BEV_charger_i) + + # Check that buses in network and aladin_data appear in same order + assert [ + idx.startswith(idx2) for (idx, idx2) in zip(BEV_charger_i, aladin_demand.index) + ] + + # Then directly use .values for assignment + p_nom = ( + aladin_demand.number_of_cars.values * snakemake.params.bev_charge_rate + ) # same logic like in prepare_sector_network + + n.links.loc[BEV_charger_i].p_nom = p_nom V2G_i = n.links[ (n.links.carrier == "V2G") & (n.links.bus0.str.startswith("DE")) ].index if not V2G_i.empty: - n.links.loc[V2G_i].p_nom *= pd.Series(factor.values, index=V2G_i) + n.links.loc[V2G_i].p_nom = p_nom * snakemake.params.bev_dsm_availability dsm_i = n.stores[ (n.stores.carrier == "EV battery") & (n.stores.bus.str.startswith("DE")) ].index + e_nom = ( + aladin_demand.number_of_cars.values + * snakemake.params.bev_energy + * snakemake.params.bev_dsm_availability + ) if not dsm_i.empty: - n.stores.loc[dsm_i].e_nom *= pd.Series(factor.values, index=dsm_i) + n.stores.loc[dsm_i].e_nom = e_nom def add_hydrogen_turbines(n): From bbc4d410565a865bc263b448a3a01618651786f5 Mon Sep 17 00:00:00 2001 From: Michael Lindner Date: Mon, 7 Jul 2025 14:29:06 +0200 Subject: [PATCH 03/26] add PHEV to number of electric cars because they provide charging capacity as well --- scripts/pypsa-de/build_mobility_demand.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/pypsa-de/build_mobility_demand.py b/scripts/pypsa-de/build_mobility_demand.py index c7eacdd88..f804e4349 100644 --- a/scripts/pypsa-de/build_mobility_demand.py +++ b/scripts/pypsa-de/build_mobility_demand.py @@ -30,7 +30,7 @@ def get_transport_data(db, year): transport_demand["Hydrogen"] = 0.0 + 0.0 + 0.0 + 0.0 transport_demand["Liquids"] = 41.81 + 1369.34 + 11.18 + 637.23 transport_demand = transport_demand.div(3.6e-6) # convert PJ to MWh - transport_demand["number_of_cars"] = 0.658407 + transport_demand["number_of_cars"] = 0.658407 + 0.120261 # BEV + PHEV else: df = db[year].loc[snakemake.params.leitmodelle["transport"]] @@ -41,9 +41,10 @@ def get_transport_data(db, year): transport_demand.loc[fuel] += df.get((key, "TWh/yr"), 0.0) transport_demand = transport_demand.mul(1e6) # convert TWh to MWh - transport_demand["number_of_cars"] = df.loc[ - "Stock|Transportation|LDV|BEV", "million" - ] + transport_demand["number_of_cars"] = ( + df.loc["Stock|Transportation|LDV|BEV", "million"] + + df.loc["Stock|Transportation|LDV|PHEV", "million"] + ) return transport_demand From c311dc7887609690bb7e37c12c95dd2132021516 Mon Sep 17 00:00:00 2001 From: Michael Lindner Date: Mon, 7 Jul 2025 15:49:42 +0200 Subject: [PATCH 04/26] syntax fixes --- scripts/pypsa-de/modify_prenetwork.py | 32 ++++++++++++++------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/scripts/pypsa-de/modify_prenetwork.py b/scripts/pypsa-de/modify_prenetwork.py index 422196171..e4d63732c 100644 --- a/scripts/pypsa-de/modify_prenetwork.py +++ b/scripts/pypsa-de/modify_prenetwork.py @@ -835,20 +835,21 @@ def aladin_mobility_demand(n): simulation_period_correction_factor = n.snapshot_weightings.objective.sum() / 8760 # oil demand - oil_demand = pd.Series( - aladin_demand.Liquids * simulation_period_correction_factor, - index=aladin_demand.index + " land transport oil", - ) + if "land transport oil" in n.loads.carrier.unique(): # i.e. before 2050 + oil_demand = pd.Series( + aladin_demand.Liquids.values * simulation_period_correction_factor, + index=aladin_demand.index + " land transport oil", + ) - profile = n.loads_t.p_set.loc[:, oil_demand.index] - profile /= profile.sum() - n.loads_t.p_set.loc[:, oil_demand.index] = (oil_demand * profile).div( - n.snapshot_weightings.objective, axis=0 - ) + profile = n.loads_t.p_set.loc[:, oil_demand.index] + profile /= profile.sum() + n.loads_t.p_set.loc[:, oil_demand.index] = (oil_demand * profile).div( + n.snapshot_weightings.objective, axis=0 + ) # hydrogen demand h2_demand = pd.Series( - aladin_demand.Hydrogen * simulation_period_correction_factor, + aladin_demand.Hydrogen.values * simulation_period_correction_factor, index=aladin_demand.index + " land transport fuel cell", ) @@ -860,7 +861,7 @@ def aladin_mobility_demand(n): # electricity demand ev_demand = pd.Series( - aladin_demand.Electricity * simulation_period_correction_factor, + aladin_demand.Electricity.values * simulation_period_correction_factor, index=aladin_demand.index + " land transport EV", ) @@ -883,27 +884,28 @@ def aladin_mobility_demand(n): # Then directly use .values for assignment p_nom = ( - aladin_demand.number_of_cars.values * snakemake.params.bev_charge_rate + aladin_demand.number_of_cars.values * 1e6 * snakemake.params.bev_charge_rate ) # same logic like in prepare_sector_network - n.links.loc[BEV_charger_i].p_nom = p_nom + n.links.loc[BEV_charger_i, "p_nom"] = p_nom V2G_i = n.links[ (n.links.carrier == "V2G") & (n.links.bus0.str.startswith("DE")) ].index if not V2G_i.empty: - n.links.loc[V2G_i].p_nom = p_nom * snakemake.params.bev_dsm_availability + n.links.loc[V2G_i, "p_nom"] = p_nom * snakemake.params.bev_dsm_availability dsm_i = n.stores[ (n.stores.carrier == "EV battery") & (n.stores.bus.str.startswith("DE")) ].index e_nom = ( aladin_demand.number_of_cars.values + * 1e6 * snakemake.params.bev_energy * snakemake.params.bev_dsm_availability ) if not dsm_i.empty: - n.stores.loc[dsm_i].e_nom = e_nom + n.stores.loc[dsm_i, "e_nom"] = e_nom def add_hydrogen_turbines(n): From 1ef17042da20a6932898284989cf5eec93a3aadc Mon Sep 17 00:00:00 2001 From: Michael Lindner Date: Mon, 7 Jul 2025 15:49:48 +0200 Subject: [PATCH 05/26] rename branch --- config/config.de.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.de.yaml b/config/config.de.yaml index 97aaa3327..d4650b048 100644 --- a/config/config.de.yaml +++ b/config/config.de.yaml @@ -4,7 +4,7 @@ # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#run run: - prefix: 20250514_dhsubnodes + prefix: 20250707_improve_transport_demand name: # - ExPol - KN2045_Mix From 1e0587ecaf24c20cf6514b657c738f9047e2053a Mon Sep 17 00:00:00 2001 From: Michael Lindner Date: Tue, 8 Jul 2025 11:30:40 +0200 Subject: [PATCH 06/26] add option to specify AGEB(+KBA) as source for transport demand in 2020 and 2025 --- Snakefile | 1 + config/config.de.yaml | 1 + scripts/pypsa-de/build_mobility_demand.py | 33 ++++++++++++++++++++--- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/Snakefile b/Snakefile index bfa089ea7..ea9b59393 100644 --- a/Snakefile +++ b/Snakefile @@ -314,6 +314,7 @@ rule build_mobility_demand: reference_scenario=config_provider("iiasa_database", "reference_scenario"), planning_horizons=config_provider("scenario", "planning_horizons"), leitmodelle=config_provider("iiasa_database", "leitmodelle"), + ageb_for_transport=config_provider("iiasa_database", "ageb_for_transport"), input: ariadne="resources/ariadne_database.csv", clustered_pop_layout=resources("pop_layout_base_s_{clusters}.csv"), diff --git a/config/config.de.yaml b/config/config.de.yaml index d4650b048..741f3e2ee 100644 --- a/config/config.de.yaml +++ b/config/config.de.yaml @@ -43,6 +43,7 @@ iiasa_database: - KN2045_NFhoch reference_scenario: KN2045_Mix region: Deutschland + ageb_for_transport: true # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#foresight foresight: myopic diff --git a/scripts/pypsa-de/build_mobility_demand.py b/scripts/pypsa-de/build_mobility_demand.py index f804e4349..665cc21ac 100644 --- a/scripts/pypsa-de/build_mobility_demand.py +++ b/scripts/pypsa-de/build_mobility_demand.py @@ -7,7 +7,7 @@ logger = logging.getLogger(__name__) -def get_transport_data(db, year): +def get_transport_data(db, year, ageb_for_transport=False): """ Retrieve the German mobility demand from the transport_data model. @@ -19,7 +19,7 @@ def get_transport_data(db, year): transport_demand = pd.Series(0.0, index=fuels) - if snakemake.wildcards.planning_horizons == "2020": + if year == "2020": logger.info( "For 2020, using hard-coded transport data from the Ariadne2-internal database." ) @@ -32,6 +32,31 @@ def get_transport_data(db, year): transport_demand = transport_demand.div(3.6e-6) # convert PJ to MWh transport_demand["number_of_cars"] = 0.658407 + 0.120261 # BEV + PHEV + if ageb_for_transport: + # AGEB 2020, https://ag-energiebilanzen.de/daten-und-fakten/bilanzen-1990-bis-2030/?_jahresbereich-bilanz=2011-2020 + transport_demand["Electricity"] = 39129 + 2394 # Schiene + Straße + transport_demand["Hydrogen"] = 0 + transport_demand["Liquids"] = ( + 140718 + 1261942 + 10782 + 638820 + ) # Bio Strasse + Diesel Strasse + Diesel Schiene + Otto Strasse + transport_demand = transport_demand.div(3.6e-3) # convert TJ to MWH + # https://www.kba.de/DE/Statistik/Produktkatalog/produkte/Fahrzeuge/fz27_b_uebersicht.html + # FZ27_202101, table FZ 27.2, 1. January 2021: + transport_demand["number_of_cars"] = 0.358498 + 0.280149 + + elif year == "2025" and ageb_for_transport: + # AGEB2024 for train demand 25, linear extrapolation with AGEB2024 + AGEB2023 for EVs + transport_demand["Electricity"] = 39761 + 2 * 21270 - 16180 + transport_demand["Hydrogen"] = 0 + # AGEB2024 for Liquids demand 25 + transport_demand["Liquids"] = 116323 + 9650 + 1158250 + 702618 + transport_demand = transport_demand.div(3.6e-3) + # FZ27_202504, 202404, table FZ 27.8, + # linear extrapolation to 1. January 2026: "1. January 2025" + ("1. January 2025" - "1. January 2024") + # 2 * (1,810,815 + 968,734) - (1,555,265 + 922,876) = 3080957 + # rounded upwards + transport_demand["number_of_cars"] = 3.1 # million, BEV + PHEV + else: df = db[year].loc[snakemake.params.leitmodelle["transport"]] @@ -78,7 +103,9 @@ def get_transport_data(db, year): f"Retrieving German mobility demand from {snakemake.params.leitmodelle['transport']} transport model." ) # get transport_data data - transport_data = get_transport_data(db, snakemake.wildcards.planning_horizons) + transport_data = get_transport_data( + db, snakemake.wildcards.planning_horizons, snakemake.params.ageb_for_transport + ) # get German mobility weighting pop_layout = pd.read_csv(snakemake.input.clustered_pop_layout, index_col=0) From 0f6764e1de8459af496ee1adddc2faa1ab912526 Mon Sep 17 00:00:00 2001 From: Michael Lindner Date: Tue, 8 Jul 2025 12:29:33 +0200 Subject: [PATCH 07/26] add warning --- scripts/pypsa-de/modify_prenetwork.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/pypsa-de/modify_prenetwork.py b/scripts/pypsa-de/modify_prenetwork.py index e4d63732c..da7036d3b 100644 --- a/scripts/pypsa-de/modify_prenetwork.py +++ b/scripts/pypsa-de/modify_prenetwork.py @@ -829,6 +829,9 @@ def aladin_mobility_demand(n): """ Change loads in Germany to use Aladin data for road demand. """ + logger.info( + "Overwriting land transport demand with Aladin data. In particular the `land_transport_electric_share` config setting will not be used." + ) # get aladin data aladin_demand = pd.read_csv(snakemake.input.aladin_demand, index_col=0) From 952c72d46059faa8bb0e0c35d69378bfea7d4511 Mon Sep 17 00:00:00 2001 From: Michael Lindner Date: Tue, 8 Jul 2025 12:32:16 +0200 Subject: [PATCH 08/26] set transport shares to dummy values --- config/config.de.yaml | 43 ++++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/config/config.de.yaml b/config/config.de.yaml index 741f3e2ee..a4fc18066 100644 --- a/config/config.de.yaml +++ b/config/config.de.yaml @@ -334,30 +334,31 @@ sector: 2040: 0.29 2045: 0.36 2050: 0.43 + # The transport_shares are just dummy setting that get overwritten in build_mobility_demand land_transport_fuel_cell_share: - 2020: 0.05 - 2025: 0.05 - 2030: 0.05 - 2035: 0.05 - 2040: 0.05 - 2045: 0.05 - 2050: 0.05 + 2020: 0.01 + 2025: 0.01 + 2030: 0.01 + 2035: 0.01 + 2040: 0.01 + 2045: 0.01 + 2050: 0.01 land_transport_electric_share: - 2020: 0.05 - 2025: 0.15 - 2030: 0.3 - 2035: 0.45 - 2040: 0.7 - 2045: 0.85 - 2050: 0.95 + 2020: 0.04 + 2025: 0.04 + 2030: 0.04 + 2035: 0.04 + 2040: 0.04 + 2045: 0.04 + 2050: 0.04 land_transport_ice_share: - 2020: 0.9 - 2025: 0.8 - 2030: 0.65 - 2035: 0.5 - 2040: 0.25 - 2045: 0.1 - 2050: 0.0 + 2020: 0.95 + 2025: 0.95 + 2030: 0.95 + 2035: 0.95 + 2040: 0.95 + 2045: 0.95 + 2050: 0.95 # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#industry industry: From 243ba974a9768819b5f348b7c18f1e654f01ecf3 Mon Sep 17 00:00:00 2001 From: Michael Lindner Date: Tue, 8 Jul 2025 16:15:10 +0200 Subject: [PATCH 09/26] add changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c22e96d02..6773a5aad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ # Changelog +- Improved the transport demand data, added an option to source 2020 and 2025 data from AGEB instead of Aladin - Simplified scenarion definition and made `Mix` the default scenario - 0.3: workflow is all public now, no longer requires credentials to internal data - Allowing myopic optimization until 2050 From f9c13bb04a344f26187b74efccae20e345cf8880 Mon Sep 17 00:00:00 2001 From: Michael Lindner Date: Fri, 11 Jul 2025 17:12:12 +0200 Subject: [PATCH 10/26] use mobility demand from uba projektionsbericht --- Snakefile | 6 +- config/config.de.yaml | 2 +- scripts/pypsa-de/build_mobility_demand.py | 123 +++++++++++++++++----- 3 files changed, 101 insertions(+), 30 deletions(-) diff --git a/Snakefile b/Snakefile index c4f8a9207..9def6be84 100644 --- a/Snakefile +++ b/Snakefile @@ -367,10 +367,14 @@ rule build_mobility_demand: reference_scenario=config_provider("iiasa_database", "reference_scenario"), planning_horizons=config_provider("scenario", "planning_horizons"), leitmodelle=config_provider("iiasa_database", "leitmodelle"), - ageb_for_transport=config_provider("iiasa_database", "ageb_for_transport"), + uba_for_mobility=config_provider("iiasa_database", "uba_for_mobility"), + shipping_oil_share=config_provider("sector", "shipping_oil_share"), + aviation_demand_factor=config_provider("sector", "aviation_demand_factor"), + energy_totals_year=config_provider("energy", "energy_totals_year"), input: ariadne="resources/ariadne_database.csv", clustered_pop_layout=resources("pop_layout_base_s_{clusters}.csv"), + energy_totals=resources("energy_totals.csv"), output: mobility_demand=resources( "mobility_demand_aladin_{clusters}_{planning_horizons}.csv" diff --git a/config/config.de.yaml b/config/config.de.yaml index a4fc18066..0ba5d2e24 100644 --- a/config/config.de.yaml +++ b/config/config.de.yaml @@ -43,7 +43,7 @@ iiasa_database: - KN2045_NFhoch reference_scenario: KN2045_Mix region: Deutschland - ageb_for_transport: true + uba_for_mobility: true # MWMS scenario from Projektionsbericht 2025 # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#foresight foresight: myopic diff --git a/scripts/pypsa-de/build_mobility_demand.py b/scripts/pypsa-de/build_mobility_demand.py index 665cc21ac..39f27b087 100644 --- a/scripts/pypsa-de/build_mobility_demand.py +++ b/scripts/pypsa-de/build_mobility_demand.py @@ -7,7 +7,12 @@ logger = logging.getLogger(__name__) -def get_transport_data(db, year, ageb_for_transport=False): +def get_transport_data( + db, + year, + non_land_liquids, + uba_for_mobility=False, +): """ Retrieve the German mobility demand from the transport_data model. @@ -24,40 +29,77 @@ def get_transport_data(db, year, ageb_for_transport=False): "For 2020, using hard-coded transport data from the Ariadne2-internal database." ) - transport_demand = pd.Series() - # if 2020 - transport_demand["Electricity"] = 0.0 + 17.0 + 35.82 + 0.0 - transport_demand["Hydrogen"] = 0.0 + 0.0 + 0.0 + 0.0 - transport_demand["Liquids"] = 41.81 + 1369.34 + 11.18 + 637.23 + transport_demand = pd.Series( + { + "Electricity": 0.0 + 17.0 + 35.82 + 0.0, + "Hydrogen": 0.0 + 0.0 + 0.0 + 0.0, + "Liquids": 41.81 + 1369.34 + 11.18 + 637.23, + } + ) + transport_demand = transport_demand.div(3.6e-6) # convert PJ to MWh transport_demand["number_of_cars"] = 0.658407 + 0.120261 # BEV + PHEV - if ageb_for_transport: + if uba_for_mobility: + logger.warning( + "For 2020, using historical AGEB and KBA data instead of UBA projections." + ) # AGEB 2020, https://ag-energiebilanzen.de/daten-und-fakten/bilanzen-1990-bis-2030/?_jahresbereich-bilanz=2011-2020 - transport_demand["Electricity"] = 39129 + 2394 # Schiene + Straße - transport_demand["Hydrogen"] = 0 - transport_demand["Liquids"] = ( - 140718 + 1261942 + 10782 + 638820 - ) # Bio Strasse + Diesel Strasse + Diesel Schiene + Otto Strasse - transport_demand = transport_demand.div(3.6e-3) # convert TJ to MWH + transport_demand = pd.Series( + { + "Electricity": 39129 + 2394, # Schiene + Straße + "Hydrogen": 0, + "Liquids": 140718 + + 1261942 + + 10782 + + 638820, # Bio Strasse + Diesel Strasse + Diesel Schiene + Otto Strasse + } + ) + transport_demand = transport_demand.div(3.6e-3) # convert PJ to MWH # https://www.kba.de/DE/Statistik/Produktkatalog/produkte/Fahrzeuge/fz27_b_uebersicht.html # FZ27_202101, table FZ 27.2, 1. January 2021: transport_demand["number_of_cars"] = 0.358498 + 0.280149 - elif year == "2025" and ageb_for_transport: - # AGEB2024 for train demand 25, linear extrapolation with AGEB2024 + AGEB2023 for EVs - transport_demand["Electricity"] = 39761 + 2 * 21270 - 16180 - transport_demand["Hydrogen"] = 0 - # AGEB2024 for Liquids demand 25 - transport_demand["Liquids"] = 116323 + 9650 + 1158250 + 702618 - transport_demand = transport_demand.div(3.6e-3) - # FZ27_202504, 202404, table FZ 27.8, - # linear extrapolation to 1. January 2026: "1. January 2025" + ("1. January 2025" - "1. January 2024") - # 2 * (1,810,815 + 968,734) - (1,555,265 + 922,876) = 3080957 - # rounded upwards - transport_demand["number_of_cars"] = 3.1 # million, BEV + PHEV + elif year == "2025" and uba_for_mobility: + # https://www.umweltbundesamt.de/sites/default/files/medien/11850/publikationen/projektionsbericht_2025.pdf, Abbildung 64 & 59, + transport_demand = pd.Series( + { + "Electricity": 21, + "Hydrogen": 0.0, + "Liquids": 524 + 51, + "number_of_cars": 2.7 + 1.2, # BEV + PHEV + } + ) + transport_demand["Liquids"] -= non_land_liquids[ + int(year) + ] # remove domestic navigation and aviation + elif year == "2030" and uba_for_mobility: + transport_demand = pd.Series( + { + "Electricity": 57, + "Hydrogen": 14, + "Liquids": 418 + 34 + 1, + "number_of_cars": 8.7 + 1.8, # BEV + PHEV + } + ) + transport_demand["Liquids"] -= non_land_liquids[int(year)] + elif year == "2035" and uba_for_mobility: + transport_demand = pd.Series( + { + "Electricity": 117, + "Hydrogen": 36, + "Liquids": 237 + 26 + 1, + "number_of_cars": 18.9 + 1.8, # BEV + PHEV + } + ) + transport_demand["Liquids"] -= non_land_liquids[int(year)] else: + if uba_for_mobility: + logger.error( + f"Year {year} is not supported for UBA mobility projections. Please use only 2020, 2025, 2030, 2035." + ) + df = db[year].loc[snakemake.params.leitmodelle["transport"]] for fuel in fuels: @@ -79,11 +121,11 @@ def get_transport_data(db, year, ageb_for_transport=False): snakemake = mock_snakemake( "build_mobility_demand", simpl="", - clusters=22, + clusters=27, opts="", ll="vopt", sector_opts="none", - planning_horizons="2020", + planning_horizons="2030", run="KN2045_Mix", ) configure_logging(snakemake) @@ -99,12 +141,37 @@ def get_transport_data(db, year, ageb_for_transport=False): :, ] + energy_totals = ( + pd.read_csv( + snakemake.input.energy_totals, + index_col=[0, 1], + ) + .xs( + snakemake.params.energy_totals_year, + level="year", + ) + .loc["DE"] + ) + + domestic_aviation = energy_totals.loc["total domestic aviation"] * pd.Series( + snakemake.params.aviation_demand_factor + ) + + domestic_navigation = energy_totals.loc["total domestic navigation"] * pd.Series( + snakemake.params.shipping_oil_share + ) + + non_land_liquids = domestic_aviation + domestic_navigation + logger.info( f"Retrieving German mobility demand from {snakemake.params.leitmodelle['transport']} transport model." ) # get transport_data data transport_data = get_transport_data( - db, snakemake.wildcards.planning_horizons, snakemake.params.ageb_for_transport + db, + snakemake.wildcards.planning_horizons, + non_land_liquids, + uba_for_mobility=snakemake.params.uba_for_mobility, ) # get German mobility weighting From 0cb4015dbab57d77ae706aaff4e2a9d825bd821b Mon Sep 17 00:00:00 2001 From: Michael Lindner Date: Mon, 14 Jul 2025 14:24:38 +0200 Subject: [PATCH 11/26] have separate option for the 2020 data --- Snakefile | 1 + config/config.de.yaml | 3 ++- scripts/pypsa-de/build_mobility_demand.py | 11 +++++++---- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Snakefile b/Snakefile index 9def6be84..7db8b5988 100644 --- a/Snakefile +++ b/Snakefile @@ -367,6 +367,7 @@ rule build_mobility_demand: reference_scenario=config_provider("iiasa_database", "reference_scenario"), planning_horizons=config_provider("scenario", "planning_horizons"), leitmodelle=config_provider("iiasa_database", "leitmodelle"), + ageb_for_mobility=config_provider("iiasa_database", "ageb_for_mobility"), uba_for_mobility=config_provider("iiasa_database", "uba_for_mobility"), shipping_oil_share=config_provider("sector", "shipping_oil_share"), aviation_demand_factor=config_provider("sector", "aviation_demand_factor"), diff --git a/config/config.de.yaml b/config/config.de.yaml index 0ba5d2e24..4421ac683 100644 --- a/config/config.de.yaml +++ b/config/config.de.yaml @@ -43,7 +43,8 @@ iiasa_database: - KN2045_NFhoch reference_scenario: KN2045_Mix region: Deutschland - uba_for_mobility: true # MWMS scenario from Projektionsbericht 2025 + ageb_for_mobility: true # In 2020 use AGEB data for final energy demand and KBA for vehicles + uba_for_mobility: true # For 2025–2035 use MWMS scenario from UBA Projektionsbericht 2025 # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#foresight foresight: myopic diff --git a/scripts/pypsa-de/build_mobility_demand.py b/scripts/pypsa-de/build_mobility_demand.py index 39f27b087..6e5dfc7f6 100644 --- a/scripts/pypsa-de/build_mobility_demand.py +++ b/scripts/pypsa-de/build_mobility_demand.py @@ -11,6 +11,7 @@ def get_transport_data( db, year, non_land_liquids, + ageb_for_mobility=True, uba_for_mobility=False, ): """ @@ -40,10 +41,11 @@ def get_transport_data( transport_demand = transport_demand.div(3.6e-6) # convert PJ to MWh transport_demand["number_of_cars"] = 0.658407 + 0.120261 # BEV + PHEV - if uba_for_mobility: - logger.warning( - "For 2020, using historical AGEB and KBA data instead of UBA projections." - ) + if ageb_for_mobility or uba_for_mobility: + if uba_for_mobility: + logger.warning( + "For 2020, using historical AGEB and KBA data instead of UBA projections." + ) # AGEB 2020, https://ag-energiebilanzen.de/daten-und-fakten/bilanzen-1990-bis-2030/?_jahresbereich-bilanz=2011-2020 transport_demand = pd.Series( { @@ -171,6 +173,7 @@ def get_transport_data( db, snakemake.wildcards.planning_horizons, non_land_liquids, + ageb_for_mobility=snakemake.params.ageb_for_mobility, uba_for_mobility=snakemake.params.uba_for_mobility, ) From 0ab4ed06b80a3a186b4690d5915e996a038a612b Mon Sep 17 00:00:00 2001 From: Michael Lindner Date: Mon, 14 Jul 2025 14:31:21 +0200 Subject: [PATCH 12/26] renaming --- Snakefile | 12 ++++----- config/config.de.yaml | 2 +- scripts/pypsa-de/build_mobility_demand.py | 2 +- scripts/pypsa-de/modify_prenetwork.py | 33 +++++++++++------------ 4 files changed, 24 insertions(+), 25 deletions(-) diff --git a/Snakefile b/Snakefile index 7db8b5988..be4a526d2 100644 --- a/Snakefile +++ b/Snakefile @@ -362,7 +362,7 @@ if config["enable"]["retrieve"] and config["enable"].get("retrieve_cost_data", T ruleorder: modify_cost_data > retrieve_cost_data -rule build_mobility_demand: +rule build_exogenous_mobility_demand: params: reference_scenario=config_provider("iiasa_database", "reference_scenario"), planning_horizons=config_provider("scenario", "planning_horizons"), @@ -378,14 +378,14 @@ rule build_mobility_demand: energy_totals=resources("energy_totals.csv"), output: mobility_demand=resources( - "mobility_demand_aladin_{clusters}_{planning_horizons}.csv" + "modified_mobility_demand_{clusters}_{planning_horizons}.csv" ), resources: mem_mb=1000, log: - logs("build_mobility_demand_{clusters}_{planning_horizons}.log"), + logs("build_exogenous_mobility_demand_{clusters}_{planning_horizons}.log"), script: - "scripts/pypsa-de/build_mobility_demand.py" + "scripts/pypsa-de/build_exogenous_mobility_demand.py" rule build_egon_data: @@ -570,8 +570,8 @@ rule modify_prenetwork: else [] ), costs=resources("costs_{planning_horizons}.csv"), - aladin_demand=resources( - "mobility_demand_aladin_{clusters}_{planning_horizons}.csv" + modified_mobility_demand=resources( + "modified_mobility_demand_{clusters}_{planning_horizons}.csv" ), biomass_potentials=resources( "biomass_potentials_s_{clusters}_{planning_horizons}.csv" diff --git a/config/config.de.yaml b/config/config.de.yaml index 4421ac683..c803394ea 100644 --- a/config/config.de.yaml +++ b/config/config.de.yaml @@ -335,7 +335,7 @@ sector: 2040: 0.29 2045: 0.36 2050: 0.43 - # The transport_shares are just dummy setting that get overwritten in build_mobility_demand + # The transport_shares are just dummy setting that get overwritten in build_exogenous_mobility_demand land_transport_fuel_cell_share: 2020: 0.01 2025: 0.01 diff --git a/scripts/pypsa-de/build_mobility_demand.py b/scripts/pypsa-de/build_mobility_demand.py index 6e5dfc7f6..46e9e77eb 100644 --- a/scripts/pypsa-de/build_mobility_demand.py +++ b/scripts/pypsa-de/build_mobility_demand.py @@ -121,7 +121,7 @@ def get_transport_data( if __name__ == "__main__": if "snakemake" not in globals(): snakemake = mock_snakemake( - "build_mobility_demand", + "build_exogenous_mobility_demand", simpl="", clusters=27, opts="", diff --git a/scripts/pypsa-de/modify_prenetwork.py b/scripts/pypsa-de/modify_prenetwork.py index da7036d3b..813fdee96 100644 --- a/scripts/pypsa-de/modify_prenetwork.py +++ b/scripts/pypsa-de/modify_prenetwork.py @@ -825,23 +825,22 @@ def must_run(n, params): n.links.loc[links_i, "p_min_pu"] = p_min_pu -def aladin_mobility_demand(n): +def modify_mobility_demand(n): """ - Change loads in Germany to use Aladin data for road demand. + Change loads in Germany to use exogenous data for road demand. """ logger.info( - "Overwriting land transport demand with Aladin data. In particular the `land_transport_electric_share` config setting will not be used." + "Overwriting land transport demand. In particular the `land_transport_electric_share` config setting will not be used." ) - # get aladin data - aladin_demand = pd.read_csv(snakemake.input.aladin_demand, index_col=0) + new_demand = pd.read_csv(snakemake.input.modified_mobility_demand, index_col=0) simulation_period_correction_factor = n.snapshot_weightings.objective.sum() / 8760 # oil demand if "land transport oil" in n.loads.carrier.unique(): # i.e. before 2050 oil_demand = pd.Series( - aladin_demand.Liquids.values * simulation_period_correction_factor, - index=aladin_demand.index + " land transport oil", + new_demand.Liquids.values * simulation_period_correction_factor, + index=new_demand.index + " land transport oil", ) profile = n.loads_t.p_set.loc[:, oil_demand.index] @@ -852,8 +851,8 @@ def aladin_mobility_demand(n): # hydrogen demand h2_demand = pd.Series( - aladin_demand.Hydrogen.values * simulation_period_correction_factor, - index=aladin_demand.index + " land transport fuel cell", + new_demand.Hydrogen.values * simulation_period_correction_factor, + index=new_demand.index + " land transport fuel cell", ) profile = n.loads_t.p_set.loc[:, h2_demand.index] @@ -864,8 +863,8 @@ def aladin_mobility_demand(n): # electricity demand ev_demand = pd.Series( - aladin_demand.Electricity.values * simulation_period_correction_factor, - index=aladin_demand.index + " land transport EV", + new_demand.Electricity.values * simulation_period_correction_factor, + index=new_demand.index + " land transport EV", ) profile = n.loads_t.p_set.loc[:, ev_demand.index] @@ -880,14 +879,14 @@ def aladin_mobility_demand(n): (n.links.carrier == "BEV charger") & (n.links.bus0.str.startswith("DE")) ].index - # Check that buses in network and aladin_data appear in same order + # Check that buses in network and new_demand data appear in same order assert [ - idx.startswith(idx2) for (idx, idx2) in zip(BEV_charger_i, aladin_demand.index) + idx.startswith(idx2) for (idx, idx2) in zip(BEV_charger_i, new_demand.index) ] # Then directly use .values for assignment p_nom = ( - aladin_demand.number_of_cars.values * 1e6 * snakemake.params.bev_charge_rate + new_demand.number_of_cars.values * 1e6 * snakemake.params.bev_charge_rate ) # same logic like in prepare_sector_network n.links.loc[BEV_charger_i, "p_nom"] = p_nom @@ -902,7 +901,7 @@ def aladin_mobility_demand(n): (n.stores.carrier == "EV battery") & (n.stores.bus.str.startswith("DE")) ].index e_nom = ( - aladin_demand.number_of_cars.values + new_demand.number_of_cars.values * 1e6 * snakemake.params.bev_energy * snakemake.params.bev_dsm_availability @@ -1282,7 +1281,7 @@ def scale_capacity(n, scaling): ) configure_logging(snakemake) - logger.info("Adding Ariadne-specific functionality") + logger.info("Adding PyPSA-DE specific functionality") n = pypsa.Network(snakemake.input.network) nhours = n.snapshot_weightings.generators.sum() @@ -1295,7 +1294,7 @@ def scale_capacity(n, scaling): nyears, ) - aladin_mobility_demand(n) + modify_mobility_demand(n) new_boiler_ban(n) From 5b365445f2a748f222c0f7796138fc7172886201 Mon Sep 17 00:00:00 2001 From: Michael Lindner Date: Mon, 14 Jul 2025 16:23:59 +0200 Subject: [PATCH 13/26] rename script and changelog --- CHANGELOG.md | 2 ++ ...demand.py => build_exogenous_mobility_demand.py} | 13 +++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) rename scripts/pypsa-de/{build_mobility_demand.py => build_exogenous_mobility_demand.py} (93%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6773a5aad..9f2f60faf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,6 @@ # Changelog +- Added an option to source mobility demand from UBA MWMS (Projektionsbericht 2025) for the years 2025-2035 +- Renamed functions and script for exogenous mobility demand - Improved the transport demand data, added an option to source 2020 and 2025 data from AGEB instead of Aladin - Simplified scenarion definition and made `Mix` the default scenario - 0.3: workflow is all public now, no longer requires credentials to internal data diff --git a/scripts/pypsa-de/build_mobility_demand.py b/scripts/pypsa-de/build_exogenous_mobility_demand.py similarity index 93% rename from scripts/pypsa-de/build_mobility_demand.py rename to scripts/pypsa-de/build_exogenous_mobility_demand.py index 46e9e77eb..cbadef7e5 100644 --- a/scripts/pypsa-de/build_mobility_demand.py +++ b/scripts/pypsa-de/build_exogenous_mobility_demand.py @@ -69,32 +69,37 @@ def get_transport_data( "Electricity": 21, "Hydrogen": 0.0, "Liquids": 524 + 51, - "number_of_cars": 2.7 + 1.2, # BEV + PHEV } ) transport_demand["Liquids"] -= non_land_liquids[ int(year) - ] # remove domestic navigation and aviation + ] # remove domestic navigation and aviation from UBA data to avoid double counting + transport_demand = transport_demand.mul(1e6) # convert TWh to MWh + transport_demand["number_of_cars"] = 2.7 + 1.2 # BEV + PHEV + elif year == "2030" and uba_for_mobility: transport_demand = pd.Series( { "Electricity": 57, "Hydrogen": 14, "Liquids": 418 + 34 + 1, - "number_of_cars": 8.7 + 1.8, # BEV + PHEV } ) transport_demand["Liquids"] -= non_land_liquids[int(year)] + transport_demand = transport_demand.mul(1e6) + transport_demand["number_of_cars"] = 8.7 + 1.8 + elif year == "2035" and uba_for_mobility: transport_demand = pd.Series( { "Electricity": 117, "Hydrogen": 36, "Liquids": 237 + 26 + 1, - "number_of_cars": 18.9 + 1.8, # BEV + PHEV } ) transport_demand["Liquids"] -= non_land_liquids[int(year)] + transport_demand = transport_demand.mul(1e6) + transport_demand["number_of_cars"] = 18.9 + 1.8 else: if uba_for_mobility: From c85efb46853d97da4af1900850fff53254fa4494 Mon Sep 17 00:00:00 2001 From: Michael Lindner Date: Mon, 14 Jul 2025 16:28:11 +0200 Subject: [PATCH 14/26] rename scripts --- Snakefile | 8 ++++---- ...y_industry_demand.py => modify_industry_production.py} | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) rename scripts/pypsa-de/{modify_industry_demand.py => modify_industry_production.py} (96%) diff --git a/Snakefile b/Snakefile index be4a526d2..6b55c5073 100644 --- a/Snakefile +++ b/Snakefile @@ -599,7 +599,7 @@ rule modify_prenetwork: "scripts/pypsa-de/modify_prenetwork.py" -ruleorder: modify_industry_demand > build_industrial_production_per_country_tomorrow +ruleorder: modify_industry_production > build_industrial_production_per_country_tomorrow rule modify_existing_heating: @@ -660,7 +660,7 @@ rule build_existing_chp_de: "scripts/pypsa-de/build_existing_chp_de.py" -rule modify_industry_demand: +rule modify_industry_production: params: reference_scenario=config_provider("iiasa_database", "reference_scenario"), input: @@ -675,9 +675,9 @@ rule modify_industry_demand: resources: mem_mb=1000, log: - logs("modify_industry_demand_{planning_horizons}.log"), + logs("modify_industry_production_{planning_horizons}.log"), script: - "scripts/pypsa-de/modify_industry_demand.py" + "scripts/pypsa-de/modify_industry_production.py" rule build_wasserstoff_kernnetz: diff --git a/scripts/pypsa-de/modify_industry_demand.py b/scripts/pypsa-de/modify_industry_production.py similarity index 96% rename from scripts/pypsa-de/modify_industry_demand.py rename to scripts/pypsa-de/modify_industry_production.py index 6494ce8c2..00771eb56 100644 --- a/scripts/pypsa-de/modify_industry_demand.py +++ b/scripts/pypsa-de/modify_industry_production.py @@ -23,7 +23,7 @@ if __name__ == "__main__": if "snakemake" not in globals(): snakemake = mock_snakemake( - "modify_industry_demand", + "modify_industry_production", simpl="", clusters=22, opts="", @@ -155,6 +155,6 @@ ], ) - existing_industry.to_csv( - snakemake.output.industrial_production_per_country_tomorrow - ) + # existing_industry.to_csv( + # snakemake.output.industrial_production_per_country_tomorrow + # ) From 81d51304743d32719b72dfa48e7763ccc0d6b67a Mon Sep 17 00:00:00 2001 From: Michael Lindner Date: Mon, 14 Jul 2025 18:15:30 +0200 Subject: [PATCH 15/26] first stab at industry demand modification --- Snakefile | 6 ++ scripts/pypsa-de/modify_prenetwork.py | 82 ++++++++++++++++++++++++++- 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/Snakefile b/Snakefile index 6b55c5073..f1f97c3ab 100644 --- a/Snakefile +++ b/Snakefile @@ -579,6 +579,12 @@ rule modify_prenetwork: industrial_demand=resources( "industrial_energy_demand_base_s_{clusters}_{planning_horizons}.csv" ), + industrial_production_per_country_tomorrow=resources( + "industrial_production_per_country_tomorrow_{planning_horizons}-modified.csv" + ), + industry_sector_ratios=resources( + "industry_sector_ratios_{planning_horizons}.csv" + ), pop_weighted_energy_totals=resources( "pop_weighted_energy_totals_s_{clusters}.csv" ), diff --git a/scripts/pypsa-de/modify_prenetwork.py b/scripts/pypsa-de/modify_prenetwork.py index 813fdee96..ac595fe58 100644 --- a/scripts/pypsa-de/modify_prenetwork.py +++ b/scripts/pypsa-de/modify_prenetwork.py @@ -1267,6 +1267,86 @@ def scale_capacity(n, scaling): ] +def modify_industry_demand(n, industry_production_file, sector_ratios_file): + sector_ratios = pd.read_csv( + snakemake.input.industry_sector_ratios, + header=[0, 1], + index_col=0, + ).rename_axis("carrier") + industry_production = pd.read_csv( + snakemake.input.industrial_production_per_country_tomorrow, + index_col="kton/a", + ).rename_axis("country") + + subcategories = ["HVC", "Methanol", "Chlorine", "Ammonia"] + carrier = ["hydrogen", "methane", "naphtha"] + + ip = industry_production.loc["DE", subcategories] # kt/a + sr = sector_ratios["DE"].loc[carrier, subcategories] # MWh/tMaterial + non_energy = sr.multiply(ip).sum(axis=1) * 1e3 + + # MWMS data 2025 + uba_data = pd.Series( + {"fossil": 324, "electricity": 211, "biomass": 30, "hydrogen": 0, "heat": 48} + ).mul(1e6) + # TODO make sure this works for periods of all lengths + # TODO what happens if load - non_energy < 0? + + uba_industry_without_non_energy = uba_data.sum() + _industry_loads = [ + "solid biomass for industry", + "gas for industry", + "H2 for industry", + "industry methanol", + "naphtha for industry", + "low-temperature heat for industry", + "industry electricity", + "coal for industry", + ] + industry_loads = n.loads.query( + f"carrier in {_industry_loads} and bus.str.startswith('DE')" + ) + pypsa_industry_without_non_energy = ( + industry_loads.p_set.sum() * 8760 - non_energy.sum() + ) # MWh/a + # TODO Should we scale the non-energy use with this factor? + non_energy_scaling_factor = ( + uba_industry_without_non_energy / pypsa_industry_without_non_energy + ) + print( + f"Notused at the moment: Scaling factor for non-energy use: {non_energy_scaling_factor:.2f}" + ) + + # H2 + h2_loads = n.loads.query( + "carrier.str.contains('H2 for industry') and bus.str.startswith('DE')" + ) + total_h2 = h2_loads.p_set.values.sum() * 8760 + scaling_factor_h2 = (non_energy["hydrogen"] + uba_data["hydrogen"]) / total_h2 + + n.loads.loc[h2_loads.index, "p_set"] *= scaling_factor_h2 + + # electricity + electricity_loads = n.loads.query( + "carrier.str.contains('industry electricity') and bus.str.startswith('DE')" + ) + total_electricity = electricity_loads.p_set.values.sum() * 8760 + scaling_factor_electricity = uba_data["electricity"] / total_electricity + + n.loads.loc[electricity_loads.index, "p_set"] *= scaling_factor_electricity + + # fossil fuels + fossil_loads = n.loads.query( + "carrier.str.contains('gas for industry|coal for industry|naphtha for industry') and bus.str.startswith('DE')" + ) + total_fossil = fossil_loads.p_set.values.sum() * 8760 + scaling_factor_fossil = ( + non_energy["methane"] + non_energy["naphtha"] + uba_data["fossil"] + ) / total_fossil + n.loads.loc[fossil_loads.index, "p_set"] *= scaling_factor_fossil + # TODO this will have to be done separately for coal, gas and oil + + if __name__ == "__main__": if "snakemake" not in globals(): snakemake = mock_snakemake( @@ -1349,4 +1429,4 @@ def scale_capacity(n, scaling): sanitize_custom_columns(n) - n.export_to_netcdf(snakemake.output.network) + # n.export_to_netcdf(snakemake.output.network) From 921505dbf8fe9161ec54634175f5fb859b663f70 Mon Sep 17 00:00:00 2001 From: Michael Lindner Date: Wed, 16 Jul 2025 11:55:23 +0200 Subject: [PATCH 16/26] modify industry demand should be working now --- Snakefile | 5 + ...rojektionsbericht2025_Abbildung31_MWMS.csv | 6 + config/config.de.yaml | 3 +- scripts/pypsa-de/modify_prenetwork.py | 144 ++++++++++++------ 4 files changed, 107 insertions(+), 51 deletions(-) create mode 100644 ariadne-data/UBA_Projektionsbericht2025_Abbildung31_MWMS.csv diff --git a/Snakefile b/Snakefile index f1f97c3ab..fff8b8e3f 100644 --- a/Snakefile +++ b/Snakefile @@ -559,6 +559,10 @@ rule modify_prenetwork: bev_charge_rate=config_provider("sector", "bev_charge_rate"), bev_energy=config_provider("sector", "bev_energy"), bev_dsm_availability=config_provider("sector", "bev_dsm_availability"), + uba_for_industry=config_provider("iiasa_database", "uba_for_industry"), + scale_industry_non_energy=config_provider( + "iiasa_database", "scale_industry_non_energy" + ), input: costs_modifications="ariadne-data/costs_{planning_horizons}-modifications.csv", network=resources( @@ -592,6 +596,7 @@ rule modify_prenetwork: regions_onshore=resources("regions_onshore_base_s_{clusters}.geojson"), regions_offshore=resources("regions_offshore_base_s_{clusters}.geojson"), offshore_connection_points="ariadne-data/offshore_connection_points.csv", + new_industrial_energy_demand="ariadne-data/UBA_Projektionsbericht2025_Abbildung31_MWMS.csv", output: network=resources( "networks/base_s_{clusters}_{opts}_{sector_opts}_{planning_horizons}_final.nc" diff --git a/ariadne-data/UBA_Projektionsbericht2025_Abbildung31_MWMS.csv b/ariadne-data/UBA_Projektionsbericht2025_Abbildung31_MWMS.csv new file mode 100644 index 000000000..9f504877c --- /dev/null +++ b/ariadne-data/UBA_Projektionsbericht2025_Abbildung31_MWMS.csv @@ -0,0 +1,6 @@ +carrier,2025,2030,2035 +fossil,324,258,191 +industry electricity,211,234,249 +solid biomass for industry,31,35,31 +H2 for industry,0,6,42 +low+AC0-temperature heat for industry,48,59,63 diff --git a/config/config.de.yaml b/config/config.de.yaml index c803394ea..d957e0581 100644 --- a/config/config.de.yaml +++ b/config/config.de.yaml @@ -45,7 +45,8 @@ iiasa_database: region: Deutschland ageb_for_mobility: true # In 2020 use AGEB data for final energy demand and KBA for vehicles uba_for_mobility: true # For 2025–2035 use MWMS scenario from UBA Projektionsbericht 2025 - + uba_for_industry: true # For 2025–2035 use MWMS scenario from UBA Projektionsbericht 2025 + scale_industry_non_energy: false # Scale non-energy industry demand directly proportional to energy demand # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#foresight foresight: myopic diff --git a/scripts/pypsa-de/modify_prenetwork.py b/scripts/pypsa-de/modify_prenetwork.py index ac595fe58..ae35f9261 100644 --- a/scripts/pypsa-de/modify_prenetwork.py +++ b/scripts/pypsa-de/modify_prenetwork.py @@ -1267,32 +1267,51 @@ def scale_capacity(n, scaling): ] -def modify_industry_demand(n, industry_production_file, sector_ratios_file): +def modify_industry_demand( + n, + year, + industry_energy_demand_file, + industry_production_file, + sector_ratios_file, + scale_non_energy=False, +): + logger.info("Modifying industry demand in Germany.") + + industry_production = pd.read_csv( + industry_production_file, + index_col="kton/a", + ).rename_axis("country") + sector_ratios = pd.read_csv( - snakemake.input.industry_sector_ratios, + sector_ratios_file, header=[0, 1], index_col=0, ).rename_axis("carrier") - industry_production = pd.read_csv( - snakemake.input.industrial_production_per_country_tomorrow, - index_col="kton/a", - ).rename_axis("country") + + new_demand = pd.read_csv( + industry_energy_demand_file, + index_col=0, + )[str(year)].mul(1e6) subcategories = ["HVC", "Methanol", "Chlorine", "Ammonia"] carrier = ["hydrogen", "methane", "naphtha"] ip = industry_production.loc["DE", subcategories] # kt/a sr = sector_ratios["DE"].loc[carrier, subcategories] # MWh/tMaterial - non_energy = sr.multiply(ip).sum(axis=1) * 1e3 - - # MWMS data 2025 - uba_data = pd.Series( - {"fossil": 324, "electricity": 211, "biomass": 30, "hydrogen": 0, "heat": 48} - ).mul(1e6) - # TODO make sure this works for periods of all lengths - # TODO what happens if load - non_energy < 0? + _non_energy = sr.multiply(ip).sum(axis=1) * 1e3 + + non_energy = pd.Series( + { + "industry electricity": 0.0, + "low-temperature heat for industry": 0.0, + "solid biomass for industry": 0.0, + "H2 for industry": _non_energy["hydrogen"], + "coal for industry": 0.0, + "gas for industry": _non_energy["methane"], + "naphtha for industry": _non_energy["naphtha"], + } + ) - uba_industry_without_non_energy = uba_data.sum() _industry_loads = [ "solid biomass for industry", "gas for industry", @@ -1306,45 +1325,60 @@ def modify_industry_demand(n, industry_production_file, sector_ratios_file): industry_loads = n.loads.query( f"carrier in {_industry_loads} and bus.str.startswith('DE')" ) - pypsa_industry_without_non_energy = ( - industry_loads.p_set.sum() * 8760 - non_energy.sum() - ) # MWh/a - # TODO Should we scale the non-energy use with this factor? - non_energy_scaling_factor = ( - uba_industry_without_non_energy / pypsa_industry_without_non_energy - ) - print( - f"Notused at the moment: Scaling factor for non-energy use: {non_energy_scaling_factor:.2f}" - ) - # H2 - h2_loads = n.loads.query( - "carrier.str.contains('H2 for industry') and bus.str.startswith('DE')" - ) - total_h2 = h2_loads.p_set.values.sum() * 8760 - scaling_factor_h2 = (non_energy["hydrogen"] + uba_data["hydrogen"]) / total_h2 + if scale_non_energy: + new_demand_without_non_energy = new_demand.sum() + pypsa_industry_without_non_energy = ( + industry_loads.p_set.sum() * 8760 - non_energy.sum() + ) + non_energy_scaling_factor = ( + new_demand_without_non_energy / pypsa_industry_without_non_energy + ) + logger.info( + f"Scaling non-energy use by {non_energy_scaling_factor:.2f} to match UBA data." + ) + non_energy_corrected = non_energy * non_energy_scaling_factor + else: + non_energy_corrected = non_energy - n.loads.loc[h2_loads.index, "p_set"] *= scaling_factor_h2 + for carrier in [ + "industry electricity", + "H2 for industry", + "solid biomass for industry", + "low-temperature heat for industry", + ]: + loads_i = n.loads.query( + f"carrier == '{carrier}' and bus.str.startswith('DE')" + ).index + logger.info( + f"Total load of {carrier} in DE before scaling: {n.loads.loc[loads_i, 'p_set'].sum() * 8760:.2f} MWh/a" + ) + total_load = industry_loads.p_set.loc[loads_i].sum() * 8760 + scaling_factor = ( + new_demand[carrier] + non_energy_corrected[carrier] + ) / total_load + n.loads.loc[loads_i, "p_set"] *= scaling_factor + logger.info( + f"Total load of {carrier} in DE after scaling: {n.loads.loc[loads_i, 'p_set'].sum() * 8760:.2f} MWh/a" + ) - # electricity - electricity_loads = n.loads.query( - "carrier.str.contains('industry electricity') and bus.str.startswith('DE')" + # Fossil fuels are aggregated in UBA MWMS but have to be scaled separately + fossil_loads = industry_loads.query("carrier.str.contains('gas|coal|naphtha')") + fossil_totals = ( + fossil_loads[["p_set", "carrier"]].groupby("carrier").p_set.sum() * 8760 ) - total_electricity = electricity_loads.p_set.values.sum() * 8760 - scaling_factor_electricity = uba_data["electricity"] / total_electricity - - n.loads.loc[electricity_loads.index, "p_set"] *= scaling_factor_electricity - - # fossil fuels - fossil_loads = n.loads.query( - "carrier.str.contains('gas for industry|coal for industry|naphtha for industry') and bus.str.startswith('DE')" + fossil_energy = fossil_totals - non_energy[fossil_totals.index] + fossil_energy_corrected = fossil_energy * new_demand["fossil"] / fossil_energy.sum() + fossil_totals_corrected = ( + fossil_energy_corrected + non_energy_corrected[fossil_totals.index] ) - total_fossil = fossil_loads.p_set.values.sum() * 8760 - scaling_factor_fossil = ( - non_energy["methane"] + non_energy["naphtha"] + uba_data["fossil"] - ) / total_fossil - n.loads.loc[fossil_loads.index, "p_set"] *= scaling_factor_fossil - # TODO this will have to be done separately for coal, gas and oil + for carrier in fossil_totals.index: + loads_i = fossil_loads.query( + f"carrier == '{carrier}' and bus.str.startswith('DE')" + ).index + n.loads.loc[loads_i, "p_set"] *= ( + fossil_totals_corrected[carrier] / fossil_totals[carrier] + ) if __name__ == "__main__": @@ -1429,4 +1463,14 @@ def modify_industry_demand(n, industry_production_file, sector_ratios_file): sanitize_custom_columns(n) - # n.export_to_netcdf(snakemake.output.network) + if snakemake.params.uba_for_industry: + modify_industry_demand( + n, + current_year, + snakemake.input.new_industrial_energy_demand, + snakemake.input.industrial_production_per_country_tomorrow, + snakemake.input.industry_sector_ratios, + scale_non_energy=snakemake.params.scale_industry_non_energy, + ) + + n.export_to_netcdf(snakemake.output.network) From eca7802dbb8581677095ae02e07b7ec82bb4e56e Mon Sep 17 00:00:00 2001 From: Michael Lindner Date: Wed, 16 Jul 2025 12:02:25 +0200 Subject: [PATCH 17/26] rename scenario, fix comment --- config/config.de.yaml | 2 +- scripts/pypsa-de/modify_industry_production.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/config.de.yaml b/config/config.de.yaml index d957e0581..13bd51694 100644 --- a/config/config.de.yaml +++ b/config/config.de.yaml @@ -4,7 +4,7 @@ # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#run run: - prefix: 20250707_improve_transport_demand + prefix: 20250716_improve_industry_demand name: # - ExPol - KN2045_Mix diff --git a/scripts/pypsa-de/modify_industry_production.py b/scripts/pypsa-de/modify_industry_production.py index 00771eb56..a473210d5 100644 --- a/scripts/pypsa-de/modify_industry_production.py +++ b/scripts/pypsa-de/modify_industry_production.py @@ -155,6 +155,6 @@ ], ) - # existing_industry.to_csv( - # snakemake.output.industrial_production_per_country_tomorrow - # ) + existing_industry.to_csv( + snakemake.output.industrial_production_per_country_tomorrow + ) From bb978a916763404d49166b08730700079a7b0a46 Mon Sep 17 00:00:00 2001 From: Michael Lindner Date: Wed, 16 Jul 2025 13:31:33 +0200 Subject: [PATCH 18/26] add index name --- ariadne-data/UBA_Projektionsbericht2025_Abbildung31_MWMS.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ariadne-data/UBA_Projektionsbericht2025_Abbildung31_MWMS.csv b/ariadne-data/UBA_Projektionsbericht2025_Abbildung31_MWMS.csv index 9f504877c..417fd22c4 100644 --- a/ariadne-data/UBA_Projektionsbericht2025_Abbildung31_MWMS.csv +++ b/ariadne-data/UBA_Projektionsbericht2025_Abbildung31_MWMS.csv @@ -3,4 +3,4 @@ fossil,324,258,191 industry electricity,211,234,249 solid biomass for industry,31,35,31 H2 for industry,0,6,42 -low+AC0-temperature heat for industry,48,59,63 +low-temperature heat for industry,48,59,63 From 1f36395c3cac031b491a418fb6388cbe30b59745 Mon Sep 17 00:00:00 2001 From: Michael Lindner Date: Wed, 16 Jul 2025 13:31:47 +0200 Subject: [PATCH 19/26] add error if using uba data after 2040 --- scripts/pypsa-de/modify_prenetwork.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/pypsa-de/modify_prenetwork.py b/scripts/pypsa-de/modify_prenetwork.py index ae35f9261..5fced02ed 100644 --- a/scripts/pypsa-de/modify_prenetwork.py +++ b/scripts/pypsa-de/modify_prenetwork.py @@ -1463,7 +1463,11 @@ def modify_industry_demand( sanitize_custom_columns(n) - if snakemake.params.uba_for_industry: + if snakemake.params.uba_for_industry and current_year >= 2025: + if current_year >= 2040: + logger.error( + "The UBA for industry data is only available for 2025, 2030 and 2035. Please check your config." + ) modify_industry_demand( n, current_year, From adcbc17f1472adbb55911f8bb7d2aad7284abd61 Mon Sep 17 00:00:00 2001 From: Michael Lindner Date: Wed, 16 Jul 2025 13:32:54 +0200 Subject: [PATCH 20/26] add to changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f2f60faf..f8369793c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,6 @@ # Changelog +- Added an option to source industry energy demand from UBA MWMS (Projektionsbericht 2025) for the years 2025-2035 +- renamed some scripts - Added an option to source mobility demand from UBA MWMS (Projektionsbericht 2025) for the years 2025-2035 - Renamed functions and script for exogenous mobility demand - Improved the transport demand data, added an option to source 2020 and 2025 data from AGEB instead of Aladin From c0bce54e66173872f8d1b58723e2d77e56dea8a1 Mon Sep 17 00:00:00 2001 From: Michael Lindner Date: Wed, 16 Jul 2025 13:49:55 +0200 Subject: [PATCH 21/26] transporte_shares matter outside of Germany! --- config/config.de.yaml | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/config/config.de.yaml b/config/config.de.yaml index c803394ea..0f55725f7 100644 --- a/config/config.de.yaml +++ b/config/config.de.yaml @@ -335,31 +335,31 @@ sector: 2040: 0.29 2045: 0.36 2050: 0.43 - # The transport_shares are just dummy setting that get overwritten in build_exogenous_mobility_demand + # For Germany these settings get overwritten in build_mobility_demand land_transport_fuel_cell_share: 2020: 0.01 2025: 0.01 - 2030: 0.01 - 2035: 0.01 - 2040: 0.01 - 2045: 0.01 - 2050: 0.01 + 2030: 0.02 + 2035: 0.03 + 2040: 0.03 + 2045: 0.03 + 2050: 0.03 land_transport_electric_share: 2020: 0.04 - 2025: 0.04 - 2030: 0.04 - 2035: 0.04 - 2040: 0.04 - 2045: 0.04 - 2050: 0.04 + 2025: 0.10 + 2030: 0.3 + 2035: 0.45 + 2040: 0.72 + 2045: 0.87 + 2050: 0.97 land_transport_ice_share: 2020: 0.95 - 2025: 0.95 - 2030: 0.95 - 2035: 0.95 - 2040: 0.95 - 2045: 0.95 - 2050: 0.95 + 2025: 0.89 + 2030: 0.68 + 2035: 0.52 + 2040: 0.25 + 2045: 0.1 + 2050: 0.0 # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#industry industry: From 451f59b5ac22cb31ccc12f4779400fe7042292d3 Mon Sep 17 00:00:00 2001 From: Michael Lindner Date: Wed, 16 Jul 2025 13:56:01 +0200 Subject: [PATCH 22/26] rename mobility_demand -> mobility_data --- Snakefile | 14 +++++++------- config/config.de.yaml | 2 +- .../pypsa-de/build_exogenous_mobility_demand.py | 6 +++--- scripts/pypsa-de/modify_prenetwork.py | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Snakefile b/Snakefile index be4a526d2..691fe5222 100644 --- a/Snakefile +++ b/Snakefile @@ -362,7 +362,7 @@ if config["enable"]["retrieve"] and config["enable"].get("retrieve_cost_data", T ruleorder: modify_cost_data > retrieve_cost_data -rule build_exogenous_mobility_demand: +rule build_exogenous_mobility_data: params: reference_scenario=config_provider("iiasa_database", "reference_scenario"), planning_horizons=config_provider("scenario", "planning_horizons"), @@ -377,15 +377,15 @@ rule build_exogenous_mobility_demand: clustered_pop_layout=resources("pop_layout_base_s_{clusters}.csv"), energy_totals=resources("energy_totals.csv"), output: - mobility_demand=resources( - "modified_mobility_demand_{clusters}_{planning_horizons}.csv" + mobility_data=resources( + "modified_mobility_data_{clusters}_{planning_horizons}.csv" ), resources: mem_mb=1000, log: - logs("build_exogenous_mobility_demand_{clusters}_{planning_horizons}.log"), + logs("build_exogenous_mobility_data_{clusters}_{planning_horizons}.log"), script: - "scripts/pypsa-de/build_exogenous_mobility_demand.py" + "scripts/pypsa-de/build_exogenous_mobility_data.py" rule build_egon_data: @@ -570,8 +570,8 @@ rule modify_prenetwork: else [] ), costs=resources("costs_{planning_horizons}.csv"), - modified_mobility_demand=resources( - "modified_mobility_demand_{clusters}_{planning_horizons}.csv" + modified_mobility_data=resources( + "modified_mobility_data_{clusters}_{planning_horizons}.csv" ), biomass_potentials=resources( "biomass_potentials_s_{clusters}_{planning_horizons}.csv" diff --git a/config/config.de.yaml b/config/config.de.yaml index 0f55725f7..df93e81d3 100644 --- a/config/config.de.yaml +++ b/config/config.de.yaml @@ -335,7 +335,7 @@ sector: 2040: 0.29 2045: 0.36 2050: 0.43 - # For Germany these settings get overwritten in build_mobility_demand + # For Germany these settings get overwritten in build_exogenous_mobility_data land_transport_fuel_cell_share: 2020: 0.01 2025: 0.01 diff --git a/scripts/pypsa-de/build_exogenous_mobility_demand.py b/scripts/pypsa-de/build_exogenous_mobility_demand.py index cbadef7e5..38bba5a9a 100644 --- a/scripts/pypsa-de/build_exogenous_mobility_demand.py +++ b/scripts/pypsa-de/build_exogenous_mobility_demand.py @@ -126,7 +126,7 @@ def get_transport_data( if __name__ == "__main__": if "snakemake" not in globals(): snakemake = mock_snakemake( - "build_exogenous_mobility_demand", + "build_exogenous_mobility_data", simpl="", clusters=27, opts="", @@ -187,10 +187,10 @@ def get_transport_data( # only get German data pop_layout = pop_layout[pop_layout.ct == "DE"].fraction - mobility_demand = pd.DataFrame( + mobility_data = pd.DataFrame( pop_layout.values[:, None] * transport_data.values, index=pop_layout.index, columns=transport_data.index, ) - mobility_demand.to_csv(snakemake.output.mobility_demand) + mobility_data.to_csv(snakemake.output.mobility_data) diff --git a/scripts/pypsa-de/modify_prenetwork.py b/scripts/pypsa-de/modify_prenetwork.py index 813fdee96..1dc2fbfad 100644 --- a/scripts/pypsa-de/modify_prenetwork.py +++ b/scripts/pypsa-de/modify_prenetwork.py @@ -832,7 +832,7 @@ def modify_mobility_demand(n): logger.info( "Overwriting land transport demand. In particular the `land_transport_electric_share` config setting will not be used." ) - new_demand = pd.read_csv(snakemake.input.modified_mobility_demand, index_col=0) + new_demand = pd.read_csv(snakemake.input.modified_mobility_data, index_col=0) simulation_period_correction_factor = n.snapshot_weightings.objective.sum() / 8760 From 00ce2746b4a50b4cdf1c0ec335a1e8f6ccad58d8 Mon Sep 17 00:00:00 2001 From: Michael Lindner Date: Wed, 16 Jul 2025 14:02:15 +0200 Subject: [PATCH 23/26] more renaming --- .../build_exogenous_mobility_demand.py | 58 +++++++++---------- scripts/pypsa-de/modify_prenetwork.py | 12 ++-- 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/scripts/pypsa-de/build_exogenous_mobility_demand.py b/scripts/pypsa-de/build_exogenous_mobility_demand.py index 38bba5a9a..b24a9d3e7 100644 --- a/scripts/pypsa-de/build_exogenous_mobility_demand.py +++ b/scripts/pypsa-de/build_exogenous_mobility_demand.py @@ -7,7 +7,7 @@ logger = logging.getLogger(__name__) -def get_transport_data( +def get_mobility_data( db, year, non_land_liquids, @@ -15,7 +15,7 @@ def get_transport_data( uba_for_mobility=False, ): """ - Retrieve the German mobility demand from the transport_data model. + Retrieve the German mobility demand from the transport model. Sum over the subsectors Bus, LDV, Rail, and Truck for the fuels electricity, hydrogen, and synthetic fuels. @@ -23,14 +23,14 @@ def get_transport_data( subsectors = ["Bus", "LDV", "Rail", "Truck"] fuels = ["Electricity", "Hydrogen", "Liquids"] - transport_demand = pd.Series(0.0, index=fuels) + mobility_data = pd.Series(0.0, index=fuels) if year == "2020": logger.info( "For 2020, using hard-coded transport data from the Ariadne2-internal database." ) - transport_demand = pd.Series( + mobility_data = pd.Series( { "Electricity": 0.0 + 17.0 + 35.82 + 0.0, "Hydrogen": 0.0 + 0.0 + 0.0 + 0.0, @@ -38,8 +38,8 @@ def get_transport_data( } ) - transport_demand = transport_demand.div(3.6e-6) # convert PJ to MWh - transport_demand["number_of_cars"] = 0.658407 + 0.120261 # BEV + PHEV + mobility_data = mobility_data.div(3.6e-6) # convert PJ to MWh + mobility_data["million_evs"] = 0.658407 + 0.120261 # BEV + PHEV if ageb_for_mobility or uba_for_mobility: if uba_for_mobility: @@ -47,7 +47,7 @@ def get_transport_data( "For 2020, using historical AGEB and KBA data instead of UBA projections." ) # AGEB 2020, https://ag-energiebilanzen.de/daten-und-fakten/bilanzen-1990-bis-2030/?_jahresbereich-bilanz=2011-2020 - transport_demand = pd.Series( + mobility_data = pd.Series( { "Electricity": 39129 + 2394, # Schiene + Straße "Hydrogen": 0, @@ -57,49 +57,49 @@ def get_transport_data( + 638820, # Bio Strasse + Diesel Strasse + Diesel Schiene + Otto Strasse } ) - transport_demand = transport_demand.div(3.6e-3) # convert PJ to MWH + mobility_data = mobility_data.div(3.6e-3) # convert PJ to MWH # https://www.kba.de/DE/Statistik/Produktkatalog/produkte/Fahrzeuge/fz27_b_uebersicht.html # FZ27_202101, table FZ 27.2, 1. January 2021: - transport_demand["number_of_cars"] = 0.358498 + 0.280149 + mobility_data["million_evs"] = 0.358498 + 0.280149 elif year == "2025" and uba_for_mobility: # https://www.umweltbundesamt.de/sites/default/files/medien/11850/publikationen/projektionsbericht_2025.pdf, Abbildung 64 & 59, - transport_demand = pd.Series( + mobility_data = pd.Series( { "Electricity": 21, "Hydrogen": 0.0, "Liquids": 524 + 51, } ) - transport_demand["Liquids"] -= non_land_liquids[ + mobility_data["Liquids"] -= non_land_liquids[ int(year) ] # remove domestic navigation and aviation from UBA data to avoid double counting - transport_demand = transport_demand.mul(1e6) # convert TWh to MWh - transport_demand["number_of_cars"] = 2.7 + 1.2 # BEV + PHEV + mobility_data = mobility_data.mul(1e6) # convert TWh to MWh + mobility_data["million_evs"] = 2.7 + 1.2 # BEV + PHEV elif year == "2030" and uba_for_mobility: - transport_demand = pd.Series( + mobility_data = pd.Series( { "Electricity": 57, "Hydrogen": 14, "Liquids": 418 + 34 + 1, } ) - transport_demand["Liquids"] -= non_land_liquids[int(year)] - transport_demand = transport_demand.mul(1e6) - transport_demand["number_of_cars"] = 8.7 + 1.8 + mobility_data["Liquids"] -= non_land_liquids[int(year)] + mobility_data = mobility_data.mul(1e6) + mobility_data["million_evs"] = 8.7 + 1.8 elif year == "2035" and uba_for_mobility: - transport_demand = pd.Series( + mobility_data = pd.Series( { "Electricity": 117, "Hydrogen": 36, "Liquids": 237 + 26 + 1, } ) - transport_demand["Liquids"] -= non_land_liquids[int(year)] - transport_demand = transport_demand.mul(1e6) - transport_demand["number_of_cars"] = 18.9 + 1.8 + mobility_data["Liquids"] -= non_land_liquids[int(year)] + mobility_data = mobility_data.mul(1e6) + mobility_data["million_evs"] = 18.9 + 1.8 else: if uba_for_mobility: @@ -112,15 +112,15 @@ def get_transport_data( for fuel in fuels: for subsector in subsectors: key = f"Final Energy|Transportation|{subsector}|{fuel}" - transport_demand.loc[fuel] += df.get((key, "TWh/yr"), 0.0) + mobility_data.loc[fuel] += df.get((key, "TWh/yr"), 0.0) - transport_demand = transport_demand.mul(1e6) # convert TWh to MWh - transport_demand["number_of_cars"] = ( + mobility_data = mobility_data.mul(1e6) # convert TWh to MWh + mobility_data["million_evs"] = ( df.loc["Stock|Transportation|LDV|BEV", "million"] + df.loc["Stock|Transportation|LDV|PHEV", "million"] ) - return transport_demand + return mobility_data if __name__ == "__main__": @@ -173,8 +173,8 @@ def get_transport_data( logger.info( f"Retrieving German mobility demand from {snakemake.params.leitmodelle['transport']} transport model." ) - # get transport_data data - transport_data = get_transport_data( + # get mobility_data data + mobility_data = get_mobility_data( db, snakemake.wildcards.planning_horizons, non_land_liquids, @@ -188,9 +188,9 @@ def get_transport_data( pop_layout = pop_layout[pop_layout.ct == "DE"].fraction mobility_data = pd.DataFrame( - pop_layout.values[:, None] * transport_data.values, + pop_layout.values[:, None] * mobility_data.values, index=pop_layout.index, - columns=transport_data.index, + columns=mobility_data.index, ) mobility_data.to_csv(snakemake.output.mobility_data) diff --git a/scripts/pypsa-de/modify_prenetwork.py b/scripts/pypsa-de/modify_prenetwork.py index 1dc2fbfad..c9829b273 100644 --- a/scripts/pypsa-de/modify_prenetwork.py +++ b/scripts/pypsa-de/modify_prenetwork.py @@ -825,14 +825,16 @@ def must_run(n, params): n.links.loc[links_i, "p_min_pu"] = p_min_pu -def modify_mobility_demand(n): +def modify_mobility_demand(n, mobility_data_file): """ Change loads in Germany to use exogenous data for road demand. + + The mobility_data contains the """ logger.info( "Overwriting land transport demand. In particular the `land_transport_electric_share` config setting will not be used." ) - new_demand = pd.read_csv(snakemake.input.modified_mobility_data, index_col=0) + new_demand = pd.read_csv(mobility_data_file, index_col=0) simulation_period_correction_factor = n.snapshot_weightings.objective.sum() / 8760 @@ -886,7 +888,7 @@ def modify_mobility_demand(n): # Then directly use .values for assignment p_nom = ( - new_demand.number_of_cars.values * 1e6 * snakemake.params.bev_charge_rate + new_demand.million_evs.values * 1e6 * snakemake.params.bev_charge_rate ) # same logic like in prepare_sector_network n.links.loc[BEV_charger_i, "p_nom"] = p_nom @@ -901,7 +903,7 @@ def modify_mobility_demand(n): (n.stores.carrier == "EV battery") & (n.stores.bus.str.startswith("DE")) ].index e_nom = ( - new_demand.number_of_cars.values + new_demand.million_evs.values * 1e6 * snakemake.params.bev_energy * snakemake.params.bev_dsm_availability @@ -1294,7 +1296,7 @@ def scale_capacity(n, scaling): nyears, ) - modify_mobility_demand(n) + modify_mobility_demand(n, snakemake.input.modified_mobility_data) new_boiler_ban(n) From ec5a9986e7f4ceddcedd38f7b9573773f5e01014 Mon Sep 17 00:00:00 2001 From: Michael Lindner Date: Thu, 17 Jul 2025 10:38:22 +0200 Subject: [PATCH 24/26] rename and refactor --- Snakefile | 1 - config/config.de.yaml | 4 +- ...nd.py => build_exogenous_mobility_data.py} | 27 ++--- scripts/pypsa-de/modify_prenetwork.py | 112 ++++++++---------- 4 files changed, 59 insertions(+), 85 deletions(-) rename scripts/pypsa-de/{build_exogenous_mobility_demand.py => build_exogenous_mobility_data.py} (87%) diff --git a/Snakefile b/Snakefile index 691fe5222..fd64a6049 100644 --- a/Snakefile +++ b/Snakefile @@ -374,7 +374,6 @@ rule build_exogenous_mobility_data: energy_totals_year=config_provider("energy", "energy_totals_year"), input: ariadne="resources/ariadne_database.csv", - clustered_pop_layout=resources("pop_layout_base_s_{clusters}.csv"), energy_totals=resources("energy_totals.csv"), output: mobility_data=resources( diff --git a/config/config.de.yaml b/config/config.de.yaml index df93e81d3..6dd4bbb7d 100644 --- a/config/config.de.yaml +++ b/config/config.de.yaml @@ -346,7 +346,7 @@ sector: 2050: 0.03 land_transport_electric_share: 2020: 0.04 - 2025: 0.10 + 2025: 0.15 2030: 0.3 2035: 0.45 2040: 0.72 @@ -354,7 +354,7 @@ sector: 2050: 0.97 land_transport_ice_share: 2020: 0.95 - 2025: 0.89 + 2025: 0.84 2030: 0.68 2035: 0.52 2040: 0.25 diff --git a/scripts/pypsa-de/build_exogenous_mobility_demand.py b/scripts/pypsa-de/build_exogenous_mobility_data.py similarity index 87% rename from scripts/pypsa-de/build_exogenous_mobility_demand.py rename to scripts/pypsa-de/build_exogenous_mobility_data.py index b24a9d3e7..fc9ac2069 100644 --- a/scripts/pypsa-de/build_exogenous_mobility_demand.py +++ b/scripts/pypsa-de/build_exogenous_mobility_data.py @@ -39,7 +39,7 @@ def get_mobility_data( ) mobility_data = mobility_data.div(3.6e-6) # convert PJ to MWh - mobility_data["million_evs"] = 0.658407 + 0.120261 # BEV + PHEV + mobility_data["million_EVs"] = 0.658407 + 0.120261 # BEV + PHEV if ageb_for_mobility or uba_for_mobility: if uba_for_mobility: @@ -60,7 +60,7 @@ def get_mobility_data( mobility_data = mobility_data.div(3.6e-3) # convert PJ to MWH # https://www.kba.de/DE/Statistik/Produktkatalog/produkte/Fahrzeuge/fz27_b_uebersicht.html # FZ27_202101, table FZ 27.2, 1. January 2021: - mobility_data["million_evs"] = 0.358498 + 0.280149 + mobility_data["million_EVs"] = 0.358498 + 0.280149 elif year == "2025" and uba_for_mobility: # https://www.umweltbundesamt.de/sites/default/files/medien/11850/publikationen/projektionsbericht_2025.pdf, Abbildung 64 & 59, @@ -75,7 +75,7 @@ def get_mobility_data( int(year) ] # remove domestic navigation and aviation from UBA data to avoid double counting mobility_data = mobility_data.mul(1e6) # convert TWh to MWh - mobility_data["million_evs"] = 2.7 + 1.2 # BEV + PHEV + mobility_data["million_EVs"] = 2.7 + 1.2 # BEV + PHEV elif year == "2030" and uba_for_mobility: mobility_data = pd.Series( @@ -87,7 +87,7 @@ def get_mobility_data( ) mobility_data["Liquids"] -= non_land_liquids[int(year)] mobility_data = mobility_data.mul(1e6) - mobility_data["million_evs"] = 8.7 + 1.8 + mobility_data["million_EVs"] = 8.7 + 1.8 elif year == "2035" and uba_for_mobility: mobility_data = pd.Series( @@ -99,7 +99,7 @@ def get_mobility_data( ) mobility_data["Liquids"] -= non_land_liquids[int(year)] mobility_data = mobility_data.mul(1e6) - mobility_data["million_evs"] = 18.9 + 1.8 + mobility_data["million_EVs"] = 18.9 + 1.8 else: if uba_for_mobility: @@ -115,7 +115,7 @@ def get_mobility_data( mobility_data.loc[fuel] += df.get((key, "TWh/yr"), 0.0) mobility_data = mobility_data.mul(1e6) # convert TWh to MWh - mobility_data["million_evs"] = ( + mobility_data["million_EVs"] = ( df.loc["Stock|Transportation|LDV|BEV", "million"] + df.loc["Stock|Transportation|LDV|PHEV", "million"] ) @@ -132,7 +132,7 @@ def get_mobility_data( opts="", ll="vopt", sector_opts="none", - planning_horizons="2030", + planning_horizons="2020", run="KN2045_Mix", ) configure_logging(snakemake) @@ -182,15 +182,4 @@ def get_mobility_data( uba_for_mobility=snakemake.params.uba_for_mobility, ) - # get German mobility weighting - pop_layout = pd.read_csv(snakemake.input.clustered_pop_layout, index_col=0) - # only get German data - pop_layout = pop_layout[pop_layout.ct == "DE"].fraction - - mobility_data = pd.DataFrame( - pop_layout.values[:, None] * mobility_data.values, - index=pop_layout.index, - columns=mobility_data.index, - ) - - mobility_data.to_csv(snakemake.output.mobility_data) + mobility_data.to_csv(snakemake.output.mobility_data, header=False) diff --git a/scripts/pypsa-de/modify_prenetwork.py b/scripts/pypsa-de/modify_prenetwork.py index c9829b273..1d8de2346 100644 --- a/scripts/pypsa-de/modify_prenetwork.py +++ b/scripts/pypsa-de/modify_prenetwork.py @@ -829,87 +829,73 @@ def modify_mobility_demand(n, mobility_data_file): """ Change loads in Germany to use exogenous data for road demand. - The mobility_data contains the + The mobility_data contains the demand of Electricity, Hydrogen and Liquids in MWh/a, and the number of EVs in million. """ logger.info( "Overwriting land transport demand. In particular the `land_transport_electric_share` config setting will not be used." ) - new_demand = pd.read_csv(mobility_data_file, index_col=0) - simulation_period_correction_factor = n.snapshot_weightings.objective.sum() / 8760 + fraction_modelyear = n.snapshot_weightings.stores.sum() / 8760 - # oil demand - if "land transport oil" in n.loads.carrier.unique(): # i.e. before 2050 - oil_demand = pd.Series( - new_demand.Liquids.values * simulation_period_correction_factor, - index=new_demand.index + " land transport oil", - ) - - profile = n.loads_t.p_set.loc[:, oil_demand.index] - profile /= profile.sum() - n.loads_t.p_set.loc[:, oil_demand.index] = (oil_demand * profile).div( - n.snapshot_weightings.objective, axis=0 - ) + new_demand = pd.read_csv(mobility_data_file, header=None, index_col=0).iloc[:, 0] - # hydrogen demand - h2_demand = pd.Series( - new_demand.Hydrogen.values * simulation_period_correction_factor, - index=new_demand.index + " land transport fuel cell", - ) - - profile = n.loads_t.p_set.loc[:, h2_demand.index] - profile /= profile.sum() - n.loads_t.p_set.loc[:, h2_demand.index] = (h2_demand * profile).div( - n.snapshot_weightings.objective, axis=0 - ) + number_of_EVs = new_demand.pop("million_EVs") * 1e6 - # electricity demand - ev_demand = pd.Series( - new_demand.Electricity.values * simulation_period_correction_factor, - index=new_demand.index + " land transport EV", - ) + new_demand *= fraction_modelyear - profile = n.loads_t.p_set.loc[:, ev_demand.index] - profile /= profile.sum() - n.loads_t.p_set.loc[:, ev_demand.index] = (ev_demand * profile).div( - n.snapshot_weightings.objective, axis=0 - ) + carrier_fuel_map = { + "land transport EV": "Electricity", + "land transport fuel cell": "Hydrogen", + "land transport oil": "Liquids", + } + for carrier, fuel in carrier_fuel_map.items(): + loads_i = n.loads[ + (n.loads.carrier == carrier) & n.loads.index.str.startswith("DE") + ] + old_demand = ( + n.loads_t.p_set.loc[:, loads_i.index] + .sum(axis=1) + .mul(n.snapshot_weightings.stores) + .sum() + ) + scale_factor = new_demand[fuel] / old_demand + logger.info( + f"Scaling {carrier} loads in Germany by {scale_factor:.2f}.\nPrevious total demand: {old_demand:.2f} MWh/a, new total demand: {new_demand[fuel]:.2f} MWh/a." + ) + n.loads_t.p_set.loc[:, loads_i.index] *= scale_factor # adjust BEV charger and V2G capacities - BEV_charger_i = n.links[ + BEV_chargers = n.links[ (n.links.carrier == "BEV charger") & (n.links.bus0.str.startswith("DE")) - ].index - - # Check that buses in network and new_demand data appear in same order - assert [ - idx.startswith(idx2) for (idx, idx2) in zip(BEV_charger_i, new_demand.index) ] - # Then directly use .values for assignment - p_nom = ( - new_demand.million_evs.values * 1e6 * snakemake.params.bev_charge_rate - ) # same logic like in prepare_sector_network + scale_factor = ( + number_of_EVs * snakemake.params.bev_charge_rate / BEV_chargers.p_nom.sum() + ) + logger.info( + f"Scaling BEV charger capacities in Germany by {scale_factor:.2f} to match the new number of EVs.\nPrevious total capacity: {BEV_chargers.p_nom.sum():.2f} MW, new total capacity: {number_of_EVs * snakemake.params.bev_charge_rate:.2f} MW." + ) + n.links.loc[BEV_chargers.index, "p_nom"] *= scale_factor - n.links.loc[BEV_charger_i, "p_nom"] = p_nom + V2G = n.links[(n.links.carrier == "V2G") & (n.links.bus0.str.startswith("DE"))] - V2G_i = n.links[ - (n.links.carrier == "V2G") & (n.links.bus0.str.startswith("DE")) - ].index - if not V2G_i.empty: - n.links.loc[V2G_i, "p_nom"] = p_nom * snakemake.params.bev_dsm_availability + if not V2G.empty: + n.links.loc[V2G.index, "p_nom"] *= ( + scale_factor * snakemake.params.bev_dsm_availability + ) - dsm_i = n.stores[ + dsm = n.stores[ (n.stores.carrier == "EV battery") & (n.stores.bus.str.startswith("DE")) - ].index - e_nom = ( - new_demand.million_evs.values - * 1e6 - * snakemake.params.bev_energy - * snakemake.params.bev_dsm_availability - ) - if not dsm_i.empty: - n.stores.loc[dsm_i, "e_nom"] = e_nom + ] + + if not dsm.empty: + scale_factor = ( + number_of_EVs + * snakemake.params.bev_energy + * snakemake.params.bev_dsm_availability + ) / dsm.e_nom.sum() + n.stores.loc[dsm.index, "e_nom"] *= scale_factor def add_hydrogen_turbines(n): @@ -1351,4 +1337,4 @@ def scale_capacity(n, scaling): sanitize_custom_columns(n) - n.export_to_netcdf(snakemake.output.network) + # n.export_to_netcdf(snakemake.output.network) From 40a83579d046a9bed533bc827c3c2cabbcae5e63 Mon Sep 17 00:00:00 2001 From: Michael Lindner Date: Thu, 17 Jul 2025 11:06:55 +0200 Subject: [PATCH 25/26] small adjustments --- config/config.de.yaml | 6 +++--- scripts/pypsa-de/modify_prenetwork.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/config.de.yaml b/config/config.de.yaml index 0a5767465..a75d9a6a9 100644 --- a/config/config.de.yaml +++ b/config/config.de.yaml @@ -44,7 +44,7 @@ iiasa_database: reference_scenario: KN2045_Mix region: Deutschland ageb_for_mobility: true # In 2020 use AGEB data for final energy demand and KBA for vehicles - uba_for_mobility: true # For 2025–2035 use MWMS scenario from UBA Projektionsbericht 2025 + uba_for_mobility: false # For 2025–2035 use MWMS scenario from UBA Projektionsbericht 2025 # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#foresight foresight: myopic @@ -344,7 +344,7 @@ sector: 2045: 0.03 2050: 0.03 land_transport_electric_share: - 2020: 0.04 + 2020: 0.05 2025: 0.15 2030: 0.3 2035: 0.45 @@ -352,7 +352,7 @@ sector: 2045: 0.87 2050: 0.97 land_transport_ice_share: - 2020: 0.95 + 2020: 0.94 2025: 0.84 2030: 0.68 2035: 0.52 diff --git a/scripts/pypsa-de/modify_prenetwork.py b/scripts/pypsa-de/modify_prenetwork.py index 1d8de2346..102622061 100644 --- a/scripts/pypsa-de/modify_prenetwork.py +++ b/scripts/pypsa-de/modify_prenetwork.py @@ -1337,4 +1337,4 @@ def scale_capacity(n, scaling): sanitize_custom_columns(n) - # n.export_to_netcdf(snakemake.output.network) + n.export_to_netcdf(snakemake.output.network) From 4b78729e1018f02054cc740d7ecf7195843214b2 Mon Sep 17 00:00:00 2001 From: Michael Lindner Date: Fri, 18 Jul 2025 17:44:17 +0200 Subject: [PATCH 26/26] improve export; disable uba_for _industry by default --- config/config.de.yaml | 2 +- scripts/pypsa-de/export_ariadne_variables.py | 66 +++++++++++++++----- 2 files changed, 53 insertions(+), 15 deletions(-) diff --git a/config/config.de.yaml b/config/config.de.yaml index 4c53cc483..18ea784f6 100644 --- a/config/config.de.yaml +++ b/config/config.de.yaml @@ -45,7 +45,7 @@ iiasa_database: region: Deutschland ageb_for_mobility: true # In 2020 use AGEB data for final energy demand and KBA for vehicles uba_for_mobility: false # For 2025–2035 use MWMS scenario from UBA Projektionsbericht 2025 - uba_for_industry: true # For 2025–2035 use MWMS scenario from UBA Projektionsbericht 2025 + uba_for_industry: false # For 2025–2035 use MWMS scenario from UBA Projektionsbericht 2025 scale_industry_non_energy: false # Scale non-energy industry demand directly proportional to energy demand # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#foresight foresight: myopic diff --git a/scripts/pypsa-de/export_ariadne_variables.py b/scripts/pypsa-de/export_ariadne_variables.py index 61401b5da..e0122b519 100644 --- a/scripts/pypsa-de/export_ariadne_variables.py +++ b/scripts/pypsa-de/export_ariadne_variables.py @@ -1817,9 +1817,17 @@ def get_secondary_energy(n, region, _industry_demand): axis=0, ).sum() mwh_coal_per_mwh_coke = 1.366 + coke_fraction = ( + industry_demand.get("coke") + * mwh_coal_per_mwh_coke + / ( + industry_demand.get("coke") * mwh_coal_per_mwh_coke + + industry_demand.get("coal") + ) + ) # Coke is added as a coal demand, so we need to convert back to units of coke for secondary energy var["Secondary Energy|Solids|Coal"] = var["Secondary Energy|Solids"] = ( - industry_demand.get("coke", 0) / mwh_coal_per_mwh_coke + sum_load(n, "coal for industry", region) * coke_fraction / mwh_coal_per_mwh_coke ) biomass_usage = ( @@ -1989,14 +1997,17 @@ def get_final_energy( # !: Pypsa-eur does not strictly distinguish between energy and # non-energy use - var["Final Energy|Industry|Electricity"] = industry_demand.get("electricity") - # or use: sum_load(n, "industry electricity", region) + var["Final Energy|Industry|Electricity"] = sum_load( + n, "industry electricity", region + ) # electricity is not used for non-energy purposes var["Final Energy|Industry excl Non-Energy Use|Electricity"] = var[ "Final Energy|Industry|Electricity" ] - var["Final Energy|Industry|Heat"] = industry_demand.get("low-temperature heat") + var["Final Energy|Industry|Heat"] = sum_load( + n, "low-temperature heat for industry", region + ) # heat is not used for non-energy purposes var["Final Energy|Industry excl Non-Energy Use|Heat"] = var[ "Final Energy|Industry|Heat" @@ -2008,7 +2019,7 @@ def get_final_energy( # var["Final Energy|Industry|Geothermal"] = \ # Not implemented - var["Final Energy|Industry|Gases"] = industry_demand.get("methane") + var["Final Energy|Industry|Gases"] = sum_load(n, "gas for industry", region) for gas_type in gas_fractions.index: var[f"Final Energy|Industry|Gases|{gas_type}"] = ( @@ -2030,7 +2041,7 @@ def get_final_energy( # var["Final Energy|Industry|Power2Heat"] = \ # Q: misleading description - var["Final Energy|Industry|Hydrogen"] = industry_demand.get("hydrogen") + var["Final Energy|Industry|Hydrogen"] = sum_load(n, "H2 for industry", region) # subtract non-energy used hydrogen from total hydrogen demand var["Final Energy|Industry excl Non-Energy Use|Hydrogen"] = ( var["Final Energy|Industry|Hydrogen"] @@ -2074,16 +2085,29 @@ def get_final_energy( # var["Final Energy|Industry|Other"] = \ - var["Final Energy|Industry|Solids|Biomass"] = industry_demand.get("solid biomass") + var["Final Energy|Industry|Solids|Biomass"] = sum_load( + n, "solid biomass for industry", region + ) var["Final Energy|Industry excl Non-Energy Use|Solids|Biomass"] = var[ "Final Energy|Industry|Solids|Biomass" ] mwh_coal_per_mwh_coke = 1.366 - # Coke is added as a coal demand, so we need to convert back to units of coke for final energy + coke_fraction = ( + industry_demand.get("coke") + * mwh_coal_per_mwh_coke + / ( + industry_demand.get("coke") * mwh_coal_per_mwh_coke + + industry_demand.get("coal") + ) + ) + # Contains coke demand, which is a coal product + # Here coke is considered a secondary energy source var["Final Energy|Industry|Solids|Coal"] = ( - industry_demand.get("coal") - + industry_demand.get("coke") / mwh_coal_per_mwh_coke + sum_load(n, "coal for industry", region) * (1 - coke_fraction) + + sum_load(n, "coal for industry", region) + * coke_fraction + / mwh_coal_per_mwh_coke ) var["Final Energy|Industry excl Non-Energy Use|Solids|Coal"] = var[ "Final Energy|Industry|Solids|Coal" @@ -2571,10 +2595,10 @@ def get_final_energy( return var * MWh2TWh -def get_emissions(n, region, _energy_totals, industry_demand): +def get_emissions(n, region, _energy_totals, _industry_demand): energy_totals = _energy_totals.loc[region[0:2]] - industry_DE = industry_demand.filter( + industry_demand = _industry_demand.filter( like=region, axis=0, ).sum() @@ -2880,8 +2904,22 @@ def get_emissions(n, region, _energy_totals, industry_demand): ) # considered 0 anyways mwh_coal_per_mwh_coke = 1.366 # from eurostat energy balance - # 0.3361 t/MWh, 1e-6 to convert to Mt - coking_emissions = industry_DE.coke * (mwh_coal_per_mwh_coke - 1) * 0.3361 * t2Mt + coke_fraction = ( + industry_demand.get("coke") + * mwh_coal_per_mwh_coke + / ( + industry_demand.get("coke") * mwh_coal_per_mwh_coke + + industry_demand.get("coal") + ) + ) + # 0.3361 t_CO2/MWh + coking_emissions = ( + sum_load(n, "coal for industry", region) + * coke_fraction + * (mwh_coal_per_mwh_coke - 1) + * 0.3361 + * t2Mt + ) var["Emissions|Gross Fossil CO2|Energy|Demand|Industry"] = ( co2_emissions.reindex( [