dune_raw/plot.rs
1use num_complex::Complex64;
2
3use crate::VarType;
4
5/// One variable (a `dvec` in the 3f5 sources) from the `Variables:` table.
6///
7/// The optional attributes mirror the "strange options" the 3f5 reader
8/// accepts on a variable line: `min=`, `max=`, `color=`, `scale=`,
9/// `grid=`, `plot=`, `dims=`.
10#[derive(Clone, Debug, PartialEq)]
11pub struct Variable {
12 pub name: String,
13 pub vtype: VarType,
14 pub min: Option<f64>,
15 pub max: Option<f64>,
16 pub color: Option<String>,
17 /// Name of another vector used as this one's scale.
18 pub scale: Option<String>,
19 pub grid: Option<i64>,
20 pub plottype: Option<i64>,
21 /// Per-vector dimensions, only when different from the plot default.
22 /// Their product is the vector's true length, which may be shorter
23 /// than the plot's `No. Points` (the rest is padding when the plot
24 /// is `padded`).
25 pub dims: Option<Vec<usize>>,
26}
27
28impl Variable {
29 /// A plain variable with no optional attributes.
30 pub fn new(name: impl Into<String>, vtype: VarType) -> Self {
31 Variable {
32 name: name.into(),
33 vtype,
34 min: None,
35 max: None,
36 color: None,
37 scale: None,
38 grid: None,
39 plottype: None,
40 dims: None,
41 }
42 }
43
44 /// The vector's true length in a plot with `npoints` rows
45 /// (`fixdims` in the C: the product of `dims=` when valid).
46 /// Overflowing dims fall back to the plot default, like any other
47 /// invalid `dims=`.
48 pub fn length(&self, npoints: usize) -> usize {
49 match &self.dims {
50 Some(d) => d
51 .iter()
52 .try_fold(1usize, |acc, &x| acc.checked_mul(x))
53 .map_or(npoints, |n| n.min(npoints)),
54 None => npoints,
55 }
56 }
57}
58
59/// Plot data, one column per variable, in `Variables:` order.
60///
61/// A plot is complex as a whole when *any* vector is complex (`Flags:
62/// complex`); real vectors inside a complex plot are written with a zero
63/// imaginary part, exactly as `raw_write` does.
64#[derive(Clone, Debug, PartialEq)]
65pub enum Data {
66 Real(Vec<Vec<f64>>),
67 Complex(Vec<Vec<Complex64>>),
68}
69
70impl Data {
71 pub fn num_vars(&self) -> usize {
72 match self {
73 Data::Real(c) => c.len(),
74 Data::Complex(c) => c.len(),
75 }
76 }
77
78 /// Length of the longest column (the plot's `No. Points`).
79 pub fn num_points(&self) -> usize {
80 match self {
81 Data::Real(c) => c.iter().map(Vec::len).max().unwrap_or(0),
82 Data::Complex(c) => c.iter().map(Vec::len).max().unwrap_or(0),
83 }
84 }
85}
86
87/// One plot: a header plus a block of data (`struct plot` in the C).
88#[derive(Clone, Debug, PartialEq)]
89pub struct Plot {
90 pub title: String,
91 pub date: String,
92 /// The `Plotname:` line, e.g. `Transient Analysis`.
93 pub name: String,
94 /// `Flags: … padded`/`unpadded`: whether short vectors are padded
95 /// with zeros up to `No. Points` in the data section.
96 pub padded: bool,
97 pub commands: Vec<String>,
98 pub options: Vec<String>,
99 /// `Dimensions:` of the plot, when multidimensional (e.g. a nested
100 /// `.dc` sweep). Product must equal `No. Points`.
101 pub dimensions: Option<Vec<usize>>,
102 pub variables: Vec<Variable>,
103 pub data: Data,
104}
105
106impl Plot {
107 pub fn num_points(&self) -> usize {
108 self.data.num_points()
109 }
110
111 pub fn is_complex(&self) -> bool {
112 matches!(self.data, Data::Complex(_))
113 }
114
115 /// The scale vector: the first variable, by 3f5 convention (the
116 /// writer moves the scale to the front before writing).
117 pub fn scale(&self) -> Option<&Variable> {
118 self.variables.first()
119 }
120
121 /// Index of a variable by name, case-insensitive (vector names in
122 /// the 3f5 front end compare with `cieq`).
123 pub fn variable_index(&self, name: &str) -> Option<usize> {
124 self.variables
125 .iter()
126 .position(|v| v.name.eq_ignore_ascii_case(name))
127 }
128
129 /// Real column by variable name, if this is a real plot.
130 pub fn real_values(&self, name: &str) -> Option<&[f64]> {
131 let i = self.variable_index(name)?;
132 match &self.data {
133 Data::Real(c) => Some(&c[i]),
134 Data::Complex(_) => None,
135 }
136 }
137
138 /// Complex column by variable name, if this is a complex plot.
139 pub fn complex_values(&self, name: &str) -> Option<&[Complex64]> {
140 let i = self.variable_index(name)?;
141 match &self.data {
142 Data::Complex(c) => Some(&c[i]),
143 Data::Real(_) => None,
144 }
145 }
146}