Skip to main content

cirq_spice_import/
lib.rs

1//! SPICE import — converts `thevenin_types::Netlist` into `cirq_ir::Circuit`.
2//!
3//! This crate provides the bridge from legacy SPICE netlists into the canonical
4//! Cirq IR. It enables gradual migration: existing SPICE files can be imported
5//! into the Cirq toolchain without manual rewriting.
6
7use std::collections::HashMap;
8
9use cirq_ir::{
10    AcAnalysis, AcSpec as IrAcSpec, Analysis as IrAnalysis, BehavioralMode, Circuit, Connection,
11    DcAnalysis, DcSweep as IrDcSweep, Element as IrElement, ElementKind as IrElementKind,
12    FrequencyScale, Id, Model as IrModel, Net, NoiseAnalysis, PzAnalysis, PzType, ResolvedParam,
13    SensAnalysis, SourceSpec, TfAnalysis, TranAnalysis, TransferType, Value,
14    Waveform as IrWaveform, XspiceConnection as IrXspiceConnection,
15};
16use thevenin_types::{
17    AcVariation, Analysis as SpiceAnalysis, ElementKind as SpiceElementKind, Expr, Item, Netlist,
18    Param, PzAnalysisType, PzInputType,
19};
20
21// ---------------------------------------------------------------------------
22// Error type
23// ---------------------------------------------------------------------------
24
25/// Errors that can occur during SPICE-to-Cirq import.
26#[derive(Debug, thiserror::Error)]
27pub enum ImportError {
28    /// The underlying SPICE parser failed.
29    #[error("SPICE parse error: {0}")]
30    Parse(#[from] thevenin_types::ParseError),
31
32    /// An element type that has no Cirq IR equivalent was encountered.
33    #[error("unsupported element: {0}")]
34    UnsupportedElement(String),
35
36    /// A model kind string could not be mapped to a `DeviceType`.
37    #[error("unknown model kind: {0}")]
38    UnknownModelKind(String),
39
40    /// A model referenced by an element was not found in the netlist.
41    #[error("model not found: {0}")]
42    ModelNotFound(String),
43
44    /// A source referenced in an analysis command was not found.
45    #[error("source not found: {0}")]
46    SourceNotFound(String),
47
48    /// An expression could not be evaluated to a numeric value.
49    #[error("unevaluable expression: {0}")]
50    UnevaluableExpr(String),
51
52    /// Subcircuit flattening failed.
53    #[error("subcircuit flattening error: {0}")]
54    SubcktError(#[from] thevenin::subckt::SubcktError),
55}
56
57// ---------------------------------------------------------------------------
58// Net interning table
59// ---------------------------------------------------------------------------
60
61/// Assigns unique `Id`s to node name strings. Ground ("0") always gets `Id(0)`.
62struct NetTable {
63    map: HashMap<String, Id>,
64    next_id: u32,
65    globals: Vec<String>,
66}
67
68impl NetTable {
69    fn new() -> Self {
70        let mut map = HashMap::new();
71        map.insert("0".to_owned(), Id(0));
72        Self {
73            map,
74            next_id: 1,
75            globals: Vec::new(),
76        }
77    }
78
79    /// Intern a node name, returning its `Id`. Creates a new entry if unseen.
80    fn intern(&mut self, name: &str) -> Id {
81        if let Some(&id) = self.map.get(name) {
82            return id;
83        }
84        let id = Id(self.next_id);
85        self.next_id += 1;
86        self.map.insert(name.to_owned(), id);
87        id
88    }
89
90    /// Mark a set of node names as global.
91    fn mark_global(&mut self, names: &[String]) {
92        for name in names {
93            self.globals.push(name.clone());
94            // Ensure the node is interned.
95            self.intern(name);
96        }
97    }
98
99    /// Produce the final `Vec<Net>`, sorted by id.
100    fn into_nets(self) -> Vec<Net> {
101        let mut nets: Vec<Net> = self
102            .map
103            .iter()
104            .map(|(name, &id)| {
105                let is_global = name == "0" || self.globals.iter().any(|g| g == name);
106                Net {
107                    id,
108                    name: name.clone(),
109                    is_global,
110                }
111            })
112            .collect();
113        nets.sort_by_key(|n| n.id.0);
114        nets
115    }
116}
117
118// ---------------------------------------------------------------------------
119// Helpers
120// ---------------------------------------------------------------------------
121
122/// Try to extract a numeric f64 from an `Expr`. Returns `Err` for parameter
123/// references and brace expressions that require evaluation.
124fn expr_to_f64(expr: &Expr) -> Result<f64, ImportError> {
125    match expr {
126        Expr::Num(v) => Ok(*v),
127        Expr::Param(name) => Err(ImportError::UnevaluableExpr(format!(
128            "parameter reference: {name}"
129        ))),
130        Expr::Brace(s) => Err(ImportError::UnevaluableExpr(format!(
131            "brace expression: {{{s}}}"
132        ))),
133    }
134}
135
136/// Build a `SourceSpec` from a `thevenin_types::Source`.
137fn build_source_spec(source: &thevenin_types::Source) -> Result<SourceSpec, ImportError> {
138    let dc = source.dc.as_ref().and_then(|e| expr_to_f64(e).ok());
139    let ac = source
140        .ac
141        .as_ref()
142        .map(|ac| {
143            Ok::<IrAcSpec, ImportError>(IrAcSpec {
144                mag: expr_to_f64(&ac.mag).unwrap_or(0.0),
145                phase: ac
146                    .phase
147                    .as_ref()
148                    .map(expr_to_f64)
149                    .transpose()?
150                    .unwrap_or(0.0),
151            })
152        })
153        .transpose()?;
154    let waveform = source.waveform.as_ref().map(convert_waveform).transpose()?;
155    Ok(SourceSpec { dc, ac, waveform })
156}
157
158/// Convert a `thevenin_types::Waveform` to an `IrWaveform`.
159fn convert_waveform(w: &thevenin_types::Waveform) -> Result<IrWaveform, ImportError> {
160    match w {
161        thevenin_types::Waveform::Pulse {
162            v1,
163            v2,
164            td,
165            tr,
166            tf,
167            pw,
168            per,
169        } => Ok(IrWaveform::Pulse {
170            v1: expr_to_f64(v1)?,
171            v2: expr_to_f64(v2)?,
172            td: td.as_ref().map(expr_to_f64).transpose()?,
173            tr: tr.as_ref().map(expr_to_f64).transpose()?,
174            tf: tf.as_ref().map(expr_to_f64).transpose()?,
175            pw: pw.as_ref().map(expr_to_f64).transpose()?,
176            per: per.as_ref().map(expr_to_f64).transpose()?,
177        }),
178        thevenin_types::Waveform::Sin {
179            v0,
180            va,
181            freq,
182            td,
183            theta,
184            phi,
185        } => Ok(IrWaveform::Sin {
186            v0: expr_to_f64(v0)?,
187            va: expr_to_f64(va)?,
188            freq: freq.as_ref().map(expr_to_f64).transpose()?,
189            td: td.as_ref().map(expr_to_f64).transpose()?,
190            theta: theta.as_ref().map(expr_to_f64).transpose()?,
191            phi: phi.as_ref().map(expr_to_f64).transpose()?,
192        }),
193        thevenin_types::Waveform::Exp {
194            v1,
195            v2,
196            td1,
197            tau1,
198            td2,
199            tau2,
200        } => Ok(IrWaveform::Exp {
201            v1: expr_to_f64(v1)?,
202            v2: expr_to_f64(v2)?,
203            td1: td1.as_ref().map(expr_to_f64).transpose()?,
204            tau1: tau1.as_ref().map(expr_to_f64).transpose()?,
205            td2: td2.as_ref().map(expr_to_f64).transpose()?,
206            tau2: tau2.as_ref().map(expr_to_f64).transpose()?,
207        }),
208        thevenin_types::Waveform::Pwl(points) => {
209            let pairs = points
210                .iter()
211                .map(|pt| Ok((expr_to_f64(&pt.time)?, expr_to_f64(&pt.value)?)))
212                .collect::<Result<Vec<(f64, f64)>, ImportError>>()?;
213            Ok(IrWaveform::Pwl(pairs))
214        }
215        thevenin_types::Waveform::Sffm { v0, va, fc, fs, md } => Ok(IrWaveform::Sffm {
216            v0: expr_to_f64(v0)?,
217            va: expr_to_f64(va)?,
218            fc: fc.as_ref().map(expr_to_f64).transpose()?,
219            fs: fs.as_ref().map(expr_to_f64).transpose()?,
220            md: md.as_ref().map(expr_to_f64).transpose()?,
221        }),
222        thevenin_types::Waveform::Am { va, vo, fc, fs, td } => Ok(IrWaveform::Am {
223            va: expr_to_f64(va)?,
224            vo: expr_to_f64(vo)?,
225            fc: expr_to_f64(fc)?,
226            fs: expr_to_f64(fs)?,
227            td: td.as_ref().map(expr_to_f64).transpose()?,
228        }),
229    }
230}
231
232/// Convert an `Expr` to a `Value`.
233fn expr_to_value(expr: &Expr) -> Value {
234    match expr {
235        Expr::Num(v) => Value::Real(*v),
236        Expr::Param(s) => Value::String(s.clone()),
237        Expr::Brace(s) => Value::String(format!("{{{s}}}")),
238    }
239}
240
241/// Convert a slice of `thevenin_types::Param` to Cirq IR param pairs.
242fn convert_params(params: &[Param]) -> Vec<(String, Value)> {
243    params
244        .iter()
245        .map(|p| (p.name.clone(), expr_to_value(&p.value)))
246        .collect()
247}
248
249fn connection(terminal: &str, net: Id) -> Connection {
250    Connection {
251        terminal: terminal.to_owned(),
252        net,
253    }
254}
255
256/// Map a SPICE model-kind string (e.g. "NPN", "D", "NMOS") to a `DeviceType`.
257fn map_device_type(kind: &str) -> Result<cirq_ir::DeviceType, ImportError> {
258    match kind.to_ascii_uppercase().as_str() {
259        "D" => Ok(cirq_ir::DeviceType::Diode),
260        "NPN" => Ok(cirq_ir::DeviceType::Npn),
261        "PNP" => Ok(cirq_ir::DeviceType::Pnp),
262        "NMOS" => Ok(cirq_ir::DeviceType::Nmos),
263        "PMOS" => Ok(cirq_ir::DeviceType::Pmos),
264        "NJF" => Ok(cirq_ir::DeviceType::NJfet),
265        "PJF" => Ok(cirq_ir::DeviceType::PJfet),
266        "NMF" | "GASFET" | "MESA" => Ok(cirq_ir::DeviceType::NMesfet),
267        "PMF" => Ok(cirq_ir::DeviceType::PMesfet),
268        other => Err(ImportError::UnknownModelKind(other.to_owned())),
269    }
270}
271
272/// Determine `ElementKind` for a BJT based on its model type.
273fn bjt_kind(
274    model_name: &str,
275    model_table: &HashMap<String, cirq_ir::DeviceType>,
276) -> Result<IrElementKind, ImportError> {
277    match model_table.get(&model_name.to_ascii_uppercase()) {
278        Some(cirq_ir::DeviceType::Pnp) => Ok(IrElementKind::Pnp),
279        Some(cirq_ir::DeviceType::Npn) | Some(_) => Ok(IrElementKind::Npn),
280        None => Err(ImportError::ModelNotFound(model_name.to_owned())),
281    }
282}
283
284/// Determine `ElementKind` for a MOSFET based on its model type.
285fn mosfet_kind(
286    model_name: &str,
287    model_table: &HashMap<String, cirq_ir::DeviceType>,
288) -> Result<IrElementKind, ImportError> {
289    match model_table.get(&model_name.to_ascii_uppercase()) {
290        Some(cirq_ir::DeviceType::Pmos) => Ok(IrElementKind::Pmos),
291        Some(cirq_ir::DeviceType::Nmos) | Some(_) => Ok(IrElementKind::Nmos),
292        None => Err(ImportError::ModelNotFound(model_name.to_owned())),
293    }
294}
295
296/// Determine `ElementKind` for a JFET based on its model type.
297fn jfet_kind(
298    model_name: &str,
299    model_table: &HashMap<String, cirq_ir::DeviceType>,
300) -> Result<IrElementKind, ImportError> {
301    match model_table.get(&model_name.to_ascii_uppercase()) {
302        Some(cirq_ir::DeviceType::PJfet) => Ok(IrElementKind::PJfet),
303        Some(cirq_ir::DeviceType::NJfet) | Some(_) => Ok(IrElementKind::NJfet),
304        None => Err(ImportError::ModelNotFound(model_name.to_owned())),
305    }
306}
307
308/// Determine `ElementKind` for a MESFET based on its model type.
309fn mesfet_kind(
310    model_name: &str,
311    model_table: &HashMap<String, cirq_ir::DeviceType>,
312) -> Result<IrElementKind, ImportError> {
313    match model_table.get(&model_name.to_ascii_uppercase()) {
314        Some(cirq_ir::DeviceType::PMesfet) => Ok(IrElementKind::PMesfet),
315        Some(cirq_ir::DeviceType::NMesfet) | Some(_) => Ok(IrElementKind::NMesfet),
316        None => Err(ImportError::ModelNotFound(model_name.to_owned())),
317    }
318}
319
320// ---------------------------------------------------------------------------
321// Main import function
322// ---------------------------------------------------------------------------
323
324/// Convert a parsed `thevenin_types::Netlist` into a `cirq_ir::Circuit`.
325pub fn import_netlist(netlist: &Netlist) -> Result<Circuit, ImportError> {
326    // 0. Flatten subcircuit calls so all elements are at the top level.
327    let flat_netlist = thevenin::subckt::flatten_netlist(netlist)?;
328    let netlist = &flat_netlist;
329
330    // 1. Build model table: model name (uppercased) → DeviceType.
331    //    Also collect model IR objects.
332    let mut model_type_table: HashMap<String, cirq_ir::DeviceType> = HashMap::new();
333    let mut ir_models: Vec<IrModel> = Vec::new();
334    let mut model_id_counter: u32 = 0;
335    let mut model_id_table: HashMap<String, Id> = HashMap::new();
336
337    for item in &netlist.items {
338        if let Item::Model(mdef) = item {
339            let device_type = match map_device_type(&mdef.kind) {
340                Ok(dt) => dt,
341                Err(_) => continue, // skip unknown model kinds
342            };
343            let id = Id(model_id_counter);
344            model_id_counter += 1;
345            model_type_table.insert(mdef.name.to_ascii_uppercase(), device_type);
346            model_id_table.insert(mdef.name.to_ascii_uppercase(), id);
347            ir_models.push(IrModel {
348                id,
349                name: mdef.name.clone(),
350                device_type,
351                params: convert_params(&mdef.params),
352            });
353        }
354    }
355
356    // 2. Discover nets: scan all elements for node names.
357    let mut net_table = NetTable::new();
358
359    // Also handle .global directives.
360    for item in &netlist.items {
361        if let Item::Global(nodes) = item {
362            net_table.mark_global(nodes);
363        }
364    }
365
366    // Pre-scan elements for node names.
367    for item in &netlist.items {
368        if let Item::Element(elem) = item {
369            intern_element_nodes(&elem.kind, &mut net_table);
370        }
371    }
372
373    // Also intern nodes referenced in analyses (e.g. PZ node names).
374    intern_analysis_nodes(&netlist.analysis, &mut net_table);
375
376    // 3. Build element name → Id table for source lookups in analyses.
377    let mut element_name_to_id: HashMap<String, Id> = HashMap::new();
378
379    // 4. Convert elements.
380    let mut ir_elements: Vec<IrElement> = Vec::new();
381    let mut elem_id_counter: u32 = 0;
382
383    for item in &netlist.items {
384        let elem = match item {
385            Item::Element(e) => e,
386            _ => continue,
387        };
388
389        let id = Id(elem_id_counter);
390        elem_id_counter += 1;
391
392        element_name_to_id.insert(elem.name.to_ascii_uppercase(), id);
393
394        let ir_elem =
395            convert_element(id, elem, &mut net_table, &model_type_table, &model_id_table)?;
396
397        if let Some(e) = ir_elem {
398            ir_elements.push(e);
399        }
400    }
401
402    // 5. Convert analysis.
403    let ir_analyses = convert_analysis(&netlist.analysis, &element_name_to_id, &mut net_table)?;
404
405    // 6. Collect .param items.
406    let mut ir_params: Vec<ResolvedParam> = Vec::new();
407    for item in &netlist.items {
408        if let Item::Param(params) = item {
409            for p in params {
410                ir_params.push(ResolvedParam {
411                    name: p.name.clone(),
412                    value: expr_to_value(&p.value),
413                });
414            }
415        }
416    }
417
418    // 7. Collect .options items.
419    let mut ir_options: Vec<(String, cirq_ir::Value)> = Vec::new();
420    for item in &netlist.items {
421        if let Item::Options(params) = item {
422            for p in params {
423                let val = expr_to_value(&p.value);
424                if let Some(existing) = ir_options.iter_mut().find(|o| o.0 == p.name) {
425                    existing.1 = val;
426                } else {
427                    ir_options.push((p.name.clone(), val));
428                }
429            }
430        }
431    }
432
433    // 8. Collect .temp.
434    let mut ir_temp: Option<f64> = None;
435    for item in &netlist.items {
436        if let Item::Temp(t) = item {
437            ir_temp = Some(*t);
438        }
439    }
440
441    // 9. Collect .save targets.
442    let mut ir_save: Vec<String> = Vec::new();
443    for item in &netlist.items {
444        if let Item::Save(targets) = item {
445            for t in targets {
446                if !ir_save.contains(t) {
447                    ir_save.push(t.clone());
448                }
449            }
450        }
451    }
452
453    // 10. Collect .func definitions.
454    let mut ir_funcs: Vec<cirq_ir::FuncDef> = Vec::new();
455    for item in &netlist.items {
456        if let Item::Func { name, args, body } = item {
457            ir_funcs.push(cirq_ir::FuncDef {
458                name: name.clone(),
459                args: args.clone(),
460                body: body.clone(),
461            });
462        }
463    }
464
465    // 11. Collect .ic initial conditions.
466    let mut ir_initial_conditions: Vec<(cirq_ir::Id, f64)> = Vec::new();
467    for item in &netlist.items {
468        if let Item::Ic(pairs) = item {
469            for (node_name, val) in pairs {
470                let net_id = net_table.intern(node_name);
471                ir_initial_conditions.push((net_id, *val));
472            }
473        }
474    }
475
476    // 12. Collect .control blocks as code blocks with "control" language tag.
477    let mut ir_code_blocks: Vec<cirq_ir::CodeBlock> = Vec::new();
478    for item in &netlist.items {
479        if let Item::Control(lines) = item {
480            ir_code_blocks.push(cirq_ir::CodeBlock {
481                language: "control".to_owned(),
482                lines: lines.clone(),
483            });
484        }
485    }
486
487    // 13. Build circuit.
488    let nets = net_table.into_nets();
489
490    Ok(Circuit {
491        name: netlist.title.clone(),
492        nets,
493        elements: ir_elements,
494        models: ir_models,
495        analyses: ir_analyses,
496        params: ir_params,
497        options: ir_options,
498        temp: ir_temp,
499        save: ir_save,
500        funcs: ir_funcs,
501        initial_conditions: ir_initial_conditions,
502        code_blocks: ir_code_blocks,
503    })
504}
505
506/// Parse SPICE source text and convert each resulting netlist into a `Circuit`.
507pub fn import_spice(source: &str) -> Result<Vec<Circuit>, ImportError> {
508    let netlists = Netlist::parse(source)?;
509    netlists.iter().map(import_netlist).collect()
510}
511
512// ---------------------------------------------------------------------------
513// Node interning for elements
514// ---------------------------------------------------------------------------
515
516fn intern_element_nodes(kind: &SpiceElementKind, nets: &mut NetTable) {
517    match kind {
518        SpiceElementKind::Resistor { pos, neg, .. }
519        | SpiceElementKind::Capacitor { pos, neg, .. }
520        | SpiceElementKind::Inductor { pos, neg, .. }
521        | SpiceElementKind::VoltageSource { pos, neg, .. }
522        | SpiceElementKind::CurrentSource { pos, neg, .. }
523        | SpiceElementKind::BehavioralSource { pos, neg, .. } => {
524            nets.intern(pos);
525            nets.intern(neg);
526        }
527        SpiceElementKind::Diode { anode, cathode, .. } => {
528            nets.intern(anode);
529            nets.intern(cathode);
530        }
531        SpiceElementKind::Bjt {
532            c, b, e, substrate, ..
533        } => {
534            nets.intern(c);
535            nets.intern(b);
536            nets.intern(e);
537            if let Some(sub) = substrate {
538                nets.intern(sub);
539            }
540        }
541        SpiceElementKind::Mosfet {
542            d,
543            g,
544            s,
545            bulk,
546            body,
547            ..
548        } => {
549            nets.intern(d);
550            nets.intern(g);
551            nets.intern(s);
552            nets.intern(bulk);
553            if let Some(b) = body {
554                nets.intern(b);
555            }
556        }
557        SpiceElementKind::Jfet { d, g, s, .. } | SpiceElementKind::Mesa { d, g, s, .. } => {
558            nets.intern(d);
559            nets.intern(g);
560            nets.intern(s);
561        }
562        SpiceElementKind::MutualCoupling { .. } => {
563            // Coupling references inductor names, not nodes directly.
564        }
565        SpiceElementKind::Vcvs {
566            out_pos,
567            out_neg,
568            in_pos,
569            in_neg,
570            ..
571        }
572        | SpiceElementKind::Vccs {
573            out_pos,
574            out_neg,
575            in_pos,
576            in_neg,
577            ..
578        } => {
579            nets.intern(out_pos);
580            nets.intern(out_neg);
581            nets.intern(in_pos);
582            nets.intern(in_neg);
583        }
584        SpiceElementKind::Ccvs {
585            out_pos, out_neg, ..
586        }
587        | SpiceElementKind::Cccs {
588            out_pos, out_neg, ..
589        } => {
590            nets.intern(out_pos);
591            nets.intern(out_neg);
592        }
593        SpiceElementKind::SubcktCall { ports, .. } => {
594            for p in ports {
595                nets.intern(p);
596            }
597        }
598        SpiceElementKind::Ltra {
599            pos1,
600            neg1,
601            pos2,
602            neg2,
603            ..
604        }
605        | SpiceElementKind::Txl {
606            pos1,
607            neg1,
608            pos2,
609            neg2,
610            ..
611        } => {
612            nets.intern(pos1);
613            nets.intern(neg1);
614            nets.intern(pos2);
615            nets.intern(neg2);
616        }
617        SpiceElementKind::Cpl {
618            in_nodes,
619            out_nodes,
620            gnd,
621            ..
622        } => {
623            for n in in_nodes {
624                nets.intern(n);
625            }
626            for n in out_nodes {
627                nets.intern(n);
628            }
629            nets.intern(gnd);
630        }
631        SpiceElementKind::Xspice { connections, .. } => {
632            for conn in connections {
633                match conn {
634                    thevenin_types::XspiceConnection::Scalar(s) => {
635                        nets.intern(s);
636                    }
637                    thevenin_types::XspiceConnection::Array(arr) => {
638                        for s in arr {
639                            nets.intern(s);
640                        }
641                    }
642                }
643            }
644        }
645        SpiceElementKind::Raw(_) => {}
646    }
647}
648
649fn intern_analysis_nodes(analysis: &SpiceAnalysis, nets: &mut NetTable) {
650    match analysis {
651        SpiceAnalysis::Noise {
652            output, ref_node, ..
653        } => {
654            nets.intern(output);
655            if let Some(r) = ref_node {
656                nets.intern(r);
657            }
658        }
659        SpiceAnalysis::Pz {
660            node_i,
661            node_g,
662            node_j,
663            node_k,
664            ..
665        } => {
666            nets.intern(node_i);
667            nets.intern(node_g);
668            nets.intern(node_j);
669            nets.intern(node_k);
670        }
671        _ => {}
672    }
673}
674
675// ---------------------------------------------------------------------------
676// Element conversion
677// ---------------------------------------------------------------------------
678
679/// Convert a single SPICE element to an IR element. Returns `None` for elements
680/// that are intentionally skipped (e.g., subcircuit calls).
681fn convert_element(
682    id: Id,
683    elem: &thevenin_types::Element,
684    nets: &mut NetTable,
685    model_types: &HashMap<String, cirq_ir::DeviceType>,
686    model_ids: &HashMap<String, Id>,
687) -> Result<Option<IrElement>, ImportError> {
688    let name = &elem.name;
689
690    match &elem.kind {
691        SpiceElementKind::Resistor {
692            pos,
693            neg,
694            value,
695            params,
696        } => {
697            let mut ir_params = vec![("value".to_owned(), expr_to_value(value))];
698            ir_params.extend(convert_params(params));
699            Ok(Some(IrElement {
700                id,
701                name: name.clone(),
702                kind: IrElementKind::Resistor,
703                connections: vec![
704                    connection("pos", nets.intern(pos)),
705                    connection("neg", nets.intern(neg)),
706                ],
707                params: ir_params,
708                model: None,
709                source_spec: None,
710            }))
711        }
712
713        SpiceElementKind::Capacitor {
714            pos,
715            neg,
716            value,
717            params,
718        } => {
719            let mut ir_params = vec![("value".to_owned(), expr_to_value(value))];
720            ir_params.extend(convert_params(params));
721            Ok(Some(IrElement {
722                id,
723                name: name.clone(),
724                kind: IrElementKind::Capacitor,
725                connections: vec![
726                    connection("pos", nets.intern(pos)),
727                    connection("neg", nets.intern(neg)),
728                ],
729                params: ir_params,
730                model: None,
731                source_spec: None,
732            }))
733        }
734
735        SpiceElementKind::Inductor {
736            pos,
737            neg,
738            value,
739            params,
740        } => {
741            let mut ir_params = vec![("value".to_owned(), expr_to_value(value))];
742            ir_params.extend(convert_params(params));
743            Ok(Some(IrElement {
744                id,
745                name: name.clone(),
746                kind: IrElementKind::Inductor,
747                connections: vec![
748                    connection("pos", nets.intern(pos)),
749                    connection("neg", nets.intern(neg)),
750                ],
751                params: ir_params,
752                model: None,
753                source_spec: None,
754            }))
755        }
756
757        SpiceElementKind::VoltageSource { pos, neg, source } => {
758            let mut ir_params = Vec::new();
759            if let Some(dc) = &source.dc {
760                ir_params.push(("dc".to_owned(), expr_to_value(dc)));
761            }
762            if let Some(ac) = &source.ac {
763                ir_params.push(("ac_mag".to_owned(), expr_to_value(&ac.mag)));
764                if let Some(phase) = &ac.phase {
765                    ir_params.push(("ac_phase".to_owned(), expr_to_value(phase)));
766                }
767            }
768            let source_spec = Some(build_source_spec(source)?);
769            Ok(Some(IrElement {
770                id,
771                name: name.clone(),
772                kind: IrElementKind::VoltageSource,
773                connections: vec![
774                    connection("pos", nets.intern(pos)),
775                    connection("neg", nets.intern(neg)),
776                ],
777                params: ir_params,
778                model: None,
779                source_spec,
780            }))
781        }
782
783        SpiceElementKind::CurrentSource { pos, neg, source } => {
784            let mut ir_params = Vec::new();
785            if let Some(dc) = &source.dc {
786                ir_params.push(("dc".to_owned(), expr_to_value(dc)));
787            }
788            if let Some(ac) = &source.ac {
789                ir_params.push(("ac_mag".to_owned(), expr_to_value(&ac.mag)));
790                if let Some(phase) = &ac.phase {
791                    ir_params.push(("ac_phase".to_owned(), expr_to_value(phase)));
792                }
793            }
794            let source_spec = Some(build_source_spec(source)?);
795            Ok(Some(IrElement {
796                id,
797                name: name.clone(),
798                kind: IrElementKind::CurrentSource,
799                connections: vec![
800                    connection("pos", nets.intern(pos)),
801                    connection("neg", nets.intern(neg)),
802                ],
803                params: ir_params,
804                model: None,
805                source_spec,
806            }))
807        }
808
809        SpiceElementKind::Diode {
810            anode,
811            cathode,
812            model,
813            params,
814        } => {
815            let model_id = model_ids.get(&model.to_ascii_uppercase()).copied();
816            Ok(Some(IrElement {
817                id,
818                name: name.clone(),
819                kind: IrElementKind::Diode,
820                connections: vec![
821                    connection("anode", nets.intern(anode)),
822                    connection("cathode", nets.intern(cathode)),
823                ],
824                params: convert_params(params),
825                model: model_id,
826                source_spec: None,
827            }))
828        }
829
830        SpiceElementKind::Bjt {
831            c,
832            b,
833            e,
834            substrate,
835            model,
836            params,
837            off,
838        } => {
839            let kind = bjt_kind(model, model_types)?;
840            let model_id = model_ids.get(&model.to_ascii_uppercase()).copied();
841            let mut conns = vec![
842                connection("collector", nets.intern(c)),
843                connection("base", nets.intern(b)),
844                connection("emitter", nets.intern(e)),
845            ];
846            if let Some(sub) = substrate {
847                conns.push(connection("substrate", nets.intern(sub)));
848            }
849            let mut ir_params = convert_params(params);
850            if *off {
851                ir_params.push(("off".to_owned(), Value::Bool(true)));
852            }
853            Ok(Some(IrElement {
854                id,
855                name: name.clone(),
856                kind,
857                connections: conns,
858                params: ir_params,
859                model: model_id,
860                source_spec: None,
861            }))
862        }
863
864        SpiceElementKind::Mosfet {
865            d,
866            g,
867            s,
868            bulk,
869            body,
870            model,
871            params,
872        } => {
873            let kind = mosfet_kind(model, model_types)?;
874            let model_id = model_ids.get(&model.to_ascii_uppercase()).copied();
875            let mut conns = vec![
876                connection("drain", nets.intern(d)),
877                connection("gate", nets.intern(g)),
878                connection("source", nets.intern(s)),
879                connection("bulk", nets.intern(bulk)),
880            ];
881            if let Some(b) = body {
882                conns.push(connection("body", nets.intern(b)));
883            }
884            Ok(Some(IrElement {
885                id,
886                name: name.clone(),
887                kind,
888                connections: conns,
889                params: convert_params(params),
890                model: model_id,
891                source_spec: None,
892            }))
893        }
894
895        SpiceElementKind::Jfet {
896            d,
897            g,
898            s,
899            model,
900            params,
901        } => {
902            let kind = jfet_kind(model, model_types)?;
903            let model_id = model_ids.get(&model.to_ascii_uppercase()).copied();
904            Ok(Some(IrElement {
905                id,
906                name: name.clone(),
907                kind,
908                connections: vec![
909                    connection("drain", nets.intern(d)),
910                    connection("gate", nets.intern(g)),
911                    connection("source", nets.intern(s)),
912                ],
913                params: convert_params(params),
914                model: model_id,
915                source_spec: None,
916            }))
917        }
918
919        SpiceElementKind::Mesa {
920            d,
921            g,
922            s,
923            model,
924            params,
925        } => {
926            // MESA devices map to MESFET kind based on model, defaulting to NMesfet.
927            let kind = mesfet_kind(model, model_types).unwrap_or(IrElementKind::NMesfet);
928            let model_id = model_ids.get(&model.to_ascii_uppercase()).copied();
929            Ok(Some(IrElement {
930                id,
931                name: name.clone(),
932                kind,
933                connections: vec![
934                    connection("drain", nets.intern(d)),
935                    connection("gate", nets.intern(g)),
936                    connection("source", nets.intern(s)),
937                ],
938                params: convert_params(params),
939                model: model_id,
940                source_spec: None,
941            }))
942        }
943
944        SpiceElementKind::MutualCoupling { l1, l2, coupling } => {
945            // Coupling references inductor element names, not net nodes.
946            // We store the inductor names as string params.
947            Ok(Some(IrElement {
948                id,
949                name: name.clone(),
950                kind: IrElementKind::Coupling,
951                connections: Vec::new(),
952                params: vec![
953                    ("l1".to_owned(), Value::String(l1.clone())),
954                    ("l2".to_owned(), Value::String(l2.clone())),
955                    ("coupling".to_owned(), expr_to_value(coupling)),
956                ],
957                model: None,
958                source_spec: None,
959            }))
960        }
961
962        SpiceElementKind::Vcvs {
963            out_pos,
964            out_neg,
965            in_pos,
966            in_neg,
967            gain,
968        } => Ok(Some(IrElement {
969            id,
970            name: name.clone(),
971            kind: IrElementKind::Vcvs,
972            connections: vec![
973                connection("out_pos", nets.intern(out_pos)),
974                connection("out_neg", nets.intern(out_neg)),
975                connection("in_pos", nets.intern(in_pos)),
976                connection("in_neg", nets.intern(in_neg)),
977            ],
978            params: vec![("gain".to_owned(), expr_to_value(gain))],
979            model: None,
980            source_spec: None,
981        })),
982
983        SpiceElementKind::Vccs {
984            out_pos,
985            out_neg,
986            in_pos,
987            in_neg,
988            gm,
989        } => Ok(Some(IrElement {
990            id,
991            name: name.clone(),
992            kind: IrElementKind::Vccs,
993            connections: vec![
994                connection("out_pos", nets.intern(out_pos)),
995                connection("out_neg", nets.intern(out_neg)),
996                connection("in_pos", nets.intern(in_pos)),
997                connection("in_neg", nets.intern(in_neg)),
998            ],
999            params: vec![("gm".to_owned(), expr_to_value(gm))],
1000            model: None,
1001            source_spec: None,
1002        })),
1003
1004        SpiceElementKind::Ccvs {
1005            out_pos,
1006            out_neg,
1007            vsrc,
1008            rm,
1009        } => Ok(Some(IrElement {
1010            id,
1011            name: name.clone(),
1012            kind: IrElementKind::Ccvs,
1013            connections: vec![
1014                connection("out_pos", nets.intern(out_pos)),
1015                connection("out_neg", nets.intern(out_neg)),
1016            ],
1017            params: vec![
1018                ("vsrc".to_owned(), Value::String(vsrc.clone())),
1019                ("rm".to_owned(), expr_to_value(rm)),
1020            ],
1021            model: None,
1022            source_spec: None,
1023        })),
1024
1025        SpiceElementKind::Cccs {
1026            out_pos,
1027            out_neg,
1028            vsrc,
1029            gain,
1030        } => Ok(Some(IrElement {
1031            id,
1032            name: name.clone(),
1033            kind: IrElementKind::Cccs,
1034            connections: vec![
1035                connection("out_pos", nets.intern(out_pos)),
1036                connection("out_neg", nets.intern(out_neg)),
1037            ],
1038            params: vec![
1039                ("vsrc".to_owned(), Value::String(vsrc.clone())),
1040                ("gain".to_owned(), expr_to_value(gain)),
1041            ],
1042            model: None,
1043            source_spec: None,
1044        })),
1045
1046        SpiceElementKind::Ltra {
1047            pos1,
1048            neg1,
1049            pos2,
1050            neg2,
1051            model,
1052            params,
1053        } => {
1054            let model_id = model_ids.get(&model.to_ascii_uppercase()).copied();
1055            Ok(Some(IrElement {
1056                id,
1057                name: name.clone(),
1058                kind: IrElementKind::TransmissionLine,
1059                connections: vec![
1060                    connection("in_pos", nets.intern(pos1)),
1061                    connection("in_neg", nets.intern(neg1)),
1062                    connection("out_pos", nets.intern(pos2)),
1063                    connection("out_neg", nets.intern(neg2)),
1064                ],
1065                params: convert_params(params),
1066                model: model_id,
1067                source_spec: None,
1068            }))
1069        }
1070
1071        SpiceElementKind::Txl {
1072            pos1,
1073            neg1,
1074            pos2,
1075            neg2,
1076            model,
1077            params,
1078        } => {
1079            let model_id = model_ids.get(&model.to_ascii_uppercase()).copied();
1080            Ok(Some(IrElement {
1081                id,
1082                name: name.clone(),
1083                kind: IrElementKind::Txl,
1084                connections: vec![
1085                    connection("in_pos", nets.intern(pos1)),
1086                    connection("in_neg", nets.intern(neg1)),
1087                    connection("out_pos", nets.intern(pos2)),
1088                    connection("out_neg", nets.intern(neg2)),
1089                ],
1090                params: convert_params(params),
1091                model: model_id,
1092                source_spec: None,
1093            }))
1094        }
1095
1096        SpiceElementKind::SubcktCall { subckt, .. } => {
1097            // If we reach here, flatten_netlist() didn't fully resolve this
1098            // call — the subcircuit definition is missing or flattening is
1099            // incomplete.  Report an error rather than silently dropping
1100            // the element (which would corrupt the circuit topology).
1101            Err(ImportError::UnsupportedElement(format!(
1102                "{name} (unresolved subcircuit call to `{subckt}`)"
1103            )))
1104        }
1105
1106        SpiceElementKind::BehavioralSource { pos, neg, spec } => {
1107            let spec_trimmed = spec.trim();
1108            // Parse "V=expr" or "I=expr" to determine mode and extract expression.
1109            let (mode, expr_str) = if let Some(rest) = spec_trimmed
1110                .strip_prefix("V=")
1111                .or_else(|| spec_trimmed.strip_prefix("v="))
1112            {
1113                (BehavioralMode::Voltage, rest.trim().to_owned())
1114            } else if let Some(rest) = spec_trimmed
1115                .strip_prefix("I=")
1116                .or_else(|| spec_trimmed.strip_prefix("i="))
1117            {
1118                (BehavioralMode::Current, rest.trim().to_owned())
1119            } else {
1120                // Default to voltage mode with the full spec as the expression.
1121                (BehavioralMode::Voltage, spec_trimmed.to_owned())
1122            };
1123            Ok(Some(IrElement {
1124                id,
1125                name: name.clone(),
1126                kind: IrElementKind::BehavioralSource {
1127                    mode,
1128                    spec: expr_str,
1129                },
1130                connections: vec![
1131                    connection("pos", nets.intern(pos)),
1132                    connection("neg", nets.intern(neg)),
1133                ],
1134                params: Vec::new(),
1135                model: None,
1136                source_spec: None,
1137            }))
1138        }
1139
1140        SpiceElementKind::Cpl {
1141            in_nodes,
1142            out_nodes,
1143            gnd,
1144            model,
1145            params,
1146        } => {
1147            let width = in_nodes.len();
1148            let mut conns = Vec::new();
1149            for (i, n) in in_nodes.iter().enumerate() {
1150                conns.push(connection(&format!("in{i}"), nets.intern(n)));
1151            }
1152            conns.push(connection("gnd", nets.intern(gnd)));
1153            for (i, n) in out_nodes.iter().enumerate() {
1154                conns.push(connection(&format!("out{i}"), nets.intern(n)));
1155            }
1156            let mut ir_params = convert_params(params);
1157            ir_params.push(("model".to_owned(), Value::String(model.clone())));
1158            Ok(Some(IrElement {
1159                id,
1160                name: name.clone(),
1161                kind: IrElementKind::CoupledLine { width },
1162                connections: conns,
1163                params: ir_params,
1164                model: None,
1165                source_spec: None,
1166            }))
1167        }
1168
1169        SpiceElementKind::Xspice { connections, model } => {
1170            let mut ir_conns: Vec<Connection> = Vec::new();
1171            let mut xspice_conns: Vec<IrXspiceConnection> = Vec::new();
1172            let mut scalar_idx = 0usize;
1173
1174            for conn_spec in connections {
1175                match conn_spec {
1176                    thevenin_types::XspiceConnection::Scalar(s) => {
1177                        let net_id = nets.intern(s);
1178                        ir_conns.push(connection(&format!("c{scalar_idx}"), net_id));
1179                        xspice_conns.push(IrXspiceConnection::Scalar(net_id));
1180                        scalar_idx += 1;
1181                    }
1182                    thevenin_types::XspiceConnection::Array(arr) => {
1183                        let ids: Vec<Id> = arr.iter().map(|s| nets.intern(s)).collect();
1184                        xspice_conns.push(IrXspiceConnection::Array(ids));
1185                    }
1186                }
1187            }
1188
1189            Ok(Some(IrElement {
1190                id,
1191                name: name.clone(),
1192                kind: IrElementKind::Xspice {
1193                    connections: xspice_conns,
1194                },
1195                connections: ir_conns,
1196                params: vec![("model".to_owned(), Value::String(model.clone()))],
1197                model: None,
1198                source_spec: None,
1199            }))
1200        }
1201
1202        SpiceElementKind::Raw(_) => {
1203            // Unrecognized element — skip gracefully rather than failing the
1204            // entire import.  The element is lost but the rest of the circuit
1205            // can still be simulated.
1206            Ok(None)
1207        }
1208    }
1209}
1210
1211// ---------------------------------------------------------------------------
1212// Analysis conversion
1213// ---------------------------------------------------------------------------
1214
1215fn convert_analysis(
1216    analysis: &SpiceAnalysis,
1217    element_names: &HashMap<String, Id>,
1218    nets: &mut NetTable,
1219) -> Result<Vec<IrAnalysis>, ImportError> {
1220    let ir = match analysis {
1221        SpiceAnalysis::Op => IrAnalysis::Op,
1222
1223        SpiceAnalysis::Dc {
1224            src,
1225            start,
1226            stop,
1227            step,
1228            src2,
1229        } => {
1230            let src_id = element_names
1231                .get(&src.to_ascii_uppercase())
1232                .copied()
1233                .ok_or_else(|| ImportError::SourceNotFound(src.clone()))?;
1234            let mut sweeps = vec![IrDcSweep {
1235                source: src_id,
1236                start: expr_to_f64(start)?,
1237                stop: expr_to_f64(stop)?,
1238                step: expr_to_f64(step)?,
1239            }];
1240            if let Some(s2) = src2 {
1241                let s2_id = element_names
1242                    .get(&s2.src.to_ascii_uppercase())
1243                    .copied()
1244                    .ok_or_else(|| ImportError::SourceNotFound(s2.src.clone()))?;
1245                sweeps.push(IrDcSweep {
1246                    source: s2_id,
1247                    start: expr_to_f64(&s2.start)?,
1248                    stop: expr_to_f64(&s2.stop)?,
1249                    step: expr_to_f64(&s2.step)?,
1250                });
1251            }
1252            IrAnalysis::Dc(DcAnalysis { sweeps })
1253        }
1254
1255        SpiceAnalysis::Ac {
1256            variation,
1257            n,
1258            fstart,
1259            fstop,
1260        } => {
1261            let scale = match variation {
1262                AcVariation::Dec => FrequencyScale::Decade,
1263                AcVariation::Oct => FrequencyScale::Octave,
1264                AcVariation::Lin => FrequencyScale::Linear,
1265            };
1266            IrAnalysis::Ac(AcAnalysis {
1267                start: expr_to_f64(fstart)?,
1268                stop: expr_to_f64(fstop)?,
1269                points: *n,
1270                scale,
1271            })
1272        }
1273
1274        SpiceAnalysis::Tran {
1275            tstep,
1276            tstop,
1277            tstart,
1278            tmax,
1279            uic,
1280        } => IrAnalysis::Tran(TranAnalysis {
1281            step: expr_to_f64(tstep)?,
1282            stop: expr_to_f64(tstop)?,
1283            start: tstart.as_ref().map(expr_to_f64).transpose()?.unwrap_or(0.0),
1284            uic: *uic,
1285            tmax: tmax.as_ref().and_then(|e| expr_to_f64(e).ok()),
1286        }),
1287
1288        SpiceAnalysis::Noise {
1289            output,
1290            ref_node,
1291            src,
1292            variation,
1293            n,
1294            fstart,
1295            fstop,
1296        } => {
1297            let output_id = nets.intern(output);
1298            let ref_id = ref_node.as_ref().map(|r| nets.intern(r)).unwrap_or(Id(0));
1299            let src_id = element_names
1300                .get(&src.to_ascii_uppercase())
1301                .copied()
1302                .ok_or_else(|| ImportError::SourceNotFound(src.clone()))?;
1303            let scale = match variation {
1304                AcVariation::Dec => FrequencyScale::Decade,
1305                AcVariation::Oct => FrequencyScale::Octave,
1306                AcVariation::Lin => FrequencyScale::Linear,
1307            };
1308            IrAnalysis::Noise(NoiseAnalysis {
1309                output_net: output_id,
1310                reference_net: ref_id,
1311                source: src_id,
1312                start: expr_to_f64(fstart)?,
1313                stop: expr_to_f64(fstop)?,
1314                points: *n,
1315                scale,
1316            })
1317        }
1318
1319        SpiceAnalysis::Tf { output, input } => {
1320            let src_id = element_names
1321                .get(&input.to_ascii_uppercase())
1322                .copied()
1323                .ok_or_else(|| ImportError::SourceNotFound(input.clone()))?;
1324            IrAnalysis::Tf(TfAnalysis {
1325                output: output.clone(),
1326                source: src_id,
1327            })
1328        }
1329
1330        SpiceAnalysis::Sens { output } => IrAnalysis::Sens(SensAnalysis {
1331            output: output.join(", "),
1332        }),
1333
1334        SpiceAnalysis::Pz {
1335            node_i,
1336            node_g,
1337            node_j,
1338            node_k,
1339            input_type,
1340            analysis_type,
1341        } => {
1342            let transfer = match input_type {
1343                PzInputType::Vol => TransferType::Voltage,
1344                PzInputType::Cur => TransferType::Current,
1345            };
1346            let pz_type = match analysis_type {
1347                PzAnalysisType::Pol => PzType::Poles,
1348                PzAnalysisType::Zer => PzType::Zeros,
1349                PzAnalysisType::Pz => PzType::Both,
1350            };
1351            IrAnalysis::Pz(PzAnalysis {
1352                input_pos: nets.intern(node_i),
1353                input_neg: nets.intern(node_g),
1354                output_pos: nets.intern(node_j),
1355                output_neg: nets.intern(node_k),
1356                transfer,
1357                analysis_type: pz_type,
1358            })
1359        }
1360    };
1361
1362    Ok(vec![ir])
1363}
1364
1365// ---------------------------------------------------------------------------
1366// Tests
1367// ---------------------------------------------------------------------------
1368
1369#[cfg(test)]
1370mod tests {
1371    use super::*;
1372
1373    #[test]
1374    fn passive_network_two_resistors() {
1375        let spice = "\
1376Passive network
1377R1 a 0 1k
1378R2 a b 2k
1379.op
1380.end
1381";
1382        let circuits = import_spice(spice).unwrap();
1383        assert_eq!(circuits.len(), 1);
1384        let c = &circuits[0];
1385
1386        // 3 nets: 0, a, b
1387        assert_eq!(c.nets.len(), 3);
1388        let ground = c.nets.iter().find(|n| n.name == "0").unwrap();
1389        assert_eq!(ground.id, Id(0));
1390        assert!(ground.is_global);
1391
1392        // 2 elements
1393        assert_eq!(c.elements.len(), 2);
1394
1395        let r1 = c.elements.iter().find(|e| e.name == "R1").unwrap();
1396        assert!(matches!(r1.kind, IrElementKind::Resistor));
1397        assert_eq!(r1.connections.len(), 2);
1398        // Value param
1399        let value_param = r1.params.iter().find(|p| p.0 == "value").unwrap();
1400        match &value_param.1 {
1401            Value::Real(v) => assert!((v - 1000.0).abs() < 1e-6),
1402            other => panic!("expected Real, got {other:?}"),
1403        }
1404
1405        let r2 = c.elements.iter().find(|e| e.name == "R2").unwrap();
1406        assert!(matches!(r2.kind, IrElementKind::Resistor));
1407        let value_param2 = r2.params.iter().find(|p| p.0 == "value").unwrap();
1408        match &value_param2.1 {
1409            Value::Real(v) => assert!((v - 2000.0).abs() < 1e-6),
1410            other => panic!("expected Real, got {other:?}"),
1411        }
1412
1413        // Analysis is Op
1414        assert_eq!(c.analyses.len(), 1);
1415        assert!(matches!(c.analyses[0], IrAnalysis::Op));
1416    }
1417
1418    #[test]
1419    fn mos_inverter() {
1420        let spice = "\
1421MOS inverter
1422.model NMOD NMOS
1423.model PMOD PMOS
1424M1 out in vdd vdd PMOD W=10u L=1u
1425M2 out in 0 0 NMOD W=5u L=1u
1426V1 vdd 0 DC 3.3
1427.op
1428.end
1429";
1430        let circuits = import_spice(spice).unwrap();
1431        assert_eq!(circuits.len(), 1);
1432        let c = &circuits[0];
1433
1434        assert_eq!(c.models.len(), 2);
1435
1436        let m1 = c.elements.iter().find(|e| e.name == "M1").unwrap();
1437        assert!(matches!(m1.kind, IrElementKind::Pmos));
1438        assert_eq!(m1.connections.len(), 4);
1439        assert!(m1.model.is_some());
1440
1441        let m2 = c.elements.iter().find(|e| e.name == "M2").unwrap();
1442        assert!(matches!(m2.kind, IrElementKind::Nmos));
1443        assert!(m2.model.is_some());
1444    }
1445
1446    #[test]
1447    fn dc_sweep_analysis() {
1448        let spice = "\
1449DC sweep test
1450V1 in 0 DC 0
1451R1 in 0 1k
1452.dc V1 0 5 0.1
1453.end
1454";
1455        let circuits = import_spice(spice).unwrap();
1456        let c = &circuits[0];
1457
1458        assert_eq!(c.analyses.len(), 1);
1459        match &c.analyses[0] {
1460            IrAnalysis::Dc(dc) => {
1461                assert_eq!(dc.sweeps.len(), 1);
1462                let sw = &dc.sweeps[0];
1463                assert!((sw.start - 0.0).abs() < 1e-12);
1464                assert!((sw.stop - 5.0).abs() < 1e-12);
1465                assert!((sw.step - 0.1).abs() < 1e-12);
1466            }
1467            other => panic!("expected Dc, got {other:?}"),
1468        }
1469    }
1470
1471    #[test]
1472    fn ac_analysis() {
1473        let spice = "\
1474AC test
1475V1 in 0 DC 0 AC 1
1476R1 in 0 1k
1477.ac DEC 10 1 1Meg
1478.end
1479";
1480        let circuits = import_spice(spice).unwrap();
1481        let c = &circuits[0];
1482
1483        assert_eq!(c.analyses.len(), 1);
1484        match &c.analyses[0] {
1485            IrAnalysis::Ac(ac) => {
1486                assert_eq!(ac.scale, FrequencyScale::Decade);
1487                assert_eq!(ac.points, 10);
1488                assert!((ac.start - 1.0).abs() < 1e-12);
1489                assert!((ac.stop - 1e6).abs() < 1e-6);
1490            }
1491            other => panic!("expected Ac, got {other:?}"),
1492        }
1493    }
1494
1495    #[test]
1496    fn tran_analysis() {
1497        let spice = "\
1498Tran test
1499V1 in 0 DC 1
1500R1 in 0 1k
1501.tran 1n 100n
1502.end
1503";
1504        let circuits = import_spice(spice).unwrap();
1505        let c = &circuits[0];
1506
1507        match &c.analyses[0] {
1508            IrAnalysis::Tran(tran) => {
1509                assert!((tran.step - 1e-9).abs() < 1e-18);
1510                assert!((tran.stop - 100e-9).abs() < 1e-18);
1511                assert!((tran.start - 0.0).abs() < 1e-18);
1512                assert!(!tran.uic);
1513            }
1514            other => panic!("expected Tran, got {other:?}"),
1515        }
1516    }
1517
1518    #[test]
1519    fn model_mapping_all_types() {
1520        // Verify map_device_type for all known kinds.
1521        assert!(matches!(
1522            map_device_type("D"),
1523            Ok(cirq_ir::DeviceType::Diode)
1524        ));
1525        assert!(matches!(
1526            map_device_type("NPN"),
1527            Ok(cirq_ir::DeviceType::Npn)
1528        ));
1529        assert!(matches!(
1530            map_device_type("PNP"),
1531            Ok(cirq_ir::DeviceType::Pnp)
1532        ));
1533        assert!(matches!(
1534            map_device_type("NMOS"),
1535            Ok(cirq_ir::DeviceType::Nmos)
1536        ));
1537        assert!(matches!(
1538            map_device_type("PMOS"),
1539            Ok(cirq_ir::DeviceType::Pmos)
1540        ));
1541        assert!(matches!(
1542            map_device_type("NJF"),
1543            Ok(cirq_ir::DeviceType::NJfet)
1544        ));
1545        assert!(matches!(
1546            map_device_type("PJF"),
1547            Ok(cirq_ir::DeviceType::PJfet)
1548        ));
1549        assert!(matches!(
1550            map_device_type("NMF"),
1551            Ok(cirq_ir::DeviceType::NMesfet)
1552        ));
1553        assert!(matches!(
1554            map_device_type("PMF"),
1555            Ok(cirq_ir::DeviceType::PMesfet)
1556        ));
1557        assert!(matches!(
1558            map_device_type("GASFET"),
1559            Ok(cirq_ir::DeviceType::NMesfet)
1560        ));
1561        // Case insensitive
1562        assert!(matches!(
1563            map_device_type("nmos"),
1564            Ok(cirq_ir::DeviceType::Nmos)
1565        ));
1566        // Unknown
1567        assert!(map_device_type("BOGUS").is_err());
1568    }
1569
1570    #[test]
1571    fn global_nets_marked() {
1572        let spice = "\
1573Global test
1574.global vdd vss
1575R1 vdd vss 1k
1576.op
1577.end
1578";
1579        let circuits = import_spice(spice).unwrap();
1580        let c = &circuits[0];
1581
1582        let vdd = c.nets.iter().find(|n| n.name == "vdd").unwrap();
1583        assert!(vdd.is_global);
1584
1585        let vss = c.nets.iter().find(|n| n.name == "vss").unwrap();
1586        assert!(vss.is_global);
1587    }
1588
1589    #[test]
1590    fn subckt_call_expanded() {
1591        let spice = "\
1592Subckt test
1593.subckt INV in out vdd vss
1594M1 out in vdd vdd PMOD
1595M2 out in vss vss NMOD
1596.ends INV
1597.model PMOD PMOS
1598.model NMOD NMOS
1599X1 a b vcc gnd INV
1600R1 a 0 1k
1601.op
1602.end
1603";
1604        let circuits = import_spice(spice).unwrap();
1605        let c = &circuits[0];
1606
1607        // X1 subckt call is expanded; R1 + two MOSFETs from the subcircuit body.
1608        assert_eq!(c.elements.len(), 3);
1609
1610        // The expanded elements are prefixed with the instance name.
1611        // M1 uses PMOD (PMOS) and M2 uses NMOD (NMOS).
1612        let m1 = c.elements.iter().find(|e| e.name == "x1.m1").unwrap();
1613        assert!(matches!(m1.kind, IrElementKind::Pmos));
1614
1615        let m2 = c.elements.iter().find(|e| e.name == "x1.m2").unwrap();
1616        assert!(matches!(m2.kind, IrElementKind::Nmos));
1617
1618        // R1 is at top level, not prefixed.
1619        let r1 = c.elements.iter().find(|e| e.name == "R1").unwrap();
1620        assert!(matches!(r1.kind, IrElementKind::Resistor));
1621    }
1622
1623    #[test]
1624    fn voltage_source_with_dc_and_ac() {
1625        let spice = "\
1626Source test
1627V1 in 0 DC 1.5 AC 1 90
1628R1 in 0 1k
1629.op
1630.end
1631";
1632        let circuits = import_spice(spice).unwrap();
1633        let c = &circuits[0];
1634
1635        let v1 = c.elements.iter().find(|e| e.name == "V1").unwrap();
1636        assert!(matches!(v1.kind, IrElementKind::VoltageSource));
1637        let dc = v1.params.iter().find(|p| p.0 == "dc").unwrap();
1638        match &dc.1 {
1639            Value::Real(v) => assert!((v - 1.5).abs() < 1e-12),
1640            other => panic!("expected Real, got {other:?}"),
1641        }
1642        let ac_mag = v1.params.iter().find(|p| p.0 == "ac_mag").unwrap();
1643        match &ac_mag.1 {
1644            Value::Real(v) => assert!((v - 1.0).abs() < 1e-12),
1645            other => panic!("expected Real, got {other:?}"),
1646        }
1647        let ac_phase = v1.params.iter().find(|p| p.0 == "ac_phase").unwrap();
1648        match &ac_phase.1 {
1649            Value::Real(v) => assert!((v - 90.0).abs() < 1e-12),
1650            other => panic!("expected Real, got {other:?}"),
1651        }
1652    }
1653
1654    #[test]
1655    fn params_collected() {
1656        let spice = "\
1657Param test
1658.param Rval=1k Cval=10p
1659R1 a 0 1k
1660.op
1661.end
1662";
1663        let circuits = import_spice(spice).unwrap();
1664        let c = &circuits[0];
1665
1666        assert_eq!(c.params.len(), 2);
1667        assert_eq!(c.params[0].name, "Rval");
1668        assert_eq!(c.params[1].name, "Cval");
1669    }
1670
1671    #[test]
1672    fn diode_with_model() {
1673        let spice = "\
1674Diode test
1675.model D1N4148 D
1676D1 anode cathode D1N4148
1677R1 anode 0 1k
1678.op
1679.end
1680";
1681        let circuits = import_spice(spice).unwrap();
1682        let c = &circuits[0];
1683
1684        let d1 = c.elements.iter().find(|e| e.name == "D1").unwrap();
1685        assert!(matches!(d1.kind, IrElementKind::Diode));
1686        assert!(d1.model.is_some());
1687        assert_eq!(d1.connections[0].terminal, "anode");
1688        assert_eq!(d1.connections[1].terminal, "cathode");
1689    }
1690
1691    #[test]
1692    fn bjt_npn_pnp() {
1693        let spice = "\
1694BJT test
1695.model QN NPN
1696.model QP PNP
1697Q1 c1 b1 e1 QN
1698Q2 c2 b2 e2 QP
1699.op
1700.end
1701";
1702        let circuits = import_spice(spice).unwrap();
1703        let c = &circuits[0];
1704
1705        let q1 = c.elements.iter().find(|e| e.name == "Q1").unwrap();
1706        assert!(matches!(q1.kind, IrElementKind::Npn));
1707        assert_eq!(q1.connections[0].terminal, "collector");
1708        assert_eq!(q1.connections[1].terminal, "base");
1709        assert_eq!(q1.connections[2].terminal, "emitter");
1710
1711        let q2 = c.elements.iter().find(|e| e.name == "Q2").unwrap();
1712        assert!(matches!(q2.kind, IrElementKind::Pnp));
1713    }
1714
1715    #[test]
1716    fn controlled_sources() {
1717        let spice = "\
1718Controlled sources
1719E1 out1 0 in1 0 10
1720G1 out2 0 in2 0 0.5
1721R1 in1 0 1k
1722R2 in2 0 1k
1723R3 out1 0 1k
1724R4 out2 0 1k
1725.op
1726.end
1727";
1728        let circuits = import_spice(spice).unwrap();
1729        let c = &circuits[0];
1730
1731        let e1 = c.elements.iter().find(|e| e.name == "E1").unwrap();
1732        assert!(matches!(e1.kind, IrElementKind::Vcvs));
1733        assert_eq!(e1.connections.len(), 4);
1734        let gain = e1.params.iter().find(|p| p.0 == "gain").unwrap();
1735        match &gain.1 {
1736            Value::Real(v) => assert!((v - 10.0).abs() < 1e-12),
1737            other => panic!("expected Real, got {other:?}"),
1738        }
1739
1740        let g1 = c.elements.iter().find(|e| e.name == "G1").unwrap();
1741        assert!(matches!(g1.kind, IrElementKind::Vccs));
1742    }
1743
1744    #[test]
1745    fn circuit_name_is_title() {
1746        let spice = "\
1747My Great Circuit
1748R1 a 0 1k
1749.op
1750.end
1751";
1752        let circuits = import_spice(spice).unwrap();
1753        assert_eq!(circuits[0].name, "My Great Circuit");
1754    }
1755
1756    #[test]
1757    fn ground_always_id_zero() {
1758        let spice = "\
1759Ground test
1760R1 a 0 1k
1761.op
1762.end
1763";
1764        let circuits = import_spice(spice).unwrap();
1765        let c = &circuits[0];
1766
1767        let ground = c.nets.iter().find(|n| n.id == Id(0)).unwrap();
1768        assert_eq!(ground.name, "0");
1769        assert!(ground.is_global);
1770    }
1771
1772    #[test]
1773    fn mesa_maps_to_mesfet() {
1774        let spice = "\
1775MESFET test
1776.model ZM1 NMF
1777Z1 d g s ZM1
1778R1 d 0 1k
1779.op
1780.end
1781";
1782        let circuits = import_spice(spice).unwrap();
1783        let c = &circuits[0];
1784        let z1 = c.elements.iter().find(|e| e.name == "Z1").unwrap();
1785        assert!(matches!(z1.kind, IrElementKind::NMesfet));
1786        assert!(z1.model.is_some());
1787    }
1788
1789    #[test]
1790    fn pmesa_maps_to_pmesfet() {
1791        let spice = "\
1792PMESFET test
1793.model ZM1 PMF
1794Z1 d g s ZM1
1795R1 d 0 1k
1796.op
1797.end
1798";
1799        let circuits = import_spice(spice).unwrap();
1800        let c = &circuits[0];
1801        let z1 = c.elements.iter().find(|e| e.name == "Z1").unwrap();
1802        assert!(matches!(z1.kind, IrElementKind::PMesfet));
1803        assert!(z1.model.is_some());
1804
1805        // Verify the model was imported with the correct device type.
1806        let model = c.models.iter().find(|m| m.name == "ZM1").unwrap();
1807        assert_eq!(model.device_type, cirq_ir::DeviceType::PMesfet);
1808    }
1809
1810    #[test]
1811    fn mesfet_round_trip() {
1812        // Build a SPICE Netlist with a Mesa element, import it to IR, convert back
1813        // to Netlist, and verify the Mesa element survives the round trip.
1814        use thevenin_types::{Analysis, Element, ElementKind as SK, Expr, Item, ModelDef, Param};
1815
1816        let netlist = Netlist {
1817            title: "MESFET round-trip".to_string(),
1818            items: vec![
1819                Item::Model(ModelDef {
1820                    name: "mesmod".to_string(),
1821                    kind: "NMF".to_string(),
1822                    params: vec![Param {
1823                        name: "vto".to_string(),
1824                        value: Expr::Num(-1.3),
1825                    }],
1826                }),
1827                Item::Element(Element {
1828                    name: "Z1".to_string(),
1829                    kind: SK::Mesa {
1830                        d: "drain".to_string(),
1831                        g: "gate".to_string(),
1832                        s: "0".to_string(),
1833                        model: "mesmod".to_string(),
1834                        params: vec![],
1835                    },
1836                }),
1837            ],
1838            analysis: Analysis::Op,
1839            source: String::new(),
1840        };
1841
1842        // Step 1: Import to IR.
1843        let ir = import_netlist(&netlist).unwrap();
1844        let z1_ir = ir.elements.iter().find(|e| e.name == "Z1").unwrap();
1845        assert!(matches!(z1_ir.kind, IrElementKind::NMesfet));
1846
1847        // Step 2: Convert IR back to Netlist.
1848        let netlists_out = cirq_frontend::to_netlist::circuit_to_netlists(&ir).unwrap();
1849        let nl_out = &netlists_out[0];
1850
1851        // Step 3: Verify the Mesa element survived.
1852        let z1_out = nl_out
1853            .items
1854            .iter()
1855            .find_map(|i| {
1856                if let Item::Element(e) = i {
1857                    if e.name == "Z1" { Some(e) } else { None }
1858                } else {
1859                    None
1860                }
1861            })
1862            .expect("Z1 should survive round-trip");
1863        match &z1_out.kind {
1864            SK::Mesa { d, g, s, model, .. } => {
1865                assert_eq!(d, "drain");
1866                assert_eq!(g, "gate");
1867                assert_eq!(s, "0");
1868                assert_eq!(model, "mesmod");
1869            }
1870            other => panic!("expected Mesa, got {other:?}"),
1871        }
1872
1873        // Step 4: Verify model survived.
1874        let model_out = nl_out.items.iter().find_map(|i| {
1875            if let Item::Model(m) = i {
1876                if m.name == "mesmod" { Some(m) } else { None }
1877            } else {
1878                None
1879            }
1880        });
1881        assert!(model_out.is_some());
1882        assert_eq!(model_out.unwrap().kind, "NMF");
1883    }
1884
1885    #[test]
1886    fn cpl_element_imported() {
1887        use thevenin_types::{Analysis, Element, ElementKind as SK};
1888
1889        let netlist = Netlist {
1890            title: "CPL test".to_string(),
1891            items: vec![Item::Element(Element {
1892                name: "P1".to_string(),
1893                kind: SK::Cpl {
1894                    in_nodes: vec!["n1".to_string(), "n2".to_string()],
1895                    out_nodes: vec!["n3".to_string(), "n4".to_string()],
1896                    gnd: "0".to_string(),
1897                    model: "cpl_mod".to_string(),
1898                    params: vec![],
1899                },
1900            })],
1901            analysis: Analysis::Op,
1902            source: String::new(),
1903        };
1904
1905        let circuit = import_netlist(&netlist).unwrap();
1906        let p1 = circuit.elements.iter().find(|e| e.name == "P1").unwrap();
1907        match &p1.kind {
1908            IrElementKind::CoupledLine { width } => assert_eq!(*width, 2),
1909            other => panic!("expected CoupledLine, got {other:?}"),
1910        }
1911        // Check connections: in0, in1, gnd, out0, out1
1912        assert_eq!(p1.connections.len(), 5);
1913        assert_eq!(p1.connections[0].terminal, "in0");
1914        assert_eq!(p1.connections[1].terminal, "in1");
1915        assert_eq!(p1.connections[2].terminal, "gnd");
1916        assert_eq!(p1.connections[3].terminal, "out0");
1917        assert_eq!(p1.connections[4].terminal, "out1");
1918        // Model stored as param
1919        let model_param = p1.params.iter().find(|p| p.0 == "model").unwrap();
1920        match &model_param.1 {
1921            Value::String(s) => assert_eq!(s, "cpl_mod"),
1922            other => panic!("expected String, got {other:?}"),
1923        }
1924    }
1925
1926    #[test]
1927    fn xspice_element_imported() {
1928        use thevenin_types::{Analysis, Element, ElementKind as SK, XspiceConnection};
1929
1930        let netlist = Netlist {
1931            title: "XSPICE test".to_string(),
1932            items: vec![Item::Element(Element {
1933                name: "A1".to_string(),
1934                kind: SK::Xspice {
1935                    connections: vec![
1936                        XspiceConnection::Scalar("in".to_string()),
1937                        XspiceConnection::Array(vec!["out1".to_string(), "out2".to_string()]),
1938                    ],
1939                    model: "buf_model".to_string(),
1940                },
1941            })],
1942            analysis: Analysis::Op,
1943            source: String::new(),
1944        };
1945
1946        let circuit = import_netlist(&netlist).unwrap();
1947        let a1 = circuit.elements.iter().find(|e| e.name == "A1").unwrap();
1948        match &a1.kind {
1949            IrElementKind::Xspice { connections } => {
1950                assert_eq!(connections.len(), 2);
1951                match &connections[0] {
1952                    IrXspiceConnection::Scalar(_) => {}
1953                    other => panic!("expected Scalar, got {other:?}"),
1954                }
1955                match &connections[1] {
1956                    IrXspiceConnection::Array(ids) => assert_eq!(ids.len(), 2),
1957                    other => panic!("expected Array, got {other:?}"),
1958                }
1959            }
1960            other => panic!("expected Xspice, got {other:?}"),
1961        }
1962        // Scalar connection appears in ir_conns.
1963        assert_eq!(a1.connections.len(), 1);
1964        assert_eq!(a1.connections[0].terminal, "c0");
1965        // Model stored as param.
1966        let model_param = a1.params.iter().find(|p| p.0 == "model").unwrap();
1967        match &model_param.1 {
1968            Value::String(s) => assert_eq!(s, "buf_model"),
1969            other => panic!("expected String, got {other:?}"),
1970        }
1971    }
1972
1973    #[test]
1974    fn subckt_round_trip_port_remapping() {
1975        // Verifies full round-trip: SPICE with subcircuit -> import -> IR
1976        // with correct prefix names and port remapping.
1977        let spice = "\
1978Subcircuit round-trip test
1979.subckt RBUF inp outp
1980R1 inp mid 100
1981R2 mid outp 200
1982.ends RBUF
1983X1 net_a net_b RBUF
1984X2 net_b net_c RBUF
1985V1 net_a 0 DC 5
1986R_load net_c 0 1k
1987.op
1988.end
1989";
1990        let circuits = import_spice(spice).unwrap();
1991        let c = &circuits[0];
1992
1993        // Two instances expanded: X1 produces x1.r1, x1.r2; X2 produces x2.r1, x2.r2.
1994        // Plus V1 and R_load at the top level = 6 elements total.
1995        assert_eq!(c.elements.len(), 6);
1996
1997        // Verify prefixed element names exist.
1998        let x1_r1 = c.elements.iter().find(|e| e.name == "x1.r1").unwrap();
1999        assert!(matches!(x1_r1.kind, IrElementKind::Resistor));
2000        let x1_r2 = c.elements.iter().find(|e| e.name == "x1.r2").unwrap();
2001        assert!(matches!(x1_r2.kind, IrElementKind::Resistor));
2002        let x2_r1 = c.elements.iter().find(|e| e.name == "x2.r1").unwrap();
2003        assert!(matches!(x2_r1.kind, IrElementKind::Resistor));
2004        let x2_r2 = c.elements.iter().find(|e| e.name == "x2.r2").unwrap();
2005        assert!(matches!(x2_r2.kind, IrElementKind::Resistor));
2006
2007        // Verify port remapping: x1.r1 should connect to net_a (inp->net_a)
2008        // and x1.mid (internal node), not to "inp" or "outp".
2009        let x1_r1_node_names: Vec<&str> = x1_r1
2010            .connections
2011            .iter()
2012            .map(|conn| {
2013                c.nets
2014                    .iter()
2015                    .find(|n| n.id == conn.net)
2016                    .unwrap()
2017                    .name
2018                    .as_str()
2019            })
2020            .collect();
2021        assert!(
2022            x1_r1_node_names.contains(&"net_a"),
2023            "x1.r1 should connect to net_a (remapped port)"
2024        );
2025        assert!(
2026            x1_r1_node_names.contains(&"x1.mid"),
2027            "x1.r1 should connect to x1.mid (prefixed internal node)"
2028        );
2029
2030        // x1.r2 connects x1.mid -> net_b (outp->net_b)
2031        let x1_r2_node_names: Vec<&str> = x1_r2
2032            .connections
2033            .iter()
2034            .map(|conn| {
2035                c.nets
2036                    .iter()
2037                    .find(|n| n.id == conn.net)
2038                    .unwrap()
2039                    .name
2040                    .as_str()
2041            })
2042            .collect();
2043        assert!(
2044            x1_r2_node_names.contains(&"x1.mid"),
2045            "x1.r2 should connect to x1.mid"
2046        );
2047        assert!(
2048            x1_r2_node_names.contains(&"net_b"),
2049            "x1.r2 should connect to net_b (remapped port)"
2050        );
2051
2052        // x2.r1 connects net_b -> x2.mid
2053        let x2_r1_node_names: Vec<&str> = x2_r1
2054            .connections
2055            .iter()
2056            .map(|conn| {
2057                c.nets
2058                    .iter()
2059                    .find(|n| n.id == conn.net)
2060                    .unwrap()
2061                    .name
2062                    .as_str()
2063            })
2064            .collect();
2065        assert!(
2066            x2_r1_node_names.contains(&"net_b"),
2067            "x2.r1 should connect to net_b (remapped port)"
2068        );
2069        assert!(
2070            x2_r1_node_names.contains(&"x2.mid"),
2071            "x2.r1 should connect to x2.mid"
2072        );
2073
2074        // Verify top-level elements are not prefixed.
2075        assert!(c.elements.iter().any(|e| e.name == "V1"));
2076        assert!(c.elements.iter().any(|e| e.name == "R_load"));
2077    }
2078}