Merge pull request #1066 from PyPSA/adding_solar_tracking_single_axis

Adding solar tracking single axis
This commit is contained in:
lisazeyen 2024-05-15 16:27:39 +02:00 committed by GitHub
commit be28996a68
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 152 additions and 15 deletions

View File

@ -107,7 +107,7 @@ electricity:
H2: 168 H2: 168
extendable_carriers: extendable_carriers:
Generator: [solar, onwind, offwind-ac, offwind-dc, offwind-float, OCGT] Generator: [solar, solar-hsat, onwind, offwind-ac, offwind-dc, offwind-float, OCGT]
StorageUnit: [] # battery, H2 StorageUnit: [] # battery, H2
Store: [battery, H2] Store: [battery, H2]
Link: [] # H2 pipeline Link: [] # H2 pipeline
@ -117,7 +117,7 @@ electricity:
everywhere_powerplants: [nuclear, oil, OCGT, CCGT, coal, lignite, geothermal, biomass] everywhere_powerplants: [nuclear, oil, OCGT, CCGT, coal, lignite, geothermal, biomass]
conventional_carriers: [nuclear, oil, OCGT, CCGT, coal, lignite, geothermal, biomass] conventional_carriers: [nuclear, oil, OCGT, CCGT, coal, lignite, geothermal, biomass]
renewable_carriers: [solar, onwind, offwind-ac, offwind-dc, offwind-float, hydro] renewable_carriers: [solar, solar-hsat, onwind, offwind-ac, offwind-dc, hydro]
estimate_renewable_capacities: estimate_renewable_capacities:
enable: true enable: true
@ -246,6 +246,21 @@ renewable:
natura: true natura: true
excluder_resolution: 100 excluder_resolution: 100
clip_p_max_pu: 1.e-2 clip_p_max_pu: 1.e-2
solar-hsat:
cutout: europe-2013-sarah
resource:
method: pv
panel: CSi
orientation:
slope: 35.
azimuth: 180.
tracking: horizontal
capacity_per_sqkm: 4.43 # 15% higher land usage acc. to NREL
corine: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 26, 31, 32]
luisa: false # [1111, 1121, 1122, 1123, 1130, 1210, 1221, 1222, 1230, 1241, 1242, 1310, 1320, 1330, 1410, 1421, 1422, 2110, 2120, 2130, 2210, 2220, 2230, 2310, 2410, 2420, 3210, 3320, 3330]
natura: true
excluder_resolution: 100
clip_p_max_pu: 1.e-2
hydro: hydro:
cutout: europe-2013-era5 cutout: europe-2013-era5
carriers: [ror, PHS, hydro] carriers: [ror, PHS, hydro]
@ -324,7 +339,6 @@ pypsa_eur:
- onwind - onwind
- offwind-ac - offwind-ac
- offwind-dc - offwind-dc
- offwind-float
- solar - solar
- ror - ror
- nuclear - nuclear
@ -588,6 +602,7 @@ sector:
biogas_upgrading_cc: false biogas_upgrading_cc: false
conventional_generation: conventional_generation:
OCGT: gas OCGT: gas
solar_utility_horizontal_axis_tracking: true
biomass_to_liquid: false biomass_to_liquid: false
biosng: false biosng: false
limit_max_growth: limit_max_growth:
@ -924,6 +939,7 @@ plotting:
# solar # solar
solar: "#f9d002" solar: "#f9d002"
solar PV: "#f9d002" solar PV: "#f9d002"
solar-hsat: "#fdb915"
solar thermal: '#ffbf2b' solar thermal: '#ffbf2b'
residential rural solar thermal: '#f1c069' residential rural solar thermal: '#f1c069'
services rural solar thermal: '#eabf61' services rural solar thermal: '#eabf61'
@ -1168,6 +1184,4 @@ plotting:
DC-DC: "#8a1caf" DC-DC: "#8a1caf"
DC link: "#8a1caf" DC link: "#8a1caf"
load: "#dd2e23" load: "#dd2e23"
waste CHP: '#e3d37d'
waste CHP CC: '#e3d3ff'
HVC to air: 'k' HVC to air: 'k'

View File

@ -34,7 +34,7 @@ electricity:
Store: [H2] Store: [H2]
Link: [H2 pipeline] Link: [H2 pipeline]
renewable_carriers: [solar, onwind, offwind-ac, offwind-dc, offwind-float] renewable_carriers: [solar, solar-hsat, onwind, offwind-ac, offwind-dc, offwind-float]
atlite: atlite:
@ -61,6 +61,8 @@ renewable:
min_depth: false min_depth: false
solar: solar:
cutout: be-03-2013-era5 cutout: be-03-2013-era5
solar-hsat:
cutout: be-03-2013-era5
clustering: clustering:

