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_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 let grad_i_brk = brk_fctr * (brk_pwr + vd * (-p.breakdown_exp) * brk_pwr_1);
232 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
258pub 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 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
287pub 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 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
313pub fn bishop88_mpp(p: &Bishop88Params) -> Bishop88Output {
317 let voc_est = estimate_voc(p.photocurrent, p.saturation_current, p.n_ns_vth);
318 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 let step = grad.dp_dv / grad.d2p_dvd;
334 vd -= step;
335 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}