Skip to main content

dune_raw/
write.rs

1use std::io::{self, Write};
2
3use crate::plot::{Data, Plot};
4
5/// Data section encoding. Binary is raw little-endian `f64`s — the 3f5
6/// writes native doubles, and every platform Dune targets (x86-64,
7/// aarch64, wasm32) is little-endian.
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum Format {
10    Ascii,
11    Binary,
12}
13
14/// `%.*e` with the given precision, as C's `printf` renders it
15/// (`1.000000000000000e+03`): sign on the exponent, at least two
16/// exponent digits. Rust's `{:e}` writes `1e3`, so this reshapes it.
17/// Public because every text surface that mimics the 3f5 (rawfiles,
18/// the CLI's `.print` columns) needs the same rendering.
19pub fn fmt_e(x: f64, prec: usize) -> String {
20    let s = format!("{x:.prec$e}");
21    match s.rfind('e') {
22        Some(pos) => {
23            let (mant, exp) = s.split_at(pos);
24            let exp: i32 = exp[1..].parse().expect("exponent is an integer");
25            format!("{mant}e{}{:02}", if exp < 0 { '-' } else { '+' }, exp.abs())
26        }
27        None => s, // inf / NaN; never produced by a simulation we write
28    }
29}
30
31/// The default ASCII precision: `DEFPREC` (15 significant decimals past
32/// the point) in `rawfile.c`.
33const DEFPREC: usize = 15;
34
35impl Plot {
36    /// Write this plot exactly as the 3f5 `raw_write` does.
37    ///
38    /// The caller is responsible for the scale being `variables[0]`
39    /// (the C moves it to the front; here it is an invariant of the
40    /// in-memory plot).
41    pub fn write(&self, w: &mut impl Write, format: Format) -> io::Result<()> {
42        let npoints = self.num_points();
43        let complex = self.is_complex();
44
45        writeln!(w, "Title: {}", self.title)?;
46        writeln!(w, "Date: {}", self.date)?;
47        writeln!(w, "Plotname: {}", self.name)?;
48        writeln!(
49            w,
50            "Flags: {}{}",
51            if complex { "complex" } else { "real" },
52            if self.padded { "" } else { " unpadded" }
53        )?;
54        writeln!(w, "No. Variables: {}", self.variables.len())?;
55        writeln!(w, "No. Points: {npoints}")?;
56        if let Some(dims) = &self.dimensions
57            && dims.len() > 1
58        {
59            writeln!(w, "Dimensions: {}", dimstring(dims))?;
60        }
61        for c in &self.commands {
62            writeln!(w, "Command: {c}")?;
63        }
64        for o in &self.options {
65            writeln!(w, "Option: {o}")?;
66        }
67
68        writeln!(w, "Variables:")?;
69        for (i, v) in self.variables.iter().enumerate() {
70            write!(w, "\t{i}\t{}\t{}", v.name, v.vtype.name())?;
71            if let Some(min) = v.min {
72                write!(w, " min={}", fmt_e(min, 6))?;
73            }
74            if let Some(max) = v.max {
75                write!(w, " max={}", fmt_e(max, 6))?;
76            }
77            if let Some(color) = &v.color {
78                write!(w, " color={color}")?;
79            }
80            if let Some(grid) = v.grid {
81                write!(w, " grid={grid}")?;
82            }
83            // Faithful to the C, which prints the *grid* type under
84            // `plot=` (`rawfile.c:139`); kept until a corpus circuit
85            // proves it matters, then it becomes a KNOWN_DIFFS entry.
86            if let Some(_plottype) = v.plottype {
87                write!(w, " plot={}", v.grid.unwrap_or(0))?;
88            }
89            if let Some(dims) = &v.dims {
90                write!(w, " dims={}", dimstring(dims))?;
91            }
92            writeln!(w)?;
93        }
94
95        match format {
96            Format::Binary => {
97                writeln!(w, "Binary:")?;
98                for i in 0..npoints {
99                    self.write_point(w, i, |w, re, im| {
100                        w.write_all(&re.to_le_bytes())?;
101                        if complex {
102                            w.write_all(&im.to_le_bytes())?;
103                        }
104                        Ok(())
105                    })?;
106                }
107            }
108            Format::Ascii => {
109                writeln!(w, "Values:")?;
110                for i in 0..npoints {
111                    write!(w, " {i}")?;
112                    // The C prints a literal `,0.0` imaginary part for a
113                    // real vector inside a complex plot; that distinction
114                    // does not survive a read (every vector of a complex
115                    // plot is complex afterwards), so a full-precision
116                    // zero is written instead, as ngspice itself does.
117                    self.write_point(w, i, |w, re, im| {
118                        if complex {
119                            writeln!(w, "\t{},{}", fmt_e(re, DEFPREC), fmt_e(im, DEFPREC))
120                        } else {
121                            writeln!(w, "\t{}", fmt_e(re, DEFPREC))
122                        }
123                    })?;
124                    writeln!(w)?;
125                }
126            }
127        }
128        Ok(())
129    }
130
131    /// Emit point `i` of every vector through `emit(re, im)`, honoring
132    /// per-vector lengths and zero padding, like the inner loop of
133    /// `raw_write`.
134    fn write_point(
135        &self,
136        w: &mut impl Write,
137        i: usize,
138        mut emit: impl FnMut(&mut dyn Write, f64, f64) -> io::Result<()>,
139    ) -> io::Result<()> {
140        match &self.data {
141            Data::Real(cols) => {
142                for col in cols {
143                    if i < col.len() {
144                        emit(w, col[i], 0.0)?;
145                    } else if self.padded {
146                        emit(w, 0.0, 0.0)?;
147                    }
148                }
149            }
150            Data::Complex(cols) => {
151                for col in cols {
152                    if i < col.len() {
153                        emit(w, col[i].re, col[i].im)?;
154                    } else if self.padded {
155                        emit(w, 0.0, 0.0)?;
156                    }
157                }
158            }
159        }
160        Ok(())
161    }
162}
163
164/// `d1,d2,…` (the C `dimstring`).
165fn dimstring(dims: &[usize]) -> String {
166    dims.iter()
167        .map(|d| d.to_string())
168        .collect::<Vec<_>>()
169        .join(",")
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use crate::{VarType, Variable};
176
177    #[test]
178    fn c_style_exponent() {
179        assert_eq!(fmt_e(1000.0, 15), "1.000000000000000e+03");
180        assert_eq!(fmt_e(-2.5e-7, 15), "-2.500000000000000e-07");
181        assert_eq!(fmt_e(0.0, 15), "0.000000000000000e+00");
182        assert_eq!(fmt_e(1.0, 6), "1.000000e+00");
183        assert_eq!(fmt_e(3.5e-112, 15), "3.500000000000000e-112");
184    }
185
186    #[test]
187    fn ascii_matches_3f5_shape() {
188        let plot = Plot {
189            title: "t".into(),
190            date: "d".into(),
191            name: "Operating Point".into(),
192            padded: true,
193            commands: vec![],
194            options: vec![],
195            dimensions: None,
196            variables: vec![
197                Variable::new("v(1)", VarType::Voltage),
198                Variable::new("i(v1)", VarType::Current),
199            ],
200            data: Data::Real(vec![vec![1.0], vec![-0.001]]),
201        };
202        let mut buf = Vec::new();
203        plot.write(&mut buf, Format::Ascii).unwrap();
204        let expected = "Title: t\n\
205             Date: d\n\
206             Plotname: Operating Point\n\
207             Flags: real\n\
208             No. Variables: 2\n\
209             No. Points: 1\n\
210             Variables:\n\
211             \t0\tv(1)\tvoltage\n\
212             \t1\ti(v1)\tcurrent\n\
213             Values:\n \
214             0\t1.000000000000000e+00\n\
215             \t-1.000000000000000e-03\n\n";
216        assert_eq!(String::from_utf8(buf).unwrap(), expected);
217    }
218}