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

iam.mddocs/

0

# Incidence Angle Modifier Models

1

2

Models for calculating the incidence angle modifier (IAM) which quantifies the fraction of direct irradiance transmitted through module materials to the cells as a function of the angle of incidence. Essential for accurate modeling of module optical losses.

3

4

## Capabilities

5

6

### ASHRAE Model

7

8

Simple empirical model using ASHRAE transmission approach with single parameter adjustment.

9

10

```python { .api }

11

def ashrae(aoi, b=0.05):

12

"""

13

ASHRAE incidence angle modifier model.

14

15

Parameters:

16

- aoi: numeric, angle of incidence in degrees

17

- b: float, parameter to adjust IAM vs AOI (default 0.05)

18

19

Returns:

20

- iam: numeric, incident angle modifier (0-1)

21

"""

22

```

23

24

### Physical Model

25

26

Physics-based model using refractive index, extinction coefficient, and glazing thickness with optional anti-reflective coating support.

27

28

```python { .api }

29

def physical(aoi, n=1.526, K=4.0, L=0.002, *, n_ar=None):

30

"""

31

Physical IAM model based on Fresnel reflections and absorption.

32

33

Parameters:

34

- aoi: numeric, angle of incidence in degrees

35

- n: numeric, effective refractive index (default 1.526 for glass)

36

- K: numeric, glazing extinction coefficient in 1/m (default 4.0)

37

- L: numeric, glazing thickness in meters (default 0.002)

38

- n_ar: numeric, refractive index of AR coating (optional)

39

40

Returns:

41

- iam: numeric, incident angle modifier

42

"""

43

```

44

45

### Martin-Ruiz Model

46

47

Analytical model providing good balance between simplicity and accuracy using exponential formulation.

48

49

```python { .api }

50

def martin_ruiz(aoi, a_r=0.16):

51

"""

52

Martin and Ruiz incidence angle model.

53

54

Parameters:

55

- aoi: numeric, angle of incidence in degrees

56

- a_r: numeric, angular losses coefficient (0.08-0.25 typical)

57

58

Returns:

59

- iam: numeric, incident angle modifier

60

"""

61

62

def martin_ruiz_diffuse(surface_tilt, a_r=0.16, c1=0.4244, c2=None):

63

"""

64

Martin-Ruiz diffuse IAM factors for sky and ground irradiance.

65

66

Parameters:

67

- surface_tilt: numeric, surface tilt angle in degrees

68

- a_r: numeric, angular losses coefficient

69

- c1: float, first fitting parameter (default 0.4244)

70

- c2: float, second fitting parameter (calculated if None)

71

72

Returns:

73

- iam_sky: numeric, IAM for sky diffuse irradiance

74

- iam_ground: numeric, IAM for ground-reflected diffuse irradiance

75

"""

76

```

77

78

### SAPM IAM Model

79

80

Sandia Array Performance Model IAM using polynomial approach with module-specific coefficients.

81

82

```python { .api }

83

def sapm(aoi, module, upper=None):

84

"""

85

SAPM incidence angle modifier model.

86

87

Parameters:

88

- aoi: numeric, angle of incidence in degrees

89

- module: dict, module parameters containing B0-B5 coefficients

90

- upper: float, upper limit on results (optional)

91

92

Returns:

93

- iam: numeric, SAPM angle of incidence loss coefficient (F2)

94

"""

95

```

96

97

### Interpolation Model

98

99

IAM calculation by interpolating measured reference values with various interpolation methods.

100

101

```python { .api }

102

def interp(aoi, theta_ref, iam_ref, method='linear', normalize=True):

103

"""

104

IAM by interpolating reference measurements.

105

106

Parameters:

107

- aoi: numeric, angle of incidence in degrees

108

- theta_ref: numeric, reference angles where IAM is known

109

- iam_ref: numeric, IAM values at reference angles

110

- method: str, interpolation method ('linear', 'quadratic', 'cubic')

111

- normalize: bool, normalize to IAM=1 at normal incidence

112

113

Returns:

114

- iam: numeric, interpolated incident angle modifier

115

"""

116

```

