1pub 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
44pub 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
78pub const VOLTAGE_BUILTIN: f64 = 0.9;
84
85const NEWTON_TOL: f64 = 1e-6;
86const NEWTON_MAXITER: usize = 100;
87
88pub 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#[derive(Debug, Clone, Copy, PartialEq)]
98pub struct Bishop88Output {
99 pub current: f64,
100 pub voltage: f64,
101 pub power: f64,
102}
103
104#[derive(Debug, Clone, Copy, PartialEq)]
106pub struct Bishop88Gradients {
107 pub di_dvd: f64,
109 pub dv_dvd: f64,
111 pub di_dv: f64,
113 pub dp_dv: f64,
115 pub d2p_dvd: f64,
117}
118
119#[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 pub d2mutau: f64,
129 pub ns_vbi: f64,
131 pub breakdown_factor: f64,
133 pub breakdown_voltage: f64,
135 pub breakdown_exp: f64,
137}
138
139impl Bishop88Params {
140 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
163pub fn bishop88(diode_voltage: f64, p: &Bishop88Params) -> Bishop88Output {
167 let (out, _) = bishop88_inner(diode_voltage, p, false);
168 out
169}
170
171pub 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 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 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
274pub fn bishop88_i_from_v(voltage: f64, p: &Bishop88Params) -> f64 {
278 let mut vd = voltage;
280 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
299pub 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 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
325pub 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 let step = grad.dp_dv / grad.d2p_dvd;
344 vd -= step;
345 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}