diff --git a/Snakefile b/Snakefile index 6179e8f3..469f43ad 100644 --- a/Snakefile +++ b/Snakefile @@ -16,12 +16,12 @@ rule cluster_all_elec_networks: rule prepare_all_elec_networks: input: - expand("networks/elec_s{simpl}_{clusters}_l{ll}_{opts}.nc", + expand("networks/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc", **config['scenario']) rule solve_all_elec_networks: input: - expand("results/networks/elec_s{simpl}_{clusters}_l{ll}_{opts}.nc", + expand("results/networks/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc", **config['scenario']) if config['enable'].get('prepare_links_p_nom', False): @@ -221,6 +221,19 @@ rule cluster_network: # group: 'build_pypsa_networks' script: "scripts/cluster_network.py" + +rule add_extra_components: + input: + network='networks/{network}_s{simpl}_{clusters}.nc', + tech_costs=COSTS, + output: 'networks/{network}_s{simpl}_{clusters}_ec.nc' + benchmark: "benchmarks/add_extra_components/{network}_s{simpl}_{clusters}_ec" + threads: 1 + resources: mem=3000 + # group: 'build_pypsa_networks' + script: "scripts/add_extra_components.py" + + # rule add_sectors: # input: # network="networks/elec_{cost}_{resarea}_{opts}.nc", @@ -232,11 +245,11 @@ rule cluster_network: # script: "scripts/add_sectors.py" rule prepare_network: - input: 'networks/{network}_s{simpl}_{clusters}.nc', tech_costs=COSTS - output: 'networks/{network}_s{simpl}_{clusters}_l{ll}_{opts}.nc' + input: 'networks/{network}_s{simpl}_{clusters}_ec.nc', tech_costs=COSTS + output: 'networks/{network}_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc' threads: 1 resources: mem=1000 - # benchmark: "benchmarks/prepare_network/{network}_s{simpl}_{clusters}_l{ll}_{opts}" + # benchmark: "benchmarks/prepare_network/{network}_s{simpl}_{clusters}_ec_l{ll}_{opts}" script: "scripts/prepare_network.py" def memory(w): @@ -253,24 +266,24 @@ def memory(w): # return 4890+310 * int(w.clusters) rule solve_network: - input: "networks/{network}_s{simpl}_{clusters}_l{ll}_{opts}.nc" - output: "results/networks/{network}_s{simpl}_{clusters}_l{ll}_{opts}.nc" + input: "networks/{network}_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc" + output: "results/networks/{network}_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc" shadow: "shallow" log: - solver="logs/{network}_s{simpl}_{clusters}_l{ll}_{opts}_solver.log", - python="logs/{network}_s{simpl}_{clusters}_l{ll}_{opts}_python.log", - memory="logs/{network}_s{simpl}_{clusters}_l{ll}_{opts}_memory.log" - benchmark: "benchmarks/solve_network/{network}_s{simpl}_{clusters}_l{ll}_{opts}" + solver="logs/{network}_s{simpl}_{clusters}_ec_l{ll}_{opts}_solver.log", + python="logs/{network}_s{simpl}_{clusters}_ec_l{ll}_{opts}_python.log", + memory="logs/{network}_s{simpl}_{clusters}_ec_l{ll}_{opts}_memory.log" + benchmark: "benchmarks/solve_network/{network}_s{simpl}_{clusters}_ec_l{ll}_{opts}" threads: 4 resources: mem=memory # group: "solve" # with group, threads is ignored https://bitbucket.org/snakemake/snakemake/issues/971/group-job-description-does-not-contain script: "scripts/solve_network.py" rule trace_solve_network: - input: "networks/{network}_s{simpl}_{clusters}_l{ll}_{opts}.nc" - output: "results/networks/{network}_s{simpl}_{clusters}_l{ll}_{opts}_trace.nc" + input: "networks/{network}_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc" + output: "results/networks/{network}_s{simpl}_{clusters}_ec_l{ll}_{opts}_trace.nc" shadow: "shallow" - log: python="logs/{network}_s{simpl}_{clusters}_l{ll}_{opts}_python_trace.log", + log: python="logs/{network}_s{simpl}_{clusters}_ec_l{ll}_{opts}_python_trace.log", threads: 4 resources: mem=memory script: "scripts/trace_solve_network.py" @@ -278,14 +291,14 @@ rule trace_solve_network: rule solve_operations_network: input: unprepared="networks/{network}_s{simpl}_{clusters}.nc", - optimized="results/networks/{network}_s{simpl}_{clusters}_l{ll}_{opts}.nc" - output: "results/networks/{network}_s{simpl}_{clusters}_l{ll}_{opts}_op.nc" + optimized="results/networks/{network}_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc" + output: "results/networks/{network}_s{simpl}_{clusters}_ec_l{ll}_{opts}_op.nc" shadow: "shallow" log: - solver="logs/solve_operations_network/{network}_s{simpl}_{clusters}_l{ll}_{opts}_op_solver.log", - python="logs/solve_operations_network/{network}_s{simpl}_{clusters}_l{ll}_{opts}_op_python.log", - memory="logs/solve_operations_network/{network}_s{simpl}_{clusters}_l{ll}_{opts}_op_memory.log" - benchmark: "benchmarks/solve_operations_network/{network}_s{simpl}_{clusters}_l{ll}_{opts}" + solver="logs/solve_operations_network/{network}_s{simpl}_{clusters}_ec_l{ll}_{opts}_op_solver.log", + python="logs/solve_operations_network/{network}_s{simpl}_{clusters}_ec_l{ll}_{opts}_op_python.log", + memory="logs/solve_operations_network/{network}_s{simpl}_{clusters}_ec_l{ll}_{opts}_op_memory.log" + benchmark: "benchmarks/solve_operations_network/{network}_s{simpl}_{clusters}_ec_l{ll}_{opts}" threads: 4 resources: mem=(lambda w: 5000 + 372 * int(w.clusters)) # group: "solve_operations" @@ -293,11 +306,11 @@ rule solve_operations_network: rule plot_network: input: - network="results/networks/{network}_s{simpl}_{clusters}_l{ll}_{opts}.nc", + network="results/networks/{network}_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc", tech_costs=COSTS output: - only_map="results/plots/{network}_s{simpl}_{clusters}_l{ll}_{opts}_{attr}.{ext}", - ext="results/plots/{network}_s{simpl}_{clusters}_l{ll}_{opts}_{attr}_ext.{ext}" + only_map="results/plots/{network}_s{simpl}_{clusters}_ec_l{ll}_{opts}_{attr}.{ext}", + ext="results/plots/{network}_s{simpl}_{clusters}_ec_l{ll}_{opts}_{attr}_ext.{ext}" script: "scripts/plot_network.py" def input_make_summary(w): @@ -309,7 +322,7 @@ def input_make_summary(w): else: ll = w.ll return ([COSTS] + - expand("results/networks/{network}_s{simpl}_{clusters}_l{ll}_{opts}.nc", + expand("results/networks/{network}_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc", network=w.network, ll=ll, **{k: config["scenario"][k] if getattr(w, k) == "all" else getattr(w, k) @@ -317,12 +330,12 @@ def input_make_summary(w): rule make_summary: input: input_make_summary - output: directory("results/summaries/{network}_s{simpl}_{clusters}_l{ll}_{opts}_{country}") + output: directory("results/summaries/{network}_s{simpl}_{clusters}_ec_l{ll}_{opts}_{country}") script: "scripts/make_summary.py" rule plot_summary: - input: "results/summaries/{network}_s{simpl}_{clusters}_l{ll}_{opts}_{country}" - output: "results/plots/summary_{summary}_{network}_s{simpl}_{clusters}_l{ll}_{opts}_{country}.{ext}" + input: "results/summaries/{network}_s{simpl}_{clusters}_ec_l{ll}_{opts}_{country}" + output: "results/plots/summary_{summary}_{network}_s{simpl}_{clusters}_ec_l{ll}_{opts}_{country}.{ext}" script: "scripts/plot_summary.py" def input_plot_p_nom_max(wildcards): diff --git a/config.default.yaml b/config.default.yaml index 5fa8fd77..d90f0d84 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -33,6 +33,7 @@ electricity: extendable_carriers: Generator: [OCGT] StorageUnit: [battery, H2] + Store: [] # battery, H2 max_hours: battery: 6 diff --git a/config.tutorial.yaml b/config.tutorial.yaml index 0e383cd9..061ba745 100644 --- a/config.tutorial.yaml +++ b/config.tutorial.yaml @@ -31,6 +31,7 @@ electricity: extendable_carriers: Generator: [OCGT] StorageUnit: [battery, H2] + Store: [] #battery, H2 max_hours: battery: 6 diff --git a/doc/configtables/electricity.csv b/doc/configtables/electricity.csv index 0db1cd0e..0df537ae 100644 --- a/doc/configtables/electricity.csv +++ b/doc/configtables/electricity.csv @@ -4,8 +4,9 @@ co2limit,:math:`t_{CO_2-eq}/a`,float,"Cap on total annual system carbon dioxide co2base,:math:`t_{CO_2-eq}/a`,float,"Reference value of total annual system carbon dioxide emissions if relative emission reduction target is specified in ``{opts}`` wildcard." agg_p_nom_limits,--,file path,"Reference to ``.csv`` file specifying per carrier generator nominal capacity constraints for individual countries if ``'CCL'`` is in ``{opts}`` wildcard. Defaults to ``data/agg_p_nom_minmax.csv``." extendable_carriers,,, --- Generator,--,"Any subset of {'OCGT','CCGT', 'nuclear'}","Places extendable conventional power plants (OCGT, CCGT and/or nuclear) where such power plants are located today without capacity limits." --- StorageUnit,--,"Any subset of {'battery','H2'}","Places extendable storage units (battery and/or hydrogen) at every node/bus without capacity limits." +-- Generator,--,"Any subset of {'OCGT','CCGT'}","Places extendable conventional power plants (OCGT and/or CCGT) where gas power plants are located today without capacity limits." +-- StorageUnit,--,"Any subset of {'battery','H2'}","Adds extendable storage units (battery and/or hydrogen) at every node/bus after clustering without capacity limits and with zero initial capacity." +-- Store,--,"Any subset of {'battery','H2'}","Adds extendable storage units (battery and/or hydrogen) at every node/bus after clustering without capacity limits and with zero initial capacity." max_hours,,, -- battery,h,float,"Maximum state of charge capacity of the battery in terms of hours at full output capacity ``p_nom``. Cf. `PyPSA documentation `_." -- H2,h,float,"Maximum state of charge capacity of the hydrogen storage in terms of hours at full output capacity ``p_nom``. Cf. `PyPSA documentation `_." diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 6cd87bcf..ec959dcb 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -11,10 +11,18 @@ This is the first release of PyPSA-Eur: * The ``conda`` environment files were updated and extended (`#81 `_). -* The power plant database was updated (`#84 `_). +* The power plant database was updated with extensive filtering options via ``pandas.query`` functionality (`#84 `_ and `#94 `_). -* Continuous integration testing with `Travis CI `_ is now included (`#82 `_). +* Continuous integration testing with `Travis CI `_ is now included for Linux, Mac and Windows (`#82 `_). -* Data dependencies were moved to `zenodo `_ (`#60 `_). +* Data dependencies were moved to `zenodo `_ and are now versioned (`#60 `_). -* Data dependencies are now retrieved from withing the snakemake workflow (`#86 `_). \ No newline at end of file +* Data dependencies are now retrieved directly from within the snakemake workflow (`#86 `_). + +* Emission prices can be added to marginal costs of generators through the keyworks ``Ep`` in the ``{opts}`` wildcard (`#100 `_). + +* An option is introduced to add extendable nuclear power plants to the network (`#98 `_). + +* Focus weights can now be specified for particular countries for the network clustering, which allows to set a proportion of the total number of clusters for particular countries (`#87 `_). + +* A new rule :mod:`add_extra_components` allows to add additional components to the network only after clustering. It is thereby possible to model storage units (e.g. battery and hydrogen) in more detail via a combination of ``Store``, ``Link`` and ``Bus`` elements (`#97 `_). diff --git a/doc/simplification.rst b/doc/simplification.rst index 37407bd2..1da3f120 100644 --- a/doc/simplification.rst +++ b/doc/simplification.rst @@ -13,9 +13,12 @@ The simplification and clustering steps are described in detail in the paper - Jonas Hörsch and Tom Brown. `The role of spatial scale in joint optimisations of generation and transmission for European highly renewable scenarios `_), *14th International Conference on the European Energy Market*, 2017. `arXiv:1705.07617 `_, `doi:10.1109/EEM.2017.7982024 `_. +After simplification and clustering of the network, additional components may be appended in the rule :mod:`add_extra_components` and the network is prepared for solving in :mod:`prepare_network`. + .. toctree:: :caption: Overview simplification/simplify_network simplification/cluster_network + simplification/add_extra_components simplification/prepare_network diff --git a/doc/simplification/add_extra_components.rst b/doc/simplification/add_extra_components.rst new file mode 100644 index 00000000..14d9b668 --- /dev/null +++ b/doc/simplification/add_extra_components.rst @@ -0,0 +1,37 @@ +.. _extra_components: + +Rule ``add_extra_components`` +============================= + +.. graphviz:: + :align: center + + digraph snakemake_dag { + graph [bgcolor=white, + margin=0, + size="8,5" + ]; + node [fontname=sans, + fontsize=10, + penwidth=2, + shape=box, + style=rounded + ]; + edge [color=grey, + penwidth=2 + ]; + 1 [color="0.56 0.6 0.85", + label=prepare_network]; + 2 [color="0.47 0.6 0.85", + fillcolor=gray, + label=add_extra_components, + style=filled]; + 2 -> 1; + 3 [color="0.03 0.6 0.85", + label=cluster_network]; + 3 -> 2; + } + +| + +.. automodule:: add_extra_components diff --git a/doc/tutorial.rst b/doc/tutorial.rst index 51164c73..b503fdd7 100644 --- a/doc/tutorial.rst +++ b/doc/tutorial.rst @@ -119,7 +119,7 @@ orders ``snakemake`` to run the script ``solve_network`` that produces the solve .. warning:: On Windows the previous command may currently cause a ``MissingRuleException`` due to problems with output files in subfolders. - This is an `open issue < https://github.com/snakemake/snakemake/issues/46>`_ at `snakemake `_. + This is an `open issue `_ at `snakemake `_. Windows users should add the option ``--keep-target-files`` to the command or instead run ``snakemake solve_all_elec_networks``. This triggers a workflow of multiple preceding jobs that depend on each rule's inputs and outputs: diff --git a/environment.docs.yaml b/environment.docs.yaml index 3322f8aa..7cec5e9e 100644 --- a/environment.docs.yaml +++ b/environment.docs.yaml @@ -17,6 +17,7 @@ dependencies: - memory_profiler - yaml - pytables + - powerplantmatching>=0.4.2 # Second order dependencies which should really be deps of atlite - xarray @@ -25,6 +26,7 @@ dependencies: #- toolz #- dask - progressbar2 + - pyyaml>=5.1.0 # Include ipython so that one does not inadvertently drop out of the conda # environment by calling ipython @@ -46,6 +48,5 @@ dependencies: - git+https://github.com/PyPSA/glaes.git#egg=glaes - git+https://github.com/PyPSA/geokit.git#egg=geokit - cdsapi - - powerplantmatching - sphinx - sphinx_rtd_theme diff --git a/environment.yaml b/environment.yaml index 2c60dc52..09e9ca84 100644 --- a/environment.yaml +++ b/environment.yaml @@ -18,6 +18,7 @@ dependencies: - memory_profiler - yaml - pytables + - powerplantmatching>=0.4.2 # Second order dependencies which should really be deps of atlite - xarray @@ -26,6 +27,7 @@ dependencies: - toolz - dask - progressbar2 + - pyyaml>=5.1.0 # Include ipython so that one does not inadvertently drop out of the conda # environment by calling ipython @@ -46,4 +48,3 @@ dependencies: - git+https://github.com/PyPSA/glaes.git#egg=glaes - git+https://github.com/PyPSA/geokit.git#egg=geokit - cdsapi - - powerplantmatching>=0.4.2 diff --git a/scripts/add_electricity.py b/scripts/add_electricity.py index a60c9597..a78ab1cb 100644 --- a/scripts/add_electricity.py +++ b/scripts/add_electricity.py @@ -1,6 +1,6 @@ # coding: utf-8 """ -Adds electrical generators and storage units to a base network. +Adds electrical generators and existing hydro storage units to a base network. Relevant Settings ----------------- @@ -21,7 +21,6 @@ Relevant Settings co2limit: extendable_carriers: Generator: - StorageUnit: estimate_renewable_capacities_from_capacity_stats: load: @@ -81,10 +80,9 @@ The rule :mod:`add_electricity` ties all the different data inputs from the prec - today's thermal and hydro power generation capacities (for the technologies listed in the config setting ``electricity: conventional_carriers``), and - today's load time-series (upsampled in a top-down approach according to population and gross domestic product) -It further adds extendable ``generators`` and ``storage_units`` with **zero** capacity for +It further adds extendable ``generators`` with **zero** capacity for - photovoltaic, onshore and AC- as well as DC-connected offshore wind installations with today's locational, hourly wind and solar capacity factors (but **no** current capacities), -- long-term hydrogen and short-term battery storage units (if listed in the config setting ``electricity: extendable_carriers``), and - additional open- and combined-cycle gas turbines (if ``OCGT`` and/or ``CCGT`` is listed in the config setting ``electricity: extendable_carriers``) """ @@ -265,7 +263,8 @@ def update_transmission_costs(n, costs, length_factor=1.0, simple_hvdc_costs=Fal costs.at['HVDC submarine', 'capital_cost']) + costs.at['HVDC inverter pair', 'capital_cost']) n.links.loc[dc_b, 'capital_cost'] = costs -# ### Generators + +### Generators def attach_wind_and_solar(n, costs): for tech in snakemake.config['renewable']: @@ -307,8 +306,6 @@ def attach_wind_and_solar(n, costs): p_max_pu=ds['profile'].transpose('time', 'bus').to_pandas()) -# # Generators - def attach_conventional_generators(n, costs, ppl): carriers = snakemake.config['electricity']['conventional_carriers'] @@ -329,6 +326,7 @@ def attach_conventional_generators(n, costs, ppl): def attach_hydro(n, costs, ppl): + if 'hydro' not in snakemake.config['renewable']: return c = snakemake.config['renewable']['hydro'] carriers = c.get('carriers', ['ror', 'PHS', 'hydro']) @@ -431,13 +429,10 @@ def attach_hydro(n, costs, ppl): def attach_extendable_generators(n, costs, ppl): elec_opts = snakemake.config['electricity'] carriers = pd.Index(elec_opts['extendable_carriers']['Generator']) - _add_missing_carriers_from_costs(n, costs, carriers) for tech in carriers: - suptech = tech.split('-')[0] - - if suptech == 'OCGT': + if tech.startswith('OCGT'): ocgt = ppl.query("carrier in ['OCGT', 'CCGT']").groupby('bus', as_index=False).first() n.madd('Generator', ocgt.index, suffix=' OCGT', @@ -449,7 +444,7 @@ def attach_extendable_generators(n, costs, ppl): marginal_cost=costs.at['OCGT', 'marginal_cost'], efficiency=costs.at['OCGT', 'efficiency']) - elif suptech == 'CCGT': + elif tech.startswith('CCGT'): ccgt = ppl.query("carrier in ['OCGT', 'CCGT']").groupby('bus', as_index=False).first() n.madd('Generator', ccgt.index, suffix=' CCGT', @@ -461,7 +456,7 @@ def attach_extendable_generators(n, costs, ppl): marginal_cost=costs.at['CCGT', 'marginal_cost'], efficiency=costs.at['CCGT', 'efficiency']) - elif suptech == 'nuclear': + elif tech.startswith('nuclear'): nuclear = ppl.query("carrier == 'nuclear'").groupby('bus', as_index=False).first() n.madd('Generator', nuclear.index, suffix=' nuclear', @@ -479,77 +474,6 @@ def attach_extendable_generators(n, costs, ppl): "Only OCGT, CCGT and nuclear are allowed at the moment.") -def attach_storage(n, costs): - elec_opts = snakemake.config['electricity'] - carriers = elec_opts['extendable_carriers']['StorageUnit'] - max_hours = elec_opts['max_hours'] - - _add_missing_carriers_from_costs(n, costs, carriers) - - buses_i = n.buses.index[n.buses.substation_lv] - - for carrier in carriers: - n.madd("StorageUnit", buses_i, ' ' + carrier, - bus=buses_i, - carrier=carrier, - p_nom_extendable=True, - capital_cost=costs.at[carrier, 'capital_cost'], - marginal_cost=costs.at[carrier, 'marginal_cost'], - efficiency_store=costs.at[carrier, 'efficiency'], - efficiency_dispatch=costs.at[carrier, 'efficiency'], - max_hours=max_hours[carrier], - cyclic_state_of_charge=True) - - ## Implementing them separately will come later! - ## - # if 'H2' in carriers: - # h2_buses = n.madd("Bus", buses + " H2", carrier="H2") - - # n.madd("Link", h2_buses + " Electrolysis", - # bus1=h2_buses, - # bus0=buses, - # p_nom_extendable=True, - # efficiency=costs.at["electrolysis", "efficiency"], - # capital_cost=costs.at["electrolysis", "capital_cost"]) - - # n.madd("Link", h2_buses + " Fuel Cell", - # bus0=h2_buses, - # bus1=buses, - # p_nom_extendable=True, - # efficiency=costs.at["fuel cell", "efficiency"], - # #NB: fixed cost is per MWel - # capital_cost=costs.at["fuel cell", "capital_cost"] * costs.at["fuel cell", "efficiency"]) - - # n.madd("Store", h2_buses, - # bus=h2_buses, - # e_nom_extendable=True, - # e_cyclic=True, - # capital_cost=costs.at["hydrogen storage", "capital_cost"]) - - # if 'battery' in carriers: - # b_buses = n.madd("Bus", buses + " battery", carrier="battery") - - # network.madd("Store", b_buses, - # bus=b_buses, - # e_cyclic=True, - # e_nom_extendable=True, - # capital_cost=costs.at['battery storage', 'capital_cost']) - - # network.madd("Link", b_buses + " charger", - # bus0=buses, - # bus1=b_buses, - # efficiency=costs.at['battery inverter', 'efficiency']**0.5, - # capital_cost=costs.at['battery inverter', 'capital_cost'], - # p_nom_extendable=True) - - # network.madd("Link", - # nodes + " battery discharger", - # bus0=nodes + " battery", - # bus1=nodes, - # efficiency=costs.at['battery inverter','efficiency']**0.5, - # marginal_cost=options['marginal_cost_storage'], - # p_nom_extendable=True) - def estimate_renewable_capacities(n, tech_map=None): if tech_map is None: tech_map = (snakemake.config['electricity'] @@ -574,8 +498,9 @@ def estimate_renewable_capacities(n, tech_map=None): .transform(lambda s: normed(s) * tech_capacities.at[s.name]) .where(lambda s: s>0.1, 0.)) # only capacities above 100kW -def add_nice_carrier_names(n): - nice_names = pd.Series(snakemake.config['plotting']['nice_names']) +def add_nice_carrier_names(n, config=None): + if config is None: config = snakemake.config + nice_names = pd.Series(config['plotting']['nice_names']) n.carriers['nice_names'] = nice_names[n.carriers.index] @@ -608,13 +533,11 @@ if __name__ == "__main__": attach_load(n) update_transmission_costs(n, costs) - attach_conventional_generators(n, costs, ppl) + attach_conventional_generators(n, costs, ppl) attach_wind_and_solar(n, costs) - if 'hydro' in snakemake.config['renewable']: - attach_hydro(n, costs, ppl) + attach_hydro(n, costs, ppl) attach_extendable_generators(n, costs, ppl) - attach_storage(n, costs) estimate_renewable_capacities(n) add_nice_carrier_names(n) diff --git a/scripts/add_extra_components.py b/scripts/add_extra_components.py new file mode 100644 index 00000000..dea774eb --- /dev/null +++ b/scripts/add_extra_components.py @@ -0,0 +1,166 @@ +# coding: utf-8 +""" +Adds extra extendable components to the clustered and simplified network. + +Relevant Settings +----------------- + +.. code:: yaml + + costs: + year: + USD2013_to_EUR2013: + dicountrate: + emission_prices: + + electricity: + max_hours: + marginal_cost: + capital_cost: + extendable_carriers: + StorageUnit: + Store: + +.. 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. + +Outputs +------- + +- ``networks/{network}_s{simpl}_{clusters}_ec.nc``: + + +Description +----------- + +The rule :mod:`add_extra_components` attaches additional extendable components to the clustered and simplified network. These can be configured in the ``config.yaml`` at ``electricity: extendable_carriers: ``. It processes ``networks/{network}_s{simpl}_{clusters}.nc`` to build ``networks/{network}_s{simpl}_{clusters}_ec.nc``, which in contrast to the former (depending on the configuration) contain with **zero** initial capacity + +- ``StorageUnits`` of carrier 'H2' and/or 'battery'. If this option is chosen, every bus is given an extendable ``StorageUnit`` of the corresponding carrier. The energy and power capacities are linked through a parameter that specifies the energy capacity as maximum hours at full dispatch power and is configured in ``electricity: max_hours:``. This linkage leads to one investment variable per storage unit. The default ``max_hours`` lead to long-term hydrogen and short-term battery storage units. + +- ``Stores`` of carrier 'H2' and/or 'battery' in combination with ``Links``. If this option is chosen, the script adds extra buses with corresponding carrier where energy ``Stores`` are attached and which are connected to the corresponding power buses via two links, one each for charging and discharging. This leads to three investment variables for the energy capacity, charging and discharging capacity of the storage unit. +""" + +import logging +import pandas as pd +import pypsa +from add_electricity import (load_costs, normed, add_nice_carrier_names, + _add_missing_carriers_from_costs) + +idx = pd.IndexSlice +logger = logging.getLogger(__name__) + + +def attach_storageunits(n, costs): + elec_opts = snakemake.config['electricity'] + carriers = elec_opts['extendable_carriers']['StorageUnit'] + max_hours = elec_opts['max_hours'] + + _add_missing_carriers_from_costs(n, costs, carriers) + + buses_i = n.buses.index + + for carrier in carriers: + n.madd("StorageUnit", buses_i, ' ' + carrier, + bus=buses_i, + carrier=carrier, + p_nom_extendable=True, + capital_cost=costs.at[carrier, 'capital_cost'], + marginal_cost=costs.at[carrier, 'marginal_cost'], + efficiency_store=costs.at[carrier, 'efficiency'], + efficiency_dispatch=costs.at[carrier, 'efficiency'], + max_hours=max_hours[carrier], + cyclic_state_of_charge=True) + +def attach_stores(n, costs): + elec_opts = snakemake.config['electricity'] + carriers = elec_opts['extendable_carriers']['Store'] + + _add_missing_carriers_from_costs(n, costs, carriers) + + buses_i = n.buses.index + bus_sub_dict = {k: n.buses[k].values for k in ['x', 'y', 'country']} + + if 'H2' in carriers: + h2_buses_i = n.madd("Bus", buses_i + " H2", carrier="H2", **bus_sub_dict) + + n.madd("Store", h2_buses_i, + bus=h2_buses_i, + carrier='H2', + e_nom_extendable=True, + e_cyclic=True, + capital_cost=costs.at["hydrogen storage", "capital_cost"]) + + n.madd("Link", h2_buses_i + " Electrolysis", + bus0=buses_i, + bus1=h2_buses_i, + carrier='H2 electrolysis', + p_nom_extendable=True, + efficiency=costs.at["electrolysis", "efficiency"], + capital_cost=costs.at["electrolysis", "capital_cost"]) + + n.madd("Link", h2_buses_i + " Fuel Cell", + bus0=h2_buses_i, + bus1=buses_i, + carrier='H2 fuel cell', + p_nom_extendable=True, + efficiency=costs.at["fuel cell", "efficiency"], + #NB: fixed cost is per MWel + capital_cost=costs.at["fuel cell", "capital_cost"] * costs.at["fuel cell", "efficiency"]) + + if 'battery' in carriers: + b_buses_i = n.madd("Bus", buses_i + " battery", carrier="battery", **bus_sub_dict) + + n.madd("Store", b_buses_i, + bus=b_buses_i, + carrier='battery', + e_cyclic=True, + e_nom_extendable=True, + capital_cost=costs.at['battery storage', 'capital_cost']) + + n.madd("Link", b_buses_i + " charger", + bus0=buses_i, + bus1=b_buses_i, + carrier='battery charger', + efficiency=costs.at['battery inverter', 'efficiency']**0.5, + capital_cost=costs.at['battery inverter', 'capital_cost'], + p_nom_extendable=True) + + n.madd("Link", b_buses_i + " discharger", + bus0=b_buses_i, + bus1=buses_i, + carrier='battery discharger', + efficiency=costs.at['battery inverter','efficiency']**0.5, + capital_cost=costs.at['battery inverter', 'capital_cost'], + p_nom_extendable=True) + + +if __name__ == "__main__": + # Detect running outside of snakemake and mock snakemake for testing + if 'snakemake' not in globals(): + from vresutils.snakemake import MockSnakemake, Dict + + snakemake = MockSnakemake(output=['networks/elec_s_5_ec.nc']) + snakemake.input = snakemake.expand( + Dict(network='networks/elec_s_5.nc', + tech_costs='data/costs.csv')) + + logging.basicConfig(level=snakemake.config['logging_level']) + + n = pypsa.Network(snakemake.input.network) + Nyears = n.snapshot_weightings.sum()/8760. + costs = load_costs(Nyears, tech_costs=snakemake.input.tech_costs, + config=snakemake.config['costs'], + elec_config=snakemake.config['electricity']) + + attach_storageunits(n, costs) + attach_stores(n, costs) + + add_nice_carrier_names(n, config=snakemake.config) + + n.export_to_netcdf(snakemake.output[0]) diff --git a/test/config.test1.yaml b/test/config.test1.yaml index a0429805..b3af72b0 100644 --- a/test/config.test1.yaml +++ b/test/config.test1.yaml @@ -31,6 +31,7 @@ electricity: extendable_carriers: Generator: [OCGT] StorageUnit: [battery, H2] + Store: [] #battery, H2 max_hours: battery: 6