Skip to main content

dune_core/
jfet.rs

1//! JFET — the Shichman–Hodges FET with the Sydney University doping-
2//! tail modification (`B`), from the 3f5 (`dev/jfet/jfetload.c`,
3//! `jfettemp.c`, `jfetset.c`, `jfetacld.c`, `jfettrun.c`). Two gate
4//! junction diodes with depletion charge (−1/2 power, linearized past
5//! `FC·PB`), drain current by region in normal and inverse mode, and
6//! series drain/source resistance via prime nodes.
7//!
8//! The three-terminal FET scaffolding — the bias-resolution ladder
9//! with `pnjlim`/`fetlim`, the gate diodes, the 13-slot state layout,
10//! the stamps and the AC load — is shared with the MESFET
11//! (`mesload.c` is line for line the same outside the drain current
12//! and the gate charge), so it lives here as `pub(crate)` helpers.
13
14use crate::consts::{BOLTZ, CHARGE, KOVERQ, REFTEMP, ROOT2};
15use crate::diode::pnjlim;
16use crate::disto::{DfCoeffs, DistoCtx, DistoSetupCtx, Kern, d1_term, df_term, jomega};
17use crate::matrix::{AcCtx, AnKind, InitMode, LoadCtx, MatrixBuilder, Slot, TRASH, Var};
18use crate::mos1::fetlim;
19
20/// `.model … njf(…)/pjf(…)` parameters, 3f5 names and defaults
21/// (`jfet.c` keyword table; defaults from `jfetset.c`).
22#[derive(Clone, Debug)]
23pub struct JfetModel {
24    /// `NJF` (true) or `PJF`.
25    pub njf: bool,
26    /// `VTO`/`VT0`: threshold (pinch-off) voltage, default −2.
27    pub vto: f64,
28    /// `BETA`: transconductance parameter, A/V², default 1e-4.
29    pub beta: f64,
30    /// `LAMBDA`: channel-length modulation, 1/V, default 0.
31    pub lambda: f64,
32    /// `RD`: drain ohmic resistance, default 0.
33    pub rd: f64,
34    /// `RS`: source ohmic resistance, default 0.
35    pub rs: f64,
36    /// `CGS`: zero-bias G-S junction capacitance, default 0.
37    pub cgs: f64,
38    /// `CGD`: zero-bias G-D junction capacitance, default 0.
39    pub cgd: f64,
40    /// `PB`: gate junction potential, default 1.
41    pub pb: f64,
42    /// `IS`: gate junction saturation current, default 1e-14.
43    pub is: f64,
44    /// `FC`: forward-bias depletion capacitance coefficient,
45    /// default 0.5 (limited to 0.95 by `JFETtemp`).
46    pub fc: f64,
47    /// `B`: doping tail parameter (Sydney University), default 1 —
48    /// which reduces the drain current to plain Shichman–Hodges.
49    pub b: f64,
50    /// `KF`/`AF` (noise only).
51    pub kf: f64,
52    pub af: f64,
53    /// `NLEV` (ngspice-47 only, default 2): noise equation selector
54    /// — `< 3` channel thermal `4kT·(2/3)|gm|`, `3` the
55    /// `GDSNOI`-scaled form; for the MOSFETs also the flicker form
56    /// (0/1/2–3).
57    pub nlev: i64,
58    /// `GDSNOI` (ngspice-47 only, default 1): channel noise
59    /// coefficient of `NLEV = 3`.
60    pub gdsnoi: f64,
61    /// `TNOM` in kelvin (the card gives °C).
62    pub tnom: Option<f64>,
63}
64
65impl JfetModel {
66    pub fn new(njf: bool) -> Self {
67        JfetModel {
68            njf,
69            vto: -2.0,
70            beta: 1e-4,
71            lambda: 0.0,
72            rd: 0.0,
73            rs: 0.0,
74            cgs: 0.0,
75            cgd: 0.0,
76            pb: 1.0,
77            is: 1e-14,
78            fc: 0.5,
79            b: 1.0,
80            kf: 0.0,
81            af: 1.0,
82            nlev: 2,
83            gdsnoi: 1.0,
84            tnom: None,
85        }
86    }
87}
88
89impl Default for JfetModel {
90    fn default() -> Self {
91        JfetModel::new(true)
92    }
93}
94
95fn egfet(t: f64) -> f64 {
96    1.16 - (7.02e-4 * t * t) / (t + 1108.0)
97}
98
99/// Model- and temperature-derived values (`JFETtemp`, the model
100/// block and the per-instance block).
101#[derive(Clone, Debug)]
102pub(crate) struct JfetTemp {
103    /// The instance temperature, kelvin (`*temp`: the card's
104    /// `TEMP=` or `CKTtemp`) — the thermal noise generators' T
105    /// (ngspice-47 `NevalSrcInstanceTemp`).
106    pub temp: f64,
107    pub type_: f64,
108    pub vt: f64,
109    pub vto: f64,
110    pub beta: f64,
111    pub lambda: f64,
112    pub b: f64,
113    /// `JFETbFac = (1 − B)/(PB − VTO)`.
114    pub b_fac: f64,
115    pub t_sat_cur: f64,
116    pub t_cgs: f64,
117    pub t_cgd: f64,
118    pub t_gate_pot: f64,
119    pub cor_dep_cap: f64,
120    pub f1: f64,
121    pub f2: f64,
122    pub f3: f64,
123    pub vcrit: f64,
124    pub drain_conduct: f64,
125    pub source_conduct: f64,
126    /// `KF`/`AF`: flicker-noise coefficient and exponent.
127    pub kf: f64,
128    pub af: f64,
129    /// `NLEV`/`GDSNOI` (noise only).
130    pub nlev: i64,
131    pub gdsnoi: f64,
132    pub notes: Vec<String>,
133}
134
135impl JfetTemp {
136    pub fn new(name: &str, m: &JfetModel, temp: f64, default_tnom: f64) -> Self {
137        let mut notes = Vec::new();
138        let tnom = m.tnom.unwrap_or(default_tnom);
139        let vtnom = KOVERQ * tnom;
140        let fact1 = tnom / REFTEMP;
141        let kt1 = BOLTZ * tnom;
142        let egfet1 = egfet(tnom);
143        let arg1 = -egfet1 / (kt1 + kt1) + 1.1150877 / (BOLTZ * (REFTEMP + REFTEMP));
144        let pbfact1 = -2.0 * vtnom * (1.5 * f64::ln(fact1) + CHARGE * arg1);
145        let pbo = (m.pb - pbfact1) / fact1;
146        let gmaold = (m.pb - pbo) / pbo;
147        let cjfact = 1.0 / (1.0 + 0.5 * (4e-4 * (tnom - REFTEMP) - gmaold));
148
149        let conduct = |r: f64| if r != 0.0 { 1.0 / r } else { 0.0 };
150        let mut fc = m.fc;
151        if fc > 0.95 {
152            notes.push(format!(
153                "{name}: Depletion cap. coefficient too large, limited to .95"
154            ));
155            fc = 0.95;
156        }
157        let xfc = f64::ln(1.0 - fc);
158        let f2 = f64::exp((1.0 + 0.5) * xfc);
159        let f3 = 1.0 - fc * (1.0 + 0.5);
160        let b_fac = (1.0 - m.b) / (m.pb - m.vto);
161
162        // Per-instance block.
163        let vt = temp * KOVERQ;
164        let fact2 = temp / REFTEMP;
165        let ratio1 = temp / tnom - 1.0;
166        let t_sat_cur = m.is * f64::exp(ratio1 * 1.11 / vt);
167        let mut t_cgs = m.cgs * cjfact;
168        let mut t_cgd = m.cgd * cjfact;
169        let kt = BOLTZ * temp;
170        let egfet_t = egfet(temp);
171        let arg = -egfet_t / (kt + kt) + 1.1150877 / (BOLTZ * (REFTEMP + REFTEMP));
172        let pbfact = -2.0 * vt * (1.5 * f64::ln(fact2) + CHARGE * arg);
173        let t_gate_pot = fact2 * pbo + pbfact;
174        let gmanew = (t_gate_pot - pbo) / pbo;
175        let cjfact1 = 1.0 + 0.5 * (4e-4 * (temp - REFTEMP) - gmanew);
176        t_cgs *= cjfact1;
177        t_cgd *= cjfact1;
178        let cor_dep_cap = fc * t_gate_pot;
179        let f1 = t_gate_pot * (1.0 - f64::exp((1.0 - 0.5) * xfc)) / (1.0 - 0.5);
180        let vcrit = vt * f64::ln(vt / (ROOT2 * t_sat_cur));
181
182        JfetTemp {
183            temp,
184            type_: if m.njf { 1.0 } else { -1.0 },
185            vt,
186            vto: m.vto,
187            beta: m.beta,
188            lambda: m.lambda,
189            b: m.b,
190            b_fac,
191            t_sat_cur,
192            t_cgs,
193            t_cgd,
194            t_gate_pot,
195            cor_dep_cap,
196            f1,
197            f2,
198            f3,
199            vcrit,
200            drain_conduct: conduct(m.rd),
201            source_conduct: conduct(m.rs),
202            kf: m.kf,
203            af: m.af,
204            nlev: m.nlev,
205            gdsnoi: m.gdsnoi,
206            notes,
207        }
208    }
209}
210
211/// Drain current and derivatives (`jfetload.c`, the Sydney
212/// University section): `(cdrain, gm, gds)` for normal (`vds ≥ 0`)
213/// and inverse mode, cutoff/linear/saturation each.
214#[allow(clippy::too_many_arguments)]
215pub(crate) fn jfet_ids(
216    vgs: f64,
217    vgd: f64,
218    vto: f64,
219    beta: f64,
220    lambda: f64,
221    b: f64,
222    b_fac: f64,
223) -> (f64, f64, f64) {
224    let vds = vgs - vgd;
225    if vds >= 0.0 {
226        let vgst = vgs - vto;
227        if vgst <= 0.0 {
228            return (0.0, 0.0, 0.0);
229        }
230        let betap = beta * (1.0 + lambda * vds);
231        if vgst >= vds {
232            // Normal mode, linear region.
233            let apart = 2.0 * b + 3.0 * b_fac * (vgst - vds);
234            let cpart = vds * (vds * (b_fac * vds - b) + vgst * apart);
235            let cdrain = betap * cpart;
236            let gm = betap * vds * (apart + 3.0 * b_fac * vgst);
237            let gds = betap * (vgst - vds) * apart + beta * lambda * cpart;
238            (cdrain, gm, gds)
239        } else {
240            // Normal mode, saturation region.
241            let bf = vgst * b_fac;
242            let gm = betap * vgst * (2.0 * b + 3.0 * bf);
243            let cpart = vgst * vgst * (b + bf);
244            (betap * cpart, gm, lambda * beta * cpart)
245        }
246    } else {
247        let vgdt = vgd - vto;
248        if vgdt <= 0.0 {
249            return (0.0, 0.0, 0.0);
250        }
251        let betap = beta * (1.0 - lambda * vds);
252        if vgdt + vds >= 0.0 {
253            // Inverse mode, linear region.
254            let apart = 2.0 * b + 3.0 * b_fac * (vgdt + vds);
255            let cpart = vds * (-vds * (-b_fac * vds - b) + vgdt * apart);
256            let cdrain = betap * cpart;
257            let gm = betap * vds * (apart + 3.0 * b_fac * vgdt);
258            let gds = betap * (vgdt + vds) * apart - beta * lambda * cpart - gm;
259            (cdrain, gm, gds)
260        } else {
261            // Inverse mode, saturation region.
262            let bf = vgdt * b_fac;
263            let gm = -betap * vgdt * (2.0 * b + 3.0 * bf);
264            let cpart = vgdt * vgdt * (b + bf);
265            (-betap * cpart, gm, lambda * beta * cpart - gm)
266        }
267    }
268}
269
270// State offsets (`jfetdefs.h`; `mesdefs.h` is identical).
271pub(crate) const VGS: usize = 0;
272pub(crate) const VGD: usize = 1;
273pub(crate) const CG: usize = 2;
274pub(crate) const CD: usize = 3;
275pub(crate) const CGD: usize = 4;
276pub(crate) const GM: usize = 5;
277pub(crate) const GDS: usize = 6;
278pub(crate) const GGS: usize = 7;
279pub(crate) const GGD: usize = 8;
280pub(crate) const QGS: usize = 9;
281pub(crate) const CQGS: usize = 10;
282pub(crate) const QGD: usize = 11;
283pub(crate) const CQGD: usize = 12;
284/// `*states += 13` (`jfetset.c`; `MESnumStates`).
285pub(crate) const FET3_NUM_STATES: usize = 13;
286
287// Slot order (the TSTALLOC list of `jfetset.c` / `messetup.c`).
288const D_DP: usize = 0;
289const G_DP: usize = 1;
290const G_SP: usize = 2;
291const S_SP: usize = 3;
292const DP_D: usize = 4;
293const DP_G: usize = 5;
294const DP_SP: usize = 6;
295const SP_G: usize = 7;
296const SP_S: usize = 8;
297const SP_DP: usize = 9;
298const D_D: usize = 10;
299const G_G: usize = 11;
300const S_S: usize = 12;
301const DP_DP: usize = 13;
302const SP_SP: usize = 14;
303
304/// Terminals, prime nodes and matrix slots of a three-terminal FET
305/// (JFET or MESFET).
306pub(crate) struct Fet3Nodes {
307    pub drain: Var,
308    pub gate: Var,
309    pub source: Var,
310    pub d_p: Var,
311    pub s_p: Var,
312    s: [Slot; 15],
313}
314
315impl Fet3Nodes {
316    /// `nodes` = drain, gate, source; `primes` = drain', source'.
317    pub fn new(nodes: [Var; 3], primes: [Var; 2]) -> Self {
318        Fet3Nodes {
319            drain: nodes[0],
320            gate: nodes[1],
321            source: nodes[2],
322            d_p: primes[0],
323            s_p: primes[1],
324            s: [TRASH; 15],
325        }
326    }
327
328    pub fn alloc(&mut self, mb: &mut MatrixBuilder) {
329        let (d, g, s, dp, sp) = (self.drain, self.gate, self.source, self.d_p, self.s_p);
330        self.s = [
331            mb.slot(d, dp),
332            mb.slot(g, dp),
333            mb.slot(g, sp),
334            mb.slot(s, sp),
335            mb.slot(dp, d),
336            mb.slot(dp, g),
337            mb.slot(dp, sp),
338            mb.slot(sp, g),
339            mb.slot(sp, s),
340            mb.slot(sp, dp),
341            mb.slot(d, d),
342            mb.slot(g, g),
343            mb.slot(s, s),
344            mb.slot(dp, dp),
345            mb.slot(sp, sp),
346        ];
347    }
348}
349
350/// Where a load's branch voltages came from: the initialization
351/// ladder of `jfetload.c`/`mesload.c`. `hat` carries the predicted
352/// gate and drain currents (`cghat`, `cdhat`) when the voltages came
353/// from the solution (or the predictor) — only then does the C's
354/// current-agreement convergence test mean anything.
355pub(crate) struct Fet3Bias {
356    pub vgs: f64,
357    pub vgd: f64,
358    pub check: bool,
359    pub hat: Option<(f64, f64)>,
360}
361
362/// The bias-resolution ladder shared by `JFETload` and `MESload`.
363/// `vt`/`vcrit` feed `DEVpnjlim` (JFET: instance temperature; MES:
364/// `CONSTvt0`), `vto` feeds `DEVfetlim`.
365#[allow(clippy::too_many_arguments)]
366pub(crate) fn fet3_bias(
367    ctx: &mut LoadCtx,
368    n: &Fet3Nodes,
369    st: usize,
370    typ: f64,
371    off: bool,
372    ic: (f64, f64),
373    vt: f64,
374    vcrit: f64,
375    vto: f64,
376) -> Fet3Bias {
377    let s0 = |ctx: &LoadCtx, o: usize| ctx.states.get(0, st + o);
378    let fixed = |vgs, vgd| Fet3Bias {
379        vgs,
380        vgd,
381        check: true,
382        hat: None,
383    };
384    match ctx.mode {
385        InitMode::Smsig => return fixed(s0(ctx, VGS), s0(ctx, VGD)),
386        InitMode::Tran => {
387            return fixed(ctx.states.get(1, st + VGS), ctx.states.get(1, st + VGD));
388        }
389        InitMode::Jct if ctx.kind == AnKind::TranOp && ctx.uic => {
390            let vds = typ * ic.0;
391            let vgs = typ * ic.1;
392            return fixed(vgs, vgs - vds);
393        }
394        InitMode::Jct if !off => return fixed(-1.0, -1.0),
395        InitMode::Jct => return fixed(0.0, 0.0),
396        InitMode::Fix if off => return fixed(0.0, 0.0),
397        _ => {}
398    }
399    let (mut vgs, mut vgd);
400    if ctx.mode == InitMode::Pred {
401        for o in [VGS, VGD, CG, CD, CGD, GM, GDS, GGS, GGD] {
402            let v = ctx.states.get(1, st + o);
403            ctx.states.set(0, st + o, v);
404        }
405        vgs = ctx.pred(st + VGS);
406        vgd = ctx.pred(st + VGD);
407    } else {
408        vgs = typ * (ctx.sol[n.gate] - ctx.sol[n.s_p]);
409        vgd = typ * (ctx.sol[n.gate] - ctx.sol[n.d_p]);
410    }
411    let delvgs = vgs - s0(ctx, VGS);
412    let delvgd = vgd - s0(ctx, VGD);
413    let delvds = delvgs - delvgd;
414    let cghat = s0(ctx, CG) + s0(ctx, GGD) * delvgd + s0(ctx, GGS) * delvgs;
415    let cdhat = s0(ctx, CD) + s0(ctx, GM) * delvgs + s0(ctx, GDS) * delvds - s0(ctx, GGD) * delvgd;
416
417    // Limit the nonlinear branch voltages (no device bypass in Dune).
418    let mut check = true;
419    let mut ichk1 = true;
420    vgs = pnjlim(vgs, s0(ctx, VGS), vt, vcrit, &mut check);
421    vgd = pnjlim(vgd, s0(ctx, VGD), vt, vcrit, &mut ichk1);
422    if ichk1 {
423        check = true;
424    }
425    vgs = fetlim(vgs, s0(ctx, VGS), vto);
426    vgd = fetlim(vgd, s0(ctx, VGD), vto);
427    Fet3Bias {
428        vgs,
429        vgd,
430        check,
431        hat: Some((cghat, cdhat)),
432    }
433}
434
435/// One gate junction: `(conductance, current)` with gmin, the
436/// `−5·vt` reverse form of `jfetload.c`/`mesload.c`.
437pub(crate) fn gate_diode(v: f64, csat: f64, vt: f64, gmin: f64) -> (f64, f64) {
438    if v <= -5.0 * vt {
439        let g = -csat / v + gmin;
440        (g, g * v)
441    } else {
442        let e = f64::exp(v / vt);
443        (csat * e / vt + gmin, csat * (e - 1.0) + gmin * v)
444    }
445}
446
447/// Small-signal and companion values a FET load hands to the stamp.
448pub(crate) struct Fet3Op {
449    pub vgs: f64,
450    pub vgd: f64,
451    pub cg: f64,
452    pub cd: f64,
453    pub cgd: f64,
454    pub gm: f64,
455    pub gds: f64,
456    pub ggs: f64,
457    pub ggd: f64,
458}
459
460/// Integrate the two gate charges (already in `state0` QGS/QGD) and
461/// fold the companions into `op` — the common tail of the charge
462/// block. Returns `true` for the `MODEINITSMSIG` pass, which stores
463/// the capacitances and stops (`continue` in the C).
464pub(crate) fn fet3_charge(
465    ctx: &mut LoadCtx,
466    st: usize,
467    capgs: f64,
468    capgd: f64,
469    op: &mut Fet3Op,
470) -> bool {
471    if ctx.kind == AnKind::TranOp && ctx.uic {
472        return false;
473    }
474    if ctx.mode == InitMode::Smsig {
475        ctx.states.set(0, st + QGS, capgs);
476        ctx.states.set(0, st + QGD, capgd);
477        return true;
478    }
479    if ctx.mode == InitMode::Tran {
480        for o in [QGS, QGD] {
481            let v = ctx.states.get(0, st + o);
482            ctx.states.set(1, st + o, v);
483        }
484    }
485    let (geq, _) = ctx.integrate(capgs, st + QGS);
486    op.ggs += geq;
487    op.cg += ctx.states.get(0, st + CQGS);
488    let (geq, _) = ctx.integrate(capgd, st + QGD);
489    op.ggd += geq;
490    let cqgd = ctx.states.get(0, st + CQGD);
491    op.cg += cqgd;
492    op.cd -= cqgd;
493    op.cgd += cqgd;
494    if ctx.mode == InitMode::Tran {
495        for o in [CQGS, CQGD] {
496            let v = ctx.states.get(0, st + o);
497            ctx.states.set(1, st + o, v);
498        }
499    }
500    false
501}
502
503/// Convergence check, state save and the RHS/matrix stamps (the
504/// tail of `jfetload.c`/`mesload.c` from "check convergence").
505#[allow(clippy::too_many_arguments)]
506pub(crate) fn fet3_finish(
507    ctx: &mut LoadCtx,
508    n: &Fet3Nodes,
509    st: usize,
510    typ: f64,
511    gdpr: f64,
512    gspr: f64,
513    bias: &Fet3Bias,
514    op: &Fet3Op,
515) {
516    if !(ctx.mode == InitMode::Fix && ctx.uic) {
517        let disagree = match bias.hat {
518            Some((cghat, cdhat)) => {
519                (cghat - op.cg).abs()
520                    >= ctx.reltol * f64::max(cghat.abs(), op.cg.abs()) + ctx.abstol
521                    || (cdhat - op.cd).abs()
522                        > ctx.reltol * f64::max(cdhat.abs(), op.cd.abs()) + ctx.abstol
523            }
524            None => true,
525        };
526        if bias.check || disagree {
527            ctx.noncon = true;
528        }
529    }
530    let vals = [
531        (VGS, op.vgs),
532        (VGD, op.vgd),
533        (CG, op.cg),
534        (CD, op.cd),
535        (CGD, op.cgd),
536        (GM, op.gm),
537        (GDS, op.gds),
538        (GGS, op.ggs),
539        (GGD, op.ggd),
540    ];
541    for (o, v) in vals {
542        ctx.states.set(0, st + o, v);
543    }
544
545    let vds = op.vgs - op.vgd;
546    let ceqgd = typ * (op.cgd - op.ggd * op.vgd);
547    let ceqgs = typ * ((op.cg - op.cgd) - op.ggs * op.vgs);
548    let cdreq = typ * ((op.cd + op.cgd) - op.gds * vds - op.gm * op.vgs);
549    ctx.add_rhs(n.gate, -ceqgs - ceqgd);
550    ctx.add_rhs(n.d_p, -cdreq + ceqgd);
551    ctx.add_rhs(n.s_p, cdreq + ceqgs);
552
553    let (gm, gds, ggs, ggd) = (op.gm, op.gds, op.ggs, op.ggd);
554    ctx.add(n.s[D_DP], -gdpr);
555    ctx.add(n.s[G_DP], -ggd);
556    ctx.add(n.s[G_SP], -ggs);
557    ctx.add(n.s[S_SP], -gspr);
558    ctx.add(n.s[DP_D], -gdpr);
559    ctx.add(n.s[DP_G], gm - ggd);
560    ctx.add(n.s[DP_SP], -gds - gm);
561    ctx.add(n.s[SP_G], -ggs - gm);
562    ctx.add(n.s[SP_S], -gspr);
563    ctx.add(n.s[SP_DP], -gds);
564    ctx.add(n.s[D_D], gdpr);
565    ctx.add(n.s[G_G], ggd + ggs);
566    ctx.add(n.s[S_S], gspr);
567    ctx.add(n.s[DP_DP], gdpr + gds + ggd);
568    ctx.add(n.s[SP_SP], gspr + gds + gm + ggs);
569}
570
571/// `JFETacLoad` / `MESacLoad`: the small-signal conductances from
572/// `state0` and the capacitances the `MODEINITSMSIG` pass left in
573/// the charge slots.
574pub(crate) fn fet3_ac(ctx: &mut AcCtx, n: &Fet3Nodes, st: usize, gdpr: f64, gspr: f64) {
575    let s0 = |o: usize| ctx.states.get(0, st + o);
576    let gm = s0(GM);
577    let gds = s0(GDS);
578    let ggs = s0(GGS);
579    let xgs = s0(QGS) * ctx.omega;
580    let ggd = s0(GGD);
581    let xgd = s0(QGD) * ctx.omega;
582    ctx.add_g(n.s[D_D], gdpr);
583    ctx.add_g(n.s[G_G], ggd + ggs);
584    ctx.add_b(n.s[G_G], xgd + xgs);
585    ctx.add_g(n.s[S_S], gspr);
586    ctx.add_g(n.s[DP_DP], gdpr + gds + ggd);
587    ctx.add_b(n.s[DP_DP], xgd);
588    ctx.add_g(n.s[SP_SP], gspr + gds + gm + ggs);
589    ctx.add_b(n.s[SP_SP], xgs);
590    ctx.add_g(n.s[D_DP], -gdpr);
591    ctx.add_g(n.s[G_DP], -ggd);
592    ctx.add_b(n.s[G_DP], -xgd);
593    ctx.add_g(n.s[G_SP], -ggs);
594    ctx.add_b(n.s[G_SP], -xgs);
595    ctx.add_g(n.s[S_SP], -gspr);
596    ctx.add_g(n.s[DP_D], -gdpr);
597    ctx.add_g(n.s[DP_G], -ggd + gm);
598    ctx.add_b(n.s[DP_G], -xgd);
599    ctx.add_g(n.s[DP_SP], -gds - gm);
600    ctx.add_g(n.s[SP_G], -ggs - gm);
601    ctx.add_b(n.s[SP_G], -xgs);
602    ctx.add_g(n.s[SP_S], -gspr);
603    ctx.add_g(n.s[SP_DP], -gds);
604}
605
606/// `JFETpzLoad` / `MESpzLoad` (`jfetpzld.c`, `mespzld.c`): the AC
607/// stamps with `s` in place of `jω`. `gdpr`/`gspr` are the series
608/// *conductances* times the area, as `mespzld.c` and ngspice's
609/// `jfetpzld.c` read them; the 3f5's `jfetpzld.c` reads
610/// `JFETdrainResist·area` — ohms stamped as siemens
611/// (`corpus/KNOWN_DIFFS.md`).
612pub(crate) fn fet3_pz(
613    ctx: &mut crate::matrix::PzCtx,
614    n: &Fet3Nodes,
615    st: usize,
616    gdpr: f64,
617    gspr: f64,
618) {
619    let s0 = |o: usize| ctx.states.get(0, st + o);
620    let gm = s0(GM);
621    let gds = s0(GDS);
622    let ggs = s0(GGS);
623    let xgs = s0(QGS);
624    let ggd = s0(GGD);
625    let xgd = s0(QGD);
626    ctx.add_g(n.s[D_D], gdpr);
627    ctx.add_g(n.s[G_G], ggd + ggs);
628    ctx.add_cap(n.s[G_G], xgd + xgs);
629    ctx.add_g(n.s[S_S], gspr);
630    ctx.add_g(n.s[DP_DP], gdpr + gds + ggd);
631    ctx.add_cap(n.s[DP_DP], xgd);
632    ctx.add_g(n.s[SP_SP], gspr + gds + gm + ggs);
633    ctx.add_cap(n.s[SP_SP], xgs);
634    ctx.add_g(n.s[D_DP], -gdpr);
635    ctx.add_g(n.s[G_DP], -ggd);
636    ctx.add_cap(n.s[G_DP], -xgd);
637    ctx.add_g(n.s[G_SP], -ggs);
638    ctx.add_cap(n.s[G_SP], -xgs);
639    ctx.add_g(n.s[S_SP], -gspr);
640    ctx.add_g(n.s[DP_D], -gdpr);
641    ctx.add_g(n.s[DP_G], -ggd + gm);
642    ctx.add_cap(n.s[DP_G], -xgd);
643    ctx.add_g(n.s[DP_SP], -gds - gm);
644    ctx.add_g(n.s[SP_G], -ggs - gm);
645    ctx.add_cap(n.s[SP_G], -xgs);
646    ctx.add_g(n.s[SP_S], -gspr);
647    ctx.add_g(n.s[SP_DP], -gds);
648}
649
650/// Name suffixes of the JFET/MESFET noise generators
651/// (`JFETnNames`, `MESnNames`).
652pub(crate) const FET3_NOISE_SUFFIXES: &[&str] = &["_rd", "_rs", "_id", "_1overf", ""];
653
654/// How a FET's channel thermal noise is evaluated: the MESFET's
655/// `NevalSrc` at `CKTtemp` with `(2/3)|gm|`, or the JFET's
656/// `NevalSrcInstanceTemp` with its `NLEV` selector (ngspice-47
657/// `jfetnoi.c`).
658pub(crate) enum Fet3Channel {
659    /// `mesnoise.c`.
660    Mes,
661    /// `jfetnoi.c`: instance temperature; `NLEV < 3` → `(2/3)|gm|`,
662    /// `NLEV = 3` → `(2/3)·β·vgst·(1+α+α²)/(1+α)·GDSNOI` with
663    /// `α = 1 − vds/vgst` in the linear region (`vgst ≥ vds`), 0 in
664    /// saturation.
665    Jfet {
666        temp: f64,
667        nlev: i64,
668        gdsnoi: f64,
669        /// `tThreshold`, `tBeta·area`.
670        vto: f64,
671        beta: f64,
672    },
673}
674
675/// `JFETnoise` (ngspice-47) / `MESnoise`: thermal noise of RD and
676/// RS, channel thermal noise (see [`Fet3Channel`]), flicker
677/// `KF·|Id|^AF/f` on the drain current of `state0`, and the total.
678/// `gdpr`/`gspr` already carry the area.
679#[allow(clippy::too_many_arguments)]
680pub(crate) fn fet3_noise(
681    ctx: &crate::matrix::NoiseCtx,
682    n: &Fet3Nodes,
683    st: usize,
684    gdpr: f64,
685    gspr: f64,
686    kf: f64,
687    af: f64,
688    chan: &Fet3Channel,
689) -> Vec<f64> {
690    const N_MINLOG: f64 = 1e-38;
691    let gm = ctx.states.get(0, st + GM);
692    let (rd, rs, id) = match *chan {
693        Fet3Channel::Mes => (
694            ctx.thermal(n.d_p, n.drain, gdpr),
695            ctx.thermal(n.s_p, n.source, gspr),
696            ctx.thermal(n.d_p, n.s_p, 2.0 / 3.0 * gm.abs()),
697        ),
698        Fet3Channel::Jfet {
699            temp,
700            nlev,
701            gdsnoi,
702            vto,
703            beta,
704        } => {
705            let sid = if nlev < 3 {
706                2.0 / 3.0 * gm.abs()
707            } else {
708                let vgs = ctx.states.get(0, st + VGS);
709                let vds = vgs - ctx.states.get(0, st + VGD);
710                let vgst = vgs - vto;
711                let alpha = if vgst >= vds {
712                    1.0 - vds / vgst // linear region
713                } else {
714                    0.0 // saturation region
715                };
716                2.0 / 3.0 * beta * vgst * (1.0 + alpha + alpha * alpha) / (1.0 + alpha) * gdsnoi
717            };
718            (
719                ctx.thermal_at(n.d_p, n.drain, gdpr, temp),
720                ctx.thermal_at(n.s_p, n.source, gspr, temp),
721                ctx.thermal_at(n.d_p, n.s_p, sid, temp),
722            )
723        }
724    };
725    let cd = ctx.states.get(0, st + CD);
726    let flicker = ctx.flicker(
727        n.d_p,
728        n.s_p,
729        kf * f64::exp(af * f64::ln(f64::max(cd.abs(), N_MINLOG))),
730        1.0,
731    );
732    vec![rd, rs, id, flicker, rd + rs + id + flicker]
733}
734
735/// The gate-charge part of a three-terminal FET's `.disto`
736/// coefficients: the JFET's two one-variable depletion charges, or the
737/// MESFET's two charges in `(vgs, vgd)`.
738#[derive(Clone, Copy, Debug)]
739#[allow(clippy::large_enum_variant)] // one per instance, never moved in bulk
740pub(crate) enum Fet3GateQ {
741    /// `jfetdset.c`: `capgs2/3` on `vgs`, `capgd2/3` on `vgd`
742    /// (Taylor coefficients of the depletion charges).
743    Junction {
744        capgs2: f64,
745        capgs3: f64,
746        capgd2: f64,
747        capgd3: f64,
748    },
749    /// `mesdset.c`: `qgs_*`/`qgd_*`, x = vgs, y = vgd.
750    Statz { qgs: DfCoeffs, qgd: DfCoeffs },
751}
752
753/// Taylor coefficients of a JFET/MESFET at the operating point (the
754/// `cdr_*`, `ggs2 …`, `capgs2 …`/`qgs_* …` instance fields of
755/// `jfetdefs.h`/`mesdefs.h`). The drain current is a function of
756/// x = vgs and y = vds (the C's `cdr_x2, cdr_y2, cdr_xy …` for the
757/// JFET, `cdr_x2, cdr_z2, cdr_xz …` for the MESFET — same slots).
758#[derive(Clone, Copy, Debug)]
759pub(crate) struct Fet3DistoCoeffs {
760    pub cdr: DfCoeffs,
761    pub ggs2: f64,
762    pub ggs3: f64,
763    pub ggd2: f64,
764    pub ggd3: f64,
765    pub q: Fet3GateQ,
766}
767
768impl Default for Fet3DistoCoeffs {
769    fn default() -> Self {
770        Fet3DistoCoeffs {
771            cdr: DfCoeffs::default(),
772            ggs2: 0.0,
773            ggs3: 0.0,
774            ggd2: 0.0,
775            ggd3: 0.0,
776            q: Fet3GateQ::Junction {
777                capgs2: 0.0,
778                capgs3: 0.0,
779                capgd2: 0.0,
780                capgd3: 0.0,
781            },
782        }
783    }
784}
785
786/// The derivatives (not yet Taylor coefficients) of a mode-normalized
787/// drain current in (vgs, vds) that `jfetdset.c`/`mesdset.c` keep:
788/// `gm1, gds1, gm2, gds2, gmds, gm3, gds3, gm2ds, gmds2`.
789#[derive(Clone, Copy, Debug, Default)]
790pub(crate) struct Fet3DrainDerivs {
791    pub gm1: f64,
792    pub gds1: f64,
793    pub gm2: f64,
794    pub gds2: f64,
795    pub gmds: f64,
796    pub gm3: f64,
797    pub gds3: f64,
798    pub gm2ds: f64,
799    pub gmds2: f64,
800}
801
802/// The closing block of `JFETdSetup`/`MESdSetup` for the drain
803/// current: "taking into account type and mode" — inverse mode is
804/// the chain rule for `id(vgs, vds) = −f(vgs − vds, −vds)` — then the
805/// Taylor factors (type on the even orders).
806pub(crate) fn fet3_cdr(typ: f64, mode: f64, g: &Fet3DrainDerivs) -> DfCoeffs {
807    let mut c = DfCoeffs::default();
808    if mode == 1.0 {
809        c.cxx = g.gm2;
810        c.cyy = g.gds2;
811        c.cxy = g.gmds;
812        c.cxxx = g.gm3;
813        c.cyyy = g.gds3;
814        c.cxxy = g.gm2ds;
815        c.cxyy = g.gmds2;
816    } else {
817        c.cxx = -g.gm2;
818        c.cyy = -(g.gm2 + g.gds2 + 2.0 * g.gmds);
819        c.cxy = g.gm2 + g.gmds;
820        c.cxxx = -g.gm3;
821        c.cyyy = g.gm3 + g.gds3 + 3.0 * (g.gm2ds + g.gmds2);
822        c.cxxy = g.gm3 + g.gm2ds;
823        c.cxyy = -(g.gm3 + 2.0 * g.gm2ds + g.gmds2);
824    }
825    c.cxx *= 0.5 * typ;
826    c.cyy *= 0.5 * typ;
827    c.cxy *= typ;
828    c.cxxx /= 6.;
829    c.cyyy /= 6.;
830    c.cxxy *= 0.5;
831    c.cxyy *= 0.5;
832    c
833}
834
835/// One gate junction's `(lgg1, lgg2, lgg3)` as `jfetdset.c`/
836/// `mesdset.c` write it: the conductance with gmin, then the second-
837/// and third-order Taylor coefficients of the current (zero in the
838/// `−5·vt` reverse branch, where the current is linear).
839pub(crate) fn fet3_gate_diode_disto(v: f64, csat: f64, vt: f64, gmin: f64) -> (f64, f64, f64) {
840    if v <= -5.0 * vt {
841        (-csat / v + gmin, 0.0, 0.0)
842    } else {
843        let ev = f64::exp(v / vt);
844        let g1 = csat * ev / vt + gmin;
845        let g2 = (g1 - gmin) / (vt * 2.0);
846        let g3 = g2 / (3.0 * vt);
847        (g1, g2, g3)
848    }
849}
850
851/// `JFETdisto` / `MESdisto` in the load modes (`jfetdist.c`,
852/// `mesdisto.c` — the same file but for the gate charges): x = vgs,
853/// y = vds on the internal nodes; the drain current, the two gate
854/// diodes (on vgs and vgd = x − y) and the gate charges.
855pub(crate) fn fet3_disto_load(d: &Fet3DistoCoeffs, n: &Fet3Nodes, ctx: &mut DistoCtx) {
856    let mode = ctx.mode;
857    let omega = ctx.omega;
858    let kx = ctx.h.kern(mode, n.gate, n.s_p);
859    let ky = ctx.h.kern(mode, n.d_p, n.s_p);
860    let kz = Kern::default();
861    let kgd = kx.sub(&ky);
862    let cdrain = df_term(mode, &d.cdr, &kx, &ky, &kz);
863    ctx.stamp(n.d_p, n.s_p, cdrain);
864    let ggs = d1_term(mode, d.ggs2, d.ggs3, &kx);
865    ctx.stamp(n.gate, n.s_p, ggs);
866    let ggd = d1_term(mode, d.ggd2, d.ggd3, &kgd);
867    ctx.stamp(n.gate, n.d_p, ggd);
868    match &d.q {
869        Fet3GateQ::Junction {
870            capgs2,
871            capgs3,
872            capgd2,
873            capgd3,
874        } => {
875            let capgs = jomega(omega, d1_term(mode, *capgs2, *capgs3, &kx));
876            ctx.stamp(n.gate, n.s_p, capgs);
877            let capgd = jomega(omega, d1_term(mode, *capgd2, *capgd3, &kgd));
878            ctx.stamp(n.gate, n.d_p, capgd);
879        }
880        Fet3GateQ::Statz { qgs, qgd } => {
881            let q = jomega(omega, df_term(mode, qgs, &kx, &kgd, &kz));
882            ctx.stamp(n.gate, n.s_p, q);
883            let q = jomega(omega, df_term(mode, qgd, &kx, &kgd, &kz));
884            ctx.stamp(n.gate, n.d_p, q);
885        }
886    }
887}
888
889/// `JFETdSetup` (`dev/jfet/jfetdset.c`, 3f5 and ngspice-47 alike but
890/// for ngspice's `m` and temperature-adjusted beta/threshold, which
891/// Dune's JFET does not have): the Taylor coefficients at the
892/// operating point `(vgs, vgd)` (type-normalized, from `CKTrhsOld`).
893///
894/// Two quirks of the C are kept literally:
895/// - the drain current is plain Shichman–Hodges — the setup ignores
896///   the Sydney doping tail `B` that the load uses, so with `B ≠ 1`
897///   the distortion coefficients are not the load's derivatives;
898/// - inverse mode swaps `vgs`/`vgd` before the gate junctions are
899///   evaluated, but the local names keep their zero-bias caps
900///   (`czgs` on the local `vgs` = real `vgd`); the interchange block
901///   then crosses them back, and the closing "adjust for type" block
902///   rewrites the second orders `ggs2`/`ggd2`, `capgs2`/`capgd2` from
903///   the local values once more. Net effect in inverse mode: the gs
904///   branch gets the gd diode's second order (the third is right),
905///   `czgs`'s charge at the real `vgd` for its second order and
906///   `czgd`'s charge at the real `vgs` for its third.
907pub(crate) fn jfet_dsetup(
908    t: &JfetTemp,
909    area: f64,
910    gmin: f64,
911    vgs: f64,
912    vgd: f64,
913) -> Fet3DistoCoeffs {
914    let typ = t.type_;
915    let beta = t.beta * area;
916    let csat = t.t_sat_cur * area;
917    let vt = t.vt;
918    let lambda = t.lambda;
919    let (mut vgs, mut vgd) = (vgs, vgd);
920    let mut vds = vgs - vgd;
921    let mode = if vds < 0.0 {
922        vds = -vds;
923        std::mem::swap(&mut vgs, &mut vgd);
924        -1.0
925    } else {
926        1.0
927    };
928    let (_, lggs2, lggs3) = fet3_gate_diode_disto(vgs, csat, vt, gmin);
929    let (_, lggd2, lggd3) = fet3_gate_diode_disto(vgd, csat, vt, gmin);
930
931    let vgst = vgs - t.vto;
932    let mut g = Fet3DrainDerivs::default();
933    if vgst > 0.0 {
934        let betap = beta * (1.0 + lambda * vds);
935        let twob = betap + betap;
936        if vgst <= vds {
937            // Normal mode, saturation region.
938            g.gm1 = twob * vgst;
939            g.gm2 = twob;
940            g.gds1 = lambda * beta * vgst * vgst;
941            g.gm2ds = 2.0 * lambda * beta;
942            g.gmds = g.gm2ds * vgst;
943        } else {
944            // Normal mode, linear region.
945            g.gm1 = twob * vds;
946            g.gmds = (beta + beta) * (1.0 + 2.0 * lambda * vds);
947            g.gds2 = 2.0 * beta * (2.0 * lambda * vgst - 1.0 - 3.0 * lambda * vds);
948            g.gds1 =
949                beta * (2.0 * (vgst - vds) + 4.0 * vgst * vds * lambda - 3.0 * lambda * vds * vds);
950            g.gmds2 = 4.0 * beta * lambda;
951            g.gds3 = -6.0 * beta * lambda;
952        }
953    }
954
955    // Charge storage elements.
956    let czgs = t.t_cgs * area;
957    let czgd = t.t_cgd * area;
958    let pot = t.t_gate_pot;
959    let twop = pot + pot;
960    let junction = |v: f64, cz: f64| {
961        if v < t.cor_dep_cap {
962            let sarg = f64::sqrt(1.0 - v / pot);
963            let c1 = cz / sarg;
964            let c2 = c1 / (pot * 4.0 * sarg * sarg);
965            let c3 = c2 / (pot * 2.0 * sarg * sarg);
966            (c2, c3)
967        } else {
968            let czf2 = cz / t.f2;
969            (czf2 / twop * 0.5, 0.0)
970        }
971    };
972    let (lcapgs2, lcapgs3) = junction(vgs, czgs);
973    let (lcapgd2, lcapgd3) = junction(vgd, czgd);
974
975    let cdr = fet3_cdr(typ, mode, &g);
976    // `here->ggs3 = lggd3` etc. in inverse mode; the second orders
977    // are then overwritten from the local values (quirk above).
978    let (ggs3, ggd3, capgs3, capgd3) = if mode == 1.0 {
979        (lggs3, lggd3, lcapgs3, lcapgd3)
980    } else {
981        (lggd3, lggs3, lcapgd3, lcapgs3)
982    };
983    Fet3DistoCoeffs {
984        cdr,
985        ggs2: typ * lggs2,
986        ggs3,
987        ggd2: typ * lggd2,
988        ggd3,
989        q: Fet3GateQ::Junction {
990            capgs2: typ * lcapgs2,
991            capgs3,
992            capgd2: typ * lcapgd2,
993            capgd3,
994        },
995    }
996}
997
998/// A JFET instance (`JFETinstance`).
999pub(crate) struct JfetI {
1000    n: Fet3Nodes,
1001    t: JfetTemp,
1002    area: f64,
1003    off: bool,
1004    /// `IC=VDS,VGS`.
1005    ic: (f64, f64),
1006    ic_given: crate::devices::IcGiven,
1007    st: usize,
1008    /// `.disto` Taylor coefficients (`JFETdSetup`).
1009    dist: Fet3DistoCoeffs,
1010}
1011
1012impl JfetI {
1013    pub fn new(
1014        nodes: [Var; 3],
1015        primes: [Var; 2],
1016        t: JfetTemp,
1017        area: f64,
1018        off: bool,
1019        ic: (f64, f64),
1020        st: usize,
1021    ) -> Self {
1022        JfetI {
1023            n: Fet3Nodes::new(nodes, primes),
1024            t,
1025            area,
1026            off,
1027            ic,
1028            ic_given: crate::devices::IcGiven::ALL,
1029            st,
1030            dist: Fet3DistoCoeffs::default(),
1031        }
1032    }
1033}
1034
1035impl crate::devices::Device for JfetI {
1036    fn par_load(&self) -> bool {
1037        true
1038    }
1039
1040    /// `JFETgetic` (`jfetic.c`): VDS and VGS not given on the card from the
1041    /// external nodes.
1042    fn set_ic(&mut self, rhs: &[f64]) {
1043        use crate::devices::getic;
1044        let g = self.ic_given;
1045        let n = &self.n;
1046        getic(g, 0, &mut self.ic.0, rhs, n.drain, n.source);
1047        getic(g, 1, &mut self.ic.1, rhs, n.gate, n.source);
1048    }
1049
1050    fn set_ic_given(&mut self, given: crate::devices::IcGiven) {
1051        self.ic_given = given;
1052    }
1053
1054    fn alloc(&mut self, mb: &mut MatrixBuilder) {
1055        self.n.alloc(mb);
1056    }
1057
1058    fn load(&mut self, ctx: &mut LoadCtx) {
1059        let t = &self.t;
1060        let st = self.st;
1061        let typ = t.type_;
1062        let beta = t.beta * self.area;
1063        let gdpr = t.drain_conduct * self.area;
1064        let gspr = t.source_conduct * self.area;
1065        let csat = t.t_sat_cur * self.area;
1066
1067        let bias = fet3_bias(
1068            ctx, &self.n, st, typ, self.off, self.ic, t.vt, t.vcrit, t.vto,
1069        );
1070        let (vgs, vgd) = (bias.vgs, bias.vgd);
1071
1072        // DC current and derivatives.
1073        let (ggs, cg) = gate_diode(vgs, csat, t.vt, ctx.gmin);
1074        let (ggd, cgd) = gate_diode(vgd, csat, t.vt, ctx.gmin);
1075        let (cdrain, gm, gds) = jfet_ids(vgs, vgd, t.vto, beta, t.lambda, t.b, t.b_fac);
1076        let mut op = Fet3Op {
1077            vgs,
1078            vgd,
1079            cg: cg + cgd,
1080            cd: cdrain - cgd,
1081            cgd,
1082            gm,
1083            gds,
1084            ggs,
1085            ggd,
1086        };
1087
1088        // Charge storage: transient, the small-signal pass and the
1089        // UIC transient operating point.
1090        if ctx.kind == AnKind::Tran
1091            || ctx.mode == InitMode::Smsig
1092            || (ctx.kind == AnKind::TranOp && ctx.uic)
1093        {
1094            let czgs = t.t_cgs * self.area;
1095            let czgd = t.t_cgd * self.area;
1096            let twop = t.t_gate_pot + t.t_gate_pot;
1097            let fcpb2 = t.cor_dep_cap * t.cor_dep_cap;
1098            let junction = |v: f64, cz: f64| {
1099                if v < t.cor_dep_cap {
1100                    let sarg = f64::sqrt(1.0 - v / t.t_gate_pot);
1101                    (twop * cz * (1.0 - sarg), cz / sarg)
1102                } else {
1103                    let czf2 = cz / t.f2;
1104                    (
1105                        cz * t.f1
1106                            + czf2 * (t.f3 * (v - t.cor_dep_cap) + (v * v - fcpb2) / (twop + twop)),
1107                        czf2 * (t.f3 + v / twop),
1108                    )
1109                }
1110            };
1111            let (qgs, capgs) = junction(vgs, czgs);
1112            let (qgd, capgd) = junction(vgd, czgd);
1113            ctx.states.set(0, st + QGS, qgs);
1114            ctx.states.set(0, st + QGD, qgd);
1115            if fet3_charge(ctx, st, capgs, capgd, &mut op) {
1116                return;
1117            }
1118        }
1119
1120        fet3_finish(ctx, &self.n, st, typ, gdpr, gspr, &bias, &op);
1121    }
1122
1123    /// `JFETtrunc`.
1124    fn trunc(&self, t: &crate::integ::TerrCtx, time_step: &mut f64) {
1125        crate::integ::terr(self.st + QGS, t, time_step);
1126        crate::integ::terr(self.st + QGD, t, time_step);
1127    }
1128
1129    /// `JFETacLoad`.
1130    fn load_ac(&mut self, ctx: &mut AcCtx) {
1131        let gdpr = self.t.drain_conduct * self.area;
1132        let gspr = self.t.source_conduct * self.area;
1133        fet3_ac(ctx, &self.n, self.st, gdpr, gspr);
1134    }
1135
1136    /// `JFETpzLoad` (`jfetpzld.c`): see [`fet3_pz`].
1137    fn load_pz(&mut self, ctx: &mut crate::matrix::PzCtx) -> Result<(), &'static str> {
1138        let gdpr = self.t.drain_conduct * self.area;
1139        let gspr = self.t.source_conduct * self.area;
1140        fet3_pz(ctx, &self.n, self.st, gdpr, gspr);
1141        Ok(())
1142    }
1143
1144    /// `JFETnoise`: see [`fet3_noise`].
1145    fn noise_suffixes(&self) -> Option<&'static [&'static str]> {
1146        Some(FET3_NOISE_SUFFIXES)
1147    }
1148
1149    fn noise_dens(&self, ctx: &crate::matrix::NoiseCtx) -> Vec<f64> {
1150        let gdpr = self.t.drain_conduct * self.area;
1151        let gspr = self.t.source_conduct * self.area;
1152        let t = &self.t;
1153        let chan = Fet3Channel::Jfet {
1154            temp: t.temp,
1155            nlev: t.nlev,
1156            gdsnoi: t.gdsnoi,
1157            vto: t.vto,
1158            beta: t.beta * self.area,
1159        };
1160        fet3_noise(ctx, &self.n, self.st, gdpr, gspr, t.kf, t.af, &chan)
1161    }
1162
1163    /// `JFETdSetup`: see [`jfet_dsetup`].
1164    fn disto_setup(&mut self, ctx: &DistoSetupCtx) -> Result<(), &'static str> {
1165        let n = &self.n;
1166        let typ = self.t.type_;
1167        let vgs = typ * (ctx.sol[n.gate] - ctx.sol[n.s_p]);
1168        let vgd = typ * (ctx.sol[n.gate] - ctx.sol[n.d_p]);
1169        self.dist = jfet_dsetup(&self.t, self.area, ctx.gmin, vgs, vgd);
1170        Ok(())
1171    }
1172
1173    /// `JFETdisto` (`jfetdist.c`).
1174    fn disto_load(&self, ctx: &mut DistoCtx) {
1175        fet3_disto_load(&self.dist, &self.n, ctx);
1176    }
1177}
1178
1179#[cfg(test)]
1180pub(crate) mod tests {
1181    use super::*;
1182
1183    #[test]
1184    fn temp_is_identity_at_tnom() {
1185        let mut m = JfetModel::new(true);
1186        m.cgs = 2e-12;
1187        m.cgd = 1e-12;
1188        m.pb = 0.8;
1189        m.rd = 50.0;
1190        let t = JfetTemp::new("j1", &m, REFTEMP, REFTEMP);
1191        assert_eq!(t.t_sat_cur, m.is);
1192        assert!((t.t_gate_pot - 0.8).abs() < 1e-12);
1193        assert!((t.t_cgs - 2e-12).abs() < 1e-24);
1194        assert!((t.t_cgd - 1e-12).abs() < 1e-24);
1195        assert_eq!(t.drain_conduct, 0.02);
1196        assert_eq!(t.source_conduct, 0.0);
1197        assert_eq!(t.b_fac, 0.0); // B = 1
1198        let vt = REFTEMP * KOVERQ;
1199        assert!((t.vcrit - vt * (vt / (ROOT2 * 1e-14)).ln()).abs() < 1e-15);
1200        // f1/f2/f3 for FC = 0.5.
1201        assert!((t.f2 - 0.5f64.powf(1.5)).abs() < 1e-15);
1202        assert!((t.f3 - 0.25).abs() < 1e-15);
1203        assert!((t.f1 - 0.8 * (1.0 - 0.5f64.sqrt()) / 0.5).abs() < 1e-14);
1204        assert!(t.notes.is_empty());
1205    }
1206
1207    #[test]
1208    fn temp_clamps_fc_and_warms_up() {
1209        let mut m = JfetModel::new(false);
1210        m.fc = 0.99;
1211        let t = JfetTemp::new("j1", &m, REFTEMP + 50.0, REFTEMP);
1212        assert_eq!(t.notes.len(), 1);
1213        assert!((t.cor_dep_cap - 0.95 * t.t_gate_pot).abs() < 1e-15);
1214        // Hotter: more saturation current, lower junction potential.
1215        assert!(t.t_sat_cur > m.is);
1216        assert!(t.t_gate_pot < m.pb);
1217        assert_eq!(t.type_, -1.0);
1218    }
1219
1220    #[test]
1221    fn ids_reduces_to_shichman_hodges_with_b_one() {
1222        let (beta, lambda, vto) = (1e-3, 0.02, -2.0);
1223        // Saturation: beta·(1+λ·vds)·vgst².
1224        let (vgs, vgd) = (-1.0, -6.0);
1225        let (id, gm, gds) = jfet_ids(vgs, vgd, vto, beta, lambda, 1.0, 0.0);
1226        let vgst = vgs - vto;
1227        let vds = vgs - vgd;
1228        assert!((id - beta * (1.0 + lambda * vds) * vgst * vgst).abs() < 1e-18);
1229        assert!((gm - 2.0 * beta * (1.0 + lambda * vds) * vgst).abs() < 1e-18);
1230        assert!((gds - lambda * beta * vgst * vgst).abs() < 1e-18);
1231        // Linear: beta·(1+λ·vds)·vds·(2·vgst − vds).
1232        let (vgs, vgd) = (0.0, -0.5);
1233        let (id, _, _) = jfet_ids(vgs, vgd, vto, beta, lambda, 1.0, 0.0);
1234        let vds = 0.5;
1235        let vgst = 2.0;
1236        assert!((id - beta * (1.0 + lambda * vds) * vds * (2.0 * vgst - vds)).abs() < 1e-18);
1237        // Cutoff.
1238        assert_eq!(
1239            jfet_ids(-3.0, -8.0, vto, beta, lambda, 1.0, 0.0),
1240            (0.0, 0.0, 0.0)
1241        );
1242    }
1243
1244    #[test]
1245    fn ids_derivatives_match_finite_differences() {
1246        // B ≠ 1 exercises the doping-tail terms; check every region
1247        // in both modes against central differences.
1248        let (beta, lambda, vto, b, pb) = (1e-3, 0.05, -2.0, 0.8, 1.0);
1249        let b_fac = (1.0 - b) / (pb - vto);
1250        let h = 1e-6;
1251        for &(vgs, vgd) in &[
1252            (-0.5, -1.0), // normal linear
1253            (-0.5, -4.0), // normal saturation
1254            (-1.0, -0.6), // inverse linear
1255            (-4.0, -0.5), // inverse saturation
1256        ] {
1257            let f = |a: f64, c: f64| jfet_ids(a, c, vto, beta, lambda, b, b_fac).0;
1258            let (_, gm, gds) = jfet_ids(vgs, vgd, vto, beta, lambda, b, b_fac);
1259            // The C's (gm, gds) are ∂id/∂vgs at fixed vds and ∂id/∂vds
1260            // at fixed vgs, with vgd = vgs − vds.
1261            let vds = vgs - vgd;
1262            let dgs = (f(vgs + h, vgs + h - vds) - f(vgs - h, vgs - h - vds)) / (2.0 * h);
1263            let dds = (f(vgs, vgs - vds - h) - f(vgs, vgs - vds + h)) / (2.0 * h);
1264            assert!(
1265                (gm - dgs).abs() < 1e-8 * (1.0 + gm.abs()),
1266                "gm at {vgs},{vgd}"
1267            );
1268            assert!(
1269                (gds - dds).abs() < 1e-8 * (1.0 + gds.abs()),
1270                "gds at {vgs},{vgd}"
1271            );
1272        }
1273    }
1274
1275    #[test]
1276    fn gate_diode_regions() {
1277        let vt = REFTEMP * KOVERQ;
1278        let (g, i) = gate_diode(-1.0, 1e-14, vt, 1e-12);
1279        assert!((i - (-1e-14 - 1e-12)).abs() < 1e-27);
1280        assert!((g - (1e-14 + 1e-12)).abs() < 1e-27);
1281        let (g, i) = gate_diode(0.6, 1e-14, vt, 0.0);
1282        let e = (0.6 / vt).exp();
1283        assert_eq!(i, 1e-14 * (e - 1.0));
1284        assert_eq!(g, 1e-14 * e / vt);
1285    }
1286
1287    /// The seven Taylor coefficients of a two-variable function at
1288    /// `(x, y)` by central differences, in `DfCoeffs` order
1289    /// `(cxx, cyy, cxy, cxxx, cyyy, cxxy, cxyy)` — ½f_xx, ½f_yy, f_xy,
1290    /// ⅙f_xxx, ⅙f_yyy, ½f_xxy, ½f_xyy.
1291    pub(crate) fn taylor2_fd(f: &dyn Fn(f64, f64) -> f64, x: f64, y: f64, h: f64) -> [f64; 7] {
1292        let fxx = (f(x + h, y) - 2.0 * f(x, y) + f(x - h, y)) / (h * h);
1293        let fyy = (f(x, y + h) - 2.0 * f(x, y) + f(x, y - h)) / (h * h);
1294        let fxy =
1295            (f(x + h, y + h) - f(x + h, y - h) - f(x - h, y + h) + f(x - h, y - h)) / (4.0 * h * h);
1296        let d3 = |dx: f64, dy: f64| {
1297            // Third difference along a direction.
1298            (f(x + 2.0 * h * dx, y + 2.0 * h * dy) - 2.0 * f(x + h * dx, y + h * dy)
1299                + 2.0 * f(x - h * dx, y - h * dy)
1300                - f(x - 2.0 * h * dx, y - 2.0 * h * dy))
1301                / (2.0 * h * h * h)
1302        };
1303        let fxxx = d3(1.0, 0.0);
1304        let fyyy = d3(0.0, 1.0);
1305        // Mixed thirds from the directional ones: D(1,1)³ = fxxx +
1306        // 3fxxy + 3fxyy + fyyy, D(1,−1)³ = fxxx − 3fxxy + 3fxyy − fyyy.
1307        let dp = d3(1.0, 1.0);
1308        let dm = d3(1.0, -1.0);
1309        let fxxy = (dp - dm - 2.0 * fyyy) / 6.0;
1310        let fxyy = (dp + dm - 2.0 * fxxx) / 6.0;
1311        [
1312            0.5 * fxx,
1313            0.5 * fyy,
1314            fxy,
1315            fxxx / 6.0,
1316            fyyy / 6.0,
1317            0.5 * fxxy,
1318            0.5 * fxyy,
1319        ]
1320    }
1321
1322    /// [`taylor2_fd`] with one Richardson step (`(4·T(h/2) − T(h))/3`,
1323    /// O(h⁴)), for the non-polynomial branches.
1324    pub(crate) fn taylor2_fd_rich(f: &dyn Fn(f64, f64) -> f64, x: f64, y: f64, h: f64) -> [f64; 7] {
1325        let a = taylor2_fd(f, x, y, h);
1326        let b = taylor2_fd(f, x, y, h / 2.0);
1327        std::array::from_fn(|k| (4.0 * b[k] - a[k]) / 3.0)
1328    }
1329
1330    pub(crate) fn df_seven(c: &DfCoeffs) -> [f64; 7] {
1331        [c.cxx, c.cyy, c.cxy, c.cxxx, c.cyyy, c.cxxy, c.cxyy]
1332    }
1333
1334    /// Compare against finite differences; the scale is the largest
1335    /// coefficient of the same order, so near-zero entries are
1336    /// judged against their siblings.
1337    pub(crate) fn assert_taylor(got: [f64; 7], want: [f64; 7], tol: f64, what: &str) -> f64 {
1338        let s2 = got[..3]
1339            .iter()
1340            .chain(&want[..3])
1341            .fold(0.0f64, |a, v| a.max(v.abs()));
1342        let s3 = got[3..]
1343            .iter()
1344            .chain(&want[3..])
1345            .fold(0.0f64, |a, v| a.max(v.abs()));
1346        let mut worst = 0.0f64;
1347        for k in 0..7 {
1348            let s = if k < 3 { s2 } else { s3 };
1349            if s == 0.0 {
1350                continue;
1351            }
1352            let e = (got[k] - want[k]).abs() / s;
1353            worst = worst.max(e);
1354            assert!(
1355                e <= tol,
1356                "{what}[{k}]: setup {} vs finite difference {} (rel {e:.2e})",
1357                got[k],
1358                want[k]
1359            );
1360        }
1361        worst
1362    }
1363
1364    fn jfet_model() -> JfetModel {
1365        let mut m = JfetModel::new(true);
1366        m.vto = -2.0;
1367        m.beta = 1.3e-3;
1368        m.lambda = 0.03;
1369        m.cgs = 3e-12;
1370        m.cgd = 1.5e-12;
1371        m.pb = 0.8;
1372        m.is = 1e-13;
1373        m
1374    }
1375
1376    /// The drain-current coefficients of `JFETdSetup` are the Taylor
1377    /// coefficients of the load's Shichman–Hodges current (B = 1) in
1378    /// the real node voltages (x = vgs, y = vds), in every region,
1379    /// both modes and both polarities; the gate-diode and depletion-
1380    /// charge coefficients are those of the load's diode current and
1381    /// charge — on the right branch in normal mode, and on the
1382    /// interchanged one for the second order in inverse mode (the
1383    /// C's quirk, see [`jfet_dsetup`]).
1384    #[test]
1385    fn jfet_dsetup_matches_finite_differences() {
1386        let gmin = 1e-12;
1387        let mut worst = 0.0f64;
1388        for njf in [true, false] {
1389            let mut m = jfet_model();
1390            m.njf = njf;
1391            let t = JfetTemp::new("j1", &m, REFTEMP, REFTEMP);
1392            let typ = t.type_;
1393            let area = 1.7;
1394            let beta = t.beta * area;
1395            let csat = t.t_sat_cur * area;
1396            // Real (signed) node voltages vgs, vds of the device.
1397            for &(vgs_n, vds_n) in &[
1398                (-0.7, 0.6),  // normal linear
1399                (-0.7, 4.0),  // normal saturation
1400                (-0.7, -0.5), // inverse linear
1401                (-1.2, -5.0), // inverse saturation
1402                (0.3, 3.0),   // forward gate diode, saturation
1403                (0.6, 3.0),   // past FC·PB (linearized charge)
1404                (-3.0, 2.0),  // cutoff
1405            ] {
1406                let (vgs_a, vds_a) = (typ * vgs_n, typ * vds_n);
1407                let d = jfet_dsetup(&t, area, gmin, vgs_n, vgs_n - vds_n);
1408                let id = |x: f64, y: f64| {
1409                    typ * jfet_ids(typ * x, typ * (x - y), t.vto, beta, t.lambda, 1.0, 0.0).0
1410                };
1411                let want = taylor2_fd(&id, vgs_a, vds_a, 1e-2);
1412                worst = worst.max(assert_taylor(
1413                    df_seven(&d.cdr),
1414                    want,
1415                    1e-6,
1416                    &format!("cdr njf={njf} at ({vgs_n},{vds_n})"),
1417                ));
1418                // One-variable branches: gate diodes and charges.
1419                let diode = |v: f64| typ * gate_diode(typ * v, csat, t.vt, gmin).1;
1420                let pot = t.t_gate_pot;
1421                let charge = |cz: f64| {
1422                    move |v: f64| {
1423                        let v = typ * v;
1424                        let q = if v < t.cor_dep_cap {
1425                            2.0 * pot * cz * (1.0 - f64::sqrt(1.0 - v / pot))
1426                        } else {
1427                            let czf2 = cz / t.f2;
1428                            let fcpb2 = t.cor_dep_cap * t.cor_dep_cap;
1429                            cz * t.f1
1430                                + czf2
1431                                    * (t.f3 * (v - t.cor_dep_cap) + (v * v - fcpb2) / (4.0 * pot))
1432                        };
1433                        typ * q
1434                    }
1435                };
1436                let one = |f: &dyn Fn(f64) -> f64, v: f64, h: f64| {
1437                    let c2 = 0.5 * (f(v + h) - 2.0 * f(v) + f(v - h)) / (h * h);
1438                    let c3 = (f(v + 2.0 * h) - 2.0 * f(v + h) + 2.0 * f(v - h) - f(v - 2.0 * h))
1439                        / (2.0 * h * h * h)
1440                        / 6.0;
1441                    (c2, c3)
1442                };
1443                let (vgd_a, inverse) = (vgs_a - vds_a, typ * vds_a < 0.0);
1444                let (gs2, gs3) = one(&diode, vgs_a, 2e-4);
1445                let (gd2, gd3) = one(&diode, vgd_a, 2e-4);
1446                let qgs = charge(t.t_cgs * area);
1447                let qgd = charge(t.t_cgd * area);
1448                // Normal mode: each charge on its own branch. Inverse
1449                // mode, as the C computes it: the local (swapped)
1450                // names keep czgs/czgd, so the second orders are
1451                // czgs's charge at the real vgd on the gs branch (and
1452                // czgd's at vgs on gd), the third orders czgd's charge
1453                // at the real vgs on gs (czgs's at vgd on gd).
1454                let (cs2, cd2, cs3, cd3) = if inverse {
1455                    (
1456                        one(&qgs, vgd_a, 1e-3).0,
1457                        one(&qgd, vgs_a, 1e-3).0,
1458                        one(&qgd, vgs_a, 1e-3).1,
1459                        one(&qgs, vgd_a, 1e-3).1,
1460                    )
1461                } else {
1462                    let (cs2, cs3) = one(&qgs, vgs_a, 1e-3);
1463                    let (cd2, cd3) = one(&qgd, vgd_a, 1e-3);
1464                    (cs2, cd2, cs3, cd3)
1465                };
1466                // Diodes (one saturation current): the second orders
1467                // swap branches in inverse mode, the thirds do not.
1468                let (wgs2, wgd2) = if inverse { (gd2, gs2) } else { (gs2, gd2) };
1469                // (absolute floor: FD rounding where the branch is
1470                // linear — reverse diode, flat charge — or the third
1471                // difference of a 1e-12 quantity)
1472                let close = |a: f64, b: f64, floor: f64, what: &str| {
1473                    let s = a.abs().max(b.abs());
1474                    assert!((a - b).abs() <= 2e-3 * s + floor, "{what}: {a} vs {b}");
1475                };
1476                close(d.ggs2, wgs2, 1e-15, "ggs2");
1477                close(d.ggd2, wgd2, 1e-15, "ggd2");
1478                close(d.ggs3, gs3, 1e-15, "ggs3");
1479                close(d.ggd3, gd3, 1e-15, "ggd3");
1480                let Fet3GateQ::Junction {
1481                    capgs2,
1482                    capgs3,
1483                    capgd2,
1484                    capgd3,
1485                } = d.q
1486                else {
1487                    panic!("JFET charges are one-variable")
1488                };
1489                close(capgs2, cs2, 1e-17, "capgs2");
1490                close(capgd2, cd2, 1e-17, "capgd2");
1491                close(capgs3, cs3, 1e-17, "capgs3");
1492                close(capgd3, cd3, 1e-17, "capgd3");
1493            }
1494        }
1495        assert!(worst < 1e-6, "worst {worst}");
1496        eprintln!("JFET cdr worst FD relative error {worst:.2e}");
1497    }
1498}