use dune_raw::{Data, VarType};
use num_complex::Complex64;
use crate::error::Error;
use crate::solution::Solution;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Part {
Real,
Imag,
Mag,
Phase,
Db,
}
#[derive(Clone, Debug, PartialEq)]
pub enum OutExpr {
Voltage {
node: String,
reference: Option<String>,
part: Option<Part>,
},
Current { source: String, part: Option<Part> },
Name(String),
All,
}
impl Part {
fn prefix(self) -> &'static str {
match self {
Part::Real => "r",
Part::Imag => "i",
Part::Mag => "m",
Part::Phase => "p",
Part::Db => "db",
}
}
fn parse(s: &str) -> Option<Option<Part>> {
Some(match s {
"" => None,
"r" => Some(Part::Real),
"i" => Some(Part::Imag),
"m" => Some(Part::Mag),
"p" => Some(Part::Phase),
"db" => Some(Part::Db),
_ => return None,
})
}
fn apply(self, z: Complex64) -> f64 {
match self {
Part::Real => z.re,
Part::Imag => z.im,
Part::Mag => z.norm(),
Part::Phase => z.im.atan2(z.re),
Part::Db => 20.0 * z.norm().log10(),
}
}
}
impl OutExpr {
pub fn parse(text: &str) -> Result<OutExpr, Error> {
let t = text.trim().to_ascii_lowercase();
if t == "all" {
return Ok(OutExpr::All);
}
let Some(open) = t.find('(') else {
if t.is_empty() {
return Err(Error::Build("empty output expression".into()));
}
return Ok(OutExpr::Name(t));
};
let Some(inner) = t[open..]
.strip_prefix('(')
.and_then(|s| s.strip_suffix(')'))
else {
return Err(Error::Build(format!(
"malformed output expression `{text}`"
)));
};
let head = &t[..open];
let (kind, part_text) = match head.as_bytes().first() {
Some(b'v') => ('v', &head[1..]),
Some(b'i') => ('i', &head[1..]),
_ => return Ok(OutExpr::Name(t.clone())),
};
let Some(part) = Part::parse(part_text) else {
return Ok(OutExpr::Name(t.clone()));
};
let args: Vec<&str> = inner.split(',').map(str::trim).collect();
match (kind, args.as_slice()) {
('v', [node]) if !node.is_empty() => Ok(OutExpr::Voltage {
node: node.to_string(),
reference: None,
part,
}),
('v', [node, reference]) if !node.is_empty() && !reference.is_empty() => {
Ok(OutExpr::Voltage {
node: node.to_string(),
reference: Some(reference.to_string()),
part,
})
}
('i', [source]) if !source.is_empty() => Ok(OutExpr::Current {
source: source.to_string(),
part,
}),
_ => Err(Error::Build(format!(
"malformed output expression `{text}`"
))),
}
}
pub fn from_ir(ov: &dune_netlist::ir::OutVar) -> OutExpr {
use dune_netlist::ir::{AcSuffix, OutVar};
let part = |s: &Option<AcSuffix>| {
s.as_ref().map(|x| match x {
AcSuffix::Real => Part::Real,
AcSuffix::Imag => Part::Imag,
AcSuffix::Mag => Part::Mag,
AcSuffix::Phase => Part::Phase,
AcSuffix::Db => Part::Db,
})
};
match ov {
OutVar::Voltage {
node,
reference,
suffix,
} => OutExpr::Voltage {
node: node.clone(),
reference: reference.clone(),
part: part(suffix),
},
OutVar::Current { vsrc, suffix } => OutExpr::Current {
source: vsrc.clone(),
part: part(suffix),
},
OutVar::Raw(s) if s == "all" => OutExpr::All,
OutVar::Raw(s) => OutExpr::Name(s.clone()),
}
}
pub fn label(&self) -> String {
match self {
OutExpr::Voltage {
node,
reference,
part,
} => {
let p = part.map(Part::prefix).unwrap_or("");
match reference {
Some(r) => format!("v{p}({node},{r})"),
None => format!("v{p}({node})"),
}
}
OutExpr::Current { source, part } => {
let p = part.map(Part::prefix).unwrap_or("");
format!("i{p}({source})")
}
OutExpr::Name(n) => n.clone(),
OutExpr::All => "all".into(),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum ColData {
Real(Vec<f64>),
Complex(Vec<Complex64>),
}
impl ColData {
pub fn len(&self) -> usize {
match self {
ColData::Real(v) => v.len(),
ColData::Complex(v) => v.len(),
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn real(&self) -> Option<&[f64]> {
match self {
ColData::Real(v) => Some(v),
ColData::Complex(_) => None,
}
}
fn as_complex(&self) -> Vec<Complex64> {
match self {
ColData::Real(v) => v.iter().map(|x| Complex64::new(*x, 0.0)).collect(),
ColData::Complex(v) => v.clone(),
}
}
fn sub(&self, other: &ColData) -> ColData {
match (self, other) {
(ColData::Real(a), ColData::Real(b)) => {
ColData::Real(a.iter().zip(b).map(|(x, y)| x - y).collect())
}
_ => {
let a = self.as_complex();
let b = other.as_complex();
ColData::Complex(a.iter().zip(&b).map(|(x, y)| x - y).collect())
}
}
}
fn part(&self, p: Part) -> ColData {
match self {
ColData::Real(v) => {
ColData::Real(v.iter().map(|x| p.apply(Complex64::new(*x, 0.0))).collect())
}
ColData::Complex(v) => ColData::Real(v.iter().map(|z| p.apply(*z)).collect()),
}
}
}
#[derive(Clone, Debug)]
pub struct OutColumn {
pub label: String,
pub unit: VarType,
pub data: ColData,
}
#[derive(Clone, Debug)]
pub struct Selection {
pub scale: Option<OutColumn>,
pub columns: Vec<OutColumn>,
}
impl Solution {
pub fn select(&self, exprs: &[OutExpr]) -> Result<Selection, Error> {
let mut columns = Vec::new();
for e in exprs {
match e {
OutExpr::All => {
for (i, (name, unit)) in self.variables.iter().enumerate() {
columns.push(OutColumn {
label: name.clone(),
unit: unit.clone(),
data: self.col(i),
});
}
}
OutExpr::Name(n) => {
let i = self.index_or_err(n)?;
columns.push(OutColumn {
label: self.variables[i].0.clone(),
unit: self.variables[i].1.clone(),
data: self.col(i),
});
}
OutExpr::Voltage {
node,
reference,
part,
} => {
let a = self.voltage_col(node)?;
let mut data = match reference {
None => a,
Some(r) if r == "0" => a,
Some(r) => a.sub(&self.voltage_col(r)?),
};
if let Some(p) = part {
data = data.part(*p);
}
columns.push(OutColumn {
label: e.label(),
unit: VarType::Voltage,
data,
});
}
OutExpr::Current { source, part } => {
let i = self.index_or_err(&format!("i({source})"))?;
let mut data = self.col(i);
if let Some(p) = part {
data = data.part(*p);
}
columns.push(OutColumn {
label: e.label(),
unit: VarType::Current,
data,
});
}
}
}
let scale = self.scale.map(|i| OutColumn {
label: self.variables[i].0.clone(),
unit: self.variables[i].1.clone(),
data: match self.col(i) {
ColData::Complex(v) => ColData::Real(v.iter().map(|z| z.re).collect()),
real => real,
},
});
Ok(Selection { scale, columns })
}
fn col(&self, i: usize) -> ColData {
match &self.data {
Data::Real(cols) => ColData::Real(cols[i].clone()),
Data::Complex(cols) => ColData::Complex(cols[i].clone()),
}
}
fn index_or_err(&self, name: &str) -> Result<usize, Error> {
self.index(name)
.ok_or_else(|| Error::UnknownVector(name.to_string()))
}
fn voltage_col(&self, node: &str) -> Result<ColData, Error> {
let wrapped = format!("v({node})");
if let Some(i) = self.index(&wrapped) {
return Ok(self.col(i));
}
let i = self.index_or_err(node)?;
Ok(self.col(i))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sol() -> Solution {
Solution {
plotname: "Transient Analysis".into(),
variables: vec![
("time".into(), VarType::Time),
("v(1)".into(), VarType::Voltage),
("v(2)".into(), VarType::Voltage),
("i(vin)".into(), VarType::Current),
],
data: Data::Real(vec![
vec![0.0, 1.0, 2.0],
vec![1.0, 1.0, 1.0],
vec![0.25, 0.5, -1.0],
vec![-1e-3, -2e-3, -3e-3],
]),
scale: Some(0),
}
}
fn sol_ac() -> Solution {
Solution {
plotname: "AC Analysis".into(),
variables: vec![
("frequency".into(), VarType::Frequency),
("v(2)".into(), VarType::Voltage),
],
data: Data::Complex(vec![
vec![Complex64::new(1.0, 0.0), Complex64::new(10.0, 0.0)],
vec![Complex64::new(1.0, 0.0), Complex64::new(0.0, -0.5)],
]),
scale: Some(0),
}
}
#[test]
fn parse_forms() {
assert_eq!(
OutExpr::parse("V(2)").unwrap(),
OutExpr::Voltage {
node: "2".into(),
reference: None,
part: None
}
);
assert_eq!(
OutExpr::parse("vdb(out,ref)").unwrap().label(),
"vdb(out,ref)"
);
assert_eq!(
OutExpr::parse("I(Vin)").unwrap(),
OutExpr::Current {
source: "vin".into(),
part: None
}
);
assert_eq!(OutExpr::parse("all").unwrap(), OutExpr::All);
assert_eq!(
OutExpr::parse("time").unwrap(),
OutExpr::Name("time".into())
);
assert!(OutExpr::parse("v(").is_err());
assert!(OutExpr::parse("").is_err());
assert_eq!(
OutExpr::parse("par(x)").unwrap(),
OutExpr::Name("par(x)".into())
);
}
#[test]
fn select_simple_and_differential() {
let s = sol();
let sel = s
.select(&[
OutExpr::parse("v(2)").unwrap(),
OutExpr::parse("v(1,2)").unwrap(),
OutExpr::parse("i(vin)").unwrap(),
])
.unwrap();
assert_eq!(sel.columns[0].data.real().unwrap(), &[0.25, 0.5, -1.0]);
assert_eq!(sel.columns[1].label, "v(1,2)");
assert_eq!(sel.columns[1].data.real().unwrap(), &[0.75, 0.5, 2.0]);
assert_eq!(sel.columns[2].unit, VarType::Current);
assert_eq!(sel.scale.as_ref().unwrap().label, "time");
}
#[test]
fn parts_on_real_data() {
let s = sol();
let sel = s
.select(&[
OutExpr::parse("vm(2)").unwrap(),
OutExpr::parse("vdb(1)").unwrap(),
OutExpr::parse("vp(2)").unwrap(),
])
.unwrap();
assert_eq!(sel.columns[0].data.real().unwrap(), &[0.25, 0.5, 1.0]);
assert_eq!(sel.columns[1].data.real().unwrap(), &[0.0, 0.0, 0.0]);
assert_eq!(sel.columns[2].data.real().unwrap()[2], std::f64::consts::PI);
}
#[test]
fn parts_on_complex_data() {
let s = sol_ac();
let sel = s
.select(&[
OutExpr::parse("v(2)").unwrap(),
OutExpr::parse("vm(2)").unwrap(),
OutExpr::parse("vdb(2)").unwrap(),
OutExpr::parse("vp(2)").unwrap(),
])
.unwrap();
assert!(matches!(sel.columns[0].data, ColData::Complex(_)));
let m = sel.columns[1].data.real().unwrap();
assert_eq!(m, &[1.0, 0.5]);
let db = sel.columns[2].data.real().unwrap();
assert!((db[1] - 20.0 * 0.5f64.log10()).abs() < 1e-12);
let ph = sel.columns[3].data.real().unwrap();
assert!((ph[1] + std::f64::consts::FRAC_PI_2).abs() < 1e-12);
assert_eq!(
sel.scale.as_ref().unwrap().data.real().unwrap(),
&[1.0, 10.0]
);
}
#[test]
fn all_and_errors() {
let s = sol();
let sel = s.select(&[OutExpr::All]).unwrap();
assert_eq!(sel.columns.len(), 4);
let err = s.select(&[OutExpr::parse("v(99)").unwrap()]).unwrap_err();
assert!(matches!(err, Error::UnknownVector(_)), "{err}");
}
#[test]
fn v_zero_reference_is_identity() {
let s = sol();
let sel = s.select(&[OutExpr::parse("v(2,0)").unwrap()]).unwrap();
assert_eq!(sel.columns[0].data.real().unwrap(), &[0.25, 0.5, -1.0]);
}
}