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

spectrum.mddocs/

0

# Spectral Modeling

1

2

Model spectral irradiance distribution and calculate spectral mismatch factors for photovoltaic systems. Comprehensive tools for spectral analysis, correction factors, and performance modeling under varying atmospheric conditions.

3

4

## Capabilities

5

6

### Spectral Irradiance Models

7

8

Calculate detailed spectral irradiance distribution across wavelengths.

9

10

```python { .api }

11

def spectrl2(apparent_zenith, aoi, surface_tilt, ground_albedo,

12

surface_pressure, relative_humidity, precipitable_water,

13

ozone=0.31, nitrogen_dioxide=0.0002, aerosol_turbidity_500nm=0.1,

14

dayofyear=1, wavelengths=None):

15

"""

16

Calculate spectral irradiance using SPECTRL2 model.

17

18

Parameters:

19

- apparent_zenith: numeric, apparent solar zenith angle in degrees

20

- aoi: numeric, angle of incidence on surface in degrees

21

- surface_tilt: numeric, surface tilt angle in degrees

22

- ground_albedo: numeric, ground reflectance (0-1)

23

- surface_pressure: numeric, surface pressure in millibars

24

- relative_humidity: numeric, relative humidity (0-100)

25

- precipitable_water: numeric, precipitable water in cm

26

- ozone: numeric, ozone column thickness in atm-cm

27

- nitrogen_dioxide: numeric, nitrogen dioxide column in atm-cm

28

- aerosol_turbidity_500nm: numeric, aerosol optical depth at 500nm

29

- dayofyear: int, day of year (1-366)

30

- wavelengths: array-like, wavelengths in nm (280-4000)

31

32

Returns:

33

tuple: (wavelengths, total_irradiance, direct_irradiance, diffuse_irradiance,

34

global_horizontal_irradiance, direct_normal_irradiance)

35

"""

36

37

def get_reference_spectra(wavelengths=None, standard="ASTM G173-03"):

38

"""

39

Get standard reference solar spectra.

40

41

Parameters:

42

- wavelengths: array-like, wavelengths in nm for interpolation

43

- standard: str, reference standard ('ASTM G173-03', 'AM15G', 'AM15D', 'AM0')

44

45

Returns:

46

pandas.DataFrame with wavelengths and spectral irradiance columns

47

"""

48

49

def average_photon_energy(spectra):

50

"""

51

Calculate average photon energy of spectrum.

52

53

Parameters:

54

- spectra: pandas.DataFrame with wavelengths (nm) and irradiance columns

55

56

Returns:

57

pandas.Series with average photon energies in eV

58

"""

59

```

60

61

### Spectral Mismatch Factors

62

63

Calculate correction factors for spectral mismatch between reference and field conditions.

64

65

```python { .api }

66

def spectral_factor_sapm(airmass_absolute, module):

67

"""

68

Calculate SAPM spectral correction factor.

69

70

Parameters:

71

- airmass_absolute: numeric, absolute airmass

72

- module: dict, module parameters containing spectral coefficients

73

Required keys: 'a0', 'a1', 'a2', 'a3', 'a4'

74

75

Returns:

76

numeric, spectral correction factor (typically 0.8-1.2)

77

"""

78

79

def spectral_factor_firstsolar(precipitable_water, airmass_absolute,

80

module_type, coefficients=None):

81

"""

82

Calculate First Solar spectral correction factor.

83

84

Parameters:

85

- precipitable_water: numeric, precipitable water in cm

86

- airmass_absolute: numeric, absolute airmass

87

- module_type: str, module technology ('cdte', 'multijunction', 'cigs')

88

- coefficients: dict, custom spectral coefficients

89

90

Returns:

91

numeric, spectral correction factor

92

"""

93

94

def spectral_factor_caballero(precipitable_water, airmass_absolute, aod500,

95

alpha=1.14, module_type='csi'):

96

"""

97

Calculate Caballero spectral correction factor.

98

99

Parameters:

100

- precipitable_water: numeric, precipitable water in cm

101

- airmass_absolute: numeric, absolute airmass

102

- aod500: numeric, aerosol optical depth at 500nm

103

- alpha: numeric, Angstrom alpha parameter

104

- module_type: str, module technology ('csi', 'asi', 'cdte', 'pc')

105

106

Returns:

107

numeric, spectral correction factor

108

"""

109

110

def spectral_factor_pvspec(airmass_absolute, clearsky_index,

111

module_type='monosi',

112

coefficients=None):

113

"""

114

Calculate PVSPEC spectral correction factor.

115

116

Parameters:

117

- airmass_absolute: numeric, absolute airmass (1.0-10.0)

118

- clearsky_index: numeric, clearsky index (0.0-1.0)

119

- module_type: str, module type ('monosi', 'xsi', 'cdte', 'asi', 'cigs', 'perovskite')

120

- coefficients: dict, custom model coefficients

121

122

Returns:

123

numeric, spectral correction factor

124

"""

125

126

def spectral_factor_jrc(airmass, clearsky_index, module_type=None,

127

pwat=None, am_coeff=None):

128

"""

129

Calculate JRC spectral correction factor.

130

131

Parameters:

132

- airmass: numeric, airmass (1.0-10.0)

133

- clearsky_index: numeric, clearsky index (0.0-1.0)

134

- module_type: str, technology type ('csi', 'asi', 'cdte', 'cigs')

135

- pwat: numeric, precipitable water in cm (optional)

136

- am_coeff: dict, custom airmass coefficients

137

138

Returns:

139

numeric, spectral correction factor

140

"""

141

142

def calc_spectral_mismatch_field(sr, e_sun, e_ref=None):

143

"""

144

Calculate spectral mismatch for field conditions.

145

146

Parameters:

147

- sr: pandas.Series, spectral response vs wavelength (nm)

148

- e_sun: pandas.Series, solar spectral irradiance vs wavelength

149

- e_ref: pandas.Series, reference spectral irradiance (default AM1.5G)

150

151

Returns:

152

numeric, spectral mismatch factor

153

"""

154

```

