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}