From 8febb73bf15e40ca274a5e40c49027ddc97d7785 Mon Sep 17 00:00:00 2001 From: Philipp Glaum Date: Tue, 21 Dec 2021 07:59:02 +0100 Subject: [PATCH 1/3] Implement line rating --- Snakefile | 18 ++- config.yaml | 265 +++++++++++++++++++++++++++++++++++ scripts/add_electricity.py | 6 + scripts/build_line_rating.py | 89 ++++++++++++ 4 files changed, 375 insertions(+), 3 deletions(-) create mode 100644 config.yaml create mode 100644 scripts/build_line_rating.py diff --git a/Snakefile b/Snakefile index cb50e3bf..eab89679 100644 --- a/Snakefile +++ b/Snakefile @@ -215,12 +215,25 @@ if 'hydro' in config['renewable'].keys(): resources: mem=5000 script: 'scripts/build_hydro_profile.py' +if config['lines'].get('line_rating', False): + rule build_line_rating: + input: + base_network="networks/base.nc", + cutout="cutouts/" + config["lines"]['cutout'] + ".nc" + output: + output="resources/line_rating.nc" + log: "logs/build_line_rating.log" + benchmark: "benchmarks/build_line_rating" + threads: ATLITE_NPROCESSES + resources: mem=ATLITE_NPROCESSES * 1000 + script: "scripts/build_line_rating.py" rule add_electricity: input: - base_network='networks/base.nc', + base_network = "networks/base.nc", tech_costs=COSTS, regions="resources/regions_onshore.geojson", + line_rating="resources/line_rating.nc" if config['lines'].get('line_rating', False) else None, powerplants='resources/powerplants.csv', hydro_capacities='data/bundle/hydro_capacities.csv', geth_hydro_capacities='data/geth2015_hydro_capacities.csv', @@ -397,5 +410,4 @@ rule plot_p_nom_max: input: input_plot_p_nom_max output: "results/plots/elec_s{simpl}_cum_p_nom_max_{clusts}_{techs}_{country}.{ext}" log: "logs/plot_p_nom_max/elec_s{simpl}_{clusts}_{techs}_{country}_{ext}.log" - script: "scripts/plot_p_nom_max.py" - + script: "scripts/plot_p_nom_max.py" \ No newline at end of file diff --git a/config.yaml b/config.yaml new file mode 100644 index 00000000..a31a5a76 --- /dev/null +++ b/config.yaml @@ -0,0 +1,265 @@ +# SPDX-FileCopyrightText: : 2017-2020 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: CC0-1.0 + +version: 0.4.0 +tutorial: true + +logging: + level: INFO + format: '%(levelname)s:%(name)s:%(message)s' + +summary_dir: results + +scenario: + simpl: [''] + ll: ['copt'] + clusters: [5] + opts: [Co2L-24H] + +countries: ['DE'] + +clustering: + simplify: + to_substations: false # network is simplified to nodes with positive or negative power injection (i.e. substations or offwind connections) + +snapshots: + start: "2013-03-01" + end: "2013-03-07" + closed: 'left' # end is not inclusive + +enable: + prepare_links_p_nom: false + retrieve_databundle: true + build_cutout: true + retrieve_cutout: false + build_natura_raster: false + retrieve_natura_raster: true + custom_busmap: false + +electricity: + voltages: [220., 300., 380.] + co2limit: 100.e+6 + + extendable_carriers: + Generator: [OCGT] + StorageUnit: [] #battery, H2 + Store: [battery, H2] + Link: [] + + max_hours: + battery: 6 + H2: 168 + + powerplants_filter: false # use pandas query strings here, e.g. Country not in ['Germany'] + custom_powerplants: false # use pandas query strings here, e.g. Country in ['Germany'] + conventional_carriers: [coal, CCGT] # [nuclear, oil, OCGT, CCGT, coal, lignite, geothermal, biomass] + +atlite: + nprocesses: 4 + cutouts: + europe-2013-era5-tutorial: + module: era5 + x: [4., 15.] + y: [46., 56.] + time: ["2013-03", "2013-03"] + +renewable: + onwind: + cutout: europe-2013-era5-tutorial + resource: + method: wind + turbine: Vestas_V112_3MW + capacity_per_sqkm: 3 # ScholzPhd Tab 4.3.1: 10MW/km^2 + # correction_factor: 0.93 + corine: + # Scholz, Y. (2012). Renewable energy based electricity supply at low costs: + # development of the REMix model and application for Europe. ( p.42 / p.28) + grid_codes: [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 31, 32] + distance: 1000 + distance_grid_codes: [1, 2, 3, 4, 5, 6] + natura: true + potential: simple # or conservative + clip_p_max_pu: 1.e-2 + offwind-ac: + cutout: europe-2013-era5-tutorial + resource: + method: wind + turbine: NREL_ReferenceTurbine_5MW_offshore + capacity_per_sqkm: 3 + # correction_factor: 0.93 + corine: [44, 255] + natura: true + max_shore_distance: 30000 + potential: simple # or conservative + clip_p_max_pu: 1.e-2 + offwind-dc: + cutout: europe-2013-era5-tutorial + resource: + method: wind + turbine: NREL_ReferenceTurbine_5MW_offshore + # ScholzPhd Tab 4.3.1: 10MW/km^2 + capacity_per_sqkm: 3 + # correction_factor: 0.93 + corine: [44, 255] + natura: true + min_shore_distance: 30000 + potential: simple # or conservative + clip_p_max_pu: 1.e-2 + solar: + cutout: europe-2013-era5-tutorial + resource: + method: pv + panel: CSi + orientation: + slope: 35. + azimuth: 180. + capacity_per_sqkm: 1.7 # ScholzPhd Tab 4.3.1: 170 MW/km^2 + # Determined by comparing uncorrected area-weighted full-load hours to those + # published in Supplementary Data to + # Pietzcker, Robert Carl, et al. "Using the sun to decarbonize the power + # sector: The economic potential of photovoltaics and concentrating solar + # power." Applied Energy 135 (2014): 704-720. + correction_factor: 0.854337 + corine: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, + 14, 15, 16, 17, 18, 19, 20, 26, 31, 32] + natura: true + potential: simple # or conservative + clip_p_max_pu: 1.e-2 + +lines: + types: + 220.: "Al/St 240/40 2-bundle 220.0" + 300.: "Al/St 240/40 3-bundle 300.0" + 380.: "Al/St 240/40 4-bundle 380.0" + s_max_pu: 0.7 + s_nom_max: .inf + length_factor: 1.25 + under_construction: 'zero' # 'zero': set capacity to zero, 'remove': remove, 'keep': with full capacity + cutout: "europe-2013-era5-tutorial" + line_rating: true + +links: + p_max_pu: 1.0 + p_nom_max: .inf + include_tyndp: true + under_construction: 'zero' # 'zero': set capacity to zero, 'remove': remove, 'keep': with full capacity + +transformers: + x: 0.1 + s_nom: 2000. + type: '' + +load: + power_statistics: True # only for files from <2019; set false in order to get ENTSOE transparency data + interpolate_limit: 3 # data gaps up until this size are interpolated linearly + time_shift_for_large_gaps: 1w # data gaps up until this size are copied by copying from + manual_adjustments: true # false + scaling_factor: 1.0 + +costs: + year: 2030 + discountrate: 0.07 # From a Lion Hirth paper, also reflects average of Noothout et al 2016 + USD2013_to_EUR2013: 0.7532 # [EUR/USD] ECB: https://www.ecb.europa.eu/stats/exchange/eurofxref/html/eurofxref-graph-usd.en.html + marginal_cost: + solar: 0.01 + onwind: 0.015 + offwind: 0.015 + H2: 0. + battery: 0. + emission_prices: # in currency per tonne emission, only used with the option Ep + co2: 0. + +solving: + options: + formulation: kirchhoff + load_shedding: false + noisy_costs: true + min_iterations: 1 + max_iterations: 1 + clip_p_max_pu: 0.01 + skip_iterations: false + track_iterations: false + solver: + name: cbc + +plotting: + map: + figsize: [7, 7] + boundaries: [-10.2, 29, 35, 72] + p_nom: + bus_size_factor: 5.e+4 + linewidth_factor: 3.e+3 + + costs_max: 800 + costs_threshold: 1 + + energy_max: 15000. + energy_min: -10000. + energy_threshold: 50. + + vre_techs: ["onwind", "offwind-ac", "offwind-dc", "solar", "ror"] + conv_techs: ["OCGT", "CCGT", "Nuclear", "Coal"] + storage_techs: ["hydro+PHS", "battery", "H2"] + load_carriers: ["AC load"] + AC_carriers: ["AC line", "AC transformer"] + link_carriers: ["DC line", "Converter AC-DC"] + tech_colors: + "onwind" : "#235ebc" + "onshore wind" : "#235ebc" + 'offwind' : "#6895dd" + 'offwind-ac' : "#6895dd" + 'offshore wind' : "#6895dd" + 'offshore wind ac' : "#6895dd" + 'offwind-dc' : "#74c6f2" + 'offshore wind dc' : "#74c6f2" + "hydro" : "#08ad97" + "hydro+PHS" : "#08ad97" + "PHS" : "#08ad97" + "hydro reservoir" : "#08ad97" + 'hydroelectricity' : '#08ad97' + "ror" : "#4adbc8" + "run of river" : "#4adbc8" + 'solar' : "#f9d002" + 'solar PV' : "#f9d002" + 'solar thermal' : '#ffef60' + 'biomass' : '#0c6013' + 'solid biomass' : '#06540d' + 'biogas' : '#23932d' + 'waste' : '#68896b' + 'geothermal' : '#ba91b1' + "OCGT" : "#d35050" + "gas" : "#d35050" + "natural gas" : "#d35050" + "CCGT" : "#b20101" + "nuclear" : "#ff9000" + "coal" : "#707070" + "lignite" : "#9e5a01" + "oil" : "#262626" + "H2" : "#ea048a" + "hydrogen storage" : "#ea048a" + "battery" : "#b8ea04" + "Electric load" : "#f9d002" + "electricity" : "#f9d002" + "lines" : "#70af1d" + "transmission lines" : "#70af1d" + "AC-AC" : "#70af1d" + "AC line" : "#70af1d" + "links" : "#8a1caf" + "HVDC links" : "#8a1caf" + "DC-DC" : "#8a1caf" + "DC link" : "#8a1caf" + nice_names: + OCGT: "Open-Cycle Gas" + CCGT: "Combined-Cycle Gas" + offwind-ac: "Offshore Wind (AC)" + offwind-dc: "Offshore Wind (DC)" + onwind: "Onshore Wind" + solar: "Solar" + PHS: "Pumped Hydro Storage" + hydro: "Reservoir & Dam" + battery: "Battery Storage" + H2: "Hydrogen Storage" + lines: "Transmission Lines" + ror: "Run of River" diff --git a/scripts/add_electricity.py b/scripts/add_electricity.py index 08a32a26..8194b906 100755 --- a/scripts/add_electricity.py +++ b/scripts/add_electricity.py @@ -539,6 +539,10 @@ def estimate_renewable_capacities(n, tech_map=None): .where(lambda s: s>0.1, 0.)) # only capacities above 100kW n.generators.loc[tech_i, 'p_nom_min'] = n.generators.loc[tech_i, 'p_nom'] +def attach_line_rating(n): + if snakemake.config["lines"]["line_rating"]: + s_max=xr.open_dataarray(snakemake.input.line_rating).to_pandas().transpose() + n.lines_t.s_max_pu=s_max/n.lines.loc[s_max.columns,:]['s_nom'] #only considers overhead lines def add_nice_carrier_names(n, config=None): if config is None: config = snakemake.config @@ -575,9 +579,11 @@ if __name__ == "__main__": attach_hydro(n, costs, ppl) attach_extendable_generators(n, costs, ppl) + estimate_renewable_capacities(n) attach_OPSD_renewables(n) update_p_nom_max(n) + attach_line_rating(n) add_nice_carrier_names(n) diff --git a/scripts/build_line_rating.py b/scripts/build_line_rating.py new file mode 100644 index 00000000..95424def --- /dev/null +++ b/scripts/build_line_rating.py @@ -0,0 +1,89 @@ +# SPDX-FileCopyrightText: : 2017-2020 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT + +# coding: utf-8 +""" +Adds dynamic line rating timeseries to the base network. + +Relevant Settings +----------------- + +.. code:: yaml + + lines_t: + s_max_pu + + +.. seealso:: + Documentation of the configuration file ``config.yaml` +Inputs +------ + +- ``data/cutouts``: +- ``networks/base.nc``: confer :ref:`base` + +Outputs +------- + +- ``networks/base_with_line_rating.nc`` + + +Description +----------- + +The rule :mod:`build_line_rating` calculates the line rating for transmission lines. +The line rating provides the maximal capacity of a transmission line considering the heat exchange with the environment. + +The folloing heat gains and losses are considered: + +- heat gain through resistive losses +- heat gain trough solar radiation +- heat loss through radiation of the trasnmission line +- heat loss through forced convection with wind +- heat loss through natural convection + + +With a heat balance considering the maximum temperature threshold of the tranmission line, +the maximal possible capacity factor "s_max_pu" for each transmission line at each time step is calculated. +""" + +import logging +from _helpers import configure_logging + +import pypsa +import pandas as pd +import numpy as np +import geopandas as gpd +from shapely.geometry import Point, LineString as Line +import atlite +import xarray as xr + + +def calculate_line_rating(n): + relevant_lines=n.lines[(n.lines['underground']==False) & (n.lines['under_construction']==False)] + buses = relevant_lines[["bus0", "bus1"]].values + x = n.buses.x + y = n.buses.y + shapes = [Line([Point(x[b0], y[b0]), Point(x[b1], y[b1])]) for (b0, b1) in buses] + shapes = gpd.GeoSeries(shapes, index=relevant_lines.index) + cutout = atlite.Cutout(snakemake.input.cutout) + if relevant_lines.r_pu.eq(0).all(): + #Overwrite standard line resistance with line resistance obtained from line type + relevant_lines["r_pu"]=relevant_lines.join(n.line_types["r_per_length"], on=["type"])['r_per_length']/1000 #in meters + Imax=cutout.line_rating(shapes, relevant_lines.r_pu) + da = xr.DataArray(data=np.sqrt(3) * Imax * relevant_lines["v_nom"].values.reshape(-1,1) * relevant_lines["num_parallel"].values.reshape(-1,1)/1e3, #in mW + attrs=dict(description="Maximal possible power in MW for given line considering line rating")) + return da + +if __name__ == "__main__": + if 'snakemake' not in globals(): + from _helpers import mock_snakemake + snakemake = mock_snakemake('build_line_rating', network='elec', simpl='', + clusters='6', ll='copt', opts='Co2L-24H') + configure_logging(snakemake) + + n = pypsa.Network(snakemake.input.base_network) + da=calculate_line_rating(n) + + da.to_netcdf(snakemake.output[0]) \ No newline at end of file From bbb609d1acb5ae419950518b181d3fee8eab0a08 Mon Sep 17 00:00:00 2001 From: Philipp Glaum Date: Tue, 21 Dec 2021 08:33:06 +0100 Subject: [PATCH 2/3] fix Snakefile --- Snakefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Snakefile b/Snakefile index eab89679..f62f3f09 100644 --- a/Snakefile +++ b/Snakefile @@ -233,7 +233,7 @@ rule add_electricity: base_network = "networks/base.nc", tech_costs=COSTS, regions="resources/regions_onshore.geojson", - line_rating="resources/line_rating.nc" if config['lines'].get('line_rating', False) else None, + line_rating="resources/line_rating.nc" if config['lines'].get('line_rating', False) else "None", powerplants='resources/powerplants.csv', hydro_capacities='data/bundle/hydro_capacities.csv', geth_hydro_capacities='data/geth2015_hydro_capacities.csv', @@ -410,4 +410,4 @@ rule plot_p_nom_max: input: input_plot_p_nom_max output: "results/plots/elec_s{simpl}_cum_p_nom_max_{clusts}_{techs}_{country}.{ext}" log: "logs/plot_p_nom_max/elec_s{simpl}_{clusts}_{techs}_{country}_{ext}.log" - script: "scripts/plot_p_nom_max.py" \ No newline at end of file + script: "scripts/plot_p_nom_max.py" From cd28f0bb71d80f47faaa886c7883ccbb27d04197 Mon Sep 17 00:00:00 2001 From: Philipp Glaum Date: Tue, 21 Dec 2021 08:33:06 +0100 Subject: [PATCH 3/3] fix Snakefile and update comments --- Snakefile | 4 ++-- scripts/build_line_rating.py | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Snakefile b/Snakefile index eab89679..04b43211 100644 --- a/Snakefile +++ b/Snakefile @@ -233,7 +233,7 @@ rule add_electricity: base_network = "networks/base.nc", tech_costs=COSTS, regions="resources/regions_onshore.geojson", - line_rating="resources/line_rating.nc" if config['lines'].get('line_rating', False) else None, + line_rating="resources/line_rating.nc" if config['lines'].get('line_rating', False) else "networks/base.nc", powerplants='resources/powerplants.csv', hydro_capacities='data/bundle/hydro_capacities.csv', geth_hydro_capacities='data/geth2015_hydro_capacities.csv', @@ -410,4 +410,4 @@ rule plot_p_nom_max: input: input_plot_p_nom_max output: "results/plots/elec_s{simpl}_cum_p_nom_max_{clusts}_{techs}_{country}.{ext}" log: "logs/plot_p_nom_max/elec_s{simpl}_{clusts}_{techs}_{country}_{ext}.log" - script: "scripts/plot_p_nom_max.py" \ No newline at end of file + script: "scripts/plot_p_nom_max.py" diff --git a/scripts/build_line_rating.py b/scripts/build_line_rating.py index 95424def..c44c4c13 100644 --- a/scripts/build_line_rating.py +++ b/scripts/build_line_rating.py @@ -11,8 +11,9 @@ Relevant Settings .. code:: yaml - lines_t: - s_max_pu + lines: + cutout: + line_rating: .. seealso:: @@ -26,7 +27,7 @@ Inputs Outputs ------- -- ``networks/base_with_line_rating.nc`` +- ``resources/line_rating.nc`` Description