Skip to main content

pvlib/
pvsystem.rs

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 } // Dynamic in reality, simplified here
28    fn get_surface_azimuth(&self) -> f64 { self.axis_azimuth }
29}
30
31/// Represents a subset of a PV system strings with identical orientation
32pub 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
67// ---------------------------------------------------------------------------
68// Physical constants
69// ---------------------------------------------------------------------------
70
71/// Boltzmann constant in eV/K
72const KB_EV: f64 = 8.617333262e-05;
73/// Boltzmann constant in J/K
74const KB_J: f64 = 1.380649e-23;
75/// Elementary charge in coulombs
76const Q_C: f64 = 1.602176634e-19;
77
78// ---------------------------------------------------------------------------
79// Single-diode model parameters output
80// ---------------------------------------------------------------------------
81
82/// Five parameters for the single-diode equation.
83#[derive(Debug, Clone, Copy)]
84pub struct SDMParams {
85    /// Light-generated (photo) current [A]
86    pub photocurrent: f64,
87    /// Diode saturation current [A]
88    pub saturation_current: f64,
89    /// Series resistance [ohm]
90    pub resistance_series: f64,
91    /// Shunt resistance [ohm]
92    pub resistance_shunt: f64,
93    /// Product n * Ns * Vth at operating conditions [V]
94    pub n_ns_vth: f64,
95}
96
97/// Calculate five single-diode model parameters using the De Soto et al. model.
98///
99/// # Parameters
100/// - `effective_irradiance`: Irradiance converted to photocurrent [W/m2]
101/// - `temp_cell`: Average cell temperature [C]
102/// - `alpha_sc`: Short-circuit current temperature coefficient [A/C]
103/// - `a_ref`: n * Ns * Vth at reference conditions [V]
104/// - `i_l_ref`: Photo current at reference conditions [A]
105/// - `i_o_ref`: Diode saturation current at reference conditions [A]
106/// - `r_sh_ref`: Shunt resistance at reference conditions [ohm]
107/// - `r_s`: Series resistance at reference conditions [ohm]
108/// - `eg_ref`: Bandgap energy at reference temperature [eV] (default 1.121 for c-Si)
109/// - `d_eg_dt`: Temperature dependence of bandgap [1/K] (default -0.0002677)
110///
111/// # References
112/// W. De Soto et al., 2006, "Improvement and validation of a model for
113/// photovoltaic array performance", Solar Energy, vol 80, pp. 78-88.
114#[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/// Calculate five single-diode model parameters using the CEC model.
160///
161/// The CEC model differs from De Soto by applying an adjustment factor
162/// to the short-circuit current temperature coefficient.
163///
164/// # Parameters
165/// Same as [`calcparams_desoto`] plus:
166/// - `adjust`: Adjustment to alpha_sc temperature coefficient [percent]
167///
168/// # References
169/// A. Dobos, 2012, "An Improved Coefficient Calculator for the California
170/// Energy Commission 6 Parameter Photovoltaic Module Model", Journal of
171/// Solar Energy Engineering, vol 134.
172#[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/// Calculate five single-diode model parameters using the PVsyst v6 model.
202///
203/// # Parameters
204/// - `effective_irradiance`: Irradiance converted to photocurrent [W/m2]
205/// - `temp_cell`: Average cell temperature [C]
206/// - `alpha_sc`: Short-circuit current temperature coefficient [A/C]
207/// - `gamma_ref`: Diode ideality factor at reference [unitless]
208/// - `mu_gamma`: Temperature coefficient for diode ideality factor [1/K]
209/// - `i_l_ref`: Photo current at reference conditions [A]
210/// - `i_o_ref`: Saturation current at reference conditions [A]
211/// - `r_sh_ref`: Shunt resistance at reference conditions [ohm]
212/// - `r_sh_0`: Shunt resistance at zero irradiance [ohm]
213/// - `r_s`: Series resistance [ohm]
214/// - `cells_in_series`: Number of cells in series
215/// - `eg_ref`: Bandgap energy at reference temperature [eV]
216///
217/// # References
218/// K. Sauer, T. Roessler, C. W. Hansen, 2015, "Modeling the Irradiance and
219/// Temperature Dependence of Photovoltaic Modules in PVsyst",
220/// IEEE Journal of Photovoltaics v5(1).
221#[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    // gamma adjusted for temperature
244    let gamma = gamma_ref + mu_gamma * (temp_cell - temp_ref);
245
246    // nNsVth = gamma * k/q * Ns * Tcell_K
247    let n_ns_vth = gamma * KB_J / Q_C * (cells_in_series as f64) * tcell_k;
248
249    // Photocurrent
250    let photocurrent = effective_irradiance / irrad_ref
251        * (i_l_ref + alpha_sc * (tcell_k - tref_k));
252
253    // Saturation current (PVsyst uses q*Eg/(k*gamma) formulation)
254    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    // Shunt resistance: PVsyst exponential model
259    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// ---------------------------------------------------------------------------
274// SAPM (Sandia Array Performance Model)
275// ---------------------------------------------------------------------------
276
277/// SAPM module parameters.
278#[derive(Debug, Clone, Copy)]
279pub struct SAPMParams {
280    /// Short-circuit current at reference [A]
281    pub isco: f64,
282    /// Max-power current at reference [A]
283    pub impo: f64,
284    /// Open-circuit voltage at reference [V]
285    pub voco: f64,
286    /// Max-power voltage at reference [V]
287    pub vmpo: f64,
288    /// Isc temperature coefficient [1/C]
289    pub aisc: f64,
290    /// Imp temperature coefficient [1/C]
291    pub aimp: f64,
292    /// Voc temperature coefficient [V/C]
293    pub bvoco: f64,
294    /// Irradiance dependence for BetaVoc [V/C]
295    pub mbvoc: f64,
296    /// Vmp temperature coefficient [V/C]
297    pub bvmpo: f64,
298    /// Irradiance dependence for BetaVmp [V/C]
299    pub mbvmp: f64,
300    /// Diode factor [unitless]
301    pub n: f64,
302    /// Number of cells in series
303    pub cells_in_series: u32,
304    /// Empirical coefficients C0..C3
305    pub c0: f64,
306    pub c1: f64,
307    pub c2: f64,
308    pub c3: f64,
309    /// Airmass coefficients A0..A4
310    pub a0: f64,
311    pub a1: f64,
312    pub a2: f64,
313    pub a3: f64,
314    pub a4: f64,
315    /// AOI coefficients B0..B5
316    pub b0: f64,
317    pub b1: f64,
318    pub b2: f64,
319    pub b3: f64,
320    pub b4: f64,
321    pub b5: f64,
322    /// Fraction of diffuse irradiance used
323    pub fd: f64,
324}
325
326/// SAPM output: key points on the I-V curve.
327#[derive(Debug, Clone, Copy)]
328pub struct SAPMOutput {
329    /// Short-circuit current [A]
330    pub i_sc: f64,
331    /// Current at max-power point [A]
332    pub i_mp: f64,
333    /// Open-circuit voltage [V]
334    pub v_oc: f64,
335    /// Voltage at max-power point [V]
336    pub v_mp: f64,
337    /// Power at max-power point [W]
338    pub p_mp: f64,
339}
340
341/// Sandia PV Array Performance Model (SAPM).
342///
343/// Generates 5 points on a PV module's I-V curve according to
344/// SAND2004-3535.
345///
346/// # Parameters
347/// - `effective_irradiance`: Irradiance reaching module cells [W/m2]
348/// - `temp_cell`: Cell temperature [C]
349/// - `module`: SAPM module parameters
350///
351/// # References
352/// King, D. et al, 2004, "Sandia Photovoltaic Array Performance Model",
353/// SAND Report 3535, Sandia National Laboratories.
354#[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
388/// SAPM spectral factor: fourth-degree polynomial in airmass.
389///
390/// Calculates the spectral mismatch factor f1 for the SAPM model.
391fn 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/// Calculate SAPM effective irradiance.
403///
404/// Accounts for spectral and angle-of-incidence losses using the SAPM model:
405/// `Ee = f1(AM) * (Eb * f2(AOI) + fd * Ed)`
406///
407/// # Parameters
408/// - `poa_direct`: Direct irradiance on the module [W/m2]
409/// - `poa_diffuse`: Diffuse irradiance on the module [W/m2]
410/// - `airmass_absolute`: Absolute airmass [unitless]
411/// - `aoi_val`: Angle of incidence [degrees]
412/// - `module`: SAPM module parameters
413///
414/// # References
415/// King, D. et al, 2004, "Sandia Photovoltaic Array Performance Model",
416/// SAND2004-3535, Sandia National Laboratories.
417#[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}