diff --git a/config.default.yaml b/config.default.yaml index d8f3697a..27ca63d0 100755 --- a/config.default.yaml +++ b/config.default.yaml @@ -19,6 +19,10 @@ scenario: countries: ['AL', 'AT', 'BA', 'BE', 'BG', 'CH', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI', 'FR', 'GB', 'GR', 'HR', 'HU', 'IE', 'IT', 'LT', 'LU', 'LV', 'ME', 'MK', 'NL', 'NO', 'PL', 'PT', 'RO', 'RS', 'SE', 'SI', 'SK'] +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-01-01" end: "2014-01-01" diff --git a/config.tutorial.yaml b/config.tutorial.yaml index 1dfde199..e551e460 100755 --- a/config.tutorial.yaml +++ b/config.tutorial.yaml @@ -19,6 +19,10 @@ scenario: 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-04-01" diff --git a/doc/configtables/clustering.csv b/doc/configtables/clustering.csv new file mode 100644 index 00000000..2f63f955 --- /dev/null +++ b/doc/configtables/clustering.csv @@ -0,0 +1,3 @@ +,Unit,Values,Description +simplify,,, +-- to_substations,bool,"{'true','false'}","Aggregates all nodes without power injection (positive or negative, i.e. demand or generation) to electrically closest ones" diff --git a/doc/release_notes.rst b/doc/release_notes.rst index af9a58f6..db46bea0 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -19,6 +19,7 @@ Upcoming Release * Fix: Value for ``co2base`` in ``config.yaml`` adjusted to 1.487e9 t CO2-eq (from 3.1e9 t CO2-eq). The new value represents emissions related to the electricity sector for EU+UK. The old value was ~2x too high and used when the emissions wildcard in ``{opts}`` was used. * Add option to include marginal costs of links representing fuel cells, electrolysis, and battery inverters [`#232 `_]. +* Add option to pre-aggregate nodes without power injections (positive or negative, i.e. generation or demand) to electrically closest nodes or neighbors in ``simplify_network``. Defaults to ``False``. This affects nodes that are no substations or have no offshore connection. * Fix: Add escape in :mod:`base_network` if all TYNDP links are already contained in the network [`#246 `_]. * Bugfix in :mod:`solve_operations_network`: optimised capacities are now fixed for all extendable links, not only HVDC links [`#244 `_]. * The ``focus_weights`` are now also considered when pre-clustering in the :mod:`simplify_network` rule [`#241 `_]. diff --git a/doc/tutorial.rst b/doc/tutorial.rst index 507b1485..d2fb8433 100644 --- a/doc/tutorial.rst +++ b/doc/tutorial.rst @@ -53,41 +53,41 @@ Likewise, the example's temporal scope can be restricted (e.g. to a single month .. literalinclude:: ../config.tutorial.yaml :language: yaml - :lines: 22-25 + :lines: 24-27 It is also possible to allow less or more carbon-dioxide emissions. Here, we limit the emissions of Germany 100 Megatonnes per year. .. literalinclude:: ../config.tutorial.yaml :language: yaml - :lines: 36,38 + :lines: 38,40 PyPSA-Eur also includes a database of existing conventional powerplants. We can select which types of powerplants we like to be included with fixed capacities: .. literalinclude:: ../config.tutorial.yaml :language: yaml - :lines: 36,52 + :lines: 38,54 To accurately model the temporal and spatial availability of renewables such as wind and solar energy, we rely on historical weather data. It is advisable to adapt the required range of coordinates to the selection of countries. .. literalinclude:: ../config.tutorial.yaml :language: yaml - :lines: 54-62 + :lines: 56-63 We can also decide which weather data source should be used to calculate potentials and capacity factor time-series for each carrier. For example, we may want to use the ERA-5 dataset for solar and not the default SARAH-2 dataset. .. literalinclude:: ../config.tutorial.yaml :language: yaml - :lines: 64,107-108 + :lines: 65,108-109 Finally, it is possible to pick a solver. For instance, this tutorial uses the open-source solvers CBC and Ipopt and does not rely on the commercial solvers Gurobi or CPLEX (for which free academic licenses are available). .. literalinclude:: ../config.tutorial.yaml :language: yaml - :lines: 170,180-181 + :lines: 171,181-182 .. note:: diff --git a/scripts/simplify_network.py b/scripts/simplify_network.py index f37899b9..6e12e5e8 100644 --- a/scripts/simplify_network.py +++ b/scripts/simplify_network.py @@ -97,7 +97,7 @@ from functools import reduce import pypsa from pypsa.io import import_components_from_dataframe, import_series_from_dataframe -from pypsa.networkclustering import busmap_by_stubs, aggregategenerators, aggregateoneport +from pypsa.networkclustering import busmap_by_stubs, aggregategenerators, aggregateoneport, get_clustering_from_busmap, _make_consense logger = logging.getLogger(__name__) @@ -312,7 +312,6 @@ def simplify_links(n): _aggregate_and_move_components(n, busmap, connection_costs_to_bus) return n, busmap - def remove_stubs(n): logger.info("Removing stubs") @@ -324,6 +323,42 @@ def remove_stubs(n): return n, busmap +def aggregate_to_substations(n, buses_i=None): + # 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 + + 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)) + + weight = pd.concat({'Line': n.lines.length/n.lines.s_nom.clip(1e-3), + 'Link': n.links.length/n.links.p_nom.clip(1e-3)}) + + adj = n.adjacency_matrix(branch_components=['Line', 'Link'], weights=weight) + + bus_indexer = n.buses.index.get_indexer(buses_i) + dist = pd.DataFrame(dijkstra(adj, directed=False, indices=bus_indexer), buses_i, n.buses.index) + + 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) + + clustering = get_clustering_from_busmap(n, busmap, + bus_strategies=dict(country=_make_consense("Bus", "country")), + aggregate_generators_weighted=True, + aggregate_generators_carriers=None, + aggregate_one_ports=["Load", "StorageUnit"], + line_length_factor=1.0, + generator_strategies={'p_nom_max': 'sum'}, + scale_link_capital_costs=False) + + return clustering.network, busmap + def cluster(n, n_clusters): logger.info(f"Clustering to {n_clusters} buses") @@ -365,6 +400,10 @@ if __name__ == "__main__": busmaps = [trafo_map, simplify_links_map, stub_map] + if snakemake.config.get('clustering', {}).get('simplify', {}).get('to_substations', False): + n, substation_map = aggregate_to_substations(n) + busmaps.append(substation_map) + if snakemake.wildcards.simpl: n, cluster_map = cluster(n, int(snakemake.wildcards.simpl)) busmaps.append(cluster_map) diff --git a/test/config.test1.yaml b/test/config.test1.yaml index 3ed02082..83ce38ad 100755 --- a/test/config.test1.yaml +++ b/test/config.test1.yaml @@ -18,6 +18,10 @@ scenario: 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-08"