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/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()