pypsa-eur/scripts/prepare_network.py
FabianHofmann 04f19f214d
fix emission prices (#171)
* fix emission prices

I'm not sure if the previous setup was intentional, but regarding that different generators might have different efficiencies and the emissions are carrier specific only, it makes more sense set net emission price.

* small fix

* update release_notes and config
2020-08-25 12:12:00 +02:00

243 lines
8.2 KiB
Python
Executable File

# SPDX-FileCopyrightText: : 2017-2020 The PyPSA-Eur Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
# coding: utf-8
"""
Prepare PyPSA network for solving according to :ref:`opts` and :ref:`ll`, such as
- adding an annual **limit** of carbon-dioxide emissions,
- adding an exogenous **price** per tonne emissions of carbon-dioxide (or other kinds),
- setting an **N-1 security margin** factor for transmission line capacities,
- specifying a limit on the **cost** of transmission expansion,
- specifying a limit on the **volume** of transmission expansion, and
- reducing the **temporal** resolution by averaging over multiple hours.
Relevant Settings
-----------------
.. code:: yaml
costs:
emission_prices:
USD2013_to_EUR2013:
discountrate:
marginal_cost:
capital_cost:
electricity:
co2limit:
max_hours:
.. seealso::
Documentation of the configuration file ``config.yaml`` at
:ref:`costs_cf`, :ref:`electricity_cf`
Inputs
------
- ``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.
- ``networks/{network}_s{simpl}_{clusters}.nc``: confer :ref:`cluster`
Outputs
-------
- ``networks/{network}_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc``: Complete PyPSA network that will be handed to the ``solve_network`` rule.
Description
-----------
.. tip::
The rule :mod:`prepare_all_networks` runs
for all ``scenario`` s in the configuration file
the rule :mod:`prepare_network`.
"""
import logging
logger = logging.getLogger(__name__)
from _helpers import configure_logging
from add_electricity import load_costs, update_transmission_costs
from six import iteritems
import numpy as np
import re
import pypsa
import pandas as pd
idx = pd.IndexSlice
def add_co2limit(n, Nyears=1., factor=None):
if factor is not None:
annual_emissions = factor*snakemake.config['electricity']['co2base']
else:
annual_emissions = snakemake.config['electricity']['co2limit']
n.add("GlobalConstraint", "CO2Limit",
carrier_attribute="co2_emissions", sense="<=",
constant=annual_emissions * Nyears)
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')
ep = (pd.Series(emission_prices).rename(lambda x: x+'_emissions') *
n.carriers.filter(like='_emissions')).sum(axis=1)
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
def set_line_s_max_pu(n):
# set n-1 security margin to 0.5 for 37 clusters and to 0.7 from 200 clusters
n_clusters = len(n.buses)
s_max_pu = np.clip(0.5 + 0.2 * (n_clusters - 37) / (200 - 37), 0.5, 0.7)
n.lines['s_max_pu'] = s_max_pu
def set_line_cost_limit(n, lc, Nyears=1.):
links_dc_b = n.links.carrier == 'DC' if not n.links.empty else pd.Series()
lines_s_nom = n.lines.s_nom.where(
n.lines.type == '',
np.sqrt(3) * n.lines.num_parallel *
n.lines.type.map(n.line_types.i_nom) *
n.lines.bus0.map(n.buses.v_nom)
)
n.lines['capital_cost_lc'] = n.lines['capital_cost']
n.links['capital_cost_lc'] = n.links['capital_cost']
total_line_cost = ((lines_s_nom * n.lines['capital_cost_lc']).sum() +
n.links.loc[links_dc_b].eval('p_nom * capital_cost_lc').sum())
if lc == 'opt':
costs = load_costs(Nyears, snakemake.input.tech_costs,
snakemake.config['costs'], snakemake.config['electricity'])
update_transmission_costs(n, costs, simple_hvdc_costs=False)
else:
# Either line_volume cap or cost
n.lines['capital_cost'] = 0.
n.links.loc[links_dc_b, 'capital_cost'] = 0.
if lc == 'opt' or float(lc) > 1.0:
n.lines['s_nom_min'] = lines_s_nom
n.lines['s_nom_extendable'] = True
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
if lc != 'opt':
line_cost = float(lc) * total_line_cost
n.add('GlobalConstraint', 'lc_limit',
type='transmission_expansion_cost_limit',
sense='<=', constant=line_cost, carrier_attribute='AC, DC')
return n
def set_line_volume_limit(n, lv, Nyears=1.):
links_dc_b = n.links.carrier == 'DC' if not n.links.empty else pd.Series()
lines_s_nom = n.lines.s_nom.where(
n.lines.type == '',
np.sqrt(3) * n.lines.num_parallel *
n.lines.type.map(n.line_types.i_nom) *
n.lines.bus0.map(n.buses.v_nom)
)
total_line_volume = ((lines_s_nom * n.lines['length']).sum() +
n.links.loc[links_dc_b].eval('p_nom * length').sum())
if lv == 'opt':
costs = load_costs(Nyears, snakemake.input.tech_costs,
snakemake.config['costs'], snakemake.config['electricity'])
update_transmission_costs(n, costs, simple_hvdc_costs=True)
else:
# Either line_volume cap or cost
n.lines['capital_cost'] = 0.
n.links.loc[links_dc_b, 'capital_cost'] = 0.
if lv == 'opt' or float(lv) > 1.0:
n.lines['s_nom_min'] = lines_s_nom
n.lines['s_nom_extendable'] = True
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
if lv != 'opt':
line_volume = float(lv) * total_line_volume
n.add('GlobalConstraint', 'lv_limit',
type='transmission_volume_expansion_limit',
sense='<=', constant=line_volume, carrier_attribute='AC, DC')
return n
def average_every_nhours(n, offset):
logger.info('Resampling the network to {}'.format(offset))
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")
for k, df in iteritems(c.pnl):
if not df.empty:
pnl[k] = df.resample(offset).mean()
return m
if __name__ == "__main__":
if 'snakemake' not in globals():
from _helpers import mock_snakemake
snakemake = mock_snakemake('prepare_network', network='elec', simpl='',
clusters='5', ll='copt', opts='Co2L-24H')
configure_logging(snakemake)
opts = snakemake.wildcards.opts.split('-')
n = pypsa.Network(snakemake.input[0])
Nyears = n.snapshot_weightings.sum()/8760.
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
else:
logger.info("No resampling")
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)
for o in opts:
oo = o.split("+")
if oo[0].startswith(tuple(n.carriers.index)):
carrier = oo[0]
cost_factor = float(oo[1])
if carrier == "AC": # lines do not have carrier
n.lines.capital_cost *= cost_factor
else:
comps = {"Generator", "Link", "StorageUnit"}
for c in n.iterate_components(comps):
sel = c.df.carrier.str.contains(carrier)
c.df.loc[sel,"capital_cost"] *= cost_factor
if 'Ep' in opts:
add_emission_prices(n)
ll_type, factor = snakemake.wildcards.ll[0], snakemake.wildcards.ll[1:]
if ll_type == 'v':
set_line_volume_limit(n, factor, Nyears)
elif ll_type == 'c':
set_line_cost_limit(n, factor, Nyears)
n.export_to_netcdf(snakemake.output[0])