Skip to main content

dune_core/
sens.rs

1//! `.sens` — DC and AC small-signal sensitivity (`ckt/cktsens.c`,
2//! `ckt/cktsgen.c`).
3//!
4//! This is the perturbation method of the 3f5's `sens_sens` (the
5//! Meikle adjoint `SENS2` was never compiled into the 3f5 front end;
6//! ngspice-47 ships the same `cktsens.c`). After the operating
7//! point — and, for AC, the small-signal solve at each frequency —
8//! every parameter `p` of every device is visited in turn
9//! (`sgen_next`): the device alone is loaded into an empty matrix
10//! and RHS, `p` is moved by `δ = 1e-6·p` (`1e-6` when `p == 0`), the
11//! device is loaded again, `p` is restored, and with `ΔY`, `ΔI` the
12//! difference of the two loads one more solve against the factored
13//! system gives
14//!
15//! `∂x/∂p ≈ Y⁻¹·(ΔI − ΔY·E) / δ`,
16//!
17//! of which the output (`V(N1,N2)` or a branch current) is kept.
18//! Vector names follow `cktsens.c`: `<inst>:<kw>` for a model
19//! parameter, `<inst>` for the principal instance parameter,
20//! `<inst>_<kw>` for the others; the parameter set is ngspice-47's
21//! (which lists zero-valued parameters the 3f5 skipped), restricted
22//! to what each Dune device models (see `Device::sens_params`).
23//! ngspice's optional filters (glob patterns on those names) select
24//! a subset.
25
26use dune_raw::{Data, VarType};
27use num_complex::Complex64;
28
29use crate::ac::AcSweep;
30use crate::circuit::Circuit;
31use crate::engine::Engine;
32use crate::error::Error;
33use crate::matrix::{AnKind, InitMode};
34use crate::options::Options;
35use crate::solution::Solution;
36use crate::tf::{OutputVar, resolve_output};
37
38/// `Sens_Delta`, `Sens_Abs_Delta` (`cktsens.c`).
39const SENS_DELTA: f64 = 1e-6;
40const SENS_ABS_DELTA: f64 = 1e-6;
41
42/// The AC form of `.sens`: `AC DEC|OCT|LIN N FSTART FSTOP`.
43#[derive(Clone, Copy, Debug, PartialEq)]
44pub struct SensAc {
45    pub sweep: AcSweep,
46    /// Points per decade/octave, or the number of points of a `LIN`
47    /// sweep (`count_steps`).
48    pub points: usize,
49    pub fstart: f64,
50    pub fstop: f64,
51}
52
53/// Result of a `.sens` analysis: one real point (DC) or one complex
54/// point per frequency (AC), one vector per perturbed parameter.
55#[derive(Clone, Debug)]
56pub struct Sens {
57    /// The `Sensitivity Analysis` plot: `v(<name>)` vectors (real for
58    /// DC; complex after a `frequency` scale for AC).
59    pub solution: Solution,
60    /// Setup remarks (diode temperature-adjust notes, …).
61    pub notes: Vec<String>,
62}
63
64/// `scan` (ngspice `cktsens.c`): glob match with `*` and `?`.
65///
66/// Iterative two-pointer form, linear in `filter.len() + name.len()`:
67/// on a mismatch it retries from the last `*`, one name byte later.
68/// The C recurses per `*`, which a hostile card (`a*a*a*a*…` against
69/// a long name) turns exponential — a deck must never hang (CLAUDE.md
70/// princípio 4). Same accepted language.
71fn glob(filter: &[u8], name: &[u8]) -> bool {
72    let (mut f, mut n) = (0, 0);
73    let mut star: Option<(usize, usize)> = None; // (filter pos after '*', name pos it matched)
74    while n < name.len() {
75        if f < filter.len() && (filter[f] == name[n] || filter[f] == b'?') {
76            f += 1;
77            n += 1;
78        } else if f < filter.len() && filter[f] == b'*' {
79            star = Some((f + 1, n));
80            f += 1;
81        } else if let Some((sf, sn)) = star {
82            f = sf;
83            n = sn + 1;
84            star = Some((sf, sn + 1));
85        } else {
86            return false;
87        }
88    }
89    while f < filter.len() && filter[f] == b'*' {
90        f += 1;
91    }
92    f == filter.len()
93}
94
95/// Position of a device type in ngspice's `DEVices[]` (`dev.c`),
96/// which fixes the order `sgen_next` walks the types in.
97fn dev_order(name: &str) -> usize {
98    match name.as_bytes().first().map(u8::to_ascii_lowercase) {
99        Some(b'c') => 0,
100        Some(b'f') => 1,
101        Some(b'h') => 2,
102        Some(b'd') => 3,
103        Some(b'l') => 4,
104        Some(b'k') => 5,
105        Some(b'i') => 6,
106        Some(b'r') => 7,
107        Some(b'g') => 8,
108        Some(b'e') => 9,
109        Some(b'v') => 10,
110        _ => 11,
111    }
112}
113
114/// `count_steps` (`cktsens.c`): number of frequencies and the step.
115/// The octave count divides `ln(high/low)` by `M_LOG2E` (so it
116/// multiplies by `ln 2` instead of dividing) — kept, as in the C.
117fn count_steps(ac: &SensAc) -> (usize, f64) {
118    let steps = ac.points.max(1) as f64;
119    let (mut low, mut high) = (ac.fstart, ac.fstop);
120    let (n, s) = match ac.sweep {
121        AcSweep::Linear => (steps, (high - low) / steps),
122        AcSweep::Decade => {
123            if low <= 0.0 {
124                low = 1e-3;
125            }
126            if high <= low {
127                high = 10.0 * low;
128            }
129            (
130                (steps * (high / low).log10() + 1.01).trunc(),
131                10f64.powf(1.0 / steps),
132            )
133        }
134        AcSweep::Octave => {
135            if low <= 0.0 {
136                low = 1e-3;
137            }
138            if high <= low {
139                high = 2.0 * low;
140            }
141            (
142                (steps * (high / low).ln() / std::f64::consts::LOG2_E + 1.01).trunc(),
143                2f64.powf(1.0 / steps),
144            )
145        }
146    };
147    let n = if n.is_finite() && n >= 1.0 {
148        n.min(1e6) as usize
149    } else {
150        1
151    };
152    (n, s)
153}
154
155impl Circuit {
156    /// Run `.sens OUTVAR [FILTER…] [AC DEC|OCT|LIN N FSTART FSTOP]`
157    /// (`filters` empty: every parameter).
158    pub fn sens(
159        &self,
160        opts: &Options,
161        output: &OutputVar,
162        filters: &[String],
163        ac: Option<SensAc>,
164    ) -> Result<Sens, Error> {
165        let built = self.build(opts)?;
166        let notes = built.notes.clone();
167        let mut engine = Engine::new(built);
168        engine.set_kind(AnKind::DcOp, false);
169        engine.op(opts)?;
170        let out = resolve_output(&engine, output, ".sens")?;
171
172        // The parameter walk, in `sgen_next` order: device types in
173        // `DEVices[]` order, instances last-to-first within a type
174        // (ngspice prepends each new instance to its model's list).
175        let params = engine.sens_params(ac.is_some())?;
176        let mut devs: Vec<usize> = (0..params.len()).collect();
177        devs.sort_by_key(|&d| (dev_order(&engine.names[d]), std::cmp::Reverse(d)));
178        let filters: Vec<String> = filters.iter().map(|f| f.to_ascii_lowercase()).collect();
179        let mut walk: Vec<(usize, &'static str, f64, String)> = Vec::new();
180        for &d in &devs {
181            for p in &params[d] {
182                let name = format!("{}{}", engine.names[d], p.suffix);
183                if filters.is_empty() || filters.iter().any(|f| glob(f.as_bytes(), name.as_bytes()))
184                {
185                    walk.push((d, p.suffix, p.value, name));
186                }
187            }
188        }
189        if walk.is_empty() {
190            return Err(Error::Build(
191                ".sens: no parameter matches the filters".into(),
192            ));
193        }
194        let delta_of = |value: f64| {
195            if value != 0.0 {
196                value * SENS_DELTA
197            } else {
198                SENS_ABS_DELTA
199            }
200        };
201
202        let mut variables: Vec<(String, VarType)> = Vec::new();
203        let solution = match ac {
204            None => {
205                let mut cols = Vec::with_capacity(walk.len());
206                for (d, suffix, value, name) in &walk {
207                    let delta = delta_of(*value);
208                    let w = engine.sens_dc_column(opts, *d, suffix, *value, delta)?;
209                    cols.push(vec![out.read(&w)]);
210                    variables.push((format!("v({name})"), VarType::Voltage));
211                }
212                Solution {
213                    plotname: "Sensitivity Analysis".to_string(),
214                    variables,
215                    data: Data::Real(cols),
216                    scale: None,
217                }
218            }
219            Some(ac) => {
220                engine.load_all_public(InitMode::Smsig, opts);
221                let (nfreqs, step) = count_steps(&ac);
222                if nfreqs.saturating_mul(walk.len() + 1) > 50_000_000 {
223                    return Err(Error::Build(
224                        ".sens produces too many points; refusing".into(),
225                    ));
226                }
227                variables.push(("frequency".to_string(), VarType::Frequency));
228                let mut cols: Vec<Vec<Complex64>> = vec![Vec::new(); walk.len() + 1];
229                let mut freq = ac.fstart;
230                for _ in 0..nfreqs {
231                    let omega = 2.0 * std::f64::consts::PI * freq;
232                    let (er, ei) = engine.sens_ac_solution(omega)?;
233                    cols[0].push(Complex64::new(freq, 0.0));
234                    for (k, (d, suffix, value, _)) in walk.iter().enumerate() {
235                        let delta = delta_of(*value);
236                        let (wr, wi) =
237                            engine.sens_ac_column(omega, *d, suffix, *value, delta, (&er, &ei))?;
238                        cols[k + 1].push(Complex64::new(out.read(&wr), out.read(&wi)));
239                    }
240                    // `inc_freq` compares the step type against
241                    // `LINEAR` (3) instead of `SENS_LINEAR` (15), so
242                    // every sweep — linear ones included — multiplies.
243                    freq *= step;
244                }
245                for (_, _, _, name) in &walk {
246                    variables.push((format!("v({name})"), VarType::Voltage));
247                }
248                Solution {
249                    plotname: "Sensitivity Analysis".to_string(),
250                    variables,
251                    data: Data::Complex(cols),
252                    scale: Some(0),
253                }
254            }
255        };
256        Ok(Sens { solution, notes })
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263
264    #[test]
265    fn glob_matches_like_ngspice_scan() {
266        assert!(glob(b"r*", b"r1_m"));
267        assert!(glob(b"*", b"anything"));
268        assert!(glob(b"d1:is", b"d1:is"));
269        assert!(!glob(b"d1:is", b"d1:isr"));
270        assert!(glob(b"?1", b"r1"));
271        assert!(glob(b"r*_m", b"r12_m"));
272        assert!(!glob(b"r*_m", b"r12_scale"));
273        assert!(!glob(b"r1", b"r1_m"));
274        assert!(glob(b"a*b*c", b"axxbyyc"));
275        assert!(glob(b"**", b"x"));
276        assert!(glob(b"a**b", b"ab"));
277        // Hostile pattern: the recursive form is exponential here;
278        // the iterative one must answer instantly.
279        let name = [b'a'; 4096];
280        let mut hostile = Vec::new();
281        for _ in 0..64 {
282            hostile.extend_from_slice(b"a*");
283        }
284        hostile.push(b'b');
285        assert!(!glob(&hostile, &name));
286        hostile.pop();
287        hostile.push(b'a');
288        assert!(glob(&hostile, &name));
289    }
290
291    #[test]
292    fn count_steps_matches_the_c() {
293        let dec = SensAc {
294            sweep: AcSweep::Decade,
295            points: 2,
296            fstart: 100.0,
297            fstop: 10e3,
298        };
299        let (n, s) = count_steps(&dec);
300        assert_eq!(n, 5);
301        assert!((s - 10f64.sqrt()).abs() < 1e-15);
302        let lin = SensAc {
303            sweep: AcSweep::Linear,
304            points: 2,
305            fstart: 100.0,
306            fstop: 200.0,
307        };
308        assert_eq!(count_steps(&lin), (2, 50.0));
309        let oct = SensAc {
310            sweep: AcSweep::Octave,
311            points: 1,
312            fstart: 1.0,
313            fstop: 8.0,
314        };
315        // ln(8)·ln(2) + 1.01 → 2, not log2(8) + 1 = 4.
316        assert_eq!(count_steps(&oct).0, 2);
317    }
318}