diff --git a/.gitignore b/.gitignore index 1401c0ad..ac252a84 100644 --- a/.gitignore +++ b/.gitignore @@ -28,7 +28,7 @@ gurobi.log /data/.nfs* /data/Industrial_Database.csv /data/retro/tabula-calculator-calcsetbuilding.csv - +/data *.org *.nc diff --git a/Snakefile b/Snakefile index b91785d9..de34f386 100644 --- a/Snakefile +++ b/Snakefile @@ -220,10 +220,10 @@ rule build_industrial_production_per_country_tomorrow: input: industrial_production_per_country="resources/industrial_production_per_country.csv" output: - industrial_production_per_country_tomorrow="resources/industrial_production_per_country_tomorrow.csv" + industrial_production_per_country_tomorrow="resources/industrial_production_per_country_tomorrow_{planning_horizons}.csv" threads: 1 resources: mem_mb=1000 - benchmark: "benchmarks/build_industrial_production_per_country_tomorrow" + benchmark: "benchmarks/build_industrial_production_per_country_tomorrow_{planning_horizons}" script: 'scripts/build_industrial_production_per_country_tomorrow.py' @@ -243,25 +243,25 @@ rule build_industrial_distribution_key: rule build_industrial_production_per_node: input: industrial_distribution_key="resources/industrial_distribution_key_elec_s{simpl}_{clusters}.csv", - industrial_production_per_country_tomorrow="resources/industrial_production_per_country_tomorrow.csv" + industrial_production_per_country_tomorrow="resources/industrial_production_per_country_tomorrow_{planning_horizons}.csv" output: - industrial_production_per_node="resources/industrial_production_elec_s{simpl}_{clusters}.csv" + industrial_production_per_node="resources/industrial_production_elec_s{simpl}_{clusters}_{planning_horizons}.csv" threads: 1 resources: mem_mb=1000 - benchmark: "benchmarks/build_industrial_production_per_node/s{simpl}_{clusters}" + benchmark: "benchmarks/build_industrial_production_per_node/s{simpl}_{clusters}_{planning_horizons}" script: 'scripts/build_industrial_production_per_node.py' rule build_industrial_energy_demand_per_node: input: industry_sector_ratios="resources/industry_sector_ratios.csv", - industrial_production_per_node="resources/industrial_production_elec_s{simpl}_{clusters}.csv", + industrial_production_per_node="resources/industrial_production_elec_s{simpl}_{clusters}_{planning_horizons}.csv", industrial_energy_demand_per_node_today="resources/industrial_energy_demand_today_elec_s{simpl}_{clusters}.csv" output: - industrial_energy_demand_per_node="resources/industrial_energy_demand_elec_s{simpl}_{clusters}.csv" + industrial_energy_demand_per_node="resources/industrial_energy_demand_elec_s{simpl}_{clusters}_{planning_horizons}.csv" threads: 1 resources: mem_mb=1000 - benchmark: "benchmarks/build_industrial_energy_demand_per_node/s{simpl}_{clusters}" + benchmark: "benchmarks/build_industrial_energy_demand_per_node/s{simpl}_{clusters}_{planning_horizons}" script: 'scripts/build_industrial_energy_demand_per_node.py' @@ -333,7 +333,7 @@ rule prepare_sector_network: busmap=pypsaeur("resources/busmap_elec_s{simpl}_{clusters}.csv"), clustered_pop_layout="resources/pop_layout_elec_s{simpl}_{clusters}.csv", simplified_pop_layout="resources/pop_layout_elec_s{simpl}.csv", - industrial_demand="resources/industrial_energy_demand_elec_s{simpl}_{clusters}.csv", + industrial_demand="resources/industrial_energy_demand_elec_s{simpl}_{clusters}_{planning_horizons}.csv", heat_demand_urban="resources/heat_demand_urban_elec_s{simpl}_{clusters}.nc", heat_demand_rural="resources/heat_demand_rural_elec_s{simpl}_{clusters}.nc", heat_demand_total="resources/heat_demand_total_elec_s{simpl}_{clusters}.nc", diff --git a/config.default.yaml b/config.default.yaml index bb60509a..35e6ad81 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -30,6 +30,7 @@ scenario: # B for biomass supply, I for industry, shipping and aviation # solar+c0.5 reduces the capital cost of solar to 50\% of reference value # solar+p3 multiplies the available installable potential by factor 3 + # co2 stored+e2 multiplies the potential of CO2 sequestration by a factor 2 # dist{n} includes distribution grids with investment cost of n times cost in data/costs.csv # for myopic/perfect foresight cb states the carbon budget in GtCO2 (cumulative # emissions throughout the transition path in the timeframe determined by the @@ -177,6 +178,15 @@ sector: agriculture_machinery_fuel_efficiency: 0.7 # for conversion of fuel oil to use agriculture_machinery_electric_efficiency: 0.3 # for conversion of electricity to use shipping_average_efficiency: 0.4 #For conversion of fuel oil to propulsion in 2011 + shipping_hydrogen_liquefaction: true # whether to consider liquefaction costs for shipping H2 demands + shipping_hydrogen_share: # 1 means all hydrogen FC + 2020: 0 + 2025: 0 + 2030: 0.05 + 2035: 0.15 + 2040: 0.3 + 2045: 0.6 + 2050: 1 time_dep_hp_cop: true #time dependent heat pump coefficient of performance heat_pump_sink_T: 55. # Celsius, based on DTU / large area radiators; used in build_cop_profiles.py # conservatively high to cover hot water and space heating in poorly-insulated buildings @@ -231,10 +241,32 @@ sector: industry: - St_primary_fraction: 0.3 # fraction of steel produced via primary route (DRI + EAF) versus secondary route (EAF); today fraction is 0.6 + St_primary_fraction: # fraction of steel produced via primary route versus secondary route (scrap+EAF); today fraction is 0.6 + 2020: 0.6 + 2025: 0.55 + 2030: 0.5 + 2035: 0.45 + 2040: 0.4 + 2045: 0.35 + 2050: 0.3 + DRI_fraction: # fraction of the primary route converted to DRI + EAF + 2020: 0 + 2025: 0 + 2030: 0.05 + 2035: 0.2 + 2040: 0.4 + 2045: 0.7 + 2050: 1 H2_DRI: 1.7 #H2 consumption in Direct Reduced Iron (DRI), MWh_H2,LHV/ton_Steel from 51kgH2/tSt in Vogl et al (2018) doi:10.1016/j.jclepro.2018.08.279 elec_DRI: 0.322 #electricity consumption in Direct Reduced Iron (DRI) shaft, MWh/tSt HYBRIT brochure https://ssabwebsitecdn.azureedge.net/-/media/hybrit/files/hybrit_brochure.pdf - Al_primary_fraction: 0.2 # fraction of aluminium produced via the primary route versus scrap; today fraction is 0.4 + Al_primary_fraction: # fraction of aluminium produced via the primary route versus scrap; today fraction is 0.4 + 2020: 0.4 + 2025: 0.375 + 2030: 0.35 + 2035: 0.325 + 2040: 0.3 + 2045: 0.25 + 2050: 0.2 MWh_CH4_per_tNH3_SMR: 10.8 # 2012's demand from https://ec.europa.eu/docsroom/documents/4165/attachments/1/translations/en/renditions/pdf MWh_elec_per_tNH3_SMR: 0.7 # same source, assuming 94-6% split methane-elec of total energy demand 11.5 MWh/tNH3 MWh_H2_per_tNH3_electrolysis: 6.5 # from https://doi.org/10.1016/j.joule.2018.04.017, around 0.197 tH2/tHN3 (>3/17 since some H2 lost and used for energy) @@ -474,4 +506,8 @@ plotting: solid biomass: '#DAA520' today: '#D2691E' shipping: '#6495ED' + shipping oil: "#6495ED" + shipping oil emissions: "#6495ED" electricity distribution grid: '#333333' + H2 for industry: "#222222" + H2 for shipping: "#6495ED" diff --git a/doc/release_notes.rst b/doc/release_notes.rst index bd99ce16..15419438 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -60,8 +60,12 @@ Future release These are included in the environment specifications of PyPSA-Eur. * Consistent use of ``__main__`` block and further unspecific code cleaning. * Distinguish costs for home battery storage and inverter from utility-scale battery costs. - - +* Added option for hydrogen liquefaction costs for hydrogen demand in shipping. + This introduces a new ``H2 liquid`` bus at each location. + It is activated via ``sector: shipping_hydrogen_liquefaction: true``. +* The share of shipping transformed into hydrogen fuel cell can be now defined for different years in the ``config.yaml`` file. The carbon emission from the remaining share is treated as a negative load on the atmospheric carbon dioxide bus, just like aviation and land transport emissions. +* The transformation of the Steel and Aluminium production can be now defined for different years in the ``config.yaml`` file. +* Include the option to alter the maximum energy capacity of a store via the ``carrier+factor`` in the ``{sector_opts}`` wildcard. This can be useful for sensitivity analyses. Example: ``co2 stored+e2`` multiplies the ``e_nom_max`` by factor 2. In this example, ``e_nom_max`` represents the CO2 sequestration potential in Europe. PyPSA-Eur-Sec 0.5.0 (21st May 2021) =================================== diff --git a/scripts/build_industrial_production_per_country_tomorrow.py b/scripts/build_industrial_production_per_country_tomorrow.py index 767779f8..ba69e0a6 100644 --- a/scripts/build_industrial_production_per_country_tomorrow.py +++ b/scripts/build_industrial_production_per_country_tomorrow.py @@ -2,6 +2,8 @@ import pandas as pd +from prepare_sector_network import get + if __name__ == '__main__': if 'snakemake' not in globals(): from helper import mock_snakemake @@ -9,27 +11,35 @@ if __name__ == '__main__': config = snakemake.config["industry"] + investment_year = int(snakemake.wildcards.planning_horizons) + fn = snakemake.input.industrial_production_per_country production = pd.read_csv(fn, index_col=0) keys = ["Integrated steelworks", "Electric arc"] total_steel = production[keys].sum(axis=1) + st_primary_fraction = get(config["St_primary_fraction"], investment_year) + dri_fraction = get(config["DRI_fraction"], investment_year) int_steel = production["Integrated steelworks"].sum() - fraction_persistent_primary = config["St_primary_fraction"] * total_steel.sum() / int_steel + fraction_persistent_primary = st_primary_fraction * total_steel.sum() / int_steel - dri = fraction_persistent_primary * production["Integrated steelworks"] + dri = dri_fraction * fraction_persistent_primary * production["Integrated steelworks"] production.insert(2, "DRI + Electric arc", dri) - production["Electric arc"] = total_steel - production["DRI + Electric arc"] - production["Integrated steelworks"] = 0. + not_dri = (1 - dri_fraction) + production["Integrated steelworks"] = not_dri * fraction_persistent_primary * production["Integrated steelworks"] + production["Electric arc"] = total_steel - production["DRI + Electric arc"] - production["Integrated steelworks"] keys = ["Aluminium - primary production", "Aluminium - secondary production"] total_aluminium = production[keys].sum(axis=1) key_pri = "Aluminium - primary production" key_sec = "Aluminium - secondary production" - fraction_persistent_primary = config["Al_primary_fraction"] * total_aluminium.sum() / production[key_pri].sum() + + al_primary_fraction = get(config["Al_primary_fraction"], investment_year) + fraction_persistent_primary = al_primary_fraction * total_aluminium.sum() / production[key_pri].sum() + production[key_pri] = fraction_persistent_primary * production[key_pri] production[key_sec] = total_aluminium - production[key_pri] diff --git a/scripts/plot_summary.py b/scripts/plot_summary.py index 45a442a1..5b045688 100644 --- a/scripts/plot_summary.py +++ b/scripts/plot_summary.py @@ -34,7 +34,9 @@ def rename_techs(label): rename_if_contains_dict = { "water tanks": "hot water storage", "retrofitting": "building retrofitting", - "H2": "hydrogen storage", + "H2 Electrolysis": "hydrogen storage", + "H2 Fuel Cell": "hydrogen storage", + "H2 pipeline": "hydrogen storage", "battery": "battery storage", "CC": "CC" } diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index e90a0e91..374da7de 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -911,7 +911,7 @@ def add_storage(n, costs): ) # hydrogen stored overground (where not already underground) - h2_capital_cost = costs.at["hydrogen storage tank", "fixed"] + h2_capital_cost = costs.at["hydrogen storage tank incl. compressor", "fixed"] nodes_overground = cavern_nodes.index.symmetric_difference(nodes) n.madd("Store", @@ -946,9 +946,9 @@ def add_storage(n, costs): p_min_pu=-1, p_nom_extendable=True, length=h2_links.length.values, - capital_cost=costs.at['H2 pipeline', 'fixed'] * h2_links.length.values, + capital_cost=costs.at['H2 (g) pipeline', 'fixed'] * h2_links.length.values, carrier="H2 pipeline", - lifetime=costs.at['H2 pipeline', 'lifetime'] + lifetime=costs.at['H2 (g) pipeline', 'lifetime'] ) n.add("Carrier", "battery") @@ -1002,7 +1002,7 @@ def add_storage(n, costs): carrier="Sabatier", efficiency=costs.at["methanation", "efficiency"], efficiency2=-costs.at["methanation", "efficiency"] * costs.at['gas', 'CO2 intensity'], - capital_cost=costs.at["methanation", "fixed"], + capital_cost=costs.at["methanation", "fixed"] * costs.at["methanation", "efficiency"], # costs given per kW_gas lifetime=costs.at['methanation', 'lifetime'] ) @@ -1719,18 +1719,66 @@ def add_industry(n, costs): p_set=industrial_demand.loc[nodes, "hydrogen"] / 8760 ) + if options["shipping_hydrogen_liquefaction"]: + + n.madd("Bus", + nodes, + suffix=" H2 liquid", + carrier="H2 liquid", + location=nodes + ) + + n.madd("Link", + nodes + " H2 liquefaction", + bus0=nodes + " H2", + bus1=nodes + " H2 liquid", + carrier="H2 liquefaction", + efficiency=costs.at["H2 liquefaction", 'efficiency'], + capital_cost=costs.at["H2 liquefaction", 'fixed'], + p_nom_extendable=True, + lifetime=costs.at['H2 liquefaction', 'lifetime'] + ) + + shipping_bus = nodes + " H2 liquid" + else: + shipping_bus = nodes + " H2" + all_navigation = ["total international navigation", "total domestic navigation"] efficiency = options['shipping_average_efficiency'] / costs.at["fuel cell", "efficiency"] - p_set = nodal_energy_totals.loc[nodes, all_navigation].sum(axis=1) * 1e6 * efficiency / 8760 + shipping_hydrogen_share = get(options['shipping_hydrogen_share'], investment_year) + p_set = shipping_hydrogen_share * nodal_energy_totals.loc[nodes, all_navigation].sum(axis=1) * 1e6 * efficiency / 8760 n.madd("Load", nodes, suffix=" H2 for shipping", - bus=nodes + " H2", + bus=shipping_bus, carrier="H2 for shipping", p_set=p_set ) + if shipping_hydrogen_share < 1: + + shipping_oil_share = 1 - shipping_hydrogen_share + + p_set = shipping_oil_share * nodal_energy_totals.loc[nodes, all_navigation].sum(axis=1) * 1e6 / 8760. + + n.madd("Load", + nodes, + suffix=" shipping oil", + bus="EU oil", + carrier="shipping oil", + p_set=p_set + ) + + co2 = shipping_oil_share * nodal_energy_totals.loc[nodes, all_navigation].sum().sum() * 1e6 / 8760 * costs.at["oil", "CO2 intensity"] + + n.add("Load", + "shipping oil emissions", + bus="co2 atmosphere", + carrier="shipping oil emissions", + p_set=-co2 + ) + if "EU oil" not in n.buses.index: n.add("Bus", @@ -1993,14 +2041,19 @@ def maybe_adjust_costs_and_potentials(n, opts): suptechs = map(lambda c: c.split("-", 2)[0], carrier_list) if oo[0].startswith(tuple(suptechs)): carrier = oo[0] - attr_lookup = {"p": "p_nom_max", "c": "capital_cost"} + attr_lookup = {"p": "p_nom_max", "e": "e_nom_max", "c": "capital_cost"} attr = attr_lookup[oo[1][0]] factor = float(oo[1][1:]) #beware if factor is 0 and p_nom_max is np.inf, 0*np.inf is nan if carrier == "AC": # lines do not have carrier n.lines[attr] *= factor else: - comps = {"Generator", "Link", "StorageUnit"} if attr == 'p_nom_max' else {"Generator", "Link", "StorageUnit", "Store"} + if attr == 'p_nom_max': + comps = {"Generator", "Link", "StorageUnit"} + elif attr == 'e_nom_max': + comps = {"Store"} + else: + comps = {"Generator", "Link", "StorageUnit", "Store"} for c in n.iterate_components(comps): if carrier=='solar': sel = c.df.carrier.str.contains(carrier) & ~c.df.carrier.str.contains("solar rooftop") diff --git a/scripts/solve_network.py b/scripts/solve_network.py index 8c0313f1..a46acc30 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -151,7 +151,6 @@ def add_chp_constraints(n): def extra_functionality(n, snapshots): - add_chp_constraints(n) add_battery_constraints(n)