diff --git a/README.md b/README.md index 83ebbbe7..f4cc34cc 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,9 @@ it. PyPSA-Eur-Sec builds on the electricity generation and transmission model [PyPSA-Eur](https://github.com/PyPSA/pypsa-eur) to add demand and supply for the following sectors: transport, space and water -heating, biomass, industry and industrial feedstocks. This completes -the energy system and includes all greenhouse gas emitters except -waste management, agriculture, forestry and land use. +heating, biomass, industry and industrial feedstocks, agriculture, +forestry and fishing. This completes the energy system and includes +all greenhouse gas emitters except waste management and land use. Please see the [documentation](https://pypsa-eur-sec.readthedocs.io/) for installation instructions and other useful information about the snakemake workflow. diff --git a/Snakefile b/Snakefile index f4c795f3..0b72ae4b 100644 --- a/Snakefile +++ b/Snakefile @@ -9,7 +9,6 @@ wildcard_constraints: lv="[a-z0-9\.]+", simpl="[a-zA-Z0-9]*", clusters="[0-9]+m?", - sectors="[+a-zA-Z0-9]+", opts="[-+a-zA-Z0-9]*", sector_opts="[-+a-zA-Z0-9\.\s]*" diff --git a/config.default.yaml b/config.default.yaml index d6d4052c..b5560133 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -21,13 +21,14 @@ scenario: opts: # only relevant for PyPSA-Eur - '' sector_opts: # this is where the main scenario settings are - - Co2L0-3H-T-H-B-I-solar+p3-dist1 + - Co2L0-3H-T-H-B-I-A-solar+p3-dist1 # to really understand the options here, look in scripts/prepare_sector_network.py # Co2Lx specifies the CO2 target in x% of the 1990 values; default will give default (5%); # Co2L0p25 will give 25% CO2 emissions; Co2Lm0p05 will give 5% negative emissions # xH is the temporal resolution; 3H is 3-hourly, i.e. one snapshot every 3 hours # single letters are sectors: T for land transport, H for building heating, - # B for biomass supply, I for industry, shipping and aviation + # B for biomass supply, I for industry, shipping and aviation, + # A for agriculture, forestry and fishing # 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 @@ -181,6 +182,9 @@ sector: 2050: 0.85 transport_fuel_cell_efficiency: 0.5 transport_internal_combustion_efficiency: 0.3 + agriculture_machinery_electric_share: 0 + agriculture_machinery_fuel_efficiency: 0.7 # fuel oil per use + agriculture_machinery_electric_efficiency: 0.3 # electricity per use shipping_average_efficiency: 0.4 #For conversion of fuel oil to propulsion in 2011 shipping_hydrogen_liquefaction: false # whether to consider liquefaction costs for shipping H2 demands shipping_hydrogen_share: # 1 means all hydrogen FC @@ -477,6 +481,10 @@ plotting: CC: k co2: '#123456' co2 vent: '#654321' + agriculture heat: '#D07A7A' + agriculture machinery oil: '#1e1e1e' + agriculture machinery oil emissions: '#111111' + agriculture electricity: '#222222' solid biomass for industry co2 from atmosphere: '#654321' solid biomass for industry co2 to stored: '#654321' gas for industry co2 to atmosphere: '#654321' diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 01935d80..8a1ed562 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -86,6 +86,10 @@ Future release spatially disaggregate biomass potentials to PyPSA-Eur regions based on overlaps with NUTS2 regions from ENSPRESO (proportional to area) (`#151 `_). * Compatibility with ``xarray`` version 0.19. +* Added option to include emissions and energy demands of agriculture, forestry and fishing sector via the letter ``A`` in the ``{sector_opts}`` wildcard. + Demands are separated into electricity, heat and oil for machinery. + Fuel-switching for machinery from oil to electricity can be set exogenously in the ``config.yaml`` + `#147 `_. * Separate basic chemicals into HVC, chlorine, methanol and ammonia [`#166 `_]. * Add option to specify reuse, primary production, and mechanical and chemical recycling fraction of platics [`#166 `_]. * Include today's district heating shares in myopic optimisation and add option to specify exogenous path for district heating share increase under ``sector: district_heating:`` [`#149 `_]. diff --git a/doc/supply_demand.rst b/doc/supply_demand.rst index 8e4f2130..3ab00d8e 100644 --- a/doc/supply_demand.rst +++ b/doc/supply_demand.rst @@ -43,7 +43,7 @@ Heat demand is split into: * ``urban central``: large-scale district heating networks in urban areas with dense heat demand * ``residential/services urban decentral``: heating for individual buildings in urban areas -* ``residential/services rural``: heating for individual buildings in rural areas +* ``residential/services rural``: heating for individual buildings in rural areas, agriculture heat uses Heat supply @@ -189,7 +189,7 @@ Only wastes and residues from the JRC ENSPRESO biomass dataset. Oil product demand ===================== -Transport fuels and naphtha as a feedstock for the chemicals industry. +Transport fuels, agriculture machinery and naphtha as a feedstock for the chemicals industry. Oil product supply ====================== diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index efb5d6da..3f376b0c 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -117,6 +117,7 @@ def reverse(dictionary): "total energy": "1 - Energy", "industrial processes": "2 - Industrial Processes and Product Use", "agriculture": "3 - Agriculture", + "agriculture, forestry and fishing": '1.A.4.c - Agriculture/Forestry/Fishing', "LULUCF": "4 - Land Use, Land-Use Change and Forestry", "waste management": "5 - Waste management", "other": "6 - Other Sector", @@ -182,7 +183,7 @@ def idees_per_country(ct, year): ct_idees = idees_rename.get(ct, ct) fn_residential = f"{base_dir}/JRC-IDEES-2015_Residential_{ct_idees}.xlsx" - fn_services = f"{base_dir}/JRC-IDEES-2015_Tertiary_{ct_idees}.xlsx" + fn_tertiary = f"{base_dir}/JRC-IDEES-2015_Tertiary_{ct_idees}.xlsx" fn_transport = f"{base_dir}/JRC-IDEES-2015_Transport_{ct_idees}.xlsx" # residential @@ -213,14 +214,14 @@ def idees_per_country(ct, year): ct_totals["electricity residential"] = df[47] assert df.index[46] == "Derived heat" - ct_totals["Derived heat residential"] = df[46] + ct_totals["derived heat residential"] = df[46] assert df.index[50] == 'Thermal uses' ct_totals["thermal uses residential"] = df[50] # services - df = pd.read_excel(fn_services, "SER_hh_fec", index_col=0)[year] + df = pd.read_excel(fn_tertiary, "SER_hh_fec", index_col=0)[year] ct_totals["total services space"] = df["Space heating"] @@ -237,7 +238,7 @@ def idees_per_country(ct, year): assert df.index[31] == "Electricity" ct_totals["electricity services cooking"] = df[31] - df = pd.read_excel(fn_services, "SER_summary", index_col=0)[year] + df = pd.read_excel(fn_tertiary, "SER_summary", index_col=0)[year] row = "Energy consumption by fuel - Eurostat structure (ktoe)" ct_totals["total services"] = df[row] @@ -251,6 +252,35 @@ def idees_per_country(ct, year): assert df.index[53] == 'Thermal uses' ct_totals["thermal uses services"] = df[53] + + # agriculture, forestry and fishing + + start = "Detailed split of energy consumption (ktoe)" + end = "Market shares of energy uses (%)" + + df = pd.read_excel(fn_tertiary, "AGR_fec", index_col=0).loc[start:end, year] + + rows = [ + "Lighting", + "Ventilation", + "Specific electricity uses", + "Pumping devices (electric)" + ] + ct_totals["total agriculture electricity"] = df[rows].sum() + + rows = ["Specific heat uses", "Low enthalpy heat"] + ct_totals["total agriculture heat"] = df[rows].sum() + + rows = [ + "Motor drives", + "Farming machine drives (diesel oil incl. biofuels)", + "Pumping devices (diesel oil incl. biofuels)", + ] + ct_totals["total agriculture machinery"] = df[rows].sum() + + row = "Agriculture, forestry and fishing" + ct_totals["total agriculture"] = df[row] + # transport df = pd.read_excel(fn_transport, "TrRoad_ene", index_col=0)[year] @@ -568,10 +598,13 @@ def build_eea_co2(year=1990): "international aviation", "domestic navigation", "international navigation", + "agriculture, forestry and fishing" ] emissions["industrial non-elec"] = emissions["total energy"] - emissions[to_subtract].sum(axis=1) - to_drop = ["total energy", "total wL", "total woL"] + emissions["agriculture"] += emissions["agriculture, forestry and fishing"] + + to_drop = ["total energy", "total wL", "total woL", "agriculture, forestry and fishing"] emissions.drop(columns=to_drop, inplace=True) # convert from Gg to Mt @@ -616,7 +649,7 @@ def build_co2_totals(countries, eea_co2, eurostat_co2): # does not include industrial process emissions or fuel processing/refining "industrial non-elec": (ct, "+", "Industry"), # does not include non-energy emissions - "agriculture": (ct, "+", "+", "Agriculture / Forestry"), + "agriculture": (eurostat_co2.index.get_level_values(0) == ct) & eurostat_co2.index.isin(["Agriculture / Forestry", "Fishing"], level=3), } for i, mi in mappings.items(): diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 5fc4a6e3..645dc9e1 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -92,6 +92,10 @@ def emission_sectors_from_opts(opts): "domestic navigation", "international navigation" ] + if "A" in opts: + sectors += [ + "agriculture" + ] return sectors @@ -882,8 +886,9 @@ def insert_electricity_distribution_grid(n, costs): capital_cost=costs.at['electricity distribution grid', 'fixed'] * cost_factor ) - # this catches regular electricity load and "industry electricity" - loads = n.loads.index[n.loads.carrier.str.contains("electricity")] + # this catches regular electricity load and "industry electricity" and + # "agriculture machinery electric" and "agriculture electricity" + loads = n.loads.index[n.loads.carrier.str.contains("electric")] n.loads.loc[loads, "bus"] += " low voltage" bevs = n.links.index[n.links.carrier == "BEV charger"] @@ -1321,8 +1326,8 @@ def add_land_transport(n, costs): co2 = ice_share / ice_efficiency * transport[nodes].sum().sum() / 8760 * costs.at["oil", 'CO2 intensity'] - n.madd("Load", - ["land transport oil emissions"], + n.add("Load", + "land transport oil emissions", bus="co2 atmosphere", carrier="land transport oil emissions", p_set=-co2 @@ -2161,6 +2166,71 @@ def add_waste_heat(n): n.links.loc[urban_central + " H2 Fuel Cell", "efficiency2"] = 0.95 - n.links.loc[urban_central + " H2 Fuel Cell", "efficiency"] +def add_agriculture(n, costs): + + logger.info('Add agriculture, forestry and fishing sector.') + + nodes = pop_layout.index + + # electricity + + n.madd("Load", + nodes, + suffix=" agriculture electricity", + bus=nodes, + carrier='agriculture electricity', + p_set=nodal_energy_totals.loc[nodes, "total agriculture electricity"] * 1e6 / 8760 + ) + + # heat + + n.madd("Load", + nodes, + suffix=" agriculture heat", + bus=nodes + " services rural heat", + carrier="agriculture heat", + p_set=nodal_energy_totals.loc[nodes, "total agriculture heat"] * 1e6 / 8760 + ) + + # machinery + + electric_share = get(options["agriculture_machinery_electric_share"], investment_year) + assert electric_share <= 1. + ice_share = 1 - electric_share + + machinery_nodal_energy = nodal_energy_totals.loc[nodes, "total agriculture machinery"] + + if electric_share > 0: + + efficiency_gain = options["agriculture_machinery_fuel_efficiency"] / options["agriculture_machinery_electric_efficiency"] + + n.madd("Load", + nodes, + suffix=" agriculture machinery electric", + bus=nodes, + carrier="agriculture machinery electric", + p_set=electric_share / efficiency_gain * machinery_nodal_energy * 1e6 / 8760, + ) + + if ice_share > 0: + + n.add("Load", + "agriculture machinery oil", + bus="EU oil", + carrier="agriculture machinery oil", + p_set=ice_share * machinery_nodal_energy.sum() * 1e6 / 8760 + ) + + co2 = ice_share * machinery_nodal_energy.sum() * 1e6 / 8760 * costs.at["oil", 'CO2 intensity'] + + n.add("Load", + "agriculture machinery oil emissions", + bus="co2 atmosphere", + carrier="agriculture machinery oil emissions", + p_set=-co2 + ) + + def decentral(n): """Removes the electricity transmission system.""" n.lines.drop(n.lines.index, inplace=True) @@ -2297,6 +2367,9 @@ def limit_individual_line_extension(n, maxext): if "I" in opts and "H" in opts: add_waste_heat(n) + if "A" in opts: # requires H and I + add_agriculture(n, costs) + if options['dac']: add_dac(n, costs)