Skip to content

Tutorial: cross-border physical flows on the NL ↔ DE link ​

Realised flows on a single interconnector come from Transmission 12.1.G — wrapped here as cross_border_physical_flows. This tutorial pulls a full week (2024-09-02 → 2024-09-09) for the Netherlands ↔ Germany/Lux border and plots a signed time-series: positive when NL imports, negative when NL exports.

A small but important quirk: ENTSO-E reports each direction in its own document. To get the net flow you query both directions and subtract. Cassettes for both directions live alongside the tutorial.

Setup ​

julia
using ENTSOE
using CairoMakie
using Dates

CairoMakie.activate!(type = "png")

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

Argument ordering ​

The wrapper takes (client, in_area, out_area, ...):

  • in_area — receiving zone

  • out_area — sending zone

Flows are reported positive when energy actually crosses into in_area. So the first call returns DE→NL flow (positive numbers when DE exports into NL):

julia
de_to_nl = BR.playback("tut_flows_DE_to_NL_week.yml") do
    cross_border_physical_flows(
        client, EIC.NL, EIC.DE_LU,
        DateTime("2024-09-01T22:00"),
        DateTime("2024-09-08T22:00"))
end
length(de_to_nl), de_to_nl[1]
(672, (time = Dates.DateTime("2024-09-01T22:00:00"), value = 1269.56))

…and the second the opposite direction (positive when NL exports into DE):

julia
nl_to_de = BR.playback("tut_flows_NL_to_DE_week.yml") do
    cross_border_physical_flows(
        client, EIC.DE_LU, EIC.NL,
        DateTime("2024-09-01T22:00"),
        DateTime("2024-09-08T22:00"))
end
nl_to_de[1]
(time = Dates.DateTime("2024-09-01T22:00:00"), value = 0.0)

Net flow ​

ENTSO-E publishes the two directions as separate (always-positive) series, hourly. Subtract them to get a signed net flow. The two direction-series sometimes carry slightly different first/last timestamps (DST shifts, missing hours), so we trim to the shared prefix length before subtracting. Column access on the StructVector makes this a one-liner:

julia
n = min(length(de_to_nl), length(nl_to_de))
net = de_to_nl.value[1:n] .- nl_to_de.value[1:n]   # positive = NL importing
extrema(net)
(-3699.48, 2422.16)

Plotting ​

julia
fig = Figure(size = (900, 380))
ax = Axis(fig[1, 1];
    xlabel = "UTC time",
    ylabel = "MW (positive = NL importing)",
    title  = "NL ↔ DE physical flow — week of 2024-09-02",
)
band!(ax, 1:length(net), zero(net), net;
    color = (ifelse.(net .>= 0, :seagreen, :firebrick)))
lines!(ax, 1:length(net), net; color = :black, linewidth = 0.6)
hlines!(ax, [0]; color = :gray70, linewidth = 0.6)
ax.xticks = (
    1:24:length(net),
    [Dates.format(de_to_nl.time[i], "E dd") for i in 1:24:length(net)],
)
fig

The visible pattern:

  • NL imports overnight (green), when its dispatchable fleet is scaled back and DE wind is plentiful.

  • NL exports during midday peaks (red), driven by Dutch solar generation when DE prices are high enough to pull power across the interconnector.

  • The trace mostly stays well inside the published NTC limits for the NL ↔ DE link (≈ 5 GW typical), which means nothing in our window was congested.

Aggregations ​

Because de_to_nl.value and nl_to_de.value are plain Vector{Float64}s, totalising the week's gross flow is one line:

julia
hours_per_pt = 1.0  # 12.1.G is hourly resolution
import_gwh = sum(max.(net, 0)) * hours_per_pt / 1_000
export_gwh = sum(min.(net, 0)) * hours_per_pt / 1_000
(net_flow_gwh = round(import_gwh + export_gwh; digits = 2),
 imports_gwh  = round(import_gwh;  digits = 2),
 exports_gwh  = round(export_gwh;  digits = 2))
(net_flow_gwh = 10.64, imports_gwh = 318.45, exports_gwh = -307.81)

Daily roll-up ​

Sum each day's signed flow into a single bar — easier to compare days at a glance than the hourly trace:

julia
hours_per_day = 24
daily_gwh = [sum(@view net[(d - 1) * hours_per_day + 1:d * hours_per_day]) / 1_000
             for d in 1:(length(net) ÷ hours_per_day)]
day_labels = [Dates.format(de_to_nl.time[(d - 1) * hours_per_day + 1], "E dd")
              for d in eachindex(daily_gwh)]

fig2 = Figure(size = (760, 320))
ax = Axis(fig2[1, 1];
    xlabel = "Day",
    ylabel = "Net GWh (positive = NL imports)",
    title  = "NL ↔ DE-LU — daily net flow, week of 2024-09-02",
    xticks = (1:length(daily_gwh), day_labels),
)
hlines!(ax, [0]; color = :gray70, linewidth = 0.5)
barplot!(ax, 1:length(daily_gwh), daily_gwh;
    color = [v >= 0 ? :seagreen : :firebrick for v in daily_gwh],
    strokewidth = 0.5, strokecolor = :white)
fig2

Where to next ​

  • For commercial (scheduled) cross-border allocations rather than realised physical flows, the relevant generated function is transmission111_a_explicit_allocations (no convenience wrapper yet — call via entsoe_apis(client).transmission).

  • For multi-zone scans, write a loop over EIC pairs and aggregate the results. Ranges longer than ENTSO-E's per-call period limit are split into windows automatically — see the multi-year tutorial.