Skip to main content

pvlib/
singlediode.rs

1/// Solves the single-diode equation for current `I` given voltage `V` using the Newton-Raphson method.
2///
3/// `I = I_L - I_0 * (exp((V + I*R_s) / (n*N_s*V_th)) - 1) - (V + I*R_s) / R_sh`
4///
5/// # Arguments
6/// * `v` - Given voltage constraint in Volts.
7/// * `photocurr` - Light-generated current `I_L` in Amperes.
8/// * `saturation_curr` - Diode saturation current `I_0` in Amperes.
9/// * `resistance_series` - Series resistance `R_s` in Ohms.
10/// * `resistance_shunt` - Shunt resistance `R_sh` in Ohms.
11/// * `n_ns_vth` - Product of diode ideality factor `n`, series cells `N_s`, and thermal voltage `V_th` in Volts.
12///
13/// # Returns
14/// Current `I` in Amperes.
15pub fn i_from_v(
16    v: f64,
17    photocurr: f64,
18    saturation_curr: f64,
19    resistance_series: f64,
20    resistance_shunt: f64,
21    n_ns_vth: f64,
22) -> f64 {
23    let mut i = photocurr - v / resistance_shunt;
24
25    for _ in 0..100 {
26        let exp_term = ((v + i * resistance_series) / n_ns_vth).exp();
27        let f = photocurr
28            - saturation_curr * (exp_term - 1.0)
29            - (v + i * resistance_series) / resistance_shunt
30            - i;
31        let df_di = -saturation_curr * (resistance_series / n_ns_vth) * exp_term
32            - (resistance_series / resistance_shunt)
33            - 1.0;
34        let step = f / df_di;
35        i -= step;
36        if step.abs() < 1e-6 {
37            break;
38        }
39    }
40
41    i
42}
43
44/// Solves the single-diode equation for voltage `V` given current `I`.
45///
46/// # Returns
47/// Voltage `V` in Volts.
48pub fn v_from_i(
49    i: f64,
50    photocurr: f64,
51    saturation_curr: f64,
52    resistance_series: f64,
53    resistance_shunt: f64,
54    n_ns_vth: f64,
55) -> f64 {
56    let mut v = n_ns_vth * ((photocurr - i) / saturation_curr).ln() - i * resistance_series;
57    if v.is_nan() {
58        v = 0.0;
59    }
60
61    for _ in 0..100 {
62        let exp_term = ((v + i * resistance_series) / n_ns_vth).exp();
63        let f = photocurr
64            - saturation_curr * (exp_term - 1.0)
65            - (v + i * resistance_series) / resistance_shunt
66            - i;
67        let df_dv = -saturation_curr / n_ns_vth * exp_term - 1.0 / resistance_shunt;
68        let step = f / df_dv;
69        v -= step;
70        if step.abs() < 1e-6 {
71            break;
72        }
73    }
74
75    v
76}
77
78// ---------------------------------------------------------------------------
79// Bishop88 single diode model
80// ---------------------------------------------------------------------------
81
82/// Default built-in voltage per cell junction for a:Si, CdTe (Mertens et al.)
83pub const VOLTAGE_BUILTIN: f64 = 0.9;
84
85const NEWTON_TOL: f64 = 1e-6;
86const NEWTON_MAXITER: usize = 100;
87
88/// Rough estimate of open circuit voltage.
89///
90/// Assumes infinite shunt resistance and zero series resistance:
91/// `Voc_est = nNsVth * ln(Iph / I0 + 1)`
92pub fn estimate_voc(photocurrent: f64, saturation_current: f64, n_ns_vth: f64) -> f64 {
93    n_ns_vth * (photocurrent / saturation_current + 1.0).ln()
94}
95
96/// Output of the Bishop88 model at one operating point.
97#[derive(Debug, Clone, Copy, PartialEq)]
98pub struct Bishop88Output {
99    pub current: f64,
100    pub voltage: f64,
101    pub power: f64,
102}
103
104/// Gradients from the Bishop88 model.
105#[derive(Debug, Clone, Copy, PartialEq)]
106pub struct Bishop88Gradients {
107    /// dI/dVd
108    pub di_dvd: f64,
109    /// dV/dVd
110    pub dv_dvd: f64,
111    /// dI/dV
112    pub di_dv: f64,
113    /// dP/dV
114    pub dp_dv: f64,
115    /// d2P/(dV dVd)
116    pub d2p_dvd: f64,
117}
118
119/// Parameters for the Bishop88 model including optional breakdown terms.
120#[derive(Debug, Clone, Copy)]
121pub struct Bishop88Params {
122    pub photocurrent: f64,
123    pub saturation_current: f64,
124    pub resistance_series: f64,
125    pub resistance_shunt: f64,
126    pub n_ns_vth: f64,
127    /// PVSyst recombination parameter (d^2 / mu*tau). Default 0.
128    pub d2mutau: f64,
129    /// Ns * Vbi. Default f64::INFINITY.
130    pub ns_vbi: f64,
131    /// Fraction of ohmic current in avalanche breakdown. Default 0.
132    pub breakdown_factor: f64,
133    /// Reverse breakdown voltage. Default -5.5 V.
134    pub breakdown_voltage: f64,
135    /// Avalanche breakdown exponent. Default 3.28.
136    pub breakdown_exp: f64,
137}
138
139impl Bishop88Params {
140    /// Create params with no recombination or breakdown terms.
141    pub fn new(
142        photocurrent: f64,
143        saturation_current: f64,
144        resistance_series: f64,
145        resistance_shunt: f64,
146        n_ns_vth: f64,
147    ) -> Self {
148        Self {
149            photocurrent,
150            saturation_current,
151            resistance_series,
152            resistance_shunt,
153            n_ns_vth,
154            d2mutau: 0.0,
155            ns_vbi: f64::INFINITY,
156            breakdown_factor: 0.0,
157            breakdown_voltage: -5.5,
158            breakdown_exp: 3.28,
159        }
160    }
161}
162
163/// Explicit calculation of (I, V, P) on the IV curve using Bishop (1988).
164///
165/// `diode_voltage` is the voltage across the diode (Vd = V + I*Rs).
166pub fn bishop88(diode_voltage: f64, p: &Bishop88Params) -> Bishop88Output {
167    let (out, _) = bishop88_inner(diode_voltage, p, false);
168    out
169}
170
171/// Bishop88 with gradients.
172pub fn bishop88_with_gradients(
173    diode_voltage: f64,
174    p: &Bishop88Params,
175) -> (Bishop88Output, Bishop88Gradients) {
176    let (out, grad) = bishop88_inner(diode_voltage, p, true);
177    (out, grad.unwrap())
178}
179
180fn bishop88_inner(
181    vd: f64,
182    p: &Bishop88Params,
183    gradients: bool,
184) -> (Bishop88Output, Option<Bishop88Gradients>) {
185    // recombination loss current
186    let (i_recomb, v_recomb) = if p.d2mutau > 0.0 {
187        let vr = p.ns_vbi - vd;
188        (p.photocurrent * p.d2mutau / vr, vr)
189    } else {
190        (0.0, f64::INFINITY)
191    };
192
193    let v_star = vd / p.n_ns_vth;
194    let g_sh = 1.0 / p.resistance_shunt;
195
196    // breakdown term (only compute when breakdown_factor is nonzero)
197    let (brk_pwr, i_breakdown) = if p.breakdown_factor != 0.0 {
198        let brk_term = 1.0 - vd / p.breakdown_voltage;
199        if brk_term <= 0.0 {
200            (f64::INFINITY, f64::INFINITY)
201        } else {
202            let bp = brk_term.powf(-p.breakdown_exp);
203            (bp, p.breakdown_factor * vd * g_sh * bp)
204        }
205    } else {
206        (1.0, 0.0)
207    };
208
209    let i = p.photocurrent - p.saturation_current * v_star.exp_m1() - vd * g_sh - i_recomb
210        - i_breakdown;
211    let v = vd - i * p.resistance_series;
212    let out = Bishop88Output {
213        current: i,
214        voltage: v,
215        power: i * v,
216    };
217
218    if !gradients {
219        return (out, None);
220    }
221
222    let grad_i_recomb = if p.d2mutau > 0.0 {
223        i_recomb / v_recomb
224    } else {
225        0.0
226    };
227    let grad_2i_recomb = if p.d2mutau > 0.0 {
228        2.0 * grad_i_recomb / v_recomb
229    } else {
230        0.0
231    };
232
233    let g_diode = p.saturation_current * v_star.exp() / p.n_ns_vth;
234
235    let (grad_i_brk, grad2i_brk) = if p.breakdown_factor != 0.0 {
236        let brk_term = 1.0 - vd / p.breakdown_voltage;
237        if brk_term <= 0.0 {
238            (f64::INFINITY, f64::INFINITY)
239        } else {
240            let brk_pwr_1 = brk_term.powf(-p.breakdown_exp - 1.0);
241            let brk_pwr_2 = brk_term.powf(-p.breakdown_exp - 2.0);
242            let brk_fctr = p.breakdown_factor * g_sh;
243            let gi = brk_fctr * (brk_pwr + vd * (-p.breakdown_exp) * brk_pwr_1);
244            let g2i = brk_fctr
245                * (-p.breakdown_exp)
246                * (2.0 * brk_pwr_1 + vd * (-p.breakdown_exp - 1.0) * brk_pwr_2);
247            (gi, g2i)
248        }
249    } else {
250        (0.0, 0.0)
251    };
252
253    let di_dvd = -g_diode - g_sh - grad_i_recomb - grad_i_brk;
254    let dv_dvd = 1.0 - di_dvd * p.resistance_series;
255    let di_dv = di_dvd / dv_dvd;
256    let dp_dv = v * di_dv + i;
257
258    let d2i_dvd = -g_diode / p.n_ns_vth - grad_2i_recomb - grad2i_brk;
259    let d2v_dvd = -d2i_dvd * p.resistance_series;
260    let d2p_dvd = dv_dvd * di_dv + v * (d2i_dvd / dv_dvd - di_dvd * d2v_dvd / (dv_dvd * dv_dvd))
261        + di_dvd;
262
263    let grad = Bishop88Gradients {
264        di_dvd,
265        dv_dvd,
266        di_dv,
267        dp_dv,
268        d2p_dvd,
269    };
270
271    (out, Some(grad))
272}
273
274/// Find current at a given voltage using Bishop88 + Newton-Raphson.
275///
276/// Searches for the diode voltage `vd` such that `bishop88(vd).voltage == voltage`.
277pub fn bishop88_i_from_v(voltage: f64, p: &Bishop88Params) -> f64 {
278    // Start from voltage as initial guess (matching pvlib-python)
279    let mut vd = voltage;
280    // Only clamp to ns_vbi if it is finite, positive, and vd exceeds it
281    if p.ns_vbi.is_finite() && p.ns_vbi > 0.0 && vd >= p.ns_vbi {
282        vd = 0.9999 * p.ns_vbi;
283    }
284
285    for _ in 0..NEWTON_MAXITER {
286        let (out, grad) = bishop88_inner(vd, p, true);
287        let grad = grad.unwrap();
288        let residual = out.voltage - voltage;
289        let step = residual / grad.dv_dvd;
290        vd -= step;
291        if step.abs() < NEWTON_TOL {
292            break;
293        }
294    }
295
296    bishop88(vd, p).current
297}
298
299/// Find voltage at a given current using Bishop88 + Newton-Raphson.
300///
301/// Searches for the diode voltage `vd` such that `bishop88(vd).current == current`.
302pub fn bishop88_v_from_i(current: f64, p: &Bishop88Params) -> f64 {
303    let voc_est = estimate_voc(p.photocurrent, p.saturation_current, p.n_ns_vth);
304    let mut vd = if p.ns_vbi.is_finite() && p.ns_vbi > 0.0 && voc_est >= p.ns_vbi {
305        0.9999 * p.ns_vbi
306    } else {
307        voc_est
308    };
309
310    for _ in 0..NEWTON_MAXITER {
311        let (out, grad) = bishop88_inner(vd, p, true);
312        let grad = grad.unwrap();
313        let residual = out.current - current;
314        // di/dvd is the derivative of current w.r.t. diode voltage
315        let step = residual / grad.di_dvd;
316        vd -= step;
317        if step.abs() < NEWTON_TOL {
318            break;
319        }
320    }
321
322    bishop88(vd, p).voltage
323}
324
325/// Find the maximum power point using Bishop88 + Newton-Raphson.
326///
327/// Searches for diode voltage where dP/dV = 0.
328pub fn bishop88_mpp(p: &Bishop88Params) -> Bishop88Output {
329    let voc_est = estimate_voc(p.photocurrent, p.saturation_current, p.n_ns_vth);
330    let mut vd = if p.ns_vbi.is_finite() && p.ns_vbi > 0.0 && voc_est >= p.ns_vbi {
331        0.9999 * p.ns_vbi
332    } else {
333        voc_est
334    };
335
336    for _ in 0..NEWTON_MAXITER {
337        let (_, grad) = bishop88_inner(vd, p, true);
338        let grad = grad.unwrap();
339        if grad.d2p_dvd.abs() < 1e-30 {
340            break;
341        }
342        // root-find dP/dV = 0, using d2P/(dV*dVd) as the derivative
343        let step = grad.dp_dv / grad.d2p_dvd;
344        vd -= step;
345        // clamp vd to reasonable range
346        if vd < 0.0 {
347            vd = 0.0;
348        }
349        if step.abs() < NEWTON_TOL {
350            break;
351        }
352    }
353
354    bishop88(vd, p)
355}