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