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 pub fn get_nameplate_dc_total(&self) -> f64 {
52 self.arrays.iter().map(|a| a.nameplate_dc).sum()
53 }
54
55 pub fn get_dc_power_total(&self, poa_global: f64, temp_cell: f64) -> f64 {
56 let mut total_power = 0.0;
57 for array in &self.arrays {
58 let pdc = array.nameplate_dc * (poa_global / 1000.0) * (1.0 + array.gamma_pdc * (temp_cell - 25.0));
59 total_power += pdc.max(0.0);
60 }
61 total_power
62 }
63}
64
65const KB_EV: f64 = 8.617333262e-05;
71const KB_J: f64 = 1.380649e-23;
73const Q_C: f64 = 1.602176634e-19;
75
76#[derive(Debug, Clone, Copy)]
82pub struct SDMParams {
83 pub photocurrent: f64,
85 pub saturation_current: f64,
87 pub resistance_series: f64,
89 pub resistance_shunt: f64,
91 pub n_ns_vth: f64,
93}
94
95#[allow(clippy::too_many_arguments)]
113pub fn calcparams_desoto(
114 effective_irradiance: f64,
115 temp_cell: f64,
116 alpha_sc: f64,
117 a_ref: f64,
118 i_l_ref: f64,
119 i_o_ref: f64,
120 r_sh_ref: f64,
121 r_s: f64,
122 eg_ref: f64,
123 d_eg_dt: f64,
124) -> SDMParams {
125 let irrad_ref = 1000.0;
126 let temp_ref = 25.0;
127 let tref_k = temp_ref + 273.15;
128 let tcell_k = temp_cell + 273.15;
129
130 let eg = eg_ref * (1.0 + d_eg_dt * (tcell_k - tref_k));
131
132 let n_ns_vth = a_ref * (tcell_k / tref_k);
133
134 let photocurrent = effective_irradiance / irrad_ref
135 * (i_l_ref + alpha_sc * (tcell_k - tref_k));
136
137 let saturation_current = i_o_ref
138 * (tcell_k / tref_k).powi(3)
139 * (eg_ref / (KB_EV * tref_k) - eg / (KB_EV * tcell_k)).exp();
140
141 let resistance_shunt = if effective_irradiance > 0.0 {
142 r_sh_ref * (irrad_ref / effective_irradiance)
143 } else {
144 f64::INFINITY
145 };
146
147 SDMParams {
148 photocurrent,
149 saturation_current,
150 resistance_series: r_s,
151 resistance_shunt,
152 n_ns_vth,
153 }
154}
155
156#[allow(clippy::too_many_arguments)]
170pub fn calcparams_cec(
171 effective_irradiance: f64,
172 temp_cell: f64,
173 alpha_sc: f64,
174 a_ref: f64,
175 i_l_ref: f64,
176 i_o_ref: f64,
177 r_sh_ref: f64,
178 r_s: f64,
179 adjust: f64,
180 eg_ref: f64,
181 d_eg_dt: f64,
182) -> SDMParams {
183 calcparams_desoto(
184 effective_irradiance,
185 temp_cell,
186 alpha_sc * (1.0 - adjust / 100.0),
187 a_ref,
188 i_l_ref,
189 i_o_ref,
190 r_sh_ref,
191 r_s,
192 eg_ref,
193 d_eg_dt,
194 )
195}
196
197#[allow(clippy::too_many_arguments)]
218pub fn calcparams_pvsyst(
219 effective_irradiance: f64,
220 temp_cell: f64,
221 alpha_sc: f64,
222 gamma_ref: f64,
223 mu_gamma: f64,
224 i_l_ref: f64,
225 i_o_ref: f64,
226 r_sh_ref: f64,
227 r_sh_0: f64,
228 r_s: f64,
229 cells_in_series: u32,
230 eg_ref: f64,
231) -> SDMParams {
232 let irrad_ref = 1000.0;
233 let temp_ref = 25.0;
234 let r_sh_exp = 5.5;
235 let tref_k = temp_ref + 273.15;
236 let tcell_k = temp_cell + 273.15;
237
238 let gamma = gamma_ref + mu_gamma * (temp_cell - temp_ref);
240
241 let n_ns_vth = gamma * KB_J / Q_C * (cells_in_series as f64) * tcell_k;
243
244 let photocurrent = effective_irradiance / irrad_ref
246 * (i_l_ref + alpha_sc * (tcell_k - tref_k));
247
248 let saturation_current = i_o_ref
250 * (tcell_k / tref_k).powi(3)
251 * ((Q_C * eg_ref) / (KB_J * gamma) * (1.0 / tref_k - 1.0 / tcell_k)).exp();
252
253 let rsh_tmp = (r_sh_ref - r_sh_0 * (-r_sh_exp as f64).exp()) / (1.0 - (-r_sh_exp as f64).exp());
255 let rsh_base = rsh_tmp.max(0.0);
256 let resistance_shunt = rsh_base
257 + (r_sh_0 - rsh_base) * (-r_sh_exp * effective_irradiance / irrad_ref).exp();
258
259 SDMParams {
260 photocurrent,
261 saturation_current,
262 resistance_series: r_s,
263 resistance_shunt,
264 n_ns_vth,
265 }
266}
267
268#[derive(Debug, Clone, Copy)]
274pub struct SAPMParams {
275 pub isco: f64,
277 pub impo: f64,
279 pub voco: f64,
281 pub vmpo: f64,
283 pub aisc: f64,
285 pub aimp: f64,
287 pub bvoco: f64,
289 pub mbvoc: f64,
291 pub bvmpo: f64,
293 pub mbvmp: f64,
295 pub n: f64,
297 pub cells_in_series: u32,
299 pub c0: f64,
301 pub c1: f64,
302 pub c2: f64,
303 pub c3: f64,
304 pub a0: f64,
306 pub a1: f64,
307 pub a2: f64,
308 pub a3: f64,
309 pub a4: f64,
310 pub b0: f64,
312 pub b1: f64,
313 pub b2: f64,
314 pub b3: f64,
315 pub b4: f64,
316 pub b5: f64,
317 pub fd: f64,
319}
320
321#[derive(Debug, Clone, Copy)]
323pub struct SAPMOutput {
324 pub i_sc: f64,
326 pub i_mp: f64,
328 pub v_oc: f64,
330 pub v_mp: f64,
332 pub p_mp: f64,
334}
335
336pub fn sapm(effective_irradiance: f64, temp_cell: f64, module: &SAPMParams) -> SAPMOutput {
350 let irradiance_ref = 1000.0;
351 let temperature_ref = 25.0;
352
353 let ee = effective_irradiance / irradiance_ref;
354 let dt = temp_cell - temperature_ref;
355
356 let delta = module.n * KB_J * (temp_cell + 273.15) / Q_C;
357 let cells = module.cells_in_series as f64;
358
359 let bvmpo = module.bvmpo + module.mbvmp * (1.0 - ee);
360 let bvoco = module.bvoco + module.mbvoc * (1.0 - ee);
361
362 let log_ee = if ee > 0.0 { ee.ln() } else { f64::NEG_INFINITY };
363
364 let i_sc = module.isco * ee * (1.0 + module.aisc * dt);
365
366 let i_mp = module.impo * (module.c0 * ee + module.c1 * ee.powi(2))
367 * (1.0 + module.aimp * dt);
368
369 let v_oc = (module.voco + cells * delta * log_ee + bvoco * dt).max(0.0);
370
371 let v_mp = (module.vmpo
372 + module.c2 * cells * delta * log_ee
373 + module.c3 * cells * (delta * log_ee).powi(2)
374 + bvmpo * dt)
375 .max(0.0);
376
377 let p_mp = i_mp * v_mp;
378
379 SAPMOutput { i_sc, i_mp, v_oc, v_mp, p_mp }
380}
381
382fn sapm_spectral_factor(airmass_absolute: f64, module: &SAPMParams) -> f64 {
386 let am = airmass_absolute;
387 let f1 = module.a0
388 + module.a1 * am
389 + module.a2 * am.powi(2)
390 + module.a3 * am.powi(3)
391 + module.a4 * am.powi(4);
392
393 if f1.is_nan() { 0.0 } else { f1.max(0.0) }
394}
395
396pub fn sapm_effective_irradiance(
412 poa_direct: f64,
413 poa_diffuse: f64,
414 airmass_absolute: f64,
415 aoi_val: f64,
416 module: &SAPMParams,
417) -> f64 {
418 let f1 = sapm_spectral_factor(airmass_absolute, module);
419 let f2 = crate::iam::sapm(aoi_val, module.b0, module.b1, module.b2, module.b3, module.b4, module.b5);
420
421 f1 * (poa_direct * f2 + module.fd * poa_diffuse)
422}