diff --git a/config/config.default.yaml b/config/config.default.yaml index 9e8e27d8d8..3c54487dde 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -425,6 +425,7 @@ renewable: # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#conventional conventional: + estimate_efficiencies: false unit_commitment: false dynamic_fuel_price: false fuel_price_rolling_window: 6 diff --git a/config/schema.default.json b/config/schema.default.json index 078a459c2b..c910b050e1 100644 --- a/config/schema.default.json +++ b/config/schema.default.json @@ -649,6 +649,11 @@ "additionalProperties": true, "description": "Configuration for `conventional` settings.", "properties": { + "estimate_efficiencies": { + "default": false, + "description": "Estimate missing plant-level efficiencies from a carrier- and age-dependent linear heuristic.", + "type": "boolean" + }, "unit_commitment": { "default": false, "description": "Allow the overwrite of ramp_limit_up, ramp_limit_start_up, ramp_limit_shut_down, p_min_pu, min_up_time, min_down_time, and start_up_cost of conventional generators. Refer to the CSV file 'unit_commitment.csv'.", @@ -9768,6 +9773,11 @@ "additionalProperties": true, "description": "Configuration for `conventional` settings.", "properties": { + "estimate_efficiencies": { + "default": false, + "description": "Estimate missing plant-level efficiencies from a carrier- and age-dependent linear heuristic.", + "type": "boolean" + }, "unit_commitment": { "default": false, "description": "Allow the overwrite of ramp_limit_up, ramp_limit_start_up, ramp_limit_shut_down, p_min_pu, min_up_time, min_down_time, and start_up_cost of conventional generators. Refer to the CSV file 'unit_commitment.csv'.", diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 954ba14d80..d89cb6fdb1 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -11,6 +11,8 @@ Upcoming Release * Add configuration schema updater that allows changes to be made in soft-forks without touching base PyPSA-Eur files (#2014). +* New ``conventional: estimate_efficiencies`` option (default ``false``) to fill missing plant-level efficiencies using a carrier- and age-dependent heuristic. + * Adjust ``powerplants_filter`` to include power plants operational in 2025. * Rewrite mapping of power plant sites to model regions / buses. Previously, power plants were mapped to the nearest bus in the same country. diff --git a/rules/build_electricity.smk b/rules/build_electricity.smk index c33e7fdb73..ac644a5c76 100755 --- a/rules/build_electricity.smk +++ b/rules/build_electricity.smk @@ -801,6 +801,7 @@ rule add_electricity: ), aggregation_strategies=config_provider("clustering", "aggregation_strategies"), exclude_carriers=config_provider("clustering", "exclude_carriers"), + estimate_efficiencies=config_provider("conventional", "estimate_efficiencies"), input: unpack(input_profile_tech), unpack(input_class_regions), diff --git a/scripts/add_electricity.py b/scripts/add_electricity.py index 0df64d148f..2c1c8b37a5 100755 --- a/scripts/add_electricity.py +++ b/scripts/add_electricity.py @@ -254,6 +254,60 @@ def sanitize_locations(n): ) +def estimate_efficiency(df: pd.DataFrame, reference_year: int = 2025) -> pd.Series: + carrier = df.carrier + year = df.dateretrofit.combine_first(df.datein) + + offset = carrier.map( + { + "lignite": 0.25, + "coal": 0.28, + "CCGT": 0.40, + "OCGT": 0.28, + "oil": 0.28, + "nuclear": 0.33, + } + ) + slope = carrier.map( + { + "lignite": 0.003, + "coal": 0.003, + "CCGT": 0.004, + "OCGT": 0.003, + "oil": 0.002, + "nuclear": 0.0, + } + ) + year0 = carrier.map( + { + "lignite": 1960, + "coal": 1960, + "CCGT": 1980, + "OCGT": 1970, + "oil": 1960, + "nuclear": 1960, + } + ) + cap = carrier.map( + { + "lignite": 0.42, + "coal": 0.44, + "CCGT": 0.60, + "OCGT": 0.41, + "oil": 0.38, + "nuclear": 0.33, + } + ) + + # heuristic linear efficiency estimation + eta = (offset + slope * (year - year0)).clip(lower=offset, upper=cap) + + # degradation of efficiency + eta *= 1 - (reference_year - year - 10).clip(lower=0) * 0.001 + + return eta + + def add_co2_emissions(n, costs, carriers): """ Add CO2 emissions to the network's carriers attribute. @@ -270,6 +324,7 @@ def load_and_aggregate_powerplants( consider_efficiency_classes: bool = False, aggregation_strategies: dict = None, exclude_carriers: list = None, + estimate_efficiencies: bool = False, ) -> pd.DataFrame: if not aggregation_strategies: aggregation_strategies = {} @@ -313,7 +368,10 @@ def load_and_aggregate_powerplants( ] ppl = ppl.join(costs[cost_columns], on="carrier", rsuffix="_r") - ppl["efficiency"] = ppl.efficiency.combine_first(ppl.efficiency_r) + efficiency = ppl.efficiency + if estimate_efficiencies: + efficiency = efficiency.combine_first(estimate_efficiency(ppl)) + ppl["efficiency"] = efficiency.combine_first(ppl.efficiency_r) ppl["lifetime"] = (ppl.dateout - ppl.datein).fillna(np.inf) ppl["build_year"] = ppl.datein.fillna(0).astype(int) ppl["marginal_cost"] = ( @@ -1154,6 +1212,7 @@ def attach_stores( params.consider_efficiency_classes, params.aggregation_strategies, params.exclude_carriers, + params.estimate_efficiencies, ) attach_load( diff --git a/scripts/lib/validation/config/conventional.py b/scripts/lib/validation/config/conventional.py index 7094aa8a94..321c3e7bb5 100644 --- a/scripts/lib/validation/config/conventional.py +++ b/scripts/lib/validation/config/conventional.py @@ -18,6 +18,10 @@ class ConventionalConfig(ConfigModel): model_config = ConfigDict(extra="allow") + estimate_efficiencies: bool = Field( + False, + description="Estimate missing plant-level efficiencies from a carrier- and age-dependent linear heuristic.", + ) unit_commitment: bool = Field( False, description="Allow the overwrite of ramp_limit_up, ramp_limit_start_up, ramp_limit_shut_down, p_min_pu, min_up_time, min_down_time, and start_up_cost of conventional generators. Refer to the CSV file 'unit_commitment.csv'.",