117

118

### Schlick Approximation

119

120

Computationally efficient Fresnel approximation with analytical integration capability for diffuse irradiance.

121

122

```python { .api }

123

def schlick(aoi):

124

"""

125

Schlick approximation to Fresnel equations for IAM.

126

127

Parameters:

128

- aoi: numeric, angle of incidence in degrees

129

130

Returns:

131

- iam: numeric, incident angle modifier

132

"""

133

134

def schlick_diffuse(surface_tilt):

135

"""

136

Analytical integration of Schlick model for diffuse IAM.

137

138

Parameters:

139

- surface_tilt: numeric, surface tilt angle in degrees

140

141

Returns:

142

- iam_sky: numeric, IAM for sky diffuse irradiance

143

- iam_ground: numeric, IAM for ground-reflected diffuse irradiance

144

"""

145

```

146

147

### Diffuse Irradiance Integration

148

149

Numerical integration methods for calculating diffuse IAM factors using Marion's solid angle approach.

150

151

```python { .api }

152

def marion_diffuse(model, surface_tilt, **kwargs):

153

"""

154

Diffuse IAM using Marion's integration method.

155

156

Parameters:

157

- model: str, IAM model name ('ashrae', 'physical', 'martin_ruiz', 'sapm', 'schlick')

158

- surface_tilt: numeric, surface tilt angle in degrees

159

- **kwargs: additional parameters for the IAM model

160

161

Returns:

162

- iam: dict with keys 'sky', 'horizon', 'ground' containing IAM values

163

"""

164

165

def marion_integrate(function, surface_tilt, region, num=None):

166

"""

167

Integrate IAM function over solid angle region.

168

169

Parameters:

170

- function: callable, IAM function to integrate

171

- surface_tilt: numeric, surface tilt angle in degrees

172

- region: str, integration region ('sky', 'horizon', 'ground')

173

- num: int, number of integration increments

174

175

Returns:

176

- iam: numeric, diffuse correction factor for specified region

177

"""

178

```

179

180

### Model Conversion and Fitting

181

182

Tools for converting between IAM models and fitting models to measured data.

183

184

```python { .api }

185

def convert(source_name, source_params, target_name, weight=None,

186

fix_n=True, xtol=None):

187

"""

188

Convert parameters from one IAM model to another.

189

190

Parameters:

191

- source_name: str, source model ('ashrae', 'martin_ruiz', 'physical')

192

- source_params: dict, parameters for source model

193

- target_name: str, target model ('ashrae', 'martin_ruiz', 'physical')

194

- weight: function, weighting function for residuals

195

- fix_n: bool, fix refractive index when converting to physical model

196

- xtol: float, optimization tolerance

197

198

Returns:

199

- dict, parameters for target model

200

"""

201

202

def fit(measured_aoi, measured_iam, model_name, weight=None, xtol=None):

203

"""

204

Fit IAM model parameters to measured data.

205

206

Parameters:

207

- measured_aoi: array-like, measured angles of incidence in degrees

208

- measured_iam: array-like, measured IAM values

209

- model_name: str, model to fit ('ashrae', 'martin_ruiz', 'physical')

210

- weight: function, weighting function for residuals

211

- xtol: float, optimization tolerance

212

213

Returns:

214

- dict, fitted model parameters

215

"""

216

```

217

218

## Usage Examples

219

220

### Basic IAM Model Comparison

221

222

