Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Industry pathway #929

Merged
merged 10 commits into from
Feb 16, 2024
9 changes: 9 additions & 0 deletions config/config.default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,15 @@ industry:
2040: 0.12
2045: 0.16
2050: 0.20
sector_ratios_fraction_future:
2020: 0.0
2025: 0.1
2030: 0.3
2035: 0.5
2040: 0.7
2045: 0.9
2050: 1.0
basic_chemicals_without_NH3_production_today: 69. #Mt/a, = 86 Mtethylene-equiv - 17 MtNH3
HVC_production_today: 52.
MWh_elec_per_tHVC_mechanical_recycling: 0.547
MWh_elec_per_tHVC_chemical_recycling: 6.9
Expand Down
2 changes: 2 additions & 0 deletions doc/configtables/industry.csv
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ HVC_primary_fraction,--,float,The fraction of high value chemicals (HVC) produce
HVC_mechanical_recycling _fraction,--,float,The fraction of high value chemicals (HVC) produced using mechanical recycling
HVC_chemical_recycling _fraction,--,float,The fraction of high value chemicals (HVC) produced using chemical recycling
,,,
sector_ratios_fraction_future,--,Dictionary with planning horizons as keys.,The fraction of total progress in fuel and process switching achieved in the industry sector.
basic_chemicals_without_NH3_production_today,Mt/a,float,"The amount of basic chemicals produced without ammonia (= 86 Mtethylene-equiv - 17 MtNH3)."
HVC_production_today,MtHVC/a,float,"The amount of high value chemicals (HVC) produced. This includes ethylene, propylene and BTX. From `DECHEMA (2017) <https://dechema.de/dechema_media/Downloads/Positionspapiere/Technology_study_Low_carbon_energy_and_feedstock_for_the_European_chemical_industry-p-20002750.pdf>`_, Figure 16, page 107"
Mwh_elec_per_tHVC _mechanical_recycling,MWh/tHVC,float,"The energy amount of electricity needed to produce a ton of high value chemical (HVC) using mechanical recycling. From SI of `Meys et al (2020) <https://doi.org/10.1016/j.resconrec.2020.105010>`_, Table S5, for HDPE, PP, PS, PET. LDPE would be 0.756."
Mwh_elec_per_tHVC _chemical_recycling,MWh/tHVC,float,"The energy amount of electricity needed to produce a ton of high value chemical (HVC) using chemical recycling. The default value is based on pyrolysis and electric steam cracking. From `Material Economics (2019) <https://materialeconomics.com/latest-updates/industrial-transformation-2050>`_, page 125"
Expand Down
9 changes: 9 additions & 0 deletions doc/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ Release Notes
Upcoming Release
================

* Improved representation of industry transition pathways. A new script was
added to interpolate industry sector ratios from today's status quo to future
systems (i.e. specific emissions and demands for energy and feedstocks). For
each country we gradually switch industry processes from today's specific
energy carrier usage per ton material output to the best-in-class energy
consumption of tomorrow. This is done on a per-country basis. The ratio of
today to tomorrow's energy consumption is set with the ``industry:
sector_ratios_fraction_future:`` parameter.

* Bugfix: Correct units of subtracted chlorine and methanol demand in
:mod:`build_industry_sector_ratios`.

Expand Down
29 changes: 27 additions & 2 deletions rules/build_sector.smk
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,31 @@ rule build_industry_sector_ratios:
"../scripts/build_industry_sector_ratios.py"


rule build_industry_sector_ratios_intermediate:
params:
industry=config["industry"],
input:
industry_sector_ratios=RESOURCES + "industry_sector_ratios.csv",
industrial_energy_demand_per_country_today=RESOURCES
+ "industrial_energy_demand_per_country_today.csv",
industrial_production_per_country=RESOURCES
+ "industrial_production_per_country.csv",
output:
industry_sector_ratios=RESOURCES
+ "industry_sector_ratios_{planning_horizons}.csv",
threads: 1
resources:
mem_mb=1000,
log:
LOGS + "build_industry_sector_ratios_{planning_horizons}.log",
benchmark:
BENCHMARKS + "build_industry_sector_ratios_{planning_horizons}"
conda:
"../envs/environment.yaml"
script:
"../scripts/build_industry_sector_ratios_intermediate.py"


rule build_industrial_production_per_country:
params:
industry=config["industry"],
Expand Down Expand Up @@ -533,7 +558,8 @@ rule build_industrial_production_per_node:

