or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

atmosphere.mdbifacial.mdclearsky.mdiam.mdindex.mdinverter.mdiotools.mdirradiance.mdlosses.mdpvsystem.mdsolar-position.mdspectrum.mdtemperature.md

bifacial.mddocs/

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

```