Skip to contents

The administrative hierarchy PyPSA-Eur builds its regions from, as a geoscales::geoscales Geoscale: 1,477 atoms in five nested geoframes.

Usage

nuts_gs

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

geoframeregionswhat it is
europe1the whole territory
nuts036countries
nuts1109NUTS1
nuts2296NUTS2
nuts31,477NUTS3 – 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:

columnunitkind
km2km2, EPSG:3035extensive
popthousandsextensive
gdpEUR per capitaintensive
gdp_totalthousand EURextensive

Model, from the NUTS3 network, located at substation buses:

columnunitkind
load_twhTWh per yearextensive
peak_mwMW, coincidentnot additive
n_busescount of substationsextensive
cap_mwMW of generation built todayextensive
hydro_mwMW of that which is hydroextensive
pot_onwind, pot_solar, pot_solar_hsatMWextensive
pot_offwind_ac, pot_offwind_dc, pot_offwind_floatMWextensive

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 weightskm2, 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.