2018-01-29 21:28:33 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
2024-02-19 15:21:48 +00:00
|
|
|
# SPDX-FileCopyrightText: : 2017-2024 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-29 21:28:33 +00:00
|
|
|
# coding: utf-8
|
2019-08-08 13:02:28 +00:00
|
|
|
"""
|
2019-08-11 20:34:18 +00:00
|
|
|
Creates networks clustered to ``{cluster}`` number of zones with aggregated
|
|
|
|
buses, generators and transmission corridors.
|
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
|
|
|
cluster_network:
|
2022-06-20 16:58:23 +00:00
|
|
|
aggregation_strategies:
|
2023-12-06 09:09:17 +00:00
|
|
|
focus_weights:
|
2019-08-11 11:17:36 +00:00
|
|
|
|
|
|
|
solving:
|
|
|
|
solver:
|
|
|
|
name:
|
|
|
|
|
|
|
|
lines:
|
|
|
|
length_factor:
|
|
|
|
|
2019-12-09 20:29:15 +00:00
|
|
|
.. seealso::
|
2023-04-21 08:41:44 +00:00
|
|
|
Documentation of the configuration file ``config/config.yaml`` at
|
2019-11-07 14:38:25 +00:00
|
|
|
:ref:`toplevel_cf`, :ref:`renewable_cf`, :ref:`solving_cf`, :ref:`lines_cf`
|
2019-08-13 08:03:46 +00:00
|
|
|
|
2019-08-11 09:40:47 +00:00
|
|
|
Inputs
|
|
|
|
------
|
|
|
|
|
2024-03-04 16:48:56 +00:00
|
|
|
- ``resources/regions_onshore_elec_s{simpl}.geojson``: confer :ref:`simplify`
|
|
|
|
- ``resources/regions_offshore_elec_s{simpl}.geojson``: confer :ref:`simplify`
|
|
|
|
- ``networks/elec_s{simpl}.nc``: confer :ref:`simplify`
|
|
|
|
- ``resources/busmap_elec_s{simpl}.csv``: confer :ref:`simplify`
|
|
|
|
- ``data/custom_busmap_elec_s{simpl}_{clusters}.csv``: optional input
|
2019-08-11 20:34:18 +00:00
|
|
|
|
2019-08-11 09:40:47 +00:00
|
|
|
Outputs
|
2019-08-10 12:25:19 +00:00
|
|
|
-------
|
|
|
|
|
2024-03-04 16:48:56 +00:00
|
|
|
- ``resources/regions_onshore_elec_s{simpl}_{clusters}.geojson``:
|
2019-08-11 20:34:18 +00:00
|
|
|
|
2023-03-09 12:28:42 +00:00
|
|
|
.. image:: img/regions_onshore_elec_s_X.png
|
2019-08-12 17:01:53 +00:00
|
|
|
:scale: 33 %
|
2019-08-11 09:40:47 +00:00
|
|
|
|
2024-03-04 16:48:56 +00:00
|
|
|
- ``resources/regions_offshore_elec_s{simpl}_{clusters}.geojson``:
|
2019-08-10 12:25:19 +00:00
|
|
|
|
2023-03-09 12:28:42 +00:00
|
|
|
.. image:: img/regions_offshore_elec_s_X.png
|
2019-08-12 17:01:53 +00:00
|
|
|
:scale: 33 %
|
2019-08-10 12:25:19 +00:00
|
|
|
|
2024-03-04 16:48:56 +00:00
|
|
|
- ``resources/busmap_elec_s{simpl}_{clusters}.csv``: Mapping of buses from ``networks/elec_s{simpl}.nc`` to ``networks/elec_s{simpl}_{clusters}.nc``;
|
|
|
|
- ``resources/linemap_elec_s{simpl}_{clusters}.csv``: Mapping of lines from ``networks/elec_s{simpl}.nc`` to ``networks/elec_s{simpl}_{clusters}.nc``;
|
|
|
|
- ``networks/elec_s{simpl}_{clusters}.nc``:
|
2019-08-10 12:25:19 +00:00
|
|
|
|
2023-03-09 12:28:42 +00:00
|
|
|
.. image:: img/elec_s_X.png
|
2019-08-12 17:01:53 +00:00
|
|
|
:scale: 40 %
|
|
|
|
|
|
|
|
Description
|
|
|
|
-----------
|
2019-08-10 12:25:19 +00:00
|
|
|
|
2019-08-12 17:01:53 +00:00
|
|
|
.. note::
|
2019-08-10 12:25:19 +00:00
|
|
|
|
2019-08-12 17:01:53 +00:00
|
|
|
**Why is clustering used both in** ``simplify_network`` **and** ``cluster_network`` **?**
|
2019-12-09 20:29:15 +00:00
|
|
|
|
2019-08-12 21:48:16 +00:00
|
|
|
Consider for example a network ``networks/elec_s100_50.nc`` in which
|
|
|
|
``simplify_network`` clusters the network to 100 buses and in a second
|
|
|
|
step ``cluster_network``` reduces it down to 50 buses.
|
|
|
|
|
|
|
|
In preliminary tests, it turns out, that the principal effect of
|
|
|
|
changing spatial resolution is actually only partially due to the
|
|
|
|
transmission network. It is more important to differentiate between
|
|
|
|
wind generators with higher capacity factors from those with lower
|
|
|
|
capacity factors, i.e. to have a higher spatial resolution in the
|
|
|
|
renewable generation than in the number of buses.
|
|
|
|
|
|
|
|
The two-step clustering allows to study this effect by looking at
|
|
|
|
networks like ``networks/elec_s100_50m.nc``. Note the additional
|
|
|
|
``m`` in the ``{cluster}`` wildcard. So in the example network
|
|
|
|
there are still up to 100 different wind generators.
|
|
|
|
|
|
|
|
In combination these two features allow you to study the spatial
|
|
|
|
resolution of the transmission network separately from the
|
|
|
|
spatial resolution of renewable generators.
|
2019-08-10 12:25:19 +00:00
|
|
|
|
2019-08-12 17:01:53 +00:00
|
|
|
**Is it possible to run the model without the** ``simplify_network`` **rule?**
|
2019-08-10 12:25:19 +00:00
|
|
|
|
2019-08-12 21:48:16 +00:00
|
|
|
No, the network clustering methods in the PyPSA module
|
2023-06-29 13:37:29 +00:00
|
|
|
`pypsa.clustering.spatial <https://github.com/PyPSA/PyPSA/blob/master/pypsa/clustering/spatial.py>`_
|
2019-08-12 21:48:16 +00:00
|
|
|
do not work reliably with multiple voltage levels and transformers.
|
2019-08-10 12:25:19 +00:00
|
|
|
|
2019-08-11 20:34:18 +00:00
|
|
|
.. tip::
|
2023-03-15 16:00:06 +00:00
|
|
|
The rule :mod:`cluster_networks` runs
|
2019-12-09 20:29:15 +00:00
|
|
|
for all ``scenario`` s in the configuration file
|
2019-08-13 15:52:33 +00:00
|
|
|
the rule :mod:`cluster_network`.
|
2019-08-11 20:34:18 +00:00
|
|
|
|
2020-06-08 18:43:35 +00:00
|
|
|
Exemplary unsolved network clustered to 512 nodes:
|
|
|
|
|
2023-03-09 12:28:42 +00:00
|
|
|
.. image:: img/elec_s_512.png
|
2020-06-08 18:43:35 +00:00
|
|
|
:scale: 40 %
|
|
|
|
:align: center
|
|
|
|
|
|
|
|
Exemplary unsolved network clustered to 256 nodes:
|
|
|
|
|
2023-03-09 12:28:42 +00:00
|
|
|
.. image:: img/elec_s_256.png
|
2020-06-08 18:43:35 +00:00
|
|
|
:scale: 40 %
|
|
|
|
:align: center
|
|
|
|
|
|
|
|
Exemplary unsolved network clustered to 128 nodes:
|
|
|
|
|
2023-03-09 12:28:42 +00:00
|
|
|
.. image:: img/elec_s_128.png
|
2020-06-08 18:43:35 +00:00
|
|
|
:scale: 40 %
|
|
|
|
:align: center
|
|
|
|
|
|
|
|
Exemplary unsolved network clustered to 37 nodes:
|
|
|
|
|
2023-03-09 12:28:42 +00:00
|
|
|
.. image:: img/elec_s_37.png
|
2020-06-08 18:43:35 +00:00
|
|
|
:scale: 40 %
|
2020-09-11 13:26:43 +00:00
|
|
|
:align: center
|
2019-08-08 13:02:28 +00:00
|
|
|
"""
|
2018-01-29 21:28:33 +00:00
|
|
|
|
2018-01-30 22:11:16 +00:00
|
|
|
import logging
|
2024-01-29 11:22:42 +00:00
|
|
|
import os
|
2022-07-18 15:28:09 +00:00
|
|
|
import warnings
|
|
|
|
from functools import reduce
|
2020-09-11 10:40:53 +00:00
|
|
|
|
2018-01-29 21:28:33 +00:00
|
|
|
import geopandas as gpd
|
2024-01-29 11:08:56 +00:00
|
|
|
import linopy
|
2019-09-23 12:32:51 +00:00
|
|
|
import matplotlib.pyplot as plt
|
2018-01-29 21:28:33 +00:00
|
|
|
import numpy as np
|
2020-12-03 18:50:53 +00:00
|
|
|
import pandas as pd
|
|
|
|
import pypsa
|
2019-09-23 12:32:51 +00:00
|
|
|
import seaborn as sns
|
2024-02-10 16:22:36 +00:00
|
|
|
from _helpers import configure_logging, set_scenario_config, update_p_nom_max
|
2024-01-19 11:16:07 +00:00
|
|
|
from add_electricity import load_costs
|
2024-02-09 12:59:15 +00:00
|
|
|
from packaging.version import Version, parse
|
2023-06-29 13:37:29 +00:00
|
|
|
from pypsa.clustering.spatial import (
|
2022-07-27 07:04:39 +00:00
|
|
|
busmap_by_greedy_modularity,
|
|
|
|
busmap_by_hac,
|
|
|
|
busmap_by_kmeans,
|
|
|
|
get_clustering_from_busmap,
|
2022-09-16 13:04:04 +00:00
|
|
|
)
|
2018-01-29 21:28:33 +00:00
|
|
|
|
2024-02-09 12:59:15 +00:00
|
|
|
PD_GE_2_2 = parse(pd.__version__) >= Version("2.2")
|
2019-09-23 14:44:48 +00:00
|
|
|
|
2022-01-14 07:43:21 +00:00
|
|
|
warnings.filterwarnings(action="ignore", category=UserWarning)
|
2020-09-11 10:40:53 +00:00
|
|
|
idx = pd.IndexSlice
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2020-12-03 18:50:53 +00:00
|
|
|
def normed(x):
|
|
|
|
return (x / x.sum()).fillna(0.0)
|
2020-09-11 10:40:53 +00:00
|
|
|
|
2018-01-29 21:28:33 +00:00
|
|
|
|
2018-01-30 22:11:16 +00:00
|
|
|
def weighting_for_country(n, x):
|
2019-02-18 17:36:56 +00:00
|
|
|
conv_carriers = {"OCGT", "CCGT", "PHS", "hydro"}
|
2018-01-29 21:28:33 +00:00
|
|
|
gen = n.generators.loc[n.generators.carrier.isin(conv_carriers)].groupby(
|
|
|
|
"bus"
|
|
|
|
).p_nom.sum().reindex(n.buses.index, fill_value=0.0) + n.storage_units.loc[
|
|
|
|
n.storage_units.carrier.isin(conv_carriers)
|
|
|
|
].groupby(
|
|
|
|
"bus"
|
|
|
|
).p_nom.sum().reindex(
|
|
|
|
n.buses.index, fill_value=0.0
|
|
|
|
)
|
|
|
|
load = n.loads_t.p_set.mean().groupby(n.loads.bus).sum()
|
|
|
|
|
|
|
|
b_i = x.index
|
|
|
|
g = normed(gen.reindex(b_i, fill_value=0))
|
|
|
|
l = normed(load.reindex(b_i, fill_value=0))
|
|
|
|
|
2020-09-11 10:40:53 +00:00
|
|
|
w = g + l
|
2018-08-27 17:14:34 +00:00
|
|
|
return (w * (100.0 / w.max())).clip(lower=1.0).astype(int)
|
2018-01-29 21:28:33 +00:00
|
|
|
|
|
|
|
|
2021-12-10 16:53:40 +00:00
|
|
|
def get_feature_for_hac(n, buses_i=None, feature=None):
|
|
|
|
if buses_i is None:
|
|
|
|
buses_i = n.buses.index
|
2021-11-17 12:46:33 +00:00
|
|
|
|
|
|
|
if feature is None:
|
|
|
|
feature = "solar+onwind-time"
|
|
|
|
|
|
|
|
carriers = feature.split("-")[0].split("+")
|
|
|
|
if "offwind" in carriers:
|
|
|
|
carriers.remove("offwind")
|
|
|
|
carriers = np.append(
|
2023-06-14 11:28:20 +00:00
|
|
|
carriers, n.generators.carrier.filter(like="offwind").unique()
|
2022-09-16 13:04:04 +00:00
|
|
|
)
|
2021-11-17 12:46:33 +00:00
|
|
|
|
|
|
|
if feature.split("-")[1] == "cap":
|
|
|
|
feature_data = pd.DataFrame(index=buses_i, columns=carriers)
|
|
|
|
for carrier in carriers:
|
2022-03-17 16:38:30 +00:00
|
|
|
gen_i = n.generators.query("carrier == @carrier").index
|
|
|
|
attach = (
|
|
|
|
n.generators_t.p_max_pu[gen_i]
|
|
|
|
.mean()
|
|
|
|
.rename(index=n.generators.loc[gen_i].bus)
|
2022-09-16 13:04:04 +00:00
|
|
|
)
|
2022-03-17 16:38:30 +00:00
|
|
|
feature_data[carrier] = attach
|
2021-11-17 12:46:33 +00:00
|
|
|
|
|
|
|
if feature.split("-")[1] == "time":
|
|
|
|
feature_data = pd.DataFrame(columns=buses_i)
|
|
|
|
for carrier in carriers:
|
2022-03-17 16:38:30 +00:00
|
|
|
gen_i = n.generators.query("carrier == @carrier").index
|
|
|
|
attach = n.generators_t.p_max_pu[gen_i].rename(
|
|
|
|
columns=n.generators.loc[gen_i].bus
|
|
|
|
)
|
|
|
|
feature_data = pd.concat([feature_data, attach], axis=0)[buses_i]
|
|
|
|
|
2021-11-17 12:46:33 +00:00
|
|
|
feature_data = feature_data.T
|
2022-03-17 16:38:30 +00:00
|
|
|
# timestamp raises error in sklearn >= v1.2:
|
|
|
|
feature_data.columns = feature_data.columns.astype(str)
|
2021-11-17 12:46:33 +00:00
|
|
|
|
|
|
|
feature_data = feature_data.fillna(0)
|
|
|
|
|
|
|
|
return feature_data
|
|
|
|
|
|
|
|
|
2024-01-29 11:08:56 +00:00
|
|
|
def distribute_clusters(n, n_clusters, focus_weights=None, solver_name="scip"):
|
2020-12-03 18:50:53 +00:00
|
|
|
"""
|
|
|
|
Determine the number of clusters per country.
|
|
|
|
"""
|
2018-02-19 09:03:25 +00:00
|
|
|
L = (
|
|
|
|
n.loads_t.p_set.mean()
|
|
|
|
.groupby(n.loads.bus)
|
|
|
|
.sum()
|
|
|
|
.groupby([n.buses.country, n.buses.sub_network])
|
|
|
|
.sum()
|
|
|
|
.pipe(normed)
|
2022-09-16 13:04:04 +00:00
|
|
|
)
|
2018-02-19 09:03:25 +00:00
|
|
|
|
2019-01-22 10:25:01 +00:00
|
|
|
N = n.buses.groupby(["country", "sub_network"]).size()
|
|
|
|
|
2019-02-13 18:03:57 +00:00
|
|
|
assert (
|
|
|
|
n_clusters >= len(N) and n_clusters <= N.sum()
|
2020-12-03 18:50:53 +00:00
|
|
|
), f"Number of clusters must be {len(N)} <= n_clusters <= {N.sum()} for this selection of countries."
|
2019-02-13 18:03:57 +00:00
|
|
|
|
2023-11-20 18:45:10 +00:00
|
|
|
if isinstance(focus_weights, dict):
|
2019-11-07 14:38:25 +00:00
|
|
|
total_focus = sum(list(focus_weights.values()))
|
|
|
|
|
|
|
|
assert (
|
|
|
|
total_focus <= 1.0
|
|
|
|
), "The sum of focus weights must be less than or equal to 1."
|
2019-12-09 20:29:15 +00:00
|
|
|
|
2019-11-07 14:38:25 +00:00
|
|
|
for country, weight in focus_weights.items():
|
2019-11-07 15:54:09 +00:00
|
|
|
L[country] = weight / len(L[country])
|
2019-11-07 14:38:25 +00:00
|
|
|
|
|
|
|
remainder = [
|
|
|
|
c not in focus_weights.keys() for c in L.index.get_level_values("country")
|
|
|
|
]
|
|
|
|
L[remainder] = L.loc[remainder].pipe(normed) * (1 - total_focus)
|
|
|
|
|
|
|
|
logger.warning("Using custom focus weights for determining number of clusters.")
|
|
|
|
|
2020-12-03 18:50:53 +00:00
|
|
|
assert np.isclose(
|
|
|
|
L.sum(), 1.0, rtol=1e-3
|
|
|
|
), f"Country weights L must sum up to 1.0 when distributing clusters. Is {L.sum()}."
|
2019-11-07 14:38:25 +00:00
|
|
|
|
2024-01-29 11:08:56 +00:00
|
|
|
m = linopy.Model()
|
|
|
|
clusters = m.add_variables(
|
|
|
|
lower=1, upper=N, coords=[L.index], name="n", integer=True
|
2018-02-19 09:03:25 +00:00
|
|
|
)
|
2024-01-29 11:08:56 +00:00
|
|
|
m.add_constraints(clusters.sum() == n_clusters, name="tot")
|
2024-01-30 16:37:44 +00:00
|
|
|
# leave out constant in objective (L * n_clusters) ** 2
|
2024-01-29 11:22:42 +00:00
|
|
|
m.objective = (clusters * clusters - 2 * clusters * L * n_clusters).sum()
|
2024-01-29 11:08:56 +00:00
|
|
|
if solver_name == "gurobi":
|
|
|
|
logging.getLogger("gurobipy").propagate = False
|
2024-02-27 10:39:26 +00:00
|
|
|
elif solver_name not in ["scip", "cplex"]:
|
2024-01-29 11:22:42 +00:00
|
|
|
logger.info(
|
|
|
|
f"The configured solver `{solver_name}` does not support quadratic objectives. Falling back to `scip`."
|
2019-11-08 15:46:29 +00:00
|
|
|
)
|
2024-01-29 11:08:56 +00:00
|
|
|
solver_name = "scip"
|
|
|
|
m.solve(solver_name=solver_name)
|
|
|
|
return m.solution["n"].to_series().astype(int)
|
2018-02-19 09:03:25 +00:00
|
|
|
|
2020-09-11 10:40:53 +00:00
|
|
|
|
2021-11-17 12:46:33 +00:00
|
|
|
def busmap_for_n_clusters(
|
|
|
|
n,
|
|
|
|
n_clusters,
|
|
|
|
solver_name,
|
|
|
|
focus_weights=None,
|
|
|
|
algorithm="kmeans",
|
|
|
|
feature=None,
|
|
|
|
**algorithm_kwds,
|
|
|
|
):
|
2019-02-06 11:06:32 +00:00
|
|
|
if algorithm == "kmeans":
|
|
|
|
algorithm_kwds.setdefault("n_init", 1000)
|
|
|
|
algorithm_kwds.setdefault("max_iter", 30000)
|
|
|
|
algorithm_kwds.setdefault("tol", 1e-6)
|
2022-02-10 14:57:16 +00:00
|
|
|
algorithm_kwds.setdefault("random_state", 0)
|
2019-01-22 10:24:07 +00:00
|
|
|
|
2022-02-04 16:19:23 +00:00
|
|
|
def fix_country_assignment_for_hac(n):
|
2022-02-04 19:27:18 +00:00
|
|
|
from scipy.sparse import csgraph
|
|
|
|
|
2022-02-04 16:19:23 +00:00
|
|
|
# overwrite country of nodes that are disconnected from their country-topology
|
|
|
|
for country in n.buses.country.unique():
|
2022-03-24 12:17:01 +00:00
|
|
|
m = n[n.buses.country == country].copy()
|
2022-02-04 16:19:23 +00:00
|
|
|
|
|
|
|
_, labels = csgraph.connected_components(
|
|
|
|
m.adjacency_matrix(), directed=False
|
|
|
|
)
|
2022-03-17 16:38:30 +00:00
|
|
|
|
2022-02-04 16:19:23 +00:00
|
|
|
component = pd.Series(labels, index=m.buses.index)
|
|
|
|
component_sizes = component.value_counts()
|
|
|
|
|
|
|
|
if len(component_sizes) > 1:
|
2022-03-17 16:38:30 +00:00
|
|
|
disconnected_bus = component[
|
|
|
|
component == component_sizes.index[-1]
|
|
|
|
].index[0]
|
|
|
|
|
|
|
|
neighbor_bus = n.lines.query(
|
|
|
|
"bus0 == @disconnected_bus or bus1 == @disconnected_bus"
|
|
|
|
).iloc[0][["bus0", "bus1"]]
|
2023-10-08 09:20:57 +00:00
|
|
|
new_country = list(set(n.buses.loc[neighbor_bus].country) - {country})[
|
|
|
|
0
|
|
|
|
]
|
2022-02-04 16:19:23 +00:00
|
|
|
|
2022-03-17 16:38:30 +00:00
|
|
|
logger.info(
|
|
|
|
f"overwriting country `{country}` of bus `{disconnected_bus}` "
|
|
|
|
f"to new country `{new_country}`, because it is disconnected "
|
2022-09-16 11:33:38 +00:00
|
|
|
"from its initial inter-country transmission grid."
|
2022-03-17 16:38:30 +00:00
|
|
|
)
|
2022-02-04 16:19:23 +00:00
|
|
|
n.buses.at[disconnected_bus, "country"] = new_country
|
|
|
|
return n
|
|
|
|
|
2021-11-17 12:46:33 +00:00
|
|
|
if algorithm == "hac":
|
|
|
|
feature = get_feature_for_hac(n, buses_i=n.buses.index, feature=feature)
|
2022-02-04 16:19:23 +00:00
|
|
|
n = fix_country_assignment_for_hac(n)
|
2022-02-04 19:27:18 +00:00
|
|
|
|
|
|
|
if (algorithm != "hac") and (feature is not None):
|
|
|
|
logger.warning(
|
|
|
|
f"Keyword argument feature is only valid for algorithm `hac`. "
|
|
|
|
f"Given feature `{feature}` will be ignored."
|
|
|
|
)
|
2021-11-17 12:46:33 +00:00
|
|
|
|
2018-01-30 22:11:16 +00:00
|
|
|
n.determine_network_topology()
|
|
|
|
|
2019-11-07 14:38:25 +00:00
|
|
|
n_clusters = distribute_clusters(
|
|
|
|
n, n_clusters, focus_weights=focus_weights, solver_name=solver_name
|
|
|
|
)
|
2018-02-19 09:03:25 +00:00
|
|
|
|
2018-01-29 21:28:33 +00:00
|
|
|
def busmap_for_country(x):
|
|
|
|
prefix = x.name[0] + x.name[1] + " "
|
2020-09-11 10:40:53 +00:00
|
|
|
logger.debug(f"Determining busmap for country {prefix[:-1]}")
|
2018-01-29 21:28:33 +00:00
|
|
|
if len(x) == 1:
|
|
|
|
return pd.Series(prefix + "0", index=x.index)
|
2018-01-30 22:11:16 +00:00
|
|
|
weight = weighting_for_country(n, x)
|
2019-02-06 11:06:32 +00:00
|
|
|
|
|
|
|
if algorithm == "kmeans":
|
|
|
|
return prefix + busmap_by_kmeans(
|
|
|
|
n, weight, n_clusters[x.name], buses_i=x.index, **algorithm_kwds
|
|
|
|
)
|
2021-11-17 12:46:33 +00:00
|
|
|
elif algorithm == "hac":
|
|
|
|
return prefix + busmap_by_hac(
|
|
|
|
n, n_clusters[x.name], buses_i=x.index, feature=feature.loc[x.index]
|
|
|
|
)
|
2022-07-13 12:51:48 +00:00
|
|
|
elif algorithm == "modularity":
|
|
|
|
return prefix + busmap_by_greedy_modularity(
|
|
|
|
n, n_clusters[x.name], buses_i=x.index
|
|
|
|
)
|
2019-02-06 11:06:32 +00:00
|
|
|
else:
|
2022-07-27 07:02:34 +00:00
|
|
|
raise ValueError(
|
|
|
|
f"`algorithm` must be one of 'kmeans' or 'hac'. Is {algorithm}."
|
2020-09-25 10:34:34 +00:00
|
|
|
)
|
2018-12-31 13:53:11 +00:00
|
|
|
|
2024-02-09 12:59:15 +00:00
|
|
|
compat_kws = dict(include_groups=False) if PD_GE_2_2 else {}
|
|
|
|
|
2022-09-16 13:04:04 +00:00
|
|
|
return (
|
2022-06-27 12:18:47 +00:00
|
|
|
n.buses.groupby(["country", "sub_network"], group_keys=False)
|
2024-02-09 12:59:15 +00:00
|
|
|
.apply(busmap_for_country, **compat_kws)
|
2020-09-25 10:34:34 +00:00
|
|
|
.squeeze()
|
|
|
|
.rename("busmap")
|
2022-09-16 13:04:04 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2020-12-03 14:17:16 +00:00
|
|
|
def clustering_for_n_clusters(
|
2022-09-16 13:04:04 +00:00
|
|
|
n,
|
2020-12-03 18:50:53 +00:00
|
|
|
n_clusters,
|
2020-12-03 14:17:16 +00:00
|
|
|
custom_busmap=False,
|
|
|
|
aggregate_carriers=None,
|
2022-06-20 16:58:23 +00:00
|
|
|
line_length_factor=1.25,
|
2022-06-27 12:18:47 +00:00
|
|
|
aggregation_strategies=dict(),
|
2024-01-29 11:22:42 +00:00
|
|
|
solver_name="scip",
|
2022-02-04 15:45:00 +00:00
|
|
|
algorithm="hac",
|
|
|
|
feature=None,
|
|
|
|
extended_link_costs=0,
|
|
|
|
focus_weights=None,
|
2022-09-16 13:04:04 +00:00
|
|
|
):
|
2022-02-27 07:07:11 +00:00
|
|
|
if not isinstance(custom_busmap, pd.Series):
|
2022-02-04 19:27:18 +00:00
|
|
|
busmap = busmap_for_n_clusters(
|
|
|
|
n, n_clusters, solver_name, focus_weights, algorithm, feature
|
|
|
|
)
|
2021-09-14 14:34:02 +00:00
|
|
|
else:
|
|
|
|
busmap = custom_busmap
|
2020-12-03 14:17:16 +00:00
|
|
|
|
2023-07-17 12:03:16 +00:00
|
|
|
line_strategies = aggregation_strategies.get("lines", dict())
|
|
|
|
generator_strategies = aggregation_strategies.get("generators", dict())
|
|
|
|
one_port_strategies = aggregation_strategies.get("one_ports", dict())
|
|
|
|
|
2018-01-29 21:28:33 +00:00
|
|
|
clustering = get_clustering_from_busmap(
|
2020-12-03 14:17:16 +00:00
|
|
|
n,
|
|
|
|
busmap,
|
2018-01-29 21:28:33 +00:00
|
|
|
aggregate_generators_weighted=True,
|
2018-12-31 13:53:11 +00:00
|
|
|
aggregate_generators_carriers=aggregate_carriers,
|
2018-05-18 15:06:55 +00:00
|
|
|
aggregate_one_ports=["Load", "StorageUnit"],
|
2018-12-31 13:53:11 +00:00
|
|
|
line_length_factor=line_length_factor,
|
2023-07-17 12:03:16 +00:00
|
|
|
line_strategies=line_strategies,
|
2022-06-20 16:58:23 +00:00
|
|
|
generator_strategies=generator_strategies,
|
2023-07-17 12:03:16 +00:00
|
|
|
one_port_strategies=one_port_strategies,
|
2019-09-23 14:44:48 +00:00
|
|
|
scale_link_capital_costs=False,
|
|
|
|
)
|
2018-01-29 21:28:33 +00:00
|
|
|
|
2020-03-29 09:39:18 +00:00
|
|
|
if not n.links.empty:
|
|
|
|
nc = clustering.network
|
|
|
|
nc.links["underwater_fraction"] = (
|
|
|
|
n.links.eval("underwater_fraction * length").div(nc.links.length).dropna()
|
|
|
|
)
|
|
|
|
nc.links["capital_cost"] = nc.links["capital_cost"].add(
|
2023-06-09 15:18:25 +00:00
|
|
|
(nc.links.length - n.links.length)
|
|
|
|
.clip(lower=0)
|
|
|
|
.mul(extended_link_costs)
|
|
|
|
.dropna(),
|
2020-03-29 09:39:18 +00:00
|
|
|
fill_value=0,
|
|
|
|
)
|
|
|
|
|
2018-01-29 21:28:33 +00:00
|
|
|
return clustering
|
|
|
|
|
2020-09-11 10:40:53 +00:00
|
|
|
|
2018-01-30 22:11:16 +00:00
|
|
|
def cluster_regions(busmaps, input=None, output=None):
|
2018-01-29 21:28:33 +00:00
|
|
|
busmap = reduce(lambda x, y: x.map(y), busmaps[1:], busmaps[0])
|
|
|
|
|
|
|
|
for which in ("regions_onshore", "regions_offshore"):
|
2022-06-23 12:25:30 +00:00
|
|
|
regions = gpd.read_file(getattr(input, which))
|
2022-07-18 15:28:09 +00:00
|
|
|
regions = regions.reindex(columns=["name", "geometry"]).set_index("name")
|
|
|
|
regions_c = regions.dissolve(busmap)
|
2018-01-30 22:11:16 +00:00
|
|
|
regions_c.index.name = "name"
|
2022-06-23 12:25:30 +00:00
|
|
|
regions_c = regions_c.reset_index()
|
|
|
|
regions_c.to_file(getattr(output, which))
|
2018-01-29 21:28:33 +00:00
|
|
|
|
2020-09-11 10:40:53 +00:00
|
|
|
|
|
|
|
def plot_busmap_for_n_clusters(n, n_clusters, fn=None):
|
|
|
|
busmap = busmap_for_n_clusters(n, n_clusters)
|
|
|
|
cs = busmap.unique()
|
|
|
|
cr = sns.color_palette("hls", len(cs))
|
|
|
|
n.plot(bus_colors=busmap.map(dict(zip(cs, cr))))
|
|
|
|
if fn is not None:
|
2020-12-03 18:50:53 +00:00
|
|
|
plt.savefig(fn, bbox_inches="tight")
|
2020-09-11 10:40:53 +00:00
|
|
|
del cs, cr
|
|
|
|
|
|
|
|
|
2018-01-29 21:28:33 +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
|
|
|
|
2023-04-29 10:41:37 +00:00
|
|
|
snakemake = mock_snakemake(
|
2024-03-04 16:55:57 +00:00
|
|
|
"cluster_network",
|
|
|
|
simpl="",
|
|
|
|
clusters="5",
|
2023-04-29 10:41:37 +00:00
|
|
|
)
|
2019-11-28 07:22:52 +00:00
|
|
|
configure_logging(snakemake)
|
2023-08-15 13:02:41 +00:00
|
|
|
set_scenario_config(snakemake)
|
2018-01-29 21:28:33 +00:00
|
|
|
|
2023-06-14 11:28:20 +00:00
|
|
|
params = snakemake.params
|
|
|
|
solver_name = snakemake.config["solving"]["solver"]["name"]
|
2019-11-07 14:38:25 +00:00
|
|
|
|
2023-06-14 11:28:20 +00:00
|
|
|
n = pypsa.Network(snakemake.input.network)
|
2018-12-31 13:53:11 +00:00
|
|
|
|
2023-12-12 17:18:03 +00:00
|
|
|
# remove integer outputs for compatibility with PyPSA v0.26.0
|
2023-12-18 08:58:13 +00:00
|
|
|
n.generators.drop("n_mod", axis=1, inplace=True, errors="ignore")
|
2023-12-12 17:18:03 +00:00
|
|
|
|
2023-06-14 11:28:20 +00:00
|
|
|
exclude_carriers = params.cluster_network["exclude_carriers"]
|
2022-09-19 09:46:58 +00:00
|
|
|
aggregate_carriers = set(n.generators.carrier) - set(exclude_carriers)
|
2023-07-14 13:47:41 +00:00
|
|
|
conventional_carriers = set(params.conventional_carriers)
|
2022-01-24 18:48:26 +00:00
|
|
|
if snakemake.wildcards.clusters.endswith("m"):
|
2018-02-19 09:03:25 +00:00
|
|
|
n_clusters = int(snakemake.wildcards.clusters[:-1])
|
2023-07-14 13:47:41 +00:00
|
|
|
aggregate_carriers = params.conventional_carriers & aggregate_carriers
|
|
|
|
elif snakemake.wildcards.clusters.endswith("c"):
|
|
|
|
n_clusters = int(snakemake.wildcards.clusters[:-1])
|
|
|
|
aggregate_carriers = aggregate_carriers - conventional_carriers
|
2022-03-02 13:43:04 +00:00
|
|
|
elif snakemake.wildcards.clusters == "all":
|
|
|
|
n_clusters = len(n.buses)
|
2018-02-19 09:03:25 +00:00
|
|
|
else:
|
|
|
|
n_clusters = int(snakemake.wildcards.clusters)
|
|
|
|
|
2023-08-04 14:20:39 +00:00
|
|
|
if params.cluster_network.get("consider_efficiency_classes", False):
|
2023-07-25 10:11:57 +00:00
|
|
|
carriers = []
|
|
|
|
for c in aggregate_carriers:
|
|
|
|
gens = n.generators.query("carrier == @c")
|
|
|
|
low = gens.efficiency.quantile(0.10)
|
|
|
|
high = gens.efficiency.quantile(0.90)
|
|
|
|
if low >= high:
|
|
|
|
carriers += [c]
|
|
|
|
else:
|
|
|
|
labels = ["low", "medium", "high"]
|
|
|
|
suffix = pd.cut(
|
|
|
|
gens.efficiency, bins=[0, low, high, 1], labels=labels
|
|
|
|
).astype(str)
|
|
|
|
carriers += [f"{c} {label} efficiency" for label in labels]
|
2024-01-31 16:10:08 +00:00
|
|
|
n.generators.update(
|
|
|
|
{"carrier": gens.carrier + " " + suffix + " efficiency"}
|
|
|
|
)
|
2023-07-25 10:11:57 +00:00
|
|
|
aggregate_carriers = carriers
|
|
|
|
|
2018-03-13 09:59:44 +00:00
|
|
|
if n_clusters == len(n.buses):
|
|
|
|
# Fast-path if no clustering is necessary
|
2018-03-13 10:50:06 +00:00
|
|
|
busmap = n.buses.index.to_series()
|
|
|
|
linemap = n.lines.index.to_series()
|
2023-06-29 13:37:29 +00:00
|
|
|
clustering = pypsa.clustering.spatial.Clustering(
|
2018-03-13 10:50:06 +00:00
|
|
|
n, busmap, linemap, linemap, pd.Series(dtype="O")
|
2022-09-16 13:04:04 +00:00
|
|
|
)
|
2018-03-13 09:59:44 +00:00
|
|
|
else:
|
2021-08-06 13:43:12 +00:00
|
|
|
Nyears = n.snapshot_weightings.objective.sum() / 8760
|
2022-01-24 18:48:26 +00:00
|
|
|
|
|
|
|
hvac_overhead_cost = load_costs(
|
|
|
|
snakemake.input.tech_costs,
|
2023-06-14 11:28:20 +00:00
|
|
|
params.costs,
|
|
|
|
params.max_hours,
|
2022-01-24 18:48:26 +00:00
|
|
|
Nyears,
|
2019-10-31 09:37:49 +00:00
|
|
|
).at["HVAC overhead", "capital_cost"]
|
2018-12-31 13:53:11 +00:00
|
|
|
|
2023-06-14 11:28:20 +00:00
|
|
|
custom_busmap = params.custom_busmap
|
2021-09-14 14:34:02 +00:00
|
|
|
if custom_busmap:
|
2022-01-24 18:48:26 +00:00
|
|
|
custom_busmap = pd.read_csv(
|
|
|
|
snakemake.input.custom_busmap, index_col=0, squeeze=True
|
|
|
|
)
|
2021-09-14 14:34:02 +00:00
|
|
|
custom_busmap.index = custom_busmap.index.astype(str)
|
2022-01-24 18:48:26 +00:00
|
|
|
logger.info(f"Imported custom busmap from {snakemake.input.custom_busmap}")
|
2022-01-14 10:30:15 +00:00
|
|
|
|
2020-12-03 14:17:16 +00:00
|
|
|
clustering = clustering_for_n_clusters(
|
|
|
|
n,
|
|
|
|
n_clusters,
|
|
|
|
custom_busmap,
|
|
|
|
aggregate_carriers,
|
2023-06-14 11:28:20 +00:00
|
|
|
params.length_factor,
|
|
|
|
params.aggregation_strategies,
|
|
|
|
solver_name,
|
|
|
|
params.cluster_network["algorithm"],
|
|
|
|
params.cluster_network["feature"],
|
2022-02-04 15:45:00 +00:00
|
|
|
hvac_overhead_cost,
|
2023-06-14 11:28:20 +00:00
|
|
|
params.focus_weights,
|
2022-02-04 15:45:00 +00:00
|
|
|
)
|
2018-01-29 21:28:33 +00:00
|
|
|
|
2022-06-20 16:58:23 +00:00
|
|
|
update_p_nom_max(clustering.network)
|
2018-01-29 21:28:33 +00:00
|
|
|
|
2023-07-25 10:11:57 +00:00
|
|
|
if params.cluster_network.get("consider_efficiency_classes"):
|
|
|
|
labels = [f" {label} efficiency" for label in ["low", "medium", "high"]]
|
|
|
|
nc = clustering.network
|
|
|
|
nc.generators["carrier"] = nc.generators.carrier.replace(labels, "", regex=True)
|
|
|
|
|
2022-06-30 06:39:03 +00:00
|
|
|
clustering.network.meta = dict(
|
|
|
|
snakemake.config, **dict(wildcards=dict(snakemake.wildcards))
|
2022-09-16 13:04:04 +00:00
|
|
|
)
|
2018-03-13 10:50:06 +00:00
|
|
|
clustering.network.export_to_netcdf(snakemake.output.network)
|
2020-10-02 10:53:56 +00:00
|
|
|
for attr in (
|
|
|
|
"busmap",
|
|
|
|
"linemap",
|
|
|
|
): # also available: linemap_positive, linemap_negative
|
|
|
|
getattr(clustering, attr).to_csv(snakemake.output[attr])
|
2018-03-13 10:50:06 +00:00
|
|
|
|
2022-01-24 18:48:26 +00:00
|
|
|
cluster_regions((clustering.busmap,), snakemake.input, snakemake.output)
|