Skip to main content

dune_core/
run.rs

1//! [`Circuit::run_card`]: one analysis card of the netlist IR,
2//! dispatched to its engine call. This is the single card→analysis
3//! mapping the CLI, the oracle, the wasm build and the language
4//! bindings share (directly, or through [`Circuit::run`], its
5//! plots-only form), so a deck runs the same everywhere.
6
7use dune_netlist::ir::{self, Analysis, Variation};
8
9use crate::ac::AcSweep;
10use crate::circuit::Circuit;
11use crate::dc::DcSweep;
12use crate::error::Error;
13use crate::options::Options;
14use crate::pss::{Pss, PssParams};
15use crate::pz::{PzInput, PzWhich};
16use crate::sens::SensAc;
17use crate::solution::Solution;
18use crate::tf::OutputVar;
19
20/// What one analysis card produced ([`Circuit::run_card`]).
21#[derive(Clone, Debug)]
22pub enum CardOutput {
23    /// Every card but `.pss`: its plots in rawfile order and its notes
24    /// (see [`Circuit::run`]).
25    Plots {
26        plots: Vec<Solution>,
27        notes: Vec<String>,
28    },
29    /// A `.pss` card, kept whole: besides its two plots, the period,
30    /// `t0` and the shooting history ([`Pss`]) that the plots alone do
31    /// not carry.
32    Pss(Box<Pss>),
33}
34
35impl CardOutput {
36    /// The card's plots in rawfile order and its notes — what
37    /// [`Circuit::run`] returns: a `.pss` gives its time plot, then
38    /// its spectrum.
39    pub fn into_plots(self) -> (Vec<Solution>, Vec<String>) {
40        match self {
41            CardOutput::Plots { plots, notes } => (plots, notes),
42            CardOutput::Pss(p) => {
43                let Pss {
44                    time,
45                    spectrum,
46                    notes,
47                    ..
48                } = *p;
49                (vec![time, spectrum], notes)
50            }
51        }
52    }
53}
54
55impl Circuit {
56    /// Run one analysis card (`.op`, `.dc`, `.tran`, `.ac`, `.pz`,
57    /// `.noise`, `.disto`, `.tf`, `.sens`, ngspice's `.sp`, and `.pss`) as
58    /// `spice3 -b` (`ngspice -b` for `.sp`) would.
59    ///
60    /// Returns the plots in rawfile order with their 3f5/ngspice
61    /// plotnames — one for most analyses; `.noise` gives the density
62    /// plot plus, when the band has more than one frequency, the
63    /// integrated totals; `.disto` gives 2 or 3 plots; `.pss` the
64    /// period and its harmonics — and the analysis' notes (non-fatal
65    /// remarks the caller may show). This is [`Circuit::run_card`]
66    /// with the `.pss` result reduced to its plots
67    /// ([`CardOutput::into_plots`]).
68    ///
69    /// A `.tf`/`.sens` output that is not `V(..)`/`I(..)` is an
70    /// [`Error::Build`]; engine failures pass through unchanged.
71    pub fn run(
72        &self,
73        analysis: &Analysis,
74        opts: &Options,
75    ) -> Result<(Vec<Solution>, Vec<String>), Error> {
76        self.run_card(analysis, opts).map(CardOutput::into_plots)
77    }
78
79    /// Run one analysis card as [`Circuit::run`] does, keeping a
80    /// `.pss` result whole ([`CardOutput::Pss`]: period, `t0` and the
81    /// shooting history besides the plots). The card's parameters are
82    /// read here and only here — for `.pss`, the [`PssParams`] of the
83    /// card (ngspice's integer truncation of the counts, the
84    /// defaults of [`PssParams::default`] for what it omits).
85    pub fn run_card(&self, analysis: &Analysis, opts: &Options) -> Result<CardOutput, Error> {
86        let (mut plots, notes) = match self.run_plots(analysis, opts)? {
87            CardOutput::Plots { plots, notes } => (plots, notes),
88            pss @ CardOutput::Pss(_) => return Ok(pss),
89        };
90        // `.options keepopinfo`: the small-signal analyses dump their
91        // operating point as a plot of its own first (`CKTdump` under
92        // `CKTkeepOpInfo`). Names are the C's, copy-paste bug
93        // included: `pzan.c` calls its plot "Distortion Operating
94        // Point" (3f5 `acan.c:73`, `pzan.c:46`, `distoan.c:126`).
95        // `.noise` and `.sp` dump only in ngspice-47 (`noisean.c:221`,
96        // `span.c:476`), which Dune follows (corpus/KNOWN_DIFFS.md).
97        if opts.keepopinfo {
98            let name = match analysis {
99                Analysis::Ac { .. } | Analysis::Sp { .. } => Some("AC Operating Point"),
100                Analysis::Pz { .. } | Analysis::Disto { .. } => Some("Distortion Operating Point"),
101                Analysis::Noise { .. } => Some("NOISE Operating Point"),
102                _ => None,
103            };
104            if let Some(name) = name {
105                let mut op = self.op(opts)?.solution;
106                op.plotname = name.to_string();
107                plots.insert(0, op);
108            }
109        }
110        Ok(CardOutput::Plots { plots, notes })
111    }
112
113    /// The card's engine call; `keepopinfo` is [`Circuit::run_card`]'s.
114    fn run_plots(&self, analysis: &Analysis, opts: &Options) -> Result<CardOutput, Error> {
115        let (plots, notes) = match analysis {
116            Analysis::Op { .. } => {
117                let op = self.op(opts)?;
118                (vec![op.solution], op.notes)
119            }
120            Analysis::Dc { sweeps, .. } => {
121                let sweeps: Vec<DcSweep> = sweeps
122                    .iter()
123                    .map(|s| DcSweep {
124                        src: s.src.clone(),
125                        start: s.start,
126                        stop: s.stop,
127                        step: s.step,
128                    })
129                    .collect();
130                let dc = self.dc(opts, &sweeps)?;
131                (vec![dc.solution], dc.notes)
132            }
133            Analysis::Tran {
134                tstep,
135                tstop,
136                tstart,
137                tmax,
138                uic,
139                ..
140            } => {
141                let tr = self.tran(opts, *tstep, *tstop, tstart.unwrap_or(0.0), *tmax, *uic)?;
142                (vec![tr.solution], tr.notes)
143            }
144            Analysis::Ac {
145                variation,
146                points,
147                fstart,
148                fstop,
149                ..
150            } => {
151                let ac = self.ac(opts, sweep(*variation), *points as usize, *fstart, *fstop)?;
152                (vec![ac.solution], ac.notes)
153            }
154            Analysis::Pz {
155                nodes,
156                input,
157                which,
158                ..
159            } => {
160                let input = match input {
161                    ir::PzInput::Current => PzInput::Current,
162                    ir::PzInput::Voltage => PzInput::Voltage,
163                };
164                let which = match which {
165                    ir::PzWhich::Poles => PzWhich::Poles,
166                    ir::PzWhich::Zeros => PzWhich::Zeros,
167                    ir::PzWhich::Both => PzWhich::Both,
168                };
169                let pz = self.pz(
170                    opts,
171                    [
172                        nodes[0].as_str(),
173                        nodes[1].as_str(),
174                        nodes[2].as_str(),
175                        nodes[3].as_str(),
176                    ],
177                    input,
178                    which,
179                )?;
180                (vec![pz.solution], pz.notes)
181            }
182            Analysis::Noise {
183                output,
184                output_ref,
185                src,
186                variation,
187                points,
188                fstart,
189                fstop,
190                pts_per_summary,
191                ..
192            } => {
193                let output = OutputVar::Voltage {
194                    node: output.clone(),
195                    reference: output_ref.clone(),
196                };
197                let no = self.noise(
198                    opts,
199                    &output,
200                    src,
201                    sweep(*variation),
202                    *points as usize,
203                    *fstart,
204                    *fstop,
205                    pts_per_summary.map(|p| p as usize),
206                )?;
207                let mut plots = vec![no.density];
208                plots.extend(no.integrated);
209                (plots, no.notes)
210            }
211            Analysis::Disto {
212                variation,
213                points,
214                fstart,
215                fstop,
216                f2overf1,
217                ..
218            } => {
219                let di = self.disto(
220                    opts,
221                    sweep(*variation),
222                    *points as usize,
223                    *fstart,
224                    *fstop,
225                    *f2overf1,
226                )?;
227                (di.solutions, di.notes)
228            }
229            Analysis::Sp {
230                variation,
231                points,
232                fstart,
233                fstop,
234                donoise,
235                ..
236            } => {
237                let sp = self.sp(
238                    opts,
239                    sweep(*variation),
240                    *points as usize,
241                    *fstart,
242                    *fstop,
243                    *donoise,
244                )?;
245                (vec![sp.solution], sp.notes)
246            }
247            Analysis::Pss {
248                freq,
249                tstab,
250                osc_node,
251                points,
252                harmonics,
253                sc_iter,
254                steady_coeff,
255                uic,
256                ..
257            } => {
258                let d = PssParams::default();
259                // ngspice reads the counts as IF_INTEGER (truncated).
260                let count = |v: Option<f64>, default: usize| match v {
261                    Some(v) if v.is_finite() && v >= 0.0 => v.trunc().min(1e9) as usize,
262                    Some(_) => 0,
263                    None => default,
264                };
265                let p = PssParams {
266                    freq: (*freq != 0.0).then_some(*freq),
267                    tstab: *tstab,
268                    osc_node: Some(osc_node.clone()),
269                    points: count(*points, d.points),
270                    harmonics: count(*harmonics, d.harmonics),
271                    max_iter: count(*sc_iter, d.max_iter),
272                    steady_coeff: steady_coeff.unwrap_or(d.steady_coeff),
273                    uic: *uic,
274                };
275                return Ok(CardOutput::Pss(Box::new(self.pss(opts, &p)?)));
276            }
277            Analysis::Tf { outvar, insrc, .. } => {
278                let tf = self.tf(opts, &output_var(outvar)?, insrc)?;
279                (vec![tf.solution], tf.notes)
280            }
281            Analysis::Sens {
282                outvar,
283                filters,
284                ac,
285                ..
286            } => {
287                let ac = ac.map(|(variation, points, fstart, fstop)| SensAc {
288                    sweep: sweep(variation),
289                    points: points as usize,
290                    fstart,
291                    fstop,
292                });
293                let sens = self.sens(opts, &output_var(outvar)?, filters, ac)?;
294                (vec![sens.solution], sens.notes)
295            }
296        };
297        Ok(CardOutput::Plots { plots, notes })
298    }
299}
300
301/// The card's sweep keyword in the engine's form.
302fn sweep(v: Variation) -> AcSweep {
303    match v {
304        Variation::Dec => AcSweep::Decade,
305        Variation::Oct => AcSweep::Octave,
306        Variation::Lin => AcSweep::Linear,
307    }
308}
309
310/// The `.tf`/`.sens` output variable in the engine's form.
311fn output_var(v: &ir::OutVar) -> Result<OutputVar, Error> {
312    match v {
313        ir::OutVar::Voltage {
314            node, reference, ..
315        } => Ok(OutputVar::Voltage {
316            node: node.clone(),
317            reference: reference.clone(),
318        }),
319        ir::OutVar::Current { vsrc, .. } => Ok(OutputVar::Current { vsrc: vsrc.clone() }),
320        ir::OutVar::Raw(r) => Err(Error::Build(format!("`{r}` is not an output variable"))),
321    }
322}