1use dune_raw::{Data, VarType};
27use num_complex::Complex64;
28
29use crate::ac::AcSweep;
30use crate::circuit::Circuit;
31use crate::engine::Engine;
32use crate::error::Error;
33use crate::matrix::{AnKind, InitMode};
34use crate::options::Options;
35use crate::solution::Solution;
36use crate::tf::{OutputVar, resolve_output};
37
38const SENS_DELTA: f64 = 1e-6;
40const SENS_ABS_DELTA: f64 = 1e-6;
41
42#[derive(Clone, Copy, Debug, PartialEq)]
44pub struct SensAc {
45 pub sweep: AcSweep,
46 pub points: usize,
49 pub fstart: f64,
50 pub fstop: f64,
51}
52
53#[derive(Clone, Debug)]
56pub struct Sens {
57 pub solution: Solution,
60 pub notes: Vec<String>,
62}
63
64fn glob(filter: &[u8], name: &[u8]) -> bool {
72 let (mut f, mut n) = (0, 0);
73 let mut star: Option<(usize, usize)> = None; while n < name.len() {
75 if f < filter.len() && (filter[f] == name[n] || filter[f] == b'?') {
76 f += 1;
77 n += 1;
78 } else if f < filter.len() && filter[f] == b'*' {
79 star = Some((f + 1, n));
80 f += 1;
81 } else if let Some((sf, sn)) = star {
82 f = sf;
83 n = sn + 1;
84 star = Some((sf, sn + 1));
85 } else {
86 return false;
87 }
88 }
89 while f < filter.len() && filter[f] == b'*' {
90 f += 1;
91 }
92 f == filter.len()
93}
94
95fn dev_order(name: &str) -> usize {
98 match name.as_bytes().first().map(u8::to_ascii_lowercase) {
99 Some(b'c') => 0,
100 Some(b'f') => 1,
101 Some(b'h') => 2,
102 Some(b'd') => 3,
103 Some(b'l') => 4,
104 Some(b'k') => 5,
105 Some(b'i') => 6,
106 Some(b'r') => 7,
107 Some(b'g') => 8,
108 Some(b'e') => 9,
109 Some(b'v') => 10,
110 _ => 11,
111 }
112}
113
114fn count_steps(ac: &SensAc) -> (usize, f64) {
118 let steps = ac.points.max(1) as f64;
119 let (mut low, mut high) = (ac.fstart, ac.fstop);
120 let (n, s) = match ac.sweep {
121 AcSweep::Linear => (steps, (high - low) / steps),
122 AcSweep::Decade => {
123 if low <= 0.0 {
124 low = 1e-3;
125 }
126 if high <= low {
127 high = 10.0 * low;
128 }
129 (
130 (steps * (high / low).log10() + 1.01).trunc(),
131 10f64.powf(1.0 / steps),
132 )
133 }
134 AcSweep::Octave => {
135 if low <= 0.0 {
136 low = 1e-3;
137 }
138 if high <= low {
139 high = 2.0 * low;
140 }
141 (
142 (steps * (high / low).ln() / std::f64::consts::LOG2_E + 1.01).trunc(),
143 2f64.powf(1.0 / steps),
144 )
145 }
146 };
147 let n = if n.is_finite() && n >= 1.0 {
148 n.min(1e6) as usize
149 } else {
150 1
151 };
152 (n, s)
153}
154
155impl Circuit {
156 pub fn sens(
159 &self,
160 opts: &Options,
161 output: &OutputVar,
162 filters: &[String],
163 ac: Option<SensAc>,
164 ) -> Result<Sens, Error> {
165 let built = self.build(opts)?;
166 let notes = built.notes.clone();
167 let mut engine = Engine::new(built);
168 engine.set_kind(AnKind::DcOp, false);
169 engine.op(opts)?;
170 let out = resolve_output(&engine, output, ".sens")?;
171
172 let params = engine.sens_params(ac.is_some())?;
176 let mut devs: Vec<usize> = (0..params.len()).collect();
177 devs.sort_by_key(|&d| (dev_order(&engine.names[d]), std::cmp::Reverse(d)));
178 let filters: Vec<String> = filters.iter().map(|f| f.to_ascii_lowercase()).collect();
179 let mut walk: Vec<(usize, &'static str, f64, String)> = Vec::new();
180 for &d in &devs {
181 for p in ¶ms[d] {
182 let name = format!("{}{}", engine.names[d], p.suffix);
183 if filters.is_empty() || filters.iter().any(|f| glob(f.as_bytes(), name.as_bytes()))
184 {
185 walk.push((d, p.suffix, p.value, name));
186 }
187 }
188 }
189 if walk.is_empty() {
190 return Err(Error::Build(
191 ".sens: no parameter matches the filters".into(),
192 ));
193 }
194 let delta_of = |value: f64| {
195 if value != 0.0 {
196 value * SENS_DELTA
197 } else {
198 SENS_ABS_DELTA
199 }
200 };
201
202 let mut variables: Vec<(String, VarType)> = Vec::new();
203 let solution = match ac {
204 None => {
205 let mut cols = Vec::with_capacity(walk.len());
206 for (d, suffix, value, name) in &walk {
207 let delta = delta_of(*value);
208 let w = engine.sens_dc_column(opts, *d, suffix, *value, delta)?;
209 cols.push(vec![out.read(&w)]);
210 variables.push((format!("v({name})"), VarType::Voltage));
211 }
212 Solution {
213 plotname: "Sensitivity Analysis".to_string(),
214 variables,
215 data: Data::Real(cols),
216 scale: None,
217 }
218 }
219 Some(ac) => {
220 engine.load_all_public(InitMode::Smsig, opts);
221 let (nfreqs, step) = count_steps(&ac);
222 if nfreqs.saturating_mul(walk.len() + 1) > 50_000_000 {
223 return Err(Error::Build(
224 ".sens produces too many points; refusing".into(),
225 ));
226 }
227 variables.push(("frequency".to_string(), VarType::Frequency));
228 let mut cols: Vec<Vec<Complex64>> = vec![Vec::new(); walk.len() + 1];
229 let mut freq = ac.fstart;
230 for _ in 0..nfreqs {
231 let omega = 2.0 * std::f64::consts::PI * freq;
232 let (er, ei) = engine.sens_ac_solution(omega)?;
233 cols[0].push(Complex64::new(freq, 0.0));
234 for (k, (d, suffix, value, _)) in walk.iter().enumerate() {
235 let delta = delta_of(*value);
236 let (wr, wi) =
237 engine.sens_ac_column(omega, *d, suffix, *value, delta, (&er, &ei))?;
238 cols[k + 1].push(Complex64::new(out.read(&wr), out.read(&wi)));
239 }
240 freq *= step;
244 }
245 for (_, _, _, name) in &walk {
246 variables.push((format!("v({name})"), VarType::Voltage));
247 }
248 Solution {
249 plotname: "Sensitivity Analysis".to_string(),
250 variables,
251 data: Data::Complex(cols),
252 scale: Some(0),
253 }
254 }
255 };
256 Ok(Sens { solution, notes })
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263
264 #[test]
265 fn glob_matches_like_ngspice_scan() {
266 assert!(glob(b"r*", b"r1_m"));
267 assert!(glob(b"*", b"anything"));
268 assert!(glob(b"d1:is", b"d1:is"));
269 assert!(!glob(b"d1:is", b"d1:isr"));
270 assert!(glob(b"?1", b"r1"));
271 assert!(glob(b"r*_m", b"r12_m"));
272 assert!(!glob(b"r*_m", b"r12_scale"));
273 assert!(!glob(b"r1", b"r1_m"));
274 assert!(glob(b"a*b*c", b"axxbyyc"));
275 assert!(glob(b"**", b"x"));
276 assert!(glob(b"a**b", b"ab"));
277 let name = [b'a'; 4096];
280 let mut hostile = Vec::new();
281 for _ in 0..64 {
282 hostile.extend_from_slice(b"a*");
283 }
284 hostile.push(b'b');
285 assert!(!glob(&hostile, &name));
286 hostile.pop();
287 hostile.push(b'a');
288 assert!(glob(&hostile, &name));
289 }
290
291 #[test]
292 fn count_steps_matches_the_c() {
293 let dec = SensAc {
294 sweep: AcSweep::Decade,
295 points: 2,
296 fstart: 100.0,
297 fstop: 10e3,
298 };
299 let (n, s) = count_steps(&dec);
300 assert_eq!(n, 5);
301 assert!((s - 10f64.sqrt()).abs() < 1e-15);
302 let lin = SensAc {
303 sweep: AcSweep::Linear,
304 points: 2,
305 fstart: 100.0,
306 fstop: 200.0,
307 };
308 assert_eq!(count_steps(&lin), (2, 50.0));
309 let oct = SensAc {
310 sweep: AcSweep::Octave,
311 points: 1,
312 fstart: 1.0,
313 fstop: 8.0,
314 };
315 assert_eq!(count_steps(&oct).0, 2);
317 }
318}