The administrative hierarchy PyPSA-Eur builds its regions from, as a
geoscales::geoscales Geoscale: 1,477 atoms in five nested geoframes.
Format
A geoscales::geoscales Geoscale with 1,477 atoms, five geoframes,
14 data columns and attached geometry.
Source
Built from PyPSA-Eur's own resources/<run>/nuts3_shapes.geojson rather than
from raw Eurostat files, so the regions match the networks this package
converts exactly. That layer combines Eurostat NUTS 2021 (© European
Union, reuse permitted with attribution) with OpenStreetMap adm1
boundaries (ODbL-1.0) for the four countries NUTS does not reach.
The model layer comes from the NUTS3 network base_s_1035_elec.nc of the
same build, whose AC bus names are NUTS3 codes.
See system.file("LICENSE.note", package = "reneuro"). Regenerated by
data-raw/nuts_gs.R.
Details
| geoframe | regions | what it is |
europe | 1 | the whole territory |
nuts0 | 36 | countries |
nuts1 | 109 | NUTS1 |
nuts2 | 296 | NUTS2 |
nuts3 | 1,477 | NUTS3 – the atom layer |
The single-member europe root exists so a continental commodity balance
has a region to live at — one Europe-wide CO2 allowance or gas market rather
than 36 national ones. Without it the coarsest balance available is per
country.
Nesting is exact and is asserted at build time: every finer code maps to
exactly one coarser code. That is what lets a commodity declare
@geoframe = "nuts1" and have its balance roll up correctly from the atoms.
Data carried per region
The leaftable holds two families of per-region data, and the distinction between them decides what each column can be used for.
Geographic, from the region shapes, defined for all 1,477 atoms:
| column | unit | kind |
km2 | km2, EPSG:3035 | extensive |
pop | thousands | extensive |
gdp | EUR per capita | intensive |
gdp_total | thousand EUR | extensive |
Model, from the NUTS3 network, located at substation buses:
| column | unit | kind |
load_twh | TWh per year | extensive |
peak_mw | MW, coincident | not additive |
n_buses | count of substations | extensive |
cap_mw | MW of generation built today | extensive |
hydro_mw | MW of that which is hydro | extensive |
pot_onwind, pot_solar, pot_solar_hsat | MW | extensive |
pot_offwind_ac, pot_offwind_dc, pot_offwind_float | MW | extensive |
gdp looks like a total and is not: summing it gives Germany "15.6
million", while a population-weighted mean recovers its real ~41,800 EUR per
capita. gdp_total = gdp * pop is carried so an extensive money weight
exists. peak_mw is the maximum of the summed hourly series and falls
below the sum of its parts wherever demand is diverse; see nuts_load.
Seven columns are declared weights – km2, pop, gdp_total,
load_twh, cap_mw, pot_onwind, pot_solar – and km2 is the default.
A weight splits a coarse quantity across atoms and averages an intensive one
back up, so the choice is a modelling decision: national demand should be
split by load_twh or pop, not by area, and a wind target by pot_onwind.
See geoscales::recast_geoscale().
The potentials overlap and must not be summed
pot_solar and pot_solar_hsat are fixed-tilt and single-axis-tracking
photovoltaics on the same land, and 313 of the 314 offshore regions carry
more than one pot_offwind_* type on the same sea area. Adding them counts
the same hectare twice. They are kept as separate columns so that no summing
rule is imposed here; the three offshore columns are excluded from the weight
set for a second reason – they are zero for every landlocked country.
442 regions carry no model data
PyPSA-Eur places demand, plant and potential at substation buses, and 442 of the 1,477 NUTS3 regions contain none. Every model column is zero there. That is the model's own allocation – a busless region's land and load were folded into the neighbour it clusters with – and not a statement about the region. The geographic columns are unaffected.
The effect propagates upward exactly as far as the node counts do: 7 NUTS2 groups and 3 NUTS1 groups contain no substation at all, and these are precisely the regions lost when 296 NUTS2 become 289 nodes and 109 NUTS1 become 106.
A weight summing to zero over a group divides by zero when averaging an intensive quantity into it. Those groups are recorded rather than patched:
attr(nuts_gs, "weight_degeneracy")Two entries are degenerate for a reason other than a missing substation, and
both are real: GBI3 (Inner London) has no generation and no land the
availability rules admit for wind, and NO07 (Nord-Norge) has no solar
potential, lying above the Arctic Circle.
Four countries have no NUTS
BA, MD, UA and XK are outside the NUTS system entirely – the same four
PyPSA-Eur caps at administrative level 1. They are filled from OpenStreetMap
adm1 boundaries (3, 37, 27 and 7 regions respectively), which become
nuts3 atoms, and the levels above them are padded: MD0 at nuts1,
MD00 at nuts2.
Padding rather than repeating matters. PyPSA-Eur repeats the adm1 code at
every level, which makes each of those regions its own parent: the
derived region hierarchy then carries identity pairs like (MD-BA, MD-BA),
and energyRt's spatial roll-up degenerates to
vOutTot[MD-BA] = ... + vOutTot[MD-BA], silently forcing everything else to
zero.
The padding follows Eurostat's own convention for identical geography –
Luxembourg is LU -> LU0 -> LU00 -> LU000 – and no NUTS country produces a
single identity pair. The honest caveat remains that "NUTS1"/"NUTS2" for
those four is a stand-in, not a real administrative level.
A known gap in the gdp weight
One region, MD-BD (Bender), carries gdp = 0 in the source, and it is left
uncorrected – substituting a plausible number would hide a real gap.
It no longer breaks anything: with the levels padded, MD-BD sits inside
MD0/MD00 rather than being its own parent, so no group sums to zero and
every weight now passes the build script's checks cleanly.
The geometry is simplified
Reduced from 37 MB of source GeoJSON with
rmapshaper::ms_simplify(keep = 0.02, keep_shapes = TRUE); the build refuses
to save if a region is lost. It is a display geometry – the km2 weight
was computed from the unsimplified polygons in EPSG:3035, so use that
rather than measuring the shipped shapes.
Examples
# The hierarchy, coarsest first
geoscales::geoscale_geoframes(nuts_gs)
#> [1] "europe" "nuts0" "nuts1" "nuts2" "nuts3"
# How many regions at each level
vapply(geoscales::geoscale_geoframes(nuts_gs),
function(f) length(geoscales::geoscale_regions(nuts_gs, f)), 0L)
#> europe nuts0 nuts1 nuts2 nuts3
#> 1 36 109 296 1477
# Demand and wind potential by country, from the atom layer
geoscales::geoscale_leaftable(nuts_gs) |>
as.data.frame() |>
dplyr::group_by(nuts0) |>
dplyr::summarise(twh = sum(load_twh), wind_gw = sum(pot_onwind) / 1e3) |>
dplyr::arrange(dplyr::desc(twh)) |>
head()
#> # A tibble: 6 × 3
#> nuts0 twh wind_gw
#> <chr> <dbl> <dbl>
#> 1 DE 509. 490.
#> 2 FR 492. 969.
#> 3 IT 316. 504.
#> 4 GB 316. 439.
#> 5 ES 246. 938.
#> 6 UA 149. 851.
