Skip to content

Tutorial: congestion-management cost decomposition ​

When the day-ahead schedule produces flows the grid can't physically deliver, the TSO has three tools — they all show up under Transmission 13.1.*:

WrapperWhat it represents
redispatching_internalWithin-zone unit re-commitment
redispatching_cross_borderBetween-zone re-commitment
countertradingEnergy trades that relieve a constraint
costs_of_congestion_managementTotal financial cost (EUR or local CCY)

This page combines volumes from the first three with costs from the fourth to show what the TSO spent on congestion relief.

Setup ​

julia
using ENTSOE
using CairoMakie
using Dates
using Statistics: mean

CairoMakie.activate!(type = "png")

include(joinpath(pkgdir(ENTSOE), "test", "_brokenrecord_helpers.jl"))
const BR = _load_brokenrecord()
client = ENTSOEClient("PLAYBACK")

Fetch the four series ​

Each uses a different cassette — recorded against the canonical Postman parameters that exercise each endpoint.

julia
redispatch_internal = BR.playback("tut_redispatch_NL_2023.yml") do
    redispatching_internal(
        client, EIC.NL,
        DateTime("2023-10-31T23:00"), DateTime("2023-11-30T23:00"),
    )
end

redispatch_cross = BR.playback("transmission131_a_redispatching_cross_border.yml") do
    redispatching_cross_border(
        client, EIC.FR, EIC.AT,
        DateTime("2023-11-01T00:00"), DateTime("2023-12-01T00:00"),
    )
end

counter = BR.playback("transmission131_b_countertrading.yml") do
    countertrading(
        client, EIC.FR, EIC.ES,
        DateTime("2023-09-12T22:00"), DateTime("2023-09-13T22:00"),
    )
end

costs = BR.playback("tut_costs_BE_2022.yml") do
    costs_of_congestion_management(
        client, EIC.BE,
        DateTime("2021-12-31T23:00"), DateTime("2022-12-31T23:00"),
    )
end

(
    redispatch_internal_n = length(redispatch_internal),
    redispatch_cross_n    = length(redispatch_cross),
    counter_n             = length(counter),
    costs_n               = length(costs),
)
(redispatch_internal_n = 384, redispatch_cross_n = 44, counter_n = 53, costs_n = 36)

Costs dashboard — BE 2022 ​

The costs cassette captures a full year of monthly cost samples for Belgium. The shape is striking — congestion costs are highly seasonal and event-driven.

julia
fig = Figure(size = (960, 410))
ax = Axis(fig[1, 1];
    title  = "BE congestion-management cost — 2022",
    xlabel = "Month",
    ylabel = "Cost (currency varies by TSO)",
)
barplot!(ax, 1:length(costs), costs.value;
    color = costs.value,
    colormap = :acton,
    strokewidth = 0.5, strokecolor = :white)
# Y label-as-millions for readability.
ax.yticks = (
    [0, 5e5, 1e6, 1.5e6, 2e6],
    ["0", "0.5M", "1M", "1.5M", "2M"],
)
fig

Redispatch volumes — three lenses ​

The three redispatch/countertrade endpoints have different time bases and zones; we stack them in a small-multiple to highlight the shape of each.

julia
fig2 = Figure(size = (960, 540))

ax1 = Axis(fig2[1, 1];
    title = "Redispatching internal — NL, Nov 2023",
    ylabel = "MW")
isempty(redispatch_internal.value) ||
    barplot!(ax1, 1:length(redispatch_internal), redispatch_internal.value;
        color = :steelblue, strokewidth = 0.4, strokecolor = :white)

ax2 = Axis(fig2[2, 1];
    title = "Redispatching cross-border — FR ← AT, Nov 2023",
    ylabel = "MW")
isempty(redispatch_cross.value) ||
    barplot!(ax2, 1:length(redispatch_cross), redispatch_cross.value;
        color = :firebrick, strokewidth = 0.4, strokecolor = :white)

ax3 = Axis(fig2[3, 1];
    title = "Countertrading — FR ↔ ES, 13 Sep 2023",
    ylabel = "MW", xlabel = "Interval index")
isempty(counter.value) ||
    barplot!(ax3, 1:length(counter), counter.value;
        color = :seagreen, strokewidth = 0.4, strokecolor = :white)

linkxaxes!(ax1, ax2, ax3)
fig2

Sanity numbers ​

julia
(
    annual_cost     = round(Int, sum(costs.value)),
    mean_monthly    = round(Int, mean(costs.value)),
    peak_month_cost = round(Int, maximum(costs.value)),
    nl_redispatch_total_mwh   = round(Int, sum(redispatch_internal.value)),
    fr_es_counter_total_mwh   = round(Int, sum(counter.value)),
)
(annual_cost = 11096637, mean_monthly = 308240, peak_month_cost = 1508142, nl_redispatch_total_mwh = 1454, fr_es_counter_total_mwh = 4034)

Where to next ​