From a033ba45edf9b91addf00322962f859d91943856 Mon Sep 17 00:00:00 2001 From: Michael Lindner Date: Mon, 12 Oct 2026 13:08:10 +0200 Subject: [PATCH 1/4] switch co2 sequestered stores to generators --- scripts/prepare_sector_network.py | 10 ++++++---- scripts/solve_network.py | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 41964ff937..33a2435238 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -714,12 +714,14 @@ def add_co2_tracking( e_nom_max = np.inf n.add( - "Store", + "Generator", sequestration_buses, - e_nom_extendable=True, - e_nom_max=e_nom_max, - marginal_cost=-0.1, bus=sequestration_buses, + p_nom_extendable=True, + p_min_pu=-1.0, + p_max_pu=0, + e_sum_min=-e_nom_max, + e_sum_max=0, lifetime=options["co2_sequestration_lifetime"], carrier="co2 sequestered", ) diff --git a/scripts/solve_network.py b/scripts/solve_network.py index 31f8712331..d17d8216b8 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -554,7 +554,7 @@ def prepare_network( if limit_max_growth is not None and limit_max_growth["enable"]: add_max_growth(n, limit_max_growth) - if n.stores.carrier.eq("co2 sequestered").any(): + if n.generators.carrier.eq("co2 sequestered").any(): limit_dict = co2_sequestration_potential add_co2_sequestration_limit( n, limit_dict=limit_dict, planning_horizons=planning_horizons From caff23b2d47b4be1ff5397485f52b2f2c94db695 Mon Sep 17 00:00:00 2001 From: Michael Lindner Date: Mon, 12 Oct 2026 17:18:55 +0200 Subject: [PATCH 2/4] simplify further; avoid carry over in add_brownfield --- scripts/prepare_sector_network.py | 48 ++++++++++++++----------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 33a2435238..49c59790b3 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -660,31 +660,6 @@ 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=np.inf, - p_nom_extendable=False, - ) - - 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=np.inf, - p_nom_extendable=False, - ) - if options["regional_co2_sequestration_potential"]["enable"]: if sequestration_potential_file is None: raise ValueError( @@ -713,16 +688,35 @@ def add_co2_tracking( else: e_nom_max = np.inf + p_nom = ( + e_nom_max + if options["regional_co2_sequestration_potential"]["enable"] + else 1e7 + ) + n.add( + "Link", + sequestration_buses, + bus0=spatial.co2.dense if co2_liquefaction else spatial.co2.nodes, + bus1=sequestration_buses, + carrier="co2 sequestered", + efficiency=1.0, + p_nom=p_nom, + p_nom_extendable=False, + lifetime=np.inf, + ) + n.add( "Generator", sequestration_buses, bus=sequestration_buses, - p_nom_extendable=True, + p_nom=p_nom, + p_nom_extendable=False, p_min_pu=-1.0, p_max_pu=0, e_sum_min=-e_nom_max, e_sum_max=0, - lifetime=options["co2_sequestration_lifetime"], + marginal_cost=-options["co2_sequestration_cost"], + lifetime=np.inf, # Lifetime inf means assets from previous horizons get dropped in add_brownfield carrier="co2 sequestered", ) From 599ba0977eb4338022e01a2d52d0eab3755a7e43 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 15 Oct 2026 09:58:08 +0000 Subject: [PATCH 3/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/prepare_sector_network.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 49c59790b3..529af39098 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -689,9 +689,7 @@ def add_co2_tracking( e_nom_max = np.inf p_nom = ( - e_nom_max - if options["regional_co2_sequestration_potential"]["enable"] - else 1e7 + e_nom_max if options["regional_co2_sequestration_potential"]["enable"] else 1e7 ) n.add( "Link", @@ -716,7 +714,7 @@ def add_co2_tracking( e_sum_min=-e_nom_max, e_sum_max=0, marginal_cost=-options["co2_sequestration_cost"], - lifetime=np.inf, # Lifetime inf means assets from previous horizons get dropped in add_brownfield + lifetime=np.inf, # Lifetime inf means assets from previous horizons get dropped in add_brownfield carrier="co2 sequestered", ) From 5aaaafdb3d718784f7b4871c5dc5c3b651ed3cab Mon Sep 17 00:00:00 2001 From: Michael Lindner Date: Thu, 15 Oct 2026 12:00:05 +0200 Subject: [PATCH 4/4] some docs --- doc/design/carbon-management.md | 4 ++++ doc/release_notes.md | 2 ++ 2 files changed, 6 insertions(+) diff --git a/doc/design/carbon-management.md b/doc/design/carbon-management.md index 294cbe813a..38315c6442 100644 --- a/doc/design/carbon-management.md +++ b/doc/design/carbon-management.md @@ -86,6 +86,10 @@ years. Storage sites can be resolved regionally from a geological database of potentials, with a choice between offshore formations only, which are larger and far from settlements, and onshore formations as well. +Regional geological potentials are divided by `years_of_storage` to obtain +the modeled annual potential. A fixed-capacity, negative-dispatch Generator +enforces each region's potential and charges the sequestration cost. + !!! warning "The sequestration limit shapes the result" The limit is a deliberate choice: it keeps sequestration for hard-to-abate emissions and prevents it from becoming a backstop that diff --git a/doc/release_notes.md b/doc/release_notes.md index a14921cf77..dc46b94240 100644 --- a/doc/release_notes.md +++ b/doc/release_notes.md @@ -6,6 +6,8 @@ +* Fix: Regional CO2 sequestration allowances no longer accumulate across myopic planning horizons. Sequestration Stores are replaced by fixed-capacity Generators with energy-sum limits and infinite lifetimes, preventing brownfield carry-over from duplicating regional potentials. Sequestration costs are charged on Generator dispatch instead of the connecting Links. This can change sequestration locations and CO2 pipeline expansion ([#2369](https://github.com/PyPSA/pypsa-eur/pull/2369), fixes [#2368](https://github.com/PyPSA/pypsa-eur/issues/2368)). + * Feature: New switches `industry: solid_biomass_for_industry_cc`, `industry: gas_for_industry_cc` and `industry: process_emissions_cc` to disable the carbon capture options for solid biomass for industry, gas for industry and industrial process emissions. All default to `true`, which matches the previous behaviour ([#2357](https://github.com/PyPSA/pypsa-eur/pull/2357)). * Feature: New option `sector: electrolysis` to allow hydrogen electrolysis to be configurable ([#2358](https://github.com/PyPSA/pypsa-eur/pull/2358)).