0
# Bifacial PV Modeling
1
2
Model bifacial photovoltaic systems that generate power from both front and rear surfaces. Comprehensive tools for calculating rear-side irradiance, view factors, and bifacial power performance including advanced geometric modeling.
3
4
## Capabilities
5
6
### Infinite Sheds Model
7
8
Calculate irradiance for infinite rows of bifacial PV modules.
9
10
```python { .api }
11
def get_irradiance_poa(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth,
12
gcr, height, pitch, ghi, dhi, dni, albedo,
13
model='isotropic', npoints=100):
14
"""
15
Calculate plane-of-array irradiance for infinite sheds bifacial configuration.
16
17
Parameters:
18
- surface_tilt: numeric, surface tilt angle in degrees
19
- surface_azimuth: numeric, surface azimuth in degrees
20
- solar_zenith: numeric, solar zenith angle in degrees
21
- solar_azimuth: numeric, solar azimuth in degrees
22
- gcr: numeric, ground coverage ratio (0-1)
23
- height: numeric, module height above ground in meters
24
- pitch: numeric, row spacing in meters
25
- ghi: numeric, global horizontal irradiance in W/m²
26
- dhi: numeric, diffuse horizontal irradiance in W/m²
27
- dni: numeric, direct normal irradiance in W/m²
28
- albedo: numeric, ground reflectance (0-1)
29
- model: str, sky diffuse model ('isotropic', 'perez')
30
- npoints: int, number of discretization points
31
32
Returns:
33
dict with keys:
34
- poa_front: front-side plane-of-array irradiance
35
- poa_rear: rear-side plane-of-array irradiance
36
- poa_total: total bifacial plane-of-array irradiance
37
"""
38
39
def get_irradiance(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth,
40
gcr, height, pitch, ghi, dhi, dni, albedo,
41
iam_front=1.0, iam_rear=1.0, bifaciality=0.8):
42
"""
43
Calculate irradiance components for infinite sheds bifacial system.
44
45
Parameters:
46
- surface_tilt: numeric, surface tilt angle in degrees
47
- surface_azimuth: numeric, surface azimuth in degrees
48
- solar_zenith: numeric, solar zenith angle in degrees
49
- solar_azimuth: numeric, solar azimuth in degrees
50
- gcr: numeric, ground coverage ratio
51
- height: numeric, module height above ground in meters
52
- pitch: numeric, row spacing in meters
53
- ghi: numeric, global horizontal irradiance in W/m²
54
- dhi: numeric, diffuse horizontal irradiance in W/m²
55
- dni: numeric, direct normal irradiance in W/m²
56
- albedo: numeric, ground reflectance (0-1)
57
- iam_front: numeric, front-side incidence angle modifier
58
- iam_rear: numeric, rear-side incidence angle modifier
59
- bifaciality: numeric, rear/front power ratio under same irradiance
60
61
Returns:
62
dict with detailed irradiance breakdown including:
63
- front_direct, front_diffuse, front_reflected
64
- rear_direct, rear_diffuse, rear_reflected
65
- total_absorbed_front, total_absorbed_rear
66
"""
67
```
68
69
### PVFactors Integration
70
71
Interface with PVFactors library for detailed bifacial modeling.
72
73
```python { .api }
74
def pvfactors_timeseries(solar_azimuth, solar_zenith, surface_azimuth, surface_tilt,
75
axis_azimuth, timestamps, dni, dhi, gcr, pvrow_height,
76
pvrow_width, albedo, n_pvrows=3, index_observed_pvrow=1,
77
rho_front_pvrow=0.03, rho_back_pvrow=0.05,
78
horizon_band_angle=15.0, run_parallel_calculations=True,
79
n_workers_for_parallel_calcs=2):
80
"""
81
Run PVFactors timeseries simulation for detailed bifacial modeling.
82
83
Parameters:
84
- solar_azimuth: array-like, solar azimuth angles in degrees
85
- solar_zenith: array-like, solar zenith angles in degrees
86
- surface_azimuth: numeric, PV surface azimuth in degrees
87
- surface_tilt: numeric, PV surface tilt in degrees
88
- axis_azimuth: numeric, axis of rotation azimuth in degrees
89
- timestamps: pandas.DatetimeIndex, simulation timestamps
90
- dni: array-like, direct normal irradiance in W/m²
91
- dhi: array-like, diffuse horizontal irradiance in W/m²
92
- gcr: numeric, ground coverage ratio
93
- pvrow_height: numeric, PV row height in meters
94
- pvrow_width: numeric, PV row width in meters
95
- albedo: numeric, ground albedo (0-1)
96
- n_pvrows: int, number of PV rows to model
97
- index_observed_pvrow: int, index of PV row to report (0-based)
98
- rho_front_pvrow: numeric, front surface reflectance
99
- rho_back_pvrow: numeric, rear surface reflectance
100
- horizon_band_angle: numeric, discretization angle for horizon band
101
- run_parallel_calculations: bool, enable parallel processing
102
- n_workers_for_parallel_calcs: int, number of parallel workers
103
104
Returns:
105
pandas.DataFrame with columns:
106
- total_inc_front, total_inc_rear: incident irradiance on front/rear
107
- total_abs_front, total_abs_rear: absorbed irradiance on front/rear
108
"""
109
```
110
111
### View Factor Calculations
112
113
Calculate view factors between surfaces for bifacial irradiance modeling.
114
115
```python { .api }
116
def vf_ground_sky_2d(rotation, gcr, x, pitch, height, max_rows=10):
117
"""
118
Calculate 2D view factors between ground and sky.
119
120
Parameters:
121
- rotation: numeric, surface rotation angle in degrees
122
- gcr: numeric, ground coverage ratio
123
- x: numeric, position along ground (normalized by pitch)
124
- pitch: numeric, row spacing in meters
125
- height: numeric, PV module height in meters
126
- max_rows: int, maximum number of rows to consider
127
128
Returns:
129
numeric, view factor from ground point to sky
130
"""
131
132
def vf_ground_sky_2d_integ(surface_tilt, gcr, height, pitch, max_rows=10,
133
x0=0, x1=1):
134
"""
135
Calculate integrated 2D view factors between ground and sky.
136
137
Parameters:
138
- surface_tilt: numeric, surface tilt angle in degrees
139
- gcr: numeric, ground coverage ratio
140
- height: numeric, PV module height in meters
141
- pitch: numeric, row spacing in meters
142
- max_rows: int, maximum number of rows to consider
143
- x0: numeric, integration start position (normalized)
144
- x1: numeric, integration end position (normalized)
145
146
Returns:
147
numeric, integrated view factor
148
"""
149
150
def vf_row_sky_2d(surface_tilt, gcr, x):
151
"""
152
Calculate 2D view factors between PV row and sky.
153
154
Parameters:
155
- surface_tilt: numeric, surface tilt angle in degrees
156
- gcr: numeric, ground coverage ratio
157
- x: numeric, position along PV row (normalized)
158
159
Returns:
160
numeric, view factor from PV row point to sky
161
"""
162
163
def vf_row_ground_2d(surface_tilt, gcr, x):
164
"""
165
Calculate 2D view factors between PV row and ground.
166
167
Parameters:
168
- surface_tilt: numeric, surface tilt angle in degrees
169
- gcr: numeric, ground coverage ratio
170
- x: numeric, position along PV row (normalized)
171
172
Returns:
173
numeric, view factor from PV row point to ground
174
"""
175
176
def vf_row_sky_2d_integ(surface_tilt, gcr, x0=0, x1=1):
177
"""
178
Calculate integrated 2D view factors between PV row and sky.
179
180
Parameters:
181
- surface_tilt: numeric, surface tilt angle in degrees
182
- gcr: numeric, ground coverage ratio
183
- x0: numeric, integration start position (normalized)
184
- x1: numeric, integration end position (normalized)
185
186
Returns:
187
numeric, integrated view factor from PV row to sky
188
"""
189
190
def vf_row_ground_2d_integ(surface_tilt, gcr, x0=0, x1=1):
191
"""
192
Calculate integrated 2D view factors between PV row and ground.
193
194
Parameters:
195
- surface_tilt: numeric, surface tilt angle in degrees
196
- gcr: numeric, ground coverage ratio
197
- x0: numeric, integration start position (normalized)
198
- x1: numeric, integration end position (normalized)
199
200
Returns:
201
numeric, integrated view factor from PV row to ground
202
"""
203
```
204
205
### Bifacial Power Calculations
206
207
Calculate power output considering both front and rear irradiance.
208
209
```python { .api }
210
def power_mismatch_deline(poa_front, poa_rear, bifaciality=0.8,
211
n_rear_strings=None, mismatch_factor=0.98):
212
"""
213
Calculate power mismatch loss for bifacial modules using Deline model.
214
215
Parameters:
216
- poa_front: array-like, front-side plane-of-array irradiance
217
- poa_rear: array-like, rear-side plane-of-array irradiance
218
- bifaciality: numeric, rear/front power ratio under same irradiance
219
- n_rear_strings: int, number of rear-illuminated strings (optional)
220
- mismatch_factor: numeric, electrical mismatch factor
221
222
Returns:
223
dict with keys:
224
- p_mp_front: front-side power at maximum power point
225
- p_mp_rear: rear-side power at maximum power point
226
- p_mp_total: total bifacial power output
227
- mismatch_loss: power loss due to mismatch
228
"""
229
230
def effective_irradiance_bifacial(poa_front, poa_rear, bifaciality=0.8):
231
"""
232
Calculate effective irradiance for bifacial modules.
233
234
Parameters:
235
- poa_front: numeric, front-side plane-of-array irradiance in W/m²
236
- poa_rear: numeric, rear-side plane-of-array irradiance in W/m²
237
- bifaciality: numeric, rear/front power ratio
238
239
Returns:
240
numeric, effective irradiance in W/m²
241
"""
242
243
def bifacial_gain(poa_front, poa_rear, bifaciality=0.8):
244
"""
245
Calculate bifacial gain compared to monofacial equivalent.
246
247
Parameters:
248
- poa_front: numeric, front-side irradiance in W/m²
249
- poa_rear: numeric, rear-side irradiance in W/m²
250
- bifaciality: numeric, rear/front efficiency ratio
251
252
Returns:
253
numeric, bifacial gain factor (>1.0 indicates benefit)
254
"""
255
```
256
257
## Usage Examples
258
259
### Basic Bifacial System Analysis
260
261
```python
262
import pvlib
263
from pvlib import bifacial, solarposition, irradiance, atmosphere
264
import pandas as pd
265
import numpy as np
266
import matplotlib.pyplot as plt
267
268
# System parameters
269
lat, lon = 40.0150, -105.2705 # Boulder, CO
270
surface_tilt = 30 # degrees
271
surface_azimuth = 180 # south-facing
272
gcr = 0.4 # ground coverage ratio
273
height = 1.5 # module height above ground in meters
274
pitch = height / (gcr * np.cos(np.radians(surface_tilt))) # calculated pitch
275
albedo = 0.25 # ground reflectance
276
bifaciality = 0.8 # rear/front efficiency ratio
277
278
# Time series for one day
279
times = pd.date_range('2023-06-21 05:00', '2023-06-21 19:00',
280
freq='H', tz='US/Mountain')
281
282
# Calculate solar position
283
solar_pos = solarposition.get_solarposition(times, lat, lon)
284
285
# Get clear sky irradiance
286
clear_sky = pvlib.clearsky.ineichen(times, lat, lon, altitude=1655)
287
288
# Calculate front-side POA irradiance (traditional method)
289
poa_front_traditional = irradiance.get_total_irradiance(
290
surface_tilt, surface_azimuth,
291
solar_pos['zenith'], solar_pos['azimuth'],
292
clear_sky['dni'], clear_sky['ghi'], clear_sky['dhi'],
293
albedo=albedo
294
)
295
296
# Calculate bifacial irradiance using infinite sheds model
297
bifacial_results = []
298
299
for i, time in enumerate(times):
300
# Get irradiance for this timestep
301
result = bifacial.infinite_sheds.get_irradiance_poa(
302
surface_tilt=surface_tilt,
303
surface_azimuth=surface_azimuth,
304
solar_zenith=solar_pos['zenith'].iloc[i],
305
solar_azimuth=solar_pos['azimuth'].iloc[i],
306
gcr=gcr,
307
height=height,
308
pitch=pitch,
309
ghi=clear_sky['ghi'].iloc[i],
310
dhi=clear_sky['dhi'].iloc[i],
311
dni=clear_sky['dni'].iloc[i],
312
albedo=albedo,
313
model='isotropic'
314
)
315
316
bifacial_results.append({
317
'time': time,
318
'poa_front': result['poa_front'],
319
'poa_rear': result['poa_rear'],
320
'poa_total': result['poa_total']
321
})
322
323
# Convert to DataFrame
324
bifacial_df = pd.DataFrame(bifacial_results)
325
bifacial_df.set_index('time', inplace=True)
326
327
# Calculate bifacial gains
328
bifacial_df['effective_irradiance'] = bifacial.effective_irradiance_bifacial(
329
bifacial_df['poa_front'], bifacial_df['poa_rear'], bifaciality
330
)
331
332
bifacial_df['bifacial_gain'] = bifacial.bifacial_gain(
333
bifacial_df['poa_front'], bifacial_df['poa_rear'], bifaciality
334
)
335
336
# Calculate daily totals
337
daily_totals = {
338
'Front POA': bifacial_df['poa_front'].sum() / 1000, # kWh/m²
339
'Rear POA': bifacial_df['poa_rear'].sum() / 1000,
340
'Effective': bifacial_df['effective_irradiance'].sum() / 1000,
341
'Traditional': poa_front_traditional['poa_global'].sum() / 1000
342
}
343
344
print("Daily Irradiation Summary (kWh/m²):")
345
for key, value in daily_totals.items():
346
print(f"{key:12s}: {value:.2f}")
347
348
daily_gain = (daily_totals['Effective'] - daily_totals['Traditional']) / daily_totals['Traditional'] * 100
349
print(f"\nDaily Bifacial Gain: {daily_gain:.1f}%")
350
351
# Plot results
352
fig, axes = plt.subplots(3, 1, figsize=(12, 10))
353
354
# Irradiance components
355
axes[0].plot(bifacial_df.index, bifacial_df['poa_front'],
356
label='Front POA', linewidth=2, color='blue')
357
axes[0].plot(bifacial_df.index, bifacial_df['poa_rear'],
358
label='Rear POA', linewidth=2, color='red')
359
axes[0].plot(bifacial_df.index, poa_front_traditional['poa_global'],
360
label='Traditional POA', linewidth=2, linestyle='--', color='gray')
361
axes[0].set_ylabel('Irradiance (W/m²)')
362
axes[0].set_title('Bifacial vs Traditional Irradiance')
363
axes[0].legend()
364
axes[0].grid(True)
365
366
# Effective irradiance and gain
367
axes[1].plot(bifacial_df.index, bifacial_df['effective_irradiance'],
368
label='Effective Irradiance', linewidth=2, color='green')
369
axes[1].set_ylabel('Effective Irradiance (W/m²)')
370
axes[1].set_title('Bifacial Effective Irradiance')
371
axes[1].legend()
372
axes[1].grid(True)
373
374
# Bifacial gain factor
375
axes[2].plot(bifacial_df.index, bifacial_df['bifacial_gain'],
376
'o-', linewidth=2, color='purple')
377
axes[2].set_ylabel('Bifacial Gain Factor')
378
axes[2].set_xlabel('Time')
379
axes[2].set_title('Bifacial Gain Throughout Day')
380
axes[2].grid(True)
381
axes[2].axhline(y=1.0, color='k', linestyle='--', alpha=0.5)
382
383
plt.tight_layout()
384
plt.show()
385
386
# Analyze rear irradiance sources
387
print(f"\nRear Irradiance Analysis:")
388
print(f"Peak rear irradiance: {bifacial_df['poa_rear'].max():.1f} W/m²")
389
print(f"Average rear/front ratio: {(bifacial_df['poa_rear'] / bifacial_df['poa_front']).mean():.3f}")
390
print(f"Peak bifacial gain: {bifacial_df['bifacial_gain'].max():.3f}")
391
```
392
393
### Advanced PVFactors Modeling
394
395
```python
396
import pvlib
397
from pvlib import bifacial, solarposition, iotools
398
import pandas as pd
399
import numpy as np
400
import matplotlib.pyplot as plt
401
402
# Load weather data
403
lat, lon = 37.8756, -122.2441 # Berkeley, CA
404
api_key = 'DEMO_KEY' # Replace with actual API key
405
email = 'user@example.com'
406
407
# Get one week of weather data
408
try:
409
weather_data, metadata = iotools.get_psm3(
410
lat, lon, api_key, email,
411
names=2023,
412
attributes=['ghi', 'dni', 'dhi'],
413
leap_day=False
414
)
415
# Select one week in summer
416
start_date = '2023-06-15'
417
end_date = '2023-06-22'
418
weather_week = weather_data[start_date:end_date]
419
420
except:
421
# Fallback to generated clear sky data
422
print("Using clear sky data (replace with actual weather data)")
423
times = pd.date_range('2023-06-15', '2023-06-22', freq='H', tz='US/Pacific')
424
clear_sky = pvlib.clearsky.ineichen(times, lat, lon)
425
weather_week = clear_sky.rename(columns={'ghi': 'ghi', 'dni': 'dni', 'dhi': 'dhi'})
426
427
# System configuration
428
system_params = {
429
'surface_tilt': 25, # degrees
430
'surface_azimuth': 180, # south-facing
431
'axis_azimuth': 180, # tracking axis orientation
432
'gcr': 0.35, # ground coverage ratio
433
'pvrow_height': 2.0, # meters
434
'pvrow_width': 4.0, # meters
435
'albedo': 0.20, # ground reflectance
436
'n_pvrows': 5, # number of rows to model
437
'index_observed_pvrow': 2, # middle row (0-indexed)
438
'bifaciality': 0.85 # rear/front efficiency ratio
439
}
440
441
# Calculate solar position
442
solar_pos = solarposition.get_solarposition(weather_week.index, lat, lon)
443
444
# Run PVFactors simulation
445
print("Running PVFactors simulation...")
446
try:
447
pvfactors_results = bifacial.pvfactors.pvfactors_timeseries(
448
solar_azimuth=solar_pos['azimuth'],
449
solar_zenith=solar_pos['zenith'],
450
surface_azimuth=system_params['surface_azimuth'],
451
surface_tilt=system_params['surface_tilt'],
452
axis_azimuth=system_params['axis_azimuth'],
453
timestamps=weather_week.index,
454
dni=weather_week['dni'],
455
dhi=weather_week['dhi'],
456
gcr=system_params['gcr'],
457
pvrow_height=system_params['pvrow_height'],
458
pvrow_width=system_params['pvrow_width'],
459
albedo=system_params['albedo'],
460
n_pvrows=system_params['n_pvrows'],
461
index_observed_pvrow=system_params['index_observed_pvrow'],
462
run_parallel_calculations=True
463
)
464
465
pvfactors_available = True
466
467
except Exception as e:
468
print(f"PVFactors not available: {e}")
469
print("Using simplified infinite sheds model...")
470
pvfactors_available = False
471
472
# Compare with infinite sheds model for validation
473
infinite_sheds_results = []
474
475
for i, (timestamp, weather_row) in enumerate(weather_week.iterrows()):
476
if i % 24 == 0: # Progress indicator
477
print(f"Processing day {i//24 + 1}/7...")
478
479
result = bifacial.infinite_sheds.get_irradiance(
480
surface_tilt=system_params['surface_tilt'],
481
surface_azimuth=system_params['surface_azimuth'],
482
solar_zenith=solar_pos['zenith'].iloc[i],
483
solar_azimuth=solar_pos['azimuth'].iloc[i],
484
gcr=system_params['gcr'],
485
height=system_params['pvrow_height'],
486
pitch=system_params['pvrow_width'] / system_params['gcr'],
487
ghi=weather_row['ghi'],
488
dhi=weather_row['dhi'],
489
dni=weather_row['dni'],
490
albedo=system_params['albedo'],
491
bifaciality=system_params['bifaciality']
492
)
493
494
infinite_sheds_results.append({
495
'timestamp': timestamp,
496
'front_poa': result['total_absorbed_front'],
497
'rear_poa': result['total_absorbed_rear']
498
})
499
500
infinite_df = pd.DataFrame(infinite_sheds_results)
501
infinite_df.set_index('timestamp', inplace=True)
502
503
# Calculate performance metrics
504
if pvfactors_available:
505
# PVFactors results
506
pvf_front = pvfactors_results['total_abs_front']
507
pvf_rear = pvfactors_results['total_abs_rear']
508
509
# Effective irradiance
510
pvf_effective = pvf_front + system_params['bifaciality'] * pvf_rear
511
512
# Compare models
513
comparison_df = pd.DataFrame({
514
'pvf_front': pvf_front,
515
'pvf_rear': pvf_rear,
516
'pvf_effective': pvf_effective,
517
'inf_front': infinite_df['front_poa'],
518
'inf_rear': infinite_df['rear_poa'],
519
'inf_effective': infinite_df['front_poa'] + system_params['bifaciality'] * infinite_df['rear_poa']
520
})
521
522
# Statistical comparison
523
print("\nModel Comparison Statistics:")
524
print("="*40)
525
526
for component in ['front', 'rear', 'effective']:
527
pvf_col = f'pvf_{component}'
528
inf_col = f'inf_{component}'
529
530
mae = np.mean(np.abs(comparison_df[pvf_col] - comparison_df[inf_col]))
531
rmse = np.sqrt(np.mean((comparison_df[pvf_col] - comparison_df[inf_col])**2))
532
mbe = np.mean(comparison_df[pvf_col] - comparison_df[inf_col])
533
534
print(f"{component.title()} Irradiance:")
535
print(f" MAE: {mae:.2f} W/m²")
536
print(f" RMSE: {rmse:.2f} W/m²")
537
print(f" MBE: {mbe:.2f} W/m²")
538
539
# Plot detailed analysis
540
if pvfactors_available:
541
fig, axes = plt.subplots(4, 1, figsize=(15, 12))
542
543
# Daily profiles comparison
544
sample_day = comparison_df.index.date[len(comparison_df)//2] # Middle day
545
day_data = comparison_df[comparison_df.index.date == sample_day]
546
547
axes[0].plot(day_data.index.hour, day_data['pvf_front'],
548
'b-', label='PVFactors Front', linewidth=2)
549
axes[0].plot(day_data.index.hour, day_data['inf_front'],
550
'b--', label='Infinite Sheds Front', linewidth=2)
551
axes[0].set_ylabel('Front Irradiance (W/m²)')
552
axes[0].set_title(f'Front Irradiance Comparison - {sample_day}')
553
axes[0].legend()
554
axes[0].grid(True)
555
556
axes[1].plot(day_data.index.hour, day_data['pvf_rear'],
557
'r-', label='PVFactors Rear', linewidth=2)
558
axes[1].plot(day_data.index.hour, day_data['inf_rear'],
559
'r--', label='Infinite Sheds Rear', linewidth=2)
560
axes[1].set_ylabel('Rear Irradiance (W/m²)')
561
axes[1].set_title('Rear Irradiance Comparison')
562
axes[1].legend()
563
axes[1].grid(True)
564
565
# Scatter plots
566
axes[2].scatter(comparison_df['inf_front'], comparison_df['pvf_front'],
567
alpha=0.6, s=10, label='Front')
568
axes[2].scatter(comparison_df['inf_rear'], comparison_df['pvf_rear'],
569
alpha=0.6, s=10, label='Rear')
570
571
max_val = max(comparison_df[['pvf_front', 'pvf_rear', 'inf_front', 'inf_rear']].max())
572
axes[2].plot([0, max_val], [0, max_val], 'k--', alpha=0.5)
573
axes[2].set_xlabel('Infinite Sheds (W/m²)')
574
axes[2].set_ylabel('PVFactors (W/m²)')
575
axes[2].set_title('Model Correlation')
576
axes[2].legend()
577
axes[2].grid(True)
578
579
# Weekly energy totals
580
daily_totals = comparison_df.resample('D').sum() / 1000 # kWh/m²/day
581
582
x_days = range(len(daily_totals))
583
width = 0.35
584
585
axes[3].bar([x - width/2 for x in x_days], daily_totals['pvf_effective'],
586
width, label='PVFactors', alpha=0.7)
587
axes[3].bar([x + width/2 for x in x_days], daily_totals['inf_effective'],
588
width, label='Infinite Sheds', alpha=0.7)
589
590
axes[3].set_xlabel('Day')
591
axes[3].set_ylabel('Daily Energy (kWh/m²/day)')
592
axes[3].set_title('Daily Effective Energy Comparison')
593
axes[3].legend()
594
axes[3].grid(True, axis='y')
595
596
plt.tight_layout()
597
plt.show()
598
599
# Weekly summary
600
weekly_totals = {
601
'PVFactors Front': comparison_df['pvf_front'].sum() / 1000,
602
'PVFactors Rear': comparison_df['pvf_rear'].sum() / 1000,
603
'PVFactors Effective': comparison_df['pvf_effective'].sum() / 1000,
604
'Infinite Sheds Front': comparison_df['inf_front'].sum() / 1000,
605
'Infinite Sheds Rear': comparison_df['inf_rear'].sum() / 1000,
606
'Infinite Sheds Effective': comparison_df['inf_effective'].sum() / 1000
607
}
608
609
print(f"\nWeekly Energy Summary (kWh/m²):")
610
print("="*40)
611
for key, value in weekly_totals.items():
612
print(f"{key:25s}: {value:.2f}")
613
614
else:
615
# Plot infinite sheds results only
616
fig, axes = plt.subplots(3, 1, figsize=(12, 10))
617
618
# Calculate effective irradiance
619
infinite_df['effective'] = (infinite_df['front_poa'] +
620
system_params['bifaciality'] * infinite_df['rear_poa'])
621
622
# Time series
623
axes[0].plot(infinite_df.index, infinite_df['front_poa'],
624
label='Front POA', linewidth=1.5)
625
axes[0].plot(infinite_df.index, infinite_df['rear_poa'],
626
label='Rear POA', linewidth=1.5)
627
axes[0].set_ylabel('Irradiance (W/m²)')
628
axes[0].set_title('Bifacial Irradiance - Infinite Sheds Model')
629
axes[0].legend()
630
axes[0].grid(True)
631
632
# Daily totals
633
daily_totals = infinite_df.resample('D').sum() / 1000
634
635
axes[1].bar(range(len(daily_totals)), daily_totals['front_poa'],
636
alpha=0.7, label='Front')
637
axes[1].bar(range(len(daily_totals)), daily_totals['rear_poa'],
638
bottom=daily_totals['front_poa'], alpha=0.7, label='Rear')
639
axes[1].set_xlabel('Day')
640
axes[1].set_ylabel('Daily Energy (kWh/m²/day)')
641
axes[1].set_title('Daily Energy Breakdown')
642
axes[1].legend()
643
axes[1].grid(True, axis='y')
644
645
# Bifacial gain analysis
646
bifacial_gain = infinite_df['effective'] / infinite_df['front_poa']
647
axes[2].plot(infinite_df.index, bifacial_gain, 'g-', linewidth=1.5)
648
axes[2].set_ylabel('Bifacial Gain Factor')
649
axes[2].set_xlabel('Time')
650
axes[2].set_title('Bifacial Gain Throughout Week')
651
axes[2].grid(True)
652
axes[2].axhline(y=1.0, color='k', linestyle='--', alpha=0.5)
653
654
plt.tight_layout()
655
plt.show()
656
657
print(f"\nWeekly Summary (Infinite Sheds Model):")
658
print("="*40)
659
print(f"Front Energy: {daily_totals['front_poa'].sum():.2f} kWh/m²")
660
print(f"Rear Energy: {daily_totals['rear_poa'].sum():.2f} kWh/m²")
661
print(f"Effective Energy: {daily_totals['effective'].sum():.2f} kWh/m²")
662
print(f"Average Bifacial Gain: {bifacial_gain.mean():.3f}")
663
print(f"Peak Bifacial Gain: {bifacial_gain.max():.3f}")
664
```
665
666
### View Factor Analysis and Optimization
667
668
```python
669
import pvlib
670
from pvlib import bifacial
671
import numpy as np
672
import matplotlib.pyplot as plt
673
from scipy.optimize import minimize_scalar
674
675
# Parameter ranges for analysis
676
surface_tilts = np.arange(0, 61, 5) # 0 to 60 degrees
677
gcrs = np.arange(0.1, 0.81, 0.05) # 0.1 to 0.8
678
heights = np.arange(0.5, 4.1, 0.25) # 0.5 to 4.0 meters
679
680
# Fixed parameters
681
pitch_base = 5.0 # meters
682
683
def analyze_view_factors(surface_tilt, gcr, height):
684
"""
685
Analyze view factors for given system configuration.
686
"""
687
pitch = height / (gcr * np.cos(np.radians(surface_tilt)))
688
689
# Ground view factors at different positions
690
x_positions = np.linspace(0, 1, 21) # 0 to 1 (normalized by pitch)
691
vf_ground_sky = []
692
693
for x in x_positions:
694
vf = bifacial.utils.vf_ground_sky_2d(
695
rotation=surface_tilt, gcr=gcr, x=x,
696
pitch=pitch, height=height, max_rows=10
697
)
698
vf_ground_sky.append(vf)
699
700
# Row view factors
701
vf_row_sky_integrated = bifacial.utils.vf_row_sky_2d_integ(surface_tilt, gcr)
702
vf_row_ground_integrated = bifacial.utils.vf_row_ground_2d_integ(surface_tilt, gcr)
703
704
# Ground integrated view factor
705
vf_ground_sky_integrated = bifacial.utils.vf_ground_sky_2d_integ(
706
surface_tilt, gcr, height, pitch, max_rows=10
707
)
708
709
return {
710
'vf_ground_sky_positions': vf_ground_sky,
711
'x_positions': x_positions,
712
'vf_row_sky': vf_row_sky_integrated,
713
'vf_row_ground': vf_row_ground_integrated,
714
'vf_ground_sky_avg': vf_ground_sky_integrated,
715
'pitch': pitch
716
}
717
718
# Analyze view factor sensitivity
719
print("Analyzing view factor sensitivity...")
720
721
# Effect of tilt angle
722
tilt_analysis = []
723
for tilt in surface_tilts:
724
result = analyze_view_factors(tilt, gcr=0.4, height=2.0)
725
tilt_analysis.append({
726
'tilt': tilt,
727
'vf_row_sky': result['vf_row_sky'],
728
'vf_row_ground': result['vf_row_ground'],
729
'vf_ground_sky': result['vf_ground_sky_avg']
730
})
731
732
# Effect of GCR
733
gcr_analysis = []
734
for gcr in gcrs:
735
result = analyze_view_factors(surface_tilt=30, gcr=gcr, height=2.0)
736
gcr_analysis.append({
737
'gcr': gcr,
738
'vf_row_sky': result['vf_row_sky'],
739
'vf_row_ground': result['vf_row_ground'],
740
'vf_ground_sky': result['vf_ground_sky_avg']
741
})
742
743
# Effect of height
744
height_analysis = []
745
for height in heights:
746
result = analyze_view_factors(surface_tilt=30, gcr=0.4, height=height)
747
height_analysis.append({
748
'height': height,
749
'vf_row_sky': result['vf_row_sky'],
750
'vf_row_ground': result['vf_row_ground'],
751
'vf_ground_sky': result['vf_ground_sky_avg']
752
})
753
754
# Plot view factor analysis
755
fig, axes = plt.subplots(2, 3, figsize=(18, 12))
756
757
# Tilt angle effects
758
tilts = [item['tilt'] for item in tilt_analysis]
759
axes[0, 0].plot(tilts, [item['vf_row_sky'] for item in tilt_analysis],
760
'b-o', label='Row→Sky', linewidth=2)
761
axes[0, 0].plot(tilts, [item['vf_row_ground'] for item in tilt_analysis],
762
'r-s', label='Row→Ground', linewidth=2)
763
axes[0, 0].plot(tilts, [item['vf_ground_sky'] for item in tilt_analysis],
764
'g-^', label='Ground→Sky', linewidth=2)
765
axes[0, 0].set_xlabel('Surface Tilt (degrees)')
766
axes[0, 0].set_ylabel('View Factor')
767
axes[0, 0].set_title('View Factors vs Surface Tilt')
768
axes[0, 0].legend()
769
axes[0, 0].grid(True)
770
771
# GCR effects
772
gcr_values = [item['gcr'] for item in gcr_analysis]
773
axes[0, 1].plot(gcr_values, [item['vf_row_sky'] for item in gcr_analysis],
774
'b-o', label='Row→Sky', linewidth=2)
775
axes[0, 1].plot(gcr_values, [item['vf_row_ground'] for item in gcr_analysis],
776
'r-s', label='Row→Ground', linewidth=2)
777
axes[0, 1].plot(gcr_values, [item['vf_ground_sky'] for item in gcr_analysis],
778
'g-^', label='Ground→Sky', linewidth=2)
779
axes[0, 1].set_xlabel('Ground Coverage Ratio')
780
axes[0, 1].set_ylabel('View Factor')
781
axes[0, 1].set_title('View Factors vs GCR')
782
axes[0, 1].legend()
783
axes[0, 1].grid(True)
784
785
# Height effects
786
height_values = [item['height'] for item in height_analysis]
787
axes[0, 2].plot(height_values, [item['vf_row_sky'] for item in height_analysis],
788
'b-o', label='Row→Sky', linewidth=2)
789
axes[0, 2].plot(height_values, [item['vf_row_ground'] for item in height_analysis],
790
'r-s', label='Row→Ground', linewidth=2)
791
axes[0, 2].plot(height_values, [item['vf_ground_sky'] for item in height_analysis],
792
'g-^', label='Ground→Sky', linewidth=2)
793
axes[0, 2].set_xlabel('Module Height (m)')
794
axes[0, 2].set_ylabel('View Factor')
795
axes[0, 2].set_title('View Factors vs Height')
796
axes[0, 2].legend()
797
axes[0, 2].grid(True)
798
799
# Detailed position analysis for specific configuration
800
detail_config = analyze_view_factors(surface_tilt=30, gcr=0.4, height=2.0)
801
802
# Ground view factors along pitch
803
axes[1, 0].plot(detail_config['x_positions'], detail_config['vf_ground_sky_positions'],
804
'ko-', linewidth=2, markersize=6)
805
axes[1, 0].set_xlabel('Position (normalized by pitch)')
806
axes[1, 0].set_ylabel('Ground→Sky View Factor')
807
axes[1, 0].set_title('Ground View Factor Variation')
808
axes[1, 0].grid(True)
809
810
# 2D heatmap of rear irradiance benefit (simplified model)
811
tilt_grid, gcr_grid = np.meshgrid(np.arange(10, 51, 5), np.arange(0.2, 0.71, 0.05))
812
benefit_matrix = np.zeros_like(tilt_grid)
813
814
for i, gcr_val in enumerate(np.arange(0.2, 0.71, 0.05)):
815
for j, tilt_val in enumerate(np.arange(10, 51, 5)):
816
result = analyze_view_factors(tilt_val, gcr_val, height=2.0)
817
# Simplified rear irradiance benefit metric
818
benefit = result['vf_row_ground'] * result['vf_ground_sky_avg']
819
benefit_matrix[i, j] = benefit
820
821
contour = axes[1, 1].contourf(tilt_grid, gcr_grid, benefit_matrix, levels=20, cmap='viridis')
822
axes[1, 1].set_xlabel('Surface Tilt (degrees)')
823
axes[1, 1].set_ylabel('Ground Coverage Ratio')
824
axes[1, 1].set_title('Rear Irradiance Benefit (Relative)')
825
plt.colorbar(contour, ax=axes[1, 1])
826
827
# Optimization example - find optimal GCR for maximum rear benefit
828
def rear_benefit_objective(gcr, tilt=30, height=2.0):
829
"""
830
Objective function for rear irradiance benefit (to maximize).
831
Returns negative value for minimization.
832
"""
833
result = analyze_view_factors(tilt, gcr, height)
834
# Simple benefit metric: product of relevant view factors
835
benefit = result['vf_row_ground'] * result['vf_ground_sky_avg']
836
return -benefit # Negative for minimization
837
838
# Find optimal GCR for different tilt angles
839
optimal_gcrs = []
840
for tilt in np.arange(10, 51, 10):
841
opt_result = minimize_scalar(
842
rear_benefit_objective,
843
bounds=(0.1, 0.8),
844
method='bounded',
845
args=(tilt, 2.0)
846
)
847
optimal_gcrs.append({
848
'tilt': tilt,
849
'optimal_gcr': opt_result.x,
850
'benefit': -opt_result.fun
851
})
852
853
opt_tilts = [item['tilt'] for item in optimal_gcrs]
854
opt_gcr_values = [item['optimal_gcr'] for item in optimal_gcrs]
855
856
axes[1, 2].plot(opt_tilts, opt_gcr_values, 'ro-', linewidth=2, markersize=8)
857
axes[1, 2].set_xlabel('Surface Tilt (degrees)')
858
axes[1, 2].set_ylabel('Optimal GCR')
859
axes[1, 2].set_title('Optimal GCR vs Tilt for Max Rear Benefit')
860
axes[1, 2].grid(True)
861
862
plt.tight_layout()
863
plt.show()
864
865
# Print optimization results
866
print("\nView Factor Optimization Results:")
867
print("="*50)
868
print("Optimal GCR for maximum rear irradiance benefit:")
869
for item in optimal_gcrs:
870
print(f"Tilt {item['tilt']:2.0f}°: Optimal GCR = {item['optimal_gcr']:.3f}, "
871
f"Benefit = {item['benefit']:.4f}")
872
873
# Configuration recommendations
874
print(f"\nConfiguration Recommendations:")
875
print("="*30)
876
print("High rear irradiance configurations:")
877
print("- Lower GCR (0.3-0.5) increases ground view of sky")
878
print("- Moderate tilt (20-35°) balances front and rear performance")
879
print("- Higher mounting (2-3m) reduces inter-row shading")
880
print("- Light-colored ground surface increases reflected irradiance")
881
882
# Practical example with recommendations
883
print(f"\nPractical Design Example:")
884
print("="*25)
885
recommended_config = analyze_view_factors(surface_tilt=25, gcr=0.35, height=2.5)
886
print(f"Recommended: 25° tilt, GCR=0.35, height=2.5m")
887
print(f"Row→Ground VF: {recommended_config['vf_row_ground']:.3f}")
888
print(f"Ground→Sky VF: {recommended_config['vf_ground_sky_avg']:.3f}")
889
print(f"Row→Sky VF: {recommended_config['vf_row_sky']:.3f}")
890
print(f"Pitch: {recommended_config['pitch']:.1f} m")
891
```