2018-01-29 21:28:33 +00:00
|
|
|
# coding: utf-8
|
|
|
|
|
|
|
|
import pandas as pd
|
|
|
|
idx = pd.IndexSlice
|
|
|
|
|
2018-01-30 22:11:16 +00:00
|
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
2018-01-29 21:28:33 +00:00
|
|
|
import os
|
|
|
|
import numpy as np
|
|
|
|
import scipy as sp
|
2018-01-30 22:11:16 +00:00
|
|
|
from scipy.sparse.csgraph import connected_components
|
2018-01-29 21:28:33 +00:00
|
|
|
import xarray as xr
|
|
|
|
import geopandas as gpd
|
|
|
|
import shapely
|
2018-01-30 22:11:16 +00:00
|
|
|
import networkx as nx
|
2018-03-13 09:59:44 +00:00
|
|
|
from shutil import copyfile
|
2018-01-29 21:28:33 +00:00
|
|
|
|
2018-01-30 22:11:16 +00:00
|
|
|
from six import iteritems
|
2018-01-29 21:28:33 +00:00
|
|
|
from six.moves import reduce
|
|
|
|
|
2018-02-19 09:03:25 +00:00
|
|
|
import pyomo.environ as po
|
|
|
|
|
2018-01-29 21:28:33 +00:00
|
|
|
import pypsa
|
2018-01-30 22:11:16 +00:00
|
|
|
from pypsa.io import import_components_from_dataframe, import_series_from_dataframe
|
2018-01-29 21:28:33 +00:00
|
|
|
from pypsa.networkclustering import (busmap_by_stubs, busmap_by_kmeans,
|
2018-01-30 22:11:16 +00:00
|
|
|
_make_consense, get_clustering_from_busmap,
|
|
|
|
aggregategenerators, aggregateoneport)
|
2018-01-29 21:28:33 +00:00
|
|
|
def normed(x):
|
|
|
|
return (x/x.sum()).fillna(0.)
|
|
|
|
|
2018-01-30 22:11:16 +00:00
|
|
|
def weighting_for_country(n, x):
|
2018-01-29 21:28:33 +00:00
|
|
|
conv_carriers = {'OCGT', 'PHS', 'hydro'}
|
|
|
|
gen = (n
|
|
|
|
.generators.loc[n.generators.carrier.isin(conv_carriers)]
|
|
|
|
.groupby('bus').p_nom.sum()
|
|
|
|
.reindex(n.buses.index, fill_value=0.) +
|
|
|
|
n
|
|
|
|
.storage_units.loc[n.storage_units.carrier.isin(conv_carriers)]
|
|
|
|
.groupby('bus').p_nom.sum()
|
|
|
|
.reindex(n.buses.index, fill_value=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))
|
|
|
|
|
|
|
|
w= g + l
|
|
|
|
return (w * (100. / w.max())).astype(int)
|
|
|
|
|
|
|
|
|
|
|
|
## Plot weighting for Germany
|
|
|
|
|
|
|
|
def plot_weighting(n, country):
|
|
|
|
n.plot(bus_sizes=(2*weighting_for_country(n.buses.loc[n.buses.country == country])).reindex(n.buses.index, fill_value=1))
|
|
|
|
p = vshapes.countries()['DE']
|
|
|
|
plt.xlim(p.bounds[0], p.bounds[2])
|
|
|
|
plt.ylim(p.bounds[1], p.bounds[3])
|
|
|
|
|
|
|
|
|
|
|
|
# # Determining the number of clusters per country
|
|
|
|
|
2018-01-30 22:11:16 +00:00
|
|
|
def distribute_clusters(n, n_clusters):
|
2018-01-29 21:28:33 +00:00
|
|
|
load = n.loads_t.p_set.mean().groupby(n.loads.bus).sum()
|
|
|
|
loadc = load.groupby([n.buses.country, n.buses.sub_network]).sum()
|
|
|
|
n_cluster_per_country = n_clusters * normed(loadc)
|
|
|
|
one_cluster_b = n_cluster_per_country < 0.5
|
|
|
|
n_one_cluster, n_one_cluster_prev = one_cluster_b.sum(), 0
|
|
|
|
|
|
|
|
while n_one_cluster > n_one_cluster_prev:
|
|
|
|
n_clusters_rem = n_clusters - one_cluster_b.sum()
|
|
|
|
assert n_clusters_rem > 0
|
|
|
|
n_cluster_per_country[~one_cluster_b] = n_clusters_rem * normed(loadc[~one_cluster_b])
|
|
|
|
one_cluster_b = n_cluster_per_country < 0.5
|
|
|
|
n_one_cluster, n_one_cluster_prev = one_cluster_b.sum(), n_one_cluster
|
|
|
|
|
|
|
|
n_cluster_per_country[one_cluster_b] = 1.1
|
|
|
|
n_cluster_per_country[~one_cluster_b] = n_cluster_per_country[~one_cluster_b] + 0.5
|
|
|
|
|
|
|
|
return n_cluster_per_country.astype(int)
|
|
|
|
|
2018-01-30 22:11:16 +00:00
|
|
|
def distribute_clusters_exactly(n, n_clusters):
|
2018-01-29 21:28:33 +00:00
|
|
|
for d in [0, 1, -1, 2, -2]:
|
2018-01-30 22:11:16 +00:00
|
|
|
n_cluster_per_country = distribute_clusters(n, n_clusters + d)
|
2018-01-29 21:28:33 +00:00
|
|
|
if n_cluster_per_country.sum() == n_clusters:
|
|
|
|
return n_cluster_per_country
|
|
|
|
else:
|
2018-01-30 22:11:16 +00:00
|
|
|
return distribute_clusters(n, n_clusters)
|
2018-01-29 21:28:33 +00:00
|
|
|
|
2018-02-19 09:03:25 +00:00
|
|
|
def distribute_clusters_optim(n, n_clusters, solver_name='gurobi'):
|
|
|
|
L = (n.loads_t.p_set.mean()
|
|
|
|
.groupby(n.loads.bus).sum()
|
|
|
|
.groupby([n.buses.country, n.buses.sub_network]).sum()
|
|
|
|
.pipe(normed))
|
|
|
|
|
|
|
|
m = po.ConcreteModel()
|
|
|
|
m.n = po.Var(list(L.index), bounds=(1, None), domain=po.Integers)
|
|
|
|
m.tot = po.Constraint(expr=(po.summation(m.n) == n_clusters))
|
|
|
|
m.objective = po.Objective(expr=po.sum((m.n[i] - L.loc[i]*n_clusters)**2
|
|
|
|
for i in L.index),
|
|
|
|
sense=po.minimize)
|
|
|
|
|
|
|
|
opt = po.SolverFactory(solver_name)
|
|
|
|
if isinstance(opt, pypsa.opf.PersistentSolver):
|
|
|
|
opt.set_instance(m)
|
|
|
|
results = opt.solve(m)
|
|
|
|
assert results['Solver'][0]['Status'].key == 'ok', "Solver returned non-optimally: {}".format(results)
|
|
|
|
|
|
|
|
return pd.Series(m.n.get_values(), index=L.index).astype(int)
|
|
|
|
|
2018-01-30 22:11:16 +00:00
|
|
|
def busmap_for_n_clusters(n, n_clusters):
|
|
|
|
n.determine_network_topology()
|
|
|
|
|
2018-02-19 09:03:25 +00:00
|
|
|
if 'snakemake' in globals():
|
|
|
|
solver_name = snakemake.config['solving']['solver']['name']
|
|
|
|
else:
|
|
|
|
solver_name = "gurobi"
|
|
|
|
|
|
|
|
n_clusters = distribute_clusters_optim(n, n_clusters, solver_name=solver_name)
|
|
|
|
|
2018-01-29 21:28:33 +00:00
|
|
|
def busmap_for_country(x):
|
|
|
|
prefix = x.name[0] + x.name[1] + ' '
|
|
|
|
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)
|
2018-01-29 21:28:33 +00:00
|
|
|
return prefix + busmap_by_kmeans(n, weight, n_clusters[x.name], buses_i=x.index)
|
|
|
|
return n.buses.groupby(['country', 'sub_network'], group_keys=False).apply(busmap_for_country)
|
|
|
|
|
2018-01-30 22:11:16 +00:00
|
|
|
def plot_busmap_for_n_clusters(n, n_clusters=50):
|
|
|
|
busmap = busmap_for_n_clusters(n, n_clusters)
|
2018-01-29 21:28:33 +00:00
|
|
|
cs = busmap.unique()
|
|
|
|
cr = sns.color_palette("hls", len(cs))
|
|
|
|
n.plot(bus_colors=busmap.map(dict(zip(cs, cr))))
|
|
|
|
del cs, cr
|
|
|
|
|
2018-05-18 15:06:55 +00:00
|
|
|
def clustering_for_n_clusters(n, n_clusters, aggregate_renewables=True, line_length_factor=1.25):
|
2018-02-19 09:03:25 +00:00
|
|
|
aggregate_generators_carriers = (None if aggregate_renewables
|
|
|
|
else (pd.Index(n.generators.carrier.unique())
|
|
|
|
.difference(['onwind', 'offwind', 'solar'])))
|
2018-01-29 21:28:33 +00:00
|
|
|
clustering = get_clustering_from_busmap(
|
2018-01-30 22:11:16 +00:00
|
|
|
n, busmap_for_n_clusters(n, n_clusters),
|
2018-01-29 21:28:33 +00:00
|
|
|
bus_strategies=dict(country=_make_consense("Bus", "country")),
|
|
|
|
aggregate_generators_weighted=True,
|
2018-02-19 09:03:25 +00:00
|
|
|
aggregate_generators_carriers=aggregate_generators_carriers,
|
2018-05-18 15:06:55 +00:00
|
|
|
aggregate_one_ports=["Load", "StorageUnit"],
|
|
|
|
line_length_factor=line_length_factor
|
2018-01-29 21:28:33 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
return clustering
|
|
|
|
|
|
|
|
def save_to_geojson(s, fn):
|
|
|
|
if os.path.exists(fn):
|
|
|
|
os.unlink(fn)
|
|
|
|
s.reset_index().to_file(fn, driver='GeoJSON')
|
|
|
|
|
2018-01-30 22:11:16 +00:00
|
|
|
def cluster_regions(busmaps, input=None, output=None):
|
|
|
|
if input is None: input = snakemake.input
|
|
|
|
if output is None: output = snakemake.output
|
|
|
|
|
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'):
|
2018-01-30 22:11:16 +00:00
|
|
|
regions = gpd.read_file(getattr(input, which)).set_index('name')
|
|
|
|
geom_c = regions.geometry.groupby(busmap).apply(shapely.ops.cascaded_union)
|
2018-01-29 21:28:33 +00:00
|
|
|
regions_c = gpd.GeoDataFrame(dict(geometry=geom_c))
|
2018-01-30 22:11:16 +00:00
|
|
|
regions_c.index.name = 'name'
|
|
|
|
save_to_geojson(regions_c, getattr(output, which))
|
2018-01-29 21:28:33 +00:00
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
# Detect running outside of snakemake and mock snakemake for testing
|
|
|
|
if 'snakemake' not in globals():
|
2018-01-30 22:11:16 +00:00
|
|
|
from vresutils.snakemake import MockSnakemake, Dict
|
|
|
|
snakemake = MockSnakemake(
|
|
|
|
wildcards=Dict(network='elec', simpl='', clusters='45'),
|
|
|
|
input=Dict(
|
|
|
|
network='networks/{network}_s{simpl}.nc',
|
|
|
|
regions_onshore='resources/regions_onshore_{network}_s{simpl}.geojson',
|
|
|
|
regions_offshore='resources/regions_offshore_{network}_s{simpl}.geojson'
|
|
|
|
),
|
|
|
|
output=Dict(
|
|
|
|
network='networks/{network}_s{simpl}_{clusters}.nc',
|
|
|
|
regions_onshore='resources/regions_onshore_{network}_s{simpl}_{clusters}.geojson',
|
|
|
|
regions_offshore='resources/regions_offshore_{network}_s{simpl}_{clusters}.geojson'
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2018-02-10 16:16:20 +00:00
|
|
|
logging.basicConfig(level=snakemake.config['logging_level'])
|
2018-01-29 21:28:33 +00:00
|
|
|
|
|
|
|
n = pypsa.Network(snakemake.input.network)
|
|
|
|
|
2018-02-19 09:03:25 +00:00
|
|
|
if snakemake.wildcards.clusters.endswith('m'):
|
|
|
|
n_clusters = int(snakemake.wildcards.clusters[:-1])
|
|
|
|
aggregate_renewables = False
|
|
|
|
else:
|
|
|
|
n_clusters = int(snakemake.wildcards.clusters)
|
|
|
|
aggregate_renewables = True
|
|
|
|
|
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()
|
|
|
|
clustering = pypsa.networkclustering.Clustering(n, busmap, linemap, linemap, pd.Series(dtype='O'))
|
2018-03-13 09:59:44 +00:00
|
|
|
else:
|
2018-05-18 15:06:55 +00:00
|
|
|
line_length_factor = snakemake.config['lines']['length_factor']
|
|
|
|
clustering = clustering_for_n_clusters(n, n_clusters, aggregate_renewables, line_length_factor=line_length_factor)
|
2018-01-29 21:28:33 +00:00
|
|
|
|
2018-03-13 10:50:06 +00:00
|
|
|
clustering.network.export_to_netcdf(snakemake.output.network)
|
|
|
|
with pd.HDFStore(snakemake.output.clustermaps, model='w') as store:
|
|
|
|
for attr in ('busmap', 'linemap', 'linemap_positive', 'linemap_negative'):
|
2018-03-14 12:19:44 +00:00
|
|
|
store.put(attr, getattr(clustering, attr), format="table", index=False)
|
2018-03-13 10:50:06 +00:00
|
|
|
|
|
|
|
cluster_regions((clustering.busmap,))
|
|
|
|
|
2018-01-29 21:28:33 +00:00
|
|
|
|