hefty 0.1.1__tar.gz → 0.1.2__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.
- {hefty-0.1.1/src/hefty.egg-info → hefty-0.1.2}/PKG-INFO +1 -1
- {hefty-0.1.1 → hefty-0.1.2}/pyproject.toml +1 -1
- {hefty-0.1.1 → hefty-0.1.2}/src/hefty/pv_model.py +120 -22
- {hefty-0.1.1 → hefty-0.1.2}/src/hefty/utilities.py +6 -3
- {hefty-0.1.1 → hefty-0.1.2/src/hefty.egg-info}/PKG-INFO +1 -1
- {hefty-0.1.1 → hefty-0.1.2}/LICENSE +0 -0
- {hefty-0.1.1 → hefty-0.1.2}/README.md +0 -0
- {hefty-0.1.1 → hefty-0.1.2}/setup.cfg +0 -0
- {hefty-0.1.1 → hefty-0.1.2}/src/hefty/__init__.py +0 -0
- {hefty-0.1.1 → hefty-0.1.2}/src/hefty/custom.py +0 -0
- {hefty-0.1.1 → hefty-0.1.2}/src/hefty/solar.py +0 -0
- {hefty-0.1.1 → hefty-0.1.2}/src/hefty/wind.py +0 -0
- {hefty-0.1.1 → hefty-0.1.2}/src/hefty.egg-info/SOURCES.txt +0 -0
- {hefty-0.1.1 → hefty-0.1.2}/src/hefty.egg-info/dependency_links.txt +0 -0
- {hefty-0.1.1 → hefty-0.1.2}/src/hefty.egg-info/requires.txt +0 -0
- {hefty-0.1.1 → hefty-0.1.2}/src/hefty.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "hefty"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
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"
|
|
@@ -100,6 +100,10 @@ def model_pv_power(
|
|
|
100
100
|
slope_aware_backtracking=True,
|
|
101
101
|
programmed_cross_axis_slope=pd.NA,
|
|
102
102
|
altitude=0,
|
|
103
|
+
transient_cell_temp=False,
|
|
104
|
+
k=None,
|
|
105
|
+
cap_adjustment=False,
|
|
106
|
+
horizon_profile=None,
|
|
103
107
|
**kwargs,
|
|
104
108
|
):
|
|
105
109
|
"""
|
|
@@ -126,6 +130,20 @@ def model_pv_power(
|
|
|
126
130
|
If True, use measured back of module temperature from ``resource_data``
|
|
127
131
|
(must have column name 'temp_module') in place of modeled cell
|
|
128
132
|
temperature.
|
|
133
|
+
transient_cell_temp: bool, default False
|
|
134
|
+
If True, apply a 10-minute rolling average to modeled cell temperature.
|
|
135
|
+
This is to account for thermal mass of modules when intervals are less
|
|
136
|
+
than 10 minutes. Based on [2]. Note that this requires uniform,
|
|
137
|
+
monotonic time intervals.
|
|
138
|
+
k: numeric, optional
|
|
139
|
+
Irradiance correction factor, defined in [3]_. Typically positive.
|
|
140
|
+
See pvlib.pvsystem.pvwatt_dc. [unitless]
|
|
141
|
+
cap_adjustment: Boolean, default False
|
|
142
|
+
If True, only apply the optional adjustment at and below 1000 Wm⁻².
|
|
143
|
+
See pvlib.pvsystem.pvwatt_dc.
|
|
144
|
+
horizon_profile: pandas.Series, optional
|
|
145
|
+
Horizon elevation angles, with index of horizon azimuth angles,
|
|
146
|
+
consistent with 'data' output from pvlib.iotools.get_pvgis_horizon.
|
|
129
147
|
|
|
130
148
|
Returns
|
|
131
149
|
-------
|
|
@@ -140,11 +158,22 @@ def model_pv_power(
|
|
|
140
158
|
----------
|
|
141
159
|
.. [1] William Hobbs, pv-plant-specification-rev4.csv,
|
|
142
160
|
https://github.com/williamhobbs/pv-plant-specifications
|
|
161
|
+
.. [2] EPRI, Jonathan Allen, Improved PV Plant Energy Production Prediction
|
|
162
|
+
(Phases 1 and 2), 2022.
|
|
163
|
+
https://www.epri.com/research/products/000000003002018708
|
|
164
|
+
.. [3] B. Marion, "Comparison of Predictive Models for Photovoltaic
|
|
165
|
+
Module Performance," In Proc. 33rd IEEE Photovoltaic Specialists
|
|
166
|
+
Conference (PVSC), San Diego, CA, USA, 2008, pp. 1-6,
|
|
167
|
+
:doi:`10.1109/PVSC.2008.4922586`.
|
|
168
|
+
Pre-print: https://docs.nrel.gov/docs/fy08osti/42511.pdf
|
|
143
169
|
"""
|
|
144
170
|
|
|
145
171
|
# ========================================================================
|
|
146
172
|
# Inputs and Basic Geometry
|
|
147
173
|
# ========================================================================
|
|
174
|
+
# make a copy to avoid mutability issues
|
|
175
|
+
resource_data = resource_data.copy()
|
|
176
|
+
|
|
148
177
|
# Fill in some necessary variables with defaults if there is no value
|
|
149
178
|
# provided.
|
|
150
179
|
# backtracking settings: default to AM/PM settings, then generic
|
|
@@ -244,6 +273,29 @@ def model_pv_power(
|
|
|
244
273
|
max_angle=max_tracker_angle,
|
|
245
274
|
backtrack=backtrack)
|
|
246
275
|
|
|
276
|
+
# add 'is_truetracking' and 'is_backtracking' booleans to output.
|
|
277
|
+
# See https://github.com/pvlib/pvlib-python/issues/2672
|
|
278
|
+
if backtrack:
|
|
279
|
+
tr_true = pvlib.tracking.singleaxis(
|
|
280
|
+
solar_position.apparent_zenith,
|
|
281
|
+
solar_position.azimuth,
|
|
282
|
+
gcr=programmed_gcr,
|
|
283
|
+
axis_tilt=axis_tilt,
|
|
284
|
+
axis_azimuth=axis_azimuth,
|
|
285
|
+
cross_axis_tilt=programmed_cross_axis_slope,
|
|
286
|
+
max_angle=max_tracker_angle,
|
|
287
|
+
backtrack=False)
|
|
288
|
+
is_truetr = ((tr.tracker_theta == tr_true.tracker_theta) &
|
|
289
|
+
(tr.tracker_theta.abs() < max_tracker_angle))
|
|
290
|
+
is_backtr = ((~is_truetr) &
|
|
291
|
+
(tr.tracker_theta.abs() < max_tracker_angle))
|
|
292
|
+
resource_data['is_truetracking'] = is_truetr
|
|
293
|
+
resource_data['is_backtracking'] = is_backtr
|
|
294
|
+
else:
|
|
295
|
+
is_truetr = (tr.tracker_theta.abs() < max_tracker_angle)
|
|
296
|
+
resource_data['is_truetracking'] = is_truetr
|
|
297
|
+
resource_data['is_backtracking'] = False
|
|
298
|
+
|
|
247
299
|
# calculate shading with slope
|
|
248
300
|
fs_array = pvlib.shading.shaded_fraction1d(
|
|
249
301
|
solar_position.apparent_zenith,
|
|
@@ -295,7 +347,7 @@ def model_pv_power(
|
|
|
295
347
|
solar_position.azimuth)
|
|
296
348
|
|
|
297
349
|
# ========================================================================
|
|
298
|
-
#
|
|
350
|
+
# DHI
|
|
299
351
|
# ========================================================================
|
|
300
352
|
if 'dhi' not in resource_data:
|
|
301
353
|
print('calculating dhi')
|
|
@@ -304,6 +356,33 @@ def model_pv_power(
|
|
|
304
356
|
resource_data['dhi'] = (resource_data.ghi - resource_data.dni *
|
|
305
357
|
pvlib.tools.cosd(solar_position.zenith))
|
|
306
358
|
|
|
359
|
+
# ========================================================================
|
|
360
|
+
# Horizon shade
|
|
361
|
+
# ========================================================================
|
|
362
|
+
# based on https://pvlib-python.readthedocs.io/en/v0.15.0/gallery/shading/plot_simple_irradiance_adjustment_for_horizon_shading.html
|
|
363
|
+
if horizon_profile is not None:
|
|
364
|
+
# interpolate horizon profile at each azimuth
|
|
365
|
+
horizon_elevation_data = np.interp(solar_position.azimuth,
|
|
366
|
+
horizon_profile.index,
|
|
367
|
+
horizon_profile
|
|
368
|
+
)
|
|
369
|
+
# convert back to a series
|
|
370
|
+
horizon_elevation_data = pd.Series(horizon_elevation_data, times)
|
|
371
|
+
# set dni to zero when the sun is below the horizon profile
|
|
372
|
+
dni_adjusted = np.where((solar_position.elevation >
|
|
373
|
+
horizon_elevation_data),
|
|
374
|
+
resource_data['dni'], 0)
|
|
375
|
+
# set ghi to be equal to dhi when the sun is below the horizon profile
|
|
376
|
+
ghi_adjusted = np.where(dni_adjusted == 0,
|
|
377
|
+
resource_data['dhi'],
|
|
378
|
+
resource_data['ghi'])
|
|
379
|
+
# write new dhi and ghi values to the resource_data dataframe
|
|
380
|
+
resource_data['dni'] = dni_adjusted
|
|
381
|
+
resource_data['ghi'] = ghi_adjusted
|
|
382
|
+
|
|
383
|
+
# ========================================================================
|
|
384
|
+
# Modeled POA
|
|
385
|
+
# ========================================================================
|
|
307
386
|
# dni
|
|
308
387
|
dni_extra = pvlib.irradiance.get_extra_radiation(resource_data.index)
|
|
309
388
|
|
|
@@ -332,8 +411,8 @@ def model_pv_power(
|
|
|
332
411
|
poa_total_without_direct_shade, aoi,
|
|
333
412
|
solar_position.apparent_zenith, solar_position.azimuth,
|
|
334
413
|
times, surface_tilt, surface_azimuth)
|
|
335
|
-
poa_direct_unshaded = irrad_dirint['dni']
|
|
336
|
-
poa_diffuse_unshaded = irrad_dirint['dhi']
|
|
414
|
+
poa_direct_unshaded = irrad_dirint['dni'] # output of gti_dirint
|
|
415
|
+
poa_diffuse_unshaded = irrad_dirint['dhi'] # output of gti_dirint
|
|
337
416
|
else:
|
|
338
417
|
# work backwards to unshaded direct irradiance for the array:
|
|
339
418
|
# poa_direct_unshaded = total_irrad.poa_direct / (1-fs_array)
|
|
@@ -412,8 +491,7 @@ def model_pv_power(
|
|
|
412
491
|
poa_total_with_direct_shade = (((1-fs) * poa_direct_unshaded.values) +
|
|
413
492
|
total_irrad['poa_diffuse'].values)
|
|
414
493
|
# diffuse fraction
|
|
415
|
-
fd = (total_irrad['poa_diffuse'].values
|
|
416
|
-
poa_total_without_direct_shade.values)
|
|
494
|
+
fd = (total_irrad['poa_diffuse'] / total_irrad['poa_global']).values
|
|
417
495
|
|
|
418
496
|
# calculate shade loss for each course/string
|
|
419
497
|
if shade_loss_model == 'linear':
|
|
@@ -426,6 +504,28 @@ def model_pv_power(
|
|
|
426
504
|
poa_front_effective = ((1 - shade_loss) *
|
|
427
505
|
poa_total_without_direct_shade.values)
|
|
428
506
|
|
|
507
|
+
# ========================================================================
|
|
508
|
+
# Non-linear irradiance response
|
|
509
|
+
# ========================================================================
|
|
510
|
+
# apply Marion's correction if k is provided
|
|
511
|
+
# similar to pvlib.pvsystem.pvwatts_dc, but applied here to front side
|
|
512
|
+
# irradiance instead of dc power
|
|
513
|
+
if k is not None:
|
|
514
|
+
# rename variables
|
|
515
|
+
effective_irradiance = poa_front_effective
|
|
516
|
+
|
|
517
|
+
# calculate error adjustments
|
|
518
|
+
err_1 = k * (1 - (1 - effective_irradiance / 200)**4)
|
|
519
|
+
err_2 = k * (1000 - effective_irradiance) / (1000 - 200)
|
|
520
|
+
err = np.where(effective_irradiance <= 200, err_1, err_2)
|
|
521
|
+
|
|
522
|
+
# cap adjustment, if needed
|
|
523
|
+
if cap_adjustment:
|
|
524
|
+
err = np.where(effective_irradiance >= 1000, 0, err)
|
|
525
|
+
|
|
526
|
+
# make error adjustment
|
|
527
|
+
poa_front_effective = poa_front_effective - 1000 * err
|
|
528
|
+
|
|
429
529
|
# ========================================================================
|
|
430
530
|
# Temperature
|
|
431
531
|
# ========================================================================
|
|
@@ -439,23 +539,18 @@ def model_pv_power(
|
|
|
439
539
|
resource_data['wind_speed']).values
|
|
440
540
|
for n in range(eff_row_side_num_mods)])
|
|
441
541
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
# based on https://stackoverflow.com/a/47490020/27574852
|
|
455
|
-
t_cell_modeled = np.array([
|
|
456
|
-
np.convolve(
|
|
457
|
-
t_cell_modeled[n], np.ones((N,))/N, 'same')
|
|
458
|
-
for n in range(eff_row_side_num_mods)])
|
|
542
|
+
if transient_cell_temp is True:
|
|
543
|
+
# apply rolling 10-min avg if interval is less than 10 min
|
|
544
|
+
# based on https://www.epri.com/research/products/000000003002018708
|
|
545
|
+
sample_int, samples_per_window = pvlib.tools._get_sample_intervals(
|
|
546
|
+
times=resource_data.index, win_length=10)
|
|
547
|
+
if sample_int < 10:
|
|
548
|
+
N = samples_per_window
|
|
549
|
+
# based on https://stackoverflow.com/a/47490020/27574852
|
|
550
|
+
t_cell_modeled = np.array([
|
|
551
|
+
np.convolve(
|
|
552
|
+
t_cell_modeled[n], np.ones((N,))/N, 'same')
|
|
553
|
+
for n in range(eff_row_side_num_mods)])
|
|
459
554
|
|
|
460
555
|
if use_measured_temp_module is True:
|
|
461
556
|
# use measured module temperature - repeat the single timeseries
|
|
@@ -568,6 +663,9 @@ def model_pv_power(
|
|
|
568
663
|
pdc_inv_total.fillna(0, inplace=True)
|
|
569
664
|
resource_data.fillna(0, inplace=True)
|
|
570
665
|
|
|
666
|
+
# add dc power to output
|
|
667
|
+
resource_data['pdc'] = pdc_inv_total
|
|
668
|
+
|
|
571
669
|
# ac power with PVWatts inverter model
|
|
572
670
|
power_ac = pvlib.inverter.pvwatts(pdc_inv_total, pdc0, eta_inv_nom)
|
|
573
671
|
|
|
@@ -82,7 +82,7 @@ def get_fcast_definition(model='gfs'):
|
|
|
82
82
|
'2024-11-12 12:00',
|
|
83
83
|
'2024-11-12 06:00'],
|
|
84
84
|
'start_hour': [0, 150, 0],
|
|
85
|
-
'end_hour': [144, 360,
|
|
85
|
+
'end_hour': [144, 360, 144],
|
|
86
86
|
'interval': [3, 6, 3],
|
|
87
87
|
'first_cycle': [0, 0, 6],
|
|
88
88
|
'update_period': [12, 12, 12],
|
|
@@ -122,13 +122,16 @@ def get_fcast_definition(model='gfs'):
|
|
|
122
122
|
# IFS ens does not have ssrd until sometime March 2024. '2024-03-12 12:00'
|
|
123
123
|
# was the first init_date used in https://github.com/williamhobbs/PVSC-2025-daily-energy-forecaster,
|
|
124
124
|
# so start there for now.
|
|
125
|
-
# delays based on https://dynamical.org/status/ as of 2026-04-24
|
|
125
|
+
# delays based on https://dynamical.org/status/ as of 2026-04-24.
|
|
126
|
+
# From https://www.ecmwf.int/en/forecasts/datasets/open-data,
|
|
127
|
+
# For times 00z &12z: 0 to 144 by 3, 150 to 360 by 6.
|
|
128
|
+
# For times 06z & 18z: 0 to 144 by 3.
|
|
126
129
|
fcast_sched_dict_ifs_ens_1 = {
|
|
127
130
|
'start_date': ['2024-03-10 12:00',
|
|
128
131
|
'2024-03-10 12:00',
|
|
129
132
|
'2024-03-10 18:00'], # https://herbie.readthedocs.io/en/stable/gallery/ecmwf_models/ecmwf.html#Data-Availability
|
|
130
133
|
'start_hour': [0, 150, 0],
|
|
131
|
-
'end_hour': [144, 240,
|
|
134
|
+
'end_hour': [144, 240, 144],
|
|
132
135
|
'interval': [3, 6, 3],
|
|
133
136
|
'first_cycle': [0, 0, 6],
|
|
134
137
|
'update_period': [12, 12, 12],
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|