District Heating Luleå

Heat Generation Mix, Industrial Contracts & Transition Projections

Author

Amna Maqsood — George Washington University / LTU

Published

June 16, 2026

Overview

Luleå’s district heating system is one of Sweden’s most distinctive — and most exposed. For over 50 years, roughly 90% of the city’s heat has come from a single industrial partner: SSAB’s steel plant on the waterfront. That partnership is now entering its most consequential transition since it began.

Note

2025 heat generation mix — Luleå Energi (official)
86% Recycled · 9% Renewable · 5% Fossil
Source: Luleå Energi environmental declaration 2025


Heat generation mix

Historical mix 2019–2025

Show code
heat_mix <- data.frame(
  year                   = c(2019, 2020, 2021, 2022, 2023, 2024, 2025),
  ssab_gas_lulekraft_pct = c(87,   86,   85,   84,   82,   80,   78),
  biofuel_pellets_pct    = c(8,    9,    9,    9,    10,   10,   9),
  electric_boilers_pct   = c(3,    3,    4,    4,    5,    6,    5),
  other_pct              = c(2,    2,    2,    3,    3,    4,    4)
)

heat_long <- heat_mix |>
  pivot_longer(-year, names_to = "source", values_to = "pct") |>
  mutate(source = factor(source,
    levels = c("ssab_gas_lulekraft_pct","biofuel_pellets_pct",
               "electric_boilers_pct","other_pct"),
    labels = c("SSAB gas (Lulekraft)","Biofuel / pellets",
               "Electric boilers","Other")
  ))

p_mix <- ggplot(heat_long, aes(x = factor(year), y = pct, fill = source,
                               text = paste0(source, ": ", pct, "%"))) +
  geom_bar(stat = "identity", width = 0.7) +
  scale_fill_manual(values = c(
    "SSAB gas (Lulekraft)" = "#A32D2D",
    "Biofuel / pellets"    = "#D4A520",
    "Electric boilers"     = "#90CAF9",
    "Other"                = "#BA7517"
  )) +
  labs(x = "Year", y = "Share (%)", fill = NULL) +
  theme_minimal(base_size = 13) +
  theme(legend.position = "bottom",
        panel.grid.major.x = element_blank())

ggplotly(p_mix, tooltip = "text") |>
  layout(legend = list(orientation = "h", y = -0.15))

2025 breakdown — official declaration

Show code
mix_2025 <- data.frame(
  category = c("Recycled", "Renewable", "Fossil"),
  pct      = c(86, 9, 5),
  color    = c("#1D9E75", "#D4A520", "#5F5E5A")
)

plot_ly(mix_2025,
  labels = ~category, values = ~pct,
  type = "pie", hole = 0.55,
  marker = list(colors = mix_2025$color,
                line = list(color = "#FFFFFF", width = 2)),
  textinfo = "label+percent",
  textfont = list(size = 14)
) |>
  layout(
    title = list(text = "Luleå DH energy mix 2025", font = list(size = 16)),
    showlegend = TRUE,
    legend = list(orientation = "h")
  )

Emission factors (2025, Luleå Energi):

Factor Value
Combustion emissions 36.91 g CO₂eq/kWh
Transport emissions 3.60 g CO₂eq/kWh
Bio-oil origin Hungary, Spain, France, Portugal
Pellet raw material Sweden (locally sourced)

Residential prices

Show code
prices <- data.frame(
  year            = c(2019,  2020,  2021,  2022,  2023,  2024),
  lulea           = c(9245,  9506,  9754,  9451, 10457, 10919),
  north_avg       = c(12884, 13201, 13550, 13890, 14820, 15671),
  south_avg       = c(15087, 15420, 15890, 16540, 18120, 19654),
  national_median = c(14200, 14600, 15100, 15800, 17900, 19466)
)

price_long <- prices |>
  pivot_longer(-year, names_to = "series", values_to = "sek") |>
  mutate(series = recode(series,
    "lulea"           = "Luleå",
    "north_avg"       = "North Sweden avg",
    "south_avg"       = "South Sweden avg",
    "national_median" = "National median"
  ))

