Skip to content

Commit f621a1f

Browse files
Merge pull request #54 from open-energy-transition/feat/fes-workbook-download-and-extract
Add FES workbook extractor
2 parents f158072 + 0e80cfe commit f621a1f

7 files changed

Lines changed: 129 additions & 25 deletions

File tree

.github/workflows/update-gb-model-pinned-env.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ jobs:
4444
uses: actions/upload-artifact@v4
4545
with:
4646
# all are uploaded to the same artifact as they will have different names for each platform so won't clash.
47-
name: lockfiles
47+
name: lockfiles-${{ matrix.os }}
4848
path: envs/**/*.pin.txt
4949

5050
create-pull-request:
@@ -55,8 +55,9 @@ jobs:
5555
- name: Download all artifacts
5656
uses: actions/download-artifact@v5
5757
with:
58-
name: lockfiles
58+
name: lockfiles-*
5959
path: envs/
60+
merge-multiple: true
6061

6162
- name: Create Pull Request
6263
uses: peter-evans/create-pull-request@v7

config/gb-model/config.common.yaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22
#
33
# SPDX-License-Identifier: MIT
44

5+
# docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#run
6+
run:
7+
name: "GB"
58

69
urls:
710
gb-etys-boundaries: https://api.neso.energy/dataset/997f4820-1ad4-499b-b1fe-4b8d3d7fbc72/resource/e914fcec-1dc9-4f1f-97e7-59c0d9521bea/download/etys-boundary-gis-data-mar25.zip
811
transmission-availability: https://www.neso.energy/document/211021/download
12+
fes-workbook: https://www.neso.energy/document/199971/download # FES 2021 workbook
913
target_crs: "EPSG:27700"
1014
area_loss_tolerance_percent: 0.01 # percentage of area loss tolerated when splitting regions
1115
min_region_area: 1000000 # minimum area of a region in square meters (1 km²) for keeping it after splitting
@@ -48,3 +52,21 @@ region_operations:
4852
- [39, 99]
4953
- [74, 75, 76]
5054
- [4, "5w"]
55+
56+
fes-sheet-config:
57+
SV.34:
58+
usecols: "I:AN"
59+
skiprows: 5
60+
index_col: 0
61+
header: 0
62+
rename:
63+
index: scenario
64+
columns: year
65+
66+
BB1:
67+
usecols: "A:C,E,H:AK" # Ignore columns: "Baseline (2020)", "Share of GSP" (empty), "DNO License Area"
68+
skiprows: 0
69+
index_col: [0, 1, 2, 3]
70+
header: 0
71+
rename:
72+
columns: year

doc/gb-model/data_sources.rst

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ The following table provides an overview of the data sources used exclusively in
1313
For data sources used in PyPSA-Eur, see `this page <../data_sources.html>`_.
1414
Different licenses apply to the data sources.
1515

16-
For data source
17-
.. toctree::
18-
:maxdepth: 1
16+
---------------------------------
17+
The Future Energy Scenarios (FES)
18+
---------------------------------
19+
20+
`The FES <https://www.neso.energy/publications/future-energy-scenarios-fes>`_ is the primary data source for defining the model, both for GB and other European countries.
21+
Here, we use the 2021 FES data workbook.
22+
Tables from the workbook we use are:
23+
24+
- BB1: Building Block Data
25+
- SV.34: Installed BECCS generation capacity (GW)
26+

doc/gb-model/release_notes.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Release Notes
1212
Upcoming Release
1313
================
1414

