hefty 0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hefty/__init__.py +0 -0
- hefty/custom.py +204 -0
- hefty/pv_model.py +574 -0
- hefty/solar.py +1770 -0
- hefty/utilities.py +261 -0
- hefty/wind.py +269 -0
- hefty-0.0.2.dist-info/METADATA +117 -0
- hefty-0.0.2.dist-info/RECORD +11 -0
- hefty-0.0.2.dist-info/WHEEL +5 -0
- hefty-0.0.2.dist-info/licenses/LICENSE +28 -0
- hefty-0.0.2.dist-info/top_level.txt +1 -0
hefty/utilities.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import warnings
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def model_input_formatter(init_date, run_length, lead_time_to_start=0,
|
|
6
|
+
model='gfs', resource_type='solar'):
|
|
7
|
+
"""
|
|
8
|
+
Helper function to format model-specific inputs for Herbie.
|
|
9
|
+
|
|
10
|
+
In the case where the user selects an invalid intitialization date, or
|
|
11
|
+
combination of init date and lead time, it tries to update the init date
|
|
12
|
+
and lead time to match a valid init date for the selected model, but this
|
|
13
|
+
hasn't been fully tested.
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
init_date : pandas-parsable datetime
|
|
18
|
+
Targetted initialization datetime.
|
|
19
|
+
|
|
20
|
+
run_length : int
|
|
21
|
+
Length of the forecast in hours.
|
|
22
|
+
|
|
23
|
+
lead_time_to_start : int
|
|
24
|
+
Number of hours from the init_date to the first interval in the
|
|
25
|
+
forecast.
|
|
26
|
+
|
|
27
|
+
model : {'gfs', 'ifs', 'aifs', 'hrrr', 'gefs'}
|
|
28
|
+
Forecast model name, case insensitive. Default is 'gfs'.
|
|
29
|
+
|
|
30
|
+
resource_type : {'solar, 'wind'}
|
|
31
|
+
Resrouce type. Default is 'solar'.
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
date : pandas-parsable datetime
|
|
36
|
+
initialization date, rounded down to the last valid date for the given
|
|
37
|
+
model if needed.
|
|
38
|
+
|
|
39
|
+
fxx_range : int or list of ints
|
|
40
|
+
fxx (lead time) values.
|
|
41
|
+
|
|
42
|
+
product : string
|
|
43
|
+
model product, e.g., 'pgrb2.0p25' for 'gfs'
|
|
44
|
+
|
|
45
|
+
search_str : string
|
|
46
|
+
wgrib2-style search string for Herbie to select variables of interest.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
if model == 'gfs':
|
|
50
|
+
# GFS:
|
|
51
|
+
# 0 to 120 by 1, 123 to 384 by 3
|
|
52
|
+
# runs every 6 hours starting at 00z
|
|
53
|
+
update_freq = '6h'
|
|
54
|
+
# round down to last actual initialization time
|
|
55
|
+
date = init_date.floor(update_freq)
|
|
56
|
+
|
|
57
|
+
# offset in hours between selected init_date and fcast run
|
|
58
|
+
init_offset = int((init_date - date).total_seconds()/3600)
|
|
59
|
+
lead_time_to_start = lead_time_to_start + init_offset
|
|
60
|
+
|
|
61
|
+
# maximum forecast horizon, update with new lead time
|
|
62
|
+
fxx_max = run_length + lead_time_to_start
|
|
63
|
+
|
|
64
|
+
# set forecast lead times
|
|
65
|
+
if lead_time_to_start <= 120 and fxx_max > 120:
|
|
66
|
+
fxx_max = round(fxx_max/3)*3
|
|
67
|
+
fxx_range = [*range(lead_time_to_start, 120+1, 1),
|
|
68
|
+
*range(123, fxx_max + 1, 3)]
|
|
69
|
+
elif lead_time_to_start > 120:
|
|
70
|
+
fxx_max = round(fxx_max/3)*3
|
|
71
|
+
lead_time_to_start = round(lead_time_to_start/3)*3
|
|
72
|
+
fxx_range = range(lead_time_to_start, fxx_max, 3)
|
|
73
|
+
else:
|
|
74
|
+
fxx_range = range(lead_time_to_start, fxx_max, 1)
|
|
75
|
+
|
|
76
|
+
# Herbie inputs
|
|
77
|
+
product = 'pgrb2.0p25'
|
|
78
|
+
if resource_type == 'solar':
|
|
79
|
+
search_str = 'DSWRF|:TMP:2 m above|[UV]GRD:10 m above'
|
|
80
|
+
elif resource_type == 'wind':
|
|
81
|
+
search_str = (
|
|
82
|
+
'[UV]GRD:10 m above|[UV]GRD:80 m above|'
|
|
83
|
+
'[UV]GRD:100 m above|:TMP:2 m above|PRES:surface|'
|
|
84
|
+
':TMP:80 m above|PRES:80 m above'
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
elif model == 'gefs':
|
|
88
|
+
# GEFS:
|
|
89
|
+
# 0.5 deg:
|
|
90
|
+
# 0 to 384 by 3, 390 to 840 by 6 for 00z cycle only
|
|
91
|
+
# 0.25 deg:
|
|
92
|
+
# 0 to 240 by 3
|
|
93
|
+
# runs every 6 hours starting at 00z
|
|
94
|
+
update_freq = '6h'
|
|
95
|
+
# round down to last actual initialization time
|
|
96
|
+
date = init_date.floor(update_freq)
|
|
97
|
+
|
|
98
|
+
# offset in hours between selected init_date and fcast run
|
|
99
|
+
init_offset = int((init_date - date).total_seconds()/3600)
|
|
100
|
+
lead_time_to_start = lead_time_to_start + init_offset
|
|
101
|
+
|
|
102
|
+
# maximum forecast horizon, update with new lead time
|
|
103
|
+
fxx_max = run_length + lead_time_to_start
|
|
104
|
+
|
|
105
|
+
# set forecast lead times
|
|
106
|
+
fxx_range = range(lead_time_to_start, fxx_max + 1, 3)
|
|
107
|
+
|
|
108
|
+
# Herbie inputs
|
|
109
|
+
if resource_type == 'solar':
|
|
110
|
+
# solar radiation is not available for f00 (lead_time_to_start=0)
|
|
111
|
+
# adjust accordingly
|
|
112
|
+
if lead_time_to_start < 3:
|
|
113
|
+
lead_time_to_start = 3
|
|
114
|
+
warnings.warn(
|
|
115
|
+
("You have specified a lead_time_to_start less"
|
|
116
|
+
"than 3 h. GHI in GEFS is only available "
|
|
117
|
+
"starting at F03. The lead_time_to_start has been"
|
|
118
|
+
"changed to 3 h."))
|
|
119
|
+
|
|
120
|
+
if fxx_max <= 240:
|
|
121
|
+
product = 'atmos.25' # 0.25 deg, 'pgrb2.0p25'
|
|
122
|
+
search_str = 'DSWRF|:TMP:2 m above|[UV]GRD:10 m above'
|
|
123
|
+
else:
|
|
124
|
+
product = 'atmos.5' # 0.5 deg, 'pgrb2.0p5'
|
|
125
|
+
search_str = 'DSWRF|:TMP:2 m above|[UV]GRD:10 m above'
|
|
126
|
+
elif resource_type == 'wind':
|
|
127
|
+
product = 'atmos.5b' # 0.5 deg, 'pgrb2.0p5
|
|
128
|
+
search_str = (
|
|
129
|
+
'[UV]GRD:80 m above|[UV]GRD:100 m above|'
|
|
130
|
+
':TMP:80 m above|PRES:80 m above'
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
elif model == 'ifs':
|
|
134
|
+
# From https://www.ecmwf.int/en/forecasts/datasets/open-data
|
|
135
|
+
# For times 00z &12z: 0 to 144 by 3, 150 to 240 by 6.
|
|
136
|
+
# For times 06z & 18z: 0 to 90 by 3.
|
|
137
|
+
# From:
|
|
138
|
+
# https://confluence.ecmwf.int/display/DAC/ECMWF+open+data%3A+real-time+forecasts+from+IFS+and+AIFS
|
|
139
|
+
# Product "oper" runs 00z, 12z, 0h to 144h by 3h, 144h to 240h by 6h
|
|
140
|
+
# Product "scda" runs 06z, 18z, 0h to 90h by 3h
|
|
141
|
+
# **BUT**, see https://github.com/blaylockbk/Herbie/discussions/421
|
|
142
|
+
# Starting 2024-11-12 06:00, 'scda' runs to 144h by 3h
|
|
143
|
+
|
|
144
|
+
# round to last 6 hours to start
|
|
145
|
+
date = init_date.floor('6h')
|
|
146
|
+
init_offset = int((init_date - date).total_seconds()/3600)
|
|
147
|
+
lead_time_to_start = lead_time_to_start + init_offset
|
|
148
|
+
fxx_max = run_length + lead_time_to_start
|
|
149
|
+
|
|
150
|
+
# pick init time based on forecast max lead time:
|
|
151
|
+
# check if 'scda' product is ideal
|
|
152
|
+
if init_date.hour == 6 or init_date.hour == 18:
|
|
153
|
+
if init_date >= pd.to_datetime('2024-11-12 06:00'):
|
|
154
|
+
scda_fxx_max = 144
|
|
155
|
+
else:
|
|
156
|
+
scda_fxx_max = 90
|
|
157
|
+
if fxx_max > scda_fxx_max: # forecast beyond scda
|
|
158
|
+
update_freq = '12h' # must use 'oper' runs
|
|
159
|
+
warnings.warn(
|
|
160
|
+
("You have specified an init_date which would have mapped "
|
|
161
|
+
"to a 06z or 18z. Those runs the IFS 'scda' product, and "
|
|
162
|
+
"'scda' only goes out 144 hours (90h prior to 2024-11-12)"
|
|
163
|
+
". You will get forecasts from the 'oper' run 6 hours "
|
|
164
|
+
"earlier, instead."))
|
|
165
|
+
else:
|
|
166
|
+
update_freq = '6h' # can use 'oper' or 'scda'
|
|
167
|
+
else:
|
|
168
|
+
update_freq = '6h' # can use 'oper' or 'scda'
|
|
169
|
+
# round down to last actual initialization time
|
|
170
|
+
date = init_date.floor(update_freq)
|
|
171
|
+
|
|
172
|
+
# offset in hours between selected init_date and fcast run
|
|
173
|
+
init_offset = int((init_date - date).total_seconds()/3600)
|
|
174
|
+
lead_time_to_start = lead_time_to_start + init_offset
|
|
175
|
+
if lead_time_to_start > 141:
|
|
176
|
+
run_length = max(run_length, 6) # make sure it's long enough
|
|
177
|
+
fxx_max = run_length + lead_time_to_start # update this
|
|
178
|
+
|
|
179
|
+
# set forecast intervals
|
|
180
|
+
if lead_time_to_start <= 144 and fxx_max > 144:
|
|
181
|
+
lead_time_to_start = round(lead_time_to_start/3)*3
|
|
182
|
+
fxx_max = round(fxx_max/6)*6
|
|
183
|
+
# make sure it goes to at least the next interval
|
|
184
|
+
fxx_max = max(fxx_max, 150)
|
|
185
|
+
fxx_range = [*range(lead_time_to_start, 145, 3),
|
|
186
|
+
*range(150, fxx_max + 1, 6)]
|
|
187
|
+
elif lead_time_to_start > 144:
|
|
188
|
+
lead_time_to_start = round(lead_time_to_start/6)*6
|
|
189
|
+
fxx_max = round(fxx_max/6)*6
|
|
190
|
+
fxx_range = range(lead_time_to_start, fxx_max + 1, 6)
|
|
191
|
+
else:
|
|
192
|
+
lead_time_to_start = round(lead_time_to_start/3)*3
|
|
193
|
+
fxx_max = round(fxx_max/3)*3
|
|
194
|
+
fxx_range = range(lead_time_to_start, fxx_max + 1, 3)
|
|
195
|
+
|
|
196
|
+
# Herbie inputs
|
|
197
|
+
if date.hour == 6 or date.hour == 18:
|
|
198
|
+
product = 'scda'
|
|
199
|
+
else:
|
|
200
|
+
product = 'oper'
|
|
201
|
+
|
|
202
|
+
if resource_type == 'solar':
|
|
203
|
+
search_str = ':ssrd|10[uv]|2t:sfc'
|
|
204
|
+
elif resource_type == 'wind':
|
|
205
|
+
search_str = ':10[uv]|:100[uv]|:2t:sfc|:sp:'
|
|
206
|
+
|
|
207
|
+
elif model == 'aifs':
|
|
208
|
+
# From https://www.ecmwf.int/en/forecasts/datasets/set-ix,
|
|
209
|
+
# https://www.ecmwf.int/en/forecasts/dataset/set-x
|
|
210
|
+
# 4 forecast runs per day (00/06/12/18)
|
|
211
|
+
# 6 hourly steps to 360 (15 days)
|
|
212
|
+
|
|
213
|
+
# round to last 6 hours to start
|
|
214
|
+
date = init_date.floor('6h')
|
|
215
|
+
init_offset = int((init_date - date).total_seconds()/3600)
|
|
216
|
+
lead_time_to_start = lead_time_to_start + init_offset
|
|
217
|
+
fxx_max = run_length + lead_time_to_start
|
|
218
|
+
|
|
219
|
+
update_freq = '6h'
|
|
220
|
+
# round down to last actual initialization time
|
|
221
|
+
date = init_date.floor(update_freq)
|
|
222
|
+
|
|
223
|
+
# offset in hours between selected init_date and fcast run
|
|
224
|
+
init_offset = int((init_date - date).total_seconds()/3600)
|
|
225
|
+
lead_time_to_start = lead_time_to_start + init_offset
|
|
226
|
+
if lead_time_to_start > 141:
|
|
227
|
+
run_length = max(run_length, 6) # make sure it's long enough
|
|
228
|
+
fxx_max = run_length + lead_time_to_start # update this
|
|
229
|
+
|
|
230
|
+
# set forecast intervals
|
|
231
|
+
fxx_range = range(lead_time_to_start, fxx_max + 1, 6)
|
|
232
|
+
|
|
233
|
+
# Herbie inputs
|
|
234
|
+
product = 'oper' # deterministic
|
|
235
|
+
|
|
236
|
+
if resource_type == 'solar':
|
|
237
|
+
search_str = ':ssrd|10[uv]|2t:sfc'
|
|
238
|
+
elif resource_type == 'wind':
|
|
239
|
+
search_str = ':10[uv]|:100[uv]|:2t:sfc|:sp:'
|
|
240
|
+
|
|
241
|
+
elif model == 'hrrr':
|
|
242
|
+
# maximum forecast horizon
|
|
243
|
+
fxx_max = run_length + lead_time_to_start
|
|
244
|
+
product = 'sfc'
|
|
245
|
+
|
|
246
|
+
if resource_type == 'solar':
|
|
247
|
+
search_str = 'DSWRF|:TMP:2 m above|[UV]GRD:10 m above'
|
|
248
|
+
elif resource_type == 'wind':
|
|
249
|
+
search_str = (
|
|
250
|
+
'[UV]GRD:10 m above|[UV]GRD:80 m above|'
|
|
251
|
+
':TMP:2 m above|PRES:surface'
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
update_freq = '1h'
|
|
255
|
+
|
|
256
|
+
# round down to last actual initialization time
|
|
257
|
+
date = init_date.floor(update_freq)
|
|
258
|
+
|
|
259
|
+
fxx_range = range(lead_time_to_start, fxx_max, 1)
|
|
260
|
+
|
|
261
|
+
return date, fxx_range, product, search_str
|
hefty/wind.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import xarray as xr
|
|
3
|
+
from herbie import Herbie
|
|
4
|
+
import time
|
|
5
|
+
from hefty.utilities import model_input_formatter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_wind_forecast(latitude, longitude, init_date, run_length,
|
|
9
|
+
lead_time_to_start=0, model='gfs', member=None,
|
|
10
|
+
attempts=2, hrrr_hour_middle=True,
|
|
11
|
+
hrrr_coursen_window=None, priority=None):
|
|
12
|
+
"""
|
|
13
|
+
Get a wind resource forecast for one or several sites from one of several
|
|
14
|
+
NWPs. This function uses Herbie [1]_ and pvlib [2]_.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
latitude : float or list of floats
|
|
19
|
+
Latitude in decimal degrees. Positive north of equator, negative
|
|
20
|
+
to south.
|
|
21
|
+
|
|
22
|
+
longitude : float or list of floats
|
|
23
|
+
Longitude in decimal degrees. Positive east of prime meridian,
|
|
24
|
+
negative to west.
|
|
25
|
+
|
|
26
|
+
init_date : pandas-parsable datetime
|
|
27
|
+
Model initialization datetime.
|
|
28
|
+
|
|
29
|
+
run_length : int
|
|
30
|
+
Length of the forecast in hours - number of hours forecasted
|
|
31
|
+
|
|
32
|
+
lead_time_to_start : int, optional
|
|
33
|
+
Number of hours between init_date (initialization) and
|
|
34
|
+
the first forecasted interval. NOAA GFS data goes out
|
|
35
|
+
384 hours, so run_length + lead_time_to_start must be less
|
|
36
|
+
than or equal to 384.
|
|
37
|
+
|
|
38
|
+
model : string, default 'gfs'
|
|
39
|
+
Forecast model. Default is NOAA GFS ('gfs'), but can also be
|
|
40
|
+
ECMWF IFS ('ifs'), NOAA HRRR ('hrrr'), or NOAA GEFS ('gefs').
|
|
41
|
+
|
|
42
|
+
member: string or int
|
|
43
|
+
For models that are ensembles, pass an appropriate single member label.
|
|
44
|
+
|
|
45
|
+
attempts : int, optional
|
|
46
|
+
Number of times to try getting forecast data. The function will pause
|
|
47
|
+
for n^2 minutes after each n attempt, e.g., 1 min after the first
|
|
48
|
+
attempt, 4 minutes after the second, etc.
|
|
49
|
+
|
|
50
|
+
hrrr_hour_middle : bool, default True
|
|
51
|
+
If model is 'hrrr', setting this False keeps the forecast at the
|
|
52
|
+
native instantaneous top-of-hour format. True (default) shifts
|
|
53
|
+
the forecast to middle of the hour, more closely representing an
|
|
54
|
+
integrated hourly forecast that is centered in the middle of the
|
|
55
|
+
hour.
|
|
56
|
+
|
|
57
|
+
hrrr_coursen_window : int or None, default None
|
|
58
|
+
If model is 'hrrr', optional setting that is the x and y window size
|
|
59
|
+
for coarsening the xarray dataset, effectively applying spatial
|
|
60
|
+
smoothing to the HRRR model. The HRRR has a native resolution of
|
|
61
|
+
about 3 km, so a value of 10 results in approx. 30 x 30 km grid.
|
|
62
|
+
|
|
63
|
+
priority : list or string
|
|
64
|
+
List of model sources to get the data in the order of download
|
|
65
|
+
priority, or string for a single source. See Herbie docs.
|
|
66
|
+
Typical values would be 'aws' or 'google'.
|
|
67
|
+
|
|
68
|
+
Returns
|
|
69
|
+
-------
|
|
70
|
+
data : pandas.DataFrane
|
|
71
|
+
timeseries forecasted wind resource data
|
|
72
|
+
|
|
73
|
+
References
|
|
74
|
+
----------
|
|
75
|
+
|
|
76
|
+
.. [1] `Blaylock, B. K. (YEAR). Herbie: Retrieve Numerical Weather
|
|
77
|
+
Prediction Model Data (Version 20xx.x.x) [Computer software].
|
|
78
|
+
<https://doi.org/10.5281/zenodo.4567540>`_
|
|
79
|
+
.. [2] `Anderson, K., et al. “pvlib python: 2023 project update.” Journal
|
|
80
|
+
of Open Source Software, 8(92), 5994, (2023).
|
|
81
|
+
<http://dx.doi.org/10.21105/joss.05994>`_
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
# # set clear sky model. could be an input variable at some point
|
|
85
|
+
# model_cs = 'haurwitz'
|
|
86
|
+
|
|
87
|
+
# variable formatting
|
|
88
|
+
# if lat, lon are single values, convert to lists for pickpoints later
|
|
89
|
+
if type(latitude) is float or type(latitude) is int:
|
|
90
|
+
latitude = [latitude]
|
|
91
|
+
longitude = [longitude]
|
|
92
|
+
# convert init_date to datetime
|
|
93
|
+
init_date = pd.to_datetime(init_date)
|
|
94
|
+
|
|
95
|
+
# get model-specific Herbie inputs
|
|
96
|
+
date, fxx_range, product, search_str = model_input_formatter(
|
|
97
|
+
init_date, run_length, lead_time_to_start, model, resource_type='wind')
|
|
98
|
+
|
|
99
|
+
i = []
|
|
100
|
+
for fxx in fxx_range:
|
|
101
|
+
# get solar, 10m wind, and 2m temp data
|
|
102
|
+
# try n times based loosely on
|
|
103
|
+
# https://thingspython.wordpress.com/2021/12/05/how-to-try-something-n-times-in-python/
|
|
104
|
+
for attempts_remaining in reversed(range(attempts)):
|
|
105
|
+
attempt_num = attempts - attempts_remaining
|
|
106
|
+
try:
|
|
107
|
+
if attempt_num == 1:
|
|
108
|
+
# try downloading
|
|
109
|
+
ds = Herbie(
|
|
110
|
+
date,
|
|
111
|
+
model=model,
|
|
112
|
+
product=product,
|
|
113
|
+
fxx=fxx,
|
|
114
|
+
member=member,
|
|
115
|
+
priority=priority
|
|
116
|
+
).xarray(search_str)
|
|
117
|
+
else:
|
|
118
|
+
# after first attempt, set overwrite=True to overwrite
|
|
119
|
+
# partial files
|
|
120
|
+
ds = Herbie(
|
|
121
|
+
date,
|
|
122
|
+
model=model,
|
|
123
|
+
product=product,
|
|
124
|
+
fxx=fxx,
|
|
125
|
+
member=member,
|
|
126
|
+
priority=priority
|
|
127
|
+
).xarray(search_str, overwrite=True)
|
|
128
|
+
except Exception:
|
|
129
|
+
if attempts_remaining:
|
|
130
|
+
print('attempt ' + str(attempt_num) + ' failed, pause for '
|
|
131
|
+
+ str((attempt_num)**2) + ' min')
|
|
132
|
+
time.sleep(60*(attempt_num)**2)
|
|
133
|
+
else:
|
|
134
|
+
break
|
|
135
|
+
else:
|
|
136
|
+
raise ValueError('download failed, ran out of attempts')
|
|
137
|
+
|
|
138
|
+
# merge - override avoids hight conflict between 2m temp and 10m wind
|
|
139
|
+
ds = xr.merge(ds, compat='override')
|
|
140
|
+
# calculate wind speed from u and v components
|
|
141
|
+
ds = ds.herbie.with_wind('both')
|
|
142
|
+
|
|
143
|
+
if model == 'hrrr' and hrrr_coursen_window is not None:
|
|
144
|
+
ds = ds.coarsen(x=hrrr_coursen_window,
|
|
145
|
+
y=hrrr_coursen_window,
|
|
146
|
+
boundary='trim').mean()
|
|
147
|
+
|
|
148
|
+
# use pick_points for single point or list of points
|
|
149
|
+
i.append(
|
|
150
|
+
ds.herbie.pick_points(
|
|
151
|
+
pd.DataFrame(
|
|
152
|
+
{
|
|
153
|
+
"latitude": latitude,
|
|
154
|
+
"longitude": longitude,
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
ts = xr.concat(i, dim="valid_time") # concatenate
|
|
160
|
+
|
|
161
|
+
# convert to dataframe, convert names and units
|
|
162
|
+
if model == 'gfs':
|
|
163
|
+
df_temp = ts.to_dataframe()[
|
|
164
|
+
['si10',
|
|
165
|
+
'ws',
|
|
166
|
+
'si100',
|
|
167
|
+
'wdir10',
|
|
168
|
+
'wdir',
|
|
169
|
+
'wdir100',
|
|
170
|
+
# 't2m', # not really needed but could be used
|
|
171
|
+
't',
|
|
172
|
+
# 'sp', # not really needed but could be used
|
|
173
|
+
'pres']
|
|
174
|
+
]
|
|
175
|
+
# df_temp['t2m'] = df_temp['t2m'] - 273.15
|
|
176
|
+
df_temp['t'] = df_temp['t'] - 273.15
|
|
177
|
+
df_temp.rename(columns={
|
|
178
|
+
'si10': 'wind_speed_10m',
|
|
179
|
+
'ws': 'wind_speed_80m',
|
|
180
|
+
'si100': 'wind_speed_100m',
|
|
181
|
+
'wdir10': 'wind_direction_10m',
|
|
182
|
+
'wdir': 'wind_direction_80m',
|
|
183
|
+
'wdir100': 'wind_direction_100m',
|
|
184
|
+
# 't2m': 'temp_air_2m', # not really needed but could be used
|
|
185
|
+
't': 'temp_air_80m',
|
|
186
|
+
# 'sp': 'pressure_0m', # not really needed but could be used
|
|
187
|
+
'pres': 'pressure_80m',
|
|
188
|
+
}, inplace=True)
|
|
189
|
+
elif model == 'gefs':
|
|
190
|
+
df_temp = ts.to_dataframe()[
|
|
191
|
+
['ws', 'si100', 'wdir', 'wdir100', 't', 'pres']
|
|
192
|
+
]
|
|
193
|
+
df_temp['t'] = df_temp['t'] - 273.15
|
|
194
|
+
df_temp.rename(columns={
|
|
195
|
+
'ws': 'wind_speed_80m',
|
|
196
|
+
'si100': 'wind_speed_100m',
|
|
197
|
+
'wdir': 'wind_direction_80m',
|
|
198
|
+
'wdir100': 'wind_direction_100m',
|
|
199
|
+
't': 'temp_air_80m',
|
|
200
|
+
'pres': 'pressure_80m',
|
|
201
|
+
}, inplace=True)
|
|
202
|
+
elif model == 'hrrr':
|
|
203
|
+
df_temp = ts.to_dataframe()[
|
|
204
|
+
['si10', 'ws', 'wdir10', 'wdir', 't2m', 'sp']
|
|
205
|
+
]
|
|
206
|
+
df_temp['t2m'] = df_temp['t2m'] - 273.15
|
|
207
|
+
df_temp.rename(columns={
|
|
208
|
+
'si10': 'wind_speed_10m',
|
|
209
|
+
'ws': 'wind_speed_80m',
|
|
210
|
+
'wdir10': 'wind_direction_10m',
|
|
211
|
+
'wdir': 'wind_direction_80m',
|
|
212
|
+
't2m': 'temp_air_2m',
|
|
213
|
+
'sp': 'pressure_0m',
|
|
214
|
+
}, inplace=True)
|
|
215
|
+
elif model == 'ifs' or model == 'aifs':
|
|
216
|
+
df_temp = ts.to_dataframe()[
|
|
217
|
+
['si10', 'si100', 'wdir10', 'wdir100', 't2m', 'sp']
|
|
218
|
+
]
|
|
219
|
+
df_temp['t2m'] = df_temp['t2m'] - 273.15
|
|
220
|
+
df_temp.rename(columns={
|
|
221
|
+
'si10': 'wind_speed_10m',
|
|
222
|
+
'si100': 'wind_speed_100m',
|
|
223
|
+
'wdir10': 'wind_direction_10m',
|
|
224
|
+
'wdir100': 'wind_direction_100m',
|
|
225
|
+
't2m': 'temp_air_2m',
|
|
226
|
+
'sp': 'pressure_0m',
|
|
227
|
+
}, inplace=True)
|
|
228
|
+
|
|
229
|
+
# work through sites
|
|
230
|
+
dfs = {} # empty list of dataframes
|
|
231
|
+
if type(latitude) is float or type(latitude) is int:
|
|
232
|
+
num_sites = 1
|
|
233
|
+
else:
|
|
234
|
+
num_sites = len(latitude)
|
|
235
|
+
|
|
236
|
+
for j in range(num_sites):
|
|
237
|
+
df = df_temp[df_temp.index.get_level_values('point') == j]
|
|
238
|
+
df = df.droplevel('point')
|
|
239
|
+
|
|
240
|
+
if model == 'hrrr' and hrrr_hour_middle is False:
|
|
241
|
+
# keep top of hour instantaneous HRRR convention
|
|
242
|
+
dfs[j] = df
|
|
243
|
+
else:
|
|
244
|
+
# 60min version of data, centered at bottom of the hour
|
|
245
|
+
# 1min interpolation, then 60min mean
|
|
246
|
+
df_60min = (
|
|
247
|
+
df
|
|
248
|
+
.resample('1min')
|
|
249
|
+
.interpolate()
|
|
250
|
+
.resample('60min').mean()
|
|
251
|
+
)
|
|
252
|
+
df_60min.index = df_60min.index + pd.Timedelta('30min')
|
|
253
|
+
dfs[j] = df_60min
|
|
254
|
+
|
|
255
|
+
# concatenate creating multiindex with keys of the list of point numbers
|
|
256
|
+
# assigned to 'point', reorder indices, and sort by valid_time
|
|
257
|
+
df_60min = (
|
|
258
|
+
pd.concat(dfs, keys=list(range(num_sites)), names=['point'])
|
|
259
|
+
.reorder_levels(["valid_time", "point"])
|
|
260
|
+
.sort_index(level='valid_time')
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# set "point" index as a column
|
|
264
|
+
df_60min = df_60min.reset_index().set_index('valid_time')
|
|
265
|
+
|
|
266
|
+
# drop unneeded columns if they exist
|
|
267
|
+
# df_60min = df_60min.drop(['t2m', 'sdswrf'], axis=1, errors='ignore')
|
|
268
|
+
|
|
269
|
+
return df_60min
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hefty
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Some (relatively) lightweight short-term energy forecasting tools for solar, wind, and load.
|
|
5
|
+
Author: Will Hobbs
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
Project-URL: Homepage, https://github.com/williamhobbs/hefty
|
|
8
|
+
Project-URL: Issues, https://github.com/williamhobbs/hefty/issues
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: herbie-data[extras]>=2025.11.2
|
|
18
|
+
Requires-Dist: pvlib>=0.13.1
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# hEFTy
|
|
22
|
+
Some (relatively) lightweight short-term **e**nergy **f**orecasting **t**ools for solar, wind, and load.
|
|
23
|
+
|
|
24
|
+
This repository currently includes solar and wind tools, but may expand one day to include electric load. Forecasts can be created using the NOAA GFS, NOAA GEFS, NOAA HRRR, and ECMWF IFS and AIFS (open data versions) models.
|
|
25
|
+
|
|
26
|
+
For solar, look at the notebook [solar_example.ipynb](examples/solar_example.ipynb) for some examples, and [more_solar_examples.ipynb](examples/more_solar_examples.ipynb) for more examples. Both of these convert the resource forecasts to power.
|
|
27
|
+
|
|
28
|
+
There are also solar ensemble forecasts demonstrated in [ensemble_example.ipynb](examples/ensemble_example.ipynb).
|
|
29
|
+
|
|
30
|
+
For wind, look at the notebook [wind_example.ipynb](examples/wind_example.ipynb). The wind tools are not as developed at the solar tools.
|
|
31
|
+
|
|
32
|
+
The [custom.py](src/hefty/custom.py) module is intended to help with getting forecasts of "custom" weather parameters, not necessarily specific to wind or solar, which migh be useful for load forecasting.
|
|
33
|
+
|
|
34
|
+
## Quick examples
|
|
35
|
+
|
|
36
|
+
Here's a quick example of getting a solar resource data forecast:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from hefty.solar import get_solar_forecast
|
|
40
|
+
|
|
41
|
+
latitude = 33.5
|
|
42
|
+
longitude = -86.8
|
|
43
|
+
init_date = '2024-06-05 6:00' # datetime the forecast model was initialized
|
|
44
|
+
resource_data = get_solar_forecast(
|
|
45
|
+
latitude,
|
|
46
|
+
longitude,
|
|
47
|
+
init_date,
|
|
48
|
+
run_length=18, # 18 hours are included in the forecast
|
|
49
|
+
lead_time_to_start=3, # forecast starts 3 hours out from the init_date
|
|
50
|
+
model='hrrr', # use NOAA HRRR
|
|
51
|
+
)
|
|
52
|
+
resource_data[
|
|
53
|
+
['ghi','dni','dhi','temp_air','wind_speed']
|
|
54
|
+
].plot(drawstyle='steps-mid')
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
with this output:
|
|
58
|
+
|
|
59
|
+
<img src="images/output.png" width="500"/>
|
|
60
|
+
|
|
61
|
+
Here's a wind resource forecast:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from hefty.wind import get_wind_forecast
|
|
65
|
+
|
|
66
|
+
latitude = 33.5
|
|
67
|
+
longitude = -86.8
|
|
68
|
+
init_date = '2024-06-05 6:00' # datetime the forecast model was initialized
|
|
69
|
+
resource_data = get_wind_forecast(
|
|
70
|
+
latitude,
|
|
71
|
+
longitude,
|
|
72
|
+
init_date,
|
|
73
|
+
run_length=18, # 18 hours are included in the forecast
|
|
74
|
+
lead_time_to_start=3, # forecast starts 3 hours out from the init_date
|
|
75
|
+
model='gfs', # use NOAA GFS
|
|
76
|
+
)
|
|
77
|
+
resource_data[
|
|
78
|
+
['wind_speed_10m', 'wind_speed_80m',
|
|
79
|
+
'wind_speed_100m', 'temp_air_2m',
|
|
80
|
+
'pressure_0m']
|
|
81
|
+
].plot(secondary_y=['pressure_0m'], drawstyle='steps-mid')
|
|
82
|
+
```
|
|
83
|
+
with this output (note that pressure is on the secondary y-axis):
|
|
84
|
+
|
|
85
|
+
<img src="images/output_wind.png" width="500"/>
|
|
86
|
+
|
|
87
|
+
## Installation
|
|
88
|
+
|
|
89
|
+
A virtual environment is strongly recommended. You can install from PyPi with:
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
pip install hefty
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
To run the example jupyter notebooks:
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
pip install jupyter
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## References
|
|
102
|
+
This project uses several Python packages, including pvlib, an open-source solar PV modeling package [1, 2], and Herbie [3, 4], a package for accessing weather forecast data from NOAA. `pv_model.py` (with the `model_pv_power()` function used here) comes from [5] which leverages some functions from [6].
|
|
103
|
+
|
|
104
|
+
<img src="images/pvlib_powered_logo_horiz.png" width="200"/>
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
[1] Anderson, K., Hansen, C., Holmgren, W., Jensen, A., Mikofski, M., and Driesse, A. “pvlib python: 2023 project update.” Journal of Open Source Software, 8(92), 5994, (2023). [DOI: 10.21105/joss.05994](http://dx.doi.org/10.21105/joss.05994).
|
|
108
|
+
|
|
109
|
+
[2] https://github.com/pvlib/pvlib-python
|
|
110
|
+
|
|
111
|
+
[3] Blaylock, B. K. (2025). Herbie: Retrieve Numerical Weather Prediction Model Data (Version 2025.3.1) [Computer software]. https://doi.org/10.5281/zenodo.4567540
|
|
112
|
+
|
|
113
|
+
[4] https://github.com/blaylockbk/Herbie
|
|
114
|
+
|
|
115
|
+
[5] https://github.com/williamhobbs/pv-system-model
|
|
116
|
+
|
|
117
|
+
[6] Hobbs, W., Anderson, K., Mikofski, M., and Ghiz, M. "An approach to modeling linear and non-linear self-shading losses with pvlib." 2024 PV Performance Modeling Collaborative (PVPMC). https://github.com/williamhobbs/2024_pvpmc_self_shade
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
hefty/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
hefty/custom.py,sha256=kiAJCepJCTfMSh05bHqPQ8nwV0qNek7Q4pE93gaVsmw,7787
|
|
3
|
+
hefty/pv_model.py,sha256=a6CjnXtuTBX9YRzW3RPHQnXB3YvVo5tDNo-UNYmi8xw,24946
|
|
4
|
+
hefty/solar.py,sha256=QLE7N4fvw7NfQI83ZtunZkOQgLGAm_EqEtESRR47i0A,71981
|
|
5
|
+
hefty/utilities.py,sha256=p-cJm3LFb8zx_w2eAXYSqJOL52vxJkCjmhG0hX5wZ6c,10475
|
|
6
|
+
hefty/wind.py,sha256=H05HeANWZU4A4SNZoTZOQDu_qglxdq9ajhWyyFfguHI,10358
|
|
7
|
+
hefty-0.0.2.dist-info/licenses/LICENSE,sha256=uhu6UMO7Y0MMk8CdVYEZ2qkKAhd7uqmC2A8HSZFCjpA,1525
|
|
8
|
+
hefty-0.0.2.dist-info/METADATA,sha256=fYALaE7lLDKjaMVikibWRTH67gxJ-xdAnvkcFxGntic,4695
|
|
9
|
+
hefty-0.0.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
10
|
+
hefty-0.0.2.dist-info/top_level.txt,sha256=IlYG3mUbbVkE-8qbgF93CunyWNsflRLZfGgDzMvG5sE,6
|
|
11
|
+
hefty-0.0.2.dist-info/RECORD,,
|