```python

223

import pvlib

224

from pvlib import iam

225

import numpy as np

226

import matplotlib.pyplot as plt

227

228

# Angle of incidence range

229

aoi = np.linspace(0, 90, 91)

230

231

# Calculate IAM using different models

232

iam_ashrae = iam.ashrae(aoi, b=0.05)

233

iam_physical = iam.physical(aoi, n=1.526, K=4.0, L=0.002)

234

iam_martin_ruiz = iam.martin_ruiz(aoi, a_r=0.16)

235

236

# Plot comparison

237

plt.figure(figsize=(10, 6))

238

plt.plot(aoi, iam_ashrae, 'b-', label='ASHRAE (b=0.05)', linewidth=2)

239

plt.plot(aoi, iam_physical, 'r--', label='Physical (n=1.526, K=4, L=2mm)', linewidth=2)

240

plt.plot(aoi, iam_martin_ruiz, 'g:', label='Martin-Ruiz (a_r=0.16)', linewidth=2)

241

plt.xlabel('Angle of Incidence (degrees)')

242

plt.ylabel('Incidence Angle Modifier')

243

plt.title('IAM Model Comparison')

244

plt.legend()

245

plt.grid(True)

246

plt.xlim(0, 90)

247

plt.ylim(0, 1)

248

plt.show()

249

250

print("IAM Values at Key Angles:")

251

print("AOI (°) ASHRAE Physical Martin-Ruiz")

252

for angle in [0, 30, 45, 60, 75, 90]:

253

idx = angle

254

print(f"{angle:5.0f} {iam_ashrae[idx]:.3f} {iam_physical[idx]:.3f} {iam_martin_ruiz[idx]:.3f}")

255

```

256

257

### Physical Model with Anti-Reflective Coating

258

259

```python

260

import pvlib

261

from pvlib import iam

262

import numpy as np

263

import matplotlib.pyplot as plt

264

265

# Angle of incidence range

266

aoi = np.linspace(0, 90, 91)

267

268

# Compare with and without AR coating

269

iam_no_ar = iam.physical(aoi, n=1.526, K=4.0, L=0.002)

270

iam_with_ar = iam.physical(aoi, n=1.526, K=4.0, L=0.002, n_ar=1.29)

271

272

# Different glass types

273

iam_low_iron = iam.physical(aoi, n=1.526, K=2.0, L=0.002) # Low-iron glass

274

iam_thick_glass = iam.physical(aoi, n=1.526, K=4.0, L=0.004) # Thicker glass

275

276

plt.figure(figsize=(12, 8))

277

278

plt.subplot(2, 2, 1)

279

plt.plot(aoi, iam_no_ar, 'b-', label='No AR coating', linewidth=2)

280

plt.plot(aoi, iam_with_ar, 'r--', label='With AR coating (n=1.29)', linewidth=2)

281

plt.xlabel('Angle of Incidence (degrees)')

282

plt.ylabel('IAM')

283

plt.title('Effect of Anti-Reflective Coating')

284

plt.legend()

285

plt.grid(True)

286

287

plt.subplot(2, 2, 2)

288

plt.plot(aoi, iam_no_ar, 'b-', label='Standard glass (K=4)', linewidth=2)

289

plt.plot(aoi, iam_low_iron, 'g--', label='Low-iron glass (K=2)', linewidth=2)

290

plt.xlabel('Angle of Incidence (degrees)')

291

plt.ylabel('IAM')

292

plt.title('Effect of Glass Type')

293

plt.legend()

294

plt.grid(True)

295

296

plt.subplot(2, 2, 3)

297

plt.plot(aoi, iam_no_ar, 'b-', label='2mm glass', linewidth=2)

298

plt.plot(aoi, iam_thick_glass, 'm--', label='4mm glass', linewidth=2)

299

plt.xlabel('Angle of Incidence (degrees)')

300

plt.ylabel('IAM')

301

plt.title('Effect of Glass Thickness')

302

plt.legend()

303

plt.grid(True)

304

305

plt.subplot(2, 2, 4)

306

# Show transmission improvement with AR coating

307

improvement = iam_with_ar - iam_no_ar

308

plt.plot(aoi, improvement * 100, 'k-', linewidth=2)

309

plt.xlabel('Angle of Incidence (degrees)')

310

plt.ylabel('IAM Improvement (%)')

311

plt.title('AR Coating Improvement')

312

plt.grid(True)

313

314

plt.tight_layout()

315

plt.show()

316

317

print("AR Coating Benefits:")

318

print("AOI (°) No AR With AR Improvement (%)")

319

for angle in [0, 30, 45, 60, 75]:

320

no_ar = iam_no_ar[angle]

321

with_ar = iam_with_ar[angle]

322

improvement = (with_ar - no_ar) * 100

323

print(f"{angle:5.0f} {no_ar:.3f} {with_ar:.3f} {improvement:10.2f}")

324

```

