2018-01-30 22:11:16 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
2022-07-26 13:05:59 +00:00
|
|
|
# SPDX-FileCopyrightText: : 2017-2022 The PyPSA-Eur Authors
|
2020-05-29 07:50:55 +00:00
|
|
|
#
|
2021-09-14 14:37:41 +00:00
|
|
|
# SPDX-License-Identifier: MIT
|
2020-05-29 07:50:55 +00:00
|
|
|
|
2018-01-30 22:11:16 +00:00
|
|
|
# coding: utf-8
|
2019-08-11 20:34:18 +00:00
|
|
|
"""
|
|
|
|
Lifts electrical transmission network to a single 380 kV voltage layer, removes
|
|
|
|
dead-ends of the network, and reduces multi-hop HVDC connections to a single
|
|
|
|
link.
|
2019-08-10 12:25:19 +00:00
|
|
|
|
2019-08-11 09:40:47 +00:00
|
|
|
Relevant Settings
|
|
|
|
-----------------
|
|
|
|
|
2019-08-11 11:17:36 +00:00
|
|
|
.. code:: yaml
|
|
|
|
|
2022-06-20 16:58:23 +00:00
|
|
|
clustering:
|
2022-06-27 19:02:45 +00:00
|
|
|
simplify_network:
|
|
|
|
cluster_network:
|
2022-06-20 16:58:23 +00:00
|
|
|
aggregation_strategies:
|
|
|
|
|
2019-08-11 11:17:36 +00:00
|
|
|
costs:
|
2020-10-16 09:38:26 +00:00
|
|
|
year:
|
|
|
|
version:
|
|
|
|
fill_values:
|
2019-08-11 11:17:36 +00:00
|
|
|
marginal_cost:
|
|
|
|
capital_cost:
|
|
|
|
|
|
|
|
electricity:
|
|
|
|
max_hours:
|
|
|
|
|
|
|
|
lines:
|
|
|
|
length_factor:
|
|
|
|
|
|
|
|
links:
|
|
|
|
p_max_pu:
|
|
|
|
|
|
|
|
solving:
|
|
|
|
solver:
|
|
|
|
name:
|
|
|
|
|
2019-11-14 16:50:24 +00:00
|
|
|
.. seealso::
|
2019-08-13 08:03:46 +00:00
|
|
|
Documentation of the configuration file ``config.yaml`` at
|
|
|
|
:ref:`costs_cf`, :ref:`electricity_cf`, :ref:`renewable_cf`,
|
|
|
|
:ref:`lines_cf`, :ref:`links_cf`, :ref:`solving_cf`
|
|
|
|
|
2019-08-11 09:40:47 +00:00
|
|
|
Inputs
|
|
|
|
------
|
|
|
|
|
2020-10-16 09:38:26 +00:00
|
|
|
- ``resources/costs.csv``: The database of cost assumptions for all included technologies for specific years from various sources; e.g. discount rate, lifetime, investment (CAPEX), fixed operation and maintenance (FOM), variable operation and maintenance (VOM), fuel costs, efficiency, carbon-dioxide intensity.
|
2019-08-11 20:34:18 +00:00
|
|
|
- ``resources/regions_onshore.geojson``: confer :ref:`busregions`
|
|
|
|
- ``resources/regions_offshore.geojson``: confer :ref:`busregions`
|
2020-12-03 18:50:53 +00:00
|
|
|
- ``networks/elec.nc``: confer :ref:`electricity`
|
2019-08-11 20:34:18 +00:00
|
|
|
|
2019-08-11 09:40:47 +00:00
|
|
|
Outputs
|
|
|
|
-------
|
|
|
|
|
2020-12-03 18:50:53 +00:00
|
|
|
- ``resources/regions_onshore_elec_s{simpl}.geojson``:
|
2019-08-12 17:01:53 +00:00
|
|
|
|
2019-08-14 13:36:46 +00:00
|
|
|
.. image:: ../img/regions_onshore_elec_s.png
|
2019-08-12 17:01:53 +00:00
|
|
|
:scale: 33 %
|
|
|
|
|
2020-12-03 18:50:53 +00:00
|
|
|
- ``resources/regions_offshore_elec_s{simpl}.geojson``:
|
2019-08-12 17:01:53 +00:00
|
|
|
|
2019-08-14 13:36:46 +00:00
|
|
|
.. image:: ../img/regions_offshore_elec_s .png
|
2019-08-12 17:01:53 +00:00
|
|
|
:scale: 33 %
|
|
|
|
|
2020-12-03 18:50:53 +00:00
|
|
|
- ``resources/busmap_elec_s{simpl}.csv``: Mapping of buses from ``networks/elec.nc`` to ``networks/elec_s{simpl}.nc``;
|
|
|
|
- ``networks/elec_s{simpl}.nc``:
|
2019-08-11 20:34:18 +00:00
|
|
|
|
2019-08-14 13:36:46 +00:00
|
|
|
.. image:: ../img/elec_s.png
|
2019-08-11 20:34:18 +00:00
|
|
|
:scale: 33 %
|
|
|
|
|
2019-08-11 09:40:47 +00:00
|
|
|
Description
|
|
|
|
-----------
|
|
|
|
|
2019-08-13 15:52:33 +00:00
|
|
|
The rule :mod:`simplify_network` does up to four things:
|
2019-08-10 12:25:19 +00:00
|
|
|
|
|
|
|
1. Create an equivalent transmission network in which all voltage levels are mapped to the 380 kV level by the function ``simplify_network(...)``.
|
|
|
|
|
2022-09-16 11:33:38 +00:00
|
|
|
2. DC only sub-networks that are connected at only two buses to the AC network are reduced to a single representative link in the function ``simplify_links(...)``. The components attached to buses in between are moved to the nearest endpoint. The grid connection cost of offshore wind generators are added to the capital costs of the generator.
|
2019-08-10 12:25:19 +00:00
|
|
|
|
2019-08-12 17:01:53 +00:00
|
|
|
3. Stub lines and links, i.e. dead-ends of the network, are sequentially removed from the network in the function ``remove_stubs(...)``. Components are moved along.
|
2019-08-10 12:25:19 +00:00
|
|
|
|
2019-08-12 17:01:53 +00:00
|
|
|
4. Optionally, if an integer were provided for the wildcard ``{simpl}`` (e.g. ``networks/elec_s500.nc``), the network is clustered to this number of clusters with the routines from the ``cluster_network`` rule with the function ``cluster_network.cluster(...)``. This step is usually skipped!
|
2019-08-08 13:02:28 +00:00
|
|
|
"""
|
2018-01-30 22:11:16 +00:00
|
|
|
|
2019-11-28 07:22:52 +00:00
|
|
|
import logging
|
2021-05-25 09:29:47 +00:00
|
|
|
from functools import reduce
|
2018-01-30 22:11:16 +00:00
|
|
|
|
|
|
|
import numpy as np
|
2019-11-14 16:50:24 +00:00
|
|
|
import pandas as pd
|
2018-01-30 22:11:16 +00:00
|
|
|
import pypsa
|
|
|
|
import scipy as sp
|
2022-06-27 12:18:47 +00:00
|
|
|
from _helpers import configure_logging, get_aggregation_strategies, update_p_nom_max
|
2019-11-14 16:50:24 +00:00
|
|
|
from add_electricity import load_costs
|
|
|
|
from cluster_network import cluster_regions, clustering_for_n_clusters
|
2018-01-30 22:11:16 +00:00
|
|
|
from pypsa.io import import_components_from_dataframe, import_series_from_dataframe
|
2022-07-27 07:02:34 +00:00
|
|
|
from pypsa.networkclustering import (
|
|
|
|
aggregategenerators,
|
|
|
|
aggregateoneport,
|
|
|
|
busmap_by_stubs,
|
|
|
|
get_clustering_from_busmap,
|
2022-09-16 13:04:04 +00:00
|
|
|
)
|
2018-10-27 01:41:38 +00:00
|
|
|
from scipy.sparse.csgraph import connected_components, dijkstra
|
2018-01-30 22:11:16 +00:00
|
|
|
|
2020-12-03 18:50:53 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
2018-01-30 22:11:16 +00:00
|
|
|
|
|
|
|
def simplify_network_to_380(n):
|
2022-09-05 10:51:30 +00:00
|
|
|
"""
|
|
|
|
Fix all lines to a voltage level of 380 kV and remove all transformers.
|
|
|
|
|
2023-01-23 21:27:19 +00:00
|
|
|
The function preserves the transmission capacity for each line while
|
|
|
|
updating
|
|
|
|
its voltage level, line type and number of parallel bundles
|
|
|
|
(num_parallel).
|
|
|
|
|
|
|
|
Transformers are removed and connected components are moved from
|
|
|
|
their
|
|
|
|
starting bus to their ending bus. The corresponding starting buses
|
|
|
|
are
|
2022-09-05 10:51:30 +00:00
|
|
|
removed as well.
|
|
|
|
"""
|
2018-01-30 22:11:16 +00:00
|
|
|
logger.info("Mapping all network lines onto a single 380kV layer")
|
|
|
|
|
|
|
|
n.buses["v_nom"] = 380.0
|
|
|
|
|
|
|
|
(linetype_380,) = n.lines.loc[n.lines.v_nom == 380.0, "type"].unique()
|
2022-09-05 10:51:30 +00:00
|
|
|
n.lines["type"] = linetype_380
|
|
|
|
n.lines["v_nom"] = 380
|
|
|
|
n.lines["i_nom"] = n.line_types.i_nom[linetype_380]
|
|
|
|
n.lines["num_parallel"] = n.lines.eval("s_nom / (sqrt(3) * v_nom * i_nom)")
|
2018-01-30 22:11:16 +00:00
|
|
|
|
2022-09-05 10:51:30 +00:00
|
|
|
trafo_map = pd.Series(n.transformers.bus1.values, n.transformers.bus0.values)
|
2018-01-30 22:11:16 +00:00
|
|
|
trafo_map = trafo_map[~trafo_map.index.duplicated(keep="first")]
|
|
|
|
several_trafo_b = trafo_map.isin(trafo_map.index)
|
2022-09-05 10:51:30 +00:00
|
|
|
trafo_map[several_trafo_b] = trafo_map[several_trafo_b].map(trafo_map)
|
2018-01-30 22:11:16 +00:00
|
|
|
missing_buses_i = n.buses.index.difference(trafo_map.index)
|
2022-01-29 15:17:46 +00:00
|
|
|
missing = pd.Series(missing_buses_i, missing_buses_i)
|
|
|
|
trafo_map = pd.concat([trafo_map, missing])
|
2018-01-30 22:11:16 +00:00
|
|
|
|
|
|
|
for c in n.one_port_components | n.branch_components:
|
|
|
|
df = n.df(c)
|
|
|
|
for col in df.columns:
|
|
|
|
if col.startswith("bus"):
|
|
|
|
df[col] = df[col].map(trafo_map)
|
|
|
|
|
|
|
|
n.mremove("Transformer", n.transformers.index)
|
|
|
|
n.mremove("Bus", n.buses.index.difference(trafo_map))
|
|
|
|
|
|
|
|
return n, trafo_map
|
|
|
|
|
2020-12-03 18:50:53 +00:00
|
|
|
|
2022-01-24 18:13:48 +00:00
|
|
|
def _prepare_connection_costs_per_link(n, costs, config):
|
2019-02-13 18:03:57 +00:00
|
|
|
if n.links.empty:
|
|
|
|
return {}
|
|
|
|
|
2018-12-19 09:30:25 +00:00
|
|
|
connection_costs_per_link = {}
|
|
|
|
|
2022-01-24 18:13:48 +00:00
|
|
|
for tech in config["renewable"]:
|
2018-12-19 09:30:25 +00:00
|
|
|
if tech.startswith("offwind"):
|
|
|
|
connection_costs_per_link[tech] = (
|
2022-01-24 18:13:48 +00:00
|
|
|
n.links.length
|
|
|
|
* config["lines"]["length_factor"]
|
|
|
|
* (
|
2018-12-19 09:30:25 +00:00
|
|
|
n.links.underwater_fraction
|
|
|
|
* costs.at[tech + "-connection-submarine", "capital_cost"]
|
|
|
|
+ (1.0 - n.links.underwater_fraction)
|
|
|
|
* costs.at[tech + "-connection-underground", "capital_cost"]
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
return connection_costs_per_link
|
|
|
|
|
2020-12-03 18:50:53 +00:00
|
|
|
|
2022-01-24 18:13:48 +00:00
|
|
|
def _compute_connection_costs_to_bus(
|
|
|
|
n, busmap, costs, config, connection_costs_per_link=None, buses=None
|
|
|
|
):
|
2018-12-19 09:30:25 +00:00
|
|
|
if connection_costs_per_link is None:
|
2022-01-24 18:13:48 +00:00
|
|
|
connection_costs_per_link = _prepare_connection_costs_per_link(n, costs, config)
|
2018-12-19 09:30:25 +00:00
|
|
|
|
|
|
|
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.0, n.lines.index),
|
|
|
|
)
|
2022-09-16 13:04:04 +00:00
|
|
|
)
|
|
|
|
)
|
2018-10-27 01:41:38 +00:00
|
|
|
|
2018-12-19 09:30:25 +00:00
|
|
|
costs_between_buses = dijkstra(
|
|
|
|
adj, directed=False, indices=n.buses.index.get_indexer(buses)
|
2022-09-16 13:04:04 +00:00
|
|
|
)
|
2018-12-19 09:30:25 +00:00
|
|
|
connection_costs_to_bus[tech] = costs_between_buses[
|
|
|
|
np.arange(len(buses)), n.buses.index.get_indexer(busmap.loc[buses])
|
|
|
|
]
|
2018-10-27 01:41:38 +00:00
|
|
|
|
2018-12-19 09:30:25 +00:00
|
|
|
return connection_costs_to_bus
|
|
|
|
|
2020-12-03 18:50:53 +00:00
|
|
|
|
2022-01-24 18:13:48 +00:00
|
|
|
def _adjust_capital_costs_using_connection_costs(n, connection_costs_to_bus, output):
|
2021-08-04 16:19:23 +00:00
|
|
|
connection_costs = {}
|
2018-12-19 09:30:25 +00:00
|
|
|
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]
|
2022-09-16 13:04:04 +00:00
|
|
|
)
|
2018-12-19 09:30:25 +00:00
|
|
|
if not costs.empty:
|
|
|
|
n.generators.loc[costs.index, "capital_cost"] += costs
|
2018-12-21 15:16:54 +00:00
|
|
|
logger.info(
|
|
|
|
"Displacing {} generator(s) and adding connection costs to capital_costs: {} ".format(
|
|
|
|
tech,
|
|
|
|
", ".join(
|
|
|
|
"{:.0f} Eur/MW/a for `{}`".format(d, b)
|
2022-12-27 12:16:05 +00:00
|
|
|
for b, d in costs.items()
|
2022-09-16 13:04:04 +00:00
|
|
|
),
|
|
|
|
)
|
|
|
|
)
|
2021-08-04 16:19:23 +00:00
|
|
|
connection_costs[tech] = costs
|
2022-09-05 10:51:30 +00:00
|
|
|
pd.DataFrame(connection_costs).to_csv(output.connection_costs)
|
|
|
|
|
2018-12-19 09:30:25 +00:00
|
|
|
|
2022-06-24 18:57:53 +00:00
|
|
|
def _aggregate_and_move_components(
|
|
|
|
n,
|
|
|
|
busmap,
|
|
|
|
connection_costs_to_bus,
|
|
|
|
output,
|
|
|
|
aggregate_one_ports={"Load", "StorageUnit"},
|
|
|
|
aggregation_strategies=dict(),
|
2022-09-19 14:34:21 +00:00
|
|
|
exclude_carriers=None,
|
2022-06-24 18:57:53 +00:00
|
|
|
):
|
2018-01-30 22:11:16 +00:00
|
|
|
def replace_components(n, c, df, pnl):
|
|
|
|
n.mremove(c, n.df(c).index)
|
|
|
|
|
|
|
|
import_components_from_dataframe(n, df, c)
|
2021-05-25 09:29:47 +00:00
|
|
|
for attr, df in pnl.items():
|
2018-01-30 22:11:16 +00:00
|
|
|
if not df.empty:
|
|
|
|
import_series_from_dataframe(n, df, c, attr)
|
|
|
|
|
2022-01-24 18:13:48 +00:00
|
|
|
_adjust_capital_costs_using_connection_costs(n, connection_costs_to_bus, output)
|
2018-10-27 01:41:38 +00:00
|
|
|
|
2022-06-27 12:18:47 +00:00
|
|
|
_, generator_strategies = get_aggregation_strategies(aggregation_strategies)
|
2022-09-19 14:34:47 +00:00
|
|
|
|
2022-09-19 09:46:58 +00:00
|
|
|
carriers = set(n.generators.carrier) - set(exclude_carriers)
|
2022-06-24 18:57:53 +00:00
|
|
|
generators, generators_pnl = aggregategenerators(
|
2022-09-16 12:25:15 +00:00
|
|
|
n, busmap, carriers=carriers, custom_strategies=generator_strategies
|
2022-06-24 18:57:53 +00:00
|
|
|
)
|
2022-06-28 06:09:39 +00:00
|
|
|
|
2018-01-30 22:11:16 +00:00
|
|
|
replace_components(n, "Generator", generators, generators_pnl)
|
|
|
|
|
|
|
|
for one_port in aggregate_one_ports:
|
|
|
|
df, pnl = aggregateoneport(n, busmap, component=one_port)
|
|
|
|
replace_components(n, one_port, df, pnl)
|
|
|
|
|
|
|
|
buses_to_del = n.buses.index.difference(busmap)
|
|
|
|
n.mremove("Bus", buses_to_del)
|
|
|
|
for c in n.branch_components:
|
|
|
|
df = n.df(c)
|
|
|
|
n.mremove(c, df.index[df.bus0.isin(buses_to_del) | df.bus1.isin(buses_to_del)])
|
|
|
|
|
2020-12-03 18:50:53 +00:00
|
|
|
|
2022-06-24 18:57:53 +00:00
|
|
|
def simplify_links(n, costs, config, output, aggregation_strategies=dict()):
|
2018-01-30 22:11:16 +00:00
|
|
|
## Complex multi-node links are folded into end-points
|
|
|
|
logger.info("Simplifying connected link components")
|
|
|
|
|
2019-02-13 18:03:57 +00:00
|
|
|
if n.links.empty:
|
|
|
|
return n, n.buses.index.to_series()
|
|
|
|
|
2018-01-30 22:11:16 +00:00
|
|
|
# Determine connected link components, ignore all links but DC
|
|
|
|
adjacency_matrix = n.adjacency_matrix(
|
|
|
|
branch_components=["Link"],
|
|
|
|
weights=dict(Link=(n.links.carrier == "DC").astype(float)),
|
|
|
|
)
|
|
|
|
|
|
|
|
_, labels = connected_components(adjacency_matrix, directed=False)
|
|
|
|
labels = pd.Series(labels, n.buses.index)
|
|
|
|
|
|
|
|
G = n.graph()
|
|
|
|
|
|
|
|
def split_links(nodes):
|
|
|
|
nodes = frozenset(nodes)
|
|
|
|
|
|
|
|
seen = set()
|
|
|
|
supernodes = {m for m in nodes if len(G.adj[m]) > 2 or (set(G.adj[m]) - nodes)}
|
|
|
|
|
|
|
|
for u in supernodes:
|
2021-05-25 09:29:47 +00:00
|
|
|
for m, ls in G.adj[u].items():
|
2018-01-30 22:11:16 +00:00
|
|
|
if m not in nodes or m in seen:
|
|
|
|
continue
|
|
|
|
|
|
|
|
buses = [u, m]
|
|
|
|
links = [list(ls)] # [name for name in ls]]
|
|
|
|
|
|
|
|
while m not in (supernodes | seen):
|
|
|
|
seen.add(m)
|
2021-05-25 09:29:47 +00:00
|
|
|
for m2, ls in G.adj[m].items():
|
2018-01-30 22:11:16 +00:00
|
|
|
if m2 in seen or m2 == u:
|
|
|
|
continue
|
|
|
|
buses.append(m2)
|
|
|
|
links.append(list(ls)) # [name for name in ls])
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
# stub
|
|
|
|
break
|
|
|
|
m = m2
|
|
|
|
if m != u:
|
|
|
|
yield pd.Index((u, m)), buses, links
|
|
|
|
seen.add(u)
|
|
|
|
|
|
|
|
busmap = n.buses.index.to_series()
|
2018-12-19 09:30:25 +00:00
|
|
|
|
2022-01-24 18:13:48 +00:00
|
|
|
connection_costs_per_link = _prepare_connection_costs_per_link(n, costs, config)
|
2018-12-19 09:30:25 +00:00
|
|
|
connection_costs_to_bus = pd.DataFrame(
|
|
|
|
0.0, index=n.buses.index, columns=list(connection_costs_per_link)
|
2022-09-16 13:04:04 +00:00
|
|
|
)
|
2018-01-30 22:11:16 +00:00
|
|
|
|
|
|
|
for lbl in labels.value_counts().loc[lambda s: s > 2].index:
|
|
|
|
for b, buses, links in split_links(labels.index[labels == lbl]):
|
|
|
|
if len(buses) <= 2:
|
|
|
|
continue
|
|
|
|
|
|
|
|
logger.debug("nodes = {}".format(labels.index[labels == lbl]))
|
|
|
|
logger.debug("b = {}\nbuses = {}\nlinks = {}".format(b, buses, links))
|
|
|
|
|
|
|
|
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]]
|
2022-01-24 18:13:48 +00:00
|
|
|
connection_costs_to_bus.loc[buses] += _compute_connection_costs_to_bus(
|
|
|
|
n, busmap, costs, config, connection_costs_per_link, buses
|
|
|
|
)
|
2018-10-27 01:41:38 +00:00
|
|
|
|
2018-01-30 22:11:16 +00:00
|
|
|
all_links = [i for _, i in sum(links, [])]
|
|
|
|
|
2022-01-24 18:13:48 +00:00
|
|
|
p_max_pu = config["links"].get("p_max_pu", 1.0)
|
2018-09-03 21:46:57 +00:00
|
|
|
lengths = n.links.loc[all_links, "length"]
|
|
|
|
name = lengths.idxmax() + "+{}".format(len(links) - 1)
|
2018-01-30 22:11:16 +00:00
|
|
|
params = dict(
|
|
|
|
carrier="DC",
|
|
|
|
bus0=b[0],
|
|
|
|
bus1=b[1],
|
|
|
|
length=sum(
|
|
|
|
n.links.loc[[i for _, i in l], "length"].mean() for l in links
|
|
|
|
),
|
|
|
|
p_nom=min(n.links.loc[[i for _, i in l], "p_nom"].sum() for l in links),
|
2018-09-03 21:46:57 +00:00
|
|
|
underwater_fraction=sum(
|
|
|
|
lengths
|
|
|
|
/ lengths.sum()
|
|
|
|
* n.links.loc[all_links, "underwater_fraction"]
|
|
|
|
),
|
2018-09-03 21:46:30 +00:00
|
|
|
p_max_pu=p_max_pu,
|
|
|
|
p_min_pu=-p_max_pu,
|
2018-01-30 22:11:16 +00:00
|
|
|
underground=False,
|
|
|
|
under_construction=False,
|
|
|
|
)
|
|
|
|
|
|
|
|
logger.info(
|
|
|
|
"Joining the links {} connecting the buses {} to simple link {}".format(
|
|
|
|
", ".join(all_links), ", ".join(buses), name
|
|
|
|
)
|
2022-09-16 13:04:04 +00:00
|
|
|
)
|
2018-01-30 22:11:16 +00:00
|
|
|
|
|
|
|
n.mremove("Link", all_links)
|
|
|
|
|
|
|
|
static_attrs = n.components["Link"]["attrs"].loc[lambda df: df.static]
|
2022-12-27 12:16:05 +00:00
|
|
|
for attr, default in static_attrs.default.items():
|
2018-01-30 22:11:16 +00:00
|
|
|
params.setdefault(attr, default)
|
|
|
|
n.links.loc[name] = pd.Series(params)
|
|
|
|
|
|
|
|
# n.add("Link", **params)
|
|
|
|
|
|
|
|
logger.debug("Collecting all components using the busmap")
|
|
|
|
|
2022-09-19 14:34:47 +00:00
|
|
|
exclude_carriers = config["clustering"]["simplify_network"].get(
|
|
|
|
"exclude_carriers", []
|
|
|
|
)
|
2022-09-19 14:34:21 +00:00
|
|
|
|
2022-06-24 18:57:53 +00:00
|
|
|
_aggregate_and_move_components(
|
|
|
|
n,
|
|
|
|
busmap,
|
|
|
|
connection_costs_to_bus,
|
|
|
|
output,
|
|
|
|
aggregation_strategies=aggregation_strategies,
|
2022-09-19 14:34:21 +00:00
|
|
|
exclude_carriers=exclude_carriers,
|
2022-06-24 18:57:53 +00:00
|
|
|
)
|
2018-01-30 22:11:16 +00:00
|
|
|
return n, busmap
|
|
|
|
|
2022-09-16 13:04:04 +00:00
|
|
|
|
2022-06-24 18:57:53 +00:00
|
|
|
def remove_stubs(n, costs, config, output, aggregation_strategies=dict()):
|
2018-01-30 22:11:16 +00:00
|
|
|
logger.info("Removing stubs")
|
|
|
|
|
2023-01-23 21:27:19 +00:00
|
|
|
across_borders = config["clustering"]["simplify_network"].get(
|
|
|
|
"remove_stubs_across_borders", True
|
|
|
|
)
|
|
|
|
matching_attrs = [] if across_borders else ["country"]
|
2022-12-28 08:33:08 +00:00
|
|
|
busmap = busmap_by_stubs(n, matching_attrs)
|
2018-10-27 14:42:58 +00:00
|
|
|
|
2022-01-24 18:13:48 +00:00
|
|
|
connection_costs_to_bus = _compute_connection_costs_to_bus(n, busmap, costs, config)
|
2018-10-27 01:41:38 +00:00
|
|
|
|
2022-09-19 14:34:47 +00:00
|
|
|
exclude_carriers = config["clustering"]["simplify_network"].get(
|
|
|
|
"exclude_carriers", []
|
|
|
|
)
|
2022-09-19 14:34:21 +00:00
|
|
|
|
2022-06-24 18:57:53 +00:00
|
|
|
_aggregate_and_move_components(
|
|
|
|
n,
|
|
|
|
busmap,
|
|
|
|
connection_costs_to_bus,
|
|
|
|
output,
|
|
|
|
aggregation_strategies=aggregation_strategies,
|
2022-09-19 14:34:21 +00:00
|
|
|
exclude_carriers=exclude_carriers,
|
2022-06-24 18:57:53 +00:00
|
|
|
)
|
2018-01-30 22:11:16 +00:00
|
|
|
|
|
|
|
return n, busmap
|
|
|
|
|
2022-09-16 13:04:04 +00:00
|
|
|
|
2022-06-21 16:42:49 +00:00
|
|
|
def aggregate_to_substations(n, aggregation_strategies=dict(), buses_i=None):
|
2021-06-01 08:55:26 +00:00
|
|
|
# can be used to aggregate a selection of buses to electrically closest neighbors
|
|
|
|
# if no buses are given, nodes that are no substations or without offshore connection are aggregated
|
2022-09-05 10:51:30 +00:00
|
|
|
|
2021-06-01 08:55:26 +00:00
|
|
|
if buses_i is None:
|
|
|
|
logger.info(
|
|
|
|
"Aggregating buses that are no substations or have no valid offshore connection"
|
|
|
|
)
|
|
|
|
buses_i = list(set(n.buses.index) - set(n.generators.bus) - set(n.loads.bus))
|
2022-09-16 13:04:04 +00:00
|
|
|
|
2021-08-26 14:00:08 +00:00
|
|
|
weight = pd.concat(
|
2022-09-16 13:04:04 +00:00
|
|
|
{
|
2021-08-26 14:00:08 +00:00
|
|
|
"Line": n.lines.length / n.lines.s_nom.clip(1e-3),
|
|
|
|
"Link": n.links.length / n.links.p_nom.clip(1e-3),
|
2022-09-16 13:04:04 +00:00
|
|
|
}
|
2021-08-26 14:00:08 +00:00
|
|
|
)
|
2021-05-21 11:54:38 +00:00
|
|
|
|
|
|
|
adj = n.adjacency_matrix(branch_components=["Line", "Link"], weights=weight)
|
|
|
|
|
2021-08-26 14:00:08 +00:00
|
|
|
bus_indexer = n.buses.index.get_indexer(buses_i)
|
|
|
|
dist = pd.DataFrame(
|
|
|
|
dijkstra(adj, directed=False, indices=bus_indexer), buses_i, n.buses.index
|
|
|
|
)
|
2021-05-21 11:54:38 +00:00
|
|
|
|
2021-08-26 14:00:08 +00:00
|
|
|
dist[
|
|
|
|
buses_i
|
|
|
|
] = np.inf # bus in buses_i should not be assigned to different bus in buses_i
|
|
|
|
|
|
|
|
for c in n.buses.country.unique():
|
|
|
|
incountry_b = n.buses.country == c
|
|
|
|
dist.loc[incountry_b, ~incountry_b] = np.inf
|
|
|
|
|
|
|
|
busmap = n.buses.index.to_series()
|
|
|
|
busmap.loc[buses_i] = dist.idxmin(1)
|
2021-05-21 11:54:38 +00:00
|
|
|
|
2022-06-27 12:18:47 +00:00
|
|
|
bus_strategies, generator_strategies = get_aggregation_strategies(
|
|
|
|
aggregation_strategies
|
|
|
|
)
|
2022-06-20 16:58:23 +00:00
|
|
|
|
2021-05-21 11:54:38 +00:00
|
|
|
clustering = get_clustering_from_busmap(
|
|
|
|
n,
|
|
|
|
busmap,
|
2022-06-20 16:58:23 +00:00
|
|
|
bus_strategies=bus_strategies,
|
2021-05-21 11:54:38 +00:00
|
|
|
aggregate_generators_weighted=True,
|
|
|
|
aggregate_generators_carriers=None,
|
|
|
|
aggregate_one_ports=["Load", "StorageUnit"],
|
|
|
|
line_length_factor=1.0,
|
2022-06-20 16:58:23 +00:00
|
|
|
generator_strategies=generator_strategies,
|
2021-05-21 11:54:38 +00:00
|
|
|
scale_link_capital_costs=False,
|
|
|
|
)
|
|
|
|
return clustering.network, busmap
|
|
|
|
|
2020-12-03 18:50:53 +00:00
|
|
|
|
2022-06-27 18:45:40 +00:00
|
|
|
def cluster(
|
|
|
|
n, n_clusters, config, algorithm="hac", feature=None, aggregation_strategies=dict()
|
|
|
|
):
|
2020-12-03 14:17:16 +00:00
|
|
|
logger.info(f"Clustering to {n_clusters} buses")
|
2019-01-22 10:33:08 +00:00
|
|
|
|
2022-01-24 18:13:48 +00:00
|
|
|
focus_weights = config.get("focus_weights", None)
|
2022-09-05 10:51:30 +00:00
|
|
|
|
2019-01-22 10:33:08 +00:00
|
|
|
renewable_carriers = pd.Index(
|
2022-09-16 13:04:04 +00:00
|
|
|
[
|
2019-01-22 10:33:08 +00:00
|
|
|
tech
|
|
|
|
for tech in n.generators.carrier.unique()
|
2022-01-24 18:13:48 +00:00
|
|
|
if tech.split("-", 2)[0] in config["renewable"]
|
2022-09-16 13:04:04 +00:00
|
|
|
]
|
2022-01-24 18:13:48 +00:00
|
|
|
)
|
2022-06-20 16:58:23 +00:00
|
|
|
|
|
|
|
clustering = clustering_for_n_clusters(
|
|
|
|
n,
|
|
|
|
n_clusters,
|
|
|
|
custom_busmap=False,
|
|
|
|
aggregation_strategies=aggregation_strategies,
|
2022-01-24 18:13:48 +00:00
|
|
|
solver_name=config["solving"]["solver"]["name"],
|
2021-11-17 12:46:33 +00:00
|
|
|
algorithm=algorithm,
|
|
|
|
feature=feature,
|
2021-05-21 13:27:34 +00:00
|
|
|
focus_weights=focus_weights,
|
|
|
|
)
|
2019-01-22 10:33:08 +00:00
|
|
|
|
|
|
|
return clustering.network, clustering.busmap
|
2018-01-30 22:11:16 +00:00
|
|
|
|
2020-12-03 18:50:53 +00:00
|
|
|
|
2018-01-30 22:11:16 +00:00
|
|
|
if __name__ == "__main__":
|
|
|
|
if "snakemake" not in globals():
|
2019-12-09 20:29:15 +00:00
|
|
|
from _helpers import mock_snakemake
|
2022-09-16 13:04:04 +00:00
|
|
|
|
2022-09-19 14:34:21 +00:00
|
|
|
snakemake = mock_snakemake("simplify_network", simpl="")
|
2019-11-28 07:22:52 +00:00
|
|
|
configure_logging(snakemake)
|
2018-01-30 22:11:16 +00:00
|
|
|
|
2022-01-24 18:13:48 +00:00
|
|
|
n = pypsa.Network(snakemake.input.network)
|
2018-01-30 22:11:16 +00:00
|
|
|
|
2022-06-20 16:58:23 +00:00
|
|
|
aggregation_strategies = snakemake.config["clustering"].get(
|
|
|
|
"aggregation_strategies", {}
|
|
|
|
)
|
2022-06-21 16:42:49 +00:00
|
|
|
# translate str entries of aggregation_strategies to pd.Series functions:
|
|
|
|
aggregation_strategies = {
|
|
|
|
p: {k: getattr(pd.Series, v) for k, v in aggregation_strategies[p].items()}
|
|
|
|
for p in aggregation_strategies.keys()
|
|
|
|
}
|
2022-06-20 16:58:23 +00:00
|
|
|
|
2018-01-30 22:11:16 +00:00
|
|
|
n, trafo_map = simplify_network_to_380(n)
|
|
|
|
|
2021-09-14 14:34:02 +00:00
|
|
|
Nyears = n.snapshot_weightings.objective.sum() / 8760
|
|
|
|
|
2022-01-24 18:13:48 +00:00
|
|
|
technology_costs = load_costs(
|
|
|
|
snakemake.input.tech_costs,
|
|
|
|
snakemake.config["costs"],
|
|
|
|
snakemake.config["electricity"],
|
|
|
|
Nyears,
|
|
|
|
)
|
|
|
|
|
2022-06-24 18:57:53 +00:00
|
|
|
n, simplify_links_map = simplify_links(
|
|
|
|
n, technology_costs, snakemake.config, snakemake.output, aggregation_strategies
|
|
|
|
)
|
2018-01-30 22:11:16 +00:00
|
|
|
|
2022-12-28 08:34:28 +00:00
|
|
|
busmaps = [trafo_map, simplify_links_map]
|
|
|
|
|
|
|
|
cluster_config = snakemake.config["clustering"]["simplify_network"]
|
|
|
|
if cluster_config.get("remove_stubs", True):
|
|
|
|
n, stub_map = remove_stubs(
|
|
|
|
n,
|
|
|
|
technology_costs,
|
|
|
|
snakemake.config,
|
|
|
|
snakemake.output,
|
|
|
|
aggregation_strategies=aggregation_strategies,
|
|
|
|
)
|
|
|
|
busmaps.append(stub_map)
|
2018-01-30 22:11:16 +00:00
|
|
|
|
2022-12-28 08:34:28 +00:00
|
|
|
if cluster_config.get("to_substations", False):
|
2022-06-21 16:42:49 +00:00
|
|
|
n, substation_map = aggregate_to_substations(n, aggregation_strategies)
|
2021-05-21 11:54:38 +00:00
|
|
|
busmaps.append(substation_map)
|
|
|
|
|
2022-03-22 15:53:05 +00:00
|
|
|
# treatment of outliers (nodes without a profile for considered carrier):
|
|
|
|
# all nodes that have no profile of the given carrier are being aggregated to closest neighbor
|
|
|
|
if (
|
|
|
|
snakemake.config.get("clustering", {})
|
|
|
|
.get("cluster_network", {})
|
|
|
|
.get("algorithm", "hac")
|
|
|
|
== "hac"
|
|
|
|
or cluster_config.get("algorithm", "hac") == "hac"
|
|
|
|
):
|
2022-02-04 19:27:18 +00:00
|
|
|
carriers = (
|
|
|
|
cluster_config.get("feature", "solar+onwind-time").split("-")[0].split("+")
|
2022-09-16 13:04:04 +00:00
|
|
|
)
|
2022-03-22 15:53:05 +00:00
|
|
|
for carrier in carriers:
|
|
|
|
buses_i = list(
|
|
|
|
set(n.buses.index) - set(n.generators.query("carrier == @carrier").bus)
|
|
|
|
)
|
|
|
|
logger.info(
|
|
|
|
f"clustering preparaton (hac): aggregating {len(buses_i)} buses of type {carrier}."
|
2022-09-16 13:04:04 +00:00
|
|
|
)
|
2022-06-27 19:02:45 +00:00
|
|
|
n, busmap_hac = aggregate_to_substations(n, aggregation_strategies, buses_i)
|
2022-02-04 19:27:18 +00:00
|
|
|
busmaps.append(busmap_hac)
|
2022-03-22 15:53:05 +00:00
|
|
|
|
2022-01-24 18:13:48 +00:00
|
|
|
if snakemake.wildcards.simpl:
|
2022-02-04 13:35:14 +00:00
|
|
|
n, cluster_map = cluster(
|
|
|
|
n,
|
|
|
|
int(snakemake.wildcards.simpl),
|
|
|
|
snakemake.config,
|
2022-06-27 18:45:40 +00:00
|
|
|
cluster_config.get("algorithm", "hac"),
|
|
|
|
cluster_config.get("feature", None),
|
|
|
|
aggregation_strategies,
|
|
|
|
)
|
2019-01-22 10:33:08 +00:00
|
|
|
busmaps.append(cluster_map)
|
2021-09-07 14:28:01 +00:00
|
|
|
|
|
|
|
# some entries in n.buses are not updated in previous functions, therefore can be wrong. as they are not needed
|
|
|
|
# and are lost when clustering (for example with the simpl wildcard), we remove them for consistency:
|
|
|
|
buses_c = {
|
|
|
|
"symbol",
|
|
|
|
"tags",
|
|
|
|
"under_construction",
|
|
|
|
"substation_lv",
|
|
|
|
"substation_off",
|
|
|
|
}.intersection(n.buses.columns)
|
|
|
|
n.buses = n.buses.drop(buses_c, axis=1)
|
2018-01-30 22:11:16 +00:00
|
|
|
|
2021-07-02 08:04:22 +00:00
|
|
|
update_p_nom_max(n)
|
2022-06-30 06:39:03 +00:00
|
|
|
|
|
|
|
n.meta = dict(snakemake.config, **dict(wildcards=dict(snakemake.wildcards)))
|
2022-01-24 18:13:48 +00:00
|
|
|
n.export_to_netcdf(snakemake.output.network)
|
2018-01-30 22:11:16 +00:00
|
|
|
|
2020-10-02 10:53:56 +00:00
|
|
|
busmap_s = reduce(lambda x, y: x.map(y), busmaps[1:], busmaps[0])
|
2022-01-24 18:13:48 +00:00
|
|
|
busmap_s.to_csv(snakemake.output.busmap)
|
2018-07-10 14:31:57 +00:00
|
|
|
|
2022-01-24 18:13:48 +00:00
|
|
|
cluster_regions(busmaps, snakemake.input, snakemake.output)
|