Skip to main content

dune_core/
options.rs

1//! Simulator options with the 3f5 defaults
2//! (`lib/ckt/cktinit.c`, `lib/ckt/cktntask.c`; names from
3//! `spice.txt` §SIMULATOR VARIABLES). Temperatures arrive in °C on
4//! the card and live in kelvin here, like the C.
5
6use dune_netlist::Diagnostic;
7use dune_netlist::ir::{Param, ParamValue};
8
9use crate::consts::{CTOK, REFTEMP};
10use crate::integ::Method;
11
12#[derive(Clone, Debug)]
13pub struct Options {
14    /// `RELTOL`, default 1e-3.
15    pub reltol: f64,
16    /// `ABSTOL`, default 1e-12 (amperes).
17    pub abstol: f64,
18    /// `VNTOL`, default 1e-6 (volts).
19    pub vntol: f64,
20    /// `GMIN`, default 1e-12.
21    pub gmin: f64,
22    /// `ITL1`: DC iteration limit, default 100.
23    pub itl1: usize,
24    /// `ITL2`: DC transfer-curve iteration limit, default 50.
25    pub itl2: usize,
26    /// `ITL4`: transient timepoint iteration limit, default 10
27    /// (`CKTtranMaxIter`).
28    pub itl4: usize,
29    /// Gmin steps in `CKTop`. 1 (the ngspice-47 default) selects the
30    /// dynamic Gillespie stepping of the diagonal gmin followed, if it
31    /// fails, by the "true gmin" stepping of the devices' `gmin`;
32    /// n > 1 runs the 3f5 fixed ladder with n steps; 0 skips gmin
33    /// stepping (`corpus/KNOWN_DIFFS.md`).
34    pub gminsteps: usize,
35    /// Source steps in `CKTop`. 1 (the ngspice-47 default,
36    /// `cktntask.c`) selects Gillespie's adaptive source ramp; n > 1
37    /// runs the 3f5 fixed ladder of n steps (the 3f5 default is 10);
38    /// 0 skips source stepping (`corpus/KNOWN_DIFFS.md`).
39    pub srcsteps: usize,
40    /// Operating temperature in kelvin (`TEMP`, default 27 °C).
41    pub temp: f64,
42    /// Nominal parameter-measurement temperature (`TNOM`, 27 °C).
43    pub tnom: f64,
44    /// `PIVTOL`, default 1e-13.
45    pub pivtol: f64,
46    /// `PIVREL`, default 1e-3.
47    pub pivrel: f64,
48    /// `NOOPITER`: go directly to gmin stepping.
49    pub noopiter: bool,
50    /// `TRTOL`: truncation-error overestimation factor, default 7.
51    pub trtol: f64,
52    /// `CHGTOL`: charge tolerance, default 1e-14.
53    pub chgtol: f64,
54    /// `METHOD`: `trap` (default), `gear`, or Dune's opt-in `trbdf2`
55    /// ([`Method::TrBdf2`]; not a SPICE value).
56    pub method: Method,
57    /// `MAXORD` for Gear, default 2.
58    pub maxord: usize,
59    /// `NOPAGE`: suppress page breaks in printed output.
60    pub nopage: bool,
61    /// `DEFL`/`DEFW`: default MOS channel length/width, meters
62    /// (`CKTdefaultMosL/W`, 1e-4).
63    pub defl: f64,
64    pub defw: f64,
65    /// `DEFAD`/`DEFAS`: default MOS drain/source diffusion area, m²
66    /// (`CKTdefaultMosAD/AS`, 0).
67    pub defad: f64,
68    pub defas: f64,
69    /// Dune's transient step budget (not a SPICE option; no `.options`
70    /// card sets it): the most internal timepoints — accepted or
71    /// rejected — one `.tran` may attempt before it stops with
72    /// [`Error::StepLimit`](crate::Error::StepLimit). The 3f5 and
73    /// ngspice-47 both parse `ITL5` ("total transient iteration
74    /// limit") and ignore it (`cktsopt.c`: `case OPT_ITL5: break;`),
75    /// so neither bounds a run whose breakpoints cascade (D-0022).
76    ///
77    /// `None` (default): automatic, scaled to the deck —
78    /// `max(TRAN_STEP_FLOOR, TRAN_STEPS_PER_TMAX · ⌈TSTOP/TMAX⌉)`
79    /// with the effective TMAX, i.e. 10 000 steps or 50 per maximum
80    /// step span, whichever is more (the corpus peaks at 13 per span,
81    /// ltra_2; a fixed budget cannot work because a cascading LTRA
82    /// deck costs O(n²) in its history length — D-0022). `Some(n)`:
83    /// at most `n` (`Some(usize::MAX)` is unlimited).
84    pub max_tran_steps: Option<usize>,
85    /// Dune's opt-in LTRA breakpoint hygiene (`.options ltrabkrate=N`;
86    /// not a SPICE option, issue #6). The slope rule of `LTRAaccept`
87    /// plants a breakpoint one delay after every accepted point where
88    /// an incident wave's slope changes; `CKTsetBreak` merges only
89    /// within `5e-5·TMAX`, so a ringing load can feed it an echo
90    /// cascade (D-0022). `Some(n)`, `n > 0`: a slope breakpoint that
91    /// falls within `TMAX/n` of one already scheduled is dropped,
92    /// which caps them at about `n` per TMAX span. `None` or
93    /// `Some(0)` (default): faithful, every request scheduled.
94    pub ltra_break_rate: Option<usize>,
95    /// Dune's opt-in recursive convolution for LTRA lines
96    /// (`.options ltrafit`; not a SPICE option, issue #8). The 3f5
97    /// recomputes every line's convolution over the whole accepted
98    /// history at each timepoint — O(n) per point, quadratic per run.
99    /// `true`: the RLC and RC impulse responses become exponential
100    /// sums (an exact branch-cut quadrature, error budget 1e-6 per
101    /// kernel, reported in the run's notes) advanced once per accepted
102    /// point, with the newest segment kept exact — O(1) per point.
103    /// `false` (default): faithful.
104    pub ltra_fit: bool,
105    /// Dune's transient refinement knob (an API argument, not a SPICE
106    /// option: no `.options` card sets it, issue #16). `Some(d)`,
107    /// `d ≥ 1` and finite: every `.tran` runs with its effective
108    /// maximum step divided by `d` — the 4th argument of the card
109    /// when given, else the default `min(TSTEP, (TSTOP−TSTART)/50)`.
110    /// Everything derived from TMAX (`delmin`, the breakpoint merge
111    /// radius, the step budget) follows, so the run is the one the
112    /// deck would give with that card's TMAX edited to `TMAX/d`.
113    /// It is not a `.options` key because TMAX is a parameter of the
114    /// analysis card, and the divisor applies to every `.tran` card of
115    /// the deck whatever TMAX each one carries. Any other value makes
116    /// `.tran` fail with [`Error::Build`](crate::Error::Build).
117    /// `None` (default): the card's TMAX as is.
118    pub tmax_divisor: Option<f64>,
119    /// `SEED`: the seed of the transient-noise sources
120    /// (`TRNOISE`/`TRRANDOM`, ngspice-47 `.options seed=N`, which sets
121    /// its `rndseed`). Default 1 — ngspice's start-up `rndseed` too
122    /// (`main.c`) —, so a deck without the option is reproducible.
123    /// The same seed gives the same noise bits on every platform (see
124    /// `docs/dune-core.md` §Ruído transiente); ngspice's
125    /// `seed=random` (a clock seed) is refused with a warning.
126    pub seed: u64,
127    /// `NOTRNOISE` (ngspice-47's `notrnoise` variable, read by
128    /// `trnoise_state_gen`): every `TRNOISE` source runs with all its
129    /// noise parameters zeroed — just its `DC` value, no grid, no
130    /// breakpoints —, so a deck compares with and without noise
131    /// without editing it. As in ngspice, `TRRANDOM` is untouched
132    /// (its state generator never reads the variable). Default false.
133    pub notrnoise: bool,
134    /// Dune's transient device noise (`.options devnoise`; not a
135    /// SPICE option — neither the 3f5 nor ngspice has it, issue #20):
136    /// every generator `.noise` knows (thermal `4kT·g`, shot `2q|I|`,
137    /// flicker `KF·…/f^EF`) becomes a stochastic current source in
138    /// `.tran`, drawn from the counted streams of `seed`. `false`
139    /// (default): devices are noiseless in time, as in SPICE.
140    pub dev_noise: bool,
141    /// The grid step of the device noise (`.options devnt=NT`,
142    /// seconds): white up to `1/(2·NT)`, a breakpoint every `NT`.
143    /// `None` (default): the `.tran` card's TSTEP.
144    pub dev_noise_step: Option<f64>,
145    /// Worker threads for the per-iteration device load (issue #9,
146    /// lever 1; an API argument, not a SPICE option). `1` (default):
147    /// the sequential load, exactly the 3f5's `CKTload`. `0`: one
148    /// thread per core. `n > 1`: the devices are split into
149    /// contiguous runs evaluated on `n` threads, and their stamps are
150    /// replayed into the matrix in device order, so every result is
151    /// bit-identical to `1` whatever the count. It only pays on
152    /// circuits with many expensive devices (MOS/BSIM); see
153    /// `docs/perf.md` for the measured break-even. Needs the
154    /// `parallel` feature (native builds); without it (and on
155    /// `wasm32`) the value is ignored and the load is sequential.
156    pub load_threads: usize,
157    /// `KEEPOPINFO` (`cktsopt.c`): `.ac`, `.pz` and `.disto` (and,
158    /// as in ngspice-47, `.noise` and `.sp`) first emit their
159    /// operating point as a plot of its own ("AC Operating Point",
160    /// "Distortion Operating Point", "NOISE Operating Point").
161    /// Default false.
162    pub keepopinfo: bool,
163    /// `TRYTOCOMPACT` (`cktsopt.c`, IF_FLAG; `CKTtryToCompact`): LTRA
164    /// lines without an explicit interpolation flag interpolate
165    /// linearly, and after every accepted timepoint where the last
166    /// three points of every LTRA port voltage and current lie on a
167    /// straight line within `COMPACTREL`/`COMPACTABS`, the middle
168    /// point is dropped from the shared history (`ltraacct.c`). Default
169    /// false. Ignored under [`Options::ltra_fit`] and in `.pss`.
170    pub trytocompact: bool,
171}
172
173impl Default for Options {
174    fn default() -> Self {
175        Options {
176            reltol: 1e-3,
177            abstol: 1e-12,
178            vntol: 1e-6,
179            gmin: 1e-12,
180            itl1: 100,
181            itl2: 50,
182            itl4: 10,
183            gminsteps: 1,
184            srcsteps: 1,
185            temp: REFTEMP,
186            tnom: REFTEMP,
187            pivtol: 1e-13,
188            pivrel: 1e-3,
189            noopiter: false,
190            trtol: 7.0,
191            chgtol: 1e-14,
192            method: Method::Trapezoidal,
193            maxord: 2,
194            nopage: false,
195            defl: 1e-4,
196            defw: 1e-4,
197            defad: 0.0,
198            defas: 0.0,
199            max_tran_steps: None,
200            ltra_break_rate: None,
201            ltra_fit: false,
202            load_threads: 1,
203            tmax_divisor: None,
204            seed: 1,
205            notrnoise: false,
206            dev_noise: false,
207            dev_noise_step: None,
208            keepopinfo: false,
209            trytocompact: false,
210        }
211    }
212}
213
214/// Floor of the automatic transient step budget
215/// ([`Options::max_tran_steps`] `= None`).
216pub const TRAN_STEP_FLOOR: usize = 10_000;
217
218/// Internal timepoints allowed per TMAX span of `[0, TSTOP]` by the
219/// automatic transient step budget (3.8× the corpus maximum of
220/// 13.0, `ltra_2`).
221pub const TRAN_STEPS_PER_TMAX: usize = 50;
222
223impl Options {
224    /// The step budget of a `.tran` running to `tstop` with effective
225    /// maximum step `max_step` (see [`Options::max_tran_steps`]).
226    pub fn tran_step_budget(&self, tstop: f64, max_step: f64) -> usize {
227        match self.max_tran_steps {
228            Some(n) => n,
229            None => {
230                let spans = (tstop / max_step).ceil();
231                let auto = if spans.is_finite() && spans >= 0.0 {
232                    (spans as usize).saturating_mul(TRAN_STEPS_PER_TMAX)
233                } else {
234                    usize::MAX
235                };
236                auto.max(TRAN_STEP_FLOOR)
237            }
238        }
239    }
240
241    /// Apply `.options` parameters from the IR. Unknown or
242    /// not-yet-implemented options produce a warning, never an error
243    /// (the 3f5 front end also lets unknown variables through).
244    pub fn apply(&mut self, params: &[Param], diags: &mut Vec<Diagnostic>) {
245        for p in params {
246            let num = match &p.value {
247                ParamValue::Num(v) => Some(*v),
248                _ => None,
249            };
250            macro_rules! num_opt {
251                ($field:ident) => {
252                    match num {
253                        Some(v) => self.$field = v,
254                        None => diags.push(Diagnostic::warning(
255                            format!("option {} needs a numeric value", p.name),
256                            p.span,
257                        )),
258                    }
259                };
260            }
261            match p.name.as_str() {
262                "reltol" => num_opt!(reltol),
263                "abstol" => num_opt!(abstol),
264                "vntol" => num_opt!(vntol),
265                "gmin" => num_opt!(gmin),
266                "pivtol" => num_opt!(pivtol),
267                "pivrel" => num_opt!(pivrel),
268                "itl1" => {
269                    if let Some(v) = count(num, p, diags) {
270                        self.itl1 = v;
271                    }
272                }
273                "itl2" => {
274                    if let Some(v) = count(num, p, diags) {
275                        self.itl2 = v;
276                    }
277                }
278                "itl4" => {
279                    if let Some(v) = count(num, p, diags) {
280                        self.itl4 = v;
281                    }
282                }
283                "maxord" => {
284                    if let Some(v) = count(num, p, diags) {
285                        self.maxord = v;
286                    }
287                }
288                "trtol" => num_opt!(trtol),
289                "chgtol" => num_opt!(chgtol),
290                "method" => match &p.value {
291                    ParamValue::Str(s) if s == "gear" => self.method = Method::Gear,
292                    ParamValue::Str(s) if s == "trap" || s == "trapezoidal" => {
293                        self.method = Method::Trapezoidal;
294                    }
295                    // Dune's opt-in L-stable composite (issue #6).
296                    ParamValue::Str(s) if s == "trbdf2" => self.method = Method::TrBdf2,
297                    _ => diags.push(Diagnostic::warning(
298                        "method must be trap, gear or trbdf2".to_string(),
299                        p.span,
300                    )),
301                },
302                "ltrabkrate" => {
303                    if let Some(v) = count(num, p, diags) {
304                        self.ltra_break_rate = Some(v);
305                    }
306                }
307                // Dune's opt-in LTRA recursive convolution (issue #8):
308                // a bare flag or `ltrafit=0|1`.
309                "ltrafit" => self.ltra_fit = num.is_none_or(|v| v != 0.0),
310                "gminsteps" => {
311                    if let Some(v) = count(num, p, diags) {
312                        self.gminsteps = v;
313                    }
314                }
315                "srcsteps" | "itl6" => {
316                    if let Some(v) = count(num, p, diags) {
317                        self.srcsteps = v;
318                    }
319                }
320                "temp" => match num {
321                    Some(v) => self.temp = v + CTOK,
322                    None => diags.push(Diagnostic::warning(
323                        "option temp needs a value in °C".to_string(),
324                        p.span,
325                    )),
326                },
327                "tnom" => match num {
328                    Some(v) => self.tnom = v + CTOK,
329                    None => diags.push(Diagnostic::warning(
330                        "option tnom needs a value in °C".to_string(),
331                        p.span,
332                    )),
333                },
334                // ngspice-47 `inp.c`: `option seed=val|random`; a
335                // value that `atoi` makes non-positive is skipped with
336                // a warning. Dune never seeds from the clock.
337                "seed" => match num {
338                    Some(v) if v >= 1.0 && v.is_finite() && v < 9.007_199_254_740_992e15 => {
339                        self.seed = v as u64;
340                    }
341                    _ => diags.push(Diagnostic::warning(
342                        format!(
343                            "option seed needs a positive integer (seed=random is not \
344                             supported: runs must be reproducible); keeping seed={}",
345                            self.seed
346                        ),
347                        p.span,
348                    )),
349                },
350                // ngspice-47 `set notrnoise` / `.options notrnoise`
351                // (a bare flag; `notrnoise=0|1` too).
352                "notrnoise" => self.notrnoise = num.is_none_or(|v| v != 0.0),
353                // Dune's transient device noise (issue #20): a bare
354                // flag or `devnoise=0|1`, and its grid step.
355                "devnoise" => self.dev_noise = num.is_none_or(|v| v != 0.0),
356                "devnt" => match num {
357                    Some(v) if v > 0.0 && v.is_finite() => self.dev_noise_step = Some(v),
358                    _ => diags.push(Diagnostic::warning(
359                        "option devnt needs a positive time step".to_string(),
360                        p.span,
361                    )),
362                },
363                "noopiter" => self.noopiter = true,
364                // IF_FLAG: a bare word (or `keepopinfo=0|1`).
365                "keepopinfo" => self.keepopinfo = num.is_none_or(|v| v != 0.0),
366                "trytocompact" => self.trytocompact = num.is_none_or(|v| v != 0.0),
367                "nopage" => self.nopage = true,
368                // `cktsopt.c` has a copy-paste bug that makes `defas`
369                // set the drain area; ngspice fixed it and so do we
370                // (`corpus/KNOWN_DIFFS.md`).
371                "defl" => num_opt!(defl),
372                "defw" => num_opt!(defw),
373                "defad" => num_opt!(defad),
374                "defas" => num_opt!(defas),
375                // Recognized 3f5 options that only matter to later
376                // phases (output formatting, statistics, devices).
377                "itl3" | "itl5" | "acct" | "list" | "nomod" | "node" | "opts" | "bypass"
378                | "badmos3" => {}
379                // `spiceif.c`'s `obsolete[]`: SPICE2 artifacts (lvlcod
380                // generated machine code for the solver) that must
381                // never do anything — the 3f5 words it "is obsolete".
382                "limpts" | "limtim" | "lvlcod" => diags.push(Diagnostic::warning(
383                    format!("option {} is an obsolete SPICE2 option; ignored", p.name),
384                    p.span,
385                )),
386                other => diags.push(Diagnostic::warning(
387                    format!("option {other} is not implemented yet; ignored"),
388                    p.span,
389                )),
390            }
391        }
392    }
393}
394
395fn count(num: Option<f64>, p: &Param, diags: &mut Vec<Diagnostic>) -> Option<usize> {
396    match num {
397        Some(v) if v >= 0.0 && v.is_finite() && v < 1e9 => Some(v as usize),
398        _ => {
399            diags.push(Diagnostic::warning(
400                format!("option {} needs a non-negative count", p.name),
401                p.span,
402            ));
403            None
404        }
405    }
406}