Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keep existing conventional generation in sector network #1366

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion config/config.default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,6 @@ pypsa_eur:
- solar-hsat
- solar
- ror
- nuclear
StorageUnit:
- PHS
- hydro
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions doc/configtables/sector.csv
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions doc/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
======================================

Expand Down
1 change: 1 addition & 0 deletions rules/build_sector.smk
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
81 changes: 72 additions & 9 deletions scripts/prepare_sector_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Comment on lines +519 to 520
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fossils = ["coal", "gas", "oil", "lignite", "uranium"]
if options["fossil_fuels"] and carrier in fossils:
fossils = ["coal", "gas", "oil", "lignite"]
if (options["fossil_fuels"] and carrier in fossils) or (carrier == "uranium"):

As uranium is not a fossil fuel, this would be less misleading.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suffix = ""

Expand Down Expand Up @@ -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,
Expand All @@ -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)
Comment on lines +1142 to +1144
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency, it would be good to use this value in add_existing_baseyearas well.

if generator != "urban central solid biomass CHP":
n.add(
"Link",
new_capacity.index,
suffix=name_suffix,
bus0=bus0,
bus1=new_capacity.index,
bus2="co2 atmosphere",
carrier=generator,
marginal_cost=costs.at[generator, "efficiency"]
* 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=new_capacity / costs.at[generator, "efficiency"],
efficiency=costs.at[generator, "efficiency"],
efficiency2=costs.at[carrier[generator], "CO2 intensity"],
build_year=grouping_year,
lifetime=lifetime_assets.loc[new_capacity.index],
)

It would be even better to use the country specific values from data/nuclear_p_max_pu.csv.

@daniel-rdt

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")

Expand Down Expand Up @@ -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):
Copy link
Contributor

@daniel-rdt daniel-rdt Nov 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if options.get("keep_existing_capacities", False):
if options.get("keep_existing_capacities", False) and snakemake.params.foresight not in ["myopic", "perfect"]:

It would make sense to limit this functionality to overnight, as for myopic and perfect optimization existing caps are added later on. fyi @martacki

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()
Expand All @@ -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"]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these not all the cases, i.e. we could leave the if condition out completely now?

add_lifetime_wind_solar(n, costs)

conventional = snakemake.params.conventional_carriers
Expand All @@ -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)

Expand Down
Loading