325

326

### SAPM IAM with Module Database

327

328

```python

329

import pvlib

330

from pvlib import iam, pvsystem

331

import numpy as np

332

import matplotlib.pyplot as plt

333

334

# Get module parameters from SAM database

335

modules = pvsystem.retrieve_sam('SandiaMod')

336

337

# Select a few different modules for comparison

338

module_names = [

339

'Canadian_Solar_CS5P_220M___2009_',

340

'SunPower_SPR_315E_WHT_D__2013_',

341

'First_Solar_FS_380__2010_'

342

]

343

344

aoi = np.linspace(0, 90, 91)

345

346

plt.figure(figsize=(12, 8))

347

348

# Plot IAM curves for different modules

349

for i, module_name in enumerate(module_names):

350

if module_name in modules:

351

module = modules[module_name]

352

iam_sapm = iam.sapm(aoi, module)

353

354

# Clean up module name for display

355

display_name = module_name.replace('_', ' ').replace(' ', ' - ')

356

plt.plot(aoi, iam_sapm, linewidth=2, label=display_name)

357

358

print(f"\nModule: {display_name}")

359

print("SAPM IAM Coefficients:")

360

for coeff in ['B0', 'B1', 'B2', 'B3', 'B4', 'B5']:

361

if coeff in module:

362

print(f" {coeff}: {module[coeff]:.6f}")

363

364

plt.xlabel('Angle of Incidence (degrees)')

365

plt.ylabel('SAPM IAM (F2)')

366

plt.title('SAPM IAM Curves for Different Module Technologies')

367

plt.legend()

368

plt.grid(True)

369

plt.xlim(0, 90)

370

plt.ylim(0, 1.1)

371

plt.show()

372

373

# Show how SAPM can exceed 1.0 at some angles

374

print("\nSAMP IAM Values (can exceed 1.0):")

375

print("AOI (°) " + " ".join([name[:15] for name in module_names if name in modules]))

376

for angle in [0, 15, 30, 45, 60, 75, 90]:

377

row = f"{angle:5.0f} "

378

for name in module_names:

379

if name in modules:

380

module = modules[name]

381

iam_val = iam.sapm(angle, module)

382

row += f"{iam_val:.3f} "

383

print(row)

384

```

385

386

### Diffuse IAM Calculation

387

388

