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
197    let brk_term = 1.0 - vd / p.breakdown_voltage;
198    let brk_pwr = brk_term.powf(-p.breakdown_exp);
199    let i_breakdown = p.breakdown_factor * vd * g_sh * brk_pwr;
200
201    let i = p.photocurrent - p.saturation_current * v_star.exp_m1() - vd * g_sh - i_recomb
202        - i_breakdown;
203    let v = vd - i * p.resistance_series;
204    let out = Bishop88Output {
205        current: i,
206        voltage: v,
207        power: i * v,
208    };
209
210    if !gradients {
211        return (out, None);
212    }
213
214    let grad_i_recomb = if p.d2mutau > 0.0 {
215        i_recomb / v_recomb
216    } else {
217        0.0
218    };
219    let grad_2i_recomb = if p.d2mutau > 0.0 {
220        2.0 * grad_i_recomb / v_recomb
221    } else {
222        0.0
223    };
224
225    let g_diode = p.saturation_current * v_star.exp() / p.n_ns_vth;
226
227    let brk_pwr_1 = brk_term.powf(-p.breakdown_exp - 1.0);
228    let brk_pwr_2 = brk_term.powf(-p.breakdown_exp - 2.0);
229    let brk_fctr = p.breakdown_factor * g_sh;
230    // Match Python: grad_i_brk = brk_fctr * (brk_pwr + vd * -m * brk_pwr_1)
231    let grad_i_brk = brk_fctr * (brk_pwr + vd * (-p.breakdown_exp) * brk_pwr_1);
232    // Match Python: grad2i_brk = brk_fctr * -m * (2*brk_pwr_1 + vd*(-m-1)*brk_pwr_2)
233    let grad2i_brk = brk_fctr
234        * (-p.breakdown_exp)
235        * (2.0 * brk_pwr_1 + vd * (-p.breakdown_exp - 1.0) * brk_pwr_2);
236
237    let di_dvd = -g_diode - g_sh - grad_i_recomb - grad_i_brk;
238    let dv_dvd = 1.0 - di_dvd * p.resistance_series;
239    let di_dv = di_dvd / dv_dvd;
240    let dp_dv = v * di_dv + i;
241
242    let d2i_dvd = -g_diode / p.n_ns_vth - grad_2i_recomb - grad2i_brk;
243    let d2v_dvd = -d2i_dvd * p.resistance_series;
244    let d2p_dvd = dv_dvd * di_dv + v * (d2i_dvd / dv_dvd - di_dvd * d2v_dvd / (dv_dvd * dv_dvd))
245        + di_dvd;
246
247    let grad = Bishop88Gradients {
248        di_dvd,
249        dv_dvd,
250        di_dv,
251        dp_dv,
252        d2p_dvd,
253    };
254
255    (out, Some(grad))
256}
257
258/// Find current at a given voltage using Bishop88 + Newton-Raphson.
259///
260/// Searches for the diode voltage `vd` such that `bishop88(vd).voltage == voltage`.
261pub fn bishop88_i_from_v(voltage: f64, p: &Bishop88Params) -> f64 {
262    let voc_est = estimate_voc(p.photocurrent, p.saturation_current, p.n_ns_vth);
263    let mut vd = if voc_est < p.ns_vbi {
264        voc_est
265    } else {
266        0.9999 * p.ns_vbi
267    };
268    // initial guess: diode_voltage ~ voltage (assuming small I*Rs)
269    if voltage < vd {
270        vd = voltage;
271    }
272
273    for _ in 0..NEWTON_MAXITER {
274        let (out, grad) = bishop88_inner(vd, p, true);
275        let grad = grad.unwrap();
276        let residual = out.voltage - voltage;
277        let step = residual / grad.dv_dvd;
278        vd -= step;
279        if step.abs() < NEWTON_TOL {
280            break;
281        }
282    }
283
284    bishop88(vd, p).current
285}
286
287/// Find voltage at a given current using Bishop88 + Newton-Raphson.
288///
289/// Searches for the diode voltage `vd` such that `bishop88(vd).current == current`.
290pub fn bishop88_v_from_i(current: f64, p: &Bishop88Params) -> f64 {
291    let voc_est = estimate_voc(p.photocurrent, p.saturation_current, p.n_ns_vth);
292    let mut vd = if voc_est < p.ns_vbi {
293        voc_est
294    } else {
295        0.9999 * p.ns_vbi
296    };
297
298    for _ in 0..NEWTON_MAXITER {
299        let (out, grad) = bishop88_inner(vd, p, true);
300        let grad = grad.unwrap();
301        let residual = out.current - current;
302        // di/dvd is the derivative of current w.r.t. diode voltage
303        let step = residual / grad.di_dvd;
304        vd -= step;
305        if step.abs() < NEWTON_TOL {
306            break;
307        }
308    }
309
310    bishop88(vd, p).voltage
311}
312
313/// Find the maximum power point using Bishop88 + Newton-Raphson.
314///
315/// Searches for diode voltage where dP/dV = 0.
316pub fn bishop88_mpp(p: &Bishop88Params) -> Bishop88Output {
317    let voc_est = estimate_voc(p.photocurrent, p.saturation_current, p.n_ns_vth);
318    // Start at Voc estimate. Only clamp to ns_vbi when breakdown modeling
319    // is active (ns_vbi > 0); otherwise use the Voc estimate directly.
320    let mut vd = if p.ns_vbi > 0.0 && voc_est >= p.ns_vbi {
321        0.9999 * p.ns_vbi
322    } else {
323        voc_est
324    };
325
326    for _ in 0..NEWTON_MAXITER {
327        let (_, grad) = bishop88_inner(vd, p, true);
328        let grad = grad.unwrap();
329        if grad.d2p_dvd.abs() < 1e-30 {
330            break;
331        }
332        // root-find dP/dV = 0, using d2P/(dV*dVd) as the derivative
333        let step = grad.dp_dv / grad.d2p_dvd;
334        vd -= step;
335        // clamp vd to reasonable range
336        if vd < 0.0 {
337            vd = 0.0;
338        }
339        if step.abs() < NEWTON_TOL {
340            break;
341        }
342    }
343
344    bishop88(vd, p)
345}