View File

@ -42,7 +42,7 @@ electricity:
Store: [H2] Store: [H2]
Link: [H2 pipeline] Link: [H2 pipeline]
renewable_carriers: [solar, onwind, offwind-ac, offwind-dc, offwind-float] renewable_carriers: [solar, solar-hsat, onwind, offwind-ac, offwind-dc, offwind-float]
atlite: atlite:
default_cutout: be-03-2013-era5 default_cutout: be-03-2013-era5
@ -68,6 +68,8 @@ renewable:
min_depth: false min_depth: false
solar: solar:
cutout: be-03-2013-era5 cutout: be-03-2013-era5
solar-hsat:
cutout: be-03-2013-era5
clustering: clustering:
temporal: temporal:

View File

@ -36,7 +36,7 @@ electricity:
Store: [H2] Store: [H2]
Link: [H2 pipeline] Link: [H2 pipeline]
renewable_carriers: [solar, onwind, offwind-ac, offwind-dc, offwind-float] renewable_carriers: [solar, solar-hsat, onwind, offwind-ac, offwind-dc, offwind-float]
atlite: atlite:
default_cutout: be-03-2013-era5 default_cutout: be-03-2013-era5
@ -62,6 +62,8 @@ renewable:
min_depth: false min_depth: false
solar: solar:
cutout: be-03-2013-era5 cutout: be-03-2013-era5
solar-hsat:
cutout: be-03-2013-era5
clustering: clustering:
temporal: temporal:

View File

@ -39,7 +39,7 @@ electricity:
Store: [H2] Store: [H2]
Link: [H2 pipeline] Link: [H2 pipeline]
renewable_carriers: [solar, onwind, offwind-ac, offwind-dc, offwind-float] renewable_carriers: [solar, solar-hsat, onwind, offwind-ac, offwind-dc, offwind-float]
sector: sector:
min_part_load_fischer_tropsch: 0 min_part_load_fischer_tropsch: 0
@ -69,6 +69,8 @@ renewable:
min_depth: false min_depth: false
solar: solar:
cutout: be-03-2013-era5 cutout: be-03-2013-era5
solar-hsat:
cutout: be-03-2013-era5
clustering: clustering:
temporal: temporal:

View File

@ -9,6 +9,9 @@ Release Notes
Upcoming Release Upcoming Release
================ ================
* New technology, solar PV with single-axis horizontal tracking (on a N-S axis),
with a carrier called ``solar-hsat`` to the networks. The default option for adding
this technology is set to ``true`` in the ``config.yaml``.
* The technology-data version was updated to v0.9.0. * The technology-data version was updated to v0.9.0.

View File

@ -193,7 +193,7 @@ rule determine_availability_matrix_MD_UA:
offshore_shapes=resources("offshore_shapes.geojson"), offshore_shapes=resources("offshore_shapes.geojson"),
regions=lambda w: ( regions=lambda w: (
resources("regions_onshore.geojson") resources("regions_onshore.geojson")
if w.technology in ("onwind", "solar") if w.technology in ("onwind", "solar", "solar-hsat")
else resources("regions_offshore.geojson") else resources("regions_offshore.geojson")
), ),
cutout=lambda w: "cutouts/" cutout=lambda w: "cutouts/"
@ -264,7 +264,7 @@ rule build_renewable_profiles:
offshore_shapes=resources("offshore_shapes.geojson"), offshore_shapes=resources("offshore_shapes.geojson"),
regions=lambda w: ( regions=lambda w: (
resources("regions_onshore.geojson") resources("regions_onshore.geojson")
if w.technology in ("onwind", "solar") if w.technology in ("onwind", "solar", "solar-hsat")
else resources("regions_offshore.geojson") else resources("regions_offshore.geojson")
), ),
cutout=lambda w: "cutouts/" cutout=lambda w: "cutouts/"

View File

