diff --git a/config/config.default.yaml b/config/config.default.yaml index a475c6fdf..92b48f57f 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -360,7 +360,6 @@ pypsa_eur: - solar-hsat - solar - ror - - nuclear StorageUnit: - PHS - hydro @@ -693,6 +692,10 @@ sector: biogas_upgrading_cc: false conventional_generation: OCGT: gas + nuclear: uranium + coal: coal + lignite: lignite + keep_existing_capacities: false biomass_to_liquid: false biomass_to_liquid_cc: false electrobiofuels: false diff --git a/doc/configtables/sector.csv b/doc/configtables/sector.csv index eeee192eb..c3ca5dc34 100644 --- a/doc/configtables/sector.csv +++ b/doc/configtables/sector.csv @@ -167,6 +167,7 @@ biomass_spatial,--,"{true, false}",Add option for resolving biomass demand regio biomass_transport,--,"{true, false}",Add option for transporting solid biomass between nodes biogas_upgrading_cc,--,"{true, false}",Add option to capture CO2 from biomass upgrading conventional_generation,,,Add a more detailed description of conventional carriers. Any power generation requires the consumption of fuel from nodes representing that fuel. +keep_existing_capacities,--,"{true, false}",Keep existing conventional carriers from the power model. Defaults to false. biomass_to_liquid,--,"{true, false}",Add option for transforming solid biomass into liquid fuel with the same properties as oil biomass_to_liquid_cc,--,"{true, false}",Add option for transforming solid biomass into liquid fuel with the same properties as oil with carbon capture biosng,--,"{true, false}",Add option for transforming solid biomass into synthesis gas with the same properties as natural gas diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 9e615a917..20e9733a5 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -83,6 +83,8 @@ Upcoming Release * Bugfix: Bug when multiple DC links are connected to the same DC bus and the DC bus is connected to an AC bus via converter. In this case, the DC links were wrongly simplified, completely dropping the shared DC bus. Bug fixed by adding preceding converter removal. Other functionalities are not impacted. +* Enable retaining existing conventional capacities added in the power only model for sector-coupeled applications. + PyPSA-Eur 0.13.0 (13th September 2024) ====================================== diff --git a/rules/build_sector.smk b/rules/build_sector.smk index c56675c17..8ecd86aff 100755 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -1023,6 +1023,7 @@ rule prepare_sector_network: countries=config_provider("countries"), adjustments=config_provider("adjustments", "sector"), emissions_scope=config_provider("energy", "emissions"), + electricity=config_provider("electricity"), biomass=config_provider("biomass"), RDIR=RDIR, heat_pump_sources=config_provider("sector", "heat_pump_sources"), diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 105018f92..30f9463bc 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -516,7 +516,7 @@ def add_carrier_buses(n, carrier, nodes=None): capital_cost=capital_cost, ) - fossils = ["coal", "gas", "oil", "lignite"] + fossils = ["coal", "gas", "oil", "lignite", "uranium"] if options["fossil_fuels"] and carrier in fossils: suffix = "" @@ -1099,18 +1099,23 @@ def annuity_factor(v): return costs -def add_generation(n, costs): +def add_generation( + n, costs, existing_capacities=0, existing_efficiencies=None, existing_nodes=None +): logger.info("Adding electricity generation") - nodes = pop_layout.index - - conventionals = options["conventional_generation"] + conventionals = options.get("conventional_generation", {}) for generator, carrier in conventionals.items(): carrier_nodes = vars(spatial)[carrier].nodes add_carrier_buses(n, carrier, carrier_nodes) + if existing_nodes is None: + nodes = pop_layout.index + else: + nodes = existing_nodes[generator] + n.add( "Link", nodes + " " + generator, @@ -1121,14 +1126,61 @@ def add_generation(n, costs): * costs.at[generator, "VOM"], # NB: VOM is per MWel capital_cost=costs.at[generator, "efficiency"] * costs.at[generator, "fixed"], # NB: fixed cost is per MWel - p_nom_extendable=True, + p_nom_extendable=( + True + if generator + in snakemake.params.electricity.get("extendable_carriers", dict()).get( + "Generator", list() + ) + else False + ), + p_nom=( + existing_capacities[generator] / existing_efficiencies[generator] + if not existing_capacities == 0 + else 0 + ), # NB: existing capacities are MWel + p_max_pu=( + 0.7 if carrier == "uranium" else 1 + ), # be conservative for nuclear (maintenance or unplanned shut downs) carrier=generator, - efficiency=costs.at[generator, "efficiency"], + efficiency=( + existing_efficiencies[generator] + if existing_efficiencies is not None + else costs.at[generator, "efficiency"] + ), efficiency2=costs.at[carrier, "CO2 intensity"], lifetime=costs.at[generator, "lifetime"], ) +def get_capacities_from_elec(n, carriers, component): + """ + Gets capacities and efficiencies for {carrier} in n.{component} that were + previously assigned in add_electricity. + """ + component_list = ["generators", "storage_units", "links", "stores"] + component_dict = {name: getattr(n, name) for name in component_list} + e_nom_carriers = ["stores"] + nom_col = {x: "e_nom" if x in e_nom_carriers else "p_nom" for x in component_list} + eff_col = "efficiency" + + capacity_dict = {} + efficiency_dict = {} + node_dict = {} + for carrier in carriers: + capacity_dict[carrier] = component_dict[component].query("carrier in @carrier")[ + nom_col[component] + ] + efficiency_dict[carrier] = component_dict[component].query( + "carrier in @carrier" + )[eff_col] + node_dict[carrier] = component_dict[component].query("carrier in @carrier")[ + "bus" + ] + + return capacity_dict, efficiency_dict, node_dict + + def add_ammonia(n, costs): logger.info("Adding ammonia carrier with synthesis, cracking and storage") @@ -4538,6 +4590,17 @@ def add_enhanced_geothermal(n, egs_potentials, egs_overlap, costs): ) pop_weighted_energy_totals.update(pop_weighted_heat_totals) + if options.get("keep_existing_capacities", False): + existing_capacities, existing_efficiencies, existing_nodes = ( + get_capacities_from_elec( + n, + carriers=options.get("conventional_generation").keys(), + component="generators", + ) + ) + else: + existing_capacities, existing_efficiencies, existing_nodes = 0, None, None + landfall_lengths = { tech: settings["landfall_length"] for tech, settings in snakemake.params.renewable.items() @@ -4551,7 +4614,7 @@ def add_enhanced_geothermal(n, egs_potentials, egs_overlap, costs): spatial = define_spatial(pop_layout.index, options) - if snakemake.params.foresight in ["myopic", "perfect"]: + if snakemake.params.foresight in ["overnight", "myopic", "perfect"]: add_lifetime_wind_solar(n, costs) conventional = snakemake.params.conventional_carriers @@ -4562,7 +4625,7 @@ def add_enhanced_geothermal(n, egs_potentials, egs_overlap, costs): add_co2_tracking(n, costs, options) - add_generation(n, costs) + add_generation(n, costs, existing_capacities, existing_efficiencies, existing_nodes) add_storage_and_grids(n, costs)