Skip to main content

pvlib/
iam.rs

1/// ASHRAE Incidence Angle Modifier (IAM) model.
2/// 
3/// `iam = 1 - b0 * (1/cos(aoi) - 1)`
4/// 
5/// # Arguments
6/// * `aoi` - Angle of incidence in degrees.
7/// * `b0` - IAM parameter (typically 0.05).
8/// 
9/// # Returns
10/// IAM multiplier (dimensionless).
11#[inline]
12pub fn ashrae(aoi: f64, b0: f64) -> f64 {
13    let aoi_rad = aoi.to_radians();
14    if aoi_rad >= std::f64::consts::PI / 2.0 {
15        return 0.0;
16    }
17    let cos_aoi = aoi_rad.cos();
18    if cos_aoi <= 0.0 {
19        return 0.0;
20    }
21    
22    let iam = 1.0 - b0 * (1.0 / cos_aoi - 1.0);
23    iam.clamp(0.0, 1.0)
24}
25
26/// Martin and Ruiz Incidence Angle Modifier (IAM) model.
27/// 
28/// # Arguments
29/// * `aoi` - Angle of incidence in degrees.
30/// * `a_r` - Angular losses coefficient.
31/// 
32/// # Returns
33/// IAM multiplier (dimensionless).
34#[inline]
35pub fn martin_ruiz(aoi: f64, a_r: f64) -> f64 {
36    if aoi >= 90.0 {
37        return 0.0;
38    }
39    let aoi_rad = aoi.to_radians();
40    // IAM = (1 - exp(-cos(aoi) / a_r)) / (1 - exp(-1 / a_r))
41    let numerator = 1.0 - (-aoi_rad.cos() / a_r).exp();
42    let denominator = 1.0 - (-1.0 / a_r).exp();
43    if denominator == 0.0 {
44        return 1.0;
45    }
46    (numerator / denominator).clamp(0.0, 1.0)
47}
48
49/// Physical IAM model using Fresnel/Snell's law.
50///
51/// Calculates transmission through a glass cover accounting for
52/// reflection and absorption losses.
53///
54/// # Arguments
55/// * `aoi` - Angle of incidence in degrees.
56/// * `n` - Effective index of refraction (default 1.526 for glass).
57/// * `k` - Glazing extinction coefficient in 1/meters (default 4.0).
58/// * `l` - Glazing thickness in meters (default 0.002).
59///
60/// # Returns
61/// IAM multiplier (dimensionless).
62#[inline]
63pub fn physical(aoi: f64, n: f64, k: f64, l: f64) -> f64 {
64    if aoi.is_nan() {
65        return f64::NAN;
66    }
67    if aoi.abs() >= 90.0 {
68        return 0.0;
69    }
70
71    let aoi_rad = aoi.to_radians();
72    let cos_aoi = aoi_rad.cos().max(0.0);
73    let sin_aoi = (1.0 - cos_aoi * cos_aoi).sqrt();
74
75    // Snell's law: refraction angle
76    let sin_refr = sin_aoi / n;
77    let cos_refr = (1.0 - sin_refr * sin_refr).sqrt();
78
79    // Fresnel reflectance for s and p polarization
80    let n1_cos1 = cos_aoi; // n1=1
81    let n2_cos1 = n * cos_aoi;
82    let n1_cos2 = cos_refr; // n1=1
83    let n2_cos2 = n * cos_refr;
84
85    let rho_s = ((n1_cos1 - n2_cos2) / (n1_cos1 + n2_cos2)).powi(2);
86    let rho_p = ((n1_cos2 - n2_cos1) / (n1_cos2 + n2_cos1)).powi(2);
87    let rho_0 = ((1.0 - n) / (1.0 + n)).powi(2);
88
89    // Transmittance through the interface
90    let tau_s = 1.0 - rho_s;
91    let tau_p = 1.0 - rho_p;
92    let tau_0 = 1.0 - rho_0;
93
94    // Absorption through the glass
95    let tau_s = tau_s * (-k * l / cos_refr).exp();
96    let tau_p = tau_p * (-k * l / cos_refr).exp();
97    let tau_0 = tau_0 * (-k * l).exp();
98
99    // IAM = average of s and p, normalized by normal incidence. Clamp
100    // to [0, 1] so tiny floating-point drift at grazing angles cannot
101    // produce an IAM > 1 or < 0 (matches `pvlib.iam.physical`).
102    ((tau_s + tau_p) / (2.0 * tau_0)).clamp(0.0, 1.0)
103}
104
105/// Schlick approximation of Fresnel reflection as IAM.
106///
107/// `iam = 1 - (1 - cos(aoi))^5`
108///
109/// # Arguments
110/// * `aoi` - Angle of incidence in degrees.
111///
112/// # Returns
113/// IAM multiplier (dimensionless).
114#[inline]
115pub fn schlick(aoi: f64) -> f64 {
116    if aoi.abs() >= 90.0 {
117        return 0.0;
118    }
119    let cos_aoi = aoi.to_radians().cos();
120    1.0 - (1.0 - cos_aoi).powi(5)
121}
122
123/// Diffuse IAM using the Schlick model (analytically integrated).
124///
125/// Returns (iam_sky, iam_ground) for isotropic diffuse irradiance
126/// on a tilted surface.
127///
128/// # Arguments
129/// * `surface_tilt` - Surface tilt angle in degrees from horizontal.
130///
131/// # Returns
132/// Tuple of (iam_sky, iam_ground).
133#[inline]
134pub fn schlick_diffuse(surface_tilt: f64) -> (f64, f64) {
135    let cos_b = surface_tilt.to_radians().cos();
136    let sin_b = surface_tilt.to_radians().sin();
137    let beta_rad = surface_tilt.to_radians();
138    let pi = std::f64::consts::PI;
139
140    // Eq 4 from Xie et al. (2022): relative transmittance of sky diffuse
141    let cuk = (2.0 / (pi * (1.0 + cos_b)))
142        * ((30.0 / 7.0) * pi - (160.0 / 21.0) * beta_rad - (10.0 / 3.0) * pi * cos_b
143            + (160.0 / 21.0) * cos_b * sin_b
144            - (5.0 / 3.0) * pi * cos_b * sin_b.powi(2)
145            + (20.0 / 7.0) * cos_b * sin_b.powi(3)
146            - (5.0 / 16.0) * pi * cos_b * sin_b.powi(4)
147            + (16.0 / 105.0) * cos_b * sin_b.powi(5));
148
149    // Eq 6: relative transmittance of ground-reflected radiation
150    let cug = if surface_tilt < 1e-6 {
151        0.0
152    } else {
153        40.0 / (21.0 * (1.0 - cos_b)) - (1.0 + cos_b) / (1.0 - cos_b) * cuk
154    };
155
156    (cuk, cug)
157}
158
159/// Diffuse IAM using the Martin and Ruiz model.
160///
161/// Returns (iam_sky, iam_ground) for diffuse irradiance.
162///
163/// # Arguments
164/// * `surface_tilt` - Surface tilt angle in degrees from horizontal.
165/// * `a_r` - Angular losses coefficient (default 0.16).
166///
167/// # Returns
168/// Tuple of (iam_sky, iam_ground).
169#[inline]
170pub fn martin_ruiz_diffuse(surface_tilt: f64, a_r: f64) -> (f64, f64) {
171    let pi = std::f64::consts::PI;
172
173    // Avoid undefined results for horizontal or upside-down surfaces
174    let tilt = if surface_tilt == 0.0 {
175        1e-6
176    } else if surface_tilt == 180.0 {
177        180.0 - 1e-6
178    } else {
179        surface_tilt
180    };
181
182    let c1 = 4.0 / 3.0 / pi; // 0.4244
183    let c2 = 0.5 * a_r - 0.154;
184
185    let beta = tilt.to_radians();
186    let cos_beta = beta.cos();
187    let sin_beta = if tilt < 90.0 {
188        beta.sin()
189    } else {
190        (pi - beta).sin()
191    };
192
193    let trig_sky = sin_beta + (pi - beta - sin_beta) / (1.0 + cos_beta);
194    let trig_gnd = sin_beta + (beta - sin_beta) / (1.0 - cos_beta);
195
196    let iam_sky = 1.0 - (-(c1 + c2 * trig_sky) * trig_sky / a_r).exp();
197    let iam_gnd = 1.0 - (-(c1 + c2 * trig_gnd) * trig_gnd / a_r).exp();
198
199    (iam_sky, iam_gnd)
200}
201
202/// Sandia Array Performance Model (SAPM) Incidence Angle Modifier.
203/// 
204/// # References
205/// King, D. et al., 2004, "Sandia Photovoltaic Array Performance Model".
206#[allow(clippy::too_many_arguments)]
207#[inline]
208pub fn sapm(aoi: f64, b0: f64, b1: f64, b2: f64, b3: f64, b4: f64, b5: f64) -> f64 {
209    if !(0.0..=90.0).contains(&aoi) { return 0.0; }
210    
211    let iam = b0 
212            + b1 * aoi 
213            + b2 * aoi.powi(2) 
214            + b3 * aoi.powi(3) 
215            + b4 * aoi.powi(4) 
216            + b5 * aoi.powi(5);
217            
218    iam.clamp(0.0, 1.0)
219}