Input Assumptions for Modelling the Industry Sector in the negaWatt-BE Scenario
INDUSTRY FINAL ENERGY DEMAND IN BELGIUM
Developer: QUOILIN Sylvain, MEYER Sébastien, LATERRE Antoine, BERNAERTS Valentine
This notebook documents how the industrial sector is represented in the negaWatt-BE / PyPSA-Eur
sufficiency scenario, makes the underlying sufficiency, circularity and efficiency hypotheses explicit,
and reconstructs the final-energy-demand inputs that PyPSA-Eur expects for industry — so that these inputs
can be generated from documented assumptions instead of being read from the external CLEVER scenario
dashboard (clever_Industry_<year>.csv).
Status / honesty note. Unlike the buildings and transport notebooks, industry was historically not modelled bottom-up inside negaWatt-BE: the scenario imported the industrial final energy consumption (FEC) for every country directly from the CLEVER project dashboard. This notebook rebuilds that layer from explicit levers — gross material-demand reductions (sufficiency), recycling rates and route shares (circularity), and energy-intensity / electrification trajectories (efficiency). Each industrial branch FEC is reconstructed as production × energy intensity, and the energy intensity is, wherever the data allow, derived from the recycling rate and the per-route energy intensities (MODEIRE/CLEVER). Where the public CLEVER data are insufficient to recompute a value, an explicit hypothesis is stated and flagged with a
> ⚠️note for future improvement.
References¶
- [CLEVER-IND] négaWatt / CLEVER (June 2022). Establishment of energy consumption convergence corridors to
2050 — Industrial sector. (
CLEVER/2206-Convergence-corridors-Industry.pdf; distilled inCLEVER/CLEVER_industry_hypotheses.md). - [CLEVER-REP] CLEVER (2023). A Collaborative Low Energy Vision for the European Region — final report.
(
CLEVER/CLEVER_final-report.pdf/CLEVER/CLEVER_final-report.md). - [MODEIRE] Route-specific energy intensities (primary vs recycled) reported by [CLEVER-IND] for steel, glass and pulp & paper.
- [JRC-IDEES] Rozsai, M. et al. (2025). JRC-IDEES-2023 (tables labelled 2021), European Commission, JRC.
- [PYPSA] PyPSA-Eur (negaWatt fork):
scripts/build_industrial_energy_demand_per_node.py.
# Automatically reload helper module if modified:
%load_ext autoreload
%autoreload 2
import warnings
warnings.filterwarnings("ignore") # silence NumPy/pandas optional-dependency noise in this environment
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from pathlib import Path
# Reuse the negaWatt-BE projection / formatting helpers (linear_growth, generate_target_years, ...):
from nW_BE_demand_model_sub_functions import *
pd.set_option("display.float_format", lambda v: f"{v:,.3f}")
# ----------------------------------------------------------------- configuration
DATA = Path("data")
JRC_DIR = DATA / "jrc-idees-2021" / "BE"
JRC_INDUSTRY = JRC_DIR / "JRC-IDEES-2021_Industry_BE.xlsx"
JRC_ENERGYBALANCE = JRC_DIR / "JRC-IDEES-2021_EnergyBalance_BE.xlsx"
CLEVER_REF_DIR = DATA / "clever_dashboard_reference" # dashboard exports kept ONLY for validation
OUT_DIR = DATA / "industry_output" # reconstructed PyPSA-Eur inputs are written here
OUT_DIR.mkdir(parents=True, exist_ok=True)
COUNTRY = "BE"
KTOE_TO_TWH = 0.01163 # 1 ktoe = 0.01163 TWh
# CLEVER industry trajectories are provided on a DECADAL grid:
YEARS = [2020, 2030, 2040, 2050]
# CLEVER uses 2015 as the index reference year, and pre-COVID 2019 statistics as the "recent" 2020 baseline.
BASE_YEAR, RECENT_YEAR = 2015, 2019
print("Configuration loaded. Decadal horizon:", YEARS)
assert JRC_INDUSTRY.exists() and JRC_ENERGYBALANCE.exists(), "JRC-IDEES BE files missing under data/jrc-idees-2021/BE/"
Configuration loaded. Decadal horizon: [2020, 2030, 2040, 2050]
1. How industry enters the negaWatt-BE / PyPSA-Eur scenario¶
The negaWatt-BE demand chain (macro → buildings / transports) overrides the transport and
residential/tertiary entries of PyPSA-Eur's energy_totals (see scripts/nW_BE.py, rule update_nW_BE).
Industry is handled on a different path and is not touched by update_nW_BE.
For the sufficiency run (run.name == "suff"), scripts/build_industrial_energy_demand_per_node.py overwrites
the industrial carrier totals, per country, from data/clever_Industry_<planning_horizon>.csv. The target
outputs this notebook must reproduce for Belgium are exactly the PyPSA-Eur carrier columns below:
| PyPSA-Eur carrier | Built from CLEVER columns |
|---|---|
ammonia |
Total FEC of the ammonia industry |
electricity |
Total final electricity consumption in industry |
coal |
Total final solid-fossil-fuel consumption in industry |
solid biomass |
Total final solid-biomass consumption in industry |
methane |
Total final gas consumption in industry |
low-temperature heat |
Total final heat consumption in industry |
hydrogen |
Total final H₂ in industry + non-energy H₂ feedstock |
naphtha |
non-energy oil feedstock + total final oil consumption in industry |
⚠️ Multi-node caveat (from PyPSA-Eur). The override assigns the full country total to every bus. With the
admclustering ofconfig_suff.yamlBelgium has a single industry bus, so this is fine here.
2. The CLEVER industry methodology (sufficiency → circularity → efficiency)¶
CLEVER builds 2050 convergence corridors per industrial branch through three successive levers
([CLEVER-IND]; full extraction in CLEVER/CLEVER_industry_hypotheses.md):
- Sufficiency — scale material demand down (deliver the service with less material) → lower production.
- Circularity — durable design + longer use (less production) and higher recycling rates (shift from primary to less energy-intensive recycled routes) → affects both production and energy intensity.
- Efficiency — reduce the energy intensity of production (technologies, fuel/material substitution, electrification).
For the heavy sectors (steel, cement, glass, pulp & paper, ammonia, HVC):
$$\text{FEC}_\text{sector}(y) = \underbrace{P_{2015}\times\tfrac{\text{demand index}(y)}{100}}_{\text{production (sufficiency+circularity)}} \times \underbrace{I(y)}_{\text{energy intensity (circularity+efficiency)}} \times 10^{-6}\;[\text{TWh}]$$
and, where data allow, the energy intensity is itself derived from the recycling/route split:
$$I(y) = s_\text{rec}(y)\,I_\text{rec}(y) + \bigl(1-s_\text{rec}(y)\bigr)\,I_\text{prim}(y)$$
with $s_\text{rec}$ the recycled-route share and $I_\text{rec}, I_\text{prim}$ the recycled/primary route intensities [MODEIRE]. For the light sectors (other chemicals, non-ferrous, food, "other industries") a direct gross-FEC reduction is applied.
Summary corridors (Table 2 of [CLEVER-IND], % of 2015 value in 2050)¶
| Sector | Production index | Energy intensity (MWh/kt) | FEC index |
|---|---|---|---|
| Cement | 52 – 99 | 560 – 800 | 31 – 64 |
| Steel | 74 – 92 | 2060 – 2690 | 42 – 52 |
| Pulp & paper | 58 – 110 | 1890 – 3780 | 31 – 64 |
| Chemicals – Ammonia | 58 – 80 | 1580 – 2500 | — |
| Chemicals – HVC | 59 – 98 | 3140 – 5680 | — |
| Chemicals – Others | — | — | 69 – 89 |
| Glass | 61 – 95 | 700 – 2190 | 23 – 68 |
| Food | — | — | 42 – 64 |
| Non-ferrous metals | — | — | 39 – 87 |
| Others | — | — | 63 – 85 |
The Belgian trajectory adopted below sits inside every one of these corridors.
3. Statistical baseline for Belgium — JRC-IDEES-2021¶
We load the physical production and the final-energy balance for Belgian industry directly from the
JRC-IDEES-2021 workbooks (data/jrc-idees-2021/BE/). These provide a transparent, peer-reviewed baseline
against which the CLEVER assumptions are checked.
# ---- physical production (kt) from the JRC-IDEES Industry workbook ----
def jrc_physical_output(sheet, label, year):
'''Physical output [kt] of a sub-sector (reads the 'Physical output' block of a JRC Industry sheet).'''
df = pd.read_excel(JRC_INDUSTRY, sheet_name=sheet, index_col=0)
idx_str = df.index.to_series().astype(str)
start = int(np.asarray(idx_str.str.contains("Physical output")).argmax())
isnull = np.asarray(df.index.isnull())
end = start + 1
while end < len(df) and not isnull[end]:
end += 1
block = df.iloc[start:end]
return float(block.loc[label, year])
# Exact JRC row labels (mind the DOUBLE space in 'Glass production (kt)' and 'Paper production (kt)'):
PROD_FIELDS = {
"crude steel": ("ISI", "Physical output (kt steel)"),
" primary (integrated)": ("ISI", "Integrated steelworks"),
" recycled (electric arc)": ("ISI", "Electric arc"),
"cement": ("NMM", "Cement (kt)"),
"glass": ("NMM", "Glass production (kt)"),
"paper": ("PPA", "Paper production (kt)"),
"pulp": ("PPA", "Pulp production (kt)"),
"basic chemicals (kt eth.eq.)": ("CHI", "Basic chemicals (kt ethylene eq.)"),
}
baseline_prod = pd.DataFrame(
{yr: {k: jrc_physical_output(s, l, yr) for k, (s, l) in PROD_FIELDS.items()}
for yr in [BASE_YEAR, RECENT_YEAR, 2021]}
)
print("Belgian physical production [kt] — JRC-IDEES-2021")
baseline_prod
Belgian physical production [kt] — JRC-IDEES-2021
| 2015 | 2019 | 2021 | |
|---|---|---|---|
| crude steel | 7,257.140 | 7,759.543 | 6,909.288 |
| primary (integrated) | 4,808.724 | 5,239.543 | 4,712.134 |
| recycled (electric arc) | 2,448.416 | 2,520.000 | 2,197.154 |
| cement | 5,550.633 | 5,834.211 | 6,072.323 |
| glass | 1,108.243 | 1,399.067 | 1,175.816 |
| paper | 2,050.101 | 1,935.108 | 1,699.567 |
| pulp | 492.048 | 514.977 | 514.977 |
| basic chemicals (kt eth.eq.) | 6,211.432 | 6,639.531 | 7,169.810 |
# ---- final energy consumption by carrier (total industry) from the JRC EnergyBalance workbook ----
FUELS = { # JRC fuel row label -> aggregated carrier
"Solid fossil fuels": "coal", "Peat and peat products": "coal", "Oil shale and oil sands": "coal",
"Oil and petroleum products": "oil",
"Manufactured gases": "gas", "Natural gas": "gas",
"Heat": "heat", "Nuclear heat": "heat",
"Renewables and biofuels": "biomass",
"Non-renewable waste": "waste",
"Electricity": "electricity",
}
def jrc_industry_fec_by_carrier(years, sheet="FC_IND_E"):
'''Total industrial FEC by carrier [TWh] (energy use only; excludes non-energy feedstock).'''
df = pd.read_excel(JRC_ENERGYBALANCE, sheet_name=sheet, index_col=0)
df.index = df.index.to_series().astype(str).str.strip()
out = {}
for yr in years:
s = {}
for label, carrier in FUELS.items():
if label in df.index:
s[carrier] = s.get(carrier, 0.0) + float(df.loc[label, yr]) * KTOE_TO_TWH
out[yr] = pd.Series(s)
return pd.DataFrame(out)
jrc_carrier = jrc_industry_fec_by_carrier([BASE_YEAR, RECENT_YEAR, 2021])
jrc_carrier.loc["TOTAL (energy use)"] = jrc_carrier.sum()
print("Belgian industry FEC by carrier [TWh] — JRC-IDEES-2021 (energy use only, excl. feedstock)")
jrc_carrier
Belgian industry FEC by carrier [TWh] — JRC-IDEES-2021 (energy use only, excl. feedstock)
| 2015 | 2019 | 2021 | |
|---|---|---|---|
| coal | 4.879 | 4.878 | 4.236 |
| oil | 20.197 | 15.170 | 16.978 |
| gas | 45.430 | 47.825 | 49.556 |
| heat | 4.756 | 4.399 | 4.266 |
| biomass | 8.077 | 7.909 | 8.380 |
| waste | 1.589 | 1.459 | 1.382 |
| electricity | 38.094 | 38.265 | 38.234 |
| TOTAL (energy use) | 123.022 | 119.906 | 123.031 |
# ---- non-energy (feedstock) use, dominated by the chemical industry ----
def jrc_total(sheet, years, label="Total"):
df = pd.read_excel(JRC_ENERGYBALANCE, sheet_name=sheet, index_col=0)
df.index = df.index.to_series().astype(str).str.strip()
return pd.Series({yr: float(df.loc[label, yr]) * KTOE_TO_TWH for yr in years}, name=sheet)
feedstock_base = pd.concat([
jrc_total("FC_IND_NE", [BASE_YEAR, RECENT_YEAR, 2021]).rename("non-energy total"),
jrc_total("FC_IND_CPC_NE", [BASE_YEAR, RECENT_YEAR, 2021]).rename("of which chemical feedstock"),
], axis=1)
print("Belgian industry NON-ENERGY (feedstock) use [TWh] — JRC-IDEES-2021")
feedstock_base
Belgian industry NON-ENERGY (feedstock) use [TWh] — JRC-IDEES-2021
| non-energy total | of which chemical feedstock | |
|---|---|---|
| 2015 | 85.916 | 84.553 |
| 2019 | 81.022 | 80.510 |
| 2021 | 84.983 | 83.897 |
⚠️ Baseline reconciliation (JRC-IDEES vs CLEVER). JRC-IDEES splits energy use (
FC_IND_E≈ 120 TWh in 2019) from non-energy feedstock (FC_IND_NE≈ 81 TWh, almost all chemical feedstock). CLEVER reports a single "Total FEC of industry" ≈ 138.7 TWh for 2020, which embeds the steel coke/blast-furnace energy in the carrier totals and computes heavy-sector FEC as production × unit-consumption. The largest reconciliation items are steel (CLEVER 30.7 TWh vs JRC iron&steel 12 TWh — coke/BF energy) and cement/glass production volumes. This is the main transparency gap (§7).
4. Explicit sufficiency, circularity & efficiency hypotheses (Belgium)¶
This is the core of the notebook: every number fed to PyPSA-Eur is expressed as an explicit lever, in the same spirit as the hard-coded assumptions of the buildings/transport notebooks. For each heavy sector we state
- a gross material-demand index (sufficiency + circularity) → production,
- a recycled-route share (circularity),
- per-route energy intensities (efficiency; MODEIRE) → from which the blended energy intensity is derived,
and for the light sectors a direct gross-FEC reduction. Every value is annotated with its CLEVER corridor.
⚠️ Provenance limit. The exact national levers (demand reductions, recycling rates, MWh/kt) were chosen by the Belgian CLEVER partner and are not fully derived in the public CLEVER notes. We make them explicit and check that they (a) lie inside the corridors and (b) reproduce the dashboard. Re-deriving the demand reductions from a Belgian end-use model is left for future work.
4.1 Sector-by-sector hypotheses (narrative)¶
Steel (corridor: production 74–92, intensity 2060–2690, FEC 42–52)
- Sufficiency + circularity (production → 85 % of 2015 by 2050, −15 %): less over-specified steel in construction (−20–30 %), +40 % building lifetime, ~10 % timber substitution; lighter vehicles, modal shift and car-sharing in transport. (The 2020 index 106.9 % simply reflects 2019 output being above the 2015 base.)
- Circularity (recycling): EAF/recycled share 27 % → 53 % (CLEVER corridor 50–77 %; Belgium at the low end, consistent with its ~30–50 % EU-average group).
- Efficiency (intensity, DERIVED): MODEIRE route intensities — primary (BF-BOF, then H-DRI) 5000 → 4060 MWh/kt; recycled (EAF) 1500 → 1020 MWh/kt — blended by the recycled share. Hydrogen for H-DRI is counted inside the intensity.
Cement (52–99, 560–800, 31–64)
- Production → 75 % (−25 %): lower cement/capita, timber & carbon-concrete construction, less new road infrastructure, concrete recycling (14 → ~34 %), clinker substitutes.
- Efficiency (intensity 904 → 650 MWh/kt, −28 %): dry-kiln conversion, clinker substitutes (GGBS/PFA/ limestone), fuel switch to biomass, waste-heat recovery. (No clean two-route split → intensity stated, not derived.)
Glass (61–95, 700–2190, 23–68)
- Production → 85 % (−15 %): material efficiency, reuse, lightweighting.
- Circularity: cullet share 40 % → 63 % (CLEVER). (The dashboard 'Share of recycled glass' column is empty → value taken from the CLEVER corridor text.)
- Efficiency (intensity 4300 → 1400 MWh/kt, −67 %): full furnace electrification (electric baths to 100 % by 2050). ⚠️ The Belgian 2020 baseline intensity (4300) is far above the MODEIRE primary value (3500); the route-based derivation only partially reconciles (see 4.3).
Ammonia (58–80, 1580–2500)
- Production → 70 % (−30 %): synthetic-fertiliser demand reduction (corridor −40–50 %: agro-ecology, legume rotations, less food waste). Belgium adopts a milder −30 %.
- Efficiency (intensity 5086 → 2000 MWh/kt, excl. feedstock): switch to green-hydrogen Haber-Bosch (electrolytic H₂). Feedstock H₂ is tracked separately (4.6 / Annex 2).
HVC (high-value chemicals) (59–98, 3140–5680)
- Production → 90 % (−10 %): plastics-demand reduction (single-use bans, reuse). ⚠️ Belgium is very conservative here (corridor allows −41 %), reflecting the weight of the Antwerp petrochemical cluster.
- Efficiency (intensity 5376 → 4400 MWh/kt): modest; note CLEVER warns intensity can rise on the H₂-via- methanol (MTO/MTA) route. Feedstock shift oil → H₂ is large and tracked separately (4.6).
Pulp & paper (58–110, 1890–3780, 31–64)
- Production → 85 % of 2015 (packaging + / graphic − / lightweighting; 2020 already at 83 %).
- Circularity: recycling 62 % → 80 % (CLEVER). (Dashboard 'Share of recycled pulp' empty → CLEVER text.)
- Efficiency (intensity 4276 → 2500 MWh/kt): impulse/steam drying, heat pumps, electrification. ⚠️ MODEIRE pulp route intensities do not map onto the paper unit-consumption (which includes drying/finishing) → the route derivation fails (see 4.3); intensity is stated explicitly.
Light sectors (direct gross-FEC reduction relative to 2020)
- Other chemicals → 79 % (corridor 69–89 %); Non-ferrous → 65 % (39–87 %; secondary metals + induction electrification); Food → 45 % (42–64 %; diet change + electrified heating/cooling); Other industries → 75 % (63–85 %; generic efficiency + electrification).
# === 4.2 Heavy-sector levers ==========================================================================
# demand_index : gross material-demand index [% of 2015] (sufficiency + circularity) -> production
# rec_share : recycled-route share [-] (circularity)
# route_int : (primary, recycled) intensities as (value_2015, value_2050) [MWh/kt] (efficiency; MODEIRE)
# intensity : adopted blended energy intensity [MWh/kt] (used when route derivation is unavailable/unreliable)
HEAVY = {
"steel": dict(
P2015=7257.0, P2015_src="JRC-IDEES-2015 crude steel",
corridor_prod="74-92", corridor_int="2060-2690",
demand_index={2020: 106.931, 2030: 97.281, 2040: 87.400, 2050: 85.000},
rec_share ={2020: 0.270, 2030: 0.370, 2040: 0.495, 2050: 0.530},
route_int ={"primary": (5000.0, 4060.0), "recycled": (1500.0, 1020.0)},
derive_intensity=True,
intensity ={2020: 3954.282, 2030: 3226.398, 2040: 2557.570, 2050: 2300.000}),
"cement": dict(
P2015=6275.0, P2015_src="CLEVER (JRC-2015=5551 kt; +13% — flagged)",
corridor_prod="52-99", corridor_int="560-800",
demand_index={2020: 107.936, 2030: 93.444, 2040: 79.000, 2050: 75.000},
rec_share=None, route_int=None, derive_intensity=False,
intensity ={2020: 904.114, 2030: 792.304, 2040: 686.749, 2050: 650.000}),
"glass": dict(
P2015=1000.0, P2015_src="CLEVER (JRC-2015=1108 kt; -10% — flagged)",
corridor_prod="61-95", corridor_int="700-2190",
demand_index={2020: 100.000, 2030: 93.400, 2040: 87.400, 2050: 85.000},
rec_share ={2020: 0.450, 2030: 0.510, 2040: 0.570, 2050: 0.630}, # CLEVER cullet (dashboard col empty)
route_int ={"primary": (3500.0, 2000.0), "recycled": (2500.0, 1300.0)},
derive_intensity=False, # MODEIRE only partially reconciles -> keep adopted intensity, show derivation
intensity ={2020: 4300.000, 2030: 3024.000, 2040: 1864.000, 2050: 1400.000}),
"ammonia": dict(
P2015=1050.0, P2015_src="CLEVER/USGS (not tracked physically by JRC-IDEES)",
corridor_prod="58-80", corridor_int="1580-2500",
demand_index={2020: 100.000, 2030: 86.800, 2040: 74.800, 2050: 70.000},
rec_share=None, route_int=None, derive_intensity=False,
intensity ={2020: 5085.714, 2030: 3728.000, 2040: 2493.714, 2050: 2000.000}),
"hvc": dict(
P2015=5580.0, P2015_src="CLEVER (HVC split from basic chemicals)",
corridor_prod="59-98", corridor_int="3140-5680",
demand_index={2020: 100.000, 2030: 95.600, 2040: 91.600, 2050: 90.000},
rec_share=None, route_int=None, derive_intensity=False,
intensity ={2020: 5376.344, 2030: 4946.753, 2040: 4556.215, 2050: 4400.000}),
"paper": dict(
P2015=2123.0, P2015_src="CLEVER paper&printing index basis (JRC-2015 paper=2050 kt)",
corridor_prod="58-110", corridor_int="1890-3780",
demand_index={2020: 83.137, 2030: 83.957, 2040: 87.400, 2050: 85.000},
rec_share ={2020: 0.620, 2030: 0.680, 2040: 0.740, 2050: 0.800}, # CLEVER recycling (dashboard col empty)
route_int ={"primary": (5400.0, 3300.0), "recycled": (460.0, 280.0)},
derive_intensity=False, # MODEIRE pulp routes do NOT map to paper unit-consumption (see 4.3)
intensity ={2020: 4275.631, 2030: 3494.353, 2040: 2722.169, 2050: 2500.000}),
}
def production_kt(sector):
p = HEAVY[sector]
return pd.Series({y: p["P2015"] * p["demand_index"][y] / 100.0 for y in YEARS}, name=sector)
production = pd.DataFrame({s: production_kt(s) for s in HEAVY}).T
print("Reconstructed Belgian production [kt] (= P2015 x gross demand index)")
production.round(1)
Reconstructed Belgian production [kt] (= P2015 x gross demand index)
| 2020 | 2030 | 2040 | 2050 | |
|---|---|---|---|---|
| steel | 7,760.000 | 7,059.700 | 6,342.600 | 6,168.400 |
| cement | 6,773.000 | 5,863.600 | 4,957.200 | 4,706.200 |
| glass | 1,000.000 | 934.000 | 874.000 | 850.000 |
| ammonia | 1,050.000 | 911.400 | 785.400 | 735.000 |
| hvc | 5,580.000 | 5,334.500 | 5,111.300 | 5,022.000 |
| paper | 1,765.000 | 1,782.400 | 1,855.500 | 1,804.600 |
# === 4.3 Energy intensity: DERIVE from recycling + route intensities where possible ==================
def _interp(v2015, v2050, y):
return v2015 + (v2050 - v2015) * (y - BASE_YEAR) / (2050 - BASE_YEAR)
def route_blend_intensity(sector, year):
'''Blended intensity [MWh/kt] = s_rec*I_rec + (1-s_rec)*I_prim, with linear route improvement 2015->2050.'''
p = HEAVY[sector]
if p["rec_share"] is None or p["route_int"] is None:
return np.nan
s = p["rec_share"][year]
ip = _interp(*p["route_int"]["primary"], year)
ir = _interp(*p["route_int"]["recycled"], year)
return s * ir + (1 - s) * ip
# Compare the route-derived intensity to the adopted (dashboard) intensity:
rows = []
for s in HEAVY:
for y in YEARS:
adopted = HEAVY[s]["intensity"][y]
derived = route_blend_intensity(s, y)
rows.append((s, y, derived, adopted,
np.nan if np.isnan(derived) else derived / adopted - 1.0))
intensity_check = pd.DataFrame(rows, columns=["sector", "year", "route-derived", "adopted", "rel.err"])
intensity_check = intensity_check.pivot(index="sector", columns="year")
print("Energy intensity [MWh/kt]: route-derived vs adopted, and relative error")
intensity_check.round(3)
Energy intensity [MWh/kt]: route-derived vs adopted, and relative error
| route-derived | adopted | rel.err | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| year | 2020 | 2030 | 2040 | 2050 | 2020 | 2030 | 2040 | 2050 | 2020 | 2030 | 2040 | 2050 |
| sector | ||||||||||||
| ammonia | NaN | NaN | NaN | NaN | 5,085.714 | 3,728.000 | 2,493.714 | 2,000.000 | NaN | NaN | NaN | NaN |
| cement | NaN | NaN | NaN | NaN | 904.114 | 792.304 | 686.749 | 650.000 | NaN | NaN | NaN | NaN |
| glass | 2,855.000 | 2,412.714 | 1,980.714 | 1,559.000 | 4,300.000 | 3,024.000 | 1,864.000 | 1,400.000 | -0.336 | -0.202 | 0.063 | 0.114 |
| hvc | NaN | NaN | NaN | NaN | 5,376.344 | 4,946.753 | 4,556.215 | 4,400.000 | NaN | NaN | NaN | NaN |
| paper | 2,207.257 | 1,700.343 | 1,259.257 | 884.000 | 4,275.631 | 3,494.353 | 2,722.169 | 2,500.000 | -0.484 | -0.513 | -0.537 | -0.646 |
| steel | 3,938.457 | 3,375.086 | 2,758.714 | 2,448.800 | 3,954.282 | 3,226.398 | 2,557.570 | 2,300.000 | -0.004 | 0.046 | 0.079 | 0.065 |
Reading 4.3. For steel the recycling/route derivation reproduces the adopted intensity within ~0–8 % (the small positive bias means CLEVER applied slightly more efficiency than a pure route blend, e.g. early H-DRI). Glass reconciles only mid-period — the Belgian 2020 baseline (4300 MWh/kt) is ~+23 % above the MODEIRE primary value (3500), so the absolute level is not explained by the documented routes. For paper the MODEIRE pulp intensities are ~2× too low versus the paper unit-consumption (which includes drying and finishing), so the route derivation is not usable.
⚠️ Decision. We therefore use the route derivation only for steel (set
derive_intensity=True) and keep the explicit adopted intensity for the other sectors, flagging glass and paper as not fully derivable.
def intensity_MWh_per_kt(sector, year):
p = HEAVY[sector]
if p.get("derive_intensity"):
return route_blend_intensity(sector, year)
return p["intensity"][year]
energy_intensity = pd.DataFrame(
{s: pd.Series({y: intensity_MWh_per_kt(s, y) for y in YEARS}) for s in HEAVY}).T
print("Adopted Belgian energy intensity [MWh/kt] (steel = route-derived, others = explicit)")
energy_intensity.round(1)
Adopted Belgian energy intensity [MWh/kt] (steel = route-derived, others = explicit)
| 2020 | 2030 | 2040 | 2050 | |
|---|---|---|---|---|
| steel | 3,938.500 | 3,375.100 | 2,758.700 | 2,448.800 |
| cement | 904.100 | 792.300 | 686.700 | 650.000 |
| glass | 4,300.000 | 3,024.000 | 1,864.000 | 1,400.000 |
| ammonia | 5,085.700 | 3,728.000 | 2,493.700 | 2,000.000 |
| hvc | 5,376.300 | 4,946.800 | 4,556.200 | 4,400.000 |
| paper | 4,275.600 | 3,494.400 | 2,722.200 | 2,500.000 |
# === 4.4 FEC by sector ===============================================================================
# Heavy: FEC = production[kt] x intensity[MWh/kt] x 1e-6 (TWh)
# Light: direct gross-FEC reduction relative to the 2020 baseline.
def heavy_fec(sector):
return pd.Series({y: production_kt(sector)[y] * intensity_MWh_per_kt(sector, y) * 1e-6 for y in YEARS},
name=sector)
# Light sectors: 2020 baseline FEC [TWh] x gross-FEC index [% of 2020]
LIGHT = {
"other chemicals": dict(fec2020=14.048, fec_index={2020: 100.0, 2030: 90.8, 2040: 82.4, 2050: 79.0}, corridor="69-89"),
"non-ferrous": dict(fec2020= 3.475, fec_index={2020: 100.0, 2030: 84.6, 2040: 70.6, 2050: 65.0}, corridor="39-87"),
"food": dict(fec2020=18.475, fec_index={2020: 100.0, 2030: 75.8, 2040: 53.8, 2050: 45.0}, corridor="42-64"),
"other industries": dict(fec2020=18.676, fec_index={2020: 100.0, 2030: 89.0, 2040: 79.0, 2050: 75.0}, corridor="63-85"),
}
def light_fec(sector):
p = LIGHT[sector]
return pd.Series({y: p["fec2020"] * p["fec_index"][y] / 100.0 for y in YEARS}, name=sector)
sector_fec = pd.DataFrame({s: heavy_fec(s) for s in HEAVY}).T
for s in LIGHT:
sector_fec.loc[s] = light_fec(s)
sector_fec.loc["chemicals (total)"] = sector_fec.loc[["ammonia", "hvc", "other chemicals"]].sum()
ordered = ["steel", "cement", "glass", "ammonia", "hvc", "other chemicals",
"chemicals (total)", "non-ferrous", "paper", "food", "other industries"]
sector_fec = sector_fec.loc[ordered]
sector_fec.loc["TOTAL"] = sector_fec.drop(index="chemicals (total)").sum()
print("Reconstructed Belgian industry FEC by sector [TWh] (from explicit hypotheses)")
sector_fec.round(3)
Reconstructed Belgian industry FEC by sector [TWh] (from explicit hypotheses)
| 2020 | 2030 | 2040 | 2050 | |
|---|---|---|---|---|
| steel | 30.562 | 23.827 | 17.497 | 15.105 |
| cement | 6.124 | 4.646 | 3.404 | 3.059 |
| glass | 4.300 | 2.824 | 1.629 | 1.190 |
| ammonia | 5.340 | 3.398 | 1.959 | 1.470 |
| hvc | 30.000 | 26.388 | 23.288 | 22.097 |
| other chemicals | 14.048 | 12.756 | 11.576 | 11.098 |
| chemicals (total) | 49.388 | 42.542 | 36.822 | 34.665 |
| non-ferrous | 3.475 | 2.940 | 2.453 | 2.259 |
| paper | 7.546 | 6.228 | 5.051 | 4.511 |
| food | 18.475 | 14.004 | 9.940 | 8.314 |
| other industries | 18.676 | 16.622 | 14.754 | 14.007 |
| TOTAL | 138.546 | 113.633 | 91.551 | 83.110 |
# === 4.5 Energy-carrier split (electrification & fuel-switch hypotheses) ==============================
# The CLEVER 'energy carrier corridor' is expressed here as explicit SHARES of total FEC. Multiplying the
# shares by the reconstructed total FEC ties the carriers to the sectoral build-up above.
# [CLEVER-REP]: EU industry electricity 32% (2019) -> 64% (2050); gas 31% -> 10%; coal phased out before 2040;
# ~190 TWh H2 as energy use in EU industry by 2050.
CARRIER_SHARE = { # share of total industrial FEC [-]
"coal": {2020: 0.1558, 2030: 0.0872, 2040: 0.000, 2050: 0.000},
"solid biomass": {2020: 0.0684, 2030: 0.0691, 2040: 0.070, 2050: 0.070},
"oil": {2020: 0.1046, 2030: 0.0586, 2040: 0.0224,2050: 0.000},
"methane": {2020: 0.3599, 2030: 0.2808, 2040: 0.130, 2050: 0.090},
"electricity": {2020: 0.2733, 2030: 0.4699, 2040: 0.656, 2050: 0.720},
"heat": {2020: 0.0380, 2030: 0.0345, 2040: 0.0316,2050: 0.030},
"hydrogen": {2020: 0.000, 2030: 0.000, 2040: 0.090, 2050: 0.090},
"waste": {2020: 0.000, 2030: 0.000, 2040: 0.000, 2050: 0.000},
"ambient heat": {2020: 0.000, 2030: 0.000, 2040: 0.000, 2050: 0.000},
"thermal solar": {2020: 0.000, 2030: 0.000, 2040: 0.000, 2050: 0.000},
}
total_fec = sector_fec.loc["TOTAL"]
carrier_share = pd.DataFrame(CARRIER_SHARE).T
# renormalise shares so they sum exactly to 1 each year (guards against rounding of the explicit shares):
carrier_share = carrier_share / carrier_share.sum()
carrier_fec = carrier_share.multiply(total_fec, axis=1)
carrier_fec.loc["TOTAL"] = carrier_fec.sum()
print("Reconstructed Belgian industry FEC by carrier [TWh] (= total FEC x explicit carrier shares)")
display(carrier_fec.round(3))
print("\nElectrification path — electricity share of industrial FEC:")
print((carrier_share.loc["electricity"] * 100).round(1).to_string())
Reconstructed Belgian industry FEC by carrier [TWh] (= total FEC x explicit carrier shares)
| 2020 | 2030 | 2040 | 2050 | |
|---|---|---|---|---|
| coal | 21.586 | 9.908 | 0.000 | 0.000 |
| solid biomass | 9.477 | 7.851 | 6.409 | 5.818 |
| oil | 14.492 | 6.658 | 2.051 | 0.000 |
| methane | 49.863 | 31.905 | 11.902 | 7.480 |
| electricity | 37.865 | 53.391 | 60.058 | 59.839 |
| heat | 5.265 | 3.920 | 2.893 | 2.493 |
| hydrogen | 0.000 | 0.000 | 8.240 | 7.480 |
| waste | 0.000 | 0.000 | 0.000 | 0.000 |
| ambient heat | 0.000 | 0.000 | 0.000 | 0.000 |
| thermal solar | 0.000 | 0.000 | 0.000 | 0.000 |
| TOTAL | 138.546 | 113.633 | 91.551 | 83.110 |
Electrification path — electricity share of industrial FEC: 2020 27.300 2030 47.000 2040 65.600 2050 72.000
# === 4.6 Non-energy (feedstock) consumption ===========================================================
# Feedstock shifts from oil/gas (naphtha cracking, methane reforming) to hydrogen (HVC via methanol, green NH3).
# [CLEVER-REP]: EU chemical feedstock 650 TWh (2019) -> 480 TWh (2050), hydrogen reaching 78% of feedstocks.
FEEDSTOCK = {
"oil (naphtha)": {2020: 37.206, 2030: 70.000, 2040: 36.000, 2050: 10.500},
"gas": {2020: 6.420, 2030: 6.184, 2040: 3.051, 2050: 1.140},
"coal": {2020: 5.051, 2030: 3.085, 2040: 0.000, 2050: 0.000},
"hydrogen": {2020: 0.000, 2030: 0.000, 2040: 24.000, 2050: 40.500},
"solid biomass": {2020: 0.000, 2030: 0.000, 2040: 0.000, 2050: 0.000},
}
feedstock_fec = pd.DataFrame(FEEDSTOCK).T
feedstock_fec.loc["TOTAL"] = feedstock_fec.sum()
print("Reconstructed Belgian industry NON-ENERGY feedstock [TWh]")
feedstock_fec.round(3)
Reconstructed Belgian industry NON-ENERGY feedstock [TWh]
| 2020 | 2030 | 2040 | 2050 | |
|---|---|---|---|---|
| oil (naphtha) | 37.206 | 70.000 | 36.000 | 10.500 |
| gas | 6.420 | 6.184 | 3.051 | 1.140 |
| coal | 5.051 | 3.085 | 0.000 | 0.000 |
| hydrogen | 0.000 | 0.000 | 24.000 | 40.500 |
| solid biomass | 0.000 | 0.000 | 0.000 | 0.000 |
| TOTAL | 48.677 | 79.269 | 63.051 | 52.140 |
⚠️ 2030 oil-feedstock spike. The non-energy oil feedstock jumps 37 → 70 → 36 → 10.5 TWh in the dashboard. This non-monotonic profile is not explained in the CLEVER documentation and looks like a dashboard artefact (possibly refinery/feedstock accounting). It is reproduced for fidelity but flagged as likely incorrect — a linear 37 → 10 TWh decline would be more consistent with the green-feedstock shift.
5. Reconstructed PyPSA-Eur industry input for Belgium¶
The eight PyPSA-Eur carrier columns of §1 are assembled from the reconstructed trajectories and written as
clever_Industry_<year>_BE_reconstructed.csv (Belgium row), ready to drop into PyPSA-Eur in place of the
dashboard export.
def pypsa_industry_inputs():
rows = {}
for y in YEARS:
rows[y] = {
"ammonia": sector_fec.loc["ammonia", y],
"electricity": carrier_fec.loc["electricity", y],
"coal": carrier_fec.loc["coal", y],
"solid biomass": carrier_fec.loc["solid biomass", y],
"methane": carrier_fec.loc["methane", y],
"low-temperature heat": carrier_fec.loc["heat", y],
"hydrogen": carrier_fec.loc["hydrogen", y] + feedstock_fec.loc["hydrogen", y],
"naphtha": feedstock_fec.loc["oil (naphtha)", y] + carrier_fec.loc["oil", y],
}
return pd.DataFrame(rows)
pypsa_inputs = pypsa_industry_inputs()
print("Reconstructed PyPSA-Eur industry carrier inputs for Belgium [TWh]")
pypsa_inputs.round(3)
Reconstructed PyPSA-Eur industry carrier inputs for Belgium [TWh]
| 2020 | 2030 | 2040 | 2050 | |
|---|---|---|---|---|
| ammonia | 5.340 | 3.398 | 1.959 | 1.470 |
| electricity | 37.865 | 53.391 | 60.058 | 59.839 |
| coal | 21.586 | 9.908 | 0.000 | 0.000 |
| solid biomass | 9.477 | 7.851 | 6.409 | 5.818 |
| methane | 49.863 | 31.905 | 11.902 | 7.480 |
| low-temperature heat | 5.265 | 3.920 | 2.893 | 2.493 |
| hydrogen | 0.000 | 0.000 | 32.240 | 47.980 |
| naphtha | 51.698 | 76.658 | 38.051 | 10.500 |
# Write one CSV per horizon, with the EXACT CLEVER dashboard column names PyPSA-Eur reads (BE row only).
CLEVER_COLS = {
"ammonia": "Total Final Energy Consumption of the ammonia industry",
"electricity": "Total Final electricity consumption in industry",
"coal": "Total Final energy consumption from solid fossil fuels (coal ...) in industry",
"solid biomass": "Total Final energy consumption from solid biomass in industry",
"methane": "Total Final energy consumption from gas grid / gas consumed locally in industry",
"low-temperature heat": "Total Final heat consumption in industry",
}
for y in YEARS:
row = {CLEVER_COLS[k]: pypsa_inputs.loc[k, y] for k in CLEVER_COLS}
row["Total Final hydrogen consumption in industry"] = carrier_fec.loc["hydrogen", y]
row["Non-energy consumption of hydrogen for the feedstock production"] = feedstock_fec.loc["hydrogen", y]
row["Total Final oil consumption in industry"] = carrier_fec.loc["oil", y]
row["Non-energy consumption of oil for the feedstock production"] = feedstock_fec.loc["oil (naphtha)", y]
pd.DataFrame({COUNTRY: row}).T.to_csv(OUT_DIR / f"clever_Industry_{y}_BE_reconstructed.csv")
print("Written reconstructed inputs to:", OUT_DIR)
sorted(p.name for p in OUT_DIR.glob("*.csv"))
Written reconstructed inputs to: data/industry_output
['clever_Industry_2020_BE_reconstructed.csv', 'clever_Industry_2030_BE_reconstructed.csv', 'clever_Industry_2040_BE_reconstructed.csv', 'clever_Industry_2050_BE_reconstructed.csv']
6. Validation against the CLEVER dashboard¶
We compare the reconstructed Belgian carrier inputs against the dashboard exports in
data/clever_dashboard_reference/ (used only for validation). Because steel intensity is now derived
(not copied), a small residual is expected; we report it explicitly.
def dashboard_pypsa_inputs():
rows = {}
for y in YEARS:
be = pd.read_csv(CLEVER_REF_DIR / f"clever_Industry_{y}.csv", index_col=0).loc[COUNTRY]
rows[y] = {
"ammonia": be["Total Final Energy Consumption of the ammonia industry"],
"electricity": be["Total Final electricity consumption in industry"],
"coal": be["Total Final energy consumption from solid fossil fuels (coal ...) in industry"],
"solid biomass": be["Total Final energy consumption from solid biomass in industry"],
"methane": be["Total Final energy consumption from gas grid / gas consumed locally in industry"],
"low-temperature heat": be["Total Final heat consumption in industry"],
"hydrogen": be["Total Final hydrogen consumption in industry"]
+ be["Non-energy consumption of hydrogen for the feedstock production"],
"naphtha": be["Non-energy consumption of oil for the feedstock production"]
+ be["Total Final oil consumption in industry"],
}
return pd.DataFrame(rows)
dash = dashboard_pypsa_inputs()
print("Absolute deviation reconstruction - dashboard [TWh]:")
display((pypsa_inputs - dash).round(3))
print("Relative deviation [%] (where dashboard != 0):")
((pypsa_inputs - dash) / dash.replace(0, np.nan) * 100).round(1)
Absolute deviation reconstruction - dashboard [TWh]:
| 2020 | 2030 | 2040 | 2050 | |
|---|---|---|---|---|
| ammonia | -0.000 | -0.240 | -0.000 | 0.000 |
| electricity | -0.035 | -0.088 | 0.841 | 0.661 |
| coal | -0.014 | -0.021 | 0.000 | 0.000 |
| solid biomass | -0.003 | -0.011 | 0.090 | 0.064 |
| methane | -0.045 | -0.050 | 0.167 | 0.083 |
| low-temperature heat | -0.010 | -0.007 | 0.040 | 0.028 |
| hydrogen | 0.000 | 0.000 | 0.115 | 0.083 |
| naphtha | -0.015 | -0.010 | 0.029 | 0.000 |
Relative deviation [%] (where dashboard != 0):
| 2020 | 2030 | 2040 | 2050 | |
|---|---|---|---|---|
| ammonia | -0.000 | -6.600 | -0.000 | 0.000 |
| electricity | -0.100 | -0.200 | 1.400 | 1.100 |
| coal | -0.100 | -0.200 | NaN | NaN |
| solid biomass | -0.000 | -0.100 | 1.400 | 1.100 |
| methane | -0.100 | -0.200 | 1.400 | 1.100 |
| low-temperature heat | -0.200 | -0.200 | 1.400 | 1.100 |
| hydrogen | NaN | NaN | 0.400 | 0.200 |
| naphtha | -0.000 | -0.000 | 0.100 | 0.000 |
max_abs = (pypsa_inputs - dash).abs().to_numpy().max()
print(f"Maximum absolute deviation across all carriers and years: {max_abs:.3f} TWh")
# steel intensity is derived from routes, so allow a small residual (~1 TWh on the steel-heavy carriers):
assert max_abs < 1.5, "Reconstruction deviates more than expected from the dashboard!"
print("VALIDATION OK — the explicit hypotheses reproduce the CLEVER dashboard within the expected tolerance.")
Maximum absolute deviation across all carriers and years: 0.841 TWh VALIDATION OK — the explicit hypotheses reproduce the CLEVER dashboard within the expected tolerance.
# Total-FEC accounting identity: sum over sectors == sum over carriers == dashboard total.
dash_total = pd.Series({y: pd.read_csv(CLEVER_REF_DIR/f"clever_Industry_{y}.csv", index_col=0)
.loc[COUNTRY, "Total FEC of industry (excl. consumption of the energy sector)"]
for y in YEARS})
check = pd.DataFrame({
"sum of sectors": sector_fec.loc["TOTAL"],
"sum of carriers": carrier_fec.loc["TOTAL"],
"dashboard total": dash_total,
})
print("Total industrial FEC [TWh] — accounting identity check")
check.round(3)
Total industrial FEC [TWh] — accounting identity check
| sum of sectors | sum of carriers | dashboard total | |
|---|---|---|---|
| 2020 | 138.546 | 138.546 | 138.669 |
| 2030 | 113.633 | 113.633 | 113.819 |
| 2040 | 91.551 | 91.551 | 90.270 |
| 2050 | 83.110 | 83.110 | 82.192 |
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
sector_fec.drop(index=["chemicals (total)", "TOTAL"]).T.plot(
kind="bar", stacked=True, ax=axes[0], colormap="tab20", width=0.7)
axes[0].set_title("Belgian industry FEC by sector"); axes[0].set_ylabel("TWh"); axes[0].set_xlabel("")
axes[0].legend(fontsize=7, ncol=2)
plot_car = carrier_fec.drop(index="TOTAL").T
plot_car = plot_car.loc[:, (plot_car != 0).any()]
plot_car.plot(kind="bar", stacked=True, ax=axes[1], colormap="Set2", width=0.7)
axes[1].set_title("Belgian industry FEC by carrier (electrification)"); axes[1].set_ylabel("TWh"); axes[1].set_xlabel("")
axes[1].legend(fontsize=8)
plt.tight_layout(); plt.show()
7. Discrepancies, gaps and future work¶
The notebook now expresses the Belgian industry inputs as explicit sufficiency / circularity / efficiency levers and derives the steel energy intensity from the recycling rate and route intensities. Remaining gaps:
- Gross demand reductions are stated, not derived. The production indices (e.g. steel −15 %, cement −25 %, ammonia −30 %, HVC −10 %) come from the Belgian CLEVER trajectory; re-deriving them from a Belgian end-use / stock model (buildings m², vehicle fleet, fertiliser demand, plastics demand) is the main open task.
- Intensity derivation only works for steel. Glass (baseline 4300 ≫ MODEIRE primary 3500) and paper (pulp routes ≠ paper unit-consumption, ~2× gap) do not reconcile with the documented MODEIRE routes → intensities kept explicit; better route data / electrification curves are needed.
- Recycling shares for glass and paper are taken from the CLEVER corridor text because the dashboard
Share of recycled glass/pulpcolumns are empty. - Baseline mismatch JRC-IDEES ↔ CLEVER (steel coke/BF energy; cement +13 %, glass −10 % production volumes). The exact bridge should be reconstructed.
- 2030 oil-feedstock spike (37 → 70 → 36 → 10.5 TWh) — non-monotonic and unexplained; likely a dashboard artefact.
- Per-sector → per-carrier allocation is still an aggregate assumption (carrier shares applied to the total), not a sector-resolved fuel mix.
- HVC under-ambition / chemicals weight. Belgium keeps HVC at −10 % (corridor allows −41 %); given the Antwerp cluster this dominates the result and deserves a dedicated sensitivity.
- Process emissions & energy sector (refineries) are out of scope here and should be added for a complete balance. Only Belgium is reconstructed; the same machinery extends to DE/FR/GB/NL.
With steel derived from levers and everything else stated explicitly, the reconstruction still matches the dashboard within ~1 TWh on every carrier and year (§6), while every number is now a documented, corridor- anchored hypothesis rather than an opaque import.
8. Website export¶
The cell below exports the explicit industry levers (sector final-energy demand,
recycling/circularity shares and the energy-carrier mix) to the public website
(website/data/industry.js). Re-run it after changing any CLEVER hypothesis so
the website stays in sync. See the Public sufficiency website section of
README.md for the data contract.
# ===== Website export: build website/data/industry.js =====
_NB = "../notebooks/nW_BE_demand_model_industry.html"
Y0, Y1 = YEARS[0], YEARS[-1] # 2020 -> 2050
_H = []
def _add(*a, **k):
_H.append(make_hypothesis(*a, **k))
# Sector final-energy demand (TWh)
def _fec(sector, hid, label, ref):
if sector in sector_fec.index:
_add(hid, label, "Industry / energy demand",
round(float(sector_fec.loc[sector, Y0]), 1),
round(float(sector_fec.loc[sector, Y1]), 1),
unit="TWh", ref_year=Y0, target_year=Y1, notebook=_NB, reference=ref)
# Circularity / recycling shares (HEAVY[*]["rec_share"])
def _rec(sector, hid, label, ref):
rs = HEAVY[sector]["rec_share"]
_add(hid, label, "Industry / circularity",
round(float(rs[Y0]) * 100, 1), round(float(rs[Y1]) * 100, 1),
unit="% recycled", ref_year=Y0, target_year=Y1, notebook=_NB, reference=ref)
# Energy-carrier mix (CARRIER_SHARE)
def _car(carrier, hid, label):
cs = CARRIER_SHARE[carrier]
_add(hid, label, "Industry / energy mix",
round(float(cs[Y0]) * 100, 1), round(float(cs[Y1]) * 100, 1),
unit="% of FEC", ref_year=Y0, target_year=Y1, notebook=_NB,
reference="CLEVER electrification corridor")
# --- Overall ---
if "TOTAL" in sector_fec.index:
_add("ind-total", "Total industrial energy demand", "Industry / energy demand",
round(float(sector_fec.loc["TOTAL", Y0]), 1),
round(float(sector_fec.loc["TOTAL", Y1]), 1),
unit="TWh", ref_year=Y0, target_year=Y1, notebook=_NB,
reference="Sum of all sectors")
# --- Energy mix ---
_car("electricity", "ind-electricity", "Electrification of industry")
_car("coal", "ind-coal", "Coal in industry")
_car("methane", "ind-methane", "Fossil gas (methane) in industry")
_car("hydrogen", "ind-hydrogen", "Hydrogen in industry")
# --- Heavy sectors ---
_fec("steel", "steel-fec", "Steel energy demand", "Sufficiency + EAF + H-DRI")
_rec("steel", "steel-recycling", "Steel from recycling (EAF)", "CLEVER steel corridor")
_fec("cement", "cement-fec", "Cement energy demand", "Less clinker, biomass fuel")
_fec("glass", "glass-fec", "Glass energy demand", "Electric furnaces by 2050")
_rec("glass", "glass-recycling", "Glass from cullet (recycled)", "CLEVER glass corridor")
_fec("ammonia", "ammonia-fec", "Ammonia energy demand", "Green-hydrogen Haber-Bosch")
_fec("hvc", "hvc-fec", "High-value chemicals energy demand", "Plastics recirculation")
_fec("paper", "paper-fec", "Pulp & paper energy demand", "Drying tech, heat pumps")
_rec("paper", "paper-recycling", "Paper from recycling", "CLEVER paper corridor")
# --- Light sectors ---
_fec("other chemicals", "otherchem-fec", "Other chemicals energy demand", "Efficiency + electrification")
_fec("non-ferrous", "nonferrous-fec", "Non-ferrous metals energy demand", "Secondary metals, induction")
_fec("food", "food-fec", "Food industry energy demand", "Diet change, electrified heat")
_fec("other industries", "otherind-fec", "Other industries energy demand", "Generic efficiency")
# --- Plot: sector FEC, 2020 vs 2050 ---
_plot_sectors = ["steel", "cement", "glass", "ammonia", "hvc", "other chemicals",
"non-ferrous", "paper", "food", "other industries"]
_plot_sectors = [s for s in _plot_sectors if s in sector_fec.index]
_plots = {
"sectorFec": {"type": "stackedBar", "x": [str(Y0), str(Y1)], "yTitle": "Final energy demand [TWh]",
"series": [{"name": s.capitalize(),
"y": [round(float(sector_fec.loc[s, Y0]), 1),
round(float(sector_fec.loc[s, Y1]), 1)]} for s in _plot_sectors]},
}
write_hypotheses_js("industry", _H, plots=_plots, title="Industry")
import pandas as _pd
_pd.DataFrame([{ "id": h["id"], "hypothesis": h["name"], "category": h["category"],
str(Y0): h["refValue"], str(Y1): h["targetValue"], "unit": h["unit"] }
for h in _H])