1use 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#[derive(Debug, thiserror::Error)]
27pub enum ImportError {
28 #[error("SPICE parse error: {0}")]
30 Parse(#[from] thevenin_types::ParseError),
31
32 #[error("unsupported element: {0}")]
34 UnsupportedElement(String),
35
36 #[error("unknown model kind: {0}")]
38 UnknownModelKind(String),
39
40 #[error("model not found: {0}")]
42 ModelNotFound(String),
43
44 #[error("source not found: {0}")]
46 SourceNotFound(String),
47
48 #[error("unevaluable expression: {0}")]
50 UnevaluableExpr(String),
51
52 #[error("subcircuit flattening error: {0}")]
54 SubcktError(#[from] thevenin::subckt::SubcktError),
55}
56
57struct 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 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 fn mark_global(&mut self, names: &[String]) {
92 for name in names {
93 self.globals.push(name.clone());
94 self.intern(name);
96 }
97 }
98
99 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
118fn 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
136fn 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
158fn 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
232fn 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
241fn 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
256fn 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
272fn 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
284fn 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
296fn 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
308fn 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
320pub fn import_netlist(netlist: &Netlist) -> Result<Circuit, ImportError> {
326 let flat_netlist = thevenin::subckt::flatten_netlist(netlist)?;
328 let netlist = &flat_netlist;
329
330 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, };
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 let mut net_table = NetTable::new();
358
359 for item in &netlist.items {
361 if let Item::Global(nodes) = item {
362 net_table.mark_global(nodes);
363 }
364 }
365
366 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 intern_analysis_nodes(&netlist.analysis, &mut net_table);
375
376 let mut element_name_to_id: HashMap<String, Id> = HashMap::new();
378
379 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 let ir_analyses = convert_analysis(&netlist.analysis, &element_name_to_id, &mut net_table)?;
404
405 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 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 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 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 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 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 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 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
506pub fn import_spice(source: &str) -> Result<Vec<Circuit>, ImportError> {
508 let netlists = Netlist::parse(source)?;
509 netlists.iter().map(import_netlist).collect()
510}
511
512fn 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 }
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
675fn 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 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 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 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 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 (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 Ok(None)
1207 }
1208 }
1209}
1210
1211fn 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#[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 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 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 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 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 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 assert!(matches!(
1563 map_device_type("nmos"),
1564 Ok(cirq_ir::DeviceType::Nmos)
1565 ));
1566 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 assert_eq!(c.elements.len(), 3);
1609
1610 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 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 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 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 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 let netlists_out = cirq_frontend::to_netlist::circuit_to_netlists(&ir).unwrap();
1849 let nl_out = &netlists_out[0];
1850
1851 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 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 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 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 assert_eq!(a1.connections.len(), 1);
1964 assert_eq!(a1.connections[0].terminal, "c0");
1965 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 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 assert_eq!(c.elements.len(), 6);
1996
1997 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 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 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 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 assert!(c.elements.iter().any(|e| e.name == "V1"));
2076 assert!(c.elements.iter().any(|e| e.name == "R_load"));
2077 }
2078}