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                _ => 0,
992            });
993            state_ranges.push((first, st.total()));
994            let node_names: &[String] = match spec {
995                Spec::Bjt(s) => &s.nodes,
996                Spec::Mos1(s) => &s.nodes,
997                Spec::Mos2(s) => &s.nodes,
998                Spec::Mos3(s) => &s.nodes,
999                Spec::Mos6(s) => &s.nodes,
1000                Spec::Bsim1(s) => &s.nodes,
1001                Spec::Bsim2(s) => &s.nodes,
1002                Spec::Bsim3(s) => &s.nodes,
1003                Spec::Resistor(s) => &s.nodes,
1004                Spec::Capacitor(s) => &s.nodes,
1005                Spec::Inductor(s) => &s.nodes,
1006                Spec::Mutual(_) => &[],
1007                Spec::Vsource(s) => &s.nodes,
1008                Spec::Isource(s) => &s.nodes,
1009                Spec::Vcvs(s) => &s.nodes,
1010                Spec::Vccs(s) => &s.nodes,
1011                Spec::Cccs(s) => &s.nodes,
1012                Spec::Ccvs(s) => &s.nodes,
1013                Spec::Diode(s) => &s.nodes,
1014                Spec::Jfet(s) => &s.nodes,
1015                Spec::Mes(s) => &s.nodes,
1016                Spec::Ltra(s) => &s.nodes,
1017                Spec::Tra(s) => &s.nodes,
1018                Spec::SwitchV(s) => &s.nodes,
1019                Spec::SwitchI(s) => &s.nodes,
1020                Spec::Asrc(s) => &s.nodes,
1021            };
1022            for n in node_names {
1023                node(&mut vars, &mut node_ids, n);
1024            }
1025            // `INPgetTree` inserts the `v()` nodes while parsing the
1026            // card (after N+ N-); `ASRCsetup` then makes the branch of
1027            // a voltage source (`b1#branch`).
1028            if let Spec::Asrc(s) = spec {
1029                let mut refs = Vec::new();
1030                let mut e = s.expr.clone();
1031                e.visit_refs_mut(&mut |n| refs.push(n.clone()), &mut |_| {});
1032                for n in &refs {
1033                    node(&mut vars, &mut node_ids, n);
1034                }
1035                if s.kind == dune_netlist::ir::SourceKind::Voltage {
1036                    let br = vars.push(s.name.clone(), VarKind::Branch);
1037                    branches.insert(s.name.clone(), br);
1038                }
1039            }
1040            match spec {
1041                Spec::Vsource(s) => {
1042                    let br = vars.push(s.name.clone(), VarKind::Branch);
1043                    branches.insert(s.name.clone(), br);
1044                    // `VSRCsetup` (RFSPICE): a port's internal node
1045                    // behind the reference impedance, made after the
1046                    // branch with `CKTmkVolt` (`v1#res`).
1047                    if s.port.is_some_and(|p| p.is_port()) {
1048                        node(&mut vars, &mut node_ids, &format!("{}#res", s.name));
1049                    }
1050                }
1051                Spec::Inductor(s) => {
1052                    let br = vars.push(s.name.clone(), VarKind::Branch);
1053                    branches.insert(s.name.clone(), br);
1054                }
1055                Spec::Vcvs(s) => {
1056                    let br = vars.push(s.name.clone(), VarKind::Branch);
1057                    branches.insert(s.name.clone(), br);
1058                }
1059                Spec::Ccvs(s) => {
1060                    let br = vars.push(s.name.clone(), VarKind::Branch);
1061                    branches.insert(s.name.clone(), br);
1062                }
1063                // `LTRAsetup`: the two port-current equations, made
1064                // with `CKTmkVolt` — voltage-typed unknowns for the
1065                // convergence test and the rawfile (`v(o1#i1)`).
1066                Spec::Ltra(s) => {
1067                    node(&mut vars, &mut node_ids, &format!("{}#i1", s.name));
1068                    node(&mut vars, &mut node_ids, &format!("{}#i2", s.name));
1069                }
1070                // `TRAsetup`: both branch equations, then both
1071                // internal nodes, all with `CKTmkVolt`.
1072                Spec::Tra(s) => {
1073                    for suffix in ["i1", "i2", "int1", "int2"] {
1074                        node(&mut vars, &mut node_ids, &format!("{}#{suffix}", s.name));
1075                    }
1076                }
1077                // `DIOsetup`: internal node past RS, named
1078                // `<name>#internal` like `CKTmkVolt` does.
1079                Spec::Diode(s) if s.model.rs != 0.0 => {
1080                    node(&mut vars, &mut node_ids, &format!("{}#internal", s.name));
1081                }
1082                // `JFETsetup`/`MESsetup`: source prime first, then
1083                // drain prime (`<name>#source`, `<name>#drain`).
1084                Spec::Jfet(s) => {
1085                    if s.model.rs != 0.0 {
1086                        node(&mut vars, &mut node_ids, &format!("{}#source", s.name));
1087                    }
1088                    if s.model.rd != 0.0 {
1089                        node(&mut vars, &mut node_ids, &format!("{}#drain", s.name));
1090                    }
1091                }
1092                Spec::Mes(s) => {
1093                    if s.model.rs != 0.0 {
1094                        node(&mut vars, &mut node_ids, &format!("{}#source", s.name));
1095                    }
1096                    if s.model.rd != 0.0 {
1097                        node(&mut vars, &mut node_ids, &format!("{}#drain", s.name));
1098                    }
1099                }
1100                // `BJTsetup`: one prime node per nonzero parasitic
1101                // resistance (`q1#collector` etc., `CKTmkVolt`).
1102                Spec::Bjt(s) => {
1103                    if s.model.rc != 0.0 {
1104                        node(&mut vars, &mut node_ids, &format!("{}#collector", s.name));
1105                    }
1106                    if s.model.rb != 0.0 {
1107                        node(&mut vars, &mut node_ids, &format!("{}#base", s.name));
1108                    }
1109                    if s.model.re != 0.0 {
1110                        node(&mut vars, &mut node_ids, &format!("{}#emitter", s.name));
1111                    }
1112                }
1113                // `MOS1setup`: prime nodes past the series
1114                // resistance (`m1#drain`/`m1#source`, `CKTmkVolt`).
1115                Spec::Mos1(s) => {
1116                    if s.model.needs_prime(s.nrd) {
1117                        node(&mut vars, &mut node_ids, &format!("{}#drain", s.name));
1118                    }
1119                    if s.model.needs_prime(s.nrs) {
1120                        node(&mut vars, &mut node_ids, &format!("{}#source", s.name));
1121                    }
1122                }
1123                Spec::Mos2(s) => {
1124                    if s.model.needs_prime(s.nrd) {
1125                        node(
1126                            &mut vars,
1127                            &mut node_ids,
1128                            &format!("{}#internal#drain", s.name),
1129                        );
1130                    }
1131                    if s.model.needs_prime(s.nrs) {
1132                        node(
1133                            &mut vars,
1134                            &mut node_ids,
1135                            &format!("{}#internal#source", s.name),
1136                        );
1137                    }
1138                }
1139                Spec::Mos3(s) => {
1140                    if s.model.needs_prime(s.nrd) {
1141                        node(
1142                            &mut vars,
1143                            &mut node_ids,
1144                            &format!("{}#internal#drain", s.name),
1145                        );
1146                    }
1147                    if s.model.needs_prime(s.nrs) {
1148                        node(
1149                            &mut vars,
1150                            &mut node_ids,
1151                            &format!("{}#internal#source", s.name),
1152                        );
1153                    }
1154                }
1155                // `MOS6setup` names them like MOS1's (`m1#drain`).
1156                Spec::Mos6(s) => {
1157                    if s.model.needs_prime(s.nrd) {
1158                        node(&mut vars, &mut node_ids, &format!("{}#drain", s.name));
1159                    }
1160                    if s.model.needs_prime(s.nrs) {
1161                        node(&mut vars, &mut node_ids, &format!("{}#source", s.name));
1162                    }
1163                }
1164                // `B1setup`/`B2setup` name them like MOS1's too.
1165                Spec::Bsim1(s) => {
1166                    if s.model.needs_prime(s.nrd) {
1167                        node(&mut vars, &mut node_ids, &format!("{}#drain", s.name));
1168                    }
1169                    if s.model.needs_prime(s.nrs) {
1170                        node(&mut vars, &mut node_ids, &format!("{}#source", s.name));
1171                    }
1172                }
1173                Spec::Bsim2(s) => {
1174                    if s.model.needs_prime(s.nrd) {
1175                        node(&mut vars, &mut node_ids, &format!("{}#drain", s.name));
1176                    }
1177                    if s.model.needs_prime(s.nrs) {
1178                        node(&mut vars, &mut node_ids, &format!("{}#source", s.name));
1179                    }
1180                }
1181                // `BSIM3setup`: `CKTmkVolt(…, "drain")` / `"source"`.
1182                Spec::Bsim3(s) => {
1183                    if s.model.needs_prime(s.nrd) {
1184                        node(&mut vars, &mut node_ids, &format!("{}#drain", s.name));
1185                    }
1186                    if s.model.needs_prime(s.nrs) {
1187                        node(&mut vars, &mut node_ids, &format!("{}#source", s.name));
1188                    }
1189                }
1190                _ => {}
1191            }
1192        }
1193        let num_states = st.total();
1194
1195        // Pass 2: instances.
1196        let mut devices: Vec<Box<dyn Device>> = Vec::new();
1197        let mut names: Vec<String> = Vec::new();
1198        let mut sources: HashMap<String, (usize, bool)> = HashMap::new();
1199        let mut ports: Vec<PortInfo> = Vec::new();
1200        let mut notes = Vec::new();
1201        let get = |node_ids: &HashMap<String, Var>, n: &str| node_ids[n];
1202        // `BSIM3temp`'s per-model knot list: models (as given) with
1203        // their set-up copy, temperature part and the size-dependent
1204        // parameter sets already built, keyed by drawn (L, W).
1205        let mut bsim3_models: Vec<crate::bsim3::Bsim3Knots> = Vec::new();
1206        for (spec, &base) in self.specs.iter().zip(&state_base) {
1207            names.push(spec.name().to_string());
1208            match spec {
1209                Spec::Bjt(s) => {
1210                    let col = get(&node_ids, &s.nodes[0]);
1211                    let bse = get(&node_ids, &s.nodes[1]);
1212                    let emi = get(&node_ids, &s.nodes[2]);
1213                    let sub = get(&node_ids, &s.nodes[3]);
1214                    let prime = |rz: f64, suffix: &str, outer: Var| {
1215                        if rz != 0.0 {
1216                            node_ids[&format!("{}#{suffix}", s.name)]
1217                        } else {
1218                            outer
1219                        }
1220                    };
1221                    let primes = [
1222                        prime(s.model.rc, "collector", col),
1223                        prime(s.model.rb, "base", bse),
1224                        prime(s.model.re, "emitter", emi),
1225                    ];
1226                    let temp = self.instance_temp(&s.name, s.temp, opts);
1227                    let bt = crate::bjt::BjtTemp::new(&s.name, &s.model, temp, opts.tnom);
1228                    notes.extend(bt.notes.iter().cloned());
1229                    devices.push(Box::new(crate::bjt::BjtI::new(
1230                        [col, bse, emi, sub],
1231                        primes,
1232                        bt,
1233                        s.area,
1234                        s.off,
1235                        s.ic,
1236                        base,
1237                    )));
1238                }
1239                Spec::Mos1(s) => {
1240                    let d = get(&node_ids, &s.nodes[0]);
1241                    let gt = get(&node_ids, &s.nodes[1]);
1242                    let so = get(&node_ids, &s.nodes[2]);
1243                    let b = get(&node_ids, &s.nodes[3]);
1244                    let dp = if s.model.needs_prime(s.nrd) {
1245                        node_ids[&format!("{}#drain", s.name)]
1246                    } else {
1247                        d
1248                    };
1249                    let sp = if s.model.needs_prime(s.nrs) {
1250                        node_ids[&format!("{}#source", s.name)]
1251                    } else {
1252                        so
1253                    };
1254                    let geom = crate::mos1::MosGeom {
1255                        l: s.l.unwrap_or(opts.defl),
1256                        w: s.w.unwrap_or(opts.defw),
1257                        ad: s.ad.unwrap_or(opts.defad),
1258                        as_: s.as_.unwrap_or(opts.defas),
1259                        pd: s.pd,
1260                        ps: s.ps,
1261                        nrd: s.nrd,
1262                        nrs: s.nrs,
1263                    };
1264                    let temp = self.instance_temp(&s.name, s.temp, opts);
1265                    let mt = crate::mos1::Mos1Temp::new(&s.name, &s.model, &geom, temp, opts.tnom)
1266                        .map_err(Error::Build)?;
1267                    notes.extend(mt.notes.iter().cloned());
1268                    devices.push(Box::new(crate::mos1::Mos1I::new(
1269                        [d, gt, so, b],
1270                        [dp, sp],
1271                        mt,
1272                        geom,
1273                        s.off,
1274                        s.ic,
1275                        base,
1276                    )));
1277                }
1278                Spec::Mos2(s) => {
1279                    let d = get(&node_ids, &s.nodes[0]);
1280                    let gt = get(&node_ids, &s.nodes[1]);
1281                    let so = get(&node_ids, &s.nodes[2]);
1282                    let b = get(&node_ids, &s.nodes[3]);
1283                    let dp = if s.model.needs_prime(s.nrd) {
1284                        node_ids[&format!("{}#internal#drain", s.name)]
1285                    } else {
1286                        d
1287                    };
1288                    let sp = if s.model.needs_prime(s.nrs) {
1289                        node_ids[&format!("{}#internal#source", s.name)]
1290                    } else {
1291                        so
1292                    };
1293                    let geom = crate::mos1::MosGeom {
1294                        l: s.l.unwrap_or(opts.defl),
1295                        w: s.w.unwrap_or(opts.defw),
1296                        ad: s.ad.unwrap_or(opts.defad),
1297                        as_: s.as_.unwrap_or(opts.defas),
1298                        pd: s.pd,
1299                        ps: s.ps,
1300                        nrd: s.nrd,
1301                        nrs: s.nrs,
1302                    };
1303                    let temp = self.instance_temp(&s.name, s.temp, opts);
1304                    let mt = crate::mos2::Mos2Temp::new(&s.name, &s.model, &geom, temp, opts.tnom)
1305                        .map_err(Error::Build)?;
1306                    notes.extend(mt.notes.iter().cloned());
1307                    devices.push(Box::new(crate::mos2::Mos2I::new(
1308                        [d, gt, so, b],
1309                        [dp, sp],
1310                        mt,
1311                        geom,
1312                        s.off,
1313                        s.ic,
1314                        base,
1315                    )));
1316                }
1317                Spec::Mos3(s) => {
1318                    let d = get(&node_ids, &s.nodes[0]);
1319                    let gt = get(&node_ids, &s.nodes[1]);
1320                    let so = get(&node_ids, &s.nodes[2]);
1321                    let b = get(&node_ids, &s.nodes[3]);
1322                    let dp = if s.model.needs_prime(s.nrd) {
1323                        node_ids[&format!("{}#internal#drain", s.name)]
1324                    } else {
1325                        d
1326                    };
1327                    let sp = if s.model.needs_prime(s.nrs) {
1328                        node_ids[&format!("{}#internal#source", s.name)]
1329                    } else {
1330                        so
1331                    };
1332                    let geom = crate::mos1::MosGeom {
1333                        l: s.l.unwrap_or(opts.defl),
1334                        w: s.w.unwrap_or(opts.defw),
1335                        ad: s.ad.unwrap_or(opts.defad),
1336                        as_: s.as_.unwrap_or(opts.defas),
1337                        pd: s.pd,
1338                        ps: s.ps,
1339                        nrd: s.nrd,
1340                        nrs: s.nrs,
1341                    };
1342                    let temp = self.instance_temp(&s.name, s.temp, opts);
1343                    let mt = crate::mos3::Mos3Temp::new(&s.name, &s.model, &geom, temp, opts.tnom)
1344                        .map_err(Error::Build)?;
1345                    devices.push(Box::new(crate::mos3::Mos3I::new(
1346                        [d, gt, so, b],
1347                        [dp, sp],
1348                        mt,
1349                        geom,
1350                        s.off,
1351                        s.ic,
1352                        base,
1353                    )));
1354                }
1355                Spec::Mos6(s) => {
1356                    let d = get(&node_ids, &s.nodes[0]);
1357                    let gt = get(&node_ids, &s.nodes[1]);
1358                    let so = get(&node_ids, &s.nodes[2]);
1359                    let b = get(&node_ids, &s.nodes[3]);
1360                    let dp = if s.model.needs_prime(s.nrd) {
1361                        node_ids[&format!("{}#drain", s.name)]
1362                    } else {
1363                        d
1364                    };
1365                    let sp = if s.model.needs_prime(s.nrs) {
1366                        node_ids[&format!("{}#source", s.name)]
1367                    } else {
1368                        so
1369                    };
1370                    // `mos6temp.c` forces zero squares back to 1.
1371                    let geom = crate::mos1::MosGeom {
1372                        l: s.l.unwrap_or(opts.defl),
1373                        w: s.w.unwrap_or(opts.defw),
1374                        ad: s.ad.unwrap_or(opts.defad),
1375                        as_: s.as_.unwrap_or(opts.defas),
1376                        pd: s.pd,
1377                        ps: s.ps,
1378                        nrd: if s.nrd == 0.0 { 1.0 } else { s.nrd },
1379                        nrs: if s.nrs == 0.0 { 1.0 } else { s.nrs },
1380                    };
1381                    let temp = self.instance_temp(&s.name, s.temp, opts);
1382                    let mt = crate::mos6::Mos6Temp::new(&s.name, &s.model, &geom, temp, opts.tnom)
1383                        .map_err(Error::Build)?;
1384                    notes.extend(mt.notes.iter().cloned());
1385                    devices.push(Box::new(crate::mos6::Mos6I::new(
1386                        [d, gt, so, b],
1387                        [dp, sp],
1388                        mt,
1389                        geom,
1390                        s.off,
1391                        s.ic,
1392                        base,
1393                    )));
1394                }
1395                Spec::Bsim1(s) => {
1396                    let d = get(&node_ids, &s.nodes[0]);
1397                    let gt = get(&node_ids, &s.nodes[1]);
1398                    let so = get(&node_ids, &s.nodes[2]);
1399                    let b = get(&node_ids, &s.nodes[3]);
1400                    let dp = if s.model.needs_prime(s.nrd) {
1401                        node_ids[&format!("{}#drain", s.name)]
1402                    } else {
1403                        d
1404                    };
1405                    let sp = if s.model.needs_prime(s.nrs) {
1406                        node_ids[&format!("{}#source", s.name)]
1407                    } else {
1408                        so
1409                    };
1410                    // `b1set.c`: L and W default to 5 µm, not the
1411                    // `.options` MOS defaults.
1412                    let geom = crate::mos1::MosGeom {
1413                        l: s.l.unwrap_or(5e-6),
1414                        w: s.w.unwrap_or(5e-6),
1415                        ad: s.ad.unwrap_or(0.0),
1416                        as_: s.as_.unwrap_or(0.0),
1417                        pd: s.pd,
1418                        ps: s.ps,
1419                        nrd: s.nrd,
1420                        nrs: s.nrs,
1421                    };
1422                    let sz = crate::bsim1::Bsim1Size::new(&s.name, &s.model, &geom)
1423                        .map_err(Error::Build)?;
1424                    devices.push(Box::new(crate::bsim1::Bsim1I::new(
1425                        [d, gt, so, b],
1426                        [dp, sp],
1427                        sz,
1428                        geom,
1429                        s.off,
1430                        s.ic,
1431                        base,
1432                    )));
1433                }
1434                Spec::Bsim2(s) => {
1435                    let d = get(&node_ids, &s.nodes[0]);
1436                    let gt = get(&node_ids, &s.nodes[1]);
1437                    let so = get(&node_ids, &s.nodes[2]);
1438                    let b = get(&node_ids, &s.nodes[3]);
1439                    let dp = if s.model.needs_prime(s.nrd) {
1440                        node_ids[&format!("{}#drain", s.name)]
1441                    } else {
1442                        d
1443                    };
1444                    let sp = if s.model.needs_prime(s.nrs) {
1445                        node_ids[&format!("{}#source", s.name)]
1446                    } else {
1447                        so
1448                    };
1449                    // `b2set.c`: L and W default to 5 µm, not the
1450                    // `.options` MOS defaults.
1451                    let geom = crate::mos1::MosGeom {
1452                        l: s.l.unwrap_or(5e-6),
1453                        w: s.w.unwrap_or(5e-6),
1454                        ad: s.ad.unwrap_or(0.0),
1455                        as_: s.as_.unwrap_or(0.0),
1456                        pd: s.pd,
1457                        ps: s.ps,
1458                        nrd: s.nrd,
1459                        nrs: s.nrs,
1460                    };
1461                    let sz = crate::bsim2::Bsim2Size::new(&s.name, &s.model, &geom)
1462                        .map_err(Error::Build)?;
1463                    devices.push(Box::new(crate::bsim2::Bsim2I::new(
1464                        [d, gt, so, b],
1465                        [dp, sp],
1466                        sz,
1467                        geom,
1468                        s.off,
1469                        s.ic,
1470                        base,
1471                    )));
1472                }
1473                Spec::Bsim3(s) => {
1474                    let d = get(&node_ids, &s.nodes[0]);
1475                    let gt = get(&node_ids, &s.nodes[1]);
1476                    let so = get(&node_ids, &s.nodes[2]);
1477                    let b = get(&node_ids, &s.nodes[3]);
1478                    let dp = if s.model.needs_prime(s.nrd) {
1479                        node_ids[&format!("{}#drain", s.name)]
1480                    } else {
1481                        d
1482                    };
1483                    let sp = if s.model.needs_prime(s.nrs) {
1484                        node_ids[&format!("{}#source", s.name)]
1485                    } else {
1486                        so
1487                    };
1488                    let geom = crate::bsim3::Bsim3Geom {
1489                        l: s.l.unwrap_or(5e-6),
1490                        w: s.w.unwrap_or(5e-6),
1491                        ad: s.ad,
1492                        as_: s.as_,
1493                        pd: s.pd,
1494                        ps: s.ps,
1495                        nrd: s.nrd,
1496                        nrs: s.nrs,
1497                        m: s.m,
1498                        delvto: s.delvto,
1499                        mulu0: s.mulu0,
1500                    };
1501                    let (model, mt, pp) = crate::bsim3::Bsim3Knots::resolve(
1502                        &mut bsim3_models,
1503                        &s.name,
1504                        &s.model,
1505                        geom.l,
1506                        geom.w,
1507                        opts.temp,
1508                        opts.tnom,
1509                        &mut notes,
1510                    )
1511                    .map_err(Error::Build)?;
1512                    devices.push(Box::new(crate::bsim3::Bsim3I::new(
1513                        [d, gt, so, b],
1514                        [dp, sp],
1515                        model,
1516                        mt,
1517                        pp,
1518                        geom,
1519                        s.off,
1520                        s.ic,
1521                        base,
1522                    )));
1523                }
1524                Spec::Resistor(s) => {
1525                    let rt = self.res_temp.get(&s.name).copied().unwrap_or_default();
1526                    devices.push(Box::new(ResistorI::with_temp(
1527                        s.value,
1528                        rt,
1529                        opts,
1530                        get(&node_ids, &s.nodes[0]),
1531                        get(&node_ids, &s.nodes[1]),
1532                    )));
1533                }
1534                Spec::Capacitor(s) => {
1535                    devices.push(Box::new(CapacitorI::new(
1536                        s.value,
1537                        s.ic,
1538                        get(&node_ids, &s.nodes[0]),
1539                        get(&node_ids, &s.nodes[1]),
1540                        base,
1541                    )));
1542                }
1543                Spec::Inductor(s) => {
1544                    devices.push(Box::new(InductorI::new(
1545                        s.value,
1546                        s.ic.unwrap_or(0.0),
1547                        get(&node_ids, &s.nodes[0]),
1548                        get(&node_ids, &s.nodes[1]),
1549                        branches[&s.name],
1550                        base,
1551                    )));
1552                }
1553                Spec::Mutual(s) => {
1554                    for l in [&s.l1, &s.l2] {
1555                        if !inductors.contains_key(l) {
1556                            return Err(Error::Build(format!(
1557                                "`{}`: coupled inductor `{l}` does not exist",
1558                                s.name
1559                            )));
1560                        }
1561                    }
1562                    let (flux1, l1) = inductors[&s.l1];
1563                    let (flux2, l2) = inductors[&s.l2];
1564                    // `MUTfactor = k·sqrt(L1·L2)` (`mutsetup.c`).
1565                    devices.push(Box::new(MutualI::new(
1566                        s.k,
1567                        (l1 * l2).abs().sqrt(),
1568                        branches[&s.l1],
1569                        branches[&s.l2],
1570                        flux1,
1571                        flux2,
1572                    )));
1573                }
1574                Spec::Vsource(s) => {
1575                    sources.insert(s.name.clone(), (devices.len(), true));
1576                    let pos = get(&node_ids, &s.nodes[0]);
1577                    let neg = get(&node_ids, &s.nodes[1]);
1578                    let br = branches[&s.name];
1579                    let noise = NoiseWave::from_wave(s.wave.as_ref(), opts, &s.name)?;
1580                    let mut dev = VsourceI::new(s.dc, s.wave.clone(), s.ac, pos, neg, br)
1581                        .with_disto(s.distof1, s.distof2)
1582                        .with_noise(noise);
1583                    if let Some(p) = s.port.filter(RfPort::is_port) {
1584                        let res = get(&node_ids, &format!("{}#res", s.name));
1585                        dev = dev.with_port(res, p.z0);
1586                        ports.push(PortInfo {
1587                            name: s.name.clone(),
1588                            num: p.num,
1589                            z0: p.z0,
1590                            pos,
1591                            neg,
1592                            br,
1593                        });
1594                    }
1595                    devices.push(Box::new(dev));
1596                }
1597                Spec::Isource(s) => {
1598                    sources.insert(s.name.clone(), (devices.len(), false));
1599                    let noise = NoiseWave::from_wave(s.wave.as_ref(), opts, &s.name)?;
1600                    devices.push(Box::new(
1601                        IsourceI::new(
1602                            s.dc,
1603                            s.wave.clone(),
1604                            s.ac,
1605                            get(&node_ids, &s.nodes[0]),
1606                            get(&node_ids, &s.nodes[1]),
1607                        )
1608                        .with_disto(s.distof1, s.distof2)
1609                        .with_noise(noise),
1610                    ));
1611                }
1612                Spec::Vcvs(s) => {
1613                    let n = [
1614                        get(&node_ids, &s.nodes[0]),
1615                        get(&node_ids, &s.nodes[1]),
1616                        get(&node_ids, &s.nodes[2]),
1617                        get(&node_ids, &s.nodes[3]),
1618                    ];
1619                    devices.push(Box::new(VcvsI::new(s.gain, n, branches[&s.name])));
1620                }
1621                Spec::Vccs(s) => {
1622                    let n = [
1623                        get(&node_ids, &s.nodes[0]),
1624                        get(&node_ids, &s.nodes[1]),
1625                        get(&node_ids, &s.nodes[2]),
1626                        get(&node_ids, &s.nodes[3]),
1627                    ];
1628                    devices.push(Box::new(VccsI::new(s.gain, n)));
1629                }
1630                Spec::Cccs(s) => {
1631                    let ctrl = ctrl_branch(&branches, &s.name, &s.ctrl)?;
1632                    devices.push(Box::new(CccsI::new(
1633                        s.gain,
1634                        get(&node_ids, &s.nodes[0]),
1635                        get(&node_ids, &s.nodes[1]),
1636                        ctrl,
1637                    )));
1638                }
1639                Spec::Asrc(s) => {
1640                    let pos = get(&node_ids, &s.nodes[0]);
1641                    let neg = get(&node_ids, &s.nodes[1]);
1642                    let voltage = s.kind == dune_netlist::ir::SourceKind::Voltage;
1643                    // ngspice's `ASRCsetup` check (the 3f5 would go on
1644                    // to a singular matrix).
1645                    if voltage && pos == neg {
1646                        return Err(Error::Build(format!(
1647                            "`{}`: voltage B source is shorted (both nodes `{}`)",
1648                            s.name, s.nodes[0]
1649                        )));
1650                    }
1651                    let tree = crate::asrc::Tree::new(&s.expr);
1652                    let mut cols = Vec::with_capacity(tree.vars.len());
1653                    for v in &tree.vars {
1654                        cols.push(match v {
1655                            crate::asrc::VarRef::Node(n) => get(&node_ids, n),
1656                            // `CKTfndBranch`: any device with a branch
1657                            // equation.
1658                            crate::asrc::VarRef::Branch(d) => {
1659                                branches.get(d).copied().ok_or_else(|| {
1660                                    Error::Build(format!(
1661                                        "`{}`: unknown controlling source `{d}`",
1662                                        s.name
1663                                    ))
1664                                })?
1665                            }
1666                        });
1667                    }
1668                    let br = if voltage { branches[&s.name] } else { 0 };
1669                    devices.push(Box::new(crate::asrc::AsrcI::new(
1670                        s.kind, pos, neg, br, tree, cols,
1671                    )));
1672                }
1673                Spec::Ccvs(s) => {
1674                    let ctrl = ctrl_branch(&branches, &s.name, &s.ctrl)?;
1675                    devices.push(Box::new(CcvsI::new(
1676                        s.gain,
1677                        get(&node_ids, &s.nodes[0]),
1678                        get(&node_ids, &s.nodes[1]),
1679                        branches[&s.name],
1680                        ctrl,
1681                    )));
1682                }
1683                Spec::Diode(s) => {
1684                    let pos = get(&node_ids, &s.nodes[0]);
1685                    let neg = get(&node_ids, &s.nodes[1]);
1686                    let pos_prime = if s.model.rs != 0.0 {
1687                        get(&node_ids, &format!("{}#internal", s.name))
1688                    } else {
1689                        pos
1690                    };
1691                    let temp = self.instance_temp(&s.name, s.temp, opts);
1692                    let t = DiodeTemp::new(
1693                        &s.name,
1694                        &s.model,
1695                        s.area,
1696                        1.0,
1697                        temp,
1698                        opts.tnom,
1699                        opts.reltol,
1700                    );
1701                    notes.extend(t.notes.iter().cloned());
1702                    devices.push(Box::new(crate::diode::DiodeI::new(
1703                        pos,
1704                        neg,
1705                        pos_prime,
1706                        t,
1707                        s.area,
1708                        s.off,
1709                        s.ic.unwrap_or(0.0),
1710                        base,
1711                        crate::diode::DiodeSensBase {
1712                            name: s.name.clone(),
1713                            model: s.model.clone(),
1714                            temp,
1715                            m: 1.0,
1716                            default_tnom: opts.tnom,
1717                            reltol: opts.reltol,
1718                        },
1719                    )));
1720                }
1721                Spec::Jfet(s) => {
1722                    let (nodes, primes) =
1723                        fet3_vars(&node_ids, &s.name, &s.nodes, s.model.rd, s.model.rs);
1724                    let temp = self.instance_temp(&s.name, s.temp, opts);
1725                    let t = crate::jfet::JfetTemp::new(&s.name, &s.model, temp, opts.tnom);
1726                    notes.extend(t.notes.iter().cloned());
1727                    devices.push(Box::new(crate::jfet::JfetI::new(
1728                        nodes, primes, t, s.area, s.off, s.ic, base,
1729                    )));
1730                }
1731                Spec::Mes(s) => {
1732                    let (nodes, primes) =
1733                        fet3_vars(&node_ids, &s.name, &s.nodes, s.model.rd, s.model.rs);
1734                    let t = crate::mes::MesTemp::new(&s.model);
1735                    devices.push(Box::new(crate::mes::MesI::new(
1736                        nodes, primes, t, s.area, s.off, s.ic, base,
1737                    )));
1738                }
1739                Spec::Ltra(s) => {
1740                    let n = [
1741                        get(&node_ids, &s.nodes[0]),
1742                        get(&node_ids, &s.nodes[1]),
1743                        get(&node_ids, &s.nodes[2]),
1744                        get(&node_ids, &s.nodes[3]),
1745                    ];
1746                    let br = [
1747                        get(&node_ids, &format!("{}#i1", s.name)),
1748                        get(&node_ids, &format!("{}#i2", s.name)),
1749                    ];
1750                    let mut p =
1751                        crate::ltra::LtraParams::new(&s.name, &s.model, opts.reltol, opts.abstol)
1752                            .map_err(Error::Build)?;
1753                    if opts.trytocompact {
1754                        p.try_to_compact();
1755                    }
1756                    notes.extend(p.notes.iter().cloned());
1757                    devices.push(Box::new(
1758                        crate::ltra::LtraI::new(p, n, br).with_fit(opts.ltra_fit),
1759                    ));
1760                }
1761                Spec::Tra(s) => {
1762                    let n = [
1763                        get(&node_ids, &s.nodes[0]),
1764                        get(&node_ids, &s.nodes[1]),
1765                        get(&node_ids, &s.nodes[2]),
1766                        get(&node_ids, &s.nodes[3]),
1767                    ];
1768                    let v = |suffix: &str| get(&node_ids, &format!("{}#{suffix}", s.name));
1769                    let p = crate::tra::TraParams::new(&s.name, s.z0, s.td, s.f, s.nl, s.ic)
1770                        .map_err(Error::Build)?;
1771                    devices.push(Box::new(crate::tra::TraI::new(
1772                        p,
1773                        n,
1774                        [v("i1"), v("i2")],
1775                        [v("int1"), v("int2")],
1776                    )));
1777                }
1778                Spec::SwitchV(s) => {
1779                    let m = &s.model;
1780                    let p = crate::switch::SwitchParams::new(m.vt, m.vh, m.ron, m.roff, opts.gmin);
1781                    let ctrl = crate::switch::Control::Voltage {
1782                        pos: get(&node_ids, &s.nodes[2]),
1783                        neg: get(&node_ids, &s.nodes[3]),
1784                    };
1785                    devices.push(Box::new(crate::switch::SwitchInst::new(
1786                        p,
1787                        get(&node_ids, &s.nodes[0]),
1788                        get(&node_ids, &s.nodes[1]),
1789                        ctrl,
1790                        s.on,
1791                        base,
1792                    )));
1793                }
1794                Spec::SwitchI(s) => {
1795                    // `CSWsetup`: "unknown controlling source".
1796                    let branch = ctrl_branch(&branches, &s.name, &s.ctrl)?;
1797                    let m = &s.model;
1798                    let p = crate::switch::SwitchParams::new(m.it, m.ih, m.ron, m.roff, opts.gmin);
1799                    devices.push(Box::new(crate::switch::SwitchInst::new(
1800                        p,
1801                        get(&node_ids, &s.nodes[0]),
1802                        get(&node_ids, &s.nodes[1]),
1803                        crate::switch::Control::Current { branch },
1804                        s.on,
1805                        base,
1806                    )));
1807                }
1808            }
1809        }
1810
1811        // `*param.c`'s `*icXGiven` bits for `DEVsetic`.
1812        for (spec, dev) in self.specs.iter().zip(devices.iter_mut()) {
1813            if let Some(&g) = self.ic_given.get(spec.name()) {
1814                dev.set_ic_given(g);
1815            }
1816        }
1817
1818        // Nodesets and ics must name existing nodes.
1819        let mut nodesets = Vec::new();
1820        for (name, v) in &self.nodesets {
1821            match node_ids.get(name) {
1822                Some(&var) if var != 0 => nodesets.push((var, *v)),
1823                Some(_) => notes.push(".nodeset on ground ignored".to_string()),
1824                None => notes.push(format!(".nodeset names unknown node `{name}`")),
1825            }
1826        }
1827        let mut ics = Vec::new();
1828        for (name, v) in &self.ics {
1829            match node_ids.get(name) {
1830                Some(&var) if var != 0 => ics.push((var, *v)),
1831                Some(_) => notes.push(".ic on ground ignored".to_string()),
1832                None => notes.push(format!(".ic names unknown node `{name}`")),
1833            }
1834        }
1835
1836        // The mutual inductor adds no matrix row of its own but
1837        // keeps `devices` and `names` aligned by construction.
1838        let ports = check_ports(ports)?;
1839        Ok(Built {
1840            vars,
1841            devices,
1842            names,
1843            nodesets,
1844            ics,
1845            sources,
1846            ports,
1847            num_states,
1848            state_ranges,
1849            notes,
1850            warm: self.warm.share(),
1851        })
1852    }
1853}
1854
1855/// `VSRCtemp`'s port checks (ngspice-47, RFSPICE): with N ports,
1856/// every index must lie in `1..=N` and none may repeat, so the ports
1857/// are exactly `1..=N`. The C walks the instances newest first (the
1858/// instance list is built by prepending), so the reported instance
1859/// is the last offending card. Returns the ports sorted by index
1860/// (`CKTrfPorts`).
1861fn check_ports(mut ports: Vec<PortInfo>) -> Result<Vec<PortInfo>, Error> {
1862    let count = ports.len();
1863    let mut seen = vec![false; count + 1];
1864    for p in ports.iter().rev() {
1865        let n = p.num as usize;
1866        if n > count {
1867            return Err(Error::Build(format!("{}: incorrect port ordering", p.name)));
1868        }
1869        if std::mem::replace(&mut seen[n], true) {
1870            return Err(Error::Build(format!("{}: duplicate port Index", p.name)));
1871        }
1872    }
1873    ports.sort_by_key(|p| p.num);
1874    Ok(ports)
1875}
1876
1877/// Terminal and prime-node variables of a JFET/MESFET.
1878fn fet3_vars(
1879    node_ids: &HashMap<String, Var>,
1880    name: &str,
1881    nodes: &[String; 3],
1882    rd: f64,
1883    rs: f64,
1884) -> ([Var; 3], [Var; 2]) {
1885    let d = node_ids[&nodes[0]];
1886    let g = node_ids[&nodes[1]];
1887    let s = node_ids[&nodes[2]];
1888    let dp = if rd != 0.0 {
1889        node_ids[&format!("{name}#drain")]
1890    } else {
1891        d
1892    };
1893    let sp = if rs != 0.0 {
1894        node_ids[&format!("{name}#source")]
1895    } else {
1896        s
1897    };
1898    ([d, g, s], [dp, sp])
1899}
1900
1901fn ctrl_branch(branches: &HashMap<String, Var>, name: &str, ctrl: &str) -> Result<Var, Error> {
1902    if !ctrl.starts_with('v') {
1903        return Err(Error::Build(format!(
1904            "`{name}`: controlling source `{ctrl}` must be a voltage source"
1905        )));
1906    }
1907    branches
1908        .get(ctrl)
1909        .copied()
1910        .ok_or_else(|| Error::Build(format!("`{name}`: controlling source `{ctrl}` not found")))
1911}