spice/
lib.rs

1//! SPICE netlist exporter.
2#![warn(missing_docs)]
3
4use crate::parser::conv::ScirConverter;
5use crate::parser::{Dialect, ParsedSpice, Parser};
6
7use arcstr::ArcStr;
8use itertools::Itertools;
9use rust_decimal::Decimal;
10use scir::schema::{FromSchema, NoSchema, NoSchemaError, Schema};
11use scir::{Instance, Library, NetlistLibConversion, ParamValue, SliceOnePath};
12use std::collections::{HashMap, HashSet};
13use std::fmt::{Display, Formatter};
14use std::path::{Path, PathBuf};
15use substrate::block::Block;
16use substrate::schematic::pex::StringPathSchema;
17use substrate::schematic::{CellBuilder, Schematic};
18use substrate::types::TwoTerminalIo;
19use unicase::UniCase;
20
21pub mod netlist;
22pub mod parser;
23#[cfg(test)]
24mod tests;
25
26/// The SPICE schema.
27pub struct Spice;
28
29impl Spice {
30    /// Converts [`ParsedSpice`] to a [`Library`].
31    pub fn scir_lib_from_parsed(parsed: &ParsedSpice) -> Library<Spice> {
32        let conv = ScirConverter::new(&parsed.ast);
33        conv.convert().unwrap()
34    }
35
36    /// Converts a SPICE string to a [`Library`].
37    pub fn scir_lib_from_str(source: &str) -> Library<Spice> {
38        let parsed = Parser::parse(Dialect::Spice, source).unwrap();
39        Spice::scir_lib_from_parsed(&parsed)
40    }
41
42    /// Converts a SPICE file to a [`Library`].
43    pub fn scir_lib_from_file(path: impl AsRef<Path>) -> Library<Spice> {
44        let parsed = Parser::parse_file(Dialect::Spice, path).unwrap();
45        Spice::scir_lib_from_parsed(&parsed)
46    }
47
48    /// Converts [`ParsedSpice`] to an unconnected [`ScirBinding`](substrate::schematic::ScirBinding)
49    /// associated with the cell named `cell_name`.
50    pub fn scir_cell_from_parsed(
51        parsed: &ParsedSpice,
52        cell_name: &str,
53    ) -> substrate::schematic::ScirBinding<Spice> {
54        let lib = Spice::scir_lib_from_parsed(parsed);
55        let cell_id = lib.cell_id_named(cell_name);
56        substrate::schematic::ScirBinding::new(lib, cell_id)
57    }
58
59    /// Converts a SPICE string to an unconnected [`ScirBinding`](substrate::schematic::ScirBinding)
60    /// associated with the cell named `cell_name`.
61    pub fn scir_cell_from_str(
62        source: &str,
63        cell_name: &str,
64    ) -> substrate::schematic::ScirBinding<Spice> {
65        let parsed = Parser::parse(Dialect::Spice, source).unwrap();
66        Spice::scir_cell_from_parsed(&parsed, cell_name)
67    }
68
69    /// Converts a SPICE file to an unconnected [`ScirBinding`](substrate::schematic::ScirBinding)
70    /// associated with the cell named `cell_name`.
71    pub fn scir_cell_from_file(
72        path: impl AsRef<Path>,
73        cell_name: &str,
74    ) -> substrate::schematic::ScirBinding<Spice> {
75        let parsed = Parser::parse_file(Dialect::Spice, path).unwrap();
76        Spice::scir_cell_from_parsed(&parsed, cell_name)
77    }
78
79    /// Converts a [`SliceOnePath`] to a Spice path string corresponding to the associated
80    /// node voltage.
81    pub fn node_voltage_path(
82        lib: &Library<Spice>,
83        conv: &NetlistLibConversion,
84        path: &SliceOnePath,
85    ) -> String {
86        Self::node_path_with_separator(lib, conv, path, ".")
87    }
88
89    /// Converts a [`SliceOnePath`] to a Spice path string corresponding to the associated
90    /// node voltage, using the given instance prefix hierarchy separator.
91    pub fn node_path_with_separator(
92        lib: &Library<Spice>,
93        conv: &NetlistLibConversion,
94        path: &SliceOnePath,
95        sep: &str,
96    ) -> String {
97        let path = lib.convert_slice_one_path_with_conv(conv, path.clone(), |name, index| {
98            if let Some(index) = index {
99                arcstr::format!("{}\\[{}\\]", name, index)
100            } else {
101                name.clone()
102            }
103        });
104        path.iter().join(sep)
105    }
106}
107
108impl StringPathSchema for Spice {
109    fn node_path(lib: &Library<Self>, conv: &NetlistLibConversion, path: &SliceOnePath) -> String {
110        Self::node_path_with_separator(lib, conv, path, "/")
111    }
112}
113
114impl Schema for Spice {
115    type Primitive = Primitive;
116}
117
118impl FromSchema<NoSchema> for Spice {
119    type Error = NoSchemaError;
120
121    fn convert_primitive(
122        _primitive: <NoSchema as Schema>::Primitive,
123    ) -> Result<<Self as Schema>::Primitive, Self::Error> {
124        Err(NoSchemaError)
125    }
126
127    fn convert_instance(
128        _instance: &mut Instance,
129        _primitive: &<NoSchema as Schema>::Primitive,
130    ) -> Result<(), Self::Error> {
131        Err(NoSchemaError)
132    }
133}
134
135/// The value of a component.
136#[derive(Debug, Clone)]
137pub enum ComponentValue {
138    /// The component has a fixed, known, numeric value.
139    Fixed(Decimal),
140    /// The component value is computed by a SPICE model.
141    Model(ArcStr),
142}
143
144impl Display for ComponentValue {
145    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
146        match self {
147            ComponentValue::Fixed(value) => write!(f, "{value}"),
148            ComponentValue::Model(model) => write!(f, "{model}"),
149        }
150    }
151}
152
153/// A SPICE primitive.
154#[derive(Debug, Clone)]
155pub enum Primitive {
156    /// A resistor primitive with ports "1" and "2" and value `value`.
157    Res2 {
158        /// The resistor value.
159        value: ComponentValue,
160        /// Parameters associated with the resistor.
161        params: HashMap<UniCase<ArcStr>, ParamValue>,
162    },
163    /// A capacitor primitive with ports "1" and "2" and value `value`.
164    Cap2 {
165        /// The capacitor value.
166        value: Decimal,
167    },
168    /// A diode primitive with ports "1" and "2".
169    Diode2 {
170        /// The name of the diode model.
171        model: ArcStr,
172        /// Parameters associated with the diode.
173        params: HashMap<UniCase<ArcStr>, ParamValue>,
174    },
175    /// A BJT primitive with ports "NC", "NB", and "NE".
176    ///
177    /// Optionally has the port "NS".
178    Bjt {
179        /// The name of the BJT model.
180        model: ArcStr,
181        /// Parameters associated with the BJT.
182        params: HashMap<UniCase<ArcStr>, ParamValue>,
183        /// Whether the primitive has a substrate port.
184        has_substrate_port: bool,
185    },
186    /// A MOS primitive with ports "D", "G", "S", and "B".
187    Mos {
188        /// The name of the MOS model.
189        model: ArcStr,
190        /// Parameters associated with the MOS primitive.
191        params: HashMap<UniCase<ArcStr>, ParamValue>,
192    },
193    /// A raw instance with an associated cell.
194    RawInstance {
195        /// The ordered ports of the instance.
196        ports: Vec<ArcStr>,
197        /// The associated cell.
198        cell: ArcStr,
199        /// Parameters associated with the raw instance.
200        params: HashMap<UniCase<ArcStr>, ParamValue>,
201    },
202    /// A raw instance with an associated cell.
203    ///
204    /// Creates the corresponding SUBCKT with the given body.
205    RawInstanceWithCell {
206        /// The ordered ports of the instance.
207        ports: Vec<ArcStr>,
208        /// The associated cell.
209        cell: ArcStr,
210        /// Parameters associated with the raw instance.
211        params: HashMap<UniCase<ArcStr>, ParamValue>,
212        /// The body of the associated cell.
213        body: ArcStr,
214    },
215    /// An instance with blackboxed contents.
216    BlackboxInstance {
217        /// The contents of the cell.
218        contents: BlackboxContents,
219    },
220    /// A raw instance with an associated cell in a SPICE netlist.
221    ///
222    /// Parameters are not supported.
223    RawInstanceWithInclude {
224        /// The name of the associated cell.
225        cell: ArcStr,
226        /// The path to the included netlist.
227        netlist: PathBuf,
228        /// The ordered ports of the instance.
229        ports: Vec<ArcStr>,
230    },
231}
232
233/// Contents of a blackboxed instance.
234#[derive(Debug, Clone)]
235pub struct BlackboxContents {
236    /// The elements that make up this blackbox.
237    pub elems: Vec<BlackboxElement>,
238}
239
240impl BlackboxContents {
241    /// Pushes an new element to the blackbox.
242    pub fn push(&mut self, elem: impl Into<BlackboxElement>) {
243        self.elems.push(elem.into());
244    }
245}
246
247/// An element of a blackbox instance.
248#[derive(Debug, Clone)]
249pub enum BlackboxElement {
250    /// A placeholder for the instance's name.
251    InstanceName,
252    /// A raw blackbox string.
253    RawString(ArcStr),
254    /// A port of the SCIR instantiation of this blackbox.
255    Port(ArcStr),
256}
257
258impl FromIterator<BlackboxElement> for BlackboxContents {
259    fn from_iter<T: IntoIterator<Item = BlackboxElement>>(iter: T) -> Self {
260        Self {
261            elems: iter.into_iter().collect(),
262        }
263    }
264}
265
266impl From<BlackboxElement> for BlackboxContents {
267    fn from(value: BlackboxElement) -> Self {
268        Self { elems: vec![value] }
269    }
270}
271
272impl<T: Into<ArcStr>> From<T> for BlackboxContents {
273    fn from(value: T) -> Self {
274        Self {
275            elems: vec![BlackboxElement::RawString(value.into())],
276        }
277    }
278}
279
280impl<T: Into<ArcStr>> From<T> for BlackboxElement {
281    fn from(value: T) -> Self {
282        Self::RawString(value.into())
283    }
284}
285
286impl Primitive {
287    /// Returns the ports for a given [`Primitive`].
288    pub fn ports(&self) -> Vec<ArcStr> {
289        match self {
290            Primitive::Res2 { .. } => vec!["1".into(), "2".into()],
291            Primitive::Cap2 { .. } => vec!["1".into(), "2".into()],
292            Primitive::Diode2 { .. } => vec!["1".into(), "2".into()],
293            Primitive::Bjt {
294                has_substrate_port, ..
295            } => {
296                if *has_substrate_port {
297                    vec!["NC".into(), "NB".into(), "NE".into(), "NS".into()]
298                } else {
299                    vec!["NC".into(), "NB".into(), "NE".into()]
300                }
301            }
302            Primitive::Mos { .. } => vec!["D".into(), "G".into(), "S".into(), "B".into()],
303            Primitive::RawInstance { ports, .. } => ports.clone(),
304            Primitive::RawInstanceWithCell { ports, .. } => ports.clone(),
305            Primitive::RawInstanceWithInclude { ports, .. } => ports.clone(),
306            Primitive::BlackboxInstance { contents } => contents
307                .elems
308                .iter()
309                .filter_map(|x| {
310                    if let BlackboxElement::Port(p) = x {
311                        Some(p.clone())
312                    } else {
313                        None
314                    }
315                })
316                .collect::<HashSet<_>>()
317                .into_iter()
318                .collect(),
319        }
320    }
321}
322
323/// An ideal 2-terminal resistor.
324#[derive(Clone, Copy, Hash, PartialEq, Eq, Block)]
325#[substrate(io = "TwoTerminalIo")]
326pub struct Resistor {
327    /// The resistor value.
328    value: Decimal,
329}
330impl Resistor {
331    /// Create a new resistor with the given value.
332    #[inline]
333    pub fn new(value: impl Into<Decimal>) -> Self {
334        Self {
335            value: value.into(),
336        }
337    }
338
339    /// The value of the resistor.
340    #[inline]
341    pub fn value(&self) -> Decimal {
342        self.value
343    }
344}
345
346impl Schematic for Resistor {
347    type Schema = Spice;
348    type NestedData = ();
349
350    fn schematic(
351        &self,
352        io: &substrate::types::schematic::IoNodeBundle<Self>,
353        cell: &mut CellBuilder<<Self as Schematic>::Schema>,
354    ) -> substrate::error::Result<Self::NestedData> {
355        let mut prim = substrate::schematic::PrimitiveBinding::new(Primitive::Res2 {
356            value: ComponentValue::Fixed(self.value()),
357            params: Default::default(),
358        });
359        prim.connect("1", io.p);
360        prim.connect("2", io.n);
361        cell.set_primitive(prim);
362        Ok(())
363    }
364}