From db073d8daaee9cbcda7719d2cc9594feb6b80f5d Mon Sep 17 00:00:00 2001 From: Jonas Hoersch Date: Wed, 19 Dec 2018 10:30:25 +0100 Subject: [PATCH] Split offshore wind turbines into AC and DC connections --- config.yaml | 16 +++++++- data/costs.csv | 13 +++++-- scripts/add_electricity.py | 25 +++++++------ scripts/simplify_network.py | 74 +++++++++++++++++++++++-------------- 4 files changed, 85 insertions(+), 43 deletions(-) diff --git a/config.yaml b/config.yaml index e39ab16a..bba0e0c3 100644 --- a/config.yaml +++ b/config.yaml @@ -72,7 +72,7 @@ renewable: distance_grid_codes: [1, 2, 3, 4, 5, 6] natura: true potential: conservative # or heuristic - offwind: + offwind-ac: cutout: europe-2013-era5 resource: method: wind @@ -83,6 +83,20 @@ renewable: corine: [44, 255] natura: true max_depth: 50 + max_shore_distance: 80000 + potential: conservative # or heuristic + offwind-dc: + cutout: europe-2013-era5 + 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 + max_depth: 50 + min_shore_distance: 80000 potential: conservative # or heuristic solar: cutout: europe-2013-sarah diff --git a/data/costs.csv b/data/costs.csv index ee216d36..0e53afe9 100644 --- a/data/costs.csv +++ b/data/costs.csv @@ -16,10 +16,15 @@ lignite,2030,lifetime,40,years,IEA2010 geothermal,2030,lifetime,40,years,IEA2010 biomass,2030,lifetime,30,years,ECF2010 in DIW DataDoc http://hdl.handle.net/10419/80348 oil,2030,lifetime,30,years,ECF2010 in DIW DataDoc http://hdl.handle.net/10419/80348 -onwind,2030,investment,910,EUR/kWel,DEA https://ens.dk/en/our-services/projections-and-models/technology-data +onwind,2030,investment,1110,EUR/kWel,DEA https://ens.dk/en/our-services/projections-and-models/technology-data +onwind-landcosts,2030,investment,200,EUR/kWel,Land costs and compensation payments conservatively estimated based on DEA https://ens.dk/en/our-services/projections-and-models/technology-data offwind,2030,investment,1640,EUR/kWel,DEA https://ens.dk/en/our-services/projections-and-models/technology-data -offwind-grid,2030,investment,255,EUR/kWel,Haertel 2017; assuming one onshore and one offshore node -offwind-grid-perlength,2030,investment,0.97,EUR/kWel/km,Haertel 2017 +offwind-ac-station,2030,investment,250,EUR/kWel,DEA https://ens.dk/en/our-services/projections-and-models/technology-data +offwind-ac-connection-submarine,2030,investment,2685,EUR/MW/km,DEA https://ens.dk/en/our-services/projections-and-models/technology-data +offwind-ac-connection-underground,2030,investment,1342,EUR/MW/km,DEA https://ens.dk/en/our-services/projections-and-models/technology-data +offwind-dc-station,2030,investment,400,EUR/kWel,Haertel 2017; assuming one onshore and one offshore node + 13% learning reduction +offwind-dc-connection-submarine,2030,investment,2000,EUR/MW/km,DTU report based on Fig 34 of https://ec.europa.eu/energy/sites/ener/files/documents/2014_nsog_report.pdf +offwind-dc-connection-underground,2030,investment,1000,EUR/MW/km,Haertel 2017; average + 13% learning reduction solar,2030,investment,600,EUR/kWel,DIW DataDoc http://hdl.handle.net/10419/80348 biomass,2030,investment,2209,EUR/kWel,DIW DataDoc http://hdl.handle.net/10419/80348 geothermal,2030,investment,3392,EUR/kWel,DIW DataDoc http://hdl.handle.net/10419/80348 @@ -177,7 +182,7 @@ HVAC overhead,2030,FOM,2,%/year,Hagspiel HVDC overhead,2030,investment,400,EUR/MW/km,Hagspiel HVDC overhead,2030,lifetime,40,years,Hagspiel HVDC overhead,2030,FOM,2,%/year,Hagspiel -HVDC submarine,2030,investment,2000,EUR/MW/km,Own analysis of European submarine HVDC projects since 2000 +HVDC submarine,2030,investment,2000,EUR/MW/km,DTU report based on Fig 34 of https://ec.europa.eu/energy/sites/ener/files/documents/2014_nsog_report.pdf HVDC submarine,2030,lifetime,40,years,Hagspiel HVDC submarine,2030,FOM,2,%/year,Hagspiel HVDC inverter pair,2030,investment,150000,EUR/MW,Hagspiel diff --git a/scripts/add_electricity.py b/scripts/add_electricity.py index ac4362f9..bfe1440e 100644 --- a/scripts/add_electricity.py +++ b/scripts/add_electricity.py @@ -161,15 +161,18 @@ def attach_wind_and_solar(n, costs): n.add("Carrier", name=tech) with xr.open_dataset(getattr(snakemake.input, 'profile_' + tech)) as ds: - capital_cost = costs.at[tech, 'capital_cost'] - if tech + "-grid" in costs.index: - if tech + "-grid-perlength" in costs.index: - grid_cost = costs.at[tech + "-grid", "capital_cost"] + costs.at[tech + "-grid-perlength", 'capital_cost'] * ds['average_distance'].to_pandas() - logger.info("Added connection cost of {:0.0f}-{:0.0f} Eur/MW/a to {}".format(grid_cost.min(), grid_cost.max(), tech)) - else: - grid_cost = costs.at[tech + "-grid", "capital_cost"] - logger.info("Added connection cost of {:0.0f} Eur/MW/a to {}".format(grid_cost, tech)) - capital_cost = capital_cost + grid_cost + suptech = tech.split('-', 2)[0] + if suptech == 'offwind': + underwater_fraction = ds['underwater_fraction'].to_pandas() + connection_cost = (snakemake.config['lines']['length_factor'] * ds['average_distance'].to_pandas() * + (underwater_fraction * costs.at[tech + '-connection-submarine', 'capital_cost'] + + (1. - underwater_fraction) * costs.at[tech + '-connection-underground', 'capital_cost'])) + capital_cost = costs.at['offwind', 'capital_cost'] + costs.at[tech + '-station', 'capital_cost'] + connection_cost + logger.info("Added connection cost of {:0.0f}-{:0.0f} Eur/MW/a to {}".format(connection_cost.min(), connection_cost.max(), tech)) + elif suptech == 'onwind': + capital_cost = costs.at['onwind', 'capital_cost'] + costs.at['onwind-landcosts', 'capital_cost'] + else: + capital_cost = costs.at[tech, 'capital_cost'] n.madd("Generator", ds.indexes['bus'], ' ' + tech, bus=ds.indexes['bus'], @@ -177,9 +180,9 @@ def attach_wind_and_solar(n, costs): p_nom_extendable=True, p_nom_max=ds['p_nom_max'].to_pandas(), weight=ds['weight'].to_pandas(), - marginal_cost=costs.at[tech, 'marginal_cost'], + marginal_cost=costs.at[suptech, 'marginal_cost'], capital_cost=capital_cost, - efficiency=costs.at[tech, 'efficiency'], + efficiency=costs.at[suptech, 'efficiency'], p_max_pu=ds['profile'].transpose('time', 'bus').to_pandas()) diff --git a/scripts/simplify_network.py b/scripts/simplify_network.py index 14811a64..6a93cbb6 100644 --- a/scripts/simplify_network.py +++ b/scripts/simplify_network.py @@ -63,22 +63,51 @@ def simplify_network_to_380(n): return n, trafo_map -def _adjust_costs_using_distance(n, distance): +def _prepare_connection_costs_per_link(n): costs = load_costs(n.snapshot_weightings.sum() / 8760, snakemake.input.tech_costs, snakemake.config['costs'], snakemake.config['electricity']) + connection_costs_per_link = {} + for tech in snakemake.config['renewable']: - if tech + "-grid-perlength" in costs.index: - cost_perlength = costs.at[tech + "-grid-perlength", "capital_cost"] - tech_b = n.generators.carrier == tech - generator_distance = n.generators.loc[tech_b, "bus"].map(distance).loc[lambda s: s>0] - if not generator_distance.empty: - n.generators.loc[generator_distance.index, "capital_cost"] += cost_perlength * generator_distance - logger.info("Displacing generator(s) {}; capital_cost is adjusted accordingly" - .format(", ".join("`{}` by {:.0f}km".format(b, d) for b, d in generator_distance.iteritems()))) + if tech.startswith('offwind'): + connection_costs_per_link[tech] = ( + n.links.length * snakemake.config['lines']['length_factor'] * + (n.links.underwater_fraction * costs.at[tech + '-connection-submarine', 'capital_cost'] + + (1. - n.links.underwater_fraction) * costs.at[tech + '-connection-underground', 'capital_cost']) + ) + return connection_costs_per_link -def _aggregate_and_move_components(n, busmap, distance, aggregate_one_ports={"Load", "StorageUnit"}): +def _compute_connection_costs_to_bus(n, busmap, connection_costs_per_link=None, buses=None): + if connection_costs_per_link is None: + connection_costs_per_link = _prepare_connection_costs_per_link(n) + + if buses is None: + buses = busmap.index[busmap.index != busmap.values] + + connection_costs_to_bus = pd.DataFrame(index=buses) + + for tech in connection_costs_per_link: + adj = n.adjacency_matrix(weights=pd.concat(dict(Link=connection_costs_per_link[tech].reindex(n.links.index), + Line=pd.Series(0., n.lines.index)))) + + costs_between_buses = dijkstra(adj, directed=False, indices=n.buses.index.get_indexer(buses)) + connection_costs_to_bus[tech] = costs_between_buses[np.arange(len(buses)), + n.buses.index.get_indexer(busmap.loc[buses])] + + return connection_costs_to_bus + +def _adjust_capital_costs_using_connection_costs(n, connection_costs_to_bus): + for tech in connection_costs_to_bus: + tech_b = n.generators.carrier == tech + costs = n.generators.loc[tech_b, "bus"].map(connection_costs_to_bus[tech]).loc[lambda s: s>0] + if not costs.empty: + n.generators.loc[costs.index, "capital_cost"] += costs + logger.info("Displacing {} generator(s) and adding connection costs {} to capital_costs" + .format(tech, ", ".join("of {:.0f} Eur/MW to `{}`".format(d, b) for b, d in costs.iteritems()))) + +def _aggregate_and_move_components(n, busmap, connection_costs_to_bus, aggregate_one_ports={"Load", "StorageUnit"}): def replace_components(n, c, df, pnl): n.mremove(c, n.df(c).index) @@ -87,7 +116,7 @@ def _aggregate_and_move_components(n, busmap, distance, aggregate_one_ports={"Lo if not df.empty: import_series_from_dataframe(n, df, c, attr) - _adjust_costs_using_distance(n, distance) + _adjust_capital_costs_using_connection_costs(n, connection_costs_to_bus) generators, generators_pnl = aggregategenerators(n, busmap) replace_components(n, "Generator", generators, generators_pnl) @@ -102,16 +131,6 @@ def _aggregate_and_move_components(n, busmap, distance, aggregate_one_ports={"Lo df = n.df(c) n.mremove(c, df.index[df.bus0.isin(buses_to_del) | df.bus1.isin(buses_to_del)]) -def _compute_distance(n, busmap, buses=None, adjacency_matrix=None): - if buses is None: - buses = busmap.index[busmap.index != busmap.values] - - if adjacency_matrix is None: - adjacency_matrix = n.adjacency_matrix(weights=pd.concat(dict(Link=n.links.length, Line=pd.Series(0., n.lines.index)))) - - dist = dijkstra(adjacency_matrix, directed=False, indices=n.buses.index.get_indexer(buses)) - return pd.Series(dist[np.arange(len(buses)), n.buses.index.get_indexer(busmap.loc[buses])], buses) - def simplify_links(n): ## Complex multi-node links are folded into end-points logger.info("Simplifying connected link components") @@ -155,8 +174,9 @@ def simplify_links(n): seen.add(u) busmap = n.buses.index.to_series() - distance = pd.Series(0., n.buses.index) - adjacency_matrix = n.adjacency_matrix(weights=pd.concat(dict(Link=n.links.length, Line=pd.Series(0., n.lines.index)))) + + connection_costs_per_link = _prepare_connection_costs_per_link(n) + connection_costs_to_bus = pd.DataFrame(0., index=n.buses.index, columns=list(connection_costs_per_link)) for lbl in labels.value_counts().loc[lambda s: s > 2].index: @@ -169,7 +189,7 @@ def simplify_links(n): m = sp.spatial.distance_matrix(n.buses.loc[b, ['x', 'y']], n.buses.loc[buses[1:-1], ['x', 'y']]) busmap.loc[buses] = b[np.r_[0, m.argmin(axis=0), 1]] - distance.loc[buses] += _compute_distance(n, busmap, buses) + connection_costs_to_bus.loc[buses] += _compute_connection_costs_to_bus(n, busmap, connection_costs_per_link, buses) all_links = [i for _, i in sum(links, [])] @@ -200,7 +220,7 @@ def simplify_links(n): logger.debug("Collecting all components using the busmap") - _aggregate_and_move_components(n, busmap, distance) + _aggregate_and_move_components(n, busmap, connection_costs_to_bus) return n, busmap def remove_stubs(n): @@ -208,9 +228,9 @@ def remove_stubs(n): busmap = busmap_by_stubs(n) # ['country']) - distance = _compute_distance(n, busmap) + connection_costs_to_bus = _compute_connection_costs_to_bus(n, busmap) - _aggregate_and_move_components(n, busmap, distance) + _aggregate_and_move_components(n, busmap, connection_costs_to_bus) return n, busmap