use dune_raw::{Data, VarType};
use num_complex::Complex64;
use crate::ac::AcSweep;
use crate::circuit::Circuit;
use crate::engine::Engine;
use crate::error::Error;
use crate::matrix::{AnKind, InitMode};
use crate::options::Options;
use crate::solution::Solution;
use crate::tf::{OutputVar, resolve_output};
const SENS_DELTA: f64 = 1e-6;
const SENS_ABS_DELTA: f64 = 1e-6;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SensAc {
pub sweep: AcSweep,
pub points: usize,
pub fstart: f64,
pub fstop: f64,
}
#[derive(Clone, Debug)]
pub struct Sens {
pub solution: Solution,
pub notes: Vec<String>,
}
fn glob(filter: &[u8], name: &[u8]) -> bool {
let (mut f, mut n) = (0, 0);
let mut star: Option<(usize, usize)> = None; while n < name.len() {
if f < filter.len() && (filter[f] == name[n] || filter[f] == b'?') {
f += 1;
n += 1;
} else if f < filter.len() && filter[f] == b'*' {
star = Some((f + 1, n));
f += 1;
} else if let Some((sf, sn)) = star {
f = sf;
n = sn + 1;
star = Some((sf, sn + 1));
} else {
return false;
}
}
while f < filter.len() && filter[f] == b'*' {
f += 1;
}
f == filter.len()
}
fn dev_order(name: &str) -> usize {
match name.as_bytes().first().map(u8::to_ascii_lowercase) {
Some(b'c') => 0,
Some(b'f') => 1,
Some(b'h') => 2,
Some(b'd') => 3,
Some(b'l') => 4,
Some(b'k') => 5,
Some(b'i') => 6,
Some(b'r') => 7,
Some(b'g') => 8,
Some(b'e') => 9,
Some(b'v') => 10,
_ => 11,
}
}
fn count_steps(ac: &SensAc) -> (usize, f64) {
let steps = ac.points.max(1) as f64;
let (mut low, mut high) = (ac.fstart, ac.fstop);
let (n, s) = match ac.sweep {
AcSweep::Linear => (steps, (high - low) / steps),
AcSweep::Decade => {
if low <= 0.0 {
low = 1e-3;
}
if high <= low {
high = 10.0 * low;
}
(
(steps * (high / low).log10() + 1.01).trunc(),
10f64.powf(1.0 / steps),
)
}
AcSweep::Octave => {
if low <= 0.0 {
low = 1e-3;
}
if high <= low {
high = 2.0 * low;
}
(
(steps * (high / low).ln() / std::f64::consts::LOG2_E + 1.01).trunc(),
2f64.powf(1.0 / steps),
)
}
};
let n = if n.is_finite() && n >= 1.0 {
n.min(1e6) as usize
} else {
1
};
(n, s)
}
impl Circuit {
pub fn sens(
&self,
opts: &Options,
output: &OutputVar,
filters: &[String],
ac: Option<SensAc>,
) -> Result<Sens, Error> {
let built = self.build(opts)?;
let notes = built.notes.clone();
let mut engine = Engine::new(built);
engine.set_kind(AnKind::DcOp, false);
engine.op(opts)?;
let out = resolve_output(&engine, output, ".sens")?;
let params = engine.sens_params(ac.is_some())?;
let mut devs: Vec<usize> = (0..params.len()).collect();
devs.sort_by_key(|&d| (dev_order(&engine.names[d]), std::cmp::Reverse(d)));
let filters: Vec<String> = filters.iter().map(|f| f.to_ascii_lowercase()).collect();
let mut walk: Vec<(usize, &'static str, f64, String)> = Vec::new();
for &d in &devs {
for p in ¶ms[d] {
let name = format!("{}{}", engine.names[d], p.suffix);
if filters.is_empty() || filters.iter().any(|f| glob(f.as_bytes(), name.as_bytes()))
{
walk.push((d, p.suffix, p.value, name));
}
}
}
if walk.is_empty() {
return Err(Error::Build(
".sens: no parameter matches the filters".into(),
));
}
let delta_of = |value: f64| {
if value != 0.0 {
value * SENS_DELTA
} else {
SENS_ABS_DELTA
}
};
let mut variables: Vec<(String, VarType)> = Vec::new();
let solution = match ac {
None => {
let mut cols = Vec::with_capacity(walk.len());
for (d, suffix, value, name) in &walk {
let delta = delta_of(*value);
let w = engine.sens_dc_column(opts, *d, suffix, *value, delta)?;
cols.push(vec![out.read(&w)]);
variables.push((format!("v({name})"), VarType::Voltage));
}
Solution {
plotname: "Sensitivity Analysis".to_string(),
variables,
data: Data::Real(cols),
scale: None,
}
}
Some(ac) => {
engine.load_all_public(InitMode::Smsig, opts);
let (nfreqs, step) = count_steps(&ac);
if nfreqs.saturating_mul(walk.len() + 1) > 50_000_000 {
return Err(Error::Build(
".sens produces too many points; refusing".into(),
));
}
variables.push(("frequency".to_string(), VarType::Frequency));
let mut cols: Vec<Vec<Complex64>> = vec![Vec::new(); walk.len() + 1];
let mut freq = ac.fstart;
for _ in 0..nfreqs {
let omega = 2.0 * std::f64::consts::PI * freq;
let (er, ei) = engine.sens_ac_solution(omega)?;
cols[0].push(Complex64::new(freq, 0.0));
for (k, (d, suffix, value, _)) in walk.iter().enumerate() {
let delta = delta_of(*value);
let (wr, wi) =
engine.sens_ac_column(omega, *d, suffix, *value, delta, (&er, &ei))?;
cols[k + 1].push(Complex64::new(out.read(&wr), out.read(&wi)));
}
freq *= step;
}
for (_, _, _, name) in &walk {
variables.push((format!("v({name})"), VarType::Voltage));
}
Solution {
plotname: "Sensitivity Analysis".to_string(),
variables,
data: Data::Complex(cols),
scale: Some(0),
}
}
};
Ok(Sens { solution, notes })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn glob_matches_like_ngspice_scan() {
assert!(glob(b"r*", b"r1_m"));
assert!(glob(b"*", b"anything"));
assert!(glob(b"d1:is", b"d1:is"));
assert!(!glob(b"d1:is", b"d1:isr"));
assert!(glob(b"?1", b"r1"));
assert!(glob(b"r*_m", b"r12_m"));
assert!(!glob(b"r*_m", b"r12_scale"));
assert!(!glob(b"r1", b"r1_m"));
assert!(glob(b"a*b*c", b"axxbyyc"));
assert!(glob(b"**", b"x"));
assert!(glob(b"a**b", b"ab"));
let name = [b'a'; 4096];
let mut hostile = Vec::new();
for _ in 0..64 {
hostile.extend_from_slice(b"a*");
}
hostile.push(b'b');
assert!(!glob(&hostile, &name));
hostile.pop();
hostile.push(b'a');
assert!(glob(&hostile, &name));
}
#[test]
fn count_steps_matches_the_c() {
let dec = SensAc {
sweep: AcSweep::Decade,
points: 2,
fstart: 100.0,
fstop: 10e3,
};
let (n, s) = count_steps(&dec);
assert_eq!(n, 5);
assert!((s - 10f64.sqrt()).abs() < 1e-15);
let lin = SensAc {
sweep: AcSweep::Linear,
points: 2,
fstart: 100.0,
fstop: 200.0,
};
assert_eq!(count_steps(&lin), (2, 50.0));
let oct = SensAc {
sweep: AcSweep::Octave,
points: 1,
fstart: 1.0,
fstop: 8.0,
};
assert_eq!(count_steps(&oct).0, 2);
}
}