1use dune_raw::{Data, VarType};
41use num_complex::Complex64;
42
43use crate::ac::AcSweep;
44use crate::circuit::{Circuit, PortInfo, VarKind, saved_node};
45use crate::engine::{AcStepType, Engine, MAX_OUTPUT_VALUES, sweep_budget};
46use crate::error::Error;
47use crate::options::Options;
48use crate::solution::Solution;
49
50#[derive(Clone, Debug)]
57pub struct Sp {
58 pub solution: Solution,
59 pub notes: Vec<String>,
60}
61
62type CMat = Vec<Vec<Complex64>>;
63
64impl Circuit {
65 pub fn sp(
69 &self,
70 opts: &Options,
71 sweep: AcSweep,
72 points: usize,
73 fstart: f64,
74 fstop: f64,
75 donoise: bool,
76 ) -> Result<Sp, Error> {
77 if !(fstart.is_finite() && fstop.is_finite()) || fstart < 0.0 || fstop < 0.0 {
80 return Err(Error::Build(".sp frequencies are inconsistent".into()));
81 }
82 if sweep != AcSweep::Linear && fstart <= 0.0 {
83 return Err(Error::Build(".sp start frequency must be positive".into()));
84 }
85 let built = self.build(opts)?;
86 let notes = built.notes.clone();
87 let ports = built.ports.clone();
88 match ports.len() {
89 0 => {
90 return Err(Error::Build(
91 "no RF port is present, cannot run sp analysis".into(),
92 ));
93 }
94 1 => {
95 return Err(Error::Build(
96 "only one RF port is found, sp needs at least two".into(),
97 ));
98 }
99 _ => {}
100 }
101 let np = ports.len();
102 let mut engine = Engine::new(built);
103
104 let n = points.max(1);
105 let step_type = match sweep {
106 AcSweep::Decade => AcStepType::Decade,
107 AcSweep::Octave => AcStepType::Octave,
108 AcSweep::Linear => AcStepType::Linear,
109 };
110 let freq_delta = match sweep {
113 AcSweep::Decade => f64::exp(f64::ln(10.0) / n as f64),
114 AcSweep::Octave => f64::exp(f64::ln(2.0) / n as f64),
115 AcSweep::Linear => {
116 if n - 1 > 1 {
117 (fstop - fstart) / (n - 1) as f64
118 } else {
119 0.0
120 }
121 }
122 };
123 let extra = np
124 .saturating_mul(np)
125 .saturating_mul(if donoise { 4 } else { 3 })
126 + 4;
127 let row_len = engine.vars.len().saturating_add(extra);
128 sweep_budget(step_type, n, fstart.max(1e-300), fstop, row_len, "sp")?;
129
130 engine.sp_prepare(opts)?;
131
132 let freq_tol = match sweep {
133 AcSweep::Linear => freq_delta * opts.reltol,
134 _ => freq_delta * fstop * opts.reltol,
135 };
136
137 let mut cols = Columns::new(&engine, np, donoise);
138 let mut freq = fstart;
139 while freq <= fstop + freq_tol {
140 if (cols.rows + 1).saturating_mul(row_len) > MAX_OUTPUT_VALUES {
141 return Err(Error::Build(
142 "sp sweep produces too many points; refusing".into(),
143 ));
144 }
145 let (forward, adjoint) = engine.sp_point(freq, &ports, donoise)?;
146 let (a, b) = power_waves(&ports, &forward);
147 let (s, y, z) = network_matrices(&a, &b, &ports);
148 let noise = donoise.then(|| {
149 let gens = engine.sp_noise_generators(freq, opts.temp);
150 let cy = noise_correlation(&gens, &adjoint, &y, &ports);
151 let params = (np == 2).then(|| noise_params(&cy, &y, &ports, opts.temp));
152 (cy, params)
153 });
154 let last = forward.last().map(Vec::as_slice).unwrap_or(&[]);
155 cols.push(freq, last, &s, &y, &z, noise.as_ref());
156
157 match sweep {
158 AcSweep::Decade | AcSweep::Octave => {
159 freq *= freq_delta;
160 if freq_delta == 1.0 {
161 break;
162 }
163 }
164 AcSweep::Linear => {
165 freq += freq_delta;
166 if freq_delta == 0.0 {
167 break;
168 }
169 }
170 }
171 }
172
173 let (variables, columns) = cols.finish();
174 Ok(Sp {
175 solution: Solution {
176 plotname: "SP Analysis".to_string(),
177 variables,
178 data: Data::Complex(columns),
179 scale: Some(0),
180 },
181 notes,
182 })
183 }
184}
185
186fn power_waves(ports: &[PortInfo], forward: &[Vec<Complex64>]) -> (CMat, CMat) {
191 let np = ports.len();
192 let mut a = vec![vec![Complex64::new(0.0, 0.0); np]; np];
193 let mut b = a.clone();
194 for (col, sol) in forward.iter().enumerate() {
195 for (row, p) in ports.iter().enumerate() {
196 let ki = 0.5 / p.z0.sqrt();
197 let i = -sol[p.br];
198 let v = sol[p.pos] - sol[p.neg];
199 a[row][col] = (v + i * p.z0) * ki;
200 b[row][col] = (v - i * p.z0) * ki;
201 }
202 }
203 (a, b)
204}
205
206fn network_matrices(a: &CMat, b: &CMat, ports: &[PortInfo]) -> (CMat, CMat, CMat) {
208 let np = ports.len();
209 let diag = |f: &dyn Fn(&PortInfo) -> f64| -> CMat {
210 let mut m = zeros(np);
211 for (i, p) in ports.iter().enumerate() {
212 m[i][i] = Complex64::new(f(p), 0.0);
213 }
214 m
215 };
216 let zref = diag(&|p| p.z0);
217 let gn = diag(&|p| 2.0 * (0.5 / p.z0.sqrt()));
218 let gninv = diag(&|p| 1.0 / (2.0 * (0.5 / p.z0.sqrt())));
219 let eye = diag(&|_| 1.0);
220
221 let s = mul(b, &inverse(a));
222 let sz0_z0 = add(&mul(&s, &zref), &zref);
223 let e_s = sub(&eye, &s);
224 let z = mul(&gninv, &mul(&inverse(&e_s), &mul(&sz0_z0, &gn)));
225 let y = mul(&gninv, &mul(&inverse(&sz0_z0), &mul(&e_s, &gn)));
226 (s, y, z)
227}
228
229fn noise_correlation(
232 gens: &[crate::matrix::NoiseGen],
233 adjoint: &[Vec<Complex64>],
234 y: &CMat,
235 ports: &[PortInfo],
236) -> CMat {
237 let np = ports.len();
238 let mut cy = zeros(np);
239 let mut v = vec![Complex64::new(0.0, 0.0); np];
240 let mut cur = v.clone();
241 for &(n1, n2, density) in gens {
242 let inoise = density.sqrt();
243 for (s, adj) in adjoint.iter().enumerate() {
244 v[s] = (adj[n1] - adj[n2]) * inoise;
245 }
246 for d in 0..np {
247 let mut c = v[d] * (1.0 / ports[d].z0);
248 for s in 0..np {
249 c += y[d][s] * v[s];
250 }
251 cur[d] = c;
252 }
253 for d in 0..np {
254 for s in 0..np {
255 cy[d][s] += cur[d] * cur[s].conj();
256 }
257 }
258 }
259 cy
260}
261
262#[derive(Clone, Copy, Debug)]
264struct NoiseParams {
265 nf: f64,
267 sopt: Complex64,
269 nfmin: f64,
271 rn: f64,
273}
274
275fn noise_params(cy: &CMat, y: &CMat, ports: &[PortInfo], temp: f64) -> NoiseParams {
276 let knorm = 4.0 * crate::consts::BOLTZ * temp;
277 let mut t: CMat = cy
278 .iter()
279 .map(|r| r.iter().map(|c| c * (1.0 / knorm)).collect())
280 .collect();
281 let y21mod = y[1][0].norm_sqr();
282 let mut rn = t[1][1].re / y21mod;
283 if rn.abs() < 1e-30 {
284 rn = 1e-30;
285 }
286 if t[1][1].re.abs() < 1e-30 && t[1][1].im.abs() < 1e-30 {
287 t[1][1].re = 1e-30;
288 }
289 let ycor = y[0][0] - (t[0][1] / t[1][1]) * y[1][0];
290 let y11_ycor = (y[0][0] - ycor).norm_sqr();
291 let gu = t[0][0].re - rn * y11_ycor;
292 let ysopt = Complex64::new((ycor.re * ycor.re + gu / rn).sqrt(), -ycor.im);
293 let y0 = Complex64::new(1.0 / ports[0].z0, 0.0);
294 let sopt = (y0 - ysopt) / (y0 + ysopt);
295 let fmin = 1.0 + 2.0 * rn * (ycor.re + ysopt.re);
296 let nf = fmin + (rn / y0.re) * (y0 - ysopt).norm_sqr();
297 NoiseParams {
298 nf: 10.0 * nf.log10(),
299 sopt,
300 nfmin: 10.0 * fmin.log10(),
301 rn,
302 }
303}
304
305struct Columns {
307 variables: Vec<(String, VarType)>,
308 saved: Vec<usize>,
310 columns: Vec<Vec<Complex64>>,
311 np: usize,
312 rows: usize,
313}
314
315impl Columns {
316 fn new(engine: &Engine, np: usize, donoise: bool) -> Self {
317 let mut variables = vec![("frequency".to_string(), VarType::Frequency)];
318 let mut saved = Vec::new();
319 let vars = &engine.vars;
320 for i in 1..vars.len() {
321 let name = &vars.names[i];
322 match vars.kinds[i] {
323 VarKind::Node if saved_node(name) => {
324 variables.push((format!("v({name})"), VarType::Voltage));
325 saved.push(i);
326 }
327 VarKind::Branch => {
328 variables.push((format!("i({name})"), VarType::Current));
329 saved.push(i);
330 }
331 _ => {}
332 }
333 }
334 let matrix = |variables: &mut Vec<(String, VarType)>, prefix: &str, t: VarType| {
338 for i in 1..=np {
339 for j in 1..=np {
340 variables.push((format!("{prefix}_{i}_{j}"), t.clone()));
341 }
342 }
343 };
344 matrix(&mut variables, "S", VarType::SParam);
345 matrix(&mut variables, "Y", VarType::Other("admittance".into()));
346 matrix(&mut variables, "Z", VarType::Other("impedance".into()));
347 if donoise {
348 for i in 1..=np {
349 for j in 1..=np {
350 variables.push((format!("i(Cy_{i}_{j})"), VarType::Current));
351 }
352 }
353 if np == 2 {
354 variables.push(("NF".into(), VarType::Other("decibel".into())));
355 variables.push(("SOpt".into(), VarType::NoType));
356 variables.push(("NFmin".into(), VarType::Other("decibel".into())));
357 variables.push(("Rn".into(), VarType::Other("impedance".into())));
358 }
359 }
360 let columns = vec![Vec::new(); variables.len()];
361 Columns {
362 variables,
363 saved,
364 columns,
365 np,
366 rows: 0,
367 }
368 }
369
370 fn push(
371 &mut self,
372 freq: f64,
373 sol: &[Complex64],
374 s: &CMat,
375 y: &CMat,
376 z: &CMat,
377 noise: Option<&(CMat, Option<NoiseParams>)>,
378 ) {
379 let mut k = 0;
380 let mut put = |cols: &mut Vec<Vec<Complex64>>, v: Complex64| {
381 cols[k].push(v);
382 k += 1;
383 };
384 put(&mut self.columns, Complex64::new(freq, 0.0));
385 for &i in &self.saved {
386 put(&mut self.columns, sol[i]);
387 }
388 let mut mats: Vec<&CMat> = vec![s, y, z];
389 if let Some((cy, _)) = noise {
390 mats.push(cy);
391 }
392 for m in mats {
393 for row in m.iter().take(self.np) {
394 for &v in row.iter().take(self.np) {
395 put(&mut self.columns, v);
396 }
397 }
398 }
399 if let Some((_, Some(p))) = noise {
400 put(&mut self.columns, Complex64::new(p.nf, 0.0));
401 put(&mut self.columns, p.sopt);
402 put(&mut self.columns, Complex64::new(p.nfmin, 0.0));
403 put(&mut self.columns, Complex64::new(p.rn, 0.0));
404 }
405 self.rows += 1;
406 }
407
408 fn finish(self) -> (Vec<(String, VarType)>, Vec<Vec<Complex64>>) {
409 (self.variables, self.columns)
410 }
411}
412
413fn zeros(n: usize) -> CMat {
414 vec![vec![Complex64::new(0.0, 0.0); n]; n]
415}
416
417fn mul(a: &CMat, b: &CMat) -> CMat {
418 let n = a.len();
419 let mut c = zeros(n);
420 for i in 0..n {
421 for k in 0..n {
422 let aik = a[i][k];
423 for j in 0..n {
424 c[i][j] += aik * b[k][j];
425 }
426 }
427 }
428 c
429}
430
431fn add(a: &CMat, b: &CMat) -> CMat {
432 a.iter()
433 .zip(b)
434 .map(|(r, s)| r.iter().zip(s).map(|(x, y)| x + y).collect())
435 .collect()
436}
437
438fn sub(a: &CMat, b: &CMat) -> CMat {
439 a.iter()
440 .zip(b)
441 .map(|(r, s)| r.iter().zip(s).map(|(x, y)| x - y).collect())
442 .collect()
443}
444
445fn inverse(a: &CMat) -> CMat {
451 let n = a.len();
452 let mut m = a.clone();
453 let mut inv = zeros(n);
454 for (i, row) in inv.iter_mut().enumerate() {
455 row[i] = Complex64::new(1.0, 0.0);
456 }
457 for col in 0..n {
458 let piv = (col..n)
459 .max_by(|&x, &y| m[x][col].norm().total_cmp(&m[y][col].norm()))
460 .unwrap_or(col);
461 if m[piv][col].norm() == 0.0 {
462 return vec![vec![Complex64::new(f64::NAN, f64::NAN); n]; n];
463 }
464 m.swap(col, piv);
465 inv.swap(col, piv);
466 let d = m[col][col].inv();
467 for j in 0..n {
468 m[col][j] *= d;
469 inv[col][j] *= d;
470 }
471 for r in 0..n {
472 if r != col {
473 let f = m[r][col];
474 if f != Complex64::new(0.0, 0.0) {
475 for j in 0..n {
476 let (mc, ic) = (m[col][j], inv[col][j]);
477 m[r][j] -= f * mc;
478 inv[r][j] -= f * ic;
479 }
480 }
481 }
482 }
483 }
484 inv
485}
486
487#[cfg(test)]
488mod tests {
489 use super::*;
490 use crate::circuit::{Resistor, RfPort, Vsource};
491
492 fn port(name: &str, node: &str, num: u32, z0: f64) -> Vsource {
493 Vsource {
494 name: name.into(),
495 nodes: [node.into(), "0".into()],
496 dc: Some(0.0),
497 wave: None,
498 ac: None,
499 distof1: None,
500 distof2: None,
501 port: Some(RfPort { num, z0 }),
502 }
503 }
504
505 fn close(a: Complex64, b: Complex64, tol: f64) -> bool {
506 (a - b).norm() <= tol * (1.0 + b.norm())
507 }
508
509 #[test]
514 fn matched_t_attenuator() {
515 let (z0, k) = (50.0, 2.0);
516 let r1 = z0 * (k - 1.0) / (k + 1.0);
517 let r2 = 2.0 * z0 * k / (k * k - 1.0);
518 let mut ckt = Circuit::new("t-pad");
519 ckt.add(port("v2", "out", 2, z0)).unwrap();
521 ckt.add(port("v1", "in", 1, z0)).unwrap();
522 for (name, a, b, r) in [
523 ("ra", "in", "mid", r1),
524 ("rb", "mid", "out", r1),
525 ("rc", "mid", "0", r2),
526 ] {
527 ckt.add(Resistor {
528 name: name.into(),
529 nodes: [a.into(), b.into()],
530 value: r,
531 })
532 .unwrap();
533 }
534 let sp = ckt
535 .sp(&Options::default(), AcSweep::Decade, 1, 1e3, 1e6, true)
536 .unwrap();
537 let sol = &sp.solution;
538 assert_eq!(sol.plotname, "SP Analysis");
539 assert_eq!(sol.num_points(), 4);
540 let at = |name: &str| sol.column_complex(name).unwrap()[2];
541 let c = |re: f64| Complex64::new(re, 0.0);
542 for (name, want) in [
543 ("S_1_1", 0.0),
544 ("S_2_2", 0.0),
545 ("S_2_1", 1.0 / k),
546 ("S_1_2", 1.0 / k),
547 ("Z_1_1", r1 + r2),
548 ("Z_2_2", r1 + r2),
549 ("Z_1_2", r2),
550 ("Z_2_1", r2),
551 ] {
552 assert!(close(at(name), c(want), 1e-12), "{name}: {}", at(name));
553 }
554 let det = (r1 + r2) * (r1 + r2) - r2 * r2;
555 assert!(close(at("Y_1_1"), c((r1 + r2) / det), 1e-12));
556 assert!(close(at("Y_2_1"), c(-r2 / det), 1e-12));
557 let four_kt = 4.0 * crate::consts::BOLTZ * Options::default().temp;
559 assert!(close(at("i(Cy_1_1)"), c(four_kt * (r1 + r2) / det), 1e-9));
560 assert!(close(at("i(Cy_2_1)"), c(-four_kt * r2 / det), 1e-9));
561 let nf = at("NF").re;
563 assert!((nf - 20.0 * k.log10()).abs() < 1e-9, "NF = {nf}");
564 assert!(close(at("v(out)"), c(0.5), 1e-12));
567 assert!(close(at("v(v2#res)"), c(1.0), 1e-12));
568 }
569
570 #[test]
571 fn port_checks() {
572 let mut ckt = Circuit::new("ports");
573 ckt.add(port("v1", "a", 1, 50.0)).unwrap();
574 ckt.add(Resistor {
575 name: "r1".into(),
576 nodes: ["a".into(), "0".into()],
577 value: 50.0,
578 })
579 .unwrap();
580 let opts = Options::default();
581 let e = ckt
582 .sp(&opts, AcSweep::Linear, 2, 1.0, 2.0, false)
583 .unwrap_err();
584 assert!(e.to_string().contains("only one RF port"), "{e}");
585 ckt.add(port("v3", "a", 3, 50.0)).unwrap();
586 let e = ckt
587 .sp(&opts, AcSweep::Linear, 2, 1.0, 2.0, false)
588 .unwrap_err();
589 assert!(e.to_string().contains("v3: incorrect port ordering"), "{e}");
590 assert!(ckt.op(&opts).is_err());
592 }
593
594 #[test]
595 fn inverse_roundtrip_and_singular() {
596 let m: CMat = vec![
597 vec![Complex64::new(0.0, 1.0), Complex64::new(2.0, 0.0)],
598 vec![Complex64::new(3.0, -1.0), Complex64::new(0.5, 0.25)],
599 ];
600 let p = mul(&m, &inverse(&m));
601 for (i, row) in p.iter().enumerate() {
602 for (j, v) in row.iter().enumerate() {
603 let want = if i == j { 1.0 } else { 0.0 };
604 assert!(close(*v, Complex64::new(want, 0.0), 1e-14));
605 }
606 }
607 let z = zeros(2);
608 assert!(inverse(&z)[0][0].re.is_nan());
609 }
610}