@ -195,8 +195,12 @@ def adjust_renewable_profiles(n, input_profiles, params, year):
) )
for carrier in params["carriers"]: for carrier in params["carriers"]:
if carrier == "hydro": if carrier == "hydro" or (
carrier == "solar-hsat"
and not snakemake.config["sector"]["solar_utility_horizontal_axis_tracking"]
):
continue continue
with xr.open_dataset(getattr(input_profiles, "profile_" + carrier)) as ds: with xr.open_dataset(getattr(input_profiles, "profile_" + carrier)) as ds:
if ds.indexes["bus"].empty or "year" not in ds.indexes: if ds.indexes["bus"].empty or "year" not in ds.indexes:
continue continue

View File

@ -235,6 +235,8 @@ def load_costs(tech_costs, config, max_hours, Nyears=1.0):
+ (1 - config["rooftop_share"]) * costs.at["solar-utility", "capital_cost"] + (1 - config["rooftop_share"]) * costs.at["solar-utility", "capital_cost"]
) )
costs = costs.rename({"solar-utility single-axis tracking": "solar-hsat"})
def costs_for_storage(store, link1, link2=None, max_hours=1.0): def costs_for_storage(store, link1, link2=None, max_hours=1.0):
capital_cost = link1["capital_cost"] + max_hours * store["capital_cost"] capital_cost = link1["capital_cost"] + max_hours * store["capital_cost"]
if link2 is not None: if link2 is not None:

View File

@ -1113,7 +1113,7 @@ def insert_gas_distribution_costs(n, costs):
def add_electricity_grid_connection(n, costs): def add_electricity_grid_connection(n, costs):
carriers = ["onwind", "solar"] carriers = ["onwind", "solar", "solar-hsat"]
gens = n.generators.index[n.generators.carrier.isin(carriers)] gens = n.generators.index[n.generators.carrier.isin(carriers)]
@ -3412,6 +3412,13 @@ def remove_h2_network(n):
n.stores.drop("EU H2 Store", inplace=True) n.stores.drop("EU H2 Store", inplace=True)
def remove_solar_tracking(n):
for tech in ["solar-hsat"]:
logger.info("removing " + tech)
n.mremove("Generator", n.generators.index[n.generators.carrier == tech])
def limit_individual_line_extension(n, maxext): def limit_individual_line_extension(n, maxext):
logger.info(f"Limiting new HVAC and HVDC extensions to {maxext} MW") logger.info(f"Limiting new HVAC and HVDC extensions to {maxext} MW")
n.lines["s_nom_max"] = n.lines["s_nom"] + maxext n.lines["s_nom_max"] = n.lines["s_nom"] + maxext
@ -3789,6 +3796,9 @@ if __name__ == "__main__":
if options["electricity_distribution_grid"]: if options["electricity_distribution_grid"]:
insert_electricity_distribution_grid(n, costs) insert_electricity_distribution_grid(n, costs)
if not options["solar_utility_horizontal_axis_tracking"]:
remove_solar_tracking(n)
maybe_adjust_costs_and_potentials(n, snakemake.params["adjustments"]) maybe_adjust_costs_and_potentials(n, snakemake.params["adjustments"])
if options["gas_distribution_grid"]: if options["gas_distribution_grid"]:

View File

