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/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,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+