Skip to main content

dune_core/
dc.rs

1//! `.dc` — DC transfer curves (`ckt/dctrcurv.c`): one or two
2//! variables swept, first innermost, operating point at every step.
3//! The 3f5 sweeps V/I sources only; ngspice-47 adds resistors and
4//! the circuit temperature (`.dc temp …`), which Dune follows
5//! (corpus/KNOWN_DIFFS.md).
6
7use dune_raw::VarType;
8
9use crate::circuit::{Circuit, Spec, VarKind};
10use crate::consts::CTOK;
11use crate::engine::{Engine, SweepRef, SweepTarget};
12use crate::error::Error;
13use crate::options::Options;
14use crate::solution::Solution;
15
16/// One sweep level: `.dc SRC START STOP STEP`.
17#[derive(Clone, Debug)]
18pub struct DcSweep {
19    pub src: String,
20    pub start: f64,
21    pub stop: f64,
22    pub step: f64,
23}
24
25/// Result of a DC transfer-curve analysis.
26#[derive(Clone, Debug)]
27pub struct Dc {
28    pub solution: Solution,
29    pub notes: Vec<String>,
30}
31
32impl Circuit {
33    /// Run a DC transfer-curve analysis over one or two sweep
34    /// variables: V/I sources, resistors or `temp` (°C).
35    pub fn dc(&self, opts: &Options, sweeps: &[DcSweep]) -> Result<Dc, Error> {
36        if sweeps.is_empty() || sweeps.len() > 2 {
37            return Err(Error::Build(".dc takes one or two sweep sources".into()));
38        }
39        let built = self.build(opts)?;
40        let notes = built.notes.clone();
41        let mut engine = Engine::new(built);
42
43        // ngspice-47 `dctrcurv.c` looks the name up as a resistor,
44        // then a V and an I source, then the word `temp`; the 3f5
45        // only knows the sources (corpus/KNOWN_DIFFS.md).
46        enum Scale {
47            V,
48            I,
49            Res,
50            Temp,
51        }
52        let mut refs = Vec::new();
53        let mut scale = Scale::V;
54        for (level, s) in sweeps.iter().enumerate() {
55            let name = s.src.to_ascii_lowercase();
56            let resistor = self
57                .specs
58                .iter()
59                .position(|sp| matches!(sp, Spec::Resistor(r) if r.name == name));
60            let (target, kind) = if let Some(d) = resistor {
61                (SweepTarget::Device(d), Scale::Res)
62            } else if let Some(&(d, is_vsrc)) = engine.sources.get(&name) {
63                (
64                    SweepTarget::Device(d),
65                    if is_vsrc { Scale::V } else { Scale::I },
66                )
67            } else if name == "temp" {
68                (SweepTarget::Temp, Scale::Temp)
69            } else {
70                // The C's "DCtrCurv: source %s not in circuit".
71                return Err(Error::Build(format!(
72                    "dc sweep source `{name}` not in circuit"
73                )));
74            };
75            // The 3f5 would loop forever on a zero step; ngspice
76            // rejects it, and so do we (corpus/KNOWN_DIFFS.md).
77            if s.step == 0.0 || !s.step.is_finite() || !s.start.is_finite() {
78                return Err(Error::Build(format!(
79                    "dc sweep of `{name}` has a zero or non-finite increment"
80                )));
81            }
82            if level == 0 {
83                scale = kind;
84            }
85            refs.push(SweepRef {
86                target,
87                start: s.start,
88                stop: s.stop,
89                step: s.step,
90            });
91        }
92
93        // A temperature level rebuilds the devices at each point
94        // (`CKTtemp`): instance `TEMP=` values stay, everything that
95        // follows the circuit temperature moves.
96        let mut retemp = |celsius: f64| -> Result<Vec<Box<dyn crate::devices::Device>>, Error> {
97            let mut o = opts.clone();
98            o.temp = celsius + CTOK;
99            Ok(self.build(&o)?.devices)
100        };
101        let rows = engine.dc(opts, &refs, &mut retemp)?;
102
103        // ngspice calls the sweep vector `v-sweep` / `i-sweep` (saved
104        // like any node/branch vector) and `res-sweep` /
105        // `temp-sweep`, typed by their own names.
106        let (scale_name, scale_type) = match scale {
107            Scale::V => ("v(v-sweep)".to_string(), VarType::Voltage),
108            Scale::I => ("i(i-sweep)".to_string(), VarType::Current),
109            Scale::Res => (
110                "res-sweep".to_string(),
111                VarType::Other("res-sweep".to_string()),
112            ),
113            Scale::Temp => (
114                "temp-sweep".to_string(),
115                VarType::Other("temp-sweep".to_string()),
116            ),
117        };
118        let mut variables = vec![(scale_name, scale_type)];
119        let mut columns = vec![rows.iter().map(|(scale, _)| *scale).collect::<Vec<f64>>()];
120        for i in 1..engine.vars.len() {
121            let name = &engine.vars.names[i];
122            match engine.vars.kinds[i] {
123                VarKind::Node if crate::circuit::saved_node(name) => {
124                    variables.push((format!("v({name})"), VarType::Voltage));
125                    columns.push(rows.iter().map(|(_, sol)| sol[i]).collect());
126                }
127                VarKind::Branch => {
128                    variables.push((format!("i({name})"), VarType::Current));
129                    columns.push(rows.iter().map(|(_, sol)| sol[i]).collect());
130                }
131                _ => {}
132            }
133        }
134        Ok(Dc {
135            solution: Solution {
136                plotname: "DC transfer characteristic".to_string(),
137                variables,
138                data: dune_raw::Data::Real(columns),
139                scale: Some(0),
140            },
141            notes,
142        })
143    }
144}