155

156

### Spectral Response Functions

157

158

Work with photovoltaic device spectral response characteristics.

159

160

```python { .api }

161

def get_example_spectral_response(wavelength=None):

162

"""

163

Get example spectral response functions for common PV technologies.

164

165

Parameters:

166

- wavelength: array-like, wavelengths in nm for interpolation

167

168

Returns:

169

dict with technology names as keys and spectral response arrays as values

170

Technologies: 'si_c', 'si_pc', 'cdte', 'cigs', 'asi', 'gaas', 'organic'

171

"""

172

173

def sr_to_qe(sr, wavelength=None, normalize=False):

174

"""

175

Convert spectral response to quantum efficiency.

176

177

Parameters:

178

- sr: pandas.Series, spectral response (A/W) vs wavelength

179

- wavelength: array-like, wavelengths in nm (default from sr index)

180

- normalize: bool, normalize QE to peak value

181

182

Returns:

183

pandas.Series, quantum efficiency vs wavelength

184

"""

185

186

def qe_to_sr(qe, wavelength=None, normalize=False):

187

"""

188

Convert quantum efficiency to spectral response.

189

190

Parameters:

191

- qe: pandas.Series, quantum efficiency vs wavelength

192

- wavelength: array-like, wavelengths in nm (default from qe index)

193

- normalize: bool, normalize SR to peak value

194

195

Returns:

196

pandas.Series, spectral response (A/W) vs wavelength

197

"""

198

```

199

200

### Advanced Spectral Analysis

201

202

Detailed spectral analysis tools for research and development.

203

204

```python { .api }

205

def spectral_factor_photovoltaic(airmass_absolute, precipitable_water,

206

aerosol_optical_depth, spectral_response,

207

wavelengths=None):

208

"""

209

Calculate spectral correction factor using custom spectral response.

210

211

Parameters:

212

- airmass_absolute: numeric, absolute airmass

213

- precipitable_water: numeric, precipitable water in cm

214

- aerosol_optical_depth: numeric, aerosol optical depth at 500nm

215

- spectral_response: array-like, device spectral response

216

- wavelengths: array-like, corresponding wavelengths in nm

217

218

Returns:

219

numeric, spectral correction factor

220

"""

221

222

def integrated_spectral_response(spectral_irradiance, spectral_response,

223

wavelengths=None):

224

"""

225

Calculate integrated spectral response for given irradiance spectrum.

226

227

Parameters:

228

- spectral_irradiance: array-like, spectral irradiance (W/m²/nm)

229

- spectral_response: array-like, device spectral response (A/W or QE)

230

- wavelengths: array-like, wavelengths in nm

231

232

Returns:

233

numeric, integrated response

234

"""

235

236

def bandwidth_utilization_factor(spectral_response, reference_spectrum,

237

field_spectrum, wavelengths=None):

238

"""

239

Calculate bandwidth utilization factor comparing field to reference conditions.

240

241

Parameters:

242

- spectral_response: array-like, device spectral response

243

- reference_spectrum: array-like, reference spectral irradiance

244

- field_spectrum: array-like, field spectral irradiance

245

- wavelengths: array-like, wavelengths in nm

246

247

Returns:

248

numeric, bandwidth utilization factor

249

"""

250

```

