Skip to main content

dune_core/
session.rs

1//! Interactive re-solve (issue #12, D-0028 track 3): a [`Session`]
2//! keeps a parsed and elaborated circuit alive, [`Circuit::alter`]
3//! changes instance values in place (the spice3 `alter` command's
4//! job, with ngspice's parameter names), and [`Session::run`] runs
5//! the deck's analyses again without re-parsing or re-elaborating.
6//!
7//! **Contract.** With warm starts off (the default) a re-run is the
8//! fresh run of the altered deck, bit for bit: an alteration writes
9//! exactly the spec field elaboration would have written from the
10//! altered card, and every analysis builds its engine from the specs
11//! as a fresh run does. What a re-run skips is the parse
12//! (lexing, `.subckt` expansion, `.include`) and the elaboration
13//! (model cards, instance parameters) — and, in the bindings, the pic
14//! extraction. The dense LU backend (D-0005, D-0013) has no symbolic
15//! phase to keep: its `analyze` only sizes the matrices, so there is
16//! no factorization worth carrying between runs; a sparse backend
17//! would hang its symbolic reuse on the same session (see
18//! `docs/dune-sessao.md`).
19//!
20//! **Warm start** ([`Session::set_warm_start`]) is the opt-in that
21//! trades the bit-for-bit contract for speed: a standalone operating
22//! point (`.op`, and the bias point of `.ac`, `.noise`, `.tf`,
23//! `.sens`, `.pz`, `.disto`, `.sp`) first tries a Newton solve
24//! continued from the previous operating point, exactly as a `.dc`
25//! sweep continues between points. A converged warm solve is a
26//! solution to the same tolerances (`RELTOL`/`VNTOL`/`ABSTOL`) as the
27//! cold one, not the same bits; when it does not converge the
28//! analysis is re-run cold from scratch, so a miss costs time, never
29//! the result. Circuits with `.nodeset`s and transient operating
30//! points are never warm-started; a circuit with several stable
31//! operating points (a latch) may settle in a different one, as a
32//! sweep would.
33
34use std::sync::{Arc, Mutex, MutexGuard};
35
36use dune_netlist::ir::{Analysis, Netlist};
37
38use crate::circuit::{Circuit, Spec};
39use crate::consts::CTOK;
40use crate::error::Error;
41use crate::options::Options;
42use crate::run::CardOutput;
43use crate::solution::Solution;
44use crate::srcfn::Wave;
45
46/// A value for [`Circuit::alter`]: a number, or the argument list of a
47/// source function (`pulse`, `sin`, `exp`, `pwl`, `sffm`).
48#[derive(Clone, Debug, PartialEq)]
49pub enum AlterValue {
50    Num(f64),
51    List(Vec<f64>),
52}
53
54impl From<f64> for AlterValue {
55    fn from(x: f64) -> Self {
56        AlterValue::Num(x)
57    }
58}
59
60impl From<Vec<f64>> for AlterValue {
61    fn from(v: Vec<f64>) -> Self {
62        AlterValue::List(v)
63    }
64}
65
66impl From<&[f64]> for AlterValue {
67    fn from(v: &[f64]) -> Self {
68        AlterValue::List(v.to_vec())
69    }
70}
71
72/// Longest source-function argument list [`Circuit::alter`] accepts
73/// (a PWL of half a million points).
74const MAX_WAVE_ARGS: usize = 1 << 20;
75
76/// The parameters [`Circuit::alter`] knows, per element letter, in
77/// the order error messages list them (ngspice `IFparm` names; `""`
78/// is the element's principal value).
79fn params_of(spec: &Spec) -> &'static [&'static str] {
80    match spec {
81        Spec::Resistor(_) => &["resistance"],
82        Spec::Capacitor(_) => &["capacitance", "ic"],
83        Spec::Inductor(_) => &["inductance", "ic"],
84        Spec::Vsource(_) | Spec::Isource(_) => &[
85            "dc", "acmag", "acphase", "pulse", "sin", "exp", "pwl", "sffm", "trnoise", "trrandom",
86        ],
87        Spec::Vcvs(_) | Spec::Vccs(_) | Spec::Cccs(_) | Spec::Ccvs(_) => &["gain"],
88        Spec::Mos1(_) | Spec::Mos2(_) | Spec::Mos3(_) | Spec::Mos6(_) => &["w", "l", "temp"],
89        Spec::Bsim1(_) | Spec::Bsim2(_) | Spec::Bsim3(_) => &["w", "l"],
90        Spec::Diode(_) | Spec::Bjt(_) | Spec::Jfet(_) => &["area", "temp"],
91        Spec::Mes(_) => &["area"],
92        _ => &[],
93    }
94}
95
96/// The principal parameter an empty `param` means, and the aliases
97/// spice3 accepts for it.
98fn canonical<'a>(spec: &Spec, param: &'a str) -> &'a str {
99    match (spec, param) {
100        (Spec::Resistor(_), "" | "r") => "resistance",
101        (Spec::Capacitor(_), "" | "c") => "capacitance",
102        (Spec::Inductor(_), "" | "l") => "inductance",
103        (Spec::Vsource(_) | Spec::Isource(_), "") => "dc",
104        (Spec::Vcvs(_) | Spec::Vccs(_) | Spec::Cccs(_) | Spec::Ccvs(_), "") => "gain",
105        (Spec::Diode(_) | Spec::Bjt(_) | Spec::Jfet(_) | Spec::Mes(_), "") => "area",
106        _ => param,
107    }
108}
109
110fn num(name: &str, param: &str, v: &AlterValue) -> Result<f64, Error> {
111    match v {
112        AlterValue::Num(x) if x.is_finite() => Ok(*x),
113        AlterValue::Num(x) => Err(Error::Build(format!(
114            "`{name}`: {param} must be finite, got {x}"
115        ))),
116        AlterValue::List(_) => Err(Error::Build(format!(
117            "`{name}`: {param} takes a number, not a list"
118        ))),
119    }
120}
121
122fn positive(name: &str, param: &str, v: &AlterValue) -> Result<f64, Error> {
123    let x = num(name, param, v)?;
124    if x > 0.0 {
125        Ok(x)
126    } else {
127        Err(Error::Build(format!(
128            "`{name}`: {param} must be positive, got {x}"
129        )))
130    }
131}
132
133fn list(name: &str, param: &str, v: &AlterValue) -> Result<Vec<f64>, Error> {
134    match v {
135        AlterValue::List(xs) if xs.len() > MAX_WAVE_ARGS => Err(Error::Build(format!(
136            "`{name}`: {param} has {} arguments; the limit is {MAX_WAVE_ARGS}",
137            xs.len()
138        ))),
139        AlterValue::List(xs) if xs.iter().all(|x| x.is_finite()) => Ok(xs.clone()),
140        AlterValue::List(_) => Err(Error::Build(format!(
141            "`{name}`: {param} arguments must be finite"
142        ))),
143        AlterValue::Num(_) => Err(Error::Build(format!(
144            "`{name}`: {param} takes the function's argument list"
145        ))),
146    }
147}
148
149/// `(kind, args)` of a source's transient function under its
150/// ngspice parameter name.
151fn wave_param(w: &Wave) -> (&'static str, &[f64]) {
152    match w {
153        Wave::Pulse(a) => ("pulse", a),
154        Wave::Sin(a) => ("sin", a),
155        Wave::Exp(a) => ("exp", a),
156        Wave::Pwl(a) => ("pwl", a),
157        Wave::Sffm(a) => ("sffm", a),
158        Wave::TrNoise(a) => ("trnoise", a),
159        Wave::TrRandom(a) => ("trrandom", a),
160    }
161}
162
163/// The source fields an alteration touches (V and I alike).
164struct SrcFields<'a> {
165    dc: &'a mut Option<f64>,
166    wave: &'a mut Option<Wave>,
167    ac: &'a mut Option<(f64, f64)>,
168}
169
170impl Circuit {
171    /// Change one parameter of element `refdes` in place — the
172    /// spice3/ngspice `alter` command. The next analysis sees the
173    /// circuit exactly as if the deck had been written with the new
174    /// value (same spec field, same unit as the card).
175    ///
176    /// `refdes` is the element name (case-insensitive; subcircuit
177    /// instances by their expanded name, e.g. `r.x1.r1`). `param` is
178    /// the ngspice parameter name, `""` for the element's principal
179    /// value:
180    ///
181    /// | element | `param` (`""` →) | value |
182    /// |---|---|---|
183    /// | `R` | `resistance` (`r`) | ohms, nonzero |
184    /// | `C` / `L` | `capacitance` (`c`) / `inductance` (`l`), `ic` | F / H; IC in V / A |
185    /// | `V` / `I` | `dc`, `acmag`, `acphase` (degrees) | V or A |
186    /// | `V` / `I` | `pulse`, `sin`, `exp`, `pwl`, `sffm`, `trnoise`, `trrandom` | [`AlterValue::List`]: the card's arguments; replaces the transient function |
187    /// | `E` `F` `G` `H` | `gain` | as on the card |
188    /// | MOS (all levels, BSIM) | `w`, `l` | meters, positive |
189    /// | MOS 1/2/3/6, `D`, `Q`, `J` | `temp` | °C, like `TEMP=` on the card |
190    /// | `D`, `Q`, `J`, `Z` | `area` | positive |
191    ///
192    /// Altering `acmag`/`acphase` of a source without an `AC`
193    /// specification gives it one (the other half 0), as the card
194    /// `AC 0 <phase>` would. Inductor values also move the coupling of
195    /// their `K` elements (`MUTfactor` is recomputed at each run).
196    /// Topology never changes, so an altered circuit keeps its
197    /// unknowns and output vectors. The circuit-wide temperature is
198    /// an option: [`Session::set_temp`].
199    ///
200    /// Errors ([`Error::Build`]): unknown element, a parameter the
201    /// element does not have (the message lists the valid ones), a
202    /// non-finite value, a zero resistance, a nonpositive `W`/`L`/
203    /// `area`, a number where a list is due or vice versa.
204    pub fn alter(
205        &mut self,
206        refdes: &str,
207        param: &str,
208        value: impl Into<AlterValue>,
209    ) -> Result<(), Error> {
210        let value = value.into();
211        let name = refdes.to_ascii_lowercase();
212        let param_lc = param.to_ascii_lowercase();
213        let spec = self
214            .specs
215            .iter_mut()
216            .find(|s| s.name() == name)
217            .ok_or_else(|| Error::Build(format!("no element `{name}` in the circuit")))?;
218        let p = canonical(spec, &param_lc).to_string();
219        let p = p.as_str();
220        let v = &value;
221        let n = name.as_str();
222        let bad = |spec: &Spec| {
223            let valid = params_of(spec);
224            if valid.is_empty() {
225                Error::Build(format!("`{n}`: this element has no alterable parameters"))
226            } else {
227                Error::Build(format!(
228                    "`{n}` has no alterable parameter `{p}`; valid: {}",
229                    valid.join(", ")
230                ))
231            }
232        };
233        match spec {
234            Spec::Resistor(r) if p == "resistance" => {
235                let x = num(n, p, v)?;
236                if x == 0.0 {
237                    // spice.txt §3.1.1, as `Circuit::add`.
238                    return Err(Error::Build(format!(
239                        "resistor `{n}` cannot have zero resistance"
240                    )));
241                }
242                r.value = x;
243            }
244            Spec::Capacitor(c) if p == "capacitance" => c.value = num(n, p, v)?,
245            Spec::Capacitor(c) if p == "ic" => c.ic = Some(num(n, p, v)?),
246            Spec::Inductor(l) if p == "inductance" => l.value = num(n, p, v)?,
247            Spec::Inductor(l) if p == "ic" => l.ic = Some(num(n, p, v)?),
248            Spec::Vsource(_) | Spec::Isource(_) if params_of(spec).contains(&p) => {
249                let f = match spec {
250                    Spec::Vsource(s) => SrcFields {
251                        dc: &mut s.dc,
252                        wave: &mut s.wave,
253                        ac: &mut s.ac,
254                    },
255                    Spec::Isource(s) => SrcFields {
256                        dc: &mut s.dc,
257                        wave: &mut s.wave,
258                        ac: &mut s.ac,
259                    },
260                    _ => unreachable!("matched V/I above"),
261                };
262                match p {
263                    "dc" => *f.dc = Some(num(n, p, v)?),
264                    "acmag" => {
265                        let x = num(n, p, v)?;
266                        *f.ac = Some((x, f.ac.map_or(0.0, |a| a.1)));
267                    }
268                    "acphase" => {
269                        let x = num(n, p, v)?;
270                        *f.ac = Some((f.ac.map_or(0.0, |a| a.0), x));
271                    }
272                    _ => {
273                        let args = list(n, p, v)?;
274                        *f.wave = Some(match p {
275                            "pulse" => Wave::Pulse(args),
276                            "sin" => Wave::Sin(args),
277                            "exp" => Wave::Exp(args),
278                            "pwl" => Wave::Pwl(args),
279                            "sffm" => Wave::Sffm(args),
280                            "trnoise" => Wave::TrNoise(args),
281                            _ => Wave::TrRandom(args),
282                        });
283                    }
284                }
285            }
286            Spec::Vcvs(s) if p == "gain" => s.gain = num(n, p, v)?,
287            Spec::Vccs(s) if p == "gain" => s.gain = num(n, p, v)?,
288            Spec::Cccs(s) if p == "gain" => s.gain = num(n, p, v)?,
289            Spec::Ccvs(s) if p == "gain" => s.gain = num(n, p, v)?,
290            Spec::Mos1(_)
291            | Spec::Mos2(_)
292            | Spec::Mos3(_)
293            | Spec::Mos6(_)
294            | Spec::Bsim1(_)
295            | Spec::Bsim2(_)
296            | Spec::Bsim3(_)
297                if params_of(spec).contains(&p) =>
298            {
299                let (w, l, temp) = match spec {
300                    Spec::Mos1(m) => (&mut m.w, &mut m.l, Some(&mut m.temp)),
301                    Spec::Mos2(m) => (&mut m.w, &mut m.l, Some(&mut m.temp)),
302                    Spec::Mos3(m) => (&mut m.w, &mut m.l, Some(&mut m.temp)),
303                    Spec::Mos6(m) => (&mut m.w, &mut m.l, Some(&mut m.temp)),
304                    Spec::Bsim1(m) => (&mut m.w, &mut m.l, None),
305                    Spec::Bsim2(m) => (&mut m.w, &mut m.l, None),
306                    Spec::Bsim3(m) => (&mut m.w, &mut m.l, None),
307                    _ => unreachable!("matched MOS above"),
308                };
309                match (p, temp) {
310                    ("w", _) => *w = Some(positive(n, p, v)?),
311                    ("l", _) => *l = Some(positive(n, p, v)?),
312                    // `TEMP=` is °C on the card (`+ CTOK` in
313                    // elaboration, the same operation).
314                    (_, Some(t)) => *t = Some(num(n, p, v)? + CTOK),
315                    (_, None) => unreachable!("BSIM lists no temp"),
316                }
317            }
318            Spec::Diode(_) | Spec::Bjt(_) | Spec::Jfet(_) | Spec::Mes(_)
319                if params_of(spec).contains(&p) =>
320            {
321                let (area, temp) = match spec {
322                    Spec::Diode(d) => (&mut d.area, Some(&mut d.temp)),
323                    Spec::Bjt(q) => (&mut q.area, Some(&mut q.temp)),
324                    Spec::Jfet(j) => (&mut j.area, Some(&mut j.temp)),
325                    Spec::Mes(z) => (&mut z.area, None),
326                    _ => unreachable!("matched D/Q/J/Z above"),
327                };
328                match (p, temp) {
329                    ("area", _) => *area = positive(n, p, v)?,
330                    (_, Some(t)) => *t = Some(num(n, p, v)? + CTOK),
331                    (_, None) => unreachable!("MES lists no temp"),
332                }
333            }
334            _ => return Err(bad(spec)),
335        }
336        Ok(())
337    }
338
339    /// The current value of an alterable parameter (same names and
340    /// units as [`Circuit::alter`]), e.g. to seed a slider. A value
341    /// the element never got — a MOS `W` left to `.options defw`, a
342    /// source without `AC` — reads as `None`; a source without a
343    /// transient function reads `None` for `pulse` and friends, and
344    /// only the function the source has reads `Some`.
345    pub fn param(&self, refdes: &str, param: &str) -> Result<Option<AlterValue>, Error> {
346        let name = refdes.to_ascii_lowercase();
347        let param_lc = param.to_ascii_lowercase();
348        let spec = self
349            .specs
350            .iter()
351            .find(|s| s.name() == name)
352            .ok_or_else(|| Error::Build(format!("no element `{name}` in the circuit")))?;
353        let p = canonical(spec, &param_lc);
354        let n = |x: f64| Some(AlterValue::Num(x));
355        let opt = |x: Option<f64>| x.map(AlterValue::Num);
356        let src = |dc: Option<f64>, ac: Option<(f64, f64)>, wave: &Option<Wave>| match p {
357            "dc" => Some(opt(dc)),
358            "acmag" => Some(opt(ac.map(|a| a.0))),
359            "acphase" => Some(opt(ac.map(|a| a.1))),
360            "pulse" | "sin" | "exp" | "pwl" | "sffm" => Some(
361                wave.as_ref()
362                    .map(wave_param)
363                    .filter(|(k, _)| *k == p)
364                    .map(|(_, a)| AlterValue::List(a.to_vec())),
365            ),
366            _ => None,
367        };
368        let got: Option<Option<AlterValue>> = match spec {
369            Spec::Resistor(r) if p == "resistance" => Some(n(r.value)),
370            Spec::Capacitor(x) if p == "capacitance" => Some(n(x.value)),
371            Spec::Capacitor(x) if p == "ic" => Some(opt(x.ic)),
372            Spec::Inductor(x) if p == "inductance" => Some(n(x.value)),
373            Spec::Inductor(x) if p == "ic" => Some(opt(x.ic)),
374            Spec::Vsource(s) => src(s.dc, s.ac, &s.wave),
375            Spec::Isource(s) => src(s.dc, s.ac, &s.wave),
376            Spec::Vcvs(s) if p == "gain" => Some(n(s.gain)),
377            Spec::Vccs(s) if p == "gain" => Some(n(s.gain)),
378            Spec::Cccs(s) if p == "gain" => Some(n(s.gain)),
379            Spec::Ccvs(s) if p == "gain" => Some(n(s.gain)),
380            Spec::Mos1(m) => mos(p, m.w, m.l, Some(m.temp)),
381            Spec::Mos2(m) => mos(p, m.w, m.l, Some(m.temp)),
382            Spec::Mos3(m) => mos(p, m.w, m.l, Some(m.temp)),
383            Spec::Mos6(m) => mos(p, m.w, m.l, Some(m.temp)),
384            Spec::Bsim1(m) => mos(p, m.w, m.l, None),
385            Spec::Bsim2(m) => mos(p, m.w, m.l, None),
386            Spec::Bsim3(m) => mos(p, m.w, m.l, None),
387            Spec::Diode(d) => area_temp(p, d.area, Some(d.temp)),
388            Spec::Bjt(q) => area_temp(p, q.area, Some(q.temp)),
389            Spec::Jfet(j) => area_temp(p, j.area, Some(j.temp)),
390            Spec::Mes(z) => area_temp(p, z.area, None),
391            _ => None,
392        };
393        fn mos(
394            p: &str,
395            w: Option<f64>,
396            l: Option<f64>,
397            temp: Option<Option<f64>>,
398        ) -> Option<Option<AlterValue>> {
399            match (p, temp) {
400                ("w", _) => Some(w.map(AlterValue::Num)),
401                ("l", _) => Some(l.map(AlterValue::Num)),
402                ("temp", Some(t)) => Some(t.map(|k| AlterValue::Num(k - CTOK))),
403                _ => None,
404            }
405        }
406        fn area_temp(p: &str, area: f64, temp: Option<Option<f64>>) -> Option<Option<AlterValue>> {
407            match (p, temp) {
408                ("area", _) => Some(Some(AlterValue::Num(area))),
409                ("temp", Some(t)) => Some(t.map(|k| AlterValue::Num(k - CTOK))),
410                _ => None,
411            }
412        }
413        got.ok_or_else(|| {
414            let valid = params_of(spec);
415            Error::Build(format!(
416                "`{name}` has no alterable parameter `{p}`; valid: {}",
417                valid.join(", ")
418            ))
419        })
420    }
421
422    /// The parameter names [`Circuit::alter`] accepts for `refdes`
423    /// (empty for an element with none).
424    pub fn alterable(&self, refdes: &str) -> Result<&'static [&'static str], Error> {
425        let name = refdes.to_ascii_lowercase();
426        self.specs
427            .iter()
428            .find(|s| s.name() == name)
429            .map(params_of)
430            .ok_or_else(|| Error::Build(format!("no element `{name}` in the circuit")))
431    }
432}
433
434/// Warm-start counters of a [`Session`] ([`Session::warm_stats`]).
435#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
436pub struct WarmStats {
437    /// Operating points solved from the previous one.
438    pub hits: u64,
439    /// Warm attempts that did not converge (each re-ran its analysis
440    /// cold).
441    pub misses: u64,
442}
443
444#[derive(Default)]
445struct Warm {
446    enabled: bool,
447    /// Set while a missed analysis re-runs cold.
448    suspended: bool,
449    /// The last operating point (`CKTrhsOld`, ground included) and
450    /// its state bank 0; empty before the first one.
451    sol: Vec<f64>,
452    states0: Vec<f64>,
453    /// The last analysis missed (read and cleared by the session).
454    missed: bool,
455    stats: WarmStats,
456}
457
458/// A circuit's handle on its session's warm-start state. A plain
459/// [`Circuit`] has none (the engine's `op` then is `CKTop`, nothing
460/// else); a [`Session`] installs one and every engine built from the
461/// circuit shares it. Cloning a circuit does not share it.
462#[derive(Default)]
463pub(crate) struct WarmSlot(Option<Arc<Mutex<Warm>>>);
464
465impl Clone for WarmSlot {
466    fn clone(&self) -> Self {
467        WarmSlot(None)
468    }
469}
470
471impl WarmSlot {
472    fn new(enabled: bool) -> Self {
473        WarmSlot(Some(Arc::new(Mutex::new(Warm {
474            enabled,
475            ..Warm::default()
476        }))))
477    }
478
479    fn lock(&self) -> Option<MutexGuard<'_, Warm>> {
480        self.0
481            .as_ref()
482            .map(|m| m.lock().unwrap_or_else(|e| e.into_inner()))
483    }
484
485    /// The same slot, for an engine built from the circuit.
486    pub(crate) fn share(&self) -> WarmSlot {
487        WarmSlot(self.0.clone())
488    }
489
490    /// The previous operating point, when warm starts are on and it
491    /// fits this engine (`n_sol` unknowns incl. ground, `n_states`
492    /// state slots).
493    pub(crate) fn guess(&self, n_sol: usize, n_states: usize) -> Option<(Vec<f64>, Vec<f64>)> {
494        let w = self.lock()?;
495        (w.enabled && !w.suspended && w.sol.len() == n_sol && w.states0.len() == n_states)
496            .then(|| (w.sol.clone(), w.states0.clone()))
497    }
498
499    pub(crate) fn store(&self, sol: &[f64], states0: &[f64]) {
500        if let Some(mut w) = self.lock() {
501            w.sol.clear();
502            w.sol.extend_from_slice(sol);
503            w.states0.clear();
504            w.states0.extend_from_slice(states0);
505        }
506    }
507
508    pub(crate) fn hit(&self, sol: &[f64], states0: &[f64]) {
509        self.store(sol, states0);
510        if let Some(mut w) = self.lock() {
511            w.stats.hits += 1;
512        }
513    }
514
515    pub(crate) fn miss(&self) {
516        if let Some(mut w) = self.lock() {
517            w.missed = true;
518            w.stats.misses += 1;
519        }
520    }
521
522    fn take_missed(&self) -> bool {
523        self.lock()
524            .is_some_and(|mut w| std::mem::take(&mut w.missed))
525    }
526
527    fn set_suspended(&self, on: bool) {
528        if let Some(mut w) = self.lock() {
529            w.suspended = on;
530        }
531    }
532
533    fn set_enabled(&self, on: bool) {
534        if let Some(mut w) = self.lock() {
535            w.enabled = on;
536            if !on {
537                w.sol.clear();
538                w.states0.clear();
539            }
540        }
541    }
542
543    fn enabled(&self) -> bool {
544        self.lock().is_some_and(|w| w.enabled)
545    }
546
547    fn stats(&self) -> WarmStats {
548        self.lock().map(|w| w.stats).unwrap_or_default()
549    }
550}
551
552/// A live circuit for interactive re-solves: the elaborated circuit,
553/// its options and its analysis cards, kept between runs (issue #12).
554///
555/// ```
556/// use dune_core::Session;
557/// let deck = "divider\nv1 1 0 dc 10\nr1 1 2 1k\nr2 2 0 1k\n.op\n.end\n";
558/// let parsed = dune_netlist::parse_str("deck.cir", deck);
559/// let (mut s, _notes) = Session::from_netlist(&parsed.netlist).unwrap();
560/// let (plots, _) = s.run().unwrap();
561/// s.alter("r2", "", 3e3).unwrap();
562/// let (again, _) = s.run().unwrap();
563/// let v2 = |p: &dune_core::Solution| p.column("v(2)").unwrap()[0];
564/// assert_eq!(v2(&plots[0]), 5.0);
565/// assert_eq!(v2(&again[0]), 7.5);
566/// ```
567///
568/// See the module notes for the bit-for-bit contract and the
569/// warm-start opt-in.
570pub struct Session {
571    circuit: Circuit,
572    options: Options,
573    analyses: Vec<Analysis>,
574}
575
576impl Clone for Session {
577    /// An independent session with the same circuit, options, cards
578    /// and warm-start setting, and no warm-start history.
579    fn clone(&self) -> Self {
580        Session::new(
581            self.circuit.clone(),
582            self.options.clone(),
583            self.analyses.clone(),
584        )
585        .with_warm_start(self.warm_start())
586    }
587}
588
589impl Session {
590    /// A session over an already built circuit; `analyses` are what
591    /// [`Session::run`] runs, in order.
592    pub fn new(mut circuit: Circuit, options: Options, analyses: Vec<Analysis>) -> Self {
593        circuit.warm = WarmSlot::new(false);
594        Session {
595            circuit,
596            options,
597            analyses,
598        }
599    }
600
601    /// Elaborate a parsed deck into a session: the circuit
602    /// ([`Circuit::from_netlist`]), the deck's `.options` (warnings
603    /// about them are dropped — apply them yourself with
604    /// [`Options::apply`] and use [`Session::new`] to keep them) and
605    /// its analysis cards. Returns the elaboration notes.
606    pub fn from_netlist(nl: &Netlist) -> Result<(Self, Vec<String>), Error> {
607        let (circuit, notes) = Circuit::from_netlist(nl)?;
608        let mut options = Options::default();
609        options.apply(&nl.options, &mut Vec::new());
610        Ok((Session::new(circuit, options, nl.analyses.clone()), notes))
611    }
612
613    fn with_warm_start(mut self, on: bool) -> Self {
614        self.set_warm_start(on);
615        self
616    }
617
618    /// [`Circuit::alter`] on the session's circuit.
619    pub fn alter(
620        &mut self,
621        refdes: &str,
622        param: &str,
623        value: impl Into<AlterValue>,
624    ) -> Result<(), Error> {
625        self.circuit.alter(refdes, param, value)
626    }
627
628    /// [`Circuit::param`] on the session's circuit.
629    pub fn param(&self, refdes: &str, param: &str) -> Result<Option<AlterValue>, Error> {
630        self.circuit.param(refdes, param)
631    }
632
633    /// The circuit temperature, °C (`.options temp=`, the card's
634    /// unit). Instance `TEMP=` overrides still win, as in the 3f5.
635    pub fn set_temp(&mut self, celsius: f64) -> Result<(), Error> {
636        if !celsius.is_finite() {
637            return Err(Error::Build(format!(
638                "temperature must be finite, got {celsius}"
639            )));
640        }
641        // `Options::apply` does the same `v + CTOK`.
642        self.options.temp = celsius + CTOK;
643        Ok(())
644    }
645
646    /// The circuit temperature, °C.
647    pub fn temp(&self) -> f64 {
648        self.options.temp - CTOK
649    }
650
651    /// Turn the operating-point warm start on or off (off by
652    /// default; turning it off forgets the stored point). See the
653    /// module notes: on, results agree with a fresh run to the
654    /// Newton tolerances instead of bit for bit.
655    pub fn set_warm_start(&mut self, on: bool) {
656        self.circuit.warm.set_enabled(on);
657    }
658
659    pub fn warm_start(&self) -> bool {
660        self.circuit.warm.enabled()
661    }
662
663    /// Warm-start hits and misses so far.
664    pub fn warm_stats(&self) -> WarmStats {
665        self.circuit.warm.stats()
666    }
667
668    pub fn circuit(&self) -> &Circuit {
669        &self.circuit
670    }
671
672    pub fn options(&self) -> &Options {
673        &self.options
674    }
675
676    /// The options, for changes beyond [`Session::set_temp`]. They
677    /// apply from the next run on.
678    pub fn options_mut(&mut self) -> &mut Options {
679        &mut self.options
680    }
681
682    /// The analysis cards [`Session::run`] runs.
683    pub fn analyses(&self) -> &[Analysis] {
684        &self.analyses
685    }
686
687    /// Run every analysis card in order, as [`Circuit::run`] does for
688    /// each: the plots in rawfile order and the analyses' notes (the
689    /// elaboration notes came with [`Session::from_netlist`]). The
690    /// first error stops the run.
691    pub fn run(&mut self) -> Result<(Vec<Solution>, Vec<String>), Error> {
692        let mut plots = Vec::new();
693        let mut notes = Vec::new();
694        for a in &self.analyses {
695            let (p, n) = run_one(&self.circuit, &self.options, a)?.into_plots();
696            plots.extend(p);
697            notes.extend(n);
698        }
699        Ok((plots, notes))
700    }
701
702    /// Run one analysis card (any card, not only the deck's) on the
703    /// session's circuit.
704    pub fn run_analysis(
705        &mut self,
706        analysis: &Analysis,
707    ) -> Result<(Vec<Solution>, Vec<String>), Error> {
708        run_one(&self.circuit, &self.options, analysis).map(CardOutput::into_plots)
709    }
710
711    /// [`Session::run_analysis`] keeping a `.pss` result whole, as
712    /// [`Circuit::run_card`] does (same warm start, same cold re-run
713    /// after a miss).
714    pub fn run_card(&mut self, analysis: &Analysis) -> Result<CardOutput, Error> {
715        run_one(&self.circuit, &self.options, analysis)
716    }
717}
718
719/// [`Circuit::run_card`], re-run cold when its warm start missed.
720fn run_one(circuit: &Circuit, options: &Options, analysis: &Analysis) -> Result<CardOutput, Error> {
721    let first = circuit.run_card(analysis, options);
722    if first.is_ok() || !circuit.warm.take_missed() {
723        return first;
724    }
725    circuit.warm.set_suspended(true);
726    let cold = circuit.run_card(analysis, options);
727    circuit.warm.set_suspended(false);
728    cold
729}