rule build_industrial_energy_demand_per_node:
input:
industry_sector_ratios=RESOURCES + "industry_sector_ratios.csv",
industry_sector_ratios=RESOURCES
+ "industry_sector_ratios_{planning_horizons}.csv",
industrial_production_per_node=RESOURCES
+ "industrial_production_elec_s{simpl}_{clusters}_{planning_horizons}.csv",
industrial_energy_demand_per_node_today=RESOURCES
Expand Down Expand Up @@ -564,7 +590,6 @@ rule build_industrial_energy_demand_per_country_today:
industry=config["industry"],
input:
jrc="data/bundle-sector/jrc-idees-2015",
ammonia_production=RESOURCES + "ammonia_production.csv",
industrial_production_per_country=RESOURCES
+ "industrial_production_per_country.csv",
output:
Expand Down
73 changes: 39 additions & 34 deletions scripts/build_industrial_energy_demand_per_country_today.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def industrial_energy_demand_per_country(country, year, jrc_dir):
def get_subsector_data(sheet):
df = df_dict[sheet][year].groupby(fuels).sum()

df["ammonia"] = 0.0
df["hydrogen"] = 0.0

df["other"] = df["all"] - df.loc[df.index != "all"].sum()

Expand All @@ -94,51 +94,50 @@ def get_subsector_data(sheet):
return df


def add_ammonia_energy_demand(demand):
# MtNH3/a
fn = snakemake.input.ammonia_production
ammonia = pd.read_csv(fn, index_col=0)[str(year)] / 1e3
def separate_basic_chemicals(demand, production):

def get_ammonia_by_fuel(x):
fuels = {
"gas": params["MWh_CH4_per_tNH3_SMR"],
"electricity": params["MWh_elec_per_tNH3_SMR"],
ammonia = pd.DataFrame(
{
"hydrogen": production["Ammonia"] * params["MWh_H2_per_tNH3_electrolysis"],
"electricity": production["Ammonia"]
* params["MWh_elec_per_tNH3_electrolysis"],
}

return pd.Series({k: x * v for k, v in fuels.items()})

ammonia_by_fuel = ammonia.apply(get_ammonia_by_fuel).T
ammonia_by_fuel = ammonia_by_fuel.unstack().reindex(
index=demand.index, fill_value=0.0
)

ammonia = pd.DataFrame({"ammonia": ammonia * params["MWh_NH3_per_tNH3"]}).T
).T
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["Ammonia"] = ammonia.unstack().reindex(index=demand.index, fill_value=0.0)

demand["Basic chemicals (without ammonia)"] = (
demand["Basic chemicals"] - ammonia_by_fuel
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["Ammonia"]
- demand["Methanol"]
- demand["Chlorine"]
)

demand["Basic chemicals (without ammonia)"].clip(lower=0, inplace=True)

demand.drop(columns="Basic chemicals", inplace=True)

demand["HVC"].clip(lower=0, inplace=True)

return demand


def add_non_eu28_industrial_energy_demand(countries, demand):
def add_non_eu28_industrial_energy_demand(countries, demand, production):
non_eu28 = countries.difference(eu28)
if non_eu28.empty:
return demand
# output in MtMaterial/a
fn = snakemake.input.industrial_production_per_country
production = pd.read_csv(fn, index_col=0) / 1e3

# recombine HVC, Chlorine and Methanol to Basic chemicals (without ammonia)
chemicals = ["HVC", "Chlorine", "Methanol"]
production["Basic chemicals (without ammonia)"] = production[chemicals].sum(axis=1)
production.drop(columns=chemicals, inplace=True)

eu28_production = production.loc[countries.intersection(eu28)].sum()
eu28_energy = demand.groupby(level=1).sum()
Expand Down Expand Up @@ -182,9 +181,15 @@ def industrial_energy_demand(countries, year):

demand = industrial_energy_demand(countries.intersection(eu28), year)

demand = add_ammonia_energy_demand(demand)
# 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_eu28_industrial_energy_demand(countries, demand)
demand = add_non_eu28_industrial_energy_demand(countries, demand, production)

# for format compatibility
demand = demand.stack(dropna=False).unstack(level=[0, 2])
Expand Down
24 changes: 16 additions & 8 deletions scripts/build_industrial_energy_demand_per_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,31 @@
planning_horizons=2030,
)