251

252

## Usage Examples

253

254

### Basic Spectral Correction Factors

255

256

```python

257

import pvlib

258

from pvlib import spectrum, atmosphere, solarposition

259

import pandas as pd

260

import numpy as np

261

import matplotlib.pyplot as plt

262

263

# Location and time

264

lat, lon = 37.8756, -122.2441 # Berkeley, CA

265

times = pd.date_range('2023-06-21 06:00', '2023-06-21 18:00',

266

freq='H', tz='US/Pacific')

267

268

# Calculate solar position and atmospheric parameters

269

solar_pos = solarposition.get_solarposition(times, lat, lon)

270

airmass_rel = atmosphere.get_relative_airmass(solar_pos['zenith'])

271

airmass_abs = atmosphere.get_absolute_airmass(airmass_rel)

272

273

# Atmospheric conditions

274

precipitable_water = 2.5 # cm

275

aod500 = 0.15 # aerosol optical depth at 500nm

276

277

# Calculate spectral correction factors for different PV technologies

278

technologies = {

279

'c-Si': {'type': 'csi', 'sapm_params': {'a0': 0.928, 'a1': 0.068, 'a2': -0.0077, 'a3': 0.0001, 'a4': -0.000002}},

280

'CdTe': {'type': 'cdte', 'sapm_params': {'a0': 0.87, 'a1': 0.122, 'a2': -0.0047, 'a3': -0.000085, 'a4': 0.0000020}},

281

'a-Si': {'type': 'asi', 'sapm_params': {'a0': 1.12, 'a1': -0.047, 'a2': -0.0085, 'a3': 0.000047, 'a4': 0.0000024}}

282

}

283

284

spectral_factors = {}

285

286

for tech_name, params in technologies.items():

287

# SAPM spectral factor

288

sapm_factor = spectrum.spectral_factor_sapm(airmass_abs, params['samp_params'])

289

290

# First Solar factor (for CdTe)

291

if tech_name == 'CdTe':

292

fs_factor = spectrum.spectral_factor_firstsolar(

293

precipitable_water, airmass_abs, 'cdte'

294

)

295

else:

296

fs_factor = None

297

298

# Caballero factor

299

caballero_factor = spectrum.spectral_factor_caballero(

300

precipitable_water, airmass_abs, aod500, module_type=params['type']

301

)

302

303

spectral_factors[tech_name] = {

304

'sapm': samp_factor,

305

'firstsolar': fs_factor,

306

'caballero': caballero_factor

307

}

308

309

# Plot spectral correction factors throughout the day

310

fig, axes = plt.subplots(2, 2, figsize=(15, 10))

311

312

# SAPM factors

313

for tech in technologies.keys():

314

axes[0, 0].plot(times, spectral_factors[tech]['sapm'],

315

label=tech, marker='o', linewidth=2)

316

axes[0, 0].set_title('SAPM Spectral Correction Factors')

317

axes[0, 0].set_ylabel('Spectral Factor')

318

axes[0, 0].legend()

319

axes[0, 0].grid(True)

320

321

# Caballero factors

322

for tech in technologies.keys():

323

axes[0, 1].plot(times, spectral_factors[tech]['caballero'],

324

label=tech, marker='s', linewidth=2)

325

axes[0, 1].set_title('Caballero Spectral Correction Factors')

326

axes[0, 1].set_ylabel('Spectral Factor')

327

axes[0, 1].legend()

328

axes[0, 1].grid(True)

329

330

# Airmass variation

331

axes[1, 0].plot(times, airmass_abs, 'ko-', linewidth=2)

332

axes[1, 0].set_title('Absolute Airmass')

333

axes[1, 0].set_ylabel('Airmass')

334

axes[1, 0].grid(True)

335

336

# Solar elevation

337

axes[1, 1].plot(times, 90 - solar_pos['zenith'], 'ro-', linewidth=2)

338

axes[1, 1].set_title('Solar Elevation Angle')

339

axes[1, 1].set_ylabel('Elevation (degrees)')

340

axes[1, 1].set_xlabel('Time')

341

axes[1, 1].grid(True)

342

343

plt.tight_layout()

344

plt.show()

345

346

# Print summary statistics

347

print("Daily Average Spectral Correction Factors:")

348

for tech in technologies.keys():

349

sapm_avg = np.mean(spectral_factors[tech]['samp'])

350

caballero_avg = np.mean(spectral_factors[tech]['caballero'])

351

print(f"{tech:4s}: SAPM = {sapm_avg:.3f}, Caballero = {caballero_avg:.3f}")

352

```

