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/pv_model.py
ADDED
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import pvlib
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# from https://github.com/williamhobbs/2024_pvpmc_self_shade
|
|
7
|
+
def shade_fractions(fs_array, eff_row_side_num_mods):
|
|
8
|
+
"""
|
|
9
|
+
Shade fractions on each _course_ of a rack or tracker.
|
|
10
|
+
|
|
11
|
+
Parameters
|
|
12
|
+
----------
|
|
13
|
+
fs_array : numeric
|
|
14
|
+
Scalar or vector of shade fractions for the entire rack or tracker.
|
|
15
|
+
Zero (0) is unshaded and one (1) is fully shaded.
|
|
16
|
+
eff_row_side_num_mods : int
|
|
17
|
+
Number of courses in the rack as modules. EG: a 2P tracker has 2
|
|
18
|
+
courses.
|
|
19
|
+
|
|
20
|
+
Returns
|
|
21
|
+
-------
|
|
22
|
+
Array with the shade fraction on each course.
|
|
23
|
+
"""
|
|
24
|
+
fs_course = np.clip([
|
|
25
|
+
fs_array * eff_row_side_num_mods - course
|
|
26
|
+
for course in range(eff_row_side_num_mods)], 0, 1)
|
|
27
|
+
return fs_course
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# from https://github.com/williamhobbs/2024_pvpmc_self_shade
|
|
31
|
+
def non_linear_shade(n_cells_up, fs, fd):
|
|
32
|
+
"""
|
|
33
|
+
Simple non-linear shade model.
|
|
34
|
+
|
|
35
|
+
Assume shade loss is linear as direct shade moves from bottom through
|
|
36
|
+
first cell, and then only diffuse for remainder of module up to the top.
|
|
37
|
+
EG: If there are 10 cells, and ``fs`` is 0.05, the bottom cell is only
|
|
38
|
+
half shaded, then the loss would be 50% of the direct irradiance. If
|
|
39
|
+
80% of POA global on module is direct irradiance, IE: ``fd = 0.2``,
|
|
40
|
+
then loss would be 40%. When the direct shade line reaches 10%, one
|
|
41
|
+
cell is completely shaded, then the loss is 80%, and there's only diffuse
|
|
42
|
+
light on the module. Any direct shade above the 1st cell has the same loss.
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
n_cells_up : int
|
|
47
|
+
Number of cells vertically up
|
|
48
|
+
fs : float
|
|
49
|
+
Fraction of shade on module, 1 is fully shaded
|
|
50
|
+
fd : numeric
|
|
51
|
+
Diffuse fraction
|
|
52
|
+
|
|
53
|
+
Returns
|
|
54
|
+
-------
|
|
55
|
+
Array of shade loss same size as ``fd``
|
|
56
|
+
"""
|
|
57
|
+
pnorm = np.where(fs < 1/n_cells_up, 1 - (1 - fd)*fs*n_cells_up, fd)
|
|
58
|
+
shade_loss = 1 - pnorm
|
|
59
|
+
return shade_loss
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# Partially based on
|
|
63
|
+
# https://github.com/williamhobbs/2024_pvpmc_self_shade/tree/iam_and_spectrum
|
|
64
|
+
def model_pv_power(
|
|
65
|
+
resource_data,
|
|
66
|
+
latitude,
|
|
67
|
+
longitude,
|
|
68
|
+
mount_type,
|
|
69
|
+
gcr,
|
|
70
|
+
nameplate_dc,
|
|
71
|
+
nameplate_ac,
|
|
72
|
+
dc_loss_fraction,
|
|
73
|
+
gamma_pdc,
|
|
74
|
+
shade_loss_model,
|
|
75
|
+
default_site_transposition_model='perez-driesse',
|
|
76
|
+
backtrack=True,
|
|
77
|
+
backtrack_fraction=1,
|
|
78
|
+
max_tracker_angle=pd.NA,
|
|
79
|
+
axis_tilt=pd.NA,
|
|
80
|
+
axis_azimuth=pd.NA,
|
|
81
|
+
fixed_tilt=pd.NA,
|
|
82
|
+
fixed_azimuth=pd.NA,
|
|
83
|
+
n_cells_up=12,
|
|
84
|
+
row_side_num_mods=pd.NA,
|
|
85
|
+
row_height_center=pd.NA,
|
|
86
|
+
row_pitch=pd.NA,
|
|
87
|
+
collector_width=pd.NA,
|
|
88
|
+
bifacial=False,
|
|
89
|
+
bifaciality_factor=0.8,
|
|
90
|
+
surface_tilt_timeseries=pd.Series([], dtype='float64'),
|
|
91
|
+
surface_azimuth_timeseries=pd.Series([], dtype='float64'),
|
|
92
|
+
use_measured_poa=False,
|
|
93
|
+
use_measured_temp_module=False,
|
|
94
|
+
cell_type='crystalline',
|
|
95
|
+
eta_inv_nom=0.98,
|
|
96
|
+
cross_axis_slope=0,
|
|
97
|
+
gcr_backtrack_setting=pd.NA,
|
|
98
|
+
programmed_gcr_am=pd.NA,
|
|
99
|
+
programmed_gcr_pm=pd.NA,
|
|
100
|
+
slope_aware_backtracking=True,
|
|
101
|
+
programmed_cross_axis_slope=pd.NA,
|
|
102
|
+
altitude=0,
|
|
103
|
+
**kwargs,
|
|
104
|
+
):
|
|
105
|
+
"""
|
|
106
|
+
Power model for PV plants. Based a bit on
|
|
107
|
+
https://github.com/williamhobbs/2024_pvpmc_self_shade/tree/iam_and_spectrum
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
resource_data : pandas.DataFrame
|
|
112
|
+
timeseries weather/resource data with the same format as is returned by
|
|
113
|
+
pvlib.iotools.get* functions
|
|
114
|
+
[long list of additional arguments defining a plant based on [1]]
|
|
115
|
+
surface_tilt_timeseries : pandas.DataFrame
|
|
116
|
+
(optional) custom timeseries of the angle between the panel surface and
|
|
117
|
+
the earth surface, accounting for panel rotation. [degrees]
|
|
118
|
+
surface_azimuth_timeseries : pandas.DataFrame
|
|
119
|
+
(optional) custom timeseries of the azimuth of the rotated panel,
|
|
120
|
+
determined by projecting the vector normal to the panel's surface to
|
|
121
|
+
the earth's surface. [degrees]
|
|
122
|
+
use_measured_poa : bool, default False
|
|
123
|
+
If True, used measure POA data from ``resource_data`` (must have
|
|
124
|
+
column name 'poa').
|
|
125
|
+
use_measured_temp_module: bool, default False
|
|
126
|
+
If True, use measured back of module temperature from ``resource_data``
|
|
127
|
+
(must have column name 'temp_module') in place of modeled cell
|
|
128
|
+
temperature.
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
power_ac : pandas.Series
|
|
133
|
+
AC power. Same units as ``dc_capacity_plant`` and
|
|
134
|
+
``power_plant_ac_max`` (ideally kW).
|
|
135
|
+
resource_data : pandas.DataFrame
|
|
136
|
+
modified version of input ``resource_data`` with modeled poa, module
|
|
137
|
+
temperature, and possibly other parameters added.
|
|
138
|
+
|
|
139
|
+
References
|
|
140
|
+
----------
|
|
141
|
+
.. [1] William Hobbs, pv-plant-specification-rev4.csv,
|
|
142
|
+
https://github.com/williamhobbs/pv-plant-specifications
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
# ========================================================================
|
|
146
|
+
# Inputs and Basic Geometry
|
|
147
|
+
# ========================================================================
|
|
148
|
+
# Fill in some necessary variables with defaults if there is no value
|
|
149
|
+
# provided.
|
|
150
|
+
# backtracking settings: default to AM/PM settings, then generic
|
|
151
|
+
# programmed value, then physical gcr
|
|
152
|
+
if pd.isna(gcr_backtrack_setting):
|
|
153
|
+
gcr_backtrack_setting = gcr
|
|
154
|
+
if pd.isna(programmed_gcr_am):
|
|
155
|
+
programmed_gcr_am = gcr_backtrack_setting
|
|
156
|
+
if pd.isna(programmed_gcr_pm):
|
|
157
|
+
programmed_gcr_pm = gcr_backtrack_setting
|
|
158
|
+
if backtrack_fraction == 0:
|
|
159
|
+
backtrack = False # switch to truetracking to avoid divide by zero
|
|
160
|
+
|
|
161
|
+
# slope-aware backtracking settings
|
|
162
|
+
if ((slope_aware_backtracking is True) &
|
|
163
|
+
(pd.isna(programmed_cross_axis_slope))):
|
|
164
|
+
programmed_cross_axis_slope = cross_axis_slope
|
|
165
|
+
elif ((slope_aware_backtracking is False) &
|
|
166
|
+
(pd.isna(programmed_cross_axis_slope))):
|
|
167
|
+
programmed_cross_axis_slope = 0
|
|
168
|
+
elif ((slope_aware_backtracking is False) &
|
|
169
|
+
(pd.notna(programmed_cross_axis_slope))):
|
|
170
|
+
slope_aware_backtracking = True
|
|
171
|
+
print("""You provided a value for programmed_cross_axis_slope
|
|
172
|
+
AND did not set slope_aware_backtracking=True. Slope-aware
|
|
173
|
+
backtracking will be enabled.""")
|
|
174
|
+
|
|
175
|
+
# geometry
|
|
176
|
+
if pd.isna(axis_tilt):
|
|
177
|
+
axis_tilt = 0 # default if no value provided
|
|
178
|
+
if pd.isna(axis_azimuth):
|
|
179
|
+
axis_azimuth = 180 # default if no value provided
|
|
180
|
+
if pd.isna(row_side_num_mods):
|
|
181
|
+
row_side_num_mods = 1 # default if no value provided
|
|
182
|
+
if pd.isna(row_height_center):
|
|
183
|
+
row_height_center = 1 # default if no value provided
|
|
184
|
+
if pd.isna(max_tracker_angle):
|
|
185
|
+
max_tracker_angle = 60
|
|
186
|
+
|
|
187
|
+
# gcr = collector_width / row_pitch, gcr is a required input, so users can
|
|
188
|
+
# define 1 of the other 2.
|
|
189
|
+
# If all 3 are defined, check to make sure relationship is correct.
|
|
190
|
+
if pd.isna(collector_width) & pd.isna(row_pitch): # neither provided
|
|
191
|
+
collector_width = 2 # default if no value provided
|
|
192
|
+
row_pitch = collector_width / gcr
|
|
193
|
+
elif pd.isna(row_pitch):
|
|
194
|
+
row_pitch = collector_width / gcr
|
|
195
|
+
elif pd.isna(collector_width):
|
|
196
|
+
collector_width = gcr * row_pitch
|
|
197
|
+
elif gcr != collector_width / row_pitch:
|
|
198
|
+
raise ValueError("""You provided collector_width, row_pitch, and gcr,
|
|
199
|
+
but they are inconsistent. gcr must equal:
|
|
200
|
+
collector_width / row_pitch.
|
|
201
|
+
Please check these values. Note that gcr is required, and only one
|
|
202
|
+
of collector_width and row_pitch are needed to fully define the
|
|
203
|
+
related geometry.""")
|
|
204
|
+
|
|
205
|
+
# general geometry calcs
|
|
206
|
+
pitch = collector_width / gcr
|
|
207
|
+
|
|
208
|
+
# ========================================================================
|
|
209
|
+
# Time and solar position
|
|
210
|
+
# ========================================================================
|
|
211
|
+
# time and solar position with correct time
|
|
212
|
+
times = resource_data.index
|
|
213
|
+
loc = pvlib.location.Location(latitude=latitude, longitude=longitude,
|
|
214
|
+
tz=times.tz, altitude=altitude)
|
|
215
|
+
solar_position = loc.get_solarposition(times)
|
|
216
|
+
|
|
217
|
+
# ========================================================================
|
|
218
|
+
# Surface tilt, azimuth, and shaded fraction
|
|
219
|
+
# ========================================================================
|
|
220
|
+
if surface_tilt_timeseries.empty | surface_azimuth_timeseries.empty:
|
|
221
|
+
if mount_type == 'single-axis':
|
|
222
|
+
# modify tracker gcr if needed
|
|
223
|
+
if backtrack is True:
|
|
224
|
+
programmed_gcr = np.where(solar_position.azimuth < 180,
|
|
225
|
+
programmed_gcr_am*backtrack_fraction,
|
|
226
|
+
programmed_gcr_pm*backtrack_fraction)
|
|
227
|
+
else:
|
|
228
|
+
programmed_gcr = gcr_backtrack_setting
|
|
229
|
+
|
|
230
|
+
# change cross_axis_slope used for tracker orientation if needed
|
|
231
|
+
# if slope_aware_backtracking is True:
|
|
232
|
+
# programmed_cross_axis_slope = cross_axis_slope
|
|
233
|
+
# else:
|
|
234
|
+
# programmed_cross_axis_slope = 0
|
|
235
|
+
|
|
236
|
+
# tracker orientation
|
|
237
|
+
tr = pvlib.tracking.singleaxis(
|
|
238
|
+
solar_position.apparent_zenith,
|
|
239
|
+
solar_position.azimuth,
|
|
240
|
+
gcr=programmed_gcr,
|
|
241
|
+
axis_tilt=axis_tilt,
|
|
242
|
+
axis_azimuth=axis_azimuth,
|
|
243
|
+
cross_axis_tilt=programmed_cross_axis_slope,
|
|
244
|
+
max_angle=max_tracker_angle,
|
|
245
|
+
backtrack=backtrack)
|
|
246
|
+
|
|
247
|
+
# calculate shading with slope
|
|
248
|
+
fs_array = pvlib.shading.shaded_fraction1d(
|
|
249
|
+
solar_position.apparent_zenith,
|
|
250
|
+
solar_position.azimuth,
|
|
251
|
+
axis_azimuth=axis_azimuth,
|
|
252
|
+
shaded_row_rotation=tr.tracker_theta,
|
|
253
|
+
collector_width=collector_width, pitch=pitch,
|
|
254
|
+
axis_tilt=axis_tilt,
|
|
255
|
+
cross_axis_slope=cross_axis_slope)
|
|
256
|
+
|
|
257
|
+
surface_tilt = tr.surface_tilt.fillna(0)
|
|
258
|
+
surface_azimuth = tr.surface_azimuth.fillna(0)
|
|
259
|
+
resource_data['tracker_theta'] = tr.tracker_theta
|
|
260
|
+
elif mount_type == 'fixed':
|
|
261
|
+
# calculate shading
|
|
262
|
+
# model fixed array as a stuck tracker for azimuth and rotation
|
|
263
|
+
fs_array = pvlib.shading.shaded_fraction1d(
|
|
264
|
+
solar_position.apparent_zenith,
|
|
265
|
+
solar_position.azimuth,
|
|
266
|
+
axis_azimuth=fixed_azimuth - 90,
|
|
267
|
+
shaded_row_rotation=fixed_tilt,
|
|
268
|
+
collector_width=collector_width, pitch=pitch,
|
|
269
|
+
axis_tilt=axis_tilt,
|
|
270
|
+
cross_axis_slope=cross_axis_slope
|
|
271
|
+
)
|
|
272
|
+
surface_tilt = float(fixed_tilt)
|
|
273
|
+
surface_azimuth = float(fixed_azimuth)
|
|
274
|
+
resource_data['tracker_theta'] = np.nan
|
|
275
|
+
else:
|
|
276
|
+
surface_tilt = surface_tilt_timeseries
|
|
277
|
+
surface_azimuth = surface_azimuth_timeseries
|
|
278
|
+
# calculate tracker theta, TODO: double-check this
|
|
279
|
+
tracker_theta = surface_tilt.where((surface_azimuth >= 180),
|
|
280
|
+
- surface_tilt)
|
|
281
|
+
|
|
282
|
+
fs_array = pvlib.shading.shaded_fraction1d(
|
|
283
|
+
solar_position.apparent_zenith,
|
|
284
|
+
solar_position.azimuth,
|
|
285
|
+
axis_azimuth=axis_azimuth,
|
|
286
|
+
shaded_row_rotation=tracker_theta,
|
|
287
|
+
collector_width=collector_width, pitch=pitch,
|
|
288
|
+
axis_tilt=axis_tilt,
|
|
289
|
+
cross_axis_slope=cross_axis_slope)
|
|
290
|
+
resource_data['tracker_theta'] = tracker_theta
|
|
291
|
+
resource_data['fs_array'] = fs_array
|
|
292
|
+
|
|
293
|
+
aoi = pvlib.irradiance.aoi(surface_tilt, surface_azimuth,
|
|
294
|
+
solar_position.apparent_zenith,
|
|
295
|
+
solar_position.azimuth)
|
|
296
|
+
|
|
297
|
+
# ========================================================================
|
|
298
|
+
# Modeled POA
|
|
299
|
+
# ========================================================================
|
|
300
|
+
if 'dhi' not in resource_data:
|
|
301
|
+
print('calculating dhi')
|
|
302
|
+
# calculate DHI with "complete sum" AKA "closure" equation:
|
|
303
|
+
# DHI = GHI - DNI * cos(zenith)
|
|
304
|
+
resource_data['dhi'] = (resource_data.ghi - resource_data.dni *
|
|
305
|
+
pvlib.tools.cosd(solar_position.zenith))
|
|
306
|
+
|
|
307
|
+
# dni
|
|
308
|
+
dni_extra = pvlib.irradiance.get_extra_radiation(resource_data.index)
|
|
309
|
+
|
|
310
|
+
# total irradiance
|
|
311
|
+
total_irrad = pvlib.irradiance.get_total_irradiance(
|
|
312
|
+
surface_tilt=surface_tilt,
|
|
313
|
+
surface_azimuth=surface_azimuth,
|
|
314
|
+
solar_zenith=solar_position.apparent_zenith,
|
|
315
|
+
solar_azimuth=solar_position.azimuth,
|
|
316
|
+
dni=resource_data.dni,
|
|
317
|
+
ghi=resource_data.ghi,
|
|
318
|
+
dhi=resource_data.dhi,
|
|
319
|
+
dni_extra=dni_extra,
|
|
320
|
+
albedo=resource_data.albedo,
|
|
321
|
+
model=default_site_transposition_model,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
resource_data['poa_modeled'] = total_irrad['poa_global']
|
|
325
|
+
|
|
326
|
+
# ========================================================================
|
|
327
|
+
# Handle measured POA, get diffuse and direct ready
|
|
328
|
+
# ========================================================================
|
|
329
|
+
if use_measured_poa is True:
|
|
330
|
+
poa_total_without_direct_shade = resource_data.poa
|
|
331
|
+
irrad_dirint = pvlib.irradiance.gti_dirint(
|
|
332
|
+
poa_total_without_direct_shade, aoi,
|
|
333
|
+
solar_position.apparent_zenith, solar_position.azimuth,
|
|
334
|
+
times, surface_tilt, surface_azimuth)
|
|
335
|
+
poa_direct_unshaded = irrad_dirint['dni'] # output of gti_dirint
|
|
336
|
+
poa_diffuse_unshaded = irrad_dirint['dhi'] # output of gti_dirint
|
|
337
|
+
else:
|
|
338
|
+
# work backwards to unshaded direct irradiance for the array:
|
|
339
|
+
# poa_direct_unshaded = total_irrad.poa_direct / (1-fs_array)
|
|
340
|
+
# !!! get_total_irradiance doesn't include shade like infinite_sheds,
|
|
341
|
+
# so no correction needed!!!
|
|
342
|
+
poa_direct_unshaded = total_irrad['poa_direct']
|
|
343
|
+
poa_diffuse_unshaded = total_irrad['poa_diffuse']
|
|
344
|
+
|
|
345
|
+
# ========================================================================
|
|
346
|
+
# IAM and Spectral correction
|
|
347
|
+
# ========================================================================
|
|
348
|
+
# iam
|
|
349
|
+
# iam = pvlib.iam.physical(aoi, L=0.0032, n_ar=1.29)
|
|
350
|
+
# iam = pvlib.iam.physical(aoi, L=0.0032)
|
|
351
|
+
iam = pvlib.iam.physical(aoi)
|
|
352
|
+
|
|
353
|
+
# spectral modifier for cdte
|
|
354
|
+
if cell_type == 'thin-film_cdte':
|
|
355
|
+
airmass = loc.get_airmass(solar_position=solar_position)
|
|
356
|
+
if 'precipitable_water' not in resource_data.columns:
|
|
357
|
+
if (('temp_air' in resource_data.columns) &
|
|
358
|
+
('relative_humidity' in resource_data.columns)):
|
|
359
|
+
resource_data['precipitable_water'] = \
|
|
360
|
+
pvlib.atmosphere.gueymard94_pw(
|
|
361
|
+
temp_air=resource_data.temp_air,
|
|
362
|
+
relative_humidity=resource_data.relative_humidity)
|
|
363
|
+
else:
|
|
364
|
+
resource_data['precipitable_water'] = 1
|
|
365
|
+
spectral_modifier = pvlib.spectrum.spectral_factor_firstsolar(
|
|
366
|
+
precipitable_water=resource_data.precipitable_water,
|
|
367
|
+
airmass_absolute=airmass.airmass_absolute,
|
|
368
|
+
module_type='cdte',
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
# apply iam
|
|
372
|
+
poa_direct_unshaded = poa_direct_unshaded * iam
|
|
373
|
+
|
|
374
|
+
# total poa on the front, but without direct shade impacts
|
|
375
|
+
# (would be keeping diffuse impacts from infinite_sheds if we used
|
|
376
|
+
# inifinite_sheds...)
|
|
377
|
+
poa_total_without_direct_shade = (poa_diffuse_unshaded +
|
|
378
|
+
poa_direct_unshaded)
|
|
379
|
+
|
|
380
|
+
if cell_type == 'thin-film_cdte':
|
|
381
|
+
poa_total_without_direct_shade = (poa_total_without_direct_shade *
|
|
382
|
+
spectral_modifier)
|
|
383
|
+
|
|
384
|
+
# set zero POA to nan to avoid divide by zero warnings
|
|
385
|
+
# this might not be needed!!!
|
|
386
|
+
poa_total_without_direct_shade = (
|
|
387
|
+
poa_total_without_direct_shade.replace(0, np.nan))
|
|
388
|
+
|
|
389
|
+
# ========================================================================
|
|
390
|
+
# Shade losses
|
|
391
|
+
# ========================================================================
|
|
392
|
+
# set the "effective" number of modules on the side of each row
|
|
393
|
+
if shade_loss_model == 'non-linear_simple':
|
|
394
|
+
eff_row_side_num_mods = int(row_side_num_mods)
|
|
395
|
+
elif shade_loss_model == 'non-linear_simple_twin_module':
|
|
396
|
+
# twin modules are treated as effectively two modules with half as
|
|
397
|
+
# many cells each
|
|
398
|
+
eff_row_side_num_mods = int(row_side_num_mods) * 2
|
|
399
|
+
n_cells_up = n_cells_up / 2
|
|
400
|
+
# for linear shade loss, it really doesn't matter how many modules there
|
|
401
|
+
# are on the side of each row, so just run everything once to save time
|
|
402
|
+
elif shade_loss_model == 'linear':
|
|
403
|
+
eff_row_side_num_mods = 1
|
|
404
|
+
else:
|
|
405
|
+
raise ValueError("""shade_loss_model must be one of:
|
|
406
|
+
'non-linear_simple', 'non-linear_simple_twin_module', or 'linear'.
|
|
407
|
+
You entered: '""" + shade_loss_model + """'""")
|
|
408
|
+
|
|
409
|
+
# shaded fraction for each course/string going up the row
|
|
410
|
+
fs = shade_fractions(fs_array, eff_row_side_num_mods)
|
|
411
|
+
# total POA *with* direct shade impacts
|
|
412
|
+
poa_total_with_direct_shade = (((1-fs) * poa_direct_unshaded.values) +
|
|
413
|
+
total_irrad['poa_diffuse'].values)
|
|
414
|
+
# diffuse fraction
|
|
415
|
+
fd = (total_irrad['poa_diffuse'].values /
|
|
416
|
+
poa_total_without_direct_shade.values)
|
|
417
|
+
|
|
418
|
+
# calculate shade loss for each course/string
|
|
419
|
+
if shade_loss_model == 'linear':
|
|
420
|
+
shade_loss = fs * (1 - fd)
|
|
421
|
+
elif (shade_loss_model == 'non-linear_simple' or
|
|
422
|
+
shade_loss_model == 'non-linear_simple_twin_module'):
|
|
423
|
+
shade_loss = non_linear_shade(n_cells_up, fs, fd)
|
|
424
|
+
|
|
425
|
+
# adjust irradiance based on modeled shade loss
|
|
426
|
+
poa_front_effective = ((1 - shade_loss) *
|
|
427
|
+
poa_total_without_direct_shade.values)
|
|
428
|
+
|
|
429
|
+
# ========================================================================
|
|
430
|
+
# Temperature
|
|
431
|
+
# ========================================================================
|
|
432
|
+
# cell temperature
|
|
433
|
+
# steady state cell temperature - faiman is much faster than fuentes,
|
|
434
|
+
# simpler than sapm
|
|
435
|
+
t_cell_modeled = np.array([
|
|
436
|
+
pvlib.temperature.faiman(
|
|
437
|
+
poa_total_with_direct_shade[n],
|
|
438
|
+
resource_data['temp_air'],
|
|
439
|
+
resource_data['wind_speed']).values
|
|
440
|
+
for n in range(eff_row_side_num_mods)])
|
|
441
|
+
|
|
442
|
+
# apply rolling 10-min avg if interval is less than 10 min
|
|
443
|
+
# based on https://www.epri.com/research/products/000000003002018708
|
|
444
|
+
# sample_interval, samples_per_window = pvlib.tools._get_sample_intervals(
|
|
445
|
+
# times=resource_data.index, win_length=10)
|
|
446
|
+
# based on pvlib.tools._get_sample_intervals, but not exactly the same,
|
|
447
|
+
# because it doesn't work when there are gaps
|
|
448
|
+
sample_interval = resource_data.index[1] - resource_data.index[0]
|
|
449
|
+
sample_interval = sample_interval.seconds / 60 # in minutes
|
|
450
|
+
win_length = 10
|
|
451
|
+
samples_per_window = int(win_length / sample_interval)
|
|
452
|
+
if sample_interval < 10:
|
|
453
|
+
N = samples_per_window
|
|
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)])
|
|
459
|
+
|
|
460
|
+
if use_measured_temp_module is True:
|
|
461
|
+
# use measured module temperature - repeat the single timeseries
|
|
462
|
+
# for each course in the array
|
|
463
|
+
t_cell = np.array([
|
|
464
|
+
resource_data['temp_module'].values
|
|
465
|
+
for n in range(eff_row_side_num_mods)])
|
|
466
|
+
else:
|
|
467
|
+
t_cell = t_cell_modeled
|
|
468
|
+
|
|
469
|
+
resource_data['t_cell_modeled'] = np.mean(t_cell_modeled, axis=0)
|
|
470
|
+
|
|
471
|
+
# ========================================================================
|
|
472
|
+
# Bifacial
|
|
473
|
+
# ========================================================================
|
|
474
|
+
if bifacial is True:
|
|
475
|
+
# transposition models allowed for infinite_sheds:
|
|
476
|
+
if default_site_transposition_model not in ['haydavies', 'isotropic']:
|
|
477
|
+
print('pvlib.bifacial.infinite_sheds does not currently accept'
|
|
478
|
+
' the ' + default_site_transposition_model + ' model.')
|
|
479
|
+
print('using haydavies instead.')
|
|
480
|
+
inf_sheds_transposition_model = 'haydavies'
|
|
481
|
+
else:
|
|
482
|
+
inf_sheds_transposition_model = default_site_transposition_model
|
|
483
|
+
|
|
484
|
+
# run infinite_sheds to get rear irradiance
|
|
485
|
+
irrad_inf_sh = pvlib.bifacial.infinite_sheds.get_irradiance(
|
|
486
|
+
surface_tilt=surface_tilt,
|
|
487
|
+
surface_azimuth=surface_azimuth,
|
|
488
|
+
solar_zenith=solar_position.apparent_zenith,
|
|
489
|
+
solar_azimuth=solar_position.azimuth,
|
|
490
|
+
gcr=gcr,
|
|
491
|
+
height=row_height_center,
|
|
492
|
+
pitch=row_pitch,
|
|
493
|
+
ghi=resource_data.ghi,
|
|
494
|
+
dhi=resource_data.dhi,
|
|
495
|
+
dni=resource_data.dni,
|
|
496
|
+
albedo=resource_data.albedo,
|
|
497
|
+
model=inf_sheds_transposition_model,
|
|
498
|
+
dni_extra=dni_extra,
|
|
499
|
+
bifaciality=bifaciality_factor,
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
# now for the rear irradiance
|
|
503
|
+
fs_array_back = irrad_inf_sh['shaded_fraction_back']
|
|
504
|
+
poa_back_direct_unshaded = (
|
|
505
|
+
irrad_inf_sh['poa_back_direct'] / (1-fs_array_back)
|
|
506
|
+
)
|
|
507
|
+
poa_back_total_without_direct_shade = (
|
|
508
|
+
irrad_inf_sh['poa_back_diffuse'] + poa_back_direct_unshaded
|
|
509
|
+
)
|
|
510
|
+
poa_back_total_without_direct_shade.replace(0, np.nan, inplace=True)
|
|
511
|
+
fs_back = shade_fractions(fs_array_back, eff_row_side_num_mods)
|
|
512
|
+
# commented out, not currently used:
|
|
513
|
+
# poa_back_total_with_direct_shade = (
|
|
514
|
+
# ((1-fs_back) * poa_back_direct_unshaded.values) +
|
|
515
|
+
# irrad_inf_sh['poa_back_diffuse'].values
|
|
516
|
+
# )
|
|
517
|
+
fd = (irrad_inf_sh['poa_back_diffuse'].values /
|
|
518
|
+
poa_back_total_without_direct_shade.values)
|
|
519
|
+
if shade_loss_model == 'linear':
|
|
520
|
+
# shade_loss = fs * (1 - fd)
|
|
521
|
+
shade_loss = fs_back * (1 - fd)
|
|
522
|
+
elif (shade_loss_model == 'non-linear_simple' or
|
|
523
|
+
shade_loss_model == 'non-linear_simple_twin_module'):
|
|
524
|
+
# shade_loss = non_linear_shade(n_cells_up, fs, fd)
|
|
525
|
+
shade_loss = non_linear_shade(n_cells_up, fs_back, fd)
|
|
526
|
+
|
|
527
|
+
# adjust irradiance based on modeled shade loss, include
|
|
528
|
+
# bifaciality_factor
|
|
529
|
+
poa_back_effective = (bifaciality_factor * (1 - shade_loss) *
|
|
530
|
+
poa_back_total_without_direct_shade.values)
|
|
531
|
+
|
|
532
|
+
# combine front and back effective POA
|
|
533
|
+
poa_effective = poa_front_effective + poa_back_effective
|
|
534
|
+
resource_data['poa_effective'] = (
|
|
535
|
+
pd.DataFrame(poa_effective.T, index=times).mean(axis=1))
|
|
536
|
+
resource_data['poa_front_effective'] = (
|
|
537
|
+
pd.DataFrame(poa_front_effective.T, index=times).mean(axis=1))
|
|
538
|
+
resource_data['poa_back_effective'] = (
|
|
539
|
+
pd.DataFrame(poa_back_effective.T, index=times).mean(axis=1))
|
|
540
|
+
resource_data['poa_back_modeled'] = (
|
|
541
|
+
poa_back_total_without_direct_shade.values)
|
|
542
|
+
else:
|
|
543
|
+
poa_effective = poa_front_effective
|
|
544
|
+
resource_data['poa_effective'] = (
|
|
545
|
+
pd.DataFrame(poa_effective.T, index=times).mean(axis=1))
|
|
546
|
+
resource_data['poa_front_effective'] = (
|
|
547
|
+
pd.DataFrame(poa_front_effective.T, index=times).mean(axis=1))
|
|
548
|
+
resource_data['poa_back_effective'] = np.nan
|
|
549
|
+
resource_data['poa_back_modeled'] = np.nan
|
|
550
|
+
|
|
551
|
+
# ========================================================================
|
|
552
|
+
# Power
|
|
553
|
+
# ========================================================================
|
|
554
|
+
# PVWatts dc power
|
|
555
|
+
pdc_shaded = pvlib.pvsystem.pvwatts_dc(
|
|
556
|
+
poa_effective, t_cell, nameplate_dc, gamma_pdc)
|
|
557
|
+
|
|
558
|
+
# dc power into the inverter after losses
|
|
559
|
+
pdc_inv = pdc_shaded * (1 - dc_loss_fraction)
|
|
560
|
+
|
|
561
|
+
# inverter dc input is ac nameplate divided by nominal inverter efficiency
|
|
562
|
+
pdc0 = nameplate_ac/eta_inv_nom
|
|
563
|
+
|
|
564
|
+
# average the dc power across n positions up the row
|
|
565
|
+
pdc_inv_total = pd.DataFrame(pdc_inv.T, index=times).mean(axis=1)
|
|
566
|
+
|
|
567
|
+
# fill nan with zero
|
|
568
|
+
pdc_inv_total.fillna(0, inplace=True)
|
|
569
|
+
resource_data.fillna(0, inplace=True)
|
|
570
|
+
|
|
571
|
+
# ac power with PVWatts inverter model
|
|
572
|
+
power_ac = pvlib.inverter.pvwatts(pdc_inv_total, pdc0, eta_inv_nom)
|
|
573
|
+
|
|
574
|
+
return power_ac, resource_data.copy()
|