Skip to main content

dune_core/
tf.rs

1//! `.tf` — DC small-signal transfer function (`ckt/tfanal.c`).
2//!
3//! After the operating point, the Jacobian of the last Newton
4//! iteration is still factored. One solve with a unit excitation on
5//! the input source — `+1` on the branch row of a voltage source, a
6//! unit current from `N+` to `N-` through a current source — yields
7//! the transfer function at the output and the input resistance
8//! (`-1/i(branch)`, or `v(N-) - v(N+)`); a second solve with a unit
9//! current injected into the output node pair (or `+1` on the output
10//! source's branch row) yields the output resistance. The three
11//! values form one real point of the `Transfer Function` plot.
12
13use dune_raw::{Data, VarType};
14
15use crate::circuit::{Circuit, Spec, VarKind};
16use crate::engine::Engine;
17use crate::error::Error;
18use crate::matrix::{AnKind, Var};
19use crate::options::Options;
20use crate::solution::Solution;
21
22/// The output variable of `.tf` and `.sens`: `V(N1<,N2>)` or
23/// `I(VXXXXXXX)` (the current through a branch-carrying element).
24#[derive(Clone, Debug, PartialEq, Eq)]
25pub enum OutputVar {
26    /// `V(NODE)` or `V(NODE,REFERENCE)`.
27    Voltage {
28        node: String,
29        reference: Option<String>,
30    },
31    /// `I(VSRC)`: the branch current of a voltage source (or any
32    /// element with a branch row: E, H, L).
33    Current { vsrc: String },
34}
35
36/// Result of a `.tf` analysis. The plot holds the same three values
37/// under the ngspice vector names (`Transfer_function`,
38/// `<src>#Input_impedance`, `output_impedance_at_V(…)` or
39/// `<src>#Output_impedance`).
40#[derive(Clone, Debug)]
41pub struct Tf {
42    pub solution: Solution,
43    /// Output over input (V/V, A/V, V/A or A/A).
44    pub transfer_function: f64,
45    /// Small-signal resistance seen by the input source (ohms).
46    pub input_impedance: f64,
47    /// Small-signal resistance at the output (ohms).
48    pub output_impedance: f64,
49    pub notes: Vec<String>,
50}
51
52/// A resolved output: node pair or branch row.
53#[derive(Clone, Copy, Debug)]
54pub(crate) enum OutRef {
55    Volt(Var, Var),
56    Branch(Var),
57}
58
59impl OutRef {
60    pub fn read(&self, x: &[f64]) -> f64 {
61        match *self {
62            OutRef::Volt(p, n) => x[p] - x[n],
63            OutRef::Branch(b) => x[b],
64        }
65    }
66}
67
68/// Resolve an [`OutputVar`] against the engine's unknowns; `what`
69/// names the analysis in errors.
70pub(crate) fn resolve_output(
71    engine: &Engine,
72    output: &OutputVar,
73    what: &str,
74) -> Result<OutRef, Error> {
75    let node = |name: &str| -> Result<Var, Error> {
76        let name = name.to_ascii_lowercase();
77        if name == "0" {
78            return Ok(0);
79        }
80        engine
81            .vars
82            .names
83            .iter()
84            .zip(&engine.vars.kinds)
85            .position(|(n, k)| *n == name && !matches!(k, VarKind::Branch))
86            .ok_or_else(|| Error::Build(format!("{what} names unknown node `{name}`")))
87    };
88    match output {
89        OutputVar::Voltage { node: p, reference } => Ok(OutRef::Volt(
90            node(p)?,
91            match reference {
92                Some(r) => node(r)?,
93                None => 0,
94            },
95        )),
96        OutputVar::Current { vsrc } => Ok(OutRef::Branch(branch_of(engine, vsrc).ok_or_else(
97            || Error::Build(format!("{what} output `{vsrc}` has no branch current")),
98        )?)),
99    }
100}
101
102/// `CKTfndBranch`: the branch row of a voltage-defined element.
103fn branch_of(engine: &Engine, name: &str) -> Option<Var> {
104    let name = name.to_ascii_lowercase();
105    engine
106        .vars
107        .names
108        .iter()
109        .zip(&engine.vars.kinds)
110        .position(|(n, k)| *n == name && matches!(k, VarKind::Branch))
111}
112
113/// The input source of `.tf`.
114enum Input {
115    V(Var),
116    I(Var, Var),
117}
118
119impl Circuit {
120    /// Run `.tf OUTVAR INSRC`.
121    pub fn tf(&self, opts: &Options, output: &OutputVar, insrc: &str) -> Result<Tf, Error> {
122        let insrc = insrc.to_ascii_lowercase();
123        // `TFanal`: the input must be an independent V or I source.
124        let mut input_nodes = None;
125        for spec in &self.specs {
126            match spec {
127                Spec::Vsource(v) if v.name == insrc => input_nodes = Some(None),
128                Spec::Isource(i) if i.name == insrc => {
129                    input_nodes = Some(Some(i.nodes.clone()));
130                }
131                _ => {}
132            }
133        }
134        let Some(input_nodes) = input_nodes else {
135            let known = self.specs.iter().any(|s| s.name() == insrc);
136            return Err(Error::Build(if known {
137                format!("transfer function source `{insrc}` not of proper type")
138            } else {
139                format!("transfer function source `{insrc}` not in circuit")
140            }));
141        };
142
143        let built = self.build(opts)?;
144        let notes = built.notes.clone();
145        let mut engine = Engine::new(built);
146        engine.set_kind(AnKind::DcOp, false);
147        engine.op(opts)?;
148
149        let out = resolve_output(&engine, output, ".tf")?;
150        let var_of = |name: &str| -> Var {
151            if name == "0" {
152                return 0;
153            }
154            engine
155                .vars
156                .names
157                .iter()
158                .zip(&engine.vars.kinds)
159                .position(|(n, k)| n == name && !matches!(k, VarKind::Branch))
160                .unwrap_or(0)
161        };
162        let input = match &input_nodes {
163            Some([n1, n2]) => Input::I(var_of(n1), var_of(n2)),
164            None => Input::V(branch_of(&engine, &insrc).ok_or_else(|| {
165                Error::Build(format!("transfer function source `{insrc}` has no branch"))
166            })?),
167        };
168
169        let n = engine.vars.len();
170        let mut rhs = vec![0.0; n];
171        match input {
172            Input::I(n1, n2) => {
173                rhs[n1] -= 1.0;
174                rhs[n2] += 1.0;
175            }
176            Input::V(br) => rhs[br] += 1.0,
177        }
178        engine.solve_factored(&mut rhs)?;
179        let transfer_function = out.read(&rhs);
180        let input_impedance = match input {
181            Input::I(n1, n2) => rhs[n2] - rhs[n1],
182            Input::V(br) => {
183                if rhs[br].abs() < 1e-20 {
184                    1e20
185                } else {
186                    -1.0 / rhs[br]
187                }
188            }
189        };
190
191        let same_source =
192            matches!(output, OutputVar::Current { vsrc } if vsrc.eq_ignore_ascii_case(&insrc));
193        let output_impedance = if same_source {
194            // "no need to compute output resistance when it is the
195            // same as the input"
196            input_impedance
197        } else {
198            rhs.fill(0.0);
199            match out {
200                OutRef::Volt(p, q) => {
201                    rhs[p] -= 1.0;
202                    rhs[q] += 1.0;
203                }
204                OutRef::Branch(b) => rhs[b] += 1.0,
205            }
206            engine.solve_factored(&mut rhs)?;
207            match out {
208                OutRef::Volt(p, q) => rhs[q] - rhs[p],
209                OutRef::Branch(b) => 1.0 / f64::max(1e-20, rhs[b]),
210            }
211        };
212
213        let out_name = match output {
214            OutputVar::Voltage { node, reference } => match reference {
215                Some(r) => format!("v(output_impedance_at_V({node},{r}))"),
216                None => format!("v(output_impedance_at_V({node}))"),
217            },
218            OutputVar::Current { vsrc } => format!("v({vsrc}#Output_impedance)"),
219        };
220        let variables = vec![
221            ("v(Transfer_function)".to_string(), VarType::Voltage),
222            (format!("v({insrc}#Input_impedance)"), VarType::Voltage),
223            (out_name, VarType::Voltage),
224        ];
225        Ok(Tf {
226            solution: Solution {
227                plotname: "Transfer Function".to_string(),
228                variables,
229                data: Data::Real(vec![
230                    vec![transfer_function],
231                    vec![input_impedance],
232                    vec![output_impedance],
233                ]),
234                scale: None,
235            },
236            transfer_function,
237            input_impedance,
238            output_impedance,
239            notes,
240        })
241    }
242}