353

354

### Detailed Spectral Irradiance Modeling

355

356

```python

357

import pvlib

358

from pvlib import spectrum, atmosphere, solarposition, clearsky

359

import pandas as pd

360

import numpy as np

361

import matplotlib.pyplot as plt

362

363

# Location and conditions

364

lat, lon = 40.0150, -105.2705 # Boulder, CO

365

elevation = 1655 # meters

366

time = pd.Timestamp('2023-06-21 12:00:00', tz='US/Mountain')

367

368

# Solar position

369

solar_pos = solarposition.get_solarposition(time, lat, lon)

370

zenith = solar_pos['zenith'].iloc[0]

371

azimuth = solar_pos['azimuth'].iloc[0]

372

373

# Atmospheric conditions

374

surface_pressure = atmosphere.alt2pres(elevation)

375

precipitable_water = 1.5 # cm

376

ozone = 0.31 # atm-cm

377

aod500 = 0.1 # aerosol optical depth

378

relative_humidity = 45 # percent

379

380

# Surface parameters

381

surface_tilt = 30 # degrees

382

surface_azimuth = 180 # degrees (south-facing)

383

ground_albedo = 0.2

384

385

# Calculate angle of incidence

386

from pvlib import irradiance

387

aoi = irradiance.aoi(surface_tilt, surface_azimuth, zenith, azimuth)

388

389

# Calculate spectral irradiance using SPECTRL2

390

wavelengths = np.arange(300, 1200, 5) # 300-1200 nm in 5nm steps

391

392

spectral_result = spectrum.spectrl2(

393

apparent_zenith=zenith,

394

aoi=aoi,

395

surface_tilt=surface_tilt,

396

ground_albedo=ground_albedo,

397

surface_pressure=surface_pressure/100, # convert Pa to mbar

398

relative_humidity=relative_humidity,

399

precipitable_water=precipitable_water,

400

ozone=ozone,

401

aerosol_turbidity_500nm=aod500,

402

dayofyear=172, # June 21

403

wavelengths=wavelengths

404

)

405

406

wavelengths_out = spectral_result[0]

407

poa_total = spectral_result[1]

408

poa_direct = spectral_result[2]

409

poa_diffuse = spectral_result[3]

410

ghi_spectral = spectral_result[4]

411

dni_spectral = spectral_result[5]

412

413

# Get reference spectrum for comparison

414

reference_spectra = spectrum.get_reference_spectra()

415

am15g_spectrum = reference_spectra['wavelength'], reference_spectra['global_tilt_37']

416

417

# Plot spectral irradiance

418

fig, axes = plt.subplots(2, 2, figsize=(15, 10))

419

420

# POA spectral components

421

axes[0, 0].plot(wavelengths_out, poa_total, label='Total POA', linewidth=2)

422

axes[0, 0].plot(wavelengths_out, poa_direct, label='Direct POA', linewidth=2)

423

axes[0, 0].plot(wavelengths_out, poa_diffuse, label='Diffuse POA', linewidth=2)

424

axes[0, 0].set_xlabel('Wavelength (nm)')

425

axes[0, 0].set_ylabel('Spectral Irradiance (W/m²/nm)')

426

axes[0, 0].set_title('Plane-of-Array Spectral Irradiance Components')

427

axes[0, 0].legend()

428

axes[0, 0].grid(True)

429

430

# Horizontal spectral components

431

axes[0, 1].plot(wavelengths_out, ghi_spectral, label='GHI', linewidth=2)

432

axes[0, 1].plot(wavelengths_out, dni_spectral, label='DNI', linewidth=2)

433

axes[0, 1].set_xlabel('Wavelength (nm)')

434

axes[0, 1].set_ylabel('Spectral Irradiance (W/m²/nm)')

435

axes[0, 1].set_title('Horizontal Spectral Irradiance')

436

axes[0, 1].legend()

437

axes[0, 1].grid(True)

438

439

# Comparison with AM1.5G reference

440

# Interpolate reference to same wavelengths for comparison

441

ref_interp = np.interp(wavelengths_out, am15g_spectrum[0], am15g_spectrum[1])

442

axes[1, 0].plot(wavelengths_out, poa_total, label='SPECTRL2 POA', linewidth=2)

443

axes[1, 0].plot(wavelengths_out, ref_interp, label='AM1.5G Reference',

444

linewidth=2, linestyle='--')

445

axes[1, 0].set_xlabel('Wavelength (nm)')

446

axes[1, 0].set_ylabel('Spectral Irradiance (W/m²/nm)')

447

axes[1, 0].set_title('Comparison with AM1.5G Reference')

448

axes[1, 0].legend()

449

axes[1, 0].grid(True)

450

451

# Spectral ratio

452

spectral_ratio = poa_total / ref_interp

453

axes[1, 1].plot(wavelengths_out, spectral_ratio, 'r-', linewidth=2)

454

axes[1, 1].set_xlabel('Wavelength (nm)')

455

axes[1, 1].set_ylabel('Ratio (Field/Reference)')

456

axes[1, 1].set_title('Spectral Ratio vs AM1.5G')

457

axes[1, 1].grid(True)

458

axes[1, 1].axhline(y=1.0, color='k', linestyle='--', alpha=0.5)

459

460

plt.tight_layout()

461

plt.show()

462

463

# Calculate integrated values

464

poa_integrated = np.trapz(poa_total, wavelengths_out)

465

ghi_integrated = np.trapz(ghi_spectral, wavelengths_out)

466

ref_integrated = np.trapz(ref_interp, wavelengths_out)

467

468

print(f"\nIntegrated Irradiance Values:")

469

print(f"POA Total: {poa_integrated:.1f} W/m²")

470

print(f"GHI: {ghi_integrated:.1f} W/m²")

471

print(f"AM1.5G Reference: {ref_integrated:.1f} W/m²")

472

print(f"POA/Reference Ratio: {poa_integrated/ref_integrated:.3f}")

473

```

