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::integ::Method;
54use crate::matrix::PeriodicPast;
55use crate::options::Options;
56use crate::solution::Solution;
57use crate::srcfn::Wave;
58
59pub const PSS_TIME_PLOT: &str = "Time Domain Periodic Steady State Analysis";
61pub const PSS_FREQ_PLOT: &str = "Frequency Domain Periodic Steady State Analysis";
63
64#[derive(Clone, Debug, PartialEq)]
66pub struct PssParams {
67 pub freq: Option<f64>,
73 pub tstab: f64,
77 pub osc_node: Option<String>,
82 pub points: usize,
85 pub harmonics: usize,
88 pub max_iter: usize,
90 pub steady_coeff: f64,
95 pub uic: bool,
97}
98
99impl Default for PssParams {
100 fn default() -> Self {
101 PssParams {
102 freq: None,
103 tstab: 0.0,
104 osc_node: None,
105 points: 128,
106 harmonics: 10,
107 max_iter: 50,
108 steady_coeff: 1.0,
109 uic: false,
110 }
111 }
112}
113
114#[derive(Clone, Debug)]
116pub struct Pss {
117 pub time: Solution,
121 pub spectrum: Solution,
127 pub period: f64,
130 pub t0: f64,
132 pub residuals: Vec<f64>,
135 pub periods: Vec<f64>,
139 pub notes: Vec<String>,
140}
141
142const FD_EPS: f64 = 1e-2;
145const REPLAY_TIGHTEN: f64 = 1e-3;
150const MAX_BACKTRACK: usize = 4;
152
153impl Circuit {
154 pub fn pss(&self, opts: &Options, p: &PssParams) -> Result<Pss, Error> {
161 let stab_method = opts.pss_stab_method();
164 let opts = &Options {
165 method: Some(opts.pss_method()),
166 ..opts.clone()
167 };
168 let bad = |m: &str| Err(Error::Build(format!(".pss {m}")));
169 if !(p.tstab.is_finite() && p.tstab >= 0.0) {
170 return bad("stabilization time must be finite and not negative");
171 }
172 if p.points < 4 || p.points > 10_000_000 {
173 return bad("needs 4 to 10 000 000 points per period");
174 }
175 if p.harmonics < 1 || p.harmonics > p.points / 2 {
176 return bad("harmonics must be between 1 and points/2");
177 }
178 if p.max_iter < 1 {
179 return bad("needs at least one shooting iteration");
180 }
181 if !(p.steady_coeff.is_finite() && p.steady_coeff > 0.0) {
182 return bad("steady coefficient must be positive");
183 }
184 if opts.dev_noise {
185 return Err(Error::Build(
190 ".pss: `.options devnoise` is on, and transient device noise is a random \
191 stimulus, which has no periodic steady state; turn it off for the shooting"
192 .into(),
193 ));
194 }
195 for spec in &self.specs {
196 let noisy = match spec {
197 Spec::Vsource(v) => &v.wave,
198 Spec::Isource(i) => &i.wave,
199 _ => &None,
200 };
201 if matches!(noisy, Some(Wave::TrNoise(_) | Wave::TrRandom(_))) {
202 return Err(Error::Unsupported {
205 name: spec.name().to_string(),
206 what: "a transient-noise source (TRNOISE/TRRANDOM) in .pss — a random \
207 stimulus has no periodic steady state"
208 .into(),
209 });
210 }
211 if let Spec::Ltra(l) = spec
212 && !opts.ltra_fit
213 {
214 let lp = crate::ltra::LtraParams::new(&l.name, &l.model, opts.reltol, opts.abstol)
217 .map_err(Error::Build)?;
218 if matches!(
219 lp.case,
220 crate::ltra::LtraCase::Rlc | crate::ltra::LtraCase::Rc
221 ) {
222 return Err(Error::Build(format!(
223 ".pss: the lossy (RLC/RC) line `{}` needs `.options ltrafit`: in the \
224 periodic regime its history is the orbit repeated over the whole past, \
225 which the exact convolution (a walk over a history that starts at an \
226 operating point) cannot hold, and the recursive model sums in closed \
227 form",
228 l.name
229 )));
230 }
231 }
232 }
233 let mut notes = Vec::new();
234 let sources = self.source_frequencies()?;
235 if sources.is_empty() {
236 return self.pss_autonomous(opts, p, stab_method);
239 }
240 let freq = match p.freq {
241 Some(f) => {
242 if !(f.is_finite() && f > 0.0) {
243 return bad("frequency must be positive");
244 }
245 for (name, fs) in &sources {
246 let k = fs / f;
247 if (k - k.round()).abs() > 1e-6 * k.max(1.0) || k.round() < 1.0 {
248 notes.push(format!(
249 "pss: source `{name}` ({fs:e} Hz) is not a harmonic of the \
250 fundamental {f:e} Hz; the circuit is not {:e}-s periodic",
251 1.0 / f
252 ));
253 }
254 }
255 f
256 }
257 None => harvest(&sources)?,
258 };
259 let period = 1.0 / freq;
260 let tstep = period / p.points as f64;
261 let max_step = f64::min(tstep, period / 50.0);
262 let n_stab = (p.tstab / period - 1e-9).ceil().max(1.0);
263 if n_stab > 1e9 {
264 return bad("stabilization spans too many periods");
265 }
266 let t0 = n_stab * period;
267 let run = PeriodRun {
268 t0,
269 period,
270 tstep,
271 max_step,
272 };
273
274 let built = self.build(opts)?;
276 notes.extend(built.notes.iter().cloned());
277 let mut engine = Engine::new(built);
278 let nvars = engine.vars.len() as f64;
283 let pts = p.points as f64;
284 if (pts + 1.0) * nvars > MAX_OUTPUT_VALUES as f64
285 || n_stab * pts > 1e8
286 || p.harmonics as f64 * pts * nvars > 1e9
287 {
288 return bad("asks for too many points or periods; refusing");
289 }
290 if let Some(node) = &p.osc_node {
291 let lc = node.to_ascii_lowercase();
292 if !engine.vars.names.contains(&lc) {
293 return Err(Error::Build(format!(".pss: no node `{node}` for oscnode")));
294 }
295 notes.push(format!(
296 "pss: oscnode `{lc}` is for autonomous circuits; the driven shooting does not use it"
297 ));
298 }
299 let ports = self.line_ports(&engine.vars.names);
300 let lines = !ports.is_empty();
301 let stab = engine.tran(
304 opts,
305 &TranParams {
306 tstep,
307 tstop: t0,
308 tstart: if lines { t0 - period } else { t0 },
309 tmax: Some(max_step),
310 uic: p.uic,
311 },
312 )?;
313 let names = engine.vars.names.clone();
314 let kinds = engine.vars.kinds.clone();
315 let mut x = engine.sol.clone();
316 drop(engine);
317
318 let mut shooter = Shooter {
319 circuit: self,
320 opts,
321 tight: tightened(opts),
322 run,
323 kinds: &kinds,
324 adaptive: 0,
325 replays: 0,
326 krylov: Vec::new(),
327 past: if lines {
328 Some(
329 Past::from_rows(&stab, None, t0 - period, period, max_step)
330 .ok_or_else(|| Error::Build(".pss: no stabilization period".into()))?,
331 )
332 } else {
333 None
334 },
335 ports,
336 history: Vec::new(),
337 refresh: Vec::new(),
338 };
339 let mut residuals = Vec::new();
340 let converged = if lines {
341 shooter.lines_excited(&mut x, p, &mut residuals)?
342 } else {
343 let mut last: Option<(Vec<f64>, Vec<f64>, f64, f64)> = None;
345 let min_frac = 0.5f64.powi(MAX_BACKTRACK as i32);
346 loop {
347 let (mut period_run, rnorm) = match shooter.adaptive(&x) {
351 Ok(r) => {
352 let n = r.rnorm;
353 (Some(r), n)
354 }
355 Err(e) => match &last {
356 Some((_, _, _, frac)) if *frac > min_frac => (None, f64::INFINITY),
357 _ => return Err(e),
358 },
359 };
360 residuals.push(rnorm);
361 if rnorm <= p.steady_coeff
362 && let Some(r) = period_run.take()
363 {
364 break r;
365 }
366 if residuals.len() >= p.max_iter {
367 return Err(Error::PssConvergence {
368 iterations: residuals.len(),
369 residuals,
370 period: None,
371 });
372 }
373 if let Some((base, step, base_norm, frac)) = &mut last
375 && rnorm > *base_norm
376 && *frac > min_frac
377 {
378 *frac *= 0.5;
379 for i in 0..x.len() {
380 x[i] = base[i] + *frac * step[i];
381 }
382 continue;
383 }
384 let Some(period_run) = period_run else {
385 unreachable!("a failed run is always backtracked")
386 };
387 let step = shooter.newton_step(&x, &period_run)?;
388 let base = x.clone();
389 for i in 1..x.len() {
390 x[i] += step[i];
391 }
392 last = Some((base, step, rnorm, 1.0));
393 }
394 };
395
396 notes.push(format!(
397 "pss: period {period:e} s (fundamental {freq:e} Hz{}); shooting over \
398 [{t0:e}, {:e}] s after {n_stab} stabilization period(s)",
399 if p.freq.is_some() {
400 String::new()
401 } else {
402 ", from the sources".to_string()
403 },
404 t0 + period
405 ));
406 notes.push(shooter.summary(&residuals));
407 notes.extend(shooter.lines_note());
408
409 let (time, spectrum) = outputs(&converged, &names, &kinds, p, t0, period);
410 let periods = vec![period; residuals.len()];
411 Ok(Pss {
412 time,
413 spectrum,
414 period,
415 t0,
416 residuals,
417 periods,
418 notes,
419 })
420 }
421
422 fn line_ports(&self, names: &[String]) -> Vec<usize> {
427 let find = |n: &str| names.iter().position(|v| v.eq_ignore_ascii_case(n));
428 let mut out = Vec::new();
429 for spec in &self.specs {
430 let (name, nodes) = match spec {
431 Spec::Ltra(l) => (&l.name, &l.nodes),
432 Spec::Tra(t) => (&t.name, &t.nodes),
433 _ => continue,
434 };
435 let branches = [format!("{name}#i1"), format!("{name}#i2")];
436 for n in nodes
437 .iter()
438 .map(String::as_str)
439 .chain(branches.iter().map(String::as_str))
440 {
441 if let Some(i) = find(n)
442 && i != 0
443 && !out.contains(&i)
444 {
445 out.push(i);
446 }
447 }
448 }
449 out
450 }
451
452 fn source_frequencies(&self) -> Result<Vec<(String, f64)>, Error> {
458 let mut out = Vec::new();
459 for spec in &self.specs {
460 let (name, wave) = match spec {
461 Spec::Vsource(v) => (&v.name, &v.wave),
462 Spec::Isource(i) => (&i.name, &i.wave),
463 _ => continue,
464 };
465 let Some(wave) = wave else { continue };
466 let given = |c: &[f64], i: usize| c.get(i).copied().filter(|&v| v != 0.0);
467 let no_period = |what: &str| {
468 Err(Error::Build(format!(
469 ".pss: source `{name}`: {what} has no fixed period"
470 )))
471 };
472 match wave {
473 Wave::Sin(c) => {
474 if given(c, 1).is_none() {
475 continue;
476 }
477 if given(c, 4).is_some() {
478 return no_period("a damped SIN");
479 }
480 match given(c, 2) {
481 Some(f) => out.push((name.clone(), f.abs())),
482 None => return no_period("a SIN without a frequency"),
483 }
484 }
485 Wave::Sffm(c) => {
486 if given(c, 1).is_none() {
487 continue;
488 }
489 match given(c, 2) {
490 Some(f) => out.push((name.clone(), f.abs())),
491 None => return no_period("an SFFM without a carrier frequency"),
492 }
493 if given(c, 3).is_some() {
494 match given(c, 4) {
495 Some(f) => out.push((name.clone(), f.abs())),
496 None => return no_period("an SFFM without a signal frequency"),
497 }
498 }
499 }
500 Wave::Pulse(c) => {
501 let v1 = c.first().copied().unwrap_or(0.0);
502 let v2 = c.get(1).copied().unwrap_or(0.0);
503 if v1 != v2
504 && let Some(per) = given(c, 6)
505 {
506 out.push((name.clone(), 1.0 / per.abs()));
507 }
508 }
509 Wave::Exp(_) | Wave::Pwl(_) => {}
510 Wave::TrNoise(_) | Wave::TrRandom(_) => {}
512 }
513 }
514 Ok(out)
515 }
516}
517
518impl Circuit {
519 fn pss_autonomous(
528 &self,
529 opts: &Options,
530 p: &PssParams,
531 stab_method: Method,
532 ) -> Result<Pss, Error> {
533 let bad = |m: &str| Err(Error::Build(format!(".pss {m}")));
534 let guess = match p.freq {
535 Some(f) if !(f.is_finite() && f > 0.0) => return bad("frequency must be positive"),
536 Some(f) => Some(1.0 / f),
537 None => None,
538 };
539 let Some(node) = &p.osc_node else {
540 return bad(
541 "of an autonomous circuit (no periodic source) needs the oscillation node \
542 OSCNODE",
543 );
544 };
545 if p.tstab <= 0.0 {
546 return bad(
547 "of an autonomous circuit needs a stabilization time TSTAB: the oscillation \
548 must start and be measured before the shooting",
549 );
550 }
551 let built = self.build(opts)?;
552 let mut notes: Vec<String> = built.notes.clone();
553 let mut engine = Engine::new(built);
554 let lc = node.to_ascii_lowercase();
555 let Some(osc) = engine.vars.names.iter().position(|n| *n == lc) else {
556 return Err(Error::Build(format!(".pss: no node `{node}` for oscnode")));
557 };
558 if osc == 0 || engine.vars.kinds[osc] != VarKind::Node {
559 return Err(Error::Build(format!(
560 ".pss: oscnode `{node}` must be a circuit node"
561 )));
562 }
563 let (tstep, max_step) = match guess {
566 Some(t) => (t / p.points as f64, f64::min(t / p.points as f64, t / 50.0)),
567 None => (p.tstab / STAB_POINTS, p.tstab / STAB_POINTS),
568 };
569 let nvars = engine.vars.len() as f64;
570 let pts = p.points as f64;
571 if (pts + 1.0) * nvars > MAX_OUTPUT_VALUES as f64
572 || p.tstab / tstep > 1e8
573 || p.harmonics as f64 * pts * nvars > 1e9
574 {
575 return bad("asks for too many points or periods; refusing");
576 }
577 let stab_opts = Options {
581 method: Some(stab_method),
582 ..opts.clone()
583 };
584 let rows = engine.tran(
585 &stab_opts,
586 &TranParams {
587 tstep,
588 tstop: p.tstab,
589 tstart: p.tstab * (1.0 - STAB_WINDOW),
590 tmax: Some(max_step),
591 uic: p.uic,
592 },
593 )?;
594 let names = engine.vars.names.clone();
595 let kinds = engine.vars.kinds.clone();
596 let ports = self.line_ports(&names);
597 drop(engine);
598 let no_past = || {
602 Error::Build(
603 ".pss: the stabilization window holds less than one period before the \
604 shooting start; lengthen TSTAB"
605 .into(),
606 )
607 };
608 let past_before = |rows: &[(f64, Vec<f64>)], at: f64, t: f64, max_step: f64| {
609 if ports.is_empty() {
610 Ok(None)
611 } else {
612 Past::from_rows(rows, None, at - t, t, max_step)
613 .map(Some)
614 .ok_or_else(no_past)
615 }
616 };
617 let mut start = osc_start(&rows, osc, &lc, guess, opts)?;
618 let mut start_rows = rows;
619 if guess.is_none() {
620 let (t_end, x_end) = start_rows.last().expect("the stabilization has rows");
624 let t = start.period;
625 let tstep = t / p.points as f64;
626 let refine = PeriodRun {
627 t0: *t_end,
628 period: REFINE_PERIODS * t,
629 tstep,
630 max_step: f64::min(tstep, t / 50.0),
631 };
632 let past = past_before(&start_rows, *t_end, t, max_step)?;
633 let mut engine = Engine::new(self.build(opts)?);
634 engine.set_periodic_past(past.as_ref().map(|p| p.at(t)));
635 let fine = engine.tran_period(opts, &refine, x_end, &mut Vec::new())?;
636 notes.push(start.note);
637 start = osc_start(&fine, osc, &lc, None, opts)?;
638 start_rows = fine;
639 start.note = format!(
640 "{} (refined over {REFINE_PERIODS} periods continued from TSTAB at T/POINTS)",
641 start.note
642 );
643 }
644 notes.push(start.note.clone());
645 let t0 = start.t0;
646 let mut x = start.x0;
647 let mut period = start.period;
648
649 let run_for = |t: f64| PeriodRun {
650 t0,
651 period: t,
652 tstep: t / p.points as f64,
653 max_step: f64::min(t / p.points as f64, t / 50.0),
654 };
655 let first_past = past_before(&start_rows, t0, period, run_for(period).max_step)?;
656 drop(start_rows);
657 let mut shooter = Shooter {
658 circuit: self,
659 opts,
660 tight: tightened(opts),
661 run: run_for(period),
662 kinds: &kinds,
663 adaptive: 0,
664 replays: 0,
665 krylov: Vec::new(),
666 past: first_past,
667 ports: ports.clone(),
668 history: Vec::new(),
669 refresh: Vec::new(),
670 };
671 let mut residuals = Vec::new();
672 let mut periods = Vec::new();
673 let mut last: Option<OscStep> = None;
674 let min_frac = 0.5f64.powi(MAX_BACKTRACK as i32);
675 let fail = |residuals: Vec<f64>, period: f64| Error::PssConvergence {
676 iterations: residuals.len(),
677 residuals,
678 period: Some(period),
679 };
680 let converged = if shooter.past.is_some() {
681 let (r, dt) = shooter.lines_autonomous(
682 &mut x,
683 &mut period,
684 osc,
685 p,
686 &run_for,
687 &mut residuals,
688 &mut periods,
689 )?;
690 last = Some(OscStep {
691 x: Vec::new(),
692 t: period,
693 dx: Vec::new(),
694 dt,
695 base_norm: 0.0,
696 frac: 1.0,
697 });
698 r
699 } else {
700 loop {
701 shooter.run = run_for(period);
702 let (mut period_run, rnorm) = match shooter.adaptive(&x) {
703 Ok(r) => {
704 let n = r.rnorm;
705 (Some(r), n)
706 }
707 Err(e) => match &last {
708 Some(l) if l.frac > min_frac => (None, f64::INFINITY),
709 Some(_) => {
710 residuals.push(f64::INFINITY);
711 periods.push(period);
712 return Err(fail(residuals, period));
713 }
714 None => return Err(e),
715 },
716 };
717 residuals.push(rnorm);
718 periods.push(period);
719 if rnorm <= p.steady_coeff
724 && last.is_some()
725 && let Some(r) = period_run.take()
726 {
727 break r;
728 }
729 if residuals.len() >= p.max_iter {
730 return Err(fail(residuals, period));
731 }
732 if let Some(l) = &mut last
733 && rnorm > l.base_norm
734 && l.frac > min_frac
735 {
736 l.frac *= 0.5;
737 for ((xi, bx), sx) in x.iter_mut().zip(&l.x).zip(&l.dx) {
738 *xi = bx + l.frac * sx;
739 }
740 period = l.t + l.frac * l.dt;
741 continue;
742 }
743 let Some(period_run) = period_run else {
744 unreachable!("a failed run is always backtracked")
745 };
746 let (mut sx, mut st) = shooter.newton_step_osc(&x, period, osc, &period_run)?;
747 if st.abs() > MAX_DT * period {
751 let k = MAX_DT * period / st.abs();
752 sx.iter_mut().for_each(|v| *v *= k);
753 st *= k;
754 }
755 let base = x.clone();
756 for i in 1..x.len() {
757 x[i] += sx[i];
758 }
759 let bt = period;
760 period += st;
761 last = Some(OscStep {
762 x: base,
763 t: bt,
764 dx: sx,
765 dt: st,
766 base_norm: rnorm,
767 frac: 1.0,
768 });
769 }
770 };
771
772 let (lo, hi) = converged
776 .rows
777 .iter()
778 .fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), r| {
779 (a.min(r.1[osc]), b.max(r.1[osc]))
780 });
781 if hi - lo <= 10.0 * converged.tol[osc] {
782 return Err(Error::Build(format!(
783 ".pss: the shooting settled on the equilibrium (v({lc}) swings {:e} V); \
784 the circuit does not oscillate",
785 hi - lo
786 )));
787 }
788 let hist: Vec<String> = residuals
789 .iter()
790 .zip(&periods)
791 .map(|(r, t)| format!("[{r:.3e}, {t:.9e}]"))
792 .collect();
793 let dt = last.as_ref().map_or(0.0, |l| l.dt * l.frac);
794 notes.push(format!(
795 "pss: autonomous oscillator: period {period:e} s (frequency {:e} Hz) is the \
796 shooting's result (last Newton correction {dt:.3e} s); [residual/tolerance, T] \
797 per iteration: {}; phase condition v({lc}) = {:e} V at t0 = {t0:e} s",
798 1.0 / period,
799 hist.join(", "),
800 x[osc]
801 ));
802 notes.push(shooter.summary(&residuals));
803 notes.extend(shooter.lines_note());
804 let (time, spectrum) = outputs(&converged, &names, &kinds, p, t0, period);
805 Ok(Pss {
806 time,
807 spectrum,
808 period,
809 t0,
810 residuals,
811 periods,
812 notes,
813 })
814 }
815}
816
817const STAB_POINTS: f64 = 1000.0;
820const STAB_WINDOW: f64 = 0.5;
823const EST_PERIODS: usize = 4;
825const REFINE_PERIODS: f64 = 5.5;
828const GRID_PASSES: usize = 2;
831const MAX_DT: f64 = 0.2;
833
834struct OscStep {
836 x: Vec<f64>,
838 t: f64,
839 dx: Vec<f64>,
841 dt: f64,
842 base_norm: f64,
844 frac: f64,
845}
846
847#[derive(Debug)]
849struct OscStart {
850 t0: f64,
852 x0: Vec<f64>,
854 period: f64,
856 note: String,
857}
858
859fn rising_crossings(rows: &[(f64, Vec<f64>)], k: usize, level: f64) -> Vec<f64> {
862 let mut out = Vec::new();
863 for w in rows.windows(2) {
864 let (a, b) = (w[0].1[k], w[1].1[k]);
865 if a < level && b >= level {
866 out.push(w[0].0 + (w[1].0 - w[0].0) * (level - a) / (b - a));
867 }
868 }
869 out
870}
871
872fn osc_start(
883 rows: &[(f64, Vec<f64>)],
884 osc: usize,
885 name: &str,
886 guess: Option<f64>,
887 opts: &Options,
888) -> Result<OscStart, Error> {
889 let span = |from: f64| {
890 rows.iter()
891 .filter(|r| r.0 >= from)
892 .fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), r| {
893 (a.min(r.1[osc]), b.max(r.1[osc]))
894 })
895 };
896 let (Some(first), Some(end)) = (rows.first(), rows.last()) else {
897 return Err(Error::Build(".pss: empty stabilization".into()));
898 };
899 let (t_first, t_end) = (first.0, end.0);
900 let (lo, hi) = span(t_first);
901 let floor = 10.0 * (opts.reltol * lo.abs().max(hi.abs()) + opts.vntol);
902 let quiet = || {
903 Err(Error::Build(format!(
904 ".pss: the circuit does not oscillate at the oscillation node within TSTAB \
905 ({:e} V of swing over the last {:.0}% of it); kick it out of its equilibrium \
906 (an initial condition with UIC, or a supply ramp by PWL/PULSE) or lengthen \
907 TSTAB",
908 hi - lo,
909 100.0 * STAB_WINDOW
910 )))
911 };
912 if (hi - lo).is_nan() || hi - lo <= floor {
913 return quiet();
914 }
915 let c = rising_crossings(rows, osc, 0.5 * (lo + hi));
917 let estimate = |c: &[f64]| -> Option<(f64, f64)> {
918 if c.len() < 3 {
919 return None;
920 }
921 let k = EST_PERIODS.min(c.len() - 1);
922 let tail = &c[c.len() - 1 - k..];
923 let mean = (tail[k] - tail[0]) / k as f64;
924 let spread = tail
925 .windows(2)
926 .map(|w| ((w[1] - w[0]) / mean - 1.0).abs())
927 .fold(0.0, f64::max);
928 Some((mean, spread))
929 };
930 let rough = match guess {
931 Some(t) => t,
932 None => match estimate(&c) {
933 Some((t, _)) => t,
934 None => {
935 return Err(Error::Build(format!(
936 ".pss: {} rising crossing(s) of the oscillation node's mid-range in the \
937 last {:.0}% of TSTAB; estimating the period needs 3 — lengthen TSTAB or \
938 give FREQ",
939 c.len(),
940 100.0 * STAB_WINDOW
941 )));
942 }
943 },
944 };
945 let (lo2, hi2) = span(f64::max(t_first, t_end - EST_PERIODS as f64 * rough));
947 if (hi2 - lo2).is_nan() || hi2 - lo2 <= floor {
948 return quiet();
949 }
950 let level = 0.5 * (lo2 + hi2);
951 let c = rising_crossings(rows, osc, level);
952 let Some(&t0) = c.last() else {
953 return quiet();
954 };
955 let guessed = guess.map_or_else(String::new, |g| format!("; FREQ's guess was {g:e} s"));
958 let (period, how) = match (estimate(&c), guess) {
959 (Some((t, spread)), _) => {
960 let k = EST_PERIODS.min(c.len() - 1);
961 (
962 t,
963 format!(
964 "estimate {t:e} s from the last {k} periods between rising crossings \
965 (spread {spread:.1e}){guessed}"
966 ),
967 )
968 }
969 (None, Some(g)) => (
970 g,
971 format!("guess {g:e} s from FREQ (too few crossings to measure one)"),
972 ),
973 (None, None) => (rough, format!("estimate {rough:e} s (first pass)")),
974 };
975 let all = [0, rows.len() - 1];
976 let x0 = interp(rows, &all, t0);
977 Ok(OscStart {
978 t0,
979 x0,
980 period,
981 note: format!(
982 "pss: no periodic source — autonomous oscillator on v({}); initial period {how}; \
983 mid-range level {level:e} V, swing {:e} V; shooting starts at its last rising \
984 crossing, t0 = {t0:e} s",
985 name,
986 hi2 - lo2
987 ),
988 })
989}
990
991fn harvest(sources: &[(String, f64)]) -> Result<f64, Error> {
995 let Some(fmax) = sources.iter().map(|s| s.1).reduce(f64::max) else {
996 return Err(Error::Build(
997 ".pss: no periodic source; give the fundamental frequency".into(),
998 ));
999 };
1000 let tol = 1e-9 * fmax;
1001 let gcd = |a: f64, b: f64| {
1002 let (mut a, mut b) = (a.max(b), a.min(b));
1003 while b > tol {
1004 let r = (a - b * (a / b).round()).abs();
1005 a = b;
1006 b = if r <= tol { 0.0 } else { r };
1007 }
1008 a
1009 };
1010 let f = sources.iter().map(|s| s.1).fold(0.0, gcd);
1011 if f.is_nan() || f <= 0.0 || fmax / f > 1e6 {
1012 return Err(Error::Build(
1013 ".pss: the source frequencies share no fundamental; give it".into(),
1014 ));
1015 }
1016 Ok(f)
1017}
1018
1019fn tightened(opts: &Options) -> Options {
1021 let mut o = opts.clone();
1022 o.reltol *= REPLAY_TIGHTEN;
1023 o.vntol *= REPLAY_TIGHTEN;
1024 o.abstol *= REPLAY_TIGHTEN;
1025 o
1026}
1027
1028struct PeriodOut {
1030 rows: Vec<(f64, Vec<f64>)>,
1031 log: Vec<StepRec>,
1032 resid: Vec<f64>,
1034 tol: Vec<f64>,
1036 rnorm: f64,
1038 hnorm: f64,
1042 period: f64,
1044}
1045
1046impl PeriodOut {
1047 fn total(&self) -> f64 {
1050 self.rnorm.max(self.hnorm)
1051 }
1052}
1053
1054#[derive(Default)]
1063struct Aitken {
1064 seen: Vec<Iterate>,
1066}
1067
1068struct Iterate {
1070 residual: f64,
1071 x: Vec<f64>,
1072 period: f64,
1073 orbit: Vec<Vec<f64>>,
1075}
1076
1077impl Aitken {
1078 const AGREE: f64 = 0.05;
1080
1081 fn push(&mut self, x: &mut [f64], period: &mut f64, cur: &mut PeriodOut, t0: f64) -> bool {
1086 let values: Vec<Vec<f64>> = cur.rows.iter().map(|(_, v)| v.clone()).collect();
1087 if self
1088 .seen
1089 .last()
1090 .is_some_and(|l| l.orbit.len() != values.len())
1091 {
1092 self.seen.clear();
1093 }
1094 self.seen.push(Iterate {
1095 residual: cur.total(),
1096 x: x.to_vec(),
1097 period: *period,
1098 orbit: values,
1099 });
1100 let n = self.seen.len();
1101 if n < 3 {
1102 return false;
1103 }
1104 let (a, b, c) = (&self.seen[n - 3], &self.seen[n - 2], &self.seen[n - 1]);
1105 let (r1, r2) = (b.residual / a.residual, c.residual / b.residual);
1106 if !(0.3..0.98).contains(&r2) || (r1 - r2).abs() > Self::AGREE * r2 {
1107 return false;
1108 }
1109 let f = r2 / (1.0 - r2);
1110 for (xi, (ci, bi)) in x.iter_mut().zip(c.x.iter().zip(&b.x)) {
1111 *xi += f * (ci - bi);
1112 }
1113 let old = cur.period;
1114 *period += f * (c.period - b.period);
1115 let new = old + f * (c.period - b.period);
1118 for ((t, v), (cv, bv)) in cur.rows.iter_mut().zip(c.orbit.iter().zip(&b.orbit)) {
1119 *t = t0 + (*t - t0) * new / old;
1120 for (vi, (ci, bi)) in v.iter_mut().zip(cv.iter().zip(bv)) {
1121 *vi += f * (ci - bi);
1122 }
1123 }
1124 for rec in &mut cur.log {
1125 rec.time = t0 + (rec.time - t0) * new / old;
1126 }
1127 cur.period = new;
1128 self.seen.clear();
1129 true
1130 }
1131}
1132
1133struct Past {
1137 orbit: PeriodicPast,
1138 breaks: Vec<usize>,
1143}
1144
1145impl Past {
1146 fn from_rows(
1153 rows: &[(f64, Vec<f64>)],
1154 log: Option<&[StepRec]>,
1155 start: f64,
1156 period: f64,
1157 max_step: f64,
1158 ) -> Option<Past> {
1159 let mb = 5e-5 * max_step;
1160 if rows.first().is_none_or(|r| r.0 > start + max_step + mb) {
1163 return None;
1164 }
1165 let mut phase = Vec::new();
1166 let mut kept = Vec::new();
1167 let mut breaks = vec![0usize];
1168 for (j, (t, x)) in rows.iter().enumerate() {
1169 if *t - start <= mb || start + period - *t <= mb {
1170 continue;
1171 }
1172 phase.push((*t - start) / period);
1173 kept.push(x.clone());
1174 if let Some(log) = log
1175 && j >= 1
1176 && log.get(j - 1).is_some_and(|r| r.brk)
1177 {
1178 breaks.push(phase.len());
1179 }
1180 }
1181 breaks.push(phase.len() + 1);
1182 Some(Past {
1183 orbit: PeriodicPast {
1184 period,
1185 phase,
1186 rows: kept,
1187 },
1188 breaks,
1189 })
1190 }
1191
1192 fn at(&self, period: f64) -> PeriodicPast {
1194 PeriodicPast {
1195 period,
1196 ..self.orbit.clone()
1197 }
1198 }
1199
1200 fn mismatch(
1204 &self,
1205 rows: &[(f64, Vec<f64>)],
1206 x: &[f64],
1207 t0: f64,
1208 period: f64,
1209 ports: &[usize],
1210 tol: &[f64],
1211 ) -> f64 {
1212 let mut closed: Vec<(f64, Vec<f64>)> = Vec::with_capacity(self.orbit.phase.len() + 2);
1213 closed.push((0.0, x.to_vec()));
1214 for (u, r) in self.orbit.phase.iter().zip(&self.orbit.rows) {
1215 closed.push((u * period, r.clone()));
1216 }
1217 closed.push((period, x.to_vec()));
1218 let mut worst = 0.0f64;
1219 let n = rows.len();
1220 for (t, sol) in rows.iter().take(n.saturating_sub(1)).skip(1) {
1221 let v = interp(&closed, &self.breaks, t - t0);
1222 for &i in ports {
1223 worst = worst.max((sol[i] - v[i]).abs() / tol[i]);
1224 }
1225 }
1226 worst
1227 }
1228}
1229
1230struct Grid {
1233 run: PeriodRun,
1234 log: Vec<StepRec>,
1235}
1236
1237impl Grid {
1238 fn of(run: PeriodRun, log: &[StepRec]) -> Grid {
1239 Grid {
1240 run,
1241 log: log.to_vec(),
1242 }
1243 }
1244
1245 fn at(&self, t: f64) -> (PeriodRun, Vec<StepRec>) {
1248 stretch(&self.run, &self.log, t)
1249 }
1250}
1251
1252fn stretch(run: &PeriodRun, log: &[StepRec], t: f64) -> (PeriodRun, Vec<StepRec>) {
1255 if t == run.period {
1256 return (*run, log.to_vec());
1257 }
1258 let k = t / run.period;
1259 let stretched = PeriodRun {
1260 t0: run.t0,
1261 period: t,
1262 tstep: run.tstep * k,
1263 max_step: run.max_step * k,
1264 };
1265 let mut log: Vec<StepRec> = log
1266 .iter()
1267 .map(|r| StepRec {
1268 time: run.t0 + (r.time - run.t0) * k,
1269 ..*r
1270 })
1271 .collect();
1272 if let Some(end) = log.last_mut() {
1273 end.time = run.t0 + t;
1274 }
1275 (stretched, log)
1276}
1277
1278struct Shooter<'a> {
1279 circuit: &'a Circuit,
1280 opts: &'a Options,
1281 tight: Options,
1282 run: PeriodRun,
1283 kinds: &'a [VarKind],
1284 adaptive: usize,
1285 replays: usize,
1286 krylov: Vec<usize>,
1288 past: Option<Past>,
1292 ports: Vec<usize>,
1293 history: Vec<f64>,
1294 refresh: Vec<f64>,
1296}
1297
1298fn period_engine(
1301 circuit: &Circuit,
1302 opts: &Options,
1303 past: Option<&Past>,
1304 period: f64,
1305) -> Result<Engine, Error> {
1306 let mut e = Engine::new(circuit.build(opts)?);
1307 e.set_periodic_past(past.map(|p| p.at(period)));
1308 Ok(e)
1309}
1310
1311impl Shooter<'_> {
1312 fn engine(&self) -> Result<Engine, Error> {
1313 period_engine(self.circuit, self.opts, self.past.as_ref(), self.run.period)
1314 }
1315
1316 fn lines_note(&self) -> Option<String> {
1319 self.past.as_ref()?;
1320 let fmt = |v: &[f64]| -> String {
1321 if v.is_empty() {
1322 "-".to_string()
1323 } else {
1324 v.iter()
1325 .map(|r| format!("{r:.3e}"))
1326 .collect::<Vec<_>>()
1327 .join(", ")
1328 }
1329 };
1330 Some(format!(
1331 "pss: transmission lines read the orbit as their history (x(t − τ) = \
1332 x(t − τ + kT)): Picard on it, Newton on x0, on a frozen grid (the first \
1333 guess is the stabilization's last period); history mismatch per replay over {} \
1334 line port unknown(s): {}; grid refreshed once from the converged orbit, the \
1335 refresh run's period mismatch (the cross-grid discretization of the lines, not \
1336 iterated on): {}",
1337 self.ports.len(),
1338 fmt(&self.history),
1339 fmt(&self.refresh)
1340 ))
1341 }
1342
1343 fn adaptive(&mut self, x: &[f64]) -> Result<PeriodOut, Error> {
1345 self.adaptive += 1;
1346 let mut log = Vec::new();
1347 let rows = self
1348 .engine()?
1349 .tran_period(self.opts, &self.run, x, &mut log)?;
1350 Ok(self.measure(rows, log, x))
1351 }
1352
1353 fn measure(&self, rows: Vec<(f64, Vec<f64>)>, log: Vec<StepRec>, x: &[f64]) -> PeriodOut {
1356 let n = x.len();
1357 let last = &rows.last().expect("the period has its end point").1;
1358 let mut peak = vec![0.0f64; n];
1359 for (_, sol) in &rows {
1360 for i in 1..n {
1361 peak[i] = peak[i].max(sol[i].abs());
1362 }
1363 }
1364 let mut tol = vec![1.0; n];
1365 let mut resid = vec![0.0; n];
1366 let mut rnorm = 0.0f64;
1367 for i in 1..n {
1368 let floor = match self.kinds[i] {
1369 VarKind::Branch => self.opts.abstol,
1370 _ => self.opts.vntol,
1371 };
1372 tol[i] = self.opts.reltol * peak[i] + floor;
1373 resid[i] = last[i] - x[i];
1374 rnorm = rnorm.max(resid[i].abs() / tol[i]);
1375 }
1376 PeriodOut {
1377 rows,
1378 log,
1379 resid,
1380 tol,
1381 rnorm,
1382 hnorm: 0.0,
1383 period: self.run.period,
1384 }
1385 }
1386
1387 fn replay_on(&mut self, x: &[f64], grid: &Grid, cur: &PeriodOut) -> Result<PeriodOut, Error> {
1395 let (t0, max_step) = (self.run.t0, self.run.max_step);
1396 let past = Past::from_rows(&cur.rows, Some(&cur.log), t0, cur.period, max_step)
1397 .expect("a period run starts at t0");
1398 let (run, log) = grid.at(self.run.period);
1399 self.replays += 1;
1400 let rows = period_engine(self.circuit, self.opts, Some(&past), run.period)?
1401 .tran_replay_rows(self.opts, &run, x, &log)?;
1402 let mut r = self.measure(rows, log, x);
1403 r.hnorm = past.mismatch(&r.rows, x, t0, run.period, &self.ports, &r.tol);
1404 self.history.push(r.hnorm);
1405 self.past = Some(past);
1406 Ok(r)
1407 }
1408
1409 fn lines_excited(
1433 &mut self,
1434 x: &mut [f64],
1435 p: &PssParams,
1436 residuals: &mut Vec<f64>,
1437 ) -> Result<PeriodOut, Error> {
1438 let fail = |residuals: &[f64]| Error::PssConvergence {
1439 iterations: residuals.len(),
1440 residuals: residuals.to_vec(),
1441 period: None,
1442 };
1443 let mut seed = self.adaptive(x)?;
1444 residuals.push(seed.rnorm);
1445 for pass in 0..GRID_PASSES {
1446 let grid = Grid::of(self.run, &seed.log);
1447 let mut cur = seed;
1448 let done = loop {
1449 if residuals.len() >= p.max_iter {
1450 return Err(fail(residuals));
1451 }
1452 let r = self.replay_on(x, &grid, &cur)?;
1453 residuals.push(r.total());
1454 if r.total() <= p.steady_coeff {
1455 break r;
1456 }
1457 if r.rnorm > p.steady_coeff {
1458 let step = self.newton_step(x, &r)?;
1459 for i in 1..x.len() {
1460 x[i] += step[i];
1461 }
1462 }
1463 cur = r;
1464 };
1465 if pass + 1 == GRID_PASSES {
1466 return Ok(done);
1467 }
1468 self.past = Some(self.past_of(&done));
1469 seed = self.adaptive(x)?;
1470 residuals.push(seed.rnorm);
1471 self.refresh.push(seed.rnorm);
1472 }
1473 unreachable!("the last pass returns")
1474 }
1475
1476 #[allow(clippy::too_many_arguments)]
1483 fn lines_autonomous(
1484 &mut self,
1485 x: &mut [f64],
1486 period: &mut f64,
1487 osc: usize,
1488 p: &PssParams,
1489 run_for: &dyn Fn(f64) -> PeriodRun,
1490 residuals: &mut Vec<f64>,
1491 periods: &mut Vec<f64>,
1492 ) -> Result<(PeriodOut, f64), Error> {
1493 let fail = |residuals: &[f64], period: f64| Error::PssConvergence {
1494 iterations: residuals.len(),
1495 residuals: residuals.to_vec(),
1496 period: Some(period),
1497 };
1498 self.run = run_for(*period);
1499 let mut seed = self.adaptive(x)?;
1500 residuals.push(seed.rnorm);
1501 periods.push(*period);
1502 let mut dt = 0.0;
1503 let mut stepped = false;
1504 for pass in 0..GRID_PASSES {
1505 let grid = Grid::of(self.run, &seed.log);
1506 let mut cur = seed;
1507 let mut aitken = Aitken::default();
1508 let mut settle = false;
1510 let done = loop {
1511 if residuals.len() >= p.max_iter {
1512 return Err(fail(residuals, *period));
1513 }
1514 self.run = run_for(*period);
1515 let r = self.replay_on(x, &grid, &cur)?;
1516 residuals.push(r.total());
1517 periods.push(*period);
1518 if r.total() <= p.steady_coeff && stepped {
1519 break r;
1520 }
1521 if (r.rnorm > p.steady_coeff && !settle) || !stepped {
1522 let (mut sx, mut st) = self.newton_step_osc(x, *period, osc, &r)?;
1523 if st.abs() > MAX_DT * *period {
1524 let k = MAX_DT * *period / st.abs();
1525 sx.iter_mut().for_each(|v| *v *= k);
1526 st *= k;
1527 }
1528 for i in 1..x.len() {
1529 x[i] += sx[i];
1530 }
1531 *period += st;
1532 dt = st;
1533 stepped = true;
1534 }
1535 cur = r;
1536 settle = aitken.push(x, period, &mut cur, self.run.t0);
1537 };
1538 if pass + 1 == GRID_PASSES {
1539 return Ok((done, dt));
1540 }
1541 self.run = run_for(*period);
1542 self.past = Some(self.past_of(&done));
1543 seed = self.adaptive(x)?;
1544 residuals.push(seed.rnorm);
1545 periods.push(*period);
1546 self.refresh.push(seed.rnorm);
1547 }
1548 unreachable!("the last pass returns")
1549 }
1550
1551 fn past_of(&self, done: &PeriodOut) -> Past {
1553 Past::from_rows(
1554 &done.rows,
1555 Some(&done.log),
1556 self.run.t0,
1557 done.period,
1558 self.run.max_step,
1559 )
1560 .expect("a period run starts at t0")
1561 }
1562
1563 fn newton_step(&mut self, x: &[f64], at: &PeriodOut) -> Result<Vec<f64>, Error> {
1568 match self.newton_step_with(x, at, true) {
1569 Ok(s) => Ok(s),
1570 Err(Error::IterLim) => self.newton_step_with(x, at, false),
1571 Err(e) => Err(e),
1572 }
1573 }
1574
1575 fn newton_step_with(
1576 &mut self,
1577 x: &[f64],
1578 at: &PeriodOut,
1579 tight: bool,
1580 ) -> Result<Vec<f64>, Error> {
1581 let opts = if tight { &self.tight } else { self.opts };
1582 let (circuit, build_opts, run) = (self.circuit, self.opts, self.run);
1583 let past = self.past.as_ref();
1584 let n = x.len();
1585 let d = &at.tol;
1586 let replays = std::cell::Cell::new(0usize);
1587 let replay = |x0: &[f64]| -> Result<Vec<f64>, Error> {
1588 replays.set(replays.get() + 1);
1589 period_engine(circuit, build_opts, past, run.period)?
1590 .tran_replay(opts, &run, x0, &at.log)
1591 };
1592 let base = replay(x)?;
1593 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 = |v: &[f64]| -> Result<Vec<f64>, Error> {
1598 let mut xp = x.to_vec();
1599 for i in 1..n {
1600 xp[i] += FD_EPS * d[i] * v[i - 1];
1601 }
1602 let y = replay(&xp)?;
1603 Ok((1..n)
1604 .map(|i| v[i - 1] - (y[i] - base[i]) / (FD_EPS * d[i]))
1605 .collect())
1606 };
1607 let solved = gmres(&b, n - 1, lin_tol, &mut apply);
1608 self.replays += replays.get();
1609 let (ds, dim) = solved?;
1610 self.krylov.push(dim);
1611 let mut step = vec![0.0; n];
1612 for i in 1..n {
1613 step[i] = ds[i - 1] * d[i];
1614 }
1615 Ok(step)
1616 }
1617
1618 fn newton_step_osc(
1628 &mut self,
1629 x: &[f64],
1630 period: f64,
1631 osc: usize,
1632 at: &PeriodOut,
1633 ) -> Result<(Vec<f64>, f64), Error> {
1634 match self.newton_step_osc_with(x, period, osc, at, true) {
1635 Ok(s) => Ok(s),
1636 Err(Error::IterLim) => self.newton_step_osc_with(x, period, osc, at, false),
1637 Err(e) => Err(e),
1638 }
1639 }
1640
1641 fn newton_step_osc_with(
1642 &mut self,
1643 x: &[f64],
1644 period: f64,
1645 osc: usize,
1646 at: &PeriodOut,
1647 tight: bool,
1648 ) -> Result<(Vec<f64>, f64), Error> {
1649 let opts = if tight { &self.tight } else { self.opts };
1650 let (circuit, build_opts, run) = (self.circuit, self.opts, self.run);
1651 let n = x.len();
1652 let d = &at.tol;
1653 let s_t = self.opts.reltol * period;
1654 let slot = |i: usize| i - 1;
1659 let t_slot = osc - 1;
1660 let replays = std::cell::Cell::new(0usize);
1661 let past = self.past.as_ref();
1662 let replay = |x0: &[f64], t: f64| -> Result<Vec<f64>, Error> {
1663 replays.set(replays.get() + 1);
1664 let mut engine = period_engine(circuit, build_opts, past, t)?;
1667 if t == period {
1668 return engine.tran_replay(opts, &run, x0, &at.log);
1669 }
1670 let k = t / period;
1671 let stretched = PeriodRun {
1672 t0: run.t0,
1673 period: t,
1674 tstep: run.tstep * k,
1675 max_step: run.max_step * k,
1676 };
1677 let mut log: Vec<StepRec> = at
1678 .log
1679 .iter()
1680 .map(|r| StepRec {
1681 time: run.t0 + (r.time - run.t0) * k,
1682 ..*r
1683 })
1684 .collect();
1685 if let Some(end) = log.last_mut() {
1686 end.time = run.t0 + t;
1687 }
1688 engine.tran_replay(opts, &stretched, x0, &log)
1689 };
1690 let base = replay(x, period)?;
1691 let b: Vec<f64> = (1..n).map(|i| at.resid[i] / d[i]).collect();
1692 let bnorm = norm(&b);
1693 let lin_tol = f64::min(1e-2, 1e-4 * bnorm);
1694 let mut apply = |u: &[f64]| -> Result<Vec<f64>, Error> {
1695 let mut xp = x.to_vec();
1696 for i in 1..n {
1697 if i != osc {
1698 xp[i] += FD_EPS * d[i] * u[slot(i)];
1699 }
1700 }
1701 let y = replay(&xp, period + FD_EPS * s_t * u[t_slot])?;
1702 Ok((1..n)
1703 .map(|i| {
1704 let ui = if i == osc { 0.0 } else { u[slot(i)] };
1705 ui - (y[i] - base[i]) / (FD_EPS * d[i])
1706 })
1707 .collect())
1708 };
1709 let solved = gmres(&b, n - 1, lin_tol, &mut apply);
1710 self.replays += replays.get();
1711 let (us, dim) = solved?;
1712 self.krylov.push(dim);
1713 let mut step = vec![0.0; n];
1714 for i in 1..n {
1715 if i != osc {
1716 step[i] = us[slot(i)] * d[i];
1717 }
1718 }
1719 Ok((step, us[t_slot] * s_t))
1720 }
1721
1722 fn summary(&self, residuals: &[f64]) -> String {
1723 let hist: Vec<String> = residuals.iter().map(|r| format!("{r:.3e}")).collect();
1724 format!(
1725 "pss: {} shooting iteration(s), residual/tolerance per iteration: {}; \
1726 {} adaptive and {} replayed period transients (GMRES dimensions {:?})",
1727 residuals.len(),
1728 hist.join(", "),
1729 self.adaptive,
1730 self.replays,
1731 self.krylov
1732 )
1733 }
1734}
1735
1736type LinearMap<'a> = dyn FnMut(&[f64]) -> Result<Vec<f64>, Error> + 'a;
1738
1739fn norm(v: &[f64]) -> f64 {
1740 v.iter().map(|x| x * x).sum::<f64>().sqrt()
1741}
1742
1743fn gmres(b: &[f64], m: usize, tol: f64, apply: &mut LinearMap) -> Result<(Vec<f64>, usize), Error> {
1748 let n = b.len();
1749 let beta = norm(b);
1750 if beta == 0.0 || m == 0 {
1751 return Ok((vec![0.0; n], 0));
1752 }
1753 let mut v: Vec<Vec<f64>> = vec![b.iter().map(|x| x / beta).collect()];
1754 let mut h: Vec<Vec<f64>> = Vec::new(); let mut cs: Vec<f64> = Vec::new();
1756 let mut sn: Vec<f64> = Vec::new();
1757 let mut g = vec![beta];
1758 let mut k = 0;
1759 while k < m {
1760 let mut w = apply(&v[k])?;
1761 let mut col = vec![0.0; k + 2];
1762 for _pass in 0..2 {
1763 for (i, vi) in v.iter().enumerate() {
1764 let c: f64 = w.iter().zip(vi).map(|(a, b)| a * b).sum();
1765 col[i] += c;
1766 for (wj, vj) in w.iter_mut().zip(vi) {
1767 *wj -= c * vj;
1768 }
1769 }
1770 }
1771 let hn = norm(&w);
1772 col[k + 1] = hn;
1773 for i in 0..k {
1774 let t = cs[i] * col[i] + sn[i] * col[i + 1];
1775 col[i + 1] = -sn[i] * col[i] + cs[i] * col[i + 1];
1776 col[i] = t;
1777 }
1778 let r = col[k].hypot(col[k + 1]);
1779 let (c, s) = if r == 0.0 {
1780 (1.0, 0.0)
1781 } else {
1782 (col[k] / r, col[k + 1] / r)
1783 };
1784 col[k] = r;
1785 col[k + 1] = 0.0;
1786 cs.push(c);
1787 sn.push(s);
1788 g.push(-s * g[k]);
1789 g[k] *= c;
1790 h.push(col);
1791 k += 1;
1792 let breakdown = hn <= 1e-14 * beta;
1793 if g[k].abs() <= tol || breakdown {
1794 break;
1795 }
1796 v.push(w.iter().map(|x| x / hn).collect());
1797 }
1798 let mut y = vec![0.0; k];
1800 for i in (0..k).rev() {
1801 let mut s = g[i];
1802 for j in i + 1..k {
1803 s -= h[j][i] * y[j];
1804 }
1805 y[i] = if h[i][i] != 0.0 { s / h[i][i] } else { 0.0 };
1806 }
1807 let mut x = vec![0.0; n];
1808 for (j, yj) in y.iter().enumerate() {
1809 for (xi, vi) in x.iter_mut().zip(&v[j]) {
1810 *xi += yj * vi;
1811 }
1812 }
1813 Ok((x, k))
1814}
1815
1816fn outputs(
1818 per: &PeriodOut,
1819 names: &[String],
1820 kinds: &[VarKind],
1821 p: &PssParams,
1822 t0: f64,
1823 period: f64,
1824) -> (Solution, Solution) {
1825 let n_pts = p.points;
1826 let times: Vec<f64> = (0..=n_pts)
1827 .map(|k| t0 + period * k as f64 / n_pts as f64)
1828 .collect();
1829 let rows = &per.rows;
1832 let mut breaks = vec![0usize];
1833 breaks.extend((1..rows.len()).filter(|&j| per.log[j - 1].brk));
1834 if *breaks.last().unwrap_or(&0) != rows.len() - 1 {
1835 breaks.push(rows.len() - 1);
1836 }
1837
1838 let mut variables = vec![("time".to_string(), VarType::Time)];
1839 let mut sel: Vec<usize> = Vec::new();
1840 for i in 1..names.len() {
1841 match kinds[i] {
1842 VarKind::Node if saved_node(&names[i]) => {
1843 variables.push((format!("v({})", names[i]), VarType::Voltage));
1844 sel.push(i);
1845 }
1846 VarKind::Branch => {
1847 variables.push((format!("i({})", names[i]), VarType::Current));
1848 sel.push(i);
1849 }
1850 _ => {}
1851 }
1852 }
1853 let mut cols: Vec<Vec<f64>> = vec![times.clone()];
1854 cols.extend(sel.iter().map(|_| Vec::with_capacity(times.len())));
1855 for (k, &t) in times.iter().enumerate() {
1856 let vals: Vec<f64> = if k == n_pts {
1857 rows[rows.len() - 1].1.clone()
1858 } else if k == 0 {
1859 rows[0].1.clone()
1860 } else {
1861 interp(rows, &breaks, t)
1862 };
1863 for (c, &i) in sel.iter().enumerate() {
1864 cols[c + 1].push(vals[i]);
1865 }
1866 }
1867
1868 let h = p.harmonics;
1870 let nn = n_pts as f64;
1871 let mut fvars = vec![("frequency".to_string(), VarType::Frequency)];
1872 fvars.extend(variables[1..].iter().cloned());
1873 let mut fcols: Vec<Vec<Complex64>> = vec![
1874 (0..h)
1875 .map(|k| Complex64::new(k as f64 / period, 0.0))
1876 .collect(),
1877 ];
1878 for col in &cols[1..] {
1879 let samples = &col[..n_pts];
1880 let mut out = Vec::with_capacity(h);
1881 for k in 0..h {
1882 let mut acc = Complex64::new(0.0, 0.0);
1883 for (m, &s) in samples.iter().enumerate() {
1884 let km = (k * m) % n_pts;
1886 let ang = -2.0 * std::f64::consts::PI * km as f64 / nn;
1887 acc += s * Complex64::new(ang.cos(), ang.sin());
1888 }
1889 out.push(if k == 0 { acc / nn } else { acc * (2.0 / nn) });
1890 }
1891 fcols.push(out);
1892 }
1893 (
1894 Solution {
1895 plotname: PSS_TIME_PLOT.to_string(),
1896 variables,
1897 data: Data::Real(cols),
1898 scale: Some(0),
1899 },
1900 Solution {
1901 plotname: PSS_FREQ_PLOT.to_string(),
1902 variables: fvars,
1903 data: Data::Complex(fcols),
1904 scale: Some(0),
1905 },
1906 )
1907}
1908
1909fn interp(rows: &[(f64, Vec<f64>)], breaks: &[usize], t: f64) -> Vec<f64> {
1914 let j = rows.partition_point(|r| r.0 <= t).clamp(1, rows.len() - 1) - 1;
1916 let b = breaks.partition_point(|&b| b <= j);
1917 let lo = breaks[b - 1];
1918 let hi = breaks.get(b).copied().unwrap_or(rows.len() - 1).max(j + 1);
1919 let want = 4usize.min(hi - lo + 1);
1920 let mut a = j.saturating_sub(1).max(lo);
1921 if a + want - 1 > hi {
1922 a = hi + 1 - want;
1923 }
1924 let n = rows[0].1.len();
1925 let mut out = vec![0.0; n];
1926 for p in a..a + want {
1927 let mut w = 1.0;
1928 for q in a..a + want {
1929 if q != p {
1930 w *= (t - rows[q].0) / (rows[p].0 - rows[q].0);
1931 }
1932 }
1933 for (o, v) in out.iter_mut().zip(&rows[p].1) {
1934 *o += w * v;
1935 }
1936 }
1937 out
1938}
1939
1940#[cfg(test)]
1941mod tests {
1942 use super::*;
1943
1944 #[test]
1945 fn harvest_takes_the_common_fundamental() {
1946 let s = |f: &[f64]| -> Vec<(String, f64)> { f.iter().map(|&f| ("v".into(), f)).collect() };
1947 assert!((harvest(&s(&[1e3])).unwrap() - 1e3).abs() < 1e-9);
1948 assert!((harvest(&s(&[1e3, 3e3])).unwrap() - 1e3).abs() < 1e-6);
1949 assert!((harvest(&s(&[2e3, 3e3])).unwrap() - 1e3).abs() < 1e-6);
1950 assert!((harvest(&s(&[50e3, 1.0 / 20e-6])).unwrap() - 50e3).abs() < 1e-3);
1951 assert!(harvest(&s(&[1.0, std::f64::consts::PI])).is_err());
1952 assert!(harvest(&[]).is_err());
1953 }
1954
1955 #[test]
1956 fn gmres_solves_a_small_system() {
1957 let u = [0.5, -0.2, 0.1];
1959 let w = [0.3, 0.4, -0.6];
1960 let mut apply = |v: &[f64]| -> Result<Vec<f64>, Error> {
1961 let d: f64 = w.iter().zip(v).map(|(a, b)| a * b).sum();
1962 Ok(v.iter().zip(&u).map(|(vi, ui)| vi - ui * d).collect())
1963 };
1964 let b = [1.0, 2.0, 3.0];
1965 let (y, k) = gmres(&b, 3, 1e-13, &mut apply).unwrap();
1966 assert!(k <= 2);
1967 let ay = apply(&y).unwrap();
1968 for (a, b) in ay.iter().zip(&b) {
1969 assert!((a - b).abs() < 1e-12);
1970 }
1971 }
1972}