Skip to main content

dune_core/
noise.rs

1//! `.noise` — small-signal noise analysis (`ckt/noisean.c`,
2//! `ckt/cktnoise.c`, `ni/niniter.c::NInzIter`, `dev/*noise.c` in
3//! their ngspice form). Per frequency: the ordinary AC system gives
4//! the input-output gain; the **adjoint** (transposed) system with a
5//! unit current at the output gives, in one solve, the transfer
6//! from every internal noise generator to the output. Each device
7//! evaluates its spectral densities against that adjoint solution;
8//! the totals integrate over the sweep with `Nintegrate`'s
9//! log-log-segment rule.
10
11use dune_raw::{Data, VarType};
12
13use crate::ac::AcSweep;
14use crate::circuit::{Circuit, Spec};
15use crate::engine::{AcStepType, Engine};
16use crate::error::Error;
17use crate::matrix::{AnKind, InitMode};
18use crate::options::Options;
19use crate::solution::Solution;
20use crate::tf::OutputVar;
21
22const N_MINGAIN: f64 = 1e-20;
23const N_MINLOG: f64 = 1e-38;
24const N_INTFTHRESH: f64 = 1e-10;
25const N_INTUSELOG: f64 = 1e-10;
26
27/// Result of a `.noise` analysis: the density sweep and, when the
28/// band has more than one frequency, the integrated totals.
29#[derive(Clone, Debug)]
30pub struct Noise {
31    pub density: Solution,
32    pub integrated: Option<Solution>,
33    pub notes: Vec<String>,
34}
35
36/// `Nintegrate`: area under a log-log line segment.
37fn nintegrate(
38    noiz_dens: f64,
39    ln_ndens: f64,
40    ln_nlst_dens: f64,
41    del_ln_freq: f64,
42    del_freq: f64,
43    ln_freq: f64,
44    ln_last_freq: f64,
45) -> f64 {
46    let limexp = |x: f64| {
47        if x > 700.0 {
48            f64::exp(700.0) * (1.0 + x - 700.0)
49        } else {
50            f64::exp(x)
51        }
52    };
53    let mut exponent = (ln_ndens - ln_nlst_dens) / del_ln_freq;
54    if exponent.abs() < N_INTFTHRESH {
55        noiz_dens * del_freq
56    } else {
57        let a = limexp(ln_ndens - exponent * ln_freq);
58        exponent += 1.0;
59        if exponent.abs() < N_INTUSELOG {
60            a * (ln_freq - ln_last_freq)
61        } else {
62            a * ((f64::exp(exponent * ln_freq) - f64::exp(exponent * ln_last_freq)) / exponent)
63        }
64    }
65}
66
67impl Circuit {
68    /// Run `.noise V(OUT<,REF>) SRC DEC|OCT|LIN PTS FSTART FSTOP
69    /// <PTS_PER_SUMMARY>`.
70    ///
71    /// `output` must be an [`OutputVar::Voltage`]: the 3f5's `.noise`
72    /// takes only `V(OUTPUT<,REF>)` (spice.txt), so a current output
73    /// is an [`Error::Build`].
74    #[allow(clippy::too_many_arguments, clippy::too_many_lines)]
75    pub fn noise(
76        &self,
77        opts: &Options,
78        output: &OutputVar,
79        src: &str,
80        sweep: AcSweep,
81        points: usize,
82        fstart: f64,
83        fstop: f64,
84        pts_per_summary: Option<usize>,
85    ) -> Result<Noise, Error> {
86        let (output, output_ref) = match output {
87            OutputVar::Voltage { node, reference } => (node.as_str(), reference.as_deref()),
88            OutputVar::Current { vsrc } => {
89                return Err(Error::Build(format!(
90                    ".noise output must be a voltage V(OUT<,REF>), not i({vsrc})"
91                )));
92            }
93        };
94        let ok = fstart > 0.0 && fstart.is_finite() && fstop.is_finite() && fstop >= fstart;
95        if !ok {
96            return Err(Error::Build(".noise frequencies are inconsistent".into()));
97        }
98        let src_lc = src.to_ascii_lowercase();
99
100        // The input source must exist and carry an AC value
101        // (`noisean.c`'s `noacinput`); it also fixes the unit of the
102        // input-referred noise.
103        let mut input_is_vsrc = None;
104        for spec in &self.specs {
105            match spec {
106                Spec::Vsource(v) if v.name == src_lc => {
107                    if v.ac.is_none() {
108                        return Err(Error::Build(".noise input source has no AC value".into()));
109                    }
110                    input_is_vsrc = Some(true);
111                }
112                Spec::Isource(i) if i.name == src_lc => {
113                    if i.ac.is_none() {
114                        return Err(Error::Build(".noise input source has no AC value".into()));
115                    }
116                    input_is_vsrc = Some(false);
117                }
118                _ => {}
119            }
120        }
121        let Some(input_is_vsrc) = input_is_vsrc else {
122            return Err(Error::Build(format!(
123                ".noise input source `{src}` not in circuit"
124            )));
125        };
126
127        let built = self.build(opts)?;
128        let notes = built.notes.clone();
129        let mut engine = Engine::new(built);
130
131        let var_of = |name: &str| -> Result<usize, Error> {
132            let name = name.to_ascii_lowercase();
133            if name == "0" {
134                return Ok(0);
135            }
136            engine
137                .vars
138                .names
139                .iter()
140                .position(|n| *n == name)
141                .ok_or_else(|| Error::Build(format!(".noise names unknown node `{name}`")))
142        };
143        let pos_out = var_of(output)?;
144        let neg_out = match output_ref {
145            Some(r) => var_of(r)?,
146            None => 0,
147        };
148
149        let step_type = match sweep {
150            AcSweep::Decade => AcStepType::Decade,
151            AcSweep::Octave => AcStepType::Octave,
152            AcSweep::Linear => AcStepType::Linear,
153        };
154        let n = points.max(1);
155        let freq_delta = match step_type {
156            AcStepType::Decade => f64::exp(f64::ln(10.0) / n as f64),
157            AcStepType::Octave => f64::exp(f64::ln(2.0) / n as f64),
158            // ngspice divides by NnumSteps−1 (the 3f5 used +1).
159            AcStepType::Linear => (fstop - fstart) / (n.max(2) - 1) as f64,
160        };
161        let freq_tol = match step_type {
162            AcStepType::Linear => freq_delta * opts.reltol,
163            _ => freq_delta * fstop * opts.reltol,
164        };
165        let stps_sm = pts_per_summary.unwrap_or(0);
166
167        // Operating point, then the small-signal pass.
168        crate::engine::sweep_budget(step_type, n, fstart, fstop, engine.vars.len(), "noise")?;
169        engine.set_kind(AnKind::DcOp, false);
170        engine.op(opts)?;
171        engine.load_all_public(InitMode::Smsig, opts);
172
173        // Noisy devices, in device order.
174        let noisy: Vec<(usize, &'static [&'static str])> = engine.noise_devices();
175
176        // Vector names for the density plot.
177        let mut dens_vars: Vec<(String, VarType)> = vec![("frequency".into(), VarType::Frequency)];
178        if stps_sm != 0 {
179            for &(d, suffixes) in &noisy {
180                for sfx in suffixes {
181                    dens_vars.push((
182                        format!("onoise{}{}{}", engine.noise_sep(d), engine.names[d], sfx),
183                        VarType::Other("voltage-density".into()),
184                    ));
185                }
186            }
187        }
188        dens_vars.push((
189            "onoise_spectrum".into(),
190            VarType::Other("voltage-density".into()),
191        ));
192        dens_vars.push((
193            "inoise_spectrum".into(),
194            VarType::Other(if input_is_vsrc {
195                "voltage-density".into()
196            } else {
197                "current-density".into()
198            }),
199        ));
200        let mut dens_cols: Vec<Vec<f64>> = vec![Vec::new(); dens_vars.len()];
201
202        // Integration state per noisy device per generator.
203        let mut ln_lst_dens: Vec<Vec<f64>> =
204            noisy.iter().map(|(_, s)| vec![0.0; s.len()]).collect();
205        let mut out_noiz_dev: Vec<Vec<f64>> =
206            noisy.iter().map(|(_, s)| vec![0.0; s.len()]).collect();
207        let mut in_noiz_dev: Vec<Vec<f64>> =
208            noisy.iter().map(|(_, s)| vec![0.0; s.len()]).collect();
209        let mut out_noiz = 0.0f64;
210        let mut in_noise = 0.0f64;
211
212        let mut freq = fstart;
213        let mut lst_freq = freq;
214        let mut step = 0usize;
215        while freq <= fstop + freq_tol {
216            let (gain2, adj_re, adj_im) = engine.noise_point(freq, pos_out, neg_out)?;
217            let gain_sq_inv = 1.0 / f64::max(gain2, N_MINGAIN);
218            let ln_gain_inv = gain_sq_inv.ln();
219
220            let del_freq = freq - lst_freq;
221            let ln_freq = f64::ln(f64::max(freq, N_MINLOG));
222            let ln_last_freq = f64::ln(f64::max(lst_freq, N_MINLOG));
223            let del_ln_freq = ln_freq - ln_last_freq;
224            let prt_summary = stps_sm != 0 && step.is_multiple_of(stps_sm);
225
226            let mut out_ndens = 0.0f64;
227            let mut row: Vec<f64> = Vec::new();
228            for (k, &(d, suffixes)) in noisy.iter().enumerate() {
229                let dens = engine.noise_dens_of(d, freq, &adj_re, &adj_im, opts.temp);
230                let nsrc = suffixes.len();
231                debug_assert_eq!(dens.len(), nsrc);
232                let ln_dens: Vec<f64> = dens
233                    .iter()
234                    .map(|v| f64::ln(f64::max(*v, N_MINLOG)))
235                    .collect();
236                // (the C takes log(total) without the floor)
237                out_ndens += dens[nsrc - 1];
238
239                if del_freq == 0.0 {
240                    ln_lst_dens[k].copy_from_slice(&ln_dens);
241                    if freq == fstart {
242                        out_noiz_dev[k].fill(0.0);
243                        in_noiz_dev[k].fill(0.0);
244                    }
245                } else {
246                    // Each generator integrates on its own; the total
247                    // entry only accumulates them. A one-generator
248                    // device (the switch, `swnoise.c`) has its total
249                    // as the only entry and integrates that.
250                    let single = nsrc == 1;
251                    for i in 0..nsrc {
252                        if single || i != nsrc - 1 {
253                            let t_out = nintegrate(
254                                dens[i],
255                                ln_dens[i],
256                                ln_lst_dens[k][i],
257                                del_ln_freq,
258                                del_freq,
259                                ln_freq,
260                                ln_last_freq,
261                            );
262                            let t_in = nintegrate(
263                                dens[i] * gain_sq_inv,
264                                ln_dens[i] + ln_gain_inv,
265                                ln_lst_dens[k][i] + ln_gain_inv,
266                                del_ln_freq,
267                                del_freq,
268                                ln_freq,
269                                ln_last_freq,
270                            );
271                            ln_lst_dens[k][i] = ln_dens[i];
272                            out_noiz += t_out;
273                            in_noise += t_in;
274                            if single {
275                                out_noiz_dev[k][0] += t_out;
276                                in_noiz_dev[k][0] += t_in;
277                            } else if stps_sm != 0 {
278                                out_noiz_dev[k][i] += t_out;
279                                out_noiz_dev[k][nsrc - 1] += t_out;
280                                in_noiz_dev[k][i] += t_in;
281                                in_noiz_dev[k][nsrc - 1] += t_in;
282                            }
283                        }
284                    }
285                }
286                if prt_summary {
287                    row.extend_from_slice(&dens);
288                }
289            }
290
291            if stps_sm == 0 || prt_summary {
292                // ngspice's default output reports √(V²/Hz) — the
293                // `.options sqrnoise` form keeps the squares; we
294                // emit the default, which is what the goldens hold.
295                let mut full = vec![freq];
296                full.extend(row.iter().map(|v| v.sqrt()));
297                full.push(out_ndens.sqrt());
298                full.push((out_ndens * gain_sq_inv).sqrt());
299                debug_assert_eq!(full.len(), dens_cols.len());
300                for (col, v) in dens_cols.iter_mut().zip(full) {
301                    col.push(v);
302                }
303            }
304
305            lst_freq = freq;
306            match step_type {
307                AcStepType::Decade | AcStepType::Octave => freq *= freq_delta,
308                AcStepType::Linear => {
309                    freq += freq_delta;
310                    if freq_delta == 0.0 {
311                        break;
312                    }
313                }
314            }
315            step += 1;
316        }
317
318        let density = Solution {
319            plotname: "Noise Spectral Density Curves".to_string(),
320            variables: dens_vars,
321            data: Data::Real(dens_cols),
322            scale: Some(0),
323        };
324
325        // Integrated totals (only over a real band).
326        let integrated = if fstart != fstop {
327            let (owrap, otype) = ("v", VarType::Voltage);
328            let (iwrap, itype) = if input_is_vsrc {
329                ("v", VarType::Voltage)
330            } else {
331                ("i", VarType::Current)
332            };
333            let mut vars: Vec<(String, VarType)> = Vec::new();
334            let mut cols: Vec<Vec<f64>> = Vec::new();
335            if stps_sm != 0 {
336                for (k, &(d, suffixes)) in noisy.iter().enumerate() {
337                    for (i, sfx) in suffixes.iter().enumerate() {
338                        vars.push((
339                            format!(
340                                "{owrap}(onoise_total{}{}{})",
341                                engine.noise_sep(d),
342                                engine.names[d],
343                                sfx
344                            ),
345                            otype.clone(),
346                        ));
347                        cols.push(vec![out_noiz_dev[k][i].sqrt()]);
348                        vars.push((
349                            format!(
350                                "{iwrap}(inoise_total{}{}{})",
351                                engine.noise_sep(d),
352                                engine.names[d],
353                                sfx
354                            ),
355                            itype.clone(),
356                        ));
357                        cols.push(vec![in_noiz_dev[k][i].sqrt()]);
358                    }
359                }
360            }
361            vars.push((format!("{owrap}(onoise_total)"), otype.clone()));
362            cols.push(vec![out_noiz.sqrt()]);
363            vars.push((format!("{iwrap}(inoise_total)"), itype.clone()));
364            cols.push(vec![in_noise.sqrt()]);
365            Some(Solution {
366                plotname: "Integrated Noise".to_string(),
367                variables: vars,
368                data: Data::Real(cols),
369                scale: None,
370            })
371        } else {
372            None
373        };
374
375        Ok(Noise {
376            density,
377            integrated,
378            notes,
379        })
380    }
381}