474

475

### Spectral Mismatch Analysis

476

477

```python

478

import pvlib

479

from pvlib import spectrum

480

import pandas as pd

481

import numpy as np

482

import matplotlib.pyplot as plt

483

484

# Get example spectral response functions

485

example_sr = spectrum.get_example_spectral_response()

486

487

# Define wavelength range

488

wavelengths = np.arange(300, 1200, 2) # 300-1200 nm in 2nm steps

489

490

# Get different spectral responses for analysis

491

technologies = ['si_c', 'si_pc', 'cdte', 'cigs', 'asi']

492

spectral_responses = {}

493

494

for tech in technologies:

495

if tech in example_sr:

496

# Interpolate to common wavelength grid

497

sr_interp = np.interp(wavelengths, example_sr['wavelength'],

498

example_sr[tech], left=0, right=0)

499

spectral_responses[tech] = pd.Series(sr_interp, index=wavelengths)

500

501

# Get reference spectra

502

ref_spectra = spectrum.get_reference_spectra(wavelengths=wavelengths)

503

am15g = ref_spectra['global_tilt_37']

504

am15d = ref_spectra['direct_normal']

505

506

# Simulate different field conditions by modifying reference spectrum

507

# Condition 1: High airmass (red-shifted)

508

am_high = am15g * np.exp(-0.1 * (wavelengths - 600) / 300) # Red shift

509

510

# Condition 2: High water vapor (IR absorption)

511

water_absorption = 1 - 0.3 * np.exp(-((wavelengths - 940) / 40)**2) # 940nm absorption

512

am_humid = am15g * water_absorption

513

514

# Condition 3: High aerosols (blue scattering)

515

blue_scattering = 1 - 0.2 * np.exp(-((wavelengths - 400) / 100)**2) # Blue reduction

516

am_aerosol = am15g * blue_scattering

517

518

field_conditions = {

519

'AM1.5G Reference': am15g,

520

'High Airmass': am_high,

521

'High Humidity': am_humid,

522

'High Aerosols': am_aerosol

523

}

524

525

# Calculate spectral mismatch factors for each technology and condition

526

mismatch_results = {}

527

528

for tech_name, sr in spectral_responses.items():

529

mismatch_results[tech_name] = {}

530

531

for condition_name, spectrum_field in field_conditions.items():

532

if condition_name == 'AM1.5G Reference':

533

mismatch_factor = 1.0 # Reference condition

534

else:

535

mismatch_factor = spectrum.calc_spectral_mismatch_field(

536

sr, spectrum_field, am15g

537

)

538

539

mismatch_results[tech_name][condition_name] = mismatch_factor

540

541

# Create results dataframe

542

results_df = pd.DataFrame(mismatch_results).T

543

results_df = results_df.round(4)

544

545

print("Spectral Mismatch Factors:")

546

print(results_df)

547

548

# Plot spectral responses and field conditions

549

fig, axes = plt.subplots(2, 2, figsize=(15, 10))

550

551

# Spectral responses

552

for tech_name, sr in spectral_responses.items():

553

axes[0, 0].plot(wavelengths, sr, label=tech_name, linewidth=2)

554

axes[0, 0].set_xlabel('Wavelength (nm)')

555

axes[0, 0].set_ylabel('Spectral Response')

556

axes[0, 0].set_title('PV Technology Spectral Response')

557

axes[0, 0].legend()

558

axes[0, 0].grid(True)

559

560

# Field spectra compared to reference

561

for condition_name, spectrum_field in field_conditions.items():

562

if condition_name != 'AM1.5G Reference':

563

ratio = spectrum_field / am15g

564

axes[0, 1].plot(wavelengths, ratio, label=condition_name, linewidth=2)

565

566

axes[0, 1].set_xlabel('Wavelength (nm)')

567

axes[0, 1].set_ylabel('Ratio to AM1.5G')

568

axes[0, 1].set_title('Field Conditions vs AM1.5G Reference')

569

axes[0, 1].legend()

570

axes[0, 1].grid(True)

571

axes[0, 1].axhline(y=1.0, color='k', linestyle='--', alpha=0.5)

572

573

# Mismatch factors by technology

574

x_pos = np.arange(len(technologies))

575

width = 0.2

576

conditions = ['High Airmass', 'High Humidity', 'High Aerosols']

577

colors = ['red', 'blue', 'green']

578

579

for i, condition in enumerate(conditions):

580

values = [mismatch_results[tech][condition] for tech in technologies]

581

axes[1, 0].bar(x_pos + i*width, values, width, label=condition,

582

color=colors[i], alpha=0.7)

583

584

axes[1, 0].set_xlabel('PV Technology')

585

axes[1, 0].set_ylabel('Spectral Mismatch Factor')

586

axes[1, 0].set_title('Spectral Mismatch by Technology and Condition')

587

axes[1, 0].set_xticks(x_pos + width)

588

axes[1, 0].set_xticklabels(technologies)

589

axes[1, 0].legend()

590

axes[1, 0].grid(True, axis='y')

591

axes[1, 0].axhline(y=1.0, color='k', linestyle='--', alpha=0.5)

592

593

# Technology ranking by spectral sensitivity

594

sensitivity = {}

595

for tech in technologies:

596

# Calculate standard deviation of mismatch factors (excluding reference)

597

values = [mismatch_results[tech][cond] for cond in conditions]

598

sensitivity[tech] = np.std(values)

599

600

sorted_techs = sorted(sensitivity.items(), key=lambda x: x[1])

601

tech_names = [item[0] for item in sorted_techs]

602

sens_values = [item[1] for item in sorted_techs]

603

604

axes[1, 1].barh(tech_names, sens_values, color='orange', alpha=0.7)

605

axes[1, 1].set_xlabel('Spectral Sensitivity (Std Dev of Mismatch)')

606

axes[1, 1].set_title('Technology Ranking by Spectral Sensitivity')

607

axes[1, 1].grid(True, axis='x')

608

609

plt.tight_layout()

610

plt.show()

611

612

# Find best and worst performing technologies

613

print(f"\nMost spectrally stable technology: {sorted_techs[0][0]} (σ = {sorted_techs[0][1]:.4f})")

614

print(f"Most spectrally sensitive technology: {sorted_techs[-1][0]} (σ = {sorted_techs[-1][1]:.4f})")

615

616

# Calculate potential energy differences

617

print(f"\nPotential annual energy impact (assuming constant conditions):")

618

for tech in technologies:

619

for condition in conditions:

620

impact = (mismatch_results[tech][condition] - 1.0) * 100

621

print(f"{tech} in {condition}: {impact:+.1f}%")

622

```

