1pub trait Mount {
2 fn get_surface_tilt(&self) -> f64;
3 fn get_surface_azimuth(&self) -> f64;
4}
5
6#[derive(Debug, Clone)]
7pub struct FixedMount {
8 pub surface_tilt: f64,
9 pub surface_azimuth: f64,
10}
11
12impl Mount for FixedMount {
13 fn get_surface_tilt(&self) -> f64 { self.surface_tilt }
14 fn get_surface_azimuth(&self) -> f64 { self.surface_azimuth }
15}
16
17#[derive(Debug, Clone)]
18pub struct SingleAxisTrackerMount {
19 pub axis_tilt: f64,
20 pub axis_azimuth: f64,
21 pub max_angle: f64,
22 pub backtrack: bool,
23 pub gcr: f64,
24}
25
26impl Mount for SingleAxisTrackerMount {
27 fn get_surface_tilt(&self) -> f64 { self.axis_tilt } fn get_surface_azimuth(&self) -> f64 { self.axis_azimuth }
29}
30
31pub struct Array {
33 pub mount: Box<dyn Mount>,
34 pub nameplate_dc: f64,
35 pub gamma_pdc: f64,
36 pub modules_per_string: u32,
37 pub strings: u32,
38 pub albedo: f64,
39}
40
41pub struct PVSystem {
42 pub arrays: Vec<Array>,
43 pub inverter_capacity: f64,
44}
45
46impl PVSystem {
47 pub fn new(arrays: Vec<Array>, inverter_capacity: f64) -> Self {
48 Self { arrays, inverter_capacity }
49 }
50
51 #[inline]
52 pub fn get_nameplate_dc_total(&self) -> f64 {
53 self.arrays.iter().map(|a| a.nameplate_dc).sum()
54 }
55
56 #[inline]
57 pub fn get_dc_power_total(&self, poa_global: f64, temp_cell: f64) -> f64 {
58 let mut total_power = 0.0;
59 for array in &self.arrays {
60 let pdc = array.nameplate_dc * (poa_global / 1000.0) * (1.0 + array.gamma_pdc * (temp_cell - 25.0));
61 total_power += pdc.max(0.0);
62 }
63 total_power
64 }
65}
66
67const KB_EV: f64 = 8.617333262e-05;
73const KB_J: f64 = 1.380649e-23;
75const Q_C: f64 = 1.602176634e-19;
77
78#[derive(Debug, Clone, Copy)]
84pub struct SDMParams {
85 pub photocurrent: f64,
87 pub saturation_current: f64,
89 pub resistance_series: f64,
91 pub resistance_shunt: f64,
93 pub n_ns_vth: f64,
95}
96
97#[allow(clippy::too_many_arguments)]
115#[inline]
116pub fn calcparams_desoto(
117 effective_irradiance: f64,
118 temp_cell: f64,
119 alpha_sc: f64,
120 a_ref: f64,
121 i_l_ref: f64,
122 i_o_ref: f64,
123 r_sh_ref: f64,
124 r_s: f64,
125 eg_ref: f64,
126 d_eg_dt: f64,
127) -> SDMParams {
128 let irrad_ref = 1000.0;
129 let temp_ref = 25.0;
130 let tref_k = temp_ref + 273.15;
131 let tcell_k = temp_cell + 273.15;
132
133 let eg = eg_ref * (1.0 + d_eg_dt * (tcell_k - tref_k));
134
135 let n_ns_vth = a_ref * (tcell_k / tref_k);
136
137 let photocurrent = effective_irradiance / irrad_ref
138 * (i_l_ref + alpha_sc * (tcell_k - tref_k));
139
140 let saturation_current = i_o_ref
141 * (tcell_k / tref_k).powi(3)
142 * (eg_ref / (KB_EV * tref_k) - eg / (KB_EV * tcell_k)).exp();
143
144 let resistance_shunt = if effective_irradiance > 0.0 {
145 r_sh_ref * (irrad_ref / effective_irradiance)
146 } else {
147 f64::INFINITY
148 };
149
150 SDMParams {
151 photocurrent,
152 saturation_current,
153 resistance_series: r_s,
154 resistance_shunt,
155 n_ns_vth,
156 }
157}
158
159#[allow(clippy::too_many_arguments)]
173#[inline]
174pub fn calcparams_cec(
175 effective_irradiance: f64,
176 temp_cell: f64,
177 alpha_sc: f64,
178 a_ref: f64,
179 i_l_ref: f64,
180 i_o_ref: f64,
181 r_sh_ref: f64,
182 r_s: f64,
183 adjust: f64,
184 eg_ref: f64,
185 d_eg_dt: f64,
186) -> SDMParams {
187 calcparams_desoto(
188 effective_irradiance,
189 temp_cell,
190 alpha_sc * (1.0 - adjust / 100.0),
191 a_ref,
192 i_l_ref,
193 i_o_ref,
194 r_sh_ref,
195 r_s,
196 eg_ref,
197 d_eg_dt,
198 )
199}
200
201#[allow(clippy::too_many_arguments)]
222#[inline]
223pub fn calcparams_pvsyst(
224 effective_irradiance: f64,
225 temp_cell: f64,
226 alpha_sc: f64,
227 gamma_ref: f64,
228 mu_gamma: f64,
229 i_l_ref: f64,
230 i_o_ref: f64,
231 r_sh_ref: f64,
232 r_sh_0: f64,
233 r_s: f64,
234 cells_in_series: u32,
235 eg_ref: f64,
236) -> SDMParams {
237 let irrad_ref = 1000.0;
238 let temp_ref = 25.0;
239 let r_sh_exp: f64 = 5.5;
240 let tref_k = temp_ref + 273.15;
241 let tcell_k = temp_cell + 273.15;
242
243 let gamma = gamma_ref + mu_gamma * (temp_cell - temp_ref);
245
246 let n_ns_vth = gamma * KB_J / Q_C * (cells_in_series as f64) * tcell_k;
248
249 let photocurrent = effective_irradiance / irrad_ref
251 * (i_l_ref + alpha_sc * (tcell_k - tref_k));
252
253 let saturation_current = i_o_ref
255 * (tcell_k / tref_k).powi(3)
256 * ((Q_C * eg_ref) / (KB_J * gamma) * (1.0 / tref_k - 1.0 / tcell_k)).exp();
257
258 let rsh_tmp = (r_sh_ref - r_sh_0 * (-r_sh_exp).exp()) / (1.0 - (-r_sh_exp).exp());
260 let rsh_base = rsh_tmp.max(0.0);
261 let resistance_shunt = rsh_base
262 + (r_sh_0 - rsh_base) * (-r_sh_exp * effective_irradiance / irrad_ref).exp();
263
264 SDMParams {
265 photocurrent,
266 saturation_current,
267 resistance_series: r_s,
268 resistance_shunt,
269 n_ns_vth,
270 }
271}
272
273#[derive(Debug, Clone, Copy)]
279pub struct SAPMParams {
280 pub isco: f64,
282 pub impo: f64,
284 pub voco: f64,
286 pub vmpo: f64,
288 pub aisc: f64,
290 pub aimp: f64,
292 pub bvoco: f64,
294 pub mbvoc: f64,
296 pub bvmpo: f64,
298 pub mbvmp: f64,
300 pub n: f64,
302 pub cells_in_series: u32,
304 pub c0: f64,
306 pub c1: f64,
307 pub c2: f64,
308 pub c3: f64,
309 pub a0: f64,
311 pub a1: f64,
312 pub a2: f64,
313 pub a3: f64,
314 pub a4: f64,
315 pub b0: f64,
317 pub b1: f64,
318 pub b2: f64,
319 pub b3: f64,
320 pub b4: f64,
321 pub b5: f64,
322 pub fd: f64,
324}
325
326#[derive(Debug, Clone, Copy)]
328pub struct SAPMOutput {
329 pub i_sc: f64,
331 pub i_mp: f64,
333 pub v_oc: f64,
335 pub v_mp: f64,
337 pub p_mp: f64,
339}
340
341#[inline]
355pub fn sapm(effective_irradiance: f64, temp_cell: f64, module: &SAPMParams) -> SAPMOutput {
356 let irradiance_ref = 1000.0;
357 let temperature_ref = 25.0;
358
359 let ee = effective_irradiance / irradiance_ref;
360 let dt = temp_cell - temperature_ref;
361
362 let delta = module.n * KB_J * (temp_cell + 273.15) / Q_C;
363 let cells = module.cells_in_series as f64;
364
365 let bvmpo = module.bvmpo + module.mbvmp * (1.0 - ee);
366 let bvoco = module.bvoco + module.mbvoc * (1.0 - ee);
367
368 let log_ee = if ee > 0.0 { ee.ln() } else { f64::NEG_INFINITY };
369
370 let i_sc = module.isco * ee * (1.0 + module.aisc * dt);
371
372 let i_mp = module.impo * (module.c0 * ee + module.c1 * ee.powi(2))
373 * (1.0 + module.aimp * dt);
374
375 let v_oc = (module.voco + cells * delta * log_ee + bvoco * dt).max(0.0);
376
377 let v_mp = (module.vmpo
378 + module.c2 * cells * delta * log_ee
379 + module.c3 * cells * (delta * log_ee).powi(2)
380 + bvmpo * dt)
381 .max(0.0);
382
383 let p_mp = i_mp * v_mp;
384
385 SAPMOutput { i_sc, i_mp, v_oc, v_mp, p_mp }
386}
387
388fn sapm_spectral_factor(airmass_absolute: f64, module: &SAPMParams) -> f64 {
392 let am = airmass_absolute;
393 let f1 = module.a0
394 + module.a1 * am
395 + module.a2 * am.powi(2)
396 + module.a3 * am.powi(3)
397 + module.a4 * am.powi(4);
398
399 if f1.is_nan() { 0.0 } else { f1.max(0.0) }
400}
401
402#[inline]
418pub fn sapm_effective_irradiance(
419 poa_direct: f64,
420 poa_diffuse: f64,
421 airmass_absolute: f64,
422 aoi_val: f64,
423 module: &SAPMParams,
424) -> f64 {
425 let f1 = sapm_spectral_factor(airmass_absolute, module);
426 let f2 = crate::iam::sapm(aoi_val, module.b0, module.b1, module.b2, module.b3, module.b4, module.b5);
427
428 f1 * (poa_direct * f2 + module.fd * poa_diffuse)
429}