p_price <- ggplot(price_long,
    aes(x = year, y = sek, color = series, linetype = series,
        text = paste0(series, " (", year, "): ",
                      format(sek, big.mark = ","), " SEK"))) +
  geom_line(linewidth = 1.4) +
  geom_point(size = 4) +
  scale_color_manual(values = c(
    "Luleå"            = "#A32D2D",
    "North Sweden avg" = "#185FA5",
    "South Sweden avg" = "#BA7517",
    "National median"  = "#888780"
  )) +
  scale_linetype_manual(values = c(
    "Luleå"            = "solid",
    "North Sweden avg" = "dashed",
    "South Sweden avg" = "dashed",
    "National median"  = "dotted"
  )) +
  scale_y_continuous(labels = label_comma()) +
  labs(x = "Year", y = "SEK per year (15,000 kWh)",
       color = NULL, linetype = NULL) +
  theme_minimal(base_size = 13) +
  theme(legend.position = "bottom")

ggplotly(p_price, tooltip = "text") |>
  layout(legend = list(orientation = "h", y = -0.15))
Tip

In 2024 Luleå residents paid 10,919 SEK/year — a 44% discount vs the national median of 19,466 SEK, and well below both the North Sweden average (15,671 SEK) and South Sweden average (19,654 SEK). This advantage is directly tied to the SSAB contract and is at risk as the transition unfolds.


Interactive heat source explorer

Drag the sliders to model different heat source mixes and see projected supply coverage update in real time.

```{ojs}
//| echo: false
html`<div style="display:grid; grid-template-columns:280px 1fr; gap:2rem; align-items:start; margin-top:1rem;">
  <div id="slider-panel">
    ${viewof ssab_eaf  = Inputs.range([0, 60], {value: 30, step: 1, label: "SSAB EAF (%)"})}
    ${viewof lkab      = Inputs.range([0, 50], {value: 25, step: 1, label: "LKAB (%)"})}
    ${viewof uniper    = Inputs.range([0, 30], {value: 8,  step: 1, label: "Uniper (%)"})}
    ${viewof own_prod  = Inputs.range([0, 60], {value: 12, step: 1, label: "Own production (%)"})}
    ${viewof biofuel   = Inputs.range([0, 30], {value: 10, step: 1, label: "Biofuel/pellets (%)"})}
    ${viewof electric  = Inputs.range([0, 20], {value: 1,  step: 1, label: "Electric boilers (%)"})}
  </div>
  <div id="chart-panel">
    ${(() => {
      const total = ssab_eaf + lkab + uniper + own_prod + biofuel + electric;
      const gap   = Math.max(0, 100 - total);
      const sources = [
        { source: "SSAB EAF",        pct: ssab_eaf, color: "#A32D2D" },
        { source: "LKAB",            pct: lkab,     color: "#1D9E75" },
        { source: "Uniper",          pct: uniper,   color: "#185FA5" },
        { source: "Own production",  pct: own_prod, color: "#888780" },
        { source: "Biofuel/pellets", pct: biofuel,  color: "#D4A520" },
        { source: "Electric boilers",pct: electric, color: "#90CAF9" },
        { source: "Gap",             pct: gap,      color: "#C8C8C4" },
      ];
      return Plot.plot({
        width: 480, height: 300,
        marginLeft: 20,
        x: { axis: null },
        y: { label: "% of heat demand", domain: [0, 100] },
        marks: [
          Plot.barY(sources, { x: "source", y: "pct", fill: "color", title: d => `${d.source}: ${d.pct}%` }),
          Plot.text(sources.filter(d => d.pct > 5), { x: "source", y: d => d.pct / 2, text: d => `${d.pct}%`, fill: "white", fontSize: 12, fontWeight: "bold" }),
          Plot.ruleY([0])
        ]
      });
    })()}
    ${(() => {
      const total = ssab_eaf + lkab + uniper + own_prod + biofuel + electric;
      const gap   = Math.max(0, 100 - total);
      return html`<div style="margin-top:0.6rem; padding:0.7rem 1rem; border-radius:8px;
        background:${gap > 20 ? '#FCEBEB' : gap > 8 ? '#FAEEDA' : '#E1F5EE'};
        border: 1px solid ${gap > 20 ? '#F09595' : gap > 8 ? '#FAC775' : '#5DCAA5'};
        font-size:14px;">
        <strong>Total confirmed: ${total}%</strong> &nbsp;·&nbsp;
        <span style="color:${gap > 20 ? '#A32D2D' : gap > 8 ? '#854F0B' : '#0F6E56'}; font-weight:600;">
          Gap: ${gap}%
          ${gap > 20 ? ' ⚠ High risk' : gap > 8 ? ' ⚡ Moderate gap' : ' ✓ Near full coverage'}
        </span>
      </div>`;
    })()}
  </div>
