Skip to main content

pvlib/
spectrum.rs

1/// Spectral Mismatch Modifier (SMM)
2/// 
3/// Applies a polynomial modifier based on Air Mass to account for spectral variations
4/// in module efficiency compared to STC (AM 1.5).
5/// 
6/// # Arguments
7/// * `airmass_absolute` - The absolute air mass.
8/// * `coefficients` - 6 empirical polynomial coefficients [C0, C1, C2, C3, C4, C5].
9/// 
10/// # Returns
11/// A unitless modifier to multiply against DC power or current.
12#[inline]
13pub fn spectral_mismatch_modifier(airmass_absolute: f64, coefficients: [f64; 6]) -> f64 {
14    let x = airmass_absolute.max(1.0);
15    
16    let ans = coefficients[0]
17        + coefficients[1] * x
18        + coefficients[2] * x.powi(2)
19        + coefficients[3] * x.powi(3)
20        + coefficients[4] * x.powi(4)
21        + coefficients[5] * x.powi(5);
22        
23    ans.max(0.0)
24}
25
26/// First Solar empirical spectral mismatch modifier.
27/// 
28/// Evaluates the impact of precipitable water and airmass on CdTe and generic silicon spectrums.
29/// 
30/// # References
31/// Lee, M. et al., 2018. "A new empirical model for spectral correction of PV performance."
32#[inline]
33pub fn first_solar_spectral_correction(precipitable_water: f64, airmass_absolute: f64, coefficients: [f64; 6]) -> f64 {
34    let pw = precipitable_water;
35    let am = airmass_absolute;
36    let m = coefficients[0] 
37          + coefficients[1]*am 
38          + coefficients[2]*pw 
39          + coefficients[3]*am.sqrt()
40          + coefficients[4]*pw.sqrt()
41          + coefficients[5]*am/pw.sqrt();
42          
43    m.max(0.0)
44}
45
46/// SAPM spectral loss factor.
47///
48/// Fourth-order polynomial of absolute airmass: f1 = A0 + A1*AM + A2*AM^2 + A3*AM^3 + A4*AM^4.
49///
50/// # Arguments
51/// * `airmass_absolute` - Absolute airmass. NaN values result in 0.
52/// * `coefficients` - Array of 5 coefficients [A0, A1, A2, A3, A4].
53///
54/// # Returns
55/// Spectral mismatch factor (unitless, non-negative).
56#[inline]
57pub fn spectral_factor_sapm(airmass_absolute: f64, coefficients: [f64; 5]) -> f64 {
58    if airmass_absolute.is_nan() {
59        return 0.0;
60    }
61    let am = airmass_absolute;
62    let f1 = coefficients[0]
63        + coefficients[1] * am
64        + coefficients[2] * am.powi(2)
65        + coefficients[3] * am.powi(3)
66        + coefficients[4] * am.powi(4);
67    f1.max(0.0)
68}
69
70/// Caballero (2018) spectral mismatch factor.
71///
72/// Estimates spectral modifier from airmass, AOD at 500nm, and precipitable water.
73///
74/// # Arguments
75/// * `precipitable_water` - Atmospheric precipitable water in cm.
76/// * `airmass_absolute` - Absolute airmass.
77/// * `aod500` - Aerosol optical depth at 500 nm.
78/// * `coefficients` - Array of 12 coefficients from Caballero (2018).
79///
80/// # Returns
81/// Spectral mismatch modifier (unitless).
82#[inline]
83pub fn spectral_factor_caballero(
84    precipitable_water: f64,
85    airmass_absolute: f64,
86    aod500: f64,
87    coefficients: [f64; 12],
88) -> f64 {
89    let ama = airmass_absolute;
90    let aod500_ref = 0.084;
91    let pw_ref = 1.4164;
92
93    let c = coefficients;
94
95    // f_AM: 4th order polynomial in airmass
96    let f_am = c[0] + c[1] * ama + c[2] * ama.powi(2) + c[3] * ama.powi(3) + c[4] * ama.powi(4);
97
98    // f_AOD: Eq 6 with Table 1 selectors (c[10], c[11])
99    let f_aod = (aod500 - aod500_ref)
100        * (c[5] + c[10] * c[6] * ama + c[11] * c[6] * ama.ln() + c[7] * ama.powi(2));
101
102    // f_PW: Eq 7 with Table 1
103    let f_pw = (precipitable_water - pw_ref) * (c[8] + c[9] * ama.ln());
104
105    f_am + f_aod + f_pw
106}
107
108/// Built-in Caballero coefficients for common module types.
109#[inline]
110pub fn caballero_coefficients(module_type: &str) -> Option<[f64; 12]> {
111    match module_type {
112        "cdte" => Some([
113            1.0044, 0.0095, -0.0037, 0.0002, 0.0000, -0.0046, -0.0182, 0.0, 0.0095, 0.0068,
114            0.0, 1.0,
115        ]),
116        "monosi" => Some([
117            0.9706, 0.0377, -0.0123, 0.0025, -0.0002, 0.0159, -0.0165, 0.0, -0.0016, -0.0027,
118            1.0, 0.0,
119        ]),
120        "multisi" => Some([
121            0.9836, 0.0254, -0.0085, 0.0016, -0.0001, 0.0094, -0.0132, 0.0, -0.0002, -0.0011,
122            1.0, 0.0,
123        ]),
124        "cigs" => Some([
125            0.9801, 0.0283, -0.0092, 0.0019, -0.0001, 0.0117, -0.0126, 0.0, -0.0011, -0.0019,
126            1.0, 0.0,
127        ]),
128        "asi" => Some([
129            1.1060, -0.0848, 0.0302, -0.0076, 0.0006, -0.1283, 0.0986, -0.0254, 0.0156, 0.0146,
130            1.0, 0.0,
131        ]),
132        "perovskite" => Some([
133            1.0637, -0.0491, 0.0180, -0.0047, 0.0004, -0.0773, 0.0583, -0.0159, 0.01251,
134            0.0109, 1.0, 0.0,
135        ]),
136        _ => None,
137    }
138}
139