2020-05-29 07:50:55 +00:00
|
|
|
# SPDX-FileCopyrightText: : 2017-2020 The PyPSA-Eur Authors
|
|
|
|
#
|
|
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
2018-01-29 21:28:33 +00:00
|
|
|
# coding: utf-8
|
2019-08-08 13:02:28 +00:00
|
|
|
"""
|
2019-08-14 09:08:10 +00:00
|
|
|
Prepare PyPSA network for solving according to :ref:`opts` and :ref:`ll`, such as
|
|
|
|
|
|
|
|
- adding an annual **limit** of carbon-dioxide emissions,
|
2020-08-25 10:12:00 +00:00
|
|
|
- adding an exogenous **price** per tonne emissions of carbon-dioxide (or other kinds),
|
2019-08-14 09:08:10 +00:00
|
|
|
- setting an **N-1 security margin** factor for transmission line capacities,
|
2020-08-28 15:59:51 +00:00
|
|
|
- specifying an expansion limit on the **cost** of transmission expansion,
|
|
|
|
- specifying an expansion limit on the **volume** of transmission expansion, and
|
2020-12-03 15:02:21 +00:00
|
|
|
- reducing the **temporal** resolution by averaging over multiple hours
|
|
|
|
or segmenting time series into chunks of varying lengths using ``tsam``.
|
2019-08-11 09:40:47 +00:00
|
|
|
|
|
|
|
Relevant Settings
|
|
|
|
-----------------
|
|
|
|
|
2019-08-11 11:17:36 +00:00
|
|
|
.. code:: yaml
|
|
|
|
|
|
|
|
costs:
|
|
|
|
emission_prices:
|
|
|
|
USD2013_to_EUR2013:
|
|
|
|
discountrate:
|
|
|
|
marginal_cost:
|
|
|
|
capital_cost:
|
|
|
|
|
|
|
|
electricity:
|
|
|
|
co2limit:
|
|
|
|
max_hours:
|
|
|
|
|
2019-11-14 15:21:00 +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`
|
|
|
|
|
2019-08-11 09:40:47 +00:00
|
|
|
Inputs
|
|
|
|
------
|
|
|
|
|
2019-08-12 17:01:53 +00:00
|
|
|
- ``data/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.
|
2020-12-03 18:50:53 +00:00
|
|
|
- ``networks/elec_s{simpl}_{clusters}.nc``: confer :ref:`cluster`
|
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
|
|
|
- ``networks/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc``: Complete PyPSA network that will be handed to the ``solve_network`` rule.
|
2019-08-11 20:34:18 +00:00
|
|
|
|
2019-08-11 09:40:47 +00:00
|
|
|
Description
|
|
|
|
-----------
|
|
|
|
|
2019-08-11 20:34:18 +00:00
|
|
|
.. tip::
|
2019-08-13 15:52:33 +00:00
|
|
|
The rule :mod:`prepare_all_networks` runs
|
2019-08-11 20:34:18 +00:00
|
|
|
for all ``scenario`` s in the configuration file
|
2019-08-13 15:52:33 +00:00
|
|
|
the rule :mod:`prepare_network`.
|
2019-08-11 20:34:18 +00:00
|
|
|
|
2019-08-08 13:02:28 +00:00
|
|
|
"""
|
2018-01-29 21:28:33 +00:00
|
|
|
|
2019-11-28 07:22:52 +00:00
|
|
|
import logging
|
|
|
|
from _helpers import configure_logging
|
|
|
|
|
2018-01-30 22:12:36 +00:00
|
|
|
import re
|
2018-01-29 21:28:33 +00:00
|
|
|
import pypsa
|
2020-12-03 18:50:53 +00:00
|
|
|
import numpy as np
|
2019-11-14 15:21:00 +00:00
|
|
|
import pandas as pd
|
2020-12-03 18:50:53 +00:00
|
|
|
from six import iteritems
|
|
|
|
|
|
|
|
from add_electricity import load_costs, update_transmission_costs
|
2019-11-14 15:21:00 +00:00
|
|
|
|
|
|
|
idx = pd.IndexSlice
|
2018-01-29 21:28:33 +00:00
|
|
|
|
2020-12-03 18:50:53 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2019-06-18 09:50:54 +00:00
|
|
|
def add_co2limit(n, Nyears=1., factor=None):
|
|
|
|
|
2019-11-05 10:21:33 +00:00
|
|
|
if factor is not None:
|
2019-06-18 09:50:54 +00:00
|
|
|
annual_emissions = factor*snakemake.config['electricity']['co2base']
|
|
|
|
else:
|
|
|
|
annual_emissions = snakemake.config['electricity']['co2limit']
|
|
|
|
|
2018-01-29 21:28:33 +00:00
|
|
|
n.add("GlobalConstraint", "CO2Limit",
|
|
|
|
carrier_attribute="co2_emissions", sense="<=",
|
2019-06-18 09:50:54 +00:00
|
|
|
constant=annual_emissions * Nyears)
|
2018-01-29 21:28:33 +00:00
|
|
|
|
2020-08-28 15:59:51 +00:00
|
|
|
|
2018-01-29 21:28:33 +00:00
|
|
|
def add_emission_prices(n, emission_prices=None, exclude_co2=False):
|
|
|
|
if emission_prices is None:
|
|
|
|
emission_prices = snakemake.config['costs']['emission_prices']
|
|
|
|
if exclude_co2: emission_prices.pop('co2')
|
2019-11-14 15:21:00 +00:00
|
|
|
ep = (pd.Series(emission_prices).rename(lambda x: x+'_emissions') *
|
|
|
|
n.carriers.filter(like='_emissions')).sum(axis=1)
|
2020-08-25 10:12:00 +00:00
|
|
|
gen_ep = n.generators.carrier.map(ep) / n.generators.efficiency
|
|
|
|
n.generators['marginal_cost'] += gen_ep
|
|
|
|
su_ep = n.storage_units.carrier.map(ep) / n.storage_units.efficiency_dispatch
|
|
|
|
n.storage_units['marginal_cost'] += su_ep
|
2018-01-29 21:28:33 +00:00
|
|
|
|
2020-08-28 15:59:51 +00:00
|
|
|
|
2018-01-30 22:12:36 +00:00
|
|
|
def set_line_s_max_pu(n):
|
2020-10-02 13:55:22 +00:00
|
|
|
s_max_pu = snakemake.config['lines']['s_max_pu']
|
2018-01-30 22:12:36 +00:00
|
|
|
n.lines['s_max_pu'] = s_max_pu
|
2020-10-02 13:55:22 +00:00
|
|
|
logger.info(f"N-1 security margin of lines set to {s_max_pu}")
|
2018-01-30 22:12:36 +00:00
|
|
|
|
2019-02-03 12:50:05 +00:00
|
|
|
|
2020-08-28 15:59:51 +00:00
|
|
|
def set_transmission_limit(n, ll_type, factor, Nyears=1):
|
2019-02-13 18:03:57 +00:00
|
|
|
links_dc_b = n.links.carrier == 'DC' if not n.links.empty else pd.Series()
|
2018-10-22 21:22:34 +00:00
|
|
|
|
2020-08-28 15:59:51 +00:00
|
|
|
_lines_s_nom = (np.sqrt(3) * n.lines.type.map(n.line_types.i_nom) *
|
|
|
|
n.lines.num_parallel * n.lines.bus0.map(n.buses.v_nom))
|
|
|
|
lines_s_nom = n.lines.s_nom.where(n.lines.type == '', _lines_s_nom)
|
2019-02-03 12:50:05 +00:00
|
|
|
|
|
|
|
|
2020-08-28 15:59:51 +00:00
|
|
|
col = 'capital_cost' if ll_type == 'c' else 'length'
|
|
|
|
ref = (lines_s_nom @ n.lines[col] +
|
2020-11-12 16:37:43 +00:00
|
|
|
n.links.loc[links_dc_b, "p_nom"] @ n.links.loc[links_dc_b, col])
|
2020-08-28 15:59:51 +00:00
|
|
|
|
|
|
|
costs = load_costs(Nyears, snakemake.input.tech_costs,
|
|
|
|
snakemake.config['costs'],
|
|
|
|
snakemake.config['electricity'])
|
|
|
|
update_transmission_costs(n, costs, simple_hvdc_costs=False)
|
2018-01-29 21:28:33 +00:00
|
|
|
|
2020-08-28 15:59:51 +00:00
|
|
|
if factor == 'opt' or float(factor) > 1.0:
|
2018-02-19 09:11:58 +00:00
|
|
|
n.lines['s_nom_min'] = lines_s_nom
|
|
|
|
n.lines['s_nom_extendable'] = True
|
2018-10-22 21:22:34 +00:00
|
|
|
|
|
|
|
n.links.loc[links_dc_b, 'p_nom_min'] = n.links.loc[links_dc_b, 'p_nom']
|
|
|
|
n.links.loc[links_dc_b, 'p_nom_extendable'] = True
|
2018-02-19 09:11:58 +00:00
|
|
|
|
2020-08-28 15:59:51 +00:00
|
|
|
if factor != 'opt':
|
|
|
|
con_type = 'expansion_cost' if ll_type == 'c' else 'volume_expansion'
|
|
|
|
rhs = float(factor) * ref
|
2020-08-28 20:28:23 +00:00
|
|
|
n.add('GlobalConstraint', f'l{ll_type}_limit',
|
2020-08-28 15:59:51 +00:00
|
|
|
type=f'transmission_{con_type}_limit',
|
|
|
|
sense='<=', constant=rhs, carrier_attribute='AC, DC')
|
2018-01-29 21:28:33 +00:00
|
|
|
|
2020-12-03 18:50:53 +00:00
|
|
|
return n
|
2020-08-28 15:59:51 +00:00
|
|
|
|
|
|
|
|
2018-01-30 22:12:36 +00:00
|
|
|
def average_every_nhours(n, offset):
|
2020-12-03 15:02:21 +00:00
|
|
|
logger.info(f"Resampling the network to {offset}")
|
2018-01-30 22:12:36 +00:00
|
|
|
m = n.copy(with_time=False)
|
|
|
|
|
|
|
|
snapshot_weightings = n.snapshot_weightings.resample(offset).sum()
|
|
|
|
m.set_snapshots(snapshot_weightings.index)
|
|
|
|
m.snapshot_weightings = snapshot_weightings
|
|
|
|
|
|
|
|
for c in n.iterate_components():
|
|
|
|
pnl = getattr(m, c.list_name+"_t")
|
2018-02-01 11:42:56 +00:00
|
|
|
for k, df in iteritems(c.pnl):
|
2018-01-30 22:12:36 +00:00
|
|
|
if not df.empty:
|
|
|
|
pnl[k] = df.resample(offset).mean()
|
|
|
|
|
|
|
|
return m
|
|
|
|
|
2020-12-03 15:02:21 +00:00
|
|
|
def apply_time_segmentation(n, segments):
|
|
|
|
logger.info(f"Aggregating time series to {segments} segments.")
|
|
|
|
try:
|
|
|
|
import tsam.timeseriesaggregation as tsam
|
|
|
|
except:
|
|
|
|
raise ModuleNotFoundError("Optional dependency 'tsam' not found."
|
|
|
|
"Install via 'pip install tsam'")
|
|
|
|
|
|
|
|
p_max_pu_norm = n.generators_t.p_max_pu.max()
|
|
|
|
p_max_pu = n.generators_t.p_max_pu / p_max_pu_norm
|
|
|
|
|
|
|
|
load_norm = n.loads_t.p_set.max()
|
|
|
|
load = n.loads_t.p_set / load_norm
|
|
|
|
|
|
|
|
inflow_norm = n.storage_units_t.inflow.max()
|
|
|
|
inflow = n.storage_units_t.inflow / inflow_norm
|
|
|
|
|
|
|
|
raw = pd.concat([p_max_pu, load, inflow], axis=1, sort=False)
|
|
|
|
|
|
|
|
solver_name = snakemake.config["solving"]["solver"]["name"]
|
|
|
|
|
|
|
|
agg = tsam.TimeSeriesAggregation(raw, hoursPerPeriod=len(raw),
|
|
|
|
noTypicalPeriods=1, noSegments=int(segments),
|
|
|
|
segmentation=True, solver=solver_name)
|
|
|
|
|
|
|
|
segmented = agg.createTypicalPeriods()
|
|
|
|
|
|
|
|
weightings = segmented.index.get_level_values("Segment Duration")
|
|
|
|
offsets = np.insert(np.cumsum(weightings[:-1]), 0, 0)
|
|
|
|
snapshots = [n.snapshots[0] + pd.Timedelta(f"{offset}h") for offset in offsets]
|
|
|
|
|
|
|
|
n.set_snapshots(pd.DatetimeIndex(snapshots, name='name'))
|
|
|
|
n.snapshot_weightings = pd.Series(weightings, index=snapshots, name="weightings", dtype="float64")
|
|
|
|
|
|
|
|
segmented.index = snapshots
|
|
|
|
n.generators_t.p_max_pu = segmented[n.generators_t.p_max_pu.columns] * p_max_pu_norm
|
|
|
|
n.loads_t.p_set = segmented[n.loads_t.p_set.columns] * load_norm
|
|
|
|
n.storage_units_t.inflow = segmented[n.storage_units_t.inflow.columns] * inflow_norm
|
|
|
|
|
|
|
|
return n
|
|
|
|
|
2020-09-26 11:10:50 +00:00
|
|
|
def enforce_autarky(n, only_crossborder=False):
|
|
|
|
if only_crossborder:
|
|
|
|
lines_rm = n.lines.loc[
|
|
|
|
n.lines.bus0.map(n.buses.country) !=
|
|
|
|
n.lines.bus1.map(n.buses.country)
|
|
|
|
].index
|
|
|
|
links_rm = n.links.loc[
|
|
|
|
n.links.bus0.map(n.buses.country) !=
|
|
|
|
n.links.bus1.map(n.buses.country)
|
|
|
|
].index
|
|
|
|
else:
|
|
|
|
lines_rm = n.lines.index
|
2021-01-11 09:27:27 +00:00
|
|
|
links_rm = n.links.loc[n.links.carrier=="DC"].index
|
2020-09-26 11:10:50 +00:00
|
|
|
n.mremove("Line", lines_rm)
|
|
|
|
n.mremove("Link", links_rm)
|
|
|
|
|
|
|
|
def set_line_nom_max(n):
|
|
|
|
s_nom_max_set = snakemake.config["lines"].get("s_nom_max,", np.inf)
|
|
|
|
p_nom_max_set = snakemake.config["links"].get("p_nom_max", np.inf)
|
|
|
|
n.lines.s_nom_max.clip(upper=s_nom_max_set, inplace=True)
|
|
|
|
n.links.p_nom_max.clip(upper=p_nom_max_set, inplace=True)
|
2018-01-30 22:12:36 +00:00
|
|
|
|
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
|
|
|
|
snakemake = mock_snakemake('prepare_network', network='elec', simpl='',
|
2020-08-28 15:59:51 +00:00
|
|
|
clusters='40', ll='v0.3', opts='Co2L-24H')
|
2019-11-28 07:22:52 +00:00
|
|
|
configure_logging(snakemake)
|
2018-01-29 21:28:33 +00:00
|
|
|
|
|
|
|
opts = snakemake.wildcards.opts.split('-')
|
|
|
|
|
|
|
|
n = pypsa.Network(snakemake.input[0])
|
2020-12-03 18:50:53 +00:00
|
|
|
Nyears = n.snapshot_weightings.sum() / 8760.
|
2018-01-29 21:28:33 +00:00
|
|
|
|
2018-01-30 22:12:36 +00:00
|
|
|
set_line_s_max_pu(n)
|
|
|
|
|
|
|
|
for o in opts:
|
|
|
|
m = re.match(r'^\d+h$', o, re.IGNORECASE)
|
|
|
|
if m is not None:
|
|
|
|
n = average_every_nhours(n, m.group(0))
|
|
|
|
break
|
2020-12-03 15:02:21 +00:00
|
|
|
|
|
|
|
for o in opts:
|
|
|
|
m = re.match(r'^\d+seg$', o, re.IGNORECASE)
|
|
|
|
if m is not None:
|
|
|
|
n = apply_time_segmentation(n, m.group(0)[:-3])
|
|
|
|
break
|
2018-01-30 22:12:36 +00:00
|
|
|
|
2019-06-18 09:50:54 +00:00
|
|
|
for o in opts:
|
|
|
|
if "Co2L" in o:
|
|
|
|
m = re.findall("[0-9]*\.?[0-9]+$", o)
|
|
|
|
if len(m) > 0:
|
|
|
|
add_co2limit(n, Nyears, float(m[0]))
|
|
|
|
else:
|
|
|
|
add_co2limit(n, Nyears)
|
2020-12-03 18:50:53 +00:00
|
|
|
break
|
2018-01-29 21:28:33 +00:00
|
|
|
|
2020-07-10 14:41:44 +00:00
|
|
|
for o in opts:
|
|
|
|
oo = o.split("+")
|
2020-09-24 08:09:11 +00:00
|
|
|
suptechs = map(lambda c: c.split("-", 2)[0], n.carriers.index)
|
|
|
|
if oo[0].startswith(tuple(suptechs)):
|
2020-07-10 14:41:44 +00:00
|
|
|
carrier = oo[0]
|
2020-11-26 16:25:14 +00:00
|
|
|
# handles only p_nom_max as stores and lines have no potentials
|
|
|
|
attr_lookup = {"p": "p_nom_max", "c": "capital_cost"}
|
|
|
|
attr = attr_lookup[oo[1][0]]
|
|
|
|
factor = float(oo[1][1:])
|
2020-07-10 14:41:44 +00:00
|
|
|
if carrier == "AC": # lines do not have carrier
|
2020-11-26 16:25:14 +00:00
|
|
|
n.lines[attr] *= factor
|
2020-07-10 14:41:44 +00:00
|
|
|
else:
|
2020-11-26 16:25:14 +00:00
|
|
|
comps = {"Generator", "Link", "StorageUnit", "Store"}
|
2020-07-10 14:41:44 +00:00
|
|
|
for c in n.iterate_components(comps):
|
|
|
|
sel = c.df.carrier.str.contains(carrier)
|
2020-11-26 16:25:14 +00:00
|
|
|
c.df.loc[sel,attr] *= factor
|
2020-07-10 14:41:44 +00:00
|
|
|
|
2019-11-14 14:41:09 +00:00
|
|
|
if 'Ep' in opts:
|
|
|
|
add_emission_prices(n)
|
2018-01-29 21:28:33 +00:00
|
|
|
|
2019-02-03 15:06:35 +00:00
|
|
|
ll_type, factor = snakemake.wildcards.ll[0], snakemake.wildcards.ll[1:]
|
2020-08-28 15:59:51 +00:00
|
|
|
set_transmission_limit(n, ll_type, factor, Nyears)
|
2018-01-29 21:28:33 +00:00
|
|
|
|
2020-09-26 11:10:50 +00:00
|
|
|
set_line_nom_max(n)
|
|
|
|
|
|
|
|
if "ATK" in opts:
|
|
|
|
enforce_autarky(n)
|
|
|
|
elif "ATKc" in opts:
|
|
|
|
enforce_autarky(n, only_crossborder=True)
|
|
|
|
|
2018-01-29 21:28:33 +00:00
|
|
|
n.export_to_netcdf(snakemake.output[0])
|