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}