1use dune_raw::{Data, VarType};
48use num_complex::Complex64;
49
50use crate::circuit::{Circuit, Spec, VarKind, saved_node};
51use crate::engine::{Engine, MAX_OUTPUT_VALUES, PeriodRun, StepRec, TranParams};
52use crate::error::Error;
53use crate::matrix::PeriodicPast;
54use crate::options::Options;
55use crate::solution::Solution;
56use crate::srcfn::Wave;
57
58pub const PSS_TIME_PLOT: &str = "Time Domain Periodic Steady State Analysis";
60pub const PSS_FREQ_PLOT: &str = "Frequency Domain Periodic Steady State Analysis";
62
63#[derive(Clone, Debug, PartialEq)]
65pub struct PssParams {
66 pub freq: Option<f64>,
72 pub tstab: f64,
76 pub osc_node: Option<String>,
81 pub points: usize,
84 pub harmonics: usize,
87 pub max_iter: usize,
89 pub steady_coeff: f64,
94 pub uic: bool,
96}
97
98impl Default for PssParams {
99 fn default() -> Self {
100 PssParams {
101 freq: None,
102 tstab: 0.0,
103 osc_node: None,
104 points: 128,
105 harmonics: 10,
106 max_iter: 50,
107 steady_coeff: 1.0,
108 uic: false,
109 }
110 }
111}
112
113#[derive(Clone, Debug)]
115pub struct Pss {
116 pub time: Solution,
120 pub spectrum: Solution,
126 pub period: f64,
129 pub t0: f64,
131 pub residuals: Vec<f64>,
134 pub periods: Vec<f64>,
138 pub notes: Vec<String>,
139}
140
141const FD_EPS: f64 = 1e-2;
144const REPLAY_TIGHTEN: f64 = 1e-3;
149const MAX_BACKTRACK: usize = 4;
151
152impl Circuit {
153 pub fn pss(&self, opts: &Options, p: &PssParams) -> Result<Pss, Error> {
160 let opts = &Options {
163 method: Some(opts.pss_method()),
164 ..opts.clone()
165 };
166 let bad = |m: &str| Err(Error::Build(format!(".pss {m}")));
167 if !(p.tstab.is_finite() && p.tstab >= 0.0) {
168 return bad("stabilization time must be finite and not negative");
169 }
170 if p.points < 4 || p.points > 10_000_000 {
171 return bad("needs 4 to 10 000 000 points per period");
172 }
173 if p.harmonics < 1 || p.harmonics > p.points / 2 {
174 return bad("harmonics must be between 1 and points/2");
175 }
176 if p.max_iter < 1 {
177 return bad("needs at least one shooting iteration");
178 }
179 if !(p.steady_coeff.is_finite() && p.steady_coeff > 0.0) {
180 return bad("steady coefficient must be positive");
181 }
182 if opts.dev_noise {
183 return Err(Error::Build(
188 ".pss: `.options devnoise` is on, and transient device noise is a random \
189 stimulus, which has no periodic steady state; turn it off for the shooting"
190 .into(),
191 ));
192 }
193 for spec in &self.specs {
194 let noisy = match spec {
195 Spec::Vsource(v) => &v.wave,
196 Spec::Isource(i) => &i.wave,
197 _ => &None,
198 };
199 if matches!(noisy, Some(Wave::TrNoise(_) | Wave::TrRandom(_))) {
200 return Err(Error::Unsupported {
203 name: spec.name().to_string(),
204 what: "a transient-noise source (TRNOISE/TRRANDOM) in .pss — a random \
205 stimulus has no periodic steady state"
206 .into(),
207 });
208 }
209 if let Spec::Ltra(l) = spec
210 && !opts.ltra_fit
211 {
212 let lp = crate::ltra::LtraParams::new(&l.name, &l.model, opts.reltol, opts.abstol)
215 .map_err(Error::Build)?;
216 if matches!(
217 lp.case,
218 crate::ltra::LtraCase::Rlc | crate::ltra::LtraCase::Rc
219 ) {
220 return Err(Error::Build(format!(
221 ".pss: the lossy (RLC/RC) line `{}` needs `.options ltrafit`: in the \
222 periodic regime its history is the orbit repeated over the whole past, \
223 which the exact convolution (a walk over a history that starts at an \
224 operating point) cannot hold, and the recursive model sums in closed \
225 form",
226 l.name
227 )));
228 }
229 }
230 }
231 let mut notes = Vec::new();
232 let sources = self.source_frequencies()?;
233 if sources.is_empty() {
234 return self.pss_autonomous(opts, p);
237 }
238 let freq = match p.freq {
239 Some(f) => {
240 if !(f.is_finite() && f > 0.0) {
241 return bad("frequency must be positive");
242 }
243 for (name, fs) in &sources {
244 let k = fs / f;
245 if (k - k.round()).abs() > 1e-6 * k.max(1.0) || k.round() < 1.0 {
246 notes.push(format!(
247 "pss: source `{name}` ({fs:e} Hz) is not a harmonic of the \
248 fundamental {f:e} Hz; the circuit is not {:e}-s periodic",
249 1.0 / f
250 ));
251 }
252 }
253 f
254 }
255 None => harvest(&sources)?,
256 };
257 let period = 1.0 / freq;
258 let tstep = period / p.points as f64;
259 let max_step = f64::min(tstep, period / 50.0);
260 let n_stab = (p.tstab / period - 1e-9).ceil().max(1.0);
261 if n_stab > 1e9 {
262 return bad("stabilization spans too many periods");
263 }
264 let t0 = n_stab * period;
265 let run = PeriodRun {
266 t0,
267 period,
268 tstep,
269 max_step,
270 };
271
272 let built = self.build(opts)?;
274 notes.extend(built.notes.iter().cloned());
275 let mut engine = Engine::new(built);
276 let nvars = engine.vars.len() as f64;
281 let pts = p.points as f64;
282 if (pts + 1.0) * nvars > MAX_OUTPUT_VALUES as f64
283 || n_stab * pts > 1e8
284 || p.harmonics as f64 * pts * nvars > 1e9
285 {
286 return bad("asks for too many points or periods; refusing");
287 }
288 if let Some(node) = &p.osc_node {
289 let lc = node.to_ascii_lowercase();
290 if !engine.vars.names.contains(&lc) {
291 return Err(Error::Build(format!(".pss: no node `{node}` for oscnode")));
292 }
293 notes.push(format!(
294 "pss: oscnode `{lc}` is for autonomous circuits; the driven shooting does not use it"
295 ));
296 }
297 let ports = self.line_ports(&engine.vars.names);
298 let lines = !ports.is_empty();
299 let stab = engine.tran(
302 opts,
303 &TranParams {
304 tstep,
305 tstop: t0,
306 tstart: if lines { t0 - period } else { t0 },
307 tmax: Some(max_step),
308 uic: p.uic,
309 },
310 )?;
311 let names = engine.vars.names.clone();
312 let kinds = engine.vars.kinds.clone();
313 let mut x = engine.sol.clone();
314 drop(engine);
315
316 let mut shooter = Shooter {
317 circuit: self,
318 opts,
319 tight: tightened(opts),
320 run,
321 kinds: &kinds,
322 adaptive: 0,
323 replays: 0,
324 krylov: Vec::new(),
325 past: if lines {
326 Some(
327 Past::from_rows(&stab, None, t0 - period, period, max_step)
328 .ok_or_else(|| Error::Build(".pss: no stabilization period".into()))?,
329 )
330 } else {
331 None
332 },
333 ports,
334 history: Vec::new(),
335 refresh: Vec::new(),
336 };
337 let mut residuals = Vec::new();
338 let converged = if lines {
339 shooter.lines_excited(&mut x, p, &mut residuals)?
340 } else {
341 let mut last: Option<(Vec<f64>, Vec<f64>, f64, f64)> = None;
343 let min_frac = 0.5f64.powi(MAX_BACKTRACK as i32);
344 loop {
345 let (mut period_run, rnorm) = match shooter.adaptive(&x) {
349 Ok(r) => {
350 let n = r.rnorm;
351 (Some(r), n)
352 }
353 Err(e) => match &last {
354 Some((_, _, _, frac)) if *frac > min_frac => (None, f64::INFINITY),
355 _ => return Err(e),
356 },
357 };
358 residuals.push(rnorm);
359 if rnorm <= p.steady_coeff
360 && let Some(r) = period_run.take()
361 {
362 break r;
363 }
364 if residuals.len() >= p.max_iter {
365 return Err(Error::PssConvergence {
366 iterations: residuals.len(),
367 residuals,
368 period: None,
369 });
370 }
371 if let Some((base, step, base_norm, frac)) = &mut last
373 && rnorm > *base_norm
374 && *frac > min_frac
375 {
376 *frac *= 0.5;
377 for i in 0..x.len() {
378 x[i] = base[i] + *frac * step[i];
379 }
380 continue;
381 }
382 let Some(period_run) = period_run else {
383 unreachable!("a failed run is always backtracked")
384 };
385 let step = shooter.newton_step(&x, &period_run)?;
386 let base = x.clone();
387 for i in 1..x.len() {
388 x[i] += step[i];
389 }
390 last = Some((base, step, rnorm, 1.0));
391 }
392 };
393
394 notes.push(format!(
395 "pss: period {period:e} s (fundamental {freq:e} Hz{}); shooting over \
396 [{t0:e}, {:e}] s after {n_stab} stabilization period(s)",
397 if p.freq.is_some() {
398 String::new()
399 } else {
400 ", from the sources".to_string()
401 },
402 t0 + period
403 ));
404 notes.push(shooter.summary(&residuals));
405 notes.extend(shooter.lines_note());
406
407 let (time, spectrum) = outputs(&converged, &names, &kinds, p, t0, period);
408 let periods = vec![period; residuals.len()];
409 Ok(Pss {
410 time,
411 spectrum,
412 period,
413 t0,
414 residuals,
415 periods,
416 notes,
417 })
418 }
419
420 fn line_ports(&self, names: &[String]) -> Vec<usize> {
425 let find = |n: &str| names.iter().position(|v| v.eq_ignore_ascii_case(n));
426 let mut out = Vec::new();
427 for spec in &self.specs {
428 let (name, nodes) = match spec {
429 Spec::Ltra(l) => (&l.name, &l.nodes),
430 Spec::Tra(t) => (&t.name, &t.nodes),
431 _ => continue,
432 };
433 let branches = [format!("{name}#i1"), format!("{name}#i2")];
434 for n in nodes
435 .iter()
436 .map(String::as_str)
437 .chain(branches.iter().map(String::as_str))
438 {
439 if let Some(i) = find(n)
440 && i != 0
441 && !out.contains(&i)
442 {
443 out.push(i);
444 }
445 }
446 }
447 out
448 }
449
450 fn source_frequencies(&self) -> Result<Vec<(String, f64)>, Error> {
456 let mut out = Vec::new();
457 for spec in &self.specs {
458 let (name, wave) = match spec {
459 Spec::Vsource(v) => (&v.name, &v.wave),
460 Spec::Isource(i) => (&i.name, &i.wave),
461 _ => continue,
462 };
463 let Some(wave) = wave else { continue };
464 let given = |c: &[f64], i: usize| c.get(i).copied().filter(|&v| v != 0.0);
465 let no_period = |what: &str| {
466 Err(Error::Build(format!(
467 ".pss: source `{name}`: {what} has no fixed period"
468 )))
469 };
470 match wave {
471 Wave::Sin(c) => {
472 if given(c, 1).is_none() {
473 continue;
474 }
475 if given(c, 4).is_some() {
476 return no_period("a damped SIN");
477 }
478 match given(c, 2) {
479 Some(f) => out.push((name.clone(), f.abs())),
480 None => return no_period("a SIN without a frequency"),
481 }
482 }
483 Wave::Sffm(c) => {
484 if given(c, 1).is_none() {
485 continue;
486 }
487 match given(c, 2) {
488 Some(f) => out.push((name.clone(), f.abs())),
489 None => return no_period("an SFFM without a carrier frequency"),
490 }
491 if given(c, 3).is_some() {
492 match given(c, 4) {
493 Some(f) => out.push((name.clone(), f.abs())),
494 None => return no_period("an SFFM without a signal frequency"),
495 }
496 }
497 }
498 Wave::Pulse(c) => {
499 let v1 = c.first().copied().unwrap_or(0.0);
500 let v2 = c.get(1).copied().unwrap_or(0.0);
501 if v1 != v2
502 && let Some(per) = given(c, 6)
503 {
504 out.push((name.clone(), 1.0 / per.abs()));
505 }
506 }
507 Wave::Exp(_) | Wave::Pwl(_) => {}
508 Wave::TrNoise(_) | Wave::TrRandom(_) => {}
510 }
511 }
512 Ok(out)
513 }
514}
515
516impl Circuit {
517 fn pss_autonomous(&self, opts: &Options, p: &PssParams) -> Result<Pss, Error> {
526 let bad = |m: &str| Err(Error::Build(format!(".pss {m}")));
527 let guess = match p.freq {
528 Some(f) if !(f.is_finite() && f > 0.0) => return bad("frequency must be positive"),
529 Some(f) => Some(1.0 / f),
530 None => None,
531 };
532 let Some(node) = &p.osc_node else {
533 return bad(
534 "of an autonomous circuit (no periodic source) needs the oscillation node \
535 OSCNODE",
536 );
537 };
538 if p.tstab <= 0.0 {
539 return bad(
540 "of an autonomous circuit needs a stabilization time TSTAB: the oscillation \
541 must start and be measured before the shooting",
542 );
543 }
544 let built = self.build(opts)?;
545 let mut notes: Vec<String> = built.notes.clone();
546 let mut engine = Engine::new(built);
547 let lc = node.to_ascii_lowercase();
548 let Some(osc) = engine.vars.names.iter().position(|n| *n == lc) else {
549 return Err(Error::Build(format!(".pss: no node `{node}` for oscnode")));
550 };
551 if osc == 0 || engine.vars.kinds[osc] != VarKind::Node {
552 return Err(Error::Build(format!(
553 ".pss: oscnode `{node}` must be a circuit node"
554 )));
555 }
556 let (tstep, max_step) = match guess {
559 Some(t) => (t / p.points as f64, f64::min(t / p.points as f64, t / 50.0)),
560 None => (p.tstab / STAB_POINTS, p.tstab / STAB_POINTS),
561 };
562 let nvars = engine.vars.len() as f64;
563 let pts = p.points as f64;
564 if (pts + 1.0) * nvars > MAX_OUTPUT_VALUES as f64
565 || p.tstab / tstep > 1e8
566 || p.harmonics as f64 * pts * nvars > 1e9
567 {
568 return bad("asks for too many points or periods; refusing");
569 }
570 let rows = engine.tran(
571 opts,
572 &TranParams {
573 tstep,
574 tstop: p.tstab,
575 tstart: p.tstab * (1.0 - STAB_WINDOW),
576 tmax: Some(max_step),
577 uic: p.uic,
578 },
579 )?;
580 let names = engine.vars.names.clone();
581 let kinds = engine.vars.kinds.clone();
582 let ports = self.line_ports(&names);
583 drop(engine);
584 let no_past = || {
588 Error::Build(
589 ".pss: the stabilization window holds less than one period before the \
590 shooting start; lengthen TSTAB"
591 .into(),
592 )
593 };
594 let past_before = |rows: &[(f64, Vec<f64>)], at: f64, t: f64, max_step: f64| {
595 if ports.is_empty() {
596 Ok(None)
597 } else {
598 Past::from_rows(rows, None, at - t, t, max_step)
599 .map(Some)
600 .ok_or_else(no_past)
601 }
602 };
603 let mut start = osc_start(&rows, osc, &lc, guess, opts)?;
604 let mut start_rows = rows;
605 if guess.is_none() {
606 let (t_end, x_end) = start_rows.last().expect("the stabilization has rows");
610 let t = start.period;
611 let tstep = t / p.points as f64;
612 let refine = PeriodRun {
613 t0: *t_end,
614 period: REFINE_PERIODS * t,
615 tstep,
616 max_step: f64::min(tstep, t / 50.0),
617 };
618 let past = past_before(&start_rows, *t_end, t, max_step)?;
619 let mut engine = Engine::new(self.build(opts)?);
620 engine.set_periodic_past(past.as_ref().map(|p| p.at(t)));
621 let fine = engine.tran_period(opts, &refine, x_end, &mut Vec::new())?;
622 notes.push(start.note);
623 start = osc_start(&fine, osc, &lc, None, opts)?;
624 start_rows = fine;
625 start.note = format!(
626 "{} (refined over {REFINE_PERIODS} periods continued from TSTAB at T/POINTS)",
627 start.note
628 );
629 }
630 notes.push(start.note.clone());
631 let t0 = start.t0;
632 let mut x = start.x0;
633 let mut period = start.period;
634
635 let run_for = |t: f64| PeriodRun {
636 t0,
637 period: t,
638 tstep: t / p.points as f64,
639 max_step: f64::min(t / p.points as f64, t / 50.0),
640 };
641 let first_past = past_before(&start_rows, t0, period, run_for(period).max_step)?;
642 drop(start_rows);
643 let mut shooter = Shooter {
644 circuit: self,
645 opts,
646 tight: tightened(opts),
647 run: run_for(period),
648 kinds: &kinds,
649 adaptive: 0,
650 replays: 0,
651 krylov: Vec::new(),
652 past: first_past,
653 ports: ports.clone(),
654 history: Vec::new(),
655 refresh: Vec::new(),
656 };
657 let mut residuals = Vec::new();
658 let mut periods = Vec::new();
659 let mut last: Option<OscStep> = None;
660 let min_frac = 0.5f64.powi(MAX_BACKTRACK as i32);
661 let fail = |residuals: Vec<f64>, period: f64| Error::PssConvergence {
662 iterations: residuals.len(),
663 residuals,
664 period: Some(period),
665 };
666 let converged = if shooter.past.is_some() {
667 let (r, dt) = shooter.lines_autonomous(
668 &mut x,
669 &mut period,
670 osc,
671 p,
672 &run_for,
673 &mut residuals,
674 &mut periods,
675 )?;
676 last = Some(OscStep {
677 x: Vec::new(),
678 t: period,
679 dx: Vec::new(),
680 dt,
681 base_norm: 0.0,
682 frac: 1.0,
683 });
684 r
685 } else {
686 loop {
687 shooter.run = run_for(period);
688 let (mut period_run, rnorm) = match shooter.adaptive(&x) {
689 Ok(r) => {
690 let n = r.rnorm;
691 (Some(r), n)
692 }
693 Err(e) => match &last {
694 Some(l) if l.frac > min_frac => (None, f64::INFINITY),
695 Some(_) => {
696 residuals.push(f64::INFINITY);
697 periods.push(period);
698 return Err(fail(residuals, period));
699 }
700 None => return Err(e),
701 },
702 };
703 residuals.push(rnorm);
704 periods.push(period);
705 if rnorm <= p.steady_coeff
710 && last.is_some()
711 && let Some(r) = period_run.take()
712 {
713 break r;
714 }
715 if residuals.len() >= p.max_iter {
716 return Err(fail(residuals, period));
717 }
718 if let Some(l) = &mut last
719 && rnorm > l.base_norm
720 && l.frac > min_frac
721 {
722 l.frac *= 0.5;
723 for ((xi, bx), sx) in x.iter_mut().zip(&l.x).zip(&l.dx) {
724 *xi = bx + l.frac * sx;
725 }
726 period = l.t + l.frac * l.dt;
727 continue;
728 }
729 let Some(period_run) = period_run else {
730 unreachable!("a failed run is always backtracked")
731 };
732 let (mut sx, mut st) = shooter.newton_step_osc(&x, period, osc, &period_run)?;
733 if st.abs() > MAX_DT * period {
737 let k = MAX_DT * period / st.abs();
738 sx.iter_mut().for_each(|v| *v *= k);
739 st *= k;
740 }
741 let base = x.clone();
742 for i in 1..x.len() {
743 x[i] += sx[i];
744 }
745 let bt = period;
746 period += st;
747 last = Some(OscStep {
748 x: base,
749 t: bt,
750 dx: sx,
751 dt: st,
752 base_norm: rnorm,
753 frac: 1.0,
754 });
755 }
756 };
757
758 let (lo, hi) = converged
762 .rows
763 .iter()
764 .fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), r| {
765 (a.min(r.1[osc]), b.max(r.1[osc]))
766 });
767 if hi - lo <= 10.0 * converged.tol[osc] {
768 return Err(Error::Build(format!(
769 ".pss: the shooting settled on the equilibrium (v({lc}) swings {:e} V); \
770 the circuit does not oscillate",
771 hi - lo
772 )));
773 }
774 let hist: Vec<String> = residuals
775 .iter()
776 .zip(&periods)
777 .map(|(r, t)| format!("[{r:.3e}, {t:.9e}]"))
778 .collect();
779 let dt = last.as_ref().map_or(0.0, |l| l.dt * l.frac);
780 notes.push(format!(
781 "pss: autonomous oscillator: period {period:e} s (frequency {:e} Hz) is the \
782 shooting's result (last Newton correction {dt:.3e} s); [residual/tolerance, T] \
783 per iteration: {}; phase condition v({lc}) = {:e} V at t0 = {t0:e} s",
784 1.0 / period,
785 hist.join(", "),
786 x[osc]
787 ));
788 notes.push(shooter.summary(&residuals));
789 notes.extend(shooter.lines_note());
790 let (time, spectrum) = outputs(&converged, &names, &kinds, p, t0, period);
791 Ok(Pss {
792 time,
793 spectrum,
794 period,
795 t0,
796 residuals,
797 periods,
798 notes,
799 })
800 }
801}
802
803const STAB_POINTS: f64 = 1000.0;
806const STAB_WINDOW: f64 = 0.5;
809const EST_PERIODS: usize = 4;
811const REFINE_PERIODS: f64 = 5.5;
814const GRID_PASSES: usize = 2;
817const MAX_DT: f64 = 0.2;
819
820struct OscStep {
822 x: Vec<f64>,
824 t: f64,
825 dx: Vec<f64>,
827 dt: f64,
828 base_norm: f64,
830 frac: f64,
831}
832
833#[derive(Debug)]
835struct OscStart {
836 t0: f64,
838 x0: Vec<f64>,
840 period: f64,
842 note: String,
843}
844
845fn rising_crossings(rows: &[(f64, Vec<f64>)], k: usize, level: f64) -> Vec<f64> {
848 let mut out = Vec::new();
849 for w in rows.windows(2) {
850 let (a, b) = (w[0].1[k], w[1].1[k]);
851 if a < level && b >= level {
852 out.push(w[0].0 + (w[1].0 - w[0].0) * (level - a) / (b - a));
853 }
854 }
855 out
856}
857
858fn osc_start(
869 rows: &[(f64, Vec<f64>)],
870 osc: usize,
871 name: &str,
872 guess: Option<f64>,
873 opts: &Options,
874) -> Result<OscStart, Error> {
875 let span = |from: f64| {
876 rows.iter()
877 .filter(|r| r.0 >= from)
878 .fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), r| {
879 (a.min(r.1[osc]), b.max(r.1[osc]))
880 })
881 };
882 let (Some(first), Some(end)) = (rows.first(), rows.last()) else {
883 return Err(Error::Build(".pss: empty stabilization".into()));
884 };
885 let (t_first, t_end) = (first.0, end.0);
886 let (lo, hi) = span(t_first);
887 let floor = 10.0 * (opts.reltol * lo.abs().max(hi.abs()) + opts.vntol);
888 let quiet = || {
889 Err(Error::Build(format!(
890 ".pss: the circuit does not oscillate at the oscillation node within TSTAB \
891 ({:e} V of swing over the last {:.0}% of it); kick it out of its equilibrium \
892 (an initial condition with UIC, or a supply ramp by PWL/PULSE) or lengthen \
893 TSTAB",
894 hi - lo,
895 100.0 * STAB_WINDOW
896 )))
897 };
898 if (hi - lo).is_nan() || hi - lo <= floor {
899 return quiet();
900 }
901 let c = rising_crossings(rows, osc, 0.5 * (lo + hi));
903 let estimate = |c: &[f64]| -> Option<(f64, f64)> {
904 if c.len() < 3 {
905 return None;
906 }
907 let k = EST_PERIODS.min(c.len() - 1);
908 let tail = &c[c.len() - 1 - k..];
909 let mean = (tail[k] - tail[0]) / k as f64;
910 let spread = tail
911 .windows(2)
912 .map(|w| ((w[1] - w[0]) / mean - 1.0).abs())
913 .fold(0.0, f64::max);
914 Some((mean, spread))
915 };
916 let rough = match guess {
917 Some(t) => t,
918 None => match estimate(&c) {
919 Some((t, _)) => t,
920 None => {
921 return Err(Error::Build(format!(
922 ".pss: {} rising crossing(s) of the oscillation node's mid-range in the \
923 last {:.0}% of TSTAB; estimating the period needs 3 — lengthen TSTAB or \
924 give FREQ",
925 c.len(),
926 100.0 * STAB_WINDOW
927 )));
928 }
929 },
930 };
931 let (lo2, hi2) = span(f64::max(t_first, t_end - EST_PERIODS as f64 * rough));
933 if (hi2 - lo2).is_nan() || hi2 - lo2 <= floor {
934 return quiet();
935 }
936 let level = 0.5 * (lo2 + hi2);
937 let c = rising_crossings(rows, osc, level);
938 let Some(&t0) = c.last() else {
939 return quiet();
940 };
941 let guessed = guess.map_or_else(String::new, |g| format!("; FREQ's guess was {g:e} s"));
944 let (period, how) = match (estimate(&c), guess) {
945 (Some((t, spread)), _) => {
946 let k = EST_PERIODS.min(c.len() - 1);
947 (
948 t,
949 format!(
950 "estimate {t:e} s from the last {k} periods between rising crossings \
951 (spread {spread:.1e}){guessed}"
952 ),
953 )
954 }
955 (None, Some(g)) => (
956 g,
957 format!("guess {g:e} s from FREQ (too few crossings to measure one)"),
958 ),
959 (None, None) => (rough, format!("estimate {rough:e} s (first pass)")),
960 };
961 let all = [0, rows.len() - 1];
962 let x0 = interp(rows, &all, t0);
963 Ok(OscStart {
964 t0,
965 x0,
966 period,
967 note: format!(
968 "pss: no periodic source — autonomous oscillator on v({}); initial period {how}; \
969 mid-range level {level:e} V, swing {:e} V; shooting starts at its last rising \
970 crossing, t0 = {t0:e} s",
971 name,
972 hi2 - lo2
973 ),
974 })
975}
976
977fn harvest(sources: &[(String, f64)]) -> Result<f64, Error> {
981 let Some(fmax) = sources.iter().map(|s| s.1).reduce(f64::max) else {
982 return Err(Error::Build(
983 ".pss: no periodic source; give the fundamental frequency".into(),
984 ));
985 };
986 let tol = 1e-9 * fmax;
987 let gcd = |a: f64, b: f64| {
988 let (mut a, mut b) = (a.max(b), a.min(b));
989 while b > tol {
990 let r = (a - b * (a / b).round()).abs();
991 a = b;
992 b = if r <= tol { 0.0 } else { r };
993 }
994 a
995 };
996 let f = sources.iter().map(|s| s.1).fold(0.0, gcd);
997 if f.is_nan() || f <= 0.0 || fmax / f > 1e6 {
998 return Err(Error::Build(
999 ".pss: the source frequencies share no fundamental; give it".into(),
1000 ));
1001 }
1002 Ok(f)
1003}
1004
1005fn tightened(opts: &Options) -> Options {
1007 let mut o = opts.clone();
1008 o.reltol *= REPLAY_TIGHTEN;
1009 o.vntol *= REPLAY_TIGHTEN;
1010 o.abstol *= REPLAY_TIGHTEN;
1011 o
1012}
1013
1014struct PeriodOut {
1016 rows: Vec<(f64, Vec<f64>)>,
1017 log: Vec<StepRec>,
1018 resid: Vec<f64>,
1020 tol: Vec<f64>,
1022 rnorm: f64,
1024 hnorm: f64,
1028 period: f64,
1030}
1031
1032impl PeriodOut {
1033 fn total(&self) -> f64 {
1036 self.rnorm.max(self.hnorm)
1037 }
1038}
1039
1040struct Past {
1044 orbit: PeriodicPast,
1045 breaks: Vec<usize>,
1050}
1051
1052impl Past {
1053 fn from_rows(
1060 rows: &[(f64, Vec<f64>)],
1061 log: Option<&[StepRec]>,
1062 start: f64,
1063 period: f64,
1064 max_step: f64,
1065 ) -> Option<Past> {
1066 let mb = 5e-5 * max_step;
1067 if rows.first().is_none_or(|r| r.0 > start + max_step + mb) {
1070 return None;
1071 }
1072 let mut phase = Vec::new();
1073 let mut kept = Vec::new();
1074 let mut breaks = vec![0usize];
1075 for (j, (t, x)) in rows.iter().enumerate() {
1076 if *t - start <= mb || start + period - *t <= mb {
1077 continue;
1078 }
1079 phase.push((*t - start) / period);
1080 kept.push(x.clone());
1081 if let Some(log) = log
1082 && j >= 1
1083 && log.get(j - 1).is_some_and(|r| r.brk)
1084 {
1085 breaks.push(phase.len());
1086 }
1087 }
1088 breaks.push(phase.len() + 1);
1089 Some(Past {
1090 orbit: PeriodicPast {
1091 period,
1092 phase,
1093 rows: kept,
1094 },
1095 breaks,
1096 })
1097 }
1098
1099 fn at(&self, period: f64) -> PeriodicPast {
1101 PeriodicPast {
1102 period,
1103 ..self.orbit.clone()
1104 }
1105 }
1106
1107 fn mismatch(
1111 &self,
1112 rows: &[(f64, Vec<f64>)],
1113 x: &[f64],
1114 t0: f64,
1115 period: f64,
1116 ports: &[usize],
1117 tol: &[f64],
1118 ) -> f64 {
1119 let mut closed: Vec<(f64, Vec<f64>)> = Vec::with_capacity(self.orbit.phase.len() + 2);
1120 closed.push((0.0, x.to_vec()));
1121 for (u, r) in self.orbit.phase.iter().zip(&self.orbit.rows) {
1122 closed.push((u * period, r.clone()));
1123 }
1124 closed.push((period, x.to_vec()));
1125 let mut worst = 0.0f64;
1126 let n = rows.len();
1127 for (t, sol) in rows.iter().take(n.saturating_sub(1)).skip(1) {
1128 let v = interp(&closed, &self.breaks, t - t0);
1129 for &i in ports {
1130 worst = worst.max((sol[i] - v[i]).abs() / tol[i]);
1131 }
1132 }
1133 worst
1134 }
1135}
1136
1137struct Grid {
1140 run: PeriodRun,
1141 log: Vec<StepRec>,
1142}
1143
1144impl Grid {
1145 fn of(run: PeriodRun, log: &[StepRec]) -> Grid {
1146 Grid {
1147 run,
1148 log: log.to_vec(),
1149 }
1150 }
1151
1152 fn at(&self, t: f64) -> (PeriodRun, Vec<StepRec>) {
1155 stretch(&self.run, &self.log, t)
1156 }
1157}
1158
1159fn stretch(run: &PeriodRun, log: &[StepRec], t: f64) -> (PeriodRun, Vec<StepRec>) {
1162 if t == run.period {
1163 return (*run, log.to_vec());
1164 }
1165 let k = t / run.period;
1166 let stretched = PeriodRun {
1167 t0: run.t0,
1168 period: t,
1169 tstep: run.tstep * k,
1170 max_step: run.max_step * k,
1171 };
1172 let mut log: Vec<StepRec> = log
1173 .iter()
1174 .map(|r| StepRec {
1175 time: run.t0 + (r.time - run.t0) * k,
1176 ..*r
1177 })
1178 .collect();
1179 if let Some(end) = log.last_mut() {
1180 end.time = run.t0 + t;
1181 }
1182 (stretched, log)
1183}
1184
1185struct Shooter<'a> {
1186 circuit: &'a Circuit,
1187 opts: &'a Options,
1188 tight: Options,
1189 run: PeriodRun,
1190 kinds: &'a [VarKind],
1191 adaptive: usize,
1192 replays: usize,
1193 krylov: Vec<usize>,
1195 past: Option<Past>,
1199 ports: Vec<usize>,
1200 history: Vec<f64>,
1201 refresh: Vec<f64>,
1203}
1204
1205fn period_engine(
1208 circuit: &Circuit,
1209 opts: &Options,
1210 past: Option<&Past>,
1211 period: f64,
1212) -> Result<Engine, Error> {
1213 let mut e = Engine::new(circuit.build(opts)?);
1214 e.set_periodic_past(past.map(|p| p.at(period)));
1215 Ok(e)
1216}
1217
1218impl Shooter<'_> {
1219 fn engine(&self) -> Result<Engine, Error> {
1220 period_engine(self.circuit, self.opts, self.past.as_ref(), self.run.period)
1221 }
1222
1223 fn lines_note(&self) -> Option<String> {
1226 self.past.as_ref()?;
1227 let fmt = |v: &[f64]| -> String {
1228 if v.is_empty() {
1229 "-".to_string()
1230 } else {
1231 v.iter()
1232 .map(|r| format!("{r:.3e}"))
1233 .collect::<Vec<_>>()
1234 .join(", ")
1235 }
1236 };
1237 Some(format!(
1238 "pss: transmission lines read the orbit as their history (x(t − τ) = \
1239 x(t − τ + kT)): Picard on it, Newton on x0, on a frozen grid (the first \
1240 guess is the stabilization's last period); history mismatch per replay over {} \
1241 line port unknown(s): {}; grid refreshed once from the converged orbit, the \
1242 refresh run's period mismatch (the cross-grid discretization of the lines, not \
1243 iterated on): {}",
1244 self.ports.len(),
1245 fmt(&self.history),
1246 fmt(&self.refresh)
1247 ))
1248 }
1249
1250 fn adaptive(&mut self, x: &[f64]) -> Result<PeriodOut, Error> {
1252 self.adaptive += 1;
1253 let mut log = Vec::new();
1254 let rows = self
1255 .engine()?
1256 .tran_period(self.opts, &self.run, x, &mut log)?;
1257 Ok(self.measure(rows, log, x))
1258 }
1259
1260 fn measure(&self, rows: Vec<(f64, Vec<f64>)>, log: Vec<StepRec>, x: &[f64]) -> PeriodOut {
1263 let n = x.len();
1264 let last = &rows.last().expect("the period has its end point").1;
1265 let mut peak = vec![0.0f64; n];
1266 for (_, sol) in &rows {
1267 for i in 1..n {
1268 peak[i] = peak[i].max(sol[i].abs());
1269 }
1270 }
1271 let mut tol = vec![1.0; n];
1272 let mut resid = vec![0.0; n];
1273 let mut rnorm = 0.0f64;
1274 for i in 1..n {
1275 let floor = match self.kinds[i] {
1276 VarKind::Branch => self.opts.abstol,
1277 _ => self.opts.vntol,
1278 };
1279 tol[i] = self.opts.reltol * peak[i] + floor;
1280 resid[i] = last[i] - x[i];
1281 rnorm = rnorm.max(resid[i].abs() / tol[i]);
1282 }
1283 PeriodOut {
1284 rows,
1285 log,
1286 resid,
1287 tol,
1288 rnorm,
1289 hnorm: 0.0,
1290 period: self.run.period,
1291 }
1292 }
1293
1294 fn replay_on(&mut self, x: &[f64], grid: &Grid, cur: &PeriodOut) -> Result<PeriodOut, Error> {
1302 let (t0, max_step) = (self.run.t0, self.run.max_step);
1303 let past = Past::from_rows(&cur.rows, Some(&cur.log), t0, cur.period, max_step)
1304 .expect("a period run starts at t0");
1305 let (run, log) = grid.at(self.run.period);
1306 self.replays += 1;
1307 let rows = period_engine(self.circuit, self.opts, Some(&past), run.period)?
1308 .tran_replay_rows(self.opts, &run, x, &log)?;
1309 let mut r = self.measure(rows, log, x);
1310 r.hnorm = past.mismatch(&r.rows, x, t0, run.period, &self.ports, &r.tol);
1311 self.history.push(r.hnorm);
1312 self.past = Some(past);
1313 Ok(r)
1314 }
1315
1316 fn lines_excited(
1340 &mut self,
1341 x: &mut [f64],
1342 p: &PssParams,
1343 residuals: &mut Vec<f64>,
1344 ) -> Result<PeriodOut, Error> {
1345 let fail = |residuals: &[f64]| Error::PssConvergence {
1346 iterations: residuals.len(),
1347 residuals: residuals.to_vec(),
1348 period: None,
1349 };
1350 let mut seed = self.adaptive(x)?;
1351 residuals.push(seed.rnorm);
1352 for pass in 0..GRID_PASSES {
1353 let grid = Grid::of(self.run, &seed.log);
1354 let mut cur = seed;
1355 let done = loop {
1356 if residuals.len() >= p.max_iter {
1357 return Err(fail(residuals));
1358 }
1359 let r = self.replay_on(x, &grid, &cur)?;
1360 residuals.push(r.total());
1361 if r.total() <= p.steady_coeff {
1362 break r;
1363 }
1364 if r.rnorm > p.steady_coeff {
1365 let step = self.newton_step(x, &r)?;
1366 for i in 1..x.len() {
1367 x[i] += step[i];
1368 }
1369 }
1370 cur = r;
1371 };
1372 if pass + 1 == GRID_PASSES {
1373 return Ok(done);
1374 }
1375 self.past = Some(self.past_of(&done));
1376 seed = self.adaptive(x)?;
1377 residuals.push(seed.rnorm);
1378 self.refresh.push(seed.rnorm);
1379 }
1380 unreachable!("the last pass returns")
1381 }
1382
1383 #[allow(clippy::too_many_arguments)]
1390 fn lines_autonomous(
1391 &mut self,
1392 x: &mut [f64],
1393 period: &mut f64,
1394 osc: usize,
1395 p: &PssParams,
1396 run_for: &dyn Fn(f64) -> PeriodRun,
1397 residuals: &mut Vec<f64>,
1398 periods: &mut Vec<f64>,
1399 ) -> Result<(PeriodOut, f64), Error> {
1400 let fail = |residuals: &[f64], period: f64| Error::PssConvergence {
1401 iterations: residuals.len(),
1402 residuals: residuals.to_vec(),
1403 period: Some(period),
1404 };
1405 self.run = run_for(*period);
1406 let mut seed = self.adaptive(x)?;
1407 residuals.push(seed.rnorm);
1408 periods.push(*period);
1409 let mut dt = 0.0;
1410 for pass in 0..GRID_PASSES {
1411 let grid = Grid::of(self.run, &seed.log);
1412 let mut cur = seed;
1413 let mut stepped = false;
1414 let done = loop {
1415 if residuals.len() >= p.max_iter {
1416 return Err(fail(residuals, *period));
1417 }
1418 self.run = run_for(*period);
1419 let r = self.replay_on(x, &grid, &cur)?;
1420 residuals.push(r.total());
1421 periods.push(*period);
1422 if r.total() <= p.steady_coeff && stepped {
1423 break r;
1424 }
1425 if r.rnorm > p.steady_coeff || !stepped {
1426 let (mut sx, mut st) = self.newton_step_osc(x, *period, osc, &r)?;
1427 if st.abs() > MAX_DT * *period {
1428 let k = MAX_DT * *period / st.abs();
1429 sx.iter_mut().for_each(|v| *v *= k);
1430 st *= k;
1431 }
1432 for i in 1..x.len() {
1433 x[i] += sx[i];
1434 }
1435 *period += st;
1436 dt = st;
1437 stepped = true;
1438 }
1439 cur = r;
1440 };
1441 if pass + 1 == GRID_PASSES {
1442 return Ok((done, dt));
1443 }
1444 self.run = run_for(*period);
1445 self.past = Some(self.past_of(&done));
1446 seed = self.adaptive(x)?;
1447 residuals.push(seed.rnorm);
1448 periods.push(*period);
1449 self.refresh.push(seed.rnorm);
1450 }
1451 unreachable!("the last pass returns")
1452 }
1453
1454 fn past_of(&self, done: &PeriodOut) -> Past {
1456 Past::from_rows(
1457 &done.rows,
1458 Some(&done.log),
1459 self.run.t0,
1460 done.period,
1461 self.run.max_step,
1462 )
1463 .expect("a period run starts at t0")
1464 }
1465
1466 fn newton_step(&mut self, x: &[f64], at: &PeriodOut) -> Result<Vec<f64>, Error> {
1471 match self.newton_step_with(x, at, true) {
1472 Ok(s) => Ok(s),
1473 Err(Error::IterLim) => self.newton_step_with(x, at, false),
1474 Err(e) => Err(e),
1475 }
1476 }
1477
1478 fn newton_step_with(
1479 &mut self,
1480 x: &[f64],
1481 at: &PeriodOut,
1482 tight: bool,
1483 ) -> Result<Vec<f64>, Error> {
1484 let opts = if tight { &self.tight } else { self.opts };
1485 let (circuit, build_opts, run) = (self.circuit, self.opts, self.run);
1486 let past = self.past.as_ref();
1487 let n = x.len();
1488 let d = &at.tol;
1489 let replays = std::cell::Cell::new(0usize);
1490 let replay = |x0: &[f64]| -> Result<Vec<f64>, Error> {
1491 replays.set(replays.get() + 1);
1492 period_engine(circuit, build_opts, past, run.period)?
1493 .tran_replay(opts, &run, x0, &at.log)
1494 };
1495 let base = replay(x)?;
1496 let b: Vec<f64> = (1..n).map(|i| at.resid[i] / d[i]).collect();
1498 let bnorm = norm(&b);
1499 let lin_tol = f64::min(1e-2, 1e-4 * bnorm);
1500 let mut apply = |v: &[f64]| -> Result<Vec<f64>, Error> {
1501 let mut xp = x.to_vec();
1502 for i in 1..n {
1503 xp[i] += FD_EPS * d[i] * v[i - 1];
1504 }
1505 let y = replay(&xp)?;
1506 Ok((1..n)
1507 .map(|i| v[i - 1] - (y[i] - base[i]) / (FD_EPS * d[i]))
1508 .collect())
1509 };
1510 let solved = gmres(&b, n - 1, lin_tol, &mut apply);
1511 self.replays += replays.get();
1512 let (ds, dim) = solved?;
1513 self.krylov.push(dim);
1514 let mut step = vec![0.0; n];
1515 for i in 1..n {
1516 step[i] = ds[i - 1] * d[i];
1517 }
1518 Ok(step)
1519 }
1520
1521 fn newton_step_osc(
1531 &mut self,
1532 x: &[f64],
1533 period: f64,
1534 osc: usize,
1535 at: &PeriodOut,
1536 ) -> Result<(Vec<f64>, f64), Error> {
1537 match self.newton_step_osc_with(x, period, osc, at, true) {
1538 Ok(s) => Ok(s),
1539 Err(Error::IterLim) => self.newton_step_osc_with(x, period, osc, at, false),
1540 Err(e) => Err(e),
1541 }
1542 }
1543
1544 fn newton_step_osc_with(
1545 &mut self,
1546 x: &[f64],
1547 period: f64,
1548 osc: usize,
1549 at: &PeriodOut,
1550 tight: bool,
1551 ) -> Result<(Vec<f64>, f64), Error> {
1552 let opts = if tight { &self.tight } else { self.opts };
1553 let (circuit, build_opts, run) = (self.circuit, self.opts, self.run);
1554 let n = x.len();
1555 let d = &at.tol;
1556 let s_t = self.opts.reltol * period;
1557 let slot = |i: usize| i - 1;
1562 let t_slot = osc - 1;
1563 let replays = std::cell::Cell::new(0usize);
1564 let past = self.past.as_ref();
1565 let replay = |x0: &[f64], t: f64| -> Result<Vec<f64>, Error> {
1566 replays.set(replays.get() + 1);
1567 let mut engine = period_engine(circuit, build_opts, past, t)?;
1570 if t == period {
1571 return engine.tran_replay(opts, &run, x0, &at.log);
1572 }
1573 let k = t / period;
1574 let stretched = PeriodRun {
1575 t0: run.t0,
1576 period: t,
1577 tstep: run.tstep * k,
1578 max_step: run.max_step * k,
1579 };
1580 let mut log: Vec<StepRec> = at
1581 .log
1582 .iter()
1583 .map(|r| StepRec {
1584 time: run.t0 + (r.time - run.t0) * k,
1585 ..*r
1586 })
1587 .collect();
1588 if let Some(end) = log.last_mut() {
1589 end.time = run.t0 + t;
1590 }
1591 engine.tran_replay(opts, &stretched, x0, &log)
1592 };
1593 let base = replay(x, period)?;
1594 let b: Vec<f64> = (1..n).map(|i| at.resid[i] / d[i]).collect();
1595 let bnorm = norm(&b);
1596 let lin_tol = f64::min(1e-2, 1e-4 * bnorm);
1597 let mut apply = |u: &[f64]| -> Result<Vec<f64>, Error> {
1598 let mut xp = x.to_vec();
1599 for i in 1..n {
1600 if i != osc {
1601 xp[i] += FD_EPS * d[i] * u[slot(i)];
1602 }
1603 }
1604 let y = replay(&xp, period + FD_EPS * s_t * u[t_slot])?;
1605 Ok((1..n)
1606 .map(|i| {
1607 let ui = if i == osc { 0.0 } else { u[slot(i)] };
1608 ui - (y[i] - base[i]) / (FD_EPS * d[i])
1609 })
1610 .collect())
1611 };
1612 let solved = gmres(&b, n - 1, lin_tol, &mut apply);
1613 self.replays += replays.get();
1614 let (us, dim) = solved?;
1615 self.krylov.push(dim);
1616 let mut step = vec![0.0; n];
1617 for i in 1..n {
1618 if i != osc {
1619 step[i] = us[slot(i)] * d[i];
1620 }
1621 }
1622 Ok((step, us[t_slot] * s_t))
1623 }
1624
1625 fn summary(&self, residuals: &[f64]) -> String {
1626 let hist: Vec<String> = residuals.iter().map(|r| format!("{r:.3e}")).collect();
1627 format!(
1628 "pss: {} shooting iteration(s), residual/tolerance per iteration: {}; \
1629 {} adaptive and {} replayed period transients (GMRES dimensions {:?})",
1630 residuals.len(),
1631 hist.join(", "),
1632 self.adaptive,
1633 self.replays,
1634 self.krylov
1635 )
1636 }
1637}
1638
1639type LinearMap<'a> = dyn FnMut(&[f64]) -> Result<Vec<f64>, Error> + 'a;
1641
1642fn norm(v: &[f64]) -> f64 {
1643 v.iter().map(|x| x * x).sum::<f64>().sqrt()
1644}
1645
1646fn gmres(b: &[f64], m: usize, tol: f64, apply: &mut LinearMap) -> Result<(Vec<f64>, usize), Error> {
1651 let n = b.len();
1652 let beta = norm(b);
1653 if beta == 0.0 || m == 0 {
1654 return Ok((vec![0.0; n], 0));
1655 }
1656 let mut v: Vec<Vec<f64>> = vec![b.iter().map(|x| x / beta).collect()];
1657 let mut h: Vec<Vec<f64>> = Vec::new(); let mut cs: Vec<f64> = Vec::new();
1659 let mut sn: Vec<f64> = Vec::new();
1660 let mut g = vec![beta];
1661 let mut k = 0;
1662 while k < m {
1663 let mut w = apply(&v[k])?;
1664 let mut col = vec![0.0; k + 2];
1665 for _pass in 0..2 {
1666 for (i, vi) in v.iter().enumerate() {
1667 let c: f64 = w.iter().zip(vi).map(|(a, b)| a * b).sum();
1668 col[i] += c;
1669 for (wj, vj) in w.iter_mut().zip(vi) {
1670 *wj -= c * vj;
1671 }
1672 }
1673 }
1674 let hn = norm(&w);
1675 col[k + 1] = hn;
1676 for i in 0..k {
1677 let t = cs[i] * col[i] + sn[i] * col[i + 1];
1678 col[i + 1] = -sn[i] * col[i] + cs[i] * col[i + 1];
1679 col[i] = t;
1680 }
1681 let r = col[k].hypot(col[k + 1]);
1682 let (c, s) = if r == 0.0 {
1683 (1.0, 0.0)
1684 } else {
1685 (col[k] / r, col[k + 1] / r)
1686 };
1687 col[k] = r;
1688 col[k + 1] = 0.0;
1689 cs.push(c);
1690 sn.push(s);
1691 g.push(-s * g[k]);
1692 g[k] *= c;
1693 h.push(col);
1694 k += 1;
1695 let breakdown = hn <= 1e-14 * beta;
1696 if g[k].abs() <= tol || breakdown {
1697 break;
1698 }
1699 v.push(w.iter().map(|x| x / hn).collect());
1700 }
1701 let mut y = vec![0.0; k];
1703 for i in (0..k).rev() {
1704 let mut s = g[i];
1705 for j in i + 1..k {
1706 s -= h[j][i] * y[j];
1707 }
1708 y[i] = if h[i][i] != 0.0 { s / h[i][i] } else { 0.0 };
1709 }
1710 let mut x = vec![0.0; n];
1711 for (j, yj) in y.iter().enumerate() {
1712 for (xi, vi) in x.iter_mut().zip(&v[j]) {
1713 *xi += yj * vi;
1714 }
1715 }
1716 Ok((x, k))
1717}
1718
1719fn outputs(
1721 per: &PeriodOut,
1722 names: &[String],
1723 kinds: &[VarKind],
1724 p: &PssParams,
1725 t0: f64,
1726 period: f64,
1727) -> (Solution, Solution) {
1728 let n_pts = p.points;
1729 let times: Vec<f64> = (0..=n_pts)
1730 .map(|k| t0 + period * k as f64 / n_pts as f64)
1731 .collect();
1732 let rows = &per.rows;
1735 let mut breaks = vec![0usize];
1736 breaks.extend((1..rows.len()).filter(|&j| per.log[j - 1].brk));
1737 if *breaks.last().unwrap_or(&0) != rows.len() - 1 {
1738 breaks.push(rows.len() - 1);
1739 }
1740
1741 let mut variables = vec![("time".to_string(), VarType::Time)];
1742 let mut sel: Vec<usize> = Vec::new();
1743 for i in 1..names.len() {
1744 match kinds[i] {
1745 VarKind::Node if saved_node(&names[i]) => {
1746 variables.push((format!("v({})", names[i]), VarType::Voltage));
1747 sel.push(i);
1748 }
1749 VarKind::Branch => {
1750 variables.push((format!("i({})", names[i]), VarType::Current));
1751 sel.push(i);
1752 }
1753 _ => {}
1754 }
1755 }
1756 let mut cols: Vec<Vec<f64>> = vec![times.clone()];
1757 cols.extend(sel.iter().map(|_| Vec::with_capacity(times.len())));
1758 for (k, &t) in times.iter().enumerate() {
1759 let vals: Vec<f64> = if k == n_pts {
1760 rows[rows.len() - 1].1.clone()
1761 } else if k == 0 {
1762 rows[0].1.clone()
1763 } else {
1764 interp(rows, &breaks, t)
1765 };
1766 for (c, &i) in sel.iter().enumerate() {
1767 cols[c + 1].push(vals[i]);
1768 }
1769 }
1770
1771 let h = p.harmonics;
1773 let nn = n_pts as f64;
1774 let mut fvars = vec![("frequency".to_string(), VarType::Frequency)];
1775 fvars.extend(variables[1..].iter().cloned());
1776 let mut fcols: Vec<Vec<Complex64>> = vec![
1777 (0..h)
1778 .map(|k| Complex64::new(k as f64 / period, 0.0))
1779 .collect(),
1780 ];
1781 for col in &cols[1..] {
1782 let samples = &col[..n_pts];
1783 let mut out = Vec::with_capacity(h);
1784 for k in 0..h {
1785 let mut acc = Complex64::new(0.0, 0.0);
1786 for (m, &s) in samples.iter().enumerate() {
1787 let km = (k * m) % n_pts;
1789 let ang = -2.0 * std::f64::consts::PI * km as f64 / nn;
1790 acc += s * Complex64::new(ang.cos(), ang.sin());
1791 }
1792 out.push(if k == 0 { acc / nn } else { acc * (2.0 / nn) });
1793 }
1794 fcols.push(out);
1795 }
1796 (
1797 Solution {
1798 plotname: PSS_TIME_PLOT.to_string(),
1799 variables,
1800 data: Data::Real(cols),
1801 scale: Some(0),
1802 },
1803 Solution {
1804 plotname: PSS_FREQ_PLOT.to_string(),
1805 variables: fvars,
1806 data: Data::Complex(fcols),
1807 scale: Some(0),
1808 },
1809 )
1810}
1811
1812fn interp(rows: &[(f64, Vec<f64>)], breaks: &[usize], t: f64) -> Vec<f64> {
1817 let j = rows.partition_point(|r| r.0 <= t).clamp(1, rows.len() - 1) - 1;
1819 let b = breaks.partition_point(|&b| b <= j);
1820 let lo = breaks[b - 1];
1821 let hi = breaks.get(b).copied().unwrap_or(rows.len() - 1).max(j + 1);
1822 let want = 4usize.min(hi - lo + 1);
1823 let mut a = j.saturating_sub(1).max(lo);
1824 if a + want - 1 > hi {
1825 a = hi + 1 - want;
1826 }
1827 let n = rows[0].1.len();
1828 let mut out = vec![0.0; n];
1829 for p in a..a + want {
1830 let mut w = 1.0;
1831 for q in a..a + want {
1832 if q != p {
1833 w *= (t - rows[q].0) / (rows[p].0 - rows[q].0);
1834 }
1835 }
1836 for (o, v) in out.iter_mut().zip(&rows[p].1) {
1837 *o += w * v;
1838 }
1839 }
1840 out
1841}
1842
1843#[cfg(test)]
1844mod tests {
1845 use super::*;
1846
1847 #[test]
1848 fn harvest_takes_the_common_fundamental() {
1849 let s = |f: &[f64]| -> Vec<(String, f64)> { f.iter().map(|&f| ("v".into(), f)).collect() };
1850 assert!((harvest(&s(&[1e3])).unwrap() - 1e3).abs() < 1e-9);
1851 assert!((harvest(&s(&[1e3, 3e3])).unwrap() - 1e3).abs() < 1e-6);
1852 assert!((harvest(&s(&[2e3, 3e3])).unwrap() - 1e3).abs() < 1e-6);
1853 assert!((harvest(&s(&[50e3, 1.0 / 20e-6])).unwrap() - 50e3).abs() < 1e-3);
1854 assert!(harvest(&s(&[1.0, std::f64::consts::PI])).is_err());
1855 assert!(harvest(&[]).is_err());
1856 }
1857
1858 #[test]
1859 fn gmres_solves_a_small_system() {
1860 let u = [0.5, -0.2, 0.1];
1862 let w = [0.3, 0.4, -0.6];
1863 let mut apply = |v: &[f64]| -> Result<Vec<f64>, Error> {
1864 let d: f64 = w.iter().zip(v).map(|(a, b)| a * b).sum();
1865 Ok(v.iter().zip(&u).map(|(vi, ui)| vi - ui * d).collect())
1866 };
1867 let b = [1.0, 2.0, 3.0];
1868 let (y, k) = gmres(&b, 3, 1e-13, &mut apply).unwrap();
1869 assert!(k <= 2);
1870 let ay = apply(&y).unwrap();
1871 for (a, b) in ay.iter().zip(&b) {
1872 assert!((a - b).abs() < 1e-12);
1873 }
1874 }
1875}