Skip to main content

dune_core/
circuit.rs

1//! Public circuit construction (D-0008): one typed spec per device,
2//! added to a [`Circuit`]; name/node resolution happens at build
3//! time so declaration order never matters (the deck's order does
4//! not either).
5
6use std::collections::HashMap;
7
8use crate::diode::{DiodeModel, DiodeTemp};
9use crate::error::Error;
10use crate::matrix::Var;
11use crate::options::Options;
12use crate::trnoise::NoiseWave;
13
14/// `RXXXXXXX N1 N2 VALUE` (ohms; nonzero).
15#[derive(Clone, Debug)]
16pub struct Resistor {
17    pub name: String,
18    pub nodes: [String; 2],
19    pub value: f64,
20}
21
22/// The temperature dependence of one resistor (`REStemp`,
23/// `dev/res/restemp.c`): `R(T) = R(TNOM)·[1 + TC1·(T−TNOM) +
24/// TC2·(T−TNOM)²]`. Temperatures in kelvin; `None` takes the
25/// circuit's `TEMP`/`TNOM` (`.options`, or the `.dc TEMP` sweep
26/// point).
27#[derive(Clone, Copy, Debug, Default)]
28pub(crate) struct ResTemp {
29    pub tc1: f64,
30    pub tc2: f64,
31    /// Model `TNOM`.
32    pub tnom: Option<f64>,
33    /// Instance `TEMP=`.
34    pub temp: Option<f64>,
35    /// Instance `DTEMP=` (ngspice-47 only, kelvin difference): the
36    /// instance runs at `CKTtemp + DTEMP` when `TEMP=` is not given
37    /// (`restemp.c`: a given `TEMP=` zeroes it).
38    pub dtemp: f64,
39}
40
41/// `CXXXXXXX N+ N- VALUE <IC=INCOND>` (farads).
42#[derive(Clone, Debug)]
43pub struct Capacitor {
44    pub name: String,
45    pub nodes: [String; 2],
46    pub value: f64,
47    /// Initial voltage, used under `.tran … UIC`.
48    pub ic: Option<f64>,
49}
50
51/// `LYYYYYYY N+ N- VALUE <IC=INCOND>` (henries).
52#[derive(Clone, Debug)]
53pub struct Inductor {
54    pub name: String,
55    pub nodes: [String; 2],
56    pub value: f64,
57    /// Initial current, used under `.tran … UIC`.
58    pub ic: Option<f64>,
59}
60
61/// `KXXXXXXX L1 L2 K`. No effect at the operating point.
62#[derive(Clone, Debug)]
63pub struct Mutual {
64    pub name: String,
65    pub l1: String,
66    pub l2: String,
67    pub k: f64,
68}
69
70/// `VXXXXXXX N+ N- <DC VALUE> <tranfn(…)>`.
71#[derive(Clone, Debug)]
72pub struct Vsource {
73    pub name: String,
74    pub nodes: [String; 2],
75    /// `None` when no DC value was given (transient functions then
76    /// also provide the DC-mode value, like `VSRCload`).
77    pub dc: Option<f64>,
78    pub wave: Option<crate::srcfn::Wave>,
79    /// `AC <MAG <PHASE°>>`: the small-signal stimulus.
80    pub ac: Option<(f64, f64)>,
81    /// `DISTOF1 <MAG <PHASE°>>`: the `.disto` input at f1.
82    pub distof1: Option<(f64, f64)>,
83    /// `DISTOF2 <MAG <PHASE°>>`: the `.disto` input at f2.
84    pub distof2: Option<(f64, f64)>,
85    /// `PORTNUM N <Z0 Z>`: makes the source an ngspice RF port —
86    /// in every analysis it then drives its `+` node through the
87    /// series reference impedance (internal node `<name>#res`), and
88    /// `.sp` excites it. `None` for an ordinary source.
89    pub port: Option<RfPort>,
90}
91
92/// An ngspice RF port (`vsrc.c` under `RFSPICE`, ngspice-47):
93/// `PORTNUM num Z0 z0`. The source is a port only when `num > 0`
94/// and `z0 > 0` (`VSRCtemp`); otherwise the field is inert. The
95/// ports of a circuit must be numbered `1..=N` without gaps or
96/// duplicates.
97#[derive(Clone, Copy, Debug, PartialEq)]
98pub struct RfPort {
99    /// Port index, 1-based (`VSRCportNum`).
100    pub num: u32,
101    /// Reference impedance in ohms (`VSRCportZ0`; 50 on a card that
102    /// gives none).
103    pub z0: f64,
104}
105
106impl RfPort {
107    /// `VSRCisPort`.
108    pub(crate) fn is_port(&self) -> bool {
109        self.num > 0 && self.z0 > 0.0
110    }
111}
112
113/// `IYYYYYYY N+ N- <DC VALUE> <tranfn(…)>` (flows through the source
114/// from `+` to `-`).
115#[derive(Clone, Debug)]
116pub struct Isource {
117    pub name: String,
118    pub nodes: [String; 2],
119    pub dc: Option<f64>,
120    pub wave: Option<crate::srcfn::Wave>,
121    /// `AC <MAG <PHASE°>>`.
122    pub ac: Option<(f64, f64)>,
123    /// `DISTOF1 <MAG <PHASE°>>`.
124    pub distof1: Option<(f64, f64)>,
125    /// `DISTOF2 <MAG <PHASE°>>`.
126    pub distof2: Option<(f64, f64)>,
127}
128
129/// `EXXXXXXX N+ N- NC+ NC- VALUE` (VCVS).
130#[derive(Clone, Debug)]
131pub struct Vcvs {
132    pub name: String,
133    pub nodes: [String; 4],
134    pub gain: f64,
135}
136
137/// `GXXXXXXX N+ N- NC+ NC- VALUE` (VCCS, mhos).
138#[derive(Clone, Debug)]
139pub struct Vccs {
140    pub name: String,
141    pub nodes: [String; 4],
142    pub gain: f64,
143}
144
145/// `FXXXXXXX N+ N- VNAM VALUE` (CCCS).
146#[derive(Clone, Debug)]
147pub struct Cccs {
148    pub name: String,
149    pub nodes: [String; 2],
150    pub ctrl: String,
151    pub gain: f64,
152}
153
154/// `HXXXXXXX N+ N- VNAM VALUE` (CCVS, ohms).
155#[derive(Clone, Debug)]
156pub struct Ccvs {
157    pub name: String,
158    pub nodes: [String; 2],
159    pub ctrl: String,
160    pub gain: f64,
161}
162
163/// `BXXXXXXX N+ N- V=EXPR` or `I=EXPR`: the arbitrary source of the
164/// 3f5 (`dev/asrc/`). `expr` is the parse tree
165/// [`dune_netlist::parse_expr`] builds; its `v(node)` and
166/// `i(device)` references resolve at build time like every other
167/// name (an `i()` must name a device with a branch equation: V, E,
168/// H, L or a voltage-type B). A `Voltage` source drives `v(N+) −
169/// v(N−)` to the expression; a `Current` one makes it flow from `N+`
170/// through the source to `N−`.
171#[derive(Clone, Debug)]
172pub struct Asrc {
173    pub name: String,
174    pub nodes: [String; 2],
175    pub kind: dune_netlist::ir::SourceKind,
176    pub expr: dune_netlist::ir::Expr,
177}
178
179/// `QXXXXXXX NC NB NE <NS> MNAME <AREA> <OFF> <IC=VBE,VCE> <TEMP=T>`
180/// with its model inline. `nodes[3]` is ground when no substrate was
181/// given.
182#[derive(Clone, Debug)]
183pub struct Bjt {
184    pub name: String,
185    /// Collector, base, emitter, substrate.
186    pub nodes: [String; 4],
187    pub model: crate::bjt::BjtModel,
188    pub area: f64,
189    pub off: bool,
190    /// `IC=VBE,VCE`.
191    pub ic: (f64, f64),
192    /// Instance temperature override, kelvin.
193    pub temp: Option<f64>,
194}
195
196/// `MXXXXXXX ND NG NS NB MNAME <L=> <W=> <AD=> <AS=> <PD=> <PS=>
197/// <NRD=> <NRS=> <OFF> <IC=VDS,VGS,VBS> <TEMP=T>` with its level-1
198/// model inline. `l`/`w`/`ad`/`as` fall back to the
199/// `.options defl/defw/defad/defas` values at build time.
200#[derive(Clone, Debug)]
201pub struct Mos1 {
202    pub name: String,
203    /// Drain, gate, source, bulk.
204    pub nodes: [String; 4],
205    pub model: crate::mos1::Mos1Model,
206    pub l: Option<f64>,
207    pub w: Option<f64>,
208    pub ad: Option<f64>,
209    pub as_: Option<f64>,
210    pub pd: f64,
211    pub ps: f64,
212    /// Squares of drain/source diffusion (`NRD`/`NRS`, default 1).
213    pub nrd: f64,
214    pub nrs: f64,
215    pub off: bool,
216    /// `IC=VDS,VGS,VBS`.
217    pub ic: (f64, f64, f64),
218    /// Instance temperature override, kelvin.
219    pub temp: Option<f64>,
220}
221
222/// Like [`Mos1`], but with the level-2 (Grove-Frohman) model. The
223/// 3f5 names its prime nodes `m1#internal#drain`/`#internal#source`
224/// (`mos2set.c`).
225#[derive(Clone, Debug)]
226pub struct Mos2 {
227    pub name: String,
228    /// Drain, gate, source, bulk.
229    pub nodes: [String; 4],
230    pub model: crate::mos2::Mos2Model,
231    pub l: Option<f64>,
232    pub w: Option<f64>,
233    pub ad: Option<f64>,
234    pub as_: Option<f64>,
235    pub pd: f64,
236    pub ps: f64,
237    pub nrd: f64,
238    pub nrs: f64,
239    pub off: bool,
240    /// `IC=VDS,VGS,VBS`.
241    pub ic: (f64, f64, f64),
242    /// Instance temperature override, kelvin.
243    pub temp: Option<f64>,
244}
245
246/// Like [`Mos1`], but with the level-3 (semi-empirical
247/// short-channel) model. Prime nodes are named like MOS2's,
248/// `m1#internal#drain`/`#internal#source` (`mos3set.c`).
249#[derive(Clone, Debug)]
250pub struct Mos3 {
251    pub name: String,
252    /// Drain, gate, source, bulk.
253    pub nodes: [String; 4],
254    pub model: crate::mos3::Mos3Model,
255    pub l: Option<f64>,
256    pub w: Option<f64>,
257    pub ad: Option<f64>,
258    pub as_: Option<f64>,
259    pub pd: f64,
260    pub ps: f64,
261    pub nrd: f64,
262    pub nrs: f64,
263    pub off: bool,
264    /// `IC=VDS,VGS,VBS`.
265    pub ic: (f64, f64, f64),
266    /// Instance temperature override, kelvin.
267    pub temp: Option<f64>,
268}
269
270/// Like [`Mos1`], but with the level-6 (Sakurai-Newton) model.
271#[derive(Clone, Debug)]
272pub struct Mos6 {
273    pub name: String,
274    /// Drain, gate, source, bulk.
275    pub nodes: [String; 4],
276    pub model: crate::mos6::Mos6Model,
277    pub l: Option<f64>,
278    pub w: Option<f64>,
279    pub ad: Option<f64>,
280    pub as_: Option<f64>,
281    pub pd: f64,
282    pub ps: f64,
283    /// `NRD`/`NRS`; the 3f5 forces 1 when 0 (`mos6temp.c`).
284    pub nrd: f64,
285    pub nrs: f64,
286    pub off: bool,
287    /// `IC=VDS,VGS,VBS`.
288    pub ic: (f64, f64, f64),
289    /// Instance temperature override, kelvin.
290    pub temp: Option<f64>,
291}
292
293/// Like [`Mos1`], but with the BSIM1 (level-4) model.
294#[derive(Clone, Debug)]
295pub struct Bsim1 {
296    pub name: String,
297    /// Drain, gate, source, bulk.
298    pub nodes: [String; 4],
299    pub model: crate::bsim1::Bsim1Model,
300    pub l: Option<f64>,
301    pub w: Option<f64>,
302    pub ad: Option<f64>,
303    pub as_: Option<f64>,
304    pub pd: f64,
305    pub ps: f64,
306    pub nrd: f64,
307    pub nrs: f64,
308    pub off: bool,
309    /// `IC=VDS,VGS,VBS`.
310    pub ic: (f64, f64, f64),
311}
312
313/// Like [`Mos1`], but with the BSIM2 (level-5) model.
314#[derive(Clone, Debug)]
315pub struct Bsim2 {
316    pub name: String,
317    /// Drain, gate, source, bulk.
318    pub nodes: [String; 4],
319    pub model: crate::bsim2::Bsim2Model,
320    pub l: Option<f64>,
321    pub w: Option<f64>,
322    pub ad: Option<f64>,
323    pub as_: Option<f64>,
324    pub pd: f64,
325    pub ps: f64,
326    pub nrd: f64,
327    pub nrs: f64,
328    pub off: bool,
329    /// `IC=VDS,VGS,VBS`.
330    pub ic: (f64, f64, f64),
331}
332
333/// A BSIM3v3.3.0 MOSFET (`LEVEL=8`/`49`, ngspice-47's instance
334/// line `MXXX D G S B MNAME <L=> <W=> <AD=> <AS=> <PD=> <PS=> <NRD=>
335/// <NRS=> <M=> <DELVTO=> <MULU0=> <OFF> <IC=VDS,VGS,VBS>`, `b3.c`).
336/// L and W default to 5 µm (`b3set.c`), not the `.options` MOS
337/// defaults.
338#[derive(Clone, Debug)]
339pub struct Bsim3 {
340    pub name: String,
341    /// Drain, gate, source, bulk.
342    pub nodes: [String; 4],
343    pub model: crate::bsim3::Bsim3Model,
344    pub l: Option<f64>,
345    pub w: Option<f64>,
346    pub ad: f64,
347    pub as_: f64,
348    pub pd: f64,
349    pub ps: f64,
350    pub nrd: f64,
351    pub nrs: f64,
352    /// `M`: parallel multiplier (1).
353    pub m: f64,
354    /// `DELVTO`: threshold shift added to `VTH0` (0).
355    pub delvto: f64,
356    /// `MULU0`: low-field mobility multiplier (1).
357    pub mulu0: f64,
358    pub off: bool,
359    /// `IC=VDS,VGS,VBS`.
360    pub ic: (f64, f64, f64),
361}
362
363/// `DXXXXXXX N+ N- MNAME <AREA> <OFF>` with its model inline.
364#[derive(Clone, Debug)]
365pub struct Diode {
366    pub name: String,
367    pub nodes: [String; 2],
368    pub model: DiodeModel,
369    pub area: f64,
370    pub off: bool,
371    /// Instance temperature override, kelvin (`TEMP=` is °C on the
372    /// card).
373    pub temp: Option<f64>,
374    /// `IC=VD`: initial junction voltage under `.tran … UIC`.
375    pub ic: Option<f64>,
376}
377
378/// `JXXXXXXX ND NG NS MNAME <AREA> <OFF> <IC=VDS,VGS> <TEMP=T>` with
379/// its model inline. Prime nodes past `RD`/`RS` are named
380/// `<name>#drain`/`<name>#source` (`JFETsetup`'s `CKTmkVolt` calls).
381#[derive(Clone, Debug)]
382pub struct Jfet {
383    pub name: String,
384    /// Drain, gate, source.
385    pub nodes: [String; 3],
386    pub model: crate::jfet::JfetModel,
387    pub area: f64,
388    pub off: bool,
389    /// `IC=VDS,VGS`.
390    pub ic: (f64, f64),
391    /// Instance temperature override, kelvin.
392    pub temp: Option<f64>,
393}
394
395/// `ZXXXXXXX ND NG NS MNAME <AREA> <OFF> <IC=VDS,VGS>` (MESFET) with
396/// its model inline; prime nodes as for [`Jfet`]. The 3f5 MESFET has
397/// no instance temperature.
398#[derive(Clone, Debug)]
399pub struct Mes {
400    pub name: String,
401    /// Drain, gate, source.
402    pub nodes: [String; 3],
403    pub model: crate::mes::MesModel,
404    pub area: f64,
405    pub off: bool,
406    /// `IC=VDS,VGS`.
407    pub ic: (f64, f64),
408}
409
410/// `OXXXXXXX N1 N2 N3 N4 MNAME`: a lossy transmission line (LTRA)
411/// between port 1 (`N1`, `N2`) and port 2 (`N3`, `N4`), with its
412/// model inline. The two port currents are extra unknowns named
413/// `<name>#i1`/`<name>#i2` (`LTRAsetup`'s `CKTmkVolt` calls).
414#[derive(Clone, Debug)]
415pub struct Ltra {
416    pub name: String,
417    /// Port 1 +/−, port 2 +/−.
418    pub nodes: [String; 4],
419    pub model: crate::ltra::LtraModel,
420}
421
422/// `TXXXXXXX N1 N2 N3 N4 Z0=VALUE <TD=VALUE | F=FREQ <NL=NRMLEN>>
423/// <IC=V1,I1,V2,I2>`: an ideal (lossless) transmission line between
424/// port 1 (`N1`, `N2`) and port 2 (`N3`, `N4`) (`dev/tra/`). Four
425/// extra unknowns, named like `TRAsetup`'s `CKTmkVolt` calls:
426/// `<name>#i1`, `<name>#i2` (port currents into `N1`/`N3`) and
427/// `<name>#int1`, `<name>#int2` (the nodes between each port's `Z0`
428/// and its delayed source).
429#[derive(Clone, Debug)]
430pub struct Tra {
431    pub name: String,
432    /// Port 1 +/−, port 2 +/−.
433    pub nodes: [String; 4],
434    /// `Z0` (or `ZO`), ohms; required.
435    pub z0: f64,
436    /// `TD`, seconds. When absent the delay is `NL/F`.
437    pub td: Option<f64>,
438    /// `F`: the frequency `NL` refers to (default 1 GHz).
439    pub f: Option<f64>,
440    /// `NL`: normalized electrical length at `F` (default 0.25).
441    pub nl: Option<f64>,
442    /// `IC=V1,I1,V2,I2` (also `V1=`, `I1=`, `V2=`, `I2=`): port
443    /// voltages and currents used under `.tran … UIC`.
444    pub ic: [f64; 4],
445}
446
447/// `SXXXXXXX N+ N- NC+ NC- MODEL <ON|OFF>`: voltage-controlled
448/// switch with its model inline (`dev/sw/`).
449#[derive(Clone, Debug)]
450pub struct SwitchV {
451    pub name: String,
452    /// Switch +/−, control +/−.
453    pub nodes: [String; 4],
454    pub model: crate::switch::SwModel,
455    /// `ON` (true) or `OFF`/nothing: the state the operating point
456    /// starts from (`SWzero_stateGiven`).
457    pub on: bool,
458}
459
460/// `WXXXXXXX N+ N- VNAM MODEL <ON|OFF>`: current-controlled switch
461/// keyed by the branch current of voltage source `ctrl`, with its
462/// model inline (`dev/csw/`).
463#[derive(Clone, Debug)]
464pub struct SwitchI {
465    pub name: String,
466    pub nodes: [String; 2],
467    pub ctrl: String,
468    pub model: crate::switch::CswModel,
469    /// `ON` (true) or `OFF`/nothing (`CSWzero_stateGiven`).
470    pub on: bool,
471}
472
473#[derive(Clone, Debug)]
474pub enum Spec {
475    Bjt(Box<Bjt>),
476    Mos1(Box<Mos1>),
477    Mos2(Box<Mos2>),
478    Mos3(Box<Mos3>),
479    Mos6(Box<Mos6>),
480    Bsim1(Box<Bsim1>),
481    Bsim2(Box<Bsim2>),
482    Bsim3(Box<Bsim3>),
483    Resistor(Resistor),
484    Capacitor(Capacitor),
485    Inductor(Inductor),
486    Mutual(Mutual),
487    Vsource(Vsource),
488    Isource(Isource),
489    Vcvs(Vcvs),
490    Vccs(Vccs),
491    Cccs(Cccs),
492    Ccvs(Ccvs),
493    Diode(Diode),
494    Jfet(Box<Jfet>),
495    Mes(Box<Mes>),
496    Ltra(Box<Ltra>),
497    Tra(Box<Tra>),
498    SwitchV(SwitchV),
499    SwitchI(SwitchI),
500    Asrc(Box<Asrc>),
501}
502
503macro_rules! impl_into_spec {
504    ($($t:ident),*) => {$(
505        impl From<$t> for Spec {
506            fn from(s: $t) -> Spec { Spec::$t(s) }
507        }
508    )*};
509}
510impl_into_spec!(
511    Resistor, Capacitor, Inductor, Mutual, Vsource, Isource, Vcvs, Vccs, Cccs, Ccvs, Diode,
512    SwitchV, SwitchI
513);
514
515impl From<Bjt> for Spec {
516    fn from(s: Bjt) -> Spec {
517        Spec::Bjt(Box::new(s))
518    }
519}
520
521impl From<Mos1> for Spec {
522    fn from(s: Mos1) -> Spec {
523        Spec::Mos1(Box::new(s))
524    }
525}
526
527impl From<Mos2> for Spec {
528    fn from(s: Mos2) -> Spec {
529        Spec::Mos2(Box::new(s))
530    }
531}
532
533impl From<Mos3> for Spec {
534    fn from(s: Mos3) -> Spec {
535        Spec::Mos3(Box::new(s))
536    }
537}
538
539impl From<Mos6> for Spec {
540    fn from(s: Mos6) -> Spec {
541        Spec::Mos6(Box::new(s))
542    }
543}
544
545impl From<Bsim1> for Spec {
546    fn from(s: Bsim1) -> Spec {
547        Spec::Bsim1(Box::new(s))
548    }
549}
550
551impl From<Bsim2> for Spec {
552    fn from(s: Bsim2) -> Spec {
553        Spec::Bsim2(Box::new(s))
554    }
555}
556
557impl From<Bsim3> for Spec {
558    fn from(s: Bsim3) -> Spec {
559        Spec::Bsim3(Box::new(s))
560    }
561}
562
563impl From<Jfet> for Spec {
564    fn from(s: Jfet) -> Spec {
565        Spec::Jfet(Box::new(s))
566    }
567}
568
569impl From<Mes> for Spec {
570    fn from(s: Mes) -> Spec {
571        Spec::Mes(Box::new(s))
572    }
573}
574
575impl From<Ltra> for Spec {
576    fn from(s: Ltra) -> Spec {
577        Spec::Ltra(Box::new(s))
578    }
579}
580
581impl From<Asrc> for Spec {
582    fn from(s: Asrc) -> Spec {
583        Spec::Asrc(Box::new(s))
584    }
585}
586
587impl From<Tra> for Spec {
588    fn from(s: Tra) -> Spec {
589        Spec::Tra(Box::new(s))
590    }
591}
592
593impl Spec {
594    pub(crate) fn name(&self) -> &str {
595        match self {
596            Spec::Bjt(s) => &s.name,
597            Spec::Mos1(s) => &s.name,
598            Spec::Mos2(s) => &s.name,
599            Spec::Mos3(s) => &s.name,
600            Spec::Mos6(s) => &s.name,
601            Spec::Bsim1(s) => &s.name,
602            Spec::Bsim2(s) => &s.name,
603            Spec::Bsim3(s) => &s.name,
604            Spec::Resistor(s) => &s.name,
605            Spec::Capacitor(s) => &s.name,
606            Spec::Inductor(s) => &s.name,
607            Spec::Mutual(s) => &s.name,
608            Spec::Vsource(s) => &s.name,
609            Spec::Isource(s) => &s.name,
610            Spec::Vcvs(s) => &s.name,
611            Spec::Vccs(s) => &s.name,
612            Spec::Cccs(s) => &s.name,
613            Spec::Ccvs(s) => &s.name,
614            Spec::Diode(s) => &s.name,
615            Spec::Jfet(s) => &s.name,
616            Spec::Mes(s) => &s.name,
617            Spec::Ltra(s) => &s.name,
618            Spec::Tra(s) => &s.name,
619            Spec::SwitchV(s) => &s.name,
620            Spec::SwitchI(s) => &s.name,
621            Spec::Asrc(s) => &s.name,
622        }
623    }
624}
625
626/// A circuit under construction. Analyses live in `analysis/`
627/// (Phase 1: [`Circuit::op`]).
628///
629/// Cloning copies the element specs, `.nodeset`s and `.ic`s; the
630/// clone never shares a [`Session`](crate::Session)'s warm-start
631/// state.
632#[derive(Clone, Default)]
633pub struct Circuit {
634    pub title: String,
635    pub(crate) specs: Vec<Spec>,
636    /// `.nodeset` entries (node, volts).
637    pub(crate) nodesets: Vec<(String, f64)>,
638    /// `.ic` entries (node, volts), applied at the transient
639    /// operating point.
640    pub(crate) ics: Vec<(String, f64)>,
641    /// Instance name → which `IC=` components its card gave (the
642    /// C's `*icXGiven` bits), recorded by the netlist front end for
643    /// the semiconductors' `DEVsetic`. A device without an entry
644    /// (built from a typed spec) has every component given.
645    pub(crate) ic_given: HashMap<String, crate::devices::IcGiven>,
646    /// Instance name → the temperature dependence of a resistor
647    /// card that named an `R` model or a `TEMP=` (`restemp.c`),
648    /// recorded by the netlist front end. A resistor without an
649    /// entry (typed spec, plain card) has `TC1 = TC2 = 0`: its
650    /// conductance is `1/R` at every temperature, as in the 3f5.
651    pub(crate) res_temp: HashMap<String, ResTemp>,
652    /// Instance name → the ngspice-47 `DTEMP=` (kelvin difference) of
653    /// a diode, BJT, JFET or MOS1/2/3/6 card, recorded by the netlist
654    /// front end: the instance temperature is `TEMP=` when given,
655    /// else `CKTtemp + DTEMP` (`*temp.c` of each device). The 3f5 has
656    /// no `DTEMP`; a device without an entry has `DTEMP = 0`.
657    pub(crate) dtemp: HashMap<String, f64>,
658    /// The warm-start channel of a [`Session`](crate::Session):
659    /// empty for a plain circuit (the engine then runs exactly the
660    /// 3f5 path).
661    pub(crate) warm: crate::session::WarmSlot,
662}
663
664impl Circuit {
665    pub fn new(title: impl Into<String>) -> Self {
666        Circuit {
667            title: title.into(),
668            ..Circuit::default()
669        }
670    }
671
672    /// Add one device. Names and nodes are case-insensitive (the 3f5
673    /// lowercases the whole deck).
674    pub fn add(&mut self, spec: impl Into<Spec>) -> Result<(), Error> {
675        let mut spec = spec.into();
676        normalize(&mut spec);
677        let name = spec.name();
678        if name.is_empty() {
679            return Err(Error::Build("device name cannot be empty".into()));
680        }
681        if self.specs.iter().any(|s| s.name() == name) {
682            return Err(Error::Build(format!("device `{name}` already defined")));
683        }
684        if let Spec::Resistor(r) = &spec
685            && (r.value == 0.0 || !r.value.is_finite())
686        {
687            // spice.txt §3.1.1: "may be positive or negative but
688            // not zero".
689            return Err(Error::Build(format!(
690                "resistor `{}` cannot have zero resistance",
691                r.name
692            )));
693        }
694        self.specs.push(spec);
695        Ok(())
696    }
697
698    /// `.nodeset v(node) = value`.
699    pub fn nodeset(&mut self, node: impl Into<String>, value: f64) {
700        self.nodesets
701            .push((node.into().to_ascii_lowercase(), value));
702    }
703
704    /// `.ic v(node) = value` (enforced at the transient operating
705    /// point; `.tran … UIC` ignores it in favor of device ICs).
706    pub fn ic(&mut self, node: impl Into<String>, value: f64) {
707        self.ics.push((node.into().to_ascii_lowercase(), value));
708    }
709
710    /// Record the `restemp.c` temperature data of resistor `name`
711    /// (the netlist front end; see [`ResTemp`]).
712    pub(crate) fn set_res_temp(&mut self, name: &str, t: ResTemp) {
713        self.res_temp.insert(name.to_ascii_lowercase(), t);
714    }
715
716    /// Record the ngspice `DTEMP=` of instance `name` (the netlist
717    /// front end; see [`Circuit::dtemp`]).
718    pub(crate) fn set_dtemp(&mut self, name: &str, dtemp: f64) {
719        self.dtemp.insert(name.to_ascii_lowercase(), dtemp);
720    }
721
722    /// The temperature of a semiconductor instance (kelvin): its
723    /// `TEMP=` when given, else the circuit's plus its `DTEMP=`
724    /// (`DIOtemp`/`BJTtemp`/`JFETtemp`/`MOS*temp` of ngspice-47).
725    fn instance_temp(&self, name: &str, temp: Option<f64>, opts: &Options) -> f64 {
726        temp.unwrap_or_else(|| opts.temp + self.dtemp.get(name).copied().unwrap_or(0.0))
727    }
728
729    /// Record which `IC=` components instance `name`'s card gave
730    /// (the netlist front end; see [`Circuit::ic_given`]).
731    pub(crate) fn set_ic_given(&mut self, name: &str, given: crate::devices::IcGiven) {
732        self.ic_given.insert(name.to_ascii_lowercase(), given);
733    }
734}
735
736fn normalize(spec: &mut Spec) {
737    #[allow(clippy::ptr_arg)] // uniform `&mut String` for iter_mut().for_each
738    fn lc(s: &mut String) {
739        s.make_ascii_lowercase();
740    }
741    match spec {
742        Spec::Resistor(s) => {
743            lc(&mut s.name);
744            s.nodes.iter_mut().for_each(lc);
745        }
746        Spec::Capacitor(s) => {
747            lc(&mut s.name);
748            s.nodes.iter_mut().for_each(lc);
749        }
750        Spec::Inductor(s) => {
751            lc(&mut s.name);
752            s.nodes.iter_mut().for_each(lc);
753        }
754        Spec::Mutual(s) => {
755            lc(&mut s.name);
756            lc(&mut s.l1);
757            lc(&mut s.l2);
758        }
759        Spec::Vsource(s) => {
760            lc(&mut s.name);
761            s.nodes.iter_mut().for_each(lc);
762        }
763        Spec::Isource(s) => {
764            lc(&mut s.name);
765            s.nodes.iter_mut().for_each(lc);
766        }
767        Spec::Vcvs(s) => {
768            lc(&mut s.name);
769            s.nodes.iter_mut().for_each(lc);
770        }
771        Spec::Vccs(s) => {
772            lc(&mut s.name);
773            s.nodes.iter_mut().for_each(lc);
774        }
775        Spec::Cccs(s) => {
776            lc(&mut s.name);
777            s.nodes.iter_mut().for_each(lc);
778            lc(&mut s.ctrl);
779        }
780        Spec::Ccvs(s) => {
781            lc(&mut s.name);
782            s.nodes.iter_mut().for_each(lc);
783            lc(&mut s.ctrl);
784        }
785        Spec::Diode(s) => {
786            lc(&mut s.name);
787            s.nodes.iter_mut().for_each(lc);
788        }
789        Spec::Bjt(s) => {
790            lc(&mut s.name);
791            s.nodes.iter_mut().for_each(lc);
792        }
793        Spec::Mos1(s) => {
794            lc(&mut s.name);
795            s.nodes.iter_mut().for_each(lc);
796        }
797        Spec::Mos2(s) => {
798            lc(&mut s.name);
799            s.nodes.iter_mut().for_each(lc);
800        }
801        Spec::Mos3(s) => {
802            lc(&mut s.name);
803            s.nodes.iter_mut().for_each(lc);
804        }
805        Spec::Mos6(s) => {
806            lc(&mut s.name);
807            s.nodes.iter_mut().for_each(lc);
808        }
809        Spec::Bsim1(s) => {
810            lc(&mut s.name);
811            s.nodes.iter_mut().for_each(lc);
812        }
813        Spec::Bsim2(s) => {
814            lc(&mut s.name);
815            s.nodes.iter_mut().for_each(lc);
816        }
817        Spec::Bsim3(s) => {
818            lc(&mut s.name);
819            s.nodes.iter_mut().for_each(lc);
820        }
821        Spec::Jfet(s) => {
822            lc(&mut s.name);
823            s.nodes.iter_mut().for_each(lc);
824        }
825        Spec::Mes(s) => {
826            lc(&mut s.name);
827            s.nodes.iter_mut().for_each(lc);
828        }
829        Spec::Ltra(s) => {
830            lc(&mut s.name);
831            s.nodes.iter_mut().for_each(lc);
832        }
833        Spec::Tra(s) => {
834            lc(&mut s.name);
835            s.nodes.iter_mut().for_each(lc);
836        }
837        Spec::SwitchV(s) => {
838            lc(&mut s.name);
839            s.nodes.iter_mut().for_each(lc);
840        }
841        Spec::SwitchI(s) => {
842            lc(&mut s.name);
843            s.nodes.iter_mut().for_each(lc);
844            lc(&mut s.ctrl);
845        }
846        Spec::Asrc(s) => {
847            lc(&mut s.name);
848            s.nodes.iter_mut().for_each(lc);
849            s.expr.visit_refs_mut(&mut |n| lc(n), &mut |d| lc(d));
850        }
851    }
852}
853
854/// The unknowns of the MNA system: index 0 is ground, then node
855/// voltages and branch currents in order of creation (the C's
856/// `CKTnode` list).
857pub(crate) struct Vars {
858    pub names: Vec<String>,
859    pub kinds: Vec<VarKind>,
860}
861
862/// Whether a node unknown is written to the output plots. ngspice's
863/// default save list (`outitf.c`, `beginPlot`) drops the device-
864/// internal nodes by suffix — `#internal`, `#source`, `#drain`,
865/// `#collector`, `#emitter`, `#base` — and keeps every other node,
866/// including the LTRA port-current equations `o1#i1`/`o1#i2`.
867pub(crate) fn saved_node(name: &str) -> bool {
868    const HIDDEN: [&str; 6] = [
869        "#internal",
870        "#source",
871        "#drain",
872        "#collector",
873        "#emitter",
874        "#base",
875    ];
876    !HIDDEN.iter().any(|h| name.contains(h))
877}
878
879#[derive(Clone, Copy, Debug, PartialEq, Eq)]
880pub(crate) enum VarKind {
881    Ground,
882    Node,
883    Branch,
884}
885
886impl Vars {
887    fn new() -> Self {
888        Vars {
889            names: vec!["0".into()],
890            kinds: vec![VarKind::Ground],
891        }
892    }
893
894    pub fn len(&self) -> usize {
895        self.names.len()
896    }
897
898    fn push(&mut self, name: String, kind: VarKind) -> Var {
899        self.names.push(name);
900        self.kinds.push(kind);
901        self.names.len() - 1
902    }
903}
904
905/// Fully resolved circuit: instances bound to variable indices.
906/// One RF port as the `.sp` analysis sees it (`VSRCinstance` with
907/// `VSRCisPort`): external terminals, branch equation and
908/// reference impedance.
909#[derive(Clone, Debug)]
910pub(crate) struct PortInfo {
911    pub name: String,
912    pub num: u32,
913    pub z0: f64,
914    pub pos: Var,
915    pub neg: Var,
916    pub br: Var,
917}
918
919pub(crate) struct Built {
920    pub vars: Vars,
921    pub devices: Vec<Box<dyn crate::devices::Device>>,
922    /// Element name per device index (noise generators are named
923    /// `onoise_<name>_<suffix>`).
924    pub names: Vec<String>,
925    pub nodesets: Vec<(Var, f64)>,
926    pub ics: Vec<(Var, f64)>,
927    /// Independent V/I source element name → index in `devices`
928    /// (what a `.dc` sweep drives) and whether it is a V source.
929    pub sources: HashMap<String, (usize, bool)>,
930    /// RF ports (`CKTrfPorts`), sorted by port number `1..=N`.
931    pub ports: Vec<PortInfo>,
932    /// Total device state slots (`CKTnumStates`).
933    pub num_states: usize,
934    /// Per device (same order as `devices`), the window of state
935    /// offsets it allocated: `[start, end)`, nondecreasing in device
936    /// order. The parallel load hands each worker the union of its
937    /// devices' windows.
938    #[cfg_attr(not(feature = "parallel"), allow(dead_code))]
939    pub state_ranges: Vec<(usize, usize)>,
940    /// Diode temperature-adjust notes etc., surfaced by the caller.
941    pub notes: Vec<String>,
942    /// The circuit's warm-start channel (see [`Circuit::warm`]).
943    pub warm: crate::session::WarmSlot,
944}
945
946impl Circuit {
947    /// Resolve names and build device instances (the 3f5's setup
948    /// pass).
949    pub(crate) fn build(&self, opts: &Options) -> Result<Built, Error> {
950        use crate::devices::*;
951
952        let mut vars = Vars::new();
953        let mut node_ids: HashMap<String, Var> = HashMap::new();
954        node_ids.insert("0".into(), 0);
955        let node = |vars: &mut Vars, node_ids: &mut HashMap<String, Var>, name: &str| -> Var {
956            *node_ids
957                .entry(name.to_string())
958                .or_insert_with(|| vars.push(name.to_string(), VarKind::Node))
959        };
960
961        // Pass 1: node variables in order of first appearance,
962        // branch variables per device, the name→branch map (F/H
963        // control references), state-slot bases (`CKTnumStates`) and
964        // the inductor info the mutuals need.
965        let mut branches: HashMap<String, Var> = HashMap::new();
966        let mut st = crate::state::StateAlloc::new();
967        let mut state_base: Vec<usize> = Vec::with_capacity(self.specs.len());
968        let mut state_ranges: Vec<(usize, usize)> = Vec::with_capacity(self.specs.len());
969        let mut inductors: HashMap<String, (usize, f64)> = HashMap::new(); // name → (flux, L)
970        for spec in &self.specs {
971            let first = st.total();
972            state_base.push(match spec {
973                Spec::Bjt(_) => st.alloc(crate::bjt::BJT_NUM_STATES),
974                Spec::Mos1(_) => st.alloc(crate::mos1::MOS1_NUM_STATES),
975                Spec::Mos2(_) => st.alloc(crate::mos2::MOS2_NUM_STATES),
976                Spec::Mos3(_) => st.alloc(crate::mos3::MOS3_NUM_STATES),
977                Spec::Mos6(_) => st.alloc(crate::mos6::MOS6_NUM_STATES),
978                Spec::Bsim1(_) => st.alloc(crate::bsim1::BSIM1_NUM_STATES),
979                Spec::Bsim2(_) => st.alloc(crate::bsim2::BSIM2_NUM_STATES),
980                Spec::Bsim3(_) => st.alloc(crate::bsim3::BSIM3_NUM_STATES),
981                Spec::Capacitor(_) => st.alloc(2),
982                Spec::Inductor(s) => {
983                    let flux = st.alloc(2);
984                    inductors.insert(s.name.clone(), (flux, s.value));
985                    flux
986                }
987                Spec::Diode(_) => st.alloc(5),
988                Spec::Jfet(_) => st.alloc(crate::jfet::FET3_NUM_STATES),
989                Spec::Mes(_) => st.alloc(crate::mes::MES_NUM_STATES),
990                Spec::SwitchV(_) | Spec::SwitchI(_) => st.alloc(crate::switch::SW_NUM_STATES),
991                Spec::Asrc(s) => st.alloc(crate::asrc::num_states(&s.expr)),
992                _ => 0,
993            });
994            state_ranges.push((first, st.total()));
995            let node_names: &[String] = match spec {
996                Spec::Bjt(s) => &s.nodes,
997                Spec::Mos1(s) => &s.nodes,
998                Spec::Mos2(s) => &s.nodes,
999                Spec::Mos3(s) => &s.nodes,
1000                Spec::Mos6(s) => &s.nodes,
1001                Spec::Bsim1(s) => &s.nodes,
1002                Spec::Bsim2(s) => &s.nodes,
1003                Spec::Bsim3(s) => &s.nodes,
1004                Spec::Resistor(s) => &s.nodes,
1005                Spec::Capacitor(s) => &s.nodes,
1006                Spec::Inductor(s) => &s.nodes,
1007                Spec::Mutual(_) => &[],
1008                Spec::Vsource(s) => &s.nodes,
1009                Spec::Isource(s) => &s.nodes,
1010                Spec::Vcvs(s) => &s.nodes,
1011                Spec::Vccs(s) => &s.nodes,
1012                Spec::Cccs(s) => &s.nodes,
1013                Spec::Ccvs(s) => &s.nodes,
1014                Spec::Diode(s) => &s.nodes,
1015                Spec::Jfet(s) => &s.nodes,
1016                Spec::Mes(s) => &s.nodes,
1017                Spec::Ltra(s) => &s.nodes,
1018                Spec::Tra(s) => &s.nodes,
1019                Spec::SwitchV(s) => &s.nodes,
1020                Spec::SwitchI(s) => &s.nodes,
1021                Spec::Asrc(s) => &s.nodes,
1022            };
1023            for n in node_names {
1024                node(&mut vars, &mut node_ids, n);
1025            }
1026            // `INPgetTree` inserts the `v()` nodes while parsing the
1027            // card (after N+ N-); `ASRCsetup` then makes the branch of
1028            // a voltage source (`b1#branch`).
1029            if let Spec::Asrc(s) = spec {
1030                let mut refs = Vec::new();
1031                let mut e = s.expr.clone();
1032                e.visit_refs_mut(&mut |n| refs.push(n.clone()), &mut |_| {});
1033                for n in &refs {
1034                    node(&mut vars, &mut node_ids, n);
1035                }
1036                if s.kind == dune_netlist::ir::SourceKind::Voltage {
1037                    let br = vars.push(s.name.clone(), VarKind::Branch);
1038                    branches.insert(s.name.clone(), br);
1039                }
1040            }
1041            match spec {
1042                Spec::Vsource(s) => {
1043                    let br = vars.push(s.name.clone(), VarKind::Branch);
1044                    branches.insert(s.name.clone(), br);
1045                    // `VSRCsetup` (RFSPICE): a port's internal node
1046                    // behind the reference impedance, made after the
1047                    // branch with `CKTmkVolt` (`v1#res`).
1048                    if s.port.is_some_and(|p| p.is_port()) {
1049                        node(&mut vars, &mut node_ids, &format!("{}#res", s.name));
1050                    }
1051                }
1052                Spec::Inductor(s) => {
1053                    let br = vars.push(s.name.clone(), VarKind::Branch);
1054                    branches.insert(s.name.clone(), br);
1055                }
1056                Spec::Vcvs(s) => {
1057                    let br = vars.push(s.name.clone(), VarKind::Branch);
1058                    branches.insert(s.name.clone(), br);
1059                }
1060                Spec::Ccvs(s) => {
1061                    let br = vars.push(s.name.clone(), VarKind::Branch);
1062                    branches.insert(s.name.clone(), br);
1063                }
1064                // `LTRAsetup`: the two port-current equations, made
1065                // with `CKTmkVolt` — voltage-typed unknowns for the
1066                // convergence test and the rawfile (`v(o1#i1)`).
1067                Spec::Ltra(s) => {
1068                    node(&mut vars, &mut node_ids, &format!("{}#i1", s.name));
1069                    node(&mut vars, &mut node_ids, &format!("{}#i2", s.name));
1070                }
1071                // `TRAsetup`: both branch equations, then both
1072                // internal nodes, all with `CKTmkVolt`.
1073                Spec::Tra(s) => {
1074                    for suffix in ["i1", "i2", "int1", "int2"] {
1075                        node(&mut vars, &mut node_ids, &format!("{}#{suffix}", s.name));
1076                    }
1077                }
1078                // `DIOsetup`: internal node past RS, named
1079                // `<name>#internal` like `CKTmkVolt` does.
1080                Spec::Diode(s) if s.model.rs != 0.0 => {
1081                    node(&mut vars, &mut node_ids, &format!("{}#internal", s.name));
1082                }
1083                // `JFETsetup`/`MESsetup`: source prime first, then
1084                // drain prime (`<name>#source`, `<name>#drain`).
1085                Spec::Jfet(s) => {
1086                    if s.model.rs != 0.0 {
1087                        node(&mut vars, &mut node_ids, &format!("{}#source", s.name));
1088                    }
1089                    if s.model.rd != 0.0 {
1090                        node(&mut vars, &mut node_ids, &format!("{}#drain", s.name));
1091                    }
1092                }
1093                Spec::Mes(s) => {
1094                    if s.model.rs != 0.0 {
1095                        node(&mut vars, &mut node_ids, &format!("{}#source", s.name));
1096                    }
1097                    if s.model.rd != 0.0 {
1098                        node(&mut vars, &mut node_ids, &format!("{}#drain", s.name));
1099                    }
1100                }
1101                // `BJTsetup`: one prime node per nonzero parasitic
1102                // resistance (`q1#collector` etc., `CKTmkVolt`).
1103                Spec::Bjt(s) => {
1104                    if s.model.rc != 0.0 {
1105                        node(&mut vars, &mut node_ids, &format!("{}#collector", s.name));
1106                    }
1107                    if s.model.rb != 0.0 {
1108                        node(&mut vars, &mut node_ids, &format!("{}#base", s.name));
1109                    }
1110                    if s.model.re != 0.0 {
1111                        node(&mut vars, &mut node_ids, &format!("{}#emitter", s.name));
1112                    }
1113                }
1114                // `MOS1setup`: prime nodes past the series
1115                // resistance (`m1#drain`/`m1#source`, `CKTmkVolt`).
1116                Spec::Mos1(s) => {
1117                    if s.model.needs_prime(s.nrd) {
1118                        node(&mut vars, &mut node_ids, &format!("{}#drain", s.name));
1119                    }
1120                    if s.model.needs_prime(s.nrs) {
1121                        node(&mut vars, &mut node_ids, &format!("{}#source", s.name));
1122                    }
1123                }
1124                Spec::Mos2(s) => {
1125                    if s.model.needs_prime(s.nrd) {
1126                        node(
1127                            &mut vars,
1128                            &mut node_ids,
1129                            &format!("{}#internal#drain", s.name),
1130                        );
1131                    }
1132                    if s.model.needs_prime(s.nrs) {
1133                        node(
1134                            &mut vars,
1135                            &mut node_ids,
1136                            &format!("{}#internal#source", s.name),
1137                        );
1138                    }
1139                }
1140                Spec::Mos3(s) => {
1141                    if s.model.needs_prime(s.nrd) {
1142                        node(
1143                            &mut vars,
1144                            &mut node_ids,
1145                            &format!("{}#internal#drain", s.name),
1146                        );
1147                    }
1148                    if s.model.needs_prime(s.nrs) {
1149                        node(
1150                            &mut vars,
1151                            &mut node_ids,
1152                            &format!("{}#internal#source", s.name),
1153                        );
1154                    }
1155                }
1156                // `MOS6setup` names them like MOS1's (`m1#drain`).
1157                Spec::Mos6(s) => {
1158                    if s.model.needs_prime(s.nrd) {
1159                        node(&mut vars, &mut node_ids, &format!("{}#drain", s.name));
1160                    }
1161                    if s.model.needs_prime(s.nrs) {
1162                        node(&mut vars, &mut node_ids, &format!("{}#source", s.name));
1163                    }
1164                }
1165                // `B1setup`/`B2setup` name them like MOS1's too.
1166                Spec::Bsim1(s) => {
1167                    if s.model.needs_prime(s.nrd) {
1168                        node(&mut vars, &mut node_ids, &format!("{}#drain", s.name));
1169                    }
1170                    if s.model.needs_prime(s.nrs) {
1171                        node(&mut vars, &mut node_ids, &format!("{}#source", s.name));
1172                    }
1173                }
1174                Spec::Bsim2(s) => {
1175                    if s.model.needs_prime(s.nrd) {
1176                        node(&mut vars, &mut node_ids, &format!("{}#drain", s.name));
1177                    }
1178                    if s.model.needs_prime(s.nrs) {
1179                        node(&mut vars, &mut node_ids, &format!("{}#source", s.name));
1180                    }
1181                }
1182                // `BSIM3setup`: `CKTmkVolt(…, "drain")` / `"source"`.
1183                Spec::Bsim3(s) => {
1184                    if s.model.needs_prime(s.nrd) {
1185                        node(&mut vars, &mut node_ids, &format!("{}#drain", s.name));
1186                    }
1187                    if s.model.needs_prime(s.nrs) {
1188                        node(&mut vars, &mut node_ids, &format!("{}#source", s.name));
1189                    }
1190                }
1191                _ => {}
1192            }
1193        }
1194        let num_states = st.total();
1195
1196        // Pass 2: instances.
1197        let mut devices: Vec<Box<dyn Device>> = Vec::new();
1198        let mut names: Vec<String> = Vec::new();
1199        let mut sources: HashMap<String, (usize, bool)> = HashMap::new();
1200        let mut ports: Vec<PortInfo> = Vec::new();
1201        let mut notes = Vec::new();
1202        let get = |node_ids: &HashMap<String, Var>, n: &str| node_ids[n];
1203        // `BSIM3temp`'s per-model knot list: models (as given) with
1204        // their set-up copy, temperature part and the size-dependent
1205        // parameter sets already built, keyed by drawn (L, W).
1206        let mut bsim3_models: Vec<crate::bsim3::Bsim3Knots> = Vec::new();
1207        for (spec, &base) in self.specs.iter().zip(&state_base) {
1208            names.push(spec.name().to_string());
1209            match spec {
1210                Spec::Bjt(s) => {
1211                    let col = get(&node_ids, &s.nodes[0]);
1212                    let bse = get(&node_ids, &s.nodes[1]);
1213                    let emi = get(&node_ids, &s.nodes[2]);
1214                    let sub = get(&node_ids, &s.nodes[3]);
1215                    let prime = |rz: f64, suffix: &str, outer: Var| {
1216                        if rz != 0.0 {
1217                            node_ids[&format!("{}#{suffix}", s.name)]
1218                        } else {
1219                            outer
1220                        }
1221                    };
1222                    let primes = [
1223                        prime(s.model.rc, "collector", col),
1224                        prime(s.model.rb, "base", bse),
1225                        prime(s.model.re, "emitter", emi),
1226                    ];
1227                    let temp = self.instance_temp(&s.name, s.temp, opts);
1228                    let bt = crate::bjt::BjtTemp::new(&s.name, &s.model, temp, opts.tnom);
1229                    notes.extend(bt.notes.iter().cloned());
1230                    devices.push(Box::new(crate::bjt::BjtI::new(
1231                        [col, bse, emi, sub],
1232                        primes,
1233                        bt,
1234                        s.area,
1235                        s.off,
1236                        s.ic,
1237                        base,
1238                    )));
1239                }
1240                Spec::Mos1(s) => {
1241                    let d = get(&node_ids, &s.nodes[0]);
1242                    let gt = get(&node_ids, &s.nodes[1]);
1243                    let so = get(&node_ids, &s.nodes[2]);
1244                    let b = get(&node_ids, &s.nodes[3]);
1245                    let dp = if s.model.needs_prime(s.nrd) {
1246                        node_ids[&format!("{}#drain", s.name)]
1247                    } else {
1248                        d
1249                    };
1250                    let sp = if s.model.needs_prime(s.nrs) {
1251                        node_ids[&format!("{}#source", s.name)]
1252                    } else {
1253                        so
1254                    };
1255                    let geom = crate::mos1::MosGeom {
1256                        l: s.l.unwrap_or(opts.defl),
1257                        w: s.w.unwrap_or(opts.defw),
1258                        ad: s.ad.unwrap_or(opts.defad),
1259                        as_: s.as_.unwrap_or(opts.defas),
1260                        pd: s.pd,
1261                        ps: s.ps,
1262                        nrd: s.nrd,
1263                        nrs: s.nrs,
1264                    };
1265                    let temp = self.instance_temp(&s.name, s.temp, opts);
1266                    let mt = crate::mos1::Mos1Temp::new(&s.name, &s.model, &geom, temp, opts.tnom)
1267                        .map_err(Error::Build)?;
1268                    notes.extend(mt.notes.iter().cloned());
1269                    devices.push(Box::new(crate::mos1::Mos1I::new(
1270                        [d, gt, so, b],
1271                        [dp, sp],
1272                        mt,
1273                        geom,
1274                        s.off,
1275                        s.ic,
1276                        base,
1277                    )));
1278                }
1279                Spec::Mos2(s) => {
1280                    let d = get(&node_ids, &s.nodes[0]);
1281                    let gt = get(&node_ids, &s.nodes[1]);
1282                    let so = get(&node_ids, &s.nodes[2]);
1283                    let b = get(&node_ids, &s.nodes[3]);
1284                    let dp = if s.model.needs_prime(s.nrd) {
1285                        node_ids[&format!("{}#internal#drain", s.name)]
1286                    } else {
1287                        d
1288                    };
1289                    let sp = if s.model.needs_prime(s.nrs) {
1290                        node_ids[&format!("{}#internal#source", s.name)]
1291                    } else {
1292                        so
1293                    };
1294                    let geom = crate::mos1::MosGeom {
1295                        l: s.l.unwrap_or(opts.defl),
1296                        w: s.w.unwrap_or(opts.defw),
1297                        ad: s.ad.unwrap_or(opts.defad),
1298                        as_: s.as_.unwrap_or(opts.defas),
1299                        pd: s.pd,
1300                        ps: s.ps,
1301                        nrd: s.nrd,
1302                        nrs: s.nrs,
1303                    };
1304                    let temp = self.instance_temp(&s.name, s.temp, opts);
1305                    let mt = crate::mos2::Mos2Temp::new(&s.name, &s.model, &geom, temp, opts.tnom)
1306                        .map_err(Error::Build)?;
1307                    notes.extend(mt.notes.iter().cloned());
1308                    devices.push(Box::new(crate::mos2::Mos2I::new(
1309                        [d, gt, so, b],
1310                        [dp, sp],
1311                        mt,
1312                        geom,
1313                        s.off,
1314                        s.ic,
1315                        base,
1316                    )));
1317                }
1318                Spec::Mos3(s) => {
1319                    let d = get(&node_ids, &s.nodes[0]);
1320                    let gt = get(&node_ids, &s.nodes[1]);
1321                    let so = get(&node_ids, &s.nodes[2]);
1322                    let b = get(&node_ids, &s.nodes[3]);
1323                    let dp = if s.model.needs_prime(s.nrd) {
1324                        node_ids[&format!("{}#internal#drain", s.name)]
1325                    } else {
1326                        d
1327                    };
1328                    let sp = if s.model.needs_prime(s.nrs) {
1329                        node_ids[&format!("{}#internal#source", s.name)]
1330                    } else {
1331                        so
1332                    };
1333                    let geom = crate::mos1::MosGeom {
1334                        l: s.l.unwrap_or(opts.defl),
1335                        w: s.w.unwrap_or(opts.defw),
1336                        ad: s.ad.unwrap_or(opts.defad),
1337                        as_: s.as_.unwrap_or(opts.defas),
1338                        pd: s.pd,
1339                        ps: s.ps,
1340                        nrd: s.nrd,
1341                        nrs: s.nrs,
1342                    };
1343                    let temp = self.instance_temp(&s.name, s.temp, opts);
1344                    let mt = crate::mos3::Mos3Temp::new(&s.name, &s.model, &geom, temp, opts.tnom)
1345                        .map_err(Error::Build)?;
1346                    devices.push(Box::new(crate::mos3::Mos3I::new(
1347                        [d, gt, so, b],
1348                        [dp, sp],
1349                        mt,
1350                        geom,
1351                        s.off,
1352                        s.ic,
1353                        base,
1354                    )));
1355                }
1356                Spec::Mos6(s) => {
1357                    let d = get(&node_ids, &s.nodes[0]);
1358                    let gt = get(&node_ids, &s.nodes[1]);
1359                    let so = get(&node_ids, &s.nodes[2]);
1360                    let b = get(&node_ids, &s.nodes[3]);
1361                    let dp = if s.model.needs_prime(s.nrd) {
1362                        node_ids[&format!("{}#drain", s.name)]
1363                    } else {
1364                        d
1365                    };
1366                    let sp = if s.model.needs_prime(s.nrs) {
1367                        node_ids[&format!("{}#source", s.name)]
1368                    } else {
1369                        so
1370                    };
1371                    // `mos6temp.c` forces zero squares back to 1.
1372                    let geom = crate::mos1::MosGeom {
1373                        l: s.l.unwrap_or(opts.defl),
1374                        w: s.w.unwrap_or(opts.defw),
1375                        ad: s.ad.unwrap_or(opts.defad),
1376                        as_: s.as_.unwrap_or(opts.defas),
1377                        pd: s.pd,
1378                        ps: s.ps,
1379                        nrd: if s.nrd == 0.0 { 1.0 } else { s.nrd },
1380                        nrs: if s.nrs == 0.0 { 1.0 } else { s.nrs },
1381                    };
1382                    let temp = self.instance_temp(&s.name, s.temp, opts);
1383                    let mt = crate::mos6::Mos6Temp::new(&s.name, &s.model, &geom, temp, opts.tnom)
1384                        .map_err(Error::Build)?;
1385                    notes.extend(mt.notes.iter().cloned());
1386                    devices.push(Box::new(crate::mos6::Mos6I::new(
1387                        [d, gt, so, b],
1388                        [dp, sp],
1389                        mt,
1390                        geom,
1391                        s.off,
1392                        s.ic,
1393                        base,
1394                    )));
1395                }
1396                Spec::Bsim1(s) => {
1397                    let d = get(&node_ids, &s.nodes[0]);
1398                    let gt = get(&node_ids, &s.nodes[1]);
1399                    let so = get(&node_ids, &s.nodes[2]);
1400                    let b = get(&node_ids, &s.nodes[3]);
1401                    let dp = if s.model.needs_prime(s.nrd) {
1402                        node_ids[&format!("{}#drain", s.name)]
1403                    } else {
1404                        d
1405                    };
1406                    let sp = if s.model.needs_prime(s.nrs) {
1407                        node_ids[&format!("{}#source", s.name)]
1408                    } else {
1409                        so
1410                    };
1411                    // `b1set.c`: L and W default to 5 µm, not the
1412                    // `.options` MOS defaults.
1413                    let geom = crate::mos1::MosGeom {
1414                        l: s.l.unwrap_or(5e-6),
1415                        w: s.w.unwrap_or(5e-6),
1416                        ad: s.ad.unwrap_or(0.0),
1417                        as_: s.as_.unwrap_or(0.0),
1418                        pd: s.pd,
1419                        ps: s.ps,
1420                        nrd: s.nrd,
1421                        nrs: s.nrs,
1422                    };
1423                    let sz = crate::bsim1::Bsim1Size::new(&s.name, &s.model, &geom)
1424                        .map_err(Error::Build)?;
1425                    devices.push(Box::new(crate::bsim1::Bsim1I::new(
1426                        [d, gt, so, b],
1427                        [dp, sp],
1428                        sz,
1429                        geom,
1430                        s.off,
1431                        s.ic,
1432                        base,
1433                    )));
1434                }
1435                Spec::Bsim2(s) => {
1436                    let d = get(&node_ids, &s.nodes[0]);
1437                    let gt = get(&node_ids, &s.nodes[1]);
1438                    let so = get(&node_ids, &s.nodes[2]);
1439                    let b = get(&node_ids, &s.nodes[3]);
1440                    let dp = if s.model.needs_prime(s.nrd) {
1441                        node_ids[&format!("{}#drain", s.name)]
1442                    } else {
1443                        d
1444                    };
1445                    let sp = if s.model.needs_prime(s.nrs) {
1446                        node_ids[&format!("{}#source", s.name)]
1447                    } else {
1448                        so
1449                    };
1450                    // `b2set.c`: L and W default to 5 µm, not the
1451                    // `.options` MOS defaults.
1452                    let geom = crate::mos1::MosGeom {
1453                        l: s.l.unwrap_or(5e-6),
1454                        w: s.w.unwrap_or(5e-6),
1455                        ad: s.ad.unwrap_or(0.0),
1456                        as_: s.as_.unwrap_or(0.0),
1457                        pd: s.pd,
1458                        ps: s.ps,
1459                        nrd: s.nrd,
1460                        nrs: s.nrs,
1461                    };
1462                    let sz = crate::bsim2::Bsim2Size::new(&s.name, &s.model, &geom)
1463                        .map_err(Error::Build)?;
1464                    devices.push(Box::new(crate::bsim2::Bsim2I::new(
1465                        [d, gt, so, b],
1466                        [dp, sp],
1467                        sz,
1468                        geom,
1469                        s.off,
1470                        s.ic,
1471                        base,
1472                    )));
1473                }
1474                Spec::Bsim3(s) => {
1475                    let d = get(&node_ids, &s.nodes[0]);
1476                    let gt = get(&node_ids, &s.nodes[1]);
1477                    let so = get(&node_ids, &s.nodes[2]);
1478                    let b = get(&node_ids, &s.nodes[3]);
1479                    let dp = if s.model.needs_prime(s.nrd) {
1480                        node_ids[&format!("{}#drain", s.name)]
1481                    } else {
1482                        d
1483                    };
1484                    let sp = if s.model.needs_prime(s.nrs) {
1485                        node_ids[&format!("{}#source", s.name)]
1486                    } else {
1487                        so
1488                    };
1489                    let geom = crate::bsim3::Bsim3Geom {
1490                        l: s.l.unwrap_or(5e-6),
1491                        w: s.w.unwrap_or(5e-6),
1492                        ad: s.ad,
1493                        as_: s.as_,
1494                        pd: s.pd,
1495                        ps: s.ps,
1496                        nrd: s.nrd,
1497                        nrs: s.nrs,
1498                        m: s.m,
1499                        delvto: s.delvto,
1500                        mulu0: s.mulu0,
1501                    };
1502                    let (model, mt, pp) = crate::bsim3::Bsim3Knots::resolve(
1503                        &mut bsim3_models,
1504                        &s.name,
1505                        &s.model,
1506                        geom.l,
1507                        geom.w,
1508                        opts.temp,
1509                        opts.tnom,
1510                        &mut notes,
1511                    )
1512                    .map_err(Error::Build)?;
1513                    devices.push(Box::new(crate::bsim3::Bsim3I::new(
1514                        [d, gt, so, b],
1515                        [dp, sp],
1516                        model,
1517                        mt,
1518                        pp,
1519                        geom,
1520                        s.off,
1521                        s.ic,
1522                        base,
1523                    )));
1524                }
1525                Spec::Resistor(s) => {
1526                    let rt = self.res_temp.get(&s.name).copied().unwrap_or_default();
1527                    devices.push(Box::new(ResistorI::with_temp(
1528                        s.value,
1529                        rt,
1530                        opts,
1531                        get(&node_ids, &s.nodes[0]),
1532                        get(&node_ids, &s.nodes[1]),
1533                    )));
1534                }
1535                Spec::Capacitor(s) => {
1536                    devices.push(Box::new(CapacitorI::new(
1537                        s.value,
1538                        s.ic,
1539                        get(&node_ids, &s.nodes[0]),
1540                        get(&node_ids, &s.nodes[1]),
1541                        base,
1542                    )));
1543                }
1544                Spec::Inductor(s) => {
1545                    devices.push(Box::new(InductorI::new(
1546                        s.value,
1547                        s.ic.unwrap_or(0.0),
1548                        get(&node_ids, &s.nodes[0]),
1549                        get(&node_ids, &s.nodes[1]),
1550                        branches[&s.name],
1551                        base,
1552                    )));
1553                }
1554                Spec::Mutual(s) => {
1555                    for l in [&s.l1, &s.l2] {
1556                        if !inductors.contains_key(l) {
1557                            return Err(Error::Build(format!(
1558                                "`{}`: coupled inductor `{l}` does not exist",
1559                                s.name
1560                            )));
1561                        }
1562                    }
1563                    let (flux1, l1) = inductors[&s.l1];
1564                    let (flux2, l2) = inductors[&s.l2];
1565                    // `MUTfactor = k·sqrt(L1·L2)` (`mutsetup.c`).
1566                    devices.push(Box::new(MutualI::new(
1567                        s.k,
1568                        (l1 * l2).abs().sqrt(),
1569                        branches[&s.l1],
1570                        branches[&s.l2],
1571                        flux1,
1572                        flux2,
1573                    )));
1574                }
1575                Spec::Vsource(s) => {
1576                    sources.insert(s.name.clone(), (devices.len(), true));
1577                    let pos = get(&node_ids, &s.nodes[0]);
1578                    let neg = get(&node_ids, &s.nodes[1]);
1579                    let br = branches[&s.name];
1580                    let noise = NoiseWave::from_wave(s.wave.as_ref(), opts, &s.name)?;
1581                    let mut dev = VsourceI::new(s.dc, s.wave.clone(), s.ac, pos, neg, br)
1582                        .with_disto(s.distof1, s.distof2)
1583                        .with_noise(noise);
1584                    if let Some(p) = s.port.filter(RfPort::is_port) {
1585                        let res = get(&node_ids, &format!("{}#res", s.name));
1586                        dev = dev.with_port(res, p.z0);
1587                        ports.push(PortInfo {
1588                            name: s.name.clone(),
1589                            num: p.num,
1590                            z0: p.z0,
1591                            pos,
1592                            neg,
1593                            br,
1594                        });
1595                    }
1596                    devices.push(Box::new(dev));
1597                }
1598                Spec::Isource(s) => {
1599                    sources.insert(s.name.clone(), (devices.len(), false));
1600                    let noise = NoiseWave::from_wave(s.wave.as_ref(), opts, &s.name)?;
1601                    devices.push(Box::new(
1602                        IsourceI::new(
1603                            s.dc,
1604                            s.wave.clone(),
1605                            s.ac,
1606                            get(&node_ids, &s.nodes[0]),
1607                            get(&node_ids, &s.nodes[1]),
1608                        )
1609                        .with_disto(s.distof1, s.distof2)
1610                        .with_noise(noise),
1611                    ));
1612                }
1613                Spec::Vcvs(s) => {
1614                    let n = [
1615                        get(&node_ids, &s.nodes[0]),
1616                        get(&node_ids, &s.nodes[1]),
1617                        get(&node_ids, &s.nodes[2]),
1618                        get(&node_ids, &s.nodes[3]),
1619                    ];
1620                    devices.push(Box::new(VcvsI::new(s.gain, n, branches[&s.name])));
1621                }
1622                Spec::Vccs(s) => {
1623                    let n = [
1624                        get(&node_ids, &s.nodes[0]),
1625                        get(&node_ids, &s.nodes[1]),
1626                        get(&node_ids, &s.nodes[2]),
1627                        get(&node_ids, &s.nodes[3]),
1628                    ];
1629                    devices.push(Box::new(VccsI::new(s.gain, n)));
1630                }
1631                Spec::Cccs(s) => {
1632                    let ctrl = ctrl_branch(&branches, &s.name, &s.ctrl)?;
1633                    devices.push(Box::new(CccsI::new(
1634                        s.gain,
1635                        get(&node_ids, &s.nodes[0]),
1636                        get(&node_ids, &s.nodes[1]),
1637                        ctrl,
1638                    )));
1639                }
1640                Spec::Asrc(s) => {
1641                    let pos = get(&node_ids, &s.nodes[0]);
1642                    let neg = get(&node_ids, &s.nodes[1]);
1643                    let voltage = s.kind == dune_netlist::ir::SourceKind::Voltage;
1644                    // ngspice's `ASRCsetup` check (the 3f5 would go on
1645                    // to a singular matrix).
1646                    if voltage && pos == neg {
1647                        return Err(Error::Build(format!(
1648                            "`{}`: voltage B source is shorted (both nodes `{}`)",
1649                            s.name, s.nodes[0]
1650                        )));
1651                    }
1652                    let tree = crate::asrc::Tree::new(&s.expr);
1653                    let mut cols = Vec::with_capacity(tree.vars.len());
1654                    for v in &tree.vars {
1655                        cols.push(match v {
1656                            crate::asrc::VarRef::Node(n) => get(&node_ids, n),
1657                            // `CKTfndBranch`: any device with a branch
1658                            // equation.
1659                            crate::asrc::VarRef::Branch(d) => {
1660                                branches.get(d).copied().ok_or_else(|| {
1661                                    Error::Build(format!(
1662                                        "`{}`: unknown controlling source `{d}`",
1663                                        s.name
1664                                    ))
1665                                })?
1666                            }
1667                        });
1668                    }
1669                    let br = if voltage { branches[&s.name] } else { 0 };
1670                    devices.push(Box::new(crate::asrc::AsrcI::new(
1671                        s.kind, pos, neg, br, tree, cols, base,
1672                    )));
1673                }
1674                Spec::Ccvs(s) => {
1675                    let ctrl = ctrl_branch(&branches, &s.name, &s.ctrl)?;
1676                    devices.push(Box::new(CcvsI::new(
1677                        s.gain,
1678                        get(&node_ids, &s.nodes[0]),
1679                        get(&node_ids, &s.nodes[1]),
1680                        branches[&s.name],
1681                        ctrl,
1682                    )));
1683                }
1684                Spec::Diode(s) => {
1685                    let pos = get(&node_ids, &s.nodes[0]);
1686                    let neg = get(&node_ids, &s.nodes[1]);
1687                    let pos_prime = if s.model.rs != 0.0 {
1688                        get(&node_ids, &format!("{}#internal", s.name))
1689                    } else {
1690                        pos
1691                    };
1692                    let temp = self.instance_temp(&s.name, s.temp, opts);
1693                    let t = DiodeTemp::new(
1694                        &s.name,
1695                        &s.model,
1696                        s.area,
1697                        1.0,
1698                        temp,
1699                        opts.tnom,
1700                        opts.reltol,
1701                    );
1702                    notes.extend(t.notes.iter().cloned());
1703                    devices.push(Box::new(crate::diode::DiodeI::new(
1704                        pos,
1705                        neg,
1706                        pos_prime,
1707                        t,
1708                        s.area,
1709                        s.off,
1710                        s.ic.unwrap_or(0.0),
1711                        base,
1712                        crate::diode::DiodeSensBase {
1713                            name: s.name.clone(),
1714                            model: s.model.clone(),
1715                            temp,
1716                            m: 1.0,
1717                            default_tnom: opts.tnom,
1718                            reltol: opts.reltol,
1719                        },
1720                    )));
1721                }
1722                Spec::Jfet(s) => {
1723                    let (nodes, primes) =
1724                        fet3_vars(&node_ids, &s.name, &s.nodes, s.model.rd, s.model.rs);
1725                    let temp = self.instance_temp(&s.name, s.temp, opts);
1726                    let t = crate::jfet::JfetTemp::new(&s.name, &s.model, temp, opts.tnom);
1727                    notes.extend(t.notes.iter().cloned());
1728                    devices.push(Box::new(crate::jfet::JfetI::new(
1729                        nodes, primes, t, s.area, s.off, s.ic, base,
1730                    )));
1731                }
1732                Spec::Mes(s) => {
1733                    let (nodes, primes) =
1734                        fet3_vars(&node_ids, &s.name, &s.nodes, s.model.rd, s.model.rs);
1735                    let t = crate::mes::MesTemp::new(&s.model);
1736                    devices.push(Box::new(crate::mes::MesI::new(
1737                        nodes, primes, t, s.area, s.off, s.ic, base,
1738                    )));
1739                }
1740                Spec::Ltra(s) => {
1741                    let n = [
1742                        get(&node_ids, &s.nodes[0]),
1743                        get(&node_ids, &s.nodes[1]),
1744                        get(&node_ids, &s.nodes[2]),
1745                        get(&node_ids, &s.nodes[3]),
1746                    ];
1747                    let br = [
1748                        get(&node_ids, &format!("{}#i1", s.name)),
1749                        get(&node_ids, &format!("{}#i2", s.name)),
1750                    ];
1751                    let mut p =
1752                        crate::ltra::LtraParams::new(&s.name, &s.model, opts.reltol, opts.abstol)
1753                            .map_err(Error::Build)?;
1754                    if opts.trytocompact {
1755                        p.try_to_compact();
1756                    }
1757                    if opts.compat == crate::options::Compat::Dune {
1758                        p.default_linear();
1759                    }
1760                    notes.extend(p.notes.iter().cloned());
1761                    devices.push(Box::new(
1762                        crate::ltra::LtraI::new(p, n, br)
1763                            .with_fit(opts.ltra_fit)
1764                            .with_slope_floor(opts.ltra_slope_rate().is_some()),
1765                    ));
1766                }
1767                Spec::Tra(s) => {
1768                    let n = [
1769                        get(&node_ids, &s.nodes[0]),
1770                        get(&node_ids, &s.nodes[1]),
1771                        get(&node_ids, &s.nodes[2]),
1772                        get(&node_ids, &s.nodes[3]),
1773                    ];
1774                    let v = |suffix: &str| get(&node_ids, &format!("{}#{suffix}", s.name));
1775                    let p = crate::tra::TraParams::new(&s.name, s.z0, s.td, s.f, s.nl, s.ic)
1776                        .map_err(Error::Build)?;
1777                    devices.push(Box::new(crate::tra::TraI::new(
1778                        p,
1779                        n,
1780                        [v("i1"), v("i2")],
1781                        [v("int1"), v("int2")],
1782                    )));
1783                }
1784                Spec::SwitchV(s) => {
1785                    let m = &s.model;
1786                    let p = crate::switch::SwitchParams::new(m.vt, m.vh, m.ron, m.roff, opts.gmin);
1787                    let ctrl = crate::switch::Control::Voltage {
1788                        pos: get(&node_ids, &s.nodes[2]),
1789                        neg: get(&node_ids, &s.nodes[3]),
1790                    };
1791                    devices.push(Box::new(crate::switch::SwitchInst::new(
1792                        p,
1793                        get(&node_ids, &s.nodes[0]),
1794                        get(&node_ids, &s.nodes[1]),
1795                        ctrl,
1796                        s.on,
1797                        base,
1798                    )));
1799                }
1800                Spec::SwitchI(s) => {
1801                    // `CSWsetup`: "unknown controlling source".
1802                    let branch = ctrl_branch(&branches, &s.name, &s.ctrl)?;
1803                    let m = &s.model;
1804                    let p = crate::switch::SwitchParams::new(m.it, m.ih, m.ron, m.roff, opts.gmin);
1805                    devices.push(Box::new(crate::switch::SwitchInst::new(
1806                        p,
1807                        get(&node_ids, &s.nodes[0]),
1808                        get(&node_ids, &s.nodes[1]),
1809                        crate::switch::Control::Current { branch },
1810                        s.on,
1811                        base,
1812                    )));
1813                }
1814            }
1815        }
1816
1817        // `*param.c`'s `*icXGiven` bits for `DEVsetic`.
1818        for (spec, dev) in self.specs.iter().zip(devices.iter_mut()) {
1819            if let Some(&g) = self.ic_given.get(spec.name()) {
1820                dev.set_ic_given(g);
1821            }
1822        }
1823
1824        // Nodesets and ics must name existing nodes.
1825        let mut nodesets = Vec::new();
1826        for (name, v) in &self.nodesets {
1827            match node_ids.get(name) {
1828                Some(&var) if var != 0 => nodesets.push((var, *v)),
1829                Some(_) => notes.push(".nodeset on ground ignored".to_string()),
1830                None => notes.push(format!(".nodeset names unknown node `{name}`")),
1831            }
1832        }
1833        let mut ics = Vec::new();
1834        for (name, v) in &self.ics {
1835            match node_ids.get(name) {
1836                Some(&var) if var != 0 => ics.push((var, *v)),
1837                Some(_) => notes.push(".ic on ground ignored".to_string()),
1838                None => notes.push(format!(".ic names unknown node `{name}`")),
1839            }
1840        }
1841
1842        // The mutual inductor adds no matrix row of its own but
1843        // keeps `devices` and `names` aligned by construction.
1844        let ports = check_ports(ports)?;
1845        Ok(Built {
1846            vars,
1847            devices,
1848            names,
1849            nodesets,
1850            ics,
1851            sources,
1852            ports,
1853            num_states,
1854            state_ranges,
1855            notes,
1856            warm: self.warm.share(),
1857        })
1858    }
1859}
1860
1861/// `VSRCtemp`'s port checks (ngspice-47, RFSPICE): with N ports,
1862/// every index must lie in `1..=N` and none may repeat, so the ports
1863/// are exactly `1..=N`. The C walks the instances newest first (the
1864/// instance list is built by prepending), so the reported instance
1865/// is the last offending card. Returns the ports sorted by index
1866/// (`CKTrfPorts`).
1867fn check_ports(mut ports: Vec<PortInfo>) -> Result<Vec<PortInfo>, Error> {
1868    let count = ports.len();
1869    let mut seen = vec![false; count + 1];
1870    for p in ports.iter().rev() {
1871        let n = p.num as usize;
1872        if n > count {
1873            return Err(Error::Build(format!("{}: incorrect port ordering", p.name)));
1874        }
1875        if std::mem::replace(&mut seen[n], true) {
1876            return Err(Error::Build(format!("{}: duplicate port Index", p.name)));
1877        }
1878    }
1879    ports.sort_by_key(|p| p.num);
1880    Ok(ports)
1881}
1882
1883/// Terminal and prime-node variables of a JFET/MESFET.
1884fn fet3_vars(
1885    node_ids: &HashMap<String, Var>,
1886    name: &str,
1887    nodes: &[String; 3],
1888    rd: f64,
1889    rs: f64,
1890) -> ([Var; 3], [Var; 2]) {
1891    let d = node_ids[&nodes[0]];
1892    let g = node_ids[&nodes[1]];
1893    let s = node_ids[&nodes[2]];
1894    let dp = if rd != 0.0 {
1895        node_ids[&format!("{name}#drain")]
1896    } else {
1897        d
1898    };
1899    let sp = if rs != 0.0 {
1900        node_ids[&format!("{name}#source")]
1901    } else {
1902        s
1903    };
1904    ([d, g, s], [dp, sp])
1905}
1906
1907fn ctrl_branch(branches: &HashMap<String, Var>, name: &str, ctrl: &str) -> Result<Var, Error> {
1908    if !ctrl.starts_with('v') {
1909        return Err(Error::Build(format!(
1910            "`{name}`: controlling source `{ctrl}` must be a voltage source"
1911        )));
1912    }
1913    branches
1914        .get(ctrl)
1915        .copied()
1916        .ok_or_else(|| Error::Build(format!("`{name}`: controlling source `{ctrl}` not found")))
1917}