Skip to main content

dune_core/
mos1.rs

1//! MOSFET level 1 — the Shichman–Hodges model from the 3f5
2//! (`dev/mos1/mos1load.c`, `mos1temp.c`, `mos1set.c`, `mos1acld.c`,
3//! `mos1trunc.c`). Square-law drain current with channel-length
4//! modulation, bulk junction diodes with depletion charge, Meyer
5//! gate capacitances (`DEVqmeyer`), and series drain/source
6//! resistance via prime nodes.
7//!
8//! The FET limiting helpers (`DEVfetlim`, `DEVlimvds`) and the Meyer
9//! capacitance evaluation live here too; the other MOS levels reuse
10//! them the way the BJT reuses the diode's `pnjlim`.
11
12use crate::consts::{BOLTZ, CHARGE, KOVERQ, REFTEMP, ROOT2};
13use crate::diode::pnjlim;
14use crate::disto::{DistoCtx, DistoSetupCtx};
15use crate::matrix::{AcCtx, AnKind, InitMode, LoadCtx, MatrixBuilder, Slot, TRASH, Var};
16use crate::mosdisto::{
17    DrainDerivs, MosDistoCoeffs, MosJunctions, bulk_diode, mos_disto_load, setup_tail,
18};
19
20/// `MAX_EXP_ARG` (`hw_ieee.h`): largest argument handed to `exp`.
21const MAX_EXP_ARG: f64 = 709.0;
22
23/// Name suffixes of the MOSFET noise generators
24/// (`MOS{1,2,3}nNames`): drain and source series resistances,
25/// channel thermal noise, flicker, total.
26pub(crate) const MOS_NOISE_SUFFIXES: &[&str] = &["_rd", "_rs", "_id", "_1overf", ""];
27
28/// The terminals and operating-point values a MOSFET noise
29/// evaluation reads.
30pub(crate) struct MosNoise {
31    pub drain: Var,
32    pub source: Var,
33    pub d_p: Var,
34    pub s_p: Var,
35    pub gdpr: f64,
36    pub gspr: f64,
37    /// `MOS*gm`, `MOS*cd` (instance fields of the last load).
38    pub gm: f64,
39    pub cd: f64,
40    pub kf: f64,
41    pub af: f64,
42    /// `NLEV`, `GDSNOI` (ngspice-47).
43    pub nlev: i64,
44    pub gdsnoi: f64,
45    /// Channel width and effective length (`W`, `L − 2·LD`).
46    pub w: f64,
47    pub eff_l: f64,
48    /// Oxide capacitance per area used by the flicker term.
49    pub cox: f64,
50    /// Instance temperature, kelvin (the thermal generators').
51    pub temp: f64,
52    /// What `NLEV = 3` reads: `state0` `vgs`/`vds` (type-normalized),
53    /// `MOS*mode`, `type·von`/`type·vdsat` as stored, the type, and
54    /// `tTransconductance`.
55    pub vgs: f64,
56    pub vds: f64,
57    pub mode: f64,
58    pub von: f64,
59    pub vdsat: f64,
60    pub typ: f64,
61    pub t_transconductance: f64,
62}
63
64/// `MOS1noise`/`MOS2noise`/`MOS3noise` (ngspice-47): thermal noise
65/// of RD and RS and the channel at the instance temperature
66/// (`NevalSrcInstanceTemp`), then the flicker term by `NLEV`:
67///
68/// - channel: `NLEV < 3` → `(2/3)|gm|`; `NLEV = 3` →
69///   `(2/3)·β·vgst·(1+α+α²)/(1+α)·GDSNOI` (`α = 0` in saturation,
70///   `1 − vds/vdsat` in the linear region, zero below threshold);
71/// - flicker: `0` → `KF·|Id|^AF/(f·Leff²·Cox)`, `1` →
72///   `KF·|Id|^AF/(f·W·Leff·Cox)`, `2`/`3` (default 2) →
73///   `KF·gm²/(f^AF·W·Leff·Cox)`; any other `NLEV` falls through the
74///   C's `switch` and leaves the bare gain (`N_GAIN`, a density of
75///   1 A²/Hz) — kept.
76///
77/// The 3f5 used `KF·|Id|^AF/(f·W·Leff·Cox²)` (ngspice keeps it only
78/// under `ngbehavior=spice3`, which Dune does not model); the
79/// goldens hold ngspice's.
80pub(crate) fn mos_noise(ctx: &crate::matrix::NoiseCtx, n: &MosNoise) -> Vec<f64> {
81    const N_MINLOG: f64 = 1e-38;
82    let rd = ctx.thermal_at(n.d_p, n.drain, n.gdpr, n.temp);
83    let rs = ctx.thermal_at(n.s_p, n.source, n.gspr, n.temp);
84    let sid = if n.nlev < 3 {
85        2.0 / 3.0 * n.gm.abs()
86    } else {
87        let vgd = n.vgs - n.vds;
88        let vgst = (if n.mode == 1.0 { n.vgs } else { vgd }) - n.typ * n.von;
89        if vgst > 0.0 {
90            let alpha = if vgst <= n.vds * n.mode {
91                0.0 // saturation
92            } else {
93                1.0 - (n.vds * n.mode / (n.typ * n.vdsat))
94            };
95            let beta = n.t_transconductance * n.w / n.eff_l;
96            2.0 / 3.0 * beta * vgst * (1.0 + alpha + alpha * alpha) / (1.0 + alpha) * n.gdsnoi
97        } else {
98            0.0 // subthreshold
99        }
100    };
101    let id = ctx.thermal_at(n.d_p, n.s_p, sid, n.temp);
102    let id_af = || f64::exp(n.af * f64::ln(f64::max(n.cd.abs(), N_MINLOG)));
103    let flicker = match n.nlev {
104        0 => ctx.flicker(
105            n.d_p,
106            n.s_p,
107            n.kf * id_af() / (n.eff_l * n.eff_l * n.cox),
108            1.0,
109        ),
110        1 => ctx.flicker(n.d_p, n.s_p, n.kf * id_af() / (n.w * n.eff_l * n.cox), 1.0),
111        2 | 3 => ctx.flicker(
112            n.d_p,
113            n.s_p,
114            n.kf * n.gm * n.gm / (n.w * n.eff_l * n.cox),
115            n.af,
116        ),
117        _ => ctx.flicker(n.d_p, n.s_p, 1.0, 0.0),
118    };
119    vec![rd, rs, id, flicker, rd + rs + id + flicker]
120}
121
122/// `.model … nmos(level=1 …)/pmos(level=1 …)` parameters, 3f5 names
123/// and defaults (`mos1.c` keyword table; defaults from `mos1set.c`).
124/// `Option` marks parameters whose *given-ness* changes the
125/// preprocessing in `MOS1temp` (e.g. `KP` computed from `U0`/`TOX`,
126/// `VTO`/`PHI`/`GAMMA` from `NSUB`, `CBD` vs `CJ·AD`).
127#[derive(Clone, Debug)]
128pub struct Mos1Model {
129    /// `NMOS` (+1) or `PMOS` (−1).
130    pub nmos: bool,
131    /// `VTO`/`VT0`: threshold voltage; default 0, or computed from
132    /// `NSUB`/`NSS`/`TPG` when those are given.
133    pub vto: Option<f64>,
134    /// `KP`: transconductance; default 2e-5, or `U0·COX` with `TOX`.
135    pub kp: Option<f64>,
136    /// `GAMMA`: bulk threshold; default 0, or computed from `NSUB`.
137    pub gamma: Option<f64>,
138    /// `PHI`: surface potential; default 0.6, or from `NSUB`.
139    pub phi: Option<f64>,
140    /// `LAMBDA`: channel-length modulation, default 0.
141    pub lambda: f64,
142    /// `RD`: drain ohmic resistance, default 0.
143    pub rd: Option<f64>,
144    /// `RS`: source ohmic resistance, default 0.
145    pub rs: Option<f64>,
146    /// `CBD`: zero-bias B-D junction capacitance, default 0.
147    pub cbd: Option<f64>,
148    /// `CBS`: zero-bias B-S junction capacitance, default 0.
149    pub cbs: Option<f64>,
150    /// `IS`: bulk junction saturation current, default 1e-14.
151    pub is: f64,
152    /// `PB`: bulk junction potential, default 0.8.
153    pub pb: f64,
154    /// `CGSO`/`CGDO`/`CGBO`: overlap capacitances per meter,
155    /// default 0.
156    pub cgso: f64,
157    pub cgdo: f64,
158    pub cgbo: f64,
159    /// `RSH`: drain/source diffusion sheet resistance, default 0.
160    pub rsh: Option<f64>,
161    /// `CJ`: bottom junction capacitance per m², default 0.
162    pub cj: Option<f64>,
163    /// `MJ`: bottom grading coefficient, default 0.5.
164    pub mj: f64,
165    /// `CJSW`: sidewall capacitance per meter, default 0.
166    pub cjsw: Option<f64>,
167    /// `MJSW`: sidewall grading coefficient, default 0.5.
168    pub mjsw: f64,
169    /// `JS`: bulk junction saturation current per m², default 0.
170    pub js: f64,
171    /// `TOX`: oxide thickness, meters.
172    pub tox: Option<f64>,
173    /// `LD`: lateral diffusion, default 0.
174    pub ld: f64,
175    /// `U0`/`UO`: surface mobility, cm²/V·s; default 600.
176    pub u0: Option<f64>,
177    /// `FC`: forward-bias depletion capacitance coefficient,
178    /// default 0.5.
179    pub fc: f64,
180    /// `NSUB`: substrate doping, 1/cm³.
181    pub nsub: Option<f64>,
182    /// `TPG`: gate material type (+1 opposite of substrate, −1 same,
183    /// 0 aluminum); default 1 when `NSUB` is given.
184    pub tpg: Option<f64>,
185    /// `NSS`: surface state density, 1/cm².
186    pub nss: Option<f64>,
187    /// `KF`/`AF` (noise only).
188    pub kf: f64,
189    pub af: f64,
190    /// `NLEV` (ngspice-47 only, default 2): noise equation selector
191    /// — `< 3` channel thermal `4kT·(2/3)|gm|`, `3` the
192    /// `GDSNOI`-scaled form; for the MOSFETs also the flicker form
193    /// (0/1/2–3).
194    pub nlev: i64,
195    /// `GDSNOI` (ngspice-47 only, default 1): channel noise
196    /// coefficient of `NLEV = 3`.
197    pub gdsnoi: f64,
198    /// `TNOM` in kelvin.
199    pub tnom: Option<f64>,
200}
201
202impl Mos1Model {
203    pub fn new(nmos: bool) -> Self {
204        Mos1Model {
205            nmos,
206            vto: None,
207            kp: None,
208            gamma: None,
209            phi: None,
210            lambda: 0.0,
211            rd: None,
212            rs: None,
213            cbd: None,
214            cbs: None,
215            is: 1e-14,
216            pb: 0.8,
217            cgso: 0.0,
218            cgdo: 0.0,
219            cgbo: 0.0,
220            rsh: None,
221            cj: None,
222            mj: 0.5,
223            cjsw: None,
224            mjsw: 0.5,
225            js: 0.0,
226            tox: None,
227            ld: 0.0,
228            u0: None,
229            fc: 0.5,
230            nsub: None,
231            tpg: None,
232            nss: None,
233            kf: 0.0,
234            af: 1.0,
235            nlev: 2,
236            gdsnoi: 1.0,
237            tnom: None,
238        }
239    }
240
241    /// Does the drain (source) side get a prime node? (`mos1set.c`:
242    /// `drainResistance != 0 || (sheetResistance != 0 && squares != 0)`.)
243    pub(crate) fn needs_prime(&self, squares: f64) -> bool {
244        self.rd.unwrap_or(0.0) != 0.0 || (self.rsh.unwrap_or(0.0) != 0.0 && squares != 0.0)
245    }
246}
247
248fn egfet(t: f64) -> f64 {
249    1.16 - (7.02e-4 * t * t) / (t + 1108.0)
250}
251
252/// Instance geometry as the setup pass sees it (`mos1set.c`
253/// instance defaults; `l`/`w`/`ad`/`as` fall back to the
254/// `.options defl/defw/defad/defas` values).
255#[derive(Clone, Copy, Debug)]
256pub(crate) struct MosGeom {
257    pub l: f64,
258    pub w: f64,
259    pub ad: f64,
260    pub as_: f64,
261    pub pd: f64,
262    pub ps: f64,
263    pub nrd: f64,
264    pub nrs: f64,
265}
266
267/// Model- and temperature-derived values (`MOS1temp`, both the
268/// model-level preprocessing and the per-instance block).
269pub(crate) struct Mos1Temp {
270    /// The instance temperature, kelvin (`*temp`: the card's
271    /// `TEMP=` or `CKTtemp`) — the thermal noise generators' T
272    /// (ngspice-47 `NevalSrcInstanceTemp`).
273    pub temp: f64,
274    pub type_: f64, // +1 NMOS, -1 PMOS
275    pub vt: f64,
276    // Model copies the load needs.
277    pub lat_diff: f64,
278    /// `KF`/`AF`: flicker-noise coefficient and exponent.
279    pub kf: f64,
280    pub af: f64,
281    /// `NLEV`/`GDSNOI` (noise only).
282    pub nlev: i64,
283    pub gdsnoi: f64,
284    pub cgso: f64,
285    pub cgdo: f64,
286    pub cgbo: f64,
287    pub oxide_cap_factor: f64,
288    pub gamma: f64,
289    pub lambda: f64,
290    pub mj: f64,
291    pub mjsw: f64,
292    // Temperature-adjusted values.
293    pub t_transconductance: f64,
294    pub t_phi: f64,
295    pub t_vbi: f64,
296    pub t_vto: f64,
297    pub t_sat_cur: f64,
298    pub t_sat_cur_dens: f64,
299    pub t_bulk_pot: f64,
300    pub t_dep_cap: f64,
301    pub drain_vcrit: f64,
302    pub source_vcrit: f64,
303    // Zero-bias junction capacitances and their linearization past
304    // `FC·PB` (`f2`/`f3`/`f4`, drain and source sides).
305    pub cbd: f64,
306    pub cbdsw: f64,
307    pub cbs: f64,
308    pub cbssw: f64,
309    pub f2d: f64,
310    pub f3d: f64,
311    pub f4d: f64,
312    pub f2s: f64,
313    pub f3s: f64,
314    pub f4s: f64,
315    pub drain_conductance: f64,
316    pub source_conductance: f64,
317    pub notes: Vec<String>,
318}
319
320impl Mos1Temp {
321    pub fn new(
322        name: &str,
323        m: &Mos1Model,
324        g: &MosGeom,
325        temp: f64,
326        default_tnom: f64,
327    ) -> Result<Self, String> {
328        let mut notes = Vec::new();
329        let typ = if m.nmos { 1.0 } else { -1.0 };
330        let tnom = m.tnom.unwrap_or(default_tnom);
331
332        let fact1 = tnom / REFTEMP;
333        let vtnom = tnom * KOVERQ;
334        let kt1 = BOLTZ * tnom;
335        let egfet1 = egfet(tnom);
336        let arg1 = -egfet1 / (kt1 + kt1) + 1.1150877 / (BOLTZ * (REFTEMP + REFTEMP));
337        let pbfact1 = -2.0 * vtnom * (1.5 * f64::ln(fact1) + CHARGE * arg1);
338
339        // Model parameter preprocessing: KP from U0·COX, and
340        // PHI/GAMMA/VTO from the process description when NSUB is
341        // given (all only when the parameter itself was not).
342        let mut kp = m.kp;
343        let mut phi = m.phi;
344        let mut gamma = m.gamma;
345        let mut vto = m.vto;
346        let oxide_cap_factor = match m.tox {
347            Some(tox) if tox != 0.0 => {
348                let ocf = 3.9 * 8.854214871e-12 / tox;
349                if kp.is_none() {
350                    let u0 = m.u0.unwrap_or(600.0);
351                    kp = Some(u0 * ocf * 1e-4 /* m²/cm² */);
352                }
353                if let Some(nsub) = m.nsub {
354                    if nsub * 1e6 /* cm³/m³ */ > 1.45e16 {
355                        if phi.is_none() {
356                            phi = Some(f64::max(0.1, 2.0 * vtnom * f64::ln(nsub * 1e6 / 1.45e16)));
357                        }
358                        let phi_v = phi.unwrap_or(0.6);
359                        let fermis = typ * 0.5 * phi_v;
360                        let tpg = m.tpg.unwrap_or(1.0);
361                        let mut wkfng = 3.2;
362                        if tpg != 0.0 {
363                            let fermig = typ * tpg * 0.5 * egfet1;
364                            wkfng = 3.25 + 0.5 * egfet1 - fermig;
365                        }
366                        let wkfngs = wkfng - (3.25 + 0.5 * egfet1 + fermis);
367                        if gamma.is_none() {
368                            gamma = Some(
369                                f64::sqrt(2.0 * 11.70 * 8.854214871e-12 * CHARGE * nsub * 1e6)
370                                    / ocf,
371                            );
372                        }
373                        if vto.is_none() {
374                            let nss = m.nss.unwrap_or(0.0);
375                            let vfb = wkfngs - nss * 1e4 /* cm²/m² */ * CHARGE / ocf;
376                            vto = Some(vfb + typ * (gamma.unwrap_or(0.0) * phi_v.sqrt() + phi_v));
377                        }
378                    } else {
379                        return Err(format!("{name}: Nsub < Ni"));
380                    }
381                }
382                ocf
383            }
384            _ => 0.0,
385        };
386        let kp = kp.unwrap_or(2e-5);
387        let phi = phi.unwrap_or(0.6);
388        let gamma = gamma.unwrap_or(0.0);
389        let vto = vto.unwrap_or(0.0);
390
391        // Per-instance temperature block.
392        let vt = temp * KOVERQ;
393        let ratio = temp / tnom;
394        let fact2 = temp / REFTEMP;
395        let kt = temp * BOLTZ;
396        let egfet_t = egfet(temp);
397        let arg = -egfet_t / (kt + kt) + 1.1150877 / (BOLTZ * (REFTEMP + REFTEMP));
398        let pbfact = -2.0 * vt * (1.5 * f64::ln(fact2) + CHARGE * arg);
399
400        if g.l - 2.0 * m.ld <= 0.0 {
401            notes.push(format!("{name}: effective channel length less than zero"));
402        }
403        let ratio4 = ratio * ratio.sqrt();
404        let t_transconductance = kp / ratio4;
405        let phio = (phi - pbfact1) / fact1;
406        let t_phi = fact2 * phio + pbfact;
407        let t_vbi =
408            vto - typ * (gamma * phi.sqrt()) + 0.5 * (egfet1 - egfet_t) + typ * 0.5 * (t_phi - phi);
409        let t_vto = t_vbi + typ * gamma * t_phi.sqrt();
410        let t_sat_cur = m.is * f64::exp(-egfet_t / vt + egfet1 / vtnom);
411        let t_sat_cur_dens = m.js * f64::exp(-egfet_t / vt + egfet1 / vtnom);
412
413        let pbo = (m.pb - pbfact1) / fact1;
414        let gmaold = (m.pb - pbo) / pbo;
415        let mut capfact = 1.0 / (1.0 + m.mj * (4e-4 * (tnom - REFTEMP) - gmaold));
416        let mut t_cbd = m.cbd.unwrap_or(0.0) * capfact;
417        let mut t_cbs = m.cbs.unwrap_or(0.0) * capfact;
418        let mut t_cj = m.cj.unwrap_or(0.0) * capfact;
419        capfact = 1.0 / (1.0 + m.mjsw * (4e-4 * (tnom - REFTEMP) - gmaold));
420        let mut t_cjsw = m.cjsw.unwrap_or(0.0) * capfact;
421        let t_bulk_pot = fact2 * pbo + pbfact;
422        let gmanew = (t_bulk_pot - pbo) / pbo;
423        capfact = 1.0 + m.mj * (4e-4 * (temp - REFTEMP) - gmanew);
424        t_cbd *= capfact;
425        t_cbs *= capfact;
426        t_cj *= capfact;
427        capfact = 1.0 + m.mjsw * (4e-4 * (temp - REFTEMP) - gmanew);
428        t_cjsw *= capfact;
429        let t_dep_cap = m.fc * t_bulk_pot;
430
431        let (drain_vcrit, source_vcrit) = if t_sat_cur_dens == 0.0 || g.ad == 0.0 || g.as_ == 0.0 {
432            let v = vt * f64::ln(vt / (ROOT2 * t_sat_cur));
433            (v, v)
434        } else {
435            (
436                vt * f64::ln(vt / (ROOT2 * t_sat_cur_dens * g.ad)),
437                vt * f64::ln(vt / (ROOT2 * t_sat_cur_dens * g.as_)),
438            )
439        };
440
441        // Zero-bias junction capacitances and the `f2`/`f3`/`f4`
442        // coefficients that continue the charge past `FC·PB`.
443        let czbd = if m.cbd.is_some() {
444            t_cbd
445        } else if m.cj.is_some() {
446            t_cj * g.ad
447        } else {
448            0.0
449        };
450        let czbdsw = if m.cjsw.is_some() { t_cjsw * g.pd } else { 0.0 };
451        let arg = 1.0 - m.fc;
452        let sarg = f64::exp(-m.mj * f64::ln(arg));
453        let sargsw = f64::exp(-m.mjsw * f64::ln(arg));
454        let f2d = czbd * (1.0 - m.fc * (1.0 + m.mj)) * sarg / arg
455            + czbdsw * (1.0 - m.fc * (1.0 + m.mjsw)) * sargsw / arg;
456        let f3d =
457            czbd * m.mj * sarg / arg / t_bulk_pot + czbdsw * m.mjsw * sargsw / arg / t_bulk_pot;
458        let f4d = czbd * t_bulk_pot * (1.0 - arg * sarg) / (1.0 - m.mj)
459            + czbdsw * t_bulk_pot * (1.0 - arg * sargsw) / (1.0 - m.mjsw)
460            - f3d / 2.0 * (t_dep_cap * t_dep_cap)
461            - t_dep_cap * f2d;
462        let czbs = if m.cbs.is_some() {
463            t_cbs
464        } else if m.cj.is_some() {
465            t_cj * g.as_
466        } else {
467            0.0
468        };
469        let czbssw = if m.cjsw.is_some() { t_cjsw * g.ps } else { 0.0 };
470        let f2s = czbs * (1.0 - m.fc * (1.0 + m.mj)) * sarg / arg
471            + czbssw * (1.0 - m.fc * (1.0 + m.mjsw)) * sargsw / arg;
472        let f3s =
473            czbs * m.mj * sarg / arg / t_bulk_pot + czbssw * m.mjsw * sargsw / arg / t_bulk_pot;
474        let f4s = czbs * t_bulk_pot * (1.0 - arg * sarg) / (1.0 - m.mj)
475            + czbssw * t_bulk_pot * (1.0 - arg * sargsw) / (1.0 - m.mjsw)
476            - f3s / 2.0 * (t_dep_cap * t_dep_cap)
477            - t_dep_cap * f2s;
478
479        let series = |r: Option<f64>, squares: f64| match (r, m.rsh) {
480            (Some(r), _) => {
481                if r != 0.0 {
482                    1.0 / r
483                } else {
484                    0.0
485                }
486            }
487            (None, Some(rsh)) => {
488                if rsh != 0.0 {
489                    1.0 / (rsh * squares)
490                } else {
491                    0.0
492                }
493            }
494            (None, None) => 0.0,
495        };
496        let drain_conductance = series(m.rd, g.nrd);
497        let source_conductance = series(m.rs, g.nrs);
498
499        Ok(Mos1Temp {
500            temp,
501            type_: typ,
502            vt,
503            lat_diff: m.ld,
504            kf: m.kf,
505            af: m.af,
506            nlev: m.nlev,
507            gdsnoi: m.gdsnoi,
508            cgso: m.cgso,
509            cgdo: m.cgdo,
510            cgbo: m.cgbo,
511            oxide_cap_factor,
512            gamma,
513            lambda: m.lambda,
514            mj: m.mj,
515            mjsw: m.mjsw,
516            t_transconductance,
517            t_phi,
518            t_vbi,
519            t_vto,
520            t_sat_cur,
521            t_sat_cur_dens,
522            t_bulk_pot,
523            t_dep_cap,
524            drain_vcrit,
525            source_vcrit,
526            cbd: czbd,
527            cbdsw: czbdsw,
528            cbs: czbs,
529            cbssw: czbssw,
530            f2d,
531            f3d,
532            f4d,
533            f2s,
534            f3s,
535            f4s,
536            drain_conductance,
537            source_conductance,
538            notes,
539        })
540    }
541}
542
543/// `DEVlimvds` (`devsup.c`): limit the per-iteration change of VDS.
544pub(crate) fn limvds(vnew: f64, vold: f64) -> f64 {
545    if vold >= 3.5 {
546        if vnew > vold {
547            f64::min(vnew, 3.0 * vold + 2.0)
548        } else if vnew < 3.5 {
549            f64::max(vnew, 2.0)
550        } else {
551            vnew
552        }
553    } else if vnew > vold {
554        f64::min(vnew, 4.0)
555    } else {
556        f64::max(vnew, -0.5)
557    }
558}
559
560/// `DEVfetlim` (`devsup.c`): limit the per-iteration change of FET
561/// gate voltages around the threshold `vto`.
562pub(crate) fn fetlim(vnew: f64, vold: f64, vto: f64) -> f64 {
563    let vtsthi = f64::abs(2.0 * (vold - vto)) + 2.0;
564    let vtstlo = vtsthi / 2.0 + 2.0;
565    let vtox = vto + 3.5;
566    let delv = vnew - vold;
567    if vold >= vto {
568        if vold >= vtox {
569            if delv <= 0.0 {
570                // Going off.
571                if vnew >= vtox {
572                    if -delv > vtstlo { vold - vtstlo } else { vnew }
573                } else {
574                    f64::max(vnew, vto + 2.0)
575                }
576            } else {
577                // Staying on.
578                if delv >= vtsthi { vold + vtsthi } else { vnew }
579            }
580        } else if delv <= 0.0 {
581            f64::max(vnew, vto - 0.5)
582        } else {
583            f64::min(vnew, vto + 4.0)
584        }
585    } else if delv <= 0.0 {
586        if -delv > vtsthi { vold - vtsthi } else { vnew }
587    } else {
588        let vtemp = vto + 0.5;
589        if vnew <= vtemp {
590            if delv > vtstlo { vold + vtstlo } else { vnew }
591        } else {
592            vtemp
593        }
594    }
595}
596
597/// `DEVqmeyer` (ngspice `devsup.c`): Meyer gate capacitances.
598/// Returns half of the non-constant portion of (capgs, capgd,
599/// capgb) at the current bias; the caller averages with the
600/// previous timepoint and adds the constant overlap part.
601///
602/// This is the **ngspice** version, not the 3f5's: `vdsat` is
603/// clamped to `MAGIC_VDS` (25 mV) and the weak-inversion region
604/// splits the gate capacitance between source and drain with the
605/// saturation smoothing (`corpus/KNOWN_DIFFS.md`).
606pub(crate) fn qmeyer(
607    vgs: f64,
608    vgd: f64,
609    _vgb: f64,
610    von: f64,
611    vdsat: f64,
612    phi: f64,
613    cox: f64,
614) -> (f64, f64, f64) {
615    const MAGIC_VDS: f64 = 0.025;
616    let vgst = vgs - von;
617    let vdsat = vdsat.max(MAGIC_VDS);
618    if vgst <= -phi {
619        (0.0, 0.0, cox / 2.0)
620    } else if vgst <= -phi / 2.0 {
621        (0.0, 0.0, -vgst * cox / (2.0 * phi))
622    } else if vgst <= 0.0 {
623        let capgb = -vgst * cox / (2.0 * phi);
624        let mut capgs = vgst * cox / (1.5 * phi) + cox / 3.0;
625        let vds = vgs - vgd;
626        let capgd;
627        if vds >= vdsat {
628            capgd = 0.0;
629        } else {
630            let vddif = 2.0 * vdsat - vds;
631            let vddif1 = vdsat - vds;
632            let vddif2 = vddif * vddif;
633            capgd = capgs * (1.0 - vdsat * vdsat / vddif2);
634            capgs *= 1.0 - vddif1 * vddif1 / vddif2;
635        }
636        (capgs, capgd, capgb)
637    } else {
638        let vds = vgs - vgd;
639        if vdsat <= vds {
640            (cox / 3.0, 0.0, 0.0)
641        } else {
642            let vddif = 2.0 * vdsat - vds;
643            let vddif1 = vdsat - vds;
644            let vddif2 = vddif * vddif;
645            (
646                cox * (1.0 - vddif1 * vddif1 / vddif2) / 3.0,
647                cox * (1.0 - vdsat * vdsat / vddif2) / 3.0,
648                0.0,
649            )
650        }
651    }
652}
653
654// State offsets (`mos1defs.h`).
655const VBD: usize = 0;
656const VBS: usize = 1;
657const VGS: usize = 2;
658const VDS: usize = 3;
659const CAPGS: usize = 4;
660const QGS: usize = 5;
661const CQGS: usize = 6;
662const CAPGD: usize = 7;
663const QGD: usize = 8;
664const CQGD: usize = 9;
665const CAPGB: usize = 10;
666const QGB: usize = 11;
667const CQGB: usize = 12;
668const QBD: usize = 13;
669const CQBD: usize = 14;
670const QBS: usize = 15;
671const CQBS: usize = 16;
672pub(crate) const MOS1_NUM_STATES: usize = 17;
673
674// Slot order (matches `mos1set.c`'s TSTALLOC list).
675const D_D: usize = 0;
676const G_G: usize = 1;
677const S_S: usize = 2;
678const B_B: usize = 3;
679const DP_DP: usize = 4;
680const SP_SP: usize = 5;
681const D_DP: usize = 6;
682const G_B: usize = 7;
683const G_DP: usize = 8;
684const G_SP: usize = 9;
685const S_SP: usize = 10;
686const B_DP: usize = 11;
687const B_SP: usize = 12;
688const DP_SP: usize = 13;
689const DP_D: usize = 14;
690const B_G: usize = 15;
691const DP_G: usize = 16;
692const SP_G: usize = 17;
693const SP_S: usize = 18;
694const DP_B: usize = 19;
695const SP_B: usize = 20;
696const SP_DP: usize = 21;
697
698/// The small-signal conductances of a four-terminal MOS instance as
699/// its pole-zero load reads them; `xnrm`/`xrev` are the C's mode
700/// flags (1/0 in normal mode, 0/1 with source and drain swapped).
701pub(crate) struct MosPzConductances {
702    pub xnrm: f64,
703    pub xrev: f64,
704    pub gm: f64,
705    pub gds: f64,
706    pub gmbs: f64,
707    pub gbd: f64,
708    pub gbs: f64,
709    pub gdpr: f64,
710    pub gspr: f64,
711}
712
713/// The conductance half of every four-terminal MOS pole-zero load —
714/// identical, statement for statement, in `mos1pzld.c`,
715/// `mos2pzld.c`, `mos3pzld.c`, `b1pzld.c` and `b2pzld.c` (and in the
716/// AC loads). `s` is the 22-slot TSTALLOC list the five devices
717/// share (`D_D` … `SP_DP`).
718pub(crate) fn mos_pz_conductances(
719    ctx: &mut crate::matrix::PzCtx,
720    s: &[Slot; 22],
721    c: &MosPzConductances,
722) {
723    let (xnrm, xrev) = (c.xnrm, c.xrev);
724    let (gm, gds, gmbs, gbd, gbs) = (c.gm, c.gds, c.gmbs, c.gbd, c.gbs);
725    let (gdpr, gspr) = (c.gdpr, c.gspr);
726    ctx.add_g(s[D_D], gdpr);
727    ctx.add_g(s[S_S], gspr);
728    ctx.add_g(s[B_B], gbd + gbs);
729    ctx.add_g(s[DP_DP], gdpr + gds + gbd + xrev * (gm + gmbs));
730    ctx.add_g(s[SP_SP], gspr + gds + gbs + xnrm * (gm + gmbs));
731    ctx.add_g(s[D_DP], -gdpr);
732    ctx.add_g(s[S_SP], -gspr);
733    ctx.add_g(s[B_DP], -gbd);
734    ctx.add_g(s[B_SP], -gbs);
735    ctx.add_g(s[DP_D], -gdpr);
736    ctx.add_g(s[DP_G], (xnrm - xrev) * gm);
737    ctx.add_g(s[DP_B], -gbd + (xnrm - xrev) * gmbs);
738    ctx.add_g(s[DP_SP], -gds - xnrm * (gm + gmbs));
739    ctx.add_g(s[SP_G], -(xnrm - xrev) * gm);
740    ctx.add_g(s[SP_S], -gspr);
741    ctx.add_g(s[SP_B], -gbs - (xnrm - xrev) * gmbs);
742    ctx.add_g(s[SP_DP], -gds - xrev * (gm + gmbs));
743}
744
745/// The Meyer capacitances of a level 1/2/3 MOS at the operating
746/// point: `capgs`/`capgd`/`capgb` already doubled and with the
747/// overlaps added (`2·state0 + C*O·W` or `·Leff`), `capbd`/`capbs`
748/// the junction capacitances the `INITSMSIG` load left.
749pub(crate) struct MeyerPzCaps {
750    pub capgs: f64,
751    pub capgd: f64,
752    pub capgb: f64,
753    pub capbd: f64,
754    pub capbs: f64,
755}
756
757/// `MOS1pzLoad` (`mos1pzld.c`; `mos2pzld.c` and `mos3pzld.c` are the
758/// same file renamed): the AC stamps with `s` in place of `jω` —
759/// every capacitance enters as `s·C` on both the real and imaginary
760/// halves.
761pub(crate) fn meyer_pz_load(
762    ctx: &mut crate::matrix::PzCtx,
763    s: &[Slot; 22],
764    caps: &MeyerPzCaps,
765    g: &MosPzConductances,
766) {
767    let xgs = caps.capgs;
768    let xgd = caps.capgd;
769    let xgb = caps.capgb;
770    let xbd = caps.capbd;
771    let xbs = caps.capbs;
772    ctx.add_cap(s[G_G], xgd + xgs + xgb);
773    ctx.add_cap(s[B_B], xgb + xbd + xbs);
774    ctx.add_cap(s[DP_DP], xgd + xbd);
775    ctx.add_cap(s[SP_SP], xgs + xbs);
776    ctx.add_cap(s[G_B], -xgb);
777    ctx.add_cap(s[G_DP], -xgd);
778    ctx.add_cap(s[G_SP], -xgs);
779    ctx.add_cap(s[B_G], -xgb);
780    ctx.add_cap(s[B_DP], -xbd);
781    ctx.add_cap(s[B_SP], -xbs);
782    ctx.add_cap(s[DP_G], -xgd);
783    ctx.add_cap(s[DP_B], -xbd);
784    ctx.add_cap(s[SP_G], -xgs);
785    ctx.add_cap(s[SP_B], -xbs);
786    mos_pz_conductances(ctx, s, g);
787}
788
789pub(crate) struct Mos1I {
790    drain: Var,
791    gate: Var,
792    source: Var,
793    bulk: Var,
794    d_p: Var,
795    s_p: Var,
796    t: Mos1Temp,
797    geom: MosGeom,
798    off: bool,
799    ic_vds: f64,
800    ic_vgs: f64,
801    ic_vbs: f64,
802    ic_given: crate::devices::IcGiven,
803    st: usize,
804    s: [Slot; 22],
805    // Values that persist between iterations (`MOS1instance`).
806    mode: f64, // +1 normal, −1 source/drain swapped
807    von: f64,
808    vdsat: f64,
809    gm: f64,
810    gds: f64,
811    gmbs: f64,
812    gbd: f64,
813    gbs: f64,
814    capbd: f64,
815    capbs: f64,
816    cd: f64,
817    cbs: f64,
818    cbd: f64,
819    /// `.disto` Taylor coefficients (`MOS1dSetup`).
820    dist: MosDistoCoeffs,
821}
822
823impl Mos1I {
824    #[allow(clippy::too_many_arguments)]
825    pub fn new(
826        nodes: [Var; 4],  // d g s b
827        primes: [Var; 2], // d' s'
828        t: Mos1Temp,
829        geom: MosGeom,
830        off: bool,
831        ic: (f64, f64, f64), // (vds, vgs, vbs)
832        st: usize,
833    ) -> Self {
834        Mos1I {
835            drain: nodes[0],
836            gate: nodes[1],
837            source: nodes[2],
838            bulk: nodes[3],
839            d_p: primes[0],
840            s_p: primes[1],
841            t,
842            geom,
843            off,
844            ic_vds: ic.0,
845            ic_vgs: ic.1,
846            ic_vbs: ic.2,
847            ic_given: crate::devices::IcGiven::ALL,
848            st,
849            s: [TRASH; 22],
850            mode: 1.0,
851            von: 0.0,
852            vdsat: 0.0,
853            gm: 0.0,
854            gds: 0.0,
855            gmbs: 0.0,
856            gbd: 0.0,
857            gbs: 0.0,
858            capbd: 0.0,
859            capbs: 0.0,
860            cd: 0.0,
861            cbs: 0.0,
862            cbd: 0.0,
863            dist: MosDistoCoeffs::default(),
864        }
865    }
866}
867
868impl crate::devices::Device for Mos1I {
869    fn par_load(&self) -> bool {
870        true
871    }
872
873    /// `MOS1getic` (`mos1ic.c`): VBS, VDS, VGS not given
874    /// on the card from the external nodes.
875    fn set_ic(&mut self, rhs: &[f64]) {
876        use crate::devices::getic;
877        let g = self.ic_given;
878        getic(g, 2, &mut self.ic_vbs, rhs, self.bulk, self.source);
879        getic(g, 0, &mut self.ic_vds, rhs, self.drain, self.source);
880        getic(g, 1, &mut self.ic_vgs, rhs, self.gate, self.source);
881    }
882
883    fn set_ic_given(&mut self, given: crate::devices::IcGiven) {
884        self.ic_given = given;
885    }
886
887    /// `Mos1convTest`: predicted vs computed drain and bulk
888    /// currents, evaluated at the fresh solution (`CKTrhs`).
889    fn conv_test(&self, ctx: &crate::matrix::ConvCtx) -> bool {
890        let typ = self.t.type_;
891        let st = self.st;
892        let vbs = typ * (ctx.rhs[self.bulk] - ctx.rhs[self.s_p]);
893        let vgs = typ * (ctx.rhs[self.gate] - ctx.rhs[self.s_p]);
894        let vds = typ * (ctx.rhs[self.d_p] - ctx.rhs[self.s_p]);
895        let vbd = vbs - vds;
896        let vgd = vgs - vds;
897        let vgdo = ctx.states.get(0, st + VGS) - ctx.states.get(0, st + VDS);
898        let delvbs = vbs - ctx.states.get(0, st + VBS);
899        let delvbd = vbd - ctx.states.get(0, st + VBD);
900        let delvgs = vgs - ctx.states.get(0, st + VGS);
901        let delvds = vds - ctx.states.get(0, st + VDS);
902        let delvgd = vgd - vgdo;
903        let cdhat = if self.mode >= 0.0 {
904            self.cd - self.gbd * delvbd + self.gmbs * delvbs + self.gm * delvgs + self.gds * delvds
905        } else {
906            self.cd - (self.gbd - self.gmbs) * delvbd - self.gm * delvgd + self.gds * delvds
907        };
908        let cbhat = self.cbs + self.cbd + self.gbd * delvbd + self.gbs * delvbs;
909        let tol = ctx.reltol * f64::max(cdhat.abs(), self.cd.abs()) + ctx.abstol;
910        if (cdhat - self.cd).abs() >= tol {
911            return false;
912        }
913        let cb = self.cbs + self.cbd;
914        let tol = ctx.reltol * f64::max(cbhat.abs(), cb.abs()) + ctx.abstol;
915        (cbhat - cb).abs() <= tol
916    }
917
918    fn alloc(&mut self, mb: &mut MatrixBuilder) {
919        let (d, g, s, b) = (self.drain, self.gate, self.source, self.bulk);
920        let (dp, sp) = (self.d_p, self.s_p);
921        self.s = [
922            mb.slot(d, d),
923            mb.slot(g, g),
924            mb.slot(s, s),
925            mb.slot(b, b),
926            mb.slot(dp, dp),
927            mb.slot(sp, sp),
928            mb.slot(d, dp),
929            mb.slot(g, b),
930            mb.slot(g, dp),
931            mb.slot(g, sp),
932            mb.slot(s, sp),
933            mb.slot(b, dp),
934            mb.slot(b, sp),
935            mb.slot(dp, sp),
936            mb.slot(dp, d),
937            mb.slot(b, g),
938            mb.slot(dp, g),
939            mb.slot(sp, g),
940            mb.slot(sp, s),
941            mb.slot(dp, b),
942            mb.slot(sp, b),
943            mb.slot(sp, dp),
944        ];
945    }
946
947    #[allow(clippy::too_many_lines)]
948    fn load(&mut self, ctx: &mut LoadCtx) {
949        let t = &self.t;
950        let typ = t.type_;
951        let vt = t.vt;
952        let st = self.st;
953
954        let eff_l = self.geom.l - 2.0 * t.lat_diff;
955        let (drain_sat_cur, source_sat_cur) =
956            if t.t_sat_cur_dens == 0.0 || self.geom.ad == 0.0 || self.geom.as_ == 0.0 {
957                (t.t_sat_cur, t.t_sat_cur)
958            } else {
959                (
960                    t.t_sat_cur_dens * self.geom.ad,
961                    t.t_sat_cur_dens * self.geom.as_,
962                )
963            };
964        let gate_source_overlap_cap = t.cgso * self.geom.w;
965        let gate_drain_overlap_cap = t.cgdo * self.geom.w;
966        let gate_bulk_overlap_cap = t.cgbo * eff_l;
967        let beta = t.t_transconductance * self.geom.w / eff_l;
968        let oxide_cap = t.oxide_cap_factor * eff_l * self.geom.w;
969
970        // Bias resolution (the C's initialization ladder). `Check`
971        // starts at 1; only `pnjlim` rewrites it.
972        let mut check = true;
973        let (mut vbs, mut vgs, mut vds);
974        let predictor = matches!(ctx.mode, InitMode::Pred | InitMode::Tran);
975        let general = matches!(ctx.mode, InitMode::Float | InitMode::Smsig)
976            || (ctx.mode == InitMode::Fix && !self.off);
977        if predictor || general {
978            if predictor {
979                // Predictor step: copy state1 → state0 and
980                // extrapolate the junction voltages.
981                let xfact = ctx.pred_xfact;
982                let mut pred = |o: usize| {
983                    let s1 = ctx.states.get(1, st + o);
984                    ctx.states.set(0, st + o, s1);
985                    (1.0 + xfact) * s1 - xfact * ctx.states.get(2, st + o)
986                };
987                vbs = pred(VBS);
988                vgs = pred(VGS);
989                vds = pred(VDS);
990                let vbd0 = ctx.states.get(0, st + VBS) - ctx.states.get(0, st + VDS);
991                ctx.states.set(0, st + VBD, vbd0);
992            } else {
993                vbs = typ * (ctx.sol[self.bulk] - ctx.sol[self.s_p]);
994                vgs = typ * (ctx.sol[self.gate] - ctx.sol[self.s_p]);
995                vds = typ * (ctx.sol[self.d_p] - ctx.sol[self.s_p]);
996            }
997            let mut vbd = vbs - vds;
998            let mut vgd = vgs - vds;
999            let vgdo = ctx.states.get(0, st + VGS) - ctx.states.get(0, st + VDS);
1000
1001            // Limiting (`DEVfetlim`/`DEVlimvds`/`DEVpnjlim`).
1002            let von = typ * self.von;
1003            if ctx.states.get(0, st + VDS) >= 0.0 {
1004                vgs = fetlim(vgs, ctx.states.get(0, st + VGS), von);
1005                vds = vgs - vgd;
1006                vds = limvds(vds, ctx.states.get(0, st + VDS));
1007            } else {
1008                vgd = fetlim(vgd, vgdo, von);
1009                vds = vgs - vgd;
1010                // `CKTfixLimit` defaults to 0: always the symmetric
1011                // form.
1012                vds = -limvds(-vds, -ctx.states.get(0, st + VDS));
1013                vgs = vgd + vds;
1014            }
1015            if vds >= 0.0 {
1016                vbs = pnjlim(
1017                    vbs,
1018                    ctx.states.get(0, st + VBS),
1019                    vt,
1020                    t.source_vcrit,
1021                    &mut check,
1022                );
1023            } else {
1024                vbd = pnjlim(
1025                    vbd,
1026                    ctx.states.get(0, st + VBD),
1027                    vt,
1028                    t.drain_vcrit,
1029                    &mut check,
1030                );
1031                vbs = vbd + vds;
1032            }
1033        } else if ctx.mode == InitMode::Jct && !self.off {
1034            vds = typ * self.ic_vds;
1035            vgs = typ * self.ic_vgs;
1036            vbs = typ * self.ic_vbs;
1037            if vds == 0.0 && vgs == 0.0 && vbs == 0.0 && !(ctx.kind == AnKind::TranOp && ctx.uic) {
1038                vbs = -1.0;
1039                vgs = typ * t.t_vto;
1040                vds = 0.0;
1041            }
1042        } else {
1043            vbs = 0.0;
1044            vgs = 0.0;
1045            vds = 0.0;
1046        }
1047
1048        let vbd = vbs - vds;
1049        let vgd = vgs - vds;
1050        let vgb = vgs - vbs;
1051
1052        // Bulk-source and bulk-drain diodes, in the ngspice form:
1053        // flat leakage below −3·vt, exponential (clamped) above,
1054        // with gmin folded into the current (`corpus/KNOWN_DIFFS.md`).
1055        let (mut gbs, mut cbs);
1056        if vbs <= -3.0 * vt {
1057            gbs = ctx.gmin;
1058            cbs = gbs * vbs - source_sat_cur;
1059        } else {
1060            let evbs = f64::exp(f64::min(MAX_EXP_ARG, vbs / vt));
1061            gbs = source_sat_cur * evbs / vt + ctx.gmin;
1062            cbs = source_sat_cur * (evbs - 1.0) + ctx.gmin * vbs;
1063        }
1064        let (mut gbd, mut cbd);
1065        if vbd <= -3.0 * vt {
1066            gbd = ctx.gmin;
1067            cbd = gbd * vbd - drain_sat_cur;
1068        } else {
1069            let evbd = f64::exp(f64::min(MAX_EXP_ARG, vbd / vt));
1070            gbd = drain_sat_cur * evbd / vt + ctx.gmin;
1071            cbd = drain_sat_cur * (evbd - 1.0) + ctx.gmin * vbd;
1072        }
1073
1074        // Normal (vds ≥ 0) or inverse mode.
1075        self.mode = if vds >= 0.0 { 1.0 } else { -1.0 };
1076        let mode = self.mode;
1077
1078        // Shichman-Hodges drain current and derivatives.
1079        let cdrain;
1080        let (gm, gds, gmbs);
1081        let (von, vdsat);
1082        {
1083            let vx = if mode == 1.0 { vbs } else { vbd };
1084            let sarg = if vx <= 0.0 {
1085                (t.t_phi - vx).sqrt()
1086            } else {
1087                let s = t.t_phi.sqrt();
1088                f64::max(0.0, s - vx / (s + s))
1089            };
1090            von = t.t_vbi * typ + t.gamma * sarg;
1091            let vgst = (if mode == 1.0 { vgs } else { vgd }) - von;
1092            vdsat = f64::max(vgst, 0.0);
1093            let arg = if sarg <= 0.0 {
1094                0.0
1095            } else {
1096                t.gamma / (sarg + sarg)
1097            };
1098            if vgst <= 0.0 {
1099                // Cutoff region.
1100                cdrain = 0.0;
1101                gm = 0.0;
1102                gds = 0.0;
1103                gmbs = 0.0;
1104            } else {
1105                let betap = beta * (1.0 + t.lambda * (vds * mode));
1106                if vgst <= vds * mode {
1107                    // Saturation region.
1108                    cdrain = betap * vgst * vgst * 0.5;
1109                    gm = betap * vgst;
1110                    gds = t.lambda * beta * vgst * vgst * 0.5;
1111                    gmbs = gm * arg;
1112                } else {
1113                    // Linear region.
1114                    cdrain = betap * (vds * mode) * (vgst - 0.5 * (vds * mode));
1115                    gm = betap * (vds * mode);
1116                    gds = betap * (vgst - vds * mode)
1117                        + t.lambda * beta * (vds * mode) * (vgst - 0.5 * (vds * mode));
1118                    gmbs = gm * arg;
1119                }
1120            }
1121        }
1122        self.von = typ * von;
1123        self.vdsat = typ * vdsat;
1124        self.gm = gm;
1125        self.gds = gds;
1126        self.gmbs = gmbs;
1127
1128        // Equivalent drain current source.
1129        let mut cd = mode * cdrain - cbd;
1130
1131        let smsig = ctx.mode == InitMode::Smsig;
1132        if ctx.kind == AnKind::Tran || ctx.kind == AnKind::TranOp || smsig {
1133            // Bulk junction depletion charge and capacitance, with
1134            // the sqrt fast path for grading coefficient 0.5 and the
1135            // linearized continuation past `FC·PB` (CAPZEROBYPASS
1136            // kept: zero-capacitance junctions skip the math).
1137            let dep = |v: f64, cz: f64, czsw: f64, f2: f64, f3: f64, f4: f64| {
1138                if cz != 0.0 || czsw != 0.0 {
1139                    if v < t.t_dep_cap {
1140                        let arg = 1.0 - v / t.t_bulk_pot;
1141                        let (sarg, sargsw);
1142                        if t.mj == t.mjsw {
1143                            if t.mj == 0.5 {
1144                                sarg = 1.0 / arg.sqrt();
1145                                sargsw = sarg;
1146                            } else {
1147                                sarg = f64::exp(-t.mj * f64::ln(arg));
1148                                sargsw = sarg;
1149                            }
1150                        } else {
1151                            sarg = if t.mj == 0.5 {
1152                                1.0 / arg.sqrt()
1153                            } else {
1154                                f64::exp(-t.mj * f64::ln(arg))
1155                            };
1156                            sargsw = if t.mjsw == 0.5 {
1157                                1.0 / arg.sqrt()
1158                            } else {
1159                                f64::exp(-t.mjsw * f64::ln(arg))
1160                            };
1161                        }
1162                        let q = t.t_bulk_pot
1163                            * (cz * (1.0 - arg * sarg) / (1.0 - t.mj)
1164                                + czsw * (1.0 - arg * sargsw) / (1.0 - t.mjsw));
1165                        (q, cz * sarg + czsw * sargsw)
1166                    } else {
1167                        (f4 + v * (f2 + v * f3 / 2.0), f2 + v * f3)
1168                    }
1169                } else {
1170                    (0.0, 0.0)
1171                }
1172            };
1173            let (qbs, capbs) = dep(vbs, t.cbs, t.cbssw, t.f2s, t.f3s, t.f4s);
1174            ctx.states.set(0, st + QBS, qbs);
1175            self.capbs = capbs;
1176            let (qbd, capbd) = dep(vbd, t.cbd, t.cbdsw, t.f2d, t.f3d, t.f4d);
1177            ctx.states.set(0, st + QBD, qbd);
1178            self.capbd = capbd;
1179
1180            if ctx.kind == AnKind::Tran {
1181                // Integrate the depletion capacitors.
1182                let (geq, _) = ctx.integrate(self.capbd, st + QBD);
1183                gbd += geq;
1184                cbd += ctx.states.get(0, st + CQBD);
1185                cd -= ctx.states.get(0, st + CQBD);
1186                let (geq, _) = ctx.integrate(self.capbs, st + QBS);
1187                gbs += geq;
1188                cbs += ctx.states.get(0, st + CQBS);
1189            }
1190        }
1191        self.gbs = gbs;
1192        self.gbd = gbd;
1193
1194        // Convergence: limiting counts as non-convergence (NEWCONV).
1195        if (!self.off || !matches!(ctx.mode, InitMode::Fix | InitMode::Smsig)) && check {
1196            ctx.noncon = true;
1197        }
1198
1199        // Save things away for next time.
1200        ctx.states.set(0, st + VBS, vbs);
1201        ctx.states.set(0, st + VBD, vbd);
1202        ctx.states.set(0, st + VGS, vgs);
1203        ctx.states.set(0, st + VDS, vds);
1204
1205        // Meyer's capacitor model.
1206        let (mut capgs, mut capgd, mut capgb) = (0.0, 0.0, 0.0);
1207        if ctx.kind == AnKind::Tran || ctx.kind == AnKind::TranOp || smsig {
1208            // Half of the non-constant portion at this bias; the
1209            // other half comes from the previous timepoint.
1210            let (half_gs, half_gd, half_gb) = if mode > 0.0 {
1211                qmeyer(vgs, vgd, vgb, von, vdsat, t.t_phi, oxide_cap)
1212            } else {
1213                let (o1, o2, o3) = qmeyer(vgd, vgs, vgb, von, vdsat, t.t_phi, oxide_cap);
1214                (o2, o1, o3)
1215            };
1216            ctx.states.set(0, st + CAPGS, half_gs);
1217            ctx.states.set(0, st + CAPGD, half_gd);
1218            ctx.states.set(0, st + CAPGB, half_gb);
1219            let vgs1 = ctx.states.get(1, st + VGS);
1220            let vgd1 = vgs1 - ctx.states.get(1, st + VDS);
1221            let vgb1 = vgs1 - ctx.states.get(1, st + VBS);
1222            if ctx.kind == AnKind::TranOp || smsig {
1223                capgs = 2.0 * ctx.states.get(0, st + CAPGS) + gate_source_overlap_cap;
1224                capgd = 2.0 * ctx.states.get(0, st + CAPGD) + gate_drain_overlap_cap;
1225                capgb = 2.0 * ctx.states.get(0, st + CAPGB) + gate_bulk_overlap_cap;
1226            } else {
1227                capgs = ctx.states.get(0, st + CAPGS)
1228                    + ctx.states.get(1, st + CAPGS)
1229                    + gate_source_overlap_cap;
1230                capgd = ctx.states.get(0, st + CAPGD)
1231                    + ctx.states.get(1, st + CAPGD)
1232                    + gate_drain_overlap_cap;
1233                capgb = ctx.states.get(0, st + CAPGB)
1234                    + ctx.states.get(1, st + CAPGB)
1235                    + gate_bulk_overlap_cap;
1236            }
1237
1238            if smsig {
1239                // The AC pass reads the Meyer halves from state0 and
1240                // the rest from the instance; nothing to stamp.
1241                return;
1242            }
1243
1244            if predictor {
1245                // Extrapolate the gate charges like the voltages.
1246                let xfact = ctx.pred_xfact;
1247                for o in [QGS, QGD, QGB] {
1248                    let v = (1.0 + xfact) * ctx.states.get(1, st + o)
1249                        - xfact * ctx.states.get(2, st + o);
1250                    ctx.states.set(0, st + o, v);
1251                }
1252            } else if ctx.kind == AnKind::Tran {
1253                let q = (vgs - vgs1) * capgs + ctx.states.get(1, st + QGS);
1254                ctx.states.set(0, st + QGS, q);
1255                let q = (vgd - vgd1) * capgd + ctx.states.get(1, st + QGD);
1256                ctx.states.set(0, st + QGD, q);
1257                let q = (vgb - vgb1) * capgb + ctx.states.get(1, st + QGB);
1258                ctx.states.set(0, st + QGB, q);
1259            } else {
1260                // Transient operating point only.
1261                ctx.states.set(0, st + QGS, vgs * capgs);
1262                ctx.states.set(0, st + QGD, vgd * capgd);
1263                ctx.states.set(0, st + QGB, vgb * capgb);
1264            }
1265        }
1266
1267        // Meyer companion models (zero at the first timepoint and in
1268        // every DC mode).
1269        let (mut gcgs, mut ceqgs, mut gcgd, mut ceqgd, mut gcgb, mut ceqgb) =
1270            (0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
1271        if ctx.mode != InitMode::Tran && ctx.kind == AnKind::Tran {
1272            if capgs == 0.0 {
1273                ctx.states.set(0, st + CQGS, 0.0);
1274            }
1275            if capgd == 0.0 {
1276                ctx.states.set(0, st + CQGD, 0.0);
1277            }
1278            if capgb == 0.0 {
1279                ctx.states.set(0, st + CQGB, 0.0);
1280            }
1281            let (g, ceq) = ctx.integrate(capgs, st + QGS);
1282            gcgs = g;
1283            ceqgs = ceq - gcgs * vgs + ctx.ag[0] * ctx.states.get(0, st + QGS);
1284            let (g, ceq) = ctx.integrate(capgd, st + QGD);
1285            gcgd = g;
1286            ceqgd = ceq - gcgd * vgd + ctx.ag[0] * ctx.states.get(0, st + QGD);
1287            let (g, ceq) = ctx.integrate(capgb, st + QGB);
1288            gcgb = g;
1289            ceqgb = ceq - gcgb * vgb + ctx.ag[0] * ctx.states.get(0, st + QGB);
1290        }
1291
1292        // Load the current vector.
1293        // ngspice folds gmin into the junction current, so the
1294        // Norton companion uses the full conductance (the 3f5
1295        // subtracted gmin here).
1296        let ceqbs = typ * (cbs - gbs * vbs);
1297        let ceqbd = typ * (cbd - gbd * vbd);
1298        let (xnrm, xrev, cdreq);
1299        if mode >= 0.0 {
1300            xnrm = 1.0;
1301            xrev = 0.0;
1302            cdreq = typ * (cdrain - gds * vds - gm * vgs - gmbs * vbs);
1303        } else {
1304            xnrm = 0.0;
1305            xrev = 1.0;
1306            cdreq = -typ * (cdrain - gds * (-vds) - gm * vgd - gmbs * vbd);
1307        }
1308        // Saved for the convergence test (`mos1conv.c`).
1309        self.cd = cd;
1310        self.cbs = cbs;
1311        self.cbd = cbd;
1312        ctx.add_rhs(self.gate, -(typ * (ceqgs + ceqgb + ceqgd)));
1313        ctx.add_rhs(self.bulk, -(ceqbs + ceqbd - typ * ceqgb));
1314        ctx.add_rhs(self.d_p, ceqbd - cdreq + typ * ceqgd);
1315        ctx.add_rhs(self.s_p, cdreq + ceqbs + typ * ceqgs);
1316
1317        // Load the Y matrix.
1318        let gdpr = t.drain_conductance;
1319        let gspr = t.source_conductance;
1320        ctx.add(self.s[D_D], gdpr);
1321        ctx.add(self.s[G_G], gcgd + gcgs + gcgb);
1322        ctx.add(self.s[S_S], gspr);
1323        ctx.add(self.s[B_B], gbd + gbs + gcgb);
1324        ctx.add(self.s[DP_DP], gdpr + gds + gbd + xrev * (gm + gmbs) + gcgd);
1325        ctx.add(self.s[SP_SP], gspr + gds + gbs + xnrm * (gm + gmbs) + gcgs);
1326        ctx.add(self.s[D_DP], -gdpr);
1327        ctx.add(self.s[G_B], -gcgb);
1328        ctx.add(self.s[G_DP], -gcgd);
1329        ctx.add(self.s[G_SP], -gcgs);
1330        ctx.add(self.s[S_SP], -gspr);
1331        ctx.add(self.s[B_G], -gcgb);
1332        ctx.add(self.s[B_DP], -gbd);
1333        ctx.add(self.s[B_SP], -gbs);
1334        ctx.add(self.s[DP_D], -gdpr);
1335        ctx.add(self.s[DP_G], (xnrm - xrev) * gm - gcgd);
1336        ctx.add(self.s[DP_B], -gbd + (xnrm - xrev) * gmbs);
1337        ctx.add(self.s[DP_SP], -gds - xnrm * (gm + gmbs));
1338        ctx.add(self.s[SP_G], -(xnrm - xrev) * gm - gcgs);
1339        ctx.add(self.s[SP_S], -gspr);
1340        ctx.add(self.s[SP_B], -gbs - (xnrm - xrev) * gmbs);
1341        ctx.add(self.s[SP_DP], -gds - xrev * (gm + gmbs));
1342    }
1343
1344    fn trunc(&self, t: &crate::integ::TerrCtx, time_step: &mut f64) {
1345        crate::integ::terr(self.st + QGS, t, time_step);
1346        crate::integ::terr(self.st + QGD, t, time_step);
1347        crate::integ::terr(self.st + QGB, t, time_step);
1348    }
1349
1350    /// `MOS1noise` (ngspice-47, default `NLEV`): see
1351    /// [`mos_noise`].
1352    fn noise_suffixes(&self) -> Option<&'static [&'static str]> {
1353        Some(MOS_NOISE_SUFFIXES)
1354    }
1355
1356    fn noise_dens(&self, ctx: &crate::matrix::NoiseCtx) -> Vec<f64> {
1357        let n = MosNoise {
1358            drain: self.drain,
1359            source: self.source,
1360            d_p: self.d_p,
1361            s_p: self.s_p,
1362            gdpr: self.t.drain_conductance,
1363            gspr: self.t.source_conductance,
1364            gm: self.gm,
1365            cd: self.cd,
1366            kf: self.t.kf,
1367            af: self.t.af,
1368            nlev: self.t.nlev,
1369            gdsnoi: self.t.gdsnoi,
1370            w: self.geom.w,
1371            eff_l: self.geom.l - 2.0 * self.t.lat_diff,
1372            temp: self.t.temp,
1373            vgs: ctx.states.get(0, self.st + VGS),
1374            vds: ctx.states.get(0, self.st + VDS),
1375            mode: self.mode,
1376            von: self.von,
1377            vdsat: self.vdsat,
1378            typ: self.t.type_,
1379            t_transconductance: self.t.t_transconductance,
1380            cox: if self.t.oxide_cap_factor == 0.0 {
1381                // `mos1noi.c`: a level-1 model without TOX assumes
1382                // tox = 1e-7 m for the flicker term.
1383                3.9 * 8.854214871e-12 / 1e-7
1384            } else {
1385                self.t.oxide_cap_factor
1386            },
1387        };
1388        mos_noise(ctx, &n)
1389    }
1390
1391    /// `MOS1acLoad`.
1392    fn load_ac(&mut self, ctx: &mut AcCtx) {
1393        let t = &self.t;
1394        let st = self.st;
1395        let (xnrm, xrev) = if self.mode < 0.0 {
1396            (0.0, 1.0)
1397        } else {
1398            (1.0, 0.0)
1399        };
1400        let eff_l = self.geom.l - 2.0 * t.lat_diff;
1401        let gate_source_overlap_cap = t.cgso * self.geom.w;
1402        let gate_drain_overlap_cap = t.cgdo * self.geom.w;
1403        let gate_bulk_overlap_cap = t.cgbo * eff_l;
1404        let capgs = 2.0 * ctx.states.get(0, st + CAPGS) + gate_source_overlap_cap;
1405        let capgd = 2.0 * ctx.states.get(0, st + CAPGD) + gate_drain_overlap_cap;
1406        let capgb = 2.0 * ctx.states.get(0, st + CAPGB) + gate_bulk_overlap_cap;
1407        let xgs = capgs * ctx.omega;
1408        let xgd = capgd * ctx.omega;
1409        let xgb = capgb * ctx.omega;
1410        let xbd = self.capbd * ctx.omega;
1411        let xbs = self.capbs * ctx.omega;
1412        let (gm, gds, gmbs) = (self.gm, self.gds, self.gmbs);
1413        let (gbd, gbs) = (self.gbd, self.gbs);
1414        let gdpr = t.drain_conductance;
1415        let gspr = t.source_conductance;
1416
1417        ctx.add_b(self.s[G_G], xgd + xgs + xgb);
1418        ctx.add_b(self.s[B_B], xgb + xbd + xbs);
1419        ctx.add_b(self.s[DP_DP], xgd + xbd);
1420        ctx.add_b(self.s[SP_SP], xgs + xbs);
1421        ctx.add_b(self.s[G_B], -xgb);
1422        ctx.add_b(self.s[G_DP], -xgd);
1423        ctx.add_b(self.s[G_SP], -xgs);
1424        ctx.add_b(self.s[B_G], -xgb);
1425        ctx.add_b(self.s[B_DP], -xbd);
1426        ctx.add_b(self.s[B_SP], -xbs);
1427        ctx.add_b(self.s[DP_G], -xgd);
1428        ctx.add_b(self.s[DP_B], -xbd);
1429        ctx.add_b(self.s[SP_G], -xgs);
1430        ctx.add_b(self.s[SP_B], -xbs);
1431        ctx.add_g(self.s[D_D], gdpr);
1432        ctx.add_g(self.s[S_S], gspr);
1433        ctx.add_g(self.s[B_B], gbd + gbs);
1434        ctx.add_g(self.s[DP_DP], gdpr + gds + gbd + xrev * (gm + gmbs));
1435        ctx.add_g(self.s[SP_SP], gspr + gds + gbs + xnrm * (gm + gmbs));
1436        ctx.add_g(self.s[D_DP], -gdpr);
1437        ctx.add_g(self.s[S_SP], -gspr);
1438        ctx.add_g(self.s[B_DP], -gbd);
1439        ctx.add_g(self.s[B_SP], -gbs);
1440        ctx.add_g(self.s[DP_D], -gdpr);
1441        ctx.add_g(self.s[DP_G], (xnrm - xrev) * gm);
1442        ctx.add_g(self.s[DP_B], -gbd + (xnrm - xrev) * gmbs);
1443        ctx.add_g(self.s[DP_SP], -gds - xnrm * (gm + gmbs));
1444        ctx.add_g(self.s[SP_G], -(xnrm - xrev) * gm);
1445        ctx.add_g(self.s[SP_S], -gspr);
1446        ctx.add_g(self.s[SP_B], -gbs - (xnrm - xrev) * gmbs);
1447        ctx.add_g(self.s[SP_DP], -gds - xrev * (gm + gmbs));
1448    }
1449
1450    /// `MOS1pzLoad` (`mos1pzld.c`): see [`meyer_pz_load`].
1451    fn load_pz(&mut self, ctx: &mut crate::matrix::PzCtx) -> Result<(), &'static str> {
1452        let t = &self.t;
1453        let st = self.st;
1454        let (xnrm, xrev) = if self.mode < 0.0 {
1455            (0.0, 1.0)
1456        } else {
1457            (1.0, 0.0)
1458        };
1459        let eff_l = self.geom.l - 2.0 * t.lat_diff;
1460        let caps = MeyerPzCaps {
1461            capgs: 2.0 * ctx.states.get(0, st + CAPGS) + t.cgso * self.geom.w,
1462            capgd: 2.0 * ctx.states.get(0, st + CAPGD) + t.cgdo * self.geom.w,
1463            capgb: 2.0 * ctx.states.get(0, st + CAPGB) + t.cgbo * eff_l,
1464            capbd: self.capbd,
1465            capbs: self.capbs,
1466        };
1467        let g = MosPzConductances {
1468            xnrm,
1469            xrev,
1470            gm: self.gm,
1471            gds: self.gds,
1472            gmbs: self.gmbs,
1473            gbd: self.gbd,
1474            gbs: self.gbs,
1475            gdpr: t.drain_conductance,
1476            gspr: t.source_conductance,
1477        };
1478        meyer_pz_load(ctx, &self.s, &caps, &g);
1479        Ok(())
1480    }
1481
1482    /// `MOS1dSetup` (`dev/mos1/mos1dset.c`): the Shichman–Hodges
1483    /// drain current's derivatives up to third order written out by
1484    /// region (cutoff, saturation, linear), with the body effect
1485    /// through `von(vbs)`; then the shared diode/capacitance/type
1486    /// bookkeeping (`mosdisto.rs`).
1487    #[allow(clippy::too_many_lines)]
1488    fn disto_setup(&mut self, ctx: &DistoSetupCtx) -> Result<(), &'static str> {
1489        let t = &self.t;
1490        let g = &self.geom;
1491        let typ = t.type_;
1492        let vt = t.vt;
1493        let sol = ctx.sol;
1494        let gmin = ctx.gmin;
1495        let eff_l = g.l - 2.0 * t.lat_diff;
1496        let (drain_sat_cur, source_sat_cur) =
1497            if t.t_sat_cur_dens == 0.0 || g.ad == 0.0 || g.as_ == 0.0 {
1498                (t.t_sat_cur, t.t_sat_cur)
1499            } else {
1500                (t.t_sat_cur_dens * g.ad, t.t_sat_cur_dens * g.as_)
1501            };
1502        let beta = t.t_transconductance * g.w / eff_l;
1503        let oxide_cap = t.oxide_cap_factor * eff_l * g.w;
1504
1505        let vbs = typ * (sol[self.bulk] - sol[self.s_p]);
1506        let vgs = typ * (sol[self.gate] - sol[self.s_p]);
1507        let vds = typ * (sol[self.d_p] - sol[self.s_p]);
1508        let vbd = vbs - vds;
1509        let vgd = vgs - vds;
1510
1511        let gbs = bulk_diode(vbs, source_sat_cur, vt, gmin, typ);
1512        let gbd = bulk_diode(vbd, drain_sat_cur, vt, gmin, typ);
1513        let mode = if vds >= 0.0 { 1.0 } else { -1.0 };
1514
1515        let lvbs = if mode == 1.0 { vbs } else { vbd };
1516        let (sarg, dvondvbs, d2vondvbs2, d3vondvbs3);
1517        if lvbs <= 0.0 {
1518            sarg = f64::sqrt(t.t_phi - lvbs);
1519            if -t.gamma != 0.0 {
1520                let d1 = -t.gamma * 0.5 / sarg;
1521                let d2 = -d1 * 0.5 / (sarg * sarg);
1522                dvondvbs = d1;
1523                d2vondvbs2 = d2;
1524                d3vondvbs3 = 1.5 * d2 / (sarg * sarg);
1525            } else {
1526                (dvondvbs, d2vondvbs2, d3vondvbs3) = (0.0, 0.0, 0.0);
1527            }
1528        } else {
1529            let s0 = f64::sqrt(t.t_phi);
1530            let d1 = if t.gamma != 0.0 {
1531                -t.gamma / (s0 + s0)
1532            } else {
1533                0.0
1534            };
1535            (d2vondvbs2, d3vondvbs3) = (0.0, 0.0);
1536            sarg = f64::max(0.0, s0 - lvbs / (s0 + s0));
1537            dvondvbs = if sarg <= 0.0 { 0.0 } else { d1 };
1538        }
1539        let von = t.t_vbi * typ + t.gamma * sarg;
1540        let vgst = (if mode == 1.0 { vgs } else { vgd }) - von;
1541        let vdsat = f64::max(vgst, 0.0);
1542        let lambda = t.lambda;
1543        let mut dd = DrainDerivs::default();
1544        if vgst > 0.0 {
1545            let betap = beta * (1.0 + lambda * (vds * mode));
1546            if vgst <= vds * mode {
1547                // Saturation: cdrain = betap·vgst²/2.
1548                let gm = betap * vgst;
1549                let gmds = vgst * lambda * beta;
1550                let gmb = -betap * dvondvbs;
1551                dd = DrainDerivs {
1552                    gm2: betap,
1553                    gds2: 0.0,
1554                    gb2: -(gm * d2vondvbs2 - betap * dvondvbs * dvondvbs),
1555                    gmds,
1556                    gbds: -gmds * dvondvbs,
1557                    gmb,
1558                    gm3: 0.0,
1559                    gb3: -(gmb * d2vondvbs2 + gm * d3vondvbs3
1560                        - betap * 2.0 * dvondvbs * d2vondvbs2),
1561                    gds3: 0.0,
1562                    gm2ds: beta * lambda,
1563                    gm2b: 0.0,
1564                    gmb2: -betap * d2vondvbs2,
1565                    gb2ds: -(gmds * d2vondvbs2 - dvondvbs * dvondvbs * beta * lambda),
1566                    gmds2: 0.0,
1567                    gbds2: 0.0,
1568                    gmbds: -beta * lambda * dvondvbs,
1569                };
1570            } else {
1571                // Linear: cdrain = betap·vds·(vgst − vds/2).
1572                let gm = betap * (vds * mode);
1573                let gmds = beta * lambda * vds * mode + betap;
1574                let gmds2 = 2.0 * lambda * beta;
1575                dd = DrainDerivs {
1576                    gm2: 0.0,
1577                    gb2: -(gm * d2vondvbs2),
1578                    gds2: 2.0 * beta * lambda * (vgst - vds * mode) - betap,
1579                    gmds,
1580                    gbds: -gmds * dvondvbs,
1581                    gmb: 0.0,
1582                    gm3: 0.0,
1583                    gb3: -gm * d3vondvbs3,
1584                    gds3: -beta * lambda * 3.,
1585                    gm2ds: 0.0,
1586                    gm2b: 0.0,
1587                    gmb2: 0.0,
1588                    gmds2,
1589                    gb2ds: -(gmds * d2vondvbs2),
1590                    gbds2: -gmds2 * dvondvbs,
1591                    gmbds: 0.0,
1592                };
1593            }
1594        }
1595
1596        let junctions = MosJunctions {
1597            cbs: t.cbs,
1598            cbssw: t.cbssw,
1599            f3s: t.f3s,
1600            cbd: t.cbd,
1601            cbdsw: t.cbdsw,
1602            f3d: t.f3d,
1603            dep_cap: t.t_dep_cap,
1604            bulk_pot: t.t_bulk_pot,
1605            mj: t.mj,
1606            mjsw: t.mjsw,
1607        };
1608        self.dist = setup_tail(
1609            typ,
1610            mode,
1611            (vbs, vbd, vds),
1612            (vgst, vdsat),
1613            t.t_phi,
1614            oxide_cap,
1615            &junctions,
1616            &dd,
1617            gbs,
1618            gbd,
1619        );
1620        Ok(())
1621    }
1622
1623    /// `MOS1disto` (`dev/mos1/mos1dist.c`).
1624    fn disto_load(&self, ctx: &mut DistoCtx) {
1625        mos_disto_load(&self.dist, (self.gate, self.bulk, self.d_p, self.s_p), ctx);
1626    }
1627}
1628
1629#[cfg(test)]
1630mod tests {
1631    use super::*;
1632
1633    #[test]
1634    fn fetlim_matches_the_c() {
1635        // On, above vtox, big positive step: clamp to vold+vtsthi.
1636        let vto = 1.0;
1637        let vold = 5.0;
1638        let vtsthi = (2.0f64 * (vold - vto)).abs() + 2.0;
1639        assert_eq!(fetlim(30.0, vold, vto), vold + vtsthi);
1640        // Off, small step passes through.
1641        assert_eq!(fetlim(0.2, 0.0, 1.0), 0.2);
1642        // Off, trying to jump far above threshold: clamp to vto+0.5.
1643        assert_eq!(fetlim(10.0, 0.0, 1.0), 1.5);
1644    }
1645
1646    #[test]
1647    fn limvds_matches_the_c() {
1648        assert_eq!(limvds(10.0, 1.0), 4.0);
1649        assert_eq!(limvds(-3.0, 1.0), -0.5);
1650        assert_eq!(limvds(20.0, 5.0), 17.0); // 3*5+2
1651        assert_eq!(limvds(1.0, 5.0), 2.0);
1652    }
1653
1654    #[test]
1655    fn qmeyer_regions() {
1656        let cox = 1e-12;
1657        let phi = 0.6;
1658        // Accumulation: all gate-bulk.
1659        let (gs, gd, gb) = qmeyer(-2.0, -2.0, 0.0, 0.0, 0.0, phi, cox);
1660        assert_eq!((gs, gd), (0.0, 0.0));
1661        assert_eq!(gb, cox / 2.0);
1662        // Saturation: capgs = cox/3.
1663        let (gs, gd, gb) = qmeyer(2.0, -3.0, 0.0, 0.0, 2.0, phi, cox);
1664        assert_eq!(gs, cox / 3.0);
1665        assert_eq!((gd, gb), (0.0, 0.0));
1666        // Linear region: both nonzero, sum tends to 2/3 cox.
1667        let (gs, gd, _) = qmeyer(2.0, 1.9, 0.0, 0.0, 2.0, phi, cox);
1668        assert!(gs > 0.0 && gd > 0.0);
1669        assert!((gs + gd) < 2.0 * cox / 3.0 + 1e-15);
1670    }
1671
1672    #[test]
1673    fn temp_defaults_match_mos1temp() {
1674        // mosmem's model: vto/phi/kp/gamma/lambda given, no tox.
1675        let mut m = Mos1Model::new(true);
1676        m.vto = Some(0.5);
1677        m.phi = Some(0.7);
1678        m.kp = Some(1.0e-6);
1679        m.gamma = Some(1.83);
1680        m.lambda = 0.115;
1681        m.cgso = 1e-6;
1682        m.cgdo = 1e-6;
1683        m.cbd = Some(50e-12);
1684        m.cbs = Some(50e-12);
1685        let g = MosGeom {
1686            l: 5e-6,
1687            w: 250e-6,
1688            ad: 0.0,
1689            as_: 0.0,
1690            pd: 0.0,
1691            ps: 0.0,
1692            nrd: 1.0,
1693            nrs: 1.0,
1694        };
1695        let t = Mos1Temp::new("m1", &m, &g, REFTEMP, REFTEMP).unwrap();
1696        // At TNOM the adjusted values collapse to the given ones.
1697        assert!((t.t_phi - 0.7).abs() < 1e-12);
1698        assert!((t.t_vto - 0.5).abs() < 1e-12);
1699        assert!((t.t_transconductance - 1.0e-6).abs() < 1e-18);
1700        assert_eq!(t.drain_conductance, 0.0);
1701        assert!((t.cbd - 50e-12).abs() < 1e-24);
1702        // No prime nodes for this model.
1703        assert!(!m.needs_prime(1.0));
1704    }
1705}