Skip to main content

dune_core/
ac.rs

1//! `.ac` — small-signal frequency sweep (`ckt/acan.c` via the
2//! engine): linearize at the operating point, solve the complex MNA
3//! system per frequency.
4
5use dune_raw::{Data, VarType};
6use num_complex::Complex64;
7
8use crate::circuit::{Circuit, VarKind, Vars};
9use crate::engine::{AcStepType, Engine};
10use crate::error::Error;
11use crate::options::Options;
12use crate::solution::Solution;
13
14/// `.ac` sweep spacing.
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum AcSweep {
17    /// `DEC n`: n points per decade.
18    Decade,
19    /// `OCT n`: n points per octave.
20    Octave,
21    /// `LIN n`: n points from fstart to fstop.
22    Linear,
23}
24
25/// Result of an AC analysis.
26#[derive(Clone, Debug)]
27pub struct Ac {
28    pub solution: Solution,
29    pub notes: Vec<String>,
30}
31
32impl Circuit {
33    /// Run `.ac DEC|OCT|LIN N FSTART FSTOP`.
34    pub fn ac(
35        &self,
36        opts: &Options,
37        sweep: AcSweep,
38        points: usize,
39        fstart: f64,
40        fstop: f64,
41    ) -> Result<Ac, Error> {
42        let ok = fstart > 0.0 && fstart.is_finite() && fstop.is_finite() && fstop >= fstart;
43        if !ok {
44            return Err(Error::Build(".ac frequencies are inconsistent".into()));
45        }
46        let built = self.build(opts)?;
47        let notes = built.notes.clone();
48        let mut engine = Engine::new(built);
49        let step_type = match sweep {
50            AcSweep::Decade => AcStepType::Decade,
51            AcSweep::Octave => AcStepType::Octave,
52            AcSweep::Linear => AcStepType::Linear,
53        };
54        let rows = engine.ac(opts, step_type, points, fstart, fstop)?;
55
56        let (variables, columns) = ac_columns(&engine.vars, &rows);
57        Ok(Ac {
58            solution: Solution {
59                plotname: "AC Analysis".to_string(),
60                variables,
61                data: Data::Complex(columns),
62                scale: Some(0),
63            },
64            notes,
65        })
66    }
67}
68
69/// `CKTacDump` columns for complex sweep rows: the `frequency` scale,
70/// then `v(node)` for every external node and `i(branch)` for every
71/// branch, in variable order (internal `#` nodes are not saved).
72pub(crate) fn ac_columns(
73    vars: &Vars,
74    rows: &[(f64, Vec<Complex64>)],
75) -> (Vec<(String, VarType)>, Vec<Vec<Complex64>>) {
76    let mut variables = vec![("frequency".to_string(), VarType::Frequency)];
77    let mut columns: Vec<Vec<Complex64>> =
78        vec![rows.iter().map(|(f, _)| Complex64::new(*f, 0.0)).collect()];
79    for i in 1..vars.len() {
80        let name = &vars.names[i];
81        match vars.kinds[i] {
82            VarKind::Node if crate::circuit::saved_node(name) => {
83                variables.push((format!("v({name})"), VarType::Voltage));
84                columns.push(rows.iter().map(|(_, sol)| sol[i]).collect());
85            }
86            VarKind::Branch => {
87                variables.push((format!("i({name})"), VarType::Current));
88                columns.push(rows.iter().map(|(_, sol)| sol[i]).collect());
89            }
90            _ => {}
91        }
92    }
93    (variables, columns)
94}