623

624

### Advanced Spectral Analysis with Atmospheric Variations

625

626

```python

627

import pvlib

628

from pvlib import spectrum, atmosphere, clearsky, solarposition

629

import pandas as pd

630

import numpy as np

631

import matplotlib.pyplot as plt

632

633

# Location and time range

634

lat, lon = 35.0522, -106.5408 # Albuquerque, NM (high altitude, arid)

635

times = pd.date_range('2023-01-01', '2023-12-31', freq='MS') # Monthly

636

637

# Calculate seasonal variations

638

seasonal_analysis = []

639

640

for time in times:

641

# Solar position at solar noon

642

solar_pos = solarposition.get_solarposition(time.replace(hour=12), lat, lon)

643

zenith = solar_pos['zenith'].iloc[0]

644

645

# Atmospheric parameters (seasonal variations)

646

month = time.month

647

648

# Seasonal precipitable water (higher in summer)

649

pw_base = 1.0 # cm

650

pw_seasonal = pw_base * (1 + 0.5 * np.sin(2 * np.pi * (month - 3) / 12))

651

652

# Aerosol optical depth (higher in summer dust season)

653

aod_base = 0.08

654

aod_seasonal = aod_base * (1 + 0.3 * np.sin(2 * np.pi * (month - 6) / 12))

655

656

# Calculate airmass

657

airmass_rel = atmosphere.get_relative_airmass(zenith)

658

airmass_abs = atmosphere.get_absolute_airmass(airmass_rel, pressure=85000) # High altitude

659

660

# Calculate clearsky index (simulate seasonal cloud variations)

661

clearsky_model = clearsky.ineichen(zenith, airmass_abs, linke_turbidity=3.0)

662

ghi_clear = clearsky_model['ghi']

663

664

# Simulate measured GHI with seasonal cloud patterns

665

cloud_factor = 0.9 - 0.2 * np.sin(2 * np.pi * (month - 1) / 12) # More clouds in winter

666

ghi_measured = ghi_clear * cloud_factor

667

clearsky_idx = ghi_measured / ghi_clear if ghi_clear > 0 else 0

668

669

# Calculate spectral correction factors for c-Si

670

sapm_params = {'a0': 0.928, 'a1': 0.068, 'a2': -0.0077, 'a3': 0.0001, 'a4': -0.000002}

671

672

spectral_sapm = spectrum.spectral_factor_sapm(airmass_abs, sapm_params)

673

spectral_fs = spectrum.spectral_factor_firstsolar(pw_seasonal, airmass_abs, 'csi')

674

spectral_caballero = spectrum.spectral_factor_caballero(

675

pw_seasonal, airmass_abs, aod_seasonal, module_type='csi'

676

)

677

spectral_pvspec = spectrum.spectral_factor_pvspec(

678

airmass_abs, clearsky_idx, module_type='monosi'

679

)

680

681

seasonal_analysis.append({

682

'month': month,

683

'zenith': zenith,

684

'airmass': airmass_abs,

685

'precipitable_water': pw_seasonal,

686

'aod500': aod_seasonal,

687

'clearsky_index': clearsky_idx,

688

'spectral_sapm': spectral_sapm,

689

'spectral_firstsolar': spectral_fs,

690

'spectral_caballero': spectral_caballero,

691

'spectral_pvspec': spectral_pvspec

692

})

693

694

# Convert to DataFrame

695

seasonal_df = pd.DataFrame(seasonal_analysis)

696

seasonal_df.set_index('month', inplace=True)

697

698

# Plot seasonal variations

699

fig, axes = plt.subplots(3, 2, figsize=(15, 12))

700

701

# Atmospheric parameters

702

axes[0, 0].plot(seasonal_df.index, seasonal_df['airmass'], 'bo-', linewidth=2)

703

axes[0, 0].set_title('Seasonal Airmass Variation')

704

axes[0, 0].set_ylabel('Airmass')

705

axes[0, 0].grid(True)

706

707

axes[0, 1].plot(seasonal_df.index, seasonal_df['precipitable_water'], 'go-', linewidth=2, label='PW')

708

ax_twin = axes[0, 1].twinx()

709

ax_twin.plot(seasonal_df.index, seasonal_df['aod500'], 'ro-', linewidth=2, label='AOD')

710

axes[0, 1].set_title('Precipitable Water and AOD')

711

axes[0, 1].set_ylabel('Precipitable Water (cm)', color='g')

712

ax_twin.set_ylabel('AOD at 500nm', color='r')

713

axes[0, 1].grid(True)

714

715

# Clearsky conditions

716

axes[1, 0].plot(seasonal_df.index, seasonal_df['clearsky_index'], 'ko-', linewidth=2)

717

axes[1, 0].set_title('Seasonal Clearsky Index')

718

axes[1, 0].set_ylabel('Clearsky Index')

719

axes[1, 0].grid(True)

720

721

# Spectral correction factors

722

spectral_cols = ['spectral_sapm', 'spectral_firstsolar', 'spectral_caballero', 'spectral_pvspec']

723

colors = ['blue', 'red', 'green', 'orange']

724

labels = ['SAPM', 'First Solar', 'Caballero', 'PVSPEC']

725

726

for i, (col, color, label) in enumerate(zip(spectral_cols, colors, labels)):

727

axes[1, 1].plot(seasonal_df.index, seasonal_df[col], 'o-',

728

color=color, linewidth=2, label=label)

729

730

axes[1, 1].set_title('Spectral Correction Factors')

731

axes[1, 1].set_ylabel('Spectral Factor')

732

axes[1, 1].legend()

733

axes[1, 1].grid(True)

734

735

# Annual performance impact

736

reference_factor = 1.0

737

performance_impact = {}

738

739

for col in spectral_cols:

740

# Calculate monthly energy impact

741

monthly_impact = (seasonal_df[col] - reference_factor) * 100

742

annual_avg_impact = monthly_impact.mean()

743

performance_impact[col] = {

744

'monthly': monthly_impact,

745

'annual_avg': annual_avg_impact,

746

'seasonal_range': monthly_impact.max() - monthly_impact.min()

747

}

748

749

# Plot performance impacts

750

months = seasonal_df.index

751

for i, (col, color, label) in enumerate(zip(spectral_cols, colors, labels)):

752

axes[2, 0].plot(months, performance_impact[col]['monthly'], 'o-',

753

color=color, linewidth=2, label=label)

754

755

axes[2, 0].set_title('Monthly Performance Impact')

756

axes[2, 0].set_xlabel('Month')

757

axes[2, 0].set_ylabel('Performance Impact (%)')

758

axes[2, 0].legend()

759

axes[2, 0].grid(True)

760

axes[2, 0].axhline(y=0, color='k', linestyle='--', alpha=0.5)

761

762

# Summary statistics

763

model_names = [label.replace(' ', '_') for label in labels]

764

annual_avgs = [performance_impact[col]['annual_avg'] for col in spectral_cols]

765

seasonal_ranges = [performance_impact[col]['seasonal_range'] for col in spectral_cols]

766

767

x_pos = np.arange(len(model_names))

768

width = 0.35

769

770

bars1 = axes[2, 1].bar(x_pos - width/2, annual_avgs, width, label='Annual Avg', alpha=0.7)

771

bars2 = axes[2, 1].bar(x_pos + width/2, seasonal_ranges, width, label='Seasonal Range', alpha=0.7)

772

773

axes[2, 1].set_title('Annual Impact Summary')

774

axes[2, 1].set_xlabel('Spectral Model')

775

axes[2, 1].set_ylabel('Performance Impact (%)')

776

axes[2, 1].set_xticks(x_pos)

777

axes[2, 1].set_xticklabels(model_names, rotation=45)

778

axes[2, 1].legend()

779

axes[2, 1].grid(True, axis='y')

780

781

plt.tight_layout()

782

plt.show()

783

784

# Print summary statistics

785

print("\nAnnual Spectral Performance Analysis:")

786

print("="*50)

787

for i, col in enumerate(spectral_cols):

788

label = labels[i]

789

avg_impact = performance_impact[col]['annual_avg']

790

range_impact = performance_impact[col]['seasonal_range']

791

792

print(f"{label:12s}: Annual Avg = {avg_impact:+.2f}%, Seasonal Range = {range_impact:.2f}%")

793

794

print(f"\nLocation: {lat:.2f}°N, {lon:.2f}°W (High altitude, arid climate)")

795

print("Analysis shows seasonal variations in spectral performance due to:")

796

print("- Solar angle variations (airmass)")

797

print("- Seasonal atmospheric moisture")

798

print("- Dust/aerosol loading")

799

print("- Cloud cover patterns")

800

```