Skip to main content

dune_core/
bsim1.rs

1//! BSIM1 — the Berkeley Short-Channel IGFET Model, level 4 in the
2//! 3f5 (`dev/bsim1/b1ld.c`, `b1eval.c`, `b1temp.c`, `b1set.c`,
3//! `b1moscap.c`, `b1acld.c`, `b1trunc.c`, `b1cvtest.c`; authors
4//! Hong J. Park and Thomas L. Quarles). Every electrical parameter
5//! comes in a (value, L-dependence, W-dependence) triple resolved
6//! per instance as `p0 + pL/Leff + pW/Weff` with Leff/Weff in µm;
7//! TOX and DL/DW are in µm, mobilities in cm² — the unit dance of
8//! the C is kept to the letter. ngspice-47 carries this code
9//! unchanged, so the 3f5 is also the oracle's reference here.
10
11use crate::consts::{ROOT2, VT0};
12use crate::diode::pnjlim;
13use crate::matrix::{AcCtx, AnKind, InitMode, LoadCtx, MatrixBuilder, Slot, TRASH, Var};
14use crate::mos1::{MosGeom, MosPzConductances, fetlim, limvds, mos_pz_conductances};
15
16/// `.model … nmos(level=4 …)` parameters, 3f5 names (`b1.c` keyword
17/// table). Everything defaults to 0 (`b1set.c`); the junction
18/// potentials are clamped to ≥ 0.1 at temp time.
19#[derive(Clone, Debug, Default)]
20pub struct Bsim1Model {
21    pub nmos: bool,
22    // (value, L, W) triples.
23    pub vfb0: f64,
24    pub vfbl: f64,
25    pub vfbw: f64,
26    pub phi0: f64,
27    pub phil: f64,
28    pub phiw: f64,
29    pub k10: f64,
30    pub k1l: f64,
31    pub k1w: f64,
32    pub k20: f64,
33    pub k2l: f64,
34    pub k2w: f64,
35    pub eta0: f64,
36    pub etal: f64,
37    pub etaw: f64,
38    pub etab0: f64,
39    pub etabl: f64,
40    pub etabw: f64,
41    pub etad0: f64,
42    pub etadl: f64,
43    pub etadw: f64,
44    /// `MUZ` has no geometry dependence of its own.
45    pub mobzero: f64,
46    pub mobzerob0: f64,
47    pub mobzerobl: f64,
48    pub mobzerobw: f64,
49    pub mobvdd0: f64,
50    pub mobvddl: f64,
51    pub mobvddw: f64,
52    pub mobvddb0: f64,
53    pub mobvddbl: f64,
54    pub mobvddbw: f64,
55    pub mobvddd0: f64,
56    pub mobvdddl: f64,
57    pub mobvdddw: f64,
58    pub ugs0: f64,
59    pub ugsl: f64,
60    pub ugsw: f64,
61    pub ugsb0: f64,
62    pub ugsbl: f64,
63    pub ugsbw: f64,
64    pub uds0: f64,
65    pub udsl: f64,
66    pub udsw: f64,
67    pub udsb0: f64,
68    pub udsbl: f64,
69    pub udsbw: f64,
70    pub udsd0: f64,
71    pub udsdl: f64,
72    pub udsdw: f64,
73    pub n00: f64,
74    pub n0l: f64,
75    pub n0w: f64,
76    pub nb0: f64,
77    pub nbl: f64,
78    pub nbw: f64,
79    pub nd0: f64,
80    pub ndl: f64,
81    pub ndw: f64,
82    /// `DL`/`DW`: channel reduction, **µm**.
83    pub deltal: f64,
84    pub deltaw: f64,
85    /// `TOX` in **µm**.
86    pub tox: f64,
87    /// `TEMP` (accepted and unused: BSIM1 runs on `CONSTvt0`).
88    pub temp: f64,
89    /// `VDD`: supply at which `MUS` was measured.
90    pub vdd: f64,
91    /// Overlap capacitances per meter.
92    pub cgso: f64,
93    pub cgdo: f64,
94    pub cgbo: f64,
95    /// `XPART`: ≥ 1 selects the 0/100 charge partition, else 40/60.
96    pub xpart: f64,
97    pub rsh: f64,
98    pub js: f64,
99    pub pb: f64,
100    pub mj: f64,
101    pub pbsw: f64,
102    pub mjsw: f64,
103    pub cj: f64,
104    pub cjsw: f64,
105    /// `WDF`/`DELL` (accepted and unused by the load, like the C).
106    pub wdf: f64,
107    pub dell: f64,
108    /// `KF`/`AF` (noise only; ngspice-47's `b1noi.c` — the 3f5's
109    /// BSIM1 has no noise and no such parameters). Defaults 0 and 1
110    /// (`b1set.c` of ngspice) — `new` sets `AF`; the derived
111    /// `Default` leaves it 0.
112    pub kf: f64,
113    pub af: f64,
114}
115
116impl Bsim1Model {
117    pub fn new(nmos: bool) -> Self {
118        Bsim1Model {
119            nmos,
120            af: 1.0,
121            ..Default::default()
122        }
123    }
124
125    pub(crate) fn needs_prime(&self, squares: f64) -> bool {
126        self.rsh != 0.0 && squares != 0.0
127    }
128}
129
130/// Name suffixes of the BSIM1/BSIM2 noise generators (`B1nNames`,
131/// `B2nNames` of ngspice-47).
132pub(crate) const B12_NOISE_SUFFIXES: &[&str] = &["_rd", "_rs", "_id", "_1overf", ""];
133
134/// What `B1noise`/`B2noise` read.
135pub(crate) struct B12Noise {
136    pub drain: Var,
137    pub source: Var,
138    pub d_p: Var,
139    pub s_p: Var,
140    pub gdpr: f64,
141    pub gspr: f64,
142    /// `state0` gm and drain current.
143    pub gm: f64,
144    pub cd: f64,
145    pub kf: f64,
146    pub af: f64,
147    /// `(W − DW·1e-6)·(L − DL·1e-6)`, m².
148    pub flicker_area: f64,
149    /// `B*Cox`, F/cm².
150    pub cox: f64,
151}
152
153#[cfg(test)]
154thread_local! {
155    /// Test hook reproducing ngspice-47's reading of `B1gm`/`B1cd`
156    /// (see [`b12_noise`]): `Some(GENstate)` makes the channel and
157    /// flicker terms use `GENstate + 7` and `GENstate + 4`.
158    pub(crate) static NGSPICE_STATE_OFFSET: std::cell::Cell<Option<f64>> =
159        const { std::cell::Cell::new(None) };
160}
161
162/// `B1noise` / `B2noise` (ngspice-47 `b1noi.c`/`b2noi.c`, the same
163/// file with the prefix renamed; the 3f5 has neither): thermal noise
164/// of the drain/source series resistances and of the channel
165/// `4kT·(2/3)|gm|`, at `CKTtemp` (plain `NevalSrc`); flicker
166/// `KF·|Id|^AF/(f·(W−DW)·(L−DL)·Cox²)` — the old 3f5-MOS form, with
167/// `Cox` in F/cm².
168///
169/// **Deliberate divergence from the oracle.** `bsim1def.h` defines
170/// `B1gm` and `B1cd` as *state offsets* (`#define B1gm B1states+ 7`),
171/// so the C's `fabs(inst->B1gm * inst->B1m)` evaluates to
172/// `GENstate + 7·m` and `(double)inst->B1cd` to `GENstate + 4` — the
173/// instance's index in the state vector, not its gm and current
174/// (measured: one BSIM1 in a deck, `GENstate = 0`, channel noise of a
175/// 7 S conductance). Dune reads the values those offsets point at,
176/// `*(CKTstate0 + B1gm)`, as the author evidently meant (and as
177/// `jfetnoi.c` does); everything else is the C's.
178pub(crate) fn b12_noise(ctx: &crate::matrix::NoiseCtx, n: &B12Noise) -> Vec<f64> {
179    const N_MINLOG: f64 = 1e-38;
180    let (gm, cd) = (n.gm, n.cd);
181    #[cfg(test)]
182    let (gm, cd) = match NGSPICE_STATE_OFFSET.get() {
183        Some(state) => (state + 7.0, state + 4.0),
184        None => (gm, cd),
185    };
186    let rd = ctx.thermal(n.d_p, n.drain, n.gdpr);
187    let rs = ctx.thermal(n.s_p, n.source, n.gspr);
188    let id = ctx.thermal(n.d_p, n.s_p, 2.0 / 3.0 * gm.abs());
189    let flicker = ctx.flicker(
190        n.d_p,
191        n.s_p,
192        n.kf * f64::exp(n.af * f64::ln(f64::max(cd.abs(), N_MINLOG)))
193            / (n.flicker_area * n.cox * n.cox),
194        1.0,
195    );
196    vec![rd, rs, id, flicker, rd + rs + id + flicker]
197}
198
199/// Per-instance size-resolved parameters (`B1temp`).
200#[derive(Debug)]
201pub(crate) struct Bsim1Size {
202    pub type_: f64,
203    pub vfb: f64,
204    pub phi: f64,
205    pub k1: f64,
206    pub k2: f64,
207    pub eta: f64,
208    pub eta_b: f64,
209    pub eta_d: f64,
210    pub beta_zero: f64,
211    pub beta_zero_b: f64,
212    pub ugs: f64,
213    pub ugs_b: f64,
214    pub uds: f64,
215    pub uds_b: f64,
216    pub uds_d: f64,
217    pub beta_vdd: f64,
218    pub beta_vdd_b: f64,
219    pub beta_vdd_d: f64,
220    pub n0: f64,
221    pub nb: f64,
222    pub nd: f64,
223    pub vt0: f64,
224    /// `b1temp.c` overlaps (effective W, drawn L) — the AC load's.
225    pub gd_overlap: f64,
226    pub gs_overlap: f64,
227    pub gb_overlap: f64,
228    /// `b1ld.c` overlaps (drawn W, effective L) — the transient
229    /// load recomputes them per iteration from the model card.
230    pub ld_gd_overlap: f64,
231    pub ld_gs_overlap: f64,
232    pub ld_gb_overlap: f64,
233    pub drain_conductance: f64,
234    pub source_conductance: f64,
235    /// `B1Cox` in F/cm².
236    pub cox: f64,
237    // Model copies the load needs.
238    pub vdd: f64,
239    pub delta_l: f64,
240    /// `DW` in µm.
241    pub delta_w: f64,
242    pub js: f64,
243    pub pb: f64,
244    pub pbsw: f64,
245    pub mj: f64,
246    pub mjsw: f64,
247    pub cj: f64,
248    pub cjsw: f64,
249    pub xpart: f64,
250    pub kf: f64,
251    pub af: f64,
252}
253
254impl Bsim1Size {
255    pub fn new(name: &str, m: &Bsim1Model, g: &MosGeom) -> Result<Self, String> {
256        let typ = if m.nmos { 1.0 } else { -1.0 };
257        let pb = f64::max(m.pb, 0.1);
258        let pbsw = f64::max(m.pbsw, 0.1);
259        if m.tox <= 0.0 {
260            return Err(format!("{name}: BSIM1 model needs TOX > 0"));
261        }
262        let cox = 3.453e-13 / (m.tox * 1.0e-4); // F/cm²
263
264        let eff_l = g.l - m.deltal * 1e-6;
265        let eff_w = g.w - m.deltaw * 1e-6;
266        if eff_l <= 0.0 {
267            return Err(format!("{name}: effective channel length <= 0"));
268        }
269        if eff_w <= 0.0 {
270            return Err(format!("{name}: effective channel width <= 0"));
271        }
272        let leff = eff_l * 1e6; // µm
273        let weff = eff_w * 1e6;
274        let cox_w_over_l = cox * weff / leff;
275
276        let p = |p0: f64, pl: f64, pw: f64| p0 + pl / leff + pw / weff;
277
278        let vfb = p(m.vfb0, m.vfbl, m.vfbw);
279        let mut phi = p(m.phi0, m.phil, m.phiw);
280        let mut k1 = p(m.k10, m.k1l, m.k1w);
281        let mut k2 = p(m.k20, m.k2l, m.k2w);
282        if phi < 0.1 {
283            phi = 0.1;
284        }
285        if k1 < 0.0 {
286            k1 = 0.0;
287        }
288        if k2 < 0.0 {
289            k2 = 0.0;
290        }
291        let vt0 = vfb + phi + k1 * phi.sqrt() - k2 * phi;
292
293        let series = |squares: f64| {
294            let r = m.rsh * squares;
295            if r != 0.0 { 1.0 / r } else { 0.0 }
296        };
297
298        Ok(Bsim1Size {
299            type_: typ,
300            vfb,
301            phi,
302            k1,
303            k2,
304            eta: p(m.eta0, m.etal, m.etaw),
305            eta_b: p(m.etab0, m.etabl, m.etabw),
306            eta_d: p(m.etad0, m.etadl, m.etadw),
307            beta_zero: m.mobzero * cox_w_over_l,
308            beta_zero_b: p(m.mobzerob0, m.mobzerobl, m.mobzerobw) * cox_w_over_l,
309            ugs: p(m.ugs0, m.ugsl, m.ugsw),
310            ugs_b: p(m.ugsb0, m.ugsbl, m.ugsbw),
311            uds: p(m.uds0, m.udsl, m.udsw),
312            uds_b: p(m.udsb0, m.udsbl, m.udsbw),
313            uds_d: p(m.udsd0, m.udsdl, m.udsdw),
314            beta_vdd: p(m.mobvdd0, m.mobvddl, m.mobvddw) * cox_w_over_l,
315            beta_vdd_b: p(m.mobvddb0, m.mobvddbl, m.mobvddbw) * cox_w_over_l,
316            beta_vdd_d: f64::max(p(m.mobvddd0, m.mobvdddl, m.mobvdddw) * cox_w_over_l, 0.0),
317            n0: p(m.n00, m.n0l, m.n0w),
318            nb: p(m.nb0, m.nbl, m.nbw),
319            nd: p(m.nd0, m.ndl, m.ndw),
320            vt0,
321            gd_overlap: eff_w * m.cgdo,
322            gs_overlap: eff_w * m.cgso,
323            gb_overlap: g.l * m.cgbo,
324            ld_gd_overlap: g.w * m.cgdo,
325            ld_gs_overlap: g.w * m.cgso,
326            ld_gb_overlap: eff_l * m.cgbo,
327            drain_conductance: series(g.nrd),
328            source_conductance: series(g.nrs),
329            cox,
330            vdd: m.vdd,
331            delta_l: m.deltal,
332            delta_w: m.deltaw,
333            js: m.js,
334            pb,
335            pbsw,
336            mj: m.mj,
337            mjsw: m.mjsw,
338            cj: m.cj,
339            cjsw: m.cjsw,
340            xpart: m.xpart,
341            kf: m.kf,
342            af: m.af,
343        })
344    }
345}
346
347// State offsets (`bsim1def.h`).
348const VBD: usize = 0;
349const VBS: usize = 1;
350const VGS: usize = 2;
351const VDS: usize = 3;
352const CD: usize = 4;
353const CBS: usize = 5;
354const CBD: usize = 6;
355const GM: usize = 7;
356const GDS: usize = 8;
357const GMBS: usize = 9;
358const GBD: usize = 10;
359const GBS: usize = 11;
360const QB: usize = 12;
361const IQB: usize = 13;
362const QG: usize = 14;
363const IQG: usize = 15;
364const QD: usize = 16;
365const IQD: usize = 17;
366const CGGB: usize = 18;
367const CGDB: usize = 19;
368const CGSB: usize = 20;
369const CBGB: usize = 21;
370const CBDB: usize = 22;
371const CBSB: usize = 23;
372const CAPBD: usize = 24;
373const CAPBS: usize = 26;
374const CDGB: usize = 28;
375const CDDB: usize = 29;
376const CDSB: usize = 30;
377const QBS: usize = 33;
378const QBD: usize = 34;
379pub(crate) const BSIM1_NUM_STATES: usize = 35;
380
381// Slot order (matches `b1set.c`'s TSTALLOC list — same as MOS1's).
382const D_D: usize = 0;
383const G_G: usize = 1;
384const S_S: usize = 2;
385const B_B: usize = 3;
386const DP_DP: usize = 4;
387const SP_SP: usize = 5;
388const D_DP: usize = 6;
389const G_B: usize = 7;
390const G_DP: usize = 8;
391const G_SP: usize = 9;
392const S_SP: usize = 10;
393const B_DP: usize = 11;
394const B_SP: usize = 12;
395const DP_SP: usize = 13;
396const DP_D: usize = 14;
397const B_G: usize = 15;
398const DP_G: usize = 16;
399const SP_G: usize = 17;
400const SP_S: usize = 18;
401const DP_B: usize = 19;
402const SP_B: usize = 20;
403const SP_DP: usize = 21;
404
405/// The overlap capacitances the AC and pole-zero loads add to the
406/// charge-model matrix (`B1GDoverlapCap`, `B1GSoverlapCap`,
407/// `B1GBoverlapCap` of `b1temp.c`; BSIM2 keeps them in `pParam`).
408pub(crate) struct BsimOverlaps {
409    pub gd: f64,
410    pub gs: f64,
411    pub gb: f64,
412}
413
414/// `B1pzLoad` (`b1pzld.c`; `b2pzld.c` is the same file reading the
415/// overlaps from `pParam`): the AC stamps of the charge-oriented
416/// capacitance matrix with `s` in place of `jω`, then the shared MOS
417/// conductances. Reads the small-signal values the `INITSMSIG` load
418/// left in `state0` at the BSIM1 offsets — BSIM2's state layout is
419/// the same (`b2defs.h` copies `bsim1def.h`; the offsets in
420/// `bsim2.rs` are identical).
421pub(crate) fn bsim_pz_load(
422    ctx: &mut crate::matrix::PzCtx,
423    s: &[Slot; 22],
424    st: usize,
425    mode: f64,
426    gdpr: f64,
427    gspr: f64,
428    ov: &BsimOverlaps,
429) {
430    let s0 = |o: usize| ctx.states.get(0, st + o);
431    let (xnrm, xrev) = if mode >= 0.0 { (1.0, 0.0) } else { (0.0, 1.0) };
432    let gm = s0(GM);
433    let gds = s0(GDS);
434    let gmbs = s0(GMBS);
435    let gbd = s0(GBD);
436    let gbs = s0(GBS);
437    let capbd = s0(CAPBD);
438    let capbs = s0(CAPBS);
439    let cggb = s0(CGGB);
440    let cgsb = s0(CGSB);
441    let cgdb = s0(CGDB);
442    let cbgb = s0(CBGB);
443    let cbsb = s0(CBSB);
444    let cbdb = s0(CBDB);
445    let cdgb = s0(CDGB);
446    let cdsb = s0(CDSB);
447    let cddb = s0(CDDB);
448    let xcdgb = cdgb - ov.gd;
449    let xcddb = cddb + capbd + ov.gd;
450    let xcdsb = cdsb;
451    let xcsgb = -(cggb + cbgb + cdgb + ov.gs);
452    let xcsdb = -(cgdb + cbdb + cddb);
453    let xcssb = capbs + ov.gs - (cgsb + cbsb + cdsb);
454    let xcggb = cggb + ov.gd + ov.gs + ov.gb;
455    let xcgdb = cgdb - ov.gd;
456    let xcgsb = cgsb - ov.gs;
457    let xcbgb = cbgb - ov.gb;
458    let xcbdb = cbdb - capbd;
459    let xcbsb = cbsb - capbs;
460
461    ctx.add_cap(s[G_G], xcggb);
462    ctx.add_cap(s[B_B], -xcbgb - xcbdb - xcbsb);
463    ctx.add_cap(s[DP_DP], xcddb);
464    ctx.add_cap(s[SP_SP], xcssb);
465    ctx.add_cap(s[G_B], -xcggb - xcgdb - xcgsb);
466    ctx.add_cap(s[G_DP], xcgdb);
467    ctx.add_cap(s[G_SP], xcgsb);
468    ctx.add_cap(s[B_G], xcbgb);
469    ctx.add_cap(s[B_DP], xcbdb);
470    ctx.add_cap(s[B_SP], xcbsb);
471    ctx.add_cap(s[DP_G], xcdgb);
472    ctx.add_cap(s[DP_B], -xcdgb - xcddb - xcdsb);
473    ctx.add_cap(s[DP_SP], xcdsb);
474    ctx.add_cap(s[SP_G], xcsgb);
475    ctx.add_cap(s[SP_B], -xcsgb - xcsdb - xcssb);
476    ctx.add_cap(s[SP_DP], xcsdb);
477    let g = MosPzConductances {
478        xnrm,
479        xrev,
480        gm,
481        gds,
482        gmbs,
483        gbd,
484        gbs,
485        gdpr,
486        gspr,
487    };
488    mos_pz_conductances(ctx, s, &g);
489}
490
491/// Everything `B1evaluate` hands back.
492#[derive(Clone, Copy, Default)]
493struct Eval {
494    gm: f64,
495    gds: f64,
496    gmbs: f64,
497    qg: f64,
498    qb: f64,
499    qd: f64,
500    cggb: f64,
501    cgdb: f64,
502    cgsb: f64,
503    cbgb: f64,
504    cbdb: f64,
505    cbsb: f64,
506    cdgb: f64,
507    cddb: f64,
508    cdsb: f64,
509    cdrain: f64,
510    von: f64,
511    vdsat: f64,
512}
513
514pub(crate) struct Bsim1I {
515    drain: Var,
516    gate: Var,
517    source: Var,
518    bulk: Var,
519    d_p: Var,
520    s_p: Var,
521    t: Bsim1Size,
522    geom: MosGeom,
523    off: bool,
524    ic_vds: f64,
525    ic_vgs: f64,
526    ic_vbs: f64,
527    ic_given: crate::devices::IcGiven,
528    st: usize,
529    s: [Slot; 22],
530    mode: f64,
531    von: f64,
532    vdsat: f64,
533    /// `B1dSetup`'s Taylor coefficients (`b1disto.rs`).
534    dist: crate::b1disto::B1DistoCoeffs,
535}
536
537impl Bsim1I {
538    #[allow(clippy::too_many_arguments)]
539    pub fn new(
540        nodes: [Var; 4],
541        primes: [Var; 2],
542        t: Bsim1Size,
543        geom: MosGeom,
544        off: bool,
545        ic: (f64, f64, f64),
546        st: usize,
547    ) -> Self {
548        let von = t.vt0; // "added for initialization" (b1temp.c)
549        Bsim1I {
550            drain: nodes[0],
551            gate: nodes[1],
552            source: nodes[2],
553            bulk: nodes[3],
554            d_p: primes[0],
555            s_p: primes[1],
556            t,
557            geom,
558            off,
559            ic_vds: ic.0,
560            ic_vgs: ic.1,
561            ic_vbs: ic.2,
562            ic_given: crate::devices::IcGiven::ALL,
563            st,
564            s: [TRASH; 22],
565            mode: 1.0,
566            von,
567            vdsat: 0.0,
568            dist: crate::b1disto::B1DistoCoeffs::default(),
569        }
570    }
571
572    /// `B1evaluate`: drain current, derivatives and terminal
573    /// charges. `vds ≥ 0` here; the caller swaps terminals for the
574    /// inverse mode.
575    #[allow(clippy::too_many_lines)]
576    fn evaluate(&self, vds: f64, vbs: f64, vgs: f64, charges: bool) -> Eval {
577        let t = &self.t;
578        let mut e = Eval::default();
579
580        let vfb = t.vfb;
581        let mut phi = t.phi;
582        let k1 = t.k1;
583        let k2 = t.k2;
584        let vdd = t.vdd;
585
586        let (ugs, d_ugs_dvbs);
587        let u = t.ugs + t.ugs_b * vbs;
588        if u <= 0.0 {
589            ugs = 0.0;
590            d_ugs_dvbs = 0.0;
591        } else {
592            ugs = u;
593            d_ugs_dvbs = t.ugs_b;
594        }
595        let (uds, d_uds_dvbs, d_uds_dvds);
596        let u = t.uds + t.uds_b * vbs + t.uds_d * (vds - vdd);
597        if u <= 0.0 {
598            uds = 0.0;
599            d_uds_dvbs = 0.0;
600            d_uds_dvds = 0.0;
601        } else {
602            let leff = self.geom.l * 1e6 - t.delta_l; // µm
603            uds = u / leff;
604            d_uds_dvbs = t.uds_b / leff;
605            d_uds_dvds = t.uds_d / leff;
606        }
607        let mut eta = t.eta + t.eta_b * vbs + t.eta_d * (vds - vdd);
608        let (d_eta_dvds, d_eta_dvbs);
609        if eta <= 0.0 {
610            eta = 0.0;
611            d_eta_dvds = 0.0;
612            d_eta_dvbs = 0.0;
613        } else if eta > 1.0 {
614            eta = 1.0;
615            d_eta_dvds = 0.0;
616            d_eta_dvbs = 0.0;
617        } else {
618            d_eta_dvds = t.eta_d;
619            d_eta_dvbs = t.eta_b;
620        }
621        let vpb = if vbs < 0.0 { phi - vbs } else { phi };
622        let sqrt_vpb = vpb.sqrt();
623        let von = vfb + phi + k1 * sqrt_vpb - k2 * vpb - eta * vds;
624        let vth = von;
625        e.von = von;
626        let d_vth_dvds = -eta - d_eta_dvds * vds;
627        let mut d_vth_dvbs = k2 - 0.5 * k1 / sqrt_vpb - d_eta_dvbs * vds;
628        let mut vgs_vth = vgs - vth;
629
630        let g = 1.0 - 1.0 / (1.744 + 0.8364 * vpb);
631        let mut a = 1.0 + 0.5 * g * k1 / sqrt_vpb;
632        a = f64::max(a, 1.0);
633        let arg = f64::max(1.0 + ugs * vgs_vth, 1.0);
634        let d_g_dvbs = -0.8364 * (1.0 - g) * (1.0 - g);
635        let d_a_dvbs = 0.25 * k1 / sqrt_vpb * (2.0 * d_g_dvbs + g / vpb);
636
637        let mut drain_current = 0.0;
638        let mut gm = 0.0;
639        let mut gds = 0.0;
640        let mut gmbs = 0.0;
641
642        if vgs_vth >= 0.0 {
643            // Quadratic interpolation for Beta0.
644            let beta_vds_0 = t.beta_zero + t.beta_zero_b * vbs;
645            let beta_vdd = t.beta_vdd + t.beta_vdd_b * vbs;
646            let d_beta_vdd_dvds = f64::max(t.beta_vdd_d, 0.0);
647            let (beta0, d_beta0_dvds, d_beta0_dvbs);
648            if vds > vdd {
649                beta0 = beta_vdd + d_beta_vdd_dvds * (vds - vdd);
650                d_beta0_dvds = d_beta_vdd_dvds;
651                d_beta0_dvbs = t.beta_vdd_b;
652            } else {
653                let vdd_square = vdd * vdd;
654                let c1 = (-beta_vdd + beta_vds_0 + d_beta_vdd_dvds * vdd) / vdd_square;
655                let c2 = 2.0 * (beta_vdd - beta_vds_0) / vdd - d_beta_vdd_dvds;
656                let d_beta_vds_0_dvbs = t.beta_zero_b;
657                let d_beta_vdd_dvbs = t.beta_vdd_b;
658                let d_c1_dvbs = (d_beta_vds_0_dvbs - d_beta_vdd_dvbs) / vdd_square;
659                let d_c2_dvbs = d_c1_dvbs * (-2.0) * vdd;
660                beta0 = (c1 * vds + c2) * vds + beta_vds_0;
661                d_beta0_dvds = 2.0 * c1 * vds + c2;
662                d_beta0_dvbs = d_c1_dvbs * vds * vds + d_c2_dvbs * vds + d_beta_vds_0_dvbs;
663            }
664
665            let beta = beta0 / arg;
666            let d_beta_dvgs = -beta * ugs / arg;
667            let d_beta_dvds = d_beta0_dvds / arg - d_beta_dvgs * d_vth_dvds;
668            let d_beta_dvbs = d_beta0_dvbs / arg + beta * ugs * d_vth_dvbs / arg
669                - beta * vgs_vth * d_ugs_dvbs / arg;
670
671            let mut vc = uds * vgs_vth / a;
672            if vc < 0.0 {
673                vc = 0.0;
674            }
675            let term1 = (1.0 + 2.0 * vc).sqrt();
676            let k = 0.5 * (1.0 + vc + term1);
677            e.vdsat = f64::max(vgs_vth / (a * k.sqrt()), 0.0);
678
679            if vds < e.vdsat {
680                // Triode region.
681                let argl1 = f64::max(1.0 + uds * vds, 1.0);
682                let argl2 = vgs_vth - 0.5 * a * vds;
683                drain_current = beta * argl2 * vds / argl1;
684                gm = (d_beta_dvgs * argl2 * vds + beta * vds) / argl1;
685                gds = (d_beta_dvds * argl2 * vds + beta * (vgs_vth - vds * d_vth_dvds - a * vds)
686                    - drain_current * (vds * d_uds_dvds + uds))
687                    / argl1;
688                gmbs = (d_beta_dvbs * argl2 * vds
689                    + beta * vds * (-d_vth_dvbs - 0.5 * vds * d_a_dvbs)
690                    - drain_current * vds * d_uds_dvbs)
691                    / argl1;
692            } else {
693                // Pinchoff (saturation) region.
694                let args1 = 1.0 + 1.0 / term1;
695                let d_vc_dvgs = uds / a;
696                let d_vc_dvds = vgs_vth * d_uds_dvds / a - d_vc_dvgs * d_vth_dvds;
697                let d_vc_dvbs =
698                    (vgs_vth * d_uds_dvbs - uds * (d_vth_dvbs + vgs_vth * d_a_dvbs / a)) / a;
699                let d_k_dvc = 0.5 * args1;
700                let d_k_dvgs = d_k_dvc * d_vc_dvgs;
701                let d_k_dvds = d_k_dvc * d_vc_dvds;
702                let d_k_dvbs = d_k_dvc * d_vc_dvbs;
703                let args2 = vgs_vth / a / k;
704                let args3 = args2 * vgs_vth;
705                drain_current = 0.5 * beta * args3;
706                gm = 0.5 * args3 * d_beta_dvgs + beta * args2 - drain_current * d_k_dvgs / k;
707                gds = 0.5 * args3 * d_beta_dvds
708                    - beta * args2 * d_vth_dvds
709                    - drain_current * d_k_dvds / k;
710                gmbs = 0.5 * d_beta_dvbs * args3
711                    - beta * args2 * d_vth_dvbs
712                    - drain_current * (d_a_dvbs / a + d_k_dvbs / k);
713            }
714        }
715
716        // Subthreshold (always evaluated unless N0 >= 200, per the
717        // Peter M. Lee fix of 6/8/90).
718        let n0 = t.n0;
719        if n0 < 200.0 {
720            let nb = t.nb;
721            let nd = t.nd;
722            let mut n = n0 + nb * vbs + nd * vds;
723            if n < 0.5 {
724                n = 0.5;
725            }
726            let warg1 = f64::exp(-vds / VT0);
727            let wds = 1.0 - warg1;
728            let wgs = f64::exp(vgs_vth / (n * VT0));
729            let vtsquare = VT0 * VT0;
730            let warg2 = 6.04965 * vtsquare * t.beta_zero;
731            let ilimit = 4.5 * vtsquare * t.beta_zero;
732            let iexp = warg2 * wgs * wds;
733            drain_current += ilimit * iexp / (ilimit + iexp);
734            let mut temp1 = ilimit / (ilimit + iexp);
735            temp1 *= temp1;
736            let mut temp3 = ilimit / (ilimit + wgs * warg2);
737            temp3 = temp3 * temp3 * warg2 * wgs;
738            gm += temp1 * iexp / (n * VT0);
739            gds += temp3 * (-wds / n / VT0 * (d_vth_dvds + vgs_vth * nd / n) + warg1 / VT0);
740            gmbs -= temp1 * iexp * (d_vth_dvbs + vgs_vth * nb / n) / (n * VT0);
741        }
742
743        // Charge computation.
744        if drain_current < 0.0 {
745            drain_current = 0.0;
746        }
747        if gm < 0.0 {
748            gm = 0.0;
749        }
750        if gds < 0.0 {
751            gds = 0.0;
752        }
753        if gmbs < 0.0 {
754            gmbs = 0.0;
755        }
756
757        // `WLCox = Cox·Leff·Weff·1e4` (Cox in F/cm², lengths in m).
758        let wl_cox =
759            t.cox * (self.geom.l - t.delta_l * 1e-6) * (self.geom.w - t.delta_w * 1e-6) * 1e4; // F
760
761        if !charges {
762            e.gm = f64::max(gm, 0.0);
763            e.gds = f64::max(gds, 0.0);
764            e.gmbs = f64::max(gmbs, 0.0);
765            e.cdrain = f64::max(drain_current, 0.0);
766            return e;
767        }
768
769        let g = 1.0 - 1.0 / (1.744 + 0.8364 * vpb);
770        let mut a = 1.0 + 0.5 * g * k1 / sqrt_vpb;
771        a = f64::max(a, 1.0);
772        phi = f64::max(0.1, phi);
773
774        let co4v15 = 4.0 / 15.0;
775        let vth0 = vfb + phi + k1 * sqrt_vpb;
776        vgs_vth = vgs - vth0;
777        let arg1 = a * vds;
778        let arg2 = vgs_vth - 0.5 * arg1;
779        let arg3 = vds - arg1;
780        let arg5 = arg1 * arg1;
781        d_vth_dvbs = -0.5 * k1 / sqrt_vpb;
782        let d_a_dvbs2 = 0.5 * k1 * (0.5 * g / vpb - 0.8364 * (1.0 - g) * (1.0 - g)) / sqrt_vpb;
783        let ent = f64::max(arg2, 1.0e-8);
784        let d_ent_dvds = -0.5 * a;
785        let d_ent_dvbs = -d_vth_dvbs - 0.5 * vds * d_a_dvbs2;
786        let vcom = vgs_vth * vgs_vth / 6.0 - 1.25e-1 * arg1 * vgs_vth + 2.5e-2 * arg5;
787        let vds_pinchoff = f64::max(vgs_vth / a, 0.0);
788        let vgb = vgs - vbs;
789        let vgb_vfb = vgb - vfb;
790
791        // The charge section's own dAdVbs (the C recomputes it).
792        let d_a_dvbs = d_a_dvbs2;
793
794        if t.xpart >= 1.0 {
795            // 0/100 partitioning.
796            if vgb_vfb < 0.0 {
797                // Accumulation.
798                e.qg = wl_cox * vgb_vfb;
799                e.qb = -e.qg;
800                e.cggb = wl_cox;
801                e.cbgb = -wl_cox;
802            } else if vgs < vth0 {
803                // Subthreshold.
804                e.qg = 0.5 * wl_cox * k1 * k1 * (-1.0 + (1.0 + 4.0 * vgb_vfb / (k1 * k1)).sqrt());
805                e.qb = -e.qg;
806                e.cggb = wl_cox / (1.0 + 4.0 * vgb_vfb / (k1 * k1)).sqrt();
807                e.cbgb = -e.cggb;
808            } else if vds < vds_pinchoff {
809                // Triode.
810                let ent_square = ent * ent;
811                let argl1 = 1.2e1 * ent_square;
812                let argl2 = 1.0 - a;
813                let argl3 = arg1 * vds;
814                let argl5 = if ent > 1.0e-8 { arg1 / ent } else { 2.0 };
815                let argl7 = argl5 / 1.2e1;
816                let argl8 = 6.0 * ent;
817                let argl9 = 0.125 * argl5 * argl5;
818                e.qg = wl_cox * (vgs - vfb - phi - 0.5 * vds + vds * argl7);
819                e.qb = wl_cox * (-vth0 + vfb + phi + 0.5 * arg3 - arg3 * argl7);
820                e.qd = -wl_cox * (0.5 * vgs_vth - 0.75 * arg1 + 0.125 * arg1 * argl5);
821                e.cggb = wl_cox * (1.0 - argl3 / argl1);
822                e.cgdb = wl_cox * (-0.5 + arg1 / argl8 - argl3 * d_ent_dvds / argl1);
823                let cgbb = wl_cox * (vds * vds * d_a_dvbs * ent - argl3 * d_ent_dvbs) / argl1;
824                e.cgsb = -(e.cggb + e.cgdb + cgbb);
825                e.cbgb = wl_cox * argl3 * argl2 / argl1;
826                e.cbdb = wl_cox * argl2 * (0.5 - arg1 / argl8 + argl3 * d_ent_dvds / argl1);
827                let cbbb = -wl_cox
828                    * (d_vth_dvbs
829                        + 0.5 * vds * d_a_dvbs
830                        + vds * vds * ((1.0 - 2.0 * a) * d_a_dvbs * ent - argl2 * a * d_ent_dvbs)
831                            / argl1);
832                e.cbsb = -(e.cbgb + e.cbdb + cbbb);
833                e.cdgb = -wl_cox * (0.5 - argl9);
834                e.cddb = wl_cox * (0.75 * a - 0.25 * a * arg1 / ent + argl9 * d_ent_dvds);
835                let cdbb = wl_cox
836                    * (0.5 * d_vth_dvbs
837                        + vds * d_a_dvbs * (0.75 - 0.25 * argl5)
838                        + argl9 * d_ent_dvbs);
839                e.cdsb = -(e.cdgb + e.cddb + cdbb);
840            } else {
841                // Saturation.
842                let args1 = 1.0 / (3.0 * a);
843                e.qg = wl_cox * (vgs - vfb - phi - vgs_vth * args1);
844                e.qb = wl_cox * (vfb + phi - vth0 + (1.0 - a) * vgs_vth * args1);
845                e.cggb = wl_cox * (1.0 - args1);
846                let cgbb = wl_cox * args1 * (d_vth_dvbs + vgs_vth * d_a_dvbs / a);
847                e.cgsb = -(e.cggb + cgbb);
848                e.cbgb = wl_cox * (args1 - 1.0 / 3.0);
849                let cbbb =
850                    -wl_cox * ((2.0 / 3.0 + args1) * d_vth_dvbs + vgs_vth * args1 * d_a_dvbs / a);
851                e.cbsb = -(e.cbgb + cbbb);
852            }
853        } else {
854            // 40/60 partitioning.
855            if vgb_vfb < 0.0 {
856                e.qg = wl_cox * vgb_vfb;
857                e.qb = -e.qg;
858                e.cggb = wl_cox;
859                e.cbgb = -wl_cox;
860            } else if vgs < vth0 {
861                e.qg = 0.5 * wl_cox * k1 * k1 * (-1.0 + (1.0 + 4.0 * vgb_vfb / (k1 * k1)).sqrt());
862                e.qb = -e.qg;
863                e.cggb = wl_cox / (1.0 + 4.0 * vgb_vfb / (k1 * k1)).sqrt();
864                e.cbgb = -e.cggb;
865            } else if vds < vds_pinchoff {
866                let vgs_vth_square = vgs_vth * vgs_vth;
867                let ent_square = ent * ent;
868                let argl1 = 1.2e1 * ent_square;
869                let argl2 = 1.0 - a;
870                let argl3 = arg1 * vds;
871                let argl4 = vcom / ent / ent_square;
872                let (argl5, argl6) = if ent > 1.0e-8 {
873                    (arg1 / ent, vcom / ent_square)
874                } else {
875                    (2.0, 4.0 / 1.5e1)
876                };
877                let argl7 = argl5 / 1.2e1;
878                let argl8 = 6.0 * ent;
879                e.qg = wl_cox * (vgs - vfb - phi - 0.5 * vds + vds * argl7);
880                e.qb = wl_cox * (-vth0 + vfb + phi + 0.5 * arg3 - arg3 * argl7);
881                e.qd = -wl_cox * (0.5 * (vgs_vth - arg1) + arg1 * argl6);
882                e.cggb = wl_cox * (1.0 - argl3 / argl1);
883                e.cgdb = wl_cox * (-0.5 + arg1 / argl8 - argl3 * d_ent_dvds / argl1);
884                let cgbb = wl_cox * (vds * vds * d_a_dvbs * ent - argl3 * d_ent_dvbs) / argl1;
885                e.cgsb = -(e.cggb + e.cgdb + cgbb);
886                e.cbgb = wl_cox * argl3 * argl2 / argl1;
887                e.cbdb = wl_cox * argl2 * (0.5 - arg1 / argl8 + argl3 * d_ent_dvds / argl1);
888                let cbbb = -wl_cox
889                    * (d_vth_dvbs
890                        + 0.5 * vds * d_a_dvbs
891                        + vds * vds * ((1.0 - 2.0 * a) * d_a_dvbs * ent - argl2 * a * d_ent_dvbs)
892                            / argl1);
893                e.cbsb = -(e.cbgb + e.cbdb + cbbb);
894                e.cdgb = -wl_cox
895                    * (0.5 + arg1 * (4.0 * vgs_vth - 1.5 * arg1) / argl1 - 2.0 * arg1 * argl4);
896                e.cddb = wl_cox
897                    * (0.5 * a + 2.0 * arg1 * d_ent_dvds * argl4
898                        - a * (2.0 * vgs_vth_square - 3.0 * arg1 * vgs_vth + 0.9 * arg5) / argl1);
899                let cdbb = wl_cox
900                    * (0.5 * d_vth_dvbs + 0.5 * vds * d_a_dvbs + 2.0 * arg1 * d_ent_dvbs * argl4
901                        - vds
902                            * (2.0 * vgs_vth_square * d_a_dvbs
903                                - 4.0 * a * vgs_vth * d_vth_dvbs
904                                - 3.0 * arg1 * vgs_vth * d_a_dvbs
905                                + 1.5 * a * arg1 * d_vth_dvbs
906                                + 0.9 * arg5 * d_a_dvbs)
907                            / argl1);
908                e.cdsb = -(e.cdgb + e.cddb + cdbb);
909            } else {
910                let args1 = 1.0 / (3.0 * a);
911                e.qg = wl_cox * (vgs - vfb - phi - vgs_vth * args1);
912                e.qb = wl_cox * (vfb + phi - vth0 + (1.0 - a) * vgs_vth * args1);
913                e.qd = -co4v15 * wl_cox * vgs_vth;
914                e.cggb = wl_cox * (1.0 - args1);
915                let cgbb = wl_cox * args1 * (d_vth_dvbs + vgs_vth * d_a_dvbs / a);
916                e.cgsb = -(e.cggb + cgbb);
917                e.cbgb = wl_cox * (args1 - 1.0 / 3.0);
918                let cbbb =
919                    -wl_cox * ((2.0 / 3.0 + args1) * d_vth_dvbs + vgs_vth * args1 * d_a_dvbs / a);
920                e.cbsb = -(e.cbgb + cbbb);
921                e.cdgb = -co4v15 * wl_cox;
922                let cdbb = co4v15 * wl_cox * d_vth_dvbs;
923                e.cdsb = -(e.cdgb + cdbb);
924            }
925        }
926
927        e.gm = f64::max(gm, 0.0);
928        e.gds = f64::max(gds, 0.0);
929        e.gmbs = f64::max(gmbs, 0.0);
930        e.cdrain = f64::max(drain_current, 0.0);
931        e
932    }
933}
934
935/// `B1evaluate` on a bare size/geometry (the `.disto` setup's
936/// cross-check): `(cdrain, qg, qb, qd)` at local `vds ≥ 0`.
937#[cfg(test)]
938pub(crate) fn evaluate_for_test(
939    t: Bsim1Size,
940    geom: MosGeom,
941    vds: f64,
942    vbs: f64,
943    vgs: f64,
944) -> (f64, f64, f64, f64) {
945    let i = Bsim1I::new([1, 2, 3, 4], [1, 3], t, geom, false, (0.0, 0.0, 0.0), 0);
946    let e = i.evaluate(vds, vbs, vgs, true);
947    (e.cdrain, e.qg, e.qb, e.qd)
948}
949
950impl crate::devices::Device for Bsim1I {
951    fn par_load(&self) -> bool {
952        true
953    }
954
955    /// `B1getic` (`b1getic.c`): VBS, VDS, VGS not given
956    /// on the card from the external nodes.
957    fn set_ic(&mut self, rhs: &[f64]) {
958        use crate::devices::getic;
959        let g = self.ic_given;
960        getic(g, 2, &mut self.ic_vbs, rhs, self.bulk, self.source);
961        getic(g, 0, &mut self.ic_vds, rhs, self.drain, self.source);
962        getic(g, 1, &mut self.ic_vgs, rhs, self.gate, self.source);
963    }
964
965    fn set_ic_given(&mut self, given: crate::devices::IcGiven) {
966        self.ic_given = given;
967    }
968
969    fn alloc(&mut self, mb: &mut MatrixBuilder) {
970        let (d, g, s, b) = (self.drain, self.gate, self.source, self.bulk);
971        let (dp, sp) = (self.d_p, self.s_p);
972        self.s = [
973            mb.slot(d, d),
974            mb.slot(g, g),
975            mb.slot(s, s),
976            mb.slot(b, b),
977            mb.slot(dp, dp),
978            mb.slot(sp, sp),
979            mb.slot(d, dp),
980            mb.slot(g, b),
981            mb.slot(g, dp),
982            mb.slot(g, sp),
983            mb.slot(s, sp),
984            mb.slot(b, dp),
985            mb.slot(b, sp),
986            mb.slot(dp, sp),
987            mb.slot(dp, d),
988            mb.slot(b, g),
989            mb.slot(dp, g),
990            mb.slot(sp, g),
991            mb.slot(sp, s),
992            mb.slot(dp, b),
993            mb.slot(sp, b),
994            mb.slot(sp, dp),
995        ];
996    }
997
998    /// `B1convTest` (`b1cvtest.c`).
999    fn conv_test(&self, ctx: &crate::matrix::ConvCtx) -> bool {
1000        let typ = self.t.type_;
1001        let st = self.st;
1002        let s0 = |o: usize| ctx.states.get(0, st + o);
1003        let vbs = typ * (ctx.rhs[self.bulk] - ctx.rhs[self.s_p]);
1004        let vgs = typ * (ctx.rhs[self.gate] - ctx.rhs[self.s_p]);
1005        let vds = typ * (ctx.rhs[self.d_p] - ctx.rhs[self.s_p]);
1006        let vbd = vbs - vds;
1007        let vgd = vgs - vds;
1008        let vgdo = s0(VGS) - s0(VDS);
1009        let delvbs = vbs - s0(VBS);
1010        let delvbd = vbd - s0(VBD);
1011        let delvgs = vgs - s0(VGS);
1012        let delvds = vds - s0(VDS);
1013        let delvgd = vgd - vgdo;
1014        let cdhat = if self.mode >= 0.0 {
1015            s0(CD) - s0(GBD) * delvbd + s0(GMBS) * delvbs + s0(GM) * delvgs + s0(GDS) * delvds
1016        } else {
1017            s0(CD) - (s0(GBD) - s0(GMBS)) * delvbd - s0(GM) * delvgd + s0(GDS) * delvds
1018        };
1019        let cbhat = s0(CBS) + s0(CBD) + s0(GBD) * delvbd + s0(GBS) * delvbs;
1020        let cd = s0(CD);
1021        let cb = s0(CBS) + s0(CBD);
1022        let tol = ctx.reltol * f64::max(cdhat.abs(), cd.abs()) + ctx.abstol;
1023        if (cdhat - cd).abs() >= tol {
1024            return false;
1025        }
1026        let tol = ctx.reltol * f64::max(cbhat.abs(), cb.abs()) + ctx.abstol;
1027        (cbhat - cb).abs() <= tol
1028    }
1029
1030    #[allow(clippy::too_many_lines)]
1031    fn load(&mut self, ctx: &mut LoadCtx) {
1032        let typ = self.t.type_;
1033        let st = self.st;
1034
1035        let drain_sat_cur = f64::max(self.geom.ad * self.t.js, 1.0e-15);
1036        let source_sat_cur = f64::max(self.geom.as_ * self.t.js, 1.0e-15);
1037        let von_inst = typ * self.von;
1038        let vt0_inst = typ * self.t.vt0;
1039
1040        let mut check = true;
1041        let (mut vbs, mut vgs, mut vds);
1042        if ctx.mode == InitMode::Smsig {
1043            vbs = ctx.states.get(0, st + VBS);
1044            vgs = ctx.states.get(0, st + VGS);
1045            vds = ctx.states.get(0, st + VDS);
1046        } else if ctx.mode == InitMode::Tran {
1047            vbs = ctx.states.get(1, st + VBS);
1048            vgs = ctx.states.get(1, st + VGS);
1049            vds = ctx.states.get(1, st + VDS);
1050        } else if ctx.mode == InitMode::Jct && !self.off {
1051            vds = typ * self.ic_vds;
1052            vgs = typ * self.ic_vgs;
1053            vbs = typ * self.ic_vbs;
1054            if vds == 0.0 && vgs == 0.0 && vbs == 0.0 && !(ctx.kind == AnKind::TranOp && ctx.uic) {
1055                vbs = -1.0;
1056                vgs = vt0_inst;
1057                vds = 0.0;
1058            }
1059        } else if matches!(ctx.mode, InitMode::Jct | InitMode::Fix) && self.off {
1060            vbs = 0.0;
1061            vgs = 0.0;
1062            vds = 0.0;
1063        } else {
1064            if ctx.mode == InitMode::Pred {
1065                let xfact = ctx.pred_xfact;
1066                let mut pred = |o: usize| {
1067                    let s1 = ctx.states.get(1, st + o);
1068                    ctx.states.set(0, st + o, s1);
1069                    (1.0 + xfact) * s1 - xfact * ctx.states.get(2, st + o)
1070                };
1071                vbs = pred(VBS);
1072                vgs = pred(VGS);
1073                vds = pred(VDS);
1074                let vbd0 = ctx.states.get(0, st + VBS) - ctx.states.get(0, st + VDS);
1075                ctx.states.set(0, st + VBD, vbd0);
1076                for o in [
1077                    CD, CBS, CBD, GM, GDS, GMBS, GBD, GBS, CGGB, CBGB, CBSB, CGDB, CGSB, CBDB,
1078                    CDGB, CDDB, CDSB,
1079                ] {
1080                    let v = ctx.states.get(1, st + o);
1081                    ctx.states.set(0, st + o, v);
1082                }
1083            } else {
1084                vbs = typ * (ctx.sol[self.bulk] - ctx.sol[self.s_p]);
1085                vgs = typ * (ctx.sol[self.gate] - ctx.sol[self.s_p]);
1086                vds = typ * (ctx.sol[self.d_p] - ctx.sol[self.s_p]);
1087            }
1088            let mut vbd = vbs - vds;
1089            let mut vgd = vgs - vds;
1090            let vgdo = ctx.states.get(0, st + VGS) - ctx.states.get(0, st + VDS);
1091
1092            if ctx.states.get(0, st + VDS) >= 0.0 {
1093                vgs = fetlim(vgs, ctx.states.get(0, st + VGS), von_inst);
1094                vds = vgs - vgd;
1095                vds = limvds(vds, ctx.states.get(0, st + VDS));
1096            } else {
1097                vgd = fetlim(vgd, vgdo, von_inst);
1098                vds = vgs - vgd;
1099                vds = -limvds(-vds, -ctx.states.get(0, st + VDS));
1100                vgs = vgd + vds;
1101            }
1102            if vds >= 0.0 {
1103                let vcrit = VT0 * f64::ln(VT0 / (ROOT2 * source_sat_cur));
1104                vbs = pnjlim(vbs, ctx.states.get(0, st + VBS), VT0, vcrit, &mut check);
1105            } else {
1106                let vcrit = VT0 * f64::ln(VT0 / (ROOT2 * drain_sat_cur));
1107                vbd = pnjlim(vbd, ctx.states.get(0, st + VBD), VT0, vcrit, &mut check);
1108                vbs = vbd + vds;
1109            }
1110        }
1111
1112        let vbd = vbs - vds;
1113        let vgd = vgs - vds;
1114        let vgb = vgs - vbs;
1115
1116        // Bulk junctions on `CONSTvt0` (not the device temperature).
1117        let (gbs, cbs);
1118        if vbs <= 0.0 {
1119            gbs = source_sat_cur / VT0 + ctx.gmin;
1120            cbs = gbs * vbs;
1121        } else {
1122            let evbs = f64::exp(vbs / VT0);
1123            gbs = source_sat_cur * evbs / VT0 + ctx.gmin;
1124            cbs = source_sat_cur * (evbs - 1.0) + ctx.gmin * vbs;
1125        }
1126        let (gbd, cbd);
1127        if vbd <= 0.0 {
1128            gbd = drain_sat_cur / VT0 + ctx.gmin;
1129            cbd = gbd * vbd;
1130        } else {
1131            let evbd = f64::exp(vbd / VT0);
1132            gbd = drain_sat_cur * evbd / VT0 + ctx.gmin;
1133            cbd = drain_sat_cur * (evbd - 1.0) + ctx.gmin * vbd;
1134        }
1135
1136        self.mode = if vds >= 0.0 { 1.0 } else { -1.0 };
1137        let mode = self.mode;
1138
1139        let charges = ctx.kind == AnKind::Tran
1140            || ctx.mode == InitMode::Smsig
1141            || (ctx.kind == AnKind::TranOp && ctx.uic);
1142
1143        // `B1evaluate`, with the terminal swap for the inverse mode:
1144        // the drain-referred charge/capacitance rows come back on
1145        // the source side when `vds < 0`.
1146        let ev = if vds >= 0.0 {
1147            self.evaluate(vds, vbs, vgs, charges)
1148        } else {
1149            self.evaluate(-vds, vbd, vgd, charges)
1150        };
1151        let (cggb, cgdb, cgsb, cbgb, cbdb, cbsb);
1152        let (cdgb, cddb, cdsb, csgb, cssb, csdb);
1153        let (qgate, qbulk, qdrn, qsrc);
1154        if vds >= 0.0 {
1155            cggb = ev.cggb;
1156            cgdb = ev.cgdb;
1157            cgsb = ev.cgsb;
1158            cbgb = ev.cbgb;
1159            cbdb = ev.cbdb;
1160            cbsb = ev.cbsb;
1161            cdgb = ev.cdgb;
1162            cddb = ev.cddb;
1163            cdsb = ev.cdsb;
1164            csgb = 0.0;
1165            cssb = 0.0;
1166            csdb = 0.0;
1167            qgate = ev.qg;
1168            qbulk = ev.qb;
1169            qdrn = ev.qd;
1170            qsrc = 0.0;
1171        } else {
1172            cggb = ev.cggb;
1173            cgsb = ev.cgdb;
1174            cgdb = ev.cgsb;
1175            cbgb = ev.cbgb;
1176            cbsb = ev.cbdb;
1177            cbdb = ev.cbsb;
1178            csgb = ev.cdgb;
1179            cssb = ev.cddb;
1180            csdb = ev.cdsb;
1181            cdgb = 0.0;
1182            cddb = 0.0;
1183            cdsb = 0.0;
1184            qgate = ev.qg;
1185            qbulk = ev.qb;
1186            qsrc = ev.qd;
1187            qdrn = 0.0;
1188        }
1189        let (gm, gds, gmbs, cdrain) = (ev.gm, ev.gds, ev.gmbs, ev.cdrain);
1190        self.von = typ * ev.von;
1191        self.vdsat = typ * ev.vdsat;
1192
1193        let cd = mode * cdrain - cbd;
1194
1195        // Depletion capacitances.
1196        let (mut capbs, mut capbd) = (0.0, 0.0);
1197        if charges {
1198            let t = &self.t;
1199            let czbd = t.cj * self.geom.ad;
1200            let czbs = t.cj * self.geom.as_;
1201            let czbdsw = t.cjsw * self.geom.pd;
1202            let czbssw = t.cjsw * self.geom.ps;
1203            let (phi_b, phi_bsw, mj, mjsw) = (t.pb, t.pbsw, t.mj, t.mjsw);
1204            if vbs < 0.0 {
1205                let arg = 1.0 - vbs / phi_b;
1206                let argsw = 1.0 - vbs / phi_bsw;
1207                let sarg = f64::exp(-mj * arg.ln());
1208                let sargsw = f64::exp(-mjsw * argsw.ln());
1209                ctx.states.set(
1210                    0,
1211                    st + QBS,
1212                    phi_b * czbs * (1.0 - arg * sarg) / (1.0 - mj)
1213                        + phi_bsw * czbssw * (1.0 - argsw * sargsw) / (1.0 - mjsw),
1214                );
1215                capbs = czbs * sarg + czbssw * sargsw;
1216            } else {
1217                ctx.states.set(
1218                    0,
1219                    st + QBS,
1220                    vbs * (czbs + czbssw)
1221                        + vbs * vbs * (czbs * mj * 0.5 / phi_b + czbssw * mjsw * 0.5 / phi_bsw),
1222                );
1223                capbs = czbs + czbssw + vbs * (czbs * mj / phi_b + czbssw * mjsw / phi_bsw);
1224            }
1225            if vbd < 0.0 {
1226                let arg = 1.0 - vbd / phi_b;
1227                let argsw = 1.0 - vbd / phi_bsw;
1228                let sarg = f64::exp(-mj * arg.ln());
1229                let sargsw = f64::exp(-mjsw * argsw.ln());
1230                ctx.states.set(
1231                    0,
1232                    st + QBD,
1233                    phi_b * czbd * (1.0 - arg * sarg) / (1.0 - mj)
1234                        + phi_bsw * czbdsw * (1.0 - argsw * sargsw) / (1.0 - mjsw),
1235                );
1236                capbd = czbd * sarg + czbdsw * sargsw;
1237            } else {
1238                ctx.states.set(
1239                    0,
1240                    st + QBD,
1241                    vbd * (czbd + czbdsw)
1242                        + vbd * vbd * (czbd * mj * 0.5 / phi_b + czbdsw * mjsw * 0.5 / phi_bsw),
1243                );
1244                capbd = czbd + czbdsw + vbd * (czbd * mj / phi_b + czbdsw * mjsw / phi_bsw);
1245            }
1246        }
1247
1248        // Convergence.
1249        if (!self.off || ctx.mode != InitMode::Fix) && check {
1250            ctx.noncon = true;
1251        }
1252
1253        ctx.states.set(0, st + VBS, vbs);
1254        ctx.states.set(0, st + VBD, vbd);
1255        ctx.states.set(0, st + VGS, vgs);
1256        ctx.states.set(0, st + VDS, vds);
1257        ctx.states.set(0, st + CD, cd);
1258        ctx.states.set(0, st + CBS, cbs);
1259        ctx.states.set(0, st + CBD, cbd);
1260        ctx.states.set(0, st + GM, gm);
1261        ctx.states.set(0, st + GDS, gds);
1262        ctx.states.set(0, st + GMBS, gmbs);
1263        ctx.states.set(0, st + GBD, gbd);
1264        ctx.states.set(0, st + GBS, gbs);
1265        ctx.states.set(0, st + CGGB, cggb);
1266        ctx.states.set(0, st + CGDB, cgdb);
1267        ctx.states.set(0, st + CGSB, cgsb);
1268        ctx.states.set(0, st + CBGB, cbgb);
1269        ctx.states.set(0, st + CBDB, cbdb);
1270        ctx.states.set(0, st + CBSB, cbsb);
1271        ctx.states.set(0, st + CDGB, cdgb);
1272        ctx.states.set(0, st + CDDB, cddb);
1273        ctx.states.set(0, st + CDSB, cdsb);
1274        ctx.states.set(0, st + CAPBS, capbs);
1275        ctx.states.set(0, st + CAPBD, capbd);
1276
1277        // Gate charges and companion conductances (`B1mosCap`).
1278        let (mut ceqqg, mut ceqqb, mut ceqqd) = (0.0, 0.0, 0.0);
1279        let mut gc = [0.0f64; 12]; // gcggb gcgdb gcgsb gcbgb gcbdb gcbsb gcdgb gcddb gcdsb gcsgb gcsdb gcssb
1280        let t = &self.t;
1281        if charges {
1282            let ag0 = ctx.ag[0];
1283            let (args, c_b, c_d): ([f64; 8], [f64; 3], [f64; 3]);
1284            if mode > 0.0 {
1285                args = [
1286                    t.ld_gd_overlap,
1287                    t.ld_gs_overlap,
1288                    t.ld_gb_overlap,
1289                    capbd,
1290                    capbs,
1291                    cggb,
1292                    cgdb,
1293                    cgsb,
1294                ];
1295                c_b = [cbgb, cbdb, cbsb];
1296                c_d = [cdgb, cddb, cdsb];
1297            } else {
1298                args = [
1299                    t.ld_gs_overlap,
1300                    t.ld_gd_overlap,
1301                    t.ld_gb_overlap,
1302                    capbs,
1303                    capbd,
1304                    cggb,
1305                    cgsb,
1306                    cgdb,
1307                ];
1308                c_b = [cbgb, cbsb, cbdb];
1309                c_d = [csgb, cssb, csdb];
1310            }
1311            // `B1mosCap`: equivalent conductances…
1312            let gcdgb_l = (c_d[0] - args[0]) * ag0;
1313            let gcddb_l = (c_d[1] + args[3] + args[0]) * ag0;
1314            let gcdsb_l = c_d[2] * ag0;
1315            let gcsgb_l = -(args[5] + c_b[0] + c_d[0] + args[1]) * ag0;
1316            let gcsdb_l = -(args[6] + c_b[1] + c_d[1]) * ag0;
1317            let gcssb_l = (args[4] + args[1] - (args[7] + c_b[2] + c_d[2])) * ag0;
1318            let gcggb_l = (args[5] + args[0] + args[1] + args[2]) * ag0;
1319            let gcgdb_l = (args[6] - args[0]) * ag0;
1320            let gcgsb_l = (args[7] - args[1]) * ag0;
1321            let gcbgb_l = (c_b[0] - args[2]) * ag0;
1322            let gcbdb_l = (c_b[1] - args[3]) * ag0;
1323            let gcbsb_l = (c_b[2] - args[4]) * ag0;
1324            // …and total terminal charges.
1325            let (vg1, vg2) = if mode > 0.0 { (vgd, vgs) } else { (vgs, vgd) };
1326            let qgd = args[0] * vg1;
1327            let qgs = args[1] * vg2;
1328            let qgb = args[2] * vgb;
1329            let qgate = qgate + qgd + qgs + qgb;
1330            let qbulk = qbulk - qgb;
1331            let (mut qdrn, mut qsrc) = (qdrn, qsrc);
1332            if mode > 0.0 {
1333                qdrn -= qgd;
1334                qsrc = -(qgate + qbulk + qdrn);
1335            } else {
1336                qsrc -= qgs;
1337                qdrn = -(qgate + qbulk + qsrc);
1338            }
1339            let _ = qsrc;
1340
1341            if mode > 0.0 {
1342                gc = [
1343                    gcggb_l, gcgdb_l, gcgsb_l, gcbgb_l, gcbdb_l, gcbsb_l, gcdgb_l, gcddb_l,
1344                    gcdsb_l, gcsgb_l, gcsdb_l, gcssb_l,
1345                ];
1346            } else {
1347                // The reverse call swaps d and s in the outputs.
1348                gc = [
1349                    gcggb_l, gcgsb_l, gcgdb_l, gcbgb_l, gcbsb_l, gcbdb_l, gcsgb_l, gcssb_l,
1350                    gcsdb_l, gcdgb_l, gcdsb_l, gcddb_l,
1351                ];
1352                // and the gc arrays above are already in (g,b,d,s)
1353                // row order for the swapped call, matching the C's
1354                // pointer permutation.
1355            }
1356
1357            ctx.states.set(0, st + QG, qgate);
1358            ctx.states
1359                .set(0, st + QD, qdrn - ctx.states.get(0, st + QBD));
1360            ctx.states.set(
1361                0,
1362                st + QB,
1363                qbulk + ctx.states.get(0, st + QBD) + ctx.states.get(0, st + QBS),
1364            );
1365
1366            if ctx.mode == InitMode::Smsig {
1367                return;
1368            }
1369            if ctx.mode == InitMode::Tran {
1370                for o in [QB, QG, QD] {
1371                    let v = ctx.states.get(0, st + o);
1372                    ctx.states.set(1, st + o, v);
1373                }
1374            }
1375            ctx.integrate(0.0, st + QB);
1376            ctx.integrate(0.0, st + QG);
1377            ctx.integrate(0.0, st + QD);
1378
1379            // Equivalent charge currents.
1380            let cqgate = ctx.states.get(0, st + IQG);
1381            let cqbulk = ctx.states.get(0, st + IQB);
1382            let cqdrn = ctx.states.get(0, st + IQD);
1383            ceqqg = cqgate - gc[0] * vgb + gc[1] * vbd + gc[2] * vbs;
1384            ceqqb = cqbulk - gc[3] * vgb + gc[4] * vbd + gc[5] * vbs;
1385            ceqqd = cqdrn - gc[6] * vgb + gc[7] * vbd + gc[8] * vbs;
1386            if ctx.mode == InitMode::Tran {
1387                for o in [IQB, IQG, IQD] {
1388                    let v = ctx.states.get(0, st + o);
1389                    ctx.states.set(1, st + o, v);
1390                }
1391            }
1392        }
1393
1394        // Load the current vector.
1395        let ceqbs = typ * (cbs - (gbs - ctx.gmin) * vbs);
1396        let ceqbd = typ * (cbd - (gbd - ctx.gmin) * vbd);
1397        let ceqqg = typ * ceqqg;
1398        let ceqqb = typ * ceqqb;
1399        let ceqqd = typ * ceqqd;
1400        let (xnrm, xrev, cdreq);
1401        if mode >= 0.0 {
1402            xnrm = 1.0;
1403            xrev = 0.0;
1404            cdreq = typ * (cdrain - gds * vds - gm * vgs - gmbs * vbs);
1405        } else {
1406            xnrm = 0.0;
1407            xrev = 1.0;
1408            cdreq = -typ * (cdrain + gds * vds - gm * vgd - gmbs * vbd);
1409        }
1410        ctx.add_rhs(self.gate, -ceqqg);
1411        ctx.add_rhs(self.bulk, -(ceqbs + ceqbd + ceqqb));
1412        ctx.add_rhs(self.d_p, ceqbd - cdreq - ceqqd);
1413        ctx.add_rhs(self.s_p, cdreq + ceqbs + ceqqg + ceqqb + ceqqd);
1414
1415        // Load the Y matrix.
1416        let gdpr = t.drain_conductance;
1417        let gspr = t.source_conductance;
1418        ctx.add(self.s[D_D], gdpr);
1419        ctx.add(self.s[G_G], gc[0]);
1420        ctx.add(self.s[S_S], gspr);
1421        ctx.add(self.s[B_B], gbd + gbs - gc[3] - gc[4] - gc[5]);
1422        ctx.add(self.s[DP_DP], gdpr + gds + gbd + xrev * (gm + gmbs) + gc[7]);
1423        ctx.add(
1424            self.s[SP_SP],
1425            gspr + gds + gbs + xnrm * (gm + gmbs) + gc[11],
1426        );
1427        ctx.add(self.s[D_DP], -gdpr);
1428        ctx.add(self.s[G_B], -gc[0] - gc[1] - gc[2]);
1429        ctx.add(self.s[G_DP], gc[1]);
1430        ctx.add(self.s[G_SP], gc[2]);
1431        ctx.add(self.s[S_SP], -gspr);
1432        ctx.add(self.s[B_G], gc[3]);
1433        ctx.add(self.s[B_DP], -gbd + gc[4]);
1434        ctx.add(self.s[B_SP], -gbs + gc[5]);
1435        ctx.add(self.s[DP_D], -gdpr);
1436        ctx.add(self.s[DP_G], (xnrm - xrev) * gm + gc[6]);
1437        ctx.add(
1438            self.s[DP_B],
1439            -gbd + (xnrm - xrev) * gmbs - gc[6] - gc[7] - gc[8],
1440        );
1441        ctx.add(self.s[DP_SP], -gds - xnrm * (gm + gmbs) + gc[8]);
1442        ctx.add(self.s[SP_G], -(xnrm - xrev) * gm + gc[9]);
1443        ctx.add(self.s[SP_S], -gspr);
1444        ctx.add(
1445            self.s[SP_B],
1446            -gbs - (xnrm - xrev) * gmbs - gc[9] - gc[10] - gc[11],
1447        );
1448        ctx.add(self.s[SP_DP], -gds - xrev * (gm + gmbs) + gc[10]);
1449    }
1450
1451    fn trunc(&self, t: &crate::integ::TerrCtx, time_step: &mut f64) {
1452        crate::integ::terr(self.st + QB, t, time_step);
1453        crate::integ::terr(self.st + QG, t, time_step);
1454        crate::integ::terr(self.st + QD, t, time_step);
1455    }
1456
1457    /// `B1noise` (ngspice-47): see [`b12_noise`].
1458    fn noise_suffixes(&self) -> Option<&'static [&'static str]> {
1459        Some(B12_NOISE_SUFFIXES)
1460    }
1461
1462    fn noise_dens(&self, ctx: &crate::matrix::NoiseCtx) -> Vec<f64> {
1463        let t = &self.t;
1464        let n = B12Noise {
1465            drain: self.drain,
1466            source: self.source,
1467            d_p: self.d_p,
1468            s_p: self.s_p,
1469            gdpr: t.drain_conductance,
1470            gspr: t.source_conductance,
1471            gm: ctx.states.get(0, self.st + GM),
1472            cd: ctx.states.get(0, self.st + CD),
1473            kf: t.kf,
1474            af: t.af,
1475            flicker_area: (self.geom.w - t.delta_w * 1e-6) * (self.geom.l - t.delta_l * 1e-6),
1476            cox: t.cox,
1477        };
1478        b12_noise(ctx, &n)
1479    }
1480
1481    /// `B1dSetup` (`dev/bsim1/b1dset.c`): see `b1disto.rs`.
1482    fn disto_setup(&mut self, ctx: &crate::disto::DistoSetupCtx) -> Result<(), &'static str> {
1483        self.dist = crate::b1disto::dsetup(
1484            &self.t,
1485            &self.geom,
1486            (self.gate, self.bulk, self.d_p, self.s_p),
1487            ctx.sol,
1488            ctx.gmin,
1489        );
1490        Ok(())
1491    }
1492
1493    /// `B1disto` (`dev/bsim1/b1disto.c`) in the load modes.
1494    fn disto_load(&self, ctx: &mut crate::disto::DistoCtx) {
1495        crate::b1disto::dload(&self.dist, (self.gate, self.bulk, self.d_p, self.s_p), ctx);
1496    }
1497
1498    /// `B1acLoad`.
1499    fn load_ac(&mut self, ctx: &mut AcCtx) {
1500        let st = self.st;
1501        let s0 = |o: usize| ctx.states.get(0, st + o);
1502        let (xnrm, xrev) = if self.mode < 0.0 {
1503            (0.0, 1.0)
1504        } else {
1505            (1.0, 0.0)
1506        };
1507        let gdpr = self.t.drain_conductance;
1508        let gspr = self.t.source_conductance;
1509        let gm = s0(GM);
1510        let gds = s0(GDS);
1511        let gmbs = s0(GMBS);
1512        let gbd = s0(GBD);
1513        let gbs = s0(GBS);
1514        let capbd = s0(CAPBD);
1515        let capbs = s0(CAPBS);
1516        let cggb = s0(CGGB);
1517        let cgsb = s0(CGSB);
1518        let cgdb = s0(CGDB);
1519        let cbgb = s0(CBGB);
1520        let cbsb = s0(CBSB);
1521        let cbdb = s0(CBDB);
1522        let cdgb = s0(CDGB);
1523        let cdsb = s0(CDSB);
1524        let cddb = s0(CDDB);
1525        let o = ctx.omega;
1526        let xcdgb = (cdgb - self.t.gd_overlap) * o;
1527        let xcddb = (cddb + capbd + self.t.gd_overlap) * o;
1528        let xcdsb = cdsb * o;
1529        let xcsgb = -(cggb + cbgb + cdgb + self.t.gs_overlap) * o;
1530        let xcsdb = -(cgdb + cbdb + cddb) * o;
1531        let xcssb = (capbs + self.t.gs_overlap - (cgsb + cbsb + cdsb)) * o;
1532        let xcggb = (cggb + self.t.gd_overlap + self.t.gs_overlap + self.t.gb_overlap) * o;
1533        let xcgdb = (cgdb - self.t.gd_overlap) * o;
1534        let xcgsb = (cgsb - self.t.gs_overlap) * o;
1535        let xcbgb = (cbgb - self.t.gb_overlap) * o;
1536        let xcbdb = (cbdb - capbd) * o;
1537        let xcbsb = (cbsb - capbs) * o;
1538
1539        ctx.add_b(self.s[G_G], xcggb);
1540        ctx.add_b(self.s[B_B], -xcbgb - xcbdb - xcbsb);
1541        ctx.add_b(self.s[DP_DP], xcddb);
1542        ctx.add_b(self.s[SP_SP], xcssb);
1543        ctx.add_b(self.s[G_B], -xcggb - xcgdb - xcgsb);
1544        ctx.add_b(self.s[G_DP], xcgdb);
1545        ctx.add_b(self.s[G_SP], xcgsb);
1546        ctx.add_b(self.s[B_G], xcbgb);
1547        ctx.add_b(self.s[B_DP], xcbdb);
1548        ctx.add_b(self.s[B_SP], xcbsb);
1549        ctx.add_b(self.s[DP_G], xcdgb);
1550        ctx.add_b(self.s[DP_B], -xcdgb - xcddb - xcdsb);
1551        ctx.add_b(self.s[DP_SP], xcdsb);
1552        ctx.add_b(self.s[SP_G], xcsgb);
1553        ctx.add_b(self.s[SP_B], -xcsgb - xcsdb - xcssb);
1554        ctx.add_b(self.s[SP_DP], xcsdb);
1555        ctx.add_g(self.s[D_D], gdpr);
1556        ctx.add_g(self.s[S_S], gspr);
1557        ctx.add_g(self.s[B_B], gbd + gbs);
1558        ctx.add_g(self.s[DP_DP], gdpr + gds + gbd + xrev * (gm + gmbs));
1559        ctx.add_g(self.s[SP_SP], gspr + gds + gbs + xnrm * (gm + gmbs));
1560        ctx.add_g(self.s[D_DP], -gdpr);
1561        ctx.add_g(self.s[S_SP], -gspr);
1562        ctx.add_g(self.s[B_DP], -gbd);
1563        ctx.add_g(self.s[B_SP], -gbs);
1564        ctx.add_g(self.s[DP_D], -gdpr);
1565        ctx.add_g(self.s[DP_G], (xnrm - xrev) * gm);
1566        ctx.add_g(self.s[DP_B], -gbd + (xnrm - xrev) * gmbs);
1567        ctx.add_g(self.s[DP_SP], -gds - xnrm * (gm + gmbs));
1568        ctx.add_g(self.s[SP_G], -(xnrm - xrev) * gm);
1569        ctx.add_g(self.s[SP_S], -gspr);
1570        ctx.add_g(self.s[SP_B], -gbs - (xnrm - xrev) * gmbs);
1571        ctx.add_g(self.s[SP_DP], -gds - xrev * (gm + gmbs));
1572    }
1573
1574    /// `B1pzLoad` (`b1pzld.c`): see [`bsim_pz_load`].
1575    fn load_pz(&mut self, ctx: &mut crate::matrix::PzCtx) -> Result<(), &'static str> {
1576        let ov = BsimOverlaps {
1577            gd: self.t.gd_overlap,
1578            gs: self.t.gs_overlap,
1579            gb: self.t.gb_overlap,
1580        };
1581        let (gdpr, gspr) = (self.t.drain_conductance, self.t.source_conductance);
1582        bsim_pz_load(ctx, &self.s, self.st, self.mode, gdpr, gspr, &ov);
1583        Ok(())
1584    }
1585}
1586
1587#[cfg(test)]
1588mod noise_tests {
1589    use crate::{Circuit, Options};
1590    use std::path::Path;
1591
1592    /// Run `regress/<stem>.cir` (one `.noise`) and compare every
1593    /// vector of both plots with the ngspice-47 golden: relative
1594    /// `1e-3` (the oracle's reltol; the residual is the 3f5-vs-CODATA
1595    /// constants, ~2e-5) plus an absolute floor far below the
1596    /// densities. Returns the worst relative error.
1597    fn compare(stem: &str, ngspice_offsets: bool, only: &[&str]) -> Result<f64, String> {
1598        let root = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../../corpus"));
1599        let text = std::fs::read_to_string(root.join(format!("regress/{stem}.cir"))).unwrap();
1600        let parsed = dune_netlist::parse_str(stem, &text);
1601        assert!(parsed.is_clean(), "{:?}", parsed.diags);
1602        let (c, _) = Circuit::from_netlist(&parsed.netlist).unwrap();
1603        let mut opts = Options::default();
1604        let mut d = Vec::new();
1605        opts.apply(&parsed.netlist.options, &mut d);
1606        super::NGSPICE_STATE_OFFSET.set(ngspice_offsets.then_some(0.0));
1607        let run = c.run(&parsed.netlist.analyses[0], &opts);
1608        super::NGSPICE_STATE_OFFSET.set(None);
1609        let (sols, _) = run.unwrap();
1610        let golden = dune_raw::Rawfile::read_from(root.join(format!("golden/{stem}.raw"))).unwrap();
1611        assert_eq!(sols.len(), golden.plots.len());
1612        let mut worst = 0.0f64;
1613        for (sol, plot) in sols.iter().zip(&golden.plots) {
1614            for v in &plot.variables {
1615                if !only.is_empty() && !only.iter().any(|o| v.name.contains(o)) {
1616                    continue;
1617                }
1618                let want = plot.real_values(&v.name).unwrap();
1619                let got = sol
1620                    .column(&v.name)
1621                    .ok_or_else(|| format!("{stem}: Dune lacks {}", v.name))?;
1622                for (k, (a, b)) in got.iter().zip(want).enumerate() {
1623                    let e = (a - b).abs() / (b.abs() + 1e-30);
1624                    if (a - b).abs() > 1e-3 * b.abs() + 1e-25 {
1625                        return Err(format!("{stem} {}[{k}]: Dune {a} vs ngspice {b}", v.name));
1626                    }
1627                    worst = worst.max(e);
1628                }
1629            }
1630        }
1631        Ok(worst)
1632    }
1633
1634    /// The gate of the `b1noise`/`b2noise` probes (in `corpus/SKIP`
1635    /// for the oracle): with ngspice-47's state-offset reading of gm
1636    /// and Id emulated (`GENstate = 0`, the only BSIM in each deck),
1637    /// Dune reproduces the golden vector for vector — so the nodes,
1638    /// conductances, coefficients, W/L/DW/DL/Cox and the integration
1639    /// are b1noi.c/b2noi.c's — and without it, the channel and
1640    /// flicker vectors leave the oracle by orders of magnitude while
1641    /// rd/rs stay equal.
1642    #[test]
1643    fn b12_noise_matches_ngspice_offsets() {
1644        for stem in ["b1noise", "b2noise"] {
1645            let worst = compare(stem, true, &[]).unwrap();
1646            eprintln!("{stem}: worst relative error {worst:.2e} (ngspice offsets emulated)");
1647            assert!(worst < 1e-4, "{stem}: {worst}");
1648            // Dune's own reading: series resistances (and frequency)
1649            // unchanged, channel and flicker not.
1650            compare(stem, false, &["frequency", "m1_rd", "m1_rs"]).unwrap();
1651            compare(stem, false, &["m1_id"]).unwrap_err();
1652            compare(stem, false, &["m1_1overf"]).unwrap_err();
1653        }
1654    }
1655}