From 6c13974643ee9687d3053e29932af73a7da01dc5 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Mon, 1 Aug 2022 18:02:55 +0200 Subject: [PATCH 01/16] add option to cluster heat buses --- config.default.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/config.default.yaml b/config.default.yaml index b2fa5f6b..ad6764c7 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -154,6 +154,7 @@ sector: # 2040: 0.6 # 2050: 1.0 district_heating_loss: 0.15 + cluster_heat_buses: False # cluster residential and service heat buses to one to save memory bev_dsm_restriction_value: 0.75 #Set to 0 for no restriction on BEV DSM bev_dsm_restriction_time: 7 #Time at which SOC of BEV has to be dsm_restriction_value transport_heating_deadband_upper: 20. From 973074de217687440af9a86aca8a813dd02ceab9 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Mon, 1 Aug 2022 18:03:11 +0200 Subject: [PATCH 02/16] add function to cluster heat buses --- scripts/prepare_sector_network.py | 99 +++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 7abdadff..1eabc37d 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -19,6 +19,7 @@ from helper import override_component_attrs, generate_periodic_profiles, update_ from networkx.algorithms.connectivity.edge_augmentation import k_edge_augmentation from networkx.algorithms import complement from pypsa.geo import haversine_pts +from pypsa.io import import_components_from_dataframe import logging logger = logging.getLogger(__name__) @@ -26,6 +27,9 @@ logger = logging.getLogger(__name__) from types import SimpleNamespace spatial = SimpleNamespace() +from distutils.version import LooseVersion +pd_version = LooseVersion(pd.__version__) +agg_group_kwargs = dict(numeric_only=False) if pd_version >= "1.3" else {} def define_spatial(nodes, options): """ @@ -2323,6 +2327,99 @@ def limit_individual_line_extension(n, maxext): hvdc = n.links.index[n.links.carrier == 'DC'] n.links.loc[hvdc, 'p_nom_max'] = n.links.loc[hvdc, 'p_nom'] + maxext + +aggregate_dict = { + "p_nom": "sum", + "s_nom": "sum", + "v_nom": "max", + "v_mag_pu_max": "min", + "v_mag_pu_min": "max", + "p_nom_max": "sum", + "s_nom_max": "sum", + "p_nom_min": "sum", + "s_nom_min": "sum", + 'v_ang_min': "max", + "v_ang_max":"min", + "terrain_factor":"mean", + "num_parallel": "sum", + "p_set": "sum", + "e_initial": "sum", + "e_nom": "sum", + "e_nom_max": "sum", + "e_nom_min": "sum", + "state_of_charge_initial": "sum", + "state_of_charge_set": "sum", + "inflow": "sum", + "p_max_pu": "first", + "x": "mean", + "y": "mean" +} + +def cluster_heat_buses(n): + """Cluster residential and service heat buses to one representative bus. + This can be done to save memory and speed up optimisation + """ + + def define_clustering(attributes, aggregate_dict): + """Define how attributes should be clustered. + Input: + attributes : pd.Index() + aggregate_dict: dictionary (key: name of attribute, value + clustering method) + + Returns: + agg : clustering dictionary + """ + keys = attributes.intersection(aggregate_dict.keys()) + agg = dict( + zip( + attributes.difference(keys), + ["first"] * len(df.columns.difference(keys)), + ) + ) + for key in keys: + agg[key] = aggregate_dict[key] + return agg + + logger.info("Cluster residential and service heat buses.") + components = ["Bus", "Carrier", "Generator", "Link", "Load", "Store"] + + for c in n.iterate_components(components): + df = c.df + cols = df.columns[df.columns.str.contains("bus") | (df.columns=="carrier")] + + # rename columns and index + df[cols] = (df[cols] + .apply(lambda x: x.str.replace("residential ","") + .str.replace("services ", ""), axis=1)) + df = df.rename(index=lambda x: x.replace("residential ","") + .replace("services ", "")) + + + # cluster heat nodes + # static dataframe + agg = define_clustering(df.columns, aggregate_dict) + df = df.groupby(level=0).agg(agg, **agg_group_kwargs) + # time-varying data + pnl = c.pnl + agg = define_clustering(pd.Index(pnl.keys()), aggregate_dict) + for k in pnl.keys(): + pnl[k].rename(columns=lambda x: x.replace("residential ","") + .replace("services ", ""), inplace=True) + pnl[k] = ( + pnl[k] + .groupby(level=0, axis=1) + .agg(agg[k], **agg_group_kwargs) + ) + + # remove unclustered assets of service/residential + to_drop = c.df.index.difference(df.index) + n.mremove(c.name, to_drop) + # add clustered assets + to_add = df.index.difference(c.df.index) + import_components_from_dataframe(n, df.loc[to_add], c.name) + + #%% if __name__ == "__main__": if 'snakemake' not in globals(): @@ -2467,4 +2564,6 @@ if __name__ == "__main__": if options['electricity_grid_connection']: add_electricity_grid_connection(n, costs) + if options["cluster_heat_buses"]: + cluster_heat_buses(n) n.export_to_netcdf(snakemake.output[0]) From cbab86c4bcf2227aaa34f9dfd198e62a6e0d2c27 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Mon, 1 Aug 2022 18:15:35 +0200 Subject: [PATCH 03/16] add heat buses clustering to myopic --- scripts/add_existing_baseyear.py | 5 ++++- scripts/prepare_sector_network.py | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index 11b8d49b..1cf532a3 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -12,7 +12,7 @@ import xarray as xr import pypsa import yaml -from prepare_sector_network import prepare_costs, define_spatial +from prepare_sector_network import prepare_costs, define_spatial, cluster_heat_buses from helper import override_component_attrs, update_config_with_sector_opts from types import SimpleNamespace @@ -495,4 +495,7 @@ if __name__ == "__main__": default_lifetime = snakemake.config['costs']['lifetime'] add_heating_capacities_installed_before_baseyear(n, baseyear, grouping_years, ashp_cop, gshp_cop, time_dep_hp_cop, costs, default_lifetime) + if options["cluster_heat_buses"]: + cluster_heat_buses(n) + n.export_to_netcdf(snakemake.output[0]) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 1eabc37d..c3b518e2 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2564,6 +2564,10 @@ if __name__ == "__main__": if options['electricity_grid_connection']: add_electricity_grid_connection(n, costs) - if options["cluster_heat_buses"]: + first_year_myopic = ((snakemake.config["foresight"] == 'myopic') and + (snakemake.config["scenario"]["planning_horizons"][0]==investment_year)) + + if options["cluster_heat_buses"] and not first_year_myopic: cluster_heat_buses(n) + n.export_to_netcdf(snakemake.output[0]) From 34a3d9aaad05706d73bb1a3857afd7a23087b1e2 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Tue, 2 Aug 2022 09:27:37 +0200 Subject: [PATCH 04/16] remove depreciated distutils.version --- scripts/prepare_sector_network.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index c3b518e2..c9494d74 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -27,9 +27,9 @@ logger = logging.getLogger(__name__) from types import SimpleNamespace spatial = SimpleNamespace() -from distutils.version import LooseVersion -pd_version = LooseVersion(pd.__version__) -agg_group_kwargs = dict(numeric_only=False) if pd_version >= "1.3" else {} +from packaging.version import Version, parse +pd_version = parse(pd.__version__) +agg_group_kwargs = dict(numeric_only=False) if pd_version >= Version("1.3") else {} def define_spatial(nodes, options): """ From 2eefba3b9520532eca4fa3d6e3039cf3867e090b Mon Sep 17 00:00:00 2001 From: Adam-Dvorak1 <92300992+Adam-Dvorak1@users.noreply.github.com> Date: Fri, 2 Dec 2022 15:58:57 +0100 Subject: [PATCH 05/16] Start refining Fixing typos --- doc/supply_demand.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/supply_demand.rst b/doc/supply_demand.rst index 71d79cf5..ca7db739 100644 --- a/doc/supply_demand.rst +++ b/doc/supply_demand.rst @@ -427,7 +427,7 @@ We assume that the primary route can be replaced by a third route in 2050, using FeO + H_2 \xrightarrow{} Fe + H_2O -This circumvents the process emissions associated with the use of coke. For hydrogen- based DRI, we assume energy requirements of 1.7 MWh :math:`_{H_2}` /t steel (Vogl et. al) `_ and 0.322 MWh :math:`_{el}`/t steel `(HYBRIT 2016) `_. +This circumvents the process emissions associated with the use of coke. For hydrogen- based DRI, we assume energy requirements of 1.7 MWh :math:`_{H_2}` /t steel `(Vogl et. al) `_ and 0.322 MWh :math:`_{el}`/t steel `(HYBRIT 2016) `_. The share of steel produced via the primary route is exogenously set in the `config file `_. The share of steel obtained via hydrogen-based DRI plus EAF is also set exogenously in the `config file `_. The remaining share is manufactured through the secondary route using scrap metal in EAF. Bioenergy as alternative to coke in blast furnaces is not considered in the model (`Mandova et.al `_, `Suopajärvi et.al `_). @@ -453,7 +453,7 @@ Statistics for the production of ammonia, which is commonly used as a fertilizer The Haber-Bosch process is not explicitly represented in the model, such that demand for ammonia enters the model as a demand for hydrogen ( 6.5 MWh :math:`_{H_2}` / t :math:`_{NH_3}` ) and electricity ( 1.17 MWh :math:`_{el}` /t :math:`_{NH_3}` ) (see `Wang et. al `_). Today, natural gas dominates in Europe as the source for the hydrogen used in the Haber-Bosch process, but the model can choose among the various hydrogen supply options described in the hydrogen section (see :ref:`Hydrogen supply`) -The total production and specific energy consumption of chlorine and methanol is taken from a `DECHEMA report `_. According to this source, the production of chlorine amounts to 9.58 MtCl/a, which is assumed to require electricity at 3.6 MWh `:math:`_{el}`/t of chlorine and yield hydrogen at 0.937 MWh :math:`_{H_2}`/t of chlorine in the chloralkali process. The production of methanol adds up to 1.5 MtMeOH/a, requiring electricity at 0.167 MWh :math:`_{el}`/t of methanol and methane at 10.25 MWh :math:`_{CH_4}`/t of methanol. +The total production and specific energy consumption of chlorine and methanol is taken from a `DECHEMA report `_. According to this source, the production of chlorine amounts to 9.58 MtCl/a, which is assumed to require electricity at 3.6 MWh :math:`_{el}`/t of chlorine and yield hydrogen at 0.937 MWh :math:`_{H_2}`/t of chlorine in the chloralkali process. The production of methanol adds up to 1.5 MtMeOH/a, requiring electricity at 0.167 MWh :math:`_{el}`/t of methanol and methane at 10.25 MWh :math:`_{CH_4}`/t of methanol. The production of ammonia, methanol, and chlorine production is deducted from the JRC IDEES basic chemicals, leaving the production totals of high-value chemicals. For this, we assume that the liquid hydrocarbon feedstock comes from synthetic or fossil- origin naphtha (14 MWh :math:`_{naphtha}`/t of HVC, similar to `Lechtenböhmer et al `_), ignoring the methanol-to-olefin route. Furthermore, we assume the following transformations of the energy-consuming processes in the production of plastics: the final energy consumption in steam processing is converted to methane since requires temperature above 500 °C (4.1 MWh :math:`_{CH_4}` /t of HVC, see `Rehfeldt et al. `_); and the remaining processes are electrified using the current efficiency of microwave for high-enthalpy heat processing, electric furnaces, electric process cooling and electric generic processes (2.85 MWh :math:`_{el}`/t of HVC). @@ -461,7 +461,7 @@ The production of ammonia, methanol, and chlorine production is deducted from th The process emissions from feedstock in the chemical industry are as high as 0.369 t :math:`_{CO_2}`/t of ethylene equivalent. We consider process emissions for all the material output, which is a conservative approach since it assumes that all plastic-embedded :math:`CO_2` will eventually be released into the atmosphere. However, plastic disposal in landfilling will avoid, or at least delay, associated :math:`CO_2` emissions. Circular economy practices drastically reduce the amount of primary feedstock needed for the production of plastics in the model (see `Kullmann et al. `_, `Meys et al. (2021) `_, `Meys et al. (2020) `_, `Gu et al. `_) and consequently, also the energy demands and level of process emission. The percentage of plastics that are assumed to be mechanically recycled can be selected in the `config file `_, as well as -the percentage that is chemically recycled, see `config file `_ The energy consumption for those recycling processes are respectively 0.547 MWh :math:`_{el}`/t of HVC (as indicated in the `config file `_) (`Meys et al. (2020) `_), and 6.9 MWh :math:`_{el}`/t of HVC (as indicated in the config file ``_) based on pyrolysis and electric steam cracking (see `Materials Economics `_ report). +the percentage that is chemically recycled, see `config file `_ The energy consumption for those recycling processes are respectively 0.547 MWh :math:`_{el}`/t of HVC (as indicated in the `config file `_) (`Meys et al. (2020) `_), and 6.9 MWh :math:`_{el}`/t of HVC (as indicated in the `config file `_) based on pyrolysis and electric steam cracking (see `Materials Economics `_ report). **Non-metallic Mineral Products** @@ -486,7 +486,7 @@ With the exception of electricity demand and biomass demand for low-temperature *Ceramics* -The ceramics sector is assumed to be fully electrified based on the current efficiency of already electrified processes which include microwave drying and sintering of raw materials, electric kilns for primary production processes, electric furnaces for the `product finishing `_. In total, the final electricity consumption is 0.44 MWh/t of ceramic. The manufacturing of ceramics includes process emissions of 0.03 t :math:`_{CO_2} `/t of ceramic. For a detailed overview of the ceramics industry sector see `Furszyfer Del Rio et al `_. +The ceramics sector is assumed to be fully electrified based on the current efficiency of already electrified processes which include microwave drying and sintering of raw materials, electric kilns for primary production processes, electric furnaces for the `product finishing `_. In total, the final electricity consumption is 0.44 MWh/t of ceramic. The manufacturing of ceramics includes process emissions of 0.03 t :math:`_{CO_2}`/t of ceramic. For a detailed overview of the ceramics industry sector see `Furszyfer Del Rio et al `_. *Glass* From bf0b52b8ae0b4304a5556898528629e860543b95 Mon Sep 17 00:00:00 2001 From: Adam-Dvorak1 <92300992+Adam-Dvorak1@users.noreply.github.com> Date: Fri, 2 Dec 2022 16:15:28 +0100 Subject: [PATCH 06/16] More refining --- doc/supply_demand.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/supply_demand.rst b/doc/supply_demand.rst index ca7db739..ce9f21dc 100644 --- a/doc/supply_demand.rst +++ b/doc/supply_demand.rst @@ -490,7 +490,7 @@ The ceramics sector is assumed to be fully electrified based on the current effi *Glass* -The production of glass is assumed to be fully electrified based on the current efficiency of electric melting tanks and electric annealing which adds up to an electricity demand of 2.07 MWh :math:`_{el}l/t` of `glass `_. The manufacturing of glass incurs process emissions of 0.1 t :math:`_{CO_2} `/t of glass. Potential efficiency improvements, which according to `Lechtenböhmer et al `_ could reduce energy demands to 0.85 MW :math:`_{el}`/t of glass, have not been considered. For a detailed overview of the glass industry sector see `Furszyfer Del Rio et al `_. +The production of glass is assumed to be fully electrified based on the current efficiency of electric melting tanks and electric annealing which adds up to an electricity demand of 2.07 MWh :math:`_{el}`/t of `glass `_. The manufacturing of glass incurs process emissions of 0.1 t :math:`_{CO_2}`/t of glass. Potential efficiency improvements, which according to `Lechtenböhmer et al `_ could reduce energy demands to 0.85 MW :math:`_{el}`/t of glass, have not been considered. For a detailed overview of the glass industry sector see `Furszyfer Del Rio et al `_. **Non-ferrous Metals** From 985cd8bae118d9dd94da19749f5c283f2afacdce Mon Sep 17 00:00:00 2001 From: Adam-Dvorak1 <92300992+Adam-Dvorak1@users.noreply.github.com> Date: Fri, 9 Dec 2022 10:37:41 +0100 Subject: [PATCH 07/16] Refine math in myopic.rst --- doc/myopic.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/myopic.rst b/doc/myopic.rst index af67fac7..bd51331d 100644 --- a/doc/myopic.rst +++ b/doc/myopic.rst @@ -81,12 +81,12 @@ Conventional carriers indicate carriers used in the existing conventional techno Options ============= The total carbon budget for the entire transition path can be indicated in the `sector_opts `_ in ``config.yaml``. The carbon budget can be split among the ``planning_horizons`` following an exponential or beta decay. -E.g. ``'cb40ex0'`` splits a carbon budget equal to 40 GtCO_2 following an exponential decay whose initial linear growth rate $r$ is zero. +E.g. ``'cb40ex0'`` splits a carbon budget equal to 40 Gt :math:`_{CO_2}` following an exponential decay whose initial linear growth rate r is zero. They can also follow some user-specified path, if defined `here `_. The paper `Speed of technological transformations required in Europe to achieve different climate goals (2022) `__ defines CO_2 budgets corresponding to global temperature increases (1.5C – 2C) as response to the emissions. Here, global carbon budgets are converted to European budgets assuming equal-per capita distribution which translates into a 6.43% share for Europe. The carbon budgets are in this paper distributed throughout the transition paths assuming an exponential decay. Emissions e(t) in every year t are limited by .. math:: - e(t) = e_0 (1+ (r+m)t) e^(-mt) + e(t) = e_0 (1+ (r+m)t) e^{-mt} where r is the initial linear growth rate, which here is assumed to be r=0, and the decay parameter m is determined by imposing the integral of the path to be equal to the budget for Europe. Following this approach, the CO_2 budget is defined. Following the same approach as in this paper, add the following to the ``scenario.sector_opts`` E.g. ``-cb25.7ex0`` (1.5C increase) From a870b603f45f30c35291f6fb4a5a8c5b6add0032 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Sat, 28 Jan 2023 08:15:43 +0100 Subject: [PATCH 08/16] automatically retrieve technology-data, no git clone --- .github/workflows/ci.yaml | 3 +-- Snakefile | 24 ++++++++++++++++-------- config.default.yaml | 3 ++- doc/installation.rst | 9 --------- test/config.myopic.yaml | 3 ++- test/config.overnight.yaml | 3 ++- 6 files changed, 23 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index da984fd3..648e5cc2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -52,10 +52,9 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Clone pypsa-eur and technology-data repositories + - name: Clone pypsa-eur subworkflow run: | git clone https://github.com/pypsa/pypsa-eur ../pypsa-eur - git clone https://github.com/pypsa/technology-data ../technology-data cp ../pypsa-eur/test/config.test1.yaml ../pypsa-eur/config.yaml - name: Setup secrets diff --git a/Snakefile b/Snakefile index 90fcfb56..95e79502 100644 --- a/Snakefile +++ b/Snakefile @@ -1,6 +1,6 @@ from os.path import exists -from shutil import copyfile +from shutil import copyfile, move from snakemake.remote.HTTP import RemoteProvider as HTTPRemoteProvider HTTP = HTTPRemoteProvider() @@ -21,7 +21,6 @@ wildcard_constraints: SDIR = config['summary_dir'] + '/' + config['run'] RDIR = config['results_dir'] + config['run'] -CDIR = config['costs_dir'] subworkflow pypsaeur: @@ -72,6 +71,15 @@ if config.get('retrieve_sector_databundle', True): script: 'scripts/retrieve_sector_databundle.py' +if config.get("retrieve_cost_data", True): + rule retrieve_cost_data: + input: HTTP.remote("raw.githubusercontent.com/PyPSA/technology-data/{}/outputs/".format(config['costs']['version']) + "costs_{year}.csv", keep_local=True) + output: "data/costs_{year}.csv" + log: "logs/" + RDIR + "retrieve_cost_data_{year}.log", + resources: mem_mb=1000, + run: move(input[0], output[0]) + + rule build_population_layouts: input: nuts3_shapes=pypsaeur('resources/nuts3_shapes.geojson'), @@ -483,7 +491,7 @@ rule prepare_sector_network: co2="data/eea/UNFCCC_v23.csv", biomass_potentials='resources/biomass_potentials_s{simpl}_{clusters}.csv', heat_profile="data/heat_load_profile_BDEW.csv", - costs=CDIR + "costs_{}.csv".format(config['costs']['year']) if config["foresight"] == "overnight" else CDIR + "costs_{planning_horizons}.csv", + costs="data/costs_{}.csv".format(config['costs']['year']) if config["foresight"] == "overnight" else "data/costs_{planning_horizons}.csv", profile_offwind_ac=pypsaeur("resources/profile_offwind-ac.nc"), profile_offwind_dc=pypsaeur("resources/profile_offwind-dc.nc"), h2_cavern="resources/salt_cavern_potentials_s{simpl}_{clusters}.csv", @@ -557,7 +565,7 @@ rule make_summary: RDIR + "/postnetworks/elec_s{simpl}_{clusters}_lv{lv}_{opts}_{sector_opts}_{planning_horizons}.nc", **config['scenario'] ), - costs=CDIR + "costs_{}.csv".format(config['costs']['year']) if config["foresight"] == "overnight" else CDIR + "costs_{}.csv".format(config['scenario']['planning_horizons'][0]), + costs="data/costs_{}.csv".format(config['costs']['year']) if config["foresight"] == "overnight" else "data/costs_{}.csv".format(config['scenario']['planning_horizons'][0]), plots=expand( RDIR + "/maps/elec_s{simpl}_{clusters}_lv{lv}_{opts}_{sector_opts}-costs-all_{planning_horizons}.pdf", **config['scenario'] @@ -607,7 +615,7 @@ if config["foresight"] == "overnight": input: overrides="data/override_component_attrs", network=RDIR + "/prenetworks/elec_s{simpl}_{clusters}_lv{lv}_{opts}_{sector_opts}_{planning_horizons}.nc", - costs=CDIR + "costs_{}.csv".format(config['costs']['year']), + costs="data/costs_{}.csv".format(config['costs']['year']), config=SDIR + '/configs/config.yaml', #env=SDIR + '/configs/environment.yaml', output: RDIR + "/postnetworks/elec_s{simpl}_{clusters}_lv{lv}_{opts}_{sector_opts}_{planning_horizons}.nc" @@ -632,7 +640,7 @@ if config["foresight"] == "myopic": busmap_s=pypsaeur("resources/busmap_elec_s{simpl}.csv"), busmap=pypsaeur("resources/busmap_elec_s{simpl}_{clusters}.csv"), clustered_pop_layout="resources/pop_layout_elec_s{simpl}_{clusters}.csv", - costs=CDIR + "costs_{}.csv".format(config['scenario']['planning_horizons'][0]), + costs="data/costs_{}.csv".format(config['scenario']['planning_horizons'][0]), cop_soil_total="resources/cop_soil_total_elec_s{simpl}_{clusters}.nc", cop_air_total="resources/cop_air_total_elec_s{simpl}_{clusters}.nc", existing_heating='data/existing_infrastructure/existing_heating_raw.csv', @@ -661,7 +669,7 @@ if config["foresight"] == "myopic": overrides="data/override_component_attrs", network=RDIR + '/prenetworks/elec_s{simpl}_{clusters}_lv{lv}_{opts}_{sector_opts}_{planning_horizons}.nc', network_p=solved_previous_horizon, #solved network at previous time step - costs=CDIR + "costs_{planning_horizons}.csv", + costs="data/costs_{planning_horizons}.csv", cop_soil_total="resources/cop_soil_total_elec_s{simpl}_{clusters}.nc", cop_air_total="resources/cop_air_total_elec_s{simpl}_{clusters}.nc" output: RDIR + "/prenetworks-brownfield/elec_s{simpl}_{clusters}_lv{lv}_{opts}_{sector_opts}_{planning_horizons}.nc" @@ -678,7 +686,7 @@ if config["foresight"] == "myopic": input: overrides="data/override_component_attrs", network=RDIR + "/prenetworks-brownfield/elec_s{simpl}_{clusters}_lv{lv}_{opts}_{sector_opts}_{planning_horizons}.nc", - costs=CDIR + "costs_{planning_horizons}.csv", + costs="data/costs_{planning_horizons}.csv", config=SDIR + '/configs/config.yaml' output: RDIR + "/postnetworks/elec_s{simpl}_{clusters}_lv{lv}_{opts}_{sector_opts}_{planning_horizons}.nc" shadow: "shallow" diff --git a/config.default.yaml b/config.default.yaml index ee1c5059..27cbcba7 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -3,10 +3,10 @@ version: 0.6.0 logging_level: INFO retrieve_sector_databundle: true +retrieve_cost_data: true results_dir: results/ summary_dir: results -costs_dir: ../technology-data/outputs/ run: your-run-name # use this to keep track of runs with different settings foresight: overnight # options are overnight, myopic, perfect (perfect is not yet implemented) # if you use myopic or perfect foresight, set the investment years in "planning_horizons" below @@ -338,6 +338,7 @@ industry: costs: year: 2030 + version: v0.4.0 lifetime: 25 #default lifetime # From a Lion Hirth paper, also reflects average of Noothout et al 2016 discountrate: 0.07 diff --git a/doc/installation.rst b/doc/installation.rst index f5cb7c7a..029f06ee 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -25,15 +25,6 @@ then download and unpack all the PyPSA-Eur data files by running the following s projects/pypsa-eur % snakemake -j 1 retrieve_databundle -Clone technology-data repository -================================ - -Next install the technology assumptions database `technology-data `_ by creating a parallel directory: - -.. code:: bash - - projects % git clone https://github.com/PyPSA/technology-data.git - Clone PyPSA-Eur-Sec repository ============================== diff --git a/test/config.myopic.yaml b/test/config.myopic.yaml index d0a6a918..934a4527 100644 --- a/test/config.myopic.yaml +++ b/test/config.myopic.yaml @@ -3,10 +3,10 @@ version: 0.6.0 logging_level: INFO retrieve_sector_databundle: true +retrieve_cost_data: true results_dir: results/ summary_dir: results -costs_dir: ../technology-data/outputs/ run: test-myopic # use this to keep track of runs with different settings foresight: myopic # options are overnight, myopic, perfect (perfect is not yet implemented) # if you use myopic or perfect foresight, set the investment years in "planning_horizons" below @@ -320,6 +320,7 @@ industry: costs: year: 2030 + version: v0.4.0 lifetime: 25 #default lifetime # From a Lion Hirth paper, also reflects average of Noothout et al 2016 discountrate: 0.07 diff --git a/test/config.overnight.yaml b/test/config.overnight.yaml index 1dc314dd..a92596e4 100644 --- a/test/config.overnight.yaml +++ b/test/config.overnight.yaml @@ -3,10 +3,10 @@ version: 0.6.0 logging_level: INFO retrieve_sector_databundle: true +retrieve_cost_data: true results_dir: results/ summary_dir: results -costs_dir: ../technology-data/outputs/ run: test-overnight # use this to keep track of runs with different settings foresight: overnight # options are overnight, myopic, perfect (perfect is not yet implemented) # if you use myopic or perfect foresight, set the investment years in "planning_horizons" below @@ -318,6 +318,7 @@ industry: costs: year: 2030 + version: v0.4.0 lifetime: 25 #default lifetime # From a Lion Hirth paper, also reflects average of Noothout et al 2016 discountrate: 0.07 From 984ab9350c4c05ecad3f9a3be7c628a4d478be59 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 30 Jan 2023 11:54:19 +0100 Subject: [PATCH 09/16] Update scripts/prepare_sector_network.py --- scripts/prepare_sector_network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index e07c672f..89d774ca 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2920,7 +2920,7 @@ if __name__ == "__main__": first_year_myopic = ((snakemake.config["foresight"] == 'myopic') and (snakemake.config["scenario"]["planning_horizons"][0]==investment_year)) - if options["cluster_heat_buses"] and not first_year_myopic: + if options.get("cluster_heat_buses", False) and not first_year_myopic: cluster_heat_buses(n) From 4a2da0a5f4d51c3bb725348a4c6bf14def11bd35 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 30 Jan 2023 11:55:40 +0100 Subject: [PATCH 10/16] Update scripts/add_existing_baseyear.py --- scripts/add_existing_baseyear.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index 3618f441..8e274d62 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -563,6 +563,9 @@ if __name__ == "__main__": add_heating_capacities_installed_before_baseyear(n, baseyear, grouping_years_heat, ashp_cop, gshp_cop, time_dep_hp_cop, costs, default_lifetime) + if options.get("cluster_heat_buses", False): + cluster_heat_buses(n) + n.meta = dict(snakemake.config, **dict(wildcards=dict(snakemake.wildcards))) n.export_to_netcdf(snakemake.output[0]) From 7982a37b6de6d85cb531ad14e185c988c85b2e9f Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Mon, 30 Jan 2023 11:56:01 +0100 Subject: [PATCH 11/16] Update config.default.yaml --- config.default.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.default.yaml b/config.default.yaml index 5ab444e3..e165e895 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -160,7 +160,7 @@ sector: 2040: 0.6 2050: 1.0 district_heating_loss: 0.15 - cluster_heat_buses: False # cluster residential and service heat buses to one to save memory + cluster_heat_buses: false # cluster residential and service heat buses to one to save memory bev_dsm_restriction_value: 0.75 #Set to 0 for no restriction on BEV DSM bev_dsm_restriction_time: 7 #Time at which SOC of BEV has to be dsm_restriction_value transport_heating_deadband_upper: 20. From 01db4f4408434ae705e88874b452e49740ec1e0d Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Fri, 10 Feb 2023 07:49:45 +0100 Subject: [PATCH 12/16] compatibility with technology-data v0.5 --- config.default.yaml | 2 +- scripts/prepare_sector_network.py | 2 +- test/config.myopic.yaml | 2 +- test/config.overnight.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config.default.yaml b/config.default.yaml index 458309f6..f9698f9f 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -339,7 +339,7 @@ industry: costs: year: 2030 - version: v0.4.0 + version: v0.5.0 lifetime: 25 #default lifetime # From a Lion Hirth paper, also reflects average of Noothout et al 2016 discountrate: 0.07 diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 89d774ca..a4e9f790 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -997,7 +997,7 @@ def add_storage_and_grids(n, costs): ) # hydrogen stored overground (where not already underground) - h2_capital_cost = costs.at["hydrogen storage tank incl. compressor", "fixed"] + h2_capital_cost = costs.at["hydrogen storage tank type 1 including compressor", "fixed"] nodes_overground = h2_caverns.index.symmetric_difference(nodes) n.madd("Store", diff --git a/test/config.myopic.yaml b/test/config.myopic.yaml index 934a4527..0228c47a 100644 --- a/test/config.myopic.yaml +++ b/test/config.myopic.yaml @@ -320,7 +320,7 @@ industry: costs: year: 2030 - version: v0.4.0 + version: v0.5.0 lifetime: 25 #default lifetime # From a Lion Hirth paper, also reflects average of Noothout et al 2016 discountrate: 0.07 diff --git a/test/config.overnight.yaml b/test/config.overnight.yaml index a92596e4..d50489f3 100644 --- a/test/config.overnight.yaml +++ b/test/config.overnight.yaml @@ -318,7 +318,7 @@ industry: costs: year: 2030 - version: v0.4.0 + version: v0.5.0 lifetime: 25 #default lifetime # From a Lion Hirth paper, also reflects average of Noothout et al 2016 discountrate: 0.07 From d54894eecc9198a955d202f8e57fa942d8f181dd Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Sat, 11 Feb 2023 09:37:09 +0100 Subject: [PATCH 13/16] pandas update: closed to inclusive --- config.default.yaml | 2 +- test/config.myopic.yaml | 2 +- test/config.overnight.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config.default.yaml b/config.default.yaml index 458309f6..272b9c12 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -66,7 +66,7 @@ snapshots: # arguments to pd.date_range start: "2013-01-01" end: "2014-01-01" - closed: left # end is not inclusive + inclusive: left # end is not inclusive atlite: cutout: ../pypsa-eur/cutouts/europe-2013-era5.nc diff --git a/test/config.myopic.yaml b/test/config.myopic.yaml index 934a4527..35217c5f 100644 --- a/test/config.myopic.yaml +++ b/test/config.myopic.yaml @@ -61,7 +61,7 @@ snapshots: # arguments to pd.date_range start: "2013-03-01" end: "2013-04-01" - closed: left # end is not inclusive + inclusive: left # end is not inclusive atlite: cutout: ../pypsa-eur/cutouts/be-03-2013-era5.nc diff --git a/test/config.overnight.yaml b/test/config.overnight.yaml index a92596e4..bba37adb 100644 --- a/test/config.overnight.yaml +++ b/test/config.overnight.yaml @@ -59,7 +59,7 @@ snapshots: # arguments to pd.date_range start: "2013-03-01" end: "2013-04-01" - closed: left # end is not inclusive + inclusive: left # end is not inclusive atlite: cutout: ../pypsa-eur/cutouts/be-03-2013-era5.nc From 5d08dfc2ec9ee3f3e2a3653d67d7e273e9294b10 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 15 Feb 2023 13:47:57 +0100 Subject: [PATCH 14/16] add min part load for FT and methanolisation via p_min_pu --- config.default.yaml | 2 ++ doc/release_notes.rst | 2 ++ scripts/prepare_sector_network.py | 2 ++ 3 files changed, 6 insertions(+) diff --git a/config.default.yaml b/config.default.yaml index e939f9c0..092fafa4 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -261,6 +261,8 @@ sector: - nearshore # within 50 km of sea # - offshore ammonia: false # can be false (no NH3 carrier), true (copperplated NH3), "regional" (regionalised NH3 without network) + min_part_load_fischer_tropsch: 0.9 # p_min_pu + min_part_load_methanolisation: 0.5 # p_min_pu use_fischer_tropsch_waste_heat: true use_fuel_cell_waste_heat: true electricity_distribution_grid: true diff --git a/doc/release_notes.rst b/doc/release_notes.rst index fd9a3549..1eef24cb 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -65,6 +65,8 @@ incorporates retrofitting options to hydrogen. * Add option for BtL (Biomass to liquid fuel/oil) with and without CC +* Add option for minimum part load for Fischer-Tropsch plants (default: 90%) and methanolisation plants (default: 50%). + * Units are assigned to the buses. These only provide a better understanding. The specifications of the units are not taken into account in the optimisation, which means that no automatic conversion of units takes place. * Option ``retrieve_sector_databundle`` to automatically retrieve and extract data bundle. diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index a4e9f790..5507a201 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2227,6 +2227,7 @@ def add_industry(n, costs): bus3=spatial.co2.nodes, carrier="methanolisation", p_nom_extendable=True, + p_min_pu=options.get("min_part_load_methanolisation", 0), capital_cost=costs.at["methanolisation", 'fixed'] * options["MWh_MeOH_per_MWh_H2"], # EUR/MW_H2/a lifetime=costs.at["methanolisation", 'lifetime'], efficiency=options["MWh_MeOH_per_MWh_H2"], @@ -2334,6 +2335,7 @@ def add_industry(n, costs): capital_cost=costs.at["Fischer-Tropsch", 'fixed'] * costs.at["Fischer-Tropsch", 'efficiency'], # EUR/MW_H2/a efficiency2=-costs.at["oil", 'CO2 intensity'] * costs.at["Fischer-Tropsch", 'efficiency'], p_nom_extendable=True, + p_min_pu=options.get("min_part_load_fischer_tropsch", 0), lifetime=costs.at['Fischer-Tropsch', 'lifetime'] ) From b1b289fc454ff01641dde8298f4fca8061fbf700 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 15 Feb 2023 14:01:00 +0100 Subject: [PATCH 15/16] add option to use electrolysis waste heat in district heating --- config.default.yaml | 1 + scripts/prepare_sector_network.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/config.default.yaml b/config.default.yaml index e939f9c0..e639938a 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -263,6 +263,7 @@ sector: ammonia: false # can be false (no NH3 carrier), true (copperplated NH3), "regional" (regionalised NH3 without network) use_fischer_tropsch_waste_heat: true use_fuel_cell_waste_heat: true + use_electrolysis_waste_heat: false electricity_distribution_grid: true electricity_distribution_grid_cost_factor: 1.0 #multiplies cost in data/costs.csv electricity_grid_connection: true # only applies to onshore wind and utility PV diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index a4e9f790..f5c3e225 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2470,6 +2470,11 @@ def add_waste_heat(n): n.links.loc[urban_central + " Fischer-Tropsch", "bus3"] = urban_central + " urban central heat" n.links.loc[urban_central + " Fischer-Tropsch", "efficiency3"] = 0.95 - n.links.loc[urban_central + " Fischer-Tropsch", "efficiency"] + # TODO integrate useable waste heat efficiency into technology-data from DEA + if options.get('use_electrolysis_waste_heat', False): + n.links.loc[urban_central + " H2 Electrolysis", "bus2"] = urban_central + " urban central heat" + n.links.loc[urban_central + " H2 Electrolysis", "efficiency2"] = 0.84 - n.links.loc[urban_central + " H2 Electrolysis", "efficiency"] + if options['use_fuel_cell_waste_heat']: n.links.loc[urban_central + " H2 Fuel Cell", "bus2"] = urban_central + " urban central heat" n.links.loc[urban_central + " H2 Fuel Cell", "efficiency2"] = 0.95 - n.links.loc[urban_central + " H2 Fuel Cell", "efficiency"] From d2f2af9245a2f750c09092caeb362c862230a87f Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Wed, 15 Feb 2023 19:04:52 +0100 Subject: [PATCH 16/16] add release note --- doc/release_notes.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index fd9a3549..966cdc88 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -69,6 +69,8 @@ incorporates retrofitting options to hydrogen. * Option ``retrieve_sector_databundle`` to automatically retrieve and extract data bundle. +* Add option to use waste heat of electrolysis in district heating networks (``use_electrolysis_waste_heat``). + * Add regionalised hydrogen salt cavern storage potentials from `Technical Potential of Salt Caverns for Hydrogen Storage in Europe `_. * Add option to sweep the global CO2 sequestration potentials with keyword ``seq200`` in the ``{sector_opts}`` wildcard (for limit of 200 Mt CO2).