Skip to main content

dune_core/
tran.rs

1//! `.tran` — transient analysis (`ckt/dctran.c` via the engine).
2
3use dune_raw::VarType;
4
5use crate::circuit::{Circuit, VarKind};
6use crate::engine::{Engine, TranParams};
7use crate::error::Error;
8use crate::options::Options;
9use crate::solution::Solution;
10
11/// Result of a transient analysis: one row per accepted timepoint.
12#[derive(Clone, Debug)]
13pub struct Tran {
14    pub solution: Solution,
15    pub notes: Vec<String>,
16}
17
18impl Circuit {
19    /// Run `.tran TSTEP TSTOP <TSTART <TMAX>> <UIC>`.
20    pub fn tran(
21        &self,
22        opts: &Options,
23        tstep: f64,
24        tstop: f64,
25        tstart: f64,
26        tmax: Option<f64>,
27        uic: bool,
28    ) -> Result<Tran, Error> {
29        // NaN fails these too (a NaN comparison is false).
30        let step_ok = tstep > 0.0 && tstep.is_finite();
31        if !step_ok {
32            return Err(Error::Build(".tran step must be positive".into()));
33        }
34        let times_ok = tstop > 0.0 && tstop.is_finite() && tstop > tstart && tstart >= 0.0;
35        if !times_ok {
36            return Err(Error::Build(".tran times are inconsistent".into()));
37        }
38        if let Some(d) = opts.tmax_divisor
39            && !(d >= 1.0 && d.is_finite())
40        {
41            return Err(Error::Build(format!(
42                "tmax divisor must be a finite number >= 1, got {d}"
43            )));
44        }
45        let (noise_opts, noise_note) = self.noise_tran_options(opts, tstep, tstop, tstart, tmax)?;
46        let opts = noise_opts.as_ref().unwrap_or(opts);
47        let built = self.build(opts)?;
48        let mut notes = built.notes.clone();
49        notes.extend(noise_note);
50        let mut engine = Engine::new(built);
51        if opts.dev_noise {
52            let nt = opts.dev_noise_step.unwrap_or(tstep);
53            engine.set_dev_noise(crate::devnoise::DevNoise::new(
54                nt,
55                opts.seed,
56                opts.temp,
57                tstop,
58                engine.vars.len(),
59            ));
60        }
61        let rows = engine.tran(
62            opts,
63            &TranParams {
64                tstep,
65                tstop,
66                tstart,
67                tmax,
68                uic,
69            },
70        )?;
71        notes.extend(engine.tran_notes());
72        notes.extend(engine.dev_noise_notes());
73
74        let mut variables = vec![("time".to_string(), VarType::Time)];
75        let mut columns = vec![rows.iter().map(|(t, _)| *t).collect::<Vec<f64>>()];
76        for i in 1..engine.vars.len() {
77            let name = &engine.vars.names[i];
78            match engine.vars.kinds[i] {
79                VarKind::Node if crate::circuit::saved_node(name) => {
80                    variables.push((format!("v({name})"), VarType::Voltage));
81                    columns.push(rows.iter().map(|(_, sol)| sol[i]).collect());
82                }
83                VarKind::Branch => {
84                    variables.push((format!("i({name})"), VarType::Current));
85                    columns.push(rows.iter().map(|(_, sol)| sol[i]).collect());
86                }
87                _ => {}
88            }
89        }
90        Ok(Tran {
91            solution: Solution {
92                plotname: "Transient Analysis".to_string(),
93                variables,
94                data: dune_raw::Data::Real(columns),
95                scale: Some(0),
96            },
97            notes,
98        })
99    }
100}