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}