@ -31,6 +31,7 @@ import logging
import os import os
import re import re
import sys import sys
from functools import reduce
import numpy as np import numpy as np
import pandas as pd import pandas as pd
@ -123,7 +124,15 @@ def add_land_use_constraint_perfect(n):
def _add_land_use_constraint(n): def _add_land_use_constraint(n):
# warning: this will miss existing offwind which is not classed AC-DC and has carrier 'offwind' # warning: this will miss existing offwind which is not classed AC-DC and has carrier 'offwind'
for carrier in ["solar", "onwind", "offwind-ac", "offwind-dc", "offwind-float"]: for carrier in [
"solar",
"solar rooftop",
"solar-hsat",
"onwind",
"offwind-ac",
"offwind-dc",
"offwind-float",
]:
extendable_i = (n.generators.carrier == carrier) & n.generators.p_nom_extendable extendable_i = (n.generators.carrier == carrier) & n.generators.p_nom_extendable
n.generators.loc[extendable_i, "p_nom_min"] = 0 n.generators.loc[extendable_i, "p_nom_min"] = 0
@ -158,7 +167,14 @@ def _add_land_use_constraint_m(n, planning_horizons, config):
grouping_years = config["existing_capacities"]["grouping_years_power"] grouping_years = config["existing_capacities"]["grouping_years_power"]
current_horizon = snakemake.wildcards.planning_horizons current_horizon = snakemake.wildcards.planning_horizons
for carrier in ["solar", "onwind", "offwind-ac", "offwind-dc", "offwind-float"]: for carrier in [
"solar",
"solar rooftop",
"solar-hsat",
"onwind",
"offwind-ac",
"offwind-dc",
]:
extendable_i = (n.generators.carrier == carrier) & n.generators.p_nom_extendable extendable_i = (n.generators.carrier == carrier) & n.generators.p_nom_extendable
n.generators.loc[extendable_i, "p_nom_min"] = 0 n.generators.loc[extendable_i, "p_nom_min"] = 0
@ -199,6 +215,83 @@ def _add_land_use_constraint_m(n, planning_horizons, config):
n.generators.p_nom_max.clip(lower=0, inplace=True) n.generators.p_nom_max.clip(lower=0, inplace=True)
def add_solar_potential_constraints(n, config):
"""
Add constraint to make sure the sum capacity of all solar technologies (fixed, tracking, ets. ) is below the region potential.
Example:
ES1 0: total solar potential is 10 GW, meaning:
solar potential : 10 GW
solar-hsat potential : 8 GW (solar with single axis tracking is assumed to have higher land use)
The constraint ensures that:
solar_p_nom + solar_hsat_p_nom * 1.13 <= 10 GW
"""
land_use_factors = {
"solar-hsat": config["renewable"]["solar"]["capacity_per_sqkm"]
/ config["renewable"]["solar-hsat"]["capacity_per_sqkm"],
}
rename = {"Generator-ext": "Generator"}
solar_carriers = ["solar", "solar-hsat"]
solar = n.generators[
n.generators.carrier.isin(solar_carriers) & n.generators.p_nom_extendable
].index
solar_today = n.generators[
(n.generators.carrier == "solar") & (n.generators.p_nom_extendable)
].index
solar_hsat = n.generators[(n.generators.carrier == "solar-hsat")].index
if solar.empty:
return
land_use = pd.DataFrame(1, index=solar, columns=["land_use_factor"])
for carrier, factor in land_use_factors.items():
land_use = land_use.apply(
lambda x: (x * factor) if carrier in x.name else x, axis=1
)
if "m" in snakemake.wildcards.clusters:
location = pd.Series(
[" ".join(i.split(" ")[:2]) for i in n.generators.index],
index=n.generators.index,
)
ggrouper = pd.Series(
n.generators.loc[solar].index.rename("bus").map(location),
index=n.generators.loc[solar].index,
).to_xarray()
rhs = (
n.generators.loc[solar_today, "p_nom_max"]
.groupby(n.generators.loc[solar_today].index.rename("bus").map(location))
.sum()
- n.generators.loc[solar_hsat, "p_nom_opt"]
.groupby(n.generators.loc[solar_hsat].index.rename("bus").map(location))
.sum()
* land_use_factors["solar-hsat"]
).clip(lower=0)
else:
location = pd.Series(n.buses.index, index=n.buses.index)
ggrouper = n.generators.loc[solar].bus
rhs = (
n.generators.loc[solar_today, "p_nom_max"]
.groupby(n.generators.loc[solar_today].bus.map(location))
.sum()
- n.generators.loc[solar_hsat, "p_nom_opt"]
.groupby(n.generators.loc[solar_hsat].bus.map(location))
.sum()
* land_use_factors["solar-hsat"]
).clip(lower=0)
lhs = (
(n.model["Generator-p_nom"].rename(rename).loc[solar] * land_use.squeeze())
.groupby(ggrouper)
.sum()
)
logger.info("Adding solar potential constraint.")
n.model.add_constraints(lhs <= rhs, name="solar_potential")
def add_co2_sequestration_limit(n, limit=200): def add_co2_sequestration_limit(n, limit=200):
""" """
Add a global constraint on the amount of Mt CO2 that can be sequestered. Add a global constraint on the amount of Mt CO2 that can be sequestered.
@ -862,6 +955,9 @@ def extra_functionality(n, snapshots):
if EQ_o := constraints["EQ"]: if EQ_o := constraints["EQ"]:
add_EQ_constraints(n, EQ_o.replace("EQ", "")) add_EQ_constraints(n, EQ_o.replace("EQ", ""))
if config["sector"]["solar_utility_horizontal_axis_tracking"]:
add_solar_potential_constraints(n, config)
add_battery_constraints(n) add_battery_constraints(n)
add_lossy_bidirectional_link_constraints(n) add_lossy_bidirectional_link_constraints(n)
add_pipe_retrofit_constraint(n) add_pipe_retrofit_constraint(n)