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