```python

389

import pvlib

390

from pvlib import iam

391

import numpy as np

392

import matplotlib.pyplot as plt

393

394

# Surface tilt angles

395

surface_tilts = np.arange(0, 91, 5)

396

397

# Calculate diffuse IAM using different approaches

398

results = {}

399

400

# Martin-Ruiz diffuse (analytical)

401

for tilt in surface_tilts:

402

iam_sky, iam_ground = iam.martin_ruiz_diffuse(tilt, a_r=0.16)

403

if 'martin_ruiz_sky' not in results:

404

results['martin_ruiz_sky'] = []

405

results['martin_ruiz_ground'] = []

406

results['martin_ruiz_sky'].append(iam_sky)

407

results['martin_ruiz_ground'].append(iam_ground)

408

409

# Schlick diffuse (analytical)

410

for tilt in surface_tilts:

411

iam_sky, iam_ground = iam.schlick_diffuse(tilt)

412

if 'schlick_sky' not in results:

413

results['schlick_sky'] = []

414

results['schlick_ground'] = []

415

results['schlick_sky'].append(iam_sky)

416

results['schlick_ground'].append(iam_ground)

417

418

# Marion integration (numerical) - sample a few points

419

sample_tilts = [0, 20, 45, 70, 90]

420

marion_results = {}

421

for tilt in sample_tilts:

422

# Physical model

423

marion_physical = iam.marion_diffuse('physical', tilt)

424

# ASHRAE model

425

marion_ashrae = iam.marion_diffuse('ashrae', tilt, b=0.05)

426

427

if tilt not in marion_results:

428

marion_results[tilt] = {}

429

marion_results[tilt]['physical'] = marion_physical

430

marion_results[tilt]['ashrae'] = marion_ashrae

431

432

# Plot results

433

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))

434

435

# Sky diffuse

436

ax1.plot(surface_tilts, results['martin_ruiz_sky'], 'b-',

437

label='Martin-Ruiz', linewidth=2)

438

ax1.plot(surface_tilts, results['schlick_sky'], 'r--',

439

label='Schlick', linewidth=2)

440

441

# Add Marion integration points

442

for tilt in sample_tilts:

443

if tilt in marion_results:

444

ax1.plot(tilt, marion_results[tilt]['physical']['sky'],

445

'go', markersize=8, label='Marion Physical' if tilt == sample_tilts[0] else "")

446

ax1.plot(tilt, marion_results[tilt]['ashrae']['sky'],

447

'mo', markersize=8, label='Marion ASHRAE' if tilt == sample_tilts[0] else "")

448

449

ax1.set_xlabel('Surface Tilt (degrees)')

450

ax1.set_ylabel('Sky Diffuse IAM')

451

ax1.set_title('Sky Diffuse IAM vs Surface Tilt')

452

ax1.legend()

453

ax1.grid(True)

454

455

# Ground diffuse

456

ax2.plot(surface_tilts, results['martin_ruiz_ground'], 'b-',

457

label='Martin-Ruiz', linewidth=2)

458

ax2.plot(surface_tilts, results['schlick_ground'], 'r--',

459

label='Schlick', linewidth=2)

460

461

# Add Marion integration points

462

for tilt in sample_tilts:

463

if tilt in marion_results:

464

ax2.plot(tilt, marion_results[tilt]['physical']['ground'],

465

'go', markersize=8, label='Marion Physical' if tilt == sample_tilts[0] else "")

466

ax2.plot(tilt, marion_results[tilt]['ashrae']['ground'],

467

'mo', markersize=8, label='Marion ASHRAE' if tilt == sample_tilts[0] else "")

468

469

ax2.set_xlabel('Surface Tilt (degrees)')

470

ax2.set_ylabel('Ground Diffuse IAM')

471

ax2.set_title('Ground Diffuse IAM vs Surface Tilt')

472

ax2.legend()

473

ax2.grid(True)

474

475

plt.tight_layout()

476

plt.show()

477

478

print("Diffuse IAM Comparison at Key Tilt Angles:")

479

print("Tilt (°) Martin-Ruiz Sky Schlick Sky Martin-Ruiz Ground Schlick Ground")

480

for i, tilt in enumerate([0, 20, 45, 70, 90]):

481

idx = tilt // 5 # Index in results arrays

482

mr_sky = results['martin_ruiz_sky'][idx]

483

schlick_sky = results['schlick_sky'][idx]

484

mr_ground = results['martin_ruiz_ground'][idx]

485

schlick_ground = results['schlick_ground'][idx]

486

print(f"{tilt:6.0f} {mr_sky:11.3f} {schlick_sky:9.3f} {mr_ground:13.3f} {schlick_ground:11.3f}")

487

```

488

