Skip to main content

dune_core/
elaborate.rs

1//! IR → engine elaboration: `Circuit::from_netlist` (D-0008). The
2//! netlist crate guarantees shape (nodes present, numbers parsed);
3//! this pass resolves models, applies defaults and rejects what the
4//! engine cannot simulate yet.
5
6use std::collections::HashMap;
7
8use dune_netlist::ir::{self, Netlist, ParamValue};
9
10use crate::circuit::{
11    Bjt, Bsim1, Bsim2, Bsim3, Capacitor, Cccs, Ccvs, Circuit, Diode, Inductor, Isource, Jfet, Ltra,
12    Mes, Mos1, Mos2, Mos3, Mos6, Mutual, Resistor, SwitchI, SwitchV, Tra, Vccs, Vcvs, Vsource,
13};
14use crate::consts::CTOK;
15use crate::devices::IcGiven;
16use crate::diode::DiodeModel;
17use crate::error::Error;
18
19impl Circuit {
20    /// Build a circuit from a parsed (and already subcircuit-
21    /// expanded) netlist. `notes` collects remarks worth printing;
22    /// anything the engine cannot represent faithfully is an error.
23    pub fn from_netlist(nl: &Netlist) -> Result<(Circuit, Vec<String>), Error> {
24        let mut problems: Vec<String> = Vec::new();
25        let mut notes: Vec<String> = Vec::new();
26
27        // Diode, BJT and MOS models.
28        let mut dmodels: HashMap<&str, DiodeModel> = HashMap::new();
29        let mut qmodels: HashMap<&str, crate::bjt::BjtModel> = HashMap::new();
30        enum MosModel {
31            M1(crate::mos1::Mos1Model),
32            M2(crate::mos2::Mos2Model),
33            M3(crate::mos3::Mos3Model),
34            M6(crate::mos6::Mos6Model),
35            B1(Box<crate::bsim1::Bsim1Model>),
36            B2(Box<crate::bsim2::Bsim2Model>),
37            B3(Box<crate::bsim3::Bsim3Model>),
38        }
39        let mut mmodels: HashMap<&str, MosModel> = HashMap::new();
40        // Unsupported-level MOS models only become an error when an
41        // element uses them: decks routinely carry alternative model
42        // cards for levels they never instantiate (mos6inv.cir).
43        let mut unsupported_models: HashMap<&str, String> = HashMap::new();
44        let mut omodels: HashMap<&str, crate::ltra::LtraModel> = HashMap::new();
45        let mut umodels: HashMap<&str, UrcModel> = HashMap::new();
46        let mut jmodels: HashMap<&str, crate::jfet::JfetModel> = HashMap::new();
47        let mut zmodels: HashMap<&str, crate::mes::MesModel> = HashMap::new();
48        let mut smodels: HashMap<&str, crate::switch::SwModel> = HashMap::new();
49        let mut wmodels: HashMap<&str, crate::switch::CswModel> = HashMap::new();
50        let mut rmodels: HashMap<&str, ResModel> = HashMap::new();
51        let mut cmodels: HashMap<&str, CapModel> = HashMap::new();
52        for m in &nl.models {
53            match m.mtype.as_str() {
54                "r" => match res_model(m, &mut notes) {
55                    Ok(rm) => {
56                        rmodels.insert(m.name.as_str(), rm);
57                    }
58                    Err(e) => problems.push(format!("model `{}`: {e}", m.name)),
59                },
60                "c" => match cap_model(m, &mut notes) {
61                    Ok(cm) => {
62                        cmodels.insert(m.name.as_str(), cm);
63                    }
64                    Err(e) => problems.push(format!("model `{}`: {e}", m.name)),
65                },
66                "sw" => match sw_model(m, &mut notes) {
67                    Ok(sm) => {
68                        smodels.insert(m.name.as_str(), sm);
69                    }
70                    Err(e) => problems.push(format!("model `{}`: {e}", m.name)),
71                },
72                "csw" => match csw_model(m, &mut notes) {
73                    Ok(wm) => {
74                        wmodels.insert(m.name.as_str(), wm);
75                    }
76                    Err(e) => problems.push(format!("model `{}`: {e}", m.name)),
77                },
78                // `LEVEL` on J/Z cards is ngspice's selector (JFET
79                // 2 = Parker-Skellern; MES 2 = MESA, 5/6 = HFET);
80                // only level 1, the 3f5 model, is implemented, and
81                // like the MOS levels an unsupported card only errs
82                // when an element instantiates it.
83                "njf" | "pjf" => {
84                    let level = num_param(&m.params, "level").unwrap_or(1.0);
85                    if level != 1.0 {
86                        unsupported_models.insert(
87                            m.name.as_str(),
88                            format!("model `{}`: JFET level {level} is not implemented", m.name),
89                        );
90                    } else {
91                        match jfet_model(m, &mut notes) {
92                            Ok(jm) => {
93                                jmodels.insert(m.name.as_str(), jm);
94                            }
95                            Err(e) => problems.push(format!("model `{}`: {e}", m.name)),
96                        }
97                    }
98                }
99                "nmf" | "pmf" => {
100                    let level = num_param(&m.params, "level").unwrap_or(1.0);
101                    if level != 1.0 {
102                        unsupported_models.insert(
103                            m.name.as_str(),
104                            format!(
105                                "model `{}`: MESFET level {level} is not implemented",
106                                m.name
107                            ),
108                        );
109                    } else {
110                        match mes_model(m, &mut notes) {
111                            Ok(zm) => {
112                                zmodels.insert(m.name.as_str(), zm);
113                            }
114                            Err(e) => problems.push(format!("model `{}`: {e}", m.name)),
115                        }
116                    }
117                }
118                "urc" => match urc_model(m, &mut notes) {
119                    Ok(um) => {
120                        umodels.insert(m.name.as_str(), um);
121                    }
122                    Err(e) => problems.push(format!("model `{}`: {e}", m.name)),
123                },
124                "ltra" => match ltra_model(m, &mut notes) {
125                    Ok(lm) => {
126                        omodels.insert(m.name.as_str(), lm);
127                    }
128                    Err(e) => problems.push(format!("model `{}`: {e}", m.name)),
129                },
130                "d" => match diode_model(m, &mut notes) {
131                    Ok(dm) => {
132                        dmodels.insert(m.name.as_str(), dm);
133                    }
134                    Err(e) => problems.push(format!("model `{}`: {e}", m.name)),
135                },
136                "npn" | "pnp" => match bjt_model(m, &mut notes) {
137                    Ok(bm) => {
138                        qmodels.insert(m.name.as_str(), bm);
139                    }
140                    Err(e) => problems.push(format!("model `{}`: {e}", m.name)),
141                },
142                "nmos" | "pmos" => {
143                    // `LEVEL` picks the MOS model; 1 is the default
144                    // (`MOSdSetup` dispatch in `inpdomod.c`).
145                    let level = num_param(&m.params, "level").unwrap_or(1.0);
146                    if level == 1.0 {
147                        match mos1_model(m, &mut notes) {
148                            Ok(mm) => {
149                                mmodels.insert(m.name.as_str(), MosModel::M1(mm));
150                            }
151                            Err(e) => problems.push(format!("model `{}`: {e}", m.name)),
152                        }
153                    } else if level == 2.0 {
154                        match mos2_model(m, &mut notes) {
155                            Ok(mm) => {
156                                mmodels.insert(m.name.as_str(), MosModel::M2(mm));
157                            }
158                            Err(e) => problems.push(format!("model `{}`: {e}", m.name)),
159                        }
160                    } else if level == 3.0 {
161                        match mos3_model(m, &mut notes) {
162                            Ok(mm) => {
163                                mmodels.insert(m.name.as_str(), MosModel::M3(mm));
164                            }
165                            Err(e) => problems.push(format!("model `{}`: {e}", m.name)),
166                        }
167                    } else if level == 4.0 {
168                        match bsim1_model(m, &mut notes) {
169                            Ok(mm) => {
170                                mmodels.insert(m.name.as_str(), MosModel::B1(Box::new(mm)));
171                            }
172                            Err(e) => problems.push(format!("model `{}`: {e}", m.name)),
173                        }
174                    } else if level == 5.0 {
175                        match bsim2_model(m, &mut notes) {
176                            Ok(mm) => {
177                                mmodels.insert(m.name.as_str(), MosModel::B2(Box::new(mm)));
178                            }
179                            Err(e) => problems.push(format!("model `{}`: {e}", m.name)),
180                        }
181                    } else if level == 8.0 || level == 49.0 {
182                        // ngspice's `inpdomod.c`: `VERSION` picks the
183                        // BSIM3 code base; 3.3 (the default) is the
184                        // one implemented.
185                        match bsim3_model(m, &mut notes) {
186                            Ok(Ok(mm)) => {
187                                mmodels.insert(m.name.as_str(), MosModel::B3(Box::new(mm)));
188                            }
189                            Ok(Err(reason)) => {
190                                unsupported_models.insert(
191                                    m.name.as_str(),
192                                    format!("model `{}`: {reason}", m.name),
193                                );
194                            }
195                            Err(e) => problems.push(format!("model `{}`: {e}", m.name)),
196                        }
197                    } else if level == 6.0 {
198                        match mos6_model(m, &mut notes) {
199                            Ok(mm) => {
200                                mmodels.insert(m.name.as_str(), MosModel::M6(mm));
201                            }
202                            Err(e) => problems.push(format!("model `{}`: {e}", m.name)),
203                        }
204                    } else {
205                        unsupported_models.insert(
206                            m.name.as_str(),
207                            format!(
208                                "model `{}`: MOS level {level} is not implemented yet (Phase 3)",
209                                m.name
210                            ),
211                        );
212                    }
213                }
214                _ => {}
215            }
216        }
217
218        let mut ckt = Circuit::new(nl.title.clone());
219
220        for el in &nl.elements {
221            let name = el.name.clone();
222            match &el.device {
223                ir::Device::Resistor {
224                    value,
225                    model,
226                    params,
227                } => {
228                    let rm = match model {
229                        Some(m) => match rmodels.get(m.as_str()) {
230                            Some(rm) => Some(rm),
231                            None => {
232                                problems.push(format!("`{name}`: unknown resistor model `{m}`"));
233                                continue;
234                            }
235                        },
236                        None => None,
237                    };
238                    match res_instance(&name, *value, rm, params, &mut notes) {
239                        Ok((r, temp)) => {
240                            if let Some(t) = temp {
241                                ckt.set_res_temp(&name, t);
242                            }
243                            add(
244                                &mut ckt,
245                                Resistor {
246                                    name,
247                                    nodes: two(&el.nodes),
248                                    value: r,
249                                },
250                                &mut problems,
251                            );
252                        }
253                        Err(e) => problems.push(e),
254                    }
255                }
256                ir::Device::Capacitor {
257                    value,
258                    model,
259                    params,
260                } => {
261                    let cm = match model {
262                        Some(m) => match cmodels.get(m.as_str()) {
263                            Some(cm) => Some(cm),
264                            None => {
265                                problems.push(format!("`{name}`: unknown capacitor model `{m}`"));
266                                continue;
267                            }
268                        },
269                        None => None,
270                    };
271                    let ic = num_param(params, "ic");
272                    match cap_instance(&name, *value, cm, params, &mut notes) {
273                        Ok(c) => add(
274                            &mut ckt,
275                            Capacitor {
276                                name,
277                                nodes: two(&el.nodes),
278                                value: c,
279                                ic,
280                            },
281                            &mut problems,
282                        ),
283                        Err(e) => problems.push(e),
284                    }
285                }
286                ir::Device::Inductor { value, params } => {
287                    let ic = num_param(params, "ic");
288                    match value {
289                        Some(v) => add(
290                            &mut ckt,
291                            Inductor {
292                                name,
293                                nodes: two(&el.nodes),
294                                value: *v,
295                                ic,
296                            },
297                            &mut problems,
298                        ),
299                        None => problems.push(format!("`{name}`: inductor has no value")),
300                    }
301                }
302                ir::Device::Mutual { l1, l2, k } => add(
303                    &mut ckt,
304                    Mutual {
305                        name,
306                        l1: l1.clone(),
307                        l2: l2.clone(),
308                        k: *k,
309                    },
310                    &mut problems,
311                ),
312                ir::Device::Source {
313                    kind,
314                    dc,
315                    tran,
316                    ac,
317                    distof1,
318                    distof2,
319                    port,
320                } => {
321                    let ac = ac.as_ref().map(|a| (a.mag, a.phase));
322                    let distof1 = distof1.as_ref().map(|d| (d.mag, d.phase));
323                    let distof2 = distof2.as_ref().map(|d| (d.mag, d.phase));
324                    let wave = tran.as_ref().map(|t| {
325                        let args = t.args.clone();
326                        match t.kind {
327                            ir::TranFnKind::Pulse => crate::srcfn::Wave::Pulse(args),
328                            ir::TranFnKind::Sin => crate::srcfn::Wave::Sin(args),
329                            ir::TranFnKind::Exp => crate::srcfn::Wave::Exp(args),
330                            ir::TranFnKind::Pwl => crate::srcfn::Wave::Pwl(args),
331                            ir::TranFnKind::Sffm => crate::srcfn::Wave::Sffm(args),
332                            ir::TranFnKind::TrNoise => crate::srcfn::Wave::TrNoise(args),
333                            ir::TranFnKind::TrRandom => crate::srcfn::Wave::TrRandom(args),
334                        }
335                    });
336                    match kind {
337                        ir::SourceKind::Voltage => add(
338                            &mut ckt,
339                            Vsource {
340                                name,
341                                nodes: two(&el.nodes),
342                                dc: *dc,
343                                wave,
344                                ac,
345                                distof1,
346                                distof2,
347                                port: port.map(|p| crate::circuit::RfPort {
348                                    num: p.num,
349                                    z0: p.z0,
350                                }),
351                            },
352                            &mut problems,
353                        ),
354                        ir::SourceKind::Current => add(
355                            &mut ckt,
356                            Isource {
357                                name,
358                                nodes: two(&el.nodes),
359                                dc: *dc,
360                                wave,
361                                ac,
362                                distof1,
363                                distof2,
364                            },
365                            &mut problems,
366                        ),
367                    }
368                }
369                ir::Device::Vcvs { gain } => add(
370                    &mut ckt,
371                    Vcvs {
372                        name,
373                        nodes: four(&el.nodes),
374                        gain: gain.unwrap_or(0.0),
375                    },
376                    &mut problems,
377                ),
378                ir::Device::Vccs { gain } => add(
379                    &mut ckt,
380                    Vccs {
381                        name,
382                        nodes: four(&el.nodes),
383                        gain: gain.unwrap_or(0.0),
384                    },
385                    &mut problems,
386                ),
387                ir::Device::Cccs { vname, gain } => add(
388                    &mut ckt,
389                    Cccs {
390                        name,
391                        nodes: two(&el.nodes),
392                        ctrl: vname.clone(),
393                        gain: gain.unwrap_or(0.0),
394                    },
395                    &mut problems,
396                ),
397                ir::Device::Ccvs { vname, gain } => add(
398                    &mut ckt,
399                    Ccvs {
400                        name,
401                        nodes: two(&el.nodes),
402                        ctrl: vname.clone(),
403                        gain: gain.unwrap_or(0.0),
404                    },
405                    &mut problems,
406                ),
407                ir::Device::Diode {
408                    model,
409                    area,
410                    off,
411                    params,
412                } => {
413                    let Some(dm) = dmodels.get(model.as_str()) else {
414                        problems.push(format!(
415                            "`{name}`: unable to find definition of model `{model}`"
416                        ));
417                        continue;
418                    };
419                    let mut temp = None;
420                    let mut ic = None;
421                    for p in params {
422                        match (p.name.as_str(), &p.value) {
423                            ("temp", ParamValue::Num(v)) => temp = Some(v + CTOK),
424                            // ngspice-47 `dio.c` (KNOWN_DIFFS).
425                            ("dtemp", ParamValue::Num(v)) => ckt.set_dtemp(&name, *v),
426                            ("ic", ParamValue::Num(v)) => ic = Some(*v),
427                            _ => problems.push(format!("`{name}`: unknown parameter ({})", p.name)),
428                        }
429                    }
430                    add(
431                        &mut ckt,
432                        Diode {
433                            name,
434                            nodes: two(&el.nodes),
435                            model: dm.clone(),
436                            area: area.unwrap_or(1.0),
437                            off: *off,
438                            temp,
439                            ic,
440                        },
441                        &mut problems,
442                    );
443                }
444                ir::Device::Bjt {
445                    model,
446                    area,
447                    off,
448                    params,
449                } => {
450                    let Some(bm) = qmodels.get(model.as_str()) else {
451                        problems.push(format!(
452                            "`{name}`: unable to find definition of model `{model}`"
453                        ));
454                        continue;
455                    };
456                    let mut temp = None;
457                    let mut ic = (0.0, 0.0);
458                    let mut icg = IcGiven::NONE;
459                    for p in params {
460                        match (p.name.as_str(), &p.value) {
461                            ("temp", ParamValue::Num(v)) => temp = Some(v + CTOK),
462                            // ngspice-47 `bjt.c` (KNOWN_DIFFS).
463                            ("dtemp", ParamValue::Num(v)) => ckt.set_dtemp(&name, *v),
464                            ("ic", ParamValue::Num(v)) => {
465                                ic = (*v, 0.0);
466                                icg = IcGiven::first(1);
467                            }
468                            ("ic", ParamValue::NumList(v)) => {
469                                ic = (
470                                    v.first().copied().unwrap_or(0.0),
471                                    v.get(1).copied().unwrap_or(0.0),
472                                );
473                                icg = IcGiven::first(v.len().min(2));
474                            }
475                            ("area", ParamValue::Num(_)) => {}
476                            _ => problems.push(format!("`{name}`: unknown parameter ({})", p.name)),
477                        }
478                    }
479                    let mut nodes4 = [
480                        el.nodes[0].clone(),
481                        el.nodes[1].clone(),
482                        el.nodes[2].clone(),
483                        "0".to_string(),
484                    ];
485                    if let Some(s) = el.nodes.get(3) {
486                        nodes4[3] = s.clone();
487                    }
488                    add_ic(
489                        &mut ckt,
490                        Bjt {
491                            name,
492                            nodes: nodes4,
493                            model: bm.clone(),
494                            area: area.unwrap_or(1.0),
495                            off: *off,
496                            ic,
497                            temp,
498                        },
499                        icg,
500                        &mut problems,
501                    );
502                }
503                ir::Device::Mosfet { model, params } => {
504                    let Some(mm) = mmodels.get(model.as_str()) else {
505                        if let Some(reason) = unsupported_models.get(model.as_str()) {
506                            problems.push(format!("`{name}`: {reason}"));
507                        } else {
508                            problems.push(format!(
509                                "`{name}`: unable to find definition of model `{model}`"
510                            ));
511                        }
512                        continue;
513                    };
514                    let mut inst = MosInst::new();
515                    let is_b3 = matches!(mm, MosModel::B3(_));
516                    for p in params {
517                        if is_b3 {
518                            // The BSIM3 instance table (`b3.c`) adds
519                            // these and has no `TEMP`.
520                            match (p.name.as_str(), &p.value) {
521                                ("m", ParamValue::Num(v)) => {
522                                    inst.m = *v;
523                                    continue;
524                                }
525                                ("delvto", ParamValue::Num(v)) => {
526                                    inst.delvto = *v;
527                                    continue;
528                                }
529                                ("mulu0", ParamValue::Num(v)) => {
530                                    inst.mulu0 = *v;
531                                    continue;
532                                }
533                                ("icvds", ParamValue::Num(v)) => {
534                                    inst.ic.0 = *v;
535                                    continue;
536                                }
537                                ("icvgs", ParamValue::Num(v)) => {
538                                    inst.ic.1 = *v;
539                                    continue;
540                                }
541                                ("icvbs", ParamValue::Num(v)) => {
542                                    inst.ic.2 = *v;
543                                    continue;
544                                }
545                                ("nqsmod" | "acnqsmod", ParamValue::Num(v)) => {
546                                    if *v != 0.0 {
547                                        problems.push(format!(
548                                            "`{name}`: BSIM3 {} = {v} (the non-quasi-static model) is not implemented",
549                                            p.name
550                                        ));
551                                    }
552                                    continue;
553                                }
554                                // `GEO` only matters with ACM > 0.
555                                ("geo", ParamValue::Num(_)) => continue,
556                                ("temp" | "dtemp", _) => {
557                                    problems
558                                        .push(format!("`{name}`: unknown parameter ({})", p.name));
559                                    continue;
560                                }
561                                _ => {}
562                            }
563                        }
564                        match (p.name.as_str(), &p.value) {
565                            ("l", ParamValue::Num(v)) => inst.l = Some(*v),
566                            ("w", ParamValue::Num(v)) => inst.w = Some(*v),
567                            ("ad", ParamValue::Num(v)) => inst.ad = Some(*v),
568                            ("as", ParamValue::Num(v)) => inst.as_ = Some(*v),
569                            ("pd", ParamValue::Num(v)) => inst.pd = *v,
570                            ("ps", ParamValue::Num(v)) => inst.ps = *v,
571                            ("nrd", ParamValue::Num(v)) => inst.nrd = *v,
572                            ("nrs", ParamValue::Num(v)) => inst.nrs = *v,
573                            ("off", ParamValue::Flag) => inst.off = true,
574                            ("temp", ParamValue::Num(v)) => inst.temp = Some(v + CTOK),
575                            // ngspice-47 `mos{1,2,3,6}.c` (KNOWN_DIFFS).
576                            ("dtemp", ParamValue::Num(v)) => inst.dtemp = Some(*v),
577                            ("ic", ParamValue::Num(v)) => {
578                                inst.ic = (*v, 0.0, 0.0);
579                                inst.ic_given = IcGiven::first(1);
580                            }
581                            ("ic", ParamValue::NumList(v)) => {
582                                inst.ic = (
583                                    v.first().copied().unwrap_or(0.0),
584                                    v.get(1).copied().unwrap_or(0.0),
585                                    v.get(2).copied().unwrap_or(0.0),
586                                );
587                                inst.ic_given = IcGiven::first(v.len().min(3));
588                            }
589                            _ => problems.push(format!("`{name}`: unknown parameter ({})", p.name)),
590                        }
591                    }
592                    if matches!(mm, MosModel::B1(_) | MosModel::B2(_)) {
593                        // `b1.c`/`b2.c` have `temp` only as a model
594                        // parameter; neither table has `dtemp`.
595                        for (given, kw) in [
596                            (inst.temp.is_some(), "temp"),
597                            (inst.dtemp.is_some(), "dtemp"),
598                        ] {
599                            if given {
600                                problems.push(format!("`{name}`: unknown parameter ({kw})"));
601                            }
602                        }
603                    } else if let Some(d) = inst.dtemp {
604                        ckt.set_dtemp(&name, d);
605                    }
606                    let nodes = four(&el.nodes);
607                    match mm {
608                        MosModel::M1(m1) => add_ic(
609                            &mut ckt,
610                            Mos1 {
611                                name,
612                                nodes,
613                                model: m1.clone(),
614                                l: inst.l,
615                                w: inst.w,
616                                ad: inst.ad,
617                                as_: inst.as_,
618                                pd: inst.pd,
619                                ps: inst.ps,
620                                nrd: inst.nrd,
621                                nrs: inst.nrs,
622                                off: inst.off,
623                                ic: inst.ic,
624                                temp: inst.temp,
625                            },
626                            inst.ic_given,
627                            &mut problems,
628                        ),
629                        MosModel::M2(m2) => add_ic(
630                            &mut ckt,
631                            Mos2 {
632                                name,
633                                nodes,
634                                model: m2.clone(),
635                                l: inst.l,
636                                w: inst.w,
637                                ad: inst.ad,
638                                as_: inst.as_,
639                                pd: inst.pd,
640                                ps: inst.ps,
641                                nrd: inst.nrd,
642                                nrs: inst.nrs,
643                                off: inst.off,
644                                ic: inst.ic,
645                                temp: inst.temp,
646                            },
647                            inst.ic_given,
648                            &mut problems,
649                        ),
650                        MosModel::M3(m3) => add_ic(
651                            &mut ckt,
652                            Mos3 {
653                                name,
654                                nodes,
655                                model: m3.clone(),
656                                l: inst.l,
657                                w: inst.w,
658                                ad: inst.ad,
659                                as_: inst.as_,
660                                pd: inst.pd,
661                                ps: inst.ps,
662                                nrd: inst.nrd,
663                                nrs: inst.nrs,
664                                off: inst.off,
665                                ic: inst.ic,
666                                temp: inst.temp,
667                            },
668                            inst.ic_given,
669                            &mut problems,
670                        ),
671                        MosModel::M6(m6) => add_ic(
672                            &mut ckt,
673                            Mos6 {
674                                name,
675                                nodes,
676                                model: m6.clone(),
677                                l: inst.l,
678                                w: inst.w,
679                                ad: inst.ad,
680                                as_: inst.as_,
681                                pd: inst.pd,
682                                ps: inst.ps,
683                                nrd: inst.nrd,
684                                nrs: inst.nrs,
685                                off: inst.off,
686                                ic: inst.ic,
687                                temp: inst.temp,
688                            },
689                            inst.ic_given,
690                            &mut problems,
691                        ),
692                        MosModel::B1(b1) => add_ic(
693                            &mut ckt,
694                            Bsim1 {
695                                name,
696                                nodes,
697                                model: (**b1).clone(),
698                                l: inst.l,
699                                w: inst.w,
700                                ad: inst.ad,
701                                as_: inst.as_,
702                                pd: inst.pd,
703                                ps: inst.ps,
704                                nrd: inst.nrd,
705                                nrs: inst.nrs,
706                                off: inst.off,
707                                ic: inst.ic,
708                            },
709                            inst.ic_given,
710                            &mut problems,
711                        ),
712                        MosModel::B3(b3) => add_ic(
713                            &mut ckt,
714                            Bsim3 {
715                                name,
716                                nodes,
717                                model: (**b3).clone(),
718                                l: inst.l,
719                                w: inst.w,
720                                ad: inst.ad.unwrap_or(0.0),
721                                as_: inst.as_.unwrap_or(0.0),
722                                pd: inst.pd,
723                                ps: inst.ps,
724                                nrd: inst.nrd,
725                                nrs: inst.nrs,
726                                m: inst.m,
727                                delvto: inst.delvto,
728                                mulu0: inst.mulu0,
729                                off: inst.off,
730                                ic: inst.ic,
731                            },
732                            inst.ic_given,
733                            &mut problems,
734                        ),
735                        MosModel::B2(b2) => add_ic(
736                            &mut ckt,
737                            Bsim2 {
738                                name,
739                                nodes,
740                                model: (**b2).clone(),
741                                l: inst.l,
742                                w: inst.w,
743                                ad: inst.ad,
744                                as_: inst.as_,
745                                pd: inst.pd,
746                                ps: inst.ps,
747                                nrd: inst.nrd,
748                                nrs: inst.nrs,
749                                off: inst.off,
750                                ic: inst.ic,
751                            },
752                            inst.ic_given,
753                            &mut problems,
754                        ),
755                    }
756                }
757                ir::Device::Jfet {
758                    model,
759                    area,
760                    off,
761                    params,
762                } => {
763                    let Some(jm) = jmodels.get(model.as_str()) else {
764                        if let Some(reason) = unsupported_models.get(model.as_str()) {
765                            problems.push(format!("`{name}`: {reason}"));
766                        } else {
767                            problems.push(format!(
768                                "`{name}`: unable to find definition of model `{model}`"
769                            ));
770                        }
771                        continue;
772                    };
773                    let mut area = *area;
774                    let mut temp = None;
775                    let mut ic = (0.0, 0.0);
776                    let mut icg = IcGiven::NONE;
777                    for p in params {
778                        match (p.name.as_str(), &p.value) {
779                            ("temp", ParamValue::Num(v)) => temp = Some(v + CTOK),
780                            // ngspice-47 `jfet.c` (KNOWN_DIFFS).
781                            ("dtemp", ParamValue::Num(v)) => ckt.set_dtemp(&name, *v),
782                            ("area", ParamValue::Num(v)) => area = Some(*v),
783                            ("ic", ParamValue::Num(v)) => {
784                                ic.0 = *v;
785                                icg = icg.with(0);
786                            }
787                            ("ic", ParamValue::NumList(v)) if (1..=2).contains(&v.len()) => {
788                                ic = (v[0], v.get(1).copied().unwrap_or(ic.1));
789                                icg = IcGiven(icg.0 | IcGiven::first(v.len()).0);
790                            }
791                            _ => problems.push(format!("`{name}`: unknown parameter ({})", p.name)),
792                        }
793                    }
794                    add_ic(
795                        &mut ckt,
796                        Jfet {
797                            name,
798                            nodes: three(&el.nodes),
799                            model: jm.clone(),
800                            area: area.unwrap_or(1.0),
801                            off: *off,
802                            ic,
803                            temp,
804                        },
805                        icg,
806                        &mut problems,
807                    );
808                }
809                ir::Device::Mesfet {
810                    model,
811                    area,
812                    off,
813                    params,
814                } => {
815                    let Some(zm) = zmodels.get(model.as_str()) else {
816                        if let Some(reason) = unsupported_models.get(model.as_str()) {
817                            problems.push(format!("`{name}`: {reason}"));
818                        } else {
819                            problems.push(format!(
820                                "`{name}`: unable to find definition of model `{model}`"
821                            ));
822                        }
823                        continue;
824                    };
825                    let mut area = *area;
826                    let mut ic = (0.0, 0.0);
827                    let mut icg = IcGiven::NONE;
828                    for p in params {
829                        match (p.name.as_str(), &p.value) {
830                            ("area", ParamValue::Num(v)) => area = Some(*v),
831                            ("icvds", ParamValue::Num(v)) => {
832                                ic.0 = *v;
833                                icg = icg.with(0);
834                            }
835                            ("icvgs", ParamValue::Num(v)) => {
836                                ic.1 = *v;
837                                icg = icg.with(1);
838                            }
839                            ("ic", ParamValue::Num(v)) => {
840                                ic.0 = *v;
841                                icg = icg.with(0);
842                            }
843                            ("ic", ParamValue::NumList(v)) if (1..=2).contains(&v.len()) => {
844                                ic = (v[0], v.get(1).copied().unwrap_or(ic.1));
845                                icg = IcGiven(icg.0 | IcGiven::first(v.len()).0);
846                            }
847                            _ => problems.push(format!("`{name}`: unknown parameter ({})", p.name)),
848                        }
849                    }
850                    add_ic(
851                        &mut ckt,
852                        Mes {
853                            name,
854                            nodes: three(&el.nodes),
855                            model: zm.clone(),
856                            area: area.unwrap_or(1.0),
857                            off: *off,
858                            ic,
859                        },
860                        icg,
861                        &mut problems,
862                    );
863                }
864                ir::Device::SwitchV { model, state } => {
865                    let Some(sm) = smodels.get(model.as_str()) else {
866                        problems.push(format!(
867                            "`{name}`: unable to find definition of model `{model}`"
868                        ));
869                        continue;
870                    };
871                    add(
872                        &mut ckt,
873                        SwitchV {
874                            name,
875                            nodes: four(&el.nodes),
876                            model: sm.clone(),
877                            on: *state == Some(ir::OnOff::On),
878                        },
879                        &mut problems,
880                    );
881                }
882                ir::Device::SwitchI {
883                    vname,
884                    model,
885                    state,
886                } => {
887                    let Some(wm) = wmodels.get(model.as_str()) else {
888                        problems.push(format!(
889                            "`{name}`: unable to find definition of model `{model}`"
890                        ));
891                        continue;
892                    };
893                    add(
894                        &mut ckt,
895                        SwitchI {
896                            name,
897                            nodes: two(&el.nodes),
898                            ctrl: vname.clone(),
899                            model: wm.clone(),
900                            on: *state == Some(ir::OnOff::On),
901                        },
902                        &mut problems,
903                    );
904                }
905                ir::Device::TlineLossy { model } => {
906                    let Some(lm) = omodels.get(model.as_str()) else {
907                        problems.push(format!(
908                            "`{name}`: unable to find definition of model `{model}`"
909                        ));
910                        continue;
911                    };
912                    add(
913                        &mut ckt,
914                        Ltra {
915                            name,
916                            nodes: four(&el.nodes),
917                            model: lm.clone(),
918                        },
919                        &mut problems,
920                    );
921                }
922                ir::Device::Behavioral { kind, expr } => add(
923                    &mut ckt,
924                    crate::circuit::Asrc {
925                        name,
926                        nodes: two(&el.nodes),
927                        kind: *kind,
928                        expr: expr.clone(),
929                    },
930                    &mut problems,
931                ),
932                ir::Device::Urc { model, params } => {
933                    let Some(um) = umodels.get(model.as_str()) else {
934                        problems.push(format!(
935                            "`{name}`: unable to find definition of model `{model}`"
936                        ));
937                        continue;
938                    };
939                    match urc_expand(&name, &el.nodes, um, params) {
940                        Ok(specs) => {
941                            for spec in specs {
942                                add(&mut ckt, spec, &mut problems);
943                            }
944                        }
945                        Err(e) => problems.push(e),
946                    }
947                }
948                ir::Device::TlineIdeal { params } => match tra_spec(&name, &el.nodes, params) {
949                    Ok(t) => add(&mut ckt, t, &mut problems),
950                    Err(e) => problems.push(e),
951                },
952                other => {
953                    problems.push(format!(
954                        "`{name}`: {} is not implemented yet (Phase {})",
955                        device_kind_name(other),
956                        device_phase(other),
957                    ));
958                }
959            }
960        }
961
962        for ni in &nl.node_inits {
963            match ni.kind {
964                ir::NodeInitKind::Nodeset => ckt.nodeset(ni.node.clone(), ni.value),
965                ir::NodeInitKind::Ic => ckt.ic(ni.node.clone(), ni.value),
966            }
967        }
968
969        if !problems.is_empty() {
970            return Err(Error::Elab(problems.join("\n")));
971        }
972        Ok((ckt, notes))
973    }
974}
975
976/// `T` card parameters (`TRApTable` in `dev/tra/tra.c`): `Z0`/`ZO`,
977/// `TD`, `F`, `NL`, `V1`, `I1`, `V2`, `I2` and the `IC=` vector,
978/// whose short forms fill from the front (`TRAparam`'s fall-through
979/// switch). `REL`/`ABS` are output-only there, so not accepted.
980fn tra_spec(name: &str, nodes: &[String], params: &[ir::Param]) -> Result<Tra, String> {
981    let mut z0 = None;
982    let mut t = Tra {
983        name: name.to_string(),
984        nodes: four(nodes),
985        z0: 0.0,
986        td: None,
987        f: None,
988        nl: None,
989        ic: [0.0; 4],
990    };
991    for p in params {
992        match (p.name.as_str(), &p.value) {
993            ("z0" | "zo", ParamValue::Num(v)) => z0 = Some(*v),
994            ("td", ParamValue::Num(v)) => t.td = Some(*v),
995            ("f", ParamValue::Num(v)) => t.f = Some(*v),
996            ("nl", ParamValue::Num(v)) => t.nl = Some(*v),
997            ("v1", ParamValue::Num(v)) => t.ic[0] = *v,
998            ("i1", ParamValue::Num(v)) => t.ic[1] = *v,
999            ("v2", ParamValue::Num(v)) => t.ic[2] = *v,
1000            ("i2", ParamValue::Num(v)) => t.ic[3] = *v,
1001            ("ic", ParamValue::Num(v)) => t.ic[0] = *v,
1002            ("ic", ParamValue::NumList(v)) if (1..=4).contains(&v.len()) => {
1003                t.ic[..v.len()].copy_from_slice(v);
1004            }
1005            _ => return Err(format!("`{name}`: unknown parameter ({})", p.name)),
1006        }
1007    }
1008    // `TRAsetup`: "transmission line z0 must be given".
1009    t.z0 = z0.ok_or_else(|| format!("`{name}`: transmission line z0 must be given"))?;
1010    Ok(t)
1011}
1012
1013/// `.model x ltra(...)` → [`crate::ltra::LtraModel`]: the ngspice
1014/// input table (`ltra.c`), which is the 3f5's plus `REL`/`ABS` as
1015/// inputs. Flags take no value (a value given anyway is ignored, as
1016/// `LTRAmParam` ignores it); `fullcontrol`, `halfcontrol`, `print`,
1017/// `noprint`, `choprel`, … are `#ifdef notdef` in the table and so
1018/// unrecognized.
1019fn ltra_model(m: &ir::Model, notes: &mut Vec<String>) -> Result<crate::ltra::LtraModel, String> {
1020    use crate::ltra::LtraInterp;
1021    let mut lm = crate::ltra::LtraModel::default();
1022    for p in &m.params {
1023        let num = match p.value {
1024            ParamValue::Num(v) => Some(v),
1025            _ => None,
1026        };
1027        let need = || num.ok_or_else(|| format!("parameter {} needs a numeric value", p.name));
1028        match p.name.as_str() {
1029            "ltra" => {}
1030            "r" => lm.r = Some(need()?),
1031            "l" => lm.l = Some(need()?),
1032            "g" => lm.g = Some(need()?),
1033            "c" => lm.c = Some(need()?),
1034            "len" => lm.len = Some(need()?),
1035            "rel" => lm.rel = Some(need()?),
1036            "abs" => lm.abs = Some(need()?),
1037            "compactrel" => lm.compactrel = need()?,
1038            "compactabs" => lm.compactabs = need()?,
1039            // `nocontrol` is the default (and only reachable) mode.
1040            "nocontrol" => {}
1041            "steplimit" => lm.steplimit = true,
1042            "nosteplimit" => lm.steplimit = false,
1043            "lininterp" => lm.interp = Some(LtraInterp::Linear),
1044            "quadinterp" => lm.interp = Some(LtraInterp::Quadratic),
1045            "mixedinterp" => lm.interp = Some(LtraInterp::Mixed),
1046            "truncnr" => lm.truncnr = true,
1047            "truncdontcut" => lm.truncdontcut = true,
1048            other => notes.push(format!(
1049                "model `{}`: unrecognized parameter ({other}) - ignored",
1050                m.name
1051            )),
1052        }
1053    }
1054    Ok(lm)
1055}
1056
1057/// `.model x npn/pnp(...)` → [`crate::bjt::BjtModel`], 3f5 names
1058/// with the documented aliases (`bjt.c`).
1059fn bjt_model(m: &ir::Model, notes: &mut Vec<String>) -> Result<crate::bjt::BjtModel, String> {
1060    let mut bm = crate::bjt::BjtModel::new(m.mtype == "npn");
1061    for p in &m.params {
1062        let v = match p.value {
1063            ParamValue::Num(v) => v,
1064            // The npn/pnp type may also appear as a flag parameter.
1065            ParamValue::Flag if p.name == "npn" || p.name == "pnp" => {
1066                bm.npn = p.name == "npn";
1067                continue;
1068            }
1069            _ => return Err(format!("parameter {} needs a numeric value", p.name)),
1070        };
1071        match p.name.as_str() {
1072            "is" => bm.is = v,
1073            "bf" => bm.bf = v,
1074            "nf" => bm.nf = v,
1075            "vaf" | "va" => bm.vaf = v,
1076            "ikf" | "ik" => bm.ikf = v,
1077            "ise" => bm.ise = v,
1078            "ne" => bm.ne = v,
1079            "br" => bm.br = v,
1080            "nr" => bm.nr = v,
1081            "var" | "vb" => bm.var = v,
1082            "ikr" => bm.ikr = v,
1083            "isc" => bm.isc = v,
1084            "nc" => bm.nc = v,
1085            "rb" => bm.rb = v,
1086            "irb" => bm.irb = v,
1087            "rbm" => bm.rbm = Some(v),
1088            "re" => bm.re = v,
1089            "rc" => bm.rc = v,
1090            "cje" => bm.cje = v,
1091            "vje" | "pe" => bm.vje = v,
1092            "mje" | "me" => bm.mje = v,
1093            "tf" => bm.tf = v,
1094            "xtf" => bm.xtf = v,
1095            "vtf" => bm.vtf = v,
1096            "itf" => bm.itf = v,
1097            "ptf" => bm.ptf = v,
1098            "cjc" => bm.cjc = v,
1099            "vjc" | "pc" => bm.vjc = v,
1100            "mjc" | "mc" => bm.mjc = v,
1101            "xcjc" => bm.xcjc = v,
1102            "tr" => bm.tr = v,
1103            "cjs" | "ccs" => bm.cjs = v,
1104            "vjs" | "ps" => bm.vjs = v,
1105            "mjs" | "ms" => bm.mjs = v,
1106            "xtb" => bm.xtb = v,
1107            "eg" => bm.eg = v,
1108            "xti" => bm.xti = v,
1109            "fc" => bm.fc = Some(v),
1110            "kf" => bm.kf = v,
1111            "af" => bm.af = v,
1112            "tnom" => bm.tnom = Some(v + CTOK),
1113            other => notes.push(format!(
1114                "model `{}`: unrecognized parameter ({other}) - ignored",
1115                m.name
1116            )),
1117        }
1118    }
1119    Ok(bm)
1120}
1121
1122/// Map `.model x nmos/pmos(level=1 …)` parameters onto
1123/// [`crate::mos1::Mos1Model`], 3f5 names with the documented aliases
1124/// (`mos1.c`).
1125fn mos1_model(m: &ir::Model, notes: &mut Vec<String>) -> Result<crate::mos1::Mos1Model, String> {
1126    let mut mm = crate::mos1::Mos1Model::new(m.mtype == "nmos");
1127    for p in &m.params {
1128        let v = match p.value {
1129            ParamValue::Num(v) => v,
1130            // The nmos/pmos type may also appear as a flag parameter.
1131            ParamValue::Flag if p.name == "nmos" || p.name == "pmos" => {
1132                mm.nmos = p.name == "nmos";
1133                continue;
1134            }
1135            _ => return Err(format!("parameter {} needs a numeric value", p.name)),
1136        };
1137        match p.name.as_str() {
1138            "level" => {} // dispatched by the caller
1139            "vto" | "vt0" => mm.vto = Some(v),
1140            "kp" => mm.kp = Some(v),
1141            "gamma" => mm.gamma = Some(v),
1142            "phi" => mm.phi = Some(v),
1143            "lambda" => mm.lambda = v,
1144            "rd" => mm.rd = Some(v),
1145            "rs" => mm.rs = Some(v),
1146            "cbd" => mm.cbd = Some(v),
1147            "cbs" => mm.cbs = Some(v),
1148            "is" => mm.is = v,
1149            "pb" => mm.pb = v,
1150            "cgso" => mm.cgso = v,
1151            "cgdo" => mm.cgdo = v,
1152            "cgbo" => mm.cgbo = v,
1153            "rsh" => mm.rsh = Some(v),
1154            "cj" => mm.cj = Some(v),
1155            "mj" => mm.mj = v,
1156            "cjsw" => mm.cjsw = Some(v),
1157            "mjsw" => mm.mjsw = v,
1158            "js" => mm.js = v,
1159            "tox" => mm.tox = Some(v),
1160            "ld" => mm.ld = v,
1161            "u0" | "uo" => mm.u0 = Some(v),
1162            "fc" => mm.fc = v,
1163            "nsub" => mm.nsub = Some(v),
1164            "tpg" => mm.tpg = Some(v),
1165            "nss" => mm.nss = Some(v),
1166            "kf" => mm.kf = v,
1167            "af" => mm.af = v,
1168            // ngspice-47 noise selectors (IF_INTEGER: `floor(0.5 + v)`).
1169            "nlev" => mm.nlev = (0.5 + v).floor() as i64,
1170            "gdsnoi" => mm.gdsnoi = v,
1171            "tnom" => mm.tnom = Some(v + CTOK),
1172            other => notes.push(format!(
1173                "model `{}`: unrecognized parameter ({other}) - ignored",
1174                m.name
1175            )),
1176        }
1177    }
1178    Ok(mm)
1179}
1180
1181/// MOSFET instance parameters shared by every level
1182/// (`l`/`w`/`ad`/`as` keep given-ness for the `.options` defaults;
1183/// `nrd`/`nrs` default to 1).
1184#[derive(Default)]
1185struct MosInst {
1186    l: Option<f64>,
1187    w: Option<f64>,
1188    ad: Option<f64>,
1189    as_: Option<f64>,
1190    pd: f64,
1191    ps: f64,
1192    nrd: f64,
1193    nrs: f64,
1194    off: bool,
1195    ic: (f64, f64, f64),
1196    /// Which `IC=` components the card gave.
1197    ic_given: IcGiven,
1198    temp: Option<f64>,
1199    /// ngspice-47 `DTEMP=` (MOS1/2/3/6).
1200    dtemp: Option<f64>,
1201    /// BSIM3 only: `M`, `DELVTO`, `MULU0`.
1202    m: f64,
1203    delvto: f64,
1204    mulu0: f64,
1205}
1206
1207impl MosInst {
1208    fn new() -> Self {
1209        MosInst {
1210            ic_given: IcGiven::NONE,
1211            nrd: 1.0,
1212            nrs: 1.0,
1213            m: 1.0,
1214            mulu0: 1.0,
1215            ..Default::default()
1216        }
1217    }
1218}
1219
1220/// Map `.model x nmos/pmos(level=2 …)` parameters onto
1221/// [`crate::mos2::Mos2Model`] (`mos2.c`).
1222fn mos2_model(m: &ir::Model, notes: &mut Vec<String>) -> Result<crate::mos2::Mos2Model, String> {
1223    let mut mm = crate::mos2::Mos2Model::new(m.mtype == "nmos");
1224    for p in &m.params {
1225        let v = match p.value {
1226            ParamValue::Num(v) => v,
1227            ParamValue::Flag if p.name == "nmos" || p.name == "pmos" => {
1228                mm.nmos = p.name == "nmos";
1229                continue;
1230            }
1231            _ => return Err(format!("parameter {} needs a numeric value", p.name)),
1232        };
1233        match p.name.as_str() {
1234            "level" => {}
1235            "vto" | "vt0" => mm.vto = Some(v),
1236            "kp" => mm.kp = Some(v),
1237            "gamma" => mm.gamma = Some(v),
1238            "phi" => mm.phi = Some(v),
1239            "lambda" => mm.lambda = v,
1240            "rd" => mm.rd = Some(v),
1241            "rs" => mm.rs = Some(v),
1242            "cbd" => mm.cbd = Some(v),
1243            "cbs" => mm.cbs = Some(v),
1244            "is" => mm.is = v,
1245            "pb" => mm.pb = v,
1246            "cgso" => mm.cgso = v,
1247            "cgdo" => mm.cgdo = v,
1248            "cgbo" => mm.cgbo = v,
1249            "rsh" => mm.rsh = Some(v),
1250            "cj" => mm.cj = Some(v),
1251            "mj" => mm.mj = v,
1252            "cjsw" => mm.cjsw = Some(v),
1253            "mjsw" => mm.mjsw = v,
1254            "js" => mm.js = v,
1255            "tox" => mm.tox = Some(v),
1256            "ld" => mm.ld = v,
1257            "u0" | "uo" => mm.u0 = Some(v),
1258            "fc" => mm.fc = v,
1259            "nsub" => mm.nsub = Some(v),
1260            "tpg" => mm.tpg = Some(v),
1261            "nss" => mm.nss = Some(v),
1262            "delta" => mm.delta = v,
1263            "uexp" => mm.uexp = v,
1264            "ucrit" => mm.ucrit = v,
1265            "vmax" => mm.vmax = v,
1266            "xj" => mm.xj = v,
1267            "neff" => mm.neff = v,
1268            "nfs" => mm.nfs = v,
1269            "kf" => mm.kf = v,
1270            "af" => mm.af = v,
1271            // ngspice-47 noise selectors (IF_INTEGER: `floor(0.5 + v)`).
1272            "nlev" => mm.nlev = (0.5 + v).floor() as i64,
1273            "gdsnoi" => mm.gdsnoi = v,
1274            "tnom" => mm.tnom = Some(v + CTOK),
1275            other => notes.push(format!(
1276                "model `{}`: unrecognized parameter ({other}) - ignored",
1277                m.name
1278            )),
1279        }
1280    }
1281    Ok(mm)
1282}
1283
1284/// Map `.model x nmos/pmos(level=3 …)` parameters onto
1285/// [`crate::mos3::Mos3Model`] (`mos3.c`). `XD`/`ALPHA` are output-
1286/// only in practice (`mos3mpar.c` has no case for them) and
1287/// `input_delta` is the C's internal alias; all three fall through
1288/// to the unrecognized-parameter note.
1289fn mos3_model(m: &ir::Model, notes: &mut Vec<String>) -> Result<crate::mos3::Mos3Model, String> {
1290    let mut mm = crate::mos3::Mos3Model::new(m.mtype == "nmos");
1291    for p in &m.params {
1292        let v = match p.value {
1293            ParamValue::Num(v) => v,
1294            ParamValue::Flag if p.name == "nmos" || p.name == "pmos" => {
1295                mm.nmos = p.name == "nmos";
1296                continue;
1297            }
1298            _ => return Err(format!("parameter {} needs a numeric value", p.name)),
1299        };
1300        match p.name.as_str() {
1301            "level" => {}
1302            "vto" | "vt0" => mm.vto = Some(v),
1303            "kp" => mm.kp = Some(v),
1304            "gamma" => mm.gamma = Some(v),
1305            "phi" => mm.phi = Some(v),
1306            "rd" => mm.rd = Some(v),
1307            "rs" => mm.rs = Some(v),
1308            "cbd" => mm.cbd = Some(v),
1309            "cbs" => mm.cbs = Some(v),
1310            "is" => mm.is = v,
1311            "pb" => mm.pb = v,
1312            "cgso" => mm.cgso = v,
1313            "cgdo" => mm.cgdo = v,
1314            "cgbo" => mm.cgbo = v,
1315            "rsh" => mm.rsh = Some(v),
1316            "cj" => mm.cj = Some(v),
1317            "mj" => mm.mj = v,
1318            "cjsw" => mm.cjsw = Some(v),
1319            "mjsw" => mm.mjsw = v,
1320            "js" => mm.js = v,
1321            "tox" => mm.tox = Some(v),
1322            "ld" => mm.ld = v,
1323            "u0" | "uo" => mm.u0 = Some(v),
1324            "fc" => mm.fc = v,
1325            "nsub" => mm.nsub = Some(v),
1326            "tpg" => mm.tpg = Some(v),
1327            "nss" => mm.nss = Some(v),
1328            "vmax" => mm.vmax = v,
1329            "xj" => mm.xj = v,
1330            "nfs" => mm.nfs = v,
1331            "eta" => mm.eta = v,
1332            "delta" => mm.delta = v,
1333            "theta" => mm.theta = v,
1334            "kappa" => mm.kappa = v,
1335            "kf" => mm.kf = v,
1336            "af" => mm.af = v,
1337            // ngspice-47 noise selectors (IF_INTEGER: `floor(0.5 + v)`).
1338            "nlev" => mm.nlev = (0.5 + v).floor() as i64,
1339            "gdsnoi" => mm.gdsnoi = v,
1340            "tnom" => mm.tnom = Some(v + CTOK),
1341            other => notes.push(format!(
1342                "model `{}`: unrecognized parameter ({other}) - ignored",
1343                m.name
1344            )),
1345        }
1346    }
1347    Ok(mm)
1348}
1349
1350/// Map `.model x nmos/pmos(level=6 …)` parameters onto
1351/// [`crate::mos6::Mos6Model`] (`mos6.c`).
1352fn mos6_model(m: &ir::Model, notes: &mut Vec<String>) -> Result<crate::mos6::Mos6Model, String> {
1353    let mut mm = crate::mos6::Mos6Model::new(m.mtype == "nmos");
1354    for p in &m.params {
1355        let v = match p.value {
1356            ParamValue::Num(v) => v,
1357            ParamValue::Flag if p.name == "nmos" || p.name == "pmos" => {
1358                mm.nmos = p.name == "nmos";
1359                continue;
1360            }
1361            _ => return Err(format!("parameter {} needs a numeric value", p.name)),
1362        };
1363        match p.name.as_str() {
1364            "level" => {}
1365            "vto" | "vt0" => mm.vto = Some(v),
1366            "kv" => mm.kv = v,
1367            "nv" => mm.nv = v,
1368            "kc" => mm.kc = Some(v),
1369            "nc" => mm.nc = v,
1370            "nvth" => mm.nvth = v,
1371            "ps" => mm.ps = v,
1372            "gamma" => mm.gamma = Some(v),
1373            "gamma1" => mm.gamma1 = v,
1374            "sigma" => mm.sigma = v,
1375            "phi" => mm.phi = Some(v),
1376            "lambda" => mm.lambda = Some(v),
1377            "lambda0" => mm.lambda0 = Some(v),
1378            "lambda1" => mm.lambda1 = v,
1379            "rd" => mm.rd = Some(v),
1380            "rs" => mm.rs = Some(v),
1381            "cbd" => mm.cbd = Some(v),
1382            "cbs" => mm.cbs = Some(v),
1383            "is" => mm.is = v,
1384            "pb" => mm.pb = v,
1385            "cgso" => mm.cgso = v,
1386            "cgdo" => mm.cgdo = v,
1387            "cgbo" => mm.cgbo = v,
1388            "rsh" => mm.rsh = Some(v),
1389            "cj" => mm.cj = Some(v),
1390            "mj" => mm.mj = v,
1391            "cjsw" => mm.cjsw = Some(v),
1392            "mjsw" => mm.mjsw = v,
1393            "js" => mm.js = v,
1394            "tox" => mm.tox = Some(v),
1395            "ld" => mm.ld = v,
1396            "u0" | "uo" => mm.u0 = Some(v),
1397            "fc" => mm.fc = v,
1398            "nsub" => mm.nsub = Some(v),
1399            "tpg" => mm.tpg = Some(v),
1400            "nss" => mm.nss = Some(v),
1401            "kf" => mm.kf = v,
1402            "af" => mm.af = v,
1403            "tnom" => mm.tnom = Some(v + CTOK),
1404            other => notes.push(format!(
1405                "model `{}`: unrecognized parameter ({other}) - ignored",
1406                m.name
1407            )),
1408        }
1409    }
1410    Ok(mm)
1411}
1412
1413/// Map `.model x nmos/pmos(level=4 …)` parameters onto
1414/// [`crate::bsim1::Bsim1Model`] (`b1.c` keyword table).
1415#[allow(clippy::too_many_lines)]
1416fn bsim1_model(m: &ir::Model, notes: &mut Vec<String>) -> Result<crate::bsim1::Bsim1Model, String> {
1417    let mut b = crate::bsim1::Bsim1Model::new(m.mtype == "nmos");
1418    for p in &m.params {
1419        let v = match p.value {
1420            ParamValue::Num(v) => v,
1421            ParamValue::Flag if p.name == "nmos" || p.name == "pmos" => {
1422                b.nmos = p.name == "nmos";
1423                continue;
1424            }
1425            _ => return Err(format!("parameter {} needs a numeric value", p.name)),
1426        };
1427        match p.name.as_str() {
1428            "level" => {}
1429            "vfb" => b.vfb0 = v,
1430            "lvfb" => b.vfbl = v,
1431            "wvfb" => b.vfbw = v,
1432            "phi" => b.phi0 = v,
1433            "lphi" => b.phil = v,
1434            "wphi" => b.phiw = v,
1435            "k1" => b.k10 = v,
1436            "lk1" => b.k1l = v,
1437            "wk1" => b.k1w = v,
1438            "k2" => b.k20 = v,
1439            "lk2" => b.k2l = v,
1440            "wk2" => b.k2w = v,
1441            "eta" => b.eta0 = v,
1442            "leta" => b.etal = v,
1443            "weta" => b.etaw = v,
1444            "x2e" => b.etab0 = v,
1445            "lx2e" => b.etabl = v,
1446            "wx2e" => b.etabw = v,
1447            "x3e" => b.etad0 = v,
1448            "lx3e" => b.etadl = v,
1449            "wx3e" => b.etadw = v,
1450            "dl" => b.deltal = v,
1451            "dw" => b.deltaw = v,
1452            "muz" => b.mobzero = v,
1453            "x2mz" => b.mobzerob0 = v,
1454            "lx2mz" => b.mobzerobl = v,
1455            "wx2mz" => b.mobzerobw = v,
1456            "mus" => b.mobvdd0 = v,
1457            "lmus" => b.mobvddl = v,
1458            "wmus" => b.mobvddw = v,
1459            "x2ms" => b.mobvddb0 = v,
1460            "lx2ms" => b.mobvddbl = v,
1461            "wx2ms" => b.mobvddbw = v,
1462            "x3ms" => b.mobvddd0 = v,
1463            "lx3ms" => b.mobvdddl = v,
1464            "wx3ms" => b.mobvdddw = v,
1465            "u0" => b.ugs0 = v,
1466            "lu0" => b.ugsl = v,
1467            "wu0" => b.ugsw = v,
1468            "x2u0" => b.ugsb0 = v,
1469            "lx2u0" => b.ugsbl = v,
1470            "wx2u0" => b.ugsbw = v,
1471            "u1" => b.uds0 = v,
1472            "lu1" => b.udsl = v,
1473            "wu1" => b.udsw = v,
1474            "x2u1" => b.udsb0 = v,
1475            "lx2u1" => b.udsbl = v,
1476            "wx2u1" => b.udsbw = v,
1477            "x3u1" => b.udsd0 = v,
1478            "lx3u1" => b.udsdl = v,
1479            "wx3u1" => b.udsdw = v,
1480            "n0" => b.n00 = v,
1481            "ln0" => b.n0l = v,
1482            "wn0" => b.n0w = v,
1483            "nb" => b.nb0 = v,
1484            "lnb" => b.nbl = v,
1485            "wnb" => b.nbw = v,
1486            "nd" => b.nd0 = v,
1487            "lnd" => b.ndl = v,
1488            "wnd" => b.ndw = v,
1489            "tox" => b.tox = v,
1490            "temp" => b.temp = v,
1491            "vdd" => b.vdd = v,
1492            "cgso" => b.cgso = v,
1493            "cgdo" => b.cgdo = v,
1494            "cgbo" => b.cgbo = v,
1495            "xpart" => b.xpart = v,
1496            "rsh" => b.rsh = v,
1497            "js" => b.js = v,
1498            "pb" => b.pb = v,
1499            "mj" => b.mj = v,
1500            "pbsw" => b.pbsw = v,
1501            "mjsw" => b.mjsw = v,
1502            "cj" => b.cj = v,
1503            "cjsw" => b.cjsw = v,
1504            "wdf" => b.wdf = v,
1505            "dell" => b.dell = v,
1506            // ngspice-47 noise parameters (`b1.c`/`b2.c`; not in the 3f5).
1507            "kf" => b.kf = v,
1508            "af" => b.af = v,
1509            other => notes.push(format!(
1510                "model `{}`: unrecognized parameter ({other}) - ignored",
1511                m.name
1512            )),
1513        }
1514    }
1515    Ok(b)
1516}
1517
1518/// Map `.model x nmos/pmos(level=8|49 …)` onto
1519/// [`crate::bsim3::Bsim3Model`] (`b3.c` keyword table via
1520/// [`crate::bsim3::Bsim3Model::set`]). The outer `Err` is a broken
1521/// card; `Ok(Err(reason))` a card for a BSIM3 variant that is not
1522/// implemented (another `VERSION`, the NQS model, `ACM > 0`, a
1523/// `CAPMOD` outside 0–3), which only errs when instantiated.
1524fn bsim3_model(
1525    m: &ir::Model,
1526    notes: &mut Vec<String>,
1527) -> Result<Result<crate::bsim3::Bsim3Model, String>, String> {
1528    let mut b = crate::bsim3::Bsim3Model::new(m.mtype == "nmos");
1529    for p in &m.params {
1530        if p.name == "version" {
1531            // A string in ngspice (`IF_STRING`); the lexer may read
1532            // it as a number or a number list (`3.3.0`).
1533            let v = match &p.value {
1534                ParamValue::Str(s) => s.clone(),
1535                ParamValue::Num(v) => format!("{v}"),
1536                ParamValue::NumList(v) => v
1537                    .iter()
1538                    .map(|x| format!("{x}"))
1539                    .collect::<Vec<_>>()
1540                    .join("."),
1541                ParamValue::Flag => return Err("parameter version needs a value".into()),
1542            };
1543            if !v.starts_with("3.3") {
1544                return Ok(Err(format!(
1545                    "BSIM3 version {v} is not implemented (only 3.3.x, ngspice's default)"
1546                )));
1547            }
1548            b.set_version(&v);
1549            continue;
1550        }
1551        let v = match p.value {
1552            ParamValue::Num(v) => v,
1553            ParamValue::Flag if p.name == "nmos" || p.name == "pmos" => continue,
1554            _ => return Err(format!("parameter {} needs a numeric value", p.name)),
1555        };
1556        if p.name == "level" {
1557            continue;
1558        }
1559        if let Err(e) = b.set(&p.name, v) {
1560            // `INPmakeMod`: unknown parameters are a warning.
1561            notes.push(format!("model `{}`: {e} (ignored)", m.name));
1562        }
1563    }
1564    let int = |k: &str| b.get_int(k);
1565    if int("nqsmod") == Some(1) || int("acnqsmod") == Some(1) {
1566        return Ok(Err(
1567            "the BSIM3 non-quasi-static model (NQSMOD/ACNQSMOD = 1) is not implemented".into(),
1568        ));
1569    }
1570    if int("acm").is_some_and(|a| a != 0) {
1571        return Ok(Err(
1572            "BSIM3 ACM > 0 junction geometry is not implemented".into()
1573        ));
1574    }
1575    if int("capmod").is_some_and(|c| !(0..=3).contains(&c)) {
1576        return Ok(Err("BSIM3 CAPMOD must be 0, 1, 2 or 3".into()));
1577    }
1578    Ok(Ok(b))
1579}
1580
1581/// Map `.model x nmos/pmos(level=5 …)` parameters onto
1582/// [`crate::bsim2::Bsim2Model`] (`b2.c` keyword table; defaults
1583/// from `b2set.c` via [`crate::bsim2::Bsim2Model::new`]).
1584#[allow(clippy::too_many_lines)]
1585fn bsim2_model(m: &ir::Model, notes: &mut Vec<String>) -> Result<crate::bsim2::Bsim2Model, String> {
1586    let mut b = crate::bsim2::Bsim2Model::new(m.mtype == "nmos");
1587    for p in &m.params {
1588        let v = match p.value {
1589            ParamValue::Num(v) => v,
1590            ParamValue::Flag if p.name == "nmos" || p.name == "pmos" => {
1591                b.nmos = p.name == "nmos";
1592                continue;
1593            }
1594            _ => return Err(format!("parameter {} needs a numeric value", p.name)),
1595        };
1596        match p.name.as_str() {
1597            "level" => {}
1598            "vfb" => b.vfb = v,
1599            "lvfb" => b.lvfb = v,
1600            "wvfb" => b.wvfb = v,
1601            "phi" => b.phi = v,
1602            "lphi" => b.lphi = v,
1603            "wphi" => b.wphi = v,
1604            "k1" => b.k1 = v,
1605            "lk1" => b.lk1 = v,
1606            "wk1" => b.wk1 = v,
1607            "k2" => b.k2 = v,
1608            "lk2" => b.lk2 = v,
1609            "wk2" => b.wk2 = v,
1610            "eta0" => b.eta0 = v,
1611            "leta0" => b.leta0 = v,
1612            "weta0" => b.weta0 = v,
1613            "etab" => b.etab = v,
1614            "letab" => b.letab = v,
1615            "wetab" => b.wetab = v,
1616            "dl" => b.dl = v,
1617            "dw" => b.dw = v,
1618            "mu0" => b.mu0 = v,
1619            "mu0b" => b.mu0b = v,
1620            "lmu0b" => b.lmu0b = v,
1621            "wmu0b" => b.wmu0b = v,
1622            "mus0" => b.mus0 = v,
1623            "lmus0" => b.lmus0 = v,
1624            "wmus0" => b.wmus0 = v,
1625            "musb" => b.musb = v,
1626            "lmusb" => b.lmusb = v,
1627            "wmusb" => b.wmusb = v,
1628            "mu20" => b.mu20 = v,
1629            "lmu20" => b.lmu20 = v,
1630            "wmu20" => b.wmu20 = v,
1631            "mu2b" => b.mu2b = v,
1632            "lmu2b" => b.lmu2b = v,
1633            "wmu2b" => b.wmu2b = v,
1634            "mu2g" => b.mu2g = v,
1635            "lmu2g" => b.lmu2g = v,
1636            "wmu2g" => b.wmu2g = v,
1637            "mu30" => b.mu30 = v,
1638            "lmu30" => b.lmu30 = v,
1639            "wmu30" => b.wmu30 = v,
1640            "mu3b" => b.mu3b = v,
1641            "lmu3b" => b.lmu3b = v,
1642            "wmu3b" => b.wmu3b = v,
1643            "mu3g" => b.mu3g = v,
1644            "lmu3g" => b.lmu3g = v,
1645            "wmu3g" => b.wmu3g = v,
1646            "mu40" => b.mu40 = v,
1647            "lmu40" => b.lmu40 = v,
1648            "wmu40" => b.wmu40 = v,
1649            "mu4b" => b.mu4b = v,
1650            "lmu4b" => b.lmu4b = v,
1651            "wmu4b" => b.wmu4b = v,
1652            "mu4g" => b.mu4g = v,
1653            "lmu4g" => b.lmu4g = v,
1654            "wmu4g" => b.wmu4g = v,
1655            "ua0" => b.ua0 = v,
1656            "lua0" => b.lua0 = v,
1657            "wua0" => b.wua0 = v,
1658            "uab" => b.uab = v,
1659            "luab" => b.luab = v,
1660            "wuab" => b.wuab = v,
1661            "ub0" => b.ub0 = v,
1662            "lub0" => b.lub0 = v,
1663            "wub0" => b.wub0 = v,
1664            "ubb" => b.ubb = v,
1665            "lubb" => b.lubb = v,
1666            "wubb" => b.wubb = v,
1667            "u10" => b.u10 = v,
1668            "lu10" => b.lu10 = v,
1669            "wu10" => b.wu10 = v,
1670            "u1b" => b.u1b = v,
1671            "lu1b" => b.lu1b = v,
1672            "wu1b" => b.wu1b = v,
1673            "u1d" => b.u1d = v,
1674            "lu1d" => b.lu1d = v,
1675            "wu1d" => b.wu1d = v,
1676            "n0" => b.n0 = v,
1677            "ln0" => b.ln0 = v,
1678            "wn0" => b.wn0 = v,
1679            "nb" => b.nb = v,
1680            "lnb" => b.lnb = v,
1681            "wnb" => b.wnb = v,
1682            "nd" => b.nd = v,
1683            "lnd" => b.lnd = v,
1684            "wnd" => b.wnd = v,
1685            "vof0" => b.vof0 = v,
1686            "lvof0" => b.lvof0 = v,
1687            "wvof0" => b.wvof0 = v,
1688            "vofb" => b.vofb = v,
1689            "lvofb" => b.lvofb = v,
1690            "wvofb" => b.wvofb = v,
1691            "vofd" => b.vofd = v,
1692            "lvofd" => b.lvofd = v,
1693            "wvofd" => b.wvofd = v,
1694            "ai0" => b.ai0 = v,
1695            "lai0" => b.lai0 = v,
1696            "wai0" => b.wai0 = v,
1697            "aib" => b.aib = v,
1698            "laib" => b.laib = v,
1699            "waib" => b.waib = v,
1700            "bi0" => b.bi0 = v,
1701            "lbi0" => b.lbi0 = v,
1702            "wbi0" => b.wbi0 = v,
1703            "bib" => b.bib = v,
1704            "lbib" => b.lbib = v,
1705            "wbib" => b.wbib = v,
1706            "vghigh" => b.vghigh = v,
1707            "lvghigh" => b.lvghigh = v,
1708            "wvghigh" => b.wvghigh = v,
1709            "vglow" => b.vglow = v,
1710            "lvglow" => b.lvglow = v,
1711            "wvglow" => b.wvglow = v,
1712            "tox" => b.tox = v,
1713            "temp" => b.temp = v,
1714            "vdd" => b.vdd = v,
1715            "vgg" => b.vgg = v,
1716            "vbb" => b.vbb = v,
1717            "cgso" => b.cgso = v,
1718            "cgdo" => b.cgdo = v,
1719            "cgbo" => b.cgbo = v,
1720            "xpart" => b.xpart = v,
1721            "rsh" => b.rsh = v,
1722            "js" => b.js = v,
1723            "pb" => b.pb = v,
1724            "mj" => b.mj = v,
1725            "pbsw" => b.pbsw = v,
1726            "mjsw" => b.mjsw = v,
1727            "cj" => b.cj = v,
1728            "cjsw" => b.cjsw = v,
1729            "wdf" => b.wdf = v,
1730            "dell" => b.dell = v,
1731            // ngspice-47 noise parameters (`b1.c`/`b2.c`; not in the 3f5).
1732            "kf" => b.kf = v,
1733            "af" => b.af = v,
1734            other => notes.push(format!(
1735                "model `{}`: unrecognized parameter ({other}) - ignored",
1736                m.name
1737            )),
1738        }
1739    }
1740    Ok(b)
1741}
1742
1743fn add(ckt: &mut Circuit, spec: impl Into<crate::circuit::Spec>, problems: &mut Vec<String>) {
1744    if let Err(e) = ckt.add(spec) {
1745        problems.push(e.to_string());
1746    }
1747}
1748
1749/// [`add`] for a device with `IC=` components, recording which ones
1750/// the card gave (for its `DEVsetic`).
1751fn add_ic(
1752    ckt: &mut Circuit,
1753    spec: impl Into<crate::circuit::Spec>,
1754    given: IcGiven,
1755    problems: &mut Vec<String>,
1756) {
1757    let spec = spec.into();
1758    let name = spec.name().to_string();
1759    match ckt.add(spec) {
1760        Ok(()) => ckt.set_ic_given(&name, given),
1761        Err(e) => problems.push(e.to_string()),
1762    }
1763}
1764
1765fn num_param(params: &[ir::Param], name: &str) -> Option<f64> {
1766    params
1767        .iter()
1768        .find_map(|p| match (&p.value, p.name == name) {
1769            (ParamValue::Num(v), true) => Some(*v),
1770            _ => None,
1771        })
1772}
1773
1774/// `.model x r(...)` (`res.c` model table, defaults of
1775/// `restemp.c`). Temperatures in kelvin.
1776#[derive(Clone, Copy, Debug)]
1777struct ResModel {
1778    rsh: f64,
1779    defw: f64,
1780    narrow: f64,
1781    tc1: f64,
1782    tc2: f64,
1783    tnom: Option<f64>,
1784}
1785
1786/// ngspice-47 resistor model parameters the 3f5 does not have
1787/// (`res.c` of ngspice): accepted and ignored with a note.
1788const RES_MODEL_NGSPICE: &[&str] = &[
1789    "short", "dlr", "tce", "kf", "af", "lf", "wf", "ef", "res", "defl", "bv_max",
1790];
1791
1792fn res_model(m: &ir::Model, notes: &mut Vec<String>) -> Result<ResModel, String> {
1793    // `REStemp`: RSH 0, DEFW 10e-6 (the code; spice.txt's 1e-6 is
1794    // wrong), NARROW 0, TC1/TC2 0, TNOM the circuit's.
1795    let mut rm = ResModel {
1796        rsh: 0.0,
1797        defw: 10e-6,
1798        narrow: 0.0,
1799        tc1: 0.0,
1800        tc2: 0.0,
1801        tnom: None,
1802    };
1803    for p in &m.params {
1804        let v = match p.value {
1805            ParamValue::Num(v) => v,
1806            ParamValue::Flag if p.name == "r" => continue,
1807            _ => return Err(format!("parameter {} needs a numeric value", p.name)),
1808        };
1809        match p.name.as_str() {
1810            "rsh" => rm.rsh = v,
1811            "defw" => rm.defw = v,
1812            "narrow" => rm.narrow = v,
1813            "tc1" => rm.tc1 = v,
1814            "tc2" => rm.tc2 = v,
1815            "tnom" => rm.tnom = Some(v + CTOK),
1816            other if RES_MODEL_NGSPICE.contains(&other) => notes.push(format!(
1817                "model `{}`: {other} is an ngspice resistor parameter, not in the 3f5 - ignored",
1818                m.name
1819            )),
1820            other => notes.push(format!(
1821                "model `{}`: unrecognized parameter ({other}) - ignored",
1822                m.name
1823            )),
1824        }
1825    }
1826    Ok(rm)
1827}
1828
1829/// One `R` card → nominal resistance and, when the card names a
1830/// model or gives `TEMP=`, its [`ResTemp`](crate::circuit::ResTemp)
1831/// (`REStemp`, `restemp.c`):
1832///
1833/// * `VALUE` (bare or `resistance=`) wins;
1834/// * else, with a model whose `RSH ≠ 0` and `L ≠ 0`,
1835///   `R = RSH·(L − NARROW)/(W − NARROW)`, `W` defaulting to `DEFW`;
1836/// * else the C warns "resistance=0, set to 1000" and uses 1 kΩ.
1837///
1838/// A plain card (no model) with no value stays an error: the 3f5
1839/// would silently make it 1 kΩ through its default model.
1840fn res_instance(
1841    name: &str,
1842    value: Option<f64>,
1843    rm: Option<&ResModel>,
1844    params: &[ir::Param],
1845    notes: &mut Vec<String>,
1846) -> Result<(f64, Option<crate::circuit::ResTemp>), String> {
1847    let mut value = value;
1848    let mut l = None;
1849    let mut w = None;
1850    let mut temp = None;
1851    let mut dtemp = None;
1852    for p in params {
1853        let v = match p.value {
1854            ParamValue::Num(v) => v,
1855            _ => {
1856                notes.push(format!("`{name}`: resistor parameter {} ignored", p.name));
1857                continue;
1858            }
1859        };
1860        match p.name.as_str() {
1861            "resistance" => value = Some(v),
1862            "l" => l = Some(v),
1863            "w" => w = Some(v),
1864            "temp" => temp = Some(v + CTOK),
1865            // ngspice-47 `res.c` (KNOWN_DIFFS).
1866            "dtemp" => dtemp = Some(v),
1867            other => notes.push(format!(
1868                "`{name}`: resistor parameter {other} is not in the 3f5 - ignored"
1869            )),
1870        }
1871    }
1872    if rm.is_none() && (l.is_some() || w.is_some()) {
1873        notes.push(format!("`{name}`: L/W without a resistor model ignored"));
1874    }
1875    let r = match (value, rm) {
1876        (Some(v), _) => v,
1877        (None, None) => return Err(format!("`{name}`: resistor has no value")),
1878        (None, Some(m)) => {
1879            let l = l.unwrap_or(0.0);
1880            let w = w.unwrap_or(m.defw);
1881            if m.rsh != 0.0 && l != 0.0 {
1882                m.rsh * (l - m.narrow) / (w - m.narrow)
1883            } else {
1884                notes.push(format!("`{name}`: resistance=0, set to 1000"));
1885                1000.0
1886            }
1887        }
1888    };
1889    // `restemp.c` (ngspice-47): a given `TEMP=` wins and zeroes
1890    // `DTEMP`, with the C's message.
1891    if temp.is_some() && dtemp.is_some() {
1892        notes.push(format!(
1893            "`{name}`: Instance temperature specified, dtemp ignored"
1894        ));
1895        dtemp = None;
1896    }
1897    let rt = (rm.is_some() || temp.is_some() || dtemp.is_some()).then(|| crate::circuit::ResTemp {
1898        tc1: rm.map_or(0.0, |m| m.tc1),
1899        tc2: rm.map_or(0.0, |m| m.tc2),
1900        tnom: rm.and_then(|m| m.tnom),
1901        temp,
1902        dtemp: dtemp.unwrap_or(0.0),
1903    });
1904    Ok((r, rt))
1905}
1906
1907/// `.model x c(...)` (`cap.c` model table, defaults of
1908/// `capsetup.c`).
1909#[derive(Clone, Copy, Debug)]
1910struct CapModel {
1911    cj: f64,
1912    cjsw: f64,
1913    defw: f64,
1914    narrow: f64,
1915}
1916
1917/// ngspice-47 capacitor model parameters the 3f5 does not have.
1918const CAP_MODEL_NGSPICE: &[&str] = &[
1919    "cap", "short", "del", "tc1", "tc2", "tnom", "di", "thick", "defl", "bv_max",
1920];
1921
1922fn cap_model(m: &ir::Model, notes: &mut Vec<String>) -> Result<CapModel, String> {
1923    // `CAPsetup`: CJ 0, CJSW 0, DEFW 10e-6 (spice.txt says 1e-6),
1924    // NARROW 0.
1925    let mut cm = CapModel {
1926        cj: 0.0,
1927        cjsw: 0.0,
1928        defw: 10e-6,
1929        narrow: 0.0,
1930    };
1931    for p in &m.params {
1932        let v = match p.value {
1933            ParamValue::Num(v) => v,
1934            ParamValue::Flag if p.name == "c" => continue,
1935            _ => return Err(format!("parameter {} needs a numeric value", p.name)),
1936        };
1937        match p.name.as_str() {
1938            "cj" => cm.cj = v,
1939            "cjsw" => cm.cjsw = v,
1940            "defw" => cm.defw = v,
1941            "narrow" => cm.narrow = v,
1942            other if CAP_MODEL_NGSPICE.contains(&other) => notes.push(format!(
1943                "model `{}`: {other} is an ngspice capacitor parameter, not in the 3f5 - ignored",
1944                m.name
1945            )),
1946            other => notes.push(format!(
1947                "model `{}`: unrecognized parameter ({other}) - ignored",
1948                m.name
1949            )),
1950        }
1951    }
1952    Ok(cm)
1953}
1954
1955/// One `C` card → capacitance (`CAPtemp`, `captemp.c`): `VALUE`
1956/// (bare or `capacitance=`) wins; else, with a model,
1957/// `C = CJ·(W − NARROW)·(L − NARROW) + 2·CJSW·((L − NARROW) +
1958/// (W − NARROW))`, `W` defaulting to `DEFW` and `L` to 0.
1959fn cap_instance(
1960    name: &str,
1961    value: Option<f64>,
1962    cm: Option<&CapModel>,
1963    params: &[ir::Param],
1964    notes: &mut Vec<String>,
1965) -> Result<f64, String> {
1966    let mut value = value;
1967    let mut l = None;
1968    let mut w = None;
1969    for p in params {
1970        let v = match p.value {
1971            ParamValue::Num(v) => v,
1972            _ => {
1973                notes.push(format!("`{name}`: capacitor parameter {} ignored", p.name));
1974                continue;
1975            }
1976        };
1977        match p.name.as_str() {
1978            "capacitance" => value = Some(v),
1979            "ic" => {}
1980            "l" => l = Some(v),
1981            "w" => w = Some(v),
1982            other => notes.push(format!(
1983                "`{name}`: capacitor parameter {other} is not in the 3f5 - ignored"
1984            )),
1985        }
1986    }
1987    if cm.is_none() && (l.is_some() || w.is_some()) {
1988        notes.push(format!("`{name}`: L/W without a capacitor model ignored"));
1989    }
1990    match (value, cm) {
1991        (Some(v), _) => Ok(v),
1992        (None, None) => Err(format!("`{name}`: capacitor has no value")),
1993        (None, Some(m)) => {
1994            let l = l.unwrap_or(0.0) - m.narrow;
1995            let w = w.unwrap_or(m.defw) - m.narrow;
1996            Ok(m.cj * w * l + m.cjsw * 2.0 * (l + w))
1997        }
1998    }
1999}
2000
2001fn two(nodes: &[String]) -> [String; 2] {
2002    [nodes[0].clone(), nodes[1].clone()]
2003}
2004
2005fn three(nodes: &[String]) -> [String; 3] {
2006    [nodes[0].clone(), nodes[1].clone(), nodes[2].clone()]
2007}
2008
2009fn four(nodes: &[String]) -> [String; 4] {
2010    [
2011        nodes[0].clone(),
2012        nodes[1].clone(),
2013        nodes[2].clone(),
2014        nodes[3].clone(),
2015    ]
2016}
2017
2018fn device_kind_name(d: &ir::Device) -> &'static str {
2019    match d {
2020        ir::Device::Bjt { .. } => "the BJT",
2021        ir::Device::Mosfet { .. } => "the MOSFET",
2022        ir::Device::Jfet { .. } => "the JFET",
2023        ir::Device::Mesfet { .. } => "the MESFET",
2024        ir::Device::SwitchV { .. } | ir::Device::SwitchI { .. } => "the switch",
2025        ir::Device::TlineIdeal { .. } | ir::Device::TlineLossy { .. } => "the transmission line",
2026        ir::Device::Urc { .. } => "the URC line",
2027        ir::Device::Behavioral { .. } => "the B source",
2028        ir::Device::SubcktCall { .. } => "an unexpanded subcircuit call",
2029        _ => "this device",
2030    }
2031}
2032
2033fn device_phase(d: &ir::Device) -> &'static str {
2034    match d {
2035        ir::Device::SubcktCall { .. } => "1 — the definition is missing",
2036        _ => "3",
2037    }
2038}
2039
2040/// Map `.model x d(...)` parameters onto [`DiodeModel`], 3f5 names
2041/// (`dev/dio/diompar.c`; `cj0` is accepted for `cjo` like the C).
2042fn diode_model(m: &ir::Model, notes: &mut Vec<String>) -> Result<DiodeModel, String> {
2043    let mut dm = DiodeModel::default();
2044    for p in &m.params {
2045        let v = match p.value {
2046            ParamValue::Num(v) => v,
2047            _ => return Err(format!("parameter {} needs a numeric value", p.name)),
2048        };
2049        match p.name.as_str() {
2050            "is" => dm.is = v,
2051            "rs" => dm.rs = v,
2052            "n" => dm.n = v,
2053            "tt" => dm.tt = v,
2054            "cjo" | "cj0" => dm.cjo = v,
2055            "vj" => dm.vj = v,
2056            "m" => dm.m = v,
2057            "eg" => dm.eg = v,
2058            "xti" => dm.xti = v,
2059            "kf" => dm.kf = v,
2060            "af" => dm.af = v,
2061            "fc" => dm.fc = v,
2062            "bv" => dm.bv = Some(v),
2063            "ibv" => dm.ibv = v,
2064            "tnom" => dm.tnom = Some(v + CTOK),
2065            other => notes.push(format!(
2066                "model `{}`: unrecognized parameter ({other}) - ignored",
2067                m.name
2068            )),
2069        }
2070    }
2071    Ok(dm)
2072}
2073
2074/// `.model x njf/pjf(...)` → [`crate::jfet::JfetModel`] (`jfet.c`
2075/// model table: `VT0` with `VTO` as alias, the Sydney `B`). `LEVEL`
2076/// is ngspice's selector (2 = Parker–Skellern); only level 1, the
2077/// 3f5 model, is accepted.
2078fn jfet_model(m: &ir::Model, notes: &mut Vec<String>) -> Result<crate::jfet::JfetModel, String> {
2079    let mut jm = crate::jfet::JfetModel::new(m.mtype == "njf");
2080    for p in &m.params {
2081        let v = match p.value {
2082            ParamValue::Num(v) => v,
2083            ParamValue::Flag if p.name == "njf" || p.name == "pjf" => {
2084                jm.njf = p.name == "njf";
2085                continue;
2086            }
2087            _ => return Err(format!("parameter {} needs a numeric value", p.name)),
2088        };
2089        match p.name.as_str() {
2090            "level" => {}
2091            "vt0" | "vto" => jm.vto = v,
2092            "beta" => jm.beta = v,
2093            "lambda" => jm.lambda = v,
2094            "rd" => jm.rd = v,
2095            "rs" => jm.rs = v,
2096            "cgs" => jm.cgs = v,
2097            "cgd" => jm.cgd = v,
2098            "pb" => jm.pb = v,
2099            "is" => jm.is = v,
2100            "fc" => jm.fc = v,
2101            "b" => jm.b = v,
2102            "tnom" => jm.tnom = Some(v + CTOK),
2103            "kf" => jm.kf = v,
2104            "af" => jm.af = v,
2105            // ngspice-47 noise selectors (IF_INTEGER: `floor(0.5 + v)`).
2106            "nlev" => jm.nlev = (0.5 + v).floor() as i64,
2107            "gdsnoi" => jm.gdsnoi = v,
2108            other => notes.push(format!(
2109                "model `{}`: unrecognized parameter ({other}) - ignored",
2110                m.name
2111            )),
2112        }
2113    }
2114    Ok(jm)
2115}
2116
2117/// `RON`/`ROFF` of a switch model: a zero resistance would make
2118/// the C's `1.0/value` conductance infinite and poison the matrix.
2119fn switch_resistance(name: &str, v: f64) -> Result<f64, String> {
2120    if v == 0.0 || !v.is_finite() {
2121        return Err(format!("{name} must be a nonzero finite resistance"));
2122    }
2123    Ok(v)
2124}
2125
2126/// `.model x sw(...)` → [`crate::switch::SwModel`] (`sw.c` model
2127/// table; `GON`/`GOFF` are output-only there). `VH` keeps its sign as
2128/// in ngspice (`swmparam.c`); the 3f5 stored `|VH|`.
2129fn sw_model(m: &ir::Model, notes: &mut Vec<String>) -> Result<crate::switch::SwModel, String> {
2130    let mut sm = crate::switch::SwModel::default();
2131    for p in &m.params {
2132        let v = match p.value {
2133            ParamValue::Num(v) => v,
2134            ParamValue::Flag if p.name == "sw" => continue,
2135            _ => return Err(format!("parameter {} needs a numeric value", p.name)),
2136        };
2137        match p.name.as_str() {
2138            "vt" => sm.vt = v,
2139            "vh" => sm.vh = v,
2140            "ron" => sm.ron = Some(switch_resistance("ron", v)?),
2141            "roff" => sm.roff = Some(switch_resistance("roff", v)?),
2142            other => notes.push(format!(
2143                "model `{}`: unrecognized parameter ({other}) - ignored",
2144                m.name
2145            )),
2146        }
2147    }
2148    Ok(sm)
2149}
2150
2151/// `.model x csw(...)` → [`crate::switch::CswModel`] (`csw.c` model
2152/// table). `IH` keeps its sign as in ngspice (`cswmpar.c`); the 3f5
2153/// stored `|IH|`.
2154fn csw_model(m: &ir::Model, notes: &mut Vec<String>) -> Result<crate::switch::CswModel, String> {
2155    let mut wm = crate::switch::CswModel::default();
2156    for p in &m.params {
2157        let v = match p.value {
2158            ParamValue::Num(v) => v,
2159            ParamValue::Flag if p.name == "csw" => continue,
2160            _ => return Err(format!("parameter {} needs a numeric value", p.name)),
2161        };
2162        match p.name.as_str() {
2163            "it" => wm.it = v,
2164            "ih" => wm.ih = v,
2165            "ron" => wm.ron = Some(switch_resistance("ron", v)?),
2166            "roff" => wm.roff = Some(switch_resistance("roff", v)?),
2167            other => notes.push(format!(
2168                "model `{}`: unrecognized parameter ({other}) - ignored",
2169                m.name
2170            )),
2171        }
2172    }
2173    Ok(wm)
2174}
2175
2176/// `.model x nmf/pmf(...)` → [`crate::mes::MesModel`] (`mes.c` model
2177/// table). `LEVEL` is ngspice's selector (1 = Statz, the 3f5 model;
2178/// 2 MESA, 5/6 HFET); only level 1 is accepted.
2179fn mes_model(m: &ir::Model, notes: &mut Vec<String>) -> Result<crate::mes::MesModel, String> {
2180    let mut zm = crate::mes::MesModel::new(m.mtype == "nmf");
2181    for p in &m.params {
2182        let v = match p.value {
2183            ParamValue::Num(v) => v,
2184            ParamValue::Flag if p.name == "nmf" || p.name == "pmf" => {
2185                zm.nmf = p.name == "nmf";
2186                continue;
2187            }
2188            _ => return Err(format!("parameter {} needs a numeric value", p.name)),
2189        };
2190        match p.name.as_str() {
2191            "level" => {}
2192            "vt0" | "vto" => zm.vto = v,
2193            "alpha" => zm.alpha = v,
2194            "beta" => zm.beta = v,
2195            "lambda" => zm.lambda = v,
2196            "b" => zm.b = v,
2197            "rd" => zm.rd = v,
2198            "rs" => zm.rs = v,
2199            "cgs" => zm.cgs = v,
2200            "cgd" => zm.cgd = v,
2201            "pb" => zm.pb = v,
2202            "is" => zm.is = v,
2203            "fc" => zm.fc = v,
2204            "kf" => zm.kf = v,
2205            "af" => zm.af = v,
2206            other => notes.push(format!(
2207                "model `{}`: unrecognized parameter ({other}) - ignored",
2208                m.name
2209            )),
2210        }
2211    }
2212    Ok(zm)
2213}
2214
2215/// `.model x URC(...)` (`URCmPTable`, `dev/urc/urc.c`), with the
2216/// defaults `URCsetup` applies.
2217#[derive(Clone, Debug)]
2218struct UrcModel {
2219    /// `K`: propagation constant (the geometric ratio between
2220    /// consecutive lumps), default 1.5.
2221    k: f64,
2222    /// `FMAX`: maximum frequency of interest, default 1 GHz.
2223    fmax: f64,
2224    /// `RPERL`, default 1000 Ω/m.
2225    rperl: f64,
2226    /// `CPERL`, default 1e-12 F/m.
2227    cperl: f64,
2228    /// `ISPERL`: when given, the shunts are diodes instead of
2229    /// capacitors.
2230    isperl: Option<f64>,
2231    /// `RSPERL`, default 0.
2232    rsperl: f64,
2233}
2234
2235fn urc_model(m: &ir::Model, notes: &mut Vec<String>) -> Result<UrcModel, String> {
2236    let mut um = UrcModel {
2237        k: 1.5,
2238        fmax: 1e9,
2239        rperl: 1000.0,
2240        cperl: 1e-12,
2241        isperl: None,
2242        rsperl: 0.0,
2243    };
2244    for p in &m.params {
2245        // `URC`: "no operation - already know we are a URC, but
2246        // this makes spice-2 like parsers happy" (`urcmpar.c`).
2247        if p.name == "urc" && p.value == ParamValue::Flag {
2248            continue;
2249        }
2250        let v = match p.value {
2251            ParamValue::Num(v) => v,
2252            _ => return Err(format!("parameter {} needs a numeric value", p.name)),
2253        };
2254        match p.name.as_str() {
2255            "k" => um.k = v,
2256            "fmax" => um.fmax = v,
2257            "rperl" => um.rperl = v,
2258            "cperl" => um.cperl = v,
2259            "isperl" => um.isperl = Some(v),
2260            "rsperl" => um.rsperl = v,
2261            other => notes.push(format!(
2262                "model `{}`: unrecognized parameter ({other}) - ignored",
2263                m.name
2264            )),
2265        }
2266    }
2267    Ok(um)
2268}
2269
2270/// Upper bound on the lumps of one URC line (the C has none; a
2271/// hostile `K` near 1 or a huge `FMAX` would otherwise ask for
2272/// billions of elements).
2273const URC_MAX_LUMPS: i64 = 10_000;
2274
2275/// `URCsetup` (`dev/urc/urcsetup.c`): the 3f5 has no URC load — the
2276/// line *is* the ladder of lumped elements it expands into. Lump `i`
2277/// (1-based) puts `rlo<i>` on the side growing from N1 and `rhi<i>`
2278/// on the side growing from N2, both `p^(i-1)·r1`, and shunts the
2279/// new inner nodes to N3 with `clo<i>`/`chi<i>` of `p^(i-1)·c1` (or
2280/// diodes `dlo<i>`/`dhi<i>` of area `p^(i-1)` when `ISPERL` is
2281/// given); the two sides meet at the last `hi` node, which carries
2282/// only `clo`. Names follow `IFnewUid`/`CKTmkVolt` (`u1#hi1`,
2283/// `u1#rlo1`, …), so the inner nodes show up in the rawfile as in
2284/// ngspice.
2285fn urc_expand(
2286    name: &str,
2287    nodes: &[String],
2288    m: &UrcModel,
2289    params: &[ir::Param],
2290) -> Result<Vec<crate::circuit::Spec>, String> {
2291    use crate::circuit::{Capacitor, Diode, Resistor, Spec};
2292    let mut len = None;
2293    let mut lumps = None;
2294    for p in params {
2295        match (p.name.as_str(), &p.value) {
2296            ("l", ParamValue::Num(v)) => len = Some(*v),
2297            // `URClumps = value->rValue` into an int: truncation.
2298            ("n", ParamValue::Num(v)) => lumps = Some(v.trunc()),
2299            _ => return Err(format!("`{name}`: unknown parameter ({})", p.name)),
2300        }
2301    }
2302    let Some(len) = len.filter(|l| *l > 0.0 && l.is_finite()) else {
2303        return Err(format!("`{name}`: URC line needs a length L > 0"));
2304    };
2305    let p = m.k;
2306    if p.is_nan() || p <= 1.0 || !p.is_finite() {
2307        return Err(format!(
2308            "`{name}`: URC model K = {p} must exceed 1 (the lump ratio)"
2309        ));
2310    }
2311    let r0 = len * m.rperl;
2312    let c0 = len * m.cperl;
2313    let i0 = len * m.isperl.unwrap_or(0.0);
2314    let n = match lumps {
2315        Some(n) => n,
2316        None => {
2317            let wnorm = m.fmax * r0 * c0 * 2.0 * std::f64::consts::PI;
2318            let est = (wnorm * (((p - 1.0) / p) * ((p - 1.0) / p))).ln() / p.ln();
2319            // `MAX(3.0, …)` then the int conversion; `wnorm < 35`
2320            // forces 3.
2321            let est = if 3.0 > est { 3.0 } else { est };
2322            if wnorm < 35.0 { 3.0 } else { est.trunc() }
2323        }
2324    };
2325    if n.is_nan() || n < 1.0 || n > URC_MAX_LUMPS as f64 {
2326        return Err(format!(
2327            "`{name}`: URC line with {n} lumps (must be 1..={URC_MAX_LUMPS})"
2328        ));
2329    }
2330    let n = n as i64;
2331    let r1 = (r0 * (p - 1.0)) / ((2.0 * p.powf(n as f64)) - 2.0);
2332    let c1 = (c0 * (p - 1.0)) / (p.powf((n - 1) as f64) * (p + 1.0) - 2.0);
2333    let i1 = (i0 * (p - 1.0)) / (p.powf((n - 1) as f64) * (p + 1.0) - 2.0);
2334    let rd = len * n as f64 * m.rsperl;
2335    let diode_model = m.isperl.map(|_| DiodeModel {
2336        cjo: c1,
2337        rs: rd,
2338        is: i1,
2339        ..DiodeModel::default()
2340    });
2341
2342    let gnd = nodes[2].clone();
2343    let mut lowl = nodes[0].clone();
2344    let mut hir = nodes[1].clone();
2345    let mut prop = 1.0;
2346    let mut out: Vec<Spec> = Vec::new();
2347    for i in 1..=n {
2348        let hil = format!("{name}#hi{i}");
2349        let lowr = if i == n {
2350            hil.clone()
2351        } else {
2352            format!("{name}#lo{i}")
2353        };
2354        let r = prop * r1;
2355        let c = prop * c1;
2356        out.push(
2357            Resistor {
2358                name: format!("{name}#rlo{i}"),
2359                nodes: [lowl.clone(), lowr.clone()],
2360                value: r,
2361            }
2362            .into(),
2363        );
2364        out.push(
2365            Resistor {
2366                name: format!("{name}#rhi{i}"),
2367                nodes: [hil.clone(), hir.clone()],
2368                value: r,
2369            }
2370            .into(),
2371        );
2372        let shunt = |tag: &str, node: &str| -> Spec {
2373            match &diode_model {
2374                Some(dm) => Diode {
2375                    name: format!("{name}#d{tag}{i}"),
2376                    nodes: [node.to_string(), gnd.clone()],
2377                    model: dm.clone(),
2378                    area: prop,
2379                    off: false,
2380                    temp: None,
2381                    ic: None,
2382                }
2383                .into(),
2384                None => Capacitor {
2385                    name: format!("{name}#c{tag}{i}"),
2386                    nodes: [node.to_string(), gnd.clone()],
2387                    value: c,
2388                    ic: None,
2389                }
2390                .into(),
2391            }
2392        };
2393        out.push(shunt("lo", &lowr));
2394        if i != n {
2395            out.push(shunt("hi", &hil));
2396        }
2397        prop *= p;
2398        lowl = lowr;
2399        hir = hil;
2400    }
2401    Ok(out)
2402}