diff --git a/config/config.default.yaml b/config/config.default.yaml index b26e73ce..18a095df 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -107,7 +107,7 @@ electricity: H2: 168 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 Store: [battery, H2] Link: [] # H2 pipeline @@ -117,7 +117,7 @@ electricity: everywhere_powerplants: [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: enable: true @@ -246,6 +246,21 @@ renewable: natura: true excluder_resolution: 100 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: cutout: europe-2013-era5 carriers: [ror, PHS, hydro] @@ -324,7 +339,6 @@ pypsa_eur: - onwind - offwind-ac - offwind-dc - - offwind-float - solar - ror - nuclear @@ -588,6 +602,7 @@ sector: biogas_upgrading_cc: false conventional_generation: OCGT: gas + solar_utility_horizontal_axis_tracking: true biomass_to_liquid: false biosng: false limit_max_growth: @@ -924,6 +939,7 @@ plotting: # solar solar: "#f9d002" solar PV: "#f9d002" + solar-hsat: "#fdb915" solar thermal: '#ffbf2b' residential rural solar thermal: '#f1c069' services rural solar thermal: '#eabf61' @@ -1168,6 +1184,4 @@ plotting: DC-DC: "#8a1caf" DC link: "#8a1caf" load: "#dd2e23" - waste CHP: '#e3d37d' - waste CHP CC: '#e3d3ff' HVC to air: 'k' diff --git a/config/test/config.electricity.yaml b/config/test/config.electricity.yaml index 0c44be82..38fa31ab 100644 --- a/config/test/config.electricity.yaml +++ b/config/test/config.electricity.yaml @@ -34,7 +34,7 @@ electricity: Store: [H2] 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: @@ -61,6 +61,8 @@ renewable: min_depth: false solar: cutout: be-03-2013-era5 + solar-hsat: + cutout: be-03-2013-era5 clustering: diff --git a/config/test/config.myopic.yaml b/config/test/config.myopic.yaml index e5a643c8..3a3a7856 100644 --- a/config/test/config.myopic.yaml +++ b/config/test/config.myopic.yaml @@ -42,7 +42,7 @@ electricity: Store: [H2] 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: default_cutout: be-03-2013-era5 @@ -68,6 +68,8 @@ renewable: min_depth: false solar: cutout: be-03-2013-era5 + solar-hsat: + cutout: be-03-2013-era5 clustering: temporal: diff --git a/config/test/config.overnight.yaml b/config/test/config.overnight.yaml index 604e00d8..92379ae2 100644 --- a/config/test/config.overnight.yaml +++ b/config/test/config.overnight.yaml @@ -36,7 +36,7 @@ electricity: Store: [H2] 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: default_cutout: be-03-2013-era5 @@ -62,6 +62,8 @@ renewable: min_depth: false solar: cutout: be-03-2013-era5 + solar-hsat: + cutout: be-03-2013-era5 clustering: temporal: diff --git a/config/test/config.perfect.yaml b/config/test/config.perfect.yaml index e0fb25a5..781b3fd4 100644 --- a/config/test/config.perfect.yaml +++ b/config/test/config.perfect.yaml @@ -39,7 +39,7 @@ electricity: Store: [H2] 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: min_part_load_fischer_tropsch: 0 @@ -69,6 +69,8 @@ renewable: min_depth: false solar: cutout: be-03-2013-era5 + solar-hsat: + cutout: be-03-2013-era5 clustering: temporal: diff --git a/doc/release_notes.rst b/doc/release_notes.rst index f7e816ee..2588ecc7 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -9,6 +9,9 @@ Release Notes 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. diff --git a/rules/build_electricity.smk b/rules/build_electricity.smk index b415d2d9..6d5a1131 100644 --- a/rules/build_electricity.smk +++ b/rules/build_electricity.smk @@ -193,7 +193,7 @@ rule determine_availability_matrix_MD_UA: offshore_shapes=resources("offshore_shapes.geojson"), regions=lambda w: ( resources("regions_onshore.geojson") - if w.technology in ("onwind", "solar") + if w.technology in ("onwind", "solar", "solar-hsat") else resources("regions_offshore.geojson") ), cutout=lambda w: "cutouts/" @@ -264,7 +264,7 @@ rule build_renewable_profiles: offshore_shapes=resources("offshore_shapes.geojson"), regions=lambda w: ( resources("regions_onshore.geojson") - if w.technology in ("onwind", "solar") + if w.technology in ("onwind", "solar", "solar-hsat") else resources("regions_offshore.geojson") ), cutout=lambda w: "cutouts/" diff --git a/scripts/add_brownfield.py b/scripts/add_brownfield.py index 76e16716..f54b24dd 100644 --- a/scripts/add_brownfield.py +++ b/scripts/add_brownfield.py @@ -195,8 +195,12 @@ def adjust_renewable_profiles(n, input_profiles, params, year): ) 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 + with xr.open_dataset(getattr(input_profiles, "profile_" + carrier)) as ds: if ds.indexes["bus"].empty or "year" not in ds.indexes: continue diff --git a/scripts/add_electricity.py b/scripts/add_electricity.py index 9fbbc223..a77a9921 100755 --- a/scripts/add_electricity.py +++ b/scripts/add_electricity.py @@ -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"] ) + costs = costs.rename({"solar-utility single-axis tracking": "solar-hsat"}) + def costs_for_storage(store, link1, link2=None, max_hours=1.0): capital_cost = link1["capital_cost"] + max_hours * store["capital_cost"] if link2 is not None: diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index d35af1c9..dbccf90b 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1113,7 +1113,7 @@ def insert_gas_distribution_costs(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)] @@ -3412,6 +3412,13 @@ def remove_h2_network(n): 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): logger.info(f"Limiting new HVAC and HVDC extensions to {maxext} MW") n.lines["s_nom_max"] = n.lines["s_nom"] + maxext @@ -3789,6 +3796,9 @@ if __name__ == "__main__": if options["electricity_distribution_grid"]: 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"]) if options["gas_distribution_grid"]: diff --git a/scripts/solve_network.py b/scripts/solve_network.py index 6438d275..92028f1e 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -31,6 +31,7 @@ import logging import os import re import sys +from functools import reduce import numpy as np import pandas as pd @@ -123,7 +124,15 @@ def add_land_use_constraint_perfect(n): def _add_land_use_constraint(n): # 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 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"] 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 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) +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): """ 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"]: 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_lossy_bidirectional_link_constraints(n) add_pipe_retrofit_constraint(n)