15+
- Add FES workbook data download and sheet extraction rule (#50).
1516
- Restructured documentation.
1617
- Added modelling methodology documentation (#20).
1718
- Added GB custom geographic boundary rule and script (#13).

envs/gb-model/workflow.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,7 @@ dependencies:
2222
- pandas>=2.1
2323
- geopandas>=1
2424
- xarray>=2024.03.0,<2025.07.0
25+
- openpyxl
26+
2527
- pip:
2628
- pdfplumber==0.9.0 # technically noarch but imagemagick dependency not available on Windows

rules/gb-model.smk

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@ configfile: "config/gb-model/config.common.yaml"
1313

1414

1515
# Rule to download and extract ETYS boundary data
16-
rule retrieve_etys_boundary_data:
16+
rule download_data:
17+
message:
18+
"Download {wildcards.gb_data} GB model data."
1719
output:
18-
boundary_shp="data/gb-model/etys-boundary-gis-data.zip",
20+
downloaded="data/gb-model/downloaded/{gb_data}",
1921
params:
20-
url=config["urls"]["gb-etys-boundaries"],
22+
url=lambda wildcards: config["urls"][Path(wildcards.gb_data).stem],
2123
log:
22-
logs("retrieve_etys_boundary_data.log"),
24+
logs("download_{gb_data}.log"),
2325
localrule: True
2426
conda:
2527
"../envs/gb-model/workflow.yaml"
@@ -31,7 +33,7 @@ rule retrieve_etys_boundary_data:
3133
rule create_region_shapes:
3234
input:
3335
country_shapes=resources("country_shapes.geojson"),
34-
etys_boundary_lines=rules.retrieve_etys_boundary_data.output.boundary_shp,
36+
etys_boundary_lines="data/gb-model/downloaded/gb-etys-boundaries.zip",
3537
output:
3638
raw_region_shapes=resources("raw_region_shapes.geojson"),
3739
log:
@@ -60,23 +62,9 @@ rule manual_region_merger:
6062
"../scripts/gb-model/manual_region_merger.py"
6163

6264

63-
rule download_transmission_availability_pdf:
64-
output:
65-
pdf_report="data/gb-model/transmission-availability.pdf",
66-
params:
67-
url=config["urls"]["transmission-availability"],
68-
log:
69-
logs("transmission_availability.log"),
70-
localrule: True
71-
conda:
72-
"../envs/gb-model/workflow.yaml"
73-
shell:
74-
"curl -sSLvo {output} {params.url}"
75-
76-
7765
rule extract_transmission_availability:
7866
input:
79-
pdf_report=rules.download_transmission_availability_pdf.output.pdf_report,
67+
pdf_report="data/gb-model/downloaded/transmission-availability.pdf",
8068
output:
8169
csv=resources("transmission_availability.csv"),
8270
log:
@@ -85,3 +73,20 @@ rule extract_transmission_availability:
8573
"../envs/gb-model/workflow.yaml"
8674
script:
8775
"../scripts/gb-model/extract_transmission_availability.py"
76+
77+
78+
rule extract_fes_workbook_sheet:
79+
message:
80+
"Extract FES workbook sheet {wildcards.fes_sheet} and process into machine-readable, 'tidy' dataframe format according to defined configuration."
81+
input:
82+
workbook="data/gb-model/downloaded/fes-workbook.xlsx",
83+
output:
84+
csv=resources("fes/{fes_sheet}.csv"),
85+
params:
86+
sheet_extract_config=lambda wildcards: config["fes-sheet-config"][
87+
wildcards.fes_sheet
88+
],
89+
log:
90+
logs("extract_fes_{fes_sheet}.log"),
91+
script:
92+
"../scripts/gb-model/extract_fes_sheet.py"
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# SPDX-FileCopyrightText: gb-open-market-model contributors
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
6+
"""
7+
FES worksheet extractor.
8+
9+
This is a generalised script to extract tables based on their manually configured positions in non-machine-readable Excel sheets.
10+
"""
11+
12+
import logging
13+
14+
import pandas as pd
15+
16+
from scripts._helpers import configure_logging, set_scenario_config
17+
18+
logger = logging.getLogger(__name__)
19+
20+
21+
def extract_fes_worksheet(
22+
excel_path: str, sheet_name: str, sheet_config: dict
23+
) -> pd.DataFrame:
24+
"""
25+
Extract FES worksheet data from an Excel file.
26+
27+
Args:
28+
excel_path (str): Path to the Excel file.
29+
sheet_name (str): Name of the sheet to extract.
30+
sheet_config (dict): Configuration for the sheet extraction.
31+
32+
Returns:
33+
pd.DataFrame: A DataFrame containing the extracted FES worksheet data.
34+
"""
35+
renamers = sheet_config.pop("rename", {})
36+
fes_data = pd.read_excel(excel_path, sheet_name=sheet_name, **sheet_config)
37+
38+
fes_data = fes_data.rename_axis(**renamers)
39+
40+
if pd.notnull(sheet_config["header"]):
41+
fes_data = fes_data.stack(sheet_config["header"])
42+
43+
if not (unnamed_data := fes_data.filter(regex="Unnamed")).empty:
44+
logger.error(
45+
"The extracted DataFrame contains unnamed columns/rows, please check the configuration."
46+
f"First rows of unnamed data:\n{unnamed_data.head()}"
47+
)
48+
raise
49+
return fes_data.to_frame("data")
50+
51+
52+
if __name__ == "__main__":
53+
if "snakemake" not in globals():
54+
from scripts._helpers import mock_snakemake
55+
56+
snakemake = mock_snakemake("extract_fes_sheet")
57+
configure_logging(snakemake)
58+
set_scenario_config(snakemake)
59+
60+
availability_df = extract_fes_worksheet(
61+
snakemake.input.workbook,
62+
snakemake.wildcards.fes_sheet,
63+
snakemake.params.sheet_extract_config,
64+
)
65+
availability_df.to_csv(snakemake.output.csv)

0 commit comments

Comments
 (0)