Skip to content

Commit bb3a895

Browse files
authored
Merge pull request #30 from Open-ISP/representative-weeks
Representative weeks
2 parents a28448d + f7f8bbb commit bb3a895

File tree

10 files changed

+383
-11
lines changed

10 files changed

+383
-11
lines changed

dodo.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
)
4343
from ispypsa.translator.lines import translate_flow_paths_to_lines
4444
from ispypsa.translator.snapshot import create_complete_snapshot_index
45+
from ispypsa.translator.temporal_filters import filter_snapshot
4546

4647
root_folder = Path("ispypsa_runs")
4748

@@ -143,12 +144,15 @@ def create_pypsa_inputs_from_config_and_ispypsa_inputs(
143144
) -> None:
144145
create_or_clean_task_output_folder(pypsa_inputs_location)
145146
pypsa_inputs = {}
146-
pypsa_inputs["snapshot"] = create_complete_snapshot_index(
147+
snapshot = create_complete_snapshot_index(
147148
start_year=config.temporal.start_year,
148149
end_year=config.temporal.end_year,
149150
operational_temporal_resolution_min=config.temporal.operational_temporal_resolution_min,
150151
year_type=config.temporal.year_type,
151152
)
153+
pypsa_inputs["snapshot"] = filter_snapshot(
154+
config=config.temporal, snapshot=snapshot
155+
)
152156
pypsa_inputs["generators"] = _translate_ecaa_generators(
153157
ispypsa_inputs_location, config.network.nodes.regional_granularity
154158
)

ispypsa_runs/development/ispypsa_inputs/ispypsa_config.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ temporal:
3232
start_year: 2025
3333
end_year: 2025
3434
reference_year_cycle: [2018]
35+
aggregation:
36+
# Representative weeks to use instead of full yearly temporal representation.
37+
# Options:
38+
# "None": Full yearly temporal representation is used.
39+
# list[int]: a list of integers specifying weeks of year to use as representative. Weeks of year are defined as
40+
# full weeks (Monday-Sunday) falling within the year. For example, if the list is "[1]" the model will only use the
41+
# first full week of each modelled year.
42+
representative_weeks: [1, 12, 25, 38]
3543
# External solver to use
3644
# Options (refer to https://pypsa.readthedocs.io/en/latest/getting-started/installation.html):
3745
# Free, and by default, installed with ISPyPSA:

src/ispypsa/config/validators.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,18 @@ class NetworkConfig(BaseModel):
1616
nodes: NodesConfig
1717

1818

19+
class TemporalAggregationConfig(BaseModel):
20+
representative_weeks: list[int] | None
21+
22+
1923
class TemporalConfig(BaseModel):
2024
operational_temporal_resolution_min: int
2125
path_to_parsed_traces: str
2226
year_type: Literal["fy", "calendar"]
2327
start_year: int
2428
end_year: int
2529
reference_year_cycle: list[int]
30+
aggregation: TemporalAggregationConfig
2631

2732
@field_validator("operational_temporal_resolution_min")
2833
@classmethod

src/ispypsa/translator/buses.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from isp_trace_parser import get_data
66

77
from ispypsa.translator.mappings import _BUS_ATTRIBUTES
8+
from ispypsa.translator.temporal_filters import time_series_filter
89
from ispypsa.translator.time_series_checker import check_time_series
910

1011

@@ -111,6 +112,7 @@ def _translate_buses_demand_timeseries(
111112
node_trace = node_traces.groupby("Datetime", as_index=False)["Value"].sum()
112113
# datetime in nanoseconds required by PyPSA
113114
node_trace["Datetime"] = node_trace["Datetime"].astype("datetime64[ns]")
115+
node_trace = time_series_filter(node_trace, snapshot)
114116
check_time_series(
115117
node_trace["Datetime"],
116118
pd.Series(snapshot.index),

src/ispypsa/translator/generators.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from isp_trace_parser import get_data
66

77
from ispypsa.translator.mappings import _GENERATOR_ATTRIBUTES
8+
from ispypsa.translator.temporal_filters import time_series_filter
89
from ispypsa.translator.time_series_checker import check_time_series
910

1011

@@ -117,7 +118,8 @@ def _translate_generator_timeseries(
117118
)
118119
# datetime in nanoseconds required by PyPSA
119120
trace["Datetime"] = trace["Datetime"].astype("datetime64[ns]")
121+
trace = time_series_filter(trace, snapshot)
120122
check_time_series(
121-
trace["Datetime"], pd.Series(snapshot.index), "generator trace data", gen
123+
trace["Datetime"], snapshot.index.to_series(), "generator trace data", gen
122124
)
123125
trace.to_parquet(Path(output_trace_path, f"{gen}.parquet"), index=False)

src/ispypsa/translator/helpers.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
def get_iteration_start_and_end_time(year_type: str, start_year: int, end_year: int):
2+
"""Get the model start year, end year, and start/end month for iteration, which depend on
3+
financial vs calendar year.
4+
"""
5+
if year_type == "fy":
6+
start_year = start_year - 1
7+
end_year = end_year
8+
month = 7
9+
else:
10+
start_year = start_year
11+
end_year = end_year + 1
12+
month = 1
13+
return start_year, end_year, month

src/ispypsa/translator/snapshot.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import pandas as pd
44

5+
from ispypsa.translator.helpers import get_iteration_start_and_end_time
6+
57

68
def create_complete_snapshot_index(
79
start_year: int,
@@ -25,14 +27,9 @@ def create_complete_snapshot_index(
2527
Returns:
2628
pd.DataFrame
2729
"""
28-
if year_type == "fy":
29-
start_year = start_year - 1
30-
end_year = end_year
31-
month = 7
32-
else:
33-
start_year = start_year
34-
end_year = end_year + 1
35-
month = 1
30+
start_year, end_year, month = get_iteration_start_and_end_time(
31+
year_type, start_year, end_year
32+
)
3633

3734
if operational_temporal_resolution_min < 60:
3835
hour = 0
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
from datetime import datetime, timedelta
2+
3+
import pandas as pd
4+
5+
from ispypsa.config.validators import TemporalConfig
6+
from ispypsa.translator.helpers import get_iteration_start_and_end_time
7+
8+
9+
def time_series_filter(time_series_data: pd.DataFrame, snapshot: pd.DataFrame):
10+
"""Filters a timeseries pandas DataFrame based using the datetime values in
11+
the snapshot index.
12+
13+
Examples:
14+
15+
>>> datetime_index = pd.date_range('2020-01-01', '2020-01-03', freq='h')
16+
>>> time_series_data = pd.DataFrame({'Datetime': datetime_index, 'Value': range(len(datetime_index))})
17+
>>> snapshot = pd.DataFrame(index=datetime_index[::12]) # Every 12 hours
18+
>>> time_series_filter(time_series_data, snapshot)
19+
Datetime Value
20+
0 2020-01-01 00:00:00 0
21+
12 2020-01-01 12:00:00 12
22+
24 2020-01-02 00:00:00 24
23+
36 2020-01-02 12:00:00 36
24+
48 2020-01-03 00:00:00 48
25+
26+
Args:
27+
time_series_data: pd.DataFrame with time series column called 'Datetime'
28+
snapshot: pd.DataFrame with datetime index
29+
30+
"""
31+
return time_series_data[time_series_data["Datetime"].isin(snapshot.index)]
32+
33+
34+
def filter_snapshot(config: TemporalConfig, snapshot: pd.DataFrame):
35+
"""Appy filter to the snapshot based on the model config.
36+
37+
- If config.representative_weeks is not None then filter the
38+
snapshot based on the supplied list of representative weeks.
39+
40+
Examples:
41+
42+
# Create dummy config class with just data need for example.
43+
44+
>>> from dataclasses import dataclass
45+
46+
>>> @dataclass
47+
... class TemporalAggregationConfig:
48+
... representative_weeks: list[int]
49+
50+
>>> @dataclass
51+
... class TemporalConfig:
52+
... start_year: int
53+
... end_year: int
54+
... year_type: str
55+
... aggregation: TemporalAggregationConfig
56+
57+
>>> config = TemporalConfig(
58+
... start_year=2024,
59+
... end_year=2024,
60+
... year_type='calendar',
61+
... aggregation=TemporalAggregationConfig(
62+
... representative_weeks=[1],
63+
... )
64+
... )
65+
66+
>>> snapshot = pd.DataFrame(index=pd.date_range('2024-01-01', '2024-12-31', freq='h'))
67+
68+
>>> snapshot = filter_snapshot(config, snapshot)
69+
70+
>>> snapshot.index[0]
71+
Timestamp('2024-01-01 01:00:00')
72+
73+
>>> snapshot.index[-1]
74+
Timestamp('2024-01-08 00:00:00')
75+
76+
Args:
77+
config: TemporalConfig defining snapshot filtering.
78+
snapshot: pd.DataFrame with datetime index containing the snapshot
79+
"""
80+
if config.aggregation.representative_weeks is not None:
81+
snapshot = filter_snapshot_for_representative_weeks(
82+
representative_weeks=config.aggregation.representative_weeks,
83+
snapshot=snapshot,
84+
start_year=config.start_year,
85+
end_year=config.end_year,
86+
year_type=config.year_type,
87+
)
88+
return snapshot
89+
90+
91+
def filter_snapshot_for_representative_weeks(
92+
representative_weeks: list[int],
93+
snapshot: pd.DataFrame,
94+
start_year: int,
95+
end_year: int,
96+
year_type: str,
97+
):
98+
"""Filters a snapshot by a list of weeks.
99+
100+
A snapshot is provided as a pandas DatFrame with a datetime index. The
101+
snapshot may be multiple years in length. The snapshot is filtered for
102+
date times that fall within the weeks defined in representative_weeks.
103+
The weeks are defined as full weeks within a financial or calendar year,
104+
depending on the year_type provided.
105+
106+
Examples:
107+
>>> # Filter for first and last full weeks of each calendar year from 2020-2022
108+
>>> df = pd.DataFrame(index=pd.date_range('2020-01-01', '2022-12-31', freq='h'))
109+
>>> filter_snapshot_for_representative_weeks(
110+
... representative_weeks=[1],
111+
... snapshot=df,
112+
... start_year=2020,
113+
... end_year=2022,
114+
... year_type='calendar'
115+
... ).head(3)
116+
Empty DataFrame
117+
Columns: []
118+
Index: [2020-01-06 01:00:00, 2020-01-06 02:00:00, 2020-01-06 03:00:00]
119+
120+
>>> # Filter for weeks 1, 26 of financial years 2021-2022 (July 2020 - June 2022)
121+
>>> df = pd.DataFrame(index=pd.date_range('2020-07-01', '2022-06-30', freq='h'))
122+
>>> filter_snapshot_for_representative_weeks(
123+
... representative_weeks=[2],
124+
... snapshot=df,
125+
... start_year=2021,
126+
... end_year=2022,
127+
... year_type='fy'
128+
... ).head(3)
129+
Empty DataFrame
130+
Columns: []
131+
Index: [2020-07-13 01:00:00, 2020-07-13 02:00:00, 2020-07-13 03:00:00]
132+
133+
Args:
134+
representative_weeks: list[int] of full weeks to filter for. The
135+
week 1 refers to the first full week (Monday-Sunday) falling
136+
with in the year.
137+
snapshot: pd.DataFrame with datetime index containing the snapshot
138+
start_year: int defining the start year of the snapshot (inclusive)
139+
end_year: int defining the end year of the snapshot (inclusive)
140+
year_type: str defining year the 'fy' for financial year or 'calendar'
141+
142+
Raises: ValueError if the end of week falls outside after the year end i.e.
143+
for all weeks 53 or greater and for some years the week 52.
144+
"""
145+
start_year, end_year, month = get_iteration_start_and_end_time(
146+
year_type, start_year, end_year
147+
)
148+
149+
snapshot = snapshot.index.to_series()
150+
151+
filtered_snapshot = []
152+
153+
for year in range(start_year, end_year):
154+
start_of_year_date_time = datetime(
155+
year=year, month=month, day=1, hour=0, minute=0
156+
)
157+
end_of_year_date_time = datetime(
158+
year=year + 1, month=month, day=1, hour=0, minute=0
159+
)
160+
days_until_monday = (7 - start_of_year_date_time.weekday()) % 7
161+
first_monday = start_of_year_date_time + timedelta(days=days_until_monday)
162+
for week_number in representative_weeks:
163+
nth_week_start = first_monday + timedelta(weeks=week_number - 1)
164+
nth_week_end = nth_week_start + timedelta(days=7)
165+
166+
if nth_week_end - timedelta(seconds=1) > end_of_year_date_time:
167+
raise ValueError(
168+
f"Representative week {week_number} ends after end of model year {year}."
169+
" Adjust config to use a smaller week_number for representative_weeks."
170+
)
171+
172+
filtered_snapshot.append(
173+
snapshot[
174+
(snapshot > nth_week_start) & (snapshot <= nth_week_end)
175+
].copy()
176+
)
177+
178+
filtered_snapshot = pd.concat(filtered_snapshot)
179+
180+
filtered_snapshot = pd.DataFrame(index=filtered_snapshot)
181+
182+
return filtered_snapshot

0 commit comments

Comments
 (0)