# import EU ratios df as csv
# import ratios
fn = snakemake.input.industry_sector_ratios
industry_sector_ratios = pd.read_csv(fn, index_col=0)
sector_ratios = pd.read_csv(fn, header=[0, 1], index_col=0)

# material demand per node and industry (kton/a)
# material demand per node and industry (Mton/a)
fn = snakemake.input.industrial_production_per_node
nodal_production = pd.read_csv(fn, index_col=0)
nodal_production = pd.read_csv(fn, index_col=0) / 1e3

# energy demand today to get current electricity
fn = snakemake.input.industrial_energy_demand_per_node_today
nodal_today = pd.read_csv(fn, index_col=0)

# final energy consumption per node and industry (TWh/a)
nodal_df = nodal_production.dot(industry_sector_ratios.T)
nodal_sector_ratios = pd.concat(
{node: sector_ratios[node[:2]] for node in nodal_production.index}, axis=1
)

nodal_production_stacked = nodal_production.stack()
nodal_production_stacked.index.names = [None, None]

# convert GWh to TWh and ktCO2 to MtCO2
nodal_df *= 0.001
# final energy consumption per node and industry (TWh/a)
nodal_df = (
(nodal_sector_ratios.multiply(nodal_production_stacked))
.T.groupby(level=0)
.sum()
)

rename_sectors = {
"elec": "electricity",
Expand Down
6 changes: 5 additions & 1 deletion scripts/build_industrial_production_per_country.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,11 @@ def separate_basic_chemicals(demand, year):
demand["Basic chemicals"].clip(lower=0.0, inplace=True)

# assume HVC, methanol, chlorine production proportional to non-ammonia basic chemicals
distribution_key = demand["Basic chemicals"] / demand["Basic chemicals"].sum()
distribution_key = (
demand["Basic chemicals"]
/ params["basic_chemicals_without_NH3_production_today"]
/ 1e3
)
demand["HVC"] = params["HVC_production_today"] * 1e3 * distribution_key
demand["Chlorine"] = params["chlorine_production_today"] * 1e3 * distribution_key
demand["Methanol"] = params["methanol_production_today"] * 1e3 * distribution_key
Expand Down
81 changes: 81 additions & 0 deletions scripts/build_industry_sector_ratios_intermediate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
# SPDX-FileCopyrightText: : 2020-2024 The PyPSA-Eur Authors
#
# SPDX-License-Identifier: MIT
"""
Build specific energy consumption by carrier and industries and by country,
that interpolates between the current average energy consumption (from
2015-2020) and the ideal future best-in-class consumption.
"""

import pandas as pd
from prepare_sector_network import get


def build_industry_sector_ratios_intermediate():

# in TWh/a
demand = pd.read_csv(
snakemake.input.industrial_energy_demand_per_country_today,
header=[0, 1],
index_col=0,
)

# in Mt/a
production = (
pd.read_csv(snakemake.input.industrial_production_per_country, index_col=0)
/ 1e3
).stack()
production.index.names = [None, None]

# in MWh/t
future_sector_ratios = pd.read_csv(
snakemake.input.industry_sector_ratios, index_col=0
)

today_sector_ratios = demand.div(production, axis=1)

today_sector_ratios.dropna(how="all", axis=1, inplace=True)

rename = {
"waste": "biomass",
"electricity": "elec",
"solid": "coke",
"gas": "methane",
"other": "biomass",
"liquid": "naphtha",
}
today_sector_ratios = today_sector_ratios.rename(rename).groupby(level=0).sum()

fraction_future = get(params["sector_ratios_fraction_future"], year)

intermediate_sector_ratios = {}
for ct, group in today_sector_ratios.T.groupby(level=0):
today_sector_ratios_ct = (
group.droplevel(0)
.T.reindex_like(future_sector_ratios)
.fillna(future_sector_ratios)
)
intermediate_sector_ratios[ct] = (
today_sector_ratios_ct * (1 - fraction_future)
+ future_sector_ratios * fraction_future
)
intermediate_sector_ratios = pd.concat(intermediate_sector_ratios, axis=1)

intermediate_sector_ratios.to_csv(snakemake.output.industry_sector_ratios)


if __name__ == "__main__":
if "snakemake" not in globals():
from _helpers import mock_snakemake

snakemake = mock_snakemake(
"build_industry_sector_ratios_intermediate",
planning_horizons="2030",
)

year = int(snakemake.wildcards.planning_horizons[-4:])

params = snakemake.params.industry

build_industry_sector_ratios_intermediate()
Loading