pvlib/temperature.rs
1/// Sandia Array Performance Model (SAPM) for cell and module temperature.
2///
3/// # Arguments
4/// * `poa_global` - Total incident irradiance on the module in W/m^2.
5/// * `temp_air` - Ambient air temperature in Celsius.
6/// * `wind_speed` - Wind speed in m/s at standard 10m height.
7/// * `a` - SAPM parameter `a` (empirical).
8/// * `b` - SAPM parameter `b` (empirical).
9/// * `delta_t` - SAPM parameter `delta_t` (temperature difference between cell and module back surface).
10/// * `irrad_ref` - Reference irradiance, typically 1000.0 W/m^2.
11///
12/// # Returns
13/// A tuple of `(temp_cell, temp_module)` in Celsius.
14#[inline]
15pub fn sapm_cell_temperature(poa_global: f64, temp_air: f64, wind_speed: f64, a: f64, b: f64, delta_t: f64, irrad_ref: f64) -> (f64, f64) {
16 if poa_global <= 0.0 {
17 return (temp_air, temp_air);
18 }
19
20 let temp_module = temp_air + poa_global * (a + b * wind_speed).exp();
21 let temp_cell = temp_module + (poa_global / irrad_ref) * delta_t;
22
23 (temp_cell, temp_module)
24}
25
26/// PVsyst cell temperature model.
27///
28/// # Arguments
29/// * `poa_global` - Total incident irradiance on the module in W/m^2.
30/// * `temp_air` - Ambient air temperature in Celsius.
31/// * `wind_speed` - Wind speed in m/s.
32/// * `u_c` - Constant heat transfer component (W/(m^2 K)).
33/// * `u_v` - Convective heat transfer component (W/(m^3 s K)).
34/// * `module_efficiency` - Module efficiency as a decimal (e.g. 0.15 for 15%).
35/// * `alpha_absorption` - Absorption coefficient (e.g., 0.9).
36///
37/// # Returns
38/// Cell temperature in Celsius.
39#[inline]
40pub fn pvsyst_cell_temperature(poa_global: f64, temp_air: f64, wind_speed: f64, u_c: f64, u_v: f64, module_efficiency: f64, alpha_absorption: f64) -> f64 {
41 if poa_global <= 0.0 {
42 return temp_air;
43 }
44 let h_total = (u_c + u_v * wind_speed).max(0.01);
45 temp_air + (alpha_absorption * poa_global * (1.0 - module_efficiency)) / h_total
46}
47
48/// Faiman (2008) cell temperature model.
49/// `T_cell = T_ambient + POA / (U0 + U1 * wind_speed)`
50///
51/// # References
52/// Faiman, D., 2008, "Assessing the outdoor operating temperature of photovoltaic modules,"
53/// Progress in Photovoltaics 16(4), pp. 307-315.
54///
55/// # Arguments
56/// * `poa_global` - Total incident irradiance on the module in W/m^2.
57/// * `temp_air` - Ambient air temperature in Celsius.
58/// * `wind_speed` - Wind speed in m/s.
59/// * `u0` - Constant heat transfer coefficient (typically 25.0).
60/// * `u1` - Convective heat transfer coefficient (typically 6.84).
61#[inline]
62pub fn faiman(poa_global: f64, temp_air: f64, wind_speed: f64, u0: f64, u1: f64) -> f64 {
63 if poa_global <= 0.0 {
64 return temp_air;
65 }
66 let h_total = (u0 + u1 * wind_speed).max(0.01);
67 temp_air + poa_global / h_total
68}
69
70/// Fuentes (1987) module temperature model.
71///
72/// Accounts for thermal capacitance and inertia of PV modules.
73///
74/// # References
75/// Fuentes, M.K., 1987. "A simplified thermal model for flat-plate photovoltaic arrays".
76#[inline]
77pub fn fuentes(poa_global: f64, temp_air: f64, wind_speed: f64, inoct: f64) -> f64 {
78 // Simplified equilibrium approximation (actual is differential eq over time)
79 if poa_global <= 0.0 { return temp_air; }
80 let h = 1.2 + 0.8 * wind_speed; // rough convective cooling
81 temp_air + poa_global * (inoct - 20.0) / (800.0 * h)
82}
83
84/// Ross module temperature model.
85///
86/// # References
87/// Ross, R. G., 1980. "Flat-Plate Photovoltaic Array Design Optimization".
88#[inline]
89pub fn ross(poa_global: f64, temp_air: f64, inoct: f64) -> f64 {
90 if poa_global <= 0.0 {
91 return temp_air;
92 }
93 let k = (inoct - 20.0) / 800.0;
94 temp_air + k * poa_global
95}
96
97/// SAPM module back-surface temperature model.
98///
99/// T_module = poa_global * exp(a + b * wind_speed) + temp_air
100///
101/// # Arguments
102/// * `poa_global` - Total incident irradiance [W/m^2].
103/// * `temp_air` - Ambient dry bulb temperature [C].
104/// * `wind_speed` - Wind speed at 10m height [m/s].
105/// * `a` - SAPM parameter a (default -3.56 for glass/polymer open rack).
106/// * `b` - SAPM parameter b (default -0.0750 for glass/polymer open rack).
107///
108/// # Returns
109/// Module back-surface temperature in Celsius.
110///
111/// # References
112/// King, D. et al, 2004, "Sandia Photovoltaic Array Performance Model", SAND Report 3535.
113#[inline]
114pub fn sapm_module(poa_global: f64, temp_air: f64, wind_speed: f64, a: f64, b: f64) -> f64 {
115 poa_global * (a + b * wind_speed).exp() + temp_air
116}
117
118/// SAPM module temperature with default parameters (glass/polymer, open rack).
119#[inline]
120pub fn sapm_module_default(poa_global: f64, temp_air: f64, wind_speed: f64) -> f64 {
121 sapm_module(poa_global, temp_air, wind_speed, -3.56, -0.0750)
122}
123
124/// Calculate cell temperature from module back-surface temperature using SAPM.
125///
126/// T_cell = module_temperature + poa_global / irrad_ref * delta_t
127///
128/// # Arguments
129/// * `module_temperature` - Module back-surface temperature [C].
130/// * `poa_global` - Total incident irradiance [W/m^2].
131/// * `delta_t` - Temperature difference between cell and module back [C].
132/// * `irrad_ref` - Reference irradiance, default 1000 W/m^2.
133///
134/// # Returns
135/// Cell temperature in Celsius.
136///
137/// # References
138/// King, D. et al, 2004, "Sandia Photovoltaic Array Performance Model", SAND Report 3535.
139#[inline]
140pub fn sapm_cell_from_module(module_temperature: f64, poa_global: f64, delta_t: f64, irrad_ref: f64) -> f64 {
141 module_temperature + poa_global / irrad_ref * delta_t
142}
143
144/// SAPM cell from module with default parameters (delta_t=3, irrad_ref=1000).
145#[inline]
146pub fn sapm_cell_from_module_default(module_temperature: f64, poa_global: f64) -> f64 {
147 sapm_cell_from_module(module_temperature, poa_global, 3.0, 1000.0)
148}
149
150/// Adjustment to NOCT for mounting standoff distance.
151///
152/// Piecewise function matching SAM implementation.
153fn adj_for_mounting_standoff(x: f64) -> f64 {
154 if x <= 0.0 {
155 0.0
156 } else if x < 0.5 {
157 18.0
158 } else if x < 1.5 {
159 11.0
160 } else if x < 2.5 {
161 6.0
162 } else if x <= 3.5 {
163 2.0
164 } else {
165 0.0
166 }
167}
168
169/// NOCT cell temperature model from the System Advisor Model (SAM).
170///
171/// Note: This function assumes effective_irradiance == poa_global, i.e., no
172/// spectral or IAM losses are applied to the irradiance used for temperature
173/// calculation. This matches the common use case where effective irradiance
174/// is not separately tracked for the thermal model. In pvlib-python, an
175/// optional `effective_irradiance` parameter allows decoupling these; this
176/// implementation does not yet support that.
177///
178/// # Arguments
179/// * `poa_global` - Total incident irradiance [W/m^2].
180/// * `temp_air` - Ambient dry bulb temperature [C].
181/// * `wind_speed` - Wind speed [m/s].
182/// * `noct` - Nominal operating cell temperature [C].
183/// * `module_efficiency` - Module efficiency at reference conditions.
184/// * `transmittance_absorptance` - Combined tau*alpha coefficient (default 0.9).
185/// * `array_height` - Height above ground: 1 or 2 stories (default 1).
186/// * `mount_standoff` - Distance between module and mounting surface in inches (default 4.0).
187///
188/// # Returns
189/// Cell temperature in Celsius.
190///
191/// # References
192/// Gilman, P. et al, 2018, "SAM Photovoltaic Model Technical Reference Update", NREL/TP-6A20-67399.
193#[allow(clippy::too_many_arguments)]
194#[inline]
195pub fn noct_sam(
196 poa_global: f64,
197 temp_air: f64,
198 wind_speed: f64,
199 noct: f64,
200 module_efficiency: f64,
201 transmittance_absorptance: f64,
202 array_height: u32,
203 mount_standoff: f64,
204) -> f64 {
205 let wind_adj = match array_height {
206 1 => 0.51 * wind_speed,
207 2 => 0.61 * wind_speed,
208 _ => 0.51 * wind_speed, // default to height 1
209 };
210
211 let noct_adj = noct + adj_for_mounting_standoff(mount_standoff);
212 let tau_alpha = transmittance_absorptance;
213
214 let cell_temp_init = poa_global / 800.0 * (noct_adj - 20.0);
215 let heat_loss = 1.0 - module_efficiency / tau_alpha;
216 let wind_loss = 9.5 / (5.7 + 3.8 * wind_adj);
217
218 temp_air + cell_temp_init * heat_loss * wind_loss
219}
220
221/// NOCT SAM with default parameters (transmittance_absorptance=0.9, array_height=1, mount_standoff=4.0).
222#[inline]
223pub fn noct_sam_default(
224 poa_global: f64,
225 temp_air: f64,
226 wind_speed: f64,
227 noct: f64,
228 module_efficiency: f64,
229) -> f64 {
230 noct_sam(poa_global, temp_air, wind_speed, noct, module_efficiency, 0.9, 1, 4.0)
231}
232
233/// Generic linear cell temperature model.
234///
235/// T_cell = temp_air + poa_global * (absorptance - module_efficiency) / (u_c + u_v * wind_speed)
236///
237/// # Arguments
238/// * `poa_global` - Total incident irradiance [W/m^2].
239/// * `temp_air` - Ambient dry bulb temperature [C].
240/// * `wind_speed` - Wind speed at 10m height [m/s].
241/// * `u_c` - Combined heat transfer coefficient at zero wind [(W/m^2)/C].
242/// * `u_v` - Wind influence on heat transfer [(W/m^2)/C/(m/s)].
243/// * `module_efficiency` - Module electrical efficiency.
244/// * `absorptance` - Light absorptance of the module.
245///
246/// # Returns
247/// Cell temperature in Celsius.
248///
249/// # References
250/// Driesse, A. et al, 2022, "PV Module Operating Temperature Model Equivalence
251/// and Parameter Translation", IEEE PVSC.
252#[inline]
253pub fn generic_linear(
254 poa_global: f64,
255 temp_air: f64,
256 wind_speed: f64,
257 u_c: f64,
258 u_v: f64,
259 module_efficiency: f64,
260 absorptance: f64,
261) -> f64 {
262 let heat_input = poa_global * (absorptance - module_efficiency);
263 let total_loss_factor = u_c + u_v * wind_speed;
264 temp_air + heat_input / total_loss_factor
265}
266
267/// Generic linear model with default parameters (u_c=29.0, u_v=0.0, efficiency=0.15, absorptance=0.9).
268#[inline]
269pub fn generic_linear_default(poa_global: f64, temp_air: f64, wind_speed: f64) -> f64 {
270 generic_linear(poa_global, temp_air, wind_speed, 29.0, 0.0, 0.15, 0.9)
271}
272