Skip to main content

dune_core/
mos3.rs

1//! MOSFET level 3 — the semi-empirical short-channel model from the
2//! 3f5 (`dev/mos3/mos3load.c`, `mos3temp.c`, `mos3set.c`,
3//! `mos3acld.c`, `mos3conv.c`, `mos3trun.c`). Threshold with the
4//! static-feedback term (`ETA`), the junction-depth short-channel
5//! factor (`XJ`, Dang's trapezoid fit `coeff0/1/2`) and the
6//! narrow-width term (`DELTA`); gate-field mobility degradation
7//! (`THETA`); velocity saturation through `VMAX` with the
8//! `KAPPA`-scaled lateral-field channel-length modulation (or the
9//! plain depletion form without `VMAX`), the punch-through clamp at
10//! half the channel, and the `NFS`/`xn` weak-inversion exponential.
11//! The scaffolding around the current evaluation (bias ladder,
12//! limiting, bulk diodes, depletion and Meyer charges, companion
13//! models, stamps) is MOS1's, which is why this file mirrors
14//! `mos1.rs`/`mos2.rs` there.
15//!
16//! Where ngspice-47 (the oracle) departs from the 3f5 and the golden
17//! shows it, Dune follows ngspice: the alansfixes drain-current
18//! derivatives and smoothed no-`VMAX` channel-length modulation in
19//! `moseq3`, the cubic reverse tail of the bulk diodes
20//! (`bulk_diode`), `ni(TNOM)` in the PHI-from-NSUB preprocessing,
21//! and the `f3/f4` junction coefficients (`junction_coeffs`).
22
23use crate::consts::{BOLTZ, CHARGE, KOVERQ, REFTEMP, ROOT2};
24use crate::dderivs::Dderivs;
25use crate::diode::pnjlim;
26use crate::disto::{DistoCtx, DistoSetupCtx};
27use crate::matrix::{AcCtx, AnKind, InitMode, LoadCtx, MatrixBuilder, Slot, TRASH, Var};
28use crate::mos1::{MeyerPzCaps, MosGeom, MosPzConductances, fetlim, limvds, meyer_pz_load, qmeyer};
29use crate::mosdisto::{
30    DrainDerivs, MosDistoCoeffs, MosJunctions, bulk_diode as disto_bulk_diode, mos_disto_load,
31    setup_tail,
32};
33
34/// Epsilon of silicon (`mos3temp.c`: `EPSSIL`).
35const EPSSIL: f64 = 11.7 * 8.854214871e-12;
36
37/// `.model … nmos(level=3 …)/pmos(level=3 …)` parameters, 3f5 names
38/// and defaults (`mos3.c` keyword table; defaults from `mos3set.c`
39/// and `mos3temp.c`). `Option` marks parameters whose given-ness
40/// changes the preprocessing, as in [`crate::Mos1Model`].
41#[derive(Clone, Debug)]
42pub struct Mos3Model {
43    /// `NMOS` (+1) or `PMOS` (−1).
44    pub nmos: bool,
45    /// `VTO`/`VT0`, default 0, or computed from `NSUB`/`NSS`/`TPG`.
46    pub vto: Option<f64>,
47    /// `KP`, default `U0·COX`.
48    pub kp: Option<f64>,
49    /// `GAMMA`, default 0, or computed from `NSUB`.
50    pub gamma: Option<f64>,
51    /// `PHI`, default 0.6, or from `NSUB`.
52    pub phi: Option<f64>,
53    /// `RD`/`RS`, default 0.
54    pub rd: Option<f64>,
55    pub rs: Option<f64>,
56    /// `CBD`/`CBS`, default 0.
57    pub cbd: Option<f64>,
58    pub cbs: Option<f64>,
59    /// `IS`, default 1e-14.
60    pub is: f64,
61    /// `PB`, default 0.8.
62    pub pb: f64,
63    /// `CGSO`/`CGDO`/`CGBO`, default 0.
64    pub cgso: f64,
65    pub cgdo: f64,
66    pub cgbo: f64,
67    /// `RSH`, default 0.
68    pub rsh: Option<f64>,
69    /// `CJ`, per m², default 0.
70    pub cj: Option<f64>,
71    /// `MJ`, default 0.5.
72    pub mj: f64,
73    /// `CJSW`, per meter, default 0.
74    pub cjsw: Option<f64>,
75    /// `MJSW`, default **0.33** (as MOS2).
76    pub mjsw: f64,
77    /// `JS`, per m², default 0.
78    pub js: f64,
79    /// `TOX`, default 1e-7 m (MOS3 always has an oxide).
80    pub tox: Option<f64>,
81    /// `LD`, default 0.
82    pub ld: f64,
83    /// `U0`/`UO`, cm²/V·s, default 600.
84    pub u0: Option<f64>,
85    /// `FC`, default 0.5.
86    pub fc: f64,
87    /// `NSUB`, 1/cm³.
88    pub nsub: Option<f64>,
89    /// `TPG`, default 1 when `NSUB` is given.
90    pub tpg: Option<f64>,
91    /// `NSS`, 1/cm², default 0.
92    pub nss: Option<f64>,
93    /// `VMAX`: maximum carrier drift velocity, m/s, default 0 (off).
94    pub vmax: f64,
95    /// `XJ`: metallurgical junction depth, default 0.
96    pub xj: f64,
97    /// `NFS`: fast surface state density, 1/cm², default 0.
98    pub nfs: f64,
99    /// `ETA`: static feedback (Vds dependence of threshold),
100    /// default 0.
101    pub eta: f64,
102    /// `DELTA`: width effect on threshold, default 0.
103    pub delta: f64,
104    /// `THETA`: mobility modulation by the gate field, 1/V,
105    /// default 0.
106    pub theta: f64,
107    /// `KAPPA`: saturation field factor, default 0.2.
108    pub kappa: f64,
109    /// `KF`/`AF` (noise only).
110    pub kf: f64,
111    pub af: f64,
112    /// `NLEV` (ngspice-47 only, default 2): noise equation selector
113    /// — `< 3` channel thermal `4kT·(2/3)|gm|`, `3` the
114    /// `GDSNOI`-scaled form; for the MOSFETs also the flicker form
115    /// (0/1/2–3).
116    pub nlev: i64,
117    /// `GDSNOI` (ngspice-47 only, default 1): channel noise
118    /// coefficient of `NLEV = 3`.
119    pub gdsnoi: f64,
120    /// `TNOM` in kelvin.
121    pub tnom: Option<f64>,
122}
123
124impl Mos3Model {
125    pub fn new(nmos: bool) -> Self {
126        Mos3Model {
127            nmos,
128            vto: None,
129            kp: None,
130            gamma: None,
131            phi: None,
132            rd: None,
133            rs: None,
134            cbd: None,
135            cbs: None,
136            is: 1e-14,
137            pb: 0.8,
138            cgso: 0.0,
139            cgdo: 0.0,
140            cgbo: 0.0,
141            rsh: None,
142            cj: None,
143            mj: 0.5,
144            cjsw: None,
145            mjsw: 0.33,
146            js: 0.0,
147            tox: None,
148            ld: 0.0,
149            u0: None,
150            fc: 0.5,
151            nsub: None,
152            tpg: None,
153            nss: None,
154            vmax: 0.0,
155            xj: 0.0,
156            nfs: 0.0,
157            eta: 0.0,
158            delta: 0.0,
159            theta: 0.0,
160            kappa: 0.2,
161            kf: 0.0,
162            af: 1.0,
163            nlev: 2,
164            gdsnoi: 1.0,
165            tnom: None,
166        }
167    }
168
169    /// Same prime-node rule as MOS1 (`mos3set.c`).
170    pub(crate) fn needs_prime(&self, squares: f64) -> bool {
171        self.rd.unwrap_or(0.0) != 0.0 || (self.rsh.unwrap_or(0.0) != 0.0 && squares != 0.0)
172    }
173}
174
175fn egfet(t: f64) -> f64 {
176    1.16 - (7.02e-4 * t * t) / (t + 1108.0)
177}
178
179/// Model- and temperature-derived values (`MOS3temp`).
180#[derive(Debug)]
181pub(crate) struct Mos3Temp {
182    /// The instance temperature, kelvin (`*temp`: the card's
183    /// `TEMP=` or `CKTtemp`) — the thermal noise generators' T
184    /// (ngspice-47 `NevalSrcInstanceTemp`).
185    pub temp: f64,
186    pub type_: f64,
187    pub vt: f64,
188    // Model copies the load needs.
189    pub lat_diff: f64,
190    /// `KF`/`AF`: flicker-noise coefficient and exponent.
191    pub kf: f64,
192    pub af: f64,
193    /// `NLEV`/`GDSNOI` (noise only).
194    pub nlev: i64,
195    pub gdsnoi: f64,
196    pub cgso: f64,
197    pub cgdo: f64,
198    pub cgbo: f64,
199    pub oxide_cap_factor: f64,
200    pub gamma: f64,
201    pub mj: f64,
202    pub mjsw: f64,
203    pub xj: f64,
204    pub nfs: f64,
205    pub vmax: f64,
206    pub eta: f64,
207    pub theta: f64,
208    pub kappa: f64,
209    /// `MOS3alpha`: `2·EPSSIL/(q·NSUB)` (0 without `NSUB`).
210    pub alpha: f64,
211    /// `MOS3coeffDepLayWidth`: `sqrt(alpha)`.
212    pub coeff_dep_lay_width: f64,
213    /// `MOS3narrowFactor`: `DELTA·π/2·EPSSIL/COX`.
214    pub narrow_factor: f64,
215    // Temperature-adjusted values.
216    pub t_transconductance: f64,
217    /// `MOS3tSurfMob`: `U0/ratio^1.5`, cm²/V·s.
218    pub t_surf_mob: f64,
219    pub t_phi: f64,
220    pub t_vbi: f64,
221    pub t_vto: f64,
222    pub t_sat_cur: f64,
223    pub t_sat_cur_dens: f64,
224    pub t_bulk_pot: f64,
225    pub t_dep_cap: f64,
226    pub drain_vcrit: f64,
227    pub source_vcrit: f64,
228    pub cbd: f64,
229    pub cbdsw: f64,
230    pub cbs: f64,
231    pub cbssw: f64,
232    pub f2d: f64,
233    pub f3d: f64,
234    pub f4d: f64,
235    pub f2s: f64,
236    pub f3s: f64,
237    pub f4s: f64,
238    pub drain_conductance: f64,
239    pub source_conductance: f64,
240}
241
242impl Mos3Temp {
243    pub fn new(
244        name: &str,
245        m: &Mos3Model,
246        g: &MosGeom,
247        temp: f64,
248        default_tnom: f64,
249    ) -> Result<Self, String> {
250        let typ = if m.nmos { 1.0 } else { -1.0 };
251        let tnom = m.tnom.unwrap_or(default_tnom);
252
253        let fact1 = tnom / REFTEMP;
254        let vtnom = tnom * KOVERQ;
255        let kt1 = BOLTZ * tnom;
256        let egfet1 = egfet(tnom);
257        let arg1 = -egfet1 / (kt1 + kt1) + 1.1150877 / (BOLTZ * (REFTEMP + REFTEMP));
258        let pbfact1 = -2.0 * vtnom * (1.5 * f64::ln(fact1) + CHARGE * arg1);
259        // ngspice-47 (MOS3 only): intrinsic density at TNOM instead of
260        // the 3f5's fixed 1.45e16 m⁻³, both in the `Nsub < Ni` check
261        // and in PHI from NSUB (≈0.6 mV at 27 °C, visible in weak
262        // inversion; `corpus/KNOWN_DIFFS.md`).
263        let mut nifact = (tnom / 300.0) * (tnom / 300.0).sqrt();
264        nifact *= f64::exp(0.5 * egfet1 * ((1.0 / 300.0) - (1.0 / tnom)) / KOVERQ);
265        let ni_temp = 1.45e16 * nifact;
266
267        // Model preprocessing (TOX defaults to 1e-7 in `mos3set.c`,
268        // so the oxide always exists).
269        let tox = m.tox.unwrap_or(1e-7);
270        let oxide_cap_factor = 3.9 * 8.854214871e-12 / tox;
271        let u0 = m.u0.unwrap_or(600.0);
272        let kp = m.kp.unwrap_or(u0 * oxide_cap_factor * 1e-4 /* m²/cm² */);
273        let mut phi = m.phi;
274        let mut gamma = m.gamma;
275        let mut vto = m.vto;
276        let mut alpha = 0.0;
277        let mut coeff_dep_lay_width = 0.0;
278        if let Some(nsub) = m.nsub {
279            if nsub * 1e6 /* cm³/m³ */ > ni_temp {
280                if phi.is_none() {
281                    phi = Some(f64::max(0.1, 2.0 * vtnom * f64::ln(nsub * 1e6 / ni_temp)));
282                }
283                let phi_v = phi.unwrap_or(0.6);
284                let fermis = typ * 0.5 * phi_v;
285                let tpg = m.tpg.unwrap_or(1.0);
286                let mut wkfng = 3.2;
287                if tpg != 0.0 {
288                    let fermig = typ * tpg * 0.5 * egfet1;
289                    wkfng = 3.25 + 0.5 * egfet1 - fermig;
290                }
291                let wkfngs = wkfng - (3.25 + 0.5 * egfet1 + fermis);
292                if gamma.is_none() {
293                    gamma = Some(f64::sqrt(2.0 * EPSSIL * CHARGE * nsub * 1e6) / oxide_cap_factor);
294                }
295                if vto.is_none() {
296                    let nss = m.nss.unwrap_or(0.0);
297                    let vfb = wkfngs - nss * 1e4 /* cm²/m² */ * CHARGE / oxide_cap_factor;
298                    vto = Some(vfb + typ * (gamma.unwrap_or(0.0) * phi_v.sqrt() + phi_v));
299                }
300                alpha = (EPSSIL + EPSSIL) / (CHARGE * nsub * 1e6);
301                coeff_dep_lay_width = alpha.sqrt();
302            } else {
303                return Err(format!("{name}: Nsub < Ni"));
304            }
305        }
306        let phi = phi.unwrap_or(0.6);
307        // ngspice-47 rejects a non-positive PHI up front (the 3f5
308        // would take sqrt of it in the load).
309        if phi <= 0.0 {
310            return Err(format!("{name}: Phi is not positive."));
311        }
312        let gamma = gamma.unwrap_or(0.0);
313        let vto = vto.unwrap_or(0.0);
314        let narrow_factor = m.delta * 0.5 * std::f64::consts::PI * EPSSIL / oxide_cap_factor;
315
316        // Per-instance temperature block.
317        let vt = temp * KOVERQ;
318        let ratio = temp / tnom;
319        let fact2 = temp / REFTEMP;
320        let kt = temp * BOLTZ;
321        let egfet_t = egfet(temp);
322        let arg = -egfet_t / (kt + kt) + 1.1150877 / (BOLTZ * (REFTEMP + REFTEMP));
323        let pbfact = -2.0 * vt * (1.5 * f64::ln(fact2) + CHARGE * arg);
324
325        // MOS3 makes this fatal (E_BADPARM), unlike MOS1/MOS2's note.
326        if g.l - 2.0 * m.ld <= 0.0 {
327            return Err(format!("{name}: effective channel length less than zero"));
328        }
329        let ratio4 = ratio * ratio.sqrt();
330        let t_transconductance = kp / ratio4;
331        let t_surf_mob = u0 / ratio4;
332        let phio = (phi - pbfact1) / fact1;
333        let t_phi = fact2 * phio + pbfact;
334        let t_vbi =
335            vto - typ * (gamma * phi.sqrt()) + 0.5 * (egfet1 - egfet_t) + typ * 0.5 * (t_phi - phi);
336        let t_vto = t_vbi + typ * gamma * t_phi.sqrt();
337        let t_sat_cur = m.is * f64::exp(-egfet_t / vt + egfet1 / vtnom);
338        let t_sat_cur_dens = m.js * f64::exp(-egfet_t / vt + egfet1 / vtnom);
339
340        let pbo = (m.pb - pbfact1) / fact1;
341        let gmaold = (m.pb - pbo) / pbo;
342        let mut capfact = 1.0 / (1.0 + m.mj * (4e-4 * (tnom - REFTEMP) - gmaold));
343        let mut t_cbd = m.cbd.unwrap_or(0.0) * capfact;
344        let mut t_cbs = m.cbs.unwrap_or(0.0) * capfact;
345        let mut t_cj = m.cj.unwrap_or(0.0) * capfact;
346        capfact = 1.0 / (1.0 + m.mjsw * (4e-4 * (tnom - REFTEMP) - gmaold));
347        let mut t_cjsw = m.cjsw.unwrap_or(0.0) * capfact;
348        let t_bulk_pot = fact2 * pbo + pbfact;
349        let gmanew = (t_bulk_pot - pbo) / pbo;
350        capfact = 1.0 + m.mj * (4e-4 * (temp - REFTEMP) - gmanew);
351        t_cbd *= capfact;
352        t_cbs *= capfact;
353        t_cj *= capfact;
354        capfact = 1.0 + m.mjsw * (4e-4 * (temp - REFTEMP) - gmanew);
355        t_cjsw *= capfact;
356        let t_dep_cap = m.fc * t_bulk_pot;
357
358        // The 3f5 keys vcrit off the model IS/JS; ngspice-47 (and
359        // Dune's MOS1/MOS2) off the temperature-adjusted ones. Only
360        // the limiting sees it.
361        let (drain_vcrit, source_vcrit) = if m.js == 0.0 || g.ad == 0.0 || g.as_ == 0.0 {
362            let v = vt * f64::ln(vt / (ROOT2 * t_sat_cur));
363            (v, v)
364        } else {
365            (
366                vt * f64::ln(vt / (ROOT2 * t_sat_cur_dens * g.ad)),
367                vt * f64::ln(vt / (ROOT2 * t_sat_cur_dens * g.as_)),
368            )
369        };
370
371        let czbd = if m.cbd.is_some() {
372            t_cbd
373        } else if m.cj.is_some() {
374            t_cj * g.ad
375        } else {
376            0.0
377        };
378        let czbdsw = if m.cjsw.is_some() { t_cjsw * g.pd } else { 0.0 };
379        let czbs = if m.cbs.is_some() {
380            t_cbs
381        } else if m.cj.is_some() {
382            t_cj * g.as_
383        } else {
384            0.0
385        };
386        let czbssw = if m.cjsw.is_some() { t_cjsw * g.ps } else { 0.0 };
387        let arg = 1.0 - m.fc;
388        let sarg = f64::exp(-m.mj * f64::ln(arg));
389        let sargsw = f64::exp(-m.mjsw * f64::ln(arg));
390        let (f2d, f3d, f4d) =
391            junction_coeffs(m, czbd, czbdsw, arg, sarg, sargsw, t_bulk_pot, t_dep_cap);
392        let (f2s, f3s, f4s) =
393            junction_coeffs(m, czbs, czbssw, arg, sarg, sargsw, t_bulk_pot, t_dep_cap);
394
395        let series = |r: Option<f64>, squares: f64| match (r, m.rsh) {
396            (Some(r), _) => {
397                if r != 0.0 {
398                    1.0 / r
399                } else {
400                    0.0
401                }
402            }
403            (None, Some(rsh)) => {
404                if rsh != 0.0 {
405                    1.0 / (rsh * squares)
406                } else {
407                    0.0
408                }
409            }
410            (None, None) => 0.0,
411        };
412
413        Ok(Mos3Temp {
414            temp,
415            type_: typ,
416            vt,
417            lat_diff: m.ld,
418            kf: m.kf,
419            af: m.af,
420            nlev: m.nlev,
421            gdsnoi: m.gdsnoi,
422            cgso: m.cgso,
423            cgdo: m.cgdo,
424            cgbo: m.cgbo,
425            oxide_cap_factor,
426            gamma,
427            mj: m.mj,
428            mjsw: m.mjsw,
429            xj: m.xj,
430            nfs: m.nfs,
431            vmax: m.vmax,
432            eta: m.eta,
433            theta: m.theta,
434            kappa: m.kappa,
435            alpha,
436            coeff_dep_lay_width,
437            narrow_factor,
438            t_transconductance,
439            t_surf_mob,
440            t_phi,
441            t_vbi,
442            t_vto,
443            t_sat_cur,
444            t_sat_cur_dens,
445            t_bulk_pot,
446            t_dep_cap,
447            drain_vcrit,
448            source_vcrit,
449            cbd: czbd,
450            cbdsw: czbdsw,
451            cbs: czbs,
452            cbssw: czbssw,
453            f2d,
454            f3d,
455            f4d,
456            f2s,
457            f3s,
458            f4s,
459            drain_conductance: series(m.rd, g.nrd),
460            source_conductance: series(m.rs, g.nrs),
461        })
462    }
463}
464
465/// Forward-bias continuation coefficients `f2/f3/f4` of one bulk
466/// junction, in ngspice-47's `mos3temp.c` form: the 3f5 divides
467/// `f3` by the model `PB` instead of the temperature-adjusted one,
468/// writes `f4` with the model `PB`, and (source side only) puts
469/// `tBulkPot` where `tDepCap` belongs, so its `qbs` jumps at
470/// `vbs = FC·PB`. ngspice uses the MOS1/MOS2 forms throughout.
471#[allow(clippy::too_many_arguments)]
472fn junction_coeffs(
473    m: &Mos3Model,
474    cz: f64,
475    czsw: f64,
476    arg: f64,
477    sarg: f64,
478    sargsw: f64,
479    t_bulk_pot: f64,
480    t_dep_cap: f64,
481) -> (f64, f64, f64) {
482    let f2 = cz * (1.0 - m.fc * (1.0 + m.mj)) * sarg / arg
483        + czsw * (1.0 - m.fc * (1.0 + m.mjsw)) * sargsw / arg;
484    let f3 = cz * m.mj * sarg / arg / t_bulk_pot + czsw * m.mjsw * sargsw / arg / t_bulk_pot;
485    let f4 = cz * t_bulk_pot * (1.0 - arg * sarg) / (1.0 - m.mj)
486        + czsw * t_bulk_pot * (1.0 - arg * sargsw) / (1.0 - m.mjsw)
487        - f3 / 2.0 * (t_dep_cap * t_dep_cap)
488        - t_dep_cap * f2;
489    (f2, f3, f4)
490}
491
492// State offsets (`mos3defs.h` — identical to MOS1's).
493const VBD: usize = 0;
494const VBS: usize = 1;
495const VGS: usize = 2;
496const VDS: usize = 3;
497const CAPGS: usize = 4;
498const QGS: usize = 5;
499const CQGS: usize = 6;
500const CAPGD: usize = 7;
501const QGD: usize = 8;
502const CQGD: usize = 9;
503const CAPGB: usize = 10;
504const QGB: usize = 11;
505const CQGB: usize = 12;
506const QBD: usize = 13;
507const CQBD: usize = 14;
508const QBS: usize = 15;
509const CQBS: usize = 16;
510pub(crate) const MOS3_NUM_STATES: usize = 17;
511
512// Slot order (matches `mos3set.c`'s TSTALLOC list — same as MOS1).
513const D_D: usize = 0;
514const G_G: usize = 1;
515const S_S: usize = 2;
516const B_B: usize = 3;
517const DP_DP: usize = 4;
518const SP_SP: usize = 5;
519const D_DP: usize = 6;
520const G_B: usize = 7;
521const G_DP: usize = 8;
522const G_SP: usize = 9;
523const S_SP: usize = 10;
524const B_DP: usize = 11;
525const B_SP: usize = 12;
526const DP_SP: usize = 13;
527const DP_D: usize = 14;
528const B_G: usize = 15;
529const DP_G: usize = 16;
530const SP_G: usize = 17;
531const SP_S: usize = 18;
532const DP_B: usize = 19;
533const SP_B: usize = 20;
534const SP_DP: usize = 21;
535
536/// One bulk junction in ngspice-47's `mos3load.c` form: returns
537/// `(g, i)` with gmin inside both.
538fn bulk_diode(v: f64, vt: f64, sat_cur: f64, gmin: f64) -> (f64, f64) {
539    const MAX_EXP_ARG: f64 = 709.0;
540    if v <= -3.0 * vt {
541        let arg = 3.0 * vt / (v * crate::consts::E);
542        let arg = arg * arg * arg;
543        (
544            sat_cur * 3.0 * arg / v + gmin,
545            -sat_cur * (1.0 + arg) + gmin * v,
546        )
547    } else {
548        let ev = f64::exp(f64::min(MAX_EXP_ARG, v / vt));
549        (sat_cur * ev / vt + gmin, sat_cur * (ev - 1.0) + gmin * v)
550    }
551}
552
553/// Dang's fit of the junction-sidewall depletion width
554/// (`mos3load.c`: `coeff0`/`coeff1`/`coeff2`).
555const COEFF0: f64 = 0.0631353;
556const COEFF1: f64 = 0.8013292;
557const COEFF2: f64 = -0.01110777;
558
559/// The drain current of [`Mos3I::eval`] and its derivatives, in the
560/// mode-normalized space.
561#[derive(Clone, Copy, Debug, Default, PartialEq)]
562pub(crate) struct Mos3Eval {
563    pub cdrain: f64,
564    pub gm: f64,
565    pub gds: f64,
566    pub gmbs: f64,
567    pub von: f64,
568    pub vdsat: f64,
569}
570
571pub(crate) struct Mos3I {
572    drain: Var,
573    gate: Var,
574    source: Var,
575    bulk: Var,
576    d_p: Var,
577    s_p: Var,
578    t: Mos3Temp,
579    geom: MosGeom,
580    off: bool,
581    ic_vds: f64,
582    ic_vgs: f64,
583    ic_vbs: f64,
584    ic_given: crate::devices::IcGiven,
585    st: usize,
586    s: [Slot; 22],
587    // Values that persist between iterations (`MOS3instance`).
588    mode: f64,
589    von: f64,
590    vdsat: f64,
591    gm: f64,
592    gds: f64,
593    gmbs: f64,
594    gbd: f64,
595    gbs: f64,
596    capbd: f64,
597    capbs: f64,
598    cd: f64,
599    cbs: f64,
600    cbd: f64,
601    /// `.disto` Taylor coefficients (`MOS3dSetup`).
602    dist: MosDistoCoeffs,
603}
604
605impl Mos3I {
606    #[allow(clippy::too_many_arguments)]
607    pub fn new(
608        nodes: [Var; 4],  // d g s b
609        primes: [Var; 2], // d' s'
610        t: Mos3Temp,
611        geom: MosGeom,
612        off: bool,
613        ic: (f64, f64, f64), // (vds, vgs, vbs)
614        st: usize,
615    ) -> Self {
616        Mos3I {
617            drain: nodes[0],
618            gate: nodes[1],
619            source: nodes[2],
620            bulk: nodes[3],
621            d_p: primes[0],
622            s_p: primes[1],
623            t,
624            geom,
625            off,
626            ic_vds: ic.0,
627            ic_vgs: ic.1,
628            ic_vbs: ic.2,
629            ic_given: crate::devices::IcGiven::ALL,
630            st,
631            s: [TRASH; 22],
632            mode: 1.0,
633            von: 0.0,
634            vdsat: 0.0,
635            gm: 0.0,
636            gds: 0.0,
637            gmbs: 0.0,
638            gbd: 0.0,
639            gbs: 0.0,
640            capbd: 0.0,
641            capbs: 0.0,
642            cd: 0.0,
643            cbs: 0.0,
644            cbd: 0.0,
645            dist: MosDistoCoeffs::default(),
646        }
647    }
648}
649
650/// The semi-empirical current evaluation (the `moseq3` block of
651/// `mos3load.c`), ported literally with the C's intermediate names —
652/// in ngspice-47's form from the normalized drain current on, since
653/// the oracle shows it (`corpus/KNOWN_DIFFS.md`): the alansfixes
654/// corrected the 3f5's saturation derivatives (`gds` from the
655/// linear-region formula, `dfddvd`'s `−fd2·onvdsc`, the missing
656/// `dfgdvb·cd1` in `gmbs`, the CLM bookkeeping) and, without
657/// `VMAX`, smoothed the channel-length modulation through vdsat
658/// (`vds−vdsat+vdsat/8` above, `sqrt(KAPPA·alpha·vdsat/8)·r⁴`
659/// below), which changes the drain current itself by up to a few
660/// percent around vdsat.
661/// `lvbs`/`lvgs`/`lvds` are the mode-normalized biases
662/// (`mode==1 ? vbs : vbd`, …, `mode·vds`); `beta` is
663/// `tTransconductance·W/Leff` and `oxide_cap` `COX·Leff·W`. Every
664/// path writes all of `gm`/`gds`/`gmbs`, so (unlike MOS2) nothing
665/// is carried over from the previous iteration.
666#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
667pub(crate) fn moseq3(
668    t: &Mos3Temp,
669    w: f64,
670    lvbs: f64,
671    lvgs: f64,
672    lvds: f64,
673    beta: f64,
674    oxide_cap: f64,
675    eff_l: f64,
676    vt: f64,
677) -> Mos3Eval {
678    let typ = t.type_;
679    let mut beta = beta;
680    let mut vdsat = 0.0;
681    let oneoverxl = 1.0 / eff_l;
682    let eta = t.eta * 8.15e-22 / (t.oxide_cap_factor * eff_l * eff_l * eff_l);
683
684    // Square root term.
685    let (phibs, sqphbs, dsqdvb);
686    if lvbs <= 0.0 {
687        phibs = t.t_phi - lvbs;
688        sqphbs = phibs.sqrt();
689        dsqdvb = -0.5 / sqphbs;
690    } else {
691        let sqphis = t.t_phi.sqrt();
692        let sqphs3 = t.t_phi * sqphis;
693        sqphbs = sqphis / (1.0 + lvbs / (t.t_phi + t.t_phi));
694        phibs = sqphbs * sqphbs;
695        dsqdvb = -phibs / (sqphs3 + sqphs3);
696    }
697
698    // Short channel effect factor.
699    let (fshort, dfsdvb);
700    if t.xj != 0.0 && t.coeff_dep_lay_width != 0.0 {
701        let wps = t.coeff_dep_lay_width * sqphbs;
702        let oneoverxj = 1.0 / t.xj;
703        let xjonxl = t.xj * oneoverxl;
704        let djonxj = t.lat_diff * oneoverxj;
705        let wponxj = wps * oneoverxj;
706        let wconxj = COEFF0 + COEFF1 * wponxj + COEFF2 * wponxj * wponxj;
707        let arga = wconxj + djonxj;
708        let argc = wponxj / (1.0 + wponxj);
709        let argb = (1.0 - argc * argc).sqrt();
710        fshort = 1.0 - xjonxl * (arga * argb - djonxj);
711        let dwpdvb = t.coeff_dep_lay_width * dsqdvb;
712        let dadvb = (COEFF1 + COEFF2 * (wponxj + wponxj)) * dwpdvb * oneoverxj;
713        let dbdvb = -argc * argc * (1.0 - argc) * dwpdvb / (argb * wps);
714        dfsdvb = -xjonxl * (dadvb * argb + arga * dbdvb);
715    } else {
716        fshort = 1.0;
717        dfsdvb = 0.0;
718    }
719
720    // Body effect.
721    let gammas = t.gamma * fshort;
722    let fbodys = 0.5 * gammas / (sqphbs + sqphbs);
723    let fbody = fbodys + t.narrow_factor / w;
724    let onfbdy = 1.0 / (1.0 + fbody);
725    let dfbdvb = -fbodys * dsqdvb / sqphbs + fbodys * dfsdvb / fshort;
726    let qbonco = gammas * sqphbs + t.narrow_factor * phibs / w;
727    let dqbdvb = gammas * dsqdvb + t.gamma * dfsdvb * sqphbs - t.narrow_factor / w;
728
729    // Static feedback effect.
730    let vbix = t.t_vbi * typ - eta * lvds;
731
732    // Threshold voltage.
733    let vth = vbix + qbonco;
734    let dvtdvd = -eta;
735    let dvtdvb = dqbdvb;
736
737    // Joint weak inversion and strong inversion.
738    let mut von = vth;
739    let (mut xn, mut dxndvb, mut dvodvd, mut dvodvb) = (0.0, 0.0, 0.0, 0.0);
740    if t.nfs != 0.0 {
741        let csonco = CHARGE * t.nfs * 1e4 /* cm²/m² */ * eff_l * w / oxide_cap;
742        let cdonco = qbonco / (phibs + phibs);
743        xn = 1.0 + csonco + cdonco;
744        von = vth + vt * xn;
745        dxndvb = dqbdvb / (phibs + phibs) - qbonco * dsqdvb / (phibs * sqphbs);
746        dvodvd = dvtdvd;
747        dvodvb = dvtdvb + vt * dxndvb;
748    } else if lvgs <= von {
749        // Cutoff region.
750        return Mos3Eval {
751            cdrain: 0.0,
752            gm: 0.0,
753            gds: 0.0,
754            gmbs: 0.0,
755            von,
756            vdsat,
757        };
758    }
759
760    // Device is on.
761    let vgsx = f64::max(lvgs, von);
762
763    // Mobility modulation by gate voltage.
764    let onfg = 1.0 + t.theta * (vgsx - vth);
765    let fgate = 1.0 / onfg;
766    let us = t.t_surf_mob * 1e-4 /* m²/cm² */ * fgate;
767    let dfgdvg = -t.theta * fgate * fgate;
768    let dfgdvd = -dfgdvg * dvtdvd;
769    let dfgdvb = -dfgdvg * dvtdvb;
770
771    // Saturation voltage.
772    vdsat = (vgsx - vth) * onfbdy;
773    let (dvsdvg, dvsdvd, dvsdvb);
774    let mut onvdsc = 0.0;
775    if t.vmax <= 0.0 {
776        dvsdvg = onfbdy;
777        dvsdvd = -dvsdvg * dvtdvd;
778        dvsdvb = -dvsdvg * dvtdvb - vdsat * dfbdvb * onfbdy;
779    } else {
780        let vdsc = eff_l * t.vmax / us;
781        onvdsc = 1.0 / vdsc;
782        let arga = (vgsx - vth) * onfbdy;
783        let argb = (arga * arga + vdsc * vdsc).sqrt();
784        vdsat = arga + vdsc - argb;
785        let dvsdga = (1.0 - arga / argb) * onfbdy;
786        dvsdvg = dvsdga - (1.0 - vdsc / argb) * vdsc * dfgdvg * onfg;
787        dvsdvd = -dvsdvg * dvtdvd;
788        dvsdvb = -dvsdvg * dvtdvb - arga * dvsdga * dfbdvb;
789    }
790
791    // Current factors in linear region.
792    let vdsx = f64::min(lvds, vdsat);
793    if vdsx == 0.0 {
794        // line900: special case of vds = 0.
795        beta *= fgate;
796        let mut gds = beta * (vgsx - vth);
797        if t.nfs != 0.0 && lvgs < von {
798            gds *= f64::exp((lvgs - von) / (vt * xn));
799        }
800        return Mos3Eval {
801            cdrain: 0.0,
802            gm: 0.0,
803            gds,
804            gmbs: 0.0,
805            von,
806            vdsat,
807        };
808    }
809    let cdo = vgsx - vth - 0.5 * (1.0 + fbody) * vdsx;
810    let dcodvb = -dvtdvb - 0.5 * dfbdvb * vdsx;
811
812    // Normalized drain current. From here on Dune follows ngspice-47
813    // (alansfixes), not the 3f5 — see the function docs.
814    let saturated = lvds > vdsat;
815    let cdnorm = cdo * vdsx;
816    let mut gm = vdsx;
817    // In saturation vdsx = vdsat, so the only direct vds dependence
818    // left is the static feedback through vth (the 3f5 used the
819    // linear-region form here too, a derivative bug).
820    let mut gds = if saturated {
821        -dvtdvd * vdsx
822    } else {
823        vgsx - vth - (1.0 + fbody + dvtdvd) * vdsx
824    };
825    let mut gmbs = dcodvb * vdsx;
826
827    // Drain current without velocity saturation effect.
828    let cd1 = beta * cdnorm;
829    beta *= fgate;
830    let mut cdrain = beta * cdnorm;
831    gm = beta * gm + dfgdvg * cd1;
832    gds = beta * gds + dfgdvd * cd1;
833    // (the 3f5 dropped the `dfgdvb·cd1` term)
834    gmbs = beta * gmbs + dfgdvb * cd1;
835
836    // Velocity saturation factor.
837    let (mut fdrain, mut dfddvg, mut dfddvd, mut dfddvb) = (0.0, 0.0, 0.0, 0.0);
838    if t.vmax > 0.0 {
839        fdrain = 1.0 / (1.0 + vdsx * onvdsc);
840        let fd2 = fdrain * fdrain;
841        let arga = fd2 * vdsx * onvdsc * onfg;
842        dfddvg = -dfgdvg * arga;
843        dfddvd = if saturated {
844            -dfgdvd * arga
845        } else {
846            -dfgdvd * arga - fd2 * onvdsc
847        };
848        dfddvb = -dfgdvb * arga;
849        // Drain current.
850        gm = fdrain * gm + dfddvg * cdrain;
851        gds = fdrain * gds + dfddvd * cdrain;
852        gmbs = fdrain * gmbs + dfddvb * cdrain;
853        cdrain *= fdrain;
854        #[allow(unused_assignments)] // kept from the C
855        {
856            beta *= fdrain;
857        }
858    }
859
860    // Channel length modulation. `gds0` stays 0 on the paths that
861    // skip it (the weak-inversion block reads it in saturation).
862    let mut gds0 = 0.0;
863    'line700: {
864        let (mut delxl, mut dldvd, mut ddldvg, mut ddldvd, mut ddldvb);
865        if !saturated {
866            // ngspice: without VMAX the channel already shortens
867            // below vdsat, `delxl = sqrt(KAPPA·alpha·vdsat/8)·r⁴`
868            // with r = vds/vdsat, which joins the smoothed line510
869            // form with a continuous slope at vds = vdsat.
870            if t.vmax > 0.0 || t.alpha == 0.0 {
871                break 'line700;
872            }
873            let mut arga = lvds / vdsat;
874            delxl = (t.kappa * t.alpha * vdsat / 8.0).sqrt();
875            dldvd = 4.0 * delxl * arga * arga * arga / vdsat;
876            arga *= arga;
877            arga *= arga;
878            delxl *= arga;
879            ddldvg = 0.0;
880            ddldvd = -dldvd;
881            ddldvb = 0.0;
882        } else if t.vmax <= 0.0 {
883            // line510, ngspice's smoothed form: `vds−vdsat` becomes
884            // `vds−vdsat+vdsat/8` (`.options badmos3` would restore
885            // the 3f5's; accepted and ignored by Dune's options).
886            let arg = lvds - vdsat + vdsat / 8.0;
887            delxl = (t.kappa * t.alpha * arg).sqrt();
888            dldvd = 0.5 * delxl / arg;
889            ddldvg = 0.0;
890            ddldvd = -dldvd;
891            ddldvb = 0.0;
892        } else {
893            if t.alpha == 0.0 {
894                break 'line700;
895            }
896            let cdsat = cdrain;
897            let mut gdsat = cdsat * (1.0 - fdrain) * onvdsc;
898            gdsat = f64::max(1.0e-12, gdsat);
899            let gdoncd = gdsat / cdsat;
900            let gdonfd = gdsat / (1.0 - fdrain);
901            let gdonfg = gdsat * onfg;
902            let dgdvg = gdoncd * gm - gdonfd * dfddvg + gdonfg * dfgdvg;
903            let dgdvd = gdoncd * gds - gdonfd * dfddvd + gdonfg * dfgdvd;
904            let dgdvb = gdoncd * gmbs - gdonfd * dfddvb + gdonfg * dfgdvb;
905
906            let emax = t.kappa * cdsat * oneoverxl / gdsat;
907            let emoncd = emax / cdsat;
908            let emongd = emax / gdsat;
909            let demdvg = emoncd * gm - emongd * dgdvg;
910            let demdvd = emoncd * gds - emongd * dgdvd;
911            let demdvb = emoncd * gmbs - emongd * dgdvb;
912
913            let arga = 0.5 * emax * t.alpha;
914            let argc = t.kappa * t.alpha;
915            let argb = (arga * arga + argc * (lvds - vdsat)).sqrt();
916            delxl = argb - arga;
917            let dldem;
918            if argb != 0.0 {
919                dldvd = argc / (argb + argb);
920                dldem = 0.5 * (arga / argb - 1.0) * t.alpha;
921            } else {
922                dldvd = 0.0;
923                dldem = 0.0;
924            }
925            ddldvg = dldem * demdvg;
926            ddldvd = dldem * demdvd - dldvd;
927            ddldvb = dldem * demdvb;
928        }
929        // line520: punch through approximation.
930        if delxl > 0.5 * eff_l {
931            delxl = eff_l - (eff_l * eff_l / (4.0 * delxl));
932            let arga = 4.0 * (eff_l - delxl) * (eff_l - delxl) / (eff_l * eff_l);
933            ddldvg *= arga;
934            ddldvd *= arga;
935            ddldvb *= arga;
936            dldvd *= arga;
937        }
938        // Saturation region (and ngspice's sub-vdsat CLM).
939        let dlonxl = delxl * oneoverxl;
940        let xlfact = 1.0 / (1.0 - dlonxl);
941        cdrain *= xlfact;
942        let diddl = cdrain / (eff_l - delxl);
943        gm = gm * xlfact + diddl * ddldvg;
944        gmbs = gmbs * xlfact + diddl * ddldvb;
945        gds0 = diddl * ddldvd;
946        gm += gds0 * dvsdvg;
947        gmbs += gds0 * dvsdvb;
948        gds = gds * xlfact + diddl * dldvd + gds0 * dvsdvd;
949    }
950
951    // line700: finish strong inversion case.
952    if lvgs < von {
953        // Weak inversion.
954        let onxn = 1.0 / xn;
955        let ondvt = onxn / vt;
956        let wfact = f64::exp((lvgs - von) * ondvt);
957        cdrain *= wfact;
958        let gms = gm * wfact;
959        let gmw = cdrain * ondvt;
960        gm = gmw;
961        if saturated {
962            gm += gds0 * dvsdvg * wfact;
963        }
964        gds = gds * wfact + (gms - gmw) * dvodvd;
965        gmbs = gmbs * wfact + (gms - gmw) * dvodvb - gmw * (lvgs - von) * onxn * dxndvb;
966    }
967
968    Mos3Eval {
969        cdrain,
970        gm,
971        gds,
972        gmbs,
973        von,
974        vdsat,
975    }
976}
977
978impl Mos3I {
979    /// `MOS3dSetup` (`dev/mos3/mos3dset.c`): the 3f5 semi-empirical
980    /// drain current re-evaluated with the `Dderivs` algebra (p = vgs,
981    /// q = vbs, r = vds, mode-normalized) — the setup's own equations,
982    /// which ngspice-47 carries unchanged apart from the effective
983    /// width (equal to `W` here, Dune has no `WD`/`XW`), not the
984    /// smoothed alansfixes current the load uses. Kept as the C has
985    /// it: the narrow-width term of `qbonco` *multiplies* by `W` in
986    /// the derivative chain while the value divides, and `fbody`
987    /// takes the derivatives of `fbodys` alone.
988    #[allow(unused_assignments, clippy::too_many_lines)]
989    fn disto_coeffs(&self, sol: &[f64], gmin: f64) -> MosDistoCoeffs {
990        let t = &self.t;
991        let g = &self.geom;
992        let typ = t.type_;
993        let vt = t.vt;
994        let eff_l = g.l - 2.0 * t.lat_diff;
995        let (drain_sat_cur, source_sat_cur) =
996            if t.t_sat_cur_dens == 0.0 || g.ad == 0.0 || g.as_ == 0.0 {
997                (t.t_sat_cur, t.t_sat_cur)
998            } else {
999                (t.t_sat_cur_dens * g.ad, t.t_sat_cur_dens * g.as_)
1000            };
1001        let mut beta = t.t_transconductance * g.w / eff_l;
1002        let oxide_cap = t.oxide_cap_factor * eff_l * g.w;
1003
1004        let vbs = typ * (sol[self.bulk] - sol[self.s_p]);
1005        let vgs = typ * (sol[self.gate] - sol[self.s_p]);
1006        let vds = typ * (sol[self.d_p] - sol[self.s_p]);
1007        let vbd = vbs - vds;
1008        let vgd = vgs - vds;
1009
1010        let gbs = disto_bulk_diode(vbs, source_sat_cur, vt, gmin, typ);
1011        let gbd = disto_bulk_diode(vbd, drain_sat_cur, vt, gmin, typ);
1012        let mode = if vds >= 0.0 { 1.0 } else { -1.0 };
1013
1014        let (lvgs, lvds, lvbs) = if mode == 1.0 {
1015            (vgs, vds, vbs)
1016        } else {
1017            (vgd, -vds, vbd)
1018        };
1019
1020        let d_p = Dderivs::var_p();
1021        let d_q = Dderivs::var_q();
1022        let d_r = Dderivs::var_r();
1023        let d_zero = Dderivs::default();
1024        let mut d_cdrain = d_zero;
1025        let mut vdsat = 0.0;
1026        let mut von;
1027
1028        'eval: {
1029            let mut d_vdsat;
1030            let oneoverxl = 1.0 / eff_l;
1031            let eta = t.eta * 8.15e-22 / (t.oxide_cap_factor * eff_l * eff_l * eff_l);
1032
1033            // Square-root term.
1034            let (phibs, sqphbs, d_phibs, d_sqphbs);
1035            if lvbs <= 0.0 {
1036                phibs = t.t_phi - lvbs;
1037                let mut d = Dderivs { value: lvbs, ..d_q }.times(-1.0);
1038                d.value += t.t_phi;
1039                d_phibs = d;
1040                sqphbs = f64::sqrt(phibs);
1041                d_sqphbs = d_phibs.sqrt();
1042            } else {
1043                let sqphis = f64::sqrt(t.t_phi);
1044                sqphbs = sqphis / (1.0 + lvbs / (t.t_phi + t.t_phi));
1045                let mut d = Dderivs { value: lvbs, ..d_q }.times(1.0 / (t.t_phi + t.t_phi));
1046                d.value += 1.0;
1047                d_sqphbs = d.inv().times(sqphis);
1048                phibs = sqphbs * sqphbs;
1049                d_phibs = d_sqphbs.mult(&d_sqphbs);
1050            }
1051
1052            // Short-channel effect factor.
1053            const COEFF0: f64 = 0.0631353e0;
1054            const COEFF1: f64 = 0.8013292e0;
1055            const COEFF2: f64 = -0.01110777e0;
1056            let (fshort, d_fshort);
1057            if t.xj != 0.0 && t.coeff_dep_lay_width != 0.0 {
1058                let wps = t.coeff_dep_lay_width * sqphbs;
1059                let d_wps = d_sqphbs.times(t.coeff_dep_lay_width);
1060                let oneoverxj = 1.0 / t.xj;
1061                let xjonxl = t.xj * oneoverxl;
1062                let djonxj = t.lat_diff * oneoverxj;
1063                let wponxj = wps * oneoverxj;
1064                let d_wponxj = d_wps.times(oneoverxj);
1065                let wconxj = COEFF0 + COEFF1 * wponxj + COEFF2 * wponxj * wponxj;
1066                let mut d_wconxj = d_wponxj.times(COEFF2);
1067                d_wconxj.value += COEFF1;
1068                let mut d_wconxj = d_wconxj.mult(&d_wponxj);
1069                d_wconxj.value += COEFF0;
1070                let arga = wconxj + djonxj;
1071                let mut d_arga = d_wconxj;
1072                d_arga.value += djonxj;
1073                let argc = wponxj / (1.0 + wponxj);
1074                let mut d_argc = d_wponxj;
1075                d_argc.value += 1.0;
1076                let d_argc = d_argc.inv().mult(&d_wponxj);
1077                let argb = f64::sqrt(1.0 - argc * argc);
1078                let mut d_argb = d_argc.mult(&d_argc).times(-1.0);
1079                d_argb.value += 1.0;
1080                let d_argb = d_argb.sqrt();
1081                fshort = 1.0 - xjonxl * (arga * argb - djonxj);
1082                let mut d = d_arga.mult(&d_argb);
1083                d.value -= djonxj;
1084                let mut d = d.times(-xjonxl);
1085                d.value += 1.0;
1086                d_fshort = d;
1087            } else {
1088                fshort = 1.0;
1089                d_fshort = Dderivs::constant(1.0);
1090            }
1091
1092            // Body effect.
1093            let gammas = t.gamma * fshort;
1094            let d_gammas = d_fshort.times(t.gamma);
1095            let fbodys = 0.5 * gammas / (sqphbs + sqphbs);
1096            let d_fbodys = d_gammas.div(&d_sqphbs).times(0.25);
1097            let fbody = fbodys + t.narrow_factor / g.w;
1098            let mut d_fbody = d_fbodys;
1099            d_fbody.value += fbody - fbodys;
1100            let onfbdy = 1.0 / (1.0 + fbody);
1101            let mut d_onfbdy = d_fbody;
1102            d_onfbdy.value += 1.0;
1103            let d_onfbdy = d_onfbdy.inv();
1104            let qbonco = gammas * sqphbs + t.narrow_factor * phibs / g.w;
1105            let d_dummy = d_phibs.times(t.narrow_factor * g.w);
1106            let d_qbonco = d_gammas.mult(&d_sqphbs).plus(&d_dummy);
1107
1108            // Static feedback, threshold voltage.
1109            let vbix = t.t_vbi * typ - eta * lvds;
1110            let d_vbix = Dderivs {
1111                value: vbix,
1112                d1_r: -eta,
1113                ..d_r
1114            };
1115            let vth = vbix + qbonco;
1116            let d_vth = d_vbix.plus(&d_qbonco);
1117            von = vth;
1118            let mut d_von = d_vth;
1119            let mut xn = 0.0;
1120            let mut d_xn = d_zero;
1121            if t.nfs != 0.0 {
1122                let csonco = CHARGE * t.nfs * 1e4 * eff_l * g.w / oxide_cap;
1123                let cdonco = 0.5 * qbonco / phibs;
1124                let d_cdonco = d_qbonco.div(&d_phibs).times(0.5);
1125                xn = 1.0 + csonco + cdonco;
1126                d_xn = d_cdonco;
1127                d_xn.value += 1.0 + csonco;
1128                von = vth + vt * xn;
1129                d_von = d_xn.times(vt).plus(&d_vth);
1130            } else if lvgs <= von {
1131                // Cutoff.
1132                d_cdrain = d_zero;
1133                break 'eval;
1134            }
1135
1136            // Device is on.
1137            let vgsx = f64::max(lvgs, von);
1138            let d_vgsx = if lvgs >= von {
1139                Dderivs { value: lvgs, ..d_p }
1140            } else {
1141                d_von
1142            };
1143            // Mobility modulation by gate voltage.
1144            let onfg = 1.0 + t.theta * (vgsx - vth);
1145            let mut d_onfg = d_vth.times(-1.0).plus(&d_vgsx).times(t.theta);
1146            d_onfg.value += 1.0;
1147            let fgate = 1.0 / onfg;
1148            let d_fgate = d_onfg.inv();
1149            let us = t.t_surf_mob * 1e-4 * fgate;
1150            let d_us = d_fgate.times(t.t_surf_mob * 1e-4);
1151            // Saturation voltage.
1152            vdsat = (vgsx - vth) * onfbdy;
1153            d_vdsat = d_vth.times(-1.0).plus(&d_vgsx).mult(&d_onfbdy);
1154            let (mut onvdsc, mut d_onvdsc) = (0.0, d_zero);
1155            if t.vmax > 0.0 {
1156                let vdsc = eff_l * t.vmax / us;
1157                let d_vdsc = d_us.inv().times(eff_l * t.vmax);
1158                onvdsc = 1.0 / vdsc;
1159                d_onvdsc = d_vdsc.inv();
1160                let arga = (vgsx - vth) * onfbdy;
1161                let d_arga = d_vdsat;
1162                let argb = f64::sqrt(arga * arga + vdsc * vdsc);
1163                let d_argb = d_vdsc.mult(&d_vdsc).plus(&d_arga.mult(&d_arga)).sqrt();
1164                vdsat = arga + vdsc - argb;
1165                d_vdsat = d_argb.times(-1.0).plus(&d_vdsc).plus(&d_arga);
1166            }
1167
1168            // Current factors in the linear region.
1169            let vdsx = f64::min(lvds, vdsat);
1170            let d_vdsx = if lvds < vdsat {
1171                Dderivs { value: lvds, ..d_r }
1172            } else {
1173                d_vdsat
1174            };
1175            if vdsx == 0.0 {
1176                // `line900`: the special case vds = 0 (`Beta` is
1177                // updated before the derivative is scaled by it, as
1178                // in the main path).
1179                beta *= fgate;
1180                let d_beta = d_fgate.times(beta);
1181                d_cdrain = d_zero;
1182                let mut d_gds = d_vth.times(-1.0).plus(&d_vgsx).mult(&d_beta);
1183                if t.nfs != 0.0 && lvgs < von {
1184                    let mut d_dummy = d_von.times(-1.0);
1185                    d_dummy.value += lvgs;
1186                    d_dummy.d1_p += 1.0;
1187                    let d_dummy = d_dummy.div(&d_xn).times(1.0 / vt).exp();
1188                    d_gds = d_gds.mult(&d_dummy);
1189                }
1190                d_cdrain.d1_r = d_gds.value;
1191                d_cdrain.d2_r2 = d_gds.d1_r;
1192                d_cdrain.d3_r3 = d_gds.d2_r2;
1193                break 'eval;
1194            }
1195            let cdo = vgsx - vth - 0.5 * (1.0 + fbody) * vdsx;
1196            let mut d_cdo = d_fbody;
1197            d_cdo.value += 1.0;
1198            let d_cdo = d_cdo
1199                .mult(&d_vdsx)
1200                .times(0.5)
1201                .plus(&d_vth)
1202                .times(-1.0)
1203                .plus(&d_vgsx);
1204            // Normalized drain current, then without velocity
1205            // saturation. The C updates `Beta = Beta·fgate` *before*
1206            // `TimesDeriv(&d_Beta, &d_fgate, Beta)`, so the
1207            // derivative chain carries `fgate²` — one extra factor
1208            // of `1/(1+THETA·(vgsx−vth))` in every drain-current
1209            // coefficient (kept; measured against the oracle).
1210            let cdnorm = cdo * vdsx;
1211            let d_cdnorm = d_cdo.mult(&d_vdsx);
1212            beta *= fgate;
1213            let d_beta = d_fgate.times(beta);
1214            let mut cdrain = beta * cdnorm;
1215            d_cdrain = d_beta.mult(&d_cdnorm);
1216            let (mut fdrain, mut d_fdrain) = (0.0, d_zero);
1217            if t.vmax != 0.0 {
1218                fdrain = 1.0 / (1.0 + vdsx * onvdsc);
1219                let mut f = d_vdsx.mult(&d_onvdsc);
1220                f.value += 1.0;
1221                d_fdrain = f.inv();
1222                cdrain *= fdrain;
1223                d_cdrain = d_cdrain.mult(&d_fdrain);
1224            }
1225
1226            // Channel-length modulation (`line510`/`line520`).
1227            if lvds > vdsat && !(t.vmax != 0.0 && t.alpha == 0.0) {
1228                let (mut delxl, mut d_delxl);
1229                if t.vmax == 0.0 {
1230                    delxl = f64::sqrt(t.kappa * (lvds - vdsat) * t.alpha);
1231                    let mut d = d_vdsat.times(-1.0);
1232                    d.value += lvds;
1233                    d.d1_r += 1.0;
1234                    d_delxl = d.times(t.kappa * t.alpha).sqrt();
1235                } else {
1236                    let cdsat = cdrain;
1237                    let d_cdsat = d_cdrain;
1238                    let mut gdsat = cdsat * (1.0 - fdrain) * onvdsc;
1239                    let mut d_dummy = d_fdrain.times(-1.0);
1240                    d_dummy.value += 1.0;
1241                    let mut d_gdsat = d_cdsat.mult(&d_dummy).mult(&d_onvdsc);
1242                    gdsat = f64::max(1.0e-12, gdsat);
1243                    if gdsat == 1.0e-12 {
1244                        d_gdsat = Dderivs::constant(gdsat);
1245                    }
1246                    let emax = cdsat * oneoverxl / gdsat;
1247                    let d_emax = d_cdsat.div(&d_gdsat).times(oneoverxl);
1248                    let arga = 0.5 * emax * t.alpha;
1249                    let d_arga = d_emax.times(0.5 * t.alpha);
1250                    let argc = t.kappa * t.alpha;
1251                    let argb = f64::sqrt(arga * arga + argc * (lvds - vdsat));
1252                    let mut d_dummy = d_vdsat.times(-1.0);
1253                    d_dummy.value += lvds;
1254                    d_dummy.d1_r += 1.0;
1255                    let d_argb = d_dummy.times(argc).plus(&d_arga.mult(&d_arga)).sqrt();
1256                    delxl = argb - arga;
1257                    d_delxl = d_argb.plus(&d_arga.times(-1.0));
1258                }
1259                // Punch-through approximation.
1260                if delxl > 0.5 * eff_l {
1261                    delxl = eff_l - (eff_l * eff_l / delxl * 0.25);
1262                    d_delxl = d_delxl.inv().times(-eff_l * eff_l * 0.25);
1263                    d_delxl.value += eff_l;
1264                }
1265                let _ = delxl;
1266                let d_dlonxl = d_delxl.times(oneoverxl);
1267                let mut d_xlfact = d_dlonxl.times(-1.0);
1268                d_xlfact.value += 1.0;
1269                d_cdrain = d_cdrain.mult(&d_xlfact.inv());
1270            }
1271
1272            // `line700`: weak inversion.
1273            if lvgs < von {
1274                let d_onxn = d_xn.inv();
1275                let d_ondvt = d_onxn.times(1.0 / vt);
1276                let mut d_wfact = d_von.times(-1.0);
1277                d_wfact.value += lvgs;
1278                d_wfact.d1_p += 1.0;
1279                let d_wfact = d_wfact.mult(&d_ondvt).exp();
1280                d_cdrain = d_cdrain.mult(&d_wfact);
1281            }
1282            let _ = (xn, cdrain, us);
1283        }
1284
1285        let junctions = MosJunctions {
1286            cbs: t.cbs,
1287            cbssw: t.cbssw,
1288            f3s: t.f3s,
1289            cbd: t.cbd,
1290            cbdsw: t.cbdsw,
1291            f3d: t.f3d,
1292            dep_cap: t.t_dep_cap,
1293            bulk_pot: t.t_bulk_pot,
1294            mj: t.mj,
1295            mjsw: t.mjsw,
1296        };
1297        setup_tail(
1298            typ,
1299            mode,
1300            (vbs, vbd, vds),
1301            (lvgs - von, vdsat),
1302            t.t_phi,
1303            oxide_cap,
1304            &junctions,
1305            &DrainDerivs::from_dderivs(&d_cdrain),
1306            gbs,
1307            gbd,
1308        )
1309    }
1310}
1311
1312impl crate::devices::Device for Mos3I {
1313    fn par_load(&self) -> bool {
1314        true
1315    }
1316
1317    /// `MOS3getic` (`mos3ic.c`): VBS, VDS, VGS not given
1318    /// on the card from the external nodes.
1319    fn set_ic(&mut self, rhs: &[f64]) {
1320        use crate::devices::getic;
1321        let g = self.ic_given;
1322        getic(g, 2, &mut self.ic_vbs, rhs, self.bulk, self.source);
1323        getic(g, 0, &mut self.ic_vds, rhs, self.drain, self.source);
1324        getic(g, 1, &mut self.ic_vgs, rhs, self.gate, self.source);
1325    }
1326
1327    fn set_ic_given(&mut self, given: crate::devices::IcGiven) {
1328        self.ic_given = given;
1329    }
1330
1331    /// `Mos3convTest`: predicted vs computed drain and bulk
1332    /// currents, evaluated at the fresh solution (`CKTrhs`).
1333    fn conv_test(&self, ctx: &crate::matrix::ConvCtx) -> bool {
1334        let typ = self.t.type_;
1335        let st = self.st;
1336        let vbs = typ * (ctx.rhs[self.bulk] - ctx.rhs[self.s_p]);
1337        let vgs = typ * (ctx.rhs[self.gate] - ctx.rhs[self.s_p]);
1338        let vds = typ * (ctx.rhs[self.d_p] - ctx.rhs[self.s_p]);
1339        let vbd = vbs - vds;
1340        let vgd = vgs - vds;
1341        let vgdo = ctx.states.get(0, st + VGS) - ctx.states.get(0, st + VDS);
1342        let delvbs = vbs - ctx.states.get(0, st + VBS);
1343        let delvbd = vbd - ctx.states.get(0, st + VBD);
1344        let delvgs = vgs - ctx.states.get(0, st + VGS);
1345        let delvds = vds - ctx.states.get(0, st + VDS);
1346        let delvgd = vgd - vgdo;
1347        let cdhat = if self.mode >= 0.0 {
1348            self.cd - self.gbd * delvbd + self.gmbs * delvbs + self.gm * delvgs + self.gds * delvds
1349        } else {
1350            self.cd - (self.gbd - self.gmbs) * delvbd - self.gm * delvgd + self.gds * delvds
1351        };
1352        let cbhat = self.cbs + self.cbd + self.gbd * delvbd + self.gbs * delvbs;
1353        let tol = ctx.reltol * f64::max(cdhat.abs(), self.cd.abs()) + ctx.abstol;
1354        if (cdhat - self.cd).abs() >= tol {
1355            return false;
1356        }
1357        let cb = self.cbs + self.cbd;
1358        let tol = ctx.reltol * f64::max(cbhat.abs(), cb.abs()) + ctx.abstol;
1359        (cbhat - cb).abs() <= tol
1360    }
1361
1362    fn alloc(&mut self, mb: &mut MatrixBuilder) {
1363        let (d, g, s, b) = (self.drain, self.gate, self.source, self.bulk);
1364        let (dp, sp) = (self.d_p, self.s_p);
1365        self.s = [
1366            mb.slot(d, d),
1367            mb.slot(g, g),
1368            mb.slot(s, s),
1369            mb.slot(b, b),
1370            mb.slot(dp, dp),
1371            mb.slot(sp, sp),
1372            mb.slot(d, dp),
1373            mb.slot(g, b),
1374            mb.slot(g, dp),
1375            mb.slot(g, sp),
1376            mb.slot(s, sp),
1377            mb.slot(b, dp),
1378            mb.slot(b, sp),
1379            mb.slot(dp, sp),
1380            mb.slot(dp, d),
1381            mb.slot(b, g),
1382            mb.slot(dp, g),
1383            mb.slot(sp, g),
1384            mb.slot(sp, s),
1385            mb.slot(dp, b),
1386            mb.slot(sp, b),
1387            mb.slot(sp, dp),
1388        ];
1389    }
1390
1391    #[allow(clippy::too_many_lines)]
1392    fn load(&mut self, ctx: &mut LoadCtx) {
1393        let typ = self.t.type_;
1394        let vt = self.t.vt;
1395        let st = self.st;
1396
1397        let eff_l = self.geom.l - 2.0 * self.t.lat_diff;
1398        let (drain_sat_cur, source_sat_cur) =
1399            if self.t.t_sat_cur_dens == 0.0 || self.geom.ad == 0.0 || self.geom.as_ == 0.0 {
1400                (self.t.t_sat_cur, self.t.t_sat_cur)
1401            } else {
1402                (
1403                    self.t.t_sat_cur_dens * self.geom.ad,
1404                    self.t.t_sat_cur_dens * self.geom.as_,
1405                )
1406            };
1407        let gate_source_overlap_cap = self.t.cgso * self.geom.w;
1408        let gate_drain_overlap_cap = self.t.cgdo * self.geom.w;
1409        let gate_bulk_overlap_cap = self.t.cgbo * eff_l;
1410        let beta = self.t.t_transconductance * self.geom.w / eff_l;
1411        let oxide_cap = self.t.oxide_cap_factor * eff_l * self.geom.w;
1412
1413        // Bias resolution — same ladder as MOS1.
1414        let mut check = true;
1415        let (mut vbs, mut vgs, mut vds);
1416        let predictor = matches!(ctx.mode, InitMode::Pred | InitMode::Tran);
1417        let general = matches!(ctx.mode, InitMode::Float | InitMode::Smsig)
1418            || (ctx.mode == InitMode::Fix && !self.off);
1419        if predictor || general {
1420            if predictor {
1421                let xfact = ctx.pred_xfact;
1422                let mut pred = |o: usize| {
1423                    let s1 = ctx.states.get(1, st + o);
1424                    ctx.states.set(0, st + o, s1);
1425                    (1.0 + xfact) * s1 - xfact * ctx.states.get(2, st + o)
1426                };
1427                vbs = pred(VBS);
1428                vgs = pred(VGS);
1429                vds = pred(VDS);
1430                let vbd0 = ctx.states.get(0, st + VBS) - ctx.states.get(0, st + VDS);
1431                ctx.states.set(0, st + VBD, vbd0);
1432            } else {
1433                vbs = typ * (ctx.sol[self.bulk] - ctx.sol[self.s_p]);
1434                vgs = typ * (ctx.sol[self.gate] - ctx.sol[self.s_p]);
1435                vds = typ * (ctx.sol[self.d_p] - ctx.sol[self.s_p]);
1436            }
1437            let mut vbd = vbs - vds;
1438            let mut vgd = vgs - vds;
1439            let vgdo = ctx.states.get(0, st + VGS) - ctx.states.get(0, st + VDS);
1440
1441            let von = typ * self.von;
1442            if ctx.states.get(0, st + VDS) >= 0.0 {
1443                vgs = fetlim(vgs, ctx.states.get(0, st + VGS), von);
1444                vds = vgs - vgd;
1445                vds = limvds(vds, ctx.states.get(0, st + VDS));
1446            } else {
1447                vgd = fetlim(vgd, vgdo, von);
1448                vds = vgs - vgd;
1449                vds = -limvds(-vds, -ctx.states.get(0, st + VDS));
1450                vgs = vgd + vds;
1451            }
1452            if vds >= 0.0 {
1453                vbs = pnjlim(
1454                    vbs,
1455                    ctx.states.get(0, st + VBS),
1456                    vt,
1457                    self.t.source_vcrit,
1458                    &mut check,
1459                );
1460            } else {
1461                vbd = pnjlim(
1462                    vbd,
1463                    ctx.states.get(0, st + VBD),
1464                    vt,
1465                    self.t.drain_vcrit,
1466                    &mut check,
1467                );
1468                vbs = vbd + vds;
1469            }
1470        } else if ctx.mode == InitMode::Jct && !self.off {
1471            vds = typ * self.ic_vds;
1472            vgs = typ * self.ic_vgs;
1473            vbs = typ * self.ic_vbs;
1474            if vds == 0.0 && vgs == 0.0 && vbs == 0.0 && !(ctx.kind == AnKind::TranOp && ctx.uic) {
1475                vbs = -1.0;
1476                vgs = typ * self.t.t_vto;
1477                vds = 0.0;
1478            }
1479        } else {
1480            vbs = 0.0;
1481            vgs = 0.0;
1482            vds = 0.0;
1483        }
1484
1485        let vbd = vbs - vds;
1486        let vgd = vgs - vds;
1487        let vgb = vgs - vbs;
1488
1489        // Bulk diodes, in the ngspice form — which for MOS3 is not
1490        // MOS1/MOS2's flat leakage but the diode's C³ cubic tail
1491        // below −3·vt (`arg = (3·vt/(v·e))³`), clamped exponential
1492        // above, gmin folded into the current
1493        // (`corpus/KNOWN_DIFFS.md`).
1494        let (mut gbs, mut cbs) = bulk_diode(vbs, vt, source_sat_cur, ctx.gmin);
1495        let (mut gbd, mut cbd) = bulk_diode(vbd, vt, drain_sat_cur, ctx.gmin);
1496
1497        self.mode = if vds >= 0.0 { 1.0 } else { -1.0 };
1498        let mode = self.mode;
1499
1500        // Semi-empirical evaluation (`moseq3`).
1501        let (lvbs, lvgs) = if mode == 1.0 { (vbs, vgs) } else { (vbd, vgd) };
1502        let r = moseq3(
1503            &self.t,
1504            self.geom.w,
1505            lvbs,
1506            lvgs,
1507            mode * vds,
1508            beta,
1509            oxide_cap,
1510            eff_l,
1511            vt,
1512        );
1513        let (cdrain, von, vdsat) = (r.cdrain, r.von, r.vdsat);
1514        let (gm, gds, gmbs) = (r.gm, r.gds, r.gmbs);
1515        self.gm = gm;
1516        self.gds = gds;
1517        self.gmbs = gmbs;
1518        self.von = typ * von;
1519        self.vdsat = typ * vdsat;
1520
1521        let mut cd = mode * cdrain - cbd;
1522
1523        let t = &self.t;
1524        let smsig = ctx.mode == InitMode::Smsig;
1525        if ctx.kind == AnKind::Tran || ctx.kind == AnKind::TranOp || smsig {
1526            // Bulk junction depletion charge and capacitance (same
1527            // sqrt fast path and f2/f3/f4 continuation as MOS1).
1528            let dep = |v: f64, cz: f64, czsw: f64, f2: f64, f3: f64, f4: f64| {
1529                if cz != 0.0 || czsw != 0.0 {
1530                    if v < t.t_dep_cap {
1531                        let arg = 1.0 - v / t.t_bulk_pot;
1532                        let (sarg, sargsw);
1533                        if t.mj == t.mjsw {
1534                            if t.mj == 0.5 {
1535                                sarg = 1.0 / arg.sqrt();
1536                                sargsw = sarg;
1537                            } else {
1538                                sarg = f64::exp(-t.mj * f64::ln(arg));
1539                                sargsw = sarg;
1540                            }
1541                        } else {
1542                            sarg = if t.mj == 0.5 {
1543                                1.0 / arg.sqrt()
1544                            } else {
1545                                f64::exp(-t.mj * f64::ln(arg))
1546                            };
1547                            sargsw = if t.mjsw == 0.5 {
1548                                1.0 / arg.sqrt()
1549                            } else {
1550                                f64::exp(-t.mjsw * f64::ln(arg))
1551                            };
1552                        }
1553                        let q = t.t_bulk_pot
1554                            * (cz * (1.0 - arg * sarg) / (1.0 - t.mj)
1555                                + czsw * (1.0 - arg * sargsw) / (1.0 - t.mjsw));
1556                        (q, cz * sarg + czsw * sargsw)
1557                    } else {
1558                        (f4 + v * (f2 + v * f3 / 2.0), f2 + v * f3)
1559                    }
1560                } else {
1561                    (0.0, 0.0)
1562                }
1563            };
1564            let (qbs, capbs) = dep(vbs, t.cbs, t.cbssw, t.f2s, t.f3s, t.f4s);
1565            ctx.states.set(0, st + QBS, qbs);
1566            self.capbs = capbs;
1567            let (qbd, capbd) = dep(vbd, t.cbd, t.cbdsw, t.f2d, t.f3d, t.f4d);
1568            ctx.states.set(0, st + QBD, qbd);
1569            self.capbd = capbd;
1570
1571            if ctx.kind == AnKind::Tran {
1572                let (geq, _) = ctx.integrate(self.capbd, st + QBD);
1573                gbd += geq;
1574                cbd += ctx.states.get(0, st + CQBD);
1575                cd -= ctx.states.get(0, st + CQBD);
1576                let (geq, _) = ctx.integrate(self.capbs, st + QBS);
1577                gbs += geq;
1578                cbs += ctx.states.get(0, st + CQBS);
1579            }
1580        }
1581        self.gbs = gbs;
1582        self.gbd = gbd;
1583
1584        if (!self.off || !matches!(ctx.mode, InitMode::Fix | InitMode::Smsig)) && check {
1585            ctx.noncon = true;
1586        }
1587
1588        ctx.states.set(0, st + VBS, vbs);
1589        ctx.states.set(0, st + VBD, vbd);
1590        ctx.states.set(0, st + VGS, vgs);
1591        ctx.states.set(0, st + VDS, vds);
1592
1593        // Meyer's capacitor model.
1594        let (mut capgs, mut capgd, mut capgb) = (0.0, 0.0, 0.0);
1595        if ctx.kind == AnKind::Tran || ctx.kind == AnKind::TranOp || smsig {
1596            let (half_gs, half_gd, half_gb) = if mode > 0.0 {
1597                qmeyer(vgs, vgd, vgb, von, vdsat, t.t_phi, oxide_cap)
1598            } else {
1599                let (o1, o2, o3) = qmeyer(vgd, vgs, vgb, von, vdsat, t.t_phi, oxide_cap);
1600                (o2, o1, o3)
1601            };
1602            ctx.states.set(0, st + CAPGS, half_gs);
1603            ctx.states.set(0, st + CAPGD, half_gd);
1604            ctx.states.set(0, st + CAPGB, half_gb);
1605            let vgs1 = ctx.states.get(1, st + VGS);
1606            let vgd1 = vgs1 - ctx.states.get(1, st + VDS);
1607            let vgb1 = vgs1 - ctx.states.get(1, st + VBS);
1608            if ctx.kind == AnKind::TranOp || smsig {
1609                capgs = 2.0 * ctx.states.get(0, st + CAPGS) + gate_source_overlap_cap;
1610                capgd = 2.0 * ctx.states.get(0, st + CAPGD) + gate_drain_overlap_cap;
1611                capgb = 2.0 * ctx.states.get(0, st + CAPGB) + gate_bulk_overlap_cap;
1612            } else {
1613                capgs = ctx.states.get(0, st + CAPGS)
1614                    + ctx.states.get(1, st + CAPGS)
1615                    + gate_source_overlap_cap;
1616                capgd = ctx.states.get(0, st + CAPGD)
1617                    + ctx.states.get(1, st + CAPGD)
1618                    + gate_drain_overlap_cap;
1619                capgb = ctx.states.get(0, st + CAPGB)
1620                    + ctx.states.get(1, st + CAPGB)
1621                    + gate_bulk_overlap_cap;
1622            }
1623
1624            if smsig {
1625                return;
1626            }
1627
1628            if predictor {
1629                let xfact = ctx.pred_xfact;
1630                for o in [QGS, QGD, QGB] {
1631                    let v = (1.0 + xfact) * ctx.states.get(1, st + o)
1632                        - xfact * ctx.states.get(2, st + o);
1633                    ctx.states.set(0, st + o, v);
1634                }
1635            } else if ctx.kind == AnKind::Tran {
1636                let q = (vgs - vgs1) * capgs + ctx.states.get(1, st + QGS);
1637                ctx.states.set(0, st + QGS, q);
1638                let q = (vgd - vgd1) * capgd + ctx.states.get(1, st + QGD);
1639                ctx.states.set(0, st + QGD, q);
1640                let q = (vgb - vgb1) * capgb + ctx.states.get(1, st + QGB);
1641                ctx.states.set(0, st + QGB, q);
1642            } else {
1643                ctx.states.set(0, st + QGS, capgs * vgs);
1644                ctx.states.set(0, st + QGD, capgd * vgd);
1645                ctx.states.set(0, st + QGB, capgb * vgb);
1646            }
1647        }
1648
1649        // Meyer companion models.
1650        let (mut gcgs, mut ceqgs, mut gcgd, mut ceqgd, mut gcgb, mut ceqgb) =
1651            (0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
1652        if ctx.mode != InitMode::Tran && ctx.kind == AnKind::Tran {
1653            if capgs == 0.0 {
1654                ctx.states.set(0, st + CQGS, 0.0);
1655            }
1656            if capgd == 0.0 {
1657                ctx.states.set(0, st + CQGD, 0.0);
1658            }
1659            if capgb == 0.0 {
1660                ctx.states.set(0, st + CQGB, 0.0);
1661            }
1662            let (g, ceq) = ctx.integrate(capgs, st + QGS);
1663            gcgs = g;
1664            ceqgs = ceq - gcgs * vgs + ctx.ag[0] * ctx.states.get(0, st + QGS);
1665            let (g, ceq) = ctx.integrate(capgd, st + QGD);
1666            gcgd = g;
1667            ceqgd = ceq - gcgd * vgd + ctx.ag[0] * ctx.states.get(0, st + QGD);
1668            let (g, ceq) = ctx.integrate(capgb, st + QGB);
1669            gcgb = g;
1670            ceqgb = ceq - gcgb * vgb + ctx.ag[0] * ctx.states.get(0, st + QGB);
1671        }
1672
1673        // Load the current vector.
1674        // ngspice folds gmin into the junction current, so the
1675        // Norton companion uses the full conductance (the 3f5
1676        // subtracted gmin here).
1677        let ceqbs = typ * (cbs - gbs * vbs);
1678        let ceqbd = typ * (cbd - gbd * vbd);
1679        let (xnrm, xrev, cdreq);
1680        if mode >= 0.0 {
1681            xnrm = 1.0;
1682            xrev = 0.0;
1683            cdreq = typ * (cdrain - gds * vds - gm * vgs - gmbs * vbs);
1684        } else {
1685            xnrm = 0.0;
1686            xrev = 1.0;
1687            cdreq = -typ * (cdrain - gds * (-vds) - gm * vgd - gmbs * vbd);
1688        }
1689        // Saved for the convergence test (`mos3conv.c`).
1690        self.cd = cd;
1691        self.cbs = cbs;
1692        self.cbd = cbd;
1693        ctx.add_rhs(self.gate, -(typ * (ceqgs + ceqgb + ceqgd)));
1694        ctx.add_rhs(self.bulk, -(ceqbs + ceqbd - typ * ceqgb));
1695        ctx.add_rhs(self.d_p, ceqbd - cdreq + typ * ceqgd);
1696        ctx.add_rhs(self.s_p, cdreq + ceqbs + typ * ceqgs);
1697
1698        // Load the Y matrix.
1699        let gdpr = self.t.drain_conductance;
1700        let gspr = self.t.source_conductance;
1701        ctx.add(self.s[D_D], gdpr);
1702        ctx.add(self.s[G_G], gcgd + gcgs + gcgb);
1703        ctx.add(self.s[S_S], gspr);
1704        ctx.add(self.s[B_B], gbd + gbs + gcgb);
1705        ctx.add(self.s[DP_DP], gdpr + gds + gbd + xrev * (gm + gmbs) + gcgd);
1706        ctx.add(self.s[SP_SP], gspr + gds + gbs + xnrm * (gm + gmbs) + gcgs);
1707        ctx.add(self.s[D_DP], -gdpr);
1708        ctx.add(self.s[G_B], -gcgb);
1709        ctx.add(self.s[G_DP], -gcgd);
1710        ctx.add(self.s[G_SP], -gcgs);
1711        ctx.add(self.s[S_SP], -gspr);
1712        ctx.add(self.s[B_G], -gcgb);
1713        ctx.add(self.s[B_DP], -gbd);
1714        ctx.add(self.s[B_SP], -gbs);
1715        ctx.add(self.s[DP_D], -gdpr);
1716        ctx.add(self.s[DP_G], (xnrm - xrev) * gm - gcgd);
1717        ctx.add(self.s[DP_B], -gbd + (xnrm - xrev) * gmbs);
1718        ctx.add(self.s[DP_SP], -gds - xnrm * (gm + gmbs));
1719        ctx.add(self.s[SP_G], -(xnrm - xrev) * gm - gcgs);
1720        ctx.add(self.s[SP_S], -gspr);
1721        ctx.add(self.s[SP_B], -gbs - (xnrm - xrev) * gmbs);
1722        ctx.add(self.s[SP_DP], -gds - xrev * (gm + gmbs));
1723    }
1724
1725    fn trunc(&self, t: &crate::integ::TerrCtx, time_step: &mut f64) {
1726        crate::integ::terr(self.st + QGS, t, time_step);
1727        crate::integ::terr(self.st + QGD, t, time_step);
1728        crate::integ::terr(self.st + QGB, t, time_step);
1729    }
1730
1731    /// `MOS3noise` (ngspice-47, default `NLEV`): see
1732    /// [`crate::mos1::mos_noise`].
1733    /// (`MOS3noise` narrows W by `2·WD`; WD is an ngspice extension
1734    /// this port does not carry, so W enters as given.)
1735    fn noise_suffixes(&self) -> Option<&'static [&'static str]> {
1736        Some(crate::mos1::MOS_NOISE_SUFFIXES)
1737    }
1738
1739    fn noise_dens(&self, ctx: &crate::matrix::NoiseCtx) -> Vec<f64> {
1740        let n = crate::mos1::MosNoise {
1741            drain: self.drain,
1742            source: self.source,
1743            d_p: self.d_p,
1744            s_p: self.s_p,
1745            gdpr: self.t.drain_conductance,
1746            gspr: self.t.source_conductance,
1747            gm: self.gm,
1748            cd: self.cd,
1749            kf: self.t.kf,
1750            af: self.t.af,
1751            nlev: self.t.nlev,
1752            gdsnoi: self.t.gdsnoi,
1753            w: self.geom.w,
1754            eff_l: self.geom.l - 2.0 * self.t.lat_diff,
1755            cox: self.t.oxide_cap_factor,
1756            temp: self.t.temp,
1757            vgs: ctx.states.get(0, self.st + VGS),
1758            vds: ctx.states.get(0, self.st + VDS),
1759            mode: self.mode,
1760            von: self.von,
1761            vdsat: self.vdsat,
1762            typ: self.t.type_,
1763            t_transconductance: self.t.t_transconductance,
1764        };
1765        crate::mos1::mos_noise(ctx, &n)
1766    }
1767
1768    /// `MOS3dSetup`: see [`Mos3I::disto_coeffs`].
1769    fn disto_setup(&mut self, ctx: &DistoSetupCtx) -> Result<(), &'static str> {
1770        self.dist = self.disto_coeffs(ctx.sol, ctx.gmin);
1771        Ok(())
1772    }
1773
1774    /// `MOS3disto` (`dev/mos3/mos3dist.c`, the MOS1 file renamed).
1775    fn disto_load(&self, ctx: &mut DistoCtx) {
1776        mos_disto_load(&self.dist, (self.gate, self.bulk, self.d_p, self.s_p), ctx);
1777    }
1778
1779    /// `MOS3acLoad` (same stamps as MOS1's).
1780    fn load_ac(&mut self, ctx: &mut AcCtx) {
1781        let t = &self.t;
1782        let st = self.st;
1783        let (xnrm, xrev) = if self.mode < 0.0 {
1784            (0.0, 1.0)
1785        } else {
1786            (1.0, 0.0)
1787        };
1788        let eff_l = self.geom.l - 2.0 * t.lat_diff;
1789        let capgs = 2.0 * ctx.states.get(0, st + CAPGS) + t.cgso * self.geom.w;
1790        let capgd = 2.0 * ctx.states.get(0, st + CAPGD) + t.cgdo * self.geom.w;
1791        let capgb = 2.0 * ctx.states.get(0, st + CAPGB) + t.cgbo * eff_l;
1792        let xgs = capgs * ctx.omega;
1793        let xgd = capgd * ctx.omega;
1794        let xgb = capgb * ctx.omega;
1795        let xbd = self.capbd * ctx.omega;
1796        let xbs = self.capbs * ctx.omega;
1797        let (gm, gds, gmbs) = (self.gm, self.gds, self.gmbs);
1798        let (gbd, gbs) = (self.gbd, self.gbs);
1799        let gdpr = t.drain_conductance;
1800        let gspr = t.source_conductance;
1801
1802        ctx.add_b(self.s[G_G], xgd + xgs + xgb);
1803        ctx.add_b(self.s[B_B], xgb + xbd + xbs);
1804        ctx.add_b(self.s[DP_DP], xgd + xbd);
1805        ctx.add_b(self.s[SP_SP], xgs + xbs);
1806        ctx.add_b(self.s[G_B], -xgb);
1807        ctx.add_b(self.s[G_DP], -xgd);
1808        ctx.add_b(self.s[G_SP], -xgs);
1809        ctx.add_b(self.s[B_G], -xgb);
1810        ctx.add_b(self.s[B_DP], -xbd);
1811        ctx.add_b(self.s[B_SP], -xbs);
1812        ctx.add_b(self.s[DP_G], -xgd);
1813        ctx.add_b(self.s[DP_B], -xbd);
1814        ctx.add_b(self.s[SP_G], -xgs);
1815        ctx.add_b(self.s[SP_B], -xbs);
1816        ctx.add_g(self.s[D_D], gdpr);
1817        ctx.add_g(self.s[S_S], gspr);
1818        ctx.add_g(self.s[B_B], gbd + gbs);
1819        ctx.add_g(self.s[DP_DP], gdpr + gds + gbd + xrev * (gm + gmbs));
1820        ctx.add_g(self.s[SP_SP], gspr + gds + gbs + xnrm * (gm + gmbs));
1821        ctx.add_g(self.s[D_DP], -gdpr);
1822        ctx.add_g(self.s[S_SP], -gspr);
1823        ctx.add_g(self.s[B_DP], -gbd);
1824        ctx.add_g(self.s[B_SP], -gbs);
1825        ctx.add_g(self.s[DP_D], -gdpr);
1826        ctx.add_g(self.s[DP_G], (xnrm - xrev) * gm);
1827        ctx.add_g(self.s[DP_B], -gbd + (xnrm - xrev) * gmbs);
1828        ctx.add_g(self.s[DP_SP], -gds - xnrm * (gm + gmbs));
1829        ctx.add_g(self.s[SP_G], -(xnrm - xrev) * gm);
1830        ctx.add_g(self.s[SP_S], -gspr);
1831        ctx.add_g(self.s[SP_B], -gbs - (xnrm - xrev) * gmbs);
1832        ctx.add_g(self.s[SP_DP], -gds - xrev * (gm + gmbs));
1833    }
1834
1835    /// `MOS3pzLoad` (`mos3pzld.c`, the MOS1 file renamed): see
1836    /// [`meyer_pz_load`] — always in the normal-mode orientation.
1837    /// `CKTpzSetup` re-runs every device's setup after the operating
1838    /// point, and `mos3set.c` (3f5 and ngspice-47 alike) resets
1839    /// `MOS3mode = 1` under a `MOS3modeGiven` flag nothing ever sets;
1840    /// MOS1/MOS2's setups don't touch the mode. A MOS3 conducting
1841    /// source-to-drain (vds < 0) is therefore stamped with `xnrm = 1`
1842    /// in `.pz`, unlike in `.ac` (gate: honoring the operating-point
1843    /// mode fails `regress/pz_mos3` at 995× its ruler).
1844    fn load_pz(&mut self, ctx: &mut crate::matrix::PzCtx) -> Result<(), &'static str> {
1845        let t = &self.t;
1846        let st = self.st;
1847        let (xnrm, xrev) = (1.0, 0.0);
1848        let eff_l = self.geom.l - 2.0 * t.lat_diff;
1849        let caps = MeyerPzCaps {
1850            capgs: 2.0 * ctx.states.get(0, st + CAPGS) + t.cgso * self.geom.w,
1851            capgd: 2.0 * ctx.states.get(0, st + CAPGD) + t.cgdo * self.geom.w,
1852            capgb: 2.0 * ctx.states.get(0, st + CAPGB) + t.cgbo * eff_l,
1853            capbd: self.capbd,
1854            capbs: self.capbs,
1855        };
1856        let g = MosPzConductances {
1857            xnrm,
1858            xrev,
1859            gm: self.gm,
1860            gds: self.gds,
1861            gmbs: self.gmbs,
1862            gbd: self.gbd,
1863            gbs: self.gbs,
1864            gdpr: t.drain_conductance,
1865            gspr: t.source_conductance,
1866        };
1867        meyer_pz_load(ctx, &self.s, &caps, &g);
1868        Ok(())
1869    }
1870}
1871
1872#[cfg(test)]
1873mod tests {
1874    use super::*;
1875
1876    fn geom(l: f64, w: f64) -> MosGeom {
1877        MosGeom {
1878            l,
1879            w,
1880            ad: 0.0,
1881            as_: 0.0,
1882            pd: 0.0,
1883            ps: 0.0,
1884            nrd: 1.0,
1885            nrs: 1.0,
1886        }
1887    }
1888
1889    /// The `n3` card of `corpus/regress/mos3_iv.cir`, with or
1890    /// without VMAX.
1891    fn n3(vmax: f64) -> Mos3Model {
1892        let mut m = Mos3Model::new(true);
1893        m.vto = Some(0.7);
1894        m.nsub = Some(3e16);
1895        m.tox = Some(2e-8);
1896        m.xj = 0.2e-6;
1897        m.ld = 0.05e-6;
1898        m.u0 = Some(500.0);
1899        m.theta = 0.08;
1900        m.eta = 0.3;
1901        m.delta = 1.2;
1902        m.vmax = vmax;
1903        m.kappa = 0.5;
1904        m.nfs = 8e11;
1905        m
1906    }
1907
1908    struct Dev {
1909        t: Mos3Temp,
1910        w: f64,
1911        eff_l: f64,
1912        beta: f64,
1913        cox: f64,
1914    }
1915
1916    impl Dev {
1917        fn new(m: &Mos3Model, l: f64, w: f64) -> Dev {
1918            let g = geom(l, w);
1919            let t = Mos3Temp::new("m1", m, &g, REFTEMP, REFTEMP).unwrap();
1920            let eff_l = l - 2.0 * t.lat_diff;
1921            let beta = t.t_transconductance * w / eff_l;
1922            let cox = t.oxide_cap_factor * eff_l * w;
1923            Dev {
1924                t,
1925                w,
1926                eff_l,
1927                beta,
1928                cox,
1929            }
1930        }
1931        fn eval(&self, vgs: f64, vds: f64, vbs: f64) -> Mos3Eval {
1932            moseq3(
1933                &self.t, self.w, vbs, vgs, vds, self.beta, self.cox, self.eff_l, self.t.vt,
1934            )
1935        }
1936        /// Central finite differences of the drain current:
1937        /// (∂/∂vgs, ∂/∂vds, ∂/∂vbs).
1938        fn fd(&self, vgs: f64, vds: f64, vbs: f64) -> (f64, f64, f64) {
1939            let h = 1e-6;
1940            let c = |a: f64, b: f64, d: f64| self.eval(a, b, d).cdrain;
1941            (
1942                (c(vgs + h, vds, vbs) - c(vgs - h, vds, vbs)) / (2.0 * h),
1943                (c(vgs, vds + h, vbs) - c(vgs, vds - h, vbs)) / (2.0 * h),
1944                (c(vgs, vds, vbs + h) - c(vgs, vds, vbs - h)) / (2.0 * h),
1945            )
1946        }
1947    }
1948
1949    fn rel(a: f64, b: f64) -> f64 {
1950        (a - b).abs() / f64::max(a.abs(), b.abs())
1951    }
1952
1953    #[test]
1954    fn temp_derives_from_nsub_with_ngspice_ni() {
1955        let d = Dev::new(&n3(1.5e5), 2e-6, 2e-6);
1956        let t = &d.t;
1957        let cox = 3.9 * 8.854214871e-12 / 2e-8;
1958        assert!(rel(t.oxide_cap_factor, cox) < 1e-15);
1959        // GAMMA = sqrt(2·EPSSIL·q·NSUB)/COX.
1960        let gamma = f64::sqrt(2.0 * EPSSIL * CHARGE * 3e16 * 1e6) / cox;
1961        assert!(rel(t.gamma, gamma) < 1e-14);
1962        // alpha = 2·EPSSIL/(q·NSUB), coeffDepLayWidth = sqrt(alpha).
1963        let alpha = 2.0 * EPSSIL / (CHARGE * 3e16 * 1e6);
1964        assert!(rel(t.alpha, alpha) < 1e-14);
1965        assert!(rel(t.coeff_dep_lay_width, alpha.sqrt()) < 1e-14);
1966        // narrowFactor = DELTA·π/2·EPSSIL/COX.
1967        let nf = 1.2 * 0.5 * std::f64::consts::PI * EPSSIL / cox;
1968        assert!(rel(t.narrow_factor, nf) < 1e-14);
1969        // KP from U0·COX; tSurfMob = U0 at TNOM.
1970        assert!(rel(t.t_transconductance, 500.0 * cox * 1e-4) < 1e-14);
1971        assert!(rel(t.t_surf_mob, 500.0) < 1e-15);
1972        // PHI from NSUB over ngspice's ni(TNOM), slightly above the
1973        // 3f5's fixed-ni value (ni(300.15 K) > 1.45e16).
1974        let vtnom = REFTEMP * KOVERQ;
1975        let phi_3f5 = 2.0 * vtnom * f64::ln(3e16 * 1e6 / 1.45e16);
1976        assert!(t.t_phi < phi_3f5 && phi_3f5 - t.t_phi < 1e-3);
1977        // VTO given: tVto = VTO at TNOM.
1978        assert!((t.t_vto - 0.7).abs() < 1e-12);
1979    }
1980
1981    #[test]
1982    fn temp_rejects_bad_cards() {
1983        let mut m = n3(0.0);
1984        m.nsub = Some(1e9);
1985        let e = Mos3Temp::new("m1", &m, &geom(2e-6, 2e-6), REFTEMP, REFTEMP).unwrap_err();
1986        assert!(e.contains("Nsub < Ni"));
1987        // Unlike MOS1/MOS2, a non-positive effective length is fatal.
1988        let e = Mos3Temp::new("m1", &n3(0.0), &geom(0.1e-6, 2e-6), REFTEMP, REFTEMP).unwrap_err();
1989        assert!(e.contains("effective channel length"));
1990        let mut m = n3(0.0);
1991        m.nsub = None;
1992        m.phi = Some(-0.1);
1993        let e = Mos3Temp::new("m1", &m, &geom(2e-6, 2e-6), REFTEMP, REFTEMP).unwrap_err();
1994        assert!(e.contains("Phi"));
1995    }
1996
1997    #[test]
1998    fn temp_scales_mobility_and_junctions_with_temperature() {
1999        let mut m = n3(0.0);
2000        m.cj = Some(4e-4);
2001        m.cjsw = Some(3e-10);
2002        let mut g = geom(2e-6, 2e-6);
2003        g.ad = 8e-12;
2004        g.pd = 12e-6;
2005        let hot = REFTEMP + 60.0;
2006        let t = Mos3Temp::new("m1", &m, &g, hot, REFTEMP).unwrap();
2007        let ratio = hot / REFTEMP;
2008        assert!(rel(t.t_surf_mob, 500.0 / (ratio * ratio.sqrt())) < 1e-14);
2009        // The forward continuation joins the depletion charge at
2010        // FC·PB with matching value and slope (ngspice's f2/f3/f4).
2011        let v = t.t_dep_cap;
2012        let arg = 1.0 - v / t.t_bulk_pot;
2013        let sarg = f64::exp(-m.mj * f64::ln(arg));
2014        let sargsw = f64::exp(-m.mjsw * f64::ln(arg));
2015        let q = t.t_bulk_pot
2016            * (t.cbd * (1.0 - arg * sarg) / (1.0 - m.mj)
2017                + t.cbdsw * (1.0 - arg * sargsw) / (1.0 - m.mjsw));
2018        let c = t.cbd * sarg + t.cbdsw * sargsw;
2019        assert!(rel(t.f4d + v * (t.f2d + v * t.f3d / 2.0), q) < 1e-12);
2020        assert!(rel(t.f2d + v * t.f3d, c) < 1e-12);
2021    }
2022
2023    #[test]
2024    fn linear_region_with_vmax_has_exact_derivatives() {
2025        // No CLM below vdsat on the VMAX path, so gm/gds/gmbs are the
2026        // exact derivatives (THETA, ETA, XJ, DELTA all active).
2027        let d = Dev::new(&n3(1.5e5), 2e-6, 2e-6);
2028        for (vgs, vds, vbs) in [(2.0, 0.3, 0.0), (3.0, 0.5, -1.0), (1.5, 0.1, -0.5)] {
2029            let r = d.eval(vgs, vds, vbs);
2030            assert!(vds < r.vdsat);
2031            let (gm, gds, gmbs) = d.fd(vgs, vds, vbs);
2032            assert!(rel(r.gm, gm) < 1e-5, "gm {} vs {gm}", r.gm);
2033            assert!(rel(r.gds, gds) < 1e-5, "gds {} vs {gds}", r.gds);
2034            assert!(rel(r.gmbs, gmbs) < 1e-5, "gmbs {} vs {gmbs}", r.gmbs);
2035        }
2036    }
2037
2038    #[test]
2039    fn weak_inversion_derivatives() {
2040        let d = Dev::new(&n3(1.5e5), 2e-6, 2e-6);
2041        let r = d.eval(0.5, 3.0, -1.0);
2042        assert!(0.5 < r.von && r.cdrain > 0.0);
2043        let (gm, gds, gmbs) = d.fd(0.5, 3.0, -1.0);
2044        assert!(rel(r.gm, gm) < 1e-3);
2045        assert!(rel(r.gds, gds) < 1e-3);
2046        assert!(rel(r.gmbs, gmbs) < 1e-3);
2047    }
2048
2049    #[test]
2050    fn saturation_derivatives_track_the_current() {
2051        // ngspice's CLM bookkeeping is approximate (∂delxl/∂vdsat is
2052        // taken as −∂delxl/∂vds), so gm/gmbs are a few 1e-3–1e-2 off
2053        // the exact derivative; gds is exact without ETA/VMAX.
2054        let mut m = n3(0.0);
2055        m.eta = 0.0;
2056        let d = Dev::new(&m, 3e-6, 6e-6);
2057        for (vgs, vds, vbs) in [(2.0, 3.0, 0.0), (3.0, 2.5, -1.0), (2.0, 0.5, 0.0)] {
2058            let r = d.eval(vgs, vds, vbs);
2059            let (gm, gds, gmbs) = d.fd(vgs, vds, vbs);
2060            assert!(rel(r.gds, gds) < 1e-5, "gds {} vs {gds}", r.gds);
2061            assert!(rel(r.gm, gm) < 2e-2, "gm {} vs {gm}", r.gm);
2062            assert!(rel(r.gmbs, gmbs) < 2e-2, "gmbs {} vs {gmbs}", r.gmbs);
2063        }
2064        let d = Dev::new(&n3(1.5e5), 2e-6, 2e-6);
2065        let r = d.eval(2.5, 3.0, -1.0);
2066        assert!(3.0 > r.vdsat);
2067        let (gm, gds, gmbs) = d.fd(2.5, 3.0, -1.0);
2068        assert!(rel(r.gm, gm) < 3e-2);
2069        assert!(rel(r.gds, gds) < 3e-2);
2070        assert!(rel(r.gmbs, gmbs) < 3e-2);
2071    }
2072
2073    #[test]
2074    fn smoothed_clm_is_continuous_through_vdsat() {
2075        // Without VMAX ngspice's CLM is C¹ at vds = vdsat (the 3f5's
2076        // sqrt(vds−vdsat) has an infinite slope there).
2077        let d = Dev::new(&n3(0.0), 3e-6, 6e-6);
2078        let vdsat = d.eval(2.0, 1.0, 0.0).vdsat;
2079        let lo = d.eval(2.0, vdsat - 1e-9, 0.0);
2080        let hi = d.eval(2.0, vdsat + 1e-9, 0.0);
2081        assert!(rel(lo.cdrain, hi.cdrain) < 1e-8);
2082        assert!(rel(lo.gds, hi.gds) < 1e-4);
2083        // And the channel already shortens below vdsat.
2084        let mut m = n3(0.0);
2085        m.kappa = 0.0;
2086        let flat = Dev::new(&m, 3e-6, 6e-6);
2087        assert!(lo.cdrain > flat.eval(2.0, vdsat - 1e-9, 0.0).cdrain);
2088    }
2089
2090    #[test]
2091    fn cutoff_without_nfs_and_vds_zero() {
2092        let mut m = n3(0.0);
2093        m.nfs = 0.0;
2094        let d = Dev::new(&m, 2e-6, 2e-6);
2095        let r = d.eval(0.2, 1.0, 0.0);
2096        assert_eq!((r.cdrain, r.gm, r.gds, r.gmbs), (0.0, 0.0, 0.0, 0.0));
2097        // line900: vds = 0 gives the channel conductance only.
2098        let r = d.eval(2.0, 0.0, 0.0);
2099        assert_eq!((r.cdrain, r.gm, r.gmbs), (0.0, 0.0, 0.0));
2100        let (_, gds, _) = d.fd(2.0, 1e-5, 0.0);
2101        assert!(rel(r.gds, gds) < 1e-3);
2102    }
2103
2104    #[test]
2105    fn bulk_diode_tail_is_c1_at_minus_3vt() {
2106        let vt = REFTEMP * KOVERQ;
2107        let v = -3.0 * vt;
2108        let (g0, i0) = bulk_diode(v, vt, 1e-14, 0.0);
2109        let (g1, i1) = bulk_diode(v + 1e-9, vt, 1e-14, 0.0);
2110        assert!(rel(i0, i1) < 1e-6);
2111        assert!(rel(g0, g1) < 1e-5);
2112        let (g, i) = bulk_diode(-5.0, vt, 1e-14, 1e-12);
2113        assert!(i < -1e-14 && g > 1e-12);
2114    }
2115}