489

### Model Fitting and Conversion

490

491

```python

492

import pvlib

493

from pvlib import iam

494

import numpy as np

495

import matplotlib.pyplot as plt

496

497

# Generate synthetic measured data (simulating real measurements)

498

np.random.seed(42)

499

aoi_measured = np.array([0, 10, 20, 30, 40, 50, 60, 70, 80])

500

501

# "True" model (physical with known parameters)

502

true_params = {'n': 1.526, 'K': 4.0, 'L': 0.002}

503

iam_true = iam.physical(aoi_measured, **true_params)

504

505

# Add measurement noise

506

iam_measured = iam_true + np.random.normal(0, 0.01, len(iam_true))

507

iam_measured = np.clip(iam_measured, 0, 1) # Keep physical bounds

508

509

print(f"Synthetic Measured Data:")

510

print("AOI (°) True IAM Measured IAM")

511

for i, angle in enumerate(aoi_measured):

512

print(f"{angle:5.0f} {iam_true[i]:.3f} {iam_measured[i]:.3f}")

513

514

# Fit different models to the measured data

515

fitted_ashrae = iam.fit(aoi_measured, iam_measured, 'ashrae')

516

fitted_martin_ruiz = iam.fit(aoi_measured, iam_measured, 'martin_ruiz')

517

fitted_physical = iam.fit(aoi_measured, iam_measured, 'physical')

518

519

print(f"\nFitted Model Parameters:")

520

print(f"ASHRAE: {fitted_ashrae}")

521

print(f"Martin-Ruiz: {fitted_martin_ruiz}")

522

print(f"Physical: {fitted_physical}")

523

print(f"True Physical: {true_params}")

524

525

# Convert between models

526

ashrae_to_physical = iam.convert('ashrae', fitted_ashrae, 'physical')

527

martin_ruiz_to_ashrae = iam.convert('martin_ruiz', fitted_martin_ruiz, 'ashrae')

528

529

print(f"\nModel Conversions:")

530

print(f"ASHRAE → Physical: {ashrae_to_physical}")

531

print(f"Martin-Ruiz → ASHRAE: {martin_ruiz_to_ashrae}")

532

533

# Evaluate fitted models over full AOI range

534

aoi_full = np.linspace(0, 90, 91)

535

iam_fitted_ashrae = iam.ashrae(aoi_full, **fitted_ashrae)

536

iam_fitted_martin_ruiz = iam.martin_ruiz(aoi_full, **fitted_martin_ruiz)

537

iam_fitted_physical = iam.physical(aoi_full, **fitted_physical)

538

iam_true_full = iam.physical(aoi_full, **true_params)

539

540

# Plot results

541

plt.figure(figsize=(12, 8))

542

543

plt.subplot(2, 2, 1)

544

plt.plot(aoi_full, iam_true_full, 'k-', label='True Physical', linewidth=3)

545

plt.plot(aoi_measured, iam_measured, 'ro', label='Measured Data', markersize=8)

546

plt.plot(aoi_full, iam_fitted_ashrae, 'b--', label='Fitted ASHRAE', linewidth=2)

547

plt.plot(aoi_full, iam_fitted_martin_ruiz, 'g:', label='Fitted Martin-Ruiz', linewidth=2)

548

plt.plot(aoi_full, iam_fitted_physical, 'r-.', label='Fitted Physical', linewidth=2)

549

plt.xlabel('Angle of Incidence (degrees)')

550

plt.ylabel('IAM')

551

plt.title('Model Fitting Comparison')

552

plt.legend()

553

plt.grid(True)

554

555

plt.subplot(2, 2, 2)

556

# Show residuals

557

residuals_ashrae = iam_true_full - iam_fitted_ashrae

558

residuals_martin_ruiz = iam_true_full - iam_fitted_martin_ruiz

559

residuals_physical = iam_true_full - iam_fitted_physical

560

561

plt.plot(aoi_full, residuals_ashrae * 100, 'b--', label='ASHRAE', linewidth=2)

562

plt.plot(aoi_full, residuals_martin_ruiz * 100, 'g:', label='Martin-Ruiz', linewidth=2)

563

plt.plot(aoi_full, residuals_physical * 100, 'r-.', label='Physical', linewidth=2)

564

plt.xlabel('Angle of Incidence (degrees)')

565

plt.ylabel('Residual (%)')

566

plt.title('Fitting Residuals (True - Fitted)')

567

plt.legend()

568

plt.grid(True)

569

570

plt.subplot(2, 2, 3)

571

# Compare converted models

572

iam_converted_physical = iam.physical(aoi_full, **ashrae_to_physical)

573

iam_converted_ashrae = iam.ashrae(aoi_full, **martin_ruiz_to_ashrae)

574

575

plt.plot(aoi_full, iam_fitted_ashrae, 'b-', label='Original ASHRAE', linewidth=2)

576

plt.plot(aoi_full, iam_converted_physical, 'b--', label='ASHRAE→Physical', linewidth=2)

577

plt.plot(aoi_full, iam_fitted_martin_ruiz, 'g-', label='Original Martin-Ruiz', linewidth=2)

578

plt.plot(aoi_full, iam_converted_ashrae, 'g--', label='Martin-Ruiz→ASHRAE', linewidth=2)

579

plt.xlabel('Angle of Incidence (degrees)')

580

plt.ylabel('IAM')

581

plt.title('Model Conversions')

582

plt.legend()

583

plt.grid(True)

584

585

plt.subplot(2, 2, 4)

586

# Error metrics

587

models = ['ASHRAE', 'Martin-Ruiz', 'Physical']

588

rmse_values = []

589

mae_values = []

590

591

for residuals in [residuals_ashrae, residuals_martin_ruiz, residuals_physical]:

592

rmse = np.sqrt(np.mean(residuals**2)) * 100

593

mae = np.mean(np.abs(residuals)) * 100

594

rmse_values.append(rmse)

595

mae_values.append(mae)

596

597

x = np.arange(len(models))

598

width = 0.35

599

600

plt.bar(x - width/2, rmse_values, width, label='RMSE', alpha=0.8)

601

plt.bar(x + width/2, mae_values, width, label='MAE', alpha=0.8)

602

plt.xlabel('Model')

603

plt.ylabel('Error (%)')

604

plt.title('Fitting Error Metrics')

605

plt.xticks(x, models)

606

plt.legend()

607

plt.grid(True, alpha=0.3)

608

609

plt.tight_layout()

610

plt.show()

611

612

print(f"\nFitting Error Summary:")

613

print("Model RMSE (%) MAE (%)")

614

for i, model in enumerate(models):

615

print(f"{model:12s} {rmse_values[i]:6.3f} {mae_values[i]:5.3f}")

616

```

