From 8ec42ac6ac9bf56873a048fe5bbdc06c01e0814c Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 16 Oct 2026 11:05:33 +0200 Subject: [PATCH 1/2] Remove pypsa_eur config section In sector-coupled models, add_electricity now skips power plants whose fuels the sector model tracks and hydrogen storage, instead of adding all components and removing them later in prepare_sector_network. --- config/config.default.yaml | 20 ----- config/schema.default.json | 88 +------------------ doc/configuration.md | 17 ---- doc/release_notes.md | 2 + rules/compose.smk | 1 - scripts/add_electricity.py | 21 ++++- scripts/lib/validation/config/_schema.py | 16 ++-- scripts/lib/validation/config/electricity.py | 2 +- .../validation/config/existing_capacities.py | 2 +- scripts/lib/validation/config/pypsa_eur.py | 49 ----------- scripts/prepare_sector_network.py | 73 +-------------- test/test_config_schema.py | 5 ++ 12 files changed, 45 insertions(+), 251 deletions(-) delete mode 100644 scripts/lib/validation/config/pypsa_eur.py diff --git a/config/config.default.yaml b/config/config.default.yaml index 491500aa1d..eb41935f16 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -535,26 +535,6 @@ load: gdp: 0.6 population: 0.4 -# docs in https://pypsa-eur.readthedocs.io/en/latest/configuration/#pypsa_eur_cf -pypsa_eur: - Bus: - - AC - Link: - - DC - Generator: - - onwind - - "offwind-ac" - - "offwind-dc" - - "offwind-float" - - "solar-hsat" - - solar - - ror - - nuclear - StorageUnit: - - PHS - - hydro - Store: [] - # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration/#energy_cf energy: energy_totals_year: 2023 diff --git a/config/schema.default.json b/config/schema.default.json index b07b291088..8601c6f55d 100644 --- a/config/schema.default.json +++ b/config/schema.default.json @@ -2419,7 +2419,7 @@ "type": "array" }, "conventional_carriers": { - "description": "List of conventional power plants to include in the model from `resources/powerplants.csv`. If an included carrier is also listed in `extendable_carriers`, the capacity is taken as a lower bound.", + "description": "List of conventional power plants to include in the model from `resources/powerplants.csv`. If an included carrier is also listed in `extendable_carriers`, the capacity is taken as a lower bound. Sector-coupled models ignore OCGT, CCGT, oil, coal, lignite, biomass and waste here, because they add these technologies as links with fuel and emission tracking.", "items": { "type": "string" }, @@ -2585,7 +2585,7 @@ "type": "number" }, "conventional_carriers": { - "description": "List of conventional power plants to include in the sectoral network.", + "description": "Fuels for which buses are added to the sector-coupled network with myopic or perfect foresight, so that existing power plants can connect to them. Does not select which existing power plants are added.", "items": { "type": "string" }, @@ -3698,46 +3698,6 @@ } } }, - "PypsaEurConfig": { - "description": "Configuration for `pypsa_eur` settings.", - "properties": { - "Bus": { - "description": "Bus carriers to keep from PyPSA-Eur.", - "items": { - "type": "string" - }, - "type": "array" - }, - "Link": { - "description": "Link carriers to keep from PyPSA-Eur.", - "items": { - "type": "string" - }, - "type": "array" - }, - "Generator": { - "description": "Generator carriers to keep from PyPSA-Eur.", - "items": { - "type": "string" - }, - "type": "array" - }, - "StorageUnit": { - "description": "StorageUnit carriers to keep from PyPSA-Eur.", - "items": { - "type": "string" - }, - "type": "array" - }, - "Store": { - "description": "Store carriers to keep from PyPSA-Eur.", - "items": { - "type": "string" - }, - "type": "array" - } - } - }, "RemoteConfig": { "description": "Configuration for top level `remote` settings.", "properties": { @@ -10431,7 +10391,7 @@ "type": "array" }, "conventional_carriers": { - "description": "List of conventional power plants to include in the model from `resources/powerplants.csv`. If an included carrier is also listed in `extendable_carriers`, the capacity is taken as a lower bound.", + "description": "List of conventional power plants to include in the model from `resources/powerplants.csv`. If an included carrier is also listed in `extendable_carriers`, the capacity is taken as a lower bound. Sector-coupled models ignore OCGT, CCGT, oil, coal, lignite, biomass and waste here, because they add these technologies as links with fuel and emission tracking.", "items": { "type": "string" }, @@ -12182,46 +12142,6 @@ } } }, - "pypsa_eur": { - "description": "Configuration for `pypsa_eur` settings.", - "properties": { - "Bus": { - "description": "Bus carriers to keep from PyPSA-Eur.", - "items": { - "type": "string" - }, - "type": "array" - }, - "Link": { - "description": "Link carriers to keep from PyPSA-Eur.", - "items": { - "type": "string" - }, - "type": "array" - }, - "Generator": { - "description": "Generator carriers to keep from PyPSA-Eur.", - "items": { - "type": "string" - }, - "type": "array" - }, - "StorageUnit": { - "description": "StorageUnit carriers to keep from PyPSA-Eur.", - "items": { - "type": "string" - }, - "type": "array" - }, - "Store": { - "description": "Store carriers to keep from PyPSA-Eur.", - "items": { - "type": "string" - }, - "type": "array" - } - } - }, "energy": { "description": "Configuration for `energy` settings.", "properties": { @@ -12376,7 +12296,7 @@ "type": "number" }, "conventional_carriers": { - "description": "List of conventional power plants to include in the sectoral network.", + "description": "Fuels for which buses are added to the sector-coupled network with myopic or perfect foresight, so that existing power plants can connect to them. Does not select which existing power plants are added.", "items": { "type": "string" }, diff --git a/doc/configuration.md b/doc/configuration.md index c9e8d3ccad..4e8438257f 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -459,23 +459,6 @@ Configuration for `load` settings. ``` -## `pypsa_eur` {#pypsa_eur_cf} - -Only used for sector-coupling studies. - -Carriers of the electricity-only network that are kept per component type when -the sector-coupling components are added. Components with other carriers, for -example conventional generators, are removed and re-added in their -sector-coupled form. - -{{ schema_table("pypsa_eur") }} - -**YAML Syntax** - -```yaml -{{ yaml_section("pypsa_eur") }} -``` - ## `energy` {#energy_cf} Only used for sector-coupling studies. diff --git a/doc/release_notes.md b/doc/release_notes.md index 6a2b9a305c..077accd997 100644 --- a/doc/release_notes.md +++ b/doc/release_notes.md @@ -6,6 +6,8 @@ +* **Breaking:** Removed the `pypsa_eur` configuration section. Sector-coupled models no longer add and later remove electricity components: power plants whose fuels the sector model tracks (OCGT, CCGT, oil, coal, lignite, biomass, waste) and hydrogen storage are skipped, all others are kept (e.g. nuclear, and now also geothermal). To drop a technology, remove it from `electricity: conventional_carriers` or `electricity: renewable_carriers` instead. + * Fix: `lines: types` must now define a line type for the 380 kV level; the config validation fails early with a clear message instead of the workflow failing later ([#2166](https://github.com/PyPSA/pypsa-eur/issues/2166)). * 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)). diff --git a/rules/compose.smk b/rules/compose.smk index b5d24b13f8..24c626c882 100644 --- a/rules/compose.smk +++ b/rules/compose.smk @@ -235,7 +235,6 @@ rule compose_network: clustering=config_provider("clustering"), clustering_temporal=config_provider("clustering", "temporal"), existing_capacities=config_provider("existing_capacities"), - pypsa_eur=config_provider("pypsa_eur"), renewable=config_provider("renewable"), conventional=config_provider("conventional"), costs=config_provider("costs"), diff --git a/scripts/add_electricity.py b/scripts/add_electricity.py index 66976f2582..eb84246e5f 100755 --- a/scripts/add_electricity.py +++ b/scripts/add_electricity.py @@ -18,7 +18,9 @@ capacity. Storage is either a `StorageUnit` whose energy-to-power ratio is fixed by `electricity.max_hours` (one investment variable) or a `Store` on an extra bus with separate charging and discharging links (three investment -variables). +variables). In sector-coupled models, conventional power plants with fuels +tracked by the sector model and hydrogen storage are skipped, because +[prepare_sector_network][] adds them itself. !!! note "Called from compose_network" This module has no Snakemake rule of its own. Its `main` function is @@ -46,6 +48,17 @@ if PYPSA_V1: pypsa.options.params.add.return_names = True +SECTOR_CARRIERS = { + "OCGT", + "CCGT", + "oil", + "coal", + "lignite", + "biomass", + "waste", + "H2", +} + STORE_LOOKUP = { "battery": { "store": "battery storage", @@ -1262,6 +1275,12 @@ def main( renewable_carriers = set(params.electricity["renewable_carriers"]) extendable_carriers = params.electricity["extendable_carriers"] conventional_carriers = params.electricity["conventional_carriers"] + if params.sector["enabled"]: + conventional_carriers = sorted(set(conventional_carriers) - SECTOR_CARRIERS) + extendable_carriers = { + c: sorted(set(carriers) - SECTOR_CARRIERS) + for c, carriers in extendable_carriers.items() + } conventional_inputs = { k: v for k, v in inputs.items() if k.startswith("conventional_") } diff --git a/scripts/lib/validation/config/_schema.py b/scripts/lib/validation/config/_schema.py index e5f8b49d6e..d73e2e8c96 100644 --- a/scripts/lib/validation/config/_schema.py +++ b/scripts/lib/validation/config/_schema.py @@ -28,7 +28,6 @@ from scripts.lib.validation.config.load import LoadConfig from scripts.lib.validation.config.overpass_api import OverpassApiConfig from scripts.lib.validation.config.plotting import PlottingConfig -from scripts.lib.validation.config.pypsa_eur import PypsaEurConfig from scripts.lib.validation.config.renewable import RenewableConfig from scripts.lib.validation.config.run import RunConfig from scripts.lib.validation.config.sector import SectorConfig @@ -180,10 +179,6 @@ def normalize_planning_horizons(cls, v): default_factory=LoadConfig, description="Electrical load configuration.", ) - pypsa_eur: PypsaEurConfig = Field( - default_factory=PypsaEurConfig, - description="PyPSA-Eur component filtering configuration.", - ) energy: EnergyConfig = Field( default_factory=EnergyConfig, description="Energy totals configuration.", @@ -248,3 +243,14 @@ def check_no_secrets_section(cls, data): "CORINE_API_TOKEN). You can set these in a .env file in the project root." ) return data + + @model_validator(mode="before") + @classmethod + def check_no_pypsa_eur_section(cls, data): + """Reject the removed `pypsa_eur` section.""" + if isinstance(data, dict) and "pypsa_eur" in data: + raise ValueError( + "The 'pypsa_eur:' section is removed. Select power plants with " + "'electricity: renewable_carriers' and 'electricity: conventional_carriers'." + ) + return data diff --git a/scripts/lib/validation/config/electricity.py b/scripts/lib/validation/config/electricity.py index 44da8df0b2..56ca8ec261 100644 --- a/scripts/lib/validation/config/electricity.py +++ b/scripts/lib/validation/config/electricity.py @@ -219,7 +219,7 @@ class ElectricityConfig(BaseModel): "waste", "biomass", ], - description="List of conventional power plants to include in the model from `resources/powerplants.csv`. If an included carrier is also listed in `extendable_carriers`, the capacity is taken as a lower bound.", + description="List of conventional power plants to include in the model from `resources/powerplants.csv`. If an included carrier is also listed in `extendable_carriers`, the capacity is taken as a lower bound. Sector-coupled models ignore OCGT, CCGT, oil, coal, lignite, biomass and waste here, because they add these technologies as links with fuel and emission tracking.", ) renewable_carriers: list[str] = Field( default_factory=lambda: [ diff --git a/scripts/lib/validation/config/existing_capacities.py b/scripts/lib/validation/config/existing_capacities.py index 843a9118a9..f673358d09 100644 --- a/scripts/lib/validation/config/existing_capacities.py +++ b/scripts/lib/validation/config/existing_capacities.py @@ -81,7 +81,7 @@ class ExistingCapacitiesConfig(ConfigModel): ) conventional_carriers: list[str] = Field( default_factory=lambda: ["lignite", "coal", "oil", "uranium"], - description="List of conventional power plants to include in the sectoral network.", + description="Fuels for which buses are added to the sector-coupled network with myopic or perfect foresight, so that existing power plants can connect to them. Does not select which existing power plants are added.", ) phase_outs: list[_PhaseOutConfig] = Field( default_factory=lambda: [ diff --git a/scripts/lib/validation/config/pypsa_eur.py b/scripts/lib/validation/config/pypsa_eur.py deleted file mode 100644 index 2ffbf0b577..0000000000 --- a/scripts/lib/validation/config/pypsa_eur.py +++ /dev/null @@ -1,49 +0,0 @@ -# SPDX-FileCopyrightText: Contributors to PyPSA-Eur -# -# SPDX-License-Identifier: MIT - -""" -PyPSA-Eur component configuration. - -Regulates what components with which carriers are kept from PyPSA-Eur. -Some technologies are removed because they are implemented differently -(e.g. battery or H2 storage) or have different year-dependent costs. -""" - -from pydantic import Field - -from scripts.lib.validation.config._base import ConfigModel - - -class PypsaEurConfig(ConfigModel): - """Configuration for `pypsa_eur` settings.""" - - Bus: list[str] = Field( - default_factory=lambda: ["AC"], - description="Bus carriers to keep from PyPSA-Eur.", - ) - Link: list[str] = Field( - default_factory=lambda: ["DC"], - description="Link carriers to keep from PyPSA-Eur.", - ) - Generator: list[str] = Field( - default_factory=lambda: [ - "onwind", - "offwind-ac", - "offwind-dc", - "offwind-float", - "solar-hsat", - "solar", - "ror", - "nuclear", - ], - description="Generator carriers to keep from PyPSA-Eur.", - ) - StorageUnit: list[str] = Field( - default_factory=lambda: ["PHS", "hydro"], - description="StorageUnit carriers to keep from PyPSA-Eur.", - ) - Store: list[str] = Field( - default_factory=list, - description="Store carriers to keep from PyPSA-Eur.", - ) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 41964ff937..a8d7222154 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -36,12 +36,7 @@ get, get_temporal_resolution, ) -from scripts.add_electricity import ( - attach_storageunits, - attach_stores, - calculate_annuity, - flatten, -) +from scripts.add_electricity import calculate_annuity, flatten from scripts.build_co2_totals import build_co2_totals, build_eea_co2, build_eurostat_co2 from scripts.build_transport_demand import transport_degree_factor from scripts.definitions.heat_sector import HeatSector @@ -467,49 +462,6 @@ def add_carrier_buses( ) -# TODO: PyPSA-Eur merge issue -def remove_elec_base_techs(n: pypsa.Network, carriers_to_keep: dict) -> None: - """ - Remove conventional generators (e.g. OCGT) and storage units (e.g. - batteries and H2) from base electricity-only network, since they're added - here differently using links. - - Parameters - ---------- - n : pypsa.Network - Network to remove components from - carriers_to_keep : dict - Dictionary specifying which carriers to keep for each component type - e.g. {'Generator': ['hydro'], 'StorageUnit': ['PHS']} - """ - for c in n.components[list(carriers_to_keep.keys())]: - if c.static.empty: - continue - to_keep = carriers_to_keep[c.name] - to_remove = pd.Index(c.static.carrier.unique()).symmetric_difference(to_keep) - if to_remove.empty: - continue - logger.info(f"Removing {c.list_name} with carrier {list(to_remove)}") - names = c.static.index[c.static.carrier.isin(to_remove)] - n.remove(c.name, names) - n.carriers.drop(to_remove, inplace=True, errors="ignore") - - -# TODO: PyPSA-Eur merge issue -def remove_non_electric_buses(n): - """ - Remove buses from pypsa-eur with carriers which are not AC buses. - """ - if to_drop := list(n.buses.query("carrier not in ['AC', 'DC']").carrier.unique()): - logger.info(f"Drop buses from PyPSA-Eur with carrier: {to_drop}") - n.buses = n.buses[n.buses.carrier.isin(["AC", "DC"])] - - -def patch_electricity_network(n, carriers_to_keep): - remove_elec_base_techs(n, carriers_to_keep) - remove_non_electric_buses(n) - - def add_eu_bus(n, x=-5.5, y=46): """ Add EU bus to the network. @@ -6157,9 +6109,6 @@ def main( fn = inputs.gas_input_nodes_simplified gas_input_nodes = pd.read_csv(fn, index_col=0) if fn else None - carriers_to_keep = params.pypsa_eur - patch_electricity_network(n, carriers_to_keep) - fn = inputs.heating_efficiencies year = int(params["energy_totals_year"]) heating_efficiencies = pd.read_csv(fn, index_col=[1, 0]).loc[year] @@ -6218,26 +6167,6 @@ def main( options=options, ) - # Hydrogen already implemented in add_h2_gas_infrastructure - extendable_carriers = params.electricity["extendable_carriers"] - extendable_storageunits = set(extendable_carriers["StorageUnit"]) - {"H2"} - extendable_stores = set(extendable_carriers["Store"]) - {"H2"} - - attach_storageunits( - n=n, - costs=costs, - buses_i=pop_layout.index, - extendable_carriers=sorted(extendable_storageunits), - max_hours=params.electricity["max_hours"], - ) - - attach_stores( - n=n, - costs=costs, - buses_i=pop_layout.index, - extendable_carriers=sorted(extendable_stores), - ) - if options["transport"]: add_land_transport( n=n, diff --git a/test/test_config_schema.py b/test/test_config_schema.py index 9d9d454a39..c0da0b18a8 100644 --- a/test/test_config_schema.py +++ b/test/test_config_schema.py @@ -244,3 +244,8 @@ def test_missing_380kv_is_rejected(self): validate_config( {"lines": {"types": {220.0: "Al/St 240/40 2-bundle 220.0"}}} ) + + +def test_removed_pypsa_eur_section_is_rejected(): + with pytest.raises(ValueError, match="pypsa_eur"): + validate_config({"pypsa_eur": {"Generator": ["onwind"]}}) From c69942b89f47dd1a1e6a83bb10c7c2f729a0c701 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 16 Oct 2026 11:05:55 +0200 Subject: [PATCH 2/2] Add PR link to release note --- doc/release_notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release_notes.md b/doc/release_notes.md index 077accd997..1283c62c35 100644 --- a/doc/release_notes.md +++ b/doc/release_notes.md @@ -6,7 +6,7 @@ -* **Breaking:** Removed the `pypsa_eur` configuration section. Sector-coupled models no longer add and later remove electricity components: power plants whose fuels the sector model tracks (OCGT, CCGT, oil, coal, lignite, biomass, waste) and hydrogen storage are skipped, all others are kept (e.g. nuclear, and now also geothermal). To drop a technology, remove it from `electricity: conventional_carriers` or `electricity: renewable_carriers` instead. +* **Breaking:** Removed the `pypsa_eur` configuration section. Sector-coupled models no longer add and later remove electricity components: power plants whose fuels the sector model tracks (OCGT, CCGT, oil, coal, lignite, biomass, waste) and hydrogen storage are skipped, all others are kept (e.g. nuclear, and now also geothermal). To drop a technology, remove it from `electricity: conventional_carriers` or `electricity: renewable_carriers` instead ([#2370](https://github.com/PyPSA/pypsa-eur/pull/2370)). * Fix: `lines: types` must now define a line type for the 380 kV level; the config validation fails early with a clear message instead of the workflow failing later ([#2166](https://github.com/PyPSA/pypsa-eur/issues/2166)).