319 lines
9.9 KiB
Python
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)
|