617

618

### Interpolation Method

619

620

```python

621

import pvlib

622

from pvlib import iam

623

import numpy as np

624

import matplotlib.pyplot as plt

625

626

# Reference data points (typically from manufacturer measurements)

627

theta_ref = np.array([0, 10, 20, 30, 40, 50, 60, 70, 80, 90])

628

iam_ref = np.array([1.000, 1.000, 0.999, 0.996, 0.992, 0.985, 0.974, 0.955, 0.921, 0.000])

629

630

# AOI range for interpolation

631

aoi = np.linspace(0, 90, 181)

632

633

# Different interpolation methods

634

iam_linear = iam.interp(aoi, theta_ref, iam_ref, method='linear')

635

iam_quadratic = iam.interp(aoi, theta_ref, iam_ref, method='quadratic')

636

iam_cubic = iam.interp(aoi, theta_ref, iam_ref, method='cubic')

637

638

# Compare with parametric models fitted to same reference data

639

fitted_ashrae = iam.fit(theta_ref, iam_ref, 'ashrae')

640

fitted_martin_ruiz = iam.fit(theta_ref, iam_ref, 'martin_ruiz')

641

642

iam_ashrae_fit = iam.ashrae(aoi, **fitted_ashrae)

643

iam_martin_ruiz_fit = iam.martin_ruiz(aoi, **fitted_martin_ruiz)

644

645

# Plot comparison

646

plt.figure(figsize=(12, 8))

647

648

plt.subplot(2, 1, 1)

649

plt.plot(theta_ref, iam_ref, 'ko', markersize=8, label='Reference Data')

650

plt.plot(aoi, iam_linear, 'b-', label='Linear Interpolation', linewidth=2)

651

plt.plot(aoi, iam_quadratic, 'r--', label='Quadratic Interpolation', linewidth=2)

652

plt.plot(aoi, iam_cubic, 'g:', label='Cubic Interpolation', linewidth=2)

653

plt.xlabel('Angle of Incidence (degrees)')

654

plt.ylabel('IAM')

655

plt.title('Interpolation Methods Comparison')

656

plt.legend()

657

plt.grid(True)

658

plt.xlim(0, 90)

659

plt.ylim(0.8, 1.05)

660

661

plt.subplot(2, 1, 2)

662

plt.plot(theta_ref, iam_ref, 'ko', markersize=8, label='Reference Data')

663

plt.plot(aoi, iam_cubic, 'g-', label='Cubic Interpolation', linewidth=2)

664

plt.plot(aoi, iam_ashrae_fit, 'm--', label=f'ASHRAE Fit (b={fitted_ashrae["b"]:.3f})', linewidth=2)

665

plt.plot(aoi, iam_martin_ruiz_fit, 'c:', label=f'Martin-Ruiz Fit (a_r={fitted_martin_ruiz["a_r"]:.3f})', linewidth=2)

666

plt.xlabel('Angle of Incidence (degrees)')

667

plt.ylabel('IAM')

668

plt.title('Interpolation vs Parametric Model Fits')

669

plt.legend()

670

plt.grid(True)

671

plt.xlim(0, 90)

672

plt.ylim(0.8, 1.05)

673

674

plt.tight_layout()

675

plt.show()

676

677

print("Interpolation vs Parametric Models at Key Angles:")

678

print("AOI (°) Linear Quadratic Cubic ASHRAE Martin-Ruiz")

679

for angle in [0, 15, 30, 45, 60, 75, 90]:

680

lin = np.interp(angle, aoi, iam_linear)

681

quad = np.interp(angle, aoi, iam_quadratic)

682

cub = np.interp(angle, aoi, iam_cubic)

683

ash = np.interp(angle, aoi, iam_ashrae_fit)

684

mr = np.interp(angle, aoi, iam_martin_ruiz_fit)

685

print(f"{angle:5.0f} {lin:.3f} {quad:.3f} {cub:.3f} {ash:.3f} {mr:.3f}")

686

687

# Calculate RMS differences from cubic interpolation (reference)

688

rms_linear = np.sqrt(np.mean((iam_linear - iam_cubic)**2)) * 100

689

rms_quadratic = np.sqrt(np.mean((iam_quadratic - iam_cubic)**2)) * 100

690

rms_ashrae = np.sqrt(np.mean((iam_ashrae_fit - iam_cubic)**2)) * 100

691

rms_martin_ruiz = np.sqrt(np.mean((iam_martin_ruiz_fit - iam_cubic)**2)) * 100

692

693

print(f"\nRMS Differences from Cubic Interpolation:")

694

print(f"Linear: {rms_linear:.4f}%")

695

print(f"Quadratic: {rms_quadratic:.4f}%")

696

print(f"ASHRAE: {rms_ashrae:.4f}%")

697

print(f"Martin-Ruiz: {rms_martin_ruiz:.4f}%")

698

```