</div>`
```
ImportantOJS Syntax Error (line 184, column 7)Assigning to rvalue

Supply gap projection

Warning

Illustrative estimates validated by Fredrik Gangström (Luleå Energi). EAF ~30% confirmed. LKAB ~30% in deep technical analysis — investment decision expected ~2026, operational ~2031–2032. Uniper is furthest out and most uncertain.

Show code
gap <- data.frame(
  year             = 2024:2035,
  ssab_bf_gas      = c(80,78,75,70,60,40, 0, 0, 0, 0, 0, 0),
  biofuel_pellets  = c( 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9),
  electric_boilers = c( 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
  ssab_eaf         = c( 0, 0, 0, 0, 0, 0,30,30,30,30,30,30),
  lkab             = c( 0, 0, 0, 0, 0, 0, 0, 5,15,25,28,30),
  uniper           = c( 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 8,12),
  own_production   = c(10,12,15,17,20,28,22,22,22,20,19,18),
  gap_unconfirmed  = c( 0, 0, 0, 3,10,22,38,33,23,12, 5, 0)
)

gap_long <- gap |>
  pivot_longer(-year, names_to = "source", values_to = "pct") |>
  mutate(source = factor(source,
    levels = c("ssab_bf_gas","biofuel_pellets","electric_boilers",
               "ssab_eaf","lkab","uniper",
               "own_production","gap_unconfirmed"),
    labels = c("SSAB BF gas (phasing out)",
               "Biofuel / pellets (~9%)",
               "Electric boilers (~1%)",
               "SSAB EAF waste heat (~30%)",
               "LKAB sulfuric acid (~30%)",
               "Uniper hydrogen heat",
               "Own production (boilers)",
               "Gap — unconfirmed supply")
  ))

p_gap <- ggplot(gap_long,
    aes(x = factor(year), y = pct, fill = source,
        text = paste0(source, " (", year, "): ", pct, "%"))) +
  geom_bar(stat = "identity", width = 0.75) +
  geom_vline(xintercept = 6.5, linetype = "dashed",
             color = "#A32D2D", linewidth = 0.9) +
  annotate("text", x = 6.6, y = 95,
           label = "EAF online\n~2030",
           hjust = 0, size = 3, color = "#A32D2D") +
  scale_fill_manual(values = c(
    "SSAB BF gas (phasing out)"   = "#A32D2D",
    "Biofuel / pellets (~9%)"     = "#D4A520",
    "Electric boilers (~1%)"      = "#90CAF9",
    "SSAB EAF waste heat (~30%)"  = "#BA7517",
    "LKAB sulfuric acid (~30%)"   = "#1D9E75",
    "Uniper hydrogen heat"        = "#185FA5",
    "Own production (boilers)"    = "#888780",
    "Gap — unconfirmed supply"    = "#C8C8C4"
  )) +
  labs(x = "Year", y = "Share of total heat demand (%)", fill = NULL) +
  theme_minimal(base_size = 13) +
  theme(legend.position = "bottom",
        panel.grid.major.x = element_blank(),
        axis.text.x = element_text(angle = 30, hjust = 1))

ggplotly(p_gap, tooltip = "text") |>
  layout(legend = list(orientation = "h", y = -0.3))

All three industrial partnerships confirmed and on schedule.

Show code
sc_a <- data.frame(
  year            = 2024:2035,
  ssab_bf_gas     = c(80,78,75,70,60,40, 0, 0, 0, 0, 0, 0),
  biofuel_pellets = c( 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9),
  electric_boilers= c( 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
  ssab_eaf        = c( 0, 0, 0, 0, 0, 0,30,30,30,30,30,30),
  lkab            = c( 0, 0, 0, 0, 0, 0, 0, 5,15,25,28,30),
  uniper          = c( 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 8,12),
  own_production  = c(10,12,15,17,20,28,22,22,22,20,19,18),
  gap             = c( 0, 0, 0, 3,10,22,38,33,23,12, 5, 0)
)

make_scenario_plot <- function(df, title_text) {
  long <- df |>
    pivot_longer(-year, names_to = "source", values_to = "pct") |>
    mutate(source = factor(source,
      levels = c("ssab_bf_gas","biofuel_pellets","electric_boilers",
                 "ssab_eaf","lkab","uniper","own_production","gap"),
      labels = c("SSAB BF gas","Biofuel/pellets","Electric boilers",
                 "SSAB EAF","LKAB","Uniper","Own production","Gap")
    ))
  p <- ggplot(long, aes(x = factor(year), y = pct, fill = source,
                        text = paste0(source, " (", year, "): ", pct, "%"))) +
    geom_bar(stat = "identity", width = 0.75) +
    geom_vline(xintercept = 6.5, linetype = "dashed",
               color = "#A32D2D", linewidth = 0.8) +
    scale_fill_manual(values = c(
      "SSAB BF gas"     = "#A32D2D",
      "Biofuel/pellets" = "#D4A520",
      "Electric boilers"= "#90CAF9",
      "SSAB EAF"        = "#BA7517",
      "LKAB"            = "#1D9E75",
      "Uniper"          = "#185FA5",
      "Own production"  = "#888780",
      "Gap"             = "#C8C8C4"
    )) +
    labs(x = "Year", y = "Share of heat demand (%)", fill = NULL,
         title = title_text) +
    theme_minimal(base_size = 12) +
    theme(legend.position = "bottom",
          panel.grid.major.x = element_blank(),
          axis.text.x = element_text(angle = 30, hjust = 1))
  ggplotly(p, tooltip = "text") |>
    layout(legend = list(orientation = "h", y = -0.3))
}

make_scenario_plot(sc_a, "Scenario A — Optimal: all three partnerships confirmed")

Uniper delayed or does not materialise. Own production fills the remainder.

Show code
sc_b <- data.frame(
  year            = 2024:2035,
  ssab_bf_gas     = c(80,78,75,70,60,40, 0, 0, 0, 0, 0, 0),
  biofuel_pellets = c( 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9),
  electric_boilers= c( 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
  ssab_eaf        = c( 0, 0, 0, 0, 0, 0,30,30,30,30,30,30),
  lkab            = c( 0, 0, 0, 0, 0, 0, 0, 5,15,25,28,30),
  uniper          = c( 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
  own_production  = c(10,12,15,17,20,28,22,22,22,24,24,25),
  gap             = c( 0, 0, 0, 3,10,22,38,33,23,11, 8, 5)
)
make_scenario_plot(sc_b, "Scenario B — Likely: EAF + LKAB, Uniper absent")

LKAB investment decision fails. Large gap filled by expensive backup boilers.

Show code
sc_c <- data.frame(
  year            = 2024:2035,
  ssab_bf_gas     = c(80,78,75,70,60,40, 0, 0, 0, 0, 0, 0),
  biofuel_pellets = c( 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9),
  electric_boilers= c( 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
  ssab_eaf        = c( 0, 0, 0, 0, 0, 0,30,30,30,30,30,30),
  lkab            = c( 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
  uniper          = c( 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
  own_production  = c(10,12,15,17,20,28,32,35,35,35,35,35),
  gap             = c( 0, 0, 0, 3,10,22,28,25,25,25,25,25)
)
make_scenario_plot(sc_c, "Scenario C — Risk: EAF only, LKAB fails — large expensive gap")

Data center waste heat via heat pumps supplements EAF. LKAB absent.

Show code
sc_d <- data.frame(
  year            = 2024:2035,
  ssab_bf_gas     = c(80,78,75,70,60,40, 0, 0, 0, 0, 0, 0),
  biofuel_pellets = c( 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9),
  electric_boilers= c( 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
  ssab_eaf        = c( 0, 0, 0, 0, 0, 0,30,30,30,30,30,30),
  lkab            = c( 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
  uniper          = c( 0, 0, 0, 0, 0, 0, 0, 5,10,12,12,12),
  own_production  = c(10,12,15,17,20,28,25,25,25,25,25,25),
  gap             = c( 0, 0, 0, 3,10,22,35,30,25,23,23,23)
)
make_scenario_plot(sc_d, "Scenario D — Alternative: EAF + data center heat pumps")

Industrial contracts

Show code
contracts <- data.frame(
  Partner = c("SSAB / LuleKraft (BF gas)",
              "SSAB EAF + biochar (MOU)",
              "Uniper hydrogen heat",
              "LKAB Svartön sulfuric acid",
              "Own reserve boilers",
              "Thermal storage (Aronstorp)"),
  Status         = c("In transition","In transition","Confirmed",
                     "Planned","Confirmed","Confirmed"),
  Heat_source    = c("Blast furnace process gas",
                     "EAF cooling process + biochar gas",
                     "Electrolysis waste heat",
                     "Sulfuric acid process heat",
                     "Pellets / bio-oil / electricity / HVO100",
                     "Buffer / flexibility asset"),
  Contract_start = c(1970, 2030, 2024, 2026, 1980, 2022),
  Contract_end   = c(2030, 2040, 2035, 2036, 2040, 2042),
  Years_left     = c(5, 15, 10, 11, 15, 17),
  Confirmed      = c("Yes","MOU only","Yes","Decision ~2026","Yes","Yes")
)

contracts |>
  kable(col.names = c("Partner","Status","Heat source","Start","End",
                      "Years left","Confirmed"),
        align = c("l","l","l","c","c","c","l")) |>
  kable_styling(bootstrap_options = c("striped","hover","condensed"),
                full_width = TRUE, font_size = 13) |>
  row_spec(which(contracts$Status == "In transition"), background = "#FFF3CD") |>
  row_spec(which(contracts$Status == "Planned"),       background = "#D1ECF1") |>
  row_spec(which(contracts$Status == "Confirmed"),     background = "#D4EDDA")
Partner Status Heat source Start End Years left Confirmed
SSAB / LuleKraft (BF gas) In transition Blast furnace process gas 1970 2030 5 Yes
SSAB EAF + biochar (MOU) In transition EAF cooling process + biochar gas 2030 2040 15 MOU only
Uniper hydrogen heat Confirmed Electrolysis waste heat 2024 2035 10 Yes
LKAB Svartön sulfuric acid Planned Sulfuric acid process heat 2026 2036 11 Decision ~2026
Own reserve boilers Confirmed Pellets / bio-oil / electricity / HVO100 1980 2040 15 Yes
Thermal storage (Aronstorp) Confirmed Buffer / flexibility asset 2022 2042 17 Yes

Contract timeline (Gantt)

Show code
contracts_plot <- contracts |>
  mutate(Partner = factor(Partner, levels = rev(Partner)),
         Status  = factor(Status,
           levels = c("Confirmed","In transition","Planned")))

p_gantt <- ggplot(contracts_plot,
    aes(y = Partner, xmin = Contract_start, xmax = Contract_end,
        color = Status,
        text = paste0(Partner, "\n",
                      Contract_start, "–", Contract_end,
                      "\nStatus: ", Status,
                      "\n", Years_left, " yrs remaining"))) +
  geom_linerange(linewidth = 7, alpha = 0.85) +
  geom_vline(xintercept = 2026, linetype = "dashed",
             color = "#444441", linewidth = 0.8) +
  annotate("text", x = 2026.4, y = 0.4, label = "Now",
           hjust = 0, size = 3.5, color = "#444441") +
  scale_color_manual(values = c(
    "In transition" = "#BA7517",
    "Planned"       = "#185FA5",
    "Confirmed"     = "#1D9E75"
  )) +
  scale_x_continuous(breaks = seq(1970, 2045, 5)) +
  labs(x = "Year", y = NULL, color = "Status") +
  theme_minimal(base_size = 12) +
  theme(legend.position = "bottom",
        panel.grid.major.y = element_blank(),
        axis.text.x = element_text(angle = 30, hjust = 1))

ggplotly(p_gantt, tooltip = "text") |>
  layout(legend = list(orientation = "h", y = -0.2))

Key events timeline

Show code
events <- data.frame(
  Year  = c(1970, 2019, 2022, 2023, 2024, 2025, 2026, 2026, 2030, 2030, 2031),
  Event = c(
    "SSAB / Luleå Energi collaboration begins",
    "LuleKraft contract extended to 2030",
    "Aronstorp thermal storage commissioned (30,000 m³)",
    "SSAB EAF waste heat MOU signed",
    "Uniper hydrogen heat agreement signed",
    "SSAB EAF groundbreaking (Sep 2025)",
    "LKAB investment decision expected",
    "Luleå Energi plant size decision point",
    "SSAB blast furnace shutdown (target)",
    "SSAB EAF operations begin (target)",
    "LKAB Svartön operations begin (estimated)"
  ),
  Type = c("Contract","Contract","Infrastructure","MOU",
           "Contract","Milestone","Critical","Critical",
           "Critical","Transition","Planned"),
  y_pos = c(2,1,2,1,2,1,2,1,2,1,2)
)

color_map <- c(
  "Contract"       = "#185FA5",
  "Infrastructure" = "#1D9E75",
  "MOU"            = "#BA7517",
  "Milestone"      = "#7B1FA2",
  "Planned"        = "#888780",
  "Critical"       = "#A32D2D",
  "Transition"     = "#BA7517"
)

p_tl <- ggplot(events,
    aes(x = Year, y = y_pos, color = Type,
        text = paste0(Year, ": ", Event))) +
  geom_point(size = 5) +
  geom_segment(aes(xend = Year, yend = 0),
               linewidth = 0.6, alpha = 0.45) +
  geom_hline(yintercept = 0, linewidth = 0.5, color = "#B4B2A9") +
  geom_vline(xintercept = 2026, linetype = "dashed",
             color = "#444441", linewidth = 0.7) +
  annotate("text", x = 2026.3, y = 2.5, label = "Now",
           hjust = 0, size = 3.2, color = "#444441") +
  scale_color_manual(values = color_map) +
  scale_x_continuous(breaks = seq(1970, 2035, 5)) +
  scale_y_continuous(limits = c(-0.5, 3)) +
  labs(x = "Year", y = NULL, color = "Event type") +
  theme_minimal(base_size = 12) +
  theme(axis.text.y = element_blank(),
        panel.grid.major.y = element_blank(),
        panel.grid.minor.y = element_blank(),
        legend.position = "bottom",
        axis.text.x = element_text(angle = 30, hjust = 1))

ggplotly(p_tl, tooltip = "text") |>
  layout(legend = list(orientation = "h", y = -0.25))

Sustainability comparison

How each heat source compares across three dimensions. Scores are qualitative assessments based on SME interviews and published lifecycle data. 5 = best, 1 = worst.

Show code
sustain <- data.frame(
  `Heat source`      = c("SSAB BF gas (current)",
                         "SSAB EAF waste heat",
                         "LKAB sulfuric acid heat",
                         "Uniper hydrogen heat",
                         "Biofuel / pellets",
                         "Electric boilers",
                         "Fossil oil backup",
                         "Data center heat pump"),
  `Carbon\n(5=lowest emissions)` = c("4","5","5","4","3","4","1","4"),
  `Cost\n(5=cheapest)`           = c("5","4","4","3","2","2","1","3"),
  `Resilience\n(5=most reliable)`= c("1","3","3","2","4","3","5","3"),
  Category = c("Current",
               "Future — waste heat",
               "Future — waste heat",
               "Future — new source",
               "Backup / supplement",
               "Backup / supplement",
               "Emergency backup",
               "Future — new source"),
  check.names = FALSE
)

sustain |>
  kable(align = c("l","c","c","c","l")) |>
  kable_styling(bootstrap_options = c("striped","hover","condensed"),
                full_width = TRUE, font_size = 13) |>
  row_spec(1, background = "#FFF3E0") |>
  row_spec(c(2,3), background = "#E8F5E9") |>
  row_spec(c(4,8), background = "#E3F2FD") |>
  row_spec(c(5,6), background = "#FFF9E6") |>
  row_spec(7, background = "#F5F5F5") |>
  column_spec(2, bold = TRUE) |>
  column_spec(3, bold = TRUE) |>
  column_spec(4, bold = TRUE)
Heat source Carbon (5=lowest emissions) | Cost (5=cheapest | Resilience (5=most reliabl ) |Category
SSAB BF gas (current) 4 5 1 Current
SSAB EAF waste heat 5 4 3 Future — waste heat
LKAB sulfuric acid heat 5 4 3 Future — waste heat
Uniper hydrogen heat 4 3 2 Future — new source
Biofuel / pellets 3 2 4 Backup / supplement
Electric boilers 4 2 3 Backup / supplement
Fossil oil backup 1 1 5 Emergency backup
Data center heat pump 4 3 3 Future — new source
Note

No single source scores 5 across all three dimensions. The optimal portfolio stacks sources to cover carbon, cost, and resilience together. SSAB blast furnace gas gets the highest cost score but a resilience score of 1 — that single number explains the entire planning problem this research addresses. Fossil oil scores highest on resilience but lowest on carbon and cost — it is the backup of last resort, not a strategy.


Data sources

Dataset Source URL
Production per price area (2019–2024) Energimarknadsinspektionen ei.se technical data
Price per price area (2019–2024) Energimarknadsinspektionen ei.se economic data
2025 energy mix declaration Luleå Energi luleaenergi.se
SSAB/Luleå Energi 2023 MOU SSAB press release ssab.com
Contract extension 2019 ENERGInyheter energinyheter.se
NSD heat gap investigation NSD (Sep 2024) nsd.se

George Washington University / Luleå University of Technology, 2026.