Merge pull request #929 from PyPSA/industry-pathway

Industry pathway
This commit is contained in:
Fabian Neumann 2024-02-16 16:28:34 +01:00 committed by GitHub
commit f59ede8af4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 187 additions and 44 deletions

View File

@ -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

View File

@ -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"

1 Unit Values Description
17 HVC_mechanical_recycling _fraction -- float The fraction of high value chemicals (HVC) produced using mechanical recycling
18 HVC_chemical_recycling _fraction -- float The fraction of high value chemicals (HVC) produced using chemical recycling
19
20 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.
21 basic_chemicals_without_NH3_production_today Mt/a float The amount of basic chemicals produced without ammonia (= 86 Mtethylene-equiv - 17 MtNH3).
22 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
23 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.
24 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

View File

@ -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`.

View File

@ -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"],
@ -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
@ -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:

View File

@ -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()
@ -94,51 +94,50 @@ def industrial_energy_demand_per_country(country, year, jrc_dir):
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["Chlorine"] = chlorine.unstack().reindex(index=demand.index, fill_value=0.0)
demand["Methanol"] = methanol.unstack().reindex(index=demand.index, fill_value=0.0)
demand["Basic chemicals (without ammonia)"] = (
demand["Basic chemicals"] - ammonia_by_fuel
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()
@ -182,9 +181,15 @@ if __name__ == "__main__":
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 = add_non_eu28_industrial_energy_demand(countries, demand)
demand = separate_basic_chemicals(demand, production)
demand = add_non_eu28_industrial_energy_demand(countries, demand, production)
# for format compatibility
demand = demand.stack(dropna=False).unstack(level=[0, 2])

View File

@ -19,23 +19,31 @@ if __name__ == "__main__":
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
)
# convert GWh to TWh and ktCO2 to MtCO2
nodal_df *= 0.001
nodal_production_stacked = nodal_production.stack()
nodal_production_stacked.index.names = [None, None]
# 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",

View File

@ -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

View File

@ -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()