1use crate::consts::{BOLTZ, CHARGE, KOVERQ, REFTEMP, ROOT2};
17use crate::disto::{
18 DistoCtx, DistoKernels, DistoMode, DistoSetupCtx, d1i2f1, d1i2f12, d1i3f1, d1if12, d1n2f1,
19 d1n2f12, d1n3f1, d1nf12,
20};
21use crate::matrix::{AnKind, InitMode, LoadCtx, MatrixBuilder, Slot, TRASH, Var};
22
23#[derive(Clone, Debug)]
26pub struct DiodeModel {
27 pub is: f64,
29 pub rs: f64,
31 pub n: f64,
33 pub tt: f64,
35 pub cjo: f64,
37 pub vj: f64,
39 pub m: f64,
41 pub eg: f64,
43 pub xti: f64,
45 pub kf: f64,
47 pub af: f64,
48 pub fc: f64,
51 pub bv: Option<f64>,
54 pub ibv: f64,
56 pub tnom: Option<f64>,
58}
59
60impl Default for DiodeModel {
61 fn default() -> Self {
62 DiodeModel {
63 is: 1e-14,
64 rs: 0.0,
65 n: 1.0,
66 tt: 0.0,
67 cjo: 0.0,
68 vj: 1.0,
69 m: 0.5,
70 eg: 1.11,
71 xti: 3.0,
72 kf: 0.0,
73 af: 1.0,
74 fc: 0.5,
75 bv: None,
76 ibv: 1e-3,
77 tnom: None,
78 }
79 }
80}
81
82pub(crate) struct DiodeTemp {
85 pub csat: f64,
86 pub gspr: f64,
87 pub vt: f64,
89 pub vte: f64,
90 pub vcrit: f64,
91 pub brkdwn: Option<f64>,
92 pub t_jct_cap: f64,
94 pub t_dep_cap: f64,
95 pub t_f1: f64,
96 pub f2: f64,
97 pub f3: f64,
98 pub m: f64,
100 pub vj: f64,
101 pub tt: f64,
102 pub notes: Vec<String>,
104}
105
106fn egfet(t: f64) -> f64 {
108 1.16 - (7.02e-4 * t * t) / (t + 1108.0)
109}
110
111impl DiodeTemp {
112 #[allow(clippy::too_many_arguments)]
118 pub fn new(
119 name: &str,
120 model: &DiodeModel,
121 area: f64,
122 mult: f64,
123 temp: f64,
124 default_tnom: f64,
125 reltol: f64,
126 ) -> Self {
127 let mut notes = Vec::new();
128 let mut m = model.m;
129 if m > 0.9 {
130 notes.push(format!(
131 "{name}: grading coefficient too large, limited to 0.9"
132 ));
133 m = 0.9;
134 }
135 let mut eg = model.eg;
136 if eg < 0.1 {
137 notes.push(format!(
138 "{name}: activation energy too small, limited to 0.1"
139 ));
140 eg = 0.1;
141 }
142 let mut fc = model.fc;
143 if fc > 0.95 {
144 notes.push(format!("{name}: coefficient Fc too large, limited to 0.95"));
145 fc = 0.95;
146 }
147 let xfc = f64::ln(1.0 - fc);
148 let f2 = f64::exp((1.0 + m) * xfc);
149 let f3 = 1.0 - fc * (1.0 + m);
150
151 let tnom = model.tnom.unwrap_or(default_tnom);
152 let vt = KOVERQ * temp;
153 let vtnom = KOVERQ * tnom;
154
155 let fact2 = temp / REFTEMP;
158 let eg_t = egfet(temp);
159 let arg = -eg_t / (2.0 * BOLTZ * temp) + 1.1150877 / (BOLTZ * (REFTEMP + REFTEMP));
160 let pbfact = -2.0 * vt * (1.5 * f64::ln(fact2) + CHARGE * arg);
161 let eg_nom = egfet(tnom);
162 let arg1 = -eg_nom / (BOLTZ * 2.0 * tnom) + 1.1150877 / (2.0 * BOLTZ * REFTEMP);
163 let fact1 = tnom / REFTEMP;
164 let pbfact1 = -2.0 * vtnom * (1.5 * f64::ln(fact1) + CHARGE * arg1);
165 let pbo = (model.vj - pbfact1) / fact1;
166 let gmaold = (model.vj - pbo) / pbo;
167 let mut t_jct_cap = model.cjo / (1.0 + m * (400e-6 * (tnom - REFTEMP) - gmaold));
168 let t_jct_pot = pbfact + fact2 * pbo;
169 let gmanew = (t_jct_pot - pbo) / pbo;
170 t_jct_cap *= 1.0 + m * (400e-6 * (temp - REFTEMP) - gmanew);
171
172 let t_sat_cur = model.is
173 * f64::exp(
174 ((temp / tnom) - 1.0) * eg / (model.n * vt)
175 + model.xti / model.n * f64::ln(temp / tnom),
176 );
177 let t_f1 = t_jct_pot * (1.0 - f64::exp((1.0 - m) * xfc)) / (1.0 - m);
178 let t_dep_cap = fc * t_jct_pot;
179 let vte = model.n * vt;
180 let vcrit = vte * f64::ln(vte / (ROOT2 * t_sat_cur));
181 let gspr = if model.rs == 0.0 { 0.0 } else { 1.0 / model.rs } * area * mult;
182
183 let brkdwn = model.bv.map(|bv| {
198 let mut cbv = model.ibv;
199 let sat = t_sat_cur * area;
200 if cbv < sat * bv / vt {
201 cbv = sat * bv / vt;
202 notes.push(format!(
203 "{name}: breakdown current increased to {cbv:.3e} to resolve \
204 incompatibility with specified saturation current"
205 ));
206 bv
207 } else {
208 let tol = reltol * cbv;
209 let vtebrk = model.n * vt;
210 let mut xbv = bv - vtebrk * f64::ln(1.0 + cbv / sat);
211 let mut matched = false;
212 for _ in 0..25 {
213 xbv = bv - vtebrk * f64::ln(cbv / sat + 1.0 - xbv / vt);
214 let xcbv = sat * (f64::exp((bv - xbv) / vtebrk) - 1.0 + xbv / vt);
215 if (xcbv - cbv).abs() <= tol {
216 matched = true;
217 break;
218 }
219 }
220 if !matched {
221 notes.push(format!(
222 "{name}: unable to match forward and reverse diode regions: \
223 bv = {xbv:.5e}, ibv = {cbv:.3e}"
224 ));
225 }
226 xbv
227 }
228 });
229
230 DiodeTemp {
231 csat: t_sat_cur * area * mult,
232 gspr,
233 vt,
234 vte,
235 vcrit,
236 brkdwn,
237 t_jct_cap,
238 t_dep_cap,
239 t_f1,
240 f2,
241 f3,
242 m,
243 vj: model.vj,
244 tt: model.tt,
245 notes,
246 }
247 }
248}
249
250pub(crate) struct DiodeI {
251 pos: Var,
252 neg: Var,
253 pos_prime: Var,
255 t: DiodeTemp,
256 area: f64,
257 off: bool,
258 init_cond: f64,
260 st: usize,
263 slots: [Slot; 7],
264 d: DioDistoCoeffs,
267 sens: DiodeSensBase,
273}
274
275pub(crate) struct DiodeSensBase {
278 pub name: String,
279 pub model: DiodeModel,
280 pub temp: f64,
281 pub m: f64,
282 pub default_tnom: f64,
283 pub reltol: f64,
284}
285
286const DIO_SENS_DC: &[&str] = &[
300 ":is", ":rs", ":n", ":vj", ":m", ":eg", ":xti", ":kf", ":af", ":fc", "_temp", "_dtemp",
301 "_area", "_pj", "_w", "_l", "_m", "_lm", "_lp", "_wm", "_wp",
302];
303const DIO_SENS_AC: &[&str] = &[
304 ":is", ":rs", ":n", ":tt", ":cjo", ":vj", ":m", ":eg", ":xti", ":kf", ":af", ":fc", "_temp",
305 "_dtemp", "_ic", "_area", "_pj", "_w", "_l", "_m", "_lm", "_lp", "_wm", "_wp",
306];
307
308#[derive(Clone, Copy, Debug, Default)]
312struct DioDistoCoeffs {
313 id_x2: f64,
314 id_x3: f64,
315 cdif_x2: f64,
316 cdif_x3: f64,
317 cjnc_x2: f64,
318 cjnc_x3: f64,
319}
320
321const VOLTAGE: usize = 0;
322const CURRENT: usize = 1;
323const CONDUCT: usize = 2;
324const CAP_CHARGE: usize = 3;
325
326impl DiodeI {
327 #[allow(clippy::too_many_arguments)]
328 pub fn new(
329 pos: Var,
330 neg: Var,
331 pos_prime: Var,
332 t: DiodeTemp,
333 area: f64,
334 off: bool,
335 init_cond: f64,
336 st: usize,
337 sens: DiodeSensBase,
338 ) -> Self {
339 DiodeI {
340 pos,
341 neg,
342 pos_prime,
343 t,
344 area,
345 off,
346 init_cond,
347 st,
348 slots: [TRASH; 7],
349 d: DioDistoCoeffs::default(),
350 sens,
351 }
352 }
353
354 fn sens_value(&self, suffix: &str) -> f64 {
356 let m = &self.sens.model;
357 match suffix {
358 ":is" => m.is,
359 ":rs" => m.rs,
360 ":n" => m.n,
361 ":tt" => m.tt,
362 ":cjo" => m.cjo,
363 ":vj" => m.vj,
364 ":m" => m.m,
365 ":eg" => m.eg,
366 ":xti" => m.xti,
367 ":kf" => m.kf,
368 ":af" => m.af,
369 ":fc" => m.fc,
370 "_temp" => self.sens.temp - crate::consts::CTOK,
372 "_area" => self.area,
373 "_m" => self.sens.m,
374 "_ic" => self.init_cond,
375 _ => 0.0,
376 }
377 }
378}
379
380impl crate::devices::Device for DiodeI {
381 fn par_load(&self) -> bool {
382 true
383 }
384
385 fn set_ic(&mut self, rhs: &[f64]) {
391 self.init_cond = rhs[self.pos] - rhs[self.neg];
392 }
393
394 fn conv_test(&self, ctx: &crate::matrix::ConvCtx) -> bool {
397 let vd = ctx.sol[self.pos_prime] - ctx.sol[self.neg];
398 let delvd = vd - ctx.states.get(0, self.st + VOLTAGE);
399 let cd = ctx.states.get(0, self.st + CURRENT);
400 let cdhat = cd + ctx.states.get(0, self.st + CONDUCT) * delvd;
401 let tol = ctx.reltol * f64::max(cdhat.abs(), cd.abs()) + ctx.abstol;
402 (cdhat - cd).abs() <= tol
403 }
404
405 fn alloc(&mut self, mb: &mut MatrixBuilder) {
406 self.slots = [
407 mb.slot(self.pos, self.pos),
408 mb.slot(self.neg, self.neg),
409 mb.slot(self.pos_prime, self.pos_prime),
410 mb.slot(self.pos, self.pos_prime),
411 mb.slot(self.neg, self.pos_prime),
412 mb.slot(self.pos_prime, self.pos),
413 mb.slot(self.pos_prime, self.neg),
414 ];
415 }
416
417 fn load(&mut self, ctx: &mut LoadCtx) {
418 let vte = self.t.vte;
419 let csat = self.t.csat;
420 let st = self.st;
421 let mut check = true;
422
423 let vd = match ctx.mode {
424 InitMode::Smsig => ctx.states.get(0, st + VOLTAGE),
425 InitMode::Tran => ctx.states.get(1, st + VOLTAGE),
426 InitMode::Jct if ctx.kind == AnKind::TranOp && ctx.uic => self.init_cond,
427 InitMode::Jct if self.off => 0.0,
428 InitMode::Jct => self.t.vcrit,
429 InitMode::Fix if self.off => 0.0,
430 _ => {
431 let mut vd = if ctx.mode == InitMode::Pred {
432 for o in [VOLTAGE, CURRENT, CONDUCT] {
438 let v = ctx.states.get(1, st + o);
439 ctx.states.set(0, st + o, v);
440 }
441 ctx.pred(st + VOLTAGE)
442 } else {
443 ctx.sol[self.pos_prime] - ctx.sol[self.neg]
444 };
445 let vd_old = ctx.states.get(0, st + VOLTAGE);
448 match self.t.brkdwn {
449 Some(bv) if vd < f64::min(0.0, -bv + 10.0 * vte) => {
450 let vdtemp =
451 pnjlim(-(vd + bv), -(vd_old + bv), vte, self.t.vcrit, &mut check);
452 vd = -(vdtemp + bv);
453 }
454 _ => {
455 vd = pnjlim(vd, vd_old, vte, self.t.vcrit, &mut check);
456 }
457 }
458 vd
459 }
460 };
461
462 let (cd0, gd0);
464 if vd >= -3.0 * vte {
465 let evd = f64::exp(vd / vte);
466 cd0 = csat * (evd - 1.0) + ctx.gmin * vd;
467 gd0 = csat * evd / vte + ctx.gmin;
468 } else if self.t.brkdwn.is_none() || vd >= -self.t.brkdwn.unwrap_or(0.0) {
469 let arg = 3.0 * vte / (vd * crate::consts::E);
470 let arg = arg * arg * arg;
471 cd0 = -csat * (1.0 + arg) + ctx.gmin * vd;
472 gd0 = csat * 3.0 * arg / vd + ctx.gmin;
473 } else {
474 let bv = self.t.brkdwn.unwrap_or(0.0);
475 let evrev = f64::exp(-(bv + vd) / vte);
476 cd0 = -csat * evrev + ctx.gmin * vd;
477 gd0 = csat * evrev / vte + ctx.gmin;
478 }
479 let mut cd = cd0;
480 let mut gd = gd0;
481
482 if ctx.kind == AnKind::Tran
485 || (ctx.kind == AnKind::TranOp && ctx.uic)
486 || ctx.mode == InitMode::Smsig
487 {
488 let czero = self.t.t_jct_cap * self.area;
489 #[allow(clippy::needless_late_init)] let capd;
491 if vd < self.t.t_dep_cap {
492 let arg = 1.0 - vd / self.t.vj;
493 let sarg = f64::exp(-self.t.m * f64::ln(arg));
494 ctx.states.set(
495 0,
496 st + CAP_CHARGE,
497 self.t.tt * cd + self.t.vj * czero * (1.0 - arg * sarg) / (1.0 - self.t.m),
498 );
499 capd = self.t.tt * gd + czero * sarg;
500 } else {
501 let czof2 = czero / self.t.f2;
502 ctx.states.set(
503 0,
504 st + CAP_CHARGE,
505 self.t.tt * cd
506 + czero * self.t.t_f1
507 + czof2
508 * (self.t.f3 * (vd - self.t.t_dep_cap)
509 + (self.t.m / (self.t.vj + self.t.vj))
510 * (vd * vd - self.t.t_dep_cap * self.t.t_dep_cap)),
511 );
512 capd = self.t.tt * gd + czof2 * (self.t.f3 + self.t.m * vd / self.t.vj);
513 }
514
515 if ctx.mode == InitMode::Smsig {
520 ctx.states.set(0, st + CAP_CHARGE + 1, capd);
521 return;
522 }
523 if ctx.kind == AnKind::Tran {
524 if ctx.mode == InitMode::Tran {
525 let q0 = ctx.states.get(0, st + CAP_CHARGE);
526 ctx.states.set(1, st + CAP_CHARGE, q0);
527 }
528 let (geq, ceq_int) = ctx.integrate(capd, st + CAP_CHARGE);
529 let _ = ceq_int;
530 gd += geq;
531 cd += ctx.states.get(0, st + CAP_CHARGE + 1);
532 if ctx.mode == InitMode::Tran {
533 let c0 = ctx.states.get(0, st + CAP_CHARGE + 1);
534 ctx.states.set(1, st + CAP_CHARGE + 1, c0);
535 }
536 }
537 }
538
539 if !(ctx.mode == InitMode::Fix && self.off) && check {
544 ctx.noncon = true;
545 }
546
547 ctx.states.set(0, st + VOLTAGE, vd);
548 ctx.states.set(0, st + CURRENT, cd);
549 ctx.states.set(0, st + CONDUCT, gd);
550
551 let cdeq = cd - gd * vd;
552 ctx.add_rhs(self.neg, cdeq);
553 ctx.add_rhs(self.pos_prime, -cdeq);
554
555 let gspr = self.t.gspr;
556 ctx.add(self.slots[0], gspr);
557 ctx.add(self.slots[1], gd);
558 ctx.add(self.slots[2], gd + gspr);
559 ctx.add(self.slots[3], -gspr);
560 ctx.add(self.slots[4], -gd);
561 ctx.add(self.slots[5], -gspr);
562 ctx.add(self.slots[6], -gd);
563 }
564
565 fn trunc(&self, t: &crate::integ::TerrCtx, time_step: &mut f64) {
566 crate::integ::terr(self.st + CAP_CHARGE, t, time_step);
568 }
569
570 fn sens_params(&self, ac: bool) -> Option<Vec<crate::devices::SensParam>> {
571 let list = if ac { DIO_SENS_AC } else { DIO_SENS_DC };
572 Some(
573 list.iter()
574 .map(|&suffix| crate::devices::SensParam {
575 suffix,
576 value: self.sens_value(suffix),
577 })
578 .collect(),
579 )
580 }
581
582 fn sens_set(&mut self, suffix: &str, value: f64) {
588 let b = &mut self.sens;
589 match suffix {
590 ":is" => b.model.is = value,
591 ":n" => b.model.n = value,
595 ":tt" => b.model.tt = value,
596 ":cjo" => b.model.cjo = value,
597 ":vj" => b.model.vj = value,
598 ":m" => b.model.m = value,
599 ":eg" => b.model.eg = value,
600 ":xti" => b.model.xti = value,
601 ":kf" => b.model.kf = value,
602 ":af" => b.model.af = value,
603 ":fc" => b.model.fc = value,
604 "_temp" => b.temp = value + crate::consts::CTOK,
605 "_area" => self.area = value,
606 "_m" => b.m = value,
607 _ => return,
608 }
609 let b = &self.sens;
610 let t = DiodeTemp::new(
611 &b.name,
612 &b.model,
613 self.area,
614 b.m,
615 b.temp,
616 b.default_tnom,
617 b.reltol,
618 );
619 self.t.csat = t.csat;
621 self.t.gspr = t.gspr;
622 self.t.vt = t.vt;
623 self.t.vte = t.vte;
624 self.t.vcrit = t.vcrit;
625 self.t.brkdwn = t.brkdwn;
626 }
627
628 fn noise_suffixes(&self) -> Option<&'static [&'static str]> {
633 Some(&["_rs", "_id", "_1overf", "_rsw", "_idsw", "_1overfsw", ""])
634 }
635
636 fn noise_dens(&self, ctx: &crate::matrix::NoiseCtx) -> Vec<f64> {
637 const N_MINLOG: f64 = 1e-38;
638 let cd = ctx.states.get(0, self.st + CURRENT);
639 let m = self.sens.m;
640 let rs = ctx.thermal_at(self.pos_prime, self.pos, self.t.gspr, self.sens.temp);
642 let id = ctx.shot(self.pos_prime, self.neg, cd);
643 let model = &self.sens.model;
644 let flicker = ctx.flicker(
645 self.pos_prime,
646 self.neg,
647 model.kf * f64::exp(model.af * f64::ln(f64::max((cd / m).abs(), N_MINLOG))) * m,
648 1.0,
649 );
650 vec![rs, id, flicker, 0.0, 0.0, 0.0, rs + id + flicker]
651 }
652
653 fn load_ac(&mut self, ctx: &mut crate::matrix::AcCtx) {
655 let gspr = self.t.gspr;
656 let geq = ctx.states.get(0, self.st + CONDUCT);
657 let xceq = ctx.states.get(0, self.st + CAP_CHARGE + 1) * ctx.omega;
658 ctx.add_g(self.slots[0], gspr);
659 ctx.add_g(self.slots[1], geq);
660 ctx.add_b(self.slots[1], xceq);
661 ctx.add_g(self.slots[2], geq + gspr);
662 ctx.add_b(self.slots[2], xceq);
663 ctx.add_g(self.slots[3], -gspr);
664 ctx.add_g(self.slots[4], -geq);
665 ctx.add_b(self.slots[4], -xceq);
666 ctx.add_g(self.slots[5], -gspr);
667 ctx.add_g(self.slots[6], -geq);
668 ctx.add_b(self.slots[6], -xceq);
669 }
670
671 fn disto_setup(&mut self, ctx: &DistoSetupCtx) -> Result<(), &'static str> {
674 let csat = self.t.csat;
675 let vt = self.t.vt;
676 let vte = self.t.vte;
677 let gmin = ctx.gmin;
678 let vd = ctx.sol[self.pos_prime] - ctx.sol[self.neg];
679
680 let (g2, g3, cdiff2, cdiff3);
682 if vd >= -5.0 * vte {
683 let evd = f64::exp(vd / vte);
684 let gd = csat * evd / vte + gmin;
685 g2 = 0.5 * (gd - gmin) / vte;
686 cdiff2 = g2 * self.t.tt;
687 g3 = g2 / 3.0 / vte;
688 cdiff3 = g3 * self.t.tt;
689 } else if self.t.brkdwn.is_none_or(|bv| bv == 0.0 || vd >= -bv) {
690 g2 = 0.0;
692 g3 = 0.0;
693 cdiff2 = 0.0;
694 cdiff3 = 0.0;
695 } else {
696 let bv = self.t.brkdwn.unwrap_or(0.0);
699 let evrev = f64::exp(-(bv + vd) / vt);
700 let gd = csat * evrev / vt;
701 g2 = -gd / 2.0 / vt;
702 g3 = -g2 / 3.0 / vt;
703 cdiff2 = 0.0;
704 cdiff3 = 0.0;
705 }
706
707 let czero = self.t.t_jct_cap * self.area;
709 let (cjunc2, cjunc3);
710 if czero != 0.0 {
711 let (m, vj) = (self.t.m, self.t.vj);
712 if vd < self.t.t_dep_cap {
713 let arg = 1.0 - vd / vj;
714 let sarg = f64::exp(-m * f64::ln(arg));
715 let cjunc1 = czero * sarg;
716 cjunc2 = cjunc1 / 2.0 / vj * m / arg;
717 cjunc3 = cjunc2 / 3.0 / vj / arg * (m + 1.0);
718 } else {
719 let czof2 = czero / self.t.f2;
720 cjunc2 = czof2 / 2.0 / vj * m;
721 cjunc3 = 0.0;
722 }
723 } else {
724 cjunc2 = 0.0;
725 cjunc3 = 0.0;
726 }
727
728 self.d = DioDistoCoeffs {
729 id_x2: g2,
730 id_x3: g3,
731 cdif_x2: cdiff2,
732 cdif_x3: cdiff3,
733 cjnc_x2: cjunc2,
734 cjnc_x3: cjunc3,
735 };
736 Ok(())
737 }
738
739 fn disto_load(&self, ctx: &mut DistoCtx) {
743 let d = &self.d;
744 let (pp, nn) = (self.pos_prime, self.neg);
745 let h = &ctx.h;
746 let omega = ctx.omega;
747 let g2 = d.id_x2;
748 let g3 = d.id_x3;
749 let c2 = d.cdif_x2 + d.cjnc_x2;
750 let c3 = d.cdif_x3 + d.cjnc_x3;
751 let (r1h1x, i1h1x) = DistoKernels::branch(h.r1h1, h.i1h1, pp, nn);
752
753 let (temp, itemp) = match ctx.mode {
754 DistoMode::TwoF1 => {
755 let mut temp = d1n2f1(g2, r1h1x, i1h1x);
756 let mut itemp = d1i2f1(g2, r1h1x, i1h1x);
757 if c2 != 0.0 {
758 temp += -omega * d1i2f1(c2, r1h1x, i1h1x);
759 itemp += omega * d1n2f1(c2, r1h1x, i1h1x);
760 }
761 (temp, itemp)
762 }
763 DistoMode::ThrF1 => {
764 let (r2h11x, i2h11x) = DistoKernels::branch(h.r2h11, h.i2h11, pp, nn);
765 let mut temp = d1n3f1(g2, g3, r1h1x, i1h1x, r2h11x, i2h11x);
766 let mut itemp = d1i3f1(g2, g3, r1h1x, i1h1x, r2h11x, i2h11x);
767 if c2 != 0.0 {
768 temp += -omega * d1i3f1(c2, c3, r1h1x, i1h1x, r2h11x, i2h11x);
769 itemp += omega * d1n3f1(c2, c3, r1h1x, i1h1x, r2h11x, i2h11x);
770 }
771 (temp, itemp)
772 }
773 DistoMode::F1pF2 | DistoMode::F1mF2 => {
774 let (r1h2x, mut i1h2x) = DistoKernels::branch(h.r1h2, h.i1h2, pp, nn);
775 if ctx.mode == DistoMode::F1mF2 {
776 i1h2x = -i1h2x;
778 }
779 let mut temp = d1nf12(g2, r1h1x, i1h1x, r1h2x, i1h2x);
780 let mut itemp = d1if12(g2, r1h1x, i1h1x, r1h2x, i1h2x);
781 if c2 != 0.0 {
782 temp += -omega * d1if12(c2, r1h1x, i1h1x, r1h2x, i1h2x);
783 itemp += omega * d1nf12(c2, r1h1x, i1h1x, r1h2x, i1h2x);
784 }
785 (temp, itemp)
786 }
787 DistoMode::TwoF1mF2 => {
788 let (r1h2x, i1h2x) = DistoKernels::branch(h.r1h2, h.i1h2, pp, nn);
789 let i1hm2x = -i1h2x;
790 let (r2h11x, i2h11x) = DistoKernels::branch(h.r2h11, h.i2h11, pp, nn);
791 let (r2h1m2x, i2h1m2x) = DistoKernels::branch(h.r2h1m2, h.i2h1m2, pp, nn);
792 let mut temp = d1n2f12(
793 g2, g3, r1h1x, i1h1x, r1h2x, i1hm2x, r2h11x, i2h11x, r2h1m2x, i2h1m2x,
794 );
795 let mut itemp = d1i2f12(
796 g2, g3, r1h1x, i1h1x, r1h2x, i1hm2x, r2h11x, i2h11x, r2h1m2x, i2h1m2x,
797 );
798 if c2 != 0.0 {
799 temp += -omega
800 * d1i2f12(
801 c2, c3, r1h1x, i1h1x, r1h2x, i1hm2x, r2h11x, i2h11x, r2h1m2x, i2h1m2x,
802 );
803 itemp += omega
804 * d1n2f12(
805 c2, c3, r1h1x, i1h1x, r1h2x, i1hm2x, r2h11x, i2h11x, r2h1m2x, i2h1m2x,
806 );
807 }
808 (temp, itemp)
809 }
810 };
811
812 ctx.rhs_re[pp] -= temp;
813 ctx.rhs_im[pp] -= itemp;
814 ctx.rhs_re[nn] += temp;
815 ctx.rhs_im[nn] += itemp;
816 }
817
818 fn load_pz(&mut self, ctx: &mut crate::matrix::PzCtx) -> Result<(), &'static str> {
820 let gspr = self.t.gspr;
821 let geq = ctx.states.get(0, self.st + CONDUCT);
822 let capd = ctx.states.get(0, self.st + CAP_CHARGE + 1);
823 ctx.add_g(self.slots[0], gspr);
824 ctx.add_g(self.slots[1], geq);
825 ctx.add_cap(self.slots[1], capd);
826 ctx.add_g(self.slots[2], geq + gspr);
827 ctx.add_cap(self.slots[2], capd);
828 ctx.add_g(self.slots[3], -gspr);
829 ctx.add_g(self.slots[4], -geq);
830 ctx.add_cap(self.slots[4], -capd);
831 ctx.add_g(self.slots[5], -gspr);
832 ctx.add_g(self.slots[6], -geq);
833 ctx.add_cap(self.slots[6], -capd);
834 Ok(())
835 }
836}
837
838pub(crate) fn pnjlim(vnew: f64, vold: f64, vt: f64, vcrit: f64, check: &mut bool) -> f64 {
846 if vnew > vcrit && (vnew - vold).abs() > vt + vt {
847 *check = true;
848 if vold > 0.0 {
849 let arg = 1.0 + (vnew - vold) / vt;
850 if arg > 0.0 {
851 vold + vt * f64::ln(arg)
852 } else {
853 vcrit
854 }
855 } else {
856 vt * f64::ln(vnew / vt)
857 }
858 } else if vnew < 0.0 {
859 let arg = if vold > 0.0 {
863 -vold - 1.0
864 } else {
865 2.0 * vold - 1.0
866 };
867 if vnew < arg {
868 *check = true;
869 arg
870 } else {
871 *check = false;
872 vnew
873 }
874 } else {
875 *check = false;
876 vnew
877 }
878}
879
880#[cfg(test)]
881mod tests {
882 use super::*;
883
884 #[test]
885 fn pnjlim_matches_the_c() {
886 let vt = 0.025852;
887 let vcrit = 0.6145;
888 let mut check = false;
889 let v = pnjlim(0.5, 0.49, vt, vcrit, &mut check);
890 assert_eq!(v, 0.5);
891 assert!(!check);
892 let v = pnjlim(5.0, 0.6, vt, vcrit, &mut check);
893 assert!(check);
894 assert!((v - (0.6 + vt * (1.0f64 + (5.0 - 0.6) / vt).ln())).abs() < 1e-15);
895 let v = pnjlim(5.0, 0.0, vt, vcrit, &mut check);
896 assert!(check);
897 assert!((v - vt * (5.0f64 / vt).ln()).abs() < 1e-15);
898 let v = pnjlim(-3.0, 0.5, vt, vcrit, &mut check);
900 assert!(check);
901 assert_eq!(v, -1.5);
902 let v = pnjlim(-3.0, -0.5, vt, vcrit, &mut check);
903 assert!(check);
904 assert_eq!(v, -2.0);
905 let v = pnjlim(-1.0, -0.5, vt, vcrit, &mut check);
906 assert!(!check);
907 assert_eq!(v, -1.0);
908 }
909
910 #[test]
911 fn temp_adjust_is_identity_at_reftemp() {
912 let m = DiodeModel::default();
913 let t = DiodeTemp::new("d1", &m, 1.0, 1.0, 300.15, 300.15, 1e-3);
914 assert_eq!(t.csat, m.is);
915 let vte = m.n * KOVERQ * 300.15;
916 assert!((t.vcrit - vte * (vte / (ROOT2 * m.is)).ln()).abs() < 1e-15);
917 assert!((t.t_jct_cap - m.cjo).abs() < 1e-12);
920 assert!((t.t_dep_cap - m.fc * m.vj).abs() < 1e-12);
921 assert!(t.notes.is_empty());
922 }
923
924 #[test]
931 fn breakdown_carries_ibv_at_bv() {
932 let vt = KOVERQ * 300.15;
933 let cases = [
934 (6.79e-18, 1.47, 1e-3, 1.0, 49.156_224_229_991_27),
936 (1e-14, 1.0, 1e-4, 10.0, 50.0 - vt * f64::ln(1e10)),
937 ];
938 for (is, n, ibv, area, xbv_3f5) in cases {
939 let m = DiodeModel {
940 is,
941 n,
942 bv: Some(50.0),
943 ibv,
944 ..DiodeModel::default()
945 };
946 let t = DiodeTemp::new("dz", &m, area, 1.0, 300.15, 300.15, 1e-3);
947 let xbv = t.brkdwn.expect("bv given");
948 let cd = t.csat * f64::exp(-(xbv - 50.0) / t.vte);
949 assert!((cd - ibv).abs() <= 1e-3 * ibv, "cd(-BV) = {cd:e}");
950 assert!(t.notes.is_empty());
951 let shift = xbv_3f5 - xbv;
952 let want = (n - 1.0) * vt * f64::ln(ibv / is) - vt * f64::ln(area);
953 assert!((shift - want).abs() < 1e-3, "shift {shift} vs {want}");
954 }
955 let m = DiodeModel {
958 is: 6.79e-18,
959 n: 1.47,
960 bv: Some(50.0),
961 ..DiodeModel::default()
962 };
963 let t = DiodeTemp::new("dz", &m, 1.0, 1.0, 300.15, 300.15, 1e-3);
964 let v = t.brkdwn.unwrap() + t.vte * f64::ln(1.6 / t.csat);
965 assert!((v - 50.2805).abs() < 1e-4, "{v}");
966 }
967
968 #[test]
969 fn setup_clamps_warn() {
970 let m = DiodeModel {
971 m: 0.95,
972 eg: 0.05,
973 fc: 0.99,
974 ..DiodeModel::default()
975 };
976 let t = DiodeTemp::new("dx", &m, 1.0, 1.0, 300.15, 300.15, 1e-3);
977 assert_eq!(t.notes.len(), 3);
978 assert_eq!(t.m, 0.9);
979 assert!((t.f3 - (1.0 - 0.95 * 1.9)).abs() < 1e-12);
980 }
981}