Skip to contents

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.

NUTSNomenclature 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:

  1. Revision and extension of the input data, including alternative sources.
  2. Review of the processing algorithms that derive model parameters from raw data: aggregation, clustering, capacity factors and loss representation.
  3. Extension to sectors beyond electricity.
  4. 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 reneuro is 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.