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
```