Skip to main content

dune_core/
mes.rs

1//! MESFET — the GaAs FET model of Statz et al., from the 3f5
2//! (`dev/mes/mesload.c`, `mestemp.c`, `messetup.c`, `mesacl.c`,
3//! `mestrunc.c`). Drain current with the `tanh` replaced by the cubic
4//! `1 − (1 − α·vds/3)³` below `3/α`, doping-tail denominator
5//! `1 + B·(vgs − VTO)`, gate junction diodes at the fixed `CONSTvt0`
6//! (the 3f5 MESFET has no temperature dependence), and Statz's total
7//! gate charge (`qggnew`) split between G-S and G-D by the
8//! four-point average around the previous timepoint.
9//!
10//! The bias ladder, gate diodes, stamps and AC load are the JFET's
11//! (`jfet.rs`): `mesload.c` is the same code outside the drain
12//! current and the charge.
13
14use crate::consts::{ROOT2, VT0};
15use crate::dderivs::Dderivs;
16use crate::disto::{DfCoeffs, DistoCtx, DistoSetupCtx};
17use crate::jfet::{
18    CQGD, CQGS, FET3_NUM_STATES, Fet3DistoCoeffs, Fet3DrainDerivs, Fet3GateQ, Fet3Nodes, Fet3Op,
19    QGD, QGS, VGD, VGS, fet3_ac, fet3_bias, fet3_cdr, fet3_charge, fet3_disto_load, fet3_finish,
20    fet3_gate_diode_disto, fet3_pz, gate_diode,
21};
22use crate::matrix::{AcCtx, AnKind, InitMode, LoadCtx, MatrixBuilder, Var};
23
24pub(crate) const MES_NUM_STATES: usize = FET3_NUM_STATES;
25const _: () = assert!(CQGS == QGS + 1 && CQGD == QGD + 1);
26
27/// `.model … nmf(…)/pmf(…)` parameters, 3f5 names and the defaults of
28/// `messetup.c` (the manual's `BETA` default of 1e-4 is not what the
29/// code does: `MESsetup` uses 2.5e-3).
30#[derive(Clone, Debug)]
31pub struct MesModel {
32    /// `NMF` (true) or `PMF`.
33    pub nmf: bool,
34    /// `VTO`/`VT0`: pinch-off voltage, default −2.
35    pub vto: f64,
36    /// `BETA`: transconductance parameter, A/V², default 2.5e-3.
37    pub beta: f64,
38    /// `B`: doping tail extending parameter, 1/V, default 0.3.
39    pub b: f64,
40    /// `ALPHA`: saturation voltage parameter, 1/V, default 2.
41    pub alpha: f64,
42    /// `LAMBDA`: channel-length modulation, 1/V, default 0.
43    pub lambda: f64,
44    /// `RD`/`RS`: drain/source ohmic resistance, default 0.
45    pub rd: f64,
46    pub rs: f64,
47    /// `CGS`/`CGD`: zero-bias G-S/G-D junction capacitance, default 0.
48    pub cgs: f64,
49    pub cgd: f64,
50    /// `PB`: gate junction potential, default 1.
51    pub pb: f64,
52    /// `IS`: junction saturation current, default 1e-14.
53    pub is: f64,
54    /// `FC`: forward-bias depletion capacitance coefficient, default
55    /// 0.5 (`MEStemp` derives `f1`–`f3` from it, which no load uses).
56    pub fc: f64,
57    /// `KF`/`AF` (noise only).
58    pub kf: f64,
59    pub af: f64,
60}
61
62impl MesModel {
63    pub fn new(nmf: bool) -> Self {
64        MesModel {
65            nmf,
66            vto: -2.0,
67            beta: 2.5e-3,
68            b: 0.3,
69            alpha: 2.0,
70            lambda: 0.0,
71            rd: 0.0,
72            rs: 0.0,
73            cgs: 0.0,
74            cgd: 0.0,
75            pb: 1.0,
76            is: 1e-14,
77            fc: 0.5,
78            kf: 0.0,
79            af: 1.0,
80        }
81    }
82}
83
84impl Default for MesModel {
85    fn default() -> Self {
86        MesModel::new(true)
87    }
88}
89
90/// `MEStemp`: series conductances and the gate-junction `vcrit` at
91/// `CONSTvt0` (model level; there is no instance block).
92#[derive(Clone, Debug)]
93pub(crate) struct MesTemp {
94    pub type_: f64,
95    pub m: MesModel,
96    pub drain_conduct: f64,
97    pub source_conduct: f64,
98    pub vcrit: f64,
99}
100
101impl MesTemp {
102    pub fn new(m: &MesModel) -> Self {
103        let conduct = |r: f64| if r != 0.0 { 1.0 / r } else { 0.0 };
104        MesTemp {
105            type_: if m.nmf { 1.0 } else { -1.0 },
106            m: m.clone(),
107            drain_conduct: conduct(m.rd),
108            source_conduct: conduct(m.rs),
109            vcrit: VT0 * f64::ln(VT0 / (ROOT2 * m.is)),
110        }
111    }
112}
113
114/// Statz drain current and derivatives (`mesload.c`): `(cdrain, gm,
115/// gds)`, normal and inverse mode.
116pub(crate) fn mes_ids(
117    vgs: f64,
118    vgd: f64,
119    vto: f64,
120    beta: f64,
121    lambda: f64,
122    b: f64,
123    alpha: f64,
124) -> (f64, f64, f64) {
125    let vds = vgs - vgd;
126    if vds >= 0.0 {
127        let vgst = vgs - vto;
128        if vgst <= 0.0 {
129            return (0.0, 0.0, 0.0);
130        }
131        let prod = 1.0 + lambda * vds;
132        let betap = beta * prod;
133        let denom = 1.0 + b * vgst;
134        let invdenom = 1.0 / denom;
135        if vds >= 3.0 / alpha {
136            // Normal mode, saturation region.
137            (
138                betap * vgst * vgst * invdenom,
139                betap * vgst * (1.0 + denom) * invdenom * invdenom,
140                lambda * beta * vgst * vgst * invdenom,
141            )
142        } else {
143            // Normal mode, linear region.
144            let afact = 1.0 - alpha * vds / 3.0;
145            let lfact = 1.0 - afact * afact * afact;
146            (
147                betap * vgst * vgst * invdenom * lfact,
148                betap * vgst * (1.0 + denom) * invdenom * invdenom * lfact,
149                beta * vgst * vgst * invdenom * (alpha * afact * afact * prod + lfact * lambda),
150            )
151        }
152    } else {
153        let vgdt = vgd - vto;
154        if vgdt <= 0.0 {
155            return (0.0, 0.0, 0.0);
156        }
157        let prod = 1.0 - lambda * vds;
158        let betap = beta * prod;
159        let denom = 1.0 + b * vgdt;
160        let invdenom = 1.0 / denom;
161        if -vds >= 3.0 / alpha {
162            // Inverse mode, saturation region.
163            let gm = -betap * vgdt * (1.0 + denom) * invdenom * invdenom;
164            (
165                -betap * vgdt * vgdt * invdenom,
166                gm,
167                lambda * beta * vgdt * vgdt * invdenom - gm,
168            )
169        } else {
170            // Inverse mode, linear region.
171            let afact = 1.0 + alpha * vds / 3.0;
172            let lfact = 1.0 - afact * afact * afact;
173            let gm = -betap * vgdt * (1.0 + denom) * invdenom * invdenom * lfact;
174            (
175                -betap * vgdt * vgdt * invdenom * lfact,
176                gm,
177                beta * vgdt * vgdt * invdenom * (alpha * afact * afact * prod + lfact * lambda)
178                    - gm,
179            )
180        }
181    }
182}
183
184/// `qggnew` (`mesload.c`): Statz's total gate charge at
185/// `(vgs, vgd)` and its partial capacitances `(q, cgs, cgd)`.
186#[allow(clippy::too_many_arguments)]
187pub(crate) fn qggnew(
188    vgs: f64,
189    vgd: f64,
190    phib: f64,
191    vcap: f64,
192    vto: f64,
193    cgs: f64,
194    cgd: f64,
195) -> (f64, f64, f64) {
196    let veroot = f64::sqrt((vgs - vgd) * (vgs - vgd) + vcap * vcap);
197    let veff1 = 0.5 * (vgs + vgd + veroot);
198    let veff2 = veff1 - veroot;
199    let del = 0.2;
200    let vnroot = f64::sqrt((veff1 - vto) * (veff1 - vto) + del * del);
201    let mut vnew1 = 0.5 * (veff1 + vto + vnroot);
202    let vnew3 = vnew1;
203    let vmax = 0.5;
204    let ext;
205    if vnew1 < vmax {
206        ext = 0.0;
207    } else {
208        vnew1 = vmax;
209        ext = (vnew3 - vmax) / f64::sqrt(1.0 - vmax / phib);
210    }
211    let qroot = f64::sqrt(1.0 - vnew1 / phib);
212    let qggval = cgs * (2.0 * phib * (1.0 - qroot) + ext) + cgd * veff2;
213    let par1 = 0.5 * (1.0 + (veff1 - vto) / vnroot);
214    let cfact = (vgs - vgd) / veroot;
215    let cplus = 0.5 * (1.0 + cfact);
216    let cminus = cplus - cfact;
217    (
218        qggval,
219        cgs / qroot * par1 * cplus + cgd * cminus,
220        cgs / qroot * par1 * cminus + cgd * cplus,
221    )
222}
223
224/// `MESdSetup` (`dev/mes/mesdset.c`, identical in ngspice-47 but for
225/// its `m`): Taylor coefficients at the operating point `(vgs, vgd)`
226/// (type-normalized, from `CKTrhsOld`), carried through the C's
227/// `Dderivs` chain operation by operation.
228///
229/// The drain current is the load's Statz current (p = vgs, r = vds
230/// of the mode-normalized device). The gate diodes are evaluated on
231/// the real `vgs`/`vgd` (before the source/drain interchange, unlike
232/// the JFET). The gate charges are NOT the load's `qggnew` split:
233/// the C assumes `qgs = vgs·cgsnew`, `qgd = vgd·cgdnew` ("probably
234/// wrong but then so is the a.c. analysis routine", its comment) and
235/// builds `cgsnew` with `czgs/(qroot·par1)` where `qggnew` has
236/// `czgs/qroot·par1` — both kept literally.
237#[allow(clippy::too_many_lines)]
238pub(crate) fn mes_dsetup(
239    t: &MesTemp,
240    area: f64,
241    gmin: f64,
242    vgs_real: f64,
243    vgd_real: f64,
244) -> Fet3DistoCoeffs {
245    let m = &t.m;
246    let typ = t.type_;
247    let beta = m.beta * area;
248    let csat = m.is * area;
249    let vto = m.vto;
250    let vds_real = vgs_real - vgd_real;
251    let (_, lggs2, lggs3) = fet3_gate_diode_disto(vgs_real, csat, VT0, gmin);
252    let (_, lggd2, lggd3) = fet3_gate_diode_disto(vgd_real, csat, VT0, gmin);
253
254    // Drain current, local (mode-normalized) voltages.
255    let (vgs, vds, mode) = if vds_real < 0.0 {
256        (vgd_real, -vds_real, -1.0)
257    } else {
258        (vgs_real, vds_real, 1.0)
259    };
260    let mut d_p = Dderivs::var_p();
261    let mut d_r = Dderivs::var_r();
262    let d_zero = Dderivs::default();
263    d_p.value = vgs;
264    d_r.value = vds;
265    let vgst = vgs - vto;
266    let mut d_vgst = d_p;
267    d_vgst.value = vgst;
268    let d_cdrain = if vgst <= 0.0 {
269        d_zero
270    } else {
271        let mut d_prod = d_r.times(m.lambda);
272        d_prod.value = 1.0 + m.lambda * vds;
273        let d_betap = d_prod.times(beta);
274        let mut d_denom = d_vgst.times(m.b);
275        d_denom.value = 1.0 + m.b * vgst;
276        let d_invdenom = d_denom.inv();
277        // Saturation region.
278        let mut d_cdrain = d_betap.mult(&d_vgst).mult(&d_vgst).mult(&d_invdenom);
279        if vds < 3.0 / m.alpha {
280            // Linear region.
281            let mut d_afact = d_r.times(-m.alpha / 3.0);
282            d_afact.value = 1.0 - m.alpha * vds / 3.0;
283            let mut d_lfact = d_afact.cube().times(-1.0);
284            d_lfact.value += 1.0;
285            d_cdrain = d_betap
286                .mult(&d_vgst)
287                .mult(&d_vgst)
288                .mult(&d_invdenom)
289                .mult(&d_lfact);
290        }
291        d_cdrain
292    };
293
294    // Charge storage: p = vgs, q = vgd (the real ones).
295    let czgs = m.cgs * area;
296    let czgd = m.cgd * area;
297    let phib = m.pb;
298    let vcap = 1.0 / m.alpha;
299    let mut d_p = Dderivs::var_p();
300    let mut d_q = Dderivs::var_q();
301    d_p.value = vgs_real;
302    d_q.value = vgd_real;
303    let mut d_veroot = d_q.times(-1.0).plus(&d_p);
304    d_veroot = d_veroot.mult(&d_veroot);
305    d_veroot.value += vcap * vcap;
306    let d_veroot = d_veroot.sqrt();
307    let d_veff1 = d_veroot.plus(&d_p).plus(&d_q).times(0.5);
308    let del = 0.2;
309    // The region test reads the plain-double `vnew1` of the C.
310    let veroot = f64::sqrt((vgs_real - vgd_real) * (vgs_real - vgd_real) + vcap * vcap);
311    let veff1 = 0.5 * (vgs_real + vgd_real + veroot);
312    let vnroot = f64::sqrt((veff1 - vto) * (veff1 - vto) + del * del);
313    let vnew1 = 0.5 * (veff1 + vto + vnroot);
314    let mut d_vnroot = d_veff1;
315    d_vnroot.value -= vto;
316    d_vnroot = d_vnroot.mult(&d_vnroot);
317    d_vnroot.value += del * del;
318    let d_vnroot = d_vnroot.sqrt();
319    let mut d_vnew1 = d_veff1.plus(&d_vnroot);
320    d_vnew1.value += vto;
321    let mut d_vnew1 = d_vnew1.times(0.5);
322    let vmax = 0.5;
323    if vnew1 >= vmax {
324        // (the C also builds `d_ext`, which no charge uses)
325        d_vnew1 = d_zero;
326        d_vnew1.value = vmax;
327    }
328    let mut d_qroot = d_vnew1.times(-1.0 / phib);
329    d_qroot.value += 1.0;
330    let d_qroot = d_qroot.sqrt();
331    let mut d_par1 = d_veff1;
332    d_par1.value -= vto;
333    let mut d_par1 = d_par1.div(&d_vnroot);
334    d_par1.value += 1.0;
335    let d_par1 = d_par1.times(0.5);
336    let d_cfact = d_q.times(-1.0).plus(&d_p).div(&d_veroot);
337    let mut d_cplus = d_cfact.times(0.5);
338    d_cplus.value += 0.5;
339    let mut d_cminus = d_cfact.times(-0.5);
340    d_cminus.value += 0.5;
341    let d_dummy = d_qroot.mult(&d_par1).inv().times(czgs);
342    let d_cgsnew = d_cminus.times(czgd).plus(&d_dummy.mult(&d_cplus));
343    let d_cgdnew = d_cplus.times(czgd).plus(&d_dummy.mult(&d_cminus));
344    let d_qgs = d_cgsnew.mult(&d_p);
345    let d_qgd = d_cgdnew.mult(&d_q);
346
347    let dc = &d_cdrain;
348    let g = Fet3DrainDerivs {
349        gm1: dc.d1_p,
350        gds1: dc.d1_r,
351        gm2: dc.d2_p2,
352        gds2: dc.d2_r2,
353        gmds: dc.d2_pr,
354        gm3: dc.d3_p3,
355        gds3: dc.d3_r3,
356        gm2ds: dc.d3_p2r,
357        gmds2: dc.d3_pr2,
358    };
359    let q2 = |d: &Dderivs| DfCoeffs {
360        cxx: 0.5 * typ * d.d2_p2,
361        cyy: 0.5 * typ * d.d2_q2,
362        cxy: typ * d.d2_pq,
363        cxxx: d.d3_p3 / 6.,
364        cyyy: d.d3_q3 / 6.,
365        cxxy: 0.5 * d.d3_p2q,
366        cxyy: 0.5 * d.d3_pq2,
367        ..DfCoeffs::default()
368    };
369    Fet3DistoCoeffs {
370        cdr: fet3_cdr(typ, mode, &g),
371        ggs2: typ * lggs2,
372        ggs3: lggs3,
373        ggd2: typ * lggd2,
374        ggd3: lggd3,
375        q: Fet3GateQ::Statz {
376            qgs: q2(&d_qgs),
377            qgd: q2(&d_qgd),
378        },
379    }
380}
381
382/// A MESFET instance (`MESinstance`).
383pub(crate) struct MesI {
384    n: Fet3Nodes,
385    t: MesTemp,
386    area: f64,
387    off: bool,
388    /// `IC=VDS,VGS`.
389    ic: (f64, f64),
390    ic_given: crate::devices::IcGiven,
391    st: usize,
392    /// `.disto` Taylor coefficients (`MESdSetup`).
393    dist: Fet3DistoCoeffs,
394}
395
396impl MesI {
397    pub fn new(
398        nodes: [Var; 3],
399        primes: [Var; 2],
400        t: MesTemp,
401        area: f64,
402        off: bool,
403        ic: (f64, f64),
404        st: usize,
405    ) -> Self {
406        MesI {
407            n: Fet3Nodes::new(nodes, primes),
408            t,
409            area,
410            off,
411            ic,
412            ic_given: crate::devices::IcGiven::ALL,
413            st,
414            dist: Fet3DistoCoeffs::default(),
415        }
416    }
417}
418
419impl crate::devices::Device for MesI {
420    fn par_load(&self) -> bool {
421        true
422    }
423
424    /// `MESgetic` (`mesgetic.c`): VDS and VGS not given on the card from the
425    /// external nodes.
426    fn set_ic(&mut self, rhs: &[f64]) {
427        use crate::devices::getic;
428        let g = self.ic_given;
429        let n = &self.n;
430        getic(g, 0, &mut self.ic.0, rhs, n.drain, n.source);
431        getic(g, 1, &mut self.ic.1, rhs, n.gate, n.source);
432    }
433
434    fn set_ic_given(&mut self, given: crate::devices::IcGiven) {
435        self.ic_given = given;
436    }
437
438    fn alloc(&mut self, mb: &mut MatrixBuilder) {
439        self.n.alloc(mb);
440    }
441
442    fn load(&mut self, ctx: &mut LoadCtx) {
443        let m = &self.t.m;
444        let st = self.st;
445        let typ = self.t.type_;
446        let beta = m.beta * self.area;
447        let gdpr = self.t.drain_conduct * self.area;
448        let gspr = self.t.source_conduct * self.area;
449        let csat = m.is * self.area;
450
451        let bias = fet3_bias(
452            ctx,
453            &self.n,
454            st,
455            typ,
456            self.off,
457            self.ic,
458            VT0,
459            self.t.vcrit,
460            m.vto,
461        );
462        let (vgs, vgd) = (bias.vgs, bias.vgd);
463
464        let (ggs, cg) = gate_diode(vgs, csat, VT0, ctx.gmin);
465        let (ggd, cgd) = gate_diode(vgd, csat, VT0, ctx.gmin);
466        let (cdrain, gm, gds) = mes_ids(vgs, vgd, m.vto, beta, m.lambda, m.b, m.alpha);
467        let mut op = Fet3Op {
468            vgs,
469            vgd,
470            cg: cg + cgd,
471            cd: cdrain - cgd,
472            cgd,
473            gm,
474            gds,
475            ggs,
476            ggd,
477        };
478
479        // Charge storage (`MODETRAN | MODEINITSMSIG`, or the UIC
480        // transient operating point — no `MODEAC` here, unlike the
481        // JFET; the small-signal pass covers it).
482        if ctx.kind == AnKind::Tran
483            || ctx.mode == InitMode::Smsig
484            || (ctx.kind == AnKind::TranOp && ctx.uic)
485        {
486            let czgs = m.cgs * self.area;
487            let czgd = m.cgd * self.area;
488            let phib = m.pb;
489            let vgs1 = ctx.states.get(1, st + VGS);
490            let vgd1 = ctx.states.get(1, st + VGD);
491            let vcap = 1.0 / m.alpha;
492            let q = |a: f64, c: f64| qggnew(a, c, phib, vcap, m.vto, czgs, czgd);
493            let (qgga, cgsna, cgdna) = q(vgs, vgd);
494            let (qggb, _, _) = q(vgs1, vgd);
495            let (qggc, _, _) = q(vgs, vgd1);
496            let (qggd, _, _) = q(vgs1, vgd1);
497            if ctx.mode == InitMode::Tran {
498                // Both slots take the total charge, as in the C.
499                ctx.states.set(1, st + QGS, qgga);
500                ctx.states.set(1, st + QGD, qgga);
501            }
502            let qgs = ctx.states.get(1, st + QGS) + 0.5 * (qgga - qggb + qggc - qggd);
503            let qgd = ctx.states.get(1, st + QGD) + 0.5 * (qgga - qggc + qggb - qggd);
504            ctx.states.set(0, st + QGS, qgs);
505            ctx.states.set(0, st + QGD, qgd);
506            if fet3_charge(ctx, st, cgsna, cgdna, &mut op) {
507                return;
508            }
509        }
510
511        fet3_finish(ctx, &self.n, st, typ, gdpr, gspr, &bias, &op);
512    }
513
514    /// `MEStrunc`.
515    fn trunc(&self, t: &crate::integ::TerrCtx, time_step: &mut f64) {
516        crate::integ::terr(self.st + QGS, t, time_step);
517        crate::integ::terr(self.st + QGD, t, time_step);
518    }
519
520    /// `MESacLoad`.
521    fn load_ac(&mut self, ctx: &mut AcCtx) {
522        let gdpr = self.t.drain_conduct * self.area;
523        let gspr = self.t.source_conduct * self.area;
524        fet3_ac(ctx, &self.n, self.st, gdpr, gspr);
525    }
526
527    /// `MESpzLoad` (`mespzld.c`): see [`crate::jfet::fet3_pz`].
528    fn load_pz(&mut self, ctx: &mut crate::matrix::PzCtx) -> Result<(), &'static str> {
529        let gdpr = self.t.drain_conduct * self.area;
530        let gspr = self.t.source_conduct * self.area;
531        fet3_pz(ctx, &self.n, self.st, gdpr, gspr);
532        Ok(())
533    }
534
535    /// `MESnoise`: see [`crate::jfet::fet3_noise`].
536    fn noise_suffixes(&self) -> Option<&'static [&'static str]> {
537        Some(crate::jfet::FET3_NOISE_SUFFIXES)
538    }
539
540    fn noise_dens(&self, ctx: &crate::matrix::NoiseCtx) -> Vec<f64> {
541        let gdpr = self.t.drain_conduct * self.area;
542        let gspr = self.t.source_conduct * self.area;
543        crate::jfet::fet3_noise(
544            ctx,
545            &self.n,
546            self.st,
547            gdpr,
548            gspr,
549            self.t.m.kf,
550            self.t.m.af,
551            &crate::jfet::Fet3Channel::Mes,
552        )
553    }
554
555    /// `MESdSetup`: see [`mes_dsetup`].
556    fn disto_setup(&mut self, ctx: &DistoSetupCtx) -> Result<(), &'static str> {
557        let n = &self.n;
558        let typ = self.t.type_;
559        let vgs = typ * (ctx.sol[n.gate] - ctx.sol[n.s_p]);
560        let vgd = typ * (ctx.sol[n.gate] - ctx.sol[n.d_p]);
561        self.dist = mes_dsetup(&self.t, self.area, ctx.gmin, vgs, vgd);
562        Ok(())
563    }
564
565    /// `MESdisto` (`mesdisto.c`).
566    fn disto_load(&self, ctx: &mut DistoCtx) {
567        fet3_disto_load(&self.dist, &self.n, ctx);
568    }
569}
570
571#[cfg(test)]
572mod tests {
573    use super::*;
574
575    #[test]
576    fn temp_matches_mestemp() {
577        let mut m = MesModel::new(true);
578        m.rs = 10.0;
579        let t = MesTemp::new(&m);
580        assert_eq!(t.source_conduct, 0.1);
581        assert_eq!(t.drain_conduct, 0.0);
582        assert!((t.vcrit - VT0 * (VT0 / (ROOT2 * 1e-14)).ln()).abs() < 1e-15);
583        assert_eq!(MesTemp::new(&MesModel::new(false)).type_, -1.0);
584    }
585
586    #[test]
587    fn ids_saturation_is_the_statz_formula() {
588        let (beta, lambda, vto, b, alpha) = (2.5e-3, 0.01, -2.0, 0.3, 2.0);
589        let (vgs, vgd) = (-0.5, -3.5); // vds = 3 > 3/alpha
590        let (id, _, _) = mes_ids(vgs, vgd, vto, beta, lambda, b, alpha);
591        let vgst: f64 = 1.5;
592        let want = beta * (1.0 + lambda * 3.0) * vgst * vgst / (1.0 + b * vgst);
593        assert!((id - want).abs() < 1e-18);
594        // Continuity at vds = 3/alpha.
595        let a = mes_ids(-0.5, -0.5 - 1.5 + 1e-12, vto, beta, lambda, b, alpha).0;
596        let c = mes_ids(-0.5, -0.5 - 1.5 - 1e-12, vto, beta, lambda, b, alpha).0;
597        assert!((a - c).abs() < 1e-12);
598    }
599
600    #[test]
601    fn ids_derivatives_match_finite_differences() {
602        let (beta, lambda, vto, b, alpha) = (2.5e-3, 0.05, -2.0, 0.3, 2.0);
603        let h = 1e-6;
604        for &(vgs, vgd) in &[(-0.5, -1.0), (-0.5, -3.0), (-1.0, -0.5), (-3.0, -0.5)] {
605            let f = |a: f64, c: f64| mes_ids(a, c, vto, beta, lambda, b, alpha).0;
606            let (_, gm, gds) = mes_ids(vgs, vgd, vto, beta, lambda, b, alpha);
607            let vds = vgs - vgd;
608            let dgs = (f(vgs + h, vgs + h - vds) - f(vgs - h, vgs - h - vds)) / (2.0 * h);
609            let dds = (f(vgs, vgs - vds - h) - f(vgs, vgs - vds + h)) / (2.0 * h);
610            assert!(
611                (gm - dgs).abs() < 1e-8 * (1.0 + gm.abs()),
612                "gm at {vgs},{vgd}"
613            );
614            assert!(
615                (gds - dds).abs() < 1e-8 * (1.0 + gds.abs()),
616                "gds at {vgs},{vgd}"
617            );
618        }
619    }
620
621    #[test]
622    fn qggnew_capacitances_are_its_partials() {
623        let (phib, vcap, vto, cgs, cgd) = (0.8, 0.5, -2.0, 2e-12, 0.5e-12);
624        let h = 1e-6;
625        for &(vgs, vgd) in &[(-0.5, -2.0), (0.3, -1.0), (-1.0, -0.2), (-3.0, -3.5)] {
626            let (_, cgsn, cgdn) = qggnew(vgs, vgd, phib, vcap, vto, cgs, cgd);
627            let q = |a: f64, c: f64| qggnew(a, c, phib, vcap, vto, cgs, cgd).0;
628            let dgs = (q(vgs + h, vgd) - q(vgs - h, vgd)) / (2.0 * h);
629            let dgd = (q(vgs, vgd + h) - q(vgs, vgd - h)) / (2.0 * h);
630            assert!((cgsn - dgs).abs() < 1e-6 * cgs, "cgs at {vgs},{vgd}");
631            assert!((cgdn - dgd).abs() < 1e-6 * cgs, "cgd at {vgs},{vgd}");
632        }
633    }
634
635    /// The plain-number form of `mesdset.c`'s charge model: `qgs =
636    /// vgs·cgsnew`, `qgd = vgd·cgdnew` with `cgsnew` built from
637    /// `czgs/(qroot·par1)` (see [`mes_dsetup`]).
638    fn dset_charges(
639        vgs: f64,
640        vgd: f64,
641        phib: f64,
642        vcap: f64,
643        vto: f64,
644        czgs: f64,
645        czgd: f64,
646    ) -> (f64, f64) {
647        let veroot = f64::sqrt((vgs - vgd) * (vgs - vgd) + vcap * vcap);
648        let veff1 = 0.5 * (vgs + vgd + veroot);
649        let vnroot = f64::sqrt((veff1 - vto) * (veff1 - vto) + 0.04);
650        let vnew1 = f64::min(0.5 * (veff1 + vto + vnroot), 0.5);
651        let qroot = f64::sqrt(1.0 - vnew1 / phib);
652        let par1 = 0.5 * (1.0 + (veff1 - vto) / vnroot);
653        let cfact = (vgs - vgd) / veroot;
654        let cplus = 0.5 * (1.0 + cfact);
655        let cminus = cplus - cfact;
656        let dummy = czgs / (qroot * par1);
657        (
658            vgs * (dummy * cplus + czgd * cminus),
659            vgd * (dummy * cminus + czgd * cplus),
660        )
661    }
662
663    /// `MESdSetup`'s coefficients against finite differences: the
664    /// drain current is the load's Statz current (`mes_ids`) in the
665    /// real node voltages (x = vgs, y = vds), both modes, both
666    /// polarities, linear/saturation/cutoff; the gate diodes are the
667    /// load's at `CONSTvt0`; the charges are the setup's own model in
668    /// (vgs, vgd).
669    #[test]
670    fn mes_dsetup_matches_finite_differences() {
671        use crate::jfet::tests::{assert_taylor, df_seven, taylor2_fd_rich};
672        let gmin = 1e-12;
673        let mut worst_cdr = 0.0f64;
674        let mut worst_q = 0.0f64;
675        for nmf in [true, false] {
676            let mut m = MesModel::new(nmf);
677            m.vto = -1.8;
678            m.beta = 3e-3;
679            m.b = 0.3;
680            m.alpha = 2.5;
681            m.lambda = 0.05;
682            m.cgs = 1e-12;
683            m.cgd = 0.2e-12;
684            m.pb = 0.8;
685            m.is = 1e-13;
686            let t = MesTemp::new(&m);
687            let typ = t.type_;
688            let area = 1.3;
689            let beta = m.beta * area;
690            let csat = m.is * area;
691            for &(vgs_n, vds_n) in &[
692                (-0.5, 0.4),  // normal linear (vds < 3/α)
693                (-0.5, 3.0),  // normal saturation
694                (-0.6, -0.5), // inverse linear
695                (-0.3, -2.5), // inverse saturation
696                (0.4, 2.0),   // forward gate, vnew1 clamped
697                (-2.5, 2.0),  // cutoff
698            ] {
699                let (vgs_a, vds_a) = (typ * vgs_n, typ * vds_n);
700                let d = mes_dsetup(&t, area, gmin, vgs_n, vgs_n - vds_n);
701                let id = |x: f64, y: f64| {
702                    typ * mes_ids(typ * x, typ * (x - y), m.vto, beta, m.lambda, m.b, m.alpha).0
703                };
704                let want = taylor2_fd_rich(&id, vgs_a, vds_a, 4e-3);
705                worst_cdr = worst_cdr.max(assert_taylor(
706                    df_seven(&d.cdr),
707                    want,
708                    1e-5,
709                    &format!("cdr nmf={nmf} at ({vgs_n},{vds_n})"),
710                ));
711                let Fet3GateQ::Statz { qgs, qgd } = d.q else {
712                    panic!("MESFET charges are two-variable")
713                };
714                let q = |which: usize| {
715                    move |x: f64, y: f64| {
716                        // x = vgs, y = vgd (real); polarity as the
717                        // other branches.
718                        let (a, b) = dset_charges(
719                            typ * x,
720                            typ * y,
721                            m.pb,
722                            1.0 / m.alpha,
723                            m.vto,
724                            m.cgs * area,
725                            m.cgd * area,
726                        );
727                        typ * if which == 0 { a } else { b }
728                    }
729                };
730                let vgd_a = vgs_a - vds_a;
731                for (k, c) in [qgs, qgd].iter().enumerate() {
732                    let want = taylor2_fd_rich(&q(k), vgs_a, vgd_a, 4e-3);
733                    worst_q = worst_q.max(assert_taylor(
734                        df_seven(c),
735                        want,
736                        1e-5,
737                        &format!("q{k} nmf={nmf} at ({vgs_n},{vds_n})"),
738                    ));
739                }
740                let one = |v: f64, h: f64| {
741                    let f = |v: f64| typ * gate_diode(typ * v, csat, VT0, gmin).1;
742                    let c2 = 0.5 * (f(v + h) - 2.0 * f(v) + f(v - h)) / (h * h);
743                    let c3 = (f(v + 2.0 * h) - 2.0 * f(v + h) + 2.0 * f(v - h) - f(v - 2.0 * h))
744                        / (2.0 * h * h * h)
745                        / 6.0;
746                    (c2, c3)
747                };
748                for (got, v) in [((d.ggs2, d.ggs3), vgs_a), ((d.ggd2, d.ggd3), vgd_a)] {
749                    let want = one(v, 2e-4);
750                    for (a, b) in [(got.0, want.0), (got.1, want.1)] {
751                        let s = a.abs().max(b.abs());
752                        assert!((a - b).abs() <= 2e-3 * s + 1e-15, "gate diode {a} vs {b}");
753                    }
754                }
755            }
756        }
757        eprintln!("MES cdr worst {worst_cdr:.2e}, charges worst {worst_q:.2e}");
758    }
759}