Skip to main content

pvlib/
pvsystem.rs

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