pypsa-eur/scripts/build_industrial_energy_demand_per_country_today.py

319 lines
9.9 KiB
Python

# -*- coding: utf-8 -*-
# SPDX-FileCopyrightText: : 2020-2024 The PyPSA-Eur Authors
#
# SPDX-License-Identifier: MIT
"""
Build industrial energy demand per country.
Inputs
-------
- ``data/jrc-idees-2021``
- ``industrial_production_per_country.csv``
Outputs
-------
- ``resources/industrial_energy_demand_per_country_today.csv``
Description
-------
This rule uses the industrial_production_per_country.csv file and the JRC-IDEES data to derive an energy demand per country and sector. If the country is not in the EU28, an average energy demand depending on the production volume is derived.
For each country and each subcategory of
- Alumina production
- Aluminium - primary production
- Aluminium - secondary production
- Ammonia
- Cement
- Ceramics & other NMM
- Chlorine
- Electric arc
- Food, beverages and tobacco
- Glass production
- HVC
- Integrated steelworks
- Machinery equipment
- Methanol
- Other industrial sectors
- Other chemicals
- Other non-ferrous metals
- Paper production
- Pharmaceutical products etc.
- Printing and media reproduction
- Pulp production
- Textiles and leather
- Transport equipment
- Wood and wood products
the output file contains the energy demand in TWh/a for the following carriers
- biomass
- electricity
- gas
- heat
- hydrogen
- liquid
- other
- solid
- waste
"""
import multiprocessing as mp
from functools import partial
import country_converter as coco
import pandas as pd
from _helpers import set_scenario_config
from tqdm import tqdm
cc = coco.CountryConverter()
ktoe_to_twh = 0.011630
# name in JRC-IDEES Energy Balances
sector_sheets = {
"Integrated steelworks": "FC_IND_IS_BF_E",
"Electric arc": "FC_IND_IS_EA_E",
"Alumina production": "FC_IND_NFM_AM_E",
"Aluminium - primary production": "FC_IND_NFM_PA_E",
"Aluminium - secondary production": "FC_IND_NFM_SA_E",
"Other non-ferrous metals": "FC_IND_NFM_OM_E",
"Basic chemicals": "FC_IND_CPC_BC_E",
"Other chemicals": "FC_IND_CPC_OC_E",
"Pharmaceutical products etc.": "FC_IND_CPC_PH_E",
"Basic chemicals feedstock": "FC_IND_CPC_NE",
"Cement": "FC_IND_NMM_CM_E",
"Ceramics & other NMM": "FC_IND_NMM_CR_E",
"Glass production": "FC_IND_NMM_GL_E",
"Pulp production": "FC_IND_PPP_PU_E",
"Paper production": "FC_IND_PPP_PA_E",
"Printing and media reproduction": "FC_IND_PPP_PR_E",
"Food, beverages and tobacco": "FC_IND_FBT_E",
"Transport equipment": "FC_IND_TE_E",
"Machinery equipment": "FC_IND_MAC_E",
"Textiles and leather": "FC_IND_TL_E",
"Wood and wood products": "FC_IND_WP_E",
"Mining and quarrying": "FC_IND_MQ_E",
"Construction": "FC_IND_CON_E",
"Non-specified": "FC_IND_NSP_E",
}
fuels = {
"Total": "all",
"Solid fossil fuels": "solid",
"Peat and peat products": "solid",
"Oil shale and oil sands": "solid",
"Oil and petroleum products": "liquid",
"Manufactured gases": "gas",
"Natural gas": "gas",
"Nuclear heat": "heat",
"Heat": "heat",
"Renewables and biofuels": "biomass",
"Non-renewable waste": "waste",
"Electricity": "electricity",
}
eu27 = cc.EU27as("ISO2").ISO2.tolist()
jrc_names = {"GR": "EL", "GB": "UK"}
def industrial_energy_demand_per_country(country, year, jrc_dir, endogenous_ammonia):
jrc_country = jrc_names.get(country, country)
fn = f"{jrc_dir}/{jrc_country}/JRC-IDEES-2021_EnergyBalance_{jrc_country}.xlsx"
sheets = list(sector_sheets.values())
df_dict = pd.read_excel(fn, sheet_name=sheets, index_col=0)
def get_subsector_data(sheet):
df = df_dict[sheet][year].groupby(fuels).sum()
df["hydrogen"] = 0.0
# ammonia is handled separately
if endogenous_ammonia:
df["ammonia"] = 0.0
df["other"] = df["all"] - df.loc[df.index != "all"].sum()
return df
df = pd.concat(
{sub: get_subsector_data(sheet) for sub, sheet in sector_sheets.items()}, axis=1
)
sel = ["Mining and quarrying", "Construction", "Non-specified"]
df["Other industrial sectors"] = df[sel].sum(axis=1)
df["Basic chemicals"] += df["Basic chemicals feedstock"]
df.drop(columns=sel + ["Basic chemicals feedstock"], index="all", inplace=True)
df *= ktoe_to_twh
return df
def separate_basic_chemicals(demand, production):
chlorine = pd.DataFrame(
{
"hydrogen": production["Chlorine"] * params["MWh_H2_per_tCl"],
"electricity": production["Chlorine"] * params["MWh_elec_per_tCl"],
}
).T
methanol = pd.DataFrame(
{
"gas": production["Methanol"] * params["MWh_CH4_per_tMeOH"],
"electricity": production["Methanol"] * params["MWh_elec_per_tMeOH"],
}
).T
demand["Chlorine"] = chlorine.unstack().reindex(index=demand.index, fill_value=0.0)
demand["Methanol"] = methanol.unstack().reindex(index=demand.index, fill_value=0.0)
demand["HVC"] = demand["Basic chemicals"] - demand["Methanol"] - demand["Chlorine"]
# Deal with ammonia separately, depending on whether it is modelled endogenously.
ammonia_exo = pd.DataFrame(
{
"hydrogen": production["Ammonia"] * params["MWh_H2_per_tNH3_electrolysis"],
"electricity": production["Ammonia"]
* params["MWh_elec_per_tNH3_electrolysis"],
}
).T
if snakemake.params.ammonia:
ammonia = pd.DataFrame(
{"ammonia": production["Ammonia"] * params["MWh_NH3_per_tNH3"]}
).T
else:
ammonia = ammonia_exo
demand["Ammonia"] = ammonia.unstack().reindex(index=demand.index, fill_value=0.0)
demand["HVC"] -= ammonia_exo.unstack().reindex(index=demand.index, fill_value=0.0)
demand.drop(columns="Basic chemicals", inplace=True)
demand["HVC"] = demand["HVC"].clip(lower=0)
return demand
def add_non_eu27_industrial_energy_demand(countries, demand, production):
non_eu27 = countries.difference(eu27)
if non_eu27.empty:
return demand
eu27_production = production.loc[countries.intersection(eu27)].sum()
eu27_energy = demand.groupby(level=1).sum()
eu27_averages = eu27_energy / eu27_production
demand_non_eu27 = pd.concat(
{k: v * eu27_averages for k, v in production.loc[non_eu27].iterrows()}
)
return pd.concat([demand, demand_non_eu27])
def industrial_energy_demand(countries, year):
nprocesses = snakemake.threads
disable_progress = snakemake.config["run"].get("disable_progressbar", False)
func = partial(
industrial_energy_demand_per_country,
year=year,
jrc_dir=snakemake.input.jrc,
endogenous_ammonia=snakemake.params.ammonia,
)
tqdm_kwargs = dict(
ascii=False,
unit=" country",
total=len(countries),
desc="Build industrial energy demand",
disable=disable_progress,
)
with mp.Pool(processes=nprocesses) as pool:
demand_l = list(tqdm(pool.imap(func, countries), **tqdm_kwargs))
return pd.concat(demand_l, keys=countries)
def add_coke_ovens(demand, fn, year, factor=0.75):
"""
Adds the energy consumption of coke ovens to the energy demand for
integrated steelworks.
This function reads the energy consumption data for coke ovens from a
CSV file, processes it to match the structure of the `demand` DataFrame,
and then adds a specified share of this energy consumption to the energy
demand for integrated steelworks.
The `factor` parameter controls what proportion of the coke ovens' energy
consumption should be attributed to the iron and steel production.
The default value of 75% is based on https://doi.org/10.1016/j.erss.2022.102565
Parameters:
demand (pd.DataFrame): A pandas DataFrame containing energy demand data
with a multi-level column index where one of the
levels corresponds to "Integrated steelworks".
fn (str): The file path to the CSV file containing the coke ovens energy
consumption data.
year (int): The year for which the coke ovens data should be selected.
factor (float, optional): The proportion of coke ovens energy consumption to add to the
integrated steelworks demand. Defaults to 0.75.
Returns:
pd.DataFrame: The updated `demand` DataFrame with the coke ovens energy
consumption added to the integrated steelworks energy demand.
"""
df = pd.read_csv(fn, index_col=[0, 1]).xs(year, level=1)
df = df.rename(columns={"Total all products": "Total"})[fuels.keys()]
df = df.rename(columns=fuels).T.groupby(level=0).sum().T
df["other"] = df["all"] - df.loc[:, df.columns != "all"].sum(axis=1)
df = df.T.reindex_like(demand.xs("Integrated steelworks", axis=1, level=1)).fillna(
0
)
sel = demand.columns.get_level_values(1) == "Integrated steelworks"
demand.loc[:, sel] += factor * df.values
return demand
if __name__ == "__main__":
if "snakemake" not in globals():
from _helpers import mock_snakemake
snakemake = mock_snakemake("build_industrial_energy_demand_per_country_today")
set_scenario_config(snakemake)
params = snakemake.params.industry
year = params.get("reference_year", 2019)
countries = pd.Index(snakemake.params.countries)
demand = industrial_energy_demand(countries.intersection(eu27), year)
# output in MtMaterial/a
production = (
pd.read_csv(snakemake.input.industrial_production_per_country, index_col=0)
/ 1e3
)
demand = separate_basic_chemicals(demand, production)
demand = add_non_eu27_industrial_energy_demand(countries, demand, production)
# for format compatibility
demand = demand.stack(future_stack=True).unstack(level=[0, 2])
# add energy consumption of coke ovens
demand = add_coke_ovens(demand, snakemake.input.transformation_output_coke, year)
# style and annotation
demand.index.name = "TWh/a"
demand.sort_index(axis=1, inplace=True)
fn = snakemake.output.industrial_energy_demand_per_country_today
demand.to_csv(fn)