1pub trait Mount: Send + Sync {
7 fn get_surface_tilt(&self) -> f64;
8 fn get_surface_azimuth(&self) -> f64;
9}
10
11#[derive(Debug, Clone)]
12pub struct FixedMount {
13 pub surface_tilt: f64,
14 pub surface_azimuth: f64,
15}
16
17impl Mount for FixedMount {
18 fn get_surface_tilt(&self) -> f64 { self.surface_tilt }
19 fn get_surface_azimuth(&self) -> f64 { self.surface_azimuth }
20}
21
22#[derive(Debug, Clone)]
23pub struct SingleAxisTrackerMount {
24 pub axis_tilt: f64,
25 pub axis_azimuth: f64,
26 pub max_angle: f64,
27 pub backtrack: bool,
28 pub gcr: f64,
29}
30
31impl Mount for SingleAxisTrackerMount {
32 fn get_surface_tilt(&self) -> f64 { self.axis_tilt } fn get_surface_azimuth(&self) -> f64 { self.axis_azimuth }
34}
35
36pub struct Array {
38 pub mount: Box<dyn Mount>,
39 pub nameplate_dc: f64,
40 pub gamma_pdc: f64,
41 pub modules_per_string: u32,
42 pub strings: u32,
43 pub albedo: f64,
44}
45
46pub struct PVSystem {
47 pub arrays: Vec<Array>,
48 pub inverter_capacity: f64,
49}
50
51impl PVSystem {
52 pub fn new(arrays: Vec<Array>, inverter_capacity: f64) -> Self {
53 Self { arrays, inverter_capacity }
54 }
55
56 #[inline]
57 pub fn get_nameplate_dc_total(&self) -> f64 {
58 self.arrays.iter().map(|a| a.nameplate_dc).sum()
59 }
60
61 #[inline]
62 pub fn get_dc_power_total(&self, poa_global: f64, temp_cell: f64) -> f64 {
63 let mut total_power = 0.0;
64 for array in &self.arrays {
65 let pdc = array.nameplate_dc * (poa_global / 1000.0) * (1.0 + array.gamma_pdc * (temp_cell - 25.0));
66 total_power += pdc.max(0.0);
67 }
68 total_power
69 }
70}
71
72const KB_EV: f64 = 8.617333262e-05;
78const KB_J: f64 = 1.380649e-23;
80const Q_C: f64 = 1.602176634e-19;
82
83#[derive(Debug, Clone, Copy)]
89pub struct SDMParams {
90 pub photocurrent: f64,
92 pub saturation_current: f64,
94 pub resistance_series: f64,
96 pub resistance_shunt: f64,
98 pub n_ns_vth: f64,
100}
101
102#[allow(clippy::too_many_arguments)]
120#[inline]
121pub fn calcparams_desoto(
122 effective_irradiance: f64,
123 temp_cell: f64,
124 alpha_sc: f64,
125 a_ref: f64,
126 i_l_ref: f64,
127 i_o_ref: f64,
128 r_sh_ref: f64,
129 r_s: f64,
130 eg_ref: f64,
131 d_eg_dt: f64,
132) -> SDMParams {
133 let irrad_ref = 1000.0;
134 let temp_ref = 25.0;
135 let tref_k = temp_ref + 273.15;
136 let tcell_k = temp_cell + 273.15;
137
138 let eg = eg_ref * (1.0 + d_eg_dt * (tcell_k - tref_k));
139
140 let n_ns_vth = a_ref * (tcell_k / tref_k);
141
142 let photocurrent = effective_irradiance / irrad_ref
143 * (i_l_ref + alpha_sc * (tcell_k - tref_k));
144
145 let saturation_current = i_o_ref
146 * (tcell_k / tref_k).powi(3)
147 * (eg_ref / (KB_EV * tref_k) - eg / (KB_EV * tcell_k)).exp();
148
149 let resistance_shunt = if effective_irradiance > 0.0 {
150 r_sh_ref * (irrad_ref / effective_irradiance)
151 } else {
152 f64::INFINITY
153 };
154
155 SDMParams {
156 photocurrent,
157 saturation_current,
158 resistance_series: r_s,
159 resistance_shunt,
160 n_ns_vth,
161 }
162}
163
164#[allow(clippy::too_many_arguments)]
178#[inline]
179pub fn calcparams_cec(
180 effective_irradiance: f64,
181 temp_cell: f64,
182 alpha_sc: f64,
183 a_ref: f64,
184 i_l_ref: f64,
185 i_o_ref: f64,
186 r_sh_ref: f64,
187 r_s: f64,
188 adjust: f64,
189 eg_ref: f64,
190 d_eg_dt: f64,
191) -> SDMParams {
192 calcparams_desoto(
193 effective_irradiance,
194 temp_cell,
195 alpha_sc * (1.0 - adjust / 100.0),
196 a_ref,
197 i_l_ref,
198 i_o_ref,
199 r_sh_ref,
200 r_s,
201 eg_ref,
202 d_eg_dt,
203 )
204}
205
206#[allow(clippy::too_many_arguments)]
227#[inline]
228pub fn calcparams_pvsyst(
229 effective_irradiance: f64,
230 temp_cell: f64,
231 alpha_sc: f64,
232 gamma_ref: f64,
233 mu_gamma: f64,
234 i_l_ref: f64,
235 i_o_ref: f64,
236 r_sh_ref: f64,
237 r_sh_0: f64,
238 r_s: f64,
239 cells_in_series: u32,
240 eg_ref: f64,
241) -> SDMParams {
242 let irrad_ref = 1000.0;
243 let temp_ref = 25.0;
244 let r_sh_exp: f64 = 5.5;
245 let tref_k = temp_ref + 273.15;
246 let tcell_k = temp_cell + 273.15;
247
248 let gamma = gamma_ref + mu_gamma * (temp_cell - temp_ref);
250
251 let n_ns_vth = gamma * KB_J / Q_C * (cells_in_series as f64) * tcell_k;
253
254 let photocurrent = effective_irradiance / irrad_ref
256 * (i_l_ref + alpha_sc * (tcell_k - tref_k));
257
258 let saturation_current = i_o_ref
260 * (tcell_k / tref_k).powi(3)
261 * ((Q_C * eg_ref) / (KB_J * gamma) * (1.0 / tref_k - 1.0 / tcell_k)).exp();
262
263 let rsh_tmp = (r_sh_ref - r_sh_0 * (-r_sh_exp).exp()) / (1.0 - (-r_sh_exp).exp());
265 let rsh_base = rsh_tmp.max(0.0);
266 let resistance_shunt = rsh_base
267 + (r_sh_0 - rsh_base) * (-r_sh_exp * effective_irradiance / irrad_ref).exp();
268
269 SDMParams {
270 photocurrent,
271 saturation_current,
272 resistance_series: r_s,
273 resistance_shunt,
274 n_ns_vth,
275 }
276}
277
278#[derive(Debug, Clone, Copy)]
284pub struct SAPMParams {
285 pub isco: f64,
287 pub impo: f64,
289 pub voco: f64,
291 pub vmpo: f64,
293 pub aisc: f64,
295 pub aimp: f64,
297 pub bvoco: f64,
299 pub mbvoc: f64,
301 pub bvmpo: f64,
303 pub mbvmp: f64,
305 pub n: f64,
307 pub cells_in_series: u32,
309 pub c0: f64,
311 pub c1: f64,
312 pub c2: f64,
313 pub c3: f64,
314 pub a0: f64,
316 pub a1: f64,
317 pub a2: f64,
318 pub a3: f64,
319 pub a4: f64,
320 pub b0: f64,
322 pub b1: f64,
323 pub b2: f64,
324 pub b3: f64,
325 pub b4: f64,
326 pub b5: f64,
327 pub fd: f64,
329}
330
331#[derive(Debug, Clone, Copy)]
333pub struct SAPMOutput {
334 pub i_sc: f64,
336 pub i_mp: f64,
338 pub v_oc: f64,
340 pub v_mp: f64,
342 pub p_mp: f64,
344}
345
346#[inline]
360pub fn sapm(effective_irradiance: f64, temp_cell: f64, module: &SAPMParams) -> SAPMOutput {
361 let irradiance_ref = 1000.0;
362 let temperature_ref = 25.0;
363
364 let ee = effective_irradiance / irradiance_ref;
365 let dt = temp_cell - temperature_ref;
366
367 let delta = module.n * KB_J * (temp_cell + 273.15) / Q_C;
368 let cells = module.cells_in_series as f64;
369
370 let bvmpo = module.bvmpo + module.mbvmp * (1.0 - ee);
371 let bvoco = module.bvoco + module.mbvoc * (1.0 - ee);
372
373 let log_ee = if ee > 0.0 { ee.ln() } else { f64::NEG_INFINITY };
374
375 let i_sc = module.isco * ee * (1.0 + module.aisc * dt);
376
377 let i_mp = module.impo * (module.c0 * ee + module.c1 * ee.powi(2))
378 * (1.0 + module.aimp * dt);
379
380 let v_oc = (module.voco + cells * delta * log_ee + bvoco * dt).max(0.0);
381
382 let v_mp = (module.vmpo
383 + module.c2 * cells * delta * log_ee
384 + module.c3 * cells * (delta * log_ee).powi(2)
385 + bvmpo * dt)
386 .max(0.0);
387
388 let p_mp = i_mp * v_mp;
389
390 SAPMOutput { i_sc, i_mp, v_oc, v_mp, p_mp }
391}
392
393fn sapm_spectral_factor(airmass_absolute: f64, module: &SAPMParams) -> f64 {
397 let am = airmass_absolute;
398 let f1 = module.a0
399 + module.a1 * am
400 + module.a2 * am.powi(2)
401 + module.a3 * am.powi(3)
402 + module.a4 * am.powi(4);
403
404 if f1.is_nan() { 0.0 } else { f1.max(0.0) }
405}
406
407#[inline]
423pub fn sapm_effective_irradiance(
424 poa_direct: f64,
425 poa_diffuse: f64,
426 airmass_absolute: f64,
427 aoi_val: f64,
428 module: &SAPMParams,
429) -> f64 {
430 let f1 = sapm_spectral_factor(airmass_absolute, module);
431 let f2 = crate::iam::sapm(aoi_val, module.b0, module.b1, module.b2, module.b3, module.b4, module.b5);
432
433 f1 * (poa_direct * f2 + module.fd * poa_diffuse)
434}