r·en·euro provides a European energy system model built
on energyRt. It was developed as
teaching material for a modelling course, and as a standalone working
model.
It currently reproduces PyPSA-Eur, an established open model of the European power system, at several spatial resolutions. A published model provides a reference solution for verifying the R implementation.
Current functionality includes:
- Import of pre-built PyPSA-Eur models from netCDF, including component
attributes that PyPSA omits when they hold default values. - Translation
into energyRt model objects: regions, commodities, demand,
generation technologies, storage, hydro, weather and transmission. -
Sampling in time (subsets of snapshots) and in space (bus aggregation or
sub-territory selection). - Solution through any energyRt
back end — GLPK, Julia/JuMP, Python/Pyomo, GAMS — and the solvers each
supports, locally or remotely via NEOS. - Comparison of converted
results against the source model, by quantity.
Transmission is represented along a configurable ladder, from an
unconstrained copperplate through capacity-limited transport with
piecewise-linear losses to Kirchhoff’s voltage law, since
energyRt uses a transport formulation while PyPSA combines
both.
Models
The release ships a range of spatial resolutions over the same European system, all on the full year (8,760 hourly snapshots) unless stated, produced by two routes.
Built by PyPSA-Eur. Clustered by PyPSA-Eur’s
cluster_network rule and read into energyRt without further
reduction, so each is comparable to a PyPSA-Eur run at the same
size.
| model | nodes | how it was clustered | period |
|---|---|---|---|
pypsa_eur_5 |
5 | k-means, Belgium only | one week, 168 h |
pypsa_eur_5cp |
5 | as above, copperplate | one week, 168 h |
pypsa_eur_41 |
41 | k-means | full year |
pypsa_eur_250 |
250 | k-means | full year |
pypsa_eur_nuts3 |
1,035 | NUTS3 busmap, not k-means † | full year |
† pypsa_eur_nuts3 uses the same PyPSA-Eur workflow on a
busmap generated by this package (data-raw/make_busmap.R)
rather than by k-means, so its regions are administrative. Everything
downstream — renewable profiles, line aggregation, costs — is
PyPSA-Eur’s.
Aggregated in R. Derived from
pypsa_eur_nuts3 with aggregate_pypsa(), which
applies PyPSA’s reduction strategies without a second PyPSA-Eur run.
Each takes seconds to minutes rather than a rebuild from raw data.
| model | nodes | aggregated to | build | period |
|---|---|---|---|---|
pypsa_eur_nuts0 |
36 | NUTS0, countries | 16 s | full year |
pypsa_eur_nuts1 |
106 | NUTS1 | 34 s | full year |
pypsa_eur_nuts2 |
289 | NUTS2 | 75 s | full year |
pypsa_eur_nuts2 (289 nodes, aggregated in R) and
pypsa_eur_250 (PyPSA’s k-means) are the same system at
nearly the same size by two independent methods, so either can be used
to check the other.
NUTS — Nomenclature of Territorial Units for Statistics — is Eurostat’s hierarchy of European regions, against which national statistics are published. It has four nested levels, each a complete partition of the territory:
| level | Eurostat’s definition | count | one nesting chain |
|---|---|---|---|
| NUTS0 | country | 36 | DE |
| NUTS1 | major socio-economic regions | 109 | DE9 |
| NUTS2 | basic regions for regional policy | 296 | DE93 |
| NUTS3 | small regions for specific diagnoses | 1,477 | DE933 |
Each code contains its ancestry: DE933 is inside
DE93, inside DE9, inside DE, so
the hierarchy is read rather than inferred.
The 2021 edition is used, as in PyPSA-Eur. An official classification lets a model be joined to published statistics — population, GDP, industrial output — without a separate crosswalk.
Four countries in the model territory are outside NUTS: Bosnia and Herzegovina, Moldova, Ukraine and Kosovo. They are filled from OpenStreetMap first-level administrative boundaries, so “NUTS1” and “NUTS2” for those four are stand-ins rather than real levels.
The four NUTS models nest over the same system, so a result can be tested for sensitivity to spatial resolution with nothing else changed. Node counts fall short of region counts because a region without a substation merges into a neighbour, keeping its demand and generation: 109 NUTS1 regions give 106 nodes, 296 NUTS2 give 289, and 1,477 NUTS3 give 1,035. Every country has at least one substation, so NUTS0 is 36 for 36.
pypsa_eur_250 is the recommended
working model. PyPSA-Eur is normally clustered to 50–250 nodes for
computational reasons; its k-means is load-weighted and respects country
boundaries.
aggregate_pypsa() applies PyPSA’s
strategies: extensive quantities sum, intensive ones take a
capacity-weighted mean and so stay within their members’ range,
intra-cluster branches are dropped, and corridor lengths are recomputed
from the new region centroids with impedance rescaled to follow. It does
not re-run the clustering from raw data, which is why a NUTS2 model
takes 75 seconds rather than a Snakemake run.
Regions
nuts_gs is the region hierarchy the four NUTS models are
built from: a geoscales object
with 1,477 NUTS3 atoms nesting through NUTS2, NUTS1 and country up to a
single European root. Aggregating a model to any level, or splitting a
national figure down to regions, is a call against this object.
It carries per-region data as well as geometry, in two families. Geographic — area, population, GDP per capita and GDP total — is defined for every region. Model — annual demand, coincident peak, substation count, existing generation and hydro capacity, and the wind and solar potentials by carrier — comes from the NUTS3 network and is located at substations, so it is zero in the 442 regions that contain none.
Seven columns are declared weights. The choice among them is a
modelling decision: national demand divides by load_twh or
pop, a wind target by pot_onwind; area is
rarely the right weight for either.
library(reneuro)
library(dplyr)
geoscales::geoscale_leaftable(nuts_gs) |>
as.data.frame() |>
group_by(nuts0) |>
summarise(twh = sum(load_twh), wind_gw = sum(pot_onwind) / 1e3) |>
arrange(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.The solar and offshore potentials overlap. Fixed-tilt and tracking photovoltaics occupy the same land, and 313 of the 314 offshore regions carry more than one offshore type on the same sea area, so these columns are kept separate and never summed.
A source model
pypsa_eur_nuts3 is not intended to be solved at full
resolution. A full-year continental model at 1,035 nodes is beyond the
open solvers this package targets: on a 96-hour sample it is 1.3 million
LP rows and about 18 minutes, and at four weeks it is 9.4 million rows,
where the interior-point method returns no solution. Use
pypsa_eur_250 or pypsa_eur_nuts2 at
continental scale.
Its use is subsetting. It holds the finest granularity the underlying data supports, so a country or study area can be taken from it at full detail and solved on its own. The coarser models cannot supply that detail, having already averaged it away.
Show the code
library(dplyr)
pt <- geoscales::geoscale_leaftable(nuts_gs) |>
as.data.frame() |>
filter(nuts0 == "PT") |>
pull(region) |>
intersect(get_region(pypsa_eur_nuts3))
m <- attach_weather(pypsa_eur_nuts3, c("onwind", "solar", "ror")) |>
energyRt::subset_model_regions(region = pt)The result is Portugal at 22 nodes: every NUTS3 region containing a
substation, with the network between them intact. region
accepts any subset of the model’s regions, so a study area need not
follow a national border.
subset_model_regions()
drops the corridors crossing the boundary and reports each. Without
replacement the model is islanded and must meet demand from its own
resources, so its cost is an upper bound; boundary_prices
replaces the dropped routes with priced import/export stubs.
The weather series are the bulk of the model. They ship as separate
objects by resource — wx_nuts3_onwind,
wx_nuts3_solar and so on — and are attached on demand:
m <- attach_weather(pypsa_eur_nuts3) # all resources
m <- attach_weather(pypsa_eur_nuts3, c("onwind", "solar"))Models are lazy-loaded and require neither Python nor a PyPSA-Eur
clone, and each carries its provenance as an attribute.
data-raw/ holds the scripts that rebuild every object from
a clone, recording the upstream commit and configuration used.
Solver benchmarks
Measured on one workstation. The same interpolated scenario was given to each back end, so the linear programme is identical and the objectives agree to solver tolerance in every case. Times are end to end, including model generation and reading the solution back.
| model | rows (LP) | GLPK | CBC | HiGHS |
|---|---|---|---|---|
| 5 nodes, 168 h | ~24 k | 10 s | 19 s | 24-47 s |
| 69 nodes, 96 h | ~300 k | 2,158 s | 809 s | 79 s |
| 1,035 nodes, 96 h | 1.3 M | – | – | 18.5 min |
The ordering reverses with size. GLPK is fastest on the smallest model, where start-up cost dominates; at 69 regions HiGHS is 27 times faster, and the margin grows from there. Julia adds a fixed 20-30 s per invocation for just-in-time compilation, which is amortised across a session but not within a single call, so Pyomo is faster end to end for one small solve even where Julia’s solver call is faster.
The NUTS3 model on a four-week sample (672 h) produces 9.4 million
rows after presolve, where the interior-point method returns no
solution. First-order (pdlp) and dual simplex methods have
not been tested at that size.
Reproduce with benchmark_solvers():
scen <- energyRt::interpolate_model(pypsa_eur_5, name = "be")
benchmark_solvers(scen)Development
Planned work, in order of priority:
- Revision and extension of the input data, including alternative sources.
- Review of the processing algorithms that derive model parameters from raw data: aggregation, clustering, capacity factors and loss representation.
- Extension to sectors beyond electricity.
- Interfaces to other modelling frameworks for cross-model comparison.
Contributing
Contributions are welcome. Issues and pull requests may be opened at github.com/optimal2050/reneuro.
The package follows the optimal2050 conventions: tidyverse style with the native pipe, roxygen2 with Markdown, and the shared pkgdown theme.
References
This package reproduces PyPSA-Eur. If you use reneuro in
research, please cite the upstream work as well as this package.
The software
- PyPSA — the power system analysis framework PyPSA-Eur is built on.
-
PyPSA-Eur — the
European model reproduced here, version
v2026.02.0. -
energyRt — the
modelling framework
reneurois built on.
The papers
For the electricity-only model, which is what this package currently reproduces:
Jonas Hörsch, Fabian Hofmann, David Schlachtberger and Tom Brown (2018). PyPSA-Eur: An open optimisation model of the European transmission system. Energy Strategy Reviews 22, 207–215. doi:10.1016/j.esr.2018.08.012, arXiv:1806.01613
For the underlying framework:
Tom Brown, Jonas Hörsch and David Schlachtberger (2018). PyPSA: Python for Power System Analysis. Journal of Open Research Software 6(1), 4. doi:10.5334/jors.188, arXiv:1707.09913
On spatial resolution and what coarsening removes:
Jonas Hörsch and Tom Brown (2017). The role of spatial scale in joint optimisations of generation and transmission for European highly renewable scenarios. 14th International Conference on the European Energy Market (EEM). doi:10.1109/EEM.2017.7982024, arXiv:1705.07617
PyPSA-Eur is now a sector-coupled model, and its authors ask that sector studies cite instead:
Fabian Neumann, Elisabeth Zeyen, Marta Victoria and Tom Brown (2023). The potential role of a hydrogen network in Europe. Joule 7, 1–25. doi:10.1016/j.joule.2023.06.016
Marta Victoria, Elisabeth Zeyen and Tom Brown (2022). Speed of technological transformations required in Europe to achieve different climate goals. Joule 6(5), 1066–1086. doi:10.1016/j.joule.2022.04.016
Each release of PyPSA and PyPSA-Eur carries a version-specific DOI on Zenodo, 10.5281/zenodo.3946412 and 10.5281/zenodo.3520874 respectively. Cite the version used.
Citing reneuro
citation("reneuro")Machine-readable metadata is in CITATION.cff.
Licence
reneuro sources |
Apache-2.0 |
reneuro with energyRt
|
AGPL-3 |
energyRt is licensed under AGPL-3 and is imported by
reneuro. R links imported namespaces into a single process,
so distributing the two together constitutes a combined work conveyed
under AGPL-3. Apache-2.0 is one-way compatible with AGPL-3. The
conversion code alone, independent of energyRt, remains
available under Apache-2.0.
Converted models inherit the licences of their inputs. OpenStreetMap supplies the transmission topology under ODbL-1.0, whose share-alike terms extend to derived databases; Copernicus land cover and EEA Natura 2000 data are CC-BY-4.0; Eurostat NUTS geometry is reusable under Commission Decision 2011/833/EU, with acknowledgement of the European Union and EuroGeographics. Two electricity demand series (OPSD and ENTSO-E) are recorded as licence unknown in PyPSA-Eur’s data inventory; this is unresolved upstream.
Shipped data carries a LICENSE.note recording provenance
for each object, including inputs excluded as non-redistributable.
