hefty 0.1.6__tar.gz → 0.2.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hefty
3
- Version: 0.1.6
3
+ Version: 0.2.0
4
4
  Summary: Some (relatively) lightweight short-term energy forecasting tools for solar, wind, and load.
5
5
  Author: Will Hobbs
6
6
  License-Expression: BSD-3-Clause
@@ -29,13 +29,13 @@ This repository currently includes solar and wind tools, but may expand one day
29
29
  - NOAA HRRR
30
30
  - ECMWF IFS (open data version)
31
31
  - ECMWF AIFS
32
- - ECMWF CAMS* version of IFS (a.k.a. "IFS-COMPO")
32
+ - ECMWF CAMS version of IFS* (a.k.a. "IFS-COMPO")
33
33
 
34
34
  A detailed pvlib-based PV weather-to-power model is included in [pv_model.py](src/hefty/pv_model.py), `model_pv_power()`.
35
35
 
36
36
  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.
37
37
 
38
- _*CAMS IFS is only available via `hefty.solar.get_solar_forecast()`, and it requires `cdsapi` to be installed and the user needs an API key (see https://ads.atmosphere.copernicus.eu/how-to-api)._
38
+ _*CAMS IFS is only available via `hefty.solar.get_solar_forecast()`, and it requires `cdsapi` to be installed and the user needs an API key (see https://ads.atmosphere.copernicus.eu/how-to-api and the [CAMS example notebook](examples/cams_example.ipynb))._
39
39
 
40
40
  ## Contents
41
41
  - [Installation](#installation) - getting started
@@ -130,7 +130,8 @@ The [examples](examples) folder contains several example Jupyter notebooks.
130
130
  - [more_solar_examples.ipynb](examples/more_solar_examples.ipynb): Expanded examples, including multiple sites and multiple forecast initialization times.
131
131
  - [cams_example.ipynb](examples/cams_example.ipynb): Solar resource forecasting with CAMS IFS.
132
132
  - [ensemble_example.ipynb](examples/ensemble_example.ipynb): Examples of multi-member ensembles with IFS, AIFS, and GEFS.
133
- - [wind_example.ipynb](examples/wind_example.ipynb): A basic wind resource forecast example using a few models, including converting the data to a format compatible with `windpowerlib`*
133
+ - [wind_example.ipynb](examples/wind_example.ipynb): A basic wind resource forecast example using a few models, including converting the data to a format compatible with `windpowerlib`*.
134
+ - [erbs_vs_dirindex.ipynb](examples/erbs_vs_dirindex.ipynb): A comparison of Erbs and DIRINDEX irradiance decomposition models using SURFRAD measurements.
134
135
 
135
136
  \* https://github.com/wind-python/windpowerlib/
136
137
 
@@ -7,13 +7,13 @@ This repository currently includes solar and wind tools, but may expand one day
7
7
  - NOAA HRRR
8
8
  - ECMWF IFS (open data version)
9
9
  - ECMWF AIFS
10
- - ECMWF CAMS* version of IFS (a.k.a. "IFS-COMPO")
10
+ - ECMWF CAMS version of IFS* (a.k.a. "IFS-COMPO")
11
11
 
12
12
  A detailed pvlib-based PV weather-to-power model is included in [pv_model.py](src/hefty/pv_model.py), `model_pv_power()`.
13
13
 
14
14
  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.
15
15
 
16
- _*CAMS IFS is only available via `hefty.solar.get_solar_forecast()`, and it requires `cdsapi` to be installed and the user needs an API key (see https://ads.atmosphere.copernicus.eu/how-to-api)._
16
+ _*CAMS IFS is only available via `hefty.solar.get_solar_forecast()`, and it requires `cdsapi` to be installed and the user needs an API key (see https://ads.atmosphere.copernicus.eu/how-to-api and the [CAMS example notebook](examples/cams_example.ipynb))._
17
17
 
18
18
  ## Contents
19
19
  - [Installation](#installation) - getting started
@@ -108,7 +108,8 @@ The [examples](examples) folder contains several example Jupyter notebooks.
108
108
  - [more_solar_examples.ipynb](examples/more_solar_examples.ipynb): Expanded examples, including multiple sites and multiple forecast initialization times.
109
109
  - [cams_example.ipynb](examples/cams_example.ipynb): Solar resource forecasting with CAMS IFS.
110
110
  - [ensemble_example.ipynb](examples/ensemble_example.ipynb): Examples of multi-member ensembles with IFS, AIFS, and GEFS.
111
- - [wind_example.ipynb](examples/wind_example.ipynb): A basic wind resource forecast example using a few models, including converting the data to a format compatible with `windpowerlib`*
111
+ - [wind_example.ipynb](examples/wind_example.ipynb): A basic wind resource forecast example using a few models, including converting the data to a format compatible with `windpowerlib`*.
112
+ - [erbs_vs_dirindex.ipynb](examples/erbs_vs_dirindex.ipynb): A comparison of Erbs and DIRINDEX irradiance decomposition models using SURFRAD measurements.
112
113
 
113
114
  \* https://github.com/wind-python/windpowerlib/
114
115
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "hefty"
7
- version = "0.1.6"
7
+ version = "0.2.0"
8
8
  description = "Some (relatively) lightweight short-term energy forecasting tools for solar, wind, and load."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -20,7 +20,8 @@ def get_solar_forecast(latitude, longitude, init_date, run_length,
20
20
  lead_time_to_start=0, model='gfs', member='avg',
21
21
  attempts=2, hrrr_hour_middle=True,
22
22
  hrrr_coursen_window=None, priority=None,
23
- cams_api_key=None, cams_area=None):
23
+ cams_api_key=None, cams_area=None,
24
+ decomp_model='dirindex'):
24
25
  """
25
26
  Get a solar resource forecast for one or several sites from one of several
26
27
  NWPs. This function uses Herbie [1]_ and pvlib [2]_.
@@ -96,6 +97,11 @@ def get_solar_forecast(latitude, longitude, init_date, run_length,
96
97
  South, and East corners of the area to be covered when using 'cams'.
97
98
  For example, [50, -125, 20, -65] approximately covers CONUS.
98
99
 
100
+ decomp_model : string, optional
101
+ Decomposition model to use for forecast models that don't include DNI.
102
+ Options include 'dirindex' and 'erbs'. Note that HRRR and CAMS include
103
+ DNI, so this parameter does not apply to those models.
104
+
99
105
  Returns
100
106
  -------
101
107
  data : pandas.DataFrane
@@ -113,7 +119,7 @@ def get_solar_forecast(latitude, longitude, init_date, run_length,
113
119
  """
114
120
 
115
121
  # set clear sky model. could be an input variable at some point
116
- model_cs = 'simplified_solis'
122
+ # model_cs = 'simplified_solis'
117
123
  model_cs_kwargs = {
118
124
  'aod700': 0.05,
119
125
  'precipitable_water': 0.5,
@@ -144,6 +150,11 @@ def get_solar_forecast(latitude, longitude, init_date, run_length,
144
150
 
145
151
  # get NWP data as dataframe
146
152
  if model != 'cams':
153
+ delimiter = '|'
154
+ search_string_list = search_str.split(delimiter)
155
+ num_datasets = len(search_string_list)
156
+ if model == 'hrrr':
157
+ num_datasets -= 1 # DNI and GHI will show up in a single dataset
147
158
  i = []
148
159
  for fxx in fxx_range:
149
160
  # get solar, 10m wind, and 2m temp data
@@ -162,6 +173,13 @@ def get_solar_forecast(latitude, longitude, init_date, run_length,
162
173
  member=member,
163
174
  priority=priority
164
175
  ).xarray(search_str)
176
+ # address GH#77
177
+ if len(ds) < num_datasets:
178
+ msg = ('Parameters appear to be '
179
+ 'missing. Another download'
180
+ ' will be attempted if there are attempts'
181
+ ' remaining.')
182
+ raise ValueError(msg)
165
183
  # merge - override avoids height conflict between 2m
166
184
  # temp and 10m wind
167
185
  ds = xr.merge(ds, compat='override')
@@ -176,6 +194,13 @@ def get_solar_forecast(latitude, longitude, init_date, run_length,
176
194
  member=member,
177
195
  priority=priority
178
196
  ).xarray(search_str, overwrite=True)
197
+ # address GH#77
198
+ if len(ds) < num_datasets:
199
+ msg = ('Parameters appear to be '
200
+ 'missing. Another download'
201
+ ' will be attempted if there are attempts'
202
+ ' remaining.')
203
+ raise ValueError(msg)
179
204
  # merge - override avoids height conflict between 2m
180
205
  # temp and 10m wind
181
206
  ds = xr.merge(ds, compat='override')
@@ -407,7 +432,16 @@ def get_solar_forecast(latitude, longitude, init_date, run_length,
407
432
  tz='UTC')
408
433
 
409
434
  # calculate clear sky ghi with pvlib
410
- cs = loc.get_clearsky(times, model=model_cs, **model_cs_kwargs)
435
+ sp = pvlib.solarposition.ephemeris(
436
+ times,
437
+ loc.latitude,
438
+ loc.longitude
439
+ )
440
+ apparent_elevation = sp['apparent_elevation']
441
+ cs = pvlib.clearsky.simplified_solis(
442
+ apparent_elevation,
443
+ **model_cs_kwargs
444
+ )
411
445
 
412
446
  # calculate average CS ghi over the intervals from the forecast
413
447
  # based on list comprehension example in
@@ -458,7 +492,18 @@ def get_solar_forecast(latitude, longitude, init_date, run_length,
458
492
  end=df.index[-1]-pd.Timedelta('30m'),
459
493
  freq='60min',
460
494
  tz='UTC')
461
- cs = loc.get_clearsky(times, model=model_cs, **model_cs_kwargs)
495
+
496
+ # calculate clear sky ghi with pvlib
497
+ sp = pvlib.solarposition.ephemeris(
498
+ times,
499
+ loc.latitude,
500
+ loc.longitude
501
+ )
502
+ apparent_elevation = sp['apparent_elevation']
503
+ cs = pvlib.clearsky.simplified_solis(
504
+ apparent_elevation,
505
+ **model_cs_kwargs
506
+ )
462
507
 
463
508
  # calculate ghi from clear sky and backfilled forecasted clear sky
464
509
  # index
@@ -467,16 +512,31 @@ def get_solar_forecast(latitude, longitude, init_date, run_length,
467
512
  # clip to avoid occasional small negative ghi in GEFS, see GH #35
468
513
  df_60min['ghi'] = df_60min['ghi'].clip(lower=0)
469
514
 
470
- # dni and dhi using pvlib erbs. could also DIRINT or
471
- # erbs-driesse
472
- sp = loc.get_solarposition(times)
473
- out_erbs = pvlib.irradiance.erbs(
474
- df_60min['ghi'],
475
- sp['zenith'],
476
- df_60min.index,
477
- )
478
- df_60min['dni'] = out_erbs['dni']
479
- df_60min['dhi'] = out_erbs['dhi']
515
+ # decompose
516
+ if decomp_model == 'erbs':
517
+ out_erbs = pvlib.irradiance.erbs(
518
+ df_60min['ghi'],
519
+ sp['zenith'],
520
+ df_60min.index,
521
+ )
522
+ df_60min['dni'] = out_erbs['dni']
523
+ df_60min['dhi'] = out_erbs['dhi']
524
+ elif decomp_model == 'dirindex':
525
+ cs_decomp = cs # could later use a different cs model...
526
+ decomp = pvlib.irradiance.dirindex(
527
+ ghi=df_60min['ghi'],
528
+ ghi_clear=cs_decomp['ghi'],
529
+ dni_clear=cs_decomp['dni'],
530
+ zenith=sp['zenith'],
531
+ times=times,
532
+ )
533
+ df_60min['dni'] = decomp.fillna(0)
534
+ cos_zenith = np.maximum(
535
+ np.cos(np.deg2rad(sp['apparent_zenith'])),
536
+ min_cos_zenith
537
+ )
538
+ df_60min['dhi'] = (df_60min['ghi'] -
539
+ (df_60min['dni'] * cos_zenith))
480
540
 
481
541
  # add clearsky ghi
482
542
  df_60min['ghi_clear'] = df_60min['ghi'] / df_60min['ghi_csi']
@@ -500,7 +560,11 @@ def get_solar_forecast(latitude, longitude, init_date, run_length,
500
560
  df.index = df.index - pd.Timedelta('30min')
501
561
 
502
562
  # direct horiz clear to dni_clear
503
- sp = loc.get_solarposition(df.index)
563
+ sp = pvlib.solarposition.ephemeris(
564
+ df.index,
565
+ loc.latitude,
566
+ loc.longitude
567
+ )
504
568
  cos_zenith = np.maximum(np.cos(np.deg2rad(sp['apparent_zenith'])),
505
569
  min_cos_zenith)
506
570
  df['dni_clear'] = (df['direct_horiz_clear'] / cos_zenith)
@@ -523,8 +587,16 @@ def get_solar_forecast(latitude, longitude, init_date, run_length,
523
587
  if hrrr_hour_middle is True:
524
588
  # clear sky index
525
589
  times = df.index
526
- cs = loc.get_clearsky(times, model=model_cs,
527
- **model_cs_kwargs)
590
+ sp = pvlib.solarposition.ephemeris(
591
+ times,
592
+ loc.latitude,
593
+ loc.longitude
594
+ )
595
+ apparent_elevation = sp['apparent_elevation']
596
+ cs = pvlib.clearsky.simplified_solis(
597
+ apparent_elevation,
598
+ **model_cs_kwargs
599
+ )
528
600
  df['csi_ghi'] = df['ghi'] / cs['ghi']
529
601
  df['csi_dni'] = df['dni'] / cs['dni']
530
602
  # avoid divide by zero issues
@@ -538,8 +610,18 @@ def get_solar_forecast(latitude, longitude, init_date, run_length,
538
610
  freq='1min',
539
611
  tz='UTC')
540
612
 
541
- cs = loc.get_clearsky(times, model=model_cs,
542
- **model_cs_kwargs)
613
+ # calculate clear sky ghi with pvlib
614
+ sp = pvlib.solarposition.ephemeris(
615
+ times,
616
+ loc.latitude,
617
+ loc.longitude
618
+ )
619
+ apparent_elevation = sp['apparent_elevation']
620
+ cs = pvlib.clearsky.simplified_solis(
621
+ apparent_elevation,
622
+ **model_cs_kwargs
623
+ )
624
+
543
625
  # calculate 1min interpolated temp_air, wind_speed, csi
544
626
  df_01min = (
545
627
  df[['temp_air', 'wind_speed', 'csi_ghi', 'csi_dni']]
@@ -561,15 +643,22 @@ def get_solar_forecast(latitude, longitude, init_date, run_length,
561
643
  df_60min = df.copy()
562
644
 
563
645
  # calculate dhi from ghi, dni, solar position
564
- sp = loc.get_solarposition(df_60min.index)
646
+ sp = pvlib.solarposition.ephemeris(
647
+ df_60min.index,
648
+ loc.latitude,
649
+ loc.longitude
650
+ )
565
651
  cos_zenith = np.maximum(np.cos(np.deg2rad(sp['apparent_zenith'])),
566
652
  min_cos_zenith)
567
653
  df_60min['dhi'] = (df_60min['ghi'] -
568
654
  (df_60min['dni'] * cos_zenith))
569
655
 
570
656
  # add clearsky ghi
571
- cs = loc.get_clearsky(df_60min.index, model=model_cs,
572
- **model_cs_kwargs)
657
+ apparent_elevation = sp['apparent_elevation']
658
+ cs = pvlib.clearsky.simplified_solis(
659
+ apparent_elevation,
660
+ **model_cs_kwargs
661
+ )
573
662
  df_60min['ghi_clear'] = cs['ghi']
574
663
  df_60min['dni_clear'] = cs['dni']
575
664
 
@@ -595,7 +684,8 @@ def get_solar_forecast(latitude, longitude, init_date, run_length,
595
684
  def get_solar_forecast_fast(latitude, longitude, init_date, run_length,
596
685
  lead_time_to_start=0, model='gfs', member='avg',
597
686
  attempts=2, hrrr_hour_middle=True,
598
- hrrr_coursen_window=None, priority=None):
687
+ hrrr_coursen_window=None, priority=None,
688
+ decomp_model='dirindex'):
599
689
  """
600
690
  Get a solar resource forecast for one or several sites from one of several
601
691
  NWPs. This function uses Herbie [1]_ and pvlib [2]_. This version
@@ -665,6 +755,11 @@ def get_solar_forecast_fast(latitude, longitude, init_date, run_length,
665
755
  priority, or string for a single source. See Herbie docs.
666
756
  Typical values would be 'aws' or 'google'.
667
757
 
758
+ decomp_model : string, optional
759
+ Decomposition model to use for forecast models that don't include DNI.
760
+ Options include 'dirindex' and 'erbs'. Note that HRRR and CAMS include
761
+ DNI, so this parameter does not apply to those models.
762
+
668
763
  Returns
669
764
  -------
670
765
  data : pandas.DataFrane
@@ -730,6 +825,10 @@ def get_solar_forecast_fast(latitude, longitude, init_date, run_length,
730
825
  FH.download(search_string_list[j])
731
826
  ds_dict[j] = FH.xarray(search_string_list[j],
732
827
  remove_grib=True)
828
+ # calculate wind speed from u and v components if relevant
829
+ if ('uv' in search_string_list[j] or
830
+ 'UV' in search_string_list[j]):
831
+ ds_dict[j] = ds_dict[j].herbie.with_wind('speed')
733
832
  # merge - override avoids height conflict between 2m temp
734
833
  # and 10m wind
735
834
  ds = xr.merge(ds_dict.values(), compat='override')
@@ -740,6 +839,10 @@ def get_solar_forecast_fast(latitude, longitude, init_date, run_length,
740
839
  ds_dict[j] = FH.xarray(search_string_list[j],
741
840
  remove_grib=True,
742
841
  overwrite=True)
842
+ # calculate wind speed from u and v components if relevant
843
+ if ('uv' in search_string_list[j] or
844
+ 'UV' in search_string_list[j]):
845
+ ds_dict[j] = ds_dict[j].herbie.with_wind('speed')
743
846
  # merge - override avoids height conflict between 2m temp
744
847
  # and 10m wind
745
848
  ds = xr.merge(ds_dict.values(), compat='override')
@@ -755,9 +858,6 @@ def get_solar_forecast_fast(latitude, longitude, init_date, run_length,
755
858
  else:
756
859
  break
757
860
 
758
- # calculate wind speed from u and v components
759
- ds = ds.herbie.with_wind('speed')
760
-
761
861
  if model == 'hrrr' and hrrr_coursen_window is not None:
762
862
  ds = ds.coarsen(x=hrrr_coursen_window,
763
863
  y=hrrr_coursen_window,
@@ -849,7 +949,17 @@ def get_solar_forecast_fast(latitude, longitude, init_date, run_length,
849
949
  freq='1min',
850
950
  tz='UTC')
851
951
 
852
- cs = loc.get_clearsky(times, model=model_cs, **model_cs_kwargs)
952
+ # calculate clear sky ghi with pvlib
953
+ sp = pvlib.solarposition.ephemeris(
954
+ times,
955
+ loc.latitude,
956
+ loc.longitude
957
+ )
958
+ apparent_elevation = sp['apparent_elevation']
959
+ cs = pvlib.clearsky.simplified_solis(
960
+ apparent_elevation,
961
+ **model_cs_kwargs
962
+ )
853
963
 
854
964
  # calculate average CS ghi over the intervals from the forecast
855
965
  # based on list comprehension example in
@@ -898,7 +1008,18 @@ def get_solar_forecast_fast(latitude, longitude, init_date, run_length,
898
1008
  end=df.index[-1]-pd.Timedelta('30m'),
899
1009
  freq='60min',
900
1010
  tz='UTC')
901
- cs = loc.get_clearsky(times, model=model_cs, **model_cs_kwargs)
1011
+
1012
+ # calculate clear sky ghi with pvlib
1013
+ sp = pvlib.solarposition.ephemeris(
1014
+ times,
1015
+ loc.latitude,
1016
+ loc.longitude
1017
+ )
1018
+ apparent_elevation = sp['apparent_elevation']
1019
+ cs = pvlib.clearsky.simplified_solis(
1020
+ apparent_elevation,
1021
+ **model_cs_kwargs
1022
+ )
902
1023
 
903
1024
  # calculate ghi from clear sky and backfilled forecasted clear sky
904
1025
  # index
@@ -907,15 +1028,31 @@ def get_solar_forecast_fast(latitude, longitude, init_date, run_length,
907
1028
  # clip to avoid occasional small negative ghi in GEFS, see GH #35
908
1029
  df_60min['ghi'] = df_60min['ghi'].clip(lower=0)
909
1030
 
910
- # dni and dhi using pvlib erbs. could also DIRINT or erbs-driesse
911
- sp = loc.get_solarposition(times)
912
- out_erbs = pvlib.irradiance.erbs(
913
- df_60min['ghi'],
914
- sp['zenith'],
915
- df_60min.index,
916
- )
917
- df_60min['dni'] = out_erbs['dni']
918
- df_60min['dhi'] = out_erbs['dhi']
1031
+ # decompose
1032
+ if decomp_model == 'erbs':
1033
+ out_erbs = pvlib.irradiance.erbs(
1034
+ df_60min['ghi'],
1035
+ sp['zenith'],
1036
+ df_60min.index,
1037
+ )
1038
+ df_60min['dni'] = out_erbs['dni']
1039
+ df_60min['dhi'] = out_erbs['dhi']
1040
+ elif decomp_model == 'dirindex':
1041
+ cs_decomp = cs # could later use a different cs model...
1042
+ decomp = pvlib.irradiance.dirindex(
1043
+ ghi=df_60min['ghi'],
1044
+ ghi_clear=cs_decomp['ghi'],
1045
+ dni_clear=cs_decomp['dni'],
1046
+ zenith=sp['zenith'],
1047
+ times=times,
1048
+ )
1049
+ df_60min['dni'] = decomp.fillna(0)
1050
+ cos_zenith = np.maximum(
1051
+ np.cos(np.deg2rad(sp['apparent_zenith'])),
1052
+ min_cos_zenith
1053
+ )
1054
+ df_60min['dhi'] = (df_60min['ghi'] -
1055
+ (df_60min['dni'] * cos_zenith))
919
1056
 
920
1057
  # add clearsky ghi
921
1058
  df_60min['ghi_clear'] = df_60min['ghi'] / df_60min['ghi_csi']
@@ -941,8 +1078,18 @@ def get_solar_forecast_fast(latitude, longitude, init_date, run_length,
941
1078
  freq='1min',
942
1079
  tz='UTC')
943
1080
 
944
- cs = loc.get_clearsky(times, model=model_cs,
945
- **model_cs_kwargs)
1081
+ # calculate clear sky ghi with pvlib
1082
+ sp = pvlib.solarposition.ephemeris(
1083
+ times,
1084
+ loc.latitude,
1085
+ loc.longitude
1086
+ )
1087
+ apparent_elevation = sp['apparent_elevation']
1088
+ cs = pvlib.clearsky.simplified_solis(
1089
+ apparent_elevation,
1090
+ **model_cs_kwargs
1091
+ )
1092
+
946
1093
  # calculate 1min interpolated temp_air, wind_speed, csi
947
1094
  df_01min = (
948
1095
  df[['temp_air', 'wind_speed', 'csi_ghi', 'csi_dni']]
@@ -964,15 +1111,22 @@ def get_solar_forecast_fast(latitude, longitude, init_date, run_length,
964
1111
  df_60min = df.copy()
965
1112
 
966
1113
  # calculate dhi from ghi, dni, solar position
967
- sp = loc.get_solarposition(df_60min.index)
1114
+ sp = pvlib.solarposition.ephemeris(
1115
+ df_60min.index,
1116
+ loc.latitude,
1117
+ loc.longitude
1118
+ )
968
1119
  cos_zenith = np.maximum(np.cos(np.deg2rad(sp['apparent_zenith'])),
969
1120
  min_cos_zenith)
970
1121
  df_60min['dhi'] = (df_60min['ghi'] -
971
1122
  (df_60min['dni'] * cos_zenith))
972
1123
 
973
1124
  # add clearsky ghi
974
- cs = loc.get_clearsky(df_60min.index, model=model_cs,
975
- **model_cs_kwargs)
1125
+ apparent_elevation = sp['apparent_elevation']
1126
+ cs = pvlib.clearsky.simplified_solis(
1127
+ apparent_elevation,
1128
+ **model_cs_kwargs
1129
+ )
976
1130
  df_60min['ghi_clear'] = cs['ghi']
977
1131
  df_60min['dni_clear'] = cs['dni']
978
1132
 
@@ -997,7 +1151,8 @@ def get_solar_forecast_fast(latitude, longitude, init_date, run_length,
997
1151
 
998
1152
  def get_solar_forecast_ensemble_subset(
999
1153
  latitude, longitude, init_date, run_length, lead_time_to_start=0,
1000
- model='ifs_ens', attempts=2, num_members=3, priority=None):
1154
+ model='ifs_ens', attempts=2, num_members=3, priority=None,
1155
+ decomp_model='dirindex'):
1001
1156
  """
1002
1157
  Get solar resource forecasts for one or several sites using a subset of
1003
1158
  ensemble members. Use `get_solar_forecast_ensemble` for all ensemble
@@ -1048,6 +1203,9 @@ def get_solar_forecast_ensemble_subset(
1048
1203
  priority, or string for a single source. See Herbie docs.
1049
1204
  Typical values would be 'aws' or 'google'.
1050
1205
 
1206
+ decomp_model : string, optional
1207
+ Decomposition model to use. Options include 'dirindex' and 'erbs'.
1208
+
1051
1209
  Returns
1052
1210
  -------
1053
1211
  data : pandas.DataFrane
@@ -1065,11 +1223,14 @@ def get_solar_forecast_ensemble_subset(
1065
1223
  """
1066
1224
 
1067
1225
  # set clear sky model. could be an input variable at some point
1068
- model_cs = 'simplified_solis'
1226
+ # model_cs = 'simplified_solis'
1069
1227
  model_cs_kwargs = {
1070
1228
  'aod700': 0.05,
1071
1229
  'precipitable_water': 0.5,
1072
1230
  }
1231
+ # minimum cosine of zenith, same default used in pvlib.irradiance
1232
+ # functions. Could be an input variable at some point
1233
+ min_cos_zenith = 0.065
1073
1234
 
1074
1235
  # check model
1075
1236
  if model.casefold() != ('ifs_ens').casefold():
@@ -1170,7 +1331,18 @@ def get_solar_forecast_ensemble_subset(
1170
1331
  end=df.index[-1],
1171
1332
  freq='1min',
1172
1333
  tz='UTC')
1173
- cs = loc.get_clearsky(times, model=model_cs, **model_cs_kwargs)
1334
+
1335
+ # calculate clear sky ghi with pvlib
1336
+ sp = pvlib.solarposition.ephemeris(
1337
+ times,
1338
+ loc.latitude,
1339
+ loc.longitude
1340
+ )
1341
+ apparent_elevation = sp['apparent_elevation']
1342
+ cs = pvlib.clearsky.simplified_solis(
1343
+ apparent_elevation,
1344
+ **model_cs_kwargs
1345
+ )
1174
1346
 
1175
1347
  # calculate average CS ghi over the intervals from the forecast
1176
1348
  # based on list comprehension example in
@@ -1222,7 +1394,18 @@ def get_solar_forecast_ensemble_subset(
1222
1394
  end=df.index[-1]-pd.Timedelta('30m'),
1223
1395
  freq='60min',
1224
1396
  tz='UTC')
1225
- cs = loc.get_clearsky(times, model=model_cs, **model_cs_kwargs)
1397
+
1398
+ # calculate clear sky ghi with pvlib
1399
+ sp = pvlib.solarposition.ephemeris(
1400
+ times,
1401
+ loc.latitude,
1402
+ loc.longitude
1403
+ )
1404
+ apparent_elevation = sp['apparent_elevation']
1405
+ cs = pvlib.clearsky.simplified_solis(
1406
+ apparent_elevation,
1407
+ **model_cs_kwargs
1408
+ )
1226
1409
 
1227
1410
  # calculate ghi from clear sky and backfilled forecasted clear sky
1228
1411
  # index
@@ -1231,15 +1414,31 @@ def get_solar_forecast_ensemble_subset(
1231
1414
  # clip to avoid occasional small negative ghi in GEFS, see GH #35
1232
1415
  df_60min['ghi'] = df_60min['ghi'].clip(lower=0)
1233
1416
 
1234
- # dni and dhi using pvlib erbs. could also DIRINT or erbs-driesse
1235
- sp = loc.get_solarposition(times)
1236
- out_erbs = pvlib.irradiance.erbs(
1237
- df_60min['ghi'],
1238
- sp['zenith'],
1239
- df_60min.index,
1240
- )
1241
- df_60min['dni'] = out_erbs['dni']
1242
- df_60min['dhi'] = out_erbs['dhi']
1417
+ # decompose
1418
+ if decomp_model == 'erbs':
1419
+ out_erbs = pvlib.irradiance.erbs(
1420
+ df_60min['ghi'],
1421
+ sp['zenith'],
1422
+ df_60min.index,
1423
+ )
1424
+ df_60min['dni'] = out_erbs['dni']
1425
+ df_60min['dhi'] = out_erbs['dhi']
1426
+ elif decomp_model == 'dirindex':
1427
+ cs_decomp = cs # could later use a different cs model...
1428
+ decomp = pvlib.irradiance.dirindex(
1429
+ ghi=df_60min['ghi'],
1430
+ ghi_clear=cs_decomp['ghi'],
1431
+ dni_clear=cs_decomp['dni'],
1432
+ zenith=sp['zenith'],
1433
+ times=times,
1434
+ )
1435
+ df_60min['dni'] = decomp.fillna(0)
1436
+ cos_zenith = np.maximum(
1437
+ np.cos(np.deg2rad(sp['apparent_zenith'])),
1438
+ min_cos_zenith
1439
+ )
1440
+ df_60min['dhi'] = (df_60min['ghi'] -
1441
+ (df_60min['dni'] * cos_zenith))
1243
1442
 
1244
1443
  # add clearsky ghi
1245
1444
  df_60min['ghi_clear'] = df_60min['ghi'] / df_60min['ghi_csi']
@@ -1368,7 +1567,8 @@ def get_solar_forecast_ensemble_subset(
1368
1567
  def get_solar_forecast_ensemble(latitude, longitude, init_date, run_length,
1369
1568
  lead_time_to_start=0, model='ifs_ens',
1370
1569
  attempts=2, priority=None,
1371
- get_ens_temp=False, get_ens_wind=False):
1570
+ get_ens_temp=False, get_ens_wind=False,
1571
+ decomp_model='dirindex'):
1372
1572
  """
1373
1573
  Get solar resource forecasts for one or several sites using all ensemble
1374
1574
  members. Using `get_solar_forecast_ensemble_subset` may be fast for a
@@ -1433,6 +1633,9 @@ def get_solar_forecast_ensemble(latitude, longitude, init_date, run_length,
1433
1633
  Get wind speed from each ensemble member if `True`. Otherwise, if
1434
1634
  `False` (default), wind speed is a generic 2 m/s value to save time.
1435
1635
 
1636
+ decomp_model : string, optional
1637
+ Decomposition model to use. Options include 'dirindex' and 'erbs'.
1638
+
1436
1639
  Returns
1437
1640
  -------
1438
1641
  data : pandas.DataFrane
@@ -1450,11 +1653,14 @@ def get_solar_forecast_ensemble(latitude, longitude, init_date, run_length,
1450
1653
  """
1451
1654
 
1452
1655
  # set clear sky model. could be an input variable at some point
1453
- model_cs = 'simplified_solis'
1656
+ # model_cs = 'simplified_solis'
1454
1657
  model_cs_kwargs = {
1455
1658
  'aod700': 0.05,
1456
1659
  'precipitable_water': 0.5,
1457
1660
  }
1661
+ # minimum cosine of zenith, same default used in pvlib.irradiance
1662
+ # functions. Could be an input variable at some point
1663
+ min_cos_zenith = 0.065
1458
1664
 
1459
1665
  # check model
1460
1666
  if (
@@ -1640,34 +1846,68 @@ def get_solar_forecast_ensemble(latitude, longitude, init_date, run_length,
1640
1846
  # work through sites (points) and members
1641
1847
  member_list = df_temp['number'].unique()
1642
1848
  dfs = []
1643
- for number in member_list:
1644
- for point in range(num_sites):
1645
- df = df_temp[(df_temp['point'] == point) &
1646
- (df_temp['number'] == number)].copy()
1849
+ for point in range(num_sites):
1850
+ # set of timestamps for solar position and clear sky
1851
+ df = df_temp[(df_temp['point'] == point) &
1852
+ (df_temp['number'] == member_list[0])].copy()
1647
1853
 
1648
- loc = pvlib.location.Location(
1854
+ # location
1855
+ loc = pvlib.location.Location(
1649
1856
  latitude=latitude[point],
1650
1857
  longitude=longitude[point],
1651
1858
  tz=df.index.tz
1652
1859
  )
1653
1860
 
1861
+ # make 1min interval clear sky data covering our time range
1862
+ times_1min = pd.date_range(
1863
+ start=df.index[0],
1864
+ end=df.index[-1],
1865
+ freq='1min',
1866
+ tz='UTC')
1867
+
1868
+ # calculate clear sky ghi with pvlib
1869
+ sp_1min = pvlib.solarposition.ephemeris(
1870
+ times_1min,
1871
+ loc.latitude,
1872
+ loc.longitude
1873
+ )
1874
+ apparent_elevation_1min = sp_1min['apparent_elevation']
1875
+ cs_1min = pvlib.clearsky.simplified_solis(
1876
+ apparent_elevation_1min,
1877
+ **model_cs_kwargs
1878
+ )
1879
+
1880
+ # make 60min interval clear sky, centered at bottom of the hour
1881
+ times_60min = pd.date_range(
1882
+ start=df.index[0]+pd.Timedelta('30m'),
1883
+ end=df.index[-1]-pd.Timedelta('30m'),
1884
+ freq='60min',
1885
+ tz='UTC')
1886
+
1887
+ # calculate clear sky ghi with pvlib
1888
+ sp_60min = pvlib.solarposition.ephemeris(
1889
+ times_60min,
1890
+ loc.latitude,
1891
+ loc.longitude
1892
+ )
1893
+ apparent_elevation_60min = sp_60min['apparent_elevation']
1894
+ cs_60min = pvlib.clearsky.simplified_solis(
1895
+ apparent_elevation_60min,
1896
+ **model_cs_kwargs
1897
+ )
1898
+
1899
+ for number in member_list:
1900
+ df = df_temp[(df_temp['point'] == point) &
1901
+ (df_temp['number'] == number)].copy()
1902
+
1654
1903
  # convert cumulative J/m^s to average W/m^2
1655
1904
  df['ghi'] = (df['sdswrf'].diff() /
1656
1905
  df.index.diff().seconds.values)
1657
1906
 
1658
- # make 1min interval clear sky data covering our time range
1659
- times = pd.date_range(
1660
- start=df.index[0],
1661
- end=df.index[-1],
1662
- freq='1min',
1663
- tz='UTC')
1664
- cs = loc.get_clearsky(times, model=model_cs,
1665
- **model_cs_kwargs)
1666
-
1667
1907
  # calculate average CS ghi over the intervals from the forecast
1668
1908
  # based on list comprehension example in
1669
1909
  # https://stackoverflow.com/a/55724134/27574852
1670
- ghi = cs['ghi']
1910
+ ghi = cs_1min['ghi']
1671
1911
  dates = df.index
1672
1912
  ghi_clear = [
1673
1913
  ghi.loc[(ghi.index > dates[i]) & (ghi.index <= dates[i+1])]
@@ -1722,29 +1962,35 @@ def get_solar_forecast_ensemble(latitude, longitude, init_date, run_length,
1722
1962
  direction='forward'
1723
1963
  ).set_index('valid_time')
1724
1964
 
1725
- # make 60min interval clear sky, centered at bottom of the hour
1726
- times = pd.date_range(
1727
- start=df.index[0]+pd.Timedelta('30m'),
1728
- end=df.index[-1]-pd.Timedelta('30m'),
1729
- freq='60min',
1730
- tz='UTC')
1731
- cs = loc.get_clearsky(times, model=model_cs,
1732
- **model_cs_kwargs)
1733
-
1734
1965
  # calculate ghi from clear sky and backfilled forecasted clear
1735
1966
  # sky index
1736
- df_60min['ghi'] = cs['ghi'] * df_60min['ghi_csi']
1737
-
1738
- # dni and dhi using pvlib erbs. could also DIRINT or
1739
- # erbs-driesse
1740
- sp = loc.get_solarposition(times)
1741
- out_erbs = pvlib.irradiance.erbs(
1742
- df_60min['ghi'],
1743
- sp['zenith'],
1744
- df_60min.index,
1745
- )
1746
- df_60min['dni'] = out_erbs['dni']
1747
- df_60min['dhi'] = out_erbs['dhi']
1967
+ df_60min['ghi'] = cs_60min['ghi'] * df_60min['ghi_csi']
1968
+
1969
+ # decompose
1970
+ if decomp_model == 'erbs':
1971
+ out_erbs = pvlib.irradiance.erbs(
1972
+ df_60min['ghi'],
1973
+ sp_60min['zenith'],
1974
+ df_60min.index,
1975
+ )
1976
+ df_60min['dni'] = out_erbs['dni']
1977
+ df_60min['dhi'] = out_erbs['dhi']
1978
+ elif decomp_model == 'dirindex':
1979
+ cs_decomp = cs_60min # could later use another cs model
1980
+ decomp = pvlib.irradiance.dirindex(
1981
+ ghi=df_60min['ghi'],
1982
+ ghi_clear=cs_decomp['ghi'],
1983
+ dni_clear=cs_decomp['dni'],
1984
+ zenith=sp_60min['zenith'],
1985
+ times=times_60min,
1986
+ )
1987
+ df_60min['dni'] = decomp.fillna(0)
1988
+ cos_zenith = np.maximum(
1989
+ np.cos(np.deg2rad(sp_60min['apparent_zenith'])),
1990
+ min_cos_zenith
1991
+ )
1992
+ df_60min['dhi'] = (df_60min['ghi'] -
1993
+ (df_60min['dni'] * cos_zenith))
1748
1994
 
1749
1995
  # add clearsky ghi
1750
1996
  df_60min['ghi_clear'] = df_60min['ghi'] / df_60min['ghi_csi']
@@ -2026,8 +2272,17 @@ def get_solar_forecast_ensemble(latitude, longitude, init_date, run_length,
2026
2272
  freq='1min',
2027
2273
  tz='UTC')
2028
2274
 
2029
- cs = loc.get_clearsky(times, model=model_cs,
2030
- **model_cs_kwargs)
2275
+ # calculate clear sky ghi with pvlib
2276
+ sp = pvlib.solarposition.ephemeris(
2277
+ times,
2278
+ loc.latitude,
2279
+ loc.longitude
2280
+ )
2281
+ apparent_elevation = sp['apparent_elevation']
2282
+ cs = pvlib.clearsky.simplified_solis(
2283
+ apparent_elevation,
2284
+ **model_cs_kwargs
2285
+ )
2031
2286
 
2032
2287
  # calculate average CS ghi over the intervals from the forecast
2033
2288
  # based on list comprehension example in
@@ -2093,8 +2348,18 @@ def get_solar_forecast_ensemble(latitude, longitude, init_date, run_length,
2093
2348
  end=df.index[-1]-pd.Timedelta('30m'),
2094
2349
  freq='60min',
2095
2350
  tz='UTC')
2096
- cs = loc.get_clearsky(times, model=model_cs,
2097
- **model_cs_kwargs)
2351
+
2352
+ # calculate clear sky ghi with pvlib
2353
+ sp = pvlib.solarposition.ephemeris(
2354
+ times,
2355
+ loc.latitude,
2356
+ loc.longitude
2357
+ )
2358
+ apparent_elevation = sp['apparent_elevation']
2359
+ cs = pvlib.clearsky.simplified_solis(
2360
+ apparent_elevation,
2361
+ **model_cs_kwargs
2362
+ )
2098
2363
 
2099
2364
  # calculate ghi from clear sky and backfilled forecasted clear
2100
2365
  # sky index
@@ -2103,16 +2368,31 @@ def get_solar_forecast_ensemble(latitude, longitude, init_date, run_length,
2103
2368
  # clip to avoid occasional small negative ghi in GEFS, GH #35
2104
2369
  df_60min['ghi'] = df_60min['ghi'].clip(lower=0)
2105
2370
 
2106
- # dni and dhi using pvlib erbs. could also DIRINT or
2107
- # erbs-driesse
2108
- sp = loc.get_solarposition(times)
2109
- out_erbs = pvlib.irradiance.erbs(
2110
- df_60min['ghi'],
2111
- sp['zenith'],
2112
- df_60min.index,
2113
- )
2114
- df_60min['dni'] = out_erbs['dni']
2115
- df_60min['dhi'] = out_erbs['dhi']
2371
+ # decompose
2372
+ if decomp_model == 'erbs':
2373
+ out_erbs = pvlib.irradiance.erbs(
2374
+ df_60min['ghi'],
2375
+ sp['zenith'],
2376
+ df_60min.index,
2377
+ )
2378
+ df_60min['dni'] = out_erbs['dni']
2379
+ df_60min['dhi'] = out_erbs['dhi']
2380
+ elif decomp_model == 'dirindex':
2381
+ cs_decomp = cs # could later use a different cs model...
2382
+ decomp = pvlib.irradiance.dirindex(
2383
+ ghi=df_60min['ghi'],
2384
+ ghi_clear=cs_decomp['ghi'],
2385
+ dni_clear=cs_decomp['dni'],
2386
+ zenith=sp['zenith'],
2387
+ times=times,
2388
+ )
2389
+ df_60min['dni'] = decomp.fillna(0)
2390
+ cos_zenith = np.maximum(
2391
+ np.cos(np.deg2rad(sp['apparent_zenith'])),
2392
+ min_cos_zenith
2393
+ )
2394
+ df_60min['dhi'] = (df_60min['ghi'] -
2395
+ (df_60min['dni'] * cos_zenith))
2116
2396
 
2117
2397
  # add clearsky ghi
2118
2398
  df_60min['ghi_clear'] = df_60min['ghi'] / df_60min['ghi_csi']
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hefty
3
- Version: 0.1.6
3
+ Version: 0.2.0
4
4
  Summary: Some (relatively) lightweight short-term energy forecasting tools for solar, wind, and load.
5
5
  Author: Will Hobbs
6
6
  License-Expression: BSD-3-Clause
@@ -29,13 +29,13 @@ This repository currently includes solar and wind tools, but may expand one day
29
29
  - NOAA HRRR
30
30
  - ECMWF IFS (open data version)
31
31
  - ECMWF AIFS
32
- - ECMWF CAMS* version of IFS (a.k.a. "IFS-COMPO")
32
+ - ECMWF CAMS version of IFS* (a.k.a. "IFS-COMPO")
33
33
 
34
34
  A detailed pvlib-based PV weather-to-power model is included in [pv_model.py](src/hefty/pv_model.py), `model_pv_power()`.
35
35
 
36
36
  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.
37
37
 
38
- _*CAMS IFS is only available via `hefty.solar.get_solar_forecast()`, and it requires `cdsapi` to be installed and the user needs an API key (see https://ads.atmosphere.copernicus.eu/how-to-api)._
38
+ _*CAMS IFS is only available via `hefty.solar.get_solar_forecast()`, and it requires `cdsapi` to be installed and the user needs an API key (see https://ads.atmosphere.copernicus.eu/how-to-api and the [CAMS example notebook](examples/cams_example.ipynb))._
39
39
 
40
40
  ## Contents
41
41
  - [Installation](#installation) - getting started
@@ -130,7 +130,8 @@ The [examples](examples) folder contains several example Jupyter notebooks.
130
130
  - [more_solar_examples.ipynb](examples/more_solar_examples.ipynb): Expanded examples, including multiple sites and multiple forecast initialization times.
131
131
  - [cams_example.ipynb](examples/cams_example.ipynb): Solar resource forecasting with CAMS IFS.
132
132
  - [ensemble_example.ipynb](examples/ensemble_example.ipynb): Examples of multi-member ensembles with IFS, AIFS, and GEFS.
133
- - [wind_example.ipynb](examples/wind_example.ipynb): A basic wind resource forecast example using a few models, including converting the data to a format compatible with `windpowerlib`*
133
+ - [wind_example.ipynb](examples/wind_example.ipynb): A basic wind resource forecast example using a few models, including converting the data to a format compatible with `windpowerlib`*.
134
+ - [erbs_vs_dirindex.ipynb](examples/erbs_vs_dirindex.ipynb): A comparison of Erbs and DIRINDEX irradiance decomposition models using SURFRAD measurements.
134
135
 
135
136
  \* https://github.com/wind-python/windpowerlib/
136
137
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes