From e79d9a35d667e94db6998584d82b725658da8592 Mon Sep 17 00:00:00 2001 From: toniseibold Date: Thu, 7 May 2026 09:25:52 +0200 Subject: [PATCH 01/17] improving carbon manangement modeling --- config/plotting.default.yaml | 3 + config/schema.default.json | 10 +++ scripts/prepare_sector_network.py | 137 +++++++++++++++++++++++++----- 3 files changed, 128 insertions(+), 22 deletions(-) diff --git a/config/plotting.default.yaml b/config/plotting.default.yaml index a2d7405123..f92739335a 100644 --- a/config/plotting.default.yaml +++ b/config/plotting.default.yaml @@ -693,6 +693,9 @@ plotting: DAC: '#ff5270' co2 stored: '#f2385a' co2 sequestered: '#f2682f' + co2 dense: '#65334d' + co2 expansion: '#c6ebbe' + co2 compression: '#a9dbb8' co2: '#f29dae' co2 vent: '#ffd4dc' CO2 pipeline: '#f5627f' diff --git a/config/schema.default.json b/config/schema.default.json index 220d3874ea..ac1deffd65 100644 --- a/config/schema.default.json +++ b/config/schema.default.json @@ -4708,6 +4708,11 @@ "description": "The cost factor for the capital cost of the carbon dioxide transmission network.", "type": "number" }, + "co2_network_liquefaction": { + "default": false, + "description": "Add option to include compressor stations for carbon dioxide transport. Before being able to be transported, carbon dioxide must be compressed to 150bar to reach a dense phase that makes large scale transport via pipeline feasible.", + "type": "boolean" + }, "cc_fraction": { "default": 0.9, "description": "The default fraction of CO2 captured with post-combustion capture.", @@ -10996,6 +11001,11 @@ "description": "The cost factor for the capital cost of the carbon dioxide transmission network.", "type": "number" }, + "co2_network_liquefaction": { + "default": false, + "description": "Add option to include compressor stations for carbon dioxide transport. Before being able to be transported, carbon dioxide must be compressed to 150bar to reach a dense phase that makes large scale transport via pipeline feasible.", + "type": "boolean" + }, "cc_fraction": { "default": 0.9, "description": "The default fraction of CO2 captured with post-combustion capture.", diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index d9434ef76e..d604732d21 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -97,11 +97,13 @@ def define_spatial(nodes, options): spatial.co2.locations = nodes spatial.co2.vents = nodes + " co2 vent" spatial.co2.process_emissions = nodes + " process emissions" + spatial.co2.dense = nodes + " co2 stored dense" else: spatial.co2.nodes = ["co2 stored"] spatial.co2.locations = ["EU"] spatial.co2.vents = ["co2 vent"] spatial.co2.process_emissions = ["process emissions"] + spatial.co2.dense = ["co2 stored dense"] spatial.co2.df = pd.DataFrame(vars(spatial.co2), index=nodes) @@ -715,7 +717,7 @@ def add_eu_bus(n, x=-5.5, y=46): def add_co2_tracking( - n, costs, options, sequestration_potential_file=None, co2_price: float = 0.0 + n, costs, options, sequestration_potential_file=None, co2_price: float = 0.0, co2_liquefaction=False, ): """ Add CO2 tracking components to the network including atmospheric CO2, @@ -743,6 +745,8 @@ def add_co2_tracking( co2_price : float, optional CO2 price that needs to be paid for emitting into the atmosphere and which is gained by removing from the atmosphere. + co2_liquefaction : bool, optional + Whether to consider the liquefaction step with investment costs and electricity demand for compressors. Returns ------- @@ -806,16 +810,56 @@ def add_co2_tracking( carrier="co2 sequestered", unit="t_co2", ) + if co2_liquefaction: + n.add("Carrier", "co2 dense") + n.add( + "Bus", + spatial.co2.dense, + x=n.buses.loc[spatial.co2.locations, "x"].values, + y=n.buses.loc[spatial.co2.locations, "y"].values, + location=spatial.co2.locations, + carrier="co2 dense", + unit="t_co2", + ) + n.add("Carrier", "co2 compression") + n.add( + "Link", + spatial.co2.dense, + bus0=spatial.co2.nodes, + bus1=spatial.co2.dense, + bus2=spatial.nodes, + capital_cost=costs.at["CO2 liquefaction", "capital_cost"], + efficiency=1.0, + efficiency2=-0.16515, # TONI TODO + p_nom=0, + p_nom_extendable=True, + carrier="co2 compression", + unit="MWh", + ) + n.add("Carrier", "co2 expansion") + n.add( + "Link", + spatial.co2.nodes, + suffix=" expansion", + bus0=spatial.co2.dense, + bus1=spatial.co2.nodes, + efficiency=1.0, + p_nom=1e7, + carrier="co2 expansion", + unit="t_co2", + ) - n.add( - "Link", - sequestration_buses, - bus0=spatial.co2.nodes, - bus1=sequestration_buses, - carrier="co2 sequestered", - efficiency=1.0, - p_nom_extendable=True, - ) + else: + n.add( + "Link", + sequestration_buses, + bus0=spatial.co2.nodes, + bus1=sequestration_buses, + carrier="co2 sequestered", + marginal_cost=options["co2_sequestration_cost"], + efficiency=1.0, + p_nom_extendable=True, + ) if options["regional_co2_sequestration_potential"]["enable"]: if sequestration_potential_file is None: @@ -850,7 +894,6 @@ def add_co2_tracking( sequestration_buses, e_nom_extendable=True, e_nom_max=e_nom_max, - capital_cost=options["co2_sequestration_cost"], marginal_cost=-0.1, bus=sequestration_buses, lifetime=options["co2_sequestration_lifetime"], @@ -871,7 +914,7 @@ def add_co2_tracking( ) -def add_co2_network(n, costs, co2_network_cost_factor=1.0): +def add_co2_network(n, costs, co2_network_cost_factor=1.0, co2_liquefaction=False): """ Add CO2 transport network to the PyPSA network. @@ -889,7 +932,8 @@ def add_co2_network(n, costs, co2_network_cost_factor=1.0): columns co2_network_cost_factor : float, optional Factor to scale the capital costs of the CO2 network, default 1.0 - + co2_liquefaction : bool, optional + Whether to consider the liquefaction step for CO2 transport in dense phase or not. If True, compressor investment costs and electricity demand for compression are considered. Returns ------- None @@ -920,11 +964,16 @@ def add_co2_network(n, costs, co2_network_cost_factor=1.0): capital_cost = cost_onshore + cost_submarine capital_cost *= co2_network_cost_factor + if co2_liquefaction: + suffix = " co2 stored dense" + else: + suffix = " co2 stored" + n.add( "Link", co2_links.index, - bus0=co2_links.bus0.values + " co2 stored", - bus1=co2_links.bus1.values + " co2 stored", + bus0=co2_links.bus0.values + suffix, + bus1=co2_links.bus1.values + suffix, p_min_pu=-1, p_nom_extendable=True, length=co2_links.length.values, @@ -1103,8 +1152,6 @@ def add_methanol_to_power(n, costs, pop_layout, types=None): "Adding methanol CCGT power plants with post-combustion carbon capture." ) - # TODO consider efficiency changes / energy inputs for CC - # efficiency * EUR/MW * (annuity + FOM) capital_cost = costs.at["CCGT", "efficiency"] * costs.at["CCGT", "capital_cost"] @@ -1113,6 +1160,12 @@ def add_methanol_to_power(n, costs, pop_layout, types=None): + costs.at["cement capture", "capital_cost"] * costs.at["methanolisation", "carbondioxide-input"] ) + efficiency_cc = ( + costs.at["CCGT", "efficiency"] + - (costs.at["cement capture", "electricity-input"] + + costs.at["cement capture", "compression-electricity-input"]) + * costs.at["methanolisation", "carbondioxide-input"] + ) n.add( "Link", @@ -1126,7 +1179,7 @@ def add_methanol_to_power(n, costs, pop_layout, types=None): p_nom_extendable=True, capital_cost=capital_cost_cc, marginal_cost=costs.at["CCGT", "VOM"], - efficiency=costs.at["CCGT", "efficiency"], + efficiency=efficiency_cc, efficiency2=costs.at["cement capture", "capture_rate"] * costs.at["methanolisation", "carbondioxide-input"], efficiency3=(1 - costs.at["cement capture", "capture_rate"]) @@ -1194,6 +1247,11 @@ def add_methanol_reforming_cc(n, costs): + costs.at["cement capture", "capital_cost"] * costs.at["methanolisation", "carbondioxide-input"] ) + electricity_cc = ( + (costs.at["cement capture", "electricity-input"] + + costs.at["cement capture", "compression-electricity-input"]) + * costs.at["methanolisation", "carbondioxide-input"] + ) n.add( "Link", @@ -1203,6 +1261,7 @@ def add_methanol_reforming_cc(n, costs): bus1=spatial.h2.nodes, bus2="co2 atmosphere", bus3=spatial.co2.nodes, + bus4=spatial.h2.locations, p_nom_extendable=True, capital_cost=capital_cost_cc, efficiency=1 / costs.at[tech, "methanol-input"], @@ -1210,6 +1269,7 @@ def add_methanol_reforming_cc(n, costs): * costs.at["methanolisation", "carbondioxide-input"], efficiency3=costs.at["cement capture", "capture_rate"] * costs.at["methanolisation", "carbondioxide-input"], + efficiency4=-electricity_cc, carrier=f"{tech} CC", lifetime=costs.at[tech, "lifetime"], ) @@ -2113,6 +2173,12 @@ def add_h2_gas_infrastructure( ) if options["coal_cc"]: + efficiency_cc = ( + costs.at["coal", "efficiency"] + - (costs.at["biomass CHP capture", "electricity-input"] + + costs.at["biomass CHP capture", "compression-electricity-input"]) + * costs.at["coal", "CO2 intensity"] + ) n.add( "Link", spatial.nodes, @@ -2129,7 +2195,7 @@ def add_h2_gas_infrastructure( * costs.at["coal", "CO2 intensity"], # NB: fixed cost is per MWel p_nom_extendable=True, carrier="coal", - efficiency=costs.at["coal", "efficiency"], + efficiency=efficiency_cc, efficiency2=costs.at["coal", "CO2 intensity"] * (1 - costs.at["biomass CHP capture", "capture_rate"]), efficiency3=costs.at["coal", "CO2 intensity"] @@ -4612,6 +4678,13 @@ def add_industry( else: link_names = spatial.biomass.industry_cc + if options["biomass_spatial"]: + bus4=spatial.biomass.locations + efficiency4=costs.at["solid biomass", "CO2 intensity"] * (costs.at["cement capture", "electricity-input"]+ costs.at["cement capture", "compression-electricity-input"]) + else: + bus4="" + efficiency4=1.0 + n.add( "Link", link_names, @@ -4619,15 +4692,17 @@ def add_industry( bus1=spatial.biomass.industry, bus2="co2 atmosphere", bus3=spatial.co2.nodes, + bus4=bus4, carrier="solid biomass for industry CC", p_nom_extendable=True, capital_cost=costs.at["cement capture", "capital_cost"] * costs.at["solid biomass", "CO2 intensity"], - efficiency=0.9, # TODO: make config option + efficiency=options["cc_fraction"], efficiency2=-costs.at["solid biomass", "CO2 intensity"] * costs.at["cement capture", "capture_rate"], efficiency3=costs.at["solid biomass", "CO2 intensity"] * costs.at["cement capture", "capture_rate"], + efficiency4=-efficiency4, lifetime=costs.at["cement capture", "lifetime"], ) @@ -4643,8 +4718,12 @@ def add_industry( if options["gas_network"]: spatial_gas_demand = gas_demand.rename(index=lambda x: x + " gas for industry") + bus4=spatial.gas.locations + efficiency4=costs.at["gas", "CO2 intensity"] * (costs.at["cement capture", "electricity-input"]+ costs.at["cement capture", "compression-electricity-input"]) else: spatial_gas_demand = gas_demand.sum() + bus4="" + efficiency4=1.0 n.add( "Load", @@ -4673,6 +4752,7 @@ def add_industry( bus1=spatial.gas.industry, bus2="co2 atmosphere", bus3=spatial.co2.nodes, + bus4=bus4, carrier="gas for industry CC", p_nom_extendable=True, capital_cost=costs.at["cement capture", "capital_cost"] @@ -4682,6 +4762,7 @@ def add_industry( * (1 - costs.at["cement capture", "capture_rate"]), efficiency3=costs.at["gas", "CO2 intensity"] * costs.at["cement capture", "capture_rate"], + efficiency4=-efficiency4, lifetime=costs.at["cement capture", "lifetime"], ) @@ -5046,6 +5127,13 @@ def add_industry( ) # assume enough local waste heat for CC + if options["co2_spatial"]: + bus3=spatial.co2.locations + efficiency3=costs.at["cement capture", "electricity-input"]+ costs.at["cement capture", "compression-electricity-input"] + else: + bus3="" + efficiency3=1.0 + n.add( "Link", spatial.co2.locations, @@ -5053,11 +5141,13 @@ def add_industry( bus0=spatial.co2.process_emissions, bus1="co2 atmosphere", bus2=spatial.co2.nodes, + bus3=bus3, carrier="process emissions CC", p_nom_extendable=True, capital_cost=costs.at["cement capture", "capital_cost"], efficiency=1 - costs.at["cement capture", "capture_rate"], efficiency2=costs.at["cement capture", "capture_rate"], + efficiency3=-efficiency3, lifetime=costs.at["cement capture", "lifetime"], ) @@ -6241,9 +6331,10 @@ def add_import_options( snakemake = mock_snakemake( "prepare_sector_network", opts="", - clusters="10", + clusters="50", sector_opts="", - planning_horizons="2050", + planning_horizons="2035", + configfiles="config/config.default.yaml", ) configure_logging(snakemake) # pylint: disable=E0606 @@ -6323,6 +6414,7 @@ def add_import_options( options, sequestration_potential_file=snakemake.input.sequestration_potential, co2_price=co2_price, + co2_liquefaction=options["co2_network_liquefaction"], ) add_generation( @@ -6504,6 +6596,7 @@ def add_import_options( co2_network_cost_factor=snakemake.config["sector"][ "co2_network_cost_factor" ], + co2_liquefaction=options["co2_network_liquefaction"], ) if options["allam_cycle_gas"]: From a3a770aa0eed9cf20525bd7e9fc5b1741ce6a74a Mon Sep 17 00:00:00 2001 From: toniseibold Date: Thu, 7 May 2026 09:27:39 +0200 Subject: [PATCH 02/17] include co2_network_liquefaction in default config --- config/config.default.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/config.default.yaml b/config/config.default.yaml index ea95ad6d58..332394536e 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -872,6 +872,7 @@ sector: co2_spatial: true co2_network: true co2_network_cost_factor: 1 + co2_network_liquefaction: false cc_fraction: 0.9 hydrogen_underground_storage: true hydrogen_underground_storage_locations: From 019db1082137f12ddf67bc22a8bac693499ac600 Mon Sep 17 00:00:00 2001 From: toniseibold Date: Thu, 7 May 2026 09:35:01 +0200 Subject: [PATCH 03/17] documentation --- doc/release_notes.rst | 2 ++ scripts/lib/validation/config/sector.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 0f5a9325df..8671ebbb55 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -8,6 +8,8 @@ Release Notes .. Upcoming Release .. ================= +* Adding option to include the compression step in carbon dioxide transport before transporting in dense phase and including electricity demand for post combustion carbon capture. + * The industry reference year and the ammonia production data have been updated to 2023 (https://github.com/PyPSA/pypsa-eur/pull/2103) * refactor: Use scripts path provider consistently (https://github.com/PyPSA/pypsa-eur/pull/2093). diff --git a/scripts/lib/validation/config/sector.py b/scripts/lib/validation/config/sector.py index 5801f7fcbe..ef00a4cfa2 100644 --- a/scripts/lib/validation/config/sector.py +++ b/scripts/lib/validation/config/sector.py @@ -748,6 +748,10 @@ class SectorConfig(BaseModel): 1, description="The cost factor for the capital cost of the carbon dioxide transmission network.", ) + co2_network_liquefaction: bool = Field( + False, + description="Add option for including compressor stations with investment costs and electricity demand for liquefaction step for carbon dioxide before transport.", + ) cc_fraction: float = Field( 0.9, description="The default fraction of CO2 captured with post-combustion capture.", From 6a16ff82739c500c9ad6970e1a250049913e42a9 Mon Sep 17 00:00:00 2001 From: "pypsa[bot]" <181215446+pypsa[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 07:37:23 +0000 Subject: [PATCH 04/17] [pypsa-bot] run `generate-config` - `pixi run generate-config` --- config/schema.default.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/schema.default.json b/config/schema.default.json index ac1deffd65..9dabccbc85 100644 --- a/config/schema.default.json +++ b/config/schema.default.json @@ -4710,7 +4710,7 @@ }, "co2_network_liquefaction": { "default": false, - "description": "Add option to include compressor stations for carbon dioxide transport. Before being able to be transported, carbon dioxide must be compressed to 150bar to reach a dense phase that makes large scale transport via pipeline feasible.", + "description": "Add option for including compressor stations with investment costs and electricity demand for liquefaction step for carbon dioxide before transport.", "type": "boolean" }, "cc_fraction": { @@ -11003,7 +11003,7 @@ }, "co2_network_liquefaction": { "default": false, - "description": "Add option to include compressor stations for carbon dioxide transport. Before being able to be transported, carbon dioxide must be compressed to 150bar to reach a dense phase that makes large scale transport via pipeline feasible.", + "description": "Add option for including compressor stations with investment costs and electricity demand for liquefaction step for carbon dioxide before transport.", "type": "boolean" }, "cc_fraction": { From 6058df33f100b6d05344cc17f1424277cbb69a2f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 07:47:13 +0000 Subject: [PATCH 05/17] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/prepare_sector_network.py | 62 ++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 9550fbd360..b299392645 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -717,7 +717,12 @@ def add_eu_bus(n, x=-5.5, y=46): def add_co2_tracking( - n, costs, options, sequestration_potential_file=None, co2_price: float = 0.0, co2_liquefaction=False, + n, + costs, + options, + sequestration_potential_file=None, + co2_price: float = 0.0, + co2_liquefaction=False, ): """ Add CO2 tracking components to the network including atmospheric CO2, @@ -830,7 +835,7 @@ def add_co2_tracking( bus2=spatial.nodes, capital_cost=costs.at["CO2 liquefaction", "capital_cost"], efficiency=1.0, - efficiency2=-0.16515, # TONI TODO + efficiency2=-0.16515, # TONI TODO p_nom=0, p_nom_extendable=True, carrier="co2 compression", @@ -934,6 +939,7 @@ def add_co2_network(n, costs, co2_network_cost_factor=1.0, co2_liquefaction=Fals Factor to scale the capital costs of the CO2 network, default 1.0 co2_liquefaction : bool, optional Whether to consider the liquefaction step for CO2 transport in dense phase or not. If True, compressor investment costs and electricity demand for compression are considered. + Returns ------- None @@ -1163,8 +1169,10 @@ def add_methanol_to_power(n, costs, pop_layout, types=None): ) efficiency_cc = ( costs.at["CCGT", "efficiency"] - - (costs.at["cement capture", "electricity-input"] - + costs.at["cement capture", "compression-electricity-input"]) + - ( + costs.at["cement capture", "electricity-input"] + + costs.at["cement capture", "compression-electricity-input"] + ) * costs.at["methanolisation", "carbondioxide-input"] ) @@ -1250,10 +1258,9 @@ def add_methanol_reforming_cc(n, costs): * costs.at["methanolisation", "carbondioxide-input"] ) electricity_cc = ( - (costs.at["cement capture", "electricity-input"] - + costs.at["cement capture", "compression-electricity-input"]) - * costs.at["methanolisation", "carbondioxide-input"] - ) + costs.at["cement capture", "electricity-input"] + + costs.at["cement capture", "compression-electricity-input"] + ) * costs.at["methanolisation", "carbondioxide-input"] n.add( "Link", @@ -2178,8 +2185,10 @@ def add_h2_gas_infrastructure( if options["coal_cc"]: efficiency_cc = ( costs.at["coal", "efficiency"] - - (costs.at["biomass CHP capture", "electricity-input"] - + costs.at["biomass CHP capture", "compression-electricity-input"]) + - ( + costs.at["biomass CHP capture", "electricity-input"] + + costs.at["biomass CHP capture", "compression-electricity-input"] + ) * costs.at["coal", "CO2 intensity"] ) n.add( @@ -4689,11 +4698,14 @@ def add_industry( link_names = spatial.biomass.industry_cc if options["biomass_spatial"]: - bus4=spatial.biomass.locations - efficiency4=costs.at["solid biomass", "CO2 intensity"] * (costs.at["cement capture", "electricity-input"]+ costs.at["cement capture", "compression-electricity-input"]) + bus4 = spatial.biomass.locations + efficiency4 = costs.at["solid biomass", "CO2 intensity"] * ( + costs.at["cement capture", "electricity-input"] + + costs.at["cement capture", "compression-electricity-input"] + ) else: - bus4="" - efficiency4=1.0 + bus4 = "" + efficiency4 = 1.0 n.add( "Link", @@ -4728,12 +4740,15 @@ def add_industry( if options["gas_network"]: spatial_gas_demand = gas_demand.rename(index=lambda x: x + " gas for industry") - bus4=spatial.gas.locations - efficiency4=costs.at["gas", "CO2 intensity"] * (costs.at["cement capture", "electricity-input"]+ costs.at["cement capture", "compression-electricity-input"]) + bus4 = spatial.gas.locations + efficiency4 = costs.at["gas", "CO2 intensity"] * ( + costs.at["cement capture", "electricity-input"] + + costs.at["cement capture", "compression-electricity-input"] + ) else: spatial_gas_demand = gas_demand.sum() - bus4="" - efficiency4=1.0 + bus4 = "" + efficiency4 = 1.0 n.add( "Load", @@ -5138,11 +5153,14 @@ def add_industry( # assume enough local waste heat for CC if options["co2_spatial"]: - bus3=spatial.co2.locations - efficiency3=costs.at["cement capture", "electricity-input"]+ costs.at["cement capture", "compression-electricity-input"] + bus3 = spatial.co2.locations + efficiency3 = ( + costs.at["cement capture", "electricity-input"] + + costs.at["cement capture", "compression-electricity-input"] + ) else: - bus3="" - efficiency3=1.0 + bus3 = "" + efficiency3 = 1.0 n.add( "Link", From 9664f71f8b1451d48522728e7277d15da191709b Mon Sep 17 00:00:00 2001 From: toniseibold Date: Thu, 7 May 2026 11:44:00 +0200 Subject: [PATCH 06/17] bug fixing --- scripts/prepare_sector_network.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index b299392645..4e3d0f0d4d 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -835,7 +835,7 @@ def add_co2_tracking( bus2=spatial.nodes, capital_cost=costs.at["CO2 liquefaction", "capital_cost"], efficiency=1.0, - efficiency2=-0.16515, # TONI TODO + efficiency2=-costs.at["CO2 liquefaction", "electricity-input"], p_nom=0, p_nom_extendable=True, carrier="co2 compression", @@ -853,6 +853,16 @@ def add_co2_tracking( carrier="co2 expansion", unit="t_co2", ) + n.add( + "Link", + sequestration_buses, + bus0=spatial.co2.dense, + bus1=sequestration_buses, + carrier="co2 sequestered", + marginal_cost=options["co2_sequestration_cost"], + efficiency=1.0, + p_nom_extendable=True, + ) else: n.add( From 11628b8c892230e35cb96b224f558988e48592fa Mon Sep 17 00:00:00 2001 From: toniseibold Date: Thu, 7 May 2026 16:37:00 +0200 Subject: [PATCH 07/17] adjusting capital costs for post combustion capture depending on flue gas concentration of carbon dioxide --- config/config.default.yaml | 6 ++++++ config/schema.default.json | 14 ++++++++++++++ doc/release_notes.rst | 2 +- scripts/lib/validation/config/sector.py | 11 ++++++++++- scripts/prepare_sector_network.py | 17 ++++++++++------- 5 files changed, 41 insertions(+), 9 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index c808afd249..fd2324326b 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -875,6 +875,12 @@ sector: co2_network_cost_factor: 1 co2_network_liquefaction: false cc_fraction: 0.9 + cc_capital_cost_factor: + gas: 2.0 + biomass: 1.8 + coal: 1.8 + waste: 1.7 + cement: 1.0 hydrogen_underground_storage: true hydrogen_underground_storage_locations: - onshore diff --git a/config/schema.default.json b/config/schema.default.json index 008c870133..4202b91cef 100644 --- a/config/schema.default.json +++ b/config/schema.default.json @@ -4723,6 +4723,13 @@ "description": "The default fraction of CO2 captured with post-combustion capture.", "type": "number" }, + "cc_capital_cost_factor": { + "additionalProperties": { + "type": "number" + }, + "description": "Size of the carbon capture unit depending on the amount of carbon dioxide in the flue gas. The more CO2, the smaller the capture unit and thus the lower the capital cost factor. The default values are based on the DEA technology-data report.", + "type": "object" + }, "hydrogen_underground_storage": { "default": true, "description": "Add options for storing hydrogen underground. Storage potential depends regionally.", @@ -11177,6 +11184,13 @@ "description": "The default fraction of CO2 captured with post-combustion capture.", "type": "number" }, + "cc_capital_cost_factor": { + "additionalProperties": { + "type": "number" + }, + "description": "Size of the carbon capture unit depending on the amount of carbon dioxide in the flue gas. The more CO2, the smaller the capture unit and thus the lower the capital cost factor. The default values are based on the DEA technology-data report.", + "type": "object" + }, "hydrogen_underground_storage": { "default": true, "description": "Add options for storing hydrogen underground. Storage potential depends regionally.", diff --git a/doc/release_notes.rst b/doc/release_notes.rst index c115570538..2e792998cb 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -9,7 +9,7 @@ Release Notes .. Upcoming Release .. ================= -* Adding option to include the compression step in carbon dioxide transport before transporting in dense phase and including electricity demand for post combustion carbon capture. +* Adding option to include the compression step in carbon dioxide transport before transporting in dense phase and including electricity demand for post combustion carbon capture. Adjusting the capital costs for post combustion capture that differs depending on the carbon dioxide percentage in the flue gas. * Fix: Re-introduce capital costs for non-bicharging discharge links in ``add_electricity.py``, e.g. fuel cells. diff --git a/scripts/lib/validation/config/sector.py b/scripts/lib/validation/config/sector.py index ef00a4cfa2..58992ed89a 100644 --- a/scripts/lib/validation/config/sector.py +++ b/scripts/lib/validation/config/sector.py @@ -756,7 +756,16 @@ class SectorConfig(BaseModel): 0.9, description="The default fraction of CO2 captured with post-combustion capture.", ) - + cc_capital_cost_factor: dict[str, float] = Field( + default_factory=lambda: { + "gas": 2.0, + "biomass": 1.8, + "coal": 1.8, + "waste": 1.7, + "cement": 1.0 + }, + description="Size of the carbon capture unit depending on the amount of carbon dioxide in the flue gas. The more CO2, the smaller the capture unit and thus the lower the capital cost factor. The default values are based on the DEA technology-data report.", + ) hydrogen_underground_storage: bool = Field( True, description="Add options for storing hydrogen underground. Storage potential depends regionally.", diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 4e3d0f0d4d..79fb5214b8 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1174,7 +1174,7 @@ def add_methanol_to_power(n, costs, pop_layout, types=None): capital_cost_cc = ( capital_cost - + costs.at["cement capture", "capital_cost"] + + costs.at["cement capture", "capital_cost"] * options["cc_capital_cost_factor"]["gas"] * costs.at["methanolisation", "carbondioxide-input"] ) efficiency_cc = ( @@ -1265,6 +1265,7 @@ def add_methanol_reforming_cc(n, costs): capital_cost_cc = ( capital_cost + costs.at["cement capture", "capital_cost"] + * options["cc_capital_cost_factor"]["gas"] * costs.at["methanolisation", "carbondioxide-input"] ) electricity_cc = ( @@ -4728,8 +4729,9 @@ def add_industry( carrier="solid biomass for industry CC", p_nom_extendable=True, capital_cost=costs.at["cement capture", "capital_cost"] - * costs.at["solid biomass", "CO2 intensity"], - efficiency=options["cc_fraction"], + * costs.at["solid biomass", "CO2 intensity"] + * options["cc_capital_cost_factor"]["biomass"], + efficiency=0.9, # TODO: make config option efficiency2=-costs.at["solid biomass", "CO2 intensity"] * costs.at["cement capture", "capture_rate"], efficiency3=costs.at["solid biomass", "CO2 intensity"] @@ -4791,6 +4793,7 @@ def add_industry( carrier="gas for industry CC", p_nom_extendable=True, capital_cost=costs.at["cement capture", "capital_cost"] + * options["cc_capital_cost_factor"]["gas"] * costs.at["gas", "CO2 intensity"], efficiency=0.9, efficiency2=costs.at["gas", "CO2 intensity"] @@ -5182,7 +5185,8 @@ def add_industry( bus3=bus3, carrier="process emissions CC", p_nom_extendable=True, - capital_cost=costs.at["cement capture", "capital_cost"], + capital_cost=costs.at["cement capture", "capital_cost"] + * options["cc_capital_cost_factor"]["cement"], efficiency=1 - costs.at["cement capture", "capture_rate"], efficiency2=costs.at["cement capture", "capture_rate"], efficiency3=-efficiency3, @@ -6369,10 +6373,9 @@ def add_import_options( snakemake = mock_snakemake( "prepare_sector_network", opts="", - clusters="50", + clusters="10", sector_opts="", - planning_horizons="2035", - configfiles="config/config.default.yaml", + planning_horizons="2050", ) configure_logging(snakemake) # pylint: disable=E0606 From fc2aa92969711b38c5cf01d1dc7beccb4a32981a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 14:37:19 +0000 Subject: [PATCH 08/17] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/lib/validation/config/sector.py | 2 +- scripts/prepare_sector_network.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/lib/validation/config/sector.py b/scripts/lib/validation/config/sector.py index 58992ed89a..c320d00a8e 100644 --- a/scripts/lib/validation/config/sector.py +++ b/scripts/lib/validation/config/sector.py @@ -762,7 +762,7 @@ class SectorConfig(BaseModel): "biomass": 1.8, "coal": 1.8, "waste": 1.7, - "cement": 1.0 + "cement": 1.0, }, description="Size of the carbon capture unit depending on the amount of carbon dioxide in the flue gas. The more CO2, the smaller the capture unit and thus the lower the capital cost factor. The default values are based on the DEA technology-data report.", ) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 79fb5214b8..50a17a3ec2 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1174,7 +1174,8 @@ def add_methanol_to_power(n, costs, pop_layout, types=None): capital_cost_cc = ( capital_cost - + costs.at["cement capture", "capital_cost"] * options["cc_capital_cost_factor"]["gas"] + + costs.at["cement capture", "capital_cost"] + * options["cc_capital_cost_factor"]["gas"] * costs.at["methanolisation", "carbondioxide-input"] ) efficiency_cc = ( @@ -4731,7 +4732,7 @@ def add_industry( capital_cost=costs.at["cement capture", "capital_cost"] * costs.at["solid biomass", "CO2 intensity"] * options["cc_capital_cost_factor"]["biomass"], - efficiency=0.9, # TODO: make config option + efficiency=0.9, # TODO: make config option efficiency2=-costs.at["solid biomass", "CO2 intensity"] * costs.at["cement capture", "capture_rate"], efficiency3=costs.at["solid biomass", "CO2 intensity"] From 68e02de470cead64c45b0ab801ac0aae81bfa20a Mon Sep 17 00:00:00 2001 From: Toni <153275395+toniseibold@users.noreply.github.com> Date: Thu, 7 May 2026 16:40:53 +0200 Subject: [PATCH 09/17] Fix unit Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- 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 50a17a3ec2..d42b5f3453 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -839,7 +839,7 @@ def add_co2_tracking( p_nom=0, p_nom_extendable=True, carrier="co2 compression", - unit="MWh", + unit="t_co2", ) n.add("Carrier", "co2 expansion") n.add( From b425f45703324ba4b750a1e4ff44349fd2b2bf75 Mon Sep 17 00:00:00 2001 From: toniseibold Date: Thu, 7 May 2026 16:46:43 +0200 Subject: [PATCH 10/17] bug fix --- 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 d42b5f3453..2ef9649fc6 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1113,7 +1113,7 @@ def add_biomass_to_methanol_cc(n, costs): ) -def add_methanol_to_power(n, costs, pop_layout, types=None): +def add_methanol_to_power(n, costs, pop_layout, options=options, types=None): if types is None: types = {} @@ -1252,7 +1252,7 @@ def add_methanol_reforming(n, costs): ) -def add_methanol_reforming_cc(n, costs): +def add_methanol_reforming_cc(n, costs, options): logger.info("Adding methanol steam reforming with carbon capture.") tech = "Methanol steam reforming" @@ -3824,6 +3824,7 @@ def add_methanol( n=n, costs=costs, pop_layout=pop_layout, + options=options, types=methanol_options["methanol_to_power"], ) @@ -3831,7 +3832,7 @@ def add_methanol( add_methanol_reforming(n=n, costs=costs) if methanol_options["methanol_reforming_cc"]: - add_methanol_reforming_cc(n=n, costs=costs) + add_methanol_reforming_cc(n=n, costs=costs, options=options) def add_biomass( From d17a13b2c7d2393d46ba33fabac5a63f66dd5c32 Mon Sep 17 00:00:00 2001 From: toniseibold Date: Thu, 7 May 2026 18:13:55 +0200 Subject: [PATCH 11/17] bug fix --- 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 2ef9649fc6..7ebe6b36ea 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1113,7 +1113,7 @@ def add_biomass_to_methanol_cc(n, costs): ) -def add_methanol_to_power(n, costs, pop_layout, options=options, types=None): +def add_methanol_to_power(n, costs, pop_layout, options, types=None): if types is None: types = {} From 2bfaa20d82c60c7d2a8d6b3383b2f01422e9e01d Mon Sep 17 00:00:00 2001 From: toniseibold Date: Tue, 12 May 2026 09:40:14 +0200 Subject: [PATCH 12/17] cleaning up and minor fixes --- scripts/prepare_sector_network.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 7ebe6b36ea..a9942c7771 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -97,13 +97,13 @@ def define_spatial(nodes, options): spatial.co2.locations = nodes spatial.co2.vents = nodes + " co2 vent" spatial.co2.process_emissions = nodes + " process emissions" - spatial.co2.dense = nodes + " co2 stored dense" + spatial.co2.dense = nodes + " co2 dense" else: spatial.co2.nodes = ["co2 stored"] spatial.co2.locations = ["EU"] spatial.co2.vents = ["co2 vent"] spatial.co2.process_emissions = ["process emissions"] - spatial.co2.dense = ["co2 stored dense"] + spatial.co2.dense = ["co2 dense"] spatial.co2.df = pd.DataFrame(vars(spatial.co2), index=nodes) @@ -981,7 +981,7 @@ def add_co2_network(n, costs, co2_network_cost_factor=1.0, co2_liquefaction=Fals capital_cost *= co2_network_cost_factor if co2_liquefaction: - suffix = " co2 stored dense" + suffix = " co2 dense" else: suffix = " co2 stored" @@ -1199,7 +1199,7 @@ def add_methanol_to_power(n, costs, pop_layout, options, types=None): p_nom_extendable=True, capital_cost=capital_cost_cc, marginal_cost=costs.at["CCGT", "VOM"] - * costs.at["CCGT", "efficiency"], # NB: VOM is per MWel + * efficiency_cc, # NB: VOM is per MWel efficiency=efficiency_cc, efficiency2=costs.at["cement capture", "capture_rate"] * costs.at["methanolisation", "carbondioxide-input"], From df9e105ffcedb7f5d37b71ffb60076a444e55804 Mon Sep 17 00:00:00 2001 From: Bobby Xiong Date: Tue, 12 May 2026 12:57:08 +0200 Subject: [PATCH 13/17] Updated plot_balance_map* scripts to include co2 dense carrier in co2 stored maps. --- scripts/plot_balance_map.py | 25 ++++++++++++++----- scripts/plot_balance_map_interactive.py | 32 ++++++++++++++++++------- 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/scripts/plot_balance_map.py b/scripts/plot_balance_map.py index a58e82b658..fcb9fd7c2f 100644 --- a/scripts/plot_balance_map.py +++ b/scripts/plot_balance_map.py @@ -80,13 +80,26 @@ n.buses["x"] = n.buses.location.map(n.buses.x) n.buses["y"] = n.buses.location.map(n.buses.y) - # bus_size according to energy balance of bus carrier - eb = n.statistics.energy_balance(bus_carrier=carrier, groupby=["bus", "carrier"]) + if carrier == "co2 stored" and "co2 dense" in n.buses.carrier.unique(): + co2_carriers = ["co2 stored", "co2 dense"] + # Aggregate energy balance of "co2 stored" and "co2 dense" to get the total CO2 balance for each bus + eb = n.statistics.energy_balance(bus_carrier=co2_carriers, groupby=["bus", "carrier"]) + eb = eb.rename(index=lambda value: value.replace("co2 dense", carrier), level="bus") + eb = eb.groupby(level=["component", "bus", "carrier"]).sum() + + # remove energy balance of transmission carriers which relate to losses + transmission_carriers = get_transmission_carriers(n, bus_carrier=co2_carriers).rename( + {"name": "carrier"} + ) + else: + # bus_size according to energy balance of bus carrier + eb = n.statistics.energy_balance(bus_carrier=carrier, groupby=["bus", "carrier"]) + + # remove energy balance of transmission carriers which relate to losses + transmission_carriers = get_transmission_carriers(n, bus_carrier=carrier).rename( + {"name": "carrier"} + ) - # remove energy balance of transmission carriers which relate to losses - transmission_carriers = get_transmission_carriers(n, bus_carrier=carrier).rename( - {"name": "carrier"} - ) components = transmission_carriers.unique("component") carriers = transmission_carriers.unique("carrier") diff --git a/scripts/plot_balance_map_interactive.py b/scripts/plot_balance_map_interactive.py index 14078bb182..d60db5c39e 100644 --- a/scripts/plot_balance_map_interactive.py +++ b/scripts/plot_balance_map_interactive.py @@ -115,18 +115,32 @@ def scalar_to_rgba( b_missing = n.carriers.query("color == '' or color.isnull()").index n.carriers.loc[b_missing, "color"] = missing_color - transmission_carriers = get_transmission_carriers(n, bus_carrier=carrier).rename( - {"name": "carrier"} - ) + if carrier == "co2 stored" and "co2 dense" in n.buses.carrier.unique(): + co2_carriers = ["co2 stored", "co2 dense"] + transmission_carriers = get_transmission_carriers( + n, bus_carrier=co2_carriers + ).rename({"name": "carrier"}) + + eb = n.statistics.energy_balance( + bus_carrier=co2_carriers, + groupby=["bus", "carrier"], + ) + eb = eb.rename(index=lambda value: value.replace("co2 dense", carrier), level="bus") + eb = eb.groupby(level=["component", "bus", "carrier"]).sum() + else: + transmission_carriers = get_transmission_carriers(n, bus_carrier=carrier).rename( + {"name": "carrier"} + ) + + ### Pie charts + eb = n.statistics.energy_balance( + bus_carrier=carrier, + groupby=["bus", "carrier"], + ) + components = transmission_carriers.unique("component") carriers = transmission_carriers.unique("carrier") - ### Pie charts - eb = n.statistics.energy_balance( - bus_carrier=carrier, - groupby=["bus", "carrier"], - ) - # Only carriers that are also in the energy balance carriers_in_eb = carriers[carriers.isin(eb.index.get_level_values("carrier"))] From b89dc7f864e58f0729baff8cbe237f07e22d6cb4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 10:57:40 +0000 Subject: [PATCH 14/17] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/plot_balance_map.py | 24 +++++++++++++++--------- scripts/plot_balance_map_interactive.py | 10 ++++++---- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/scripts/plot_balance_map.py b/scripts/plot_balance_map.py index fcb9fd7c2f..6307e513b3 100644 --- a/scripts/plot_balance_map.py +++ b/scripts/plot_balance_map.py @@ -83,22 +83,28 @@ if carrier == "co2 stored" and "co2 dense" in n.buses.carrier.unique(): co2_carriers = ["co2 stored", "co2 dense"] # Aggregate energy balance of "co2 stored" and "co2 dense" to get the total CO2 balance for each bus - eb = n.statistics.energy_balance(bus_carrier=co2_carriers, groupby=["bus", "carrier"]) - eb = eb.rename(index=lambda value: value.replace("co2 dense", carrier), level="bus") + eb = n.statistics.energy_balance( + bus_carrier=co2_carriers, groupby=["bus", "carrier"] + ) + eb = eb.rename( + index=lambda value: value.replace("co2 dense", carrier), level="bus" + ) eb = eb.groupby(level=["component", "bus", "carrier"]).sum() # remove energy balance of transmission carriers which relate to losses - transmission_carriers = get_transmission_carriers(n, bus_carrier=co2_carriers).rename( - {"name": "carrier"} - ) + transmission_carriers = get_transmission_carriers( + n, bus_carrier=co2_carriers + ).rename({"name": "carrier"}) else: # bus_size according to energy balance of bus carrier - eb = n.statistics.energy_balance(bus_carrier=carrier, groupby=["bus", "carrier"]) + eb = n.statistics.energy_balance( + bus_carrier=carrier, groupby=["bus", "carrier"] + ) # remove energy balance of transmission carriers which relate to losses - transmission_carriers = get_transmission_carriers(n, bus_carrier=carrier).rename( - {"name": "carrier"} - ) + transmission_carriers = get_transmission_carriers( + n, bus_carrier=carrier + ).rename({"name": "carrier"}) components = transmission_carriers.unique("component") carriers = transmission_carriers.unique("carrier") diff --git a/scripts/plot_balance_map_interactive.py b/scripts/plot_balance_map_interactive.py index d60db5c39e..fe1d972671 100644 --- a/scripts/plot_balance_map_interactive.py +++ b/scripts/plot_balance_map_interactive.py @@ -125,12 +125,14 @@ def scalar_to_rgba( bus_carrier=co2_carriers, groupby=["bus", "carrier"], ) - eb = eb.rename(index=lambda value: value.replace("co2 dense", carrier), level="bus") + eb = eb.rename( + index=lambda value: value.replace("co2 dense", carrier), level="bus" + ) eb = eb.groupby(level=["component", "bus", "carrier"]).sum() else: - transmission_carriers = get_transmission_carriers(n, bus_carrier=carrier).rename( - {"name": "carrier"} - ) + transmission_carriers = get_transmission_carriers( + n, bus_carrier=carrier + ).rename({"name": "carrier"}) ### Pie charts eb = n.statistics.energy_balance( From a14b4580bc3947d6b1cac7af0cd0fd34c11a295e Mon Sep 17 00:00:00 2001 From: toniseibold Date: Tue, 12 May 2026 13:43:11 +0200 Subject: [PATCH 15/17] change technology data name in line with technology-data PR --- 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 a9942c7771..def10a64ab 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -833,9 +833,9 @@ def add_co2_tracking( bus0=spatial.co2.nodes, bus1=spatial.co2.dense, bus2=spatial.nodes, - capital_cost=costs.at["CO2 liquefaction", "capital_cost"], + capital_cost=costs.at["CO2 dense phase compression", "capital_cost"], efficiency=1.0, - efficiency2=-costs.at["CO2 liquefaction", "electricity-input"], + efficiency2=-costs.at["CO2 dense phase compression", "electricity-input"], p_nom=0, p_nom_extendable=True, carrier="co2 compression", From 103366cb5fcfc0d622e99e05554fe3e83bcf10a7 Mon Sep 17 00:00:00 2001 From: toniseibold Date: Tue, 12 May 2026 13:55:51 +0200 Subject: [PATCH 16/17] making technology-data pr prequisite for this pr --- data/versions.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/versions.csv b/data/versions.csv index 4cf11f88cf..f6a07f450e 100644 --- a/data/versions.csv +++ b/data/versions.csv @@ -18,7 +18,7 @@ copernicus_land_cover,v3.0.1,archive,latest supported,2026-01-20,"The primary is copernicus_land_cover,v2.0.2,archive,deprecated supported,2026-01-13,,https://data.pypsa.org/workflows/eur/copernicus_land_cover/v2.0.2/PROBAV_LC100_global_v3.0.1_2015-base_Discrete-Classification-map_EPSG-4326.tif corine,v18_5,archive,latest supported,2026-01-13,,https://data.pypsa.org/workflows/eur/corine/v18_5/corine.zip corine,unknown,primary,latest supported,2025-12-02,Need to register with CLMS API and create an access token. The download URL is dynamic, -costs,v0.14.0,primary,latest supported,2026-02-13,Part of the `technologydata` repository and versioned on GitHub.,https://raw.githubusercontent.com/PyPSA/technology-data/refs/tags/v0.14.0/outputs +costs,co2_management,primary,latest supported,2026-05-12,Part of the `technologydata` repository and versioned on GitHub.,https://github.com/PyPSA/technology-data/tree/co2_management/outputs costs,v0.14.0,archive,latest supported,2026-02-13,Part of the `technologydata` repository and versioned on GitHub.,https://data.pypsa.org/workflows/eur/costs/v0.14.0 costs,v0.13.4,primary,supported,2025-12-02,Part of the `technologydata` repository and versioned on GitHub.,https://raw.githubusercontent.com/PyPSA/technology-data/refs/tags/v0.13.4/outputs costs,v0.13.4,archive,supported,2026-01-21,Part of the `technologydata` repository and versioned on GitHub.,https://data.pypsa.org/workflows/eur/costs/v0.13.4 From 50418992ef5705659c630122c5e25c8c3add6967 Mon Sep 17 00:00:00 2001 From: toniseibold Date: Tue, 26 May 2026 11:14:52 +0200 Subject: [PATCH 17/17] addressing change requests of documentation, bugfixing and spatially resolving gas and biomass for industry --- config/config.default.yaml | 6 +- config/schema.default.json | 4 +- scripts/lib/validation/config/sector.py | 8 +-- scripts/prepare_sector_network.py | 87 +++++++++---------------- 4 files changed, 41 insertions(+), 64 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index fd2324326b..3bcf65adbb 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -877,9 +877,9 @@ sector: cc_fraction: 0.9 cc_capital_cost_factor: gas: 2.0 - biomass: 1.8 - coal: 1.8 - waste: 1.7 + biomass: 1.1 + coal: 1.1 + waste: 1.2 cement: 1.0 hydrogen_underground_storage: true hydrogen_underground_storage_locations: diff --git a/config/schema.default.json b/config/schema.default.json index 4202b91cef..f9874bf191 100644 --- a/config/schema.default.json +++ b/config/schema.default.json @@ -4727,7 +4727,7 @@ "additionalProperties": { "type": "number" }, - "description": "Size of the carbon capture unit depending on the amount of carbon dioxide in the flue gas. The more CO2, the smaller the capture unit and thus the lower the capital cost factor. The default values are based on the DEA technology-data report.", + "description": "Size of the carbon capture unit depending on the amount of carbon dioxide in the flue gas. The more CO2, the smaller the capture unit and thus the lower the capital cost factor. Factors are given relative to cement capture. The default values are based on the DEA technology-data report on carbon capture, transport and storage Table 8 / Figure 12 (https://ens.dk/en/analyses-and-statistics/technology-data-carbon-capture-transport-and-storage).", "type": "object" }, "hydrogen_underground_storage": { @@ -11188,7 +11188,7 @@ "additionalProperties": { "type": "number" }, - "description": "Size of the carbon capture unit depending on the amount of carbon dioxide in the flue gas. The more CO2, the smaller the capture unit and thus the lower the capital cost factor. The default values are based on the DEA technology-data report.", + "description": "Size of the carbon capture unit depending on the amount of carbon dioxide in the flue gas. The more CO2, the smaller the capture unit and thus the lower the capital cost factor. Factors are given relative to cement capture. The default values are based on the DEA technology-data report on carbon capture, transport and storage Table 8 / Figure 12 (https://ens.dk/en/analyses-and-statistics/technology-data-carbon-capture-transport-and-storage).", "type": "object" }, "hydrogen_underground_storage": { diff --git a/scripts/lib/validation/config/sector.py b/scripts/lib/validation/config/sector.py index c320d00a8e..37d4f030f7 100644 --- a/scripts/lib/validation/config/sector.py +++ b/scripts/lib/validation/config/sector.py @@ -759,12 +759,12 @@ class SectorConfig(BaseModel): cc_capital_cost_factor: dict[str, float] = Field( default_factory=lambda: { "gas": 2.0, - "biomass": 1.8, - "coal": 1.8, - "waste": 1.7, + "biomass": 1.1, + "coal": 1.1, + "waste": 1.2, "cement": 1.0, }, - description="Size of the carbon capture unit depending on the amount of carbon dioxide in the flue gas. The more CO2, the smaller the capture unit and thus the lower the capital cost factor. The default values are based on the DEA technology-data report.", + description="Size of the carbon capture unit depending on the amount of carbon dioxide in the flue gas. The more CO2, the smaller the capture unit and thus the lower the capital cost factor. Factors are given relative to cement capture. The default values are based on the DEA technology-data report on carbon capture, transport and storage Table 8 / Figure 12 (https://ens.dk/en/analyses-and-statistics/technology-data-carbon-capture-transport-and-storage).", ) hydrogen_underground_storage: bool = Field( True, diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index def10a64ab..ecd4fd9fdd 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -71,8 +71,6 @@ def define_spatial(nodes, options): spatial.biomass.nodes_unsustainable = nodes + " unsustainable solid biomass" spatial.biomass.bioliquids = nodes + " unsustainable bioliquids" spatial.biomass.locations = nodes - spatial.biomass.industry = nodes + " solid biomass for industry" - spatial.biomass.industry_cc = nodes + " solid biomass for industry CC" spatial.msw.nodes = nodes + " municipal solid waste" spatial.msw.locations = nodes else: @@ -80,10 +78,11 @@ def define_spatial(nodes, options): spatial.biomass.nodes_unsustainable = ["EU unsustainable solid biomass"] spatial.biomass.bioliquids = ["EU unsustainable bioliquids"] spatial.biomass.locations = ["EU"] - spatial.biomass.industry = ["solid biomass for industry"] - spatial.biomass.industry_cc = ["solid biomass for industry CC"] spatial.msw.nodes = ["EU municipal solid waste"] spatial.msw.locations = ["EU"] + spatial.biomass.industry = nodes + " solid biomass for industry" + spatial.biomass.industry_cc = nodes + " solid biomass for industry CC" + spatial.biomass.industry.locations = nodes spatial.biomass.df = pd.DataFrame(vars(spatial.biomass), index=nodes) spatial.msw.df = pd.DataFrame(vars(spatial.msw), index=nodes) @@ -115,24 +114,20 @@ def define_spatial(nodes, options): spatial.gas.nodes = nodes + " gas" spatial.gas.locations = nodes spatial.gas.biogas = nodes + " biogas" - 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"] spatial.gas.biogas = ["EU biogas"] - spatial.gas.industry = ["gas for industry"] spatial.gas.biogas_to_gas = ["EU biogas to gas"] if options.get("biomass_spatial", options["biomass_transport"]): spatial.gas.biogas_to_gas_cc = nodes + " biogas to gas CC" else: spatial.gas.biogas_to_gas_cc = ["EU biogas to gas CC"] - if options.get("co2_spatial", options["co2_network"]): - spatial.gas.industry_cc = nodes + " gas for industry CC" - else: - spatial.gas.industry_cc = ["gas for industry CC"] + spatial.gas.industry = nodes + " gas for industry" + spatial.gas.industry_cc = nodes + " gas for industry CC" + spatial.gas.industry.locations = nodes spatial.gas.df = pd.DataFrame(vars(spatial.gas), index=nodes) @@ -836,7 +831,6 @@ def add_co2_tracking( capital_cost=costs.at["CO2 dense phase compression", "capital_cost"], efficiency=1.0, efficiency2=-costs.at["CO2 dense phase compression", "electricity-input"], - p_nom=0, p_nom_extendable=True, carrier="co2 compression", unit="t_co2", @@ -861,7 +855,8 @@ def add_co2_tracking( carrier="co2 sequestered", marginal_cost=options["co2_sequestration_cost"], efficiency=1.0, - p_nom_extendable=True, + p_nom=np.inf, + p_nom_extendable=False, ) else: @@ -873,7 +868,8 @@ def add_co2_tracking( carrier="co2 sequestered", marginal_cost=options["co2_sequestration_cost"], efficiency=1.0, - p_nom_extendable=True, + p_nom=np.inf, + p_nom_extendable=False, ) if options["regional_co2_sequestration_potential"]["enable"]: @@ -1199,7 +1195,7 @@ def add_methanol_to_power(n, costs, pop_layout, options, types=None): p_nom_extendable=True, capital_cost=capital_cost_cc, marginal_cost=costs.at["CCGT", "VOM"] - * efficiency_cc, # NB: VOM is per MWel + * costs.at["CCGT", "efficiency"], # NB: VOM is per MWel efficiency=efficiency_cc, efficiency2=costs.at["cement capture", "capture_rate"] * costs.at["methanolisation", "carbondioxide-input"], @@ -4672,20 +4668,17 @@ def add_industry( n.add( "Bus", spatial.biomass.industry, - location=spatial.biomass.locations, + location=spatial.biomass.industry.locations, carrier="solid biomass for industry", unit="MWh_LHV", ) - if options.get("biomass_spatial", options["biomass_transport"]): - p_set = ( - industrial_demand.loc[spatial.biomass.locations, "solid biomass"].rename( - index=lambda x: x + " solid biomass for industry" - ) - / nhours + p_set = ( + industrial_demand.loc[spatial.biomass.industry.locations, "solid biomass"].rename( + index=lambda x: x + " solid biomass for industry" ) - else: - p_set = industrial_demand["solid biomass"].sum() / nhours + / nhours + ) n.add( "Load", @@ -4705,29 +4698,19 @@ def add_industry( efficiency=1.0, ) - if len(spatial.biomass.industry_cc) <= 1 and len(spatial.co2.nodes) > 1: - link_names = nodes + " " + spatial.biomass.industry_cc - else: - link_names = spatial.biomass.industry_cc - - if options["biomass_spatial"]: - bus4 = spatial.biomass.locations - efficiency4 = costs.at["solid biomass", "CO2 intensity"] * ( - costs.at["cement capture", "electricity-input"] - + costs.at["cement capture", "compression-electricity-input"] - ) - else: - bus4 = "" - efficiency4 = 1.0 + ele_for_cc = costs.at["solid biomass", "CO2 intensity"] * ( + costs.at["cement capture", "electricity-input"] + + costs.at["cement capture", "compression-electricity-input"] + ) n.add( "Link", - link_names, + spatial.biomass.industry_cc, bus0=spatial.biomass.nodes, bus1=spatial.biomass.industry, bus2="co2 atmosphere", bus3=spatial.co2.nodes, - bus4=bus4, + bus4=spatial.biomass.industry.locations, carrier="solid biomass for industry CC", p_nom_extendable=True, capital_cost=costs.at["cement capture", "capital_cost"] @@ -4738,31 +4721,21 @@ def add_industry( * costs.at["cement capture", "capture_rate"], efficiency3=costs.at["solid biomass", "CO2 intensity"] * costs.at["cement capture", "capture_rate"], - efficiency4=-efficiency4, + efficiency4=-ele_for_cc, lifetime=costs.at["cement capture", "lifetime"], ) n.add( "Bus", spatial.gas.industry, - location=spatial.gas.locations, + location=spatial.gas.industry.locations, carrier="gas for industry", unit="MWh_LHV", ) gas_demand = industrial_demand.loc[nodes, "methane"] / nhours - if options["gas_network"]: - spatial_gas_demand = gas_demand.rename(index=lambda x: x + " gas for industry") - bus4 = spatial.gas.locations - efficiency4 = costs.at["gas", "CO2 intensity"] * ( - costs.at["cement capture", "electricity-input"] - + costs.at["cement capture", "compression-electricity-input"] - ) - else: - spatial_gas_demand = gas_demand.sum() - bus4 = "" - efficiency4 = 1.0 + spatial_gas_demand = gas_demand.rename(index=lambda x: x + " gas for industry") n.add( "Load", @@ -4784,6 +4757,10 @@ def add_industry( efficiency2=costs.at["gas", "CO2 intensity"], ) + ele_for_cc = costs.at["gas", "CO2 intensity"] * ( + costs.at["cement capture", "electricity-input"] + + costs.at["cement capture", "compression-electricity-input"] + ) n.add( "Link", spatial.gas.industry_cc, @@ -4791,7 +4768,7 @@ def add_industry( bus1=spatial.gas.industry, bus2="co2 atmosphere", bus3=spatial.co2.nodes, - bus4=bus4, + bus4=spatial.gas.industry.locations, carrier="gas for industry CC", p_nom_extendable=True, capital_cost=costs.at["cement capture", "capital_cost"] @@ -4802,7 +4779,7 @@ def add_industry( * (1 - costs.at["cement capture", "capture_rate"]), efficiency3=costs.at["gas", "CO2 intensity"] * costs.at["cement capture", "capture_rate"], - efficiency4=-efficiency4, + efficiency4=-ele_for_cc, lifetime=costs.at["cement capture", "lifetime"], )