Skip to main content

dune_core/
options.rs

1//! Simulator options (`lib/ckt/cktinit.c`, `lib/ckt/cktntask.c`;
2//! names from `spice.txt` §SIMULATOR VARIABLES). Temperatures arrive
3//! in °C on the card and live in kelvin here, like the C.
4//!
5//! [`Options::default`] is Dune's profile; [`Options::spice3`]
6//! (`.options compat=spice3`) restores every default where Dune
7//! departs from the 3f5 — see [`Compat`].
8
9use dune_netlist::Diagnostic;
10use dune_netlist::ir::{Param, ParamValue};
11
12use crate::consts::{CTOK, REFTEMP};
13use crate::integ::Method;
14
15/// The defaults profile (`.options compat=`, D-0046).
16#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
17pub enum Compat {
18    /// `dune` (default): a `.tran` without `method=` integrates with
19    /// [`Method::TrBdf2`]. Measured on the corpus against refined
20    /// references (`dune-oracle --accuracy`, docs/dune-oracle.md), it
21    /// is closer to the converged answer than the trapezoidal rule at
22    /// the same step on 16 of 29 decks and farther on none, and it
23    /// does not ring. The `.pss` shooting uses the same method
24    /// ([`Options::pss_method`]), and a transient locates the instant
25    /// an `S`/`W` switch flips to within `RELTOL·TMAX` instead of
26    /// flipping it at the first timepoint past the threshold
27    /// (D-0047). The slope breakpoints of the LTRA lines are capped
28    /// ([`Options::ltra_slope_rate`]).
29    #[default]
30    Dune,
31    /// `spice3`: the SPICE3f5 defaults — a `.tran` without `method=`
32    /// uses the trapezoidal rule, and switches flip on the grid. What
33    /// the ngspice oracle runs under, and what reproduces a SPICE3
34    /// run.
35    Spice3,
36}
37
38#[derive(Clone, Debug)]
39pub struct Options {
40    /// The defaults profile: what an analysis does where the fields
41    /// below leave it the choice ([`Options::method`]), and whether
42    /// a transient locates switch events.
43    pub compat: Compat,
44    /// `RELTOL`, default 1e-3.
45    pub reltol: f64,
46    /// `ABSTOL`, default 1e-12 (amperes).
47    pub abstol: f64,
48    /// `VNTOL`, default 1e-6 (volts).
49    pub vntol: f64,
50    /// `GMIN`, default 1e-12.
51    pub gmin: f64,
52    /// `ITL1`: DC iteration limit, default 100.
53    pub itl1: usize,
54    /// `ITL2`: DC transfer-curve iteration limit, default 50.
55    pub itl2: usize,
56    /// `ITL4`: transient timepoint iteration limit, default 10
57    /// (`CKTtranMaxIter`).
58    pub itl4: usize,
59    /// Gmin steps in `CKTop`. 1 (the ngspice-47 default) selects the
60    /// dynamic Gillespie stepping of the diagonal gmin followed, if it
61    /// fails, by the "true gmin" stepping of the devices' `gmin`;
62    /// n > 1 runs the 3f5 fixed ladder with n steps; 0 skips gmin
63    /// stepping (`corpus/KNOWN_DIFFS.md`).
64    pub gminsteps: usize,
65    /// Source steps in `CKTop`. 1 (the ngspice-47 default,
66    /// `cktntask.c`) selects Gillespie's adaptive source ramp; n > 1
67    /// runs the 3f5 fixed ladder of n steps (the 3f5 default is 10);
68    /// 0 skips source stepping (`corpus/KNOWN_DIFFS.md`).
69    pub srcsteps: usize,
70    /// Operating temperature in kelvin (`TEMP`, default 27 °C).
71    pub temp: f64,
72    /// Nominal parameter-measurement temperature (`TNOM`, 27 °C).
73    pub tnom: f64,
74    /// `PIVTOL`, default 1e-13.
75    pub pivtol: f64,
76    /// `PIVREL`, default 1e-3.
77    pub pivrel: f64,
78    /// `NOOPITER`: go directly to gmin stepping.
79    pub noopiter: bool,
80    /// `TRTOL`: truncation-error overestimation factor, default 7.
81    /// What the step control uses is [`Options::lte_trtol`].
82    pub trtol: f64,
83    /// `CHGTOL`: charge tolerance, default 1e-14.
84    pub chgtol: f64,
85    /// `METHOD`: `trap`, `gear` or Dune's `trbdf2`
86    /// ([`Method::TrBdf2`]; not a SPICE value). `None` (default)
87    /// leaves it to the analysis: see [`Options::tran_method`] and
88    /// [`Options::pss_method`].
89    pub method: Option<Method>,
90    /// `MAXORD` for Gear, default 2.
91    pub maxord: usize,
92    /// `NOPAGE`: suppress page breaks in printed output.
93    pub nopage: bool,
94    /// `DEFL`/`DEFW`: default MOS channel length/width, meters
95    /// (`CKTdefaultMosL/W`, 1e-4).
96    pub defl: f64,
97    pub defw: f64,
98    /// `DEFAD`/`DEFAS`: default MOS drain/source diffusion area, m²
99    /// (`CKTdefaultMosAD/AS`, 0).
100    pub defad: f64,
101    pub defas: f64,
102    /// Dune's transient step budget (not a SPICE option; no `.options`
103    /// card sets it): the most internal timepoints — accepted or
104    /// rejected — one `.tran` may attempt before it stops with
105    /// [`Error::StepLimit`](crate::Error::StepLimit). The 3f5 and
106    /// ngspice-47 both parse `ITL5` ("total transient iteration
107    /// limit") and ignore it (`cktsopt.c`: `case OPT_ITL5: break;`),
108    /// so neither bounds a run whose breakpoints cascade (D-0022).
109    ///
110    /// `None` (default): automatic, scaled to the deck —
111    /// `max(TRAN_STEP_FLOOR, TRAN_STEPS_PER_TMAX · ⌈TSTOP/TMAX⌉)`
112    /// with the effective TMAX, i.e. 10 000 steps or 50 per maximum
113    /// step span, whichever is more (the corpus peaks at 13 per span,
114    /// ltra_2; a fixed budget cannot work because a cascading LTRA
115    /// deck costs O(n²) in its history length — D-0022). `Some(n)`:
116    /// at most `n` (`Some(usize::MAX)` is unlimited).
117    pub max_tran_steps: Option<usize>,
118    /// Dune's LTRA breakpoint hygiene (`.options ltrabkrate=N`; not a
119    /// SPICE option, issue #6). The slope rule of `LTRAaccept` plants
120    /// a breakpoint one delay after every accepted point where an
121    /// incident wave's slope changes; `CKTsetBreak` merges only
122    /// within `5e-5·TMAX`, so a ringing load can feed it an echo
123    /// cascade (D-0022). `Some(n)`, `n > 0`: a slope breakpoint that
124    /// falls within `TMAX/n` of one already scheduled is dropped,
125    /// which caps them at about `n` per TMAX span. `Some(0)`:
126    /// faithful, every request scheduled. `None` (default): the
127    /// profile's — [`LTRA_BREAK_RATE`] under [`Compat::Dune`],
128    /// faithful under [`Compat::Spice3`]; see
129    /// [`Options::ltra_slope_rate`].
130    pub ltra_break_rate: Option<usize>,
131    /// Dune's opt-in recursive convolution for LTRA lines
132    /// (`.options ltrafit`; not a SPICE option, issue #8). The 3f5
133    /// recomputes every line's convolution over the whole accepted
134    /// history at each timepoint — O(n) per point, quadratic per run.
135    /// `true`: the RLC and RC impulse responses become exponential
136    /// sums (an exact branch-cut quadrature, error budget 1e-6 per
137    /// kernel, reported in the run's notes) advanced once per accepted
138    /// point, with the newest segment kept exact — O(1) per point.
139    /// `false` (default): faithful.
140    pub ltra_fit: bool,
141    /// Dune's transient refinement knob (an API argument, not a SPICE
142    /// option: no `.options` card sets it, issue #16). `Some(d)`,
143    /// `d ≥ 1` and finite: every `.tran` runs with its effective
144    /// maximum step divided by `d` — the 4th argument of the card
145    /// when given, else the default `min(TSTEP, (TSTOP−TSTART)/50)`.
146    /// Everything derived from TMAX (`delmin`, the breakpoint merge
147    /// radius, the step budget) follows, so the run is the one the
148    /// deck would give with that card's TMAX edited to `TMAX/d`.
149    /// It is not a `.options` key because TMAX is a parameter of the
150    /// analysis card, and the divisor applies to every `.tran` card of
151    /// the deck whatever TMAX each one carries. Any other value makes
152    /// `.tran` fail with [`Error::Build`](crate::Error::Build).
153    /// `None` (default): the card's TMAX as is.
154    pub tmax_divisor: Option<f64>,
155    /// `SEED`: the seed of the transient-noise sources
156    /// (`TRNOISE`/`TRRANDOM`, ngspice-47 `.options seed=N`, which sets
157    /// its `rndseed`). Default 1 — ngspice's start-up `rndseed` too
158    /// (`main.c`) —, so a deck without the option is reproducible.
159    /// The same seed gives the same noise bits on every platform (see
160    /// `docs/dune-core.md` §Ruído transiente); ngspice's
161    /// `seed=random` (a clock seed) is refused with a warning.
162    pub seed: u64,
163    /// `NOTRNOISE` (ngspice-47's `notrnoise` variable, read by
164    /// `trnoise_state_gen`): every `TRNOISE` source runs with all its
165    /// noise parameters zeroed — just its `DC` value, no grid, no
166    /// breakpoints —, so a deck compares with and without noise
167    /// without editing it. As in ngspice, `TRRANDOM` is untouched
168    /// (its state generator never reads the variable). Default false.
169    pub notrnoise: bool,
170    /// Dune's transient device noise (`.options devnoise`; not a
171    /// SPICE option — neither the 3f5 nor ngspice has it, issue #20):
172    /// every generator `.noise` knows (thermal `4kT·g`, shot `2q|I|`,
173    /// flicker `KF·…/f^EF`) becomes a stochastic current source in
174    /// `.tran`, drawn from the counted streams of `seed`. `false`
175    /// (default): devices are noiseless in time, as in SPICE.
176    pub dev_noise: bool,
177    /// The grid step of the device noise (`.options devnt=NT`,
178    /// seconds): white up to `1/(2·NT)`, a breakpoint every `NT`.
179    /// `None` (default): the `.tran` card's TSTEP.
180    pub dev_noise_step: Option<f64>,
181    /// Worker threads for the per-iteration device load (issue #9,
182    /// lever 1; an API argument, not a SPICE option). `1` (default):
183    /// the sequential load, exactly the 3f5's `CKTload`. `0`: one
184    /// thread per core. `n > 1`: the devices are split into
185    /// contiguous runs evaluated on `n` threads, and their stamps are
186    /// replayed into the matrix in device order, so every result is
187    /// bit-identical to `1` whatever the count. It only pays on
188    /// circuits with many expensive devices (MOS/BSIM); see
189    /// `docs/perf.md` for the measured break-even. Needs the
190    /// `parallel` feature (native builds); without it (and on
191    /// `wasm32`) the value is ignored and the load is sequential.
192    pub load_threads: usize,
193    /// `KEEPOPINFO` (`cktsopt.c`): `.ac`, `.pz` and `.disto` (and,
194    /// as in ngspice-47, `.noise` and `.sp`) first emit their
195    /// operating point as a plot of its own ("AC Operating Point",
196    /// "Distortion Operating Point", "NOISE Operating Point").
197    /// Default false.
198    pub keepopinfo: bool,
199    /// `TRYTOCOMPACT` (`cktsopt.c`, IF_FLAG; `CKTtryToCompact`): LTRA
200    /// lines without an explicit interpolation flag interpolate
201    /// linearly, and after every accepted timepoint where the last
202    /// three points of every LTRA port voltage and current lie on a
203    /// straight line within `COMPACTREL`/`COMPACTABS`, the middle
204    /// point is dropped from the shared history (`ltraacct.c`). Default
205    /// false. Ignored under [`Options::ltra_fit`] and in `.pss`.
206    pub trytocompact: bool,
207}
208
209impl Default for Options {
210    fn default() -> Self {
211        Options {
212            compat: Compat::Dune,
213            reltol: 1e-3,
214            abstol: 1e-12,
215            vntol: 1e-6,
216            gmin: 1e-12,
217            itl1: 100,
218            itl2: 50,
219            itl4: 10,
220            gminsteps: 1,
221            srcsteps: 1,
222            temp: REFTEMP,
223            tnom: REFTEMP,
224            pivtol: 1e-13,
225            pivrel: 1e-3,
226            noopiter: false,
227            trtol: SPICE_TRTOL,
228            chgtol: 1e-14,
229            method: None,
230            maxord: 2,
231            nopage: false,
232            defl: 1e-4,
233            defw: 1e-4,
234            defad: 0.0,
235            defas: 0.0,
236            max_tran_steps: None,
237            ltra_break_rate: None,
238            ltra_fit: false,
239            load_threads: 1,
240            tmax_divisor: None,
241            seed: 1,
242            notrnoise: false,
243            dev_noise: false,
244            dev_noise_step: None,
245            keepopinfo: false,
246            trytocompact: false,
247        }
248    }
249}
250
251/// Floor of the automatic transient step budget
252/// ([`Options::max_tran_steps`] `= None`).
253pub const TRAN_STEP_FLOOR: usize = 10_000;
254
255/// Internal timepoints allowed per TMAX span of `[0, TSTOP]` by the
256/// automatic transient step budget (3.8× the corpus maximum of
257/// 13.0, `ltra_2`).
258pub const TRAN_STEPS_PER_TMAX: usize = 50;
259
260/// SPICE's default `TRTOL`, the overestimation its step control
261/// assumes of `CKTterr` ([`Options::lte_trtol`]).
262pub const SPICE_TRTOL: f64 = 7.0;
263
264/// The LTRA breakpoint hygiene of the Dune profile
265/// ([`Options::ltra_break_rate`] `= None`): about this many slope
266/// breakpoints per TMAX span (D-0047). Without an echo cascade it
267/// changes nothing — every LTRA deck of the corpus is bit-identical
268/// with it on and off at the deck's step — and with one it decides
269/// whether the run ends: `regress/ltra_compact` refined 20 and 40
270/// times exceeds the step budget without it. A lower rate already
271/// changes that deck at its own step.
272pub const LTRA_BREAK_RATE: usize = 10;
273
274impl Options {
275    /// The SPICE3f5 defaults ([`Compat::Spice3`]).
276    pub fn spice3() -> Self {
277        Options {
278            compat: Compat::Spice3,
279            ..Options::default()
280        }
281    }
282
283    /// The integration method of a `.tran`: [`Options::method`] when
284    /// given, else the profile's — TR-BDF2 under [`Compat::Dune`],
285    /// the trapezoidal rule under [`Compat::Spice3`].
286    pub fn tran_method(&self) -> Method {
287        self.method.unwrap_or(match self.compat {
288            Compat::Dune => Method::TrBdf2,
289            Compat::Spice3 => Method::Trapezoidal,
290        })
291    }
292
293    /// Whether a transient locates switch events (D-0047): under
294    /// [`Compat::Dune`] a step in which an `S`/`W` switch flips is
295    /// cut so that the flip lands within `RELTOL·TMAX` of the instant
296    /// its control crossed the threshold. Under [`Compat::Spice3`]
297    /// the switch flips at whatever timepoint first sees the control
298    /// past the threshold, as in the 3f5 and ngspice.
299    pub(crate) fn locates_events(&self) -> bool {
300        self.compat == Compat::Dune
301    }
302
303    /// The factor the transient step control multiplies its error
304    /// tolerance by. `TRTOL` (7 by default) exists because the
305    /// divided-difference estimate of `CKTterr` overestimates the
306    /// local truncation error of the trapezoidal rule and of Gear.
307    /// The composite TR-BDF2 step carries an embedded estimate that
308    /// does not, so under [`Compat::Dune`] a TR-BDF2 transient is
309    /// held to `TRTOL/7` — the default is the estimate itself — and
310    /// `.options trtol=` still scales it (D-0049). Measured on the
311    /// accuracy ruler: margins sum 1.83 → 1.17 for 3 % more
312    /// timepoints (`mosamp2` 0.45 → 0.08). Every other combination
313    /// uses `TRTOL` as SPICE does.
314    pub fn lte_trtol(&self) -> f64 {
315        match (self.compat, self.tran_method()) {
316            (Compat::Dune, Method::TrBdf2) => self.trtol / SPICE_TRTOL,
317            _ => self.trtol,
318        }
319    }
320
321    /// The LTRA breakpoint hygiene in force
322    /// ([`Options::ltra_break_rate`]): the slope breakpoints allowed
323    /// per TMAX span, `None` when every request is scheduled.
324    pub fn ltra_slope_rate(&self) -> Option<usize> {
325        match (self.ltra_break_rate, self.compat) {
326            (Some(0), _) | (None, Compat::Spice3) => None,
327            (Some(n), _) => Some(n),
328            (None, Compat::Dune) => Some(LTRA_BREAK_RATE),
329        }
330    }
331
332    /// The integration method of the `.pss` shooting:
333    /// [`Options::method`] when given, else the profile's, as for
334    /// `.tran` ([`Options::tran_method`]).
335    pub fn pss_method(&self) -> Method {
336        self.tran_method()
337    }
338
339    /// The integration method of the stabilization run of an
340    /// autonomous `.pss` (the transient to TSTAB in which the
341    /// oscillation starts and its period is counted): the trapezoidal
342    /// rule, whatever [`Options::method`] says. That run steps on a
343    /// grid that does not know the period yet (TMAX = TSTAB/1000,
344    /// which may exceed it), and the trapezoidal rule is the one
345    /// method whose growth region is exactly the right half-plane: a
346    /// mode that grows in the circuit grows in the run at any step.
347    /// An L-stable method damps it instead (`osc_colpitts` never
348    /// starts under TR-BDF2 or Gear there). The run only supplies the
349    /// shooting's starting point and period estimate; the result is
350    /// integrated with [`Options::pss_method`].
351    pub fn pss_stab_method(&self) -> Method {
352        Method::Trapezoidal
353    }
354
355    /// The step budget of a `.tran` running to `tstop` with effective
356    /// maximum step `max_step` (see [`Options::max_tran_steps`]).
357    pub fn tran_step_budget(&self, tstop: f64, max_step: f64) -> usize {
358        match self.max_tran_steps {
359            Some(n) => n,
360            None => {
361                let spans = (tstop / max_step).ceil();
362                let auto = if spans.is_finite() && spans >= 0.0 {
363                    (spans as usize).saturating_mul(TRAN_STEPS_PER_TMAX)
364                } else {
365                    usize::MAX
366                };
367                auto.max(TRAN_STEP_FLOOR)
368            }
369        }
370    }
371
372    /// Apply `.options` parameters from the IR. Unknown or
373    /// not-yet-implemented options produce a warning, never an error
374    /// (the 3f5 front end also lets unknown variables through).
375    pub fn apply(&mut self, params: &[Param], diags: &mut Vec<Diagnostic>) {
376        for p in params {
377            let num = match &p.value {
378                ParamValue::Num(v) => Some(*v),
379                _ => None,
380            };
381            macro_rules! num_opt {
382                ($field:ident) => {
383                    match num {
384                        Some(v) => self.$field = v,
385                        None => diags.push(Diagnostic::warning(
386                            format!("option {} needs a numeric value", p.name),
387                            p.span,
388                        )),
389                    }
390                };
391            }
392            match p.name.as_str() {
393                "reltol" => num_opt!(reltol),
394                "abstol" => num_opt!(abstol),
395                "vntol" => num_opt!(vntol),
396                "gmin" => num_opt!(gmin),
397                "pivtol" => num_opt!(pivtol),
398                "pivrel" => num_opt!(pivrel),
399                "itl1" => {
400                    if let Some(v) = count(num, p, diags) {
401                        self.itl1 = v;
402                    }
403                }
404                "itl2" => {
405                    if let Some(v) = count(num, p, diags) {
406                        self.itl2 = v;
407                    }
408                }
409                "itl4" => {
410                    if let Some(v) = count(num, p, diags) {
411                        self.itl4 = v;
412                    }
413                }
414                "maxord" => {
415                    if let Some(v) = count(num, p, diags) {
416                        self.maxord = v;
417                    }
418                }
419                "trtol" => num_opt!(trtol),
420                "chgtol" => num_opt!(chgtol),
421                "method" => match &p.value {
422                    ParamValue::Str(s) if s == "gear" => self.method = Some(Method::Gear),
423                    ParamValue::Str(s) if s == "trap" || s == "trapezoidal" => {
424                        self.method = Some(Method::Trapezoidal);
425                    }
426                    // Dune's L-stable composite (issue #6).
427                    ParamValue::Str(s) if s == "trbdf2" => self.method = Some(Method::TrBdf2),
428                    _ => diags.push(Diagnostic::warning(
429                        "method must be trap, gear or trbdf2".to_string(),
430                        p.span,
431                    )),
432                },
433                // The defaults profile (D-0046).
434                "compat" => match &p.value {
435                    ParamValue::Str(s) if s == "spice3" => self.compat = Compat::Spice3,
436                    ParamValue::Str(s) if s == "dune" => self.compat = Compat::Dune,
437                    _ => diags.push(Diagnostic::warning(
438                        "compat must be spice3 or dune".to_string(),
439                        p.span,
440                    )),
441                },
442                "ltrabkrate" => {
443                    if let Some(v) = count(num, p, diags) {
444                        self.ltra_break_rate = Some(v);
445                    }
446                }
447                // Dune's opt-in LTRA recursive convolution (issue #8):
448                // a bare flag or `ltrafit=0|1`.
449                "ltrafit" => self.ltra_fit = num.is_none_or(|v| v != 0.0),
450                "gminsteps" => {
451                    if let Some(v) = count(num, p, diags) {
452                        self.gminsteps = v;
453                    }
454                }
455                "srcsteps" | "itl6" => {
456                    if let Some(v) = count(num, p, diags) {
457                        self.srcsteps = v;
458                    }
459                }
460                "temp" => match num {
461                    Some(v) => self.temp = v + CTOK,
462                    None => diags.push(Diagnostic::warning(
463                        "option temp needs a value in °C".to_string(),
464                        p.span,
465                    )),
466                },
467                "tnom" => match num {
468                    Some(v) => self.tnom = v + CTOK,
469                    None => diags.push(Diagnostic::warning(
470                        "option tnom needs a value in °C".to_string(),
471                        p.span,
472                    )),
473                },
474                // ngspice-47 `inp.c`: `option seed=val|random`; a
475                // value that `atoi` makes non-positive is skipped with
476                // a warning. Dune never seeds from the clock.
477                "seed" => match num {
478                    Some(v) if v >= 1.0 && v.is_finite() && v < 9.007_199_254_740_992e15 => {
479                        self.seed = v as u64;
480                    }
481                    _ => diags.push(Diagnostic::warning(
482                        format!(
483                            "option seed needs a positive integer (seed=random is not \
484                             supported: runs must be reproducible); keeping seed={}",
485                            self.seed
486                        ),
487                        p.span,
488                    )),
489                },
490                // ngspice-47 `set notrnoise` / `.options notrnoise`
491                // (a bare flag; `notrnoise=0|1` too).
492                "notrnoise" => self.notrnoise = num.is_none_or(|v| v != 0.0),
493                // Dune's transient device noise (issue #20): a bare
494                // flag or `devnoise=0|1`, and its grid step.
495                "devnoise" => self.dev_noise = num.is_none_or(|v| v != 0.0),
496                "devnt" => match num {
497                    Some(v) if v > 0.0 && v.is_finite() => self.dev_noise_step = Some(v),
498                    _ => diags.push(Diagnostic::warning(
499                        "option devnt needs a positive time step".to_string(),
500                        p.span,
501                    )),
502                },
503                "noopiter" => self.noopiter = true,
504                // IF_FLAG: a bare word (or `keepopinfo=0|1`).
505                "keepopinfo" => self.keepopinfo = num.is_none_or(|v| v != 0.0),
506                "trytocompact" => self.trytocompact = num.is_none_or(|v| v != 0.0),
507                "nopage" => self.nopage = true,
508                // `cktsopt.c` has a copy-paste bug that makes `defas`
509                // set the drain area; ngspice fixed it and so do we
510                // (`corpus/KNOWN_DIFFS.md`).
511                "defl" => num_opt!(defl),
512                "defw" => num_opt!(defw),
513                "defad" => num_opt!(defad),
514                "defas" => num_opt!(defas),
515                // Recognized 3f5 options that only matter to later
516                // phases (output formatting, statistics, devices).
517                "itl3" | "itl5" | "acct" | "list" | "nomod" | "node" | "opts" | "bypass"
518                | "badmos3" => {}
519                // `spiceif.c`'s `obsolete[]`: SPICE2 artifacts (lvlcod
520                // generated machine code for the solver) that must
521                // never do anything — the 3f5 words it "is obsolete".
522                "limpts" | "limtim" | "lvlcod" => diags.push(Diagnostic::warning(
523                    format!("option {} is an obsolete SPICE2 option; ignored", p.name),
524                    p.span,
525                )),
526                other => diags.push(Diagnostic::warning(
527                    format!("option {other} is not implemented yet; ignored"),
528                    p.span,
529                )),
530            }
531        }
532    }
533}
534
535fn count(num: Option<f64>, p: &Param, diags: &mut Vec<Diagnostic>) -> Option<usize> {
536    match num {
537        Some(v) if v >= 0.0 && v.is_finite() && v < 1e9 => Some(v as usize),
538        _ => {
539            diags.push(Diagnostic::warning(
540                format!("option {} needs a non-negative count", p.name),
541                p.span,
542            ));
543            None
544        }
545    }
546}